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  )