Fast Context Switch: commands

Signed-off-by: Simon Ferquel <simon.ferquel@docker.com>
This commit is contained in:
Simon Ferquel 2018-11-09 15:10:41 +01:00
parent b34f340346
commit 591385a1d0
48 changed files with 2295 additions and 168 deletions

View File

@ -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
@ -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.
@ -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(
@ -182,7 +181,7 @@ 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
}
@ -190,6 +189,7 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
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) {
@ -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
}
@ -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
@ -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 {
@ -280,11 +280,9 @@ func resolveDockerEndpoint(s store.Store, contextName string, opts *cliflags.Com
return docker.Endpoint{
EndpointMeta: docker.EndpointMeta{
EndpointMetaBase: dcontext.EndpointMetaBase{
Host: host,
SkipTLSVerify: skipTLSVerify,
},
},
TLSData: tlsData,
}, nil
}
@ -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
}
@ -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 {
@ -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 ContextDockerHost, nil
return config.CurrentContext, err
}
return "", nil
}

View File

@ -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)),

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -0,0 +1,175 @@
package context
import (
"io/ioutil"
"os"
"testing"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/context/docker"
"github.com/docker/cli/cli/context/kubernetes"
"github.com/docker/cli/cli/context/store"
"github.com/docker/cli/internal/test"
"gotest.tools/assert"
"gotest.tools/env"
)
func makeFakeCli(t *testing.T, opts ...func(*test.FakeCli)) (*test.FakeCli, func()) {
dir, err := ioutil.TempDir("", t.Name())
assert.NilError(t, err)
storeConfig := store.NewConfig(
func() interface{} { return &command.DockerContext{} },
store.EndpointTypeGetter(docker.DockerEndpoint, func() interface{} { return &docker.EndpointMeta{} }),
store.EndpointTypeGetter(kubernetes.KubernetesEndpoint, func() interface{} { return &kubernetes.EndpointMeta{} }),
)
store := store.New(dir, storeConfig)
cleanup := func() {
os.RemoveAll(dir)
}
result := test.NewFakeCli(nil, opts...)
for _, o := range opts {
o(result)
}
result.SetContextStore(store)
return result, cleanup
}
func withCliConfig(configFile *configfile.ConfigFile) func(*test.FakeCli) {
return func(m *test.FakeCli) {
m.SetConfigFile(configFile)
}
}
func TestCreateInvalids(t *testing.T) {
cli, cleanup := makeFakeCli(t)
defer cleanup()
assert.NilError(t, cli.ContextStore().CreateOrUpdateContext(store.ContextMetadata{Name: "existing-context"}))
tests := []struct {
options createOptions
expecterErr string
}{
{
expecterErr: `context name cannot be empty`,
},
{
options: createOptions{
name: " ",
},
expecterErr: `context name " " is invalid`,
},
{
options: createOptions{
name: "existing-context",
},
expecterErr: `context "existing-context" already exists`,
},
{
options: createOptions{
name: "invalid-docker-host",
docker: map[string]string{
keyHost: "some///invalid/host",
},
},
expecterErr: `unable to parse docker host`,
},
{
options: createOptions{
name: "invalid-orchestrator",
defaultStackOrchestrator: "invalid",
},
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",
defaultStackOrchestrator: "kubernetes",
docker: map[string]string{},
},
expecterErr: `cannot specify orchestrator "kubernetes" without configuring a Kubernetes endpoint`,
},
{
options: createOptions{
name: "orchestrator-all-no-endpoint",
defaultStackOrchestrator: "all",
docker: map[string]string{},
},
expecterErr: `cannot specify orchestrator "all" without configuring a Kubernetes endpoint`,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.options.name, func(t *testing.T) {
err := runCreate(cli, &tc.options)
assert.ErrorContains(t, err, tc.expecterErr)
})
}
}
func TestCreateOrchestratorSwarm(t *testing.T) {
cli, cleanup := makeFakeCli(t)
defer cleanup()
err := runCreate(cli, &createOptions{
name: "test",
defaultStackOrchestrator: "swarm",
docker: map[string]string{},
})
assert.NilError(t, err)
assert.Equal(t, "test\n", cli.OutBuffer().String())
assert.Equal(t, "Successfully created context \"test\"\n", cli.ErrBuffer().String())
}
func TestCreateOrchestratorEmpty(t *testing.T) {
cli, cleanup := makeFakeCli(t)
defer cleanup()
err := runCreate(cli, &createOptions{
name: "test",
docker: map[string]string{},
})
assert.NilError(t, err)
}
func validateTestKubeEndpoint(t *testing.T, s store.Store, name string) {
t.Helper()
ctxMetadata, err := s.GetContextMetadata(name)
assert.NilError(t, err)
kubeMeta := ctxMetadata.Endpoints[kubernetes.KubernetesEndpoint].(kubernetes.EndpointMeta)
kubeEP, err := kubeMeta.WithTLSData(s, name)
assert.NilError(t, err)
assert.Equal(t, "https://someserver", kubeEP.Host)
assert.Equal(t, "the-ca", string(kubeEP.TLSData.CA))
assert.Equal(t, "the-cert", string(kubeEP.TLSData.Cert))
assert.Equal(t, "the-key", string(kubeEP.TLSData.Key))
}
func createTestContextWithKube(t *testing.T, cli command.Cli) {
t.Helper()
revert := env.Patch(t, "KUBECONFIG", "./testdata/test-kubeconfig")
defer revert()
err := runCreate(cli, &createOptions{
name: "test",
defaultStackOrchestrator: "all",
kubernetes: map[string]string{
keyFromCurrent: "true",
},
docker: map[string]string{},
})
assert.NilError(t, err)
}
func TestCreateOrchestratorAllKubernetesEndpointFromCurrent(t *testing.T) {
cli, cleanup := makeFakeCli(t)
defer cleanup()
createTestContextWithKube(t, cli)
validateTestKubeEndpoint(t, cli.ContextStore(), "test")
}

View File

@ -0,0 +1,110 @@
package context
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/docker/cli/cli/command"
"gotest.tools/assert"
)
func TestExportImportWithFile(t *testing.T) {
contextDir, err := ioutil.TempDir("", t.Name()+"context")
assert.NilError(t, err)
defer os.RemoveAll(contextDir)
contextFile := filepath.Join(contextDir, "exported")
cli, cleanup := makeFakeCli(t)
defer cleanup()
createTestContextWithKube(t, cli)
cli.ErrBuffer().Reset()
assert.NilError(t, runExport(cli, &exportOptions{
contextName: "test",
dest: contextFile,
}))
assert.Equal(t, cli.ErrBuffer().String(), fmt.Sprintf("Written file %q\n", contextFile))
cli.OutBuffer().Reset()
cli.ErrBuffer().Reset()
assert.NilError(t, runImport(cli, "test2", contextFile))
context1, err := cli.ContextStore().GetContextMetadata("test")
assert.NilError(t, err)
context2, err := cli.ContextStore().GetContextMetadata("test2")
assert.NilError(t, err)
assert.DeepEqual(t, context1.Endpoints, context2.Endpoints)
assert.DeepEqual(t, context1.Metadata, context2.Metadata)
assert.Equal(t, "test", context1.Name)
assert.Equal(t, "test2", context2.Name)
assert.Equal(t, "test2\n", cli.OutBuffer().String())
assert.Equal(t, "Successfully imported context \"test2\"\n", cli.ErrBuffer().String())
}
func TestExportImportPipe(t *testing.T) {
cli, cleanup := makeFakeCli(t)
defer cleanup()
createTestContextWithKube(t, cli)
cli.ErrBuffer().Reset()
cli.OutBuffer().Reset()
assert.NilError(t, runExport(cli, &exportOptions{
contextName: "test",
dest: "-",
}))
assert.Equal(t, cli.ErrBuffer().String(), "")
cli.SetIn(command.NewInStream(ioutil.NopCloser(bytes.NewBuffer(cli.OutBuffer().Bytes()))))
cli.OutBuffer().Reset()
cli.ErrBuffer().Reset()
assert.NilError(t, runImport(cli, "test2", "-"))
context1, err := cli.ContextStore().GetContextMetadata("test")
assert.NilError(t, err)
context2, err := cli.ContextStore().GetContextMetadata("test2")
assert.NilError(t, err)
assert.DeepEqual(t, context1.Endpoints, context2.Endpoints)
assert.DeepEqual(t, context1.Metadata, context2.Metadata)
assert.Equal(t, "test", context1.Name)
assert.Equal(t, "test2", context2.Name)
assert.Equal(t, "test2\n", cli.OutBuffer().String())
assert.Equal(t, "Successfully imported context \"test2\"\n", cli.ErrBuffer().String())
}
func TestExportKubeconfig(t *testing.T) {
contextDir, err := ioutil.TempDir("", t.Name()+"context")
assert.NilError(t, err)
defer os.RemoveAll(contextDir)
contextFile := filepath.Join(contextDir, "exported")
cli, cleanup := makeFakeCli(t)
defer cleanup()
createTestContextWithKube(t, cli)
cli.ErrBuffer().Reset()
assert.NilError(t, runExport(cli, &exportOptions{
contextName: "test",
dest: contextFile,
kubeconfig: true,
}))
assert.Equal(t, cli.ErrBuffer().String(), fmt.Sprintf("Written file %q\n", contextFile))
assert.NilError(t, runCreate(cli, &createOptions{
name: "test2",
kubernetes: map[string]string{
keyKubeconfig: contextFile,
},
docker: map[string]string{},
}))
validateTestKubeEndpoint(t, cli.ContextStore(), "test2")
}
func TestExportExistingFile(t *testing.T) {
contextDir, err := ioutil.TempDir("", t.Name()+"context")
assert.NilError(t, err)
defer os.RemoveAll(contextDir)
contextFile := filepath.Join(contextDir, "exported")
cli, cleanup := makeFakeCli(t)
defer cleanup()
createTestContextWithKube(t, cli)
cli.ErrBuffer().Reset()
assert.NilError(t, ioutil.WriteFile(contextFile, []byte{}, 0644))
err = runExport(cli, &exportOptions{contextName: "test", dest: contextFile})
assert.Assert(t, os.IsExist(err))
}

View File

