diff --git a/pkg/e2e/scenario.go b/pkg/e2e/scenario.go index 68b5c676c..4aa3a0b52 100644 --- a/pkg/e2e/scenario.go +++ b/pkg/e2e/scenario.go @@ -324,47 +324,27 @@ func (s *Scenario) command(action Action) icmd.Cmd { } // snapshot captures the current state of the project's containers. -// Containers are inspected one by one: helper containers (e.g. the ones -// running lifecycle hooks) carry the project label but can vanish between the -// listing and the inspect, and one gone container must not blank the whole +// The containers are inspected in one batch; if that fails — helper +// containers (e.g. the ones running lifecycle hooks) carry the project label +// but can vanish between the listing and the inspect — each container is +// retried individually so one gone container does not blank the whole // snapshot. Helpers are also excluded from the snapshot itself: a container // belongs to a service if it is numbered as a replica or marked one-off. func (s *Scenario) snapshot() snapshot { s.t.Helper() snap := snapshot{} - for _, id := range s.projectContainerIDs() { - res := icmd.RunCmd(s.cli.NewDockerCmd(s.t, "inspect", id)) - if res.ExitCode != 0 { - continue - } - var containers []struct { - ID string `json:"Id"` - Name string `json:"Name"` - State struct { - Status string `json:"Status"` - StartedAt string `json:"StartedAt"` - } `json:"State"` - Config struct { - Labels map[string]string `json:"Labels"` - } `json:"Config"` - } - if err := json.Unmarshal([]byte(res.Stdout()), &containers); err != nil { - continue - } - for _, c := range containers { - oneOff := c.Config.Labels["com.docker.compose.oneoff"] == "True" - if !oneOff && c.Config.Labels["com.docker.compose.container-number"] == "" { - continue + ids := s.projectContainerIDs() + if len(ids) == 0 { + return snap + } + res := icmd.RunCmd(s.cli.NewDockerCmd(s.t, append([]string{"inspect"}, ids...)...)) + if res.ExitCode == 0 { + s.collect(snap, res.Stdout()) + } else { + for _, id := range ids { + if res := icmd.RunCmd(s.cli.NewDockerCmd(s.t, "inspect", id)); res.ExitCode == 0 { + s.collect(snap, res.Stdout()) } - service := c.Config.Labels["com.docker.compose.service"] - snap[service] = append(snap[service], containerState{ - ID: c.ID, - Name: strings.TrimPrefix(c.Name, "/"), - State: c.State.Status, - StartedAt: c.State.StartedAt, - OneOff: oneOff, - Labels: c.Config.Labels, - }) } } for service := range snap { @@ -373,6 +353,40 @@ func (s *Scenario) snapshot() snapshot { return snap } +// collect parses a docker inspect output into the snapshot, skipping helper +// containers (neither numbered as a replica nor one-off). +func (s *Scenario) collect(snap snapshot, inspectOutput string) { + var containers []struct { + ID string `json:"Id"` + Name string `json:"Name"` + State struct { + Status string `json:"Status"` + StartedAt string `json:"StartedAt"` + } `json:"State"` + Config struct { + Labels map[string]string `json:"Labels"` + } `json:"Config"` + } + if err := json.Unmarshal([]byte(inspectOutput), &containers); err != nil { + return + } + for _, c := range containers { + oneOff := c.Config.Labels["com.docker.compose.oneoff"] == "True" + if !oneOff && c.Config.Labels["com.docker.compose.container-number"] == "" { + continue + } + service := c.Config.Labels["com.docker.compose.service"] + snap[service] = append(snap[service], containerState{ + ID: c.ID, + Name: strings.TrimPrefix(c.Name, "/"), + State: c.State.Status, + StartedAt: c.State.StartedAt, + OneOff: oneOff, + Labels: c.Config.Labels, + }) + } +} + func (s *Scenario) projectContainerIDs() []string { res := icmd.RunCmd(s.cli.NewDockerCmd(s.t, "ps", "-a", "--no-trunc", "--filter", "label=com.docker.compose.project="+s.project, "--format", "{{.ID}}")) diff --git a/pkg/e2e/up_test.go b/pkg/e2e/up_test.go index 2ace27082..3983efb14 100644 --- a/pkg/e2e/up_test.go +++ b/pkg/e2e/up_test.go @@ -262,15 +262,19 @@ services: } func TestUpStopWithLogsMixed(t *testing.T) { + // service2 pings forever so the abort always interrupts it: with a bounded + // ping, on a fast machine it can exit on its own before the abort reaches + // it, and the pre_stop hook never runs. s := NewScenario(t, "on abort, logs of surviving services must keep flowing while others stop, hooks included") s.Compose(` services: service1: image: alpine - command: /bin/true + command: sh -c "sleep 2" service2: image: alpine - command: ping -c 2 localhost + init: true + command: ping localhost pre_stop: - command: echo "stop hook running..." `).