Merge pull request #1501 from simonferquel/use-context-commands

Fast context switch: commands
This commit is contained in:
Sebastiaan van Stijn 2019-01-15 18:11:38 +01:00 committed by GitHub
commit 48bd4c6deb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 4013 additions and 146 deletions

View File

@ -3,26 +3,25 @@ package command
import (
"context"
"io"
"net"
"net/http"
"os"
"path/filepath"
"runtime"
"strconv"
"time"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/config"
cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/connhelper"
dcontext "github.com/docker/cli/cli/context"
"github.com/docker/cli/cli/context/docker"
kubcontext "github.com/docker/cli/cli/context/kubernetes"
"github.com/docker/cli/cli/context/store"
cliflags "github.com/docker/cli/cli/flags"
manifeststore "github.com/docker/cli/cli/manifest/store"
registryclient "github.com/docker/cli/cli/registry/client"
"github.com/docker/cli/cli/trust"
dopts "github.com/docker/cli/opts"
clitypes "github.com/docker/cli/types"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/client"
@ -57,6 +56,10 @@ type Cli interface {
RegistryClient(bool) registryclient.RegistryClient
ContentTrustEnabled() bool
NewContainerizedEngineClient(sockPath string) (clitypes.ContainerizedClient, error)
ContextStore() store.Store
CurrentContext() string
StackOrchestrator(flagValue string) (Orchestrator, error)
DockerEndpoint() docker.Endpoint
}
// DockerCli is an instance the docker command line client.
@ -71,8 +74,17 @@ type DockerCli struct {
clientInfo ClientInfo
contentTrust bool
newContainerizeClient func(string) (clitypes.ContainerizedClient, error)
contextStore store.Store
currentContext string
dockerEndpoint docker.Endpoint
}
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.
func (cli *DockerCli) DefaultVersion() string {
return cli.clientInfo.DefaultVersion
@ -167,14 +179,24 @@ func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.Registry
// line flags are parsed.
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err)
var err error
cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile)
cli.contextStore = store.New(cliconfig.ContextStoreDir(), storeConfig)
cli.currentContext, err = resolveContextName(opts.Common, cli.configFile, cli.contextStore)
if err != nil {
return err
}
endpoint, err := resolveDockerEndpoint(cli.contextStore, cli.currentContext, opts.Common)
if err != nil {
return errors.Wrap(err, "unable to resolve docker endpoint")
}
cli.dockerEndpoint = endpoint
cli.client, err = newAPIClientFromEndpoint(endpoint, cli.configFile)
if tlsconfig.IsErrEncryptedKey(err) {
passRetriever := passphrase.PromptRetrieverWithInOut(cli.In(), cli.Out(), nil)
newClient := func(password string) (client.APIClient, error) {
opts.Common.TLSOptions.Passphrase = password
return NewAPIClientFromFlags(opts.Common, cli.configFile)
endpoint.TLSPassword = password
return newAPIClientFromEndpoint(endpoint, cli.configFile)
}
cli.client, err = getClientWithPassword(passRetriever, newClient)
}
@ -198,6 +220,73 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
return nil
}
// NewAPIClientFromFlags creates a new APIClient from command line flags
func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
store := store.New(cliconfig.ContextStoreDir(), storeConfig)
contextName, err := resolveContextName(opts, configFile, store)
if err != nil {
return nil, err
}
endpoint, err := resolveDockerEndpoint(store, contextName, opts)
if err != nil {
return nil, errors.Wrap(err, "unable to resolve docker endpoint")
}
return newAPIClientFromEndpoint(endpoint, configFile)
}
func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile) (client.APIClient, error) {
clientOpts, err := ep.ClientOpts()
if err != nil {
return nil, err
}
customHeaders := configFile.HTTPHeaders
if customHeaders == nil {
customHeaders = map[string]string{}
}
customHeaders["User-Agent"] = UserAgent()
clientOpts = append(clientOpts, client.WithHTTPHeaders(customHeaders))
return client.NewClientWithOpts(clientOpts...)
}
func resolveDockerEndpoint(s store.Store, contextName string, opts *cliflags.CommonOptions) (docker.Endpoint, error) {
if contextName != "" {
ctxMeta, err := s.GetContextMetadata(contextName)
if err != nil {
return docker.Endpoint{}, err
}
epMeta, err := docker.EndpointFromContext(ctxMeta)
if err != nil {
return docker.Endpoint{}, err
}
return docker.WithTLSData(s, contextName, epMeta)
}
host, err := getServerHost(opts.Hosts, opts.TLSOptions)
if err != nil {
return docker.Endpoint{}, err
}
var (
skipTLSVerify bool
tlsData *dcontext.TLSData
)
if opts.TLSOptions != nil {
skipTLSVerify = opts.TLSOptions.InsecureSkipVerify
tlsData, err = dcontext.TLSDataFromFiles(opts.TLSOptions.CAFile, opts.TLSOptions.CertFile, opts.TLSOptions.KeyFile)
if err != nil {
return docker.Endpoint{}, err
}
}
return docker.Endpoint{
EndpointMeta: docker.EndpointMeta{
Host: host,
SkipTLSVerify: skipTLSVerify,
},
TLSData: tlsData,
}, nil
}
func isEnabled(value string) (bool, error) {
switch value {
case "enabled":
@ -253,6 +342,57 @@ func (cli *DockerCli) NewContainerizedEngineClient(sockPath string) (clitypes.Co
return cli.newContainerizeClient(sockPath)
}
// ContextStore returns the ContextStore
func (cli *DockerCli) ContextStore() store.Store {
return cli.contextStore
}
// CurrentContext returns the current context name
func (cli *DockerCli) CurrentContext() string {
return cli.currentContext
}
// StackOrchestrator resolves which stack orchestrator is in use
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()
if currentContext == "" {
currentContext = configFile.CurrentContext
}
if currentContext != "" {
contextstore := cli.contextStore
if contextstore == nil {
contextstore = store.New(cliconfig.ContextStoreDir(), storeConfig)
}
ctxRaw, err := contextstore.GetContextMetadata(currentContext)
if store.IsErrContextDoesNotExist(err) {
// case where the currentContext has been removed (CLI behavior is to fallback to using DOCKER_HOST based resolution)
return GetStackOrchestrator(flagValue, "", configFile.StackOrchestrator, cli.Err())
}
if err != nil {
return "", err
}
ctxMeta, err := GetDockerContext(ctxRaw)
if err != nil {
return "", err
}
ctxOrchestrator = string(ctxMeta.StackOrchestrator)
}
return GetStackOrchestrator(flagValue, ctxOrchestrator, configFile.StackOrchestrator, cli.Err())
}
// DockerEndpoint returns the current docker endpoint
func (cli *DockerCli) DockerEndpoint() docker.Endpoint {
return cli.dockerEndpoint
}
// ServerInfo stores details about the supported features and platform of the
// server
type ServerInfo struct {
@ -272,51 +412,6 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, isTrusted bool, containe
return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err, contentTrust: isTrusted, newContainerizeClient: containerizedFn}
}
// NewAPIClientFromFlags creates a new APIClient from command line flags
func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
host, err := getServerHost(opts.Hosts, opts.TLSOptions)
if err != nil {
return &client.Client{}, err
}
var clientOpts []func(*client.Client) error
helper, err := connhelper.GetConnectionHelper(host)
if err != nil {
return &client.Client{}, err
}
if helper == nil {
clientOpts = append(clientOpts, withHTTPClient(opts.TLSOptions))
clientOpts = append(clientOpts, client.WithHost(host))
} else {
clientOpts = append(clientOpts, func(c *client.Client) error {
httpClient := &http.Client{
// No tls
// No proxy
Transport: &http.Transport{
DialContext: helper.Dialer,
},
}
return client.WithHTTPClient(httpClient)(c)
})
clientOpts = append(clientOpts, client.WithHost(helper.Host))
clientOpts = append(clientOpts, client.WithDialContext(helper.Dialer))
}
customHeaders := configFile.HTTPHeaders
if customHeaders == nil {
customHeaders = map[string]string{}
}
customHeaders["User-Agent"] = UserAgent()
clientOpts = append(clientOpts, client.WithHTTPHeaders(customHeaders))
verStr := api.DefaultVersion
if tmpStr := os.Getenv("DOCKER_API_VERSION"); tmpStr != "" {
verStr = tmpStr
}
clientOpts = append(clientOpts, client.WithVersion(verStr))
return client.NewClientWithOpts(clientOpts...)
}
func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error) {
var host string
switch len(hosts) {
@ -331,35 +426,41 @@ func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error
return dopts.ParseHost(tlsOptions != nil, host)
}
func withHTTPClient(tlsOpts *tlsconfig.Options) func(*client.Client) error {
return func(c *client.Client) error {
if tlsOpts == nil {
// Use the default HTTPClient
return nil
}
opts := *tlsOpts
opts.ExclusiveRootPools = true
tlsConfig, err := tlsconfig.Client(opts)
if err != nil {
return err
}
httpClient := &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
DialContext: (&net.Dialer{
KeepAlive: 30 * time.Second,
Timeout: 30 * time.Second,
}).DialContext,
},
CheckRedirect: client.CheckRedirect,
}
return client.WithHTTPClient(httpClient)(c)
}
}
// UserAgent returns the user agent string used for making API requests
func UserAgent() string {
return "Docker-Client/" + cli.Version + " (" + runtime.GOOS + ")"
}
// resolveContextName resolves the current context name with the following rules:
// - setting both --context and --host flags is ambiguous
// - if --context is set, use this value
// - if --host flag or DOCKER_HOST is set, fallbacks to use the same logic as before context-store was added
// for backward compatibility with existing scripts
// - if DOCKER_CONTEXT is set, use this value
// - if Config file has a globally set "CurrentContext", use this value
// - fallbacks to default HOST, uses TLS config from flags/env vars
func resolveContextName(opts *cliflags.CommonOptions, config *configfile.ConfigFile, contextstore store.Store) (string, error) {
if opts.Context != "" && len(opts.Hosts) > 0 {
return "", errors.New("Conflicting options: either specify --host or --context, not both")
}
if opts.Context != "" {
return opts.Context, nil
}
if len(opts.Hosts) > 0 {
return "", nil
}
if _, present := os.LookupEnv("DOCKER_HOST"); present {
return "", nil
}
if ctxName, ok := os.LookupEnv("DOCKER_CONTEXT"); ok {
return ctxName, nil
}
if config != nil && config.CurrentContext != "" {
_, err := contextstore.GetContextMetadata(config.CurrentContext)
if store.IsErrContextDoesNotExist(err) {
return "", errors.Errorf("Current context %q is not found on the file system, please check your config file at %s", config.CurrentContext, config.Filename)
}
return config.CurrentContext, err
}
return "", nil
}

View File

