1/* 2 Copyright 2020 Docker Compose CLI authors 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15*/ 16 17package compose 18 19import ( 20 "bytes" 21 "context" 22 "encoding/json" 23 "fmt" 24 "io/ioutil" 25 "path" 26 "path/filepath" 27 "strconv" 28 "strings" 29 30 "github.com/compose-spec/compose-go/types" 31 moby "github.com/docker/docker/api/types" 32 "github.com/docker/docker/api/types/blkiodev" 33 "github.com/docker/docker/api/types/container" 34 "github.com/docker/docker/api/types/mount" 35 "github.com/docker/docker/api/types/network" 36 "github.com/docker/docker/api/types/strslice" 37 volume_api "github.com/docker/docker/api/types/volume" 38 "github.com/docker/docker/errdefs" 39 "github.com/docker/go-connections/nat" 40 "github.com/docker/go-units" 41 "github.com/pkg/errors" 42 "github.com/sirupsen/logrus" 43 44 "github.com/docker/compose/v2/pkg/api" 45 "github.com/docker/compose/v2/pkg/progress" 46 "github.com/docker/compose/v2/pkg/utils" 47) 48 49func (s *composeService) Create(ctx context.Context, project *types.Project, options api.CreateOptions) error { 50 return progress.Run(ctx, func(ctx context.Context) error { 51 return s.create(ctx, project, options) 52 }) 53} 54 55func (s *composeService) create(ctx context.Context, project *types.Project, options api.CreateOptions) error { 56 if len(options.Services) == 0 { 57 options.Services = project.ServiceNames() 58 } 59 60 var observedState Containers 61 observedState, err := s.getContainers(ctx, project.Name, oneOffInclude, true) 62 if err != nil { 63 return err 64 } 65 66 err = s.ensureImagesExists(ctx, project, options.QuietPull) 67 if err != nil { 68 return err 69 } 70 71 prepareNetworks(project) 72 73 err = prepareVolumes(project) 74 if err != nil { 75 return err 76 } 77 78 if err := s.ensureNetworks(ctx, project.Networks); err != nil { 79 return err 80 } 81 82 if err := s.ensureProjectVolumes(ctx, project); err != nil { 83 return err 84 } 85 86 allServices := project.AllServices() 87 allServiceNames := []string{} 88 for _, service := range allServices { 89 allServiceNames = append(allServiceNames, service.Name) 90 } 91 orphans := observedState.filter(isNotService(allServiceNames...)) 92 if len(orphans) > 0 && !options.IgnoreOrphans { 93 if options.RemoveOrphans { 94 w := progress.ContextWriter(ctx) 95 err := s.removeContainers(ctx, w, orphans, nil, false) 96 if err != nil { 97 return err 98 } 99 } else { 100 logrus.Warnf("Found orphan containers (%s) for this project. If "+ 101 "you removed or renamed this service in your compose "+ 102 "file, you can run this command with the "+ 103 "--remove-orphans flag to clean it up.", orphans.names()) 104 } 105 } 106 107 err = prepareServicesDependsOn(project) 108 if err != nil { 109 return err 110 } 111 112 return newConvergence(options.Services, observedState, s).apply(ctx, project, options) 113} 114 115func prepareVolumes(p *types.Project) error { 116 for i := range p.Services { 117 volumesFrom, dependServices, err := getVolumesFrom(p, p.Services[i].VolumesFrom) 118 if err != nil { 119 return err 120 } 121 p.Services[i].VolumesFrom = volumesFrom 122 if len(dependServices) > 0 { 123 if p.Services[i].DependsOn == nil { 124 p.Services[i].DependsOn = make(types.DependsOnConfig, len(dependServices)) 125 } 126 for _, service := range p.Services { 127 if utils.StringContains(dependServices, service.Name) { 128 p.Services[i].DependsOn[service.Name] = types.ServiceDependency{ 129 Condition: types.ServiceConditionStarted, 130 } 131 } 132 } 133 } 134 } 135 return nil 136} 137 138func prepareNetworks(project *types.Project) { 139 for k, network := range project.Networks { 140 network.Labels = network.Labels.Add(api.NetworkLabel, k) 141 network.Labels = network.Labels.Add(api.ProjectLabel, project.Name) 142 network.Labels = network.Labels.Add(api.VersionLabel, api.ComposeVersion) 143 project.Networks[k] = network 144 } 145} 146 147func prepareServicesDependsOn(p *types.Project) error { 148 for i, service := range p.Services { 149 var dependencies []string 150 networkDependency := getDependentServiceFromMode(service.NetworkMode) 151 if networkDependency != "" { 152 dependencies = append(dependencies, networkDependency) 153 } 154 155 ipcDependency := getDependentServiceFromMode(service.Ipc) 156 if ipcDependency != "" { 157 dependencies = append(dependencies, ipcDependency) 158 } 159 160 pidDependency := getDependentServiceFromMode(service.Pid) 161 if pidDependency != "" { 162 dependencies = append(dependencies, pidDependency) 163 } 164 165 for _, vol := range service.VolumesFrom { 166 spec := strings.Split(vol, ":") 167 if len(spec) == 0 { 168 continue 169 } 170 if spec[0] == "container" { 171 continue 172 } 173 dependencies = append(dependencies, spec[0]) 174 } 175 176 if len(dependencies) == 0 { 177 continue 178 } 179 if service.DependsOn == nil { 180 service.DependsOn = make(types.DependsOnConfig) 181 } 182 deps, err := p.GetServices(dependencies...) 183 if err != nil { 184 return err 185 } 186 for _, d := range deps { 187 if _, ok := service.DependsOn[d.Name]; !ok { 188 service.DependsOn[d.Name] = types.ServiceDependency{ 189 Condition: types.ServiceConditionStarted, 190 } 191 } 192 } 193 p.Services[i] = service 194 } 195 return nil 196} 197 198func (s *composeService) ensureNetworks(ctx context.Context, networks types.Networks) error { 199 for _, network := range networks { 200 err := s.ensureNetwork(ctx, network) 201 if err != nil { 202 return err 203 } 204 } 205 return nil 206} 207 208func (s *composeService) ensureProjectVolumes(ctx context.Context, project *types.Project) error { 209 for k, volume := range project.Volumes { 210 volume.Labels = volume.Labels.Add(api.VolumeLabel, k) 211 volume.Labels = volume.Labels.Add(api.ProjectLabel, project.Name) 212 volume.Labels = volume.Labels.Add(api.VersionLabel, api.ComposeVersion) 213 err := s.ensureVolume(ctx, volume) 214 if err != nil { 215 return err 216 } 217 } 218 return nil 219} 220 221func getImageName(service types.ServiceConfig, projectName string) string { 222 imageName := service.Image 223 if imageName == "" { 224 imageName = projectName + "_" + service.Name 225 } 226 return imageName 227} 228 229func (s *composeService) getCreateOptions(ctx context.Context, p *types.Project, service types.ServiceConfig, 230 number int, inherit *moby.Container, autoRemove bool, attachStdin bool) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) { 231 232 labels, err := s.prepareLabels(p, service, number) 233 if err != nil { 234 return nil, nil, nil, err 235 } 236 237 var ( 238 runCmd strslice.StrSlice 239 entrypoint strslice.StrSlice 240 ) 241 if service.Command != nil { 242 runCmd = strslice.StrSlice(service.Command) 243 } 244 if service.Entrypoint != nil { 245 entrypoint = strslice.StrSlice(service.Entrypoint) 246 } 247 248 var ( 249 tty = service.Tty 250 stdinOpen = service.StdinOpen 251 ) 252 253 volumeMounts, binds, mounts, err := s.buildContainerVolumes(ctx, *p, service, inherit) 254 if err != nil { 255 return nil, nil, nil, err 256 } 257 258 proxyConfig := types.MappingWithEquals(s.configFile.ParseProxyConfig(s.apiClient.DaemonHost(), nil)) 259 env := proxyConfig.OverrideBy(service.Environment) 260 261 containerConfig := container.Config{ 262 Hostname: service.Hostname, 263 Domainname: service.DomainName, 264 User: service.User, 265 ExposedPorts: buildContainerPorts(service), 266 Tty: tty, 267 OpenStdin: stdinOpen, 268 StdinOnce: attachStdin && stdinOpen, 269 AttachStdin: attachStdin, 270 AttachStderr: true, 271 AttachStdout: true, 272 Cmd: runCmd, 273 Image: getImageName(service, p.Name), 274 WorkingDir: service.WorkingDir, 275 Entrypoint: entrypoint, 276 NetworkDisabled: service.NetworkMode == "disabled", 277 MacAddress: service.MacAddress, 278 Labels: labels, 279 StopSignal: service.StopSignal, 280 Env: ToMobyEnv(env), 281 Healthcheck: ToMobyHealthCheck(service.HealthCheck), 282 Volumes: volumeMounts, 283 StopTimeout: ToSeconds(service.StopGracePeriod), 284 } 285 286 portBindings := buildContainerPortBindingOptions(service) 287 288 resources := getDeployResources(service) 289 290 if service.NetworkMode == "" { 291 service.NetworkMode = getDefaultNetworkMode(p, service) 292 } 293 294 var networkConfig *network.NetworkingConfig 295 296 for _, id := range service.NetworksByPriority() { 297 net := p.Networks[id] 298 config := service.Networks[id] 299 var ipam *network.EndpointIPAMConfig 300 var ( 301 ipv4Address string 302 ipv6Address string 303 ) 304 if config != nil { 305 ipv4Address = config.Ipv4Address 306 ipv6Address = config.Ipv6Address 307 ipam = &network.EndpointIPAMConfig{ 308 IPv4Address: ipv4Address, 309 IPv6Address: ipv6Address, 310 } 311 } 312 networkConfig = &network.NetworkingConfig{ 313 EndpointsConfig: map[string]*network.EndpointSettings{ 314 net.Name: { 315 Aliases: getAliases(service, config), 316 IPAddress: ipv4Address, 317 IPv6Gateway: ipv6Address, 318 IPAMConfig: ipam, 319 }, 320 }, 321 } 322 break //nolint:staticcheck 323 } 324 325 tmpfs := map[string]string{} 326 for _, t := range service.Tmpfs { 327 if arr := strings.SplitN(t, ":", 2); len(arr) > 1 { 328 tmpfs[arr[0]] = arr[1] 329 } else { 330 tmpfs[arr[0]] = "" 331 } 332 } 333 334 var logConfig container.LogConfig 335 if service.Logging != nil { 336 logConfig = container.LogConfig{ 337 Type: service.Logging.Driver, 338 Config: service.Logging.Options, 339 } 340 } 341 342 var volumesFrom []string 343 for _, v := range service.VolumesFrom { 344 if !strings.HasPrefix(v, "container:") { 345 return nil, nil, nil, fmt.Errorf("invalid volume_from: %s", v) 346 } 347 volumesFrom = append(volumesFrom, v[len("container:"):]) 348 } 349 350 securityOpts, err := parseSecurityOpts(p, service.SecurityOpt) 351 if err != nil { 352 return nil, nil, nil, err 353 } 354 hostConfig := container.HostConfig{ 355 AutoRemove: autoRemove, 356 Binds: binds, 357 Mounts: mounts, 358 CapAdd: strslice.StrSlice(service.CapAdd), 359 CapDrop: strslice.StrSlice(service.CapDrop), 360 NetworkMode: container.NetworkMode(service.NetworkMode), 361 Init: service.Init, 362 IpcMode: container.IpcMode(service.Ipc), 363 ReadonlyRootfs: service.ReadOnly, 364 RestartPolicy: getRestartPolicy(service), 365 ShmSize: int64(service.ShmSize), 366 Sysctls: service.Sysctls, 367 PortBindings: portBindings, 368 Resources: resources, 369 VolumeDriver: service.VolumeDriver, 370 VolumesFrom: volumesFrom, 371 DNS: service.DNS, 372 DNSSearch: service.DNSSearch, 373 DNSOptions: service.DNSOpts, 374 ExtraHosts: service.ExtraHosts, 375 SecurityOpt: securityOpts, 376 UsernsMode: container.UsernsMode(service.UserNSMode), 377 Privileged: service.Privileged, 378 PidMode: container.PidMode(service.Pid), 379 Tmpfs: tmpfs, 380 Isolation: container.Isolation(service.Isolation), 381 LogConfig: logConfig, 382 } 383 384 return &containerConfig, &hostConfig, networkConfig, nil 385} 386 387// copy/pasted from https://github.com/docker/cli/blob/9de1b162f/cli/command/container/opts.go#L673-L697 + RelativePath 388// TODO find so way to share this code with docker/cli 389func parseSecurityOpts(p *types.Project, securityOpts []string) ([]string, error) { 390 for key, opt := range securityOpts { 391 con := strings.SplitN(opt, "=", 2) 392 if len(con) == 1 && con[0] != "no-new-privileges" { 393 if strings.Contains(opt, ":") { 394 con = strings.SplitN(opt, ":", 2) 395 } else { 396 return securityOpts, errors.Errorf("Invalid security-opt: %q", opt) 397 } 398 } 399 if con[0] == "seccomp" && con[1] != "unconfined" { 400 f, err := ioutil.ReadFile(p.RelativePath(con[1])) 401 if err != nil { 402 return securityOpts, errors.Errorf("opening seccomp profile (%s) failed: %v", con[1], err) 403 } 404 b := bytes.NewBuffer(nil) 405 if err := json.Compact(b, f); err != nil { 406 return securityOpts, errors.Errorf("compacting json for seccomp profile (%s) failed: %v", con[1], err) 407 } 408 securityOpts[key] = fmt.Sprintf("seccomp=%s", b.Bytes()) 409 } 410 } 411 412 return securityOpts, nil 413} 414 415func (s *composeService) prepareLabels(p *types.Project, service types.ServiceConfig, number int) (map[string]string, error) { 416 labels := map[string]string{} 417 for k, v := range service.Labels { 418 labels[k] = v 419 } 420 421 labels[api.ProjectLabel] = p.Name 422 labels[api.ServiceLabel] = service.Name 423 labels[api.VersionLabel] = api.ComposeVersion 424 if _, ok := service.Labels[api.OneoffLabel]; !ok { 425 labels[api.OneoffLabel] = "False" 426 } 427 428 hash, err := ServiceHash(service) 429 if err != nil { 430 return nil, err 431 } 432 433 labels[api.ConfigHashLabel] = hash 434 labels[api.WorkingDirLabel] = p.WorkingDir 435 labels[api.ConfigFilesLabel] = strings.Join(p.ComposeFiles, ",") 436 labels[api.ContainerNumberLabel] = strconv.Itoa(number) 437 var dependencies []string 438 for s := range service.DependsOn { 439 dependencies = append(dependencies, s) 440 } 441 labels[api.DependenciesLabel] = strings.Join(dependencies, ",") 442 return labels, nil 443} 444 445func getDefaultNetworkMode(project *types.Project, service types.ServiceConfig) string { 446 mode := "none" 447 if len(project.Networks) > 0 { 448 for name := range getNetworksForService(service) { 449 mode = project.Networks[name].Name 450 break 451 } 452 } 453 return mode 454} 455 456func getRestartPolicy(service types.ServiceConfig) container.RestartPolicy { 457 var restart container.RestartPolicy 458 if service.Restart != "" { 459 split := strings.Split(service.Restart, ":") 460 var attempts int 461 if len(split) > 1 { 462 attempts, _ = strconv.Atoi(split[1]) 463 } 464 restart = container.RestartPolicy{ 465 Name: split[0], 466 MaximumRetryCount: attempts, 467 } 468 } 469 if service.Deploy != nil && service.Deploy.RestartPolicy != nil { 470 policy := *service.Deploy.RestartPolicy 471 var attempts int 472 if policy.MaxAttempts != nil { 473 attempts = int(*policy.MaxAttempts) 474 } 475 restart = container.RestartPolicy{ 476 Name: policy.Condition, 477 MaximumRetryCount: attempts, 478 } 479 } 480 return restart 481} 482 483func getDeployResources(s types.ServiceConfig) container.Resources { 484 var swappiness *int64 485 if s.MemSwappiness != 0 { 486 val := int64(s.MemSwappiness) 487 swappiness = &val 488 } 489 resources := container.Resources{ 490 CgroupParent: s.CgroupParent, 491 Memory: int64(s.MemLimit), 492 MemorySwap: int64(s.MemSwapLimit), 493 MemorySwappiness: swappiness, 494 MemoryReservation: int64(s.MemReservation), 495 CPUCount: s.CPUCount, 496 CPUPeriod: s.CPUPeriod, 497 CPUQuota: s.CPUQuota, 498 CPURealtimePeriod: s.CPURTPeriod, 499 CPURealtimeRuntime: s.CPURTRuntime, 500 CPUShares: s.CPUShares, 501 CPUPercent: int64(s.CPUS * 100), 502 CpusetCpus: s.CPUSet, 503 } 504 505 setBlkio(s.BlkioConfig, &resources) 506 507 if s.Deploy != nil { 508 setLimits(s.Deploy.Resources.Limits, &resources) 509 setReservations(s.Deploy.Resources.Reservations, &resources) 510 } 511 512 for _, device := range s.Devices { 513 // FIXME should use docker/cli parseDevice, unfortunately private 514 src := "" 515 dst := "" 516 permissions := "rwm" 517 arr := strings.Split(device, ":") 518 switch len(arr) { 519 case 3: 520 permissions = arr[2] 521 fallthrough 522 case 2: 523 dst = arr[1] 524 fallthrough 525 case 1: 526 src = arr[0] 527 } 528 resources.Devices = append(resources.Devices, container.DeviceMapping{ 529 PathOnHost: src, 530 PathInContainer: dst, 531 CgroupPermissions: permissions, 532 }) 533 } 534 535 for name, u := range s.Ulimits { 536 soft := u.Single 537 if u.Soft != 0 { 538 soft = u.Soft 539 } 540 hard := u.Single 541 if u.Hard != 0 { 542 hard = u.Hard 543 } 544 resources.Ulimits = append(resources.Ulimits, &units.Ulimit{ 545 Name: name, 546 Hard: int64(hard), 547 Soft: int64(soft), 548 }) 549 } 550 return resources 551} 552 553func setReservations(reservations *types.Resource, resources *container.Resources) { 554 if reservations == nil { 555 return 556 } 557 for _, device := range reservations.Devices { 558 resources.DeviceRequests = append(resources.DeviceRequests, container.DeviceRequest{ 559 Capabilities: [][]string{device.Capabilities}, 560 Count: int(device.Count), 561 DeviceIDs: device.IDs, 562 Driver: device.Driver, 563 }) 564 } 565} 566 567func setLimits(limits *types.Resource, resources *container.Resources) { 568 if limits == nil { 569 return 570 } 571 if limits.MemoryBytes != 0 { 572 resources.Memory = int64(limits.MemoryBytes) 573 } 574 if limits.NanoCPUs != "" { 575 i, _ := strconv.ParseInt(limits.NanoCPUs, 10, 64) 576 resources.NanoCPUs = i 577 } 578} 579 580func setBlkio(blkio *types.BlkioConfig, resources *container.Resources) { 581 if blkio == nil { 582 return 583 } 584 resources.BlkioWeight = blkio.Weight 585 for _, b := range blkio.WeightDevice { 586 resources.BlkioWeightDevice = append(resources.BlkioWeightDevice, &blkiodev.WeightDevice{ 587 Path: b.Path, 588 Weight: b.Weight, 589 }) 590 } 591 for _, b := range blkio.DeviceReadBps { 592 resources.BlkioDeviceReadBps = append(resources.BlkioDeviceReadBps, &blkiodev.ThrottleDevice{ 593 Path: b.Path, 594 Rate: b.Rate, 595 }) 596 } 597 for _, b := range blkio.DeviceReadIOps { 598 resources.BlkioDeviceReadIOps = append(resources.BlkioDeviceReadIOps, &blkiodev.ThrottleDevice{ 599 Path: b.Path, 600 Rate: b.Rate, 601 }) 602 } 603 for _, b := range blkio.DeviceWriteBps { 604 resources.BlkioDeviceWriteBps = append(resources.BlkioDeviceWriteBps, &blkiodev.ThrottleDevice{ 605 Path: b.Path, 606 Rate: b.Rate, 607 }) 608 } 609 for _, b := range blkio.DeviceWriteIOps { 610 resources.BlkioDeviceWriteIOps = append(resources.BlkioDeviceWriteIOps, &blkiodev.ThrottleDevice{ 611 Path: b.Path, 612 Rate: b.Rate, 613 }) 614 } 615} 616 617func buildContainerPorts(s types.ServiceConfig) nat.PortSet { 618 ports := nat.PortSet{} 619 for _, s := range s.Expose { 620 p := nat.Port(s) 621 ports[p] = struct{}{} 622 } 623 for _, p := range s.Ports { 624 p := nat.Port(fmt.Sprintf("%d/%s", p.Target, p.Protocol)) 625 ports[p] = struct{}{} 626 } 627 return ports 628} 629 630func buildContainerPortBindingOptions(s types.ServiceConfig) nat.PortMap { 631 bindings := nat.PortMap{} 632 for _, port := range s.Ports { 633 p := nat.Port(fmt.Sprintf("%d/%s", port.Target, port.Protocol)) 634 bind := bindings[p] 635 binding := nat.PortBinding{ 636 HostIP: port.HostIP, 637 } 638 if port.Published > 0 { 639 binding.HostPort = fmt.Sprint(port.Published) 640 } 641 bind = append(bind, binding) 642 bindings[p] = bind 643 } 644 return bindings 645} 646 647func getVolumesFrom(project *types.Project, volumesFrom []string) ([]string, []string, error) { 648 var volumes = []string{} 649 var services = []string{} 650 // parse volumes_from 651 if len(volumesFrom) == 0 { 652 return volumes, services, nil 653 } 654 for _, vol := range volumesFrom { 655 spec := strings.Split(vol, ":") 656 if len(spec) == 0 { 657 continue 658 } 659 if spec[0] == "container" { 660 volumes = append(volumes, strings.Join(spec[1:], ":")) 661 continue 662 } 663 serviceName := spec[0] 664 services = append(services, serviceName) 665 service, err := project.GetService(serviceName) 666 if err != nil { 667 return nil, nil, err 668 } 669 670 firstContainer := getContainerName(project.Name, service, 1) 671 v := fmt.Sprintf("container:%s", firstContainer) 672 if len(spec) > 2 { 673 v = fmt.Sprintf("container:%s:%s", firstContainer, strings.Join(spec[1:], ":")) 674 } 675 volumes = append(volumes, v) 676 } 677 return volumes, services, nil 678 679} 680 681func getDependentServiceFromMode(mode string) string { 682 if strings.HasPrefix(mode, types.NetworkModeServicePrefix) { 683 return mode[len(types.NetworkModeServicePrefix):] 684 } 685 return "" 686} 687 688func (s *composeService) buildContainerVolumes(ctx context.Context, p types.Project, service types.ServiceConfig, 689 inherit *moby.Container) (map[string]struct{}, []string, []mount.Mount, error) { 690 var mounts = []mount.Mount{} 691 692 image := getImageName(service, p.Name) 693 imgInspect, _, err := s.apiClient.ImageInspectWithRaw(ctx, image) 694 if err != nil { 695 return nil, nil, nil, err 696 } 697 698 mountOptions, err := buildContainerMountOptions(p, service, imgInspect, inherit) 699 if err != nil { 700 return nil, nil, nil, err 701 } 702 703 volumeMounts := map[string]struct{}{} 704 binds := []string{} 705MOUNTS: 706 for _, m := range mountOptions { 707 volumeMounts[m.Target] = struct{}{} 708 // `Bind` API is used when host path need to be created if missing, `Mount` is preferred otherwise 709 if m.Type == mount.TypeBind || m.Type == mount.TypeNamedPipe { 710 for _, v := range service.Volumes { 711 if v.Target == m.Target && v.Bind != nil && v.Bind.CreateHostPath { 712 mode := "rw" 713 if m.ReadOnly { 714 mode = "ro" 715 } 716 binds = append(binds, fmt.Sprintf("%s:%s:%s", m.Source, m.Target, mode)) 717 continue MOUNTS 718 } 719 } 720 } 721 mounts = append(mounts, m) 722 } 723 return volumeMounts, binds, mounts, nil 724} 725 726func buildContainerMountOptions(p types.Project, s types.ServiceConfig, img moby.ImageInspect, inherit *moby.Container) ([]mount.Mount, error) { 727 var mounts = map[string]mount.Mount{} 728 if inherit != nil { 729 for _, m := range inherit.Mounts { 730 if m.Type == "tmpfs" { 731 continue 732 } 733 src := m.Source 734 if m.Type == "volume" { 735 src = m.Name 736 } 737 m.Destination = path.Clean(m.Destination) 738 739 if img.Config != nil { 740 if _, ok := img.Config.Volumes[m.Destination]; ok { 741 // inherit previous container's anonymous volume 742 mounts[m.Destination] = mount.Mount{ 743 Type: m.Type, 744 Source: src, 745 Target: m.Destination, 746 ReadOnly: !m.RW, 747 } 748 } 749 } 750 for i, v := range s.Volumes { 751 if v.Target != m.Destination { 752 continue 753 } 754 if v.Source == "" { 755 // inherit previous container's anonymous volume 756 mounts[m.Destination] = mount.Mount{ 757 Type: m.Type, 758 Source: src, 759 Target: m.Destination, 760 ReadOnly: !m.RW, 761 } 762 // Avoid mount to be later re-defined 763 l := len(s.Volumes) - 1 764 s.Volumes[i] = s.Volumes[l] 765 s.Volumes = s.Volumes[:l] 766 } 767 } 768 } 769 } 770 771 mounts, err := fillBindMounts(p, s, mounts) 772 if err != nil { 773 return nil, err 774 } 775 776 values := make([]mount.Mount, 0, len(mounts)) 777 for _, v := range mounts { 778 values = append(values, v) 779 } 780 return values, nil 781} 782 783func fillBindMounts(p types.Project, s types.ServiceConfig, m map[string]mount.Mount) (map[string]mount.Mount, error) { 784 for _, v := range s.Volumes { 785 bindMount, err := buildMount(p, v) 786 if err != nil { 787 return nil, err 788 } 789 m[bindMount.Target] = bindMount 790 } 791 792 secrets, err := buildContainerSecretMounts(p, s) 793 if err != nil { 794 return nil, err 795 } 796 for _, s := range secrets { 797 if _, found := m[s.Target]; found { 798 continue 799 } 800 m[s.Target] = s 801 } 802 803 configs, err := buildContainerConfigMounts(p, s) 804 if err != nil { 805 return nil, err 806 } 807 for _, c := range configs { 808 if _, found := m[c.Target]; found { 809 continue 810 } 811 m[c.Target] = c 812 } 813 return m, nil 814} 815 816func buildContainerConfigMounts(p types.Project, s types.ServiceConfig) ([]mount.Mount, error) { 817 var mounts = map[string]mount.Mount{} 818 819 configsBaseDir := "/" 820 for _, config := range s.Configs { 821 target := config.Target 822 if config.Target == "" { 823 target = configsBaseDir + config.Source 824 } else if !isUnixAbs(config.Target) { 825 target = configsBaseDir + config.Target 826 } 827 828 definedConfig := p.Configs[config.Source] 829 if definedConfig.External.External { 830 return nil, fmt.Errorf("unsupported external config %s", definedConfig.Name) 831 } 832 833 bindMount, err := buildMount(p, types.ServiceVolumeConfig{ 834 Type: types.VolumeTypeBind, 835 Source: definedConfig.File, 836 Target: target, 837 ReadOnly: true, 838 }) 839 if err != nil { 840 return nil, err 841 } 842 mounts[target] = bindMount 843 } 844 values := make([]mount.Mount, 0, len(mounts)) 845 for _, v := range mounts { 846 values = append(values, v) 847 } 848 return values, nil 849} 850 851func buildContainerSecretMounts(p types.Project, s types.ServiceConfig) ([]mount.Mount, error) { 852 var mounts = map[string]mount.Mount{} 853 854 secretsDir := "/run/secrets/" 855 for _, secret := range s.Secrets { 856 target := secret.Target 857 if secret.Target == "" { 858 target = secretsDir + secret.Source 859 } else if !isUnixAbs(secret.Target) { 860 target = secretsDir + secret.Target 861 } 862 863 definedSecret := p.Secrets[secret.Source] 864 if definedSecret.External.External { 865 return nil, fmt.Errorf("unsupported external secret %s", definedSecret.Name) 866 } 867 868 mount, err := buildMount(p, types.ServiceVolumeConfig{ 869 Type: types.VolumeTypeBind, 870 Source: definedSecret.File, 871 Target: target, 872 ReadOnly: true, 873 }) 874 if err != nil { 875 return nil, err 876 } 877 mounts[target] = mount 878 } 879 values := make([]mount.Mount, 0, len(mounts)) 880 for _, v := range mounts { 881 values = append(values, v) 882 } 883 return values, nil 884} 885 886func isUnixAbs(path string) bool { 887 return strings.HasPrefix(path, "/") 888} 889 890func buildMount(project types.Project, volume types.ServiceVolumeConfig) (mount.Mount, error) { 891 source := volume.Source 892 // on windows, filepath.IsAbs(source) is false for unix style abs path like /var/run/docker.sock. 893 // do not replace these with filepath.Abs(source) that will include a default drive. 894 if volume.Type == types.VolumeTypeBind && !filepath.IsAbs(source) && !strings.HasPrefix(source, "/") { 895 // volume source has already been prefixed with workdir if required, by compose-go project loader 896 var err error 897 source, err = filepath.Abs(source) 898 if err != nil { 899 return mount.Mount{}, err 900 } 901 } 902 if volume.Type == types.VolumeTypeVolume { 903 if volume.Source != "" { 904 pVolume, ok := project.Volumes[volume.Source] 905 if ok { 906 source = pVolume.Name 907 } 908 } 909 } 910 911 bind, vol, tmpfs := buildMountOptions(volume) 912 913 volume.Target = path.Clean(volume.Target) 914 915 return mount.Mount{ 916 Type: mount.Type(volume.Type), 917 Source: source, 918 Target: volume.Target, 919 ReadOnly: volume.ReadOnly, 920 Consistency: mount.Consistency(volume.Consistency), 921 BindOptions: bind, 922 VolumeOptions: vol, 923 TmpfsOptions: tmpfs, 924 }, nil 925} 926 927func buildMountOptions(volume types.ServiceVolumeConfig) (*mount.BindOptions, *mount.VolumeOptions, *mount.TmpfsOptions) { 928 switch volume.Type { 929 case "bind": 930 if volume.Volume != nil { 931 logrus.Warnf("mount of type `bind` should not define `volume` option") 932 } 933 if volume.Tmpfs != nil { 934 logrus.Warnf("mount of type `tmpfs` should not define `tmpfs` option") 935 } 936 return buildBindOption(volume.Bind), nil, nil 937 case "volume": 938 if volume.Bind != nil { 939 logrus.Warnf("mount of type `volume` should not define `bind` option") 940 } 941 if volume.Tmpfs != nil { 942 logrus.Warnf("mount of type `volume` should not define `tmpfs` option") 943 } 944 return nil, buildVolumeOptions(volume.Volume), nil 945 case "tmpfs": 946 if volume.Bind != nil { 947 logrus.Warnf("mount of type `tmpfs` should not define `bind` option") 948 } 949 if volume.Tmpfs != nil { 950 logrus.Warnf("mount of type `tmpfs` should not define `volumeZ` option") 951 } 952 return nil, nil, buildTmpfsOptions(volume.Tmpfs) 953 } 954 return nil, nil, nil 955} 956 957func buildBindOption(bind *types.ServiceVolumeBind) *mount.BindOptions { 958 if bind == nil { 959 return nil 960 } 961 return &mount.BindOptions{ 962 Propagation: mount.Propagation(bind.Propagation), 963 // NonRecursive: false, FIXME missing from model ? 964 } 965} 966 967func buildVolumeOptions(vol *types.ServiceVolumeVolume) *mount.VolumeOptions { 968 if vol == nil { 969 return nil 970 } 971 return &mount.VolumeOptions{ 972 NoCopy: vol.NoCopy, 973 // Labels: , // FIXME missing from model ? 974 // DriverConfig: , // FIXME missing from model ? 975 } 976} 977 978func buildTmpfsOptions(tmpfs *types.ServiceVolumeTmpfs) *mount.TmpfsOptions { 979 if tmpfs == nil { 980 return nil 981 } 982 return &mount.TmpfsOptions{ 983 SizeBytes: int64(tmpfs.Size), 984 // Mode: , // FIXME missing from model ? 985 } 986} 987 988func getAliases(s types.ServiceConfig, c *types.ServiceNetworkConfig) []string { 989 aliases := []string{s.Name} 990 if c != nil { 991 aliases = append(aliases, c.Aliases...) 992 } 993 return aliases 994} 995 996func getNetworksForService(s types.ServiceConfig) map[string]*types.ServiceNetworkConfig { 997 if len(s.Networks) > 0 { 998 return s.Networks 999 } 1000 if s.NetworkMode != "" { 1001 return nil 1002 } 1003 return map[string]*types.ServiceNetworkConfig{"default": nil} 1004} 1005 1006func (s *composeService) ensureNetwork(ctx context.Context, n types.NetworkConfig) error { 1007 _, err := s.apiClient.NetworkInspect(ctx, n.Name, moby.NetworkInspectOptions{}) 1008 if err != nil { 1009 if errdefs.IsNotFound(err) { 1010 if n.External.External { 1011 return fmt.Errorf("network %s declared as external, but could not be found", n.Name) 1012 } 1013 var ipam *network.IPAM 1014 if n.Ipam.Config != nil { 1015 var config []network.IPAMConfig 1016 for _, pool := range n.Ipam.Config { 1017 config = append(config, network.IPAMConfig{ 1018 Subnet: pool.Subnet, 1019 IPRange: pool.IPRange, 1020 Gateway: pool.Gateway, 1021 AuxAddress: pool.AuxiliaryAddresses, 1022 }) 1023 } 1024 ipam = &network.IPAM{ 1025 Driver: n.Ipam.Driver, 1026 Config: config, 1027 } 1028 } 1029 createOpts := moby.NetworkCreate{ 1030 // TODO NameSpace Labels 1031 Labels: n.Labels, 1032 Driver: n.Driver, 1033 Options: n.DriverOpts, 1034 Internal: n.Internal, 1035 Attachable: n.Attachable, 1036 IPAM: ipam, 1037 } 1038 1039 if n.Ipam.Driver != "" || len(n.Ipam.Config) > 0 { 1040 createOpts.IPAM = &network.IPAM{} 1041 } 1042 1043 if n.Ipam.Driver != "" { 1044 createOpts.IPAM.Driver = n.Ipam.Driver 1045 } 1046 1047 for _, ipamConfig := range n.Ipam.Config { 1048 config := network.IPAMConfig{ 1049 Subnet: ipamConfig.Subnet, 1050 } 1051 createOpts.IPAM.Config = append(createOpts.IPAM.Config, config) 1052 } 1053 networkEventName := fmt.Sprintf("Network %s", n.Name) 1054 w := progress.ContextWriter(ctx) 1055 w.Event(progress.CreatingEvent(networkEventName)) 1056 if _, err := s.apiClient.NetworkCreate(ctx, n.Name, createOpts); err != nil { 1057 w.Event(progress.ErrorEvent(networkEventName)) 1058 return errors.Wrapf(err, "failed to create network %s", n.Name) 1059 } 1060 w.Event(progress.CreatedEvent(networkEventName)) 1061 return nil 1062 } 1063 return err 1064 } 1065 return nil 1066} 1067 1068func (s *composeService) removeNetwork(ctx context.Context, networkID string, networkName string) error { 1069 w := progress.ContextWriter(ctx) 1070 eventName := fmt.Sprintf("Network %s", networkName) 1071 w.Event(progress.RemovingEvent(eventName)) 1072 1073 if err := s.apiClient.NetworkRemove(ctx, networkID); err != nil { 1074 w.Event(progress.ErrorEvent(eventName)) 1075 return errors.Wrapf(err, fmt.Sprintf("failed to remove network %s", networkID)) 1076 } 1077 1078 w.Event(progress.RemovedEvent(eventName)) 1079 return nil 1080} 1081 1082func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeConfig) error { 1083 // TODO could identify volume by label vs name 1084 _, err := s.apiClient.VolumeInspect(ctx, volume.Name) 1085 if err != nil { 1086 if !errdefs.IsNotFound(err) { 1087 return err 1088 } 1089 eventName := fmt.Sprintf("Volume %q", volume.Name) 1090 w := progress.ContextWriter(ctx) 1091 w.Event(progress.CreatingEvent(eventName)) 1092 _, err := s.apiClient.VolumeCreate(ctx, volume_api.VolumeCreateBody{ 1093 Labels: volume.Labels, 1094 Name: volume.Name, 1095 Driver: volume.Driver, 1096 DriverOpts: volume.DriverOpts, 1097 }) 1098 if err != nil { 1099 w.Event(progress.ErrorEvent(eventName)) 1100 return err 1101 } 1102 w.Event(progress.CreatedEvent(eventName)) 1103 } 1104 return nil 1105} 1106