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  )