github.com/yogeshlonkar/moby@v1.13.2-0.20201203103638-c0b64beaea94/cli/compose/convert/service.go (about)

     1  package convert
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"sort"
     7  	"time"
     8  
     9  	"github.com/docker/docker/api/types"
    10  	"github.com/docker/docker/api/types/container"
    11  	"github.com/docker/docker/api/types/swarm"
    12  	servicecli "github.com/docker/docker/cli/command/service"
    13  	composetypes "github.com/docker/docker/cli/compose/types"
    14  	"github.com/docker/docker/client"
    15  	"github.com/docker/docker/opts"
    16  	runconfigopts "github.com/docker/docker/runconfig/opts"
    17  	"github.com/docker/go-connections/nat"
    18  )
    19  
    20  const defaultNetwork = "default"
    21  
    22  // Services from compose-file types to engine API types
    23  // TODO: fix secrets API so that SecretAPIClient is not required here
    24  func Services(
    25  	namespace Namespace,
    26  	config *composetypes.Config,
    27  	client client.SecretAPIClient,
    28  ) (map[string]swarm.ServiceSpec, error) {
    29  	result := make(map[string]swarm.ServiceSpec)
    30  
    31  	services := config.Services
    32  	volumes := config.Volumes
    33  	networks := config.Networks
    34  
    35  	for _, service := range services {
    36  
    37  		secrets, err := convertServiceSecrets(client, namespace, service.Secrets, config.Secrets)
    38  		if err != nil {
    39  			return nil, err
    40  		}
    41  		serviceSpec, err := convertService(namespace, service, networks, volumes, secrets)
    42  		if err != nil {
    43  			return nil, err
    44  		}
    45  		result[service.Name] = serviceSpec
    46  	}
    47  
    48  	return result, nil
    49  }
    50  
    51  func convertService(
    52  	namespace Namespace,
    53  	service composetypes.ServiceConfig,
    54  	networkConfigs map[string]composetypes.NetworkConfig,
    55  	volumes map[string]composetypes.VolumeConfig,
    56  	secrets []*swarm.SecretReference,
    57  ) (swarm.ServiceSpec, error) {
    58  	name := namespace.Scope(service.Name)
    59  
    60  	endpoint, err := convertEndpointSpec(service.Ports)
    61  	if err != nil {
    62  		return swarm.ServiceSpec{}, err
    63  	}
    64  
    65  	mode, err := convertDeployMode(service.Deploy.Mode, service.Deploy.Replicas)
    66  	if err != nil {
    67  		return swarm.ServiceSpec{}, err
    68  	}
    69  
    70  	mounts, err := Volumes(service.Volumes, volumes, namespace)
    71  	if err != nil {
    72  		// TODO: better error message (include service name)
    73  		return swarm.ServiceSpec{}, err
    74  	}
    75  
    76  	resources, err := convertResources(service.Deploy.Resources)
    77  	if err != nil {
    78  		return swarm.ServiceSpec{}, err
    79  	}
    80  
    81  	restartPolicy, err := convertRestartPolicy(
    82  		service.Restart, service.Deploy.RestartPolicy)
    83  	if err != nil {
    84  		return swarm.ServiceSpec{}, err
    85  	}
    86  
    87  	healthcheck, err := convertHealthcheck(service.HealthCheck)
    88  	if err != nil {
    89  		return swarm.ServiceSpec{}, err
    90  	}
    91  
    92  	networks, err := convertServiceNetworks(service.Networks, networkConfigs, namespace, service.Name)
    93  	if err != nil {
    94  		return swarm.ServiceSpec{}, err
    95  	}
    96  
    97  	var logDriver *swarm.Driver
    98  	if service.Logging != nil {
    99  		logDriver = &swarm.Driver{
   100  			Name:    service.Logging.Driver,
   101  			Options: service.Logging.Options,
   102  		}
   103  	}
   104  
   105  	serviceSpec := swarm.ServiceSpec{
   106  		Annotations: swarm.Annotations{
   107  			Name:   name,
   108  			Labels: AddStackLabel(namespace, service.Deploy.Labels),
   109  		},
   110  		TaskTemplate: swarm.TaskSpec{
   111  			ContainerSpec: swarm.ContainerSpec{
   112  				Image:           service.Image,
   113  				Command:         service.Entrypoint,
   114  				Args:            service.Command,
   115  				Hostname:        service.Hostname,
   116  				Hosts:           sortStrings(convertExtraHosts(service.ExtraHosts)),
   117  				Healthcheck:     healthcheck,
   118  				Env:             sortStrings(convertEnvironment(service.Environment)),
   119  				Labels:          AddStackLabel(namespace, service.Labels),
   120  				Dir:             service.WorkingDir,
   121  				User:            service.User,
   122  				Mounts:          mounts,
   123  				StopGracePeriod: service.StopGracePeriod,
   124  				TTY:             service.Tty,
   125  				OpenStdin:       service.StdinOpen,
   126  				Secrets:         secrets,
   127  			},
   128  			LogDriver:     logDriver,
   129  			Resources:     resources,
   130  			RestartPolicy: restartPolicy,
   131  			Placement: &swarm.Placement{
   132  				Constraints: service.Deploy.Placement.Constraints,
   133  			},
   134  		},
   135  		EndpointSpec: endpoint,
   136  		Mode:         mode,
   137  		Networks:     networks,
   138  		UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig),
   139  	}
   140  
   141  	return serviceSpec, nil
   142  }
   143  
   144  func sortStrings(strs []string) []string {
   145  	sort.Strings(strs)
   146  	return strs
   147  }
   148  
   149  type byNetworkTarget []swarm.NetworkAttachmentConfig
   150  
   151  func (a byNetworkTarget) Len() int           { return len(a) }
   152  func (a byNetworkTarget) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
   153  func (a byNetworkTarget) Less(i, j int) bool { return a[i].Target < a[j].Target }
   154  
   155  func convertServiceNetworks(
   156  	networks map[string]*composetypes.ServiceNetworkConfig,
   157  	networkConfigs networkMap,
   158  	namespace Namespace,
   159  	name string,
   160  ) ([]swarm.NetworkAttachmentConfig, error) {
   161  	if len(networks) == 0 {
   162  		networks = map[string]*composetypes.ServiceNetworkConfig{
   163  			defaultNetwork: {},
   164  		}
   165  	}
   166  
   167  	nets := []swarm.NetworkAttachmentConfig{}
   168  	for networkName, network := range networks {
   169  		networkConfig, ok := networkConfigs[networkName]
   170  		if !ok && networkName != defaultNetwork {
   171  			return []swarm.NetworkAttachmentConfig{}, fmt.Errorf(
   172  				"service %q references network %q, which is not declared", name, networkName)
   173  		}
   174  		var aliases []string
   175  		if network != nil {
   176  			aliases = network.Aliases
   177  		}
   178  		target := namespace.Scope(networkName)
   179  		if networkConfig.External.External {
   180  			target = networkConfig.External.Name
   181  		}
   182  		nets = append(nets, swarm.NetworkAttachmentConfig{
   183  			Target:  target,
   184  			Aliases: append(aliases, name),
   185  		})
   186  	}
   187  
   188  	sort.Sort(byNetworkTarget(nets))
   189  
   190  	return nets, nil
   191  }
   192  
   193  // TODO: fix secrets API so that SecretAPIClient is not required here
   194  func convertServiceSecrets(
   195  	client client.SecretAPIClient,
   196  	namespace Namespace,
   197  	secrets []composetypes.ServiceSecretConfig,
   198  	secretSpecs map[string]composetypes.SecretConfig,
   199  ) ([]*swarm.SecretReference, error) {
   200  	opts := []*types.SecretRequestOption{}
   201  	for _, secret := range secrets {
   202  		target := secret.Target
   203  		if target == "" {
   204  			target = secret.Source
   205  		}
   206  
   207  		source := namespace.Scope(secret.Source)
   208  		secretSpec := secretSpecs[secret.Source]
   209  		if secretSpec.External.External {
   210  			source = secretSpec.External.Name
   211  		}
   212  
   213  		uid := secret.UID
   214  		gid := secret.GID
   215  		if uid == "" {
   216  			uid = "0"
   217  		}
   218  		if gid == "" {
   219  			gid = "0"
   220  		}
   221  		mode := secret.Mode
   222  		if mode == nil {
   223  			mode = uint32Ptr(0444)
   224  		}
   225  
   226  		opts = append(opts, &types.SecretRequestOption{
   227  			Source: source,
   228  			Target: target,
   229  			UID:    uid,
   230  			GID:    gid,
   231  			Mode:   os.FileMode(*mode),
   232  		})
   233  	}
   234  
   235  	return servicecli.ParseSecrets(client, opts)
   236  }
   237  
   238  func uint32Ptr(value uint32) *uint32 {
   239  	return &value
   240  }
   241  
   242  func convertExtraHosts(extraHosts map[string]string) []string {
   243  	hosts := []string{}
   244  	for host, ip := range extraHosts {
   245  		hosts = append(hosts, fmt.Sprintf("%s %s", ip, host))
   246  	}
   247  	return hosts
   248  }
   249  
   250  func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container.HealthConfig, error) {
   251  	if healthcheck == nil {
   252  		return nil, nil
   253  	}
   254  	var (
   255  		err               error
   256  		timeout, interval time.Duration
   257  		retries           int
   258  	)
   259  	if healthcheck.Disable {
   260  		if len(healthcheck.Test) != 0 {
   261  			return nil, fmt.Errorf("test and disable can't be set at the same time")
   262  		}
   263  		return &container.HealthConfig{
   264  			Test: []string{"NONE"},
   265  		}, nil
   266  
   267  	}
   268  	if healthcheck.Timeout != "" {
   269  		timeout, err = time.ParseDuration(healthcheck.Timeout)
   270  		if err != nil {
   271  			return nil, err
   272  		}
   273  	}
   274  	if healthcheck.Interval != "" {
   275  		interval, err = time.ParseDuration(healthcheck.Interval)
   276  		if err != nil {
   277  			return nil, err
   278  		}
   279  	}
   280  	if healthcheck.Retries != nil {
   281  		retries = int(*healthcheck.Retries)
   282  	}
   283  	return &container.HealthConfig{
   284  		Test:     healthcheck.Test,
   285  		Timeout:  timeout,
   286  		Interval: interval,
   287  		Retries:  retries,
   288  	}, nil
   289  }
   290  
   291  func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) {
   292  	// TODO: log if restart is being ignored
   293  	if source == nil {
   294  		policy, err := runconfigopts.ParseRestartPolicy(restart)
   295  		if err != nil {
   296  			return nil, err
   297  		}
   298  		switch {
   299  		case policy.IsNone():
   300  			return nil, nil
   301  		case policy.IsAlways(), policy.IsUnlessStopped():
   302  			return &swarm.RestartPolicy{
   303  				Condition: swarm.RestartPolicyConditionAny,
   304  			}, nil
   305  		case policy.IsOnFailure():
   306  			attempts := uint64(policy.MaximumRetryCount)
   307  			return &swarm.RestartPolicy{
   308  				Condition:   swarm.RestartPolicyConditionOnFailure,
   309  				MaxAttempts: &attempts,
   310  			}, nil
   311  		default:
   312  			return nil, fmt.Errorf("unknown restart policy: %s", restart)
   313  		}
   314  	}
   315  	return &swarm.RestartPolicy{
   316  		Condition:   swarm.RestartPolicyCondition(source.Condition),
   317  		Delay:       source.Delay,
   318  		MaxAttempts: source.MaxAttempts,
   319  		Window:      source.Window,
   320  	}, nil
   321  }
   322  
   323  func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig {
   324  	if source == nil {
   325  		return nil
   326  	}
   327  	parallel := uint64(1)
   328  	if source.Parallelism != nil {
   329  		parallel = *source.Parallelism
   330  	}
   331  	return &swarm.UpdateConfig{
   332  		Parallelism:     parallel,
   333  		Delay:           source.Delay,
   334  		FailureAction:   source.FailureAction,
   335  		Monitor:         source.Monitor,
   336  		MaxFailureRatio: source.MaxFailureRatio,
   337  	}
   338  }
   339  
   340  func convertResources(source composetypes.Resources) (*swarm.ResourceRequirements, error) {
   341  	resources := &swarm.ResourceRequirements{}
   342  	var err error
   343  	if source.Limits != nil {
   344  		var cpus int64
   345  		if source.Limits.NanoCPUs != "" {
   346  			cpus, err = opts.ParseCPUs(source.Limits.NanoCPUs)
   347  			if err != nil {
   348  				return nil, err
   349  			}
   350  		}
   351  		resources.Limits = &swarm.Resources{
   352  			NanoCPUs:    cpus,
   353  			MemoryBytes: int64(source.Limits.MemoryBytes),
   354  		}
   355  	}
   356  	if source.Reservations != nil {
   357  		var cpus int64
   358  		if source.Reservations.NanoCPUs != "" {
   359  			cpus, err = opts.ParseCPUs(source.Reservations.NanoCPUs)
   360  			if err != nil {
   361  				return nil, err
   362  			}
   363  		}
   364  		resources.Reservations = &swarm.Resources{
   365  			NanoCPUs:    cpus,
   366  			MemoryBytes: int64(source.Reservations.MemoryBytes),
   367  		}
   368  	}
   369  	return resources, nil
   370  
   371  }
   372  
   373  type byPublishedPort []swarm.PortConfig
   374  
   375  func (a byPublishedPort) Len() int           { return len(a) }
   376  func (a byPublishedPort) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
   377  func (a byPublishedPort) Less(i, j int) bool { return a[i].PublishedPort < a[j].PublishedPort }
   378  
   379  func convertEndpointSpec(source []string) (*swarm.EndpointSpec, error) {
   380  	portConfigs := []swarm.PortConfig{}
   381  	ports, portBindings, err := nat.ParsePortSpecs(source)
   382  	if err != nil {
   383  		return nil, err
   384  	}
   385  
   386  	for port := range ports {
   387  		portConfigs = append(
   388  			portConfigs,
   389  			opts.ConvertPortToPortConfig(port, portBindings)...)
   390  	}
   391  
   392  	// Sorting to make sure these are always in the same order
   393  	sort.Sort(byPublishedPort(portConfigs))
   394  
   395  	return &swarm.EndpointSpec{Ports: portConfigs}, nil
   396  }
   397  
   398  func convertEnvironment(source map[string]string) []string {
   399  	var output []string
   400  
   401  	for name, value := range source {
   402  		output = append(output, fmt.Sprintf("%s=%s", name, value))
   403  	}
   404  
   405  	return output
   406  }
   407  
   408  func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) {
   409  	serviceMode := swarm.ServiceMode{}
   410  
   411  	switch mode {
   412  	case "global":
   413  		if replicas != nil {
   414  			return serviceMode, fmt.Errorf("replicas can only be used with replicated mode")
   415  		}
   416  		serviceMode.Global = &swarm.GlobalService{}
   417  	case "replicated", "":
   418  		serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas}
   419  	default:
   420  		return serviceMode, fmt.Errorf("Unknown mode: %s", mode)
   421  	}
   422  	return serviceMode, nil
   423  }