github.com/olljanat/moby@v1.13.1/cli/command/service/opts.go (about)

     1  package service
     2  
     3  import (
     4  	"encoding/csv"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"strconv"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/docker/docker/api/types/container"
    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 "bytes"
    39  }
    40  
    41  func (m *memBytes) Value() int64 {
    42  	return int64(*m)
    43  }
    44  
    45  // PositiveDurationOpt is an option type for time.Duration that uses a pointer.
    46  // It bahave similarly to DurationOpt but only allows positive duration values.
    47  type PositiveDurationOpt struct {
    48  	DurationOpt
    49  }
    50  
    51  // Set a new value on the option. Setting a negative duration value will cause
    52  // an error to be returned.
    53  func (d *PositiveDurationOpt) Set(s string) error {
    54  	err := d.DurationOpt.Set(s)
    55  	if err != nil {
    56  		return err
    57  	}
    58  	if *d.DurationOpt.value < 0 {
    59  		return fmt.Errorf("duration cannot be negative")
    60  	}
    61  	return nil
    62  }
    63  
    64  // DurationOpt is an option type for time.Duration that uses a pointer. This
    65  // allows us to get nil values outside, instead of defaulting to 0
    66  type DurationOpt struct {
    67  	value *time.Duration
    68  }
    69  
    70  // Set a new value on the option
    71  func (d *DurationOpt) Set(s string) error {
    72  	v, err := time.ParseDuration(s)
    73  	d.value = &v
    74  	return err
    75  }
    76  
    77  // Type returns the type of this option, which will be displayed in `--help` output
    78  func (d *DurationOpt) Type() string {
    79  	return "duration"
    80  }
    81  
    82  // String returns a string repr of this option
    83  func (d *DurationOpt) String() string {
    84  	if d.value != nil {
    85  		return d.value.String()
    86  	}
    87  	return ""
    88  }
    89  
    90  // Value returns the time.Duration
    91  func (d *DurationOpt) Value() *time.Duration {
    92  	return d.value
    93  }
    94  
    95  // Uint64Opt represents a uint64.
    96  type Uint64Opt struct {
    97  	value *uint64
    98  }
    99  
   100  // Set a new value on the option
   101  func (i *Uint64Opt) Set(s string) error {
   102  	v, err := strconv.ParseUint(s, 0, 64)
   103  	i.value = &v
   104  	return err
   105  }
   106  
   107  // Type returns the type of this option, which will be displayed in `--help` output
   108  func (i *Uint64Opt) Type() string {
   109  	return "uint"
   110  }
   111  
   112  // String returns a string repr of this option
   113  func (i *Uint64Opt) String() string {
   114  	if i.value != nil {
   115  		return fmt.Sprintf("%v", *i.value)
   116  	}
   117  	return ""
   118  }
   119  
   120  // Value returns the uint64
   121  func (i *Uint64Opt) Value() *uint64 {
   122  	return i.value
   123  }
   124  
   125  type floatValue float32
   126  
   127  func (f *floatValue) Set(s string) error {
   128  	v, err := strconv.ParseFloat(s, 32)
   129  	*f = floatValue(v)
   130  	return err
   131  }
   132  
   133  func (f *floatValue) Type() string {
   134  	return "float"
   135  }
   136  
   137  func (f *floatValue) String() string {
   138  	return strconv.FormatFloat(float64(*f), 'g', -1, 32)
   139  }
   140  
   141  func (f *floatValue) Value() float32 {
   142  	return float32(*f)
   143  }
   144  
   145  // SecretRequestSpec is a type for requesting secrets
   146  type SecretRequestSpec struct {
   147  	source string
   148  	target string
   149  	uid    string
   150  	gid    string
   151  	mode   os.FileMode
   152  }
   153  
   154  // SecretOpt is a Value type for parsing secrets
   155  type SecretOpt struct {
   156  	values []*SecretRequestSpec
   157  }
   158  
   159  // Set a new secret value
   160  func (o *SecretOpt) Set(value string) error {
   161  	csvReader := csv.NewReader(strings.NewReader(value))
   162  	fields, err := csvReader.Read()
   163  	if err != nil {
   164  		return err
   165  	}
   166  
   167  	spec := &SecretRequestSpec{
   168  		source: "",
   169  		target: "",
   170  		uid:    "0",
   171  		gid:    "0",
   172  		mode:   0444,
   173  	}
   174  
   175  	for _, field := range fields {
   176  		parts := strings.SplitN(field, "=", 2)
   177  		key := strings.ToLower(parts[0])
   178  
   179  		if len(parts) != 2 {
   180  			return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
   181  		}
   182  
   183  		value := parts[1]
   184  		switch key {
   185  		case "source", "src":
   186  			spec.source = value
   187  		case "target":
   188  			tDir, _ := filepath.Split(value)
   189  			if tDir != "" {
   190  				return fmt.Errorf("target must not have a path")
   191  			}
   192  			spec.target = value
   193  		case "uid":
   194  			spec.uid = value
   195  		case "gid":
   196  			spec.gid = value
   197  		case "mode":
   198  			m, err := strconv.ParseUint(value, 0, 32)
   199  			if err != nil {
   200  				return fmt.Errorf("invalid mode specified: %v", err)
   201  			}
   202  
   203  			spec.mode = os.FileMode(m)
   204  		default:
   205  			return fmt.Errorf("invalid field in secret request: %s", key)
   206  		}
   207  	}
   208  
   209  	if spec.source == "" {
   210  		return fmt.Errorf("source is required")
   211  	}
   212  
   213  	o.values = append(o.values, spec)
   214  	return nil
   215  }
   216  
   217  // Type returns the type of this option
   218  func (o *SecretOpt) Type() string {
   219  	return "secret"
   220  }
   221  
   222  // String returns a string repr of this option
   223  func (o *SecretOpt) String() string {
   224  	secrets := []string{}
   225  	for _, secret := range o.values {
   226  		repr := fmt.Sprintf("%s -> %s", secret.source, secret.target)
   227  		secrets = append(secrets, repr)
   228  	}
   229  	return strings.Join(secrets, ", ")
   230  }
   231  
   232  // Value returns the secret requests
   233  func (o *SecretOpt) Value() []*SecretRequestSpec {
   234  	return o.values
   235  }
   236  
   237  type updateOptions struct {
   238  	parallelism     uint64
   239  	delay           time.Duration
   240  	monitor         time.Duration
   241  	onFailure       string
   242  	maxFailureRatio floatValue
   243  }
   244  
   245  type resourceOptions struct {
   246  	limitCPU      opts.NanoCPUs
   247  	limitMemBytes memBytes
   248  	resCPU        opts.NanoCPUs
   249  	resMemBytes   memBytes
   250  }
   251  
   252  func (r *resourceOptions) ToResourceRequirements() *swarm.ResourceRequirements {
   253  	return &swarm.ResourceRequirements{
   254  		Limits: &swarm.Resources{
   255  			NanoCPUs:    r.limitCPU.Value(),
   256  			MemoryBytes: r.limitMemBytes.Value(),
   257  		},
   258  		Reservations: &swarm.Resources{
   259  			NanoCPUs:    r.resCPU.Value(),
   260  			MemoryBytes: r.resMemBytes.Value(),
   261  		},
   262  	}
   263  }
   264  
   265  type restartPolicyOptions struct {
   266  	condition   string
   267  	delay       DurationOpt
   268  	maxAttempts Uint64Opt
   269  	window      DurationOpt
   270  }
   271  
   272  func (r *restartPolicyOptions) ToRestartPolicy() *swarm.RestartPolicy {
   273  	return &swarm.RestartPolicy{
   274  		Condition:   swarm.RestartPolicyCondition(r.condition),
   275  		Delay:       r.delay.Value(),
   276  		MaxAttempts: r.maxAttempts.Value(),
   277  		Window:      r.window.Value(),
   278  	}
   279  }
   280  
   281  func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig {
   282  	nets := []swarm.NetworkAttachmentConfig{}
   283  	for _, network := range networks {
   284  		nets = append(nets, swarm.NetworkAttachmentConfig{Target: network})
   285  	}
   286  	return nets
   287  }
   288  
   289  type endpointOptions struct {
   290  	mode         string
   291  	publishPorts opts.PortOpt
   292  }
   293  
   294  func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec {
   295  	return &swarm.EndpointSpec{
   296  		Mode:  swarm.ResolutionMode(strings.ToLower(e.mode)),
   297  		Ports: e.publishPorts.Value(),
   298  	}
   299  }
   300  
   301  type logDriverOptions struct {
   302  	name string
   303  	opts opts.ListOpts
   304  }
   305  
   306  func newLogDriverOptions() logDriverOptions {
   307  	return logDriverOptions{opts: opts.NewListOpts(runconfigopts.ValidateEnv)}
   308  }
   309  
   310  func (ldo *logDriverOptions) toLogDriver() *swarm.Driver {
   311  	if ldo.name == "" {
   312  		return nil
   313  	}
   314  
   315  	// set the log driver only if specified.
   316  	return &swarm.Driver{
   317  		Name:    ldo.name,
   318  		Options: runconfigopts.ConvertKVStringsToMap(ldo.opts.GetAll()),
   319  	}
   320  }
   321  
   322  type healthCheckOptions struct {
   323  	cmd           string
   324  	interval      PositiveDurationOpt
   325  	timeout       PositiveDurationOpt
   326  	retries       int
   327  	noHealthcheck bool
   328  }
   329  
   330  func (opts *healthCheckOptions) toHealthConfig() (*container.HealthConfig, error) {
   331  	var healthConfig *container.HealthConfig
   332  	haveHealthSettings := opts.cmd != "" ||
   333  		opts.interval.Value() != nil ||
   334  		opts.timeout.Value() != nil ||
   335  		opts.retries != 0
   336  	if opts.noHealthcheck {
   337  		if haveHealthSettings {
   338  			return nil, fmt.Errorf("--%s conflicts with --health-* options", flagNoHealthcheck)
   339  		}
   340  		healthConfig = &container.HealthConfig{Test: []string{"NONE"}}
   341  	} else if haveHealthSettings {
   342  		var test []string
   343  		if opts.cmd != "" {
   344  			test = []string{"CMD-SHELL", opts.cmd}
   345  		}
   346  		var interval, timeout time.Duration
   347  		if ptr := opts.interval.Value(); ptr != nil {
   348  			interval = *ptr
   349  		}
   350  		if ptr := opts.timeout.Value(); ptr != nil {
   351  			timeout = *ptr
   352  		}
   353  		healthConfig = &container.HealthConfig{
   354  			Test:     test,
   355  			Interval: interval,
   356  			Timeout:  timeout,
   357  			Retries:  opts.retries,
   358  		}
   359  	}
   360  	return healthConfig, nil
   361  }
   362  
   363  // ValidatePort validates a string is in the expected format for a port definition
   364  func ValidatePort(value string) (string, error) {
   365  	portMappings, err := nat.ParsePortSpec(value)
   366  	for _, portMapping := range portMappings {
   367  		if portMapping.Binding.HostIP != "" {
   368  			return "", fmt.Errorf("HostIP is not supported by a service.")
   369  		}
   370  	}
   371  	return value, err
   372  }
   373  
   374  // convertExtraHostsToSwarmHosts converts an array of extra hosts in cli
   375  //     <host>:<ip>
   376  // into a swarmkit host format:
   377  //     IP_address canonical_hostname [aliases...]
   378  // This assumes input value (<host>:<ip>) has already been validated
   379  func convertExtraHostsToSwarmHosts(extraHosts []string) []string {
   380  	hosts := []string{}
   381  	for _, extraHost := range extraHosts {
   382  		parts := strings.SplitN(extraHost, ":", 2)
   383  		hosts = append(hosts, fmt.Sprintf("%s %s", parts[1], parts[0]))
   384  	}
   385  	return hosts
   386  }
   387  
   388  type serviceOptions struct {
   389  	name            string
   390  	labels          opts.ListOpts
   391  	containerLabels opts.ListOpts
   392  	image           string
   393  	args            []string
   394  	hostname        string
   395  	env             opts.ListOpts
   396  	envFile         opts.ListOpts
   397  	workdir         string
   398  	user            string
   399  	groups          opts.ListOpts
   400  	tty             bool
   401  	mounts          opts.MountOpt
   402  	dns             opts.ListOpts
   403  	dnsSearch       opts.ListOpts
   404  	dnsOption       opts.ListOpts
   405  	hosts           opts.ListOpts
   406  
   407  	resources resourceOptions
   408  	stopGrace DurationOpt
   409  
   410  	replicas Uint64Opt
   411  	mode     string
   412  
   413  	restartPolicy restartPolicyOptions
   414  	constraints   opts.ListOpts
   415  	update        updateOptions
   416  	networks      opts.ListOpts
   417  	endpoint      endpointOptions
   418  
   419  	registryAuth bool
   420  
   421  	logDriver logDriverOptions
   422  
   423  	healthcheck healthCheckOptions
   424  	secrets     opts.SecretOpt
   425  }
   426  
   427  func newServiceOptions() *serviceOptions {
   428  	return &serviceOptions{
   429  		labels:          opts.NewListOpts(runconfigopts.ValidateEnv),
   430  		constraints:     opts.NewListOpts(nil),
   431  		containerLabels: opts.NewListOpts(runconfigopts.ValidateEnv),
   432  		env:             opts.NewListOpts(runconfigopts.ValidateEnv),
   433  		envFile:         opts.NewListOpts(nil),
   434  		groups:          opts.NewListOpts(nil),
   435  		logDriver:       newLogDriverOptions(),
   436  		dns:             opts.NewListOpts(opts.ValidateIPAddress),
   437  		dnsOption:       opts.NewListOpts(nil),
   438  		dnsSearch:       opts.NewListOpts(opts.ValidateDNSSearch),
   439  		hosts:           opts.NewListOpts(runconfigopts.ValidateExtraHost),
   440  		networks:        opts.NewListOpts(nil),
   441  	}
   442  }
   443  
   444  func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
   445  	var service swarm.ServiceSpec
   446  
   447  	envVariables, err := runconfigopts.ReadKVStrings(opts.envFile.GetAll(), opts.env.GetAll())
   448  	if err != nil {
   449  		return service, err
   450  	}
   451  
   452  	currentEnv := make([]string, 0, len(envVariables))
   453  	for _, env := range envVariables { // need to process each var, in order
   454  		k := strings.SplitN(env, "=", 2)[0]
   455  		for i, current := range currentEnv { // remove duplicates
   456  			if current == env {
   457  				continue // no update required, may hide this behind flag to preserve order of envVariables
   458  			}
   459  			if strings.HasPrefix(current, k+"=") {
   460  				currentEnv = append(currentEnv[:i], currentEnv[i+1:]...)
   461  			}
   462  		}
   463  		currentEnv = append(currentEnv, env)
   464  	}
   465  
   466  	service = swarm.ServiceSpec{
   467  		Annotations: swarm.Annotations{
   468  			Name:   opts.name,
   469  			Labels: runconfigopts.ConvertKVStringsToMap(opts.labels.GetAll()),
   470  		},
   471  		TaskTemplate: swarm.TaskSpec{
   472  			ContainerSpec: swarm.ContainerSpec{
   473  				Image:    opts.image,
   474  				Args:     opts.args,
   475  				Env:      currentEnv,
   476  				Hostname: opts.hostname,
   477  				Labels:   runconfigopts.ConvertKVStringsToMap(opts.containerLabels.GetAll()),
   478  				Dir:      opts.workdir,
   479  				User:     opts.user,
   480  				Groups:   opts.groups.GetAll(),
   481  				TTY:      opts.tty,
   482  				Mounts:   opts.mounts.Value(),
   483  				DNSConfig: &swarm.DNSConfig{
   484  					Nameservers: opts.dns.GetAll(),
   485  					Search:      opts.dnsSearch.GetAll(),
   486  					Options:     opts.dnsOption.GetAll(),
   487  				},
   488  				Hosts:           convertExtraHostsToSwarmHosts(opts.hosts.GetAll()),
   489  				StopGracePeriod: opts.stopGrace.Value(),
   490  				Secrets:         nil,
   491  			},
   492  			Networks:      convertNetworks(opts.networks.GetAll()),
   493  			Resources:     opts.resources.ToResourceRequirements(),
   494  			RestartPolicy: opts.restartPolicy.ToRestartPolicy(),
   495  			Placement: &swarm.Placement{
   496  				Constraints: opts.constraints.GetAll(),
   497  			},
   498  			LogDriver: opts.logDriver.toLogDriver(),
   499  		},
   500  		Networks: convertNetworks(opts.networks.GetAll()),
   501  		Mode:     swarm.ServiceMode{},
   502  		UpdateConfig: &swarm.UpdateConfig{
   503  			Parallelism:     opts.update.parallelism,
   504  			Delay:           opts.update.delay,
   505  			Monitor:         opts.update.monitor,
   506  			FailureAction:   opts.update.onFailure,
   507  			MaxFailureRatio: opts.update.maxFailureRatio.Value(),
   508  		},
   509  		EndpointSpec: opts.endpoint.ToEndpointSpec(),
   510  	}
   511  
   512  	healthConfig, err := opts.healthcheck.toHealthConfig()
   513  	if err != nil {
   514  		return service, err
   515  	}
   516  	service.TaskTemplate.ContainerSpec.Healthcheck = healthConfig
   517  
   518  	switch opts.mode {
   519  	case "global":
   520  		if opts.replicas.Value() != nil {
   521  			return service, fmt.Errorf("replicas can only be used with replicated mode")
   522  		}
   523  
   524  		service.Mode.Global = &swarm.GlobalService{}
   525  	case "replicated":
   526  		service.Mode.Replicated = &swarm.ReplicatedService{
   527  			Replicas: opts.replicas.Value(),
   528  		}
   529  	default:
   530  		return service, fmt.Errorf("Unknown mode: %s", opts.mode)
   531  	}
   532  	return service, nil
   533  }
   534  
   535  // addServiceFlags adds all flags that are common to both `create` and `update`.
   536  // Any flags that are not common are added separately in the individual command
   537  func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) {
   538  	flags := cmd.Flags()
   539  
   540  	flags.StringVarP(&opts.workdir, flagWorkdir, "w", "", "Working directory inside the container")
   541  	flags.StringVarP(&opts.user, flagUser, "u", "", "Username or UID (format: <name|uid>[:<group|gid>])")
   542  	flags.StringVar(&opts.hostname, flagHostname, "", "Container hostname")
   543  
   544  	flags.Var(&opts.resources.limitCPU, flagLimitCPU, "Limit CPUs")
   545  	flags.Var(&opts.resources.limitMemBytes, flagLimitMemory, "Limit Memory")
   546  	flags.Var(&opts.resources.resCPU, flagReserveCPU, "Reserve CPUs")
   547  	flags.Var(&opts.resources.resMemBytes, flagReserveMemory, "Reserve Memory")
   548  	flags.Var(&opts.stopGrace, flagStopGracePeriod, "Time to wait before force killing a container (ns|us|ms|s|m|h)")
   549  
   550  	flags.Var(&opts.replicas, flagReplicas, "Number of tasks")
   551  
   552  	flags.StringVar(&opts.restartPolicy.condition, flagRestartCondition, "", "Restart when condition is met (none, on-failure, or any)")
   553  	flags.Var(&opts.restartPolicy.delay, flagRestartDelay, "Delay between restart attempts (ns|us|ms|s|m|h)")
   554  	flags.Var(&opts.restartPolicy.maxAttempts, flagRestartMaxAttempts, "Maximum number of restarts before giving up")
   555  	flags.Var(&opts.restartPolicy.window, flagRestartWindow, "Window used to evaluate the restart policy (ns|us|ms|s|m|h)")
   556  
   557  	flags.Uint64Var(&opts.update.parallelism, flagUpdateParallelism, 1, "Maximum number of tasks updated simultaneously (0 to update all at once)")
   558  	flags.DurationVar(&opts.update.delay, flagUpdateDelay, time.Duration(0), "Delay between updates (ns|us|ms|s|m|h) (default 0s)")
   559  	flags.DurationVar(&opts.update.monitor, flagUpdateMonitor, time.Duration(0), "Duration after each task update to monitor for failure (ns|us|ms|s|m|h) (default 0s)")
   560  	flags.StringVar(&opts.update.onFailure, flagUpdateFailureAction, "pause", "Action on update failure (pause|continue)")
   561  	flags.Var(&opts.update.maxFailureRatio, flagUpdateMaxFailureRatio, "Failure rate to tolerate during an update")
   562  
   563  	flags.StringVar(&opts.endpoint.mode, flagEndpointMode, "", "Endpoint mode (vip or dnsrr)")
   564  
   565  	flags.BoolVar(&opts.registryAuth, flagRegistryAuth, false, "Send registry authentication details to swarm agents")
   566  
   567  	flags.StringVar(&opts.logDriver.name, flagLogDriver, "", "Logging driver for service")
   568  	flags.Var(&opts.logDriver.opts, flagLogOpt, "Logging driver options")
   569  
   570  	flags.StringVar(&opts.healthcheck.cmd, flagHealthCmd, "", "Command to run to check health")
   571  	flags.Var(&opts.healthcheck.interval, flagHealthInterval, "Time between running the check (ns|us|ms|s|m|h)")
   572  	flags.Var(&opts.healthcheck.timeout, flagHealthTimeout, "Maximum time to allow one check to run (ns|us|ms|s|m|h)")
   573  	flags.IntVar(&opts.healthcheck.retries, flagHealthRetries, 0, "Consecutive failures needed to report unhealthy")
   574  	flags.BoolVar(&opts.healthcheck.noHealthcheck, flagNoHealthcheck, false, "Disable any container-specified HEALTHCHECK")
   575  
   576  	flags.BoolVarP(&opts.tty, flagTTY, "t", false, "Allocate a pseudo-TTY")
   577  }
   578  
   579  const (
   580  	flagConstraint            = "constraint"
   581  	flagConstraintRemove      = "constraint-rm"
   582  	flagConstraintAdd         = "constraint-add"
   583  	flagContainerLabel        = "container-label"
   584  	flagContainerLabelRemove  = "container-label-rm"
   585  	flagContainerLabelAdd     = "container-label-add"
   586  	flagDNS                   = "dns"
   587  	flagDNSRemove             = "dns-rm"
   588  	flagDNSAdd                = "dns-add"
   589  	flagDNSOption             = "dns-option"
   590  	flagDNSOptionRemove       = "dns-option-rm"
   591  	flagDNSOptionAdd          = "dns-option-add"
   592  	flagDNSSearch             = "dns-search"
   593  	flagDNSSearchRemove       = "dns-search-rm"
   594  	flagDNSSearchAdd          = "dns-search-add"
   595  	flagEndpointMode          = "endpoint-mode"
   596  	flagHost                  = "host"
   597  	flagHostAdd               = "host-add"
   598  	flagHostRemove            = "host-rm"
   599  	flagHostname              = "hostname"
   600  	flagEnv                   = "env"
   601  	flagEnvFile               = "env-file"
   602  	flagEnvRemove             = "env-rm"
   603  	flagEnvAdd                = "env-add"
   604  	flagGroup                 = "group"
   605  	flagGroupAdd              = "group-add"
   606  	flagGroupRemove           = "group-rm"
   607  	flagLabel                 = "label"
   608  	flagLabelRemove           = "label-rm"
   609  	flagLabelAdd              = "label-add"
   610  	flagLimitCPU              = "limit-cpu"
   611  	flagLimitMemory           = "limit-memory"
   612  	flagMode                  = "mode"
   613  	flagMount                 = "mount"
   614  	flagMountRemove           = "mount-rm"
   615  	flagMountAdd              = "mount-add"
   616  	flagName                  = "name"
   617  	flagNetwork               = "network"
   618  	flagPublish               = "publish"
   619  	flagPublishRemove         = "publish-rm"
   620  	flagPublishAdd            = "publish-add"
   621  	flagReplicas              = "replicas"
   622  	flagReserveCPU            = "reserve-cpu"
   623  	flagReserveMemory         = "reserve-memory"
   624  	flagRestartCondition      = "restart-condition"
   625  	flagRestartDelay          = "restart-delay"
   626  	flagRestartMaxAttempts    = "restart-max-attempts"
   627  	flagRestartWindow         = "restart-window"
   628  	flagStopGracePeriod       = "stop-grace-period"
   629  	flagTTY                   = "tty"
   630  	flagUpdateDelay           = "update-delay"
   631  	flagUpdateFailureAction   = "update-failure-action"
   632  	flagUpdateMaxFailureRatio = "update-max-failure-ratio"
   633  	flagUpdateMonitor         = "update-monitor"
   634  	flagUpdateParallelism     = "update-parallelism"
   635  	flagUser                  = "user"
   636  	flagWorkdir               = "workdir"
   637  	flagRegistryAuth          = "with-registry-auth"
   638  	flagLogDriver             = "log-driver"
   639  	flagLogOpt                = "log-opt"
   640  	flagHealthCmd             = "health-cmd"
   641  	flagHealthInterval        = "health-interval"
   642  	flagHealthRetries         = "health-retries"
   643  	flagHealthTimeout         = "health-timeout"
   644  	flagNoHealthcheck         = "no-healthcheck"
   645  	flagSecret                = "secret"
   646  	flagSecretAdd             = "secret-add"
   647  	flagSecretRemove          = "secret-rm"
   648  )