376 lines
10 KiB
Go
376 lines
10 KiB
Go
package service
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/docker/cli/opts"
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/api/types/network"
|
|
"github.com/docker/docker/api/types/strslice"
|
|
"github.com/docker/go-connections/nat"
|
|
"github.com/docker/go-units"
|
|
"github.com/docker/libcompose/config"
|
|
composeclient "github.com/docker/libcompose/docker/client"
|
|
composecontainer "github.com/docker/libcompose/docker/container"
|
|
"github.com/docker/libcompose/project"
|
|
"github.com/docker/libcompose/utils"
|
|
"golang.org/x/net/context"
|
|
)
|
|
|
|
// ConfigWrapper wraps Config, HostConfig and NetworkingConfig for a container.
|
|
type ConfigWrapper struct {
|
|
Config *container.Config
|
|
HostConfig *container.HostConfig
|
|
NetworkingConfig *network.NetworkingConfig
|
|
}
|
|
|
|
// Filter filters the specified string slice with the specified function.
|
|
func Filter(vs []string, f func(string) bool) []string {
|
|
r := make([]string, 0, len(vs))
|
|
for _, v := range vs {
|
|
if f(v) {
|
|
r = append(r, v)
|
|
}
|
|
}
|
|
return r
|
|
}
|
|
|
|
func toMap(vs []string) map[string]struct{} {
|
|
m := map[string]struct{}{}
|
|
for _, v := range vs {
|
|
if v != "" {
|
|
m[v] = struct{}{}
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
func isBind(s string) bool {
|
|
return strings.ContainsRune(s, ':')
|
|
}
|
|
|
|
func isVolume(s string) bool {
|
|
return !isBind(s)
|
|
}
|
|
|
|
// ConvertToAPI converts a service configuration to a docker API container configuration.
|
|
func ConvertToAPI(serviceConfig *config.ServiceConfig, ctx project.Context, clientFactory composeclient.Factory) (*ConfigWrapper, error) {
|
|
config, hostConfig, err := Convert(serviceConfig, ctx, clientFactory)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := ConfigWrapper{
|
|
Config: config,
|
|
HostConfig: hostConfig,
|
|
}
|
|
return &result, nil
|
|
}
|
|
|
|
func volumes(c *config.ServiceConfig, ctx project.Context) []string {
|
|
if c.Volumes == nil {
|
|
return []string{}
|
|
}
|
|
volumes := make([]string, len(c.Volumes.Volumes))
|
|
for _, v := range c.Volumes.Volumes {
|
|
vol := v
|
|
if len(ctx.ComposeFiles) > 0 && !project.IsNamedVolume(v.Source) {
|
|
sourceVol := ctx.ResourceLookup.ResolvePath(v.String(), ctx.ComposeFiles[0])
|
|
vol.Source = strings.SplitN(sourceVol, ":", 2)[0]
|
|
}
|
|
volumes = append(volumes, vol.String())
|
|
}
|
|
return volumes
|
|
}
|
|
|
|
func restartPolicy(c *config.ServiceConfig) (*container.RestartPolicy, error) {
|
|
restart, err := opts.ParseRestartPolicy(c.Restart)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &container.RestartPolicy{Name: restart.Name, MaximumRetryCount: restart.MaximumRetryCount}, nil
|
|
}
|
|
|
|
func ports(c *config.ServiceConfig) (map[nat.Port]struct{}, nat.PortMap, error) {
|
|
ports, binding, err := nat.ParsePortSpecs(c.Ports)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
exPorts, _, err := nat.ParsePortSpecs(c.Expose)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
for k, v := range exPorts {
|
|
ports[k] = v
|
|
}
|
|
|
|
exposedPorts := map[nat.Port]struct{}{}
|
|
for k, v := range ports {
|
|
exposedPorts[nat.Port(k)] = v
|
|
}
|
|
|
|
portBindings := nat.PortMap{}
|
|
for k, bv := range binding {
|
|
dcbs := make([]nat.PortBinding, len(bv))
|
|
for k, v := range bv {
|
|
dcbs[k] = nat.PortBinding{HostIP: v.HostIP, HostPort: v.HostPort}
|
|
}
|
|
portBindings[nat.Port(k)] = dcbs
|
|
}
|
|
return exposedPorts, portBindings, nil
|
|
}
|
|
|
|
// Convert converts a service configuration to an docker API structures (Config and HostConfig)
|
|
func Convert(c *config.ServiceConfig, ctx project.Context, clientFactory composeclient.Factory) (*container.Config, *container.HostConfig, error) {
|
|
restartPolicy, err := restartPolicy(c)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
exposedPorts, portBindings, err := ports(c)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
deviceMappings, err := parseDevices(c.Devices)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
var volumesFrom []string
|
|
if c.VolumesFrom != nil {
|
|
volumesFrom, err = getVolumesFrom(c.VolumesFrom, ctx.Project.ServiceConfigs, ctx.ProjectName)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
|
|
vols := volumes(c, ctx)
|
|
|
|
config := &container.Config{
|
|
Entrypoint: strslice.StrSlice(utils.CopySlice(c.Entrypoint)),
|
|
Hostname: c.Hostname,
|
|
Domainname: c.DomainName,
|
|
User: c.User,
|
|
Env: utils.CopySlice(c.Environment),
|
|
Cmd: strslice.StrSlice(utils.CopySlice(c.Command)),
|
|
Image: c.Image,
|
|
Labels: utils.CopyMap(c.Labels),
|
|
ExposedPorts: exposedPorts,
|
|
Tty: c.Tty,
|
|
OpenStdin: c.StdinOpen,
|
|
WorkingDir: c.WorkingDir,
|
|
Volumes: toMap(Filter(vols, isVolume)),
|
|
MacAddress: c.MacAddress,
|
|
StopSignal: c.StopSignal,
|
|
StopTimeout: utils.DurationStrToSecondsInt(c.StopGracePeriod),
|
|
}
|
|
|
|
ulimits := []*units.Ulimit{}
|
|
if c.Ulimits.Elements != nil {
|
|
for _, ulimit := range c.Ulimits.Elements {
|
|
ulimits = append(ulimits, &units.Ulimit{
|
|
Name: ulimit.Name,
|
|
Soft: ulimit.Soft,
|
|
Hard: ulimit.Hard,
|
|
})
|
|
}
|
|
}
|
|
|
|
memorySwappiness := int64(c.MemSwappiness)
|
|
|
|
resources := container.Resources{
|
|
CgroupParent: c.CgroupParent,
|
|
Memory: int64(c.MemLimit),
|
|
MemoryReservation: int64(c.MemReservation),
|
|
MemorySwap: int64(c.MemSwapLimit),
|
|
MemorySwappiness: &memorySwappiness,
|
|
CPUShares: int64(c.CPUShares),
|
|
CPUQuota: int64(c.CPUQuota),
|
|
CpusetCpus: c.CPUSet,
|
|
Ulimits: ulimits,
|
|
Devices: deviceMappings,
|
|
OomKillDisable: &c.OomKillDisable,
|
|
}
|
|
|
|
networkMode := c.NetworkMode
|
|
if c.NetworkMode == "" {
|
|
if c.Networks != nil && len(c.Networks.Networks) > 0 {
|
|
networkMode = c.Networks.Networks[0].RealName
|
|
}
|
|
} else {
|
|
switch {
|
|
case strings.HasPrefix(c.NetworkMode, "service:"):
|
|
serviceName := c.NetworkMode[8:]
|
|
if serviceConfig, ok := ctx.Project.ServiceConfigs.Get(serviceName); ok {
|
|
// FIXME(vdemeester) this is actually not right, should be fixed but not there
|
|
service, err := ctx.ServiceFactory.Create(ctx.Project, serviceName, serviceConfig)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
containers, err := service.Containers(context.Background())
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if len(containers) != 0 {
|
|
container := containers[0]
|
|
containerID := container.ID()
|
|
networkMode = "container:" + containerID
|
|
}
|
|
// FIXME(vdemeester) log/warn in case of len(containers) == 0
|
|
}
|
|
case strings.HasPrefix(c.NetworkMode, "container:"):
|
|
containerName := c.NetworkMode[10:]
|
|
client := clientFactory.Create(nil)
|
|
container, err := composecontainer.Get(context.Background(), client, containerName)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
networkMode = "container:" + container.ID
|
|
default:
|
|
// do nothing :)
|
|
}
|
|
}
|
|
|
|
tmpfs := map[string]string{}
|
|
for _, path := range c.Tmpfs {
|
|
split := strings.SplitN(path, ":", 2)
|
|
if len(split) == 1 {
|
|
tmpfs[split[0]] = ""
|
|
} else if len(split) == 2 {
|
|
tmpfs[split[0]] = split[1]
|
|
}
|
|
}
|
|
|
|
hostConfig := &container.HostConfig{
|
|
VolumesFrom: volumesFrom,
|
|
CapAdd: strslice.StrSlice(utils.CopySlice(c.CapAdd)),
|
|
CapDrop: strslice.StrSlice(utils.CopySlice(c.CapDrop)),
|
|
GroupAdd: c.GroupAdd,
|
|
ExtraHosts: utils.CopySlice(c.ExtraHosts),
|
|
Privileged: c.Privileged,
|
|
Binds: Filter(vols, isBind),
|
|
DNS: utils.CopySlice(c.DNS),
|
|
DNSOptions: utils.CopySlice(c.DNSOpts),
|
|
DNSSearch: utils.CopySlice(c.DNSSearch),
|
|
Isolation: container.Isolation(c.Isolation),
|
|
LogConfig: container.LogConfig{
|
|
Type: c.Logging.Driver,
|
|
Config: utils.CopyMap(c.Logging.Options),
|
|
},
|
|
NetworkMode: container.NetworkMode(networkMode),
|
|
ReadonlyRootfs: c.ReadOnly,
|
|
OomScoreAdj: int(c.OomScoreAdj),
|
|
PidMode: container.PidMode(c.Pid),
|
|
UTSMode: container.UTSMode(c.Uts),
|
|
IpcMode: container.IpcMode(c.Ipc),
|
|
PortBindings: portBindings,
|
|
RestartPolicy: *restartPolicy,
|
|
ShmSize: int64(c.ShmSize),
|
|
SecurityOpt: utils.CopySlice(c.SecurityOpt),
|
|
Tmpfs: tmpfs,
|
|
VolumeDriver: c.VolumeDriver,
|
|
Resources: resources,
|
|
}
|
|
|
|
if config.Labels == nil {
|
|
config.Labels = map[string]string{}
|
|
}
|
|
|
|
return config, hostConfig, nil
|
|
}
|
|
|
|
func getVolumesFrom(volumesFrom []string, serviceConfigs *config.ServiceConfigs, projectName string) ([]string, error) {
|
|
volumes := []string{}
|
|
for _, volumeFrom := range volumesFrom {
|
|
if serviceConfig, ok := serviceConfigs.Get(volumeFrom); ok {
|
|
// It's a service - Use the first one
|
|
name := fmt.Sprintf("%s_%s_1", projectName, volumeFrom)
|
|
// If a container name is specified, use that instead
|
|
if serviceConfig.ContainerName != "" {
|
|
name = serviceConfig.ContainerName
|
|
}
|
|
volumes = append(volumes, name)
|
|
} else {
|
|
volumes = append(volumes, volumeFrom)
|
|
}
|
|
}
|
|
return volumes, nil
|
|
}
|
|
|
|
func parseDevices(devices []string) ([]container.DeviceMapping, error) {
|
|
// parse device mappings
|
|
deviceMappings := []container.DeviceMapping{}
|
|
for _, device := range devices {
|
|
v, err := parseDevice(device)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
deviceMappings = append(deviceMappings, container.DeviceMapping{
|
|
PathOnHost: v.PathOnHost,
|
|
PathInContainer: v.PathInContainer,
|
|
CgroupPermissions: v.CgroupPermissions,
|
|
})
|
|
}
|
|
|
|
return deviceMappings, nil
|
|
}
|
|
|
|
// parseDevice parses a device mapping string to a container.DeviceMapping struct
|
|
// FIXME(vdemeester) de-duplicate this by re-exporting it in docker/docker
|
|
func parseDevice(device string) (container.DeviceMapping, error) {
|
|
src := ""
|
|
dst := ""
|
|
permissions := "rwm"
|
|
arr := strings.Split(device, ":")
|
|
switch len(arr) {
|
|
case 3:
|
|
permissions = arr[2]
|
|
fallthrough
|
|
case 2:
|
|
if validDeviceMode(arr[1]) {
|
|
permissions = arr[1]
|
|
} else {
|
|
dst = arr[1]
|
|
}
|
|
fallthrough
|
|
case 1:
|
|
src = arr[0]
|
|
default:
|
|
return container.DeviceMapping{}, fmt.Errorf("invalid device specification: %s", device)
|
|
}
|
|
|
|
if dst == "" {
|
|
dst = src
|
|
}
|
|
|
|
deviceMapping := container.DeviceMapping{
|
|
PathOnHost: src,
|
|
PathInContainer: dst,
|
|
CgroupPermissions: permissions,
|
|
}
|
|
return deviceMapping, nil
|
|
}
|
|
|
|
// validDeviceMode checks if the mode for device is valid or not.
|
|
// Valid mode is a composition of r (read), w (write), and m (mknod).
|
|
func validDeviceMode(mode string) bool {
|
|
var legalDeviceMode = map[rune]bool{
|
|
'r': true,
|
|
'w': true,
|
|
'm': true,
|
|
}
|
|
if mode == "" {
|
|
return false
|
|
}
|
|
for _, c := range mode {
|
|
if !legalDeviceMode[c] {
|
|
return false
|
|
}
|
|
legalDeviceMode[c] = false
|
|
}
|
|
return true
|
|
}
|