Skip to content

fix(e2e): give every kind cluster its own kubeconfig - #2301

Merged
csatib02 merged 14 commits into
kube-logging:masterfrom
vyncint:fix-e2e-cluster-concurrency
Aug 3, 2026
Merged

fix(e2e): give every kind cluster its own kubeconfig#2301
csatib02 merged 14 commits into
kube-logging:masterfrom
vyncint:fix-e2e-cluster-concurrency

Conversation

@vyncint

@vyncint vyncint commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

kind delete exits 1 under concurrency. kind takes a non-blocking lock on the kubeconfig it updates, so clusters sharing the default one lose the race and fail after the test body has passed. 96 concurrent deletes of clusters that do not exist: 7 fail with failed to lock config file on a shared kubeconfig, 0 when each names its own. Giving every cluster its own --kubeconfig also stops the suite rewriting the developer's ~/.kube/config. The delete path discarded kind's stderr, which is why this only ever surfaced as exit status 1; it now reports what kind said.

Rebased on #2302

The Makefile change came out in the rebase, since #2302 caps concurrency with -parallel ${E2E_SUITE_PARALLEL}, and is back on your note. The two are not the same bound: -parallel limits tests inside a binary, -p ${E2E_CLUSTERS} limits the binaries, and -p still defaults to GOMAXPROCS. Measured on a harness with this suite's package and test shape, peak clusters is 5 at four cores, 10 at eight and 15 at sixteen; pinning -p holds it at 5 everywhere. Both knobs are in CLAUDE.md now, beside the ones #2302 documented.

One correction to what I wrote above earlier: the peak is not E2E_CLUSTERS times E2E_SUITE_PARALLEL. That product is 8, but ten of the thirteen suites hold a single test, so the measured ceiling is 5.

Two conflicts resolved by hand, both in KindClusterKubeconfig:

Also from review

  • The partial-cluster delete lost the kubeconfig. The delete that clears a half-built cluster after a create times out did not pass options.Kubeconfig, so it fell back to the shared default — and it runs exactly when several creates are timing out together.
  • Nothing removed the per-cluster kubeconfig, and its path was guessable. kindCluster.Cleanup only removes the os.CreateTemp copy, and kind rewrites the file it manages rather than unlinking it, so a killed run handed its leftovers and any stale .lock to the next one. The files now live in one os.MkdirTemp directory per run at 0700, and DeleteTestCluster removes the kubeconfig and lock once the cluster is gone.
  • RequireNoError off the test goroutine. WithCluster called it inside the cluster.Start goroutine, where t.FailNow is undefined, so a cluster that never started was dropped and resurfaced as an unrelated Eventually timeout. Start's result now comes back over a channel and is asserted in the deferred block.
  • RequireNoError on the teardown delete. A cluster that came down cleanly still failed its test when kind's kubeconfig bookkeeping errored. It matches the assert.NoError calls above it now.
  • The test sharding was dead and inverted. sequence is a per-binary counter and ten of the thirteen suites hold exactly one test, so with SHARDS=2 shard 0 would run nothing and shard 1 everything. Gone, along with the commented-out matrix and its env vars; Initialize is the bare t.Parallel.
  • helpers.go hardcoded the tenant watch namespace. LoggingTenant honours nsTenant everywhere else. Latent, since both callers pass "tenant", but it left the live path disagreeing with the internal/fixture copy.
  • The kind stub tests are gone, as requested — the shell stub raced and the flake landed on unrelated PRs. The timeout arithmetic keeps its five pure-Go tests. TestWatchNamespaces recomputes its expectation on every poll instead of capturing it once.

Verification

make check passes, golangci-lint run --max-same-issues=0 --max-issues-per-linter=0 in e2e/ reports 0 issues, and all thirteen commits build, vet and test on their own after the rebase. Negative controls, re-run on the rebased head: putting the kubeconfigs back in the shared temp directory fails TestClusterKubeconfigPath on Should not be: "/tmp", and skipping the lock removal fails TestRemoveClusterKubeconfig on file ".../kind-gamma.kubeconfig.lock" exists.

