Merge pull request #1071 from docker/local_ecs_moby

use compose-in-go code to implement local ECS simulation context
This commit is contained in:
Guillaume Tardif 2020-12-17 18:34:48 +01:00 committed by GitHub
commit a17e397df3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 243 additions and 153 deletions

View File

@ -64,7 +64,7 @@ func (cs *aciComposeService) Start(ctx context.Context, project *types.Project,
return errdefs.ErrNotImplemented return errdefs.ErrNotImplemented
} }
func (cs *aciComposeService) Up(ctx context.Context, project *types.Project, detach bool) error { func (cs *aciComposeService) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error {
logrus.Debugf("Up on project with name %q", project.Name) logrus.Debugf("Up on project with name %q", project.Name)
if err := autocreateFileshares(ctx, project); err != nil { if err := autocreateFileshares(ctx, project); err != nil {
@ -102,14 +102,14 @@ func (cs aciComposeService) warnKeepVolumeOnDown(ctx context.Context, projectNam
return nil return nil
} }
func (cs *aciComposeService) Down(ctx context.Context, project string) error { func (cs *aciComposeService) Down(ctx context.Context, projectName string, options compose.DownOptions) error {
logrus.Debugf("Down on project with name %q", project) logrus.Debugf("Down on projectName with name %q", projectName)
if err := cs.warnKeepVolumeOnDown(ctx, project); err != nil { if err := cs.warnKeepVolumeOnDown(ctx, projectName); err != nil {
return err return err
} }
cg, err := deleteACIContainerGroup(ctx, cs.ctx, project) cg, err := deleteACIContainerGroup(ctx, cs.ctx, projectName)
if err != nil { if err != nil {
return err return err
} }
@ -198,6 +198,6 @@ func (cs *aciComposeService) Logs(ctx context.Context, projectName string, consu
return errdefs.ErrNotImplemented return errdefs.ErrNotImplemented
} }
func (cs *aciComposeService) Convert(ctx context.Context, project *types.Project, format string) ([]byte, error) { func (cs *aciComposeService) Convert(ctx context.Context, project *types.Project, options compose.ConvertOptions) ([]byte, error) {
return nil, errdefs.ErrNotImplemented return nil, errdefs.ErrNotImplemented
} }

View File

@ -48,11 +48,11 @@ func (c *composeService) Start(ctx context.Context, project *types.Project, cons
return errdefs.ErrNotImplemented return errdefs.ErrNotImplemented
} }
func (c *composeService) Up(context.Context, *types.Project, bool) error { func (c *composeService) Up(context.Context, *types.Project, compose.UpOptions) error {
return errdefs.ErrNotImplemented return errdefs.ErrNotImplemented
} }
func (c *composeService) Down(context.Context, string) error { func (c *composeService) Down(context.Context, string, compose.DownOptions) error {
return errdefs.ErrNotImplemented return errdefs.ErrNotImplemented
} }
@ -68,6 +68,6 @@ func (c *composeService) List(context.Context, string) ([]compose.Stack, error)
return nil, errdefs.ErrNotImplemented return nil, errdefs.ErrNotImplemented
} }
func (c *composeService) Convert(context.Context, *types.Project, string) ([]byte, error) { func (c *composeService) Convert(context.Context, *types.Project, compose.ConvertOptions) ([]byte, error) {
return nil, errdefs.ErrNotImplemented return nil, errdefs.ErrNotImplemented
} }

View File

@ -35,9 +35,9 @@ type Service interface {
// Start executes the equivalent to a `compose start` // Start executes the equivalent to a `compose start`
Start(ctx context.Context, project *types.Project, consumer LogConsumer) error Start(ctx context.Context, project *types.Project, consumer LogConsumer) error
// Up executes the equivalent to a `compose up` // Up executes the equivalent to a `compose up`
Up(ctx context.Context, project *types.Project, detach bool) error Up(ctx context.Context, project *types.Project, options UpOptions) error
// Down executes the equivalent to a `compose down` // Down executes the equivalent to a `compose down`
Down(ctx context.Context, projectName string) error Down(ctx context.Context, projectName string, options DownOptions) error
// Logs executes the equivalent to a `compose logs` // Logs executes the equivalent to a `compose logs`
Logs(ctx context.Context, projectName string, consumer LogConsumer, options LogOptions) error Logs(ctx context.Context, projectName string, consumer LogConsumer, options LogOptions) error
// Ps executes the equivalent to a `compose ps` // Ps executes the equivalent to a `compose ps`
@ -45,7 +45,25 @@ type Service interface {
// List executes the equivalent to a `docker stack ls` // List executes the equivalent to a `docker stack ls`
List(ctx context.Context, projectName string) ([]Stack, error) List(ctx context.Context, projectName string) ([]Stack, error)
// Convert translate compose model into backend's native format // Convert translate compose model into backend's native format
Convert(ctx context.Context, project *types.Project, format string) ([]byte, error) Convert(ctx context.Context, project *types.Project, options ConvertOptions) ([]byte, error)
}
// UpOptions group options of the Up API
type UpOptions struct {
// Detach will create services and return immediately
Detach bool
}
// DownOptions group options of the Down API
type DownOptions struct {
// RemoveOrphans will cleanup containers that are not declared on the compose model but own the same labels
RemoveOrphans bool
}
// ConvertOptions group options of the Convert API
type ConvertOptions struct {
// Format define the output format used to dump converted application model (json|yaml)
Format string
} }
// PortPublisher hold status about published port // PortPublisher hold status about published port

View File

@ -20,6 +20,8 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/docker/compose-cli/api/compose"
"github.com/compose-spec/compose-go/cli" "github.com/compose-spec/compose-go/cli"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -61,7 +63,9 @@ func runConvert(ctx context.Context, opts composeOptions) error {
return err return err
} }
json, err = c.ComposeService().Convert(ctx, project, opts.Format) json, err = c.ComposeService().Convert(ctx, project, compose.ConvertOptions{
Format: opts.Format,
})
if err != nil { if err != nil {
return err return err
} }

