From ab7cf401802466ded97ff754703906da8ff5f56b Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 2 Nov 2016 14:57:40 -0400 Subject: [PATCH] Convert deploy to use a compose-file. Signed-off-by: Daniel Nephin Upstream-commit: f702b722d8e7420820ab1eb1262829e0b590f57a Component: cli --- components/cli/command/service/opts.go | 5 +- components/cli/command/service/update.go | 2 +- components/cli/command/stack/cmd.go | 1 - components/cli/command/stack/config.go | 39 ---- components/cli/command/stack/deploy.go | 253 ++++++++++++++++------- components/cli/command/stack/opts.go | 9 +- 6 files changed, 185 insertions(+), 124 deletions(-) delete mode 100644 components/cli/command/stack/config.go diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index c48c952e0c..2113fdfede 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/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/components/cli/command/service/update.go b/components/cli/command/service/update.go index 9741f67d54..d1c695d75d 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/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/components/cli/command/stack/cmd.go b/components/cli/command/stack/cmd.go index 4189504403..ff71e0ddfa 100644 --- a/components/cli/command/stack/cmd.go +++ b/components/cli/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/components/cli/command/stack/config.go b/components/cli/command/stack/config.go deleted file mode 100644 index 56e554a86e..0000000000 --- a/components/cli/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/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index 435a9193b4..c1faa0521c 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/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/components/cli/command/stack/opts.go b/components/cli/command/stack/opts.go index 5f2d8b5d0a..c2cc0d1e70 100644 --- a/components/cli/command/stack/opts.go +++ b/components/cli/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) {