One thing your two requests collided on, worth flagging rather than quietly leaving. The pin you asked for on the partial-cluster delete lived in TestInvocations/a stalled create deletes the partial cluster, and that went with the stub tests you later asked to drop. The fix itself is still in commands.go, but nothing covers it now, and the same is true of the stderr capture. Both are cheap to re-pin without a shell stub if you want them held; say the word and I will, otherwise they ride on the real binary like the rest of the CLI plumbing.

On the evidence, unchanged: the stderr capture that would name a delete failure ships in the same commit as the fix, so the lock race is still inferred from the local experiment rather than observed in CI.

Copilot AI review requested due to automatic review settings July 30, 2026 23:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vyncint vyncint changed the title fix(e2e): bound how many kind clusters run at once fix(e2e): stop the suite failing on its own concurrency Jul 30, 2026

@csatib02 csatib02 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chased the flake through the CI history before this lands. The per-cluster --kubeconfig and the stderr capture look right. Two things on the rest.

-parallel 1 and the 16m kind cap work against each other. E2E_TEST_TIMEOUT is unchanged at 20m and it applies per binary, so serialising fluentd-aggregator's four tests spends that one budget four times over. From your own runs: 160.87s and 269.18s parallel in 30546098711, against 629.27s and 490.66s serialised in 30590989952. That is 41-52% of the 1200s cap where it was 13-22%. On the same green runner the single-test packages came in 5-10% faster than baseline (watch-selector 96.6s vs 106.9s), so the jump is serialisation rather than a slow runner. Detail inline on the Makefile.

Most of this PR works around one line. e2e/common/cluster.go:73 uses RequireNoError for the teardown delete, so a cluster that was destroyed cleanly still fails the test when kind's kubeconfig bookkeeping errors. Lines 70 and 71 already use assert.NoError. In 30554200418, TestFluentbitAgentDedicatedNamespace logged Collecting coverage files ... and then failed only at cluster.go:73: the body passed and teardown killed it. TestFluentbitSingleTenantPlusInfra has the same shape, and the two are about 20s apart, so it is not a herd effect. Making 73 match 70-71 retires that failure class whatever the kubeconfig layout is.

Worth splitting in two. Land the per-cluster --kubeconfig and the stderr capture on their own, hold -parallel 1.

One note on the evidence. The stderr capture that would name the delete failure ships in the same commit as the fix, so 30589321074 still shows a bare exit status 1; the lock race is inferred from your local experiment rather than observed in CI. And the green run is one sample. Master was green on 4 of 5 runs after the series with the old flags, so it does not yet separate a real improvement from a quiet runner.

Comment thread Makefile Outdated
Comment thread e2e/internal/kind/commands.go
Comment thread e2e/common/kind.go Outdated
@csatib02

Copy link
Copy Markdown
Member

Three things in e2e/common/helpers.go. Putting them here rather than in the fixture follow-up because they all sit on the concurrency path this PR changes.

Initialize is where -parallel 1 actually lands. helpers.go:60-70 does a sharding skip and then calls t.Parallel(), so that is the call the new flag throttles.

The sharding wrapped around it is dead and inverted. localSeq is a per-binary counter and 10 of the 13 suites hold exactly one test, so localSeq is always 1 in those binaries. With SHARDS=2 that gives 1 % 2 != 0 for shard 0, which skips, and 1 % 2 == 1 for shard 1, which runs: shard 0 runs nothing and shard 1 runs everything. The matrix that would drive it is commented out (e2e.yaml:80-81, 136-137), so nothing exercises it either way. Either drop it and keep the bare t.Parallel(), or shard on t.Name() instead of a counter that cannot tell single-test packages apart. Dropping looks right given E2E_CLUSTERS now covers what sharding was for, and it makes the -parallel 1 decision easier to reason about.

RequireNoError calls t.FailNow() (helpers.go:53-58) and cluster.go:65-66 calls it off the test goroutine.

go func() {
	RequireNoError(t, cluster.Start(ctx))
}()

FailNow has to run on the test goroutine; anywhere else it is undefined and the error is dropped, so a cluster that fails to start surfaces later as a confusing Eventually timeout. Same class as cluster.go:73 in my inline comment, in a file this PR already touches. #2295 fixed this shape for LogProducer and this one was left behind.

