diff --git a/test/e2e/framework/framework.go b/test/e2e/framework/framework.go index 5a387784a..89911e6fd 100644 --- a/test/e2e/framework/framework.go +++ b/test/e2e/framework/framework.go @@ -39,12 +39,14 @@ type Framework struct { RootCA *x509.CertPool MetricsClientCert *tls.Certificate OperatorNamespace string + ClusterVersion *configv1.ClusterVersion } // Setup finalizes the initilization of the Framework object by setting // parameters which are specific to OpenShift. func (f *Framework) Setup() error { clusterVersion := &configv1.ClusterVersion{} + if err := f.K8sClient.Get(context.Background(), client.ObjectKey{Name: "version"}, clusterVersion); err != nil { if meta.IsNoMatchError(err) { return nil @@ -53,6 +55,7 @@ func (f *Framework) Setup() error { return fmt.Errorf("failed to get clusterversion %w", err) } + f.ClusterVersion = clusterVersion f.IsOpenshiftCluster = true // Load the service CA operator's certificate authority. @@ -260,19 +263,58 @@ func (f *Framework) CleanUp(t *testing.T, cleanupFunc func()) { }) } +// TODO: remove ForceFailure — temporary helper to exercise DumpOnFailure logging. +func (f *Framework) ForceFailure(t *testing.T) { + t.Helper() + t.Error("forced failure to test debug dump output") +} + +// DebugFunc is a diagnostic function invoked when a test fails. +// Implementations should use t.Logf to emit relevant state. +type DebugFunc func(t *testing.T) + +// DumpOnFailure registers a t.Cleanup that runs the given debug functions +// when the test fails. It can be called multiple times to add more debug +// functions as resources become available during the test. +// +// Cleanups run in LIFO order, so call DumpOnFailure before registering +// resource deletions via CleanUp to ensure debug info is captured while +// the resources still exist. +func (f *Framework) DumpOnFailure(t *testing.T, fns ...DebugFunc) { + t.Helper() + t.Cleanup(func() { + if !t.Failed() { + return + } + for _, fn := range fns { + fn(t) + } + }) +} + +// DebugNamespace returns a DebugFunc that dumps deployments, pods, and events +// for the given namespaces. +func (f *Framework) DebugNamespace(namespaces ...string) DebugFunc { + return func(t *testing.T) { + t.Helper() + for _, ns := range namespaces { + t.Logf("--- Dumping debug info for namespace %s ---", ns) + f.DumpNamespaceDebug(t, ns) + } + } +} + // SkipIfClusterVersionBelow skips the test if the cluster version is below // minVersion. The minVersion string should be a semver-compatible version // (e.g. "4.19" or "v4.19"). func (f *Framework) SkipIfClusterVersionBelow(t *testing.T, minVersion string) { t.Helper() - cv := &configv1.ClusterVersion{} - err := f.K8sClient.Get(t.Context(), client.ObjectKey{Name: "version"}, cv) - if err != nil { - t.Fatalf("failed to determine cluster version: %v", err) + if f.ClusterVersion == nil { + t.Fatal("cluster version not available (non-OpenShift cluster?)") return } - actual := cv.Status.Desired.Version + actual := f.ClusterVersion.Status.Desired.Version if actual == "" { t.Fatal("cluster version is empty") return @@ -295,7 +337,7 @@ func (f *Framework) SkipIfClusterVersionBelow(t *testing.T, minVersion string) { } if semver.Compare(canonicalActual, canonicalMin) < 0 { - t.Skipf("Skipping: cluster version %s is below minimum required %s", cv.Status.Desired.Version, minVersion) + t.Skipf("Skipping: cluster version %s is below minimum required %s", f.ClusterVersion.Status.Desired.Version, minVersion) } } diff --git a/test/e2e/monitoring_stack_controller_test.go b/test/e2e/monitoring_stack_controller_test.go index 7ee1d74f3..618a6761a 100644 --- a/test/e2e/monitoring_stack_controller_test.go +++ b/test/e2e/monitoring_stack_controller_test.go @@ -48,6 +48,8 @@ func assertCRDExists(t *testing.T, crds ...string) { } func TestMonitoringStackController(t *testing.T) { + f.DumpOnFailure(t, f.DebugNamespace(e2eTestNamespace)) + f.ForceFailure(t) // TODO: remove — temporary, exercises debug dump err := stack.AddToScheme(scheme.Scheme) assert.NilError(t, err, "adding stack to scheme failed") assertCRDExists(t, diff --git a/test/e2e/observability_installer_test.go b/test/e2e/observability_installer_test.go index c679c4d01..b3aa716c0 100644 --- a/test/e2e/observability_installer_test.go +++ b/test/e2e/observability_installer_test.go @@ -45,6 +45,8 @@ func TestObservabilityInstallerController(t *testing.T) { } func testObservabilityInstallerTracing(t *testing.T) { + f.DumpOnFailure(t, f.DebugNamespace(f.OperatorNamespace, "tracing-observability")) + f.ForceFailure(t) // TODO: remove — temporary, exercises debug dump ctx := context.Background() // The ObservabilityInstaller installs operators via subscriptions, diff --git a/test/e2e/operator_metrics_test.go b/test/e2e/operator_metrics_test.go index ff7f0e31c..711d20d05 100644 --- a/test/e2e/operator_metrics_test.go +++ b/test/e2e/operator_metrics_test.go @@ -11,6 +11,8 @@ import ( ) func TestOperatorMetrics(t *testing.T) { + f.DumpOnFailure(t, f.DebugNamespace(f.OperatorNamespace)) + f.ForceFailure(t) // TODO: remove — temporary, exercises debug dump t.Run("operator exposes metrics", func(t *testing.T) { pod := f.GetOperatorPod(t) diff --git a/test/e2e/po_admission_webhook_test.go b/test/e2e/po_admission_webhook_test.go index d10d32b23..c089fb2a7 100644 --- a/test/e2e/po_admission_webhook_test.go +++ b/test/e2e/po_admission_webhook_test.go @@ -12,6 +12,8 @@ import ( ) func TestPrometheusRuleWebhook(t *testing.T) { + f.DumpOnFailure(t, f.DebugNamespace(e2eTestNamespace)) + f.ForceFailure(t) // TODO: remove — temporary, exercises debug dump assertCRDExists(t, "prometheusrules.monitoring.rhobs", ) diff --git a/test/e2e/prometheus_operator_test.go b/test/e2e/prometheus_operator_test.go index 4ecda84ec..ca9a90c66 100644 --- a/test/e2e/prometheus_operator_test.go +++ b/test/e2e/prometheus_operator_test.go @@ -29,6 +29,8 @@ type testCase struct { } func TestPrometheusOperatorForNonOwnedResources(t *testing.T) { + f.DumpOnFailure(t, f.DebugNamespace(e2eTestNamespace)) + f.ForceFailure(t) // TODO: remove — temporary, exercises debug dump resources := []client.Object{ newPrometheus(nil), newAlertmanager(nil), @@ -74,6 +76,8 @@ func TestPrometheusOperatorForNonOwnedResources(t *testing.T) { } func TestPrometheusOperatorForOwnedResources(t *testing.T) { + f.DumpOnFailure(t, f.DebugNamespace(e2eTestNamespace)) + f.ForceFailure(t) // TODO: remove — temporary, exercises debug dump resources := []client.Object{ newPrometheus(ownedResourceLabels), newAlertmanager(ownedResourceLabels), diff --git a/test/e2e/thanos_querier_controller_test.go b/test/e2e/thanos_querier_controller_test.go index 0863e034b..caf01c2a2 100644 --- a/test/e2e/thanos_querier_controller_test.go +++ b/test/e2e/thanos_querier_controller_test.go @@ -24,6 +24,8 @@ import ( ) func TestThanosQuerierController(t *testing.T) { + f.DumpOnFailure(t, f.DebugNamespace(e2eTestNamespace)) + f.ForceFailure(t) // TODO: remove — temporary, exercises debug dump assertCRDExists(t, "thanosqueriers.monitoring.rhobs") ts := []testCase{ diff --git a/test/e2e/uiplugin_cluster_health_analyzer_test.go b/test/e2e/uiplugin_cluster_health_analyzer_test.go index ff3639f48..bb06e48d3 100644 --- a/test/e2e/uiplugin_cluster_health_analyzer_test.go +++ b/test/e2e/uiplugin_cluster_health_analyzer_test.go @@ -32,21 +32,21 @@ func clusterHealthAnalyzer(t *testing.T) { err := monv1.AddToScheme(f.K8sClient.Scheme()) assert.NilError(t, err, "failed to add monv1 to scheme") + f.DumpOnFailure(t, f.DebugNamespace(uiPluginInstallNS)) + plugin := resetMonitoringUIPlugin(t) err = f.K8sClient.Create(t.Context(), plugin) assert.NilError(t, err, "failed to create monitoring UIPlugin") - t.Cleanup(func() { - if t.Failed() { - dumpClusterHealthAnalyzerDebug(t, plugin.Name) - } - }) + f.DumpOnFailure(t, dumpUIPluginDebug(plugin.Name)) + f.ForceFailure(t) // TODO: remove — temporary, exercises debug dump t.Log("Waiting for health-analyzer deployment to become ready...") haDeployment := appsv1.Deployment{} f.GetResourceWithRetry(t, healthAnalyzerDeploymentName, uiPluginInstallNS, &haDeployment) f.AssertDeploymentReady(healthAnalyzerDeploymentName, uiPluginInstallNS, framework.WithTimeout(5*time.Minute))(t) + // Use a unique suffix so re-runs don't conflict with leftover rules from prior executions. suffix := strconv.FormatInt(time.Now().UnixNano()%100000, 10) ruleName := "e2e-health-analyzer-" + suffix alertName := "E2EHealthAnalyzer" + suffix @@ -179,15 +179,16 @@ func newAlwaysFiringRule(t *testing.T, ruleName, alertName string) *monv1.Promet return rule } -func dumpClusterHealthAnalyzerDebug(t *testing.T, pluginName string) { - t.Helper() - ctx := context.WithoutCancel(t.Context()) +func dumpUIPluginDebug(pluginName string) framework.DebugFunc { + return func(t *testing.T) { + t.Helper() + ctx := context.WithoutCancel(t.Context()) - // UIPlugin-specific diagnostics - var plugin uiv1.UIPlugin - if err := f.K8sClient.Get(ctx, client.ObjectKey{Name: pluginName}, &plugin); err != nil { - t.Logf("Failed to get UIPlugin %q: %v", pluginName, err) - } else { + var plugin uiv1.UIPlugin + if err := f.K8sClient.Get(ctx, client.ObjectKey{Name: pluginName}, &plugin); err != nil { + t.Logf("Failed to get UIPlugin %q: %v", pluginName, err) + return + } t.Logf("UIPlugin %q generation=%d, resourceVersion=%s", pluginName, plugin.Generation, plugin.ResourceVersion) t.Logf("UIPlugin spec.type=%s", plugin.Spec.Type) if plugin.Spec.Monitoring != nil { @@ -204,18 +205,15 @@ func dumpClusterHealthAnalyzerDebug(t *testing.T, pluginName string) { for _, c := range plugin.Status.Conditions { t.Logf("UIPlugin condition: type=%s status=%s reason=%s message=%s", c.Type, c.Status, c.Reason, c.Message) } - } - var plugins uiv1.UIPluginList - if err := f.K8sClient.List(ctx, &plugins); err != nil { - t.Logf("Failed to list UIPlugins: %v", err) - } else { - t.Logf("Total UIPlugins in cluster: %d", len(plugins.Items)) - for _, p := range plugins.Items { - t.Logf(" UIPlugin: name=%s type=%s conditions=%d", p.Name, p.Spec.Type, len(p.Status.Conditions)) + var plugins uiv1.UIPluginList + if err := f.K8sClient.List(ctx, &plugins); err != nil { + t.Logf("Failed to list UIPlugins: %v", err) + } else { + t.Logf("Total UIPlugins in cluster: %d", len(plugins.Items)) + for _, p := range plugins.Items { + t.Logf(" UIPlugin: name=%s type=%s conditions=%d", p.Name, p.Spec.Type, len(p.Status.Conditions)) + } } } - - // Generic namespace diagnostics (deployments, pods, events) - f.DumpNamespaceDebug(t, uiPluginInstallNS) } diff --git a/test/e2e/uiplugin_test.go b/test/e2e/uiplugin_test.go index 6cb27abca..d4ef2069c 100644 --- a/test/e2e/uiplugin_test.go +++ b/test/e2e/uiplugin_test.go @@ -46,6 +46,8 @@ func TestUIPlugin(t *testing.T) { } func dashboardsUIPlugin(t *testing.T) { + f.DumpOnFailure(t, f.DebugNamespace(uiPluginInstallNS)) + f.ForceFailure(t) // TODO: remove — temporary, exercises debug dump db := newDashboardsUIPlugin(t) err := f.K8sClient.Create(context.Background(), db) assert.NilError(t, err, "failed to create a dashboards UIPlugin")