e2e: fix test resource isolation and make standalone parallelism configurable

Several e2e tests used fixed host ports (8070, 8080, 8090, etc.), global
buildx builders registered with --use, and a hardcoded IPC container name.
Under parallel execution these shared daemon-level resources caused port
conflicts, buildkitd container-name collisions, and intermittent failures.
Separately, the standalone make target hardcoded -parallel=1, forcing the
entire standalone suite to run serially even though the same tests run in
parallel in plugin mode.

Changes:
- Convert fixed host ports in four fixtures (network-test, sentences,
  build-test, volume-test) to ephemeral bindings; add ServicePublishedPort
  helper to framework.go to resolve the actual mapped port at runtime.
- Give each buildx-builder test a unique daemon-scoped name via BuilderName
  helper, preventing container-name collisions between parallel goroutines.
  Applies to TestBuildPlatformsWithCorrectBuildxConfig, TestBuildPrivileged,
  TestBuildBuilder, TestBuildEntitlements, TestBuildTLS.
- Rename the fixed ipc_mode_container to a project-scoped name
  (ipc_e2e-src) and update the ipc-test fixture accordingly.
- Replace the hardcoded -parallel=1 in make e2e-compose-standalone with
  E2E_PARALLEL_PLUGIN?=4 and E2E_STANDALONE_PARALLEL?=4, aligning both
  modes with the effective GOMAXPROCS on 4-core GitHub Actions runners.
  Both variables are overridable (E2E_STANDALONE_PARALLEL=1 restores
  serial local debugging).
- Raise the poll delay in watch_test.go long-running polls (90s/120s
  timeout) from the default 100ms to 1s; bump cascade_test.go and
  publish_test.go from 100ms to 500ms. Reduces CPU pressure without
  affecting effective test duration.

The logging-driver fixture retains its fixed port 24224: the Docker fluentd
log driver encodes fluentd-address at service-start time, making ephemeral
resolution non-trivial. TestLoggingDriver is serial so there is no
collision risk in practice.