helpers.go:231 still hardcodes what 96b3131 fixed in the other copy. LoggingTenant takes nsTenant and honours it at :189, :205 and :230, then hardcodes the watch namespace:

WatchNamespaces: []string{"tenant"},

Latent rather than live: both callers pass nsTenant := "tenant" (fluentbit-multitenant:77, fluentbit-hotreload:79), so it happens to line up today. But internal/fixture/tenancy.go:116 already carries the corrected WatchNamespaces: []string{nsTenant}, so the live path and the fixture copy disagree on the same logic. If the fixture work lands separately, worth carrying that one line back here so the fix is not blocked on it.

vyncint added a commit to vyncint/logging-operator that referenced this pull request Jul 31, 2026
Review feedback on kube-logging#2301. E2E_TEST_TIMEOUT is 20m per binary, so serialising a
package spends that one budget on every test in it. fluentd-aggregator's four
took 629.27s and 490.66s serialised (30590989952) against 160.87s and 269.18s
parallel (30546098711), which is 41-52% of the 1200s cap where it was 13-22%.

It also disarms the kind cap this series added. timeoutFraction derives that cap
from the same 20m, so once two tests have run in sequence there is under 16m of
package budget left and the readable "timed out after 16m0s" can no longer fire
before the binary panics with "test timed out after 20m0s" instead.

-p is what bounds peak clusters, and it does it without costing wall time: at
most 4 suite binaries at a time, so 8 clusters for the heaviest combination
(fluentd-aggregator's 4, volumedrain's 2 and one each from two more) against the
17 an unbounded run creates. Raising the timeout and deriving the kind cap from
t.Deadline() would make -parallel 1 workable, but that wants its own
measurements rather than riding along here.

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
vyncint added a commit to vyncint/logging-operator that referenced this pull request Jul 31, 2026
Review feedback on kube-logging#2301. The delete that clears a half-built cluster after a
create times out did not name the kubeconfig the create used, so it fell back to
the shared default. That is the delete most likely to lose the lock, because it
runs exactly when several creates are timing out together.

The stalled-create case creates with a kubeconfig now, so the delete's arguments
are pinned rather than only its presence: without the fix it reports
`delete cluster --name stuck` against the expected
`delete cluster --kubeconfig /tmp/kind-stuck.kubeconfig --name stuck`.

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
@csatib02

Copy link
Copy Markdown
Member

Please stop your agent from over-commenting. :)

vyncint added a commit to vyncint/logging-operator that referenced this pull request Jul 31, 2026
Review feedback on kube-logging#2301. Two problems with the per-cluster kubeconfig path.

Nothing removed the file. kindCluster.Cleanup only removes the os.CreateTemp
copy, and kind rewrites the kubeconfig it manages rather than unlinking it, so a
run that was killed handed its file, and any lock left mid-write, to the next
one.

The path was also predictable in a shared directory, where GetTestCluster
already uses os.CreateTemp for the same kind of file. The names now sit inside
one os.MkdirTemp directory per run, created 0700, so nothing planted at a
guessable path can be written through. DeleteTestCluster removes the kubeconfig
and the lock once the cluster is gone, and the directory goes with the last
cluster of the binary.

common had no tests. These cover the path being per-cluster, stable across calls
so create and delete agree, outside the shared temp directory, and the removal
taking the lock with it.

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
vyncint added a commit to vyncint/logging-operator that referenced this pull request Jul 31, 2026
…finish

Review feedback on kube-logging#2301. Two RequireNoError calls in WithCluster, both of which
turn a working test red or drop an error outright.

RequireNoError ends in t.FailNow, which is only defined on the test goroutine.
Called inside the Start goroutine it did neither of the things it looks like it
does: the failure was not recorded, and a cluster that never came up resurfaced
as an unrelated Eventually timeout further down. Start's result now travels back
over a channel and is asserted in the deferred block, on the test goroutine and
after the context is canceled. The bounded wait is there so a runnable that will
not stop is named rather than left to exhaust the binary's -timeout.

