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 .",