Skip to content

Commit

Permalink
detect replacement container is createed and inform printer so it att…
Browse files Browse the repository at this point in the history
…ach and don't stop

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
  • Loading branch information
ndeloof committed Feb 3, 2023
1 parent 1640f15 commit bc46f17
Show file tree
Hide file tree
Showing 9 changed files with 91 additions and 31 deletions.
2 changes: 1 addition & 1 deletion cmd/compose/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions pkg/api/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions pkg/compose/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,15 @@ func (s *composeService) attachContainer(ctx context.Context, container moby.Con
listener(api.ContainerEvent{
Type: api.ContainerEventAttach,
Container: containerName,
ID: container.ID,
Service: serviceName,
})

wOut := utils.GetWriter(func(line string) {
listener(api.ContainerEvent{
Type: api.ContainerEventLog,
Container: containerName,
ID: container.ID,
Service: serviceName,
Line: line,
})
Expand All @@ -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,
})
Expand Down
29 changes: 17 additions & 12 deletions pkg/compose/convergence.go
Original file line number Diff line number Diff line change
Expand Up @@ -416,35 +416,40 @@ 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})

number, err := strconv.Atoi(replaced.Labels[api.ContainerNumberLabel])
if err != nil {
return created, err
}
name := getCanonicalContainerName(replaced)

var inherited *moby.Container
if inherit {
inherited = &replaced
}
name := getContainerName(project.Name, service, number)
tmpName := fmt.Sprintf("%s_%s", replaced.ID[:12], name)
err = s.apiClient().ContainerRename(ctx, replaced.ID, tmpName)
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
}
number, err := strconv.Atoi(replaced.Labels[api.ContainerNumberLabel])

timeoutInSecond := utils.DurationSecondToInt(timeout)
err = s.apiClient().ContainerStop(ctx, replaced.ID, containerType.StopOptions{Timeout: timeoutInSecond})
if err != nil {
return created, err
}

var inherited *moby.Container
if inherit {
inherited = &replaced
}
name = getContainerName(project.Name, service, number)
created, err = s.createMobyContainer(ctx, project, service, name, number, inherited, false, true, false, w)
err = s.apiClient().ContainerRemove(ctx, replaced.ID, moby.ContainerRemoveOptions{})
if err != nil {
return created, err
}
err = s.apiClient().ContainerRemove(ctx, replaced.ID, moby.ContainerRemoveOptions{})

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
Expand Down
10 changes: 10 additions & 0 deletions pkg/compose/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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],
})
}
Expand All @@ -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{
Expand All @@ -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
Expand Down
13 changes: 8 additions & 5 deletions pkg/compose/printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
58 changes: 46 additions & 12 deletions pkg/compose/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
})
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down Expand Up @@ -157,23 +166,36 @@ 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) {
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) {
eType = api.ContainerEventRecreated
}

listener(api.ContainerEvent{
Type: api.ContainerEventExit,
Type: eType,
Container: name,
ID: container.ID,
Service: service,
ExitCode: inspected.State.ExitCode,
Restarting: willRestart,
Expand All @@ -182,15 +204,15 @@ 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]
mustAttach := ok && count > 0 // Container restarted, need to re-attach
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 {
Expand All @@ -200,6 +222,18 @@ 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)
onRecreate(container, event.Timestamp)
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()
Expand Down
2 changes: 1 addition & 1 deletion pkg/utils/slices.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit bc46f17

Please sign in to comment.