From 3bb4fe163c154e6349ed3a438eb137b0b9ace8f3 Mon Sep 17 00:00:00 2001
From: Djordje Lukic <djordje.lukic@docker.com>
Date: Fri, 24 Apr 2020 18:04:32 +0200
Subject: [PATCH 1/8] Add `docker context create` command

This creates a context with a name and a type
---
 Makefile                    |   5 +-
 cli/cmd/context.go          |  83 ++++++++++++++++++
 {cmd => cli/cmd}/example.go |  18 ++--
 cli/main.go                 | 165 ++++++++++++++++++++++++++++++++++++
 cmd/main.go                 | 131 ----------------------------
 context/config.go           |  31 ++++++-
 {cmd => context}/context.go |  16 +---
 context/flags.go            |  56 +++++++-----
 context/store.go            |  65 --------------
 context/store/store.go      | 157 ++++++++++++++++++++++++++++++++++
 context/store/store_test.go |  73 ++++++++++++++++
 go.mod                      |   8 +-
 go.sum                      |  93 +++++++++++++++++++-
 util/util.go                |   2 +-
 14 files changed, 657 insertions(+), 246 deletions(-)
 create mode 100644 cli/cmd/context.go
 rename {cmd => cli/cmd}/example.go (89%)
 create mode 100644 cli/main.go
 delete mode 100644 cmd/main.go
 rename {cmd => context}/context.go (78%)
 delete mode 100644 context/store.go
 create mode 100644 context/store/store.go
 create mode 100644 context/store/store_test.go

diff --git a/Makefile b/Makefile
index d6451f291..4a693bc7a 100644
--- a/Makefile
+++ b/Makefile
@@ -40,7 +40,7 @@ protos:
 	@protoc -I. --go_out=plugins=grpc,paths=source_relative:. ${PROTOS}
 
 cli: protos
-	cd cmd && GOOS=${GOOS} 	GOARCH=${GOARCH} go build -v -o ../bin/docker
+	cd cli && GOOS=${GOOS} GOARCH=${GOARCH} go build -v -o ../bin/docker
 
 example: protos
 	cd example/backend && go build -v -o ../../bin/backend-example
@@ -72,6 +72,9 @@ dxbins: dbins
 	--output type=local,dest=./bin \
 	--target xbins
 
+test:
+	gotestsum ./...
+
 FORCE:
 
 .PHONY: all xall protos example xexample xcli cli bins dbins dxbins dprotos
