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 <nick.adcock@docker.com>
This commit is contained in:
Nick Adcock 2019-03-22 14:20:40 +00:00
parent f40f9c240a
commit 8bb152d967
6 changed files with 301 additions and 56 deletions

View File

@ -21,6 +21,7 @@ type CreateOptions struct {
DefaultStackOrchestrator string DefaultStackOrchestrator string
Docker map[string]string Docker map[string]string
Kubernetes map[string]string Kubernetes map[string]string
From string
} }
func longCreateDescription() 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)") "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.Docker, "docker", nil, "set the docker endpoint")
flags.StringToStringVar(&opts.Kubernetes, "kubernetes", nil, "set the kubernetes endpoint") flags.StringToStringVar(&opts.Kubernetes, "kubernetes", nil, "set the kubernetes endpoint")
flags.StringVar(&opts.From, "from", "", "create context from a named context")
return cmd return cmd
} }
@ -76,17 +78,20 @@ func RunCreate(cli command.Cli, o *CreateOptions) error {
if err != nil { if err != nil {
return errors.Wrap(err, "unable to parse default-stack-orchestrator") return errors.Wrap(err, "unable to parse default-stack-orchestrator")
} }
contextMetadata := store.ContextMetadata{ if o.From == "" && o.Docker == nil && o.Kubernetes == nil {
Endpoints: make(map[string]interface{}), return createFromExistingContext(s, cli.CurrentContext(), stackOrchestrator, o)
Metadata: command.DockerContext{
Description: o.Description,
StackOrchestrator: stackOrchestrator,
},
Name: o.Name,
} }
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 { if o.Docker == nil {
return errors.New("docker endpoint configuration is required") return errors.New("docker endpoint configuration is required")
} }
contextMetadata := newContextMetadata(stackOrchestrator, o)
contextTLSData := store.ContextTLSData{ contextTLSData := store.ContextTLSData{
Endpoints: make(map[string]store.EndpointTLSData), Endpoints: make(map[string]store.EndpointTLSData),
} }
@ -139,3 +144,52 @@ func checkContextNameForCreation(s store.Store, name string) error {
} }
return nil 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,
}
}

View File

@ -105,13 +105,6 @@ func TestCreateInvalids(t *testing.T) {
}, },
expecterErr: `specified orchestrator "invalid" is invalid, please use either kubernetes, swarm or all`, 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{ options: CreateOptions{
Name: "orchestrator-kubernetes-no-endpoint", Name: "orchestrator-kubernetes-no-endpoint",
@ -185,7 +178,7 @@ func createTestContextWithKube(t *testing.T, cli command.Cli) {
Name: "test", Name: "test",
DefaultStackOrchestrator: "all", DefaultStackOrchestrator: "all",
Kubernetes: map[string]string{ Kubernetes: map[string]string{
keyFromCurrent: "true", keyFrom: "default",
}, },
Docker: map[string]string{}, Docker: map[string]string{},
}) })
@ -198,3 +191,157 @@ func TestCreateOrchestratorAllKubernetesEndpointFromCurrent(t *testing.T) {
createTestContextWithKube(t, cli) createTestContextWithKube(t, cli)
validateTestKubeEndpoint(t, cli.ContextStore(), "test") 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")
})
}
}

View File

@ -17,7 +17,7 @@ func createTestContextWithKubeAndSwarm(t *testing.T, cli command.Cli, name strin
Name: name, Name: name,
DefaultStackOrchestrator: orchestrator, DefaultStackOrchestrator: orchestrator,
Description: "description of " + name, Description: "description of " + name,
Kubernetes: map[string]string{keyFromCurrent: "true"}, Kubernetes: map[string]string{keyFrom: "default"},
Docker: map[string]string{keyHost: "https://someswarmserver"}, Docker: map[string]string{keyHost: "https://someswarmserver"},
}) })
assert.NilError(t, err) assert.NilError(t, err)

View File