View File

@ -19,6 +19,8 @@ package compose
import ( import (
"context" "context"
"github.com/docker/compose-cli/api/compose"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/client"
@ -52,7 +54,9 @@ func runDown(ctx context.Context, opts composeOptions) error {
if err != nil { if err != nil {
return "", err return "", err
} }
return projectName, c.ComposeService().Down(ctx, projectName) return projectName, c.ComposeService().Down(ctx, projectName, compose.DownOptions{
RemoveOrphans: false,
})
}) })
return err return err
} }

View File

@ -40,7 +40,7 @@ func upCommand(contextType string) *cobra.Command {
Short: "Create and start containers", Short: "Create and start containers",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
switch contextType { switch contextType {
case store.LocalContextType, store.DefaultContextType: case store.LocalContextType, store.DefaultContextType, store.EcsLocalSimulationContextType:
return runCreateStart(cmd.Context(), opts, args) return runCreateStart(cmd.Context(), opts, args)
default: default:
return runUp(cmd.Context(), opts, args) return runUp(cmd.Context(), opts, args)
@ -68,7 +68,9 @@ func runUp(ctx context.Context, opts composeOptions, services []string) error {
} }
_, err = progress.Run(ctx, func(ctx context.Context) (string, error) { _, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
return "", c.ComposeService().Up(ctx, project, opts.Detach) return "", c.ComposeService().Up(ctx, project, compose.UpOptions{
Detach: opts.Detach,
})
}) })
return err return err
} }
@ -96,7 +98,7 @@ func runCreateStart(ctx context.Context, opts composeOptions, services []string)
fmt.Println("Gracefully stopping...") fmt.Println("Gracefully stopping...")
ctx = context.Background() ctx = context.Background()
_, err = progress.Run(ctx, func(ctx context.Context) (string, error) { _, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
return "", c.ComposeService().Down(ctx, project.Name) return "", c.ComposeService().Down(ctx, project.Name, compose.DownOptions{})
}) })
} }
return err return err

View File

