From 348df94c74ddc6c6e3d431667934376a919289d1 Mon Sep 17 00:00:00 2001 From: Fabricio Aguiar Date: Wed, 4 Nov 2026 17:33:58 +0000 Subject: [PATCH 1/3] refactor(e2e): expose shared scenario state for component steps Prepare the PR3 package boundary with reusable world, resource, condition, and command helpers. Keep step registration intact and observe demo commands through an optional interface so shared steps do not depend on the demo recorder. Refs: OPRUN-4784 Signed-off-by: Fabricio Aguiar rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED --- test/internal/e2e/steps/asciicast_recorder.go | 2 +- test/internal/e2e/steps/conditions.go | 14 +- test/internal/e2e/steps/demo_steps.go | 70 +- test/internal/e2e/steps/group_steps.go | 78 +- test/internal/e2e/steps/ha_steps.go | 24 +- test/internal/e2e/steps/hooks.go | 177 ++-- test/internal/e2e/steps/proxy_steps.go | 50 +- test/internal/e2e/steps/steps.go | 992 +++++++++--------- test/internal/e2e/steps/tls_steps.go | 48 +- test/internal/e2e/steps/upgrade_steps.go | 84 +- 10 files changed, 784 insertions(+), 755 deletions(-) diff --git a/test/internal/e2e/steps/asciicast_recorder.go b/test/internal/e2e/steps/asciicast_recorder.go index 62ef69e01b..6f3dad623e 100644 --- a/test/internal/e2e/steps/asciicast_recorder.go +++ b/test/internal/e2e/steps/asciicast_recorder.go @@ -13,7 +13,7 @@ import ( type recorderContextKey struct{} func WithRecorder(ctx context.Context, rec *AsciiCastRecorder) context.Context { - return context.WithValue(ctx, recorderContextKey{}, rec) + return context.WithValue(WithCommandRecorder(ctx, rec), recorderContextKey{}, rec) } func RecorderFromContext(ctx context.Context) *AsciiCastRecorder { diff --git a/test/internal/e2e/steps/conditions.go b/test/internal/e2e/steps/conditions.go index a0c5aed505..b59e539e51 100644 --- a/test/internal/e2e/steps/conditions.go +++ b/test/internal/e2e/steps/conditions.go @@ -18,8 +18,8 @@ type resourceConditions struct { } `json:"status"` } -func readResourceConditions(ctx context.Context, resourceType, resourceName string) (*resourceConditions, error) { - out, err := k8sClient(ctx, "get", resourceType, resourceName, "-o", "json") +func ReadResourceConditions(ctx context.Context, resourceType, resourceName string) (*resourceConditions, error) { + out, err := K8sClient(ctx, "get", resourceType, resourceName, "-o", "json") if err != nil { return nil, fmt.Errorf("reading %s %q: %w", resourceType, resourceName, err) } @@ -30,21 +30,21 @@ func readResourceConditions(ctx context.Context, resourceType, resourceName stri return &resource, nil } -func generationObserved(generation, observedGeneration int64) bool { +func GenerationObserved(generation, observedGeneration int64) bool { return generation > 0 && observedGeneration == generation } -// currentCondition returns only the requested condition, and only if it reflects +// CurrentCondition returns only the requested condition, and only if it reflects // the current generation. An unrelated up-to-date condition is not sufficient. -func currentCondition(generation int64, conditions []metav1.Condition, conditionType string) *metav1.Condition { +func CurrentCondition(generation int64, conditions []metav1.Condition, conditionType string) *metav1.Condition { condition := apimeta.FindStatusCondition(conditions, conditionType) - if condition == nil || !generationObserved(generation, condition.ObservedGeneration) { + if condition == nil || !GenerationObserved(generation, condition.ObservedGeneration) { return nil } return condition } -func conditionMatches(condition *metav1.Condition, conditionStatus metav1.ConditionStatus, reason *string, msgCmp msgMatchFn) bool { +func ConditionMatches(condition *metav1.Condition, conditionStatus metav1.ConditionStatus, reason *string, msgCmp MsgMatchFn) bool { return condition != nil && condition.Status == conditionStatus && (reason == nil || condition.Reason == *reason) && (msgCmp == nil || msgCmp(condition.Message)) diff --git a/test/internal/e2e/steps/demo_steps.go b/test/internal/e2e/steps/demo_steps.go index 28a79dbf68..eebe4f7850 100644 --- a/test/internal/e2e/steps/demo_steps.go +++ b/test/internal/e2e/steps/demo_steps.go @@ -50,25 +50,25 @@ func bash(ctx context.Context, script string) (string, error) { } logger.V(1).Info("Output", "command", script, "output", stdout) - if rec := RecorderFromContext(ctx); rec != nil { + if rec := CommandRecorderFromContext(ctx); rec != nil { rec.RecordCommand(script, stdout, stderr, elapsed) } return stdout, err } func CatalogReportsConditionWithoutReason(ctx context.Context, catalogUserName, conditionType, conditionStatus string) error { - sc := scenarioCtx(ctx) - catalogName, ok := sc.catalogs[catalogUserName] + sc := Scenario(ctx) + catalogName, ok := sc.Catalogs[catalogUserName] if !ok { - if _, err := k8sClient(ctx, "get", "clustercatalog", catalogUserName); err != nil { + if _, err := K8sClient(ctx, "get", "clustercatalog", catalogUserName); err != nil { return fmt.Errorf("catalog %q was not created by this scenario and does not exist on the cluster", catalogUserName) } catalogName = catalogUserName } - err := waitForCondition(ctx, "clustercatalog", catalogName, conditionType, metav1.ConditionStatus(conditionStatus), nil, nil) + err := WaitForCondition(ctx, "clustercatalog", catalogName, conditionType, metav1.ConditionStatus(conditionStatus), nil, nil) if err == nil { - if rec := RecorderFromContext(ctx); rec != nil { - out, _ := k8sClient(ctx, "get", "clustercatalog", catalogName, + if rec := CommandRecorderFromContext(ctx); rec != nil { + out, _ := K8sClient(ctx, "get", "clustercatalog", catalogName, "-o", fmt.Sprintf("jsonpath={.status.conditions[?(@.type==\"%s\")]}", conditionType)) var pretty bytes.Buffer if jsonErr := json.Indent(&pretty, []byte(out), "", " "); jsonErr == nil { @@ -85,16 +85,16 @@ func CatalogReportsConditionWithoutReason(ctx context.Context, catalogUserName, } func ensureCatalogPortForward(ctx context.Context) (string, error) { - sc := scenarioCtx(ctx) - if sc.catalogAddr != "" { - if catalogPortForwardAlive(sc.catalogAddr) { - return sc.catalogAddr, nil + sc := Scenario(ctx) + if sc.CatalogAddr != "" { + if catalogPortForwardAlive(sc.CatalogAddr) { + return sc.CatalogAddr, nil } - logger.V(1).Info("Catalog port-forward is dead, re-establishing", "addr", sc.catalogAddr) + logger.V(1).Info("Catalog port-forward is dead, re-establishing", "addr", sc.CatalogAddr) resetCatalogPortForward(ctx) } - ns := componentNamespaces["catalogd"] + ns := ComponentNamespaces["catalogd"] target, err := catalogdLeaderPod(ctx, ns) port := int32(443) if err != nil { @@ -108,17 +108,17 @@ func ensureCatalogPortForward(ctx context.Context) (string, error) { if err != nil { return "", fmt.Errorf("failed to start catalog port-forward to %s: %w", target, err) } - sc.catalogAddr = addr - sc.catalogCleanup = cleanup + sc.CatalogAddr = addr + sc.CatalogCleanup = cleanup - waitFor(ctx, func() bool { + WaitFor(ctx, func() bool { return catalogPortForwardAlive(addr) }) return addr, nil } func catalogdLeaderPod(ctx context.Context, ns string) (string, error) { - holder, err := k8sClient(ctx, "get", "lease", "catalogd-operator-lock", "-n", ns, + holder, err := K8sClient(ctx, "get", "lease", "catalogd-operator-lock", "-n", ns, "-o", "jsonpath={.spec.holderIdentity}") if err != nil { return "", fmt.Errorf("failed to get catalogd leader lease: %w", err) @@ -156,12 +156,12 @@ func catalogPortForwardAlive(addr string) bool { // CatalogdHA, non-leader pods return 404 (empty local cache); resetting // lets the next retry potentially reach the leader pod. func resetCatalogPortForward(ctx context.Context) { - sc := scenarioCtx(ctx) - if sc.catalogCleanup != nil { - sc.catalogCleanup() + sc := Scenario(ctx) + if sc.CatalogCleanup != nil { + sc.CatalogCleanup() } - sc.catalogAddr = "" - sc.catalogCleanup = nil + sc.CatalogAddr = "" + sc.CatalogCleanup = nil } func catalogCurlJq(ctx context.Context, catalogName, jqFilter string) (string, error) { @@ -185,7 +185,7 @@ func catalogCurlJq(ctx context.Context, catalogName, jqFilter string) (string, e } func CatalogContainsSomePackages(ctx context.Context, catalogName string) error { - waitFor(ctx, func() bool { + WaitFor(ctx, func() bool { out, err := catalogCurlJq(ctx, catalogName, `objects | select(.schema == "olm.package") | .name`) if err != nil { @@ -202,7 +202,7 @@ func CatalogContainsSomePackages(ctx context.Context, catalogName string) error } func PackageHasSomeChannels(ctx context.Context, packageName, catalogName string) error { - waitFor(ctx, func() bool { + WaitFor(ctx, func() bool { out, err := catalogCurlJq(ctx, catalogName, fmt.Sprintf(`objects | select(.schema == "olm.channel") | select(.package == "%s") | .name`, packageName)) if err != nil { @@ -219,7 +219,7 @@ func PackageHasSomeChannels(ctx context.Context, packageName, catalogName string } func PackageHasSomeBundles(ctx context.Context, packageName, catalogName string) error { - waitFor(ctx, func() bool { + WaitFor(ctx, func() bool { out, err := catalogCurlJq(ctx, catalogName, fmt.Sprintf(`objects | select(.schema == "olm.bundle") | select(.package == "%s") | .name`, packageName)) if err != nil { @@ -236,8 +236,8 @@ func PackageHasSomeBundles(ctx context.Context, packageName, catalogName string) } func RolebindingsReferenceServiceAccount(ctx context.Context, rbNamespace, saName, saNamespace string) error { - saNamespace = substituteScenarioVars(saNamespace, scenarioCtx(ctx)) - out, err := k8sClient(ctx, "get", "rolebindings", "-n", rbNamespace, "-o", "json") + saNamespace = SubstituteScenarioVars(saNamespace, Scenario(ctx)) + out, err := K8sClient(ctx, "get", "rolebindings", "-n", rbNamespace, "-o", "json") if err != nil { return fmt.Errorf("failed to list rolebindings in namespace %q: %w", rbNamespace, err) } @@ -261,8 +261,8 @@ func RolebindingsReferenceServiceAccount(ctx context.Context, rbNamespace, saNam for _, rb := range rbList.Items { for _, s := range rb.Subjects { if s.Kind == "ServiceAccount" && s.Name == saName && s.Namespace == saNamespace { - if rec := RecorderFromContext(ctx); rec != nil { - subjectsOut, _ := k8sClient(ctx, "get", "rolebinding", rb.Metadata.Name, "-n", rbNamespace, + if rec := CommandRecorderFromContext(ctx); rec != nil { + subjectsOut, _ := K8sClient(ctx, "get", "rolebinding", rb.Metadata.Name, "-n", rbNamespace, "-o", "jsonpath={.subjects}") rec.RecordCustom( fmt.Sprintf("kubectl get rolebinding %s -n %s -o jsonpath='{.subjects}' | jq .", rb.Metadata.Name, rbNamespace), @@ -277,10 +277,10 @@ func RolebindingsReferenceServiceAccount(ctx context.Context, rbNamespace, saNam } func PodHasContainerCount(ctx context.Context, podName string, expected int) error { - sc := scenarioCtx(ctx) + sc := Scenario(ctx) var actual int - waitFor(ctx, func() bool { - out, err := k8sClient(ctx, "get", "pod", podName, "-n", sc.namespace, "-o", "jsonpath={.spec.containers[*].name}") + WaitFor(ctx, func() bool { + out, err := K8sClient(ctx, "get", "pod", podName, "-n", sc.Namespace, "-o", "jsonpath={.spec.containers[*].name}") if err != nil { return false } @@ -288,10 +288,10 @@ func PodHasContainerCount(ctx context.Context, podName string, expected int) err actual = len(names) return actual == expected }) - if rec := RecorderFromContext(ctx); rec != nil { - out, _ := k8sClient(ctx, "get", "pod", podName, "-n", sc.namespace, "-o", "jsonpath={.spec.containers[*].name}") + if rec := CommandRecorderFromContext(ctx); rec != nil { + out, _ := K8sClient(ctx, "get", "pod", podName, "-n", sc.Namespace, "-o", "jsonpath={.spec.containers[*].name}") rec.RecordCustom( - fmt.Sprintf("kubectl get pod %s -n %s -o jsonpath='{.spec.containers[*].name}'", podName, sc.namespace), + fmt.Sprintf("kubectl get pod %s -n %s -o jsonpath='{.spec.containers[*].name}'", podName, sc.Namespace), out+"\n", "", ) } diff --git a/test/internal/e2e/steps/group_steps.go b/test/internal/e2e/steps/group_steps.go index bf5111ba98..a947ed2354 100644 --- a/test/internal/e2e/steps/group_steps.go +++ b/test/internal/e2e/steps/group_steps.go @@ -18,16 +18,16 @@ import ( ) // ResourceWithNamespaceOwnerApplied resolves a real cluster-scoped Namespace owner -// before applying the resource so garbage collection sees valid owner metadata. +// before applying the Resource so garbage collection sees valid owner metadata. func ResourceWithNamespaceOwnerApplied(ctx context.Context, ownerName string, template *godog.DocString) error { - sc := scenarioCtx(ctx) - obj, err := toUnstructured(substituteScenarioVars(template.Content, sc)) + sc := Scenario(ctx) + obj, err := ToUnstructured(SubstituteScenarioVars(template.Content, sc)) if err != nil { return err } - ownerName = substituteScenarioVars(ownerName, sc) + ownerName = SubstituteScenarioVars(ownerName, sc) if ownerName != "" { - owner, err := getResource("namespace", ownerName, "") + owner, err := GetResource("namespace", ownerName, "") if err != nil { return err } @@ -46,35 +46,35 @@ func ResourceWithNamespaceOwnerApplied(ctx context.Context, ownerName string, te } func RememberResourceUID(ctx context.Context, resourceName string) error { - sc := scenarioCtx(ctx) - resourceName = substituteScenarioVars(resourceName, sc) + sc := Scenario(ctx) + resourceName = SubstituteScenarioVars(resourceName, sc) kind, name, found := strings.Cut(resourceName, "/") if !found { return fmt.Errorf("resource %s is not in the format /", resourceName) } - obj, err := getResource(kind, name, sc.namespace) + obj, err := GetResource(kind, name, sc.Namespace) if err != nil { return err } if obj.GetUID() == "" { return fmt.Errorf("resource %s has no UID", resourceName) } - if sc.resourceUIDs == nil { - sc.resourceUIDs = map[string]types.UID{} + if sc.ResourceUIDs == nil { + sc.ResourceUIDs = map[string]types.UID{} } - sc.resourceUIDs[resourceName] = obj.GetUID() + sc.ResourceUIDs[resourceName] = obj.GetUID() return nil } func ResourceKeepsUID(ctx context.Context, resourceName string) error { - sc := scenarioCtx(ctx) - resourceName = substituteScenarioVars(resourceName, sc) - uid, found := sc.resourceUIDs[resourceName] + sc := Scenario(ctx) + resourceName = SubstituteScenarioVars(resourceName, sc) + uid, found := sc.ResourceUIDs[resourceName] if !found { return fmt.Errorf("no UID remembered for resource %s", resourceName) } kind, name, _ := strings.Cut(resourceName, "/") - obj, err := getResource(kind, name, sc.namespace) + obj, err := GetResource(kind, name, sc.Namespace) if err != nil { return err } @@ -84,9 +84,9 @@ func ResourceKeepsUID(ctx context.Context, resourceName string) error { return nil } -// listClusterObjectSetsInGroup selects revisions by group on the API server. -func listClusterObjectSetsInGroup(ctx context.Context, group string) ([]ocv1.ClusterObjectSet, error) { - out, err := k8sClient(ctx, "get", "clusterobjectsets", "--field-selector", "spec.group="+group, "-o", "json") +// ListClusterObjectSetsInGroup selects revisions by group on the API server. +func ListClusterObjectSetsInGroup(ctx context.Context, group string) ([]ocv1.ClusterObjectSet, error) { + out, err := K8sClient(ctx, "get", "clusterobjectsets", "--field-selector", "spec.group="+group, "-o", "json") if err != nil { return nil, err } @@ -98,11 +98,11 @@ func listClusterObjectSetsInGroup(ctx context.Context, group string) ([]ocv1.Clu } func ClusterObjectSetHasGroup(ctx context.Context, revisionName, group string) error { - sc := scenarioCtx(ctx) - revisionName = substituteScenarioVars(revisionName, sc) - group = substituteScenarioVars(group, sc) - waitFor(ctx, func() bool { - obj, err := getResource("clusterobjectset", revisionName, "") + sc := Scenario(ctx) + revisionName = SubstituteScenarioVars(revisionName, sc) + group = SubstituteScenarioVars(group, sc) + WaitFor(ctx, func() bool { + obj, err := GetResource("clusterobjectset", revisionName, "") if err != nil { return false } @@ -115,11 +115,11 @@ func ClusterObjectSetHasGroup(ctx context.Context, revisionName, group string) e // ClusterObjectSetObjectsUseSSAManager checks actual managed fields on every // inline or externally stored phase object, including their revision owner. func ClusterObjectSetObjectsUseSSAManager(ctx context.Context, revisionName, manager string) error { - sc := scenarioCtx(ctx) - revisionName = substituteScenarioVars(revisionName, sc) - manager = substituteScenarioVars(manager, sc) - waitFor(ctx, func() bool { - obj, err := getResource("clusterobjectset", revisionName, "") + sc := Scenario(ctx) + revisionName = SubstituteScenarioVars(revisionName, sc) + manager = SubstituteScenarioVars(manager, sc) + WaitFor(ctx, func() bool { + obj, err := GetResource("clusterobjectset", revisionName, "") if err != nil { return false } @@ -132,7 +132,7 @@ func ClusterObjectSetObjectsUseSSAManager(ctx context.Context, revisionName, man for _, phaseObj := range phase.Objects { desired := &phaseObj.InlineManifest if phaseObj.Type == ocv1.ObjectTypeObjectReference { - desired, err = resolveObjectRef(phaseObj.ObjectRef) + desired, err = ResolveObjectRef(phaseObj.ObjectRef) if err != nil { return false } @@ -142,7 +142,7 @@ func ClusterObjectSetObjectsUseSSAManager(ctx context.Context, revisionName, man if desired.GetNamespace() != "" { args = append(args, "-n", desired.GetNamespace()) } - out, err := k8sClient(ctx, args...) + out, err := K8sClient(ctx, args...) if err != nil { return false } @@ -178,26 +178,26 @@ func ClusterObjectSetObjectsUseSSAManager(ctx context.Context, revisionName, man } func RememberClusterObjectSetSecrets(ctx context.Context, revisionName string) error { - sc := scenarioCtx(ctx) - revisionName = substituteScenarioVars(revisionName, sc) - names, err := collectReferredSecretNames(ctx, revisionName) + sc := Scenario(ctx) + revisionName = SubstituteScenarioVars(revisionName, sc) + names, err := CollectReferredSecretNames(ctx, revisionName) if err != nil { return err } for _, name := range names { - sc.revisionSecrets = append(sc.revisionSecrets, resource{kind: "secret", name: name, namespace: olmNamespace}) + sc.RevisionSecrets = append(sc.RevisionSecrets, Resource{Kind: "secret", Name: name, Namespace: OLMNamespace}) } return nil } func RememberedRevisionSecretsRemoved(ctx context.Context) error { - sc := scenarioCtx(ctx) - if len(sc.revisionSecrets) == 0 { + sc := Scenario(ctx) + if len(sc.RevisionSecrets) == 0 { return fmt.Errorf("no revision secrets were remembered") } - for _, secret := range sc.revisionSecrets { - waitFor(ctx, func() bool { - out, err := k8sClient(ctx, "get", secret.kind, secret.name, "-n", secret.namespace, "--ignore-not-found", "-o", "name") + for _, secret := range sc.RevisionSecrets { + WaitFor(ctx, func() bool { + out, err := K8sClient(ctx, "get", secret.Kind, secret.Name, "-n", secret.Namespace, "--ignore-not-found", "-o", "name") return err == nil && strings.TrimSpace(out) == "" }) } diff --git a/test/internal/e2e/steps/ha_steps.go b/test/internal/e2e/steps/ha_steps.go index 3209299c66..8e003a0e58 100644 --- a/test/internal/e2e/steps/ha_steps.go +++ b/test/internal/e2e/steps/ha_steps.go @@ -10,28 +10,28 @@ import ( // catalogdHAFeature gates scenarios that require a multi-node cluster. // It is set to true in BeforeSuite when the cluster has at least 2 nodes, -// which is the case for the experimental e2e suite (kind-config-2node.yaml) +// which is the case for the experimental e2e suite (Kind-config-2node.yaml) // but not the standard suite. const catalogdHAFeature featuregate.Feature = "CatalogdHA" // CatalogdLeaderPodIsForceDeleted force-deletes the catalogd leader pod to simulate leader loss. -// The pod is identified from sc.leaderPods["catalogd"] (populated by a prior +// The pod is identified from sc.LeaderPods["catalogd"] (populated by a prior // "catalogd is ready to reconcile resources" step). Force-deletion is equivalent to // an abrupt process crash: the lease is no longer renewed and the surviving pod // acquires leadership after the lease expires. // -// Note: stopping the kind node container is not used here because both nodes in the +// Note: stopping the Kind node container is not used here because both nodes in the // experimental 2-node cluster are control-plane nodes that run etcd — stopping either // would break etcd quorum and make the API server unreachable for the rest of the test. func CatalogdLeaderPodIsForceDeleted(ctx context.Context) error { - sc := scenarioCtx(ctx) - leaderPod := sc.leaderPods["catalogd"] + sc := Scenario(ctx) + leaderPod := sc.LeaderPods["catalogd"] if leaderPod == "" { return fmt.Errorf("catalogd leader pod not found in scenario context; run 'catalogd is ready to reconcile resources' first") } logger.Info("Force-deleting catalogd leader pod", "pod", leaderPod) - if _, err := k8sClient(ctx, "delete", "pod", leaderPod, "-n", namespaceForComponent("catalogd"), + if _, err := K8sClient(ctx, "delete", "pod", leaderPod, "-n", NamespaceForComponent("catalogd"), "--force", "--grace-period=0"); err != nil { return fmt.Errorf("failed to force-delete catalogd leader pod %q: %w", leaderPod, err) } @@ -40,13 +40,13 @@ func CatalogdLeaderPodIsForceDeleted(ctx context.Context) error { // NewCatalogdLeaderIsElected polls the catalogd leader election lease until the holder // identity changes to a pod other than the deleted leader. It updates -// sc.leaderPods["catalogd"] with the new leader pod name. +// sc.LeaderPods["catalogd"] with the new leader pod Name. func NewCatalogdLeaderIsElected(ctx context.Context) error { - sc := scenarioCtx(ctx) - oldLeader := sc.leaderPods["catalogd"] + sc := Scenario(ctx) + oldLeader := sc.LeaderPods["catalogd"] - waitFor(ctx, func() bool { - holder, err := k8sClient(ctx, "get", "lease", leaseNames["catalogd"], "-n", namespaceForComponent("catalogd"), + WaitFor(ctx, func() bool { + holder, err := K8sClient(ctx, "get", "lease", leaseNames["catalogd"], "-n", NamespaceForComponent("catalogd"), "-o", "jsonpath={.spec.holderIdentity}") if err != nil || holder == "" { return false @@ -55,7 +55,7 @@ func NewCatalogdLeaderIsElected(ctx context.Context) error { if newPod == oldLeader { return false } - sc.leaderPods["catalogd"] = newPod + sc.LeaderPods["catalogd"] = newPod logger.Info("New catalogd leader elected", "pod", newPod) return true }) diff --git a/test/internal/e2e/steps/hooks.go b/test/internal/e2e/steps/hooks.go index 7f319c83e0..4ba07f5ced 100644 --- a/test/internal/e2e/steps/hooks.go +++ b/test/internal/e2e/steps/hooks.go @@ -11,6 +11,7 @@ import ( "regexp" "strconv" "strings" + "time" "github.com/cucumber/godog" "github.com/go-logr/logr" @@ -26,62 +27,63 @@ import ( "github.com/operator-framework/operator-controller/internal/operator-controller/features" ) -type resource struct { - name string - kind string - namespace string +type Resource struct { + Name string + Kind string + Namespace string } // deploymentRestore records the original state of a deployment so it can be // rolled back after a test that modifies deployment configuration. type deploymentRestore struct { - name string // deployment name - namespace string + Name string // deployment name + Namespace string containerName string // container to patch (for env var restores) patchedArgs bool // true when container args were modified (for TLS profile patches) originalArgs []string // original container args; may be nil if args were unset originalEnv []string // original env vars as "NAME=VALUE" (for proxy patches) } -type scenarioContext struct { - id string - featureName string - scenarioName string - namespace string - clusterExtensionName string - clusterObjectSetName string - catalogs map[string]string // user-chosen name -> ClusterCatalog resource name - catalogPackageNames map[string]string // original package name -> parameterized name - addedResources []resource - removedResources []unstructured.Unstructured - metricsResponse map[string]string - leaderPods map[string]string // component name -> leader pod name +// World holds scenario state shared by component step packages. +type World struct { + ID string + FeatureName string + ScenarioName string + Namespace string + ClusterExtensionName string + ClusterObjectSetName string + Catalogs map[string]string // user-chosen name -> ClusterCatalog resource name + CatalogPackageNames map[string]string // original package name -> parameterized name + AddedResources []Resource + RemovedResources []unstructured.Unstructured + MetricsResponse map[string]string + LeaderPods map[string]string // component name -> leader pod name deploymentRestores []deploymentRestore - extensionObjects []client.Object - revisionSecrets []resource - resourceUIDs map[string]types.UID + ExtensionObjects []client.Object + RevisionSecrets []Resource + ResourceUIDs map[string]types.UID proxy *recordingProxy - catalogAddr string - catalogCleanup func() - lastApplyStderr string + CatalogAddr string + CatalogCleanup func() + LastApplyStderr string } // GatherClusterExtensionObjects collects all resources related to the ClusterExtension container in // either their Helm release Secret or ClusterObjectSet depending on the applier being used // and saves them into the context. -func (s *scenarioContext) GatherClusterExtensionObjects() error { - objs, err := listExtensionResources(s.clusterExtensionName) +func (s *World) GatherClusterExtensionObjects() error { + objs, err := listExtensionResources(s.ClusterExtensionName) if err != nil { return fmt.Errorf("failed to load extension resources into context: %w", err) } - s.extensionObjects = objs + s.ExtensionObjects = objs return nil } // GetClusterExtensionObjects returns the ClusterExtension objects currently saved into the context. // Will always return nil until GatherClusterExtensionObjects is called -func (s *scenarioContext) GetClusterExtensionObjects() []client.Object { - return s.extensionObjects +func (s *World) GetClusterExtensionObjects() []client.Object { + return s.ExtensionObjects } type contextKey string @@ -92,7 +94,7 @@ const ( var ( devMode = false - featureGates = map[featuregate.Feature]bool{ + FeatureGates = map[featuregate.Feature]bool{ features.WebhookProviderCertManager: true, features.SingleOwnNamespaceInstallSupport: false, features.WebhookProviderOpenshiftServiceCA: false, @@ -121,7 +123,7 @@ func RegisterHooks(sc *godog.ScenarioContext) { // The catalogd return value may be nil when OLM is not yet installed (upgrade scenarios // install it in a Background step). func detectOLMDeployments() (*appsv1.Deployment, *appsv1.Deployment, error) { - raw, err := k8sClient(context.Background(), "get", "deployments", "-A", "-l", "app.kubernetes.io/part-of=olm", "-o", "jsonpath={.items}") + raw, err := K8sClient(context.Background(), "get", "deployments", "-A", "-l", "app.kubernetes.io/part-of=olm", "-o", "jsonpath={.items}") if err != nil { return nil, nil, err } @@ -155,9 +157,9 @@ func BeforeSuite() { // Enable HA scenarios when the cluster has at least 2 nodes. This runs // unconditionally so that upgrade scenarios (which install OLM in a Background // step and return early below) still get the gate set correctly. - if out, err := k8sClient(context.Background(), "get", "nodes", "--no-headers", "-o", "name"); err == nil && + if out, err := K8sClient(context.Background(), "get", "nodes", "--no-headers", "-o", "name"); err == nil && len(strings.Fields(strings.TrimSpace(out))) >= 2 { - featureGates[catalogdHAFeature] = true + FeatureGates[catalogdHAFeature] = true } olm, catalogdDep, err := detectOLMDeployments() @@ -165,15 +167,15 @@ func BeforeSuite() { logger.Info("OLM deployments not found; skipping feature gate detection (upgrade scenarios will install OLM in Background)") return } - olmNamespace = olm.Namespace - componentNamespaces["operator-controller"] = olmNamespace + OLMNamespace = olm.Namespace + ComponentNamespaces["operator-controller"] = OLMNamespace - // Catalogd may be in a different namespace than operator-controller. - catalogdNS := olmNamespace + // Catalogd may be in a different Namespace than operator-controller. + catalogdNS := OLMNamespace if catalogdDep != nil { catalogdNS = catalogdDep.Namespace } - componentNamespaces["catalogd"] = catalogdNS + ComponentNamespaces["catalogd"] = catalogdNS // Refine CatalogdHA based on actual catalogd replica count now that catalogdNS is // known. The node-count check above can fire on any multi-node cluster even when @@ -182,7 +184,7 @@ func BeforeSuite() { // was not found or the replica count is not parseable. if catalogdDep != nil { if replicas := catalogdDep.Spec.Replicas; replicas != nil { - featureGates[catalogdHAFeature] = *replicas >= 2 + FeatureGates[catalogdHAFeature] = *replicas >= 2 } } @@ -195,18 +197,18 @@ func BeforeSuite() { if err != nil { panic(fmt.Errorf("failed to parse feature gate %q=%q: %v", matches[1], matches[2], err)) } - featureGates[featuregate.Feature(matches[1])] = v + FeatureGates[featuregate.Feature(matches[1])] = v } } } } - logger.Info(fmt.Sprintf("Enabled feature gates: %v", featureGates)) + logger.Info(fmt.Sprintf("Enabled feature gates: %v", FeatureGates)) } func CheckFeatureTags(ctx context.Context, sc *godog.Scenario) (context.Context, error) { for _, tag := range sc.Tags { - if enabled, found := featureGates[featuregate.Feature(tag.Name[1:])]; found && !enabled { + if enabled, found := FeatureGates[featuregate.Feature(tag.Name[1:])]; found && !enabled { logger.Info(fmt.Sprintf("Skipping scenario %q because feature gate %q is disabled", sc.Name, tag.Name[1:])) return ctx, godog.ErrSkip } @@ -220,23 +222,23 @@ func CreateScenarioContext(ctx context.Context, sc *godog.Scenario) (context.Con featureName := strings.TrimSuffix(filepath.Base(sc.Uri), filepath.Ext(sc.Uri)) featureHash := sha256.Sum256([]byte(featureName)) id := fmt.Sprintf("%x-%s", featureHash[:4], sc.Id) - scCtx := &scenarioContext{ - id: id, - featureName: featureName, - scenarioName: sc.Name, - namespace: fmt.Sprintf("ns-%s", id), - clusterExtensionName: fmt.Sprintf("ce-%s", id), - clusterObjectSetName: fmt.Sprintf("ce-%s-1", id), - catalogs: make(map[string]string), - catalogPackageNames: make(map[string]string), - metricsResponse: make(map[string]string), - leaderPods: make(map[string]string), + scCtx := &World{ + ID: id, + FeatureName: featureName, + ScenarioName: sc.Name, + Namespace: fmt.Sprintf("ns-%s", id), + ClusterExtensionName: fmt.Sprintf("ce-%s", id), + ClusterObjectSetName: fmt.Sprintf("ce-%s-1", id), + Catalogs: make(map[string]string), + CatalogPackageNames: make(map[string]string), + MetricsResponse: make(map[string]string), + LeaderPods: make(map[string]string), } return context.WithValue(ctx, scenarioContextKey, scCtx), nil } -func scenarioCtx(ctx context.Context) *scenarioContext { - return ctx.Value(scenarioContextKey).(*scenarioContext) +func Scenario(ctx context.Context) *World { + return ctx.Value(scenarioContextKey).(*World) } func stderrOutput(err error) string { @@ -248,14 +250,14 @@ func stderrOutput(err error) string { } func ScenarioCleanup(ctx context.Context, _ *godog.Scenario, err error) (context.Context, error) { - sc := scenarioCtx(ctx) + sc := Scenario(ctx) // Stop the in-process recording proxy if one was started. if sc.proxy != nil { sc.proxy.stop() } // Stop catalog port-forward if one was started. - if sc.catalogCleanup != nil { - sc.catalogCleanup() + if sc.CatalogCleanup != nil { + sc.CatalogCleanup() } // Restore any deployments that were modified during the scenario. Runs @@ -265,15 +267,15 @@ func ScenarioCleanup(ctx context.Context, _ *godog.Scenario, err error) (context for i := len(sc.deploymentRestores) - 1; i >= 0; i-- { dr := sc.deploymentRestores[i] if dr.patchedArgs { - if err2 := patchDeploymentArgs(dr.namespace, dr.name, dr.originalArgs); err2 != nil { - logger.Info("Error restoring deployment args", "name", dr.name, "error", err2) - } else if _, err2 := k8sClient(ctx, "rollout", "status", "-n", dr.namespace, - fmt.Sprintf("deployment/%s", dr.name), "--timeout=2m"); err2 != nil { - logger.Info("Timeout waiting for deployment rollout after restore", "name", dr.name) + if err2 := patchDeploymentArgs(dr.Namespace, dr.Name, dr.originalArgs); err2 != nil { + logger.Info("Error restoring deployment args", "name", dr.Name, "error", err2) + } else if _, err2 := K8sClient(ctx, "rollout", "status", "-n", dr.Namespace, + fmt.Sprintf("deployment/%s", dr.Name), "--timeout=2m"); err2 != nil { + logger.Info("Timeout waiting for deployment rollout after restore", "name", dr.Name) } } if err2 := restoreDeployment(dr); err2 != nil { - logger.Info("Error restoring deployment env", "deployment", dr.name, "namespace", dr.namespace, "error", err2) + logger.Info("Error restoring deployment env", "deployment", dr.Name, "namespace", dr.Namespace, "error", err2) } } @@ -281,22 +283,22 @@ func ScenarioCleanup(ctx context.Context, _ *godog.Scenario, err error) (context return ctx, err } - forDeletion := sc.addedResources - for _, catalogName := range sc.catalogs { - forDeletion = append(forDeletion, resource{name: catalogName, kind: "clustercatalog"}) + forDeletion := sc.AddedResources + for _, catalogName := range sc.Catalogs { + forDeletion = append(forDeletion, Resource{Name: catalogName, Kind: "clustercatalog"}) } - forDeletion = append(forDeletion, resource{name: sc.namespace, kind: "namespace"}) + forDeletion = append(forDeletion, Resource{Name: sc.Namespace, Kind: "namespace"}) g := new(errgroup.Group) g.SetLimit(8) for _, r := range forDeletion { g.Go(func() error { - args := []string{"delete", r.kind, r.name, "--ignore-not-found=true"} - if r.namespace != "" { - args = append(args, "-n", r.namespace) + args := []string{"delete", r.Kind, r.Name, "--ignore-not-found=true"} + if r.Namespace != "" { + args = append(args, "-n", r.Namespace) } - if _, err := k8sClient(ctx, args...); err != nil { - logger.Info("Error deleting resource", "name", r.name, "namespace", r.namespace, "stderr", stderrOutput(err)) + if _, err := K8sClient(ctx, args...); err != nil { + logger.Info("Error deleting resource", "name", r.Name, "namespace", r.Namespace, "stderr", stderrOutput(err)) } return nil }) @@ -304,3 +306,30 @@ func ScenarioCleanup(ctx context.Context, _ *godog.Scenario, err error) (context _ = g.Wait() return ctx, nil } + +// Log returns the logger configured by BeforeSuite. +func Log() logr.Logger { return logger } + +// CommandRecorder receives command output from reusable step helpers. +// Demo suites provide the implementation; other suites leave it unset. +type CommandRecorder interface { + RecordCommand(command, stdout, stderr string, duration time.Duration) + RecordCommandWithInput(command, stdin, stdout, stderr string, duration time.Duration) + RecordCustom(command, stdout, stderr string) +} + +type commandRecorderKey struct{} + +// WithCommandRecorder attaches an optional command observer to a scenario. +func WithCommandRecorder(ctx context.Context, recorder CommandRecorder) context.Context { + return context.WithValue(ctx, commandRecorderKey{}, recorder) +} + +// CommandRecorderFromContext returns the scenario's command observer, if any. +func CommandRecorderFromContext(ctx context.Context) CommandRecorder { + recorder, _ := ctx.Value(commandRecorderKey{}).(CommandRecorder) + return recorder +} + +// StderrOutput extracts captured stderr from a failed command. +func StderrOutput(err error) string { return stderrOutput(err) } diff --git a/test/internal/e2e/steps/proxy_steps.go b/test/internal/e2e/steps/proxy_steps.go index 02ac969114..f2fc0c2e17 100644 --- a/test/internal/e2e/steps/proxy_steps.go +++ b/test/internal/e2e/steps/proxy_steps.go @@ -124,9 +124,9 @@ func (p *recordingProxy) recordedHosts() []string { // Helpers // --------------------------------------------------------------------------- -// kindGatewayIP returns the gateway IP of the "kind" network for the +// kindGatewayIP returns the gateway IP of the "Kind" network for the // configured container runtime, which is the address that pods inside the -// kind cluster use to reach the host machine. The runtime is read from the +// Kind cluster use to reach the host machine. The runtime is read from the // CONTAINER_RUNTIME environment variable; it defaults to "docker". func kindGatewayIP() (string, error) { runtime := os.Getenv("CONTAINER_RUNTIME") @@ -134,7 +134,7 @@ func kindGatewayIP() (string, error) { runtime = "docker" } // Range over all IPAM config entries rather than hard-coding index 0. - // Some kind setups place an IPv6 config at index 0 (with no Gateway) and + // Some Kind setups place an IPv6 config at index 0 (with no Gateway) and // the IPv4 config at index 1; indexing 0 directly would return empty. out, err := exec.Command(runtime, "network", "inspect", "kind", //nolint:gosec "--format", "{{range .IPAM.Config}}{{.Gateway}} {{end}}").Output() @@ -154,10 +154,10 @@ func kindGatewayIP() (string, error) { } // kubernetesClusterIP returns the cluster IP of the "kubernetes" service in -// the "default" namespace, which is the address client-go uses to reach the +// the "default" Namespace, which is the address client-go uses to reach the // API server from inside a pod (via the KUBERNETES_SERVICE_HOST env var). func kubernetesClusterIP() (string, error) { - ip, err := k8sClient(context.Background(), "get", "service", "kubernetes", "-n", "default", + ip, err := K8sClient(context.Background(), "get", "service", "kubernetes", "-n", "default", "-o", "jsonpath={.spec.clusterIP}") if err != nil { return "", fmt.Errorf("failed to get kubernetes service cluster IP: %w", err) @@ -168,7 +168,7 @@ func kubernetesClusterIP() (string, error) { // getDeploymentContainerEnv returns the environment variables for the named // container in the given deployment, as a slice of "NAME=VALUE" strings. func getDeploymentContainerEnv(deploymentName, namespace, containerName string) ([]string, error) { - raw, err := k8sClient(context.Background(), "get", "deployment", deploymentName, "-n", namespace, "-o", "json") + raw, err := K8sClient(context.Background(), "get", "deployment", deploymentName, "-n", namespace, "-o", "json") if err != nil { return nil, fmt.Errorf("failed to get deployment %s/%s: %w", namespace, deploymentName, err) } @@ -192,11 +192,11 @@ func getDeploymentContainerEnv(deploymentName, namespace, containerName string) // setDeploymentEnvVars replaces the environment of the named container with // the provided "NAME=VALUE" pairs and waits for the rollout to complete. -// It locates the container by name (rather than assuming index 0) and uses +// It locates the container by Name (rather than assuming index 0) and uses // the JSON Patch "add" operation, which creates the env field if absent. func setDeploymentEnvVars(deploymentName, namespace, containerName string, env []string) error { // Fetch the deployment to find the container index. - raw, err := k8sClient(context.Background(), "get", "deployment", deploymentName, "-n", namespace, "-o", "json") + raw, err := K8sClient(context.Background(), "get", "deployment", deploymentName, "-n", namespace, "-o", "json") if err != nil { return fmt.Errorf("failed to get deployment %s/%s: %w", namespace, deploymentName, err) } @@ -243,12 +243,12 @@ func setDeploymentEnvVars(deploymentName, namespace, containerName string, env [ return fmt.Errorf("failed to marshal patch: %w", err) } - if _, err := k8sClient(context.Background(), "patch", "deployment", deploymentName, "-n", namespace, + if _, err := K8sClient(context.Background(), "patch", "deployment", deploymentName, "-n", namespace, "--type=json", fmt.Sprintf("--patch=%s", string(patchBytes))); err != nil { return fmt.Errorf("failed to patch deployment %s/%s: %w", namespace, deploymentName, err) } - if _, err := k8sClient(context.Background(), "rollout", "status", "deployment", deploymentName, "-n", namespace, + if _, err := K8sClient(context.Background(), "rollout", "status", "deployment", deploymentName, "-n", namespace, "--timeout=5m"); err != nil { return fmt.Errorf("rollout of deployment %s/%s did not complete: %w", namespace, deploymentName, err) } @@ -277,14 +277,14 @@ func restoreDeployment(r deploymentRestore) error { if r.originalEnv == nil { return nil } - return setDeploymentEnvVars(r.name, r.namespace, r.containerName, r.originalEnv) + return setDeploymentEnvVars(r.Name, r.Namespace, r.containerName, r.originalEnv) } // configureDeploymentProxy patches the named deployment to set HTTPS_PROXY to // proxyURL and NO_PROXY to the Kubernetes API server cluster IP, then waits // for the rollout. The original env is saved for cleanup. func configureDeploymentProxy(ctx context.Context, component, proxyURL string) error { - sc := scenarioCtx(ctx) + sc := Scenario(ctx) var deployName string switch component { @@ -300,25 +300,25 @@ func configureDeploymentProxy(ctx context.Context, component, proxyURL string) e // patched in this scenario. Subsequent env patches must not overwrite the // saved original, otherwise cleanup would restore to an intermediate state. // We check for containerName != "" to distinguish env-restore records from - // args-restore records (TLS profile patches), which share the same name and - // namespace fields but have no containerName or originalEnv set. + // args-restore records (TLS profile patches), which share the same Name and + // Namespace fields but have no containerName or originalEnv set. alreadyTracked := false for _, r := range sc.deploymentRestores { - if r.name == deployName && r.namespace == olmNamespace && r.containerName != "" { + if r.Name == deployName && r.Namespace == OLMNamespace && r.containerName != "" { alreadyTracked = true break } } - origEnv, err := getDeploymentContainerEnv(deployName, olmNamespace, "manager") + origEnv, err := getDeploymentContainerEnv(deployName, OLMNamespace, "manager") if err != nil { return err } if !alreadyTracked { sc.deploymentRestores = append(sc.deploymentRestores, deploymentRestore{ - name: deployName, - namespace: olmNamespace, + Name: deployName, + Namespace: OLMNamespace, containerName: "manager", originalEnv: origEnv, }) @@ -327,7 +327,7 @@ func configureDeploymentProxy(ctx context.Context, component, proxyURL string) e // Exclude the Kubernetes API server from proxying so the controller can // still reconcile resources. client-go connects to KUBERNETES_SERVICE_HOST // which is the cluster IP of the "kubernetes" service — a plain IP, not a - // DNS name, so DNS-wildcard NO_PROXY entries won't match it. + // DNS Name, so DNS-wildcard NO_PROXY entries won't match it. k8sIP, err := kubernetesClusterIP() if err != nil { return err @@ -335,7 +335,7 @@ func configureDeploymentProxy(ctx context.Context, component, proxyURL string) e newEnv := addOrReplaceEnvVar(origEnv, "HTTPS_PROXY", proxyURL) newEnv = addOrReplaceEnvVar(newEnv, "NO_PROXY", k8sIP) - return setDeploymentEnvVars(deployName, olmNamespace, "manager", newEnv) + return setDeploymentEnvVars(deployName, OLMNamespace, "manager", newEnv) } // --------------------------------------------------------------------------- @@ -350,10 +350,10 @@ func ConfigureDeploymentWithHTTPSProxy(ctx context.Context, component, proxyURL } // StartRecordingProxyAndConfigureDeployment starts an in-process HTTP CONNECT -// proxy reachable from the cluster via the container-runtime kind network +// proxy reachable from the cluster via the container-runtime Kind network // gateway, then patches the component deployment to route HTTPS through it. func StartRecordingProxyAndConfigureDeployment(ctx context.Context, component string) error { - sc := scenarioCtx(ctx) + sc := Scenario(ctx) p, err := newRecordingProxy() if err != nil { @@ -379,19 +379,19 @@ func StartRecordingProxyAndConfigureDeployment(ctx context.Context, component st // RecordingProxyReceivedCONNECTForCatalogd polls until the recording proxy has // received at least one CONNECT request whose target host contains "catalogd", -// or the polling timeout is reached. +// or the polling Timeout is reached. // // Note: the recording proxy runs on the host and cannot route to in-cluster // service addresses, so it responds with 502 Bad Gateway after recording the // CONNECT. This is intentional — the step only verifies that operator-controller // respected HTTPS_PROXY and sent the request through the proxy. func RecordingProxyReceivedCONNECTForCatalogd(ctx context.Context) error { - sc := scenarioCtx(ctx) + sc := Scenario(ctx) if sc.proxy == nil { return fmt.Errorf("no recording proxy was started in this scenario") } - waitFor(ctx, func() bool { + WaitFor(ctx, func() bool { for _, h := range sc.proxy.recordedHosts() { if strings.Contains(h, "catalogd") { logger.Info("Recording proxy confirmed CONNECT for catalogd", "host", h) diff --git a/test/internal/e2e/steps/steps.go b/test/internal/e2e/steps/steps.go index 39f0a38fba..544289e94b 100644 --- a/test/internal/e2e/steps/steps.go +++ b/test/internal/e2e/steps/steps.go @@ -54,13 +54,13 @@ const ( olmDeploymentName = "operator-controller-controller-manager" catalogdDeploymentName = "catalogd-controller-manager" defaultTimeout = 5 * time.Minute - tick = 1 * time.Second + Tick = 1 * time.Second ) -// timeout is the per-step wait timeout. It defaults to defaultTimeout (5m) but +// Timeout is the per-step wait Timeout. It defaults to defaultTimeout (5m) but // can be overridden via E2E_STEP_TIMEOUT to accommodate runtimes (e.g. // BoxcutterRuntime) that take longer to complete an installation. -var timeout = func() time.Duration { +var Timeout = func() time.Duration { if s := os.Getenv("E2E_STEP_TIMEOUT"); s != "" { if d, err := time.ParseDuration(s); err == nil { return d @@ -70,13 +70,13 @@ var timeout = func() time.Duration { }() var ( - olmNamespace = "olmv1-system" - componentNamespaces = map[string]string{} // keyed by component label (e.g. "catalogd"); falls back to olmNamespace - kubeconfigPath string - k8sCli string + OLMNamespace = "olmv1-system" + ComponentNamespaces = map[string]string{} // keyed by component label (e.g. "catalogd"); falls back to olmNamespace + KubeconfigPath string + K8sCLI string deployImageRegistry = sync.OnceValue(func() error { - // Only deploy the registry on kind clusters - providerID, err := k8sClient(context.Background(), "get", "nodes", "-o", "jsonpath={.items[0].spec.providerID}") + // Only deploy the registry on Kind clusters + providerID, err := K8sClient(context.Background(), "get", "nodes", "-o", "jsonpath={.items[0].spec.providerID}") if err != nil || !strings.HasPrefix(providerID, "kind://") { return nil } @@ -252,31 +252,31 @@ func RegisterSteps(sc *godog.ScenarioContext) { func init() { flagSet := pflag.CommandLine - flagSet.StringVar(&k8sCli, "k8s.cli", "kubectl", "Path to k8s cli") + flagSet.StringVar(&K8sCLI, "k8s.cli", "kubectl", "Path to k8s cli") if v, found := os.LookupEnv("KUBECONFIG"); found { - kubeconfigPath = v + KubeconfigPath = v } else { home, err := os.UserHomeDir() if err != nil { panic(fmt.Sprintf("cannot determine user home directory: %v", err)) } - flagSet.StringVar(&kubeconfigPath, "kubeconfig", filepath.Join(home, ".kube", "config"), "Paths to a kubeconfig. Only required if out-of-cluster.") + flagSet.StringVar(&KubeconfigPath, "kubeconfig", filepath.Join(home, ".kube", "config"), "Paths to a kubeconfig. Only required if out-of-cluster.") } } -// namespaceForComponent returns the namespace for the named OLM component. -// Falls back to olmNamespace when the component has no dedicated entry (e.g. upstream single-namespace deployments). -func namespaceForComponent(component string) string { - if ns, ok := componentNamespaces[component]; ok { +// NamespaceForComponent returns the Namespace for the named OLM component. +// Falls back to OLMNamespace when the component has no dedicated entry (e.g. upstream single-Namespace deployments). +func NamespaceForComponent(component string) string { + if ns, ok := ComponentNamespaces[component]; ok { return ns } - return olmNamespace + return OLMNamespace } -func k8sClient(ctx context.Context, args ...string) (string, error) { - cmd := exec.CommandContext(ctx, k8sCli, args...) +func K8sClient(ctx context.Context, args ...string) (string, error) { + cmd := exec.CommandContext(ctx, K8sCLI, args...) logger.V(1).Info("Running", "command", strings.Join(cmd.Args, " ")) - cmd.Env = append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", kubeconfigPath)) + cmd.Env = append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", KubeconfigPath)) var stdoutBuf, stderrBuf bytes.Buffer cmd.Stdout = &stdoutBuf @@ -300,16 +300,16 @@ func k8sClient(ctx context.Context, args ...string) (string, error) { } logger.V(1).Info("Output", "command", strings.Join(cmd.Args, " "), "output", output) - if rec := RecorderFromContext(ctx); rec != nil { + if rec := CommandRecorderFromContext(ctx); rec != nil { rec.RecordCommand(strings.Join(cmd.Args, " "), output, stderrStr, elapsed) } return output, err } -func k8scliWithInput(ctx context.Context, yaml string, args ...string) (string, string, error) { - cmd := exec.CommandContext(ctx, k8sCli, args...) +func K8sCLIWithInput(ctx context.Context, yaml string, args ...string) (string, string, error) { + cmd := exec.CommandContext(ctx, K8sCLI, args...) cmd.Stdin = bytes.NewBufferString(yaml) - cmd.Env = append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", kubeconfigPath)) + cmd.Env = append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", KubeconfigPath)) var stdoutBuf, stderrBuf bytes.Buffer cmd.Stdout = &stdoutBuf @@ -329,7 +329,7 @@ func k8scliWithInput(ctx context.Context, yaml string, args ...string) (string, } } - if rec := RecorderFromContext(ctx); rec != nil { + if rec := CommandRecorderFromContext(ctx); rec != nil { rec.RecordCommandWithInput(strings.Join(cmd.Args, " "), yaml, output, stderrStr, elapsed) } return output, stderrStr, err @@ -372,34 +372,34 @@ func ImageRegistryIsAvailable() error { return deployImageRegistry() } -// OLMisAvailable waits for the OLM operator-controller deployment to become available. Polls with timeout. +// OLMisAvailable waits for the OLM operator-controller deployment to become available. Polls with Timeout. func OLMisAvailable(ctx context.Context) error { require.Eventually(godog.T(ctx), func() bool { - v, err := k8sClient(ctx, "get", "deployment", "-n", olmNamespace, olmDeploymentName, "-o", "json") + v, err := K8sClient(ctx, "get", "deployment", "-n", OLMNamespace, olmDeploymentName, "-o", "json") if err != nil { return false } var deployment appsv1.Deployment - if err := json.Unmarshal([]byte(v), &deployment); err != nil || !generationObserved(deployment.Generation, deployment.Status.ObservedGeneration) { + if err := json.Unmarshal([]byte(v), &deployment); err != nil || !GenerationObserved(deployment.Generation, deployment.Status.ObservedGeneration) { return false } condition := deploymentutil.GetDeploymentCondition(deployment.Status, appsv1.DeploymentAvailable) return condition != nil && condition.Status == corev1.ConditionTrue - }, timeout, tick) + }, Timeout, Tick) - if rec := RecorderFromContext(ctx); rec != nil { - out, _ := k8sClient(ctx, "get", "deployments", "-n", olmNamespace, "-l", "app.kubernetes.io/part-of=olm") - rec.RecordCustom(fmt.Sprintf("kubectl get deployments -n %s", olmNamespace), out, "") + if rec := CommandRecorderFromContext(ctx); rec != nil { + out, _ := K8sClient(ctx, "get", "deployments", "-n", OLMNamespace, "-l", "app.kubernetes.io/part-of=olm") + rec.RecordCustom(fmt.Sprintf("kubectl get deployments -n %s", OLMNamespace), out, "") } return nil } -// BundleInstalled waits for the ClusterExtension to report the specified bundle name and version as installed. Polls with timeout. +// BundleInstalled waits for the ClusterExtension to report the specified bundle Name and version as installed. Polls with Timeout. func BundleInstalled(ctx context.Context, name, version string) error { - sc := scenarioCtx(ctx) - name = substituteScenarioVars(name, sc) - waitFor(ctx, func() bool { - v, err := k8sClient(ctx, "get", "clusterextension", sc.clusterExtensionName, "-o", "json") + sc := Scenario(ctx) + name = SubstituteScenarioVars(name, sc) + WaitFor(ctx, func() bool { + v, err := K8sClient(ctx, "get", "clusterextension", sc.ClusterExtensionName, "-o", "json") if err != nil { return false } @@ -407,14 +407,14 @@ func BundleInstalled(ctx context.Context, name, version string) error { if err := json.Unmarshal([]byte(v), &ext); err != nil { return false } - installed := currentCondition(ext.Generation, ext.Status.Conditions, ocv1.TypeInstalled) - return conditionMatches(installed, metav1.ConditionTrue, nil, nil) && + installed := CurrentCondition(ext.Generation, ext.Status.Conditions, ocv1.TypeInstalled) + return ConditionMatches(installed, metav1.ConditionTrue, nil, nil) && ext.Status.Install.Bundle.Name == name && ext.Status.Install.Bundle.Version == version }) return nil } -func toUnstructured(yamlContent string) (*unstructured.Unstructured, error) { +func ToUnstructured(yamlContent string) (*unstructured.Unstructured, error) { var u map[string]any if err := yaml.Unmarshal([]byte(yamlContent), &u); err != nil { return nil, err @@ -422,43 +422,43 @@ func toUnstructured(yamlContent string) (*unstructured.Unstructured, error) { return &unstructured.Unstructured{Object: u}, nil } -func injectTestAnnotations(obj *unstructured.Unstructured, sc *scenarioContext) { +func injectTestAnnotations(obj *unstructured.Unstructured, sc *World) { annotations := obj.GetAnnotations() if annotations == nil { annotations = make(map[string]string) } - annotations["e2e.olm.operatorframework.io/feature"] = sc.featureName - annotations["e2e.olm.operatorframework.io/scenario"] = sc.scenarioName + annotations["e2e.olm.operatorframework.io/feature"] = sc.FeatureName + annotations["e2e.olm.operatorframework.io/scenario"] = sc.ScenarioName obj.SetAnnotations(annotations) } -func substituteScenarioVars(content string, sc *scenarioContext) string { +func SubstituteScenarioVars(content string, sc *World) string { vars := map[string]string{ - "TEST_NAMESPACE": sc.namespace, - "NAME": sc.clusterExtensionName, - "COS_NAME": sc.clusterObjectSetName, - "SCENARIO_ID": sc.id, - "OLM_NAMESPACE": olmNamespace, + "TEST_NAMESPACE": sc.Namespace, + "NAME": sc.ClusterExtensionName, + "COS_NAME": sc.ClusterObjectSetName, + "SCENARIO_ID": sc.ID, + "OLM_NAMESPACE": OLMNamespace, } - for orig, param := range sc.catalogPackageNames { + for orig, param := range sc.CatalogPackageNames { vars[fmt.Sprintf("PACKAGE:%s", orig)] = param } - for name, catalogName := range sc.catalogs { + for name, catalogName := range sc.Catalogs { vars[fmt.Sprintf("CATALOG:%s", name)] = catalogName } return templateContent(content, vars) } -// ResourceApplyFails waits for kubectl apply of the provided YAML to fail with the expected error message. Polls with timeout. +// ResourceApplyFails waits for kubectl apply of the provided YAML to fail with the expected error message. Polls with Timeout. func ResourceApplyFails(ctx context.Context, errMsg string, yamlTemplate *godog.DocString) error { - sc := scenarioCtx(ctx) - yamlContent := substituteScenarioVars(yamlTemplate.Content, sc) - _, err := toUnstructured(yamlContent) + sc := Scenario(ctx) + yamlContent := SubstituteScenarioVars(yamlTemplate.Content, sc) + _, err := ToUnstructured(yamlContent) if err != nil { return fmt.Errorf("failed to parse resource yaml: %v", err) } - waitFor(ctx, func() bool { - _, stdErr, err := k8scliWithInput(ctx, yamlContent, "apply", "-f", "-") + WaitFor(ctx, func() bool { + _, stdErr, err := K8sCLIWithInput(ctx, yamlContent, "apply", "-f", "-") if err == nil { return false } @@ -472,12 +472,12 @@ func ResourceApplyFails(ctx context.Context, errMsg string, yamlTemplate *godog. // ResourceCreateFails checks a create request, so an existing object cannot be updated by apply. func ResourceCreateFails(ctx context.Context, errMsg string, yamlTemplate *godog.DocString) error { - sc := scenarioCtx(ctx) - yamlContent := substituteScenarioVars(yamlTemplate.Content, sc) - if _, err := toUnstructured(yamlContent); err != nil { + sc := Scenario(ctx) + yamlContent := SubstituteScenarioVars(yamlTemplate.Content, sc) + if _, err := ToUnstructured(yamlContent); err != nil { return fmt.Errorf("failed to parse resource yaml: %w", err) } - _, stderr, err := k8scliWithInput(ctx, yamlContent, "create", "-f", "-") + _, stderr, err := K8sCLIWithInput(ctx, yamlContent, "create", "-f", "-") if err == nil { return fmt.Errorf("expected create error containing %q, but creation succeeded", errMsg) } @@ -489,9 +489,9 @@ func ResourceCreateFails(ctx context.Context, errMsg string, yamlTemplate *godog // ResourceIsAppliedAsUser applies the provided YAML using kubectl impersonation (--as/--as-group). func ResourceIsAppliedAsUser(ctx context.Context, user, group string, yamlTemplate *godog.DocString) error { - sc := scenarioCtx(ctx) - yamlContent := substituteScenarioVars(yamlTemplate.Content, sc) - res, err := toUnstructured(yamlContent) + sc := Scenario(ctx) + yamlContent := SubstituteScenarioVars(yamlTemplate.Content, sc) + res, err := ToUnstructured(yamlContent) if err != nil { return fmt.Errorf("failed to parse resource yaml: %v", err) } @@ -500,22 +500,22 @@ func ResourceIsAppliedAsUser(ctx context.Context, user, group string, yamlTempla if err != nil { return fmt.Errorf("failed to marshal resource yaml: %w", err) } - out, stdErr, err := k8scliWithInput(ctx, string(annotatedYAML), "apply", "-f", "-", "--as="+user, "--as-group="+group) + out, stdErr, err := K8sCLIWithInput(ctx, string(annotatedYAML), "apply", "-f", "-", "--as="+user, "--as-group="+group) if err != nil { return fmt.Errorf("failed to apply resource as %s/%s: %v; stderr: %s", user, group, out, stdErr) } - sc.lastApplyStderr = stdErr + sc.LastApplyStderr = stdErr if res.GetKind() == "ClusterExtension" { - sc.addedResources = append(sc.addedResources, resource{name: res.GetName(), kind: "clusterextension"}) + sc.AddedResources = append(sc.AddedResources, Resource{Name: res.GetName(), Kind: "clusterextension"}) } else { namespace := res.GetNamespace() if namespace == "" { - namespace = sc.namespace + namespace = sc.Namespace } - sc.addedResources = append(sc.addedResources, resource{ - name: res.GetName(), - kind: strings.ToLower(res.GetKind()), - namespace: namespace, + sc.AddedResources = append(sc.AddedResources, Resource{ + Name: res.GetName(), + Kind: strings.ToLower(res.GetKind()), + Namespace: namespace, }) } return nil @@ -523,21 +523,21 @@ func ResourceIsAppliedAsUser(ctx context.Context, user, group string, yamlTempla // ResourceApplyAsUserFails polls kubectl apply --dry-run=server with impersonation until the admission controller // denies the request with the expected error message. Server-side dry-run sends each request through the full -// admission chain without persisting the resource, which allows polling until eventually-consistent admission +// admission chain without persisting the Resource, which allows polling until eventually-consistent admission // policies (e.g. ValidatingAdmissionPolicy) become active. Once the dry-run confirms denial, a real apply // (without dry-run) is issued to verify the actual request is also denied. func ResourceApplyAsUserFails(ctx context.Context, user, group, errMsg string, yamlTemplate *godog.DocString) error { - sc := scenarioCtx(ctx) - yamlContent := substituteScenarioVars(yamlTemplate.Content, sc) - res, err := toUnstructured(yamlContent) + sc := Scenario(ctx) + yamlContent := SubstituteScenarioVars(yamlTemplate.Content, sc) + res, err := ToUnstructured(yamlContent) if err != nil { return fmt.Errorf("failed to parse resource yaml: %v", err) } // dry-run=server: ValidatingAdmissionPolicy enforcement is eventually consistent — the API server's - // informer cache may not have the binding yet. Dry-run avoids persisting the resource on the first + // informer cache may not have the binding yet. Dry-run avoids persisting the Resource on the first // attempt, so we can safely poll until the admission chain starts denying. - waitFor(ctx, func() bool { - _, stdErr, err := k8scliWithInput(ctx, yamlContent, "apply", "--dry-run=server", "-f", "-", "--as="+user, "--as-group="+group) + WaitFor(ctx, func() bool { + _, stdErr, err := K8sCLIWithInput(ctx, yamlContent, "apply", "--dry-run=server", "-f", "-", "--as="+user, "--as-group="+group) if err == nil { return false } @@ -548,22 +548,22 @@ func ResourceApplyAsUserFails(ctx context.Context, user, group, errMsg string, y // VAP binding enforcement propagates per-replica and is eventually consistent, so a single dry-run // denial does not guarantee the next request is denied. Poll the real apply until it is denied with // the expected message; if a request slips through the propagation window, the object is persisted, - // so delete it and retry. This converges without leaking the resource or reporting a false + // so delete it and retry. This converges without leaking the Resource or reporting a false // "it succeeded". var lastErr error // pendingCleanup is true once an apply has been admitted and the persisted object has not yet been // confirmed removed. We must not report success while a leaked object remains — a following - // "is not found" step would time out, and ScenarioCleanup skips tracked-resource deletion once the + // "is not found" step would time out, and ScenarioCleanup skips tracked-Resource deletion once the // scenario has failed, so the object would also leak into the next scenario. pendingCleanup := false - deadline := time.Now().Add(timeout) + deadline := time.Now().Add(Timeout) for { - _, stdErr, applyErr := k8scliWithInput(ctx, yamlContent, "apply", "-f", "-", "--as="+user, "--as-group="+group) + _, stdErr, applyErr := K8sCLIWithInput(ctx, yamlContent, "apply", "-f", "-", "--as="+user, "--as-group="+group) switch { case applyErr == nil: // Admitted before enforcement propagated to this replica. Track for cleanup and remove it below. if !pendingCleanup { - sc.addedResources = append(sc.addedResources, resource{name: res.GetName(), kind: strings.ToLower(res.GetKind())}) + sc.AddedResources = append(sc.AddedResources, Resource{Name: res.GetName(), Kind: strings.ToLower(res.GetKind())}) } pendingCleanup = true lastErr = fmt.Errorf("apply was admitted before enforcement propagated") @@ -578,74 +578,74 @@ func ResourceApplyAsUserFails(ctx context.Context, user, group, errMsg string, y if pendingCleanup { // A blocking delete (default --wait) returns only once the object is actually removed, so a // nil error confirms removal. Keep pendingCleanup set on failure so a later denial is not accepted. - // Bound the wait by the remaining step budget (at least one tick) so a finalizer-blocked delete — + // Bound the wait by the remaining step budget (at least one Tick) so a finalizer-blocked delete — // e.g. a ClusterExtension waiting on its controller — cannot push the step past deadline. remaining := time.Until(deadline) - if remaining < tick { - remaining = tick + if remaining < Tick { + remaining = Tick } delArgs := []string{"delete", res.GetKind(), res.GetName(), "--ignore-not-found", "--timeout=" + remaining.String()} if ns := res.GetNamespace(); ns != "" { delArgs = append(delArgs, "-n", ns) } - if _, delErr := k8sClient(ctx, delArgs...); delErr != nil { + if _, delErr := K8sClient(ctx, delArgs...); delErr != nil { lastErr = fmt.Errorf("failed to remove admitted resource: %w", delErr) } else { pendingCleanup = false } } if time.Now().After(deadline) { - return fmt.Errorf("expected apply to fail with %q within %s: %w", errMsg, timeout, lastErr) + return fmt.Errorf("expected apply to fail with %q within %s: %w", errMsg, Timeout, lastErr) } - time.Sleep(tick) + time.Sleep(Tick) } } // ResourceApplyEmitsWarning asserts the last kubectl apply produced stderr output containing the expected warning. func ResourceApplyEmitsWarning(ctx context.Context, expected *godog.DocString) error { - sc := scenarioCtx(ctx) + sc := Scenario(ctx) expectedText := strings.TrimSpace(expected.Content) - if !strings.Contains(sc.lastApplyStderr, expectedText) { - return fmt.Errorf("expected apply warning %q, got stderr: %q", expectedText, sc.lastApplyStderr) + if !strings.Contains(sc.LastApplyStderr, expectedText) { + return fmt.Errorf("expected apply warning %q, got stderr: %q", expectedText, sc.LastApplyStderr) } return nil } func ResourceApplyDoesNotEmitWarning(ctx context.Context) error { - sc := scenarioCtx(ctx) - if strings.Contains(sc.lastApplyStderr, "Warning:") { - return fmt.Errorf("expected no warnings, got stderr: %q", sc.lastApplyStderr) + sc := Scenario(ctx) + if strings.Contains(sc.LastApplyStderr, "Warning:") { + return fmt.Errorf("expected no warnings, got stderr: %q", sc.LastApplyStderr) } return nil } -// TrackCurrentClusterExtensionForCleanup saves the current ClusterExtension name in the cleanup list +// TrackCurrentClusterExtensionForCleanup saves the current ClusterExtension Name in the cleanup list // so it gets deleted at the end of the scenario. Call this before applying a second ClusterExtension -// in the same scenario, because ResourceIsApplied overwrites the tracked name. +// in the same scenario, because ResourceIsApplied overwrites the tracked Name. func TrackCurrentClusterExtensionForCleanup(ctx context.Context) error { - sc := scenarioCtx(ctx) - if sc.clusterExtensionName != "" { - sc.extensionObjects = append(sc.extensionObjects, &ocv1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{Name: sc.clusterExtensionName}, + sc := Scenario(ctx) + if sc.ClusterExtensionName != "" { + sc.ExtensionObjects = append(sc.ExtensionObjects, &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{Name: sc.ClusterExtensionName}, }) } return nil } // ClusterExtensionOwnsClusterObjectSets waits for the named ClusterExtension to own exactly the -// expected number of ClusterObjectSets. Polls with timeout. +// expected number of ClusterObjectSets. Polls with Timeout. func ClusterExtensionOwnsClusterObjectSets(ctx context.Context, extName string, expectedCount int) error { - sc := scenarioCtx(ctx) - extName = substituteScenarioVars(extName, sc) - waitFor(ctx, func() bool { - revisions, err := listClusterObjectSetsInGroup(ctx, extName) + sc := Scenario(ctx) + extName = SubstituteScenarioVars(extName, sc) + WaitFor(ctx, func() bool { + revisions, err := ListClusterObjectSetsInGroup(ctx, extName) if err != nil { return false } if len(revisions) != expectedCount { return false } - ext, err := getResource("clusterextension", extName, "") + ext, err := GetResource("clusterextension", extName, "") if err != nil { return false } @@ -663,14 +663,14 @@ func ClusterExtensionOwnsClusterObjectSets(ctx context.Context, extName string, // ClusterObjectDeploymentOwnsClusterObjectSets waits until the named ClusterObjectDeployment is the // controller owner of exactly expectedCount ClusterObjectSet revisions in its group. func ClusterObjectDeploymentOwnsClusterObjectSets(ctx context.Context, codName string, expectedCount int) error { - sc := scenarioCtx(ctx) - codName = substituteScenarioVars(codName, sc) - waitFor(ctx, func() bool { - revisions, err := listClusterObjectSetsInGroup(ctx, codName) + sc := Scenario(ctx) + codName = SubstituteScenarioVars(codName, sc) + WaitFor(ctx, func() bool { + revisions, err := ListClusterObjectSetsInGroup(ctx, codName) if err != nil || len(revisions) != expectedCount { return false } - cod, err := getResource("clusterobjectdeployment", codName, "") + cod, err := GetResource("clusterobjectdeployment", codName, "") if err != nil { return false } @@ -688,15 +688,15 @@ func ClusterObjectDeploymentOwnsClusterObjectSets(ctx context.Context, codName s // ClusterObjectDeploymentIsOwnedByClusterExtension waits until the named ClusterObjectDeployment has // the named ClusterExtension as its controller owner. func ClusterObjectDeploymentIsOwnedByClusterExtension(ctx context.Context, codName, extName string) error { - sc := scenarioCtx(ctx) - codName = substituteScenarioVars(codName, sc) - extName = substituteScenarioVars(extName, sc) - waitFor(ctx, func() bool { - cod, err := getResource("clusterobjectdeployment", codName, "") + sc := Scenario(ctx) + codName = SubstituteScenarioVars(codName, sc) + extName = SubstituteScenarioVars(extName, sc) + WaitFor(ctx, func() bool { + cod, err := GetResource("clusterobjectdeployment", codName, "") if err != nil { return false } - ext, err := getResource("clusterextension", extName, "") + ext, err := GetResource("clusterextension", extName, "") if err != nil { return false } @@ -706,24 +706,24 @@ func ClusterObjectDeploymentIsOwnedByClusterExtension(ctx context.Context, codNa return nil } -// NamedClusterExtensionReportsCondition waits for a specific ClusterExtension (by name) to have a condition -// matching type and status. Polls with timeout. +// NamedClusterExtensionReportsCondition waits for a specific ClusterExtension (by Name) to have a condition +// matching type and status. Polls with Timeout. func NamedClusterExtensionReportsCondition(ctx context.Context, extName, conditionType, conditionStatus string) error { - sc := scenarioCtx(ctx) - extName = substituteScenarioVars(extName, sc) - return waitForCondition(ctx, "clusterextension", extName, conditionType, metav1.ConditionStatus(conditionStatus), nil, nil) + sc := Scenario(ctx) + extName = SubstituteScenarioVars(extName, sc) + return WaitForCondition(ctx, "clusterextension", extName, conditionType, metav1.ConditionStatus(conditionStatus), nil, nil) } -// NamedClusterExtensionReportsConditionWithMessageFragment waits for a specific ClusterExtension (by name) +// NamedClusterExtensionReportsConditionWithMessageFragment waits for a specific ClusterExtension (by Name) // to have a condition matching type, status, reason, with a message containing the specified fragment. func NamedClusterExtensionReportsConditionWithMessageFragment(ctx context.Context, extName, conditionType, conditionStatus, conditionReason string, msgFragment *godog.DocString) error { - extName = substituteScenarioVars(extName, scenarioCtx(ctx)) - return waitForCondition(ctx, "clusterextension", extName, conditionType, metav1.ConditionStatus(conditionStatus), &conditionReason, messageFragmentComparison(ctx, msgFragment)) + extName = SubstituteScenarioVars(extName, Scenario(ctx)) + return WaitForCondition(ctx, "clusterextension", extName, conditionType, metav1.ConditionStatus(conditionStatus), &conditionReason, MessageFragmentComparison(ctx, msgFragment)) } // ClusterExtensionVersionUpdate patches the ClusterExtension's catalog version to the specified value. func ClusterExtensionVersionUpdate(ctx context.Context, version string) error { - sc := scenarioCtx(ctx) + sc := Scenario(ctx) patch := map[string]any{ "spec": map[string]any{ "source": map[string]any{ @@ -737,14 +737,14 @@ func ClusterExtensionVersionUpdate(ctx context.Context, version string) error { if err != nil { return err } - _, err = k8sClient(ctx, "patch", "clusterextension", sc.clusterExtensionName, "--type", "merge", "-p", string(pb)) + _, err = K8sClient(ctx, "patch", "clusterextension", sc.ClusterExtensionName, "--type", "merge", "-p", string(pb)) return err } // ClusterObjectSetLifecycleUpdate patches the ClusterObjectSet's lifecycleState to the specified value. func ClusterObjectSetLifecycleUpdate(ctx context.Context, cosName, lifecycle string) error { - sc := scenarioCtx(ctx) - cosName = substituteScenarioVars(cosName, sc) + sc := Scenario(ctx) + cosName = SubstituteScenarioVars(cosName, sc) patch := map[string]any{ "spec": map[string]any{ "lifecycleState": lifecycle, @@ -754,16 +754,16 @@ func ClusterObjectSetLifecycleUpdate(ctx context.Context, cosName, lifecycle str if err != nil { return err } - _, err = k8sClient(ctx, "patch", "clusterobjectset", cosName, "--type", "merge", "-p", string(pb)) + _, err = K8sClient(ctx, "patch", "clusterobjectset", cosName, "--type", "merge", "-p", string(pb)) return err } -// ResourceIsApplied applies the provided YAML resource to the cluster and in case of ClusterExtension or ClusterObjectSet it captures -// its name in the test context so that it can be referred to in later steps with ${NAME} or ${COS_NAME}, respectively +// ResourceIsApplied applies the provided YAML Resource to the cluster and in case of ClusterExtension or ClusterObjectSet it captures +// its Name in the test context so that it can be referred to in later steps with ${NAME} or ${COS_NAME}, respectively func ResourceIsApplied(ctx context.Context, yamlTemplate *godog.DocString) error { - sc := scenarioCtx(ctx) - yamlContent := substituteScenarioVars(yamlTemplate.Content, sc) - res, err := toUnstructured(yamlContent) + sc := Scenario(ctx) + yamlContent := SubstituteScenarioVars(yamlTemplate.Content, sc) + res, err := ToUnstructured(yamlContent) if err != nil { return fmt.Errorf("failed to parse resource yaml: %v", err) } @@ -772,45 +772,45 @@ func ResourceIsApplied(ctx context.Context, yamlTemplate *godog.DocString) error if err != nil { return fmt.Errorf("failed to marshal resource yaml: %w", err) } - out, stdErr, err := k8scliWithInput(ctx, string(annotatedYAML), "apply", "-f", "-") + out, stdErr, err := K8sCLIWithInput(ctx, string(annotatedYAML), "apply", "-f", "-") if err != nil { return fmt.Errorf("failed to apply resource %v; err: %w; stderr: %s", out, err, stdErr) } - sc.lastApplyStderr = stdErr + sc.LastApplyStderr = stdErr if res.GetKind() == "ClusterExtension" { - sc.addedResources = append(sc.addedResources, resource{name: res.GetName(), kind: "clusterextension"}) + sc.AddedResources = append(sc.AddedResources, Resource{Name: res.GetName(), Kind: "clusterextension"}) } else if res.GetKind() == "ClusterObjectSet" { - sc.clusterObjectSetName = res.GetName() // used for ${COS_NAME} variable substitution in YAML templates - sc.addedResources = append(sc.addedResources, resource{name: res.GetName(), kind: "clusterobjectset"}) + sc.ClusterObjectSetName = res.GetName() // used for ${COS_NAME} variable substitution in YAML templates + sc.AddedResources = append(sc.AddedResources, Resource{Name: res.GetName(), Kind: "clusterobjectset"}) } else { namespace := res.GetNamespace() if namespace == "" { - namespace = sc.namespace + namespace = sc.Namespace } - sc.addedResources = append(sc.addedResources, resource{ - name: res.GetName(), - kind: strings.ToLower(res.GetKind()), - namespace: namespace, + sc.AddedResources = append(sc.AddedResources, Resource{ + Name: res.GetName(), + Kind: strings.ToLower(res.GetKind()), + Namespace: namespace, }) } return nil } -// ClusterExtensionIsAvailable waits for the ClusterExtension's Installed condition to be True. Polls with timeout. +// ClusterExtensionIsAvailable waits for the ClusterExtension's Installed condition to be True. Polls with Timeout. func ClusterExtensionIsAvailable(ctx context.Context) error { - sc := scenarioCtx(ctx) + sc := Scenario(ctx) if err := waitForExtensionCondition(ctx, ocv1.TypeInstalled, metav1.ConditionTrue, nil, nil); err != nil { return err } - if rec := RecorderFromContext(ctx); rec != nil { - out, _ := k8sClient(ctx, "get", "clusterextension", sc.clusterExtensionName, "-o", "jsonpath={.status}") + if rec := CommandRecorderFromContext(ctx); rec != nil { + out, _ := K8sClient(ctx, "get", "clusterextension", sc.ClusterExtensionName, "-o", "jsonpath={.status}") var pretty bytes.Buffer if err := json.Indent(&pretty, []byte(out), "", " "); err == nil { out = pretty.String() } rec.RecordCustom( - fmt.Sprintf("kubectl get clusterextension %s -o jsonpath='{.status}' | jq .", sc.clusterExtensionName), + fmt.Sprintf("kubectl get clusterextension %s -o jsonpath='{.status}' | jq .", sc.ClusterExtensionName), out+"\n", "", ) } @@ -821,28 +821,28 @@ func ClusterExtensionIsAvailable(ctx context.Context) error { // to observe the current generation, regardless of its status or reason. This // confirms the spec was processed, not that the rollout succeeded. func ClusterExtensionReconciledLatestGeneration(ctx context.Context) error { - sc := scenarioCtx(ctx) - waitFor(ctx, func() bool { - resource, err := readResourceConditions(ctx, "clusterextension", sc.clusterExtensionName) + sc := Scenario(ctx) + WaitFor(ctx, func() bool { + resource, err := ReadResourceConditions(ctx, "clusterextension", sc.ClusterExtensionName) if err != nil { return false } - return currentCondition(resource.Metadata.Generation, resource.Status.Conditions, ocv1.TypeProgressing) != nil + return CurrentCondition(resource.Metadata.Generation, resource.Status.Conditions, ocv1.TypeProgressing) != nil }) return nil } // ClusterExtensionIsRolledOut waits for the ClusterExtension's Progressing condition to be True with reason Succeeded, -// for the current generation, then gathers its constituent resources into the scenario context. Polls with timeout. +// for the current generation, then gathers its constituent resources into the scenario context. Polls with Timeout. func ClusterExtensionIsRolledOut(ctx context.Context) error { - sc := scenarioCtx(ctx) + sc := Scenario(ctx) if err := waitForExtensionCondition(ctx, ocv1.TypeProgressing, metav1.ConditionTrue, ptr.To(ocv1.ReasonSucceeded), nil); err != nil { return err } - if rec := RecorderFromContext(ctx); rec != nil { - out, _ := k8sClient(ctx, "get", "clusterextension", sc.clusterExtensionName) - rec.RecordCustom(fmt.Sprintf("kubectl get clusterextension %s", sc.clusterExtensionName), out, "") + if rec := CommandRecorderFromContext(ctx); rec != nil { + out, _ := K8sClient(ctx, "get", "clusterextension", sc.ClusterExtensionName) + rec.RecordCustom(fmt.Sprintf("kubectl get clusterextension %s", sc.ClusterExtensionName), out, "") } // Save ClusterExtension resources to test context for posterior checks @@ -852,18 +852,18 @@ func ClusterExtensionIsRolledOut(ctx context.Context) error { return nil } -// ClusterExtensionResourcesCreatedAndAreLabeled verifies each constituent resource has the expected OLM owner-kind -// and owner-name labels. Polls with timeout per resource. +// ClusterExtensionResourcesCreatedAndAreLabeled verifies each constituent Resource has the expected OLM owner-Kind +// and owner-Name labels. Polls with Timeout per Resource. func ClusterExtensionResourcesCreatedAndAreLabeled(ctx context.Context) error { - sc := scenarioCtx(ctx) + sc := Scenario(ctx) if len(sc.GetClusterExtensionObjects()) == 0 { return fmt.Errorf("extension objects not found in context") } for _, obj := range sc.GetClusterExtensionObjects() { - waitFor(ctx, func() bool { + WaitFor(ctx, func() bool { kind := obj.GetObjectKind().GroupVersionKind().Kind - clusterObj, err := getResource(kind, obj.GetName(), obj.GetNamespace()) + clusterObj, err := GetResource(kind, obj.GetName(), obj.GetNamespace()) if err != nil { logger.V(1).Error(err, "error getting resource", "name", obj.GetName(), "namespace", obj.GetNamespace(), "kind", kind) return false @@ -877,7 +877,7 @@ func ClusterExtensionResourcesCreatedAndAreLabeled(ctx context.Context) error { for key, expectedValue := range map[string]string{ "olm.operatorframework.io/owner-kind": "ClusterExtension", - "olm.operatorframework.io/owner-name": sc.clusterExtensionName, + "olm.operatorframework.io/owner-name": sc.ClusterExtensionName, } { if labels[key] != expectedValue { logger.V(1).Info("invalid resource label value", "name", obj.GetName(), "namespace", obj.GetNamespace(), "kind", kind, "label", key, "expected", expectedValue, "actual", labels["olm.operatorframework.io/owner-kind"]) @@ -892,13 +892,13 @@ func ClusterExtensionResourcesCreatedAndAreLabeled(ctx context.Context) error { // ClusterExtensionIsRemoved deletes the current ClusterExtension, saving its state for potential restore checks. func ClusterExtensionIsRemoved(ctx context.Context) error { - sc := scenarioCtx(ctx) - return ResourceRemoved(ctx, fmt.Sprintf("clusterextension/%s", sc.clusterExtensionName)) + sc := Scenario(ctx) + return ResourceRemoved(ctx, fmt.Sprintf("clusterextension/%s", sc.ClusterExtensionName)) } -// ClusterExtensionResourcesRemoved waits for each previously gathered constituent resource to be deleted. Polls with timeout per resource. +// ClusterExtensionResourcesRemoved waits for each previously gathered constituent Resource to be deleted. Polls with Timeout per Resource. func ClusterExtensionResourcesRemoved(ctx context.Context) error { - sc := scenarioCtx(ctx) + sc := Scenario(ctx) if len(sc.GetClusterExtensionObjects()) == 0 { return fmt.Errorf("extension objects not found in context") } @@ -910,18 +910,18 @@ func ClusterExtensionResourcesRemoved(ctx context.Context) error { return nil } -func waitFor(ctx context.Context, conditionFn func() bool) { - require.Eventually(godog.T(ctx), conditionFn, timeout, tick) +func WaitFor(ctx context.Context, conditionFn func() bool) { + require.Eventually(godog.T(ctx), conditionFn, Timeout, Tick) } -type msgMatchFn func(string) bool +type MsgMatchFn func(string) bool -func alwaysMatch(_ string) bool { return true } +func AlwaysMatch(_ string) bool { return true } -func messageComparison(ctx context.Context, msg *godog.DocString) msgMatchFn { - msgCmp := alwaysMatch +func MessageComparison(ctx context.Context, msg *godog.DocString) MsgMatchFn { + msgCmp := AlwaysMatch if msg != nil { - expectedMsg := substituteScenarioVars(strings.Join(strings.Fields(msg.Content), " "), scenarioCtx(ctx)) + expectedMsg := SubstituteScenarioVars(strings.Join(strings.Fields(msg.Content), " "), Scenario(ctx)) msgCmp = func(actual string) bool { return actual == expectedMsg } @@ -929,55 +929,55 @@ func messageComparison(ctx context.Context, msg *godog.DocString) msgMatchFn { return msgCmp } -func messageFragmentComparison(ctx context.Context, msgFragment *godog.DocString) msgMatchFn { +func MessageFragmentComparison(ctx context.Context, msgFragment *godog.DocString) MsgMatchFn { if msgFragment == nil { - return alwaysMatch + return AlwaysMatch } - expectedFragment := substituteScenarioVars(strings.Join(strings.Fields(msgFragment.Content), " "), scenarioCtx(ctx)) + expectedFragment := SubstituteScenarioVars(strings.Join(strings.Fields(msgFragment.Content), " "), Scenario(ctx)) return func(actualMsg string) bool { return strings.Contains(strings.Join(strings.Fields(actualMsg), " "), expectedFragment) } } -// waitForCondition requires the asserted condition to reflect metadata.generation, +// WaitForCondition requires the asserted condition to reflect metadata.generation, // in addition to matching the requested status, reason, and message. -func waitForCondition(ctx context.Context, resourceType, resourceName, conditionType string, conditionStatus metav1.ConditionStatus, conditionReason *string, msgCmp msgMatchFn) error { +func WaitForCondition(ctx context.Context, resourceType, resourceName, conditionType string, conditionStatus metav1.ConditionStatus, conditionReason *string, msgCmp MsgMatchFn) error { require.Eventually(godog.T(ctx), func() bool { - resource, err := readResourceConditions(ctx, resourceType, resourceName) + resource, err := ReadResourceConditions(ctx, resourceType, resourceName) if err != nil { return false } - condition := currentCondition(resource.Metadata.Generation, resource.Status.Conditions, conditionType) - return conditionMatches(condition, conditionStatus, conditionReason, msgCmp) - }, timeout, tick) + condition := CurrentCondition(resource.Metadata.Generation, resource.Status.Conditions, conditionType) + return ConditionMatches(condition, conditionStatus, conditionReason, msgCmp) + }, Timeout, Tick) return nil } -func waitForExtensionCondition(ctx context.Context, conditionType string, conditionStatus metav1.ConditionStatus, conditionReason *string, msgCmp msgMatchFn) error { - sc := scenarioCtx(ctx) - return waitForCondition(ctx, "clusterextension", sc.clusterExtensionName, conditionType, conditionStatus, conditionReason, msgCmp) +func waitForExtensionCondition(ctx context.Context, conditionType string, conditionStatus metav1.ConditionStatus, conditionReason *string, msgCmp MsgMatchFn) error { + sc := Scenario(ctx) + return WaitForCondition(ctx, "clusterextension", sc.ClusterExtensionName, conditionType, conditionStatus, conditionReason, msgCmp) } // ClusterExtensionReportsCondition waits for the ClusterExtension to have a condition matching the specified type, -// status, reason, and exact message. Polls with timeout. +// status, reason, and exact message. Polls with Timeout. func ClusterExtensionReportsCondition(ctx context.Context, conditionType, conditionStatus, conditionReason string, msg *godog.DocString) error { - return waitForExtensionCondition(ctx, conditionType, metav1.ConditionStatus(conditionStatus), &conditionReason, messageComparison(ctx, msg)) + return waitForExtensionCondition(ctx, conditionType, metav1.ConditionStatus(conditionStatus), &conditionReason, MessageComparison(ctx, msg)) } // ClusterExtensionReportsConditionWithMessageFragment waits for the ClusterExtension to have a condition matching -// type, status, and reason, with a message containing the specified fragment. Polls with timeout. +// type, status, and reason, with a message containing the specified fragment. Polls with Timeout. func ClusterExtensionReportsConditionWithMessageFragment(ctx context.Context, conditionType, conditionStatus, conditionReason string, msgFragment *godog.DocString) error { - return waitForExtensionCondition(ctx, conditionType, metav1.ConditionStatus(conditionStatus), &conditionReason, messageFragmentComparison(ctx, msgFragment)) + return waitForExtensionCondition(ctx, conditionType, metav1.ConditionStatus(conditionStatus), &conditionReason, MessageFragmentComparison(ctx, msgFragment)) } // ClusterExtensionReportsConditionWithoutMsg waits for the ClusterExtension to have a condition matching type, -// status, and reason, without checking the message. Polls with timeout. +// status, and reason, without checking the message. Polls with Timeout. func ClusterExtensionReportsConditionWithoutMsg(ctx context.Context, conditionType, conditionStatus, conditionReason string) error { return ClusterExtensionReportsCondition(ctx, conditionType, conditionStatus, conditionReason, nil) } // ClusterExtensionReportsConditionWithoutReason waits for the ClusterExtension to have a condition matching type -// and status, without checking reason or message. Polls with timeout. +// and status, without checking reason or message. Polls with Timeout. func ClusterExtensionReportsConditionWithoutReason(ctx context.Context, conditionType, conditionStatus string) error { return waitForExtensionCondition(ctx, conditionType, metav1.ConditionStatus(conditionStatus), nil, nil) } @@ -985,22 +985,22 @@ func ClusterExtensionReportsConditionWithoutReason(ctx context.Context, conditio // ClusterExtensionReportsConditionTransitionTime asserts that a condition's lastTransitionTime falls within // the specified minute range since the ClusterObjectSet's creation, when its progress deadline starts. func ClusterExtensionReportsConditionTransitionTime(ctx context.Context, conditionType string, minMinutes, maxMinutes int, revisionName string) error { - sc := scenarioCtx(ctx) + sc := Scenario(ctx) t := godog.T(ctx) - revisionName = substituteScenarioVars(revisionName, sc) - creationTimestampValue, err := k8sClient(ctx, "get", "clusterobjectset", revisionName, "-o", "jsonpath={.metadata.creationTimestamp}") + revisionName = SubstituteScenarioVars(revisionName, sc) + creationTimestampValue, err := K8sClient(ctx, "get", "clusterobjectset", revisionName, "-o", "jsonpath={.metadata.creationTimestamp}") require.NoError(t, err) creationTimestamp, err := time.Parse(time.RFC3339, creationTimestampValue) require.NoError(t, err, "failed to parse ClusterObjectSet creationTimestamp") var condition *metav1.Condition - waitFor(ctx, func() bool { - resource, err := readResourceConditions(ctx, "clusterextension", sc.clusterExtensionName) + WaitFor(ctx, func() bool { + resource, err := ReadResourceConditions(ctx, "clusterextension", sc.ClusterExtensionName) if err != nil { return false } - condition = currentCondition(resource.Metadata.Generation, resource.Status.Conditions, conditionType) + condition = CurrentCondition(resource.Metadata.Generation, resource.Status.Conditions, conditionType) return condition != nil && !condition.LastTransitionTime.IsZero() }) lastTransitionTime := condition.LastTransitionTime.Time @@ -1018,33 +1018,33 @@ func ClusterExtensionReportsConditionTransitionTime(ctx context.Context, conditi } // ClusterObjectSetReportsConditionWithoutMsg waits for the named ClusterObjectSet to have a condition -// matching type, status, and reason. Polls with timeout. +// matching type, status, and reason. Polls with Timeout. func ClusterObjectSetReportsConditionWithoutMsg(ctx context.Context, revisionName, conditionType, conditionStatus, conditionReason string) error { - return waitForCondition(ctx, "clusterobjectset", substituteScenarioVars(revisionName, scenarioCtx(ctx)), conditionType, metav1.ConditionStatus(conditionStatus), &conditionReason, nil) + return WaitForCondition(ctx, "clusterobjectset", SubstituteScenarioVars(revisionName, Scenario(ctx)), conditionType, metav1.ConditionStatus(conditionStatus), &conditionReason, nil) } // ClusterObjectSetReportsConditionWithMsg waits for the named ClusterObjectSet to have a condition -// matching type, status, reason, and message. Polls with timeout. +// matching type, status, reason, and message. Polls with Timeout. func ClusterObjectSetReportsConditionWithMsg(ctx context.Context, revisionName, conditionType, conditionStatus, conditionReason string, msg *godog.DocString) error { - return waitForCondition(ctx, "clusterobjectset", substituteScenarioVars(revisionName, scenarioCtx(ctx)), conditionType, metav1.ConditionStatus(conditionStatus), &conditionReason, messageComparison(ctx, msg)) + return WaitForCondition(ctx, "clusterobjectset", SubstituteScenarioVars(revisionName, Scenario(ctx)), conditionType, metav1.ConditionStatus(conditionStatus), &conditionReason, MessageComparison(ctx, msg)) } // ClusterObjectSetReportsConditionWithMessageFragment waits for the named ClusterObjectSet to have a condition -// matching type, status, reason, with a message containing the specified fragment. Polls with timeout. +// matching type, status, reason, with a message containing the specified fragment. Polls with Timeout. func ClusterObjectSetReportsConditionWithMessageFragment(ctx context.Context, revisionName, conditionType, conditionStatus, conditionReason string, msgFragment *godog.DocString) error { - return waitForCondition(ctx, "clusterobjectset", substituteScenarioVars(revisionName, scenarioCtx(ctx)), conditionType, metav1.ConditionStatus(conditionStatus), &conditionReason, messageFragmentComparison(ctx, msgFragment)) + return WaitForCondition(ctx, "clusterobjectset", SubstituteScenarioVars(revisionName, Scenario(ctx)), conditionType, metav1.ConditionStatus(conditionStatus), &conditionReason, MessageFragmentComparison(ctx, msgFragment)) } // ClusterObjectDeploymentReportsConditionWithoutMsg waits for the named ClusterObjectDeployment to have a -// condition matching type, status, and reason. Polls with timeout. +// condition matching type, status, and reason. Polls with Timeout. func ClusterObjectDeploymentReportsConditionWithoutMsg(ctx context.Context, name, conditionType, conditionStatus, conditionReason string) error { - return waitForCondition(ctx, "clusterobjectdeployment", substituteScenarioVars(name, scenarioCtx(ctx)), conditionType, metav1.ConditionStatus(conditionStatus), &conditionReason, nil) + return WaitForCondition(ctx, "clusterobjectdeployment", SubstituteScenarioVars(name, Scenario(ctx)), conditionType, metav1.ConditionStatus(conditionStatus), &conditionReason, nil) } // ClusterObjectDeploymentReportsConditionWithMessageFragment waits for the named ClusterObjectDeployment to have a -// condition matching type, status, reason, with a message containing the specified fragment. Polls with timeout. +// condition matching type, status, reason, with a message containing the specified fragment. Polls with Timeout. func ClusterObjectDeploymentReportsConditionWithMessageFragment(ctx context.Context, name, conditionType, conditionStatus, conditionReason string, msgFragment *godog.DocString) error { - return waitForCondition(ctx, "clusterobjectdeployment", substituteScenarioVars(name, scenarioCtx(ctx)), conditionType, metav1.ConditionStatus(conditionStatus), &conditionReason, messageFragmentComparison(ctx, msgFragment)) + return WaitForCondition(ctx, "clusterobjectdeployment", SubstituteScenarioVars(name, Scenario(ctx)), conditionType, metav1.ConditionStatus(conditionStatus), &conditionReason, MessageFragmentComparison(ctx, msgFragment)) } // ClusterObjectDeploymentConsistentlyReportsCondition fails if, over the given @@ -1057,7 +1057,7 @@ func ClusterObjectDeploymentConsistentlyReportsCondition(ctx context.Context, na if err != nil { return fmt.Errorf("invalid duration %q: %w", durationStr, err) } - return consistentlyCondition(ctx, "clusterobjectdeployment", substituteScenarioVars(name, scenarioCtx(ctx)), conditionType, metav1.ConditionStatus(conditionStatus), conditionReason, d) + return consistentlyCondition(ctx, "clusterobjectdeployment", SubstituteScenarioVars(name, Scenario(ctx)), conditionType, metav1.ConditionStatus(conditionStatus), conditionReason, d) } // consistently invokes check every tick until the expected state has held for the full @@ -1083,7 +1083,7 @@ func consistently(ctx context.Context, duration time.Duration, check func(contex // the window postpones it instead of failing. A transient check error is tolerated only // within maxTransientGap of the last successful observation; a hard failure inside check // fails the test immediately via require. - err := wait.PollUntilContextTimeout(ctx, tick, duration+maxTransientGap, true, func(ctx context.Context) (bool, error) { + err := wait.PollUntilContextTimeout(ctx, Tick, duration+maxTransientGap, true, func(ctx context.Context) (bool, error) { if cerr := check(ctx); cerr != nil { require.LessOrEqualf(t, time.Since(lastObserved), maxTransientGap, "consistency check could not observe the expected state for over %s: %v", maxTransientGap, cerr) @@ -1110,18 +1110,18 @@ func consistently(ctx context.Context, duration time.Duration, check func(contex } // conditionCheck returns a consistently check that verifies a single condition's status -// and reason for the current generation on the named resource. A failed API read is returned as a transient error +// and reason for the current generation on the named Resource. A failed API read is returned as a transient error // (the condition's real state is unknown while the read is failing). A successful read // that does not contain the condition, or shows a different status or reason, fails // immediately: a condition that disappears or changes is not being reported consistently. func conditionCheck(resourceType, resourceName, conditionType string, conditionStatus metav1.ConditionStatus, conditionReason string) func(context.Context) error { return func(ctx context.Context) error { t := godog.T(ctx) - resource, err := readResourceConditions(ctx, resourceType, resourceName) + resource, err := ReadResourceConditions(ctx, resourceType, resourceName) if err != nil { return err } - condition := currentCondition(resource.Metadata.Generation, resource.Status.Conditions, conditionType) + condition := CurrentCondition(resource.Metadata.Generation, resource.Status.Conditions, conditionType) require.NotNilf(t, condition, "%s %s condition disappeared or has not observed generation %d during the consistency window", resourceType, conditionType, resource.Metadata.Generation) require.Equalf(t, conditionStatus, condition.Status, @@ -1133,7 +1133,7 @@ func conditionCheck(resourceType, resourceName, conditionType string, conditionS } // consistentlyCondition fails the test if, at any point during duration, the named -// resource's condition deviates from the expected type/status/reason or disappears. +// Resource's condition deviates from the expected type/status/reason or disappears. // Transient API read failures are tolerated only briefly; see consistently and // conditionCheck for the exact semantics. func consistentlyCondition(ctx context.Context, resourceType, resourceName, conditionType string, conditionStatus metav1.ConditionStatus, conditionReason string, duration time.Duration) error { @@ -1143,27 +1143,27 @@ func consistentlyCondition(ctx context.Context, resourceType, resourceName, cond // TriggerClusterObjectSetReconciliation annotates the named ClusterObjectSet // to trigger a new reconciliation cycle. func TriggerClusterObjectSetReconciliation(ctx context.Context, cosName string) error { - sc := scenarioCtx(ctx) - cosName = substituteScenarioVars(cosName, sc) - _, err := k8sClient(ctx, "annotate", "clusterobjectset", cosName, "--overwrite", + sc := Scenario(ctx) + cosName = SubstituteScenarioVars(cosName, sc) + _, err := K8sClient(ctx, "annotate", "clusterobjectset", cosName, "--overwrite", fmt.Sprintf("e2e-trigger=%d", time.Now().UnixNano())) return err } // ClusterObjectSetHasObservedPhase waits for the named ClusterObjectSet to have -// an observedPhases entry matching the given phase name with a non-empty digest. Polls with timeout. +// an observedPhases entry matching the given phase Name with a non-empty digest. Polls with Timeout. func ClusterObjectSetHasObservedPhase(ctx context.Context, cosName, phaseName string) error { - sc := scenarioCtx(ctx) - cosName = substituteScenarioVars(cosName, sc) - phaseName = substituteScenarioVars(phaseName, sc) + sc := Scenario(ctx) + cosName = SubstituteScenarioVars(cosName, sc) + phaseName = SubstituteScenarioVars(phaseName, sc) - waitFor(ctx, func() bool { - out, err := k8sClient(ctx, "get", "clusterobjectset", cosName, "-o", "json") + WaitFor(ctx, func() bool { + out, err := K8sClient(ctx, "get", "clusterobjectset", cosName, "-o", "json") if err != nil { return false } var cos ocv1.ClusterObjectSet - if err := json.Unmarshal([]byte(out), &cos); err != nil || currentCondition(cos.Generation, cos.Status.Conditions, "Ready") == nil { + if err := json.Unmarshal([]byte(out), &cos); err != nil || CurrentCondition(cos.Generation, cos.Status.Conditions, "Ready") == nil { return false } for _, phase := range cos.Status.ObservedPhases { @@ -1177,18 +1177,18 @@ func ClusterObjectSetHasObservedPhase(ctx context.Context, cosName, phaseName st } // ClusterObjectSetIsArchived waits for the named ClusterObjectSet to have Ready=False -// with reason Archived. Polls with timeout. +// with reason Archived. Polls with Timeout. func ClusterObjectSetIsArchived(ctx context.Context, revisionName string) error { - return waitForCondition(ctx, "clusterobjectset", substituteScenarioVars(revisionName, scenarioCtx(ctx)), "Ready", metav1.ConditionFalse, ptr.To("Archived"), nil) + return WaitForCondition(ctx, "clusterobjectset", SubstituteScenarioVars(revisionName, Scenario(ctx)), "Ready", metav1.ConditionFalse, ptr.To("Archived"), nil) } // ClusterObjectSetIsEventuallyDeleted waits for the named ClusterObjectSet to be // removed from the cluster, e.g. garbage collected once it is archived beyond the -// owning ClusterObjectDeployment's revisionHistoryLimit. Polls with timeout. +// owning ClusterObjectDeployment's revisionHistoryLimit. Polls with Timeout. func ClusterObjectSetIsEventuallyDeleted(ctx context.Context, revisionName string) error { - revisionName = substituteScenarioVars(strings.TrimSpace(revisionName), scenarioCtx(ctx)) - waitFor(ctx, func() bool { - out, err := k8sClient(ctx, "get", "clusterobjectset", revisionName, "--ignore-not-found", "-o", "json") + revisionName = SubstituteScenarioVars(strings.TrimSpace(revisionName), Scenario(ctx)) + WaitFor(ctx, func() bool { + out, err := K8sClient(ctx, "get", "clusterobjectset", revisionName, "--ignore-not-found", "-o", "json") if err != nil { return false } @@ -1198,18 +1198,18 @@ func ClusterObjectSetIsEventuallyDeleted(ctx context.Context, revisionName strin } // ClusterObjectSetPhaseHasObjectCounts waits for a specific phase of the named ClusterObjectSet to -// report exact object counts for total, present, synced, and available. Polls with timeout. +// report exact object counts for total, present, synced, and available. Polls with Timeout. func ClusterObjectSetPhaseHasObjectCounts(ctx context.Context, cosName, phaseName string, total, present, synced, ready int64) error { - sc := scenarioCtx(ctx) - cosName = substituteScenarioVars(cosName, sc) - phaseName = substituteScenarioVars(phaseName, sc) - waitFor(ctx, func() bool { - out, err := k8sClient(ctx, "get", "clusterobjectset", cosName, "-o", "json") + sc := Scenario(ctx) + cosName = SubstituteScenarioVars(cosName, sc) + phaseName = SubstituteScenarioVars(phaseName, sc) + WaitFor(ctx, func() bool { + out, err := K8sClient(ctx, "get", "clusterobjectset", cosName, "-o", "json") if err != nil { return false } var cos ocv1.ClusterObjectSet - if err := json.Unmarshal([]byte(out), &cos); err != nil || currentCondition(cos.Generation, cos.Status.Conditions, "Ready") == nil { + if err := json.Unmarshal([]byte(out), &cos); err != nil || CurrentCondition(cos.Generation, cos.Status.Conditions, "Ready") == nil { return false } for _, phase := range cos.Status.ObservedPhases { @@ -1228,17 +1228,17 @@ func ClusterObjectSetPhaseHasObjectCounts(ctx context.Context, cosName, phaseNam } // ClusterObjectSetHasAggregateObjectCounts waits for the named ClusterObjectSet to report exact -// aggregate object counts (summed across all phases). Polls with timeout. +// aggregate object counts (summed across all phases). Polls with Timeout. func ClusterObjectSetHasAggregateObjectCounts(ctx context.Context, cosName string, total, present, synced, ready int64) error { - sc := scenarioCtx(ctx) - cosName = substituteScenarioVars(cosName, sc) - waitFor(ctx, func() bool { - out, err := k8sClient(ctx, "get", "clusterobjectset", cosName, "-o", "json") + sc := Scenario(ctx) + cosName = SubstituteScenarioVars(cosName, sc) + WaitFor(ctx, func() bool { + out, err := K8sClient(ctx, "get", "clusterobjectset", cosName, "-o", "json") if err != nil { return false } var cos ocv1.ClusterObjectSet - if err := json.Unmarshal([]byte(out), &cos); err != nil || currentCondition(cos.Generation, cos.Status.Conditions, "Ready") == nil { + if err := json.Unmarshal([]byte(out), &cos); err != nil || CurrentCondition(cos.Generation, cos.Status.Conditions, "Ready") == nil { return false } // Counts are required *int64 pointers; a nil (uncomputed) count @@ -1252,16 +1252,16 @@ func ClusterObjectSetHasAggregateObjectCounts(ctx context.Context, cosName strin } // ClusterObjectSetHasAnnotationWithValue waits for the named ClusterObjectSet to have the specified -// annotation with the expected value. Polls with timeout. +// annotation with the expected value. Polls with Timeout. func ClusterObjectSetHasAnnotationWithValue(ctx context.Context, revisionName, annotationKey string, annotationValue *godog.DocString) error { - sc := scenarioCtx(ctx) - revisionName = substituteScenarioVars(strings.TrimSpace(revisionName), sc) + sc := Scenario(ctx) + revisionName = SubstituteScenarioVars(strings.TrimSpace(revisionName), sc) expectedValue := "" if annotationValue != nil { expectedValue = annotationValue.Content } - waitFor(ctx, func() bool { - obj, err := getResource("clusterobjectset", revisionName, "") + WaitFor(ctx, func() bool { + obj, err := GetResource("clusterobjectset", revisionName, "") if err != nil { logger.V(1).Error(err, "failed to get clusterobjectset", "name", revisionName) return false @@ -1275,13 +1275,13 @@ func ClusterObjectSetHasAnnotationWithValue(ctx context.Context, revisionName, a } // ClusterObjectSetHasLabelWithValue waits for the named ClusterObjectSet to have the specified label -// with the expected value. Polls with timeout. +// with the expected value. Polls with Timeout. func ClusterObjectSetHasLabelWithValue(ctx context.Context, revisionName, labelKey, labelValue string) error { - sc := scenarioCtx(ctx) - revisionName = substituteScenarioVars(strings.TrimSpace(revisionName), sc) - labelValue = substituteScenarioVars(labelValue, sc) - waitFor(ctx, func() bool { - obj, err := getResource("clusterobjectset", revisionName, "") + sc := Scenario(ctx) + revisionName = SubstituteScenarioVars(strings.TrimSpace(revisionName), sc) + labelValue = SubstituteScenarioVars(labelValue, sc) + WaitFor(ctx, func() bool { + obj, err := GetResource("clusterobjectset", revisionName, "") if err != nil { logger.V(1).Error(err, "failed to get clusterobjectset", "name", revisionName) return false @@ -1296,15 +1296,15 @@ func ClusterObjectSetHasLabelWithValue(ctx context.Context, revisionName, labelK // ClusterObjectSetObjectsNotFoundOrNotOwned waits for all objects described in the named // ClusterObjectSet's phases to either not exist on the cluster or not contain the revision -// in their ownerReferences. Polls with timeout. +// in their ownerReferences. Polls with Timeout. func ClusterObjectSetObjectsNotFoundOrNotOwned(ctx context.Context, revisionName string) error { - sc := scenarioCtx(ctx) - revisionName = substituteScenarioVars(strings.TrimSpace(revisionName), sc) + sc := Scenario(ctx) + revisionName = SubstituteScenarioVars(strings.TrimSpace(revisionName), sc) // Get the ClusterObjectSet to extract its phase objects var rev ocv1.ClusterObjectSet - waitFor(ctx, func() bool { - out, err := k8sClient(ctx, "get", "clusterobjectset", revisionName, "-o", "json") + WaitFor(ctx, func() bool { + out, err := K8sClient(ctx, "get", "clusterobjectset", revisionName, "-o", "json") if err != nil { return false } @@ -1318,7 +1318,7 @@ func ClusterObjectSetObjectsNotFoundOrNotOwned(ctx context.Context, revisionName var obj *unstructured.Unstructured switch phaseObj.Type { case ocv1.ObjectTypeObjectReference: - resolved, err := resolveObjectRef(phaseObj.ObjectRef) + resolved, err := ResolveObjectRef(phaseObj.ObjectRef) if err != nil { return fmt.Errorf("resolving ref in phase %q object %d: %w", phase.Name, j, err) } @@ -1339,20 +1339,20 @@ func ClusterObjectSetObjectsNotFoundOrNotOwned(ctx context.Context, revisionName return fmt.Errorf("clusterobjectset %q has a phase object with empty name (kind %q, namespace %q)", revisionName, kind, namespace) } - waitFor(ctx, func() bool { + WaitFor(ctx, func() bool { args := []string{"get", kind, name, "--ignore-not-found", "-o", "json"} if namespace != "" { args = append(args, "-n", namespace) } - out, err := k8sClient(ctx, args...) + out, err := K8sClient(ctx, args...) if err != nil { return false } - // If output is empty, the resource does not exist — condition satisfied + // If output is empty, the Resource does not exist — condition satisfied if strings.TrimSpace(out) == "" { return true } - clusterObj, err := toUnstructured(out) + clusterObj, err := ToUnstructured(out) if err != nil { return false } @@ -1374,13 +1374,13 @@ func ClusterObjectSetObjectsNotFoundOrNotOwned(ctx context.Context, revisionName } // ClusterObjectSetPhaseObjectsManagedInSecrets verifies that every object in every phase of the named -// ClusterObjectSet uses a ref (not an inline object). Polls with timeout. +// ClusterObjectSet uses a ref (not an inline object). Polls with Timeout. func ClusterObjectSetPhaseObjectsManagedInSecrets(ctx context.Context, revisionName string) error { - sc := scenarioCtx(ctx) - revisionName = substituteScenarioVars(strings.TrimSpace(revisionName), sc) + sc := Scenario(ctx) + revisionName = SubstituteScenarioVars(strings.TrimSpace(revisionName), sc) - waitFor(ctx, func() bool { - obj, err := getResource("clusterobjectset", revisionName, "") + WaitFor(ctx, func() bool { + obj, err := GetResource("clusterobjectset", revisionName, "") if err != nil { return false } @@ -1420,20 +1420,20 @@ func ClusterObjectSetPhaseObjectsManagedInSecrets(ctx context.Context, revisionN } // ClusterObjectSetReferredSecretsExist verifies that all Secrets referenced by the named -// ClusterObjectSet's phase objects exist in the given namespace. Polls with timeout. +// ClusterObjectSet's phase objects exist in the given Namespace. Polls with Timeout. func ClusterObjectSetReferredSecretsExist(ctx context.Context, revisionName, namespace string) error { - sc := scenarioCtx(ctx) - revisionName = substituteScenarioVars(strings.TrimSpace(revisionName), sc) - namespace = substituteScenarioVars(strings.TrimSpace(namespace), sc) + sc := Scenario(ctx) + revisionName = SubstituteScenarioVars(strings.TrimSpace(revisionName), sc) + namespace = SubstituteScenarioVars(strings.TrimSpace(namespace), sc) - secretNames, err := collectReferredSecretNames(ctx, revisionName) + secretNames, err := CollectReferredSecretNames(ctx, revisionName) if err != nil { return err } for _, name := range secretNames { - waitFor(ctx, func() bool { - _, err := getResource("secret", name, namespace) + WaitFor(ctx, func() bool { + _, err := GetResource("secret", name, namespace) return err == nil }) } @@ -1441,10 +1441,10 @@ func ClusterObjectSetReferredSecretsExist(ctx context.Context, revisionName, nam } // ClusterObjectSetReferredSecretsAreImmutable verifies that all referred Secrets for the named -// ClusterObjectSet are immutable. Polls with timeout. +// ClusterObjectSet are immutable. Polls with Timeout. func ClusterObjectSetReferredSecretsAreImmutable(ctx context.Context, revisionName string) error { - sc := scenarioCtx(ctx) - revisionName = substituteScenarioVars(strings.TrimSpace(revisionName), sc) + sc := Scenario(ctx) + revisionName = SubstituteScenarioVars(strings.TrimSpace(revisionName), sc) secrets, err := listReferredSecrets(ctx, revisionName) if err != nil { @@ -1462,17 +1462,17 @@ func ClusterObjectSetReferredSecretsAreImmutable(ctx context.Context, revisionNa } // ClusterObjectSetReferredSecretsContainLabels verifies that all referred Secrets for the named -// ClusterObjectSet have the expected labels specified in the data table. Polls with timeout. +// ClusterObjectSet have the expected labels specified in the data table. Polls with Timeout. func ClusterObjectSetReferredSecretsContainLabels(ctx context.Context, revisionName string, table *godog.Table) error { - sc := scenarioCtx(ctx) - revisionName = substituteScenarioVars(strings.TrimSpace(revisionName), sc) + sc := Scenario(ctx) + revisionName = SubstituteScenarioVars(strings.TrimSpace(revisionName), sc) expected, err := parseKeyValueTable(table, sc) if err != nil { return fmt.Errorf("invalid labels table: %w", err) } - waitFor(ctx, func() bool { + WaitFor(ctx, func() bool { secrets, err := listReferredSecrets(ctx, revisionName) if err != nil || len(secrets) == 0 { return false @@ -1490,10 +1490,10 @@ func ClusterObjectSetReferredSecretsContainLabels(ctx context.Context, revisionN // ClusterObjectSetReferredSecretsOwnedByObjectSet verifies that all referred Secrets for the named // ClusterObjectSet have an ownerReference pointing to the ClusterObjectSet with controller=true. func ClusterObjectSetReferredSecretsOwnedByObjectSet(ctx context.Context, revisionName string) error { - sc := scenarioCtx(ctx) - revisionName = substituteScenarioVars(strings.TrimSpace(revisionName), sc) + sc := Scenario(ctx) + revisionName = SubstituteScenarioVars(strings.TrimSpace(revisionName), sc) - cosObj, err := getResource("clusterobjectset", revisionName, "") + cosObj, err := GetResource("clusterobjectset", revisionName, "") if err != nil { return fmt.Errorf("getting ClusterObjectSet %q: %w", revisionName, err) } @@ -1528,8 +1528,8 @@ func ClusterObjectSetReferredSecretsOwnedByObjectSet(ctx context.Context, revisi // ClusterObjectSetReferredSecretsHaveType verifies that all referred Secrets for the named // ClusterObjectSet have the specified Secret type. func ClusterObjectSetReferredSecretsHaveType(ctx context.Context, revisionName, expectedType string) error { - sc := scenarioCtx(ctx) - revisionName = substituteScenarioVars(strings.TrimSpace(revisionName), sc) + sc := Scenario(ctx) + revisionName = SubstituteScenarioVars(strings.TrimSpace(revisionName), sc) secrets, err := listReferredSecrets(ctx, revisionName) if err != nil { @@ -1551,11 +1551,11 @@ func ClusterObjectSetReferredSecretsHaveType(ctx context.Context, revisionName, // named revision is controller-owned by the named ClusterExtension. On the current operator-controller // path the ClusterExtension owns the content Secrets directly (so deleting it garbage-collects them). func ClusterObjectSetReferredSecretsOwnedByClusterExtension(ctx context.Context, revisionName, extName string) error { - sc := scenarioCtx(ctx) - revisionName = substituteScenarioVars(strings.TrimSpace(revisionName), sc) - extName = substituteScenarioVars(strings.TrimSpace(extName), sc) + sc := Scenario(ctx) + revisionName = SubstituteScenarioVars(strings.TrimSpace(revisionName), sc) + extName = SubstituteScenarioVars(strings.TrimSpace(extName), sc) - ext, err := getResource("clusterextension", extName, "") + ext, err := GetResource("clusterextension", extName, "") if err != nil { return fmt.Errorf("getting ClusterExtension %q: %w", extName, err) } @@ -1586,15 +1586,15 @@ func ClusterObjectSetReferredSecretsOwnedByClusterExtension(ctx context.Context, return nil } -// collectReferredSecretNames returns the unique set of Secret names referenced by the ClusterObjectSet's phase objects. -func collectReferredSecretNames(ctx context.Context, revisionName string) ([]string, error) { +// CollectReferredSecretNames returns the unique set of Secret names referenced by the ClusterObjectSet's phase objects. +func CollectReferredSecretNames(ctx context.Context, revisionName string) ([]string, error) { var names []string seen := sets.New[string]() var obj *unstructured.Unstructured - waitFor(ctx, func() bool { + WaitFor(ctx, func() bool { var err error - obj, err = getResource("clusterobjectset", revisionName, "") + obj, err = GetResource("clusterobjectset", revisionName, "") return err == nil }) @@ -1624,20 +1624,20 @@ func collectReferredSecretNames(ctx context.Context, revisionName string) ([]str } type referredSecretRef struct { - name string - namespace string + Name string + Namespace string } -// collectReferredSecretRefs returns the unique Secrets (name + namespace) referenced by the -// ClusterObjectSet's phase objects. An empty ref namespace defaults to the OLM system namespace. +// collectReferredSecretRefs returns the unique Secrets (Name + Namespace) referenced by the +// ClusterObjectSet's phase objects. An empty ref Namespace defaults to the OLM system Namespace. func collectReferredSecretRefs(ctx context.Context, revisionName string) ([]referredSecretRef, error) { var refs []referredSecretRef seen := sets.New[string]() var obj *unstructured.Unstructured - waitFor(ctx, func() bool { + WaitFor(ctx, func() bool { var err error - obj, err = getResource("clusterobjectset", revisionName, "") + obj, err = GetResource("clusterobjectset", revisionName, "") return err == nil }) @@ -1659,14 +1659,14 @@ func collectReferredSecretRefs(ctx context.Context, revisionName string) ([]refe } ns, _, _ := unstructured.NestedString(phaseObj, "objectRef", "namespace") if ns == "" { - ns = olmNamespace + ns = OLMNamespace } key := ns + "/" + name if seen.Has(key) { continue } seen.Insert(key) - refs = append(refs, referredSecretRef{name: name, namespace: ns}) + refs = append(refs, referredSecretRef{Name: name, Namespace: ns}) } } if len(refs) == 0 { @@ -1685,89 +1685,89 @@ func listReferredSecrets(ctx context.Context, revisionName string) ([]corev1.Sec } secrets := make([]corev1.Secret, 0, len(refs)) for _, r := range refs { - out, err := k8sClient(ctx, "get", "secret", r.name, "-n", r.namespace, "-o", "json") + out, err := K8sClient(ctx, "get", "secret", r.Name, "-n", r.Namespace, "-o", "json") if err != nil { - return nil, fmt.Errorf("getting referred secret %s/%s: %w", r.namespace, r.name, err) + return nil, fmt.Errorf("getting referred secret %s/%s: %w", r.Namespace, r.Name, err) } var s corev1.Secret if err := json.Unmarshal([]byte(out), &s); err != nil { - return nil, fmt.Errorf("unmarshalling secret %s/%s: %w", r.namespace, r.name, err) + return nil, fmt.Errorf("unmarshalling secret %s/%s: %w", r.Namespace, r.Name, err) } secrets = append(secrets, s) } return secrets, nil } -// ResourceAvailable waits for the specified resource (kind/name format) to exist in the test namespace. Polls with timeout. +// ResourceAvailable waits for the specified Resource (Kind/Name format) to exist in the test Namespace. Polls with Timeout. func ResourceAvailable(ctx context.Context, resource string) error { - sc := scenarioCtx(ctx) - resource = substituteScenarioVars(resource, sc) + sc := Scenario(ctx) + resource = SubstituteScenarioVars(resource, sc) kind, name, found := strings.Cut(resource, "/") if !found { return fmt.Errorf("resource %s is not in the format /", resource) } - waitFor(ctx, func() bool { - _, err := k8sClient(ctx, "get", kind, name, "-n", sc.namespace) + WaitFor(ctx, func() bool { + _, err := K8sClient(ctx, "get", kind, name, "-n", sc.Namespace) return err == nil }) return nil } -// ResourceRemoved saves the resource state for potential restore checks, then deletes it from the test namespace. +// ResourceRemoved saves the Resource state for potential restore checks, then deletes it from the test Namespace. func ResourceRemoved(ctx context.Context, resource string) error { - sc := scenarioCtx(ctx) - resource = substituteScenarioVars(resource, sc) + sc := Scenario(ctx) + resource = SubstituteScenarioVars(resource, sc) kind, name, found := strings.Cut(resource, "/") if !found { return fmt.Errorf("resource %s is not in the format /", resource) } - yaml, err := k8sClient(ctx, "get", kind, name, "-n", sc.namespace, "-o", "yaml") + yaml, err := K8sClient(ctx, "get", kind, name, "-n", sc.Namespace, "-o", "yaml") if err != nil { return err } - obj, err := toUnstructured(yaml) + obj, err := ToUnstructured(yaml) if err != nil { return err } - sc.removedResources = append(sc.removedResources, *obj) - _, err = k8sClient(ctx, "delete", kind, name, "-n", sc.namespace) + sc.RemovedResources = append(sc.RemovedResources, *obj) + _, err = K8sClient(ctx, "delete", kind, name, "-n", sc.Namespace) return err } -// ResourceEventuallyNotFound waits for the specified resource to be fully deleted from the test namespace. Polls with timeout. +// ResourceEventuallyNotFound waits for the specified Resource to be fully deleted from the test Namespace. Polls with Timeout. func ResourceEventuallyNotFound(ctx context.Context, resource string) error { - sc := scenarioCtx(ctx) - resource = substituteScenarioVars(resource, sc) + sc := Scenario(ctx) + resource = SubstituteScenarioVars(resource, sc) kind, name, found := strings.Cut(resource, "/") if !found { return fmt.Errorf("resource %s is not in the format /", resource) } - waitFor(ctx, func() bool { - obj, err := k8sClient(ctx, "get", kind, name, "-n", sc.namespace, "--ignore-not-found", "-o", "yaml") + WaitFor(ctx, func() bool { + obj, err := K8sClient(ctx, "get", kind, name, "-n", sc.Namespace, "--ignore-not-found", "-o", "yaml") return err == nil && strings.TrimSpace(obj) == "" }) return nil } -// ResourceMatches waits for the specified resource to match the expected content using JSON merge patch comparison. Polls with timeout. +// ResourceMatches waits for the specified Resource to match the expected content using JSON merge patch comparison. Polls with Timeout. func ResourceMatches(ctx context.Context, resource string, requiredContentTemplate *godog.DocString) error { - sc := scenarioCtx(ctx) - resource = substituteScenarioVars(resource, sc) + sc := Scenario(ctx) + resource = SubstituteScenarioVars(resource, sc) kind, name, found := strings.Cut(resource, "/") if !found { return fmt.Errorf("resource %s is not in the format /", resource) } - requiredContent, err := toUnstructured(substituteScenarioVars(requiredContentTemplate.Content, sc)) + requiredContent, err := ToUnstructured(SubstituteScenarioVars(requiredContentTemplate.Content, sc)) if err != nil { return fmt.Errorf("failed to parse required resource yaml: %v", err) } - waitFor(ctx, func() bool { - objJson, err := k8sClient(ctx, "get", kind, name, "-n", sc.namespace, "-o", "json") + WaitFor(ctx, func() bool { + objJson, err := K8sClient(ctx, "get", kind, name, "-n", sc.Namespace, "-o", "json") if err != nil { return false } - obj, err := toUnstructured(objJson) + obj, err := ToUnstructured(objJson) if err != nil { return false } @@ -1779,7 +1779,7 @@ func ResourceMatches(ctx context.Context, resource string, requiredContentTempla if err != nil { return false } - upd, err := toUnstructured(string(updJson)) + upd, err := ToUnstructured(string(updJson)) if err != nil { return false } @@ -1789,26 +1789,26 @@ func ResourceMatches(ctx context.Context, resource string, requiredContentTempla return nil } -// ResourceRestored waits for a previously removed resource to be re-created with matching spec or data. Polls with timeout. +// ResourceRestored waits for a previously removed Resource to be re-created with matching spec or data. Polls with Timeout. func ResourceRestored(ctx context.Context, resource string) error { - sc := scenarioCtx(ctx) - resource = substituteScenarioVars(resource, sc) + sc := Scenario(ctx) + resource = SubstituteScenarioVars(resource, sc) kind, name, found := strings.Cut(resource, "/") if !found { return fmt.Errorf("resource %s is not in the format /", resource) } - waitFor(ctx, func() bool { - yaml, err := k8sClient(ctx, "get", kind, name, "-n", sc.namespace, "-o", "yaml") + WaitFor(ctx, func() bool { + yaml, err := K8sClient(ctx, "get", kind, name, "-n", sc.Namespace, "-o", "yaml") if err != nil { return false } - obj, err := toUnstructured(yaml) + obj, err := ToUnstructured(yaml) if err != nil { return false } ct := obj.GetCreationTimestamp() - for i, removed := range sc.removedResources { + for i, removed := range sc.RemovedResources { rct := removed.GetCreationTimestamp() if removed.GetName() == obj.GetName() && removed.GetKind() == obj.GetKind() && rct.Before(&ct) { switch kind { @@ -1821,7 +1821,7 @@ func ResourceRestored(ctx context.Context, resource string) error { return false } } - sc.removedResources = append(sc.removedResources[:i], sc.removedResources[i+1:]...) + sc.RemovedResources = append(sc.RemovedResources[:i], sc.RemovedResources[i+1:]...) return true } } @@ -1831,9 +1831,9 @@ func ResourceRestored(ctx context.Context, resource string) error { } func applyServiceAccount(ctx context.Context, serviceAccount string, keyValue ...string) error { - sc := scenarioCtx(ctx) + sc := Scenario(ctx) vars := extendMap(map[string]string{ - "TEST_NAMESPACE": sc.namespace, + "TEST_NAMESPACE": sc.Namespace, "SERVICE_ACCOUNT_NAME": serviceAccount, "SERVICEACCOUNT_NAME": serviceAccount, }, keyValue...) @@ -1845,7 +1845,7 @@ func applyServiceAccount(ctx context.Context, serviceAccount string, keyValue .. } // Apply the ServiceAccount configuration - _, stdErr, err := k8scliWithInput(ctx, yaml, "apply", "-f", "-") + _, stdErr, err := K8sCLIWithInput(ctx, yaml, "apply", "-f", "-") if err != nil { return fmt.Errorf("failed to apply ServiceAccount configuration: %v: %s", err, stdErr) } @@ -1854,16 +1854,16 @@ func applyServiceAccount(ctx context.Context, serviceAccount string, keyValue .. } func applyPermissionsToServiceAccount(ctx context.Context, serviceAccount, rbacTemplate string, keyValue ...string) error { - sc := scenarioCtx(ctx) + sc := Scenario(ctx) if err := applyServiceAccount(ctx, serviceAccount); err != nil { return err } vars := extendMap(map[string]string{ - "TEST_NAMESPACE": sc.namespace, + "TEST_NAMESPACE": sc.Namespace, "SERVICE_ACCOUNT_NAME": serviceAccount, "SERVICEACCOUNT_NAME": serviceAccount, - "CLUSTER_EXTENSION_NAME": sc.clusterExtensionName, - "CLUSTEREXTENSION_NAME": sc.clusterExtensionName, + "CLUSTER_EXTENSION_NAME": sc.ClusterExtensionName, + "CLUSTEREXTENSION_NAME": sc.ClusterExtensionName, }, keyValue...) _, thisFile, _, _ := runtime.Caller(0) @@ -1873,7 +1873,7 @@ func applyPermissionsToServiceAccount(ctx context.Context, serviceAccount, rbacT } // Apply the RBAC configuration - _, rbacStdErr, err := k8scliWithInput(ctx, rbacYaml, "apply", "-f", "-") + _, rbacStdErr, err := K8sCLIWithInput(ctx, rbacYaml, "apply", "-f", "-") if err != nil { return fmt.Errorf("failed to apply RBAC configuration: %v: %s", err, rbacStdErr) } @@ -1884,23 +1884,23 @@ func applyPermissionsToServiceAccount(ctx context.Context, serviceAccount, rbacT if doc == "" { continue } - res, err := toUnstructured(doc) + res, err := ToUnstructured(doc) if err != nil { continue } kind := res.GetKind() if kind == "ClusterRole" || kind == "ClusterRoleBinding" { - sc.addedResources = append(sc.addedResources, resource{name: res.GetName(), kind: strings.ToLower(kind)}) + sc.AddedResources = append(sc.AddedResources, Resource{Name: res.GetName(), Kind: strings.ToLower(kind)}) } } return nil } -// NamespaceIsAvailable ensures the given namespace exists by applying a namespace template. +// NamespaceIsAvailable ensures the given Namespace exists by applying a Namespace template. func NamespaceIsAvailable(ctx context.Context, ns string) error { - sc := scenarioCtx(ctx) - ns = substituteScenarioVars(ns, sc) + sc := Scenario(ctx) + ns = SubstituteScenarioVars(ns, sc) _, thisFile, _, _ := runtime.Caller(0) yaml, err := templateYaml(filepath.Join(filepath.Dir(thisFile), "testdata", "namespace-template.yaml"), map[string]string{ "NAMESPACE": ns, @@ -1908,13 +1908,13 @@ func NamespaceIsAvailable(ctx context.Context, ns string) error { if err != nil { return fmt.Errorf("failed to template namespace yaml: %v", err) } - if _, _, err := k8scliWithInput(ctx, yaml, "apply", "-f", "-"); err != nil { + if _, _, err := K8sCLIWithInput(ctx, yaml, "apply", "-f", "-"); err != nil { return fmt.Errorf("failed to apply namespace %s: %w", ns, err) } return nil } -// ServiceAccountIsAvailableInNamespace creates a ServiceAccount in the test namespace without RBAC permissions. +// ServiceAccountIsAvailableInNamespace creates a ServiceAccount in the test Namespace without RBAC permissions. func ServiceAccountIsAvailableInNamespace(ctx context.Context, serviceAccount string) error { return applyServiceAccount(ctx, serviceAccount) } @@ -1923,7 +1923,7 @@ func ServiceAccountIsAvailableInNamespace(ctx context.Context, serviceAccount st // The RBAC template is selected based on the service account and BoxcutterRuntime feature gate: --rbac-template.yaml func ServiceAccountWithNeededPermissionsIsAvailableInTestNamespace(ctx context.Context, serviceAccount string) error { kernel := "helm" - if enabled, found := featureGates[features.BoxcutterRuntime]; found && enabled { + if enabled, found := FeatureGates[features.BoxcutterRuntime]; found && enabled { kernel = "boxcutter" } rbacTemplate := fmt.Sprintf("%s-%s-rbac-template.yaml", serviceAccount, kernel) @@ -1932,9 +1932,9 @@ func ServiceAccountWithNeededPermissionsIsAvailableInTestNamespace(ctx context.C // ServiceAccountWithNeededPermissionsIsAvailableInGivenNamespace creates a ServiceAccount and enables creation of any cluster extension on behalf of this account. func ServiceAccountWithNeededPermissionsIsAvailableInGivenNamespace(ctx context.Context, serviceAccount string, ns string) error { - sc := scenarioCtx(ctx) - ns = substituteScenarioVars(ns, sc) - sc.addedResources = append(sc.addedResources, resource{name: ns, kind: "namespace"}) + sc := Scenario(ctx) + ns = SubstituteScenarioVars(ns, sc) + sc.AddedResources = append(sc.AddedResources, Resource{Name: ns, Kind: "namespace"}) if err := applyServiceAccount(ctx, serviceAccount, "TEST_NAMESPACE", ns); err != nil { return err } @@ -1971,9 +1971,9 @@ func httpGet(url string, token string) (*http.Response, error) { } // SendMetricsRequest sets up port-forwarding to the controller's service pods and waits for the metrics endpoint -// to return a successful response. Stores the response body per pod in the scenario context. Polls with timeout. +// to return a successful response. Stores the response body per pod in the scenario context. Polls with Timeout. func SendMetricsRequest(ctx context.Context, serviceAccount string, endpoint string, controllerName string) error { - sc := scenarioCtx(ctx) + sc := Scenario(ctx) svc, err := getMetricsService(controllerName) if err != nil { return err @@ -1987,7 +1987,7 @@ func SendMetricsRequest(ctx context.Context, serviceAccount string, endpoint str for k, v := range svc.Spec.Selector { podNameCmd = append(podNameCmd, fmt.Sprintf("--selector=%s=%s", k, v)) } - v, err := k8sClient(ctx, podNameCmd...) + v, err := K8sClient(ctx, podNameCmd...) if err != nil { return err } @@ -1996,12 +1996,12 @@ func SendMetricsRequest(ctx context.Context, serviceAccount string, endpoint str if err := json.Unmarshal([]byte(v), &pods); err != nil { return err } - token, err := k8sClient(ctx, "create", "token", serviceAccount, "-n", sc.namespace) + token, err := K8sClient(ctx, "create", "token", serviceAccount, "-n", sc.Namespace) if err != nil { return err } - sc.metricsResponse = make(map[string]string) + sc.MetricsResponse = make(map[string]string) for _, p := range pods { if err := func() error { addr, cleanup, err := portForward(ctx, p.Namespace, fmt.Sprintf("pod/%s", p.Name), mPort) @@ -2009,7 +2009,7 @@ func SendMetricsRequest(ctx context.Context, serviceAccount string, endpoint str return err } defer cleanup() - waitFor(ctx, func() bool { + WaitFor(ctx, func() bool { resp, err := httpGet(fmt.Sprintf("https://%s%s", addr, endpoint), token) if err != nil { return false @@ -2021,7 +2021,7 @@ func SendMetricsRequest(ctx context.Context, serviceAccount string, endpoint str if err != nil { return false } - sc.metricsResponse[p.Name] = string(b) + sc.MetricsResponse[p.Name] = string(b) return true } b, err := io.ReadAll(resp.Body) @@ -2042,12 +2042,12 @@ func SendMetricsRequest(ctx context.Context, serviceAccount string, endpoint str // ScenarioCatalogIsDeleted deletes a named per-scenario ClusterCatalog and waits for it to be removed. func ScenarioCatalogIsDeleted(ctx context.Context, catalogUserName string) error { - sc := scenarioCtx(ctx) - catalogName, ok := sc.catalogs[catalogUserName] + sc := Scenario(ctx) + catalogName, ok := sc.Catalogs[catalogUserName] if !ok { return fmt.Errorf("no catalog %q has been created for this scenario", catalogUserName) } - _, err := k8sClient(ctx, "delete", "clustercatalog", catalogName, "--ignore-not-found=true", "--wait=true") + _, err := K8sClient(ctx, "delete", "clustercatalog", catalogName, "--ignore-not-found=true", "--wait=true") if err != nil { return fmt.Errorf("failed to delete catalog %q: %v", catalogUserName, err) } @@ -2055,12 +2055,12 @@ func ScenarioCatalogIsDeleted(ctx context.Context, catalogUserName string) error } func CatalogIsLabeledWith(ctx context.Context, catalogUserName, label string) error { - sc := scenarioCtx(ctx) - catalogName, ok := sc.catalogs[catalogUserName] + sc := Scenario(ctx) + catalogName, ok := sc.Catalogs[catalogUserName] if !ok { return fmt.Errorf("no catalog %q has been created for this scenario", catalogUserName) } - _, err := k8sClient(ctx, "label", "clustercatalog", catalogName, label, "--overwrite") + _, err := K8sClient(ctx, "label", "clustercatalog", catalogName, label, "--overwrite") if err != nil { return fmt.Errorf("failed to label catalog %q with %q: %v", catalogUserName, label, err) } @@ -2069,12 +2069,12 @@ func CatalogIsLabeledWith(ctx context.Context, catalogUserName, label string) er // ValidatingAdmissionPolicyIsActive waits for the named ValidatingAdmissionPolicy to be compiled // and active by checking that status.observedGeneration matches metadata.generation. -// The scenario fails on timeout if the policy never becomes active. +// The scenario fails on Timeout if the policy never becomes active. func ValidatingAdmissionPolicyIsActive(ctx context.Context, name string) error { - sc := scenarioCtx(ctx) - name = substituteScenarioVars(name, sc) - waitFor(ctx, func() bool { - out, err := k8sClient(ctx, "get", "validatingadmissionpolicy", name, "-o", + sc := Scenario(ctx) + name = SubstituteScenarioVars(name, sc) + WaitFor(ctx, func() bool { + out, err := K8sClient(ctx, "get", "validatingadmissionpolicy", name, "-o", "jsonpath={.metadata.generation},{.status.observedGeneration}") if err != nil { return false @@ -2089,14 +2089,14 @@ func ValidatingAdmissionPolicyIsActive(ctx context.Context, name string) error { // It pushes a new catalog image with the specified tag but does NOT update the ClusterCatalog. // Use ScenarioCatalogIsUpdatedToVersion to patch the ClusterCatalog to use the new version. func CatalogVersionWithPackages(ctx context.Context, catalogUserName, version string, table *godog.Table) error { - sc := scenarioCtx(ctx) + sc := Scenario(ctx) pkgOpts, err := parseCatalogTable(table) if err != nil { return err } - cat := catalog.NewCatalog(catalogUserName, sc.id, pkgOpts...) + cat := catalog.NewCatalog(catalogUserName, sc.ID, pkgOpts...) localRegistry, clusterRegistry, err := registryHosts() if err != nil { return err @@ -2109,7 +2109,7 @@ func CatalogVersionWithPackages(ctx context.Context, catalogUserName, version st // Accumulate package names (bundles may differ between versions) for orig, param := range result.PackageNames { - sc.catalogPackageNames[orig] = param + sc.CatalogPackageNames[orig] = param } return nil @@ -2117,12 +2117,12 @@ func CatalogVersionWithPackages(ctx context.Context, catalogUserName, version st // ScenarioCatalogIsUpdatedToVersion patches a named per-scenario ClusterCatalog's image ref to a new version tag. func ScenarioCatalogIsUpdatedToVersion(ctx context.Context, catalogUserName, version string) error { - sc := scenarioCtx(ctx) - catalogName, ok := sc.catalogs[catalogUserName] + sc := Scenario(ctx) + catalogName, ok := sc.Catalogs[catalogUserName] if !ok { return fmt.Errorf("no catalog %q has been created for this scenario", catalogUserName) } - ref, err := k8sClient(ctx, "get", "clustercatalog", catalogName, "-o", "jsonpath={.spec.source.image.ref}") + ref, err := K8sClient(ctx, "get", "clustercatalog", catalogName, "-o", "jsonpath={.spec.source.image.ref}") if err != nil { return err } @@ -2144,7 +2144,7 @@ func ScenarioCatalogIsUpdatedToVersion(ctx context.Context, catalogUserName, ver if err != nil { return err } - _, err = k8sClient(ctx, "patch", "clustercatalog", catalogName, "--type", "merge", "-p", string(pb)) + _, err = K8sClient(ctx, "patch", "clustercatalog", catalogName, "--type", "merge", "-p", string(pb)) return err } @@ -2154,8 +2154,8 @@ func ScenarioCatalogTagImage(ctx context.Context, catalogUserName, oldTag, newTa if err != nil { return err } - sc := scenarioCtx(ctx) - imageRef := fmt.Sprintf("%s/e2e/%s-catalog-%s:%s", localRegistry, catalogUserName, sc.id, oldTag) + sc := Scenario(ctx) + imageRef := fmt.Sprintf("%s/e2e/%s-catalog-%s:%s", localRegistry, catalogUserName, sc.ID, oldTag) return crane.Tag(imageRef, newTag, crane.Insecure) } @@ -2245,14 +2245,14 @@ func parseCatalogTable(table *godog.Table) ([]catalog.PackageOption, error) { // CatalogWithPackages builds a per-scenario catalog from a Gherkin data table. // Table columns: package | version | channel | replaces | contents func CatalogWithPackages(ctx context.Context, catalogUserName string, table *godog.Table) error { - sc := scenarioCtx(ctx) + sc := Scenario(ctx) pkgOpts, err := parseCatalogTable(table) if err != nil { return err } - cat := catalog.NewCatalog(catalogUserName, sc.id, pkgOpts...) + cat := catalog.NewCatalog(catalogUserName, sc.ID, pkgOpts...) localRegistry, clusterRegistry, err := registryHosts() if err != nil { return err @@ -2263,9 +2263,9 @@ func CatalogWithPackages(ctx context.Context, catalogUserName string, table *god return fmt.Errorf("failed to build catalog: %w", err) } - sc.catalogs[catalogUserName] = result.CatalogName + sc.Catalogs[catalogUserName] = result.CatalogName for orig, param := range result.PackageNames { - sc.catalogPackageNames[orig] = param + sc.CatalogPackageNames[orig] = param } catalogYAML := fmt.Sprintf(`apiVersion: olm.operatorframework.io/v1 @@ -2281,7 +2281,7 @@ spec: ref: %s `, result.CatalogName, result.CatalogImageRef) - catalogObj, err := toUnstructured(catalogYAML) + catalogObj, err := ToUnstructured(catalogYAML) if err != nil { return fmt.Errorf("failed to parse catalog YAML: %w", err) } @@ -2291,7 +2291,7 @@ spec: return fmt.Errorf("failed to marshal catalog YAML: %w", err) } - if _, _, err := k8scliWithInput(ctx, string(annotatedYAML), "apply", "-f", "-"); err != nil { + if _, _, err := K8sCLIWithInput(ctx, string(annotatedYAML), "apply", "-f", "-"); err != nil { return fmt.Errorf("failed to apply ClusterCatalog: %w", err) } @@ -2354,8 +2354,8 @@ func parseContents(contents string) ([]catalog.BundleOption, error) { // PrometheusMetricsAreReturned validates that each pod's stored metrics response is non-empty and parses as valid Prometheus text format. func PrometheusMetricsAreReturned(ctx context.Context) error { - sc := scenarioCtx(ctx) - for podName, mr := range sc.metricsResponse { + sc := Scenario(ctx) + for podName, mr := range sc.MetricsResponse { if mr == "" { return fmt.Errorf("metrics response is empty for pod %s", podName) } @@ -2373,10 +2373,10 @@ func PrometheusMetricsAreReturned(ctx context.Context) error { // OperatorTargetNamespace asserts that the operator deployment has the expected olm.targetNamespaces annotation. func OperatorTargetNamespace(ctx context.Context, operator, namespace string) error { - sc := scenarioCtx(ctx) - operator = substituteScenarioVars(operator, sc) - namespace = substituteScenarioVars(namespace, sc) - raw, err := k8sClient(ctx, "get", "deployment", "-n", sc.namespace, operator, "-o", "json") + sc := Scenario(ctx) + operator = SubstituteScenarioVars(operator, sc) + namespace = SubstituteScenarioVars(namespace, sc) + raw, err := K8sClient(ctx, "get", "deployment", "-n", sc.Namespace, operator, "-o", "json") if err != nil { return err } @@ -2390,8 +2390,8 @@ func OperatorTargetNamespace(ctx context.Context, operator, namespace string) er return fmt.Errorf("expected target namespace %s, got %s", namespace, tns) } - if rec := RecorderFromContext(ctx); rec != nil { - cmd := fmt.Sprintf("kubectl get deployment -n %s %s -o jsonpath='{.spec.template.metadata.annotations.olm\\.targetNamespaces}'", sc.namespace, operator) + if rec := CommandRecorderFromContext(ctx); rec != nil { + cmd := fmt.Sprintf("kubectl get deployment -n %s %s -o jsonpath='{.spec.template.metadata.annotations.olm\\.targetNamespaces}'", sc.Namespace, operator) rec.RecordCustom(cmd, tns+"\n", "") } return nil @@ -2399,9 +2399,9 @@ func OperatorTargetNamespace(ctx context.Context, operator, namespace string) er // MarkDeploymentReadiness controls a deployment's readiness probe by removing or creating the readiness file in its pod. func MarkDeploymentReadiness(ctx context.Context, deploymentName, state string) error { - sc := scenarioCtx(ctx) - deploymentName = substituteScenarioVars(deploymentName, sc) - v, err := k8sClient(ctx, "get", "deployment", "-n", sc.namespace, deploymentName, "-o", "jsonpath={.spec.selector.matchLabels}") + sc := Scenario(ctx) + deploymentName = SubstituteScenarioVars(deploymentName, sc) + v, err := K8sClient(ctx, "get", "deployment", "-n", sc.Namespace, deploymentName, "-o", "jsonpath={.spec.selector.matchLabels}") if err != nil { return err } @@ -2409,11 +2409,11 @@ func MarkDeploymentReadiness(ctx context.Context, deploymentName, state string) if err := json.Unmarshal([]byte(v), &labels); err != nil { return err } - podNameCmd := []string{"get", "pod", "-n", sc.namespace, "-o", "jsonpath={.items[0].metadata.name}"} + podNameCmd := []string{"get", "pod", "-n", sc.Namespace, "-o", "jsonpath={.items[0].metadata.name}"} for k, v := range labels { podNameCmd = append(podNameCmd, fmt.Sprintf("--selector=%s=%s", k, v)) } - podName, err := k8sClient(ctx, podNameCmd...) + podName, err := K8sClient(ctx, podNameCmd...) if err != nil { return err } @@ -2426,7 +2426,7 @@ func MarkDeploymentReadiness(ctx context.Context, deploymentName, state string) default: return fmt.Errorf("invalid state %s", state) } - _, err = k8sClient(ctx, "exec", podName, "-n", sc.namespace, "--", op, "/tmp/www/ready") + _, err = K8sClient(ctx, "exec", podName, "-n", sc.Namespace, "--", op, "/tmp/www/ready") return err } @@ -2455,7 +2455,7 @@ func SetCRDFieldMinValue(ctx context.Context, resourceType, jsonPath string, min patchPath := fmt.Sprintf("/spec/versions/0/schema/openAPIV3Schema/%s/minimum", strings.Join(schemaParts, "/")) patch := fmt.Sprintf(`[{"op": "replace", "path": "%s", "value": %d}]`, patchPath, minValue) - _, err := k8sClient(ctx, "patch", "crd", crdName, "--type=json", "-p", patch) + _, err := K8sClient(ctx, "patch", "crd", crdName, "--type=json", "-p", patch) return err } @@ -2491,12 +2491,12 @@ func extendMap(m map[string]string, keyValue ...string) map[string]string { return m } -func getResource(kind string, name string, namespace string) (*unstructured.Unstructured, error) { - out, err := k8sClient(context.Background(), "get", kind, name, "-n", namespace, "-o", "yaml") +func GetResource(kind string, name string, namespace string) (*unstructured.Unstructured, error) { + out, err := K8sClient(context.Background(), "get", kind, name, "-n", namespace, "-o", "yaml") if err != nil { return nil, err } - obj, err := toUnstructured(out) + obj, err := ToUnstructured(out) if err != nil { return nil, err } @@ -2507,7 +2507,7 @@ func getResource(kind string, name string, namespace string) (*unstructured.Unst // this method is best called when the extension has been installed successfully. An error is returned if there was // any issue in determining the extension's resources. func listExtensionResources(extName string) ([]client.Object, error) { - if enabled, found := featureGates[features.BoxcutterRuntime]; found && enabled { + if enabled, found := FeatureGates[features.BoxcutterRuntime]; found && enabled { return listExtensionRevisionResources(extName) } return listHelmReleaseResources(extName) @@ -2533,9 +2533,9 @@ func listHelmReleaseResources(extName string) ([]client.Object, error) { return objs, nil } -// helmReleaseSecretForExtension returns the Helm release secret for the extension with name extName +// helmReleaseSecretForExtension returns the Helm release secret for the extension with Name extName func helmReleaseSecretForExtension(extName string) (*corev1.Secret, error) { - out, err := k8sClient(context.Background(), "get", "secrets", "-n", olmNamespace, + out, err := K8sClient(context.Background(), "get", "secrets", "-n", OLMNamespace, "-l", fmt.Sprintf("name=%s,status=deployed", extName), "--field-selector", "type=operatorframework.io/index.v1", "-o", "json") if err != nil { @@ -2613,7 +2613,7 @@ func listExtensionRevisionResources(extName string) ([]client.Object, error) { specObj := &phase.Objects[j] switch specObj.Type { case ocv1.ObjectTypeObjectReference: - resolved, err := resolveObjectRef(specObj.ObjectRef) + resolved, err := ResolveObjectRef(specObj.ObjectRef) if err != nil { return nil, fmt.Errorf("resolving ref in phase %q object %d: %w", phase.Name, j, err) } @@ -2629,9 +2629,9 @@ func listExtensionRevisionResources(extName string) ([]client.Object, error) { return objs, nil } -// resolveObjectRef fetches an object from a Secret ref using kubectl. -func resolveObjectRef(ref ocv1.ObjectSourceRef) (*unstructured.Unstructured, error) { - out, err := k8sClient(context.Background(), "get", "secret", ref.Name, "-n", ref.Namespace, "-o", "json") +// ResolveObjectRef fetches an object from a Secret ref using kubectl. +func ResolveObjectRef(ref ocv1.ObjectSourceRef) (*unstructured.Unstructured, error) { + out, err := K8sClient(context.Background(), "get", "secret", ref.Name, "-n", ref.Namespace, "-o", "json") if err != nil { return nil, fmt.Errorf("getting Secret %s/%s: %w", ref.Namespace, ref.Name, err) } @@ -2670,7 +2670,7 @@ func resolveObjectRef(ref ocv1.ObjectSourceRef) (*unstructured.Unstructured, err // latestActiveRevisionForExtension returns the latest active revision for the extension called extName func latestActiveRevisionForExtension(extName string) (*ocv1.ClusterObjectSet, error) { - revisions, err := listClusterObjectSetsInGroup(context.Background(), extName) + revisions, err := ListClusterObjectSetsInGroup(context.Background(), extName) if err != nil { return nil, fmt.Errorf("error listing revisions for extension '%s': %w", extName, err) } @@ -2692,12 +2692,12 @@ func latestActiveRevisionForExtension(extName string) (*ocv1.ClusterObjectSet, e return latest, nil } -// AnnotationsAreAdded adds annotations from a data table to a resource using kubectl annotate. +// AnnotationsAreAdded adds annotations from a data table to a Resource using kubectl annotate. // The table must have "key" and "value" columns. -// Only supports namespaced resources (always uses sc.namespace). +// Only supports namespaced resources (always uses sc.Namespace). func AnnotationsAreAdded(ctx context.Context, resourceName string, table *godog.Table) error { - sc := scenarioCtx(ctx) - resourceName = substituteScenarioVars(resourceName, sc) + sc := Scenario(ctx) + resourceName = SubstituteScenarioVars(resourceName, sc) pairs, err := parseKeyValueTable(table, sc) if err != nil { @@ -2713,19 +2713,19 @@ func AnnotationsAreAdded(ctx context.Context, resourceName string, table *godog. for k, v := range pairs { args = append(args, fmt.Sprintf("%s=%s", k, v)) } - args = append(args, "--overwrite", "-n", sc.namespace) - if _, err := k8sClient(ctx, args...); err != nil { + args = append(args, "--overwrite", "-n", sc.Namespace) + if _, err := K8sClient(ctx, args...); err != nil { return fmt.Errorf("failed to annotate %s: %w; stderr: %s", resourceName, err, stderrOutput(err)) } return nil } -// LabelsAreAdded adds labels from a data table to a resource using kubectl label. +// LabelsAreAdded adds labels from a data table to a Resource using kubectl label. // The table must have "key" and "value" columns. -// Only supports namespaced resources (always uses sc.namespace). +// Only supports namespaced resources (always uses sc.Namespace). func LabelsAreAdded(ctx context.Context, resourceName string, table *godog.Table) error { - sc := scenarioCtx(ctx) - resourceName = substituteScenarioVars(resourceName, sc) + sc := Scenario(ctx) + resourceName = SubstituteScenarioVars(resourceName, sc) pairs, err := parseKeyValueTable(table, sc) if err != nil { @@ -2741,19 +2741,19 @@ func LabelsAreAdded(ctx context.Context, resourceName string, table *godog.Table for k, v := range pairs { args = append(args, fmt.Sprintf("%s=%s", k, v)) } - args = append(args, "--overwrite", "-n", sc.namespace) - if _, err := k8sClient(ctx, args...); err != nil { + args = append(args, "--overwrite", "-n", sc.Namespace) + if _, err := K8sClient(ctx, args...); err != nil { return fmt.Errorf("failed to label %s: %w; stderr: %s", resourceName, err, stderrOutput(err)) } return nil } -// ResourceHasAnnotations waits for a resource to have all annotations specified in the data table. +// ResourceHasAnnotations waits for a Resource to have all annotations specified in the data table. // The table must have "key" and "value" columns. -// Only supports namespaced resources (always uses sc.namespace). +// Only supports namespaced resources (always uses sc.Namespace). func ResourceHasAnnotations(ctx context.Context, resourceName string, table *godog.Table) error { - sc := scenarioCtx(ctx) - resourceName = substituteScenarioVars(resourceName, sc) + sc := Scenario(ctx) + resourceName = SubstituteScenarioVars(resourceName, sc) expected, err := parseKeyValueTable(table, sc) if err != nil { @@ -2765,8 +2765,8 @@ func ResourceHasAnnotations(ctx context.Context, resourceName string, table *god return fmt.Errorf("invalid resource name format: %q (expected kind/name)", resourceName) } - waitFor(ctx, func() bool { - out, err := k8sClient(ctx, "get", kind, name, "-n", sc.namespace, "-o", "json") + WaitFor(ctx, func() bool { + out, err := K8sClient(ctx, "get", kind, name, "-n", sc.Namespace, "-o", "json") if err != nil { return false } @@ -2797,12 +2797,12 @@ func matchLabels(actual, expected map[string]string) (string, string, bool) { return "", "", true } -// ResourceHasLabels waits for a resource to have all labels specified in the data table. +// ResourceHasLabels waits for a Resource to have all labels specified in the data table. // The table must have "key" and "value" columns. -// Only supports namespaced resources (always uses sc.namespace). +// Only supports namespaced resources (always uses sc.Namespace). func ResourceHasLabels(ctx context.Context, resourceName string, table *godog.Table) error { - sc := scenarioCtx(ctx) - resourceName = substituteScenarioVars(resourceName, sc) + sc := Scenario(ctx) + resourceName = SubstituteScenarioVars(resourceName, sc) expected, err := parseKeyValueTable(table, sc) if err != nil { @@ -2814,8 +2814,8 @@ func ResourceHasLabels(ctx context.Context, resourceName string, table *godog.Ta return fmt.Errorf("invalid resource name format: %q (expected kind/name)", resourceName) } - waitFor(ctx, func() bool { - out, err := k8sClient(ctx, "get", kind, name, "-n", sc.namespace, "-o", "json") + WaitFor(ctx, func() bool { + out, err := K8sClient(ctx, "get", kind, name, "-n", sc.Namespace, "-o", "json") if err != nil { return false } @@ -2832,18 +2832,18 @@ func ResourceHasLabels(ctx context.Context, resourceName string, table *godog.Ta return nil } -// NamespaceHasLabels waits for a namespace (cluster-scoped) to have all labels specified in the data table. +// NamespaceHasLabels waits for a Namespace (cluster-scoped) to have all labels specified in the data table. func NamespaceHasLabels(ctx context.Context, nsName string, table *godog.Table) error { - sc := scenarioCtx(ctx) - nsName = substituteScenarioVars(nsName, sc) + sc := Scenario(ctx) + nsName = SubstituteScenarioVars(nsName, sc) expected, err := parseKeyValueTable(table, sc) if err != nil { return fmt.Errorf("invalid labels table: %w", err) } - waitFor(ctx, func() bool { - out, err := k8sClient(ctx, "get", "namespace", nsName, "-o", "json") + WaitFor(ctx, func() bool { + out, err := K8sClient(ctx, "get", "namespace", nsName, "-o", "json") if err != nil { return false } @@ -2860,12 +2860,12 @@ func NamespaceHasLabels(ctx context.Context, nsName string, table *godog.Table) return nil } -// NamespaceDoesNotHaveLabel verifies a namespace does not have the specified label. +// NamespaceDoesNotHaveLabel verifies a Namespace does not have the specified label. func NamespaceDoesNotHaveLabel(ctx context.Context, nsName string, labelKey string) error { - sc := scenarioCtx(ctx) - nsName = substituteScenarioVars(nsName, sc) + sc := Scenario(ctx) + nsName = SubstituteScenarioVars(nsName, sc) - out, err := k8sClient(ctx, "get", "namespace", nsName, "-o", "json") + out, err := K8sClient(ctx, "get", "namespace", nsName, "-o", "json") if err != nil { return fmt.Errorf("failed to get namespace %q: %w", nsName, err) } @@ -2879,18 +2879,18 @@ func NamespaceDoesNotHaveLabel(ctx context.Context, nsName string, labelKey stri return nil } -// NamespaceIsManagedByOLM waits for a namespace to be owned by the extension's active ClusterObjectSet. +// NamespaceIsManagedByOLM waits for a Namespace to be owned by the extension's active ClusterObjectSet. func NamespaceIsManagedByOLM(ctx context.Context, nsName string) error { - sc := scenarioCtx(ctx) - nsName = substituteScenarioVars(nsName, sc) + sc := Scenario(ctx) + nsName = SubstituteScenarioVars(nsName, sc) - waitFor(ctx, func() bool { - revision, err := latestActiveRevisionForExtension(sc.clusterExtensionName) + WaitFor(ctx, func() bool { + revision, err := latestActiveRevisionForExtension(sc.ClusterExtensionName) if err != nil { - logger.V(1).Error(err, "failed to get active revision", "extension", sc.clusterExtensionName) + logger.V(1).Error(err, "failed to get active revision", "extension", sc.ClusterExtensionName) return false } - out, err := k8sClient(ctx, "get", "namespace", nsName, "-o", "json") + out, err := K8sClient(ctx, "get", "namespace", nsName, "-o", "json") if err != nil { return false } @@ -2910,12 +2910,12 @@ func NamespaceIsManagedByOLM(ctx context.Context, nsName string) error { return nil } -// NamespaceHasNoOwnerReferences verifies a namespace remains unmanaged after installation. +// NamespaceHasNoOwnerReferences verifies a Namespace remains unmanaged after installation. func NamespaceHasNoOwnerReferences(ctx context.Context, nsName string) error { - sc := scenarioCtx(ctx) - nsName = substituteScenarioVars(nsName, sc) + sc := Scenario(ctx) + nsName = SubstituteScenarioVars(nsName, sc) - out, err := k8sClient(ctx, "get", "namespace", nsName, "-o", "json") + out, err := K8sClient(ctx, "get", "namespace", nsName, "-o", "json") if err != nil { return fmt.Errorf("failed to get namespace %q: %w", nsName, err) } @@ -2950,7 +2950,7 @@ func nestedString(obj map[string]interface{}, keys ...string) (string, bool) { // parseKeyValueTable extracts key-value pairs from a godog data table. // The table must have a header row with "key" and "value" columns. // If sc is non-nil, variable substitution is applied to both keys and values. -func parseKeyValueTable(table *godog.Table, sc *scenarioContext) (map[string]string, error) { +func parseKeyValueTable(table *godog.Table, sc *World) (map[string]string, error) { if len(table.Rows) < 2 { return nil, fmt.Errorf("table must have a header row and at least one data row") } @@ -2982,19 +2982,19 @@ func parseKeyValueTable(table *godog.Table, sc *scenarioContext) (map[string]str key := row.Cells[keyIdx].Value value := row.Cells[valueIdx].Value if sc != nil { - key = substituteScenarioVars(key, sc) - value = substituteScenarioVars(value, sc) + key = SubstituteScenarioVars(key, sc) + value = SubstituteScenarioVars(value, sc) } result[key] = value } return result, nil } -// RolloutRestartIsPerformed runs "kubectl rollout restart deployment/". +// RolloutRestartIsPerformed runs "kubectl rollout restart deployment/". // See: https://github.com/operator-framework/operator-lifecycle-manager/issues/3392 func RolloutRestartIsPerformed(ctx context.Context, resourceName string) error { - sc := scenarioCtx(ctx) - resourceName = substituteScenarioVars(resourceName, sc) + sc := Scenario(ctx) + resourceName = SubstituteScenarioVars(resourceName, sc) kind, deploymentName, ok := strings.Cut(resourceName, "/") if !ok { @@ -3007,7 +3007,7 @@ func RolloutRestartIsPerformed(ctx context.Context, resourceName string) error { // Run kubectl rollout restart to add the restart annotation. // This is the real command users run, so we test actual user behavior. - out, err := k8sClient(ctx, "rollout", "restart", resourceName, "-n", sc.namespace) + out, err := K8sClient(ctx, "rollout", "restart", resourceName, "-n", sc.Namespace) if err != nil { return fmt.Errorf("failed to rollout restart %s: %w; stderr: %s", resourceName, err, stderrOutput(err)) } @@ -3019,13 +3019,13 @@ func RolloutRestartIsPerformed(ctx context.Context, resourceName string) error { // DeploymentPodTemplateHasAnnotation waits for the deployment's pod template to have // the given annotation key. Uses JSON parsing to avoid JSONPath issues with dots -// in annotation keys. Polls with timeout. +// in annotation keys. Polls with Timeout. func DeploymentPodTemplateHasAnnotation(ctx context.Context, deploymentName, annotationKey string) error { - sc := scenarioCtx(ctx) - deploymentName = substituteScenarioVars(deploymentName, sc) + sc := Scenario(ctx) + deploymentName = SubstituteScenarioVars(deploymentName, sc) - waitFor(ctx, func() bool { - out, err := k8sClient(ctx, "get", "deployment", deploymentName, "-n", sc.namespace, "-o", "json") + WaitFor(ctx, func() bool { + out, err := K8sClient(ctx, "get", "deployment", deploymentName, "-n", sc.Namespace, "-o", "json") if err != nil { return false } @@ -3054,16 +3054,16 @@ func DeploymentPodTemplateHasAnnotation(ctx context.Context, deploymentName, ann // fields, which are pruned by structural schemas). Toggling ensures that each call // results in a spec change, reliably bumping .metadata.generation. func TriggerClusterExtensionReconciliation(ctx context.Context) error { - sc := scenarioCtx(ctx) + sc := Scenario(ctx) - out, err := k8sClient(ctx, "get", "clusterextension", sc.clusterExtensionName, "-o", "json") + out, err := K8sClient(ctx, "get", "clusterextension", sc.ClusterExtensionName, "-o", "json") if err != nil { - return fmt.Errorf("failed to get ClusterExtension %s: %w; stderr: %s", sc.clusterExtensionName, err, stderrOutput(err)) + return fmt.Errorf("failed to get ClusterExtension %s: %w; stderr: %s", sc.ClusterExtensionName, err, stderrOutput(err)) } var obj map[string]interface{} if err := json.Unmarshal([]byte(out), &obj); err != nil { - return fmt.Errorf("failed to unmarshal ClusterExtension %s JSON: %w", sc.clusterExtensionName, err) + return fmt.Errorf("failed to unmarshal ClusterExtension %s JSON: %w", sc.ClusterExtensionName, err) } currentEnforcement, _ := nestedString(obj, "spec", "install", "preflight", "crdUpgradeSafety", "enforcement") @@ -3074,11 +3074,11 @@ func TriggerClusterExtensionReconciliation(ctx context.Context) error { } payload := fmt.Sprintf(`{"spec":{"install":{"preflight":{"crdUpgradeSafety":{"enforcement":%q}}}}}`, newEnforcement) - _, err = k8sClient(ctx, "patch", "clusterextension", sc.clusterExtensionName, + _, err = K8sClient(ctx, "patch", "clusterextension", sc.ClusterExtensionName, "--type=merge", "-p", payload) if err != nil { - return fmt.Errorf("failed to trigger reconciliation for ClusterExtension %s: %w; stderr: %s", sc.clusterExtensionName, err, stderrOutput(err)) + return fmt.Errorf("failed to trigger reconciliation for ClusterExtension %s: %w; stderr: %s", sc.ClusterExtensionName, err, stderrOutput(err)) } return nil } @@ -3086,11 +3086,11 @@ func TriggerClusterExtensionReconciliation(ctx context.Context) error { // DeploymentRolloutIsComplete verifies that a deployment rollout has completed successfully. // This ensures the new ReplicaSet is fully scaled up and the old one is scaled down. func DeploymentRolloutIsComplete(ctx context.Context, deploymentName string) error { - sc := scenarioCtx(ctx) - deploymentName = substituteScenarioVars(deploymentName, sc) + sc := Scenario(ctx) + deploymentName = SubstituteScenarioVars(deploymentName, sc) - waitFor(ctx, func() bool { - out, err := k8sClient(ctx, "rollout", "status", "deployment/"+deploymentName, "-n", sc.namespace, "--watch=false") + WaitFor(ctx, func() bool { + out, err := K8sClient(ctx, "rollout", "status", "deployment/"+deploymentName, "-n", sc.Namespace, "--watch=false") if err != nil { logger.V(1).Info("Failed to get rollout status", "deployment", deploymentName, "error", err) return false @@ -3109,11 +3109,11 @@ func DeploymentRolloutIsComplete(ctx context.Context, deploymentName string) err // DeploymentHasReplicaSets verifies that a deployment has the expected number of ReplicaSets // and that at least one owned ReplicaSet is active with pods running. func DeploymentHasReplicaSets(ctx context.Context, deploymentName string, expectedCount int) error { - sc := scenarioCtx(ctx) - deploymentName = substituteScenarioVars(deploymentName, sc) + sc := Scenario(ctx) + deploymentName = SubstituteScenarioVars(deploymentName, sc) - waitFor(ctx, func() bool { - deploymentOut, err := k8sClient(ctx, "get", "deployment", deploymentName, "-n", sc.namespace, "-o", "json") + WaitFor(ctx, func() bool { + deploymentOut, err := K8sClient(ctx, "get", "deployment", deploymentName, "-n", sc.Namespace, "-o", "json") if err != nil { logger.V(1).Info("Failed to get deployment", "deployment", deploymentName, "error", err) return false @@ -3124,11 +3124,11 @@ func DeploymentHasReplicaSets(ctx context.Context, deploymentName string, expect logger.V(1).Info("Failed to parse deployment", "error", err) return false } - if !generationObserved(deployment.Generation, deployment.Status.ObservedGeneration) { + if !GenerationObserved(deployment.Generation, deployment.Status.ObservedGeneration) { return false } - args := []string{"get", "rs", "-n", sc.namespace} + args := []string{"get", "rs", "-n", sc.Namespace} if deployment.Spec.Selector != nil && len(deployment.Spec.Selector.MatchLabels) > 0 { var parts []string for k, v := range deployment.Spec.Selector.MatchLabels { @@ -3137,7 +3137,7 @@ func DeploymentHasReplicaSets(ctx context.Context, deploymentName string, expect args = append(args, "-l", strings.Join(parts, ",")) } args = append(args, "-o", "json") - out, err := k8sClient(ctx, args...) + out, err := K8sClient(ctx, args...) if err != nil { logger.V(1).Info("Failed to get ReplicaSets", "deployment", deploymentName, "error", err) return false @@ -3169,7 +3169,7 @@ func DeploymentHasReplicaSets(ctx context.Context, deploymentName string, expect // Verify at least one ReplicaSet has active replicas hasActiveRS := false for _, rs := range rsList { - if generationObserved(rs.Generation, rs.Status.ObservedGeneration) && rs.Status.Replicas > 0 && rs.Status.ReadyReplicas > 0 { + if GenerationObserved(rs.Generation, rs.Status.ObservedGeneration) && rs.Status.Replicas > 0 && rs.Status.ReadyReplicas > 0 { hasActiveRS = true logger.V(1).Info("Found active ReplicaSet", "name", rs.Name, "replicas", rs.Status.Replicas, "ready", rs.Status.ReadyReplicas) } diff --git a/test/internal/e2e/steps/tls_steps.go b/test/internal/e2e/steps/tls_steps.go index eac18b1202..37fc3647c0 100644 --- a/test/internal/e2e/steps/tls_steps.go +++ b/test/internal/e2e/steps/tls_steps.go @@ -15,7 +15,7 @@ import ( corev1 "k8s.io/api/core/v1" ) -// tlsCipherSuiteByName is a name→ID map that covers TLS 1.3 cipher suites +// tlsCipherSuiteByName is a Name→ID map that covers TLS 1.3 cipher suites // (not returned by tls.CipherSuites) as well as all TLS 1.2 suites. var tlsCipherSuiteByName = func() map[string]uint16 { m := map[string]uint16{ @@ -33,7 +33,7 @@ var tlsCipherSuiteByName = func() map[string]uint16 { return m }() -// tlsCipherSuiteName returns a human-readable name for a cipher suite ID, +// tlsCipherSuiteName returns a human-readable Name for a cipher suite ID, // including TLS 1.3 suites that tls.CipherSuiteName does not recognise. func tlsCipherSuiteName(id uint16) string { for name, cid := range tlsCipherSuiteByName { @@ -57,10 +57,10 @@ var curveIDByName = map[string]tls.CurveID{ } // getMetricsService returns the full Service object for the named component's metrics service. -// The namespace is available via svc.Namespace. +// The Namespace is available via svc.Namespace. func getMetricsService(component string) (*corev1.Service, error) { serviceName := fmt.Sprintf("%s-service", component) - serviceNs, err := k8sClient(context.Background(), "get", "service", "-A", "-o", + serviceNs, err := K8sClient(context.Background(), "get", "service", "-A", "-o", fmt.Sprintf(`jsonpath={.items[?(@.metadata.name=="%s")].metadata.namespace}`, serviceName)) if err != nil { return nil, fmt.Errorf("failed to find namespace for service %s: %w", serviceName, err) @@ -70,7 +70,7 @@ func getMetricsService(component string) (*corev1.Service, error) { return nil, fmt.Errorf("service %s not found in any namespace", serviceName) } - raw, err := k8sClient(context.Background(), "get", "service", "-n", serviceNs, serviceName, "-o", "json") + raw, err := K8sClient(context.Background(), "get", "service", "-n", serviceNs, serviceName, "-o", "json") if err != nil { return nil, fmt.Errorf("failed to get service %s: %w", serviceName, err) } @@ -101,7 +101,7 @@ func randomAvailablePort() (int, error) { } // portForward starts a kubectl port-forward to target (e.g. "service/foo" or "pod/bar") -// in the given namespace, mapping a random local port to remotePort. It returns the +// in the given Namespace, mapping a random local port to remotePort. It returns the // local address and a cleanup function. The caller is responsible for calling cleanup. func portForward(ctx context.Context, ns, target string, remotePort int32) (string, func(), error) { localPort, err := randomAvailablePort() @@ -109,9 +109,9 @@ func portForward(ctx context.Context, ns, target string, remotePort int32) (stri return "", nil, fmt.Errorf("failed to find a free local port: %w", err) } - pfCmd := exec.Command(k8sCli, "port-forward", "-n", ns, //nolint:gosec + pfCmd := exec.Command(K8sCLI, "port-forward", "-n", ns, //nolint:gosec target, fmt.Sprintf("%d:%d", localPort, remotePort)) - pfCmd.Env = append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", kubeconfigPath)) + pfCmd.Env = append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", KubeconfigPath)) if err := pfCmd.Start(); err != nil { return "", nil, fmt.Errorf("failed to start port-forward to %s: %w", target, err) } @@ -123,9 +123,9 @@ func portForward(ctx context.Context, ns, target string, remotePort int32) (stri } } - if rec := RecorderFromContext(ctx); rec != nil { + if rec := CommandRecorderFromContext(ctx); rec != nil { rec.RecordCustom( - fmt.Sprintf("%s port-forward -n %s %s %d:%d &", k8sCli, ns, target, localPort, remotePort), + fmt.Sprintf("%s port-forward -n %s %s %d:%d &", K8sCLI, ns, target, localPort, remotePort), "", "", ) } @@ -155,9 +155,9 @@ func withMetricsPortForward(ctx context.Context, component string, fn func(addr // Wait until the port-forward is accepting connections. A plain TLS dial (no version // restrictions) serves as the readiness probe; any successful TLS handshake confirms - // the tunnel is up. A short per-attempt timeout prevents the dial from blocking + // the tunnel is up. A short per-attempt Timeout prevents the dial from blocking // indefinitely if the local port is open but the upstream handshake stalls. - waitFor(ctx, func() bool { + WaitFor(ctx, func() bool { dialer := &net.Dialer{Timeout: 3 * time.Second} conn, err := tls.DialWithDialer(dialer, "tcp", addr, &tls.Config{InsecureSkipVerify: true}) //nolint:gosec if err != nil { @@ -222,8 +222,8 @@ func MetricsEndpointAcceptsConnectionUsingCurve(ctx context.Context, component, }) } -// componentDeploymentName maps the short component name used in feature files to -// the actual Kubernetes Deployment name. +// componentDeploymentName maps the short component Name used in feature files to +// the actual Kubernetes Deployment Name. func componentDeploymentName(component string) (string, error) { switch component { case "operator-controller": @@ -238,7 +238,7 @@ func componentDeploymentName(component string) (string, error) { // getDeploymentContainerArgs returns the args list of the container named "manager" // inside the named deployment. func getDeploymentContainerArgs(ns, name string) ([]string, error) { - raw, err := k8sClient(context.Background(), "get", "deployment", name, "-n", ns, "-o", "json") + raw, err := K8sClient(context.Background(), "get", "deployment", name, "-n", ns, "-o", "json") if err != nil { return nil, fmt.Errorf("getting deployment %s/%s: %w", ns, name, err) } @@ -257,7 +257,7 @@ func getDeploymentContainerArgs(ns, name string) ([]string, error) { // getDeploymentContainerIndex returns the index of the container named "manager" // inside the named deployment. func getDeploymentContainerIndex(ns, name string) (int, error) { - raw, err := k8sClient(context.Background(), "get", "deployment", name, "-n", ns, "-o", "json") + raw, err := K8sClient(context.Background(), "get", "deployment", name, "-n", ns, "-o", "json") if err != nil { return -1, fmt.Errorf("getting deployment %s/%s: %w", ns, name, err) } @@ -285,7 +285,7 @@ func patchDeploymentArgs(ns, name string, args []string) error { return err } patch := fmt.Sprintf(`[{"op":"replace","path":"/spec/template/spec/containers/%d/args","value":%s}]`, idx, string(argsJSON)) - _, err = k8sClient(context.Background(), "patch", "deployment", name, "-n", ns, "--type=json", "-p", patch) + _, err = K8sClient(context.Background(), "patch", "deployment", name, "-n", ns, "--type=json", "-p", patch) return err } @@ -322,26 +322,26 @@ func configureDeploymentCustomTLS(ctx context.Context, component, version, ciphe return err } - origArgs, err := getDeploymentContainerArgs(olmNamespace, deploymentName) + origArgs, err := getDeploymentContainerArgs(OLMNamespace, deploymentName) if err != nil { return err } - sc := scenarioCtx(ctx) + sc := Scenario(ctx) sc.deploymentRestores = append(sc.deploymentRestores, deploymentRestore{ - name: deploymentName, - namespace: olmNamespace, + Name: deploymentName, + Namespace: OLMNamespace, patchedArgs: true, originalArgs: origArgs, }) newArgs := buildCustomTLSArgs(origArgs, version, ciphers, curves) - if err := patchDeploymentArgs(olmNamespace, deploymentName, newArgs); err != nil { + if err := patchDeploymentArgs(OLMNamespace, deploymentName, newArgs); err != nil { return fmt.Errorf("patching %s with custom TLS args: %w", deploymentName, err) } - waitFor(ctx, func() bool { - _, err := k8sClient(ctx, "rollout", "status", "-n", olmNamespace, + WaitFor(ctx, func() bool { + _, err := K8sClient(ctx, "rollout", "status", "-n", OLMNamespace, fmt.Sprintf("deployment/%s", deploymentName), "--timeout=10s") return err == nil }) diff --git a/test/internal/e2e/steps/upgrade_steps.go b/test/internal/e2e/steps/upgrade_steps.go index ad7fdda720..4fbb3fa4f4 100644 --- a/test/internal/e2e/steps/upgrade_steps.go +++ b/test/internal/e2e/steps/upgrade_steps.go @@ -32,7 +32,7 @@ var ( if err != nil { return err } - olmNamespace = olm.Namespace + OLMNamespace = olm.Namespace return nil }) upgradeOperator = sync.OnceValue(func() error { @@ -65,7 +65,7 @@ func runInstallScript(envVar string) error { return fmt.Errorf("ROOT_DIR environment variable not set") } cmd.Dir = dir - cmd.Env = append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", kubeconfigPath)) + cmd.Env = append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", KubeconfigPath)) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() @@ -78,13 +78,13 @@ func OLMIsUpgraded(ctx context.Context) error { } // ComponentIsReadyToReconcile waits for the named component's deployment to be fully rolled out, -// then checks the leader election lease and stores the leader pod name in the scenario context. +// then checks the leader election lease and stores the leader pod Name in the scenario context. func ComponentIsReadyToReconcile(ctx context.Context, component string) error { - sc := scenarioCtx(ctx) - ns := namespaceForComponent(component) + sc := Scenario(ctx) + ns := NamespaceForComponent(component) // Wait for deployment rollout to complete - depName, err := k8sClient(ctx, "get", "deployments", "-n", ns, + depName, err := K8sClient(ctx, "get", "deployments", "-n", ns, "-l", fmt.Sprintf("app.kubernetes.io/name=%s", component), "-o", "jsonpath={.items[0].metadata.name}") if err != nil { @@ -93,8 +93,8 @@ func ComponentIsReadyToReconcile(ctx context.Context, component string) error { if depName == "" { return fmt.Errorf("failed to find deployment for component %s in namespace %s: no matching deployments found", component, ns) } - if _, err := k8sClient(ctx, "rollout", "status", fmt.Sprintf("deployment/%s", depName), - "-n", ns, fmt.Sprintf("--timeout=%s", timeout)); err != nil { + if _, err := K8sClient(ctx, "rollout", "status", fmt.Sprintf("deployment/%s", depName), + "-n", ns, fmt.Sprintf("--timeout=%s", Timeout)); err != nil { return fmt.Errorf("deployment rollout failed for %s: %w", component, err) } @@ -105,33 +105,33 @@ func ComponentIsReadyToReconcile(ctx context.Context, component string) error { } // Leader election can take up to LeaseDuration (137s) + RetryPeriod (26s) ≈ 163s in the worst case - waitFor(ctx, func() bool { - output, err := k8sClient(ctx, "get", "lease", leaseName, "-n", ns, + WaitFor(ctx, func() bool { + output, err := K8sClient(ctx, "get", "lease", leaseName, "-n", ns, "-o", "jsonpath={.spec.holderIdentity}") if err != nil || output == "" { return false } - // Extract pod name from lease holder identity (format: _) + // Extract pod Name from lease holder identity (format: _) podName := strings.Split(output, "_")[0] - sc.leaderPods[component] = podName + sc.LeaderPods[component] = podName return true }) - logger.V(1).Info("Component is ready to reconcile", "component", component, "leaderPod", sc.leaderPods[component]) + logger.V(1).Info("Component is ready to reconcile", "component", component, "leaderPod", sc.LeaderPods[component]) return nil } -// resourceTypeToComponent maps resource type names to their controller component labels. +// resourceTypeToComponent maps Resource type names to their controller component labels. var resourceTypeToComponent = map[string]string{ "ClusterCatalog": "catalogd", "ClusterExtension": "operator-controller", } // reconcileEndingCheck returns a function that checks whether the leader pod's logs -// contain a "reconcile ending" entry for the given resource name. +// contain a "reconcile ending" entry for the given Resource Name. func reconcileEndingCheck(ctx context.Context, namespace, leaderPod, resourceName string) func() bool { return func() bool { - logs, err := k8sClient(ctx, "logs", leaderPod, "-n", namespace, "--all-containers=true", "--tail=1000") + logs, err := K8sClient(ctx, "logs", leaderPod, "-n", namespace, "--all-containers=true", "--tail=1000") if err != nil { return false } @@ -147,10 +147,10 @@ func reconcileEndingCheck(ctx context.Context, namespace, leaderPod, resourceNam // ClusterExtensionIsReconciled waits for the ClusterExtension to be reconciled // by checking the leader pod's logs for a "reconcile ending" entry. func ClusterExtensionIsReconciled(ctx context.Context) error { - sc := scenarioCtx(ctx) + sc := Scenario(ctx) component := "operator-controller" - resourceName := sc.clusterExtensionName + resourceName := sc.ClusterExtensionName if resourceName == "" { return fmt.Errorf("no ClusterExtension found in scenario context") } @@ -159,8 +159,8 @@ func ClusterExtensionIsReconciled(ctx context.Context) error { return err } - leaderPod := sc.leaderPods[component] - waitFor(ctx, reconcileEndingCheck(ctx, namespaceForComponent(component), leaderPod, resourceName)) + leaderPod := sc.LeaderPods[component] + WaitFor(ctx, reconcileEndingCheck(ctx, NamespaceForComponent(component), leaderPod, resourceName)) return nil } @@ -168,7 +168,7 @@ func ClusterExtensionIsReconciled(ctx context.Context) error { // ClusterCatalog is serving and its lastUnpacked timestamp is after the leader pod's creation. func clusterCatalogUnpackedAfterPodCreation(ctx context.Context, namespace, resourceName, leaderPod string) func() bool { return func() bool { - catalogJSON, err := k8sClient(ctx, "get", "clustercatalog", resourceName, "-o", "json") + catalogJSON, err := K8sClient(ctx, "get", "clustercatalog", resourceName, "-o", "json") if err != nil { return false } @@ -177,11 +177,11 @@ func clusterCatalogUnpackedAfterPodCreation(ctx context.Context, namespace, reso return false } - serving := currentCondition(catalog.Generation, catalog.Status.Conditions, catalogdv1.TypeServing) + serving := CurrentCondition(catalog.Generation, catalog.Status.Conditions, catalogdv1.TypeServing) if serving == nil || serving.Status != metav1.ConditionTrue || serving.Reason != catalogdv1.ReasonAvailable { return false } - progressing := currentCondition(catalog.Generation, catalog.Status.Conditions, catalogdv1.TypeProgressing) + progressing := CurrentCondition(catalog.Generation, catalog.Status.Conditions, catalogdv1.TypeProgressing) if progressing == nil || progressing.Status != metav1.ConditionTrue || progressing.Reason != catalogdv1.ReasonSucceeded { return false } @@ -190,7 +190,7 @@ func clusterCatalogUnpackedAfterPodCreation(ctx context.Context, namespace, reso return false } - podJSON, err := k8sClient(ctx, "get", "pod", leaderPod, "-n", namespace, "-o", "json") + podJSON, err := K8sClient(ctx, "get", "pod", leaderPod, "-n", namespace, "-o", "json") if err != nil { return false } @@ -204,23 +204,23 @@ func clusterCatalogUnpackedAfterPodCreation(ctx context.Context, namespace, reso } // allResourcesAreReconciled discovers all cluster-scoped resources of the given type and verifies -// that the leader pod's logs contain a "reconcile ending" entry for each resource. +// that the leader pod's logs contain a "reconcile ending" entry for each Resource. func allResourcesAreReconciled(ctx context.Context, resourceType string) error { - sc := scenarioCtx(ctx) + sc := Scenario(ctx) component, ok := resourceTypeToComponent[resourceType] if !ok { return fmt.Errorf("unknown resource type: %s", resourceType) } - leaderPod, ok := sc.leaderPods[component] + leaderPod, ok := sc.LeaderPods[component] if !ok { return fmt.Errorf("leader pod not found for component %s; run '%s is ready to reconcile resources' first", component, component) } // Discover all resources pluralType := strings.ToLower(resourceType) + "s" - output, err := k8sClient(ctx, "get", pluralType, "-o", "jsonpath={.items[*].metadata.name}") + output, err := K8sClient(ctx, "get", pluralType, "-o", "jsonpath={.items[*].metadata.name}") if err != nil { return fmt.Errorf("failed to list %s resources: %w", resourceType, err) } @@ -229,9 +229,9 @@ func allResourcesAreReconciled(ctx context.Context, resourceType string) error { return fmt.Errorf("no %s resources found", resourceType) } - ns := namespaceForComponent(component) + ns := NamespaceForComponent(component) for _, name := range resourceNames { - waitFor(ctx, reconcileEndingCheck(ctx, ns, leaderPod, name)) + WaitFor(ctx, reconcileEndingCheck(ctx, ns, leaderPod, name)) } return nil @@ -239,8 +239,8 @@ func allResourcesAreReconciled(ctx context.Context, resourceType string) error { // ScenarioCatalogIsReconciled waits for a named per-scenario ClusterCatalog to be reconciled by checking leader pod logs. func ScenarioCatalogIsReconciled(ctx context.Context, catalogUserName string) error { - sc := scenarioCtx(ctx) - catalogName, ok := sc.catalogs[catalogUserName] + sc := Scenario(ctx) + catalogName, ok := sc.Catalogs[catalogUserName] if !ok { return fmt.Errorf("no catalog %q has been created for this scenario", catalogUserName) } @@ -250,34 +250,34 @@ func ScenarioCatalogIsReconciled(ctx context.Context, catalogUserName string) er return err } - leaderPod, ok := sc.leaderPods[component] + leaderPod, ok := sc.LeaderPods[component] if !ok { return fmt.Errorf("leader pod not found for component %s", component) } - ns := namespaceForComponent(component) - waitFor(ctx, reconcileEndingCheck(ctx, ns, leaderPod, catalogName)) + ns := NamespaceForComponent(component) + WaitFor(ctx, reconcileEndingCheck(ctx, ns, leaderPod, catalogName)) // Also verify that lastUnpacked is after the leader pod's creation. // This mitigates flakiness caused by https://github.com/operator-framework/operator-controller/issues/1626 - waitFor(ctx, clusterCatalogUnpackedAfterPodCreation(ctx, ns, catalogName, leaderPod)) + WaitFor(ctx, clusterCatalogUnpackedAfterPodCreation(ctx, ns, catalogName, leaderPod)) return nil } // ScenarioCatalogReportsCondition waits for a named per-scenario ClusterCatalog to have the specified condition. func ScenarioCatalogReportsCondition(ctx context.Context, catalogUserName, conditionType, conditionStatus, conditionReason string) error { - sc := scenarioCtx(ctx) - catalogName, ok := sc.catalogs[catalogUserName] + sc := Scenario(ctx) + catalogName, ok := sc.Catalogs[catalogUserName] if !ok { - if _, err := k8sClient(ctx, "get", "clustercatalog", catalogUserName); err != nil { + if _, err := K8sClient(ctx, "get", "clustercatalog", catalogUserName); err != nil { return fmt.Errorf("catalog %q was not created by this scenario and does not exist on the cluster", catalogUserName) } catalogName = catalogUserName } - err := waitForCondition(ctx, "clustercatalog", catalogName, conditionType, metav1.ConditionStatus(conditionStatus), &conditionReason, nil) + err := WaitForCondition(ctx, "clustercatalog", catalogName, conditionType, metav1.ConditionStatus(conditionStatus), &conditionReason, nil) if err == nil { - if rec := RecorderFromContext(ctx); rec != nil { - out, _ := k8sClient(ctx, "get", "clustercatalog", catalogName, + if rec := CommandRecorderFromContext(ctx); rec != nil { + out, _ := K8sClient(ctx, "get", "clustercatalog", catalogName, "-o", fmt.Sprintf("jsonpath={.status.conditions[?(@.type==\"%s\")]}", conditionType)) rec.RecordCustom( fmt.Sprintf("kubectl get clustercatalog %s -o jsonpath='{.status.conditions[?(@.type==\"%s\")]}' | jq .", From ce52d8edd61bf2849ac2fef60e3abd55cb1f4e22 Mon Sep 17 00:00:00 2001 From: Fabricio Aguiar Date: Tue, 10 Nov 2026 14:01:16 +0000 Subject: [PATCH 2/3] refactor(e2e): prepare independent component step extractions Extract port-forward and metrics helpers before moving catalogd steps. Make recording hooks explicit in platform and legacy suites, and document the incremental ownership boundary. Keep cross-component object lookups shared so component branches can build and land independently. Refs: OPRUN-4784 Signed-off-by: Fabricio Aguiar rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED --- test/e2e/README.md | 64 ++++++++++++------ test/e2e/features_test.go | 2 +- test/e2e/platform/suite_test.go | 2 +- test/internal/e2e/steps/demo_steps.go | 2 +- test/internal/e2e/steps/hooks.go | 7 +- test/internal/e2e/steps/portforward.go | 91 ++++++++++++++++++++++++++ test/internal/e2e/steps/steps.go | 44 ++++++------- test/internal/e2e/steps/tls_steps.go | 87 +----------------------- 8 files changed, 167 insertions(+), 132 deletions(-) create mode 100644 test/internal/e2e/steps/portforward.go diff --git a/test/e2e/README.md b/test/e2e/README.md index f5ee56682d..4bb2677a0a 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -19,6 +19,9 @@ non-technical stakeholders. ## Project Structure +Component step packages are introduced incrementally. The layout below shows +their intended ownership after all component extractions land. + ``` test/ ├── e2e/ @@ -33,8 +36,12 @@ test/ └── internal/ ├── e2e/ │ ├── runner.go # Godog options, scenario selection, summary wiring - │ ├── steps/ # All existing step definitions and hooks + │ ├── steps/ # Shared world, hooks, catalog, resource, RBAC, and condition helpers │ │ └── testdata/ # RBAC and resource templates + │ ├── operator-controller/steps/ # ClusterExtension lifecycle and integration assertions + │ ├── catalogd/steps/ # HA/proxy/TLS steps and restoration hooks + │ ├── object-controller/steps/ # ClusterObjectSet/Deployment assertions + │ ├── platform/steps/ # Demo steps and recording │ └── support-bundle.yaml # Shared diagnostics └── summary/ # Summary generation and embedded templates ``` @@ -43,7 +50,7 @@ test/ ### 1. Test Runner (`operator-controller/suite_test.go`) -The component entry point registers the full shared step set and hooks, then calls +The component entry point registers shared steps, hooks, and its component steps, then calls `e2e.Run` from `test/internal/e2e` with its feature directory. The shared runner configures Godog options, scenario selection, and summary generation. @@ -53,10 +60,22 @@ controller unit tests live under `internal/testutil`, which is accessible throug the repository. Features follow the ownership in the [per-component suite design](../../docs/designs/e2e-per-component.md). -Each component has a thin entry point that still registers the full shared step set; -step ownership is separated in the next stage. The newer `cos_*`, `cod_*`, and -`group` features belong to object-controller. Feature contents and step behavior -are unchanged, including ClusterExtension integration assertions in object features. +The newer `cos_*`, `cod_*`, and `group` features belong to object-controller. +Suites register additional component steps when existing features contain integration +assertions: operator-controller uses object assertions, object-controller retains +ClusterExtension integration coverage, and catalogd proxy tests use ClusterExtension +steps. Platform registers ClusterExtension steps for demos. Feature contents, +scenario names, tags, and assertions are unchanged. + +`test/internal/e2e/steps` owns `World` and the `Scenario(ctx)` accessor, generic resource and +condition helpers, catalog/registry prerequisites, RBAC templates, and cleanup. +`ListClusterObjectSetsInGroup` and `ResolveObjectRef` remain shared because +both ClusterExtension and ClusterObjectSet assertions consume them. +Catalogd owns deployment restoration and proxy state. Platform owns recording; +shared helpers observe commands through an optional `CommandRecorder` interface. +Pass extra lifecycle registration to `sharedsteps.RegisterHooks` to keep suite hooks +between shared scenario setup and cleanup. The upgrade helpers remain shared because +catalogd HA scenarios also consume component readiness and catalog reconciliation steps. ### 2. Feature Files (`operator-controller/features/*.feature`) @@ -112,7 +131,7 @@ Feature: Install ClusterExtension And ClusterExtension is available ``` -### 3. Step Definitions (`test/internal/e2e/steps/steps.go`) +### 3. Step Definitions (`test/internal/e2e//steps/steps.go`) Go functions that implement the steps defined in feature files. Each step is registered with a regex pattern that matches the Gherkin text. @@ -121,9 +140,8 @@ matches the Gherkin text. ```go func RegisterSteps(sc *godog.ScenarioContext) { -sc.Step(`^OLM is available$`, OLMisAvailable) sc.Step(`^bundle "([^"]+)" is installed in version "([^"]+)"$`, BundleInstalled) -sc.Step(`^ClusterExtension is applied$`, ResourceIsApplied) +sc.Step(`^ClusterExtension is applied$`, shared.ResourceIsApplied) // ... more steps } ``` @@ -132,7 +150,7 @@ sc.Step(`^ClusterExtension is applied$`, ResourceIsApplied) Status assertions must read metadata and status from the same API response and require the asserted condition's `observedGeneration` to equal `metadata.generation`. -Use `waitForCondition` for condition checks. Bundle metadata uses the `Installed` +Use `shared.WaitForCondition` for condition checks. Bundle metadata uses the `Installed` condition, and COS phase digests and object counts use the `Ready` condition to verify that the status reflects the current spec. Deployments instead expose `status.observedGeneration` at the resource level. @@ -146,9 +164,9 @@ That step accepts any current-generation `Progressing` status or reason, whereas ```go func BundleInstalled(ctx context.Context, name, version string) error { - sc := scenarioCtx(ctx) - waitFor(ctx, func() bool { - v, err := k8sClient(ctx, "get", "clusterextension", sc.clusterExtensionName, "-o", "json") + sc := shared.Scenario(ctx) + shared.WaitFor(ctx, func() bool { + v, err := shared.K8sClient(ctx, "get", "clusterextension", sc.ClusterExtensionName, "-o", "json") if err != nil { return false } @@ -156,8 +174,8 @@ func BundleInstalled(ctx context.Context, name, version string) error { if err := json.Unmarshal([]byte(v), &ext); err != nil { return false } - installed := currentCondition(ext.Generation, ext.Status.Conditions, ocv1.TypeInstalled) - return conditionMatches(installed, metav1.ConditionTrue, nil, nil) && + installed := shared.CurrentCondition(ext.Generation, ext.Status.Conditions, ocv1.TypeInstalled) + return shared.ConditionMatches(installed, metav1.ConditionTrue, nil, nil) && ext.Status.Install.Bundle.Name == name && ext.Status.Install.Bundle.Version == version }) return nil @@ -199,7 +217,7 @@ Feature: Your Feature Name ### 2. Implement Step Definitions -Add step implementations in `test/internal/e2e/steps/steps.go`: +Add component assertions in `test/internal/e2e//steps/`; add reusable prerequisites and helpers in `test/internal/e2e/steps/`: ```go func RegisterSteps(sc *godog.ScenarioContext) { @@ -208,7 +226,7 @@ func RegisterSteps(sc *godog.ScenarioContext) { } func YourStepFunction(ctx context.Context, param string) error { - sc := scenarioCtx(ctx) + sc := shared.Scenario(ctx) // Implementation return nil } @@ -230,7 +248,7 @@ Use these variables in YAML templates: - `${NAME}`: Scenario-specific ClusterExtension name (e.g., `ce-123`) - `${COS_NAME}`: Scenario-specific ClusterObjectSet name (e.g., `cos-123`; for applying ClusterObjectSets directly) - `${TEST_NAMESPACE}`: Scenario-specific namespace (e.g., `ns-123`) -- `${SCENARIO_ID}`: Unique scenario identifier used for resource name isolation +- `${SCENARIO_ID}`: Feature-scoped scenario identifier used for resource isolation across concurrent Go suites - `${PACKAGE:}`: Parameterized package name (e.g., `${PACKAGE:test}` expands to `test-`) - `${CATALOG:}`: Catalog resource name (e.g., `${CATALOG:test}` expands to `test-catalog-`) @@ -427,8 +445,12 @@ go test -count=1 ./test/e2e/operator-controller/... -args --log.debug --k8s.cli= Each scenario runs in its own namespace with unique resource names, ensuring complete isolation: -- Namespace: `ns-{scenario-id}` -- ClusterExtension: `ce-{scenario-id}` +- Namespace: `ns-{feature-hash}-{godog-id}` +- ClusterExtension: `ce-{feature-hash}-{godog-id}` + +Godog restarts numeric IDs in each process. The feature hash prevents collisions +between concurrent component suites and stays stable when a feature moves. Feature +and scenario annotations retain their existing values. ### 2. Test-Identifying Annotations @@ -476,7 +498,7 @@ When ClusterExtension is applied All asynchronous operations use `waitFor` with consistent timeout (300s) and tick (1s): ```go -waitFor(ctx, func() bool { +shared.WaitFor(ctx, func() bool { // Check condition return conditionMet }) diff --git a/test/e2e/features_test.go b/test/e2e/features_test.go index 4be0828b24..539cd36df0 100644 --- a/test/e2e/features_test.go +++ b/test/e2e/features_test.go @@ -42,6 +42,6 @@ func TestMain(m *testing.M) { }, func(sc *godog.ScenarioContext) { steps.RegisterSteps(sc) steps.RegisterDemoSteps(sc) - steps.RegisterHooks(sc) + steps.RegisterHooks(sc, steps.RegisterAsciiCastHooks) })) } diff --git a/test/e2e/platform/suite_test.go b/test/e2e/platform/suite_test.go index 721ee038df..2f38fde827 100644 --- a/test/e2e/platform/suite_test.go +++ b/test/e2e/platform/suite_test.go @@ -16,6 +16,6 @@ func TestMain(m *testing.M) { }, func(sc *godog.ScenarioContext) { steps.RegisterSteps(sc) steps.RegisterDemoSteps(sc) - steps.RegisterHooks(sc) + steps.RegisterHooks(sc, steps.RegisterAsciiCastHooks) })) } diff --git a/test/internal/e2e/steps/demo_steps.go b/test/internal/e2e/steps/demo_steps.go index eebe4f7850..088e43fb86 100644 --- a/test/internal/e2e/steps/demo_steps.go +++ b/test/internal/e2e/steps/demo_steps.go @@ -104,7 +104,7 @@ func ensureCatalogPortForward(ctx context.Context) (string, error) { port = 8443 } - addr, cleanup, err := portForward(ctx, ns, target, port) + addr, cleanup, err := PortForward(ctx, ns, target, port) if err != nil { return "", fmt.Errorf("failed to start catalog port-forward to %s: %w", target, err) } diff --git a/test/internal/e2e/steps/hooks.go b/test/internal/e2e/steps/hooks.go index 4ba07f5ced..c23f31dbdb 100644 --- a/test/internal/e2e/steps/hooks.go +++ b/test/internal/e2e/steps/hooks.go @@ -110,10 +110,13 @@ func init() { flagSet.BoolVar(&devMode, "log.debug", false, "print debug log level") } -func RegisterHooks(sc *godog.ScenarioContext) { +// RegisterHooks installs shared lifecycle hooks, with optional suite hooks between setup and cleanup. +func RegisterHooks(sc *godog.ScenarioContext, hooks ...func(*godog.ScenarioContext)) { sc.Before(CheckFeatureTags) sc.Before(CreateScenarioContext) - RegisterAsciiCastHooks(sc) + for _, register := range hooks { + register(sc) + } sc.After(ScenarioCleanup) } diff --git a/test/internal/e2e/steps/portforward.go b/test/internal/e2e/steps/portforward.go new file mode 100644 index 0000000000..3954045231 --- /dev/null +++ b/test/internal/e2e/steps/portforward.go @@ -0,0 +1,91 @@ +package steps + +import ( + "context" + "encoding/json" + "fmt" + "net" + "os" + "os/exec" + "strings" + + corev1 "k8s.io/api/core/v1" +) + +func randomAvailablePort() (int, error) { + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return 0, err + } + defer l.Close() + return l.Addr().(*net.TCPAddr).Port, nil +} + +// portForward starts a kubectl port-forward to target (e.g. "service/foo" or "pod/bar") +// in the given Namespace, mapping a random local port to remotePort. It returns the +// local address and a cleanup function. The caller is responsible for calling cleanup. +func PortForward(ctx context.Context, ns, target string, remotePort int32) (string, func(), error) { + localPort, err := randomAvailablePort() + if err != nil { + return "", nil, fmt.Errorf("failed to find a free local port: %w", err) + } + + pfCmd := exec.Command(K8sCLI, "port-forward", "-n", ns, //nolint:gosec + target, fmt.Sprintf("%d:%d", localPort, remotePort)) + pfCmd.Env = append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", KubeconfigPath)) + if err := pfCmd.Start(); err != nil { + return "", nil, fmt.Errorf("failed to start port-forward to %s: %w", target, err) + } + + cleanup := func() { + if p := pfCmd.Process; p != nil { + _ = p.Kill() + _ = pfCmd.Wait() + } + } + + if rec := CommandRecorderFromContext(ctx); rec != nil { + rec.RecordCustom( + fmt.Sprintf("%s port-forward -n %s %s %d:%d &", K8sCLI, ns, target, localPort, remotePort), + "", "", + ) + } + + addr := fmt.Sprintf("127.0.0.1:%d", localPort) + return addr, cleanup, nil +} + +// getMetricsService returns the full Service object for the named component's metrics service. +// The Namespace is available via svc.Namespace. +func GetMetricsService(component string) (*corev1.Service, error) { + serviceName := fmt.Sprintf("%s-service", component) + serviceNs, err := K8sClient(context.Background(), "get", "service", "-A", "-o", + fmt.Sprintf(`jsonpath={.items[?(@.metadata.name=="%s")].metadata.namespace}`, serviceName)) + if err != nil { + return nil, fmt.Errorf("failed to find namespace for service %s: %w", serviceName, err) + } + serviceNs = strings.TrimSpace(serviceNs) + if serviceNs == "" { + return nil, fmt.Errorf("service %s not found in any namespace", serviceName) + } + + raw, err := K8sClient(context.Background(), "get", "service", "-n", serviceNs, serviceName, "-o", "json") + if err != nil { + return nil, fmt.Errorf("failed to get service %s: %w", serviceName, err) + } + var svc corev1.Service + if err := json.Unmarshal([]byte(raw), &svc); err != nil { + return nil, fmt.Errorf("failed to unmarshal service %s: %w", serviceName, err) + } + return &svc, nil +} + +// metricsPort returns the port number of the port named "metrics" on the given service. +func MetricsPort(svc *corev1.Service) (int32, error) { + for _, p := range svc.Spec.Ports { + if p.Name == "metrics" { + return p.Port, nil + } + } + return 0, fmt.Errorf("no port named 'metrics' found on service %s", svc.Name) +} diff --git a/test/internal/e2e/steps/steps.go b/test/internal/e2e/steps/steps.go index 544289e94b..3ad354e235 100644 --- a/test/internal/e2e/steps/steps.go +++ b/test/internal/e2e/steps/steps.go @@ -1057,7 +1057,7 @@ func ClusterObjectDeploymentConsistentlyReportsCondition(ctx context.Context, na if err != nil { return fmt.Errorf("invalid duration %q: %w", durationStr, err) } - return consistentlyCondition(ctx, "clusterobjectdeployment", SubstituteScenarioVars(name, Scenario(ctx)), conditionType, metav1.ConditionStatus(conditionStatus), conditionReason, d) + return ConsistentlyCondition(ctx, "clusterobjectdeployment", SubstituteScenarioVars(name, Scenario(ctx)), conditionType, metav1.ConditionStatus(conditionStatus), conditionReason, d) } // consistently invokes check every tick until the expected state has held for the full @@ -1132,11 +1132,11 @@ func conditionCheck(resourceType, resourceName, conditionType string, conditionS } } -// consistentlyCondition fails the test if, at any point during duration, the named +// ConsistentlyCondition fails the test if, at any point during duration, the named // Resource's condition deviates from the expected type/status/reason or disappears. // Transient API read failures are tolerated only briefly; see consistently and // conditionCheck for the exact semantics. -func consistentlyCondition(ctx context.Context, resourceType, resourceName, conditionType string, conditionStatus metav1.ConditionStatus, conditionReason string, duration time.Duration) error { +func ConsistentlyCondition(ctx context.Context, resourceType, resourceName, conditionType string, conditionStatus metav1.ConditionStatus, conditionReason string, duration time.Duration) error { return consistently(ctx, duration, conditionCheck(resourceType, resourceName, conditionType, conditionStatus, conditionReason)) } @@ -1467,7 +1467,7 @@ func ClusterObjectSetReferredSecretsContainLabels(ctx context.Context, revisionN sc := Scenario(ctx) revisionName = SubstituteScenarioVars(strings.TrimSpace(revisionName), sc) - expected, err := parseKeyValueTable(table, sc) + expected, err := ParseKeyValueTable(table, sc) if err != nil { return fmt.Errorf("invalid labels table: %w", err) } @@ -1478,7 +1478,7 @@ func ClusterObjectSetReferredSecretsContainLabels(ctx context.Context, revisionN return false } for _, s := range secrets { - if _, _, ok := matchLabels(s.Labels, expected); !ok { + if _, _, ok := MatchLabels(s.Labels, expected); !ok { return false } } @@ -1974,11 +1974,11 @@ func httpGet(url string, token string) (*http.Response, error) { // to return a successful response. Stores the response body per pod in the scenario context. Polls with Timeout. func SendMetricsRequest(ctx context.Context, serviceAccount string, endpoint string, controllerName string) error { sc := Scenario(ctx) - svc, err := getMetricsService(controllerName) + svc, err := GetMetricsService(controllerName) if err != nil { return err } - mPort, err := metricsPort(svc) + mPort, err := MetricsPort(svc) if err != nil { return err } @@ -2004,7 +2004,7 @@ func SendMetricsRequest(ctx context.Context, serviceAccount string, endpoint str sc.MetricsResponse = make(map[string]string) for _, p := range pods { if err := func() error { - addr, cleanup, err := portForward(ctx, p.Namespace, fmt.Sprintf("pod/%s", p.Name), mPort) + addr, cleanup, err := PortForward(ctx, p.Namespace, fmt.Sprintf("pod/%s", p.Name), mPort) if err != nil { return err } @@ -2699,7 +2699,7 @@ func AnnotationsAreAdded(ctx context.Context, resourceName string, table *godog. sc := Scenario(ctx) resourceName = SubstituteScenarioVars(resourceName, sc) - pairs, err := parseKeyValueTable(table, sc) + pairs, err := ParseKeyValueTable(table, sc) if err != nil { return fmt.Errorf("invalid annotations table: %w", err) } @@ -2727,7 +2727,7 @@ func LabelsAreAdded(ctx context.Context, resourceName string, table *godog.Table sc := Scenario(ctx) resourceName = SubstituteScenarioVars(resourceName, sc) - pairs, err := parseKeyValueTable(table, sc) + pairs, err := ParseKeyValueTable(table, sc) if err != nil { return fmt.Errorf("invalid labels table: %w", err) } @@ -2755,7 +2755,7 @@ func ResourceHasAnnotations(ctx context.Context, resourceName string, table *god sc := Scenario(ctx) resourceName = SubstituteScenarioVars(resourceName, sc) - expected, err := parseKeyValueTable(table, sc) + expected, err := ParseKeyValueTable(table, sc) if err != nil { return fmt.Errorf("invalid annotations table: %w", err) } @@ -2786,9 +2786,9 @@ func ResourceHasAnnotations(ctx context.Context, resourceName string, table *god return nil } -// matchLabels checks that actual labels contain all expected key-value pairs. +// MatchLabels checks that actual labels contain all expected key-value pairs. // Returns the first mismatched key, the actual value, and false if a mismatch is found. -func matchLabels(actual, expected map[string]string) (string, string, bool) { +func MatchLabels(actual, expected map[string]string) (string, string, bool) { for k, v := range expected { if a, found := actual[k]; !found || a != v { return k, a, false @@ -2804,7 +2804,7 @@ func ResourceHasLabels(ctx context.Context, resourceName string, table *godog.Ta sc := Scenario(ctx) resourceName = SubstituteScenarioVars(resourceName, sc) - expected, err := parseKeyValueTable(table, sc) + expected, err := ParseKeyValueTable(table, sc) if err != nil { return fmt.Errorf("invalid labels table: %w", err) } @@ -2823,7 +2823,7 @@ func ResourceHasLabels(ctx context.Context, resourceName string, table *godog.Ta if err := json.Unmarshal([]byte(out), &obj); err != nil { return false } - if key, got, ok := matchLabels(obj.GetLabels(), expected); !ok { + if key, got, ok := MatchLabels(obj.GetLabels(), expected); !ok { logger.V(1).Info("Label not yet present or value mismatch", "resource", resourceName, "key", key, "expected", expected[key], "actual", got) return false } @@ -2837,7 +2837,7 @@ func NamespaceHasLabels(ctx context.Context, nsName string, table *godog.Table) sc := Scenario(ctx) nsName = SubstituteScenarioVars(nsName, sc) - expected, err := parseKeyValueTable(table, sc) + expected, err := ParseKeyValueTable(table, sc) if err != nil { return fmt.Errorf("invalid labels table: %w", err) } @@ -2851,7 +2851,7 @@ func NamespaceHasLabels(ctx context.Context, nsName string, table *godog.Table) if err := json.Unmarshal([]byte(out), &obj); err != nil { return false } - if key, got, ok := matchLabels(obj.GetLabels(), expected); !ok { + if key, got, ok := MatchLabels(obj.GetLabels(), expected); !ok { logger.V(1).Info("Namespace label not yet present or value mismatch", "namespace", nsName, "key", key, "expected", expected[key], "actual", got) return false } @@ -2929,9 +2929,9 @@ func NamespaceHasNoOwnerReferences(ctx context.Context, nsName string) error { return nil } -// nestedString traverses a nested map[string]interface{} by the given keys +// NestedString traverses a nested map[string]interface{} by the given keys // and returns the leaf value as a string. -func nestedString(obj map[string]interface{}, keys ...string) (string, bool) { +func NestedString(obj map[string]interface{}, keys ...string) (string, bool) { if len(keys) == 0 { return "", false } @@ -2947,10 +2947,10 @@ func nestedString(obj map[string]interface{}, keys ...string) (string, bool) { return v, ok } -// parseKeyValueTable extracts key-value pairs from a godog data table. +// ParseKeyValueTable extracts key-value pairs from a godog data table. // The table must have a header row with "key" and "value" columns. // If sc is non-nil, variable substitution is applied to both keys and values. -func parseKeyValueTable(table *godog.Table, sc *World) (map[string]string, error) { +func ParseKeyValueTable(table *godog.Table, sc *World) (map[string]string, error) { if len(table.Rows) < 2 { return nil, fmt.Errorf("table must have a header row and at least one data row") } @@ -3066,7 +3066,7 @@ func TriggerClusterExtensionReconciliation(ctx context.Context) error { return fmt.Errorf("failed to unmarshal ClusterExtension %s JSON: %w", sc.ClusterExtensionName, err) } - currentEnforcement, _ := nestedString(obj, "spec", "install", "preflight", "crdUpgradeSafety", "enforcement") + currentEnforcement, _ := NestedString(obj, "spec", "install", "preflight", "crdUpgradeSafety", "enforcement") newEnforcement := "None" if currentEnforcement == "None" { diff --git a/test/internal/e2e/steps/tls_steps.go b/test/internal/e2e/steps/tls_steps.go index 37fc3647c0..6cd56f2aab 100644 --- a/test/internal/e2e/steps/tls_steps.go +++ b/test/internal/e2e/steps/tls_steps.go @@ -6,13 +6,10 @@ import ( "encoding/json" "fmt" "net" - "os" - "os/exec" "strings" "time" appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" ) // tlsCipherSuiteByName is a Name→ID map that covers TLS 1.3 cipher suites @@ -56,98 +53,20 @@ var curveIDByName = map[string]tls.CurveID{ "secp521r1": tls.CurveP521, } -// getMetricsService returns the full Service object for the named component's metrics service. -// The Namespace is available via svc.Namespace. -func getMetricsService(component string) (*corev1.Service, error) { - serviceName := fmt.Sprintf("%s-service", component) - serviceNs, err := K8sClient(context.Background(), "get", "service", "-A", "-o", - fmt.Sprintf(`jsonpath={.items[?(@.metadata.name=="%s")].metadata.namespace}`, serviceName)) - if err != nil { - return nil, fmt.Errorf("failed to find namespace for service %s: %w", serviceName, err) - } - serviceNs = strings.TrimSpace(serviceNs) - if serviceNs == "" { - return nil, fmt.Errorf("service %s not found in any namespace", serviceName) - } - - raw, err := K8sClient(context.Background(), "get", "service", "-n", serviceNs, serviceName, "-o", "json") - if err != nil { - return nil, fmt.Errorf("failed to get service %s: %w", serviceName, err) - } - var svc corev1.Service - if err := json.Unmarshal([]byte(raw), &svc); err != nil { - return nil, fmt.Errorf("failed to unmarshal service %s: %w", serviceName, err) - } - return &svc, nil -} - -// metricsPort returns the port number of the port named "metrics" on the given service. -func metricsPort(svc *corev1.Service) (int32, error) { - for _, p := range svc.Spec.Ports { - if p.Name == "metrics" { - return p.Port, nil - } - } - return 0, fmt.Errorf("no port named 'metrics' found on service %s", svc.Name) -} - -func randomAvailablePort() (int, error) { - l, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - return 0, err - } - defer l.Close() - return l.Addr().(*net.TCPAddr).Port, nil -} - -// portForward starts a kubectl port-forward to target (e.g. "service/foo" or "pod/bar") -// in the given Namespace, mapping a random local port to remotePort. It returns the -// local address and a cleanup function. The caller is responsible for calling cleanup. -func portForward(ctx context.Context, ns, target string, remotePort int32) (string, func(), error) { - localPort, err := randomAvailablePort() - if err != nil { - return "", nil, fmt.Errorf("failed to find a free local port: %w", err) - } - - pfCmd := exec.Command(K8sCLI, "port-forward", "-n", ns, //nolint:gosec - target, fmt.Sprintf("%d:%d", localPort, remotePort)) - pfCmd.Env = append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", KubeconfigPath)) - if err := pfCmd.Start(); err != nil { - return "", nil, fmt.Errorf("failed to start port-forward to %s: %w", target, err) - } - - cleanup := func() { - if p := pfCmd.Process; p != nil { - _ = p.Kill() - _ = pfCmd.Wait() - } - } - - if rec := CommandRecorderFromContext(ctx); rec != nil { - rec.RecordCustom( - fmt.Sprintf("%s port-forward -n %s %s %d:%d &", K8sCLI, ns, target, localPort, remotePort), - "", "", - ) - } - - addr := fmt.Sprintf("127.0.0.1:%d", localPort) - return addr, cleanup, nil -} - // withMetricsPortForward starts a kubectl port-forward to the component's metrics service, // waits until a basic TLS connection succeeds (confirming the port-forward is ready), // then calls fn with the local address. The port-forward is torn down when fn returns. func withMetricsPortForward(ctx context.Context, component string, fn func(addr string) error) error { - svc, err := getMetricsService(component) + svc, err := GetMetricsService(component) if err != nil { return err } - port, err := metricsPort(svc) + port, err := MetricsPort(svc) if err != nil { return err } - addr, cleanup, err := portForward(ctx, svc.Namespace, fmt.Sprintf("service/%s", svc.Name), port) + addr, cleanup, err := PortForward(ctx, svc.Namespace, fmt.Sprintf("service/%s", svc.Name), port) if err != nil { return err } From e3e5184d4ca13380aa3633fcaca66333c2a6b815 Mon Sep 17 00:00:00 2001 From: Fabricio Aguiar Date: Tue, 10 Nov 2026 14:01:25 +0000 Subject: [PATCH 3/3] refactor(e2e): move catalogd steps and cleanup to its suite Move HA, proxy, and TLS assertions with catalog-specific state and restoration hooks. Consume shared preparation without depending on platform, operator-controller, or object-controller step extractions. Refs: OPRUN-4784 Signed-off-by: Fabricio Aguiar rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED --- test/e2e/catalogd/suite_test.go | 3 + test/e2e/features_test.go | 3 + .../e2e/{ => catalogd}/steps/ha_steps.go | 22 ++--- test/internal/e2e/catalogd/steps/hooks.go | 85 +++++++++++++++++++ .../e2e/{ => catalogd}/steps/proxy_steps.go | 34 ++++---- .../e2e/{ => catalogd}/steps/tls_steps.go | 28 +++--- test/internal/e2e/steps/hooks.go | 42 +-------- test/internal/e2e/steps/steps.go | 17 ---- 8 files changed, 135 insertions(+), 99 deletions(-) rename test/internal/e2e/{ => catalogd}/steps/ha_steps.go (69%) create mode 100644 test/internal/e2e/catalogd/steps/hooks.go rename test/internal/e2e/{ => catalogd}/steps/proxy_steps.go (90%) rename test/internal/e2e/{ => catalogd}/steps/tls_steps.go (94%) diff --git a/test/e2e/catalogd/suite_test.go b/test/e2e/catalogd/suite_test.go index 721ee038df..b0ac4b9694 100644 --- a/test/e2e/catalogd/suite_test.go +++ b/test/e2e/catalogd/suite_test.go @@ -7,6 +7,7 @@ import ( "github.com/cucumber/godog" "github.com/operator-framework/operator-controller/test/internal/e2e" + catalogsteps "github.com/operator-framework/operator-controller/test/internal/e2e/catalogd/steps" "github.com/operator-framework/operator-controller/test/internal/e2e/steps" ) @@ -15,7 +16,9 @@ func TestMain(m *testing.M) { tc.BeforeSuite(steps.BeforeSuite) }, func(sc *godog.ScenarioContext) { steps.RegisterSteps(sc) + catalogsteps.RegisterSteps(sc) steps.RegisterDemoSteps(sc) + catalogsteps.RegisterHooks(sc) steps.RegisterHooks(sc) })) } diff --git a/test/e2e/features_test.go b/test/e2e/features_test.go index 539cd36df0..d52553e64d 100644 --- a/test/e2e/features_test.go +++ b/test/e2e/features_test.go @@ -14,6 +14,7 @@ import ( "github.com/cucumber/godog" "github.com/operator-framework/operator-controller/test/internal/e2e" + catalogsteps "github.com/operator-framework/operator-controller/test/internal/e2e/catalogd/steps" "github.com/operator-framework/operator-controller/test/internal/e2e/steps" ) @@ -41,7 +42,9 @@ func TestMain(m *testing.M) { tc.BeforeSuite(steps.BeforeSuite) }, func(sc *godog.ScenarioContext) { steps.RegisterSteps(sc) + catalogsteps.RegisterSteps(sc) steps.RegisterDemoSteps(sc) + catalogsteps.RegisterHooks(sc) steps.RegisterHooks(sc, steps.RegisterAsciiCastHooks) })) } diff --git a/test/internal/e2e/steps/ha_steps.go b/test/internal/e2e/catalogd/steps/ha_steps.go similarity index 69% rename from test/internal/e2e/steps/ha_steps.go rename to test/internal/e2e/catalogd/steps/ha_steps.go index 8e003a0e58..37336bdaa4 100644 --- a/test/internal/e2e/steps/ha_steps.go +++ b/test/internal/e2e/catalogd/steps/ha_steps.go @@ -5,15 +5,9 @@ import ( "fmt" "strings" - "k8s.io/component-base/featuregate" + shared "github.com/operator-framework/operator-controller/test/internal/e2e/steps" ) -// catalogdHAFeature gates scenarios that require a multi-node cluster. -// It is set to true in BeforeSuite when the cluster has at least 2 nodes, -// which is the case for the experimental e2e suite (Kind-config-2node.yaml) -// but not the standard suite. -const catalogdHAFeature featuregate.Feature = "CatalogdHA" - // CatalogdLeaderPodIsForceDeleted force-deletes the catalogd leader pod to simulate leader loss. // The pod is identified from sc.LeaderPods["catalogd"] (populated by a prior // "catalogd is ready to reconcile resources" step). Force-deletion is equivalent to @@ -24,14 +18,14 @@ const catalogdHAFeature featuregate.Feature = "CatalogdHA" // experimental 2-node cluster are control-plane nodes that run etcd — stopping either // would break etcd quorum and make the API server unreachable for the rest of the test. func CatalogdLeaderPodIsForceDeleted(ctx context.Context) error { - sc := Scenario(ctx) + sc := shared.Scenario(ctx) leaderPod := sc.LeaderPods["catalogd"] if leaderPod == "" { return fmt.Errorf("catalogd leader pod not found in scenario context; run 'catalogd is ready to reconcile resources' first") } - logger.Info("Force-deleting catalogd leader pod", "pod", leaderPod) - if _, err := K8sClient(ctx, "delete", "pod", leaderPod, "-n", NamespaceForComponent("catalogd"), + shared.Log().Info("Force-deleting catalogd leader pod", "pod", leaderPod) + if _, err := shared.K8sClient(ctx, "delete", "pod", leaderPod, "-n", shared.NamespaceForComponent("catalogd"), "--force", "--grace-period=0"); err != nil { return fmt.Errorf("failed to force-delete catalogd leader pod %q: %w", leaderPod, err) } @@ -42,11 +36,11 @@ func CatalogdLeaderPodIsForceDeleted(ctx context.Context) error { // identity changes to a pod other than the deleted leader. It updates // sc.LeaderPods["catalogd"] with the new leader pod Name. func NewCatalogdLeaderIsElected(ctx context.Context) error { - sc := Scenario(ctx) + sc := shared.Scenario(ctx) oldLeader := sc.LeaderPods["catalogd"] - WaitFor(ctx, func() bool { - holder, err := K8sClient(ctx, "get", "lease", leaseNames["catalogd"], "-n", NamespaceForComponent("catalogd"), + shared.WaitFor(ctx, func() bool { + holder, err := shared.K8sClient(ctx, "get", "lease", "catalogd-operator-lock", "-n", shared.NamespaceForComponent("catalogd"), "-o", "jsonpath={.spec.holderIdentity}") if err != nil || holder == "" { return false @@ -56,7 +50,7 @@ func NewCatalogdLeaderIsElected(ctx context.Context) error { return false } sc.LeaderPods["catalogd"] = newPod - logger.Info("New catalogd leader elected", "pod", newPod) + shared.Log().Info("New catalogd leader elected", "pod", newPod) return true }) return nil diff --git a/test/internal/e2e/catalogd/steps/hooks.go b/test/internal/e2e/catalogd/steps/hooks.go new file mode 100644 index 0000000000..d05e864e41 --- /dev/null +++ b/test/internal/e2e/catalogd/steps/hooks.go @@ -0,0 +1,85 @@ +package steps + +import ( + "context" + "fmt" + + "github.com/cucumber/godog" + + shared "github.com/operator-framework/operator-controller/test/internal/e2e/steps" +) + +// deploymentRestore records the original state of a deployment so it can be +// rolled back after a test that modifies deployment configuration. +type deploymentRestore struct { + Name string // deployment name + Namespace string + containerName string // container to patch (for env var restores) + patchedArgs bool // true when container args were modified (for TLS profile patches) + originalArgs []string // original container args; may be nil if args were unset + originalEnv []string // original env vars as "NAME=VALUE" (for proxy patches) +} + +type catalogContextKey struct{} +type catalogContext struct { + proxy *recordingProxy + deploymentRestores []deploymentRestore +} + +func catalogScenario(ctx context.Context) *catalogContext { + return ctx.Value(catalogContextKey{}).(*catalogContext) +} + +// RegisterHooks installs catalog-specific state and unconditional deployment restoration. +// Register it before shared hooks so modified deployments are restored before resource cleanup. +func RegisterHooks(sc *godog.ScenarioContext) { + sc.Before(func(ctx context.Context, _ *godog.Scenario) (context.Context, error) { + return context.WithValue(ctx, catalogContextKey{}, &catalogContext{}), nil + }) + sc.After(func(ctx context.Context, _ *godog.Scenario, err error) (context.Context, error) { + sc := catalogScenario(ctx) + // Stop the in-process recording proxy if one was started. + if sc.proxy != nil { + sc.proxy.stop() + } + // Restore any deployments that were modified during the scenario. Runs + // unconditionally (even on failure) to prevent a misconfigured deployment + // from bleeding into subsequent scenarios. Restored in LIFO order so that + // multiple patches to the same deployment unwind to the true original. + for i := len(sc.deploymentRestores) - 1; i >= 0; i-- { + dr := sc.deploymentRestores[i] + if dr.patchedArgs { + if err2 := patchDeploymentArgs(dr.Namespace, dr.Name, dr.originalArgs); err2 != nil { + shared.Log().Info("Error restoring deployment args", "name", dr.Name, "error", err2) + } else if _, err2 := shared.K8sClient(ctx, "rollout", "status", "-n", dr.Namespace, + fmt.Sprintf("deployment/%s", dr.Name), "--timeout=2m"); err2 != nil { + shared.Log().Info("Timeout waiting for deployment rollout after restore", "name", dr.Name) + } + } + if err2 := restoreDeployment(dr); err2 != nil { + shared.Log().Info("Error restoring deployment env", "deployment", dr.Name, "namespace", dr.Namespace, "error", err2) + } + } + + return ctx, err + }) +} + +// RegisterSteps registers catalogd HA, proxy, and TLS assertions. +func RegisterSteps(sc *godog.ScenarioContext) { + sc.Step(`^(?i)the "([^"]+)" deployment is configured with custom TLS minimum version "([^"]+)"$`, ConfigureDeploymentWithCustomTLSVersion) + sc.Step(`^(?i)the "([^"]+)" deployment is configured with custom TLS version "([^"]+)", ciphers "([^"]+)", and curves "([^"]+)"$`, ConfigureDeploymentWithCustomTLSFull) + sc.Step(`^(?i)the "([^"]+)" metrics endpoint accepts a TLS 1\.3 connection$`, MetricsEndpointAcceptsTLS13) + sc.Step(`^(?i)the "([^"]+)" metrics endpoint rejects a TLS 1\.2 connection$`, MetricsEndpointRejectsTLS12) + sc.Step(`^(?i)the "([^"]+)" metrics endpoint negotiates cipher "([^"]+)" over TLS 1\.2$`, MetricsEndpointNegotiatesTLS12Cipher) + sc.Step(`^(?i)the "([^"]+)" metrics endpoint rejects a TLS 1\.2 connection offering only cipher "([^"]+)"$`, MetricsEndpointRejectsTLS12ConnectionWithCipher) + sc.Step(`^(?i)the "([^"]+)" metrics endpoint accepts a TLS 1\.2 connection with cipher "([^"]+)" and curve "([^"]+)"$`, MetricsEndpointAcceptsTLS12ConnectionWithCurve) + sc.Step(`^(?i)the "([^"]+)" metrics endpoint rejects a TLS 1\.2 connection with cipher "([^"]+)" and only curve "([^"]+)"$`, MetricsEndpointRejectsTLS12ConnectionWithCurve) + sc.Step(`^(?i)the "([^"]+)" metrics endpoint accepts a connection using only curve "([^"]+)"$`, MetricsEndpointAcceptsConnectionUsingCurve) + sc.Step(`^(?i)the "([^"]+)" metrics endpoint rejects a connection using only curve "([^"]+)"$`, MetricsEndpointRejectsConnectionUsingOnlyCurve) + sc.Step(`^(?i)the "([^"]+)" component is configured with HTTPS_PROXY "([^"]+)"$`, ConfigureDeploymentWithHTTPSProxy) + sc.Step(`^(?i)the "([^"]+)" component is configured with HTTPS_PROXY pointing to a recording proxy$`, StartRecordingProxyAndConfigureDeployment) + sc.Step(`^(?i)the recording proxy received a CONNECT request for the catalogd service$`, RecordingProxyReceivedCONNECTForCatalogd) + sc.Step(`^(?i)the catalogd leader pod is force-deleted$`, CatalogdLeaderPodIsForceDeleted) + sc.Step(`^(?i)a new catalogd leader is elected$`, NewCatalogdLeaderIsElected) +} diff --git a/test/internal/e2e/steps/proxy_steps.go b/test/internal/e2e/catalogd/steps/proxy_steps.go similarity index 90% rename from test/internal/e2e/steps/proxy_steps.go rename to test/internal/e2e/catalogd/steps/proxy_steps.go index f2fc0c2e17..84875b70c1 100644 --- a/test/internal/e2e/steps/proxy_steps.go +++ b/test/internal/e2e/catalogd/steps/proxy_steps.go @@ -15,6 +15,8 @@ import ( "time" appsv1 "k8s.io/api/apps/v1" + + shared "github.com/operator-framework/operator-controller/test/internal/e2e/steps" ) // --------------------------------------------------------------------------- @@ -157,7 +159,7 @@ func kindGatewayIP() (string, error) { // the "default" Namespace, which is the address client-go uses to reach the // API server from inside a pod (via the KUBERNETES_SERVICE_HOST env var). func kubernetesClusterIP() (string, error) { - ip, err := K8sClient(context.Background(), "get", "service", "kubernetes", "-n", "default", + ip, err := shared.K8sClient(context.Background(), "get", "service", "kubernetes", "-n", "default", "-o", "jsonpath={.spec.clusterIP}") if err != nil { return "", fmt.Errorf("failed to get kubernetes service cluster IP: %w", err) @@ -168,7 +170,7 @@ func kubernetesClusterIP() (string, error) { // getDeploymentContainerEnv returns the environment variables for the named // container in the given deployment, as a slice of "NAME=VALUE" strings. func getDeploymentContainerEnv(deploymentName, namespace, containerName string) ([]string, error) { - raw, err := K8sClient(context.Background(), "get", "deployment", deploymentName, "-n", namespace, "-o", "json") + raw, err := shared.K8sClient(context.Background(), "get", "deployment", deploymentName, "-n", namespace, "-o", "json") if err != nil { return nil, fmt.Errorf("failed to get deployment %s/%s: %w", namespace, deploymentName, err) } @@ -196,7 +198,7 @@ func getDeploymentContainerEnv(deploymentName, namespace, containerName string) // the JSON Patch "add" operation, which creates the env field if absent. func setDeploymentEnvVars(deploymentName, namespace, containerName string, env []string) error { // Fetch the deployment to find the container index. - raw, err := K8sClient(context.Background(), "get", "deployment", deploymentName, "-n", namespace, "-o", "json") + raw, err := shared.K8sClient(context.Background(), "get", "deployment", deploymentName, "-n", namespace, "-o", "json") if err != nil { return fmt.Errorf("failed to get deployment %s/%s: %w", namespace, deploymentName, err) } @@ -243,12 +245,12 @@ func setDeploymentEnvVars(deploymentName, namespace, containerName string, env [ return fmt.Errorf("failed to marshal patch: %w", err) } - if _, err := K8sClient(context.Background(), "patch", "deployment", deploymentName, "-n", namespace, + if _, err := shared.K8sClient(context.Background(), "patch", "deployment", deploymentName, "-n", namespace, "--type=json", fmt.Sprintf("--patch=%s", string(patchBytes))); err != nil { return fmt.Errorf("failed to patch deployment %s/%s: %w", namespace, deploymentName, err) } - if _, err := K8sClient(context.Background(), "rollout", "status", "deployment", deploymentName, "-n", namespace, + if _, err := shared.K8sClient(context.Background(), "rollout", "status", "deployment", deploymentName, "-n", namespace, "--timeout=5m"); err != nil { return fmt.Errorf("rollout of deployment %s/%s did not complete: %w", namespace, deploymentName, err) } @@ -284,12 +286,12 @@ func restoreDeployment(r deploymentRestore) error { // proxyURL and NO_PROXY to the Kubernetes API server cluster IP, then waits // for the rollout. The original env is saved for cleanup. func configureDeploymentProxy(ctx context.Context, component, proxyURL string) error { - sc := Scenario(ctx) + sc := catalogScenario(ctx) var deployName string switch component { case "operator-controller": - deployName = olmDeploymentName + deployName = "operator-controller-controller-manager" case "catalogd": deployName = "catalogd-controller-manager" default: @@ -304,13 +306,13 @@ func configureDeploymentProxy(ctx context.Context, component, proxyURL string) e // Namespace fields but have no containerName or originalEnv set. alreadyTracked := false for _, r := range sc.deploymentRestores { - if r.Name == deployName && r.Namespace == OLMNamespace && r.containerName != "" { + if r.Name == deployName && r.Namespace == shared.OLMNamespace && r.containerName != "" { alreadyTracked = true break } } - origEnv, err := getDeploymentContainerEnv(deployName, OLMNamespace, "manager") + origEnv, err := getDeploymentContainerEnv(deployName, shared.OLMNamespace, "manager") if err != nil { return err } @@ -318,7 +320,7 @@ func configureDeploymentProxy(ctx context.Context, component, proxyURL string) e if !alreadyTracked { sc.deploymentRestores = append(sc.deploymentRestores, deploymentRestore{ Name: deployName, - Namespace: OLMNamespace, + Namespace: shared.OLMNamespace, containerName: "manager", originalEnv: origEnv, }) @@ -335,7 +337,7 @@ func configureDeploymentProxy(ctx context.Context, component, proxyURL string) e newEnv := addOrReplaceEnvVar(origEnv, "HTTPS_PROXY", proxyURL) newEnv = addOrReplaceEnvVar(newEnv, "NO_PROXY", k8sIP) - return setDeploymentEnvVars(deployName, OLMNamespace, "manager", newEnv) + return setDeploymentEnvVars(deployName, shared.OLMNamespace, "manager", newEnv) } // --------------------------------------------------------------------------- @@ -353,7 +355,7 @@ func ConfigureDeploymentWithHTTPSProxy(ctx context.Context, component, proxyURL // proxy reachable from the cluster via the container-runtime Kind network // gateway, then patches the component deployment to route HTTPS through it. func StartRecordingProxyAndConfigureDeployment(ctx context.Context, component string) error { - sc := Scenario(ctx) + sc := catalogScenario(ctx) p, err := newRecordingProxy() if err != nil { @@ -372,7 +374,7 @@ func StartRecordingProxyAndConfigureDeployment(ctx context.Context, component st } proxyURL := fmt.Sprintf("http://%s:%s", gatewayIP, port) - logger.Info("Recording proxy started", "url", proxyURL) + shared.Log().Info("Recording proxy started", "url", proxyURL) return configureDeploymentProxy(ctx, component, proxyURL) } @@ -386,15 +388,15 @@ func StartRecordingProxyAndConfigureDeployment(ctx context.Context, component st // CONNECT. This is intentional — the step only verifies that operator-controller // respected HTTPS_PROXY and sent the request through the proxy. func RecordingProxyReceivedCONNECTForCatalogd(ctx context.Context) error { - sc := Scenario(ctx) + sc := catalogScenario(ctx) if sc.proxy == nil { return fmt.Errorf("no recording proxy was started in this scenario") } - WaitFor(ctx, func() bool { + shared.WaitFor(ctx, func() bool { for _, h := range sc.proxy.recordedHosts() { if strings.Contains(h, "catalogd") { - logger.Info("Recording proxy confirmed CONNECT for catalogd", "host", h) + shared.Log().Info("Recording proxy confirmed CONNECT for catalogd", "host", h) return true } } diff --git a/test/internal/e2e/steps/tls_steps.go b/test/internal/e2e/catalogd/steps/tls_steps.go similarity index 94% rename from test/internal/e2e/steps/tls_steps.go rename to test/internal/e2e/catalogd/steps/tls_steps.go index 6cd56f2aab..87ebf5bb6d 100644 --- a/test/internal/e2e/steps/tls_steps.go +++ b/test/internal/e2e/catalogd/steps/tls_steps.go @@ -10,6 +10,8 @@ import ( "time" appsv1 "k8s.io/api/apps/v1" + + shared "github.com/operator-framework/operator-controller/test/internal/e2e/steps" ) // tlsCipherSuiteByName is a Name→ID map that covers TLS 1.3 cipher suites @@ -57,16 +59,16 @@ var curveIDByName = map[string]tls.CurveID{ // waits until a basic TLS connection succeeds (confirming the port-forward is ready), // then calls fn with the local address. The port-forward is torn down when fn returns. func withMetricsPortForward(ctx context.Context, component string, fn func(addr string) error) error { - svc, err := GetMetricsService(component) + svc, err := shared.GetMetricsService(component) if err != nil { return err } - port, err := MetricsPort(svc) + port, err := shared.MetricsPort(svc) if err != nil { return err } - addr, cleanup, err := PortForward(ctx, svc.Namespace, fmt.Sprintf("service/%s", svc.Name), port) + addr, cleanup, err := shared.PortForward(ctx, svc.Namespace, fmt.Sprintf("service/%s", svc.Name), port) if err != nil { return err } @@ -76,7 +78,7 @@ func withMetricsPortForward(ctx context.Context, component string, fn func(addr // restrictions) serves as the readiness probe; any successful TLS handshake confirms // the tunnel is up. A short per-attempt Timeout prevents the dial from blocking // indefinitely if the local port is open but the upstream handshake stalls. - WaitFor(ctx, func() bool { + shared.WaitFor(ctx, func() bool { dialer := &net.Dialer{Timeout: 3 * time.Second} conn, err := tls.DialWithDialer(dialer, "tcp", addr, &tls.Config{InsecureSkipVerify: true}) //nolint:gosec if err != nil { @@ -157,7 +159,7 @@ func componentDeploymentName(component string) (string, error) { // getDeploymentContainerArgs returns the args list of the container named "manager" // inside the named deployment. func getDeploymentContainerArgs(ns, name string) ([]string, error) { - raw, err := K8sClient(context.Background(), "get", "deployment", name, "-n", ns, "-o", "json") + raw, err := shared.K8sClient(context.Background(), "get", "deployment", name, "-n", ns, "-o", "json") if err != nil { return nil, fmt.Errorf("getting deployment %s/%s: %w", ns, name, err) } @@ -176,7 +178,7 @@ func getDeploymentContainerArgs(ns, name string) ([]string, error) { // getDeploymentContainerIndex returns the index of the container named "manager" // inside the named deployment. func getDeploymentContainerIndex(ns, name string) (int, error) { - raw, err := K8sClient(context.Background(), "get", "deployment", name, "-n", ns, "-o", "json") + raw, err := shared.K8sClient(context.Background(), "get", "deployment", name, "-n", ns, "-o", "json") if err != nil { return -1, fmt.Errorf("getting deployment %s/%s: %w", ns, name, err) } @@ -204,7 +206,7 @@ func patchDeploymentArgs(ns, name string, args []string) error { return err } patch := fmt.Sprintf(`[{"op":"replace","path":"/spec/template/spec/containers/%d/args","value":%s}]`, idx, string(argsJSON)) - _, err = K8sClient(context.Background(), "patch", "deployment", name, "-n", ns, "--type=json", "-p", patch) + _, err = shared.K8sClient(context.Background(), "patch", "deployment", name, "-n", ns, "--type=json", "-p", patch) return err } @@ -241,26 +243,26 @@ func configureDeploymentCustomTLS(ctx context.Context, component, version, ciphe return err } - origArgs, err := getDeploymentContainerArgs(OLMNamespace, deploymentName) + origArgs, err := getDeploymentContainerArgs(shared.OLMNamespace, deploymentName) if err != nil { return err } - sc := Scenario(ctx) + sc := catalogScenario(ctx) sc.deploymentRestores = append(sc.deploymentRestores, deploymentRestore{ Name: deploymentName, - Namespace: OLMNamespace, + Namespace: shared.OLMNamespace, patchedArgs: true, originalArgs: origArgs, }) newArgs := buildCustomTLSArgs(origArgs, version, ciphers, curves) - if err := patchDeploymentArgs(OLMNamespace, deploymentName, newArgs); err != nil { + if err := patchDeploymentArgs(shared.OLMNamespace, deploymentName, newArgs); err != nil { return fmt.Errorf("patching %s with custom TLS args: %w", deploymentName, err) } - WaitFor(ctx, func() bool { - _, err := K8sClient(ctx, "rollout", "status", "-n", OLMNamespace, + shared.WaitFor(ctx, func() bool { + _, err := shared.K8sClient(ctx, "rollout", "status", "-n", shared.OLMNamespace, fmt.Sprintf("deployment/%s", deploymentName), "--timeout=10s") return err == nil }) diff --git a/test/internal/e2e/steps/hooks.go b/test/internal/e2e/steps/hooks.go index c23f31dbdb..eb57f9b8bd 100644 --- a/test/internal/e2e/steps/hooks.go +++ b/test/internal/e2e/steps/hooks.go @@ -33,17 +33,6 @@ type Resource struct { Namespace string } -// deploymentRestore records the original state of a deployment so it can be -// rolled back after a test that modifies deployment configuration. -type deploymentRestore struct { - Name string // deployment name - Namespace string - containerName string // container to patch (for env var restores) - patchedArgs bool // true when container args were modified (for TLS profile patches) - originalArgs []string // original container args; may be nil if args were unset - originalEnv []string // original env vars as "NAME=VALUE" (for proxy patches) -} - // World holds scenario state shared by component step packages. type World struct { ID string @@ -58,11 +47,9 @@ type World struct { RemovedResources []unstructured.Unstructured MetricsResponse map[string]string LeaderPods map[string]string // component name -> leader pod name - deploymentRestores []deploymentRestore ExtensionObjects []client.Object RevisionSecrets []Resource ResourceUIDs map[string]types.UID - proxy *recordingProxy CatalogAddr string CatalogCleanup func() LastApplyStderr string @@ -100,7 +87,7 @@ var ( features.WebhookProviderOpenshiftServiceCA: false, features.BoxcutterRuntime: false, features.DeploymentConfig: false, - catalogdHAFeature: false, + featuregate.Feature("CatalogdHA"): false, } logger logr.Logger ) @@ -162,7 +149,7 @@ func BeforeSuite() { // step and return early below) still get the gate set correctly. if out, err := K8sClient(context.Background(), "get", "nodes", "--no-headers", "-o", "name"); err == nil && len(strings.Fields(strings.TrimSpace(out))) >= 2 { - FeatureGates[catalogdHAFeature] = true + FeatureGates[featuregate.Feature("CatalogdHA")] = true } olm, catalogdDep, err := detectOLMDeployments() @@ -187,7 +174,7 @@ func BeforeSuite() { // was not found or the replica count is not parseable. if catalogdDep != nil { if replicas := catalogdDep.Spec.Replicas; replicas != nil { - FeatureGates[catalogdHAFeature] = *replicas >= 2 + FeatureGates[featuregate.Feature("CatalogdHA")] = *replicas >= 2 } } @@ -254,34 +241,11 @@ func stderrOutput(err error) string { func ScenarioCleanup(ctx context.Context, _ *godog.Scenario, err error) (context.Context, error) { sc := Scenario(ctx) - // Stop the in-process recording proxy if one was started. - if sc.proxy != nil { - sc.proxy.stop() - } // Stop catalog port-forward if one was started. if sc.CatalogCleanup != nil { sc.CatalogCleanup() } - // Restore any deployments that were modified during the scenario. Runs - // unconditionally (even on failure) to prevent a misconfigured deployment - // from bleeding into subsequent scenarios. Restored in LIFO order so that - // multiple patches to the same deployment unwind to the true original. - for i := len(sc.deploymentRestores) - 1; i >= 0; i-- { - dr := sc.deploymentRestores[i] - if dr.patchedArgs { - if err2 := patchDeploymentArgs(dr.Namespace, dr.Name, dr.originalArgs); err2 != nil { - logger.Info("Error restoring deployment args", "name", dr.Name, "error", err2) - } else if _, err2 := K8sClient(ctx, "rollout", "status", "-n", dr.Namespace, - fmt.Sprintf("deployment/%s", dr.Name), "--timeout=2m"); err2 != nil { - logger.Info("Timeout waiting for deployment rollout after restore", "name", dr.Name) - } - } - if err2 := restoreDeployment(dr); err2 != nil { - logger.Info("Error restoring deployment env", "deployment", dr.Name, "namespace", dr.Namespace, "error", err2) - } - } - if err != nil { return ctx, err } diff --git a/test/internal/e2e/steps/steps.go b/test/internal/e2e/steps/steps.go index 3ad354e235..02a3f0eebc 100644 --- a/test/internal/e2e/steps/steps.go +++ b/test/internal/e2e/steps/steps.go @@ -220,18 +220,8 @@ func RegisterSteps(sc *godog.ScenarioContext) { sc.Step(`^(?i)ClusterExtension "([^"]+)" reports ([[:alnum:]]+) as ([[:alnum:]]+) with Reason ([[:alnum:]]+) and Message includes:$`, NamedClusterExtensionReportsConditionWithMessageFragment) // TLS profile enforcement steps — deployment configuration - sc.Step(`^(?i)the "([^"]+)" deployment is configured with custom TLS minimum version "([^"]+)"$`, ConfigureDeploymentWithCustomTLSVersion) - sc.Step(`^(?i)the "([^"]+)" deployment is configured with custom TLS version "([^"]+)", ciphers "([^"]+)", and curves "([^"]+)"$`, ConfigureDeploymentWithCustomTLSFull) // TLS profile enforcement steps — connection assertions - sc.Step(`^(?i)the "([^"]+)" metrics endpoint accepts a TLS 1\.3 connection$`, MetricsEndpointAcceptsTLS13) - sc.Step(`^(?i)the "([^"]+)" metrics endpoint rejects a TLS 1\.2 connection$`, MetricsEndpointRejectsTLS12) - sc.Step(`^(?i)the "([^"]+)" metrics endpoint negotiates cipher "([^"]+)" over TLS 1\.2$`, MetricsEndpointNegotiatesTLS12Cipher) - sc.Step(`^(?i)the "([^"]+)" metrics endpoint rejects a TLS 1\.2 connection offering only cipher "([^"]+)"$`, MetricsEndpointRejectsTLS12ConnectionWithCipher) - sc.Step(`^(?i)the "([^"]+)" metrics endpoint accepts a TLS 1\.2 connection with cipher "([^"]+)" and curve "([^"]+)"$`, MetricsEndpointAcceptsTLS12ConnectionWithCurve) - sc.Step(`^(?i)the "([^"]+)" metrics endpoint rejects a TLS 1\.2 connection with cipher "([^"]+)" and only curve "([^"]+)"$`, MetricsEndpointRejectsTLS12ConnectionWithCurve) - sc.Step(`^(?i)the "([^"]+)" metrics endpoint accepts a connection using only curve "([^"]+)"$`, MetricsEndpointAcceptsConnectionUsingCurve) - sc.Step(`^(?i)the "([^"]+)" metrics endpoint rejects a connection using only curve "([^"]+)"$`, MetricsEndpointRejectsConnectionUsingOnlyCurve) // Upgrade-specific steps sc.Step(`^(?i)the latest stable OLM release is installed$`, LatestStableOLMReleaseIsInstalled) @@ -241,13 +231,6 @@ func RegisterSteps(sc *godog.ScenarioContext) { sc.Step(`^(?i)ClusterExtension is reconciled$`, ClusterExtensionIsReconciled) sc.Step(`^(?i)catalog "([^"]+)" is reconciled$`, ScenarioCatalogIsReconciled) sc.Step(`^(?i)catalog "([^"]+)" reports ([[:alnum:]]+) as ([[:alnum:]]+) with Reason ([[:alnum:]]+)$`, ScenarioCatalogReportsCondition) - - sc.Step(`^(?i)the "([^"]+)" component is configured with HTTPS_PROXY "([^"]+)"$`, ConfigureDeploymentWithHTTPSProxy) - sc.Step(`^(?i)the "([^"]+)" component is configured with HTTPS_PROXY pointing to a recording proxy$`, StartRecordingProxyAndConfigureDeployment) - sc.Step(`^(?i)the recording proxy received a CONNECT request for the catalogd service$`, RecordingProxyReceivedCONNECTForCatalogd) - - sc.Step(`^(?i)the catalogd leader pod is force-deleted$`, CatalogdLeaderPodIsForceDeleted) - sc.Step(`^(?i)a new catalogd leader is elected$`, NewCatalogdLeaderIsElected) } func init() {