github.com/zhuohuang-hust/src-cbuild@v0.0.0-20230105071821-c7aab3e7c840/cli/command/stack/deploy.go (about) 1 package stack 2 3 import ( 4 "errors" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "sort" 9 "strings" 10 "time" 11 12 "github.com/spf13/cobra" 13 "golang.org/x/net/context" 14 15 "github.com/aanand/compose-file/loader" 16 composetypes "github.com/aanand/compose-file/types" 17 "github.com/docker/docker/api/types" 18 "github.com/docker/docker/api/types/container" 19 "github.com/docker/docker/api/types/mount" 20 networktypes "github.com/docker/docker/api/types/network" 21 "github.com/docker/docker/api/types/swarm" 22 "github.com/docker/docker/cli" 23 "github.com/docker/docker/cli/command" 24 servicecmd "github.com/docker/docker/cli/command/service" 25 dockerclient "github.com/docker/docker/client" 26 "github.com/docker/docker/opts" 27 runconfigopts "github.com/docker/docker/runconfig/opts" 28 "github.com/docker/go-connections/nat" 29 ) 30 31 const ( 32 defaultNetworkDriver = "overlay" 33 ) 34 35 type deployOptions struct { 36 bundlefile string 37 composefile string 38 namespace string 39 sendRegistryAuth bool 40 } 41 42 func newDeployCommand(dockerCli *command.DockerCli) *cobra.Command { 43 var opts deployOptions 44 45 cmd := &cobra.Command{ 46 Use: "deploy [OPTIONS] STACK", 47 Aliases: []string{"up"}, 48 Short: "Deploy a new stack or update an existing stack", 49 Args: cli.ExactArgs(1), 50 RunE: func(cmd *cobra.Command, args []string) error { 51 opts.namespace = args[0] 52 return runDeploy(dockerCli, opts) 53 }, 54 } 55 56 flags := cmd.Flags() 57 addBundlefileFlag(&opts.bundlefile, flags) 58 addComposefileFlag(&opts.composefile, flags) 59 addRegistryAuthFlag(&opts.sendRegistryAuth, flags) 60 return cmd 61 } 62 63 func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error { 64 ctx := context.Background() 65 66 switch { 67 case opts.bundlefile == "" && opts.composefile == "": 68 return fmt.Errorf("Please specify either a bundle file (with --bundle-file) or a Compose file (with --compose-file).") 69 case opts.bundlefile != "" && opts.composefile != "": 70 return fmt.Errorf("You cannot specify both a bundle file and a Compose file.") 71 case opts.bundlefile != "": 72 return deployBundle(ctx, dockerCli, opts) 73 default: 74 return deployCompose(ctx, dockerCli, opts) 75 } 76 } 77 78 // checkDaemonIsSwarmManager does an Info API call to verify that the daemon is 79 // a swarm manager. This is necessary because we must create networks before we 80 // create services, but the API call for creating a network does not return a 81 // proper status code when it can't create a network in the "global" scope. 82 func checkDaemonIsSwarmManager(ctx context.Context, dockerCli *command.DockerCli) error { 83 info, err := dockerCli.Client().Info(ctx) 84 if err != nil { 85 return err 86 } 87 if !info.Swarm.ControlAvailable { 88 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.") 89 } 90 return nil 91 } 92 93 func deployCompose(ctx context.Context, dockerCli *command.DockerCli, opts deployOptions) error { 94 configDetails, err := getConfigDetails(opts) 95 if err != nil { 96 return err 97 } 98 99 config, err := loader.Load(configDetails) 100 if err != nil { 101 if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok { 102 return fmt.Errorf("Compose file contains unsupported options:\n\n%s\n", 103 propertyWarnings(fpe.Properties)) 104 } 105 106 return err 107 } 108 109 unsupportedProperties := loader.GetUnsupportedProperties(configDetails) 110 if len(unsupportedProperties) > 0 { 111 fmt.Fprintf(dockerCli.Err(), "Ignoring unsupported options: %s\n\n", 112 strings.Join(unsupportedProperties, ", ")) 113 } 114 115 deprecatedProperties := loader.GetDeprecatedProperties(configDetails) 116 if len(deprecatedProperties) > 0 { 117 fmt.Fprintf(dockerCli.Err(), "Ignoring deprecated options:\n\n%s\n\n", 118 propertyWarnings(deprecatedProperties)) 119 } 120 121 if err := checkDaemonIsSwarmManager(ctx, dockerCli); err != nil { 122 return err 123 } 124 125 namespace := namespace{name: opts.namespace} 126 127 networks, externalNetworks := convertNetworks(namespace, config.Networks) 128 if err := validateExternalNetworks(ctx, dockerCli, externalNetworks); err != nil { 129 return err 130 } 131 if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil { 132 return err 133 } 134 services, err := convertServices(namespace, config) 135 if err != nil { 136 return err 137 } 138 return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth) 139 } 140 141 func propertyWarnings(properties map[string]string) string { 142 var msgs []string 143 for name, description := range properties { 144 msgs = append(msgs, fmt.Sprintf("%s: %s", name, description)) 145 } 146 sort.Strings(msgs) 147 return strings.Join(msgs, "\n\n") 148 } 149 150 func getConfigDetails(opts deployOptions) (composetypes.ConfigDetails, error) { 151 var details composetypes.ConfigDetails 152 var err error 153 154 details.WorkingDir, err = os.Getwd() 155 if err != nil { 156 return details, err 157 } 158 159 configFile, err := getConfigFile(opts.composefile) 160 if err != nil { 161 return details, err 162 } 163 // TODO: support multiple files 164 details.ConfigFiles = []composetypes.ConfigFile{*configFile} 165 return details, nil 166 } 167 168 func getConfigFile(filename string) (*composetypes.ConfigFile, error) { 169 bytes, err := ioutil.ReadFile(filename) 170 if err != nil { 171 return nil, err 172 } 173 config, err := loader.ParseYAML(bytes) 174 if err != nil { 175 return nil, err 176 } 177 return &composetypes.ConfigFile{ 178 Filename: filename, 179 Config: config, 180 }, nil 181 } 182 183 func convertNetworks( 184 namespace namespace, 185 networks map[string]composetypes.NetworkConfig, 186 ) (map[string]types.NetworkCreate, []string) { 187 if networks == nil { 188 networks = make(map[string]composetypes.NetworkConfig) 189 } 190 191 // TODO: only add default network if it's used 192 networks["default"] = composetypes.NetworkConfig{} 193 194 externalNetworks := []string{} 195 result := make(map[string]types.NetworkCreate) 196 197 for internalName, network := range networks { 198 if network.External.External { 199 externalNetworks = append(externalNetworks, network.External.Name) 200 continue 201 } 202 203 createOpts := types.NetworkCreate{ 204 Labels: getStackLabels(namespace.name, network.Labels), 205 Driver: network.Driver, 206 Options: network.DriverOpts, 207 } 208 209 if network.Ipam.Driver != "" || len(network.Ipam.Config) > 0 { 210 createOpts.IPAM = &networktypes.IPAM{} 211 } 212 213 if network.Ipam.Driver != "" { 214 createOpts.IPAM.Driver = network.Ipam.Driver 215 } 216 for _, ipamConfig := range network.Ipam.Config { 217 config := networktypes.IPAMConfig{ 218 Subnet: ipamConfig.Subnet, 219 } 220 createOpts.IPAM.Config = append(createOpts.IPAM.Config, config) 221 } 222 result[internalName] = createOpts 223 } 224 225 return result, externalNetworks 226 } 227 228 func validateExternalNetworks( 229 ctx context.Context, 230 dockerCli *command.DockerCli, 231 externalNetworks []string) error { 232 client := dockerCli.Client() 233 234 for _, networkName := range externalNetworks { 235 network, err := client.NetworkInspect(ctx, networkName) 236 if err != nil { 237 if dockerclient.IsErrNetworkNotFound(err) { 238 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) 239 } 240 return err 241 } 242 if network.Scope != "swarm" { 243 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") 244 } 245 } 246 247 return nil 248 } 249 250 func createNetworks( 251 ctx context.Context, 252 dockerCli *command.DockerCli, 253 namespace namespace, 254 networks map[string]types.NetworkCreate, 255 ) error { 256 client := dockerCli.Client() 257 258 existingNetworks, err := getStackNetworks(ctx, client, namespace.name) 259 if err != nil { 260 return err 261 } 262 263 existingNetworkMap := make(map[string]types.NetworkResource) 264 for _, network := range existingNetworks { 265 existingNetworkMap[network.Name] = network 266 } 267 268 for internalName, createOpts := range networks { 269 name := namespace.scope(internalName) 270 if _, exists := existingNetworkMap[name]; exists { 271 continue 272 } 273 274 if createOpts.Driver == "" { 275 createOpts.Driver = defaultNetworkDriver 276 } 277 278 fmt.Fprintf(dockerCli.Out(), "Creating network %s\n", name) 279 if _, err := client.NetworkCreate(ctx, name, createOpts); err != nil { 280 return err 281 } 282 } 283 284 return nil 285 } 286 287 func convertServiceNetworks( 288 networks map[string]*composetypes.ServiceNetworkConfig, 289 networkConfigs map[string]composetypes.NetworkConfig, 290 namespace namespace, 291 name string, 292 ) ([]swarm.NetworkAttachmentConfig, error) { 293 if len(networks) == 0 { 294 return []swarm.NetworkAttachmentConfig{ 295 { 296 Target: namespace.scope("default"), 297 Aliases: []string{name}, 298 }, 299 }, nil 300 } 301 302 nets := []swarm.NetworkAttachmentConfig{} 303 for networkName, network := range networks { 304 networkConfig, ok := networkConfigs[networkName] 305 if !ok { 306 return []swarm.NetworkAttachmentConfig{}, fmt.Errorf("invalid network: %s", networkName) 307 } 308 var aliases []string 309 if network != nil { 310 aliases = network.Aliases 311 } 312 target := namespace.scope(networkName) 313 if networkConfig.External.External { 314 target = networkName 315 } 316 nets = append(nets, swarm.NetworkAttachmentConfig{ 317 Target: target, 318 Aliases: append(aliases, name), 319 }) 320 } 321 return nets, nil 322 } 323 324 func convertVolumes( 325 serviceVolumes []string, 326 stackVolumes map[string]composetypes.VolumeConfig, 327 namespace namespace, 328 ) ([]mount.Mount, error) { 329 var mounts []mount.Mount 330 331 for _, volumeSpec := range serviceVolumes { 332 mount, err := convertVolumeToMount(volumeSpec, stackVolumes, namespace) 333 if err != nil { 334 return nil, err 335 } 336 mounts = append(mounts, mount) 337 } 338 return mounts, nil 339 } 340 341 func convertVolumeToMount( 342 volumeSpec string, 343 stackVolumes map[string]composetypes.VolumeConfig, 344 namespace namespace, 345 ) (mount.Mount, error) { 346 var source, target string 347 var mode []string 348 349 // TODO: split Windows path mappings properly 350 parts := strings.SplitN(volumeSpec, ":", 3) 351 352 switch len(parts) { 353 case 3: 354 source = parts[0] 355 target = parts[1] 356 mode = strings.Split(parts[2], ",") 357 case 2: 358 source = parts[0] 359 target = parts[1] 360 case 1: 361 target = parts[0] 362 default: 363 return mount.Mount{}, fmt.Errorf("invald volume: %s", volumeSpec) 364 } 365 366 // TODO: catch Windows paths here 367 if strings.HasPrefix(source, "/") { 368 return mount.Mount{ 369 Type: mount.TypeBind, 370 Source: source, 371 Target: target, 372 ReadOnly: isReadOnly(mode), 373 BindOptions: getBindOptions(mode), 374 }, nil 375 } 376 377 stackVolume, exists := stackVolumes[source] 378 if !exists { 379 return mount.Mount{}, fmt.Errorf("undefined volume: %s", source) 380 } 381 382 var volumeOptions *mount.VolumeOptions 383 if stackVolume.External.Name != "" { 384 source = stackVolume.External.Name 385 } else { 386 volumeOptions = &mount.VolumeOptions{ 387 Labels: getStackLabels(namespace.name, stackVolume.Labels), 388 NoCopy: isNoCopy(mode), 389 } 390 391 if stackVolume.Driver != "" { 392 volumeOptions.DriverConfig = &mount.Driver{ 393 Name: stackVolume.Driver, 394 Options: stackVolume.DriverOpts, 395 } 396 } 397 source = namespace.scope(source) 398 } 399 return mount.Mount{ 400 Type: mount.TypeVolume, 401 Source: source, 402 Target: target, 403 ReadOnly: isReadOnly(mode), 404 VolumeOptions: volumeOptions, 405 }, nil 406 } 407 408 func modeHas(mode []string, field string) bool { 409 for _, item := range mode { 410 if item == field { 411 return true 412 } 413 } 414 return false 415 } 416 417 func isReadOnly(mode []string) bool { 418 return modeHas(mode, "ro") 419 } 420 421 func isNoCopy(mode []string) bool { 422 return modeHas(mode, "nocopy") 423 } 424 425 func getBindOptions(mode []string) *mount.BindOptions { 426 for _, item := range mode { 427 if strings.Contains(item, "private") || strings.Contains(item, "shared") || strings.Contains(item, "slave") { 428 return &mount.BindOptions{Propagation: mount.Propagation(item)} 429 } 430 } 431 return nil 432 } 433 434 func deployServices( 435 ctx context.Context, 436 dockerCli *command.DockerCli, 437 services map[string]swarm.ServiceSpec, 438 namespace namespace, 439 sendAuth bool, 440 ) error { 441 apiClient := dockerCli.Client() 442 out := dockerCli.Out() 443 444 existingServices, err := getServices(ctx, apiClient, namespace.name) 445 if err != nil { 446 return err 447 } 448 449 existingServiceMap := make(map[string]swarm.Service) 450 for _, service := range existingServices { 451 existingServiceMap[service.Spec.Name] = service 452 } 453 454 for internalName, serviceSpec := range services { 455 name := namespace.scope(internalName) 456 457 encodedAuth := "" 458 if sendAuth { 459 // Retrieve encoded auth token from the image reference 460 image := serviceSpec.TaskTemplate.ContainerSpec.Image 461 encodedAuth, err = command.RetrieveAuthTokenFromImage(ctx, dockerCli, image) 462 if err != nil { 463 return err 464 } 465 } 466 467 if service, exists := existingServiceMap[name]; exists { 468 fmt.Fprintf(out, "Updating service %s (id: %s)\n", name, service.ID) 469 470 updateOpts := types.ServiceUpdateOptions{} 471 if sendAuth { 472 updateOpts.EncodedRegistryAuth = encodedAuth 473 } 474 response, err := apiClient.ServiceUpdate( 475 ctx, 476 service.ID, 477 service.Version, 478 serviceSpec, 479 updateOpts, 480 ) 481 if err != nil { 482 return err 483 } 484 485 for _, warning := range response.Warnings { 486 fmt.Fprintln(dockerCli.Err(), warning) 487 } 488 } else { 489 fmt.Fprintf(out, "Creating service %s\n", name) 490 491 createOpts := types.ServiceCreateOptions{} 492 if sendAuth { 493 createOpts.EncodedRegistryAuth = encodedAuth 494 } 495 if _, err := apiClient.ServiceCreate(ctx, serviceSpec, createOpts); err != nil { 496 return err 497 } 498 } 499 } 500 501 return nil 502 } 503 504 func convertServices( 505 namespace namespace, 506 config *composetypes.Config, 507 ) (map[string]swarm.ServiceSpec, error) { 508 result := make(map[string]swarm.ServiceSpec) 509 510 services := config.Services 511 volumes := config.Volumes 512 networks := config.Networks 513 514 for _, service := range services { 515 serviceSpec, err := convertService(namespace, service, networks, volumes) 516 if err != nil { 517 return nil, err 518 } 519 result[service.Name] = serviceSpec 520 } 521 522 return result, nil 523 } 524 525 func convertService( 526 namespace namespace, 527 service composetypes.ServiceConfig, 528 networkConfigs map[string]composetypes.NetworkConfig, 529 volumes map[string]composetypes.VolumeConfig, 530 ) (swarm.ServiceSpec, error) { 531 name := namespace.scope(service.Name) 532 533 endpoint, err := convertEndpointSpec(service.Ports) 534 if err != nil { 535 return swarm.ServiceSpec{}, err 536 } 537 538 mode, err := convertDeployMode(service.Deploy.Mode, service.Deploy.Replicas) 539 if err != nil { 540 return swarm.ServiceSpec{}, err 541 } 542 543 mounts, err := convertVolumes(service.Volumes, volumes, namespace) 544 if err != nil { 545 // TODO: better error message (include service name) 546 return swarm.ServiceSpec{}, err 547 } 548 549 resources, err := convertResources(service.Deploy.Resources) 550 if err != nil { 551 return swarm.ServiceSpec{}, err 552 } 553 554 restartPolicy, err := convertRestartPolicy( 555 service.Restart, service.Deploy.RestartPolicy) 556 if err != nil { 557 return swarm.ServiceSpec{}, err 558 } 559 560 healthcheck, err := convertHealthcheck(service.HealthCheck) 561 if err != nil { 562 return swarm.ServiceSpec{}, err 563 } 564 565 networks, err := convertServiceNetworks(service.Networks, networkConfigs, namespace, service.Name) 566 if err != nil { 567 return swarm.ServiceSpec{}, err 568 } 569 570 var logDriver *swarm.Driver 571 if service.Logging != nil { 572 logDriver = &swarm.Driver{ 573 Name: service.Logging.Driver, 574 Options: service.Logging.Options, 575 } 576 } 577 578 serviceSpec := swarm.ServiceSpec{ 579 Annotations: swarm.Annotations{ 580 Name: name, 581 Labels: getStackLabels(namespace.name, service.Deploy.Labels), 582 }, 583 TaskTemplate: swarm.TaskSpec{ 584 ContainerSpec: swarm.ContainerSpec{ 585 Image: service.Image, 586 Command: service.Entrypoint, 587 Args: service.Command, 588 Hostname: service.Hostname, 589 Hosts: convertExtraHosts(service.ExtraHosts), 590 Healthcheck: healthcheck, 591 Env: convertEnvironment(service.Environment), 592 Labels: getStackLabels(namespace.name, service.Labels), 593 Dir: service.WorkingDir, 594 User: service.User, 595 Mounts: mounts, 596 StopGracePeriod: service.StopGracePeriod, 597 TTY: service.Tty, 598 OpenStdin: service.StdinOpen, 599 }, 600 LogDriver: logDriver, 601 Resources: resources, 602 RestartPolicy: restartPolicy, 603 Placement: &swarm.Placement{ 604 Constraints: service.Deploy.Placement.Constraints, 605 }, 606 }, 607 EndpointSpec: endpoint, 608 Mode: mode, 609 Networks: networks, 610 UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig), 611 } 612 613 return serviceSpec, nil 614 } 615 616 func convertExtraHosts(extraHosts map[string]string) []string { 617 hosts := []string{} 618 for host, ip := range extraHosts { 619 hosts = append(hosts, fmt.Sprintf("%s %s", ip, host)) 620 } 621 return hosts 622 } 623 624 func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container.HealthConfig, error) { 625 if healthcheck == nil { 626 return nil, nil 627 } 628 var ( 629 err error 630 timeout, interval time.Duration 631 retries int 632 ) 633 if healthcheck.Disable { 634 if len(healthcheck.Test) != 0 { 635 return nil, fmt.Errorf("command and disable key can't be set at the same time") 636 } 637 return &container.HealthConfig{ 638 Test: []string{"NONE"}, 639 }, nil 640 641 } 642 if healthcheck.Timeout != "" { 643 timeout, err = time.ParseDuration(healthcheck.Timeout) 644 if err != nil { 645 return nil, err 646 } 647 } 648 if healthcheck.Interval != "" { 649 interval, err = time.ParseDuration(healthcheck.Interval) 650 if err != nil { 651 return nil, err 652 } 653 } 654 if healthcheck.Retries != nil { 655 retries = int(*healthcheck.Retries) 656 } 657 return &container.HealthConfig{ 658 Test: healthcheck.Test, 659 Timeout: timeout, 660 Interval: interval, 661 Retries: retries, 662 }, nil 663 } 664 665 func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) { 666 // TODO: log if restart is being ignored 667 if source == nil { 668 policy, err := runconfigopts.ParseRestartPolicy(restart) 669 if err != nil { 670 return nil, err 671 } 672 // TODO: is this an accurate convertion? 673 switch { 674 case policy.IsNone(): 675 return nil, nil 676 case policy.IsAlways(), policy.IsUnlessStopped(): 677 return &swarm.RestartPolicy{ 678 Condition: swarm.RestartPolicyConditionAny, 679 }, nil 680 case policy.IsOnFailure(): 681 attempts := uint64(policy.MaximumRetryCount) 682 return &swarm.RestartPolicy{ 683 Condition: swarm.RestartPolicyConditionOnFailure, 684 MaxAttempts: &attempts, 685 }, nil 686 } 687 } 688 return &swarm.RestartPolicy{ 689 Condition: swarm.RestartPolicyCondition(source.Condition), 690 Delay: source.Delay, 691 MaxAttempts: source.MaxAttempts, 692 Window: source.Window, 693 }, nil 694 } 695 696 func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig { 697 if source == nil { 698 return nil 699 } 700 parallel := uint64(1) 701 if source.Parallelism != nil { 702 parallel = *source.Parallelism 703 } 704 return &swarm.UpdateConfig{ 705 Parallelism: parallel, 706 Delay: source.Delay, 707 FailureAction: source.FailureAction, 708 Monitor: source.Monitor, 709 MaxFailureRatio: source.MaxFailureRatio, 710 } 711 } 712 713 func convertResources(source composetypes.Resources) (*swarm.ResourceRequirements, error) { 714 resources := &swarm.ResourceRequirements{} 715 if source.Limits != nil { 716 cpus, err := opts.ParseCPUs(source.Limits.NanoCPUs) 717 if err != nil { 718 return nil, err 719 } 720 resources.Limits = &swarm.Resources{ 721 NanoCPUs: cpus, 722 MemoryBytes: int64(source.Limits.MemoryBytes), 723 } 724 } 725 if source.Reservations != nil { 726 cpus, err := opts.ParseCPUs(source.Reservations.NanoCPUs) 727 if err != nil { 728 return nil, err 729 } 730 resources.Reservations = &swarm.Resources{ 731 NanoCPUs: cpus, 732 MemoryBytes: int64(source.Reservations.MemoryBytes), 733 } 734 } 735 return resources, nil 736 } 737 738 func convertEndpointSpec(source []string) (*swarm.EndpointSpec, error) { 739 portConfigs := []swarm.PortConfig{} 740 ports, portBindings, err := nat.ParsePortSpecs(source) 741 if err != nil { 742 return nil, err 743 } 744 745 for port := range ports { 746 portConfigs = append( 747 portConfigs, 748 servicecmd.ConvertPortToPortConfig(port, portBindings)...) 749 } 750 751 return &swarm.EndpointSpec{Ports: portConfigs}, nil 752 } 753 754 func convertEnvironment(source map[string]string) []string { 755 var output []string 756 757 for name, value := range source { 758 output = append(output, fmt.Sprintf("%s=%s", name, value)) 759 } 760 761 return output 762 } 763 764 func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) { 765 serviceMode := swarm.ServiceMode{} 766 767 switch mode { 768 case "global": 769 if replicas != nil { 770 return serviceMode, fmt.Errorf("replicas can only be used with replicated mode") 771 } 772 serviceMode.Global = &swarm.GlobalService{} 773 case "replicated", "": 774 serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas} 775 default: 776 return serviceMode, fmt.Errorf("Unknown mode: %s", mode) 777 } 778 return serviceMode, nil 779 }