Make default context behaves like a real context:

- when using "--context default" parameter
- when printing the list of contexts
- when exporting the default context to a tarball

Signed-off-by: Jean-Christophe Sirot <jean-christophe.sirot@docker.com>
(+1 squashed commit)
Squashed commits:
[20670495] Fix CLI initialization for the `docker stack deploy --help` command and ensure that the dockerCli.CurrentContext() always returns a non empty context name (default as a fallback)
Remove now obsolete code handling empty string context name
Minor code cleanup

Signed-off-by: Jean-Christophe Sirot <jean-christophe.sirot@docker.com>
This commit is contained in:
Jean-Christophe Sirot 2019-03-06 15:01:12 +01:00
parent 86a5a489f7
commit b3aa17187f
18 changed files with 575 additions and 106 deletions

View File

@ -3,6 +3,7 @@ package command
import ( import (
"context" "context"
"io" "io"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
@ -209,12 +210,18 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...Initialize
cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err) cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err)
cli.contextStore = store.New(cliconfig.ContextStoreDir(), cli.contextStoreConfig) baseContextSore := store.New(cliconfig.ContextStoreDir(), cli.contextStoreConfig)
cli.contextStore = &ContextStoreWithDefault{
Store: baseContextSore,
Resolver: func() (*DefaultContext, error) {
return resolveDefaultContext(opts.Common, cli.ConfigFile(), cli.Err())
},
}
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
} }
cli.dockerEndpoint, err = resolveDockerEndpoint(cli.contextStore, cli.currentContext, opts.Common) cli.dockerEndpoint, err = resolveDockerEndpoint(cli.contextStore, cli.currentContext)
if err != nil { if err != nil {
return errors.Wrap(err, "unable to resolve docker endpoint") return errors.Wrap(err, "unable to resolve docker endpoint")
} }
@ -252,12 +259,17 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...Initialize
// 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(), defaultContextStoreConfig()) store := &ContextStoreWithDefault{
Store: store.New(cliconfig.ContextStoreDir(), defaultContextStoreConfig()),
Resolver: func() (*DefaultContext, error) {
return resolveDefaultContext(opts, configFile, ioutil.Discard)
},
}
contextName, err := resolveContextName(opts, configFile, store) contextName, err := resolveContextName(opts, configFile, store)
if err != nil { if err != nil {
return nil, err return nil, err
} }
endpoint, err := resolveDockerEndpoint(store, contextName, opts) endpoint, err := resolveDockerEndpoint(store, contextName)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "unable to resolve docker endpoint") return nil, errors.Wrap(err, "unable to resolve docker endpoint")
} }
@ -278,8 +290,7 @@ func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigF
return client.NewClientWithOpts(clientOpts...) return client.NewClientWithOpts(clientOpts...)
} }
func resolveDockerEndpoint(s store.Store, contextName string, opts *cliflags.CommonOptions) (docker.Endpoint, error) { func resolveDockerEndpoint(s store.Store, contextName string) (docker.Endpoint, error) {
if contextName != "" {
ctxMeta, err := s.GetContextMetadata(contextName) ctxMeta, err := s.GetContextMetadata(contextName)
if err != nil { if err != nil {
return docker.Endpoint{}, err return docker.Endpoint{}, err
@ -289,7 +300,10 @@ func resolveDockerEndpoint(s store.Store, contextName string, opts *cliflags.Com
return docker.Endpoint{}, err return docker.Endpoint{}, err
} }
return docker.WithTLSData(s, contextName, epMeta) return docker.WithTLSData(s, contextName, epMeta)
} }
// Resolve the Docker endpoint for the default context (based on config, env vars and CLI flags)
func resolveDefaultDockerEndpoint(opts *cliflags.CommonOptions) (docker.Endpoint, error) {
host, err := getServerHost(opts.Hosts, opts.TLSOptions) host, err := getServerHost(opts.Hosts, opts.TLSOptions)
if err != nil { if err != nil {
return docker.Endpoint{}, err return docker.Endpoint{}, err
@ -384,26 +398,11 @@ func (cli *DockerCli) CurrentContext() string {
// StackOrchestrator resolves which stack orchestrator is in use // StackOrchestrator resolves which stack orchestrator is in use
func (cli *DockerCli) StackOrchestrator(flagValue string) (Orchestrator, error) { func (cli *DockerCli) StackOrchestrator(flagValue string) (Orchestrator, error) {
var ctxOrchestrator string
configFile := cli.configFile
if configFile == nil {
configFile = cliconfig.LoadDefaultConfigFile(cli.Err())
}
currentContext := cli.CurrentContext() currentContext := cli.CurrentContext()
if currentContext == "" { ctxRaw, err := cli.ContextStore().GetContextMetadata(currentContext)
currentContext = configFile.CurrentContext
}
if currentContext != "" {
contextstore := cli.contextStore
if contextstore == nil {
contextstore = store.New(cliconfig.ContextStoreDir(), cli.contextStoreConfig)
}
ctxRaw, err := contextstore.GetContextMetadata(currentContext)
if store.IsErrContextDoesNotExist(err) { if store.IsErrContextDoesNotExist(err) {
// case where the currentContext has been removed (CLI behavior is to fallback to using DOCKER_HOST based resolution) // 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()) return GetStackOrchestrator(flagValue, "", cli.ConfigFile().StackOrchestrator, cli.Err())
} }
if err != nil { if err != nil {
return "", err return "", err
@ -412,10 +411,8 @@ func (cli *DockerCli) StackOrchestrator(flagValue string) (Orchestrator, error)
if err != nil { if err != nil {
return "", err return "", err
} }
ctxOrchestrator = string(ctxMeta.StackOrchestrator) ctxOrchestrator := string(ctxMeta.StackOrchestrator)
} return GetStackOrchestrator(flagValue, ctxOrchestrator, cli.ConfigFile().StackOrchestrator, cli.Err())
return GetStackOrchestrator(flagValue, ctxOrchestrator, configFile.StackOrchestrator, cli.Err())
} }
// DockerEndpoint returns the current docker endpoint // DockerEndpoint returns the current docker endpoint
@ -511,10 +508,10 @@ func resolveContextName(opts *cliflags.CommonOptions, config *configfile.ConfigF
return opts.Context, nil return opts.Context, nil
} }
if len(opts.Hosts) > 0 { if len(opts.Hosts) > 0 {
return "", nil return DefaultContextName, nil
} }
if _, present := os.LookupEnv("DOCKER_HOST"); present { if _, present := os.LookupEnv("DOCKER_HOST"); present {
return "", nil return DefaultContextName, nil
} }
if ctxName, ok := os.LookupEnv("DOCKER_CONTEXT"); ok { if ctxName, ok := os.LookupEnv("DOCKER_CONTEXT"); ok {
return ctxName, nil return ctxName, nil
@ -526,7 +523,7 @@ func resolveContextName(opts *cliflags.CommonOptions, config *configfile.ConfigF
} }
return config.CurrentContext, err return config.CurrentContext, err
} }
return "", nil return DefaultContextName, nil
} }
func defaultContextStoreConfig() store.Config { func defaultContextStoreConfig() store.Config {

View File

@ -23,7 +23,26 @@ func makeFakeCli(t *testing.T, opts ...func(*test.FakeCli)) (*test.FakeCli, func
store.EndpointTypeGetter(docker.DockerEndpoint, func() interface{} { return &docker.EndpointMeta{} }), store.EndpointTypeGetter(docker.DockerEndpoint, func() interface{} { return &docker.EndpointMeta{} }),
store.EndpointTypeGetter(kubernetes.KubernetesEndpoint, func() interface{} { return &kubernetes.EndpointMeta{} }), store.EndpointTypeGetter(kubernetes.KubernetesEndpoint, func() interface{} { return &kubernetes.EndpointMeta{} }),
) )
store := store.New(dir, storeConfig) store := &command.ContextStoreWithDefault{
Store: store.New(dir, storeConfig),
Resolver: func() (*command.DefaultContext, error) {
return &command.DefaultContext{
Meta: store.ContextMetadata{
Endpoints: map[string]interface{}{
docker.DockerEndpoint: docker.EndpointMeta{
Host: "unix:///var/run/docker.sock",
},
},
Metadata: command.DockerContext{
Description: "",
StackOrchestrator: command.OrchestratorSwarm,
},
Name: command.DefaultContextName,
},
TLS: store.ContextTLSData{},
}, nil
},
}
cleanup := func() { cleanup := func() {
os.RemoveAll(dir) os.RemoveAll(dir)
} }
@ -52,6 +71,12 @@ func TestCreateInvalids(t *testing.T) {
{ {
expecterErr: `context name cannot be empty`, expecterErr: `context name cannot be empty`,
}, },
{
options: CreateOptions{
Name: "default",
},
expecterErr: `"default" is a reserved context name`,
},
{ {
options: CreateOptions{ options: CreateOptions{
Name: " ", Name: " ",

View File

@ -77,7 +77,7 @@ func writeTo(dockerCli command.Cli, reader io.Reader, dest string) error {
// RunExport exports a Docker context // RunExport exports a Docker context
func RunExport(dockerCli command.Cli, opts *ExportOptions) error { func RunExport(dockerCli command.Cli, opts *ExportOptions) error {
if err := validateContextName(opts.ContextName); err != nil { if err := validateContextName(opts.ContextName); err != nil && opts.ContextName != command.DefaultContextName {
return err return err
} }
ctxMeta, err := dockerCli.ContextStore().GetContextMetadata(opts.ContextName) ctxMeta, err := dockerCli.ContextStore().GetContextMetadata(opts.ContextName)

View File

@ -40,9 +40,6 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command {
func runInspect(dockerCli command.Cli, opts inspectOptions) error { func runInspect(dockerCli command.Cli, opts inspectOptions) error {
getRefFunc := func(ref string) (interface{}, []byte, 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) c, err := dockerCli.ContextStore().GetContextMetadata(ref)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err

View File

@ -9,7 +9,6 @@ import (
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/context/docker" "github.com/docker/cli/cli/context/docker"
kubecontext "github.com/docker/cli/cli/context/kubernetes" kubecontext "github.com/docker/cli/cli/context/kubernetes"
"github.com/docker/cli/kubernetes"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"vbom.ml/util/sortorder" "vbom.ml/util/sortorder"
) )
@ -61,6 +60,9 @@ func runList(dockerCli command.Cli, opts *listOptions) error {
if kubernetesEndpoint != nil { if kubernetesEndpoint != nil {
kubEndpointText = fmt.Sprintf("%s (%s)", kubernetesEndpoint.Host, kubernetesEndpoint.DefaultNamespace) kubEndpointText = fmt.Sprintf("%s (%s)", kubernetesEndpoint.Host, kubernetesEndpoint.DefaultNamespace)
} }
if rawMeta.Name == command.DefaultContextName {
meta.Description = "Current DOCKER_HOST based configuration"
}
desc := formatter.ClientContext{ desc := formatter.ClientContext{
Name: rawMeta.Name, Name: rawMeta.Name,
Current: rawMeta.Name == curContext, Current: rawMeta.Name == curContext,
@ -71,29 +73,6 @@ func runList(dockerCli command.Cli, opts *listOptions) error {
} }
contexts = append(contexts, &desc) 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 { sort.Slice(contexts, func(i, j int) bool {
return sortorder.NaturalLess(contexts[i].Name, contexts[j].Name) return sortorder.NaturalLess(contexts[i].Name, contexts[j].Name)
}) })

View File

@ -4,7 +4,6 @@ import (
"testing" "testing"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/context/docker"
"gotest.tools/assert" "gotest.tools/assert"
"gotest.tools/env" "gotest.tools/env"
"gotest.tools/golden" "gotest.tools/golden"
@ -36,20 +35,6 @@ func TestList(t *testing.T) {
golden.Assert(t, cli.OutBuffer().String(), "list.golden") 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) { func TestListQuiet(t *testing.T) {
cli, cleanup := makeFakeCli(t) cli, cleanup := makeFakeCli(t)
defer cleanup() defer cleanup()

View File

@ -62,3 +62,12 @@ func TestRemoveCurrentForce(t *testing.T) {
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, "", reloadedConfig.CurrentContext) assert.Equal(t, "", reloadedConfig.CurrentContext)
} }
func TestRemoveDefault(t *testing.T) {
cli, cleanup := makeFakeCli(t)
defer cleanup()
createTestContextWithKubeAndSwarm(t, cli, "other", "all")
cli.SetCurrentContext("current")
err := RunRemove(cli, RemoveOptions{}, []string{"default"})
assert.ErrorContains(t, err, `default: context "default" cannot be removed`)
}

View File

@ -1,5 +1,5 @@
NAME DESCRIPTION DOCKER ENDPOINT KUBERNETES ENDPOINT ORCHESTRATOR NAME DESCRIPTION DOCKER ENDPOINT KUBERNETES ENDPOINT ORCHESTRATOR
current * description of current https://someswarmserver https://someserver (default) all current * description of current https://someswarmserver https://someserver (default) all
default Current DOCKER_HOST based configuration default Current DOCKER_HOST based configuration unix:///var/run/docker.sock swarm
other description of other https://someswarmserver https://someserver (default) all other description of other https://someswarmserver https://someserver (default) all
unset description of unset https://someswarmserver https://someserver (default) unset description of unset https://someswarmserver https://someserver (default)

View File

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

View File

@ -1,2 +1,3 @@
current current
default
other other

View File

@ -0,0 +1,198 @@
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"
)
const (
// DefaultContextName is the name reserved for the default context (config & env based)
DefaultContextName = "default"
)
// DefaultContext contains the default context data for all enpoints
type DefaultContext struct {
Meta store.ContextMetadata
TLS store.ContextTLSData
}
// DefaultContextResolver is a function which resolves the default context base on the configuration and the env variables
type DefaultContextResolver func() (*DefaultContext, error)
// ContextStoreWithDefault implements the store.Store interface with a support for the default context
type ContextStoreWithDefault struct {
store.Store
Resolver DefaultContextResolver
}
// resolveDefaultContext creates a ContextMetadata for the current CLI invocation parameters
func resolveDefaultContext(opts *cliflags.CommonOptions, config *configfile.ConfigFile, stderr io.Writer) (*DefaultContext, error) {
stackOrchestrator, err := GetStackOrchestrator("", "", config.StackOrchestrator, stderr)
if err != nil {
return nil, err
}
contextTLSData := store.ContextTLSData{
Endpoints: make(map[string]store.EndpointTLSData),
}
contextMetadata := store.ContextMetadata{
Endpoints: make(map[string]interface{}),
Metadata: DockerContext{
Description: "",
StackOrchestrator: stackOrchestrator,
},
Name: DefaultContextName,
}
dockerEP, err := resolveDefaultDockerEndpoint(opts)
if err != nil {
return nil, err
}
contextMetadata.Endpoints[docker.DockerEndpoint] = dockerEP.EndpointMeta
if dockerEP.TLSData != nil {
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()
}
}
return &DefaultContext{Meta: contextMetadata, TLS: contextTLSData}, nil
}
// ListContexts implements store.Store's ListContexts
func (s *ContextStoreWithDefault) ListContexts() ([]store.ContextMetadata, error) {
contextList, err := s.Store.ListContexts()
if err != nil {
return nil, err
}
defaultContext, err := s.Resolver()
if err != nil {
return nil, err
}
return append(contextList, defaultContext.Meta), nil
}
// CreateOrUpdateContext is not allowed for the default context and fails
func (s *ContextStoreWithDefault) CreateOrUpdateContext(meta store.ContextMetadata) error {
if meta.Name == DefaultContextName {
return errors.New("default context cannot be created nor updated")
}
return s.Store.CreateOrUpdateContext(meta)
}
// RemoveContext is not allowed for the default context and fails
func (s *ContextStoreWithDefault) RemoveContext(name string) error {
if name == DefaultContextName {
return errors.New("default context cannot be removed")
}
return s.Store.RemoveContext(name)
}
// GetContextMetadata implements store.Store's GetContextMetadata
func (s *ContextStoreWithDefault) GetContextMetadata(name string) (store.ContextMetadata, error) {
if name == DefaultContextName {
defaultContext, err := s.Resolver()
if err != nil {
return store.ContextMetadata{}, err
}
return defaultContext.Meta, nil
}
return s.Store.GetContextMetadata(name)
}
// ResetContextTLSMaterial is not implemented for default context and fails
func (s *ContextStoreWithDefault) ResetContextTLSMaterial(name string, data *store.ContextTLSData) error {
if name == DefaultContextName {
return errors.New("The default context store does not support ResetContextTLSMaterial")
}
return s.Store.ResetContextTLSMaterial(name, data)
}
// ResetContextEndpointTLSMaterial is not implemented for default context and fails
func (s *ContextStoreWithDefault) ResetContextEndpointTLSMaterial(contextName string, endpointName string, data *store.EndpointTLSData) error {
if contextName == DefaultContextName {
return errors.New("The default context store does not support ResetContextEndpointTLSMaterial")
}
return s.Store.ResetContextEndpointTLSMaterial(contextName, endpointName, data)
}
// ListContextTLSFiles implements store.Store's ListContextTLSFiles
func (s *ContextStoreWithDefault) ListContextTLSFiles(name string) (map[string]store.EndpointFiles, error) {
if name == DefaultContextName {
defaultContext, err := s.Resolver()
if err != nil {
return nil, err
}
tlsfiles := make(map[string]store.EndpointFiles)
for epName, epTLSData := range defaultContext.TLS.Endpoints {
var files store.EndpointFiles
for filename := range epTLSData.Files {
files = append(files, filename)
}
tlsfiles[epName] = files
}
return tlsfiles, nil
}
return s.Store.ListContextTLSFiles(name)
}
// GetContextTLSData implements store.Store's GetContextTLSData
func (s *ContextStoreWithDefault) GetContextTLSData(contextName, endpointName, fileName string) ([]byte, error) {
if contextName == DefaultContextName {
defaultContext, err := s.Resolver()
if err != nil {
return nil, err
}
if defaultContext.TLS.Endpoints[endpointName].Files[fileName] == nil {
return nil, &noDefaultTLSDataError{endpointName: endpointName, fileName: fileName}
}
return defaultContext.TLS.Endpoints[endpointName].Files[fileName], nil
}
return s.Store.GetContextTLSData(contextName, endpointName, fileName)
}
type noDefaultTLSDataError struct {
endpointName string
fileName string
}
func (e *noDefaultTLSDataError) Error() string {
return fmt.Sprintf("tls data for %s/%s/%s does not exist", DefaultContextName, e.endpointName, e.fileName)
}
// NotFound satisfies interface github.com/docker/docker/errdefs.ErrNotFound
func (e *noDefaultTLSDataError) NotFound() {}
// IsTLSDataDoesNotExist satisfies github.com/docker/cli/cli/context/store.tlsDataDoesNotExist
func (e *noDefaultTLSDataError) IsTLSDataDoesNotExist() {}
// GetContextStorageInfo implements store.Store's GetContextStorageInfo
func (s *ContextStoreWithDefault) GetContextStorageInfo(contextName string) store.ContextStorageInfo {
if contextName == DefaultContextName {
return store.ContextStorageInfo{MetadataPath: "<IN MEMORY>", TLSPath: "<IN MEMORY>"}
}
return s.Store.GetContextStorageInfo(contextName)
}

View File

@ -0,0 +1,193 @@
package command
import (
"crypto/rand"
"io/ioutil"
"os"
"testing"
"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"
"gotest.tools/assert"
"gotest.tools/env"
"gotest.tools/golden"
)
type endpoint struct {
Foo string `json:"a_very_recognizable_field_name"`
}
type testContext struct {
Bar string `json:"another_very_recognizable_field_name"`
}
var testCfg = store.NewConfig(func() interface{} { return &testContext{} },
store.EndpointTypeGetter("ep1", func() interface{} { return &endpoint{} }),
store.EndpointTypeGetter("ep2", func() interface{} { return &endpoint{} }),
)
func testDefaultMetadata() store.ContextMetadata {
return store.ContextMetadata{
Endpoints: map[string]interface{}{
"ep1": endpoint{Foo: "bar"},
},
Metadata: testContext{Bar: "baz"},
Name: DefaultContextName,
}
}
func testStore(t *testing.T, meta store.ContextMetadata, tls store.ContextTLSData) (store.Store, func()) {
//meta := testDefaultMetadata()
testDir, err := ioutil.TempDir("", t.Name())
assert.NilError(t, err)
//defer os.RemoveAll(testDir)
store := &ContextStoreWithDefault{
Store: store.New(testDir, testCfg),
Resolver: func() (*DefaultContext, error) {
return &DefaultContext{
Meta: meta,
TLS: tls,
}, nil
},
}
return store, func() {
os.RemoveAll(testDir)
}
}
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",
}
ctx, err := resolveDefaultContext(&cliflags.CommonOptions{
TLS: true,
TLSOptions: &tlsconfig.Options{
CAFile: "./testdata/ca.pem",
},
}, cli.ConfigFile(), cli.Err())
assert.NilError(t, err)
assert.Equal(t, "default", ctx.Meta.Name)
assert.Equal(t, OrchestratorAll, 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) {
file1 := make([]byte, 1500)
rand.Read(file1)
file2 := make([]byte, 3700)
rand.Read(file2)
s, cleanup := testStore(t, testDefaultMetadata(), store.ContextTLSData{
Endpoints: map[string]store.EndpointTLSData{
"ep2": {
Files: map[string][]byte{
"file1": file1,
"file2": file2,
},
},
},
})
defer cleanup()
r := store.Export("default", s)
defer r.Close()
err := store.Import("dest", s, r)
assert.NilError(t, err)
srcMeta, err := s.GetContextMetadata("default")
assert.NilError(t, err)
destMeta, err := s.GetContextMetadata("dest")
assert.NilError(t, err)
assert.DeepEqual(t, destMeta.Metadata, srcMeta.Metadata)
assert.DeepEqual(t, destMeta.Endpoints, srcMeta.Endpoints)
srcFileList, err := s.ListContextTLSFiles("default")
assert.NilError(t, err)
destFileList, err := s.ListContextTLSFiles("dest")
assert.NilError(t, err)
assert.Equal(t, 1, len(destFileList))
assert.Equal(t, 1, len(srcFileList))
assert.Equal(t, 2, len(destFileList["ep2"]))
assert.Equal(t, 2, len(srcFileList["ep2"]))
srcData1, err := s.GetContextTLSData("default", "ep2", "file1")
assert.NilError(t, err)
assert.DeepEqual(t, file1, srcData1)
srcData2, err := s.GetContextTLSData("default", "ep2", "file2")
assert.NilError(t, err)
assert.DeepEqual(t, file2, srcData2)
destData1, err := s.GetContextTLSData("dest", "ep2", "file1")
assert.NilError(t, err)
assert.DeepEqual(t, file1, destData1)
destData2, err := s.GetContextTLSData("dest", "ep2", "file2")
assert.NilError(t, err)
assert.DeepEqual(t, file2, destData2)
}
func TestListDefaultContext(t *testing.T) {
meta := testDefaultMetadata()
s, cleanup := testStore(t, meta, store.ContextTLSData{})
defer cleanup()
result, err := s.ListContexts()
assert.NilError(t, err)
assert.Equal(t, 1, len(result))
assert.DeepEqual(t, meta, result[0])
}
func TestGetDefaultContextStorageInfo(t *testing.T) {
s, cleanup := testStore(t, testDefaultMetadata(), store.ContextTLSData{})
defer cleanup()
result := s.GetContextStorageInfo(DefaultContextName)
assert.Equal(t, "<IN MEMORY>", result.MetadataPath)
assert.Equal(t, "<IN MEMORY>", result.TLSPath)
}
func TestGetDefaultContextMetadata(t *testing.T) {
meta := testDefaultMetadata()
s, cleanup := testStore(t, meta, store.ContextTLSData{})
defer cleanup()
result, err := s.GetContextMetadata(DefaultContextName)
assert.NilError(t, err)
assert.Equal(t, DefaultContextName, result.Name)
assert.DeepEqual(t, meta.Metadata, result.Metadata)
assert.DeepEqual(t, meta.Endpoints, result.Endpoints)
}
func TestErrCreateDefault(t *testing.T) {
meta := testDefaultMetadata()
s, cleanup := testStore(t, meta, store.ContextTLSData{})
defer cleanup()
err := s.CreateOrUpdateContext(store.ContextMetadata{
Endpoints: map[string]interface{}{
"ep1": endpoint{Foo: "bar"},
},
Metadata: testContext{Bar: "baz"},
Name: "default",
})
assert.Error(t, err, "default context cannot be created nor updated")
}
func TestErrRemoveDefault(t *testing.T) {
meta := testDefaultMetadata()
s, cleanup := testStore(t, meta, store.ContextTLSData{})
defer cleanup()
err := s.RemoveContext("default")
assert.Error(t, err, "default context cannot be removed")
}
func TestErrTLSDataError(t *testing.T) {
meta := testDefaultMetadata()
s, cleanup := testStore(t, meta, store.ContextTLSData{})
defer cleanup()
_, err := s.GetContextTLSData("default", "noop", "noop")
assert.Check(t, store.IsErrTLSDataDoesNotExist(err))
}

View File

@ -48,6 +48,10 @@ func NewStackCommand(dockerCli command.Cli) *cobra.Command {
} }
defaultHelpFunc := cmd.HelpFunc() defaultHelpFunc := cmd.HelpFunc()
cmd.SetHelpFunc(func(c *cobra.Command, args []string) { cmd.SetHelpFunc(func(c *cobra.Command, args []string) {
if err := cmd.Root().PersistentPreRunE(c, args); err != nil {
fmt.Fprintln(dockerCli.Err(), err)
return
}
if err := cmd.PersistentPreRunE(c, args); err != nil { if err := cmd.PersistentPreRunE(c, args); err != nil {
fmt.Fprintln(dockerCli.Err(), err) fmt.Fprintln(dockerCli.Err(), err)
return return

18
cli/command/testdata/ca.pem vendored Normal file
View File

@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIIC+TCCAeGgAwIBAgIBATANBgkqhkiG9w0BAQsFADAeMQswCQYDVQQGEwJGUjEP
MA0GA1UEChMGRG9ja2VyMB4XDTE5MDMwMzIzMDAwMFoXDTI0MDMwMTIzMDAwMFow
HjELMAkGA1UEBhMCRlIxDzANBgNVBAoTBkRvY2tlcjCCASIwDQYJKoZIhvcNAQEB
BQADggEPADCCAQoCggEBAMkifL8Ne9B9LQ8+pKD20meVuV34Ol/xUcH/OfxbiBMa
HrlIKsGIaO9GraBLq1DJyaZ6sP6ntfwXqwBYQrAN2fQL1AmwMetqpNjby307XqRa
GUQekjG710LfAFKsS/yD/R8L944MFmTbYwGyjROExs8ZAA4fkA8SATzRXhM3a8dE
YcrXacZQqd5dwFFS/UyJQbMoNx7IgzrXySqpt3rV8qD8MAUebgshd2p9CQO6zzoU
ImOJImMc/15LFZymemm2KvzXTM4J9UYdibXZGzpxcnyGNCb4FVV0HF0Ya+NMDwvY
nNpW5rea64ppS8McejePRCmLS8DxMxKTLB7eW97LuDECAwEAAaNCMEAwDwYDVR0T
AQH/BAUwAwEB/zAdBgNVHQ4EFgQU6zuJSXHxniajbNcc4SHoM+fatvMwDgYDVR0P
AQH/BAQDAgE+MA0GCSqGSIb3DQEBCwUAA4IBAQB2l46NnKzoTOCuUjxGmUv3s1np
rENRWlq0mHjCzoYSocg/IcwY7fz41XkwTVV8O3h/Jm25YGnj4lqaXlEKYJ63W8eI
wGLcirUAORSspcf+jd7OOjluzYCtuvVtOKR8w22pp5oE/AooGaO5y0ysefZBopr4
CNUNsEYhDKFg7tfj6Govi6+0PNxvB53we4nU7NhJNMaClhh/pi8zbeaEf67S6eKn
Z3DFqO+8FW4wEePLwhCftESCTwx6Q24v/WIYnzYOXC5mb2B9MwkyJXJIJxxPIeSs
PycNQ2kw7gk/TKkLMNQbX4fgFB0zfdofidTAkqOIqFHq/8iD2DYEZQFgCD3v
-----END CERTIFICATE-----

20
cli/command/testdata/test-kubeconfig vendored Normal file
View File

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

View File

@ -12,7 +12,8 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/opencontainers/go-digest" "github.com/docker/docker/errdefs"
digest "github.com/opencontainers/go-digest"
) )
// Store provides a context store for easily remembering endpoints configuration // Store provides a context store for easily remembering endpoints configuration
@ -299,6 +300,11 @@ func (e *contextDoesNotExistError) setContext(name string) {
// NotFound satisfies interface github.com/docker/docker/errdefs.ErrNotFound // NotFound satisfies interface github.com/docker/docker/errdefs.ErrNotFound
func (e *contextDoesNotExistError) NotFound() {} func (e *contextDoesNotExistError) NotFound() {}
type tlsDataDoesNotExist interface {
errdefs.ErrNotFound
IsTLSDataDoesNotExist()
}
type tlsDataDoesNotExistError struct { type tlsDataDoesNotExistError struct {
context, endpoint, file string context, endpoint, file string
} }
@ -314,6 +320,9 @@ func (e *tlsDataDoesNotExistError) setContext(name string) {
// NotFound satisfies interface github.com/docker/docker/errdefs.ErrNotFound // NotFound satisfies interface github.com/docker/docker/errdefs.ErrNotFound
func (e *tlsDataDoesNotExistError) NotFound() {} func (e *tlsDataDoesNotExistError) NotFound() {}
// IsTLSDataDoesNotExist satisfies tlsDataDoesNotExist
func (e *tlsDataDoesNotExistError) IsTLSDataDoesNotExist() {}
// IsErrContextDoesNotExist checks if the given error is a "context does not exist" condition // IsErrContextDoesNotExist checks if the given error is a "context does not exist" condition
func IsErrContextDoesNotExist(err error) bool { func IsErrContextDoesNotExist(err error) bool {
_, ok := err.(*contextDoesNotExistError) _, ok := err.(*contextDoesNotExistError)
@ -322,7 +331,7 @@ func IsErrContextDoesNotExist(err error) bool {
// IsErrTLSDataDoesNotExist checks if the given error is a "context does not exist" condition // IsErrTLSDataDoesNotExist checks if the given error is a "context does not exist" condition
func IsErrTLSDataDoesNotExist(err error) bool { func IsErrTLSDataDoesNotExist(err error) bool {
_, ok := err.(*tlsDataDoesNotExistError) _, ok := err.(tlsDataDoesNotExist)
return ok return ok
} }

View File

@ -1,6 +1,7 @@
package store package store
import ( import (
"crypto/rand"
"io/ioutil" "io/ioutil"
"os" "os"
"testing" "testing"
@ -35,9 +36,14 @@ func TestExportImport(t *testing.T) {
Name: "source", Name: "source",
}) })
assert.NilError(t, err) assert.NilError(t, err)
file1 := make([]byte, 1500)
rand.Read(file1)
file2 := make([]byte, 3700)
rand.Read(file2)
err = s.ResetContextEndpointTLSMaterial("source", "ep1", &EndpointTLSData{ err = s.ResetContextEndpointTLSMaterial("source", "ep1", &EndpointTLSData{
Files: map[string][]byte{ Files: map[string][]byte{
"file1": []byte("test-data"), "file1": file1,
"file2": file2,
}, },
}) })
assert.NilError(t, err) assert.NilError(t, err)
@ -55,13 +61,22 @@ func TestExportImport(t *testing.T) {
assert.NilError(t, err) assert.NilError(t, err)
destFileList, err := s.ListContextTLSFiles("dest") destFileList, err := s.ListContextTLSFiles("dest")
assert.NilError(t, err) assert.NilError(t, err)
assert.DeepEqual(t, srcFileList, destFileList) assert.Equal(t, 1, len(destFileList))
srcData, err := s.GetContextTLSData("source", "ep1", "file1") assert.Equal(t, 1, len(srcFileList))
assert.Equal(t, 2, len(destFileList["ep1"]))
assert.Equal(t, 2, len(srcFileList["ep1"]))
srcData1, err := s.GetContextTLSData("source", "ep1", "file1")
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, "test-data", string(srcData)) assert.DeepEqual(t, file1, srcData1)
destData, err := s.GetContextTLSData("dest", "ep1", "file1") srcData2, err := s.GetContextTLSData("source", "ep1", "file2")
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, "test-data", string(destData)) assert.DeepEqual(t, file2, srcData2)
destData1, err := s.GetContextTLSData("dest", "ep1", "file1")
assert.NilError(t, err)
assert.DeepEqual(t, file1, destData1)
destData2, err := s.GetContextTLSData("dest", "ep1", "file2")
assert.NilError(t, err)
assert.DeepEqual(t, file2, destData2)
} }
func TestRemove(t *testing.T) { func TestRemove(t *testing.T) {

View File

@ -224,3 +224,24 @@ func (c *FakeCli) NewContainerizedEngineClient(sockPath string) (clitypes.Contai
func (c *FakeCli) SetContainerizedEngineClient(containerizedEngineClientFunc containerizedEngineFuncType) { func (c *FakeCli) SetContainerizedEngineClient(containerizedEngineClientFunc containerizedEngineFuncType) {
c.containerizedEngineClientFunc = containerizedEngineClientFunc c.containerizedEngineClientFunc = containerizedEngineClientFunc
} }
// StackOrchestrator return the selected stack orchestrator
func (c *FakeCli) StackOrchestrator(flagValue string) (command.Orchestrator, error) {
configOrchestrator := ""
if c.configfile != nil {
configOrchestrator = c.configfile.StackOrchestrator
}
ctxOrchestrator := ""
if c.currentContext != "" && c.contextStore != nil {
meta, err := c.contextStore.GetContextMetadata(c.currentContext)
if err != nil {
return "", err
}
context, err := command.GetDockerContext(meta)
if err != nil {
return "", err
}
ctxOrchestrator = string(context.StackOrchestrator)
}
return command.GetStackOrchestrator(flagValue, ctxOrchestrator, configOrchestrator, c.err)
}