diff --git a/cli/cmd/context.go b/cli/cmd/context.go
new file mode 100644
index 000000000..1d8547109
--- /dev/null
+++ b/cli/cmd/context.go
@@ -0,0 +1,83 @@
+/*
+	Copyright (c) 2020 Docker Inc.
+
+	Permission is hereby granted, free of charge, to any person
+	obtaining a copy of this software and associated documentation
+	files (the "Software"), to deal in the Software without
+	restriction, including without limitation the rights to use, copy,
+	modify, merge, publish, distribute, sublicense, and/or sell copies
+	of the Software, and to permit persons to whom the Software is
+	furnished to do so, subject to the following conditions:
+
+	The above copyright notice and this permission notice shall be
+	included in all copies or substantial portions of the Software.
+
+	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+	EXPRESS OR IMPLIED,
+	INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+	IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+	HOLDERS BE LIABLE FOR ANY CLAIM,
+	DAMAGES OR OTHER LIABILITY,
+	WHETHER IN AN ACTION OF CONTRACT,
+	TORT OR OTHERWISE,
+	ARISING FROM, OUT OF OR IN CONNECTION WITH
+	THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+package cmd
+
+import (
+	"context"
+
+	"github.com/docker/api/context/store"
+	"github.com/spf13/cobra"
+)
+
+type CliContext struct {
+}
+
+func ContextCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "context",
+		Short: "Manage contexts",
+	}
+
+	cmd.AddCommand(
+		createCommand(),
+	)
+
+	return cmd
+}
+
+type createOpts struct {
+	description string
+}
+
+func createCommand() *cobra.Command {
+	var opts createOpts
+	cmd := &cobra.Command{
+		Use:   "create",
+		Short: "Create a context",
+		Args:  cobra.ExactArgs(2),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runCreate(cmd.Context(), opts, args[0], args[1])
+		},
+	}
+
+	cmd.Flags().StringVar(&opts.description, "description", "", "Description of the context")
+
+	return cmd
+}
+
+func runCreate(ctx context.Context, opts createOpts, name string, contextType string) error {
+	s := store.ContextStore(ctx)
+	return s.Create(name, store.TypeContext{
+		Type:        contextType,
+		Description: opts.description,
+	}, map[string]interface{}{
+		// If we don't set anything here the main docker cli
+		// doesn't know how to read the context any more
+		"docker": CliContext{},
+	})
+}
diff --git a/cmd/example.go b/cli/cmd/example.go
similarity index 89%
rename from cmd/example.go
rename to cli/cmd/example.go
index b73344a31..bc3077325 100644
--- a/cmd/example.go
+++ b/cli/cmd/example.go
@@ -25,7 +25,7 @@
 	THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
 
-package main
+package cmd
 
 import (
 	"context"
@@ -35,20 +35,16 @@ import (
 	"time"
 
 	"github.com/docker/api/client"
-	"github.com/docker/api/util"
-
 	"github.com/golang/protobuf/ptypes/empty"
 	"github.com/pkg/errors"
-	"github.com/urfave/cli/v2"
+	"github.com/spf13/cobra"
 )
 
-var exampleCommand = cli.Command{
-	Name:  "example",
-	Usage: "sample command using backend, to be removed later",
-	Action: func(clix *cli.Context) error {
-		// return information for the current context
-		ctx, cancel := util.NewSigContext()
-		defer cancel()
+var ExampleCommand = cobra.Command{
+	Use:   "example",
+	Short: "sample command using backend, to be removed later",
+	RunE: func(cmd *cobra.Command, args []string) error {
+		ctx := cmd.Context()
 
 		// get our current context
 		ctx = current(ctx)
diff --git a/cli/main.go b/cli/main.go
new file mode 100644
index 000000000..16d019f3d
--- /dev/null
+++ b/cli/main.go
@@ -0,0 +1,165 @@
+/*
+	Copyright (c) 2020 Docker Inc.
+
+	Permission is hereby granted, free of charge, to any person
+	obtaining a copy of this software and associated documentation
+	files (the "Software"), to deal in the Software without
+	restriction, including without limitation the rights to use, copy,
+	modify, merge, publish, distribute, sublicense, and/or sell copies
+	of the Software, and to permit persons to whom the Software is
+	furnished to do so, subject to the following conditions:
+
+	The above copyright notice and this permission notice shall be
+	included in all copies or substantial portions of the Software.
+
+	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+	EXPRESS OR IMPLIED,
+	INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+	IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+	HOLDERS BE LIABLE FOR ANY CLAIM,
+	DAMAGES OR OTHER LIABILITY,
+	WHETHER IN AN ACTION OF CONTRACT,
+	TORT OR OTHERWISE,
+	ARISING FROM, OUT OF OR IN CONNECTION WITH
+	THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+package main
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"os"
+	"os/exec"
+	"path/filepath"
+
+	"github.com/docker/api/cli/cmd"
+	apicontext "github.com/docker/api/context"
+	"github.com/docker/api/context/store"
+	"github.com/docker/api/util"
+	"github.com/sirupsen/logrus"
+	"github.com/spf13/cobra"
+)
+
+type mainOpts struct {
+	apicontext.ContextFlags
+	debug bool
+}
+
+func init() {
+	// initial hack to get the path of the project's bin dir
+	// into the env of this cli for development
+	path, err := filepath.Abs(filepath.Dir(os.Args[0]))
+	if err != nil {
+		log.Fatal(err)
+	}
+	if err := os.Setenv("PATH", fmt.Sprintf("%s:%s", os.Getenv("PATH"), path)); err != nil {
+		panic(err)
+	}
+}
+
+func main() {
+	var opts mainOpts
+	root := &cobra.Command{
+		Use:  "docker",
+		Long: "docker for the 2020s",
+		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+			execMoby(cmd.Context())
+			return nil
+		},
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return cmd.Help()
+		},
+	}
+
+	helpFunc := root.HelpFunc()
+	root.SetHelpFunc(func(cmd *cobra.Command, args []string) {
+		execMoby(cmd.Context())
+		helpFunc(cmd, args)
+	})
+
+	root.PersistentFlags().BoolVarP(&opts.debug, "debug", "d", false, "enable debug output in the logs")
+	opts.AddFlags(root.PersistentFlags())
+
+	// populate the opts with the global flags
+	_ = root.PersistentFlags().Parse(os.Args[1:])
+	if opts.debug {
+		logrus.SetLevel(logrus.DebugLevel)
+	}
+
+	root.AddCommand(
+		cmd.ContextCommand(),
+		&cmd.ExampleCommand,
+	)
+
+	ctx, cancel := util.NewSigContext()
+	defer cancel()
+
+	ctx, err := withCurrentContext(ctx, opts)
+	if err != nil {
+		logrus.Fatal(err)
+	}
+
+	s, err := store.New(opts.Config)
+	if err != nil {
+		logrus.Fatal(err)
+	}
+	ctx = store.WithContextStore(ctx, s)
+
+	if err = root.ExecuteContext(ctx); err != nil {
+		os.Exit(1)
+	}
+}
+
+type currentContextKey struct{}
+
+func withCurrentContext(ctx context.Context, opts mainOpts) (context.Context, error) {
+	config, err := apicontext.LoadConfigFile(opts.Config, "config.json")
+	if err != nil {
+		return ctx, err
+	}
+
+	currentContext := opts.Context
+	if currentContext == "" {
+		currentContext = config.CurrentContext
+	}
+	if currentContext == "" {
+		currentContext = "default"
+	}
+	logrus.Debugf("Current context %q", currentContext)
+	return context.WithValue(ctx, currentContextKey{}, currentContext), nil
+}
+
+// CurrentContext returns the current context name
+func CurrentContext(ctx context.Context) string {
+	cc, _ := ctx.Value(currentContextKey{}).(string)
+	return cc
+}
+
+func execMoby(ctx context.Context) {
+	currentContext := CurrentContext(ctx)
+	s := store.ContextStore(ctx)
+
+	cc, err := s.Get(currentContext)
+	if err != nil {
+		logrus.Fatal(err)
+	}
+	_, ok := cc.Metadata.(store.TypeContext)
+	if !ok {
+		cmd := exec.Command("docker", os.Args[1:]...)
+		cmd.Stdin = os.Stdin
+		cmd.Stdout = os.Stdout
+		cmd.Stderr = os.Stderr
+		if err := cmd.Run(); err != nil {
+			if err != nil {
+				if exiterr, ok := err.(*exec.ExitError); ok {
+					os.Exit(exiterr.ExitCode())
+				}
+				os.Exit(1)
+			}
+		}
+		os.Exit(0)
+	}
+}
diff --git a/cmd/main.go b/cmd/main.go
deleted file mode 100644
index 5bcc5c9c5..000000000
--- a/cmd/main.go
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
-	Copyright (c) 2020 Docker Inc.
-
-	Permission is hereby granted, free of charge, to any person
-	obtaining a copy of this software and associated documentation
-	files (the "Software"), to deal in the Software without
-	restriction, including without limitation the rights to use, copy,
-	modify, merge, publish, distribute, sublicense, and/or sell copies
-	of the Software, and to permit persons to whom the Software is
-	furnished to do so, subject to the following conditions:
-
-	The above copyright notice and this permission notice shall be
-	included in all copies or substantial portions of the Software.
-
-	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-	EXPRESS OR IMPLIED,
-	INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-	IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
-	HOLDERS BE LIABLE FOR ANY CLAIM,
-	DAMAGES OR OTHER LIABILITY,
-	WHETHER IN AN ACTION OF CONTRACT,
-	TORT OR OTHERWISE,
-	ARISING FROM, OUT OF OR IN CONNECTION WITH
-	THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-*/
-
-package main
-
-import (
-	"fmt"
-	"io"
-	"log"
-	"os"
-	"os/exec"
-	"path/filepath"
-	"sort"
-
-	"github.com/docker/api/context"
-	"github.com/sirupsen/logrus"
-	"github.com/urfave/cli/v2"
-)
-
-func init() {
-	// initial hack to get the path of the project's bin dir
-	// into the env of this cli for development
-
-	path, err := filepath.Abs(filepath.Dir(os.Args[0]))
-	if err != nil {
-		log.Fatal(err)
-	}
-	if err := os.Setenv("PATH", fmt.Sprintf("%s:%s", os.Getenv("PATH"), path)); err != nil {
-		panic(err)
-	}
-}
-
-func main() {
-	app := cli.NewApp()
-	app.Name = "docker"
-	app.Usage = "Docker for the 2020s"
-	app.UseShortOptionHandling = true
-	app.EnableBashCompletion = true
-	app.Flags = []cli.Flag{
-		&cli.BoolFlag{
-			Name:  "debug",
-			Usage: "enable debug output in the logs",
-		},
-		&context.ConfigFlag,
-		&context.ContextFlag,
-	}
-
-	// Make a copy of the default HelpPrinter function
-	originalHelpPrinter := cli.HelpPrinter
-	// Change the HelpPrinter function to shell out to the Moby CLI help
-	// when the current context is pointing to Docker engine
-	// else we use the copy of the original HelpPrinter
-	cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) {
-		ctx, err := context.GetContext()
-		if err != nil {
-			logrus.Fatal(err)
-		}
-		if ctx.Metadata.Type == "Moby" {
-			shellOutToDefaultEngine()
-		} else {
-			originalHelpPrinter(w, templ, data)
-		}
-	}
-
-	app.Before = func(clix *cli.Context) error {
-		if clix.Bool("debug") {
-			logrus.SetLevel(logrus.DebugLevel)
-		}
-		ctx, err := context.GetContext()
-		if err != nil {
-			logrus.Fatal(err)
-		}
-		if ctx.Metadata.Type == "Moby" {
-			shellOutToDefaultEngine()
-		}
-		// TODO select backend based on context.Metadata.Type
-		return nil
-	}
-	app.Commands = []*cli.Command{
-		&contextCommand,
-		&exampleCommand,
-	}
-
-	sort.Sort(cli.FlagsByName(app.Flags))
-	sort.Sort(cli.CommandsByName(app.Commands))
-
-	if err := app.Run(os.Args); err != nil {
-		fmt.Fprintln(os.Stderr, err)
-		os.Exit(1)
-	}
-}
-
-func shellOutToDefaultEngine() {
-	cmd := exec.Command("docker", os.Args[1:]...)
-	cmd.Stdin = os.Stdin
-	cmd.Stdout = os.Stdout
-	cmd.Stderr = os.Stderr
-	if err := cmd.Run(); err != nil {
-		if err != nil {
-			if exiterr, ok := err.(*exec.ExitError); ok {
-				os.Exit(exiterr.ExitCode())
-			}
-			os.Exit(1)
-		}
-	}
-	os.Exit(0)
-}
diff --git a/context/config.go b/context/config.go
index 0a858e24c..30227dfa1 100644
--- a/context/config.go
+++ b/context/config.go
@@ -1,3 +1,30 @@
+/*
+	Copyright (c) 2020 Docker Inc.
+
+	Permission is hereby granted, free of charge, to any person
+	obtaining a copy of this software and associated documentation
+	files (the "Software"), to deal in the Software without
+	restriction, including without limitation the rights to use, copy,
+	modify, merge, publish, distribute, sublicense, and/or sell copies
+	of the Software, and to permit persons to whom the Software is
+	furnished to do so, subject to the following conditions:
+
+	The above copyright notice and this permission notice shall be
+	included in all copies or substantial portions of the Software.
+
+	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+	EXPRESS OR IMPLIED,
+	INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+	IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+	HOLDERS BE LIABLE FOR ANY CLAIM,
+	DAMAGES OR OTHER LIABILITY,
+	WHETHER IN AN ACTION OF CONTRACT,
+	TORT OR OTHERWISE,
+	ARISING FROM, OUT OF OR IN CONNECTION WITH
+	THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
 package context
 
 import (
@@ -7,8 +34,8 @@ import (
 	"path/filepath"
 )
 
-func LoadConfigFile() (*ConfigFile, error) {
-	filename := filepath.Join(ConfigDir, ConfigFileName)
+func LoadConfigFile(configDir string, configFileName string) (*ConfigFile, error) {
+	filename := filepath.Join(configDir, configFileName)
 	configFile := &ConfigFile{
 		Filename: filename,
 	}
diff --git a/cmd/context.go b/context/context.go
similarity index 78%
rename from cmd/context.go
rename to context/context.go
index ccdd61f03..100ad3111 100644
--- a/cmd/context.go
+++ b/context/context.go
@@ -25,18 +25,8 @@
 	THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
 
-package main
+package context
 
-import (
-	"github.com/pkg/errors"
-	"github.com/urfave/cli/v2"
-)
-
-var contextCommand = cli.Command{
-	Name:  "context",
-	Usage: "manage contexts",
-	Action: func(clix *cli.Context) error {
-		// return information for the current context
-		return errors.New("Error : To be implemented")
-	},
+type TypeContext struct {
+	Type string
 }
diff --git a/context/flags.go b/context/flags.go
index f875a9fc9..3c671cdea 100644
--- a/context/flags.go
+++ b/context/flags.go
@@ -1,10 +1,38 @@
+/*
+	Copyright (c) 2020 Docker Inc.
+
+	Permission is hereby granted, free of charge, to any person
+	obtaining a copy of this software and associated documentation
+	files (the "Software"), to deal in the Software without
+	restriction, including without limitation the rights to use, copy,
+	modify, merge, publish, distribute, sublicense, and/or sell copies
+	of the Software, and to permit persons to whom the Software is
+	furnished to do so, subject to the following conditions:
+
+	The above copyright notice and this permission notice shall be
+	included in all copies or substantial portions of the Software.
+
+	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+	EXPRESS OR IMPLIED,
+	INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+	IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+	HOLDERS BE LIABLE FOR ANY CLAIM,
+	DAMAGES OR OTHER LIABILITY,
+	WHETHER IN AN ACTION OF CONTRACT,
+	TORT OR OTHERWISE,
+	ARISING FROM, OUT OF OR IN CONNECTION WITH
+	THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
 package context
 
 import (
+	"os"
 	"path/filepath"
 
 	"github.com/mitchellh/go-homedir"
-	"github.com/urfave/cli/v2"
+	"github.com/spf13/pflag"
 )
 
 const (
@@ -13,25 +41,15 @@ const (
 	configFileDir  = ".docker"
 )
 
-var (
-	ConfigDir   string
-	ContextName string
-	ConfigFlag  = cli.StringFlag{
-		Name:        "config",
-		Usage:       "Location of client config files `DIRECTORY`",
-		EnvVars:      []string{"DOCKER_CONFIG"},
-		Value:       filepath.Join(home(), configFileDir),
-		Destination: &ConfigDir,
-	}
+type ContextFlags struct {
+	Config  string
+	Context string
+}
 
-	ContextFlag = cli.StringFlag{
-		Name:        "context",
-		Aliases: 	 []string{"c"},
-		Usage:       "Name of the context `CONTEXT` to use to connect to the daemon (overrides DOCKER_HOST env var and default context set with \"docker context use\")",
-		EnvVars:      []string{"DOCKER_CONTEXT"},
-		Destination: &ContextName,
-	}
-)
+func (c *ContextFlags) AddFlags(flags *pflag.FlagSet) {
+	flags.StringVar(&c.Config, "config", filepath.Join(home(), configFileDir), "Location of the client config files `DIRECTORY`")
+	flags.StringVarP(&c.Context, "context", "c", os.Getenv("DOCKER_CONTEXT"), "context")
+}
 
 func home() string {
 	home, _ := homedir.Dir()
diff --git a/context/store.go b/context/store.go
deleted file mode 100644
index 8c298fd28..000000000
--- a/context/store.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package context
-
-import (
-	"encoding/json"
-	"io/ioutil"
-	"path/filepath"
-
-	"github.com/opencontainers/go-digest"
-)
-
-const (
-	contextsDir = "contexts"
-	metadataDir = "meta"
-	metaFile    = "meta.json"
-)
-
-// ContextStoreDir returns the directory the docker contexts are stored in
-func ContextStoreDir() string {
-	return filepath.Join(ConfigDir, contextsDir)
-}
-
-type Metadata struct {
-	Name      string                 `json:",omitempty"`
-	Metadata  TypeContext            `json:",omitempty"`
-	Endpoints map[string]interface{} `json:",omitempty"`
-}
-
-type TypeContext struct {
-	Type string
-}
-
-func GetContext() (*Metadata, error) {
-	config, err := LoadConfigFile()
-	if err != nil {
-		return nil, err
-	}
-	r := &Metadata{
-		Endpoints: make(map[string]interface{}),
-	}
-
-	if ContextName == "" {
-		ContextName = config.CurrentContext
-	}
-	if ContextName == "" || ContextName == "default" {
-		r.Metadata.Type = "Moby"
-		return r, nil
-	}
-
-	meta := filepath.Join(ConfigDir, contextsDir, metadataDir, contextdirOf(ContextName), metaFile)
-	bytes, err := ioutil.ReadFile(meta)
-	if err != nil {
-		return nil, err
-	}
-
-	if err := json.Unmarshal(bytes, r); err != nil {
-		return r, err
-	}
-
-	r.Name = ContextName
-	return r, nil
-}
-
-func contextdirOf(name string) string {
-	return digest.FromString(name).Encoded()
-}
diff --git a/context/store/store.go b/context/store/store.go
new file mode 100644
index 000000000..8750f386f
--- /dev/null
+++ b/context/store/store.go
@@ -0,0 +1,157 @@
+/*
+	Copyright (c) 2020 Docker Inc.
+
+	Permission is hereby granted, free of charge, to any person
+	obtaining a copy of this software and associated documentation
+	files (the "Software"), to deal in the Software without
+	restriction, including without limitation the rights to use, copy,
+	modify, merge, publish, distribute, sublicense, and/or sell copies
+	of the Software, and to permit persons to whom the Software is
+	furnished to do so, subject to the following conditions:
+
+	The above copyright notice and this permission notice shall be
+	included in all copies or substantial portions of the Software.
+
+	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+	EXPRESS OR IMPLIED,
+	INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+	IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+	HOLDERS BE LIABLE FOR ANY CLAIM,
+	DAMAGES OR OTHER LIABILITY,
+	WHETHER IN AN ACTION OF CONTRACT,
+	TORT OR OTHERWISE,
+	ARISING FROM, OUT OF OR IN CONNECTION WITH
+	THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+package store
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"reflect"
+
+	"github.com/opencontainers/go-digest"
+)
+
+const (
+	contextsDir = "contexts"
+	metadataDir = "meta"
+	metaFile    = "meta.json"
+)
+
+type contextStoreKey struct{}
+
+func WithContextStore(ctx context.Context, store Store) context.Context {
+	return context.WithValue(ctx, contextStoreKey{}, store)
+}
+
+func ContextStore(ctx context.Context) Store {
+	s, _ := ctx.Value(contextStoreKey{}).(Store)
+	return s
+}
+
+type Store interface {
+	Get(name string) (*Metadata, error)
+	Create(name string, data interface{}, endpoints map[string]interface{}) error
+}
+
+type store struct {
+	root string
+}
+
+// New returns a configured context store
+func New(root string) (Store, error) {
+	cd := filepath.Join(root, contextsDir)
+	if _, err := os.Stat(cd); os.IsNotExist(err) {
+		if err = os.Mkdir(cd, 0755); err != nil {
+			return nil, err
+		}
+	}
+	m := filepath.Join(cd, metadataDir)
+	if _, err := os.Stat(m); os.IsNotExist(err) {
+		if err = os.Mkdir(m, 0755); err != nil {
+			return nil, err
+		}
+	}
+
+	return &store{
+		root: root,
+	}, nil
+}
+
+// Get returns the context with the given name
+func (s *store) Get(name string) (*Metadata, error) {
+	if name == "default" {
+		return &Metadata{}, nil
+	}
+
+	meta := filepath.Join(s.root, contextsDir, metadataDir, contextdirOf(name), metaFile)
+	bytes, err := ioutil.ReadFile(meta)
+	if err != nil {
+		return nil, err
+	}
+
+	r := &Metadata{
+		Endpoints: make(map[string]interface{}),
+	}
+
+	typed := getter()
+	if err := json.Unmarshal(bytes, typed); err != nil {
+		return r, err
+	}
+
+	r.Metadata = reflect.ValueOf(typed).Elem().Interface()
+
+	return r, nil
+}
+
+func (s *store) Create(name string, data interface{}, endpoints map[string]interface{}) error {
+	dir := contextdirOf(name)
+	metaDir := filepath.Join(s.root, contextsDir, metadataDir, dir)
+	if _, err := os.Stat(metaDir); !os.IsNotExist(err) {
+		return fmt.Errorf("Context %q already exists", name)
+	}
+
+	err := os.Mkdir(metaDir, 0755)
+	if err != nil {
+		return err
+	}
+
+	meta := Metadata{
+		Name:      name,
+		Metadata:  data,
+		Endpoints: endpoints,
+	}
+
+	bytes, err := json.Marshal(&meta)
+	if err != nil {
+		return err
+	}
+
+	return ioutil.WriteFile(filepath.Join(metaDir, metaFile), bytes, 0644)
+}
+
+func contextdirOf(name string) string {
+	return digest.FromString(name).Encoded()
+}
+
+type Metadata struct {
+	Name      string                 `json:",omitempty"`
+	Metadata  interface{}            `json:",omitempty"`
+	Endpoints map[string]interface{} `json:",omitempty"`
+}
+
+type TypeContext struct {
+	Type        string
+	Description string
+}
+
+func getter() interface{} {
+	return &TypeContext{}
+}
diff --git a/context/store/store_test.go b/context/store/store_test.go
new file mode 100644
index 000000000..a5db64d9c
--- /dev/null
+++ b/context/store/store_test.go
@@ -0,0 +1,73 @@
+/*
+	Copyright (c) 2020 Docker Inc.
+
+	Permission is hereby granted, free of charge, to any person
+	obtaining a copy of this software and associated documentation
+	files (the "Software"), to deal in the Software without
+	restriction, including without limitation the rights to use, copy,
+	modify, merge, publish, distribute, sublicense, and/or sell copies
+	of the Software, and to permit persons to whom the Software is
+	furnished to do so, subject to the following conditions:
+
+	The above copyright notice and this permission notice shall be
+	included in all copies or substantial portions of the Software.
+
+	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+	EXPRESS OR IMPLIED,
+	INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+	IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+	HOLDERS BE LIABLE FOR ANY CLAIM,
+	DAMAGES OR OTHER LIABILITY,
+	WHETHER IN AN ACTION OF CONTRACT,
+	TORT OR OTHERWISE,
+	ARISING FROM, OUT OF OR IN CONNECTION WITH
+	THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+package store
+
+import (
+	_ "crypto/sha256"
+	"io/ioutil"
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func setup(t *testing.T, cb func(*testing.T, Store)) {
+	dir, err := ioutil.TempDir("", "store")
+	assert.Nil(t, err)
+	defer os.RemoveAll(dir)
+
+	store, err := New(dir)
+	assert.Nil(t, err)
+
+	cb(t, store)
+}
+
+func TestGetUnknown(t *testing.T) {
+	setup(t, func(t *testing.T, store Store) {
+		meta, err := store.Get("unknown")
+		assert.Nil(t, meta)
+		assert.Error(t, err)
+	})
+}
+
+func TestCreate(t *testing.T) {
+	setup(t, func(t *testing.T, store Store) {
+		err := store.Create("test", nil, nil)
+		assert.Nil(t, err)
+	})
+}
+
+func TestGet(t *testing.T) {
+	setup(t, func(t *testing.T, store Store) {
+		err := store.Create("test", nil, nil)
+		assert.Nil(t, err)
+		meta, err := store.Get("test")
+		assert.Nil(t, err)
+		assert.NotNil(t, meta)
+	})
+}
diff --git a/go.mod b/go.mod
index cf47268c5..226add973 100644
--- a/go.mod
+++ b/go.mod
@@ -10,7 +10,13 @@ require (
 	github.com/pkg/errors v0.9.1
 	github.com/prometheus/client_golang v1.5.1 // indirect
 	github.com/sirupsen/logrus v1.5.0
+	github.com/spf13/cobra v1.0.0
+	github.com/spf13/pflag v1.0.5
+	github.com/stretchr/testify v1.5.1
 	github.com/urfave/cli/v2 v2.2.0
-	google.golang.org/grpc v1.28.1
+	golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 // indirect
+	golang.org/x/text v0.3.2 // indirect
+	google.golang.org/grpc v1.29.1
 	google.golang.org/protobuf v1.21.0
+	gopkg.in/yaml.v2 v2.2.8 // indirect
 )
diff --git a/go.sum b/go.sum
index f2dee35bb..d82778e6a 100644
--- a/go.sum
+++ b/go.sum
@@ -1,33 +1,51 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
+github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -39,17 +57,29 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
 github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
 github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ=
 github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
 github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
@@ -58,17 +88,26 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
 github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
+github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -76,6 +115,7 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
 github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
 github.com/prometheus/client_golang v1.5.1 h1:bdHYieyGlH+6OLEk2YQha8THib30KP0/yD0YH9m6xcA=
 github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
@@ -84,13 +124,18 @@ github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
 github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U=
 github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8=
 github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
+github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
@@ -99,15 +144,42 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q=
 github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
+github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
+github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
+github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
+github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
 github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4=
 github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -116,19 +188,25 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU=
 golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI=
+golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
@@ -137,6 +215,11 @@ golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgm
 golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
@@ -149,10 +232,11 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
-google.golang.org/grpc v1.28.1 h1:C1QC6KzgSiLyBabDi87BbjaGreoRgGUF5nOyvfrAZ1k=
-google.golang.org/grpc v1.28.1/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
+google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4=
+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -161,12 +245,17 @@ google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zim
 google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c=
 gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/util/util.go b/util/util.go
index 9ad70d3d6..92d337583 100644
--- a/util/util.go
+++ b/util/util.go
@@ -1,5 +1,5 @@
 /*
-	Copyright (c) 2019 Docker Inc.
+	Copyright (c) 2020 Docker Inc.
 
 	Permission is hereby granted, free of charge, to any person
 	obtaining a copy of this software and associated documentation

From e2c7370a82fc2895504a7291c3cddaabfa4bc677 Mon Sep 17 00:00:00 2001
From: Djordje Lukic <djordje.lukic@docker.com>
Date: Sun, 26 Apr 2020 22:07:50 +0200
Subject: [PATCH 2/8] Implement context list

---
 cli/cmd/context.go          | 37 ++++++++++++++++++++++
 context/context.go          | 32 -------------------
 context/store/store.go      | 61 ++++++++++++++++++++++++++++++++-----
 context/store/store_test.go | 31 ++++++++++++++++++-
 go.mod                      |  2 +-
 go.sum                      |  4 +++
 6 files changed, 125 insertions(+), 42 deletions(-)
 delete mode 100644 context/context.go

diff --git a/cli/cmd/context.go b/cli/cmd/context.go
index 1d8547109..8e8041fe3 100644
--- a/cli/cmd/context.go
+++ b/cli/cmd/context.go
@@ -29,6 +29,9 @@ package cmd
 
 import (
 	"context"
+	"fmt"
+	"os"
+	"text/tabwriter"
 
 	"github.com/docker/api/context/store"
 	"github.com/spf13/cobra"
@@ -45,6 +48,7 @@ func ContextCommand() *cobra.Command {
 
 	cmd.AddCommand(
 		createCommand(),
+		listCommand(),
 	)
 
 	return cmd
@@ -70,6 +74,17 @@ func createCommand() *cobra.Command {
 	return cmd
 }
 
+func listCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:     "list",
+		Aliases: []string{"ls"},
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runList(cmd.Context())
+		},
+	}
+	return cmd
+}
+
 func runCreate(ctx context.Context, opts createOpts, name string, contextType string) error {
 	s := store.ContextStore(ctx)
 	return s.Create(name, store.TypeContext{
@@ -81,3 +96,25 @@ func runCreate(ctx context.Context, opts createOpts, name string, contextType st
 		"docker": CliContext{},
 	})
 }
+
+func runList(ctx context.Context) error {
+	s := store.ContextStore(ctx)
+	contexts, err := s.List()
+	if err != nil {
+		return err
+	}
+
+	w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
+	fmt.Fprintln(w, "NAME\tDESCRIPTION\tTYPE")
+	format := "%s\t%s\t%s\n"
+
+	for _, c := range contexts {
+		meta, ok := c.Metadata.(store.TypeContext)
+		if !ok {
+			return fmt.Errorf("Unable to list contexts, context %q is not valid", c.Name)
+		}
+		fmt.Fprintf(w, format, c.Name, meta.Description, meta.Type)
+	}
+
+	return w.Flush()
+}
diff --git a/context/context.go b/context/context.go
deleted file mode 100644
index 100ad3111..000000000
--- a/context/context.go
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
-	Copyright (c) 2020 Docker Inc.
-
-	Permission is hereby granted, free of charge, to any person
-	obtaining a copy of this software and associated documentation
-	files (the "Software"), to deal in the Software without
-	restriction, including without limitation the rights to use, copy,
-	modify, merge, publish, distribute, sublicense, and/or sell copies
-	of the Software, and to permit persons to whom the Software is
-	furnished to do so, subject to the following conditions:
-
-	The above copyright notice and this permission notice shall be
-	included in all copies or substantial portions of the Software.
-
-	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-	EXPRESS OR IMPLIED,
-	INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-	IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
-	HOLDERS BE LIABLE FOR ANY CLAIM,
-	DAMAGES OR OTHER LIABILITY,
-	WHETHER IN AN ACTION OF CONTRACT,
-	TORT OR OTHERWISE,
-	ARISING FROM, OUT OF OR IN CONNECTION WITH
-	THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-*/
-
-package context
-
-type TypeContext struct {
-	Type string
-}
diff --git a/context/store/store.go b/context/store/store.go
index 8750f386f..d693b90a1 100644
--- a/context/store/store.go
+++ b/context/store/store.go
@@ -56,9 +56,16 @@ func ContextStore(ctx context.Context) Store {
 	return s
 }
 
