github.com/itscaro/cli@v0.0.0-20190705081621-c9db0fe93829/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/pkg/errors" 18 ) 19 20 const ( 21 defaultNetwork = "default" 22 // LabelImage is the label used to store image name provided in the compose file 23 LabelImage = "com.docker.stack.image" 24 ) 25 26 // Services from compose-file types to engine API types 27 func Services( 28 namespace Namespace, 29 config *composetypes.Config, 30 client client.CommonAPIClient, 31 ) (map[string]swarm.ServiceSpec, error) { 32 result := make(map[string]swarm.ServiceSpec) 33 34 services := config.Services 35 volumes := config.Volumes 36 networks := config.Networks 37 38 for _, service := range services { 39 secrets, err := convertServiceSecrets(client, namespace, service.Secrets, config.Secrets) 40 if err != nil { 41 return nil, errors.Wrapf(err, "service %s", service.Name) 42 } 43 configs, err := convertServiceConfigObjs(client, namespace, service, config.Configs) 44 if err != nil { 45 return nil, errors.Wrapf(err, "service %s", service.Name) 46 } 47 48 serviceSpec, err := Service(client.ClientVersion(), namespace, service, networks, volumes, secrets, configs) 49 if err != nil { 50 return nil, errors.Wrapf(err, "service %s", service.Name) 51 } 52 result[service.Name] = serviceSpec 53 } 54 55 return result, nil 56 } 57 58 // Service converts a ServiceConfig into a swarm ServiceSpec 59 func Service( 60 apiVersion string, 61 namespace Namespace, 62 service composetypes.ServiceConfig, 63 networkConfigs map[string]composetypes.NetworkConfig, 64 volumes map[string]composetypes.VolumeConfig, 65 secrets []*swarm.SecretReference, 66 configs []*swarm.ConfigReference, 67 ) (swarm.ServiceSpec, error) { 68 name := namespace.Scope(service.Name) 69 70 endpoint, err := convertEndpointSpec(service.Deploy.EndpointMode, service.Ports) 71 if err != nil { 72 return swarm.ServiceSpec{}, err 73 } 74 75 mode, err := convertDeployMode(service.Deploy.Mode, service.Deploy.Replicas) 76 if err != nil { 77 return swarm.ServiceSpec{}, err 78 } 79 80 mounts, err := Volumes(service.Volumes, volumes, namespace) 81 if err != nil { 82 return swarm.ServiceSpec{}, err 83 } 84 85 resources, err := convertResources(service.Deploy.Resources) 86 if err != nil { 87 return swarm.ServiceSpec{}, err 88 } 89 90 restartPolicy, err := convertRestartPolicy( 91 service.Restart, service.Deploy.RestartPolicy) 92 if err != nil { 93 return swarm.ServiceSpec{}, err 94 } 95 96 healthcheck, err := convertHealthcheck(service.HealthCheck) 97 if err != nil { 98 return swarm.ServiceSpec{}, err 99 } 100 101 networks, err := convertServiceNetworks(service.Networks, networkConfigs, namespace, service.Name) 102 if err != nil { 103 return swarm.ServiceSpec{}, err 104 } 105 106 dnsConfig, err := convertDNSConfig(service.DNS, service.DNSSearch) 107 if err != nil { 108 return swarm.ServiceSpec{}, err 109 } 110 111 var privileges swarm.Privileges 112 privileges.CredentialSpec, err = convertCredentialSpec( 113 namespace, service.CredentialSpec, configs, 114 ) 115 if err != nil { 116 return swarm.ServiceSpec{}, err 117 } 118 119 var logDriver *swarm.Driver 120 if service.Logging != nil { 121 logDriver = &swarm.Driver{ 122 Name: service.Logging.Driver, 123 Options: service.Logging.Options, 124 } 125 } 126 127 serviceSpec := swarm.ServiceSpec{ 128 Annotations: swarm.Annotations{ 129 Name: name, 130 Labels: AddStackLabel(namespace, service.Deploy.Labels), 131 }, 132 TaskTemplate: swarm.TaskSpec{ 133 ContainerSpec: &swarm.ContainerSpec{ 134 Image: service.Image, 135 Command: service.Entrypoint, 136 Args: service.Command, 137 Hostname: service.Hostname, 138 Hosts: convertExtraHosts(service.ExtraHosts), 139 DNSConfig: dnsConfig, 140 Healthcheck: healthcheck, 141 Env: sortStrings(convertEnvironment(service.Environment)), 142 Labels: AddStackLabel(namespace, service.Labels), 143 Dir: service.WorkingDir, 144 User: service.User, 145 Mounts: mounts, 146 StopGracePeriod: composetypes.ConvertDurationPtr(service.StopGracePeriod), 147 StopSignal: service.StopSignal, 148 TTY: service.Tty, 149 OpenStdin: service.StdinOpen, 150 Secrets: secrets, 151 Configs: configs, 152 ReadOnly: service.ReadOnly, 153 Privileges: &privileges, 154 Isolation: container.Isolation(service.Isolation), 155 Init: service.Init, 156 Sysctls: service.Sysctls, 157 }, 158 LogDriver: logDriver, 159 Resources: resources, 160 RestartPolicy: restartPolicy, 161 Placement: &swarm.Placement{ 162 Constraints: service.Deploy.Placement.Constraints, 163 Preferences: getPlacementPreference(service.Deploy.Placement.Preferences), 164 MaxReplicas: service.Deploy.Placement.MaxReplicas, 165 }, 166 }, 167 EndpointSpec: endpoint, 168 Mode: mode, 169 UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig), 170 RollbackConfig: convertUpdateConfig(service.Deploy.RollbackConfig), 171 } 172 173 // add an image label to serviceSpec 174 serviceSpec.Labels[LabelImage] = service.Image 175 176 // ServiceSpec.Networks is deprecated and should not have been used by 177 // this package. It is possible to update TaskTemplate.Networks, but it 178 // is not possible to update ServiceSpec.Networks. Unfortunately, we 179 // can't unconditionally start using TaskTemplate.Networks, because that 180 // will break with older daemons that don't support migrating from 181 // ServiceSpec.Networks to TaskTemplate.Networks. So which field to use 182 // is conditional on daemon version. 183 if versions.LessThan(apiVersion, "1.29") { 184 serviceSpec.Networks = networks 185 } else { 186 serviceSpec.TaskTemplate.Networks = networks 187 } 188 return serviceSpec, nil 189 } 190 191 func getPlacementPreference(preferences []composetypes.PlacementPreferences) []swarm.PlacementPreference { 192 result := []swarm.PlacementPreference{} 193 for _, preference := range preferences { 194 spreadDescriptor := preference.Spread 195 result = append(result, swarm.PlacementPreference{ 196 Spread: &swarm.SpreadOver{ 197 SpreadDescriptor: spreadDescriptor, 198 }, 199 }) 200 } 201 return result 202 } 203 204 func sortStrings(strs []string) []string { 205 sort.Strings(strs) 206 return strs 207 } 208 209 func convertServiceNetworks( 210 networks map[string]*composetypes.ServiceNetworkConfig, 211 networkConfigs networkMap, 212 namespace Namespace, 213 name string, 214 ) ([]swarm.NetworkAttachmentConfig, error) { 215 if len(networks) == 0 { 216 networks = map[string]*composetypes.ServiceNetworkConfig{ 217 defaultNetwork: {}, 218 } 219 } 220 221 nets := []swarm.NetworkAttachmentConfig{} 222 for networkName, network := range networks { 223 networkConfig, ok := networkConfigs[networkName] 224 if !ok && networkName != defaultNetwork { 225 return nil, errors.Errorf("undefined network %q", networkName) 226 } 227 var aliases []string 228 if network != nil { 229 aliases = network.Aliases 230 } 231 target := namespace.Scope(networkName) 232 if networkConfig.Name != "" { 233 target = networkConfig.Name 234 } 235 netAttachConfig := swarm.NetworkAttachmentConfig{ 236 Target: target, 237 Aliases: aliases, 238 } 239 // Only add default aliases to user defined networks. Other networks do 240 // not support aliases. 241 if container.NetworkMode(target).IsUserDefined() { 242 netAttachConfig.Aliases = append(netAttachConfig.Aliases, name) 243 } 244 nets = append(nets, netAttachConfig) 245 } 246 247 sort.Slice(nets, func(i, j int) bool { 248 return nets[i].Target < nets[j].Target 249 }) 250 return nets, nil 251 } 252 253 // TODO: fix secrets API so that SecretAPIClient is not required here 254 func convertServiceSecrets( 255 client client.SecretAPIClient, 256 namespace Namespace, 257 secrets []composetypes.ServiceSecretConfig, 258 secretSpecs map[string]composetypes.SecretConfig, 259 ) ([]*swarm.SecretReference, error) { 260 refs := []*swarm.SecretReference{} 261 262 lookup := func(key string) (composetypes.FileObjectConfig, error) { 263 secretSpec, exists := secretSpecs[key] 264 if !exists { 265 return composetypes.FileObjectConfig{}, errors.Errorf("undefined secret %q", key) 266 } 267 return composetypes.FileObjectConfig(secretSpec), nil 268 } 269 for _, secret := range secrets { 270 obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(secret), lookup) 271 if err != nil { 272 return nil, err 273 } 274 275 file := swarm.SecretReferenceFileTarget(obj.File) 276 refs = append(refs, &swarm.SecretReference{ 277 File: &file, 278 SecretName: obj.Name, 279 }) 280 } 281 282 secrs, err := servicecli.ParseSecrets(client, refs) 283 if err != nil { 284 return nil, err 285 } 286 // sort to ensure idempotence (don't restart services just because the entries are in different order) 287 sort.SliceStable(secrs, func(i, j int) bool { return secrs[i].SecretName < secrs[j].SecretName }) 288 return secrs, err 289 } 290 291 // convertServiceConfigObjs takes an API client, a namespace, a ServiceConfig, 292 // and a set of compose Config specs, and creates the swarm ConfigReferences 293 // required by the serivce. Unlike convertServiceSecrets, this takes the whole 294 // ServiceConfig, because some Configs may be needed as a result of other 295 // fields (like CredentialSpecs). 296 // 297 // TODO: fix configs API so that ConfigsAPIClient is not required here 298 func convertServiceConfigObjs( 299 client client.ConfigAPIClient, 300 namespace Namespace, 301 service composetypes.ServiceConfig, 302 configSpecs map[string]composetypes.ConfigObjConfig, 303 ) ([]*swarm.ConfigReference, error) { 304 refs := []*swarm.ConfigReference{} 305 306 lookup := func(key string) (composetypes.FileObjectConfig, error) { 307 configSpec, exists := configSpecs[key] 308 if !exists { 309 return composetypes.FileObjectConfig{}, errors.Errorf("undefined config %q", key) 310 } 311 return composetypes.FileObjectConfig(configSpec), nil 312 } 313 for _, config := range service.Configs { 314 obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(config), lookup) 315 if err != nil { 316 return nil, err 317 } 318 319 file := swarm.ConfigReferenceFileTarget(obj.File) 320 refs = append(refs, &swarm.ConfigReference{ 321 File: &file, 322 ConfigName: obj.Name, 323 }) 324 } 325 326 // finally, after converting all of the file objects, create any 327 // Runtime-type configs that are needed. these are configs that are not 328 // mounted into the container, but are used in some other way by the 329 // container runtime. Currently, this only means CredentialSpecs, but in 330 // the future it may be used for other fields 331 332 // grab the CredentialSpec out of the Service 333 credSpec := service.CredentialSpec 334 // if the credSpec uses a config, then we should grab the config name, and 335 // create a config reference for it. A File or Registry-type CredentialSpec 336 // does not need this operation. 337 if credSpec.Config != "" { 338 // look up the config in the configSpecs. 339 obj, err := lookup(credSpec.Config) 340 if err != nil { 341 return nil, err 342 } 343 344 // get the actual correct name. 345 name := namespace.Scope(credSpec.Config) 346 if obj.Name != "" { 347 name = obj.Name 348 } 349 350 // now append a Runtime-type config. 351 refs = append(refs, &swarm.ConfigReference{ 352 ConfigName: name, 353 Runtime: &swarm.ConfigReferenceRuntimeTarget{}, 354 }) 355 356 } 357 358 confs, err := servicecli.ParseConfigs(client, refs) 359 if err != nil { 360 return nil, err 361 } 362 // sort to ensure idempotence (don't restart services just because the entries are in different order) 363 sort.SliceStable(confs, func(i, j int) bool { return confs[i].ConfigName < confs[j].ConfigName }) 364 return confs, err 365 } 366 367 type swarmReferenceTarget struct { 368 Name string 369 UID string 370 GID string 371 Mode os.FileMode 372 } 373 374 type swarmReferenceObject struct { 375 File swarmReferenceTarget 376 ID string 377 Name string 378 } 379 380 func convertFileObject( 381 namespace Namespace, 382 config composetypes.FileReferenceConfig, 383 lookup func(key string) (composetypes.FileObjectConfig, error), 384 ) (swarmReferenceObject, error) { 385 obj, err := lookup(config.Source) 386 if err != nil { 387 return swarmReferenceObject{}, err 388 } 389 390 source := namespace.Scope(config.Source) 391 if obj.Name != "" { 392 source = obj.Name 393 } 394 395 target := config.Target 396 if target == "" { 397 target = config.Source 398 } 399 400 uid := config.UID 401 gid := config.GID 402 if uid == "" { 403 uid = "0" 404 } 405 if gid == "" { 406 gid = "0" 407 } 408 mode := config.Mode 409 if mode == nil { 410 mode = uint32Ptr(0444) 411 } 412 413 return swarmReferenceObject{ 414 File: swarmReferenceTarget{ 415 Name: target, 416 UID: uid, 417 GID: gid, 418 Mode: os.FileMode(*mode), 419 }, 420 Name: source, 421 }, nil 422 } 423 424 func uint32Ptr(value uint32) *uint32 { 425 return &value 426 } 427 428 // convertExtraHosts converts <host>:<ip> mappings to SwarmKit notation: 429 // "IP-address hostname(s)". The original order of mappings is preserved. 430 func convertExtraHosts(extraHosts composetypes.HostsList) []string { 431 hosts := []string{} 432 for _, hostIP := range extraHosts { 433 if v := strings.SplitN(hostIP, ":", 2); len(v) == 2 { 434 // Convert to SwarmKit notation: IP-address hostname(s) 435 hosts = append(hosts, fmt.Sprintf("%s %s", v[1], v[0])) 436 } 437 } 438 return hosts 439 } 440 441 func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container.HealthConfig, error) { 442 if healthcheck == nil { 443 return nil, nil 444 } 445 var ( 446 timeout, interval, startPeriod time.Duration 447 retries int 448 ) 449 if healthcheck.Disable { 450 if len(healthcheck.Test) != 0 { 451 return nil, errors.Errorf("test and disable can't be set at the same time") 452 } 453 return &container.HealthConfig{ 454 Test: []string{"NONE"}, 455 }, nil 456 457 } 458 if healthcheck.Timeout != nil { 459 timeout = time.Duration(*healthcheck.Timeout) 460 } 461 if healthcheck.Interval != nil { 462 interval = time.Duration(*healthcheck.Interval) 463 } 464 if healthcheck.StartPeriod != nil { 465 startPeriod = time.Duration(*healthcheck.StartPeriod) 466 } 467 if healthcheck.Retries != nil { 468 retries = int(*healthcheck.Retries) 469 } 470 return &container.HealthConfig{ 471 Test: healthcheck.Test, 472 Timeout: timeout, 473 Interval: interval, 474 Retries: retries, 475 StartPeriod: startPeriod, 476 }, nil 477 } 478 479 func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) { 480 // TODO: log if restart is being ignored 481 if source == nil { 482 policy, err := opts.ParseRestartPolicy(restart) 483 if err != nil { 484 return nil, err 485 } 486 switch { 487 case policy.IsNone(): 488 return nil, nil 489 case policy.IsAlways(), policy.IsUnlessStopped(): 490 return &swarm.RestartPolicy{ 491 Condition: swarm.RestartPolicyConditionAny, 492 }, nil 493 case policy.IsOnFailure(): 494 attempts := uint64(policy.MaximumRetryCount) 495 return &swarm.RestartPolicy{ 496 Condition: swarm.RestartPolicyConditionOnFailure, 497 MaxAttempts: &attempts, 498 }, nil 499 default: 500 return nil, errors.Errorf("unknown restart policy: %s", restart) 501 } 502 } 503 504 return &swarm.RestartPolicy{ 505 Condition: swarm.RestartPolicyCondition(source.Condition), 506 Delay: composetypes.ConvertDurationPtr(source.Delay), 507 MaxAttempts: source.MaxAttempts, 508 Window: composetypes.ConvertDurationPtr(source.Window), 509 }, nil 510 } 511 512 func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig { 513 if source == nil { 514 return nil 515 } 516 parallel := uint64(1) 517 if source.Parallelism != nil { 518 parallel = *source.Parallelism 519 } 520 return &swarm.UpdateConfig{ 521 Parallelism: parallel, 522 Delay: time.Duration(source.Delay), 523 FailureAction: source.FailureAction, 524 Monitor: time.Duration(source.Monitor), 525 MaxFailureRatio: source.MaxFailureRatio, 526 Order: source.Order, 527 } 528 } 529 530 func convertResources(source composetypes.Resources) (*swarm.ResourceRequirements, error) { 531 resources := &swarm.ResourceRequirements{} 532 var err error 533 if source.Limits != nil { 534 var cpus int64 535 if source.Limits.NanoCPUs != "" { 536 cpus, err = opts.ParseCPUs(source.Limits.NanoCPUs) 537 if err != nil { 538 return nil, err 539 } 540 } 541 resources.Limits = &swarm.Resources{ 542 NanoCPUs: cpus, 543 MemoryBytes: int64(source.Limits.MemoryBytes), 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, error) { 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 }, nil 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, error) { 633 if DNS != nil || DNSSearch != nil { 634 return &swarm.DNSConfig{ 635 Nameservers: DNS, 636 Search: DNSSearch, 637 }, nil 638 } 639 return nil, 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 }