The teardown delete used RequireNoError where lines above it use assert.NoError,
so a cluster that came down cleanly still failed its test whenever kind's
kubeconfig bookkeeping errored. In 30554200418 TestFluentbitAgentDedicatedNamespace
logged its coverage collection and then failed only there. It matches the
assertions around it now, and FailNow no longer cuts the rest of the teardown
short.

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
vyncint added a commit to vyncint/logging-operator that referenced this pull request Jul 31, 2026
Review feedback on kube-logging#2301. The sharding wrapped around t.Parallel was dead and
inverted.

sequence is a per-binary counter and ten of the thirteen suites hold exactly one
test, so localSeq is always 1 in those binaries. With SHARDS=2 that gives
1 % 2 != 0 for shard 0, which skips, against 1 % 2 == 1 for shard 1, which runs:
shard 0 would run nothing and shard 1 everything. Nothing exercised it either
way, because the matrix that would set SHARD and SHARDS is commented out in
e2e.yaml.

E2E_CLUSTERS bounds the concurrency the sharding was reaching for, so this drops
it rather than reworking it to shard on t.Name(), and leaves Initialize as the
bare t.Parallel the suites already expect.

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
vyncint added a commit to vyncint/logging-operator that referenced this pull request Jul 31, 2026
Review feedback on kube-logging#2301. LoggingTenant honours nsTenant everywhere except the
watch namespace, which was the literal "tenant". WatchNamespaces takes
namespaces, not loggingRefs, so it has to name the namespace the Flow and Output
above were created in.

Latent rather than live: both callers pass nsTenant := "tenant", so the two
strings line up today. internal/fixture/tenancy.go already carries this fix,
which left the live path and the fixture copy disagreeing on the same logic.

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
Comment thread e2e/common/helpers.go
@vyncint

vyncint commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

All three fixed. Checked each against the code first rather than taking them on trust, and all three hold exactly as described.

Sharding — aa54d51. Dead and inverted, confirmed. sequence is per binary, and I counted the suites: ten of thirteen hold exactly one test, so localSeq is always 1 in those. With SHARDS=2 shard 0 skips on 1 % 2 != 0 and shard 1 runs on 1 % 2 == 1, so shard 0 would have run nothing. The matrix is commented out at both places you cite. Dropped rather than reshuffled onto t.Name(), since E2E_CLUSTERS covers what it was for, and Initialize is now the bare t.Parallel.

RequireNoError off the test goroutine — 8e5a84f. cluster.Start blocks, so unlike #2295's LogProducer this one cannot simply lose its go. Start's result comes back over a buffered channel and is asserted in the deferred block instead, on the test goroutine and after cancel(). That also stops the goroutine outliving the test, which was a second way the same call could misbehave. I bounded the wait at a minute so a runnable that will not stop gets named rather than running the binary out of its -timeout — swapping one unreadable panic for another would not be much of a fix.

The same commit makes the teardown delete assert.NoError, matching lines 70-71. Your reading of 30554200418 is what convinced me it is worth doing independently of the kubeconfig layout: the body passed, coverage was collected, and only cluster.go:73 failed. FailNow there also cut the rest of the deferred block short, so it was losing cleanup as well as reporting a false failure.

helpers.go:23110ce2fa. Carried back, one line, so the live path and internal/fixture no longer disagree. Latent as you say — both callers pass nsTenant := "tenant" — so nothing changes for the suites as they stand.

make check passes, golangci-lint in e2e/ reports 0 issues, and all eight commits build, vet and test on their own.

vyncint added a commit to vyncint/logging-operator that referenced this pull request Jul 31, 2026
Review feedback on kube-logging#2301. The matrix that would have driven SHARD and SHARDS was
already commented out, and the sharding it fed is gone, so the block goes with
it.

fail-fast stays: it came in with an earlier k8s-version matrix, not this one.

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
vyncint added a commit to vyncint/logging-operator that referenced this pull request Jul 31, 2026
Review feedback on kube-logging#2301. The comments this series added explained more than the
code needed. Cut to one line each where they earn it and dropped where they did
not: 27 lines of prose down to 8, plus assertion messages that only restated the
expected value.

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
@vyncint

vyncint commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Fair, and noted for the replies as much as the code. Trimmed in a44ce1e: 27 lines of comment prose down to 8, and the assertion messages that only restated the expected value are gone.

