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