diff --git a/cli/command/cli.go b/cli/command/cli.go index 33eeb10949..fc557635c4 100644 --- a/cli/command/cli.go +++ b/cli/command/cli.go @@ -81,14 +81,9 @@ type DockerCli struct { contextStore store.Store currentContext string dockerEndpoint docker.Endpoint + contextStoreConfig store.Config } -var storeConfig = store.NewConfig( - func() interface{} { return &DockerContext{} }, - store.EndpointTypeGetter(docker.DockerEndpoint, func() interface{} { return &docker.EndpointMeta{} }), - store.EndpointTypeGetter(kubcontext.KubernetesEndpoint, func() interface{} { return &kubcontext.EndpointMeta{} }), -) - // DefaultVersion returns api.defaultVersion or DOCKER_API_VERSION if specified. func (cli *DockerCli) DefaultVersion() string { return cli.clientInfo.DefaultVersion @@ -184,7 +179,7 @@ func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.Registry func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error { cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err) var err error - cli.contextStore = store.New(cliconfig.ContextStoreDir(), storeConfig) + cli.contextStore = store.New(cliconfig.ContextStoreDir(), cli.contextStoreConfig) cli.currentContext, err = resolveContextName(opts.Common, cli.configFile, cli.contextStore) if err != nil { return err @@ -226,7 +221,7 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error { // 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(), storeConfig) + store := store.New(cliconfig.ContextStoreDir(), defaultContextStoreConfig()) contextName, err := resolveContextName(opts, configFile, store) if err != nil { return nil, err @@ -372,7 +367,7 @@ func (cli *DockerCli) StackOrchestrator(flagValue string) (Orchestrator, error) if currentContext != "" { contextstore := cli.contextStore if contextstore == nil { - contextstore = store.New(cliconfig.ContextStoreDir(), storeConfig) + contextstore = store.New(cliconfig.ContextStoreDir(), cli.contextStoreConfig) } ctxRaw, err := contextstore.GetContextMetadata(currentContext) if store.IsErrContextDoesNotExist(err) { @@ -430,6 +425,7 @@ func NewDockerCli(ops ...DockerCliOption) (*DockerCli, error) { WithContentTrustFromEnv(), WithContainerizedClient(containerizedengine.NewClient), } + cli.contextStoreConfig = defaultContextStoreConfig() ops = append(defaultOps, ops...) if err := cli.Apply(ops...); err != nil { return nil, err @@ -501,3 +497,11 @@ func resolveContextName(opts *cliflags.CommonOptions, config *configfile.ConfigF } return "", nil } + +func defaultContextStoreConfig() store.Config { + return store.NewConfig( + func() interface{} { return &DockerContext{} }, + store.EndpointTypeGetter(docker.DockerEndpoint, func() interface{} { return &docker.EndpointMeta{} }), + store.EndpointTypeGetter(kubcontext.KubernetesEndpoint, func() interface{} { return &kubcontext.EndpointMeta{} }), + ) +} diff --git a/cli/command/cli_options.go b/cli/command/cli_options.go index 43db626211..4f48ca4ef2 100644 --- a/cli/command/cli_options.go +++ b/cli/command/cli_options.go @@ -1,10 +1,14 @@ package command import ( + "fmt" "io" "os" "strconv" + "github.com/docker/cli/cli/context/docker" + "github.com/docker/cli/cli/context/kubernetes" + "github.com/docker/cli/cli/context/store" "github.com/docker/cli/cli/streams" clitypes "github.com/docker/cli/types" "github.com/docker/docker/pkg/term" @@ -87,3 +91,16 @@ func WithContainerizedClient(containerizedFn func(string) (clitypes.Containerize return nil } } + +// WithContextEndpointType add support for an additional typed endpoint in the context store +// Plugins should use this to store additional endpoints configuration in the context store +func WithContextEndpointType(endpointName string, endpointType store.TypeGetter) DockerCliOption { + return func(cli *DockerCli) error { + switch endpointName { + case docker.DockerEndpoint, kubernetes.KubernetesEndpoint: + return fmt.Errorf("cannot change %q endpoint type", endpointName) + } + cli.contextStoreConfig.SetEndpoint(endpointName, endpointType) + return nil + } +} diff --git a/cli/command/context/create.go b/cli/command/context/create.go index c51d5214b5..ce53fcffdf 100644 --- a/cli/command/context/create.go +++ b/cli/command/context/create.go @@ -14,12 +14,13 @@ import ( "github.com/spf13/cobra" ) -type createOptions struct { - name string - description string - defaultStackOrchestrator string - docker map[string]string - kubernetes map[string]string +// CreateOptions are the options used for creating a context +type CreateOptions struct { + Name string + Description string + DefaultStackOrchestrator string + Docker map[string]string + Kubernetes map[string]string } func longCreateDescription() string { @@ -43,52 +44,53 @@ func longCreateDescription() string { } func newCreateCommand(dockerCli command.Cli) *cobra.Command { - opts := &createOptions{} + opts := &CreateOptions{} cmd := &cobra.Command{ Use: "create [OPTIONS] CONTEXT", Short: "Create a context", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts.name = args[0] - return runCreate(dockerCli, opts) + opts.Name = args[0] + return RunCreate(dockerCli, opts) }, Long: longCreateDescription(), } flags := cmd.Flags() - flags.StringVar(&opts.description, "description", "", "Description of the context") + flags.StringVar(&opts.Description, "description", "", "Description of the context") flags.StringVar( - &opts.defaultStackOrchestrator, + &opts.DefaultStackOrchestrator, "default-stack-orchestrator", "", "Default orchestrator for stack operations to use with this context (swarm|kubernetes|all)") - flags.StringToStringVar(&opts.docker, "docker", nil, "set the docker endpoint") - flags.StringToStringVar(&opts.kubernetes, "kubernetes", nil, "set the kubernetes endpoint") + flags.StringToStringVar(&opts.Docker, "docker", nil, "set the docker endpoint") + flags.StringToStringVar(&opts.Kubernetes, "kubernetes", nil, "set the kubernetes endpoint") return cmd } -func runCreate(cli command.Cli, o *createOptions) error { +// RunCreate creates a Docker context +func RunCreate(cli command.Cli, o *CreateOptions) error { s := cli.ContextStore() - if err := checkContextNameForCreation(s, o.name); err != nil { + if err := checkContextNameForCreation(s, o.Name); err != nil { return err } - stackOrchestrator, err := command.NormalizeOrchestrator(o.defaultStackOrchestrator) + stackOrchestrator, err := command.NormalizeOrchestrator(o.DefaultStackOrchestrator) if err != nil { return errors.Wrap(err, "unable to parse default-stack-orchestrator") } contextMetadata := store.ContextMetadata{ Endpoints: make(map[string]interface{}), Metadata: command.DockerContext{ - Description: o.description, + Description: o.Description, StackOrchestrator: stackOrchestrator, }, - Name: o.name, + Name: o.Name, } - if o.docker == nil { + if o.Docker == nil { return errors.New("docker endpoint configuration is required") } contextTLSData := store.ContextTLSData{ Endpoints: make(map[string]store.EndpointTLSData), } - dockerEP, dockerTLS, err := getDockerEndpointMetadataAndTLS(cli, o.docker) + dockerEP, dockerTLS, err := getDockerEndpointMetadataAndTLS(cli, o.Docker) if err != nil { return errors.Wrap(err, "unable to create docker endpoint config") } @@ -96,8 +98,8 @@ func runCreate(cli command.Cli, o *createOptions) error { if dockerTLS != nil { contextTLSData.Endpoints[docker.DockerEndpoint] = *dockerTLS } - if o.kubernetes != nil { - kubernetesEP, kubernetesTLS, err := getKubernetesEndpointMetadataAndTLS(cli, o.kubernetes) + if o.Kubernetes != nil { + kubernetesEP, kubernetesTLS, err := getKubernetesEndpointMetadataAndTLS(cli, o.Kubernetes) if err != nil { return errors.Wrap(err, "unable to create kubernetes endpoint config") } @@ -117,11 +119,11 @@ func runCreate(cli command.Cli, o *createOptions) error { if err := s.CreateOrUpdateContext(contextMetadata); err != nil { return err } - if err := s.ResetContextTLSMaterial(o.name, &contextTLSData); err != nil { + if err := s.ResetContextTLSMaterial(o.Name, &contextTLSData); err != nil { return err } - fmt.Fprintln(cli.Out(), o.name) - fmt.Fprintf(cli.Err(), "Successfully created context %q\n", o.name) + fmt.Fprintln(cli.Out(), o.Name) + fmt.Fprintf(cli.Err(), "Successfully created context %q\n", o.Name) return nil } diff --git a/cli/command/context/create_test.go b/cli/command/context/create_test.go index 52521f9f7e..161d50b71d 100644 --- a/cli/command/context/create_test.go +++ b/cli/command/context/create_test.go @@ -46,68 +46,68 @@ func TestCreateInvalids(t *testing.T) { defer cleanup() assert.NilError(t, cli.ContextStore().CreateOrUpdateContext(store.ContextMetadata{Name: "existing-context"})) tests := []struct { - options createOptions + options CreateOptions expecterErr string }{ { expecterErr: `context name cannot be empty`, }, { - options: createOptions{ - name: " ", + options: CreateOptions{ + Name: " ", }, expecterErr: `context name " " is invalid`, }, { - options: createOptions{ - name: "existing-context", + options: CreateOptions{ + Name: "existing-context", }, expecterErr: `context "existing-context" already exists`, }, { - options: createOptions{ - name: "invalid-docker-host", - docker: map[string]string{ + options: CreateOptions{ + Name: "invalid-docker-host", + Docker: map[string]string{ keyHost: "some///invalid/host", }, }, expecterErr: `unable to parse docker host`, }, { - options: createOptions{ - name: "invalid-orchestrator", - defaultStackOrchestrator: "invalid", + options: CreateOptions{ + Name: "invalid-orchestrator", + DefaultStackOrchestrator: "invalid", }, expecterErr: `specified orchestrator "invalid" is invalid, please use either kubernetes, swarm or all`, }, { - options: createOptions{ - name: "orchestrator-swarm-no-endpoint", - defaultStackOrchestrator: "swarm", + options: CreateOptions{ + Name: "orchestrator-swarm-no-endpoint", + DefaultStackOrchestrator: "swarm", }, expecterErr: `docker endpoint configuration is required`, }, { - options: createOptions{ - name: "orchestrator-kubernetes-no-endpoint", - defaultStackOrchestrator: "kubernetes", - docker: map[string]string{}, + options: CreateOptions{ + Name: "orchestrator-kubernetes-no-endpoint", + DefaultStackOrchestrator: "kubernetes", + Docker: map[string]string{}, }, expecterErr: `cannot specify orchestrator "kubernetes" without configuring a Kubernetes endpoint`, }, { - options: createOptions{ - name: "orchestrator-all-no-endpoint", - defaultStackOrchestrator: "all", - docker: map[string]string{}, + options: CreateOptions{ + Name: "orchestrator-all-no-endpoint", + DefaultStackOrchestrator: "all", + Docker: map[string]string{}, }, expecterErr: `cannot specify orchestrator "all" without configuring a Kubernetes endpoint`, }, } for _, tc := range tests { tc := tc - t.Run(tc.options.name, func(t *testing.T) { - err := runCreate(cli, &tc.options) + t.Run(tc.options.Name, func(t *testing.T) { + err := RunCreate(cli, &tc.options) assert.ErrorContains(t, err, tc.expecterErr) }) } @@ -117,10 +117,10 @@ func TestCreateOrchestratorSwarm(t *testing.T) { cli, cleanup := makeFakeCli(t) defer cleanup() - err := runCreate(cli, &createOptions{ - name: "test", - defaultStackOrchestrator: "swarm", - docker: map[string]string{}, + err := RunCreate(cli, &CreateOptions{ + Name: "test", + DefaultStackOrchestrator: "swarm", + Docker: map[string]string{}, }) assert.NilError(t, err) assert.Equal(t, "test\n", cli.OutBuffer().String()) @@ -131,9 +131,9 @@ func TestCreateOrchestratorEmpty(t *testing.T) { cli, cleanup := makeFakeCli(t) defer cleanup() - err := runCreate(cli, &createOptions{ - name: "test", - docker: map[string]string{}, + err := RunCreate(cli, &CreateOptions{ + Name: "test", + Docker: map[string]string{}, }) assert.NilError(t, err) } @@ -156,13 +156,13 @@ func createTestContextWithKube(t *testing.T, cli command.Cli) { revert := env.Patch(t, "KUBECONFIG", "./testdata/test-kubeconfig") defer revert() - err := runCreate(cli, &createOptions{ - name: "test", - defaultStackOrchestrator: "all", - kubernetes: map[string]string{ + err := RunCreate(cli, &CreateOptions{ + Name: "test", + DefaultStackOrchestrator: "all", + Kubernetes: map[string]string{ keyFromCurrent: "true", }, - docker: map[string]string{}, + Docker: map[string]string{}, }) assert.NilError(t, err) } diff --git a/cli/command/context/export-import_test.go b/cli/command/context/export-import_test.go index 94d8e6aafe..9a7a41a2b8 100644 --- a/cli/command/context/export-import_test.go +++ b/cli/command/context/export-import_test.go @@ -21,14 +21,14 @@ func TestExportImportWithFile(t *testing.T) { defer cleanup() createTestContextWithKube(t, cli) cli.ErrBuffer().Reset() - assert.NilError(t, runExport(cli, &exportOptions{ - contextName: "test", - dest: contextFile, + assert.NilError(t, RunExport(cli, &ExportOptions{ + ContextName: "test", + Dest: contextFile, })) assert.Equal(t, cli.ErrBuffer().String(), fmt.Sprintf("Written file %q\n", contextFile)) cli.OutBuffer().Reset() cli.ErrBuffer().Reset() - assert.NilError(t, runImport(cli, "test2", contextFile)) + assert.NilError(t, RunImport(cli, "test2", contextFile)) context1, err := cli.ContextStore().GetContextMetadata("test") assert.NilError(t, err) context2, err := cli.ContextStore().GetContextMetadata("test2") @@ -48,15 +48,15 @@ func TestExportImportPipe(t *testing.T) { createTestContextWithKube(t, cli) cli.ErrBuffer().Reset() cli.OutBuffer().Reset() - assert.NilError(t, runExport(cli, &exportOptions{ - contextName: "test", - dest: "-", + assert.NilError(t, RunExport(cli, &ExportOptions{ + ContextName: "test", + Dest: "-", })) assert.Equal(t, cli.ErrBuffer().String(), "") cli.SetIn(streams.NewIn(ioutil.NopCloser(bytes.NewBuffer(cli.OutBuffer().Bytes())))) cli.OutBuffer().Reset() cli.ErrBuffer().Reset() - assert.NilError(t, runImport(cli, "test2", "-")) + assert.NilError(t, RunImport(cli, "test2", "-")) context1, err := cli.ContextStore().GetContextMetadata("test") assert.NilError(t, err) context2, err := cli.ContextStore().GetContextMetadata("test2") @@ -79,18 +79,18 @@ func TestExportKubeconfig(t *testing.T) { defer cleanup() createTestContextWithKube(t, cli) cli.ErrBuffer().Reset() - assert.NilError(t, runExport(cli, &exportOptions{ - contextName: "test", - dest: contextFile, - kubeconfig: true, + assert.NilError(t, RunExport(cli, &ExportOptions{ + ContextName: "test", + Dest: contextFile, + Kubeconfig: true, })) assert.Equal(t, cli.ErrBuffer().String(), fmt.Sprintf("Written file %q\n", contextFile)) - assert.NilError(t, runCreate(cli, &createOptions{ - name: "test2", - kubernetes: map[string]string{ + assert.NilError(t, RunCreate(cli, &CreateOptions{ + Name: "test2", + Kubernetes: map[string]string{ keyKubeconfig: contextFile, }, - docker: map[string]string{}, + Docker: map[string]string{}, })) validateTestKubeEndpoint(t, cli.ContextStore(), "test2") } @@ -105,6 +105,6 @@ func TestExportExistingFile(t *testing.T) { createTestContextWithKube(t, cli) cli.ErrBuffer().Reset() assert.NilError(t, ioutil.WriteFile(contextFile, []byte{}, 0644)) - err = runExport(cli, &exportOptions{contextName: "test", dest: contextFile}) + err = RunExport(cli, &ExportOptions{ContextName: "test", Dest: contextFile}) assert.Assert(t, os.IsExist(err)) } diff --git a/cli/command/context/export.go b/cli/command/context/export.go index 060abf977d..870efcc3a4 100644 --- a/cli/command/context/export.go +++ b/cli/command/context/export.go @@ -15,36 +15,37 @@ import ( "k8s.io/client-go/tools/clientcmd" ) -type exportOptions struct { - kubeconfig bool - contextName string - dest string +// ExportOptions are the options used for exporting a context +type ExportOptions struct { + Kubeconfig bool + ContextName string + Dest string } func newExportCommand(dockerCli command.Cli) *cobra.Command { - opts := &exportOptions{} + opts := &ExportOptions{} cmd := &cobra.Command{ Use: "export [OPTIONS] CONTEXT [FILE|-]", Short: "Export a context to a tar or kubeconfig file", Args: cli.RequiresRangeArgs(1, 2), RunE: func(cmd *cobra.Command, args []string) error { - opts.contextName = args[0] + opts.ContextName = args[0] if len(args) == 2 { - opts.dest = args[1] + opts.Dest = args[1] } else { - opts.dest = opts.contextName - if opts.kubeconfig { - opts.dest += ".kubeconfig" + opts.Dest = opts.ContextName + if opts.Kubeconfig { + opts.Dest += ".kubeconfig" } else { - opts.dest += ".dockercontext" + opts.Dest += ".dockercontext" } } - return runExport(dockerCli, opts) + return RunExport(dockerCli, opts) }, } flags := cmd.Flags() - flags.BoolVar(&opts.kubeconfig, "kubeconfig", false, "Export as a kubeconfig file") + flags.BoolVar(&opts.Kubeconfig, "kubeconfig", false, "Export as a kubeconfig file") return cmd } @@ -74,24 +75,25 @@ func writeTo(dockerCli command.Cli, reader io.Reader, dest string) error { return nil } -func runExport(dockerCli command.Cli, opts *exportOptions) error { - if err := validateContextName(opts.contextName); err != nil { +// RunExport exports a Docker context +func RunExport(dockerCli command.Cli, opts *ExportOptions) error { + if err := validateContextName(opts.ContextName); err != nil { return err } - ctxMeta, err := dockerCli.ContextStore().GetContextMetadata(opts.contextName) + ctxMeta, err := dockerCli.ContextStore().GetContextMetadata(opts.ContextName) if err != nil { return err } - if !opts.kubeconfig { - reader := store.Export(opts.contextName, dockerCli.ContextStore()) + if !opts.Kubeconfig { + reader := store.Export(opts.ContextName, dockerCli.ContextStore()) defer reader.Close() - return writeTo(dockerCli, reader, opts.dest) + return writeTo(dockerCli, reader, opts.Dest) } kubernetesEndpointMeta := kubernetes.EndpointFromContext(ctxMeta) if kubernetesEndpointMeta == nil { - return fmt.Errorf("context %q has no kubernetes endpoint", opts.contextName) + return fmt.Errorf("context %q has no kubernetes endpoint", opts.ContextName) } - kubernetesEndpoint, err := kubernetesEndpointMeta.WithTLSData(dockerCli.ContextStore(), opts.contextName) + kubernetesEndpoint, err := kubernetesEndpointMeta.WithTLSData(dockerCli.ContextStore(), opts.ContextName) if err != nil { return err } @@ -104,5 +106,5 @@ func runExport(dockerCli command.Cli, opts *exportOptions) error { if err != nil { return err } - return writeTo(dockerCli, bytes.NewBuffer(data), opts.dest) + return writeTo(dockerCli, bytes.NewBuffer(data), opts.Dest) } diff --git a/cli/command/context/import.go b/cli/command/context/import.go index b1f68ec4ee..27d5432e48 100644 --- a/cli/command/context/import.go +++ b/cli/command/context/import.go @@ -17,13 +17,14 @@ func newImportCommand(dockerCli command.Cli) *cobra.Command { Short: "Import a context from a tar file", Args: cli.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - return runImport(dockerCli, args[0], args[1]) + return RunImport(dockerCli, args[0], args[1]) }, } return cmd } -func runImport(dockerCli command.Cli, name string, source string) error { +// RunImport imports a Docker context +func RunImport(dockerCli command.Cli, name string, source string) error { if err := checkContextNameForCreation(dockerCli.ContextStore(), name); err != nil { return err } diff --git a/cli/command/context/list_test.go b/cli/command/context/list_test.go index 1edf34ae2a..902f620c28 100644 --- a/cli/command/context/list_test.go +++ b/cli/command/context/list_test.go @@ -14,12 +14,12 @@ func createTestContextWithKubeAndSwarm(t *testing.T, cli command.Cli, name strin revert := env.Patch(t, "KUBECONFIG", "./testdata/test-kubeconfig") defer revert() - err := runCreate(cli, &createOptions{ - name: name, - defaultStackOrchestrator: orchestrator, - description: "description of " + name, - kubernetes: map[string]string{keyFromCurrent: "true"}, - docker: map[string]string{keyHost: "https://someswarmserver"}, + err := RunCreate(cli, &CreateOptions{ + Name: name, + DefaultStackOrchestrator: orchestrator, + Description: "description of " + name, + Kubernetes: map[string]string{keyFromCurrent: "true"}, + Docker: map[string]string{keyHost: "https://someswarmserver"}, }) assert.NilError(t, err) } diff --git a/cli/command/context/remove.go b/cli/command/context/remove.go index bacff0b0b8..9b7411df82 100644 --- a/cli/command/context/remove.go +++ b/cli/command/context/remove.go @@ -10,32 +10,34 @@ import ( "github.com/spf13/cobra" ) -type removeOptions struct { - force bool +// RemoveOptions are the options used to remove contexts +type RemoveOptions struct { + Force bool } func newRemoveCommand(dockerCli command.Cli) *cobra.Command { - var opts removeOptions + var opts RemoveOptions cmd := &cobra.Command{ Use: "rm CONTEXT [CONTEXT...]", Aliases: []string{"remove"}, Short: "Remove one or more contexts", Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return runRemove(dockerCli, opts, args) + return RunRemove(dockerCli, opts, args) }, } - cmd.Flags().BoolVarP(&opts.force, "force", "f", false, "Force the removal of a context in use") + cmd.Flags().BoolVarP(&opts.Force, "force", "f", false, "Force the removal of a context in use") return cmd } -func runRemove(dockerCli command.Cli, opts removeOptions, names []string) error { +// RunRemove removes one or more contexts +func RunRemove(dockerCli command.Cli, opts RemoveOptions, names []string) error { var errs []string currentCtx := dockerCli.CurrentContext() for _, name := range names { if name == "default" { errs = append(errs, `default: context "default" cannot be removed`) - } else if err := doRemove(dockerCli, name, name == currentCtx, opts.force); err != nil { + } else if err := doRemove(dockerCli, name, name == currentCtx, opts.Force); err != nil { errs = append(errs, fmt.Sprintf("%s: %s", name, err)) } else { fmt.Fprintln(dockerCli.Out(), name) diff --git a/cli/command/context/remove_test.go b/cli/command/context/remove_test.go index bc6438fb78..53ad12f516 100644 --- a/cli/command/context/remove_test.go +++ b/cli/command/context/remove_test.go @@ -17,7 +17,7 @@ func TestRemove(t *testing.T) { defer cleanup() createTestContextWithKubeAndSwarm(t, cli, "current", "all") createTestContextWithKubeAndSwarm(t, cli, "other", "all") - assert.NilError(t, runRemove(cli, removeOptions{}, []string{"other"})) + assert.NilError(t, RunRemove(cli, RemoveOptions{}, []string{"other"})) _, err := cli.ContextStore().GetContextMetadata("current") assert.NilError(t, err) _, err = cli.ContextStore().GetContextMetadata("other") @@ -29,7 +29,7 @@ func TestRemoveNotAContext(t *testing.T) { defer cleanup() createTestContextWithKubeAndSwarm(t, cli, "current", "all") createTestContextWithKubeAndSwarm(t, cli, "other", "all") - err := runRemove(cli, removeOptions{}, []string{"not-a-context"}) + err := RunRemove(cli, RemoveOptions{}, []string{"not-a-context"}) assert.ErrorContains(t, err, `context "not-a-context" does not exist`) } @@ -39,7 +39,7 @@ func TestRemoveCurrent(t *testing.T) { createTestContextWithKubeAndSwarm(t, cli, "current", "all") createTestContextWithKubeAndSwarm(t, cli, "other", "all") cli.SetCurrentContext("current") - err := runRemove(cli, removeOptions{}, []string{"current"}) + err := RunRemove(cli, RemoveOptions{}, []string{"current"}) assert.ErrorContains(t, err, "current: context is in use, set -f flag to force remove") } @@ -57,7 +57,7 @@ func TestRemoveCurrentForce(t *testing.T) { createTestContextWithKubeAndSwarm(t, cli, "current", "all") createTestContextWithKubeAndSwarm(t, cli, "other", "all") cli.SetCurrentContext("current") - assert.NilError(t, runRemove(cli, removeOptions{force: true}, []string{"current"})) + assert.NilError(t, RunRemove(cli, RemoveOptions{Force: true}, []string{"current"})) reloadedConfig, err := config.Load(configDir) assert.NilError(t, err) assert.Equal(t, "", reloadedConfig.CurrentContext) diff --git a/cli/command/context/update.go b/cli/command/context/update.go index 24fae3b61f..67e1bc0fcb 100644 --- a/cli/command/context/update.go +++ b/cli/command/context/update.go @@ -14,12 +14,13 @@ import ( "github.com/spf13/cobra" ) -type updateOptions struct { - name string - description string - defaultStackOrchestrator string - docker map[string]string - kubernetes map[string]string +// UpdateOptions are the options used to update a context +type UpdateOptions struct { + Name string + Description string + DefaultStackOrchestrator string + Docker map[string]string + Kubernetes map[string]string } func longUpdateDescription() string { @@ -43,34 +44,35 @@ func longUpdateDescription() string { } func newUpdateCommand(dockerCli command.Cli) *cobra.Command { - opts := &updateOptions{} + opts := &UpdateOptions{} cmd := &cobra.Command{ Use: "update [OPTIONS] CONTEXT", Short: "Update a context", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts.name = args[0] - return runUpdate(dockerCli, opts) + opts.Name = args[0] + return RunUpdate(dockerCli, opts) }, Long: longUpdateDescription(), } flags := cmd.Flags() - flags.StringVar(&opts.description, "description", "", "Description of the context") + flags.StringVar(&opts.Description, "description", "", "Description of the context") flags.StringVar( - &opts.defaultStackOrchestrator, + &opts.DefaultStackOrchestrator, "default-stack-orchestrator", "", "Default orchestrator for stack operations to use with this context (swarm|kubernetes|all)") - flags.StringToStringVar(&opts.docker, "docker", nil, "set the docker endpoint") - flags.StringToStringVar(&opts.kubernetes, "kubernetes", nil, "set the kubernetes endpoint") + flags.StringToStringVar(&opts.Docker, "docker", nil, "set the docker endpoint") + flags.StringToStringVar(&opts.Kubernetes, "kubernetes", nil, "set the kubernetes endpoint") return cmd } -func runUpdate(cli command.Cli, o *updateOptions) error { - if err := validateContextName(o.name); err != nil { +// RunUpdate updates a Docker context +func RunUpdate(cli command.Cli, o *UpdateOptions) error { + if err := validateContextName(o.Name); err != nil { return err } s := cli.ContextStore() - c, err := s.GetContextMetadata(o.name) + c, err := s.GetContextMetadata(o.Name) if err != nil { return err } @@ -78,31 +80,31 @@ func runUpdate(cli command.Cli, o *updateOptions) error { if err != nil { return err } - if o.defaultStackOrchestrator != "" { - stackOrchestrator, err := command.NormalizeOrchestrator(o.defaultStackOrchestrator) + if o.DefaultStackOrchestrator != "" { + stackOrchestrator, err := command.NormalizeOrchestrator(o.DefaultStackOrchestrator) if err != nil { return errors.Wrap(err, "unable to parse default-stack-orchestrator") } dockerContext.StackOrchestrator = stackOrchestrator } - if o.description != "" { - dockerContext.Description = o.description + if o.Description != "" { + dockerContext.Description = o.Description } c.Metadata = dockerContext tlsDataToReset := make(map[string]*store.EndpointTLSData) - if o.docker != nil { - dockerEP, dockerTLS, err := getDockerEndpointMetadataAndTLS(cli, o.docker) + if o.Docker != nil { + dockerEP, dockerTLS, err := getDockerEndpointMetadataAndTLS(cli, o.Docker) if err != nil { return errors.Wrap(err, "unable to create docker endpoint config") } c.Endpoints[docker.DockerEndpoint] = dockerEP tlsDataToReset[docker.DockerEndpoint] = dockerTLS } - if o.kubernetes != nil { - kubernetesEP, kubernetesTLS, err := getKubernetesEndpointMetadataAndTLS(cli, o.kubernetes) + if o.Kubernetes != nil { + kubernetesEP, kubernetesTLS, err := getKubernetesEndpointMetadataAndTLS(cli, o.Kubernetes) if err != nil { return errors.Wrap(err, "unable to create kubernetes endpoint config") } @@ -120,13 +122,13 @@ func runUpdate(cli command.Cli, o *updateOptions) error { return err } for ep, tlsData := range tlsDataToReset { - if err := s.ResetContextEndpointTLSMaterial(o.name, ep, tlsData); err != nil { + if err := s.ResetContextEndpointTLSMaterial(o.Name, ep, tlsData); err != nil { return err } } - fmt.Fprintln(cli.Out(), o.name) - fmt.Fprintf(cli.Err(), "Successfully updated context %q\n", o.name) + fmt.Fprintln(cli.Out(), o.Name) + fmt.Fprintf(cli.Err(), "Successfully updated context %q\n", o.Name) return nil } diff --git a/cli/command/context/update_test.go b/cli/command/context/update_test.go index 49109bf9dd..61310e7a6c 100644 --- a/cli/command/context/update_test.go +++ b/cli/command/context/update_test.go @@ -13,17 +13,17 @@ import ( func TestUpdateDescriptionOnly(t *testing.T) { cli, cleanup := makeFakeCli(t) defer cleanup() - err := runCreate(cli, &createOptions{ - name: "test", - defaultStackOrchestrator: "swarm", - docker: map[string]string{}, + err := RunCreate(cli, &CreateOptions{ + Name: "test", + DefaultStackOrchestrator: "swarm", + Docker: map[string]string{}, }) assert.NilError(t, err) cli.OutBuffer().Reset() cli.ErrBuffer().Reset() - assert.NilError(t, runUpdate(cli, &updateOptions{ - name: "test", - description: "description", + assert.NilError(t, RunUpdate(cli, &UpdateOptions{ + Name: "test", + Description: "description", })) c, err := cli.ContextStore().GetContextMetadata("test") assert.NilError(t, err) @@ -40,9 +40,9 @@ func TestUpdateDockerOnly(t *testing.T) { cli, cleanup := makeFakeCli(t) defer cleanup() createTestContextWithKubeAndSwarm(t, cli, "test", "swarm") - assert.NilError(t, runUpdate(cli, &updateOptions{ - name: "test", - docker: map[string]string{ + assert.NilError(t, RunUpdate(cli, &UpdateOptions{ + Name: "test", + Docker: map[string]string{ keyHost: "tcp://some-host", }, })) @@ -60,15 +60,15 @@ func TestUpdateDockerOnly(t *testing.T) { func TestUpdateStackOrchestratorStrategy(t *testing.T) { cli, cleanup := makeFakeCli(t) defer cleanup() - err := runCreate(cli, &createOptions{ - name: "test", - defaultStackOrchestrator: "swarm", - docker: map[string]string{}, + err := RunCreate(cli, &CreateOptions{ + Name: "test", + DefaultStackOrchestrator: "swarm", + Docker: map[string]string{}, }) assert.NilError(t, err) - err = runUpdate(cli, &updateOptions{ - name: "test", - defaultStackOrchestrator: "kubernetes", + err = RunUpdate(cli, &UpdateOptions{ + Name: "test", + DefaultStackOrchestrator: "kubernetes", }) assert.ErrorContains(t, err, `cannot specify orchestrator "kubernetes" without configuring a Kubernetes endpoint`) } @@ -77,9 +77,9 @@ func TestUpdateStackOrchestratorStrategyRemoveKubeEndpoint(t *testing.T) { cli, cleanup := makeFakeCli(t) defer cleanup() createTestContextWithKubeAndSwarm(t, cli, "test", "kubernetes") - err := runUpdate(cli, &updateOptions{ - name: "test", - kubernetes: map[string]string{}, + err := RunUpdate(cli, &UpdateOptions{ + Name: "test", + Kubernetes: map[string]string{}, }) assert.ErrorContains(t, err, `cannot specify orchestrator "kubernetes" without configuring a Kubernetes endpoint`) } @@ -87,14 +87,14 @@ func TestUpdateStackOrchestratorStrategyRemoveKubeEndpoint(t *testing.T) { func TestUpdateInvalidDockerHost(t *testing.T) { cli, cleanup := makeFakeCli(t) defer cleanup() - err := runCreate(cli, &createOptions{ - name: "test", - docker: map[string]string{}, + err := RunCreate(cli, &CreateOptions{ + Name: "test", + Docker: map[string]string{}, }) assert.NilError(t, err) - err = runUpdate(cli, &updateOptions{ - name: "test", - docker: map[string]string{ + err = RunUpdate(cli, &UpdateOptions{ + Name: "test", + Docker: map[string]string{ keyHost: "some///invalid/host", }, }) diff --git a/cli/command/context/use.go b/cli/command/context/use.go index bdffda3c9f..e6b296d965 100644 --- a/cli/command/context/use.go +++ b/cli/command/context/use.go @@ -14,26 +14,30 @@ func newUseCommand(dockerCli command.Cli) *cobra.Command { Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { name := args[0] - - if err := validateContextName(name); err != nil && name != "default" { - return err - } - if _, err := dockerCli.ContextStore().GetContextMetadata(name); err != nil && name != "default" { - return err - } - configValue := name - if configValue == "default" { - configValue = "" - } - dockerConfig := dockerCli.ConfigFile() - dockerConfig.CurrentContext = configValue - if err := dockerConfig.Save(); err != nil { - return err - } - fmt.Fprintln(dockerCli.Out(), name) - fmt.Fprintf(dockerCli.Err(), "Current context is now %q\n", name) - return nil + return RunUse(dockerCli, name) }, } return cmd } + +// RunUse set the current Docker context +func RunUse(dockerCli command.Cli, name string) error { + if err := validateContextName(name); err != nil && name != "default" { + return err + } + if _, err := dockerCli.ContextStore().GetContextMetadata(name); err != nil && name != "default" { + return err + } + configValue := name + if configValue == "default" { + configValue = "" + } + dockerConfig := dockerCli.ConfigFile() + dockerConfig.CurrentContext = configValue + if err := dockerConfig.Save(); err != nil { + return err + } + fmt.Fprintln(dockerCli.Out(), name) + fmt.Fprintf(dockerCli.Err(), "Current context is now %q\n", name) + return nil +} diff --git a/cli/command/context/use_test.go b/cli/command/context/use_test.go index 7fd309b47a..139189af35 100644 --- a/cli/command/context/use_test.go +++ b/cli/command/context/use_test.go @@ -20,9 +20,9 @@ func TestUse(t *testing.T) { testCfg := configfile.New(configFilePath) cli, cleanup := makeFakeCli(t, withCliConfig(testCfg)) defer cleanup() - err = runCreate(cli, &createOptions{ - name: "test", - docker: map[string]string{}, + err = RunCreate(cli, &CreateOptions{ + Name: "test", + Docker: map[string]string{}, }) assert.NilError(t, err) assert.NilError(t, newUseCommand(cli).RunE(nil, []string{"test"})) diff --git a/cli/context/store/storeconfig.go b/cli/context/store/storeconfig.go index 9746d93d77..b282a9d10e 100644 --- a/cli/context/store/storeconfig.go +++ b/cli/context/store/storeconfig.go @@ -25,6 +25,11 @@ type Config struct { endpointTypes map[string]TypeGetter } +// SetEndpoint set an endpoint typing information +func (c Config) SetEndpoint(name string, getter TypeGetter) { + c.endpointTypes[name] = getter +} + // NewConfig creates a config object func NewConfig(contextType TypeGetter, endpoints ...NamedTypeGetter) Config { res := Config{ diff --git a/cli/context/store/storeconfig_test.go b/cli/context/store/storeconfig_test.go new file mode 100644 index 0000000000..8cc5c79070 --- /dev/null +++ b/cli/context/store/storeconfig_test.go @@ -0,0 +1,31 @@ +package store + +import ( + "testing" + + "gotest.tools/assert" +) + +type testCtx struct{} +type testEP1 struct{} +type testEP2 struct{} +type testEP3 struct{} + +func TestConfigModification(t *testing.T) { + cfg := NewConfig(func() interface{} { return &testCtx{} }, EndpointTypeGetter("ep1", func() interface{} { return &testEP1{} })) + assert.Equal(t, &testCtx{}, cfg.contextType()) + assert.Equal(t, &testEP1{}, cfg.endpointTypes["ep1"]()) + cfgCopy := cfg + + // modify existing endpoint + cfg.SetEndpoint("ep1", func() interface{} { return &testEP2{} }) + // add endpoint + cfg.SetEndpoint("ep2", func() interface{} { return &testEP3{} }) + assert.Equal(t, &testCtx{}, cfg.contextType()) + assert.Equal(t, &testEP2{}, cfg.endpointTypes["ep1"]()) + assert.Equal(t, &testEP3{}, cfg.endpointTypes["ep2"]()) + // check it applied on already initialized store + assert.Equal(t, &testCtx{}, cfgCopy.contextType()) + assert.Equal(t, &testEP2{}, cfgCopy.endpointTypes["ep1"]()) + assert.Equal(t, &testEP3{}, cfgCopy.endpointTypes["ep2"]()) +}