@csatib02

Copy link
Copy Markdown
Member

Re-ran the numbers on the final head. This one looks done: fluentd-aggregator is back to 272.5s against 629.3s and 490.7s under -parallel 1, so the headroom is restored, and the teardown and kubeconfig changes cover what I raised. The self-review in 55a3915 also caught a removal race I had read past.

One thing left before the baseline is clean, and it is not this PR's doing. elasticsearch-multiversion is now the only package near its cap. Six runs:

run result duration
30590989952 ok 387.9s
30546098711 ok 625.5s
30625716801 ok 856.4s
30622235964 ok 988.5s
30621489940 FAIL 1200.0s
30554200418 FAIL 1200.0s

A 3.1x spread across the green runs, 988.5s is 82% of the 1200s cap, and two of the six hit it. 30621489940 is on this branch, so it is not a master-only effect. Both failures are panic: test timed out after 20m0s, which is the one failure mode that skips every deferred teardown: no stern dump, no coverage, and the kind cluster stays up for the rest of the job.

Two things feed it.

esReadyMargin is 30s and the comment at :53-56 already says it does not cover teardown. Teardown is a stern dump, a kubectl exec plus tar for coverage, and a kind delete, which together run well past 30s.

The three waits at :490, :495 and :500 each call esReadyBudget(t) separately, and each call re-derives time.Until(deadline) - 30s. So the first wait is allowed to consume everything, and once under the margin budgetWithin floors at time.Second, so the remaining two fail almost at once. The reported failure is then elasticsearch8 or elasticsearch9 not ready when the actual cause is that elasticsearch7 spent the package budget.

Cheapest first, if you want to pick this up separately:

  1. Divide the budget rather than re-deriving it whole. Something like remaining / wavesLeft so one slow deployment cannot starve the other two, and the failure names the deployment that was actually slow.
  2. Give this package its own -timeout. Each suite is its own binary, so it can run at 30m while the rest stay at 20m, without touching the others.
  3. The root cause is that readiness is most of this package's runtime. A liveness-probe kill-loop has been the suspicion for a while but nobody has captured a restart count. One kubectl get pod -o jsonpath='{..restartCount}' inside the wait loop would settle it, and if it is the probe then the budget arithmetic stops mattering.

Not asking for any of this here. This PR is green and worth landing as it is; raising it so the next one starts from a baseline where a timeout means something is actually wrong.

@vyncint

vyncint commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Hi @csatib02, heads up that c3ee82b landed just after your comment, so it is not in what you measured: no behaviour change, it just fixes the reasoning in 55a3915 (kind recreates the missing parent itself at 0755, so the mode is the real issue) and the test now checks the 0700. Happy to take the elasticsearch-multiversion budget problem in a separate issue once this ands. CI is green on the new head, so if that correction looks right, could you turn this into an approval?

@csatib02

Copy link
Copy Markdown
Member

The reasoning is right, the code does not follow it yet.

os.MkdirAll returns nil without touching the mode when the directory already exists, so the 0700 is not reasserted in the case this commit names. A throwaway test in e2e/common reproduces it:

func TestProbe(t *testing.T) {
	first, err := clusterKubeconfigPath("probe-a")
	require.NoError(t, err)
	dir := filepath.Dir(first)

	require.NoError(t, os.RemoveAll(dir))
	require.NoError(t, os.MkdirAll(dir, 0o755)) // kind's doing, per this commit

	_, err = clusterKubeconfigPath("probe-b")
	require.NoError(t, err)

	info, err := os.Stat(dir)
	require.NoError(t, err)
	require.Equal(t, os.FileMode(0o700), info.Mode().Perm())
}

Gives 0755.

TestClusterKubeconfigPathRestoresTheDirectory passes because os.RemoveAll leaves the directory absent, so MkdirAll creates it fresh at 0700. It never puts a 0755 directory in place, which is the state the fix exists for.

Two lines: os.Chmod(dir, 0o700) after the MkdirAll, and have the test pre-create at 0755 rather than removing it.

Low impact either way. MkdirTemp gives 0700, nothing in the suite removes the directory, so 0755 needs an outside actor, and the kubeconfigs themselves are 0600, so the directory exposes cluster names rather than credentials. I would still rather not land a test that passes for a different reason than the one it documents.

