github.1git.de/docker/cli@v26.1.3+incompatible/cli/command/service/opts.go (about) 1 // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: 2 //go:build go1.19 3 4 package service 5 6 import ( 7 "context" 8 "fmt" 9 "sort" 10 "strconv" 11 "strings" 12 "time" 13 14 "github.com/docker/cli/cli/command" 15 "github.com/docker/cli/opts" 16 "github.com/docker/docker/api/types" 17 "github.com/docker/docker/api/types/container" 18 "github.com/docker/docker/api/types/swarm" 19 "github.com/docker/docker/client" 20 gogotypes "github.com/gogo/protobuf/types" 21 "github.com/google/shlex" 22 "github.com/moby/swarmkit/v2/api" 23 "github.com/moby/swarmkit/v2/api/defaults" 24 "github.com/pkg/errors" 25 "github.com/spf13/pflag" 26 ) 27 28 type int64Value interface { 29 Value() int64 30 } 31 32 // Uint64Opt represents a uint64. 33 type Uint64Opt struct { 34 value *uint64 35 } 36 37 // Set a new value on the option 38 func (i *Uint64Opt) Set(s string) error { 39 v, err := strconv.ParseUint(s, 0, 64) 40 i.value = &v 41 return err 42 } 43 44 // Type returns the type of this option, which will be displayed in `--help` output 45 func (i *Uint64Opt) Type() string { 46 return "uint" 47 } 48 49 // String returns a string repr of this option 50 func (i *Uint64Opt) String() string { 51 if i.value != nil { 52 return strconv.FormatUint(*i.value, 10) 53 } 54 return "" 55 } 56 57 // Value returns the uint64 58 func (i *Uint64Opt) Value() *uint64 { 59 return i.value 60 } 61 62 type floatValue float32 63 64 func (f *floatValue) Set(s string) error { 65 v, err := strconv.ParseFloat(s, 32) 66 *f = floatValue(v) 67 return err 68 } 69 70 func (f *floatValue) Type() string { 71 return "float" 72 } 73 74 func (f *floatValue) String() string { 75 return strconv.FormatFloat(float64(*f), 'g', -1, 32) 76 } 77 78 func (f *floatValue) Value() float32 { 79 return float32(*f) 80 } 81 82 // placementPrefOpts holds a list of placement preferences. 83 type placementPrefOpts struct { 84 prefs []swarm.PlacementPreference 85 strings []string 86 } 87 88 func (o *placementPrefOpts) String() string { 89 if len(o.strings) == 0 { 90 return "" 91 } 92 return fmt.Sprintf("%v", o.strings) 93 } 94 95 // Set validates the input value and adds it to the internal slices. 96 // Note: in the future strategies other than "spread", may be supported, 97 // as well as additional comma-separated options. 98 func (o *placementPrefOpts) Set(value string) error { 99 strategy, arg, ok := strings.Cut(value, "=") 100 if !ok || strategy == "" { 101 return errors.New(`placement preference must be of the format "<strategy>=<arg>"`) 102 } 103 if strategy != "spread" { 104 return errors.Errorf("unsupported placement preference %s (only spread is supported)", strategy) 105 } 106 107 o.prefs = append(o.prefs, swarm.PlacementPreference{ 108 Spread: &swarm.SpreadOver{ 109 SpreadDescriptor: arg, 110 }, 111 }) 112 o.strings = append(o.strings, value) 113 return nil 114 } 115 116 // Type returns a string name for this Option type 117 func (o *placementPrefOpts) Type() string { 118 return "pref" 119 } 120 121 // ShlexOpt is a flag Value which parses a string as a list of shell words 122 type ShlexOpt []string 123 124 // Set the value 125 func (s *ShlexOpt) Set(value string) error { 126 valueSlice, err := shlex.Split(value) 127 if err != nil { 128 return err 129 } 130 *s = valueSlice 131 return nil 132 } 133 134 // Type returns the tyep of the value 135 func (s *ShlexOpt) Type() string { 136 return "command" 137 } 138 139 func (s *ShlexOpt) String() string { 140 if len(*s) == 0 { 141 return "" 142 } 143 return fmt.Sprint(*s) 144 } 145 146 // Value returns the value as a string slice 147 func (s *ShlexOpt) Value() []string { 148 return []string(*s) 149 } 150 151 type updateOptions struct { 152 parallelism uint64 153 delay time.Duration 154 monitor time.Duration 155 onFailure string 156 maxFailureRatio floatValue 157 order string 158 } 159 160 func updateConfigFromDefaults(defaultUpdateConfig *api.UpdateConfig) *swarm.UpdateConfig { 161 defaultFailureAction := strings.ToLower(api.UpdateConfig_FailureAction_name[int32(defaultUpdateConfig.FailureAction)]) 162 defaultMonitor, _ := gogotypes.DurationFromProto(defaultUpdateConfig.Monitor) 163 return &swarm.UpdateConfig{ 164 Parallelism: defaultUpdateConfig.Parallelism, 165 Delay: defaultUpdateConfig.Delay, 166 Monitor: defaultMonitor, 167 FailureAction: defaultFailureAction, 168 MaxFailureRatio: defaultUpdateConfig.MaxFailureRatio, 169 Order: defaultOrder(defaultUpdateConfig.Order), 170 } 171 } 172 173 func (o updateOptions) updateConfig(flags *pflag.FlagSet) *swarm.UpdateConfig { 174 if !anyChanged(flags, flagUpdateParallelism, flagUpdateDelay, flagUpdateMonitor, flagUpdateFailureAction, flagUpdateMaxFailureRatio, flagUpdateOrder) { 175 return nil 176 } 177 178 updateConfig := updateConfigFromDefaults(defaults.Service.Update) 179 180 if flags.Changed(flagUpdateParallelism) { 181 updateConfig.Parallelism = o.parallelism 182 } 183 if flags.Changed(flagUpdateDelay) { 184 updateConfig.Delay = o.delay 185 } 186 if flags.Changed(flagUpdateMonitor) { 187 updateConfig.Monitor = o.monitor 188 } 189 if flags.Changed(flagUpdateFailureAction) { 190 updateConfig.FailureAction = o.onFailure 191 } 192 if flags.Changed(flagUpdateMaxFailureRatio) { 193 updateConfig.MaxFailureRatio = o.maxFailureRatio.Value() 194 } 195 if flags.Changed(flagUpdateOrder) { 196 updateConfig.Order = o.order 197 } 198 199 return updateConfig 200 } 201 202 func (o updateOptions) rollbackConfig(flags *pflag.FlagSet) *swarm.UpdateConfig { 203 if !anyChanged(flags, flagRollbackParallelism, flagRollbackDelay, flagRollbackMonitor, flagRollbackFailureAction, flagRollbackMaxFailureRatio, flagRollbackOrder) { 204 return nil 205 } 206 207 updateConfig := updateConfigFromDefaults(defaults.Service.Rollback) 208 209 if flags.Changed(flagRollbackParallelism) { 210 updateConfig.Parallelism = o.parallelism 211 } 212 if flags.Changed(flagRollbackDelay) { 213 updateConfig.Delay = o.delay 214 } 215 if flags.Changed(flagRollbackMonitor) { 216 updateConfig.Monitor = o.monitor 217 } 218 if flags.Changed(flagRollbackFailureAction) { 219 updateConfig.FailureAction = o.onFailure 220 } 221 if flags.Changed(flagRollbackMaxFailureRatio) { 222 updateConfig.MaxFailureRatio = o.maxFailureRatio.Value() 223 } 224 if flags.Changed(flagRollbackOrder) { 225 updateConfig.Order = o.order 226 } 227 228 return updateConfig 229 } 230 231 type resourceOptions struct { 232 limitCPU opts.NanoCPUs 233 limitMemBytes opts.MemBytes 234 limitPids int64 235 resCPU opts.NanoCPUs 236 resMemBytes opts.MemBytes 237 resGenericResources []string 238 } 239 240 func (r *resourceOptions) ToResourceRequirements() (*swarm.ResourceRequirements, error) { 241 generic, err := ParseGenericResources(r.resGenericResources) 242 if err != nil { 243 return nil, err 244 } 245 246 return &swarm.ResourceRequirements{ 247 Limits: &swarm.Limit{ 248 NanoCPUs: r.limitCPU.Value(), 249 MemoryBytes: r.limitMemBytes.Value(), 250 Pids: r.limitPids, 251 }, 252 Reservations: &swarm.Resources{ 253 NanoCPUs: r.resCPU.Value(), 254 MemoryBytes: r.resMemBytes.Value(), 255 GenericResources: generic, 256 }, 257 }, nil 258 } 259 260 type restartPolicyOptions struct { 261 condition string 262 delay opts.DurationOpt 263 maxAttempts Uint64Opt 264 window opts.DurationOpt 265 } 266 267 func defaultRestartPolicy() *swarm.RestartPolicy { 268 defaultMaxAttempts := defaults.Service.Task.Restart.MaxAttempts 269 rp := &swarm.RestartPolicy{ 270 MaxAttempts: &defaultMaxAttempts, 271 } 272 273 if defaults.Service.Task.Restart.Delay != nil { 274 defaultRestartDelay, _ := gogotypes.DurationFromProto(defaults.Service.Task.Restart.Delay) 275 rp.Delay = &defaultRestartDelay 276 } 277 if defaults.Service.Task.Restart.Window != nil { 278 defaultRestartWindow, _ := gogotypes.DurationFromProto(defaults.Service.Task.Restart.Window) 279 rp.Window = &defaultRestartWindow 280 } 281 rp.Condition = defaultRestartCondition() 282 283 return rp 284 } 285 286 func defaultRestartCondition() swarm.RestartPolicyCondition { 287 switch defaults.Service.Task.Restart.Condition { 288 case api.RestartOnNone: 289 return "none" 290 case api.RestartOnFailure: 291 return "on-failure" 292 case api.RestartOnAny: 293 return "any" 294 default: 295 return "" 296 } 297 } 298 299 func defaultOrder(order api.UpdateConfig_UpdateOrder) string { 300 switch order { 301 case api.UpdateConfig_STOP_FIRST: 302 return "stop-first" 303 case api.UpdateConfig_START_FIRST: 304 return "start-first" 305 default: 306 return "" 307 } 308 } 309 310 func (r *restartPolicyOptions) ToRestartPolicy(flags *pflag.FlagSet) *swarm.RestartPolicy { 311 if !anyChanged(flags, flagRestartDelay, flagRestartMaxAttempts, flagRestartWindow, flagRestartCondition) { 312 return nil 313 } 314 315 restartPolicy := defaultRestartPolicy() 316 317 if flags.Changed(flagRestartDelay) { 318 restartPolicy.Delay = r.delay.Value() 319 } 320 if flags.Changed(flagRestartCondition) { 321 restartPolicy.Condition = swarm.RestartPolicyCondition(r.condition) 322 } 323 if flags.Changed(flagRestartMaxAttempts) { 324 restartPolicy.MaxAttempts = r.maxAttempts.Value() 325 } 326 if flags.Changed(flagRestartWindow) { 327 restartPolicy.Window = r.window.Value() 328 } 329 330 return restartPolicy 331 } 332 333 type credentialSpecOpt struct { 334 value *swarm.CredentialSpec 335 source string 336 } 337 338 func (c *credentialSpecOpt) Set(value string) error { 339 c.source = value 340 c.value = &swarm.CredentialSpec{} 341 switch { 342 case strings.HasPrefix(value, "config://"): 343 // NOTE(dperny): we allow the user to specify the value of 344 // CredentialSpec Config using the Name of the config, but the API 345 // requires the ID of the config. For simplicity, we will parse 346 // whatever value is provided into the "Config" field, but before 347 // making API calls, we may need to swap the Config Name for the ID. 348 // Therefore, this isn't the definitive location for the value of 349 // Config that is passed to the API. 350 c.value.Config = strings.TrimPrefix(value, "config://") 351 case strings.HasPrefix(value, "file://"): 352 c.value.File = strings.TrimPrefix(value, "file://") 353 case strings.HasPrefix(value, "registry://"): 354 c.value.Registry = strings.TrimPrefix(value, "registry://") 355 case value == "": 356 // if the value of the flag is an empty string, that means there is no 357 // CredentialSpec needed. This is useful for removing a CredentialSpec 358 // during a service update. 359 default: 360 return errors.New(`invalid credential spec: value must be prefixed with "config://", "file://", or "registry://"`) 361 } 362 363 return nil 364 } 365 366 func (c *credentialSpecOpt) Type() string { 367 return "credential-spec" 368 } 369 370 func (c *credentialSpecOpt) String() string { 371 return c.source 372 } 373 374 func (c *credentialSpecOpt) Value() *swarm.CredentialSpec { 375 return c.value 376 } 377 378 func resolveNetworkID(ctx context.Context, apiClient client.NetworkAPIClient, networkIDOrName string) (string, error) { 379 nw, err := apiClient.NetworkInspect(ctx, networkIDOrName, types.NetworkInspectOptions{Scope: "swarm"}) 380 return nw.ID, err 381 } 382 383 func convertNetworks(networks opts.NetworkOpt) []swarm.NetworkAttachmentConfig { 384 nws := networks.Value() 385 netAttach := make([]swarm.NetworkAttachmentConfig, 0, len(nws)) 386 for _, net := range nws { 387 netAttach = append(netAttach, swarm.NetworkAttachmentConfig{ 388 Target: net.Target, 389 Aliases: net.Aliases, 390 DriverOpts: net.DriverOpts, 391 }) 392 } 393 return netAttach 394 } 395 396 type endpointOptions struct { 397 mode string 398 publishPorts opts.PortOpt 399 } 400 401 func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec { 402 return &swarm.EndpointSpec{ 403 Mode: swarm.ResolutionMode(strings.ToLower(e.mode)), 404 Ports: e.publishPorts.Value(), 405 } 406 } 407 408 type logDriverOptions struct { 409 name string 410 opts opts.ListOpts 411 } 412 413 func newLogDriverOptions() logDriverOptions { 414 return logDriverOptions{opts: opts.NewListOpts(opts.ValidateEnv)} 415 } 416 417 func (ldo *logDriverOptions) toLogDriver() *swarm.Driver { 418 if ldo.name == "" { 419 return nil 420 } 421 422 // set the log driver only if specified. 423 return &swarm.Driver{ 424 Name: ldo.name, 425 Options: opts.ConvertKVStringsToMap(ldo.opts.GetAll()), 426 } 427 } 428 429 type healthCheckOptions struct { 430 cmd string 431 interval opts.PositiveDurationOpt 432 timeout opts.PositiveDurationOpt 433 retries int 434 startPeriod opts.PositiveDurationOpt 435 startInterval opts.PositiveDurationOpt 436 noHealthcheck bool 437 } 438 439 func (o *healthCheckOptions) toHealthConfig() (*container.HealthConfig, error) { 440 var healthConfig *container.HealthConfig 441 haveHealthSettings := o.cmd != "" || 442 o.interval.Value() != nil || 443 o.timeout.Value() != nil || 444 o.startPeriod.Value() != nil || 445 o.startInterval.Value() != nil || 446 o.retries != 0 447 if o.noHealthcheck { 448 if haveHealthSettings { 449 return nil, errors.Errorf("--%s conflicts with --health-* options", flagNoHealthcheck) 450 } 451 healthConfig = &container.HealthConfig{Test: []string{"NONE"}} 452 } else if haveHealthSettings { 453 var test []string 454 if o.cmd != "" { 455 test = []string{"CMD-SHELL", o.cmd} 456 } 457 var interval, timeout, startPeriod, startInterval time.Duration 458 if ptr := o.interval.Value(); ptr != nil { 459 interval = *ptr 460 } 461 if ptr := o.timeout.Value(); ptr != nil { 462 timeout = *ptr 463 } 464 if ptr := o.startPeriod.Value(); ptr != nil { 465 startPeriod = *ptr 466 } 467 if ptr := o.startInterval.Value(); ptr != nil { 468 startInterval = *ptr 469 } 470 healthConfig = &container.HealthConfig{ 471 Test: test, 472 Interval: interval, 473 Timeout: timeout, 474 Retries: o.retries, 475 StartPeriod: startPeriod, 476 StartInterval: startInterval, 477 } 478 } 479 return healthConfig, nil 480 } 481 482 // convertExtraHostsToSwarmHosts converts an array of extra hosts in cli 483 // 484 // <host>:<ip> 485 // 486 // into a swarmkit host format: 487 // 488 // IP_address canonical_hostname [aliases...] 489 // 490 // This assumes input value (<host>:<ip>) has already been validated 491 func convertExtraHostsToSwarmHosts(extraHosts []string) []string { 492 hosts := make([]string, 0, len(extraHosts)) 493 for _, extraHost := range extraHosts { 494 host, ip, ok := strings.Cut(extraHost, ":") 495 if ok { 496 hosts = append(hosts, ip+" "+host) 497 } 498 } 499 return hosts 500 } 501 502 type serviceOptions struct { 503 detach bool 504 quiet bool 505 506 name string 507 labels opts.ListOpts 508 containerLabels opts.ListOpts 509 image string 510 entrypoint ShlexOpt 511 args []string 512 hostname string 513 env opts.ListOpts 514 envFile opts.ListOpts 515 workdir string 516 user string 517 groups opts.ListOpts 518 credentialSpec credentialSpecOpt 519 init bool 520 stopSignal string 521 tty bool 522 readOnly bool 523 mounts opts.MountOpt 524 dns opts.ListOpts 525 dnsSearch opts.ListOpts 526 dnsOption opts.ListOpts 527 hosts opts.ListOpts 528 sysctls opts.ListOpts 529 capAdd opts.ListOpts 530 capDrop opts.ListOpts 531 ulimits opts.UlimitOpt 532 533 resources resourceOptions 534 stopGrace opts.DurationOpt 535 536 replicas Uint64Opt 537 mode string 538 maxConcurrent Uint64Opt 539 540 restartPolicy restartPolicyOptions 541 constraints opts.ListOpts 542 placementPrefs placementPrefOpts 543 maxReplicas uint64 544 update updateOptions 545 rollback updateOptions 546 networks opts.NetworkOpt 547 endpoint endpointOptions 548 549 registryAuth bool 550 noResolveImage bool 551 552 logDriver logDriverOptions 553 554 healthcheck healthCheckOptions 555 secrets opts.SecretOpt 556 configs opts.ConfigOpt 557 558 isolation string 559 } 560 561 func newServiceOptions() *serviceOptions { 562 return &serviceOptions{ 563 labels: opts.NewListOpts(opts.ValidateLabel), 564 constraints: opts.NewListOpts(nil), 565 containerLabels: opts.NewListOpts(opts.ValidateLabel), 566 env: opts.NewListOpts(opts.ValidateEnv), 567 envFile: opts.NewListOpts(nil), 568 groups: opts.NewListOpts(nil), 569 logDriver: newLogDriverOptions(), 570 dns: opts.NewListOpts(opts.ValidateIPAddress), 571 dnsOption: opts.NewListOpts(nil), 572 dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch), 573 hosts: opts.NewListOpts(opts.ValidateExtraHost), 574 sysctls: opts.NewListOpts(nil), 575 capAdd: opts.NewListOpts(nil), 576 capDrop: opts.NewListOpts(nil), 577 ulimits: *opts.NewUlimitOpt(nil), 578 } 579 } 580 581 func (options *serviceOptions) ToServiceMode() (swarm.ServiceMode, error) { 582 serviceMode := swarm.ServiceMode{} 583 switch options.mode { 584 case "global": 585 if options.replicas.Value() != nil { 586 return serviceMode, errors.Errorf("replicas can only be used with replicated or replicated-job mode") 587 } 588 589 if options.maxReplicas > 0 { 590 return serviceMode, errors.New("replicas-max-per-node can only be used with replicated or replicated-job mode") 591 } 592 if options.maxConcurrent.Value() != nil { 593 return serviceMode, errors.New("max-concurrent can only be used with replicated-job mode") 594 } 595 596 serviceMode.Global = &swarm.GlobalService{} 597 case "replicated": 598 if options.maxConcurrent.Value() != nil { 599 return serviceMode, errors.New("max-concurrent can only be used with replicated-job mode") 600 } 601 602 serviceMode.Replicated = &swarm.ReplicatedService{ 603 Replicas: options.replicas.Value(), 604 } 605 case "replicated-job": 606 concurrent := options.maxConcurrent.Value() 607 if concurrent == nil { 608 concurrent = options.replicas.Value() 609 } 610 serviceMode.ReplicatedJob = &swarm.ReplicatedJob{ 611 MaxConcurrent: concurrent, 612 TotalCompletions: options.replicas.Value(), 613 } 614 case "global-job": 615 if options.maxReplicas > 0 { 616 return serviceMode, errors.New("replicas-max-per-node can only be used with replicated or replicated-job mode") 617 } 618 if options.maxConcurrent.Value() != nil { 619 return serviceMode, errors.New("max-concurrent can only be used with replicated-job mode") 620 } 621 if options.replicas.Value() != nil { 622 return serviceMode, errors.Errorf("replicas can only be used with replicated or replicated-job mode") 623 } 624 serviceMode.GlobalJob = &swarm.GlobalJob{} 625 default: 626 return serviceMode, errors.Errorf("Unknown mode: %s, only replicated and global supported", options.mode) 627 } 628 return serviceMode, nil 629 } 630 631 func (options *serviceOptions) ToStopGracePeriod(flags *pflag.FlagSet) *time.Duration { 632 if flags.Changed(flagStopGracePeriod) { 633 return options.stopGrace.Value() 634 } 635 return nil 636 } 637 638 // makeEnv gets the environment variables from the command line options and 639 // returns a slice of strings to use in the service spec when doing ToService 640 func (options *serviceOptions) makeEnv() ([]string, error) { 641 envVariables, err := opts.ReadKVEnvStrings(options.envFile.GetAll(), options.env.GetAll()) 642 if err != nil { 643 return nil, err 644 } 645 currentEnv := make([]string, 0, len(envVariables)) 646 for _, env := range envVariables { // need to process each var, in order 647 k, _, _ := strings.Cut(env, "=") 648 for i, current := range currentEnv { // remove duplicates 649 if current == env { 650 continue // no update required, may hide this behind flag to preserve order of envVariables 651 } 652 if strings.HasPrefix(current, k+"=") { 653 currentEnv = append(currentEnv[:i], currentEnv[i+1:]...) 654 } 655 } 656 currentEnv = append(currentEnv, env) 657 } 658 659 return currentEnv, nil 660 } 661 662 // ToService takes the set of flags passed to the command and converts them 663 // into a service spec. 664 // 665 // Takes an API client as the second argument in order to resolve network names 666 // from the flags into network IDs. 667 // 668 // Returns an error if any flags are invalid or contradictory. 669 func (options *serviceOptions) ToService(ctx context.Context, apiClient client.NetworkAPIClient, flags *pflag.FlagSet) (swarm.ServiceSpec, error) { 670 var service swarm.ServiceSpec 671 672 currentEnv, err := options.makeEnv() 673 if err != nil { 674 return service, err 675 } 676 677 healthConfig, err := options.healthcheck.toHealthConfig() 678 if err != nil { 679 return service, err 680 } 681 682 serviceMode, err := options.ToServiceMode() 683 if err != nil { 684 return service, err 685 } 686 687 updateConfig := options.update.updateConfig(flags) 688 rollbackConfig := options.rollback.rollbackConfig(flags) 689 690 // update and rollback configuration is not supported for jobs. If these 691 // flags are not set, then the values will be nil. If they are non-nil, 692 // then return an error. 693 if (serviceMode.ReplicatedJob != nil || serviceMode.GlobalJob != nil) && (updateConfig != nil || rollbackConfig != nil) { 694 return service, errors.Errorf("update and rollback configuration is not supported for jobs") 695 } 696 697 networks := convertNetworks(options.networks) 698 for i, net := range networks { 699 nwID, err := resolveNetworkID(ctx, apiClient, net.Target) 700 if err != nil { 701 return service, err 702 } 703 networks[i].Target = nwID 704 } 705 sort.Slice(networks, func(i, j int) bool { 706 return networks[i].Target < networks[j].Target 707 }) 708 709 resources, err := options.resources.ToResourceRequirements() 710 if err != nil { 711 return service, err 712 } 713 714 capAdd, capDrop := opts.EffectiveCapAddCapDrop(options.capAdd.GetAll(), options.capDrop.GetAll()) 715 716 service = swarm.ServiceSpec{ 717 Annotations: swarm.Annotations{ 718 Name: options.name, 719 Labels: opts.ConvertKVStringsToMap(options.labels.GetAll()), 720 }, 721 TaskTemplate: swarm.TaskSpec{ 722 ContainerSpec: &swarm.ContainerSpec{ 723 Image: options.image, 724 Args: options.args, 725 Command: options.entrypoint.Value(), 726 Env: currentEnv, 727 Hostname: options.hostname, 728 Labels: opts.ConvertKVStringsToMap(options.containerLabels.GetAll()), 729 Dir: options.workdir, 730 User: options.user, 731 Groups: options.groups.GetAll(), 732 StopSignal: options.stopSignal, 733 TTY: options.tty, 734 ReadOnly: options.readOnly, 735 Mounts: options.mounts.Value(), 736 Init: &options.init, 737 DNSConfig: &swarm.DNSConfig{ 738 Nameservers: options.dns.GetAll(), 739 Search: options.dnsSearch.GetAll(), 740 Options: options.dnsOption.GetAll(), 741 }, 742 Hosts: convertExtraHostsToSwarmHosts(options.hosts.GetAll()), 743 StopGracePeriod: options.ToStopGracePeriod(flags), 744 Healthcheck: healthConfig, 745 Isolation: container.Isolation(options.isolation), 746 Sysctls: opts.ConvertKVStringsToMap(options.sysctls.GetAll()), 747 CapabilityAdd: capAdd, 748 CapabilityDrop: capDrop, 749 Ulimits: options.ulimits.GetList(), 750 }, 751 Networks: networks, 752 Resources: resources, 753 RestartPolicy: options.restartPolicy.ToRestartPolicy(flags), 754 Placement: &swarm.Placement{ 755 Constraints: options.constraints.GetAll(), 756 Preferences: options.placementPrefs.prefs, 757 MaxReplicas: options.maxReplicas, 758 }, 759 LogDriver: options.logDriver.toLogDriver(), 760 }, 761 Mode: serviceMode, 762 UpdateConfig: updateConfig, 763 RollbackConfig: rollbackConfig, 764 EndpointSpec: options.endpoint.ToEndpointSpec(), 765 } 766 767 if options.credentialSpec.String() != "" && options.credentialSpec.Value() != nil { 768 service.TaskTemplate.ContainerSpec.Privileges = &swarm.Privileges{ 769 CredentialSpec: options.credentialSpec.Value(), 770 } 771 } 772 773 return service, nil 774 } 775 776 type flagDefaults map[string]any 777 778 func (fd flagDefaults) getUint64(flagName string) uint64 { 779 if val, ok := fd[flagName].(uint64); ok { 780 return val 781 } 782 return 0 783 } 784 785 func (fd flagDefaults) getString(flagName string) string { 786 if val, ok := fd[flagName].(string); ok { 787 return val 788 } 789 return "" 790 } 791 792 func buildServiceDefaultFlagMapping() flagDefaults { 793 defaultFlagValues := make(map[string]any) 794 795 defaultFlagValues[flagStopGracePeriod], _ = gogotypes.DurationFromProto(defaults.Service.Task.GetContainer().StopGracePeriod) 796 defaultFlagValues[flagRestartCondition] = `"` + defaultRestartCondition() + `"` 797 defaultFlagValues[flagRestartDelay], _ = gogotypes.DurationFromProto(defaults.Service.Task.Restart.Delay) 798 799 if defaults.Service.Task.Restart.MaxAttempts != 0 { 800 defaultFlagValues[flagRestartMaxAttempts] = defaults.Service.Task.Restart.MaxAttempts 801 } 802 803 defaultRestartWindow, _ := gogotypes.DurationFromProto(defaults.Service.Task.Restart.Window) 804 if defaultRestartWindow != 0 { 805 defaultFlagValues[flagRestartWindow] = defaultRestartWindow 806 } 807 808 defaultFlagValues[flagUpdateParallelism] = defaults.Service.Update.Parallelism 809 defaultFlagValues[flagUpdateDelay] = defaults.Service.Update.Delay 810 defaultFlagValues[flagUpdateMonitor], _ = gogotypes.DurationFromProto(defaults.Service.Update.Monitor) 811 defaultFlagValues[flagUpdateFailureAction] = `"` + strings.ToLower(api.UpdateConfig_FailureAction_name[int32(defaults.Service.Update.FailureAction)]) + `"` 812 defaultFlagValues[flagUpdateMaxFailureRatio] = defaults.Service.Update.MaxFailureRatio 813 defaultFlagValues[flagUpdateOrder] = `"` + defaultOrder(defaults.Service.Update.Order) + `"` 814 815 defaultFlagValues[flagRollbackParallelism] = defaults.Service.Rollback.Parallelism 816 defaultFlagValues[flagRollbackDelay] = defaults.Service.Rollback.Delay 817 defaultFlagValues[flagRollbackMonitor], _ = gogotypes.DurationFromProto(defaults.Service.Rollback.Monitor) 818 defaultFlagValues[flagRollbackFailureAction] = `"` + strings.ToLower(api.UpdateConfig_FailureAction_name[int32(defaults.Service.Rollback.FailureAction)]) + `"` 819 defaultFlagValues[flagRollbackMaxFailureRatio] = defaults.Service.Rollback.MaxFailureRatio 820 defaultFlagValues[flagRollbackOrder] = `"` + defaultOrder(defaults.Service.Rollback.Order) + `"` 821 822 defaultFlagValues[flagEndpointMode] = "vip" 823 824 return defaultFlagValues 825 } 826 827 func addDetachFlag(flags *pflag.FlagSet, detach *bool) { 828 flags.BoolVarP(detach, flagDetach, "d", false, "Exit immediately instead of waiting for the service to converge") 829 flags.SetAnnotation(flagDetach, "version", []string{"1.29"}) 830 } 831 832 // addServiceFlags adds all flags that are common to both `create` and `update`. 833 // Any flags that are not common are added separately in the individual command 834 func addServiceFlags(flags *pflag.FlagSet, options *serviceOptions, defaultFlagValues flagDefaults) { 835 flagDesc := func(flagName string, desc string) string { 836 if defaultValue, ok := defaultFlagValues[flagName]; ok { 837 return fmt.Sprintf("%s (default %v)", desc, defaultValue) 838 } 839 return desc 840 } 841 842 addDetachFlag(flags, &options.detach) 843 flags.BoolVarP(&options.quiet, flagQuiet, "q", false, "Suppress progress output") 844 845 flags.StringVarP(&options.workdir, flagWorkdir, "w", "", "Working directory inside the container") 846 flags.StringVarP(&options.user, flagUser, "u", "", "Username or UID (format: <name|uid>[:<group|gid>])") 847 flags.Var(&options.credentialSpec, flagCredentialSpec, "Credential spec for managed service account (Windows only)") 848 flags.SetAnnotation(flagCredentialSpec, "version", []string{"1.29"}) 849 flags.StringVar(&options.hostname, flagHostname, "", "Container hostname") 850 flags.SetAnnotation(flagHostname, "version", []string{"1.25"}) 851 flags.Var(&options.entrypoint, flagEntrypoint, "Overwrite the default ENTRYPOINT of the image") 852 flags.Var(&options.capAdd, flagCapAdd, "Add Linux capabilities") 853 flags.SetAnnotation(flagCapAdd, "version", []string{"1.41"}) 854 flags.Var(&options.capDrop, flagCapDrop, "Drop Linux capabilities") 855 flags.SetAnnotation(flagCapDrop, "version", []string{"1.41"}) 856 857 flags.Var(&options.resources.limitCPU, flagLimitCPU, "Limit CPUs") 858 flags.Var(&options.resources.limitMemBytes, flagLimitMemory, "Limit Memory") 859 flags.Var(&options.resources.resCPU, flagReserveCPU, "Reserve CPUs") 860 flags.Var(&options.resources.resMemBytes, flagReserveMemory, "Reserve Memory") 861 flags.Int64Var(&options.resources.limitPids, flagLimitPids, 0, "Limit maximum number of processes (default 0 = unlimited)") 862 flags.SetAnnotation(flagLimitPids, "version", []string{"1.41"}) 863 864 flags.Var(&options.stopGrace, flagStopGracePeriod, flagDesc(flagStopGracePeriod, "Time to wait before force killing a container (ns|us|ms|s|m|h)")) 865 flags.Var(&options.replicas, flagReplicas, "Number of tasks") 866 flags.Var(&options.maxConcurrent, flagConcurrent, "Number of job tasks to run concurrently (default equal to --replicas)") 867 flags.SetAnnotation(flagConcurrent, "version", []string{"1.41"}) 868 flags.Uint64Var(&options.maxReplicas, flagMaxReplicas, defaultFlagValues.getUint64(flagMaxReplicas), "Maximum number of tasks per node (default 0 = unlimited)") 869 flags.SetAnnotation(flagMaxReplicas, "version", []string{"1.40"}) 870 871 flags.StringVar(&options.restartPolicy.condition, flagRestartCondition, "", flagDesc(flagRestartCondition, `Restart when condition is met ("none", "on-failure", "any")`)) 872 flags.Var(&options.restartPolicy.delay, flagRestartDelay, flagDesc(flagRestartDelay, "Delay between restart attempts (ns|us|ms|s|m|h)")) 873 flags.Var(&options.restartPolicy.maxAttempts, flagRestartMaxAttempts, flagDesc(flagRestartMaxAttempts, "Maximum number of restarts before giving up")) 874 875 flags.Var(&options.restartPolicy.window, flagRestartWindow, flagDesc(flagRestartWindow, "Window used to evaluate the restart policy (ns|us|ms|s|m|h)")) 876 877 flags.Uint64Var(&options.update.parallelism, flagUpdateParallelism, defaultFlagValues.getUint64(flagUpdateParallelism), "Maximum number of tasks updated simultaneously (0 to update all at once)") 878 flags.DurationVar(&options.update.delay, flagUpdateDelay, 0, flagDesc(flagUpdateDelay, "Delay between updates (ns|us|ms|s|m|h)")) 879 flags.DurationVar(&options.update.monitor, flagUpdateMonitor, 0, flagDesc(flagUpdateMonitor, "Duration after each task update to monitor for failure (ns|us|ms|s|m|h)")) 880 flags.SetAnnotation(flagUpdateMonitor, "version", []string{"1.25"}) 881 flags.StringVar(&options.update.onFailure, flagUpdateFailureAction, "", flagDesc(flagUpdateFailureAction, `Action on update failure ("pause", "continue", "rollback")`)) 882 flags.Var(&options.update.maxFailureRatio, flagUpdateMaxFailureRatio, flagDesc(flagUpdateMaxFailureRatio, "Failure rate to tolerate during an update")) 883 flags.SetAnnotation(flagUpdateMaxFailureRatio, "version", []string{"1.25"}) 884 flags.StringVar(&options.update.order, flagUpdateOrder, "", flagDesc(flagUpdateOrder, `Update order ("start-first", "stop-first")`)) 885 flags.SetAnnotation(flagUpdateOrder, "version", []string{"1.29"}) 886 887 flags.Uint64Var(&options.rollback.parallelism, flagRollbackParallelism, defaultFlagValues.getUint64(flagRollbackParallelism), 888 "Maximum number of tasks rolled back simultaneously (0 to roll back all at once)") 889 flags.SetAnnotation(flagRollbackParallelism, "version", []string{"1.28"}) 890 flags.DurationVar(&options.rollback.delay, flagRollbackDelay, 0, flagDesc(flagRollbackDelay, "Delay between task rollbacks (ns|us|ms|s|m|h)")) 891 flags.SetAnnotation(flagRollbackDelay, "version", []string{"1.28"}) 892 flags.DurationVar(&options.rollback.monitor, flagRollbackMonitor, 0, flagDesc(flagRollbackMonitor, "Duration after each task rollback to monitor for failure (ns|us|ms|s|m|h)")) 893 flags.SetAnnotation(flagRollbackMonitor, "version", []string{"1.28"}) 894 flags.StringVar(&options.rollback.onFailure, flagRollbackFailureAction, "", flagDesc(flagRollbackFailureAction, `Action on rollback failure ("pause", "continue")`)) 895 flags.SetAnnotation(flagRollbackFailureAction, "version", []string{"1.28"}) 896 flags.Var(&options.rollback.maxFailureRatio, flagRollbackMaxFailureRatio, flagDesc(flagRollbackMaxFailureRatio, "Failure rate to tolerate during a rollback")) 897 flags.SetAnnotation(flagRollbackMaxFailureRatio, "version", []string{"1.28"}) 898 flags.StringVar(&options.rollback.order, flagRollbackOrder, "", flagDesc(flagRollbackOrder, `Rollback order ("start-first", "stop-first")`)) 899 flags.SetAnnotation(flagRollbackOrder, "version", []string{"1.29"}) 900 901 flags.StringVar(&options.endpoint.mode, flagEndpointMode, defaultFlagValues.getString(flagEndpointMode), "Endpoint mode (vip or dnsrr)") 902 903 flags.BoolVar(&options.registryAuth, flagRegistryAuth, false, "Send registry authentication details to swarm agents") 904 flags.BoolVar(&options.noResolveImage, flagNoResolveImage, false, "Do not query the registry to resolve image digest and supported platforms") 905 flags.SetAnnotation(flagNoResolveImage, "version", []string{"1.30"}) 906 907 flags.StringVar(&options.logDriver.name, flagLogDriver, "", "Logging driver for service") 908 flags.Var(&options.logDriver.opts, flagLogOpt, "Logging driver options") 909 910 flags.StringVar(&options.healthcheck.cmd, flagHealthCmd, "", "Command to run to check health") 911 flags.SetAnnotation(flagHealthCmd, "version", []string{"1.25"}) 912 flags.Var(&options.healthcheck.interval, flagHealthInterval, "Time between running the check (ms|s|m|h)") 913 flags.SetAnnotation(flagHealthInterval, "version", []string{"1.25"}) 914 flags.Var(&options.healthcheck.timeout, flagHealthTimeout, "Maximum time to allow one check to run (ms|s|m|h)") 915 flags.SetAnnotation(flagHealthTimeout, "version", []string{"1.25"}) 916 flags.IntVar(&options.healthcheck.retries, flagHealthRetries, 0, "Consecutive failures needed to report unhealthy") 917 flags.SetAnnotation(flagHealthRetries, "version", []string{"1.25"}) 918 flags.Var(&options.healthcheck.startPeriod, flagHealthStartPeriod, "Start period for the container to initialize before counting retries towards unstable (ms|s|m|h)") 919 flags.SetAnnotation(flagHealthStartPeriod, "version", []string{"1.29"}) 920 flags.Var(&options.healthcheck.startInterval, flagHealthStartInterval, "Time between running the check during the start period (ms|s|m|h)") 921 flags.SetAnnotation(flagHealthStartInterval, "version", []string{"1.44"}) 922 flags.BoolVar(&options.healthcheck.noHealthcheck, flagNoHealthcheck, false, "Disable any container-specified HEALTHCHECK") 923 flags.SetAnnotation(flagNoHealthcheck, "version", []string{"1.25"}) 924 925 flags.BoolVarP(&options.tty, flagTTY, "t", false, "Allocate a pseudo-TTY") 926 flags.SetAnnotation(flagTTY, "version", []string{"1.25"}) 927 928 flags.BoolVar(&options.readOnly, flagReadOnly, false, "Mount the container's root filesystem as read only") 929 flags.SetAnnotation(flagReadOnly, "version", []string{"1.28"}) 930 931 flags.StringVar(&options.stopSignal, flagStopSignal, "", "Signal to stop the container") 932 flags.SetAnnotation(flagStopSignal, "version", []string{"1.28"}) 933 flags.StringVar(&options.isolation, flagIsolation, "", "Service container isolation mode") 934 flags.SetAnnotation(flagIsolation, "version", []string{"1.35"}) 935 } 936 937 const ( 938 flagCredentialSpec = "credential-spec" //nolint:gosec // ignore G101: Potential hardcoded credentials 939 flagPlacementPref = "placement-pref" 940 flagPlacementPrefAdd = "placement-pref-add" 941 flagPlacementPrefRemove = "placement-pref-rm" 942 flagConstraint = "constraint" 943 flagConstraintRemove = "constraint-rm" 944 flagConstraintAdd = "constraint-add" 945 flagContainerLabel = "container-label" 946 flagContainerLabelRemove = "container-label-rm" 947 flagContainerLabelAdd = "container-label-add" 948 flagDetach = "detach" 949 flagDNS = "dns" 950 flagDNSRemove = "dns-rm" 951 flagDNSAdd = "dns-add" 952 flagDNSOption = "dns-option" 953 flagDNSOptionRemove = "dns-option-rm" 954 flagDNSOptionAdd = "dns-option-add" 955 flagDNSSearch = "dns-search" 956 flagDNSSearchRemove = "dns-search-rm" 957 flagDNSSearchAdd = "dns-search-add" 958 flagEndpointMode = "endpoint-mode" 959 flagEntrypoint = "entrypoint" 960 flagEnv = "env" 961 flagEnvFile = "env-file" 962 flagEnvRemove = "env-rm" 963 flagEnvAdd = "env-add" 964 flagGenericResourcesRemove = "generic-resource-rm" 965 flagGenericResourcesAdd = "generic-resource-add" 966 flagGroup = "group" 967 flagGroupAdd = "group-add" 968 flagGroupRemove = "group-rm" 969 flagHost = "host" 970 flagHostAdd = "host-add" 971 flagHostRemove = "host-rm" 972 flagHostname = "hostname" 973 flagLabel = "label" 974 flagLabelRemove = "label-rm" 975 flagLabelAdd = "label-add" 976 flagLimitCPU = "limit-cpu" 977 flagLimitMemory = "limit-memory" 978 flagLimitPids = "limit-pids" 979 flagMaxReplicas = "replicas-max-per-node" 980 flagConcurrent = "max-concurrent" 981 flagMode = "mode" 982 flagMount = "mount" 983 flagMountRemove = "mount-rm" 984 flagMountAdd = "mount-add" 985 flagName = "name" 986 flagNetwork = "network" 987 flagNetworkAdd = "network-add" 988 flagNetworkRemove = "network-rm" 989 flagPublish = "publish" 990 flagPublishRemove = "publish-rm" 991 flagPublishAdd = "publish-add" 992 flagQuiet = "quiet" 993 flagReadOnly = "read-only" 994 flagReplicas = "replicas" 995 flagReserveCPU = "reserve-cpu" 996 flagReserveMemory = "reserve-memory" 997 flagRestartCondition = "restart-condition" 998 flagRestartDelay = "restart-delay" 999 flagRestartMaxAttempts = "restart-max-attempts" 1000 flagRestartWindow = "restart-window" 1001 flagRollback = "rollback" 1002 flagRollbackDelay = "rollback-delay" 1003 flagRollbackFailureAction = "rollback-failure-action" 1004 flagRollbackMaxFailureRatio = "rollback-max-failure-ratio" 1005 flagRollbackMonitor = "rollback-monitor" 1006 flagRollbackOrder = "rollback-order" 1007 flagRollbackParallelism = "rollback-parallelism" 1008 flagInit = "init" 1009 flagSysCtl = "sysctl" 1010 flagSysCtlAdd = "sysctl-add" 1011 flagSysCtlRemove = "sysctl-rm" 1012 flagStopGracePeriod = "stop-grace-period" 1013 flagStopSignal = "stop-signal" 1014 flagTTY = "tty" 1015 flagUpdateDelay = "update-delay" 1016 flagUpdateFailureAction = "update-failure-action" 1017 flagUpdateMaxFailureRatio = "update-max-failure-ratio" // #nosec G101 -- ignoring: Potential hardcoded credentials (gosec) 1018 flagUpdateMonitor = "update-monitor" 1019 flagUpdateOrder = "update-order" 1020 flagUpdateParallelism = "update-parallelism" 1021 flagUser = "user" 1022 flagWorkdir = "workdir" 1023 flagRegistryAuth = "with-registry-auth" 1024 flagNoResolveImage = "no-resolve-image" 1025 flagLogDriver = "log-driver" 1026 flagLogOpt = "log-opt" 1027 flagHealthCmd = "health-cmd" 1028 flagHealthInterval = "health-interval" 1029 flagHealthRetries = "health-retries" 1030 flagHealthTimeout = "health-timeout" 1031 flagHealthStartPeriod = "health-start-period" 1032 flagHealthStartInterval = "health-start-interval" 1033 flagNoHealthcheck = "no-healthcheck" 1034 flagSecret = "secret" 1035 flagSecretAdd = "secret-add" 1036 flagSecretRemove = "secret-rm" 1037 flagConfig = "config" 1038 flagConfigAdd = "config-add" 1039 flagConfigRemove = "config-rm" 1040 flagIsolation = "isolation" 1041 flagCapAdd = "cap-add" 1042 flagCapDrop = "cap-drop" 1043 flagUlimit = "ulimit" 1044 flagUlimitAdd = "ulimit-add" 1045 flagUlimitRemove = "ulimit-rm" 1046 ) 1047 1048 func validateAPIVersion(c swarm.ServiceSpec, serverAPIVersion string) error { 1049 for _, m := range c.TaskTemplate.ContainerSpec.Mounts { 1050 if err := command.ValidateMountWithAPIVersion(m, serverAPIVersion); err != nil { 1051 return err 1052 } 1053 } 1054 return nil 1055 }