github.com/openshift/moby-moby@v1.13.2-0.20170601211448-f5ec1e2936dc/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 "" 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 "" 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.PortOpt 292 } 293 294 func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec { 295 return &swarm.EndpointSpec{ 296 Mode: swarm.ResolutionMode(strings.ToLower(e.mode)), 297 Ports: e.publishPorts.Value(), 298 } 299 } 300 301 type logDriverOptions struct { 302 name string 303 opts opts.ListOpts 304 } 305 306 func newLogDriverOptions() logDriverOptions { 307 return logDriverOptions{opts: opts.NewListOpts(runconfigopts.ValidateEnv)} 308 } 309 310 func (ldo *logDriverOptions) toLogDriver() *swarm.Driver { 311 if ldo.name == "" { 312 return nil 313 } 314 315 // set the log driver only if specified. 316 return &swarm.Driver{ 317 Name: ldo.name, 318 Options: runconfigopts.ConvertKVStringsToMap(ldo.opts.GetAll()), 319 } 320 } 321 322 type healthCheckOptions struct { 323 cmd string 324 interval PositiveDurationOpt 325 timeout PositiveDurationOpt 326 retries int 327 noHealthcheck bool 328 } 329 330 func (opts *healthCheckOptions) toHealthConfig() (*container.HealthConfig, error) { 331 var healthConfig *container.HealthConfig 332 haveHealthSettings := opts.cmd != "" || 333 opts.interval.Value() != nil || 334 opts.timeout.Value() != nil || 335 opts.retries != 0 336 if opts.noHealthcheck { 337 if haveHealthSettings { 338 return nil, fmt.Errorf("--%s conflicts with --health-* options", flagNoHealthcheck) 339 } 340 healthConfig = &container.HealthConfig{Test: []string{"NONE"}} 341 } else if haveHealthSettings { 342 var test []string 343 if opts.cmd != "" { 344 test = []string{"CMD-SHELL", opts.cmd} 345 } 346 var interval, timeout time.Duration 347 if ptr := opts.interval.Value(); ptr != nil { 348 interval = *ptr 349 } 350 if ptr := opts.timeout.Value(); ptr != nil { 351 timeout = *ptr 352 } 353 healthConfig = &container.HealthConfig{ 354 Test: test, 355 Interval: interval, 356 Timeout: timeout, 357 Retries: opts.retries, 358 } 359 } 360 return healthConfig, nil 361 } 362 363 // ValidatePort validates a string is in the expected format for a port definition 364 func ValidatePort(value string) (string, error) { 365 portMappings, err := nat.ParsePortSpec(value) 366 for _, portMapping := range portMappings { 367 if portMapping.Binding.HostIP != "" { 368 return "", fmt.Errorf("HostIP is not supported by a service.") 369 } 370 } 371 return value, err 372 } 373 374 // convertExtraHostsToSwarmHosts converts an array of extra hosts in cli 375 // <host>:<ip> 376 // into a swarmkit host format: 377 // IP_address canonical_hostname [aliases...] 378 // This assumes input value (<host>:<ip>) has already been validated 379 func convertExtraHostsToSwarmHosts(extraHosts []string) []string { 380 hosts := []string{} 381 for _, extraHost := range extraHosts { 382 parts := strings.SplitN(extraHost, ":", 2) 383 hosts = append(hosts, fmt.Sprintf("%s %s", parts[1], parts[0])) 384 } 385 return hosts 386 } 387 388 type serviceOptions struct { 389 name string 390 labels opts.ListOpts 391 containerLabels opts.ListOpts 392 image string 393 args []string 394 hostname string 395 env opts.ListOpts 396 envFile opts.ListOpts 397 workdir string 398 user string 399 groups opts.ListOpts 400 tty bool 401 mounts opts.MountOpt 402 dns opts.ListOpts 403 dnsSearch opts.ListOpts 404 dnsOption opts.ListOpts 405 hosts opts.ListOpts 406 407 resources resourceOptions 408 stopGrace DurationOpt 409 410 replicas Uint64Opt 411 mode string 412 413 restartPolicy restartPolicyOptions 414 constraints opts.ListOpts 415 update updateOptions 416 networks opts.ListOpts 417 endpoint endpointOptions 418 419 registryAuth bool 420 421 logDriver logDriverOptions 422 423 healthcheck healthCheckOptions 424 secrets opts.SecretOpt 425 } 426 427 func newServiceOptions() *serviceOptions { 428 return &serviceOptions{ 429 labels: opts.NewListOpts(runconfigopts.ValidateEnv), 430 constraints: opts.NewListOpts(nil), 431 containerLabels: opts.NewListOpts(runconfigopts.ValidateEnv), 432 env: opts.NewListOpts(runconfigopts.ValidateEnv), 433 envFile: opts.NewListOpts(nil), 434 groups: opts.NewListOpts(nil), 435 logDriver: newLogDriverOptions(), 436 dns: opts.NewListOpts(opts.ValidateIPAddress), 437 dnsOption: opts.NewListOpts(nil), 438 dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch), 439 hosts: opts.NewListOpts(runconfigopts.ValidateExtraHost), 440 networks: opts.NewListOpts(nil), 441 } 442 } 443 444 func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { 445 var service swarm.ServiceSpec 446 447 envVariables, err := runconfigopts.ReadKVStrings(opts.envFile.GetAll(), opts.env.GetAll()) 448 if err != nil { 449 return service, err 450 } 451 452 currentEnv := make([]string, 0, len(envVariables)) 453 for _, env := range envVariables { // need to process each var, in order 454 k := strings.SplitN(env, "=", 2)[0] 455 for i, current := range currentEnv { // remove duplicates 456 if current == env { 457 continue // no update required, may hide this behind flag to preserve order of envVariables 458 } 459 if strings.HasPrefix(current, k+"=") { 460 currentEnv = append(currentEnv[:i], currentEnv[i+1:]...) 461 } 462 } 463 currentEnv = append(currentEnv, env) 464 } 465 466 service = swarm.ServiceSpec{ 467 Annotations: swarm.Annotations{ 468 Name: opts.name, 469 Labels: runconfigopts.ConvertKVStringsToMap(opts.labels.GetAll()), 470 }, 471 TaskTemplate: swarm.TaskSpec{ 472 ContainerSpec: swarm.ContainerSpec{ 473 Image: opts.image, 474 Args: opts.args, 475 Env: currentEnv, 476 Hostname: opts.hostname, 477 Labels: runconfigopts.ConvertKVStringsToMap(opts.containerLabels.GetAll()), 478 Dir: opts.workdir, 479 User: opts.user, 480 Groups: opts.groups.GetAll(), 481 TTY: opts.tty, 482 Mounts: opts.mounts.Value(), 483 DNSConfig: &swarm.DNSConfig{ 484 Nameservers: opts.dns.GetAll(), 485 Search: opts.dnsSearch.GetAll(), 486 Options: opts.dnsOption.GetAll(), 487 }, 488 Hosts: convertExtraHostsToSwarmHosts(opts.hosts.GetAll()), 489 StopGracePeriod: opts.stopGrace.Value(), 490 Secrets: nil, 491 }, 492 Networks: convertNetworks(opts.networks.GetAll()), 493 Resources: opts.resources.ToResourceRequirements(), 494 RestartPolicy: opts.restartPolicy.ToRestartPolicy(), 495 Placement: &swarm.Placement{ 496 Constraints: opts.constraints.GetAll(), 497 }, 498 LogDriver: opts.logDriver.toLogDriver(), 499 }, 500 Networks: convertNetworks(opts.networks.GetAll()), 501 Mode: swarm.ServiceMode{}, 502 UpdateConfig: &swarm.UpdateConfig{ 503 Parallelism: opts.update.parallelism, 504 Delay: opts.update.delay, 505 Monitor: opts.update.monitor, 506 FailureAction: opts.update.onFailure, 507 MaxFailureRatio: opts.update.maxFailureRatio.Value(), 508 }, 509 EndpointSpec: opts.endpoint.ToEndpointSpec(), 510 } 511 512 healthConfig, err := opts.healthcheck.toHealthConfig() 513 if err != nil { 514 return service, err 515 } 516 service.TaskTemplate.ContainerSpec.Healthcheck = healthConfig 517 518 switch opts.mode { 519 case "global": 520 if opts.replicas.Value() != nil { 521 return service, fmt.Errorf("replicas can only be used with replicated mode") 522 } 523 524 service.Mode.Global = &swarm.GlobalService{} 525 case "replicated": 526 service.Mode.Replicated = &swarm.ReplicatedService{ 527 Replicas: opts.replicas.Value(), 528 } 529 default: 530 return service, fmt.Errorf("Unknown mode: %s", opts.mode) 531 } 532 return service, nil 533 } 534 535 // addServiceFlags adds all flags that are common to both `create` and `update`. 536 // Any flags that are not common are added separately in the individual command 537 func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { 538 flags := cmd.Flags() 539 540 flags.StringVarP(&opts.workdir, flagWorkdir, "w", "", "Working directory inside the container") 541 flags.StringVarP(&opts.user, flagUser, "u", "", "Username or UID (format: <name|uid>[:<group|gid>])") 542 flags.StringVar(&opts.hostname, flagHostname, "", "Container hostname") 543 544 flags.Var(&opts.resources.limitCPU, flagLimitCPU, "Limit CPUs") 545 flags.Var(&opts.resources.limitMemBytes, flagLimitMemory, "Limit Memory") 546 flags.Var(&opts.resources.resCPU, flagReserveCPU, "Reserve CPUs") 547 flags.Var(&opts.resources.resMemBytes, flagReserveMemory, "Reserve Memory") 548 flags.Var(&opts.stopGrace, flagStopGracePeriod, "Time to wait before force killing a container (ns|us|ms|s|m|h)") 549 550 flags.Var(&opts.replicas, flagReplicas, "Number of tasks") 551 552 flags.StringVar(&opts.restartPolicy.condition, flagRestartCondition, "", "Restart when condition is met (none, on-failure, or any)") 553 flags.Var(&opts.restartPolicy.delay, flagRestartDelay, "Delay between restart attempts (ns|us|ms|s|m|h)") 554 flags.Var(&opts.restartPolicy.maxAttempts, flagRestartMaxAttempts, "Maximum number of restarts before giving up") 555 flags.Var(&opts.restartPolicy.window, flagRestartWindow, "Window used to evaluate the restart policy (ns|us|ms|s|m|h)") 556 557 flags.Uint64Var(&opts.update.parallelism, flagUpdateParallelism, 1, "Maximum number of tasks updated simultaneously (0 to update all at once)") 558 flags.DurationVar(&opts.update.delay, flagUpdateDelay, time.Duration(0), "Delay between updates (ns|us|ms|s|m|h) (default 0s)") 559 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)") 560 flags.StringVar(&opts.update.onFailure, flagUpdateFailureAction, "pause", "Action on update failure (pause|continue)") 561 flags.Var(&opts.update.maxFailureRatio, flagUpdateMaxFailureRatio, "Failure rate to tolerate during an update") 562 563 flags.StringVar(&opts.endpoint.mode, flagEndpointMode, "", "Endpoint mode (vip or dnsrr)") 564 565 flags.BoolVar(&opts.registryAuth, flagRegistryAuth, false, "Send registry authentication details to swarm agents") 566 567 flags.StringVar(&opts.logDriver.name, flagLogDriver, "", "Logging driver for service") 568 flags.Var(&opts.logDriver.opts, flagLogOpt, "Logging driver options") 569 570 flags.StringVar(&opts.healthcheck.cmd, flagHealthCmd, "", "Command to run to check health") 571 flags.Var(&opts.healthcheck.interval, flagHealthInterval, "Time between running the check (ns|us|ms|s|m|h)") 572 flags.Var(&opts.healthcheck.timeout, flagHealthTimeout, "Maximum time to allow one check to run (ns|us|ms|s|m|h)") 573 flags.IntVar(&opts.healthcheck.retries, flagHealthRetries, 0, "Consecutive failures needed to report unhealthy") 574 flags.BoolVar(&opts.healthcheck.noHealthcheck, flagNoHealthcheck, false, "Disable any container-specified HEALTHCHECK") 575 576 flags.BoolVarP(&opts.tty, flagTTY, "t", false, "Allocate a pseudo-TTY") 577 } 578 579 const ( 580 flagConstraint = "constraint" 581 flagConstraintRemove = "constraint-rm" 582 flagConstraintAdd = "constraint-add" 583 flagContainerLabel = "container-label" 584 flagContainerLabelRemove = "container-label-rm" 585 flagContainerLabelAdd = "container-label-add" 586 flagDNS = "dns" 587 flagDNSRemove = "dns-rm" 588 flagDNSAdd = "dns-add" 589 flagDNSOption = "dns-option" 590 flagDNSOptionRemove = "dns-option-rm" 591 flagDNSOptionAdd = "dns-option-add" 592 flagDNSSearch = "dns-search" 593 flagDNSSearchRemove = "dns-search-rm" 594 flagDNSSearchAdd = "dns-search-add" 595 flagEndpointMode = "endpoint-mode" 596 flagHost = "host" 597 flagHostAdd = "host-add" 598 flagHostRemove = "host-rm" 599 flagHostname = "hostname" 600 flagEnv = "env" 601 flagEnvFile = "env-file" 602 flagEnvRemove = "env-rm" 603 flagEnvAdd = "env-add" 604 flagGroup = "group" 605 flagGroupAdd = "group-add" 606 flagGroupRemove = "group-rm" 607 flagLabel = "label" 608 flagLabelRemove = "label-rm" 609 flagLabelAdd = "label-add" 610 flagLimitCPU = "limit-cpu" 611 flagLimitMemory = "limit-memory" 612 flagMode = "mode" 613 flagMount = "mount" 614 flagMountRemove = "mount-rm" 615 flagMountAdd = "mount-add" 616 flagName = "name" 617 flagNetwork = "network" 618 flagPublish = "publish" 619 flagPublishRemove = "publish-rm" 620 flagPublishAdd = "publish-add" 621 flagReplicas = "replicas" 622 flagReserveCPU = "reserve-cpu" 623 flagReserveMemory = "reserve-memory" 624 flagRestartCondition = "restart-condition" 625 flagRestartDelay = "restart-delay" 626 flagRestartMaxAttempts = "restart-max-attempts" 627 flagRestartWindow = "restart-window" 628 flagStopGracePeriod = "stop-grace-period" 629 flagTTY = "tty" 630 flagUpdateDelay = "update-delay" 631 flagUpdateFailureAction = "update-failure-action" 632 flagUpdateMaxFailureRatio = "update-max-failure-ratio" 633 flagUpdateMonitor = "update-monitor" 634 flagUpdateParallelism = "update-parallelism" 635 flagUser = "user" 636 flagWorkdir = "workdir" 637 flagRegistryAuth = "with-registry-auth" 638 flagLogDriver = "log-driver" 639 flagLogOpt = "log-opt" 640 flagHealthCmd = "health-cmd" 641 flagHealthInterval = "health-interval" 642 flagHealthRetries = "health-retries" 643 flagHealthTimeout = "health-timeout" 644 flagNoHealthcheck = "no-healthcheck" 645 flagSecret = "secret" 646 flagSecretAdd = "secret-add" 647 flagSecretRemove = "secret-rm" 648 )