<commit_before>package release

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"net/url"
	"os/exec"
	"path"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/golang/glog"
)

// git is a wrapper to invoke git safely, similar to
// github.com/openshift/library-go/pkg/git but giving access to lower level
// calls. Consider improving pkg/git in the future.
type git struct {
	path string
}

var noSuchRepo = errors.New("location is not a git repo")

func (g *git) exec(command ...string) (string, error) {
	buf := &bytes.Buffer{}
	bufErr := &bytes.Buffer{}
	cmd := exec.Command("git", append([]string{"-C", g.path}, command...)...)
	glog.V(5).Infof("Executing git: %v\n", cmd.Args)
	cmd.Stdout = buf
	cmd.Stderr = bufErr
	err := cmd.Run()
	if err != nil {
		return bufErr.String(), err
	}
	return buf.String(), nil
}

func (g *git) streamExec(out, errOut io.Writer, command ...string) error {
	cmd := exec.Command("git", append([]string{"--git-dir", filepath.Join(g.path, ".git")}, command...)...)
	cmd.Stdout = out
	cmd.Stderr = errOut
	return cmd.Run()
}

func (g *git) ChangeContext(path string) (*git, error) {
	location := &git{path: path}
	if errOut, err := location.exec("rev-parse", "--git-dir"); err != nil {
		if strings.Contains(errOut, "not a git repository") {
			return location, noSuchRepo
		}
		return location, err
	}
	return location, nil
}

func (g *git) Clone(repository string, out, errOut io.Writer) error {
	return (&git{}).streamExec(out, errOut, "clone", repository, g.path)
}

func (g *git) parent() *git {
	return &git{path: filepath.Dir(g.path)}
}

func (g *git) basename() string {
	return filepath.Base(g.path)
}

func (g *git) CheckoutCommit(repo, commit string) error {
	_, err := g.exec("checkout", commit)
	if err == nil {
		return nil
	}

	// try to fetch by URL
	if _, err := g.exec("fetch", repo); err == nil {
		if _, err := g.exec("checkout", commit); err == nil {
			return nil
		}
	}

	// TODO: what if that transport URL does not exist?

	return fmt.Errorf("could not locate commit %s", commit)
}

var reMatch = regexp.MustCompile(`^([a-zA-Z0-9\-\_]+)@([^:]+):(.+)$`)

func sourceLocationAsURL(location string) (*url.URL, error) {
	if matches := reMatch.FindStringSubmatch(location); matches != nil {
		return &url.URL{Scheme: "git", User: url.UserPassword(matches[1], ""), Host: matches[2], Path: matches[3]}, nil
	}
	return url.Parse(location)
}

func sourceLocationAsRelativePath(dir, location string) (string, error) {
	u, err := sourceLocationAsURL(location)
	if err != nil {
		return "", err
	}
	gitPath := u.Path
	if strings.HasSuffix(gitPath, ".git") {
		gitPath = strings.TrimSuffix(gitPath, ".git")
	}
	gitPath = path.Clean(gitPath)
	basePath := filepath.Join(dir, u.Host, filepath.FromSlash(gitPath))
	return basePath, nil
}

type MergeCommit struct {
	CommitDate time.Time

	Commit        string
	ParentCommits []string

	PullRequest int
	Bug         int

	Subject string
}

func gitOutputToError(err error, out string) error {
	out = strings.TrimSpace(out)
	if strings.HasPrefix(out, "fatal: ") {
		out = strings.TrimPrefix(out, "fatal: ")
	}
	if len(out) == 0 {
		return err
	}
	return fmt.Errorf(out)
}

func mergeLogForRepo(g *git, from, to string) ([]MergeCommit, error) {
	if from == to {
		return nil, nil
	}

	rePR, err := regexp.Compile(`^Merge pull request #(\d+) from`)
	if err != nil {
		return nil, err
	}
	reBug, err := regexp.Compile(`^Bug (\d+)\s*(-|:)\s*`)
	if err != nil {
		return nil, err
	}

	args := []string{"log", "--merges", "--topo-order", "-z", "--pretty=format:%H %P%x1E%ct%x1E%s%x1E%b", "--reverse", fmt.Sprintf("%s..%s", from, to)}
	out, err := g.exec(args...)
	if err != nil {
		// retry once if there's a chance we haven't fetched the latest commits
		if !strings.Contains(out, "Invalid revision range") {
			return nil, gitOutputToError(err, out)
		}
		if _, err := g.exec("fetch", "--all"); err != nil {
			return nil, gitOutputToError(err, out)
		}
		if _, err := g.exec("cat-file", "-e", from+"^{commit}"); err != nil {
			return nil, fmt.Errorf("from commit %s does not exist", from)
		}
		if _, err := g.exec("cat-file", "-e", to+"^{commit}"); err != nil {
			return nil, fmt.Errorf("to commit %s does not exist", to)
		}
		out, err = g.exec(args...)
		if err != nil {
			return nil, gitOutputToError(err, out)
		}
	}

	if glog.V(5) {
		glog.Infof("Got commit info:\n%s", strconv.Quote(out))
	}

	var commits []MergeCommit
	for _, entry := range strings.Split(out, "\x00") {
		records := strings.Split(entry, "\x1e")
		if len(records) != 4 {
			return nil, fmt.Errorf("unexpected git log output width %d columns", len(records))
		}
		unixTS, err := strconv.ParseInt(records[1], 10, 64)
		if err != nil {
			return nil, fmt.Errorf("unexpected timestamp: %v", err)
		}
		commitValues := strings.Split(records[0], " ")

		mergeCommit := MergeCommit{
			CommitDate:    time.Unix(unixTS, 0).UTC(),
			Commit:        commitValues[0],
			ParentCommits: commitValues[1:],
		}

		msg := records[3]
		if m := reBug.FindStringSubmatch(msg); m != nil {
			mergeCommit.Subject = msg[len(m[0]):]
			mergeCommit.Bug, err = strconv.Atoi(m[1])
			if err != nil {
				return nil, fmt.Errorf("could not extract bug number from %q: %v", msg, err)
			}
		} else {
			mergeCommit.Subject = msg
		}
		mergeCommit.Subject = strings.TrimSpace(mergeCommit.Subject)
		mergeCommit.Subject = strings.SplitN(mergeCommit.Subject, "\n", 2)[0]

		mergeMsg := records[2]
		if m := rePR.FindStringSubmatch(mergeMsg); m != nil {
			mergeCommit.PullRequest, err = strconv.Atoi(m[1])
			if err != nil {
				return nil, fmt.Errorf("could not extract PR number from %q: %v", mergeMsg, err)
			}
		} else {
			glog.V(2).Infof("Omitted commit %s which has no pull-request", mergeCommit.Commit)
			continue
		}
		if len(mergeCommit.Subject) == 0 {
			mergeCommit.Subject = "Merge"
		}

		commits = append(commits, mergeCommit)
	}

	return commits, nil
}
<commit_msg>If the git log is empty don't error out from `release info --changelog`<commit_after>package release

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"net/url"
	"os/exec"
	"path"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/golang/glog"
)

// git is a wrapper to invoke git safely, similar to
// github.com/openshift/library-go/pkg/git but giving access to lower level
// calls. Consider improving pkg/git in the future.
type git struct {
	path string
}

var noSuchRepo = errors.New("location is not a git repo")

func (g *git) exec(command ...string) (string, error) {
	buf := &bytes.Buffer{}
	bufErr := &bytes.Buffer{}
	cmd := exec.Command("git", append([]string{"-C", g.path}, command...)...)
	glog.V(5).Infof("Executing git: %v\n", cmd.Args)
	cmd.Stdout = buf
	cmd.Stderr = bufErr
	err := cmd.Run()
	if err != nil {
		return bufErr.String(), err
	}
	return buf.String(), nil
}

func (g *git) streamExec(out, errOut io.Writer, command ...string) error {
	cmd := exec.Command("git", append([]string{"--git-dir", filepath.Join(g.path, ".git")}, command...)...)
	cmd.Stdout = out
	cmd.Stderr = errOut
	return cmd.Run()
}

func (g *git) ChangeContext(path string) (*git, error) {
	location := &git{path: path}
	if errOut, err := location.exec("rev-parse", "--git-dir"); err != nil {
		if strings.Contains(errOut, "not a git repository") {
			return location, noSuchRepo
		}
		return location, err
	}
	return location, nil
}

func (g *git) Clone(repository string, out, errOut io.Writer) error {
	return (&git{}).streamExec(out, errOut, "clone", repository, g.path)
}

func (g *git) parent() *git {
	return &git{path: filepath.Dir(g.path)}
}

func (g *git) basename() string {
	return filepath.Base(g.path)
}

func (g *git) CheckoutCommit(repo, commit string) error {
	_, err := g.exec("checkout", commit)
	if err == nil {
		return nil
	}

	// try to fetch by URL
	if _, err := g.exec("fetch", repo); err == nil {
		if _, err := g.exec("checkout", commit); err == nil {
			return nil
		}
	}

	// TODO: what if that transport URL does not exist?

	return fmt.Errorf("could not locate commit %s", commit)
}

var reMatch = regexp.MustCompile(`^([a-zA-Z0-9\-\_]+)@([^:]+):(.+)$`)

func sourceLocationAsURL(location string) (*url.URL, error) {
	if matches := reMatch.FindStringSubmatch(location); matches != nil {
		return &url.URL{Scheme: "git", User: url.UserPassword(matches[1], ""), Host: matches[2], Path: matches[3]}, nil
	}
	return url.Parse(location)
}

func sourceLocationAsRelativePath(dir, location string) (string, error) {
	u, err := sourceLocationAsURL(location)
	if err != nil {
		return "", err
	}
	gitPath := u.Path
	if strings.HasSuffix(gitPath, ".git") {
		gitPath = strings.TrimSuffix(gitPath, ".git")
	}
	gitPath = path.Clean(gitPath)
	basePath := filepath.Join(dir, u.Host, filepath.FromSlash(gitPath))
	return basePath, nil
}

type MergeCommit struct {
	CommitDate time.Time

	Commit        string
	ParentCommits []string

	PullRequest int
	Bug         int

	Subject string
}

func gitOutputToError(err error, out string) error {
	out = strings.TrimSpace(out)
	if strings.HasPrefix(out, "fatal: ") {
		out = strings.TrimPrefix(out, "fatal: ")
	}
	if len(out) == 0 {
		return err
	}
	return fmt.Errorf(out)
}

