github.com/AliyunContainerService/cli@v0.0.0-20181009023821-814ced4b30d0/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.Configs, 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(service.CredentialSpec)
   113  	if err != nil {
   114  		return swarm.ServiceSpec{}, err
   115  	}
   116  
   117  	var logDriver *swarm.Driver
   118  	if service.Logging != nil {
   119  		logDriver = &swarm.Driver{
   120  			Name:    service.Logging.Driver,
   121  			Options: service.Logging.Options,
   122  		}
   123  	}
   124  
   125  	serviceSpec := swarm.ServiceSpec{
   126  		Annotations: swarm.Annotations{
   127  			Name:   name,
   128  			Labels: AddStackLabel(namespace, service.Deploy.Labels),
   129  		},
   130  		TaskTemplate: swarm.TaskSpec{
   131  			ContainerSpec: &swarm.ContainerSpec{
   132  				Image:           service.Image,
   133  				Command:         service.Entrypoint,
   134  				Args:            service.Command,
   135  				Hostname:        service.Hostname,
   136  				Hosts:           convertExtraHosts(service.ExtraHosts),
   137  				DNSConfig:       dnsConfig,
   138  				Healthcheck:     healthcheck,
   139  				Env:             sortStrings(convertEnvironment(service.Environment)),
   140  				Labels:          AddStackLabel(namespace, service.Labels),
   141  				Dir:             service.WorkingDir,
   142  				User:            service.User,
   143  				Mounts:          mounts,
   144  				StopGracePeriod: composetypes.ConvertDurationPtr(service.StopGracePeriod),
   145  				StopSignal:      service.StopSignal,
   146  				TTY:             service.Tty,
   147  				OpenStdin:       service.StdinOpen,
   148  				Secrets:         secrets,
   149  				Configs:         configs,
   150  				ReadOnly:        service.ReadOnly,
   151  				Privileges:      &privileges,
   152  				Isolation:       container.Isolation(service.Isolation),
   153  				Init:            service.Init,
   154  			},
   155  			LogDriver:     logDriver,
   156  			Resources:     resources,
   157  			RestartPolicy: restartPolicy,
   158  			Placement: &swarm.Placement{
   159  				Constraints: service.Deploy.Placement.Constraints,
   160  				Preferences: getPlacementPreference(service.Deploy.Placement.Preferences),
   161  			},
   162  		},
   163  		EndpointSpec:   endpoint,
   164  		Mode:           mode,
   165  		UpdateConfig:   convertUpdateConfig(service.Deploy.UpdateConfig),
   166  		RollbackConfig: convertUpdateConfig(service.Deploy.RollbackConfig),
   167  	}
   168  
   169  	// add an image label to serviceSpec
   170  	serviceSpec.Labels[LabelImage] = service.Image
   171  
   172  	// ServiceSpec.Networks is deprecated and should not have been used by
   173  	// this package. It is possible to update TaskTemplate.Networks, but it
   174  	// is not possible to update ServiceSpec.Networks. Unfortunately, we
   175  	// can't unconditionally start using TaskTemplate.Networks, because that
   176  	// will break with older daemons that don't support migrating from
   177  	// ServiceSpec.Networks to TaskTemplate.Networks. So which field to use
   178  	// is conditional on daemon version.
   179  	if versions.LessThan(apiVersion, "1.29") {
   180  		serviceSpec.Networks = networks
   181  	} else {
   182  		serviceSpec.TaskTemplate.Networks = networks
   183  	}
   184  	return serviceSpec, nil
   185  }
   186  
   187  func getPlacementPreference(preferences []composetypes.PlacementPreferences) []swarm.PlacementPreference {
   188  	result := []swarm.PlacementPreference{}
   189  	for _, preference := range preferences {
   190  		spreadDescriptor := preference.Spread
   191  		result = append(result, swarm.PlacementPreference{
   192  			Spread: &swarm.SpreadOver{
   193  				SpreadDescriptor: spreadDescriptor,
   194  			},
   195  		})
   196  	}
   197  	return result
   198  }
   199  
   200  func sortStrings(strs []string) []string {
   201  	sort.Strings(strs)
   202  	return strs
   203  }
   204  
   205  func convertServiceNetworks(
   206  	networks map[string]*composetypes.ServiceNetworkConfig,
   207  	networkConfigs networkMap,
   208  	namespace Namespace,
   209  	name string,
   210  ) ([]swarm.NetworkAttachmentConfig, error) {
   211  	if len(networks) == 0 {
   212  		networks = map[string]*composetypes.ServiceNetworkConfig{
   213  			defaultNetwork: {},
   214  		}
   215  	}
   216  
   217  	nets := []swarm.NetworkAttachmentConfig{}
   218  	for networkName, network := range networks {
   219  		networkConfig, ok := networkConfigs[networkName]
   220  		if !ok && networkName != defaultNetwork {
   221  			return nil, errors.Errorf("undefined network %q", networkName)
   222  		}
   223  		var aliases []string
   224  		if network != nil {
   225  			aliases = network.Aliases
   226  		}
   227  		target := namespace.Scope(networkName)
   228  		if networkConfig.Name != "" {
   229  			target = networkConfig.Name
   230  		}
   231  		netAttachConfig := swarm.NetworkAttachmentConfig{
   232  			Target:  target,
   233  			Aliases: aliases,
   234  		}
   235  		// Only add default aliases to user defined networks. Other networks do
   236  		// not support aliases.
   237  		if container.NetworkMode(target).IsUserDefined() {
   238  			netAttachConfig.Aliases = append(netAttachConfig.Aliases, name)
   239  		}
   240  		nets = append(nets, netAttachConfig)
   241  	}
   242  
   243  	sort.Slice(nets, func(i, j int) bool {
   244  		return nets[i].Target < nets[j].Target
   245  	})
   246  	return nets, nil
   247  }
   248  
   249  // TODO: fix secrets API so that SecretAPIClient is not required here
   250  func convertServiceSecrets(
   251  	client client.SecretAPIClient,
   252  	namespace Namespace,
   253  	secrets []composetypes.ServiceSecretConfig,
   254  	secretSpecs map[string]composetypes.SecretConfig,
   255  ) ([]*swarm.SecretReference, error) {
   256  	refs := []*swarm.SecretReference{}
   257  
   258  	lookup := func(key string) (composetypes.FileObjectConfig, error) {
   259  		secretSpec, exists := secretSpecs[key]
   260  		if !exists {
   261  			return composetypes.FileObjectConfig{}, errors.Errorf("undefined secret %q", key)
   262  		}
   263  		return composetypes.FileObjectConfig(secretSpec), nil
   264  	}
   265  	for _, secret := range secrets {
   266  		obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(secret), lookup)
   267  		if err != nil {
   268  			return nil, err
   269  		}
   270  
   271  		file := swarm.SecretReferenceFileTarget(obj.File)
   272  		refs = append(refs, &swarm.SecretReference{
   273  			File:       &file,
   274  			SecretName: obj.Name,
   275  		})
   276  	}
   277  
   278  	secrs, err := servicecli.ParseSecrets(client, refs)
   279  	if err != nil {
   280  		return nil, err
   281  	}
   282  	// sort to ensure idempotence (don't restart services just because the entries are in different order)
   283  	sort.SliceStable(secrs, func(i, j int) bool { return secrs[i].SecretName < secrs[j].SecretName })
   284  	return secrs, err
   285  }
   286  
   287  // TODO: fix configs API so that ConfigsAPIClient is not required here
   288  func convertServiceConfigObjs(
   289  	client client.ConfigAPIClient,
   290  	namespace Namespace,
   291  	configs []composetypes.ServiceConfigObjConfig,
   292  	configSpecs map[string]composetypes.ConfigObjConfig,
   293  ) ([]*swarm.ConfigReference, error) {
   294  	refs := []*swarm.ConfigReference{}
   295  
   296  	lookup := func(key string) (composetypes.FileObjectConfig, error) {
   297  		configSpec, exists := configSpecs[key]
   298  		if !exists {
   299  			return composetypes.FileObjectConfig{}, errors.Errorf("undefined config %q", key)
   300  		}
   301  		return composetypes.FileObjectConfig(configSpec), nil
   302  	}
   303  	for _, config := range configs {
   304  		obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(config), lookup)
   305  		if err != nil {
   306  			return nil, err
   307  		}
   308  
   309  		file := swarm.ConfigReferenceFileTarget(obj.File)
   310  		refs = append(refs, &swarm.ConfigReference{
   311  			File:       &file,
   312  			ConfigName: obj.Name,
   313  		})
   314  	}
   315  
   316  	confs, err := servicecli.ParseConfigs(client, refs)
   317  	if err != nil {
   318  		return nil, err
   319  	}
   320  	// sort to ensure idempotence (don't restart services just because the entries are in different order)
   321  	sort.SliceStable(confs, func(i, j int) bool { return confs[i].ConfigName < confs[j].ConfigName })
   322  	return confs, err
   323  }
   324  
   325  type swarmReferenceTarget struct {
   326  	Name string
   327  	UID  string
   328  	GID  string
   329  	Mode os.FileMode
   330  }
   331  
   332  type swarmReferenceObject struct {
   333  	File swarmReferenceTarget
   334  	ID   string
   335  	Name string
   336  }
   337  
   338  func convertFileObject(
   339  	namespace Namespace,
   340  	config composetypes.FileReferenceConfig,
   341  	lookup func(key string) (composetypes.FileObjectConfig, error),
   342  ) (swarmReferenceObject, error) {
   343  	target := config.Target
   344  	if target == "" {
   345  		target = config.Source
   346  	}
   347  
   348  	obj, err := lookup(config.Source)
   349  	if err != nil {
   350  		return swarmReferenceObject{}, err
   351  	}
   352  
   353  	source := namespace.Scope(config.Source)
   354  	if obj.Name != "" {
   355  		source = obj.Name
   356  	}
   357  
   358  	uid := config.UID
   359  	gid := config.GID
   360  	if uid == "" {
   361  		uid = "0"
   362  	}
   363  	if gid == "" {
   364  		gid = "0"
   365  	}
   366  	mode := config.Mode
   367  	if mode == nil {
   368  		mode = uint32Ptr(0444)
   369  	}
   370  
   371  	return swarmReferenceObject{
   372  		File: swarmReferenceTarget{
   373  			Name: target,
   374  			UID:  uid,
   375  			GID:  gid,
   376  			Mode: os.FileMode(*mode),
   377  		},
   378  		Name: source,
   379  	}, nil
   380  }
   381  
   382  func uint32Ptr(value uint32) *uint32 {
   383  	return &value
   384  }
   385  
   386  // convertExtraHosts converts <host>:<ip> mappings to SwarmKit notation:
   387  // "IP-address hostname(s)". The original order of mappings is preserved.
   388  func convertExtraHosts(extraHosts composetypes.HostsList) []string {
   389  	hosts := []string{}
   390  	for _, hostIP := range extraHosts {
   391  		if v := strings.SplitN(hostIP, ":", 2); len(v) == 2 {
   392  			// Convert to SwarmKit notation: IP-address hostname(s)
   393  			hosts = append(hosts, fmt.Sprintf("%s %s", v[1], v[0]))
   394  		}
   395  	}
   396  	return hosts
   397  }
   398  
   399  func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container.HealthConfig, error) {
   400  	if healthcheck == nil {
   401  		return nil, nil
   402  	}
   403  	var (
   404  		timeout, interval, startPeriod time.Duration
   405  		retries                        int
   406  	)
   407  	if healthcheck.Disable {
   408  		if len(healthcheck.Test) != 0 {
   409  			return nil, errors.Errorf("test and disable can't be set at the same time")
   410  		}
   411  		return &container.HealthConfig{
   412  			Test: []string{"NONE"},
   413  		}, nil
   414  
   415  	}
   416  	if healthcheck.Timeout != nil {
   417  		timeout = time.Duration(*healthcheck.Timeout)
   418  	}
   419  	if healthcheck.Interval != nil {
   420  		interval = time.Duration(*healthcheck.Interval)
   421  	}
   422  	if healthcheck.StartPeriod != nil {
   423  		startPeriod = time.Duration(*healthcheck.StartPeriod)
   424  	}
   425  	if healthcheck.Retries != nil {
   426  		retries = int(*healthcheck.Retries)
   427  	}
   428  	return &container.HealthConfig{
   429  		Test:        healthcheck.Test,
   430  		Timeout:     timeout,
   431  		Interval:    interval,
   432  		Retries:     retries,
   433  		StartPeriod: startPeriod,
   434  	}, nil
   435  }
   436  
   437  func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) {
   438  	// TODO: log if restart is being ignored
   439  	if source == nil {
   440  		policy, err := opts.ParseRestartPolicy(restart)
   441  		if err != nil {
   442  			return nil, err
   443  		}
   444  		switch {
   445  		case policy.IsNone():
   446  			return nil, nil
   447  		case policy.IsAlways(), policy.IsUnlessStopped():
   448  			return &swarm.RestartPolicy{
   449  				Condition: swarm.RestartPolicyConditionAny,
   450  			}, nil
   451  		case policy.IsOnFailure():
   452  			attempts := uint64(policy.MaximumRetryCount)
   453  			return &swarm.RestartPolicy{
   454  				Condition:   swarm.RestartPolicyConditionOnFailure,
   455  				MaxAttempts: &attempts,
   456  			}, nil
   457  		default:
   458  			return nil, errors.Errorf("unknown restart policy: %s", restart)
   459  		}
   460  	}
   461  
   462  	return &swarm.RestartPolicy{
   463  		Condition:   swarm.RestartPolicyCondition(source.Condition),
   464  		Delay:       composetypes.ConvertDurationPtr(source.Delay),
   465  		MaxAttempts: source.MaxAttempts,
   466  		Window:      composetypes.ConvertDurationPtr(source.Window),
   467  	}, nil
   468  }
   469  
   470  func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig {
   471  	if source == nil {
   472  		return nil
   473  	}
   474  	parallel := uint64(1)
   475  	if source.Parallelism != nil {
   476  		parallel = *source.Parallelism
   477  	}
   478  	return &swarm.UpdateConfig{
   479  		Parallelism:     parallel,
   480  		Delay:           time.Duration(source.Delay),
   481  		FailureAction:   source.FailureAction,
   482  		Monitor:         time.Duration(source.Monitor),
   483  		MaxFailureRatio: source.MaxFailureRatio,
   484  		Order:           source.Order,
   485  	}
   486  }
   487  
   488  func convertResources(source composetypes.Resources) (*swarm.ResourceRequirements, error) {
   489  	resources := &swarm.ResourceRequirements{}
   490  	var err error
   491  	if source.Limits != nil {
   492  		var cpus int64
   493  		if source.Limits.NanoCPUs != "" {
   494  			cpus, err = opts.ParseCPUs(source.Limits.NanoCPUs)
   495  			if err != nil {
   496  				return nil, err
   497  			}
   498  		}
   499  		resources.Limits = &swarm.Resources{
   500  			NanoCPUs:    cpus,
   501  			MemoryBytes: int64(source.Limits.MemoryBytes),
   502  		}
   503  	}
   504  	if source.Reservations != nil {
   505  		var cpus int64
   506  		if source.Reservations.NanoCPUs != "" {
   507  			cpus, err = opts.ParseCPUs(source.Reservations.NanoCPUs)
   508  			if err != nil {
   509  				return nil, err
   510  			}
   511  		}
   512  
   513  		var generic []swarm.GenericResource
   514  		for _, res := range source.Reservations.GenericResources {
   515  			var r swarm.GenericResource
   516  
   517  			if res.DiscreteResourceSpec != nil {
   518  				r.DiscreteResourceSpec = &swarm.DiscreteGenericResource{
   519  					Kind:  res.DiscreteResourceSpec.Kind,
   520  					Value: res.DiscreteResourceSpec.Value,
   521  				}
   522  			}
   523  
   524  			generic = append(generic, r)
   525  		}
   526  
   527  		resources.Reservations = &swarm.Resources{
   528  			NanoCPUs:         cpus,
   529  			MemoryBytes:      int64(source.Reservations.MemoryBytes),
   530  			GenericResources: generic,
   531  		}
   532  	}
   533  	return resources, nil
   534  }
   535  
   536  func convertEndpointSpec(endpointMode string, source []composetypes.ServicePortConfig) (*swarm.EndpointSpec, error) {
   537  	portConfigs := []swarm.PortConfig{}
   538  	for _, port := range source {
   539  		portConfig := swarm.PortConfig{
   540  			Protocol:      swarm.PortConfigProtocol(port.Protocol),
   541  			TargetPort:    port.Target,
   542  			PublishedPort: port.Published,
   543  			PublishMode:   swarm.PortConfigPublishMode(port.Mode),
   544  		}
   545  		portConfigs = append(portConfigs, portConfig)
   546  	}
   547  
   548  	sort.Slice(portConfigs, func(i, j int) bool {
   549  		return portConfigs[i].PublishedPort < portConfigs[j].PublishedPort
   550  	})
   551  
   552  	return &swarm.EndpointSpec{
   553  		Mode:  swarm.ResolutionMode(strings.ToLower(endpointMode)),
   554  		Ports: portConfigs,
   555  	}, nil
   556  }
   557  
   558  func convertEnvironment(source map[string]*string) []string {
   559  	var output []string
   560  
   561  	for name, value := range source {
   562  		switch value {
   563  		case nil:
   564  			output = append(output, name)
   565  		default:
   566  			output = append(output, fmt.Sprintf("%s=%s", name, *value))
   567  		}
   568  	}
   569  
   570  	return output
   571  }
   572  
   573  func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) {
   574  	serviceMode := swarm.ServiceMode{}
   575  
   576  	switch mode {
   577  	case "global":
   578  		if replicas != nil {
   579  			return serviceMode, errors.Errorf("replicas can only be used with replicated mode")
   580  		}
   581  		serviceMode.Global = &swarm.GlobalService{}
   582  	case "replicated", "":
   583  		serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas}
   584  	default:
   585  		return serviceMode, errors.Errorf("Unknown mode: %s", mode)
   586  	}
   587  	return serviceMode, nil
   588  }
   589  
   590  func convertDNSConfig(DNS []string, DNSSearch []string) (*swarm.DNSConfig, error) {
   591  	if DNS != nil || DNSSearch != nil {
   592  		return &swarm.DNSConfig{
   593  			Nameservers: DNS,
   594  			Search:      DNSSearch,
   595  		}, nil
   596  	}
   597  	return nil, nil
   598  }
   599  
   600  func convertCredentialSpec(spec composetypes.CredentialSpecConfig) (*swarm.CredentialSpec, error) {
   601  	if spec.File == "" && spec.Registry == "" {
   602  		return nil, nil
   603  	}
   604  	if spec.File != "" && spec.Registry != "" {
   605  		return nil, errors.New("Invalid credential spec - must provide one of `File` or `Registry`")
   606  	}
   607  	swarmCredSpec := swarm.CredentialSpec(spec)
   608  	return &swarmCredSpec, nil
   609  }