github.com/flavio/docker@v0.1.3-0.20170117145210-f63d1a6eec47/cli/command/stack/deploy.go (about)

     1  package stack
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"sort"
     9  	"strings"
    10  
    11  	"github.com/spf13/cobra"
    12  	"golang.org/x/net/context"
    13  
    14  	"github.com/docker/docker/api/types"
    15  	"github.com/docker/docker/api/types/swarm"
    16  	"github.com/docker/docker/cli"
    17  	"github.com/docker/docker/cli/command"
    18  	"github.com/docker/docker/cli/compose/convert"
    19  	"github.com/docker/docker/cli/compose/loader"
    20  	composetypes "github.com/docker/docker/cli/compose/types"
    21  	dockerclient "github.com/docker/docker/client"
    22  )
    23  
    24  const (
    25  	defaultNetworkDriver = "overlay"
    26  )
    27  
    28  type deployOptions struct {
    29  	bundlefile       string
    30  	composefile      string
    31  	namespace        string
    32  	sendRegistryAuth bool
    33  }
    34  
    35  func newDeployCommand(dockerCli *command.DockerCli) *cobra.Command {
    36  	var opts deployOptions
    37  
    38  	cmd := &cobra.Command{
    39  		Use:     "deploy [OPTIONS] STACK",
    40  		Aliases: []string{"up"},
    41  		Short:   "Deploy a new stack or update an existing stack",
    42  		Args:    cli.ExactArgs(1),
    43  		RunE: func(cmd *cobra.Command, args []string) error {
    44  			opts.namespace = args[0]
    45  			return runDeploy(dockerCli, opts)
    46  		},
    47  	}
    48  
    49  	flags := cmd.Flags()
    50  	addBundlefileFlag(&opts.bundlefile, flags)
    51  	addComposefileFlag(&opts.composefile, flags)
    52  	addRegistryAuthFlag(&opts.sendRegistryAuth, flags)
    53  	return cmd
    54  }
    55  
    56  func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error {
    57  	ctx := context.Background()
    58  
    59  	switch {
    60  	case opts.bundlefile == "" && opts.composefile == "":
    61  		return fmt.Errorf("Please specify either a bundle file (with --bundle-file) or a Compose file (with --compose-file).")
    62  	case opts.bundlefile != "" && opts.composefile != "":
    63  		return fmt.Errorf("You cannot specify both a bundle file and a Compose file.")
    64  	case opts.bundlefile != "":
    65  		return deployBundle(ctx, dockerCli, opts)
    66  	default:
    67  		return deployCompose(ctx, dockerCli, opts)
    68  	}
    69  }
    70  
    71  // checkDaemonIsSwarmManager does an Info API call to verify that the daemon is
    72  // a swarm manager. This is necessary because we must create networks before we
    73  // create services, but the API call for creating a network does not return a
    74  // proper status code when it can't create a network in the "global" scope.
    75  func checkDaemonIsSwarmManager(ctx context.Context, dockerCli *command.DockerCli) error {
    76  	info, err := dockerCli.Client().Info(ctx)
    77  	if err != nil {
    78  		return err
    79  	}
    80  	if !info.Swarm.ControlAvailable {
    81  		return errors.New("This node is not a swarm manager. Use \"docker swarm init\" or \"docker swarm join\" to connect this node to swarm and try again.")
    82  	}
    83  	return nil
    84  }
    85  
    86  func deployCompose(ctx context.Context, dockerCli *command.DockerCli, opts deployOptions) error {
    87  	configDetails, err := getConfigDetails(opts)
    88  	if err != nil {
    89  		return err
    90  	}
    91  
    92  	config, err := loader.Load(configDetails)
    93  	if err != nil {
    94  		if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok {
    95  			return fmt.Errorf("Compose file contains unsupported options:\n\n%s\n",
    96  				propertyWarnings(fpe.Properties))
    97  		}
    98  
    99  		return err
   100  	}
   101  
   102  	unsupportedProperties := loader.GetUnsupportedProperties(configDetails)
   103  	if len(unsupportedProperties) > 0 {
   104  		fmt.Fprintf(dockerCli.Err(), "Ignoring unsupported options: %s\n\n",
   105  			strings.Join(unsupportedProperties, ", "))
   106  	}
   107  
   108  	deprecatedProperties := loader.GetDeprecatedProperties(configDetails)
   109  	if len(deprecatedProperties) > 0 {
   110  		fmt.Fprintf(dockerCli.Err(), "Ignoring deprecated options:\n\n%s\n\n",
   111  			propertyWarnings(deprecatedProperties))
   112  	}
   113  
   114  	if err := checkDaemonIsSwarmManager(ctx, dockerCli); err != nil {
   115  		return err
   116  	}
   117  
   118  	namespace := convert.NewNamespace(opts.namespace)
   119  
   120  	serviceNetworks := getServicesDeclaredNetworks(config.Services)
   121  
   122  	networks, externalNetworks := convert.Networks(namespace, config.Networks, serviceNetworks)
   123  	if err := validateExternalNetworks(ctx, dockerCli, externalNetworks); err != nil {
   124  		return err
   125  	}
   126  	if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil {
   127  		return err
   128  	}
   129  	services, err := convert.Services(namespace, config)
   130  	if err != nil {
   131  		return err
   132  	}
   133  	return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth)
   134  }
   135  
   136  func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} {
   137  	serviceNetworks := map[string]struct{}{}
   138  	for _, serviceConfig := range serviceConfigs {
   139  		if len(serviceConfig.Networks) == 0 {
   140  			serviceNetworks["default"] = struct{}{}
   141  			continue
   142  		}
   143  		for network := range serviceConfig.Networks {
   144  			serviceNetworks[network] = struct{}{}
   145  		}
   146  	}
   147  	return serviceNetworks
   148  }
   149  
   150  func propertyWarnings(properties map[string]string) string {
   151  	var msgs []string
   152  	for name, description := range properties {
   153  		msgs = append(msgs, fmt.Sprintf("%s: %s", name, description))
   154  	}
   155  	sort.Strings(msgs)
   156  	return strings.Join(msgs, "\n\n")
   157  }
   158  
   159  func getConfigDetails(opts deployOptions) (composetypes.ConfigDetails, error) {
   160  	var details composetypes.ConfigDetails
   161  	var err error
   162  
   163  	details.WorkingDir, err = os.Getwd()
   164  	if err != nil {
   165  		return details, err
   166  	}
   167  
   168  	configFile, err := getConfigFile(opts.composefile)
   169  	if err != nil {
   170  		return details, err
   171  	}
   172  	// TODO: support multiple files
   173  	details.ConfigFiles = []composetypes.ConfigFile{*configFile}
   174  	return details, nil
   175  }
   176  
   177  func getConfigFile(filename string) (*composetypes.ConfigFile, error) {
   178  	bytes, err := ioutil.ReadFile(filename)
   179  	if err != nil {
   180  		return nil, err
   181  	}
   182  	config, err := loader.ParseYAML(bytes)
   183  	if err != nil {
   184  		return nil, err
   185  	}
   186  	return &composetypes.ConfigFile{
   187  		Filename: filename,
   188  		Config:   config,
   189  	}, nil
   190  }
   191  
   192  func validateExternalNetworks(
   193  	ctx context.Context,
   194  	dockerCli *command.DockerCli,
   195  	externalNetworks []string) error {
   196  	client := dockerCli.Client()
   197  
   198  	for _, networkName := range externalNetworks {
   199  		network, err := client.NetworkInspect(ctx, networkName)
   200  		if err != nil {
   201  			if dockerclient.IsErrNetworkNotFound(err) {
   202  				return fmt.Errorf("network %q is declared as external, but could not be found. You need to create the network before the stack is deployed (with overlay driver)", networkName)
   203  			}
   204  			return err
   205  		}
   206  		if network.Scope != "swarm" {
   207  			return fmt.Errorf("network %q is declared as external, but it is not in the right scope: %q instead of %q", networkName, network.Scope, "swarm")
   208  		}
   209  	}
   210  
   211  	return nil
   212  }
   213  
   214  func createNetworks(
   215  	ctx context.Context,
   216  	dockerCli *command.DockerCli,
   217  	namespace convert.Namespace,
   218  	networks map[string]types.NetworkCreate,
   219  ) error {
   220  	client := dockerCli.Client()
   221  
   222  	existingNetworks, err := getStackNetworks(ctx, client, namespace.Name())
   223  	if err != nil {
   224  		return err
   225  	}
   226  
   227  	existingNetworkMap := make(map[string]types.NetworkResource)
   228  	for _, network := range existingNetworks {
   229  		existingNetworkMap[network.Name] = network
   230  	}
   231  
   232  	for internalName, createOpts := range networks {
   233  		name := namespace.Scope(internalName)
   234  		if _, exists := existingNetworkMap[name]; exists {
   235  			continue
   236  		}
   237  
   238  		if createOpts.Driver == "" {
   239  			createOpts.Driver = defaultNetworkDriver
   240  		}
   241  
   242  		fmt.Fprintf(dockerCli.Out(), "Creating network %s\n", name)
   243  		if _, err := client.NetworkCreate(ctx, name, createOpts); err != nil {
   244  			return err
   245  		}
   246  	}
   247  
   248  	return nil
   249  }
   250  
   251  func deployServices(
   252  	ctx context.Context,
   253  	dockerCli *command.DockerCli,
   254  	services map[string]swarm.ServiceSpec,
   255  	namespace convert.Namespace,
   256  	sendAuth bool,
   257  ) error {
   258  	apiClient := dockerCli.Client()
   259  	out := dockerCli.Out()
   260  
   261  	existingServices, err := getServices(ctx, apiClient, namespace.Name())
   262  	if err != nil {
   263  		return err
   264  	}
   265  
   266  	existingServiceMap := make(map[string]swarm.Service)
   267  	for _, service := range existingServices {
   268  		existingServiceMap[service.Spec.Name] = service
   269  	}
   270  
   271  	for internalName, serviceSpec := range services {
   272  		name := namespace.Scope(internalName)
   273  
   274  		encodedAuth := ""
   275  		if sendAuth {
   276  			// Retrieve encoded auth token from the image reference
   277  			image := serviceSpec.TaskTemplate.ContainerSpec.Image
   278  			encodedAuth, err = command.RetrieveAuthTokenFromImage(ctx, dockerCli, image)
   279  			if err != nil {
   280  				return err
   281  			}
   282  		}
   283  
   284  		if service, exists := existingServiceMap[name]; exists {
   285  			fmt.Fprintf(out, "Updating service %s (id: %s)\n", name, service.ID)
   286  
   287  			updateOpts := types.ServiceUpdateOptions{}
   288  			if sendAuth {
   289  				updateOpts.EncodedRegistryAuth = encodedAuth
   290  			}
   291  			response, err := apiClient.ServiceUpdate(
   292  				ctx,
   293  				service.ID,
   294  				service.Version,
   295  				serviceSpec,
   296  				updateOpts,
   297  			)
   298  			if err != nil {
   299  				return err
   300  			}
   301  
   302  			for _, warning := range response.Warnings {
   303  				fmt.Fprintln(dockerCli.Err(), warning)
   304  			}
   305  		} else {
   306  			fmt.Fprintf(out, "Creating service %s\n", name)
   307  
   308  			createOpts := types.ServiceCreateOptions{}
   309  			if sendAuth {
   310  				createOpts.EncodedRegistryAuth = encodedAuth
   311  			}
   312  			if _, err := apiClient.ServiceCreate(ctx, serviceSpec, createOpts); err != nil {
   313  				return err
   314  			}
   315  		}
   316  	}
   317  
   318  	return nil
   319  }