github.com/ali-iotechsys/cli@v20.10.0+incompatible/cli/compose/convert/service.go (about) 1 package convert 2 3 import ( 4 "fmt" 5 "os" 6 "sort" 7 "strings" 8 "time" 9 10 servicecli "github.com/docker/cli/cli/command/service" 11 composetypes "github.com/docker/cli/cli/compose/types" 12 "github.com/docker/cli/opts" 13 "github.com/docker/docker/api/types/container" 14 "github.com/docker/docker/api/types/swarm" 15 "github.com/docker/docker/api/types/versions" 16 "github.com/docker/docker/client" 17 "github.com/docker/go-units" 18 "github.com/pkg/errors" 19 ) 20 21 const ( 22 defaultNetwork = "default" 23 // LabelImage is the label used to store image name provided in the compose file 24 LabelImage = "com.docker.stack.image" 25 ) 26 27 // Services from compose-file types to engine API types 28 func Services( 29 namespace Namespace, 30 config *composetypes.Config, 31 client client.CommonAPIClient, 32 ) (map[string]swarm.ServiceSpec, error) { 33 result := make(map[string]swarm.ServiceSpec) 34 35 services := config.Services 36 volumes := config.Volumes 37 networks := config.Networks 38 39 for _, service := range services { 40 secrets, err := convertServiceSecrets(client, namespace, service.Secrets, config.Secrets) 41 if err != nil { 42 return nil, errors.Wrapf(err, "service %s", service.Name) 43 } 44 configs, err := convertServiceConfigObjs(client, namespace, service, config.Configs) 45 if err != nil { 46 return nil, errors.Wrapf(err, "service %s", service.Name) 47 } 48 49 serviceSpec, err := Service(client.ClientVersion(), namespace, service, networks, volumes, secrets, configs) 50 if err != nil { 51 return nil, errors.Wrapf(err, "service %s", service.Name) 52 } 53 result[service.Name] = serviceSpec 54 } 55 56 return result, nil 57 } 58 59 // Service converts a ServiceConfig into a swarm ServiceSpec 60 func Service( 61 apiVersion string, 62 namespace Namespace, 63 service composetypes.ServiceConfig, 64 networkConfigs map[string]composetypes.NetworkConfig, 65 volumes map[string]composetypes.VolumeConfig, 66 secrets []*swarm.SecretReference, 67 configs []*swarm.ConfigReference, 68 ) (swarm.ServiceSpec, error) { 69 name := namespace.Scope(service.Name) 70 endpoint := convertEndpointSpec(service.Deploy.EndpointMode, service.Ports) 71 72 mode, err := convertDeployMode(service.Deploy.Mode, service.Deploy.Replicas) 73 if err != nil { 74 return swarm.ServiceSpec{}, err 75 } 76 77 mounts, err := Volumes(service.Volumes, volumes, namespace) 78 if err != nil { 79 return swarm.ServiceSpec{}, err 80 } 81 82 resources, err := convertResources(service.Deploy.Resources) 83 if err != nil { 84 return swarm.ServiceSpec{}, err 85 } 86 87 restartPolicy, err := convertRestartPolicy( 88 service.Restart, service.Deploy.RestartPolicy) 89 if err != nil { 90 return swarm.ServiceSpec{}, err 91 } 92 93 healthcheck, err := convertHealthcheck(service.HealthCheck) 94 if err != nil { 95 return swarm.ServiceSpec{}, err 96 } 97 98 networks, err := convertServiceNetworks(service.Networks, networkConfigs, namespace, service.Name) 99 if err != nil { 100 return swarm.ServiceSpec{}, err 101 } 102 103 dnsConfig := convertDNSConfig(service.DNS, service.DNSSearch) 104 105 var privileges swarm.Privileges 106 privileges.CredentialSpec, err = convertCredentialSpec( 107 namespace, service.CredentialSpec, configs, 108 ) 109 if err != nil { 110 return swarm.ServiceSpec{}, err 111 } 112 113 var logDriver *swarm.Driver 114 if service.Logging != nil { 115 logDriver = &swarm.Driver{ 116 Name: service.Logging.Driver, 117 Options: service.Logging.Options, 118 } 119 } 120 121 capAdd, capDrop := opts.EffectiveCapAddCapDrop(service.CapAdd, service.CapDrop) 122 123 serviceSpec := swarm.ServiceSpec{ 124 Annotations: swarm.Annotations{ 125 Name: name, 126 Labels: AddStackLabel(namespace, service.Deploy.Labels), 127 }, 128 TaskTemplate: swarm.TaskSpec{ 129 ContainerSpec: &swarm.ContainerSpec{ 130 Image: service.Image, 131 Command: service.Entrypoint, 132 Args: service.Command, 133 Hostname: service.Hostname, 134 Hosts: convertExtraHosts(service.ExtraHosts), 135 DNSConfig: dnsConfig, 136 Healthcheck: healthcheck, 137 Env: sortStrings(convertEnvironment(service.Environment)), 138 Labels: AddStackLabel(namespace, service.Labels), 139 Dir: service.WorkingDir, 140 User: service.User, 141 Mounts: mounts, 142 StopGracePeriod: composetypes.ConvertDurationPtr(service.StopGracePeriod), 143 StopSignal: service.StopSignal, 144 TTY: service.Tty, 145 OpenStdin: service.StdinOpen, 146 Secrets: secrets, 147 Configs: configs, 148 ReadOnly: service.ReadOnly, 149 Privileges: &privileges, 150 Isolation: container.Isolation(service.Isolation), 151 Init: service.Init, 152 Sysctls: service.Sysctls, 153 CapabilityAdd: capAdd, 154 CapabilityDrop: capDrop, 155 Ulimits: convertUlimits(service.Ulimits), 156 }, 157 LogDriver: logDriver, 158 Resources: resources, 159 RestartPolicy: restartPolicy, 160 Placement: &swarm.Placement{ 161 Constraints: service.Deploy.Placement.Constraints, 162 Preferences: getPlacementPreference(service.Deploy.Placement.Preferences), 163 MaxReplicas: service.Deploy.Placement.MaxReplicas, 164 }, 165 }, 166 EndpointSpec: endpoint, 167 Mode: mode, 168 UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig), 169 RollbackConfig: convertUpdateConfig(service.Deploy.RollbackConfig), 170 } 171 172 // add an image label to serviceSpec 173 serviceSpec.Labels[LabelImage] = service.Image 174 175 // ServiceSpec.Networks is deprecated and should not have been used by 176 // this package. It is possible to update TaskTemplate.Networks, but it 177 // is not possible to update ServiceSpec.Networks. Unfortunately, we 178 // can't unconditionally start using TaskTemplate.Networks, because that 179 // will break with older daemons that don't support migrating from 180 // ServiceSpec.Networks to TaskTemplate.Networks. So which field to use 181 // is conditional on daemon version. 182 if versions.LessThan(apiVersion, "1.29") { 183 serviceSpec.Networks = networks 184 } else { 185 serviceSpec.TaskTemplate.Networks = networks 186 } 187 return serviceSpec, nil 188 } 189 190 func getPlacementPreference(preferences []composetypes.PlacementPreferences) []swarm.PlacementPreference { 191 result := []swarm.PlacementPreference{} 192 for _, preference := range preferences { 193 spreadDescriptor := preference.Spread 194 result = append(result, swarm.PlacementPreference{ 195 Spread: &swarm.SpreadOver{ 196 SpreadDescriptor: spreadDescriptor, 197 }, 198 }) 199 } 200 return result 201 } 202 203 func sortStrings(strs []string) []string { 204 sort.Strings(strs) 205 return strs 206 } 207 208 func convertServiceNetworks( 209 networks map[string]*composetypes.ServiceNetworkConfig, 210 networkConfigs networkMap, 211 namespace Namespace, 212 name string, 213 ) ([]swarm.NetworkAttachmentConfig, error) { 214 if len(networks) == 0 { 215 networks = map[string]*composetypes.ServiceNetworkConfig{ 216 defaultNetwork: {}, 217 } 218 } 219 220 nets := []swarm.NetworkAttachmentConfig{} 221 for networkName, network := range networks { 222 networkConfig, ok := networkConfigs[networkName] 223 if !ok && networkName != defaultNetwork { 224 return nil, errors.Errorf("undefined network %q", networkName) 225 } 226 var aliases []string 227 if network != nil { 228 aliases = network.Aliases 229 } 230 target := namespace.Scope(networkName) 231 if networkConfig.Name != "" { 232 target = networkConfig.Name 233 } 234 netAttachConfig := swarm.NetworkAttachmentConfig{ 235 Target: target, 236 Aliases: aliases, 237 } 238 // Only add default aliases to user defined networks. Other networks do 239 // not support aliases. 240 if container.NetworkMode(target).IsUserDefined() { 241 netAttachConfig.Aliases = append(netAttachConfig.Aliases, name) 242 } 243 nets = append(nets, netAttachConfig) 244 } 245 246 sort.Slice(nets, func(i, j int) bool { 247 return nets[i].Target < nets[j].Target 248 }) 249 return nets, nil 250 } 251 252 // TODO: fix secrets API so that SecretAPIClient is not required here 253 func convertServiceSecrets( 254 client client.SecretAPIClient, 255 namespace Namespace, 256 secrets []composetypes.ServiceSecretConfig, 257 secretSpecs map[string]composetypes.SecretConfig, 258 ) ([]*swarm.SecretReference, error) { 259 refs := []*swarm.SecretReference{} 260 261 lookup := func(key string) (composetypes.FileObjectConfig, error) { 262 secretSpec, exists := secretSpecs[key] 263 if !exists { 264 return composetypes.FileObjectConfig{}, errors.Errorf("undefined secret %q", key) 265 } 266 return composetypes.FileObjectConfig(secretSpec), nil 267 } 268 for _, secret := range secrets { 269 obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(secret), lookup) 270 if err != nil { 271 return nil, err 272 } 273 274 file := swarm.SecretReferenceFileTarget(obj.File) 275 refs = append(refs, &swarm.SecretReference{ 276 File: &file, 277 SecretName: obj.Name, 278 }) 279 } 280 281 secrs, err := servicecli.ParseSecrets(client, refs) 282 if err != nil { 283 return nil, err 284 } 285 // sort to ensure idempotence (don't restart services just because the entries are in different order) 286 sort.SliceStable(secrs, func(i, j int) bool { return secrs[i].SecretName < secrs[j].SecretName }) 287 return secrs, err 288 } 289 290 // convertServiceConfigObjs takes an API client, a namespace, a ServiceConfig, 291 // and a set of compose Config specs, and creates the swarm ConfigReferences 292 // required by the serivce. Unlike convertServiceSecrets, this takes the whole 293 // ServiceConfig, because some Configs may be needed as a result of other 294 // fields (like CredentialSpecs). 295 // 296 // TODO: fix configs API so that ConfigsAPIClient is not required here 297 func convertServiceConfigObjs( 298 client client.ConfigAPIClient, 299 namespace Namespace, 300 service composetypes.ServiceConfig, 301 configSpecs map[string]composetypes.ConfigObjConfig, 302 ) ([]*swarm.ConfigReference, error) { 303 refs := []*swarm.ConfigReference{} 304 305 lookup := func(key string) (composetypes.FileObjectConfig, error) { 306 configSpec, exists := configSpecs[key] 307 if !exists { 308 return composetypes.FileObjectConfig{}, errors.Errorf("undefined config %q", key) 309 } 310 return composetypes.FileObjectConfig(configSpec), nil 311 } 312 for _, config := range service.Configs { 313 obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(config), lookup) 314 if err != nil { 315 return nil, err 316 } 317 318 file := swarm.ConfigReferenceFileTarget(obj.File) 319 refs = append(refs, &swarm.ConfigReference{ 320 File: &file, 321 ConfigName: obj.Name, 322 }) 323 } 324 325 // finally, after converting all of the file objects, create any 326 // Runtime-type configs that are needed. these are configs that are not 327 // mounted into the container, but are used in some other way by the 328 // container runtime. Currently, this only means CredentialSpecs, but in 329 // the future it may be used for other fields 330 331 // grab the CredentialSpec out of the Service 332 credSpec := service.CredentialSpec 333 // if the credSpec uses a config, then we should grab the config name, and 334 // create a config reference for it. A File or Registry-type CredentialSpec 335 // does not need this operation. 336 if credSpec.Config != "" { 337 // look up the config in the configSpecs. 338 obj, err := lookup(credSpec.Config) 339 if err != nil { 340 return nil, err 341 } 342 343 // get the actual correct name. 344 name := namespace.Scope(credSpec.Config) 345 if obj.Name != "" { 346 name = obj.Name 347 } 348 349 // now append a Runtime-type config. 350 refs = append(refs, &swarm.ConfigReference{ 351 ConfigName: name, 352 Runtime: &swarm.ConfigReferenceRuntimeTarget{}, 353 }) 354 355 } 356 357 confs, err := servicecli.ParseConfigs(client, refs) 358 if err != nil { 359 return nil, err 360 } 361 // sort to ensure idempotence (don't restart services just because the entries are in different order) 362 sort.SliceStable(confs, func(i, j int) bool { return confs[i].ConfigName < confs[j].ConfigName }) 363 return confs, err 364 } 365 366 type swarmReferenceTarget struct { 367 Name string 368 UID string 369 GID string 370 Mode os.FileMode 371 } 372 373 type swarmReferenceObject struct { 374 File swarmReferenceTarget 375 ID string 376 Name string 377 } 378 379 func convertFileObject( 380 namespace Namespace, 381 config composetypes.FileReferenceConfig, 382 lookup func(key string) (composetypes.FileObjectConfig, error), 383 ) (swarmReferenceObject, error) { 384 obj, err := lookup(config.Source) 385 if err != nil { 386 return swarmReferenceObject{}, err 387 } 388 389 source := namespace.Scope(config.Source) 390 if obj.Name != "" { 391 source = obj.Name 392 } 393 394 target := config.Target 395 if target == "" { 396 target = config.Source 397 } 398 399 uid := config.UID 400 gid := config.GID 401 if uid == "" { 402 uid = "0" 403 } 404 if gid == "" { 405 gid = "0" 406 } 407 mode := config.Mode 408 if mode == nil { 409 mode = uint32Ptr(0444) 410 } 411 412 return swarmReferenceObject{ 413 File: swarmReferenceTarget{ 414 Name: target, 415 UID: uid, 416 GID: gid, 417 Mode: os.FileMode(*mode), 418 }, 419 Name: source, 420 }, nil 421 } 422 423 func uint32Ptr(value uint32) *uint32 { 424 return &value 425 } 426 427 // convertExtraHosts converts <host>:<ip> mappings to SwarmKit notation: 428 // "IP-address hostname(s)". The original order of mappings is preserved. 429 func convertExtraHosts(extraHosts composetypes.HostsList) []string { 430 hosts := []string{} 431 for _, hostIP := range extraHosts { 432 if v := strings.SplitN(hostIP, ":", 2); len(v) == 2 { 433 // Convert to SwarmKit notation: IP-address hostname(s) 434 hosts = append(hosts, fmt.Sprintf("%s %s", v[1], v[0])) 435 } 436 } 437 return hosts 438 } 439 440 func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container.HealthConfig, error) { 441 if healthcheck == nil { 442 return nil, nil 443 } 444 var ( 445 timeout, interval, startPeriod time.Duration 446 retries int 447 ) 448 if healthcheck.Disable { 449 if len(healthcheck.Test) != 0 { 450 return nil, errors.Errorf("test and disable can't be set at the same time") 451 } 452 return &container.HealthConfig{ 453 Test: []string{"NONE"}, 454 }, nil 455 456 } 457 if healthcheck.Timeout != nil { 458 timeout = time.Duration(*healthcheck.Timeout) 459 } 460 if healthcheck.Interval != nil { 461 interval = time.Duration(*healthcheck.Interval) 462 } 463 if healthcheck.StartPeriod != nil { 464 startPeriod = time.Duration(*healthcheck.StartPeriod) 465 } 466 if healthcheck.Retries != nil { 467 retries = int(*healthcheck.Retries) 468 } 469 return &container.HealthConfig{ 470 Test: healthcheck.Test, 471 Timeout: timeout, 472 Interval: interval, 473 Retries: retries, 474 StartPeriod: startPeriod, 475 }, nil 476 } 477 478 func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) { 479 // TODO: log if restart is being ignored 480 if source == nil { 481 policy, err := opts.ParseRestartPolicy(restart) 482 if err != nil { 483 return nil, err 484 } 485 switch { 486 case policy.IsNone(): 487 return nil, nil 488 case policy.IsAlways(), policy.IsUnlessStopped(): 489 return &swarm.RestartPolicy{ 490 Condition: swarm.RestartPolicyConditionAny, 491 }, nil 492 case policy.IsOnFailure(): 493 attempts := uint64(policy.MaximumRetryCount) 494 return &swarm.RestartPolicy{ 495 Condition: swarm.RestartPolicyConditionOnFailure, 496 MaxAttempts: &attempts, 497 }, nil 498 default: 499 return nil, errors.Errorf("unknown restart policy: %s", restart) 500 } 501 } 502 503 return &swarm.RestartPolicy{ 504 Condition: swarm.RestartPolicyCondition(source.Condition), 505 Delay: composetypes.ConvertDurationPtr(source.Delay), 506 MaxAttempts: source.MaxAttempts, 507 Window: composetypes.ConvertDurationPtr(source.Window), 508 }, nil 509 } 510 511 func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig { 512 if source == nil { 513 return nil 514 } 515 parallel := uint64(1) 516 if source.Parallelism != nil { 517 parallel = *source.Parallelism 518 } 519 return &swarm.UpdateConfig{ 520 Parallelism: parallel, 521 Delay: time.Duration(source.Delay), 522 FailureAction: source.FailureAction, 523 Monitor: time.Duration(source.Monitor), 524 MaxFailureRatio: source.MaxFailureRatio, 525 Order: source.Order, 526 } 527 } 528 529 func convertResources(source composetypes.Resources) (*swarm.ResourceRequirements, error) { 530 resources := &swarm.ResourceRequirements{} 531 var err error 532 if source.Limits != nil { 533 var cpus int64 534 if source.Limits.NanoCPUs != "" { 535 cpus, err = opts.ParseCPUs(source.Limits.NanoCPUs) 536 if err != nil { 537 return nil, err 538 } 539 } 540 resources.Limits = &swarm.Limit{ 541 NanoCPUs: cpus, 542 MemoryBytes: int64(source.Limits.MemoryBytes), 543 Pids: source.Limits.Pids, 544 } 545 } 546 if source.Reservations != nil { 547 var cpus int64 548 if source.Reservations.NanoCPUs != "" { 549 cpus, err = opts.ParseCPUs(source.Reservations.NanoCPUs) 550 if err != nil { 551 return nil, err 552 } 553 } 554 555 var generic []swarm.GenericResource 556 for _, res := range source.Reservations.GenericResources { 557 var r swarm.GenericResource 558 559 if res.DiscreteResourceSpec != nil { 560 r.DiscreteResourceSpec = &swarm.DiscreteGenericResource{ 561 Kind: res.DiscreteResourceSpec.Kind, 562 Value: res.DiscreteResourceSpec.Value, 563 } 564 } 565 566 generic = append(generic, r) 567 } 568 569 resources.Reservations = &swarm.Resources{ 570 NanoCPUs: cpus, 571 MemoryBytes: int64(source.Reservations.MemoryBytes), 572 GenericResources: generic, 573 } 574 } 575 return resources, nil 576 } 577 578 func convertEndpointSpec(endpointMode string, source []composetypes.ServicePortConfig) *swarm.EndpointSpec { 579 portConfigs := []swarm.PortConfig{} 580 for _, port := range source { 581 portConfig := swarm.PortConfig{ 582 Protocol: swarm.PortConfigProtocol(port.Protocol), 583 TargetPort: port.Target, 584 PublishedPort: port.Published, 585 PublishMode: swarm.PortConfigPublishMode(port.Mode), 586 } 587 portConfigs = append(portConfigs, portConfig) 588 } 589 590 sort.Slice(portConfigs, func(i, j int) bool { 591 return portConfigs[i].PublishedPort < portConfigs[j].PublishedPort 592 }) 593 594 return &swarm.EndpointSpec{ 595 Mode: swarm.ResolutionMode(strings.ToLower(endpointMode)), 596 Ports: portConfigs, 597 } 598 } 599 600 func convertEnvironment(source map[string]*string) []string { 601 var output []string 602 603 for name, value := range source { 604 switch value { 605 case nil: 606 output = append(output, name) 607 default: 608 output = append(output, fmt.Sprintf("%s=%s", name, *value)) 609 } 610 } 611 612 return output 613 } 614 615 func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) { 616 serviceMode := swarm.ServiceMode{} 617 618 switch mode { 619 case "global": 620 if replicas != nil { 621 return serviceMode, errors.Errorf("replicas can only be used with replicated mode") 622 } 623 serviceMode.Global = &swarm.GlobalService{} 624 case "replicated", "": 625 serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas} 626 default: 627 return serviceMode, errors.Errorf("Unknown mode: %s", mode) 628 } 629 return serviceMode, nil 630 } 631 632 func convertDNSConfig(DNS []string, DNSSearch []string) *swarm.DNSConfig { 633 if DNS != nil || DNSSearch != nil { 634 return &swarm.DNSConfig{ 635 Nameservers: DNS, 636 Search: DNSSearch, 637 } 638 } 639 return nil 640 } 641 642 func convertCredentialSpec(namespace Namespace, spec composetypes.CredentialSpecConfig, refs []*swarm.ConfigReference) (*swarm.CredentialSpec, error) { 643 var o []string 644 645 // Config was added in API v1.40 646 if spec.Config != "" { 647 o = append(o, `"Config"`) 648 } 649 if spec.File != "" { 650 o = append(o, `"File"`) 651 } 652 if spec.Registry != "" { 653 o = append(o, `"Registry"`) 654 } 655 l := len(o) 656 switch { 657 case l == 0: 658 return nil, nil 659 case l == 2: 660 return nil, errors.Errorf("invalid credential spec: cannot specify both %s and %s", o[0], o[1]) 661 case l > 2: 662 return nil, errors.Errorf("invalid credential spec: cannot specify both %s, and %s", strings.Join(o[:l-1], ", "), o[l-1]) 663 } 664 swarmCredSpec := swarm.CredentialSpec(spec) 665 // if we're using a swarm Config for the credential spec, over-write it 666 // here with the config ID 667 if swarmCredSpec.Config != "" { 668 for _, config := range refs { 669 if swarmCredSpec.Config == config.ConfigName { 670 swarmCredSpec.Config = config.ConfigID 671 return &swarmCredSpec, nil 672 } 673 } 674 // if none of the configs match, try namespacing 675 for _, config := range refs { 676 if namespace.Scope(swarmCredSpec.Config) == config.ConfigName { 677 swarmCredSpec.Config = config.ConfigID 678 return &swarmCredSpec, nil 679 } 680 } 681 return nil, errors.Errorf("invalid credential spec: spec specifies config %v, but no such config can be found", swarmCredSpec.Config) 682 } 683 return &swarmCredSpec, nil 684 } 685 686 func convertUlimits(origUlimits map[string]*composetypes.UlimitsConfig) []*units.Ulimit { 687 newUlimits := make(map[string]*units.Ulimit) 688 for name, u := range origUlimits { 689 if u.Single != 0 { 690 newUlimits[name] = &units.Ulimit{ 691 Name: name, 692 Soft: int64(u.Single), 693 Hard: int64(u.Single), 694 } 695 } else { 696 newUlimits[name] = &units.Ulimit{ 697 Name: name, 698 Soft: int64(u.Soft), 699 Hard: int64(u.Hard), 700 } 701 } 702 } 703 var ulimits []*units.Ulimit 704 for _, ulimit := range newUlimits { 705 ulimits = append(ulimits, ulimit) 706 } 707 sort.SliceStable(ulimits, func(i, j int) bool { 708 return ulimits[i].Name < ulimits[j].Name 709 }) 710 return ulimits 711 }