+// Store
 type Store interface {
+	// Get returns the context with with name, it returns an error if the
+	// context doesn't exist
 	Get(name string) (*Metadata, error)
+	// Create creates a new context, it returns an error if a context with the
+	// same name exists already.
 	Create(name string, data interface{}, endpoints map[string]interface{}) error
+	// List returns the list of created contexts
+	List() ([]*Metadata, error)
 }
 
 type store struct {
@@ -92,23 +99,33 @@ func (s *store) Get(name string) (*Metadata, error) {
 	}
 
 	meta := filepath.Join(s.root, contextsDir, metadataDir, contextdirOf(name), metaFile)
+	return read(meta)
+}
+
+func read(meta string) (*Metadata, error) {
 	bytes, err := ioutil.ReadFile(meta)
 	if err != nil {
 		return nil, err
 	}
 
-	r := &Metadata{
-		Endpoints: make(map[string]interface{}),
+	var r untypedContextMetadata
+	if err := json.Unmarshal(bytes, &r); err != nil {
+		return nil, err
+	}
+
+	result := &Metadata{
+		Name:      r.Name,
+		Endpoints: r.Endpoints,
 	}
 
 	typed := getter()
-	if err := json.Unmarshal(bytes, typed); err != nil {
-		return r, err
+	if err := json.Unmarshal(r.Metadata, typed); err != nil {
+		return nil, err
 	}
 
-	r.Metadata = reflect.ValueOf(typed).Elem().Interface()
+	result.Metadata = reflect.ValueOf(typed).Elem().Interface()
 
-	return r, nil
+	return result, nil
 }
 
 func (s *store) Create(name string, data interface{}, endpoints map[string]interface{}) error {
@@ -137,6 +154,28 @@ func (s *store) Create(name string, data interface{}, endpoints map[string]inter
 	return ioutil.WriteFile(filepath.Join(metaDir, metaFile), bytes, 0644)
 }
 
+func (s *store) List() ([]*Metadata, error) {
+	root := filepath.Join(s.root, contextsDir, metadataDir)
+	c, err := ioutil.ReadDir(root)
+	if err != nil {
+		return nil, err
+	}
+
+	var result []*Metadata
+	for _, fi := range c {
+		if fi.IsDir() {
+			meta := filepath.Join(root, fi.Name(), metaFile)
+			r, err := read(meta)
+			if err != nil {
+				return nil, err
+			}
+			result = append(result, r)
+		}
+	}
+
+	return result, nil
+}
+
 func contextdirOf(name string) string {
 	return digest.FromString(name).Encoded()
 }
@@ -147,9 +186,15 @@ type Metadata struct {
 	Endpoints map[string]interface{} `json:",omitempty"`
 }
 
+type untypedContextMetadata struct {
+	Metadata  json.RawMessage        `json:"metadata,omitempty"`
+	Endpoints map[string]interface{} `json:"endpoints,omitempty"`
+	Name      string                 `json:"name,omitempty"`
+}
+
 type TypeContext struct {
-	Type        string
-	Description string
+	Type        string `json:",omitempty"`
+	Description string `json:",omitempty"`
 }
 
 func getter() interface{} {
diff --git a/context/store/store_test.go b/context/store/store_test.go
index a5db64d9c..43c96b47c 100644
--- a/context/store/store_test.go
+++ b/context/store/store_test.go
@@ -29,6 +29,7 @@ package store
 
 import (
 	_ "crypto/sha256"
+	"fmt"
 	"io/ioutil"
 	"os"
 	"testing"
@@ -64,10 +65,38 @@ func TestCreate(t *testing.T) {
 
 func TestGet(t *testing.T) {
 	setup(t, func(t *testing.T, store Store) {
-		err := store.Create("test", nil, nil)
+		err := store.Create("test", TypeContext{
+			Type:        "type",
+			Description: "description",
+		}, nil)
 		assert.Nil(t, err)
+
 		meta, err := store.Get("test")
 		assert.Nil(t, err)
 		assert.NotNil(t, meta)
+		assert.Equal(t, "test", meta.Name)
+
+		m, ok := meta.Metadata.(TypeContext)
+		assert.Equal(t, ok, true)
+		fmt.Printf("%#v\n", meta)
+		assert.Equal(t, "description", m.Description)
+		assert.Equal(t, "type", m.Type)
+	})
+}
+
+func TestList(t *testing.T) {
+	setup(t, func(t *testing.T, store Store) {
+		err := store.Create("test1", TypeContext{}, nil)
+		assert.Nil(t, err)
+
+		err = store.Create("test2", TypeContext{}, nil)
+		assert.Nil(t, err)
+
+		contexts, err := store.List()
+		assert.Nil(t, err)
+
+		assert.Equal(t, len(contexts), 2)
+		assert.Equal(t, contexts[0].Name, "test1")
+		assert.Equal(t, contexts[1].Name, "test2")
 	})
 }
diff --git a/go.mod b/go.mod
index 226add973..47237d188 100644
--- a/go.mod
+++ b/go.mod
@@ -14,7 +14,7 @@ require (
 	github.com/spf13/pflag v1.0.5
 	github.com/stretchr/testify v1.5.1
 	github.com/urfave/cli/v2 v2.2.0
-	golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 // indirect
+	golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 // indirect
 	golang.org/x/text v0.3.2 // indirect
 	google.golang.org/grpc v1.29.1
 	google.golang.org/protobuf v1.21.0
diff --git a/go.sum b/go.sum
index d82778e6a..8568eec5a 100644
--- a/go.sum
+++ b/go.sum
@@ -197,6 +197,8 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowK
 golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI=
 golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U=
+golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -213,6 +215,8 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/p
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU=
 golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=

From 32da9e65e8a588d60cf8a8007a42fc403696f6f1 Mon Sep 17 00:00:00 2001
From: Djordje Lukic <djordje.lukic@docker.com>
Date: Sun, 26 Apr 2020 22:13:35 +0200
Subject: [PATCH 3/8] Only execute moby if the command is not a context command

---
 cli/main.go | 22 ++++++++++++++++++++--
 1 file changed, 20 insertions(+), 2 deletions(-)

diff --git a/cli/main.go b/cli/main.go
index 16d019f3d..c7f6bcaf4 100644
--- a/cli/main.go
+++ b/cli/main.go
@@ -60,13 +60,25 @@ func init() {
 	}
 }
 
+func isContextCommand(cmd *cobra.Command) bool {
+	if cmd == nil {
+		return false
+	}
+	if cmd.Name() == "context" {
+		return true
+	}
+	return isContextCommand(cmd.Parent())
+}
+
 func main() {
 	var opts mainOpts
 	root := &cobra.Command{
 		Use:  "docker",
 		Long: "docker for the 2020s",
 		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
-			execMoby(cmd.Context())
+			if !isContextCommand(cmd) {
+				execMoby(cmd.Context())
+			}
 			return nil
 		},
 		RunE: func(cmd *cobra.Command, args []string) error {
@@ -76,7 +88,9 @@ func main() {
 
 	helpFunc := root.HelpFunc()
 	root.SetHelpFunc(func(cmd *cobra.Command, args []string) {
-		execMoby(cmd.Context())
+		if !isContextCommand(cmd) {
+			execMoby(cmd.Context())
+		}
 		helpFunc(cmd, args)
 	})
 
@@ -128,7 +142,9 @@ func withCurrentContext(ctx context.Context, opts mainOpts) (context.Context, er
 	if currentContext == "" {
 		currentContext = "default"
 	}
+
 	logrus.Debugf("Current context %q", currentContext)
+
 	return context.WithValue(ctx, currentContextKey{}, currentContext), nil
 }
 
@@ -146,6 +162,8 @@ func execMoby(ctx context.Context) {
 	if err != nil {
 		logrus.Fatal(err)
 	}
+	// Only run original docker command if the current context is not
+	// ours.
 	_, ok := cc.Metadata.(store.TypeContext)
 	if !ok {
 		cmd := exec.Command("docker", os.Args[1:]...)

From cdff00d571e5fcf3eceed71cc4395e046d994373 Mon Sep 17 00:00:00 2001
From: Djordje Lukic <djordje.lukic@docker.com>
Date: Mon, 27 Apr 2020 10:17:10 +0200
Subject: [PATCH 4/8] Run tests inside a container

---
 Dockerfile | 6 +++++-
 Makefile   | 4 ++++
 2 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/Dockerfile b/Dockerfile
index a30b3538a..bea06624c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -10,7 +10,8 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
     git \
     protobuf-compiler \
     libprotobuf-dev
-RUN go get github.com/golang/protobuf/protoc-gen-go
+RUN go get github.com/golang/protobuf/protoc-gen-go && \
+    go get gotest.tools/gotestsum
 WORKDIR ${PWD}
 ADD go.* ${PWD}
 RUN go mod download
@@ -25,6 +26,9 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
     GOARCH=${TARGET_ARCH} \
     make bins
 
+FROM make-protos as make-test
+RUN make test
+
 FROM make-protos AS make-xbins
 RUN --mount=type=cache,target=/root/.cache/go-build \
     make xbins
diff --git a/Makefile b/Makefile
index 4a693bc7a..7286a142b 100644
--- a/Makefile
+++ b/Makefile
@@ -72,6 +72,10 @@ dxbins: dbins
 	--output type=local,dest=./bin \
 	--target xbins
 
+dtest:
+	docker build . \
+	--target make-test
+
 test:
 	gotestsum ./...
 

From 10bc4b93f62f460f88f6673f15c96aef2c9a7a30 Mon Sep 17 00:00:00 2001
From: Djordje Lukic <djordje.lukic@docker.com>
Date: Mon, 27 Apr 2020 11:32:16 +0200
Subject: [PATCH 5/8] Call moby if the command is unknown

Will also check if the context is an original docker context
---
 cli/main.go                 | 10 ++++++++--
 context/store/store.go      |  4 ++--
 context/store/store_test.go |  2 --
 3 files changed, 10 insertions(+), 6 deletions(-)

diff --git a/cli/main.go b/cli/main.go
index c7f6bcaf4..418765501 100644
--- a/cli/main.go
+++ b/cli/main.go
@@ -34,6 +34,7 @@ import (
 	"os"
 	"os/exec"
 	"path/filepath"
+	"strings"
 
 	"github.com/docker/api/cli/cmd"
 	apicontext "github.com/docker/api/context"
@@ -73,8 +74,9 @@ func isContextCommand(cmd *cobra.Command) bool {
 func main() {
 	var opts mainOpts
 	root := &cobra.Command{
-		Use:  "docker",
-		Long: "docker for the 2020s",
+		Use:           "docker",
+		Long:          "docker for the 2020s",
+		SilenceErrors: true,
 		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
 			if !isContextCommand(cmd) {
 				execMoby(cmd.Context())
@@ -123,6 +125,10 @@ func main() {
 	ctx = store.WithContextStore(ctx, s)
 
 	if err = root.ExecuteContext(ctx); err != nil {
+		if strings.Contains(err.Error(), "unknown command") {
+			execMoby(ctx)
+		}
+		fmt.Println(err)
 		os.Exit(1)
 	}
 }
diff --git a/context/store/store.go b/context/store/store.go
index d693b90a1..6f864d978 100644
--- a/context/store/store.go
+++ b/context/store/store.go
@@ -58,8 +58,8 @@ func ContextStore(ctx context.Context) Store {
 
 // Store
 type Store interface {
-	// Get returns the context with with name, it returns an error if the
-	// context doesn't exist
+	// Get returns the context with name, it returns an error if the  context
+	// doesn't exist
 	Get(name string) (*Metadata, error)
 	// Create creates a new context, it returns an error if a context with the
 	// same name exists already.
diff --git a/context/store/store_test.go b/context/store/store_test.go
index 43c96b47c..fd331695c 100644
--- a/context/store/store_test.go
+++ b/context/store/store_test.go
@@ -29,7 +29,6 @@ package store
 
 import (
 	_ "crypto/sha256"
-	"fmt"
 	"io/ioutil"
 	"os"
 	"testing"
@@ -78,7 +77,6 @@ func TestGet(t *testing.T) {
 
 		m, ok := meta.Metadata.(TypeContext)
 		assert.Equal(t, ok, true)
-		fmt.Printf("%#v\n", meta)
 		assert.Equal(t, "description", m.Description)
 		assert.Equal(t, "type", m.Type)
 	})

From 756836ffabd5c64b67372847a684d62f57808ba4 Mon Sep 17 00:00:00 2001
From: Djordje Lukic <djordje.lukic@docker.com>
Date: Mon, 27 Apr 2020 11:45:23 +0200
Subject: [PATCH 6/8] Use testify/suite and testify/require

---
 context/store/store_test.go | 101 +++++++++++++++++++-----------------
 go.mod                      |   1 +
 go.sum                      |   2 +
 3 files changed, 57 insertions(+), 47 deletions(-)

diff --git a/context/store/store_test.go b/context/store/store_test.go
index fd331695c..b853de4fc 100644
--- a/context/store/store_test.go
+++ b/context/store/store_test.go
@@ -34,67 +34,74 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"github.com/stretchr/testify/suite"
 )
 
-func setup(t *testing.T, cb func(*testing.T, Store)) {
+type StoreTestSuite struct {
+	suite.Suite
+	store Store
+	dir   string
+}
+
+func (suite *StoreTestSuite) BeforeTest(suiteName, testName string) {
 	dir, err := ioutil.TempDir("", "store")
-	assert.Nil(t, err)
-	defer os.RemoveAll(dir)
+	require.Nil(suite.T(), err)
 
 	store, err := New(dir)
-	assert.Nil(t, err)
+	require.Nil(suite.T(), err)
 
-	cb(t, store)
+	suite.dir = dir
+	suite.store = store
 }
 
-func TestGetUnknown(t *testing.T) {
-	setup(t, func(t *testing.T, store Store) {
-		meta, err := store.Get("unknown")
-		assert.Nil(t, meta)
-		assert.Error(t, err)
-	})
+func (suite *StoreTestSuite) AfterTest(suiteName, testName string) {
+	os.RemoveAll(suite.dir)
 }
 
-func TestCreate(t *testing.T) {
-	setup(t, func(t *testing.T, store Store) {
-		err := store.Create("test", nil, nil)
-		assert.Nil(t, err)
-	})
+func (suite *StoreTestSuite) TestCreate() {
+	err := suite.store.Create("test", nil, nil)
+	assert.Nil(suite.T(), err)
 }
 
-func TestGet(t *testing.T) {
-	setup(t, func(t *testing.T, store Store) {
-		err := store.Create("test", TypeContext{
-			Type:        "type",
-			Description: "description",
-		}, nil)
-		assert.Nil(t, err)
-
-		meta, err := store.Get("test")
-		assert.Nil(t, err)
-		assert.NotNil(t, meta)
-		assert.Equal(t, "test", meta.Name)
-
-		m, ok := meta.Metadata.(TypeContext)
-		assert.Equal(t, ok, true)
-		assert.Equal(t, "description", m.Description)
-		assert.Equal(t, "type", m.Type)
-	})
+func (suite *StoreTestSuite) TestGetUnknown() {
+	meta, err := suite.store.Get("unknown")
+	assert.Nil(suite.T(), meta)
+	assert.Error(suite.T(), err)
 }
 
-func TestList(t *testing.T) {
-	setup(t, func(t *testing.T, store Store) {
-		err := store.Create("test1", TypeContext{}, nil)
-		assert.Nil(t, err)
+func (suite *StoreTestSuite) TestGet() {
+	err := suite.store.Create("test", TypeContext{
+		Type:        "type",
+		Description: "description",
+	}, nil)
+	assert.Nil(suite.T(), err)
 
-		err = store.Create("test2", TypeContext{}, nil)
-		assert.Nil(t, err)
+	meta, err := suite.store.Get("test")
+	assert.Nil(suite.T(), err)
+	assert.NotNil(suite.T(), meta)
+	assert.Equal(suite.T(), "test", meta.Name)
 
-		contexts, err := store.List()
-		assert.Nil(t, err)
-
-		assert.Equal(t, len(contexts), 2)
-		assert.Equal(t, contexts[0].Name, "test1")
-		assert.Equal(t, contexts[1].Name, "test2")
-	})
+	m, ok := meta.Metadata.(TypeContext)
+	assert.Equal(suite.T(), ok, true)
+	assert.Equal(suite.T(), "description", m.Description)
+	assert.Equal(suite.T(), "type", m.Type)
+}
+func (suite *StoreTestSuite) TestList() {
+	err := suite.store.Create("test1", TypeContext{}, nil)
+	assert.Nil(suite.T(), err)
+
+	err = suite.store.Create("test2", TypeContext{}, nil)
+	assert.Nil(suite.T(), err)
+
+	contexts, err := suite.store.List()
+	assert.Nil(suite.T(), err)
+
+	require.Equal(suite.T(), len(contexts), 2)
+	assert.Equal(suite.T(), contexts[0].Name, "test1")
+	assert.Equal(suite.T(), contexts[1].Name, "test2")
+}
+
+func TestExampleTestSuite(t *testing.T) {
+	suite.Run(t, new(StoreTestSuite))
 }
diff --git a/go.mod b/go.mod
index 47237d188..dc204573c 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module github.com/docker/api
 go 1.13
 
 require (
+	github.com/coreos/etcd v3.3.10+incompatible
 	github.com/golang/protobuf v1.4.0
 	github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
 	github.com/mitchellh/go-homedir v1.1.0
diff --git a/go.sum b/go.sum
index 8568eec5a..c9c3e8ee3 100644
--- a/go.sum
+++ b/go.sum
@@ -18,6 +18,7 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04=
 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
@@ -73,6 +74,7 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=

From e6597d61394bd4dca86e00452dd46b7c371d0a94 Mon Sep 17 00:00:00 2001
From: Djordje Lukic <djordje.lukic@docker.com>
Date: Mon, 27 Apr 2020 14:04:53 +0200
Subject: [PATCH 7/8] Don't cd into a directory before building

We pass the directory to build to the `go build` command
---
 Makefile                    | 14 +++++++-------
 cli/cmd/context.go          |  3 ++-
 context/store/store_test.go |  1 +
 3 files changed, 10 insertions(+), 8 deletions(-)

diff --git a/Makefile b/Makefile
index 7286a142b..87186234e 100644
--- a/Makefile
+++ b/Makefile
@@ -40,20 +40,20 @@ protos:
 	@protoc -I. --go_out=plugins=grpc,paths=source_relative:. ${PROTOS}
 
 cli: protos
-	cd cli && GOOS=${GOOS} GOARCH=${GOARCH} go build -v -o ../bin/docker
+	GOOS=${GOOS} GOARCH=${GOARCH} go build -v -o bin/docker ./cli
 
 example: protos
 	cd example/backend && go build -v -o ../../bin/backend-example
 
 xcli: cli
-	cd cmd && GOOS=linux 	GOARCH=amd64 	go build -v -o ../bin/docker-linux-amd64
-	cd cmd && GOOS=darwin 	GOARCH=amd64 	go build -v -o ../bin/docker-darwin-amd64
-	cd cmd && GOOS=windows 	GOARCH=amd64 	go build -v -o ../bin/docker-windows-amd64.exe
+	GOOS=linux   GOARCH=amd64 go build -v -o bin/docker-linux-amd64 ./cli
+	GOOS=darwin  GOARCH=amd64 go build -v -o bin/docker-darwin-amd64 ./cli
+	GOOS=windows GOARCH=amd64 go build -v -o bin/docker-windows-amd64.exe ./cli
 
 xexample: example
-	cd example/backend && GOOS=linux	GOARCH=amd64	go build -v -o ../../bin/backend-example-linux-amd64
-	cd example/backend && GOOS=darwin	GOARCH=amd64	go build -v -o ../../bin/backend-example-darwin-amd64
-	cd example/backend && GOOS=windows	GOARCH=amd64	go build -v -o ../../bin/backend-example-windows-amd64.exe
+	GOOS=linux   GOARCH=amd64 go build -v -o bin/backend-example-linux-amd64 ./example/backend
+	GOOS=darwin  GOARCH=amd64 go build -v -o bin/backend-example-darwin-amd64 ./example/backend
+	GOOS=windows GOARCH=amd64 go build -v -o bin/backend-example-windows-amd64.exe ./example/backend
 
 dprotos:
 	docker build . \
diff --git a/cli/cmd/context.go b/cli/cmd/context.go
index 8e8041fe3..eff416e1a 100644
--- a/cli/cmd/context.go
+++ b/cli/cmd/context.go
@@ -61,7 +61,7 @@ type createOpts struct {
 func createCommand() *cobra.Command {
 	var opts createOpts
 	cmd := &cobra.Command{
-		Use:   "create",
+		Use:   "create CONTEXT BACKEND [OPTIONS]",
 		Short: "Create a context",
 		Args:  cobra.ExactArgs(2),
 		RunE: func(cmd *cobra.Command, args []string) error {
@@ -78,6 +78,7 @@ func listCommand() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:     "list",
 		Aliases: []string{"ls"},
+		Args:    cobra.NoArgs,
 		RunE: func(cmd *cobra.Command, args []string) error {
 			return runList(cmd.Context())
 		},
diff --git a/context/store/store_test.go b/context/store/store_test.go
index b853de4fc..d424e078b 100644
--- a/context/store/store_test.go
+++ b/context/store/store_test.go
@@ -87,6 +87,7 @@ func (suite *StoreTestSuite) TestGet() {
 	assert.Equal(suite.T(), "description", m.Description)
 	assert.Equal(suite.T(), "type", m.Type)
 }
+
 func (suite *StoreTestSuite) TestList() {
 	err := suite.store.Create("test1", TypeContext{}, nil)
 	assert.Nil(suite.T(), err)

From 474cdbae11d6a225077dd701e43541ee946d8e4b Mon Sep 17 00:00:00 2001
From: Djordje Lukic <djordje.lukic@docker.com>
Date: Mon, 27 Apr 2020 15:56:23 +0200
Subject: [PATCH 8/8] Remove unnecessary if

---
 cli/main.go | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/cli/main.go b/cli/main.go
index 418765501..0bd929cbc 100644
--- a/cli/main.go
+++ b/cli/main.go
@@ -177,12 +177,10 @@ func execMoby(ctx context.Context) {
 		cmd.Stdout = os.Stdout
 		cmd.Stderr = os.Stderr
 		if err := cmd.Run(); err != nil {
-			if err != nil {
-				if exiterr, ok := err.(*exec.ExitError); ok {
-					os.Exit(exiterr.ExitCode())
-				}
-				os.Exit(1)
+			if exiterr, ok := err.(*exec.ExitError); ok {
+				os.Exit(exiterr.ExitCode())
 			}
+			os.Exit(1)
 		}
 		os.Exit(0)
 	}