Skip to content

Commit

Permalink
Fast Context Switch: commands
Browse files Browse the repository at this point in the history
Signed-off-by: Simon Ferquel <simon.ferquel@docker.com>
  • Loading branch information
simonferquel committed Jan 10, 2019
1 parent b34f340 commit 591385a
Show file tree
Hide file tree
Showing 48 changed files with 2,295 additions and 168 deletions.
50 changes: 29 additions & 21 deletions cli/command/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,6 @@ import (
"github.com/theupdateframework/notary/passphrase"
)

// ContextDockerHost is the reported context when DOCKER_HOST env var or -H flag is set
const ContextDockerHost = "<DOCKER_HOST>"

// Streams is an interface which exposes the standard input and output streams
type Streams interface {
In() *InStream
Expand All @@ -62,6 +59,7 @@ type Cli interface {
ContextStore() store.Store
CurrentContext() string
StackOrchestrator(flagValue string) (Orchestrator, error)
DockerEndpoint() docker.Endpoint
}

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

var storeConfig = store.NewConfig(
Expand Down Expand Up @@ -182,14 +181,15 @@ 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.currentContext, err = resolveContextName(opts.Common, cli.configFile)
cli.currentContext, err = resolveContextName(opts.Common, cli.configFile, cli.contextStore)
if err != nil {
return err
}
endpoint, err := resolveDockerEndpoint(cli.contextStore, cli.currentContext, opts.Common)
if err != nil {
return errors.Wrap(err, "unable to resolve docker endpoint")
}
cli.dockerEndpoint = endpoint

cli.client, err = newAPIClientFromEndpoint(endpoint, cli.configFile)
if tlsconfig.IsErrEncryptedKey(err) {
Expand Down Expand Up @@ -223,7 +223,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)
contextName, err := resolveContextName(opts, configFile)
contextName, err := resolveContextName(opts, configFile, store)
if err != nil {
return nil, err
}
Expand All @@ -249,7 +249,7 @@ func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigF
}

func resolveDockerEndpoint(s store.Store, contextName string, opts *cliflags.CommonOptions) (docker.Endpoint, error) {
if contextName != ContextDockerHost {
if contextName != "" {
ctxMeta, err := s.GetContextMetadata(contextName)
if err != nil {
return docker.Endpoint{}, err
Expand All @@ -258,7 +258,7 @@ func resolveDockerEndpoint(s store.Store, contextName string, opts *cliflags.Com
if err != nil {
return docker.Endpoint{}, err
}
return epMeta.WithTLSData(s, contextName)
return docker.WithTLSData(s, contextName, epMeta)
}
host, err := getServerHost(opts.Hosts, opts.TLSOptions)
if err != nil {
Expand All @@ -280,10 +280,8 @@ func resolveDockerEndpoint(s store.Store, contextName string, opts *cliflags.Com

return docker.Endpoint{
EndpointMeta: docker.EndpointMeta{
EndpointMetaBase: dcontext.EndpointMetaBase{
Host: host,
SkipTLSVerify: skipTLSVerify,
},
Host: host,
SkipTLSVerify: skipTLSVerify,
},
TLSData: tlsData,
}, nil
Expand Down Expand Up @@ -367,15 +365,16 @@ func (cli *DockerCli) StackOrchestrator(flagValue string) (Orchestrator, error)
if currentContext == "" {
currentContext = configFile.CurrentContext
}
if currentContext == "" {
currentContext = ContextDockerHost
}
if currentContext != ContextDockerHost {
if currentContext != "" {
contextstore := cli.contextStore
if contextstore == nil {
contextstore = store.New(cliconfig.ContextStoreDir(), storeConfig)
}
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
}
Expand All @@ -389,6 +388,11 @@ func (cli *DockerCli) StackOrchestrator(flagValue string) (Orchestrator, error)
return GetStackOrchestrator(flagValue, ctxOrchestrator, configFile.StackOrchestrator, cli.Err())
}

// DockerEndpoint returns the current docker endpoint
func (cli *DockerCli) DockerEndpoint() docker.Endpoint {
return cli.dockerEndpoint
}

// ServerInfo stores details about the supported features and platform of the
// server
type ServerInfo struct {
Expand Down Expand Up @@ -435,24 +439,28 @@ func UserAgent() string {
// - if DOCKER_CONTEXT is set, use this value
// - if Config file has a globally set "CurrentContext", use this value
// - fallbacks to default HOST, uses TLS config from flags/env vars
func resolveContextName(opts *cliflags.CommonOptions, config *configfile.ConfigFile) (string, error) {
func resolveContextName(opts *cliflags.CommonOptions, config *configfile.ConfigFile, contextstore store.Store) (string, error) {
if opts.Context != "" && len(opts.Hosts) > 0 {
return "", errors.New("Conflicting options: either specify --host or --context, not bot")
return "", errors.New("Conflicting options: either specify --host or --context, not both")
}
if opts.Context != "" {
return opts.Context, nil
}
if len(opts.Hosts) > 0 {
return ContextDockerHost, nil
return "", nil
}
if _, present := os.LookupEnv("DOCKER_HOST"); present {
return ContextDockerHost, nil
return "", nil
}
if ctxName, ok := os.LookupEnv("DOCKER_CONTEXT"); ok {
return ctxName, nil
}
if config != nil && config.CurrentContext != "" {
return config.CurrentContext, nil
_, err := contextstore.GetContextMetadata(config.CurrentContext)
if store.IsErrContextDoesNotExist(err) {
return "", errors.Errorf("Current context %q is not found on the file system, please check your config file at %s", config.CurrentContext, config.Filename)
}
return config.CurrentContext, err
}
return ContextDockerHost, nil
return "", nil
}
4 changes: 4 additions & 0 deletions cli/command/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/docker/cli/cli/command/checkpoint"
"github.com/docker/cli/cli/command/config"
"github.com/docker/cli/cli/command/container"
"github.com/docker/cli/cli/command/context"
"github.com/docker/cli/cli/command/engine"
"github.com/docker/cli/cli/command/image"
"github.com/docker/cli/cli/command/manifest"
Expand Down Expand Up @@ -86,6 +87,9 @@ func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
// volume
volume.NewVolumeCommand(dockerCli),

// context
context.NewContextCommand(dockerCli),

// legacy commands may be hidden
hide(system.NewEventsCommand(dockerCli)),
hide(system.NewInfoCommand(dockerCli)),
Expand Down
4 changes: 2 additions & 2 deletions cli/command/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import (

// DockerContext is a typed representation of what we put in Context metadata
type DockerContext struct {
Description string `json:"description,omitempty"`
StackOrchestrator Orchestrator `json:"stack_orchestrator,omitempty"`
Description string `json:",omitempty"`
StackOrchestrator Orchestrator `json:",omitempty"`
}

// GetDockerContext extracts metadata from stored context metadata
Expand Down
49 changes: 49 additions & 0 deletions cli/command/context/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package context

import (
"errors"
"fmt"
"regexp"

"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
)

// NewContextCommand returns the context cli subcommand
func NewContextCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "context",
Short: "Manage contexts",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCli.Err()),
}
cmd.AddCommand(
newCreateCommand(dockerCli),
newListCommand(dockerCli),
newUseCommand(dockerCli),
newExportCommand(dockerCli),
newImportCommand(dockerCli),
newRemoveCommand(dockerCli),
newUpdateCommand(dockerCli),
newInspectCommand(dockerCli),
)
return cmd
}

const restrictedNamePattern = "^[a-zA-Z0-9][a-zA-Z0-9_.+-]+$"

var restrictedNameRegEx = regexp.MustCompile(restrictedNamePattern)

func validateContextName(name string) error {
if name == "" {
return errors.New("context name cannot be empty")
}
if name == "default" {
return errors.New(`"default" is a reserved context name`)
}
if !restrictedNameRegEx.MatchString(name) {
return fmt.Errorf("context name %q is invalid, names are validated against regexp %q", name, restrictedNamePattern)
}
return nil
}
139 changes: 139 additions & 0 deletions cli/command/context/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package context

import (
"bytes"
"fmt"
"text/tabwriter"

"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/context/docker"
"github.com/docker/cli/cli/context/kubernetes"
"github.com/docker/cli/cli/context/store"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)

type createOptions struct {
name string
description string
defaultStackOrchestrator string
docker map[string]string
kubernetes map[string]string
}

func longCreateDescription() string {
buf := bytes.NewBuffer(nil)
buf.WriteString("Create a context\n\nDocker endpoint config:\n\n")
tw := tabwriter.NewWriter(buf, 20, 1, 3, ' ', 0)
fmt.Fprintln(tw, "NAME\tDESCRIPTION")
for _, d := range dockerConfigKeysDescriptions {
fmt.Fprintf(tw, "%s\t%s\n", d.name, d.description)
}
tw.Flush()
buf.WriteString("\nKubernetes endpoint config:\n\n")
tw = tabwriter.NewWriter(buf, 20, 1, 3, ' ', 0)
fmt.Fprintln(tw, "NAME\tDESCRIPTION")
for _, d := range kubernetesConfigKeysDescriptions {
fmt.Fprintf(tw, "%s\t%s\n", d.name, d.description)
}
tw.Flush()
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")
return buf.String()
}

func newCreateCommand(dockerCli command.Cli) *cobra.Command {
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)
},
Long: longCreateDescription(),
}
flags := cmd.Flags()
flags.StringVar(&opts.description, "description", "", "Description of the context")
flags.StringVar(
&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")
return cmd
}

func runCreate(cli command.Cli, o *createOptions) error {
s := cli.ContextStore()
if err := checkContextNameForCreation(s, o.name); err != nil {
return err
}
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,
StackOrchestrator: stackOrchestrator,
},
Name: o.name,
}
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)
if err != nil {
return errors.Wrap(err, "unable to create docker endpoint config")
}
contextMetadata.Endpoints[docker.DockerEndpoint] = dockerEP
if dockerTLS != nil {
contextTLSData.Endpoints[docker.DockerEndpoint] = *dockerTLS
}
if o.kubernetes != nil {
kubernetesEP, kubernetesTLS, err := getKubernetesEndpointMetadataAndTLS(cli, o.kubernetes)
if err != nil {
return errors.Wrap(err, "unable to create kubernetes endpoint config")
}
if kubernetesEP == nil && stackOrchestrator.HasKubernetes() {
return errors.Errorf("cannot specify orchestrator %q without configuring a Kubernetes endpoint", stackOrchestrator)
}
if kubernetesEP != nil {
contextMetadata.Endpoints[kubernetes.KubernetesEndpoint] = kubernetesEP
}
if kubernetesTLS != nil {
contextTLSData.Endpoints[kubernetes.KubernetesEndpoint] = *kubernetesTLS
}
}
if err := validateEndpointsAndOrchestrator(contextMetadata); err != nil {
return err
}
if err := s.CreateOrUpdateContext(contextMetadata); err != nil {
return err
}
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)
return nil
}

func checkContextNameForCreation(s store.Store, name string) error {
if err := validateContextName(name); err != nil {
return err
}
if _, err := s.GetContextMetadata(name); !store.IsErrContextDoesNotExist(err) {
if err != nil {
return errors.Wrap(err, "error while getting existing contexts")
}
return errors.Errorf("context %q already exists", name)
}
return nil
}
Loading

0 comments on commit 591385a

Please sign in to comment.