From a08fd722d9dc36fefa21c871953ec657d8ee213a Mon Sep 17 00:00:00 2001 From: Simon Ferquel Date: Fri, 9 Nov 2018 15:10:41 +0100 Subject: [PATCH] Fast Context Switch: commands Signed-off-by: Simon Ferquel --- cli/command/commands/commands.go | 4 + cli/command/context/cmd.go | 28 +++ cli/command/context/create.go | 222 +++++++++++++++++++++++ cli/command/context/export.go | 81 +++++++++ cli/command/context/formatter/context.go | 81 +++++++++ cli/command/context/import.go | 56 ++++++ cli/command/context/list.go | 86 +++++++++ cli/command/context/remove.go | 32 ++++ cli/command/context/use.go | 23 +++ cli/command/system/info.go | 1 + cli/command/system/version.go | 3 + 11 files changed, 617 insertions(+) create mode 100644 cli/command/context/cmd.go create mode 100644 cli/command/context/create.go create mode 100644 cli/command/context/export.go create mode 100644 cli/command/context/formatter/context.go create mode 100644 cli/command/context/import.go create mode 100644 cli/command/context/list.go create mode 100644 cli/command/context/remove.go create mode 100644 cli/command/context/use.go diff --git a/cli/command/commands/commands.go b/cli/command/commands/commands.go index ca2f6ad0966f..59999bda5d52 100644 --- a/cli/command/commands/commands.go +++ b/cli/command/commands/commands.go @@ -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" @@ -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)), diff --git a/cli/command/context/cmd.go b/cli/command/context/cmd.go new file mode 100644 index 000000000000..5b2b3629fade --- /dev/null +++ b/cli/command/context/cmd.go @@ -0,0 +1,28 @@ +package context + +import ( + "github.com/spf13/cobra" + + "github.com/docker/cli/cli" + "github.com/docker/cli/cli/command" +) + +// NewContextCommand returns the context cli subcommand +func NewContextCommand(dockerCli command.Cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "context", + Aliases: []string{"ctx"}, + 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), + ) + return cmd +} diff --git a/cli/command/context/create.go b/cli/command/context/create.go new file mode 100644 index 000000000000..4c81ddd3eaa3 --- /dev/null +++ b/cli/command/context/create.go @@ -0,0 +1,222 @@ +package context + +import ( + "io/ioutil" + "os" + + "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" + "github.com/spf13/pflag" +) + +type createOptions struct { + name string + description string // --description string: set the description + defaultStackOrchestrator string + docker dockerEndpointOptions + kubernetes kubernetedEndpointOptions +} + +func (o *createOptions) addFlags(flags *pflag.FlagSet) { + flags.StringVar(&o.description, "description", "", "set the description of the context") + flags.StringVar(&o.defaultStackOrchestrator, "default-stack-orchestrator", "", "set the default orchestrator for stack operations if different to the default one, to use with this context (swarm|kubernetes|all)") + o.docker.addFlags(flags) + o.kubernetes.addFlags(flags) +} + +type dockerEndpointOptions struct { + host string //--moby-host string: set moby endpoint host (same format as DOCKER_HOST) + apiVersion string + ca string //--moby-ca string: path to a CA file + cert string //--moby-cert string: path to a client cert file + key string //--moby-key string: path to a client key file + skipTLSVerify bool +} + +func (o *dockerEndpointOptions) addFlags(flags *pflag.FlagSet) { + flags.StringVar(&o.host, "docker-host", "", "required: specify the docker endpoint on wich to connect") + flags.StringVar(&o.apiVersion, "docker-api-version", "", "override negociated api version") + flags.StringVar(&o.ca, "docker-tls-ca", "", "path to the ca file to validate docker endpoint") + flags.StringVar(&o.cert, "docker-tls-cert", "", "path to the cert file to authenticate to the docker endpoint") + flags.StringVar(&o.key, "docker-tls-key", "", "path to the key file to authenticate to the docker endpoint") + flags.BoolVar(&o.skipTLSVerify, "docker-tls-skip-verify", false, "skip tls verify when connecting to the docker endpoint") +} + +func (o *dockerEndpointOptions) toEndpoint(contextName string) (docker.Endpoint, error) { + var ( + ca, key, cert []byte + err error + tlsData *docker.TLSData + ) + if o.ca != "" { + if ca, err = ioutil.ReadFile(o.ca); err != nil { + return docker.Endpoint{}, err + } + } + if o.cert != "" { + if cert, err = ioutil.ReadFile(o.cert); err != nil { + return docker.Endpoint{}, err + } + } + if o.key != "" { + if key, err = ioutil.ReadFile(o.key); err != nil { + return docker.Endpoint{}, err + } + } + if ca != nil || key != nil || cert != nil { + tlsData = &docker.TLSData{ + CA: ca, + Cert: cert, + Key: key, + } + } + return docker.Endpoint{ + EndpointMeta: docker.EndpointMeta{ + APIVersion: o.apiVersion, + ContextName: contextName, + Host: o.host, + SkipTLSVerify: o.skipTLSVerify, + }, + TLSData: tlsData, + }, nil +} + +type kubernetedEndpointOptions struct { + server string //--kubernetes-server string: kubernetes api server address + ca string //--kubernetes-ca string: path to a CA file + cert string //--kubernetes-cert string: path to a client cert file + key string //--kubernetes-key string: path to a client key file + skipTLSVerify bool + defaultNamespace string + kubeconfigFile string //--kubernetes-kubeconfig-file string: path to a kubernetes cli config file + kubeconfigContext string //--kubernetes-kubeconfig-context string: name of the kubernetes cli config file context to use +} + +func (o *kubernetedEndpointOptions) addFlags(flags *pflag.FlagSet) { + flags.StringVar(&o.server, "kubernetes-host", "", "required: specify the docker endpoint on wich to connect") + flags.StringVar(&o.ca, "kubernetes-tls-ca", "", "path to the ca file to validate kubernetes endpoint") + flags.StringVar(&o.cert, "kubernetes-tls-cert", "", "path to the cert file to authenticate to the kubernetes endpoint") + flags.StringVar(&o.key, "kubernetes-tls-key", "", "path to the key file to authenticate to the kubernetes endpoint") + flags.BoolVar(&o.skipTLSVerify, "kubernetes-tls-skip-verify", false, "skip tls verify when connecting to the kubernetes endpoint") + flags.StringVar(&o.defaultNamespace, "kubernetes-default-namespace", "default", "override default namespace when connecting to kubernetes endpoint") + flags.StringVar(&o.kubeconfigFile, "kubernetes-kubeconfig", "", "path to an existing kubeconfig file") + flags.StringVar(&o.kubeconfigContext, "kubernetes-kubeconfig-context", "", `context to use in the kubeconfig file referenced in "kubernetes-kubeconfig"`) +} + +func (o *kubernetedEndpointOptions) toEndpoint(contextName string) (*kubernetes.Endpoint, error) { + if o.kubeconfigFile != "" { + ep, err := kubernetes.FromKubeConfig(contextName, o.kubeconfigFile, o.kubeconfigContext, o.defaultNamespace) + if err != nil { + return nil, err + } + return &ep, nil + } + if o.server != "" { + var ( + ca, key, cert []byte + err error + tlsData *kubernetes.TLSData + ) + if o.ca != "" { + if ca, err = ioutil.ReadFile(o.ca); err != nil { + return nil, err + } + } + if o.cert != "" { + if cert, err = ioutil.ReadFile(o.cert); err != nil { + return nil, err + } + } + if o.key != "" { + if key, err = ioutil.ReadFile(o.key); err != nil { + return nil, err + } + } + if ca != nil || key != nil || cert != nil { + tlsData = &kubernetes.TLSData{ + CA: ca, + Cert: cert, + Key: key, + } + } + return &kubernetes.Endpoint{ + EndpointMeta: kubernetes.EndpointMeta{ + ContextName: contextName, + DefaultNamespace: o.defaultNamespace, + Server: o.server, + SkipTLSVerify: o.skipTLSVerify, + }, + TLSData: tlsData, + }, nil + } + return nil, nil +} + +func loadFileIfNotEmpty(path string) ([]byte, error) { + if path == "" { + return nil, nil + } + return ioutil.ReadFile(path) +} + +func (o *createOptions) process(s store.Store) error { + if _, err := s.GetContextMetadata(o.name); !os.IsNotExist(err) { + if err != nil { + return errors.Wrap(err, "error while getting existing contexts") + } + return errors.Errorf("context %q already exists", o.name) + } + stackOrchestrator, err := command.NormalizeOrchestrator(o.defaultStackOrchestrator) + if err != nil { + return errors.Wrap(err, "unable to parse default-stack-orchestrator") + } + dockerEP, err := o.docker.toEndpoint(o.name) + if err != nil { + return errors.Wrap(err, "unable to create docker endpoint config") + } + if err := docker.Save(s, dockerEP); err != nil { + return errors.Wrap(err, "unable to save docker endpoint config") + } + kubernetesEP, err := o.kubernetes.toEndpoint(o.name) + if err != nil { + return errors.Wrap(err, "unable to create kubernetes endpoint config") + } + if kubernetesEP != nil { + if err := kubernetes.Save(s, *kubernetesEP); err != nil { + return errors.Wrap(err, "unable to save kubernetes endpoint config") + } + } + + // at this point, the context should exist with endpoints configuration + ctx, err := s.GetContextMetadata(o.name) + if err != nil { + return errors.Wrap(err, "error while getting context") + } + command.SetContextMetadata(&ctx, command.ContextMetadata{ + Description: o.description, + StackOrchestrator: stackOrchestrator, + }) + + return s.CreateOrUpdateContext(o.name, ctx) +} + +func newCreateCommand(dockerCli command.Cli) *cobra.Command { + opts := &createOptions{} + cmd := &cobra.Command{ + Use: "create [options]", + Short: "create a context", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.name = args[0] + return opts.process(dockerCli.ContextStore()) + }, + } + + opts.addFlags(cmd.Flags()) + return cmd +} diff --git a/cli/command/context/export.go b/cli/command/context/export.go new file mode 100644 index 000000000000..007888c77753 --- /dev/null +++ b/cli/command/context/export.go @@ -0,0 +1,81 @@ +package context + +import ( + "fmt" + "io" + "os" + + "github.com/docker/cli/cli" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/context/kubernetes" + "github.com/docker/cli/cli/context/store" + "github.com/spf13/cobra" + "k8s.io/client-go/tools/clientcmd" +) + +type exportOptions struct { + kubeconfig bool + contextName string + dest string +} + +func newExportCommand(dockerCli command.Cli) *cobra.Command { + opts := &exportOptions{} + cmd := &cobra.Command{ + Use: "export [output file]", + Short: "Export a context", + Args: cli.RequiresRangeArgs(1, 2), + RunE: func(cmd *cobra.Command, args []string) error { + opts.contextName = args[0] + if len(args) == 2 { + opts.dest = args[1] + } else { + opts.dest = opts.contextName + if opts.kubeconfig { + opts.dest += ".kubeconfig" + } else { + opts.dest += ".dockercontext" + } + } + return runExport(dockerCli, opts) + }, + } + + flags := cmd.Flags() + flags.BoolVarP(&opts.kubeconfig, "kubeconfig", "k", false, "export as a kubeconfig file") + return cmd +} +func runExport(dockerCli command.Cli, opts *exportOptions) error { + ctxMeta, err := dockerCli.ContextStore().GetContextMetadata(opts.contextName) + if err != nil { + return err + } + if !opts.kubeconfig { + reader := store.Export(opts.contextName, dockerCli.ContextStore()) + defer reader.Close() + f, err := os.OpenFile(opts.dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer f.Close() + _, err = io.Copy(f, reader) + return err + } + kubernetesEndpointMeta := kubernetes.Parse(opts.contextName, ctxMeta) + if kubernetesEndpointMeta == nil { + return fmt.Errorf("context %q has no kubernetes endpoint", opts.contextName) + } + kubernetesEndpoint, err := kubernetesEndpointMeta.WithTLSData(dockerCli.ContextStore()) + if err != nil { + return err + } + kubeConfig, err := kubernetesEndpoint.KubernetesConfig() + if err != nil { + return err + } + rawCfg, err := kubeConfig.RawConfig() + if err != nil { + return err + } + return clientcmd.WriteToFile(rawCfg, opts.dest) +} diff --git a/cli/command/context/formatter/context.go b/cli/command/context/formatter/context.go new file mode 100644 index 000000000000..a403ff520f96 --- /dev/null +++ b/cli/command/context/formatter/context.go @@ -0,0 +1,81 @@ +package formatter + +import ( + "github.com/docker/cli/cli/command/formatter" +) + +const ( + // ClientContextTableFormat is the default client context format + ClientContextTableFormat = "table {{.NameWithCurrent}}\t{{.Description}}\t{{.DockerEndpoint}}\t{{.KubernetesEndpoint}}\t{{.StackOrchestrator}}" + + dockerEndpointHeader = "DOCKER ENDPOINT" + kubernetesEndpointHeader = "KUBERNETES ENDPOINT" + stackOrchestrastorHeader = "ORCHESTRATOR" +) + +// ClientContext is a context for display +type ClientContext struct { + Name string + Description string + DockerEndpoint string + KubernetesEndpoint string + StackOrchestrator string + Current bool +} + +// ClientContextWrite writes formatted contexts using the Context +func ClientContextWrite(ctx formatter.Context, contexts []*ClientContext) error { + render := func(format func(subContext formatter.SubContext) error) error { + for _, context := range contexts { + if err := format(&clientContextContext{c: context}); err != nil { + return err + } + } + return nil + } + return ctx.Write(newClientContextContext(), render) +} + +type clientContextContext struct { + formatter.HeaderContext + c *ClientContext +} + +func newClientContextContext() *clientContextContext { + ctx := clientContextContext{} + ctx.Header = formatter.SubHeaderContext{ + "NameWithCurrent": formatter.NameHeader, + "Description": formatter.DescriptionHeader, + "DockerEndpoint": dockerEndpointHeader, + "KubernetesEndpoint": kubernetesEndpointHeader, + "StackOrchestrator": stackOrchestrastorHeader, + } + return &ctx +} + +func (c *clientContextContext) MarshalJSON() ([]byte, error) { + return formatter.MarshalJSON(c) +} + +func (c *clientContextContext) NameWithCurrent() string { + if !c.c.Current { + return c.c.Name + } + return c.c.Name + " *" +} + +func (c *clientContextContext) Description() string { + return c.c.Description +} + +func (c *clientContextContext) DockerEndpoint() string { + return c.c.DockerEndpoint +} + +func (c *clientContextContext) KubernetesEndpoint() string { + return c.c.KubernetesEndpoint +} + +func (c *clientContextContext) StackOrchestrator() string { + return c.c.StackOrchestrator +} diff --git a/cli/command/context/import.go b/cli/command/context/import.go new file mode 100644 index 000000000000..1ab2739eadf6 --- /dev/null +++ b/cli/command/context/import.go @@ -0,0 +1,56 @@ +package context + +import ( + "errors" + "fmt" + "os" + + "github.com/docker/cli/cli" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/context/store" + "github.com/spf13/cobra" +) + +type importOptions struct { + use bool + force bool + name string +} + +func newImportCommand(dockerCli command.Cli) *cobra.Command { + opts := &importOptions{} + cmd := &cobra.Command{ + Use: "import [OPTIONS]", + Short: "Import a context", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if opts.name == "" { + return errors.New("name is required") + } + file := args[0] + _, err := dockerCli.ContextStore().GetContextMetadata(opts.name) + exists := err == nil + if exists && !opts.force { + return fmt.Errorf("context %q already exists", opts.name) + } + f, err := os.Open(file) + if err != nil { + return err + } + defer f.Close() + if err := store.Import(opts.name, dockerCli.ContextStore(), f); err != nil { + return err + } + if opts.use { + return dockerCli.ContextStore().SetCurrentContext(opts.name) + } + return nil + }, + } + + flags := cmd.Flags() + flags.BoolVar(&opts.use, "use", false, "make this context the default one") + flags.BoolVar(&opts.force, "force", false, "overwrite any existing context with the same name") + flags.StringVar(&opts.name, "name", "", "name of the context") + return cmd +} diff --git a/cli/command/context/list.go b/cli/command/context/list.go new file mode 100644 index 000000000000..0b0c0a67f100 --- /dev/null +++ b/cli/command/context/list.go @@ -0,0 +1,86 @@ +package context + +import ( + "fmt" + "sort" + + "vbom.ml/util/sortorder" + + "github.com/docker/cli/cli" + "github.com/docker/cli/cli/command" + ctxformatter "github.com/docker/cli/cli/command/context/formatter" + "github.com/docker/cli/cli/command/formatter" + "github.com/docker/cli/cli/context/docker" + "github.com/docker/cli/cli/context/kubernetes" + "github.com/spf13/cobra" +) + +type listOptions struct { + format string +} + +func newListCommand(dockerCli command.Cli) *cobra.Command { + opts := &listOptions{} + cmd := &cobra.Command{ + Use: "ls [OPTIONS]", + Aliases: []string{"list"}, + Short: "List contexts", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runList(dockerCli, opts) + }, + } + + flags := cmd.Flags() + flags.StringVar(&opts.format, "format", "", "Pretty-print contexts using a Go template") + return cmd +} + +func runList(dockerCli command.Cli, opts *listOptions) error { + curContext := dockerCli.CurrentContext() + contextMap, err := dockerCli.ContextStore().ListContexts() + if err != nil { + return err + } + var contexts []*ctxformatter.ClientContext + for name, rawMeta := range contextMap { + meta, err := command.GetContextMetadata(rawMeta) + if err != nil { + return err + } + dockerEndpoint, err := docker.Parse(name, rawMeta) + if err != nil { + return err + } + kubernetesEndpoint := kubernetes.Parse(name, rawMeta) + kubEndpointText := "" + if kubernetesEndpoint != nil { + kubEndpointText = fmt.Sprintf("%s (%s)", kubernetesEndpoint.Server, kubernetesEndpoint.DefaultNamespace) + } + desc := ctxformatter.ClientContext{ + Name: name, + Current: name == curContext, + Description: meta.Description, + StackOrchestrator: string(meta.StackOrchestrator), + DockerEndpoint: dockerEndpoint.Host, + KubernetesEndpoint: kubEndpointText, + } + contexts = append(contexts, &desc) + } + sort.Slice(contexts, func(i, j int) bool { + return sortorder.NaturalLess(contexts[i].Name, contexts[j].Name) + }) + return format(dockerCli, opts, contexts) +} + +func format(dockerCli command.Cli, opts *listOptions, contexts []*ctxformatter.ClientContext) error { + format := opts.format + if format == "" { + format = ctxformatter.ClientContextTableFormat + } + contextCtx := formatter.Context{ + Output: dockerCli.Out(), + Format: formatter.Format(format), + } + return ctxformatter.ClientContextWrite(contextCtx, contexts) +} diff --git a/cli/command/context/remove.go b/cli/command/context/remove.go new file mode 100644 index 000000000000..09093c87b718 --- /dev/null +++ b/cli/command/context/remove.go @@ -0,0 +1,32 @@ +package context + +import ( + "fmt" + "strings" + + "github.com/docker/cli/cli" + "github.com/docker/cli/cli/command" + "github.com/spf13/cobra" +) + +func newRemoveCommand(dockerCli command.Cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "rm [...]", + Aliases: []string{"remove"}, + Short: "Remove contexts", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + currentCtx := strings.ToLower(dockerCli.CurrentContext()) + for _, name := range args { + if strings.ToLower(name) == currentCtx { + return fmt.Errorf("%q is the current context", name) + } + if err := dockerCli.ContextStore().RemoveContext(name); err != nil { + return err + } + } + return nil + }, + } + return cmd +} diff --git a/cli/command/context/use.go b/cli/command/context/use.go new file mode 100644 index 000000000000..6283b0e08abf --- /dev/null +++ b/cli/command/context/use.go @@ -0,0 +1,23 @@ +package context + +import ( + "github.com/docker/cli/cli/command" + "github.com/spf13/cobra" +) + +func newUseCommand(dockerCli command.Cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "use ", + Aliases: []string{"select", "switch"}, + Short: "Set the current docker context", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + name := args[0] + if _, err := dockerCli.ContextStore().GetContextMetadata(name); err != nil { + return err + } + return dockerCli.ContextStore().SetCurrentContext(name) + }, + } + return cmd +} diff --git a/cli/command/system/info.go b/cli/command/system/info.go index 92fc2cd3e77a..731e65eedde4 100644 --- a/cli/command/system/info.go +++ b/cli/command/system/info.go @@ -55,6 +55,7 @@ func runInfo(dockerCli command.Cli, opts *infoOptions) error { // nolint: gocyclo func prettyPrintInfo(dockerCli command.Cli, info types.Info) error { + fmt.Fprintln(dockerCli.Out(), "Context:", dockerCli.CurrentContext()) fmt.Fprintln(dockerCli.Out(), "Containers:", info.Containers) fmt.Fprintln(dockerCli.Out(), " Running:", info.ContainersRunning) fmt.Fprintln(dockerCli.Out(), " Paused:", info.ContainersPaused) diff --git a/cli/command/system/version.go b/cli/command/system/version.go index f741f7e5adc9..7e3a01e6dfde 100644 --- a/cli/command/system/version.go +++ b/cli/command/system/version.go @@ -22,6 +22,7 @@ import ( var versionTemplate = `{{with .Client -}} Client:{{if ne .Platform.Name ""}} {{.Platform.Name}}{{end}} + Context: {{.Context}} Version: {{.Version}} API version: {{.APIVersion}}{{if ne .APIVersion .DefaultAPIVersion}} (downgraded from {{.DefaultAPIVersion}}){{end}} Go version: {{.GoVersion}} @@ -77,6 +78,7 @@ type clientVersion struct { Arch string BuildTime string `json:",omitempty"` Experimental bool + Context string } type kubernetesVersion struct { @@ -143,6 +145,7 @@ func runVersion(dockerCli command.Cli, opts *versionOptions) error { Os: runtime.GOOS, Arch: runtime.GOARCH, Experimental: dockerCli.ClientInfo().HasExperimental, + Context: dockerCli.CurrentContext(), }, }