github.com/xiaobinqt/libcompose@v1.1.0/docker/service/convert.go (about)

     1  package service
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"github.com/docker/cli/opts"
     8  	"github.com/docker/docker/api/types/container"
     9  	"github.com/docker/docker/api/types/network"
    10  	"github.com/docker/docker/api/types/strslice"
    11  	"github.com/docker/go-connections/nat"
    12  	"github.com/docker/go-units"
    13  	"github.com/xiaobinqt/libcompose/config"
    14  	composeclient "github.com/xiaobinqt/libcompose/docker/client"
    15  	composecontainer "github.com/xiaobinqt/libcompose/docker/container"
    16  	"github.com/xiaobinqt/libcompose/project"
    17  	"github.com/xiaobinqt/libcompose/utils"
    18  	"golang.org/x/net/context"
    19  )
    20  
    21  // ConfigWrapper wraps Config, HostConfig and NetworkingConfig for a container.
    22  type ConfigWrapper struct {
    23  	Config           *container.Config
    24  	HostConfig       *container.HostConfig
    25  	NetworkingConfig *network.NetworkingConfig
    26  }
    27  
    28  // Filter filters the specified string slice with the specified function.
    29  func Filter(vs []string, f func(string) bool) []string {
    30  	r := make([]string, 0, len(vs))
    31  	for _, v := range vs {
    32  		if f(v) {
    33  			r = append(r, v)
    34  		}
    35  	}
    36  	return r
    37  }
    38  
    39  func toMap(vs []string) map[string]struct{} {
    40  	m := map[string]struct{}{}
    41  	for _, v := range vs {
    42  		if v != "" {
    43  			m[v] = struct{}{}
    44  		}
    45  	}
    46  	return m
    47  }
    48  
    49  func isBind(s string) bool {
    50  	return strings.ContainsRune(s, ':')
    51  }
    52  
    53  func isVolume(s string) bool {
    54  	return !isBind(s)
    55  }
    56  
    57  // ConvertToAPI converts a service configuration to a docker API container configuration.
    58  func ConvertToAPI(serviceConfig *config.ServiceConfig, ctx project.Context, clientFactory composeclient.Factory) (*ConfigWrapper, error) {
    59  	config, hostConfig, err := Convert(serviceConfig, ctx, clientFactory)
    60  	if err != nil {
    61  		return nil, err
    62  	}
    63  
    64  	result := ConfigWrapper{
    65  		Config:     config,
    66  		HostConfig: hostConfig,
    67  	}
    68  	return &result, nil
    69  }
    70  
    71  func volumes(c *config.ServiceConfig, ctx project.Context) []string {
    72  	if c.Volumes == nil {
    73  		return []string{}
    74  	}
    75  	volumes := make([]string, len(c.Volumes.Volumes))
    76  	for _, v := range c.Volumes.Volumes {
    77  		vol := v
    78  		if len(ctx.ComposeFiles) > 0 && !project.IsNamedVolume(v.Source) {
    79  			sourceVol := ctx.ResourceLookup.ResolvePath(v.String(), ctx.ComposeFiles[0])
    80  			vol.Source = strings.SplitN(sourceVol, ":", 2)[0]
    81  		}
    82  		volumes = append(volumes, vol.String())
    83  	}
    84  	return volumes
    85  }
    86  
    87  func restartPolicy(c *config.ServiceConfig) (*container.RestartPolicy, error) {
    88  	restart, err := opts.ParseRestartPolicy(c.Restart)
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  	return &container.RestartPolicy{Name: restart.Name, MaximumRetryCount: restart.MaximumRetryCount}, nil
    93  }
    94  
    95  func ports(c *config.ServiceConfig) (map[nat.Port]struct{}, nat.PortMap, error) {
    96  	ports, binding, err := nat.ParsePortSpecs(c.Ports)
    97  	if err != nil {
    98  		return nil, nil, err
    99  	}
   100  
   101  	exPorts, _, err := nat.ParsePortSpecs(c.Expose)
   102  	if err != nil {
   103  		return nil, nil, err
   104  	}
   105  
   106  	for k, v := range exPorts {
   107  		ports[k] = v
   108  	}
   109  
   110  	exposedPorts := map[nat.Port]struct{}{}
   111  	for k, v := range ports {
   112  		exposedPorts[nat.Port(k)] = v
   113  	}
   114  
   115  	portBindings := nat.PortMap{}
   116  	for k, bv := range binding {
   117  		dcbs := make([]nat.PortBinding, len(bv))
   118  		for k, v := range bv {
   119  			dcbs[k] = nat.PortBinding{HostIP: v.HostIP, HostPort: v.HostPort}
   120  		}
   121  		portBindings[nat.Port(k)] = dcbs
   122  	}
   123  	return exposedPorts, portBindings, nil
   124  }
   125  
   126  // Convert converts a service configuration to an docker API structures (Config and HostConfig)
   127  func Convert(c *config.ServiceConfig, ctx project.Context, clientFactory composeclient.Factory) (*container.Config, *container.HostConfig, error) {
   128  	restartPolicy, err := restartPolicy(c)
   129  	if err != nil {
   130  		return nil, nil, err
   131  	}
   132  
   133  	exposedPorts, portBindings, err := ports(c)
   134  	if err != nil {
   135  		return nil, nil, err
   136  	}
   137  
   138  	deviceMappings, err := parseDevices(c.Devices)
   139  	if err != nil {
   140  		return nil, nil, err
   141  	}
   142  
   143  	var volumesFrom []string
   144  	if c.VolumesFrom != nil {
   145  		volumesFrom, err = getVolumesFrom(c.VolumesFrom, ctx.Project.ServiceConfigs, ctx.ProjectName)
   146  		if err != nil {
   147  			return nil, nil, err
   148  		}
   149  	}
   150  
   151  	vols := volumes(c, ctx)
   152  
   153  	config := &container.Config{
   154  		Entrypoint:   strslice.StrSlice(utils.CopySlice(c.Entrypoint)),
   155  		Hostname:     c.Hostname,
   156  		Domainname:   c.DomainName,
   157  		User:         c.User,
   158  		Env:          utils.CopySlice(c.Environment),
   159  		Cmd:          strslice.StrSlice(utils.CopySlice(c.Command)),
   160  		Image:        c.Image,
   161  		Labels:       utils.CopyMap(c.Labels),
   162  		ExposedPorts: exposedPorts,
   163  		Tty:          c.Tty,
   164  		OpenStdin:    c.StdinOpen,
   165  		WorkingDir:   c.WorkingDir,
   166  		Volumes:      toMap(Filter(vols, isVolume)),
   167  		MacAddress:   c.MacAddress,
   168  		StopSignal:   c.StopSignal,
   169  		StopTimeout:  utils.DurationStrToSecondsInt(c.StopGracePeriod),
   170  	}
   171  
   172  	ulimits := []*units.Ulimit{}
   173  	if c.Ulimits.Elements != nil {
   174  		for _, ulimit := range c.Ulimits.Elements {
   175  			ulimits = append(ulimits, &units.Ulimit{
   176  				Name: ulimit.Name,
   177  				Soft: ulimit.Soft,
   178  				Hard: ulimit.Hard,
   179  			})
   180  		}
   181  	}
   182  
   183  	memorySwappiness := int64(c.MemSwappiness)
   184  
   185  	resources := container.Resources{
   186  		CgroupParent:      c.CgroupParent,
   187  		Memory:            int64(c.MemLimit),
   188  		MemoryReservation: int64(c.MemReservation),
   189  		MemorySwap:        int64(c.MemSwapLimit),
   190  		MemorySwappiness:  &memorySwappiness,
   191  		CPUShares:         int64(c.CPUShares),
   192  		CPUQuota:          int64(c.CPUQuota),
   193  		CpusetCpus:        c.CPUSet,
   194  		Ulimits:           ulimits,
   195  		Devices:           deviceMappings,
   196  		OomKillDisable:    &c.OomKillDisable,
   197  	}
   198  
   199  	networkMode := c.NetworkMode
   200  	if c.NetworkMode == "" {
   201  		if c.Networks != nil && len(c.Networks.Networks) > 0 {
   202  			networkMode = c.Networks.Networks[0].RealName
   203  		}
   204  	} else {
   205  		switch {
   206  		case strings.HasPrefix(c.NetworkMode, "service:"):
   207  			serviceName := c.NetworkMode[8:]
   208  			if serviceConfig, ok := ctx.Project.ServiceConfigs.Get(serviceName); ok {
   209  				// FIXME(vdemeester) this is actually not right, should be fixed but not there
   210  				service, err := ctx.ServiceFactory.Create(ctx.Project, serviceName, serviceConfig)
   211  				if err != nil {
   212  					return nil, nil, err
   213  				}
   214  				containers, err := service.Containers(context.Background())
   215  				if err != nil {
   216  					return nil, nil, err
   217  				}
   218  				if len(containers) != 0 {
   219  					container := containers[0]
   220  					containerID := container.ID()
   221  					networkMode = "container:" + containerID
   222  				}
   223  				// FIXME(vdemeester) log/warn in case of len(containers) == 0
   224  			}
   225  		case strings.HasPrefix(c.NetworkMode, "container:"):
   226  			containerName := c.NetworkMode[10:]
   227  			client := clientFactory.Create(nil)
   228  			container, err := composecontainer.Get(context.Background(), client, containerName)
   229  			if err != nil {
   230  				return nil, nil, err
   231  			}
   232  			networkMode = "container:" + container.ID
   233  		default:
   234  			// do nothing :)
   235  		}
   236  	}
   237  
   238  	tmpfs := map[string]string{}
   239  	for _, path := range c.Tmpfs {
   240  		split := strings.SplitN(path, ":", 2)
   241  		if len(split) == 1 {
   242  			tmpfs[split[0]] = ""
   243  		} else if len(split) == 2 {
   244  			tmpfs[split[0]] = split[1]
   245  		}
   246  	}
   247  
   248  	hostConfig := &container.HostConfig{
   249  		VolumesFrom: volumesFrom,
   250  		CapAdd:      strslice.StrSlice(utils.CopySlice(c.CapAdd)),
   251  		CapDrop:     strslice.StrSlice(utils.CopySlice(c.CapDrop)),
   252  		GroupAdd:    c.GroupAdd,
   253  		ExtraHosts:  utils.CopySlice(c.ExtraHosts),
   254  		Privileged:  c.Privileged,
   255  		Binds:       Filter(vols, isBind),
   256  		DNS:         utils.CopySlice(c.DNS),
   257  		DNSOptions:  utils.CopySlice(c.DNSOpts),
   258  		DNSSearch:   utils.CopySlice(c.DNSSearch),
   259  		Isolation:   container.Isolation(c.Isolation),
   260  		LogConfig: container.LogConfig{
   261  			Type:   c.Logging.Driver,
   262  			Config: utils.CopyMap(c.Logging.Options),
   263  		},
   264  		NetworkMode:    container.NetworkMode(networkMode),
   265  		ReadonlyRootfs: c.ReadOnly,
   266  		OomScoreAdj:    int(c.OomScoreAdj),
   267  		PidMode:        container.PidMode(c.Pid),
   268  		UTSMode:        container.UTSMode(c.Uts),
   269  		IpcMode:        container.IpcMode(c.Ipc),
   270  		PortBindings:   portBindings,
   271  		RestartPolicy:  *restartPolicy,
   272  		ShmSize:        int64(c.ShmSize),
   273  		SecurityOpt:    utils.CopySlice(c.SecurityOpt),
   274  		Tmpfs:          tmpfs,
   275  		VolumeDriver:   c.VolumeDriver,
   276  		Resources:      resources,
   277  	}
   278  
   279  	if config.Labels == nil {
   280  		config.Labels = map[string]string{}
   281  	}
   282  
   283  	return config, hostConfig, nil
   284  }
   285  
   286  func getVolumesFrom(volumesFrom []string, serviceConfigs *config.ServiceConfigs, projectName string) ([]string, error) {
   287  	volumes := []string{}
   288  	for _, volumeFrom := range volumesFrom {
   289  		if serviceConfig, ok := serviceConfigs.Get(volumeFrom); ok {
   290  			// It's a service - Use the first one
   291  			name := fmt.Sprintf("%s_%s_1", projectName, volumeFrom)
   292  			// If a container name is specified, use that instead
   293  			if serviceConfig.ContainerName != "" {
   294  				name = serviceConfig.ContainerName
   295  			}
   296  			volumes = append(volumes, name)
   297  		} else {
   298  			volumes = append(volumes, volumeFrom)
   299  		}
   300  	}
   301  	return volumes, nil
   302  }
   303  
   304  func parseDevices(devices []string) ([]container.DeviceMapping, error) {
   305  	// parse device mappings
   306  	deviceMappings := []container.DeviceMapping{}
   307  	for _, device := range devices {
   308  		v, err := parseDevice(device)
   309  		if err != nil {
   310  			return nil, err
   311  		}
   312  		deviceMappings = append(deviceMappings, container.DeviceMapping{
   313  			PathOnHost:        v.PathOnHost,
   314  			PathInContainer:   v.PathInContainer,
   315  			CgroupPermissions: v.CgroupPermissions,
   316  		})
   317  	}
   318  
   319  	return deviceMappings, nil
   320  }
   321  
   322  // parseDevice parses a device mapping string to a container.DeviceMapping struct
   323  // FIXME(vdemeester) de-duplicate this by re-exporting it in docker/docker
   324  func parseDevice(device string) (container.DeviceMapping, error) {
   325  	src := ""
   326  	dst := ""
   327  	permissions := "rwm"
   328  	arr := strings.Split(device, ":")
   329  	switch len(arr) {
   330  	case 3:
   331  		permissions = arr[2]
   332  		fallthrough
   333  	case 2:
   334  		if validDeviceMode(arr[1]) {
   335  			permissions = arr[1]
   336  		} else {
   337  			dst = arr[1]
   338  		}
   339  		fallthrough
   340  	case 1:
   341  		src = arr[0]
   342  	default:
   343  		return container.DeviceMapping{}, fmt.Errorf("invalid device specification: %s", device)
   344  	}
   345  
   346  	if dst == "" {
   347  		dst = src
   348  	}
   349  
   350  	deviceMapping := container.DeviceMapping{
   351  		PathOnHost:        src,
   352  		PathInContainer:   dst,
   353  		CgroupPermissions: permissions,
   354  	}
   355  	return deviceMapping, nil
   356  }
   357  
   358  // validDeviceMode checks if the mode for device is valid or not.
   359  // Valid mode is a composition of r (read), w (write), and m (mknod).
   360  func validDeviceMode(mode string) bool {
   361  	var legalDeviceMode = map[rune]bool{
   362  		'r': true,
   363  		'w': true,
   364  		'm': true,
   365  	}
   366  	if mode == "" {
   367  		return false
   368  	}
   369  	for _, c := range mode {
   370  		if !legalDeviceMode[c] {
   371  			return false
   372  		}
   373  		legalDeviceMode[c] = false
   374  	}
   375  	return true
   376  }