From 0f5b5ccbd0377ea011249780f1efd3f29e1f56c2 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Fri, 3 Feb 2023 14:36:39 +0100 Subject: [PATCH] detect replacement container is created and inform printer so it attach and don't stop Signed-off-by: Nicolas De Loof --- cmd/compose/up.go | 2 +- pkg/api/api.go | 3 ++ pkg/api/labels.go | 2 ++ pkg/compose/attach.go | 3 ++ pkg/compose/containers.go | 11 ------- pkg/compose/convergence.go | 31 +++++++++++-------- pkg/compose/logs.go | 10 ++++++ pkg/compose/printer.go | 13 +++++--- pkg/compose/start.go | 63 ++++++++++++++++++++++++++++++-------- pkg/utils/slices.go | 2 +- 10 files changed, 97 insertions(+), 43 deletions(-) diff --git a/cmd/compose/up.go b/cmd/compose/up.go index 29529a0c2..70575399f 100644 --- a/cmd/compose/up.go +++ b/cmd/compose/up.go @@ -171,7 +171,7 @@ func runUp(ctx context.Context, streams api.Streams, backend api.Service, create if len(attachTo) == 0 { attachTo = project.ServiceNames() } - attachTo = utils.RemoveAll(attachTo, upOptions.noAttach) + attachTo = utils.Remove(attachTo, upOptions.noAttach...) create := api.CreateOptions{ Services: services, diff --git a/pkg/api/api.go b/pkg/api/api.go index fee66de11..7fedda48b 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -468,6 +468,7 @@ type ContainerEvent struct { // This is only suitable for display purposes within Compose, as it's // not guaranteed to be unique across services. Container string + ID string Service string Line string // ContainerEventExit only @@ -484,6 +485,8 @@ const ( ContainerEventAttach // ContainerEventStopped is a ContainerEvent of type stopped. ContainerEventStopped + // ContainerEventRecreated let consumer know container stopped but his being replaced + ContainerEventRecreated // ContainerEventExit is a ContainerEvent of type exit. ExitCode is set ContainerEventExit // UserCancel user cancelled compose up, we are stopping containers diff --git a/pkg/api/labels.go b/pkg/api/labels.go index 4352d9c03..ba110c831 100644 --- a/pkg/api/labels.go +++ b/pkg/api/labels.go @@ -55,6 +55,8 @@ const ( VersionLabel = "com.docker.compose.version" // ImageBuilderLabel stores the builder (classic or BuildKit) used to produce the image. ImageBuilderLabel = "com.docker.compose.image.builder" + // ContainerReplaceLabel is set when container is created to replace another container (recreated) + ContainerReplaceLabel = "com.docker.compose.replace" ) // ComposeVersion is the compose tool version as declared by label VersionLabel diff --git a/pkg/compose/attach.go b/pkg/compose/attach.go index 4f4e766c7..d019068dc 100644 --- a/pkg/compose/attach.go +++ b/pkg/compose/attach.go @@ -66,6 +66,7 @@ func (s *composeService) attachContainer(ctx context.Context, container moby.Con listener(api.ContainerEvent{ Type: api.ContainerEventAttach, Container: containerName, + ID: container.ID, Service: serviceName, }) @@ -73,6 +74,7 @@ func (s *composeService) attachContainer(ctx context.Context, container moby.Con listener(api.ContainerEvent{ Type: api.ContainerEventLog, Container: containerName, + ID: container.ID, Service: serviceName, Line: line, }) @@ -81,6 +83,7 @@ func (s *composeService) attachContainer(ctx context.Context, container moby.Con listener(api.ContainerEvent{ Type: api.ContainerEventErr, Container: containerName, + ID: container.ID, Service: serviceName, Line: line, }) diff --git a/pkg/compose/containers.go b/pkg/compose/containers.go index af05915db..0b7a3df97 100644 --- a/pkg/compose/containers.go +++ b/pkg/compose/containers.go @@ -148,14 +148,3 @@ func (containers Containers) sorted() Containers { }) return containers } - -func (containers Containers) remove(id string) Containers { - for i, c := range containers { - if c.ID == id { - l := len(containers) - 1 - containers[i] = containers[l] - return containers[:l] - } - } - return containers -} diff --git a/pkg/compose/convergence.go b/pkg/compose/convergence.go index 621c982ce..ea27e9bcd 100644 --- a/pkg/compose/convergence.go +++ b/pkg/compose/convergence.go @@ -416,17 +416,7 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P var created moby.Container w := progress.ContextWriter(ctx) w.Event(progress.NewEvent(getContainerProgressName(replaced), progress.Working, "Recreate")) - timeoutInSecond := utils.DurationSecondToInt(timeout) - err := s.apiClient().ContainerStop(ctx, replaced.ID, containerType.StopOptions{Timeout: timeoutInSecond}) - if err != nil { - return created, err - } - name := getCanonicalContainerName(replaced) - tmpName := fmt.Sprintf("%s_%s", replaced.ID[:12], name) - err = s.apiClient().ContainerRename(ctx, replaced.ID, tmpName) - if err != nil { - return created, err - } + number, err := strconv.Atoi(replaced.Labels[api.ContainerNumberLabel]) if err != nil { return created, err @@ -436,15 +426,30 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P if inherit { inherited = &replaced } - name = getContainerName(project.Name, service, number) - created, err = s.createMobyContainer(ctx, project, service, name, number, inherited, false, true, false, w) + name := getContainerName(project.Name, service, number) + tmpName := fmt.Sprintf("%s_%s", replaced.ID[:12], name) + service.Labels[api.ContainerReplaceLabel] = replaced.ID + created, err = s.createMobyContainer(ctx, project, service, tmpName, number, inherited, false, true, false, w) if err != nil { return created, err } + + timeoutInSecond := utils.DurationSecondToInt(timeout) + err = s.apiClient().ContainerStop(ctx, replaced.ID, containerType.StopOptions{Timeout: timeoutInSecond}) + if err != nil { + return created, err + } + err = s.apiClient().ContainerRemove(ctx, replaced.ID, moby.ContainerRemoveOptions{}) if err != nil { return created, err } + + err = s.apiClient().ContainerRename(ctx, created.ID, name) + if err != nil { + return created, err + } + w.Event(progress.NewEvent(getContainerProgressName(replaced), progress.Done, "Recreated")) setDependentLifecycle(project, service.Name, forceRecreate) return created, err diff --git a/pkg/compose/logs.go b/pkg/compose/logs.go index 18d0e2114..c9a5ff966 100644 --- a/pkg/compose/logs.go +++ b/pkg/compose/logs.go @@ -83,6 +83,7 @@ func (s *composeService) Logs( printer.HandleEvent(api.ContainerEvent{ Type: api.ContainerEventAttach, Container: getContainerNameWithoutProject(c), + ID: c.ID, Service: c.Labels[api.ServiceLabel], }) } @@ -92,6 +93,7 @@ func (s *composeService) Logs( printer.HandleEvent(api.ContainerEvent{ Type: api.ContainerEventAttach, Container: getContainerNameWithoutProject(c), + ID: c.ID, Service: c.Labels[api.ServiceLabel], }) err := s.logContainers(ctx, consumer, c, api.LogOptions{ @@ -106,6 +108,14 @@ func (s *composeService) Logs( return nil } return err + }, func(c types.Container, t time.Time) error { + printer.HandleEvent(api.ContainerEvent{ + Type: api.ContainerEventAttach, + Container: "", // actual name will be set by start event + ID: c.ID, + Service: c.Labels[api.ServiceLabel], + }) + return nil }) printer.Stop() return err diff --git a/pkg/compose/printer.go b/pkg/compose/printer.go index 85c180528..5c8db7e80 100644 --- a/pkg/compose/printer.go +++ b/pkg/compose/printer.go @@ -74,22 +74,25 @@ func (p *printer) Run(cascadeStop bool, exitCodeFrom string, stopFn func() error case <-p.stopCh: return exitCode, nil case event := <-p.queue: - container := event.Container + container, id := event.Container, event.ID switch event.Type { case api.UserCancel: aborting = true case api.ContainerEventAttach: - if _, ok := containers[container]; ok { + if _, ok := containers[id]; ok { continue } - containers[container] = struct{}{} + containers[id] = struct{}{} p.consumer.Register(container) - case api.ContainerEventExit, api.ContainerEventStopped: + case api.ContainerEventExit, api.ContainerEventStopped, api.ContainerEventRecreated: if !event.Restarting { - delete(containers, container) + delete(containers, id) } if !aborting { p.consumer.Status(container, fmt.Sprintf("exited with code %d", event.ExitCode)) + if event.Type == api.ContainerEventRecreated { + p.consumer.Status(container, "has been recreated") + } } if cascadeStop { if !aborting { diff --git a/pkg/compose/start.go b/pkg/compose/start.go index e2c7bc263..09d02e42e 100644 --- a/pkg/compose/start.go +++ b/pkg/compose/start.go @@ -63,6 +63,14 @@ func (s *composeService) start(ctx context.Context, projectName string, options return s.watchContainers(context.Background(), project.Name, options.AttachTo, options.Services, listener, attached, func(container moby.Container, _ time.Time) error { return s.attachContainer(ctx, container, listener) + }, func(container moby.Container, _ time.Time) error { + listener(api.ContainerEvent{ + Type: api.ContainerEventAttach, + Container: "", // actual name will be set by start event + ID: container.ID, + Service: container.Labels[api.ServiceLabel], + }) + return nil }) }) } @@ -114,7 +122,7 @@ type containerWatchFn func(container moby.Container, t time.Time) error // watchContainers uses engine events to capture container start/die and notify ContainerEventListener func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo projectName string, services, required []string, - listener api.ContainerEventListener, containers Containers, onStart containerWatchFn) error { + listener api.ContainerEventListener, containers Containers, onStart, onRecreate containerWatchFn) error { if len(containers) == 0 { return nil } @@ -123,12 +131,13 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo } var ( - expected Containers + expected []string watched = map[string]int{} + replaced []string ) for _, c := range containers { if utils.Contains(required, c.Labels[api.ServiceLabel]) { - expected = append(expected, c) + expected = append(expected, c.ID) } watched[c.ID] = 0 } @@ -157,23 +166,38 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo service := container.Labels[api.ServiceLabel] switch event.Status { case "stop": - listener(api.ContainerEvent{ - Type: api.ContainerEventStopped, - Container: name, - Service: service, - }) + if _, ok := watched[container.ID]; ok { + eType := api.ContainerEventStopped + if utils.Contains(replaced, container.ID) { + utils.Remove(replaced, container.ID) + eType = api.ContainerEventRecreated + } + listener(api.ContainerEvent{ + Type: eType, + Container: name, + ID: container.ID, + Service: service, + }) + } delete(watched, container.ID) - expected = expected.remove(container.ID) + expected = utils.Remove(expected, container.ID) case "die": restarted := watched[container.ID] watched[container.ID] = restarted + 1 // Container terminated. willRestart := inspected.State.Restarting + eType := api.ContainerEventExit + if utils.Contains(replaced, container.ID) { + utils.Remove(replaced, container.ID) + eType = api.ContainerEventRecreated + } + listener(api.ContainerEvent{ - Type: api.ContainerEventExit, + Type: eType, Container: name, + ID: container.ID, Service: service, ExitCode: inspected.State.ExitCode, Restarting: willRestart, @@ -182,7 +206,7 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo if !willRestart { // we're done with this one delete(watched, container.ID) - expected = expected.remove(container.ID) + expected = utils.Remove(expected, container.ID) } case "start": count, ok := watched[container.ID] @@ -190,7 +214,7 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo if !ok { // A new container has just been added to service by scale watched[container.ID] = 0 - expected = append(expected, container) + expected = append(expected, container.ID) mustAttach = true } if mustAttach { @@ -200,6 +224,21 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo return err } } + case "create": + if id, ok := container.Labels[api.ContainerReplaceLabel]; ok { + replaced = append(replaced, id) + err = onRecreate(container, event.Timestamp) + if err != nil { + return err + } + if utils.StringContains(expected, id) { + expected = append(expected, inspected.ID) + } + watched[container.ID] = 1 + if utils.Contains(expected, id) { + expected = append(expected, container.ID) + } + } } if len(expected) == 0 { stop() diff --git a/pkg/utils/slices.go b/pkg/utils/slices.go index 8f05aa1b7..6e39c8d37 100644 --- a/pkg/utils/slices.go +++ b/pkg/utils/slices.go @@ -30,7 +30,7 @@ func Contains[T any](origin []T, element T) bool { } // RemoveAll removes all elements from origin slice -func RemoveAll[T any](origin []T, elements []T) []T { +func Remove[T any](origin []T, elements ...T) []T { var filtered []T for _, v := range origin { if !Contains(elements, v) {