github.com/zhuohuang-hust/src-cbuild@v0.0.0-20230105071821-c7aab3e7c840/cli/command/service/opts.go (about) 1 package service 2 3 import ( 4 "encoding/csv" 5 "fmt" 6 "os" 7 "path/filepath" 8 "strconv" 9 "strings" 10 "time" 11 12 "github.com/docker/docker/api/types/container" 13 "github.com/docker/docker/api/types/swarm" 14 "github.com/docker/docker/opts" 15 runconfigopts "github.com/docker/docker/runconfig/opts" 16 "github.com/docker/go-connections/nat" 17 units "github.com/docker/go-units" 18 "github.com/spf13/cobra" 19 ) 20 21 type int64Value interface { 22 Value() int64 23 } 24 25 type memBytes int64 26 27 func (m *memBytes) String() string { 28 return units.BytesSize(float64(m.Value())) 29 } 30 31 func (m *memBytes) Set(value string) error { 32 val, err := units.RAMInBytes(value) 33 *m = memBytes(val) 34 return err 35 } 36 37 func (m *memBytes) Type() string { 38 return "bytes" 39 } 40 41 func (m *memBytes) Value() int64 { 42 return int64(*m) 43 } 44 45 // PositiveDurationOpt is an option type for time.Duration that uses a pointer. 46 // It bahave similarly to DurationOpt but only allows positive duration values. 47 type PositiveDurationOpt struct { 48 DurationOpt 49 } 50 51 // Set a new value on the option. Setting a negative duration value will cause 52 // an error to be returned. 53 func (d *PositiveDurationOpt) Set(s string) error { 54 err := d.DurationOpt.Set(s) 55 if err != nil { 56 return err 57 } 58 if *d.DurationOpt.value < 0 { 59 return fmt.Errorf("duration cannot be negative") 60 } 61 return nil 62 } 63 64 // DurationOpt is an option type for time.Duration that uses a pointer. This 65 // allows us to get nil values outside, instead of defaulting to 0 66 type DurationOpt struct { 67 value *time.Duration 68 } 69 70 // Set a new value on the option 71 func (d *DurationOpt) Set(s string) error { 72 v, err := time.ParseDuration(s) 73 d.value = &v 74 return err 75 } 76 77 // Type returns the type of this option, which will be displayed in `--help` output 78 func (d *DurationOpt) Type() string { 79 return "duration" 80 } 81 82 // String returns a string repr of this option 83 func (d *DurationOpt) String() string { 84 if d.value != nil { 85 return d.value.String() 86 } 87 return "none" 88 } 89 90 // Value returns the time.Duration 91 func (d *DurationOpt) Value() *time.Duration { 92 return d.value 93 } 94 95 // Uint64Opt represents a uint64. 96 type Uint64Opt struct { 97 value *uint64 98 } 99 100 // Set a new value on the option 101 func (i *Uint64Opt) Set(s string) error { 102 v, err := strconv.ParseUint(s, 0, 64) 103 i.value = &v 104 return err 105 } 106 107 // Type returns the type of this option, which will be displayed in `--help` output 108 func (i *Uint64Opt) Type() string { 109 return "uint" 110 } 111 112 // String returns a string repr of this option 113 func (i *Uint64Opt) String() string { 114 if i.value != nil { 115 return fmt.Sprintf("%v", *i.value) 116 } 117 return "none" 118 } 119 120 // Value returns the uint64 121 func (i *Uint64Opt) Value() *uint64 { 122 return i.value 123 } 124 125 type floatValue float32 126 127 func (f *floatValue) Set(s string) error { 128 v, err := strconv.ParseFloat(s, 32) 129 *f = floatValue(v) 130 return err 131 } 132 133 func (f *floatValue) Type() string { 134 return "float" 135 } 136 137 func (f *floatValue) String() string { 138 return strconv.FormatFloat(float64(*f), 'g', -1, 32) 139 } 140 141 func (f *floatValue) Value() float32 { 142 return float32(*f) 143 } 144 145 // SecretRequestSpec is a type for requesting secrets 146 type SecretRequestSpec struct { 147 source string 148 target string 149 uid string 150 gid string 151 mode os.FileMode 152 } 153 154 // SecretOpt is a Value type for parsing secrets 155 type SecretOpt struct { 156 values []*SecretRequestSpec 157 } 158 159 // Set a new secret value 160 func (o *SecretOpt) Set(value string) error { 161 csvReader := csv.NewReader(strings.NewReader(value)) 162 fields, err := csvReader.Read() 163 if err != nil { 164 return err 165 } 166 167 spec := &SecretRequestSpec{ 168 source: "", 169 target: "", 170 uid: "0", 171 gid: "0", 172 mode: 0444, 173 } 174 175 for _, field := range fields { 176 parts := strings.SplitN(field, "=", 2) 177 key := strings.ToLower(parts[0]) 178 179 if len(parts) != 2 { 180 return fmt.Errorf("invalid field '%s' must be a key=value pair", field) 181 } 182 183 value := parts[1] 184 switch key { 185 case "source", "src": 186 spec.source = value 187 case "target": 188 tDir, _ := filepath.Split(value) 189 if tDir != "" { 190 return fmt.Errorf("target must not have a path") 191 } 192 spec.target = value 193 case "uid": 194 spec.uid = value 195 case "gid": 196 spec.gid = value 197 case "mode": 198 m, err := strconv.ParseUint(value, 0, 32) 199 if err != nil { 200 return fmt.Errorf("invalid mode specified: %v", err) 201 } 202 203 spec.mode = os.FileMode(m) 204 default: 205 return fmt.Errorf("invalid field in secret request: %s", key) 206 } 207 } 208 209 if spec.source == "" { 210 return fmt.Errorf("source is required") 211 } 212 213 o.values = append(o.values, spec) 214 return nil 215 } 216 217 // Type returns the type of this option 218 func (o *SecretOpt) Type() string { 219 return "secret" 220 } 221 222 // String returns a string repr of this option 223 func (o *SecretOpt) String() string { 224 secrets := []string{} 225 for _, secret := range o.values { 226 repr := fmt.Sprintf("%s -> %s", secret.source, secret.target) 227 secrets = append(secrets, repr) 228 } 229 return strings.Join(secrets, ", ") 230 } 231 232 // Value returns the secret requests 233 func (o *SecretOpt) Value() []*SecretRequestSpec { 234 return o.values 235 } 236 237 type updateOptions struct { 238 parallelism uint64 239 delay time.Duration 240 monitor time.Duration 241 onFailure string 242 maxFailureRatio floatValue 243 } 244 245 type resourceOptions struct { 246 limitCPU opts.NanoCPUs 247 limitMemBytes memBytes 248 resCPU opts.NanoCPUs 249 resMemBytes memBytes 250 } 251 252 func (r *resourceOptions) ToResourceRequirements() *swarm.ResourceRequirements { 253 return &swarm.ResourceRequirements{ 254 Limits: &swarm.Resources{ 255 NanoCPUs: r.limitCPU.Value(), 256 MemoryBytes: r.limitMemBytes.Value(), 257 }, 258 Reservations: &swarm.Resources{ 259 NanoCPUs: r.resCPU.Value(), 260 MemoryBytes: r.resMemBytes.Value(), 261 }, 262 } 263 } 264 265 type restartPolicyOptions struct { 266 condition string 267 delay DurationOpt 268 maxAttempts Uint64Opt 269 window DurationOpt 270 } 271 272 func (r *restartPolicyOptions) ToRestartPolicy() *swarm.RestartPolicy { 273 return &swarm.RestartPolicy{ 274 Condition: swarm.RestartPolicyCondition(r.condition), 275 Delay: r.delay.Value(), 276 MaxAttempts: r.maxAttempts.Value(), 277 Window: r.window.Value(), 278 } 279 } 280 281 func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig { 282 nets := []swarm.NetworkAttachmentConfig{} 283 for _, network := range networks { 284 nets = append(nets, swarm.NetworkAttachmentConfig{Target: network}) 285 } 286 return nets 287 } 288 289 type endpointOptions struct { 290 mode string 291 publishPorts opts.ListOpts 292 expandedPorts opts.PortOpt 293 } 294 295 func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec { 296 portConfigs := []swarm.PortConfig{} 297 // We can ignore errors because the format was already validated by ValidatePort 298 ports, portBindings, _ := nat.ParsePortSpecs(e.publishPorts.GetAll()) 299 300 for port := range ports { 301 portConfigs = append(portConfigs, ConvertPortToPortConfig(port, portBindings)...) 302 } 303 304 return &swarm.EndpointSpec{ 305 Mode: swarm.ResolutionMode(strings.ToLower(e.mode)), 306 Ports: append(portConfigs, e.expandedPorts.Value()...), 307 } 308 } 309 310 // ConvertPortToPortConfig converts ports to the swarm type 311 func ConvertPortToPortConfig( 312 port nat.Port, 313 portBindings map[nat.Port][]nat.PortBinding, 314 ) []swarm.PortConfig { 315 ports := []swarm.PortConfig{} 316 317 for _, binding := range portBindings[port] { 318 hostPort, _ := strconv.ParseUint(binding.HostPort, 10, 16) 319 ports = append(ports, swarm.PortConfig{ 320 //TODO Name: ? 321 Protocol: swarm.PortConfigProtocol(strings.ToLower(port.Proto())), 322 TargetPort: uint32(port.Int()), 323 PublishedPort: uint32(hostPort), 324 }) 325 } 326 return ports 327 } 328 329 type logDriverOptions struct { 330 name string 331 opts opts.ListOpts 332 } 333 334 func newLogDriverOptions() logDriverOptions { 335 return logDriverOptions{opts: opts.NewListOpts(runconfigopts.ValidateEnv)} 336 } 337 338 func (ldo *logDriverOptions) toLogDriver() *swarm.Driver { 339 if ldo.name == "" { 340 return nil 341 } 342 343 // set the log driver only if specified. 344 return &swarm.Driver{ 345 Name: ldo.name, 346 Options: runconfigopts.ConvertKVStringsToMap(ldo.opts.GetAll()), 347 } 348 } 349 350 type healthCheckOptions struct { 351 cmd string 352 interval PositiveDurationOpt 353 timeout PositiveDurationOpt 354 retries int 355 noHealthcheck bool 356 } 357 358 func (opts *healthCheckOptions) toHealthConfig() (*container.HealthConfig, error) { 359 var healthConfig *container.HealthConfig 360 haveHealthSettings := opts.cmd != "" || 361 opts.interval.Value() != nil || 362 opts.timeout.Value() != nil || 363 opts.retries != 0 364 if opts.noHealthcheck { 365 if haveHealthSettings { 366 return nil, fmt.Errorf("--%s conflicts with --health-* options", flagNoHealthcheck) 367 } 368 healthConfig = &container.HealthConfig{Test: []string{"NONE"}} 369 } else if haveHealthSettings { 370 var test []string 371 if opts.cmd != "" { 372 test = []string{"CMD-SHELL", opts.cmd} 373 } 374 var interval, timeout time.Duration 375 if ptr := opts.interval.Value(); ptr != nil { 376 interval = *ptr 377 } 378 if ptr := opts.timeout.Value(); ptr != nil { 379 timeout = *ptr 380 } 381 healthConfig = &container.HealthConfig{ 382 Test: test, 383 Interval: interval, 384 Timeout: timeout, 385 Retries: opts.retries, 386 } 387 } 388 return healthConfig, nil 389 } 390 391 // ValidatePort validates a string is in the expected format for a port definition 392 func ValidatePort(value string) (string, error) { 393 portMappings, err := nat.ParsePortSpec(value) 394 for _, portMapping := range portMappings { 395 if portMapping.Binding.HostIP != "" { 396 return "", fmt.Errorf("HostIP is not supported by a service.") 397 } 398 } 399 return value, err 400 } 401 402 // convertExtraHostsToSwarmHosts converts an array of extra hosts in cli 403 // <host>:<ip> 404 // into a swarmkit host format: 405 // IP_address canonical_hostname [aliases...] 406 // This assumes input value (<host>:<ip>) has already been validated 407 func convertExtraHostsToSwarmHosts(extraHosts []string) []string { 408 hosts := []string{} 409 for _, extraHost := range extraHosts { 410 parts := strings.SplitN(extraHost, ":", 2) 411 hosts = append(hosts, fmt.Sprintf("%s %s", parts[1], parts[0])) 412 } 413 return hosts 414 } 415 416 type serviceOptions struct { 417 name string 418 labels opts.ListOpts 419 containerLabels opts.ListOpts 420 image string 421 args []string 422 hostname string 423 env opts.ListOpts 424 envFile opts.ListOpts 425 workdir string 426 user string 427 groups opts.ListOpts 428 tty bool 429 mounts opts.MountOpt 430 dns opts.ListOpts 431 dnsSearch opts.ListOpts 432 dnsOption opts.ListOpts 433 hosts opts.ListOpts 434 435 resources resourceOptions 436 stopGrace DurationOpt 437 438 replicas Uint64Opt 439 mode string 440 441 restartPolicy restartPolicyOptions 442 constraints opts.ListOpts 443 update updateOptions 444 networks opts.ListOpts 445 endpoint endpointOptions 446 447 registryAuth bool 448 449 logDriver logDriverOptions 450 451 healthcheck healthCheckOptions 452 secrets opts.SecretOpt 453 } 454 455 func newServiceOptions() *serviceOptions { 456 return &serviceOptions{ 457 labels: opts.NewListOpts(runconfigopts.ValidateEnv), 458 constraints: opts.NewListOpts(nil), 459 containerLabels: opts.NewListOpts(runconfigopts.ValidateEnv), 460 env: opts.NewListOpts(runconfigopts.ValidateEnv), 461 envFile: opts.NewListOpts(nil), 462 endpoint: endpointOptions{ 463 publishPorts: opts.NewListOpts(ValidatePort), 464 }, 465 groups: opts.NewListOpts(nil), 466 logDriver: newLogDriverOptions(), 467 dns: opts.NewListOpts(opts.ValidateIPAddress), 468 dnsOption: opts.NewListOpts(nil), 469 dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch), 470 hosts: opts.NewListOpts(runconfigopts.ValidateExtraHost), 471 networks: opts.NewListOpts(nil), 472 } 473 } 474 475 func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { 476 var service swarm.ServiceSpec 477 478 envVariables, err := runconfigopts.ReadKVStrings(opts.envFile.GetAll(), opts.env.GetAll()) 479 if err != nil { 480 return service, err 481 } 482 483 currentEnv := make([]string, 0, len(envVariables)) 484 for _, env := range envVariables { // need to process each var, in order 485 k := strings.SplitN(env, "=", 2)[0] 486 for i, current := range currentEnv { // remove duplicates 487 if current == env { 488 continue // no update required, may hide this behind flag to preserve order of envVariables 489 } 490 if strings.HasPrefix(current, k+"=") { 491 currentEnv = append(currentEnv[:i], currentEnv[i+1:]...) 492 } 493 } 494 currentEnv = append(currentEnv, env) 495 } 496 497 service = swarm.ServiceSpec{ 498 Annotations: swarm.Annotations{ 499 Name: opts.name, 500 Labels: runconfigopts.ConvertKVStringsToMap(opts.labels.GetAll()), 501 }, 502 TaskTemplate: swarm.TaskSpec{ 503 ContainerSpec: swarm.ContainerSpec{ 504 Image: opts.image, 505 Args: opts.args, 506 Env: currentEnv, 507 Hostname: opts.hostname, 508 Labels: runconfigopts.ConvertKVStringsToMap(opts.containerLabels.GetAll()), 509 Dir: opts.workdir, 510 User: opts.user, 511 Groups: opts.groups.GetAll(), 512 TTY: opts.tty, 513 Mounts: opts.mounts.Value(), 514 DNSConfig: &swarm.DNSConfig{ 515 Nameservers: opts.dns.GetAll(), 516 Search: opts.dnsSearch.GetAll(), 517 Options: opts.dnsOption.GetAll(), 518 }, 519 Hosts: convertExtraHostsToSwarmHosts(opts.hosts.GetAll()), 520 StopGracePeriod: opts.stopGrace.Value(), 521 Secrets: nil, 522 }, 523 Networks: convertNetworks(opts.networks.GetAll()), 524 Resources: opts.resources.ToResourceRequirements(), 525 RestartPolicy: opts.restartPolicy.ToRestartPolicy(), 526 Placement: &swarm.Placement{ 527 Constraints: opts.constraints.GetAll(), 528 }, 529 LogDriver: opts.logDriver.toLogDriver(), 530 }, 531 Networks: convertNetworks(opts.networks.GetAll()), 532 Mode: swarm.ServiceMode{}, 533 UpdateConfig: &swarm.UpdateConfig{ 534 Parallelism: opts.update.parallelism, 535 Delay: opts.update.delay, 536 Monitor: opts.update.monitor, 537 FailureAction: opts.update.onFailure, 538 MaxFailureRatio: opts.update.maxFailureRatio.Value(), 539 }, 540 EndpointSpec: opts.endpoint.ToEndpointSpec(), 541 } 542 543 healthConfig, err := opts.healthcheck.toHealthConfig() 544 if err != nil { 545 return service, err 546 } 547 service.TaskTemplate.ContainerSpec.Healthcheck = healthConfig 548 549 switch opts.mode { 550 case "global": 551 if opts.replicas.Value() != nil { 552 return service, fmt.Errorf("replicas can only be used with replicated mode") 553 } 554 555 service.Mode.Global = &swarm.GlobalService{} 556 case "replicated": 557 service.Mode.Replicated = &swarm.ReplicatedService{ 558 Replicas: opts.replicas.Value(), 559 } 560 default: 561 return service, fmt.Errorf("Unknown mode: %s", opts.mode) 562 } 563 return service, nil 564 } 565 566 // addServiceFlags adds all flags that are common to both `create` and `update`. 567 // Any flags that are not common are added separately in the individual command 568 func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { 569 flags := cmd.Flags() 570 571 flags.StringVarP(&opts.workdir, flagWorkdir, "w", "", "Working directory inside the container") 572 flags.StringVarP(&opts.user, flagUser, "u", "", "Username or UID (format: <name|uid>[:<group|gid>])") 573 flags.StringVar(&opts.hostname, flagHostname, "", "Container hostname") 574 575 flags.Var(&opts.resources.limitCPU, flagLimitCPU, "Limit CPUs") 576 flags.Var(&opts.resources.limitMemBytes, flagLimitMemory, "Limit Memory") 577 flags.Var(&opts.resources.resCPU, flagReserveCPU, "Reserve CPUs") 578 flags.Var(&opts.resources.resMemBytes, flagReserveMemory, "Reserve Memory") 579 flags.Var(&opts.stopGrace, flagStopGracePeriod, "Time to wait before force killing a container (ns|us|ms|s|m|h)") 580 581 flags.Var(&opts.replicas, flagReplicas, "Number of tasks") 582 583 flags.StringVar(&opts.restartPolicy.condition, flagRestartCondition, "", "Restart when condition is met (none, on-failure, or any)") 584 flags.Var(&opts.restartPolicy.delay, flagRestartDelay, "Delay between restart attempts (ns|us|ms|s|m|h)") 585 flags.Var(&opts.restartPolicy.maxAttempts, flagRestartMaxAttempts, "Maximum number of restarts before giving up") 586 flags.Var(&opts.restartPolicy.window, flagRestartWindow, "Window used to evaluate the restart policy (ns|us|ms|s|m|h)") 587 588 flags.Uint64Var(&opts.update.parallelism, flagUpdateParallelism, 1, "Maximum number of tasks updated simultaneously (0 to update all at once)") 589 flags.DurationVar(&opts.update.delay, flagUpdateDelay, time.Duration(0), "Delay between updates (ns|us|ms|s|m|h) (default 0s)") 590 flags.DurationVar(&opts.update.monitor, flagUpdateMonitor, time.Duration(0), "Duration after each task update to monitor for failure (ns|us|ms|s|m|h) (default 0s)") 591 flags.StringVar(&opts.update.onFailure, flagUpdateFailureAction, "pause", "Action on update failure (pause|continue)") 592 flags.Var(&opts.update.maxFailureRatio, flagUpdateMaxFailureRatio, "Failure rate to tolerate during an update") 593 594 flags.StringVar(&opts.endpoint.mode, flagEndpointMode, "", "Endpoint mode (vip or dnsrr)") 595 596 flags.BoolVar(&opts.registryAuth, flagRegistryAuth, false, "Send registry authentication details to swarm agents") 597 598 flags.StringVar(&opts.logDriver.name, flagLogDriver, "", "Logging driver for service") 599 flags.Var(&opts.logDriver.opts, flagLogOpt, "Logging driver options") 600 601 flags.StringVar(&opts.healthcheck.cmd, flagHealthCmd, "", "Command to run to check health") 602 flags.Var(&opts.healthcheck.interval, flagHealthInterval, "Time between running the check (ns|us|ms|s|m|h)") 603 flags.Var(&opts.healthcheck.timeout, flagHealthTimeout, "Maximum time to allow one check to run (ns|us|ms|s|m|h)") 604 flags.IntVar(&opts.healthcheck.retries, flagHealthRetries, 0, "Consecutive failures needed to report unhealthy") 605 flags.BoolVar(&opts.healthcheck.noHealthcheck, flagNoHealthcheck, false, "Disable any container-specified HEALTHCHECK") 606 607 flags.BoolVarP(&opts.tty, flagTTY, "t", false, "Allocate a pseudo-TTY") 608 } 609 610 const ( 611 flagConstraint = "constraint" 612 flagConstraintRemove = "constraint-rm" 613 flagConstraintAdd = "constraint-add" 614 flagContainerLabel = "container-label" 615 flagContainerLabelRemove = "container-label-rm" 616 flagContainerLabelAdd = "container-label-add" 617 flagDNS = "dns" 618 flagDNSRemove = "dns-rm" 619 flagDNSAdd = "dns-add" 620 flagDNSOption = "dns-option" 621 flagDNSOptionRemove = "dns-option-rm" 622 flagDNSOptionAdd = "dns-option-add" 623 flagDNSSearch = "dns-search" 624 flagDNSSearchRemove = "dns-search-rm" 625 flagDNSSearchAdd = "dns-search-add" 626 flagEndpointMode = "endpoint-mode" 627 flagHost = "host" 628 flagHostAdd = "host-add" 629 flagHostRemove = "host-rm" 630 flagHostname = "hostname" 631 flagEnv = "env" 632 flagEnvFile = "env-file" 633 flagEnvRemove = "env-rm" 634 flagEnvAdd = "env-add" 635 flagGroup = "group" 636 flagGroupAdd = "group-add" 637 flagGroupRemove = "group-rm" 638 flagLabel = "label" 639 flagLabelRemove = "label-rm" 640 flagLabelAdd = "label-add" 641 flagLimitCPU = "limit-cpu" 642 flagLimitMemory = "limit-memory" 643 flagMode = "mode" 644 flagMount = "mount" 645 flagMountRemove = "mount-rm" 646 flagMountAdd = "mount-add" 647 flagName = "name" 648 flagNetwork = "network" 649 flagPublish = "publish" 650 flagPublishRemove = "publish-rm" 651 flagPublishAdd = "publish-add" 652 flagPort = "port" 653 flagPortAdd = "port-add" 654 flagPortRemove = "port-rm" 655 flagReplicas = "replicas" 656 flagReserveCPU = "reserve-cpu" 657 flagReserveMemory = "reserve-memory" 658 flagRestartCondition = "restart-condition" 659 flagRestartDelay = "restart-delay" 660 flagRestartMaxAttempts = "restart-max-attempts" 661 flagRestartWindow = "restart-window" 662 flagStopGracePeriod = "stop-grace-period" 663 flagTTY = "tty" 664 flagUpdateDelay = "update-delay" 665 flagUpdateFailureAction = "update-failure-action" 666 flagUpdateMaxFailureRatio = "update-max-failure-ratio" 667 flagUpdateMonitor = "update-monitor" 668 flagUpdateParallelism = "update-parallelism" 669 flagUser = "user" 670 flagWorkdir = "workdir" 671 flagRegistryAuth = "with-registry-auth" 672 flagLogDriver = "log-driver" 673 flagLogOpt = "log-opt" 674 flagHealthCmd = "health-cmd" 675 flagHealthInterval = "health-interval" 676 flagHealthRetries = "health-retries" 677 flagHealthTimeout = "health-timeout" 678 flagNoHealthcheck = "no-healthcheck" 679 flagSecret = "secret" 680 flagSecretAdd = "secret-add" 681 flagSecretRemove = "secret-rm" 682 )