Skip to content

Commit 591385a

Browse files
committed
Fast Context Switch: commands
Signed-off-by: Simon Ferquel <simon.ferquel@docker.com>
1 parent b34f340 commit 591385a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+2295
-168
lines changed

cli/command/cli.go

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,6 @@ import (
3333
"github.com/theupdateframework/notary/passphrase"
3434
)
3535

36-
// ContextDockerHost is the reported context when DOCKER_HOST env var or -H flag is set
37-
const ContextDockerHost = "<DOCKER_HOST>"
38-
3936
// Streams is an interface which exposes the standard input and output streams
4037
type Streams interface {
4138
In() *InStream
@@ -62,6 +59,7 @@ type Cli interface {
6259
ContextStore() store.Store
6360
CurrentContext() string
6461
StackOrchestrator(flagValue string) (Orchestrator, error)
62+
DockerEndpoint() docker.Endpoint
6563
}
6664

6765
// DockerCli is an instance the docker command line client.
@@ -78,6 +76,7 @@ type DockerCli struct {
7876
newContainerizeClient func(string) (clitypes.ContainerizedClient, error)
7977
contextStore store.Store
8078
currentContext string
79+
dockerEndpoint docker.Endpoint
8180
}
8281

8382
var storeConfig = store.NewConfig(
@@ -182,14 +181,15 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
182181
cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err)
183182
var err error
184183
cli.contextStore = store.New(cliconfig.ContextStoreDir(), storeConfig)
185-
cli.currentContext, err = resolveContextName(opts.Common, cli.configFile)
184+
cli.currentContext, err = resolveContextName(opts.Common, cli.configFile, cli.contextStore)
186185
if err != nil {
187186
return err
188187
}
189188
endpoint, err := resolveDockerEndpoint(cli.contextStore, cli.currentContext, opts.Common)
190189
if err != nil {
191190
return errors.Wrap(err, "unable to resolve docker endpoint")
192191
}
192+
cli.dockerEndpoint = endpoint
193193

194194
cli.client, err = newAPIClientFromEndpoint(endpoint, cli.configFile)
195195
if tlsconfig.IsErrEncryptedKey(err) {
@@ -223,7 +223,7 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
223223
// NewAPIClientFromFlags creates a new APIClient from command line flags
224224
func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
225225
store := store.New(cliconfig.ContextStoreDir(), storeConfig)
226-
contextName, err := resolveContextName(opts, configFile)
226+
contextName, err := resolveContextName(opts, configFile, store)
227227
if err != nil {
228228
return nil, err
229229
}
@@ -249,7 +249,7 @@ func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigF
249249
}
250250

251251
func resolveDockerEndpoint(s store.Store, contextName string, opts *cliflags.CommonOptions) (docker.Endpoint, error) {
252-
if contextName != ContextDockerHost {
252+
if contextName != "" {
253253
ctxMeta, err := s.GetContextMetadata(contextName)
254254
if err != nil {
255255
return docker.Endpoint{}, err
@@ -258,7 +258,7 @@ func resolveDockerEndpoint(s store.Store, contextName string, opts *cliflags.Com
258258
if err != nil {
259259
return docker.Endpoint{}, err
260260
}
261-
return epMeta.WithTLSData(s, contextName)
261+
return docker.WithTLSData(s, contextName, epMeta)
262262
}
263263
host, err := getServerHost(opts.Hosts, opts.TLSOptions)
264264
if err != nil {
@@ -280,10 +280,8 @@ func resolveDockerEndpoint(s store.Store, contextName string, opts *cliflags.Com
280280

281281
return docker.Endpoint{
282282
EndpointMeta: docker.EndpointMeta{
283-
EndpointMetaBase: dcontext.EndpointMetaBase{
284-
Host: host,
285-
SkipTLSVerify: skipTLSVerify,
286-
},
283+
Host: host,
284+
SkipTLSVerify: skipTLSVerify,
287285
},
288286
TLSData: tlsData,
289287
}, nil
@@ -367,15 +365,16 @@ func (cli *DockerCli) StackOrchestrator(flagValue string) (Orchestrator, error)
367365
if currentContext == "" {
368366
currentContext = configFile.CurrentContext
369367
}
370-
if currentContext == "" {
371-
currentContext = ContextDockerHost
372-
}
373-
if currentContext != ContextDockerHost {
368+
if currentContext != "" {
374369
contextstore := cli.contextStore
375370
if contextstore == nil {
376371
contextstore = store.New(cliconfig.ContextStoreDir(), storeConfig)
377372
}
378373
ctxRaw, err := contextstore.GetContextMetadata(currentContext)
374+
if store.IsErrContextDoesNotExist(err) {
375+
// case where the currentContext has been removed (CLI behavior is to fallback to using DOCKER_HOST based resolution)
376+
return GetStackOrchestrator(flagValue, "", configFile.StackOrchestrator, cli.Err())
377+
}
379378
if err != nil {
380379
return "", err
381380
}
@@ -389,6 +388,11 @@ func (cli *DockerCli) StackOrchestrator(flagValue string) (Orchestrator, error)
389388
return GetStackOrchestrator(flagValue, ctxOrchestrator, configFile.StackOrchestrator, cli.Err())
390389
}
391390

391+
// DockerEndpoint returns the current docker endpoint
392+
func (cli *DockerCli) DockerEndpoint() docker.Endpoint {
393+
return cli.dockerEndpoint
394+
}
395+
392396
// ServerInfo stores details about the supported features and platform of the
393397
// server
394398
type ServerInfo struct {
@@ -435,24 +439,28 @@ func UserAgent() string {
435439
// - if DOCKER_CONTEXT is set, use this value
436440
// - if Config file has a globally set "CurrentContext", use this value
437441
// - fallbacks to default HOST, uses TLS config from flags/env vars
438-
func resolveContextName(opts *cliflags.CommonOptions, config *configfile.ConfigFile) (string, error) {
442+
func resolveContextName(opts *cliflags.CommonOptions, config *configfile.ConfigFile, contextstore store.Store) (string, error) {
439443
if opts.Context != "" && len(opts.Hosts) > 0 {
440-
return "", errors.New("Conflicting options: either specify --host or --context, not bot")
444+
return "", errors.New("Conflicting options: either specify --host or --context, not both")
441445
}
442446
if opts.Context != "" {
443447
return opts.Context, nil
444448
}
445449
if len(opts.Hosts) > 0 {
446-
return ContextDockerHost, nil
450+
return "", nil
447451
}
448452
if _, present := os.LookupEnv("DOCKER_HOST"); present {
449-
return ContextDockerHost, nil
453+
return "", nil
450454
}
451455
if ctxName, ok := os.LookupEnv("DOCKER_CONTEXT"); ok {
452456
return ctxName, nil
453457
}
454458
if config != nil && config.CurrentContext != "" {
455-
return config.CurrentContext, nil
459+
_, err := contextstore.GetContextMetadata(config.CurrentContext)
460+
if store.IsErrContextDoesNotExist(err) {
461+
return "", errors.Errorf("Current context %q is not found on the file system, please check your config file at %s", config.CurrentContext, config.Filename)
462+
}
463+
return config.CurrentContext, err
456464
}
457-
return ContextDockerHost, nil
465+
return "", nil
458466
}

cli/command/commands/commands.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/docker/cli/cli/command/checkpoint"
1010
"github.com/docker/cli/cli/command/config"
1111
"github.com/docker/cli/cli/command/container"
12+
"github.com/docker/cli/cli/command/context"
1213
"github.com/docker/cli/cli/command/engine"
1314
"github.com/docker/cli/cli/command/image"
1415
"github.com/docker/cli/cli/command/manifest"
@@ -86,6 +87,9 @@ func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
8687
// volume
8788
volume.NewVolumeCommand(dockerCli),
8889

90+
// context
91+
context.NewContextCommand(dockerCli),
92+
8993
// legacy commands may be hidden
9094
hide(system.NewEventsCommand(dockerCli)),
9195
hide(system.NewInfoCommand(dockerCli)),

cli/command/context.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import (
88

99
// DockerContext is a typed representation of what we put in Context metadata
1010
type DockerContext struct {
11-
Description string `json:"description,omitempty"`
12-
StackOrchestrator Orchestrator `json:"stack_orchestrator,omitempty"`
11+
Description string `json:",omitempty"`
12+
StackOrchestrator Orchestrator `json:",omitempty"`
1313
}
1414

1515
// GetDockerContext extracts metadata from stored context metadata

cli/command/context/cmd.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package context
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"regexp"
7+
8+
"github.com/docker/cli/cli"
9+
"github.com/docker/cli/cli/command"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
// NewContextCommand returns the context cli subcommand
14+
func NewContextCommand(dockerCli command.Cli) *cobra.Command {
15+
cmd := &cobra.Command{
16+
Use: "context",
17+
Short: "Manage contexts",
18+
Args: cli.NoArgs,
19+
RunE: command.ShowHelp(dockerCli.Err()),
20+
}
21+
cmd.AddCommand(
22+
newCreateCommand(dockerCli),
23+
newListCommand(dockerCli),
24+
newUseCommand(dockerCli),
25+
newExportCommand(dockerCli),
26+
newImportCommand(dockerCli),
27+
newRemoveCommand(dockerCli),
28+
newUpdateCommand(dockerCli),
29+
newInspectCommand(dockerCli),
30+
)
31+
return cmd
32+
}
33+
34+
const restrictedNamePattern = "^[a-zA-Z0-9][a-zA-Z0-9_.+-]+$"
35+
36+
var restrictedNameRegEx = regexp.MustCompile(restrictedNamePattern)
37+
38+
func validateContextName(name string) error {
39+
if name == "" {
40+
return errors.New("context name cannot be empty")
41+
}
42+
if name == "default" {
43+
return errors.New(`"default" is a reserved context name`)
44+
}
45+
if !restrictedNameRegEx.MatchString(name) {
46+
return fmt.Errorf("context name %q is invalid, names are validated against regexp %q", name, restrictedNamePattern)
47+
}
48+
return nil
49+
}

cli/command/context/create.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package context
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"text/tabwriter"
7+
8+
"github.com/docker/cli/cli"
9+
"github.com/docker/cli/cli/command"
10+
"github.com/docker/cli/cli/context/docker"
11+
"github.com/docker/cli/cli/context/kubernetes"
12+
"github.com/docker/cli/cli/context/store"
13+
"github.com/pkg/errors"
14+
"github.com/spf13/cobra"
15+
)
16+
17+
type createOptions struct {
18+
name string
19+
description string
20+
defaultStackOrchestrator string
21+
docker map[string]string
22+
kubernetes map[string]string
23+
}
24+
25+
func longCreateDescription() string {
26+
buf := bytes.NewBuffer(nil)
27+
buf.WriteString("Create a context\n\nDocker endpoint config:\n\n")
28+
tw := tabwriter.NewWriter(buf, 20, 1, 3, ' ', 0)
29+
fmt.Fprintln(tw, "NAME\tDESCRIPTION")
30+
for _, d := range dockerConfigKeysDescriptions {
31+
fmt.Fprintf(tw, "%s\t%s\n", d.name, d.description)
32+
}
33+
tw.Flush()
34+
buf.WriteString("\nKubernetes endpoint config:\n\n")
35+
tw = tabwriter.NewWriter(buf, 20, 1, 3, ' ', 0)
36+
fmt.Fprintln(tw, "NAME\tDESCRIPTION")
37+
for _, d := range kubernetesConfigKeysDescriptions {
38+
fmt.Fprintf(tw, "%s\t%s\n", d.name, d.description)
39+
}
40+
tw.Flush()
41+
buf.WriteString("\nExample:\n\n$ docker context create my-context --description \"some description\" --docker \"host=tcp://myserver:2376,ca=~/ca-file,cert=~/cert-file,key=~/key-file\"\n")
42+
return buf.String()
43+
}
44+
45+
func newCreateCommand(dockerCli command.Cli) *cobra.Command {
46+
opts := &createOptions{}
47+
cmd := &cobra.Command{
48+
Use: "create [OPTIONS] CONTEXT",
49+
Short: "Create a context",
50+
Args: cli.ExactArgs(1),
51+
RunE: func(cmd *cobra.Command, args []string) error {
52+
opts.name = args[0]
53+
return runCreate(dockerCli, opts)
54+
},
55+
Long: longCreateDescription(),
56+
}
57+
flags := cmd.Flags()
58+
flags.StringVar(&opts.description, "description", "", "Description of the context")
59+
flags.StringVar(
60+
&opts.defaultStackOrchestrator,
61+
"default-stack-orchestrator", "",
62+
"Default orchestrator for stack operations to use with this context (swarm|kubernetes|all)")
63+
flags.StringToStringVar(&opts.docker, "docker", nil, "set the docker endpoint")
64+
flags.StringToStringVar(&opts.kubernetes, "kubernetes", nil, "set the kubernetes endpoint")
65+
return cmd
66+
}
67+
68+
func runCreate(cli command.Cli, o *createOptions) error {
69+
s := cli.ContextStore()
70+
if err := checkContextNameForCreation(s, o.name); err != nil {
71+
return err
72+
}
73+
stackOrchestrator, err := command.NormalizeOrchestrator(o.defaultStackOrchestrator)
74+
if err != nil {
75+
return errors.Wrap(err, "unable to parse default-stack-orchestrator")
76+
}
77+
contextMetadata := store.ContextMetadata{
78+
Endpoints: make(map[string]interface{}),
79+
Metadata: command.DockerContext{
80+
Description: o.description,
81+
StackOrchestrator: stackOrchestrator,
82+
},
83+
Name: o.name,
84+
}
85+
if o.docker == nil {
86+
return errors.New("docker endpoint configuration is required")
87+
}
88+
contextTLSData := store.ContextTLSData{
89+
Endpoints: make(map[string]store.EndpointTLSData),
90+
}
91+
dockerEP, dockerTLS, err := getDockerEndpointMetadataAndTLS(cli, o.docker)
92+
if err != nil {
93+
return errors.Wrap(err, "unable to create docker endpoint config")
94+
}
95+
contextMetadata.Endpoints[docker.DockerEndpoint] = dockerEP
96+
if dockerTLS != nil {
97+
contextTLSData.Endpoints[docker.DockerEndpoint] = *dockerTLS
98+
}
99+
if o.kubernetes != nil {
100+
kubernetesEP, kubernetesTLS, err := getKubernetesEndpointMetadataAndTLS(cli, o.kubernetes)
101+
if err != nil {
102+
return errors.Wrap(err, "unable to create kubernetes endpoint config")
103+
}
104+
if kubernetesEP == nil && stackOrchestrator.HasKubernetes() {
105+
return errors.Errorf("cannot specify orchestrator %q without configuring a Kubernetes endpoint", stackOrchestrator)
106+
}
107+
if kubernetesEP != nil {
108+
contextMetadata.Endpoints[kubernetes.KubernetesEndpoint] = kubernetesEP
109+
}
110+
if kubernetesTLS != nil {
111+
contextTLSData.Endpoints[kubernetes.KubernetesEndpoint] = *kubernetesTLS
112+
}
113+
}
114+
if err := validateEndpointsAndOrchestrator(contextMetadata); err != nil {
115+
return err
116+
}
117+
if err := s.CreateOrUpdateContext(contextMetadata); err != nil {
118+
return err
119+
}
120+
if err := s.ResetContextTLSMaterial(o.name, &contextTLSData); err != nil {
121+
return err
122+
}
123+
fmt.Fprintln(cli.Out(), o.name)
124+
fmt.Fprintf(cli.Err(), "Successfully created context %q\n", o.name)
125+
return nil
126+
}
127+
128+
func checkContextNameForCreation(s store.Store, name string) error {
129+
if err := validateContextName(name); err != nil {
130+
return err
131+
}
132+
if _, err := s.GetContextMetadata(name); !store.IsErrContextDoesNotExist(err) {
133+
if err != nil {
134+
return errors.Wrap(err, "error while getting existing contexts")
135+
}
136+
return errors.Errorf("context %q already exists", name)
137+
}
138+
return nil
139+
}

0 commit comments

Comments
 (0)