Skip to content

Commit 3f3f087

Browse files
nephomaniacclaude
andcommitted
Add read-only local e2e test mode with backplane support
Add Ginkgo labels (read-only/mutating) to all test specs, a wrapper script for running tests locally, and backplane impersonation support. - Label all tests as read-only or mutating for filtering - Add test/e2e/run-local.sh wrapper (auto-detects backplane, builds binary, filters by label) - Add test/e2e/lint-labels.sh to enforce labels on new tests - Support E2E_IMPERSONATE_USER for backplane cluster-admin impersonation - Support E2E_READ_ONLY to make Prometheus client non-fatal (metric tests skip when prom unavailable due to restricted RBAC) Pipeline behavior is unchanged: no env vars set, no label filter, all tests run with strict assertions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9a455bb commit 3f3f087

3 files changed

Lines changed: 222 additions & 26 deletions

File tree

test/e2e/configure_alertmanager_operator_tests.go

Lines changed: 55 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package osde2etests
77
import (
88
"context"
99
"fmt"
10+
"os"
1011
"strings"
1112
"time"
1213

@@ -53,26 +54,44 @@ var _ = Describe("Configure AlertManager Operator", Ordered, func() {
5354

5455
cfg, err := config.GetConfig()
5556
Expect(err).Should(BeNil(), "failed to get kubeconfig")
57+
58+
if impersonateUser := os.Getenv("E2E_IMPERSONATE_USER"); impersonateUser != "" {
59+
GinkgoLogr.Info("Impersonating user from E2E_IMPERSONATE_USER", "user", impersonateUser)
60+
cfg.Impersonate.UserName = impersonateUser
61+
}
62+
5663
client, err = resources.New(cfg)
5764
Expect(err).Should(BeNil(), "resources.New error")
5865

5966
dynamicClient, err = dynamic.NewForConfig(cfg)
6067
Expect(err).ShouldNot(HaveOccurred(), "failed to configure Dynamic client")
6168

62-
// Create openshift client locally for prometheus client setup
6369
k8s, err := openshift.New(GinkgoLogr)
64-
Expect(err).ShouldNot(HaveOccurred(), "unable to setup openshift client")
65-
66-
prom, err = prometheus.New(ctx, k8s)
67-
Expect(err).ShouldNot(HaveOccurred(), "unable to setup prometheus client")
70+
if err != nil {
71+
if os.Getenv("E2E_READ_ONLY") == "true" {
72+
GinkgoLogr.Info("OpenShift client unavailable (read-only mode) - metric tests will be skipped", "error", err)
73+
} else {
74+
Expect(err).ShouldNot(HaveOccurred(), "unable to setup openshift client")
75+
}
76+
} else {
77+
prom, err = prometheus.New(ctx, k8s)
78+
if err != nil {
79+
if os.Getenv("E2E_READ_ONLY") == "true" {
80+
GinkgoLogr.Info("Prometheus client unavailable (read-only mode) - metric tests will be skipped", "error", err)
81+
prom = nil
82+
} else {
83+
Expect(err).ShouldNot(HaveOccurred(), "unable to setup prometheus client")
84+
}
85+
}
86+
}
6887

6988
// Detect deployment method (OLM, PKO, both, or unknown)
7089
deploymentMethod = detectDeploymentMethod(ctx, dynamicClient, namespace, operatorName)
7190
GinkgoLogr.Info("Detected deployment method", "method", deploymentMethod)
7291
GinkgoWriter.Printf("Deployment method: %s\n", deploymentMethod)
7392
})
7493
// Validate deployment method and ensure only one method is active
75-
It("has valid deployment method and correct resources", func(ctx context.Context) {
94+
It("has valid deployment method and correct resources", Label("read-only"), func(ctx context.Context) {
7695
switch deploymentMethod {
7796
case "both":
7897
Fail(`CRITICAL: Both OLM and PKO deployment artifacts detected simultaneously!
@@ -140,19 +159,19 @@ POSSIBLE CAUSES:
140159
}
141160
})
142161

143-
It("service accounts exist", func(ctx context.Context) {
162+
It("service accounts exist", Label("read-only"), func(ctx context.Context) {
144163
for _, serviceAccount := range serviceAccounts {
145164
err := client.Get(ctx, serviceAccount, namespace, &v1.ServiceAccount{})
146165
Expect(err).ShouldNot(HaveOccurred(), "Service account %s not found", serviceAccount)
147166
}
148167
})
149168

150-
It("deployment exists", func(ctx context.Context) {
169+
It("deployment exists", Label("read-only"), func(ctx context.Context) {
151170
err := wait.For(conditions.New(client).DeploymentAvailable(operatorName, namespace))
152171
Expect(err).ShouldNot(HaveOccurred(), "Deployment %s not available", operatorName)
153172
})
154173

155-
It("roles exist", func(ctx context.Context) {
174+
It("roles exist", Label("read-only"), func(ctx context.Context) {
156175
if deploymentMethod == "olm" {
157176
var roles rbacv1.RoleList
158177
err := client.WithNamespace(namespace).List(ctx, &roles, resources.WithLabelSelector(labelSelector))
@@ -164,7 +183,7 @@ POSSIBLE CAUSES:
164183
}
165184
})
166185

167-
It("role bindings exist", func(ctx context.Context) {
186+
It("role bindings exist", Label("read-only"), func(ctx context.Context) {
168187
if deploymentMethod == "olm" {
169188
var roleBindings rbacv1.RoleBindingList
170189
err := client.WithNamespace(namespace).List(ctx, &roleBindings, resources.WithLabelSelector(labelSelector))
@@ -176,7 +195,7 @@ POSSIBLE CAUSES:
176195
}
177196
})
178197

179-
It("cluster roles exist", func(ctx context.Context) {
198+
It("cluster roles exist", Label("read-only"), func(ctx context.Context) {
180199
if deploymentMethod == "olm" {
181200
var clusterRoles rbacv1.ClusterRoleList
182201
err := client.WithNamespace(namespace).List(ctx, &clusterRoles, resources.WithLabelSelector(labelSelector))
@@ -190,7 +209,7 @@ POSSIBLE CAUSES:
190209
}
191210
})
192211

193-
It("cluster role bindings exist", func(ctx context.Context) {
212+
It("cluster role bindings exist", Label("read-only"), func(ctx context.Context) {
194213
if deploymentMethod == "olm" {
195214
var clusterRoleBindings rbacv1.ClusterRoleBindingList
196215
err := client.List(ctx, &clusterRoleBindings, resources.WithLabelSelector(labelSelector))
@@ -204,19 +223,19 @@ POSSIBLE CAUSES:
204223
}
205224
})
206225

207-
It("config map exists", func(ctx context.Context) {
226+
It("config map exists", Label("read-only"), func(ctx context.Context) {
208227
err := client.Get(ctx, configMapLockFile, namespace, &v1.ConfigMap{})
209228
Expect(err).ShouldNot(HaveOccurred(), "Failed to get config map %s", configMapLockFile)
210229
})
211230

212-
It("secrets exist", func(ctx context.Context) {
231+
It("secrets exist", Label("read-only"), func(ctx context.Context) {
213232
for _, secret := range secrets {
214233
err := client.Get(ctx, secret, namespace, &v1.Secret{})
215234
Expect(err).ShouldNot(HaveOccurred(), "Secret %s not found", secret)
216235
}
217236
})
218237

219-
It("alertmanager-main secret contains valid configuration", func(ctx context.Context) {
238+
It("alertmanager-main secret contains valid configuration", Label("read-only"), func(ctx context.Context) {
220239
// Get the alertmanager-main secret from openshift-monitoring
221240
var secret v1.Secret
222241
err := client.Get(ctx, "alertmanager-main", namespace, &secret)
@@ -233,7 +252,10 @@ POSSIBLE CAUSES:
233252
Expect(err).ShouldNot(HaveOccurred(), "alertmanager config validation failed: %v", err)
234253
})
235254

236-
It("validation metric exists and shows config is valid", func(ctx context.Context) {
255+
It("validation metric exists and shows config is valid", Label("read-only"), func(ctx context.Context) {
256+
if prom == nil {
257+
Skip("Prometheus client unavailable - skipping metric test")
258+
}
237259
// Query the alertmanager_config_validation_failed metric
238260
// Metric value: 0 = validation succeeded, 1 = validation failed
239261
query := `alertmanager_config_validation_failed{name="configure-alertmanager-operator"}`
@@ -266,7 +288,7 @@ POSSIBLE CAUSES:
266288
// These tests validate that cluster-scoped resources remain accessible
267289
// when the operator cache is scoped to openshift-monitoring namespace
268290

269-
It("can access ClusterVersion cluster-scoped resource", func(ctx context.Context) {
291+
It("can access ClusterVersion cluster-scoped resource", Label("read-only"), func(ctx context.Context) {
270292
ginkgo.By("Verifying operator can read ClusterVersion despite namespace-scoped cache")
271293

272294
// ClusterVersion is a cluster-scoped resource that the operator needs for:
@@ -394,7 +416,7 @@ ClusterVersion spec: %+v
394416
GinkgoLogr.Info("ClusterVersion access verified", "clusterID", clusterID)
395417
})
396418

397-
It("can access Proxy cluster-scoped resource", func(ctx context.Context) {
419+
It("can access Proxy cluster-scoped resource", Label("read-only"), func(ctx context.Context) {
398420
ginkgo.By("Verifying operator can read cluster Proxy configuration")
399421

400422
// Proxy is a cluster-scoped resource that the operator needs for:
@@ -523,7 +545,7 @@ NEXT STEPS:
523545
}
524546
})
525547

526-
It("can access Infrastructure cluster-scoped resource", func(ctx context.Context) {
548+
It("can access Infrastructure cluster-scoped resource", Label("read-only"), func(ctx context.Context) {
527549
ginkgo.By("Verifying operator can read Infrastructure and detect cluster type")
528550

529551
// Infrastructure is a cluster-scoped resource that the operator needs for:
@@ -723,7 +745,7 @@ If ConfigMap is intentionally missing, this warning can be ignored.
723745
}
724746
})
725747

726-
It("operator pod memory usage is within expected limits", func(ctx context.Context) {
748+
It("operator pod memory usage is within expected limits", Label("read-only"), func(ctx context.Context) {
727749
ginkgo.By("Verifying namespace scoping reduces operator memory footprint")
728750

729751
// With namespace scoping, the operator should use significantly less memory
@@ -875,7 +897,7 @@ Current Pod: %s (Age: %s)
875897
)
876898
})
877899

878-
It("routes alerts for management cluster namespaces when on MC cluster", func(ctx context.Context) {
900+
It("routes alerts for management cluster namespaces when on MC cluster", Label("read-only"), func(ctx context.Context) {
879901
ginkgo.By("Checking if this is a HyperShift Management Cluster")
880902

881903
// First, determine if this is a management cluster
@@ -1252,7 +1274,7 @@ Alerts from hosted control plane namespaces will be routed correctly.
12521274
}
12531275
})
12541276

1255-
Describe("Alertmanager configuration structure", func() {
1277+
Describe("Alertmanager configuration structure", Label("read-only"), func() {
12561278
It("has required config structure with global, route, and receivers", func(ctx context.Context) {
12571279
configBytes, err := utils.GetAlertmanagerConfigBytes(ctx, client, namespace)
12581280
Expect(err).ShouldNot(HaveOccurred(), "failed to get alertmanager config from secret")
@@ -1281,7 +1303,7 @@ Alerts from hosted control plane namespaces will be routed correctly.
12811303
})
12821304
})
12831305

1284-
Describe("PagerDuty receiver configuration", func() {
1306+
Describe("PagerDuty receiver configuration", Label("read-only"), func() {
12851307
It("contains pagerduty receiver when pd-secret exists", func(ctx context.Context) {
12861308
configBytes, err := utils.GetAlertmanagerConfigBytes(ctx, client, namespace)
12871309
Expect(err).ShouldNot(HaveOccurred(), "failed to get alertmanager config from secret")
@@ -1314,7 +1336,7 @@ Alerts from hosted control plane namespaces will be routed correctly.
13141336
})
13151337
})
13161338

1317-
Describe("Alert routing rules", func() {
1339+
Describe("Alert routing rules", Label("read-only"), func() {
13181340
It("contains null receiver for alert suppression", func(ctx context.Context) {
13191341
configBytes, err := utils.GetAlertmanagerConfigBytes(ctx, client, namespace)
13201342
Expect(err).ShouldNot(HaveOccurred(), "failed to get alertmanager config from secret")
@@ -1355,7 +1377,11 @@ Alerts from hosted control plane namespaces will be routed correctly.
13551377
// ========================================================================
13561378
// Reconciliation and secret lifecycle tests
13571379
// ========================================================================
1358-
Describe("Reconciliation and secret lifecycle", Ordered, func() {
1380+
// Some tests in this group are read-only (existence checks, metric queries) but are
1381+
// labeled mutating because they are ordered prerequisites for the mutating tests that
1382+
// follow, and the GoAlert Context uses a BeforeEach that creates/backs up secrets.
1383+
// Moving them out would require a larger refactor of the Ordered test structure.
1384+
Describe("Reconciliation and secret lifecycle", Label("mutating"), Ordered, func() {
13591385
const (
13601386
reconcileTimeout = 2 * time.Minute
13611387
reconcileInterval = 5 * time.Second
@@ -1612,8 +1638,11 @@ Alerts from hosted control plane namespaces will be routed correctly.
16121638
})
16131639

16141640
// --- Reconciliation metrics ---
1615-
Describe("Reconciliation metrics", func() {
1641+
Describe("Reconciliation metrics", Label("read-only"), func() {
16161642
It("secret existence metrics reflect cluster state", func(ctx context.Context) {
1643+
if prom == nil {
1644+
Skip("Prometheus client unavailable - skipping metric test")
1645+
}
16171646
type metricCheck struct {
16181647
query string
16191648
secretName string

test/e2e/lint-labels.sh

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/bin/bash
2+
#
3+
# Lint: Verify all e2e test specs have a read-only or mutating label.
4+
#
5+
# Every It() block must either:
6+
# 1. Have Label("read-only") or Label("mutating") directly, OR
7+
# 2. Be nested inside a Describe/Context that has the label
8+
#
9+
# This script checks that no top-level It() blocks are missing labels.
10+
# Nested It() blocks inherit from their parent Describe/Context.
11+
12+
set -euo pipefail
13+
14+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15+
TEST_FILE="$SCRIPT_DIR/configure_alertmanager_operator_tests.go"
16+
17+
ERRORS=0
18+
19+
# Find It( calls that are NOT inside a labeled Describe/Context and don't have their own label.
20+
# Top-level It blocks start with a single tab. Nested ones have more indentation.
21+
# We check single-tab It() calls for labels.
22+
while IFS= read -r line; do
23+
lineno=$(echo "$line" | cut -d: -f1)
24+
content=$(echo "$line" | cut -d: -f2-)
25+
if ! echo "$content" | grep -q 'Label('; then
26+
echo "ERROR: line $lineno: It() block missing Label(\"read-only\") or Label(\"mutating\"): $content"
27+
ERRORS=$((ERRORS + 1))
28+
fi
29+
done < <(grep -n '^ It(' "$TEST_FILE" | grep -v 'PIt(')
30+
31+
if [[ $ERRORS -gt 0 ]]; then
32+
echo ""
33+
echo "FAIL: $ERRORS It() block(s) missing required labels."
34+
echo "Every top-level It() must have Label(\"read-only\") or Label(\"mutating\")."
35+
echo "Tests nested inside a labeled Describe/Context inherit the parent label."
36+
exit 1
37+
fi
38+
39+
echo "OK: All top-level It() blocks have required labels."

0 commit comments

Comments
 (0)