func mergeLogForRepo(g *git, from, to string) ([]MergeCommit, error) {
	if from == to {
		return nil, nil
	}

	rePR, err := regexp.Compile(`^Merge pull request #(\d+) from`)
	if err != nil {
		return nil, err
	}
	reBug, err := regexp.Compile(`^Bug (\d+)\s*(-|:)\s*`)
	if err != nil {
		return nil, err
	}

	args := []string{"log", "--merges", "--topo-order", "-z", "--pretty=format:%H %P%x1E%ct%x1E%s%x1E%b", "--reverse", fmt.Sprintf("%s..%s", from, to)}
	out, err := g.exec(args...)
	if err != nil {
		// retry once if there's a chance we haven't fetched the latest commits
		if !strings.Contains(out, "Invalid revision range") {
			return nil, gitOutputToError(err, out)
		}
		if _, err := g.exec("fetch", "--all"); err != nil {
			return nil, gitOutputToError(err, out)
		}
		if _, err := g.exec("cat-file", "-e", from+"^{commit}"); err != nil {
			return nil, fmt.Errorf("from commit %s does not exist", from)
		}
		if _, err := g.exec("cat-file", "-e", to+"^{commit}"); err != nil {
			return nil, fmt.Errorf("to commit %s does not exist", to)
		}
		out, err = g.exec(args...)
		if err != nil {
			return nil, gitOutputToError(err, out)
		}
	}

	if glog.V(5) {
		glog.Infof("Got commit info:\n%s", strconv.Quote(out))
	}

	var commits []MergeCommit
	if len(out) == 0 {
		return nil, nil
	}
	for _, entry := range strings.Split(out, "\x00") {
		records := strings.Split(entry, "\x1e")
		if len(records) != 4 {
			return nil, fmt.Errorf("unexpected git log output width %d columns", len(records))
		}
		unixTS, err := strconv.ParseInt(records[1], 10, 64)
		if err != nil {
			return nil, fmt.Errorf("unexpected timestamp: %v", err)
		}
		commitValues := strings.Split(records[0], " ")

		mergeCommit := MergeCommit{
			CommitDate:    time.Unix(unixTS, 0).UTC(),
			Commit:        commitValues[0],
			ParentCommits: commitValues[1:],
		}

		msg := records[3]
		if m := reBug.FindStringSubmatch(msg); m != nil {
			mergeCommit.Subject = msg[len(m[0]):]
			mergeCommit.Bug, err = strconv.Atoi(m[1])
			if err != nil {
				return nil, fmt.Errorf("could not extract bug number from %q: %v", msg, err)
			}
		} else {
			mergeCommit.Subject = msg
		}
		mergeCommit.Subject = strings.TrimSpace(mergeCommit.Subject)
		mergeCommit.Subject = strings.SplitN(mergeCommit.Subject, "\n", 2)[0]

		mergeMsg := records[2]
		if m := rePR.FindStringSubmatch(mergeMsg); m != nil {
			mergeCommit.PullRequest, err = strconv.Atoi(m[1])
			if err != nil {
				return nil, fmt.Errorf("could not extract PR number from %q: %v", mergeMsg, err)
			}
		} else {
			glog.V(2).Infof("Omitted commit %s which has no pull-request", mergeCommit.Commit)
			continue
		}
		if len(mergeCommit.Subject) == 0 {
			mergeCommit.Subject = "Merge"
		}

		commits = append(commits, mergeCommit)
	}

	return commits, nil
}
<|endoftext|>


<commit_before>package cli

const (
	bashCompletionFunc = `# call oc get $1,
__oc_override_flag_list=(config cluster user context namespace server)
__oc_override_flags()
{
    local ${__oc_override_flag_list[*]} two_word_of of
    for w in "${words[@]}"; do
        if [ -n "${two_word_of}" ]; then
            eval "${two_word_of}=\"--${two_word_of}=\${w}\""
            two_word_of=
            continue
        fi
        for of in "${__oc_override_flag_list[@]}"; do
            case "${w}" in
                --${of}=*)
                    eval "${of}=\"${w}\""
                    ;;
                --${of})
                    two_word_of="${of}"
                    ;;
            esac
        done
    done
    for of in "${__oc_override_flag_list[@]}"; do
        if eval "test -n \"\$${of}\""; then
            eval "echo \${${of}}"
        fi
    done
}
__oc_parse_get()
{

    local template
    template="{{ range .items  }}{{ .metadata.name }} {{ end }}"
    local oc_out
    if oc_out=$(oc get $(__oc_override_flags) -o template --template="${template}" "$1" 2>/dev/null); then
        COMPREPLY=( $( compgen -W "${oc_out[*]}" -- "$cur" ) )
    fi
}

__oc_get_namespaces()
{
    local template oc_out
    template="{{ range .items  }}{{ .metadata.name }} {{ end }}"
    if oc_out=$(oc get -o template --template="${template}" namespace 2>/dev/null); then
        COMPREPLY=( $( compgen -W "${oc_out[*]}" -- "$cur" ) )
    fi
}

__oc_get_resource()
{
    if [[ ${#nouns[@]} -eq 0 ]]; then
      local oc_out
      if oc_out=$(oc api-resources $(__oc_override_flags) -o name --cached --request-timeout=5s --verbs=get 2>/dev/null); then
          COMPREPLY=( $( compgen -W "${oc_out[*]}" -- "$cur" ) )
          return 0
      fi
      return 1
    fi
    __oc_parse_get "${nouns[${#nouns[@]} -1]}"
}

# $1 is the name of the pod we want to get the list of containers inside
__oc_get_containers()
{
    local template
    template="{{ range .spec.containers  }}{{ .name }} {{ end }}"
    __oc_debug "${FUNCNAME} nouns are ${nouns[@]}"

    local len="${#nouns[@]}"
    if [[ ${len} -ne 1 ]]; then
        return
    fi
    local last=${nouns[${len} -1]}
    local oc_out
    if oc_out=$(oc get -o template --template="${template}" pods "${last}" 2>/dev/null); then
        COMPREPLY=( $( compgen -W "${oc_out[*]}" -- "$cur" ) )
    fi
}

# Require both a pod and a container to be specified
__oc_require_pod_and_container()
{
    if [[ ${#nouns[@]} -eq 0 ]]; then
        __oc_parse_get pods
        return 0
    fi;
    __oc_get_containers
    return 0
}

__custom_func() {
    case ${last_command} in

        # first arg is the kind according to ValidArgs, second is resource name
        oc_get | oc_describe | oc_delete | oc_label | oc_expose | oc_export | oc_patch | oc_annotate | oc_env | oc_edit | oc_volume | oc_scale | oc_observe )
            __oc_get_resource
            return
            ;;

        # first arg is a pod name
        oc_rsh | oc_exec)
            if [[ ${#nouns[@]} -eq 0 ]]; then
                __oc_parse_get pods
            fi;
            return
            ;;

        # first arg is a pod name, second is a container name
        oc_logs | oc_attach)
            __oc_require_pod_and_container
            return
            ;;

        # args other than the first are filenames
        oc_secrets_new)
            # Complete args other than the first as filenames
            if [[ ${#nouns[@]} -gt 0 ]]; then
                _filedir
            fi;
            return
            ;;

        # first arg is a build config name
        oc_start-build | oc_cancel-build)
            if [[ ${#nouns[@]} -eq 0 ]]; then
                __oc_parse_get buildconfigs
            fi;
            return
            ;;

        # first arg is a deployment config
        oc_deploy)
            if [[ ${#nouns[@]} -eq 0 ]]; then
                __oc_parse_get deploymentconfigs
            fi;
            return
            ;;

        # first arg is a deployment config OR deployment
        oc_rollback)
            if [[ ${#nouns[@]} -eq 0 ]]; then
                __oc_parse_get deploymentconfigs,replicationcontrollers
            fi;
            return
            ;;

        # first arg is a project name
        oc_project)
            if [[ ${#nouns[@]} -eq 0 ]]; then
                __oc_parse_get projects
            fi;
            return
            ;;

        # first arg is an image stream
        oc_import-image)
            if [[ ${#nouns[@]} -eq 0 ]]; then
                __oc_parse_get imagestreams
            fi;
            return
            ;;

        *)
            ;;
    esac
}
`
)
<commit_msg>Update custom func in completion<commit_after>package cli

const (
	bashCompletionFunc = `# call oc get $1,
__oc_override_flag_list=(config cluster user context namespace server)
__oc_override_flags()
{
    local ${__oc_override_flag_list[*]} two_word_of of
    for w in "${words[@]}"; do
        if [ -n "${two_word_of}" ]; then
            eval "${two_word_of}=\"--${two_word_of}=\${w}\""
            two_word_of=
            continue
        fi
        for of in "${__oc_override_flag_list[@]}"; do
            case "${w}" in
                --${of}=*)
                    eval "${of}=\"${w}\""
                    ;;
                --${of})
                    two_word_of="${of}"
                    ;;
            esac
        done
    done
    for of in "${__oc_override_flag_list[@]}"; do
        if eval "test -n \"\$${of}\""; then
            eval "echo \${${of}}"
        fi
    done
}
__oc_parse_get()
{

    local template
    template="{{ range .items  }}{{ .metadata.name }} {{ end }}"
    local oc_out
    if oc_out=$(oc get $(__oc_override_flags) -o template --template="${template}" "$1" 2>/dev/null); then
        COMPREPLY=( $( compgen -W "${oc_out[*]}" -- "$cur" ) )
    fi
}

__oc_get_namespaces()
{
    local template oc_out
    template="{{ range .items  }}{{ .metadata.name }} {{ end }}"
    if oc_out=$(oc get -o template --template="${template}" namespace 2>/dev/null); then
        COMPREPLY=( $( compgen -W "${oc_out[*]}" -- "$cur" ) )
    fi
}

__oc_get_resource()
{
    if [[ ${#nouns[@]} -eq 0 ]]; then
      local oc_out
      if oc_out=$(oc api-resources $(__oc_override_flags) -o name --cached --request-timeout=5s --verbs=get 2>/dev/null); then
          COMPREPLY=( $( compgen -W "${oc_out[*]}" -- "$cur" ) )
          return 0
      fi
      return 1
    fi
    __oc_parse_get "${nouns[${#nouns[@]} -1]}"
}

# $1 is the name of the pod we want to get the list of containers inside
__oc_get_containers()
{
    local template
    template="{{ range .spec.containers  }}{{ .name }} {{ end }}"
    __oc_debug "${FUNCNAME} nouns are ${nouns[@]}"

    local len="${#nouns[@]}"
    if [[ ${len} -ne 1 ]]; then
        return
    fi
    local last=${nouns[${len} -1]}
    local oc_out
    if oc_out=$(oc get -o template --template="${template}" pods "${last}" 2>/dev/null); then
        COMPREPLY=( $( compgen -W "${oc_out[*]}" -- "$cur" ) )
    fi
}

# Require both a pod and a container to be specified
__oc_require_pod_and_container()
{
    if [[ ${#nouns[@]} -eq 0 ]]; then
        __oc_parse_get pods
        return 0
    fi;
    __oc_get_containers
    return 0
}

__custom_func() {
    case ${last_command} in

        # first arg is the kind according to ValidArgs, second is resource name
        oc_get | oc_describe | oc_delete | oc_label | oc_expose | oc_export | oc_patch | oc_annotate | oc_edit | oc_scale | oc_autoscale | oc_observe )
            __oc_get_resource
            return
            ;;

        # first arg is a pod name
        oc_rsh | oc_exec | oc_port-forward | oc_attach)
            if [[ ${#nouns[@]} -eq 0 ]]; then
                __oc_parse_get pods
            fi;
            return
            ;;

        # first arg is a pod name, second is a container name
        oc_logs)
            __oc_require_pod_and_container
            return
            ;;

        # first arg is a build config name
        oc_start-build | oc_cancel-build)
            if [[ ${#nouns[@]} -eq 0 ]]; then
                __oc_parse_get buildconfigs
            fi;
            return
            ;;

        # first arg is a deployment config OR deployment
        oc_rollback)
            if [[ ${#nouns[@]} -eq 0 ]]; then
                __oc_parse_get deploymentconfigs,replicationcontrollers
            fi;
            return
            ;;

        # first arg is a project name
        oc_project)
            if [[ ${#nouns[@]} -eq 0 ]]; then
                __oc_parse_get projects
            fi;
            return
            ;;

        # first arg is an image stream
        oc_import-image)
            if [[ ${#nouns[@]} -eq 0 ]]; then
                __oc_parse_get imagestreams
            fi;
            return
            ;;

        *)
            ;;
    esac
}
`
)
<|endoftext|>


