diff --git a/.github/workflows/update-demos.yaml b/.github/workflows/update-demos.yaml index 37b83321d6..6327361756 100644 --- a/.github/workflows/update-demos.yaml +++ b/.github/workflows/update-demos.yaml @@ -8,16 +8,16 @@ on: - 'api/*' - 'config/*' - 'test/e2e/platform/features/demos.feature' - - 'test/internal/e2e/steps/demo_steps.go' - - 'test/internal/e2e/steps/asciicast_*.go' + - 'test/internal/e2e/platform/steps/demo_steps.go' + - 'test/internal/e2e/platform/steps/asciicast_*.go' - '.github/workflows/update-demos.yaml' pull_request: paths: - 'api/*' - 'config/*' - 'test/e2e/platform/features/demos.feature' - - 'test/internal/e2e/steps/demo_steps.go' - - 'test/internal/e2e/steps/asciicast_*.go' + - 'test/internal/e2e/platform/steps/demo_steps.go' + - 'test/internal/e2e/platform/steps/asciicast_*.go' - '.github/workflows/update-demos.yaml' jobs: diff --git a/test/e2e/catalogd/suite_test.go b/test/e2e/catalogd/suite_test.go index b0ac4b9694..a03bc6214a 100644 --- a/test/e2e/catalogd/suite_test.go +++ b/test/e2e/catalogd/suite_test.go @@ -17,7 +17,6 @@ func TestMain(m *testing.M) { }, func(sc *godog.ScenarioContext) { steps.RegisterSteps(sc) catalogsteps.RegisterSteps(sc) - steps.RegisterDemoSteps(sc) catalogsteps.RegisterHooks(sc) steps.RegisterHooks(sc) })) diff --git a/test/e2e/features_test.go b/test/e2e/features_test.go index d52553e64d..190a4d6cd6 100644 --- a/test/e2e/features_test.go +++ b/test/e2e/features_test.go @@ -15,6 +15,7 @@ import ( "github.com/operator-framework/operator-controller/test/internal/e2e" catalogsteps "github.com/operator-framework/operator-controller/test/internal/e2e/catalogd/steps" + platformsteps "github.com/operator-framework/operator-controller/test/internal/e2e/platform/steps" "github.com/operator-framework/operator-controller/test/internal/e2e/steps" ) @@ -43,8 +44,8 @@ func TestMain(m *testing.M) { }, func(sc *godog.ScenarioContext) { steps.RegisterSteps(sc) catalogsteps.RegisterSteps(sc) - steps.RegisterDemoSteps(sc) + platformsteps.RegisterDemoSteps(sc) catalogsteps.RegisterHooks(sc) - steps.RegisterHooks(sc, steps.RegisterAsciiCastHooks) + steps.RegisterHooks(sc, platformsteps.RegisterAsciiCastHooks) })) } diff --git a/test/e2e/object-controller/suite_test.go b/test/e2e/object-controller/suite_test.go index 721ee038df..2449fab579 100644 --- a/test/e2e/object-controller/suite_test.go +++ b/test/e2e/object-controller/suite_test.go @@ -15,7 +15,6 @@ func TestMain(m *testing.M) { tc.BeforeSuite(steps.BeforeSuite) }, func(sc *godog.ScenarioContext) { steps.RegisterSteps(sc) - steps.RegisterDemoSteps(sc) steps.RegisterHooks(sc) })) } diff --git a/test/e2e/operator-controller/suite_test.go b/test/e2e/operator-controller/suite_test.go index 721ee038df..2449fab579 100644 --- a/test/e2e/operator-controller/suite_test.go +++ b/test/e2e/operator-controller/suite_test.go @@ -15,7 +15,6 @@ func TestMain(m *testing.M) { tc.BeforeSuite(steps.BeforeSuite) }, func(sc *godog.ScenarioContext) { steps.RegisterSteps(sc) - steps.RegisterDemoSteps(sc) steps.RegisterHooks(sc) })) } diff --git a/test/e2e/platform/suite_test.go b/test/e2e/platform/suite_test.go index 2f38fde827..8ce9e190fa 100644 --- a/test/e2e/platform/suite_test.go +++ b/test/e2e/platform/suite_test.go @@ -7,6 +7,7 @@ import ( "github.com/cucumber/godog" "github.com/operator-framework/operator-controller/test/internal/e2e" + platformsteps "github.com/operator-framework/operator-controller/test/internal/e2e/platform/steps" "github.com/operator-framework/operator-controller/test/internal/e2e/steps" ) @@ -15,7 +16,7 @@ func TestMain(m *testing.M) { tc.BeforeSuite(steps.BeforeSuite) }, func(sc *godog.ScenarioContext) { steps.RegisterSteps(sc) - steps.RegisterDemoSteps(sc) - steps.RegisterHooks(sc, steps.RegisterAsciiCastHooks) + platformsteps.RegisterDemoSteps(sc) + steps.RegisterHooks(sc, platformsteps.RegisterAsciiCastHooks) })) } diff --git a/test/internal/e2e/steps/asciicast_hooks.go b/test/internal/e2e/platform/steps/asciicast_hooks.go similarity index 83% rename from test/internal/e2e/steps/asciicast_hooks.go rename to test/internal/e2e/platform/steps/asciicast_hooks.go index 9a2333ee13..85d7f96a09 100644 --- a/test/internal/e2e/steps/asciicast_hooks.go +++ b/test/internal/e2e/platform/steps/asciicast_hooks.go @@ -6,6 +6,8 @@ import ( "strings" "github.com/cucumber/godog" + + shared "github.com/operator-framework/operator-controller/test/internal/e2e/steps" ) const demoTag = "@demo" @@ -39,7 +41,7 @@ func startRecordingIfDemo(ctx context.Context, sc *godog.Scenario) (context.Cont return ctx, nil } rec := NewAsciiCastRecorder(sc.Name, demoOutputDir) - logger.Info("Starting asciicast recording", "scenario", sc.Name, "output", rec.castPath) + shared.Log().Info("Starting asciicast recording", "scenario", sc.Name, "output", rec.castPath) return WithRecorder(ctx, rec), nil } @@ -69,14 +71,14 @@ func stopRecordingIfDemo(ctx context.Context, sc *godog.Scenario, err error) (co return ctx, nil } if stopErr := rec.Stop(); stopErr != nil { - logger.Info("Failed to write asciicast file", "error", stopErr) + shared.Log().Info("Failed to write asciicast file", "error", stopErr) if err == nil { return ctx, stopErr } } else { // Slugify scenario name the same way as the recorder slug := strings.ToLower(strings.ReplaceAll(sc.Name, " ", "-")) - logger.Info("Asciicast recording saved", "file", demoOutputDir+"/"+slug+".cast") + shared.Log().Info("Asciicast recording saved", "file", demoOutputDir+"/"+slug+".cast") } return ctx, nil } diff --git a/test/internal/e2e/steps/asciicast_recorder.go b/test/internal/e2e/platform/steps/asciicast_recorder.go similarity index 97% rename from test/internal/e2e/steps/asciicast_recorder.go rename to test/internal/e2e/platform/steps/asciicast_recorder.go index 003d0833e0..3e61673eab 100644 --- a/test/internal/e2e/steps/asciicast_recorder.go +++ b/test/internal/e2e/platform/steps/asciicast_recorder.go @@ -8,6 +8,8 @@ import ( "path/filepath" "strings" "time" + + shared "github.com/operator-framework/operator-controller/test/internal/e2e/steps" ) type recorderContextKey struct{} @@ -17,7 +19,7 @@ type recorderContextKey struct{} // intentionally use the same instance; callers must not override the command // recorder separately on the returned context. func WithRecorder(ctx context.Context, rec *AsciiCastRecorder) context.Context { - return context.WithValue(WithCommandRecorder(ctx, rec), recorderContextKey{}, rec) + return context.WithValue(shared.WithCommandRecorder(ctx, rec), recorderContextKey{}, rec) } func RecorderFromContext(ctx context.Context) *AsciiCastRecorder { diff --git a/test/internal/e2e/steps/demo_steps.go b/test/internal/e2e/platform/steps/demo_steps.go similarity index 72% rename from test/internal/e2e/steps/demo_steps.go rename to test/internal/e2e/platform/steps/demo_steps.go index 088e43fb86..feb4a07792 100644 --- a/test/internal/e2e/steps/demo_steps.go +++ b/test/internal/e2e/platform/steps/demo_steps.go @@ -15,6 +15,8 @@ import ( "github.com/cucumber/godog" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + shared "github.com/operator-framework/operator-controller/test/internal/e2e/steps" ) func RegisterDemoSteps(sc *godog.ScenarioContext) { @@ -28,7 +30,7 @@ func RegisterDemoSteps(sc *godog.ScenarioContext) { func bash(ctx context.Context, script string) (string, error) { cmd := exec.CommandContext(ctx, "bash", "-c", script) - logger.V(1).Info("Running", "command", script) + shared.Log().V(1).Info("Running", "command", script) var stdoutBuf, stderrBuf bytes.Buffer cmd.Stdout = &stdoutBuf @@ -42,33 +44,33 @@ func bash(ctx context.Context, script string) (string, error) { stderr := stderrBuf.String() if err != nil { - logger.V(1).Info("Failed to run", "command", script, "stderr", stderr, "error", err) + shared.Log().V(1).Info("Failed to run", "command", script, "stderr", stderr, "error", err) var exitErr *exec.ExitError if errors.As(err, &exitErr) { exitErr.Stderr = stderrBuf.Bytes() } } - logger.V(1).Info("Output", "command", script, "output", stdout) + shared.Log().V(1).Info("Output", "command", script, "output", stdout) - if rec := CommandRecorderFromContext(ctx); rec != nil { + if rec := shared.CommandRecorderFromContext(ctx); rec != nil { rec.RecordCommand(script, stdout, stderr, elapsed) } return stdout, err } func CatalogReportsConditionWithoutReason(ctx context.Context, catalogUserName, conditionType, conditionStatus string) error { - sc := Scenario(ctx) + sc := shared.Scenario(ctx) catalogName, ok := sc.Catalogs[catalogUserName] if !ok { - if _, err := K8sClient(ctx, "get", "clustercatalog", catalogUserName); err != nil { + if _, err := shared.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 := shared.WaitForCondition(ctx, "clustercatalog", catalogName, conditionType, metav1.ConditionStatus(conditionStatus), nil, nil) if err == nil { - if rec := CommandRecorderFromContext(ctx); rec != nil { - out, _ := K8sClient(ctx, "get", "clustercatalog", catalogName, + if rec := shared.CommandRecorderFromContext(ctx); rec != nil { + out, _ := shared.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,40 +87,40 @@ func CatalogReportsConditionWithoutReason(ctx context.Context, catalogUserName, } func ensureCatalogPortForward(ctx context.Context) (string, error) { - sc := Scenario(ctx) + sc := shared.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) + shared.Log().V(1).Info("Catalog port-forward is dead, re-establishing", "addr", sc.CatalogAddr) resetCatalogPortForward(ctx) } - ns := ComponentNamespaces["catalogd"] + ns := shared.ComponentNamespaces["catalogd"] target, err := catalogdLeaderPod(ctx, ns) port := int32(443) if err != nil { - logger.V(1).Info("Could not resolve catalogd leader pod, falling back to service", "error", err) + shared.Log().V(1).Info("Could not resolve catalogd leader pod, falling back to service", "error", err) target = "service/catalogd-service" } else { port = 8443 } - addr, cleanup, err := PortForward(ctx, ns, target, port) + addr, cleanup, err := shared.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 - WaitFor(ctx, func() bool { + shared.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 := shared.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) @@ -131,7 +133,7 @@ func catalogdLeaderPod(ctx context.Context, ns string) (string, error) { if podName == "" { return "", fmt.Errorf("catalogd leader lease has empty holderIdentity") } - logger.Info("Resolved catalogd leader pod", "holder", holder, "pod", podName) + shared.Log().Info("Resolved catalogd leader pod", "holder", holder, "pod", podName) return fmt.Sprintf("pod/%s", podName), nil } @@ -156,7 +158,7 @@ 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 := Scenario(ctx) + sc := shared.Scenario(ctx) if sc.CatalogCleanup != nil { sc.CatalogCleanup() } @@ -185,15 +187,15 @@ func catalogCurlJq(ctx context.Context, catalogName, jqFilter string) (string, e } func CatalogContainsSomePackages(ctx context.Context, catalogName string) error { - WaitFor(ctx, func() bool { + shared.WaitFor(ctx, func() bool { out, err := catalogCurlJq(ctx, catalogName, `objects | select(.schema == "olm.package") | .name`) if err != nil { - logger.Info("Catalog query failed, retrying", "catalog", catalogName, "error", err, "stderr", stderrOutput(err)) + shared.Log().Info("Catalog query failed, retrying", "catalog", catalogName, "error", err, "stderr", shared.StderrOutput(err)) return false } if strings.TrimSpace(out) == "" { - logger.Info("Catalog returned no packages, retrying", "catalog", catalogName) + shared.Log().Info("Catalog returned no packages, retrying", "catalog", catalogName) return false } return true @@ -202,15 +204,15 @@ func CatalogContainsSomePackages(ctx context.Context, catalogName string) error } func PackageHasSomeChannels(ctx context.Context, packageName, catalogName string) error { - WaitFor(ctx, func() bool { + shared.WaitFor(ctx, func() bool { out, err := catalogCurlJq(ctx, catalogName, fmt.Sprintf(`objects | select(.schema == "olm.channel") | select(.package == "%s") | .name`, packageName)) if err != nil { - logger.Info("Catalog query failed, retrying", "catalog", catalogName, "package", packageName, "error", err, "stderr", stderrOutput(err)) + shared.Log().Info("Catalog query failed, retrying", "catalog", catalogName, "package", packageName, "error", err, "stderr", shared.StderrOutput(err)) return false } if strings.TrimSpace(out) == "" { - logger.Info("Package has no channels, retrying", "catalog", catalogName, "package", packageName) + shared.Log().Info("Package has no channels, retrying", "catalog", catalogName, "package", packageName) return false } return true @@ -219,15 +221,15 @@ func PackageHasSomeChannels(ctx context.Context, packageName, catalogName string } func PackageHasSomeBundles(ctx context.Context, packageName, catalogName string) error { - WaitFor(ctx, func() bool { + shared.WaitFor(ctx, func() bool { out, err := catalogCurlJq(ctx, catalogName, fmt.Sprintf(`objects | select(.schema == "olm.bundle") | select(.package == "%s") | .name`, packageName)) if err != nil { - logger.Info("Catalog query failed, retrying", "catalog", catalogName, "package", packageName, "error", err, "stderr", stderrOutput(err)) + shared.Log().Info("Catalog query failed, retrying", "catalog", catalogName, "package", packageName, "error", err, "stderr", shared.StderrOutput(err)) return false } if strings.TrimSpace(out) == "" { - logger.Info("Package has no bundles, retrying", "catalog", catalogName, "package", packageName) + shared.Log().Info("Package has no bundles, retrying", "catalog", catalogName, "package", packageName) return false } return true @@ -236,8 +238,8 @@ func PackageHasSomeBundles(ctx context.Context, packageName, catalogName string) } func RolebindingsReferenceServiceAccount(ctx context.Context, rbNamespace, saName, saNamespace string) error { - saNamespace = SubstituteScenarioVars(saNamespace, Scenario(ctx)) - out, err := K8sClient(ctx, "get", "rolebindings", "-n", rbNamespace, "-o", "json") + saNamespace = shared.SubstituteScenarioVars(saNamespace, shared.Scenario(ctx)) + out, err := shared.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 +263,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 := CommandRecorderFromContext(ctx); rec != nil { - subjectsOut, _ := K8sClient(ctx, "get", "rolebinding", rb.Metadata.Name, "-n", rbNamespace, + if rec := shared.CommandRecorderFromContext(ctx); rec != nil { + subjectsOut, _ := shared.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 +279,10 @@ func RolebindingsReferenceServiceAccount(ctx context.Context, rbNamespace, saNam } func PodHasContainerCount(ctx context.Context, podName string, expected int) error { - sc := Scenario(ctx) + sc := shared.Scenario(ctx) var actual int - WaitFor(ctx, func() bool { - out, err := K8sClient(ctx, "get", "pod", podName, "-n", sc.Namespace, "-o", "jsonpath={.spec.containers[*].name}") + shared.WaitFor(ctx, func() bool { + out, err := shared.K8sClient(ctx, "get", "pod", podName, "-n", sc.Namespace, "-o", "jsonpath={.spec.containers[*].name}") if err != nil { return false } @@ -288,8 +290,8 @@ func PodHasContainerCount(ctx context.Context, podName string, expected int) err actual = len(names) return actual == expected }) - if rec := CommandRecorderFromContext(ctx); rec != nil { - out, _ := K8sClient(ctx, "get", "pod", podName, "-n", sc.Namespace, "-o", "jsonpath={.spec.containers[*].name}") + if rec := shared.CommandRecorderFromContext(ctx); rec != nil { + out, _ := shared.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), out+"\n", "", diff --git a/test/internal/e2e/steps/asciicast_recorder_test.go b/test/internal/e2e/steps/asciicast_recorder_test.go index 7f9566c27e..d697b25e37 100644 --- a/test/internal/e2e/steps/asciicast_recorder_test.go +++ b/test/internal/e2e/steps/asciicast_recorder_test.go @@ -2,21 +2,48 @@ package steps_test import ( "context" + "os" + "os/exec" + "path/filepath" "testing" + "github.com/spf13/pflag" "github.com/stretchr/testify/require" + platformsteps "github.com/operator-framework/operator-controller/test/internal/e2e/platform/steps" "github.com/operator-framework/operator-controller/test/internal/e2e/steps" ) func TestWithRecorder(t *testing.T) { - parent := steps.WithCommandRecorder(context.Background(), &steps.AsciiCastRecorder{}) - recorder := &steps.AsciiCastRecorder{} + parent := steps.WithCommandRecorder(context.Background(), &platformsteps.AsciiCastRecorder{}) + recorder := &platformsteps.AsciiCastRecorder{} - ctx := steps.WithRecorder(parent, recorder) + ctx := platformsteps.WithRecorder(parent, recorder) - require.Same(t, recorder, steps.RecorderFromContext(ctx)) + require.Same(t, recorder, platformsteps.RecorderFromContext(ctx)) require.Same(t, recorder, steps.CommandRecorderFromContext(ctx)) require.NotSame(t, recorder, steps.CommandRecorderFromContext(parent)) - require.Nil(t, steps.RecorderFromContext(parent)) + require.Nil(t, platformsteps.RecorderFromContext(parent)) +} + +func TestSharedCommandsReachPlatformRecorder(t *testing.T) { + cli, err := exec.LookPath("echo") + require.NoError(t, err) + original := pflag.Lookup("k8s.cli").Value.String() + require.NoError(t, pflag.Set("k8s.cli", cli)) + t.Cleanup(func() { require.NoError(t, pflag.Set("k8s.cli", original)) }) + dir := t.TempDir() + recorder := platformsteps.NewAsciiCastRecorder("Shared command recording", dir) + ctx := platformsteps.WithRecorder(context.Background(), recorder) + recorder.BeginStep("List pods") + _, err = steps.K8sClient(ctx, "get", "pods") + require.NoError(t, err) + recorder.CommitStep() + require.NoError(t, recorder.Stop()) + content, err := os.ReadFile(filepath.Join(dir, "shared-command-recording.cast")) + require.NoError(t, err) + require.Contains(t, string(content), "get pods") + require.Contains(t, string(content), "List pods") + require.Contains(t, string(content), `"version":2`) + require.Nil(t, steps.CommandRecorderFromContext(context.Background())) }