github.com/kunnos/engine@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 }