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