diff --git a/cli/command/stack/kubernetes/convert.go b/cli/command/stack/kubernetes/convert.go index 78d63c2afc..fbddacc4e8 100644 --- a/cli/command/stack/kubernetes/convert.go +++ b/cli/command/stack/kubernetes/convert.go @@ -1,6 +1,8 @@ package kubernetes import ( + "io" + "io/ioutil" "regexp" "strconv" "strings" @@ -36,7 +38,7 @@ func stackFromV1beta1(in *v1beta1.Stack) (stack, error) { return stack{ name: in.ObjectMeta.Name, composeFile: in.Spec.ComposeFile, - spec: fromComposeConfig(cfg), + spec: fromComposeConfig(ioutil.Discard, cfg), }, nil } @@ -67,10 +69,11 @@ func stackToV1beta2(s stack) *v1beta2.Stack { } } -func fromComposeConfig(c *composeTypes.Config) *v1beta2.StackSpec { +func fromComposeConfig(stderr io.Writer, c *composeTypes.Config) *v1beta2.StackSpec { if c == nil { return nil } + warnUnsupportedFeatures(stderr, c) serviceConfigs := make([]v1beta2.ServiceConfig, len(c.Services)) for i, s := range c.Services { serviceConfigs[i] = fromComposeServiceConfig(s) diff --git a/cli/command/stack/kubernetes/deploy.go b/cli/command/stack/kubernetes/deploy.go index d4f5e7ff8d..b708b9f4e6 100644 --- a/cli/command/stack/kubernetes/deploy.go +++ b/cli/command/stack/kubernetes/deploy.go @@ -31,7 +31,7 @@ func RunDeploy(dockerCli *KubeCli, opts options.Deploy) error { if err != nil { return err } - stack, err := stacks.FromCompose(opts.Namespace, *cfg) + stack, err := stacks.FromCompose(dockerCli.Err(), opts.Namespace, cfg) if err != nil { return err } diff --git a/cli/command/stack/kubernetes/stackclient.go b/cli/command/stack/kubernetes/stackclient.go index ffde558b64..686023a03b 100644 --- a/cli/command/stack/kubernetes/stackclient.go +++ b/cli/command/stack/kubernetes/stackclient.go @@ -2,6 +2,7 @@ package kubernetes import ( "fmt" + "io" composetypes "github.com/docker/cli/cli/compose/types" composev1beta1 "github.com/docker/cli/kubernetes/client/clientset/typed/compose/v1beta1" @@ -20,7 +21,7 @@ type stackClient interface { Get(name string) (stack, error) List(opts metav1.ListOptions) ([]stack, error) IsColliding(servicesClient corev1.ServiceInterface, s stack) error - FromCompose(name string, cfg composetypes.Config) (stack, error) + FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (stack, error) } // stackV1Beta1 implements stackClient interface and talks to compose component v1beta1. @@ -103,16 +104,17 @@ func verify(services corev1.ServiceInterface, stackName string, service string) return nil } -func (s *stackV1Beta1) FromCompose(name string, cfg composetypes.Config) (stack, error) { +func (s *stackV1Beta1) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (stack, error) { + st, err := fromCompose(stderr, name, cfg) + if err != nil { + return stack{}, err + } res, err := yaml.Marshal(cfg) if err != nil { return stack{}, err } - return stack{ - name: name, - composeFile: string(res), - spec: fromComposeConfig(&cfg), - }, nil + st.composeFile = string(res) + return st, nil } // stackV1Beta2 implements stackClient interface and talks to compose component v1beta2. @@ -169,9 +171,13 @@ func (s *stackV1Beta2) IsColliding(servicesClient corev1.ServiceInterface, st st return nil } -func (s *stackV1Beta2) FromCompose(name string, cfg composetypes.Config) (stack, error) { +func (s *stackV1Beta2) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (stack, error) { + return fromCompose(stderr, name, cfg) +} + +func fromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (stack, error) { return stack{ name: name, - spec: fromComposeConfig(&cfg), + spec: fromComposeConfig(stderr, cfg), }, nil } diff --git a/cli/command/stack/kubernetes/stackclient_test.go b/cli/command/stack/kubernetes/stackclient_test.go index c42be700fe..86af976936 100644 --- a/cli/command/stack/kubernetes/stackclient_test.go +++ b/cli/command/stack/kubernetes/stackclient_test.go @@ -1,6 +1,7 @@ package kubernetes import ( + "io/ioutil" "testing" composetypes "github.com/docker/cli/cli/compose/types" @@ -9,7 +10,7 @@ import ( func TestFromCompose(t *testing.T) { stackClient := &stackV1Beta1{} - s, err := stackClient.FromCompose("foo", composetypes.Config{ + s, err := stackClient.FromCompose(ioutil.Discard, "foo", &composetypes.Config{ Version: "3.1", Filename: "banana", Services: []composetypes.ServiceConfig{ diff --git a/cli/command/stack/kubernetes/testdata/warnings.golden b/cli/command/stack/kubernetes/testdata/warnings.golden new file mode 100644 index 0000000000..1ee14b0fc9 --- /dev/null +++ b/cli/command/stack/kubernetes/testdata/warnings.golden @@ -0,0 +1,31 @@ +top-level network "global" is ignored +service "front": network "private" is ignored +service "front": update_config.delay is not supported +service "front": update_config.failure_action is not supported +service "front": update_config.monitor is not supported +service "front": update_config.max_failure_ratio is not supported +service "front": restart_policy.delay is ignored +service "front": restart_policy.max_attempts is ignored +service "front": restart_policy.window is ignored +service "front": container_name is deprecated +service "front": expose is deprecated +service "front": build is ignored +service "front": cgroup_parent is ignored +service "front": devices are ignored +service "front": domainname is ignored +service "front": external_links are ignored +service "front": links are ignored +service "front": mac_address is ignored +service "front": network_mode is ignored +service "front": restart is ignored +service "front": security_opt are ignored +service "front": ulimits are ignored +service "front": depends_on are ignored +service "front": credential_spec is ignored +service "front": dns are ignored +service "front": dns_search are ignored +service "front": env_file are ignored +service "front": stop_signal is ignored +service "front": logging is ignored +service "front": volume.propagation is ignored +service "front": volume.nocopy is ignored diff --git a/cli/command/stack/kubernetes/warnings.go b/cli/command/stack/kubernetes/warnings.go new file mode 100644 index 0000000000..eb4598db45 --- /dev/null +++ b/cli/command/stack/kubernetes/warnings.go @@ -0,0 +1,145 @@ +package kubernetes + +import ( + "fmt" + "io" + + composetypes "github.com/docker/cli/cli/compose/types" +) + +func warnUnsupportedFeatures(stderr io.Writer, cfg *composetypes.Config) { + warnForGlobalNetworks(stderr, cfg) + for _, s := range cfg.Services { + warnForServiceNetworks(stderr, s) + warnForUnsupportedDeploymentStrategy(stderr, s) + warnForUnsupportedRestartPolicy(stderr, s) + warnForDeprecatedProperties(stderr, s) + warnForUnsupportedProperties(stderr, s) + } +} + +func warnForGlobalNetworks(stderr io.Writer, config *composetypes.Config) { + for network := range config.Networks { + fmt.Fprintf(stderr, "top-level network %q is ignored\n", network) + } +} + +func warnServicef(stderr io.Writer, service, format string, args ...interface{}) { + fmt.Fprintf(stderr, "service \"%s\": %s\n", service, fmt.Sprintf(format, args...)) +} + +func warnForServiceNetworks(stderr io.Writer, s composetypes.ServiceConfig) { + for network := range s.Networks { + warnServicef(stderr, s.Name, "network %q is ignored", network) + } +} + +func warnForDeprecatedProperties(stderr io.Writer, s composetypes.ServiceConfig) { + if s.ContainerName != "" { + warnServicef(stderr, s.Name, "container_name is deprecated") + } + if len(s.Expose) > 0 { + warnServicef(stderr, s.Name, "expose is deprecated") + } +} + +func warnForUnsupportedDeploymentStrategy(stderr io.Writer, s composetypes.ServiceConfig) { + config := s.Deploy.UpdateConfig + if config == nil { + return + } + if config.Delay != 0 { + warnServicef(stderr, s.Name, "update_config.delay is not supported") + } + if config.FailureAction != "" { + warnServicef(stderr, s.Name, "update_config.failure_action is not supported") + } + if config.Monitor != 0 { + warnServicef(stderr, s.Name, "update_config.monitor is not supported") + } + if config.MaxFailureRatio != 0 { + warnServicef(stderr, s.Name, "update_config.max_failure_ratio is not supported") + } +} + +func warnForUnsupportedRestartPolicy(stderr io.Writer, s composetypes.ServiceConfig) { + policy := s.Deploy.RestartPolicy + if policy == nil { + return + } + + if policy.Delay != nil { + warnServicef(stderr, s.Name, "restart_policy.delay is ignored") + } + if policy.MaxAttempts != nil { + warnServicef(stderr, s.Name, "restart_policy.max_attempts is ignored") + } + if policy.Window != nil { + warnServicef(stderr, s.Name, "restart_policy.window is ignored") + } +} + +func warnForUnsupportedProperties(stderr io.Writer, s composetypes.ServiceConfig) { // nolint: gocyclo + if build := s.Build; build.Context != "" || build.Dockerfile != "" || len(build.Args) > 0 || len(build.Labels) > 0 || len(build.CacheFrom) > 0 || build.Network != "" || build.Target != "" { + warnServicef(stderr, s.Name, "build is ignored") + } + if s.CgroupParent != "" { + warnServicef(stderr, s.Name, "cgroup_parent is ignored") + } + if len(s.Devices) > 0 { + warnServicef(stderr, s.Name, "devices are ignored") + } + if s.DomainName != "" { + warnServicef(stderr, s.Name, "domainname is ignored") + } + if len(s.ExternalLinks) > 0 { + warnServicef(stderr, s.Name, "external_links are ignored") + } + if len(s.Links) > 0 { + warnServicef(stderr, s.Name, "links are ignored") + } + if s.MacAddress != "" { + warnServicef(stderr, s.Name, "mac_address is ignored") + } + if s.NetworkMode != "" { + warnServicef(stderr, s.Name, "network_mode is ignored") + } + if s.Restart != "" { + warnServicef(stderr, s.Name, "restart is ignored") + } + if len(s.SecurityOpt) > 0 { + warnServicef(stderr, s.Name, "security_opt are ignored") + } + if len(s.Ulimits) > 0 { + warnServicef(stderr, s.Name, "ulimits are ignored") + } + if len(s.DependsOn) > 0 { + warnServicef(stderr, s.Name, "depends_on are ignored") + } + if s.CredentialSpec.File != "" { + warnServicef(stderr, s.Name, "credential_spec is ignored") + } + if len(s.DNS) > 0 { + warnServicef(stderr, s.Name, "dns are ignored") + } + if len(s.DNSSearch) > 0 { + warnServicef(stderr, s.Name, "dns_search are ignored") + } + if len(s.EnvFile) > 0 { + warnServicef(stderr, s.Name, "env_file are ignored") + } + if s.StopSignal != "" { + warnServicef(stderr, s.Name, "stop_signal is ignored") + } + if s.Logging != nil { + warnServicef(stderr, s.Name, "logging is ignored") + } + for _, m := range s.Volumes { + if m.Volume != nil && m.Volume.NoCopy { + warnServicef(stderr, s.Name, "volume.nocopy is ignored") + } + if m.Bind != nil && m.Bind.Propagation != "" { + warnServicef(stderr, s.Name, "volume.propagation is ignored") + } + } +} diff --git a/cli/command/stack/kubernetes/warnings_test.go b/cli/command/stack/kubernetes/warnings_test.go new file mode 100644 index 0000000000..b1d911e094 --- /dev/null +++ b/cli/command/stack/kubernetes/warnings_test.go @@ -0,0 +1,78 @@ +package kubernetes + +import ( + "bytes" + "testing" + "time" + + composetypes "github.com/docker/cli/cli/compose/types" + "github.com/gotestyourself/gotestyourself/golden" +) + +func TestWarnings(t *testing.T) { + duration := 5 * time.Second + attempts := uint64(3) + config := &composetypes.Config{ + Version: "3.4", + Services: []composetypes.ServiceConfig{ + { + Name: "front", + Build: composetypes.BuildConfig{ + Context: "ignored", + }, + ContainerName: "ignored", + CgroupParent: "ignored", + CredentialSpec: composetypes.CredentialSpecConfig{File: "ignored"}, + DependsOn: []string{"ignored"}, + Deploy: composetypes.DeployConfig{ + UpdateConfig: &composetypes.UpdateConfig{ + Delay: 5 * time.Second, + FailureAction: "rollback", + Monitor: 10 * time.Second, + MaxFailureRatio: 0.5, + }, + RestartPolicy: &composetypes.RestartPolicy{ + Delay: &duration, + MaxAttempts: &attempts, + Window: &duration, + }, + }, + Devices: []string{"ignored"}, + DNSSearch: []string{"ignored"}, + DNS: []string{"ignored"}, + DomainName: "ignored", + EnvFile: []string{"ignored"}, + Expose: []string{"80"}, + ExternalLinks: []string{"ignored"}, + Image: "dockerdemos/front", + Links: []string{"ignored"}, + Logging: &composetypes.LoggingConfig{Driver: "syslog"}, + MacAddress: "ignored", + Networks: map[string]*composetypes.ServiceNetworkConfig{"private": {}}, + NetworkMode: "ignored", + Restart: "ignored", + SecurityOpt: []string{"ignored"}, + StopSignal: "ignored", + Ulimits: map[string]*composetypes.UlimitsConfig{"nproc": {Hard: 65535}}, + User: "ignored", + Volumes: []composetypes.ServiceVolumeConfig{ + { + Type: "bind", + Bind: &composetypes.ServiceVolumeBind{Propagation: "ignored"}, + }, + { + Type: "volume", + Volume: &composetypes.ServiceVolumeVolume{NoCopy: true}, + }, + }, + }, + }, + Networks: map[string]composetypes.NetworkConfig{ + "global": {}, + }, + } + var buf bytes.Buffer + warnUnsupportedFeatures(&buf, config) + warnings := buf.String() + golden.Assert(t, warnings, "warnings.golden") +} diff --git a/cli/command/stack/swarm/deploy.go b/cli/command/stack/swarm/deploy.go index a308e2ee69..ee6a61784f 100644 --- a/cli/command/stack/swarm/deploy.go +++ b/cli/command/stack/swarm/deploy.go @@ -75,7 +75,7 @@ func pruneServices(ctx context.Context, dockerCli command.Cli, namespace convert oldServices, err := getServices(ctx, client, namespace.Name()) if err != nil { - fmt.Fprintf(dockerCli.Err(), "Failed to list services: %s", err) + fmt.Fprintf(dockerCli.Err(), "Failed to list services: %s\n", err) } pruneServices := []swarm.Service{}