From f702b722d8e7420820ab1eb1262829e0b590f57a Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 2 Nov 2016 14:57:40 -0400 Subject: [PATCH 01/14] Convert deploy to use a compose-file. Signed-off-by: Daniel Nephin --- command/service/opts.go | 5 +- command/service/update.go | 2 +- command/stack/cmd.go | 1 - command/stack/config.go | 39 ------ command/stack/deploy.go | 253 ++++++++++++++++++++++++++------------ command/stack/opts.go | 9 +- 6 files changed, 185 insertions(+), 124 deletions(-) delete mode 100644 command/stack/config.go diff --git a/command/service/opts.go b/command/service/opts.go index c48c952e0c..2113fdfede 100644 --- a/command/service/opts.go +++ b/command/service/opts.go @@ -297,7 +297,7 @@ func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec { ports, portBindings, _ := nat.ParsePortSpecs(e.ports.GetAll()) for port := range ports { - portConfigs = append(portConfigs, convertPortToPortConfig(port, portBindings)...) + portConfigs = append(portConfigs, ConvertPortToPortConfig(port, portBindings)...) } return &swarm.EndpointSpec{ @@ -306,7 +306,8 @@ func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec { } } -func convertPortToPortConfig( +// ConvertPortToPortConfig converts ports to the swarm type +func ConvertPortToPortConfig( port nat.Port, portBindings map[nat.Port][]nat.PortBinding, ) []swarm.PortConfig { diff --git a/command/service/update.go b/command/service/update.go index 9741f67d54..d1c695d75d 100644 --- a/command/service/update.go +++ b/command/service/update.go @@ -631,7 +631,7 @@ func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error { ports, portBindings, _ := nat.ParsePortSpecs(values) for port := range ports { - newConfigs := convertPortToPortConfig(port, portBindings) + newConfigs := ConvertPortToPortConfig(port, portBindings) for _, entry := range newConfigs { if v, ok := portSet[portConfigToString(&entry)]; ok && v != entry { return fmt.Errorf("conflicting port mapping between %v:%v/%s and %v:%v/%s", entry.PublishedPort, entry.TargetPort, entry.Protocol, v.PublishedPort, v.TargetPort, v.Protocol) diff --git a/command/stack/cmd.go b/command/stack/cmd.go index 4189504403..ff71e0ddfa 100644 --- a/command/stack/cmd.go +++ b/command/stack/cmd.go @@ -19,7 +19,6 @@ func NewStackCommand(dockerCli *command.DockerCli) *cobra.Command { Tags: map[string]string{"experimental": "", "version": "1.25"}, } cmd.AddCommand( - newConfigCommand(dockerCli), newDeployCommand(dockerCli), newListCommand(dockerCli), newRemoveCommand(dockerCli), diff --git a/command/stack/config.go b/command/stack/config.go deleted file mode 100644 index 56e554a86e..0000000000 --- a/command/stack/config.go +++ /dev/null @@ -1,39 +0,0 @@ -package stack - -import ( - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/bundlefile" - "github.com/spf13/cobra" -) - -type configOptions struct { - bundlefile string - namespace string -} - -func newConfigCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts configOptions - - cmd := &cobra.Command{ - Use: "config [OPTIONS] STACK", - Short: "Print the stack configuration", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.namespace = args[0] - return runConfig(dockerCli, opts) - }, - } - - flags := cmd.Flags() - addBundlefileFlag(&opts.bundlefile, flags) - return cmd -} - -func runConfig(dockerCli *command.DockerCli, opts configOptions) error { - bundle, err := loadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile) - if err != nil { - return err - } - return bundlefile.Print(dockerCli.Out(), bundle) -} diff --git a/command/stack/deploy.go b/command/stack/deploy.go index 435a9193b4..c1faa0521c 100644 --- a/command/stack/deploy.go +++ b/command/stack/deploy.go @@ -2,16 +2,22 @@ package stack import ( "fmt" - "strings" + "io/ioutil" + "os" + "time" "github.com/spf13/cobra" "golang.org/x/net/context" + "github.com/aanand/compose-file/loader" + composetypes "github.com/aanand/compose-file/types" "github.com/docker/docker/api/types" + networktypes "github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/bundlefile" + servicecmd "github.com/docker/docker/cli/command/service" + "github.com/docker/go-connections/nat" ) const ( @@ -19,7 +25,7 @@ const ( ) type deployOptions struct { - bundlefile string + composefile string namespace string sendRegistryAuth bool } @@ -30,63 +36,69 @@ func newDeployCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "deploy [OPTIONS] STACK", Aliases: []string{"up"}, - Short: "Create and update a stack from a Distributed Application Bundle (DAB)", + Short: "Deploy a new stack or update an existing stack", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts.namespace = strings.TrimSuffix(args[0], ".dab") + opts.namespace = args[0] return runDeploy(dockerCli, opts) }, Tags: map[string]string{"experimental": "", "version": "1.25"}, } flags := cmd.Flags() - addBundlefileFlag(&opts.bundlefile, flags) + addComposefileFlag(&opts.composefile, flags) addRegistryAuthFlag(&opts.sendRegistryAuth, flags) return cmd } func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error { - bundle, err := loadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile) + configDetails, err := getConfigDetails(opts) if err != nil { return err } - info, err := dockerCli.Client().Info(context.Background()) + config, err := loader.Load(configDetails) if err != nil { return err } - if !info.Swarm.ControlAvailable { - return fmt.Errorf("This node is not a swarm manager. Use \"docker swarm init\" or \"docker swarm join\" to connect this node to swarm and try again.") - } - networks := getUniqueNetworkNames(bundle.Services) ctx := context.Background() - - if err := updateNetworks(ctx, dockerCli, networks, opts.namespace); err != nil { + if err := createNetworks(ctx, dockerCli, config.Networks, opts.namespace); err != nil { return err } - return deployServices(ctx, dockerCli, bundle.Services, opts.namespace, opts.sendRegistryAuth) + return deployServices(ctx, dockerCli, config, opts.namespace, opts.sendRegistryAuth) } -func getUniqueNetworkNames(services map[string]bundlefile.Service) []string { - networkSet := make(map[string]bool) - for _, service := range services { - for _, network := range service.Networks { - networkSet[network] = true - } +func getConfigDetails(opts deployOptions) (composetypes.ConfigDetails, error) { + var details composetypes.ConfigDetails + var err error + + details.WorkingDir, err = os.Getwd() + if err != nil { + return details, err } - networks := []string{} - for network := range networkSet { - networks = append(networks, network) + configFile, err := getConfigFile(opts.composefile) + if err != nil { + return details, err } - return networks + // TODO: support multiple files + details.ConfigFiles = []composetypes.ConfigFile{*configFile} + return details, nil } -func updateNetworks( +func getConfigFile(filename string) (*composetypes.ConfigFile, error) { + bytes, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + return loader.ParseYAML(bytes, filename) +} + +func createNetworks( ctx context.Context, dockerCli *command.DockerCli, - networks []string, + networks map[string]composetypes.NetworkConfig, namespace string, ) error { client := dockerCli.Client() @@ -101,17 +113,34 @@ func updateNetworks( existingNetworkMap[network.Name] = network } - createOpts := types.NetworkCreate{ - Labels: getStackLabels(namespace, nil), - Driver: defaultNetworkDriver, - } + for internalName, network := range networks { + if network.ExternalName != "" { + continue + } - for _, internalName := range networks { name := fmt.Sprintf("%s_%s", namespace, internalName) - if _, exists := existingNetworkMap[name]; exists { continue } + + createOpts := types.NetworkCreate{ + // TODO: support network labels from compose file + Labels: getStackLabels(namespace, nil), + Driver: network.Driver, + Options: network.DriverOpts, + } + + if network.Ipam.Driver != "" { + createOpts.IPAM = &networktypes.IPAM{ + Driver: network.Ipam.Driver, + } + } + // TODO: IPAMConfig.Config + + if createOpts.Driver == "" { + createOpts.Driver = defaultNetworkDriver + } + fmt.Fprintf(dockerCli.Out(), "Creating network %s\n", name) if _, err := client.NetworkCreate(ctx, name, createOpts); err != nil { return err @@ -120,12 +149,17 @@ func updateNetworks( return nil } -func convertNetworks(networks []string, namespace string, name string) []swarm.NetworkAttachmentConfig { +func convertNetworks( + networks map[string]*composetypes.ServiceNetworkConfig, + namespace string, + name string, +) []swarm.NetworkAttachmentConfig { nets := []swarm.NetworkAttachmentConfig{} - for _, network := range networks { + for networkName, network := range networks { nets = append(nets, swarm.NetworkAttachmentConfig{ - Target: namespace + "_" + network, - Aliases: []string{name}, + // TODO: only do this name mangling in one function + Target: namespace + "_" + networkName, + Aliases: append(network.Aliases, name), }) } return nets @@ -134,12 +168,14 @@ func convertNetworks(networks []string, namespace string, name string) []swarm.N func deployServices( ctx context.Context, dockerCli *command.DockerCli, - services map[string]bundlefile.Service, + config *composetypes.Config, namespace string, sendAuth bool, ) error { apiClient := dockerCli.Client() out := dockerCli.Out() + services := config.Services + volumes := config.Volumes existingServices, err := getServices(ctx, apiClient, namespace) if err != nil { @@ -151,46 +187,12 @@ func deployServices( existingServiceMap[service.Spec.Name] = service } - for internalName, service := range services { - name := fmt.Sprintf("%s_%s", namespace, internalName) + for _, service := range services { + name := fmt.Sprintf("%s_%s", namespace, service.Name) - var ports []swarm.PortConfig - for _, portSpec := range service.Ports { - ports = append(ports, swarm.PortConfig{ - Protocol: swarm.PortConfigProtocol(portSpec.Protocol), - TargetPort: portSpec.Port, - }) - } - - serviceSpec := swarm.ServiceSpec{ - Annotations: swarm.Annotations{ - Name: name, - Labels: getStackLabels(namespace, service.Labels), - }, - TaskTemplate: swarm.TaskSpec{ - ContainerSpec: swarm.ContainerSpec{ - Image: service.Image, - Command: service.Command, - Args: service.Args, - Env: service.Env, - // Service Labels will not be copied to Containers - // automatically during the deployment so we apply - // it here. - Labels: getStackLabels(namespace, nil), - }, - }, - EndpointSpec: &swarm.EndpointSpec{ - Ports: ports, - }, - Networks: convertNetworks(service.Networks, namespace, internalName), - } - - cspec := &serviceSpec.TaskTemplate.ContainerSpec - if service.WorkingDir != nil { - cspec.Dir = *service.WorkingDir - } - if service.User != nil { - cspec.User = *service.User + serviceSpec, err := convertService(namespace, service, volumes) + if err != nil { + return err } encodedAuth := "" @@ -234,3 +236,100 @@ func deployServices( return nil } + +func convertService( + namespace string, + service composetypes.ServiceConfig, + volumes map[string]composetypes.VolumeConfig, +) (swarm.ServiceSpec, error) { + // TODO: remove this duplication + name := fmt.Sprintf("%s_%s", namespace, service.Name) + + endpoint, err := convertEndpointSpec(service.Ports) + if err != nil { + return swarm.ServiceSpec{}, err + } + + mode, err := convertDeployMode(service.Deploy.Mode, service.Deploy.Replicas) + if err != nil { + return swarm.ServiceSpec{}, err + } + + serviceSpec := swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: name, + Labels: getStackLabels(namespace, service.Labels), + }, + TaskTemplate: swarm.TaskSpec{ + ContainerSpec: swarm.ContainerSpec{ + Image: service.Image, + Command: service.Entrypoint, + Args: service.Command, + Env: convertEnvironment(service.Environment), + Labels: getStackLabels(namespace, service.Deploy.Labels), + Dir: service.WorkingDir, + User: service.User, + }, + Placement: &swarm.Placement{ + Constraints: service.Deploy.Placement.Constraints, + }, + }, + EndpointSpec: endpoint, + Mode: mode, + Networks: convertNetworks(service.Networks, namespace, service.Name), + } + + if service.StopGracePeriod != nil { + stopGrace, err := time.ParseDuration(*service.StopGracePeriod) + if err != nil { + return swarm.ServiceSpec{}, err + } + serviceSpec.TaskTemplate.ContainerSpec.StopGracePeriod = &stopGrace + } + + // TODO: convert mounts + return serviceSpec, nil +} + +func convertEndpointSpec(source []string) (*swarm.EndpointSpec, error) { + portConfigs := []swarm.PortConfig{} + ports, portBindings, err := nat.ParsePortSpecs(source) + if err != nil { + return nil, err + } + + for port := range ports { + portConfigs = append( + portConfigs, + servicecmd.ConvertPortToPortConfig(port, portBindings)...) + } + + return &swarm.EndpointSpec{Ports: portConfigs}, nil +} + +func convertEnvironment(source map[string]string) []string { + var output []string + + for name, value := range source { + output = append(output, fmt.Sprintf("%s=%s", name, value)) + } + + return output +} + +func convertDeployMode(mode string, replicas uint64) (swarm.ServiceMode, error) { + serviceMode := swarm.ServiceMode{} + + switch mode { + case "global": + if replicas != 0 { + return serviceMode, fmt.Errorf("replicas can only be used with replicated mode") + } + serviceMode.Global = &swarm.GlobalService{} + case "replicated": + serviceMode.Replicated = &swarm.ReplicatedService{Replicas: &replicas} + default: + return serviceMode, fmt.Errorf("Unknown mode: %s", mode) + } + return serviceMode, nil +} diff --git a/command/stack/opts.go b/command/stack/opts.go index 5f2d8b5d0a..c2cc0d1e70 100644 --- a/command/stack/opts.go +++ b/command/stack/opts.go @@ -9,11 +9,12 @@ import ( "github.com/spf13/pflag" ) +func addComposefileFlag(opt *string, flags *pflag.FlagSet) { + flags.StringVar(opt, "compose-file", "", "Path to a Compose file") +} + func addBundlefileFlag(opt *string, flags *pflag.FlagSet) { - flags.StringVar( - opt, - "file", "", - "Path to a Distributed Application Bundle file (Default: STACK.dab)") + flags.StringVar(opt, "bundle-file", "", "Path to a Distributed Application Bundle file") } func addRegistryAuthFlag(opt *bool, flags *pflag.FlagSet) { From a9fc9b60feba5a6962e5583ca0a3a428c6c58b0e Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 25 Oct 2016 14:41:45 -0700 Subject: [PATCH 02/14] Add support for service-level 'volumes' key Support volume driver + options Support external volumes Support hostname in Compose file Signed-off-by: Aanand Prasad --- command/stack/deploy.go | 108 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 99 insertions(+), 9 deletions(-) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index c1faa0521c..96bd175450 100644 --- a/command/stack/deploy.go +++ b/command/stack/deploy.go @@ -4,6 +4,7 @@ import ( "fmt" "io/ioutil" "os" + "strings" "time" "github.com/spf13/cobra" @@ -12,6 +13,7 @@ import ( "github.com/aanand/compose-file/loader" composetypes "github.com/aanand/compose-file/types" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/mount" networktypes "github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" @@ -92,7 +94,14 @@ func getConfigFile(filename string) (*composetypes.ConfigFile, error) { if err != nil { return nil, err } - return loader.ParseYAML(bytes, filename) + config, err := loader.ParseYAML(bytes) + if err != nil { + return nil, err + } + return &composetypes.ConfigFile{ + Filename: filename, + Config: config, + }, nil } func createNetworks( @@ -114,7 +123,7 @@ func createNetworks( } for internalName, network := range networks { - if network.ExternalName != "" { + if network.External.Name != "" { continue } @@ -165,6 +174,80 @@ func convertNetworks( return nets } +func convertVolumes( + serviceVolumes []string, + stackVolumes map[string]composetypes.VolumeConfig, + namespace string, +) ([]mount.Mount, error) { + var mounts []mount.Mount + + for _, volumeString := range serviceVolumes { + var ( + source, target string + mountType mount.Type + readOnly bool + volumeOptions *mount.VolumeOptions + ) + + // TODO: split Windows path mappings properly + parts := strings.SplitN(volumeString, ":", 3) + + if len(parts) == 3 { + source = parts[0] + target = parts[1] + if parts[2] == "ro" { + readOnly = true + } + } else if len(parts) == 2 { + source = parts[0] + target = parts[1] + } else if len(parts) == 1 { + target = parts[0] + } + + // TODO: catch Windows paths here + if strings.HasPrefix(source, "/") { + mountType = mount.TypeBind + } else { + mountType = mount.TypeVolume + + stackVolume, exists := stackVolumes[source] + if !exists { + // TODO: better error message (include service name) + return nil, fmt.Errorf("Undefined volume: %s", source) + } + + if stackVolume.External.Name != "" { + source = stackVolume.External.Name + } else { + volumeOptions = &mount.VolumeOptions{ + Labels: stackVolume.Labels, + } + + if stackVolume.Driver != "" { + volumeOptions.DriverConfig = &mount.Driver{ + Name: stackVolume.Driver, + Options: stackVolume.DriverOpts, + } + } + + // TODO: remove this duplication + source = fmt.Sprintf("%s_%s", namespace, source) + } + } + + mounts = append(mounts, mount.Mount{ + Type: mountType, + Source: source, + Target: target, + ReadOnly: readOnly, + VolumeOptions: volumeOptions, + }) + } + + return mounts, nil +} + func deployServices( ctx context.Context, dockerCli *command.DockerCli, @@ -255,6 +338,11 @@ func convertService( return swarm.ServiceSpec{}, err } + mounts, err := convertVolumes(service.Volumes, volumes, namespace) + if err != nil { + return swarm.ServiceSpec{}, err + } + serviceSpec := swarm.ServiceSpec{ Annotations: swarm.Annotations{ Name: name, @@ -262,13 +350,15 @@ func convertService( }, TaskTemplate: swarm.TaskSpec{ ContainerSpec: swarm.ContainerSpec{ - Image: service.Image, - Command: service.Entrypoint, - Args: service.Command, - Env: convertEnvironment(service.Environment), - Labels: getStackLabels(namespace, service.Deploy.Labels), - Dir: service.WorkingDir, - User: service.User, + Image: service.Image, + Command: service.Entrypoint, + Args: service.Command, + Hostname: service.Hostname, + Env: convertEnvironment(service.Environment), + Labels: getStackLabels(namespace, service.Deploy.Labels), + Dir: service.WorkingDir, + User: service.User, + Mounts: mounts, }, Placement: &swarm.Placement{ Constraints: service.Deploy.Placement.Constraints, From e1b96b6447d3c64fce5d3b92508aac7bb504f3ea Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 28 Oct 2016 17:30:20 -0400 Subject: [PATCH 03/14] Add swarmkit fields to stack service. Signed-off-by: Daniel Nephin --- command/stack/deploy.go | 118 ++++++++++++++++++++++++++++++++-------- 1 file changed, 95 insertions(+), 23 deletions(-) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index 96bd175450..e72abcc8cc 100644 --- a/command/stack/deploy.go +++ b/command/stack/deploy.go @@ -5,7 +5,6 @@ import ( "io/ioutil" "os" "strings" - "time" "github.com/spf13/cobra" "golang.org/x/net/context" @@ -19,6 +18,8 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" servicecmd "github.com/docker/docker/cli/command/service" + runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/docker/docker/opts" "github.com/docker/go-connections/nat" ) @@ -343,23 +344,37 @@ func convertService( return swarm.ServiceSpec{}, err } + resources, err := convertResources(service.Deploy.Resources) + if err != nil { + return swarm.ServiceSpec{}, err + } + + restartPolicy, err := convertRestartPolicy( + service.Restart, service.Deploy.RestartPolicy) + if err != nil { + return swarm.ServiceSpec{}, err + } + serviceSpec := swarm.ServiceSpec{ Annotations: swarm.Annotations{ Name: name, - Labels: getStackLabels(namespace, service.Labels), + Labels: getStackLabels(namespace, service.Deploy.Labels), }, TaskTemplate: swarm.TaskSpec{ ContainerSpec: swarm.ContainerSpec{ - Image: service.Image, - Command: service.Entrypoint, - Args: service.Command, - Hostname: service.Hostname, - Env: convertEnvironment(service.Environment), - Labels: getStackLabels(namespace, service.Deploy.Labels), - Dir: service.WorkingDir, - User: service.User, - Mounts: mounts, + Image: service.Image, + Command: service.Entrypoint, + Args: service.Command, + Hostname: service.Hostname, + Env: convertEnvironment(service.Environment), + Labels: getStackLabels(namespace, service.Labels), + Dir: service.WorkingDir, + User: service.User, + Mounts: mounts, + StopGracePeriod: service.StopGracePeriod, }, + Resources: resources, + RestartPolicy: restartPolicy, Placement: &swarm.Placement{ Constraints: service.Deploy.Placement.Constraints, }, @@ -367,20 +382,77 @@ func convertService( EndpointSpec: endpoint, Mode: mode, Networks: convertNetworks(service.Networks, namespace, service.Name), + UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig), } - if service.StopGracePeriod != nil { - stopGrace, err := time.ParseDuration(*service.StopGracePeriod) - if err != nil { - return swarm.ServiceSpec{}, err - } - serviceSpec.TaskTemplate.ContainerSpec.StopGracePeriod = &stopGrace - } - - // TODO: convert mounts return serviceSpec, nil } +func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) { + // TODO: log if restart is being ignored + if source == nil { + policy, err := runconfigopts.ParseRestartPolicy(restart) + if err != nil { + return nil, err + } + // TODO: is this an accurate convertion? + switch { + case policy.IsNone(), policy.IsAlways(), policy.IsUnlessStopped(): + return nil, nil + case policy.IsOnFailure(): + attempts := uint64(policy.MaximumRetryCount) + return &swarm.RestartPolicy{ + Condition: swarm.RestartPolicyConditionOnFailure, + MaxAttempts: &attempts, + }, nil + } + } + return &swarm.RestartPolicy{ + Condition: swarm.RestartPolicyCondition(source.Condition), + Delay: source.Delay, + MaxAttempts: source.MaxAttempts, + Window: source.Window, + }, nil +} + +func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig { + if source == nil { + return nil + } + return &swarm.UpdateConfig{ + Parallelism: source.Parallelism, + Delay: source.Delay, + FailureAction: source.FailureAction, + Monitor: source.Monitor, + MaxFailureRatio: source.MaxFailureRatio, + } +} + +func convertResources(source composetypes.Resources) (*swarm.ResourceRequirements, error) { + resources := &swarm.ResourceRequirements{} + if source.Limits != nil { + cpus, err := opts.ParseCPUs(source.Limits.NanoCPUs) + if err != nil { + return nil, err + } + resources.Limits = &swarm.Resources{ + NanoCPUs: cpus, + MemoryBytes: int64(source.Limits.MemoryBytes), + } + } + if source.Reservations != nil { + cpus, err := opts.ParseCPUs(source.Reservations.NanoCPUs) + if err != nil { + return nil, err + } + resources.Reservations = &swarm.Resources{ + NanoCPUs: cpus, + MemoryBytes: int64(source.Reservations.MemoryBytes), + } + } + return resources, nil +} + func convertEndpointSpec(source []string) (*swarm.EndpointSpec, error) { portConfigs := []swarm.PortConfig{} ports, portBindings, err := nat.ParsePortSpecs(source) @@ -407,17 +479,17 @@ func convertEnvironment(source map[string]string) []string { return output } -func convertDeployMode(mode string, replicas uint64) (swarm.ServiceMode, error) { +func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) { serviceMode := swarm.ServiceMode{} switch mode { case "global": - if replicas != 0 { + if replicas != nil { return serviceMode, fmt.Errorf("replicas can only be used with replicated mode") } serviceMode.Global = &swarm.GlobalService{} case "replicated": - serviceMode.Replicated = &swarm.ReplicatedService{Replicas: &replicas} + serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas} default: return serviceMode, fmt.Errorf("Unknown mode: %s", mode) } From dfab8f2bd42e6ca94fb3ad06b800402abd927198 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 31 Oct 2016 12:43:47 -0700 Subject: [PATCH 04/14] Handle unsupported, deprecated and forbidden properties Signed-off-by: Aanand Prasad --- command/stack/deploy.go | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index e72abcc8cc..6a15609231 100644 --- a/command/stack/deploy.go +++ b/command/stack/deploy.go @@ -4,6 +4,7 @@ import ( "fmt" "io/ioutil" "os" + "sort" "strings" "github.com/spf13/cobra" @@ -62,9 +63,26 @@ func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error { config, err := loader.Load(configDetails) if err != nil { + if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok { + return fmt.Errorf("Compose file contains unsupported options:\n\n%s\n", + propertyWarnings(fpe.Properties)) + } + return err } + unsupportedProperties := loader.GetUnsupportedProperties(configDetails) + if len(unsupportedProperties) > 0 { + fmt.Printf("Ignoring unsupported options: %s\n\n", + strings.Join(unsupportedProperties, ", ")) + } + + deprecatedProperties := loader.GetDeprecatedProperties(configDetails) + if len(deprecatedProperties) > 0 { + fmt.Printf("Ignoring deprecated options:\n\n%s\n\n", + propertyWarnings(deprecatedProperties)) + } + ctx := context.Background() if err := createNetworks(ctx, dockerCli, config.Networks, opts.namespace); err != nil { return err @@ -72,6 +90,15 @@ func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error { return deployServices(ctx, dockerCli, config, opts.namespace, opts.sendRegistryAuth) } +func propertyWarnings(properties map[string]string) string { + var msgs []string + for name, description := range properties { + msgs = append(msgs, fmt.Sprintf("%s: %s", name, description)) + } + sort.Strings(msgs) + return strings.Join(msgs, "\n\n") +} + func getConfigDetails(opts deployOptions) (composetypes.ConfigDetails, error) { var details composetypes.ConfigDetails var err error @@ -407,10 +434,11 @@ func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (* }, nil } } + attempts := uint64(*source.MaxAttempts) return &swarm.RestartPolicy{ Condition: swarm.RestartPolicyCondition(source.Condition), Delay: source.Delay, - MaxAttempts: source.MaxAttempts, + MaxAttempts: &attempts, Window: source.Window, }, nil } From 25c93d4ebb906b29736b63dc5bf9aff53ff682b8 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 2 Nov 2016 13:10:34 +0000 Subject: [PATCH 05/14] Default to replicated mode Signed-off-by: Aanand Prasad --- command/stack/deploy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index 6a15609231..83d55324de 100644 --- a/command/stack/deploy.go +++ b/command/stack/deploy.go @@ -516,7 +516,7 @@ func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) return serviceMode, fmt.Errorf("replicas can only be used with replicated mode") } serviceMode.Global = &swarm.GlobalService{} - case "replicated": + case "replicated", "": serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas} default: return serviceMode, fmt.Errorf("Unknown mode: %s", mode) From ae8f00182973637c07c7bc852ee9c401b2c9ba91 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 2 Nov 2016 12:19:37 -0400 Subject: [PATCH 06/14] Send warnings to stderr. Signed-off-by: Daniel Nephin --- command/stack/deploy.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index 83d55324de..b92662c3c5 100644 --- a/command/stack/deploy.go +++ b/command/stack/deploy.go @@ -73,13 +73,13 @@ func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error { unsupportedProperties := loader.GetUnsupportedProperties(configDetails) if len(unsupportedProperties) > 0 { - fmt.Printf("Ignoring unsupported options: %s\n\n", + fmt.Fprintf(dockerCli.Err(), "Ignoring unsupported options: %s\n\n", strings.Join(unsupportedProperties, ", ")) } deprecatedProperties := loader.GetDeprecatedProperties(configDetails) if len(deprecatedProperties) > 0 { - fmt.Printf("Ignoring deprecated options:\n\n%s\n\n", + fmt.Fprintf(dockerCli.Err(), "Ignoring deprecated options:\n\n%s\n\n", propertyWarnings(deprecatedProperties)) } @@ -434,11 +434,10 @@ func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (* }, nil } } - attempts := uint64(*source.MaxAttempts) return &swarm.RestartPolicy{ Condition: swarm.RestartPolicyCondition(source.Condition), Delay: source.Delay, - MaxAttempts: &attempts, + MaxAttempts: source.MaxAttempts, Window: source.Window, }, nil } From d89cb4c62fa3c7422bf9a7f0b81f8e1dbbfc512a Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 3 Nov 2016 17:40:48 -0600 Subject: [PATCH 07/14] Always use a default network if no other networks are set. also add network labels. Signed-off-by: Daniel Nephin --- command/stack/deploy.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index b92662c3c5..bb3e73e6e1 100644 --- a/command/stack/deploy.go +++ b/command/stack/deploy.go @@ -150,6 +150,9 @@ func createNetworks( existingNetworkMap[network.Name] = network } + // TODO: only add default network if it's used + networks["default"] = composetypes.NetworkConfig{} + for internalName, network := range networks { if network.External.Name != "" { continue @@ -161,8 +164,7 @@ func createNetworks( } createOpts := types.NetworkCreate{ - // TODO: support network labels from compose file - Labels: getStackLabels(namespace, nil), + Labels: getStackLabels(namespace, network.Labels), Driver: network.Driver, Options: network.DriverOpts, } @@ -191,6 +193,16 @@ func convertNetworks( namespace string, name string, ) []swarm.NetworkAttachmentConfig { + if len(networks) == 0 { + return []swarm.NetworkAttachmentConfig{ + { + // TODO: only do this name mangling in one function + Target: namespace + "_" + "default", + Aliases: []string{name}, + }, + } + } + nets := []swarm.NetworkAttachmentConfig{} for networkName, network := range networks { nets = append(nets, swarm.NetworkAttachmentConfig{ From ef845be6a52cdc93900885cffad48e62ccc4f385 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 3 Nov 2016 17:50:03 -0600 Subject: [PATCH 08/14] Remove duplication of name mangling. Signed-off-by: Daniel Nephin --- command/stack/common.go | 8 ++++++++ command/stack/deploy.go | 41 +++++++++++++++++++---------------------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/command/stack/common.go b/command/stack/common.go index 4776ec1b42..b94c108667 100644 --- a/command/stack/common.go +++ b/command/stack/common.go @@ -46,3 +46,11 @@ func getNetworks( ctx, types.NetworkListOptions{Filters: getStackFilter(namespace)}) } + +type namespace struct { + name string +} + +func (n namespace) scope(name string) string { + return n.name + "_" + name +} diff --git a/command/stack/deploy.go b/command/stack/deploy.go index bb3e73e6e1..fccd89eb5e 100644 --- a/command/stack/deploy.go +++ b/command/stack/deploy.go @@ -84,10 +84,11 @@ func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error { } ctx := context.Background() - if err := createNetworks(ctx, dockerCli, config.Networks, opts.namespace); err != nil { + namespace := namespace{name: opts.namespace} + if err := createNetworks(ctx, dockerCli, config.Networks, namespace); err != nil { return err } - return deployServices(ctx, dockerCli, config, opts.namespace, opts.sendRegistryAuth) + return deployServices(ctx, dockerCli, config, namespace, opts.sendRegistryAuth) } func propertyWarnings(properties map[string]string) string { @@ -136,11 +137,11 @@ func createNetworks( ctx context.Context, dockerCli *command.DockerCli, networks map[string]composetypes.NetworkConfig, - namespace string, + namespace namespace, ) error { client := dockerCli.Client() - existingNetworks, err := getNetworks(ctx, client, namespace) + existingNetworks, err := getNetworks(ctx, client, namespace.name) if err != nil { return err } @@ -158,13 +159,13 @@ func createNetworks( continue } - name := fmt.Sprintf("%s_%s", namespace, internalName) + name := namespace.scope(internalName) if _, exists := existingNetworkMap[name]; exists { continue } createOpts := types.NetworkCreate{ - Labels: getStackLabels(namespace, network.Labels), + Labels: getStackLabels(namespace.name, network.Labels), Driver: network.Driver, Options: network.DriverOpts, } @@ -190,14 +191,13 @@ func createNetworks( func convertNetworks( networks map[string]*composetypes.ServiceNetworkConfig, - namespace string, + namespace namespace, name string, ) []swarm.NetworkAttachmentConfig { if len(networks) == 0 { return []swarm.NetworkAttachmentConfig{ { - // TODO: only do this name mangling in one function - Target: namespace + "_" + "default", + Target: namespace.scope("default"), Aliases: []string{name}, }, } @@ -206,8 +206,7 @@ func convertNetworks( nets := []swarm.NetworkAttachmentConfig{} for networkName, network := range networks { nets = append(nets, swarm.NetworkAttachmentConfig{ - // TODO: only do this name mangling in one function - Target: namespace + "_" + networkName, + Target: namespace.scope(networkName), Aliases: append(network.Aliases, name), }) } @@ -217,7 +216,7 @@ func convertNetworks( func convertVolumes( serviceVolumes []string, stackVolumes map[string]composetypes.VolumeConfig, - namespace string, + namespace namespace, ) ([]mount.Mount, error) { var mounts []mount.Mount @@ -271,8 +270,7 @@ func convertVolumes( } } - // TODO: remove this duplication - source = fmt.Sprintf("%s_%s", namespace, source) + source = namespace.scope(source) } } @@ -292,7 +290,7 @@ func deployServices( ctx context.Context, dockerCli *command.DockerCli, config *composetypes.Config, - namespace string, + namespace namespace, sendAuth bool, ) error { apiClient := dockerCli.Client() @@ -300,7 +298,7 @@ func deployServices( services := config.Services volumes := config.Volumes - existingServices, err := getServices(ctx, apiClient, namespace) + existingServices, err := getServices(ctx, apiClient, namespace.name) if err != nil { return err } @@ -311,7 +309,7 @@ func deployServices( } for _, service := range services { - name := fmt.Sprintf("%s_%s", namespace, service.Name) + name := namespace.scope(service.Name) serviceSpec, err := convertService(namespace, service, volumes) if err != nil { @@ -361,12 +359,11 @@ func deployServices( } func convertService( - namespace string, + namespace namespace, service composetypes.ServiceConfig, volumes map[string]composetypes.VolumeConfig, ) (swarm.ServiceSpec, error) { - // TODO: remove this duplication - name := fmt.Sprintf("%s_%s", namespace, service.Name) + name := namespace.scope(service.Name) endpoint, err := convertEndpointSpec(service.Ports) if err != nil { @@ -397,7 +394,7 @@ func convertService( serviceSpec := swarm.ServiceSpec{ Annotations: swarm.Annotations{ Name: name, - Labels: getStackLabels(namespace, service.Deploy.Labels), + Labels: getStackLabels(namespace.name, service.Deploy.Labels), }, TaskTemplate: swarm.TaskSpec{ ContainerSpec: swarm.ContainerSpec{ @@ -406,7 +403,7 @@ func convertService( Args: service.Command, Hostname: service.Hostname, Env: convertEnvironment(service.Environment), - Labels: getStackLabels(namespace, service.Labels), + Labels: getStackLabels(namespace.name, service.Labels), Dir: service.WorkingDir, User: service.User, Mounts: mounts, From 3875355a3e593fac42bea3f88025c76acd3c18dc Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 4 Nov 2016 13:59:14 -0600 Subject: [PATCH 09/14] Remove bundlefile Signed-off-by: Daniel Nephin --- command/bundlefile/bundlefile.go | 69 ------------------------ command/bundlefile/bundlefile_test.go | 77 --------------------------- command/stack/opts.go | 39 +------------- 3 files changed, 1 insertion(+), 184 deletions(-) delete mode 100644 command/bundlefile/bundlefile.go delete mode 100644 command/bundlefile/bundlefile_test.go diff --git a/command/bundlefile/bundlefile.go b/command/bundlefile/bundlefile.go deleted file mode 100644 index 7fd1e4f6c4..0000000000 --- a/command/bundlefile/bundlefile.go +++ /dev/null @@ -1,69 +0,0 @@ -package bundlefile - -import ( - "encoding/json" - "fmt" - "io" -) - -// Bundlefile stores the contents of a bundlefile -type Bundlefile struct { - Version string - Services map[string]Service -} - -// Service is a service from a bundlefile -type Service struct { - Image string - Command []string `json:",omitempty"` - Args []string `json:",omitempty"` - Env []string `json:",omitempty"` - Labels map[string]string `json:",omitempty"` - Ports []Port `json:",omitempty"` - WorkingDir *string `json:",omitempty"` - User *string `json:",omitempty"` - Networks []string `json:",omitempty"` -} - -// Port is a port as defined in a bundlefile -type Port struct { - Protocol string - Port uint32 -} - -// LoadFile loads a bundlefile from a path to the file -func LoadFile(reader io.Reader) (*Bundlefile, error) { - bundlefile := &Bundlefile{} - - decoder := json.NewDecoder(reader) - if err := decoder.Decode(bundlefile); err != nil { - switch jsonErr := err.(type) { - case *json.SyntaxError: - return nil, fmt.Errorf( - "JSON syntax error at byte %v: %s", - jsonErr.Offset, - jsonErr.Error()) - case *json.UnmarshalTypeError: - return nil, fmt.Errorf( - "Unexpected type at byte %v. Expected %s but received %s.", - jsonErr.Offset, - jsonErr.Type, - jsonErr.Value) - } - return nil, err - } - - return bundlefile, nil -} - -// Print writes the contents of the bundlefile to the output writer -// as human readable json -func Print(out io.Writer, bundle *Bundlefile) error { - bytes, err := json.MarshalIndent(*bundle, "", " ") - if err != nil { - return err - } - - _, err = out.Write(bytes) - return err -} diff --git a/command/bundlefile/bundlefile_test.go b/command/bundlefile/bundlefile_test.go deleted file mode 100644 index c343410df3..0000000000 --- a/command/bundlefile/bundlefile_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package bundlefile - -import ( - "bytes" - "strings" - "testing" - - "github.com/docker/docker/pkg/testutil/assert" -) - -func TestLoadFileV01Success(t *testing.T) { - reader := strings.NewReader(`{ - "Version": "0.1", - "Services": { - "redis": { - "Image": "redis@sha256:4b24131101fa0117bcaa18ac37055fffd9176aa1a240392bb8ea85e0be50f2ce", - "Networks": ["default"] - }, - "web": { - "Image": "dockercloud/hello-world@sha256:fe79a2cfbd17eefc344fb8419420808df95a1e22d93b7f621a7399fd1e9dca1d", - "Networks": ["default"], - "User": "web" - } - } - }`) - - bundle, err := LoadFile(reader) - assert.NilError(t, err) - assert.Equal(t, bundle.Version, "0.1") - assert.Equal(t, len(bundle.Services), 2) -} - -func TestLoadFileSyntaxError(t *testing.T) { - reader := strings.NewReader(`{ - "Version": "0.1", - "Services": unquoted string - }`) - - _, err := LoadFile(reader) - assert.Error(t, err, "syntax error at byte 37: invalid character 'u'") -} - -func TestLoadFileTypeError(t *testing.T) { - reader := strings.NewReader(`{ - "Version": "0.1", - "Services": { - "web": { - "Image": "redis", - "Networks": "none" - } - } - }`) - - _, err := LoadFile(reader) - assert.Error(t, err, "Unexpected type at byte 94. Expected []string but received string") -} - -func TestPrint(t *testing.T) { - var buffer bytes.Buffer - bundle := &Bundlefile{ - Version: "0.1", - Services: map[string]Service{ - "web": { - Image: "image", - Command: []string{"echo", "something"}, - }, - }, - } - assert.NilError(t, Print(&buffer, bundle)) - output := buffer.String() - assert.Contains(t, output, "\"Image\": \"image\"") - assert.Contains(t, output, - `"Command": [ - "echo", - "something" - ]`) -} diff --git a/command/stack/opts.go b/command/stack/opts.go index c2cc0d1e70..a33e7707e8 100644 --- a/command/stack/opts.go +++ b/command/stack/opts.go @@ -1,48 +1,11 @@ package stack -import ( - "fmt" - "io" - "os" - - "github.com/docker/docker/cli/command/bundlefile" - "github.com/spf13/pflag" -) +import "github.com/spf13/pflag" func addComposefileFlag(opt *string, flags *pflag.FlagSet) { flags.StringVar(opt, "compose-file", "", "Path to a Compose file") } -func addBundlefileFlag(opt *string, flags *pflag.FlagSet) { - flags.StringVar(opt, "bundle-file", "", "Path to a Distributed Application Bundle file") -} - func addRegistryAuthFlag(opt *bool, flags *pflag.FlagSet) { flags.BoolVar(opt, "with-registry-auth", false, "Send registry authentication details to Swarm agents") } - -func loadBundlefile(stderr io.Writer, namespace string, path string) (*bundlefile.Bundlefile, error) { - defaultPath := fmt.Sprintf("%s.dab", namespace) - - if path == "" { - path = defaultPath - } - if _, err := os.Stat(path); err != nil { - return nil, fmt.Errorf( - "Bundle %s not found. Specify the path with --file", - path) - } - - fmt.Fprintf(stderr, "Loading bundle from %s\n", path) - reader, err := os.Open(path) - if err != nil { - return nil, err - } - defer reader.Close() - - bundle, err := bundlefile.LoadFile(reader) - if err != nil { - return nil, fmt.Errorf("Error reading %s: %v\n", path, err) - } - return bundle, err -} From d05510d954ea006ac985ce25d44e2dfcdf502c0c Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 4 Nov 2016 14:55:24 -0600 Subject: [PATCH 10/14] Add integration test for stack deploy. Signed-off-by: Daniel Nephin --- command/stack/deploy.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index fccd89eb5e..6201c2bd2e 100644 --- a/command/stack/deploy.go +++ b/command/stack/deploy.go @@ -19,8 +19,8 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" servicecmd "github.com/docker/docker/cli/command/service" - runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/docker/docker/opts" + runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/docker/go-connections/nat" ) @@ -85,7 +85,12 @@ func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error { ctx := context.Background() namespace := namespace{name: opts.namespace} - if err := createNetworks(ctx, dockerCli, config.Networks, namespace); err != nil { + + networks := config.Networks + if networks == nil { + networks = make(map[string]composetypes.NetworkConfig) + } + if err := createNetworks(ctx, dockerCli, networks, namespace); err != nil { return err } return deployServices(ctx, dockerCli, config, namespace, opts.sendRegistryAuth) From 791b68784858c74def4a8648a962d35d80d88d9c Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 8 Nov 2016 17:05:23 +0000 Subject: [PATCH 11/14] Reinstate --bundle-file argument to 'docker deploy' Signed-off-by: Aanand Prasad --- command/bundlefile/bundlefile.go | 69 +++++++++ command/bundlefile/bundlefile_test.go | 77 ++++++++++ command/stack/deploy.go | 205 +++++++++++++++++++++----- command/stack/opts.go | 39 ++++- 4 files changed, 351 insertions(+), 39 deletions(-) create mode 100644 command/bundlefile/bundlefile.go create mode 100644 command/bundlefile/bundlefile_test.go diff --git a/command/bundlefile/bundlefile.go b/command/bundlefile/bundlefile.go new file mode 100644 index 0000000000..7fd1e4f6c4 --- /dev/null +++ b/command/bundlefile/bundlefile.go @@ -0,0 +1,69 @@ +package bundlefile + +import ( + "encoding/json" + "fmt" + "io" +) + +// Bundlefile stores the contents of a bundlefile +type Bundlefile struct { + Version string + Services map[string]Service +} + +// Service is a service from a bundlefile +type Service struct { + Image string + Command []string `json:",omitempty"` + Args []string `json:",omitempty"` + Env []string `json:",omitempty"` + Labels map[string]string `json:",omitempty"` + Ports []Port `json:",omitempty"` + WorkingDir *string `json:",omitempty"` + User *string `json:",omitempty"` + Networks []string `json:",omitempty"` +} + +// Port is a port as defined in a bundlefile +type Port struct { + Protocol string + Port uint32 +} + +// LoadFile loads a bundlefile from a path to the file +func LoadFile(reader io.Reader) (*Bundlefile, error) { + bundlefile := &Bundlefile{} + + decoder := json.NewDecoder(reader) + if err := decoder.Decode(bundlefile); err != nil { + switch jsonErr := err.(type) { + case *json.SyntaxError: + return nil, fmt.Errorf( + "JSON syntax error at byte %v: %s", + jsonErr.Offset, + jsonErr.Error()) + case *json.UnmarshalTypeError: + return nil, fmt.Errorf( + "Unexpected type at byte %v. Expected %s but received %s.", + jsonErr.Offset, + jsonErr.Type, + jsonErr.Value) + } + return nil, err + } + + return bundlefile, nil +} + +// Print writes the contents of the bundlefile to the output writer +// as human readable json +func Print(out io.Writer, bundle *Bundlefile) error { + bytes, err := json.MarshalIndent(*bundle, "", " ") + if err != nil { + return err + } + + _, err = out.Write(bytes) + return err +} diff --git a/command/bundlefile/bundlefile_test.go b/command/bundlefile/bundlefile_test.go new file mode 100644 index 0000000000..c343410df3 --- /dev/null +++ b/command/bundlefile/bundlefile_test.go @@ -0,0 +1,77 @@ +package bundlefile + +import ( + "bytes" + "strings" + "testing" + + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestLoadFileV01Success(t *testing.T) { + reader := strings.NewReader(`{ + "Version": "0.1", + "Services": { + "redis": { + "Image": "redis@sha256:4b24131101fa0117bcaa18ac37055fffd9176aa1a240392bb8ea85e0be50f2ce", + "Networks": ["default"] + }, + "web": { + "Image": "dockercloud/hello-world@sha256:fe79a2cfbd17eefc344fb8419420808df95a1e22d93b7f621a7399fd1e9dca1d", + "Networks": ["default"], + "User": "web" + } + } + }`) + + bundle, err := LoadFile(reader) + assert.NilError(t, err) + assert.Equal(t, bundle.Version, "0.1") + assert.Equal(t, len(bundle.Services), 2) +} + +func TestLoadFileSyntaxError(t *testing.T) { + reader := strings.NewReader(`{ + "Version": "0.1", + "Services": unquoted string + }`) + + _, err := LoadFile(reader) + assert.Error(t, err, "syntax error at byte 37: invalid character 'u'") +} + +func TestLoadFileTypeError(t *testing.T) { + reader := strings.NewReader(`{ + "Version": "0.1", + "Services": { + "web": { + "Image": "redis", + "Networks": "none" + } + } + }`) + + _, err := LoadFile(reader) + assert.Error(t, err, "Unexpected type at byte 94. Expected []string but received string") +} + +func TestPrint(t *testing.T) { + var buffer bytes.Buffer + bundle := &Bundlefile{ + Version: "0.1", + Services: map[string]Service{ + "web": { + Image: "image", + Command: []string{"echo", "something"}, + }, + }, + } + assert.NilError(t, Print(&buffer, bundle)) + output := buffer.String() + assert.Contains(t, output, "\"Image\": \"image\"") + assert.Contains(t, output, + `"Command": [ + "echo", + "something" + ]`) +} diff --git a/command/stack/deploy.go b/command/stack/deploy.go index 6201c2bd2e..895442a04d 100644 --- a/command/stack/deploy.go +++ b/command/stack/deploy.go @@ -29,6 +29,7 @@ const ( ) type deployOptions struct { + bundlefile string composefile string namespace string sendRegistryAuth bool @@ -50,12 +51,108 @@ func newDeployCommand(dockerCli *command.DockerCli) *cobra.Command { } flags := cmd.Flags() + addBundlefileFlag(&opts.bundlefile, flags) addComposefileFlag(&opts.composefile, flags) addRegistryAuthFlag(&opts.sendRegistryAuth, flags) return cmd } func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error { + if opts.bundlefile == "" && opts.composefile == "" { + return fmt.Errorf("Please specify either a bundle file (with --bundle-file) or a Compose file (with --compose-file).") + } + + if opts.bundlefile != "" && opts.composefile != "" { + return fmt.Errorf("You cannot specify both a bundle file and a Compose file.") + } + + info, err := dockerCli.Client().Info(context.Background()) + if err != nil { + return err + } + if !info.Swarm.ControlAvailable { + return fmt.Errorf("This node is not a swarm manager. Use \"docker swarm init\" or \"docker swarm join\" to connect this node to swarm and try again.") + } + + if opts.bundlefile != "" { + return deployBundle(dockerCli, opts) + } else { + return deployCompose(dockerCli, opts) + } +} + +func deployBundle(dockerCli *command.DockerCli, opts deployOptions) error { + bundle, err := loadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile) + if err != nil { + return err + } + + namespace := namespace{name: opts.namespace} + + networks := make(map[string]types.NetworkCreate) + for _, service := range bundle.Services { + for _, networkName := range service.Networks { + networks[networkName] = types.NetworkCreate{ + Labels: getStackLabels(namespace.name, nil), + } + } + } + + services := make(map[string]swarm.ServiceSpec) + for internalName, service := range bundle.Services { + name := namespace.scope(internalName) + + var ports []swarm.PortConfig + for _, portSpec := range service.Ports { + ports = append(ports, swarm.PortConfig{ + Protocol: swarm.PortConfigProtocol(portSpec.Protocol), + TargetPort: portSpec.Port, + }) + } + + nets := []swarm.NetworkAttachmentConfig{} + for _, networkName := range service.Networks { + nets = append(nets, swarm.NetworkAttachmentConfig{ + Target: namespace.scope(networkName), + Aliases: []string{networkName}, + }) + } + + serviceSpec := swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: name, + Labels: getStackLabels(namespace.name, service.Labels), + }, + TaskTemplate: swarm.TaskSpec{ + ContainerSpec: swarm.ContainerSpec{ + Image: service.Image, + Command: service.Command, + Args: service.Args, + Env: service.Env, + // Service Labels will not be copied to Containers + // automatically during the deployment so we apply + // it here. + Labels: getStackLabels(namespace.name, nil), + }, + }, + EndpointSpec: &swarm.EndpointSpec{ + Ports: ports, + }, + Networks: nets, + } + + services[internalName] = serviceSpec + } + + ctx := context.Background() + + if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil { + return err + } + return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth) +} + +func deployCompose(dockerCli *command.DockerCli, opts deployOptions) error { configDetails, err := getConfigDetails(opts) if err != nil { return err @@ -86,14 +183,15 @@ func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error { ctx := context.Background() namespace := namespace{name: opts.namespace} - networks := config.Networks - if networks == nil { - networks = make(map[string]composetypes.NetworkConfig) - } - if err := createNetworks(ctx, dockerCli, networks, namespace); err != nil { + networks := convertNetworks(namespace, config.Networks) + if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil { return err } - return deployServices(ctx, dockerCli, config, namespace, opts.sendRegistryAuth) + services, err := convertServices(namespace, config) + if err != nil { + return err + } + return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth) } func propertyWarnings(properties map[string]string) string { @@ -138,37 +236,24 @@ func getConfigFile(filename string) (*composetypes.ConfigFile, error) { }, nil } -func createNetworks( - ctx context.Context, - dockerCli *command.DockerCli, - networks map[string]composetypes.NetworkConfig, +func convertNetworks( namespace namespace, -) error { - client := dockerCli.Client() - - existingNetworks, err := getNetworks(ctx, client, namespace.name) - if err != nil { - return err - } - - existingNetworkMap := make(map[string]types.NetworkResource) - for _, network := range existingNetworks { - existingNetworkMap[network.Name] = network + networks map[string]composetypes.NetworkConfig, +) map[string]types.NetworkCreate { + if networks == nil { + networks = make(map[string]composetypes.NetworkConfig) } // TODO: only add default network if it's used networks["default"] = composetypes.NetworkConfig{} + result := make(map[string]types.NetworkCreate) + for internalName, network := range networks { if network.External.Name != "" { continue } - name := namespace.scope(internalName) - if _, exists := existingNetworkMap[name]; exists { - continue - } - createOpts := types.NetworkCreate{ Labels: getStackLabels(namespace.name, network.Labels), Driver: network.Driver, @@ -182,6 +267,36 @@ func createNetworks( } // TODO: IPAMConfig.Config + result[internalName] = createOpts + } + + return result +} + +func createNetworks( + ctx context.Context, + dockerCli *command.DockerCli, + namespace namespace, + networks map[string]types.NetworkCreate, +) error { + client := dockerCli.Client() + + existingNetworks, err := getNetworks(ctx, client, namespace.name) + if err != nil { + return err + } + + existingNetworkMap := make(map[string]types.NetworkResource) + for _, network := range existingNetworks { + existingNetworkMap[network.Name] = network + } + + for internalName, createOpts := range networks { + name := namespace.scope(internalName) + if _, exists := existingNetworkMap[name]; exists { + continue + } + if createOpts.Driver == "" { createOpts.Driver = defaultNetworkDriver } @@ -191,10 +306,11 @@ func createNetworks( return err } } + return nil } -func convertNetworks( +func convertServiceNetworks( networks map[string]*composetypes.ServiceNetworkConfig, namespace namespace, name string, @@ -294,14 +410,12 @@ func convertVolumes( func deployServices( ctx context.Context, dockerCli *command.DockerCli, - config *composetypes.Config, + services map[string]swarm.ServiceSpec, namespace namespace, sendAuth bool, ) error { apiClient := dockerCli.Client() out := dockerCli.Out() - services := config.Services - volumes := config.Volumes existingServices, err := getServices(ctx, apiClient, namespace.name) if err != nil { @@ -313,13 +427,8 @@ func deployServices( existingServiceMap[service.Spec.Name] = service } - for _, service := range services { - name := namespace.scope(service.Name) - - serviceSpec, err := convertService(namespace, service, volumes) - if err != nil { - return err - } + for internalName, serviceSpec := range services { + name := namespace.scope(internalName) encodedAuth := "" if sendAuth { @@ -363,6 +472,26 @@ func deployServices( return nil } +func convertServices( + namespace namespace, + config *composetypes.Config, +) (map[string]swarm.ServiceSpec, error) { + result := make(map[string]swarm.ServiceSpec) + + services := config.Services + volumes := config.Volumes + + for _, service := range services { + serviceSpec, err := convertService(namespace, service, volumes) + if err != nil { + return nil, err + } + result[service.Name] = serviceSpec + } + + return result, nil +} + func convertService( namespace namespace, service composetypes.ServiceConfig, @@ -422,7 +551,7 @@ func convertService( }, EndpointSpec: endpoint, Mode: mode, - Networks: convertNetworks(service.Networks, namespace, service.Name), + Networks: convertServiceNetworks(service.Networks, namespace, service.Name), UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig), } diff --git a/command/stack/opts.go b/command/stack/opts.go index a33e7707e8..c2cc0d1e70 100644 --- a/command/stack/opts.go +++ b/command/stack/opts.go @@ -1,11 +1,48 @@ package stack -import "github.com/spf13/pflag" +import ( + "fmt" + "io" + "os" + + "github.com/docker/docker/cli/command/bundlefile" + "github.com/spf13/pflag" +) func addComposefileFlag(opt *string, flags *pflag.FlagSet) { flags.StringVar(opt, "compose-file", "", "Path to a Compose file") } +func addBundlefileFlag(opt *string, flags *pflag.FlagSet) { + flags.StringVar(opt, "bundle-file", "", "Path to a Distributed Application Bundle file") +} + func addRegistryAuthFlag(opt *bool, flags *pflag.FlagSet) { flags.BoolVar(opt, "with-registry-auth", false, "Send registry authentication details to Swarm agents") } + +func loadBundlefile(stderr io.Writer, namespace string, path string) (*bundlefile.Bundlefile, error) { + defaultPath := fmt.Sprintf("%s.dab", namespace) + + if path == "" { + path = defaultPath + } + if _, err := os.Stat(path); err != nil { + return nil, fmt.Errorf( + "Bundle %s not found. Specify the path with --file", + path) + } + + fmt.Fprintf(stderr, "Loading bundle from %s\n", path) + reader, err := os.Open(path) + if err != nil { + return nil, err + } + defer reader.Close() + + bundle, err := bundlefile.LoadFile(reader) + if err != nil { + return nil, fmt.Errorf("Error reading %s: %v\n", path, err) + } + return bundle, err +} From 458ffcd2e66871e35b7ff0dc6fc5dad07cb3a2cf Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 8 Nov 2016 15:20:16 -0500 Subject: [PATCH 12/14] Restore stack deploy integration test with dab Signed-off-by: Daniel Nephin --- command/stack/deploy.go | 92 ++---------------------------- command/stack/deploy_bundlefile.go | 80 ++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 87 deletions(-) create mode 100644 command/stack/deploy_bundlefile.go diff --git a/command/stack/deploy.go b/command/stack/deploy.go index 895442a04d..6a633c9a8f 100644 --- a/command/stack/deploy.go +++ b/command/stack/deploy.go @@ -58,100 +58,18 @@ func newDeployCommand(dockerCli *command.DockerCli) *cobra.Command { } func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error { - if opts.bundlefile == "" && opts.composefile == "" { + switch { + case opts.bundlefile == "" && opts.composefile == "": return fmt.Errorf("Please specify either a bundle file (with --bundle-file) or a Compose file (with --compose-file).") - } - - if opts.bundlefile != "" && opts.composefile != "" { + case opts.bundlefile != "" && opts.composefile != "": return fmt.Errorf("You cannot specify both a bundle file and a Compose file.") - } - - info, err := dockerCli.Client().Info(context.Background()) - if err != nil { - return err - } - if !info.Swarm.ControlAvailable { - return fmt.Errorf("This node is not a swarm manager. Use \"docker swarm init\" or \"docker swarm join\" to connect this node to swarm and try again.") - } - - if opts.bundlefile != "" { + case opts.bundlefile != "": return deployBundle(dockerCli, opts) - } else { + default: return deployCompose(dockerCli, opts) } } -func deployBundle(dockerCli *command.DockerCli, opts deployOptions) error { - bundle, err := loadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile) - if err != nil { - return err - } - - namespace := namespace{name: opts.namespace} - - networks := make(map[string]types.NetworkCreate) - for _, service := range bundle.Services { - for _, networkName := range service.Networks { - networks[networkName] = types.NetworkCreate{ - Labels: getStackLabels(namespace.name, nil), - } - } - } - - services := make(map[string]swarm.ServiceSpec) - for internalName, service := range bundle.Services { - name := namespace.scope(internalName) - - var ports []swarm.PortConfig - for _, portSpec := range service.Ports { - ports = append(ports, swarm.PortConfig{ - Protocol: swarm.PortConfigProtocol(portSpec.Protocol), - TargetPort: portSpec.Port, - }) - } - - nets := []swarm.NetworkAttachmentConfig{} - for _, networkName := range service.Networks { - nets = append(nets, swarm.NetworkAttachmentConfig{ - Target: namespace.scope(networkName), - Aliases: []string{networkName}, - }) - } - - serviceSpec := swarm.ServiceSpec{ - Annotations: swarm.Annotations{ - Name: name, - Labels: getStackLabels(namespace.name, service.Labels), - }, - TaskTemplate: swarm.TaskSpec{ - ContainerSpec: swarm.ContainerSpec{ - Image: service.Image, - Command: service.Command, - Args: service.Args, - Env: service.Env, - // Service Labels will not be copied to Containers - // automatically during the deployment so we apply - // it here. - Labels: getStackLabels(namespace.name, nil), - }, - }, - EndpointSpec: &swarm.EndpointSpec{ - Ports: ports, - }, - Networks: nets, - } - - services[internalName] = serviceSpec - } - - ctx := context.Background() - - if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil { - return err - } - return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth) -} - func deployCompose(dockerCli *command.DockerCli, opts deployOptions) error { configDetails, err := getConfigDetails(opts) if err != nil { diff --git a/command/stack/deploy_bundlefile.go b/command/stack/deploy_bundlefile.go new file mode 100644 index 0000000000..5ec8a2a05b --- /dev/null +++ b/command/stack/deploy_bundlefile.go @@ -0,0 +1,80 @@ +package stack + +import ( + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/command" +) + +func deployBundle(dockerCli *command.DockerCli, opts deployOptions) error { + bundle, err := loadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile) + if err != nil { + return err + } + + namespace := namespace{name: opts.namespace} + + networks := make(map[string]types.NetworkCreate) + for _, service := range bundle.Services { + for _, networkName := range service.Networks { + networks[networkName] = types.NetworkCreate{ + Labels: getStackLabels(namespace.name, nil), + } + } + } + + services := make(map[string]swarm.ServiceSpec) + for internalName, service := range bundle.Services { + name := namespace.scope(internalName) + + var ports []swarm.PortConfig + for _, portSpec := range service.Ports { + ports = append(ports, swarm.PortConfig{ + Protocol: swarm.PortConfigProtocol(portSpec.Protocol), + TargetPort: portSpec.Port, + }) + } + + nets := []swarm.NetworkAttachmentConfig{} + for _, networkName := range service.Networks { + nets = append(nets, swarm.NetworkAttachmentConfig{ + Target: namespace.scope(networkName), + Aliases: []string{networkName}, + }) + } + + serviceSpec := swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: name, + Labels: getStackLabels(namespace.name, service.Labels), + }, + TaskTemplate: swarm.TaskSpec{ + ContainerSpec: swarm.ContainerSpec{ + Image: service.Image, + Command: service.Command, + Args: service.Args, + Env: service.Env, + // Service Labels will not be copied to Containers + // automatically during the deployment so we apply + // it here. + Labels: getStackLabels(namespace.name, nil), + }, + }, + EndpointSpec: &swarm.EndpointSpec{ + Ports: ports, + }, + Networks: nets, + } + + services[internalName] = serviceSpec + } + + ctx := context.Background() + + if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil { + return err + } + return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth) +} From 0333117b88cd8fd43ff47b9d20feb62c1977e907 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 10 Nov 2016 11:33:00 -0500 Subject: [PATCH 13/14] Handle bind options and volume options Signed-off-by: Daniel Nephin --- command/stack/deploy.go | 154 +++++++++++++++++++++++++--------------- 1 file changed, 96 insertions(+), 58 deletions(-) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index 6a633c9a8f..f68ca85555 100644 --- a/command/stack/deploy.go +++ b/command/stack/deploy.go @@ -259,70 +259,107 @@ func convertVolumes( ) ([]mount.Mount, error) { var mounts []mount.Mount - for _, volumeString := range serviceVolumes { - var ( - source, target string - mountType mount.Type - readOnly bool - volumeOptions *mount.VolumeOptions - ) - - // TODO: split Windows path mappings properly - parts := strings.SplitN(volumeString, ":", 3) - - if len(parts) == 3 { - source = parts[0] - target = parts[1] - if parts[2] == "ro" { - readOnly = true - } - } else if len(parts) == 2 { - source = parts[0] - target = parts[1] - } else if len(parts) == 1 { - target = parts[0] + for _, volumeSpec := range serviceVolumes { + mount, err := convertVolumeToMount(volumeSpec, stackVolumes, namespace) + if err != nil { + return nil, err } + mounts = append(mounts, mount) + } + return mounts, nil +} - // TODO: catch Windows paths here - if strings.HasPrefix(source, "/") { - mountType = mount.TypeBind - } else { - mountType = mount.TypeVolume +func convertVolumeToMount( + volumeSpec string, + stackVolumes map[string]composetypes.VolumeConfig, + namespace namespace, +) (mount.Mount, error) { + var source, target string + var mode []string - stackVolume, exists := stackVolumes[source] - if !exists { - // TODO: better error message (include service name) - return nil, fmt.Errorf("Undefined volume: %s", source) - } + // TODO: split Windows path mappings properly + parts := strings.SplitN(volumeSpec, ":", 3) - if stackVolume.External.Name != "" { - source = stackVolume.External.Name - } else { - volumeOptions = &mount.VolumeOptions{ - Labels: stackVolume.Labels, - } - - if stackVolume.Driver != "" { - volumeOptions.DriverConfig = &mount.Driver{ - Name: stackVolume.Driver, - Options: stackVolume.DriverOpts, - } - } - - source = namespace.scope(source) - } - } - - mounts = append(mounts, mount.Mount{ - Type: mountType, - Source: source, - Target: target, - ReadOnly: readOnly, - VolumeOptions: volumeOptions, - }) + switch len(parts) { + case 3: + source = parts[0] + target = parts[1] + mode = strings.Split(parts[2], ",") + case 2: + source = parts[0] + target = parts[1] + case 1: + target = parts[0] + default: + return mount.Mount{}, fmt.Errorf("invald volume: %s", volumeSpec) } - return mounts, nil + // TODO: catch Windows paths here + if strings.HasPrefix(source, "/") { + return mount.Mount{ + Type: mount.TypeBind, + Source: source, + Target: target, + ReadOnly: isReadOnly(mode), + BindOptions: getBindOptions(mode), + }, nil + } + + stackVolume, exists := stackVolumes[source] + if !exists { + return mount.Mount{}, fmt.Errorf("undefined volume: %s", source) + } + + var volumeOptions *mount.VolumeOptions + if stackVolume.External.Name != "" { + source = stackVolume.External.Name + } else { + volumeOptions = &mount.VolumeOptions{ + Labels: stackVolume.Labels, + NoCopy: isNoCopy(mode), + } + + if stackVolume.Driver != "" { + volumeOptions.DriverConfig = &mount.Driver{ + Name: stackVolume.Driver, + Options: stackVolume.DriverOpts, + } + } + source = namespace.scope(source) + } + return mount.Mount{ + Type: mount.TypeVolume, + Source: source, + Target: target, + ReadOnly: isReadOnly(mode), + VolumeOptions: volumeOptions, + }, nil +} + +func modeHas(mode []string, field string) bool { + for _, item := range mode { + if item == field { + return true + } + } + return false +} + +func isReadOnly(mode []string) bool { + return modeHas(mode, "ro") +} + +func isNoCopy(mode []string) bool { + return modeHas(mode, "nocopy") +} + +func getBindOptions(mode []string) *mount.BindOptions { + for _, item := range mode { + if strings.Contains(item, "private") || strings.Contains(item, "shared") || strings.Contains(item, "slave") { + return &mount.BindOptions{Propagation: mount.Propagation(item)} + } + } + return nil } func deployServices( @@ -429,6 +466,7 @@ func convertService( mounts, err := convertVolumes(service.Volumes, volumes, namespace) if err != nil { + // TODO: better error message (include service name) return swarm.ServiceSpec{}, err } From cb1783590c93a828fa1545a9cd3b8674ef2bdf67 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 10 Nov 2016 16:22:31 -0500 Subject: [PATCH 14/14] Implement ipamconfig.subnet and be more explicit about restart policy always. Signed-off-by: Daniel Nephin --- command/stack/deploy.go | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index f68ca85555..33dd15e5a7 100644 --- a/command/stack/deploy.go +++ b/command/stack/deploy.go @@ -178,13 +178,19 @@ func convertNetworks( Options: network.DriverOpts, } - if network.Ipam.Driver != "" { - createOpts.IPAM = &networktypes.IPAM{ - Driver: network.Ipam.Driver, - } + if network.Ipam.Driver != "" || len(network.Ipam.Config) > 0 { + createOpts.IPAM = &networktypes.IPAM{} } - // TODO: IPAMConfig.Config + if network.Ipam.Driver != "" { + createOpts.IPAM.Driver = network.Ipam.Driver + } + for _, ipamConfig := range network.Ipam.Config { + config := networktypes.IPAMConfig{ + Subnet: ipamConfig.Subnet, + } + createOpts.IPAM.Config = append(createOpts.IPAM.Config, config) + } result[internalName] = createOpts } @@ -523,8 +529,12 @@ func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (* } // TODO: is this an accurate convertion? switch { - case policy.IsNone(), policy.IsAlways(), policy.IsUnlessStopped(): + case policy.IsNone(): return nil, nil + case policy.IsAlways(), policy.IsUnlessStopped(): + return &swarm.RestartPolicy{ + Condition: swarm.RestartPolicyConditionAny, + }, nil case policy.IsOnFailure(): attempts := uint64(policy.MaximumRetryCount) return &swarm.RestartPolicy{