From 1eb40999e27a40393bc3bfe25bb1bfcd985c266f Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Tue, 8 Dec 2020 11:53:36 +0100 Subject: [PATCH] Split compose.go into command-focussed go files Signed-off-by: Nicolas De Loof --- local/backend.go | 9 +- local/compose.go | 1109 ---------------------- local/compose/attach.go | 129 +++ local/{ => compose}/build.go | 20 +- local/compose/compose.go | 66 ++ local/{ => compose}/compose_test.go | 4 +- local/{ => compose}/convergence.go | 11 +- local/compose/create.go | 411 ++++++++ local/{ => compose}/dependencies.go | 2 +- local/{ => compose}/dependencies_test.go | 5 +- local/compose/down.go | 149 +++ local/{ => compose}/labels.go | 2 +- local/compose/logs.go | 95 ++ local/compose/ls.go | 86 ++ local/compose/ps.go | 85 ++ local/compose/pull.go | 158 +++ local/compose/push.go | 137 +++ local/compose/start.go | 48 + local/{ => compose}/util.go | 2 +- local/container.go | 62 -- local/containers.go | 11 +- local/moby/container.go | 75 ++ local/{ => moby}/convert.go | 26 +- local/{ => moby}/convert_test.go | 8 +- 24 files changed, 1501 insertions(+), 1209 deletions(-) delete mode 100644 local/compose.go create mode 100644 local/compose/attach.go rename local/{ => compose}/build.go (90%) create mode 100644 local/compose/compose.go rename local/{ => compose}/compose_test.go (98%) rename local/{ => compose}/convergence.go (97%) create mode 100644 local/compose/create.go rename local/{ => compose}/dependencies.go (99%) rename local/{ => compose}/dependencies_test.go (99%) create mode 100644 local/compose/down.go rename local/{ => compose}/labels.go (99%) create mode 100644 local/compose/logs.go create mode 100644 local/compose/ls.go create mode 100644 local/compose/ps.go create mode 100644 local/compose/pull.go create mode 100644 local/compose/push.go create mode 100644 local/compose/start.go rename local/{ => compose}/util.go (98%) delete mode 100644 local/container.go create mode 100644 local/moby/container.go rename local/{ => moby}/convert.go (82%) rename local/{ => moby}/convert_test.go (95%) diff --git a/local/backend.go b/local/backend.go index de0380e1c..8e25d8b54 100644 --- a/local/backend.go +++ b/local/backend.go @@ -28,12 +28,13 @@ import ( "github.com/docker/compose-cli/api/volumes" "github.com/docker/compose-cli/backend" "github.com/docker/compose-cli/context/cloud" + local_compose "github.com/docker/compose-cli/local/compose" ) type local struct { - *containerService - *volumeService - *composeService + containerService *containerService + volumeService *volumeService + composeService compose.Service } func init() { @@ -49,7 +50,7 @@ func service(ctx context.Context) (backend.Service, error) { return &local{ containerService: &containerService{apiClient}, volumeService: &volumeService{apiClient}, - composeService: &composeService{apiClient}, + composeService: local_compose.NewComposeService(apiClient), }, nil } diff --git a/local/compose.go b/local/compose.go deleted file mode 100644 index 7a03c5354..000000000 --- a/local/compose.go +++ /dev/null @@ -1,1109 +0,0 @@ -/* - 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 ( - "bytes" - "context" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "path/filepath" - "sort" - "strconv" - "strings" - - "github.com/compose-spec/compose-go/cli" - "github.com/compose-spec/compose-go/types" - "github.com/docker/buildx/build" - cliconfig "github.com/docker/cli/cli/config" - "github.com/docker/distribution/reference" - moby "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/mount" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/strslice" - mobyvolume "github.com/docker/docker/api/types/volume" - "github.com/docker/docker/client" - "github.com/docker/docker/errdefs" - "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/pkg/stdcopy" - "github.com/docker/docker/registry" - "github.com/docker/go-connections/nat" - "github.com/pkg/errors" - "github.com/sanathkr/go-yaml" - "golang.org/x/sync/errgroup" - - "github.com/docker/compose-cli/api/compose" - "github.com/docker/compose-cli/config" - errdefs2 "github.com/docker/compose-cli/errdefs" - "github.com/docker/compose-cli/progress" -) - -type composeService struct { - apiClient *client.Client -} - -func (s *composeService) Build(ctx context.Context, project *types.Project) error { - opts := map[string]build.Options{} - for _, service := range project.Services { - if service.Build != nil { - imageName := service.Image - if imageName == "" { - imageName = project.Name + "_" + service.Name - } - opts[imageName] = s.toBuildOptions(service, project.WorkingDir, imageName) - } - } - - return s.build(ctx, project, opts) -} - -func (s *composeService) Push(ctx context.Context, project *types.Project) error { - configFile, err := cliconfig.Load(config.Dir(ctx)) - if err != nil { - return err - } - eg, ctx := errgroup.WithContext(ctx) - - info, err := s.apiClient.Info(ctx) - if err != nil { - return err - } - if info.IndexServerAddress == "" { - info.IndexServerAddress = registry.IndexServer - } - - for _, service := range project.Services { - if service.Build == nil { - continue - } - service := service - eg.Go(func() error { - w := progress.ContextWriter(ctx) - - ref, err := reference.ParseNormalizedNamed(service.Image) - if err != nil { - return err - } - - repoInfo, err := registry.ParseRepositoryInfo(ref) - if err != nil { - return err - } - - key := repoInfo.Index.Name - if repoInfo.Index.Official { - key = info.IndexServerAddress - } - authConfig, err := configFile.GetAuthConfig(key) - if err != nil { - return err - } - - buf, err := json.Marshal(authConfig) - if err != nil { - return err - } - - stream, err := s.apiClient.ImagePush(ctx, service.Image, moby.ImagePushOptions{ - RegistryAuth: base64.URLEncoding.EncodeToString(buf), - }) - if err != nil { - return err - } - dec := json.NewDecoder(stream) - for { - var jm jsonmessage.JSONMessage - if err := dec.Decode(&jm); err != nil { - if err == io.EOF { - break - } - return err - } - if jm.Error != nil { - return errors.New(jm.Error.Message) - } - toProgressEvent("Pushing "+service.Name, jm, w) - } - return nil - }) - } - return eg.Wait() -} - -func (s *composeService) Pull(ctx context.Context, project *types.Project) error { - configFile, err := cliconfig.Load(config.Dir(ctx)) - if err != nil { - return err - } - info, err := s.apiClient.Info(ctx) - if err != nil { - return err - } - - if info.IndexServerAddress == "" { - info.IndexServerAddress = registry.IndexServer - } - - w := progress.ContextWriter(ctx) - eg, ctx := errgroup.WithContext(ctx) - - for _, srv := range project.Services { - service := srv - eg.Go(func() error { - w.Event(progress.Event{ - ID: service.Name, - Status: progress.Working, - Text: "Pulling", - }) - ref, err := reference.ParseNormalizedNamed(service.Image) - if err != nil { - return err - } - - repoInfo, err := registry.ParseRepositoryInfo(ref) - if err != nil { - return err - } - - key := repoInfo.Index.Name - if repoInfo.Index.Official { - key = info.IndexServerAddress - } - - authConfig, err := configFile.GetAuthConfig(key) - if err != nil { - return err - } - - buf, err := json.Marshal(authConfig) - if err != nil { - return err - } - - stream, err := s.apiClient.ImagePull(ctx, service.Image, moby.ImagePullOptions{ - RegistryAuth: base64.URLEncoding.EncodeToString(buf), - }) - if err != nil { - w.Event(progress.Event{ - ID: service.Name, - Status: progress.Error, - Text: "Error", - }) - return err - } - - dec := json.NewDecoder(stream) - for { - var jm jsonmessage.JSONMessage - if err := dec.Decode(&jm); err != nil { - if err == io.EOF { - break - } - return err - } - if jm.Error != nil { - return errors.New(jm.Error.Message) - } - toPullProgressEvent(service.Name, jm, w) - } - w.Event(progress.Event{ - ID: service.Name, - Status: progress.Done, - Text: "Pulled", - }) - return nil - }) - } - - return eg.Wait() -} - -func toPullProgressEvent(parent string, jm jsonmessage.JSONMessage, w progress.Writer) { - if jm.ID == "" || jm.Progress == nil { - return - } - - var ( - text string - status = progress.Working - ) - - text = jm.Progress.String() - - if jm.Status == "Pull complete" || - jm.Status == "Already exists" || - strings.Contains(jm.Status, "Image is up to date") || - strings.Contains(jm.Status, "Downloaded newer image") { - status = progress.Done - } - - if jm.Error != nil { - status = progress.Error - text = jm.Error.Message - } - - w.Event(progress.Event{ - ID: jm.ID, - ParentID: parent, - Text: jm.Status, - Status: status, - StatusText: text, - }) -} - -func toProgressEvent(prefix string, jm jsonmessage.JSONMessage, w progress.Writer) { - if jm.ID == "" { - // skipped - return - } - var ( - text string - status = progress.Working - ) - if jm.Status == "Pull complete" || jm.Status == "Already exists" { - status = progress.Done - } - if jm.Error != nil { - status = progress.Error - text = jm.Error.Message - } - if jm.Progress != nil { - text = jm.Progress.String() - } - w.Event(progress.Event{ - ID: fmt.Sprintf("Pushing %s: %s", prefix, jm.ID), - Text: jm.Status, - Status: status, - StatusText: text, - }) -} - -func (s *composeService) Up(ctx context.Context, project *types.Project, detach bool) error { - return errdefs2.ErrNotImplemented -} - -func (s *composeService) Create(ctx context.Context, project *types.Project) error { - err := s.ensureImagesExists(ctx, project) - if err != nil { - return err - } - - for k, network := range project.Networks { - if !network.External.External && network.Name != "" { - network.Name = fmt.Sprintf("%s_%s", project.Name, k) - project.Networks[k] = network - } - network.Labels = network.Labels.Add(networkLabel, k) - network.Labels = network.Labels.Add(projectLabel, project.Name) - network.Labels = network.Labels.Add(versionLabel, ComposeVersion) - err := s.ensureNetwork(ctx, network) - if err != nil { - return err - } - } - - for k, volume := range project.Volumes { - if !volume.External.External && volume.Name != "" { - volume.Name = fmt.Sprintf("%s_%s", project.Name, k) - project.Volumes[k] = volume - } - volume.Labels = volume.Labels.Add(volumeLabel, k) - volume.Labels = volume.Labels.Add(projectLabel, project.Name) - volume.Labels = volume.Labels.Add(versionLabel, ComposeVersion) - err := s.ensureVolume(ctx, volume) - if err != nil { - return err - } - } - - return InDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error { - return s.ensureService(c, project, service) - }) -} - -func (s *composeService) Start(ctx context.Context, project *types.Project, consumer compose.LogConsumer) error { - var group *errgroup.Group - if consumer != nil { - eg, err := s.attach(ctx, project, consumer) - if err != nil { - return err - } - group = eg - } - - err := InDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error { - return s.startService(ctx, project, service) - }) - if err != nil { - return err - } - if group != nil { - return group.Wait() - } - return nil -} - -func (s *composeService) attach(ctx context.Context, project *types.Project, consumer compose.LogConsumer) (*errgroup.Group, error) { - containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{ - Filters: filters.NewArgs( - projectFilter(project.Name), - ), - All: true, - }) - if err != nil { - return nil, err - } - - var names []string - for _, c := range containers { - names = append(names, getContainerName(c)) - } - fmt.Printf("Attaching to %s\n", strings.Join(names, ", ")) - - eg, ctx := errgroup.WithContext(ctx) - for _, c := range containers { - container := c - eg.Go(func() error { - return s.attachContainer(ctx, container, consumer, project) - }) - } - return eg, nil -} - -func (s *composeService) attachContainer(ctx context.Context, container moby.Container, consumer compose.LogConsumer, project *types.Project) error { - serviceName := container.Labels[serviceLabel] - w := getWriter(serviceName, container.ID, consumer) - - service, err := project.GetService(serviceName) - if err != nil { - return err - } - - return s.attachContainerStreams(ctx, container, service.Tty, nil, w) -} - -func (s *composeService) attachContainerStreams(ctx context.Context, container moby.Container, tty bool, r io.Reader, w io.Writer) error { - stdin, stdout, err := s.getContainerStreams(ctx, container) - if err != nil { - return err - } - - go func() { - <-ctx.Done() - stdout.Close() //nolint:errcheck - stdin.Close() //nolint:errcheck - }() - - if r != nil && stdin != nil { - go func() { - io.Copy(stdin, r) //nolint:errcheck - }() - } - - if w != nil { - if tty { - _, err = io.Copy(w, stdout) - } else { - _, err = stdcopy.StdCopy(w, w, stdout) - } - } - return err -} - -func (s *composeService) getContainerStreams(ctx context.Context, container moby.Container) (io.WriteCloser, io.ReadCloser, error) { - var stdout io.ReadCloser - var stdin io.WriteCloser - if container.State == containerRunning { - logs, err := s.apiClient.ContainerLogs(ctx, container.ID, moby.ContainerLogsOptions{ - ShowStdout: true, - ShowStderr: true, - Follow: true, - }) - if err != nil { - return nil, nil, err - } - stdout = logs - } else { - cnx, err := s.apiClient.ContainerAttach(ctx, container.ID, moby.ContainerAttachOptions{ - Stream: true, - Stdin: true, - Stdout: true, - Stderr: true, - }) - if err != nil { - return nil, nil, err - } - stdout = containerStdout{cnx} - stdin = containerStdin{cnx} - } - return stdin, stdout, nil -} - -func getContainerName(c moby.Container) string { - // Names return container canonical name /foo + link aliases /linked_by/foo - for _, name := range c.Names { - if strings.LastIndex(name, "/") == 0 { - return name[1:] - } - } - return c.Names[0][1:] -} - -func (s *composeService) Down(ctx context.Context, projectName string) error { - eg, _ := errgroup.WithContext(ctx) - w := progress.ContextWriter(ctx) - - project, err := s.projectFromContainerLabels(ctx, projectName) - if err != nil { - return err - } - - err = InReverseDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error { - filter := filters.NewArgs(projectFilter(project.Name), serviceFilter(service.Name)) - return s.removeContainers(ctx, w, eg, filter) - }) - - if err != nil { - return err - } - err = eg.Wait() - if err != nil { - return err - } - networks, err := s.apiClient.NetworkList(ctx, moby.NetworkListOptions{ - Filters: filters.NewArgs( - projectFilter(projectName), - ), - }) - if err != nil { - return err - } - for _, n := range networks { - networkID := n.ID - networkName := n.Name - eg.Go(func() error { - return s.ensureNetworkDown(ctx, networkID, networkName) - }) - } - - return eg.Wait() -} - -func (s *composeService) removeContainers(ctx context.Context, w progress.Writer, eg *errgroup.Group, filter filters.Args) error { - containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{ - Filters: filter, - All: true, - }) - if err != nil { - return err - } - for _, container := range containers { - eg.Go(func() error { - eventName := "Container " + getContainerName(container) - w.Event(progress.StoppingEvent(eventName)) - err := s.apiClient.ContainerStop(ctx, container.ID, nil) - if err != nil { - w.Event(progress.ErrorMessageEvent(eventName, "Error while Stopping")) - return err - } - w.Event(progress.RemovingEvent(eventName)) - err = s.apiClient.ContainerRemove(ctx, container.ID, moby.ContainerRemoveOptions{}) - if err != nil { - w.Event(progress.ErrorMessageEvent(eventName, "Error while Removing")) - return err - } - w.Event(progress.RemovedEvent(eventName)) - return nil - }) - } - return nil -} - -func (s *composeService) projectFromContainerLabels(ctx context.Context, projectName string) (*types.Project, error) { - containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{ - Filters: filters.NewArgs( - projectFilter(projectName), - ), - All: true, - }) - if err != nil { - return nil, err - } - fakeProject := &types.Project{ - Name: projectName, - } - if len(containers) == 0 { - return fakeProject, nil - } - options, err := loadProjectOptionsFromLabels(containers[0]) - if err != nil { - return nil, err - } - if options.ConfigPaths[0] == "-" { - for _, container := range containers { - fakeProject.Services = append(fakeProject.Services, types.ServiceConfig{ - Name: container.Labels[serviceLabel], - }) - } - return fakeProject, nil - } - project, err := cli.ProjectFromOptions(options) - if err != nil { - return nil, err - } - - return project, nil -} - -func loadProjectOptionsFromLabels(c moby.Container) (*cli.ProjectOptions, error) { - var configFiles []string - relativePathConfigFiles := strings.Split(c.Labels[configFilesLabel], ",") - for _, c := range relativePathConfigFiles { - configFiles = append(configFiles, filepath.Base(c)) - } - return cli.NewProjectOptions(configFiles, - cli.WithOsEnv, - cli.WithWorkingDirectory(c.Labels[workingDirLabel]), - cli.WithName(c.Labels[projectLabel])) -} - -func (s *composeService) Logs(ctx context.Context, projectName string, consumer compose.LogConsumer) error { - list, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{ - Filters: filters.NewArgs( - projectFilter(projectName), - ), - }) - if err != nil { - return err - } - eg, ctx := errgroup.WithContext(ctx) - for _, c := range list { - service := c.Labels[serviceLabel] - container, err := s.apiClient.ContainerInspect(ctx, c.ID) - if err != nil { - return err - } - - eg.Go(func() error { - r, err := s.apiClient.ContainerLogs(ctx, container.ID, moby.ContainerLogsOptions{ - ShowStdout: true, - ShowStderr: true, - Follow: true, - }) - defer r.Close() // nolint errcheck - - if err != nil { - return err - } - w := getWriter(service, container.ID, consumer) - if container.Config.Tty { - _, err = io.Copy(w, r) - } else { - _, err = stdcopy.StdCopy(w, w, r) - } - return err - }) - } - return eg.Wait() -} - -type splitBuffer struct { - service string - container string - consumer compose.LogConsumer -} - -// getWriter creates a io.Writer that will actually split by line and format by LogConsumer -func getWriter(service, container string, l compose.LogConsumer) io.Writer { - return splitBuffer{ - service: service, - container: container, - consumer: l, - } -} - -func (s splitBuffer) Write(b []byte) (n int, err error) { - split := bytes.Split(b, []byte{'\n'}) - for _, line := range split { - if len(line) != 0 { - s.consumer.Log(s.service, s.container, string(line)) - } - } - return len(b), nil -} - -func (s *composeService) Ps(ctx context.Context, projectName string) ([]compose.ServiceStatus, error) { - list, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{ - Filters: filters.NewArgs( - projectFilter(projectName), - ), - }) - if err != nil { - return nil, err - } - return containersToServiceStatus(list) -} - -func containersToServiceStatus(containers []moby.Container) ([]compose.ServiceStatus, error) { - containersByLabel, keys, err := groupContainerByLabel(containers, serviceLabel) - if err != nil { - return nil, err - } - var services []compose.ServiceStatus - for _, service := range keys { - containers := containersByLabel[service] - runnningContainers := []moby.Container{} - for _, container := range containers { - if container.State == containerRunning { - runnningContainers = append(runnningContainers, container) - } - } - services = append(services, compose.ServiceStatus{ - ID: service, - Name: service, - Desired: len(containers), - Replicas: len(runnningContainers), - }) - } - return services, nil -} - -func groupContainerByLabel(containers []moby.Container, labelName string) (map[string][]moby.Container, []string, error) { - containersByLabel := map[string][]moby.Container{} - keys := []string{} - for _, c := range containers { - label, ok := c.Labels[labelName] - if !ok { - return nil, nil, fmt.Errorf("No label %q set on container %q of compose project", labelName, c.ID) - } - labelContainers, ok := containersByLabel[label] - if !ok { - labelContainers = []moby.Container{} - keys = append(keys, label) - } - labelContainers = append(labelContainers, c) - containersByLabel[label] = labelContainers - } - sort.Strings(keys) - return containersByLabel, keys, nil -} - -func (s *composeService) List(ctx context.Context, projectName string) ([]compose.Stack, error) { - list, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{ - Filters: filters.NewArgs(hasProjectLabelFilter()), - }) - if err != nil { - return nil, err - } - - return containersToStacks(list) -} - -func containersToStacks(containers []moby.Container) ([]compose.Stack, error) { - containersByLabel, keys, err := groupContainerByLabel(containers, projectLabel) - if err != nil { - return nil, err - } - var projects []compose.Stack - for _, project := range keys { - projects = append(projects, compose.Stack{ - ID: project, - Name: project, - Status: combinedStatus(containerToState(containersByLabel[project])), - }) - } - return projects, nil -} - -func containerToState(containers []moby.Container) []string { - statuses := []string{} - for _, c := range containers { - statuses = append(statuses, c.State) - } - return statuses -} - -func combinedStatus(statuses []string) string { - nbByStatus := map[string]int{} - keys := []string{} - for _, status := range statuses { - nb, ok := nbByStatus[status] - if !ok { - nb = 0 - keys = append(keys, status) - } - nbByStatus[status] = nb + 1 - } - sort.Strings(keys) - result := "" - for _, status := range keys { - nb := nbByStatus[status] - if result != "" { - result = result + ", " - } - result = result + fmt.Sprintf("%s(%d)", status, nb) - } - return result -} - -func (s *composeService) Convert(ctx context.Context, project *types.Project, format string) ([]byte, error) { - switch format { - case "json": - return json.MarshalIndent(project, "", " ") - case "yaml": - return yaml.Marshal(project) - default: - return nil, fmt.Errorf("unsupported format %q", format) - } -} - -func getContainerCreateOptions(p *types.Project, s types.ServiceConfig, number int, inherit *moby.Container) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) { - hash, err := jsonHash(s) - if err != nil { - return nil, nil, nil, err - } - // TODO: change oneoffLabel value for containers started with `docker compose run` - labels := map[string]string{ - projectLabel: p.Name, - serviceLabel: s.Name, - versionLabel: ComposeVersion, - oneoffLabel: "False", - configHashLabel: hash, - workingDirLabel: p.WorkingDir, - configFilesLabel: strings.Join(p.ComposeFiles, ","), - containerNumberLabel: strconv.Itoa(number), - } - - 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: toMobyEnv(s.Environment), - Healthcheck: toMobyHealthCheck(s.HealthCheck), - // Volumes: // FIXME unclear to me the overlap with HostConfig.Mounts - StopTimeout: toSeconds(s.StopGracePeriod), - } - - mountOptions, err := buildContainerMountOptions(s, inherit) - if err != nil { - return nil, nil, nil, err - } - bindings := buildContainerBindingOptions(s) - - 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 { - 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 -} - -func buildContainerMountOptions(s types.ServiceConfig, inherit *moby.Container) ([]mount.Mount, error) { - mounts := []mount.Mount{} - var inherited []string - if inherit != nil { - for _, m := range inherit.Mounts { - if m.Type == "tmpfs" { - continue - } - src := m.Source - if m.Type == "volume" { - src = m.Name - } - mounts = append(mounts, mount.Mount{ - Type: m.Type, - Source: src, - Target: m.Destination, - ReadOnly: !m.RW, - }) - inherited = append(inherited, m.Destination) - } - } - - for _, v := range s.Volumes { - if contains(inherited, v.Target) { - continue - } - mount, err := buildMount(v) - if err != nil { - return nil, err - } - mounts = append(mounts, mount) - } - return mounts, nil -} - -func buildMount(volume types.ServiceVolumeConfig) (mount.Mount, error) { - source := volume.Source - if volume.Type == "bind" && !filepath.IsAbs(source) { - // volume source has already been prefixed with workdir if required, by compose-go project loader - var err error - source, err = filepath.Abs(source) - if err != nil { - return mount.Mount{}, err - } - } - - return mount.Mount{ - Type: mount.Type(volume.Type), - Source: source, - Target: volume.Target, - ReadOnly: volume.ReadOnly, - Consistency: mount.Consistency(volume.Consistency), - BindOptions: buildBindOption(volume.Bind), - VolumeOptions: buildVolumeOptions(volume.Volume), - TmpfsOptions: buildTmpfsOptions(volume.Tmpfs), - }, 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 *composeService) ensureNetwork(ctx context.Context, n types.NetworkConfig) error { - _, err := s.apiClient.NetworkInspect(ctx, n.Name, moby.NetworkInspectOptions{}) - if err != nil { - if errdefs.IsNotFound(err) { - if n.External.External { - return fmt.Errorf("network %s declared as external, but could not be found", n.Name) - } - 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) - } - networkEventName := fmt.Sprintf("Network %q", n.Name) - w := progress.ContextWriter(ctx) - w.Event(progress.CreatingEvent(networkEventName)) - if _, err := s.apiClient.NetworkCreate(ctx, n.Name, createOpts); err != nil { - w.Event(progress.ErrorEvent(networkEventName)) - return errors.Wrapf(err, "failed to create network %s", n.Name) - } - w.Event(progress.CreatedEvent(networkEventName)) - return nil - } - return err - } - return nil -} - -func (s *composeService) ensureNetworkDown(ctx context.Context, networkID string, networkName string) error { - w := progress.ContextWriter(ctx) - eventName := fmt.Sprintf("Network %q", networkName) - w.Event(progress.RemovingEvent(eventName)) - - if err := s.apiClient.NetworkRemove(ctx, networkID); err != nil { - w.Event(progress.ErrorEvent(eventName)) - return errors.Wrapf(err, fmt.Sprintf("failed to create network %s", networkID)) - } - - w.Event(progress.RemovedEvent(eventName)) - return nil -} - -func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeConfig) error { - // TODO could identify volume by label vs name - _, err := s.apiClient.VolumeInspect(ctx, volume.Name) - if err != nil { - if !errdefs.IsNotFound(err) { - return err - } - eventName := fmt.Sprintf("Volume %q", volume.Name) - w := progress.ContextWriter(ctx) - w.Event(progress.CreatingEvent(eventName)) - // TODO we miss support for driver_opts and labels - _, err := s.apiClient.VolumeCreate(ctx, mobyvolume.VolumeCreateBody{ - Labels: volume.Labels, - Name: volume.Name, - Driver: volume.Driver, - DriverOpts: volume.DriverOpts, - }) - if err != nil { - w.Event(progress.ErrorEvent(eventName)) - return err - } - w.Event(progress.CreatedEvent(eventName)) - } - return nil -} diff --git a/local/compose/attach.go b/local/compose/attach.go new file mode 100644 index 000000000..37980170c --- /dev/null +++ b/local/compose/attach.go @@ -0,0 +1,129 @@ +/* + 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 compose + +import ( + "context" + "fmt" + "io" + "strings" + + "github.com/docker/compose-cli/api/compose" + convert "github.com/docker/compose-cli/local/moby" + + "github.com/compose-spec/compose-go/types" + moby "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/pkg/stdcopy" + "golang.org/x/sync/errgroup" +) + +func (s *composeService) attach(ctx context.Context, project *types.Project, consumer compose.LogConsumer) (*errgroup.Group, error) { + containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{ + Filters: filters.NewArgs( + projectFilter(project.Name), + ), + All: true, + }) + if err != nil { + return nil, err + } + + var names []string + for _, c := range containers { + names = append(names, getContainerName(c)) + } + fmt.Printf("Attaching to %s\n", strings.Join(names, ", ")) + + eg, ctx := errgroup.WithContext(ctx) + for _, c := range containers { + container := c + eg.Go(func() error { + return s.attachContainer(ctx, container, consumer, project) + }) + } + return eg, nil +} + +func (s *composeService) attachContainer(ctx context.Context, container moby.Container, consumer compose.LogConsumer, project *types.Project) error { + serviceName := container.Labels[serviceLabel] + w := getWriter(serviceName, container.ID, consumer) + + service, err := project.GetService(serviceName) + if err != nil { + return err + } + + return s.attachContainerStreams(ctx, container, service.Tty, nil, w) +} + +func (s *composeService) attachContainerStreams(ctx context.Context, container moby.Container, tty bool, r io.Reader, w io.Writer) error { + stdin, stdout, err := s.getContainerStreams(ctx, container) + if err != nil { + return err + } + + go func() { + <-ctx.Done() + stdout.Close() //nolint:errcheck + stdin.Close() //nolint:errcheck + }() + + if r != nil && stdin != nil { + go func() { + io.Copy(stdin, r) //nolint:errcheck + }() + } + + if w != nil { + if tty { + _, err = io.Copy(w, stdout) + } else { + _, err = stdcopy.StdCopy(w, w, stdout) + } + } + return err +} + +func (s *composeService) getContainerStreams(ctx context.Context, container moby.Container) (io.WriteCloser, io.ReadCloser, error) { + var stdout io.ReadCloser + var stdin io.WriteCloser + if container.State == convert.ContainerRunning { + logs, err := s.apiClient.ContainerLogs(ctx, container.ID, moby.ContainerLogsOptions{ + ShowStdout: true, + ShowStderr: true, + Follow: true, + }) + if err != nil { + return nil, nil, err + } + stdout = logs + } else { + cnx, err := s.apiClient.ContainerAttach(ctx, container.ID, moby.ContainerAttachOptions{ + Stream: true, + Stdin: true, + Stdout: true, + Stderr: true, + }) + if err != nil { + return nil, nil, err + } + stdout = convert.ContainerStdout{HijackedResponse: cnx} + stdin = convert.ContainerStdin{HijackedResponse: cnx} + } + return stdin, stdout, nil +} diff --git a/local/build.go b/local/compose/build.go similarity index 90% rename from local/build.go rename to local/compose/build.go index 21abaf38e..041372393 100644 --- a/local/build.go +++ b/local/compose/build.go @@ -14,7 +14,7 @@ limitations under the License. */ -package local +package compose import ( "context" @@ -23,15 +23,29 @@ import ( "path" "strings" - "github.com/docker/docker/errdefs" - "github.com/compose-spec/compose-go/types" "github.com/docker/buildx/build" "github.com/docker/buildx/driver" _ "github.com/docker/buildx/driver/docker" // required to get default driver registered "github.com/docker/buildx/util/progress" + "github.com/docker/docker/errdefs" ) +func (s *composeService) Build(ctx context.Context, project *types.Project) error { + opts := map[string]build.Options{} + for _, service := range project.Services { + if service.Build != nil { + imageName := service.Image + if imageName == "" { + imageName = project.Name + "_" + service.Name + } + opts[imageName] = s.toBuildOptions(service, project.WorkingDir, imageName) + } + } + + return s.build(ctx, project, opts) +} + func (s *composeService) ensureImagesExists(ctx context.Context, project *types.Project) error { opts := map[string]build.Options{} for _, service := range project.Services { diff --git a/local/compose/compose.go b/local/compose/compose.go new file mode 100644 index 000000000..6d220c649 --- /dev/null +++ b/local/compose/compose.go @@ -0,0 +1,66 @@ +/* + 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 compose + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/docker/compose-cli/api/compose" + + "github.com/compose-spec/compose-go/types" + errdefs2 "github.com/docker/compose-cli/errdefs" + moby "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + "github.com/sanathkr/go-yaml" +) + +// NewComposeService create a local implementation of the compose.Service API +func NewComposeService(apiClient *client.Client) compose.Service { + return &composeService{apiClient: apiClient} +} + +type composeService struct { + apiClient *client.Client +} + +func (s *composeService) Up(ctx context.Context, project *types.Project, detach bool) error { + return errdefs2.ErrNotImplemented +} + +func getContainerName(c moby.Container) string { + // Names return container canonical name /foo + link aliases /linked_by/foo + for _, name := range c.Names { + if strings.LastIndex(name, "/") == 0 { + return name[1:] + } + } + return c.Names[0][1:] +} + +func (s *composeService) Convert(ctx context.Context, project *types.Project, format string) ([]byte, error) { + switch format { + case "json": + return json.MarshalIndent(project, "", " ") + case "yaml": + return yaml.Marshal(project) + default: + return nil, fmt.Errorf("unsupported format %q", format) + } +} diff --git a/local/compose_test.go b/local/compose/compose_test.go similarity index 98% rename from local/compose_test.go rename to local/compose/compose_test.go index ea1ec0e59..d57c25b7b 100644 --- a/local/compose_test.go +++ b/local/compose/compose_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package local +package compose import ( "os" @@ -113,7 +113,7 @@ func TestStacksMixedStatus(t *testing.T) { func TestBuildBindMount(t *testing.T) { volume := composetypes.ServiceVolumeConfig{ Type: composetypes.VolumeTypeBind, - Source: "compose/e2e/volume-test", + Source: "e2e/volume-test", Target: "/data", } mount, err := buildMount(volume) diff --git a/local/convergence.go b/local/compose/convergence.go similarity index 97% rename from local/convergence.go rename to local/compose/convergence.go index e62a566f3..53df364db 100644 --- a/local/convergence.go +++ b/local/compose/convergence.go @@ -14,7 +14,7 @@ limitations under the License. */ -package local +package compose import ( "context" @@ -28,6 +28,7 @@ import ( "github.com/docker/docker/api/types/network" "golang.org/x/sync/errgroup" + status "github.com/docker/compose-cli/local/moby" "github.com/docker/compose-cli/progress" ) @@ -99,10 +100,10 @@ func (s *composeService) ensureService(ctx context.Context, project *types.Proje w := progress.ContextWriter(ctx) switch container.State { - case containerRunning: + case status.ContainerRunning: w.Event(progress.RunningEvent(name)) - case containerCreated: - case containerRestarting: + case status.ContainerCreated: + case status.ContainerRestarting: w.Event(progress.CreatedEvent(name)) default: eg.Go(func() error { @@ -304,7 +305,7 @@ func (s *composeService) startService(ctx context.Context, project *types.Projec eg, ctx := errgroup.WithContext(ctx) for _, c := range containers { container := c - if container.State == containerRunning { + if container.State == status.ContainerRunning { continue } eg.Go(func() error { diff --git a/local/compose/create.go b/local/compose/create.go new file mode 100644 index 000000000..49b08e1d1 --- /dev/null +++ b/local/compose/create.go @@ -0,0 +1,411 @@ +/* + 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 compose + +import ( + "context" + "fmt" + "path/filepath" + "strconv" + "strings" + + convert "github.com/docker/compose-cli/local/moby" + "github.com/docker/compose-cli/progress" + + "github.com/compose-spec/compose-go/types" + 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" + volume_api "github.com/docker/docker/api/types/volume" + "github.com/docker/docker/errdefs" + "github.com/docker/go-connections/nat" + "github.com/pkg/errors" +) + +func (s *composeService) Create(ctx context.Context, project *types.Project) error { + err := s.ensureImagesExists(ctx, project) + if err != nil { + return err + } + + for k, network := range project.Networks { + if !network.External.External && network.Name != "" { + network.Name = fmt.Sprintf("%s_%s", project.Name, k) + project.Networks[k] = network + } + network.Labels = network.Labels.Add(networkLabel, k) + network.Labels = network.Labels.Add(projectLabel, project.Name) + network.Labels = network.Labels.Add(versionLabel, ComposeVersion) + err := s.ensureNetwork(ctx, network) + if err != nil { + return err + } + } + + for k, volume := range project.Volumes { + if !volume.External.External && volume.Name != "" { + volume.Name = fmt.Sprintf("%s_%s", project.Name, k) + project.Volumes[k] = volume + } + volume.Labels = volume.Labels.Add(volumeLabel, k) + volume.Labels = volume.Labels.Add(projectLabel, project.Name) + volume.Labels = volume.Labels.Add(versionLabel, ComposeVersion) + err := s.ensureVolume(ctx, volume) + if err != nil { + return err + } + } + + return InDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error { + return s.ensureService(c, project, service) + }) +} + +func getContainerCreateOptions(p *types.Project, s types.ServiceConfig, number int, inherit *moby.Container) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) { + hash, err := jsonHash(s) + if err != nil { + return nil, nil, nil, err + } + // TODO: change oneoffLabel value for containers started with `docker compose run` + labels := map[string]string{ + projectLabel: p.Name, + serviceLabel: s.Name, + versionLabel: ComposeVersion, + oneoffLabel: "False", + configHashLabel: hash, + workingDirLabel: p.WorkingDir, + configFilesLabel: strings.Join(p.ComposeFiles, ","), + containerNumberLabel: strconv.Itoa(number), + } + + 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: convert.ToMobyEnv(s.Environment), + Healthcheck: convert.ToMobyHealthCheck(s.HealthCheck), + // Volumes: // FIXME unclear to me the overlap with HostConfig.Mounts + StopTimeout: convert.ToSeconds(s.StopGracePeriod), + } + + mountOptions, err := buildContainerMountOptions(s, inherit) + if err != nil { + return nil, nil, nil, err + } + bindings := buildContainerBindingOptions(s) + + 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 { + 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 +} + +func buildContainerMountOptions(s types.ServiceConfig, inherit *moby.Container) ([]mount.Mount, error) { + mounts := []mount.Mount{} + var inherited []string + if inherit != nil { + for _, m := range inherit.Mounts { + if m.Type == "tmpfs" { + continue + } + src := m.Source + if m.Type == "volume" { + src = m.Name + } + mounts = append(mounts, mount.Mount{ + Type: m.Type, + Source: src, + Target: m.Destination, + ReadOnly: !m.RW, + }) + inherited = append(inherited, m.Destination) + } + } + + for _, v := range s.Volumes { + if contains(inherited, v.Target) { + continue + } + mount, err := buildMount(v) + if err != nil { + return nil, err + } + mounts = append(mounts, mount) + } + return mounts, nil +} + +func buildMount(volume types.ServiceVolumeConfig) (mount.Mount, error) { + source := volume.Source + if volume.Type == "bind" && !filepath.IsAbs(source) { + // volume source has already been prefixed with workdir if required, by compose-go project loader + var err error + source, err = filepath.Abs(source) + if err != nil { + return mount.Mount{}, err + } + } + + return mount.Mount{ + Type: mount.Type(volume.Type), + Source: source, + Target: volume.Target, + ReadOnly: volume.ReadOnly, + Consistency: mount.Consistency(volume.Consistency), + BindOptions: buildBindOption(volume.Bind), + VolumeOptions: buildVolumeOptions(volume.Volume), + TmpfsOptions: buildTmpfsOptions(volume.Tmpfs), + }, 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 *composeService) ensureNetwork(ctx context.Context, n types.NetworkConfig) error { + _, err := s.apiClient.NetworkInspect(ctx, n.Name, moby.NetworkInspectOptions{}) + if err != nil { + if errdefs.IsNotFound(err) { + if n.External.External { + return fmt.Errorf("network %s declared as external, but could not be found", n.Name) + } + 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) + } + networkEventName := fmt.Sprintf("Network %q", n.Name) + w := progress.ContextWriter(ctx) + w.Event(progress.CreatingEvent(networkEventName)) + if _, err := s.apiClient.NetworkCreate(ctx, n.Name, createOpts); err != nil { + w.Event(progress.ErrorEvent(networkEventName)) + return errors.Wrapf(err, "failed to create network %s", n.Name) + } + w.Event(progress.CreatedEvent(networkEventName)) + return nil + } + return err + } + return nil +} + +func (s *composeService) ensureNetworkDown(ctx context.Context, networkID string, networkName string) error { + w := progress.ContextWriter(ctx) + eventName := fmt.Sprintf("Network %q", networkName) + w.Event(progress.RemovingEvent(eventName)) + + if err := s.apiClient.NetworkRemove(ctx, networkID); err != nil { + w.Event(progress.ErrorEvent(eventName)) + return errors.Wrapf(err, fmt.Sprintf("failed to create network %s", networkID)) + } + + w.Event(progress.RemovedEvent(eventName)) + return nil +} + +func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeConfig) error { + // TODO could identify volume by label vs name + _, err := s.apiClient.VolumeInspect(ctx, volume.Name) + if err != nil { + if !errdefs.IsNotFound(err) { + return err + } + eventName := fmt.Sprintf("Volume %q", volume.Name) + w := progress.ContextWriter(ctx) + w.Event(progress.CreatingEvent(eventName)) + // TODO we miss support for driver_opts and labels + _, err := s.apiClient.VolumeCreate(ctx, volume_api.VolumeCreateBody{ + Labels: volume.Labels, + Name: volume.Name, + Driver: volume.Driver, + DriverOpts: volume.DriverOpts, + }) + if err != nil { + w.Event(progress.ErrorEvent(eventName)) + return err + } + w.Event(progress.CreatedEvent(eventName)) + } + return nil +} diff --git a/local/dependencies.go b/local/compose/dependencies.go similarity index 99% rename from local/dependencies.go rename to local/compose/dependencies.go index ecfe06b45..91a0c033a 100644 --- a/local/dependencies.go +++ b/local/compose/dependencies.go @@ -14,7 +14,7 @@ limitations under the License. */ -package local +package compose import ( "context" diff --git a/local/dependencies_test.go b/local/compose/dependencies_test.go similarity index 99% rename from local/dependencies_test.go rename to local/compose/dependencies_test.go index 736bb50c8..841274b76 100644 --- a/local/dependencies_test.go +++ b/local/compose/dependencies_test.go @@ -14,15 +14,14 @@ limitations under the License. */ -package local +package compose import ( "context" "testing" - "gotest.tools/v3/assert" - "github.com/compose-spec/compose-go/types" + "gotest.tools/v3/assert" ) var project = types.Project{ diff --git a/local/compose/down.go b/local/compose/down.go new file mode 100644 index 000000000..a9ada09e0 --- /dev/null +++ b/local/compose/down.go @@ -0,0 +1,149 @@ +/* + 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 compose + +import ( + "context" + "path/filepath" + "strings" + + "github.com/docker/compose-cli/progress" + + "github.com/compose-spec/compose-go/cli" + "github.com/compose-spec/compose-go/types" + moby "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "golang.org/x/sync/errgroup" +) + +func (s *composeService) Down(ctx context.Context, projectName string) error { + eg, _ := errgroup.WithContext(ctx) + w := progress.ContextWriter(ctx) + + project, err := s.projectFromContainerLabels(ctx, projectName) + if err != nil { + return err + } + + err = InReverseDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error { + filter := filters.NewArgs(projectFilter(project.Name), serviceFilter(service.Name)) + return s.removeContainers(ctx, w, eg, filter) + }) + + if err != nil { + return err + } + err = eg.Wait() + if err != nil { + return err + } + networks, err := s.apiClient.NetworkList(ctx, moby.NetworkListOptions{ + Filters: filters.NewArgs( + projectFilter(projectName), + ), + }) + if err != nil { + return err + } + for _, n := range networks { + networkID := n.ID + networkName := n.Name + eg.Go(func() error { + return s.ensureNetworkDown(ctx, networkID, networkName) + }) + } + + return eg.Wait() +} + +func (s *composeService) removeContainers(ctx context.Context, w progress.Writer, eg *errgroup.Group, filter filters.Args) error { + containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{ + Filters: filter, + All: true, + }) + if err != nil { + return err + } + for _, container := range containers { + eg.Go(func() error { + eventName := "Container " + getContainerName(container) + w.Event(progress.StoppingEvent(eventName)) + err := s.apiClient.ContainerStop(ctx, container.ID, nil) + if err != nil { + w.Event(progress.ErrorMessageEvent(eventName, "Error while Stopping")) + return err + } + w.Event(progress.RemovingEvent(eventName)) + err = s.apiClient.ContainerRemove(ctx, container.ID, moby.ContainerRemoveOptions{}) + if err != nil { + w.Event(progress.ErrorMessageEvent(eventName, "Error while Removing")) + return err + } + w.Event(progress.RemovedEvent(eventName)) + return nil + }) + } + return nil +} + +func (s *composeService) projectFromContainerLabels(ctx context.Context, projectName string) (*types.Project, error) { + containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{ + Filters: filters.NewArgs( + projectFilter(projectName), + ), + All: true, + }) + if err != nil { + return nil, err + } + fakeProject := &types.Project{ + Name: projectName, + } + if len(containers) == 0 { + return fakeProject, nil + } + options, err := loadProjectOptionsFromLabels(containers[0]) + if err != nil { + return nil, err + } + if options.ConfigPaths[0] == "-" { + for _, container := range containers { + fakeProject.Services = append(fakeProject.Services, types.ServiceConfig{ + Name: container.Labels[serviceLabel], + }) + } + return fakeProject, nil + } + project, err := cli.ProjectFromOptions(options) + if err != nil { + return nil, err + } + + return project, nil +} + +func loadProjectOptionsFromLabels(c moby.Container) (*cli.ProjectOptions, error) { + var configFiles []string + relativePathConfigFiles := strings.Split(c.Labels[configFilesLabel], ",") + for _, c := range relativePathConfigFiles { + configFiles = append(configFiles, filepath.Base(c)) + } + return cli.NewProjectOptions(configFiles, + cli.WithOsEnv, + cli.WithWorkingDirectory(c.Labels[workingDirLabel]), + cli.WithName(c.Labels[projectLabel])) +} diff --git a/local/labels.go b/local/compose/labels.go similarity index 99% rename from local/labels.go rename to local/compose/labels.go index 2fd388f59..ce36bf49a 100644 --- a/local/labels.go +++ b/local/compose/labels.go @@ -14,7 +14,7 @@ limitations under the License. */ -package local +package compose import ( "fmt" diff --git a/local/compose/logs.go b/local/compose/logs.go new file mode 100644 index 000000000..6d59a8334 --- /dev/null +++ b/local/compose/logs.go @@ -0,0 +1,95 @@ +/* + 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 compose + +import ( + "bytes" + "context" + "io" + + "github.com/docker/compose-cli/api/compose" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/pkg/stdcopy" + "golang.org/x/sync/errgroup" +) + +func (s *composeService) Logs(ctx context.Context, projectName string, consumer compose.LogConsumer) error { + list, err := s.apiClient.ContainerList(ctx, types.ContainerListOptions{ + Filters: filters.NewArgs( + projectFilter(projectName), + ), + }) + if err != nil { + return err + } + eg, ctx := errgroup.WithContext(ctx) + for _, c := range list { + service := c.Labels[serviceLabel] + container, err := s.apiClient.ContainerInspect(ctx, c.ID) + if err != nil { + return err + } + + eg.Go(func() error { + r, err := s.apiClient.ContainerLogs(ctx, container.ID, types.ContainerLogsOptions{ + ShowStdout: true, + ShowStderr: true, + Follow: true, + }) + defer r.Close() // nolint errcheck + + if err != nil { + return err + } + w := getWriter(service, container.ID, consumer) + if container.Config.Tty { + _, err = io.Copy(w, r) + } else { + _, err = stdcopy.StdCopy(w, w, r) + } + return err + }) + } + return eg.Wait() +} + +type splitBuffer struct { + service string + container string + consumer compose.LogConsumer +} + +// getWriter creates a io.Writer that will actually split by line and format by LogConsumer +func getWriter(service, container string, l compose.LogConsumer) io.Writer { + return splitBuffer{ + service: service, + container: container, + consumer: l, + } +} + +func (s splitBuffer) Write(b []byte) (n int, err error) { + split := bytes.Split(b, []byte{'\n'}) + for _, line := range split { + if len(line) != 0 { + s.consumer.Log(s.service, s.container, string(line)) + } + } + return len(b), nil +} diff --git a/local/compose/ls.go b/local/compose/ls.go new file mode 100644 index 000000000..322ad5b65 --- /dev/null +++ b/local/compose/ls.go @@ -0,0 +1,86 @@ +/* + 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 compose + +import ( + "context" + "fmt" + "sort" + + "github.com/docker/compose-cli/api/compose" + + moby "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" +) + +func (s *composeService) List(ctx context.Context, projectName string) ([]compose.Stack, error) { + list, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{ + Filters: filters.NewArgs(hasProjectLabelFilter()), + }) + if err != nil { + return nil, err + } + + return containersToStacks(list) +} + +func containersToStacks(containers []moby.Container) ([]compose.Stack, error) { + containersByLabel, keys, err := groupContainerByLabel(containers, projectLabel) + if err != nil { + return nil, err + } + var projects []compose.Stack + for _, project := range keys { + projects = append(projects, compose.Stack{ + ID: project, + Name: project, + Status: combinedStatus(containerToState(containersByLabel[project])), + }) + } + return projects, nil +} + +func containerToState(containers []moby.Container) []string { + statuses := []string{} + for _, c := range containers { + statuses = append(statuses, c.State) + } + return statuses +} + +func combinedStatus(statuses []string) string { + nbByStatus := map[string]int{} + keys := []string{} + for _, status := range statuses { + nb, ok := nbByStatus[status] + if !ok { + nb = 0 + keys = append(keys, status) + } + nbByStatus[status] = nb + 1 + } + sort.Strings(keys) + result := "" + for _, status := range keys { + nb := nbByStatus[status] + if result != "" { + result = result + ", " + } + result = result + fmt.Sprintf("%s(%d)", status, nb) + } + return result +} diff --git a/local/compose/ps.go b/local/compose/ps.go new file mode 100644 index 000000000..39b1ecfaf --- /dev/null +++ b/local/compose/ps.go @@ -0,0 +1,85 @@ +/* + 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 compose + +import ( + "context" + "fmt" + "sort" + + convert "github.com/docker/compose-cli/local/moby" + + "github.com/docker/compose-cli/api/compose" + moby "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" +) + +func (s *composeService) Ps(ctx context.Context, projectName string) ([]compose.ServiceStatus, error) { + list, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{ + Filters: filters.NewArgs( + projectFilter(projectName), + ), + }) + if err != nil { + return nil, err + } + return containersToServiceStatus(list) +} + +func containersToServiceStatus(containers []moby.Container) ([]compose.ServiceStatus, error) { + containersByLabel, keys, err := groupContainerByLabel(containers, serviceLabel) + if err != nil { + return nil, err + } + var services []compose.ServiceStatus + for _, service := range keys { + containers := containersByLabel[service] + runnningContainers := []moby.Container{} + for _, container := range containers { + if container.State == convert.ContainerRunning { + runnningContainers = append(runnningContainers, container) + } + } + services = append(services, compose.ServiceStatus{ + ID: service, + Name: service, + Desired: len(containers), + Replicas: len(runnningContainers), + }) + } + return services, nil +} + +func groupContainerByLabel(containers []moby.Container, labelName string) (map[string][]moby.Container, []string, error) { + containersByLabel := map[string][]moby.Container{} + keys := []string{} + for _, c := range containers { + label, ok := c.Labels[labelName] + if !ok { + return nil, nil, fmt.Errorf("No label %q set on container %q of compose project", labelName, c.ID) + } + labelContainers, ok := containersByLabel[label] + if !ok { + labelContainers = []moby.Container{} + keys = append(keys, label) + } + labelContainers = append(labelContainers, c) + containersByLabel[label] = labelContainers + } + sort.Strings(keys) + return containersByLabel, keys, nil +} diff --git a/local/compose/pull.go b/local/compose/pull.go new file mode 100644 index 000000000..2ee181b1b --- /dev/null +++ b/local/compose/pull.go @@ -0,0 +1,158 @@ +/* + 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 compose + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "io" + "strings" + + "github.com/compose-spec/compose-go/types" + cliconfig "github.com/docker/cli/cli/config" + "github.com/docker/distribution/reference" + moby "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/jsonmessage" + "github.com/docker/docker/registry" + "golang.org/x/sync/errgroup" + + "github.com/docker/compose-cli/config" + "github.com/docker/compose-cli/progress" +) + +func (s *composeService) Pull(ctx context.Context, project *types.Project) error { + configFile, err := cliconfig.Load(config.Dir(ctx)) + if err != nil { + return err + } + info, err := s.apiClient.Info(ctx) + if err != nil { + return err + } + + if info.IndexServerAddress == "" { + info.IndexServerAddress = registry.IndexServer + } + + w := progress.ContextWriter(ctx) + eg, ctx := errgroup.WithContext(ctx) + + for _, srv := range project.Services { + service := srv + eg.Go(func() error { + w.Event(progress.Event{ + ID: service.Name, + Status: progress.Working, + Text: "Pulling", + }) + ref, err := reference.ParseNormalizedNamed(service.Image) + if err != nil { + return err + } + + repoInfo, err := registry.ParseRepositoryInfo(ref) + if err != nil { + return err + } + + key := repoInfo.Index.Name + if repoInfo.Index.Official { + key = info.IndexServerAddress + } + + authConfig, err := configFile.GetAuthConfig(key) + if err != nil { + return err + } + + buf, err := json.Marshal(authConfig) + if err != nil { + return err + } + + stream, err := s.apiClient.ImagePull(ctx, service.Image, moby.ImagePullOptions{ + RegistryAuth: base64.URLEncoding.EncodeToString(buf), + }) + if err != nil { + w.Event(progress.Event{ + ID: service.Name, + Status: progress.Error, + Text: "Error", + }) + return err + } + + dec := json.NewDecoder(stream) + for { + var jm jsonmessage.JSONMessage + if err := dec.Decode(&jm); err != nil { + if err == io.EOF { + break + } + return err + } + if jm.Error != nil { + return errors.New(jm.Error.Message) + } + toPullProgressEvent(service.Name, jm, w) + } + w.Event(progress.Event{ + ID: service.Name, + Status: progress.Done, + Text: "Pulled", + }) + return nil + }) + } + + return eg.Wait() +} + +func toPullProgressEvent(parent string, jm jsonmessage.JSONMessage, w progress.Writer) { + if jm.ID == "" || jm.Progress == nil { + return + } + + var ( + text string + status = progress.Working + ) + + text = jm.Progress.String() + + if jm.Status == "Pull complete" || + jm.Status == "Already exists" || + strings.Contains(jm.Status, "Image is up to date") || + strings.Contains(jm.Status, "Downloaded newer image") { + status = progress.Done + } + + if jm.Error != nil { + status = progress.Error + text = jm.Error.Message + } + + w.Event(progress.Event{ + ID: jm.ID, + ParentID: parent, + Text: jm.Status, + Status: status, + StatusText: text, + }) +} diff --git a/local/compose/push.go b/local/compose/push.go new file mode 100644 index 000000000..9a4cd57e3 --- /dev/null +++ b/local/compose/push.go @@ -0,0 +1,137 @@ +/* + 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 compose + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + + "github.com/docker/compose-cli/config" + "github.com/docker/compose-cli/progress" + + "github.com/compose-spec/compose-go/types" + cliconfig "github.com/docker/cli/cli/config" + "github.com/docker/distribution/reference" + moby "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/jsonmessage" + "github.com/docker/docker/registry" + "github.com/pkg/errors" + "golang.org/x/sync/errgroup" +) + +func (s *composeService) Push(ctx context.Context, project *types.Project) error { + configFile, err := cliconfig.Load(config.Dir(ctx)) + if err != nil { + return err + } + eg, ctx := errgroup.WithContext(ctx) + + info, err := s.apiClient.Info(ctx) + if err != nil { + return err + } + if info.IndexServerAddress == "" { + info.IndexServerAddress = registry.IndexServer + } + + for _, service := range project.Services { + if service.Build == nil { + continue + } + service := service + eg.Go(func() error { + w := progress.ContextWriter(ctx) + + ref, err := reference.ParseNormalizedNamed(service.Image) + if err != nil { + return err + } + + repoInfo, err := registry.ParseRepositoryInfo(ref) + if err != nil { + return err + } + + key := repoInfo.Index.Name + if repoInfo.Index.Official { + key = info.IndexServerAddress + } + authConfig, err := configFile.GetAuthConfig(key) + if err != nil { + return err + } + + buf, err := json.Marshal(authConfig) + if err != nil { + return err + } + + stream, err := s.apiClient.ImagePush(ctx, service.Image, moby.ImagePushOptions{ + RegistryAuth: base64.URLEncoding.EncodeToString(buf), + }) + if err != nil { + return err + } + dec := json.NewDecoder(stream) + for { + var jm jsonmessage.JSONMessage + if err := dec.Decode(&jm); err != nil { + if err == io.EOF { + break + } + return err + } + if jm.Error != nil { + return errors.New(jm.Error.Message) + } + toPushProgressEvent("Pushing "+service.Name, jm, w) + } + return nil + }) + } + return eg.Wait() +} + +func toPushProgressEvent(prefix string, jm jsonmessage.JSONMessage, w progress.Writer) { + if jm.ID == "" { + // skipped + return + } + var ( + text string + status = progress.Working + ) + if jm.Status == "Pull complete" || jm.Status == "Already exists" { + status = progress.Done + } + if jm.Error != nil { + status = progress.Error + text = jm.Error.Message + } + if jm.Progress != nil { + text = jm.Progress.String() + } + w.Event(progress.Event{ + ID: fmt.Sprintf("Pushing %s: %s", prefix, jm.ID), + Text: jm.Status, + Status: status, + StatusText: text, + }) +} diff --git a/local/compose/start.go b/local/compose/start.go new file mode 100644 index 000000000..ab2e7ed52 --- /dev/null +++ b/local/compose/start.go @@ -0,0 +1,48 @@ +/* + 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 compose + +import ( + "context" + + "github.com/docker/compose-cli/api/compose" + + "github.com/compose-spec/compose-go/types" + "golang.org/x/sync/errgroup" +) + +func (s *composeService) Start(ctx context.Context, project *types.Project, consumer compose.LogConsumer) error { + var group *errgroup.Group + if consumer != nil { + eg, err := s.attach(ctx, project, consumer) + if err != nil { + return err + } + group = eg + } + + err := InDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error { + return s.startService(ctx, project, service) + }) + if err != nil { + return err + } + if group != nil { + return group.Wait() + } + return nil +} diff --git a/local/util.go b/local/compose/util.go similarity index 98% rename from local/util.go rename to local/compose/util.go index 6aef20703..dd9cbbcfe 100644 --- a/local/util.go +++ b/local/compose/util.go @@ -14,7 +14,7 @@ limitations under the License. */ -package local +package compose import ( "encoding/json" diff --git a/local/container.go b/local/container.go deleted file mode 100644 index a82fd8c03..000000000 --- a/local/container.go +++ /dev/null @@ -1,62 +0,0 @@ -/* - 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 ( - "io" - - moby "github.com/docker/docker/api/types" -) - -const ( - containerCreated = "created" - containerRestarting = "restarting" - containerRunning = "running" - containerRemoving = "removing" //nolint - containerPaused = "paused" //nolint - containerExited = "exited" //nolint - containerDead = "dead" //nolint -) - -var _ io.ReadCloser = containerStdout{} - -type containerStdout struct { - moby.HijackedResponse -} - -func (l containerStdout) Read(p []byte) (n int, err error) { - return l.Reader.Read(p) -} - -func (l containerStdout) Close() error { - l.HijackedResponse.Close() - return nil -} - -var _ io.WriteCloser = containerStdin{} - -type containerStdin struct { - moby.HijackedResponse -} - -func (c containerStdin) Write(p []byte) (n int, err error) { - return c.Conn.Write(p) -} - -func (c containerStdin) Close() error { - return c.CloseWrite() -} diff --git a/local/containers.go b/local/containers.go index b05e56992..3c6d7e6b3 100644 --- a/local/containers.go +++ b/local/containers.go @@ -35,6 +35,7 @@ import ( "github.com/docker/compose-cli/api/containers" "github.com/docker/compose-cli/errdefs" + "github.com/docker/compose-cli/local/moby" ) type containerService struct { @@ -58,8 +59,8 @@ func (cs *containerService) Inspect(ctx context.Context, id string) (containers. command = strings.Join(c.Config.Cmd, " ") } - rc := toRuntimeConfig(&c) - hc := toHostConfig(&c) + rc := moby.ToRuntimeConfig(&c) + hc := moby.ToHostConfig(&c) return containers.Container{ ID: stringid.TruncateID(c.ID), @@ -92,7 +93,7 @@ func (cs *containerService) List(ctx context.Context, all bool) ([]containers.Co // statuses. We also need to add a `Created` property on the gRPC side. Status: container.Status, Command: container.Command, - Ports: toPorts(container.Ports), + Ports: moby.ToPorts(container.Ports), }) } @@ -100,7 +101,7 @@ func (cs *containerService) List(ctx context.Context, all bool) ([]containers.Co } func (cs *containerService) Run(ctx context.Context, r containers.ContainerConfig) error { - exposedPorts, hostBindings, err := fromPorts(r.Ports) + exposedPorts, hostBindings, err := moby.FromPorts(r.Ports) if err != nil { return err } @@ -127,7 +128,7 @@ func (cs *containerService) Run(ctx context.Context, r containers.ContainerConfi PortBindings: hostBindings, Mounts: mounts, AutoRemove: r.AutoRemove, - RestartPolicy: toRestartPolicy(r.RestartPolicyCondition), + RestartPolicy: moby.ToRestartPolicy(r.RestartPolicyCondition), Resources: container.Resources{ NanoCPUs: int64(r.CPULimit * 1e9), Memory: int64(r.MemLimit), diff --git a/local/moby/container.go b/local/moby/container.go new file mode 100644 index 000000000..faaf0baa1 --- /dev/null +++ b/local/moby/container.go @@ -0,0 +1,75 @@ +/* + 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 moby + +import ( + "io" + + moby "github.com/docker/docker/api/types" +) + +const ( + // ContainerCreated created status + ContainerCreated = "created" + // ContainerRestarting restarting status + ContainerRestarting = "restarting" + // ContainerRunning running status + ContainerRunning = "running" + // ContainerRemoving removing status + ContainerRemoving = "removing" //nolint + // ContainerPaused paused status + ContainerPaused = "paused" //nolint + // ContainerExited exited status + ContainerExited = "exited" //nolint + // ContainerDead dead status + ContainerDead = "dead" //nolint +) + +var _ io.ReadCloser = ContainerStdout{} + +// ContainerStdout implement ReadCloser for moby.HijackedResponse +type ContainerStdout struct { + moby.HijackedResponse +} + +// Read implement io.ReadCloser +func (l ContainerStdout) Read(p []byte) (n int, err error) { + return l.Reader.Read(p) +} + +// Close implement io.ReadCloser +func (l ContainerStdout) Close() error { + l.HijackedResponse.Close() + return nil +} + +var _ io.WriteCloser = ContainerStdin{} + +// ContainerStdin implement WriteCloser for moby.HijackedResponse +type ContainerStdin struct { + moby.HijackedResponse +} + +// Write implement io.WriteCloser +func (c ContainerStdin) Write(p []byte) (n int, err error) { + return c.Conn.Write(p) +} + +// Close implement io.WriteCloser +func (c ContainerStdin) Close() error { + return c.CloseWrite() +} diff --git a/local/convert.go b/local/moby/convert.go similarity index 82% rename from local/convert.go rename to local/moby/convert.go index 2b567f9df..39443ebeb 100644 --- a/local/convert.go +++ b/local/moby/convert.go @@ -14,7 +14,7 @@ limitations under the License. */ -package local +package moby import ( "fmt" @@ -31,7 +31,8 @@ import ( "github.com/docker/compose-cli/api/containers" ) -func toRuntimeConfig(m *types.ContainerJSON) *containers.RuntimeConfig { +// ToRuntimeConfig convert into containers.RuntimeConfig +func ToRuntimeConfig(m *types.ContainerJSON) *containers.RuntimeConfig { if m.Config == nil { return nil } @@ -66,7 +67,8 @@ func toRuntimeConfig(m *types.ContainerJSON) *containers.RuntimeConfig { } } -func toHostConfig(m *types.ContainerJSON) *containers.HostConfig { +// ToHostConfig convert into containers.HostConfig +func ToHostConfig(m *types.ContainerJSON) *containers.HostConfig { if m.HostConfig == nil { return nil } @@ -79,7 +81,8 @@ func toHostConfig(m *types.ContainerJSON) *containers.HostConfig { } } -func toPorts(ports []types.Port) []containers.Port { +// ToPorts convert into containers.Port +func ToPorts(ports []types.Port) []containers.Port { result := []containers.Port{} for _, port := range ports { result = append(result, containers.Port{ @@ -93,7 +96,8 @@ func toPorts(ports []types.Port) []containers.Port { return result } -func toMobyEnv(environment compose.MappingWithEquals) []string { +// ToMobyEnv convert into []string +func ToMobyEnv(environment compose.MappingWithEquals) []string { var env []string for k, v := range environment { if v == nil { @@ -105,7 +109,8 @@ func toMobyEnv(environment compose.MappingWithEquals) []string { return env } -func toMobyHealthCheck(check *compose.HealthCheckConfig) *container.HealthConfig { +// ToMobyHealthCheck convert into container.HealthConfig +func ToMobyHealthCheck(check *compose.HealthCheckConfig) *container.HealthConfig { if check == nil { return nil } @@ -136,7 +141,8 @@ func toMobyHealthCheck(check *compose.HealthCheckConfig) *container.HealthConfig } } -func toSeconds(d *compose.Duration) *int { +// ToSeconds convert into seconds +func ToSeconds(d *compose.Duration) *int { if d == nil { return nil } @@ -144,7 +150,8 @@ func toSeconds(d *compose.Duration) *int { return &s } -func fromPorts(ports []containers.Port) (map[nat.Port]struct{}, map[nat.Port][]nat.PortBinding, error) { +// FromPorts convert to nat.Port / nat.PortBinding +func FromPorts(ports []containers.Port) (map[nat.Port]struct{}, map[nat.Port][]nat.PortBinding, error) { var ( exposedPorts = make(map[nat.Port]struct{}, len(ports)) bindings = make(map[nat.Port][]nat.PortBinding) @@ -187,7 +194,8 @@ func fromRestartPolicyName(m string) string { } } -func toRestartPolicy(p string) container.RestartPolicy { +// ToRestartPolicy convert to container.RestartPolicy +func ToRestartPolicy(p string) container.RestartPolicy { switch p { case containers.RestartPolicyAny: return container.RestartPolicy{Name: "always"} diff --git a/local/convert_test.go b/local/moby/convert_test.go similarity index 95% rename from local/convert_test.go rename to local/moby/convert_test.go index fa93138db..a9e5a4135 100644 --- a/local/convert_test.go +++ b/local/moby/convert_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package local +package moby import ( "testing" @@ -34,7 +34,7 @@ func TestToRuntimeConfig(t *testing.T) { Labels: map[string]string{"foo1": "bar1", "foo2": "bar2"}, }, } - rc := toRuntimeConfig(m) + rc := ToRuntimeConfig(m) res := &containers.RuntimeConfig{ Env: map[string]string{"FOO1": "BAR1", "FOO2": "BAR2"}, Labels: []string{"foo1=bar1", "foo2=bar2"}, @@ -63,7 +63,7 @@ func TestToHostConfig(t *testing.T) { }, ContainerJSONBase: base, } - hc := toHostConfig(m) + hc := ToHostConfig(m) res := &containers.HostConfig{ AutoRemove: true, RestartPolicy: containers.RestartPolicyNone, @@ -96,6 +96,6 @@ func TestToRestartPolicy(t *testing.T) { {Name: "no"}, } for i, p := range ours { - assert.Equal(t, toRestartPolicy(p), moby[i]) + assert.Equal(t, ToRestartPolicy(p), moby[i]) } }