github.com/panekj/cli@v0.0.0-20230304125325-467dd2f3797e/cli/command/stack/swarm/deploy_composefile.go (about)

     1  package swarm
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  
     7  	"github.com/docker/cli/cli/command"
     8  	"github.com/docker/cli/cli/command/stack/options"
     9  	"github.com/docker/cli/cli/compose/convert"
    10  	composetypes "github.com/docker/cli/cli/compose/types"
    11  	"github.com/docker/docker/api/types"
    12  	"github.com/docker/docker/api/types/container"
    13  	"github.com/docker/docker/api/types/swarm"
    14  	apiclient "github.com/docker/docker/client"
    15  	"github.com/pkg/errors"
    16  )
    17  
    18  func deployCompose(ctx context.Context, dockerCli command.Cli, opts options.Deploy, config *composetypes.Config) error {
    19  	if err := checkDaemonIsSwarmManager(ctx, dockerCli); err != nil {
    20  		return err
    21  	}
    22  
    23  	namespace := convert.NewNamespace(opts.Namespace)
    24  
    25  	if opts.Prune {
    26  		services := map[string]struct{}{}
    27  		for _, service := range config.Services {
    28  			services[service.Name] = struct{}{}
    29  		}
    30  		pruneServices(ctx, dockerCli, namespace, services)
    31  	}
    32  
    33  	serviceNetworks := getServicesDeclaredNetworks(config.Services)
    34  	networks, externalNetworks := convert.Networks(namespace, config.Networks, serviceNetworks)
    35  	if err := validateExternalNetworks(ctx, dockerCli.Client(), externalNetworks); err != nil {
    36  		return err
    37  	}
    38  	if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil {
    39  		return err
    40  	}
    41  
    42  	secrets, err := convert.Secrets(namespace, config.Secrets)
    43  	if err != nil {
    44  		return err
    45  	}
    46  	if err := createSecrets(ctx, dockerCli, secrets); err != nil {
    47  		return err
    48  	}
    49  
    50  	configs, err := convert.Configs(namespace, config.Configs)
    51  	if err != nil {
    52  		return err
    53  	}
    54  	if err := createConfigs(ctx, dockerCli, configs); err != nil {
    55  		return err
    56  	}
    57  
    58  	services, err := convert.Services(namespace, config, dockerCli.Client())
    59  	if err != nil {
    60  		return err
    61  	}
    62  	return deployServices(ctx, dockerCli, services, namespace, opts.SendRegistryAuth, opts.ResolveImage)
    63  }
    64  
    65  func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} {
    66  	serviceNetworks := map[string]struct{}{}
    67  	for _, serviceConfig := range serviceConfigs {
    68  		if len(serviceConfig.Networks) == 0 {
    69  			serviceNetworks["default"] = struct{}{}
    70  			continue
    71  		}
    72  		for network := range serviceConfig.Networks {
    73  			serviceNetworks[network] = struct{}{}
    74  		}
    75  	}
    76  	return serviceNetworks
    77  }
    78  
    79  func validateExternalNetworks(ctx context.Context, client apiclient.NetworkAPIClient, externalNetworks []string) error {
    80  	for _, networkName := range externalNetworks {
    81  		if !container.NetworkMode(networkName).IsUserDefined() {
    82  			// Networks that are not user defined always exist on all nodes as
    83  			// local-scoped networks, so there's no need to inspect them.
    84  			continue
    85  		}
    86  		network, err := client.NetworkInspect(ctx, networkName, types.NetworkInspectOptions{})
    87  		switch {
    88  		case apiclient.IsErrNotFound(err):
    89  			return errors.Errorf("network %q is declared as external, but could not be found. You need to create a swarm-scoped network before the stack is deployed", networkName)
    90  		case err != nil:
    91  			return err
    92  		case network.Scope != "swarm":
    93  			return errors.Errorf("network %q is declared as external, but it is not in the right scope: %q instead of \"swarm\"", networkName, network.Scope)
    94  		}
    95  	}
    96  	return nil
    97  }
    98  
    99  func createSecrets(ctx context.Context, dockerCli command.Cli, secrets []swarm.SecretSpec) error {
   100  	client := dockerCli.Client()
   101  
   102  	for _, secretSpec := range secrets {
   103  		secret, _, err := client.SecretInspectWithRaw(ctx, secretSpec.Name)
   104  		switch {
   105  		case err == nil:
   106  			// secret already exists, then we update that
   107  			if err := client.SecretUpdate(ctx, secret.ID, secret.Meta.Version, secretSpec); err != nil {
   108  				return errors.Wrapf(err, "failed to update secret %s", secretSpec.Name)
   109  			}
   110  		case apiclient.IsErrNotFound(err):
   111  			// secret does not exist, then we create a new one.
   112  			fmt.Fprintf(dockerCli.Out(), "Creating secret %s\n", secretSpec.Name)
   113  			if _, err := client.SecretCreate(ctx, secretSpec); err != nil {
   114  				return errors.Wrapf(err, "failed to create secret %s", secretSpec.Name)
   115  			}
   116  		default:
   117  			return err
   118  		}
   119  	}
   120  	return nil
   121  }
   122  
   123  func createConfigs(ctx context.Context, dockerCli command.Cli, configs []swarm.ConfigSpec) error {
   124  	client := dockerCli.Client()
   125  
   126  	for _, configSpec := range configs {
   127  		config, _, err := client.ConfigInspectWithRaw(ctx, configSpec.Name)
   128  		switch {
   129  		case err == nil:
   130  			// config already exists, then we update that
   131  			if err := client.ConfigUpdate(ctx, config.ID, config.Meta.Version, configSpec); err != nil {
   132  				return errors.Wrapf(err, "failed to update config %s", configSpec.Name)
   133  			}
   134  		case apiclient.IsErrNotFound(err):
   135  			// config does not exist, then we create a new one.
   136  			fmt.Fprintf(dockerCli.Out(), "Creating config %s\n", configSpec.Name)
   137  			if _, err := client.ConfigCreate(ctx, configSpec); err != nil {
   138  				return errors.Wrapf(err, "failed to create config %s", configSpec.Name)
   139  			}
   140  		default:
   141  			return err
   142  		}
   143  	}
   144  	return nil
   145  }
   146  
   147  func createNetworks(ctx context.Context, dockerCli command.Cli, namespace convert.Namespace, networks map[string]types.NetworkCreate) error {
   148  	client := dockerCli.Client()
   149  
   150  	existingNetworks, err := getStackNetworks(ctx, client, namespace.Name())
   151  	if err != nil {
   152  		return err
   153  	}
   154  
   155  	existingNetworkMap := make(map[string]types.NetworkResource)
   156  	for _, network := range existingNetworks {
   157  		existingNetworkMap[network.Name] = network
   158  	}
   159  
   160  	for name, createOpts := range networks {
   161  		if _, exists := existingNetworkMap[name]; exists {
   162  			continue
   163  		}
   164  
   165  		if createOpts.Driver == "" {
   166  			createOpts.Driver = defaultNetworkDriver
   167  		}
   168  
   169  		fmt.Fprintf(dockerCli.Out(), "Creating network %s\n", name)
   170  		if _, err := client.NetworkCreate(ctx, name, createOpts); err != nil {
   171  			return errors.Wrapf(err, "failed to create network %s", name)
   172  		}
   173  	}
   174  	return nil
   175  }
   176  
   177  func deployServices(ctx context.Context, dockerCli command.Cli, services map[string]swarm.ServiceSpec, namespace convert.Namespace, sendAuth bool, resolveImage string) error {
   178  	apiClient := dockerCli.Client()
   179  	out := dockerCli.Out()
   180  
   181  	existingServices, err := getStackServices(ctx, apiClient, namespace.Name())
   182  	if err != nil {
   183  		return err
   184  	}
   185  
   186  	existingServiceMap := make(map[string]swarm.Service)
   187  	for _, service := range existingServices {
   188  		existingServiceMap[service.Spec.Name] = service
   189  	}
   190  
   191  	for internalName, serviceSpec := range services {
   192  		var (
   193  			name        = namespace.Scope(internalName)
   194  			image       = serviceSpec.TaskTemplate.ContainerSpec.Image
   195  			encodedAuth string
   196  		)
   197  
   198  		if sendAuth {
   199  			// Retrieve encoded auth token from the image reference
   200  			encodedAuth, err = command.RetrieveAuthTokenFromImage(ctx, dockerCli, image)
   201  			if err != nil {
   202  				return err
   203  			}
   204  		}
   205  
   206  		if service, exists := existingServiceMap[name]; exists {
   207  			fmt.Fprintf(out, "Updating service %s (id: %s)\n", name, service.ID)
   208  
   209  			updateOpts := types.ServiceUpdateOptions{EncodedRegistryAuth: encodedAuth}
   210  
   211  			switch resolveImage {
   212  			case ResolveImageAlways:
   213  				// image should be updated by the server using QueryRegistry
   214  				updateOpts.QueryRegistry = true
   215  			case ResolveImageChanged:
   216  				if image != service.Spec.Labels[convert.LabelImage] {
   217  					// Query the registry to resolve digest for the updated image
   218  					updateOpts.QueryRegistry = true
   219  				} else {
   220  					// image has not changed; update the serviceSpec with the
   221  					// existing information that was set by QueryRegistry on the
   222  					// previous deploy. Otherwise this will trigger an incorrect
   223  					// service update.
   224  					serviceSpec.TaskTemplate.ContainerSpec.Image = service.Spec.TaskTemplate.ContainerSpec.Image
   225  				}
   226  			default:
   227  				if image == service.Spec.Labels[convert.LabelImage] {
   228  					// image has not changed; update the serviceSpec with the
   229  					// existing information that was set by QueryRegistry on the
   230  					// previous deploy. Otherwise this will trigger an incorrect
   231  					// service update.
   232  					serviceSpec.TaskTemplate.ContainerSpec.Image = service.Spec.TaskTemplate.ContainerSpec.Image
   233  				}
   234  			}
   235  
   236  			// Stack deploy does not have a `--force` option. Preserve existing
   237  			// ForceUpdate value so that tasks are not re-deployed if not updated.
   238  			// TODO move this to API client?
   239  			serviceSpec.TaskTemplate.ForceUpdate = service.Spec.TaskTemplate.ForceUpdate
   240  
   241  			response, err := apiClient.ServiceUpdate(ctx, service.ID, service.Version, serviceSpec, updateOpts)
   242  			if err != nil {
   243  				return errors.Wrapf(err, "failed to update service %s", name)
   244  			}
   245  
   246  			for _, warning := range response.Warnings {
   247  				fmt.Fprintln(dockerCli.Err(), warning)
   248  			}
   249  		} else {
   250  			fmt.Fprintf(out, "Creating service %s\n", name)
   251  
   252  			createOpts := types.ServiceCreateOptions{EncodedRegistryAuth: encodedAuth}
   253  
   254  			// query registry if flag disabling it was not set
   255  			if resolveImage == ResolveImageAlways || resolveImage == ResolveImageChanged {
   256  				createOpts.QueryRegistry = true
   257  			}
   258  
   259  			if _, err := apiClient.ServiceCreate(ctx, serviceSpec, createOpts); err != nil {
   260  				return errors.Wrapf(err, "failed to create service %s", name)
   261  			}
   262  		}
   263  	}
   264  	return nil
   265  }