From b3aa17187ffa910e32e2931d0487f84153a5cefc Mon Sep 17 00:00:00 2001 From: Jean-Christophe Sirot Date: Wed, 6 Mar 2019 15:01:12 +0100 Subject: [PATCH] Make default context behaves like a real context: - when using "--context default" parameter - when printing the list of contexts - when exporting the default context to a tarball Signed-off-by: Jean-Christophe Sirot (+1 squashed commit) Squashed commits: [20670495] Fix CLI initialization for the `docker stack deploy --help` command and ensure that the dockerCli.CurrentContext() always returns a non empty context name (default as a fallback) Remove now obsolete code handling empty string context name Minor code cleanup Signed-off-by: Jean-Christophe Sirot --- cli/command/cli.go | 91 ++++---- cli/command/context/create_test.go | 27 ++- cli/command/context/export.go | 2 +- cli/command/context/inspect.go | 3 - cli/command/context/list.go | 27 +-- cli/command/context/list_test.go | 15 -- cli/command/context/remove_test.go | 9 + cli/command/context/testdata/list.golden | 10 +- .../context/testdata/list.no-context.golden | 2 - .../context/testdata/quiet-list.golden | 1 + cli/command/defaultcontextstore.go | 198 ++++++++++++++++++ cli/command/defaultcontextstore_test.go | 193 +++++++++++++++++ cli/command/stack/cmd.go | 4 + cli/command/testdata/ca.pem | 18 ++ cli/command/testdata/test-kubeconfig | 20 ++ cli/context/store/store.go | 13 +- cli/context/store/store_test.go | 27 ++- internal/test/cli.go | 21 ++ 18 files changed, 575 insertions(+), 106 deletions(-) delete mode 100644 cli/command/context/testdata/list.no-context.golden create mode 100644 cli/command/defaultcontextstore.go create mode 100644 cli/command/defaultcontextstore_test.go create mode 100644 cli/command/testdata/ca.pem create mode 100644 cli/command/testdata/test-kubeconfig diff --git a/cli/command/cli.go b/cli/command/cli.go index 3208a79041..36ac41c56a 100644 --- a/cli/command/cli.go +++ b/cli/command/cli.go @@ -3,6 +3,7 @@ package command import ( "context" "io" + "io/ioutil" "os" "path/filepath" "runtime" @@ -209,12 +210,18 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...Initialize cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err) - cli.contextStore = store.New(cliconfig.ContextStoreDir(), cli.contextStoreConfig) + baseContextSore := store.New(cliconfig.ContextStoreDir(), cli.contextStoreConfig) + cli.contextStore = &ContextStoreWithDefault{ + Store: baseContextSore, + Resolver: func() (*DefaultContext, error) { + return resolveDefaultContext(opts.Common, cli.ConfigFile(), cli.Err()) + }, + } cli.currentContext, err = resolveContextName(opts.Common, cli.configFile, cli.contextStore) if err != nil { return err } - cli.dockerEndpoint, err = resolveDockerEndpoint(cli.contextStore, cli.currentContext, opts.Common) + cli.dockerEndpoint, err = resolveDockerEndpoint(cli.contextStore, cli.currentContext) if err != nil { return errors.Wrap(err, "unable to resolve docker endpoint") } @@ -252,12 +259,17 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...Initialize // NewAPIClientFromFlags creates a new APIClient from command line flags func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) { - store := store.New(cliconfig.ContextStoreDir(), defaultContextStoreConfig()) + store := &ContextStoreWithDefault{ + Store: store.New(cliconfig.ContextStoreDir(), defaultContextStoreConfig()), + Resolver: func() (*DefaultContext, error) { + return resolveDefaultContext(opts, configFile, ioutil.Discard) + }, + } contextName, err := resolveContextName(opts, configFile, store) if err != nil { return nil, err } - endpoint, err := resolveDockerEndpoint(store, contextName, opts) + endpoint, err := resolveDockerEndpoint(store, contextName) if err != nil { return nil, errors.Wrap(err, "unable to resolve docker endpoint") } @@ -278,18 +290,20 @@ func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigF return client.NewClientWithOpts(clientOpts...) } -func resolveDockerEndpoint(s store.Store, contextName string, opts *cliflags.CommonOptions) (docker.Endpoint, error) { - if contextName != "" { - ctxMeta, err := s.GetContextMetadata(contextName) - if err != nil { - return docker.Endpoint{}, err - } - epMeta, err := docker.EndpointFromContext(ctxMeta) - if err != nil { - return docker.Endpoint{}, err - } - return docker.WithTLSData(s, contextName, epMeta) +func resolveDockerEndpoint(s store.Store, contextName string) (docker.Endpoint, error) { + ctxMeta, err := s.GetContextMetadata(contextName) + if err != nil { + return docker.Endpoint{}, err } + epMeta, err := docker.EndpointFromContext(ctxMeta) + if err != nil { + return docker.Endpoint{}, err + } + return docker.WithTLSData(s, contextName, epMeta) +} + +// Resolve the Docker endpoint for the default context (based on config, env vars and CLI flags) +func resolveDefaultDockerEndpoint(opts *cliflags.CommonOptions) (docker.Endpoint, error) { host, err := getServerHost(opts.Hosts, opts.TLSOptions) if err != nil { return docker.Endpoint{}, err @@ -384,38 +398,21 @@ func (cli *DockerCli) CurrentContext() string { // StackOrchestrator resolves which stack orchestrator is in use func (cli *DockerCli) StackOrchestrator(flagValue string) (Orchestrator, error) { - var ctxOrchestrator string - - configFile := cli.configFile - if configFile == nil { - configFile = cliconfig.LoadDefaultConfigFile(cli.Err()) - } - currentContext := cli.CurrentContext() - if currentContext == "" { - currentContext = configFile.CurrentContext + ctxRaw, err := cli.ContextStore().GetContextMetadata(currentContext) + if store.IsErrContextDoesNotExist(err) { + // case where the currentContext has been removed (CLI behavior is to fallback to using DOCKER_HOST based resolution) + return GetStackOrchestrator(flagValue, "", cli.ConfigFile().StackOrchestrator, cli.Err()) } - if currentContext != "" { - contextstore := cli.contextStore - if contextstore == nil { - contextstore = store.New(cliconfig.ContextStoreDir(), cli.contextStoreConfig) - } - ctxRaw, err := contextstore.GetContextMetadata(currentContext) - if store.IsErrContextDoesNotExist(err) { - // case where the currentContext has been removed (CLI behavior is to fallback to using DOCKER_HOST based resolution) - return GetStackOrchestrator(flagValue, "", configFile.StackOrchestrator, cli.Err()) - } - if err != nil { - return "", err - } - ctxMeta, err := GetDockerContext(ctxRaw) - if err != nil { - return "", err - } - ctxOrchestrator = string(ctxMeta.StackOrchestrator) + if err != nil { + return "", err } - - return GetStackOrchestrator(flagValue, ctxOrchestrator, configFile.StackOrchestrator, cli.Err()) + ctxMeta, err := GetDockerContext(ctxRaw) + if err != nil { + return "", err + } + ctxOrchestrator := string(ctxMeta.StackOrchestrator) + return GetStackOrchestrator(flagValue, ctxOrchestrator, cli.ConfigFile().StackOrchestrator, cli.Err()) } // DockerEndpoint returns the current docker endpoint @@ -511,10 +508,10 @@ func resolveContextName(opts *cliflags.CommonOptions, config *configfile.ConfigF return opts.Context, nil } if len(opts.Hosts) > 0 { - return "", nil + return DefaultContextName, nil } if _, present := os.LookupEnv("DOCKER_HOST"); present { - return "", nil + return DefaultContextName, nil } if ctxName, ok := os.LookupEnv("DOCKER_CONTEXT"); ok { return ctxName, nil @@ -526,7 +523,7 @@ func resolveContextName(opts *cliflags.CommonOptions, config *configfile.ConfigF } return config.CurrentContext, err } - return "", nil + return DefaultContextName, nil } func defaultContextStoreConfig() store.Config { diff --git a/cli/command/context/create_test.go b/cli/command/context/create_test.go index 161d50b71d..eea3731900 100644 --- a/cli/command/context/create_test.go +++ b/cli/command/context/create_test.go @@ -23,7 +23,26 @@ func makeFakeCli(t *testing.T, opts ...func(*test.FakeCli)) (*test.FakeCli, func store.EndpointTypeGetter(docker.DockerEndpoint, func() interface{} { return &docker.EndpointMeta{} }), store.EndpointTypeGetter(kubernetes.KubernetesEndpoint, func() interface{} { return &kubernetes.EndpointMeta{} }), ) - store := store.New(dir, storeConfig) + store := &command.ContextStoreWithDefault{ + Store: store.New(dir, storeConfig), + Resolver: func() (*command.DefaultContext, error) { + return &command.DefaultContext{ + Meta: store.ContextMetadata{ + Endpoints: map[string]interface{}{ + docker.DockerEndpoint: docker.EndpointMeta{ + Host: "unix:///var/run/docker.sock", + }, + }, + Metadata: command.DockerContext{ + Description: "", + StackOrchestrator: command.OrchestratorSwarm, + }, + Name: command.DefaultContextName, + }, + TLS: store.ContextTLSData{}, + }, nil + }, + } cleanup := func() { os.RemoveAll(dir) } @@ -52,6 +71,12 @@ func TestCreateInvalids(t *testing.T) { { expecterErr: `context name cannot be empty`, }, + { + options: CreateOptions{ + Name: "default", + }, + expecterErr: `"default" is a reserved context name`, + }, { options: CreateOptions{ Name: " ", diff --git a/cli/command/context/export.go b/cli/command/context/export.go index 870efcc3a4..f0d1308523 100644 --- a/cli/command/context/export.go +++ b/cli/command/context/export.go @@ -77,7 +77,7 @@ func writeTo(dockerCli command.Cli, reader io.Reader, dest string) error { // RunExport exports a Docker context func RunExport(dockerCli command.Cli, opts *ExportOptions) error { - if err := validateContextName(opts.ContextName); err != nil { + if err := validateContextName(opts.ContextName); err != nil && opts.ContextName != command.DefaultContextName { return err } ctxMeta, err := dockerCli.ContextStore().GetContextMetadata(opts.ContextName) diff --git a/cli/command/context/inspect.go b/cli/command/context/inspect.go index 678b818f4c..492addc013 100644 --- a/cli/command/context/inspect.go +++ b/cli/command/context/inspect.go @@ -40,9 +40,6 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command { func runInspect(dockerCli command.Cli, opts inspectOptions) error { getRefFunc := func(ref string) (interface{}, []byte, error) { - if ref == "default" { - return nil, nil, errors.New(`context "default" cannot be inspected`) - } c, err := dockerCli.ContextStore().GetContextMetadata(ref) if err != nil { return nil, nil, err diff --git a/cli/command/context/list.go b/cli/command/context/list.go index 1cda69767d..54ab78ce40 100644 --- a/cli/command/context/list.go +++ b/cli/command/context/list.go @@ -9,7 +9,6 @@ import ( "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/context/docker" kubecontext "github.com/docker/cli/cli/context/kubernetes" - "github.com/docker/cli/kubernetes" "github.com/spf13/cobra" "vbom.ml/util/sortorder" ) @@ -61,6 +60,9 @@ func runList(dockerCli command.Cli, opts *listOptions) error { if kubernetesEndpoint != nil { kubEndpointText = fmt.Sprintf("%s (%s)", kubernetesEndpoint.Host, kubernetesEndpoint.DefaultNamespace) } + if rawMeta.Name == command.DefaultContextName { + meta.Description = "Current DOCKER_HOST based configuration" + } desc := formatter.ClientContext{ Name: rawMeta.Name, Current: rawMeta.Name == curContext, @@ -71,29 +73,6 @@ func runList(dockerCli command.Cli, opts *listOptions) error { } contexts = append(contexts, &desc) } - if !opts.quiet { - desc := &formatter.ClientContext{ - Name: "default", - Description: "Current DOCKER_HOST based configuration", - } - if dockerCli.CurrentContext() == "" { - orchestrator, _ := dockerCli.StackOrchestrator("") - kubEndpointText := "" - kubeconfig := kubernetes.NewKubernetesConfig("") - if cfg, err := kubeconfig.ClientConfig(); err == nil { - ns, _, _ := kubeconfig.Namespace() - if ns == "" { - ns = "default" - } - kubEndpointText = fmt.Sprintf("%s (%s)", cfg.Host, ns) - } - desc.Current = true - desc.StackOrchestrator = string(orchestrator) - desc.DockerEndpoint = dockerCli.DockerEndpoint().Host - desc.KubernetesEndpoint = kubEndpointText - } - contexts = append(contexts, desc) - } sort.Slice(contexts, func(i, j int) bool { return sortorder.NaturalLess(contexts[i].Name, contexts[j].Name) }) diff --git a/cli/command/context/list_test.go b/cli/command/context/list_test.go index 902f620c28..8f1d05ee93 100644 --- a/cli/command/context/list_test.go +++ b/cli/command/context/list_test.go @@ -4,7 +4,6 @@ import ( "testing" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/context/docker" "gotest.tools/assert" "gotest.tools/env" "gotest.tools/golden" @@ -36,20 +35,6 @@ func TestList(t *testing.T) { golden.Assert(t, cli.OutBuffer().String(), "list.golden") } -func TestListNoContext(t *testing.T) { - cli, cleanup := makeFakeCli(t) - defer cleanup() - defer env.Patch(t, "KUBECONFIG", "./testdata/test-kubeconfig")() - cli.SetDockerEndpoint(docker.Endpoint{ - EndpointMeta: docker.EndpointMeta{ - Host: "https://someswarmserver", - }, - }) - cli.OutBuffer().Reset() - assert.NilError(t, runList(cli, &listOptions{})) - golden.Assert(t, cli.OutBuffer().String(), "list.no-context.golden") -} - func TestListQuiet(t *testing.T) { cli, cleanup := makeFakeCli(t) defer cleanup() diff --git a/cli/command/context/remove_test.go b/cli/command/context/remove_test.go index 53ad12f516..98c3bec8da 100644 --- a/cli/command/context/remove_test.go +++ b/cli/command/context/remove_test.go @@ -62,3 +62,12 @@ func TestRemoveCurrentForce(t *testing.T) { assert.NilError(t, err) assert.Equal(t, "", reloadedConfig.CurrentContext) } + +func TestRemoveDefault(t *testing.T) { + cli, cleanup := makeFakeCli(t) + defer cleanup() + createTestContextWithKubeAndSwarm(t, cli, "other", "all") + cli.SetCurrentContext("current") + err := RunRemove(cli, RemoveOptions{}, []string{"default"}) + assert.ErrorContains(t, err, `default: context "default" cannot be removed`) +} diff --git a/cli/command/context/testdata/list.golden b/cli/command/context/testdata/list.golden index c32be2e28c..a07c22f304 100644 --- a/cli/command/context/testdata/list.golden +++ b/cli/command/context/testdata/list.golden @@ -1,5 +1,5 @@ -NAME DESCRIPTION DOCKER ENDPOINT KUBERNETES ENDPOINT ORCHESTRATOR -current * description of current https://someswarmserver https://someserver (default) all -default Current DOCKER_HOST based configuration -other description of other https://someswarmserver https://someserver (default) all -unset description of unset https://someswarmserver https://someserver (default) +NAME DESCRIPTION DOCKER ENDPOINT KUBERNETES ENDPOINT ORCHESTRATOR +current * description of current https://someswarmserver https://someserver (default) all +default Current DOCKER_HOST based configuration unix:///var/run/docker.sock swarm +other description of other https://someswarmserver https://someserver (default) all +unset description of unset https://someswarmserver https://someserver (default) diff --git a/cli/command/context/testdata/list.no-context.golden b/cli/command/context/testdata/list.no-context.golden deleted file mode 100644 index 5e11422f00..0000000000 --- a/cli/command/context/testdata/list.no-context.golden +++ /dev/null @@ -1,2 +0,0 @@ -NAME DESCRIPTION DOCKER ENDPOINT KUBERNETES ENDPOINT ORCHESTRATOR -default * Current DOCKER_HOST based configuration https://someswarmserver https://someserver (default) swarm diff --git a/cli/command/context/testdata/quiet-list.golden b/cli/command/context/testdata/quiet-list.golden index c9bef2c3e4..dd00383fd7 100644 --- a/cli/command/context/testdata/quiet-list.golden +++ b/cli/command/context/testdata/quiet-list.golden @@ -1,2 +1,3 @@ current +default other diff --git a/cli/command/defaultcontextstore.go b/cli/command/defaultcontextstore.go new file mode 100644 index 0000000000..10f8dc16ba --- /dev/null +++ b/cli/command/defaultcontextstore.go @@ -0,0 +1,198 @@ +package command + +import ( + "fmt" + "io" + "os" + "path/filepath" + + "github.com/docker/cli/cli/config/configfile" + "github.com/docker/cli/cli/context/docker" + "github.com/docker/cli/cli/context/kubernetes" + "github.com/docker/cli/cli/context/store" + cliflags "github.com/docker/cli/cli/flags" + "github.com/docker/docker/pkg/homedir" + "github.com/pkg/errors" +) + +const ( + // DefaultContextName is the name reserved for the default context (config & env based) + DefaultContextName = "default" +) + +// DefaultContext contains the default context data for all enpoints +type DefaultContext struct { + Meta store.ContextMetadata + TLS store.ContextTLSData +} + +// DefaultContextResolver is a function which resolves the default context base on the configuration and the env variables +type DefaultContextResolver func() (*DefaultContext, error) + +// ContextStoreWithDefault implements the store.Store interface with a support for the default context +type ContextStoreWithDefault struct { + store.Store + Resolver DefaultContextResolver +} + +// resolveDefaultContext creates a ContextMetadata for the current CLI invocation parameters +func resolveDefaultContext(opts *cliflags.CommonOptions, config *configfile.ConfigFile, stderr io.Writer) (*DefaultContext, error) { + stackOrchestrator, err := GetStackOrchestrator("", "", config.StackOrchestrator, stderr) + if err != nil { + return nil, err + } + contextTLSData := store.ContextTLSData{ + Endpoints: make(map[string]store.EndpointTLSData), + } + contextMetadata := store.ContextMetadata{ + Endpoints: make(map[string]interface{}), + Metadata: DockerContext{ + Description: "", + StackOrchestrator: stackOrchestrator, + }, + Name: DefaultContextName, + } + + dockerEP, err := resolveDefaultDockerEndpoint(opts) + if err != nil { + return nil, err + } + contextMetadata.Endpoints[docker.DockerEndpoint] = dockerEP.EndpointMeta + if dockerEP.TLSData != nil { + contextTLSData.Endpoints[docker.DockerEndpoint] = *dockerEP.TLSData.ToStoreTLSData() + } + + // Default context uses env-based kubeconfig for Kubernetes endpoint configuration + kubeconfig := os.Getenv("KUBECONFIG") + if kubeconfig == "" { + kubeconfig = filepath.Join(homedir.Get(), ".kube/config") + } + kubeEP, err := kubernetes.FromKubeConfig(kubeconfig, "", "") + if (stackOrchestrator == OrchestratorKubernetes || stackOrchestrator == OrchestratorAll) && err != nil { + return nil, errors.Wrapf(err, "default orchestrator is %s but kubernetes endpoint could not be found", stackOrchestrator) + } + if err == nil { + contextMetadata.Endpoints[kubernetes.KubernetesEndpoint] = kubeEP.EndpointMeta + if kubeEP.TLSData != nil { + contextTLSData.Endpoints[kubernetes.KubernetesEndpoint] = *kubeEP.TLSData.ToStoreTLSData() + } + } + + return &DefaultContext{Meta: contextMetadata, TLS: contextTLSData}, nil +} + +// ListContexts implements store.Store's ListContexts +func (s *ContextStoreWithDefault) ListContexts() ([]store.ContextMetadata, error) { + contextList, err := s.Store.ListContexts() + if err != nil { + return nil, err + } + defaultContext, err := s.Resolver() + if err != nil { + return nil, err + } + return append(contextList, defaultContext.Meta), nil +} + +// CreateOrUpdateContext is not allowed for the default context and fails +func (s *ContextStoreWithDefault) CreateOrUpdateContext(meta store.ContextMetadata) error { + if meta.Name == DefaultContextName { + return errors.New("default context cannot be created nor updated") + } + return s.Store.CreateOrUpdateContext(meta) +} + +// RemoveContext is not allowed for the default context and fails +func (s *ContextStoreWithDefault) RemoveContext(name string) error { + if name == DefaultContextName { + return errors.New("default context cannot be removed") + } + return s.Store.RemoveContext(name) +} + +// GetContextMetadata implements store.Store's GetContextMetadata +func (s *ContextStoreWithDefault) GetContextMetadata(name string) (store.ContextMetadata, error) { + if name == DefaultContextName { + defaultContext, err := s.Resolver() + if err != nil { + return store.ContextMetadata{}, err + } + return defaultContext.Meta, nil + } + return s.Store.GetContextMetadata(name) +} + +// ResetContextTLSMaterial is not implemented for default context and fails +func (s *ContextStoreWithDefault) ResetContextTLSMaterial(name string, data *store.ContextTLSData) error { + if name == DefaultContextName { + return errors.New("The default context store does not support ResetContextTLSMaterial") + } + return s.Store.ResetContextTLSMaterial(name, data) +} + +// ResetContextEndpointTLSMaterial is not implemented for default context and fails +func (s *ContextStoreWithDefault) ResetContextEndpointTLSMaterial(contextName string, endpointName string, data *store.EndpointTLSData) error { + if contextName == DefaultContextName { + return errors.New("The default context store does not support ResetContextEndpointTLSMaterial") + } + return s.Store.ResetContextEndpointTLSMaterial(contextName, endpointName, data) +} + +// ListContextTLSFiles implements store.Store's ListContextTLSFiles +func (s *ContextStoreWithDefault) ListContextTLSFiles(name string) (map[string]store.EndpointFiles, error) { + if name == DefaultContextName { + defaultContext, err := s.Resolver() + if err != nil { + return nil, err + } + tlsfiles := make(map[string]store.EndpointFiles) + for epName, epTLSData := range defaultContext.TLS.Endpoints { + var files store.EndpointFiles + for filename := range epTLSData.Files { + files = append(files, filename) + } + tlsfiles[epName] = files + } + return tlsfiles, nil + } + return s.Store.ListContextTLSFiles(name) +} + +// GetContextTLSData implements store.Store's GetContextTLSData +func (s *ContextStoreWithDefault) GetContextTLSData(contextName, endpointName, fileName string) ([]byte, error) { + if contextName == DefaultContextName { + defaultContext, err := s.Resolver() + if err != nil { + return nil, err + } + if defaultContext.TLS.Endpoints[endpointName].Files[fileName] == nil { + return nil, &noDefaultTLSDataError{endpointName: endpointName, fileName: fileName} + } + return defaultContext.TLS.Endpoints[endpointName].Files[fileName], nil + + } + return s.Store.GetContextTLSData(contextName, endpointName, fileName) +} + +type noDefaultTLSDataError struct { + endpointName string + fileName string +} + +func (e *noDefaultTLSDataError) Error() string { + return fmt.Sprintf("tls data for %s/%s/%s does not exist", DefaultContextName, e.endpointName, e.fileName) +} + +// NotFound satisfies interface github.com/docker/docker/errdefs.ErrNotFound +func (e *noDefaultTLSDataError) NotFound() {} + +// IsTLSDataDoesNotExist satisfies github.com/docker/cli/cli/context/store.tlsDataDoesNotExist +func (e *noDefaultTLSDataError) IsTLSDataDoesNotExist() {} + +// GetContextStorageInfo implements store.Store's GetContextStorageInfo +func (s *ContextStoreWithDefault) GetContextStorageInfo(contextName string) store.ContextStorageInfo { + if contextName == DefaultContextName { + return store.ContextStorageInfo{MetadataPath: "", TLSPath: ""} + } + return s.Store.GetContextStorageInfo(contextName) +} diff --git a/cli/command/defaultcontextstore_test.go b/cli/command/defaultcontextstore_test.go new file mode 100644 index 0000000000..753bb6fa53 --- /dev/null +++ b/cli/command/defaultcontextstore_test.go @@ -0,0 +1,193 @@ +package command + +import ( + "crypto/rand" + "io/ioutil" + "os" + "testing" + + "github.com/docker/cli/cli/config/configfile" + "github.com/docker/cli/cli/context/docker" + "github.com/docker/cli/cli/context/kubernetes" + "github.com/docker/cli/cli/context/store" + cliflags "github.com/docker/cli/cli/flags" + "github.com/docker/go-connections/tlsconfig" + "gotest.tools/assert" + "gotest.tools/env" + "gotest.tools/golden" +) + +type endpoint struct { + Foo string `json:"a_very_recognizable_field_name"` +} + +type testContext struct { + Bar string `json:"another_very_recognizable_field_name"` +} + +var testCfg = store.NewConfig(func() interface{} { return &testContext{} }, + store.EndpointTypeGetter("ep1", func() interface{} { return &endpoint{} }), + store.EndpointTypeGetter("ep2", func() interface{} { return &endpoint{} }), +) + +func testDefaultMetadata() store.ContextMetadata { + return store.ContextMetadata{ + Endpoints: map[string]interface{}{ + "ep1": endpoint{Foo: "bar"}, + }, + Metadata: testContext{Bar: "baz"}, + Name: DefaultContextName, + } +} + +func testStore(t *testing.T, meta store.ContextMetadata, tls store.ContextTLSData) (store.Store, func()) { + //meta := testDefaultMetadata() + testDir, err := ioutil.TempDir("", t.Name()) + assert.NilError(t, err) + //defer os.RemoveAll(testDir) + store := &ContextStoreWithDefault{ + Store: store.New(testDir, testCfg), + Resolver: func() (*DefaultContext, error) { + return &DefaultContext{ + Meta: meta, + TLS: tls, + }, nil + }, + } + return store, func() { + os.RemoveAll(testDir) + } +} + +func TestDefaultContextInitializer(t *testing.T) { + cli, err := NewDockerCli() + assert.NilError(t, err) + defer env.Patch(t, "DOCKER_HOST", "ssh://someswarmserver")() + defer env.Patch(t, "KUBECONFIG", "./testdata/test-kubeconfig")() + cli.configFile = &configfile.ConfigFile{ + StackOrchestrator: "all", + } + ctx, err := resolveDefaultContext(&cliflags.CommonOptions{ + TLS: true, + TLSOptions: &tlsconfig.Options{ + CAFile: "./testdata/ca.pem", + }, + }, cli.ConfigFile(), cli.Err()) + assert.NilError(t, err) + assert.Equal(t, "default", ctx.Meta.Name) + assert.Equal(t, OrchestratorAll, ctx.Meta.Metadata.(DockerContext).StackOrchestrator) + assert.DeepEqual(t, "ssh://someswarmserver", ctx.Meta.Endpoints[docker.DockerEndpoint].(docker.EndpointMeta).Host) + golden.Assert(t, string(ctx.TLS.Endpoints[docker.DockerEndpoint].Files["ca.pem"]), "ca.pem") + assert.DeepEqual(t, "zoinx", ctx.Meta.Endpoints[kubernetes.KubernetesEndpoint].(kubernetes.EndpointMeta).DefaultNamespace) +} + +func TestExportDefaultImport(t *testing.T) { + file1 := make([]byte, 1500) + rand.Read(file1) + file2 := make([]byte, 3700) + rand.Read(file2) + s, cleanup := testStore(t, testDefaultMetadata(), store.ContextTLSData{ + Endpoints: map[string]store.EndpointTLSData{ + "ep2": { + Files: map[string][]byte{ + "file1": file1, + "file2": file2, + }, + }, + }, + }) + defer cleanup() + r := store.Export("default", s) + defer r.Close() + err := store.Import("dest", s, r) + assert.NilError(t, err) + + srcMeta, err := s.GetContextMetadata("default") + assert.NilError(t, err) + destMeta, err := s.GetContextMetadata("dest") + assert.NilError(t, err) + assert.DeepEqual(t, destMeta.Metadata, srcMeta.Metadata) + assert.DeepEqual(t, destMeta.Endpoints, srcMeta.Endpoints) + + srcFileList, err := s.ListContextTLSFiles("default") + assert.NilError(t, err) + destFileList, err := s.ListContextTLSFiles("dest") + assert.NilError(t, err) + assert.Equal(t, 1, len(destFileList)) + assert.Equal(t, 1, len(srcFileList)) + assert.Equal(t, 2, len(destFileList["ep2"])) + assert.Equal(t, 2, len(srcFileList["ep2"])) + + srcData1, err := s.GetContextTLSData("default", "ep2", "file1") + assert.NilError(t, err) + assert.DeepEqual(t, file1, srcData1) + srcData2, err := s.GetContextTLSData("default", "ep2", "file2") + assert.NilError(t, err) + assert.DeepEqual(t, file2, srcData2) + + destData1, err := s.GetContextTLSData("dest", "ep2", "file1") + assert.NilError(t, err) + assert.DeepEqual(t, file1, destData1) + destData2, err := s.GetContextTLSData("dest", "ep2", "file2") + assert.NilError(t, err) + assert.DeepEqual(t, file2, destData2) +} + +func TestListDefaultContext(t *testing.T) { + meta := testDefaultMetadata() + s, cleanup := testStore(t, meta, store.ContextTLSData{}) + defer cleanup() + result, err := s.ListContexts() + assert.NilError(t, err) + assert.Equal(t, 1, len(result)) + assert.DeepEqual(t, meta, result[0]) +} + +func TestGetDefaultContextStorageInfo(t *testing.T) { + s, cleanup := testStore(t, testDefaultMetadata(), store.ContextTLSData{}) + defer cleanup() + result := s.GetContextStorageInfo(DefaultContextName) + assert.Equal(t, "", result.MetadataPath) + assert.Equal(t, "", result.TLSPath) +} + +func TestGetDefaultContextMetadata(t *testing.T) { + meta := testDefaultMetadata() + s, cleanup := testStore(t, meta, store.ContextTLSData{}) + defer cleanup() + result, err := s.GetContextMetadata(DefaultContextName) + assert.NilError(t, err) + assert.Equal(t, DefaultContextName, result.Name) + assert.DeepEqual(t, meta.Metadata, result.Metadata) + assert.DeepEqual(t, meta.Endpoints, result.Endpoints) +} + +func TestErrCreateDefault(t *testing.T) { + meta := testDefaultMetadata() + s, cleanup := testStore(t, meta, store.ContextTLSData{}) + defer cleanup() + err := s.CreateOrUpdateContext(store.ContextMetadata{ + Endpoints: map[string]interface{}{ + "ep1": endpoint{Foo: "bar"}, + }, + Metadata: testContext{Bar: "baz"}, + Name: "default", + }) + assert.Error(t, err, "default context cannot be created nor updated") +} + +func TestErrRemoveDefault(t *testing.T) { + meta := testDefaultMetadata() + s, cleanup := testStore(t, meta, store.ContextTLSData{}) + defer cleanup() + err := s.RemoveContext("default") + assert.Error(t, err, "default context cannot be removed") +} + +func TestErrTLSDataError(t *testing.T) { + meta := testDefaultMetadata() + s, cleanup := testStore(t, meta, store.ContextTLSData{}) + defer cleanup() + _, err := s.GetContextTLSData("default", "noop", "noop") + assert.Check(t, store.IsErrTLSDataDoesNotExist(err)) +} diff --git a/cli/command/stack/cmd.go b/cli/command/stack/cmd.go index b1ad21dafd..080732f56b 100644 --- a/cli/command/stack/cmd.go +++ b/cli/command/stack/cmd.go @@ -48,6 +48,10 @@ func NewStackCommand(dockerCli command.Cli) *cobra.Command { } defaultHelpFunc := cmd.HelpFunc() cmd.SetHelpFunc(func(c *cobra.Command, args []string) { + if err := cmd.Root().PersistentPreRunE(c, args); err != nil { + fmt.Fprintln(dockerCli.Err(), err) + return + } if err := cmd.PersistentPreRunE(c, args); err != nil { fmt.Fprintln(dockerCli.Err(), err) return diff --git a/cli/command/testdata/ca.pem b/cli/command/testdata/ca.pem new file mode 100644 index 0000000000..a289ce7ca6 --- /dev/null +++ b/cli/command/testdata/ca.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC+TCCAeGgAwIBAgIBATANBgkqhkiG9w0BAQsFADAeMQswCQYDVQQGEwJGUjEP +MA0GA1UEChMGRG9ja2VyMB4XDTE5MDMwMzIzMDAwMFoXDTI0MDMwMTIzMDAwMFow +HjELMAkGA1UEBhMCRlIxDzANBgNVBAoTBkRvY2tlcjCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAMkifL8Ne9B9LQ8+pKD20meVuV34Ol/xUcH/OfxbiBMa +HrlIKsGIaO9GraBLq1DJyaZ6sP6ntfwXqwBYQrAN2fQL1AmwMetqpNjby307XqRa +GUQekjG710LfAFKsS/yD/R8L944MFmTbYwGyjROExs8ZAA4fkA8SATzRXhM3a8dE +YcrXacZQqd5dwFFS/UyJQbMoNx7IgzrXySqpt3rV8qD8MAUebgshd2p9CQO6zzoU +ImOJImMc/15LFZymemm2KvzXTM4J9UYdibXZGzpxcnyGNCb4FVV0HF0Ya+NMDwvY +nNpW5rea64ppS8McejePRCmLS8DxMxKTLB7eW97LuDECAwEAAaNCMEAwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQU6zuJSXHxniajbNcc4SHoM+fatvMwDgYDVR0P +AQH/BAQDAgE+MA0GCSqGSIb3DQEBCwUAA4IBAQB2l46NnKzoTOCuUjxGmUv3s1np +rENRWlq0mHjCzoYSocg/IcwY7fz41XkwTVV8O3h/Jm25YGnj4lqaXlEKYJ63W8eI +wGLcirUAORSspcf+jd7OOjluzYCtuvVtOKR8w22pp5oE/AooGaO5y0ysefZBopr4 +CNUNsEYhDKFg7tfj6Govi6+0PNxvB53we4nU7NhJNMaClhh/pi8zbeaEf67S6eKn +Z3DFqO+8FW4wEePLwhCftESCTwx6Q24v/WIYnzYOXC5mb2B9MwkyJXJIJxxPIeSs +PycNQ2kw7gk/TKkLMNQbX4fgFB0zfdofidTAkqOIqFHq/8iD2DYEZQFgCD3v +-----END CERTIFICATE----- diff --git a/cli/command/testdata/test-kubeconfig b/cli/command/testdata/test-kubeconfig new file mode 100644 index 0000000000..e96df74a50 --- /dev/null +++ b/cli/command/testdata/test-kubeconfig @@ -0,0 +1,20 @@ +apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: dGhlLWNh + server: https://someserver + name: test-cluster +contexts: +- context: + cluster: test-cluster + user: test-user + namespace: zoinx + name: test +current-context: test +kind: Config +preferences: {} +users: +- name: test-user + user: + client-certificate-data: dGhlLWNlcnQ= + client-key-data: dGhlLWtleQ== diff --git a/cli/context/store/store.go b/cli/context/store/store.go index 5afb30749d..f3561b3cf6 100644 --- a/cli/context/store/store.go +++ b/cli/context/store/store.go @@ -12,7 +12,8 @@ import ( "path/filepath" "strings" - "github.com/opencontainers/go-digest" + "github.com/docker/docker/errdefs" + digest "github.com/opencontainers/go-digest" ) // Store provides a context store for easily remembering endpoints configuration @@ -299,6 +300,11 @@ func (e *contextDoesNotExistError) setContext(name string) { // NotFound satisfies interface github.com/docker/docker/errdefs.ErrNotFound func (e *contextDoesNotExistError) NotFound() {} +type tlsDataDoesNotExist interface { + errdefs.ErrNotFound + IsTLSDataDoesNotExist() +} + type tlsDataDoesNotExistError struct { context, endpoint, file string } @@ -314,6 +320,9 @@ func (e *tlsDataDoesNotExistError) setContext(name string) { // NotFound satisfies interface github.com/docker/docker/errdefs.ErrNotFound func (e *tlsDataDoesNotExistError) NotFound() {} +// IsTLSDataDoesNotExist satisfies tlsDataDoesNotExist +func (e *tlsDataDoesNotExistError) IsTLSDataDoesNotExist() {} + // IsErrContextDoesNotExist checks if the given error is a "context does not exist" condition func IsErrContextDoesNotExist(err error) bool { _, ok := err.(*contextDoesNotExistError) @@ -322,7 +331,7 @@ func IsErrContextDoesNotExist(err error) bool { // IsErrTLSDataDoesNotExist checks if the given error is a "context does not exist" condition func IsErrTLSDataDoesNotExist(err error) bool { - _, ok := err.(*tlsDataDoesNotExistError) + _, ok := err.(tlsDataDoesNotExist) return ok } diff --git a/cli/context/store/store_test.go b/cli/context/store/store_test.go index c18bcbb7b5..b2cee1c05f 100644 --- a/cli/context/store/store_test.go +++ b/cli/context/store/store_test.go @@ -1,6 +1,7 @@ package store import ( + "crypto/rand" "io/ioutil" "os" "testing" @@ -35,9 +36,14 @@ func TestExportImport(t *testing.T) { Name: "source", }) assert.NilError(t, err) + file1 := make([]byte, 1500) + rand.Read(file1) + file2 := make([]byte, 3700) + rand.Read(file2) err = s.ResetContextEndpointTLSMaterial("source", "ep1", &EndpointTLSData{ Files: map[string][]byte{ - "file1": []byte("test-data"), + "file1": file1, + "file2": file2, }, }) assert.NilError(t, err) @@ -55,13 +61,22 @@ func TestExportImport(t *testing.T) { assert.NilError(t, err) destFileList, err := s.ListContextTLSFiles("dest") assert.NilError(t, err) - assert.DeepEqual(t, srcFileList, destFileList) - srcData, err := s.GetContextTLSData("source", "ep1", "file1") + assert.Equal(t, 1, len(destFileList)) + assert.Equal(t, 1, len(srcFileList)) + assert.Equal(t, 2, len(destFileList["ep1"])) + assert.Equal(t, 2, len(srcFileList["ep1"])) + srcData1, err := s.GetContextTLSData("source", "ep1", "file1") assert.NilError(t, err) - assert.Equal(t, "test-data", string(srcData)) - destData, err := s.GetContextTLSData("dest", "ep1", "file1") + assert.DeepEqual(t, file1, srcData1) + srcData2, err := s.GetContextTLSData("source", "ep1", "file2") assert.NilError(t, err) - assert.Equal(t, "test-data", string(destData)) + assert.DeepEqual(t, file2, srcData2) + destData1, err := s.GetContextTLSData("dest", "ep1", "file1") + assert.NilError(t, err) + assert.DeepEqual(t, file1, destData1) + destData2, err := s.GetContextTLSData("dest", "ep1", "file2") + assert.NilError(t, err) + assert.DeepEqual(t, file2, destData2) } func TestRemove(t *testing.T) { diff --git a/internal/test/cli.go b/internal/test/cli.go index 12b8b621d9..8e9a7eb8e8 100644 --- a/internal/test/cli.go +++ b/internal/test/cli.go @@ -224,3 +224,24 @@ func (c *FakeCli) NewContainerizedEngineClient(sockPath string) (clitypes.Contai func (c *FakeCli) SetContainerizedEngineClient(containerizedEngineClientFunc containerizedEngineFuncType) { c.containerizedEngineClientFunc = containerizedEngineClientFunc } + +// StackOrchestrator return the selected stack orchestrator +func (c *FakeCli) StackOrchestrator(flagValue string) (command.Orchestrator, error) { + configOrchestrator := "" + if c.configfile != nil { + configOrchestrator = c.configfile.StackOrchestrator + } + ctxOrchestrator := "" + if c.currentContext != "" && c.contextStore != nil { + meta, err := c.contextStore.GetContextMetadata(c.currentContext) + if err != nil { + return "", err + } + context, err := command.GetDockerContext(meta) + if err != nil { + return "", err + } + ctxOrchestrator = string(context.StackOrchestrator) + } + return command.GetStackOrchestrator(flagValue, ctxOrchestrator, configOrchestrator, c.err) +}