github.1git.de/docker/cli@v26.1.3+incompatible/cli/command/service/opts.go (about)

     1  // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
     2  //go:build go1.19
     3  
     4  package service
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"sort"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/docker/cli/cli/command"
    15  	"github.com/docker/cli/opts"
    16  	"github.com/docker/docker/api/types"
    17  	"github.com/docker/docker/api/types/container"
    18  	"github.com/docker/docker/api/types/swarm"
    19  	"github.com/docker/docker/client"
    20  	gogotypes "github.com/gogo/protobuf/types"
    21  	"github.com/google/shlex"
    22  	"github.com/moby/swarmkit/v2/api"
    23  	"github.com/moby/swarmkit/v2/api/defaults"
    24  	"github.com/pkg/errors"
    25  	"github.com/spf13/pflag"
    26  )
    27  
    28  type int64Value interface {
    29  	Value() int64
    30  }
    31  
    32  // Uint64Opt represents a uint64.
    33  type Uint64Opt struct {
    34  	value *uint64
    35  }
    36  
    37  // Set a new value on the option
    38  func (i *Uint64Opt) Set(s string) error {
    39  	v, err := strconv.ParseUint(s, 0, 64)
    40  	i.value = &v
    41  	return err
    42  }
    43  
    44  // Type returns the type of this option, which will be displayed in `--help` output
    45  func (i *Uint64Opt) Type() string {
    46  	return "uint"
    47  }
    48  
    49  // String returns a string repr of this option
    50  func (i *Uint64Opt) String() string {
    51  	if i.value != nil {
    52  		return strconv.FormatUint(*i.value, 10)
    53  	}
    54  	return ""
    55  }
    56  
    57  // Value returns the uint64
    58  func (i *Uint64Opt) Value() *uint64 {
    59  	return i.value
    60  }
    61  
    62  type floatValue float32
    63  
    64  func (f *floatValue) Set(s string) error {
    65  	v, err := strconv.ParseFloat(s, 32)
    66  	*f = floatValue(v)
    67  	return err
    68  }
    69  
    70  func (f *floatValue) Type() string {
    71  	return "float"
    72  }
    73  
    74  func (f *floatValue) String() string {
    75  	return strconv.FormatFloat(float64(*f), 'g', -1, 32)
    76  }
    77  
    78  func (f *floatValue) Value() float32 {
    79  	return float32(*f)
    80  }
    81  
    82  // placementPrefOpts holds a list of placement preferences.
    83  type placementPrefOpts struct {
    84  	prefs   []swarm.PlacementPreference
    85  	strings []string
    86  }
    87  
    88  func (o *placementPrefOpts) String() string {
    89  	if len(o.strings) == 0 {
    90  		return ""
    91  	}
    92  	return fmt.Sprintf("%v", o.strings)
    93  }
    94  
    95  // Set validates the input value and adds it to the internal slices.
    96  // Note: in the future strategies other than "spread", may be supported,
    97  // as well as additional comma-separated options.
    98  func (o *placementPrefOpts) Set(value string) error {
    99  	strategy, arg, ok := strings.Cut(value, "=")
   100  	if !ok || strategy == "" {
   101  		return errors.New(`placement preference must be of the format "<strategy>=<arg>"`)
   102  	}
   103  	if strategy != "spread" {
   104  		return errors.Errorf("unsupported placement preference %s (only spread is supported)", strategy)
   105  	}
   106  
   107  	o.prefs = append(o.prefs, swarm.PlacementPreference{
   108  		Spread: &swarm.SpreadOver{
   109  			SpreadDescriptor: arg,
   110  		},
   111  	})
   112  	o.strings = append(o.strings, value)
   113  	return nil
   114  }
   115  
   116  // Type returns a string name for this Option type
   117  func (o *placementPrefOpts) Type() string {
   118  	return "pref"
   119  }
   120  
   121  // ShlexOpt is a flag Value which parses a string as a list of shell words
   122  type ShlexOpt []string
   123  
   124  // Set the value
   125  func (s *ShlexOpt) Set(value string) error {
   126  	valueSlice, err := shlex.Split(value)
   127  	if err != nil {
   128  		return err
   129  	}
   130  	*s = valueSlice
   131  	return nil
   132  }
   133  
   134  // Type returns the tyep of the value
   135  func (s *ShlexOpt) Type() string {
   136  	return "command"
   137  }
   138  
   139  func (s *ShlexOpt) String() string {
   140  	if len(*s) == 0 {
   141  		return ""
   142  	}
   143  	return fmt.Sprint(*s)
   144  }
   145  
   146  // Value returns the value as a string slice
   147  func (s *ShlexOpt) Value() []string {
   148  	return []string(*s)
   149  }
   150  
   151  type updateOptions struct {
   152  	parallelism     uint64
   153  	delay           time.Duration
   154  	monitor         time.Duration
   155  	onFailure       string
   156  	maxFailureRatio floatValue
   157  	order           string
   158  }
   159  
   160  func updateConfigFromDefaults(defaultUpdateConfig *api.UpdateConfig) *swarm.UpdateConfig {
   161  	defaultFailureAction := strings.ToLower(api.UpdateConfig_FailureAction_name[int32(defaultUpdateConfig.FailureAction)])
   162  	defaultMonitor, _ := gogotypes.DurationFromProto(defaultUpdateConfig.Monitor)
   163  	return &swarm.UpdateConfig{
   164  		Parallelism:     defaultUpdateConfig.Parallelism,
   165  		Delay:           defaultUpdateConfig.Delay,
   166  		Monitor:         defaultMonitor,
   167  		FailureAction:   defaultFailureAction,
   168  		MaxFailureRatio: defaultUpdateConfig.MaxFailureRatio,
   169  		Order:           defaultOrder(defaultUpdateConfig.Order),
   170  	}
   171  }
   172  
   173  func (o updateOptions) updateConfig(flags *pflag.FlagSet) *swarm.UpdateConfig {
   174  	if !anyChanged(flags, flagUpdateParallelism, flagUpdateDelay, flagUpdateMonitor, flagUpdateFailureAction, flagUpdateMaxFailureRatio, flagUpdateOrder) {
   175  		return nil
   176  	}
   177  
   178  	updateConfig := updateConfigFromDefaults(defaults.Service.Update)
   179  
   180  	if flags.Changed(flagUpdateParallelism) {
   181  		updateConfig.Parallelism = o.parallelism
   182  	}
   183  	if flags.Changed(flagUpdateDelay) {
   184  		updateConfig.Delay = o.delay
   185  	}
   186  	if flags.Changed(flagUpdateMonitor) {
   187  		updateConfig.Monitor = o.monitor
   188  	}
   189  	if flags.Changed(flagUpdateFailureAction) {
   190  		updateConfig.FailureAction = o.onFailure
   191  	}
   192  	if flags.Changed(flagUpdateMaxFailureRatio) {
   193  		updateConfig.MaxFailureRatio = o.maxFailureRatio.Value()
   194  	}
   195  	if flags.Changed(flagUpdateOrder) {
   196  		updateConfig.Order = o.order
   197  	}
   198  
   199  	return updateConfig
   200  }
   201  
   202  func (o updateOptions) rollbackConfig(flags *pflag.FlagSet) *swarm.UpdateConfig {
   203  	if !anyChanged(flags, flagRollbackParallelism, flagRollbackDelay, flagRollbackMonitor, flagRollbackFailureAction, flagRollbackMaxFailureRatio, flagRollbackOrder) {
   204  		return nil
   205  	}
   206  
   207  	updateConfig := updateConfigFromDefaults(defaults.Service.Rollback)
   208  
   209  	if flags.Changed(flagRollbackParallelism) {
   210  		updateConfig.Parallelism = o.parallelism
   211  	}
   212  	if flags.Changed(flagRollbackDelay) {
   213  		updateConfig.Delay = o.delay
   214  	}
   215  	if flags.Changed(flagRollbackMonitor) {
   216  		updateConfig.Monitor = o.monitor
   217  	}
   218  	if flags.Changed(flagRollbackFailureAction) {
   219  		updateConfig.FailureAction = o.onFailure
   220  	}
   221  	if flags.Changed(flagRollbackMaxFailureRatio) {
   222  		updateConfig.MaxFailureRatio = o.maxFailureRatio.Value()
   223  	}
   224  	if flags.Changed(flagRollbackOrder) {
   225  		updateConfig.Order = o.order
   226  	}
   227  
   228  	return updateConfig
   229  }
   230  
   231  type resourceOptions struct {
   232  	limitCPU            opts.NanoCPUs
   233  	limitMemBytes       opts.MemBytes
   234  	limitPids           int64
   235  	resCPU              opts.NanoCPUs
   236  	resMemBytes         opts.MemBytes
   237  	resGenericResources []string
   238  }
   239  
   240  func (r *resourceOptions) ToResourceRequirements() (*swarm.ResourceRequirements, error) {
   241  	generic, err := ParseGenericResources(r.resGenericResources)
   242  	if err != nil {
   243  		return nil, err
   244  	}
   245  
   246  	return &swarm.ResourceRequirements{
   247  		Limits: &swarm.Limit{
   248  			NanoCPUs:    r.limitCPU.Value(),
   249  			MemoryBytes: r.limitMemBytes.Value(),
   250  			Pids:        r.limitPids,
   251  		},
   252  		Reservations: &swarm.Resources{
   253  			NanoCPUs:         r.resCPU.Value(),
   254  			MemoryBytes:      r.resMemBytes.Value(),
   255  			GenericResources: generic,
   256  		},
   257  	}, nil
   258  }
   259  
   260  type restartPolicyOptions struct {
   261  	condition   string
   262  	delay       opts.DurationOpt
   263  	maxAttempts Uint64Opt
   264  	window      opts.DurationOpt
   265  }
   266  
   267  func defaultRestartPolicy() *swarm.RestartPolicy {
   268  	defaultMaxAttempts := defaults.Service.Task.Restart.MaxAttempts
   269  	rp := &swarm.RestartPolicy{
   270  		MaxAttempts: &defaultMaxAttempts,
   271  	}
   272  
   273  	if defaults.Service.Task.Restart.Delay != nil {
   274  		defaultRestartDelay, _ := gogotypes.DurationFromProto(defaults.Service.Task.Restart.Delay)
   275  		rp.Delay = &defaultRestartDelay
   276  	}
   277  	if defaults.Service.Task.Restart.Window != nil {
   278  		defaultRestartWindow, _ := gogotypes.DurationFromProto(defaults.Service.Task.Restart.Window)
   279  		rp.Window = &defaultRestartWindow
   280  	}
   281  	rp.Condition = defaultRestartCondition()
   282  
   283  	return rp
   284  }
   285  
   286  func defaultRestartCondition() swarm.RestartPolicyCondition {
   287  	switch defaults.Service.Task.Restart.Condition {
   288  	case api.RestartOnNone:
   289  		return "none"
   290  	case api.RestartOnFailure:
   291  		return "on-failure"
   292  	case api.RestartOnAny:
   293  		return "any"
   294  	default:
   295  		return ""
   296  	}
   297  }
   298  
   299  func defaultOrder(order api.UpdateConfig_UpdateOrder) string {
   300  	switch order {
   301  	case api.UpdateConfig_STOP_FIRST:
   302  		return "stop-first"
   303  	case api.UpdateConfig_START_FIRST:
   304  		return "start-first"
   305  	default:
   306  		return ""
   307  	}
   308  }
   309  
   310  func (r *restartPolicyOptions) ToRestartPolicy(flags *pflag.FlagSet) *swarm.RestartPolicy {
   311  	if !anyChanged(flags, flagRestartDelay, flagRestartMaxAttempts, flagRestartWindow, flagRestartCondition) {
   312  		return nil
   313  	}
   314  
   315  	restartPolicy := defaultRestartPolicy()
   316  
   317  	if flags.Changed(flagRestartDelay) {
   318  		restartPolicy.Delay = r.delay.Value()
   319  	}
   320  	if flags.Changed(flagRestartCondition) {
   321  		restartPolicy.Condition = swarm.RestartPolicyCondition(r.condition)
   322  	}
   323  	if flags.Changed(flagRestartMaxAttempts) {
   324  		restartPolicy.MaxAttempts = r.maxAttempts.Value()
   325  	}
   326  	if flags.Changed(flagRestartWindow) {
   327  		restartPolicy.Window = r.window.Value()
   328  	}
   329  
   330  	return restartPolicy
   331  }
   332  
   333  type credentialSpecOpt struct {
   334  	value  *swarm.CredentialSpec
   335  	source string
   336  }
   337  
   338  func (c *credentialSpecOpt) Set(value string) error {
   339  	c.source = value
   340  	c.value = &swarm.CredentialSpec{}
   341  	switch {
   342  	case strings.HasPrefix(value, "config://"):
   343  		// NOTE(dperny): we allow the user to specify the value of
   344  		// CredentialSpec Config using the Name of the config, but the API
   345  		// requires the ID of the config. For simplicity, we will parse
   346  		// whatever value is provided into the "Config" field, but before
   347  		// making API calls, we may need to swap the Config Name for the ID.
   348  		// Therefore, this isn't the definitive location for the value of
   349  		// Config that is passed to the API.
   350  		c.value.Config = strings.TrimPrefix(value, "config://")
   351  	case strings.HasPrefix(value, "file://"):
   352  		c.value.File = strings.TrimPrefix(value, "file://")
   353  	case strings.HasPrefix(value, "registry://"):
   354  		c.value.Registry = strings.TrimPrefix(value, "registry://")
   355  	case value == "":
   356  		// if the value of the flag is an empty string, that means there is no
   357  		// CredentialSpec needed. This is useful for removing a CredentialSpec
   358  		// during a service update.
   359  	default:
   360  		return errors.New(`invalid credential spec: value must be prefixed with "config://", "file://", or "registry://"`)
   361  	}
   362  
   363  	return nil
   364  }
   365  
   366  func (c *credentialSpecOpt) Type() string {
   367  	return "credential-spec"
   368  }
   369  
   370  func (c *credentialSpecOpt) String() string {
   371  	return c.source
   372  }
   373  
   374  func (c *credentialSpecOpt) Value() *swarm.CredentialSpec {
   375  	return c.value
   376  }
   377  
   378  func resolveNetworkID(ctx context.Context, apiClient client.NetworkAPIClient, networkIDOrName string) (string, error) {
   379  	nw, err := apiClient.NetworkInspect(ctx, networkIDOrName, types.NetworkInspectOptions{Scope: "swarm"})
   380  	return nw.ID, err
   381  }
   382  
   383  func convertNetworks(networks opts.NetworkOpt) []swarm.NetworkAttachmentConfig {
   384  	nws := networks.Value()
   385  	netAttach := make([]swarm.NetworkAttachmentConfig, 0, len(nws))
   386  	for _, net := range nws {
   387  		netAttach = append(netAttach, swarm.NetworkAttachmentConfig{
   388  			Target:     net.Target,
   389  			Aliases:    net.Aliases,
   390  			DriverOpts: net.DriverOpts,
   391  		})
   392  	}
   393  	return netAttach
   394  }
   395  
   396  type endpointOptions struct {
   397  	mode         string
   398  	publishPorts opts.PortOpt
   399  }
   400  
   401  func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec {
   402  	return &swarm.EndpointSpec{
   403  		Mode:  swarm.ResolutionMode(strings.ToLower(e.mode)),
   404  		Ports: e.publishPorts.Value(),
   405  	}
   406  }
   407  
   408  type logDriverOptions struct {
   409  	name string
   410  	opts opts.ListOpts
   411  }
   412  
   413  func newLogDriverOptions() logDriverOptions {
   414  	return logDriverOptions{opts: opts.NewListOpts(opts.ValidateEnv)}
   415  }
   416  
   417  func (ldo *logDriverOptions) toLogDriver() *swarm.Driver {
   418  	if ldo.name == "" {
   419  		return nil
   420  	}
   421  
   422  	// set the log driver only if specified.
   423  	return &swarm.Driver{
   424  		Name:    ldo.name,
   425  		Options: opts.ConvertKVStringsToMap(ldo.opts.GetAll()),
   426  	}
   427  }
   428  
   429  type healthCheckOptions struct {
   430  	cmd           string
   431  	interval      opts.PositiveDurationOpt
   432  	timeout       opts.PositiveDurationOpt
   433  	retries       int
   434  	startPeriod   opts.PositiveDurationOpt
   435  	startInterval opts.PositiveDurationOpt
   436  	noHealthcheck bool
   437  }
   438  
   439  func (o *healthCheckOptions) toHealthConfig() (*container.HealthConfig, error) {
   440  	var healthConfig *container.HealthConfig
   441  	haveHealthSettings := o.cmd != "" ||
   442  		o.interval.Value() != nil ||
   443  		o.timeout.Value() != nil ||
   444  		o.startPeriod.Value() != nil ||
   445  		o.startInterval.Value() != nil ||
   446  		o.retries != 0
   447  	if o.noHealthcheck {
   448  		if haveHealthSettings {
   449  			return nil, errors.Errorf("--%s conflicts with --health-* options", flagNoHealthcheck)
   450  		}
   451  		healthConfig = &container.HealthConfig{Test: []string{"NONE"}}
   452  	} else if haveHealthSettings {
   453  		var test []string
   454  		if o.cmd != "" {
   455  			test = []string{"CMD-SHELL", o.cmd}
   456  		}
   457  		var interval, timeout, startPeriod, startInterval time.Duration
   458  		if ptr := o.interval.Value(); ptr != nil {
   459  			interval = *ptr
   460  		}
   461  		if ptr := o.timeout.Value(); ptr != nil {
   462  			timeout = *ptr
   463  		}
   464  		if ptr := o.startPeriod.Value(); ptr != nil {
   465  			startPeriod = *ptr
   466  		}
   467  		if ptr := o.startInterval.Value(); ptr != nil {
   468  			startInterval = *ptr
   469  		}
   470  		healthConfig = &container.HealthConfig{
   471  			Test:          test,
   472  			Interval:      interval,
   473  			Timeout:       timeout,
   474  			Retries:       o.retries,
   475  			StartPeriod:   startPeriod,
   476  			StartInterval: startInterval,
   477  		}
   478  	}
   479  	return healthConfig, nil
   480  }
   481  
   482  // convertExtraHostsToSwarmHosts converts an array of extra hosts in cli
   483  //
   484  //	<host>:<ip>
   485  //
   486  // into a swarmkit host format:
   487  //
   488  //	IP_address canonical_hostname [aliases...]
   489  //
   490  // This assumes input value (<host>:<ip>) has already been validated
   491  func convertExtraHostsToSwarmHosts(extraHosts []string) []string {
   492  	hosts := make([]string, 0, len(extraHosts))
   493  	for _, extraHost := range extraHosts {
   494  		host, ip, ok := strings.Cut(extraHost, ":")
   495  		if ok {
   496  			hosts = append(hosts, ip+" "+host)
   497  		}
   498  	}
   499  	return hosts
   500  }
   501  
   502  type serviceOptions struct {
   503  	detach bool
   504  	quiet  bool
   505  
   506  	name            string
   507  	labels          opts.ListOpts
   508  	containerLabels opts.ListOpts
   509  	image           string
   510  	entrypoint      ShlexOpt
   511  	args            []string
   512  	hostname        string
   513  	env             opts.ListOpts
   514  	envFile         opts.ListOpts
   515  	workdir         string
   516  	user            string
   517  	groups          opts.ListOpts
   518  	credentialSpec  credentialSpecOpt
   519  	init            bool
   520  	stopSignal      string
   521  	tty             bool
   522  	readOnly        bool
   523  	mounts          opts.MountOpt
   524  	dns             opts.ListOpts
   525  	dnsSearch       opts.ListOpts
   526  	dnsOption       opts.ListOpts
   527  	hosts           opts.ListOpts
   528  	sysctls         opts.ListOpts
   529  	capAdd          opts.ListOpts
   530  	capDrop         opts.ListOpts
   531  	ulimits         opts.UlimitOpt
   532  
   533  	resources resourceOptions
   534  	stopGrace opts.DurationOpt
   535  
   536  	replicas      Uint64Opt
   537  	mode          string
   538  	maxConcurrent Uint64Opt
   539  
   540  	restartPolicy  restartPolicyOptions
   541  	constraints    opts.ListOpts
   542  	placementPrefs placementPrefOpts
   543  	maxReplicas    uint64
   544  	update         updateOptions
   545  	rollback       updateOptions
   546  	networks       opts.NetworkOpt
   547  	endpoint       endpointOptions
   548  
   549  	registryAuth   bool
   550  	noResolveImage bool
   551  
   552  	logDriver logDriverOptions
   553  
   554  	healthcheck healthCheckOptions
   555  	secrets     opts.SecretOpt
   556  	configs     opts.ConfigOpt
   557  
   558  	isolation string
   559  }
   560  
   561  func newServiceOptions() *serviceOptions {
   562  	return &serviceOptions{
   563  		labels:          opts.NewListOpts(opts.ValidateLabel),
   564  		constraints:     opts.NewListOpts(nil),
   565  		containerLabels: opts.NewListOpts(opts.ValidateLabel),
   566  		env:             opts.NewListOpts(opts.ValidateEnv),
   567  		envFile:         opts.NewListOpts(nil),
   568  		groups:          opts.NewListOpts(nil),
   569  		logDriver:       newLogDriverOptions(),
   570  		dns:             opts.NewListOpts(opts.ValidateIPAddress),
   571  		dnsOption:       opts.NewListOpts(nil),
   572  		dnsSearch:       opts.NewListOpts(opts.ValidateDNSSearch),
   573  		hosts:           opts.NewListOpts(opts.ValidateExtraHost),
   574  		sysctls:         opts.NewListOpts(nil),
   575  		capAdd:          opts.NewListOpts(nil),
   576  		capDrop:         opts.NewListOpts(nil),
   577  		ulimits:         *opts.NewUlimitOpt(nil),
   578  	}
   579  }
   580  
   581  func (options *serviceOptions) ToServiceMode() (swarm.ServiceMode, error) {
   582  	serviceMode := swarm.ServiceMode{}
   583  	switch options.mode {
   584  	case "global":
   585  		if options.replicas.Value() != nil {
   586  			return serviceMode, errors.Errorf("replicas can only be used with replicated or replicated-job mode")
   587  		}
   588  
   589  		if options.maxReplicas > 0 {
   590  			return serviceMode, errors.New("replicas-max-per-node can only be used with replicated or replicated-job mode")
   591  		}
   592  		if options.maxConcurrent.Value() != nil {
   593  			return serviceMode, errors.New("max-concurrent can only be used with replicated-job mode")
   594  		}
   595  
   596  		serviceMode.Global = &swarm.GlobalService{}
   597  	case "replicated":
   598  		if options.maxConcurrent.Value() != nil {
   599  			return serviceMode, errors.New("max-concurrent can only be used with replicated-job mode")
   600  		}
   601  
   602  		serviceMode.Replicated = &swarm.ReplicatedService{
   603  			Replicas: options.replicas.Value(),
   604  		}
   605  	case "replicated-job":
   606  		concurrent := options.maxConcurrent.Value()
   607  		if concurrent == nil {
   608  			concurrent = options.replicas.Value()
   609  		}
   610  		serviceMode.ReplicatedJob = &swarm.ReplicatedJob{
   611  			MaxConcurrent:    concurrent,
   612  			TotalCompletions: options.replicas.Value(),
   613  		}
   614  	case "global-job":
   615  		if options.maxReplicas > 0 {
   616  			return serviceMode, errors.New("replicas-max-per-node can only be used with replicated or replicated-job mode")
   617  		}
   618  		if options.maxConcurrent.Value() != nil {
   619  			return serviceMode, errors.New("max-concurrent can only be used with replicated-job mode")
   620  		}
   621  		if options.replicas.Value() != nil {
   622  			return serviceMode, errors.Errorf("replicas can only be used with replicated or replicated-job mode")
   623  		}
   624  		serviceMode.GlobalJob = &swarm.GlobalJob{}
   625  	default:
   626  		return serviceMode, errors.Errorf("Unknown mode: %s, only replicated and global supported", options.mode)
   627  	}
   628  	return serviceMode, nil
   629  }
   630  
   631  func (options *serviceOptions) ToStopGracePeriod(flags *pflag.FlagSet) *time.Duration {
   632  	if flags.Changed(flagStopGracePeriod) {
   633  		return options.stopGrace.Value()
   634  	}
   635  	return nil
   636  }
   637  
   638  // makeEnv gets the environment variables from the command line options and
   639  // returns a slice of strings to use in the service spec when doing ToService
   640  func (options *serviceOptions) makeEnv() ([]string, error) {
   641  	envVariables, err := opts.ReadKVEnvStrings(options.envFile.GetAll(), options.env.GetAll())
   642  	if err != nil {
   643  		return nil, err
   644  	}
   645  	currentEnv := make([]string, 0, len(envVariables))
   646  	for _, env := range envVariables { // need to process each var, in order
   647  		k, _, _ := strings.Cut(env, "=")
   648  		for i, current := range currentEnv { // remove duplicates
   649  			if current == env {
   650  				continue // no update required, may hide this behind flag to preserve order of envVariables
   651  			}
   652  			if strings.HasPrefix(current, k+"=") {
   653  				currentEnv = append(currentEnv[:i], currentEnv[i+1:]...)
   654  			}
   655  		}
   656  		currentEnv = append(currentEnv, env)
   657  	}
   658  
   659  	return currentEnv, nil
   660  }
   661  
   662  // ToService takes the set of flags passed to the command and converts them
   663  // into a service spec.
   664  //
   665  // Takes an API client as the second argument in order to resolve network names
   666  // from the flags into network IDs.
   667  //
   668  // Returns an error if any flags are invalid or contradictory.
   669  func (options *serviceOptions) ToService(ctx context.Context, apiClient client.NetworkAPIClient, flags *pflag.FlagSet) (swarm.ServiceSpec, error) {
   670  	var service swarm.ServiceSpec
   671  
   672  	currentEnv, err := options.makeEnv()
   673  	if err != nil {
   674  		return service, err
   675  	}
   676  
   677  	healthConfig, err := options.healthcheck.toHealthConfig()
   678  	if err != nil {
   679  		return service, err
   680  	}
   681  
   682  	serviceMode, err := options.ToServiceMode()
   683  	if err != nil {
   684  		return service, err
   685  	}
   686  
   687  	updateConfig := options.update.updateConfig(flags)
   688  	rollbackConfig := options.rollback.rollbackConfig(flags)
   689  
   690  	// update and rollback configuration is not supported for jobs. If these
   691  	// flags are not set, then the values will be nil. If they are non-nil,
   692  	// then return an error.
   693  	if (serviceMode.ReplicatedJob != nil || serviceMode.GlobalJob != nil) && (updateConfig != nil || rollbackConfig != nil) {
   694  		return service, errors.Errorf("update and rollback configuration is not supported for jobs")
   695  	}
   696  
   697  	networks := convertNetworks(options.networks)
   698  	for i, net := range networks {
   699  		nwID, err := resolveNetworkID(ctx, apiClient, net.Target)
   700  		if err != nil {
   701  			return service, err
   702  		}
   703  		networks[i].Target = nwID
   704  	}
   705  	sort.Slice(networks, func(i, j int) bool {
   706  		return networks[i].Target < networks[j].Target
   707  	})
   708  
   709  	resources, err := options.resources.ToResourceRequirements()
   710  	if err != nil {
   711  		return service, err
   712  	}
   713  
   714  	capAdd, capDrop := opts.EffectiveCapAddCapDrop(options.capAdd.GetAll(), options.capDrop.GetAll())
   715  
   716  	service = swarm.ServiceSpec{
   717  		Annotations: swarm.Annotations{
   718  			Name:   options.name,
   719  			Labels: opts.ConvertKVStringsToMap(options.labels.GetAll()),
   720  		},
   721  		TaskTemplate: swarm.TaskSpec{
   722  			ContainerSpec: &swarm.ContainerSpec{
   723  				Image:      options.image,
   724  				Args:       options.args,
   725  				Command:    options.entrypoint.Value(),
   726  				Env:        currentEnv,
   727  				Hostname:   options.hostname,
   728  				Labels:     opts.ConvertKVStringsToMap(options.containerLabels.GetAll()),
   729  				Dir:        options.workdir,
   730  				User:       options.user,
   731  				Groups:     options.groups.GetAll(),
   732  				StopSignal: options.stopSignal,
   733  				TTY:        options.tty,
   734  				ReadOnly:   options.readOnly,
   735  				Mounts:     options.mounts.Value(),
   736  				Init:       &options.init,
   737  				DNSConfig: &swarm.DNSConfig{
   738  					Nameservers: options.dns.GetAll(),
   739  					Search:      options.dnsSearch.GetAll(),
   740  					Options:     options.dnsOption.GetAll(),
   741  				},
   742  				Hosts:           convertExtraHostsToSwarmHosts(options.hosts.GetAll()),
   743  				StopGracePeriod: options.ToStopGracePeriod(flags),
   744  				Healthcheck:     healthConfig,
   745  				Isolation:       container.Isolation(options.isolation),
   746  				Sysctls:         opts.ConvertKVStringsToMap(options.sysctls.GetAll()),
   747  				CapabilityAdd:   capAdd,
   748  				CapabilityDrop:  capDrop,
   749  				Ulimits:         options.ulimits.GetList(),
   750  			},
   751  			Networks:      networks,
   752  			Resources:     resources,
   753  			RestartPolicy: options.restartPolicy.ToRestartPolicy(flags),
   754  			Placement: &swarm.Placement{
   755  				Constraints: options.constraints.GetAll(),
   756  				Preferences: options.placementPrefs.prefs,
   757  				MaxReplicas: options.maxReplicas,
   758  			},
   759  			LogDriver: options.logDriver.toLogDriver(),
   760  		},
   761  		Mode:           serviceMode,
   762  		UpdateConfig:   updateConfig,
   763  		RollbackConfig: rollbackConfig,
   764  		EndpointSpec:   options.endpoint.ToEndpointSpec(),
   765  	}
   766  
   767  	if options.credentialSpec.String() != "" && options.credentialSpec.Value() != nil {
   768  		service.TaskTemplate.ContainerSpec.Privileges = &swarm.Privileges{
   769  			CredentialSpec: options.credentialSpec.Value(),
   770  		}
   771  	}
   772  
   773  	return service, nil
   774  }
   775  
   776  type flagDefaults map[string]any
   777  
   778  func (fd flagDefaults) getUint64(flagName string) uint64 {
   779  	if val, ok := fd[flagName].(uint64); ok {
   780  		return val
   781  	}
   782  	return 0
   783  }
   784  
   785  func (fd flagDefaults) getString(flagName string) string {
   786  	if val, ok := fd[flagName].(string); ok {
   787  		return val
   788  	}
   789  	return ""
   790  }
   791  
   792  func buildServiceDefaultFlagMapping() flagDefaults {
   793  	defaultFlagValues := make(map[string]any)
   794  
   795  	defaultFlagValues[flagStopGracePeriod], _ = gogotypes.DurationFromProto(defaults.Service.Task.GetContainer().StopGracePeriod)
   796  	defaultFlagValues[flagRestartCondition] = `"` + defaultRestartCondition() + `"`
   797  	defaultFlagValues[flagRestartDelay], _ = gogotypes.DurationFromProto(defaults.Service.Task.Restart.Delay)
   798  
   799  	if defaults.Service.Task.Restart.MaxAttempts != 0 {
   800  		defaultFlagValues[flagRestartMaxAttempts] = defaults.Service.Task.Restart.MaxAttempts
   801  	}
   802  
   803  	defaultRestartWindow, _ := gogotypes.DurationFromProto(defaults.Service.Task.Restart.Window)
   804  	if defaultRestartWindow != 0 {
   805  		defaultFlagValues[flagRestartWindow] = defaultRestartWindow
   806  	}
   807  
   808  	defaultFlagValues[flagUpdateParallelism] = defaults.Service.Update.Parallelism
   809  	defaultFlagValues[flagUpdateDelay] = defaults.Service.Update.Delay
   810  	defaultFlagValues[flagUpdateMonitor], _ = gogotypes.DurationFromProto(defaults.Service.Update.Monitor)
   811  	defaultFlagValues[flagUpdateFailureAction] = `"` + strings.ToLower(api.UpdateConfig_FailureAction_name[int32(defaults.Service.Update.FailureAction)]) + `"`
   812  	defaultFlagValues[flagUpdateMaxFailureRatio] = defaults.Service.Update.MaxFailureRatio
   813  	defaultFlagValues[flagUpdateOrder] = `"` + defaultOrder(defaults.Service.Update.Order) + `"`
   814  
   815  	defaultFlagValues[flagRollbackParallelism] = defaults.Service.Rollback.Parallelism
   816  	defaultFlagValues[flagRollbackDelay] = defaults.Service.Rollback.Delay
   817  	defaultFlagValues[flagRollbackMonitor], _ = gogotypes.DurationFromProto(defaults.Service.Rollback.Monitor)
   818  	defaultFlagValues[flagRollbackFailureAction] = `"` + strings.ToLower(api.UpdateConfig_FailureAction_name[int32(defaults.Service.Rollback.FailureAction)]) + `"`
   819  	defaultFlagValues[flagRollbackMaxFailureRatio] = defaults.Service.Rollback.MaxFailureRatio
   820  	defaultFlagValues[flagRollbackOrder] = `"` + defaultOrder(defaults.Service.Rollback.Order) + `"`
   821  
   822  	defaultFlagValues[flagEndpointMode] = "vip"
   823  
   824  	return defaultFlagValues
   825  }
   826  
   827  func addDetachFlag(flags *pflag.FlagSet, detach *bool) {
   828  	flags.BoolVarP(detach, flagDetach, "d", false, "Exit immediately instead of waiting for the service to converge")
   829  	flags.SetAnnotation(flagDetach, "version", []string{"1.29"})
   830  }
   831  
   832  // addServiceFlags adds all flags that are common to both `create` and `update`.
   833  // Any flags that are not common are added separately in the individual command
   834  func addServiceFlags(flags *pflag.FlagSet, options *serviceOptions, defaultFlagValues flagDefaults) {
   835  	flagDesc := func(flagName string, desc string) string {
   836  		if defaultValue, ok := defaultFlagValues[flagName]; ok {
   837  			return fmt.Sprintf("%s (default %v)", desc, defaultValue)
   838  		}
   839  		return desc
   840  	}
   841  
   842  	addDetachFlag(flags, &options.detach)
   843  	flags.BoolVarP(&options.quiet, flagQuiet, "q", false, "Suppress progress output")
   844  
   845  	flags.StringVarP(&options.workdir, flagWorkdir, "w", "", "Working directory inside the container")
   846  	flags.StringVarP(&options.user, flagUser, "u", "", "Username or UID (format: <name|uid>[:<group|gid>])")
   847  	flags.Var(&options.credentialSpec, flagCredentialSpec, "Credential spec for managed service account (Windows only)")
   848  	flags.SetAnnotation(flagCredentialSpec, "version", []string{"1.29"})
   849  	flags.StringVar(&options.hostname, flagHostname, "", "Container hostname")
   850  	flags.SetAnnotation(flagHostname, "version", []string{"1.25"})
   851  	flags.Var(&options.entrypoint, flagEntrypoint, "Overwrite the default ENTRYPOINT of the image")
   852  	flags.Var(&options.capAdd, flagCapAdd, "Add Linux capabilities")
   853  	flags.SetAnnotation(flagCapAdd, "version", []string{"1.41"})
   854  	flags.Var(&options.capDrop, flagCapDrop, "Drop Linux capabilities")
   855  	flags.SetAnnotation(flagCapDrop, "version", []string{"1.41"})
   856  
   857  	flags.Var(&options.resources.limitCPU, flagLimitCPU, "Limit CPUs")
   858  	flags.Var(&options.resources.limitMemBytes, flagLimitMemory, "Limit Memory")
   859  	flags.Var(&options.resources.resCPU, flagReserveCPU, "Reserve CPUs")
   860  	flags.Var(&options.resources.resMemBytes, flagReserveMemory, "Reserve Memory")
   861  	flags.Int64Var(&options.resources.limitPids, flagLimitPids, 0, "Limit maximum number of processes (default 0 = unlimited)")
   862  	flags.SetAnnotation(flagLimitPids, "version", []string{"1.41"})
   863  
   864  	flags.Var(&options.stopGrace, flagStopGracePeriod, flagDesc(flagStopGracePeriod, "Time to wait before force killing a container (ns|us|ms|s|m|h)"))
   865  	flags.Var(&options.replicas, flagReplicas, "Number of tasks")
   866  	flags.Var(&options.maxConcurrent, flagConcurrent, "Number of job tasks to run concurrently (default equal to --replicas)")
   867  	flags.SetAnnotation(flagConcurrent, "version", []string{"1.41"})
   868  	flags.Uint64Var(&options.maxReplicas, flagMaxReplicas, defaultFlagValues.getUint64(flagMaxReplicas), "Maximum number of tasks per node (default 0 = unlimited)")
   869  	flags.SetAnnotation(flagMaxReplicas, "version", []string{"1.40"})
   870  
   871  	flags.StringVar(&options.restartPolicy.condition, flagRestartCondition, "", flagDesc(flagRestartCondition, `Restart when condition is met ("none", "on-failure", "any")`))
   872  	flags.Var(&options.restartPolicy.delay, flagRestartDelay, flagDesc(flagRestartDelay, "Delay between restart attempts (ns|us|ms|s|m|h)"))
   873  	flags.Var(&options.restartPolicy.maxAttempts, flagRestartMaxAttempts, flagDesc(flagRestartMaxAttempts, "Maximum number of restarts before giving up"))
   874  
   875  	flags.Var(&options.restartPolicy.window, flagRestartWindow, flagDesc(flagRestartWindow, "Window used to evaluate the restart policy (ns|us|ms|s|m|h)"))
   876  
   877  	flags.Uint64Var(&options.update.parallelism, flagUpdateParallelism, defaultFlagValues.getUint64(flagUpdateParallelism), "Maximum number of tasks updated simultaneously (0 to update all at once)")
   878  	flags.DurationVar(&options.update.delay, flagUpdateDelay, 0, flagDesc(flagUpdateDelay, "Delay between updates (ns|us|ms|s|m|h)"))
   879  	flags.DurationVar(&options.update.monitor, flagUpdateMonitor, 0, flagDesc(flagUpdateMonitor, "Duration after each task update to monitor for failure (ns|us|ms|s|m|h)"))
   880  	flags.SetAnnotation(flagUpdateMonitor, "version", []string{"1.25"})
   881  	flags.StringVar(&options.update.onFailure, flagUpdateFailureAction, "", flagDesc(flagUpdateFailureAction, `Action on update failure ("pause", "continue", "rollback")`))
   882  	flags.Var(&options.update.maxFailureRatio, flagUpdateMaxFailureRatio, flagDesc(flagUpdateMaxFailureRatio, "Failure rate to tolerate during an update"))
   883  	flags.SetAnnotation(flagUpdateMaxFailureRatio, "version", []string{"1.25"})
   884  	flags.StringVar(&options.update.order, flagUpdateOrder, "", flagDesc(flagUpdateOrder, `Update order ("start-first", "stop-first")`))
   885  	flags.SetAnnotation(flagUpdateOrder, "version", []string{"1.29"})
   886  
   887  	flags.Uint64Var(&options.rollback.parallelism, flagRollbackParallelism, defaultFlagValues.getUint64(flagRollbackParallelism),
   888  		"Maximum number of tasks rolled back simultaneously (0 to roll back all at once)")
   889  	flags.SetAnnotation(flagRollbackParallelism, "version", []string{"1.28"})
   890  	flags.DurationVar(&options.rollback.delay, flagRollbackDelay, 0, flagDesc(flagRollbackDelay, "Delay between task rollbacks (ns|us|ms|s|m|h)"))
   891  	flags.SetAnnotation(flagRollbackDelay, "version", []string{"1.28"})
   892  	flags.DurationVar(&options.rollback.monitor, flagRollbackMonitor, 0, flagDesc(flagRollbackMonitor, "Duration after each task rollback to monitor for failure (ns|us|ms|s|m|h)"))
   893  	flags.SetAnnotation(flagRollbackMonitor, "version", []string{"1.28"})
   894  	flags.StringVar(&options.rollback.onFailure, flagRollbackFailureAction, "", flagDesc(flagRollbackFailureAction, `Action on rollback failure ("pause", "continue")`))
   895  	flags.SetAnnotation(flagRollbackFailureAction, "version", []string{"1.28"})
   896  	flags.Var(&options.rollback.maxFailureRatio, flagRollbackMaxFailureRatio, flagDesc(flagRollbackMaxFailureRatio, "Failure rate to tolerate during a rollback"))
   897  	flags.SetAnnotation(flagRollbackMaxFailureRatio, "version", []string{"1.28"})
   898  	flags.StringVar(&options.rollback.order, flagRollbackOrder, "", flagDesc(flagRollbackOrder, `Rollback order ("start-first", "stop-first")`))
   899  	flags.SetAnnotation(flagRollbackOrder, "version", []string{"1.29"})
   900  
   901  	flags.StringVar(&options.endpoint.mode, flagEndpointMode, defaultFlagValues.getString(flagEndpointMode), "Endpoint mode (vip or dnsrr)")
   902  
   903  	flags.BoolVar(&options.registryAuth, flagRegistryAuth, false, "Send registry authentication details to swarm agents")
   904  	flags.BoolVar(&options.noResolveImage, flagNoResolveImage, false, "Do not query the registry to resolve image digest and supported platforms")
   905  	flags.SetAnnotation(flagNoResolveImage, "version", []string{"1.30"})
   906  
   907  	flags.StringVar(&options.logDriver.name, flagLogDriver, "", "Logging driver for service")
   908  	flags.Var(&options.logDriver.opts, flagLogOpt, "Logging driver options")
   909  
   910  	flags.StringVar(&options.healthcheck.cmd, flagHealthCmd, "", "Command to run to check health")
   911  	flags.SetAnnotation(flagHealthCmd, "version", []string{"1.25"})
   912  	flags.Var(&options.healthcheck.interval, flagHealthInterval, "Time between running the check (ms|s|m|h)")
   913  	flags.SetAnnotation(flagHealthInterval, "version", []string{"1.25"})
   914  	flags.Var(&options.healthcheck.timeout, flagHealthTimeout, "Maximum time to allow one check to run (ms|s|m|h)")
   915  	flags.SetAnnotation(flagHealthTimeout, "version", []string{"1.25"})
   916  	flags.IntVar(&options.healthcheck.retries, flagHealthRetries, 0, "Consecutive failures needed to report unhealthy")
   917  	flags.SetAnnotation(flagHealthRetries, "version", []string{"1.25"})
   918  	flags.Var(&options.healthcheck.startPeriod, flagHealthStartPeriod, "Start period for the container to initialize before counting retries towards unstable (ms|s|m|h)")
   919  	flags.SetAnnotation(flagHealthStartPeriod, "version", []string{"1.29"})
   920  	flags.Var(&options.healthcheck.startInterval, flagHealthStartInterval, "Time between running the check during the start period (ms|s|m|h)")
   921  	flags.SetAnnotation(flagHealthStartInterval, "version", []string{"1.44"})
   922  	flags.BoolVar(&options.healthcheck.noHealthcheck, flagNoHealthcheck, false, "Disable any container-specified HEALTHCHECK")
   923  	flags.SetAnnotation(flagNoHealthcheck, "version", []string{"1.25"})
   924  
   925  	flags.BoolVarP(&options.tty, flagTTY, "t", false, "Allocate a pseudo-TTY")
   926  	flags.SetAnnotation(flagTTY, "version", []string{"1.25"})
   927  
   928  	flags.BoolVar(&options.readOnly, flagReadOnly, false, "Mount the container's root filesystem as read only")
   929  	flags.SetAnnotation(flagReadOnly, "version", []string{"1.28"})
   930  
   931  	flags.StringVar(&options.stopSignal, flagStopSignal, "", "Signal to stop the container")
   932  	flags.SetAnnotation(flagStopSignal, "version", []string{"1.28"})
   933  	flags.StringVar(&options.isolation, flagIsolation, "", "Service container isolation mode")
   934  	flags.SetAnnotation(flagIsolation, "version", []string{"1.35"})
   935  }
   936  
   937  const (
   938  	flagCredentialSpec          = "credential-spec" //nolint:gosec // ignore G101: Potential hardcoded credentials
   939  	flagPlacementPref           = "placement-pref"
   940  	flagPlacementPrefAdd        = "placement-pref-add"
   941  	flagPlacementPrefRemove     = "placement-pref-rm"
   942  	flagConstraint              = "constraint"
   943  	flagConstraintRemove        = "constraint-rm"
   944  	flagConstraintAdd           = "constraint-add"
   945  	flagContainerLabel          = "container-label"
   946  	flagContainerLabelRemove    = "container-label-rm"
   947  	flagContainerLabelAdd       = "container-label-add"
   948  	flagDetach                  = "detach"
   949  	flagDNS                     = "dns"
   950  	flagDNSRemove               = "dns-rm"
   951  	flagDNSAdd                  = "dns-add"
   952  	flagDNSOption               = "dns-option"
   953  	flagDNSOptionRemove         = "dns-option-rm"
   954  	flagDNSOptionAdd            = "dns-option-add"
   955  	flagDNSSearch               = "dns-search"
   956  	flagDNSSearchRemove         = "dns-search-rm"
   957  	flagDNSSearchAdd            = "dns-search-add"
   958  	flagEndpointMode            = "endpoint-mode"
   959  	flagEntrypoint              = "entrypoint"
   960  	flagEnv                     = "env"
   961  	flagEnvFile                 = "env-file"
   962  	flagEnvRemove               = "env-rm"
   963  	flagEnvAdd                  = "env-add"
   964  	flagGenericResourcesRemove  = "generic-resource-rm"
   965  	flagGenericResourcesAdd     = "generic-resource-add"
   966  	flagGroup                   = "group"
   967  	flagGroupAdd                = "group-add"
   968  	flagGroupRemove             = "group-rm"
   969  	flagHost                    = "host"
   970  	flagHostAdd                 = "host-add"
   971  	flagHostRemove              = "host-rm"
   972  	flagHostname                = "hostname"
   973  	flagLabel                   = "label"
   974  	flagLabelRemove             = "label-rm"
   975  	flagLabelAdd                = "label-add"
   976  	flagLimitCPU                = "limit-cpu"
   977  	flagLimitMemory             = "limit-memory"
   978  	flagLimitPids               = "limit-pids"
   979  	flagMaxReplicas             = "replicas-max-per-node"
   980  	flagConcurrent              = "max-concurrent"
   981  	flagMode                    = "mode"
   982  	flagMount                   = "mount"
   983  	flagMountRemove             = "mount-rm"
   984  	flagMountAdd                = "mount-add"
   985  	flagName                    = "name"
   986  	flagNetwork                 = "network"
   987  	flagNetworkAdd              = "network-add"
   988  	flagNetworkRemove           = "network-rm"
   989  	flagPublish                 = "publish"
   990  	flagPublishRemove           = "publish-rm"
   991  	flagPublishAdd              = "publish-add"
   992  	flagQuiet                   = "quiet"
   993  	flagReadOnly                = "read-only"
   994  	flagReplicas                = "replicas"
   995  	flagReserveCPU              = "reserve-cpu"
   996  	flagReserveMemory           = "reserve-memory"
   997  	flagRestartCondition        = "restart-condition"
   998  	flagRestartDelay            = "restart-delay"
   999  	flagRestartMaxAttempts      = "restart-max-attempts"
  1000  	flagRestartWindow           = "restart-window"
  1001  	flagRollback                = "rollback"
  1002  	flagRollbackDelay           = "rollback-delay"
  1003  	flagRollbackFailureAction   = "rollback-failure-action"
  1004  	flagRollbackMaxFailureRatio = "rollback-max-failure-ratio"
  1005  	flagRollbackMonitor         = "rollback-monitor"
  1006  	flagRollbackOrder           = "rollback-order"
  1007  	flagRollbackParallelism     = "rollback-parallelism"
  1008  	flagInit                    = "init"
  1009  	flagSysCtl                  = "sysctl"
  1010  	flagSysCtlAdd               = "sysctl-add"
  1011  	flagSysCtlRemove            = "sysctl-rm"
  1012  	flagStopGracePeriod         = "stop-grace-period"
  1013  	flagStopSignal              = "stop-signal"
  1014  	flagTTY                     = "tty"
  1015  	flagUpdateDelay             = "update-delay"
  1016  	flagUpdateFailureAction     = "update-failure-action"
  1017  	flagUpdateMaxFailureRatio   = "update-max-failure-ratio" // #nosec G101 -- ignoring: Potential hardcoded credentials (gosec)
  1018  	flagUpdateMonitor           = "update-monitor"
  1019  	flagUpdateOrder             = "update-order"
  1020  	flagUpdateParallelism       = "update-parallelism"
  1021  	flagUser                    = "user"
  1022  	flagWorkdir                 = "workdir"
  1023  	flagRegistryAuth            = "with-registry-auth"
  1024  	flagNoResolveImage          = "no-resolve-image"
  1025  	flagLogDriver               = "log-driver"
  1026  	flagLogOpt                  = "log-opt"
  1027  	flagHealthCmd               = "health-cmd"
  1028  	flagHealthInterval          = "health-interval"
  1029  	flagHealthRetries           = "health-retries"
  1030  	flagHealthTimeout           = "health-timeout"
  1031  	flagHealthStartPeriod       = "health-start-period"
  1032  	flagHealthStartInterval     = "health-start-interval"
  1033  	flagNoHealthcheck           = "no-healthcheck"
  1034  	flagSecret                  = "secret"
  1035  	flagSecretAdd               = "secret-add"
  1036  	flagSecretRemove            = "secret-rm"
  1037  	flagConfig                  = "config"
  1038  	flagConfigAdd               = "config-add"
  1039  	flagConfigRemove            = "config-rm"
  1040  	flagIsolation               = "isolation"
  1041  	flagCapAdd                  = "cap-add"
  1042  	flagCapDrop                 = "cap-drop"
  1043  	flagUlimit                  = "ulimit"
  1044  	flagUlimitAdd               = "ulimit-add"
  1045  	flagUlimitRemove            = "ulimit-rm"
  1046  )
  1047  
  1048  func validateAPIVersion(c swarm.ServiceSpec, serverAPIVersion string) error {
  1049  	for _, m := range c.TaskTemplate.ContainerSpec.Mounts {
  1050  		if err := command.ValidateMountWithAPIVersion(m, serverAPIVersion); err != nil {
  1051  			return err
  1052  		}
  1053  	}
  1054  	return nil
  1055  }