detect replacement container is created and inform printer so it attach and don't stop

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2023-02-03 14:36:39 +01:00 committed by Nicolas De loof
parent b3ec110612
commit 0f5b5ccbd0
10 changed files with 97 additions and 43 deletions

View File

@ -171,7 +171,7 @@ func runUp(ctx context.Context, streams api.Streams, backend api.Service, create
if len(attachTo) == 0 { if len(attachTo) == 0 {
attachTo = project.ServiceNames() attachTo = project.ServiceNames()
} }
attachTo = utils.RemoveAll(attachTo, upOptions.noAttach) attachTo = utils.Remove(attachTo, upOptions.noAttach...)
create := api.CreateOptions{ create := api.CreateOptions{
Services: services, Services: services,

View File

@ -468,6 +468,7 @@ type ContainerEvent struct {
// This is only suitable for display purposes within Compose, as it's // This is only suitable for display purposes within Compose, as it's
// not guaranteed to be unique across services. // not guaranteed to be unique across services.
Container string Container string
ID string
Service string Service string
Line string Line string
// ContainerEventExit only // ContainerEventExit only
@ -484,6 +485,8 @@ const (
ContainerEventAttach ContainerEventAttach
// ContainerEventStopped is a ContainerEvent of type stopped. // ContainerEventStopped is a ContainerEvent of type stopped.
ContainerEventStopped ContainerEventStopped
// ContainerEventRecreated let consumer know container stopped but his being replaced
ContainerEventRecreated
// ContainerEventExit is a ContainerEvent of type exit. ExitCode is set // ContainerEventExit is a ContainerEvent of type exit. ExitCode is set
ContainerEventExit ContainerEventExit
// UserCancel user cancelled compose up, we are stopping containers // UserCancel user cancelled compose up, we are stopping containers

View File

@ -55,6 +55,8 @@ const (
VersionLabel = "com.docker.compose.version" VersionLabel = "com.docker.compose.version"
// ImageBuilderLabel stores the builder (classic or BuildKit) used to produce the image. // ImageBuilderLabel stores the builder (classic or BuildKit) used to produce the image.
ImageBuilderLabel = "com.docker.compose.image.builder" 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 // ComposeVersion is the compose tool version as declared by label VersionLabel

View File

@ -66,6 +66,7 @@ func (s *composeService) attachContainer(ctx context.Context, container moby.Con
listener(api.ContainerEvent{ listener(api.ContainerEvent{
Type: api.ContainerEventAttach, Type: api.ContainerEventAttach,
Container: containerName, Container: containerName,
ID: container.ID,
Service: serviceName, Service: serviceName,
}) })
@ -73,6 +74,7 @@ func (s *composeService) attachContainer(ctx context.Context, container moby.Con
listener(api.ContainerEvent{ listener(api.ContainerEvent{
Type: api.ContainerEventLog, Type: api.ContainerEventLog,
Container: containerName, Container: containerName,
ID: container.ID,
Service: serviceName, Service: serviceName,
Line: line, Line: line,
}) })
@ -81,6 +83,7 @@ func (s *composeService) attachContainer(ctx context.Context, container moby.Con
listener(api.ContainerEvent{ listener(api.ContainerEvent{
Type: api.ContainerEventErr, Type: api.ContainerEventErr,
Container: containerName, Container: containerName,
ID: container.ID,
Service: serviceName, Service: serviceName,
Line: line, Line: line,
}) })

View File

@ -148,14 +148,3 @@ func (containers Containers) sorted() Containers {
}) })
return 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
}

View File

@ -416,17 +416,7 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P
var created moby.Container var created moby.Container
w := progress.ContextWriter(ctx) w := progress.ContextWriter(ctx)
w.Event(progress.NewEvent(getContainerProgressName(replaced), progress.Working, "Recreate")) 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]) number, err := strconv.Atoi(replaced.Labels[api.ContainerNumberLabel])
if err != nil { if err != nil {
return created, err return created, err
@ -436,15 +426,30 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P
if inherit { if inherit {
inherited = &replaced inherited = &replaced
} }
name = getContainerName(project.Name, service, number) name := getContainerName(project.Name, service, number)
created, err = s.createMobyContainer(ctx, project, service, name, number, inherited, false, true, false, w) 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 { if err != nil {
return created, err 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{}) err = s.apiClient().ContainerRemove(ctx, replaced.ID, moby.ContainerRemoveOptions{})
if err != nil { if err != nil {
return created, err 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")) w.Event(progress.NewEvent(getContainerProgressName(replaced), progress.Done, "Recreated"))
setDependentLifecycle(project, service.Name, forceRecreate) setDependentLifecycle(project, service.Name, forceRecreate)
return created, err return created, err

View File

@ -83,6 +83,7 @@ func (s *composeService) Logs(
printer.HandleEvent(api.ContainerEvent{ printer.HandleEvent(api.ContainerEvent{
Type: api.ContainerEventAttach, Type: api.ContainerEventAttach,
Container: getContainerNameWithoutProject(c), Container: getContainerNameWithoutProject(c),
ID: c.ID,
Service: c.Labels[api.ServiceLabel], Service: c.Labels[api.ServiceLabel],
}) })
} }
@ -92,6 +93,7 @@ func (s *composeService) Logs(
printer.HandleEvent(api.ContainerEvent{ printer.HandleEvent(api.ContainerEvent{
Type: api.ContainerEventAttach, Type: api.ContainerEventAttach,
Container: getContainerNameWithoutProject(c), Container: getContainerNameWithoutProject(c),
ID: c.ID,
Service: c.Labels[api.ServiceLabel], Service: c.Labels[api.ServiceLabel],
}) })
err := s.logContainers(ctx, consumer, c, api.LogOptions{ err := s.logContainers(ctx, consumer, c, api.LogOptions{
@ -106,6 +108,14 @@ func (s *composeService) Logs(
return nil return nil
} }
return err 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() printer.Stop()
return err return err

View File

@ -74,22 +74,25 @@ func (p *printer) Run(cascadeStop bool, exitCodeFrom string, stopFn func() error
case <-p.stopCh: case <-p.stopCh:
return exitCode, nil return exitCode, nil
case event := <-p.queue: case event := <-p.queue:
container := event.Container container, id := event.Container, event.ID
switch event.Type { switch event.Type {
case api.UserCancel: case api.UserCancel:
aborting = true aborting = true
case api.ContainerEventAttach: case api.ContainerEventAttach:
if _, ok := containers[container]; ok { if _, ok := containers[id]; ok {
continue continue
} }
containers[container] = struct{}{} containers[id] = struct{}{}
p.consumer.Register(container) p.consumer.Register(container)
case api.ContainerEventExit, api.ContainerEventStopped: case api.ContainerEventExit, api.ContainerEventStopped, api.ContainerEventRecreated:
if !event.Restarting { if !event.Restarting {
delete(containers, container) delete(containers, id)
} }
if !aborting { if !aborting {
p.consumer.Status(container, fmt.Sprintf("exited with code %d", event.ExitCode)) 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 cascadeStop {
if !aborting { if !aborting {

View File

@ -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, return s.watchContainers(context.Background(), project.Name, options.AttachTo, options.Services, listener, attached,
func(container moby.Container, _ time.Time) error { func(container moby.Container, _ time.Time) error {
return s.attachContainer(ctx, container, listener) 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 // watchContainers uses engine events to capture container start/die and notify ContainerEventListener
func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
projectName string, services, required []string, 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 { if len(containers) == 0 {
return nil return nil
} }
@ -123,12 +131,13 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
} }
var ( var (
expected Containers expected []string
watched = map[string]int{} watched = map[string]int{}
replaced []string
) )
for _, c := range containers { for _, c := range containers {
if utils.Contains(required, c.Labels[api.ServiceLabel]) { if utils.Contains(required, c.Labels[api.ServiceLabel]) {
expected = append(expected, c) expected = append(expected, c.ID)
} }
watched[c.ID] = 0 watched[c.ID] = 0
} }
@ -157,23 +166,38 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
service := container.Labels[api.ServiceLabel] service := container.Labels[api.ServiceLabel]
switch event.Status { switch event.Status {
case "stop": case "stop":
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{ listener(api.ContainerEvent{
Type: api.ContainerEventStopped, Type: eType,
Container: name, Container: name,
ID: container.ID,
Service: service, Service: service,
}) })
}
delete(watched, container.ID) delete(watched, container.ID)
expected = expected.remove(container.ID) expected = utils.Remove(expected, container.ID)
case "die": case "die":
restarted := watched[container.ID] restarted := watched[container.ID]
watched[container.ID] = restarted + 1 watched[container.ID] = restarted + 1
// Container terminated. // Container terminated.
willRestart := inspected.State.Restarting willRestart := inspected.State.Restarting
eType := api.ContainerEventExit
if utils.Contains(replaced, container.ID) {
utils.Remove(replaced, container.ID)
eType = api.ContainerEventRecreated
}
listener(api.ContainerEvent{ listener(api.ContainerEvent{
Type: api.ContainerEventExit, Type: eType,
Container: name, Container: name,
ID: container.ID,
Service: service, Service: service,
ExitCode: inspected.State.ExitCode, ExitCode: inspected.State.ExitCode,
Restarting: willRestart, Restarting: willRestart,
@ -182,7 +206,7 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
if !willRestart { if !willRestart {
// we're done with this one // we're done with this one
delete(watched, container.ID) delete(watched, container.ID)
expected = expected.remove(container.ID) expected = utils.Remove(expected, container.ID)
} }
case "start": case "start":
count, ok := watched[container.ID] count, ok := watched[container.ID]
@ -190,7 +214,7 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
if !ok { if !ok {
// A new container has just been added to service by scale // A new container has just been added to service by scale
watched[container.ID] = 0 watched[container.ID] = 0
expected = append(expected, container) expected = append(expected, container.ID)
mustAttach = true mustAttach = true
} }
if mustAttach { if mustAttach {
@ -200,6 +224,21 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
return err 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 { if len(expected) == 0 {
stop() stop()

View File

@ -30,7 +30,7 @@ func Contains[T any](origin []T, element T) bool {
} }
// RemoveAll removes all elements from origin slice // 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 var filtered []T
for _, v := range origin { for _, v := range origin {
if !Contains(elements, v) { if !Contains(elements, v) {