From 77222ef87846647826e03bfed2dcc2ef5bdd3cf4 Mon Sep 17 00:00:00 2001 From: erosente Date: Fri, 6 Feb 2026 13:52:04 -0500 Subject: [PATCH 01/12] Create e2e tests for RMO on hypershift. --- controllers/hostedcontrolplane/healthcheck.go | 10 + .../hostedcontrolplane/hostedcontrolplane.go | 5 + go.mod | 148 ++- go.sum | 380 ++++-- test/e2e/route_monitor_operator_tests.go | 1176 +++++++++++++++++ 5 files changed, 1567 insertions(+), 152 deletions(-) diff --git a/controllers/hostedcontrolplane/healthcheck.go b/controllers/hostedcontrolplane/healthcheck.go index 992501fe..868e9738 100644 --- a/controllers/hostedcontrolplane/healthcheck.go +++ b/controllers/hostedcontrolplane/healthcheck.go @@ -64,6 +64,11 @@ const ( // "routemonitor.managed.openshift.io/successful-healthchecks" can be added-to/edited-on the configmap with a large number (ie - 999) to bypass // this functionality func (r *HostedControlPlaneReconciler) hcpReady(ctx context.Context, hostedcontrolplane *hypershiftv1beta1.HostedControlPlane) (bool, error) { + // Skip health check for test HCPs (e.g., osde2e tests without real kube-apiserver) + if _, osde2eTesting := hostedcontrolplane.Annotations["routemonitor.openshift.io/osde2e-testing"]; osde2eTesting { + return true, nil + } + if olderThan(hostedcontrolplane, hcpHealthCheckSkipAge) { return true, nil } @@ -228,6 +233,11 @@ func olderThan(obj metav1.Object, age time.Duration) bool { // isVpcEndpointReady checks if the VPC Endpoint associated with the HostedControlPlane is ready. func (r *HostedControlPlaneReconciler) isVpcEndpointReady(ctx context.Context, hostedcontrolplane *hypershiftv1beta1.HostedControlPlane) (bool, error) { + // Skip VPC endpoint check for test HCPs (e.g., osde2e tests without real VPC infrastructure) + if _, osde2eTesting := hostedcontrolplane.Annotations["routemonitor.openshift.io/osde2e-testing"]; osde2eTesting { + return true, nil + } + // Create an instance of the VpcEndpoint vpcEndpoint := &avov1alpha2.VpcEndpoint{} diff --git a/controllers/hostedcontrolplane/hostedcontrolplane.go b/controllers/hostedcontrolplane/hostedcontrolplane.go index 73de3f54..ff5c511c 100644 --- a/controllers/hostedcontrolplane/hostedcontrolplane.go +++ b/controllers/hostedcontrolplane/hostedcontrolplane.go @@ -323,6 +323,11 @@ func (r *HostedControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.R // deployInternalMonitoringObjects creates or updates the objects needed to monitor the kube-apiserver using cluster-internal routes func (r *HostedControlPlaneReconciler) deployInternalMonitoringObjects(ctx context.Context, log logr.Logger, hostedcontrolplane *hypershiftv1beta1.HostedControlPlane) error { + // Skip internal monitoring for test HCPs (e.g., osde2e tests without real kube-apiserver infrastructure) + if _, osde2eTesting := hostedcontrolplane.Annotations["routemonitor.openshift.io/osde2e-testing"]; osde2eTesting { + return nil + } + // Create or update route object expectedRoute := r.buildInternalMonitoringRoute(hostedcontrolplane) err := r.Create(ctx, &expectedRoute) diff --git a/go.mod b/go.mod index 1d5370f9..c2cd1a2a 100644 --- a/go.mod +++ b/go.mod @@ -3,85 +3,135 @@ module github.com/openshift/route-monitor-operator go 1.25.5 require ( - github.com/go-logr/logr v1.4.2 + github.com/go-logr/logr v1.4.3 github.com/google/gofuzz v1.2.0 - github.com/hashicorp/go-version v1.6.0 + github.com/hashicorp/go-version v1.7.0 github.com/onsi/ginkgo v1.16.5 - github.com/onsi/ginkgo/v2 v2.19.0 - github.com/onsi/gomega v1.33.1 - github.com/openshift/api v0.0.0-20240524162738-d899f8877d22 + github.com/onsi/ginkgo/v2 v2.27.3 + github.com/onsi/gomega v1.39.0 + github.com/openshift/api v0.0.0-20250409155250-8fcc4e71758a github.com/openshift/aws-vpce-operator v0.0.0-20241211114942-1daecf2e4364 github.com/openshift/hypershift/api v0.0.0-20241204143212-857ccab4fd7c - github.com/openshift/osde2e-common v0.0.0-20240604133256-b7200cad0cca - github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.63.0 - github.com/prometheus/client_golang v1.19.1 - github.com/prometheus/common v0.54.0 + github.com/openshift/osde2e v0.0.0-20260202205050-a418c410e9bc + github.com/openshift/osde2e-common v0.0.0-20260126213857-862206787cf9 + github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.64.0 + github.com/prometheus/client_golang v1.23.2 + github.com/prometheus/common v0.67.5 github.com/rhobs/obo-prometheus-operator/pkg/apis/monitoring v0.60.0-rhobs1 go.uber.org/mock v0.4.0 gopkg.in/inf.v0 v0.9.1 - k8s.io/api v0.30.3 - k8s.io/apiextensions-apiserver v0.29.5 - k8s.io/apimachinery v0.30.3 - k8s.io/client-go v0.29.5 - k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 - sigs.k8s.io/controller-runtime v0.17.2 + k8s.io/api v0.32.3 + k8s.io/apiextensions-apiserver v0.32.2 + k8s.io/apimachinery v0.32.3 + k8s.io/client-go v0.32.3 + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff + sigs.k8s.io/controller-runtime v0.20.2 ) require ( + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/adamliesko/retry v0.0.0-20200123222335-86c8baac277d // indirect + github.com/aws/aws-sdk-go v1.55.6 // indirect + github.com/aws/aws-sdk-go-v2 v1.41.1 // indirect + github.com/aws/aws-sdk-go-v2/config v1.32.7 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.7 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/service/cloudformation v1.71.4 // indirect + github.com/aws/aws-sdk-go-v2/service/ec2 v1.277.0 // 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.17 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect + github.com/aws/smithy-go v1.24.0 // indirect + github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect - github.com/evanphx/json-patch v5.6.0+incompatible // indirect - github.com/evanphx/json-patch/v5 v5.8.0 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/emicklei/go-restful/v3 v3.11.2 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fsnotify/fsnotify v1.9.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.19.6 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.22.3 // 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-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + github.com/golang/glog v1.2.4 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect - github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 // indirect - github.com/google/uuid v1.3.1 // indirect - github.com/gorilla/websocket v1.5.0 // indirect - github.com/imdario/mergo v0.3.15 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/css v1.0.1 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-retryablehttp v0.7.8 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/moby/spdystream v0.2.0 // indirect + github.com/microcosm-cc/bluemonday v1.0.26 // indirect + github.com/mitchellh/mapstructure v1.5.0 // 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/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/nxadm/tail v1.4.8 // indirect + github.com/openshift-online/ocm-api-model/clientapi v0.0.440 // indirect + github.com/openshift-online/ocm-api-model/model v0.0.440 // indirect + github.com/openshift-online/ocm-sdk-go v0.1.486 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/procfs v0.12.0 // indirect - github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.12.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/spf13/viper v1.19.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/x448/float16 v0.8.4 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect - golang.org/x/net v0.39.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/term v0.31.0 // indirect - golang.org/x/text v0.24.0 // indirect - golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect + golang.org/x/mod v0.30.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.0 // indirect + golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.39.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/protobuf v1.34.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/component-base v0.29.5 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 // indirect - sigs.k8s.io/e2e-framework v0.3.0 // indirect - sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect + sigs.k8s.io/e2e-framework v0.5.0 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index c9e5b1b4..a8278569 100644 --- a/go.sum +++ b/go.sum @@ -1,41 +1,100 @@ +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/adamliesko/retry v0.0.0-20200123222335-86c8baac277d h1:CR59ujwsy3PUDpbdbEn21JNzf/6UPg08MsEseEynkTM= +github.com/adamliesko/retry v0.0.0-20200123222335-86c8baac277d/go.mod h1:4R6y4+0tu25C+QXB8baLvMQfZrFKGKhSBnSpntSC3sA= 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 v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= +github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= +github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY= +github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY= +github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= +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/cloudformation v1.71.4 h1:9dwMueqbHIp0KTw2Zt0rhVobiPMlAI8UgyxiaBzM+1E= +github.com/aws/aws-sdk-go-v2/service/cloudformation v1.71.4/go.mod h1:R4SVh77rxRZut8uzbNhnXcwA5m99OT4hqhHkZjh5NAk= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.277.0 h1:RHJSkRXDGkAKrV4CTEsZsZkOmSpxXKO4aKx4rXd94K4= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.277.0/go.mod h1:Wg68QRgy2gEGGdmTPU/UbVpdv8sM14bUZmF64KFwAsY= +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.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ= +github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= +github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 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/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= -github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.11.2 h1:1onLa9DcsMYO9P+CXaL0dStDqQ2EHHXLiz+BtnqkLAU= +github.com/emicklei/go-restful/v3 v3.11.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch/v5 v5.8.0 h1:lRj6N9Nci7MvzrXuX6HFzU8XjmhPiXPlsKEy1u0KQro= -github.com/evanphx/json-patch/v5 v5.8.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= +github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= +github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= +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.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +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-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 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/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc= +github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -45,46 +104,98 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= -github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= -github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= -github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= -github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= -github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/itchyny/gojq v0.12.7 h1:hYPTpeWfrJ1OT+2j6cvBScbhl0TkdwGM4bc66onUSOQ= +github.com/itchyny/gojq v0.12.7/go.mod h1:ZdvNHVlzPgUf8pgjnuDTmGfHA/21KoutQUJ3An/xNuw= +github.com/itchyny/timefmt-go v0.1.3 h1:7M3LGVDsqcd0VZH2U+x393obrzZisp7C0uEe921iRkU= +github.com/itchyny/timefmt-go v0.1.3/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= +github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= +github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgtype v1.14.2 h1:QBdZQTKpPdBlw2AdKwHEyqUcm/lrl2cwWAHjCMyln/o= +github.com/jackc/pgtype v1.14.2/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= +github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 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/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= +github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 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/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 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.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= -github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= +github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= +github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= +github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +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= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -101,55 +212,99 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= -github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= +github.com/onsi/ginkgo/v2 v2.27.3 h1:ICsZJ8JoYafeXFFlFAG75a7CxMsJHwgKwtO+82SE9L8= +github.com/onsi/ginkgo/v2 v2.27.3/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= -github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= -github.com/openshift/api v0.0.0-20240524162738-d899f8877d22 h1:AW8KUN4k7qR2egrCCe3x95URHQ3N188+a/b0qpRyAHg= -github.com/openshift/api v0.0.0-20240524162738-d899f8877d22/go.mod h1:7Hm1kLJGxWT6eysOpD2zUztdn+w91eiERn6KtI5o9aw= +github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q= +github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= +github.com/openshift-online/ocm-api-model/clientapi v0.0.440 h1:BGWikczo8UuSvzEkoTf6q9iodg/pC7ibfvn5bvuD5c0= +github.com/openshift-online/ocm-api-model/clientapi v0.0.440/go.mod h1:fZwy5HY2URG9nrExvQeXrDU/08TGqZ16f8oymVEN5lo= +github.com/openshift-online/ocm-api-model/model v0.0.440 h1:sfi+fEw3ORh32keJdkE7ZA0g1uCBf457dRg6Qs8yJ6s= +github.com/openshift-online/ocm-api-model/model v0.0.440/go.mod h1:PQIoq6P8Vlb7goOdRMLK8nJY+B7HH0RTqYAa4kyidTE= +github.com/openshift-online/ocm-sdk-go v0.1.486 h1:fhrTGmuLrMkEOoQTOc7hbN7Pmu7n7VatdrMve8qznHM= +github.com/openshift-online/ocm-sdk-go v0.1.486/go.mod h1:HsSAZFf12U8seRMuWpgehW6saw3ufaPnNN+jPg8BPTs= +github.com/openshift/api v0.0.0-20250409155250-8fcc4e71758a h1:d2WEiysc+Gx51E5pQUvB5CHuXiUTsuZdKZNPHkGAZZg= +github.com/openshift/api v0.0.0-20250409155250-8fcc4e71758a/go.mod h1:yk60tHAmHhtVpJQo3TwVYq2zpuP70iJIFDCmeKMIzPw= github.com/openshift/aws-vpce-operator v0.0.0-20241211114942-1daecf2e4364 h1:wrR8DiJqUaS4pv7F1p1pOIsUvQN/z81C7TvM3PcS8GQ= github.com/openshift/aws-vpce-operator v0.0.0-20241211114942-1daecf2e4364/go.mod h1:+/t18O2NwRYEzWbi76OImNMivJXi6bbFVG3ODGeQjMk= github.com/openshift/hypershift/api v0.0.0-20241204143212-857ccab4fd7c h1:NBH3no8kaW7Hem/mf/0lIfZZEIeaNTwo7XfTqUm2cJc= github.com/openshift/hypershift/api v0.0.0-20241204143212-857ccab4fd7c/go.mod h1:K1e56UPfFQaO8BYXbVS1TbNqb6rmPo+fc6Jxjv+w0Xo= -github.com/openshift/osde2e-common v0.0.0-20240604133256-b7200cad0cca h1:Sy0D7WuQ2zzzgOsjpyMXywY6Yl5iLj8CO9jLZvtDbC0= -github.com/openshift/osde2e-common v0.0.0-20240604133256-b7200cad0cca/go.mod h1:oA2+sZPgApVQHbLLWFInDMzheCovGpnWg9ABXppFQpQ= +github.com/openshift/osde2e v0.0.0-20260202205050-a418c410e9bc h1:W6qCZPI/2f64vRbDbUH0wrVzXTcjV5UBBTrIfD/giO8= +github.com/openshift/osde2e v0.0.0-20260202205050-a418c410e9bc/go.mod h1:VW4cnPpR92sfzwuhzPWatXYAo7/ZgyjYhCDPPONjUfk= +github.com/openshift/osde2e-common v0.0.0-20260126213857-862206787cf9 h1:oUYQpPED774xWW41aRfrhvPSEcIN3FR6XeePOVqcljk= +github.com/openshift/osde2e-common v0.0.0-20260126213857-862206787cf9/go.mod h1:nMeOffePy+qdcCXuwTw5YcMMn/cTfjYQf7KRLjhKweo= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.63.0 h1:efsW3CfymG5bZUpeIsYfdihB33YItCn7uHBOEbnHQG8= -github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.63.0/go.mod h1:/UtstAaWVaS3Z9GK9jo8+4SN9T+RMSq7VlOcQMmiEsc= -github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8= -github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.64.0 h1:bqFOzWYCuSZEcuFx/ez8DFW+fqGiUEATrgezynCjpP4= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.64.0/go.mod h1:cfNgxpCPGyIydmt3HcwDqKDt0nYdlGRhzftl+DZH7WA= +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.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rhobs/obo-prometheus-operator/pkg/apis/monitoring v0.60.0-rhobs1 h1:nOo8Po45JtpFJP67cNVnyraW5h4noL+O7am5ESD2GV0= github.com/rhobs/obo-prometheus-operator/pkg/apis/monitoring v0.60.0-rhobs1/go.mod h1:nHbhLfDBgkAo/YpZ84PSIHq+ETu0B2PqCaRSbVh7b4g= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace h1:9PNP1jnUjRhfmGMlkXHjYPishpcw4jpSt/V/xYY3FMA= -github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= +github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/vladimirvivien/gexe v0.2.0 h1:nbdAQ6vbZ+ZNsolCgSVb9Fno60kzSuvtzVh6Ytqi/xY= -github.com/vladimirvivien/gexe v0.2.0/go.mod h1:LHQL00w/7gDUKIak24n801ABp8C+ni6eBht9vGVst8w= +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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/vladimirvivien/gexe v0.3.0 h1:4xwiOwGrDob5OMR6E92B9olDXYDglXdHhzR1ggYtWJM= +github.com/vladimirvivien/gexe v0.3.0/go.mod h1:fp7cy60ON1xjhtEI/+bfSEIXX35qgmI+iRYlGOqbBFM= +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.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= @@ -158,27 +313,37 @@ 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= -golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= -golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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= @@ -188,23 +353,23 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= -golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 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-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +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= @@ -217,14 +382,18 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= -google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -236,29 +405,34 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.30.3 h1:ImHwK9DCsPA9uoU3rVh4QHAHHK5dTSv1nxJUapx8hoQ= -k8s.io/api v0.30.3/go.mod h1:GPc8jlzoe5JG3pb0KJCSLX5oAFIW3/qNJITlDj8BH04= -k8s.io/apiextensions-apiserver v0.29.5 h1:njDywexhE6n+1NEl3A4axT0TMQHREnndrk3/ztdWcNE= -k8s.io/apiextensions-apiserver v0.29.5/go.mod h1:pfIvij+MH9a8NQKtW7MD4EFnzvUjJ1ZQsDL8wuP8fnc= -k8s.io/apimachinery v0.30.3 h1:q1laaWCmrszyQuSQCfNB8cFgCuDAoPszKY4ucAjDwHc= -k8s.io/apimachinery v0.30.3/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= -k8s.io/client-go v0.29.5 h1:nlASXmPQy190qTteaVP31g3c/wi2kycznkTP7Sv1zPc= -k8s.io/client-go v0.29.5/go.mod h1:aY5CnqUUvXYccJhm47XHoPcRyX6vouHdIBHaKZGTbK4= -k8s.io/component-base v0.29.5 h1:Ptj8AzG+p8c2a839XriHwxakDpZH9uvIgYz+o1agjg8= -k8s.io/component-base v0.29.5/go.mod h1:9nBUoPxW/yimISIgAG7sJDrUGJlu7t8HnDafIrOdU8Q= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= +k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= +k8s.io/apiextensions-apiserver v0.32.2 h1:2YMk285jWMk2188V2AERy5yDwBYrjgWYggscghPCvV4= +k8s.io/apiextensions-apiserver v0.32.2/go.mod h1:GPwf8sph7YlJT3H6aKUWtd0E+oyShk/YHWQHf/OOgCA= +k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= +k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= +k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= +k8s.io/component-base v0.32.3 h1:98WJvvMs3QZ2LYHBzvltFSeJjEx7t5+8s71P7M74u8k= +k8s.io/component-base v0.32.3/go.mod h1:LWi9cR+yPAv7cu2X9rZanTiFKB2kHA+JjmhkKjCZRpI= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= -k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= -k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 h1:jgGTlFYnhF1PM1Ax/lAlxUPE+KfCIXHaathvJg1C3ak= -k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/controller-runtime v0.17.2 h1:FwHwD1CTUemg0pW2otk7/U5/i5m2ymzvOXdbeGOUvw0= -sigs.k8s.io/controller-runtime v0.17.2/go.mod h1:+MngTvIQQQhfXtwfdGw/UOQ/aIaqsYywfCINOtwMO/s= -sigs.k8s.io/e2e-framework v0.3.0 h1:eqQALBtPCth8+ulTs6lcPK7ytV5rZSSHJzQHZph4O7U= -sigs.k8s.io/e2e-framework v0.3.0/go.mod h1:C+ef37/D90Dc7Xq1jQnNbJYscrUGpxrWog9bx2KIa+c= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro= +k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.20.2 h1:/439OZVxoEc02psi1h4QO3bHzTgu49bb347Xp4gW1pc= +sigs.k8s.io/controller-runtime v0.20.2/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= +sigs.k8s.io/e2e-framework v0.5.0 h1:YLhk8R7EHuTFQAe6Fxy5eBzn5Vb+yamR5u8MH1Rq3cE= +sigs.k8s.io/e2e-framework v0.5.0/go.mod h1:jJSH8u2RNmruekUZgHAtmRjb5Wj67GErli9UjLSY7Zc= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/test/e2e/route_monitor_operator_tests.go b/test/e2e/route_monitor_operator_tests.go index d5ccb52f..ddc30cdb 100644 --- a/test/e2e/route_monitor_operator_tests.go +++ b/test/e2e/route_monitor_operator_tests.go @@ -6,27 +6,154 @@ package osde2etests import ( "context" + "encoding/json" "fmt" + "io" + "net/http" + "net/url" + "os" + "strings" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" routev1 "github.com/openshift/api/route/v1" + awsvpceapi "github.com/openshift/aws-vpce-operator/api/v1alpha2" + hypershiftv1beta1 "github.com/openshift/hypershift/api/hypershift/v1beta1" "github.com/openshift/osde2e-common/pkg/clients/openshift" . "github.com/openshift/osde2e-common/pkg/gomega/assertions" . "github.com/openshift/osde2e-common/pkg/gomega/matchers" + "github.com/openshift/osde2e/pkg/common/providers" routemonitorv1alpha1 "github.com/openshift/route-monitor-operator/api/v1alpha1" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" kubev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/client-go/dynamic" "sigs.k8s.io/controller-runtime/pkg/log" ) +// Local minimal ClusterDeployment type for testing +// We define this locally to avoid pulling in the full hive dependency +type ClusterDeployment struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec ClusterDeploymentSpec `json:"spec,omitempty"` +} + +// DeepCopyObject implements runtime.Object interface +func (cd *ClusterDeployment) DeepCopyObject() runtime.Object { + if cd == nil { + return nil + } + out := new(ClusterDeployment) + cd.DeepCopyInto(out) + return out +} + +// DeepCopyInto copies all properties of this object into another object of the same type +func (cd *ClusterDeployment) DeepCopyInto(out *ClusterDeployment) { + *out = *cd + out.TypeMeta = cd.TypeMeta + cd.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = cd.Spec +} + +type ClusterDeploymentSpec struct { + ClusterName string `json:"clusterName"` + BaseDomain string `json:"baseDomain"` +} + +const ( + // Embedded CRD definitions - no external dependencies required + clusterDeploymentCRD = ` +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: clusterdeployments.hive.openshift.io +spec: + group: hive.openshift.io + names: + kind: ClusterDeployment + listKind: ClusterDeploymentList + plural: clusterdeployments + shortNames: + - cd + singular: clusterdeployment + scope: Namespaced + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + x-kubernetes-preserve-unknown-fields: true + subresources: + status: {} +` + + hostedControlPlaneCRD = ` +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: hostedcontrolplanes.hypershift.openshift.io +spec: + group: hypershift.openshift.io + names: + kind: HostedControlPlane + listKind: HostedControlPlaneList + plural: hostedcontrolplanes + shortNames: + - hcp + singular: hostedcontrolplane + scope: Namespaced + versions: + - name: v1beta1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + x-kubernetes-preserve-unknown-fields: true + subresources: + status: {} +` + + vpcEndpointCRD = ` +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: vpcendpoints.avo.openshift.io +spec: + group: avo.openshift.io + names: + kind: VpcEndpoint + listKind: VpcEndpointList + plural: vpcendpoints + singular: vpcendpoint + scope: Namespaced + versions: + - name: v1alpha2 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + x-kubernetes-preserve-unknown-fields: true + subresources: + status: {} +` +) + var _ = Describe("Route Monitor Operator", Ordered, func() { var ( k8s *openshift.Client @@ -249,3 +376,1052 @@ var _ = Describe("Route Monitor Operator", Ordered, func() { }) }) }) +var _ = Describe("RHOBS Synthetic Monitoring", Ordered, func() { + var ( + k8s *openshift.Client + rhobsAPIURL string + rhobsTenant string + testNamespace string + pollingDuration time.Duration + probeActivationTimeout time.Duration + oidcCredentials *OIDCCredentials + ) + + const ( + rmoNamespace = "openshift-route-monitor-operator" + ) + + BeforeAll(func(ctx context.Context) { + log.SetLogger(GinkgoLogr) + var err error + k8s, err = openshift.New(GinkgoLogr) + Expect(err).ShouldNot(HaveOccurred(), "unable to setup k8s client") + + // Install required CRDs if they don't exist + By("ensuring required CRDs are installed") + err = ensureCRDsInstalled(ctx, k8s) + Expect(err).ShouldNot(HaveOccurred(), "failed to ensure CRDs are installed") + + // Restart RMO to pick up CRDs (in case RMO was deployed before test ran) + By("restarting RMO to ensure HostedControlPlane controller is running") + err = restartRMODeployment(ctx, k8s) + if err != nil { + GinkgoLogr.Info("Warning: could not restart RMO deployment", "error", err) + } + + // Register HyperShift and AWS VPCE schemes + Expect(hypershiftv1beta1.AddToScheme(k8s.GetScheme())).Should(Succeed(), "unable to register HyperShift scheme") + Expect(awsvpceapi.AddToScheme(k8s.GetScheme())).Should(Succeed(), "unable to register AWS VPCE scheme") + + // Determine environment from osde2e provider + var environment string + provider, err := providers.ClusterProvider() + if err != nil { + GinkgoLogr.Error(err, "Failed to get cluster provider, falling back to stage") + environment = "stage" + } else { + environment = provider.Environment() // Returns "int", "stage", or "prod" + } + + // Fetch OIDC credentials from environment variables + By("fetching OIDC credentials") + + // Log which credential source is available for debugging + if os.Getenv("EXTERNAL_SECRET_OIDC_CLIENT_ID") != "" { + GinkgoLogr.Info("EXTERNAL_SECRET_ credentials detected") + } else { + GinkgoLogr.Info("No EXTERNAL_SECRET_ credentials found, test will fail") + } + + // Get OIDC credentials from ConfigMap first, fall back to env vars if needed + // This checks ConfigMap first, then environment variables, then creates/updates ConfigMap + By("getting OIDC credentials from ConfigMap or environment variables") + creds, err := getOrCreateOIDCCredentials(ctx, k8s, environment) + Expect(err).ShouldNot(HaveOccurred(), "failed to fetch credentials") + oidcCredentials = creds // Store for use in listRHOBSProbes + + // Set RHOBS API URL (from credentials or environment) + if creds.ProbeAPIURL != "" { + rhobsAPIURL = creds.ProbeAPIURL + } else { + rhobsAPIURL = getRHOBSAPIURL(environment) + } + + rhobsTenant = getEnvOrDefault("RHOBS_TENANT", "hcp") + testNamespace = getEnvOrDefault("HCP_TEST_NAMESPACE", "clusters") + pollingDuration = 3 * time.Minute + probeActivationTimeout = 5 * time.Minute + + // Label the test cluster as a management cluster + By("labeling test cluster as management-cluster") + err = labelTestClusterAsManagementCluster(ctx, k8s) + if err != nil { + GinkgoLogr.Info("Warning: could not label test cluster as management cluster", "error", err) + } + + GinkgoLogr.Info("RHOBS Synthetic Monitoring test suite initialized", + "environment", environment, + "probeAPIURL", rhobsAPIURL, + "tenant", rhobsTenant, + "testNamespace", testNamespace, + "oidcConfigured", creds.ClientID != "") + }) + + // Phase 1 Test 1: Verify RHOBS monitoring configuration + It("has RHOBS monitoring configured", func(ctx context.Context) { + By("checking RMO deployment exists") + deployment := &appsv1.Deployment{} + err := k8s.Get(ctx, "route-monitor-operator-controller-manager", rmoNamespace, deployment) + Expect(err).ShouldNot(HaveOccurred(), "RMO deployment not found") + + By("verifying RMO deployment is ready") + Expect(deployment.Status.ReadyReplicas).Should(BeNumerically(">", 0), "RMO deployment has no ready replicas") + + By("verifying RMO config ConfigMap has OIDC credentials") + configMap := &corev1.ConfigMap{} + err = k8s.Get(ctx, "route-monitor-operator-config", rmoNamespace, configMap) + Expect(err).ShouldNot(HaveOccurred(), "RMO config ConfigMap not found") + Expect(configMap.Data).Should(HaveKey("oidc-client-id"), "ConfigMap missing oidc-client-id") + Expect(configMap.Data).Should(HaveKey("oidc-client-secret"), "ConfigMap missing oidc-client-secret") + Expect(configMap.Data).Should(HaveKey("oidc-issuer-url"), "ConfigMap missing oidc-issuer-url") + Expect(configMap.Data).Should(HaveKey("probe-api-url"), "ConfigMap missing probe-api-url") + Expect(configMap.Data["oidc-client-id"]).ShouldNot(BeEmpty(), "oidc-client-id should not be empty") + Expect(configMap.Data["oidc-client-secret"]).ShouldNot(BeEmpty(), "oidc-client-secret should not be empty") + + GinkgoLogr.Info("RMO deployment and configuration verified", + "namespace", rmoNamespace, + "readyReplicas", deployment.Status.ReadyReplicas, + "configuredOIDC", true, + "probeAPIURL", configMap.Data["probe-api-url"]) + }) + + // Phase 1 Test 2: Create probe for public HostedControlPlane + It("creates probe for public HostedControlPlane", func(ctx context.Context) { + clusterID := fmt.Sprintf("test-osde2e-public-%d", time.Now().Unix()) + hcpName := fmt.Sprintf("public-hcp-%d", time.Now().UnixNano()%100000) + // Use MC-style namespace pattern: {prefix}-{cluster-id}-{cluster-name} + namespace := fmt.Sprintf("%s-%s-%s", testNamespace, clusterID, hcpName) + + By(fmt.Sprintf("creating MC-style namespace: %s", namespace)) + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + }, + } + err := k8s.Create(ctx, ns) + Expect(err).ShouldNot(HaveOccurred(), "failed to create namespace") + +By(fmt.Sprintf("creating public HostedControlPlane with cluster ID: %s", clusterID)) + hcp := createMCStyleHCP(clusterID, hcpName, namespace, hypershiftv1beta1.Public) + + err = k8s.Create(ctx, hcp) + Expect(err).ShouldNot(HaveOccurred(), "failed to create HostedControlPlane") + + By("setting HostedControlPlane status to Available") + err = setHostedControlPlaneAvailable(ctx, k8s, hcp) + Expect(err).ShouldNot(HaveOccurred(), "failed to update HostedControlPlane status") + GinkgoLogr.Info("HCP status set to Available", "clusterID", clusterID) + + By("waiting for RMO to reconcile and create probe (up to 3 minutes)") + var probe map[string]interface{} + err = wait.PollUntilContextTimeout(ctx, 10*time.Second, pollingDuration, false, func(ctx context.Context) (bool, error) { + probes, err := listRHOBSProbes(rhobsAPIURL, fmt.Sprintf("cluster-id=%s", clusterID), oidcCredentials) + if err != nil { + GinkgoLogr.Info("Error querying RHOBS API (will retry)", "error", err) + return false, nil // Continue polling on error + } + + if len(probes) > 0 { + probe = probes[0] + return true, nil + } + + GinkgoLogr.Info("Waiting for probe to be created...", "clusterID", clusterID) + return false, nil + }) + Expect(err).ShouldNot(HaveOccurred(), "probe was not created within timeout") + Expect(probe).ShouldNot(BeNil(), "probe should exist") + + By("validating probe configuration") + probeID, ok := probe["id"].(string) + Expect(ok).Should(BeTrue(), "probe ID should be a string") + staticURL, ok := probe["static_url"].(string) + Expect(ok).Should(BeTrue(), "probe static_url should be a string") + labels, ok := probe["labels"].(map[string]interface{}) + Expect(ok).Should(BeTrue(), "probe labels should be a map") + + GinkgoLogr.Info("Probe created successfully", + "probeID", probeID, + "staticURL", staticURL, + "labels", labels) + + // Extract hostname from HCP spec + hostname := hcp.Spec.Services[0].ServicePublishingStrategy.Route.Hostname + Expect(staticURL).Should(ContainSubstring(hostname), "probe URL should contain HCP hostname") + Expect(labels["cluster-id"]).Should(Equal(clusterID), "probe should have correct cluster-id label") + Expect(labels["private"]).Should(Equal("false"), "probe should be marked as public") + + // Optional: Wait for probe to become active + By("optionally waiting for probe to become active") + err = wait.PollUntilContextTimeout(ctx, 15*time.Second, probeActivationTimeout, false, func(ctx context.Context) (bool, error) { + p, err := getRHOBSProbe(rhobsAPIURL, probeID) + if err != nil { + return false, nil + } + status, ok := p["status"].(string) + if ok && status == "active" { + GinkgoLogr.Info("Probe activated successfully", "probeID", probeID) + return true, nil + } + GinkgoLogr.Info("Waiting for probe activation...", "currentStatus", status) + return false, nil + }) + if err != nil { + GinkgoLogr.Info("Warning: probe did not reach active status within timeout (may be expected)", "error", err) + } + + // Cleanup + DeferCleanup(func(ctx context.Context) { + By("cleaning up test HostedControlPlane") + err := k8s.Delete(ctx, hcp) + if err != nil { + GinkgoLogr.Info("Warning: failed to delete HCP", "error", err) + } + +By("verifying probe is deleted from RHOBS API") + err = wait.PollUntilContextTimeout(ctx, 5*time.Second, 2*time.Minute, false, func(ctx context.Context) (bool, error) { + probes, err := listRHOBSProbes(rhobsAPIURL, fmt.Sprintf("cluster-id=%s", clusterID), oidcCredentials) + if err != nil { + return false, nil + } + if len(probes) == 0 { + GinkgoLogr.Info("Probe successfully deleted", "clusterID", clusterID) + return true, nil + } + GinkgoLogr.Info("Waiting for probe deletion...", "remainingProbes", len(probes)) + return false, nil + }) + if err != nil { + GinkgoLogr.Info("Warning: probe may not have been cleaned up", "clusterID", clusterID) + } + + By("cleaning up test namespace") + err = k8s.Delete(ctx, ns) + if err != nil { + GinkgoLogr.Info("Warning: failed to delete namespace", "error", err) + } + }) + }) + + // Phase 1 Test 3: Create probe for private HostedControlPlane + It("creates probe for private HostedControlPlane", func(ctx context.Context) { + clusterID := fmt.Sprintf("test-osde2e-private-%d", time.Now().Unix()) + hcpName := fmt.Sprintf("private-hcp-%d", time.Now().UnixNano()%100000) + // Use MC-style namespace pattern + namespace := fmt.Sprintf("%s-%s-%s", testNamespace, clusterID, hcpName) + + By(fmt.Sprintf("creating MC-style namespace: %s", namespace)) + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + }, + } + err := k8s.Create(ctx, ns) + Expect(err).ShouldNot(HaveOccurred(), "failed to create namespace") + + By(fmt.Sprintf("creating private HostedControlPlane with cluster ID: %s", clusterID)) + hcp := createMCStyleHCP(clusterID, hcpName, namespace, hypershiftv1beta1.Private) + + err = k8s.Create(ctx, hcp) + Expect(err).ShouldNot(HaveOccurred(), "failed to create HostedControlPlane") + + By("setting HostedControlPlane status to Available") + err = setHostedControlPlaneAvailable(ctx, k8s, hcp) + Expect(err).ShouldNot(HaveOccurred(), "failed to update HostedControlPlane status") + GinkgoLogr.Info("Private HCP status set to Available", "clusterID", clusterID) + + By("waiting for RMO to reconcile and create probe") + var probe map[string]interface{} + err = wait.PollUntilContextTimeout(ctx, 10*time.Second, pollingDuration, false, func(ctx context.Context) (bool, error) { + probes, err := listRHOBSProbes(rhobsAPIURL, fmt.Sprintf("cluster-id=%s", clusterID), oidcCredentials) + if err != nil { + GinkgoLogr.Info("Error querying RHOBS API (will retry)", "error", err) + return false, nil + } + + if len(probes) > 0 { + probe = probes[0] + return true, nil + } + + GinkgoLogr.Info("Waiting for probe to be created...", "clusterID", clusterID) + return false, nil + }) + Expect(err).ShouldNot(HaveOccurred(), "probe was not created within timeout") + Expect(probe).ShouldNot(BeNil(), "probe should exist") + + By("validating probe configuration for private cluster") + probeID, ok := probe["id"].(string) + Expect(ok).Should(BeTrue(), "probe ID should be a string") + labels, ok := probe["labels"].(map[string]interface{}) + Expect(ok).Should(BeTrue(), "probe labels should be a map") + + GinkgoLogr.Info("Probe created successfully for private cluster", + "probeID", probeID, + "labels", labels) + + Expect(labels["cluster-id"]).Should(Equal(clusterID), "probe should have correct cluster-id label") + Expect(labels["private"]).Should(Equal("true"), "probe should be marked as private") + + // Cleanup + DeferCleanup(func(ctx context.Context) { + By("cleaning up test resources") + err := k8s.Delete(ctx, hcp) + if err != nil { + GinkgoLogr.Info("Warning: failed to delete HCP", "error", err) + } + + By("verifying probe is deleted from RHOBS API") + err = wait.PollUntilContextTimeout(ctx, 5*time.Second, 2*time.Minute, false, func(ctx context.Context) (bool, error) { + probes, err := listRHOBSProbes(rhobsAPIURL, fmt.Sprintf("cluster-id=%s", clusterID), oidcCredentials) + if err != nil { + return false, nil + } + if len(probes) == 0 { + GinkgoLogr.Info("Probe successfully deleted", "clusterID", clusterID) + return true, nil + } + return false, nil + }) + if err != nil { + GinkgoLogr.Info("Warning: probe may not have been cleaned up", "clusterID", clusterID) + } + + By("cleaning up test namespace") + err = k8s.Delete(ctx, ns) + if err != nil { + GinkgoLogr.Info("Warning: failed to delete namespace", "error", err) + } + }) + }) + + // Phase 1 Test 4: Probe deletion on HCP deletion (SREP-2832) + It("deletes probe when HostedControlPlane is deleted", func(ctx context.Context) { + clusterID := fmt.Sprintf("test-osde2e-deletion-%d", time.Now().Unix()) + hcpName := fmt.Sprintf("deletion-hcp-%d", time.Now().UnixNano()%100000) + // Use MC-style namespace pattern + namespace := fmt.Sprintf("%s-%s-%s", testNamespace, clusterID, hcpName) + + By(fmt.Sprintf("creating MC-style namespace: %s", namespace)) + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + }, + } + err := k8s.Create(ctx, ns) + Expect(err).ShouldNot(HaveOccurred(), "failed to create namespace") + +By(fmt.Sprintf("creating HostedControlPlane for deletion test with cluster ID: %s", clusterID)) + hcp := createMCStyleHCP(clusterID, hcpName, namespace, hypershiftv1beta1.Public) + + err = k8s.Create(ctx, hcp) + Expect(err).ShouldNot(HaveOccurred(), "failed to create HostedControlPlane") + + By("setting HostedControlPlane status to Available") + err = setHostedControlPlaneAvailable(ctx, k8s, hcp) + Expect(err).ShouldNot(HaveOccurred(), "failed to update HostedControlPlane status") + GinkgoLogr.Info("HCP status set to Available for deletion test", "clusterID", clusterID) + + By("waiting for probe to be created") + var probeID string + err = wait.PollUntilContextTimeout(ctx, 10*time.Second, pollingDuration, false, func(ctx context.Context) (bool, error) { + probes, err := listRHOBSProbes(rhobsAPIURL, fmt.Sprintf("cluster-id=%s", clusterID), oidcCredentials) + if err != nil { + return false, nil + } + if len(probes) > 0 { + if id, ok := probes[0]["id"].(string); ok { + probeID = id + GinkgoLogr.Info("Probe created", "probeID", probeID) + return true, nil + } + } + return false, nil + }) + Expect(err).ShouldNot(HaveOccurred(), "probe was not created within timeout") + Expect(probeID).ShouldNot(BeEmpty(), "probe ID should be set") + + By("deleting HostedControlPlane CR") + err = k8s.Delete(ctx, hcp) + Expect(err).ShouldNot(HaveOccurred(), "failed to delete HostedControlPlane") + + By("waiting for probe to be deleted from RHOBS API (validates SREP-2832 fix)") + err = wait.PollUntilContextTimeout(ctx, 5*time.Second, 2*time.Minute, false, func(ctx context.Context) (bool, error) { + probes, err := listRHOBSProbes(rhobsAPIURL, fmt.Sprintf("cluster-id=%s", clusterID), oidcCredentials) + if err != nil { + // API error - continue polling + return false, nil + } + + if len(probes) == 0 { + GinkgoLogr.Info("Probe successfully deleted - no orphaned probes (SREP-2832)", "clusterID", clusterID) + return true, nil + } + + // Check if probe is in terminating/deleted state + for _, p := range probes { + if status, ok := p["status"].(string); ok { + if status == "terminating" || status == "deleted" { + GinkgoLogr.Info("Probe in terminating state", "status", status) + return true, nil + } + } + } + + GinkgoLogr.Info("Waiting for probe deletion...", "remainingProbes", len(probes)) + return false, nil + }) + Expect(err).ShouldNot(HaveOccurred(), "probe was not deleted - ORPHANED PROBE DETECTED (SREP-2832)") + + By("verifying HostedControlPlane is fully deleted (no stuck finalizers)") + err = wait.PollUntilContextTimeout(ctx, 2*time.Second, 1*time.Minute, false, func(ctx context.Context) (bool, error) { + hcpCheck := &hypershiftv1beta1.HostedControlPlane{} + err := k8s.Get(ctx, hcpName, namespace, hcpCheck) + if err != nil && isNotFoundError(err) { + GinkgoLogr.Info("HostedControlPlane fully deleted", "name", hcpName) + return true, nil + } + if err == nil { + GinkgoLogr.Info("Waiting for HCP deletion...", "finalizers", hcpCheck.Finalizers) + } + return false, nil + }) + Expect(err).ShouldNot(HaveOccurred(), "HostedControlPlane was not deleted - finalizer may be stuck (SREP-2966)") + + // Cleanup namespace after test + By("cleaning up test namespace") + err = k8s.Delete(ctx, ns) + if err != nil { + GinkgoLogr.Info("Warning: failed to delete namespace", "error", err) + } + }) +}) + +// Helper functions for RHOBS API interaction + +// getOIDCAccessToken gets an access token from the OIDC provider using client credentials flow +func getOIDCAccessToken(creds *OIDCCredentials) (string, error) { + data := url.Values{} + data.Set("grant_type", "client_credentials") + data.Set("client_id", creds.ClientID) + data.Set("client_secret", creds.ClientSecret) + + req, err := http.NewRequest("POST", creds.IssuerURL, strings.NewReader(data.Encode())) + if err != nil { + return "", fmt.Errorf("failed to create token request: %w", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("failed to get access token: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return "", fmt.Errorf("OIDC token endpoint returned status %d: %s", resp.StatusCode, string(body)) + } + + var tokenResp struct { + AccessToken string `json:"access_token"` + } + if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { + return "", fmt.Errorf("failed to decode token response: %w", err) + } + + return tokenResp.AccessToken, nil +} + +func listRHOBSProbes(baseURL, labelSelector string, creds *OIDCCredentials) ([]map[string]interface{}, error) { + // Get OIDC access token + accessToken, err := getOIDCAccessToken(creds) + if err != nil { + return nil, fmt.Errorf("failed to get OIDC access token: %w", err) + } + + client := &http.Client{Timeout: 10 * time.Second} + + reqURL := baseURL + if labelSelector != "" { + reqURL += "?label_selector=" + labelSelector + } + + req, err := http.NewRequest("GET", reqURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Authorization", "Bearer "+accessToken) + + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to query RHOBS API: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("RHOBS API returned status %d: %s", resp.StatusCode, string(body)) + } + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + // Try to decode as array + var probes []map[string]interface{} + if err := json.Unmarshal(bodyBytes, &probes); err != nil { + // Try wrapped response format + var wrapper map[string]interface{} + if err2 := json.Unmarshal(bodyBytes, &wrapper); err2 == nil { + if probesData, ok := wrapper["probes"].([]interface{}); ok { + probes = make([]map[string]interface{}, len(probesData)) + for i, p := range probesData { + if pm, ok := p.(map[string]interface{}); ok { + probes[i] = pm + } + } + return probes, nil + } + } + return nil, fmt.Errorf("failed to decode RHOBS API response: %w", err) + } + + return probes, nil +} + +func getRHOBSProbe(baseURL, probeID string) (map[string]interface{}, error) { + client := &http.Client{Timeout: 10 * time.Second} + + resp, err := client.Get(baseURL + "/" + probeID) + if err != nil { + return nil, fmt.Errorf("failed to get probe: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("RHOBS API returned status %d: %s", resp.StatusCode, string(body)) + } + + var probe map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&probe); err != nil { + return nil, fmt.Errorf("failed to decode probe response: %w", err) + } + + return probe, nil +} + +func getEnvOrDefault(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} + +func getRHOBSAPIURL(environment string) string { + switch environment { + case "int": // osde2e uses "int" for integration + return "https://rhobs.us-west-2.api.integration.openshift.com/api/metrics/v1/hcp/probes" + case "stage": + return "https://rhobs.us-east-1-0.api.stage.openshift.com/api/metrics/v1/hcp/probes" + case "prod": + // Production RHOBS not yet available - return empty string + return "" + default: + // Default to stage + return "https://rhobs.us-east-1-0.api.stage.openshift.com/api/metrics/v1/hcp/probes" + } +} + +func contains(s, substr string) bool { + return strings.Contains(s, substr) +} + +func isAlreadyExistsError(err error) bool { + return k8serrors.IsAlreadyExists(err) +} + +func isNotFoundError(err error) bool { + return k8serrors.IsNotFound(err) +} + +func setHostedControlPlaneAvailable(ctx context.Context, k8s *openshift.Client, hcp *hypershiftv1beta1.HostedControlPlane) error { + // Update the status to simulate HyperShift controller behavior + // RMO only processes HCPs with Available=true status + // Use retry loop with optimistic concurrency control + + // Retry up to 5 times if we get "object has been modified" errors + maxRetries := 5 + for attempt := 0; attempt < maxRetries; attempt++ { + // Fetch the latest version + u := &unstructured.Unstructured{} + u.SetGroupVersionKind(hypershiftv1beta1.GroupVersion.WithKind("HostedControlPlane")) + err := k8s.Get(ctx, hcp.Name, hcp.Namespace, u) + if err != nil { + return fmt.Errorf("failed to fetch HCP: %w", err) + } + + // Update the status field + now := metav1.Now() + conditions := []interface{}{ + map[string]interface{}{ + "type": string(hypershiftv1beta1.HostedControlPlaneAvailable), + "status": string(metav1.ConditionTrue), + "reason": "AsExpected", + "message": "HostedControlPlane is available", + "lastTransitionTime": now.UTC().Format(time.RFC3339), + }, + } + + status := map[string]interface{}{ + "conditions": conditions, + } + if err := unstructured.SetNestedField(u.Object, status, "status"); err != nil { + return fmt.Errorf("failed to set status field: %w", err) + } + + // Try to update + err = k8s.Update(ctx, u) + if err == nil { + // Success! + return nil + } + + // Check if it's a conflict error + if strings.Contains(err.Error(), "object has been modified") || + strings.Contains(err.Error(), "the object has been modified") { + // Retry after a short delay + GinkgoLogr.V(1).Info("HCP was modified, retrying status update", "attempt", attempt+1, "maxRetries", maxRetries) + time.Sleep(100 * time.Millisecond) + continue + } + + // Some other error, return it + return fmt.Errorf("failed to update HCP status: %w", err) + } + + return fmt.Errorf("failed to update HCP status after %d retries", maxRetries) +} + +func createMCStyleHCP(clusterID, name, namespace string, endpointAccess hypershiftv1beta1.AWSEndpointAccessType) *hypershiftv1beta1.HostedControlPlane { + // Create HCP matching Management Cluster patterns observed in staging + hostname := fmt.Sprintf("api.%s.test.devshift.org", name) + + return &hypershiftv1beta1.HostedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: map[string]string{ + // MC standard labels + "api.openshift.com/id": clusterID, + "api.openshift.com/name": name, + "api.openshift.com/environment": "staging", + "test-type": "osde2e-rhobs-synthetics", + }, + Annotations: map[string]string{ + // MC standard annotation pointing to HostedCluster + "hypershift.openshift.io/cluster": fmt.Sprintf("%s/%s", namespace, name), + // Skip all infrastructure checks for osde2e test HCPs (health check, VPC endpoint, internal monitoring) + "routemonitor.openshift.io/osde2e-testing": "true", + }, + }, + Spec: hypershiftv1beta1.HostedControlPlaneSpec{ + ClusterID: clusterID, + Platform: hypershiftv1beta1.PlatformSpec{ + Type: hypershiftv1beta1.AWSPlatform, + AWS: &hypershiftv1beta1.AWSPlatformSpec{ + Region: "us-west-2", + EndpointAccess: endpointAccess, + }, + }, + Services: []hypershiftv1beta1.ServicePublishingStrategyMapping{ + { + Service: hypershiftv1beta1.APIServer, + ServicePublishingStrategy: hypershiftv1beta1.ServicePublishingStrategy{ + Type: hypershiftv1beta1.Route, + Route: &hypershiftv1beta1.RoutePublishingStrategy{ + Hostname: hostname, + }, + }, + }, + }, + }, + } +} + +// restartRMODeployment restarts the RMO deployment to pick up newly installed CRDs +func restartRMODeployment(ctx context.Context, k8s *openshift.Client) error { + const rmoNamespace = "openshift-route-monitor-operator" + + // List all deployments in the RMO namespace to find the controller-manager + deploymentList := &appsv1.DeploymentList{} + err := k8s.List(ctx, deploymentList) + if err != nil { + return fmt.Errorf("failed to list deployments: %w", err) + } + + var rmoDeployment *appsv1.Deployment + for i := range deploymentList.Items { + if deploymentList.Items[i].Namespace == rmoNamespace && + strings.Contains(deploymentList.Items[i].Name, "route-monitor-operator-controller-manager") { + rmoDeployment = &deploymentList.Items[i] + break + } + } + + if rmoDeployment == nil { + return fmt.Errorf("RMO deployment not found in namespace %s", rmoNamespace) + } + + // List all pods to find RMO pods + podList := &corev1.PodList{} + err = k8s.List(ctx, podList) + if err != nil { + return fmt.Errorf("failed to list pods: %w", err) + } + + // Delete RMO pods matching the deployment's label selector + for i := range podList.Items { + pod := &podList.Items[i] + if pod.Namespace != rmoNamespace { + continue + } + + // Check if pod matches deployment's selector + labelSelector := labels.SelectorFromSet(rmoDeployment.Spec.Selector.MatchLabels) + if labelSelector.Matches(labels.Set(pod.Labels)) { + GinkgoLogr.Info("Deleting RMO pod to pick up CRDs", "pod", pod.Name) + if err := k8s.Delete(ctx, pod); err != nil { + return fmt.Errorf("failed to delete pod %s: %w", pod.Name, err) + } + } + } + + // Wait for new pod to be ready + GinkgoLogr.Info("Waiting for RMO deployment to be ready after restart") + err = wait.PollUntilContextTimeout(ctx, 2*time.Second, 60*time.Second, false, func(ctx context.Context) (bool, error) { + deployment := &appsv1.Deployment{} + err := k8s.Get(ctx, rmoDeployment.Name, rmoNamespace, deployment) + if err != nil { + return false, err + } + return deployment.Status.ReadyReplicas > 0, nil + }) + if err != nil { + return fmt.Errorf("RMO deployment did not become ready: %w", err) + } + + GinkgoLogr.Info("RMO deployment restarted successfully") + return nil +} + +// ensureCRDsInstalled checks if required CRDs exist and installs them if missing +func ensureCRDsInstalled(ctx context.Context, k8s *openshift.Client) error { + crdsToInstall := []struct { + name string + yamlDef string + }{ + { + name: "hostedcontrolplanes.hypershift.openshift.io", + yamlDef: hostedControlPlaneCRD, + }, + { + name: "vpcendpoints.avo.openshift.io", + yamlDef: vpcEndpointCRD, + }, + { + name: "clusterdeployments.hive.openshift.io", + yamlDef: clusterDeploymentCRD, + }, + } + + var missingCRDs []string + + for _, crd := range crdsToInstall { + // Check if CRD already exists + exists, err := crdExists(ctx, k8s, crd.name) + if err != nil { + return fmt.Errorf("failed to check if CRD %s exists: %w", crd.name, err) + } + + if exists { + GinkgoLogr.Info("CRD already exists, skipping installation", "crd", crd.name) + continue + } + + // CRD doesn't exist, try to install it from embedded YAML + GinkgoLogr.Info("Installing CRD from embedded definition", "crd", crd.name) + if err := installCRDFromYAML(ctx, k8s, crd.yamlDef); err != nil { + // Check if CRD already exists (race condition or detection failure) + if strings.Contains(err.Error(), "AlreadyExists") || strings.Contains(err.Error(), "already exists") { + GinkgoLogr.Info("CRD already exists (detected during create), skipping", "crd", crd.name) + continue + } + // Check if this is a permission error + if strings.Contains(err.Error(), "Forbidden") || strings.Contains(err.Error(), "forbidden") { + GinkgoLogr.Error(err, "Permission denied - cannot install CRD", "crd", crd.name) + missingCRDs = append(missingCRDs, crd.name) + continue + } + // Other error - fail immediately + return fmt.Errorf("failed to install CRD %s: %w", crd.name, err) + } + GinkgoLogr.Info("Successfully installed CRD", "crd", crd.name) + } + + // If we couldn't install CRDs due to permissions, provide helpful error + if len(missingCRDs) > 0 { + return fmt.Errorf(` +Missing required CRDs that could not be installed due to insufficient permissions: + %s + +You need cluster-admin privileges to install CRDs. Please ask a cluster administrator to run: + + kubectl apply -f - <<'EOF' +%s%s +EOF + +Alternatively, if these CRDs are already installed in a different namespace or with a different name, +you may need to adjust the test configuration. +`, strings.Join(missingCRDs, "\n "), hostedControlPlaneCRD, vpcEndpointCRD) + } + + return nil +} + +// crdExists checks if a CRD with the given name exists +func crdExists(ctx context.Context, k8s *openshift.Client, crdName string) (bool, error) { + // Use unstructured client to check for CRD existence + u := &unstructured.Unstructured{} + u.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "apiextensions.k8s.io", + Version: "v1", + Kind: "CustomResourceDefinition", + }) + + err := k8s.Get(ctx, crdName, "", u) + if err != nil { + // Check if it's a "not found" error + if strings.Contains(err.Error(), "not found") || strings.Contains(err.Error(), "NotFound") { + return false, nil + } + // Some other error occurred + return false, err + } + // No error means CRD exists + return true, nil +} + +// installCRDFromYAML applies a CRD from embedded YAML definition +func installCRDFromYAML(ctx context.Context, k8s *openshift.Client, crdYAML string) error { + // Parse YAML into unstructured object + obj := &unstructured.Unstructured{} + decoder := yaml.NewYAMLOrJSONDecoder(strings.NewReader(crdYAML), 4096) + if err := decoder.Decode(obj); err != nil { + return fmt.Errorf("failed to decode YAML: %w", err) + } + + // Use the k8s client to create the CRD + if err := k8s.Create(ctx, obj); err != nil { + return fmt.Errorf("failed to create CRD: %w", err) + } + + GinkgoLogr.V(1).Info("CRD installed successfully", "name", obj.GetName()) + return nil +} + +// OIDCCredentials holds OIDC credentials for RMO +type OIDCCredentials struct { + ClientID string + ClientSecret string + IssuerURL string + ProbeAPIURL string +} + +// getOrCreateOIDCCredentials fetches credentials from ConfigMap first, then environment variables +// If ConfigMap doesn't exist or has incomplete credentials, falls back to environment variables +// and creates/updates the ConfigMap with those credentials +func getOrCreateOIDCCredentials(ctx context.Context, k8s *openshift.Client, environment string) (*OIDCCredentials, error) { + // First, try to get credentials from existing ConfigMap + creds, err := getOIDCCredentialsFromConfigMap(ctx, k8s) + if err == nil && creds != nil { + GinkgoLogr.Info("Using OIDC credentials from existing ConfigMap") + return creds, nil + } + if err != nil && !k8serrors.IsNotFound(err) { + GinkgoLogr.Info("Warning: failed to check ConfigMap, falling back to environment variables", "error", err) + } + + // ConfigMap doesn't exist or is incomplete, fall back to environment variables + GinkgoLogr.Info("ConfigMap not found or incomplete, fetching credentials from environment variables") + creds, err = getOIDCCredentials(ctx, environment) + if err != nil { + return nil, err + } + + // Create/update ConfigMap with credentials from environment variables + GinkgoLogr.Info("Creating/updating RMO config ConfigMap with credentials from environment variables") + if err := createRMOConfigMap(ctx, k8s, creds); err != nil { + return nil, fmt.Errorf("failed to create/update ConfigMap: %w", err) + } + + return creds, nil +} + +// getOIDCCredentialsFromConfigMap fetches RMO credentials from the ConfigMap +// Returns nil if ConfigMap doesn't exist or has incomplete credentials +func getOIDCCredentialsFromConfigMap(ctx context.Context, k8s *openshift.Client) (*OIDCCredentials, error) { + existingCM := &corev1.ConfigMap{} + err := k8s.Get(ctx, "route-monitor-operator-config", "openshift-route-monitor-operator", existingCM) + if err != nil { + return nil, err + } + + // Check if all required fields are present + clientID := existingCM.Data["oidc-client-id"] + clientSecret := existingCM.Data["oidc-client-secret"] + issuerURL := existingCM.Data["oidc-issuer-url"] + probeAPIURL := existingCM.Data["probe-api-url"] + + if clientID == "" || clientSecret == "" || issuerURL == "" { + GinkgoLogr.Info("ConfigMap exists but has incomplete credentials", "has_client_id", clientID != "", "has_client_secret", clientSecret != "", "has_issuer_url", issuerURL != "") + return nil, fmt.Errorf("incomplete credentials in ConfigMap") + } + + return &OIDCCredentials{ + ClientID: clientID, + ClientSecret: clientSecret, + IssuerURL: issuerURL, + ProbeAPIURL: probeAPIURL, + }, nil +} + +// getOIDCCredentials fetches RMO credentials from environment variables +// Uses EXTERNAL_SECRET_ env vars (same for both local testing and CI/CD via SDCICD-1739) +func getOIDCCredentials(ctx context.Context, environment string) (*OIDCCredentials, error) { + // Check EXTERNAL_SECRET_ environment variables (used for both local and osde2e/CI) + // In CI/CD, these are auto-loaded by osde2e from app-interface (see SDCICD-1739) + // For local testing, export these same variables + if clientID := os.Getenv("EXTERNAL_SECRET_OIDC_CLIENT_ID"); clientID != "" { + GinkgoLogr.Info("Using OIDC credentials from EXTERNAL_SECRET_ environment variables") + return &OIDCCredentials{ + ClientID: clientID, + ClientSecret: os.Getenv("EXTERNAL_SECRET_OIDC_CLIENT_SECRET"), + IssuerURL: os.Getenv("EXTERNAL_SECRET_OIDC_ISSUER_URL"), + ProbeAPIURL: os.Getenv("PROBE_API_URL"), + }, nil + } + + // No credentials found + return nil, fmt.Errorf(`no OIDC credentials found in environment variables + +Please set the following environment variables: + export EXTERNAL_SECRET_OIDC_CLIENT_ID="your-client-id" + export EXTERNAL_SECRET_OIDC_CLIENT_SECRET="your-client-secret" + export EXTERNAL_SECRET_OIDC_ISSUER_URL="your-issuer-url" + +Optional (will auto-detect based on environment if not set): + export PROBE_API_URL="https://rhobs.us-east-1-0.api.stage.openshift.com/api/metrics/v1/hcp/probes" + +Note: In osde2e/CI, the EXTERNAL_SECRET_* variables are automatically loaded from app-interface (SDCICD-1739).`) +} + +// createRMOConfigMap creates the RMO config ConfigMap with OIDC credentials +func createRMOConfigMap(ctx context.Context, k8s *openshift.Client, creds *OIDCCredentials) error { + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "route-monitor-operator-config", + Namespace: "openshift-route-monitor-operator", + }, + Data: map[string]string{ + "probe-api-url": creds.ProbeAPIURL, + "oidc-client-id": creds.ClientID, + "oidc-client-secret": creds.ClientSecret, + "oidc-issuer-url": creds.IssuerURL, + }, + } + + // Check if ConfigMap already exists + existingCM := &corev1.ConfigMap{} + err := k8s.Get(ctx, "route-monitor-operator-config", "openshift-route-monitor-operator", existingCM) + if err == nil { + // ConfigMap exists, update it + GinkgoLogr.Info("ConfigMap already exists, updating", "name", "route-monitor-operator-config") + configMap.ResourceVersion = existingCM.ResourceVersion + if err := k8s.Update(ctx, configMap); err != nil { + return fmt.Errorf("failed to update ConfigMap: %w", err) + } + return nil + } else if !k8serrors.IsNotFound(err) { + return fmt.Errorf("failed to check for existing ConfigMap: %w", err) + } + + // ConfigMap doesn't exist, create it + GinkgoLogr.Info("Creating RMO config ConfigMap with OIDC credentials", "namespace", "openshift-route-monitor-operator") + if err := k8s.Create(ctx, configMap); err != nil { + return fmt.Errorf("failed to create ConfigMap: %w", err) + } + + return nil +} + +// labelTestClusterAsManagementCluster labels the cluster the test is running on as a management cluster +func labelTestClusterAsManagementCluster(ctx context.Context, k8s *openshift.Client) error { + GinkgoLogr.Info("Looking for ClusterDeployment to label as management-cluster") + + // List all ClusterDeployments to find the one for this cluster + cdList := &unstructured.UnstructuredList{} + cdList.SetAPIVersion("hive.openshift.io/v1") + cdList.SetKind("ClusterDeploymentList") + + err := k8s.List(ctx, cdList) + if err != nil { + return fmt.Errorf("failed to list ClusterDeployments: %w", err) + } + + if len(cdList.Items) == 0 { + return fmt.Errorf("no ClusterDeployments found in cluster") + } + + // Label all ClusterDeployments we find (typically there should only be one for the test cluster) + for idx := range cdList.Items { + cd := &cdList.Items[idx] + + // Add the management-cluster label + labels := cd.GetLabels() + if labels == nil { + labels = make(map[string]string) + } + labels["ext-hypershift.openshift.io/cluster-type"] = "management-cluster" + cd.SetLabels(labels) + + // Update the ClusterDeployment + err = k8s.Update(ctx, cd) + if err != nil { + GinkgoLogr.Info("Warning: failed to update ClusterDeployment", + "name", cd.GetName(), + "namespace", cd.GetNamespace(), + "error", err) + continue + } + + GinkgoLogr.Info("Successfully labeled ClusterDeployment as management-cluster", + "name", cd.GetName(), + "namespace", cd.GetNamespace()) + } + + return nil +} From c409f2bf4e7cf33b75462c53958899d29a14c62e Mon Sep 17 00:00:00 2001 From: erosente Date: Mon, 9 Feb 2026 14:28:38 -0500 Subject: [PATCH 02/12] Clean up unused labels and functions that remained after troubleshooting. --- test/e2e/route_monitor_operator_tests.go | 102 +++-------------------- 1 file changed, 12 insertions(+), 90 deletions(-) diff --git a/test/e2e/route_monitor_operator_tests.go b/test/e2e/route_monitor_operator_tests.go index ddc30cdb..a9214ec1 100644 --- a/test/e2e/route_monitor_operator_tests.go +++ b/test/e2e/route_monitor_operator_tests.go @@ -29,9 +29,9 @@ import ( corev1 "k8s.io/api/core/v1" kubev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/labels" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/intstr" @@ -423,26 +423,15 @@ var _ = Describe("RHOBS Synthetic Monitoring", Ordered, func() { environment = provider.Environment() // Returns "int", "stage", or "prod" } - // Fetch OIDC credentials from environment variables - By("fetching OIDC credentials") - - // Log which credential source is available for debugging - if os.Getenv("EXTERNAL_SECRET_OIDC_CLIENT_ID") != "" { - GinkgoLogr.Info("EXTERNAL_SECRET_ credentials detected") - } else { - GinkgoLogr.Info("No EXTERNAL_SECRET_ credentials found, test will fail") - } - // Get OIDC credentials from ConfigMap first, fall back to env vars if needed // This checks ConfigMap first, then environment variables, then creates/updates ConfigMap By("getting OIDC credentials from ConfigMap or environment variables") - creds, err := getOrCreateOIDCCredentials(ctx, k8s, environment) + oidcCredentials, err = getOrCreateOIDCCredentials(ctx, k8s, environment) Expect(err).ShouldNot(HaveOccurred(), "failed to fetch credentials") - oidcCredentials = creds // Store for use in listRHOBSProbes // Set RHOBS API URL (from credentials or environment) - if creds.ProbeAPIURL != "" { - rhobsAPIURL = creds.ProbeAPIURL + if oidcCredentials.ProbeAPIURL != "" { + rhobsAPIURL = oidcCredentials.ProbeAPIURL } else { rhobsAPIURL = getRHOBSAPIURL(environment) } @@ -452,19 +441,12 @@ var _ = Describe("RHOBS Synthetic Monitoring", Ordered, func() { pollingDuration = 3 * time.Minute probeActivationTimeout = 5 * time.Minute - // Label the test cluster as a management cluster - By("labeling test cluster as management-cluster") - err = labelTestClusterAsManagementCluster(ctx, k8s) - if err != nil { - GinkgoLogr.Info("Warning: could not label test cluster as management cluster", "error", err) - } - GinkgoLogr.Info("RHOBS Synthetic Monitoring test suite initialized", "environment", environment, "probeAPIURL", rhobsAPIURL, "tenant", rhobsTenant, "testNamespace", testNamespace, - "oidcConfigured", creds.ClientID != "") + "oidcConfigured", oidcCredentials.ClientID != "") }) // Phase 1 Test 1: Verify RHOBS monitoring configuration @@ -511,7 +493,7 @@ var _ = Describe("RHOBS Synthetic Monitoring", Ordered, func() { err := k8s.Create(ctx, ns) Expect(err).ShouldNot(HaveOccurred(), "failed to create namespace") -By(fmt.Sprintf("creating public HostedControlPlane with cluster ID: %s", clusterID)) + By(fmt.Sprintf("creating public HostedControlPlane with cluster ID: %s", clusterID)) hcp := createMCStyleHCP(clusterID, hcpName, namespace, hypershiftv1beta1.Public) err = k8s.Create(ctx, hcp) @@ -588,7 +570,7 @@ By(fmt.Sprintf("creating public HostedControlPlane with cluster ID: %s", cluster GinkgoLogr.Info("Warning: failed to delete HCP", "error", err) } -By("verifying probe is deleted from RHOBS API") + By("verifying probe is deleted from RHOBS API") err = wait.PollUntilContextTimeout(ctx, 5*time.Second, 2*time.Minute, false, func(ctx context.Context) (bool, error) { probes, err := listRHOBSProbes(rhobsAPIURL, fmt.Sprintf("cluster-id=%s", clusterID), oidcCredentials) if err != nil { @@ -721,7 +703,7 @@ By("verifying probe is deleted from RHOBS API") err := k8s.Create(ctx, ns) Expect(err).ShouldNot(HaveOccurred(), "failed to create namespace") -By(fmt.Sprintf("creating HostedControlPlane for deletion test with cluster ID: %s", clusterID)) + By(fmt.Sprintf("creating HostedControlPlane for deletion test with cluster ID: %s", clusterID)) hcp := createMCStyleHCP(clusterID, hcpName, namespace, hypershiftv1beta1.Public) err = k8s.Create(ctx, hcp) @@ -787,7 +769,7 @@ By(fmt.Sprintf("creating HostedControlPlane for deletion test with cluster ID: % err = wait.PollUntilContextTimeout(ctx, 2*time.Second, 1*time.Minute, false, func(ctx context.Context) (bool, error) { hcpCheck := &hypershiftv1beta1.HostedControlPlane{} err := k8s.Get(ctx, hcpName, namespace, hcpCheck) - if err != nil && isNotFoundError(err) { + if err != nil && k8serrors.IsNotFound(err) { GinkgoLogr.Info("HostedControlPlane fully deleted", "name", hcpName) return true, nil } @@ -946,18 +928,6 @@ func getRHOBSAPIURL(environment string) string { } } -func contains(s, substr string) bool { - return strings.Contains(s, substr) -} - -func isAlreadyExistsError(err error) bool { - return k8serrors.IsAlreadyExists(err) -} - -func isNotFoundError(err error) bool { - return k8serrors.IsNotFound(err) -} - func setHostedControlPlaneAvailable(ctx context.Context, k8s *openshift.Client, hcp *hypershiftv1beta1.HostedControlPlane) error { // Update the status to simulate HyperShift controller behavior // RMO only processes HCPs with Available=true status @@ -1002,7 +972,7 @@ func setHostedControlPlaneAvailable(ctx context.Context, k8s *openshift.Client, // Check if it's a conflict error if strings.Contains(err.Error(), "object has been modified") || - strings.Contains(err.Error(), "the object has been modified") { + strings.Contains(err.Error(), "the object has been modified") { // Retry after a short delay GinkgoLogr.V(1).Info("HCP was modified, retrying status update", "attempt", attempt+1, "maxRetries", maxRetries) time.Sleep(100 * time.Millisecond) @@ -1131,8 +1101,8 @@ func restartRMODeployment(ctx context.Context, k8s *openshift.Client) error { // ensureCRDsInstalled checks if required CRDs exist and installs them if missing func ensureCRDsInstalled(ctx context.Context, k8s *openshift.Client) error { crdsToInstall := []struct { - name string - yamlDef string + name string + yamlDef string }{ { name: "hostedcontrolplanes.hypershift.openshift.io", @@ -1377,51 +1347,3 @@ func createRMOConfigMap(ctx context.Context, k8s *openshift.Client, creds *OIDCC return nil } - -// labelTestClusterAsManagementCluster labels the cluster the test is running on as a management cluster -func labelTestClusterAsManagementCluster(ctx context.Context, k8s *openshift.Client) error { - GinkgoLogr.Info("Looking for ClusterDeployment to label as management-cluster") - - // List all ClusterDeployments to find the one for this cluster - cdList := &unstructured.UnstructuredList{} - cdList.SetAPIVersion("hive.openshift.io/v1") - cdList.SetKind("ClusterDeploymentList") - - err := k8s.List(ctx, cdList) - if err != nil { - return fmt.Errorf("failed to list ClusterDeployments: %w", err) - } - - if len(cdList.Items) == 0 { - return fmt.Errorf("no ClusterDeployments found in cluster") - } - - // Label all ClusterDeployments we find (typically there should only be one for the test cluster) - for idx := range cdList.Items { - cd := &cdList.Items[idx] - - // Add the management-cluster label - labels := cd.GetLabels() - if labels == nil { - labels = make(map[string]string) - } - labels["ext-hypershift.openshift.io/cluster-type"] = "management-cluster" - cd.SetLabels(labels) - - // Update the ClusterDeployment - err = k8s.Update(ctx, cd) - if err != nil { - GinkgoLogr.Info("Warning: failed to update ClusterDeployment", - "name", cd.GetName(), - "namespace", cd.GetNamespace(), - "error", err) - continue - } - - GinkgoLogr.Info("Successfully labeled ClusterDeployment as management-cluster", - "name", cd.GetName(), - "namespace", cd.GetNamespace()) - } - - return nil -} From 326dbb7a12fcbfa34fef8afa96c93d43569d6d5b Mon Sep 17 00:00:00 2001 From: erosente Date: Tue, 10 Feb 2026 13:55:12 -0500 Subject: [PATCH 03/12] switched healthchecks to feature flags. --- config/manager/manager.yaml | 3 + controllers/hostedcontrolplane/healthcheck.go | 12 ++-- .../hostedcontrolplane/hostedcontrolplane.go | 15 +++-- .../hostedcontrolplane_test.go | 4 +- controllers/hostedcontrolplane/synthetics.go | 13 +++-- go.mod | 2 +- go.sum | 4 +- hack/olm-registry/rmo-config-template.yaml | 5 ++ main.go | 55 +++++++++++++------ test/e2e/route_monitor_operator_tests.go | 9 +-- 10 files changed, 77 insertions(+), 45 deletions(-) diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index dd7fcb47..6c34673d 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -48,6 +48,7 @@ spec: - --oidc-client-secret=$(OIDC_CLIENT_SECRET) - --oidc-issuer-url=$(OIDC_ISSUER_URL) - --only-public-clusters=$(ONLY_PUBLIC_CLUSTERS) + - --skip-infrastructure-tests=$(SKIP_INFRASTRUCTURE_TESTS) env: - name: LOG_LEVEL # level 1 is debug, so when we want to raise the level we can @@ -70,6 +71,8 @@ spec: value: "" # Default to empty - can be overridden in app-interface - name: ONLY_PUBLIC_CLUSTERS value: "false" # Default to false - can be overridden in app-interface + - name: SKIP_INFRASTRUCTURE_TESTS + value: "false" # Default to false - can be overridden in app-interface image: controller:latest name: manager securityContext: diff --git a/controllers/hostedcontrolplane/healthcheck.go b/controllers/hostedcontrolplane/healthcheck.go index 868e9738..6191bc96 100644 --- a/controllers/hostedcontrolplane/healthcheck.go +++ b/controllers/hostedcontrolplane/healthcheck.go @@ -63,9 +63,9 @@ const ( // HCP namespace, and this process will be restarted. Additionally, should healthchecking need to be skipped for any reason, the annotation // "routemonitor.managed.openshift.io/successful-healthchecks" can be added-to/edited-on the configmap with a large number (ie - 999) to bypass // this functionality -func (r *HostedControlPlaneReconciler) hcpReady(ctx context.Context, hostedcontrolplane *hypershiftv1beta1.HostedControlPlane) (bool, error) { - // Skip health check for test HCPs (e.g., osde2e tests without real kube-apiserver) - if _, osde2eTesting := hostedcontrolplane.Annotations["routemonitor.openshift.io/osde2e-testing"]; osde2eTesting { +func (r *HostedControlPlaneReconciler) hcpReady(ctx context.Context, hostedcontrolplane *hypershiftv1beta1.HostedControlPlane, cfg RHOBSConfig) (bool, error) { + // Skip health check for test environments (e.g., osde2e tests without real kube-apiserver) + if cfg.SkipInfrastructureTests { return true, nil } @@ -232,9 +232,9 @@ func olderThan(obj metav1.Object, age time.Duration) bool { } // isVpcEndpointReady checks if the VPC Endpoint associated with the HostedControlPlane is ready. -func (r *HostedControlPlaneReconciler) isVpcEndpointReady(ctx context.Context, hostedcontrolplane *hypershiftv1beta1.HostedControlPlane) (bool, error) { - // Skip VPC endpoint check for test HCPs (e.g., osde2e tests without real VPC infrastructure) - if _, osde2eTesting := hostedcontrolplane.Annotations["routemonitor.openshift.io/osde2e-testing"]; osde2eTesting { +func (r *HostedControlPlaneReconciler) isVpcEndpointReady(ctx context.Context, hostedcontrolplane *hypershiftv1beta1.HostedControlPlane, cfg RHOBSConfig) (bool, error) { + // Skip VPC endpoint check for test environments (e.g., osde2e tests without real VPC infrastructure) + if cfg.SkipInfrastructureTests { return true, nil } diff --git a/controllers/hostedcontrolplane/hostedcontrolplane.go b/controllers/hostedcontrolplane/hostedcontrolplane.go index ff5c511c..8991630b 100644 --- a/controllers/hostedcontrolplane/hostedcontrolplane.go +++ b/controllers/hostedcontrolplane/hostedcontrolplane.go @@ -133,6 +133,9 @@ func (r *HostedControlPlaneReconciler) getRHOBSConfig(ctx context.Context) RHOBS if strings.TrimSpace(configMap.Data["only-public-clusters"]) == "true" { cfg.OnlyPublicClusters = true } + if strings.TrimSpace(configMap.Data["skip-infrastructure-tests"]) == "true" { + cfg.SkipInfrastructureTests = true + } return cfg } @@ -261,7 +264,7 @@ func (r *HostedControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.R } // Ensure cluster is ready to be monitored before deploying any probing objects - hcpReady, err := r.hcpReady(ctx, hostedcontrolplane) + hcpReady, err := r.hcpReady(ctx, hostedcontrolplane, rhobsConfig) if err != nil { log.Error(err, "HCP readiness check failed") return utilreconcile.RequeueWith(fmt.Errorf("HCP readiness check failed: %v", err)) @@ -271,7 +274,7 @@ func (r *HostedControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.R return utilreconcile.RequeueAfter(healthcheckIntervalSeconds * time.Second), nil } - vpcEndpointReady, err := r.isVpcEndpointReady(ctx, hostedcontrolplane) + vpcEndpointReady, err := r.isVpcEndpointReady(ctx, hostedcontrolplane, rhobsConfig) if err != nil { log.Error(err, "VPC Endpoint check failed") return utilreconcile.RequeueWith(err) @@ -283,7 +286,7 @@ func (r *HostedControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.R // Cluster ready - deploy kube-apiserver monitoring objects log.Info("Deploying internal monitoring objects") - err = r.deployInternalMonitoringObjects(ctx, log, hostedcontrolplane) + err = r.deployInternalMonitoringObjects(ctx, log, hostedcontrolplane, rhobsConfig) if err != nil { log.Error(err, "failed to deploy internal monitoring components") return utilreconcile.RequeueWith(err) @@ -322,9 +325,9 @@ func (r *HostedControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.R } // deployInternalMonitoringObjects creates or updates the objects needed to monitor the kube-apiserver using cluster-internal routes -func (r *HostedControlPlaneReconciler) deployInternalMonitoringObjects(ctx context.Context, log logr.Logger, hostedcontrolplane *hypershiftv1beta1.HostedControlPlane) error { - // Skip internal monitoring for test HCPs (e.g., osde2e tests without real kube-apiserver infrastructure) - if _, osde2eTesting := hostedcontrolplane.Annotations["routemonitor.openshift.io/osde2e-testing"]; osde2eTesting { +func (r *HostedControlPlaneReconciler) deployInternalMonitoringObjects(ctx context.Context, log logr.Logger, hostedcontrolplane *hypershiftv1beta1.HostedControlPlane, cfg RHOBSConfig) error { + // Skip internal monitoring for test environments (e.g., osde2e tests without real kube-apiserver infrastructure) + if cfg.SkipInfrastructureTests { return nil } diff --git a/controllers/hostedcontrolplane/hostedcontrolplane_test.go b/controllers/hostedcontrolplane/hostedcontrolplane_test.go index 4c25f1e2..dc33461b 100644 --- a/controllers/hostedcontrolplane/hostedcontrolplane_test.go +++ b/controllers/hostedcontrolplane/hostedcontrolplane_test.go @@ -571,7 +571,7 @@ func TestHostedControlPlaneReconciler_deployInternalMonitoringObjects(t *testing for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := newTestReconciler(t, tt.objs...) - err := r.deployInternalMonitoringObjects(tt.args.ctx, tt.args.log, tt.args.hostedcontrolplane) + err := r.deployInternalMonitoringObjects(tt.args.ctx, tt.args.log, tt.args.hostedcontrolplane, RHOBSConfig{}) passed, reason := tt.eval(err, r) if !passed { t.Errorf("HostedControlPlaneReconciler.deployInternalMonitoringObjects() did not pass due to = %s", reason) @@ -827,7 +827,7 @@ func TestIsVpcEndpointReady(t *testing.T) { } // Test the function - result, err := r.isVpcEndpointReady(context.Background(), hcp) + result, err := r.isVpcEndpointReady(context.Background(), hcp, RHOBSConfig{}) // Validate the results if result != tt.expectedResult { diff --git a/controllers/hostedcontrolplane/synthetics.go b/controllers/hostedcontrolplane/synthetics.go index 870c4397..ed505f80 100644 --- a/controllers/hostedcontrolplane/synthetics.go +++ b/controllers/hostedcontrolplane/synthetics.go @@ -251,12 +251,13 @@ func (r *HostedControlPlaneReconciler) deleteDynatraceHttpMonitorResources(dynat // RHOBSConfig holds RHOBS API configuration type RHOBSConfig struct { - ProbeAPIURL string - Tenant string - OIDCClientID string - OIDCClientSecret string - OIDCIssuerURL string - OnlyPublicClusters bool + ProbeAPIURL string + Tenant string + OIDCClientID string + OIDCClientSecret string + OIDCIssuerURL string + OnlyPublicClusters bool + SkipInfrastructureTests bool } // ensureRHOBSProbe ensures that a RHOBS probe exists for the HostedControlPlane diff --git a/go.mod b/go.mod index c2cd1a2a..7f5eebb3 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/openshift/api v0.0.0-20250409155250-8fcc4e71758a github.com/openshift/aws-vpce-operator v0.0.0-20241211114942-1daecf2e4364 github.com/openshift/hypershift/api v0.0.0-20241204143212-857ccab4fd7c - github.com/openshift/osde2e v0.0.0-20260202205050-a418c410e9bc + github.com/openshift/osde2e v0.0.0-20260209164730-1a4541c8e2b1 github.com/openshift/osde2e-common v0.0.0-20260126213857-862206787cf9 github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.64.0 github.com/prometheus/client_golang v1.23.2 diff --git a/go.sum b/go.sum index a8278569..8a417424 100644 --- a/go.sum +++ b/go.sum @@ -230,8 +230,8 @@ github.com/openshift/aws-vpce-operator v0.0.0-20241211114942-1daecf2e4364 h1:wrR github.com/openshift/aws-vpce-operator v0.0.0-20241211114942-1daecf2e4364/go.mod h1:+/t18O2NwRYEzWbi76OImNMivJXi6bbFVG3ODGeQjMk= github.com/openshift/hypershift/api v0.0.0-20241204143212-857ccab4fd7c h1:NBH3no8kaW7Hem/mf/0lIfZZEIeaNTwo7XfTqUm2cJc= github.com/openshift/hypershift/api v0.0.0-20241204143212-857ccab4fd7c/go.mod h1:K1e56UPfFQaO8BYXbVS1TbNqb6rmPo+fc6Jxjv+w0Xo= -github.com/openshift/osde2e v0.0.0-20260202205050-a418c410e9bc h1:W6qCZPI/2f64vRbDbUH0wrVzXTcjV5UBBTrIfD/giO8= -github.com/openshift/osde2e v0.0.0-20260202205050-a418c410e9bc/go.mod h1:VW4cnPpR92sfzwuhzPWatXYAo7/ZgyjYhCDPPONjUfk= +github.com/openshift/osde2e v0.0.0-20260209164730-1a4541c8e2b1 h1:2tqxJRv06Psmb1r9CYHEhjK0FW/uZmH6ZorQn9wJHT4= +github.com/openshift/osde2e v0.0.0-20260209164730-1a4541c8e2b1/go.mod h1:VW4cnPpR92sfzwuhzPWatXYAo7/ZgyjYhCDPPONjUfk= github.com/openshift/osde2e-common v0.0.0-20260126213857-862206787cf9 h1:oUYQpPED774xWW41aRfrhvPSEcIN3FR6XeePOVqcljk= github.com/openshift/osde2e-common v0.0.0-20260126213857-862206787cf9/go.mod h1:nMeOffePy+qdcCXuwTw5YcMMn/cTfjYQf7KRLjhKweo= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= diff --git a/hack/olm-registry/rmo-config-template.yaml b/hack/olm-registry/rmo-config-template.yaml index b463b4fe..f79a7a0e 100644 --- a/hack/olm-registry/rmo-config-template.yaml +++ b/hack/olm-registry/rmo-config-template.yaml @@ -42,6 +42,10 @@ parameters: - name: ONLY_PUBLIC_CLUSTERS value: "" required: false +- name: SKIP_INFRASTRUCTURE_TESTS + value: "false" + required: false + description: "Skip infrastructure health checks for test environments" objects: - apiVersion: hive.openshift.io/v1 @@ -71,3 +75,4 @@ objects: oidc-client-secret: ${OIDC_CLIENT_SECRET} oidc-issuer-url: ${OIDC_ISSUER_URL} only-public-clusters: ${ONLY_PUBLIC_CLUSTERS} + skip-infrastructure-tests: ${SKIP_INFRASTRUCTURE_TESTS} diff --git a/main.go b/main.go index f557d963..a7f18d74 100644 --- a/main.go +++ b/main.go @@ -98,6 +98,7 @@ func main() { var oidcClientSecret string var oidcIssuerURL string var onlyPublicClusters bool + var skipInfrastructureTests bool flag.StringVar(&blackboxExporterImage, "blackbox-image", "quay.io/prometheus/blackbox-exporter@sha256:b04a9fef4fa086a02fc7fcd8dcdbc4b7b35cc30cdee860fdc6a19dd8b208d63e", "The image that will be used for the blackbox-exporter deployment") flag.StringVar(&blackboxExporterNamespace, "blackbox-namespace", config.OperatorNamespace, "Blackbox-exporter deployment will reside on this Namespace") @@ -107,6 +108,7 @@ func main() { flag.StringVar(&oidcClientSecret, "oidc-client-secret", "", "OIDC client secret for RHOBS API authentication. When empty, no OIDC authentication is used.") flag.StringVar(&oidcIssuerURL, "oidc-issuer-url", "", "OIDC issuer URL for RHOBS API authentication. When empty, no OIDC authentication is used.") flag.BoolVar(&onlyPublicClusters, "only-public-clusters", false, "When true, only create RHOBS probes for public (non-private) HostedClusters. Defaults to false (process all clusters).") + flag.BoolVar(&skipInfrastructureTests, "skip-infrastructure-tests", false, "When true, skip infrastructure health checks (HCP ready, VPC endpoint ready) for test environments. Defaults to false.") opts := zap.Options{} opts.BindFlags(flag.CommandLine) @@ -171,6 +173,14 @@ func main() { flagParams = append(flagParams, "only-public-clusters") } + if configData.SkipInfrastructureTests { + setupLog.V(1).Info("Using skip-infrastructure-tests from ConfigMap", "skipInfrastructureTests", configData.SkipInfrastructureTests) + skipInfrastructureTests = configData.SkipInfrastructureTests + configMapParams = append(configMapParams, "skip-infrastructure-tests") + } else { + flagParams = append(flagParams, "skip-infrastructure-tests") + } + // Summarize configuration sources if len(configMapParams) > 0 && len(flagParams) > 0 { setupLog.Info("Using mixed configuration sources", @@ -273,12 +283,13 @@ func main() { if enableHCP { rhobsConfig := hostedcontrolplane.RHOBSConfig{ - ProbeAPIURL: probeAPIURL, - Tenant: probeTenant, - OIDCClientID: oidcClientID, - OIDCClientSecret: oidcClientSecret, - OIDCIssuerURL: oidcIssuerURL, - OnlyPublicClusters: onlyPublicClusters, + ProbeAPIURL: probeAPIURL, + Tenant: probeTenant, + OIDCClientID: oidcClientID, + OIDCClientSecret: oidcClientSecret, + OIDCIssuerURL: oidcIssuerURL, + OnlyPublicClusters: onlyPublicClusters, + SkipInfrastructureTests: skipInfrastructureTests, } hostedControlPlaneReconciler := hostedcontrolplane.NewHostedControlPlaneReconciler(mgr, rhobsConfig) if err = hostedControlPlaneReconciler.SetupWithManager(mgr); err != nil { @@ -331,12 +342,13 @@ func shouldEnableHCP() (bool, error) { // OperatorConfig holds configuration values from ConfigMap type OperatorConfig struct { - ProbeAPIURL string - ProbeTenant string - OIDCClientID string - OIDCClientSecret string - OIDCIssuerURL string - OnlyPublicClusters bool + ProbeAPIURL string + ProbeTenant string + OIDCClientID string + OIDCClientSecret string + OIDCIssuerURL string + OnlyPublicClusters bool + SkipInfrastructureTests bool } // getConfigFromConfigMap reads configuration from the route-monitor-operator-config ConfigMap @@ -368,12 +380,13 @@ func getConfigFromConfigMap() (*OperatorConfig, error) { // Extract configuration values, trimming whitespace cfg := &OperatorConfig{ - ProbeAPIURL: strings.TrimSpace(configMap.Data["probe-api-url"]), - ProbeTenant: strings.TrimSpace(configMap.Data["probe-tenant"]), - OIDCClientID: strings.TrimSpace(configMap.Data["oidc-client-id"]), - OIDCClientSecret: strings.TrimSpace(configMap.Data["oidc-client-secret"]), - OIDCIssuerURL: strings.TrimSpace(configMap.Data["oidc-issuer-url"]), - OnlyPublicClusters: strings.TrimSpace(configMap.Data["only-public-clusters"]) == "true", + ProbeAPIURL: strings.TrimSpace(configMap.Data["probe-api-url"]), + ProbeTenant: strings.TrimSpace(configMap.Data["probe-tenant"]), + OIDCClientID: strings.TrimSpace(configMap.Data["oidc-client-id"]), + OIDCClientSecret: strings.TrimSpace(configMap.Data["oidc-client-secret"]), + OIDCIssuerURL: strings.TrimSpace(configMap.Data["oidc-issuer-url"]), + OnlyPublicClusters: strings.TrimSpace(configMap.Data["only-public-clusters"]) == "true", + SkipInfrastructureTests: strings.TrimSpace(configMap.Data["skip-infrastructure-tests"]) == "true", } // Log detailed information about what was found in the ConfigMap @@ -416,6 +429,12 @@ func getConfigFromConfigMap() (*OperatorConfig, error) { missingParams = append(missingParams, "only-public-clusters") } + if configMap.Data["skip-infrastructure-tests"] != "" { + foundParams = append(foundParams, "skip-infrastructure-tests") + } else { + missingParams = append(missingParams, "skip-infrastructure-tests") + } + setupLog.Info("ConfigMap found and processed", "configmap", configMapName, "namespace", config.OperatorNamespace, diff --git a/test/e2e/route_monitor_operator_tests.go b/test/e2e/route_monitor_operator_tests.go index a9214ec1..92d769e6 100644 --- a/test/e2e/route_monitor_operator_tests.go +++ b/test/e2e/route_monitor_operator_tests.go @@ -1317,10 +1317,11 @@ func createRMOConfigMap(ctx context.Context, k8s *openshift.Client, creds *OIDCC Namespace: "openshift-route-monitor-operator", }, Data: map[string]string{ - "probe-api-url": creds.ProbeAPIURL, - "oidc-client-id": creds.ClientID, - "oidc-client-secret": creds.ClientSecret, - "oidc-issuer-url": creds.IssuerURL, + "probe-api-url": creds.ProbeAPIURL, + "oidc-client-id": creds.ClientID, + "oidc-client-secret": creds.ClientSecret, + "oidc-issuer-url": creds.IssuerURL, + "skip-infrastructure-tests": "true", }, } From 2163fe479d6d8c6b645d748703e52841236b7249 Mon Sep 17 00:00:00 2001 From: erosente Date: Tue, 10 Feb 2026 15:58:51 -0500 Subject: [PATCH 04/12] make generate results and update rhobs api paths. --- ...perator-controller-manager.Deployment.yaml | 3 ++ pkg/rhobs/client_test.go | 28 +++++++++---------- test/e2e/route_monitor_operator_tests.go | 8 +++--- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/deploy/route-monitor-operator-controller-manager.Deployment.yaml b/deploy/route-monitor-operator-controller-manager.Deployment.yaml index 673b1ac6..b71ab4ef 100644 --- a/deploy/route-monitor-operator-controller-manager.Deployment.yaml +++ b/deploy/route-monitor-operator-controller-manager.Deployment.yaml @@ -44,6 +44,7 @@ spec: - --oidc-client-secret=$(OIDC_CLIENT_SECRET) - --oidc-issuer-url=$(OIDC_ISSUER_URL) - --only-public-clusters=$(ONLY_PUBLIC_CLUSTERS) + - --skip-infrastructure-tests=$(SKIP_INFRASTRUCTURE_TESTS) command: - /manager env: @@ -65,6 +66,8 @@ spec: value: "" - name: ONLY_PUBLIC_CLUSTERS value: "false" + - name: SKIP_INFRASTRUCTURE_TESTS + value: "false" image: route-monitor-operator:latest livenessProbe: httpGet: diff --git a/pkg/rhobs/client_test.go b/pkg/rhobs/client_test.go index 651b54c1..bfde0a4d 100644 --- a/pkg/rhobs/client_test.go +++ b/pkg/rhobs/client_test.go @@ -1061,13 +1061,13 @@ func TestFullURLSupport(t *testing.T) { }{ { name: "full URL with /probes endpoint", - baseURL: "https://rhobs.us-west-2.api.integration.openshift.com/api/metrics/v1/hcp/probes", - expectedURL: "https://rhobs.us-west-2.api.integration.openshift.com/api/metrics/v1/hcp/probes", + baseURL: "https://us-west-2.rhobs.api.integration.openshift.com/api/metrics/v1/hcp/probes", + expectedURL: "https://us-west-2.rhobs.api.integration.openshift.com/api/metrics/v1/hcp/probes", }, { name: "base URL without path", - baseURL: "https://rhobs.us-west-2.api.integration.openshift.com", - expectedURL: "https://rhobs.us-west-2.api.integration.openshift.com/api/metrics/v1/test-tenant/probes", + baseURL: "https://us-west-2.rhobs.api.integration.openshift.com", + expectedURL: "https://us-west-2.rhobs.api.integration.openshift.com/api/metrics/v1/test-tenant/probes", }, } @@ -1091,8 +1091,8 @@ func TestFullURLSupport(t *testing.T) { // Replace the example domain with test server for the full URL test baseURL := tt.baseURL - if strings.Contains(baseURL, "rhobs.us-west-2.api.integration.openshift.com") { - baseURL = strings.Replace(baseURL, "https://rhobs.us-west-2.api.integration.openshift.com", server.URL, 1) + if strings.Contains(baseURL, "us-west-2.rhobs.api.integration.openshift.com") { + baseURL = strings.Replace(baseURL, "https://us-west-2.rhobs.api.integration.openshift.com", server.URL, 1) } else { baseURL = server.URL } @@ -1114,7 +1114,7 @@ func TestFullURLSupport(t *testing.T) { } // Verify the URL used - expectedPath := strings.TrimPrefix(tt.expectedURL, "https://rhobs.us-west-2.api.integration.openshift.com") + expectedPath := strings.TrimPrefix(tt.expectedURL, "https://us-west-2.rhobs.api.integration.openshift.com") if !strings.HasSuffix(actualURL, expectedPath) { t.Errorf("Expected URL to end with %s, got %s", expectedPath, actualURL) } @@ -1132,15 +1132,15 @@ func TestFullURLSupportForSpecificProbe(t *testing.T) { }{ { name: "full URL with /probes endpoint for specific probe", - baseURL: "https://rhobs.us-west-2.api.integration.openshift.com/api/metrics/v1/hcp/probes", + baseURL: "https://us-west-2.rhobs.api.integration.openshift.com/api/metrics/v1/hcp/probes", clusterID: "test-cluster-123", - expectedURL: "https://rhobs.us-west-2.api.integration.openshift.com/api/metrics/v1/hcp/probes/probe-123", + expectedURL: "https://us-west-2.rhobs.api.integration.openshift.com/api/metrics/v1/hcp/probes/probe-123", }, { name: "base URL without path for specific probe", - baseURL: "https://rhobs.us-west-2.api.integration.openshift.com", + baseURL: "https://us-west-2.rhobs.api.integration.openshift.com", clusterID: "test-cluster-123", - expectedURL: "https://rhobs.us-west-2.api.integration.openshift.com/api/metrics/v1/test-tenant/probes/probe-123", + expectedURL: "https://us-west-2.rhobs.api.integration.openshift.com/api/metrics/v1/test-tenant/probes/probe-123", }, } @@ -1171,8 +1171,8 @@ func TestFullURLSupportForSpecificProbe(t *testing.T) { // Replace the example domain with test server for the full URL test baseURL := tt.baseURL - if strings.Contains(baseURL, "rhobs.us-west-2.api.integration.openshift.com") { - baseURL = strings.Replace(baseURL, "https://rhobs.us-west-2.api.integration.openshift.com", server.URL, 1) + if strings.Contains(baseURL, "us-west-2.rhobs.api.integration.openshift.com") { + baseURL = strings.Replace(baseURL, "https://us-west-2.rhobs.api.integration.openshift.com", server.URL, 1) } else { baseURL = server.URL } @@ -1186,7 +1186,7 @@ func TestFullURLSupportForSpecificProbe(t *testing.T) { } // Verify the URL used - expectedPath := strings.TrimPrefix(tt.expectedURL, "https://rhobs.us-west-2.api.integration.openshift.com") + expectedPath := strings.TrimPrefix(tt.expectedURL, "https://us-west-2.rhobs.api.integration.openshift.com") if !strings.HasSuffix(actualURL, expectedPath) { t.Errorf("Expected URL to end with %s, got %s", expectedPath, actualURL) } diff --git a/test/e2e/route_monitor_operator_tests.go b/test/e2e/route_monitor_operator_tests.go index 92d769e6..252975aa 100644 --- a/test/e2e/route_monitor_operator_tests.go +++ b/test/e2e/route_monitor_operator_tests.go @@ -916,15 +916,15 @@ func getEnvOrDefault(key, defaultValue string) string { func getRHOBSAPIURL(environment string) string { switch environment { case "int": // osde2e uses "int" for integration - return "https://rhobs.us-west-2.api.integration.openshift.com/api/metrics/v1/hcp/probes" + return "https://us-west-2.rhobs.api.integration.openshift.com/api/metrics/v1/hcp/probes" case "stage": - return "https://rhobs.us-east-1-0.api.stage.openshift.com/api/metrics/v1/hcp/probes" + return "https://us-east-1-0.rhobs.api.stage.openshift.com/api/metrics/v1/hcp/probes" case "prod": // Production RHOBS not yet available - return empty string return "" default: // Default to stage - return "https://rhobs.us-east-1-0.api.stage.openshift.com/api/metrics/v1/hcp/probes" + return "https://us-east-1-0.rhobs.api.stage.openshift.com/api/metrics/v1/hcp/probes" } } @@ -1304,7 +1304,7 @@ Please set the following environment variables: export EXTERNAL_SECRET_OIDC_ISSUER_URL="your-issuer-url" Optional (will auto-detect based on environment if not set): - export PROBE_API_URL="https://rhobs.us-east-1-0.api.stage.openshift.com/api/metrics/v1/hcp/probes" + export PROBE_API_URL="https://us-east-1-0.rhobs.api.stage.openshift.com/api/metrics/v1/hcp/probes" Note: In osde2e/CI, the EXTERNAL_SECRET_* variables are automatically loaded from app-interface (SDCICD-1739).`) } From c6417dea6f5dcf408cbeee378eb654c4f6ad2304 Mon Sep 17 00:00:00 2001 From: erosente Date: Wed, 11 Feb 2026 09:51:34 -0500 Subject: [PATCH 05/12] updated to read env var from saas file. --- test/e2e/route_monitor_operator_tests.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/e2e/route_monitor_operator_tests.go b/test/e2e/route_monitor_operator_tests.go index 252975aa..496f62a7 100644 --- a/test/e2e/route_monitor_operator_tests.go +++ b/test/e2e/route_monitor_operator_tests.go @@ -1311,6 +1311,9 @@ Note: In osde2e/CI, the EXTERNAL_SECRET_* variables are automatically loaded fro // createRMOConfigMap creates the RMO config ConfigMap with OIDC credentials func createRMOConfigMap(ctx context.Context, k8s *openshift.Client, creds *OIDCCredentials) error { + // Read skip-infrastructure-tests from environment, default to "false" + skipInfraTests := getEnvOrDefault("SKIP_INFRASTRUCTURE_TESTS", "false") + configMap := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "route-monitor-operator-config", @@ -1321,7 +1324,7 @@ func createRMOConfigMap(ctx context.Context, k8s *openshift.Client, creds *OIDCC "oidc-client-id": creds.ClientID, "oidc-client-secret": creds.ClientSecret, "oidc-issuer-url": creds.IssuerURL, - "skip-infrastructure-tests": "true", + "skip-infrastructure-tests": skipInfraTests, }, } From da20425c501507753aa1c61d767846af2c85a928 Mon Sep 17 00:00:00 2001 From: erosente Date: Wed, 11 Feb 2026 10:29:41 -0500 Subject: [PATCH 06/12] removed defined api endpoints into app-interface. --- test/e2e/route_monitor_operator_tests.go | 42 +++++++----------------- 1 file changed, 11 insertions(+), 31 deletions(-) diff --git a/test/e2e/route_monitor_operator_tests.go b/test/e2e/route_monitor_operator_tests.go index 496f62a7..b6b59a51 100644 --- a/test/e2e/route_monitor_operator_tests.go +++ b/test/e2e/route_monitor_operator_tests.go @@ -429,12 +429,9 @@ var _ = Describe("RHOBS Synthetic Monitoring", Ordered, func() { oidcCredentials, err = getOrCreateOIDCCredentials(ctx, k8s, environment) Expect(err).ShouldNot(HaveOccurred(), "failed to fetch credentials") - // Set RHOBS API URL (from credentials or environment) - if oidcCredentials.ProbeAPIURL != "" { - rhobsAPIURL = oidcCredentials.ProbeAPIURL - } else { - rhobsAPIURL = getRHOBSAPIURL(environment) - } + // Set RHOBS API URL from credentials (which comes from PROBE_API_URL env var) + rhobsAPIURL = oidcCredentials.ProbeAPIURL + Expect(rhobsAPIURL).ShouldNot(BeEmpty(), "PROBE_API_URL must be set (via app-interface in CI/CD or manually for local testing)") rhobsTenant = getEnvOrDefault("RHOBS_TENANT", "hcp") testNamespace = getEnvOrDefault("HCP_TEST_NAMESPACE", "clusters") @@ -913,21 +910,6 @@ func getEnvOrDefault(key, defaultValue string) string { return defaultValue } -func getRHOBSAPIURL(environment string) string { - switch environment { - case "int": // osde2e uses "int" for integration - return "https://us-west-2.rhobs.api.integration.openshift.com/api/metrics/v1/hcp/probes" - case "stage": - return "https://us-east-1-0.rhobs.api.stage.openshift.com/api/metrics/v1/hcp/probes" - case "prod": - // Production RHOBS not yet available - return empty string - return "" - default: - // Default to stage - return "https://us-east-1-0.rhobs.api.stage.openshift.com/api/metrics/v1/hcp/probes" - } -} - func setHostedControlPlaneAvailable(ctx context.Context, k8s *openshift.Client, hcp *hypershiftv1beta1.HostedControlPlane) error { // Update the status to simulate HyperShift controller behavior // RMO only processes HCPs with Available=true status @@ -1298,15 +1280,13 @@ func getOIDCCredentials(ctx context.Context, environment string) (*OIDCCredentia // No credentials found return nil, fmt.Errorf(`no OIDC credentials found in environment variables -Please set the following environment variables: +Please set the following environment variables or set them in the route-monitor-operator-config ConfigMap: export EXTERNAL_SECRET_OIDC_CLIENT_ID="your-client-id" export EXTERNAL_SECRET_OIDC_CLIENT_SECRET="your-client-secret" export EXTERNAL_SECRET_OIDC_ISSUER_URL="your-issuer-url" + export PROBE_API_URL="your-rhobs-api-url" -Optional (will auto-detect based on environment if not set): - export PROBE_API_URL="https://us-east-1-0.rhobs.api.stage.openshift.com/api/metrics/v1/hcp/probes" - -Note: In osde2e/CI, the EXTERNAL_SECRET_* variables are automatically loaded from app-interface (SDCICD-1739).`) +Note: In osde2e/CI, these variables (including SKIP_INFRASTRUCTURE_TESTS) are automatically loaded from app-interface (SDCICD-1739).`) } // createRMOConfigMap creates the RMO config ConfigMap with OIDC credentials @@ -1320,11 +1300,11 @@ func createRMOConfigMap(ctx context.Context, k8s *openshift.Client, creds *OIDCC Namespace: "openshift-route-monitor-operator", }, Data: map[string]string{ - "probe-api-url": creds.ProbeAPIURL, - "oidc-client-id": creds.ClientID, - "oidc-client-secret": creds.ClientSecret, - "oidc-issuer-url": creds.IssuerURL, - "skip-infrastructure-tests": skipInfraTests, + "probe-api-url": creds.ProbeAPIURL, + "oidc-client-id": creds.ClientID, + "oidc-client-secret": creds.ClientSecret, + "oidc-issuer-url": creds.IssuerURL, + "skip-infrastructure-tests": skipInfraTests, }, } From e92d65c5cba31ab3121bf46c36f31820a55f04e9 Mon Sep 17 00:00:00 2001 From: erosente Date: Wed, 11 Feb 2026 13:08:27 -0500 Subject: [PATCH 07/12] moved the environment setting to it's own function. --- test/e2e/route_monitor_operator_tests.go | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/test/e2e/route_monitor_operator_tests.go b/test/e2e/route_monitor_operator_tests.go index b6b59a51..5f3cbdeb 100644 --- a/test/e2e/route_monitor_operator_tests.go +++ b/test/e2e/route_monitor_operator_tests.go @@ -414,14 +414,7 @@ var _ = Describe("RHOBS Synthetic Monitoring", Ordered, func() { Expect(awsvpceapi.AddToScheme(k8s.GetScheme())).Should(Succeed(), "unable to register AWS VPCE scheme") // Determine environment from osde2e provider - var environment string - provider, err := providers.ClusterProvider() - if err != nil { - GinkgoLogr.Error(err, "Failed to get cluster provider, falling back to stage") - environment = "stage" - } else { - environment = provider.Environment() // Returns "int", "stage", or "prod" - } + environment := getEnvironment() // Get OIDC credentials from ConfigMap first, fall back to env vars if needed // This checks ConfigMap first, then environment variables, then creates/updates ConfigMap @@ -910,6 +903,21 @@ func getEnvOrDefault(key, defaultValue string) string { return defaultValue } +// getEnvironment determines the current environment (int, stage, prod) from osde2e provider +// Falls back to "stage" if provider cannot be determined +func getEnvironment() string { + provider, err := providers.ClusterProvider() + if err != nil { + GinkgoLogr.Error(err, "Failed to get cluster provider, falling back to stage") + return "stage" + } + if provider == nil { + GinkgoLogr.Info("Cluster provider is nil, falling back to stage") + return "stage" + } + return provider.Environment() // Returns "int", "stage", or "prod" +} + func setHostedControlPlaneAvailable(ctx context.Context, k8s *openshift.Client, hcp *hypershiftv1beta1.HostedControlPlane) error { // Update the status to simulate HyperShift controller behavior // RMO only processes HCPs with Available=true status From a9d310774c003b3a9d9de147d091a83fc1667edb Mon Sep 17 00:00:00 2001 From: erosente Date: Wed, 11 Feb 2026 14:36:50 -0500 Subject: [PATCH 08/12] Remove dead code for clusterDeployment simulation. --- test/e2e/route_monitor_operator_tests.go | 62 ------------------------ 1 file changed, 62 deletions(-) diff --git a/test/e2e/route_monitor_operator_tests.go b/test/e2e/route_monitor_operator_tests.go index 5f3cbdeb..f6a7f451 100644 --- a/test/e2e/route_monitor_operator_tests.go +++ b/test/e2e/route_monitor_operator_tests.go @@ -32,7 +32,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/wait" @@ -41,65 +40,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" ) -// Local minimal ClusterDeployment type for testing -// We define this locally to avoid pulling in the full hive dependency -type ClusterDeployment struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - Spec ClusterDeploymentSpec `json:"spec,omitempty"` -} - -// DeepCopyObject implements runtime.Object interface -func (cd *ClusterDeployment) DeepCopyObject() runtime.Object { - if cd == nil { - return nil - } - out := new(ClusterDeployment) - cd.DeepCopyInto(out) - return out -} - -// DeepCopyInto copies all properties of this object into another object of the same type -func (cd *ClusterDeployment) DeepCopyInto(out *ClusterDeployment) { - *out = *cd - out.TypeMeta = cd.TypeMeta - cd.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = cd.Spec -} - -type ClusterDeploymentSpec struct { - ClusterName string `json:"clusterName"` - BaseDomain string `json:"baseDomain"` -} - const ( // Embedded CRD definitions - no external dependencies required - clusterDeploymentCRD = ` -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: clusterdeployments.hive.openshift.io -spec: - group: hive.openshift.io - names: - kind: ClusterDeployment - listKind: ClusterDeploymentList - plural: clusterdeployments - shortNames: - - cd - singular: clusterdeployment - scope: Namespaced - versions: - - name: v1 - served: true - storage: true - schema: - openAPIV3Schema: - type: object - x-kubernetes-preserve-unknown-fields: true - subresources: - status: {} -` hostedControlPlaneCRD = ` apiVersion: apiextensions.k8s.io/v1 @@ -1102,10 +1044,6 @@ func ensureCRDsInstalled(ctx context.Context, k8s *openshift.Client) error { name: "vpcendpoints.avo.openshift.io", yamlDef: vpcEndpointCRD, }, - { - name: "clusterdeployments.hive.openshift.io", - yamlDef: clusterDeploymentCRD, - }, } var missingCRDs []string From 248638f9e14263e519494433d8d06eee1da39aca Mon Sep 17 00:00:00 2001 From: erosente Date: Wed, 11 Feb 2026 15:58:38 -0500 Subject: [PATCH 09/12] update skip health check variable for better clarity. --- config/manager/manager.yaml | 4 +- controllers/hostedcontrolplane/healthcheck.go | 4 +- .../hostedcontrolplane/hostedcontrolplane.go | 6 +- controllers/hostedcontrolplane/synthetics.go | 12 ++-- ...perator-controller-manager.Deployment.yaml | 4 +- hack/olm-registry/rmo-config-template.yaml | 4 +- main.go | 62 +++++++++---------- test/e2e/route_monitor_operator_tests.go | 16 ++--- 8 files changed, 56 insertions(+), 56 deletions(-) diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 6c34673d..b0fdf6cc 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -48,7 +48,7 @@ spec: - --oidc-client-secret=$(OIDC_CLIENT_SECRET) - --oidc-issuer-url=$(OIDC_ISSUER_URL) - --only-public-clusters=$(ONLY_PUBLIC_CLUSTERS) - - --skip-infrastructure-tests=$(SKIP_INFRASTRUCTURE_TESTS) + - --skip-infrastructure-health-check=$(SKIP_INFRASTRUCTURE_HEALTH_CHECK) env: - name: LOG_LEVEL # level 1 is debug, so when we want to raise the level we can @@ -71,7 +71,7 @@ spec: value: "" # Default to empty - can be overridden in app-interface - name: ONLY_PUBLIC_CLUSTERS value: "false" # Default to false - can be overridden in app-interface - - name: SKIP_INFRASTRUCTURE_TESTS + - name: SKIP_INFRASTRUCTURE_HEALTH_CHECK value: "false" # Default to false - can be overridden in app-interface image: controller:latest name: manager diff --git a/controllers/hostedcontrolplane/healthcheck.go b/controllers/hostedcontrolplane/healthcheck.go index 6191bc96..35743d9a 100644 --- a/controllers/hostedcontrolplane/healthcheck.go +++ b/controllers/hostedcontrolplane/healthcheck.go @@ -65,7 +65,7 @@ const ( // this functionality func (r *HostedControlPlaneReconciler) hcpReady(ctx context.Context, hostedcontrolplane *hypershiftv1beta1.HostedControlPlane, cfg RHOBSConfig) (bool, error) { // Skip health check for test environments (e.g., osde2e tests without real kube-apiserver) - if cfg.SkipInfrastructureTests { + if cfg.SkipInfrastructureHealthCheck { return true, nil } @@ -234,7 +234,7 @@ func olderThan(obj metav1.Object, age time.Duration) bool { // isVpcEndpointReady checks if the VPC Endpoint associated with the HostedControlPlane is ready. func (r *HostedControlPlaneReconciler) isVpcEndpointReady(ctx context.Context, hostedcontrolplane *hypershiftv1beta1.HostedControlPlane, cfg RHOBSConfig) (bool, error) { // Skip VPC endpoint check for test environments (e.g., osde2e tests without real VPC infrastructure) - if cfg.SkipInfrastructureTests { + if cfg.SkipInfrastructureHealthCheck { return true, nil } diff --git a/controllers/hostedcontrolplane/hostedcontrolplane.go b/controllers/hostedcontrolplane/hostedcontrolplane.go index 8991630b..e9ebc9e2 100644 --- a/controllers/hostedcontrolplane/hostedcontrolplane.go +++ b/controllers/hostedcontrolplane/hostedcontrolplane.go @@ -133,8 +133,8 @@ func (r *HostedControlPlaneReconciler) getRHOBSConfig(ctx context.Context) RHOBS if strings.TrimSpace(configMap.Data["only-public-clusters"]) == "true" { cfg.OnlyPublicClusters = true } - if strings.TrimSpace(configMap.Data["skip-infrastructure-tests"]) == "true" { - cfg.SkipInfrastructureTests = true + if strings.TrimSpace(configMap.Data["skip-infrastructure-health-check"]) == "true" { + cfg.SkipInfrastructureHealthCheck = true } return cfg @@ -327,7 +327,7 @@ func (r *HostedControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.R // deployInternalMonitoringObjects creates or updates the objects needed to monitor the kube-apiserver using cluster-internal routes func (r *HostedControlPlaneReconciler) deployInternalMonitoringObjects(ctx context.Context, log logr.Logger, hostedcontrolplane *hypershiftv1beta1.HostedControlPlane, cfg RHOBSConfig) error { // Skip internal monitoring for test environments (e.g., osde2e tests without real kube-apiserver infrastructure) - if cfg.SkipInfrastructureTests { + if cfg.SkipInfrastructureHealthCheck { return nil } diff --git a/controllers/hostedcontrolplane/synthetics.go b/controllers/hostedcontrolplane/synthetics.go index ed505f80..9c83e779 100644 --- a/controllers/hostedcontrolplane/synthetics.go +++ b/controllers/hostedcontrolplane/synthetics.go @@ -252,12 +252,12 @@ func (r *HostedControlPlaneReconciler) deleteDynatraceHttpMonitorResources(dynat // RHOBSConfig holds RHOBS API configuration type RHOBSConfig struct { ProbeAPIURL string - Tenant string - OIDCClientID string - OIDCClientSecret string - OIDCIssuerURL string - OnlyPublicClusters bool - SkipInfrastructureTests bool + Tenant string + OIDCClientID string + OIDCClientSecret string + OIDCIssuerURL string + OnlyPublicClusters bool + SkipInfrastructureHealthCheck bool } // ensureRHOBSProbe ensures that a RHOBS probe exists for the HostedControlPlane diff --git a/deploy/route-monitor-operator-controller-manager.Deployment.yaml b/deploy/route-monitor-operator-controller-manager.Deployment.yaml index b71ab4ef..e167f6c9 100644 --- a/deploy/route-monitor-operator-controller-manager.Deployment.yaml +++ b/deploy/route-monitor-operator-controller-manager.Deployment.yaml @@ -44,7 +44,7 @@ spec: - --oidc-client-secret=$(OIDC_CLIENT_SECRET) - --oidc-issuer-url=$(OIDC_ISSUER_URL) - --only-public-clusters=$(ONLY_PUBLIC_CLUSTERS) - - --skip-infrastructure-tests=$(SKIP_INFRASTRUCTURE_TESTS) + - --skip-infrastructure-health-check=$(SKIP_INFRASTRUCTURE_HEALTH_CHECK) command: - /manager env: @@ -66,7 +66,7 @@ spec: value: "" - name: ONLY_PUBLIC_CLUSTERS value: "false" - - name: SKIP_INFRASTRUCTURE_TESTS + - name: SKIP_INFRASTRUCTURE_HEALTH_CHECK value: "false" image: route-monitor-operator:latest livenessProbe: diff --git a/hack/olm-registry/rmo-config-template.yaml b/hack/olm-registry/rmo-config-template.yaml index f79a7a0e..d99f3ad9 100644 --- a/hack/olm-registry/rmo-config-template.yaml +++ b/hack/olm-registry/rmo-config-template.yaml @@ -42,7 +42,7 @@ parameters: - name: ONLY_PUBLIC_CLUSTERS value: "" required: false -- name: SKIP_INFRASTRUCTURE_TESTS +- name: SKIP_INFRASTRUCTURE_HEALTH_CHECK value: "false" required: false description: "Skip infrastructure health checks for test environments" @@ -75,4 +75,4 @@ objects: oidc-client-secret: ${OIDC_CLIENT_SECRET} oidc-issuer-url: ${OIDC_ISSUER_URL} only-public-clusters: ${ONLY_PUBLIC_CLUSTERS} - skip-infrastructure-tests: ${SKIP_INFRASTRUCTURE_TESTS} + skip-infrastructure-health-check: ${SKIP_INFRASTRUCTURE_HEALTH_CHECK} diff --git a/main.go b/main.go index a7f18d74..45a2cf65 100644 --- a/main.go +++ b/main.go @@ -98,7 +98,7 @@ func main() { var oidcClientSecret string var oidcIssuerURL string var onlyPublicClusters bool - var skipInfrastructureTests bool + var skipInfrastructureHealthCheck bool flag.StringVar(&blackboxExporterImage, "blackbox-image", "quay.io/prometheus/blackbox-exporter@sha256:b04a9fef4fa086a02fc7fcd8dcdbc4b7b35cc30cdee860fdc6a19dd8b208d63e", "The image that will be used for the blackbox-exporter deployment") flag.StringVar(&blackboxExporterNamespace, "blackbox-namespace", config.OperatorNamespace, "Blackbox-exporter deployment will reside on this Namespace") @@ -108,7 +108,7 @@ func main() { flag.StringVar(&oidcClientSecret, "oidc-client-secret", "", "OIDC client secret for RHOBS API authentication. When empty, no OIDC authentication is used.") flag.StringVar(&oidcIssuerURL, "oidc-issuer-url", "", "OIDC issuer URL for RHOBS API authentication. When empty, no OIDC authentication is used.") flag.BoolVar(&onlyPublicClusters, "only-public-clusters", false, "When true, only create RHOBS probes for public (non-private) HostedClusters. Defaults to false (process all clusters).") - flag.BoolVar(&skipInfrastructureTests, "skip-infrastructure-tests", false, "When true, skip infrastructure health checks (HCP ready, VPC endpoint ready) for test environments. Defaults to false.") + flag.BoolVar(&skipInfrastructureHealthCheck, "skip-infrastructure-health-check", false, "When true, skip infrastructure health checks (HCP ready, VPC endpoint ready) for test environments. Defaults to false.") opts := zap.Options{} opts.BindFlags(flag.CommandLine) @@ -173,12 +173,12 @@ func main() { flagParams = append(flagParams, "only-public-clusters") } - if configData.SkipInfrastructureTests { - setupLog.V(1).Info("Using skip-infrastructure-tests from ConfigMap", "skipInfrastructureTests", configData.SkipInfrastructureTests) - skipInfrastructureTests = configData.SkipInfrastructureTests - configMapParams = append(configMapParams, "skip-infrastructure-tests") + if configData.SkipInfrastructureHealthCheck { + setupLog.V(1).Info("Using skip-infrastructure-health-check from ConfigMap", "skipInfrastructureHealthCheck", configData.SkipInfrastructureHealthCheck) + skipInfrastructureHealthCheck = configData.SkipInfrastructureHealthCheck + configMapParams = append(configMapParams, "skip-infrastructure-health-check") } else { - flagParams = append(flagParams, "skip-infrastructure-tests") + flagParams = append(flagParams, "skip-infrastructure-health-check") } // Summarize configuration sources @@ -283,13 +283,13 @@ func main() { if enableHCP { rhobsConfig := hostedcontrolplane.RHOBSConfig{ - ProbeAPIURL: probeAPIURL, - Tenant: probeTenant, - OIDCClientID: oidcClientID, - OIDCClientSecret: oidcClientSecret, - OIDCIssuerURL: oidcIssuerURL, - OnlyPublicClusters: onlyPublicClusters, - SkipInfrastructureTests: skipInfrastructureTests, + ProbeAPIURL: probeAPIURL, + Tenant: probeTenant, + OIDCClientID: oidcClientID, + OIDCClientSecret: oidcClientSecret, + OIDCIssuerURL: oidcIssuerURL, + OnlyPublicClusters: onlyPublicClusters, + SkipInfrastructureHealthCheck: skipInfrastructureHealthCheck, } hostedControlPlaneReconciler := hostedcontrolplane.NewHostedControlPlaneReconciler(mgr, rhobsConfig) if err = hostedControlPlaneReconciler.SetupWithManager(mgr); err != nil { @@ -342,13 +342,13 @@ func shouldEnableHCP() (bool, error) { // OperatorConfig holds configuration values from ConfigMap type OperatorConfig struct { - ProbeAPIURL string - ProbeTenant string - OIDCClientID string - OIDCClientSecret string - OIDCIssuerURL string - OnlyPublicClusters bool - SkipInfrastructureTests bool + ProbeAPIURL string + ProbeTenant string + OIDCClientID string + OIDCClientSecret string + OIDCIssuerURL string + OnlyPublicClusters bool + SkipInfrastructureHealthCheck bool } // getConfigFromConfigMap reads configuration from the route-monitor-operator-config ConfigMap @@ -380,13 +380,13 @@ func getConfigFromConfigMap() (*OperatorConfig, error) { // Extract configuration values, trimming whitespace cfg := &OperatorConfig{ - ProbeAPIURL: strings.TrimSpace(configMap.Data["probe-api-url"]), - ProbeTenant: strings.TrimSpace(configMap.Data["probe-tenant"]), - OIDCClientID: strings.TrimSpace(configMap.Data["oidc-client-id"]), - OIDCClientSecret: strings.TrimSpace(configMap.Data["oidc-client-secret"]), - OIDCIssuerURL: strings.TrimSpace(configMap.Data["oidc-issuer-url"]), - OnlyPublicClusters: strings.TrimSpace(configMap.Data["only-public-clusters"]) == "true", - SkipInfrastructureTests: strings.TrimSpace(configMap.Data["skip-infrastructure-tests"]) == "true", + ProbeAPIURL: strings.TrimSpace(configMap.Data["probe-api-url"]), + ProbeTenant: strings.TrimSpace(configMap.Data["probe-tenant"]), + OIDCClientID: strings.TrimSpace(configMap.Data["oidc-client-id"]), + OIDCClientSecret: strings.TrimSpace(configMap.Data["oidc-client-secret"]), + OIDCIssuerURL: strings.TrimSpace(configMap.Data["oidc-issuer-url"]), + OnlyPublicClusters: strings.TrimSpace(configMap.Data["only-public-clusters"]) == "true", + SkipInfrastructureHealthCheck: strings.TrimSpace(configMap.Data["skip-infrastructure-health-check"]) == "true", } // Log detailed information about what was found in the ConfigMap @@ -429,10 +429,10 @@ func getConfigFromConfigMap() (*OperatorConfig, error) { missingParams = append(missingParams, "only-public-clusters") } - if configMap.Data["skip-infrastructure-tests"] != "" { - foundParams = append(foundParams, "skip-infrastructure-tests") + if configMap.Data["skip-infrastructure-health-check"] != "" { + foundParams = append(foundParams, "skip-infrastructure-health-check") } else { - missingParams = append(missingParams, "skip-infrastructure-tests") + missingParams = append(missingParams, "skip-infrastructure-health-check") } setupLog.Info("ConfigMap found and processed", diff --git a/test/e2e/route_monitor_operator_tests.go b/test/e2e/route_monitor_operator_tests.go index f6a7f451..a40118bf 100644 --- a/test/e2e/route_monitor_operator_tests.go +++ b/test/e2e/route_monitor_operator_tests.go @@ -1232,13 +1232,13 @@ Please set the following environment variables or set them in the route-monitor- export EXTERNAL_SECRET_OIDC_ISSUER_URL="your-issuer-url" export PROBE_API_URL="your-rhobs-api-url" -Note: In osde2e/CI, these variables (including SKIP_INFRASTRUCTURE_TESTS) are automatically loaded from app-interface (SDCICD-1739).`) +Note: In osde2e/CI, these variables (including SKIP_INFRASTRUCTURE_HEALTH_CHECK) are automatically loaded from app-interface (SDCICD-1739).`) } // createRMOConfigMap creates the RMO config ConfigMap with OIDC credentials func createRMOConfigMap(ctx context.Context, k8s *openshift.Client, creds *OIDCCredentials) error { - // Read skip-infrastructure-tests from environment, default to "false" - skipInfraTests := getEnvOrDefault("SKIP_INFRASTRUCTURE_TESTS", "false") + // Read skip-infrastructure-health-check from environment, default to "false" + skipInfraHealthCheck := getEnvOrDefault("SKIP_INFRASTRUCTURE_HEALTH_CHECK", "false") configMap := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ @@ -1246,11 +1246,11 @@ func createRMOConfigMap(ctx context.Context, k8s *openshift.Client, creds *OIDCC Namespace: "openshift-route-monitor-operator", }, Data: map[string]string{ - "probe-api-url": creds.ProbeAPIURL, - "oidc-client-id": creds.ClientID, - "oidc-client-secret": creds.ClientSecret, - "oidc-issuer-url": creds.IssuerURL, - "skip-infrastructure-tests": skipInfraTests, + "probe-api-url": creds.ProbeAPIURL, + "oidc-client-id": creds.ClientID, + "oidc-client-secret": creds.ClientSecret, + "oidc-issuer-url": creds.IssuerURL, + "skip-infrastructure-health-check": skipInfraHealthCheck, }, } From 2d82dedc1a30b43d8668cfcca09874b507414b34 Mon Sep 17 00:00:00 2001 From: erosente Date: Thu, 12 Feb 2026 14:34:10 -0500 Subject: [PATCH 10/12] Update readme with details for new tests. --- test/e2e/README.md | 634 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 623 insertions(+), 11 deletions(-) diff --git a/test/e2e/README.md b/test/e2e/README.md index f6ddc260..3a406e4b 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -1,14 +1,626 @@ -## Locally running e2e test suite -When updating your operator it's beneficial to add e2e tests for new functionality AND ensure existing functionality is not breaking using e2e tests. -To do this, following steps are recommended +# Route Monitor Operator - E2E Test Suite -1. Run "make e2e-binary-build" to make sure e2e tests build -2. Deploy your new version of operator in a test cluster -3. Run "go install github.com/onsi/ginkgo/ginkgo@latest" -4. Get kubeadmin credentials from your cluster using +This directory contains end-to-end tests for the Route Monitor Operator (RMO), validating its integration with the RHOBS synthetic monitoring system. -ocm get /api/clusters_mgmt/v1/clusters/(cluster-id)/credentials | jq -r .kubeconfig > /(path-to)/kubeconfig +## Table of Contents -5. Run test suite using - -DISABLE_JUNIT_REPORT=true KUBECONFIG=/(path-to)/kubeconfig ./(path-to)/bin/ginkgo --tags=osde2e -v test/e2e +- [Overview](#overview) +- [Test Files](#test-files) +- [Local E2E Testing](#local-e2e-testing) +- [CI/CD Testing (osde2e)](#cicd-testing-osde2e) +- [Troubleshooting](#troubleshooting) +- [Contributing](#contributing) + +--- + +## Overview + +The RMO e2e test suite validates the complete synthetic monitoring workflow: + +``` +HostedControlPlane CR → RMO → RHOBS API → Synthetics Agent → Prometheus/Blackbox Exporter +``` + +**What We Test:** +- RMO watches HostedControlPlane CRs and creates synthetic probes +- Public vs private cluster detection (via VpcEndpoint CRs) +- Probe lifecycle: creation, updates, deletion +- Retry logic and error handling (SREP-2832, SREP-2966) +- Integration with RHOBS Synthetics API + +**What We Don't Test:** +- Real HCP cluster provisioning (too slow/expensive for e2e tests) +- Actual probe execution and metrics collection (tested separately in rhobs-synthetics-agent) +- Network connectivity to real API servers (we use fake endpoints) + +--- + +## Test Files + +### `route_monitor_operator_tests.go` (osde2e) + +**Build Tag:** `//go:build osde2e` + +**Test Suites:** + +#### Suite 1: "Route Monitor Operator" - Basic Installation +Tests RMO deployment and basic functionality: + +| Test | Description | +|------|-------------| +| `is installed` | Verifies RMO deployment exists and is ready | +| `can be upgraded` | Pending - upgrade testing | +| `has all of the required resources` | Checks for Deployment, ServiceAccount, ClusterRole, ClusterRoleBinding | +| `required dependent resources are created` | Verifies RouteMonitor CRs are created for test routes | + +#### Suite 2: "RHOBS Synthetic Monitoring" - HostedControlPlane Integration +Tests synthetic monitoring workflow with simulated Management Cluster environment: + +| Test | Description | What It Validates | +|------|-------------|-------------------| +| `has RHOBS monitoring configured` | Checks RHOBS credentials in ConfigMap | OIDC auth setup | +| `creates probe for public HostedControlPlane` | Creates fake public HCP CR, waits for probe creation | Public cluster detection, probe creation with `private=false` label | +| `creates probe for private HostedControlPlane` | Creates fake private HCP with VpcEndpoint CR | Private cluster detection via VpcEndpoint, probe with `private=true` label | +| `deletes probe when HostedControlPlane is deleted` | Deletes HCP, verifies probe cleanup | Finalizer logic, probe deletion, no orphaned probes | + +**Key Feature:** These tests simulate a Management Cluster by creating fake HostedControlPlane and VpcEndpoint CRs that match production patterns exactly. + +**Environment Detection:** Tests detect whether they're running in integration vs staging by querying the osde2e cluster provider (`provider.Environment()`). However, the RHOBS API endpoint URL is **not** auto-detected - it must be explicitly configured via the `PROBE_API_URL` environment variable, which is automatically injected by app-interface based on the target environment. + +### `full_integration_test.go` (e2e) + +**Build Tag:** `//go:build e2e` + +**Test:** `TestFullStackIntegration` + +**Purpose:** Validates the complete RMO → API → Agent flow locally without a real cluster. + +**What It Tests:** +1. RMO creates a probe from a fake HostedControlPlane CR +2. RHOBS Synthetics API stores the probe configuration +3. RHOBS Synthetics Agent fetches the probe from API +4. Test mocks agent processing (agent needs real K8s to deploy resources) + +**Requirements:** +- Local clones of `rhobs-synthetics-api` and `rhobs-synthetics-agent` repos +- No Kubernetes cluster or Docker needed +- Runs in ~20 seconds + +**What Gets Tested:** +- RMO controller logic with fake K8s client +- API CRUD operations (file-based storage) +- Agent API polling +- End-to-end data flow + +**What Gets Mocked:** +- Kubernetes cluster (using `fake.NewClientBuilder()`) +- Agent resource deployment (test updates probe status directly) +- Dynatrace endpoints +- Probe target endpoints + +### `probe_deletion_retry_test.go` (e2e) + +**Build Tag:** `//go:build e2e` + +**Test:** `TestProbeDeletionRetry` + +**Purpose:** Validates SREP-2832 + SREP-2966 fixes (hybrid retry-then-fail-open approach). + +**Scenario:** +1. Create probe normally via API +2. Stop API to simulate unavailability +3. Attempt probe deletion via RMO +4. Verify RMO returns error (fail closed) - prevents orphaned probes +5. Verify finalizer is NOT removed (blocks HCP deletion) +6. Restart API +7. Verify probe deletion succeeds on retry + +**What It Validates:** +- **Within 15-minute timeout:** RMO fails closed (retries, blocks deletion) +- **After 15-minute timeout:** RMO fails open (allows deletion to prevent indefinite blocking) +- Structured logging with behavior indicators (`fail_closed`, `fail_open`) +- No orphaned probes during transient API failures +- HCP deletion not blocked indefinitely during prolonged API outages + +**Related Issues:** +- SREP-2832: Orphaned probes not being garbage collected +- SREP-2966: RMO blocking cluster deletion indefinitely + +### `helpers.go` (e2e) + +**Build Tag:** `//go:build e2e` + +**Purpose:** Shared utilities for local e2e tests. + +**Key Functions:** + +| Function | Description | +|----------|-------------| +| `createProbeViaAPI()` | Creates a probe directly through RHOBS API | +| `getProbeByID()` | Fetches probe by ID from API | +| `deleteProbeByID()` | Deletes probe from API | +| `updateProbeStatus()` | Mocks agent behavior by updating probe status | +| `startMockDynatraceServer()` | HTTP server mocking Dynatrace endpoints | +| `startMockProbeTargetServer()` | HTTP server mocking probe target endpoints | +| `testWriter` | Captures logs for validation in tests | + +**Why We Mock Agent Behavior:** +The real agent needs a Kubernetes cluster to deploy Prometheus and Blackbox Exporter resources. In local tests, we bypass this by updating probe status directly via the API. + +### `api_manager.go` (e2e) + +**Build Tag:** `//go:build e2e` + +**Purpose:** Manages RHOBS Synthetics API lifecycle in tests. + +**Key Functions:** +- `NewRealAPIManager()` - Creates manager with auto-detected or custom API path +- `Start()` - Builds API binary and starts server +- `Stop()` - Gracefully stops API server +- `GetURL()` - Returns API base URL +- `ClearAllProbes()` - Cleanup utility for test isolation + +### `agent_manager.go` (e2e) + +**Build Tag:** `//go:build e2e` + +**Purpose:** Manages RHOBS Synthetics Agent lifecycle in tests. + +**Key Functions:** +- `NewAgentManager()` - Creates manager with auto-detected or custom agent path +- `Start()` - Builds agent binary and starts it +- `Stop()` - Gracefully stops agent process + +### `process_manager.go` (e2e) + +**Build Tag:** `//go:build e2e` + +**Purpose:** Low-level process management utilities for starting/stopping Go binaries in tests. + +### `route_monitor_operator_runner_test.go` (osde2e) + +**Build Tag:** `//go:build osde2e` + +**Purpose:** Test runner that initializes Ginkgo test suite for osde2e tests. + +**Key Function:** +```go +func TestRouteMonitorOperator(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Route Monitor Operator Suite") +} +``` + +### `run-e2e.sh` + +**Purpose:** Bash wrapper for running local e2e tests. + +**What It Does:** +1. Auto-detects or validates RHOBS repo paths +2. Exports environment variables +3. Runs `TestFullStackIntegration` with 5-minute timeout +4. Reports success/failure + +**Usage:** +```bash +cd test/e2e +./run-e2e.sh +``` + +--- + +## Local E2E Testing + +These tests run completely locally on your development machine without requiring a Kubernetes cluster or CI/CD infrastructure. + +**Build Tag:** `//go:build e2e` + +**Files:** `full_integration_test.go`, `probe_deletion_retry_test.go`, `helpers.go`, `api_manager.go`, `agent_manager.go`, `process_manager.go`, `run-e2e.sh` + +### What Gets Tested Locally + +- **Full Stack Integration:** RMO → RHOBS API → Synthetics Agent flow +- **Probe Deletion Retry Logic:** SREP-2832 + SREP-2966 fixes (hybrid retry-then-fail-open) +- **Controller Logic:** Using fake Kubernetes clients +- **API Operations:** CRUD operations with file-based storage +- **Agent Behavior:** API polling and probe fetching + +### What Gets Mocked + +- Kubernetes cluster (using `fake.NewClientBuilder()`) +- Agent resource deployment (test updates probe status directly via API) +- Dynatrace endpoints +- Probe target endpoints + +### Prerequisites + +Clone RHOBS repos as sibling directories: + +```bash +cd /path/to/repos +git clone https://github.com/openshift/route-monitor-operator.git +git clone https://github.com/rhobs/rhobs-synthetics-api.git +git clone https://github.com/rhobs/rhobs-synthetics-agent.git +``` + +**OR** set custom paths: + +```bash +export RHOBS_SYNTHETICS_API_PATH=/path/to/rhobs-synthetics-api +export RHOBS_SYNTHETICS_AGENT_PATH=/path/to/rhobs-synthetics-agent +``` + +### Running Local Tests + +**Full integration test (RMO → API → Agent):** +```bash +# From repo root +make test-e2e-full + +# Or directly +cd test/e2e +./run-e2e.sh +``` + +**Probe deletion retry test:** +```bash +cd test/e2e +go test -v -tags=e2e -timeout=5m . -run TestProbeDeletionRetry +``` + +**Run all local e2e tests:** +```bash +cd test/e2e +go test -v -tags=e2e -timeout=10m . +``` + +### Environment Variables (Local) + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `RHOBS_SYNTHETICS_API_PATH` | Yes* | `../rhobs-synthetics-api` | Path to API repo | +| `RHOBS_SYNTHETICS_AGENT_PATH` | Yes* | `../rhobs-synthetics-agent` | Path to Agent repo | + +*Auto-detected if repos are sibling directories + +### Local Test Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Test Process │ +│ │ +│ ┌──────────────┐ │ +│ │ RMO │ (fake K8s client) │ +│ │ Controller │──┐ │ +│ └──────────────┘ │ │ +│ │ HTTP POST /probes │ +│ ▼ │ +│ ┌──────────────────────────────┐ │ +│ │ RHOBS Synthetics API │ (local Go process) │ +│ │ (file-based storage) │ │ +│ └──────────────────────────────┘ │ +│ │ │ +│ │ HTTP GET /probes │ +│ ▼ │ +│ ┌──────────────────────────────┐ │ +│ │ RHOBS Synthetics Agent │ (local Go process) │ +│ │ (fetches probes) │ │ +│ └──────────────────────────────┘ │ +│ │ +│ Mock Servers: │ +│ - Dynatrace endpoint (HTTP 200) │ +│ - Probe targets (/livez, /readyz endpoints) │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## CI/CD Testing (osde2e) + +These tests run automatically in the CI/CD pipeline on real OpenShift clusters managed by the osde2e framework. + +**Build Tag:** `//go:build osde2e` + +**Files:** `route_monitor_operator_tests.go`, `route_monitor_operator_runner_test.go`, `e2e-template.yml` + +### What Gets Tested in CI/CD + +**Suite 1: "Route Monitor Operator" - Basic Installation** +- RMO deployment exists and is ready +- All required resources present (Deployment, ServiceAccount, ClusterRole, etc.) +- RouteMonitor CRs created for test routes + +**Suite 2: "RHOBS Synthetic Monitoring" - HostedControlPlane Integration** +- RHOBS credentials configured correctly +- Probe creation for **public** HostedControlPlane (with `private=false` label) +- Probe creation for **private** HostedControlPlane (with `private=true` label, VpcEndpoint detection) +- Probe deletion and finalizer cleanup when HCP is deleted + +### How CI/CD Tests Work + +**Key Feature:** Tests simulate a Management Cluster by creating **fake HostedControlPlane and VpcEndpoint CRs** that match production patterns exactly. No actual HCP clusters are provisioned. + +### CI/CD Pipeline Flow + +**Configuration:** [`osde2e-focus-test.yaml`](https://gitlab.cee.redhat.com/service/app-interface/-/blob/master/data/services/osd-operators/cicd/saas/saas-route-monitor-operator/osde2e-focus-test.yaml) + +1. **Cluster Provisioning:** osde2e framework creates or uses existing test cluster (`USE_EXISTING_CLUSTER=TRUE`) + +2. **Secret Injection:** Environment variables **automatically loaded from Vault**: + - `EXTERNAL_SECRET_OIDC_CLIENT_ID` + - `EXTERNAL_SECRET_OIDC_CLIENT_SECRET` + - `EXTERNAL_SECRET_OIDC_ISSUER_URL` + - `PROBE_API_URL` + - `SKIP_INFRASTRUCTURE_HEALTH_CHECK` + +3. **CRD Installation:** Test installs required CRDs (HostedControlPlane, VpcEndpoint) in `BeforeAll` + +4. **Test Execution:** Ginkgo runs tests tagged with `//go:build osde2e` + +5. **Environment Detection:** Tests detect environment name (integration/staging) via osde2e provider for logging purposes. RHOBS API endpoint is explicitly configured via `PROBE_API_URL` environment variable + +### CI/CD Environments + +| Environment | Management Cluster | RHOBS API Endpoint | +|-------------|-------------------|-------------------| +| **Integration** | `hivei01ue1` (us-east-1) | `https://us-west-2.rhobs.api.integration.openshift.com/api/metrics/v1/hcp/probes` | +| **Staging** | `hives02ue1` (us-east-1) | `https://us-east-1-0.rhobs.api.stage.openshift.com/api/metrics/v1/hcp/probes` | + +### Vault Secret Paths + +Secrets are stored in Vault and automatically injected by osde2e: + +- **Integration:** `osd-sre/integration/route-monitor-operator-credentials` +- **Staging:** `osd-sre/staging/route-monitor-operator-credentials` + +**Required Fields:** +- `OIDC_CLIENT_ID` +- `OIDC_CLIENT_SECRET` +- `OIDC_ISSUER_URL` + +### Pipeline Triggers + +- **Automatic:** Every merge to main branch +- **Promotion-based:** Integration tests must pass before staging deployment +- **Manual:** Via app-interface MR + +**Test Image:** `quay.io/redhat-services-prod/openshift/route-monitor-operator-e2e` + +### Running osde2e Tests Manually + +**Prerequisites:** +1. Access to a real OpenShift cluster (integration or staging) +2. RMO deployed on the cluster +3. RHOBS credentials configured + +**Option 1: Using Ginkgo** +```bash +# Install ginkgo +go install github.com/onsi/ginkgo/v2/ginkgo@latest + +# Get cluster kubeconfig +ocm get /api/clusters_mgmt/v1/clusters//credentials | jq -r .kubeconfig > /tmp/kubeconfig + +# Run all tests +DISABLE_JUNIT_REPORT=true \ +KUBECONFIG=/tmp/kubeconfig \ +ginkgo --tags=osde2e -v test/e2e +``` + +**Option 2: Running specific test suites** +```bash +# Only basic installation tests +ginkgo --tags=osde2e -v --focus="Route Monitor Operator" test/e2e + +# Only RHOBS synthetic monitoring tests +ginkgo --tags=osde2e -v --focus="RHOBS Synthetic Monitoring" test/e2e +``` + +### Environment Variables (CI/CD) + +| Variable | Required | Source | Description | +|----------|----------|--------|-------------| +| `EXTERNAL_SECRET_OIDC_CLIENT_ID` | Yes | Vault (auto-loaded) | OIDC client ID for RHOBS API auth | +| `EXTERNAL_SECRET_OIDC_CLIENT_SECRET` | Yes | Vault (auto-loaded) | OIDC client secret | +| `EXTERNAL_SECRET_OIDC_ISSUER_URL` | Yes | Vault (auto-loaded) | OIDC issuer URL | +| `PROBE_API_URL` | Yes | app-interface config | RHOBS API endpoint URL | +| `SKIP_INFRASTRUCTURE_HEALTH_CHECK` | No | `"true"` | Skip infra checks for test HCPs | +| `KUBECONFIG` | Yes | Manual/OCM | Path to cluster credentials | + +### CI/CD Test Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Real OpenShift Cluster (Integration/Staging) │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ Test Namespace │ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────────────┐ │ │ +│ │ │ Fake HostedControlPlane CR │ │ │ +│ │ │ - Matches production MC patterns │ │ │ +│ │ │ - Status manually set to Available=True │ │ │ +│ │ └─────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────────────┐ │ │ +│ │ │ Fake VpcEndpoint CR (for private tests) │ │ │ +│ │ │ - purpose: backplane label │ │ │ +│ │ │ - Fake VPC endpoint ID │ │ │ +│ │ └─────────────────────────────────────────────┘ │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ openshift-route-monitor-operator namespace │ │ +│ │ │ │ +│ │ RMO watches fake HCPs and calls real RHOBS API │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ + │ + │ HTTPS (OIDC auth) + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ RHOBS Cell (rhobsi01uw2 or rhobs staging) │ +│ │ +│ Real RHOBS Synthetics API creates actual probe objects │ +└─────────────────────────────────────────────────────────────┘ +``` + +**Key Insight:** osde2e tests use **fake CRs that look real** to test RMO logic without provisioning actual HCP clusters. Fast (30-40s per test), cost-effective, validates real controller behavior. + +--- + +## Troubleshooting + +### Local E2E Test Issues + +#### `RHOBS Synthetics API path not set` + +The test cannot find the RHOBS API or Agent repositories. + +**Solution:** +```bash +# Option 1: Clone as sibling directories +cd /path/to/repos +git clone https://github.com/rhobs/rhobs-synthetics-api.git +git clone https://github.com/rhobs/rhobs-synthetics-agent.git + +# Option 2: Set environment variables +export RHOBS_SYNTHETICS_API_PATH=/custom/path/to/api +export RHOBS_SYNTHETICS_AGENT_PATH=/custom/path/to/agent +``` + +#### `Build failed` or `Binary not found` + +The test cannot compile the API or Agent binaries. + +**Solution:** Ensure RHOBS repos are up-to-date and have no build errors: +```bash +cd $RHOBS_SYNTHETICS_API_PATH && go build ./cmd/rhobs-synthetics-api +cd $RHOBS_SYNTHETICS_AGENT_PATH && go build ./cmd/rhobs-synthetics-agent +``` + +#### Test hangs or times out + +The test gets stuck waiting for something. + +**Solution:** +- Check if ports 8080 (API) or other random ports are already in use +- Verify mock servers started successfully (check test logs) +- Run with increased timeout: `go test -timeout=10m ...` + +--- + +### CI/CD (osde2e) Test Issues + +#### `no OIDC credentials found` + +The test cannot authenticate with RHOBS API. + +**Solution:** Verify secrets exist in Vault and are properly configured in app-interface: +```bash +# Check integration credentials +vault kv get osd-sre/integration/route-monitor-operator-credentials + +# Check staging credentials +vault kv get osd-sre/staging/route-monitor-operator-credentials +``` + +Ensure these secrets have the required fields: +- `OIDC_CLIENT_ID` +- `OIDC_CLIENT_SECRET` +- `OIDC_ISSUER_URL` + +#### `no matches for kind "HostedControlPlane"` + +Required CRDs are not installed on the test cluster. + +**Solution:** The test should install CRDs automatically in `BeforeAll`. If this fails: +- Check test logs for CRD installation errors +- Verify cluster has permissions to create CRDs +- The CRD definitions are embedded in `route_monitor_operator_tests.go` (lines 46-96) + +#### Tests can't reach RHOBS API + +Network connectivity or authentication issues with RHOBS endpoints. + +**Solution:** +- Verify `PROBE_API_URL` is correct for your environment: + - Integration: `https://us-west-2.rhobs.api.integration.openshift.com/api/metrics/v1/hcp/probes` + - Staging: `https://us-east-1-0.rhobs.api.stage.openshift.com/api/metrics/v1/hcp/probes` +- Check OIDC credentials are valid +- Test authentication manually from the test cluster +- Verify firewall/network policies allow outbound HTTPS to RHOBS endpoints + +#### Test creates orphaned probes + +Probes remain in RHOBS after test completes. + +**Solution:** Tests should clean up automatically using finalizers. For manual cleanup: +```bash +# List all probes with test cluster-id pattern +curl -H "Authorization: Bearer " \ + https:///api/metrics/v1/hcp/probes?cluster-id=test-osde2e-* + +# Delete specific probe +curl -X DELETE -H "Authorization: Bearer " \ + https:///api/metrics/v1/hcp/probes/ +``` + +#### PROBE_API_URL not set or incorrect + +Test fails because it cannot connect to RHOBS API or connects to wrong environment. + +**Explanation:** The PROBE_API_URL is NOT auto-detected. It must be explicitly set via environment variable. + +**Solution:** + +In CI/CD, this is automatically set by app-interface configuration for each environment. For manual testing, set it explicitly: + +```bash +# For integration environment +export PROBE_API_URL="https://us-west-2.rhobs.api.integration.openshift.com/api/metrics/v1/hcp/probes" + +# For staging environment +export PROBE_API_URL="https://us-east-1-0.rhobs.api.stage.openshift.com/api/metrics/v1/hcp/probes" + +# Then run tests +ginkgo --tags=osde2e -v test/e2e +``` + +--- + +## Related Documentation + +- [Full Integration Tests](https://github.com/openshift/route-monitor-operator/blob/main/test/e2e/FULL_INTEGRATION_TESTS.md) - Deep dive on local e2e tests +- [app-interface osde2e config](https://gitlab.cee.redhat.com/service/app-interface/-/blob/master/data/services/osd-operators/cicd/saas/saas-route-monitor-operator/osde2e-focus-test.yaml) - CI/CD configuration +- [Route Monitor Operator GitHub](https://github.com/openshift/route-monitor-operator) - Main repository + +--- + +## Contributing + +When adding new tests: + +1. **Choose the right test type:** + - osde2e (`//go:build osde2e`) - For integration tests requiring real K8s + - e2e (`//go:build e2e`) - For fast, local unit/integration tests + +2. **Add build tags:** First line of test file must be `//go:build ` + +3. **Use helpers:** Reuse functions from `helpers.go` instead of duplicating + +4. **Clean up resources:** Use `defer` or `AfterEach` to clean up test resources + +5. **Update this documentation:** Document new tests in the [Test Files](#test-files) section + +6. **Test locally first:** Run tests locally before pushing: + ```bash + make test-e2e-full # Local e2e tests + ``` + +--- + +**Last Updated:** 2026-02-12 +**Maintainers:** ROSA Rocket Team (@team-rosa-rocket) +**Slack:** [#team-rosa-rocket](https://redhat-internal.slack.com/archives/C08N5S632V8) From 07a44347458488ba4b385294622cc3debcaeefd2 Mon Sep 17 00:00:00 2001 From: erosente Date: Mon, 23 Feb 2026 10:50:31 -0500 Subject: [PATCH 11/12] add emergency kill switch for case where RMO breaks the rhobsAPI but we need the rhobsAPI to pass the e2e pipeline. --- test/e2e/route_monitor_operator_tests.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/test/e2e/route_monitor_operator_tests.go b/test/e2e/route_monitor_operator_tests.go index a40118bf..9a610d74 100644 --- a/test/e2e/route_monitor_operator_tests.go +++ b/test/e2e/route_monitor_operator_tests.go @@ -334,6 +334,13 @@ var _ = Describe("RHOBS Synthetic Monitoring", Ordered, func() { ) BeforeAll(func(ctx context.Context) { + // Emergency skip switch - allows disabling RHOBS API tests through app-interface + // In the case of an emergency outage in the rhobs API caused by an RMO misconfiguration + // set SKIP_RHOBS_SYNTHETICS_TESTS: "true" to in the app-interface job and rhobs API tests will be skipped. + if os.Getenv("SKIP_RHOBS_SYNTHETICS_TESTS") == "true" { + Skip("RHOBS Synthetic Monitoring tests skipped via SKIP_RHOBS_SYNTHETICS_TESTS environment variable") + } + log.SetLogger(GinkgoLogr) var err error k8s, err = openshift.New(GinkgoLogr) @@ -1246,10 +1253,10 @@ func createRMOConfigMap(ctx context.Context, k8s *openshift.Client, creds *OIDCC Namespace: "openshift-route-monitor-operator", }, Data: map[string]string{ - "probe-api-url": creds.ProbeAPIURL, - "oidc-client-id": creds.ClientID, - "oidc-client-secret": creds.ClientSecret, - "oidc-issuer-url": creds.IssuerURL, + "probe-api-url": creds.ProbeAPIURL, + "oidc-client-id": creds.ClientID, + "oidc-client-secret": creds.ClientSecret, + "oidc-issuer-url": creds.IssuerURL, "skip-infrastructure-health-check": skipInfraHealthCheck, }, } From cc18b774f196b20c909c68420b43e6a93b70c57e Mon Sep 17 00:00:00 2001 From: erosente Date: Tue, 24 Feb 2026 12:33:32 -0500 Subject: [PATCH 12/12] Cleaned up code according to coderabbit suggestions. --- test/e2e/README.md | 2 +- test/e2e/route_monitor_operator_tests.go | 255 +++++++++-------------- 2 files changed, 105 insertions(+), 152 deletions(-) diff --git a/test/e2e/README.md b/test/e2e/README.md index 3a406e4b..fede3571 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -538,7 +538,7 @@ Required CRDs are not installed on the test cluster. **Solution:** The test should install CRDs automatically in `BeforeAll`. If this fails: - Check test logs for CRD installation errors - Verify cluster has permissions to create CRDs -- The CRD definitions are embedded in `route_monitor_operator_tests.go` (lines 46-96) +- The CRD definitions are embedded in `route_monitor_operator_tests.go` #### Tests can't reach RHOBS API diff --git a/test/e2e/route_monitor_operator_tests.go b/test/e2e/route_monitor_operator_tests.go index 9a610d74..c8adc2dd 100644 --- a/test/e2e/route_monitor_operator_tests.go +++ b/test/e2e/route_monitor_operator_tests.go @@ -320,13 +320,12 @@ var _ = Describe("Route Monitor Operator", Ordered, func() { }) var _ = Describe("RHOBS Synthetic Monitoring", Ordered, func() { var ( - k8s *openshift.Client - rhobsAPIURL string - rhobsTenant string - testNamespace string - pollingDuration time.Duration - probeActivationTimeout time.Duration - oidcCredentials *OIDCCredentials + k8s *openshift.Client + rhobsAPIURL string + rhobsTenant string + testNamespace string + pollingDuration time.Duration + oidcCredentials *OIDCCredentials ) const ( @@ -351,13 +350,6 @@ var _ = Describe("RHOBS Synthetic Monitoring", Ordered, func() { err = ensureCRDsInstalled(ctx, k8s) Expect(err).ShouldNot(HaveOccurred(), "failed to ensure CRDs are installed") - // Restart RMO to pick up CRDs (in case RMO was deployed before test ran) - By("restarting RMO to ensure HostedControlPlane controller is running") - err = restartRMODeployment(ctx, k8s) - if err != nil { - GinkgoLogr.Info("Warning: could not restart RMO deployment", "error", err) - } - // Register HyperShift and AWS VPCE schemes Expect(hypershiftv1beta1.AddToScheme(k8s.GetScheme())).Should(Succeed(), "unable to register HyperShift scheme") Expect(awsvpceapi.AddToScheme(k8s.GetScheme())).Should(Succeed(), "unable to register AWS VPCE scheme") @@ -371,14 +363,26 @@ var _ = Describe("RHOBS Synthetic Monitoring", Ordered, func() { oidcCredentials, err = getOrCreateOIDCCredentials(ctx, k8s, environment) Expect(err).ShouldNot(HaveOccurred(), "failed to fetch credentials") + // Restart RMO to pick up CRDs and ConfigMap (in case RMO was deployed before test ran) + By("restarting RMO to ensure HostedControlPlane controller and ConfigMap are loaded") + err = restartRMODeployment(ctx, k8s) + if err != nil { + GinkgoLogr.Info("Warning: could not restart RMO deployment", "error", err) + } + // Set RHOBS API URL from credentials (which comes from PROBE_API_URL env var) rhobsAPIURL = oidcCredentials.ProbeAPIURL Expect(rhobsAPIURL).ShouldNot(BeEmpty(), "PROBE_API_URL must be set (via app-interface in CI/CD or manually for local testing)") - rhobsTenant = getEnvOrDefault("RHOBS_TENANT", "hcp") - testNamespace = getEnvOrDefault("HCP_TEST_NAMESPACE", "clusters") + rhobsTenant = os.Getenv("RHOBS_TENANT") + if rhobsTenant == "" { + rhobsTenant = "hcp" + } + testNamespace = os.Getenv("HCP_TEST_NAMESPACE") + if testNamespace == "" { + testNamespace = "clusters" + } pollingDuration = 3 * time.Minute - probeActivationTimeout = 5 * time.Minute GinkgoLogr.Info("RHOBS Synthetic Monitoring test suite initialized", "environment", environment, @@ -424,18 +428,12 @@ var _ = Describe("RHOBS Synthetic Monitoring", Ordered, func() { namespace := fmt.Sprintf("%s-%s-%s", testNamespace, clusterID, hcpName) By(fmt.Sprintf("creating MC-style namespace: %s", namespace)) - ns := &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: namespace, - }, - } - err := k8s.Create(ctx, ns) + _, err := createNamespaceWithCleanup(ctx, k8s, namespace) Expect(err).ShouldNot(HaveOccurred(), "failed to create namespace") By(fmt.Sprintf("creating public HostedControlPlane with cluster ID: %s", clusterID)) hcp := createMCStyleHCP(clusterID, hcpName, namespace, hypershiftv1beta1.Public) - - err = k8s.Create(ctx, hcp) + err = createHCPWithCleanup(ctx, k8s, hcp, clusterID, rhobsAPIURL, oidcCredentials) Expect(err).ShouldNot(HaveOccurred(), "failed to create HostedControlPlane") By("setting HostedControlPlane status to Available") @@ -481,57 +479,6 @@ var _ = Describe("RHOBS Synthetic Monitoring", Ordered, func() { Expect(staticURL).Should(ContainSubstring(hostname), "probe URL should contain HCP hostname") Expect(labels["cluster-id"]).Should(Equal(clusterID), "probe should have correct cluster-id label") Expect(labels["private"]).Should(Equal("false"), "probe should be marked as public") - - // Optional: Wait for probe to become active - By("optionally waiting for probe to become active") - err = wait.PollUntilContextTimeout(ctx, 15*time.Second, probeActivationTimeout, false, func(ctx context.Context) (bool, error) { - p, err := getRHOBSProbe(rhobsAPIURL, probeID) - if err != nil { - return false, nil - } - status, ok := p["status"].(string) - if ok && status == "active" { - GinkgoLogr.Info("Probe activated successfully", "probeID", probeID) - return true, nil - } - GinkgoLogr.Info("Waiting for probe activation...", "currentStatus", status) - return false, nil - }) - if err != nil { - GinkgoLogr.Info("Warning: probe did not reach active status within timeout (may be expected)", "error", err) - } - - // Cleanup - DeferCleanup(func(ctx context.Context) { - By("cleaning up test HostedControlPlane") - err := k8s.Delete(ctx, hcp) - if err != nil { - GinkgoLogr.Info("Warning: failed to delete HCP", "error", err) - } - - By("verifying probe is deleted from RHOBS API") - err = wait.PollUntilContextTimeout(ctx, 5*time.Second, 2*time.Minute, false, func(ctx context.Context) (bool, error) { - probes, err := listRHOBSProbes(rhobsAPIURL, fmt.Sprintf("cluster-id=%s", clusterID), oidcCredentials) - if err != nil { - return false, nil - } - if len(probes) == 0 { - GinkgoLogr.Info("Probe successfully deleted", "clusterID", clusterID) - return true, nil - } - GinkgoLogr.Info("Waiting for probe deletion...", "remainingProbes", len(probes)) - return false, nil - }) - if err != nil { - GinkgoLogr.Info("Warning: probe may not have been cleaned up", "clusterID", clusterID) - } - - By("cleaning up test namespace") - err = k8s.Delete(ctx, ns) - if err != nil { - GinkgoLogr.Info("Warning: failed to delete namespace", "error", err) - } - }) }) // Phase 1 Test 3: Create probe for private HostedControlPlane @@ -542,18 +489,12 @@ var _ = Describe("RHOBS Synthetic Monitoring", Ordered, func() { namespace := fmt.Sprintf("%s-%s-%s", testNamespace, clusterID, hcpName) By(fmt.Sprintf("creating MC-style namespace: %s", namespace)) - ns := &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: namespace, - }, - } - err := k8s.Create(ctx, ns) + _, err := createNamespaceWithCleanup(ctx, k8s, namespace) Expect(err).ShouldNot(HaveOccurred(), "failed to create namespace") By(fmt.Sprintf("creating private HostedControlPlane with cluster ID: %s", clusterID)) hcp := createMCStyleHCP(clusterID, hcpName, namespace, hypershiftv1beta1.Private) - - err = k8s.Create(ctx, hcp) + err = createHCPWithCleanup(ctx, k8s, hcp, clusterID, rhobsAPIURL, oidcCredentials) Expect(err).ShouldNot(HaveOccurred(), "failed to create HostedControlPlane") By("setting HostedControlPlane status to Available") @@ -593,37 +534,6 @@ var _ = Describe("RHOBS Synthetic Monitoring", Ordered, func() { Expect(labels["cluster-id"]).Should(Equal(clusterID), "probe should have correct cluster-id label") Expect(labels["private"]).Should(Equal("true"), "probe should be marked as private") - - // Cleanup - DeferCleanup(func(ctx context.Context) { - By("cleaning up test resources") - err := k8s.Delete(ctx, hcp) - if err != nil { - GinkgoLogr.Info("Warning: failed to delete HCP", "error", err) - } - - By("verifying probe is deleted from RHOBS API") - err = wait.PollUntilContextTimeout(ctx, 5*time.Second, 2*time.Minute, false, func(ctx context.Context) (bool, error) { - probes, err := listRHOBSProbes(rhobsAPIURL, fmt.Sprintf("cluster-id=%s", clusterID), oidcCredentials) - if err != nil { - return false, nil - } - if len(probes) == 0 { - GinkgoLogr.Info("Probe successfully deleted", "clusterID", clusterID) - return true, nil - } - return false, nil - }) - if err != nil { - GinkgoLogr.Info("Warning: probe may not have been cleaned up", "clusterID", clusterID) - } - - By("cleaning up test namespace") - err = k8s.Delete(ctx, ns) - if err != nil { - GinkgoLogr.Info("Warning: failed to delete namespace", "error", err) - } - }) }) // Phase 1 Test 4: Probe deletion on HCP deletion (SREP-2832) @@ -634,12 +544,7 @@ var _ = Describe("RHOBS Synthetic Monitoring", Ordered, func() { namespace := fmt.Sprintf("%s-%s-%s", testNamespace, clusterID, hcpName) By(fmt.Sprintf("creating MC-style namespace: %s", namespace)) - ns := &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: namespace, - }, - } - err := k8s.Create(ctx, ns) + _, err := createNamespaceWithCleanup(ctx, k8s, namespace) Expect(err).ShouldNot(HaveOccurred(), "failed to create namespace") By(fmt.Sprintf("creating HostedControlPlane for deletion test with cluster ID: %s", clusterID)) @@ -648,6 +553,17 @@ var _ = Describe("RHOBS Synthetic Monitoring", Ordered, func() { err = k8s.Create(ctx, hcp) Expect(err).ShouldNot(HaveOccurred(), "failed to create HostedControlPlane") + // Register HCP cleanup for failure scenarios (test deletes it manually on success) + DeferCleanup(func(ctx context.Context) { + // Only clean up if HCP still exists (test may have deleted it) + hcpCheck := &hypershiftv1beta1.HostedControlPlane{} + err := k8s.Get(ctx, hcpName, namespace, hcpCheck) + if err == nil { + By(fmt.Sprintf("cleaning up HostedControlPlane after test failure: %s", hcpName)) + _ = k8s.Delete(ctx, hcp) + } + }) + By("setting HostedControlPlane status to Available") err = setHostedControlPlaneAvailable(ctx, k8s, hcp) Expect(err).ShouldNot(HaveOccurred(), "failed to update HostedControlPlane status") @@ -718,13 +634,6 @@ var _ = Describe("RHOBS Synthetic Monitoring", Ordered, func() { return false, nil }) Expect(err).ShouldNot(HaveOccurred(), "HostedControlPlane was not deleted - finalizer may be stuck (SREP-2966)") - - // Cleanup namespace after test - By("cleaning up test namespace") - err = k8s.Delete(ctx, ns) - if err != nil { - GinkgoLogr.Info("Warning: failed to delete namespace", "error", err) - } }) }) @@ -776,7 +685,7 @@ func listRHOBSProbes(baseURL, labelSelector string, creds *OIDCCredentials) ([]m reqURL := baseURL if labelSelector != "" { - reqURL += "?label_selector=" + labelSelector + reqURL += "?label_selector=" + url.QueryEscape(labelSelector) } req, err := http.NewRequest("GET", reqURL, nil) @@ -823,33 +732,67 @@ func listRHOBSProbes(baseURL, labelSelector string, creds *OIDCCredentials) ([]m return probes, nil } -func getRHOBSProbe(baseURL, probeID string) (map[string]interface{}, error) { - client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.Get(baseURL + "/" + probeID) - if err != nil { - return nil, fmt.Errorf("failed to get probe: %w", err) +// createNamespaceWithCleanup creates a namespace and registers cleanup via DeferCleanup +func createNamespaceWithCleanup(ctx context.Context, k8s *openshift.Client, namespace string) (*corev1.Namespace, error) { + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + }, } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return nil, fmt.Errorf("RHOBS API returned status %d: %s", resp.StatusCode, string(body)) + err := k8s.Create(ctx, ns) + if err != nil { + return nil, err } - var probe map[string]interface{} - if err := json.NewDecoder(resp.Body).Decode(&probe); err != nil { - return nil, fmt.Errorf("failed to decode probe response: %w", err) - } + // Register cleanup immediately + DeferCleanup(func(ctx context.Context) { + By(fmt.Sprintf("cleaning up test namespace: %s", namespace)) + err := k8s.Delete(ctx, ns) + if err != nil && !k8serrors.IsNotFound(err) { + GinkgoLogr.Info("Warning: failed to delete namespace", "namespace", namespace, "error", err) + } + }) - return probe, nil + return ns, nil } -func getEnvOrDefault(key, defaultValue string) string { - if value := os.Getenv(key); value != "" { - return value +// createHCPWithCleanup creates an HCP and registers cleanup (including probe cleanup from RHOBS API) +func createHCPWithCleanup(ctx context.Context, k8s *openshift.Client, hcp *hypershiftv1beta1.HostedControlPlane, clusterID, rhobsAPIURL string, oidcCredentials *OIDCCredentials) error { + err := k8s.Create(ctx, hcp) + if err != nil { + return err } - return defaultValue + + // Register cleanup immediately + DeferCleanup(func(ctx context.Context) { + By(fmt.Sprintf("cleaning up test HostedControlPlane: %s", hcp.Name)) + err := k8s.Delete(ctx, hcp) + if err != nil && !k8serrors.IsNotFound(err) { + GinkgoLogr.Info("Warning: failed to delete HCP", "hcp", hcp.Name, "error", err) + } + + // Also verify probe cleanup from RHOBS API + By(fmt.Sprintf("verifying probe cleanup for cluster: %s", clusterID)) + err = wait.PollUntilContextTimeout(ctx, 5*time.Second, 2*time.Minute, false, func(ctx context.Context) (bool, error) { + probes, err := listRHOBSProbes(rhobsAPIURL, fmt.Sprintf("cluster-id=%s", clusterID), oidcCredentials) + if err != nil { + return false, nil + } + if len(probes) == 0 { + GinkgoLogr.Info("Probe successfully deleted", "clusterID", clusterID) + return true, nil + } + GinkgoLogr.Info("Waiting for probe deletion...", "remainingProbes", len(probes)) + return false, nil + }) + if err != nil { + GinkgoLogr.Info("Warning: probe may not have been cleaned up", "clusterID", clusterID) + } + }) + + return nil } // getEnvironment determines the current environment (int, stage, prod) from osde2e provider @@ -1221,12 +1164,19 @@ func getOIDCCredentials(ctx context.Context, environment string) (*OIDCCredentia // In CI/CD, these are auto-loaded by osde2e from app-interface (see SDCICD-1739) // For local testing, export these same variables if clientID := os.Getenv("EXTERNAL_SECRET_OIDC_CLIENT_ID"); clientID != "" { + clientSecret := os.Getenv("EXTERNAL_SECRET_OIDC_CLIENT_SECRET") + issuerURL := os.Getenv("EXTERNAL_SECRET_OIDC_ISSUER_URL") + probeAPIURL := os.Getenv("PROBE_API_URL") + if clientSecret == "" || issuerURL == "" || probeAPIURL == "" { + return nil, fmt.Errorf("EXTERNAL_SECRET_OIDC_CLIENT_ID is set but one or more required env vars are missing: EXTERNAL_SECRET_OIDC_CLIENT_SECRET=%t, EXTERNAL_SECRET_OIDC_ISSUER_URL=%t, PROBE_API_URL=%t", + clientSecret != "", issuerURL != "", probeAPIURL != "") + } GinkgoLogr.Info("Using OIDC credentials from EXTERNAL_SECRET_ environment variables") return &OIDCCredentials{ ClientID: clientID, - ClientSecret: os.Getenv("EXTERNAL_SECRET_OIDC_CLIENT_SECRET"), - IssuerURL: os.Getenv("EXTERNAL_SECRET_OIDC_ISSUER_URL"), - ProbeAPIURL: os.Getenv("PROBE_API_URL"), + ClientSecret: clientSecret, + IssuerURL: issuerURL, + ProbeAPIURL: probeAPIURL, }, nil } @@ -1245,7 +1195,10 @@ Note: In osde2e/CI, these variables (including SKIP_INFRASTRUCTURE_HEALTH_CHECK) // createRMOConfigMap creates the RMO config ConfigMap with OIDC credentials func createRMOConfigMap(ctx context.Context, k8s *openshift.Client, creds *OIDCCredentials) error { // Read skip-infrastructure-health-check from environment, default to "false" - skipInfraHealthCheck := getEnvOrDefault("SKIP_INFRASTRUCTURE_HEALTH_CHECK", "false") + skipInfraHealthCheck := os.Getenv("SKIP_INFRASTRUCTURE_HEALTH_CHECK") + if skipInfraHealthCheck == "" { + skipInfraHealthCheck = "false" + } configMap := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{