Add context store config options and expose context commands

This will allow plugins to have custom typed endpoints, as well as
create/remove/update contexts with the exact same results as the main
CLI (thinking of things like `docker ee login https://my-ucp-server
--context ucp-prod)`

Signed-off-by: Simon Ferquel <simon.ferquel@docker.com>
This commit is contained in:
Simon Ferquel 2019-01-21 09:37:20 +01:00
parent cf6c238660
commit 3126920af1
16 changed files with 273 additions and 203 deletions

View File

@ -81,14 +81,9 @@ type DockerCli struct {
contextStore store.Store contextStore store.Store
currentContext string currentContext string
dockerEndpoint docker.Endpoint dockerEndpoint docker.Endpoint
contextStoreConfig store.Config
} }
var storeConfig = store.NewConfig(
func() interface{} { return &DockerContext{} },
store.EndpointTypeGetter(docker.DockerEndpoint, func() interface{} { return &docker.EndpointMeta{} }),
store.EndpointTypeGetter(kubcontext.KubernetesEndpoint, func() interface{} { return &kubcontext.EndpointMeta{} }),
)
// DefaultVersion returns api.defaultVersion or DOCKER_API_VERSION if specified. // DefaultVersion returns api.defaultVersion or DOCKER_API_VERSION if specified.
func (cli *DockerCli) DefaultVersion() string { func (cli *DockerCli) DefaultVersion() string {
return cli.clientInfo.DefaultVersion return cli.clientInfo.DefaultVersion
@ -184,7 +179,7 @@ func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.Registry
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error { func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err) cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err)
var err error var err error
cli.contextStore = store.New(cliconfig.ContextStoreDir(), storeConfig) cli.contextStore = store.New(cliconfig.ContextStoreDir(), cli.contextStoreConfig)
cli.currentContext, err = resolveContextName(opts.Common, cli.configFile, cli.contextStore) cli.currentContext, err = resolveContextName(opts.Common, cli.configFile, cli.contextStore)
if err != nil { if err != nil {
return err return err
@ -226,7 +221,7 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
// NewAPIClientFromFlags creates a new APIClient from command line flags // NewAPIClientFromFlags creates a new APIClient from command line flags
func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) { func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
store := store.New(cliconfig.ContextStoreDir(), storeConfig) store := store.New(cliconfig.ContextStoreDir(), defaultContextStoreConfig())
contextName, err := resolveContextName(opts, configFile, store) contextName, err := resolveContextName(opts, configFile, store)
if err != nil { if err != nil {
return nil, err return nil, err
@ -372,7 +367,7 @@ func (cli *DockerCli) StackOrchestrator(flagValue string) (Orchestrator, error)
if currentContext != "" { if currentContext != "" {
contextstore := cli.contextStore contextstore := cli.contextStore
if contextstore == nil { if contextstore == nil {
contextstore = store.New(cliconfig.ContextStoreDir(), storeConfig) contextstore = store.New(cliconfig.ContextStoreDir(), cli.contextStoreConfig)
} }
ctxRaw, err := contextstore.GetContextMetadata(currentContext) ctxRaw, err := contextstore.GetContextMetadata(currentContext)
if store.IsErrContextDoesNotExist(err) { if store.IsErrContextDoesNotExist(err) {
@ -430,6 +425,7 @@ func NewDockerCli(ops ...DockerCliOption) (*DockerCli, error) {
WithContentTrustFromEnv(), WithContentTrustFromEnv(),
WithContainerizedClient(containerizedengine.NewClient), WithContainerizedClient(containerizedengine.NewClient),
} }
cli.contextStoreConfig = defaultContextStoreConfig()
ops = append(defaultOps, ops...) ops = append(defaultOps, ops...)
if err := cli.Apply(ops...); err != nil { if err := cli.Apply(ops...); err != nil {
return nil, err return nil, err
@ -501,3 +497,11 @@ func resolveContextName(opts *cliflags.CommonOptions, config *configfile.ConfigF
} }
return "", nil return "", nil
} }
func defaultContextStoreConfig() store.Config {
return store.NewConfig(
func() interface{} { return &DockerContext{} },
store.EndpointTypeGetter(docker.DockerEndpoint, func() interface{} { return &docker.EndpointMeta{} }),
store.EndpointTypeGetter(kubcontext.KubernetesEndpoint, func() interface{} { return &kubcontext.EndpointMeta{} }),
)
}

View File

@ -1,10 +1,14 @@
package command package command
import ( import (
"fmt"
"io" "io"
"os" "os"
"strconv" "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" "github.com/docker/cli/cli/streams"
clitypes "github.com/docker/cli/types" clitypes "github.com/docker/cli/types"
"github.com/docker/docker/pkg/term" "github.com/docker/docker/pkg/term"
@ -87,3 +91,16 @@ func WithContainerizedClient(containerizedFn func(string) (clitypes.Containerize
return nil return nil
} }
} }
// WithContextEndpointType add support for an additional typed endpoint in the context store
// Plugins should use this to store additional endpoints configuration in the context store
func WithContextEndpointType(endpointName string, endpointType store.TypeGetter) DockerCliOption {
return func(cli *DockerCli) error {
switch endpointName {
case docker.DockerEndpoint, kubernetes.KubernetesEndpoint:
return fmt.Errorf("cannot change %q endpoint type", endpointName)
}
cli.contextStoreConfig.SetEndpoint(endpointName, endpointType)
return nil
}
}

View File

@ -14,12 +14,13 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
type createOptions struct { // CreateOptions are the options used for creating a context
name string type CreateOptions struct {
description string Name string
defaultStackOrchestrator string Description string
docker map[string]string DefaultStackOrchestrator string
kubernetes map[string]string Docker map[string]string
Kubernetes map[string]string
} }
func longCreateDescription() string { func longCreateDescription() string {
@ -43,52 +44,53 @@ func longCreateDescription() string {
} }
func newCreateCommand(dockerCli command.Cli) *cobra.Command { func newCreateCommand(dockerCli command.Cli) *cobra.Command {
opts := &createOptions{} opts := &CreateOptions{}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "create [OPTIONS] CONTEXT", Use: "create [OPTIONS] CONTEXT",
Short: "Create a context", Short: "Create a context",
Args: cli.ExactArgs(1), Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
opts.name = args[0] opts.Name = args[0]
return runCreate(dockerCli, opts) return RunCreate(dockerCli, opts)
}, },
Long: longCreateDescription(), Long: longCreateDescription(),
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.StringVar(&opts.description, "description", "", "Description of the context") flags.StringVar(&opts.Description, "description", "", "Description of the context")
flags.StringVar( flags.StringVar(
&opts.defaultStackOrchestrator, &opts.DefaultStackOrchestrator,
"default-stack-orchestrator", "", "default-stack-orchestrator", "",
"Default orchestrator for stack operations to use with this context (swarm|kubernetes|all)") "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.Docker, "docker", nil, "set the docker endpoint")
flags.StringToStringVar(&opts.kubernetes, "kubernetes", nil, "set the kubernetes endpoint") flags.StringToStringVar(&opts.Kubernetes, "kubernetes", nil, "set the kubernetes endpoint")
return cmd return cmd
} }
func runCreate(cli command.Cli, o *createOptions) error { // RunCreate creates a Docker context
func RunCreate(cli command.Cli, o *CreateOptions) error {
s := cli.ContextStore() s := cli.ContextStore()
if err := checkContextNameForCreation(s, o.name); err != nil { if err := checkContextNameForCreation(s, o.Name); err != nil {
return err return err
} }
stackOrchestrator, err := command.NormalizeOrchestrator(o.defaultStackOrchestrator) stackOrchestrator, err := command.NormalizeOrchestrator(o.DefaultStackOrchestrator)
if err != nil { if err != nil {
return errors.Wrap(err, "unable to parse default-stack-orchestrator") return errors.Wrap(err, "unable to parse default-stack-orchestrator")
} }
contextMetadata := store.ContextMetadata{ contextMetadata := store.ContextMetadata{
Endpoints: make(map[string]interface{}), Endpoints: make(map[string]interface{}),
Metadata: command.DockerContext{ Metadata: command.DockerContext{
Description: o.description, Description: o.Description,
StackOrchestrator: stackOrchestrator, StackOrchestrator: stackOrchestrator,
}, },
Name: o.name, Name: o.Name,
} }
if o.docker == nil { if o.Docker == nil {
return errors.New("docker endpoint configuration is required") return errors.New("docker endpoint configuration is required")
} }
contextTLSData := store.ContextTLSData{ contextTLSData := store.ContextTLSData{
Endpoints: make(map[string]store.EndpointTLSData), Endpoints: make(map[string]store.EndpointTLSData),
} }
dockerEP, dockerTLS, err := getDockerEndpointMetadataAndTLS(cli, o.docker) dockerEP, dockerTLS, err := getDockerEndpointMetadataAndTLS(cli, o.Docker)
if err != nil { if err != nil {
return errors.Wrap(err, "unable to create docker endpoint config") return errors.Wrap(err, "unable to create docker endpoint config")
} }
@ -96,8 +98,8 @@ func runCreate(cli command.Cli, o *createOptions) error {
if dockerTLS != nil { if dockerTLS != nil {
contextTLSData.Endpoints[docker.DockerEndpoint] = *dockerTLS contextTLSData.Endpoints[docker.DockerEndpoint] = *dockerTLS
} }
if o.kubernetes != nil { if o.Kubernetes != nil {
kubernetesEP, kubernetesTLS, err := getKubernetesEndpointMetadataAndTLS(cli, o.kubernetes) kubernetesEP, kubernetesTLS, err := getKubernetesEndpointMetadataAndTLS(cli, o.Kubernetes)
if err != nil { if err != nil {
return errors.Wrap(err, "unable to create kubernetes endpoint config") return errors.Wrap(err, "unable to create kubernetes endpoint config")
} }
@ -117,11 +119,11 @@ func runCreate(cli command.Cli, o *createOptions) error {
if err := s.CreateOrUpdateContext(contextMetadata); err != nil { if err := s.CreateOrUpdateContext(contextMetadata); err != nil {
return err return err
} }
if err := s.ResetContextTLSMaterial(o.name, &contextTLSData); err != nil { if err := s.ResetContextTLSMaterial(o.Name, &contextTLSData); err != nil {
return err return err
} }
fmt.Fprintln(cli.Out(), o.name) fmt.Fprintln(cli.Out(), o.Name)
fmt.Fprintf(cli.Err(), "Successfully created context %q\n", o.name) fmt.Fprintf(cli.Err(), "Successfully created context %q\n", o.Name)
return nil return nil
} }

View File

@ -46,68 +46,68 @@ func TestCreateInvalids(t *testing.T) {
defer cleanup() defer cleanup()
assert.NilError(t, cli.ContextStore().CreateOrUpdateContext(store.ContextMetadata{Name: "existing-context"})) assert.NilError(t, cli.ContextStore().CreateOrUpdateContext(store.ContextMetadata{Name: "existing-context"}))
tests := []struct { tests := []struct {
options createOptions options CreateOptions
expecterErr string expecterErr string
}{ }{
{ {
expecterErr: `context name cannot be empty`, expecterErr: `context name cannot be empty`,
}, },
{ {
options: createOptions{ options: CreateOptions{
name: " ", Name: " ",
}, },
expecterErr: `context name " " is invalid`, expecterErr: `context name " " is invalid`,
}, },
{ {
options: createOptions{ options: CreateOptions{
name: "existing-context", Name: "existing-context",
}, },
expecterErr: `context "existing-context" already exists`, expecterErr: `context "existing-context" already exists`,
}, },
{ {
options: createOptions{ options: CreateOptions{
name: "invalid-docker-host", Name: "invalid-docker-host",
docker: map[string]string{ Docker: map[string]string{
keyHost: "some///invalid/host", keyHost: "some///invalid/host",
}, },
}, },
expecterErr: `unable to parse docker host`, expecterErr: `unable to parse docker host`,
}, },
{ {
options: createOptions{ options: CreateOptions{
name: "invalid-orchestrator", Name: "invalid-orchestrator",
defaultStackOrchestrator: "invalid", DefaultStackOrchestrator: "invalid",
}, },
expecterErr: `specified orchestrator "invalid" is invalid, please use either kubernetes, swarm or all`, expecterErr: `specified orchestrator "invalid" is invalid, please use either kubernetes, swarm or all`,
}, },
{ {
options: createOptions{ options: CreateOptions{
name: "orchestrator-swarm-no-endpoint", Name: "orchestrator-swarm-no-endpoint",
defaultStackOrchestrator: "swarm", DefaultStackOrchestrator: "swarm",
}, },
expecterErr: `docker endpoint configuration is required`, expecterErr: `docker endpoint configuration is required`,
}, },
{ {
options: createOptions{ options: CreateOptions{
name: "orchestrator-kubernetes-no-endpoint", Name: "orchestrator-kubernetes-no-endpoint",
defaultStackOrchestrator: "kubernetes", DefaultStackOrchestrator: "kubernetes",
docker: map[string]string{}, Docker: map[string]string{},
}, },
expecterErr: `cannot specify orchestrator "kubernetes" without configuring a Kubernetes endpoint`, expecterErr: `cannot specify orchestrator "kubernetes" without configuring a Kubernetes endpoint`,
}, },
{ {
options: createOptions{ options: CreateOptions{
name: "orchestrator-all-no-endpoint", Name: "orchestrator-all-no-endpoint",
defaultStackOrchestrator: "all", DefaultStackOrchestrator: "all",
docker: map[string]string{}, Docker: map[string]string{},
}, },
expecterErr: `cannot specify orchestrator "all" without configuring a Kubernetes endpoint`, expecterErr: `cannot specify orchestrator "all" without configuring a Kubernetes endpoint`,
}, },
} }
for _, tc := range tests { for _, tc := range tests {
tc := tc tc := tc
t.Run(tc.options.name, func(t *testing.T) { t.Run(tc.options.Name, func(t *testing.T) {
err := runCreate(cli, &tc.options) err := RunCreate(cli, &tc.options)
assert.ErrorContains(t, err, tc.expecterErr) assert.ErrorContains(t, err, tc.expecterErr)
}) })
} }
@ -117,10 +117,10 @@ func TestCreateOrchestratorSwarm(t *testing.T) {
cli, cleanup := makeFakeCli(t) cli, cleanup := makeFakeCli(t)
defer cleanup() defer cleanup()
err := runCreate(cli, &createOptions{ err := RunCreate(cli, &CreateOptions{
name: "test", Name: "test",
defaultStackOrchestrator: "swarm", DefaultStackOrchestrator: "swarm",
docker: map[string]string{}, Docker: map[string]string{},
}) })
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, "test\n", cli.OutBuffer().String()) assert.Equal(t, "test\n", cli.OutBuffer().String())
@ -131,9 +131,9 @@ func TestCreateOrchestratorEmpty(t *testing.T) {
cli, cleanup := makeFakeCli(t) cli, cleanup := makeFakeCli(t)
defer cleanup() defer cleanup()
err := runCreate(cli, &createOptions{ err := RunCreate(cli, &CreateOptions{
name: "test", Name: "test",
docker: map[string]string{}, Docker: map[string]string{},
}) })
assert.NilError(t, err) assert.NilError(t, err)
} }
@ -156,13 +156,13 @@ func createTestContextWithKube(t *testing.T, cli command.Cli) {
revert := env.Patch(t, "KUBECONFIG", "./testdata/test-kubeconfig") revert := env.Patch(t, "KUBECONFIG", "./testdata/test-kubeconfig")
defer revert() defer revert()
err := runCreate(cli, &createOptions{ err := RunCreate(cli, &CreateOptions{
name: "test", Name: "test",
defaultStackOrchestrator: "all", DefaultStackOrchestrator: "all",
kubernetes: map[string]string{ Kubernetes: map[string]string{
keyFromCurrent: "true", keyFromCurrent: "true",
}, },
docker: map[string]string{}, Docker: map[string]string{},
}) })
assert.NilError(t, err) assert.NilError(t, err)
} }

View File

@ -21,14 +21,14 @@ func TestExportImportWithFile(t *testing.T) {
defer cleanup() defer cleanup()
createTestContextWithKube(t, cli) createTestContextWithKube(t, cli)
cli.ErrBuffer().Reset() cli.ErrBuffer().Reset()
assert.NilError(t, runExport(cli, &exportOptions{ assert.NilError(t, RunExport(cli, &ExportOptions{
contextName: "test", ContextName: "test",
dest: contextFile, Dest: contextFile,
})) }))
assert.Equal(t, cli.ErrBuffer().String(), fmt.Sprintf("Written file %q\n", contextFile)) assert.Equal(t, cli.ErrBuffer().String(), fmt.Sprintf("Written file %q\n", contextFile))
cli.OutBuffer().Reset() cli.OutBuffer().Reset()
cli.ErrBuffer().Reset() cli.ErrBuffer().Reset()
assert.NilError(t, runImport(cli, "test2", contextFile)) assert.NilError(t, RunImport(cli, "test2", contextFile))
context1, err := cli.ContextStore().GetContextMetadata("test") context1, err := cli.ContextStore().GetContextMetadata("test")
assert.NilError(t, err) assert.NilError(t, err)
context2, err := cli.ContextStore().GetContextMetadata("test2") context2, err := cli.ContextStore().GetContextMetadata("test2")
@ -48,15 +48,15 @@ func TestExportImportPipe(t *testing.T) {
createTestContextWithKube(t, cli) createTestContextWithKube(t, cli)
cli.ErrBuffer().Reset() cli.ErrBuffer().Reset()
cli.OutBuffer().Reset() cli.OutBuffer().Reset()
assert.NilError(t, runExport(cli, &exportOptions{ assert.NilError(t, RunExport(cli, &ExportOptions{
contextName: "test", ContextName: "test",
dest: "-", Dest: "-",
})) }))
assert.Equal(t, cli.ErrBuffer().String(), "") assert.Equal(t, cli.ErrBuffer().String(), "")
cli.SetIn(streams.NewIn(ioutil.NopCloser(bytes.NewBuffer(cli.OutBuffer().Bytes())))) cli.SetIn(streams.NewIn(ioutil.NopCloser(bytes.NewBuffer(cli.OutBuffer().Bytes()))))
cli.OutBuffer().Reset() cli.OutBuffer().Reset()
cli.ErrBuffer().Reset() cli.ErrBuffer().Reset()
assert.NilError(t, runImport(cli, "test2", "-")) assert.NilError(t, RunImport(cli, "test2", "-"))
context1, err := cli.ContextStore().GetContextMetadata("test") context1, err := cli.ContextStore().GetContextMetadata("test")
assert.NilError(t, err) assert.NilError(t, err)
context2, err := cli.ContextStore().GetContextMetadata("test2") context2, err := cli.ContextStore().GetContextMetadata("test2")
@ -79,18 +79,18 @@ func TestExportKubeconfig(t *testing.T) {
defer cleanup() defer cleanup()
createTestContextWithKube(t, cli) createTestContextWithKube(t, cli)
cli.ErrBuffer().Reset() cli.ErrBuffer().Reset()
assert.NilError(t, runExport(cli, &exportOptions{ assert.NilError(t, RunExport(cli, &ExportOptions{
contextName: "test", ContextName: "test",
dest: contextFile, Dest: contextFile,
kubeconfig: true, Kubeconfig: true,
})) }))
assert.Equal(t, cli.ErrBuffer().String(), fmt.Sprintf("Written file %q\n", contextFile)) assert.Equal(t, cli.ErrBuffer().String(), fmt.Sprintf("Written file %q\n", contextFile))
assert.NilError(t, runCreate(cli, &createOptions{ assert.NilError(t, RunCreate(cli, &CreateOptions{
name: "test2", Name: "test2",
kubernetes: map[string]string{ Kubernetes: map[string]string{
keyKubeconfig: contextFile, keyKubeconfig: contextFile,
}, },
docker: map[string]string{}, Docker: map[string]string{},
})) }))
validateTestKubeEndpoint(t, cli.ContextStore(), "test2") validateTestKubeEndpoint(t, cli.ContextStore(), "test2")
} }
@ -105,6 +105,6 @@ func TestExportExistingFile(t *testing.T) {
createTestContextWithKube(t, cli) createTestContextWithKube(t, cli)
cli.ErrBuffer().Reset() cli.ErrBuffer().Reset()
assert.NilError(t, ioutil.WriteFile(contextFile, []byte{}, 0644)) assert.NilError(t, ioutil.WriteFile(contextFile, []byte{}, 0644))
err = runExport(cli, &exportOptions{contextName: "test", dest: contextFile}) err = RunExport(cli, &ExportOptions{ContextName: "test", Dest: contextFile})
assert.Assert(t, os.IsExist(err)) assert.Assert(t, os.IsExist(err))
} }

View File

@ -15,36 +15,37 @@ import (
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
) )
type exportOptions struct { // ExportOptions are the options used for exporting a context
kubeconfig bool type ExportOptions struct {
contextName string Kubeconfig bool
dest string ContextName string
Dest string
} }
func newExportCommand(dockerCli command.Cli) *cobra.Command { func newExportCommand(dockerCli command.Cli) *cobra.Command {
opts := &exportOptions{} opts := &ExportOptions{}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "export [OPTIONS] CONTEXT [FILE|-]", Use: "export [OPTIONS] CONTEXT [FILE|-]",
Short: "Export a context to a tar or kubeconfig file", Short: "Export a context to a tar or kubeconfig file",
Args: cli.RequiresRangeArgs(1, 2), Args: cli.RequiresRangeArgs(1, 2),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
opts.contextName = args[0] opts.ContextName = args[0]
if len(args) == 2 { if len(args) == 2 {
opts.dest = args[1] opts.Dest = args[1]
} else { } else {
opts.dest = opts.contextName opts.Dest = opts.ContextName
if opts.kubeconfig { if opts.Kubeconfig {
opts.dest += ".kubeconfig" opts.Dest += ".kubeconfig"
} else { } else {
opts.dest += ".dockercontext" opts.Dest += ".dockercontext"
} }
} }
return runExport(dockerCli, opts) return RunExport(dockerCli, opts)
}, },
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.BoolVar(&opts.kubeconfig, "kubeconfig", false, "Export as a kubeconfig file") flags.BoolVar(&opts.Kubeconfig, "kubeconfig", false, "Export as a kubeconfig file")
return cmd return cmd
} }
@ -74,24 +75,25 @@ func writeTo(dockerCli command.Cli, reader io.Reader, dest string) error {
return nil return nil
} }
func runExport(dockerCli command.Cli, opts *exportOptions) error { // RunExport exports a Docker context
if err := validateContextName(opts.contextName); err != nil { func RunExport(dockerCli command.Cli, opts *ExportOptions) error {
if err := validateContextName(opts.ContextName); err != nil {
return err return err
} }
ctxMeta, err := dockerCli.ContextStore().GetContextMetadata(opts.contextName) ctxMeta, err := dockerCli.ContextStore().GetContextMetadata(opts.ContextName)
if err != nil { if err != nil {
return err return err
} }
if !opts.kubeconfig { if !opts.Kubeconfig {
reader := store.Export(opts.contextName, dockerCli.ContextStore()) reader := store.Export(opts.ContextName, dockerCli.ContextStore())
defer reader.Close() defer reader.Close()
return writeTo(dockerCli, reader, opts.dest) return writeTo(dockerCli, reader, opts.Dest)
} }
kubernetesEndpointMeta := kubernetes.EndpointFromContext(ctxMeta) kubernetesEndpointMeta := kubernetes.EndpointFromContext(ctxMeta)
if kubernetesEndpointMeta == nil { if kubernetesEndpointMeta == nil {
return fmt.Errorf("context %q has no kubernetes endpoint", opts.contextName) return fmt.Errorf("context %q has no kubernetes endpoint", opts.ContextName)
} }
kubernetesEndpoint, err := kubernetesEndpointMeta.WithTLSData(dockerCli.ContextStore(), opts.contextName) kubernetesEndpoint, err := kubernetesEndpointMeta.WithTLSData(dockerCli.ContextStore(), opts.ContextName)
if err != nil { if err != nil {
return err return err
} }
@ -104,5 +106,5 @@ func runExport(dockerCli command.Cli, opts *exportOptions) error {
if err != nil { if err != nil {
return err return err
} }
return writeTo(dockerCli, bytes.NewBuffer(data), opts.dest) return writeTo(dockerCli, bytes.NewBuffer(data), opts.Dest)
} }

View File

@ -17,13 +17,14 @@ func newImportCommand(dockerCli command.Cli) *cobra.Command {
Short: "Import a context from a tar file", Short: "Import a context from a tar file",
Args: cli.ExactArgs(2), Args: cli.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runImport(dockerCli, args[0], args[1]) return RunImport(dockerCli, args[0], args[1])
}, },
} }
return cmd return cmd
} }
func runImport(dockerCli command.Cli, name string, source string) error { // RunImport imports a Docker context
func RunImport(dockerCli command.Cli, name string, source string) error {
if err := checkContextNameForCreation(dockerCli.ContextStore(), name); err != nil { if err := checkContextNameForCreation(dockerCli.ContextStore(), name); err != nil {
return err return err
} }

View File

@ -14,12 +14,12 @@ func createTestContextWithKubeAndSwarm(t *testing.T, cli command.Cli, name strin
revert := env.Patch(t, "KUBECONFIG", "./testdata/test-kubeconfig") revert := env.Patch(t, "KUBECONFIG", "./testdata/test-kubeconfig")
defer revert() defer revert()
err := runCreate(cli, &createOptions{ err := RunCreate(cli, &CreateOptions{
name: name, Name: name,
defaultStackOrchestrator: orchestrator, DefaultStackOrchestrator: orchestrator,
description: "description of " + name, Description: "description of " + name,
kubernetes: map[string]string{keyFromCurrent: "true"}, Kubernetes: map[string]string{keyFromCurrent: "true"},
docker: map[string]string{keyHost: "https://someswarmserver"}, Docker: map[string]string{keyHost: "https://someswarmserver"},
}) })
assert.NilError(t, err) assert.NilError(t, err)
} }

View File

@ -10,32 +10,34 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
type removeOptions struct { // RemoveOptions are the options used to remove contexts
force bool type RemoveOptions struct {
Force bool
} }
func newRemoveCommand(dockerCli command.Cli) *cobra.Command { func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
var opts removeOptions var opts RemoveOptions
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "rm CONTEXT [CONTEXT...]", Use: "rm CONTEXT [CONTEXT...]",
Aliases: []string{"remove"}, Aliases: []string{"remove"},
Short: "Remove one or more contexts", Short: "Remove one or more contexts",
Args: cli.RequiresMinArgs(1), Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runRemove(dockerCli, opts, args) return RunRemove(dockerCli, opts, args)
}, },
} }
cmd.Flags().BoolVarP(&opts.force, "force", "f", false, "Force the removal of a context in use") cmd.Flags().BoolVarP(&opts.Force, "force", "f", false, "Force the removal of a context in use")
return cmd return cmd
} }
func runRemove(dockerCli command.Cli, opts removeOptions, names []string) error { // RunRemove removes one or more contexts
func RunRemove(dockerCli command.Cli, opts RemoveOptions, names []string) error {
var errs []string var errs []string
currentCtx := dockerCli.CurrentContext() currentCtx := dockerCli.CurrentContext()
for _, name := range names { for _, name := range names {
if name == "default" { if name == "default" {
errs = append(errs, `default: context "default" cannot be removed`) errs = append(errs, `default: context "default" cannot be removed`)
} else if err := doRemove(dockerCli, name, name == currentCtx, opts.force); err != nil { } else if err := doRemove(dockerCli, name, name == currentCtx, opts.Force); err != nil {
errs = append(errs, fmt.Sprintf("%s: %s", name, err)) errs = append(errs, fmt.Sprintf("%s: %s", name, err))
} else { } else {
fmt.Fprintln(dockerCli.Out(), name) fmt.Fprintln(dockerCli.Out(), name)

View File

@ -17,7 +17,7 @@ func TestRemove(t *testing.T) {
defer cleanup() defer cleanup()
createTestContextWithKubeAndSwarm(t, cli, "current", "all") createTestContextWithKubeAndSwarm(t, cli, "current", "all")
createTestContextWithKubeAndSwarm(t, cli, "other", "all") createTestContextWithKubeAndSwarm(t, cli, "other", "all")
assert.NilError(t, runRemove(cli, removeOptions{}, []string{"other"})) assert.NilError(t, RunRemove(cli, RemoveOptions{}, []string{"other"}))
_, err := cli.ContextStore().GetContextMetadata("current") _, err := cli.ContextStore().GetContextMetadata("current")
assert.NilError(t, err) assert.NilError(t, err)
_, err = cli.ContextStore().GetContextMetadata("other") _, err = cli.ContextStore().GetContextMetadata("other")
@ -29,7 +29,7 @@ func TestRemoveNotAContext(t *testing.T) {
defer cleanup() defer cleanup()
createTestContextWithKubeAndSwarm(t, cli, "current", "all") createTestContextWithKubeAndSwarm(t, cli, "current", "all")
createTestContextWithKubeAndSwarm(t, cli, "other", "all") createTestContextWithKubeAndSwarm(t, cli, "other", "all")
err := runRemove(cli, removeOptions{}, []string{"not-a-context"}) err := RunRemove(cli, RemoveOptions{}, []string{"not-a-context"})
assert.ErrorContains(t, err, `context "not-a-context" does not exist`) assert.ErrorContains(t, err, `context "not-a-context" does not exist`)
} }
@ -39,7 +39,7 @@ func TestRemoveCurrent(t *testing.T) {
createTestContextWithKubeAndSwarm(t, cli, "current", "all") createTestContextWithKubeAndSwarm(t, cli, "current", "all")
createTestContextWithKubeAndSwarm(t, cli, "other", "all") createTestContextWithKubeAndSwarm(t, cli, "other", "all")
cli.SetCurrentContext("current") cli.SetCurrentContext("current")
err := runRemove(cli, removeOptions{}, []string{"current"}) err := RunRemove(cli, RemoveOptions{}, []string{"current"})
assert.ErrorContains(t, err, "current: context is in use, set -f flag to force remove") assert.ErrorContains(t, err, "current: context is in use, set -f flag to force remove")
} }
@ -57,7 +57,7 @@ func TestRemoveCurrentForce(t *testing.T) {
createTestContextWithKubeAndSwarm(t, cli, "current", "all") createTestContextWithKubeAndSwarm(t, cli, "current", "all")
createTestContextWithKubeAndSwarm(t, cli, "other", "all") createTestContextWithKubeAndSwarm(t, cli, "other", "all")
cli.SetCurrentContext("current") cli.SetCurrentContext("current")
assert.NilError(t, runRemove(cli, removeOptions{force: true}, []string{"current"})) assert.NilError(t, RunRemove(cli, RemoveOptions{Force: true}, []string{"current"}))
reloadedConfig, err := config.Load(configDir) reloadedConfig, err := config.Load(configDir)
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, "", reloadedConfig.CurrentContext) assert.Equal(t, "", reloadedConfig.CurrentContext)

View File

@ -14,12 +14,13 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
type updateOptions struct { // UpdateOptions are the options used to update a context
name string type UpdateOptions struct {
description string Name string
defaultStackOrchestrator string Description string
docker map[string]string DefaultStackOrchestrator string
kubernetes map[string]string Docker map[string]string
Kubernetes map[string]string
} }
func longUpdateDescription() string { func longUpdateDescription() string {
@ -43,34 +44,35 @@ func longUpdateDescription() string {
} }
func newUpdateCommand(dockerCli command.Cli) *cobra.Command { func newUpdateCommand(dockerCli command.Cli) *cobra.Command {
opts := &updateOptions{} opts := &UpdateOptions{}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "update [OPTIONS] CONTEXT", Use: "update [OPTIONS] CONTEXT",
Short: "Update a context", Short: "Update a context",
Args: cli.ExactArgs(1), Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
opts.name = args[0] opts.Name = args[0]
return runUpdate(dockerCli, opts) return RunUpdate(dockerCli, opts)
}, },
Long: longUpdateDescription(), Long: longUpdateDescription(),
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.StringVar(&opts.description, "description", "", "Description of the context") flags.StringVar(&opts.Description, "description", "", "Description of the context")
flags.StringVar( flags.StringVar(
&opts.defaultStackOrchestrator, &opts.DefaultStackOrchestrator,
"default-stack-orchestrator", "", "default-stack-orchestrator", "",
"Default orchestrator for stack operations to use with this context (swarm|kubernetes|all)") "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.Docker, "docker", nil, "set the docker endpoint")
flags.StringToStringVar(&opts.kubernetes, "kubernetes", nil, "set the kubernetes endpoint") flags.StringToStringVar(&opts.Kubernetes, "kubernetes", nil, "set the kubernetes endpoint")
return cmd return cmd
} }
func runUpdate(cli command.Cli, o *updateOptions) error { // RunUpdate updates a Docker context
if err := validateContextName(o.name); err != nil { func RunUpdate(cli command.Cli, o *UpdateOptions) error {
if err := validateContextName(o.Name); err != nil {
return err return err
} }
s := cli.ContextStore() s := cli.ContextStore()
c, err := s.GetContextMetadata(o.name) c, err := s.GetContextMetadata(o.Name)
if err != nil { if err != nil {
return err return err
} }
@ -78,31 +80,31 @@ func runUpdate(cli command.Cli, o *updateOptions) error {
if err != nil { if err != nil {
return err return err
} }
if o.defaultStackOrchestrator != "" { if o.DefaultStackOrchestrator != "" {
stackOrchestrator, err := command.NormalizeOrchestrator(o.defaultStackOrchestrator) stackOrchestrator, err := command.NormalizeOrchestrator(o.DefaultStackOrchestrator)
if err != nil { if err != nil {
return errors.Wrap(err, "unable to parse default-stack-orchestrator") return errors.Wrap(err, "unable to parse default-stack-orchestrator")
} }
dockerContext.StackOrchestrator = stackOrchestrator dockerContext.StackOrchestrator = stackOrchestrator
} }
if o.description != "" { if o.Description != "" {
dockerContext.Description = o.description dockerContext.Description = o.Description
} }
c.Metadata = dockerContext c.Metadata = dockerContext
tlsDataToReset := make(map[string]*store.EndpointTLSData) tlsDataToReset := make(map[string]*store.EndpointTLSData)
if o.docker != nil { if o.Docker != nil {
dockerEP, dockerTLS, err := getDockerEndpointMetadataAndTLS(cli, o.docker) dockerEP, dockerTLS, err := getDockerEndpointMetadataAndTLS(cli, o.Docker)
if err != nil { if err != nil {
return errors.Wrap(err, "unable to create docker endpoint config") return errors.Wrap(err, "unable to create docker endpoint config")
} }
c.Endpoints[docker.DockerEndpoint] = dockerEP c.Endpoints[docker.DockerEndpoint] = dockerEP
tlsDataToReset[docker.DockerEndpoint] = dockerTLS tlsDataToReset[docker.DockerEndpoint] = dockerTLS
} }
if o.kubernetes != nil { if o.Kubernetes != nil {
kubernetesEP, kubernetesTLS, err := getKubernetesEndpointMetadataAndTLS(cli, o.kubernetes) kubernetesEP, kubernetesTLS, err := getKubernetesEndpointMetadataAndTLS(cli, o.Kubernetes)
if err != nil { if err != nil {
return errors.Wrap(err, "unable to create kubernetes endpoint config") return errors.Wrap(err, "unable to create kubernetes endpoint config")
} }
@ -120,13 +122,13 @@ func runUpdate(cli command.Cli, o *updateOptions) error {
return err return err
} }
for ep, tlsData := range tlsDataToReset { for ep, tlsData := range tlsDataToReset {
if err := s.ResetContextEndpointTLSMaterial(o.name, ep, tlsData); err != nil { if err := s.ResetContextEndpointTLSMaterial(o.Name, ep, tlsData); err != nil {
return err return err
} }
} }
fmt.Fprintln(cli.Out(), o.name) fmt.Fprintln(cli.Out(), o.Name)
fmt.Fprintf(cli.Err(), "Successfully updated context %q\n", o.name) fmt.Fprintf(cli.Err(), "Successfully updated context %q\n", o.Name)
return nil return nil
} }

View File

@ -13,17 +13,17 @@ import (
func TestUpdateDescriptionOnly(t *testing.T) { func TestUpdateDescriptionOnly(t *testing.T) {
cli, cleanup := makeFakeCli(t) cli, cleanup := makeFakeCli(t)
defer cleanup() defer cleanup()
err := runCreate(cli, &createOptions{ err := RunCreate(cli, &CreateOptions{
name: "test", Name: "test",
defaultStackOrchestrator: "swarm", DefaultStackOrchestrator: "swarm",
docker: map[string]string{}, Docker: map[string]string{},
}) })
assert.NilError(t, err) assert.NilError(t, err)
cli.OutBuffer().Reset() cli.OutBuffer().Reset()
cli.ErrBuffer().Reset() cli.ErrBuffer().Reset()
assert.NilError(t, runUpdate(cli, &updateOptions{ assert.NilError(t, RunUpdate(cli, &UpdateOptions{
name: "test", Name: "test",
description: "description", Description: "description",
})) }))
c, err := cli.ContextStore().GetContextMetadata("test") c, err := cli.ContextStore().GetContextMetadata("test")
assert.NilError(t, err) assert.NilError(t, err)
@ -40,9 +40,9 @@ func TestUpdateDockerOnly(t *testing.T) {
cli, cleanup := makeFakeCli(t) cli, cleanup := makeFakeCli(t)
defer cleanup() defer cleanup()
createTestContextWithKubeAndSwarm(t, cli, "test", "swarm") createTestContextWithKubeAndSwarm(t, cli, "test", "swarm")
assert.NilError(t, runUpdate(cli, &updateOptions{ assert.NilError(t, RunUpdate(cli, &UpdateOptions{
name: "test", Name: "test",
docker: map[string]string{ Docker: map[string]string{
keyHost: "tcp://some-host", keyHost: "tcp://some-host",
}, },
})) }))
@ -60,15 +60,15 @@ func TestUpdateDockerOnly(t *testing.T) {
func TestUpdateStackOrchestratorStrategy(t *testing.T) { func TestUpdateStackOrchestratorStrategy(t *testing.T) {
cli, cleanup := makeFakeCli(t) cli, cleanup := makeFakeCli(t)
defer cleanup() defer cleanup()
err := runCreate(cli, &createOptions{ err := RunCreate(cli, &CreateOptions{
name: "test", Name: "test",
defaultStackOrchestrator: "swarm", DefaultStackOrchestrator: "swarm",
docker: map[string]string{}, Docker: map[string]string{},
}) })
assert.NilError(t, err) assert.NilError(t, err)
err = runUpdate(cli, &updateOptions{ err = RunUpdate(cli, &UpdateOptions{
name: "test", Name: "test",
defaultStackOrchestrator: "kubernetes", DefaultStackOrchestrator: "kubernetes",
}) })
assert.ErrorContains(t, err, `cannot specify orchestrator "kubernetes" without configuring a Kubernetes endpoint`) assert.ErrorContains(t, err, `cannot specify orchestrator "kubernetes" without configuring a Kubernetes endpoint`)
} }
@ -77,9 +77,9 @@ func TestUpdateStackOrchestratorStrategyRemoveKubeEndpoint(t *testing.T) {
cli, cleanup := makeFakeCli(t) cli, cleanup := makeFakeCli(t)
defer cleanup() defer cleanup()
createTestContextWithKubeAndSwarm(t, cli, "test", "kubernetes") createTestContextWithKubeAndSwarm(t, cli, "test", "kubernetes")
err := runUpdate(cli, &updateOptions{ err := RunUpdate(cli, &UpdateOptions{
name: "test", Name: "test",
kubernetes: map[string]string{}, Kubernetes: map[string]string{},
}) })
assert.ErrorContains(t, err, `cannot specify orchestrator "kubernetes" without configuring a Kubernetes endpoint`) assert.ErrorContains(t, err, `cannot specify orchestrator "kubernetes" without configuring a Kubernetes endpoint`)
} }
@ -87,14 +87,14 @@ func TestUpdateStackOrchestratorStrategyRemoveKubeEndpoint(t *testing.T) {
func TestUpdateInvalidDockerHost(t *testing.T) { func TestUpdateInvalidDockerHost(t *testing.T) {
cli, cleanup := makeFakeCli(t) cli, cleanup := makeFakeCli(t)
defer cleanup() defer cleanup()
err := runCreate(cli, &createOptions{ err := RunCreate(cli, &CreateOptions{
name: "test", Name: "test",
docker: map[string]string{}, Docker: map[string]string{},
}) })
assert.NilError(t, err) assert.NilError(t, err)
err = runUpdate(cli, &updateOptions{ err = RunUpdate(cli, &UpdateOptions{
name: "test", Name: "test",
docker: map[string]string{ Docker: map[string]string{
keyHost: "some///invalid/host", keyHost: "some///invalid/host",
}, },
}) })

View File

@ -14,26 +14,30 @@ func newUseCommand(dockerCli command.Cli) *cobra.Command {
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
name := args[0] name := args[0]
return RunUse(dockerCli, name)
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 return cmd
} }
// RunUse set the current Docker context
func RunUse(dockerCli command.Cli, name string) error {
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
}

View File

@ -20,9 +20,9 @@ func TestUse(t *testing.T) {
testCfg := configfile.New(configFilePath) testCfg := configfile.New(configFilePath)
cli, cleanup := makeFakeCli(t, withCliConfig(testCfg)) cli, cleanup := makeFakeCli(t, withCliConfig(testCfg))
defer cleanup() defer cleanup()
err = runCreate(cli, &createOptions{ err = RunCreate(cli, &CreateOptions{
name: "test", Name: "test",
docker: map[string]string{}, Docker: map[string]string{},
}) })
assert.NilError(t, err) assert.NilError(t, err)
assert.NilError(t, newUseCommand(cli).RunE(nil, []string{"test"})) assert.NilError(t, newUseCommand(cli).RunE(nil, []string{"test"}))

View File

@ -25,6 +25,11 @@ type Config struct {
endpointTypes map[string]TypeGetter endpointTypes map[string]TypeGetter
} }
// SetEndpoint set an endpoint typing information
func (c Config) SetEndpoint(name string, getter TypeGetter) {
c.endpointTypes[name] = getter
}
// NewConfig creates a config object // NewConfig creates a config object
func NewConfig(contextType TypeGetter, endpoints ...NamedTypeGetter) Config { func NewConfig(contextType TypeGetter, endpoints ...NamedTypeGetter) Config {
res := Config{ res := Config{

View File

@ -0,0 +1,31 @@
package store
import (
"testing"
"gotest.tools/assert"
)
type testCtx struct{}
type testEP1 struct{}
type testEP2 struct{}
type testEP3 struct{}
func TestConfigModification(t *testing.T) {
cfg := NewConfig(func() interface{} { return &testCtx{} }, EndpointTypeGetter("ep1", func() interface{} { return &testEP1{} }))
assert.Equal(t, &testCtx{}, cfg.contextType())
assert.Equal(t, &testEP1{}, cfg.endpointTypes["ep1"]())
cfgCopy := cfg
// modify existing endpoint
cfg.SetEndpoint("ep1", func() interface{} { return &testEP2{} })
// add endpoint
cfg.SetEndpoint("ep2", func() interface{} { return &testEP3{} })
assert.Equal(t, &testCtx{}, cfg.contextType())
assert.Equal(t, &testEP2{}, cfg.endpointTypes["ep1"]())
assert.Equal(t, &testEP3{}, cfg.endpointTypes["ep2"]())
// check it applied on already initialized store
assert.Equal(t, &testCtx{}, cfgCopy.contextType())
assert.Equal(t, &testEP2{}, cfgCopy.endpointTypes["ep1"]())
assert.Equal(t, &testEP3{}, cfgCopy.endpointTypes["ep2"]())
}