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