From 8bb152d967e09444234c5122511e8fce5d155061 Mon Sep 17 00:00:00 2001 From: Nick Adcock Date: Fri, 22 Mar 2019 14:20:40 +0000 Subject: [PATCH] add --from option to context create --from creates a context from a named context. By default `context create` will create a context from the current context. Replaced "from-current=" docker/kubernetes option with "from=" to allow specifying which context to copy the settings from. Signed-off-by: Nick Adcock --- cli/command/context/create.go | 68 +++++++- cli/command/context/create_test.go | 163 ++++++++++++++++++- cli/command/context/list_test.go | 2 +- cli/command/context/options.go | 54 +++--- docs/reference/commandline/context_create.md | 66 ++++++-- docs/reference/commandline/context_update.md | 4 +- 6 files changed, 301 insertions(+), 56 deletions(-) diff --git a/cli/command/context/create.go b/cli/command/context/create.go index ce53fcffdf..3687aa5313 100644 --- a/cli/command/context/create.go +++ b/cli/command/context/create.go @@ -21,6 +21,7 @@ type CreateOptions struct { DefaultStackOrchestrator string Docker map[string]string Kubernetes map[string]string + From string } func longCreateDescription() string { @@ -63,6 +64,7 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command { "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.StringVar(&opts.From, "from", "", "create context from a named context") return cmd } @@ -76,17 +78,20 @@ func RunCreate(cli command.Cli, o *CreateOptions) error { 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, - StackOrchestrator: stackOrchestrator, - }, - Name: o.Name, + if o.From == "" && o.Docker == nil && o.Kubernetes == nil { + return createFromExistingContext(s, cli.CurrentContext(), stackOrchestrator, o) } + if o.From != "" { + return createFromExistingContext(s, o.From, stackOrchestrator, o) + } + return createNewContext(o, stackOrchestrator, cli, s) +} + +func createNewContext(o *CreateOptions, stackOrchestrator command.Orchestrator, cli command.Cli, s store.Store) error { if o.Docker == nil { return errors.New("docker endpoint configuration is required") } + contextMetadata := newContextMetadata(stackOrchestrator, o) contextTLSData := store.ContextTLSData{ Endpoints: make(map[string]store.EndpointTLSData), } @@ -139,3 +144,52 @@ func checkContextNameForCreation(s store.Store, name string) error { } return nil } + +func createFromExistingContext(s store.Store, fromContextName string, stackOrchestrator command.Orchestrator, o *CreateOptions) error { + if len(o.Docker) != 0 || len(o.Kubernetes) != 0 { + return errors.New("cannot use --docker or --kubernetes flags when --from is set") + } + reader := store.Export(fromContextName, &descriptionAndOrchestratorStoreDecorator{ + Store: s, + description: o.Description, + orchestrator: stackOrchestrator, + }) + defer reader.Close() + return store.Import(o.Name, s, reader) +} + +type descriptionAndOrchestratorStoreDecorator struct { + store.Store + description string + orchestrator command.Orchestrator +} + +func (d *descriptionAndOrchestratorStoreDecorator) GetContextMetadata(name string) (store.ContextMetadata, error) { + c, err := d.Store.GetContextMetadata(name) + if err != nil { + return c, err + } + typedContext, err := command.GetDockerContext(c) + if err != nil { + return c, err + } + if d.description != "" { + typedContext.Description = d.description + } + if d.orchestrator != command.Orchestrator("") { + typedContext.StackOrchestrator = d.orchestrator + } + c.Metadata = typedContext + return c, nil +} + +func newContextMetadata(stackOrchestrator command.Orchestrator, o *CreateOptions) store.ContextMetadata { + return store.ContextMetadata{ + Endpoints: make(map[string]interface{}), + Metadata: command.DockerContext{ + Description: o.Description, + StackOrchestrator: stackOrchestrator, + }, + Name: o.Name, + } +} diff --git a/cli/command/context/create_test.go b/cli/command/context/create_test.go index eea3731900..54bbcbe413 100644 --- a/cli/command/context/create_test.go +++ b/cli/command/context/create_test.go @@ -105,13 +105,6 @@ func TestCreateInvalids(t *testing.T) { }, expecterErr: `specified orchestrator "invalid" is invalid, please use either kubernetes, swarm or all`, }, - { - options: CreateOptions{ - Name: "orchestrator-swarm-no-endpoint", - DefaultStackOrchestrator: "swarm", - }, - expecterErr: `docker endpoint configuration is required`, - }, { options: CreateOptions{ Name: "orchestrator-kubernetes-no-endpoint", @@ -185,7 +178,7 @@ func createTestContextWithKube(t *testing.T, cli command.Cli) { Name: "test", DefaultStackOrchestrator: "all", Kubernetes: map[string]string{ - keyFromCurrent: "true", + keyFrom: "default", }, Docker: map[string]string{}, }) @@ -198,3 +191,157 @@ func TestCreateOrchestratorAllKubernetesEndpointFromCurrent(t *testing.T) { createTestContextWithKube(t, cli) validateTestKubeEndpoint(t, cli.ContextStore(), "test") } + +func TestCreateFromContext(t *testing.T) { + cases := []struct { + name string + description string + orchestrator string + expectedDescription string + docker map[string]string + kubernetes map[string]string + expectedOrchestrator command.Orchestrator + }{ + { + name: "no-override", + expectedDescription: "original description", + expectedOrchestrator: command.OrchestratorSwarm, + }, + { + name: "override-description", + description: "new description", + expectedDescription: "new description", + expectedOrchestrator: command.OrchestratorSwarm, + }, + { + name: "override-orchestrator", + orchestrator: "kubernetes", + expectedDescription: "original description", + expectedOrchestrator: command.OrchestratorKubernetes, + }, + } + + cli, cleanup := makeFakeCli(t) + defer cleanup() + revert := env.Patch(t, "KUBECONFIG", "./testdata/test-kubeconfig") + defer revert() + assert.NilError(t, RunCreate(cli, &CreateOptions{ + Name: "original", + Description: "original description", + Docker: map[string]string{ + keyHost: "tcp://42.42.42.42:2375", + }, + Kubernetes: map[string]string{ + keyFrom: "default", + }, + DefaultStackOrchestrator: "swarm", + })) + assert.NilError(t, RunCreate(cli, &CreateOptions{ + Name: "dummy", + Description: "dummy description", + Docker: map[string]string{ + keyHost: "tcp://24.24.24.24:2375", + }, + Kubernetes: map[string]string{ + keyFrom: "default", + }, + DefaultStackOrchestrator: "swarm", + })) + + cli.SetCurrentContext("dummy") + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + err := RunCreate(cli, &CreateOptions{ + From: "original", + Name: c.name, + Description: c.description, + DefaultStackOrchestrator: c.orchestrator, + Docker: c.docker, + Kubernetes: c.kubernetes, + }) + assert.NilError(t, err) + newContext, err := cli.ContextStore().GetContextMetadata(c.name) + assert.NilError(t, err) + newContextTyped, err := command.GetDockerContext(newContext) + assert.NilError(t, err) + dockerEndpoint, err := docker.EndpointFromContext(newContext) + assert.NilError(t, err) + kubeEndpoint := kubernetes.EndpointFromContext(newContext) + assert.Check(t, kubeEndpoint != nil) + assert.Equal(t, newContextTyped.Description, c.expectedDescription) + assert.Equal(t, newContextTyped.StackOrchestrator, c.expectedOrchestrator) + assert.Equal(t, dockerEndpoint.Host, "tcp://42.42.42.42:2375") + assert.Equal(t, kubeEndpoint.Host, "https://someserver") + }) + } +} + +func TestCreateFromCurrent(t *testing.T) { + cases := []struct { + name string + description string + orchestrator string + expectedDescription string + expectedOrchestrator command.Orchestrator + }{ + { + name: "no-override", + expectedDescription: "original description", + expectedOrchestrator: command.OrchestratorSwarm, + }, + { + name: "override-description", + description: "new description", + expectedDescription: "new description", + expectedOrchestrator: command.OrchestratorSwarm, + }, + { + name: "override-orchestrator", + orchestrator: "kubernetes", + expectedDescription: "original description", + expectedOrchestrator: command.OrchestratorKubernetes, + }, + } + + cli, cleanup := makeFakeCli(t) + defer cleanup() + revert := env.Patch(t, "KUBECONFIG", "./testdata/test-kubeconfig") + defer revert() + assert.NilError(t, RunCreate(cli, &CreateOptions{ + Name: "original", + Description: "original description", + Docker: map[string]string{ + keyHost: "tcp://42.42.42.42:2375", + }, + Kubernetes: map[string]string{ + keyFrom: "default", + }, + DefaultStackOrchestrator: "swarm", + })) + + cli.SetCurrentContext("original") + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + err := RunCreate(cli, &CreateOptions{ + Name: c.name, + Description: c.description, + DefaultStackOrchestrator: c.orchestrator, + }) + assert.NilError(t, err) + newContext, err := cli.ContextStore().GetContextMetadata(c.name) + assert.NilError(t, err) + newContextTyped, err := command.GetDockerContext(newContext) + assert.NilError(t, err) + dockerEndpoint, err := docker.EndpointFromContext(newContext) + assert.NilError(t, err) + kubeEndpoint := kubernetes.EndpointFromContext(newContext) + assert.Check(t, kubeEndpoint != nil) + assert.Equal(t, newContextTyped.Description, c.expectedDescription) + assert.Equal(t, newContextTyped.StackOrchestrator, c.expectedOrchestrator) + assert.Equal(t, dockerEndpoint.Host, "tcp://42.42.42.42:2375") + assert.Equal(t, kubeEndpoint.Host, "https://someserver") + }) + } +} diff --git a/cli/command/context/list_test.go b/cli/command/context/list_test.go index 8f1d05ee93..df4534dcd3 100644 --- a/cli/command/context/list_test.go +++ b/cli/command/context/list_test.go @@ -17,7 +17,7 @@ func createTestContextWithKubeAndSwarm(t *testing.T, cli command.Cli, name strin Name: name, DefaultStackOrchestrator: orchestrator, Description: "description of " + name, - Kubernetes: map[string]string{keyFromCurrent: "true"}, + Kubernetes: map[string]string{keyFrom: "default"}, Docker: map[string]string{keyHost: "https://someswarmserver"}, }) assert.NilError(t, err) diff --git a/cli/command/context/options.go b/cli/command/context/options.go index 338e808835..910ea85969 100644 --- a/cli/command/context/options.go +++ b/cli/command/context/options.go @@ -18,7 +18,7 @@ import ( ) const ( - keyFromCurrent = "from-current" + keyFrom = "from" keyHost = "host" keyCA = "ca" keyCert = "cert" @@ -36,7 +36,7 @@ type configKeyDescription struct { var ( allowedDockerConfigKeys = map[string]struct{}{ - keyFromCurrent: {}, + keyFrom: {}, keyHost: {}, keyCA: {}, keyCert: {}, @@ -44,15 +44,15 @@ var ( keySkipTLSVerify: {}, } allowedKubernetesConfigKeys = map[string]struct{}{ - keyFromCurrent: {}, + keyFrom: {}, keyKubeconfig: {}, keyKubecontext: {}, keyKubenamespace: {}, } dockerConfigKeysDescriptions = []configKeyDescription{ { - name: keyFromCurrent, - description: "Copy current Docker endpoint configuration", + name: keyFrom, + description: "Copy named context's Docker endpoint configuration", }, { name: keyHost, @@ -77,8 +77,8 @@ var ( } kubernetesConfigKeysDescriptions = []configKeyDescription{ { - name: keyFromCurrent, - description: "Copy current Kubernetes endpoint configuration", + name: keyFrom, + description: "Copy named context's Kubernetes endpoint configuration", }, { name: keyKubeconfig, @@ -121,12 +121,15 @@ func getDockerEndpoint(dockerCli command.Cli, config map[string]string) (docker. if err := validateConfig(config, allowedDockerConfigKeys); err != nil { return docker.Endpoint{}, err } - fromCurrent, err := parseBool(config, keyFromCurrent) - if err != nil { - return docker.Endpoint{}, err - } - if fromCurrent { - return dockerCli.DockerEndpoint(), nil + if contextName, ok := config[keyFrom]; ok { + metadata, err := dockerCli.ContextStore().GetContextMetadata(contextName) + if err != nil { + return docker.Endpoint{}, err + } + if ep, ok := metadata.Endpoints[docker.DockerEndpoint].(docker.EndpointMeta); ok { + return docker.Endpoint{EndpointMeta: ep}, nil + } + return docker.Endpoint{}, errors.Errorf("unable to get endpoint from context %q", contextName) } tlsData, err := context.TLSDataFromFiles(config[keyCA], config[keyCert], config[keyKey]) if err != nil { @@ -169,25 +172,20 @@ func getKubernetesEndpoint(dockerCli command.Cli, config map[string]string) (*ku if len(config) == 0 { return nil, nil } - fromCurrent, err := parseBool(config, keyFromCurrent) - if err != nil { - return nil, err - } - if fromCurrent { - if dockerCli.CurrentContext() != "" { - ctxMeta, err := dockerCli.ContextStore().GetContextMetadata(dockerCli.CurrentContext()) + if contextName, ok := config[keyFrom]; ok { + ctxMeta, err := dockerCli.ContextStore().GetContextMetadata(contextName) + if err != nil { + return nil, err + } + endpointMeta := kubernetes.EndpointFromContext(ctxMeta) + if endpointMeta != nil { + res, err := endpointMeta.WithTLSData(dockerCli.ContextStore(), dockerCli.CurrentContext()) if err != nil { return nil, err } - endpointMeta := kubernetes.EndpointFromContext(ctxMeta) - if endpointMeta != nil { - res, err := endpointMeta.WithTLSData(dockerCli.ContextStore(), dockerCli.CurrentContext()) - if err != nil { - return nil, err - } - return &res, nil - } + return &res, nil } + // fallback to env-based kubeconfig kubeconfig := os.Getenv("KUBECONFIG") if kubeconfig == "" { diff --git a/docs/reference/commandline/context_create.md b/docs/reference/commandline/context_create.md index 171f284289..996dce2a93 100644 --- a/docs/reference/commandline/context_create.md +++ b/docs/reference/commandline/context_create.md @@ -23,7 +23,7 @@ Create a context Docker endpoint config: NAME DESCRIPTION -from-current Copy current Docker endpoint configuration +from Copy Docker endpoint configuration from an existing context host Docker endpoint on which to connect ca Trust certs signed only by this CA cert Path to TLS certificate file @@ -33,14 +33,16 @@ skip-tls-verify Skip TLS certificate validation Kubernetes endpoint config: NAME DESCRIPTION -from-current Copy current Kubernetes endpoint configuration +from Copy Kubernetes endpoint configuration from an existing context config-file Path to a Kubernetes config file context-override Overrides the context set in the kubernetes config file namespace-override Overrides the namespace set in the kubernetes config file Example: -$ docker context create my-context --description "some description" --docker "host=tcp://myserver:2376,ca=~/ca-file,cert=~/cert-file,key=~/key-file" +$ docker context create my-context \ + --description "some description" \ + --docker "host=tcp://myserver:2376,ca=~/ca-file,cert=~/cert-file,key=~/key-file" Options: --default-stack-orchestrator string Default orchestrator for @@ -52,24 +54,68 @@ Options: (default []) --kubernetes stringToString set the kubernetes endpoint (default []) + --from string Create the context from an existing context ``` ## Description -Creates a new `context`. This will allow you to quickly switch the cli configuration to connect to different clusters or single nodes. +Creates a new `context`. This allows you to quickly switch the cli +configuration to connect to different clusters or single nodes. -To create a `context` out of an existing `DOCKER_HOST` based script, you can use the `from-current` config key: +To create a context from scratch provide the docker and, if required, +kubernetes options. The example below creates the context `my-context` +with a docker endpoint of `/var/run/docker.sock` and a kubernetes configuration +sourced from the file `/home/me/my-kube-config`: + +```bash +$ docker context create my-context \ + --docker host=/var/run/docker.sock \ + --kubernetes config-file=/home/me/my-kube-config +``` + +Use the `--from=` option to create a new context from +an existing context. The example below creates a new context named `my-context` +from the existing context `existing-context`: + +```bash +$ docker context create my-context --from existing-context +``` + +If the `--from` option is not set, the `context` is created from the current context: + +```bash +$ docker context create my-context +``` + +This can be used to create a context out of an existing `DOCKER_HOST` based script: ```bash $ source my-setup-script.sh -$ docker context create my-context --docker "from-current=true" +$ docker context create my-context ``` -Similarly, to reference the currently active Kubernetes configuration, you can use `--kubernetes "from-current=true"`: +To source only the `docker` endpoint configuration from an existing context +use the `--docker from=` option. The example below creates a +new context named `my-context` using the docker endpoint configuration from +the existing context `existing-context` and a kubernetes configuration sourced +from the file `/home/me/my-kube-config`: ```bash -$ export KUBECONFIG=/path/to/my/kubeconfig -$ docker context create my-context --kubernetes "from-current=true" --docker "host=/var/run/docker.sock" +$ docker context create my-context \ + --docker from=existing-context \ + --kubernetes config-file=/home/me/my-kube-config ``` -Docker and Kubernetes endpoints configurations, as well as default stack orchestrator and description can be modified with `docker context update` \ No newline at end of file +To source only the `kubernetes` configuration from an existing context use the +`--kubernetes from=` option. The example below creates a new +context named `my-context` using the kuberentes configuration from the existing +context `existing-context` and a docker endpoint of `/var/run/docker.sock`: + +```bash +$ docker context create my-context \ + --docker host=/var/run/docker.sock \ + --kubernetes from=existing-context +``` + +Docker and Kubernetes endpoints configurations, as well as default stack +orchestrator and description can be modified with `docker context update` \ No newline at end of file diff --git a/docs/reference/commandline/context_update.md b/docs/reference/commandline/context_update.md index 94add90112..fb143282f8 100644 --- a/docs/reference/commandline/context_update.md +++ b/docs/reference/commandline/context_update.md @@ -23,7 +23,7 @@ Update a context Docker endpoint config: NAME DESCRIPTION -from-current Copy current Docker endpoint configuration +from Copy Docker endpoint configuration from an existing context host Docker endpoint on which to connect ca Trust certs signed only by this CA cert Path to TLS certificate file @@ -33,7 +33,7 @@ skip-tls-verify Skip TLS certificate validation Kubernetes endpoint config: NAME DESCRIPTION -from-current Copy current Kubernetes endpoint configuration +from Copy Kubernetes endpoint configuration from an existing context config-file Path to a Kubernetes config file context-override Overrides the context set in the kubernetes config file namespace-override Overrides the namespace set in the kubernetes config file