<commit_before>package etcd

import (
	"context"
	"fmt"
	"reflect"
	"sort"
	"strings"

	"github.com/Aptomi/aptomi/pkg/runtime"
	"github.com/Aptomi/aptomi/pkg/runtime/store"
	etcd "github.com/coreos/etcd/clientv3"
	etcdconc "github.com/coreos/etcd/clientv3/concurrency"
	"github.com/coreos/etcd/clientv3/namespace"
	"github.com/davecgh/go-spew/spew"
)

type etcdStore struct {
	client *etcd.Client
	types  *runtime.Types
	codec  store.Codec
}

func New(cfg Config, types *runtime.Types, codec store.Codec) (store.Interface, error) {
	if len(cfg.Endpoints) == 0 {
		cfg.Endpoints = []string{"localhost:2379"}
	}

	client, err := etcd.New(etcd.Config{
		Endpoints:            cfg.Endpoints,
		DialTimeout:          dialTimeout,
		DialKeepAliveTime:    keepaliveTime,
		DialKeepAliveTimeout: keepaliveTimeout,
	})
	if err != nil {
		return nil, fmt.Errorf("error while connecting to etcd: %s", err)
	}

	cfg.Prefix = strings.Trim(cfg.Prefix, "/")
	if cfg.Prefix != "" {
		cfg.Prefix = "/" + cfg.Prefix
		client.KV = namespace.NewKV(client.KV, cfg.Prefix)
		client.Lease = namespace.NewLease(client.Lease, cfg.Prefix)
		client.Watcher = namespace.NewWatcher(client.Watcher, cfg.Prefix)
	}

	// todo run compactor?

	return &etcdStore{
		client: client,
		types:  types,
		codec:  codec,
	}, nil
}

func (s *etcdStore) Close() error {
	return s.client.Close()
}

// todo need to rework keys to not include kind or to start with kind at least???

// Save saves Storable object with specified options into Etcd and updates indexes when appropriate.
// Workflow:
// 1. for non-versioned object key is always static, just put object into etcd and no indexes need to be updated (only
//    generation indexes currently exists)
// 2. for versioned object all manipulations are done inside a single transaction to guarantee atomic operations
//    (like index update, getting last existing generation or comparing with existing object), in addition to that
//    generation set for the object is always ignored if "forceGenOrReplace" option isn't used
// 3. if "replaceOrForceGen" option used, there should be non-zero generation set in the object, last generation will
//    not be checked in that case and old object will be removed from indexes, while new one will be added to them
// 4. default option is saving object with new generation if it differs from the last generation object (or first time
//    created), so, it'll only require adding object to indexes
func (s *etcdStore) Save(newStorable runtime.Storable, opts ...store.SaveOpt) (bool, error) {
	if newStorable == nil {
		return false, fmt.Errorf("can't save nil")
	}

	saveOpts := store.NewSaveOpts(opts)
	info := s.types.Get(newStorable.GetKind())
	indexes := store.IndexesFor(info)
	key := "/" + runtime.KeyForStorable(newStorable)

	if !info.Versioned {
		data := s.marshal(newStorable)
		_, err := s.client.KV.Put(context.TODO(), "/object"+key+"@"+runtime.LastOrEmptyGen.String(), string(data))
		// todo should it be true or false always?
		return false, err
	}

	var newVersion bool
	newObj := newStorable.(runtime.Versioned)
	// todo prefetch all needed keys for STM to maximize performance (in fact it'll get all data in one first request)
	// todo consider unmarshal to the info.New() to support gob w/o need to register types?
	_, err := etcdconc.NewSTM(s.client, func(stm etcdconc.STM) error {
		// need to remove this obj from indexes
		var prevObj runtime.Storable

		if saveOpts.IsReplaceOrForceGen() {
			newGen := newObj.GetGeneration()
			if newGen == runtime.LastOrEmptyGen {
				return fmt.Errorf("error while saving object %s with replaceOrForceGen option but with empty generation", key)
			}
			// need to check if there is an object already exists with gen from the object, if yes - remove it from indexes
			oldObjRaw := stm.Get("/object" + key + "@" + newGen.String())
			if oldObjRaw != "" {
				// todo avoid
				prevObj = info.New().(runtime.Storable)
				/*
					add field require not nil val for unmarshal field into codec
					if nil passed => create instance of desired object (w/o casting to storable) and pass to unmarshal
					if not nil => error if incorrect type
				*/
				s.unmarshal([]byte(oldObjRaw), prevObj)
			}

			// todo compare - if not changed - nothing to do
		} else {
			// need to get last gen using index, if exists - compare with, if different - increment revision and delete old from indexes
			lastGenRaw := stm.Get("/index/" + indexes.KeyForStorable(store.LastGenIndex, newStorable, s.codec))
			if lastGenRaw == "" {
				newObj.SetGeneration(runtime.FirstGen)
				newVersion = true
			} else {
				lastGen := s.unmarshalGen(lastGenRaw)
				oldObjRaw := stm.Get("/object" + key + "@" + lastGen.String())
				if oldObjRaw == "" {
					return fmt.Errorf("last gen index for %s seems to be corrupted: generation doesn't exist", key)
				}
				// todo avoid
				prevObj = info.New().(runtime.Storable)
				s.unmarshal([]byte(oldObjRaw), prevObj)
				if !reflect.DeepEqual(prevObj, newObj) {
					newObj.SetGeneration(lastGen.Next())
					newVersion = true
				} else {
					newObj.SetGeneration(lastGen)
					// nothing to do - object wasn't changed
					return nil
				}
			}
		}

		data := s.marshal(newObj)
		newGen := newObj.GetGeneration()
		stm.Put("/object"+key+"@"+newGen.String(), string(data))

		if prevObj != nil && prevObj.(runtime.Versioned).GetGeneration() == newGen {
			for _, index := range indexes.List {
				indexKey := "/index/" + index.KeyForStorable(prevObj, s.codec)
				if index.Type == store.IndexTypeListGen {
					s.updateIndex(stm, indexKey, prevObj.(runtime.Versioned).GetGeneration(), true)
				}
			}
		}

		for _, index := range indexes.List {
			indexKey := "/index/" + index.KeyForStorable(newStorable, s.codec)
			if index.Type == store.IndexTypeLastGen {
				stm.Put(indexKey, s.marshalGen(newGen))
			} else if index.Type == store.IndexTypeListGen {
				s.updateIndex(stm, indexKey, newGen, false)
			} else {
				panic("only indexes with types store.IndexTypeLastGen and store.IndexTypeListGen are currently supported by Etcd store")
			}
		}

		return nil
	})

	return newVersion, err
}

func (s *etcdStore) updateIndex(stm etcdconc.STM, indexKey string, newGen runtime.Generation, delete bool) {
	valueList := &store.IndexValueList{}
	valueListRaw := stm.Get(indexKey)
	if valueListRaw != "" {
		s.unmarshal([]byte(valueListRaw), valueList)
	}
	// todo avoid marshaling gens for indexes by using special index value list type for gens
	gen := []byte(s.marshalGen(newGen))
	if delete {
		valueList.Remove(gen)
	} else {
		valueList.Add(gen)
	}
	data := s.marshal(valueList)
	stm.Put(indexKey, string(data))
}

/*
Current Find use cases:

* Find(kind, keyPrefix)
* Find(kind, key, gen)  (gen=0 for non-versioned)
* Find(kind, key, WithWhereEq)
* Find(kind, key, WithWhereEq, WithGetFirst)
* Find(kind, key, WithWhereEq, WithGetLast)

\\ summary: keyPrefix OR key+gen OR key + whereEq+list/first/last

Workflow:
* validate parameters and result
* identify requested list or one(first or last)
* build list of keys that are result (could be just build key from parameters or use index)
* based on requested list/first/last get corresponding element from the key list and query value for it

*/
func (s *etcdStore) Find(kind runtime.Kind, result interface{}, opts ...store.FindOpt) error {
	findOpts := store.NewFindOpts(opts)
	info := s.types.Get(kind)

	resultTypeElem := reflect.TypeOf(info.New())
	resultTypeSingle := reflect.PtrTo(reflect.TypeOf(info.New()))
	resultTypeList := reflect.PtrTo(reflect.SliceOf(resultTypeElem))

	resultList := false

	resultType := reflect.TypeOf(result)
	if resultType == resultTypeSingle {
		// ok!
	} else if resultType == resultTypeList {
		// ok!
		resultList = true
	} else {
		fmt.Printf("result should be %s or %s, but found: %s\n", resultTypeSingle, resultTypeList, resultType)
		//return fmt.Errorf("result should be %s or %s, but found: %s", resultTypeSingle, resultTypeList, resultType)
	}

	fmt.Println("findOpts: ", spew.Sdump(findOpts))
	fmt.Println("resultList: ", resultList)

	v := reflect.ValueOf(result).Elem()
	if findOpts.GetKeyPrefix() != "" {
		return s.findByKeyPrefix(findOpts, info, func(elem interface{}) {
			// todo validate type of the elem
			// todo if !resultList
			v.Set(reflect.Append(v, reflect.ValueOf(elem)))
		})
	} else if findOpts.GetKey() != "" && findOpts.GetFieldEqName() == "" {
		return s.findByKey(findOpts, info, func(elem interface{}) {
			// todo validate type of the elem
			if elem == nil {
				v.Set(reflect.Zero(v.Type()))
			} else {
				v.Set(reflect.ValueOf(elem))
			}
		})
	} else {
		return s.findByFieldEq(findOpts, info, func(elem interface{}) {
			// todo validate type of the elem
			if !resultList {
				if elem == nil {
					v.Set(reflect.Zero(v.Type()))
				} else {
					v.Set(reflect.ValueOf(elem))
				}
			} else {
				v.Set(reflect.Append(v, reflect.ValueOf(elem)))
			}
		})
	}
}

func (s *etcdStore) findByKeyPrefix(findOpts *store.FindOpts, info *runtime.TypeInfo, addToResult func(interface{})) error {
	if info.Versioned {
		return fmt.Errorf("searching with key prefix is only supported for non versioned objects")
	}

	resp, err := s.client.KV.Get(context.TODO(), "/object"+"/"+findOpts.GetKeyPrefix(), etcd.WithPrefix())
	if err != nil {
		return err
	}

	for _, kv := range resp.Kvs {
		// todo avoid
		elem := info.New()
		s.unmarshal(kv.Value, elem)
		addToResult(elem)
	}

	return nil
}