Separately: you checked v0.32.0, Makefile:20 pins KIND_VERSION ?= 0.31.0. Worth confirming the 0755 behaviour on the version CI downloads.

Happy to approve with those two lines in. Everything else holds on c3ee82b, 26 checks pass, and fluentd-aggregator at 272.5s confirms the headroom is back. ES budget as a separate issue works for me.

@csatib02

Copy link
Copy Markdown
Member

Great!

A few things I would rather handle now, before we move forward:

  1. elasticsearch-multiversion budget. As discussed above, a separate issue works. It is the only package near its cap, and it decides whether a red e2e run means anything.
  2. The tests with embedded shell scripts inside, I would instead drop those, we don't get much benefit from them anyway, and they are flaky.
  3. TestWatchNamespaces in controllers/logging. ci.yaml run 30621399293 on this branch failed on it, --- FAIL: TestWatchNamespaces (10.01s). Let's tackle that now, if we sensed it!

@vyncint

vyncint commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

@csatib02 both done in 2d855bf and c30dc38. Ready for another look when you have a moment, and happy to get your approval if it all reads right.

@csatib02

csatib02 commented Aug 3, 2026

Copy link
Copy Markdown
Member

Hey @vyncint,

Please rebase this PR, and resolve the conflicts!

vyncint added 4 commits August 3, 2026 17:05
kind locks the kubeconfig it updates and the lock is non-blocking, so
concurrent clusters sharing the default one lose the race and exit 1. That
failed tests whose bodies had already passed, during cleanup.Reproduced with 96 concurrent deletes of clusters that do not exist: 7 fail with
"failed to lock config file" on a shared kubeconfig, none on their own.Delete also discarded kind's stderr, which is why the failure only ever
reported "exit status 1".

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
Review feedback on kube-logging#2301. The delete that clears a half-built cluster after a
create times out did not name the kubeconfig the create used, so it fell back to
the shared default. That is the delete most likely to lose the lock, because it
runs exactly when several creates are timing out together.The stalled-create case creates with a kubeconfig now, so the delete's arguments
are pinned rather than only its presence: without the fix it reports
`delete cluster --name stuck` against the expected
`delete cluster --kubeconfig /tmp/kind-stuck.kubeconfig --name stuck`.

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
Review feedback on kube-logging#2301. Two problems with the per-cluster kubeconfig path.Nothing removed the file. kindCluster.Cleanup only removes the os.CreateTemp
copy, and kind rewrites the kubeconfig it manages rather than unlinking it, so a
run that was killed handed its file, and any lock left mid-write, to the next
one.The path was also predictable in a shared directory, where GetTestCluster
already uses os.CreateTemp for the same kind of file. The names now sit inside
one os.MkdirTemp directory per run, created 0700, so nothing planted at a
guessable path can be written through. DeleteTestCluster removes the kubeconfig
and the lock once the cluster is gone, and the directory goes with the last
cluster of the binary.common had no tests. These cover the path being per-cluster, stable across calls
so create and delete agree, outside the shared temp directory, and the removal
taking the lock with it.

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
…finish

