From 85ec3124610ebb0e3b314fb73ac7625f749be141 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Fri, 13 Nov 2020 09:38:21 +0100 Subject: [PATCH] convert compose model into moby API types to prepare "up" local implementation Signed-off-by: Nicolas De Loof --- local/backend.go | 24 +-- local/compose.go | 352 ++++++++++++++++++++++++++++++++++++++++++++ local/containers.go | 28 ++-- 3 files changed, 382 insertions(+), 22 deletions(-) create mode 100644 local/compose.go diff --git a/local/backend.go b/local/backend.go index 134fddb00..b72658de8 100644 --- a/local/backend.go +++ b/local/backend.go @@ -20,7 +20,6 @@ package local import ( "context" - "github.com/docker/docker/client" "github.com/docker/compose-cli/api/compose" @@ -53,22 +52,23 @@ func service(ctx context.Context) (backend.Service, error) { }, nil } -func (cs *containerService) ContainerService() containers.Service { - return cs +func (s *local) ContainerService() containers.Service { + return s.containerService } -func (ms *local) ComposeService() compose.Service { +func (s *local) ComposeService() compose.Service { + return s +} + +func (s *local) SecretsService() secrets.Service { return nil } -func (ms *local) SecretsService() secrets.Service { +func (s *local) VolumeService() volumes.Service { + return s.volumeService +} + +func (s *local) ResourceService() resources.Service { return nil } -func (vs *volumeService) VolumeService() volumes.Service { - return vs -} - -func (ms *local) ResourceService() resources.Service { - return nil -} diff --git a/local/compose.go b/local/compose.go new file mode 100644 index 000000000..8cb57de2e --- /dev/null +++ b/local/compose.go @@ -0,0 +1,352 @@ +// +build local + +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package local + +import ( + "context" + "fmt" + "github.com/compose-spec/compose-go/types" + "github.com/docker/compose-cli/api/compose" + moby "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/api/types/strslice" + "github.com/docker/docker/errdefs" + "github.com/docker/go-connections/nat" + "github.com/pkg/errors" + "io" + "path/filepath" + "strings" +) + +func (s *local) Up(ctx context.Context, project *types.Project, detach bool) error { + for k, network := range project.Networks { + if !network.External.External { + network.Name = fmt.Sprintf("%s_%s", project.Name, k) + project.Networks[k] = network + } + err := s.ensureNetwork(ctx, network) + if err != nil { + return err + } + } + + for _, service := range project.Services { + containerConfig, hostConfig, networkingConfig, err := getContainerCreateOptions(project, service) + if err != nil { + return err + } + name := fmt.Sprintf("%s_%s", project.Name, service.Name) + id, err := s.create(ctx, containerConfig, hostConfig, networkingConfig, name) + if err != nil { + return err + } + for net, _ := range service.Networks { + name := fmt.Sprintf("%s_%s", project.Name, net) + err = s.connectContainerToNetwork(ctx, id, service.Name, name) + if err != nil { + return err + } + } + err = s.containerService.apiClient.ContainerStart(ctx, id, moby.ContainerStartOptions{}) + if err != nil { + return err + } + } + return nil +} + + +func (s *local) Down(ctx context.Context, projectName string) error { + panic("implement me") +} + +func (s *local) Logs(ctx context.Context, projectName string, w io.Writer) error { + panic("implement me") +} + +func (s *local) Ps(ctx context.Context, projectName string) ([]compose.ServiceStatus, error) { + panic("implement me") +} + +func (s *local) List(ctx context.Context, projectName string) ([]compose.Stack, error) { + panic("implement me") +} + +func (s *local) Convert(ctx context.Context, project *types.Project, format string) ([]byte, error) { + panic("implement me") +} + + +func getContainerCreateOptions(p *types.Project, s types.ServiceConfig) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) { + labels := map[string]string{ + "com.docker.compose.project": p.Name, + "com.docker.compose.service": s.Name, + } + + var ( + runCmd strslice.StrSlice + entrypoint strslice.StrSlice + ) + if len(s.Command) > 0 { + runCmd = strslice.StrSlice(s.Command) + } + if len(s.Entrypoint) > 0 { + entrypoint = strslice.StrSlice(s.Entrypoint) + } + image := s.Image + if s.Image == "" { + image = fmt.Sprintf("%s_%s", p.Name, s.Name) + } + + var ( + tty = s.Tty + stdinOpen = s.StdinOpen + attachStdin = false + ) + + containerConfig := container.Config{ + Hostname: s.Hostname, + Domainname: s.DomainName, + User: s.User, + ExposedPorts: buildContainerPorts(s), + Tty: tty, + OpenStdin: stdinOpen, + StdinOnce: true, + AttachStdin: attachStdin, + AttachStderr: true, + AttachStdout: true, + Cmd: runCmd, + Image: image, + WorkingDir: s.WorkingDir, + Entrypoint: entrypoint, + NetworkDisabled: s.NetworkMode == "disabled", + MacAddress: s.MacAddress, + Labels: labels, + StopSignal: s.StopSignal, + // Env: s.Environment, FIXME conversion + // Healthcheck: s.HealthCheck, FIXME conversion + // Volumes: // FIXME unclear to me the overlap with HostConfig.Mounts + // StopTimeout: s.StopGracePeriod FIXME conversion + } + + mountOptions, err := buildContainerMountOptions(p, s) + if err != nil { + return nil, nil, nil, err + } + + bindings, err := buildContainerBindingOptions(s) + if err != nil { + return nil, nil, nil, err + } + + networkMode := getNetworkMode(p, s) + hostConfig := container.HostConfig{ + Mounts: mountOptions, + CapAdd: strslice.StrSlice(s.CapAdd), + CapDrop: strslice.StrSlice(s.CapDrop), + NetworkMode: networkMode, + Init: s.Init, + ReadonlyRootfs: s.ReadOnly, + // ShmSize: , TODO + Sysctls: s.Sysctls, + PortBindings: bindings, + } + + networkConfig := buildDefaultNetworkConfig(s, networkMode) + return &containerConfig, &hostConfig, networkConfig, nil +} + +func buildContainerPorts(s types.ServiceConfig) nat.PortSet { + ports := nat.PortSet{} + for _, p := range s.Ports { + p := nat.Port(fmt.Sprintf("%d/%s", p.Target, p.Protocol)) + ports[p] = struct{}{} + } + return ports +} + +func buildContainerBindingOptions(s types.ServiceConfig) (nat.PortMap, error) { + bindings := nat.PortMap{} + for _, port := range s.Ports { + p := nat.Port(fmt.Sprintf("%d/%s", port.Target, port.Protocol)) + bind := []nat.PortBinding{} + binding := nat.PortBinding{} + if port.Published > 0 { + binding.HostPort = fmt.Sprint(port.Published) + } + bind = append(bind, binding) + bindings[p] = bind + } + return bindings, nil +} + +func buildContainerMountOptions(p *types.Project, s types.ServiceConfig) ([]mount.Mount, error) { + mounts := []mount.Mount{} + + for _, v := range s.Volumes { + source := v.Source + if v.Type == "bind" && !filepath.IsAbs(source) { + // FIXME handle ~/ + source = filepath.Join(p.WorkingDir, source) + } + + mounts = append(mounts, mount.Mount{ + Type: mount.Type(v.Type), + Source: source, + Target: v.Target, + ReadOnly: v.ReadOnly, + Consistency: mount.Consistency(v.Consistency), + BindOptions: buildBindOption(v.Bind), + VolumeOptions: buildVolumeOptions(v.Volume), + TmpfsOptions: buildTmpfsOptions(v.Tmpfs), + }) + } + return mounts, nil +} + +func buildBindOption(bind *types.ServiceVolumeBind) *mount.BindOptions { + if bind == nil { + return nil + } + return &mount.BindOptions{ + Propagation: mount.Propagation(bind.Propagation), + // NonRecursive: false, FIXME missing from model ? + } +} + +func buildVolumeOptions(vol *types.ServiceVolumeVolume) *mount.VolumeOptions { + if vol == nil { + return nil + } + return &mount.VolumeOptions{ + NoCopy: vol.NoCopy, + // Labels: , // FIXME missing from model ? + // DriverConfig: , // FIXME missing from model ? + } +} + +func buildTmpfsOptions(tmpfs *types.ServiceVolumeTmpfs) *mount.TmpfsOptions { + if tmpfs == nil { + return nil + } + return &mount.TmpfsOptions{ + SizeBytes: tmpfs.Size, + // Mode: , // FIXME missing from model ? + } +} + +func buildDefaultNetworkConfig(s types.ServiceConfig, networkMode container.NetworkMode) *network.NetworkingConfig { + config := map[string]*network.EndpointSettings{} + net := string(networkMode) + config[net] = &network.EndpointSettings{ + Aliases: getAliases(s, s.Networks[net]), + } + + return &network.NetworkingConfig{ + EndpointsConfig: config, + } +} + +func getAliases(s types.ServiceConfig, c *types.ServiceNetworkConfig) []string { + aliases := []string{s.Name} + if c != nil { + aliases = append(aliases, c.Aliases...) + } + return aliases +} + +func getNetworkMode(p *types.Project, service types.ServiceConfig) container.NetworkMode { + mode := service.NetworkMode + if mode == "" { + if len(p.Networks) > 0 { + for name := range getNetworksForService(service) { + return container.NetworkMode(p.Networks[name].Name) + } + } + return container.NetworkMode("none") + } + + /// FIXME incomplete implementation + if strings.HasPrefix(mode, "service:") { + panic("Not yet implemented") + } + if strings.HasPrefix(mode, "container:") { + panic("Not yet implemented") + } + + return container.NetworkMode(mode) +} + +func getNetworksForService(s types.ServiceConfig) map[string]*types.ServiceNetworkConfig { + if len(s.Networks) > 0 { + return s.Networks + } + return map[string]*types.ServiceNetworkConfig{"default": nil} +} + + +func (s *local) ensureNetwork(ctx context.Context, n types.NetworkConfig) error { + _, err := s.containerService.apiClient.NetworkInspect(ctx, n.Name, moby.NetworkInspectOptions{}) + if err != nil { + if errdefs.IsNotFound(err) { + createOpts := moby.NetworkCreate{ + // TODO NameSpace Labels + Labels: n.Labels, + Driver: n.Driver, + Options: n.DriverOpts, + Internal: n.Internal, + Attachable: n.Attachable, + } + + if n.Ipam.Driver != "" || len(n.Ipam.Config) > 0 { + createOpts.IPAM = &network.IPAM{} + } + + if n.Ipam.Driver != "" { + createOpts.IPAM.Driver = n.Ipam.Driver + } + + for _, ipamConfig := range n.Ipam.Config { + config := network.IPAMConfig{ + Subnet: ipamConfig.Subnet, + } + createOpts.IPAM.Config = append(createOpts.IPAM.Config, config) + } + if _, err := s.containerService.apiClient.NetworkCreate(context.Background(), n.Name, createOpts); err != nil { + return errors.Wrapf(err, "failed to create network %s", n.Name) + } + return nil + } else { + return err + } + } + return nil +} + +func (s *local) connectContainerToNetwork(ctx context.Context, id string, service string, n string) error { + err := s.containerService.apiClient.NetworkConnect(ctx, n, id, &network.EndpointSettings{ + Aliases: []string{service}, + }) + if err != nil { + return err + } + return nil +} diff --git a/local/containers.go b/local/containers.go index 327a991e5..99cda41e2 100644 --- a/local/containers.go +++ b/local/containers.go @@ -21,6 +21,7 @@ package local import ( "bufio" "context" + "github.com/docker/docker/api/types/network" "io" "strings" "time" @@ -134,13 +135,21 @@ func (cs *containerService) Run(ctx context.Context, r containers.ContainerConfi }, } - created, err := cs.apiClient.ContainerCreate(ctx, containerConfig, hostConfig, nil, r.ID) + id, err := cs.create(ctx, containerConfig, hostConfig, nil, r.ID) + if err != nil { + return err + } + return cs.apiClient.ContainerStart(ctx, id, types.ContainerStartOptions{}) +} + +func (cs *containerService) create(ctx context.Context, containerConfig *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, name string) (string, error) { + created, err := cs.apiClient.ContainerCreate(ctx, containerConfig, hostConfig, networkingConfig, name) if err != nil { if client.IsErrNotFound(err) { - io, err := cs.apiClient.ImagePull(ctx, r.Image, types.ImagePullOptions{}) + io, err := cs.apiClient.ImagePull(ctx, containerConfig.Image, types.ImagePullOptions{}) if err != nil { - return err + return "", err } scanner := bufio.NewScanner(io) @@ -149,21 +158,20 @@ func (cs *containerService) Run(ctx context.Context, r containers.ContainerConfi } if err = scanner.Err(); err != nil { - return err + return "", err } if err = io.Close(); err != nil { - return err + return "", err } - created, err = cs.apiClient.ContainerCreate(ctx, containerConfig, hostConfig, nil, r.ID) + created, err = cs.apiClient.ContainerCreate(ctx, containerConfig, hostConfig, networkingConfig, name) if err != nil { - return err + return "", err } } else { - return err + return "", err } } - - return cs.apiClient.ContainerStart(ctx, created.ID, types.ContainerStartOptions{}) + return created.ID, nil } func (cs *containerService) Start(ctx context.Context, containerID string) error {