mirror of https://github.com/docker/cli.git
Merge pull request #1501 from simonferquel/use-context-commands
Fast context switch: commands
This commit is contained in:
commit
48bd4c6deb
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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{}
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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>"
|
||||
}
|
||||
}
|
||||
]
|
|
@ -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)
|
|
@ -0,0 +1,2 @@
|
|||
NAME DESCRIPTION DOCKER ENDPOINT KUBERNETES ENDPOINT ORCHESTRATOR
|
||||
default * Current DOCKER_HOST based configuration https://someswarmserver https://someserver (default) swarm
|
|
@ -0,0 +1,2 @@
|
|||
current
|
||||
other
|
|
@ -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==
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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()))
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 == "" {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package docker
|
||||
|
||||
const (
|
||||
// DockerEndpoint is the name of the docker endpoint in a stored context
|
||||
DockerEndpoint = "docker"
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package kubernetes
|
||||
|
||||
const (
|
||||
// KubernetesEndpoint is the kubernetes endpoint name in a stored context
|
||||
KubernetesEndpoint = "kubernetes"
|
||||
)
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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{})
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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`
|
|
@ -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 -`.
|
|
@ -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
|
||||
```
|
|
@ -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
|
||||
```
|
|
@ -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
|
||||
```
|
|
@ -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)
|
|
@ -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.
|
|
@ -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 |
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue