github.com/khulnasoft/cli@v0.0.0-20240402070845-01bcad7beefa/cli/command/stack/swarm/deploy_composefile.go (about)

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