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 }