github.com/itscaro/cli@v0.0.0-20190705081621-c9db0fe93829/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/pkg/errors"
    18  )
    19  
    20  const (
    21  	defaultNetwork = "default"
    22  	// LabelImage is the label used to store image name provided in the compose file
    23  	LabelImage = "com.docker.stack.image"
    24  )
    25  
    26  // Services from compose-file types to engine API types
    27  func Services(
    28  	namespace Namespace,
    29  	config *composetypes.Config,
    30  	client client.CommonAPIClient,
    31  ) (map[string]swarm.ServiceSpec, error) {
    32  	result := make(map[string]swarm.ServiceSpec)
    33  
    34  	services := config.Services
    35  	volumes := config.Volumes
    36  	networks := config.Networks
    37  
    38  	for _, service := range services {
    39  		secrets, err := convertServiceSecrets(client, namespace, service.Secrets, config.Secrets)
    40  		if err != nil {
    41  			return nil, errors.Wrapf(err, "service %s", service.Name)
    42  		}
    43  		configs, err := convertServiceConfigObjs(client, namespace, service, config.Configs)
    44  		if err != nil {
    45  			return nil, errors.Wrapf(err, "service %s", service.Name)
    46  		}
    47  
    48  		serviceSpec, err := Service(client.ClientVersion(), namespace, service, networks, volumes, secrets, configs)
    49  		if err != nil {
    50  			return nil, errors.Wrapf(err, "service %s", service.Name)
    51  		}
    52  		result[service.Name] = serviceSpec
    53  	}
    54  
    55  	return result, nil
    56  }
    57  
    58  // Service converts a ServiceConfig into a swarm ServiceSpec
    59  func Service(
    60  	apiVersion string,
    61  	namespace Namespace,
    62  	service composetypes.ServiceConfig,
    63  	networkConfigs map[string]composetypes.NetworkConfig,
    64  	volumes map[string]composetypes.VolumeConfig,
    65  	secrets []*swarm.SecretReference,
    66  	configs []*swarm.ConfigReference,
    67  ) (swarm.ServiceSpec, error) {
    68  	name := namespace.Scope(service.Name)
    69  
    70  	endpoint, err := convertEndpointSpec(service.Deploy.EndpointMode, service.Ports)
    71  	if err != nil {
    72  		return swarm.ServiceSpec{}, err
    73  	}
    74  
    75  	mode, err := convertDeployMode(service.Deploy.Mode, service.Deploy.Replicas)
    76  	if err != nil {
    77  		return swarm.ServiceSpec{}, err
    78  	}
    79  
    80  	mounts, err := Volumes(service.Volumes, volumes, namespace)
    81  	if err != nil {
    82  		return swarm.ServiceSpec{}, err
    83  	}
    84  
    85  	resources, err := convertResources(service.Deploy.Resources)
    86  	if err != nil {
    87  		return swarm.ServiceSpec{}, err
    88  	}
    89  
    90  	restartPolicy, err := convertRestartPolicy(
    91  		service.Restart, service.Deploy.RestartPolicy)
    92  	if err != nil {
    93  		return swarm.ServiceSpec{}, err
    94  	}
    95  
    96  	healthcheck, err := convertHealthcheck(service.HealthCheck)
    97  	if err != nil {
    98  		return swarm.ServiceSpec{}, err
    99  	}
   100  
   101  	networks, err := convertServiceNetworks(service.Networks, networkConfigs, namespace, service.Name)
   102  	if err != nil {
   103  		return swarm.ServiceSpec{}, err
   104  	}
   105  
   106  	dnsConfig, err := convertDNSConfig(service.DNS, service.DNSSearch)
   107  	if err != nil {
   108  		return swarm.ServiceSpec{}, err
   109  	}
   110  
   111  	var privileges swarm.Privileges
   112  	privileges.CredentialSpec, err = convertCredentialSpec(
   113  		namespace, service.CredentialSpec, configs,
   114  	)
   115  	if err != nil {
   116  		return swarm.ServiceSpec{}, err
   117  	}
   118  
   119  	var logDriver *swarm.Driver
   120  	if service.Logging != nil {
   121  		logDriver = &swarm.Driver{
   122  			Name:    service.Logging.Driver,
   123  			Options: service.Logging.Options,
   124  		}
   125  	}
   126  
   127  	serviceSpec := swarm.ServiceSpec{
   128  		Annotations: swarm.Annotations{
   129  			Name:   name,
   130  			Labels: AddStackLabel(namespace, service.Deploy.Labels),
   131  		},
   132  		TaskTemplate: swarm.TaskSpec{
   133  			ContainerSpec: &swarm.ContainerSpec{
   134  				Image:           service.Image,
   135  				Command:         service.Entrypoint,
   136  				Args:            service.Command,
   137  				Hostname:        service.Hostname,
   138  				Hosts:           convertExtraHosts(service.ExtraHosts),
   139  				DNSConfig:       dnsConfig,
   140  				Healthcheck:     healthcheck,
   141  				Env:             sortStrings(convertEnvironment(service.Environment)),
   142  				Labels:          AddStackLabel(namespace, service.Labels),
   143  				Dir:             service.WorkingDir,
   144  				User:            service.User,
   145  				Mounts:          mounts,
   146  				StopGracePeriod: composetypes.ConvertDurationPtr(service.StopGracePeriod),
   147  				StopSignal:      service.StopSignal,
   148  				TTY:             service.Tty,
   149  				OpenStdin:       service.StdinOpen,
   150  				Secrets:         secrets,
   151  				Configs:         configs,
   152  				ReadOnly:        service.ReadOnly,
   153  				Privileges:      &privileges,
   154  				Isolation:       container.Isolation(service.Isolation),
   155  				Init:            service.Init,
   156  				Sysctls:         service.Sysctls,
   157  			},
   158  			LogDriver:     logDriver,
   159  			Resources:     resources,
   160  			RestartPolicy: restartPolicy,
   161  			Placement: &swarm.Placement{
   162  				Constraints: service.Deploy.Placement.Constraints,
   163  				Preferences: getPlacementPreference(service.Deploy.Placement.Preferences),
   164  				MaxReplicas: service.Deploy.Placement.MaxReplicas,
   165  			},
   166  		},
   167  		EndpointSpec:   endpoint,
   168  		Mode:           mode,
   169  		UpdateConfig:   convertUpdateConfig(service.Deploy.UpdateConfig),
   170  		RollbackConfig: convertUpdateConfig(service.Deploy.RollbackConfig),
   171  	}
   172  
   173  	// add an image label to serviceSpec
   174  	serviceSpec.Labels[LabelImage] = service.Image
   175  
   176  	// ServiceSpec.Networks is deprecated and should not have been used by
   177  	// this package. It is possible to update TaskTemplate.Networks, but it
   178  	// is not possible to update ServiceSpec.Networks. Unfortunately, we
   179  	// can't unconditionally start using TaskTemplate.Networks, because that
   180  	// will break with older daemons that don't support migrating from
   181  	// ServiceSpec.Networks to TaskTemplate.Networks. So which field to use
   182  	// is conditional on daemon version.
   183  	if versions.LessThan(apiVersion, "1.29") {
   184  		serviceSpec.Networks = networks
   185  	} else {
   186  		serviceSpec.TaskTemplate.Networks = networks
   187  	}
   188  	return serviceSpec, nil
   189  }
   190  
   191  func getPlacementPreference(preferences []composetypes.PlacementPreferences) []swarm.PlacementPreference {
   192  	result := []swarm.PlacementPreference{}
   193  	for _, preference := range preferences {
   194  		spreadDescriptor := preference.Spread
   195  		result = append(result, swarm.PlacementPreference{
   196  			Spread: &swarm.SpreadOver{
   197  				SpreadDescriptor: spreadDescriptor,
   198  			},
   199  		})
   200  	}
   201  	return result
   202  }
   203  
   204  func sortStrings(strs []string) []string {
   205  	sort.Strings(strs)
   206  	return strs
   207  }
   208  
   209  func convertServiceNetworks(
   210  	networks map[string]*composetypes.ServiceNetworkConfig,
   211  	networkConfigs networkMap,
   212  	namespace Namespace,
   213  	name string,
   214  ) ([]swarm.NetworkAttachmentConfig, error) {
   215  	if len(networks) == 0 {
   216  		networks = map[string]*composetypes.ServiceNetworkConfig{
   217  			defaultNetwork: {},
   218  		}
   219  	}
   220  
   221  	nets := []swarm.NetworkAttachmentConfig{}
   222  	for networkName, network := range networks {
   223  		networkConfig, ok := networkConfigs[networkName]
   224  		if !ok && networkName != defaultNetwork {
   225  			return nil, errors.Errorf("undefined network %q", networkName)
   226  		}
   227  		var aliases []string
   228  		if network != nil {
   229  			aliases = network.Aliases
   230  		}
   231  		target := namespace.Scope(networkName)
   232  		if networkConfig.Name != "" {
   233  			target = networkConfig.Name
   234  		}
   235  		netAttachConfig := swarm.NetworkAttachmentConfig{
   236  			Target:  target,
   237  			Aliases: aliases,
   238  		}
   239  		// Only add default aliases to user defined networks. Other networks do
   240  		// not support aliases.
   241  		if container.NetworkMode(target).IsUserDefined() {
   242  			netAttachConfig.Aliases = append(netAttachConfig.Aliases, name)
   243  		}
   244  		nets = append(nets, netAttachConfig)
   245  	}
   246  
   247  	sort.Slice(nets, func(i, j int) bool {
   248  		return nets[i].Target < nets[j].Target
   249  	})
   250  	return nets, nil
   251  }
   252  
   253  // TODO: fix secrets API so that SecretAPIClient is not required here
   254  func convertServiceSecrets(
   255  	client client.SecretAPIClient,
   256  	namespace Namespace,
   257  	secrets []composetypes.ServiceSecretConfig,
   258  	secretSpecs map[string]composetypes.SecretConfig,
   259  ) ([]*swarm.SecretReference, error) {
   260  	refs := []*swarm.SecretReference{}
   261  
   262  	lookup := func(key string) (composetypes.FileObjectConfig, error) {
   263  		secretSpec, exists := secretSpecs[key]
   264  		if !exists {
   265  			return composetypes.FileObjectConfig{}, errors.Errorf("undefined secret %q", key)
   266  		}
   267  		return composetypes.FileObjectConfig(secretSpec), nil
   268  	}
   269  	for _, secret := range secrets {
   270  		obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(secret), lookup)
   271  		if err != nil {
   272  			return nil, err
   273  		}
   274  
   275  		file := swarm.SecretReferenceFileTarget(obj.File)
   276  		refs = append(refs, &swarm.SecretReference{
   277  			File:       &file,
   278  			SecretName: obj.Name,
   279  		})
   280  	}
   281  
   282  	secrs, err := servicecli.ParseSecrets(client, refs)
   283  	if err != nil {
   284  		return nil, err
   285  	}
   286  	// sort to ensure idempotence (don't restart services just because the entries are in different order)
   287  	sort.SliceStable(secrs, func(i, j int) bool { return secrs[i].SecretName < secrs[j].SecretName })
   288  	return secrs, err
   289  }
   290  
   291  // convertServiceConfigObjs takes an API client, a namespace, a ServiceConfig,
   292  // and a set of compose Config specs, and creates the swarm ConfigReferences
   293  // required by the serivce. Unlike convertServiceSecrets, this takes the whole
   294  // ServiceConfig, because some Configs may be needed as a result of other
   295  // fields (like CredentialSpecs).
   296  //
   297  // TODO: fix configs API so that ConfigsAPIClient is not required here
   298  func convertServiceConfigObjs(
   299  	client client.ConfigAPIClient,
   300  	namespace Namespace,
   301  	service composetypes.ServiceConfig,
   302  	configSpecs map[string]composetypes.ConfigObjConfig,
   303  ) ([]*swarm.ConfigReference, error) {
   304  	refs := []*swarm.ConfigReference{}
   305  
   306  	lookup := func(key string) (composetypes.FileObjectConfig, error) {
   307  		configSpec, exists := configSpecs[key]
   308  		if !exists {
   309  			return composetypes.FileObjectConfig{}, errors.Errorf("undefined config %q", key)
   310  		}
   311  		return composetypes.FileObjectConfig(configSpec), nil
   312  	}
   313  	for _, config := range service.Configs {
   314  		obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(config), lookup)
   315  		if err != nil {
   316  			return nil, err
   317  		}
   318  
   319  		file := swarm.ConfigReferenceFileTarget(obj.File)
   320  		refs = append(refs, &swarm.ConfigReference{
   321  			File:       &file,
   322  			ConfigName: obj.Name,
   323  		})
   324  	}
   325  
   326  	// finally, after converting all of the file objects, create any
   327  	// Runtime-type configs that are needed. these are configs that are not
   328  	// mounted into the container, but are used in some other way by the
   329  	// container runtime. Currently, this only means CredentialSpecs, but in
   330  	// the future it may be used for other fields
   331  
   332  	// grab the CredentialSpec out of the Service
   333  	credSpec := service.CredentialSpec
   334  	// if the credSpec uses a config, then we should grab the config name, and
   335  	// create a config reference for it. A File or Registry-type CredentialSpec
   336  	// does not need this operation.
   337  	if credSpec.Config != "" {
   338  		// look up the config in the configSpecs.
   339  		obj, err := lookup(credSpec.Config)
   340  		if err != nil {
   341  			return nil, err
   342  		}
   343  
   344  		// get the actual correct name.
   345  		name := namespace.Scope(credSpec.Config)
   346  		if obj.Name != "" {
   347  			name = obj.Name
   348  		}
   349  
   350  		// now append a Runtime-type config.
   351  		refs = append(refs, &swarm.ConfigReference{
   352  			ConfigName: name,
   353  			Runtime:    &swarm.ConfigReferenceRuntimeTarget{},
   354  		})
   355  
   356  	}
   357  
   358  	confs, err := servicecli.ParseConfigs(client, refs)
   359  	if err != nil {
   360  		return nil, err
   361  	}
   362  	// sort to ensure idempotence (don't restart services just because the entries are in different order)
   363  	sort.SliceStable(confs, func(i, j int) bool { return confs[i].ConfigName < confs[j].ConfigName })
   364  	return confs, err
   365  }
   366  
   367  type swarmReferenceTarget struct {
   368  	Name string
   369  	UID  string
   370  	GID  string
   371  	Mode os.FileMode
   372  }
   373  
   374  type swarmReferenceObject struct {
   375  	File swarmReferenceTarget
   376  	ID   string
   377  	Name string
   378  }
   379  
   380  func convertFileObject(
   381  	namespace Namespace,
   382  	config composetypes.FileReferenceConfig,
   383  	lookup func(key string) (composetypes.FileObjectConfig, error),
   384  ) (swarmReferenceObject, error) {
   385  	obj, err := lookup(config.Source)
   386  	if err != nil {
   387  		return swarmReferenceObject{}, err
   388  	}
   389  
   390  	source := namespace.Scope(config.Source)
   391  	if obj.Name != "" {
   392  		source = obj.Name
   393  	}
   394  
   395  	target := config.Target
   396  	if target == "" {
   397  		target = config.Source
   398  	}
   399  
   400  	uid := config.UID
   401  	gid := config.GID
   402  	if uid == "" {
   403  		uid = "0"
   404  	}
   405  	if gid == "" {
   406  		gid = "0"
   407  	}
   408  	mode := config.Mode
   409  	if mode == nil {
   410  		mode = uint32Ptr(0444)
   411  	}
   412  
   413  	return swarmReferenceObject{
   414  		File: swarmReferenceTarget{
   415  			Name: target,
   416  			UID:  uid,
   417  			GID:  gid,
   418  			Mode: os.FileMode(*mode),
   419  		},
   420  		Name: source,
   421  	}, nil
   422  }
   423  
   424  func uint32Ptr(value uint32) *uint32 {
   425  	return &value
   426  }
   427  
   428  // convertExtraHosts converts <host>:<ip> mappings to SwarmKit notation:
   429  // "IP-address hostname(s)". The original order of mappings is preserved.
   430  func convertExtraHosts(extraHosts composetypes.HostsList) []string {
   431  	hosts := []string{}
   432  	for _, hostIP := range extraHosts {
   433  		if v := strings.SplitN(hostIP, ":", 2); len(v) == 2 {
   434  			// Convert to SwarmKit notation: IP-address hostname(s)
   435  			hosts = append(hosts, fmt.Sprintf("%s %s", v[1], v[0]))
   436  		}
   437  	}
   438  	return hosts
   439  }
   440  
   441  func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container.HealthConfig, error) {
   442  	if healthcheck == nil {
   443  		return nil, nil
   444  	}
   445  	var (
   446  		timeout, interval, startPeriod time.Duration
   447  		retries                        int
   448  	)
   449  	if healthcheck.Disable {
   450  		if len(healthcheck.Test) != 0 {
   451  			return nil, errors.Errorf("test and disable can't be set at the same time")
   452  		}
   453  		return &container.HealthConfig{
   454  			Test: []string{"NONE"},
   455  		}, nil
   456  
   457  	}
   458  	if healthcheck.Timeout != nil {
   459  		timeout = time.Duration(*healthcheck.Timeout)
   460  	}
   461  	if healthcheck.Interval != nil {
   462  		interval = time.Duration(*healthcheck.Interval)
   463  	}
   464  	if healthcheck.StartPeriod != nil {
   465  		startPeriod = time.Duration(*healthcheck.StartPeriod)
   466  	}
   467  	if healthcheck.Retries != nil {
   468  		retries = int(*healthcheck.Retries)
   469  	}
   470  	return &container.HealthConfig{
   471  		Test:        healthcheck.Test,
   472  		Timeout:     timeout,
   473  		Interval:    interval,
   474  		Retries:     retries,
   475  		StartPeriod: startPeriod,
   476  	}, nil
   477  }
   478  
   479  func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) {
   480  	// TODO: log if restart is being ignored
   481  	if source == nil {
   482  		policy, err := opts.ParseRestartPolicy(restart)
   483  		if err != nil {
   484  			return nil, err
   485  		}
   486  		switch {
   487  		case policy.IsNone():
   488  			return nil, nil
   489  		case policy.IsAlways(), policy.IsUnlessStopped():
   490  			return &swarm.RestartPolicy{
   491  				Condition: swarm.RestartPolicyConditionAny,
   492  			}, nil
   493  		case policy.IsOnFailure():
   494  			attempts := uint64(policy.MaximumRetryCount)
   495  			return &swarm.RestartPolicy{
   496  				Condition:   swarm.RestartPolicyConditionOnFailure,
   497  				MaxAttempts: &attempts,
   498  			}, nil
   499  		default:
   500  			return nil, errors.Errorf("unknown restart policy: %s", restart)
   501  		}
   502  	}
   503  
   504  	return &swarm.RestartPolicy{
   505  		Condition:   swarm.RestartPolicyCondition(source.Condition),
   506  		Delay:       composetypes.ConvertDurationPtr(source.Delay),
   507  		MaxAttempts: source.MaxAttempts,
   508  		Window:      composetypes.ConvertDurationPtr(source.Window),
   509  	}, nil
   510  }
   511  
   512  func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig {
   513  	if source == nil {
   514  		return nil
   515  	}
   516  	parallel := uint64(1)
   517  	if source.Parallelism != nil {
   518  		parallel = *source.Parallelism
   519  	}
   520  	return &swarm.UpdateConfig{
   521  		Parallelism:     parallel,
   522  		Delay:           time.Duration(source.Delay),
   523  		FailureAction:   source.FailureAction,
   524  		Monitor:         time.Duration(source.Monitor),
   525  		MaxFailureRatio: source.MaxFailureRatio,
   526  		Order:           source.Order,
   527  	}
   528  }
   529  
   530  func convertResources(source composetypes.Resources) (*swarm.ResourceRequirements, error) {
   531  	resources := &swarm.ResourceRequirements{}
   532  	var err error
   533  	if source.Limits != nil {
   534  		var cpus int64
   535  		if source.Limits.NanoCPUs != "" {
   536  			cpus, err = opts.ParseCPUs(source.Limits.NanoCPUs)
   537  			if err != nil {
   538  				return nil, err
   539  			}
   540  		}
   541  		resources.Limits = &swarm.Resources{
   542  			NanoCPUs:    cpus,
   543  			MemoryBytes: int64(source.Limits.MemoryBytes),
   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, error) {
   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  	}, nil
   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, error) {
   633  	if DNS != nil || DNSSearch != nil {
   634  		return &swarm.DNSConfig{
   635  			Nameservers: DNS,
   636  			Search:      DNSSearch,
   637  		}, nil
   638  	}
   639  	return nil, 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  }