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