github.com/AliyunContainerService/cli@v0.0.0-20181009023821-814ced4b30d0/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.Configs, 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(service.CredentialSpec) 113 if err != nil { 114 return swarm.ServiceSpec{}, err 115 } 116 117 var logDriver *swarm.Driver 118 if service.Logging != nil { 119 logDriver = &swarm.Driver{ 120 Name: service.Logging.Driver, 121 Options: service.Logging.Options, 122 } 123 } 124 125 serviceSpec := swarm.ServiceSpec{ 126 Annotations: swarm.Annotations{ 127 Name: name, 128 Labels: AddStackLabel(namespace, service.Deploy.Labels), 129 }, 130 TaskTemplate: swarm.TaskSpec{ 131 ContainerSpec: &swarm.ContainerSpec{ 132 Image: service.Image, 133 Command: service.Entrypoint, 134 Args: service.Command, 135 Hostname: service.Hostname, 136 Hosts: convertExtraHosts(service.ExtraHosts), 137 DNSConfig: dnsConfig, 138 Healthcheck: healthcheck, 139 Env: sortStrings(convertEnvironment(service.Environment)), 140 Labels: AddStackLabel(namespace, service.Labels), 141 Dir: service.WorkingDir, 142 User: service.User, 143 Mounts: mounts, 144 StopGracePeriod: composetypes.ConvertDurationPtr(service.StopGracePeriod), 145 StopSignal: service.StopSignal, 146 TTY: service.Tty, 147 OpenStdin: service.StdinOpen, 148 Secrets: secrets, 149 Configs: configs, 150 ReadOnly: service.ReadOnly, 151 Privileges: &privileges, 152 Isolation: container.Isolation(service.Isolation), 153 Init: service.Init, 154 }, 155 LogDriver: logDriver, 156 Resources: resources, 157 RestartPolicy: restartPolicy, 158 Placement: &swarm.Placement{ 159 Constraints: service.Deploy.Placement.Constraints, 160 Preferences: getPlacementPreference(service.Deploy.Placement.Preferences), 161 }, 162 }, 163 EndpointSpec: endpoint, 164 Mode: mode, 165 UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig), 166 RollbackConfig: convertUpdateConfig(service.Deploy.RollbackConfig), 167 } 168 169 // add an image label to serviceSpec 170 serviceSpec.Labels[LabelImage] = service.Image 171 172 // ServiceSpec.Networks is deprecated and should not have been used by 173 // this package. It is possible to update TaskTemplate.Networks, but it 174 // is not possible to update ServiceSpec.Networks. Unfortunately, we 175 // can't unconditionally start using TaskTemplate.Networks, because that 176 // will break with older daemons that don't support migrating from 177 // ServiceSpec.Networks to TaskTemplate.Networks. So which field to use 178 // is conditional on daemon version. 179 if versions.LessThan(apiVersion, "1.29") { 180 serviceSpec.Networks = networks 181 } else { 182 serviceSpec.TaskTemplate.Networks = networks 183 } 184 return serviceSpec, nil 185 } 186 187 func getPlacementPreference(preferences []composetypes.PlacementPreferences) []swarm.PlacementPreference { 188 result := []swarm.PlacementPreference{} 189 for _, preference := range preferences { 190 spreadDescriptor := preference.Spread 191 result = append(result, swarm.PlacementPreference{ 192 Spread: &swarm.SpreadOver{ 193 SpreadDescriptor: spreadDescriptor, 194 }, 195 }) 196 } 197 return result 198 } 199 200 func sortStrings(strs []string) []string { 201 sort.Strings(strs) 202 return strs 203 } 204 205 func convertServiceNetworks( 206 networks map[string]*composetypes.ServiceNetworkConfig, 207 networkConfigs networkMap, 208 namespace Namespace, 209 name string, 210 ) ([]swarm.NetworkAttachmentConfig, error) { 211 if len(networks) == 0 { 212 networks = map[string]*composetypes.ServiceNetworkConfig{ 213 defaultNetwork: {}, 214 } 215 } 216 217 nets := []swarm.NetworkAttachmentConfig{} 218 for networkName, network := range networks { 219 networkConfig, ok := networkConfigs[networkName] 220 if !ok && networkName != defaultNetwork { 221 return nil, errors.Errorf("undefined network %q", networkName) 222 } 223 var aliases []string 224 if network != nil { 225 aliases = network.Aliases 226 } 227 target := namespace.Scope(networkName) 228 if networkConfig.Name != "" { 229 target = networkConfig.Name 230 } 231 netAttachConfig := swarm.NetworkAttachmentConfig{ 232 Target: target, 233 Aliases: aliases, 234 } 235 // Only add default aliases to user defined networks. Other networks do 236 // not support aliases. 237 if container.NetworkMode(target).IsUserDefined() { 238 netAttachConfig.Aliases = append(netAttachConfig.Aliases, name) 239 } 240 nets = append(nets, netAttachConfig) 241 } 242 243 sort.Slice(nets, func(i, j int) bool { 244 return nets[i].Target < nets[j].Target 245 }) 246 return nets, nil 247 } 248 249 // TODO: fix secrets API so that SecretAPIClient is not required here 250 func convertServiceSecrets( 251 client client.SecretAPIClient, 252 namespace Namespace, 253 secrets []composetypes.ServiceSecretConfig, 254 secretSpecs map[string]composetypes.SecretConfig, 255 ) ([]*swarm.SecretReference, error) { 256 refs := []*swarm.SecretReference{} 257 258 lookup := func(key string) (composetypes.FileObjectConfig, error) { 259 secretSpec, exists := secretSpecs[key] 260 if !exists { 261 return composetypes.FileObjectConfig{}, errors.Errorf("undefined secret %q", key) 262 } 263 return composetypes.FileObjectConfig(secretSpec), nil 264 } 265 for _, secret := range secrets { 266 obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(secret), lookup) 267 if err != nil { 268 return nil, err 269 } 270 271 file := swarm.SecretReferenceFileTarget(obj.File) 272 refs = append(refs, &swarm.SecretReference{ 273 File: &file, 274 SecretName: obj.Name, 275 }) 276 } 277 278 secrs, err := servicecli.ParseSecrets(client, refs) 279 if err != nil { 280 return nil, err 281 } 282 // sort to ensure idempotence (don't restart services just because the entries are in different order) 283 sort.SliceStable(secrs, func(i, j int) bool { return secrs[i].SecretName < secrs[j].SecretName }) 284 return secrs, err 285 } 286 287 // TODO: fix configs API so that ConfigsAPIClient is not required here 288 func convertServiceConfigObjs( 289 client client.ConfigAPIClient, 290 namespace Namespace, 291 configs []composetypes.ServiceConfigObjConfig, 292 configSpecs map[string]composetypes.ConfigObjConfig, 293 ) ([]*swarm.ConfigReference, error) { 294 refs := []*swarm.ConfigReference{} 295 296 lookup := func(key string) (composetypes.FileObjectConfig, error) { 297 configSpec, exists := configSpecs[key] 298 if !exists { 299 return composetypes.FileObjectConfig{}, errors.Errorf("undefined config %q", key) 300 } 301 return composetypes.FileObjectConfig(configSpec), nil 302 } 303 for _, config := range configs { 304 obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(config), lookup) 305 if err != nil { 306 return nil, err 307 } 308 309 file := swarm.ConfigReferenceFileTarget(obj.File) 310 refs = append(refs, &swarm.ConfigReference{ 311 File: &file, 312 ConfigName: obj.Name, 313 }) 314 } 315 316 confs, err := servicecli.ParseConfigs(client, refs) 317 if err != nil { 318 return nil, err 319 } 320 // sort to ensure idempotence (don't restart services just because the entries are in different order) 321 sort.SliceStable(confs, func(i, j int) bool { return confs[i].ConfigName < confs[j].ConfigName }) 322 return confs, err 323 } 324 325 type swarmReferenceTarget struct { 326 Name string 327 UID string 328 GID string 329 Mode os.FileMode 330 } 331 332 type swarmReferenceObject struct { 333 File swarmReferenceTarget 334 ID string 335 Name string 336 } 337 338 func convertFileObject( 339 namespace Namespace, 340 config composetypes.FileReferenceConfig, 341 lookup func(key string) (composetypes.FileObjectConfig, error), 342 ) (swarmReferenceObject, error) { 343 target := config.Target 344 if target == "" { 345 target = config.Source 346 } 347 348 obj, err := lookup(config.Source) 349 if err != nil { 350 return swarmReferenceObject{}, err 351 } 352 353 source := namespace.Scope(config.Source) 354 if obj.Name != "" { 355 source = obj.Name 356 } 357 358 uid := config.UID 359 gid := config.GID 360 if uid == "" { 361 uid = "0" 362 } 363 if gid == "" { 364 gid = "0" 365 } 366 mode := config.Mode 367 if mode == nil { 368 mode = uint32Ptr(0444) 369 } 370 371 return swarmReferenceObject{ 372 File: swarmReferenceTarget{ 373 Name: target, 374 UID: uid, 375 GID: gid, 376 Mode: os.FileMode(*mode), 377 }, 378 Name: source, 379 }, nil 380 } 381 382 func uint32Ptr(value uint32) *uint32 { 383 return &value 384 } 385 386 // convertExtraHosts converts <host>:<ip> mappings to SwarmKit notation: 387 // "IP-address hostname(s)". The original order of mappings is preserved. 388 func convertExtraHosts(extraHosts composetypes.HostsList) []string { 389 hosts := []string{} 390 for _, hostIP := range extraHosts { 391 if v := strings.SplitN(hostIP, ":", 2); len(v) == 2 { 392 // Convert to SwarmKit notation: IP-address hostname(s) 393 hosts = append(hosts, fmt.Sprintf("%s %s", v[1], v[0])) 394 } 395 } 396 return hosts 397 } 398 399 func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container.HealthConfig, error) { 400 if healthcheck == nil { 401 return nil, nil 402 } 403 var ( 404 timeout, interval, startPeriod time.Duration 405 retries int 406 ) 407 if healthcheck.Disable { 408 if len(healthcheck.Test) != 0 { 409 return nil, errors.Errorf("test and disable can't be set at the same time") 410 } 411 return &container.HealthConfig{ 412 Test: []string{"NONE"}, 413 }, nil 414 415 } 416 if healthcheck.Timeout != nil { 417 timeout = time.Duration(*healthcheck.Timeout) 418 } 419 if healthcheck.Interval != nil { 420 interval = time.Duration(*healthcheck.Interval) 421 } 422 if healthcheck.StartPeriod != nil { 423 startPeriod = time.Duration(*healthcheck.StartPeriod) 424 } 425 if healthcheck.Retries != nil { 426 retries = int(*healthcheck.Retries) 427 } 428 return &container.HealthConfig{ 429 Test: healthcheck.Test, 430 Timeout: timeout, 431 Interval: interval, 432 Retries: retries, 433 StartPeriod: startPeriod, 434 }, nil 435 } 436 437 func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) { 438 // TODO: log if restart is being ignored 439 if source == nil { 440 policy, err := opts.ParseRestartPolicy(restart) 441 if err != nil { 442 return nil, err 443 } 444 switch { 445 case policy.IsNone(): 446 return nil, nil 447 case policy.IsAlways(), policy.IsUnlessStopped(): 448 return &swarm.RestartPolicy{ 449 Condition: swarm.RestartPolicyConditionAny, 450 }, nil 451 case policy.IsOnFailure(): 452 attempts := uint64(policy.MaximumRetryCount) 453 return &swarm.RestartPolicy{ 454 Condition: swarm.RestartPolicyConditionOnFailure, 455 MaxAttempts: &attempts, 456 }, nil 457 default: 458 return nil, errors.Errorf("unknown restart policy: %s", restart) 459 } 460 } 461 462 return &swarm.RestartPolicy{ 463 Condition: swarm.RestartPolicyCondition(source.Condition), 464 Delay: composetypes.ConvertDurationPtr(source.Delay), 465 MaxAttempts: source.MaxAttempts, 466 Window: composetypes.ConvertDurationPtr(source.Window), 467 }, nil 468 } 469 470 func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig { 471 if source == nil { 472 return nil 473 } 474 parallel := uint64(1) 475 if source.Parallelism != nil { 476 parallel = *source.Parallelism 477 } 478 return &swarm.UpdateConfig{ 479 Parallelism: parallel, 480 Delay: time.Duration(source.Delay), 481 FailureAction: source.FailureAction, 482 Monitor: time.Duration(source.Monitor), 483 MaxFailureRatio: source.MaxFailureRatio, 484 Order: source.Order, 485 } 486 } 487 488 func convertResources(source composetypes.Resources) (*swarm.ResourceRequirements, error) { 489 resources := &swarm.ResourceRequirements{} 490 var err error 491 if source.Limits != nil { 492 var cpus int64 493 if source.Limits.NanoCPUs != "" { 494 cpus, err = opts.ParseCPUs(source.Limits.NanoCPUs) 495 if err != nil { 496 return nil, err 497 } 498 } 499 resources.Limits = &swarm.Resources{ 500 NanoCPUs: cpus, 501 MemoryBytes: int64(source.Limits.MemoryBytes), 502 } 503 } 504 if source.Reservations != nil { 505 var cpus int64 506 if source.Reservations.NanoCPUs != "" { 507 cpus, err = opts.ParseCPUs(source.Reservations.NanoCPUs) 508 if err != nil { 509 return nil, err 510 } 511 } 512 513 var generic []swarm.GenericResource 514 for _, res := range source.Reservations.GenericResources { 515 var r swarm.GenericResource 516 517 if res.DiscreteResourceSpec != nil { 518 r.DiscreteResourceSpec = &swarm.DiscreteGenericResource{ 519 Kind: res.DiscreteResourceSpec.Kind, 520 Value: res.DiscreteResourceSpec.Value, 521 } 522 } 523 524 generic = append(generic, r) 525 } 526 527 resources.Reservations = &swarm.Resources{ 528 NanoCPUs: cpus, 529 MemoryBytes: int64(source.Reservations.MemoryBytes), 530 GenericResources: generic, 531 } 532 } 533 return resources, nil 534 } 535 536 func convertEndpointSpec(endpointMode string, source []composetypes.ServicePortConfig) (*swarm.EndpointSpec, error) { 537 portConfigs := []swarm.PortConfig{} 538 for _, port := range source { 539 portConfig := swarm.PortConfig{ 540 Protocol: swarm.PortConfigProtocol(port.Protocol), 541 TargetPort: port.Target, 542 PublishedPort: port.Published, 543 PublishMode: swarm.PortConfigPublishMode(port.Mode), 544 } 545 portConfigs = append(portConfigs, portConfig) 546 } 547 548 sort.Slice(portConfigs, func(i, j int) bool { 549 return portConfigs[i].PublishedPort < portConfigs[j].PublishedPort 550 }) 551 552 return &swarm.EndpointSpec{ 553 Mode: swarm.ResolutionMode(strings.ToLower(endpointMode)), 554 Ports: portConfigs, 555 }, nil 556 } 557 558 func convertEnvironment(source map[string]*string) []string { 559 var output []string 560 561 for name, value := range source { 562 switch value { 563 case nil: 564 output = append(output, name) 565 default: 566 output = append(output, fmt.Sprintf("%s=%s", name, *value)) 567 } 568 } 569 570 return output 571 } 572 573 func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) { 574 serviceMode := swarm.ServiceMode{} 575 576 switch mode { 577 case "global": 578 if replicas != nil { 579 return serviceMode, errors.Errorf("replicas can only be used with replicated mode") 580 } 581 serviceMode.Global = &swarm.GlobalService{} 582 case "replicated", "": 583 serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas} 584 default: 585 return serviceMode, errors.Errorf("Unknown mode: %s", mode) 586 } 587 return serviceMode, nil 588 } 589 590 func convertDNSConfig(DNS []string, DNSSearch []string) (*swarm.DNSConfig, error) { 591 if DNS != nil || DNSSearch != nil { 592 return &swarm.DNSConfig{ 593 Nameservers: DNS, 594 Search: DNSSearch, 595 }, nil 596 } 597 return nil, nil 598 } 599 600 func convertCredentialSpec(spec composetypes.CredentialSpecConfig) (*swarm.CredentialSpec, error) { 601 if spec.File == "" && spec.Registry == "" { 602 return nil, nil 603 } 604 if spec.File != "" && spec.Registry != "" { 605 return nil, errors.New("Invalid credential spec - must provide one of `File` or `Registry`") 606 } 607 swarmCredSpec := swarm.CredentialSpec(spec) 608 return &swarmCredSpec, nil 609 }