@ -23,6 +23,8 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/docker/compose-cli/api/compose"
ecsapi "github.com/aws/aws-sdk-go/service/ecs" ecsapi "github.com/aws/aws-sdk-go/service/ecs"
"github.com/aws/aws-sdk-go/service/elbv2" "github.com/aws/aws-sdk-go/service/elbv2"
cloudmapapi "github.com/aws/aws-sdk-go/service/servicediscovery" cloudmapapi "github.com/aws/aws-sdk-go/service/servicediscovery"
@ -37,13 +39,13 @@ import (
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
) )
func (b *ecsAPIService) Convert(ctx context.Context, project *types.Project, format string) ([]byte, error) { func (b *ecsAPIService) Convert(ctx context.Context, project *types.Project, options compose.ConvertOptions) ([]byte, error) {
template, err := b.convert(ctx, project) template, err := b.convert(ctx, project)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return marshall(template, format) return marshall(template, options.Format)
} }
func (b *ecsAPIService) convert(ctx context.Context, project *types.Project) (*cloudformation.Template, error) { func (b *ecsAPIService) convert(ctx context.Context, project *types.Project) (*cloudformation.Template, error) {

View File

@ -19,11 +19,13 @@ package ecs
import ( import (
"context" "context"
"github.com/docker/compose-cli/api/compose"
"github.com/docker/compose-cli/progress" "github.com/docker/compose-cli/progress"
) )
func (b *ecsAPIService) Down(ctx context.Context, project string) error { func (b *ecsAPIService) Down(ctx context.Context, projectName string, options compose.DownOptions) error {
resources, err := b.aws.ListStackResources(ctx, project) resources, err := b.aws.ListStackResources(ctx, projectName)
if err != nil { if err != nil {
return err return err
} }
@ -38,16 +40,16 @@ func (b *ecsAPIService) Down(ctx context.Context, project string) error {
return err return err
} }
previousEvents, err := b.previousStackEvents(ctx, project) previousEvents, err := b.previousStackEvents(ctx, projectName)
if err != nil { if err != nil {
return err return err
} }
err = b.aws.DeleteStack(ctx, project) err = b.aws.DeleteStack(ctx, projectName)
if err != nil { if err != nil {
return err return err
} }
return b.WaitStackCompletion(ctx, project, stackDelete, previousEvents...) return b.WaitStackCompletion(ctx, projectName, stackDelete, previousEvents...)
} }
func (b *ecsAPIService) previousStackEvents(ctx context.Context, project string) ([]string, error) { func (b *ecsAPIService) previousStackEvents(ctx context.Context, project string) ([]string, error) {

View File

@ -19,6 +19,8 @@ package local
import ( import (
"context" "context"
local_compose "github.com/docker/compose-cli/local/compose"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/compose"
@ -38,17 +40,19 @@ func init() {
} }
type ecsLocalSimulation struct { type ecsLocalSimulation struct {
moby *client.Client moby *client.Client
compose compose.Service
} }
func service(ctx context.Context) (backend.Service, error) { func service(ctx context.Context) (backend.Service, error) {
apiClient, err := client.NewClientWithOpts(client.FromEnv) apiClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &ecsLocalSimulation{ return &ecsLocalSimulation{
moby: apiClient, moby: apiClient,
compose: local_compose.NewComposeService(apiClient),
}, nil }, nil
} }

View File

@ -17,80 +17,76 @@
package local package local
import ( import (
"bufio"
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
types2 "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/pkg/errors"
"github.com/sanathkr/go-yaml"
"golang.org/x/mod/semver"
"github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/compose"
"github.com/docker/compose-cli/errdefs" "github.com/docker/compose-cli/errdefs"
"github.com/sanathkr/go-yaml"
) )
func (e ecsLocalSimulation) Build(ctx context.Context, project *types.Project) error { func (e ecsLocalSimulation) Build(ctx context.Context, project *types.Project) error {
return errdefs.ErrNotImplemented return e.compose.Build(ctx, project)
} }
func (e ecsLocalSimulation) Push(ctx context.Context, project *types.Project) error { func (e ecsLocalSimulation) Push(ctx context.Context, project *types.Project) error {
return errdefs.ErrNotImplemented return e.compose.Push(ctx, project)
} }
func (e ecsLocalSimulation) Pull(ctx context.Context, project *types.Project) error { func (e ecsLocalSimulation) Pull(ctx context.Context, project *types.Project) error {
return errdefs.ErrNotImplemented return e.compose.Pull(ctx, project)
} }
func (e ecsLocalSimulation) Create(ctx context.Context, project *types.Project) error { func (e ecsLocalSimulation) Create(ctx context.Context, project *types.Project) error {
return errdefs.ErrNotImplemented enhanced, err := e.enhanceForLocalSimulation(project)
}
func (e ecsLocalSimulation) Start(ctx context.Context, project *types.Project, consumer compose.LogConsumer) error {
return errdefs.ErrNotImplemented
}
func (e ecsLocalSimulation) Up(ctx context.Context, project *types.Project, detach bool) error {
cmd := exec.Command("docker-compose", "version", "--short")
b := bytes.Buffer{}
b.WriteString("v")
cmd.Stdout = bufio.NewWriter(&b)
err := cmd.Run()
if err != nil {
return errors.Wrap(err, "ECS simulation mode require Docker-compose 1.27")
}
version := semver.MajorMinor(strings.TrimSpace(b.String()))
if version == "" {
return fmt.Errorf("can't parse docker-compose version: %s", b.String())
}
if semver.Compare(version, "v1.27") < 0 {
return fmt.Errorf("ECS simulation mode require Docker-compose 1.27, found %s", version)
}
converted, err := e.Convert(ctx, project, "json")
if err != nil { if err != nil {
return err return err
} }
cmd = exec.Command("docker-compose", "--context", "default", "--project-directory", project.WorkingDir, "--project-name", project.Name, "-f", "-", "up") return e.compose.Create(ctx, enhanced)
cmd.Stdin = strings.NewReader(string(converted))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
} }
func (e ecsLocalSimulation) Convert(ctx context.Context, project *types.Project, format string) ([]byte, error) { func (e ecsLocalSimulation) Start(ctx context.Context, project *types.Project, consumer compose.LogConsumer) error {
return e.compose.Start(ctx, project, consumer)
}
func (e ecsLocalSimulation) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error {
return errdefs.ErrNotImplemented
}
func (e ecsLocalSimulation) Convert(ctx context.Context, project *types.Project, options compose.ConvertOptions) ([]byte, error) {
enhanced, err := e.enhanceForLocalSimulation(project)
if err != nil {
return nil, err
}
delete(enhanced.Networks, "default")
config := map[string]interface{}{
"services": enhanced.Services,
"networks": enhanced.Networks,
"volumes": enhanced.Volumes,
"secrets": enhanced.Secrets,
"configs": enhanced.Configs,
}
switch options.Format {
case "json":
return json.MarshalIndent(config, "", " ")
case "yaml":
return yaml.Marshal(config)
default:
return nil, fmt.Errorf("unsupported format %q", options)
}
}
func (e ecsLocalSimulation) enhanceForLocalSimulation(project *types.Project) (*types.Project, error) {
project.Networks["credentials_network"] = types.NetworkConfig{ project.Networks["credentials_network"] = types.NetworkConfig{
Name: "credentials_network",
Driver: "bridge", Driver: "bridge",
Ipam: types.IPAMConfig{ Ipam: types.IPAMConfig{
Config: []*types.IPAMPool{ Config: []*types.IPAMPool{
@ -148,68 +144,21 @@ func (e ecsLocalSimulation) Convert(ctx context.Context, project *types.Project,
}, },
}, },
}) })
return project, nil
delete(project.Networks, "default")
config := map[string]interface{}{
"services": project.Services,
"networks": project.Networks,
"volumes": project.Volumes,
"secrets": project.Secrets,
"configs": project.Configs,
}
switch format {
case "json":
return json.MarshalIndent(config, "", " ")
case "yaml":
return yaml.Marshal(config)
default:
return nil, fmt.Errorf("unsupported format %q", format)
}
} }
func (e ecsLocalSimulation) Down(ctx context.Context, projectName string) error { func (e ecsLocalSimulation) Down(ctx context.Context, projectName string, options compose.DownOptions) error {
cmd := exec.Command("docker-compose", "--context", "default", "--project-name", projectName, "-f", "-", "down", "--remove-orphans") options.RemoveOrphans = true
cmd.Stdin = strings.NewReader(string(` return e.compose.Down(ctx, projectName, options)
services:
ecs-local-endpoints:
image: "amazon/amazon-ecs-local-container-endpoints"
`))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
} }
func (e ecsLocalSimulation) Logs(ctx context.Context, projectName string, consumer compose.LogConsumer, options compose.LogOptions) error { func (e ecsLocalSimulation) Logs(ctx context.Context, projectName string, consumer compose.LogConsumer, options compose.LogOptions) error {
list, err := e.moby.ContainerList(ctx, types2.ContainerListOptions{ return e.compose.Logs(ctx, projectName, consumer, options)
Filters: filters.NewArgs(filters.Arg("label", "com.docker.compose.project="+projectName)),
})
if err != nil {
return err
}
services := map[string]types.ServiceConfig{}
for _, c := range list {
services[c.Labels["com.docker.compose.service"]] = types.ServiceConfig{
Image: "unused",
}
}
marshal, err := yaml.Marshal(map[string]interface{}{
"services": services,
})
if err != nil {
return err
}
cmd := exec.Command("docker-compose", "--context", "default", "--project-name", projectName, "-f", "-", "logs", "-f")
cmd.Stdin = strings.NewReader(string(marshal))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
} }
func (e ecsLocalSimulation) Ps(ctx context.Context, projectName string) ([]compose.ContainerSummary, error) { func (e ecsLocalSimulation) Ps(ctx context.Context, projectName string) ([]compose.ContainerSummary, error) {
return nil, errors.Wrap(errdefs.ErrNotImplemented, "use docker-compose ps") return e.compose.Ps(ctx, projectName)
} }
func (e ecsLocalSimulation) List(ctx context.Context, projectName string) ([]compose.Stack, error) { func (e ecsLocalSimulation) List(ctx context.Context, projectName string) ([]compose.Stack, error) {
return nil, errors.Wrap(errdefs.ErrNotImplemented, "use docker-compose ls") return e.compose.List(ctx, projectName)
} }

View File

@ -49,14 +49,14 @@ func (b *ecsAPIService) Start(ctx context.Context, project *types.Project, consu
return errdefs.ErrNotImplemented return errdefs.ErrNotImplemented
} }
func (b *ecsAPIService) Up(ctx context.Context, project *types.Project, detach bool) error { func (b *ecsAPIService) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error {
err := b.aws.CheckRequirements(ctx, b.Region) err := b.aws.CheckRequirements(ctx, b.Region)
if err != nil { if err != nil {
return err return err
} }
template, err := b.Convert(ctx, project, "yaml") template, err := b.Convert(ctx, project, compose.ConvertOptions{Format: "yaml"})
if err != nil { if err != nil {
return err return err
} }
@ -82,7 +82,7 @@ func (b *ecsAPIService) Up(ctx context.Context, project *types.Project, detach b
return err return err
} }
} }
if detach { if options.Detach {
return nil return nil
} }
signalChan := make(chan os.Signal, 1) signalChan := make(chan os.Signal, 1)
@ -90,7 +90,7 @@ func (b *ecsAPIService) Up(ctx context.Context, project *types.Project, detach b
go func() { go func() {
<-signalChan <-signalChan
fmt.Println("user interrupted deployment. Deleting stack...") fmt.Println("user interrupted deployment. Deleting stack...")
b.Down(ctx, project.Name) // nolint:errcheck b.Down(ctx, project.Name, compose.DownOptions{}) // nolint:errcheck
}() }()
err = b.WaitStackCompletion(ctx, project.Name, operation) err = b.WaitStackCompletion(ctx, project.Name, operation)

View File

@ -158,14 +158,14 @@ func (cs *composeService) Start(ctx context.Context, project *types.Project, con
return errdefs.ErrNotImplemented return errdefs.ErrNotImplemented
} }
func (cs *composeService) Up(ctx context.Context, project *types.Project, detach bool) error { func (cs *composeService) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error {
fmt.Printf("Up command on project %q", project.Name) fmt.Printf("Up command on project %q", project.Name)
return nil return nil
} }
func (cs *composeService) Down(ctx context.Context, project string) error { func (cs *composeService) Down(ctx context.Context, projectName string, options compose.DownOptions) error {
fmt.Printf("Down command on project %q", project) fmt.Printf("Down command on project %q", projectName)
return nil return nil
} }
@ -179,6 +179,6 @@ func (cs *composeService) Logs(ctx context.Context, projectName string, consumer
return errdefs.ErrNotImplemented return errdefs.ErrNotImplemented
} }
func (cs *composeService) Convert(ctx context.Context, project *types.Project, format string) ([]byte, error) { func (cs *composeService) Convert(ctx context.Context, project *types.Project, options compose.ConvertOptions) ([]byte, error) {
return nil, errdefs.ErrNotImplemented return nil, errdefs.ErrNotImplemented
} }

