Convert deploy to use a compose-file.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
Daniel Nephin 2016-11-02 14:57:40 -04:00
parent ed35648151
commit cbbc6b4d9b
6 changed files with 185 additions and 124 deletions

View file

@ -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 {

View file

@ -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)

View file

@ -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),

View file

@ -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)
}

View file

@ -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
}

View file

@ -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) {