diff --git a/cli/command/cli.go b/cli/command/cli.go index 161163e00b..186854f866 100644 --- a/cli/command/cli.go +++ b/cli/command/cli.go @@ -14,7 +14,6 @@ import ( "github.com/docker/cli/cli/config/configfile" dcontext "github.com/docker/cli/cli/context" "github.com/docker/cli/cli/context/docker" - kubecontext "github.com/docker/cli/cli/context/kubernetes" "github.com/docker/cli/cli/context/store" "github.com/docker/cli/cli/debug" cliflags "github.com/docker/cli/cli/flags" @@ -210,11 +209,11 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...Initialize cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err) - baseContextSore := store.New(cliconfig.ContextStoreDir(), cli.contextStoreConfig) + baseContextStore := store.New(cliconfig.ContextStoreDir(), cli.contextStoreConfig) cli.contextStore = &ContextStoreWithDefault{ - Store: baseContextSore, + Store: baseContextStore, Resolver: func() (*DefaultContext, error) { - return resolveDefaultContext(opts.Common, cli.ConfigFile(), cli.Err()) + return ResolveDefaultContext(opts.Common, cli.ConfigFile(), cli.contextStoreConfig, cli.Err()) }, } cli.currentContext, err = resolveContextName(opts.Common, cli.configFile, cli.contextStore) @@ -259,10 +258,11 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...Initialize // NewAPIClientFromFlags creates a new APIClient from command line flags func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) { + storeConfig := DefaultContextStoreConfig() store := &ContextStoreWithDefault{ - Store: store.New(cliconfig.ContextStoreDir(), defaultContextStoreConfig()), + Store: store.New(cliconfig.ContextStoreDir(), storeConfig), Resolver: func() (*DefaultContext, error) { - return resolveDefaultContext(opts, configFile, ioutil.Discard) + return ResolveDefaultContext(opts, configFile, storeConfig, ioutil.Discard) }, } contextName, err := resolveContextName(opts, configFile, store) @@ -453,7 +453,7 @@ func NewDockerCli(ops ...DockerCliOption) (*DockerCli, error) { WithContentTrustFromEnv(), WithContainerizedClient(containerizedengine.NewClient), } - cli.contextStoreConfig = defaultContextStoreConfig() + cli.contextStoreConfig = DefaultContextStoreConfig() ops = append(defaultOps, ops...) if err := cli.Apply(ops...); err != nil { return nil, err @@ -526,10 +526,22 @@ func resolveContextName(opts *cliflags.CommonOptions, config *configfile.ConfigF return DefaultContextName, nil } -func defaultContextStoreConfig() store.Config { +var defaultStoreEndpoints = []store.NamedTypeGetter{ + store.EndpointTypeGetter(docker.DockerEndpoint, func() interface{} { return &docker.EndpointMeta{} }), +} + +// RegisterDefaultStoreEndpoints registers a new named endpoint +// metadata type with the default context store config, so that +// endpoint will be supported by stores using the config returned by +// DefaultContextStoreConfig. +func RegisterDefaultStoreEndpoints(ep ...store.NamedTypeGetter) { + defaultStoreEndpoints = append(defaultStoreEndpoints, ep...) +} + +// DefaultContextStoreConfig returns a new store.Config with the default set of endpoints configured. +func DefaultContextStoreConfig() store.Config { return store.NewConfig( func() interface{} { return &DockerContext{} }, - store.EndpointTypeGetter(docker.DockerEndpoint, func() interface{} { return &docker.EndpointMeta{} }), - store.EndpointTypeGetter(kubecontext.KubernetesEndpoint, func() interface{} { return &kubecontext.EndpointMeta{} }), + defaultStoreEndpoints..., ) } diff --git a/cli/command/cli_options.go b/cli/command/cli_options.go index 4f48ca4ef2..607cd220a3 100644 --- a/cli/command/cli_options.go +++ b/cli/command/cli_options.go @@ -7,7 +7,6 @@ import ( "strconv" "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/cli/streams" clitypes "github.com/docker/cli/types" @@ -97,7 +96,7 @@ func WithContainerizedClient(containerizedFn func(string) (clitypes.Containerize func WithContextEndpointType(endpointName string, endpointType store.TypeGetter) DockerCliOption { return func(cli *DockerCli) error { switch endpointName { - case docker.DockerEndpoint, kubernetes.KubernetesEndpoint: + case docker.DockerEndpoint: return fmt.Errorf("cannot change %q endpoint type", endpointName) } cli.contextStoreConfig.SetEndpoint(endpointName, endpointType) diff --git a/cli/command/defaultcontextstore.go b/cli/command/defaultcontextstore.go index 9da76df75d..3140dc5030 100644 --- a/cli/command/defaultcontextstore.go +++ b/cli/command/defaultcontextstore.go @@ -3,15 +3,11 @@ package command import ( "fmt" "io" - "os" - "path/filepath" "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" cliflags "github.com/docker/cli/cli/flags" - "github.com/docker/docker/pkg/homedir" "github.com/pkg/errors" ) @@ -20,7 +16,7 @@ const ( DefaultContextName = "default" ) -// DefaultContext contains the default context data for all enpoints +// DefaultContext contains the default context data for all endpoints type DefaultContext struct { Meta store.Metadata TLS store.ContextTLSData @@ -35,8 +31,21 @@ type ContextStoreWithDefault struct { Resolver DefaultContextResolver } -// resolveDefaultContext creates a Metadata for the current CLI invocation parameters -func resolveDefaultContext(opts *cliflags.CommonOptions, config *configfile.ConfigFile, stderr io.Writer) (*DefaultContext, error) { +// EndpointDefaultResolver is implemented by any EndpointMeta object +// which wants to be able to populate the store with whatever their default is. +type EndpointDefaultResolver interface { + // ResolveDefault returns values suitable for storing in store.Metadata.Endpoints + // and store.ContextTLSData.Endpoints. + // + // An error is only returned for something fatal, not simply + // the lack of a default (e.g. because the config file which + // would contain it is missing). If there is no default then + // returns nil, nil, nil. + ResolveDefault(Orchestrator) (interface{}, *store.EndpointTLSData, error) +} + +// ResolveDefaultContext creates a Metadata for the current CLI invocation parameters +func ResolveDefaultContext(opts *cliflags.CommonOptions, config *configfile.ConfigFile, storeconfig store.Config, stderr io.Writer) (*DefaultContext, error) { stackOrchestrator, err := GetStackOrchestrator("", "", config.StackOrchestrator, stderr) if err != nil { return nil, err @@ -62,20 +71,28 @@ func resolveDefaultContext(opts *cliflags.CommonOptions, config *configfile.Conf contextTLSData.Endpoints[docker.DockerEndpoint] = *dockerEP.TLSData.ToStoreTLSData() } - // Default context uses env-based kubeconfig for Kubernetes endpoint configuration - kubeconfig := os.Getenv("KUBECONFIG") - if kubeconfig == "" { - kubeconfig = filepath.Join(homedir.Get(), ".kube/config") - } - kubeEP, err := kubernetes.FromKubeConfig(kubeconfig, "", "") - if (stackOrchestrator == OrchestratorKubernetes || stackOrchestrator == OrchestratorAll) && err != nil { - return nil, errors.Wrapf(err, "default orchestrator is %s but kubernetes endpoint could not be found", stackOrchestrator) - } - if err == nil { - contextMetadata.Endpoints[kubernetes.KubernetesEndpoint] = kubeEP.EndpointMeta - if kubeEP.TLSData != nil { - contextTLSData.Endpoints[kubernetes.KubernetesEndpoint] = *kubeEP.TLSData.ToStoreTLSData() + if err := storeconfig.ForeachEndpointType(func(n string, get store.TypeGetter) error { + if n == docker.DockerEndpoint { // handled above + return nil } + ep := get() + if i, ok := ep.(EndpointDefaultResolver); ok { + meta, tls, err := i.ResolveDefault(stackOrchestrator) + if err != nil { + return err + } + if meta == nil { + return nil + } + contextMetadata.Endpoints[n] = meta + if tls != nil { + contextTLSData.Endpoints[n] = *tls + } + } + // Nothing to be done + return nil + }); err != nil { + return nil, err } return &DefaultContext{Meta: contextMetadata, TLS: contextTLSData}, nil diff --git a/cli/command/defaultcontextstore_test.go b/cli/command/defaultcontextstore_test.go index 7acdfd3dbc..e1c7c1df9b 100644 --- a/cli/command/defaultcontextstore_test.go +++ b/cli/command/defaultcontextstore_test.go @@ -8,7 +8,6 @@ import ( "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" cliflags "github.com/docker/cli/cli/flags" "github.com/docker/go-connections/tlsconfig" @@ -63,22 +62,20 @@ func TestDefaultContextInitializer(t *testing.T) { cli, err := NewDockerCli() assert.NilError(t, err) defer env.Patch(t, "DOCKER_HOST", "ssh://someswarmserver")() - defer env.Patch(t, "KUBECONFIG", "./testdata/test-kubeconfig")() cli.configFile = &configfile.ConfigFile{ - StackOrchestrator: "all", + StackOrchestrator: "swarm", } - ctx, err := resolveDefaultContext(&cliflags.CommonOptions{ + ctx, err := ResolveDefaultContext(&cliflags.CommonOptions{ TLS: true, TLSOptions: &tlsconfig.Options{ CAFile: "./testdata/ca.pem", }, - }, cli.ConfigFile(), cli.Err()) + }, cli.ConfigFile(), DefaultContextStoreConfig(), cli.Err()) assert.NilError(t, err) assert.Equal(t, "default", ctx.Meta.Name) - assert.Equal(t, OrchestratorAll, ctx.Meta.Metadata.(DockerContext).StackOrchestrator) + assert.Equal(t, OrchestratorSwarm, ctx.Meta.Metadata.(DockerContext).StackOrchestrator) assert.DeepEqual(t, "ssh://someswarmserver", ctx.Meta.Endpoints[docker.DockerEndpoint].(docker.EndpointMeta).Host) golden.Assert(t, string(ctx.TLS.Endpoints[docker.DockerEndpoint].Files["ca.pem"]), "ca.pem") - assert.DeepEqual(t, "zoinx", ctx.Meta.Endpoints[kubernetes.KubernetesEndpoint].(kubernetes.EndpointMeta).DefaultNamespace) } func TestExportDefaultImport(t *testing.T) { diff --git a/cli/context/kubernetes/load.go b/cli/context/kubernetes/load.go index c218d94dee..113ec1ad78 100644 --- a/cli/context/kubernetes/load.go +++ b/cli/context/kubernetes/load.go @@ -1,9 +1,15 @@ package kubernetes import ( + "os" + "path/filepath" + + "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/context" "github.com/docker/cli/cli/context/store" api "github.com/docker/compose-on-kubernetes/api" + "github.com/docker/docker/pkg/homedir" + "github.com/pkg/errors" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" ) @@ -17,6 +23,8 @@ type EndpointMeta struct { Exec *clientcmdapi.ExecConfig `json:",omitempty"` } +var _ command.EndpointDefaultResolver = &EndpointMeta{} + // Endpoint is a typed wrapper around a context-store generic endpoint describing // a Kubernetes endpoint, with TLS data type Endpoint struct { @@ -24,6 +32,12 @@ type Endpoint struct { TLSData *context.TLSData } +func init() { + command.RegisterDefaultStoreEndpoints( + store.EndpointTypeGetter(KubernetesEndpoint, func() interface{} { return &EndpointMeta{} }), + ) +} + // WithTLSData loads TLS materials for the endpoint func (c *EndpointMeta) WithTLSData(s store.Reader, contextName string) (Endpoint, error) { tlsData, err := context.LoadTLSData(s, contextName, KubernetesEndpoint) @@ -61,6 +75,32 @@ func (c *Endpoint) KubernetesConfig() clientcmd.ClientConfig { return clientcmd.NewDefaultClientConfig(*cfg, &clientcmd.ConfigOverrides{}) } +// ResolveDefault returns endpoint metadata for the default Kubernetes +// endpoint, which is derived from the env-based kubeconfig. +func (c *EndpointMeta) ResolveDefault(stackOrchestrator command.Orchestrator) (interface{}, *store.EndpointTLSData, error) { + kubeconfig := os.Getenv("KUBECONFIG") + if kubeconfig == "" { + kubeconfig = filepath.Join(homedir.Get(), ".kube/config") + } + kubeEP, err := FromKubeConfig(kubeconfig, "", "") + if err != nil { + if stackOrchestrator == command.OrchestratorKubernetes || stackOrchestrator == command.OrchestratorAll { + return nil, nil, errors.Wrapf(err, "default orchestrator is %s but unable to resolve kubernetes endpoint", stackOrchestrator) + } + + // We deliberately quash the error here, returning nil + // for the first argument is sufficient to indicate we weren't able to + // provide a default + return nil, nil, nil + } + + var tls *store.EndpointTLSData + if kubeEP.TLSData != nil { + tls = kubeEP.TLSData.ToStoreTLSData() + } + return kubeEP.EndpointMeta, tls, nil +} + // EndpointFromContext extracts kubernetes endpoint info from current context func EndpointFromContext(metadata store.Metadata) *EndpointMeta { ep, ok := metadata.Endpoints[KubernetesEndpoint] diff --git a/cli/context/kubernetes/load_test.go b/cli/context/kubernetes/load_test.go new file mode 100644 index 0000000000..203f5dbb9a --- /dev/null +++ b/cli/context/kubernetes/load_test.go @@ -0,0 +1,25 @@ +package kubernetes + +import ( + "testing" + + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/config/configfile" + cliflags "github.com/docker/cli/cli/flags" + "gotest.tools/assert" + "gotest.tools/env" +) + +func TestDefaultContextInitializer(t *testing.T) { + cli, err := command.NewDockerCli() + assert.NilError(t, err) + defer env.Patch(t, "KUBECONFIG", "./testdata/test-kubeconfig")() + configFile := &configfile.ConfigFile{ + StackOrchestrator: "all", + } + ctx, err := command.ResolveDefaultContext(&cliflags.CommonOptions{}, configFile, command.DefaultContextStoreConfig(), cli.Err()) + assert.NilError(t, err) + assert.Equal(t, "default", ctx.Meta.Name) + assert.Equal(t, command.OrchestratorAll, ctx.Meta.Metadata.(command.DockerContext).StackOrchestrator) + assert.DeepEqual(t, "zoinx", ctx.Meta.Endpoints[KubernetesEndpoint].(EndpointMeta).DefaultNamespace) +} diff --git a/cli/command/testdata/test-kubeconfig b/cli/context/kubernetes/testdata/test-kubeconfig similarity index 100% rename from cli/command/testdata/test-kubeconfig rename to cli/context/kubernetes/testdata/test-kubeconfig diff --git a/cli/context/store/storeconfig.go b/cli/context/store/storeconfig.go index b282a9d10e..2a4bc57c1c 100644 --- a/cli/context/store/storeconfig.go +++ b/cli/context/store/storeconfig.go @@ -30,6 +30,16 @@ func (c Config) SetEndpoint(name string, getter TypeGetter) { c.endpointTypes[name] = getter } +// ForeachEndpointType calls cb on every endpoint type registered with the Config +func (c Config) ForeachEndpointType(cb func(string, TypeGetter) error) error { + for n, ep := range c.endpointTypes { + if err := cb(n, ep); err != nil { + return err + } + } + return nil +} + // NewConfig creates a config object func NewConfig(contextType TypeGetter, endpoints ...NamedTypeGetter) Config { res := Config{ diff --git a/e2e/context/context_test.go b/e2e/context/context_test.go new file mode 100644 index 0000000000..b427217116 --- /dev/null +++ b/e2e/context/context_test.go @@ -0,0 +1,21 @@ +package context + +import ( + "testing" + + "gotest.tools/golden" + "gotest.tools/icmd" +) + +func TestContextList(t *testing.T) { + cmd := icmd.Command("docker", "context", "ls") + cmd.Env = append(cmd.Env, + "DOCKER_CONFIG=./testdata/test-dockerconfig", + "KUBECONFIG=./testdata/test-kubeconfig", + ) + result := icmd.RunCmd(cmd).Assert(t, icmd.Expected{ + Err: icmd.None, + ExitCode: 0, + }) + golden.Assert(t, result.Stdout(), "context-ls.golden") +} diff --git a/e2e/context/main_test.go b/e2e/context/main_test.go new file mode 100644 index 0000000000..a3b1c53016 --- /dev/null +++ b/e2e/context/main_test.go @@ -0,0 +1,17 @@ +package context + +import ( + "fmt" + "os" + "testing" + + "github.com/docker/cli/internal/test/environment" +) + +func TestMain(m *testing.M) { + if err := environment.Setup(); err != nil { + fmt.Println(err.Error()) + os.Exit(3) + } + os.Exit(m.Run()) +} diff --git a/e2e/context/testdata/context-ls.golden b/e2e/context/testdata/context-ls.golden new file mode 100644 index 0000000000..2e2569ce10 --- /dev/null +++ b/e2e/context/testdata/context-ls.golden @@ -0,0 +1,3 @@ +NAME DESCRIPTION DOCKER ENDPOINT KUBERNETES ENDPOINT ORCHESTRATOR +default * Current DOCKER_HOST based configuration unix:///var/run/docker.sock https://someserver (zoinx) swarm +remote my remote cluster ssh://someserver https://someserver (default) kubernetes diff --git a/e2e/context/testdata/test-dockerconfig/config.json b/e2e/context/testdata/test-dockerconfig/config.json new file mode 100644 index 0000000000..832b5c10b6 --- /dev/null +++ b/e2e/context/testdata/test-dockerconfig/config.json @@ -0,0 +1,7 @@ +{ + "auths": {}, + "HttpHeaders": { + "User-Agent": "Docker-Client/19.09.0-dev (linux)" + }, + "credsStore": "secretservice" +} \ No newline at end of file diff --git a/e2e/context/testdata/test-dockerconfig/contexts/meta/b71199ebd070b36beab7317920c2c2f1d777df8d05e5527d8458fda57cb17a7a/meta.json b/e2e/context/testdata/test-dockerconfig/contexts/meta/b71199ebd070b36beab7317920c2c2f1d777df8d05e5527d8458fda57cb17a7a/meta.json new file mode 100644 index 0000000000..11c9b4126e --- /dev/null +++ b/e2e/context/testdata/test-dockerconfig/contexts/meta/b71199ebd070b36beab7317920c2c2f1d777df8d05e5527d8458fda57cb17a7a/meta.json @@ -0,0 +1 @@ +{"Name":"remote","Metadata":{"Description":"my remote cluster","StackOrchestrator":"kubernetes"},"Endpoints":{"docker":{"Host":"ssh://someserver","SkipTLSVerify":false},"kubernetes":{"Host":"https://someserver","SkipTLSVerify":false,"DefaultNamespace":"default","Exec":{"command":"heptio-authenticator-aws","args":["token","-i","eks-cf"],"env":null,"apiVersion":"client.authentication.k8s.io/v1alpha1"}}}} diff --git a/e2e/context/testdata/test-kubeconfig b/e2e/context/testdata/test-kubeconfig new file mode 100644 index 0000000000..e96df74a50 --- /dev/null +++ b/e2e/context/testdata/test-kubeconfig @@ -0,0 +1,20 @@ +apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: dGhlLWNh + server: https://someserver + name: test-cluster +contexts: +- context: + cluster: test-cluster + user: test-user + namespace: zoinx + name: test +current-context: test +kind: Config +preferences: {} +users: +- name: test-user + user: + client-certificate-data: dGhlLWNlcnQ= + client-key-data: dGhlLWtleQ==