@ -0,0 +1,108 @@
package context
import (
"bytes"
"errors"
"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 [OPTIONS] CONTEXT [FILE|-]",
Short: "Export a context to a tar or kubeconfig file",
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.BoolVar(&opts.kubeconfig, "kubeconfig", false, "Export as a kubeconfig file")
return cmd
}
func writeTo(dockerCli command.Cli, reader io.Reader, dest string) error {
var writer io.Writer
var printDest bool
if dest == "-" {
if dockerCli.Out().IsTerminal() {
return errors.New("cowardly refusing to export to a terminal, please specify a file path")
}
writer = dockerCli.Out()
} else {
f, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
if err != nil {
return err
}
defer f.Close()
writer = f
printDest = true
}
if _, err := io.Copy(writer, reader); err != nil {
return err
}
if printDest {
fmt.Fprintf(dockerCli.Err(), "Written file %q\n", dest)
}
return nil
}
func runExport(dockerCli command.Cli, opts *exportOptions) error {
if err := validateContextName(opts.contextName); err != nil {
return err
}
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()
return writeTo(dockerCli, reader, opts.dest)
}
kubernetesEndpointMeta := kubernetes.EndpointFromContext(ctxMeta)
if kubernetesEndpointMeta == nil {
return fmt.Errorf("context %q has no kubernetes endpoint", opts.contextName)
}
kubernetesEndpoint, err := kubernetesEndpointMeta.WithTLSData(dockerCli.ContextStore(), opts.contextName)
if err != nil {
return err
}
kubeConfig := kubernetesEndpoint.KubernetesConfig()
rawCfg, err := kubeConfig.RawConfig()
if err != nil {
return err
}
data, err := clientcmd.Write(rawCfg)
if err != nil {
return err
}
return writeTo(dockerCli, bytes.NewBuffer(data), opts.dest)
}

View File

@ -0,0 +1,48 @@
package context
import (
"fmt"
"io"
"os"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/context/store"
"github.com/spf13/cobra"
)
func newImportCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "import CONTEXT FILE|-",
Short: "Import a context from a tar file",
Args: cli.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
return runImport(dockerCli, args[0], args[1])
},
}
return cmd
}
func runImport(dockerCli command.Cli, name string, source string) error {
if err := checkContextNameForCreation(dockerCli.ContextStore(), name); err != nil {
return err
}
var reader io.Reader
if source == "-" {
reader = dockerCli.In()
} else {
f, err := os.Open(source)
if err != nil {
return err
}
defer f.Close()
reader = f
}
if err := store.Import(name, dockerCli.ContextStore(), reader); err != nil {
return err
}
fmt.Fprintln(dockerCli.Out(), name)
fmt.Fprintf(dockerCli.Err(), "Successfully imported context %q\n", name)
return nil
}

View File

@ -0,0 +1,67 @@
package context
import (
"errors"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/inspect"
"github.com/docker/cli/cli/context/store"
"github.com/spf13/cobra"
)
type inspectOptions struct {
format string
refs []string
}
// newInspectCommand creates a new cobra.Command for `docker image inspect`
func newInspectCommand(dockerCli command.Cli) *cobra.Command {
var opts inspectOptions
cmd := &cobra.Command{
Use: "inspect [OPTIONS] [CONTEXT] [CONTEXT...]",
Short: "Display detailed information on one or more contexts",
RunE: func(cmd *cobra.Command, args []string) error {
opts.refs = args
if len(opts.refs) == 0 {
if dockerCli.CurrentContext() == "" {
return errors.New("no context specified")
}
opts.refs = []string{dockerCli.CurrentContext()}
}
return runInspect(dockerCli, opts)
},
}
flags := cmd.Flags()
flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template")
return cmd
}
func runInspect(dockerCli command.Cli, opts inspectOptions) error {
getRefFunc := func(ref string) (interface{}, []byte, error) {
if ref == "default" {
return nil, nil, errors.New(`context "default" cannot be inspected`)
}
c, err := dockerCli.ContextStore().GetContextMetadata(ref)
if err != nil {
return nil, nil, err
}
tlsListing, err := dockerCli.ContextStore().ListContextTLSFiles(ref)
if err != nil {
return nil, nil, err
}
return contextWithTLSListing{
ContextMetadata: c,
TLSMaterial: tlsListing,
Storage: dockerCli.ContextStore().GetContextStorageInfo(ref),
}, nil, nil
}
return inspect.Inspect(dockerCli.Out(), opts.refs, opts.format, getRefFunc)
}
type contextWithTLSListing struct {
store.ContextMetadata
TLSMaterial map[string]store.EndpointFiles
Storage store.ContextStorageInfo
}

View File

