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