1
go.mod
View File

@ -51,7 +51,6 @@ require (
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.6.1
github.com/valyala/fasttemplate v1.2.1 // indirect github.com/valyala/fasttemplate v1.2.1 // indirect
golang.org/x/mod v0.3.0
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b golang.org/x/net v0.0.0-20201110031124-69a78807bb2b
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58 golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9

View File

@ -40,7 +40,7 @@ type composeService struct {
apiClient *client.Client apiClient *client.Client
} }
func (s *composeService) Up(ctx context.Context, project *types.Project, detach bool) error { func (s *composeService) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error {
return errdefs2.ErrNotImplemented return errdefs2.ErrNotImplemented
} }
@ -54,13 +54,13 @@ func getContainerName(c moby.Container) string {
return c.Names[0][1:] return c.Names[0][1:]
} }
func (s *composeService) Convert(ctx context.Context, project *types.Project, format string) ([]byte, error) { func (s *composeService) Convert(ctx context.Context, project *types.Project, options compose.ConvertOptions) ([]byte, error) {
switch format { switch options.Format {
case "json": case "json":
return json.MarshalIndent(project, "", " ") return json.MarshalIndent(project, "", " ")
case "yaml": case "yaml":
return yaml.Marshal(project) return yaml.Marshal(project)
default: default:
return nil, fmt.Errorf("unsupported format %q", format) return nil, fmt.Errorf("unsupported format %q", options)
} }
} }

