diff --git a/command/service/opts.go b/command/service/opts.go index 52971ae833..4ea78c6af7 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 f5acc2c511..1214b03a53 100644 --- a/command/service/update.go +++ b/command/service/update.go @@ -639,7 +639,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/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/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..33dd15e5a7 100644 --- a/command/stack/deploy.go +++ b/command/stack/deploy.go @@ -2,16 +2,26 @@ package stack import ( "fmt" + "io/ioutil" + "os" + "sort" "strings" "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" + "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" "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/bundlefile" + servicecmd "github.com/docker/docker/cli/command/service" + "github.com/docker/docker/opts" + runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/docker/go-connections/nat" ) const ( @@ -20,6 +30,7 @@ const ( type deployOptions struct { bundlefile string + composefile string namespace string sendRegistryAuth bool } @@ -30,10 +41,10 @@ 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"}, @@ -41,57 +52,160 @@ 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 { - bundle, err := loadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile) - if err != nil { - return err + 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).") + case opts.bundlefile != "" && opts.composefile != "": + return fmt.Errorf("You cannot specify both a bundle file and a Compose file.") + case opts.bundlefile != "": + return deployBundle(dockerCli, opts) + default: + return deployCompose(dockerCli, opts) } - - 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.") - } - - networks := getUniqueNetworkNames(bundle.Services) - ctx := context.Background() - - if err := updateNetworks(ctx, dockerCli, networks, opts.namespace); err != nil { - return err - } - return deployServices(ctx, dockerCli, bundle.Services, 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 deployCompose(dockerCli *command.DockerCli, opts deployOptions) error { + configDetails, err := getConfigDetails(opts) + if err != nil { + return err + } + + 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 } - networks := []string{} - for network := range networkSet { - networks = append(networks, network) + unsupportedProperties := loader.GetUnsupportedProperties(configDetails) + if len(unsupportedProperties) > 0 { + fmt.Fprintf(dockerCli.Err(), "Ignoring unsupported options: %s\n\n", + strings.Join(unsupportedProperties, ", ")) } - return networks + + deprecatedProperties := loader.GetDeprecatedProperties(configDetails) + if len(deprecatedProperties) > 0 { + fmt.Fprintf(dockerCli.Err(), "Ignoring deprecated options:\n\n%s\n\n", + propertyWarnings(deprecatedProperties)) + } + + ctx := context.Background() + namespace := namespace{name: opts.namespace} + + networks := convertNetworks(namespace, config.Networks) + if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil { + return err + } + services, err := convertServices(namespace, config) + if err != nil { + return err + } + return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth) } -func updateNetworks( +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 + + details.WorkingDir, err = os.Getwd() + if err != nil { + return details, err + } + + configFile, err := getConfigFile(opts.composefile) + if err != nil { + return details, err + } + // TODO: support multiple files + details.ConfigFiles = []composetypes.ConfigFile{*configFile} + return details, nil +} + +func getConfigFile(filename string) (*composetypes.ConfigFile, error) { + bytes, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + config, err := loader.ParseYAML(bytes) + if err != nil { + return nil, err + } + return &composetypes.ConfigFile{ + Filename: filename, + Config: config, + }, nil +} + +func convertNetworks( + namespace namespace, + 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 + } + + createOpts := types.NetworkCreate{ + Labels: getStackLabels(namespace.name, network.Labels), + Driver: network.Driver, + Options: network.DriverOpts, + } + + if network.Ipam.Driver != "" || len(network.Ipam.Config) > 0 { + createOpts.IPAM = &networktypes.IPAM{} + } + + 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 + } + + return result +} + +func createNetworks( ctx context.Context, dockerCli *command.DockerCli, - networks []string, - namespace string, + namespace namespace, + networks map[string]types.NetworkCreate, ) error { client := dockerCli.Client() - existingNetworks, err := getNetworks(ctx, client, namespace) + existingNetworks, err := getNetworks(ctx, client, namespace.name) if err != nil { return err } @@ -101,47 +215,170 @@ func updateNetworks( existingNetworkMap[network.Name] = network } - createOpts := types.NetworkCreate{ - Labels: getStackLabels(namespace, nil), - Driver: defaultNetworkDriver, - } - - for _, internalName := range networks { - name := fmt.Sprintf("%s_%s", namespace, internalName) - + for internalName, createOpts := range networks { + name := namespace.scope(internalName) if _, exists := existingNetworkMap[name]; exists { continue } + + 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 } } + return nil } -func convertNetworks(networks []string, namespace string, name string) []swarm.NetworkAttachmentConfig { +func convertServiceNetworks( + networks map[string]*composetypes.ServiceNetworkConfig, + namespace namespace, + name string, +) []swarm.NetworkAttachmentConfig { + if len(networks) == 0 { + return []swarm.NetworkAttachmentConfig{ + { + Target: namespace.scope("default"), + Aliases: []string{name}, + }, + } + } + nets := []swarm.NetworkAttachmentConfig{} - for _, network := range networks { + for networkName, network := range networks { nets = append(nets, swarm.NetworkAttachmentConfig{ - Target: namespace + "_" + network, - Aliases: []string{name}, + Target: namespace.scope(networkName), + Aliases: append(network.Aliases, name), }) } return nets } +func convertVolumes( + serviceVolumes []string, + stackVolumes map[string]composetypes.VolumeConfig, + namespace namespace, +) ([]mount.Mount, error) { + var mounts []mount.Mount + + for _, volumeSpec := range serviceVolumes { + mount, err := convertVolumeToMount(volumeSpec, stackVolumes, namespace) + if err != nil { + return nil, err + } + mounts = append(mounts, mount) + } + return mounts, nil +} + +func convertVolumeToMount( + volumeSpec string, + stackVolumes map[string]composetypes.VolumeConfig, + namespace namespace, +) (mount.Mount, error) { + var source, target string + var mode []string + + // TODO: split Windows path mappings properly + parts := strings.SplitN(volumeSpec, ":", 3) + + 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) + } + + // 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( ctx context.Context, dockerCli *command.DockerCli, - services map[string]bundlefile.Service, - namespace string, + services map[string]swarm.ServiceSpec, + namespace namespace, sendAuth bool, ) error { apiClient := dockerCli.Client() out := dockerCli.Out() - existingServices, err := getServices(ctx, apiClient, namespace) + existingServices, err := getServices(ctx, apiClient, namespace.name) if err != nil { return err } @@ -151,47 +388,8 @@ func deployServices( existingServiceMap[service.Spec.Name] = service } - for internalName, service := range services { - name := fmt.Sprintf("%s_%s", namespace, internalName) - - 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 - } + for internalName, serviceSpec := range services { + name := namespace.scope(internalName) encodedAuth := "" if sendAuth { @@ -234,3 +432,202 @@ 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, + volumes map[string]composetypes.VolumeConfig, +) (swarm.ServiceSpec, error) { + name := namespace.scope(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 + } + + mounts, err := convertVolumes(service.Volumes, volumes, namespace) + if err != nil { + // TODO: better error message (include service name) + 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.name, 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.name, 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, + }, + }, + EndpointSpec: endpoint, + Mode: mode, + Networks: convertServiceNetworks(service.Networks, namespace, service.Name), + UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig), + } + + 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(): + 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{ + 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) + 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 != 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} + default: + return serviceMode, fmt.Errorf("Unknown mode: %s", mode) + } + return serviceMode, 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) +} 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) {