From f0a6aeedff796c09c9ce760e13779aed3338faba Mon Sep 17 00:00:00 2001 From: Chris Collins Date: Mon, 22 Dec 2025 15:18:53 -1000 Subject: [PATCH 1/3] Add Alertmanager config validation to prevent invalid configurations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change adds pre-write validation of Alertmanager configurations to prevent invalid configs from being deployed, which could cause Alertmanager to crash on restart. Changes: - Use Prometheus Alertmanager's official config.Load() for validation - Block invalid config writes, preserving last-known-good configuration - Create Kubernetes Events on validation failures with actionable guidance - Expose alertmanager_config_validation_status metric (0=valid, 1=invalid) - Add unit tests for validation logic and event creation - Add e2e tests for config validation and metric exposure - Update README with validation documentation This addresses production incident ITN-2025-00331 where an invalid label name "route-to-cad" (containing hyphens) caused Alertmanager to crash, resulting in a 6-hour monitoring outage. Regression test added: Test_validateAlertManagerConfig_InvalidLabelNameWithHyphens 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 68 +++++- controllers/secret_controller.go | 82 ++++++- controllers/secret_controller_test.go | 213 ++++++++++++++++++ go.mod | 70 ++++-- go.sum | 142 ++++++++---- pkg/metrics/metrics.go | 15 ++ .../configure_alertmanager_operator_tests.go | 60 ++++- 7 files changed, 579 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index a87d5382..79429e1e 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,51 @@ The Secret Controller watches over the resources in the table below. Changes to | ConfigMap | `openshift-monitoring/managed-namespaces` | Defines a list of OpenShift "managed" namespaces. The operator will route alerts originating from these namespaces to PagerDuty and/or GoAlert. | | ConfigMap | `openshift-monitoring/ocp-namespaces` | Defines a list of OpenShift Container Platform namespaces. The operator will route alerts originating from these namespaces to PagerDuty and/or GoAlert. | +## Alertmanager Config Validation + +The operator validates all Alertmanager configurations before writing them to the `alertmanager-main` secret. This prevents invalid configurations from being deployed, which could cause Alertmanager to fail on restart. + +### How Validation Works + +1. **Pre-write Validation**: Before writing any configuration to `alertmanager-main`, the operator validates it using Prometheus Alertmanager's official `config.Load()` function - the exact same validation that Alertmanager performs on startup. + +2. **Validation Failure Handling**: If validation fails: + - The invalid config is **not written** to the secret (preserving the last-known-good configuration) + - A Kubernetes Event is created in `openshift-monitoring` namespace with reason `AlertmanagerConfigValidationFailure` + - The `alertmanager_config_validation_status` metric is set to `1` (invalid) + - The reconcile loop returns an error, triggering automatic retry + +3. **Validation Success**: If validation succeeds: + - The config is written to `alertmanager-main` + - The `alertmanager_config_validation_status` metric is set to `0` (valid) + +### Monitoring Validation Status + +**Via Prometheus Metric**: +```promql +alertmanager_config_validation_status{name="configure-alertmanager-operator"} +``` +- Value `0` = configuration is valid +- Value `1` = configuration validation failed + +**Via Kubernetes Events**: +```bash +oc get events -n openshift-monitoring --field-selector reason=AlertmanagerConfigValidationFailure +``` + +Failed validation events include: +- The specific validation error from Alertmanager +- Guidance to check source secrets and configmaps for invalid data +- A reference to operator logs for detailed debugging + +### Common Validation Failures + +- **Invalid label names**: Prometheus label names must match `[a-zA-Z_][a-zA-Z0-9_]*` (no hyphens allowed) +- **Duplicate receiver names**: Each receiver must have a unique name +- **Missing required fields**: Route and at least one receiver are required +- **Invalid duration formats**: Must use valid Go duration strings (e.g., "5m", "1h") +- **Invalid regex patterns**: MatchRE patterns must be valid regular expressions + ## Cluster Readiness To avoid alert noise while a cluster is in the early stages of being installed and configured, this operator waits to configure Pager Duty -- effectively silencing alerts -- until a predetermined set of health checks, performed by [osd-cluster-ready](https://github.com/openshift/osd-cluster-ready/), has completed. @@ -49,17 +94,18 @@ This determination is made through the presence of a completed `Job` named `osd- ## Metrics The Configure Alertmanager Operator exposes the following Prometheus metrics: -| Metric name | Purpose | -|---------------------------------------|-------------------------------------------------------------------------------------------------------| -| `ga_secret_exists` | indicates that a Secret named `goalert-secret` exists in the `openshift-monitoring` namespace. | -| `pd_secret_exists` | indicates that a Secret named `pd-secret` exists in the `openshift-monitoring` namespace. | -| `dms_secret_exists` | indicates that a Secret named `dms-secret` exists in the `openshift-monitoring` namespace. | -| `am_secret_exists` | indicates that a Secret named `alertmanager-main` exists in the `openshift-monitoring` namespace. | -| `managed_namespaces_configmap_exists` | indicates that a ConfigMap named `managed-namespaces` exists in the `openshift-monitoring` namespace. | -| `ocp_namespaces_configmap_exists` | indicates that a ConfigMap named `ocp-namespaces` exists in the `openshift-monitoring` namespace. | -| `am_secret_contains_ga` | indicates the GoAlert receiver is present in alertmanager.yaml. | -| `am_secret_contains_pd` | indicates the Pager Duty receiver is present in alertmanager.yaml. | -| `am_secret_contains_dms` | indicates the Dead Man's Snitch receiver is present in alertmanager.yaml. | +| Metric name | Purpose | +|------------------------------------------------|-------------------------------------------------------------------------------------------------------| +| `ga_secret_exists` | indicates that a Secret named `goalert-secret` exists in the `openshift-monitoring` namespace. | +| `pd_secret_exists` | indicates that a Secret named `pd-secret` exists in the `openshift-monitoring` namespace. | +| `dms_secret_exists` | indicates that a Secret named `dms-secret` exists in the `openshift-monitoring` namespace. | +| `am_secret_exists` | indicates that a Secret named `alertmanager-main` exists in the `openshift-monitoring` namespace. | +| `managed_namespaces_configmap_exists` | indicates that a ConfigMap named `managed-namespaces` exists in the `openshift-monitoring` namespace. | +| `ocp_namespaces_configmap_exists` | indicates that a ConfigMap named `ocp-namespaces` exists in the `openshift-monitoring` namespace. | +| `am_secret_contains_ga` | indicates the GoAlert receiver is present in alertmanager.yaml. | +| `am_secret_contains_pd` | indicates the Pager Duty receiver is present in alertmanager.yaml. | +| `am_secret_contains_dms` | indicates the Dead Man's Snitch receiver is present in alertmanager.yaml. | +| `alertmanager_config_validation_status` | indicates Alertmanager config validation status: `0` = valid, `1` = invalid. | The operator creates a `Service` and `ServiceMonitor` named `configure-alertmanager-operator` to expose these metrics to Prometheus. diff --git a/controllers/secret_controller.go b/controllers/secret_controller.go index b2ed82fb..40b5f3d5 100644 --- a/controllers/secret_controller.go +++ b/controllers/secret_controller.go @@ -36,6 +36,7 @@ import ( "github.com/go-logr/logr" configv1 "github.com/openshift/api/config/v1" + amconfig "github.com/prometheus/alertmanager/config" "github.com/openshift/configure-alertmanager-operator/config" "github.com/openshift/configure-alertmanager-operator/pkg/metrics" @@ -272,7 +273,10 @@ func (r *SecretReconciler) Reconcile(ctx context.Context, request ctrl.Request) osdNamespaces) // write the alertmanager Config - writeAlertManagerConfig(r, reqLogger, alertmanagerconfig) + if err := writeAlertManagerConfig(r, reqLogger, alertmanagerconfig); err != nil { + reqLogger.Error(err, "Failed to write alertmanager config") + return reconcile.Result{}, err + } // Update metrics after all reconcile operations are complete. metrics.UpdateSecretsMetrics(secretList, alertmanagerconfig) @@ -1259,11 +1263,78 @@ func readSecretKey(r *SecretReconciler, secretName string, secretNamespace strin return string(secret.Data[fieldName]) } +// validateAlertManagerConfig validates the alertmanager config using Alertmanager's official validation +// This ensures the config will be accepted by Alertmanager on startup/reload +func validateAlertManagerConfig(reqLogger logr.Logger, cfg *alertmanager.Config) error { + // Marshal our config to YAML + cfgBytes, marshalerr := yaml.Marshal(cfg) + if marshalerr != nil { + return fmt.Errorf("failed to marshal alertmanager config for validation: %w", marshalerr) + } + + // Use Alertmanager's official config.Load() to validate + // This is the same validation Alertmanager performs on startup + _, err := amconfig.Load(string(cfgBytes)) + if err != nil { + return fmt.Errorf("alertmanager config validation failed: %w", err) + } + + reqLogger.Info("INFO: Alertmanager config validation passed") + return nil +} + +// recordConfigValidationEvent creates a Kubernetes event to alert SRE when Alertmanager config validation fails +func (r *SecretReconciler) recordConfigValidationEvent(err error) { + event := &corev1.Event{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("alertmanager-config-validation-failure-%d", time.Now().Unix()), + Namespace: "openshift-monitoring", + }, + InvolvedObject: corev1.ObjectReference{ + Kind: "Secret", + Namespace: "openshift-monitoring", + Name: secretNameAlertmanager, + }, + Reason: "AlertmanagerConfigValidationFailure", + Message: fmt.Sprintf("CRITICAL: Failed to validate Alertmanager configuration - config will not be written to prevent Alertmanager from failing on restart. Error: %v. Action required: Check source configmaps and secrets for invalid data. Review operator logs for details.", err), + Type: corev1.EventTypeWarning, + EventTime: metav1.MicroTime{ + Time: time.Now(), + }, + FirstTimestamp: metav1.Time{ + Time: time.Now(), + }, + LastTimestamp: metav1.Time{ + Time: time.Now(), + }, + Count: 1, + } + + // Best effort event creation - don't fail reconciliation if event creation fails + if createErr := r.Client.Create(context.TODO(), event); createErr != nil { + log.Error(createErr, "Failed to create Alertmanager config validation failure event") + } +} + // writeAlertManagerConfig writes the updated alertmanager config to the `alertmanager-main` secret in namespace `openshift-monitoring`. -func writeAlertManagerConfig(r *SecretReconciler, reqLogger logr.Logger, amconfig *alertmanager.Config) { +// It validates the config before writing to prevent Alertmanager from failing on restart. +func writeAlertManagerConfig(r *SecretReconciler, reqLogger logr.Logger, amconfig *alertmanager.Config) error { + // Validate the config before writing + if err := validateAlertManagerConfig(reqLogger, amconfig); err != nil { + reqLogger.Error(err, "ERROR: Alertmanager config validation failed - will not write to secret") + // Record Kubernetes event to alert SRE + r.recordConfigValidationEvent(err) + // Update metric to indicate validation failure + metrics.UpdateAlertmanagerConfigValidationMetric(false) + return fmt.Errorf("alertmanager config validation failed, config not written: %w", err) + } + + // Config is valid, proceed with marshaling and writing amconfigbyte, marshalerr := yaml.Marshal(amconfig) if marshalerr != nil { reqLogger.Error(marshalerr, "ERROR: failed to marshal Alertmanager config") + metrics.UpdateAlertmanagerConfigValidationMetric(false) + return fmt.Errorf("failed to marshal alertmanager config: %w", marshalerr) } // This is commented out because it prints secrets, but it might be useful for debugging when running locally. //reqLogger.Info("DEBUG: Marshalled Alertmanager config:", string(amconfigbyte)) @@ -1290,7 +1361,12 @@ func writeAlertManagerConfig(r *SecretReconciler, reqLogger logr.Logger, amconfi if err != nil { reqLogger.Error(err, "ERROR: Could not write secret alertmanger-main", "namespace", secret.Namespace) - return + metrics.UpdateAlertmanagerConfigValidationMetric(false) + return fmt.Errorf("failed to write alertmanager-main secret: %w", err) } + reqLogger.Info("INFO: Secret alertmanager-main successfully updated") + // Update metric to indicate validation and write success + metrics.UpdateAlertmanagerConfigValidationMetric(true) + return nil } diff --git a/controllers/secret_controller_test.go b/controllers/secret_controller_test.go index bbec844d..de5ead60 100644 --- a/controllers/secret_controller_test.go +++ b/controllers/secret_controller_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "reflect" + "strings" "testing" configv1 "github.com/openshift/api/config/v1" @@ -1794,3 +1795,215 @@ func Test_SecretReconciler_Readiness(t *testing.T) { assertEquals(t, configExpected.String(), configActual.String(), tt.name) } } + +// Test_validateAlertManagerConfig_ValidConfig verifies that a valid Alertmanager config passes validation +func Test_validateAlertManagerConfig_ValidConfig(t *testing.T) { + validConfig := &alertmanager.Config{ + Global: &alertmanager.GlobalConfig{ + ResolveTimeout: "5m", + PagerdutyURL: "https://events.pagerduty.com/v2/enqueue", + }, + Route: &alertmanager.Route{ + Receiver: "null", + GroupByStr: []string{"alertname"}, + GroupWait: "30s", + GroupInterval: "5m", + RepeatInterval: "12h", + }, + Receivers: []*alertmanager.Receiver{ + {Name: "null"}, + }, + Templates: []string{}, + } + + err := validateAlertManagerConfig(reqLogger, validConfig) + if err != nil { + t.Fatalf("Expected validation to pass for valid config, got error: %v", err) + } +} + +// Test_validateAlertManagerConfig_InvalidConfig verifies that an invalid config fails validation +func Test_validateAlertManagerConfig_InvalidConfig(t *testing.T) { + // Config with invalid resolve_timeout format + invalidConfig := &alertmanager.Config{ + Global: &alertmanager.GlobalConfig{ + ResolveTimeout: "invalid-time-format", + }, + Route: &alertmanager.Route{ + Receiver: "null", + }, + Receivers: []*alertmanager.Receiver{ + {Name: "null"}, + }, + } + + err := validateAlertManagerConfig(reqLogger, invalidConfig) + if err == nil { + t.Fatal("Expected validation to fail for invalid resolve_timeout, but it passed") + } +} + +// Test_validateAlertManagerConfig_InvalidLabelNameWithHyphens verifies that label names with hyphens fail validation +// Regression test for ITN-2025-00331: CAMO generated invalid label "route-to-cad" causing AlertManager failures +func Test_validateAlertManagerConfig_InvalidLabelNameWithHyphens(t *testing.T) { + invalidConfig := &alertmanager.Config{ + Global: &alertmanager.GlobalConfig{ + ResolveTimeout: "5m", + PagerdutyURL: "https://events.pagerduty.com/v2/enqueue", + }, + Route: &alertmanager.Route{ + Receiver: "null", + Routes: []*alertmanager.Route{ + { + // Invalid label name with hyphens (Prometheus labels must match [a-zA-Z_][a-zA-Z0-9_]*) + Match: map[string]string{ + "route-to-cad": "true", + }, + Receiver: "cad-pagerduty", + }, + }, + }, + Receivers: []*alertmanager.Receiver{ + {Name: "null"}, + {Name: "cad-pagerduty"}, + }, + Templates: []string{}, + } + + err := validateAlertManagerConfig(reqLogger, invalidConfig) + if err == nil { + t.Fatal("Expected validation to fail for label name 'route-to-cad' (contains hyphens), but it passed") + } + + // Verify error message indicates invalid label name + errStr := err.Error() + if !strings.Contains(errStr, "invalid label name") && !strings.Contains(errStr, "route-to-cad") { + t.Fatalf("Expected error about invalid label name 'route-to-cad', got: %v", err) + } +} + +// Test_validateAlertManagerConfig_DuplicateReceiverNames verifies that duplicate receiver names fail validation +func Test_validateAlertManagerConfig_DuplicateReceiverNames(t *testing.T) { + invalidConfig := &alertmanager.Config{ + Global: &alertmanager.GlobalConfig{ + ResolveTimeout: "5m", + }, + Route: &alertmanager.Route{ + Receiver: "duplicate-name", + }, + Receivers: []*alertmanager.Receiver{ + {Name: "duplicate-name"}, + {Name: "duplicate-name"}, // Duplicate receiver name + }, + Templates: []string{}, + } + + err := validateAlertManagerConfig(reqLogger, invalidConfig) + if err == nil { + t.Fatal("Expected validation to fail for duplicate receiver names, but it passed") + } + + // Verify error message indicates duplicate receiver name + errStr := err.Error() + if !strings.Contains(errStr, "not unique") && !strings.Contains(errStr, "duplicate") { + t.Fatalf("Expected error about duplicate receiver name, got: %v", err) + } +} + +// Test_validateAlertManagerConfig_MissingRoute verifies that a config with no route fails validation +func Test_validateAlertManagerConfig_MissingRoute(t *testing.T) { + invalidConfig := &alertmanager.Config{ + Global: &alertmanager.GlobalConfig{ + ResolveTimeout: "5m", + }, + Route: nil, // Missing route + Receivers: []*alertmanager.Receiver{ + {Name: "null"}, + }, + Templates: []string{}, + } + + err := validateAlertManagerConfig(reqLogger, invalidConfig) + if err == nil { + t.Fatal("Expected validation to fail for missing route, but it passed") + } + + // Verify error message indicates missing route + errStr := err.Error() + if !strings.Contains(errStr, "no route") { + t.Fatalf("Expected error about missing route, got: %v", err) + } +} + +// Test_recordConfigValidationEvent verifies that a Kubernetes event is created when validation fails +func Test_recordConfigValidationEvent(t *testing.T) { + // Create test reconciler with fake client + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockReadiness := readiness.NewMockInterface(ctrl) + reconciler := createReconciler(t, mockReadiness) + createNamespace(reconciler, t) + + // Create a validation error + validationErr := fmt.Errorf("alertmanager config validation failed: invalid label name \"route-to-cad\"") + + // Record the event + reconciler.recordConfigValidationEvent(validationErr) + + // Verify event was created + eventList := &corev1.EventList{} + err := reconciler.Client.List(context.TODO(), eventList, &client.ListOptions{ + Namespace: "openshift-monitoring", + }) + if err != nil { + t.Fatalf("Failed to list events: %v", err) + } + + if len(eventList.Items) == 0 { + t.Fatal("Expected at least one event to be created, got none") + } + + // Find our validation event + var validationEvent *corev1.Event + for i := range eventList.Items { + if eventList.Items[i].Reason == "AlertmanagerConfigValidationFailure" { + validationEvent = &eventList.Items[i] + break + } + } + + if validationEvent == nil { + t.Fatal("Expected to find AlertmanagerConfigValidationFailure event, but didn't") + } + + // Verify event fields + if validationEvent.Type != corev1.EventTypeWarning { + t.Errorf("Expected event type %s, got %s", corev1.EventTypeWarning, validationEvent.Type) + } + + if validationEvent.InvolvedObject.Kind != "Secret" { + t.Errorf("Expected InvolvedObject.Kind 'Secret', got '%s'", validationEvent.InvolvedObject.Kind) + } + + if validationEvent.InvolvedObject.Name != secretNameAlertmanager { + t.Errorf("Expected InvolvedObject.Name '%s', got '%s'", secretNameAlertmanager, validationEvent.InvolvedObject.Name) + } + + if validationEvent.InvolvedObject.Namespace != "openshift-monitoring" { + t.Errorf("Expected InvolvedObject.Namespace 'openshift-monitoring', got '%s'", validationEvent.InvolvedObject.Namespace) + } + + // Verify error message is included in event message + if !strings.Contains(validationEvent.Message, validationErr.Error()) { + t.Errorf("Expected event message to contain validation error '%s', got: %s", validationErr.Error(), validationEvent.Message) + } + + // Verify event message contains actionable guidance + if !strings.Contains(validationEvent.Message, "CRITICAL") { + t.Error("Expected event message to contain 'CRITICAL'") + } + + if !strings.Contains(validationEvent.Message, "Check source configmaps and secrets") { + t.Error("Expected event message to contain guidance about checking source configmaps and secrets") + } +} diff --git a/go.mod b/go.mod index e0fd1a7e..c09b33c5 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/openshift/configure-alertmanager-operator -go 1.23.0 +go 1.24.0 toolchain go1.24.1 @@ -12,8 +12,8 @@ require ( github.com/openshift/osde2e-common v0.0.0-20250812151315-081151385798 github.com/operator-framework/operator-lib v0.18.0 github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.73.1 - github.com/prometheus/client_golang v1.23.0 - github.com/prometheus/common v0.65.0 + github.com/prometheus/client_golang v1.23.2 + github.com/prometheus/common v0.67.4 go.uber.org/mock v0.4.0 gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.32.8 @@ -23,9 +23,26 @@ require ( sigs.k8s.io/e2e-framework v0.6.0 ) -require github.com/openshift/api v0.0.0-20240522145529-93d6bda14341 +require ( + github.com/openshift/api v0.0.0-20240522145529-93d6bda14341 + github.com/prometheus/alertmanager v0.30.0 +) require ( + github.com/aws/aws-sdk-go-v2 v1.40.1 // indirect + github.com/aws/aws-sdk-go-v2/config v1.32.3 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.3 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.15 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.15 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.15 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.15 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.3 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.6 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.11 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.3 // indirect + github.com/aws/smithy-go v1.24.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -35,11 +52,23 @@ require ( github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-logr/zapr v1.3.0 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-openapi/jsonpointer v0.22.1 // indirect + github.com/go-openapi/jsonreference v0.21.3 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect @@ -48,32 +77,37 @@ require ( github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect - github.com/josharian/intern v1.0.0 // indirect + github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/mailru/easyjson v0.7.7 // indirect github.com/moby/spdystream v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/openshift/client-go v0.0.0-20240510131258-f646d5f29250 // indirect + github.com/openshift/library-go v0.0.0-20240517135010-e93e442c2b18 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/sigv4 v0.3.0 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/x448/float16 v0.8.4 // indirect go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/net v0.41.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.15.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/term v0.32.0 // indirect - golang.org/x/text v0.26.0 // indirect - golang.org/x/time v0.7.0 // indirect - golang.org/x/tools v0.33.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/oauth2 v0.32.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/time v0.13.0 // indirect + golang.org/x/tools v0.39.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/protobuf v1.36.6 // indirect + google.golang.org/protobuf v1.36.10 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index def46dba..b10d9eda 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,33 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/aws/aws-sdk-go-v2 v1.40.1 h1:difXb4maDZkRH0x//Qkwcfpdg1XQVXEAEs2DdXldFFc= +github.com/aws/aws-sdk-go-v2 v1.40.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2/config v1.32.3 h1:cpz7H2uMNTDa0h/5CYL5dLUEzPSLo2g0NkbxTRJtSSU= +github.com/aws/aws-sdk-go-v2/config v1.32.3/go.mod h1:srtPKaJJe3McW6T/+GMBZyIPc+SeqJsNPJsd4mOYZ6s= +github.com/aws/aws-sdk-go-v2/credentials v1.19.3 h1:01Ym72hK43hjwDeJUfi1l2oYLXBAOR8gNSZNmXmvuas= +github.com/aws/aws-sdk-go-v2/credentials v1.19.3/go.mod h1:55nWF/Sr9Zvls0bGnWkRxUdhzKqj9uRNlPvgV1vgxKc= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.15 h1:utxLraaifrSBkeyII9mIbVwXXWrZdlPO7FIKmyLCEcY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.15/go.mod h1:hW6zjYUDQwfz3icf4g2O41PHi77u10oAzJ84iSzR/lo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.15 h1:Y5YXgygXwDI5P4RkteB5yF7v35neH7LfJKBG+hzIons= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.15/go.mod h1:K+/1EpG42dFSY7CBj+Fruzm8PsCGWTXJ3jdeJ659oGQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.15 h1:AvltKnW9ewxX2hFmQS0FyJH93aSvJVUEFvXfU+HWtSE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.15/go.mod h1:3I4oCdZdmgrREhU74qS1dK9yZ62yumob+58AbFR4cQA= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.15 h1:3/u/4yZOffg5jdNk1sDpOQ4Y+R6Xbh+GzpDrSZjuy3U= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.15/go.mod h1:4Zkjq0FKjE78NKjabuM4tRXKFzUJWXgP0ItEZK8l7JU= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.3 h1:d/6xOGIllc/XW1lzG9a4AUBMmpLA9PXcQnVPTuHHcik= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.3/go.mod h1:fQ7E7Qj9GiW8y0ClD7cUJk3Bz5Iw8wZkWDHsTe8vDKs= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.6 h1:8sTTiw+9yuNXcfWeqKF2x01GqCF49CpP4Z9nKrrk/ts= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.6/go.mod h1:8WYg+Y40Sn3X2hioaaWAAIngndR8n1XFdRPPX+7QBaM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.11 h1:E+KqWoVsSrj1tJ6I/fjDIu5xoS2Zacuu1zT+H7KtiIk= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.11/go.mod h1:qyWHz+4lvkXcr3+PoGlGHEI+3DLLiU6/GdrFfMaAhB0= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.3 h1:tzMkjh0yTChUqJDgGkcDdxvZDSrJ/WB6R6ymI5ehqJI= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.3/go.mod h1:T270C0R5sZNLbWUe8ueiAF42XSZxxPocTaGSgs5c/60= +github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= +github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= @@ -24,16 +52,46 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= +github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= +github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc= +github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= @@ -54,8 +112,6 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -70,8 +126,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -91,6 +145,10 @@ github.com/onsi/gomega v1.38.0 h1:c/WX+w8SLAinvuKKQFh77WEucCnPk4j2OTUr7lt7BeY= github.com/onsi/gomega v1.38.0/go.mod h1:OcXcwId0b9QsE7Y49u+BTrL4IdKOBOKnD6VQNTJEB6o= github.com/openshift/api v0.0.0-20240522145529-93d6bda14341 h1:JQpzgk+p24rkgNbNsrNR0yLm63WTKapuT60INU5BqT8= github.com/openshift/api v0.0.0-20240522145529-93d6bda14341/go.mod h1:qNtV0315F+f8ld52TLtPvrfivZpdimOzTi3kn9IVbtU= +github.com/openshift/client-go v0.0.0-20240510131258-f646d5f29250 h1:WQ0e89xebsNeChdYw8QrxggYkFtEQOaLBEKJDkMmcUk= +github.com/openshift/client-go v0.0.0-20240510131258-f646d5f29250/go.mod h1:tjGjTE59N1+5W0YE4Wqf0zJpFLIY4d+EpMihDKOn1oA= +github.com/openshift/library-go v0.0.0-20240517135010-e93e442c2b18 h1:aUFgWf2nsvxzHcyYFpY8OpVncJgZWc4bZsy5vncjgP0= +github.com/openshift/library-go v0.0.0-20240517135010-e93e442c2b18/go.mod h1:lFwyRj0XjUf25Da3Q00y+KuaxCWTJ6YzYPDX1+96nco= github.com/openshift/osde2e-common v0.0.0-20250812151315-081151385798 h1:3dlO64uGD5v3Kkjfjf91wOi5ATnLAYt4VMWJTXzQPVw= github.com/openshift/osde2e-common v0.0.0-20250812151315-081151385798/go.mod h1:lBnywjHwKJt0fGhUvEkYBlTsaeZch8tJRWMVXkPBFkE= github.com/operator-framework/operator-lib v0.18.0 h1:6OaWemt/CuyrjFMkLyk4O8Vj4CPHxt/m1DMuMAmPwXo= @@ -104,14 +162,18 @@ github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4 github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.73.1 h1:VvIIWSQbvGaDgIeTrOintF/czS6UpLB086EUslAm7aI= github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.73.1/go.mod h1:yJ3CawR/A5qEYFEeCOUVYLTwYxmacfHQhJS+b/2QiaM= -github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= -github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= +github.com/prometheus/alertmanager v0.30.0 h1:E4dnxSFXK8V2Bb8iqudlisTmaIrF3hRJSWnliG08tBM= +github.com/prometheus/alertmanager v0.30.0/go.mod h1:93PBumcTLr/gNtNtM0m7BcCffbvYP5bKuLBWiOnISaA= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= -github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/sigv4 v0.3.0 h1:QIG7nTbu0JTnNidGI1Uwl5AGVIChWUACxn2B/BQ1kms= +github.com/prometheus/sigv4 v0.3.0/go.mod h1:fKtFYDus2M43CWKMNtGvFNHGXnAJJEGZbiYCmVp/F8I= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= @@ -120,18 +182,18 @@ github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/vladimirvivien/gexe v0.4.1 h1:W9gWkp8vSPjDoXDu04Yp4KljpVMaSt8IQuHswLDd5LY= github.com/vladimirvivien/gexe v0.4.1/go.mod h1:3gjgTqE2c0VyHnU5UOIwk7gyNzZDGulPb/DJPgcw64E= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= -go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= -go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= -go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -142,6 +204,10 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -151,42 +217,42 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= +golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= -golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= -golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= +golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index c80b8855..2662f95b 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -66,6 +66,10 @@ var ( Name: "ocp_namespaces_configmap_exists", Help: "ocp-namespaces configMap exists", }, []string{"name"}) + metricAlertmanagerConfigValidationStatus = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "alertmanager_config_validation_status", + Help: "Alertmanager config validation status (0=valid, 1=invalid)", + }, []string{"name"}) metricsList = []prometheus.Collector{ metricGASecretExists, @@ -77,6 +81,7 @@ var ( metricAMSecretContainsDMS, metricManNSConfigMapExists, metricOcpNSConfigMapExists, + metricAlertmanagerConfigValidationStatus, } ) @@ -225,3 +230,13 @@ func UpdateConfigMapMetrics(list *corev1.ConfigMapList) { metricOcpNSConfigMapExists.With(prometheus.Labels{"name": config.OperatorName}).Set(float64(0)) } } + +// UpdateAlertmanagerConfigValidationMetric updates the validation status metric +// validationPassed should be true if validation succeeded, false if it failed +func UpdateAlertmanagerConfigValidationMetric(validationPassed bool) { + if validationPassed { + metricAlertmanagerConfigValidationStatus.With(prometheus.Labels{"name": config.OperatorName}).Set(float64(0)) + } else { + metricAlertmanagerConfigValidationStatus.With(prometheus.Labels{"name": config.OperatorName}).Set(float64(1)) + } +} diff --git a/test/e2e/configure_alertmanager_operator_tests.go b/test/e2e/configure_alertmanager_operator_tests.go index 673ebef1..e6952e50 100644 --- a/test/e2e/configure_alertmanager_operator_tests.go +++ b/test/e2e/configure_alertmanager_operator_tests.go @@ -13,6 +13,8 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/openshift/osde2e-common/pkg/clients/openshift" + "github.com/openshift/osde2e-common/pkg/clients/prometheus" + amconfig "github.com/prometheus/alertmanager/config" v1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -30,6 +32,7 @@ var _ = Describe("Configure AlertManager Operator", Ordered, func() { var ( client *resources.Resources dynamicClient dynamic.Interface + prom *prometheus.Client secrets = []string{"pd-secret", "dms-secret"} serviceAccounts = []string{"configure-alertmanager-operator"} ) @@ -43,7 +46,9 @@ var _ = Describe("Configure AlertManager Operator", Ordered, func() { labelSelector = "operators.coreos.com/configure-alertmanager-operator.openshift-monitoring" ) - BeforeAll(func() { + BeforeAll(func(ctx context.Context) { + log.SetLogger(GinkgoLogr) + cfg, err := config.GetConfig() Expect(err).Should(BeNil(), "failed to get kubeconfig") client, err = resources.New(cfg) @@ -51,6 +56,13 @@ var _ = Describe("Configure AlertManager Operator", Ordered, func() { dynamicClient, err = dynamic.NewForConfig(cfg) Expect(err).ShouldNot(HaveOccurred(), "failed to configure Dynamic client") + + // Create openshift client locally for prometheus client setup + k8s, err := openshift.New(GinkgoLogr) + Expect(err).ShouldNot(HaveOccurred(), "unable to setup openshift client") + + prom, err = prometheus.New(ctx, k8s) + Expect(err).ShouldNot(HaveOccurred(), "unable to setup prometheus client") }) // Allow for one CSV request failure before exiting Eventually() loop... csvErrCounter := 0 @@ -150,6 +162,52 @@ var _ = Describe("Configure AlertManager Operator", Ordered, func() { } }) + It("alertmanager-main secret contains valid configuration", func(ctx context.Context) { + // Get the alertmanager-main secret from openshift-monitoring + var secret v1.Secret + err := client.Get(ctx, "alertmanager-main", namespace, &secret) + Expect(err).ShouldNot(HaveOccurred(), "Failed to get alertmanager-main secret") + + // Extract the alertmanager.yaml data + configData, exists := secret.Data["alertmanager.yaml"] + Expect(exists).Should(BeTrue(), "alertmanager.yaml not found in secret") + Expect(configData).ShouldNot(BeEmpty(), "alertmanager.yaml is empty") + + // Validate the config using Alertmanager's official validation + // This is the same validation that Alertmanager performs on startup + _, err = amconfig.Load(string(configData)) + Expect(err).ShouldNot(HaveOccurred(), "alertmanager config validation failed: %v", err) + }) + + It("validation metric exists and shows config is valid", func(ctx context.Context) { + // Query the alertmanager_config_validation_status metric + // Metric value: 0 = valid config, 1 = invalid config + query := `alertmanager_config_validation_status{name="configure-alertmanager-operator"}` + + // Use Eventually to allow time for metric to be scraped + Eventually(ctx, func(ctx context.Context) error { + results, err := prom.InstantQuery(ctx, query) + if err != nil { + return fmt.Errorf("failed to query prometheus: %w", err) + } + + if len(results) == 0 { + return fmt.Errorf("metric not found: %s", query) + } + + // Verify metric value is 0 (valid config) + metricValue := int(results[0].Value) + if metricValue != 0 { + return fmt.Errorf("expected metric value 0 (valid), got %d (invalid)", metricValue) + } + + return nil + }). + WithPolling(10 * time.Second). + WithTimeout(2 * time.Minute). + Should(Succeed(), "validation metric should exist and show config is valid") + }) + PIt("can be upgraded", func(ctx context.Context) { log.SetLogger(GinkgoLogr) k8sClient, err := openshift.New(ginkgo.GinkgoLogr) From 9c279b330ad4cc5acfa4d817c252bae7c7064022 Mon Sep 17 00:00:00 2001 From: Chris Collins Date: Tue, 23 Dec 2025 13:28:56 -1000 Subject: [PATCH 2/3] Fix errcheck lint violations in test setup code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Check error return values from writeAlertManagerConfig() calls in test setup to satisfy errcheck linter requirement. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- controllers/secret_controller_test.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/controllers/secret_controller_test.go b/controllers/secret_controller_test.go index de5ead60..46dfdc00 100644 --- a/controllers/secret_controller_test.go +++ b/controllers/secret_controller_test.go @@ -1632,7 +1632,9 @@ func Test_SecretReconciler(t *testing.T) { // Create the secrets for this specific test. if tt.amExists { - writeAlertManagerConfig(reconciler, reqLogger, createAlertManagerConfig(reqLogger, pdKey, "", gaLowURL, gaHighURL, gaHeartURL, wdURL, oaURL, "", "", defaultNamespaces)) + if err := writeAlertManagerConfig(reconciler, reqLogger, createAlertManagerConfig(reqLogger, pdKey, "", gaLowURL, gaHighURL, gaHeartURL, wdURL, oaURL, "", "", defaultNamespaces)); err != nil { + t.Fatalf("Failed to write alertmanager config in test setup: %v", err) + } } if tt.dmsExists { wdURL = "https://hjklasdf09876" @@ -1738,7 +1740,9 @@ func Test_SecretReconciler_Readiness(t *testing.T) { createClusterVersion(reconciler) createClusterProxy(reconciler) - writeAlertManagerConfig(reconciler, reqLogger, createAlertManagerConfig(reqLogger, "", "", "", "", "", "", "", "", "", defaultNamespaces)) + if err := writeAlertManagerConfig(reconciler, reqLogger, createAlertManagerConfig(reqLogger, "", "", "", "", "", "", "", "", "", defaultNamespaces)); err != nil { + t.Fatalf("Failed to write alertmanager config in test setup: %v", err) + } pdKey := "asdfjkl123" dmsURL := "https://hjklasdf09876" From 4da949634253d6364049ab795b5e9dd5b42e14e9 Mon Sep 17 00:00:00 2001 From: Chris Collins Date: Tue, 6 Jan 2026 12:40:06 -1000 Subject: [PATCH 3/3] Add unit tests for writeAlertManagerConfig validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Test validation failure path with invalid config - Test success path with valid config - Verify secret creation/non-creation behavior - Improves code coverage for config validation feature 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- controllers/secret_controller_test.go | 96 +++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/controllers/secret_controller_test.go b/controllers/secret_controller_test.go index 46dfdc00..655e3b69 100644 --- a/controllers/secret_controller_test.go +++ b/controllers/secret_controller_test.go @@ -2011,3 +2011,99 @@ func Test_recordConfigValidationEvent(t *testing.T) { t.Error("Expected event message to contain guidance about checking source configmaps and secrets") } } + +// Test_writeAlertManagerConfig_ValidationFailure verifies writeAlertManagerConfig returns error when validation fails +func Test_writeAlertManagerConfig_ValidationFailure(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockReadiness := readiness.NewMockInterface(ctrl) + reconciler := createReconciler(t, mockReadiness) + createNamespace(reconciler, t) + + // Create invalid config (bad duration format) + invalidConfig := &alertmanager.Config{ + Global: &alertmanager.GlobalConfig{ + ResolveTimeout: "invalid-time-format", + }, + Route: &alertmanager.Route{ + Receiver: "null", + }, + Receivers: []*alertmanager.Receiver{ + {Name: "null"}, + }, + } + + err := writeAlertManagerConfig(reconciler, reqLogger, invalidConfig) + if err == nil { + t.Fatal("Expected writeAlertManagerConfig to return error for invalid config, but got nil") + } + + if !strings.Contains(err.Error(), "alertmanager config validation failed") { + t.Errorf("Expected error to mention validation failure, got: %v", err) + } + + // Verify secret was not created + secret := &corev1.Secret{} + objectKey := client.ObjectKey{ + Namespace: "openshift-monitoring", + Name: secretNameAlertmanager, + } + getErr := reconciler.Client.Get(context.TODO(), objectKey, secret) + if getErr == nil { + t.Error("Expected secret to not exist after validation failure, but it was created") + } +} + +// Test_writeAlertManagerConfig_Success verifies writeAlertManagerConfig succeeds with valid config +func Test_writeAlertManagerConfig_Success(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockReadiness := readiness.NewMockInterface(ctrl) + reconciler := createReconciler(t, mockReadiness) + createNamespace(reconciler, t) + + // Create valid config + validConfig := &alertmanager.Config{ + Global: &alertmanager.GlobalConfig{ + ResolveTimeout: "5m", + PagerdutyURL: "https://events.pagerduty.com/v2/enqueue", + }, + Route: &alertmanager.Route{ + Receiver: "null", + GroupByStr: []string{"alertname"}, + GroupWait: "30s", + GroupInterval: "5m", + RepeatInterval: "12h", + }, + Receivers: []*alertmanager.Receiver{ + {Name: "null"}, + }, + Templates: []string{}, + } + + err := writeAlertManagerConfig(reconciler, reqLogger, validConfig) + if err != nil { + t.Fatalf("Expected writeAlertManagerConfig to succeed for valid config, got error: %v", err) + } + + // Verify secret was created + secret := &corev1.Secret{} + objectKey := client.ObjectKey{ + Namespace: "openshift-monitoring", + Name: secretNameAlertmanager, + } + getErr := reconciler.Client.Get(context.TODO(), objectKey, secret) + if getErr != nil { + t.Fatalf("Expected secret to exist after successful write, got error: %v", getErr) + } + + // Verify secret contains the config + configData, exists := secret.Data["alertmanager.yaml"] + if !exists { + t.Fatal("Expected alertmanager.yaml to exist in secret") + } + if len(configData) == 0 { + t.Fatal("Expected alertmanager.yaml to have content") + } +} +