github.com/vieux/docker@v0.6.3-0.20161004191708-e097c2a938c7/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  	onFailure   string
   273  }
   274  
   275  type resourceOptions struct {
   276  	limitCPU      nanoCPUs
   277  	limitMemBytes memBytes
   278  	resCPU        nanoCPUs
   279  	resMemBytes   memBytes
   280  }
   281  
   282  func (r *resourceOptions) ToResourceRequirements() *swarm.ResourceRequirements {
   283  	return &swarm.ResourceRequirements{
   284  		Limits: &swarm.Resources{
   285  			NanoCPUs:    r.limitCPU.Value(),
   286  			MemoryBytes: r.limitMemBytes.Value(),
   287  		},
   288  		Reservations: &swarm.Resources{
   289  			NanoCPUs:    r.resCPU.Value(),
   290  			MemoryBytes: r.resMemBytes.Value(),
   291  		},
   292  	}
   293  }
   294  
   295  type restartPolicyOptions struct {
   296  	condition   string
   297  	delay       DurationOpt
   298  	maxAttempts Uint64Opt
   299  	window      DurationOpt
   300  }
   301  
   302  func (r *restartPolicyOptions) ToRestartPolicy() *swarm.RestartPolicy {
   303  	return &swarm.RestartPolicy{
   304  		Condition:   swarm.RestartPolicyCondition(r.condition),
   305  		Delay:       r.delay.Value(),
   306  		MaxAttempts: r.maxAttempts.Value(),
   307  		Window:      r.window.Value(),
   308  	}
   309  }
   310  
   311  func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig {
   312  	nets := []swarm.NetworkAttachmentConfig{}
   313  	for _, network := range networks {
   314  		nets = append(nets, swarm.NetworkAttachmentConfig{Target: network})
   315  	}
   316  	return nets
   317  }
   318  
   319  type endpointOptions struct {
   320  	mode  string
   321  	ports opts.ListOpts
   322  }
   323  
   324  func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec {
   325  	portConfigs := []swarm.PortConfig{}
   326  	// We can ignore errors because the format was already validated by ValidatePort
   327  	ports, portBindings, _ := nat.ParsePortSpecs(e.ports.GetAll())
   328  
   329  	for port := range ports {
   330  		portConfigs = append(portConfigs, convertPortToPortConfig(port, portBindings)...)
   331  	}
   332  
   333  	return &swarm.EndpointSpec{
   334  		Mode:  swarm.ResolutionMode(strings.ToLower(e.mode)),
   335  		Ports: portConfigs,
   336  	}
   337  }
   338  
   339  func convertPortToPortConfig(
   340  	port nat.Port,
   341  	portBindings map[nat.Port][]nat.PortBinding,
   342  ) []swarm.PortConfig {
   343  	ports := []swarm.PortConfig{}
   344  
   345  	for _, binding := range portBindings[port] {
   346  		hostPort, _ := strconv.ParseUint(binding.HostPort, 10, 16)
   347  		ports = append(ports, swarm.PortConfig{
   348  			//TODO Name: ?
   349  			Protocol:      swarm.PortConfigProtocol(strings.ToLower(port.Proto())),
   350  			TargetPort:    uint32(port.Int()),
   351  			PublishedPort: uint32(hostPort),
   352  		})
   353  	}
   354  	return ports
   355  }
   356  
   357  type logDriverOptions struct {
   358  	name string
   359  	opts opts.ListOpts
   360  }
   361  
   362  func newLogDriverOptions() logDriverOptions {
   363  	return logDriverOptions{opts: opts.NewListOpts(runconfigopts.ValidateEnv)}
   364  }
   365  
   366  func (ldo *logDriverOptions) toLogDriver() *swarm.Driver {
   367  	if ldo.name == "" {
   368  		return nil
   369  	}
   370  
   371  	// set the log driver only if specified.
   372  	return &swarm.Driver{
   373  		Name:    ldo.name,
   374  		Options: runconfigopts.ConvertKVStringsToMap(ldo.opts.GetAll()),
   375  	}
   376  }
   377  
   378  // ValidatePort validates a string is in the expected format for a port definition
   379  func ValidatePort(value string) (string, error) {
   380  	portMappings, err := nat.ParsePortSpec(value)
   381  	for _, portMapping := range portMappings {
   382  		if portMapping.Binding.HostIP != "" {
   383  			return "", fmt.Errorf("HostIP is not supported by a service.")
   384  		}
   385  	}
   386  	return value, err
   387  }
   388  
   389  type serviceOptions struct {
   390  	name            string
   391  	labels          opts.ListOpts
   392  	containerLabels opts.ListOpts
   393  	image           string
   394  	args            []string
   395  	env             opts.ListOpts
   396  	workdir         string
   397  	user            string
   398  	groups          []string
   399  	mounts          MountOpt
   400  
   401  	resources resourceOptions
   402  	stopGrace DurationOpt
   403  
   404  	replicas Uint64Opt
   405  	mode     string
   406  
   407  	restartPolicy restartPolicyOptions
   408  	constraints   []string
   409  	update        updateOptions
   410  	networks      []string
   411  	endpoint      endpointOptions
   412  
   413  	registryAuth bool
   414  
   415  	logDriver logDriverOptions
   416  }
   417  
   418  func newServiceOptions() *serviceOptions {
   419  	return &serviceOptions{
   420  		labels:          opts.NewListOpts(runconfigopts.ValidateEnv),
   421  		containerLabels: opts.NewListOpts(runconfigopts.ValidateEnv),
   422  		env:             opts.NewListOpts(runconfigopts.ValidateEnv),
   423  		endpoint: endpointOptions{
   424  			ports: opts.NewListOpts(ValidatePort),
   425  		},
   426  		logDriver: newLogDriverOptions(),
   427  	}
   428  }
   429  
   430  func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
   431  	var service swarm.ServiceSpec
   432  
   433  	service = swarm.ServiceSpec{
   434  		Annotations: swarm.Annotations{
   435  			Name:   opts.name,
   436  			Labels: runconfigopts.ConvertKVStringsToMap(opts.labels.GetAll()),
   437  		},
   438  		TaskTemplate: swarm.TaskSpec{
   439  			ContainerSpec: swarm.ContainerSpec{
   440  				Image:           opts.image,
   441  				Args:            opts.args,
   442  				Env:             opts.env.GetAll(),
   443  				Labels:          runconfigopts.ConvertKVStringsToMap(opts.containerLabels.GetAll()),
   444  				Dir:             opts.workdir,
   445  				User:            opts.user,
   446  				Groups:          opts.groups,
   447  				Mounts:          opts.mounts.Value(),
   448  				StopGracePeriod: opts.stopGrace.Value(),
   449  			},
   450  			Networks:      convertNetworks(opts.networks),
   451  			Resources:     opts.resources.ToResourceRequirements(),
   452  			RestartPolicy: opts.restartPolicy.ToRestartPolicy(),
   453  			Placement: &swarm.Placement{
   454  				Constraints: opts.constraints,
   455  			},
   456  			LogDriver: opts.logDriver.toLogDriver(),
   457  		},
   458  		Networks: convertNetworks(opts.networks),
   459  		Mode:     swarm.ServiceMode{},
   460  		UpdateConfig: &swarm.UpdateConfig{
   461  			Parallelism:   opts.update.parallelism,
   462  			Delay:         opts.update.delay,
   463  			FailureAction: opts.update.onFailure,
   464  		},
   465  		EndpointSpec: opts.endpoint.ToEndpointSpec(),
   466  	}
   467  
   468  	switch opts.mode {
   469  	case "global":
   470  		if opts.replicas.Value() != nil {
   471  			return service, fmt.Errorf("replicas can only be used with replicated mode")
   472  		}
   473  
   474  		service.Mode.Global = &swarm.GlobalService{}
   475  	case "replicated":
   476  		service.Mode.Replicated = &swarm.ReplicatedService{
   477  			Replicas: opts.replicas.Value(),
   478  		}
   479  	default:
   480  		return service, fmt.Errorf("Unknown mode: %s", opts.mode)
   481  	}
   482  	return service, nil
   483  }
   484  
   485  // addServiceFlags adds all flags that are common to both `create` and `update`.
   486  // Any flags that are not common are added separately in the individual command
   487  func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) {
   488  	flags := cmd.Flags()
   489  	flags.StringVar(&opts.name, flagName, "", "Service name")
   490  
   491  	flags.StringVarP(&opts.workdir, flagWorkdir, "w", "", "Working directory inside the container")
   492  	flags.StringVarP(&opts.user, flagUser, "u", "", "Username or UID (format: <name|uid>[:<group|gid>])")
   493  	flags.StringSliceVar(&opts.groups, flagGroupAdd, []string{}, "Add additional user groups to the container")
   494  
   495  	flags.Var(&opts.resources.limitCPU, flagLimitCPU, "Limit CPUs")
   496  	flags.Var(&opts.resources.limitMemBytes, flagLimitMemory, "Limit Memory")
   497  	flags.Var(&opts.resources.resCPU, flagReserveCPU, "Reserve CPUs")
   498  	flags.Var(&opts.resources.resMemBytes, flagReserveMemory, "Reserve Memory")
   499  	flags.Var(&opts.stopGrace, flagStopGracePeriod, "Time to wait before force killing a container")
   500  
   501  	flags.Var(&opts.replicas, flagReplicas, "Number of tasks")
   502  
   503  	flags.StringVar(&opts.restartPolicy.condition, flagRestartCondition, "", "Restart when condition is met (none, on-failure, or any)")
   504  	flags.Var(&opts.restartPolicy.delay, flagRestartDelay, "Delay between restart attempts")
   505  	flags.Var(&opts.restartPolicy.maxAttempts, flagRestartMaxAttempts, "Maximum number of restarts before giving up")
   506  	flags.Var(&opts.restartPolicy.window, flagRestartWindow, "Window used to evaluate the restart policy")
   507  
   508  	flags.Uint64Var(&opts.update.parallelism, flagUpdateParallelism, 1, "Maximum number of tasks updated simultaneously (0 to update all at once)")
   509  	flags.DurationVar(&opts.update.delay, flagUpdateDelay, time.Duration(0), "Delay between updates")
   510  	flags.StringVar(&opts.update.onFailure, flagUpdateFailureAction, "pause", "Action on update failure (pause|continue)")
   511  
   512  	flags.StringVar(&opts.endpoint.mode, flagEndpointMode, "", "Endpoint mode (vip or dnsrr)")
   513  
   514  	flags.BoolVar(&opts.registryAuth, flagRegistryAuth, false, "Send registry authentication details to swarm agents")
   515  
   516  	flags.StringVar(&opts.logDriver.name, flagLogDriver, "", "Logging driver for service")
   517  	flags.Var(&opts.logDriver.opts, flagLogOpt, "Logging driver options")
   518  }
   519  
   520  const (
   521  	flagConstraint           = "constraint"
   522  	flagConstraintRemove     = "constraint-rm"
   523  	flagConstraintAdd        = "constraint-add"
   524  	flagContainerLabel       = "container-label"
   525  	flagContainerLabelRemove = "container-label-rm"
   526  	flagContainerLabelAdd    = "container-label-add"
   527  	flagEndpointMode         = "endpoint-mode"
   528  	flagEnv                  = "env"
   529  	flagEnvRemove            = "env-rm"
   530  	flagEnvAdd               = "env-add"
   531  	flagGroupAdd             = "group-add"
   532  	flagGroupRemove          = "group-rm"
   533  	flagLabel                = "label"
   534  	flagLabelRemove          = "label-rm"
   535  	flagLabelAdd             = "label-add"
   536  	flagLimitCPU             = "limit-cpu"
   537  	flagLimitMemory          = "limit-memory"
   538  	flagMode                 = "mode"
   539  	flagMount                = "mount"
   540  	flagMountRemove          = "mount-rm"
   541  	flagMountAdd             = "mount-add"
   542  	flagName                 = "name"
   543  	flagNetwork              = "network"
   544  	flagPublish              = "publish"
   545  	flagPublishRemove        = "publish-rm"
   546  	flagPublishAdd           = "publish-add"
   547  	flagReplicas             = "replicas"
   548  	flagReserveCPU           = "reserve-cpu"
   549  	flagReserveMemory        = "reserve-memory"
   550  	flagRestartCondition     = "restart-condition"
   551  	flagRestartDelay         = "restart-delay"
   552  	flagRestartMaxAttempts   = "restart-max-attempts"
   553  	flagRestartWindow        = "restart-window"
   554  	flagStopGracePeriod      = "stop-grace-period"
   555  	flagUpdateDelay          = "update-delay"
   556  	flagUpdateFailureAction  = "update-failure-action"
   557  	flagUpdateParallelism    = "update-parallelism"
   558  	flagUser                 = "user"
   559  	flagWorkdir              = "workdir"
   560  	flagRegistryAuth         = "with-registry-auth"
   561  	flagLogDriver            = "log-driver"
   562  	flagLogOpt               = "log-opt"
   563  )