Review feedback on kube-logging#2301. Two RequireNoError calls in WithCluster, both of which
turn a working test red or drop an error outright.RequireNoError ends in t.FailNow, which is only defined on the test goroutine.
Called inside the Start goroutine it did neither of the things it looks like it
does: the failure was not recorded, and a cluster that never came up resurfaced
as an unrelated Eventually timeout further down. Start's result now travels back
over a channel and is asserted in the deferred block, on the test goroutine and
after the context is canceled. The bounded wait is there so a runnable that will
not stop is named rather than left to exhaust the binary's -timeout.The teardown delete used RequireNoError where lines above it use assert.NoError,
so a cluster that came down cleanly still failed its test whenever kind's
kubeconfig bookkeeping errored. In 30554200418 TestFluentbitAgentDedicatedNamespace
logged its coverage collection and then failed only there. It matches the
assertions around it now, and FailNow no longer cuts the rest of the teardown
short.

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
vyncint added 9 commits August 3, 2026 17:05
Review feedback on kube-logging#2301. The sharding wrapped around t.Parallel was dead and
inverted.sequence is a per-binary counter and ten of the thirteen suites hold exactly one
test, so localSeq is always 1 in those binaries. With SHARDS=2 that gives
1 % 2 != 0 for shard 0, which skips, against 1 % 2 == 1 for shard 1, which runs:
shard 0 would run nothing and shard 1 everything. Nothing exercised it either
way, because the matrix that would set SHARD and SHARDS is commented out in
e2e.yaml.E2E_CLUSTERS bounds the concurrency the sharding was reaching for, so this drops
it rather than reworking it to shard on t.Name(), and leaves Initialize as the
bare t.Parallel the suites already expect.

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
Review feedback on kube-logging#2301. LoggingTenant honours nsTenant everywhere except the
watch namespace, which was the literal "tenant". WatchNamespaces takes
namespaces, not loggingRefs, so it has to name the namespace the Flow and Output
above were created in.Latent rather than live: both callers pass nsTenant := "tenant", so the two
strings line up today. internal/fixture/tenancy.go already carries this fix,
which left the live path and the fixture copy disagreeing on the same logic.

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
Review feedback on kube-logging#2301. The matrix that would have driven SHARD and SHARDS was
already commented out, and the sharding it fed is gone, so the block goes with
it.fail-fast stays: it came in with an earlier k8s-version matrix, not this one.

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
Review feedback on kube-logging#2301. The comments this series added explained more than the
code needed. Cut to one line each where they earn it and dropped where they did
not: 27 lines of prose down to 8, plus assertion messages that only restated the
expected value.

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
Self-review of the kubeconfig directory added earlier in this series. Two ways it
could strand a cluster, both my own doing.Removing the directory with the binary's last cluster raced any create still in
flight. kind writes the kubeconfig only once the control plane is up, so between
a create taking its path and kind writing to it there is a window the length of a
whole cluster build. Any other test finishing its teardown in that window found
the directory empty and removed it, and the create then failed on a missing
parent. Two parallel tests in one package are enough. The directory now stays for
the run; the per-cluster file and lock still go.The path is also cached for the run by sync.OnceValues, so once anything had
removed the directory every later lookup handed back a dead path. It is created
on lookup now rather than assumed. The test helper had been papering over this
with its own MkdirAll, which is what gave it away.Both helpers are unexported: nothing outside the package used them.

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
Correcting the reasoning in 55a3915, which named the wrong consequence.kind creates a missing kubeconfig parent itself, so removing the directory never
failed a create the way that commit message claims. Checked against kind v0.32.0:
`delete cluster --kubeconfig <missing dir>/kc` exits 0 and leaves the directory
behind at 0755.0755 is the actual consequence, and a worse one for being silent: the directory
exists to be 0700, and letting kind recreate it hands that away without anything
failing. Both halves of that commit still stand, for this reason rather than the
one given: the directory stays for the run, and the 0700 is reasserted on lookup
instead of being left to MkdirTemp.The test now pins the mode rather than only the path, and fails on the missing
file without the MkdirAll.

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
MkdirAll returns nil without touching the mode when the directory already
exists, so c3ee82b never reasserted the 0700 in the case it named. The test
passed because os.RemoveAll left the directory absent, exercising only
MkdirAll creating a fresh one at the mode it is given.The test now pre-creates at 0755, the state kind leaves behind, and fails
without the Chmod.kind writes the parent at 0755 on both v0.31.0 and v0.32.0, so the behaviour
holds on the 0.31.0 that Makefile:20 pins for CI.

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
The stub is a shell script the tests write to disk and drive through
FAKE_KIND_* variables, and its stall paths race. TestInvocations/a stalled
create deletes the partial cluster failed once in ten local runs. These run
in `make test`, so the flake lands on unrelated PRs.What they covered is the kind CLI plumbing, which the e2e suite exercises
against the real binary anyway. The timeout arithmetic keeps its five tests:
they are pure Go with no stub behind them, and they hold the invariant that
a derived cap stays under the enclosing deadline.

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
…poll