View File

@ -21,6 +21,8 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/docker/compose-cli/api/compose"
"github.com/docker/compose-cli/progress" "github.com/docker/compose-cli/progress"
"github.com/compose-spec/compose-go/cli" "github.com/compose-spec/compose-go/cli"
@ -30,7 +32,7 @@ import (
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
func (s *composeService) Down(ctx context.Context, projectName string) error { func (s *composeService) Down(ctx context.Context, projectName string, options compose.DownOptions) error {
eg, _ := errgroup.WithContext(ctx) eg, _ := errgroup.WithContext(ctx)
w := progress.ContextWriter(ctx) w := progress.ContextWriter(ctx)
@ -39,10 +41,27 @@ func (s *composeService) Down(ctx context.Context, projectName string) error {
return err return err
} }
err = InReverseDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error { containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
filter := filters.NewArgs(projectFilter(project.Name), serviceFilter(service.Name)) Filters: filters.NewArgs(projectFilter(project.Name)),
return s.removeContainers(ctx, w, eg, filter) All: true,
}) })
if err != nil {
return err
}
err = InReverseDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error {
serviceContainers, others := split(containers, isService(service.Name))
err := s.removeContainers(ctx, w, eg, serviceContainers)
containers = others
return err
})
if options.RemoveOrphans {
err := s.removeContainers(ctx, w, eg, containers)
if err != nil {
return err
}
}
if err != nil { if err != nil {
return err return err
@ -70,14 +89,7 @@ func (s *composeService) Down(ctx context.Context, projectName string) error {
return eg.Wait() return eg.Wait()
} }
func (s *composeService) removeContainers(ctx context.Context, w progress.Writer, eg *errgroup.Group, filter filters.Args) error { func (s *composeService) removeContainers(ctx context.Context, w progress.Writer, eg *errgroup.Group, containers []moby.Container) error {
containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
Filters: filter,
All: true,
})
if err != nil {
return err
}
for _, container := range containers { for _, container := range containers {
eg.Go(func() error { eg.Go(func() error {
eventName := "Container " + getContainerName(container) eventName := "Container " + getContainerName(container)
@ -147,3 +159,25 @@ func loadProjectOptionsFromLabels(c moby.Container) (*cli.ProjectOptions, error)
cli.WithWorkingDirectory(c.Labels[workingDirLabel]), cli.WithWorkingDirectory(c.Labels[workingDirLabel]),
cli.WithName(c.Labels[projectLabel])) cli.WithName(c.Labels[projectLabel]))
} }
type containerPredicate func(c moby.Container) bool
func isService(service string) containerPredicate {
return func(c moby.Container) bool {
return c.Labels[serviceLabel] == service
}
}
// split return a container slice with elements to match predicate
func split(containers []moby.Container, predicate containerPredicate) ([]moby.Container, []moby.Container) {
var right []moby.Container
var left []moby.Container
for _, c := range containers {
if predicate(c) {
right = append(right, c)
} else {
left = append(left, c)
}
}
return right, left
}

View File

@ -34,6 +34,7 @@ func (s *composeService) Logs(ctx context.Context, projectName string, consumer
Filters: filters.NewArgs( Filters: filters.NewArgs(
projectFilter(projectName), projectFilter(projectName),
), ),
All: true,
}) })
ignore := func(string) bool { ignore := func(string) bool {

View File

@ -19,6 +19,8 @@ package proxy
import ( import (
"context" "context"
"github.com/docker/compose-cli/api/compose"
"github.com/compose-spec/compose-go/cli" "github.com/compose-spec/compose-go/cli"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
@ -30,7 +32,8 @@ func (p *proxy) Up(ctx context.Context, request *composev1.ComposeUpRequest) (*c
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &composev1.ComposeUpResponse{ProjectName: project.Name}, Client(ctx).ComposeService().Up(ctx, project, true) err = Client(ctx).ComposeService().Up(ctx, project, compose.UpOptions{Detach: true})
return &composev1.ComposeUpResponse{ProjectName: project.Name}, err
} }
func (p *proxy) Down(ctx context.Context, request *composev1.ComposeDownRequest) (*composev1.ComposeDownResponse, error) { func (p *proxy) Down(ctx context.Context, request *composev1.ComposeDownRequest) (*composev1.ComposeDownResponse, error) {
@ -42,7 +45,8 @@ func (p *proxy) Down(ctx context.Context, request *composev1.ComposeDownRequest)
} }
projectName = project.Name projectName = project.Name
} }
return &composev1.ComposeDownResponse{ProjectName: projectName}, Client(ctx).ComposeService().Down(ctx, projectName) err := Client(ctx).ComposeService().Down(ctx, projectName, compose.DownOptions{})
return &composev1.ComposeDownResponse{ProjectName: projectName}, err
} }
func (p *proxy) Services(ctx context.Context, request *composev1.ComposeServicesRequest) (*composev1.ComposeServicesResponse, error) { func (p *proxy) Services(ctx context.Context, request *composev1.ComposeServicesRequest) (*composev1.ComposeServicesResponse, error) {

View File

@ -0,0 +1,7 @@
services:
ping:
image: busybox:1.27.2
command: ping localhost -c 1
hello:
image: busybox:1.31.0-uclibc
command: echo hello

View File

@ -0,0 +1,60 @@
/*
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 e2e
import (
"strings"
"testing"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
. "github.com/docker/compose-cli/tests/framework"
)
func TestLocalComposeLogs(t *testing.T) {
c := NewParallelE2eCLI(t, binDir)
const projectName = "compose-e2e-logs"
t.Run("up", func(t *testing.T) {
c.RunDockerCmd("compose", "up", "-d", "-f", "./fixtures/logs-test/compose.yaml", "--project-name", projectName, "-d")
})
t.Run("logs", func(t *testing.T) {
res := c.RunDockerCmd("compose", "logs", "--project-name", projectName)
res.Assert(t, icmd.Expected{Out: `PING localhost (127.0.0.1)`})
res.Assert(t, icmd.Expected{Out: `hello`})
})
t.Run("logs ping", func(t *testing.T) {
res := c.RunDockerCmd("compose", "logs", "--project-name", projectName, "ping")
res.Assert(t, icmd.Expected{Out: `PING localhost (127.0.0.1)`})
assert.Assert(t, !strings.Contains(res.Stdout(), "hello"))
})
t.Run("logs hello", func(t *testing.T) {
res := c.RunDockerCmd("compose", "logs", "--project-name", projectName, "hello", "ping")
res.Assert(t, icmd.Expected{Out: `PING localhost (127.0.0.1)`})
res.Assert(t, icmd.Expected{Out: `hello`})
})
t.Run("down", func(t *testing.T) {
_ = c.RunDockerCmd("compose", "down", "--project-name", projectName)
})
}