github.com/khulnasoft/cli@v0.0.0-20240402070845-01bcad7beefa/cli/compose/convert/service.go (about)

     1  package convert
     2  
     3  import (
     4  	"context"
     5  	"os"
     6  	"sort"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/docker/docker/api/types/container"
    11  	"github.com/docker/docker/api/types/swarm"
    12  	"github.com/docker/docker/api/types/versions"
    13  	"github.com/docker/docker/client"
    14  	"github.com/khulnasoft-lab/go-units"
    15  	servicecli "github.com/khulnasoft/cli/cli/command/service"
    16  	composetypes "github.com/khulnasoft/cli/cli/compose/types"
    17  	"github.com/khulnasoft/cli/opts"
    18  	"github.com/pkg/errors"
    19  )
    20  
    21  const (
    22  	defaultNetwork = "default"
    23  	// LabelImage is the label used to store image name provided in the compose file
    24  	LabelImage = "com.docker.stack.image"
    25  )
    26  
    27  // Services from compose-file types to engine API types
    28  func Services(
    29  	ctx context.Context,
    30  	namespace Namespace,
    31  	config *composetypes.Config,
    32  	apiClient client.CommonAPIClient,
    33  ) (map[string]swarm.ServiceSpec, error) {
    34  	result := make(map[string]swarm.ServiceSpec)
    35  	for _, service := range config.Services {
    36  		secrets, err := convertServiceSecrets(ctx, apiClient, namespace, service.Secrets, config.Secrets)
    37  		if err != nil {
    38  			return nil, errors.Wrapf(err, "service %s", service.Name)
    39  		}
    40  		configs, err := convertServiceConfigObjs(ctx, apiClient, namespace, service, config.Configs)
    41  		if err != nil {
    42  			return nil, errors.Wrapf(err, "service %s", service.Name)
    43  		}
    44  
    45  		serviceSpec, err := Service(apiClient.ClientVersion(), namespace, service, config.Networks, config.Volumes, secrets, configs)
    46  		if err != nil {
    47  			return nil, errors.Wrapf(err, "service %s", service.Name)
    48  		}
    49  		result[service.Name] = serviceSpec
    50  	}
    51  
    52  	return result, nil
    53  }
    54  
    55  // Service converts a ServiceConfig into a swarm ServiceSpec
    56  func Service(
    57  	apiVersion string,
    58  	namespace Namespace,
    59  	service composetypes.ServiceConfig,
    60  	networkConfigs map[string]composetypes.NetworkConfig,
    61  	volumes map[string]composetypes.VolumeConfig,
    62  	secrets []*swarm.SecretReference,
    63  	configs []*swarm.ConfigReference,
    64  ) (swarm.ServiceSpec, error) {
    65  	name := namespace.Scope(service.Name)
    66  	endpoint := convertEndpointSpec(service.Deploy.EndpointMode, service.Ports)
    67  
    68  	mode, err := convertDeployMode(service.Deploy.Mode, service.Deploy.Replicas)
    69  	if err != nil {
    70  		return swarm.ServiceSpec{}, err
    71  	}
    72  
    73  	mounts, err := Volumes(service.Volumes, volumes, namespace)
    74  	if err != nil {
    75  		return swarm.ServiceSpec{}, err
    76  	}
    77  
    78  	resources, err := convertResources(service.Deploy.Resources)
    79  	if err != nil {
    80  		return swarm.ServiceSpec{}, err
    81  	}
    82  
    83  	restartPolicy, err := convertRestartPolicy(
    84  		service.Restart, service.Deploy.RestartPolicy)
    85  	if err != nil {
    86  		return swarm.ServiceSpec{}, err
    87  	}
    88  
    89  	healthcheck, err := convertHealthcheck(service.HealthCheck)
    90  	if err != nil {
    91  		return swarm.ServiceSpec{}, err
    92  	}
    93  
    94  	networks, err := convertServiceNetworks(service.Networks, networkConfigs, namespace, service.Name)
    95  	if err != nil {
    96  		return swarm.ServiceSpec{}, err
    97  	}
    98  
    99  	dnsConfig := convertDNSConfig(service.DNS, service.DNSSearch)
   100  
   101  	var privileges swarm.Privileges
   102  	privileges.CredentialSpec, err = convertCredentialSpec(
   103  		namespace, service.CredentialSpec, configs,
   104  	)
   105  	if err != nil {
   106  		return swarm.ServiceSpec{}, err
   107  	}
   108  
   109  	var logDriver *swarm.Driver
   110  	if service.Logging != nil {
   111  		logDriver = &swarm.Driver{
   112  			Name:    service.Logging.Driver,
   113  			Options: service.Logging.Options,
   114  		}
   115  	}
   116  
   117  	capAdd, capDrop := opts.EffectiveCapAddCapDrop(service.CapAdd, service.CapDrop)
   118  
   119  	serviceSpec := swarm.ServiceSpec{
   120  		Annotations: swarm.Annotations{
   121  			Name:   name,
   122  			Labels: AddStackLabel(namespace, service.Deploy.Labels),
   123  		},
   124  		TaskTemplate: swarm.TaskSpec{
   125  			ContainerSpec: &swarm.ContainerSpec{
   126  				Image:           service.Image,
   127  				Command:         service.Entrypoint,
   128  				Args:            service.Command,
   129  				Hostname:        service.Hostname,
   130  				Hosts:           convertExtraHosts(service.ExtraHosts),
   131  				DNSConfig:       dnsConfig,
   132  				Healthcheck:     healthcheck,
   133  				Env:             convertEnvironment(service.Environment),
   134  				Labels:          AddStackLabel(namespace, service.Labels),
   135  				Dir:             service.WorkingDir,
   136  				User:            service.User,
   137  				Mounts:          mounts,
   138  				StopGracePeriod: composetypes.ConvertDurationPtr(service.StopGracePeriod),
   139  				StopSignal:      service.StopSignal,
   140  				TTY:             service.Tty,
   141  				OpenStdin:       service.StdinOpen,
   142  				Secrets:         secrets,
   143  				Configs:         configs,
   144  				ReadOnly:        service.ReadOnly,
   145  				Privileges:      &privileges,
   146  				Isolation:       container.Isolation(service.Isolation),
   147  				Init:            service.Init,
   148  				Sysctls:         service.Sysctls,
   149  				CapabilityAdd:   capAdd,
   150  				CapabilityDrop:  capDrop,
   151  				Ulimits:         convertUlimits(service.Ulimits),
   152  			},
   153  			LogDriver:     logDriver,
   154  			Resources:     resources,
   155  			RestartPolicy: restartPolicy,
   156  			Placement: &swarm.Placement{
   157  				Constraints: service.Deploy.Placement.Constraints,
   158  				Preferences: getPlacementPreference(service.Deploy.Placement.Preferences),
   159  				MaxReplicas: service.Deploy.Placement.MaxReplicas,
   160  			},
   161  		},
   162  		EndpointSpec:   endpoint,
   163  		Mode:           mode,
   164  		UpdateConfig:   convertUpdateConfig(service.Deploy.UpdateConfig),
   165  		RollbackConfig: convertUpdateConfig(service.Deploy.RollbackConfig),
   166  	}
   167  
   168  	// add an image label to serviceSpec
   169  	serviceSpec.Labels[LabelImage] = service.Image
   170  
   171  	// ServiceSpec.Networks is deprecated and should not have been used by
   172  	// this package. It is possible to update TaskTemplate.Networks, but it
   173  	// is not possible to update ServiceSpec.Networks. Unfortunately, we
   174  	// can't unconditionally start using TaskTemplate.Networks, because that
   175  	// will break with older daemons that don't support migrating from
   176  	// ServiceSpec.Networks to TaskTemplate.Networks. So which field to use
   177  	// is conditional on daemon version.
   178  	if versions.LessThan(apiVersion, "1.29") {
   179  		serviceSpec.Networks = networks //nolint:staticcheck // ignore SA1019: field is deprecated.
   180  	} else {
   181  		serviceSpec.TaskTemplate.Networks = networks
   182  	}
   183  	return serviceSpec, nil
   184  }
   185  
   186  func getPlacementPreference(preferences []composetypes.PlacementPreferences) []swarm.PlacementPreference {
   187  	result := []swarm.PlacementPreference{}
   188  	for _, preference := range preferences {
   189  		spreadDescriptor := preference.Spread
   190  		result = append(result, swarm.PlacementPreference{
   191  			Spread: &swarm.SpreadOver{
   192  				SpreadDescriptor: spreadDescriptor,
   193  			},
   194  		})
   195  	}
   196  	return result
   197  }
   198  
   199  func convertServiceNetworks(
   200  	networks map[string]*composetypes.ServiceNetworkConfig,
   201  	networkConfigs networkMap,
   202  	namespace Namespace,
   203  	name string,
   204  ) ([]swarm.NetworkAttachmentConfig, error) {
   205  	if len(networks) == 0 {
   206  		networks = map[string]*composetypes.ServiceNetworkConfig{
   207  			defaultNetwork: {},
   208  		}
   209  	}
   210  
   211  	nets := []swarm.NetworkAttachmentConfig{}
   212  	for networkName, network := range networks {
   213  		networkConfig, ok := networkConfigs[networkName]
   214  		if !ok && networkName != defaultNetwork {
   215  			return nil, errors.Errorf("undefined network %q", networkName)
   216  		}
   217  		var aliases []string
   218  		if network != nil {
   219  			aliases = network.Aliases
   220  		}
   221  		target := namespace.Scope(networkName)
   222  		if networkConfig.Name != "" {
   223  			target = networkConfig.Name
   224  		}
   225  		netAttachConfig := swarm.NetworkAttachmentConfig{
   226  			Target:  target,
   227  			Aliases: aliases,
   228  		}
   229  		// Only add default aliases to user defined networks. Other networks do
   230  		// not support aliases.
   231  		if container.NetworkMode(target).IsUserDefined() {
   232  			netAttachConfig.Aliases = append(netAttachConfig.Aliases, name)
   233  		}
   234  		nets = append(nets, netAttachConfig)
   235  	}
   236  
   237  	sort.Slice(nets, func(i, j int) bool {
   238  		return nets[i].Target < nets[j].Target
   239  	})
   240  	return nets, nil
   241  }
   242  
   243  // TODO: fix secrets API so that SecretAPIClient is not required here
   244  func convertServiceSecrets(
   245  	ctx context.Context,
   246  	apiClient client.SecretAPIClient,
   247  	namespace Namespace,
   248  	secrets []composetypes.ServiceSecretConfig,
   249  	secretSpecs map[string]composetypes.SecretConfig,
   250  ) ([]*swarm.SecretReference, error) {
   251  	refs := []*swarm.SecretReference{}
   252  
   253  	lookup := func(key string) (composetypes.FileObjectConfig, error) {
   254  		secretSpec, exists := secretSpecs[key]
   255  		if !exists {
   256  			return composetypes.FileObjectConfig{}, errors.Errorf("undefined secret %q", key)
   257  		}
   258  		return composetypes.FileObjectConfig(secretSpec), nil
   259  	}
   260  	for _, secret := range secrets {
   261  		obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(secret), lookup)
   262  		if err != nil {
   263  			return nil, err
   264  		}
   265  
   266  		file := swarm.SecretReferenceFileTarget(obj.File)
   267  		refs = append(refs, &swarm.SecretReference{
   268  			File:       &file,
   269  			SecretName: obj.Name,
   270  		})
   271  	}
   272  
   273  	secrs, err := servicecli.ParseSecrets(ctx, apiClient, refs)
   274  	if err != nil {
   275  		return nil, err
   276  	}
   277  	// sort to ensure idempotence (don't restart services just because the entries are in different order)
   278  	sort.SliceStable(secrs, func(i, j int) bool { return secrs[i].SecretName < secrs[j].SecretName })
   279  	return secrs, err
   280  }
   281  
   282  // convertServiceConfigObjs takes an API client, a namespace, a ServiceConfig,
   283  // and a set of compose Config specs, and creates the swarm ConfigReferences
   284  // required by the serivce. Unlike convertServiceSecrets, this takes the whole
   285  // ServiceConfig, because some Configs may be needed as a result of other
   286  // fields (like CredentialSpecs).
   287  //
   288  // TODO: fix configs API so that ConfigsAPIClient is not required here
   289  func convertServiceConfigObjs(
   290  	ctx context.Context,
   291  	apiClient client.ConfigAPIClient,
   292  	namespace Namespace,
   293  	service composetypes.ServiceConfig,
   294  	configSpecs map[string]composetypes.ConfigObjConfig,
   295  ) ([]*swarm.ConfigReference, error) {
   296  	refs := []*swarm.ConfigReference{}
   297  
   298  	lookup := func(key string) (composetypes.FileObjectConfig, error) {
   299  		configSpec, exists := configSpecs[key]
   300  		if !exists {
   301  			return composetypes.FileObjectConfig{}, errors.Errorf("undefined config %q", key)
   302  		}
   303  		return composetypes.FileObjectConfig(configSpec), nil
   304  	}
   305  	for _, config := range service.Configs {
   306  		obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(config), lookup)
   307  		if err != nil {
   308  			return nil, err
   309  		}
   310  
   311  		file := swarm.ConfigReferenceFileTarget(obj.File)
   312  		refs = append(refs, &swarm.ConfigReference{
   313  			File:       &file,
   314  			ConfigName: obj.Name,
   315  		})
   316  	}
   317  
   318  	// finally, after converting all of the file objects, create any
   319  	// Runtime-type configs that are needed. these are configs that are not
   320  	// mounted into the container, but are used in some other way by the
   321  	// container runtime. Currently, this only means CredentialSpecs, but in
   322  	// the future it may be used for other fields
   323  
   324  	// grab the CredentialSpec out of the Service
   325  	credSpec := service.CredentialSpec
   326  	// if the credSpec uses a config, then we should grab the config name, and
   327  	// create a config reference for it. A File or Registry-type CredentialSpec
   328  	// does not need this operation.
   329  	if credSpec.Config != "" {
   330  		// look up the config in the configSpecs.
   331  		obj, err := lookup(credSpec.Config)
   332  		if err != nil {
   333  			return nil, err
   334  		}
   335  
   336  		// get the actual correct name.
   337  		name := namespace.Scope(credSpec.Config)
   338  		if obj.Name != "" {
   339  			name = obj.Name
   340  		}
   341  
   342  		// now append a Runtime-type config.
   343  		refs = append(refs, &swarm.ConfigReference{
   344  			ConfigName: name,
   345  			Runtime:    &swarm.ConfigReferenceRuntimeTarget{},
   346  		})
   347  	}
   348  
   349  	confs, err := servicecli.ParseConfigs(ctx, apiClient, refs)
   350  	if err != nil {
   351  		return nil, err
   352  	}
   353  	// sort to ensure idempotence (don't restart services just because the entries are in different order)
   354  	sort.SliceStable(confs, func(i, j int) bool { return confs[i].ConfigName < confs[j].ConfigName })
   355  	return confs, err
   356  }
   357  
   358  type swarmReferenceTarget struct {
   359  	Name string
   360  	UID  string
   361  	GID  string
   362  	Mode os.FileMode
   363  }
   364  
   365  type swarmReferenceObject struct {
   366  	File swarmReferenceTarget
   367  	ID   string
   368  	Name string
   369  }
   370  
   371  func convertFileObject(
   372  	namespace Namespace,
   373  	config composetypes.FileReferenceConfig,
   374  	lookup func(key string) (composetypes.FileObjectConfig, error),
   375  ) (swarmReferenceObject, error) {
   376  	obj, err := lookup(config.Source)
   377  	if err != nil {
   378  		return swarmReferenceObject{}, err
   379  	}
   380  
   381  	source := namespace.Scope(config.Source)
   382  	if obj.Name != "" {
   383  		source = obj.Name
   384  	}
   385  
   386  	target := config.Target
   387  	if target == "" {
   388  		target = config.Source
   389  	}
   390  
   391  	uid := config.UID
   392  	gid := config.GID
   393  	if uid == "" {
   394  		uid = "0"
   395  	}
   396  	if gid == "" {
   397  		gid = "0"
   398  	}
   399  	mode := config.Mode
   400  	if mode == nil {
   401  		mode = uint32Ptr(0o444)
   402  	}
   403  
   404  	return swarmReferenceObject{
   405  		File: swarmReferenceTarget{
   406  			Name: target,
   407  			UID:  uid,
   408  			GID:  gid,
   409  			Mode: os.FileMode(*mode),
   410  		},
   411  		Name: source,
   412  	}, nil
   413  }
   414  
   415  func uint32Ptr(value uint32) *uint32 {
   416  	return &value
   417  }
   418  
   419  // convertExtraHosts converts <host>:<ip> mappings to SwarmKit notation:
   420  // "IP-address hostname(s)". The original order of mappings is preserved.
   421  func convertExtraHosts(extraHosts composetypes.HostsList) []string {
   422  	hosts := make([]string, 0, len(extraHosts))
   423  	for _, hostIP := range extraHosts {
   424  		if hostName, ipAddr, ok := strings.Cut(hostIP, ":"); ok {
   425  			// Convert to SwarmKit notation: IP-address hostname(s)
   426  			hosts = append(hosts, ipAddr+" "+hostName)
   427  		}
   428  	}
   429  	return hosts
   430  }
   431  
   432  func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container.HealthConfig, error) {
   433  	if healthcheck == nil {
   434  		return nil, nil
   435  	}
   436  	var (
   437  		timeout, interval, startPeriod, startInterval time.Duration
   438  		retries                                       int
   439  	)
   440  	if healthcheck.Disable {
   441  		if len(healthcheck.Test) != 0 {
   442  			return nil, errors.Errorf("test and disable can't be set at the same time")
   443  		}
   444  		return &container.HealthConfig{
   445  			Test: []string{"NONE"},
   446  		}, nil
   447  	}
   448  	if healthcheck.Timeout != nil {
   449  		timeout = time.Duration(*healthcheck.Timeout)
   450  	}
   451  	if healthcheck.Interval != nil {
   452  		interval = time.Duration(*healthcheck.Interval)
   453  	}
   454  	if healthcheck.StartPeriod != nil {
   455  		startPeriod = time.Duration(*healthcheck.StartPeriod)
   456  	}
   457  	if healthcheck.StartInterval != nil {
   458  		startInterval = time.Duration(*healthcheck.StartInterval)
   459  	}
   460  	if healthcheck.Retries != nil {
   461  		retries = int(*healthcheck.Retries)
   462  	}
   463  	return &container.HealthConfig{
   464  		Test:          healthcheck.Test,
   465  		Timeout:       timeout,
   466  		Interval:      interval,
   467  		Retries:       retries,
   468  		StartPeriod:   startPeriod,
   469  		StartInterval: startInterval,
   470  	}, nil
   471  }
   472  
   473  func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) {
   474  	// TODO: log if restart is being ignored
   475  	if source == nil {
   476  		policy, err := opts.ParseRestartPolicy(restart)
   477  		if err != nil {
   478  			return nil, err
   479  		}
   480  		switch {
   481  		case policy.IsNone():
   482  			return nil, nil
   483  		case policy.IsAlways(), policy.IsUnlessStopped():
   484  			return &swarm.RestartPolicy{
   485  				Condition: swarm.RestartPolicyConditionAny,
   486  			}, nil
   487  		case policy.IsOnFailure():
   488  			attempts := uint64(policy.MaximumRetryCount)
   489  			return &swarm.RestartPolicy{
   490  				Condition:   swarm.RestartPolicyConditionOnFailure,
   491  				MaxAttempts: &attempts,
   492  			}, nil
   493  		default:
   494  			return nil, errors.Errorf("unknown restart policy: %s", restart)
   495  		}
   496  	}
   497  
   498  	return &swarm.RestartPolicy{
   499  		Condition:   swarm.RestartPolicyCondition(source.Condition),
   500  		Delay:       composetypes.ConvertDurationPtr(source.Delay),
   501  		MaxAttempts: source.MaxAttempts,
   502  		Window:      composetypes.ConvertDurationPtr(source.Window),
   503  	}, nil
   504  }
   505  
   506  func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig {
   507  	if source == nil {
   508  		return nil
   509  	}
   510  	parallel := uint64(1)
   511  	if source.Parallelism != nil {
   512  		parallel = *source.Parallelism
   513  	}
   514  	return &swarm.UpdateConfig{
   515  		Parallelism:     parallel,
   516  		Delay:           time.Duration(source.Delay),
   517  		FailureAction:   source.FailureAction,
   518  		Monitor:         time.Duration(source.Monitor),
   519  		MaxFailureRatio: source.MaxFailureRatio,
   520  		Order:           source.Order,
   521  	}
   522  }
   523  
   524  func convertResources(source composetypes.Resources) (*swarm.ResourceRequirements, error) {
   525  	resources := &swarm.ResourceRequirements{}
   526  	var err error
   527  	if source.Limits != nil {
   528  		var cpus int64
   529  		if source.Limits.NanoCPUs != "" {
   530  			cpus, err = opts.ParseCPUs(source.Limits.NanoCPUs)
   531  			if err != nil {
   532  				return nil, err
   533  			}
   534  		}
   535  		resources.Limits = &swarm.Limit{
   536  			NanoCPUs:    cpus,
   537  			MemoryBytes: int64(source.Limits.MemoryBytes),
   538  			Pids:        source.Limits.Pids,
   539  		}
   540  	}
   541  	if source.Reservations != nil {
   542  		var cpus int64
   543  		if source.Reservations.NanoCPUs != "" {
   544  			cpus, err = opts.ParseCPUs(source.Reservations.NanoCPUs)
   545  			if err != nil {
   546  				return nil, err
   547  			}
   548  		}
   549  
   550  		var generic []swarm.GenericResource
   551  		for _, res := range source.Reservations.GenericResources {
   552  			var r swarm.GenericResource
   553  
   554  			if res.DiscreteResourceSpec != nil {
   555  				r.DiscreteResourceSpec = &swarm.DiscreteGenericResource{
   556  					Kind:  res.DiscreteResourceSpec.Kind,
   557  					Value: res.DiscreteResourceSpec.Value,
   558  				}
   559  			}
   560  
   561  			generic = append(generic, r)
   562  		}
   563  
   564  		resources.Reservations = &swarm.Resources{
   565  			NanoCPUs:         cpus,
   566  			MemoryBytes:      int64(source.Reservations.MemoryBytes),
   567  			GenericResources: generic,
   568  		}
   569  	}
   570  	return resources, nil
   571  }
   572  
   573  func convertEndpointSpec(endpointMode string, source []composetypes.ServicePortConfig) *swarm.EndpointSpec {
   574  	portConfigs := []swarm.PortConfig{}
   575  	for _, port := range source {
   576  		portConfig := swarm.PortConfig{
   577  			Protocol:      swarm.PortConfigProtocol(port.Protocol),
   578  			TargetPort:    port.Target,
   579  			PublishedPort: port.Published,
   580  			PublishMode:   swarm.PortConfigPublishMode(port.Mode),
   581  		}
   582  		portConfigs = append(portConfigs, portConfig)
   583  	}
   584  
   585  	sort.Slice(portConfigs, func(i, j int) bool {
   586  		return portConfigs[i].PublishedPort < portConfigs[j].PublishedPort
   587  	})
   588  
   589  	return &swarm.EndpointSpec{
   590  		Mode:  swarm.ResolutionMode(strings.ToLower(endpointMode)),
   591  		Ports: portConfigs,
   592  	}
   593  }
   594  
   595  // convertEnvironment converts key/value mappings to a slice, and sorts
   596  // the results.
   597  func convertEnvironment(source map[string]*string) []string {
   598  	var output []string
   599  
   600  	for name, value := range source {
   601  		switch value {
   602  		case nil:
   603  			output = append(output, name)
   604  		default:
   605  			output = append(output, name+"="+*value)
   606  		}
   607  	}
   608  	sort.Strings(output)
   609  	return output
   610  }
   611  
   612  func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) {
   613  	serviceMode := swarm.ServiceMode{}
   614  
   615  	switch mode {
   616  	case "global-job":
   617  		if replicas != nil {
   618  			return serviceMode, errors.Errorf("replicas can only be used with replicated or replicated-job mode")
   619  		}
   620  		serviceMode.GlobalJob = &swarm.GlobalJob{}
   621  	case "global":
   622  		if replicas != nil {
   623  			return serviceMode, errors.Errorf("replicas can only be used with replicated or replicated-job mode")
   624  		}
   625  		serviceMode.Global = &swarm.GlobalService{}
   626  	case "replicated-job":
   627  		serviceMode.ReplicatedJob = &swarm.ReplicatedJob{
   628  			MaxConcurrent:    replicas,
   629  			TotalCompletions: replicas,
   630  		}
   631  	case "replicated", "":
   632  		serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas}
   633  	default:
   634  		return serviceMode, errors.Errorf("Unknown mode: %s", mode)
   635  	}
   636  	return serviceMode, nil
   637  }
   638  
   639  func convertDNSConfig(dns []string, dnsSearch []string) *swarm.DNSConfig {
   640  	if dns != nil || dnsSearch != nil {
   641  		return &swarm.DNSConfig{
   642  			Nameservers: dns,
   643  			Search:      dnsSearch,
   644  		}
   645  	}
   646  	return nil
   647  }
   648  
   649  func convertCredentialSpec(namespace Namespace, spec composetypes.CredentialSpecConfig, refs []*swarm.ConfigReference) (*swarm.CredentialSpec, error) {
   650  	var o []string
   651  
   652  	// Config was added in API v1.40
   653  	if spec.Config != "" {
   654  		o = append(o, `"Config"`)
   655  	}
   656  	if spec.File != "" {
   657  		o = append(o, `"File"`)
   658  	}
   659  	if spec.Registry != "" {
   660  		o = append(o, `"Registry"`)
   661  	}
   662  	l := len(o)
   663  	switch {
   664  	case l == 0:
   665  		return nil, nil
   666  	case l == 2:
   667  		return nil, errors.Errorf("invalid credential spec: cannot specify both %s and %s", o[0], o[1])
   668  	case l > 2:
   669  		return nil, errors.Errorf("invalid credential spec: cannot specify both %s, and %s", strings.Join(o[:l-1], ", "), o[l-1])
   670  	}
   671  	swarmCredSpec := swarm.CredentialSpec(spec)
   672  	// if we're using a swarm Config for the credential spec, over-write it
   673  	// here with the config ID
   674  	if swarmCredSpec.Config != "" {
   675  		for _, config := range refs {
   676  			if swarmCredSpec.Config == config.ConfigName {
   677  				swarmCredSpec.Config = config.ConfigID
   678  				return &swarmCredSpec, nil
   679  			}
   680  		}
   681  		// if none of the configs match, try namespacing
   682  		for _, config := range refs {
   683  			if namespace.Scope(swarmCredSpec.Config) == config.ConfigName {
   684  				swarmCredSpec.Config = config.ConfigID
   685  				return &swarmCredSpec, nil
   686  			}
   687  		}
   688  		return nil, errors.Errorf("invalid credential spec: spec specifies config %v, but no such config can be found", swarmCredSpec.Config)
   689  	}
   690  	return &swarmCredSpec, nil
   691  }
   692  
   693  func convertUlimits(origUlimits map[string]*composetypes.UlimitsConfig) []*units.Ulimit {
   694  	newUlimits := make(map[string]*units.Ulimit)
   695  	for name, u := range origUlimits {
   696  		if u.Single != 0 {
   697  			newUlimits[name] = &units.Ulimit{
   698  				Name: name,
   699  				Soft: int64(u.Single),
   700  				Hard: int64(u.Single),
   701  			}
   702  		} else {
   703  			newUlimits[name] = &units.Ulimit{
   704  				Name: name,
   705  				Soft: int64(u.Soft),
   706  				Hard: int64(u.Hard),
   707  			}
   708  		}
   709  	}
   710  	ulimits := make([]*units.Ulimit, 0, len(newUlimits))
   711  	for _, ulimit := range newUlimits {
   712  		ulimits = append(ulimits, ulimit)
   713  	}
   714  	sort.SliceStable(ulimits, func(i, j int) bool {
   715  		return ulimits[i].Name < ulimits[j].Name
   716  	})
   717  	return ulimits
   718  }