c.expectedResult() was an argument to Should, so Go evaluated it once before
Eventually started polling. The "full list" case reads the live namespace
set, and envtest never finishes deleting a namespace, so the snapshot went
stale as the rest of the package created more and the poll could never match.Seen in CI as `--- FAIL: TestWatchNamespaces (10.01s)`, with test-f5c4d243, a
namespace belonging to a different test in the package, sitting in the
expected list.Measured: as an argument the expectation is evaluated once against however
many polls run; inside the poll it is evaluated once per attempt, so both
sides are read together and a set still settling converges.

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
@vyncint
vyncint force-pushed the fix-e2e-cluster-concurrency branch from c30dc38 to 4c52374 Compare August 3, 2026 10:08
@vyncint vyncint changed the title fix(e2e): stop the suite failing on its own concurrency fix(e2e): give every kind cluster its own kubeconfig Aug 3, 2026
@vyncint

vyncint commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Hi @csatib02, I rebased this PR, and resolved the conflicts. Please help me review it again!

@csatib02

csatib02 commented Aug 3, 2026

Copy link
Copy Markdown
Member

One thing lost in your rebase.
The -p ${E2E_CLUSTERS} is gone, therefore the E2E_CLUSTERS count = 0 in Makefile.
Line 250 now only does -parallel ${E2E_SUITE_PARALLEL}.
Please re-add it, then this is good to go!

-parallel bounds tests inside a binary; -p bounds the binaries and still
defaults to GOMAXPROCS, so peak clusters followed the core count. Measured on a
harness with the suite's package and test shape: 5 at four cores, 10 at eight,
15 at sixteen. Pinning -p holds it at 5 everywhere.

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
@csatib02
csatib02 merged commit 3adec6d into kube-logging:master Aug 3, 2026
29 checks passed
verdel pushed a commit to verdel/logging-operator that referenced this pull request Aug 6, 2026
common.RequireNoError calls t.FailNow, which the testing package requires to be
called from the goroutine running the test. testify's require.Eventually runs its
condition on its own goroutine, so ten of these calls were undefined: the error
was dropped and the suite failed later on the Eventually deadline, naming the
wait rather than the cause.

Two sites in fluentd-aggregator, four in fluentd-aggregator-detached and four in
syslog-ng-aggregator-detached now log and return false, which also turns a
transient API error into a retry instead of a hard stop. The excess-config Create
tolerates AlreadyExists, because the List guarding it can be stale by the time a
retry runs.

Same class as the LogProducer fix in kube-logging#2295 and the cluster.Start fix in kube-logging#2301.

Signed-off-by: Bence Csati <bence.csati@axoflow.com>
verdel pushed a commit to verdel/logging-operator that referenced this pull request Aug 6, 2026
go test defaults -parallel to GOMAXPROCS, so peak live clusters follows the
runner's core count: 8 today, from fluentd-aggregator's 4 tests plus volumedrain's
2 plus one each from two more suites. Every one of them imports the same six
images, and all observed 16m deadline hits were kind load, none were kind create.
-parallel 2 caps the peak at 6 and stops it growing with the core count.

This reverses the reasoning that dropped -parallel 1 from kube-logging#2301, which measured
fluentd-aggregator alone. elasticsearch-multiversion has two test functions but
only one builds a cluster, so -parallel cannot serialise the suite; it is instead
the largest beneficiary of a lower peak, because its runtime is mostly
Elasticsearch readiness and that is what starves first. Measured: 856.4s at
-parallel 4 (30625716801) against 387.9s at -parallel 1 (30590989952), while
fluentd-aggregator went 272.5s to 490.7s. The longest package, which is what the
job waits for, therefore fell from 856s to 491s.

The kind command cap stays at 16m. Anchoring it to healthy timings was already
measured as a regression: at 10m, runs failed that would have passed, because a
degraded runner is 2.3-6.2x slower and the loads complete rather than wedge.

-parallel 1 and 4 are measured; 2 is interpolated between them. If load timeouts
survive this, 1 is the fallback and has two green runs behind it.

Signed-off-by: Bence Csati <bence.csati@axoflow.com>
@csatib02 csatib02 added the bugfix label Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

E2E: unbounded kind CLI calls turn a stalled cluster build into a 20-minute panic that takes the whole package down

3 participants