func (s *etcdStore) findByKey(findOpts *store.FindOpts, info *runtime.TypeInfo, addToResult func(interface{})) error {

	if !info.Versioned && findOpts.GetGen() != runtime.LastOrEmptyGen {
		return fmt.Errorf("requested specific version for non versioned object")
	}

	var data []byte

	if !info.Versioned || findOpts.GetGen() != runtime.LastOrEmptyGen {
		resp, respErr := s.client.KV.Get(context.TODO(), "/object"+"/"+findOpts.GetKey()+"@"+findOpts.GetGen().String())
		if respErr != nil {
			return respErr
		} else if resp.Count > 0 {
			data = resp.Kvs[0].Value
		}
	} else {
		indexes := store.IndexesFor(info)
		// todo wrap into STM to ensure we're getting really last unchanged element / consider is it important? we can't delete generation, so, probably no need for STM here
		resp, respErr := s.client.KV.Get(context.TODO(), "/index/"+indexes.KeyForValue(store.LastGenIndex, findOpts.GetKey(), nil, s.codec))
		if respErr != nil {
			return respErr
		} else if resp.Count > 0 {
			lastGen := s.unmarshalGen(string(resp.Kvs[0].Value))
			resp, respErr = s.client.KV.Get(context.TODO(), "/object"+"/"+findOpts.GetKey()+"@"+lastGen.String())
			if respErr != nil {
				return respErr
			} else if resp.Count > 0 {
				data = resp.Kvs[0].Value
			}
		}
	}

	if data == nil {
		addToResult(nil)
	} else {
		// todo avoid
		result := info.New()
		s.unmarshal(data, result)

		addToResult(result)
	}

	return nil
}

func (s *etcdStore) findByFieldEq(findOpts *store.FindOpts, info *runtime.TypeInfo, addToResult func(interface{})) error {
	indexes := store.IndexesFor(info)
	resultGens := make([]runtime.Generation, 0)

	_, err := etcdconc.NewSTM(s.client, func(stm etcdconc.STM) error {
		for _, fieldValue := range findOpts.GetFieldEqValues() {
			indexKey := "/index/" + indexes.KeyForValue(findOpts.GetFieldEqName(), findOpts.GetKey(), fieldValue, s.codec)
			indexValue := stm.Get(indexKey)
			if indexValue != "" {
				valueList := &store.IndexValueList{}
				s.unmarshal([]byte(indexValue), valueList)
				for _, val := range *valueList {
					resultGens = append(resultGens, s.unmarshalGen(string(val)))
				}
			}
		}

		sort.Slice(resultGens, func(i, j int) bool {
			return resultGens[i] < resultGens[j]
		})

		if len(resultGens) > 0 {
			if findOpts.IsGetFirst() {
				resultGens = []runtime.Generation{resultGens[0]}
			} else if findOpts.IsGetLast() {
				resultGens = []runtime.Generation{resultGens[len(resultGens)-1]}
			}
			for _, gen := range resultGens {
				data := stm.Get("/object" + "/" + findOpts.GetKey() + "@" + gen.String())
				if data == "" {
					return fmt.Errorf("index is invalid :(")
				}
				result := info.New()
				s.unmarshal([]byte(data), result)
				addToResult(result)
			}
		}

		return nil
	})
	if err != nil {
		return err
	}

	return nil
}

func (s *etcdStore) Delete(kind runtime.Kind, key runtime.Key) error {
	info := s.types.Get(kind)

	if info.Versioned {
		return fmt.Errorf("versioned object couldn't be deleted using store.Delete, use deleted flag + store.Save instead")
	}

	_, err := s.client.KV.Delete(context.TODO(), "/object"+"/"+key+"@"+runtime.LastOrEmptyGen.String())

	return err
}
<commit_msg>Add todo to return back result verification in store.Find<commit_after>package etcd

import (
	"context"
	"fmt"
	"reflect"
	"sort"
	"strings"

	"github.com/Aptomi/aptomi/pkg/runtime"
	"github.com/Aptomi/aptomi/pkg/runtime/store"
	etcd "github.com/coreos/etcd/clientv3"
	etcdconc "github.com/coreos/etcd/clientv3/concurrency"
	"github.com/coreos/etcd/clientv3/namespace"
	"github.com/davecgh/go-spew/spew"
)

type etcdStore struct {
	client *etcd.Client
	types  *runtime.Types
	codec  store.Codec
}

func New(cfg Config, types *runtime.Types, codec store.Codec) (store.Interface, error) {
	if len(cfg.Endpoints) == 0 {
		cfg.Endpoints = []string{"localhost:2379"}
	}

	client, err := etcd.New(etcd.Config{
		Endpoints:            cfg.Endpoints,
		DialTimeout:          dialTimeout,
		DialKeepAliveTime:    keepaliveTime,
		DialKeepAliveTimeout: keepaliveTimeout,
	})
	if err != nil {
		return nil, fmt.Errorf("error while connecting to etcd: %s", err)
	}

	cfg.Prefix = strings.Trim(cfg.Prefix, "/")
	if cfg.Prefix != "" {
		cfg.Prefix = "/" + cfg.Prefix
		client.KV = namespace.NewKV(client.KV, cfg.Prefix)
		client.Lease = namespace.NewLease(client.Lease, cfg.Prefix)
		client.Watcher = namespace.NewWatcher(client.Watcher, cfg.Prefix)
	}

	// todo run compactor?

	return &etcdStore{
		client: client,
		types:  types,
		codec:  codec,
	}, nil
}

func (s *etcdStore) Close() error {
	return s.client.Close()
}

// todo need to rework keys to not include kind or to start with kind at least???

// Save saves Storable object with specified options into Etcd and updates indexes when appropriate.
// Workflow:
// 1. for non-versioned object key is always static, just put object into etcd and no indexes need to be updated (only
//    generation indexes currently exists)
// 2. for versioned object all manipulations are done inside a single transaction to guarantee atomic operations
//    (like index update, getting last existing generation or comparing with existing object), in addition to that
//    generation set for the object is always ignored if "forceGenOrReplace" option isn't used
// 3. if "replaceOrForceGen" option used, there should be non-zero generation set in the object, last generation will
//    not be checked in that case and old object will be removed from indexes, while new one will be added to them
// 4. default option is saving object with new generation if it differs from the last generation object (or first time
//    created), so, it'll only require adding object to indexes
func (s *etcdStore) Save(newStorable runtime.Storable, opts ...store.SaveOpt) (bool, error) {
	if newStorable == nil {
		return false, fmt.Errorf("can't save nil")
	}

	saveOpts := store.NewSaveOpts(opts)
	info := s.types.Get(newStorable.GetKind())
	indexes := store.IndexesFor(info)
	key := "/" + runtime.KeyForStorable(newStorable)

	if !info.Versioned {
		data := s.marshal(newStorable)
		_, err := s.client.KV.Put(context.TODO(), "/object"+key+"@"+runtime.LastOrEmptyGen.String(), string(data))
		// todo should it be true or false always?
		return false, err
	}

	var newVersion bool
	newObj := newStorable.(runtime.Versioned)
	// todo prefetch all needed keys for STM to maximize performance (in fact it'll get all data in one first request)
	// todo consider unmarshal to the info.New() to support gob w/o need to register types?
	_, err := etcdconc.NewSTM(s.client, func(stm etcdconc.STM) error {
		// need to remove this obj from indexes
		var prevObj runtime.Storable

		if saveOpts.IsReplaceOrForceGen() {
			newGen := newObj.GetGeneration()
			if newGen == runtime.LastOrEmptyGen {
				return fmt.Errorf("error while saving object %s with replaceOrForceGen option but with empty generation", key)
			}
			// need to check if there is an object already exists with gen from the object, if yes - remove it from indexes
			oldObjRaw := stm.Get("/object" + key + "@" + newGen.String())
			if oldObjRaw != "" {
				// todo avoid
				prevObj = info.New().(runtime.Storable)
				/*
					add field require not nil val for unmarshal field into codec
					if nil passed => create instance of desired object (w/o casting to storable) and pass to unmarshal
					if not nil => error if incorrect type
				*/
				s.unmarshal([]byte(oldObjRaw), prevObj)
			}

			// todo compare - if not changed - nothing to do
		} else {
			// need to get last gen using index, if exists - compare with, if different - increment revision and delete old from indexes
			lastGenRaw := stm.Get("/index/" + indexes.KeyForStorable(store.LastGenIndex, newStorable, s.codec))
			if lastGenRaw == "" {
				newObj.SetGeneration(runtime.FirstGen)
				newVersion = true
			} else {
				lastGen := s.unmarshalGen(lastGenRaw)
				oldObjRaw := stm.Get("/object" + key + "@" + lastGen.String())
				if oldObjRaw == "" {
					return fmt.Errorf("last gen index for %s seems to be corrupted: generation doesn't exist", key)
				}
				// todo avoid
				prevObj = info.New().(runtime.Storable)
				s.unmarshal([]byte(oldObjRaw), prevObj)
				if !reflect.DeepEqual(prevObj, newObj) {
					newObj.SetGeneration(lastGen.Next())
					newVersion = true
				} else {
					newObj.SetGeneration(lastGen)
					// nothing to do - object wasn't changed
					return nil
				}
			}
		}

		data := s.marshal(newObj)
		newGen := newObj.GetGeneration()
		stm.Put("/object"+key+"@"+newGen.String(), string(data))

		if prevObj != nil && prevObj.(runtime.Versioned).GetGeneration() == newGen {
			for _, index := range indexes.List {
				indexKey := "/index/" + index.KeyForStorable(prevObj, s.codec)
				if index.Type == store.IndexTypeListGen {
					s.updateIndex(stm, indexKey, prevObj.(runtime.Versioned).GetGeneration(), true)
				}
			}
		}

		for _, index := range indexes.List {
			indexKey := "/index/" + index.KeyForStorable(newStorable, s.codec)
			if index.Type == store.IndexTypeLastGen {
				stm.Put(indexKey, s.marshalGen(newGen))
			} else if index.Type == store.IndexTypeListGen {
				s.updateIndex(stm, indexKey, newGen, false)
			} else {
				panic("only indexes with types store.IndexTypeLastGen and store.IndexTypeListGen are currently supported by Etcd store")
			}
		}

		return nil
	})

	return newVersion, err
}

func (s *etcdStore) updateIndex(stm etcdconc.STM, indexKey string, newGen runtime.Generation, delete bool) {
	valueList := &store.IndexValueList{}
	valueListRaw := stm.Get(indexKey)
	if valueListRaw != "" {
		s.unmarshal([]byte(valueListRaw), valueList)
	}
	// todo avoid marshaling gens for indexes by using special index value list type for gens
	gen := []byte(s.marshalGen(newGen))
	if delete {
		valueList.Remove(gen)
	} else {
		valueList.Add(gen)
	}
	data := s.marshal(valueList)
	stm.Put(indexKey, string(data))
}