Signed-off-by: Guillaume Lours <[email protected]>
This commit is contained in:
Guillaume Lours
2026-08-19 17:41:02 +02:00
committed by Nicolas De loof
parent f8ed846e4d
commit 8bd79da4bc
14 changed files with 92 additions and 31 deletions
+4 -2
View File
@@ -32,6 +32,8 @@ endif
BUILD_FLAGS?=
TEST_FLAGS?=
E2E_TEST?=
E2E_PARALLEL_PLUGIN?=4
E2E_STANDALONE_PARALLEL?=4
ifneq ($(E2E_TEST),)
TEST_FLAGS:=$(TEST_FLAGS) -run '$(E2E_TEST)'
endif
@@ -75,11 +77,11 @@ install: binary
.PHONY: e2e-compose
e2e-compose: example-provider ## Run end to end local tests in plugin mode. Set E2E_TEST=TestName to run a single test
go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- -v $(TEST_FLAGS) -count=1 -timeout 20m ./pkg/e2e
go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- -v $(TEST_FLAGS) -count=1 -parallel=$(E2E_PARALLEL_PLUGIN) -timeout 20m ./pkg/e2e
.PHONY: e2e-compose-standalone
e2e-compose-standalone: ## Run End to end local tests in standalone mode. Set E2E_TEST=TestName to run a single test
go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- $(TEST_FLAGS) -v -count=1 -parallel=1 -timeout 20m --tags=standalone ./pkg/e2e
go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- $(TEST_FLAGS) -v -count=1 -parallel=$(E2E_STANDALONE_PARALLEL) -timeout 20m --tags=standalone ./pkg/e2e
.PHONY: build-and-e2e-compose
build-and-e2e-compose: build e2e-compose ## Compile the compose cli-plugin and run end to end local tests in plugin mode. Set E2E_TEST=TestName to run a single test
+22 -13
View File
@@ -105,7 +105,7 @@ func TestLocalComposeBuild(t *testing.T) {
res.Assert(t, icmd.Expected{Out: "COPY static /usr/share/nginx/html"})
res.Assert(t, icmd.Expected{Out: "COPY static2 /usr/share/nginx/html"})
output := HTTPGetWithRetry(t, "http://localhost:8070", http.StatusOK, 2*time.Second, 20*time.Second)
output := HTTPGetWithRetry(t, fmt.Sprintf("http://localhost:%d", c.ServicePublishedPort(t, "build-test", "nginx", 80)), http.StatusOK, 2*time.Second, 20*time.Second)
assert.Assert(t, strings.Contains(output, "Hello from Nginx container"))
c.RunDockerCmd(t, "image", "inspect", "build-test-nginx")
@@ -294,13 +294,15 @@ func TestBuildPlatformsWithCorrectBuildxConfig(t *testing.T) {
}
c := NewParallelCLI(t)
// declare builder
result := c.RunDockerCmd(t, "buildx", "create", "--name", "build-platform", "--use", "--bootstrap")
// declare a per-test-unique builder to avoid container-name collisions
// when tests run in parallel on the same Docker daemon.
builderName := BuilderName(t, "build-platform")
result := c.RunDockerCmd(t, "buildx", "create", "--name", builderName, "--use", "--bootstrap")
assert.NilError(t, result.Error)
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test/platforms", "down")
_ = c.RunDockerCmd(t, "buildx", "rm", "-f", "build-platform")
_ = c.RunDockerCmd(t, "buildx", "rm", "-f", builderName)
})
t.Run("platform not supported by builder", func(t *testing.T) {
@@ -358,14 +360,16 @@ func TestBuildPlatformsWithCorrectBuildxConfig(t *testing.T) {
func TestBuildPrivileged(t *testing.T) {
c := NewParallelCLI(t)
// declare builder
result := c.RunDockerCmd(t, "buildx", "create", "--name", "build-privileged", "--use", "--bootstrap", "--buildkitd-flags",
// declare a per-test-unique builder to avoid container-name collisions
// when tests run in parallel on the same Docker daemon.
builderName := BuilderName(t, "build-privileged")
result := c.RunDockerCmd(t, "buildx", "create", "--name", builderName, "--use", "--bootstrap", "--buildkitd-flags",
`'--allow-insecure-entitlement=security.insecure'`)
assert.NilError(t, result.Error)
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test/privileged", "down")
_ = c.RunDockerCmd(t, "buildx", "rm", "-f", "build-privileged")
_ = c.RunDockerCmd(t, "buildx", "rm", "-f", builderName)
})
t.Run("use build privileged mode to run insecure build command", func(t *testing.T) {
@@ -473,8 +477,9 @@ func TestBuildPlatformsStandardErrors(t *testing.T) {
func TestBuildBuilder(t *testing.T) {
c := NewParallelCLI(t)
builderName := "build-with-builder"
// declare builder
// declare a per-test-unique builder to avoid container-name collisions
// when tests run in parallel on the same Docker daemon.
builderName := BuilderName(t, "build-with-builder")
result := c.RunDockerCmd(t, "buildx", "create", "--name", builderName, "--use", "--bootstrap")
assert.NilError(t, result.Error)
@@ -500,14 +505,16 @@ func TestBuildBuilder(t *testing.T) {
func TestBuildEntitlements(t *testing.T) {
c := NewParallelCLI(t)
// declare builder
result := c.RunDockerCmd(t, "buildx", "create", "--name", "build-insecure", "--use", "--bootstrap", "--buildkitd-flags",
// declare a per-test-unique builder to avoid container-name collisions
// when tests run in parallel on the same Docker daemon.
builderName := BuilderName(t, "build-insecure")
result := c.RunDockerCmd(t, "buildx", "create", "--name", builderName, "--use", "--bootstrap", "--buildkitd-flags",
`'--allow-insecure-entitlement=security.insecure'`)
assert.NilError(t, result.Error)
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test/entitlements", "down")
_ = c.RunDockerCmd(t, "buildx", "rm", "-f", "build-insecure")
_ = c.RunDockerCmd(t, "buildx", "rm", "-f", builderName)
})
t.Run("use build privileged mode to run insecure build command", func(t *testing.T) {
@@ -599,7 +606,9 @@ func TestBuildTLS(t *testing.T) {
t.Helper()
c := NewParallelCLI(t)
const dindBuilder = "e2e-dind-builder"
// Use a per-test-unique name to avoid container/context collisions when
// tests run in parallel on the same Docker daemon.
dindBuilder := BuilderName(t, "e2e-dind-builder")
tmp := t.TempDir()
t.Cleanup(func() {
+1 -1
View File
@@ -61,7 +61,7 @@ func TestCascadeIgnoresOneOffContainer(t *testing.T) {
})
poll.WaitOn(t, expectOutput(res, "Attaching to running-1"),
poll.WithDelay(100*time.Millisecond), poll.WithTimeout(30*time.Second))
poll.WithDelay(500*time.Millisecond), poll.WithTimeout(30*time.Second))
c.RunDockerComposeCmd(t, "-f", "./fixtures/cascade/compose.yaml",
"run", "--rm", "--no-deps", "running", "/bin/true")
+2 -1
View File
@@ -43,7 +43,8 @@ func TestLocalComposeUp(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-p", projectName, "ps")
res.Assert(t, icmd.Expected{Out: `web`})
endpoint := "http://localhost:90"
webPort := c.ServicePublishedPort(t, projectName, "web", 80)
endpoint := fmt.Sprintf("http://localhost:%d", webPort)
output := HTTPGetWithRetry(t, endpoint+"/words/noun", http.StatusOK, 2*time.Second, 20*time.Second)
assert.Assert(t, strings.Contains(output, `"word":`))
+1 -1
View File
@@ -2,7 +2,7 @@ services:
nginx:
build: nginx-build
ports:
- 8070:80
- 80
nginx2:
build: nginx-build2
+2 -2
View File
@@ -15,7 +15,7 @@ services:
image: gtardif/sentences-api
init: true
ports:
- "8080:8080"
- "8080"
networks:
- dbnet
- servicenet
@@ -23,7 +23,7 @@ services:
image: gtardif/sentences-web
init: true
ports:
- "80:80"
- "80"
labels:
- "my-label=test"
networks:
+2 -2
View File
@@ -6,12 +6,12 @@ services:
image: gtardif/sentences-api
init: true
ports:
- "95:8080"
- "8080"
web:
image: gtardif/sentences-web
init: true
ports:
- "90:80"
- "80"
labels:
- "my-label=test"
healthcheck:
+2 -2
View File
@@ -4,7 +4,7 @@ services:
volumes:
- ./static:/usr/share/nginx/html
ports:
- 8090:80
- 80
nginx2:
build: nginx-build
@@ -13,7 +13,7 @@ services:
- /usr/src/app/node_modules
- otherVol:/usr/share/nginx/test
ports:
- 9090:80
- 80
configs:
- myconfig
secrets:
+41
View File
@@ -22,10 +22,12 @@ import (
"fmt"
"io"
"io/fs"
"net"
"net/http"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"testing"
"time"
@@ -510,3 +512,42 @@ func (c *CLI) cleanupWithDown(t testing.TB, project string, args ...string) {
t.Helper()
c.RunDockerComposeCmd(t, append([]string{"-p", project, "down", "-v", "--remove-orphans"}, args...)...)
}
// ServicePublishedPort returns the ephemeral host port mapped to targetPort
// on the named service in the given compose project. It requires the service
// to be already running. The test fails immediately if the mapping cannot be
// resolved.
func (c *CLI) ServicePublishedPort(t testing.TB, project, service string, targetPort int) int {
t.Helper()
res := c.RunDockerComposeCmd(t, "-p", project, "port", service, strconv.Itoa(targetPort))
addr := strings.TrimSpace(res.Stdout())
_, portStr, err := net.SplitHostPort(addr)
if err != nil {
t.Fatalf("ServicePublishedPort: cannot parse compose port output %q: %v", addr, err)
}
port, err := strconv.Atoi(portStr)
if err != nil {
t.Fatalf("ServicePublishedPort: invalid port number %q: %v", portStr, err)
}
return port
}
// BuilderName returns a buildx builder name that is unique to the test,
// preventing container-name collisions when tests run in parallel on the
// same Docker daemon.
func BuilderName(t testing.TB, base string) string {
t.Helper()
safe := strings.Map(func(r rune) rune {
switch {
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '-':
return r
default:
return '-'
}
}, t.Name())
name := base + "-" + safe
if len(name) > 60 {
name = strings.TrimRight(name[:60], "-")
}
return name
}
+3 -2
View File
@@ -42,7 +42,8 @@ func TestNetworks(t *testing.T) {
res := c.RunDockerComposeCmd(t, "ps")
res.Assert(t, icmd.Expected{Out: `web`})
endpoint := "http://localhost:80"
webPort := c.ServicePublishedPort(t, projectName, "web", 80)
endpoint := fmt.Sprintf("http://localhost:%d", webPort)
output := HTTPGetWithRetry(t, endpoint+"/words/noun", http.StatusOK, 2*time.Second, 20*time.Second)
assert.Assert(t, strings.Contains(output, `"word":`))
@@ -51,7 +52,7 @@ func TestNetworks(t *testing.T) {
res.Assert(t, icmd.Expected{Out: "microservices"})
res = c.RunDockerComposeCmd(t, "port", "words", "8080")
res.Assert(t, icmd.Expected{Out: `0.0.0.0:8080`})
res.Assert(t, icmd.Expected{Out: `0.0.0.0:`})
c.RunDockerComposeCmd(t, "down", "-t0", "-v")
res = c.RunDockerCmd(t, "network", "ls")
+1 -1
View File
@@ -155,7 +155,7 @@ func TestPublish(t *testing.T) {
return poll.Success()
}
return poll.Continue("registry not ready, status %d", resp.StatusCode)
}, poll.WithTimeout(10*time.Second), poll.WithDelay(100*time.Millisecond))
}, poll.WithTimeout(10*time.Second), poll.WithDelay(500*time.Millisecond))
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/publish/oci/compose.yaml", "-f", "./fixtures/publish/oci/compose-override.yaml",
"-p", projectName, "publish", "--with-env", "--yes", "--insecure-registry", registry+"/test:test")
+5
View File
@@ -2,6 +2,11 @@ services:
fluentbit:
image: fluent/fluent-bit:3.1.7-debug
ports:
# Port 24224 intentionally fixed: the Docker fluentd log driver sets
# fluentd-address at service-start time (see compose.yaml and
# TestLoggingDriver in compose_up_test.go). Dynamic resolution would
# require a two-phase compose up which is unnecessarily invasive.
# TestLoggingDriver is serial (no t.Parallel()), so collision risk is nil.
- "24224:24224"
- "24224:24224/udp"
environment:
+3 -1
View File
@@ -17,6 +17,7 @@
package e2e
import (
"fmt"
"net/http"
"path/filepath"
"runtime"
@@ -43,7 +44,8 @@ func TestLocalComposeVolume(t *testing.T) {
})
t.Run("access bind mount data", func(t *testing.T) {
output := HTTPGetWithRetry(t, "http://localhost:8090", http.StatusOK, 2*time.Second, 20*time.Second)
port := c.ServicePublishedPort(t, projectName, "nginx", 80)
output := HTTPGetWithRetry(t, fmt.Sprintf("http://localhost:%d", port), http.StatusOK, 2*time.Second, 20*time.Second)
assert.Assert(t, strings.Contains(output, "Hello from Nginx container"))
})
+3 -3
View File
@@ -345,7 +345,7 @@ func TestWatchMultiServices(t *testing.T) {
return poll.Success()
}
return poll.Continue("%v", watch.Stdout())
}, poll.WithTimeout(90*time.Second))
}, poll.WithTimeout(90*time.Second), poll.WithDelay(time.Second))
waitRebuild := func(service string, expected string) {
poll.WaitOn(t, func(l poll.LogT) poll.Result {
@@ -354,7 +354,7 @@ func TestWatchMultiServices(t *testing.T) {
return poll.Success()
}
return poll.Continue("%v", cat.Combined())
}, poll.WithTimeout(90*time.Second))
}, poll.WithTimeout(90*time.Second), poll.WithDelay(time.Second))
}
waitRebuild("a", "test")
waitRebuild("b", "test")
@@ -412,7 +412,7 @@ func TestWatchRebuildIgnoresDependencies(t *testing.T) {
return poll.Success()
}
return poll.Continue("waiting for watch to start: %v", buffer.String())
}, poll.WithTimeout(120*time.Second))
}, poll.WithTimeout(120*time.Second), poll.WithDelay(time.Second))
// Record the cutoff point in the log buffer so we only inspect output
// produced AFTER the file change triggers the rebuild.