diff --git a/local/backend.go b/local/backend.go index 11f9ff496..78d1195cb 100644 --- a/local/backend.go +++ b/local/backend.go @@ -73,3 +73,4 @@ func (s *local) ResourceService() resources.Service { } + diff --git a/local/compose.go b/local/compose.go index 3091d5f25..5997134ce 100644 --- a/local/compose.go +++ b/local/compose.go @@ -22,8 +22,10 @@ import ( "context" "encoding/json" "fmt" + "golang.org/x/sync/errgroup" "io" "path/filepath" + "strconv" "strings" "sync" @@ -169,13 +171,42 @@ func (s *local) Down(ctx context.Context, projectName string) error { if err != nil { return err } + + eg, ctx := errgroup.WithContext(ctx) + w := progress.ContextWriter(ctx) for _, c := range list { - err := s.containerService.Stop(ctx, c.ID, nil) - if err != nil { - return err - } + container := c + eg.Go(func() error { + w.Event(progress.Event{ + ID: getContainerName(container), + Text: "Stopping", + Status: progress.Working, + Done: false, + }) + err := s.containerService.Stop(ctx, container.ID, nil) + if err != nil { + return err + } + w.Event(progress.Event{ + ID: getContainerName(container), + Text: "Removing", + Status: progress.Working, + Done: false, + }) + err = s.containerService.Delete(ctx, container.ID, containers.DeleteRequest{}) + if err != nil { + return err + } + w.Event(progress.Event{ + ID: getContainerName(container), + Text: "Removed", + Status: progress.Done, + Done: true, + }) + return nil + }) } - return nil + return eg.Wait() } func (s *local) Logs(ctx context.Context, projectName string, w io.Writer) error { @@ -250,7 +281,7 @@ func (s *local) Convert(ctx context.Context, project *types.Project, format stri } } -func getContainerCreateOptions(p *types.Project, s types.ServiceConfig, inherit *moby.Container) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) { +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 @@ -259,6 +290,7 @@ func getContainerCreateOptions(p *types.Project, s types.ServiceConfig, inherit "com.docker.compose.project": p.Name, "com.docker.compose.service": s.Name, "com.docker.compose.config-hash": hash, + "com.docker.compose.container-number": strconv.Itoa(number), } var ( @@ -523,7 +555,7 @@ func (s *local) ensureNetwork(ctx context.Context, n types.NetworkConfig) error } w.Event(progress.Event{ ID: fmt.Sprintf("Network %q", n.Name), - Status: progress.Working, + Status: progress.Done, StatusText: "Created", Done: true, }) diff --git a/local/convergence.go b/local/convergence.go index b7c3494a6..55ca3049f 100644 --- a/local/convergence.go +++ b/local/convergence.go @@ -21,14 +21,14 @@ package local import ( "context" "fmt" - "github.com/docker/docker/api/types/network" - + "github.com/compose-spec/compose-go/types" "github.com/docker/compose-cli/api/containers" "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/filters" + "github.com/docker/docker/api/types/network" + "golang.org/x/sync/errgroup" + "strconv" ) func (s *local) ensureService(ctx context.Context, project *types.Project, service types.ServiceConfig) error { @@ -42,30 +42,90 @@ func (s *local) ensureService(ctx context.Context, project *types.Project, servi return err } + scale := getScale(service) + + eg, ctx := errgroup.WithContext(ctx) + if len(actual) < scale { + next, err := nextContainerNumber(actual) + if err != nil { + return err + } + missing := scale - len(actual) + for i := 0; i < missing; i++ { + number := next + i + name := fmt.Sprintf("%s_%s_%d", project.Name, service.Name, number) + eg.Go(func() error { + return s.createContainer(ctx, project, service, name, number) + }) + } + } + + if len(actual) > scale { + for i := scale; i < len(actual); i++ { + container := actual[i] + eg.Go(func() error { + err := s.containerService.Stop(ctx, container.ID, nil) + if err != nil { + return err + } + return s.containerService.Delete(ctx, container.ID, containers.DeleteRequest{}) + }) + } + actual = actual[:scale] + } + expected, err := jsonHash(service) if err != nil { return err } + for _, container := range actual { + container := container + diverged := container.Labels["com.docker.compose.config-hash"] != expected + if diverged { + eg.Go(func() error { + return s.recreateContainer(ctx, project, service, container) + }) + continue + } - if len(actual) == 0 { - return s.createContainer(ctx, project, service) + if container.State == "running" { + // already running, skip + continue + } + + eg.Go(func() error { + return s.restartContainer(ctx, service, container) + }) } - - container := actual[0] // TODO handle services with replicas - diverged := container.Labels["com.docker.compose.config-hash"] != expected - if diverged { - return s.recreateContainer(ctx, project, service, container) - } - - if container.State == "running" { - // already running, skip - return nil - } - - return s.restartContainer(ctx, service, container) + return eg.Wait() } -func (s *local) createContainer(ctx context.Context, project *types.Project, service types.ServiceConfig) error { +func nextContainerNumber(containers []moby.Container) (int, error) { + max := 0 + for _, c := range containers { + n, err := strconv.Atoi(c.Labels["com.docker.compose.container-number"]) + if err != nil { + return 0, err + } + if n > max { + max = n + } + } + return max + 1, nil + +} + +func getScale(config types.ServiceConfig) int { + if config.Deploy != nil && config.Deploy.Replicas != nil { + return int(*config.Deploy.Replicas) + } + if config.Scale != 0 { + return config.Scale + } + return 1 +} + +func (s *local) createContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, name string, number int) error { w := progress.ContextWriter(ctx) w.Event(progress.Event{ ID: fmt.Sprintf("Service %q", service.Name), @@ -73,8 +133,7 @@ func (s *local) createContainer(ctx context.Context, project *types.Project, ser StatusText: "Create", Done: false, }) - name := fmt.Sprintf("%s_%s", project.Name, service.Name) - err := s.runContainer(ctx, project, service, name, nil) + err := s.runContainer(ctx, project, service, name, number, nil) if err != nil { return err } @@ -105,7 +164,11 @@ func (s *local) recreateContainer(ctx context.Context, project *types.Project, s if err != nil { return err } - err = s.runContainer(ctx, project, service, name, &container) + number, err := strconv.Atoi(container.Labels["com.docker.compose.container-number"]) + if err != nil { + return err + } + err = s.runContainer(ctx, project, service, name, number, &container) if err != nil { return err } @@ -143,8 +206,8 @@ func (s *local) restartContainer(ctx context.Context, service types.ServiceConfi return nil } -func (s *local) runContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, name string, container *moby.Container) error { - containerConfig, hostConfig, networkingConfig, err := getContainerCreateOptions(project, service, container) +func (s *local) runContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, name string, number int, container *moby.Container) error { + containerConfig, hostConfig, networkingConfig, err := getContainerCreateOptions(project, service, number, container) if err != nil { return err }