@ -0,0 +1,24 @@
package context
import (
"strings"
"testing"
"gotest.tools/assert"
"gotest.tools/golden"
)
func TestInspect(t *testing.T) {
cli, cleanup := makeFakeCli(t)
defer cleanup()
createTestContextWithKubeAndSwarm(t, cli, "current", "all")
cli.OutBuffer().Reset()
assert.NilError(t, runInspect(cli, inspectOptions{
refs: []string{"current"},
}))
expected := string(golden.Get(t, "inspect.golden"))
si := cli.ContextStore().GetContextStorageInfo("current")
expected = strings.Replace(expected, "<METADATA_PATH>", strings.Replace(si.MetadataPath, `\`, `\\`, -1), 1)
expected = strings.Replace(expected, "<TLS_PATH>", strings.Replace(si.TLSPath, `\`, `\\`, -1), 1)
assert.Equal(t, cli.OutBuffer().String(), expected)
}

109
cli/command/context/list.go Normal file
View File

@ -0,0 +1,109 @@
package context
import (
"fmt"
"sort"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/context/docker"
kubecontext "github.com/docker/cli/cli/context/kubernetes"
"github.com/docker/cli/kubernetes"
"github.com/spf13/cobra"
"vbom.ml/util/sortorder"
)
type listOptions struct {
format string
quiet bool
}
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")
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only show context names")
return cmd
}
func runList(dockerCli command.Cli, opts *listOptions) error {
if opts.format == "" {
opts.format = formatter.TableFormatKey
}
curContext := dockerCli.CurrentContext()
contextMap, err := dockerCli.ContextStore().ListContexts()
if err != nil {
return err
}
var contexts []*formatter.ClientContext
for _, rawMeta := range contextMap {
meta, err := command.GetDockerContext(rawMeta)
if err != nil {
return err
}
dockerEndpoint, err := docker.EndpointFromContext(rawMeta)
if err != nil {
return err
}
kubernetesEndpoint := kubecontext.EndpointFromContext(rawMeta)
kubEndpointText := ""
if kubernetesEndpoint != nil {
kubEndpointText = fmt.Sprintf("%s (%s)", kubernetesEndpoint.Host, kubernetesEndpoint.DefaultNamespace)
}
desc := formatter.ClientContext{
Name: rawMeta.Name,
Current: rawMeta.Name == curContext,
Description: meta.Description,
StackOrchestrator: string(meta.StackOrchestrator),
DockerEndpoint: dockerEndpoint.Host,
KubernetesEndpoint: kubEndpointText,
}
contexts = append(contexts, &desc)
}
if !opts.quiet {
desc := &formatter.ClientContext{
Name: "default",
Description: "Current DOCKER_HOST based configuration",
}
if dockerCli.CurrentContext() == "" {
orchestrator, _ := dockerCli.StackOrchestrator("")
kubEndpointText := ""
kubeconfig := kubernetes.NewKubernetesConfig("")
if cfg, err := kubeconfig.ClientConfig(); err == nil {
ns, _, _ := kubeconfig.Namespace()
if ns == "" {
ns = "default"
}
kubEndpointText = fmt.Sprintf("%s (%s)", cfg.Host, ns)
}
desc.Current = true
desc.StackOrchestrator = string(orchestrator)
desc.DockerEndpoint = dockerCli.DockerEndpoint().Host
desc.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 []*formatter.ClientContext) error {
contextCtx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewClientContextFormat(opts.format, opts.quiet),
}
return formatter.ClientContextWrite(contextCtx, contexts)
}

View File

@ -0,0 +1,62 @@
package context
import (
"testing"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/context/docker"
"gotest.tools/assert"
"gotest.tools/env"
"gotest.tools/golden"
)
func createTestContextWithKubeAndSwarm(t *testing.T, cli command.Cli, name string, orchestrator string) {
revert := env.Patch(t, "KUBECONFIG", "./testdata/test-kubeconfig")
defer revert()
err := runCreate(cli, &createOptions{
name: name,
defaultStackOrchestrator: orchestrator,
description: "description of " + name,
kubernetes: map[string]string{keyFromCurrent: "true"},
docker: map[string]string{keyHost: "https://someswarmserver"},
})
assert.NilError(t, err)
}
func TestList(t *testing.T) {
cli, cleanup := makeFakeCli(t)
defer cleanup()
createTestContextWithKubeAndSwarm(t, cli, "current", "all")
createTestContextWithKubeAndSwarm(t, cli, "other", "all")
createTestContextWithKubeAndSwarm(t, cli, "unset", "unset")
cli.SetCurrentContext("current")
cli.OutBuffer().Reset()
assert.NilError(t, runList(cli, &listOptions{}))
golden.Assert(t, cli.OutBuffer().String(), "list.golden")
}
func TestListNoContext(t *testing.T) {
cli, cleanup := makeFakeCli(t)
defer cleanup()
defer env.Patch(t, "KUBECONFIG", "./testdata/test-kubeconfig")()
cli.SetDockerEndpoint(docker.Endpoint{
EndpointMeta: docker.EndpointMeta{
Host: "https://someswarmserver",
},
})
cli.OutBuffer().Reset()
assert.NilError(t, runList(cli, &listOptions{}))
golden.Assert(t, cli.OutBuffer().String(), "list.no-context.golden")
}
func TestListQuiet(t *testing.T) {
cli, cleanup := makeFakeCli(t)
defer cleanup()
createTestContextWithKubeAndSwarm(t, cli, "current", "all")
createTestContextWithKubeAndSwarm(t, cli, "other", "all")
cli.SetCurrentContext("current")
cli.OutBuffer().Reset()
assert.NilError(t, runList(cli, &listOptions{quiet: true}))
golden.Assert(t, cli.OutBuffer().String(), "quiet-list.golden")
}

View File

@ -0,0 +1,221 @@
package context
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/context"
"github.com/docker/cli/cli/context/docker"
"github.com/docker/cli/cli/context/kubernetes"
"github.com/docker/cli/cli/context/store"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/homedir"
"github.com/pkg/errors"
)
const (
keyFromCurrent = "from-current"
keyHost = "host"
keyCA = "ca"
keyCert = "cert"
keyKey = "key"
keySkipTLSVerify = "skip-tls-verify"
keyKubeconfig = "config-file"
keyKubecontext = "context-override"
keyKubenamespace = "namespace-override"
)
type configKeyDescription struct {
name string
description string
}
var (
allowedDockerConfigKeys = map[string]struct{}{
keyFromCurrent: {},
keyHost: {},
keyCA: {},
keyCert: {},
keyKey: {},
keySkipTLSVerify: {},
}
allowedKubernetesConfigKeys = map[string]struct{}{
keyFromCurrent: {},
keyKubeconfig: {},
keyKubecontext: {},
keyKubenamespace: {},
}
dockerConfigKeysDescriptions = []configKeyDescription{
{
name: keyFromCurrent,
description: "Copy current Docker endpoint configuration",
},
{
name: keyHost,
description: "Docker endpoint on which to connect",
},
{
name: keyCA,
description: "Trust certs signed only by this CA",
},
{
name: keyCert,
description: "Path to TLS certificate file",
},
{
name: keyKey,
description: "Path to TLS key file",
},
{
name: keySkipTLSVerify,
description: "Skip TLS certificate validation",
},
}
kubernetesConfigKeysDescriptions = []configKeyDescription{
{
name: keyFromCurrent,
description: "Copy current Kubernetes endpoint configuration",
},
{
name: keyKubeconfig,
description: "Path to a Kubernetes config file",
},
{
name: keyKubecontext,
description: "Overrides the context set in the kubernetes config file",
},
{
name: keyKubenamespace,
description: "Overrides the namespace set in the kubernetes config file",
},
}
)
func parseBool(config map[string]string, name string) (bool, error) {
strVal, ok := config[name]
if !ok {
return false, nil
}
res, err := strconv.ParseBool(strVal)
return res, errors.Wrap(err, name)
}
func validateConfig(config map[string]string, allowedKeys map[string]struct{}) error {
var errs []string
for k := range config {
if _, ok := allowedKeys[k]; !ok {
errs = append(errs, fmt.Sprintf("%s: unrecognized config key", k))
}
}
if len(errs) == 0 {
return nil
}
return errors.New(strings.Join(errs, "\n"))
}
func getDockerEndpoint(dockerCli command.Cli, config map[string]string) (docker.Endpoint, error) {
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
}
tlsData, err := context.TLSDataFromFiles(config[keyCA], config[keyCert], config[keyKey])
if err != nil {
return docker.Endpoint{}, err
}
skipTLSVerify, err := parseBool(config, keySkipTLSVerify)
if err != nil {
return docker.Endpoint{}, err
}
ep := docker.Endpoint{
EndpointMeta: docker.EndpointMeta{
Host: config[keyHost],
SkipTLSVerify: skipTLSVerify,
},
TLSData: tlsData,
}
// try to resolve a docker client, validating the configuration
opts, err := ep.ClientOpts()
if err != nil {
return docker.Endpoint{}, errors.Wrap(err, "invalid docker endpoint options")
}
if _, err := client.NewClientWithOpts(opts...); err != nil {
return docker.Endpoint{}, errors.Wrap(err, "unable to apply docker endpoint options")
}
return ep, nil
}
func getDockerEndpointMetadataAndTLS(dockerCli command.Cli, config map[string]string) (docker.EndpointMeta, *store.EndpointTLSData, error) {
ep, err := getDockerEndpoint(dockerCli, config)
if err != nil {
return docker.EndpointMeta{}, nil, err
}
return ep.EndpointMeta, ep.TLSData.ToStoreTLSData(), nil
}
func getKubernetesEndpoint(dockerCli command.Cli, config map[string]string) (*kubernetes.Endpoint, error) {
if err := validateConfig(config, allowedKubernetesConfigKeys); err != nil {
return nil, err
}
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 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
}
}
// fallback to env-based kubeconfig
kubeconfig := os.Getenv("KUBECONFIG")
if kubeconfig == "" {
kubeconfig = filepath.Join(homedir.Get(), ".kube/config")
}
ep, err := kubernetes.FromKubeConfig(kubeconfig, "", "")
if err != nil {
return nil, err
}
return &ep, nil
}
if config[keyKubeconfig] != "" {
ep, err := kubernetes.FromKubeConfig(config[keyKubeconfig], config[keyKubecontext], config[keyKubenamespace])
if err != nil {
return nil, err
}
return &ep, nil
}
return nil, nil
}
func getKubernetesEndpointMetadataAndTLS(dockerCli command.Cli, config map[string]string) (*kubernetes.EndpointMeta, *store.EndpointTLSData, error) {
ep, err := getKubernetesEndpoint(dockerCli, config)
if err != nil {
return nil, nil, err
}
if ep == nil {
return nil, nil, err
}
return &ep.EndpointMeta, ep.TLSData.ToStoreTLSData(), nil
}

View File

@ -0,0 +1,66 @@
package context
import (
"errors"
"fmt"
"strings"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
)
type removeOptions struct {
force bool
}
func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
var opts removeOptions
cmd := &cobra.Command{
Use: "rm CONTEXT [CONTEXT...]",
Aliases: []string{"remove"},
Short: "Remove one or more contexts",
Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runRemove(dockerCli, opts, args)
},
}
cmd.Flags().BoolVarP(&opts.force, "force", "f", false, "Force the removal of a context in use")
return cmd
}
func runRemove(dockerCli command.Cli, opts removeOptions, names []string) error {
var errs []string
currentCtx := dockerCli.CurrentContext()
for _, name := range names {
if name == "default" {
errs = append(errs, `default: context "default" cannot be removed`)
} else if err := doRemove(dockerCli, name, name == currentCtx, opts.force); err != nil {
errs = append(errs, fmt.Sprintf("%s: %s", name, err))
} else {
fmt.Fprintln(dockerCli.Out(), name)
}
}
if len(errs) > 0 {
return errors.New(strings.Join(errs, "\n"))
}
return nil
}
func doRemove(dockerCli command.Cli, name string, isCurrent, force bool) error {
if _, err := dockerCli.ContextStore().GetContextMetadata(name); err != nil {
return err
}
if isCurrent {
if !force {
return errors.New("context is in use, set -f flag to force remove")
}
// fallback to DOCKER_HOST
cfg := dockerCli.ConfigFile()
cfg.CurrentContext = ""
if err := cfg.Save(); err != nil {
return err
}
}
return dockerCli.ContextStore().RemoveContext(name)
}

View File

@ -0,0 +1,64 @@
package context
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/context/store"
"gotest.tools/assert"
)
func TestRemove(t *testing.T) {
cli, cleanup := makeFakeCli(t)
defer cleanup()
createTestContextWithKubeAndSwarm(t, cli, "current", "all")
createTestContextWithKubeAndSwarm(t, cli, "other", "all")
assert.NilError(t, runRemove(cli, removeOptions{}, []string{"other"}))
_, err := cli.ContextStore().GetContextMetadata("current")
assert.NilError(t, err)
_, err = cli.ContextStore().GetContextMetadata("other")
assert.Check(t, store.IsErrContextDoesNotExist(err))
}
func TestRemoveNotAContext(t *testing.T) {
cli, cleanup := makeFakeCli(t)
defer cleanup()
createTestContextWithKubeAndSwarm(t, cli, "current", "all")
createTestContextWithKubeAndSwarm(t, cli, "other", "all")
err := runRemove(cli, removeOptions{}, []string{"not-a-context"})
assert.ErrorContains(t, err, `context "not-a-context" does not exist`)
}
func TestRemoveCurrent(t *testing.T) {
cli, cleanup := makeFakeCli(t)
defer cleanup()
createTestContextWithKubeAndSwarm(t, cli, "current", "all")
createTestContextWithKubeAndSwarm(t, cli, "other", "all")
cli.SetCurrentContext("current")
err := runRemove(cli, removeOptions{}, []string{"current"})
assert.ErrorContains(t, err, "current: context is in use, set -f flag to force remove")
}
func TestRemoveCurrentForce(t *testing.T) {
configDir, err := ioutil.TempDir("", t.Name()+"config")
assert.NilError(t, err)
defer os.RemoveAll(configDir)
configFilePath := filepath.Join(configDir, "config.json")
testCfg := configfile.New(configFilePath)
testCfg.CurrentContext = "current"
assert.NilError(t, testCfg.Save())
cli, cleanup := makeFakeCli(t, withCliConfig(testCfg))
defer cleanup()
createTestContextWithKubeAndSwarm(t, cli, "current", "all")
createTestContextWithKubeAndSwarm(t, cli, "other", "all")
cli.SetCurrentContext("current")
assert.NilError(t, runRemove(cli, removeOptions{force: true}, []string{"current"}))
reloadedConfig, err := config.Load(configDir)
assert.NilError(t, err)
assert.Equal(t, "", reloadedConfig.CurrentContext)
}

View File

@ -0,0 +1,31 @@
[
{
"Name": "current",
"Metadata": {
"Description": "description of current",
"StackOrchestrator": "all"
},
"Endpoints": {
"docker": {
"Host": "https://someswarmserver",
"SkipTLSVerify": false
},
"kubernetes": {
"Host": "https://someserver",
"SkipTLSVerify": false,
"DefaultNamespace": "default"
}
},
"TLSMaterial": {
"kubernetes": [
"ca.pem",
"cert.pem",
"key.pem"
]
},
"Storage": {
"MetadataPath": "<METADATA_PATH>",
"TLSPath": "<TLS_PATH>"
}
}
]

View File

@ -0,0 +1,5 @@
NAME DESCRIPTION DOCKER ENDPOINT KUBERNETES ENDPOINT ORCHESTRATOR
current * description of current https://someswarmserver https://someserver (default) all
default Current DOCKER_HOST based configuration
other description of other https://someswarmserver https://someserver (default) all
unset description of unset https://someswarmserver https://someserver (default)

View File

@ -0,0 +1,2 @@
NAME DESCRIPTION DOCKER ENDPOINT KUBERNETES ENDPOINT ORCHESTRATOR
default * Current DOCKER_HOST based configuration https://someswarmserver https://someserver (default) swarm

View File

@ -0,0 +1,2 @@
current
other

View File

@ -0,0 +1,19 @@
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: dGhlLWNh
server: https://someserver
name: test-cluster
contexts:
- context:
cluster: test-cluster
user: test-user
name: test
current-context: test
kind: Config
preferences: {}
users:
- name: test-user
user:
client-certificate-data: dGhlLWNlcnQ=
client-key-data: dGhlLWtleQ==

View File

@ -0,0 +1,142 @@
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 updateOptions struct {
name string
description string
defaultStackOrchestrator string
docker map[string]string
kubernetes map[string]string
}
func longUpdateDescription() string {
buf := bytes.NewBuffer(nil)
buf.WriteString("Update 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 update my-context --description \"some description\" --docker \"host=tcp://myserver:2376,ca=~/ca-file,cert=~/cert-file,key=~/key-file\"\n")
return buf.String()
}
func newUpdateCommand(dockerCli command.Cli) *cobra.Command {
opts := &updateOptions{}
cmd := &cobra.Command{
Use: "update [OPTIONS] CONTEXT",
Short: "Update a context",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.name = args[0]
return runUpdate(dockerCli, opts)
},
Long: longUpdateDescription(),
}
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 runUpdate(cli command.Cli, o *updateOptions) error {
if err := validateContextName(o.name); err != nil {
return err
}
s := cli.ContextStore()
c, err := s.GetContextMetadata(o.name)
if err != nil {
return err
}
dockerContext, err := command.GetDockerContext(c)
if err != nil {
return err
}
if o.defaultStackOrchestrator != "" {
stackOrchestrator, err := command.NormalizeOrchestrator(o.defaultStackOrchestrator)
if err != nil {
return errors.Wrap(err, "unable to parse default-stack-orchestrator")
}
dockerContext.StackOrchestrator = stackOrchestrator
}
if o.description != "" {
dockerContext.Description = o.description
}
c.Metadata = dockerContext
tlsDataToReset := make(map[string]*store.EndpointTLSData)
if o.docker != nil {
dockerEP, dockerTLS, err := getDockerEndpointMetadataAndTLS(cli, o.docker)
if err != nil {
return errors.Wrap(err, "unable to create docker endpoint config")
}
c.Endpoints[docker.DockerEndpoint] = dockerEP
tlsDataToReset[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 {
delete(c.Endpoints, kubernetes.KubernetesEndpoint)
} else {
c.Endpoints[kubernetes.KubernetesEndpoint] = kubernetesEP
tlsDataToReset[kubernetes.KubernetesEndpoint] = kubernetesTLS
}
}
if err := validateEndpointsAndOrchestrator(c); err != nil {
return err
}
if err := s.CreateOrUpdateContext(c); err != nil {
return err
}
for ep, tlsData := range tlsDataToReset {
if err := s.ResetContextEndpointTLSMaterial(o.name, ep, tlsData); err != nil {
return err
}
}
fmt.Fprintln(cli.Out(), o.name)
fmt.Fprintf(cli.Err(), "Successfully updated context %q\n", o.name)
return nil
}
func validateEndpointsAndOrchestrator(c store.ContextMetadata) error {
dockerContext, err := command.GetDockerContext(c)
if err != nil {
return err
}
if _, ok := c.Endpoints[kubernetes.KubernetesEndpoint]; !ok && dockerContext.StackOrchestrator.HasKubernetes() {
return errors.Errorf("cannot specify orchestrator %q without configuring a Kubernetes endpoint", dockerContext.StackOrchestrator)
}
return nil
}

View File

@ -0,0 +1,102 @@
package context
import (
"testing"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/context/docker"
"github.com/docker/cli/cli/context/kubernetes"
"gotest.tools/assert"
"gotest.tools/assert/cmp"
)
func TestUpdateDescriptionOnly(t *testing.T) {
cli, cleanup := makeFakeCli(t)
defer cleanup()
err := runCreate(cli, &createOptions{
name: "test",
defaultStackOrchestrator: "swarm",
docker: map[string]string{},
})
assert.NilError(t, err)
cli.OutBuffer().Reset()
cli.ErrBuffer().Reset()
assert.NilError(t, runUpdate(cli, &updateOptions{
name: "test",
description: "description",
}))
c, err := cli.ContextStore().GetContextMetadata("test")
assert.NilError(t, err)
dc, err := command.GetDockerContext(c)
assert.NilError(t, err)
assert.Equal(t, dc.StackOrchestrator, command.OrchestratorSwarm)
assert.Equal(t, dc.Description, "description")
assert.Equal(t, "test\n", cli.OutBuffer().String())
assert.Equal(t, "Successfully updated context \"test\"\n", cli.ErrBuffer().String())
}
func TestUpdateDockerOnly(t *testing.T) {
cli, cleanup := makeFakeCli(t)
defer cleanup()
createTestContextWithKubeAndSwarm(t, cli, "test", "swarm")
assert.NilError(t, runUpdate(cli, &updateOptions{
name: "test",
docker: map[string]string{
keyHost: "tcp://some-host",
},
}))
c, err := cli.ContextStore().GetContextMetadata("test")
assert.NilError(t, err)
dc, err := command.GetDockerContext(c)
assert.NilError(t, err)
assert.Equal(t, dc.StackOrchestrator, command.OrchestratorSwarm)
assert.Equal(t, dc.Description, "description of test")
assert.Check(t, cmp.Contains(c.Endpoints, kubernetes.KubernetesEndpoint))
assert.Check(t, cmp.Contains(c.Endpoints, docker.DockerEndpoint))
assert.Equal(t, c.Endpoints[docker.DockerEndpoint].(docker.EndpointMeta).Host, "tcp://some-host")
}
func TestUpdateStackOrchestratorStrategy(t *testing.T) {
cli, cleanup := makeFakeCli(t)
defer cleanup()
err := runCreate(cli, &createOptions{
name: "test",
defaultStackOrchestrator: "swarm",
docker: map[string]string{},
})
assert.NilError(t, err)
err = runUpdate(cli, &updateOptions{
name: "test",
defaultStackOrchestrator: "kubernetes",
})
assert.ErrorContains(t, err, `cannot specify orchestrator "kubernetes" without configuring a Kubernetes endpoint`)
}
func TestUpdateStackOrchestratorStrategyRemoveKubeEndpoint(t *testing.T) {
cli, cleanup := makeFakeCli(t)
defer cleanup()
createTestContextWithKubeAndSwarm(t, cli, "test", "kubernetes")
err := runUpdate(cli, &updateOptions{
name: "test",
kubernetes: map[string]string{},
})
assert.ErrorContains(t, err, `cannot specify orchestrator "kubernetes" without configuring a Kubernetes endpoint`)
}
func TestUpdateInvalidDockerHost(t *testing.T) {
cli, cleanup := makeFakeCli(t)
defer cleanup()
err := runCreate(cli, &createOptions{
name: "test",
docker: map[string]string{},
})
assert.NilError(t, err)
err = runUpdate(cli, &updateOptions{
name: "test",
docker: map[string]string{
keyHost: "some///invalid/host",
},
})
assert.ErrorContains(t, err, "unable to parse docker host")
}

View File

@ -0,0 +1,39 @@
package context
import (
"fmt"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
)
func newUseCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "use CONTEXT",
Short: "Set the current docker context",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
if err := validateContextName(name); err != nil && name != "default" {
return err
}
if _, err := dockerCli.ContextStore().GetContextMetadata(name); err != nil && name != "default" {
return err
}
configValue := name
if configValue == "default" {
configValue = ""
}
dockerConfig := dockerCli.ConfigFile()
dockerConfig.CurrentContext = configValue
if err := dockerConfig.Save(); err != nil {
return err
}
fmt.Fprintln(dockerCli.Out(), name)
fmt.Fprintf(dockerCli.Err(), "Current context is now %q\n", name)
return nil
},
}
return cmd
}

View File

@ -0,0 +1,49 @@
package context
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/context/store"
"gotest.tools/assert"
)
func TestUse(t *testing.T) {
configDir, err := ioutil.TempDir("", t.Name()+"config")
assert.NilError(t, err)
defer os.RemoveAll(configDir)
configFilePath := filepath.Join(configDir, "config.json")
testCfg := configfile.New(configFilePath)
cli, cleanup := makeFakeCli(t, withCliConfig(testCfg))
defer cleanup()
err = runCreate(cli, &createOptions{
name: "test",
docker: map[string]string{},
})
assert.NilError(t, err)
assert.NilError(t, newUseCommand(cli).RunE(nil, []string{"test"}))
reloadedConfig, err := config.Load(configDir)
assert.NilError(t, err)
assert.Equal(t, "test", reloadedConfig.CurrentContext)
// switch back to default
cli.OutBuffer().Reset()
cli.ErrBuffer().Reset()
assert.NilError(t, newUseCommand(cli).RunE(nil, []string{"default"}))
reloadedConfig, err = config.Load(configDir)
assert.NilError(t, err)
assert.Equal(t, "", reloadedConfig.CurrentContext)
assert.Equal(t, "default\n", cli.OutBuffer().String())
assert.Equal(t, "Current context is now \"default\"\n", cli.ErrBuffer().String())
}
func TestUseNoExist(t *testing.T) {
cli, cleanup := makeFakeCli(t)
defer cleanup()
err := newUseCommand(cli).RunE(nil, []string{"test"})
assert.Check(t, store.IsErrContextDoesNotExist(err))
}

View File

@ -0,0 +1,90 @@
package formatter
const (
// ClientContextTableFormat is the default client context format
ClientContextTableFormat = "table {{.Name}}{{if .Current}} *{{end}}\t{{.Description}}\t{{.DockerEndpoint}}\t{{.KubernetesEndpoint}}\t{{.StackOrchestrator}}"
dockerEndpointHeader = "DOCKER ENDPOINT"
kubernetesEndpointHeader = "KUBERNETES ENDPOINT"
stackOrchestrastorHeader = "ORCHESTRATOR"
quietContextFormat = "{{.Name}}"
)
// NewClientContextFormat returns a Format for rendering using a Context
func NewClientContextFormat(source string, quiet bool) Format {
if quiet {
return Format(quietContextFormat)
}
if source == TableFormatKey {
return Format(ClientContextTableFormat)
}
return Format(source)
}
// 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 Context, contexts []*ClientContext) error {
render := func(format func(subContext 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 {
HeaderContext
c *ClientContext
}
func newClientContextContext() *clientContextContext {
ctx := clientContextContext{}
ctx.Header = SubHeaderContext{
"Name": NameHeader,
"Description": DescriptionHeader,
"DockerEndpoint": dockerEndpointHeader,
"KubernetesEndpoint": kubernetesEndpointHeader,
"StackOrchestrator": stackOrchestrastorHeader,
}
return &ctx
}
func (c *clientContextContext) MarshalJSON() ([]byte, error) {
return MarshalJSON(c)
}
func (c *clientContextContext) Current() bool {
return c.c.Current
}
func (c *clientContextContext) Name() string {
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
}

View File

@ -16,7 +16,7 @@ const (
OrchestratorSwarm = Orchestrator("swarm")
// OrchestratorAll orchestrator
OrchestratorAll = Orchestrator("all")
orchestratorUnset = Orchestrator("unset")
orchestratorUnset = Orchestrator("")
defaultOrchestrator = OrchestratorSwarm
envVarDockerStackOrchestrator = "DOCKER_STACK_ORCHESTRATOR"
@ -44,7 +44,7 @@ func normalize(value string) (Orchestrator, error) {
return OrchestratorKubernetes, nil
case "swarm":
return OrchestratorSwarm, nil
case "", "unset":
case "", "unset": // unset is the old value for orchestratorUnset. Keep accepting this for backward compat
return orchestratorUnset, nil
case "all":
return OrchestratorAll, nil

View File

@ -241,7 +241,7 @@ func getKubernetesVersion(dockerCli command.Cli, kubeConfig string) *kubernetesV
clientConfig clientcmd.ClientConfig
err error
)
if dockerCli.CurrentContext() == command.ContextDockerHost {
if dockerCli.CurrentContext() == "" {
clientConfig = kubernetes.NewKubernetesConfig(kubeConfig)
} else {
clientConfig, err = kubecontext.ConfigFromContext(dockerCli.CurrentContext(), dockerCli.ContextStore())

View File

@ -20,10 +20,7 @@ import (
// EndpointMeta is a typed wrapper around a context-store generic endpoint describing
// a Docker Engine endpoint, without its tls config
type EndpointMeta struct {
context.EndpointMetaBase
APIVersion string `json:"api_version,omitempty"`
}
type EndpointMeta = context.EndpointMetaBase
// Endpoint is a typed wrapper around a context-store generic endpoint describing
// a Docker Engine endpoint, with its tls data
@ -34,13 +31,13 @@ type Endpoint struct {
}
// WithTLSData loads TLS materials for the endpoint
func (c *EndpointMeta) WithTLSData(s store.Store, contextName string) (Endpoint, error) {
func WithTLSData(s store.Store, contextName string, m EndpointMeta) (Endpoint, error) {
tlsData, err := context.LoadTLSData(s, contextName, DockerEndpoint)
if err != nil {
return Endpoint{}, err
}
return Endpoint{
EndpointMeta: *c,
EndpointMeta: m,
TLSData: tlsData,
}, nil
}
@ -128,9 +125,6 @@ func (c *Endpoint) ClientOpts() ([]func(*client.Client) error, error) {
}
version := os.Getenv("DOCKER_API_VERSION")
if version == "" {
version = c.APIVersion
}
if version != "" {
result = append(result, client.WithVersion(version))
}

View File

@ -2,6 +2,6 @@ package context
// EndpointMetaBase contains fields we expect to be common for most context endpoints
type EndpointMetaBase struct {
Host string `json:"host,omitempty"`
SkipTLSVerify bool `json:"skip_tls_verify"`
Host string `json:",omitempty"`
SkipTLSVerify bool
}

View File

@ -12,7 +12,7 @@ import (
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
)
func testEndpoint(server, defaultNamespace string, ca, cert, key []byte, skipTLSVerify bool) *Endpoint {
func testEndpoint(server, defaultNamespace string, ca, cert, key []byte, skipTLSVerify bool) Endpoint {
var tlsData *context.TLSData
if ca != nil || cert != nil || key != nil {
tlsData = &context.TLSData{
@ -21,7 +21,7 @@ func testEndpoint(server, defaultNamespace string, ca, cert, key []byte, skipTLS
Key: key,
}
}
return &Endpoint{
return Endpoint{
EndpointMeta: EndpointMeta{
EndpointMetaBase: context.EndpointMetaBase{
Host: server,
@ -45,9 +45,9 @@ func TestSaveLoadContexts(t *testing.T) {
assert.NilError(t, err)
defer os.RemoveAll(storeDir)
store := store.New(storeDir, testStoreCfg)
assert.NilError(t, testEndpoint("https://test", "test", nil, nil, nil, false).Save(store, "raw-notls"))
assert.NilError(t, testEndpoint("https://test", "test", nil, nil, nil, true).Save(store, "raw-notls-skip"))
assert.NilError(t, testEndpoint("https://test", "test", []byte("ca"), []byte("cert"), []byte("key"), true).Save(store, "raw-tls"))
assert.NilError(t, save(store, testEndpoint("https://test", "test", nil, nil, nil, false), "raw-notls"))
assert.NilError(t, save(store, testEndpoint("https://test", "test", nil, nil, nil, true), "raw-notls-skip"))
assert.NilError(t, save(store, testEndpoint("https://test", "test", []byte("ca"), []byte("cert"), []byte("key"), true), "raw-tls"))
kcFile, err := ioutil.TempFile(os.TempDir(), "test-load-save-k8-context")
assert.NilError(t, err)
@ -82,8 +82,8 @@ func TestSaveLoadContexts(t *testing.T) {
assert.NilError(t, err)
epContext2, err := FromKubeConfig(kcFile.Name(), "context2", "namespace-override")
assert.NilError(t, err)
assert.NilError(t, epDefault.Save(store, "embed-default-context"))
assert.NilError(t, epContext2.Save(store, "embed-context2"))
assert.NilError(t, save(store, epDefault, "embed-default-context"))
assert.NilError(t, save(store, epContext2, "embed-context2"))
rawNoTLSMeta, err := store.GetContextMetadata("raw-notls")
assert.NilError(t, err)
@ -132,6 +132,19 @@ func checkClientConfig(t *testing.T, s store.Store, ep Endpoint, server, namespa
assert.Equal(t, skipTLSVerify, cfg.Insecure)
}
func save(s store.Store, ep Endpoint, name string) error {
meta := store.ContextMetadata{
Endpoints: map[string]interface{}{
KubernetesEndpoint: ep.EndpointMeta,
},
Name: name,
}
if err := s.CreateOrUpdateContext(meta); err != nil {
return err
}
return s.ResetContextEndpointTLSMaterial(name, KubernetesEndpoint, ep.TLSData.ToStoreTLSData())
}
func TestSaveLoadGKEConfig(t *testing.T) {
storeDir, err := ioutil.TempDir("", t.Name())
assert.NilError(t, err)
@ -144,7 +157,7 @@ func TestSaveLoadGKEConfig(t *testing.T) {
assert.NilError(t, err)
ep, err := FromKubeConfig("testdata/gke-kubeconfig", "", "")
assert.NilError(t, err)
assert.NilError(t, ep.Save(store, "gke-context"))
assert.NilError(t, save(store, ep, "gke-context"))
persistedMetadata, err := store.GetContextMetadata("gke-context")
assert.NilError(t, err)
persistedEPMeta := EndpointFromContext(persistedMetadata)
@ -169,7 +182,7 @@ func TestSaveLoadEKSConfig(t *testing.T) {
assert.NilError(t, err)
ep, err := FromKubeConfig("testdata/eks-kubeconfig", "", "")
assert.NilError(t, err)
assert.NilError(t, ep.Save(store, "eks-context"))
assert.NilError(t, save(store, ep, "eks-context"))
persistedMetadata, err := store.GetContextMetadata("eks-context")
assert.NilError(t, err)
persistedEPMeta := EndpointFromContext(persistedMetadata)

View File

@ -12,9 +12,9 @@ import (
// a Kubernetes endpoint, without TLS data
type EndpointMeta struct {
context.EndpointMetaBase
DefaultNamespace string `json:"default_namespace,omitempty"`
AuthProvider *clientcmdapi.AuthProviderConfig `json:"auth_provider,omitempty"`
Exec *clientcmdapi.ExecConfig `json:"exec,omitempty"`
DefaultNamespace string `json:",omitempty"`
AuthProvider *clientcmdapi.AuthProviderConfig `json:",omitempty"`
Exec *clientcmdapi.ExecConfig `json:",omitempty"`
}
// Endpoint is a typed wrapper around a context-store generic endpoint describing

View File

@ -4,7 +4,6 @@ import (
"io/ioutil"
"github.com/docker/cli/cli/context"
"github.com/docker/cli/cli/context/store"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
)
@ -60,20 +59,3 @@ func readFileOrDefault(path string, defaultValue []byte) ([]byte, error) {
}
return defaultValue, nil
}
// Save the endpoint metadata and TLS bundle in the context store
func (ep *Endpoint) Save(s store.Store, contextName string) error {
tlsData := ep.TLSData.ToStoreTLSData()
existingContext, err := s.GetContextMetadata(contextName)
if err != nil && !store.IsErrContextDoesNotExist(err) {
return err
}
if existingContext.Endpoints == nil {
existingContext.Endpoints = make(map[string]interface{})
}
existingContext.Endpoints[KubernetesEndpoint] = ep.EndpointMeta
if err := s.CreateOrUpdateContext(contextName, existingContext); err != nil {
return err
}
return s.ResetContextEndpointTLSMaterial(contextName, KubernetesEndpoint, tlsData)
}

View File

@ -10,11 +10,14 @@ import (
"gotest.tools/assert/cmp"
)
var testMetadata = ContextMetadata{
func testMetadata(name string) ContextMetadata {
return ContextMetadata{
Endpoints: map[string]interface{}{
"ep1": endpoint{Foo: "bar"},
},
Metadata: context{Bar: "baz"},
Name: name,
}
}
func TestMetadataGetNotExisting(t *testing.T) {
@ -37,26 +40,28 @@ func TestMetadataCreateGetRemove(t *testing.T) {
"ep2": endpoint{Foo: "bee"},
},
Metadata: context{Bar: "foo"},
Name: "test-context",
}
err = testee.createOrUpdate("test-context", testMetadata)
testMeta := testMetadata("test-context")
err = testee.createOrUpdate(testMeta)
assert.NilError(t, err)
// create a new instance to check it does not depend on some sort of state
testee = metadataStore{root: testDir, config: testCfg}
meta, err := testee.get("test-context")
meta, err := testee.get(contextdirOf("test-context"))
assert.NilError(t, err)
assert.DeepEqual(t, meta, testMetadata)
assert.DeepEqual(t, meta, testMeta)
// update
err = testee.createOrUpdate("test-context", expected2)
err = testee.createOrUpdate(expected2)
assert.NilError(t, err)
meta, err = testee.get("test-context")
meta, err = testee.get(contextdirOf("test-context"))
assert.NilError(t, err)
assert.DeepEqual(t, meta, expected2)
assert.NilError(t, testee.remove("test-context"))
assert.NilError(t, testee.remove("test-context")) // support duplicate remove
_, err = testee.get("test-context")
assert.NilError(t, testee.remove(contextdirOf("test-context")))
assert.NilError(t, testee.remove(contextdirOf("test-context"))) // support duplicate remove
_, err = testee.get(contextdirOf("test-context"))
assert.Assert(t, IsErrContextDoesNotExist(err))
}
@ -65,8 +70,8 @@ func TestMetadataRespectJsonAnnotation(t *testing.T) {
assert.NilError(t, err)
defer os.RemoveAll(testDir)
testee := metadataStore{root: testDir, config: testCfg}
assert.NilError(t, testee.createOrUpdate("test", testMetadata))
bytes, err := ioutil.ReadFile(filepath.Join(testDir, "test", "meta.json"))
assert.NilError(t, testee.createOrUpdate(testMetadata("test")))
bytes, err := ioutil.ReadFile(filepath.Join(testDir, string(contextdirOf("test")), "meta.json"))
assert.NilError(t, err)
assert.Assert(t, cmp.Contains(string(bytes), "a_very_recognizable_field_name"))
assert.Assert(t, cmp.Contains(string(bytes), "another_very_recognizable_field_name"))
@ -77,16 +82,14 @@ func TestMetadataList(t *testing.T) {
assert.NilError(t, err)
defer os.RemoveAll(testDir)
testee := metadataStore{root: testDir, config: testCfg}
wholeData := map[string]ContextMetadata{
"simple": testMetadata,
"simple2": testMetadata,
"nested/context": testMetadata,
"nestedwith-parent/context": testMetadata,
"nestedwith-parent": testMetadata,
wholeData := []ContextMetadata{
testMetadata("context1"),
testMetadata("context2"),
testMetadata("context3"),
}
for k, s := range wholeData {
err = testee.createOrUpdate(k, s)
for _, s := range wholeData {
err = testee.createOrUpdate(s)
assert.NilError(t, err)
}
@ -100,16 +103,14 @@ func TestEmptyConfig(t *testing.T) {
assert.NilError(t, err)
defer os.RemoveAll(testDir)
testee := metadataStore{root: testDir}
wholeData := map[string]ContextMetadata{
"simple": testMetadata,
"simple2": testMetadata,
"nested/context": testMetadata,
"nestedwith-parent/context": testMetadata,
"nestedwith-parent": testMetadata,
wholeData := []ContextMetadata{
testMetadata("context1"),
testMetadata("context2"),
testMetadata("context3"),
}
for k, s := range wholeData {
err = testee.createOrUpdate(k, s)
for _, s := range wholeData {
err = testee.createOrUpdate(s)
assert.NilError(t, err)
}
@ -135,8 +136,8 @@ func TestWithEmbedding(t *testing.T) {
Val: "Hello",
},
}
assert.NilError(t, testee.createOrUpdate("test", ContextMetadata{Metadata: testCtxMeta}))
res, err := testee.get("test")
assert.NilError(t, testee.createOrUpdate(ContextMetadata{Metadata: testCtxMeta, Name: "test"}))
res, err := testee.get(contextdirOf("test"))
assert.NilError(t, err)
assert.Equal(t, testCtxMeta, res.Metadata)
}

View File

@ -7,6 +7,9 @@ import (
"os"
"path/filepath"
"reflect"
"sort"
"vbom.ml/util/sortorder"
)
const (
@ -19,12 +22,12 @@ type metadataStore struct {
config Config
}
func (s *metadataStore) contextDir(name string) string {
return filepath.Join(s.root, name)
func (s *metadataStore) contextDir(id contextdir) string {
return filepath.Join(s.root, string(id))
}
func (s *metadataStore) createOrUpdate(name string, meta ContextMetadata) error {
contextDir := s.contextDir(name)
func (s *metadataStore) createOrUpdate(meta ContextMetadata) error {
contextDir := s.contextDir(contextdirOf(meta.Name))
if err := os.MkdirAll(contextDir, 0755); err != nil {
return err
}
@ -53,11 +56,11 @@ func parseTypedOrMap(payload []byte, getter TypeGetter) (interface{}, error) {
return reflect.ValueOf(typed).Elem().Interface(), nil
}
func (s *metadataStore) get(name string) (ContextMetadata, error) {
contextDir := s.contextDir(name)
func (s *metadataStore) get(id contextdir) (ContextMetadata, error) {
contextDir := s.contextDir(id)
bytes, err := ioutil.ReadFile(filepath.Join(contextDir, metaFile))
if err != nil {
return ContextMetadata{}, convertContextDoesNotExist(name, err)
return ContextMetadata{}, convertContextDoesNotExist(err)
}
var untyped untypedContextMetadata
r := ContextMetadata{
@ -66,6 +69,7 @@ func (s *metadataStore) get(name string) (ContextMetadata, error) {
if err := json.Unmarshal(bytes, &untyped); err != nil {
return ContextMetadata{}, err
}
r.Name = untyped.Name
if r.Metadata, err = parseTypedOrMap(untyped.Metadata, s.config.contextType); err != nil {
return ContextMetadata{}, err
}
@ -77,28 +81,30 @@ func (s *metadataStore) get(name string) (ContextMetadata, error) {
return r, err
}
func (s *metadataStore) remove(name string) error {
contextDir := s.contextDir(name)
func (s *metadataStore) remove(id contextdir) error {
contextDir := s.contextDir(id)
return os.RemoveAll(contextDir)
}
func (s *metadataStore) list() (map[string]ContextMetadata, error) {
ctxNames, err := listRecursivelyMetadataDirs(s.root)
func (s *metadataStore) list() ([]ContextMetadata, error) {
ctxDirs, err := listRecursivelyMetadataDirs(s.root)
if err != nil {
if os.IsNotExist(err) {
// store is empty, meta dir does not exist yet
// this should not be considered an error
return map[string]ContextMetadata{}, nil
return nil, nil
}
return nil, err
}
res := make(map[string]ContextMetadata)
for _, name := range ctxNames {
res[name], err = s.get(name)
var res []ContextMetadata
for _, dir := range ctxDirs {
c, err := s.get(contextdir(dir))
if err != nil {
return nil, err
}
res = append(res, c)
}
sort.Slice(res, func(i, j int) bool {
return sortorder.NaturalLess(res[i].Name, res[j].Name)
})
return res, nil
}
@ -133,9 +139,9 @@ func listRecursivelyMetadataDirs(root string) ([]string, error) {
return result, nil
}
func convertContextDoesNotExist(name string, err error) error {
func convertContextDoesNotExist(err error) error {
if os.IsNotExist(err) {
return &contextDoesNotExistError{name: name}
return &contextDoesNotExistError{}
}
return err
}
@ -143,4 +149,5 @@ func convertContextDoesNotExist(name string, err error) error {
type untypedContextMetadata struct {
Metadata json.RawMessage `json:"metadata,omitempty"`
Endpoints map[string]json.RawMessage `json:"endpoints,omitempty"`
Name string `json:"name,omitempty"`
}

View File

@ -2,6 +2,7 @@ package store
import (
"archive/tar"
_ "crypto/sha256" // ensure ids can be computed
"encoding/json"
"errors"
"fmt"
@ -10,24 +11,34 @@ import (
"path"
"path/filepath"
"strings"
"github.com/opencontainers/go-digest"
)
// Store provides a context store for easily remembering endpoints configuration
type Store interface {
ListContexts() (map[string]ContextMetadata, error)
CreateOrUpdateContext(name string, meta ContextMetadata) error
ListContexts() ([]ContextMetadata, error)
CreateOrUpdateContext(meta ContextMetadata) error
RemoveContext(name string) error
GetContextMetadata(name string) (ContextMetadata, error)
ResetContextTLSMaterial(name string, data *ContextTLSData) error
ResetContextEndpointTLSMaterial(contextName string, endpointName string, data *EndpointTLSData) error
ListContextTLSFiles(name string) (map[string]EndpointFiles, error)
GetContextTLSData(contextName, endpointName, fileName string) ([]byte, error)
GetContextStorageInfo(contextName string) ContextStorageInfo
}
// ContextMetadata contains metadata about a context and its endpoints
type ContextMetadata struct {
Metadata interface{} `json:"metadata,omitempty"`
Endpoints map[string]interface{} `json:"endpoints,omitempty"`
Name string `json:",omitempty"`
Metadata interface{} `json:",omitempty"`
Endpoints map[string]interface{} `json:",omitempty"`
}
// ContextStorageInfo contains data about where a given context is stored
type ContextStorageInfo struct {
MetadataPath string
TLSPath string
}
// EndpointTLSData represents tls data for a given endpoint
@ -62,36 +73,40 @@ type store struct {
tls *tlsStore
}
func (s *store) ListContexts() (map[string]ContextMetadata, error) {
func (s *store) ListContexts() ([]ContextMetadata, error) {
return s.meta.list()
}
func (s *store) CreateOrUpdateContext(name string, meta ContextMetadata) error {
return s.meta.createOrUpdate(name, meta)
func (s *store) CreateOrUpdateContext(meta ContextMetadata) error {
return s.meta.createOrUpdate(meta)
}
func (s *store) RemoveContext(name string) error {
if err := s.meta.remove(name); err != nil {
return err
id := contextdirOf(name)
if err := s.meta.remove(id); err != nil {
return patchErrContextName(err, name)
}
return s.tls.removeAllContextData(name)
return patchErrContextName(s.tls.removeAllContextData(id), name)
}
func (s *store) GetContextMetadata(name string) (ContextMetadata, error) {
return s.meta.get(name)
res, err := s.meta.get(contextdirOf(name))
patchErrContextName(err, name)
return res, err
}
func (s *store) ResetContextTLSMaterial(name string, data *ContextTLSData) error {
if err := s.tls.removeAllContextData(name); err != nil {
return err
id := contextdirOf(name)
if err := s.tls.removeAllContextData(id); err != nil {
return patchErrContextName(err, name)
}
if data == nil {
return nil
}
for ep, files := range data.Endpoints {
for fileName, data := range files.Files {
if err := s.tls.createOrUpdate(name, ep, fileName, data); err != nil {
return err
if err := s.tls.createOrUpdate(id, ep, fileName, data); err != nil {
return patchErrContextName(err, name)
}
}
}
@ -99,26 +114,37 @@ func (s *store) ResetContextTLSMaterial(name string, data *ContextTLSData) error
}
func (s *store) ResetContextEndpointTLSMaterial(contextName string, endpointName string, data *EndpointTLSData) error {
if err := s.tls.removeAllEndpointData(contextName, endpointName); err != nil {
return err
id := contextdirOf(contextName)
if err := s.tls.removeAllEndpointData(id, endpointName); err != nil {
return patchErrContextName(err, contextName)
}
if data == nil {
return nil
}
for fileName, data := range data.Files {
if err := s.tls.createOrUpdate(contextName, endpointName, fileName, data); err != nil {
return err
if err := s.tls.createOrUpdate(id, endpointName, fileName, data); err != nil {
return patchErrContextName(err, contextName)
}
}
return nil
}
func (s *store) ListContextTLSFiles(name string) (map[string]EndpointFiles, error) {
return s.tls.listContextData(name)
res, err := s.tls.listContextData(contextdirOf(name))
return res, patchErrContextName(err, name)
}
func (s *store) GetContextTLSData(contextName, endpointName, fileName string) ([]byte, error) {
return s.tls.getData(contextName, endpointName, fileName)
res, err := s.tls.getData(contextdirOf(contextName), endpointName, fileName)
return res, patchErrContextName(err, contextName)
}
func (s *store) GetContextStorageInfo(contextName string) ContextStorageInfo {
dir := contextdirOf(contextName)
return ContextStorageInfo{
MetadataPath: s.meta.contextDir(dir),
TLSPath: s.tls.contextDir(dir),
}
}
// Export exports an existing namespace into an opaque data stream
@ -227,7 +253,8 @@ func Import(name string, s Store, reader io.Reader) error {
if err := json.Unmarshal(data, &meta); err != nil {
return err
}
if err := s.CreateOrUpdateContext(name, meta); err != nil {
meta.Name = name
if err := s.CreateOrUpdateContext(meta); err != nil {
return err
}
} else if strings.HasPrefix(hdr.Name, "tls/") {
@ -253,6 +280,10 @@ func Import(name string, s Store, reader io.Reader) error {
return s.ResetContextTLSMaterial(name, &tlsData)
}
type setContextName interface {
setContext(name string)
}
type contextDoesNotExistError struct {
name string
}
@ -261,6 +292,13 @@ func (e *contextDoesNotExistError) Error() string {
return fmt.Sprintf("context %q does not exist", e.name)
}
func (e *contextDoesNotExistError) setContext(name string) {
e.name = name
}
// NotFound satisfies interface github.com/docker/docker/errdefs.ErrNotFound
func (e *contextDoesNotExistError) NotFound() {}
type tlsDataDoesNotExistError struct {
context, endpoint, file string
}
@ -269,6 +307,13 @@ func (e *tlsDataDoesNotExistError) Error() string {
return fmt.Sprintf("tls data for %s/%s/%s does not exist", e.context, e.endpoint, e.file)
}
func (e *tlsDataDoesNotExistError) setContext(name string) {
e.context = name
}
// NotFound satisfies interface github.com/docker/docker/errdefs.ErrNotFound
func (e *tlsDataDoesNotExistError) NotFound() {}
// IsErrContextDoesNotExist checks if the given error is a "context does not exist" condition
func IsErrContextDoesNotExist(err error) bool {
_, ok := err.(*contextDoesNotExistError)
@ -280,3 +325,16 @@ func IsErrTLSDataDoesNotExist(err error) bool {
_, ok := err.(*tlsDataDoesNotExistError)
return ok
}
type contextdir string
func contextdirOf(name string) contextdir {
return contextdir(digest.FromString(name).Encoded())
}
func patchErrContextName(err error, name string) error {
if typed, ok := err.(setContextName); ok {
typed.setContext(name)
}
return err
}

View File

@ -26,12 +26,13 @@ func TestExportImport(t *testing.T) {
assert.NilError(t, err)
defer os.RemoveAll(testDir)
s := New(testDir, testCfg)
err = s.CreateOrUpdateContext("source",
err = s.CreateOrUpdateContext(
ContextMetadata{
Endpoints: map[string]interface{}{
"ep1": endpoint{Foo: "bar"},
},
Metadata: context{Bar: "baz"},
Name: "source",
})
assert.NilError(t, err)
err = s.ResetContextEndpointTLSMaterial("source", "ep1", &EndpointTLSData{
@ -48,7 +49,8 @@ func TestExportImport(t *testing.T) {
assert.NilError(t, err)
destMeta, err := s.GetContextMetadata("dest")
assert.NilError(t, err)
assert.DeepEqual(t, destMeta, srcMeta)
assert.DeepEqual(t, destMeta.Metadata, srcMeta.Metadata)
assert.DeepEqual(t, destMeta.Endpoints, srcMeta.Endpoints)
srcFileList, err := s.ListContextTLSFiles("source")
assert.NilError(t, err)
destFileList, err := s.ListContextTLSFiles("dest")
@ -67,12 +69,13 @@ func TestRemove(t *testing.T) {
assert.NilError(t, err)
defer os.RemoveAll(testDir)
s := New(testDir, testCfg)
err = s.CreateOrUpdateContext("source",
err = s.CreateOrUpdateContext(
ContextMetadata{
Endpoints: map[string]interface{}{
"ep1": endpoint{Foo: "bar"},
},
Metadata: context{Bar: "baz"},
Name: "source",
})
assert.NilError(t, err)
assert.NilError(t, s.ResetContextEndpointTLSMaterial("source", "ep1", &EndpointTLSData{
@ -95,6 +98,15 @@ func TestListEmptyStore(t *testing.T) {
store := New(testDir, testCfg)
result, err := store.ListContexts()
assert.NilError(t, err)
assert.Check(t, result != nil)
assert.Check(t, len(result) == 0)
}
func TestErrHasCorrectContext(t *testing.T) {
testDir, err := ioutil.TempDir("", t.Name())
assert.NilError(t, err)
defer os.RemoveAll(testDir)
store := New(testDir, testCfg)
_, err = store.GetContextMetadata("no-exists")
assert.ErrorContains(t, err, "no-exists")
assert.Check(t, IsErrContextDoesNotExist(err))
}

View File

@ -12,20 +12,20 @@ type tlsStore struct {
root string
}
func (s *tlsStore) contextDir(name string) string {
return filepath.Join(s.root, name)
func (s *tlsStore) contextDir(id contextdir) string {
return filepath.Join(s.root, string(id))
}
func (s *tlsStore) endpointDir(contextName, name string) string {
return filepath.Join(s.root, contextName, name)
func (s *tlsStore) endpointDir(contextID contextdir, name string) string {
return filepath.Join(s.root, string(contextID), name)
}
func (s *tlsStore) filePath(contextName, endpointName, filename string) string {
return filepath.Join(s.root, contextName, endpointName, filename)
func (s *tlsStore) filePath(contextID contextdir, endpointName, filename string) string {
return filepath.Join(s.root, string(contextID), endpointName, filename)
}
func (s *tlsStore) createOrUpdate(contextName, endpointName, filename string, data []byte) error {
epdir := s.endpointDir(contextName, endpointName)
func (s *tlsStore) createOrUpdate(contextID contextdir, endpointName, filename string, data []byte) error {
epdir := s.endpointDir(contextID, endpointName)
parentOfRoot := filepath.Dir(s.root)
if err := os.MkdirAll(parentOfRoot, 0755); err != nil {
return err
@ -33,35 +33,35 @@ func (s *tlsStore) createOrUpdate(contextName, endpointName, filename string, da
if err := os.MkdirAll(epdir, 0700); err != nil {
return err
}
return ioutil.WriteFile(s.filePath(contextName, endpointName, filename), data, 0600)
return ioutil.WriteFile(s.filePath(contextID, endpointName, filename), data, 0600)
}
func (s *tlsStore) getData(contextName, endpointName, filename string) ([]byte, error) {
data, err := ioutil.ReadFile(s.filePath(contextName, endpointName, filename))
func (s *tlsStore) getData(contextID contextdir, endpointName, filename string) ([]byte, error) {
data, err := ioutil.ReadFile(s.filePath(contextID, endpointName, filename))
if err != nil {
return nil, convertTLSDataDoesNotExist(contextName, endpointName, filename, err)
return nil, convertTLSDataDoesNotExist(endpointName, filename, err)
}
return data, nil
}
func (s *tlsStore) remove(contextName, endpointName, filename string) error {
err := os.Remove(s.filePath(contextName, endpointName, filename))
func (s *tlsStore) remove(contextID contextdir, endpointName, filename string) error {
err := os.Remove(s.filePath(contextID, endpointName, filename))
if os.IsNotExist(err) {
return nil
}
return err
}
func (s *tlsStore) removeAllEndpointData(contextName, endpointName string) error {
return os.RemoveAll(s.endpointDir(contextName, endpointName))
func (s *tlsStore) removeAllEndpointData(contextID contextdir, endpointName string) error {
return os.RemoveAll(s.endpointDir(contextID, endpointName))
}
func (s *tlsStore) removeAllContextData(contextName string) error {
return os.RemoveAll(s.contextDir(contextName))
func (s *tlsStore) removeAllContextData(contextID contextdir) error {
return os.RemoveAll(s.contextDir(contextID))
}
func (s *tlsStore) listContextData(contextName string) (map[string]EndpointFiles, error) {
epFSs, err := ioutil.ReadDir(s.contextDir(contextName))
func (s *tlsStore) listContextData(contextID contextdir) (map[string]EndpointFiles, error) {
epFSs, err := ioutil.ReadDir(s.contextDir(contextID))
if err != nil {
if os.IsNotExist(err) {
return map[string]EndpointFiles{}, nil
@ -71,7 +71,7 @@ func (s *tlsStore) listContextData(contextName string) (map[string]EndpointFiles
r := make(map[string]EndpointFiles)
for _, epFS := range epFSs {
if epFS.IsDir() {
epDir := s.endpointDir(contextName, epFS.Name())
epDir := s.endpointDir(contextID, epFS.Name())
fss, err := ioutil.ReadDir(epDir)
if err != nil {
return nil, err
@ -91,9 +91,9 @@ func (s *tlsStore) listContextData(contextName string) (map[string]EndpointFiles
// EndpointFiles is a slice of strings representing file names
type EndpointFiles []string
func convertTLSDataDoesNotExist(context, endpoint, file string, err error) error {
func convertTLSDataDoesNotExist(endpoint, file string, err error) error {
if os.IsNotExist(err) {
return &tlsDataDoesNotExistError{context: context, endpoint: endpoint, file: file}
return &tlsDataDoesNotExistError{endpoint: endpoint, file: file}
}
return err
}

View File

@ -27,6 +27,7 @@ A self-sufficient runtime for containers.
Options:
--config string Location of client config files (default "/root/.docker")
-c, --context string Name of the context to use to connect to the daemon (overrides DOCKER_HOST env var and default context set with "docker context use")
-D, --debug Enable debug mode
--help Print usage
-H, --host value Daemon socket(s) to connect to (default [])
@ -78,6 +79,7 @@ by the `docker` command line:
`docker pull`) in `docker help` output, and only `Management commands` per object-type (e.g., `docker container`) are
printed. This may become the default in a future release, at which point this environment-variable is removed.
* `DOCKER_TMPDIR` Location for temporary Docker files.
* `DOCKER_CONTEXT` Specify the context to use (overrides DOCKER_HOST env var and default context set with "docker context use")
Because Docker is developed using Go, you can also use any environment
variables used by the Go runtime. In particular, you may find these useful:

View File

@ -0,0 +1,75 @@
---
title: "context create"
description: "The context create command description and usage"
keywords: "context, create"
---
<!-- This file is maintained within the docker/cli GitHub
repository at https://github.com/docker/cli/. Make all
pull requests against that repo. If you see this file in
another repository, consider it read-only there, as it will
periodically be overwritten by the definitive file. Pull
requests which include edits to this file in other repositories
will be rejected.
-->
# context create
```markdown
Usage: docker context create [OPTIONS] CONTEXT
Create a context
Docker endpoint config:
NAME DESCRIPTION
from-current Copy current Docker endpoint configuration
host Docker endpoint on which to connect
ca Trust certs signed only by this CA
cert Path to TLS certificate file
key Path to TLS key file
skip-tls-verify Skip TLS certificate validation
Kubernetes endpoint config:
NAME DESCRIPTION
from-current Copy current Kubernetes endpoint configuration
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"
Options:
--default-stack-orchestrator string Default orchestrator for
stack operations to use with
this context
(swarm|kubernetes|all)
--description string Description of the context
--docker stringToString set the docker endpoint
(default [])
--kubernetes stringToString set the kubernetes endpoint
(default [])
```
## Description
Creates a new `context`. This will allow 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:
```bash
$ source my-setup-script.sh
$ docker context create my-context --docker "from-current=true"
```
Similarly, to reference the currently active Kubernetes configuration, you can use `--kubernetes "from-current=true"`:
```bash
$ export KUBECONFIG=/path/to/my/kubeconfig
$ docker context create my-context --kubernetes "from-current=true" --docker "host=/var/run/docker.sock"
```
Docker and Kubernetes endpoints configurations, as well as default stack orchestrator and description can be modified with `docker context update`

View File

@ -0,0 +1,31 @@
---
title: "context export"
description: "The context export command description and usage"
keywords: "context, export"
---
<!-- This file is maintained within the docker/cli GitHub
repository at https://github.com/docker/cli/. Make all
pull requests against that repo. If you see this file in
another repository, consider it read-only there, as it will
periodically be overwritten by the definitive file. Pull
requests which include edits to this file in other repositories
will be rejected.
-->
# context export
```markdown
Usage: docker context export [OPTIONS] CONTEXT [FILE|-]
Export a context to a tar or kubeconfig file
Options:
--kubeconfig Export as a kubeconfig file
```
## Description
Exports a context in a file that can then be used with `docker context import` (or with `kubectl` if `--kubeconfig` is set).
Default output filename is `<CONTEXT>.dockercontext`, or `<CONTEXT>.kubeconfig` if `--kubeconfig` is set.
To export to `STDOUT`, you can run `docker context export my-context -`.

View File

@ -0,0 +1,22 @@
---
title: "context import"
description: "The context import command description and usage"
keywords: "context, import"
---
<!-- This file is maintained within the docker/cli GitHub
repository at https://github.com/docker/cli/. Make all
pull requests against that repo. If you see this file in
another repository, consider it read-only there, as it will
periodically be overwritten by the definitive file. Pull
requests which include edits to this file in other repositories
will be rejected.
-->
# context import
```markdown
Usage: docker context import [OPTIONS] CONTEXT FILE|-
Import a context from a tar file
```

View File

@ -0,0 +1,30 @@
---
title: "context ls"
description: "The context ls command description and usage"
keywords: "context, ls"
---
<!-- This file is maintained within the docker/cli GitHub
repository at https://github.com/docker/cli/. Make all
pull requests against that repo. If you see this file in
another repository, consider it read-only there, as it will
periodically be overwritten by the definitive file. Pull
requests which include edits to this file in other repositories
will be rejected.
-->
# context ls
```markdown
Usage: docker context ls [OPTIONS]
List contexts
Aliases:
ls, list
Options:
--format string Pretty-print contexts using a Go template
(default "table")
-q, --quiet Only show context names
```

View File

@ -0,0 +1,28 @@
---
title: "context rm"
description: "The context rm command description and usage"
keywords: "context, rm"
---
<!-- This file is maintained within the docker/cli GitHub
repository at https://github.com/docker/cli/. Make all
pull requests against that repo. If you see this file in
another repository, consider it read-only there, as it will
periodically be overwritten by the definitive file. Pull
requests which include edits to this file in other repositories
will be rejected.
-->
# context rm
```markdown
Usage: docker context rm CONTEXT [CONTEXT...]
Remove one or more contexts
Aliases:
rm, remove
Options:
-f, --force Force the removal of a context in use
```

View File

@ -0,0 +1,60 @@
---
title: "context update"
description: "The context update command description and usage"
keywords: "context, update"
---
<!-- This file is maintained within the docker/cli GitHub
repository at https://github.com/docker/cli/. Make all
pull requests against that repo. If you see this file in
another repository, consider it read-only there, as it will
periodically be overwritten by the definitive file. Pull
requests which include edits to this file in other repositories
will be rejected.
-->
# context update
```markdown
Usage: docker context update [OPTIONS] CONTEXT
Update a context
Docker endpoint config:
NAME DESCRIPTION
from-current Copy current Docker endpoint configuration
host Docker endpoint on which to connect
ca Trust certs signed only by this CA
cert Path to TLS certificate file
key Path to TLS key file
skip-tls-verify Skip TLS certificate validation
Kubernetes endpoint config:
NAME DESCRIPTION
from-current Copy current Kubernetes endpoint configuration
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 update 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
stack operations to use with
this context
(swarm|kubernetes|all)
--description string Description of the context
--docker stringToString set the docker endpoint
(default [])
--kubernetes stringToString set the kubernetes endpoint
(default [])
```
## Description
Updates an existing `context`.
See [context create](context_create.md)

View File

@ -0,0 +1,25 @@
---
title: "context use"
description: "The context use command description and usage"
keywords: "context, use"
---
<!-- This file is maintained within the docker/cli GitHub
repository at https://github.com/docker/cli/. Make all
pull requests against that repo. If you see this file in
another repository, consider it read-only there, as it will
periodically be overwritten by the definitive file. Pull
requests which include edits to this file in other repositories
will be rejected.
-->
# context use
```markdown
Usage: docker context use CONTEXT
Set the current docker context
```
## Description
Set the default context to use, when `DOCKER_HOST`, `DOCKER_CONTEXT` environment variables and `--host`, `--context` global options are not set.

View File

@ -182,3 +182,15 @@ read the [`dockerd`](dockerd.md) reference page.
| [plugin push](plugin_push.md) | Push a plugin to a registry |
| [plugin rm](plugin_rm.md) | Remove a plugin |
| [plugin set](plugin_set.md) | Change settings for a plugin |
### Context commands
| Command | Description |
|:--------|:-------------------------------------------------------------------|
| [context create](context_create.md) | Create a context |
| [context export](context_export.md) | Export a context |
| [context import](context_import.md) | Import a context |
| [context ls](context_ls.md) | List contexts |
| [context rm](context_rm.md) | Remove one or more contexts |
| [context update](context_update.md) | Update a context |
| [context use](context_use.md) | Set the current docker context |

View File

@ -9,6 +9,8 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/context/docker"
"github.com/docker/cli/cli/context/store"
manifeststore "github.com/docker/cli/cli/manifest/store"
registryclient "github.com/docker/cli/cli/registry/client"
"github.com/docker/cli/cli/trust"
@ -38,6 +40,9 @@ type FakeCli struct {
registryClient registryclient.RegistryClient
contentTrust bool
containerizedEngineClientFunc containerizedEngineFuncType
contextStore store.Store
currentContext string
dockerEndpoint docker.Endpoint
}
// NewFakeCli returns a fake for the command.Cli interface
@ -70,11 +75,31 @@ func (c *FakeCli) SetErr(err *bytes.Buffer) {
c.err = err
}
// SetOut sets the stdout stream for the cli to the specified io.Writer
func (c *FakeCli) SetOut(out *command.OutStream) {
c.out = out
}
// SetConfigFile sets the "fake" config file
func (c *FakeCli) SetConfigFile(configfile *configfile.ConfigFile) {
c.configfile = configfile
}
// SetContextStore sets the "fake" context store
func (c *FakeCli) SetContextStore(store store.Store) {
c.contextStore = store
}
// SetCurrentContext sets the "fake" current context
func (c *FakeCli) SetCurrentContext(name string) {
c.currentContext = name
}
// SetDockerEndpoint sets the "fake" docker endpoint
func (c *FakeCli) SetDockerEndpoint(ep docker.Endpoint) {
c.dockerEndpoint = ep
}
// Client returns a docker API client
func (c *FakeCli) Client() client.APIClient {
return c.client
@ -100,6 +125,21 @@ func (c *FakeCli) ConfigFile() *configfile.ConfigFile {
return c.configfile
}
// ContextStore returns the cli context store
func (c *FakeCli) ContextStore() store.Store {
return c.contextStore
}
// CurrentContext returns the cli context
func (c *FakeCli) CurrentContext() string {
return c.currentContext
}
// DockerEndpoint returns the current DockerEndpoint
func (c *FakeCli) DockerEndpoint() docker.Endpoint {
return c.dockerEndpoint
}
// ServerInfo returns API server information for the server used by this client
func (c *FakeCli) ServerInfo() command.ServerInfo {
return c.server