Merge component 'cli' from git@github.com:docker/cli master

This commit is contained in:
GordonTheTurtle
2018-05-28 16:41:35 +00:00
23 changed files with 206 additions and 76 deletions

View File

@ -166,7 +166,7 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
if err != nil {
return errors.Wrap(err, "Experimental field")
}
orchestrator, err := GetOrchestrator(hasExperimental, opts.Common.Orchestrator, cli.configFile.Orchestrator)
orchestrator, err := GetOrchestrator(opts.Common.Orchestrator, cli.configFile.Orchestrator)
if err != nil {
return err
}
@ -244,7 +244,7 @@ type ClientInfo struct {
// HasKubernetes checks if kubernetes orchestrator is enabled
func (c ClientInfo) HasKubernetes() bool {
return c.HasExperimental && (c.Orchestrator == OrchestratorKubernetes || c.Orchestrator == OrchestratorAll)
return c.Orchestrator == OrchestratorKubernetes || c.Orchestrator == OrchestratorAll
}
// HasSwarm checks if swarm orchestrator is enabled

View File

@ -176,28 +176,14 @@ func TestOrchestratorSwitch(t *testing.T) {
{
doc: "default",
configfile: `{
"experimental": "enabled"
}`,
expectedOrchestrator: "swarm",
expectedKubernetes: false,
expectedSwarm: true,
},
{
doc: "kubernetesIsExperimental",
configfile: `{
"experimental": "disabled",
"orchestrator": "kubernetes"
}`,
envOrchestrator: "kubernetes",
flagOrchestrator: "kubernetes",
expectedOrchestrator: "swarm",
expectedKubernetes: false,
expectedSwarm: true,
},
{
doc: "kubernetesConfigFile",
configfile: `{
"experimental": "enabled",
"orchestrator": "kubernetes"
}`,
expectedOrchestrator: "kubernetes",
@ -207,7 +193,6 @@ func TestOrchestratorSwitch(t *testing.T) {
{
doc: "kubernetesEnv",
configfile: `{
"experimental": "enabled"
}`,
envOrchestrator: "kubernetes",
expectedOrchestrator: "kubernetes",
@ -217,7 +202,6 @@ func TestOrchestratorSwitch(t *testing.T) {
{
doc: "kubernetesFlag",
configfile: `{
"experimental": "enabled"
}`,
flagOrchestrator: "kubernetes",
expectedOrchestrator: "kubernetes",
@ -227,7 +211,6 @@ func TestOrchestratorSwitch(t *testing.T) {
{
doc: "allOrchestratorFlag",
configfile: `{
"experimental": "enabled"
}`,
flagOrchestrator: "all",
expectedOrchestrator: "all",
@ -237,7 +220,6 @@ func TestOrchestratorSwitch(t *testing.T) {
{
doc: "envOverridesConfigFile",
configfile: `{
"experimental": "enabled",
"orchestrator": "kubernetes"
}`,
envOrchestrator: "swarm",
@ -248,7 +230,6 @@ func TestOrchestratorSwitch(t *testing.T) {
{
doc: "flagOverridesEnv",
configfile: `{
"experimental": "enabled"
}`,
envOrchestrator: "kubernetes",
flagOrchestrator: "swarm",

View File

@ -38,11 +38,7 @@ func normalize(value string) (Orchestrator, error) {
// GetOrchestrator checks DOCKER_ORCHESTRATOR environment variable and configuration file
// orchestrator value and returns user defined Orchestrator.
func GetOrchestrator(isExperimental bool, flagValue, value string) (Orchestrator, error) {
// Non experimental CLI has kubernetes disabled
if !isExperimental {
return defaultOrchestrator, nil
}
func GetOrchestrator(flagValue, value string) (Orchestrator, error) {
// Check flag
if o, err := normalize(flagValue); o != orchestratorUnset {
return o, err

View File

@ -33,7 +33,6 @@ func NewStackCommand(dockerCli command.Cli) *cobra.Command {
flags := cmd.PersistentFlags()
flags.String("kubeconfig", "", "Kubernetes config file")
flags.SetAnnotation("kubeconfig", "kubernetes", nil)
flags.SetAnnotation("kubeconfig", "experimentalCLI", nil)
return cmd
}

View File

@ -42,7 +42,6 @@ func NewOptions(flags *flag.FlagSet) Options {
func AddNamespaceFlag(flags *flag.FlagSet) {
flags.String("namespace", "", "Kubernetes namespace to use")
flags.SetAnnotation("namespace", "kubernetes", nil)
flags.SetAnnotation("namespace", "experimentalCLI", nil)
}
// WrapCli wraps command.Cli with kubernetes specifics

View File

@ -10,6 +10,7 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/command/stack/options"
"github.com/docker/cli/cli/config/configfile"
"github.com/pkg/errors"
core_v1 "k8s.io/api/core/v1"
apierrs "k8s.io/apimachinery/pkg/api/errors"
@ -19,11 +20,18 @@ import (
// GetStacks lists the kubernetes stacks
func GetStacks(kubeCli *KubeCli, opts options.List) ([]*formatter.Stack, error) {
if opts.AllNamespaces || len(opts.Namespaces) == 0 {
if isAllNamespacesDisabled(kubeCli.ConfigFile().Kubernetes) {
opts.AllNamespaces = true
}
return getStacksWithAllNamespaces(kubeCli, opts)
}
return getStacksWithNamespaces(kubeCli, opts, removeDuplicates(opts.Namespaces))
}
func isAllNamespacesDisabled(kubeCliConfig *configfile.KubernetesConfig) bool {
return kubeCliConfig == nil || kubeCliConfig != nil && kubeCliConfig.AllNamespaces != "disabled"
}
func getStacks(kubeCli *KubeCli, opts options.List) ([]*formatter.Stack, error) {
composeClient, err := kubeCli.composeClient()
if err != nil {

View File

@ -30,10 +30,8 @@ func newListCommand(dockerCli command.Cli) *cobra.Command {
flags.StringVar(&opts.Format, "format", "", "Pretty-print stacks using a Go template")
flags.StringSliceVar(&opts.Namespaces, "namespace", []string{}, "Kubernetes namespaces to use")
flags.SetAnnotation("namespace", "kubernetes", nil)
flags.SetAnnotation("namespace", "experimentalCLI", nil)
flags.BoolVarP(&opts.AllNamespaces, "all-namespaces", "", false, "List stacks from all Kubernetes namespaces")
flags.SetAnnotation("all-namespaces", "kubernetes", nil)
flags.SetAnnotation("all-namespaces", "experimentalCLI", nil)
return cmd
}

View File

@ -2,6 +2,9 @@ package swarm
import (
"context"
"fmt"
"strings"
"unicode"
"github.com/docker/cli/cli/compose/convert"
"github.com/docker/cli/opts"
@ -17,7 +20,7 @@ func getStackFilter(namespace string) filters.Args {
return filter
}
func getServiceFilter(namespace string) filters.Args {
func getStackServiceFilter(namespace string) filters.Args {
return getStackFilter(namespace)
}
@ -33,42 +36,43 @@ func getAllStacksFilter() filters.Args {
return filter
}
func getServices(
ctx context.Context,
apiclient client.APIClient,
namespace string,
) ([]swarm.Service, error) {
return apiclient.ServiceList(
ctx,
types.ServiceListOptions{Filters: getServiceFilter(namespace)})
func getStackServices(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Service, error) {
return apiclient.ServiceList(ctx, types.ServiceListOptions{Filters: getStackServiceFilter(namespace)})
}
func getStackNetworks(
ctx context.Context,
apiclient client.APIClient,
namespace string,
) ([]types.NetworkResource, error) {
return apiclient.NetworkList(
ctx,
types.NetworkListOptions{Filters: getStackFilter(namespace)})
func getStackNetworks(ctx context.Context, apiclient client.APIClient, namespace string) ([]types.NetworkResource, error) {
return apiclient.NetworkList(ctx, types.NetworkListOptions{Filters: getStackFilter(namespace)})
}
func getStackSecrets(
ctx context.Context,
apiclient client.APIClient,
namespace string,
) ([]swarm.Secret, error) {
return apiclient.SecretList(
ctx,
types.SecretListOptions{Filters: getStackFilter(namespace)})
func getStackSecrets(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Secret, error) {
return apiclient.SecretList(ctx, types.SecretListOptions{Filters: getStackFilter(namespace)})
}
func getStackConfigs(
ctx context.Context,
apiclient client.APIClient,
namespace string,
) ([]swarm.Config, error) {
return apiclient.ConfigList(
ctx,
types.ConfigListOptions{Filters: getStackFilter(namespace)})
func getStackConfigs(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Config, error) {
return apiclient.ConfigList(ctx, types.ConfigListOptions{Filters: getStackFilter(namespace)})
}
// validateStackName checks if the provided string is a valid stack name (namespace).
//
// It currently only does a rudimentary check if the string is empty, or consists
// of only whitespace and quoting characters.
func validateStackName(namespace string) error {
v := strings.TrimFunc(namespace, quotesOrWhitespace)
if len(v) == 0 {
return fmt.Errorf("invalid stack name: %q", namespace)
}
return nil
}
func validateStackNames(namespaces []string) error {
for _, ns := range namespaces {
if err := validateStackName(ns); err != nil {
return err
}
}
return nil
}
func quotesOrWhitespace(r rune) bool {
return unicode.IsSpace(r) || r == '"' || r == '\''
}

View File

@ -24,6 +24,9 @@ const (
func RunDeploy(dockerCli command.Cli, opts options.Deploy) error {
ctx := context.Background()
if err := validateStackName(opts.Namespace); err != nil {
return err
}
if err := validateResolveImageFlag(dockerCli, &opts); err != nil {
return err
}
@ -73,7 +76,7 @@ func checkDaemonIsSwarmManager(ctx context.Context, dockerCli command.Cli) error
func pruneServices(ctx context.Context, dockerCli command.Cli, namespace convert.Namespace, services map[string]struct{}) {
client := dockerCli.Client()
oldServices, err := getServices(ctx, client, namespace.Name())
oldServices, err := getStackServices(ctx, client, namespace.Name())
if err != nil {
fmt.Fprintf(dockerCli.Err(), "Failed to list services: %s\n", err)
}

View File

@ -16,6 +16,9 @@ import (
)
func deployBundle(ctx context.Context, dockerCli command.Cli, opts options.Deploy) error {
if err := validateStackName(opts.Namespace); err != nil {
return err
}
bundle, err := loadBundlefile(dockerCli.Err(), opts.Namespace, opts.Bundlefile)
if err != nil {
return err

View File

@ -18,6 +18,9 @@ import (
)
func deployCompose(ctx context.Context, dockerCli command.Cli, opts options.Deploy) error {
if err := validateStackName(opts.Namespace); err != nil {
return err
}
config, err := loader.LoadComposefile(dockerCli, opts)
if err != nil {
return err
@ -209,7 +212,7 @@ func deployServices(
apiClient := dockerCli.Client()
out := dockerCli.Out()
existingServices, err := getServices(ctx, apiClient, namespace.Name())
existingServices, err := getStackServices(ctx, apiClient, namespace.Name())
if err != nil {
return err
}

View File

@ -4,6 +4,7 @@ import (
"context"
"testing"
"github.com/docker/cli/cli/command/stack/options"
"github.com/docker/cli/cli/compose/convert"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
@ -26,6 +27,15 @@ func TestPruneServices(t *testing.T) {
assert.Check(t, is.DeepEqual(buildObjectIDs([]string{objectName("foo", "remove")}), client.removedServices))
}
func TestDeployWithEmptyName(t *testing.T) {
ctx := context.Background()
client := &fakeClient{}
dockerCli := test.NewFakeCli(client)
err := deployCompose(ctx, dockerCli, options.Deploy{Namespace: "' '", Prune: true})
assert.Check(t, is.Error(err, `invalid stack name: "' '"`))
}
// TestServiceUpdateResolveImageChanged tests that the service's
// image digest, and "ForceUpdate" is preserved if the image did not change in
// the compose file

View File

@ -13,19 +13,21 @@ import (
// RunPS is the swarm implementation of docker stack ps
func RunPS(dockerCli command.Cli, opts options.PS) error {
namespace := opts.Namespace
client := dockerCli.Client()
ctx := context.Background()
if err := validateStackName(opts.Namespace); err != nil {
return err
}
filter := getStackFilterFromOpt(opts.Namespace, opts.Filter)
ctx := context.Background()
client := dockerCli.Client()
tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: filter})
if err != nil {
return err
}
if len(tasks) == 0 {
return fmt.Errorf("nothing found in stack: %s", namespace)
return fmt.Errorf("nothing found in stack: %s", opts.Namespace)
}
format := opts.Format

View File

@ -0,0 +1,18 @@
package swarm
import (
"testing"
"github.com/docker/cli/cli/command/stack/options"
"github.com/docker/cli/internal/test"
"github.com/gotestyourself/gotestyourself/assert"
is "github.com/gotestyourself/gotestyourself/assert/cmp"
)
func TestRunPSWithEmptyName(t *testing.T) {
client := &fakeClient{}
dockerCli := test.NewFakeCli(client)
err := RunPS(dockerCli, options.PS{Namespace: "' '"})
assert.Check(t, is.Error(err, `invalid stack name: "' '"`))
}

View File

@ -16,13 +16,16 @@ import (
// RunRemove is the swarm implementation of docker stack remove
func RunRemove(dockerCli command.Cli, opts options.Remove) error {
namespaces := opts.Namespaces
if err := validateStackNames(opts.Namespaces); err != nil {
return err
}
client := dockerCli.Client()
ctx := context.Background()
var errs []string
for _, namespace := range namespaces {
services, err := getServices(ctx, client, namespace)
for _, namespace := range opts.Namespaces {
services, err := getStackServices(ctx, client, namespace)
if err != nil {
return err
}

View File

@ -0,0 +1,18 @@
package swarm
import (
"testing"
"github.com/docker/cli/cli/command/stack/options"
"github.com/docker/cli/internal/test"
"github.com/gotestyourself/gotestyourself/assert"
is "github.com/gotestyourself/gotestyourself/assert/cmp"
)
func TestRunRemoveWithEmptyName(t *testing.T) {
client := &fakeClient{}
dockerCli := test.NewFakeCli(client)
err := RunRemove(dockerCli, options.Remove{Namespaces: []string{"good", "' '", "alsogood"}})
assert.Check(t, is.Error(err, `invalid stack name: "' '"`))
}

View File

@ -14,6 +14,9 @@ import (
// RunServices is the swarm implementation of docker stack services
func RunServices(dockerCli command.Cli, opts options.Services) error {
if err := validateStackName(opts.Namespace); err != nil {
return err
}
ctx := context.Background()
client := dockerCli.Client()

View File

@ -0,0 +1,18 @@
package swarm
import (
"testing"
"github.com/docker/cli/cli/command/stack/options"
"github.com/docker/cli/internal/test"
"github.com/gotestyourself/gotestyourself/assert"
is "github.com/gotestyourself/gotestyourself/assert/cmp"
)
func TestRunServicesWithEmptyName(t *testing.T) {
client := &fakeClient{}
dockerCli := test.NewFakeCli(client)
err := RunServices(dockerCli, options.Services{Namespace: "' '"})
assert.Check(t, is.Error(err, `invalid stack name: "' '"`))
}

View File

@ -108,7 +108,6 @@ func NewVersionCommand(dockerCli command.Cli) *cobra.Command {
flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template")
flags.StringVarP(&opts.kubeConfig, "kubeconfig", "k", "", "Kubernetes config file")
flags.SetAnnotation("kubeconfig", "kubernetes", nil)
flags.SetAnnotation("kubeconfig", "experimentalCLI", nil)
return cmd
}

View File

@ -3,6 +3,7 @@ package configfile
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
@ -46,6 +47,7 @@ type ConfigFile struct {
Proxies map[string]ProxyConfig `json:"proxies,omitempty"`
Experimental string `json:"experimental,omitempty"`
Orchestrator string `json:"orchestrator,omitempty"`
Kubernetes *KubernetesConfig `json:"kubernetes,omitempty"`
}
// ProxyConfig contains proxy configuration settings
@ -56,6 +58,11 @@ type ProxyConfig struct {
FTPProxy string `json:"ftpProxy,omitempty"`
}
// KubernetesConfig contains Kubernetes orchestrator settings
type KubernetesConfig struct {
AllNamespaces string `json:"allNamespaces,omitempty"`
}
// New initializes an empty configuration file for the given filename 'fn'
func New(fn string) *ConfigFile {
return &ConfigFile{
@ -119,7 +126,7 @@ func (configFile *ConfigFile) LoadFromReader(configData io.Reader) error {
ac.ServerAddress = addr
configFile.AuthConfigs[addr] = ac
}
return nil
return checkKubernetesConfiguration(configFile.Kubernetes)
}
// ContainsAuth returns whether there is authentication configured
@ -312,3 +319,17 @@ func (configFile *ConfigFile) GetAllCredentials() (map[string]types.AuthConfig,
func (configFile *ConfigFile) GetFilename() string {
return configFile.Filename
}
func checkKubernetesConfiguration(kubeConfig *KubernetesConfig) error {
if kubeConfig == nil {
return nil
}
switch kubeConfig.AllNamespaces {
case "":
case "enabled":
case "disabled":
default:
return fmt.Errorf("invalid 'kubernetes.allNamespaces' value, should be 'enabled' or 'disabled': %s", kubeConfig.AllNamespaces)
}
return nil
}

View File

@ -371,3 +371,45 @@ func TestGetAllCredentialsCredHelperOverridesDefaultStore(t *testing.T) {
assert.Check(t, is.Equal(1, testCredsStore.(*mockNativeStore).GetAllCallCount))
assert.Check(t, is.Equal(0, testCredHelper.(*mockNativeStore).GetAllCallCount))
}
func TestCheckKubernetesConfigurationRaiseAnErrorOnInvalidValue(t *testing.T) {
testCases := []struct {
name string
config *KubernetesConfig
expectError bool
}{
{
"no kubernetes config is valid",
nil,
false,
},
{
"enabled is valid",
&KubernetesConfig{AllNamespaces: "enabled"},
false,
},
{
"disabled is valid",
&KubernetesConfig{AllNamespaces: "disabled"},
false,
},
{
"empty string is valid",
&KubernetesConfig{AllNamespaces: ""},
false,
},
{
"other value is invalid",
&KubernetesConfig{AllNamespaces: "unknown"},
true,
},
}
for _, test := range testCases {
err := checkKubernetesConfiguration(test.config)
if test.expectError {
assert.Assert(t, err != nil, test.name)
} else {
assert.NilError(t, err, test.name)
}
}
}

View File

@ -55,8 +55,6 @@ func (commonOpts *CommonOptions) InstallFlags(flags *pflag.FlagSet) {
flags.StringVarP(&commonOpts.LogLevel, "log-level", "l", "info", `Set the logging level ("debug"|"info"|"warn"|"error"|"fatal")`)
flags.BoolVar(&commonOpts.TLS, "tls", dockerTLS, "Use TLS; implied by --tlsverify")
flags.BoolVar(&commonOpts.TLSVerify, FlagTLSVerify, dockerTLSVerify, "Use TLS and verify the remote")
flags.StringVar(&commonOpts.Orchestrator, "orchestrator", "", "Orchestrator to use (swarm|kubernetes|all) (experimental)")
flags.SetAnnotation("orchestrator", "experimentalCLI", nil)
// TODO use flag flags.String("identity"}, "i", "", "Path to libtrust key file")

View File

@ -51,6 +51,10 @@ func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command {
flags.StringVar(&opts.ConfigDir, "config", cliconfig.Dir(), "Location of client config files")
opts.Common.InstallFlags(flags)
// Install persistent flags
persistentFlags := cmd.PersistentFlags()
persistentFlags.StringVar(&opts.Common.Orchestrator, "orchestrator", "", "Orchestrator to use (swarm|kubernetes|all)")
setFlagErrorFunc(dockerCli, cmd, flags, opts)
setHelpFunc(dockerCli, cmd, flags, opts)