/*
Current Find use cases:

* Find(kind, keyPrefix)
* Find(kind, key, gen)  (gen=0 for non-versioned)
* Find(kind, key, WithWhereEq)
* Find(kind, key, WithWhereEq, WithGetFirst)
* Find(kind, key, WithWhereEq, WithGetLast)

\\ summary: keyPrefix OR key+gen OR key + whereEq+list/first/last

Workflow:
* validate parameters and result
* identify requested list or one(first or last)
* build list of keys that are result (could be just build key from parameters or use index)
* based on requested list/first/last get corresponding element from the key list and query value for it

*/
func (s *etcdStore) Find(kind runtime.Kind, result interface{}, opts ...store.FindOpt) error {
	findOpts := store.NewFindOpts(opts)
	info := s.types.Get(kind)

	resultTypeElem := reflect.TypeOf(info.New())
	resultTypeSingle := reflect.PtrTo(reflect.TypeOf(info.New()))
	resultTypeList := reflect.PtrTo(reflect.SliceOf(resultTypeElem))

	resultList := false

	resultType := reflect.TypeOf(result)
	if resultType == resultTypeSingle {
		// ok!
	} else if resultType == resultTypeList {
		// ok!
		resultList = true
	} else {
		// todo return back verification
		fmt.Printf("result should be %s or %s, but found: %s\n", resultTypeSingle, resultTypeList, resultType)
		//return fmt.Errorf("result should be %s or %s, but found: %s", resultTypeSingle, resultTypeList, resultType)
	}

	fmt.Println("findOpts: ", spew.Sdump(findOpts))
	fmt.Println("resultList: ", resultList)

	v := reflect.ValueOf(result).Elem()
	if findOpts.GetKeyPrefix() != "" {
		return s.findByKeyPrefix(findOpts, info, func(elem interface{}) {
			// todo validate type of the elem
			// todo if !resultList
			v.Set(reflect.Append(v, reflect.ValueOf(elem)))
		})
	} else if findOpts.GetKey() != "" && findOpts.GetFieldEqName() == "" {
		return s.findByKey(findOpts, info, func(elem interface{}) {
			// todo validate type of the elem
			if elem == nil {
				v.Set(reflect.Zero(v.Type()))
			} else {
				v.Set(reflect.ValueOf(elem))
			}
		})
	} else {
		return s.findByFieldEq(findOpts, info, func(elem interface{}) {
			// todo validate type of the elem
			if !resultList {
				if elem == nil {
					v.Set(reflect.Zero(v.Type()))
				} else {
					v.Set(reflect.ValueOf(elem))
				}
			} else {
				v.Set(reflect.Append(v, reflect.ValueOf(elem)))
			}
		})
	}
}

func (s *etcdStore) findByKeyPrefix(findOpts *store.FindOpts, info *runtime.TypeInfo, addToResult func(interface{})) error {
	if info.Versioned {
		return fmt.Errorf("searching with key prefix is only supported for non versioned objects")
	}

	resp, err := s.client.KV.Get(context.TODO(), "/object"+"/"+findOpts.GetKeyPrefix(), etcd.WithPrefix())
	if err != nil {
		return err
	}

	for _, kv := range resp.Kvs {
		// todo avoid
		elem := info.New()
		s.unmarshal(kv.Value, elem)
		addToResult(elem)
	}

	return nil
}

func (s *etcdStore) findByKey(findOpts *store.FindOpts, info *runtime.TypeInfo, addToResult func(interface{})) error {

	if !info.Versioned && findOpts.GetGen() != runtime.LastOrEmptyGen {
		return fmt.Errorf("requested specific version for non versioned object")
	}

	var data []byte

	if !info.Versioned || findOpts.GetGen() != runtime.LastOrEmptyGen {
		resp, respErr := s.client.KV.Get(context.TODO(), "/object"+"/"+findOpts.GetKey()+"@"+findOpts.GetGen().String())
		if respErr != nil {
			return respErr
		} else if resp.Count > 0 {
			data = resp.Kvs[0].Value
		}
	} else {
		indexes := store.IndexesFor(info)
		// todo wrap into STM to ensure we're getting really last unchanged element / consider is it important? we can't delete generation, so, probably no need for STM here
		resp, respErr := s.client.KV.Get(context.TODO(), "/index/"+indexes.KeyForValue(store.LastGenIndex, findOpts.GetKey(), nil, s.codec))
		if respErr != nil {
			return respErr
		} else if resp.Count > 0 {
			lastGen := s.unmarshalGen(string(resp.Kvs[0].Value))
			resp, respErr = s.client.KV.Get(context.TODO(), "/object"+"/"+findOpts.GetKey()+"@"+lastGen.String())
			if respErr != nil {
				return respErr
			} else if resp.Count > 0 {
				data = resp.Kvs[0].Value
			}
		}
	}

	if data == nil {
		addToResult(nil)
	} else {
		// todo avoid
		result := info.New()
		s.unmarshal(data, result)

		addToResult(result)
	}

	return nil
}

func (s *etcdStore) findByFieldEq(findOpts *store.FindOpts, info *runtime.TypeInfo, addToResult func(interface{})) error {
	indexes := store.IndexesFor(info)
	resultGens := make([]runtime.Generation, 0)

	_, err := etcdconc.NewSTM(s.client, func(stm etcdconc.STM) error {
		for _, fieldValue := range findOpts.GetFieldEqValues() {
			indexKey := "/index/" + indexes.KeyForValue(findOpts.GetFieldEqName(), findOpts.GetKey(), fieldValue, s.codec)
			indexValue := stm.Get(indexKey)
			if indexValue != "" {
				valueList := &store.IndexValueList{}
				s.unmarshal([]byte(indexValue), valueList)
				for _, val := range *valueList {
					resultGens = append(resultGens, s.unmarshalGen(string(val)))
				}
			}
		}

		sort.Slice(resultGens, func(i, j int) bool {
			return resultGens[i] < resultGens[j]
		})

		if len(resultGens) > 0 {
			if findOpts.IsGetFirst() {
				resultGens = []runtime.Generation{resultGens[0]}
			} else if findOpts.IsGetLast() {
				resultGens = []runtime.Generation{resultGens[len(resultGens)-1]}
			}
			for _, gen := range resultGens {
				data := stm.Get("/object" + "/" + findOpts.GetKey() + "@" + gen.String())
				if data == "" {
					return fmt.Errorf("index is invalid :(")
				}
				result := info.New()
				s.unmarshal([]byte(data), result)
				addToResult(result)
			}
		}

		return nil
	})
	if err != nil {
		return err
	}

	return nil
}

func (s *etcdStore) Delete(kind runtime.Kind, key runtime.Key) error {
	info := s.types.Get(kind)

	if info.Versioned {
		return fmt.Errorf("versioned object couldn't be deleted using store.Delete, use deleted flag + store.Save instead")
	}

	_, err := s.client.KV.Delete(context.TODO(), "/object"+"/"+key+"@"+runtime.LastOrEmptyGen.String())

	return err
}
<|endoftext|>


<commit_before>package etcd

import (
	"context"
	"fmt"
	"reflect"
	"sort"
	"strings"

	"github.com/Aptomi/aptomi/pkg/runtime"
	"github.com/Aptomi/aptomi/pkg/runtime/store"
	etcd "github.com/coreos/etcd/clientv3"
	etcdconc "github.com/coreos/etcd/clientv3/concurrency"
	"github.com/coreos/etcd/clientv3/namespace"
	"github.com/davecgh/go-spew/spew"
)

type etcdStore struct {
	client *etcd.Client
	types  *runtime.Types
	codec  store.Codec
}

func New(cfg Config, types *runtime.Types, codec store.Codec) (store.Interface, error) {
	if len(cfg.Endpoints) == 0 {
		cfg.Endpoints = []string{"localhost:2379"}
	}

	client, err := etcd.New(etcd.Config{
		Endpoints:            cfg.Endpoints,
		DialTimeout:          dialTimeout,
		DialKeepAliveTime:    keepaliveTime,
		DialKeepAliveTimeout: keepaliveTimeout,
	})
	if err != nil {
		return nil, fmt.Errorf("error while connecting to etcd: %s", err)
	}

	cfg.Prefix = strings.Trim(cfg.Prefix, "/")
	if cfg.Prefix != "" {
		cfg.Prefix = "/" + cfg.Prefix
		client.KV = namespace.NewKV(client.KV, cfg.Prefix)
		client.Lease = namespace.NewLease(client.Lease, cfg.Prefix)
		client.Watcher = namespace.NewWatcher(client.Watcher, cfg.Prefix)
	}

	// todo run compactor?

	return &etcdStore{
		client: client,
		types:  types,
		codec:  codec,
	}, nil
}

func (s *etcdStore) Close() error {
	return s.client.Close()
}

// todo need to rework keys to not include kind or to start with kind at least???

// Save saves Storable object with specified options into Etcd and updates indexes when appropriate.
// Workflow:
// 1. for non-versioned object key is always static, just put object into etcd and no indexes need to be updated (only
//    generation indexes currently exists)
// 2. for versioned object all manipulations are done inside a single transaction to guarantee atomic operations
//    (like index update, getting last existing generation or comparing with existing object), in addition to that
//    generation set for the object is always ignored if "forceGenOrReplace" option isn't used
// 3. if "replaceOrForceGen" option used, there should be non-zero generation set in the object, last generation will
//    not be checked in that case and old object will be removed from indexes, while new one will be added to them
// 4. default option is saving object with new generation if it differs from the last generation object (or first time
//    created), so, it'll only require adding object to indexes
func (s *etcdStore) Save(newStorable runtime.Storable, opts ...store.SaveOpt) (bool, error) {
	if newStorable == nil {
		return false, fmt.Errorf("can't save nil")
	}

	saveOpts := store.NewSaveOpts(opts)
	info := s.types.Get(newStorable.GetKind())
	indexes := store.IndexesFor(info)
	key := "/" + runtime.KeyForStorable(newStorable)

	if !info.Versioned {
		data := s.marshal(newStorable)
		_, err := s.client.KV.Put(context.TODO(), "/object"+key+"@"+runtime.LastOrEmptyGen.String(), string(data))
		// todo should it be true or false always?
		return false, err
	}

	var newVersion bool
	newObj := newStorable.(runtime.Versioned)
	// todo prefetch all needed keys for STM to maximize performance (in fact it'll get all data in one first request)
	// todo consider unmarshal to the info.New() to support gob w/o need to register types?
	_, err := etcdconc.NewSTM(s.client, func(stm etcdconc.STM) error {
		// need to remove this obj from indexes
		var prevObj runtime.Storable

		if saveOpts.IsReplaceOrForceGen() {
			newGen := newObj.GetGeneration()
			if newGen == runtime.LastOrEmptyGen {
				return fmt.Errorf("error while saving object %s with replaceOrForceGen option but with empty generation", key)
			}
			// need to check if there is an object already exists with gen from the object, if yes - remove it from indexes
			oldObjRaw := stm.Get("/object" + key + "@" + newGen.String())
			if oldObjRaw != "" {
				// todo avoid
				prevObj = info.New().(runtime.Storable)
				/*
					add field require not nil val for unmarshal field into codec
					if nil passed => create instance of desired object (w/o casting to storable) and pass to unmarshal
					if not nil => error if incorrect type
				*/
				s.unmarshal([]byte(oldObjRaw), prevObj)
			}

			// todo compare - if not changed - nothing to do
		} else {
			// need to get last gen using index, if exists - compare with, if different - increment revision and delete old from indexes
			lastGenRaw := stm.Get("/index/" + indexes.KeyForStorable(store.LastGenIndex, newStorable, s.codec))
			if lastGenRaw == "" {
				newObj.SetGeneration(runtime.FirstGen)
				newVersion = true
			} else {
				lastGen := s.unmarshalGen(lastGenRaw)
				oldObjRaw := stm.Get("/object" + key + "@" + lastGen.String())
				if oldObjRaw == "" {
					return fmt.Errorf("last gen index for %s seems to be corrupted: generation doesn't exist", key)
				}
				// todo avoid
				prevObj = info.New().(runtime.Storable)
				s.unmarshal([]byte(oldObjRaw), prevObj)
				if !reflect.DeepEqual(prevObj, newObj) {
					newObj.SetGeneration(lastGen.Next())
					newVersion = true
				} else {
					newObj.SetGeneration(lastGen)
					// nothing to do - object wasn't changed
					return nil
				}
			}
		}

		data := s.marshal(newObj)
		newGen := newObj.GetGeneration()
		stm.Put("/object"+key+"@"+newGen.String(), string(data))

		if prevObj != nil && prevObj.(runtime.Versioned).GetGeneration() == newGen {
			for _, index := range indexes.List {
				indexKey := "/index/" + index.KeyForStorable(prevObj, s.codec)
				if index.Type == store.IndexTypeListGen {
					s.updateIndex(stm, indexKey, prevObj.(runtime.Versioned).GetGeneration(), true)
				}
			}
		}

		for _, index := range indexes.List {
			indexKey := "/index/" + index.KeyForStorable(newStorable, s.codec)
			if index.Type == store.IndexTypeLastGen {
				stm.Put(indexKey, s.marshalGen(newGen))
			} else if index.Type == store.IndexTypeListGen {
				s.updateIndex(stm, indexKey, newGen, false)
			} else {
				panic("only indexes with types store.IndexTypeLastGen and store.IndexTypeListGen are currently supported by Etcd store")
			}
		}

		return nil
	})

	return newVersion, err
}

