Skip to content

Commit 31e84e1

Browse files
authored
Merge pull request #2302 from kube-logging/fix/e2e-flakes-cap-failnow
fix(e2e): remove the elasticsearch kill loop and five flake sources
2 parents 04e6f77 + c1ff747 commit 31e84e1

8 files changed

Lines changed: 126 additions & 69 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,8 @@ v1alpha1 is the legacy API; conversion functions exist to v1beta1 (which is the
154154
## Testing
155155

156156
- Unit/integration tests use `envtest` (embedded Kubernetes API server + etcd); no cluster needed
157-
- E2E tests in `e2e/` use KIND and cover scenarios: fluentd-aggregator, fluentbit-multitenant, syslog-ng-aggregator
157+
- E2E tests in `e2e/` use KIND: one directory per suite, each its own Go package and test binary, provisioning its own KIND cluster. Run a single suite with `make test-e2e E2E_TEST=<suite-dir>`. Shared helpers live in `e2e/common/` and `e2e/internal/` and are excluded from suite selection
158+
- E2E knobs (all `make` overrides): `E2E_TEST_TIMEOUT` (per suite binary, default 20m), `E2E_SUITE_PARALLEL` (clusters one suite binary builds at once, default 2 — raising it starves the aggregators on a 4-vCPU runner), `KIND_COMMAND_TIMEOUT` (per kind invocation; derived from `E2E_TEST_TIMEOUT` when unset)
158159
- Coverage profile config in `.testcoverage.yml`; tool: `go-test-coverage`
159160
- Test framework: Ginkgo + Gomega for BDD-style tests; testify for unit tests
160161

Makefile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ OPERATOR_IMG_DEBUG ?= controller:debug
5353
CRD_OPTIONS ?= crd:maxDescLen=0
5454

5555
E2E_TEST_TIMEOUT ?= 20m
56+
57+
# Clusters one suite binary builds at once. Caps peak concurrency, which
58+
# otherwise follows the core count and starves the aggregators.
59+
E2E_SUITE_PARALLEL ?= 2
60+
5661
TEST_COV_DIR := $(shell mkdir -p build/_test_coverage && realpath build/_test_coverage)
5762

5863
CONTROLLER_GEN := ${BIN}/controller-gen
@@ -242,7 +247,7 @@ test-e2e-nodeps:
242247
KIND_IMAGE="$(KIND_IMAGE)" \
243248
PROJECT_DIR="$(PWD)" \
244249
E2E_TEST_COV_DIR=${TEST_COV_DIR} \
245-
go test -count=1 -v -timeout ${E2E_TEST_TIMEOUT} $$(go list ./${E2E_TEST}/... | grep -vE '/e2e/(common|internal)(/|$$)')
250+
go test -count=1 -v -parallel ${E2E_SUITE_PARALLEL} -timeout ${E2E_TEST_TIMEOUT} $$(go list ./${E2E_TEST}/... | grep -vE '/e2e/(common|internal)(/|$$)')
246251
go tool covdata textfmt -i=${TEST_COV_DIR}/covdatafiles -o ${TEST_COV_DIR}/coverage_e2e.out
247252
@echo "--- E2E test coverage report"
248253
go tool covdata percent -i=${TEST_COV_DIR}/covdatafiles

e2e/common/cluster.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package common
1616

1717
import (
18+
"bytes"
1819
"context"
1920
"fmt"
2021
"os"
@@ -59,6 +60,11 @@ func WithCluster(name string, t *testing.T, fn func(*testing.T, Cluster), before
5960
ctrl.SetLogger(zapLogger)
6061

6162
cluster, err := GetTestCluster(name, opts...)
63+
if err != nil {
64+
// The cluster is created before the client can fail, and the deferred
65+
// teardown below is not registered yet.
66+
assert.NoError(t, DeleteTestCluster(name))
67+
}
6268
RequireNoError(t, err)
6369

6470
ctx, cancel := context.WithCancel(context.Background())
@@ -134,12 +140,18 @@ func (c kindCluster) CollectTestCoverageFiles(ns string, loggingOperatorName str
134140
return errors.WrapIfWithDetails(err, "Error in sending signal to logging-operator", cmdOut)
135141
}
136142
testCovDir := os.Getenv("E2E_TEST_COV_DIR")
137-
cmd = exec.Command("sh", "-c", fmt.Sprintf(
138-
"kubectl --kubeconfig %s -n %s exec deployment/%s -- tar -cf - /covdatafiles | tar -xf - -C %s",
139-
c.KubeConfigFilePath(), ns, loggingOperatorName, testCovDir))
140-
cmdOut, err = cmd.Output()
143+
archive := CmdEnv(exec.Command("kubectl", "-n", ns,
144+
"exec", fmt.Sprintf("deployment/%s", loggingOperatorName), "--",
145+
"tar", "-cf", "-", "/covdatafiles"), c)
146+
tarball, err := archive.Output()
141147
if err != nil {
142-
return errors.WrapIfWithDetails(err, "Error in collecting test coverage files", cmdOut)
148+
return errors.WrapIfWithDetails(err, "Error in reading test coverage files", tarball)
149+
}
150+
151+
extract := exec.Command("tar", "-xf", "-", "-C", testCovDir)
152+
extract.Stdin = bytes.NewReader(tarball)
153+
if cmdOut, err := extract.CombinedOutput(); err != nil {
154+
return errors.WrapIfWithDetails(err, "Error in extracting test coverage files", cmdOut)
143155
}
144156
return nil
145157
}

e2e/common/kind.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515
package common
1616

1717
import (
18+
"fmt"
1819
"strings"
1920

21+
"emperror.dev/errors"
22+
2023
"github.com/kube-logging/logging-operator/e2e/internal/kind"
2124
)
2225

@@ -28,13 +31,24 @@ const KindClusterCreationTimeout = "3m"
2831
var kindCLI = kind.New()
2932

3033
func KindClusterKubeconfig(name string) ([]byte, error) {
31-
err := kindCLI.CreateCluster(kind.CreateClusterOptions{
34+
create := kind.CreateClusterOptions{
3235
Name: name,
3336
Wait: KindClusterCreationTimeout,
34-
})
35-
if err != nil && !isClusterAlreadyExistsError(err) {
37+
}
38+
39+
err := kindCLI.CreateCluster(create)
40+
if err != nil && isClusterAlreadyExistsError(err) {
41+
// Adopting a leftover would hand the suite an unknown operator and data.
42+
fmt.Printf("kind cluster %q already exists, recreating it\n", name)
43+
if err := kindCLI.DeleteCluster(kind.DeleteClusterOptions{Name: name}); err != nil {
44+
return nil, errors.WrapIfWithDetails(err, "deleting a leftover kind cluster", "clusterName", name)
45+
}
46+
err = kindCLI.CreateCluster(create)
47+
}
48+
if err != nil {
3649
return nil, err
3750
}
51+
3852
return kindCLI.GetKubeconfig(kind.GetKubeconfigOptions{
3953
Name: name,
4054
})

e2e/elasticsearch-multiversion/elasticsearch_multiversion_test.go

Lines changed: 42 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,29 @@ func init() {
7070
}
7171
}
7272

73+
// logContainerRestarts names a restart loop while a readiness wait is still
74+
// running, so it is a reported number rather than an unexplained stall.
75+
func logContainerRestarts(t *testing.T, c common.Cluster, ctx context.Context, ns, app string) {
76+
var pods corev1.PodList
77+
if err := c.GetClient().List(ctx, &pods, client.InNamespace(ns), client.MatchingLabels{"app": app}); err != nil {
78+
t.Logf("listing %s pods failed: %v", app, err)
79+
return
80+
}
81+
82+
for _, pod := range pods.Items {
83+
for _, status := range pod.Status.ContainerStatuses {
84+
if status.RestartCount == 0 {
85+
continue
86+
}
87+
reason := "unknown"
88+
if terminated := status.LastTerminationState.Terminated; terminated != nil {
89+
reason = fmt.Sprintf("%s, exit %d", terminated.Reason, terminated.ExitCode)
90+
}
91+
t.Logf("%s/%s restarted %d times, last termination: %s", pod.Name, status.Name, status.RestartCount, reason)
92+
}
93+
}
94+
}
95+
7396
// esReadyBudget caps a wait strictly below the enclosing -timeout, so an
7497
// Elasticsearch deployment that never becomes ready fails by name instead of
7598
// letting the package binary panic and report only a goroutine dump.
@@ -210,20 +233,12 @@ func TestElasticsearch_MultiVersion(t *testing.T) {
210233
corev1.ResourceCPU: resource.MustParse("500m"),
211234
},
212235
Limits: corev1.ResourceList{
213-
corev1.ResourceMemory: resource.MustParse("1Gi"),
236+
corev1.ResourceMemory: resource.MustParse("1536Mi"),
214237
corev1.ResourceCPU: resource.MustParse("1000m"),
215238
},
216239
},
217-
LivenessProbe: &corev1.Probe{
218-
ProbeHandler: corev1.ProbeHandler{
219-
HTTPGet: &corev1.HTTPGetAction{
220-
Path: "/_cluster/health",
221-
Port: intstr.FromInt(9200),
222-
},
223-
},
224-
InitialDelaySeconds: 60,
225-
PeriodSeconds: 10,
226-
},
240+
// No liveness probe: readiness already gates the wait, and a
241+
// 60s + 3x10s deadline killed the JVM mid-boot on a loaded runner.
227242
ReadinessProbe: &corev1.Probe{
228243
ProbeHandler: corev1.ProbeHandler{
229244
HTTPGet: &corev1.HTTPGetAction{
@@ -332,20 +347,12 @@ func TestElasticsearch_MultiVersion(t *testing.T) {
332347
corev1.ResourceCPU: resource.MustParse("500m"),
333348
},
334349
Limits: corev1.ResourceList{
335-
corev1.ResourceMemory: resource.MustParse("1Gi"),
350+
corev1.ResourceMemory: resource.MustParse("1536Mi"),
336351
corev1.ResourceCPU: resource.MustParse("1000m"),
337352
},
338353
},
339-
LivenessProbe: &corev1.Probe{
340-
ProbeHandler: corev1.ProbeHandler{
341-
HTTPGet: &corev1.HTTPGetAction{
342-
Path: "/_cluster/health",
343-
Port: intstr.FromInt(9200),
344-
},
345-
},
346-
InitialDelaySeconds: 60,
347-
PeriodSeconds: 10,
348-
},
354+
// No liveness probe: readiness already gates the wait, and a
355+
// 60s + 3x10s deadline killed the JVM mid-boot on a loaded runner.
349356
ReadinessProbe: &corev1.Probe{
350357
ProbeHandler: corev1.ProbeHandler{
351358
HTTPGet: &corev1.HTTPGetAction{
@@ -454,20 +461,12 @@ func TestElasticsearch_MultiVersion(t *testing.T) {
454461
corev1.ResourceCPU: resource.MustParse("500m"),
455462
},
456463
Limits: corev1.ResourceList{
457-
corev1.ResourceMemory: resource.MustParse("1Gi"),
464+
corev1.ResourceMemory: resource.MustParse("1536Mi"),
458465
corev1.ResourceCPU: resource.MustParse("1000m"),
459466
},
460467
},
461-
LivenessProbe: &corev1.Probe{
462-
ProbeHandler: corev1.ProbeHandler{
463-
HTTPGet: &corev1.HTTPGetAction{
464-
Path: "/_cluster/health",
465-
Port: intstr.FromInt(9200),
466-
},
467-
},
468-
InitialDelaySeconds: 60,
469-
PeriodSeconds: 10,
470-
},
468+
// No liveness probe: readiness already gates the wait, and a
469+
// 60s + 3x10s deadline killed the JVM mid-boot on a loaded runner.
471470
ReadinessProbe: &corev1.Probe{
472471
ProbeHandler: corev1.ProbeHandler{
473472
HTTPGet: &corev1.HTTPGetAction{
@@ -486,20 +485,16 @@ func TestElasticsearch_MultiVersion(t *testing.T) {
486485
}
487486
common.RequireNoError(t, c.GetClient().Create(ctx, es9Deployment))
488487

489-
t.Log("Waiting for Elasticsearch 7 deployment to be ready...")
490-
require.Eventually(t, func() bool {
491-
return wait.DeploymentAvailable(t, c.GetClient(), ctx, ns, "elasticsearch7")()
492-
}, esReadyBudget(t), 10*time.Second)
493-
494-
t.Log("Waiting for Elasticsearch 8 deployment to be ready...")
495-
require.Eventually(t, func() bool {
496-
return wait.DeploymentAvailable(t, c.GetClient(), ctx, ns, "elasticsearch8")()
497-
}, esReadyBudget(t), 10*time.Second)
498-
499-
t.Log("Waiting for Elasticsearch 9 deployment to be ready...")
500-
require.Eventually(t, func() bool {
501-
return wait.DeploymentAvailable(t, c.GetClient(), ctx, ns, "elasticsearch9")()
502-
}, esReadyBudget(t), 10*time.Second)
488+
for _, name := range []string{"elasticsearch7", "elasticsearch8", "elasticsearch9"} {
489+
t.Logf("Waiting for %s deployment to be ready...", name)
490+
require.Eventually(t, func() bool {
491+
if wait.DeploymentAvailable(t, c.GetClient(), ctx, ns, name)() {
492+
return true
493+
}
494+
logContainerRestarts(t, c, ctx, ns, name)
495+
return false
496+
}, esReadyBudget(t), 10*time.Second)
497+
}
503498

504499
logging := v1beta1.Logging{
505500
ObjectMeta: metav1.ObjectMeta{

e2e/fluentd-aggregator-detached/fluentd_aggregator_detached_test.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,10 @@ func TestFluentdAggregator_detached_MultiWorker(t *testing.T) {
259259
return false
260260
}
261261
if len(logging.Status.FluentdConfigName) == 0 || logging.Status.FluentdConfigName != fluentd.Name {
262-
common.RequireNoError(t, c.GetClient().Get(ctx, utils.ObjectKeyFromObjectMeta(&logging), &logging))
262+
if err := c.GetClient().Get(ctx, utils.ObjectKeyFromObjectMeta(&logging), &logging); err != nil {
263+
t.Logf("reading the Logging failed, retrying: %v", err)
264+
return false
265+
}
263266
t.Logf("logging should use the detached fluentd configuration (name=%s), found: %v", fluentd.Name, logging.Status.FluentdConfigName)
264267
return false
265268
}
@@ -268,15 +271,25 @@ func TestFluentdAggregator_detached_MultiWorker(t *testing.T) {
268271
return false
269272
}
270273
var detachedFluentds v1beta1.FluentdConfigList
271-
common.RequireNoError(t, c.GetClient().List(ctx, &detachedFluentds))
274+
if err := c.GetClient().List(ctx, &detachedFluentds); err != nil {
275+
t.Logf("listing the detached fluentd configurations failed, retrying: %v", err)
276+
return false
277+
}
272278
if len(detachedFluentds.Items) != 2 {
273-
// Add a new detached fluentd that is not going to be used
274-
common.RequireNoError(t, c.GetClient().Create(ctx, &excessFluentd))
279+
// Add a new detached fluentd that is not going to be used.
280+
// AlreadyExists is tolerated: the list above can be stale on a retry.
281+
if err := client.IgnoreAlreadyExists(c.GetClient().Create(ctx, &excessFluentd)); err != nil {
282+
t.Logf("creating the excess detached fluentd failed, retrying: %v", err)
283+
return false
284+
}
275285
t.Log("creating excess detached fluentd")
276286
return false
277287
} else if isValid := wait.CheckExcessFluentdStatus(t, c.GetClient(), ctx, &excessFluentd); !isValid && len(detachedFluentds.Items) == 2 {
278288
t.Log("checking excess detached fluentd status")
279-
common.RequireNoError(t, c.GetClient().Get(ctx, utils.ObjectKeyFromObjectMeta(&excessFluentd), &excessFluentd))
289+
if err := c.GetClient().Get(ctx, utils.ObjectKeyFromObjectMeta(&excessFluentd), &excessFluentd); err != nil {
290+
t.Logf("reading the excess detached fluentd failed, retrying: %v", err)
291+
return false
292+
}
280293
return false
281294
}
282295

e2e/fluentd-aggregator/fluentd_aggregator_test.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,10 @@ func TestFluentdAggregator_ConfigChecks(t *testing.T) {
407407
output.Spec.FileOutput.Path = "/tmp/zzz"
408408
common.RequireNoError(t, c.GetClient().Patch(ctx, &output, patch))
409409
require.Eventually(t, func() bool {
410-
common.RequireNoError(t, c.GetClient().Get(ctx, utils.ObjectKeyFromObjectMeta(&logging), &logging))
410+
if err := c.GetClient().Get(ctx, utils.ObjectKeyFromObjectMeta(&logging), &logging); err != nil {
411+
t.Logf("reading the Logging failed, retrying: %v", err)
412+
return false
413+
}
411414
if logging.Status.ProblemsCount > 0 {
412415
for _, problem := range logging.Status.Problems {
413416
if configCheckFailure.MatchString(problem) {
@@ -425,7 +428,10 @@ func TestFluentdAggregator_ConfigChecks(t *testing.T) {
425428
output.Spec.FileOutput.Path = "/tmp/logs/${tag}/%Y/%m/%d.%H.%M"
426429
common.RequireNoError(t, c.GetClient().Patch(ctx, &output, patch))
427430
require.Eventually(t, func() bool {
428-
common.RequireNoError(t, c.GetClient().Get(ctx, utils.ObjectKeyFromObjectMeta(&logging), &logging))
431+
if err := c.GetClient().Get(ctx, utils.ObjectKeyFromObjectMeta(&logging), &logging); err != nil {
432+
t.Logf("reading the Logging failed, retrying: %v", err)
433+
return false
434+
}
429435
if logging.Status.ProblemsCount > 0 {
430436
for _, problem := range logging.Status.Problems {
431437
if configCheckFailure.MatchString(problem) {

e2e/syslog-ng-aggregator-detached/syslog_ng_aggregator_detached_test.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,9 @@ func TestSyslogNGDetachedIsRunningAndForwardingLogs(t *testing.T) {
294294
}
295295
if logging.Status.SyslogNGConfigName != syslogNG.Name {
296296
t.Logf("logging should use the detached SyslogNG configuration (name=%s), found: %v", syslogNG.Name, logging.Status.SyslogNGConfigName)
297-
common.RequireNoError(t, c.GetClient().Get(ctx, utils.ObjectKeyFromObjectMeta(&logging), &logging))
297+
if err := c.GetClient().Get(ctx, utils.ObjectKeyFromObjectMeta(&logging), &logging); err != nil {
298+
t.Logf("reading the Logging failed, retrying: %v", err)
299+
}
298300
return false
299301
}
300302

@@ -303,15 +305,24 @@ func TestSyslogNGDetachedIsRunningAndForwardingLogs(t *testing.T) {
303305
return false
304306
}
305307
var detachedSyslogNGs v1beta1.SyslogNGConfigList
306-
common.RequireNoError(t, c.GetClient().List(ctx, &detachedSyslogNGs))
308+
if err := c.GetClient().List(ctx, &detachedSyslogNGs); err != nil {
309+
t.Logf("listing the detached syslog-ng configurations failed, retrying: %v", err)
310+
return false
311+
}
307312
if len(detachedSyslogNGs.Items) != 2 {
308-
// Add a new detached syslogng that is not going to be used
309-
common.RequireNoError(t, c.GetClient().Create(ctx, &excessSyslogNG))
313+
// Add a new detached syslogng that is not going to be used.
314+
// AlreadyExists is tolerated: the list above can be stale on a retry.
315+
if err := client.IgnoreAlreadyExists(c.GetClient().Create(ctx, &excessSyslogNG)); err != nil {
316+
t.Logf("creating the excess detached syslog-ng failed, retrying: %v", err)
317+
return false
318+
}
310319
t.Log("creating excess detached syslog-ng")
311320
return false
312321
} else if isValid := wait.CheckExcessSyslogNGStatus(t, c.GetClient(), ctx, &excessSyslogNG); !isValid && len(detachedSyslogNGs.Items) == 2 {
313322
t.Log("checking excess detached SyslogNG status")
314-
common.RequireNoError(t, c.GetClient().Get(ctx, utils.ObjectKeyFromObjectMeta(&excessSyslogNG), &excessSyslogNG))
323+
if err := c.GetClient().Get(ctx, utils.ObjectKeyFromObjectMeta(&excessSyslogNG), &excessSyslogNG); err != nil {
324+
t.Logf("reading the excess detached syslog-ng failed, retrying: %v", err)
325+
}
315326
return false
316327
}
317328

0 commit comments

Comments
 (0)