From 1c4643498ceff59a0faa1c7d07ee1906abcabe28 Mon Sep 17 00:00:00 2001 From: Fabricio Aguiar Date: Wed, 4 Nov 2026 17:33:58 +0000 Subject: [PATCH] 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 | 6 +- .../e2e/steps/asciicast_recorder_test.go | 22 + .../e2e/steps/command_recorder_test.go | 32 + test/internal/e2e/steps/conditions.go | 14 +- test/internal/e2e/steps/demo_steps.go | 70 +- test/internal/e2e/steps/group_steps.go | 76 +- test/internal/e2e/steps/ha_steps.go | 20 +- test/internal/e2e/steps/hooks.go | 179 ++-- test/internal/e2e/steps/proxy_steps.go | 30 +- test/internal/e2e/steps/steps.go | 855 +++++++++--------- test/internal/e2e/steps/tls_steps.go | 34 +- test/internal/e2e/steps/upgrade_steps.go | 74 +- 12 files changed, 756 insertions(+), 656 deletions(-) create mode 100644 test/internal/e2e/steps/asciicast_recorder_test.go create mode 100644 test/internal/e2e/steps/command_recorder_test.go diff --git a/test/internal/e2e/steps/asciicast_recorder.go b/test/internal/e2e/steps/asciicast_recorder.go index 62ef69e01b..003d0833e0 100644 --- a/test/internal/e2e/steps/asciicast_recorder.go +++ b/test/internal/e2e/steps/asciicast_recorder.go @@ -12,8 +12,12 @@ import ( type recorderContextKey struct{} +// WithRecorder attaches rec as both the demo asciicast recorder and the command +// observer used by shared helpers, including bash and K8sClient. Both roles +// intentionally use the same instance; callers must not override the command +// recorder separately on the returned context. 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/asciicast_recorder_test.go b/test/internal/e2e/steps/asciicast_recorder_test.go new file mode 100644 index 0000000000..7f9566c27e --- /dev/null +++ b/test/internal/e2e/steps/asciicast_recorder_test.go @@ -0,0 +1,22 @@ +package steps_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/operator-framework/operator-controller/test/internal/e2e/steps" +) + +func TestWithRecorder(t *testing.T) { + parent := steps.WithCommandRecorder(context.Background(), &steps.AsciiCastRecorder{}) + recorder := &steps.AsciiCastRecorder{} + + ctx := steps.WithRecorder(parent, recorder) + + require.Same(t, recorder, steps.RecorderFromContext(ctx)) + require.Same(t, recorder, steps.CommandRecorderFromContext(ctx)) + require.NotSame(t, recorder, steps.CommandRecorderFromContext(parent)) + require.Nil(t, steps.RecorderFromContext(parent)) +} diff --git a/test/internal/e2e/steps/command_recorder_test.go b/test/internal/e2e/steps/command_recorder_test.go new file mode 100644 index 0000000000..5d8909e352 --- /dev/null +++ b/test/internal/e2e/steps/command_recorder_test.go @@ -0,0 +1,32 @@ +package steps_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/operator-framework/operator-controller/test/internal/e2e/steps" +) + +type fakeCommandRecorder struct{} + +func (*fakeCommandRecorder) RecordCommand(string, string, string, time.Duration) {} +func (*fakeCommandRecorder) RecordCommandWithInput(string, string, string, string, time.Duration) { +} +func (*fakeCommandRecorder) RecordCustom(string, string, string) {} + +func TestCommandRecorderFromContextWithoutRecorder(t *testing.T) { + require.Nil(t, steps.CommandRecorderFromContext(context.Background())) +} + +func TestWithCommandRecorder(t *testing.T) { + parent := context.Background() + recorder := &fakeCommandRecorder{} + + ctx := steps.WithCommandRecorder(parent, recorder) + + require.Same(t, recorder, steps.CommandRecorderFromContext(ctx)) + require.Nil(t, steps.CommandRecorderFromContext(parent)) +} 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..a335a4cedb 100644 --- a/test/internal/e2e/steps/group_steps.go +++ b/test/internal/e2e/steps/group_steps.go @@ -20,14 +20,14 @@ import ( // ResourceWithNamespaceOwnerApplied resolves a real cluster-scoped Namespace owner // 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..f39c440972 100644 --- a/test/internal/e2e/steps/ha_steps.go +++ b/test/internal/e2e/steps/ha_steps.go @@ -15,7 +15,7 @@ import ( 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. @@ -24,14 +24,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 := 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..af56f0cee4 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 @@ -91,8 +93,10 @@ const ( ) var ( - devMode = false - featureGates = map[featuregate.Feature]bool{ + devMode = false + // FeatureGates holds defaults refined by BeforeSuite from the cluster configuration. + // Consumers must not mutate this map. + FeatureGates = map[featuregate.Feature]bool{ features.WebhookProviderCertManager: true, features.SingleOwnNamespaceInstallSupport: false, features.WebhookProviderOpenshiftServiceCA: false, @@ -121,7 +125,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 +159,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 +169,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 + 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 +186,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 +199,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 +224,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 +252,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 +269,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 +285,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 +308,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..d92fc7eb98 100644 --- a/test/internal/e2e/steps/proxy_steps.go +++ b/test/internal/e2e/steps/proxy_steps.go @@ -157,7 +157,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 := 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) } @@ -196,7 +196,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 := 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 { @@ -304,21 +304,21 @@ 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 == 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, }) @@ -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) } // --------------------------------------------------------------------------- @@ -353,7 +353,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 := scenarioCtx(ctx) + sc := Scenario(ctx) p, err := newRecordingProxy() if err != nil { @@ -386,12 +386,12 @@ 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 := 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 23d7f1db17..f29d21f754 100644 --- a/test/internal/e2e/steps/steps.go +++ b/test/internal/e2e/steps/steps.go @@ -54,13 +54,15 @@ const ( olmDeploymentName = "operator-controller-controller-manager" catalogdDeploymentName = "catalogd-controller-manager" defaultTimeout = 5 * time.Minute - tick = 1 * time.Second + // Tick is the polling interval used by wait helpers. + 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 { +// It is initialized when the package loads; consumers must not mutate it. +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 +72,22 @@ 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 is the OLM system namespace, initialized during suite setup + // or upgrade installation. Consumers must not mutate it. + OLMNamespace = "olmv1-system" + // ComponentNamespaces maps component labels to namespaces discovered by + // BeforeSuite. Missing entries fall back to OLMNamespace. + // Consumers must not mutate this map. + ComponentNamespaces = map[string]string{} + // KubeconfigPath is configured in init from KUBECONFIG or the kubeconfig flag. + // Consumers must not mutate it after suite startup. + KubeconfigPath string + // K8sCLI is configured in init through the k8s.cli flag. + // Consumers must not mutate it after suite startup. + 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}") + providerID, err := K8sClient(context.Background(), "get", "nodes", "-o", "jsonpath={.items[0].spec.providerID}") if err != nil || !strings.HasPrefix(providerID, "kind://") { return nil } @@ -251,31 +262,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 @@ -299,16 +310,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 @@ -328,7 +339,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 @@ -374,31 +385,31 @@ func ImageRegistryIsAvailable() error { // 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. 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 } @@ -406,14 +417,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 @@ -421,28 +432,28 @@ 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) @@ -450,14 +461,14 @@ func substituteScenarioVars(content string, sc *scenarioContext) string { // 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 } @@ -471,12 +482,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) } @@ -488,9 +499,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) } @@ -499,22 +510,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 @@ -526,17 +537,17 @@ func ResourceIsAppliedAsUser(ctx context.Context, user, group string, yamlTempla // 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 // 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 } @@ -555,14 +566,14 @@ func ResourceApplyAsUserFails(ctx context.Context, user, group, errMsg string, y // "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") @@ -580,40 +591,40 @@ func ResourceApplyAsUserFails(ctx context.Context, user, group, errMsg string, y // 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 } @@ -622,10 +633,10 @@ func ResourceApplyDoesNotEmitWarning(ctx context.Context) error { // 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. 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 @@ -634,17 +645,17 @@ func TrackCurrentClusterExtensionForCleanup(ctx context.Context) error { // ClusterExtensionOwnsClusterObjectSets waits for the named ClusterExtension to own exactly the // 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 } @@ -662,14 +673,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 } @@ -687,15 +698,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 } @@ -708,21 +719,21 @@ func ClusterObjectDeploymentIsOwnedByClusterExtension(ctx context.Context, codNa // 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) // 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{ @@ -736,14 +747,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, @@ -753,16 +764,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 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) } @@ -771,25 +782,25 @@ 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 @@ -797,19 +808,19 @@ func ResourceIsApplied(ctx context.Context, yamlTemplate *godog.DocString) error // 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", "", ) } @@ -820,13 +831,13 @@ 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 } @@ -834,14 +845,14 @@ func ClusterExtensionReconciledLatestGeneration(ctx context.Context) error { // 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. 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 @@ -854,15 +865,15 @@ func ClusterExtensionIsRolledOut(ctx context.Context) error { // 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 @@ -876,7 +887,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"]) @@ -891,13 +902,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. 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") } @@ -909,18 +920,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 } @@ -928,45 +939,45 @@ 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. 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. 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, @@ -984,22 +995,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 @@ -1019,31 +1030,31 @@ func ClusterExtensionReportsConditionTransitionTime(ctx context.Context, conditi // ClusterObjectSetReportsConditionWithoutMsg waits for the named ClusterObjectSet to have a condition // 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. 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. 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. 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. 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 @@ -1056,7 +1067,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 @@ -1082,7 +1093,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) @@ -1116,11 +1127,11 @@ func consistently(ctx context.Context, duration time.Duration, check func(contex 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, @@ -1142,9 +1153,9 @@ 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 } @@ -1152,17 +1163,17 @@ func TriggerClusterObjectSetReconciliation(ctx context.Context, cosName string) // ClusterObjectSetHasObservedPhase waits for the named ClusterObjectSet to have // 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 { @@ -1178,16 +1189,16 @@ func ClusterObjectSetHasObservedPhase(ctx context.Context, cosName, phaseName st // ClusterObjectSetIsArchived waits for the named ClusterObjectSet to have Ready=False // 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. 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 } @@ -1199,15 +1210,15 @@ func ClusterObjectSetIsEventuallyDeleted(ctx context.Context, revisionName strin // ClusterObjectSetHasAggregateObjectCounts waits for the named ClusterObjectSet to report exact // 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 @@ -1223,14 +1234,14 @@ 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. 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 @@ -1246,11 +1257,11 @@ 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. 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 @@ -1267,13 +1278,13 @@ func ClusterObjectSetHasLabelWithValue(ctx context.Context, revisionName, labelK // ClusterObjectSet's phases to either not exist on the cluster or not contain the revision // 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 } @@ -1287,7 +1298,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) } @@ -1308,12 +1319,12 @@ 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 } @@ -1321,7 +1332,7 @@ func ClusterObjectSetObjectsNotFoundOrNotOwned(ctx context.Context, revisionName if strings.TrimSpace(out) == "" { return true } - clusterObj, err := toUnstructured(out) + clusterObj, err := ToUnstructured(out) if err != nil { return false } @@ -1345,11 +1356,11 @@ 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. 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 } @@ -1391,18 +1402,18 @@ 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. 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 }) } @@ -1412,8 +1423,8 @@ func ClusterObjectSetReferredSecretsExist(ctx context.Context, revisionName, nam // ClusterObjectSetReferredSecretsAreImmutable verifies that all referred Secrets for the named // 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 { @@ -1433,15 +1444,15 @@ 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. 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 @@ -1459,10 +1470,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) } @@ -1497,8 +1508,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 { @@ -1520,11 +1531,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) } @@ -1555,15 +1566,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 }) @@ -1593,8 +1604,8 @@ 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 @@ -1604,9 +1615,9 @@ func collectReferredSecretRefs(ctx context.Context, revisionName string) ([]refe 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 }) @@ -1628,14 +1639,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 { @@ -1654,13 +1665,13 @@ 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) } @@ -1669,14 +1680,14 @@ func listReferredSecrets(ctx context.Context, revisionName string) ([]corev1.Sec // 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 @@ -1684,36 +1695,36 @@ func ResourceAvailable(ctx context.Context, resource string) error { // 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. 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 @@ -1721,22 +1732,22 @@ func ResourceEventuallyNotFound(ctx context.Context, resource string) error { // 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 } @@ -1748,7 +1759,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 } @@ -1760,24 +1771,24 @@ func ResourceMatches(ctx context.Context, resource string, requiredContentTempla // 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 { @@ -1790,7 +1801,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 } } @@ -1800,9 +1811,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...) @@ -1814,7 +1825,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) } @@ -1823,16 +1834,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) @@ -1842,7 +1853,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) } @@ -1853,13 +1864,13 @@ 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)}) } } @@ -1868,8 +1879,8 @@ func applyPermissionsToServiceAccount(ctx context.Context, serviceAccount, rbacT // 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, @@ -1877,7 +1888,7 @@ 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 @@ -1892,7 +1903,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) @@ -1901,9 +1912,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 } @@ -1942,7 +1953,7 @@ 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. 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 @@ -1956,7 +1967,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 } @@ -1965,12 +1976,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) @@ -1978,7 +1989,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 @@ -1990,7 +2001,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) @@ -2011,12 +2022,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) } @@ -2024,12 +2035,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) } @@ -2040,10 +2051,10 @@ func CatalogIsLabeledWith(ctx context.Context, catalogUserName, label string) er // and active by checking that status.observedGeneration matches metadata.generation. // 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 @@ -2058,14 +2069,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 @@ -2078,7 +2089,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 @@ -2086,12 +2097,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 } @@ -2113,7 +2124,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 } @@ -2123,8 +2134,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) } @@ -2214,14 +2225,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 @@ -2232,9 +2243,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 @@ -2250,7 +2261,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) } @@ -2260,7 +2271,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) } @@ -2323,8 +2334,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) } @@ -2342,10 +2353,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 } @@ -2359,8 +2370,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 @@ -2368,9 +2379,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 } @@ -2378,11 +2389,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 } @@ -2395,7 +2406,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 } @@ -2424,7 +2435,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 } @@ -2460,12 +2471,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 } @@ -2476,7 +2487,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) @@ -2504,7 +2515,7 @@ func listHelmReleaseResources(extName string) ([]client.Object, error) { // 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 { @@ -2582,7 +2593,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) } @@ -2598,9 +2609,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) } @@ -2639,7 +2650,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) } @@ -2663,10 +2674,10 @@ func latestActiveRevisionForExtension(extName string) (*ocv1.ClusterObjectSet, e // 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 { @@ -2682,8 +2693,8 @@ 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 @@ -2691,10 +2702,10 @@ func AnnotationsAreAdded(ctx context.Context, resourceName string, table *godog. // 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 { @@ -2710,8 +2721,8 @@ 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 @@ -2719,10 +2730,10 @@ func LabelsAreAdded(ctx context.Context, resourceName string, table *godog.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 { @@ -2734,8 +2745,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 } @@ -2768,10 +2779,10 @@ func matchLabels(actual, expected map[string]string) (string, string, bool) { // 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 { @@ -2783,8 +2794,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 } @@ -2803,16 +2814,16 @@ func ResourceHasLabels(ctx context.Context, resourceName string, table *godog.Ta // 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 } @@ -2831,10 +2842,10 @@ func NamespaceHasLabels(ctx context.Context, nsName string, table *godog.Table) // 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) } @@ -2850,16 +2861,16 @@ func NamespaceDoesNotHaveLabel(ctx context.Context, nsName string, labelKey stri // 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 } @@ -2881,10 +2892,10 @@ func NamespaceIsManagedByOLM(ctx context.Context, nsName string) error { // 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) } @@ -2919,7 +2930,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") } @@ -2951,8 +2962,8 @@ 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 } @@ -2962,8 +2973,8 @@ func parseKeyValueTable(table *godog.Table, sc *scenarioContext) (map[string]str // 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 { @@ -2976,7 +2987,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)) } @@ -2990,11 +3001,11 @@ func RolloutRestartIsPerformed(ctx context.Context, resourceName string) error { // the given annotation key. Uses JSON parsing to avoid JSONPath issues with dots // 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 } @@ -3023,16 +3034,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") @@ -3043,11 +3054,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 } @@ -3055,11 +3066,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 @@ -3078,11 +3089,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 @@ -3093,11 +3104,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 { @@ -3106,7 +3117,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 @@ -3138,7 +3149,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..dc0e924604 100644 --- a/test/internal/e2e/steps/tls_steps.go +++ b/test/internal/e2e/steps/tls_steps.go @@ -60,7 +60,7 @@ var curveIDByName = map[string]tls.CurveID{ // 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) } @@ -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), "", "", ) } @@ -157,7 +157,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 { + 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 { @@ -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..0a6e4f08a0 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() @@ -80,11 +80,11 @@ 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. 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,19 +105,19 @@ 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: _) 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 } @@ -131,7 +131,7 @@ var resourceTypeToComponent = map[string]string{ // 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 } @@ -206,21 +206,21 @@ 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. 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 .",