github.com/ali-iotechsys/cli@v20.10.0+incompatible/cli/command/service/opts.go (about)

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