@ -18,7 +18,7 @@ import (
) )
const ( const (
keyFromCurrent = "from-current" keyFrom = "from"
keyHost = "host" keyHost = "host"
keyCA = "ca" keyCA = "ca"
keyCert = "cert" keyCert = "cert"
@ -36,7 +36,7 @@ type configKeyDescription struct {
var ( var (
allowedDockerConfigKeys = map[string]struct{}{ allowedDockerConfigKeys = map[string]struct{}{
keyFromCurrent: {}, keyFrom: {},
keyHost: {}, keyHost: {},
keyCA: {}, keyCA: {},
keyCert: {}, keyCert: {},
@ -44,15 +44,15 @@ var (
keySkipTLSVerify: {}, keySkipTLSVerify: {},
} }
allowedKubernetesConfigKeys = map[string]struct{}{ allowedKubernetesConfigKeys = map[string]struct{}{
keyFromCurrent: {}, keyFrom: {},
keyKubeconfig: {}, keyKubeconfig: {},
keyKubecontext: {}, keyKubecontext: {},
keyKubenamespace: {}, keyKubenamespace: {},
} }
dockerConfigKeysDescriptions = []configKeyDescription{ dockerConfigKeysDescriptions = []configKeyDescription{
{ {
name: keyFromCurrent, name: keyFrom,
description: "Copy current Docker endpoint configuration", description: "Copy named context's Docker endpoint configuration",
}, },
{ {
name: keyHost, name: keyHost,
@ -77,8 +77,8 @@ var (
} }
kubernetesConfigKeysDescriptions = []configKeyDescription{ kubernetesConfigKeysDescriptions = []configKeyDescription{
{ {
name: keyFromCurrent, name: keyFrom,
description: "Copy current Kubernetes endpoint configuration", description: "Copy named context's Kubernetes endpoint configuration",
}, },
{ {
name: keyKubeconfig, name: keyKubeconfig,
@ -121,12 +121,15 @@ func getDockerEndpoint(dockerCli command.Cli, config map[string]string) (docker.
if err := validateConfig(config, allowedDockerConfigKeys); err != nil { if err := validateConfig(config, allowedDockerConfigKeys); err != nil {
return docker.Endpoint{}, err return docker.Endpoint{}, err
} }
fromCurrent, err := parseBool(config, keyFromCurrent) if contextName, ok := config[keyFrom]; ok {
if err != nil { metadata, err := dockerCli.ContextStore().GetContextMetadata(contextName)
return docker.Endpoint{}, err if err != nil {
} return docker.Endpoint{}, err
if fromCurrent { }
return dockerCli.DockerEndpoint(), nil 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]) tlsData, err := context.TLSDataFromFiles(config[keyCA], config[keyCert], config[keyKey])
if err != nil { if err != nil {
@ -169,25 +172,20 @@ func getKubernetesEndpoint(dockerCli command.Cli, config map[string]string) (*ku
if len(config) == 0 { if len(config) == 0 {
return nil, nil return nil, nil
} }
fromCurrent, err := parseBool(config, keyFromCurrent) if contextName, ok := config[keyFrom]; ok {
if err != nil { ctxMeta, err := dockerCli.ContextStore().GetContextMetadata(contextName)
return nil, err if err != nil {
} return nil, err
if fromCurrent { }
if dockerCli.CurrentContext() != "" { endpointMeta := kubernetes.EndpointFromContext(ctxMeta)
ctxMeta, err := dockerCli.ContextStore().GetContextMetadata(dockerCli.CurrentContext()) if endpointMeta != nil {
res, err := endpointMeta.WithTLSData(dockerCli.ContextStore(), dockerCli.CurrentContext())
if err != nil { if err != nil {
return nil, err return nil, err
} }
endpointMeta := kubernetes.EndpointFromContext(ctxMeta) return &res, nil
if endpointMeta != nil {
res, err := endpointMeta.WithTLSData(dockerCli.ContextStore(), dockerCli.CurrentContext())
if err != nil {
return nil, err
}
return &res, nil
}
} }
// fallback to env-based kubeconfig // fallback to env-based kubeconfig
kubeconfig := os.Getenv("KUBECONFIG") kubeconfig := os.Getenv("KUBECONFIG")
if kubeconfig == "" { if kubeconfig == "" {

View File

@ -23,7 +23,7 @@ Create a context
Docker endpoint config: Docker endpoint config:
NAME DESCRIPTION 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 host Docker endpoint on which to connect
ca Trust certs signed only by this CA ca Trust certs signed only by this CA
cert Path to TLS certificate file cert Path to TLS certificate file
@ -33,14 +33,16 @@ skip-tls-verify Skip TLS certificate validation
Kubernetes endpoint config: Kubernetes endpoint config:
NAME DESCRIPTION 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 config-file Path to a Kubernetes config file
context-override Overrides the context set in the 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 namespace-override Overrides the namespace set in the kubernetes config file
Example: 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: Options:
--default-stack-orchestrator string Default orchestrator for --default-stack-orchestrator string Default orchestrator for
@ -52,24 +54,68 @@ Options:
(default []) (default [])
--kubernetes stringToString set the kubernetes endpoint --kubernetes stringToString set the kubernetes endpoint
(default []) (default [])
--from string Create the context from an existing context
``` ```
## Description ## 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=<context-name>` 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 ```bash
$ source my-setup-script.sh $ 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=<context-name>` 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 ```bash
$ export KUBECONFIG=/path/to/my/kubeconfig $ docker context create my-context \
$ docker context create my-context --kubernetes "from-current=true" --docker "host=/var/run/docker.sock" --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` To source only the `kubernetes` configuration from an existing context use the
`--kubernetes from=<context-name>` 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`

View File

@ -23,7 +23,7 @@ Update a context
Docker endpoint config: Docker endpoint config:
NAME DESCRIPTION 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 host Docker endpoint on which to connect
ca Trust certs signed only by this CA ca Trust certs signed only by this CA
cert Path to TLS certificate file cert Path to TLS certificate file
@ -33,7 +33,7 @@ skip-tls-verify Skip TLS certificate validation
Kubernetes endpoint config: Kubernetes endpoint config:
NAME DESCRIPTION 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 config-file Path to a Kubernetes config file
context-override Overrides the context set in the 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 namespace-override Overrides the namespace set in the kubernetes config file