func (s *etcdStore) updateIndex(stm etcdconc.STM, indexKey string, newGen runtime.Generation, delete bool) {
	valueList := &store.IndexValueList{}
	valueListRaw := stm.Get(indexKey)
	if valueListRaw != "" {
		s.unmarshal([]byte(valueListRaw), valueList)
	}
	// todo avoid marshaling gens for indexes by using special index value list type for gens
	gen := []byte(s.marshalGen(newGen))
	if delete {
		valueList.Remove(gen)
	} else {
		valueList.Add(gen)
	}
	data := s.marshal(valueList)
	stm.Put(indexKey, string(data))
}

/*
Current Find use cases:

* Find(kind, keyPrefix)
* Find(kind, key, gen)  (gen=0 for non-versioned)
* Find(kind, key, WithWhereEq)
* Find(kind, key, WithWhereEq, WithGetFirst)
* Find(kind, key, WithWhereEq, WithGetLast)

\\ summary: keyPrefix OR key+gen OR key + whereEq+list/first/last

Workflow:
* validate parameters and result
* identify requested list or one(first or last)
* build list of keys that are result (could be just build key from parameters or use index)
* based on requested list/first/last get corresponding element from the key list and query value for it

*/
func (s *etcdStore) Find(kind runtime.Kind, result interface{}, opts ...store.FindOpt) error {
	findOpts := store.NewFindOpts(opts)
	info := s.types.Get(kind)

	resultTypeElem := reflect.TypeOf(info.New())
	resultTypeSingle := reflect.PtrTo(reflect.TypeOf(info.New()))
	resultTypeList := reflect.PtrTo(reflect.SliceOf(resultTypeElem))

	resultList := false

	resultType := reflect.TypeOf(result)
	if resultType == resultTypeSingle {
		// ok!
	} else if resultType == resultTypeList {
		// ok!
		resultList = true
	} else {
		// todo return back verification
		fmt.Printf("result should be %s or %s, but found: %s\n", resultTypeSingle, resultTypeList, resultType)
		//return fmt.Errorf("result should be %s or %s, but found: %s", resultTypeSingle, resultTypeList, resultType)
	}

	v := reflect.ValueOf(result).Elem()
	if findOpts.GetKeyPrefix() != "" {
		return s.findByKeyPrefix(findOpts, info, func(elem interface{}) {
			// todo validate type of the elem
			// todo if !resultList
			v.Set(reflect.Append(v, reflect.ValueOf(elem)))
		})
	} else if findOpts.GetKey() != "" && findOpts.GetFieldEqName() == "" {
		return s.findByKey(findOpts, info, func(elem interface{}) {
			// todo validate type of the elem
			if elem == nil {
				v.Set(reflect.Zero(v.Type()))
			} else {
				v.Set(reflect.ValueOf(elem))
			}
		})
	} else {
		return s.findByFieldEq(findOpts, info, func(elem interface{}) {
			// todo validate type of the elem
			if !resultList {
				if elem == nil {
					v.Set(reflect.Zero(v.Type()))
				} else {
					v.Set(reflect.ValueOf(elem))
				}
			} else {
				v.Set(reflect.Append(v, reflect.ValueOf(elem)))
			}
		})
	}
}

func (s *etcdStore) findByKeyPrefix(findOpts *store.FindOpts, info *runtime.TypeInfo, addToResult func(interface{})) error {
	if info.Versioned {
		return fmt.Errorf("searching with key prefix is only supported for non versioned objects")
	}

	resp, err := s.client.KV.Get(context.TODO(), "/object"+"/"+findOpts.GetKeyPrefix(), etcd.WithPrefix())
	if err != nil {
		return err
	}

	for _, kv := range resp.Kvs {
		// todo avoid
		elem := info.New()
		s.unmarshal(kv.Value, elem)
		addToResult(elem)
	}

	return nil
}

func (s *etcdStore) findByKey(findOpts *store.FindOpts, info *runtime.TypeInfo, addToResult func(interface{})) error {

	if !info.Versioned && findOpts.GetGen() != runtime.LastOrEmptyGen {
		return fmt.Errorf("requested specific version for non versioned object")
	}

	var data []byte

	if !info.Versioned || findOpts.GetGen() != runtime.LastOrEmptyGen {
		resp, respErr := s.client.KV.Get(context.TODO(), "/object"+"/"+findOpts.GetKey()+"@"+findOpts.GetGen().String())
		if respErr != nil {
			return respErr
		} else if resp.Count > 0 {
			data = resp.Kvs[0].Value
		}
	} else {
		indexes := store.IndexesFor(info)
		// todo wrap into STM to ensure we're getting really last unchanged element / consider is it important? we can't delete generation, so, probably no need for STM here
		resp, respErr := s.client.KV.Get(context.TODO(), "/index/"+indexes.KeyForValue(store.LastGenIndex, findOpts.GetKey(), nil, s.codec))
		if respErr != nil {
			return respErr
		} else if resp.Count > 0 {
			lastGen := s.unmarshalGen(string(resp.Kvs[0].Value))
			resp, respErr = s.client.KV.Get(context.TODO(), "/object"+"/"+findOpts.GetKey()+"@"+lastGen.String())
			if respErr != nil {
				return respErr
			} else if resp.Count > 0 {
				data = resp.Kvs[0].Value
			}
		}
	}

	if data == nil {
		addToResult(nil)
	} else {
		// todo avoid
		result := info.New()
		s.unmarshal(data, result)

		addToResult(result)
	}

	return nil
}

func (s *etcdStore) findByFieldEq(findOpts *store.FindOpts, info *runtime.TypeInfo, addToResult func(interface{})) error {
	indexes := store.IndexesFor(info)
	resultGens := make([]runtime.Generation, 0)

	_, err := etcdconc.NewSTM(s.client, func(stm etcdconc.STM) error {
		for _, fieldValue := range findOpts.GetFieldEqValues() {
			indexKey := "/index/" + indexes.KeyForValue(findOpts.GetFieldEqName(), findOpts.GetKey(), fieldValue, s.codec)
			indexValue := stm.Get(indexKey)
			if indexValue != "" {
				valueList := &store.IndexValueList{}
				s.unmarshal([]byte(indexValue), valueList)
				for _, val := range *valueList {
					resultGens = append(resultGens, s.unmarshalGen(string(val)))
				}
			}
		}

		sort.Slice(resultGens, func(i, j int) bool {
			return resultGens[i] < resultGens[j]
		})

		if len(resultGens) > 0 {
			if findOpts.IsGetFirst() {
				resultGens = []runtime.Generation{resultGens[0]}
			} else if findOpts.IsGetLast() {
				resultGens = []runtime.Generation{resultGens[len(resultGens)-1]}
			}
			for _, gen := range resultGens {
				data := stm.Get("/object" + "/" + findOpts.GetKey() + "@" + gen.String())
				if data == "" {
					return fmt.Errorf("index is invalid :(")
				}
				result := info.New()
				s.unmarshal([]byte(data), result)
				addToResult(result)
			}
		}

		return nil
	})
	if err != nil {
		return err
	}

	return nil
}

func (s *etcdStore) Delete(kind runtime.Kind, key runtime.Key) error {
	info := s.types.Get(kind)

	if info.Versioned {
		return fmt.Errorf("versioned object couldn't be deleted using store.Delete, use deleted flag + store.Save instead")
	}

	_, err := s.client.KV.Delete(context.TODO(), "/object"+"/"+key+"@"+runtime.LastOrEmptyGen.String())

	return err
}
<commit_msg>Always compare object with same generation when saving<commit_after>package etcd

import (
	"context"
	"fmt"
	"reflect"
	"sort"
	"strings"

	"github.com/Aptomi/aptomi/pkg/runtime"
	"github.com/Aptomi/aptomi/pkg/runtime/store"
	etcd "github.com/coreos/etcd/clientv3"
	etcdconc "github.com/coreos/etcd/clientv3/concurrency"
	"github.com/coreos/etcd/clientv3/namespace"
)

type etcdStore struct {
	client *etcd.Client
	types  *runtime.Types
	codec  store.Codec
}

func New(cfg Config, types *runtime.Types, codec store.Codec) (store.Interface, error) {
	if len(cfg.Endpoints) == 0 {
		cfg.Endpoints = []string{"localhost:2379"}
	}

	client, err := etcd.New(etcd.Config{
		Endpoints:            cfg.Endpoints,
		DialTimeout:          dialTimeout,
		DialKeepAliveTime:    keepaliveTime,
		DialKeepAliveTimeout: keepaliveTimeout,
	})
	if err != nil {
		return nil, fmt.Errorf("error while connecting to etcd: %s", err)
	}

	cfg.Prefix = strings.Trim(cfg.Prefix, "/")
	if cfg.Prefix != "" {
		cfg.Prefix = "/" + cfg.Prefix
		client.KV = namespace.NewKV(client.KV, cfg.Prefix)
		client.Lease = namespace.NewLease(client.Lease, cfg.Prefix)
		client.Watcher = namespace.NewWatcher(client.Watcher, cfg.Prefix)
	}

	// todo run compactor?

	return &etcdStore{
		client: client,
		types:  types,
		codec:  codec,
	}, nil
}

func (s *etcdStore) Close() error {
	return s.client.Close()
}

// todo need to rework keys to not include kind or to start with kind at least???

