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

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