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 }