// Save saves Storable object with specified options into Etcd and updates indexes when appropriate.
// Workflow:
// 1. for non-versioned object key is always static, just put object into etcd and no indexes need to be updated (only
//    generation indexes currently exists)
// 2. for versioned object all manipulations are done inside a single transaction to guarantee atomic operations
//    (like index update, getting last existing generation or comparing with existing object), in addition to that
//    generation set for the object is always ignored if "forceGenOrReplace" option isn't used
// 3. if "replaceOrForceGen" option used, there should be non-zero generation set in the object, last generation will
//    not be checked in that case and old object will be removed from indexes, while new one will be added to them
// 4. default option is saving object with new generation if it differs from the last generation object (or first time
//    created), so, it'll only require adding object to indexes
func (s *etcdStore) Save(newStorable runtime.Storable, opts ...store.SaveOpt) (bool, error) {
	if newStorable == nil {
		return false, fmt.Errorf("can't save nil")
	}

	saveOpts := store.NewSaveOpts(opts)
	info := s.types.Get(newStorable.GetKind())
	indexes := store.IndexesFor(info)
	key := "/" + runtime.KeyForStorable(newStorable)

	if !info.Versioned {
		data := s.marshal(newStorable)
		_, err := s.client.KV.Put(context.TODO(), "/object"+key+"@"+runtime.LastOrEmptyGen.String(), string(data))
		// todo should it be true or false always?
		return false, err
	}

	var newVersion bool
	newObj := newStorable.(runtime.Versioned)
	// todo prefetch all needed keys for STM to maximize performance (in fact it'll get all data in one first request)
	// todo consider unmarshal to the info.New() to support gob w/o need to register types?
	_, err := etcdconc.NewSTM(s.client, func(stm etcdconc.STM) error {
		// need to remove this obj from indexes
		var prevObj runtime.Storable

		if saveOpts.IsReplaceOrForceGen() {
			newGen := newObj.GetGeneration()
			if newGen == runtime.LastOrEmptyGen {
				return fmt.Errorf("error while saving object %s with replaceOrForceGen option but with empty generation", key)
			}
			// need to check if there is an object already exists with gen from the object, if yes - remove it from indexes
			oldObjRaw := stm.Get("/object" + key + "@" + newGen.String())
			if oldObjRaw != "" {
				// todo avoid
				prevObj = info.New().(runtime.Storable)
				/*
					add field require not nil val for unmarshal field into codec
					if nil passed => create instance of desired object (w/o casting to storable) and pass to unmarshal
					if not nil => error if incorrect type
				*/
				s.unmarshal([]byte(oldObjRaw), prevObj)
			}

			// todo compare - if not changed - nothing to do
		} else {
			// need to get last gen using index, if exists - compare with, if different - increment revision and delete old from indexes
			lastGenRaw := stm.Get("/index/" + indexes.KeyForStorable(store.LastGenIndex, newStorable, s.codec))
			if lastGenRaw == "" {
				newObj.SetGeneration(runtime.FirstGen)
				newVersion = true
			} else {
				lastGen := s.unmarshalGen(lastGenRaw)
				oldObjRaw := stm.Get("/object" + key + "@" + lastGen.String())
				if oldObjRaw == "" {
					return fmt.Errorf("last gen index for %s seems to be corrupted: generation doesn't exist", key)
				}
				// todo avoid
				prevObj = info.New().(runtime.Storable)
				s.unmarshal([]byte(oldObjRaw), prevObj)
				newObj.SetGeneration(lastGen)
				if reflect.DeepEqual(prevObj, newObj) {
					return nil
				} else {
					newObj.SetGeneration(lastGen.Next())
					newVersion = true
				}
			}
		}

		data := s.marshal(newObj)
		newGen := newObj.GetGeneration()
		stm.Put("/object"+key+"@"+newGen.String(), string(data))

		if prevObj != nil && prevObj.(runtime.Versioned).GetGeneration() == newGen {
			for _, index := range indexes.List {
				indexKey := "/index/" + index.KeyForStorable(prevObj, s.codec)
				if index.Type == store.IndexTypeListGen {
					s.updateIndex(stm, indexKey, prevObj.(runtime.Versioned).GetGeneration(), true)
				}
			}
		}

		for _, index := range indexes.List {
			indexKey := "/index/" + index.KeyForStorable(newStorable, s.codec)
			if index.Type == store.IndexTypeLastGen {
				stm.Put(indexKey, s.marshalGen(newGen))
			} else if index.Type == store.IndexTypeListGen {
				s.updateIndex(stm, indexKey, newGen, false)
			} else {
				panic("only indexes with types store.IndexTypeLastGen and store.IndexTypeListGen are currently supported by Etcd store")
			}
		}

		return nil
	})

	return newVersion, err
}

func (s *etcdStore) updateIndex(stm etcdconc.STM, indexKey string, newGen runtime.Generation, delete bool) {
	valueList := &store.IndexValueList{}
	valueListRaw := stm.Get(indexKey)
	if valueListRaw != "" {
		s.unmarshal([]byte(valueListRaw), valueList)
	}
	// todo avoid marshaling gens for indexes by using special index value list type for gens
	gen := []byte(s.marshalGen(newGen))
	if delete {
		valueList.Remove(gen)
	} else {
		valueList.Add(gen)
	}
	data := s.marshal(valueList)
	stm.Put(indexKey, string(data))
}

/*
Current Find use cases:

* Find(kind, keyPrefix)
* Find(kind, key, gen)  (gen=0 for non-versioned)
* Find(kind, key, WithWhereEq)
* Find(kind, key, WithWhereEq, WithGetFirst)
* Find(kind, key, WithWhereEq, WithGetLast)

\\ summary: keyPrefix OR key+gen OR key + whereEq+list/first/last

Workflow:
* validate parameters and result
* identify requested list or one(first or last)
* build list of keys that are result (could be just build key from parameters or use index)
* based on requested list/first/last get corresponding element from the key list and query value for it

*/
func (s *etcdStore) Find(kind runtime.Kind, result interface{}, opts ...store.FindOpt) error {
	findOpts := store.NewFindOpts(opts)
	info := s.types.Get(kind)

	resultTypeElem := reflect.TypeOf(info.New())
	resultTypeSingle := reflect.PtrTo(reflect.TypeOf(info.New()))
	resultTypeList := reflect.PtrTo(reflect.SliceOf(resultTypeElem))

	resultList := false

	resultType := reflect.TypeOf(result)
	if resultType == resultTypeSingle {
		// ok!
	} else if resultType == resultTypeList {
		// ok!
		resultList = true
	} else {
		// todo return back verification
		fmt.Printf("result should be %s or %s, but found: %s\n", resultTypeSingle, resultTypeList, resultType)
		//return fmt.Errorf("result should be %s or %s, but found: %s", resultTypeSingle, resultTypeList, resultType)
	}

	v := reflect.ValueOf(result).Elem()
	if findOpts.GetKeyPrefix() != "" {
		return s.findByKeyPrefix(findOpts, info, func(elem interface{}) {
			// todo validate type of the elem
			// todo if !resultList
			v.Set(reflect.Append(v, reflect.ValueOf(elem)))
		})
	} else if findOpts.GetKey() != "" && findOpts.GetFieldEqName() == "" {
		return s.findByKey(findOpts, info, func(elem interface{}) {
			// todo validate type of the elem
			if elem == nil {
				v.Set(reflect.Zero(v.Type()))
			} else {
				v.Set(reflect.ValueOf(elem))
			}
		})
	} else {
		return s.findByFieldEq(findOpts, info, func(elem interface{}) {
			// todo validate type of the elem
			if !resultList {
				if elem == nil {
					v.Set(reflect.Zero(v.Type()))
				} else {
					v.Set(reflect.ValueOf(elem))
				}
			} else {
				v.Set(reflect.Append(v, reflect.ValueOf(elem)))
			}
		})
	}
}

func (s *etcdStore) findByKeyPrefix(findOpts *store.FindOpts, info *runtime.TypeInfo, addToResult func(interface{})) error {
	if info.Versioned {
		return fmt.Errorf("searching with key prefix is only supported for non versioned objects")
	}

	resp, err := s.client.KV.Get(context.TODO(), "/object"+"/"+findOpts.GetKeyPrefix(), etcd.WithPrefix())
	if err != nil {
		return err
	}

	for _, kv := range resp.Kvs {
		// todo avoid
		elem := info.New()
		s.unmarshal(kv.Value, elem)
		addToResult(elem)
	}

	return nil
}

func (s *etcdStore) findByKey(findOpts *store.FindOpts, info *runtime.TypeInfo, addToResult func(interface{})) error {

	if !info.Versioned && findOpts.GetGen() != runtime.LastOrEmptyGen {
		return fmt.Errorf("requested specific version for non versioned object")
	}

	var data []byte

	if !info.Versioned || findOpts.GetGen() != runtime.LastOrEmptyGen {
		resp, respErr := s.client.KV.Get(context.TODO(), "/object"+"/"+findOpts.GetKey()+"@"+findOpts.GetGen().String())
		if respErr != nil {
			return respErr
		} else if resp.Count > 0 {
			data = resp.Kvs[0].Value
		}
	} else {
		indexes := store.IndexesFor(info)
		// todo wrap into STM to ensure we're getting really last unchanged element / consider is it important? we can't delete generation, so, probably no need for STM here
		resp, respErr := s.client.KV.Get(context.TODO(), "/index/"+indexes.KeyForValue(store.LastGenIndex, findOpts.GetKey(), nil, s.codec))
		if respErr != nil {
			return respErr
		} else if resp.Count > 0 {
			lastGen := s.unmarshalGen(string(resp.Kvs[0].Value))
			resp, respErr = s.client.KV.Get(context.TODO(), "/object"+"/"+findOpts.GetKey()+"@"+lastGen.String())
			if respErr != nil {
				return respErr
			} else if resp.Count > 0 {
				data = resp.Kvs[0].Value
			}
		}
	}

	if data == nil {
		addToResult(nil)
	} else {
		// todo avoid
		result := info.New()
		s.unmarshal(data, result)

		addToResult(result)
	}

	return nil
}

func (s *etcdStore) findByFieldEq(findOpts *store.FindOpts, info *runtime.TypeInfo, addToResult func(interface{})) error {
	indexes := store.IndexesFor(info)
	resultGens := make([]runtime.Generation, 0)

	_, err := etcdconc.NewSTM(s.client, func(stm etcdconc.STM) error {
		for _, fieldValue := range findOpts.GetFieldEqValues() {
			indexKey := "/index/" + indexes.KeyForValue(findOpts.GetFieldEqName(), findOpts.GetKey(), fieldValue, s.codec)
			indexValue := stm.Get(indexKey)
			if indexValue != "" {
				valueList := &store.IndexValueList{}
				s.unmarshal([]byte(indexValue), valueList)
				for _, val := range *valueList {
					resultGens = append(resultGens, s.unmarshalGen(string(val)))
				}
			}
		}

		sort.Slice(resultGens, func(i, j int) bool {
			return resultGens[i] < resultGens[j]
		})

		if len(resultGens) > 0 {
			if findOpts.IsGetFirst() {
				resultGens = []runtime.Generation{resultGens[0]}
			} else if findOpts.IsGetLast() {
				resultGens = []runtime.Generation{resultGens[len(resultGens)-1]}
			}
			for _, gen := range resultGens {
				data := stm.Get("/object" + "/" + findOpts.GetKey() + "@" + gen.String())
				if data == "" {
					return fmt.Errorf("index is invalid :(")
				}
				result := info.New()
				s.unmarshal([]byte(data), result)
				addToResult(result)
			}
		}

		return nil
	})
	if err != nil {
		return err
	}

	return nil
}

func (s *etcdStore) Delete(kind runtime.Kind, key runtime.Key) error {
	info := s.types.Get(kind)

	if info.Versioned {
		return fmt.Errorf("versioned object couldn't be deleted using store.Delete, use deleted flag + store.Save instead")
	}

	_, err := s.client.KV.Delete(context.TODO(), "/object"+"/"+key+"@"+runtime.LastOrEmptyGen.String())

	return err
}
<|endoftext|>


