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 }