From cfe7ffef1bef89f517294c296bdb0a615d293906 Mon Sep 17 00:00:00 2001 From: Dominik Hanak Date: Thu, 1 Oct 2026 10:49:39 +0200 Subject: [PATCH 1/6] Fix incosistencies on plugin test enhacement --- packages/kn-plugin-workflow/Makefile | 38 +++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/packages/kn-plugin-workflow/Makefile b/packages/kn-plugin-workflow/Makefile index f8d7ba0d367..2b589cdf855 100644 --- a/packages/kn-plugin-workflow/Makefile +++ b/packages/kn-plugin-workflow/Makefile @@ -47,15 +47,21 @@ KUBE_RBAC_PROXY_DST := gcr.io/kubebuilder/kube-rbac-proxy:v0.13.0 # When CATALOG_INDEX_IMAGE is set, kind-preload-images will also: # - Pull OPERATOR_IMAGE (proxy ref) and retag it as OPERATOR_IMAGE_RHT (registry.redhat.io ref # embedded in the CSV), then load it into kind so containerd resolves it locally by digest. +# - Pull OPERATOR_BUNDLE_IMAGE (proxy ref) and load it into kind tagged as OPERATOR_BUNDLE_IMAGE_STAGE +# (registry.stage.redhat.io ref embedded in the IIB catalog), so OLM bundle-unpack jobs resolve +# the image locally without hitting the stage registry. # - Pull KUBE_RBAC_PROXY_PRODUCT (proxy ref) and retag it as KUBE_RBAC_PROXY_PRODUCT_RHT # (registry.redhat.io ref embedded in the CSV), then load it into kind. # -# OPERATOR_IMAGE - proxy-accessible source image (registry-proxy.engineering.redhat.com/...) -# OPERATOR_IMAGE_RHT - registry.redhat.io target ref (must match what the CSV embeds) -# KUBE_RBAC_PROXY_PRODUCT - proxy-accessible source image (registry-proxy.engineering.redhat.com/...) +# OPERATOR_IMAGE - proxy-accessible source image (registry-proxy.engineering.redhat.com/...) +# OPERATOR_IMAGE_RHT - registry.redhat.io target ref (must match what the CSV embeds) +# OPERATOR_BUNDLE_IMAGE - proxy-accessible source image (registry-proxy.engineering.redhat.com/...) +# OPERATOR_BUNDLE_IMAGE_STAGE - registry.stage.redhat.io target ref (must match what the IIB catalog embeds) +# KUBE_RBAC_PROXY_PRODUCT - proxy-accessible source image (registry-proxy.engineering.redhat.com/...) # KUBE_RBAC_PROXY_PRODUCT_RHT - registry.redhat.io target ref (must match what the CSV embeds) CATALOG_INDEX_IMAGE ?= OPERATOR_BUNDLE_IMAGE ?= +OPERATOR_BUNDLE_IMAGE_STAGE ?= OPERATOR_IMAGE ?= OPERATOR_IMAGE_RHT ?= KUBE_RBAC_PROXY_PRODUCT ?= @@ -147,10 +153,12 @@ go-test-e2e-report: .PHONY: kind-preload-images kind-preload-images: - @echo "Preloading kube-rbac-proxy image into kind..." - docker pull $(KUBE_RBAC_PROXY_SRC) - docker tag $(KUBE_RBAC_PROXY_SRC) $(KUBE_RBAC_PROXY_DST) - kind load docker-image --name $(KIND_CLUSTER) $(KUBE_RBAC_PROXY_DST) + @if [ -n "$(KUBE_RBAC_PROXY_SRC)" ] && [ -n "$(KUBE_RBAC_PROXY_DST)" ]; then \ + echo "Preloading kube-rbac-proxy image into kind..."; \ + docker pull $(KUBE_RBAC_PROXY_SRC); \ + docker tag $(KUBE_RBAC_PROXY_SRC) $(KUBE_RBAC_PROXY_DST); \ + kind load docker-image --name $(KIND_CLUSTER) $(KUBE_RBAC_PROXY_DST); \ + fi @if [ -n "$(CATALOG_INDEX_IMAGE)" ]; then \ echo "Preloading index image $(CATALOG_INDEX_IMAGE) into kind..."; \ docker pull $(CATALOG_INDEX_IMAGE); \ @@ -172,6 +180,22 @@ kind-preload-images: fi; \ rm -f /tmp/operator-image.tar; \ fi + @if [ -n "$(OPERATOR_BUNDLE_IMAGE)" ] && [ -n "$(OPERATOR_BUNDLE_IMAGE_STAGE)" ]; then \ + echo "Preloading bundle image $(OPERATOR_BUNDLE_IMAGE) -> $(OPERATOR_BUNDLE_IMAGE_STAGE) into kind..."; \ + docker pull $(OPERATOR_BUNDLE_IMAGE); \ + docker save $(OPERATOR_BUNDLE_IMAGE) -o /tmp/bundle-image.tar; \ + BUNDLE_STAGE_BASE=$$(echo "$(OPERATOR_BUNDLE_IMAGE_STAGE)" | cut -d'@' -f1); \ + BUNDLE_STAGE_DIGEST=$$(echo "$(OPERATOR_BUNDLE_IMAGE_STAGE)" | cut -d'@' -f2); \ + docker exec -i $(KIND_CLUSTER)-control-plane ctr --namespace k8s.io images import --digests --base-name $$BUNDLE_STAGE_BASE - < /tmp/bundle-image.tar; \ + MANIFEST_DIGEST=$$(docker exec $(KIND_CLUSTER)-control-plane ctr --namespace k8s.io images ls 2>/dev/null | grep "$$BUNDLE_STAGE_BASE@" | grep "manifest.v1" | awk '{print $$1}' | head -1 | cut -d'@' -f2); \ + if [ -n "$$MANIFEST_DIGEST" ] && [ "$$MANIFEST_DIGEST" != "$$BUNDLE_STAGE_DIGEST" ]; then \ + echo "Adding digest alias $$BUNDLE_STAGE_DIGEST -> $$MANIFEST_DIGEST in containerd..."; \ + docker exec $(KIND_CLUSTER)-control-plane ctr --namespace k8s.io images tag \ + "$$BUNDLE_STAGE_BASE@$$MANIFEST_DIGEST" \ + "$$BUNDLE_STAGE_BASE@$$BUNDLE_STAGE_DIGEST"; \ + fi; \ + rm -f /tmp/bundle-image.tar; \ + fi @if [ -n "$(KUBE_RBAC_PROXY_PRODUCT)" ] && [ -n "$(KUBE_RBAC_PROXY_PRODUCT_RHT)" ]; then \ echo "Preloading product kube-rbac-proxy $(KUBE_RBAC_PROXY_PRODUCT) -> $(KUBE_RBAC_PROXY_PRODUCT_RHT) into kind..."; \ docker pull $(KUBE_RBAC_PROXY_PRODUCT); \ From aac11827515ea634fc0209fcc258dd09e785c18a Mon Sep 17 00:00:00 2001 From: Dominik Hanak Date: Fri, 2 Oct 2026 11:36:00 +0200 Subject: [PATCH 2/6] Stabilization of kn-workflow-plugin tests for product build Fixes install and unninstal. Ensures no updates are happening if there is newer operator - manual aproval. Fixes kind-preload-images step --- packages/kn-plugin-workflow/Makefile | 39 ++++-- .../registry.stage.redhat.io/hosts.toml | 6 + .../kn-plugin-workflow/e2e-tests/main_test.go | 2 + .../e2e-tests/operator_helper.go | 118 +++++++++++++++++- .../e2e-tests/quarkus_run_test.go | 61 ++++++++- .../kn-plugin-workflow/e2e-tests/test_env.go | 3 + 6 files changed, 212 insertions(+), 17 deletions(-) create mode 100644 packages/kn-plugin-workflow/e2e-tests/containerd-certs.d/registry.stage.redhat.io/hosts.toml diff --git a/packages/kn-plugin-workflow/Makefile b/packages/kn-plugin-workflow/Makefile index 2b589cdf855..8e3f0ff05b4 100644 --- a/packages/kn-plugin-workflow/Makefile +++ b/packages/kn-plugin-workflow/Makefile @@ -40,6 +40,9 @@ KIND_CLUSTER ?= kind KUBE_RBAC_PROXY_SRC := quay.io/brancz/kube-rbac-proxy:v0.22.0 KUBE_RBAC_PROXY_DST := gcr.io/kubebuilder/kube-rbac-proxy:v0.13.0 +KUBE_RBAC_PROXY_SRC := quay.io/brancz/kube-rbac-proxy:v0.13.1 +KUBE_RBAC_PROXY_DST := gcr.io/kubebuilder/kube-rbac-proxy:v0.13.1 + # Optional: set the following variables to install the operator from a custom OLM CatalogSource # (product/OSL builds) instead of the public operatorhubio-catalog. # Only CATALOG_INDEX_IMAGE is required to activate the custom-catalog install path. @@ -135,6 +138,9 @@ create-cluster: install-kind install-operator-framework: curl -sL https://github.com/operator-framework/operator-lifecycle-manager/releases/download/$(OLM_VERSION)/install.sh | bash -s $(OLM_VERSION) +# Optional: set TEST_SKIP to a regex to skip matching tests, e.g. TEST_SKIP=Quarkus +TEST_SKIP ?= + .PHONY: go-test-e2e go-test-e2e: rm -rf dist-tests-e2e @@ -142,7 +148,9 @@ go-test-e2e: CATALOG_INDEX_IMAGE=$(CATALOG_INDEX_IMAGE) \ OPERATOR_BUNDLE_IMAGE=$(OPERATOR_BUNDLE_IMAGE) \ OPERATOR_IMAGE=$(OPERATOR_IMAGE) \ - go test -v ./e2e-tests/... -tags e2e_tests -run TestQuarkusRunCommand -timeout 20m 2>&1 | tee ./dist-tests-e2e/go-test-output-e2e.txt + go test -v ./e2e-tests/... -tags e2e_tests -timeout 20m \ + $(if $(TEST_SKIP),-skip $(TEST_SKIP)) \ + 2>&1 | tee ./dist-tests-e2e/go-test-output-e2e.txt .PHONY: go-test-e2e-report go-test-e2e-report: @@ -154,15 +162,18 @@ go-test-e2e-report: .PHONY: kind-preload-images kind-preload-images: @if [ -n "$(KUBE_RBAC_PROXY_SRC)" ] && [ -n "$(KUBE_RBAC_PROXY_DST)" ]; then \ - echo "Preloading kube-rbac-proxy image into kind..."; \ + echo "Preloading kube-rbac-proxy image $(KUBE_RBAC_PROXY_SRC) -> $(KUBE_RBAC_PROXY_DST) into kind..."; \ docker pull $(KUBE_RBAC_PROXY_SRC); \ - docker tag $(KUBE_RBAC_PROXY_SRC) $(KUBE_RBAC_PROXY_DST); \ - kind load docker-image --name $(KIND_CLUSTER) $(KUBE_RBAC_PROXY_DST); \ + docker save $(KUBE_RBAC_PROXY_SRC) -o /tmp/kube-rbac-proxy.tar; \ + docker exec -i $(KIND_CLUSTER)-control-plane ctr --namespace k8s.io images import --digests --base-name $(KUBE_RBAC_PROXY_DST) - < /tmp/kube-rbac-proxy.tar; \ + rm -f /tmp/kube-rbac-proxy.tar; \ fi @if [ -n "$(CATALOG_INDEX_IMAGE)" ]; then \ echo "Preloading index image $(CATALOG_INDEX_IMAGE) into kind..."; \ docker pull $(CATALOG_INDEX_IMAGE); \ - kind load docker-image --name $(KIND_CLUSTER) $(CATALOG_INDEX_IMAGE); \ + docker save $(CATALOG_INDEX_IMAGE) -o /tmp/catalog-index-image.tar; \ + docker exec -i $(KIND_CLUSTER)-control-plane ctr --namespace k8s.io images import --digests - < /tmp/catalog-index-image.tar; \ + rm -f /tmp/catalog-index-image.tar; \ fi @if [ -n "$(OPERATOR_IMAGE)" ] && [ -n "$(OPERATOR_IMAGE_RHT)" ]; then \ echo "Preloading operator image $(OPERATOR_IMAGE) -> $(OPERATOR_IMAGE_RHT) into kind..."; \ @@ -199,8 +210,16 @@ kind-preload-images: @if [ -n "$(KUBE_RBAC_PROXY_PRODUCT)" ] && [ -n "$(KUBE_RBAC_PROXY_PRODUCT_RHT)" ]; then \ echo "Preloading product kube-rbac-proxy $(KUBE_RBAC_PROXY_PRODUCT) -> $(KUBE_RBAC_PROXY_PRODUCT_RHT) into kind..."; \ docker pull $(KUBE_RBAC_PROXY_PRODUCT); \ - kind load docker-image --name $(KIND_CLUSTER) $(KUBE_RBAC_PROXY_PRODUCT); \ - KUBE_RBAC_RHT_NAME=$$(echo "$(KUBE_RBAC_PROXY_PRODUCT_RHT)" | cut -d'@' -f1); \ - docker tag $(KUBE_RBAC_PROXY_PRODUCT) $$KUBE_RBAC_RHT_NAME; \ - kind load docker-image --name $(KIND_CLUSTER) $$KUBE_RBAC_RHT_NAME; \ - fi \ No newline at end of file + docker save $(KUBE_RBAC_PROXY_PRODUCT) -o /tmp/kube-rbac-proxy-product.tar; \ + KUBE_RBAC_RHT_BASE=$$(echo "$(KUBE_RBAC_PROXY_PRODUCT_RHT)" | cut -d'@' -f1); \ + KUBE_RBAC_RHT_DIGEST=$$(echo "$(KUBE_RBAC_PROXY_PRODUCT_RHT)" | cut -d'@' -f2); \ + docker exec -i $(KIND_CLUSTER)-control-plane ctr --namespace k8s.io images import --digests --base-name $$KUBE_RBAC_RHT_BASE - < /tmp/kube-rbac-proxy-product.tar; \ + MANIFEST_DIGEST=$$(docker exec $(KIND_CLUSTER)-control-plane ctr --namespace k8s.io images ls 2>/dev/null | grep "$$KUBE_RBAC_RHT_BASE@" | grep "manifest.v1" | awk '{print $$1}' | head -1 | cut -d'@' -f2); \ + if [ -n "$$MANIFEST_DIGEST" ] && [ "$$MANIFEST_DIGEST" != "$$KUBE_RBAC_RHT_DIGEST" ]; then \ + echo "Adding digest alias $$KUBE_RBAC_RHT_DIGEST -> $$MANIFEST_DIGEST in containerd..."; \ + docker exec $(KIND_CLUSTER)-control-plane ctr --namespace k8s.io images tag \ + "$$KUBE_RBAC_RHT_BASE@$$MANIFEST_DIGEST" \ + "$$KUBE_RBAC_RHT_BASE@$$KUBE_RBAC_RHT_DIGEST"; \ + fi; \ + rm -f /tmp/kube-rbac-proxy-product.tar; \ + fi diff --git a/packages/kn-plugin-workflow/e2e-tests/containerd-certs.d/registry.stage.redhat.io/hosts.toml b/packages/kn-plugin-workflow/e2e-tests/containerd-certs.d/registry.stage.redhat.io/hosts.toml new file mode 100644 index 00000000000..4e9fa8388c2 --- /dev/null +++ b/packages/kn-plugin-workflow/e2e-tests/containerd-certs.d/registry.stage.redhat.io/hosts.toml @@ -0,0 +1,6 @@ +server = "https://registry.stage.redhat.io" + +[host."https://registry-proxy.engineering.redhat.com"] + capabilities = ["pull", "resolve"] + skip_verify = true + override_path = true diff --git a/packages/kn-plugin-workflow/e2e-tests/main_test.go b/packages/kn-plugin-workflow/e2e-tests/main_test.go index 58bc6253667..5e292fb8e3e 100644 --- a/packages/kn-plugin-workflow/e2e-tests/main_test.go +++ b/packages/kn-plugin-workflow/e2e-tests/main_test.go @@ -59,10 +59,12 @@ func TestMain(m *testing.M) { CatalogIndexImage = os.Getenv("CATALOG_INDEX_IMAGE") OperatorBundleImage = os.Getenv("OPERATOR_BUNDLE_IMAGE") OperatorImage = os.Getenv("OPERATOR_IMAGE") + OperatorStartingCSV = os.Getenv("OPERATOR_STARTING_CSV") fmt.Printf("🔧 CATALOG_INDEX_IMAGE: %s\n", orNotSet(CatalogIndexImage)) fmt.Printf("🔧 OPERATOR_BUNDLE_IMAGE: %s\n", orNotSet(OperatorBundleImage)) fmt.Printf("🔧 OPERATOR_IMAGE: %s\n", orNotSet(OperatorImage)) + fmt.Printf("🔧 OPERATOR_STARTING_CSV: %s\n", orNotSet(OperatorStartingCSV)) if CatalogIndexImage != "" { fmt.Println("🔧 Custom catalog mode: enabled (product build)") } else { diff --git a/packages/kn-plugin-workflow/e2e-tests/operator_helper.go b/packages/kn-plugin-workflow/e2e-tests/operator_helper.go index 3c4ff596fcf..d394d1b8e05 100644 --- a/packages/kn-plugin-workflow/e2e-tests/operator_helper.go +++ b/packages/kn-plugin-workflow/e2e-tests/operator_helper.go @@ -42,11 +42,23 @@ const ( ) var catalogSourcesGVR = schema.GroupVersionResource{ - Group: "operators.coreos.com", - Version: "v1alpha1", + Group: "operators.coreos.com", + Version: "v1alpha1", Resource: "catalogsources", } +var installPlansGVR = schema.GroupVersionResource{ + Group: "operators.coreos.com", + Version: "v1alpha1", + Resource: "installplans", +} + +var subscriptionsGVR = schema.GroupVersionResource{ + Group: "operators.coreos.com", + Version: "v1alpha1", + Resource: "subscriptions", +} + var operatorManager = common.NewOperatorManager("") func orNotSet(v string) string { @@ -81,10 +93,14 @@ func installOperator() { waitForCatalogSourceReady(customCatalogName, customCatalogNamespace) - if err := operatorManager.InstallOperatorFromCatalog(metadata.LogicOperatorName, "stable", customCatalogName, customCatalogNamespace); err != nil { - fmt.Println("Failed to install operator from custom catalog:", err) + if err := createPinnedSubscription(metadata.LogicOperatorName, "stable", customCatalogName, customCatalogNamespace, OperatorStartingCSV); err != nil { + fmt.Println("Failed to create subscription from custom catalog:", err) os.Exit(1) } + + if OperatorStartingCSV != "" { + approveInstallPlan(metadata.LogicOperatorName, OperatorStartingCSV) + } return } @@ -239,3 +255,97 @@ func uninstallOperator() { os.Exit(1) } } + +// createPinnedSubscription creates an OLM Subscription for the given operator. When startingCSV +// is non-empty it pins the subscription to that exact CSV and sets installPlanApproval to Manual +// so OLM never auto-upgrades to a newer version during the test run. +// This is intentionally kept in the test helper (not in pkg/common) because pinning is a +// test-only concern: production installs should always follow the channel head. +func createPinnedSubscription(operatorName, channel, source, sourceNamespace, startingCSV string) error { + dynamicClient, err := k8sclient.DynamicClient() + if err != nil { + return fmt.Errorf("failed to create dynamic client: %v", err) + } + + spec := map[string]interface{}{ + "channel": channel, + "name": operatorName, + "source": source, + "sourceNamespace": sourceNamespace, + } + if startingCSV != "" { + spec["startingCSV"] = startingCSV + spec["installPlanApproval"] = "Manual" + fmt.Printf("🔧 Pinning subscription to CSV %q (installPlanApproval: Manual)\n", startingCSV) + } + + sub := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "operators.coreos.com/v1alpha1", + "kind": "Subscription", + "metadata": map[string]interface{}{ + "name": operatorName, + "namespace": "operators", + }, + "spec": spec, + }, + } + + _, err = dynamicClient.Resource(subscriptionsGVR).Namespace("operators").Create( + context.Background(), sub, v1.CreateOptions{}) + if err != nil { + return fmt.Errorf("failed to create subscription %q: %v", operatorName, err) + } + fmt.Println("✅ Subscription created successfully") + return nil +} + +// approveInstallPlan waits for an InstallPlan referencing the given CSV to appear in the +// subscription's namespace, then patches it to approved=true so that a Manual-approval +// subscription actually installs the operator. +func approveInstallPlan(subscriptionName, csvName string) { + dynamicClient, err := k8sclient.DynamicClient() + if err != nil { + fmt.Println("Failed to create dynamic client:", err) + os.Exit(1) + } + + fmt.Printf("⏳ Waiting for InstallPlan for CSV %q to appear...\n", csvName) + timeoutCh := time.After(5 * time.Minute) + + for { + select { + case <-timeoutCh: + fmt.Printf("Timeout waiting for InstallPlan for CSV %q\n", csvName) + os.Exit(1) + default: + } + + plans, err := dynamicClient.Resource(installPlansGVR).Namespace("operators").List( + context.Background(), v1.ListOptions{}) + if err != nil { + time.Sleep(5 * time.Second) + continue + } + + for _, plan := range plans.Items { + clusterServiceVersionNames, _, _ := unstructured.NestedStringSlice(plan.Object, "spec", "clusterServiceVersionNames") + for _, name := range clusterServiceVersionNames { + if name == csvName { + planName := plan.GetName() + fmt.Printf(" - Approving InstallPlan %q for CSV %q...\n", planName, csvName) + plan.Object["spec"].(map[string]interface{})["approved"] = true + _, err := dynamicClient.Resource(installPlansGVR).Namespace("operators").Update( + context.Background(), &plan, v1.UpdateOptions{}) + if err != nil { + fmt.Printf("Failed to approve InstallPlan %q: %v\n", planName, err) + os.Exit(1) + } + fmt.Printf(" - ✅ InstallPlan %q approved\n", planName) + return + } + } + } + time.Sleep(5 * time.Second) + } +} diff --git a/packages/kn-plugin-workflow/e2e-tests/quarkus_run_test.go b/packages/kn-plugin-workflow/e2e-tests/quarkus_run_test.go index cbeeb2ac967..29f1bed57f3 100644 --- a/packages/kn-plugin-workflow/e2e-tests/quarkus_run_test.go +++ b/packages/kn-plugin-workflow/e2e-tests/quarkus_run_test.go @@ -22,10 +22,12 @@ package e2e_tests import ( + "bufio" "fmt" "os" "os/exec" "path/filepath" + "strings" "sync" "testing" "time" @@ -90,6 +92,9 @@ func RunQuarkusRunTest(t *testing.T, cfgTestInputPrepareQuarkusCreateRun CfgTest // Create and build the quarkus project projectName := RunQuarkusCreateTest(t, cfgTestInputPrepareQuarkusCreateRun) + if t.Failed() { + return projectName + } projectDir := filepath.Join(TempTestsPath, projectName) err = os.Chdir(projectDir) @@ -100,11 +105,15 @@ func RunQuarkusRunTest(t *testing.T, cfgTestInputPrepareQuarkusCreateRun CfgTest var wg sync.WaitGroup wg.Add(1) - // Run the `quarkus run` command + // runFailed is closed by the goroutine if the run process outputs a fatal error line. + runFailed := make(chan string, 1) + processDone := make(chan error, 1) + + // Run the `quarkus run` command, watching stdout for early-failure markers. go func() { defer wg.Done() - _, err = ExecuteKnWorkflowQuarkusWithCmd(cmd, transformQuarkusRunCmdCfgToArgs(test.input)...) - require.Truef(t, err == nil || IsSignalInterrupt(err), "Expected nil error or signal interrupt, got %v", err) + _, runErr := executeKnWorkflowQuarkusWithCmdAndErrWatch(cmd, runFailed, transformQuarkusRunCmdCfgToArgs(test.input)...) + processDone <- runErr }() // Check if the project is successfully run and accessible within a specified time limit. @@ -117,6 +126,13 @@ func RunQuarkusRunTest(t *testing.T, cfgTestInputPrepareQuarkusCreateRun CfgTest select { case <-ready: cmd.Process.Signal(os.Interrupt) + case runErr := <-processDone: + if runErr != nil && !IsSignalInterrupt(runErr) { + t.Fatalf("quarkus run process exited with error before becoming ready: %v", runErr) + } + case errLine := <-runFailed: + cmd.Process.Signal(os.Interrupt) + t.Fatalf("quarkus run process reported a fatal error before becoming ready: %s", errLine) case <-time.After(timeout): t.Fatalf("Test case timed out after %s. The project was not ready within the specified time.", timeout) cmd.Process.Signal(os.Interrupt) @@ -126,3 +142,42 @@ func RunQuarkusRunTest(t *testing.T, cfgTestInputPrepareQuarkusCreateRun CfgTest return projectName } + +// executeKnWorkflowQuarkusWithCmdAndErrWatch runs `kn-workflow quarkus ` and streams stdout. +// If a line matching a known fatal-error pattern is seen, it sends that line to errCh so the +// caller can fail fast without waiting for the process to exit on its own. +func executeKnWorkflowQuarkusWithCmdAndErrWatch(cmd *exec.Cmd, errCh chan<- string, args ...string) (string, error) { + newArgs := append([]string{"quarkus"}, args...) + cmd.Args = append([]string{cmd.Path}, newArgs...) + + stdoutPipe, err := cmd.StdoutPipe() + if err != nil { + return "", fmt.Errorf("failed to create stdout pipe: %w", err) + } + cmd.Stderr = os.Stderr + + if err := cmd.Start(); err != nil { + return "", fmt.Errorf("failed to start process: %w", err) + } + + var buf strings.Builder + scanner := bufio.NewScanner(stdoutPipe) + for scanner.Scan() { + line := scanner.Text() + buf.WriteString(line + "\n") + if *TestPrintCmdOutput { + fmt.Println(line) + } + // Detect the kn-workflow error prefix — means Maven failed and the process + // is stuck in ReadyCheck; signal failure immediately rather than timing out. + if strings.HasPrefix(line, "❌ ERROR:") { + select { + case errCh <- line: + default: + } + } + } + + runErr := cmd.Wait() + return buf.String(), runErr +} diff --git a/packages/kn-plugin-workflow/e2e-tests/test_env.go b/packages/kn-plugin-workflow/e2e-tests/test_env.go index 937c3b08a8b..511de1f9672 100644 --- a/packages/kn-plugin-workflow/e2e-tests/test_env.go +++ b/packages/kn-plugin-workflow/e2e-tests/test_env.go @@ -27,3 +27,6 @@ var testPrintCmdOutput = false var CatalogIndexImage string var OperatorBundleImage string var OperatorImage string +// OperatorStartingCSV pins the subscription to a specific CSV (e.g. "logic-operator.v1.37.3") +// so OLM never attempts an automatic upgrade during the test run. +var OperatorStartingCSV string From 5b457a18bdcc9e177cd4b02fc1835644cbdf8d9f Mon Sep 17 00:00:00 2001 From: Dominik Hanak Date: Fri, 2 Oct 2026 12:30:23 +0200 Subject: [PATCH 3/6] Collect logs of operator installaton --- .../e2e-tests/operator_helper.go | 282 ++++++++++++------ 1 file changed, 187 insertions(+), 95 deletions(-) diff --git a/packages/kn-plugin-workflow/e2e-tests/operator_helper.go b/packages/kn-plugin-workflow/e2e-tests/operator_helper.go index d394d1b8e05..37f04f18f55 100644 --- a/packages/kn-plugin-workflow/e2e-tests/operator_helper.go +++ b/packages/kn-plugin-workflow/e2e-tests/operator_helper.go @@ -23,17 +23,22 @@ package e2e_tests import ( "context" + "encoding/json" "fmt" + "io" "os" + "path/filepath" "time" "github.com/apache/incubator-kie-tools/packages/kn-plugin-workflow/pkg/command/operator" "github.com/apache/incubator-kie-tools/packages/kn-plugin-workflow/pkg/common" "github.com/apache/incubator-kie-tools/packages/kn-plugin-workflow/pkg/common/k8sclient" "github.com/apache/incubator-kie-tools/packages/kn-plugin-workflow/pkg/metadata" + corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/kubernetes" ) const ( @@ -59,6 +64,12 @@ var subscriptionsGVR = schema.GroupVersionResource{ Resource: "subscriptions", } +var clusterServiceVersionsGVR = schema.GroupVersionResource{ + Group: "operators.coreos.com", + Version: "v1alpha1", + Resource: "clusterserviceversions", +} + var operatorManager = common.NewOperatorManager("") func orNotSet(v string) string { @@ -68,17 +79,33 @@ func orNotSet(v string) string { return v } +// operatorLogsDir is the directory where operator diagnostic snapshots are written. +// go test runs with cwd = the package source dir (e2e-tests/), so we go up one level +// to reach the package root and then into dist-tests-e2e — keeping all test artefacts +// together for CI collection. +const operatorLogsDir = "../../dist-tests-e2e/operator-logs" + func InstallOperator() { - installOperator() - waitForOperatorReady() + if err := installOperator(); err != nil { + fmt.Println("❌ Operator installation failed:", err) + collectOperatorLogs("install-failure") + os.Exit(1) + } + if err := waitForOperatorReady(); err != nil { + fmt.Println("❌ Operator did not become ready:", err) + collectOperatorLogs("install-failure") + os.Exit(1) + } checkOperatorInstalled() + collectOperatorLogs("post-install") } func UninstallOperator() { + collectOperatorLogs("pre-uninstall") uninstallOperator() } -func installOperator() { +func installOperator() error { if CatalogIndexImage != "" { // Product/OSL build: install from the custom CatalogSource. fmt.Println("🚀 Installing operator from custom CatalogSource (product build)...") @@ -87,123 +114,100 @@ func installOperator() { fmt.Printf(" OPERATOR_IMAGE: %s\n", orNotSet(OperatorImage)) if err := operatorManager.CreateCustomCatalogSource(customCatalogName, customCatalogNamespace, CatalogIndexImage); err != nil { - fmt.Println("Failed to create custom CatalogSource:", err) - os.Exit(1) + return fmt.Errorf("failed to create custom CatalogSource: %w", err) } - waitForCatalogSourceReady(customCatalogName, customCatalogNamespace) + if err := waitForCatalogSourceReady(customCatalogName, customCatalogNamespace); err != nil { + return fmt.Errorf("CatalogSource not ready: %w", err) + } if err := createPinnedSubscription(metadata.LogicOperatorName, "stable", customCatalogName, customCatalogNamespace, OperatorStartingCSV); err != nil { - fmt.Println("Failed to create subscription from custom catalog:", err) - os.Exit(1) + return fmt.Errorf("failed to create subscription from custom catalog: %w", err) } if OperatorStartingCSV != "" { - approveInstallPlan(metadata.LogicOperatorName, OperatorStartingCSV) + if err := approveInstallPlan(metadata.LogicOperatorName, OperatorStartingCSV); err != nil { + return fmt.Errorf("failed to approve InstallPlan: %w", err) + } } - return + return nil } // Default path: install from the public catalog via the standard CLI command. var install = operator.NewInstallOperatorCommand() - err := install.Execute() - if err != nil { - fmt.Println("Failed to install operator:", err) - os.Exit(1) + if err := install.Execute(); err != nil { + return fmt.Errorf("failed to install operator: %w", err) } + return nil } -func waitForCatalogSourceReady(name, namespace string) { +func waitForCatalogSourceReady(name, namespace string) error { dynamicClient, err := k8sclient.DynamicClient() if err != nil { - fmt.Println("Failed to create dynamic client:", err) - os.Exit(1) + return fmt.Errorf("failed to create dynamic client: %w", err) } - ready := make(chan bool) - defer close(ready) timeoutCh := time.After(5 * time.Minute) + for { + select { + case <-timeoutCh: + return fmt.Errorf("timeout waiting for CatalogSource %q to be ready", name) + default: + } - go func() { - for { - select { - case <-timeoutCh: - fmt.Printf("Timeout waiting for CatalogSource %q to be ready\n", name) - os.Exit(1) - default: - cs, err := dynamicClient.Resource(catalogSourcesGVR).Namespace(namespace).Get( - context.Background(), name, v1.GetOptions{}) - if err != nil { - time.Sleep(5 * time.Second) - continue - } - - state, _, _ := unstructured.NestedString(cs.Object, - "status", "connectionState", "lastObservedState") - if state == "READY" { - ready <- true - return - } - time.Sleep(5 * time.Second) - } + cs, err := dynamicClient.Resource(catalogSourcesGVR).Namespace(namespace).Get( + context.Background(), name, v1.GetOptions{}) + if err != nil { + time.Sleep(5 * time.Second) + continue } - }() - select { - case <-ready: - fmt.Printf(" - ✅ CatalogSource %q is ready\n", name) + state, _, _ := unstructured.NestedString(cs.Object, + "status", "connectionState", "lastObservedState") + if state == "READY" { + fmt.Printf(" - ✅ CatalogSource %q is ready\n", name) + return nil + } + time.Sleep(5 * time.Second) } } -func waitForOperatorReady() { - deployed := make(chan bool) - defer close(deployed) +func waitForOperatorReady() error { timeoutCh := time.After(5 * time.Minute) + for { + select { + case <-timeoutCh: + return fmt.Errorf("timeout waiting for operator to become ready") + default: + } - go func() { - for { - select { - case <-timeoutCh: - fmt.Println("Timeout waiting for operator to be ready") - os.Exit(1) - default: - resources, err := operatorManager.ListOperatorResources() - if err != nil { - fmt.Println("Failed to list operator resources:", err) - os.Exit(1) - } - - if len(resources) == 0 { - continue - } + resources, err := operatorManager.ListOperatorResources() + if err != nil { + time.Sleep(5 * time.Second) + continue + } - var ready = true - for _, resource := range resources { - phase, found, err := unstructured.NestedString(resource.Object, "status", "phase") - if !found { - ready = false - } - if err != nil { - fmt.Println("Failed to get resource status:", err) - os.Exit(1) - } - if phase != "Succeeded" { - ready = false - } - } + if len(resources) == 0 { + time.Sleep(5 * time.Second) + continue + } - if ready { - deployed <- true - return - } - time.Sleep(5 * time.Second) + ready := true + for _, resource := range resources { + phase, _, err := unstructured.NestedString(resource.Object, "status", "phase") + if err != nil { + return fmt.Errorf("failed to get resource status: %w", err) + } + if phase != "Succeeded" { + ready = false } } - }() - select { - case <-deployed: - fmt.Printf(" - ✅ Operator is ready\n") + if ready { + fmt.Printf(" - ✅ Operator is ready\n") + return nil + } + time.Sleep(5 * time.Second) } } @@ -303,11 +307,10 @@ func createPinnedSubscription(operatorName, channel, source, sourceNamespace, st // approveInstallPlan waits for an InstallPlan referencing the given CSV to appear in the // subscription's namespace, then patches it to approved=true so that a Manual-approval // subscription actually installs the operator. -func approveInstallPlan(subscriptionName, csvName string) { +func approveInstallPlan(subscriptionName, csvName string) error { dynamicClient, err := k8sclient.DynamicClient() if err != nil { - fmt.Println("Failed to create dynamic client:", err) - os.Exit(1) + return fmt.Errorf("failed to create dynamic client: %w", err) } fmt.Printf("⏳ Waiting for InstallPlan for CSV %q to appear...\n", csvName) @@ -316,8 +319,7 @@ func approveInstallPlan(subscriptionName, csvName string) { for { select { case <-timeoutCh: - fmt.Printf("Timeout waiting for InstallPlan for CSV %q\n", csvName) - os.Exit(1) + return fmt.Errorf("timeout waiting for InstallPlan for CSV %q", csvName) default: } @@ -338,14 +340,104 @@ func approveInstallPlan(subscriptionName, csvName string) { _, err := dynamicClient.Resource(installPlansGVR).Namespace("operators").Update( context.Background(), &plan, v1.UpdateOptions{}) if err != nil { - fmt.Printf("Failed to approve InstallPlan %q: %v\n", planName, err) - os.Exit(1) + return fmt.Errorf("failed to approve InstallPlan %q: %w", planName, err) } fmt.Printf(" - ✅ InstallPlan %q approved\n", planName) - return + return nil } } } time.Sleep(5 * time.Second) } } + +// collectOperatorLogs gathers diagnostic information about the operator installation and writes +// it to operatorLogsDir//. Collected artefacts: +// - Pod logs for every pod in the "operators" namespace (the operator itself) +// - Pod logs for OLM system pods in the "olm" namespace (catalog-operator, olm-operator) +// - JSON snapshots of Subscription, CSV, InstallPlan, and CatalogSource objects +// +// Errors are only printed as warnings — log collection must never fail the test run. +func collectOperatorLogs(phase string) { + dir := filepath.Join(operatorLogsDir, phase) + if err := os.MkdirAll(dir, 0750); err != nil { + fmt.Printf("⚠️ operator-logs: cannot create directory %q: %v\n", dir, err) + return + } + fmt.Printf("📋 Collecting operator diagnostic logs → %s/\n", dir) + + restConfig, err := k8sclient.KubeRestConfig() + if err != nil { + fmt.Printf("⚠️ operator-logs: cannot build REST config: %v\n", err) + return + } + clientset, err := kubernetes.NewForConfig(restConfig) + if err != nil { + fmt.Printf("⚠️ operator-logs: cannot create clientset: %v\n", err) + return + } + dynamicClient, err := k8sclient.DynamicClient() + if err != nil { + fmt.Printf("⚠️ operator-logs: cannot create dynamic client: %v\n", err) + return + } + + // --- Pod logs --- + for _, ns := range []string{"operators", "olm"} { + pods, err := clientset.CoreV1().Pods(ns).List(context.Background(), v1.ListOptions{}) + if err != nil { + fmt.Printf("⚠️ operator-logs: cannot list pods in %q: %v\n", ns, err) + continue + } + for _, pod := range pods.Items { + for _, container := range append(pod.Spec.InitContainers, pod.Spec.Containers...) { + logFile := filepath.Join(dir, fmt.Sprintf("%s_%s_%s.log", ns, pod.Name, container.Name)) + req := clientset.CoreV1().Pods(ns).GetLogs(pod.Name, &corev1.PodLogOptions{ + Container: container.Name, + }) + stream, err := req.Stream(context.Background()) + if err != nil { + // Pod may not have started yet — write a note and continue. + _ = os.WriteFile(logFile, []byte(fmt.Sprintf("(log unavailable: %v)\n", err)), 0640) + continue + } + data, _ := io.ReadAll(stream) + stream.Close() + if err := os.WriteFile(logFile, data, 0640); err != nil { + fmt.Printf("⚠️ operator-logs: cannot write %q: %v\n", logFile, err) + } + } + } + } + + // --- OLM resource snapshots --- + type gvrTarget struct { + gvr schema.GroupVersionResource + namespace string + label string + } + targets := []gvrTarget{ + {subscriptionsGVR, "operators", "subscriptions"}, + {clusterServiceVersionsGVR, "operators", "clusterserviceversions"}, + {installPlansGVR, "operators", "installplans"}, + {catalogSourcesGVR, "olm", "catalogsources"}, + } + for _, t := range targets { + list, err := dynamicClient.Resource(t.gvr).Namespace(t.namespace).List( + context.Background(), v1.ListOptions{}) + if err != nil { + fmt.Printf("⚠️ operator-logs: cannot list %s: %v\n", t.label, err) + continue + } + data, err := json.MarshalIndent(list, "", " ") + if err != nil { + continue + } + outFile := filepath.Join(dir, t.label+".json") + if err := os.WriteFile(outFile, data, 0640); err != nil { + fmt.Printf("⚠️ operator-logs: cannot write %q: %v\n", outFile, err) + } + } + + fmt.Printf("📋 Operator diagnostic logs collected in %s/\n", dir) +} From f206e92bfed0fc51f8f45d2c8a3cd71cc5c893fa Mon Sep 17 00:00:00 2001 From: Dominik Hanak Date: Mon, 5 Oct 2026 10:49:17 +0200 Subject: [PATCH 4/6] Remove community definitions of kube-rbac images --- packages/kn-plugin-workflow/Makefile | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/kn-plugin-workflow/Makefile b/packages/kn-plugin-workflow/Makefile index 8e3f0ff05b4..298c2652373 100644 --- a/packages/kn-plugin-workflow/Makefile +++ b/packages/kn-plugin-workflow/Makefile @@ -40,9 +40,6 @@ KIND_CLUSTER ?= kind KUBE_RBAC_PROXY_SRC := quay.io/brancz/kube-rbac-proxy:v0.22.0 KUBE_RBAC_PROXY_DST := gcr.io/kubebuilder/kube-rbac-proxy:v0.13.0 -KUBE_RBAC_PROXY_SRC := quay.io/brancz/kube-rbac-proxy:v0.13.1 -KUBE_RBAC_PROXY_DST := gcr.io/kubebuilder/kube-rbac-proxy:v0.13.1 - # Optional: set the following variables to install the operator from a custom OLM CatalogSource # (product/OSL builds) instead of the public operatorhubio-catalog. # Only CATALOG_INDEX_IMAGE is required to activate the custom-catalog install path. From fa056cb7a1f77dbb52030c79d802c389db948806 Mon Sep 17 00:00:00 2001 From: Dominik Hanak Date: Tue, 6 Oct 2026 09:36:52 +0200 Subject: [PATCH 5/6] Fix docker config, readd community images --- packages/kn-plugin-workflow/Makefile | 5 ++++- .../containerd-certs.d/registry.redhat.io/hosts.toml | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/kn-plugin-workflow/Makefile b/packages/kn-plugin-workflow/Makefile index 298c2652373..1ef7dd08833 100644 --- a/packages/kn-plugin-workflow/Makefile +++ b/packages/kn-plugin-workflow/Makefile @@ -66,6 +66,8 @@ OPERATOR_IMAGE ?= OPERATOR_IMAGE_RHT ?= KUBE_RBAC_PROXY_PRODUCT ?= KUBE_RBAC_PROXY_PRODUCT_RHT ?= +KUBE_RBAC_PROXY_SRC := quay.io/brancz/kube-rbac-proxy:v0.13.1 +KUBE_RBAC_PROXY_DST := gcr.io/kubebuilder/kube-rbac-proxy:v0.13.1 ARCH := $(shell uname -m) ifeq ($(ARCH),arm64) @@ -123,7 +125,8 @@ create-cluster: install-kind echo "Copying registry TLS/mirror config into kind node..."; \ docker cp $(CONTAINERD_CERTS_SRC)/. $(KIND_CLUSTER)-control-plane:$(CONTAINERD_CERTS_DST)/; \ echo "Copying docker credentials into kind node for authenticated registry pulls..."; \ - docker cp $(HOME)/.docker/config.json $(KIND_CLUSTER)-control-plane:/var/lib/kubelet/config.json; \ + DOCKER_CFG=$${DOCKER_CONFIG:-$(HOME)/.docker}/config.json; \ + docker cp $$DOCKER_CFG $(KIND_CLUSTER)-control-plane:/var/lib/kubelet/config.json; \ docker exec $(KIND_CLUSTER)-control-plane systemctl restart containerd; \ echo "Waiting for containerd to restart..."; \ sleep 5; \ diff --git a/packages/kn-plugin-workflow/e2e-tests/containerd-certs.d/registry.redhat.io/hosts.toml b/packages/kn-plugin-workflow/e2e-tests/containerd-certs.d/registry.redhat.io/hosts.toml index 792a8f8fb8c..27de49e7b21 100644 --- a/packages/kn-plugin-workflow/e2e-tests/containerd-certs.d/registry.redhat.io/hosts.toml +++ b/packages/kn-plugin-workflow/e2e-tests/containerd-certs.d/registry.redhat.io/hosts.toml @@ -3,4 +3,3 @@ server = "https://registry.redhat.io" [host."https://registry-proxy.engineering.redhat.com"] capabilities = ["pull", "resolve"] skip_verify = true - override_path = true From e266b5c6aaac6dc35bf0bf5d25d7ed13cc9b8611 Mon Sep 17 00:00:00 2001 From: Dominik Hanak Date: Tue, 6 Oct 2026 14:34:35 +0200 Subject: [PATCH 6/6] Fix kube-rbac-proxy community images --- packages/kn-plugin-workflow/Makefile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/kn-plugin-workflow/Makefile b/packages/kn-plugin-workflow/Makefile index 1ef7dd08833..5849c2fce6d 100644 --- a/packages/kn-plugin-workflow/Makefile +++ b/packages/kn-plugin-workflow/Makefile @@ -37,8 +37,6 @@ LDFLAGS := "-X $(SET_QUARKUS_PLATFORM_GROUP_ID) -X $(SET_Q KIND_VERSION ?= v0.20.0 OLM_VERSION = v0.31.0 KIND_CLUSTER ?= kind -KUBE_RBAC_PROXY_SRC := quay.io/brancz/kube-rbac-proxy:v0.22.0 -KUBE_RBAC_PROXY_DST := gcr.io/kubebuilder/kube-rbac-proxy:v0.13.0 # Optional: set the following variables to install the operator from a custom OLM CatalogSource # (product/OSL builds) instead of the public operatorhubio-catalog. @@ -66,7 +64,7 @@ OPERATOR_IMAGE ?= OPERATOR_IMAGE_RHT ?= KUBE_RBAC_PROXY_PRODUCT ?= KUBE_RBAC_PROXY_PRODUCT_RHT ?= -KUBE_RBAC_PROXY_SRC := quay.io/brancz/kube-rbac-proxy:v0.13.1 +KUBE_RBAC_PROXY_SRC := quay.io/brancz/kube-rbac-proxy:v0.22.0 KUBE_RBAC_PROXY_DST := gcr.io/kubebuilder/kube-rbac-proxy:v0.13.1 ARCH := $(shell uname -m)