<commit_before>/*
Copyright 2021 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package server

/*
We make an attempt here to identify the events that take place during
lifecycle of the apiserver.

We also identify each event with a name so we can refer to it.

Events:
- ShutdownInitiated: KILL signal received
- AfterShutdownDelayDuration: shutdown delay duration has passed
- InFlightRequestsDrained: all in flight request(s) have been drained
- HasBeenReady is signaled when the readyz endpoint succeeds for the first time

The following is a sequence of shutdown events that we expect to see with
  'ShutdownSendRetryAfter' = false:

T0: ShutdownInitiated: KILL signal received
	- /readyz starts returning red
    - run pre shutdown hooks

T0+70s: AfterShutdownDelayDuration: shutdown delay duration has passed
	- the default value of 'ShutdownDelayDuration' is '70s'
	- it's time to initiate shutdown of the HTTP Server, server.Shutdown is invoked
	- as a consequene, the Close function has is called for all listeners
 	- the HTTP Server stops listening immediately
	- any new request arriving on a new TCP socket is denied with
      a network error similar to 'connection refused'
    - the HTTP Server waits gracefully for existing requests to complete
      up to '60s' (dictated by ShutdownTimeout)
	- active long running requests will receive a GOAWAY.

T0+70s: HTTPServerStoppedListening:
	- this event is signaled when the HTTP Server has stopped listening
      which is immediately after server.Shutdown has been invoked

T0 + 70s + up-to 60s: InFlightRequestsDrained: existing in flight requests have been drained
	- long running requests are outside of this scope
	- up-to 60s: the default value of 'ShutdownTimeout' is 60s, this means that
      any request in flight has a hard timeout of 60s.
	- it's time to call 'Shutdown' on the audit events since all
	  in flight request(s) have drained.

The following is a sequence of shutdown events that we expect to see with
  'ShutdownSendRetryAfter' = true:

T0: ShutdownInitiated: KILL signal received
	- /readyz starts returning red
    - run pre shutdown hooks

T0+70s: AfterShutdownDelayDuration: shutdown delay duration has passed
	- the default value of 'ShutdownDelayDuration' is '70s'
	- the HTTP Server will continue to listen
	- the apiserver is not accepting new request(s)
		- it includes new request(s) on a new or an existing TCP connection
		- new request(s) arriving after this point are replied with a 429
      	  and the  response headers: 'Retry-After: 1` and 'Connection: close'
	- note: these new request(s) will not show up in audit logs

T0 + 70s + up to 60s: InFlightRequestsDrained: existing in flight requests have been drained
	- long running requests are outside of this scope
	- up to 60s: the default value of 'ShutdownTimeout' is 60s, this means that
      any request in flight has a hard timeout of 60s.
	- server.Shutdown is called, the HTTP Server stops listening immediately
    - the HTTP Server waits gracefully for existing requests to complete
      up to '2s' (it's hard coded right now)
*/

// lifecycleSignal encapsulates a named apiserver event
type lifecycleSignal interface {
	// Signal signals the event, indicating that the event has occurred.
	// Signal is idempotent, once signaled the event stays signaled and
	// it immediately unblocks any goroutine waiting for this event.
	Signal()

	// Signaled returns a channel that is closed when the underlying event
	// has been signaled. Successive calls to Signaled return the same value.
	Signaled() <-chan struct{}

	// Name returns the name of the signal, useful for logging.
	Name() string
}

// lifecycleSignals provides an abstraction of the events that
// transpire during the lifecycle of the apiserver. This abstraction makes it easy
// for us to write unit tests that can verify expected graceful termination behavior.
//
// GenericAPIServer can use these to either:
//  - signal that a particular termination event has transpired
//  - wait for a designated termination event to transpire and do some action.
type lifecycleSignals struct {
	// ShutdownInitiated event is signaled when an apiserver shutdown has been initiated.
	// It is signaled when the `stopCh` provided by the main goroutine
	// receives a KILL signal and is closed as a consequence.
	ShutdownInitiated lifecycleSignal

	// AfterShutdownDelayDuration event is signaled as soon as ShutdownDelayDuration
	// has elapsed since the ShutdownInitiated event.
	// ShutdownDelayDuration allows the apiserver to delay shutdown for some time.
	AfterShutdownDelayDuration lifecycleSignal

	// InFlightRequestsDrained event is signaled when the existing requests
	// in flight have completed. This is used as signal to shut down the audit backends
	InFlightRequestsDrained lifecycleSignal

	// HTTPServerStoppedListening termination event is signaled when the
	// HTTP Server has stopped listening to the underlying socket.
	HTTPServerStoppedListening lifecycleSignal

	// HasBeenReady is signaled when the readyz endpoint succeeds for the first time.
	HasBeenReady lifecycleSignal
}

// newLifecycleSignals returns an instance of lifecycleSignals interface to be used
// to coordinate lifecycle of the apiserver
func newLifecycleSignals() lifecycleSignals {
	return lifecycleSignals{
		ShutdownInitiated:          newNamedChannelWrapper("ShutdownInitiated"),
		AfterShutdownDelayDuration: newNamedChannelWrapper("AfterShutdownDelayDuration"),
		InFlightRequestsDrained:    newNamedChannelWrapper("InFlightRequestsDrained"),
		HTTPServerStoppedListening: newNamedChannelWrapper("HTTPServerStoppedListening"),
		HasBeenReady:               newNamedChannelWrapper("HasBeenReady"),
	}
}

func newNamedChannelWrapper(name string) lifecycleSignal {
	return &namedChannelWrapper{
		name: name,
		ch:   make(chan struct{}),
	}
}

type namedChannelWrapper struct {
	name string
	ch   chan struct{}
}

func (e *namedChannelWrapper) Signal() {
	select {
	case <-e.ch:
		// already closed, don't close again.
	default:
		close(e.ch)
	}
}

func (e *namedChannelWrapper) Signaled() <-chan struct{} {
	return e.ch
}

func (e *namedChannelWrapper) Name() string {
	return e.name
}
<commit_msg>make lifecycle signal thread safe<commit_after>/*
Copyright 2021 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package server

import (
	"sync"
)

/*
We make an attempt here to identify the events that take place during
lifecycle of the apiserver.

We also identify each event with a name so we can refer to it.

Events:
- ShutdownInitiated: KILL signal received
- AfterShutdownDelayDuration: shutdown delay duration has passed
- InFlightRequestsDrained: all in flight request(s) have been drained
- HasBeenReady is signaled when the readyz endpoint succeeds for the first time

The following is a sequence of shutdown events that we expect to see with
  'ShutdownSendRetryAfter' = false:

T0: ShutdownInitiated: KILL signal received
	- /readyz starts returning red
    - run pre shutdown hooks

T0+70s: AfterShutdownDelayDuration: shutdown delay duration has passed
	- the default value of 'ShutdownDelayDuration' is '70s'
	- it's time to initiate shutdown of the HTTP Server, server.Shutdown is invoked
	- as a consequene, the Close function has is called for all listeners
 	- the HTTP Server stops listening immediately
	- any new request arriving on a new TCP socket is denied with
      a network error similar to 'connection refused'
    - the HTTP Server waits gracefully for existing requests to complete
      up to '60s' (dictated by ShutdownTimeout)
	- active long running requests will receive a GOAWAY.

T0+70s: HTTPServerStoppedListening:
	- this event is signaled when the HTTP Server has stopped listening
      which is immediately after server.Shutdown has been invoked

T0 + 70s + up-to 60s: InFlightRequestsDrained: existing in flight requests have been drained
	- long running requests are outside of this scope
	- up-to 60s: the default value of 'ShutdownTimeout' is 60s, this means that
      any request in flight has a hard timeout of 60s.
	- it's time to call 'Shutdown' on the audit events since all
	  in flight request(s) have drained.

The following is a sequence of shutdown events that we expect to see with
  'ShutdownSendRetryAfter' = true:

T0: ShutdownInitiated: KILL signal received
	- /readyz starts returning red
    - run pre shutdown hooks

T0+70s: AfterShutdownDelayDuration: shutdown delay duration has passed
	- the default value of 'ShutdownDelayDuration' is '70s'
	- the HTTP Server will continue to listen
	- the apiserver is not accepting new request(s)
		- it includes new request(s) on a new or an existing TCP connection
		- new request(s) arriving after this point are replied with a 429
      	  and the  response headers: 'Retry-After: 1` and 'Connection: close'
	- note: these new request(s) will not show up in audit logs

T0 + 70s + up to 60s: InFlightRequestsDrained: existing in flight requests have been drained
	- long running requests are outside of this scope
	- up to 60s: the default value of 'ShutdownTimeout' is 60s, this means that
      any request in flight has a hard timeout of 60s.
	- server.Shutdown is called, the HTTP Server stops listening immediately
    - the HTTP Server waits gracefully for existing requests to complete
      up to '2s' (it's hard coded right now)
*/

// lifecycleSignal encapsulates a named apiserver event
type lifecycleSignal interface {
	// Signal signals the event, indicating that the event has occurred.
	// Signal is idempotent, once signaled the event stays signaled and
	// it immediately unblocks any goroutine waiting for this event.
	Signal()

	// Signaled returns a channel that is closed when the underlying event
	// has been signaled. Successive calls to Signaled return the same value.
	Signaled() <-chan struct{}

	// Name returns the name of the signal, useful for logging.
	Name() string
}

// lifecycleSignals provides an abstraction of the events that
// transpire during the lifecycle of the apiserver. This abstraction makes it easy
// for us to write unit tests that can verify expected graceful termination behavior.
//
// GenericAPIServer can use these to either:
//  - signal that a particular termination event has transpired
//  - wait for a designated termination event to transpire and do some action.
type lifecycleSignals struct {
	// ShutdownInitiated event is signaled when an apiserver shutdown has been initiated.
	// It is signaled when the `stopCh` provided by the main goroutine
	// receives a KILL signal and is closed as a consequence.
	ShutdownInitiated lifecycleSignal

	// AfterShutdownDelayDuration event is signaled as soon as ShutdownDelayDuration
	// has elapsed since the ShutdownInitiated event.
	// ShutdownDelayDuration allows the apiserver to delay shutdown for some time.
	AfterShutdownDelayDuration lifecycleSignal

	// InFlightRequestsDrained event is signaled when the existing requests
	// in flight have completed. This is used as signal to shut down the audit backends
	InFlightRequestsDrained lifecycleSignal

	// HTTPServerStoppedListening termination event is signaled when the
	// HTTP Server has stopped listening to the underlying socket.
	HTTPServerStoppedListening lifecycleSignal

	// HasBeenReady is signaled when the readyz endpoint succeeds for the first time.
	HasBeenReady lifecycleSignal
}

// newLifecycleSignals returns an instance of lifecycleSignals interface to be used
// to coordinate lifecycle of the apiserver
func newLifecycleSignals() lifecycleSignals {
	return lifecycleSignals{
		ShutdownInitiated:          newNamedChannelWrapper("ShutdownInitiated"),
		AfterShutdownDelayDuration: newNamedChannelWrapper("AfterShutdownDelayDuration"),
		InFlightRequestsDrained:    newNamedChannelWrapper("InFlightRequestsDrained"),
		HTTPServerStoppedListening: newNamedChannelWrapper("HTTPServerStoppedListening"),
		HasBeenReady:               newNamedChannelWrapper("HasBeenReady"),
	}
}

func newNamedChannelWrapper(name string) lifecycleSignal {
	return &namedChannelWrapper{
		name: name,
		once: sync.Once{},
		ch:   make(chan struct{}),
	}
}

type namedChannelWrapper struct {
	name string
	once sync.Once
	ch   chan struct{}
}

func (e *namedChannelWrapper) Signal() {
	e.once.Do(func() {
		close(e.ch)
	})
}

func (e *namedChannelWrapper) Signaled() <-chan struct{} {
	return e.ch
}

func (e *namedChannelWrapper) Name() string {
	return e.name
}
<|endoftext|>