@ -66,6 +66,7 @@ func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) {
func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
customVersion := "v3.3.3"
defer env.Patch(t, "DOCKER_API_VERSION", customVersion)()
defer env.Patch(t, "DOCKER_HOST", ":2375")()
opts := &flags.CommonOptions{}
configFile := &configfile.ConfigFile{}

View File

@ -9,6 +9,7 @@ import (
"github.com/docker/cli/cli/command/checkpoint"
"github.com/docker/cli/cli/command/config"
"github.com/docker/cli/cli/command/container"
"github.com/docker/cli/cli/command/context"
"github.com/docker/cli/cli/command/engine"
"github.com/docker/cli/cli/command/image"
"github.com/docker/cli/cli/command/manifest"
@ -86,6 +87,9 @@ func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
// volume
volume.NewVolumeCommand(dockerCli),
// context
context.NewContextCommand(dockerCli),
// legacy commands may be hidden
hide(system.NewEventsCommand(dockerCli)),
hide(system.NewInfoCommand(dockerCli)),

27
cli/command/context.go Normal file
View File

@ -0,0 +1,27 @@
package command
import (
"errors"
"github.com/docker/cli/cli/context/store"
)
// DockerContext is a typed representation of what we put in Context metadata
type DockerContext struct {
Description string `json:",omitempty"`
StackOrchestrator Orchestrator `json:",omitempty"`
}
// GetDockerContext extracts metadata from stored context metadata
func GetDockerContext(storeMetadata store.ContextMetadata) (DockerContext, error) {
if storeMetadata.Metadata == nil {
// can happen if we save endpoints before assigning a context metadata
// it is totally valid, and we should return a default initialized value
return DockerContext{}, nil
}
res, ok := storeMetadata.Metadata.(DockerContext)
if !ok {
return DockerContext{}, errors.New("context metadata is not a valid DockerContext")
}
return res, nil
}

View File

@ -0,0 +1,49 @@
package context
import (
"errors"
"fmt"
"regexp"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
)
// NewContextCommand returns the context cli subcommand
func NewContextCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "context",
Short: "Manage contexts",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCli.Err()),
}
cmd.AddCommand(
newCreateCommand(dockerCli),
newListCommand(dockerCli),
newUseCommand(dockerCli),
newExportCommand(dockerCli),
newImportCommand(dockerCli),
newRemoveCommand(dockerCli),
newUpdateCommand(dockerCli),
newInspectCommand(dockerCli),
)
return cmd
}
const restrictedNamePattern = "^[a-zA-Z0-9][a-zA-Z0-9_.+-]+$"
var restrictedNameRegEx = regexp.MustCompile(restrictedNamePattern)
func validateContextName(name string) error {
if name == "" {
return errors.New("context name cannot be empty")
}
if name == "default" {
return errors.New(`"default" is a reserved context name`)
}
if !restrictedNameRegEx.MatchString(name) {
return fmt.Errorf("context name %q is invalid, names are validated against regexp %q", name, restrictedNamePattern)
}
return nil
}

View File

@ -0,0 +1,139 @@
package context
import (
"bytes"
"fmt"
"text/tabwriter"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/context/docker"
"github.com/docker/cli/cli/context/kubernetes"
"github.com/docker/cli/cli/context/store"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
type createOptions struct {
name string
description string
defaultStackOrchestrator string
docker map[string]string
kubernetes map[string]string
}
func longCreateDescription() string {
buf := bytes.NewBuffer(nil)
buf.WriteString("Create a context\n\nDocker endpoint config:\n\n")
tw := tabwriter.NewWriter(buf, 20, 1, 3, ' ', 0)
fmt.Fprintln(tw, "NAME\tDESCRIPTION")
for _, d := range dockerConfigKeysDescriptions {
fmt.Fprintf(tw, "%s\t%s\n", d.name, d.description)
}
tw.Flush()
buf.WriteString("\nKubernetes endpoint config:\n\n")
tw = tabwriter.NewWriter(buf, 20, 1, 3, ' ', 0)
fmt.Fprintln(tw, "NAME\tDESCRIPTION")
for _, d := range kubernetesConfigKeysDescriptions {
fmt.Fprintf(tw, "%s\t%s\n", d.name, d.description)
}
tw.Flush()
buf.WriteString("\nExample:\n\n$ docker context create my-context --description \"some description\" --docker \"host=tcp://myserver:2376,ca=~/ca-file,cert=~/cert-file,key=~/key-file\"\n")
return buf.String()
}
func newCreateCommand(dockerCli command.Cli) *cobra.Command {
opts := &createOptions{}
cmd := &cobra.Command{
Use: "create [OPTIONS] CONTEXT",
Short: "Create a context",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.name = args[0]
return runCreate(dockerCli, opts)
},
Long: longCreateDescription(),
}
flags := cmd.Flags()
flags.StringVar(&opts.description, "description", "", "Description of the context")
flags.StringVar(
&opts.defaultStackOrchestrator,
"default-stack-orchestrator", "",
"Default orchestrator for stack operations to use with this context (swarm|kubernetes|all)")
flags.StringToStringVar(&opts.docker, "docker", nil, "set the docker endpoint")
flags.StringToStringVar(&opts.kubernetes, "kubernetes", nil, "set the kubernetes endpoint")
return cmd
}
func runCreate(cli command.Cli, o *createOptions) error {
s := cli.ContextStore()
if err := checkContextNameForCreation(s, o.name); err != nil {
return err
}
stackOrchestrator, err := command.NormalizeOrchestrator(o.defaultStackOrchestrator)
if err != nil {
return errors.Wrap(err, "unable to parse default-stack-orchestrator")
}
contextMetadata := store.ContextMetadata{
Endpoints: make(map[string]interface{}),
Metadata: command.DockerContext{
Description: o.description,
StackOrchestrator: stackOrchestrator,
},
Name: o.name,
}
if o.docker == nil {
return errors.New("docker endpoint configuration is required")
}
contextTLSData := store.ContextTLSData{
Endpoints: make(map[string]store.EndpointTLSData),
}
dockerEP, dockerTLS, err := getDockerEndpointMetadataAndTLS(cli, o.docker)
if err != nil {
return errors.Wrap(err, "unable to create docker endpoint config")
}
contextMetadata.Endpoints[docker.DockerEndpoint] = dockerEP
if dockerTLS != nil {
contextTLSData.Endpoints[docker.DockerEndpoint] = *dockerTLS
}
if o.kubernetes != nil {
kubernetesEP, kubernetesTLS, err := getKubernetesEndpointMetadataAndTLS(cli, o.kubernetes)
if err != nil {
return errors.Wrap(err, "unable to create kubernetes endpoint config")
}
if kubernetesEP == nil && stackOrchestrator.HasKubernetes() {
return errors.Errorf("cannot specify orchestrator %q without configuring a Kubernetes endpoint", stackOrchestrator)
}
if kubernetesEP != nil {
contextMetadata.Endpoints[kubernetes.KubernetesEndpoint] = kubernetesEP
}
if kubernetesTLS != nil {
contextTLSData.Endpoints[kubernetes.KubernetesEndpoint] = *kubernetesTLS
}
}
if err := validateEndpointsAndOrchestrator(contextMetadata); err != nil {
return err
}
if err := s.CreateOrUpdateContext(contextMetadata); err != nil {
return err
}
if err := s.ResetContextTLSMaterial(o.name, &contextTLSData); err != nil {
return err
}
fmt.Fprintln(cli.Out(), o.name)
fmt.Fprintf(cli.Err(), "Successfully created context %q\n", o.name)
return nil
}
func checkContextNameForCreation(s store.Store, name string) error {
if err := validateContextName(name); err != nil {
return err
}
if _, err := s.GetContextMetadata(name); !store.IsErrContextDoesNotExist(err) {
if err != nil {
return errors.Wrap(err, "error while getting existing contexts")
}
return errors.Errorf("context %q already exists", name)
}
return nil
}

View File

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

View File

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

View File

@ -0,0 +1,108 @@
package context
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/context/kubernetes"
"github.com/docker/cli/cli/context/store"
"github.com/spf13/cobra"
"k8s.io/client-go/tools/clientcmd"
)
type exportOptions struct {
kubeconfig bool
contextName string
dest string
}
func newExportCommand(dockerCli command.Cli) *cobra.Command {
opts := &exportOptions{}
cmd := &cobra.Command{
Use: "export [OPTIONS] CONTEXT [FILE|-]",
Short: "Export a context to a tar or kubeconfig file",
Args: cli.RequiresRangeArgs(1, 2),
RunE: func(cmd *cobra.Command, args []string) error {
opts.contextName = args[0]
if len(args) == 2 {
opts.dest = args[1]
} else {
opts.dest = opts.contextName
if opts.kubeconfig {
opts.dest += ".kubeconfig"
} else {
opts.dest += ".dockercontext"
}
}
return runExport(dockerCli, opts)
},
}
flags := cmd.Flags()
flags.BoolVar(&opts.kubeconfig, "kubeconfig", false, "Export as a kubeconfig file")
return cmd
}
func writeTo(dockerCli command.Cli, reader io.Reader, dest string) error {
var writer io.Writer
var printDest bool
if dest == "-" {
if dockerCli.Out().IsTerminal() {
return errors.New("cowardly refusing to export to a terminal, please specify a file path")
}
writer = dockerCli.Out()
} else {
f, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
if err != nil {
return err
}
defer f.Close()
writer = f
printDest = true
}
if _, err := io.Copy(writer, reader); err != nil {
return err
}
if printDest {
fmt.Fprintf(dockerCli.Err(), "Written file %q\n", dest)
}
return nil
}
func runExport(dockerCli command.Cli, opts *exportOptions) error {
if err := validateContextName(opts.contextName); err != nil {
return err
}
ctxMeta, err := dockerCli.ContextStore().GetContextMetadata(opts.contextName)
if err != nil {
return err
}
if !opts.kubeconfig {
reader := store.Export(opts.contextName, dockerCli.ContextStore())
defer reader.Close()
return writeTo(dockerCli, reader, opts.dest)
}
kubernetesEndpointMeta := kubernetes.EndpointFromContext(ctxMeta)
if kubernetesEndpointMeta == nil {
return fmt.Errorf("context %q has no kubernetes endpoint", opts.contextName)
}
kubernetesEndpoint, err := kubernetesEndpointMeta.WithTLSData(dockerCli.ContextStore(), opts.contextName)
if err != nil {
return err
}
kubeConfig := kubernetesEndpoint.KubernetesConfig()
rawCfg, err := kubeConfig.RawConfig()
if err != nil {
return err
}
data, err := clientcmd.Write(rawCfg)
if err != nil {
return err
}
return writeTo(dockerCli, bytes.NewBuffer(data), opts.dest)
}

View File

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

View File

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

View File

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

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

