github.com/olljanat/moby@v1.13.1/cli/command/stack/deploy.go (about)

     1  package stack
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"os"
     7  	"sort"
     8  	"strings"
     9  
    10  	"github.com/docker/docker/api/types"
    11  	"github.com/docker/docker/api/types/swarm"
    12  	"github.com/docker/docker/cli"
    13  	"github.com/docker/docker/cli/command"
    14  	secretcli "github.com/docker/docker/cli/command/secret"
    15  	"github.com/docker/docker/cli/compose/convert"
    16  	"github.com/docker/docker/cli/compose/loader"
    17  	composetypes "github.com/docker/docker/cli/compose/types"
    18  	dockerclient "github.com/docker/docker/client"
    19  	"github.com/pkg/errors"
    20  	"github.com/spf13/cobra"
    21  	"golang.org/x/net/context"
    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  	networks, externalNetworks := convert.Networks(namespace, config.Networks, serviceNetworks)
   122  	if err := validateExternalNetworks(ctx, dockerCli, externalNetworks); err != nil {
   123  		return err
   124  	}
   125  	if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil {
   126  		return err
   127  	}
   128  
   129  	secrets, err := convert.Secrets(namespace, config.Secrets)
   130  	if err != nil {
   131  		return err
   132  	}
   133  	if err := createSecrets(ctx, dockerCli, namespace, secrets); err != nil {
   134  		return err
   135  	}
   136  
   137  	services, err := convert.Services(namespace, config, dockerCli.Client())
   138  	if err != nil {
   139  		return err
   140  	}
   141  	return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth)
   142  }
   143  func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} {
   144  	serviceNetworks := map[string]struct{}{}
   145  	for _, serviceConfig := range serviceConfigs {
   146  		if len(serviceConfig.Networks) == 0 {
   147  			serviceNetworks["default"] = struct{}{}
   148  			continue
   149  		}
   150  		for network := range serviceConfig.Networks {
   151  			serviceNetworks[network] = struct{}{}
   152  		}
   153  	}
   154  	return serviceNetworks
   155  }
   156  
   157  func propertyWarnings(properties map[string]string) string {
   158  	var msgs []string
   159  	for name, description := range properties {
   160  		msgs = append(msgs, fmt.Sprintf("%s: %s", name, description))
   161  	}
   162  	sort.Strings(msgs)
   163  	return strings.Join(msgs, "\n\n")
   164  }
   165  
   166  func getConfigDetails(opts deployOptions) (composetypes.ConfigDetails, error) {
   167  	var details composetypes.ConfigDetails
   168  	var err error
   169  
   170  	details.WorkingDir, err = os.Getwd()
   171  	if err != nil {
   172  		return details, err
   173  	}
   174  
   175  	configFile, err := getConfigFile(opts.composefile)
   176  	if err != nil {
   177  		return details, err
   178  	}
   179  	// TODO: support multiple files
   180  	details.ConfigFiles = []composetypes.ConfigFile{*configFile}
   181  	return details, nil
   182  }
   183  
   184  func getConfigFile(filename string) (*composetypes.ConfigFile, error) {
   185  	bytes, err := ioutil.ReadFile(filename)
   186  	if err != nil {
   187  		return nil, err
   188  	}
   189  	config, err := loader.ParseYAML(bytes)
   190  	if err != nil {
   191  		return nil, err
   192  	}
   193  	return &composetypes.ConfigFile{
   194  		Filename: filename,
   195  		Config:   config,
   196  	}, nil
   197  }
   198  
   199  func validateExternalNetworks(
   200  	ctx context.Context,
   201  	dockerCli *command.DockerCli,
   202  	externalNetworks []string) error {
   203  	client := dockerCli.Client()
   204  
   205  	for _, networkName := range externalNetworks {
   206  		network, err := client.NetworkInspect(ctx, networkName)
   207  		if err != nil {
   208  			if dockerclient.IsErrNetworkNotFound(err) {
   209  				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)
   210  			}
   211  			return err
   212  		}
   213  		if network.Scope != "swarm" {
   214  			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")
   215  		}
   216  	}
   217  
   218  	return nil
   219  }
   220  
   221  func createSecrets(
   222  	ctx context.Context,
   223  	dockerCli *command.DockerCli,
   224  	namespace convert.Namespace,
   225  	secrets []swarm.SecretSpec,
   226  ) error {
   227  	client := dockerCli.Client()
   228  
   229  	for _, secretSpec := range secrets {
   230  		// TODO: fix this after https://github.com/docker/docker/pull/29218
   231  		secrets, err := secretcli.GetSecretsByNameOrIDPrefixes(ctx, client, []string{secretSpec.Name})
   232  		switch {
   233  		case err != nil:
   234  			return err
   235  		case len(secrets) > 1:
   236  			return errors.Errorf("ambiguous secret name: %s", secretSpec.Name)
   237  		case len(secrets) == 0:
   238  			fmt.Fprintf(dockerCli.Out(), "Creating secret %s\n", secretSpec.Name)
   239  			_, err = client.SecretCreate(ctx, secretSpec)
   240  		default:
   241  			secret := secrets[0]
   242  			// Update secret to ensure that the local data hasn't changed
   243  			err = client.SecretUpdate(ctx, secret.ID, secret.Meta.Version, secretSpec)
   244  		}
   245  		if err != nil {
   246  			return err
   247  		}
   248  	}
   249  	return nil
   250  }
   251  
   252  func createNetworks(
   253  	ctx context.Context,
   254  	dockerCli *command.DockerCli,
   255  	namespace convert.Namespace,
   256  	networks map[string]types.NetworkCreate,
   257  ) error {
   258  	client := dockerCli.Client()
   259  
   260  	existingNetworks, err := getStackNetworks(ctx, client, namespace.Name())
   261  	if err != nil {
   262  		return err
   263  	}
   264  
   265  	existingNetworkMap := make(map[string]types.NetworkResource)
   266  	for _, network := range existingNetworks {
   267  		existingNetworkMap[network.Name] = network
   268  	}
   269  
   270  	for internalName, createOpts := range networks {
   271  		name := namespace.Scope(internalName)
   272  		if _, exists := existingNetworkMap[name]; exists {
   273  			continue
   274  		}
   275  
   276  		if createOpts.Driver == "" {
   277  			createOpts.Driver = defaultNetworkDriver
   278  		}
   279  
   280  		fmt.Fprintf(dockerCli.Out(), "Creating network %s\n", name)
   281  		if _, err := client.NetworkCreate(ctx, name, createOpts); err != nil {
   282  			return err
   283  		}
   284  	}
   285  
   286  	return nil
   287  }
   288  
   289  func deployServices(
   290  	ctx context.Context,
   291  	dockerCli *command.DockerCli,
   292  	services map[string]swarm.ServiceSpec,
   293  	namespace convert.Namespace,
   294  	sendAuth bool,
   295  ) error {
   296  	apiClient := dockerCli.Client()
   297  	out := dockerCli.Out()
   298  
   299  	existingServices, err := getServices(ctx, apiClient, namespace.Name())
   300  	if err != nil {
   301  		return err
   302  	}
   303  
   304  	existingServiceMap := make(map[string]swarm.Service)
   305  	for _, service := range existingServices {
   306  		existingServiceMap[service.Spec.Name] = service
   307  	}
   308  
   309  	for internalName, serviceSpec := range services {
   310  		name := namespace.Scope(internalName)
   311  
   312  		encodedAuth := ""
   313  		if sendAuth {
   314  			// Retrieve encoded auth token from the image reference
   315  			image := serviceSpec.TaskTemplate.ContainerSpec.Image
   316  			encodedAuth, err = command.RetrieveAuthTokenFromImage(ctx, dockerCli, image)
   317  			if err != nil {
   318  				return err
   319  			}
   320  		}
   321  
   322  		if service, exists := existingServiceMap[name]; exists {
   323  			fmt.Fprintf(out, "Updating service %s (id: %s)\n", name, service.ID)
   324  
   325  			updateOpts := types.ServiceUpdateOptions{}
   326  			if sendAuth {
   327  				updateOpts.EncodedRegistryAuth = encodedAuth
   328  			}
   329  			response, err := apiClient.ServiceUpdate(
   330  				ctx,
   331  				service.ID,
   332  				service.Version,
   333  				serviceSpec,
   334  				updateOpts,
   335  			)
   336  			if err != nil {
   337  				return err
   338  			}
   339  
   340  			for _, warning := range response.Warnings {
   341  				fmt.Fprintln(dockerCli.Err(), warning)
   342  			}
   343  		} else {
   344  			fmt.Fprintf(out, "Creating service %s\n", name)
   345  
   346  			createOpts := types.ServiceCreateOptions{}
   347  			if sendAuth {
   348  				createOpts.EncodedRegistryAuth = encodedAuth
   349  			}
   350  			if _, err := apiClient.ServiceCreate(ctx, serviceSpec, createOpts); err != nil {
   351  				return err
   352  			}
   353  		}
   354  	}
   355  
   356  	return nil
   357  }