@ -0,0 +1,109 @@
package context
import (
"fmt"
"sort"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/context/docker"
kubecontext "github.com/docker/cli/cli/context/kubernetes"
"github.com/docker/cli/kubernetes"
"github.com/spf13/cobra"
"vbom.ml/util/sortorder"
)
type listOptions struct {
format string
quiet bool
}
func newListCommand(dockerCli command.Cli) *cobra.Command {
opts := &listOptions{}
cmd := &cobra.Command{
Use: "ls [OPTIONS]",
Aliases: []string{"list"},
Short: "List contexts",
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runList(dockerCli, opts)
},
}
flags := cmd.Flags()
flags.StringVar(&opts.format, "format", "", "Pretty-print contexts using a Go template")
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only show context names")
return cmd
}
func runList(dockerCli command.Cli, opts *listOptions) error {
if opts.format == "" {
opts.format = formatter.TableFormatKey
}
curContext := dockerCli.CurrentContext()
contextMap, err := dockerCli.ContextStore().ListContexts()
if err != nil {
return err
}
var contexts []*formatter.ClientContext
for _, rawMeta := range contextMap {
meta, err := command.GetDockerContext(rawMeta)
if err != nil {
return err
}
dockerEndpoint, err := docker.EndpointFromContext(rawMeta)
if err != nil {
return err
}
kubernetesEndpoint := kubecontext.EndpointFromContext(rawMeta)
kubEndpointText := ""
if kubernetesEndpoint != nil {
kubEndpointText = fmt.Sprintf("%s (%s)", kubernetesEndpoint.Host, kubernetesEndpoint.DefaultNamespace)
}
desc := formatter.ClientContext{
Name: rawMeta.Name,
Current: rawMeta.Name == curContext,
Description: meta.Description,
StackOrchestrator: string(meta.StackOrchestrator),
DockerEndpoint: dockerEndpoint.Host,
KubernetesEndpoint: kubEndpointText,
}
contexts = append(contexts, &desc)
}
if !opts.quiet {
desc := &formatter.ClientContext{
Name: "default",
Description: "Current DOCKER_HOST based configuration",
}
if dockerCli.CurrentContext() == "" {
orchestrator, _ := dockerCli.StackOrchestrator("")
kubEndpointText := ""
kubeconfig := kubernetes.NewKubernetesConfig("")
if cfg, err := kubeconfig.ClientConfig(); err == nil {
ns, _, _ := kubeconfig.Namespace()
if ns == "" {
ns = "default"
}
kubEndpointText = fmt.Sprintf("%s (%s)", cfg.Host, ns)
}
desc.Current = true
desc.StackOrchestrator = string(orchestrator)
desc.DockerEndpoint = dockerCli.DockerEndpoint().Host
desc.KubernetesEndpoint = kubEndpointText
}
contexts = append(contexts, desc)
}
sort.Slice(contexts, func(i, j int) bool {
return sortorder.NaturalLess(contexts[i].Name, contexts[j].Name)
})
return format(dockerCli, opts, contexts)
}
func format(dockerCli command.Cli, opts *listOptions, contexts []*formatter.ClientContext) error {
contextCtx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewClientContextFormat(opts.format, opts.quiet),
}
return formatter.ClientContextWrite(contextCtx, contexts)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,142 @@
package context
import (
"bytes"
"fmt"
"text/tabwriter"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/context/docker"
"github.com/docker/cli/cli/context/kubernetes"
"github.com/docker/cli/cli/context/store"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
type updateOptions struct {
name string
description string
defaultStackOrchestrator string
docker map[string]string
kubernetes map[string]string
}
func longUpdateDescription() string {
buf := bytes.NewBuffer(nil)
buf.WriteString("Update a context\n\nDocker endpoint config:\n\n")
tw := tabwriter.NewWriter(buf, 20, 1, 3, ' ', 0)
fmt.Fprintln(tw, "NAME\tDESCRIPTION")
for _, d := range dockerConfigKeysDescriptions {
fmt.Fprintf(tw, "%s\t%s\n", d.name, d.description)
}
tw.Flush()
buf.WriteString("\nKubernetes endpoint config:\n\n")
tw = tabwriter.NewWriter(buf, 20, 1, 3, ' ', 0)
fmt.Fprintln(tw, "NAME\tDESCRIPTION")
for _, d := range kubernetesConfigKeysDescriptions {
fmt.Fprintf(tw, "%s\t%s\n", d.name, d.description)
}
tw.Flush()
buf.WriteString("\nExample:\n\n$ docker context update my-context --description \"some description\" --docker \"host=tcp://myserver:2376,ca=~/ca-file,cert=~/cert-file,key=~/key-file\"\n")
return buf.String()
}
func newUpdateCommand(dockerCli command.Cli) *cobra.Command {
opts := &updateOptions{}
cmd := &cobra.Command{
Use: "update [OPTIONS] CONTEXT",
Short: "Update a context",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.name = args[0]
return runUpdate(dockerCli, opts)
},
Long: longUpdateDescription(),
}
flags := cmd.Flags()
flags.StringVar(&opts.description, "description", "", "Description of the context")
flags.StringVar(
&opts.defaultStackOrchestrator,
"default-stack-orchestrator", "",
"Default orchestrator for stack operations to use with this context (swarm|kubernetes|all)")
flags.StringToStringVar(&opts.docker, "docker", nil, "set the docker endpoint")
flags.StringToStringVar(&opts.kubernetes, "kubernetes", nil, "set the kubernetes endpoint")
return cmd
}
func runUpdate(cli command.Cli, o *updateOptions) error {
if err := validateContextName(o.name); err != nil {
return err
}
s := cli.ContextStore()
c, err := s.GetContextMetadata(o.name)
if err != nil {
return err
}
dockerContext, err := command.GetDockerContext(c)
if err != nil {
return err
}
if o.defaultStackOrchestrator != "" {
stackOrchestrator, err := command.NormalizeOrchestrator(o.defaultStackOrchestrator)
if err != nil {
return errors.Wrap(err, "unable to parse default-stack-orchestrator")
}
dockerContext.StackOrchestrator = stackOrchestrator
}
if o.description != "" {
dockerContext.Description = o.description
}
c.Metadata = dockerContext
tlsDataToReset := make(map[string]*store.EndpointTLSData)
if o.docker != nil {
dockerEP, dockerTLS, err := getDockerEndpointMetadataAndTLS(cli, o.docker)
if err != nil {
return errors.Wrap(err, "unable to create docker endpoint config")
}
c.Endpoints[docker.DockerEndpoint] = dockerEP
tlsDataToReset[docker.DockerEndpoint] = dockerTLS
}
if o.kubernetes != nil {
kubernetesEP, kubernetesTLS, err := getKubernetesEndpointMetadataAndTLS(cli, o.kubernetes)
if err != nil {
return errors.Wrap(err, "unable to create kubernetes endpoint config")
}
if kubernetesEP == nil {
delete(c.Endpoints, kubernetes.KubernetesEndpoint)
} else {
c.Endpoints[kubernetes.KubernetesEndpoint] = kubernetesEP
tlsDataToReset[kubernetes.KubernetesEndpoint] = kubernetesTLS
}
}
if err := validateEndpointsAndOrchestrator(c); err != nil {
return err
}
if err := s.CreateOrUpdateContext(c); err != nil {
return err
}
for ep, tlsData := range tlsDataToReset {
if err := s.ResetContextEndpointTLSMaterial(o.name, ep, tlsData); err != nil {
return err
}
}
fmt.Fprintln(cli.Out(), o.name)
fmt.Fprintf(cli.Err(), "Successfully updated context %q\n", o.name)
return nil
}
func validateEndpointsAndOrchestrator(c store.ContextMetadata) error {
dockerContext, err := command.GetDockerContext(c)
if err != nil {
return err
}
if _, ok := c.Endpoints[kubernetes.KubernetesEndpoint]; !ok && dockerContext.StackOrchestrator.HasKubernetes() {
return errors.Errorf("cannot specify orchestrator %q without configuring a Kubernetes endpoint", dockerContext.StackOrchestrator)
}
return nil
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,90 @@
package formatter
const (
// ClientContextTableFormat is the default client context format
ClientContextTableFormat = "table {{.Name}}{{if .Current}} *{{end}}\t{{.Description}}\t{{.DockerEndpoint}}\t{{.KubernetesEndpoint}}\t{{.StackOrchestrator}}"
dockerEndpointHeader = "DOCKER ENDPOINT"
kubernetesEndpointHeader = "KUBERNETES ENDPOINT"
stackOrchestrastorHeader = "ORCHESTRATOR"
quietContextFormat = "{{.Name}}"
)
// NewClientContextFormat returns a Format for rendering using a Context
func NewClientContextFormat(source string, quiet bool) Format {
if quiet {
return Format(quietContextFormat)
}
if source == TableFormatKey {
return Format(ClientContextTableFormat)
}
return Format(source)
}
// ClientContext is a context for display
type ClientContext struct {
Name string
Description string
DockerEndpoint string
KubernetesEndpoint string
StackOrchestrator string
Current bool
}
// ClientContextWrite writes formatted contexts using the Context
func ClientContextWrite(ctx Context, contexts []*ClientContext) error {
render := func(format func(subContext SubContext) error) error {
for _, context := range contexts {
if err := format(&clientContextContext{c: context}); err != nil {
return err
}
}
return nil
}
return ctx.Write(newClientContextContext(), render)
}
type clientContextContext struct {
HeaderContext
c *ClientContext
}
func newClientContextContext() *clientContextContext {
ctx := clientContextContext{}
ctx.Header = SubHeaderContext{
"Name": NameHeader,
"Description": DescriptionHeader,
"DockerEndpoint": dockerEndpointHeader,
"KubernetesEndpoint": kubernetesEndpointHeader,
"StackOrchestrator": stackOrchestrastorHeader,
}
return &ctx
}
func (c *clientContextContext) MarshalJSON() ([]byte, error) {
return MarshalJSON(c)
}
func (c *clientContextContext) Current() bool {
return c.c.Current
}
func (c *clientContextContext) Name() string {
return c.c.Name
}
func (c *clientContextContext) Description() string {
return c.c.Description
}
func (c *clientContextContext) DockerEndpoint() string {
return c.c.DockerEndpoint
}
func (c *clientContextContext) KubernetesEndpoint() string {
return c.c.KubernetesEndpoint
}
func (c *clientContextContext) StackOrchestrator() string {
return c.c.StackOrchestrator
}

View File

@ -16,7 +16,7 @@ const (
OrchestratorSwarm = Orchestrator("swarm")
// OrchestratorAll orchestrator
OrchestratorAll = Orchestrator("all")
orchestratorUnset = Orchestrator("unset")
orchestratorUnset = Orchestrator("")
defaultOrchestrator = OrchestratorSwarm
envVarDockerStackOrchestrator = "DOCKER_STACK_ORCHESTRATOR"
@ -44,7 +44,7 @@ func normalize(value string) (Orchestrator, error) {
return OrchestratorKubernetes, nil
case "swarm":
return OrchestratorSwarm, nil
case "":
case "", "unset": // unset is the old value for orchestratorUnset. Keep accepting this for backward compat
return orchestratorUnset, nil
case "all":
return OrchestratorAll, nil
@ -53,9 +53,14 @@ func normalize(value string) (Orchestrator, error) {
}
}
// NormalizeOrchestrator parses an orchestrator value and checks if it is valid
func NormalizeOrchestrator(value string) (Orchestrator, error) {
return normalize(value)
}
// GetStackOrchestrator checks DOCKER_STACK_ORCHESTRATOR environment variable and configuration file
// orchestrator value and returns user defined Orchestrator.
func GetStackOrchestrator(flagValue, value string, stderr io.Writer) (Orchestrator, error) {
func GetStackOrchestrator(flagValue, contextValue, globalDefault string, stderr io.Writer) (Orchestrator, error) {
// Check flag
if o, err := normalize(flagValue); o != orchestratorUnset {
return o, err
@ -68,8 +73,10 @@ func GetStackOrchestrator(flagValue, value string, stderr io.Writer) (Orchestrat
if o, err := normalize(env); o != orchestratorUnset {
return o, err
}
// Check specified orchestrator
if o, err := normalize(value); o != orchestratorUnset {
if o, err := normalize(contextValue); o != orchestratorUnset {
return o, err
}
if o, err := normalize(globalDefault); o != orchestratorUnset {
return o, err
}
// Nothing set, use default orchestrator

View File

@ -2,50 +2,39 @@ package command
import (
"io/ioutil"
"os"
"testing"
cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/flags"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
"gotest.tools/env"
"gotest.tools/fs"
)
func TestOrchestratorSwitch(t *testing.T) {
defaultVersion := "v0.00"
var testcases = []struct {
doc string
configfile string
globalOrchestrator string
envOrchestrator string
flagOrchestrator string
contextOrchestrator string
expectedOrchestrator string
expectedKubernetes bool
expectedSwarm bool
}{
{
doc: "default",
configfile: `{
}`,
expectedOrchestrator: "swarm",
expectedKubernetes: false,
expectedSwarm: true,
},
{
doc: "kubernetesConfigFile",
configfile: `{
"stackOrchestrator": "kubernetes"
}`,
globalOrchestrator: "kubernetes",
expectedOrchestrator: "kubernetes",
expectedKubernetes: true,
expectedSwarm: false,
},
{
doc: "kubernetesEnv",
configfile: `{
}`,
envOrchestrator: "kubernetes",
expectedOrchestrator: "kubernetes",
expectedKubernetes: true,
@ -53,8 +42,6 @@ func TestOrchestratorSwitch(t *testing.T) {
},
{
doc: "kubernetesFlag",
configfile: `{
}`,
flagOrchestrator: "kubernetes",
expectedOrchestrator: "kubernetes",
expectedKubernetes: true,
@ -62,18 +49,28 @@ func TestOrchestratorSwitch(t *testing.T) {
},
{
doc: "allOrchestratorFlag",
configfile: `{
}`,
flagOrchestrator: "all",
expectedOrchestrator: "all",
expectedKubernetes: true,
expectedSwarm: true,
},
{
doc: "kubernetesContext",
contextOrchestrator: "kubernetes",
expectedOrchestrator: "kubernetes",
expectedKubernetes: true,
},
{
doc: "contextOverridesConfigFile",
globalOrchestrator: "kubernetes",
contextOrchestrator: "swarm",
expectedOrchestrator: "swarm",
expectedKubernetes: false,
expectedSwarm: true,
},
{
doc: "envOverridesConfigFile",
configfile: `{
"stackOrchestrator": "kubernetes"
}`,
globalOrchestrator: "kubernetes",
envOrchestrator: "swarm",
expectedOrchestrator: "swarm",
expectedKubernetes: false,
@ -81,8 +78,6 @@ func TestOrchestratorSwitch(t *testing.T) {
},
{
doc: "flagOverridesEnv",
configfile: `{
}`,
envOrchestrator: "kubernetes",
flagOrchestrator: "swarm",
expectedOrchestrator: "swarm",
@ -93,22 +88,10 @@ func TestOrchestratorSwitch(t *testing.T) {
for _, testcase := range testcases {
t.Run(testcase.doc, func(t *testing.T) {
dir := fs.NewDir(t, testcase.doc, fs.WithFile("config.json", testcase.configfile))
defer dir.Remove()
apiclient := &fakeClient{
version: defaultVersion,
}
if testcase.envOrchestrator != "" {
defer env.Patch(t, "DOCKER_STACK_ORCHESTRATOR", testcase.envOrchestrator)()
}
cli := &DockerCli{client: apiclient, err: os.Stderr}
cliconfig.SetDir(dir.Path())
options := flags.NewClientOptions()
err := cli.Initialize(options)
assert.NilError(t, err)
orchestrator, err := GetStackOrchestrator(testcase.flagOrchestrator, cli.ConfigFile().StackOrchestrator, ioutil.Discard)
orchestrator, err := GetStackOrchestrator(testcase.flagOrchestrator, testcase.contextOrchestrator, testcase.globalOrchestrator, ioutil.Discard)
assert.NilError(t, err)
assert.Check(t, is.Equal(testcase.expectedKubernetes, orchestrator.HasKubernetes()))
assert.Check(t, is.Equal(testcase.expectedSwarm, orchestrator.HasSwarm()))

View File

@ -3,13 +3,10 @@ package stack
import (
"errors"
"fmt"
"io"
"strings"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
@ -28,11 +25,7 @@ func NewStackCommand(dockerCli command.Cli) *cobra.Command {
Short: "Manage Docker stacks",
Args: cli.NoArgs,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
configFile := dockerCli.ConfigFile()
if configFile == nil {
configFile = cliconfig.LoadDefaultConfigFile(dockerCli.Err())
}
orchestrator, err := getOrchestrator(configFile, cmd, dockerCli.Err())
orchestrator, err := getOrchestrator(dockerCli, cmd)
if err != nil {
return err
}
@ -81,12 +74,12 @@ func NewTopLevelDeployCommand(dockerCli command.Cli) *cobra.Command {
return cmd
}
func getOrchestrator(config *configfile.ConfigFile, cmd *cobra.Command, stderr io.Writer) (command.Orchestrator, error) {
func getOrchestrator(dockerCli command.Cli, cmd *cobra.Command) (command.Orchestrator, error) {
var orchestratorFlag string
if o, err := cmd.Flags().GetString("orchestrator"); err == nil {
orchestratorFlag = o
}
return command.GetStackOrchestrator(orchestratorFlag, config.StackOrchestrator, stderr)
return dockerCli.StackOrchestrator(orchestratorFlag)
}
func hideOrchestrationFlags(cmd *cobra.Command, orchestrator command.Orchestrator) {

View File

@ -7,12 +7,14 @@ import (
"os"
"github.com/docker/cli/cli/command"
kubecontext "github.com/docker/cli/cli/context/kubernetes"
kubernetes "github.com/docker/compose-on-kubernetes/api"
cliv1beta1 "github.com/docker/compose-on-kubernetes/api/client/clientset/typed/compose/v1beta1"
"github.com/pkg/errors"
flag "github.com/spf13/pflag"
kubeclient "k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)
// KubeCli holds kubernetes specifics (client, namespace) with the command.Cli
@ -55,7 +57,18 @@ func WrapCli(dockerCli command.Cli, opts Options) (*KubeCli, error) {
cli := &KubeCli{
Cli: dockerCli,
}
clientConfig := kubernetes.NewKubernetesConfig(opts.Config)
var (
clientConfig clientcmd.ClientConfig
err error
)
if dockerCli.CurrentContext() == "" {
clientConfig = kubernetes.NewKubernetesConfig(opts.Config)
} else {
clientConfig, err = kubecontext.ConfigFromContext(dockerCli.CurrentContext(), dockerCli.ContextStore())
}
if err != nil {
return nil, err
}
cli.kubeNamespace = opts.Namespace
if opts.Namespace == "" {

View File

@ -11,6 +11,7 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
kubecontext "github.com/docker/cli/cli/context/kubernetes"
"github.com/docker/cli/templates"
kubernetes "github.com/docker/compose-on-kubernetes/api"
"github.com/docker/docker/api/types"
@ -18,6 +19,7 @@ import (
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
kubernetesClient "k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
)
var versionTemplate = `{{with .Client -}}
@ -126,7 +128,7 @@ func runVersion(dockerCli command.Cli, opts *versionOptions) error {
return cli.StatusError{StatusCode: 64, Status: err.Error()}
}
orchestrator, err := command.GetStackOrchestrator("", dockerCli.ConfigFile().StackOrchestrator, dockerCli.Err())
orchestrator, err := dockerCli.StackOrchestrator("")
if err != nil {
return cli.StatusError{StatusCode: 64, Status: err.Error()}
}
@ -151,7 +153,7 @@ func runVersion(dockerCli command.Cli, opts *versionOptions) error {
vd.Server = &sv
var kubeVersion *kubernetesVersion
if orchestrator.HasKubernetes() {
kubeVersion = getKubernetesVersion(opts.kubeConfig)
kubeVersion = getKubernetesVersion(dockerCli, opts.kubeConfig)
}
foundEngine := false
foundKubernetes := false
@ -230,17 +232,29 @@ func getDetailsOrder(v types.ComponentVersion) []string {
return out
}
func getKubernetesVersion(kubeConfig string) *kubernetesVersion {
func getKubernetesVersion(dockerCli command.Cli, kubeConfig string) *kubernetesVersion {
version := kubernetesVersion{
Kubernetes: "Unknown",
StackAPI: "Unknown",
}
clientConfig := kubernetes.NewKubernetesConfig(kubeConfig)
config, err := clientConfig.ClientConfig()
var (
clientConfig clientcmd.ClientConfig
err error
)
if dockerCli.CurrentContext() == "" {
clientConfig = kubernetes.NewKubernetesConfig(kubeConfig)
} else {
clientConfig, err = kubecontext.ConfigFromContext(dockerCli.CurrentContext(), dockerCli.ContextStore())
}
if err != nil {
logrus.Debugf("failed to get Kubernetes configuration: %s", err)
return &version
}
config, err := clientConfig.ClientConfig()
if err != nil {
logrus.Debugf("failed to get Kubernetes client config: %s", err)
return &version
}
kubeClient, err := kubernetesClient.NewForConfig(config)
if err != nil {
logrus.Debugf("failed to get Kubernetes client: %s", err)

View File

@ -18,6 +18,7 @@ const (
ConfigFileName = "config.json"
configFileDir = ".docker"
oldConfigfile = ".dockercfg"
contextsDir = "contexts"
)
var (
@ -35,6 +36,11 @@ func Dir() string {
return configDir
}
// ContextStoreDir returns the directory the docker contexts are stored in
func ContextStoreDir() string {
return filepath.Join(Dir(), contextsDir)
}
// SetDir sets the directory the configuration file is stored in
func SetDir(dir string) {
configDir = dir

View File

@ -48,6 +48,7 @@ type ConfigFile struct {
Experimental string `json:"experimental,omitempty"`
StackOrchestrator string `json:"stackOrchestrator,omitempty"`
Kubernetes *KubernetesConfig `json:"kubernetes,omitempty"`
CurrentContext string `json:"currentContext,omitempty"`
}
// ProxyConfig contains proxy configuration settings

View File

@ -0,0 +1,6 @@
package docker
const (
// DockerEndpoint is the name of the docker endpoint in a stored context
DockerEndpoint = "docker"
)

166
cli/context/docker/load.go Normal file
View File

@ -0,0 +1,166 @@
package docker
import (
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"net"
"net/http"
"os"
"time"
"github.com/docker/cli/cli/connhelper"
"github.com/docker/cli/cli/context"
"github.com/docker/cli/cli/context/store"
"github.com/docker/docker/client"
"github.com/docker/go-connections/tlsconfig"
"github.com/pkg/errors"
)
// EndpointMeta is a typed wrapper around a context-store generic endpoint describing
// a Docker Engine endpoint, without its tls config
type EndpointMeta = context.EndpointMetaBase
// Endpoint is a typed wrapper around a context-store generic endpoint describing
// a Docker Engine endpoint, with its tls data
type Endpoint struct {
EndpointMeta
TLSData *context.TLSData
TLSPassword string
}
// WithTLSData loads TLS materials for the endpoint
func WithTLSData(s store.Store, contextName string, m EndpointMeta) (Endpoint, error) {
tlsData, err := context.LoadTLSData(s, contextName, DockerEndpoint)
if err != nil {
return Endpoint{}, err
}
return Endpoint{
EndpointMeta: m,
TLSData: tlsData,
}, nil
}
// tlsConfig extracts a context docker endpoint TLS config
func (c *Endpoint) tlsConfig() (*tls.Config, error) {
if c.TLSData == nil && !c.SkipTLSVerify {
// there is no specific tls config
return nil, nil
}
var tlsOpts []func(*tls.Config)
if c.TLSData != nil && c.TLSData.CA != nil {
certPool := x509.NewCertPool()
if !certPool.AppendCertsFromPEM(c.TLSData.CA) {
return nil, errors.New("failed to retrieve context tls info: ca.pem seems invalid")
}
tlsOpts = append(tlsOpts, func(cfg *tls.Config) {
cfg.RootCAs = certPool
})
}
if c.TLSData != nil && c.TLSData.Key != nil && c.TLSData.Cert != nil {
keyBytes := c.TLSData.Key
pemBlock, _ := pem.Decode(keyBytes)
if pemBlock == nil {
return nil, fmt.Errorf("no valid private key found")
}
var err error
if x509.IsEncryptedPEMBlock(pemBlock) {
keyBytes, err = x509.DecryptPEMBlock(pemBlock, []byte(c.TLSPassword))
if err != nil {
return nil, errors.Wrap(err, "private key is encrypted, but could not decrypt it")
}
keyBytes = pem.EncodeToMemory(&pem.Block{Type: pemBlock.Type, Bytes: keyBytes})
}
x509cert, err := tls.X509KeyPair(c.TLSData.Cert, keyBytes)
if err != nil {
return nil, errors.Wrap(err, "failed to retrieve context tls info")
}
tlsOpts = append(tlsOpts, func(cfg *tls.Config) {
cfg.Certificates = []tls.Certificate{x509cert}
})
}
if c.SkipTLSVerify {
tlsOpts = append(tlsOpts, func(cfg *tls.Config) {
cfg.InsecureSkipVerify = true
})
}
return tlsconfig.ClientDefault(tlsOpts...), nil
}
// ClientOpts returns a slice of Client options to configure an API client with this endpoint
func (c *Endpoint) ClientOpts() ([]func(*client.Client) error, error) {
var result []func(*client.Client) error
if c.Host != "" {
helper, err := connhelper.GetConnectionHelper(c.Host)
if err != nil {
return nil, err
}
if helper == nil {
tlsConfig, err := c.tlsConfig()
if err != nil {
return nil, err
}
result = append(result,
client.WithHost(c.Host),
withHTTPClient(tlsConfig),
)
} else {
httpClient := &http.Client{
// No tls
// No proxy
Transport: &http.Transport{
DialContext: helper.Dialer,
},
}
result = append(result,
client.WithHTTPClient(httpClient),
client.WithHost(helper.Host),
client.WithDialContext(helper.Dialer),
)
}
}
version := os.Getenv("DOCKER_API_VERSION")
if version != "" {
result = append(result, client.WithVersion(version))
}
return result, nil
}
func withHTTPClient(tlsConfig *tls.Config) func(*client.Client) error {
return func(c *client.Client) error {
if tlsConfig == nil {
// Use the default HTTPClient
return nil
}
httpClient := &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
DialContext: (&net.Dialer{
KeepAlive: 30 * time.Second,
Timeout: 30 * time.Second,
}).DialContext,
},
CheckRedirect: client.CheckRedirect,
}
return client.WithHTTPClient(httpClient)(c)
}
}
// EndpointFromContext parses a context docker endpoint metadata into a typed EndpointMeta structure
func EndpointFromContext(metadata store.ContextMetadata) (EndpointMeta, error) {
ep, ok := metadata.Endpoints[DockerEndpoint]
if !ok {
return EndpointMeta{}, errors.New("cannot find docker endpoint in context")
}
typed, ok := ep.(EndpointMeta)
if !ok {
return EndpointMeta{}, errors.Errorf("endpoint %q is not of type EndpointMeta", DockerEndpoint)
}
return typed, nil
}

7
cli/context/endpoint.go Normal file
View File

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

View File

@ -0,0 +1,6 @@
package kubernetes
const (
// KubernetesEndpoint is the kubernetes endpoint name in a stored context
KubernetesEndpoint = "kubernetes"
)

View File

@ -0,0 +1,196 @@
package kubernetes
import (
"io/ioutil"
"os"
"testing"
"github.com/docker/cli/cli/context"
"github.com/docker/cli/cli/context/store"
"gotest.tools/assert"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
)
func testEndpoint(server, defaultNamespace string, ca, cert, key []byte, skipTLSVerify bool) Endpoint {
var tlsData *context.TLSData
if ca != nil || cert != nil || key != nil {
tlsData = &context.TLSData{
CA: ca,
Cert: cert,
Key: key,
}
}
return Endpoint{
EndpointMeta: EndpointMeta{
EndpointMetaBase: context.EndpointMetaBase{
Host: server,
SkipTLSVerify: skipTLSVerify,
},
DefaultNamespace: defaultNamespace,
},
TLSData: tlsData,
}
}
var testStoreCfg = store.NewConfig(
func() interface{} {
return &map[string]interface{}{}
},
store.EndpointTypeGetter(KubernetesEndpoint, func() interface{} { return &EndpointMeta{} }),
)
func TestSaveLoadContexts(t *testing.T) {
storeDir, err := ioutil.TempDir("", "test-load-save-k8-context")
assert.NilError(t, err)
defer os.RemoveAll(storeDir)
store := store.New(storeDir, testStoreCfg)
assert.NilError(t, save(store, testEndpoint("https://test", "test", nil, nil, nil, false), "raw-notls"))
assert.NilError(t, save(store, testEndpoint("https://test", "test", nil, nil, nil, true), "raw-notls-skip"))
assert.NilError(t, save(store, testEndpoint("https://test", "test", []byte("ca"), []byte("cert"), []byte("key"), true), "raw-tls"))
kcFile, err := ioutil.TempFile(os.TempDir(), "test-load-save-k8-context")
assert.NilError(t, err)
defer os.Remove(kcFile.Name())
defer kcFile.Close()
cfg := clientcmdapi.NewConfig()
cfg.AuthInfos["user"] = clientcmdapi.NewAuthInfo()
cfg.Contexts["context1"] = clientcmdapi.NewContext()
cfg.Clusters["cluster1"] = clientcmdapi.NewCluster()
cfg.Contexts["context2"] = clientcmdapi.NewContext()
cfg.Clusters["cluster2"] = clientcmdapi.NewCluster()
cfg.AuthInfos["user"].ClientCertificateData = []byte("cert")
cfg.AuthInfos["user"].ClientKeyData = []byte("key")
cfg.Clusters["cluster1"].Server = "https://server1"
cfg.Clusters["cluster1"].InsecureSkipTLSVerify = true
cfg.Clusters["cluster2"].Server = "https://server2"
cfg.Clusters["cluster2"].CertificateAuthorityData = []byte("ca")
cfg.Contexts["context1"].AuthInfo = "user"
cfg.Contexts["context1"].Cluster = "cluster1"
cfg.Contexts["context1"].Namespace = "namespace1"
cfg.Contexts["context2"].AuthInfo = "user"
cfg.Contexts["context2"].Cluster = "cluster2"
cfg.Contexts["context2"].Namespace = "namespace2"
cfg.CurrentContext = "context1"
cfgData, err := clientcmd.Write(*cfg)
assert.NilError(t, err)
_, err = kcFile.Write(cfgData)
assert.NilError(t, err)
kcFile.Close()
epDefault, err := FromKubeConfig(kcFile.Name(), "", "")
assert.NilError(t, err)
epContext2, err := FromKubeConfig(kcFile.Name(), "context2", "namespace-override")
assert.NilError(t, err)
assert.NilError(t, save(store, epDefault, "embed-default-context"))
assert.NilError(t, save(store, epContext2, "embed-context2"))
rawNoTLSMeta, err := store.GetContextMetadata("raw-notls")
assert.NilError(t, err)
rawNoTLSSkipMeta, err := store.GetContextMetadata("raw-notls-skip")
assert.NilError(t, err)
rawTLSMeta, err := store.GetContextMetadata("raw-tls")
assert.NilError(t, err)
embededDefaultMeta, err := store.GetContextMetadata("embed-default-context")
assert.NilError(t, err)
embededContext2Meta, err := store.GetContextMetadata("embed-context2")
assert.NilError(t, err)
rawNoTLS := EndpointFromContext(rawNoTLSMeta)
rawNoTLSSkip := EndpointFromContext(rawNoTLSSkipMeta)
rawTLS := EndpointFromContext(rawTLSMeta)
embededDefault := EndpointFromContext(embededDefaultMeta)
embededContext2 := EndpointFromContext(embededContext2Meta)
rawNoTLSEP, err := rawNoTLS.WithTLSData(store, "raw-notls")
assert.NilError(t, err)
checkClientConfig(t, store, rawNoTLSEP, "https://test", "test", nil, nil, nil, false)
rawNoTLSSkipEP, err := rawNoTLSSkip.WithTLSData(store, "raw-notls-skip")
assert.NilError(t, err)
checkClientConfig(t, store, rawNoTLSSkipEP, "https://test", "test", nil, nil, nil, true)
rawTLSEP, err := rawTLS.WithTLSData(store, "raw-tls")
assert.NilError(t, err)
checkClientConfig(t, store, rawTLSEP, "https://test", "test", []byte("ca"), []byte("cert"), []byte("key"), true)
embededDefaultEP, err := embededDefault.WithTLSData(store, "embed-default-context")
assert.NilError(t, err)
checkClientConfig(t, store, embededDefaultEP, "https://server1", "namespace1", nil, []byte("cert"), []byte("key"), true)
embededContext2EP, err := embededContext2.WithTLSData(store, "embed-context2")
assert.NilError(t, err)
checkClientConfig(t, store, embededContext2EP, "https://server2", "namespace-override", []byte("ca"), []byte("cert"), []byte("key"), false)
}
func checkClientConfig(t *testing.T, s store.Store, ep Endpoint, server, namespace string, ca, cert, key []byte, skipTLSVerify bool) {
config := ep.KubernetesConfig()
cfg, err := config.ClientConfig()
assert.NilError(t, err)
ns, _, _ := config.Namespace()
assert.Equal(t, server, cfg.Host)
assert.Equal(t, namespace, ns)
assert.DeepEqual(t, ca, cfg.CAData)
assert.DeepEqual(t, cert, cfg.CertData)
assert.DeepEqual(t, key, cfg.KeyData)
assert.Equal(t, skipTLSVerify, cfg.Insecure)
}
func save(s store.Store, ep Endpoint, name string) error {
meta := store.ContextMetadata{
Endpoints: map[string]interface{}{
KubernetesEndpoint: ep.EndpointMeta,
},
Name: name,
}
if err := s.CreateOrUpdateContext(meta); err != nil {
return err
}
return s.ResetContextEndpointTLSMaterial(name, KubernetesEndpoint, ep.TLSData.ToStoreTLSData())
}
func TestSaveLoadGKEConfig(t *testing.T) {
storeDir, err := ioutil.TempDir("", t.Name())
assert.NilError(t, err)
defer os.RemoveAll(storeDir)
store := store.New(storeDir, testStoreCfg)
cfg, err := clientcmd.LoadFromFile("testdata/gke-kubeconfig")
assert.NilError(t, err)
clientCfg := clientcmd.NewDefaultClientConfig(*cfg, &clientcmd.ConfigOverrides{})
expectedCfg, err := clientCfg.ClientConfig()
assert.NilError(t, err)
ep, err := FromKubeConfig("testdata/gke-kubeconfig", "", "")
assert.NilError(t, err)
assert.NilError(t, save(store, ep, "gke-context"))
persistedMetadata, err := store.GetContextMetadata("gke-context")
assert.NilError(t, err)
persistedEPMeta := EndpointFromContext(persistedMetadata)
assert.Check(t, persistedEPMeta != nil)
persistedEP, err := persistedEPMeta.WithTLSData(store, "gke-context")
assert.NilError(t, err)
persistedCfg := persistedEP.KubernetesConfig()
actualCfg, err := persistedCfg.ClientConfig()
assert.NilError(t, err)
assert.DeepEqual(t, expectedCfg.AuthProvider, actualCfg.AuthProvider)
}
func TestSaveLoadEKSConfig(t *testing.T) {
storeDir, err := ioutil.TempDir("", t.Name())
assert.NilError(t, err)
defer os.RemoveAll(storeDir)
store := store.New(storeDir, testStoreCfg)
cfg, err := clientcmd.LoadFromFile("testdata/eks-kubeconfig")
assert.NilError(t, err)
clientCfg := clientcmd.NewDefaultClientConfig(*cfg, &clientcmd.ConfigOverrides{})
expectedCfg, err := clientCfg.ClientConfig()
assert.NilError(t, err)
ep, err := FromKubeConfig("testdata/eks-kubeconfig", "", "")
assert.NilError(t, err)
assert.NilError(t, save(store, ep, "eks-context"))
persistedMetadata, err := store.GetContextMetadata("eks-context")
assert.NilError(t, err)
persistedEPMeta := EndpointFromContext(persistedMetadata)
assert.Check(t, persistedEPMeta != nil)
persistedEP, err := persistedEPMeta.WithTLSData(store, "eks-context")
assert.NilError(t, err)
persistedCfg := persistedEP.KubernetesConfig()
actualCfg, err := persistedCfg.ClientConfig()
assert.NilError(t, err)
assert.DeepEqual(t, expectedCfg.ExecProvider, actualCfg.ExecProvider)
}

View File

@ -0,0 +1,95 @@
package kubernetes
import (
"github.com/docker/cli/cli/context"
"github.com/docker/cli/cli/context/store"
"github.com/docker/cli/kubernetes"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
)
// EndpointMeta is a typed wrapper around a context-store generic endpoint describing
// a Kubernetes endpoint, without TLS data
type EndpointMeta struct {
context.EndpointMetaBase
DefaultNamespace string `json:",omitempty"`
AuthProvider *clientcmdapi.AuthProviderConfig `json:",omitempty"`
Exec *clientcmdapi.ExecConfig `json:",omitempty"`
}
// Endpoint is a typed wrapper around a context-store generic endpoint describing
// a Kubernetes endpoint, with TLS data
type Endpoint struct {
EndpointMeta
TLSData *context.TLSData
}
// WithTLSData loads TLS materials for the endpoint
func (c *EndpointMeta) WithTLSData(s store.Store, contextName string) (Endpoint, error) {
tlsData, err := context.LoadTLSData(s, contextName, KubernetesEndpoint)
if err != nil {
return Endpoint{}, err
}
return Endpoint{
EndpointMeta: *c,
TLSData: tlsData,
}, nil
}
// KubernetesConfig creates the kubernetes client config from the endpoint
func (c *Endpoint) KubernetesConfig() clientcmd.ClientConfig {
cfg := clientcmdapi.NewConfig()
cluster := clientcmdapi.NewCluster()
cluster.Server = c.Host
cluster.InsecureSkipTLSVerify = c.SkipTLSVerify
authInfo := clientcmdapi.NewAuthInfo()
if c.TLSData != nil {
cluster.CertificateAuthorityData = c.TLSData.CA
authInfo.ClientCertificateData = c.TLSData.Cert
authInfo.ClientKeyData = c.TLSData.Key
}
authInfo.AuthProvider = c.AuthProvider
authInfo.Exec = c.Exec
cfg.Clusters["cluster"] = cluster
cfg.AuthInfos["authInfo"] = authInfo
ctx := clientcmdapi.NewContext()
ctx.AuthInfo = "authInfo"
ctx.Cluster = "cluster"
ctx.Namespace = c.DefaultNamespace
cfg.Contexts["context"] = ctx
cfg.CurrentContext = "context"
return clientcmd.NewDefaultClientConfig(*cfg, &clientcmd.ConfigOverrides{})
}
// EndpointFromContext extracts kubernetes endpoint info from current context
func EndpointFromContext(metadata store.ContextMetadata) *EndpointMeta {
ep, ok := metadata.Endpoints[KubernetesEndpoint]
if !ok {
return nil
}
typed, ok := ep.(EndpointMeta)
if !ok {
return nil
}
return &typed
}
// ConfigFromContext resolves a kubernetes client config for the specified context.
// If kubeconfigOverride is specified, use this config file instead of the context defaults.ConfigFromContext
// if command.ContextDockerHost is specified as the context name, fallsback to the default user's kubeconfig file
func ConfigFromContext(name string, s store.Store) (clientcmd.ClientConfig, error) {
ctxMeta, err := s.GetContextMetadata(name)
if err != nil {
return nil, err
}
epMeta := EndpointFromContext(ctxMeta)
if epMeta != nil {
ep, err := epMeta.WithTLSData(s, name)
if err != nil {
return nil, err
}
return ep.KubernetesConfig(), nil
}
// context has no kubernetes endpoint
return kubernetes.NewKubernetesConfig(""), nil
}

View File

@ -0,0 +1,61 @@
package kubernetes
import (
"io/ioutil"
"github.com/docker/cli/cli/context"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
)
// FromKubeConfig creates a Kubernetes endpoint from a Kubeconfig file
func FromKubeConfig(kubeconfig, kubeContext, namespaceOverride string) (Endpoint, error) {
cfg := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
&clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfig},
&clientcmd.ConfigOverrides{CurrentContext: kubeContext, Context: clientcmdapi.Context{Namespace: namespaceOverride}})
ns, _, err := cfg.Namespace()
if err != nil {
return Endpoint{}, err
}
clientcfg, err := cfg.ClientConfig()
if err != nil {
return Endpoint{}, err
}
var ca, key, cert []byte
if ca, err = readFileOrDefault(clientcfg.CAFile, clientcfg.CAData); err != nil {
return Endpoint{}, err
}
if key, err = readFileOrDefault(clientcfg.KeyFile, clientcfg.KeyData); err != nil {
return Endpoint{}, err
}
if cert, err = readFileOrDefault(clientcfg.CertFile, clientcfg.CertData); err != nil {
return Endpoint{}, err
}
var tlsData *context.TLSData
if ca != nil || cert != nil || key != nil {
tlsData = &context.TLSData{
CA: ca,
Cert: cert,
Key: key,
}
}
return Endpoint{
EndpointMeta: EndpointMeta{
EndpointMetaBase: context.EndpointMetaBase{
Host: clientcfg.Host,
SkipTLSVerify: clientcfg.Insecure,
},
DefaultNamespace: ns,
AuthProvider: clientcfg.AuthProvider,
Exec: clientcfg.ExecProvider,
},
TLSData: tlsData,
}, nil
}
func readFileOrDefault(path string, defaultValue []byte) ([]byte, error) {
if path != "" {
return ioutil.ReadFile(path)
}
return defaultValue, nil
}

View File

@ -0,0 +1,23 @@
apiVersion: v1
clusters:
- cluster:
server: https://some-server
name: kubernetes
contexts:
- context:
cluster: kubernetes
user: aws
name: aws
current-context: aws
kind: Config
preferences: {}
users:
- name: aws
user:
exec:
apiVersion: client.authentication.k8s.io/v1alpha1
command: heptio-authenticator-aws
args:
- "token"
- "-i"
- "eks-cf"

View File

@ -0,0 +1,23 @@
apiVersion: v1
clusters:
- cluster:
server: https://some-server
name: gke_sample
contexts:
- context:
cluster: gke_sample
user: gke_sample
name: gke_sample
current-context: gke_sample
kind: Config
preferences: {}
users:
- name: gke_sample
user:
auth-provider:
config:
cmd-args: config config-helper --format=json
cmd-path: /google/google-cloud-sdk/bin/gcloud
expiry-key: '{.credential.token_expiry}'
token-key: '{.credential.access_token}'
name: gcp

21
cli/context/store/doc.go Normal file
View File

@ -0,0 +1,21 @@
// Package store provides a generic way to store credentials to connect to virtually any kind of remote system.
// The term `context` comes from the similar feature in Kubernetes kubectl config files.
//
// Conceptually, a context is a set of metadata and TLS data, that can be used to connect to various endpoints
// of a remote system. TLS data and metadata are stored separately, so that in the future, we will be able to store sensitive
// information in a more secure way, depending on the os we are running on (e.g.: on Windows we could use the user Certificate Store, on Mac OS the user Keychain...).
//
// Current implementation is purely file based with the following structure:
// ${CONTEXT_ROOT}
// - meta/
// - context1/meta.json: contains context medata (key/value pairs) as well as a list of endpoints (themselves containing key/value pair metadata)
// - contexts/can/also/be/folded/like/this/meta.json: same as context1, but for a context named `contexts/can/also/be/folded/like/this`
// - tls/
// - context1/endpoint1/: directory containing TLS data for the endpoint1 in context1
//
// The context store itself has absolutely no knowledge about what a docker or a kubernetes endpoint should contain in term of metadata or TLS config.
// Client code is responsible for generating and parsing endpoint metadata and TLS files.
// The multi-endpoints approach of this package allows to combine many different endpoints in the same "context" (e.g., the Docker CLI
// is able for a single context to define both a docker endpoint and a Kubernetes endpoint for the same cluster, and also specify which
// orchestrator to use by default when deploying a compose stack on this cluster).
package store

View File

@ -0,0 +1,143 @@
package store
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"gotest.tools/assert"
"gotest.tools/assert/cmp"
)
func testMetadata(name string) ContextMetadata {
return ContextMetadata{
Endpoints: map[string]interface{}{
"ep1": endpoint{Foo: "bar"},
},
Metadata: context{Bar: "baz"},
Name: name,
}
}
func TestMetadataGetNotExisting(t *testing.T) {
testDir, err := ioutil.TempDir("", t.Name())
assert.NilError(t, err)
defer os.RemoveAll(testDir)
testee := metadataStore{root: testDir, config: testCfg}
_, err = testee.get("noexist")
assert.Assert(t, IsErrContextDoesNotExist(err))
}
func TestMetadataCreateGetRemove(t *testing.T) {
testDir, err := ioutil.TempDir("", t.Name())
assert.NilError(t, err)
defer os.RemoveAll(testDir)
testee := metadataStore{root: testDir, config: testCfg}
expected2 := ContextMetadata{
Endpoints: map[string]interface{}{
"ep1": endpoint{Foo: "baz"},
"ep2": endpoint{Foo: "bee"},
},
Metadata: context{Bar: "foo"},
Name: "test-context",
}
testMeta := testMetadata("test-context")
err = testee.createOrUpdate(testMeta)
assert.NilError(t, err)
// create a new instance to check it does not depend on some sort of state
testee = metadataStore{root: testDir, config: testCfg}
meta, err := testee.get(contextdirOf("test-context"))
assert.NilError(t, err)
assert.DeepEqual(t, meta, testMeta)
// update
err = testee.createOrUpdate(expected2)
assert.NilError(t, err)
meta, err = testee.get(contextdirOf("test-context"))
assert.NilError(t, err)
assert.DeepEqual(t, meta, expected2)
assert.NilError(t, testee.remove(contextdirOf("test-context")))
assert.NilError(t, testee.remove(contextdirOf("test-context"))) // support duplicate remove
_, err = testee.get(contextdirOf("test-context"))
assert.Assert(t, IsErrContextDoesNotExist(err))
}
func TestMetadataRespectJsonAnnotation(t *testing.T) {
testDir, err := ioutil.TempDir("", t.Name())
assert.NilError(t, err)
defer os.RemoveAll(testDir)
testee := metadataStore{root: testDir, config: testCfg}
assert.NilError(t, testee.createOrUpdate(testMetadata("test")))
bytes, err := ioutil.ReadFile(filepath.Join(testDir, string(contextdirOf("test")), "meta.json"))
assert.NilError(t, err)
assert.Assert(t, cmp.Contains(string(bytes), "a_very_recognizable_field_name"))
assert.Assert(t, cmp.Contains(string(bytes), "another_very_recognizable_field_name"))
}
func TestMetadataList(t *testing.T) {
testDir, err := ioutil.TempDir("", t.Name())
assert.NilError(t, err)
defer os.RemoveAll(testDir)
testee := metadataStore{root: testDir, config: testCfg}
wholeData := []ContextMetadata{
testMetadata("context1"),
testMetadata("context2"),
testMetadata("context3"),
}
for _, s := range wholeData {
err = testee.createOrUpdate(s)
assert.NilError(t, err)
}
data, err := testee.list()
assert.NilError(t, err)
assert.DeepEqual(t, data, wholeData)
}
func TestEmptyConfig(t *testing.T) {
testDir, err := ioutil.TempDir("", t.Name())
assert.NilError(t, err)
defer os.RemoveAll(testDir)
testee := metadataStore{root: testDir}
wholeData := []ContextMetadata{
testMetadata("context1"),
testMetadata("context2"),
testMetadata("context3"),
}
for _, s := range wholeData {
err = testee.createOrUpdate(s)
assert.NilError(t, err)
}
data, err := testee.list()
assert.NilError(t, err)
assert.Equal(t, len(data), len(wholeData))
}
type contextWithEmbedding struct {
embeddedStruct
}
type embeddedStruct struct {
Val string
}
func TestWithEmbedding(t *testing.T) {
testDir, err := ioutil.TempDir("", t.Name())
assert.NilError(t, err)
defer os.RemoveAll(testDir)
testee := metadataStore{root: testDir, config: NewConfig(func() interface{} { return &contextWithEmbedding{} })}
testCtxMeta := contextWithEmbedding{
embeddedStruct: embeddedStruct{
Val: "Hello",
},
}
assert.NilError(t, testee.createOrUpdate(ContextMetadata{Metadata: testCtxMeta, Name: "test"}))
res, err := testee.get(contextdirOf("test"))
assert.NilError(t, err)
assert.Equal(t, testCtxMeta, res.Metadata)
}

View File

@ -0,0 +1,153 @@
package store
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"sort"
"vbom.ml/util/sortorder"
)
const (
metadataDir = "meta"
metaFile = "meta.json"
)
type metadataStore struct {
root string
config Config
}
func (s *metadataStore) contextDir(id contextdir) string {
return filepath.Join(s.root, string(id))
}
func (s *metadataStore) createOrUpdate(meta ContextMetadata) error {
contextDir := s.contextDir(contextdirOf(meta.Name))
if err := os.MkdirAll(contextDir, 0755); err != nil {
return err
}
bytes, err := json.Marshal(&meta)
if err != nil {
return err
}
return ioutil.WriteFile(filepath.Join(contextDir, metaFile), bytes, 0644)
}
func parseTypedOrMap(payload []byte, getter TypeGetter) (interface{}, error) {
if len(payload) == 0 || string(payload) == "null" {
return nil, nil
}
if getter == nil {
var res map[string]interface{}
if err := json.Unmarshal(payload, &res); err != nil {
return nil, err
}
return res, nil
}
typed := getter()
if err := json.Unmarshal(payload, typed); err != nil {
return nil, err
}
return reflect.ValueOf(typed).Elem().Interface(), nil
}
func (s *metadataStore) get(id contextdir) (ContextMetadata, error) {
contextDir := s.contextDir(id)
bytes, err := ioutil.ReadFile(filepath.Join(contextDir, metaFile))
if err != nil {
return ContextMetadata{}, convertContextDoesNotExist(err)
}
var untyped untypedContextMetadata
r := ContextMetadata{
Endpoints: make(map[string]interface{}),
}
if err := json.Unmarshal(bytes, &untyped); err != nil {
return ContextMetadata{}, err
}
r.Name = untyped.Name
if r.Metadata, err = parseTypedOrMap(untyped.Metadata, s.config.contextType); err != nil {
return ContextMetadata{}, err
}
for k, v := range untyped.Endpoints {
if r.Endpoints[k], err = parseTypedOrMap(v, s.config.endpointTypes[k]); err != nil {
return ContextMetadata{}, err
}
}
return r, err
}
func (s *metadataStore) remove(id contextdir) error {
contextDir := s.contextDir(id)
return os.RemoveAll(contextDir)
}
func (s *metadataStore) list() ([]ContextMetadata, error) {
ctxDirs, err := listRecursivelyMetadataDirs(s.root)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
var res []ContextMetadata
for _, dir := range ctxDirs {
c, err := s.get(contextdir(dir))
if err != nil {
return nil, err
}
res = append(res, c)
}
sort.Slice(res, func(i, j int) bool {
return sortorder.NaturalLess(res[i].Name, res[j].Name)
})
return res, nil
}
func isContextDir(path string) bool {
s, err := os.Stat(filepath.Join(path, metaFile))
if err != nil {
return false
}
return !s.IsDir()
}
func listRecursivelyMetadataDirs(root string) ([]string, error) {
fis, err := ioutil.ReadDir(root)
if err != nil {
return nil, err
}
var result []string
for _, fi := range fis {
if fi.IsDir() {
if isContextDir(filepath.Join(root, fi.Name())) {
result = append(result, fi.Name())
}
subs, err := listRecursivelyMetadataDirs(filepath.Join(root, fi.Name()))
if err != nil {
return nil, err
}
for _, s := range subs {
result = append(result, fmt.Sprintf("%s/%s", fi.Name(), s))
}
}
}
return result, nil
}
func convertContextDoesNotExist(err error) error {
if os.IsNotExist(err) {
return &contextDoesNotExistError{}
}
return err
}
type untypedContextMetadata struct {
Metadata json.RawMessage `json:"metadata,omitempty"`
Endpoints map[string]json.RawMessage `json:"endpoints,omitempty"`
Name string `json:"name,omitempty"`
}

340
cli/context/store/store.go Normal file
View File

@ -0,0 +1,340 @@
package store
import (
"archive/tar"
_ "crypto/sha256" // ensure ids can be computed
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"path"
"path/filepath"
"strings"
"github.com/opencontainers/go-digest"
)
// Store provides a context store for easily remembering endpoints configuration
type Store interface {
ListContexts() ([]ContextMetadata, error)
CreateOrUpdateContext(meta ContextMetadata) error
RemoveContext(name string) error
GetContextMetadata(name string) (ContextMetadata, error)
ResetContextTLSMaterial(name string, data *ContextTLSData) error
ResetContextEndpointTLSMaterial(contextName string, endpointName string, data *EndpointTLSData) error
ListContextTLSFiles(name string) (map[string]EndpointFiles, error)
GetContextTLSData(contextName, endpointName, fileName string) ([]byte, error)
GetContextStorageInfo(contextName string) ContextStorageInfo
}
// ContextMetadata contains metadata about a context and its endpoints
type ContextMetadata struct {
Name string `json:",omitempty"`
Metadata interface{} `json:",omitempty"`
Endpoints map[string]interface{} `json:",omitempty"`
}
// ContextStorageInfo contains data about where a given context is stored
type ContextStorageInfo struct {
MetadataPath string
TLSPath string
}
// EndpointTLSData represents tls data for a given endpoint
type EndpointTLSData struct {
Files map[string][]byte
}
// ContextTLSData represents tls data for a whole context
type ContextTLSData struct {
Endpoints map[string]EndpointTLSData
}
// New creates a store from a given directory.
// If the directory does not exist or is empty, initialize it
func New(dir string, cfg Config) Store {
metaRoot := filepath.Join(dir, metadataDir)
tlsRoot := filepath.Join(dir, tlsDir)
return &store{
meta: &metadataStore{
root: metaRoot,
config: cfg,
},
tls: &tlsStore{
root: tlsRoot,
},
}
}
type store struct {
meta *metadataStore
tls *tlsStore
}
func (s *store) ListContexts() ([]ContextMetadata, error) {
return s.meta.list()
}
func (s *store) CreateOrUpdateContext(meta ContextMetadata) error {
return s.meta.createOrUpdate(meta)
}
func (s *store) RemoveContext(name string) error {
id := contextdirOf(name)
if err := s.meta.remove(id); err != nil {
return patchErrContextName(err, name)
}
return patchErrContextName(s.tls.removeAllContextData(id), name)
}
func (s *store) GetContextMetadata(name string) (ContextMetadata, error) {
res, err := s.meta.get(contextdirOf(name))
patchErrContextName(err, name)
return res, err
}
func (s *store) ResetContextTLSMaterial(name string, data *ContextTLSData) error {
id := contextdirOf(name)
if err := s.tls.removeAllContextData(id); err != nil {
return patchErrContextName(err, name)
}
if data == nil {
return nil
}
for ep, files := range data.Endpoints {
for fileName, data := range files.Files {
if err := s.tls.createOrUpdate(id, ep, fileName, data); err != nil {
return patchErrContextName(err, name)
}
}
}
return nil
}
func (s *store) ResetContextEndpointTLSMaterial(contextName string, endpointName string, data *EndpointTLSData) error {
id := contextdirOf(contextName)
if err := s.tls.removeAllEndpointData(id, endpointName); err != nil {
return patchErrContextName(err, contextName)
}
if data == nil {
return nil
}
for fileName, data := range data.Files {
if err := s.tls.createOrUpdate(id, endpointName, fileName, data); err != nil {
return patchErrContextName(err, contextName)
}
}
return nil
}
func (s *store) ListContextTLSFiles(name string) (map[string]EndpointFiles, error) {
res, err := s.tls.listContextData(contextdirOf(name))
return res, patchErrContextName(err, name)
}
func (s *store) GetContextTLSData(contextName, endpointName, fileName string) ([]byte, error) {
res, err := s.tls.getData(contextdirOf(contextName), endpointName, fileName)
return res, patchErrContextName(err, contextName)
}
func (s *store) GetContextStorageInfo(contextName string) ContextStorageInfo {
dir := contextdirOf(contextName)
return ContextStorageInfo{
MetadataPath: s.meta.contextDir(dir),
TLSPath: s.tls.contextDir(dir),
}
}
// Export exports an existing namespace into an opaque data stream
// This stream is actually a tarball containing context metadata and TLS materials, but it does
// not map 1:1 the layout of the context store (don't try to restore it manually without calling store.Import)
func Export(name string, s Store) io.ReadCloser {
reader, writer := io.Pipe()
go func() {
tw := tar.NewWriter(writer)
defer tw.Close()
defer writer.Close()
meta, err := s.GetContextMetadata(name)
if err != nil {
writer.CloseWithError(err)
return
}
metaBytes, err := json.Marshal(&meta)
if err != nil {
writer.CloseWithError(err)
return
}
if err = tw.WriteHeader(&tar.Header{
Name: metaFile,
Mode: 0644,
Size: int64(len(metaBytes)),
}); err != nil {
writer.CloseWithError(err)
return
}
if _, err = tw.Write(metaBytes); err != nil {
writer.CloseWithError(err)
return
}
tlsFiles, err := s.ListContextTLSFiles(name)
if err != nil {
writer.CloseWithError(err)
return
}
if err = tw.WriteHeader(&tar.Header{
Name: "tls",
Mode: 0700,
Size: 0,
Typeflag: tar.TypeDir,
}); err != nil {
writer.CloseWithError(err)
return
}
for endpointName, endpointFiles := range tlsFiles {
if err = tw.WriteHeader(&tar.Header{
Name: path.Join("tls", endpointName),
Mode: 0700,
Size: 0,
Typeflag: tar.TypeDir,
}); err != nil {
writer.CloseWithError(err)
return
}
for _, fileName := range endpointFiles {
data, err := s.GetContextTLSData(name, endpointName, fileName)
if err != nil {
writer.CloseWithError(err)
return
}
if err = tw.WriteHeader(&tar.Header{
Name: path.Join("tls", endpointName, fileName),
Mode: 0600,
Size: int64(len(data)),
}); err != nil {
writer.CloseWithError(err)
return
}
if _, err = tw.Write(data); err != nil {
writer.CloseWithError(err)
return
}
}
}
}()
return reader
}
// Import imports an exported context into a store
func Import(name string, s Store, reader io.Reader) error {
tr := tar.NewReader(reader)
tlsData := ContextTLSData{
Endpoints: map[string]EndpointTLSData{},
}
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
if hdr.Typeflag == tar.TypeDir {
// skip this entry, only taking files into account
continue
}
if hdr.Name == metaFile {
data, err := ioutil.ReadAll(tr)
if err != nil {
return err
}
var meta ContextMetadata
if err := json.Unmarshal(data, &meta); err != nil {
return err
}
meta.Name = name
if err := s.CreateOrUpdateContext(meta); err != nil {
return err
}
} else if strings.HasPrefix(hdr.Name, "tls/") {
relative := strings.TrimPrefix(hdr.Name, "tls/")
parts := strings.SplitN(relative, "/", 2)
if len(parts) != 2 {
return errors.New("archive format is invalid")
}
endpointName := parts[0]
fileName := parts[1]
data, err := ioutil.ReadAll(tr)
if err != nil {
return err
}
if _, ok := tlsData.Endpoints[endpointName]; !ok {
tlsData.Endpoints[endpointName] = EndpointTLSData{
Files: map[string][]byte{},
}
}
tlsData.Endpoints[endpointName].Files[fileName] = data
}
}
return s.ResetContextTLSMaterial(name, &tlsData)
}
type setContextName interface {
setContext(name string)
}
type contextDoesNotExistError struct {
name string
}
func (e *contextDoesNotExistError) Error() string {
return fmt.Sprintf("context %q does not exist", e.name)
}
func (e *contextDoesNotExistError) setContext(name string) {
e.name = name
}
// NotFound satisfies interface github.com/docker/docker/errdefs.ErrNotFound
func (e *contextDoesNotExistError) NotFound() {}
type tlsDataDoesNotExistError struct {
context, endpoint, file string
}
func (e *tlsDataDoesNotExistError) Error() string {
return fmt.Sprintf("tls data for %s/%s/%s does not exist", e.context, e.endpoint, e.file)
}
func (e *tlsDataDoesNotExistError) setContext(name string) {
e.context = name
}
// NotFound satisfies interface github.com/docker/docker/errdefs.ErrNotFound
func (e *tlsDataDoesNotExistError) NotFound() {}
// IsErrContextDoesNotExist checks if the given error is a "context does not exist" condition
func IsErrContextDoesNotExist(err error) bool {
_, ok := err.(*contextDoesNotExistError)
return ok
}
// IsErrTLSDataDoesNotExist checks if the given error is a "context does not exist" condition
func IsErrTLSDataDoesNotExist(err error) bool {
_, ok := err.(*tlsDataDoesNotExistError)
return ok
}
type contextdir string
func contextdirOf(name string) contextdir {
return contextdir(digest.FromString(name).Encoded())
}
func patchErrContextName(err error, name string) error {
if typed, ok := err.(setContextName); ok {
typed.setContext(name)
}
return err
}

View File

@ -0,0 +1,112 @@
package store
import (
"io/ioutil"
"os"
"testing"
"gotest.tools/assert"
)
type endpoint struct {
Foo string `json:"a_very_recognizable_field_name"`
}
type context struct {
Bar string `json:"another_very_recognizable_field_name"`
}
var testCfg = NewConfig(func() interface{} { return &context{} },
EndpointTypeGetter("ep1", func() interface{} { return &endpoint{} }),
EndpointTypeGetter("ep2", func() interface{} { return &endpoint{} }),
)
func TestExportImport(t *testing.T) {
testDir, err := ioutil.TempDir("", t.Name())
assert.NilError(t, err)
defer os.RemoveAll(testDir)
s := New(testDir, testCfg)
err = s.CreateOrUpdateContext(
ContextMetadata{
Endpoints: map[string]interface{}{
"ep1": endpoint{Foo: "bar"},
},
Metadata: context{Bar: "baz"},
Name: "source",
})
assert.NilError(t, err)
err = s.ResetContextEndpointTLSMaterial("source", "ep1", &EndpointTLSData{
Files: map[string][]byte{
"file1": []byte("test-data"),
},
})
assert.NilError(t, err)
r := Export("source", s)
defer r.Close()
err = Import("dest", s, r)
assert.NilError(t, err)
srcMeta, err := s.GetContextMetadata("source")
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("source")
assert.NilError(t, err)
destFileList, err := s.ListContextTLSFiles("dest")
assert.NilError(t, err)
assert.DeepEqual(t, srcFileList, destFileList)
srcData, err := s.GetContextTLSData("source", "ep1", "file1")
assert.NilError(t, err)
assert.Equal(t, "test-data", string(srcData))
destData, err := s.GetContextTLSData("dest", "ep1", "file1")
assert.NilError(t, err)
assert.Equal(t, "test-data", string(destData))
}
func TestRemove(t *testing.T) {
testDir, err := ioutil.TempDir("", t.Name())
assert.NilError(t, err)
defer os.RemoveAll(testDir)
s := New(testDir, testCfg)
err = s.CreateOrUpdateContext(
ContextMetadata{
Endpoints: map[string]interface{}{
"ep1": endpoint{Foo: "bar"},
},
Metadata: context{Bar: "baz"},
Name: "source",
})
assert.NilError(t, err)
assert.NilError(t, s.ResetContextEndpointTLSMaterial("source", "ep1", &EndpointTLSData{
Files: map[string][]byte{
"file1": []byte("test-data"),
},
}))
assert.NilError(t, s.RemoveContext("source"))
_, err = s.GetContextMetadata("source")
assert.Check(t, IsErrContextDoesNotExist(err))
f, err := s.ListContextTLSFiles("source")
assert.NilError(t, err)
assert.Equal(t, 0, len(f))
}
func TestListEmptyStore(t *testing.T) {
testDir, err := ioutil.TempDir("", t.Name())
assert.NilError(t, err)
defer os.RemoveAll(testDir)
store := New(testDir, testCfg)
result, err := store.ListContexts()
assert.NilError(t, err)
assert.Check(t, len(result) == 0)
}
func TestErrHasCorrectContext(t *testing.T) {
testDir, err := ioutil.TempDir("", t.Name())
assert.NilError(t, err)
defer os.RemoveAll(testDir)
store := New(testDir, testCfg)
_, err = store.GetContextMetadata("no-exists")
assert.ErrorContains(t, err, "no-exists")
assert.Check(t, IsErrContextDoesNotExist(err))
}

View File

@ -0,0 +1,38 @@
package store
// TypeGetter is a func used to determine the concrete type of a context or
// endpoint metadata by returning a pointer to an instance of the object
// eg: for a context of type DockerContext, the corresponding TypeGetter should return new(DockerContext)
type TypeGetter func() interface{}
// NamedTypeGetter is a TypeGetter associated with a name
type NamedTypeGetter struct {
name string
typeGetter TypeGetter
}
// EndpointTypeGetter returns a NamedTypeGetter with the spcecified name and getter
func EndpointTypeGetter(name string, getter TypeGetter) NamedTypeGetter {
return NamedTypeGetter{
name: name,
typeGetter: getter,
}
}
// Config is used to configure the metadata marshaler of the context store
type Config struct {
contextType TypeGetter
endpointTypes map[string]TypeGetter
}
// NewConfig creates a config object
func NewConfig(contextType TypeGetter, endpoints ...NamedTypeGetter) Config {
res := Config{
contextType: contextType,
endpointTypes: make(map[string]TypeGetter),
}
for _, e := range endpoints {
res.endpointTypes[e.name] = e.typeGetter
}
return res
}

View File

@ -0,0 +1,99 @@
package store
import (
"io/ioutil"
"os"
"path/filepath"
)
const tlsDir = "tls"
type tlsStore struct {
root string
}
func (s *tlsStore) contextDir(id contextdir) string {
return filepath.Join(s.root, string(id))
}
func (s *tlsStore) endpointDir(contextID contextdir, name string) string {
return filepath.Join(s.root, string(contextID), name)
}
func (s *tlsStore) filePath(contextID contextdir, endpointName, filename string) string {
return filepath.Join(s.root, string(contextID), endpointName, filename)
}
func (s *tlsStore) createOrUpdate(contextID contextdir, endpointName, filename string, data []byte) error {
epdir := s.endpointDir(contextID, endpointName)
parentOfRoot := filepath.Dir(s.root)
if err := os.MkdirAll(parentOfRoot, 0755); err != nil {
return err
}
if err := os.MkdirAll(epdir, 0700); err != nil {
return err
}
return ioutil.WriteFile(s.filePath(contextID, endpointName, filename), data, 0600)
}
func (s *tlsStore) getData(contextID contextdir, endpointName, filename string) ([]byte, error) {
data, err := ioutil.ReadFile(s.filePath(contextID, endpointName, filename))
if err != nil {
return nil, convertTLSDataDoesNotExist(endpointName, filename, err)
}
return data, nil
}
func (s *tlsStore) remove(contextID contextdir, endpointName, filename string) error {
err := os.Remove(s.filePath(contextID, endpointName, filename))
if os.IsNotExist(err) {
return nil
}
return err
}
func (s *tlsStore) removeAllEndpointData(contextID contextdir, endpointName string) error {
return os.RemoveAll(s.endpointDir(contextID, endpointName))
}
func (s *tlsStore) removeAllContextData(contextID contextdir) error {
return os.RemoveAll(s.contextDir(contextID))
}
func (s *tlsStore) listContextData(contextID contextdir) (map[string]EndpointFiles, error) {
epFSs, err := ioutil.ReadDir(s.contextDir(contextID))
if err != nil {
if os.IsNotExist(err) {
return map[string]EndpointFiles{}, nil
}
return nil, err
}
r := make(map[string]EndpointFiles)
for _, epFS := range epFSs {
if epFS.IsDir() {
epDir := s.endpointDir(contextID, epFS.Name())
fss, err := ioutil.ReadDir(epDir)
if err != nil {
return nil, err
}
var files EndpointFiles
for _, fs := range fss {
if !fs.IsDir() {
files = append(files, fs.Name())
}
}
r[epFS.Name()] = files
}
}
return r, nil
}
// EndpointFiles is a slice of strings representing file names
type EndpointFiles []string
func convertTLSDataDoesNotExist(endpoint, file string, err error) error {
if os.IsNotExist(err) {
return &tlsDataDoesNotExistError{endpoint: endpoint, file: file}
}
return err
}

View File

@ -0,0 +1,79 @@
package store
import (
"io/ioutil"
"os"
"testing"
"gotest.tools/assert"
)
func TestTlsCreateUpdateGetRemove(t *testing.T) {
testDir, err := ioutil.TempDir("", "TestTlsCreateUpdateGetRemove")
assert.NilError(t, err)
defer os.RemoveAll(testDir)
testee := tlsStore{root: testDir}
_, err = testee.getData("test-ctx", "test-ep", "test-data")
assert.Equal(t, true, IsErrTLSDataDoesNotExist(err))
err = testee.createOrUpdate("test-ctx", "test-ep", "test-data", []byte("data"))
assert.NilError(t, err)
data, err := testee.getData("test-ctx", "test-ep", "test-data")
assert.NilError(t, err)
assert.Equal(t, string(data), "data")
err = testee.createOrUpdate("test-ctx", "test-ep", "test-data", []byte("data2"))
assert.NilError(t, err)
data, err = testee.getData("test-ctx", "test-ep", "test-data")
assert.NilError(t, err)
assert.Equal(t, string(data), "data2")
err = testee.remove("test-ctx", "test-ep", "test-data")
assert.NilError(t, err)
err = testee.remove("test-ctx", "test-ep", "test-data")
assert.NilError(t, err)
_, err = testee.getData("test-ctx", "test-ep", "test-data")
assert.Equal(t, true, IsErrTLSDataDoesNotExist(err))
}
func TestTlsListAndBatchRemove(t *testing.T) {
testDir, err := ioutil.TempDir("", "TestTlsListAndBatchRemove")
assert.NilError(t, err)
defer os.RemoveAll(testDir)
testee := tlsStore{root: testDir}
all := map[string]EndpointFiles{
"ep1": {"f1", "f2", "f3"},
"ep2": {"f1", "f2", "f3"},
"ep3": {"f1", "f2", "f3"},
}
ep1ep2 := map[string]EndpointFiles{
"ep1": {"f1", "f2", "f3"},
"ep2": {"f1", "f2", "f3"},
}
for name, files := range all {
for _, file := range files {
err = testee.createOrUpdate("test-ctx", name, file, []byte("data"))
assert.NilError(t, err)
}
}
resAll, err := testee.listContextData("test-ctx")
assert.NilError(t, err)
assert.DeepEqual(t, resAll, all)
err = testee.removeAllEndpointData("test-ctx", "ep3")
assert.NilError(t, err)
resEp1ep2, err := testee.listContextData("test-ctx")
assert.NilError(t, err)
assert.DeepEqual(t, resEp1ep2, ep1ep2)
err = testee.removeAllContextData("test-ctx")
assert.NilError(t, err)
resEmpty, err := testee.listContextData("test-ctx")
assert.NilError(t, err)
assert.DeepEqual(t, resEmpty, map[string]EndpointFiles{})
}

98
cli/context/tlsdata.go Normal file
View File

@ -0,0 +1,98 @@
package context
import (
"io/ioutil"
"github.com/docker/cli/cli/context/store"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
const (
caKey = "ca.pem"
certKey = "cert.pem"
keyKey = "key.pem"
)
// TLSData holds ca/cert/key raw data
type TLSData struct {
CA []byte
Key []byte
Cert []byte
}
// ToStoreTLSData converts TLSData to the store representation
func (data *TLSData) ToStoreTLSData() *store.EndpointTLSData {
if data == nil {
return nil
}
result := store.EndpointTLSData{
Files: make(map[string][]byte),
}
if data.CA != nil {
result.Files[caKey] = data.CA
}
if data.Cert != nil {
result.Files[certKey] = data.Cert
}
if data.Key != nil {
result.Files[keyKey] = data.Key
}
return &result
}
// LoadTLSData loads TLS data from the store
func LoadTLSData(s store.Store, contextName, endpointName string) (*TLSData, error) {
tlsFiles, err := s.ListContextTLSFiles(contextName)
if err != nil {
return nil, errors.Wrapf(err, "failed to retrieve context tls files for context %q", contextName)
}
if epTLSFiles, ok := tlsFiles[endpointName]; ok {
var tlsData TLSData
for _, f := range epTLSFiles {
data, err := s.GetContextTLSData(contextName, endpointName, f)
if err != nil {
return nil, errors.Wrapf(err, "failed to retrieve context tls data for file %q of context %q", f, contextName)
}
switch f {
case caKey:
tlsData.CA = data
case certKey:
tlsData.Cert = data
case keyKey:
tlsData.Key = data
default:
logrus.Warnf("unknown file %s in context %s tls bundle", f, contextName)
}
}
return &tlsData, nil
}
return nil, nil
}
// TLSDataFromFiles reads files into a TLSData struct (or returns nil if all paths are empty)
func TLSDataFromFiles(caPath, certPath, keyPath string) (*TLSData, error) {
var (
ca, cert, key []byte
err error
)
if caPath != "" {
if ca, err = ioutil.ReadFile(caPath); err != nil {
return nil, err
}
}
if certPath != "" {
if cert, err = ioutil.ReadFile(certPath); err != nil {
return nil, err
}
}
if keyPath != "" {
if key, err = ioutil.ReadFile(keyPath); err != nil {
return nil, err
}
}
if ca == nil && cert == nil && key == nil {
return nil, nil
}
return &TLSData{CA: ca, Cert: cert, Key: key}, nil
}

View File

@ -37,6 +37,7 @@ type CommonOptions struct {
TLS bool
TLSVerify bool
TLSOptions *tlsconfig.Options
Context string
}
// NewCommonOptions returns a new CommonOptions
@ -70,6 +71,8 @@ func (commonOpts *CommonOptions) InstallFlags(flags *pflag.FlagSet) {
// opts.ValidateHost is not used here, so as to allow connection helpers
hostOpt := opts.NewNamedListOptsRef("hosts", &commonOpts.Hosts, nil)
flags.VarP(hostOpt, "host", "H", "Daemon socket(s) to connect to")
flags.StringVarP(&commonOpts.Context, "context", "c", "",
`Name of the context to use to connect to the daemon (overrides DOCKER_HOST env var and default context set with "docker context use")`)
}
// SetDefaultOptions sets default values for options after flag parsing is

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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