From b4455001ff1e8e58f4f47e797eb9ccc2a86699b5 Mon Sep 17 00:00:00 2001 From: Rostislav Lan Date: Thu, 20 Aug 2026 10:36:28 +0200 Subject: [PATCH 01/13] test: add OSL smoke grep and serviceUrl classifiers #13375 Co-authored-by: Cursor --- .../plans/2026-08-20-osl-rc-smoke-subset.md | 732 ++++++++++++++++++ .../specs/2026-08-20-osl-rc-smoke-subset.md | 71 ++ utils/orchestrator/osl_smoke.py | 86 ++ utils/orchestrator/test_osl_smoke.py | 110 +++ 4 files changed, 999 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-20-osl-rc-smoke-subset.md create mode 100644 docs/superpowers/specs/2026-08-20-osl-rc-smoke-subset.md create mode 100644 utils/orchestrator/osl_smoke.py create mode 100644 utils/orchestrator/test_osl_smoke.py diff --git a/docs/superpowers/plans/2026-08-20-osl-rc-smoke-subset.md b/docs/superpowers/plans/2026-08-20-osl-rc-smoke-subset.md new file mode 100644 index 0000000..5ab0dcf --- /dev/null +++ b/docs/superpowers/plans/2026-08-20-osl-rc-smoke-subset.md @@ -0,0 +1,732 @@ +# OSL RC smoke subset Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Default OSL RC `--test` probes raw Data Index GraphQL, then runs four Playwright tests (Greeting + Failswitch statuses + retrigger + token-propagation). + +**Architecture:** Extract classification and Playwright grep into a stdlib Python module with unittest. The existing bash driver always deploys greeting, failswitch, and token-propagation, calls the probe, then Playwright `-g` for the four titles. Do not change overlays git files; keep copying `playwright/osl-regression-smoke.spec.ts` at runtime. + +**Tech Stack:** bash, Python 3 stdlib unittest, oc, Playwright (overlays e2e-tests), existing smoke wrapper. + +**Spec:** `docs/superpowers/specs/2026-08-20-osl-rc-smoke-subset.md` + +## Global Constraints + +- Repo: `rhdh-test-instance` only, branch `feat/rhidp-13375-osl-smoke`, worktree `/home/rlan/redhat/rhdh-test-instance/.worktrees/rhidp-13375-osl-smoke`. +- Do not edit `rhdh-plugin-export-overlays`, `rhdh-plugins`, or `rhdh-e2e-test-utils`. +- Do not commit `.env`, `.env.osl`, or cluster credentials. +- Python helpers: stdlib only. No new pip packages. +- Default Playwright titles (exact): `Run Greeting workflow and verify Workflows tab`, `Run Failswitch workflow and verify statuses`, `Rerun Failswitch from failure point`, `Execute token-propagation workflow via API`. +- Probe the raw Data Index service, never `osl-di-rewrite`. +- `--full-e2e` must keep running the full overlays orchestrator project with no title grep. +- Commit messages: conventional commits, include `#13375`. +- `export PATH="/home/rlan/bin:$HOME/.local/bin:$PATH"` before any `oc` command. + +--- + +## File map + +| File | Responsibility | +|---|---| +| `utils/orchestrator/osl_smoke.py` | Smoke titles, Playwright `-g` regex, GraphQL JSON classification, `probe` CLI | +| `utils/orchestrator/test_osl_smoke.py` | unittest for titles, grep, URL classification, probe JSON | +| `run-osl-regression.sh` | `--allow-relative-service-url`, always deploy token-propagation on smoke, call probe, pass `-g` | +| `playwright/osl-regression-smoke.spec.ts` | Always register token-propagation tests | +| `README.md` | Default 4-test smoke, probe, `--full-e2e` = plugin gate | +| `Makefile` | Pass-through `ALLOW_RELATIVE_SERVICE_URL=1` | + +Do **not** implement Orchestrator plugin `serviceUrl` derivation here. That is `docs/superpowers/plans/2026-08-20-orchestrator-serviceurl-from-endpoint.md`. + +--- + +### Task 1: Python smoke helper + unit tests + +**Files:** +- Create: `utils/orchestrator/osl_smoke.py` +- Create: `utils/orchestrator/test_osl_smoke.py` + +**Interfaces:** +- Consumes: none +- Produces: + - `SMOKE_TITLES: list[str]` (four titles, including token-propagation) + - `playwright_grep() -> str` + - `is_absolute_http_url(value: str | None) -> bool` + - `classify_definitions(definitions: list) -> dict` with keys `ok` (bool), `problems` (list of `{id, serviceUrl, endpoint, reason}`) + - CLI: `python utils/orchestrator/osl_smoke.py grep` prints the regex to stdout + - CLI: `python utils/orchestrator/osl_smoke.py classify` reads GraphQL JSON on stdin, exit 0/2 + +- [ ] **Step 1: Write the failing tests** + +Create `utils/orchestrator/test_osl_smoke.py`: + +```python +#!/usr/bin/env python3 +import json +import subprocess +import sys +import unittest +from pathlib import Path + +HERE = Path(__file__).resolve().parent +sys.path.insert(0, str(HERE)) + +import osl_smoke # noqa: E402 + + +class TestPlaywrightGrep(unittest.TestCase): + def test_default_titles(self): + self.assertEqual( + osl_smoke.SMOKE_TITLES, + [ + "Run Greeting workflow and verify Workflows tab", + "Run Failswitch workflow and verify statuses", + "Rerun Failswitch from failure point", + "Execute token-propagation workflow via API", + ], + ) + + def test_grep_joins_four_escaped_titles(self): + pattern = osl_smoke.playwright_grep() + self.assertIn("Run Greeting workflow and verify Workflows tab", pattern) + self.assertIn("Run Failswitch workflow and verify statuses", pattern) + self.assertIn("Rerun Failswitch from failure point", pattern) + self.assertIn("Execute token-propagation workflow via API", pattern) + self.assertNotIn("Verify Workflow All Runs", pattern) + + +class TestServiceUrl(unittest.TestCase): + def test_absolute_http(self): + self.assertTrue( + osl_smoke.is_absolute_http_url( + "http://greeting.orchestrator.svc.cluster.local" + ) + ) + + def test_absolute_https(self): + self.assertTrue(osl_smoke.is_absolute_http_url("https://example.example")) + + def test_relative_path(self): + self.assertFalse(osl_smoke.is_absolute_http_url("/greeting")) + + def test_empty_and_none(self): + self.assertFalse(osl_smoke.is_absolute_http_url("")) + self.assertFalse(osl_smoke.is_absolute_http_url(None)) + + +class TestClassifyDefinitions(unittest.TestCase): + def test_all_absolute_ok(self): + result = osl_smoke.classify_definitions( + [ + { + "id": "greeting", + "serviceUrl": "http://greeting.orchestrator.svc", + "endpoint": "http://greeting.orchestrator.svc/greeting", + } + ] + ) + self.assertTrue(result["ok"]) + self.assertEqual(result["problems"], []) + + def test_relative_service_url_is_problem(self): + result = osl_smoke.classify_definitions( + [ + { + "id": "greeting", + "serviceUrl": "/greeting", + "endpoint": "http://greeting.orchestrator.svc/greeting", + } + ] + ) + self.assertFalse(result["ok"]) + self.assertEqual(result["problems"][0]["id"], "greeting") + self.assertEqual(result["problems"][0]["reason"], "relative-or-missing-serviceUrl") + + def test_empty_list_not_ok(self): + result = osl_smoke.classify_definitions([]) + self.assertFalse(result["ok"]) + self.assertEqual(result["problems"][0]["reason"], "no-process-definitions") + + +class TestClassifyCli(unittest.TestCase): + def test_classify_stdin_exit_2_on_relative(self): + payload = json.dumps( + { + "data": { + "ProcessDefinitions": [ + {"id": "greeting", "serviceUrl": "/greeting", "endpoint": "http://x/greeting"} + ] + } + } + ) + proc = subprocess.run( + [sys.executable, str(HERE / "osl_smoke.py"), "classify"], + input=payload, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(proc.returncode, 2) + + +if __name__ == "__main__": + unittest.main() +``` + +- [ ] **Step 2: Run tests to verify they fail** + +```bash +export PATH="/home/rlan/bin:$HOME/.local/bin:$PATH" +cd /home/rlan/redhat/rhdh-test-instance/.worktrees/rhidp-13375-osl-smoke +python3 utils/orchestrator/test_osl_smoke.py +``` + +Expected: FAIL with `ModuleNotFoundError: No module named 'osl_smoke'` or import error. + +- [ ] **Step 3: Write minimal implementation** + +Create `utils/orchestrator/osl_smoke.py`: + +```python +#!/usr/bin/env python3 +"""OSL RC smoke helpers: Playwright grep and Data Index serviceUrl contract.""" +from __future__ import annotations + +import argparse +import json +import re +import sys +from typing import Any, Optional + +SMOKE_TITLES = [ + "Run Greeting workflow and verify Workflows tab", + "Run Failswitch workflow and verify statuses", + "Rerun Failswitch from failure point", + "Execute token-propagation workflow via API", +] + + +def playwright_grep() -> str: + return "|".join(re.escape(t) for t in SMOKE_TITLES) + + +def is_absolute_http_url(value: Optional[str]) -> bool: + if not value: + return False + return value.startswith("http://") or value.startswith("https://") + + +def classify_definitions(definitions: list) -> dict[str, Any]: + if not definitions: + return { + "ok": False, + "problems": [ + { + "id": None, + "serviceUrl": None, + "endpoint": None, + "reason": "no-process-definitions", + } + ], + } + problems = [] + for item in definitions: + service_url = item.get("serviceUrl") + if not is_absolute_http_url(service_url): + problems.append( + { + "id": item.get("id"), + "serviceUrl": service_url, + "endpoint": item.get("endpoint"), + "reason": "relative-or-missing-serviceUrl", + } + ) + return {"ok": not problems, "problems": problems} + + +def _cmd_grep() -> int: + print(playwright_grep()) + return 0 + + +def _cmd_classify() -> int: + payload = json.load(sys.stdin) + definitions = (payload.get("data") or {}).get("ProcessDefinitions") or [] + result = classify_definitions(definitions) + json.dump(result, sys.stdout, indent=2) + sys.stdout.write("\n") + if result["ok"]: + return 0 + if result["problems"] and result["problems"][0]["reason"] == "no-process-definitions": + return 1 + return 2 + + +def main(argv: Optional[list[str]] = None) -> int: + parser = argparse.ArgumentParser(prog="osl_smoke.py") + sub = parser.add_subparsers(dest="cmd", required=True) + sub.add_parser("grep") + sub.add_parser("classify") + args = parser.parse_args(argv) + if args.cmd == "grep": + return _cmd_grep() + return _cmd_classify() + + +if __name__ == "__main__": + sys.exit(main()) +``` + +- [ ] **Step 4: Run tests to verify they pass** + +```bash +cd /home/rlan/redhat/rhdh-test-instance/.worktrees/rhidp-13375-osl-smoke +python3 utils/orchestrator/test_osl_smoke.py +``` + +Expected: PASS (all tests). + +- [ ] **Step 5: Commit** + +```bash +git add utils/orchestrator/osl_smoke.py utils/orchestrator/test_osl_smoke.py \ + docs/superpowers/specs/2026-08-20-osl-rc-smoke-subset.md \ + docs/superpowers/plans/2026-08-20-osl-rc-smoke-subset.md +git commit -m "$(cat <<'EOF' +test: add OSL smoke grep and serviceUrl classifiers #13375 + +EOF +)" +``` + +--- + +### Task 2: Probe raw Data Index from the cluster + +**Files:** +- Modify: `utils/orchestrator/osl_smoke.py` (add `probe` subcommand) +- Modify: `utils/orchestrator/test_osl_smoke.py` (build curl argv; no live cluster) +- Modify: `run-osl-regression.sh` (`phase_test` after `ensure_smoke_workflows`) + +**Interfaces:** +- Consumes: `classify_definitions` from Task 1 +- Produces: + - `graphql_query() -> str` returning `{ ProcessDefinitions { id serviceUrl endpoint } }` + - `curl_probe_argv(namespace: str) -> list[str]` for `oc exec` + - CLI `probe --namespace ` runs oc, pipes JSON to classify, honors `ALLOW_RELATIVE_SERVICE_URL=1` + +- [ ] **Step 1: Write the failing tests** + +Append to `utils/orchestrator/test_osl_smoke.py`: + +```python +class TestProbeArgv(unittest.TestCase): + def test_curl_targets_raw_data_index_not_rewrite(self): + argv = osl_smoke.curl_probe_argv("orchestrator") + joined = " ".join(argv) + self.assertIn("sonataflow-platform-data-index-service.orchestrator.svc.cluster.local/graphql", joined) + self.assertNotIn("osl-di-rewrite", joined) + self.assertIn("ProcessDefinitions", joined) + + def test_graphql_query_asks_for_service_url_and_endpoint(self): + q = osl_smoke.graphql_query() + self.assertIn("serviceUrl", q) + self.assertIn("endpoint", q) + self.assertIn("ProcessDefinitions", q) +``` + +- [ ] **Step 2: Run the new tests to verify they fail** + +```bash +python3 utils/orchestrator/test_osl_smoke.py TestProbeArgv -v +``` + +Expected: FAIL with `AttributeError: module 'osl_smoke' has no attribute 'curl_probe_argv'`. + +- [ ] **Step 3: Implement probe helpers and CLI** + +Add to `osl_smoke.py` (keep existing functions). `curl_probe_argv` must be a list `oc` can consume: + +```python +GRAPHQL_QUERY = "{ ProcessDefinitions { id serviceUrl endpoint } }" + + +def graphql_query() -> str: + return GRAPHQL_QUERY + + +def curl_probe_argv(namespace: str) -> list[str]: + body = json.dumps({"query": graphql_query()}) + url = ( + f"http://sonataflow-platform-data-index-service.{namespace}" + ".svc.cluster.local/graphql" + ) + return [ + "oc", + "exec", + "-n", + namespace, + "deploy/redhat-developer-hub", + "--", + "curl", + "-sS", + "-X", + "POST", + "-H", + "Content-Type: application/json", + "-d", + body, + url, + ] +``` + +Add `probe` subparser: + +```python +def _cmd_probe(namespace: str, allow_relative: bool) -> int: + import subprocess + + argv = curl_probe_argv(namespace) + proc = subprocess.run(argv, capture_output=True, text=True, check=False) + if proc.returncode != 0: + sys.stderr.write(proc.stderr or proc.stdout or "oc exec curl failed\n") + return 1 + try: + payload = json.loads(proc.stdout) + except json.JSONDecodeError: + sys.stderr.write(f"Data Index did not return JSON: {proc.stdout[:500]}\n") + return 1 + if payload.get("errors"): + sys.stderr.write(json.dumps(payload["errors"]) + "\n") + return 1 + definitions = (payload.get("data") or {}).get("ProcessDefinitions") or [] + result = classify_definitions(definitions) + json.dump(result, sys.stderr, indent=2) + sys.stderr.write("\n") + if result["ok"]: + return 0 + if allow_relative: + sys.stderr.write( + "WARNING: relative/missing serviceUrl allowed by ALLOW_RELATIVE_SERVICE_URL\n" + ) + return 0 + if result["problems"] and result["problems"][0]["reason"] == "no-process-definitions": + return 1 + return 2 +``` + +Wire argparse: `probe --namespace` required; `--allow-relative` flag **or** env `ALLOW_RELATIVE_SERVICE_URL=1`. + +- [ ] **Step 4: Run unit tests** + +```bash +python3 utils/orchestrator/test_osl_smoke.py +``` + +Expected: PASS. + +- [ ] **Step 5: Call probe from `phase_test`** + +In `run-osl-regression.sh`, add a `run_all=false` style flag: + +```bash +allow_relative_service_url=false +``` + +Parse: + +```bash +--allow-relative-service-url) allow_relative_service_url=true; shift ;; +``` + +After `ensure_smoke_workflows` (smoke path only, not `--full-e2e`), before Playwright: + +```bash +probe_args=(python3 "${SCRIPT_DIR}/utils/orchestrator/osl_smoke.py" probe --namespace "$namespace") +if [[ "$allow_relative_service_url" == "true" || "${ALLOW_RELATIVE_SERVICE_URL:-}" == "1" ]]; then + probe_args+=(--allow-relative) +fi +log "probing raw Data Index GraphQL ProcessDefinitions.serviceUrl" +"${probe_args[@]}" +``` + +Also document in `usage()`. + +- [ ] **Step 6: Commit** + +```bash +git add utils/orchestrator/osl_smoke.py utils/orchestrator/test_osl_smoke.py run-osl-regression.sh +git commit -m "$(cat <<'EOF' +feat: probe raw Data Index serviceUrl before OSL Playwright #13375 + +EOF +)" +``` + +--- + +### Task 3: Default Playwright grep to the four OSL tests + +**Files:** +- Modify: `run-osl-regression.sh` (`phase_test` Playwright invocation) +- Modify: `README.md` OSL RC smoke section + +**Interfaces:** +- Consumes: `osl_smoke.py grep` from Task 1 +- Produces: default `--test` runs four titles; `--full-e2e` unchanged + +- [ ] **Step 1: Write a failing driver assertion (script check)** + +Add to `utils/orchestrator/test_osl_smoke.py`: + +```python +class TestDriverGrepWiring(unittest.TestCase): + def test_run_script_mentions_osl_smoke_grep(self): + text = Path(__file__).resolve().parents[2].joinpath("run-osl-regression.sh").read_text() + self.assertIn("osl_smoke.py", text) + self.assertIn("grep", text) + self.assertIn("--grep", text) +``` + +Playwright CLI flag is `-g` / `--grep`. The driver must pass `--grep "$(python3 ... grep)"`. + +- [ ] **Step 2: Run the wiring test to see it fail** + +```bash +python3 utils/orchestrator/test_osl_smoke.py TestDriverGrepWiring -v +``` + +Expected: FAIL (`--grep` not in `run-osl-regression.sh`). + +- [ ] **Step 3: Change the smoke Playwright invocation** + +In `phase_test`, replace the smoke branch: + +```bash + else + smoke_grep="$(python3 "${SCRIPT_DIR}/utils/orchestrator/osl_smoke.py" grep)" + log "Playwright grep: ${smoke_grep}" + # shellcheck disable=SC2086 + (cd "$e2e" && $pw test --project=orchestrator --workers=1 --grep "$smoke_grep" "$smoke_spec") + fi +``` + +Leave the `--full-e2e` branch **without** `--grep`. + +- [ ] **Step 4: Run unit tests** + +```bash +python3 utils/orchestrator/test_osl_smoke.py +``` + +Expected: PASS, including `TestDriverGrepWiring`. + +- [ ] **Step 5: Update README** + +Replace the OSL RC smoke paragraph in `README.md` so it states: + +- Default smoke is four tests (list the titles, including token-propagation). +- Token-propagation workflow + sample-server are always deployed on the smoke path. +- A GraphQL probe runs first against raw Data Index. +- `--allow-relative-service-url` continues after SRVLOGIC-1137-class relative `serviceUrl` (needed on 1.39.CR1 until the plugin fix ships). +- `--full-e2e` is the RHDH plugin suite (RBAC, entity, ui:props, Loki, all workflows), not the OSL CR default. + +Example block: + +```bash +./run-osl-regression.sh --all --rhdh next --osl-release 1.39.0.CR1 --namespace orchestrator +# 1.39.CR1 currently needs the DI contract override plus rewrite proxy: +ALLOW_RELATIVE_SERVICE_URL=1 ./run-osl-regression.sh --test --namespace orchestrator +./run-osl-regression.sh --test --full-e2e --namespace orchestrator +``` + +- [ ] **Step 6: Commit** + +```bash +git add run-osl-regression.sh README.md utils/orchestrator/test_osl_smoke.py +git commit -m "$(cat <<'EOF' +feat: limit OSL RC Playwright to greeting, failswitch, retrigger, token-propagation #13375 + +EOF +)" +``` + +--- + +### Task 4: Always deploy and register token-propagation on smoke + +**Files:** +- Modify: `run-osl-regression.sh` (`ensure_token_propagation_workflow`, call it from `ensure_smoke_workflows`, wait for the deployment) +- Modify: `playwright/osl-regression-smoke.spec.ts` +- Modify: `README.md` (if Task 3 copy still called token-propagation optional) +- Modify: `utils/orchestrator/test_osl_smoke.py` (driver and wrapper wiring) + +**Interfaces:** +- Consumes: overlays token test module (read-only at runtime); Keycloak env already exported in `phase_test` +- Produces: every smoke `--test` deploys sample-server + token-propagation, registers `Execute token-propagation workflow via API`, waits Ready before the GraphQL probe + +- [ ] **Step 1: Write failing wiring tests** + +Append: + +```python +class TestTokenSmokeWiring(unittest.TestCase): + def test_driver_always_deploys_token_propagation(self): + text = Path(__file__).resolve().parents[2].joinpath("run-osl-regression.sh").read_text() + self.assertIn("ensure_token_propagation_workflow", text) + self.assertIn("token-propagation", text) + self.assertNotIn("--include-token-propagation", text) + self.assertNotIn("OSL_SMOKE_TOKEN_PROPAGATION", text) + + def test_smoke_wrapper_always_registers_token_tests(self): + text = ( + Path(__file__).resolve().parents[2] + / "playwright" + / "osl-regression-smoke.spec.ts" + ).read_text() + self.assertIn("registerTokenPropagationWorkflowTests", text) + self.assertNotIn("OSL_SMOKE_TOKEN_PROPAGATION", text) +``` + +- [ ] **Step 2: Run to verify fail** + +```bash +python3 utils/orchestrator/test_osl_smoke.py TestTokenSmokeWiring -v +``` + +Expected: FAIL (deploy function / import missing). + +- [ ] **Step 3: Patch the smoke wrapper** + +Add imports at the top of `playwright/osl-regression-smoke.spec.ts` with the other imports: + +```typescript +import { registerTokenPropagationWorkflowTests } from "./specs/orchestrator-token-propagation.tests.js"; +import { requireEnvVar } from "./support/utils/orchestrator-workflow-helpers.js"; +``` + +After `registerOrchestratorCoreWorkflowTests(ensureDataIndexOrSkip);` always call: + +```typescript +registerTokenPropagationWorkflowTests(requireEnvVar); +``` + +Do not gate this on an env var. + +- [ ] **Step 4: Always deploy token-propagation from `ensure_smoke_workflows`** + +Add `ensure_token_propagation_workflow` modeled on overlays `deployTokenPropagationWorkflow` in `rhdh-plugin-export-overlays/workspaces/orchestrator/e2e-tests/tests/support/utils/workflow-deployment-helpers.ts` (function starts ~line 460). Required behavior: + +1. Require `KEYCLOAK_BASE_URL` (already exported in `phase_test` before `ensure_smoke_workflows`). If `ensure_smoke_workflows` runs before Keycloak env is set, set `KEYCLOAK_BASE_URL` first — `phase_test` already does this before `ensure_dataindex_rewrite`; move `ensure_smoke_workflows` so it runs **after** `KEYCLOAK_BASE_URL` is exported (it already does today). +2. `authServerUrl="${KEYCLOAK_BASE_URL}/realms/${KEYCLOAK_REALM}"` with realm `rhdh`. +3. `tokenUrl="${authServerUrl}/protocol/openid-connect/token"`. +4. Clone `https://github.com/rhdhorchestrator/orchestrator-demo.git` shallow into a temp dir. +5. Rewrite `09_token_propagation/manifests/01-configmap_token-propagation-props.yaml`: + - `http://example-kc-service.keycloak:8080/realms/quarkus` → `$authServerUrl` + - `client-id=quarkus-app` → `client-id=rhdh-client` + - `client-secret=lVGSvdaoDUem7lqeAnqXn1F92dCPbQea` → `client-secret=rhdh-client-secret` + - `http://sample-server-service.rhdh-operator` → `http://sample-server-service.${ns}:8080` +6. Rewrite `09_token_propagation/manifests/03-configmap_02-token-propagation-resources-specs.yaml` token URL to `$tokenUrl`. +7. Apply the sample-server Deployment/Service YAML from that overlays function (image `quay.io/orchestrator/sample-server:latest`), wait Available 120s. +8. `oc apply -n "$ns" -f "$manifestsDir"`. +9. Extend `patch_smoke_workflow` so `name=token-propagation` patches **persistence only** (do not change `podTemplate.container.image`; demo manifests already set it). Persistence JSON must match greeting: `backstage-psql-secret` / `POSTGRES_USER` / `POSTGRES_PASSWORD`, `serviceRef.name=backstage-psql`, `databaseName=backstage_plugin_orchestrator`, `databaseSchema=token-propagation`. +10. Include `token-propagation` in `wait_smoke_workflows_ready` alongside `greeting` and `failswitch` (all three must report `readyReplicas=1`). +11. `rm -rf` the clone. + +At the end of `ensure_smoke_workflows`, after greeting/failswitch apply+patch, call: + +```bash +ensure_token_propagation_workflow "$ns" +``` + +Keep a single wait loop that includes all three deployments. Probe (Task 2) stays after `ensure_smoke_workflows`, so token-propagation is Ready before GraphQL classify. + +- [ ] **Step 5: README** + +State that default smoke always deploys and runs token-propagation (JWT/OpenAPI into the workflow). Do not document `--include-token-propagation`. + +- [ ] **Step 6: Run unit tests** + +```bash +python3 utils/orchestrator/test_osl_smoke.py +``` + +Expected: PASS. + +- [ ] **Step 7: Commit** + +```bash +git add run-osl-regression.sh playwright/osl-regression-smoke.spec.ts README.md \ + utils/orchestrator/test_osl_smoke.py +git commit -m "$(cat <<'EOF' +feat: include token-propagation in default OSL RC smoke #13375 + +EOF +)" +``` + +--- + +### Task 5: Makefile pass-through and verification notes + +**Files:** +- Modify: `Makefile` (`osl-regression` target) + +**Interfaces:** +- Consumes: `--allow-relative-service-url` from Task 2 +- Produces: `make osl-regression` can pass `ALLOW_RELATIVE_SERVICE_URL=1` + +- [ ] **Step 1: Extend `osl-regression`** + +```make +osl-regression: ## Cleanup + prepare OSL + deploy + 4-test smoke (VERSION, OSL_RELEASE) +ifndef OSL_RELEASE + $(error OSL_RELEASE is required, e.g. make osl-regression VERSION=next OSL_RELEASE=1.39.0.CR1) +endif + ./run-osl-regression.sh --all --rhdh $(VERSION) --osl-release $(OSL_RELEASE) --namespace $(ORCH_NAMESPACE) \ + $(if $(filter 1,$(ALLOW_RELATIVE_SERVICE_URL)),--allow-relative-service-url,) +``` + +- [ ] **Step 2: Dry-run help** + +```bash +./run-osl-regression.sh --help +``` + +Expected: usage lists `--allow-relative-service-url`. It must **not** list `--include-token-propagation`. + +- [ ] **Step 3: Commit** + +```bash +git add Makefile README.md +git commit -m "$(cat <<'EOF' +docs: document OSL RC 4-test smoke including token-propagation #13375 + +EOF +)" +``` + +--- + +## Cluster verification (after Tasks 1–5, not a code task) + +On a logged-in cluster with RHDH already up: + +```bash +export PATH="/home/rlan/bin:$HOME/.local/bin:$PATH" +cd /home/rlan/redhat/rhdh-test-instance/.worktrees/rhidp-13375-osl-smoke +python3 utils/orchestrator/osl_smoke.py probe --namespace orchestrator; echo exit:$? +# 1.39.CR1 expected: exit 2, problems reason relative-or-missing-serviceUrl +ALLOW_RELATIVE_SERVICE_URL=1 ./run-osl-regression.sh --test --namespace orchestrator +``` + +Expected Playwright: **4 passed** (not 10). The report must include `Execute token-propagation workflow via API` and must not list `Verify Workflow All Runs` as executed. + +Do not treat this cluster run as part of the git tasks; it is the human/agent gate after the commits. + +--- + +## Self-review + +1. **Spec coverage:** 4-test default including token-propagation → Tasks 1, 3, 4. GraphQL probe → Task 2. `--full-e2e` as plugin suite → Task 3 README. Makefile → Task 5. Plugin `serviceUrl` productization → sibling plan, not this file. +2. **Placeholders:** none. +3. **Types:** `playwright_grep() -> str` (four titles, no `include_token` argument), `classify_definitions(...) -> dict` with `ok`/`problems`, probe exit 0/1/2, `--allow-relative` matches `ALLOW_RELATIVE_SERVICE_URL=1`. No `--include-token-propagation`. diff --git a/docs/superpowers/specs/2026-08-20-osl-rc-smoke-subset.md b/docs/superpowers/specs/2026-08-20-osl-rc-smoke-subset.md new file mode 100644 index 0000000..4cc9cac --- /dev/null +++ b/docs/superpowers/specs/2026-08-20-osl-rc-smoke-subset.md @@ -0,0 +1,71 @@ +# Spec: OSL RC smoke subset (RHIDP-13375) + +Research for this spec: RHIDP-13375, RHDH 1.10 Orchestrator docs, OSL 1.37–1.38 release notes, SRVLOGIC-1137 / SRVLOGIC-1124, and the Orchestrator backend execute path (`POST {serviceUrl}/{id}` plus `/management/processes/...`). + +## Problem + +`./run-osl-regression.sh --test` (without `--full-e2e`) copies `playwright/osl-regression-smoke.spec.ts` into overlays e2e and runs **all 10** `registerOrchestratorCoreWorkflowTests` cases. That is more Playwright than an OSL CR gate needs: abort / status-detail / All Runs / suggested-link duplicate Failswitch OSL APIs and mostly assert RHDH UI. A single Greeting execute is **not** enough either: it misses Jobs Service timers, abort, switch/error, retrigger, and JWT/OpenAPI auth into the workflow runtime. + +OSL 1.39.CR1 also changed Data Index `ProcessDefinitions.serviceUrl` to a relative path (SRVLOGIC-1137). The current `osl-di-rewrite` proxy hides that from Playwright. There is no pre-Playwright check against the **raw** Data Index. + +## Goal + +Make the default OSL RC path (`--all` / `make osl-regression`) a **lean OSL contract + workflow gate**: + +1. Probe raw Data Index GraphQL before Playwright. +2. Run exactly four Playwright tests (Greeting, Failswitch statuses, Failswitch retrigger, token-propagation). +3. Keep `--full-e2e` as the RHDH **plugin** regression gate, not the OSL CR default. + +## In scope (this repo: `rhdh-test-instance`) + +- Python helper + unit tests for smoke titles, Playwright `-g` regex, and GraphQL contract classification. +- `run-osl-regression.sh` wiring: probe, default grep, `--allow-relative-service-url`. +- Smoke wrapper always registers token-propagation tests (no env flag). +- Always deploy `sample-server` + `token-propagation` on the smoke path (same Keycloak substitutions overlays uses). +- README / Makefile copy. + +## Out of scope (separate plan) + +- Changing Orchestrator plugin code to derive `serviceUrl` origin from `endpoint`. +- Removing `osl-di-rewrite` (only after the plugin ships). +- Editing `rhdh-plugin-export-overlays` test files in git (runtime copy of the smoke wrapper stays). + +## Default Playwright titles + +Exact strings from overlays specs: + +1. `Run Greeting workflow and verify Workflows tab` +2. `Run Failswitch workflow and verify statuses` +3. `Rerun Failswitch from failure point` +4. `Execute token-propagation workflow via API` + +## GraphQL probe + +- Query **raw** `http://sonataflow-platform-data-index-service..svc.cluster.local/graphql` from inside the cluster (RHDH pod `curl`), **not** `osl-di-rewrite`. +- Query body: `{ ProcessDefinitions { id serviceUrl endpoint } }`. +- A `serviceUrl` is valid only if it starts with `http://` or `https://`. +- If any definition has a missing or relative `serviceUrl`, exit **2** unless `ALLOW_RELATIVE_SERVICE_URL=1` / `--allow-relative-service-url` (then print a warning and continue so Playwright can still run behind the rewrite proxy). +- If the query fails or returns zero definitions after smoke workflows are Ready, exit **1**. + +## Token-propagation (always on for smoke) + +Default `--test` / `--all` always: + +- Deploys `sample-server` and `token-propagation` from `https://github.com/rhdhorchestrator/orchestrator-demo.git` path `09_token_propagation/manifests`, with the same Keycloak URL substitutions overlays uses. +- Registers overlays `Execute token-propagation workflow via API` in the smoke wrapper (unconditional). +- Includes that title in the Playwright grep. +- Waits for `deployment/token-propagation` Ready before the GraphQL probe. + +There is no `--include-token-propagation` flag. + +## `--full-e2e` + +Unchanged: runs overlays `--project=orchestrator` with no smoke wrapper and no title grep. Document as the plugin-release suite (RBAC, entity, ui:props, Loki, all workflows). + +## Constraints + +- Do not commit `.env`, `.env.osl`, cluster passwords, or Keycloak secrets. +- Do not edit files outside `rhdh-test-instance` for this spec. +- Python helpers: stdlib only (`unittest`, `json`, `urllib`/`json` parsing). No new pip deps. +- `oc` / `helm` may live in `/home/rlan/bin`; driver already assumes they are on `PATH`. +- Conventional commits; reference `#13375`. diff --git a/utils/orchestrator/osl_smoke.py b/utils/orchestrator/osl_smoke.py new file mode 100644 index 0000000..b2435ca --- /dev/null +++ b/utils/orchestrator/osl_smoke.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +"""OSL RC smoke helpers: Playwright grep and Data Index serviceUrl contract.""" +from __future__ import annotations + +import argparse +import json +import sys +from typing import Any, Optional + +SMOKE_TITLES = [ + "Run Greeting workflow and verify Workflows tab", + "Run Failswitch workflow and verify statuses", + "Rerun Failswitch from failure point", + "Execute token-propagation workflow via API", +] + + +def playwright_grep() -> str: + return "|".join(SMOKE_TITLES) + + +def is_absolute_http_url(value: Optional[str]) -> bool: + if not value: + return False + return value.startswith("http://") or value.startswith("https://") + + +def classify_definitions(definitions: list) -> dict[str, Any]: + if not definitions: + return { + "ok": False, + "problems": [ + { + "id": None, + "serviceUrl": None, + "endpoint": None, + "reason": "no-process-definitions", + } + ], + } + problems = [] + for item in definitions: + service_url = item.get("serviceUrl") + if not is_absolute_http_url(service_url): + problems.append( + { + "id": item.get("id"), + "serviceUrl": service_url, + "endpoint": item.get("endpoint"), + "reason": "relative-or-missing-serviceUrl", + } + ) + return {"ok": not problems, "problems": problems} + + +def _cmd_grep() -> int: + print(playwright_grep()) + return 0 + + +def _cmd_classify() -> int: + payload = json.load(sys.stdin) + definitions = (payload.get("data") or {}).get("ProcessDefinitions") or [] + result = classify_definitions(definitions) + json.dump(result, sys.stdout, indent=2) + sys.stdout.write("\n") + if result["ok"]: + return 0 + if result["problems"] and result["problems"][0]["reason"] == "no-process-definitions": + return 1 + return 2 + + +def main(argv: Optional[list[str]] = None) -> int: + parser = argparse.ArgumentParser(prog="osl_smoke.py") + sub = parser.add_subparsers(dest="cmd", required=True) + sub.add_parser("grep") + sub.add_parser("classify") + args = parser.parse_args(argv) + if args.cmd == "grep": + return _cmd_grep() + return _cmd_classify() + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/utils/orchestrator/test_osl_smoke.py b/utils/orchestrator/test_osl_smoke.py new file mode 100644 index 0000000..409d04b --- /dev/null +++ b/utils/orchestrator/test_osl_smoke.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +import json +import subprocess +import sys +import unittest +from pathlib import Path + +HERE = Path(__file__).resolve().parent +sys.path.insert(0, str(HERE)) + +import osl_smoke # noqa: E402 + + +class TestPlaywrightGrep(unittest.TestCase): + def test_default_titles(self): + self.assertEqual( + osl_smoke.SMOKE_TITLES, + [ + "Run Greeting workflow and verify Workflows tab", + "Run Failswitch workflow and verify statuses", + "Rerun Failswitch from failure point", + "Execute token-propagation workflow via API", + ], + ) + + def test_grep_joins_four_escaped_titles(self): + pattern = osl_smoke.playwright_grep() + self.assertIn("Run Greeting workflow and verify Workflows tab", pattern) + self.assertIn("Run Failswitch workflow and verify statuses", pattern) + self.assertIn("Rerun Failswitch from failure point", pattern) + self.assertIn("Execute token-propagation workflow via API", pattern) + self.assertNotIn("Verify Workflow All Runs", pattern) + + +class TestServiceUrl(unittest.TestCase): + def test_absolute_http(self): + self.assertTrue( + osl_smoke.is_absolute_http_url( + "http://greeting.orchestrator.svc.cluster.local" + ) + ) + + def test_absolute_https(self): + self.assertTrue(osl_smoke.is_absolute_http_url("https://example.example")) + + def test_relative_path(self): + self.assertFalse(osl_smoke.is_absolute_http_url("/greeting")) + + def test_empty_and_none(self): + self.assertFalse(osl_smoke.is_absolute_http_url("")) + self.assertFalse(osl_smoke.is_absolute_http_url(None)) + + +class TestClassifyDefinitions(unittest.TestCase): + def test_all_absolute_ok(self): + result = osl_smoke.classify_definitions( + [ + { + "id": "greeting", + "serviceUrl": "http://greeting.orchestrator.svc", + "endpoint": "http://greeting.orchestrator.svc/greeting", + } + ] + ) + self.assertTrue(result["ok"]) + self.assertEqual(result["problems"], []) + + def test_relative_service_url_is_problem(self): + result = osl_smoke.classify_definitions( + [ + { + "id": "greeting", + "serviceUrl": "/greeting", + "endpoint": "http://greeting.orchestrator.svc/greeting", + } + ] + ) + self.assertFalse(result["ok"]) + self.assertEqual(result["problems"][0]["id"], "greeting") + self.assertEqual(result["problems"][0]["reason"], "relative-or-missing-serviceUrl") + + def test_empty_list_not_ok(self): + result = osl_smoke.classify_definitions([]) + self.assertFalse(result["ok"]) + self.assertEqual(result["problems"][0]["reason"], "no-process-definitions") + + +class TestClassifyCli(unittest.TestCase): + def test_classify_stdin_exit_2_on_relative(self): + payload = json.dumps( + { + "data": { + "ProcessDefinitions": [ + {"id": "greeting", "serviceUrl": "/greeting", "endpoint": "http://x/greeting"} + ] + } + } + ) + proc = subprocess.run( + [sys.executable, str(HERE / "osl_smoke.py"), "classify"], + input=payload, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(proc.returncode, 2) + + +if __name__ == "__main__": + unittest.main() From b4a5b4de77dadcb80eb29e3e0c68f8bf946c8f80 Mon Sep 17 00:00:00 2001 From: Rostislav Lan Date: Thu, 20 Aug 2026 10:37:37 +0200 Subject: [PATCH 02/13] feat: probe raw Data Index serviceUrl before OSL Playwright #13375 Co-authored-by: Cursor --- run-osl-regression.sh | 571 +++++++++++++++++++++++++++ utils/orchestrator/osl_smoke.py | 69 ++++ utils/orchestrator/test_osl_smoke.py | 15 + 3 files changed, 655 insertions(+) create mode 100755 run-osl-regression.sh diff --git a/run-osl-regression.sh b/run-osl-regression.sh new file mode 100755 index 0000000..b41a7a1 --- /dev/null +++ b/run-osl-regression.sh @@ -0,0 +1,571 @@ +#!/bin/bash +# +# Thin OSL RC regression driver (RHIDP-13375). +# Phases: cleanup -> prepare-osl -> deploy -> test. +# Smoke Playwright skips overlays orchestrator.spec.ts beforeAll and only +# registers greeting/failswitch tests via playwright/osl-regression-smoke.spec.ts. +# +# Usage: +# ./run-osl-regression.sh --all --rhdh next --osl-release 1.39.0.CR1 +# ./run-osl-regression.sh --cleanup --include-operators --namespace orchestrator +# ./run-osl-regression.sh --test --overlays-dir ../rhdh-plugin-export-overlays +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +_git_common="$(cd "$SCRIPT_DIR" && git rev-parse --git-common-dir 2>/dev/null)" +_main_repo_root="$(cd "$SCRIPT_DIR" && cd "$_git_common/.." 2>/dev/null && pwd)" +WORKSPACE_DIR="$(dirname "${_main_repo_root:-$SCRIPT_DIR}")" +unset _git_common _main_repo_root + +DEFAULT_OVERLAYS="${WORKSPACE_DIR}/rhdh-plugin-export-overlays" +KEYCLOAK_NS="rhdh-keycloak" +KEYCLOAK_RELEASE="keycloak" +RHDH_RELEASE="redhat-developer-hub" +SMOKE_WRAPPER_SRC="${SCRIPT_DIR}/playwright/osl-regression-smoke.spec.ts" +SMOKE_WRAPPER_NAME="osl-regression-smoke.spec.ts" +WORKFLOW_REPO="${SERVERLESS_WORKFLOWS_REPO:-https://github.com/rhdhorchestrator/serverless-workflows.git}" +WORKFLOW_REPO_REF="${SERVERLESS_WORKFLOWS_REF:-daeeee8dec16beab6d96a81774ef500081a2c2b0}" + +run_all=false +run_cleanup=false +run_prepare=false +run_deploy=false +run_test=false +include_operators=false +full_e2e=false +allow_relative_service_url=false +rhdh="" +osl_release="" +osl_manifest="" +namespace="orchestrator" +overlays_dir="$DEFAULT_OVERLAYS" + +usage() { + cat < RHDH version (required with --deploy / --all) + --osl-release Load config/osl-releases/.json + --osl-manifest Explicit OSL manifest path + --namespace RHDH/orchestrator namespace (default: orchestrator) + --overlays-dir rhdh-plugin-export-overlays checkout + --include-operators Cleanup also removes operators/catalog/mirror + --full-e2e Full overlays orchestrator Playwright project + --allow-relative-service-url Continue smoke if Data Index serviceUrl is relative + -h, --help Show this help +EOF +} + +log() { echo "==> $*"; } +die() { echo "Error: $*" >&2; exit 1; } + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --all) run_all=true; shift ;; + --cleanup) run_cleanup=true; shift ;; + --prepare-osl) run_prepare=true; shift ;; + --deploy) run_deploy=true; shift ;; + --test) run_test=true; shift ;; + --rhdh) rhdh="${2:-}"; shift 2 ;; + --osl-release) osl_release="${2:-}"; shift 2 ;; + --osl-manifest) osl_manifest="${2:-}"; shift 2 ;; + --namespace) namespace="${2:-}"; shift 2 ;; + --overlays-dir) overlays_dir="${2:-}"; shift 2 ;; + --include-operators) include_operators=true; shift ;; + --full-e2e) full_e2e=true; shift ;; + --allow-relative-service-url) allow_relative_service_url=true; shift ;; + -h|--help) usage; exit 0 ;; + *) usage; die "unknown option: $1" ;; + esac +done + +if [[ "$run_all" == "true" ]]; then + run_cleanup=true + run_prepare=true + run_deploy=true + run_test=true + include_operators=true +fi + +if [[ "$run_cleanup" != "true" && "$run_prepare" != "true" && "$run_deploy" != "true" && "$run_test" != "true" ]]; then + usage + die "at least one phase flag (or --all) is required" +fi + +overlays_e2e_dir() { + echo "${overlays_dir}/workspaces/orchestrator/e2e-tests" +} + +resolve_manifest() { + if [[ -n "$osl_manifest" ]]; then + echo "$osl_manifest" + return + fi + if [[ -n "$osl_release" ]]; then + echo "${SCRIPT_DIR}/config/osl-releases/${osl_release}.json" + return + fi + echo "" +} + +preflight() { + require_cmd oc + require_cmd helm + require_cmd jq + oc whoami >/dev/null 2>&1 || die "oc whoami failed; log into a cluster first" + + if [[ "$run_prepare" == "true" ]]; then + require_cmd podman + require_cmd skopeo + require_cmd python + local manifest + manifest="$(resolve_manifest)" + [[ -n "$manifest" ]] || die "--prepare-osl requires --osl-release or --osl-manifest" + [[ -f "$manifest" ]] || die "OSL manifest not found: $manifest" + fi + + if [[ "$run_deploy" == "true" ]]; then + [[ -n "$rhdh" ]] || die "--rhdh is required when --deploy is selected" + if [[ "$run_prepare" != "true" && ! -f "${SCRIPT_DIR}/.env.osl" ]]; then + die ".env.osl is missing; run --prepare-osl first or include it in this invocation" + fi + fi + + if [[ "$run_test" == "true" ]]; then + require_cmd git + local pkg + pkg="$(overlays_e2e_dir)/package.json" + [[ -f "$pkg" ]] || die "overlays e2e package.json not found: $pkg (pass --overlays-dir)" + [[ -f "$SMOKE_WRAPPER_SRC" ]] || die "missing smoke wrapper: $SMOKE_WRAPPER_SRC" + if ! command -v yarn >/dev/null 2>&1 && ! command -v corepack >/dev/null 2>&1; then + die "yarn or corepack is required for --test" + fi + fi +} + +cluster_router_base() { + local domain + domain="$(oc get ingresses.config/cluster -o jsonpath='{.spec.domain}' 2>/dev/null || true)" + if [[ -n "$domain" ]]; then + echo "$domain" + return + fi + local host + host="$(oc get route console -n openshift-console -o jsonpath='{.spec.host}' 2>/dev/null || true)" + [[ "$host" == *.* ]] || die "could not discover cluster router base" + echo "${host#*.}" +} + +route_url() { + local name="$1" ns="$2" default_scheme="${3:-https}" + local host tls scheme + host="$(oc get route "$name" -n "$ns" -o jsonpath='{.spec.host}' 2>/dev/null || true)" + [[ -n "$host" ]] || die "route $name in $ns has no host" + tls="$(oc get route "$name" -n "$ns" -o jsonpath='{.spec.tls.termination}' 2>/dev/null || true)" + scheme="$default_scheme" + [[ -n "$tls" ]] && scheme="https" + echo "${scheme}://${host}" +} + +csv_mm_for_package() { + local package="$1" + local version + version="$(oc get csv -n openshift-operators -o json 2>/dev/null | jq -r --arg p "$package" ' + .items[] + | select(.status.phase == "Succeeded") + | select((.spec.name == $p) or ((.metadata.name // "") | startswith($p + "."))) + | .spec.version // empty + ' | head -n 1)" + echo "$version" | grep -oE '^[0-9]+\.[0-9]+' || true +} + +workflow_osl_image_tag() { + local os_mm osl_mm chosen + os_mm="$(csv_mm_for_package serverless-operator)" + osl_mm="$(csv_mm_for_package logic-operator)" + if [[ -n "$os_mm" && -n "$osl_mm" ]]; then + if [[ "$(printf '%s\n%s\n' "$os_mm" "$osl_mm" | sort -V | head -n 1)" == "$os_mm" ]]; then + chosen="$os_mm" + else + chosen="$osl_mm" + fi + else + chosen="${os_mm:-${osl_mm:-1.37}}" + fi + echo "${chosen//./_}" +} + +patch_smoke_workflow() { + local ns="$1" name="$2" tag="$3" image + case "$name" in + greeting) image="quay.io/orchestrator/serverless-workflow-greeting:osl_${tag}" ;; + failswitch) image="quay.io/orchestrator/fail-switch:osl_${tag}" ;; + *) die "unknown smoke workflow: $name" ;; + esac + oc -n "$ns" patch sonataflow "$name" --type merge -p "{ + \"spec\": { + \"persistence\": { + \"dbMigrationStrategy\": \"job\", + \"postgresql\": { + \"secretRef\": { + \"name\": \"backstage-psql-secret\", + \"userKey\": \"POSTGRES_USER\", + \"passwordKey\": \"POSTGRES_PASSWORD\" + }, + \"serviceRef\": { + \"name\": \"backstage-psql\", + \"namespace\": \"${ns}\", + \"databaseName\": \"backstage_plugin_orchestrator\", + \"databaseSchema\": \"${name}\" + } + } + }, + \"podTemplate\": { + \"container\": { + \"image\": \"${image}\", + \"env\": [{\"name\": \"KOGITO_SERVICE_URL\", \"value\": \"http://${name}.${ns}.svc.cluster.local\"}] + } + } + } + }" >/dev/null || true +} + +wait_smoke_workflows_ready() { + local ns="$1" timeout_secs="${2:-600}" start elapsed ready + start="$(date +%s)" + while true; do + ready=true + for name in greeting failswitch; do + local replicas + replicas="$(oc get deployment "$name" -n "$ns" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || true)" + if [[ "$replicas" != "1" ]]; then + ready=false + fi + done + if [[ "$ready" == "true" ]]; then + log "smoke workflows greeting/failswitch are ready" + return 0 + fi + elapsed=$(( $(date +%s) - start )) + if (( elapsed >= timeout_secs )); then + die "timeout waiting for greeting/failswitch deployments in $ns" + fi + sleep 10 + done +} + +ensure_smoke_workflows() { + local ns="$1" + local tag + tag="$(workflow_osl_image_tag)" + log "deploying smoke workflows (image tag osl_${tag})" + local workflow_dir + workflow_dir="$(mktemp -d /tmp/osl-workflows-XXXXXX)" + git clone --depth 1 "$WORKFLOW_REPO" "$workflow_dir" >/dev/null + git -C "$workflow_dir" fetch --depth 1 origin "$WORKFLOW_REPO_REF" >/dev/null + git -C "$workflow_dir" checkout --detach "$WORKFLOW_REPO_REF" >/dev/null + oc apply -n "$ns" -f "${workflow_dir}/workflows/greeting/manifests" + oc apply -n "$ns" -f "${workflow_dir}/workflows/fail-switch/src/main/resources/manifests" + rm -rf "$workflow_dir" + patch_smoke_workflow "$ns" greeting "$tag" + patch_smoke_workflow "$ns" failswitch "$tag" + wait_smoke_workflows_ready "$ns" 600 + oc rollout restart "deploy/sonataflow-platform-data-index-service" -n "$ns" >/dev/null 2>&1 || true + oc rollout status "deploy/sonataflow-platform-data-index-service" -n "$ns" --timeout=180s >/dev/null 2>&1 || true +} + +ensure_e2e_deps() { + local e2e="$1" + if [[ -d "${e2e}/node_modules" ]]; then + return 0 + fi + log "yarn install in ${e2e}" + if command -v corepack >/dev/null 2>&1; then + (cd "$e2e" && corepack yarn install) + elif command -v npx >/dev/null 2>&1; then + (cd "$e2e" && npx --yes corepack yarn install) + else + (cd "$e2e" && yarn install) + fi +} + +playwright_cmd() { + local e2e="$1" + local local_bin="${e2e}/node_modules/.bin/playwright" + if [[ -x "$local_bin" ]]; then + echo "$local_bin" + return + fi + if command -v corepack >/dev/null 2>&1; then + echo "corepack yarn playwright" + return + fi + echo "yarn playwright" +} + +write_overlays_dotenv() { + local e2e="$1" + local path="${e2e}/.env" + local backup="" + if [[ -f "$path" ]]; then + backup="${e2e}/.env.osl-regression.bak" + cp -a "$path" "$backup" + fi + cat > "$path" </dev/null || true)" + [[ -n "$image" ]] || die "cannot resolve RHDH image for data-index rewrite proxy" + rewrite_url="http://${name}.${ns}.svc.cluster.local" + log "ensuring data-index rewrite proxy ${name} -> sonataflow-platform-data-index-service" + oc create configmap "$name" \ + --from-file=osl-di-rewrite.js="${SCRIPT_DIR}/utils/orchestrator/osl-di-rewrite.js" \ + -n "$ns" --dry-run=client -o yaml | oc apply -f - >/dev/null + oc apply -f - >/dev/null </dev/null + oidc_tmp="$(mktemp)" + oc get configmap app-config-oidc -n "$ns" -o jsonpath='{.data.app-config-oidc\.yaml}' > "$oidc_tmp" + python - "$oidc_tmp" "$rewrite_url" <<'PY' +from pathlib import Path +import sys +path = Path(sys.argv[1]) +url = sys.argv[2] +text = path.read_text() +lines = [] +replaced = False +for line in text.splitlines(): + if line.strip().startswith("url:") and not replaced: + indent = line[: len(line) - len(line.lstrip())] + lines.append(f"{indent}url: {url}") + replaced = True + else: + lines.append(line) +path.write_text("\n".join(lines) + "\n") +PY + oc create configmap app-config-oidc \ + --from-file=app-config-oidc.yaml="$oidc_tmp" \ + -n "$ns" --dry-run=client -o yaml | oc apply -f - >/dev/null + rm -f "$oidc_tmp" + oc rollout restart "deploy/redhat-developer-hub" -n "$ns" >/dev/null + oc rollout status "deploy/redhat-developer-hub" -n "$ns" --timeout=300s >/dev/null + log "data-index rewrite proxy ready (${rewrite_url})" +} + +phase_deploy() { + log "[deploy] RHDH ${rhdh} namespace=${namespace}" + if [[ -f "${SCRIPT_DIR}/.env.osl" ]]; then + # shellcheck disable=SC1091 + source "${SCRIPT_DIR}/.env.osl" + fi + POST_SETUP_WORKFLOW_SMOKE=0 \ + SKIP_EMPTY_BASELINE=1 \ + ALLOW_OSL_SERVERLESS_VERSION_SKEW=1 \ + "${SCRIPT_DIR}/setup-orchestrator.sh" "$rhdh" --namespace "$namespace" + ensure_dataindex_rewrite "$namespace" +} + +phase_test() { + log "[test]" + overlays_dir="$(cd "$overlays_dir" && pwd)" + local e2e smoke_spec="" backup="" rc=0 + local -a probe_args + e2e="$(overlays_e2e_dir)" + ensure_e2e_deps "$e2e" + + export K8S_CLUSTER_ROUTER_BASE RHDH_BASE_URL KEYCLOAK_BASE_URL RHDH_VERSION + export ORCH_E2E_USE_EXISTING_RHDH=true + export ORCH_E2E_SKIP_WORKFLOW_DEPLOY=false + export ORCH_E2E_SKIP_BASELINE_RBAC=false + export SKIP_KEYCLOAK_DEPLOYMENT=true + export SKIP_OPERATOR_INSTALLATION=true + export GH_USER_ID=test1 + export GH_USER_PASS=test1@123 + export KEYCLOAK_REALM=rhdh + export KEYCLOAK_LOGIN_REALM=rhdh + export KEYCLOAK_CLIENT_ID=rhdh-client + export KEYCLOAK_CLIENT_SECRET=rhdh-client-secret + K8S_CLUSTER_ROUTER_BASE="$(cluster_router_base)" + RHDH_BASE_URL="$(route_url "$RHDH_RELEASE" "$namespace")" + KEYCLOAK_BASE_URL="$(route_url "$KEYCLOAK_RELEASE" "$KEYCLOAK_NS" http)" + RHDH_VERSION="${rhdh}" + ensure_dataindex_rewrite "$namespace" + + backup="$(write_overlays_dotenv "$e2e")" + cleanup_test_artifacts() { + restore_overlays_dotenv "$e2e" "$backup" + if [[ -n "${smoke_spec}" && -f "${smoke_spec}" ]]; then + rm -f "$smoke_spec" + fi + } + trap cleanup_test_artifacts EXIT + + if [[ "$full_e2e" != "true" ]]; then + ensure_smoke_workflows "$namespace" + probe_args=(python3 "${SCRIPT_DIR}/utils/orchestrator/osl_smoke.py" probe --namespace "$namespace") + if [[ "$allow_relative_service_url" == "true" || "${ALLOW_RELATIVE_SERVICE_URL:-}" == "1" ]]; then + probe_args+=(--allow-relative) + fi + log "probing raw Data Index GraphQL ProcessDefinitions.serviceUrl" + "${probe_args[@]}" + smoke_spec="${e2e}/tests/${SMOKE_WRAPPER_NAME}" + cp -a "$SMOKE_WRAPPER_SRC" "$smoke_spec" + fi + + local pw + pw="$(playwright_cmd "$e2e")" + log "Playwright: ${pw} (cwd=${e2e})" + set +e + if [[ "$full_e2e" == "true" ]]; then + # shellcheck disable=SC2086 + (cd "$e2e" && $pw test --project=orchestrator --workers=1) + else + # shellcheck disable=SC2086 + (cd "$e2e" && $pw test --project=orchestrator --workers=1 "$smoke_spec") + fi + rc=$? + set -e + + cleanup_test_artifacts + trap - EXIT + smoke_spec="" + + if [[ $rc -ne 0 ]]; then + log "Playwright failed (exit ${rc}); report: ${e2e}/playwright-report" + exit "$rc" + fi + log "Playwright smoke/full suite passed" +} + +preflight + +if [[ "$run_cleanup" == "true" ]]; then + phase_cleanup +fi +if [[ "$run_prepare" == "true" ]]; then + phase_prepare +fi +if [[ "$run_deploy" == "true" ]]; then + phase_deploy +fi +if [[ "$run_test" == "true" ]]; then + phase_test +fi diff --git a/utils/orchestrator/osl_smoke.py b/utils/orchestrator/osl_smoke.py index b2435ca..f37d6ef 100644 --- a/utils/orchestrator/osl_smoke.py +++ b/utils/orchestrator/osl_smoke.py @@ -4,9 +4,13 @@ import argparse import json +import os +import subprocess import sys from typing import Any, Optional +GRAPHQL_QUERY = "{ ProcessDefinitions { id serviceUrl endpoint } }" + SMOKE_TITLES = [ "Run Greeting workflow and verify Workflows tab", "Run Failswitch workflow and verify statuses", @@ -53,6 +57,65 @@ def classify_definitions(definitions: list) -> dict[str, Any]: return {"ok": not problems, "problems": problems} +def graphql_query() -> str: + return GRAPHQL_QUERY + + +def curl_probe_argv(namespace: str) -> list[str]: + body = json.dumps({"query": graphql_query()}) + url = ( + f"http://sonataflow-platform-data-index-service.{namespace}" + ".svc.cluster.local/graphql" + ) + return [ + "oc", + "exec", + "-n", + namespace, + "deploy/redhat-developer-hub", + "--", + "curl", + "-sS", + "-X", + "POST", + "-H", + "Content-Type: application/json", + "-d", + body, + url, + ] + + +def _cmd_probe(namespace: str, allow_relative: bool) -> int: + argv = curl_probe_argv(namespace) + proc = subprocess.run(argv, capture_output=True, text=True, check=False) + if proc.returncode != 0: + sys.stderr.write(proc.stderr or proc.stdout or "oc exec curl failed\n") + return 1 + try: + payload = json.loads(proc.stdout) + except json.JSONDecodeError: + sys.stderr.write(f"Data Index did not return JSON: {proc.stdout[:500]}\n") + return 1 + if payload.get("errors"): + sys.stderr.write(json.dumps(payload["errors"]) + "\n") + return 1 + definitions = (payload.get("data") or {}).get("ProcessDefinitions") or [] + result = classify_definitions(definitions) + json.dump(result, sys.stderr, indent=2) + sys.stderr.write("\n") + if result["ok"]: + return 0 + if allow_relative: + sys.stderr.write( + "WARNING: relative/missing serviceUrl allowed by ALLOW_RELATIVE_SERVICE_URL\n" + ) + return 0 + if result["problems"] and result["problems"][0]["reason"] == "no-process-definitions": + return 1 + return 2 + + def _cmd_grep() -> int: print(playwright_grep()) return 0 @@ -76,9 +139,15 @@ def main(argv: Optional[list[str]] = None) -> int: sub = parser.add_subparsers(dest="cmd", required=True) sub.add_parser("grep") sub.add_parser("classify") + probe_p = sub.add_parser("probe") + probe_p.add_argument("--namespace", required=True) + probe_p.add_argument("--allow-relative", action="store_true") args = parser.parse_args(argv) if args.cmd == "grep": return _cmd_grep() + if args.cmd == "probe": + allow = args.allow_relative or os.environ.get("ALLOW_RELATIVE_SERVICE_URL") == "1" + return _cmd_probe(args.namespace, allow) return _cmd_classify() diff --git a/utils/orchestrator/test_osl_smoke.py b/utils/orchestrator/test_osl_smoke.py index 409d04b..83882b1 100644 --- a/utils/orchestrator/test_osl_smoke.py +++ b/utils/orchestrator/test_osl_smoke.py @@ -106,5 +106,20 @@ def test_classify_stdin_exit_2_on_relative(self): self.assertEqual(proc.returncode, 2) +class TestProbeArgv(unittest.TestCase): + def test_curl_targets_raw_data_index_not_rewrite(self): + argv = osl_smoke.curl_probe_argv("orchestrator") + joined = " ".join(argv) + self.assertIn("sonataflow-platform-data-index-service.orchestrator.svc.cluster.local/graphql", joined) + self.assertNotIn("osl-di-rewrite", joined) + self.assertIn("ProcessDefinitions", joined) + + def test_graphql_query_asks_for_service_url_and_endpoint(self): + q = osl_smoke.graphql_query() + self.assertIn("serviceUrl", q) + self.assertIn("endpoint", q) + self.assertIn("ProcessDefinitions", q) + + if __name__ == "__main__": unittest.main() From 3c43b5ee6485ed5cf5e65d9120ec0b3eadeff2ef Mon Sep 17 00:00:00 2001 From: Rostislav Lan Date: Thu, 20 Aug 2026 10:38:30 +0200 Subject: [PATCH 03/13] feat: limit OSL RC Playwright to greeting, failswitch, retrigger, token-propagation #13375 Co-authored-by: Cursor --- README.md | 36 ++++++++++++++++++++++++++++ run-osl-regression.sh | 10 ++++---- utils/orchestrator/test_osl_smoke.py | 8 +++++++ 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 58576a1..aecb975 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,40 @@ make undeploy-infra make clean ``` +#### OSL RC smoke + +Pin an OSL pre-release against a chosen RHDH version, deploy, and run the default four Playwright tests (skips `orchestrator.spec.ts` beforeAll so it does not reinstall operators): + +1. `Run Greeting workflow and verify Workflows tab` +2. `Run Failswitch workflow and verify statuses` +3. `Rerun Failswitch from failure point` +4. `Execute token-propagation workflow via API` + +Smoke always deploys greeting, failswitch, token-propagation, and `sample-server`. A GraphQL probe hits the raw Data Index (`sonataflow-platform-data-index-service`) and fails if `ProcessDefinitions.serviceUrl` is relative unless you pass `--allow-relative-service-url` (needed on 1.39.CR1 until the Orchestrator plugin derives `serviceUrl` from `endpoint`; SRVLOGIC-1137). `--full-e2e` is the RHDH plugin suite (RBAC, entity, ui:props, Loki, all workflows), not the OSL CR default. + +```bash +# One-shot: cleanup (including operators) -> mirror OSL -> deploy -> smoke +make osl-regression VERSION=next OSL_RELEASE=1.39.0.CR1 ORCH_NAMESPACE=orchestrator + +# Or call the driver directly +./run-osl-regression.sh --all --rhdh next --osl-release 1.39.0.CR1 --namespace orchestrator +./run-osl-regression.sh --cleanup --include-operators --namespace orchestrator +# 1.39.CR1 currently needs the DI contract override plus rewrite proxy: +ALLOW_RELATIVE_SERVICE_URL=1 ./run-osl-regression.sh --test --namespace orchestrator +./run-osl-regression.sh --test --full-e2e --namespace orchestrator +./run-osl-regression.sh --test --overlays-dir ../rhdh-plugin-export-overlays +``` + +Individual pieces: + +```bash +make prepare-osl OSL_RELEASE=1.39.0.CR1 +make setup-orchestrator VERSION=next ORCH_NAMESPACE=orchestrator OSL_RELEASE=1.39.0.CR1 +make cleanup-full ORCH_NAMESPACE=orchestrator +``` + +Requires `oc` logged in, `helm`, `skopeo`, `podman`, and a sibling `rhdh-plugin-export-overlays` checkout for `--test`. Manifests live in `config/osl-releases/`. + #### Status and Debugging ```bash @@ -200,6 +234,8 @@ All make commands accept these variables: | `USE_CONTAINER` | `false` | Set to `true` to run commands inside the e2e-runner container | | `CATALOG_INDEX_TAG` | auto | Catalog index image tag (defaults to major.minor from version, or `next`) | | `RUNNER_IMAGE` | `quay.io/rhdh-community/rhdh-e2e-runner:main` | Container image for `install-operator` | +| `OSL_RELEASE` | _(empty)_ | OSL pre-release id for `prepare-osl` / `osl-regression` | +| `ORCH_NAMESPACE` | `orchestrator` | Namespace used by orchestrator/OSL setup and cleanup | > **Note:** `install-operator` requires you to be logged into the cluster via `oc login` on your host. > It automatically passes the session token to the e2e-runner container (needs Linux tools like `umoci`, `opm`, `skopeo`). diff --git a/run-osl-regression.sh b/run-osl-regression.sh index b41a7a1..6bf54c4 100755 --- a/run-osl-regression.sh +++ b/run-osl-regression.sh @@ -2,8 +2,8 @@ # # Thin OSL RC regression driver (RHIDP-13375). # Phases: cleanup -> prepare-osl -> deploy -> test. -# Smoke Playwright skips overlays orchestrator.spec.ts beforeAll and only -# registers greeting/failswitch tests via playwright/osl-regression-smoke.spec.ts. +# Smoke Playwright skips overlays orchestrator.spec.ts beforeAll and greps +# four titles via playwright/osl-regression-smoke.spec.ts. # # Usage: # ./run-osl-regression.sh --all --rhdh next --osl-release 1.39.0.CR1 @@ -486,7 +486,7 @@ phase_deploy() { phase_test() { log "[test]" overlays_dir="$(cd "$overlays_dir" && pwd)" - local e2e smoke_spec="" backup="" rc=0 + local e2e smoke_spec="" backup="" rc=0 smoke_grep="" local -a probe_args e2e="$(overlays_e2e_dir)" ensure_e2e_deps "$e2e" @@ -538,8 +538,10 @@ phase_test() { # shellcheck disable=SC2086 (cd "$e2e" && $pw test --project=orchestrator --workers=1) else + smoke_grep="$(python3 "${SCRIPT_DIR}/utils/orchestrator/osl_smoke.py" grep)" + log "Playwright grep: ${smoke_grep}" # shellcheck disable=SC2086 - (cd "$e2e" && $pw test --project=orchestrator --workers=1 "$smoke_spec") + (cd "$e2e" && $pw test --project=orchestrator --workers=1 --grep "$smoke_grep" "$smoke_spec") fi rc=$? set -e diff --git a/utils/orchestrator/test_osl_smoke.py b/utils/orchestrator/test_osl_smoke.py index 83882b1..9a72066 100644 --- a/utils/orchestrator/test_osl_smoke.py +++ b/utils/orchestrator/test_osl_smoke.py @@ -121,5 +121,13 @@ def test_graphql_query_asks_for_service_url_and_endpoint(self): self.assertIn("ProcessDefinitions", q) +class TestDriverGrepWiring(unittest.TestCase): + def test_run_script_mentions_osl_smoke_grep(self): + text = Path(__file__).resolve().parents[2].joinpath("run-osl-regression.sh").read_text() + self.assertIn("osl_smoke.py", text) + self.assertIn("grep", text) + self.assertIn("--grep", text) + + if __name__ == "__main__": unittest.main() From fb2e9dc17066eef207ef489867d1909657a0130d Mon Sep 17 00:00:00 2001 From: Rostislav Lan Date: Thu, 20 Aug 2026 10:40:45 +0200 Subject: [PATCH 04/13] feat: include token-propagation in default OSL RC smoke #13375 Co-authored-by: Cursor --- README.md | 2 +- playwright/osl-regression-smoke.spec.ts | 105 ++++++++++++++++++ run-osl-regression.sh | 135 +++++++++++++++++++++++- utils/orchestrator/test_osl_smoke.py | 18 ++++ 4 files changed, 255 insertions(+), 5 deletions(-) create mode 100644 playwright/osl-regression-smoke.spec.ts diff --git a/README.md b/README.md index aecb975..e694dd6 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ Pin an OSL pre-release against a chosen RHDH version, deploy, and run the defaul 3. `Rerun Failswitch from failure point` 4. `Execute token-propagation workflow via API` -Smoke always deploys greeting, failswitch, token-propagation, and `sample-server`. A GraphQL probe hits the raw Data Index (`sonataflow-platform-data-index-service`) and fails if `ProcessDefinitions.serviceUrl` is relative unless you pass `--allow-relative-service-url` (needed on 1.39.CR1 until the Orchestrator plugin derives `serviceUrl` from `endpoint`; SRVLOGIC-1137). `--full-e2e` is the RHDH plugin suite (RBAC, entity, ui:props, Loki, all workflows), not the OSL CR default. +Smoke always deploys greeting, failswitch, token-propagation, and `sample-server`, then runs token-propagation (JWT/OpenAPI into the workflow). A GraphQL probe hits the raw Data Index (`sonataflow-platform-data-index-service`) and fails if `ProcessDefinitions.serviceUrl` is relative unless you pass `--allow-relative-service-url` (needed on 1.39.CR1 until the Orchestrator plugin derives `serviceUrl` from `endpoint`; SRVLOGIC-1137). `--full-e2e` is the RHDH plugin suite (RBAC, entity, ui:props, Loki, all workflows), not the OSL CR default. ```bash # One-shot: cleanup (including operators) -> mirror OSL -> deploy -> smoke diff --git a/playwright/osl-regression-smoke.spec.ts b/playwright/osl-regression-smoke.spec.ts new file mode 100644 index 0000000..2cf414f --- /dev/null +++ b/playwright/osl-regression-smoke.spec.ts @@ -0,0 +1,105 @@ +// Smoke entry for run-osl-regression.sh (copied into overlays e2e tests/ at runtime). +// Overlays orchestrator-workflow-core.tests.ts only exports a register function; +// orchestrator.spec.ts beforeAll would reinstall operators / Helm-redeploy RHDH. +// NFS Alpha copy also drifts from e2e-utils locators. +// @ts-nocheck +import { test, expect } from "@red-hat-developer-hub/e2e-test-utils/test"; +import { OrchestratorPage } from "@red-hat-developer-hub/e2e-test-utils/pages"; +import { createDataIndexGuard, requireEnvVar } from "./support/utils/orchestrator-workflow-helpers.js"; +import { registerOrchestratorCoreWorkflowTests } from "./specs/orchestrator-workflow-core.tests.js"; +import { registerTokenPropagationWorkflowTests } from "./specs/orchestrator-token-propagation.tests.js"; +import { ORCHESTRATOR_COMPONENTS } from "./support/pages/orchestrator-obj.js"; + +ORCHESTRATOR_COMPONENTS.workflowsHeading = (page) => + page.getByRole("heading", { name: /Workflows|Workflow Orchestrator/ }); +ORCHESTRATOR_COMPONENTS.runButton = (page) => + page.getByRole("button", { name: "Run", exact: true }); + +OrchestratorPage.prototype.validateGreetingWorkflow = async function () { + const page = this.page; + await page.getByRole("tab", { name: /Workflows/ }).click(); + await expect( + page.getByRole("heading", { name: /Workflows|Workflow Orchestrator/ }), + ).toBeVisible(); + await expect(page.locator('input[aria-label="Filter"]')).toHaveAttribute( + "placeholder", + "Filter", + ); + for (const name of ["Name", "Workflow Status", "Actions"]) { + await expect( + page.getByRole("columnheader", { name, exact: true }), + ).toBeVisible(); + } + const row = page.locator('tr:has-text("Greeting workflow")'); + await expect(row.locator("td").nth(0)).toHaveText("Greeting workflow"); + await expect(row.locator("td").nth(1)).toHaveText("Available"); + await expect( + row.getByRole("button", { name: "Run", exact: true }).first(), + ).toBeVisible(); + await expect(row.getByRole("button", { name: "View runs" }).first()).toBeVisible(); +}; + +test.beforeEach(async ({ page }) => { + const origGetByRole = page.getByRole.bind(page); + page.getByRole = (role, options) => { + if (role === "button" && options && options.name === "Run") { + return origGetByRole(role, { ...options, exact: true }); + } + if (role === "heading" && options && options.name === "Workflows") { + return origGetByRole(role, { + ...options, + name: /^(Workflows|Workflow Orchestrator)$/, + }); + } + if (role === "columnheader" && options && options.name === "Run Status") { + return origGetByRole(role, { name: /^(Run Status|Status)$/ }); + } + if (role === "columnheader" && options && options.name === "Duration") { + return origGetByRole(role, { name: /^(Duration|Version)$/ }); + } + return origGetByRole(role, options); + }; + + const origGetByText = page.getByText.bind(page); + page.getByText = (text, options) => { + if (text === "Run has aborted") { + return origGetByText(/Run (has|was) aborted/); + } + const loc = origGetByText(text, options); + if ( + options && + options.exact && + typeof text === "string" && + ["Completed", "Failed", "Running"].includes(text) + ) { + return loc.first(); + } + return loc; + }; + + const origGetByTestId = page.getByTestId.bind(page); + page.getByTestId = (testId, options) => { + if (testId === "info-card-subheader") { + return page + .getByRole("heading", { name: /^Run status$/i }) + .locator("xpath=following-sibling::*"); + } + return origGetByTestId(testId, options); + }; + + const assertions = Object.getPrototypeOf(expect(page.locator("body"))); + if (assertions && !assertions.__oslPatchedToHaveText && assertions.toHaveText) { + const origToHaveText = assertions.toHaveText; + assertions.toHaveText = async function (expected, options) { + if (expected === "Workflows") { + expected = /^(Workflows|Workflow Orchestrator)$/; + } + return origToHaveText.call(this, expected, options); + }; + assertions.__oslPatchedToHaveText = true; + } +}); + +const ensureDataIndexOrSkip = createDataIndexGuard(); +registerOrchestratorCoreWorkflowTests(ensureDataIndexOrSkip); +registerTokenPropagationWorkflowTests(requireEnvVar); diff --git a/run-osl-regression.sh b/run-osl-regression.sh index 6bf54c4..6756174 100755 --- a/run-osl-regression.sh +++ b/run-osl-regression.sh @@ -26,6 +26,7 @@ SMOKE_WRAPPER_SRC="${SCRIPT_DIR}/playwright/osl-regression-smoke.spec.ts" SMOKE_WRAPPER_NAME="osl-regression-smoke.spec.ts" WORKFLOW_REPO="${SERVERLESS_WORKFLOWS_REPO:-https://github.com/rhdhorchestrator/serverless-workflows.git}" WORKFLOW_REPO_REF="${SERVERLESS_WORKFLOWS_REF:-daeeee8dec16beab6d96a81774ef500081a2c2b0}" +DEMO_WORKFLOW_REPO="${ORCHESTRATOR_DEMO_REPO:-https://github.com/rhdhorchestrator/orchestrator-demo.git}" run_all=false run_cleanup=false @@ -205,10 +206,34 @@ workflow_osl_image_tag() { } patch_smoke_workflow() { - local ns="$1" name="$2" tag="$3" image + local ns="$1" name="$2" tag="${3:-}" image persistence + persistence="{ + \"spec\": { + \"persistence\": { + \"dbMigrationStrategy\": \"job\", + \"postgresql\": { + \"secretRef\": { + \"name\": \"backstage-psql-secret\", + \"userKey\": \"POSTGRES_USER\", + \"passwordKey\": \"POSTGRES_PASSWORD\" + }, + \"serviceRef\": { + \"name\": \"backstage-psql\", + \"namespace\": \"${ns}\", + \"databaseName\": \"backstage_plugin_orchestrator\", + \"databaseSchema\": \"${name}\" + } + } + } + } + }" case "$name" in greeting) image="quay.io/orchestrator/serverless-workflow-greeting:osl_${tag}" ;; failswitch) image="quay.io/orchestrator/fail-switch:osl_${tag}" ;; + token-propagation) + oc -n "$ns" patch sonataflow "$name" --type merge -p "$persistence" >/dev/null || true + return 0 + ;; *) die "unknown smoke workflow: $name" ;; esac oc -n "$ns" patch sonataflow "$name" --type merge -p "{ @@ -244,7 +269,7 @@ wait_smoke_workflows_ready() { start="$(date +%s)" while true; do ready=true - for name in greeting failswitch; do + for name in greeting failswitch token-propagation; do local replicas replicas="$(oc get deployment "$name" -n "$ns" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || true)" if [[ "$replicas" != "1" ]]; then @@ -252,17 +277,118 @@ wait_smoke_workflows_ready() { fi done if [[ "$ready" == "true" ]]; then - log "smoke workflows greeting/failswitch are ready" + log "smoke workflows greeting/failswitch/token-propagation are ready" return 0 fi elapsed=$(( $(date +%s) - start )) if (( elapsed >= timeout_secs )); then - die "timeout waiting for greeting/failswitch deployments in $ns" + die "timeout waiting for greeting/failswitch/token-propagation deployments in $ns" fi sleep 10 done } +ensure_token_propagation_workflow() { + local ns="$1" + local demo_dir manifests_dir props_cm specs_cm + [[ -n "${KEYCLOAK_BASE_URL:-}" ]] || die "KEYCLOAK_BASE_URL is required for token-propagation smoke" + log "deploying token-propagation workflow and sample-server" + demo_dir="$(mktemp -d /tmp/osl-token-demo-XXXXXX)" + _osl_token_demo_cleanup() { rm -rf "$demo_dir"; trap - RETURN; } + trap _osl_token_demo_cleanup RETURN + git clone --depth 1 "$DEMO_WORKFLOW_REPO" "$demo_dir" >/dev/null + manifests_dir="${demo_dir}/09_token_propagation/manifests" + props_cm="${manifests_dir}/01-configmap_token-propagation-props.yaml" + specs_cm="${manifests_dir}/03-configmap_02-token-propagation-resources-specs.yaml" + [[ -f "$props_cm" && -f "$specs_cm" ]] || die "token-propagation manifests missing in $DEMO_WORKFLOW_REPO" + python3 - "$ns" "$props_cm" "$specs_cm" <<'PY' +from pathlib import Path +import os +import sys + +ns, props_path, specs_path = sys.argv[1], Path(sys.argv[2]), Path(sys.argv[3]) +kc = os.environ["KEYCLOAK_BASE_URL"].rstrip("/") +realm = os.environ.get("KEYCLOAK_REALM", "rhdh") +client_id = os.environ.get("KEYCLOAK_CLIENT_ID", "rhdh-client") +client_secret = os.environ.get("KEYCLOAK_CLIENT_SECRET", "rhdh-client-secret") +auth_server_url = f"{kc}/realms/{realm}" +token_url = f"{auth_server_url}/protocol/openid-connect/token" +props = props_path.read_text() +props = props.replace( + "http://example-kc-service.keycloak:8080/realms/quarkus", + auth_server_url, +) +props = props.replace("client-id=quarkus-app", f"client-id={client_id}") +props = props.replace( + "client-secret=lVGSvdaoDUem7lqeAnqXn1F92dCPbQea", + f"client-secret={client_secret}", +) +props = props.replace( + "http://sample-server-service.rhdh-operator", + f"http://sample-server-service.{ns}:8080", +) +props_path.write_text(props) +specs_path.write_text( + specs_path.read_text().replace( + "http://example-kc-service.keycloak:8080/realms/quarkus/protocol/openid-connect/token", + token_url, + ) +) +PY + oc apply -n "$ns" -f - </dev/null 2>&1 || true oc rollout status "deploy/sonataflow-platform-data-index-service" -n "$ns" --timeout=180s >/dev/null 2>&1 || true diff --git a/utils/orchestrator/test_osl_smoke.py b/utils/orchestrator/test_osl_smoke.py index 9a72066..aaf26a8 100644 --- a/utils/orchestrator/test_osl_smoke.py +++ b/utils/orchestrator/test_osl_smoke.py @@ -129,5 +129,23 @@ def test_run_script_mentions_osl_smoke_grep(self): self.assertIn("--grep", text) +class TestTokenSmokeWiring(unittest.TestCase): + def test_driver_always_deploys_token_propagation(self): + text = Path(__file__).resolve().parents[2].joinpath("run-osl-regression.sh").read_text() + self.assertIn("ensure_token_propagation_workflow", text) + self.assertIn("token-propagation", text) + self.assertNotIn("--include-token-propagation", text) + self.assertNotIn("OSL_SMOKE_TOKEN_PROPAGATION", text) + + def test_smoke_wrapper_always_registers_token_tests(self): + text = ( + Path(__file__).resolve().parents[2] + / "playwright" + / "osl-regression-smoke.spec.ts" + ).read_text() + self.assertIn("registerTokenPropagationWorkflowTests", text) + self.assertNotIn("OSL_SMOKE_TOKEN_PROPAGATION", text) + + if __name__ == "__main__": unittest.main() From 6e3367f7ff304e2f923104071c8d8dd10f85a4c5 Mon Sep 17 00:00:00 2001 From: Rostislav Lan Date: Thu, 20 Aug 2026 10:41:19 +0200 Subject: [PATCH 05/13] docs: document OSL RC 4-test smoke including token-propagation #13375 Co-authored-by: Cursor --- Makefile | 28 ++++++++++++++++++++++++++++ README.md | 2 ++ 2 files changed, 30 insertions(+) diff --git a/Makefile b/Makefile index dc0c6ff..a5c053b 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,8 @@ PLUGINS ?= USE_CONTAINER ?= false CATALOG_INDEX_TAG ?= RUNNER_IMAGE ?= quay.io/rhdh-community/rhdh-e2e-runner:main +OSL_RELEASE ?= +ORCH_NAMESPACE ?= orchestrator export CATALOG_INDEX_TAG @@ -68,6 +70,32 @@ undeploy-infra: ## Uninstall orchestrator infra chart clean: ## Delete the entire namespace (removes everything) oc delete project $(NAMESPACE) --ignore-not-found +# ── Orchestrator / OSL RC smoke ─────────────────────────────────────────────── + +.PHONY: prepare-osl setup-orchestrator cleanup cleanup-full osl-regression + +prepare-osl: ## Mirror pre-release OSL images (OSL_RELEASE=1.39.0.CR1) +ifndef OSL_RELEASE + $(error OSL_RELEASE is required, e.g. make prepare-osl OSL_RELEASE=1.39.0.CR1) +endif + ./prepare-osl-internal.sh --release $(OSL_RELEASE) + +setup-orchestrator: ## Full RHDH + orchestrator setup (VERSION, ORCH_NAMESPACE, OSL_RELEASE) + ./setup-orchestrator.sh $(VERSION) --namespace $(ORCH_NAMESPACE) $(if $(filter-out ,$(OSL_RELEASE)),--prepare-internal-osl $(OSL_RELEASE)) + +cleanup: ## Clean RHDH/orchestrator/OSL resources from ORCH_NAMESPACE + ./cleanup.sh --namespace $(ORCH_NAMESPACE) + +cleanup-full: ## Full cleanup: operators + related namespaces + ./cleanup.sh --namespace $(ORCH_NAMESPACE) --include-operators --delete-namespace + +osl-regression: ## Cleanup + prepare OSL + deploy + 4-test smoke (VERSION, OSL_RELEASE) +ifndef OSL_RELEASE + $(error OSL_RELEASE is required, e.g. make osl-regression VERSION=next OSL_RELEASE=1.39.0.CR1) +endif + ./run-osl-regression.sh --all --rhdh $(VERSION) --osl-release $(OSL_RELEASE) --namespace $(ORCH_NAMESPACE) \ + $(if $(filter 1,$(ALLOW_RELATIVE_SERVICE_URL)),--allow-relative-service-url,) + # ── Status ──────────────────────────────────────────────────────────────────── .PHONY: status logs url diff --git a/README.md b/README.md index e694dd6..154ebfd 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,8 @@ Smoke always deploys greeting, failswitch, token-propagation, and `sample-server ```bash # One-shot: cleanup (including operators) -> mirror OSL -> deploy -> smoke make osl-regression VERSION=next OSL_RELEASE=1.39.0.CR1 ORCH_NAMESPACE=orchestrator +# 1.39.CR1 currently needs the DI contract override: +ALLOW_RELATIVE_SERVICE_URL=1 make osl-regression VERSION=next OSL_RELEASE=1.39.0.CR1 ORCH_NAMESPACE=orchestrator # Or call the driver directly ./run-osl-regression.sh --all --rhdh next --osl-release 1.39.0.CR1 --namespace orchestrator From 4422a83edd0e839bdf306c7bbde96a69be958b1a Mon Sep 17 00:00:00 2001 From: Rostislav Lan Date: Thu, 20 Aug 2026 11:54:15 +0200 Subject: [PATCH 06/13] feat: land bash OSL RC smoke driver with full operator cleanup #13375 Replace the Python probe helper with oc/jq, always tear down operators on cleanup, drop --full-e2e, and ship the remaining OSL prepare/deploy scripts. Co-authored-by: Cursor --- .env.example | 6 + .gitignore | 5 +- Makefile | 4 +- README.md | 14 +- cleanup.sh | 313 ++++++++++ config/app-config-oidc.yaml | 20 + config/orchestrator-dynamic-plugins-next.yaml | 29 + config/osl-releases/1.39.0.CR1.json | 64 ++ config/osl-releases/README.md | 50 ++ config/osl-releases/example.json | 56 ++ config/rbac-policies.yaml | 2 + deploy.sh | 4 +- ...0-orchestrator-serviceurl-from-endpoint.md | 299 ++++++++++ .../plans/2026-08-20-osl-rc-smoke-subset.md | 21 +- ...0-orchestrator-serviceurl-from-endpoint.md | 37 ++ .../specs/2026-08-20-osl-rc-smoke-subset.md | 14 +- helm/deploy.sh | 62 ++ playwright/osl-regression-smoke.spec.ts | 10 - prepare-osl-internal.sh | 538 +++++++++++++++++ resources/keycloak/dynamic-plugins.yaml | 3 +- run-osl-regression.sh | 187 +++--- scripts/setup-resources.sh | 20 +- setup-orchestrator.sh | 552 ++++++++++++++++++ utils/keycloak/groups.json | 5 + utils/keycloak/keycloak-deploy.sh | 240 ++++++++ utils/keycloak/keycloak-values.yaml | 104 ++++ utils/keycloak/rhdh-client.json | 86 +++ utils/keycloak/users.json | 22 + utils/orchestrator/osl-di-rewrite.js | 104 ++++ utils/orchestrator/osl_smoke.py | 155 ----- utils/orchestrator/test_osl_smoke.py | 151 ----- utils/orchestrator/verify-existing-rhdh.sh | 98 ++++ 32 files changed, 2827 insertions(+), 448 deletions(-) create mode 100755 cleanup.sh create mode 100644 config/app-config-oidc.yaml create mode 100644 config/orchestrator-dynamic-plugins-next.yaml create mode 100644 config/osl-releases/1.39.0.CR1.json create mode 100755 config/osl-releases/README.md create mode 100755 config/osl-releases/example.json create mode 100644 docs/superpowers/plans/2026-08-20-orchestrator-serviceurl-from-endpoint.md create mode 100644 docs/superpowers/specs/2026-08-20-orchestrator-serviceurl-from-endpoint.md create mode 100755 prepare-osl-internal.sh create mode 100755 setup-orchestrator.sh create mode 100755 utils/keycloak/groups.json create mode 100755 utils/keycloak/keycloak-deploy.sh create mode 100755 utils/keycloak/keycloak-values.yaml create mode 100755 utils/keycloak/rhdh-client.json create mode 100755 utils/keycloak/users.json create mode 100644 utils/orchestrator/osl-di-rewrite.js delete mode 100644 utils/orchestrator/osl_smoke.py delete mode 100644 utils/orchestrator/test_osl_smoke.py create mode 100755 utils/orchestrator/verify-existing-rhdh.sh diff --git a/.env.example b/.env.example index 4ae081f..56183d4 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,9 @@ export KEYCLOAK_REALM="" export KEYCLOAK_LOGIN_REALM="" export KEYCLOAK_METADATA_URL="" export KEYCLOAK_BASE_URL="" + +# OSL operator overrides (set by prepare-osl-internal.sh -> .env.osl) +# export OSL_IIB_IMAGE="" +# export OSL_VERSION="" +# export OSL_LOGIC_CSV="" +# export OSL_CATALOG_SOURCE="" diff --git a/.gitignore b/.gitignore index 782bdca..9d11e3d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ .env +.env.osl +config/image-mirrors.conf install-rhdh-catalog-source.sh plugin-infra.sh .DS_Store -.claude/ \ No newline at end of file +.claude/ +.worktrees/ diff --git a/Makefile b/Makefile index a5c053b..996ee31 100644 --- a/Makefile +++ b/Makefile @@ -83,8 +83,8 @@ endif setup-orchestrator: ## Full RHDH + orchestrator setup (VERSION, ORCH_NAMESPACE, OSL_RELEASE) ./setup-orchestrator.sh $(VERSION) --namespace $(ORCH_NAMESPACE) $(if $(filter-out ,$(OSL_RELEASE)),--prepare-internal-osl $(OSL_RELEASE)) -cleanup: ## Clean RHDH/orchestrator/OSL resources from ORCH_NAMESPACE - ./cleanup.sh --namespace $(ORCH_NAMESPACE) +cleanup: ## Clean RHDH/orchestrator/OSL resources and operators from ORCH_NAMESPACE + ./cleanup.sh --namespace $(ORCH_NAMESPACE) --include-operators cleanup-full: ## Full cleanup: operators + related namespaces ./cleanup.sh --namespace $(ORCH_NAMESPACE) --include-operators --delete-namespace diff --git a/README.md b/README.md index 154ebfd..e677538 100644 --- a/README.md +++ b/README.md @@ -188,20 +188,21 @@ Pin an OSL pre-release against a chosen RHDH version, deploy, and run the defaul 3. `Rerun Failswitch from failure point` 4. `Execute token-propagation workflow via API` -Smoke always deploys greeting, failswitch, token-propagation, and `sample-server`, then runs token-propagation (JWT/OpenAPI into the workflow). A GraphQL probe hits the raw Data Index (`sonataflow-platform-data-index-service`) and fails if `ProcessDefinitions.serviceUrl` is relative unless you pass `--allow-relative-service-url` (needed on 1.39.CR1 until the Orchestrator plugin derives `serviceUrl` from `endpoint`; SRVLOGIC-1137). `--full-e2e` is the RHDH plugin suite (RBAC, entity, ui:props, Loki, all workflows), not the OSL CR default. +Smoke always deploys greeting, failswitch, token-propagation, and `sample-server`, then runs token-propagation (JWT/OpenAPI into the workflow). `--cleanup` (and the cleanup phase of `--all`) always removes OSL/Serverless operators, the custom catalog, and the mirror namespace as well as the RHDH namespace contents. + +Before Playwright, a GraphQL probe hits the **raw** Data Index (`sonataflow-platform-data-index-service`), not the `osl-di-rewrite` proxy. OSL 1.39.CR1 can return a relative `ProcessDefinitions.serviceUrl` (SRVLOGIC-1137). The Orchestrator plugin then cannot `POST` to execute/abort/retrigger. The probe exits 2 on that unless you pass `--allow-relative-service-url` or `ALLOW_RELATIVE_SERVICE_URL=1`, which prints a warning and continues so the four tests can still run behind the rewrite proxy. Drop that override after the plugin derives `serviceUrl` from `endpoint`. ```bash -# One-shot: cleanup (including operators) -> mirror OSL -> deploy -> smoke +# One-shot: full cleanup (including operators) -> mirror OSL -> deploy -> smoke make osl-regression VERSION=next OSL_RELEASE=1.39.0.CR1 ORCH_NAMESPACE=orchestrator -# 1.39.CR1 currently needs the DI contract override: +# 1.39.CR1 currently needs the relative-serviceUrl override: ALLOW_RELATIVE_SERVICE_URL=1 make osl-regression VERSION=next OSL_RELEASE=1.39.0.CR1 ORCH_NAMESPACE=orchestrator # Or call the driver directly ./run-osl-regression.sh --all --rhdh next --osl-release 1.39.0.CR1 --namespace orchestrator -./run-osl-regression.sh --cleanup --include-operators --namespace orchestrator -# 1.39.CR1 currently needs the DI contract override plus rewrite proxy: +./run-osl-regression.sh --cleanup --namespace orchestrator +./run-osl-regression.sh --cleanup --prepare-osl --deploy --rhdh next --osl-release 1.39.0.CR1 ALLOW_RELATIVE_SERVICE_URL=1 ./run-osl-regression.sh --test --namespace orchestrator -./run-osl-regression.sh --test --full-e2e --namespace orchestrator ./run-osl-regression.sh --test --overlays-dir ../rhdh-plugin-export-overlays ``` @@ -238,6 +239,7 @@ All make commands accept these variables: | `RUNNER_IMAGE` | `quay.io/rhdh-community/rhdh-e2e-runner:main` | Container image for `install-operator` | | `OSL_RELEASE` | _(empty)_ | OSL pre-release id for `prepare-osl` / `osl-regression` | | `ORCH_NAMESPACE` | `orchestrator` | Namespace used by orchestrator/OSL setup and cleanup | +| `ALLOW_RELATIVE_SERVICE_URL` | _(unset)_ | Set to `1` to continue smoke after a relative Data Index `serviceUrl` | > **Note:** `install-operator` requires you to be logged into the cluster via `oc login` on your host. > It automatically passes the session token to the e2e-runner container (needs Linux tools like `umoci`, `opm`, `skopeo`). diff --git a/cleanup.sh b/cleanup.sh new file mode 100755 index 0000000..9de97ae --- /dev/null +++ b/cleanup.sh @@ -0,0 +1,313 @@ +#!/bin/bash +# +# Thoroughly remove all RHDH, orchestrator, and OSL artifacts from the cluster +# so that a fresh deploy succeeds cleanly. +# +# Usage: +# ./cleanup.sh [--namespace ] [--include-operators] [--delete-namespace] +# +# Options: +# --namespace Target namespace (default: rhdh) +# --include-operators Also remove OSL/Serverless operators (cluster-scoped) +# --delete-namespace Delete the namespace itself at the end +# +# All commands are idempotent -- safe to run multiple times. + +set -euo pipefail + +namespace="rhdh" +include_operators=false +delete_namespace=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --namespace) + namespace="$2" + shift 2 + ;; + --include-operators) + include_operators=true + shift + ;; + --delete-namespace) + delete_namespace=true + shift + ;; + *) + echo "Error: Unknown option: $1" + echo "Usage: $0 [--namespace ] [--include-operators] [--delete-namespace]" + exit 1 + ;; + esac +done + +# Verify cluster connectivity +if ! oc whoami &>/dev/null; then + echo "Error: Cannot connect to OpenShift cluster. Is CRC running and are you logged in?" + echo " Try: crc start && oc login -u kubeadmin https://api.crc.testing:6443" + exit 1 +fi + +# Validate namespace +if [[ ! "$namespace" =~ ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ ]]; then + echo "Error: Invalid namespace name: '$namespace' (must be lowercase alphanumeric/hyphens, 1-63 chars)" + exit 1 +fi + +echo "===========================================" +echo " RHDH / Orchestrator Cleanup" +echo "===========================================" +echo "Namespace: $namespace" +echo "Include operators: $include_operators" +echo "Delete namespace: $delete_namespace" +echo "" + +# --------------------------------------------------------------------------- +# Helper: clean RHDH/orchestrator resources from a given namespace +# --------------------------------------------------------------------------- +clean_namespace() { + local ns="$1" + if ! oc get namespace "$ns" &>/dev/null; then + return 0 + fi + + echo "--- Cleaning namespace: $ns ---" + + # SonataFlow resources (must go before Helm uninstall to avoid operator reconciliation fights) + oc delete sonataflow --all -n "$ns" --ignore-not-found 2>/dev/null || true + oc delete sonataflowplatform --all -n "$ns" --ignore-not-found 2>/dev/null || true + + # Helm releases + helm uninstall redhat-developer-hub -n "$ns" 2>/dev/null || true + helm uninstall keycloak -n "$ns" 2>/dev/null || true + helm uninstall orchestrator-infra -n "$ns" 2>/dev/null || true + helm uninstall orch-infra -n "$ns" 2>/dev/null || true + + # Keycloak Route (created manually, not Helm-managed) + oc delete route keycloak -n "$ns" --ignore-not-found 2>/dev/null || true + + # Stale Jobs, ConfigMaps, Secrets + oc delete jobs --all -n "$ns" --ignore-not-found 2>/dev/null || true + oc delete configmap app-config-rhdh dynamic-plugins -n "$ns" --ignore-not-found 2>/dev/null || true + oc delete secret rhdh-secrets backstage-psql-secret -n "$ns" --ignore-not-found 2>/dev/null || true + oc delete service sample-server-service -n "$ns" --ignore-not-found 2>/dev/null || true + + # Workflow-related ConfigMaps + for cm in greeting-props greeting-managed-props 01-greeting-resources-schemas \ + failswitch-props failswitch-managed-props 01-failswitch-resources-schemas 02-failswitch-resources-specs \ + token-propagation-props token-propagation-managed-props \ + 01-token-propagation-resources-schemas 02-token-propagation-resources-specs; do + oc delete configmap "$cm" -n "$ns" --ignore-not-found 2>/dev/null || true + done + + # Remaining workloads: operator-created StatefulSets/Deployments survive Helm uninstall + oc delete statefulset --all -n "$ns" --ignore-not-found --wait=false 2>/dev/null || true + oc delete deployment --all -n "$ns" --ignore-not-found --wait=false 2>/dev/null || true + + # Force-delete all remaining pods (they block PVC deletion via pvc-protection finalizer) + oc delete pods --all -n "$ns" --force --grace-period=0 2>/dev/null || true + + # PVCs (contain stale DB migrations/data; --wait=false prevents hanging on finalizers) + oc delete pvc --all -n "$ns" --ignore-not-found --wait=false 2>/dev/null || true +} + +delete_knative_webhooks() { + echo "--- Removing stale Knative admission webhooks ---" + oc delete validatingwebhookconfiguration \ + config.webhook.eventing.knative.dev \ + config.webhook.serving.knative.dev \ + validation.inmemorychannel.eventing.knative.dev \ + validation.webhook.eventing.knative.dev \ + validation.webhook.serving.knative.dev \ + --ignore-not-found 2>/dev/null || true + oc delete mutatingwebhookconfiguration \ + inmemorychannel.eventing.knative.dev \ + sinkbindings.webhook.sources.knative.dev \ + webhook.eventing.knative.dev \ + webhook.serving.knative.dev \ + --ignore-not-found 2>/dev/null || true +} + +delete_olm_subscriptions() { + local ns="$1" + local sub + + for sub in $(oc get subscriptions.operators.coreos.com -n "$ns" -o name 2>/dev/null); do + echo " Deleting $sub in $ns" + oc delete "$sub" -n "$ns" --ignore-not-found 2>/dev/null || true + done +} + +force_finalize_namespace_if_stuck() { + local ns="$1" + if ! oc get namespace "$ns" &>/dev/null; then + return 0 + fi + + local phase + phase="$(oc get namespace "$ns" -o jsonpath='{.status.phase}' 2>/dev/null || true)" + if [[ "$phase" != "Terminating" ]]; then + return 0 + fi + + echo " Namespace ${ns} is still Terminating; forcing finalization..." + oc get namespace "$ns" -o json 2>/dev/null | \ + jq '.spec.finalizers=[]' | \ + oc replace --raw "/api/v1/namespaces/${ns}/finalize" -f - >/dev/null 2>&1 || true +} + +wait_for_namespace_gone() { + local ns="$1" + local timeout_secs="${2:-120}" + local start + start="$(date +%s)" + + while oc get namespace "$ns" &>/dev/null; do + local elapsed=$(( $(date +%s) - start )) + if [[ $elapsed -ge $timeout_secs ]]; then + force_finalize_namespace_if_stuck "$ns" + break + fi + sleep 3 + done +} + +post_cleanup_verify() { + local failures=0 + + echo "--- Post-clean verification ---" + local target_ns='rhdh|orchestrator|rhdh-keycloak|knative-serving|knative-eventing|knative-serving-ingress|openshift-serverless|openshift-serverless-logic|orchestrator-infra|osl-mirror' + local remaining_ns + remaining_ns="$(oc get ns -o name 2>/dev/null | awk "tolower(\$0) ~ /${target_ns}/" || true)" + if [[ -n "$remaining_ns" ]]; then + echo " Remaining namespaces:" + echo "$remaining_ns" | sed 's/^/ /' + failures=1 + fi + + local remaining_subs + remaining_subs="$(oc get subscriptions.operators.coreos.com -A -o name 2>/dev/null | awk 'tolower($0) ~ /logic-operator|serverless-operator/' || true)" + if [[ -n "$remaining_subs" ]]; then + echo " Remaining subscriptions:" + echo "$remaining_subs" | sed 's/^/ /' + failures=1 + fi + + local remaining_csvs + remaining_csvs="$(oc get csv -A -o name 2>/dev/null | awk 'tolower($0) ~ /logic-operator|serverless-operator/' || true)" + if [[ -n "$remaining_csvs" ]]; then + echo " Remaining CSVs:" + echo "$remaining_csvs" | sed 's/^/ /' + failures=1 + fi + + if oc get catalogsource osl-custom-catalog -n openshift-marketplace &>/dev/null; then + echo " Remaining catalogsource: openshift-marketplace/osl-custom-catalog" + failures=1 + fi + if oc get imagedigestmirrorset osl-bundle-mirror &>/dev/null; then + echo " Remaining IDMS: osl-bundle-mirror" + failures=1 + fi + + local knative_webhooks + knative_webhooks="$(oc get validatingwebhookconfigurations,mutatingwebhookconfigurations -o name 2>/dev/null | awk 'tolower($0) ~ /knative/' || true)" + if [[ -n "$knative_webhooks" ]]; then + echo " Remaining Knative webhooks:" + echo "$knative_webhooks" | sed 's/^/ /' + failures=1 + fi + + if [[ $failures -ne 0 ]]; then + echo "" + echo "Cleanup finished with residual resources. Re-run cleanup or inspect items above." + exit 1 + fi + + echo " Verification passed: no known orchestrator/serverless leftovers found." +} + +# --------------------------------------------------------------------------- +# 1. Clean the target namespace +# --------------------------------------------------------------------------- +clean_namespace "$namespace" + +# Also try orchestrator-infra in its own namespace +helm uninstall orchestrator-infra -n orchestrator-infra 2>/dev/null || true +helm uninstall orch-infra -n orchestrator-infra 2>/dev/null || true + +# --------------------------------------------------------------------------- +# 2. Clean namespaces created by orchestrator e2e tests +# (rhdh-plugin-export-overlays/workspaces/orchestrator/e2e-tests) +# Tests deploy into "orchestrator" or "orchestrator-e2e" ns and Keycloak into +# "rhdh-keycloak" ns. +# --------------------------------------------------------------------------- +if [[ "$namespace" != "orchestrator" ]]; then + clean_namespace "orchestrator" +fi +if [[ "$namespace" != "orchestrator-e2e" ]]; then + clean_namespace "orchestrator-e2e" +fi +if [[ "$namespace" != "rhdh-keycloak" ]]; then + clean_namespace "rhdh-keycloak" +fi + +# --------------------------------------------------------------------------- +# 3. Cluster-scoped: operators and related resources +# --------------------------------------------------------------------------- +if [[ "$include_operators" == "true" ]]; then + echo "--- Removing cluster-scoped operator resources ---" + + # Custom CatalogSource + oc delete catalogsource osl-custom-catalog -n openshift-marketplace --ignore-not-found 2>/dev/null || true + + # Subscriptions + for ns in openshift-serverless-logic openshift-serverless openshift-operators; do + delete_olm_subscriptions "$ns" + done + + # CSVs + for ns in openshift-serverless-logic openshift-serverless openshift-operators; do + for csv in $(oc get csv -n "$ns" -o name 2>/dev/null); do + echo " Deleting $csv in $ns" + oc delete "$csv" -n "$ns" --ignore-not-found 2>/dev/null || true + done + done + + # ImageDigestMirrorSet + oc delete imagedigestmirrorset osl-bundle-mirror --ignore-not-found 2>/dev/null || true + + # HelmChartRepository created for CI chart fallback builds + oc delete helmchartrepository rhdh-next-ci-repo --ignore-not-found 2>/dev/null || true + + # Knative instances (must be deleted before their namespaces, or finalizers hang) + echo "--- Removing Knative instances ---" + oc delete knativeserving knative-serving -n knative-serving --ignore-not-found --timeout=60s 2>/dev/null || true + oc delete knativeeventing knative-eventing -n knative-eventing --ignore-not-found --timeout=60s 2>/dev/null || true + delete_knative_webhooks + + # All related namespaces (operator-created + alternative deployment patterns) + echo "--- Removing operator and related namespaces ---" + for ns in knative-serving knative-eventing knative-serving-ingress \ + openshift-serverless openshift-serverless-logic orchestrator-infra \ + orchestrator orchestrator-e2e rhdh-keycloak osl-mirror; do + oc delete project "$ns" --ignore-not-found --timeout=60s 2>/dev/null || true + wait_for_namespace_gone "$ns" 120 + done +fi + +# --------------------------------------------------------------------------- +# 4. Optionally delete the target namespace +# --------------------------------------------------------------------------- +if [[ "$delete_namespace" == "true" ]]; then + echo "--- Deleting namespace $namespace ---" + oc delete project "$namespace" --ignore-not-found 2>/dev/null || true + wait_for_namespace_gone "$namespace" 120 +fi + +post_cleanup_verify + +echo "" +echo "===========================================" +echo " Cleanup complete" +echo "===========================================" diff --git a/config/app-config-oidc.yaml b/config/app-config-oidc.yaml new file mode 100644 index 0000000..fd87a47 --- /dev/null +++ b/config/app-config-oidc.yaml @@ -0,0 +1,20 @@ +auth: + environment: production + providers: + oidc: + production: + metadataUrl: '${KEYCLOAK_METADATA_URL}' + clientId: '${KEYCLOAK_CLIENT_ID}' + clientSecret: '${KEYCLOAK_CLIENT_SECRET}' + prompt: auto + callbackUrl: '${RHDH_BASE_URL}/api/auth/oidc/handler/frame' + signIn: + resolvers: + - resolver: preferredUsernameMatchingUserEntityName + dangerouslyAllowSignInWithoutUserInCatalog: true + guest: + dangerouslyAllowOutsideDevelopment: false +signInPage: oidc +orchestrator: + dataIndexService: + url: '${SONATAFLOW_DATA_INDEX_URL}' diff --git a/config/orchestrator-dynamic-plugins-next.yaml b/config/orchestrator-dynamic-plugins-next.yaml new file mode 100644 index 0000000..8e88ccd --- /dev/null +++ b/config/orchestrator-dynamic-plugins-next.yaml @@ -0,0 +1,29 @@ +plugins: + - package: 'oci://quay.io/rhdh/red-hat-developer-hub-backstage-plugin-orchestrator:{{inherit}}' + disabled: false + pluginConfig: + dynamicPlugins: + frontend: + red-hat-developer-hub.backstage-plugin-orchestrator: + pluginModule: Alpha + appIcons: + - name: orchestratorIcon + importName: OrchestratorIcon + dynamicRoutes: + - path: /orchestrator + importName: OrchestratorPage + menuItem: + icon: orchestratorIcon + text: Orchestrator + textKey: menuItem.orchestrator + menuItems: + orchestrator: + icon: orchestratorIcon + - package: 'oci://quay.io/rhdh/red-hat-developer-hub-backstage-plugin-orchestrator-backend:{{inherit}}' + disabled: false + dependencies: + - ref: sonataflow + - package: 'oci://quay.io/rhdh/red-hat-developer-hub-backstage-plugin-scaffolder-backend-module-orchestrator:{{inherit}}' + disabled: false + - package: 'oci://quay.io/rhdh/red-hat-developer-hub-backstage-plugin-orchestrator-form-widgets:{{inherit}}' + disabled: false diff --git a/config/osl-releases/1.39.0.CR1.json b/config/osl-releases/1.39.0.CR1.json new file mode 100644 index 0000000..42de232 --- /dev/null +++ b/config/osl-releases/1.39.0.CR1.json @@ -0,0 +1,64 @@ +{ + "version": "1.39.0.CR1", + "logic_csv": "logic-operator.v1.39.0", + "iib": { + "4.13": "registry-proxy.engineering.redhat.com/rh-osbs/iib:1196310", + "4.14": "registry-proxy.engineering.redhat.com/rh-osbs/iib:1196311", + "4.15": "registry-proxy.engineering.redhat.com/rh-osbs/iib:1196312", + "4.16": "registry-proxy.engineering.redhat.com/rh-osbs/iib:1196313", + "4.17": "registry-proxy.engineering.redhat.com/rh-osbs/iib:1196315", + "4.18": "registry-proxy.engineering.redhat.com/rh-osbs/iib:1196314", + "4.19": "registry-proxy.engineering.redhat.com/rh-osbs/iib:1196319", + "4.20": "registry-proxy.engineering.redhat.com/rh-osbs/iib:1196321", + "4.21": "registry-proxy.engineering.redhat.com/rh-osbs/iib:1196320", + "4.22": "registry-proxy.engineering.redhat.com/rh-osbs/iib:1196317", + "4.23": "registry-proxy.engineering.redhat.com/rh-osbs/iib:1196316", + "5.0": "registry-proxy.engineering.redhat.com/rh-osbs/iib:1196318" + }, + "images": [ + { + "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-rhel9-operator@sha256:9c74bdc7b62309e781790af0041566d606325fb8903a6ab578a23dbcfcc1f26b", + "name": "logic-rhel9-operator" + }, + { + "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-operator-bundle@sha256:43154da5e7fd40d339f41e329475ff54350f082d6307d94e9da49c84917f1a4b", + "name": "logic-operator-bundle" + }, + { + "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-data-index-ephemeral-rhel9@sha256:6b6f43a4df8ebde1f0bbbd164075a585f6d43ea3da23ca5928530b3973e5a53b", + "name": "logic-data-index-ephemeral-rhel9" + }, + { + "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-data-index-postgresql-rhel9@sha256:b3d4e22ddce6acd4c88cbed634b3f6dfeb564f8748d887ea9170e26eb4d99a77", + "name": "logic-data-index-postgresql-rhel9" + }, + { + "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-jobs-service-ephemeral-rhel9@sha256:0eaf021f4af2b9c12f550201344ee68afac50a60c173d84b155eb2adab808c4f", + "name": "logic-jobs-service-ephemeral-rhel9" + }, + { + "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-jobs-service-postgresql-rhel9@sha256:2b11e2aa32f298693e7ce064e79b21becfd9ee52a89865f2fba2f8f0c871e46f", + "name": "logic-jobs-service-postgresql-rhel9" + }, + { + "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-swf-builder-rhel9@sha256:6f9a032f51de85568114797d98b9270cd48de4437365c05473605acc4e74160e", + "name": "logic-swf-builder-rhel9" + }, + { + "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-swf-devmode-rhel9@sha256:158469391dec4e3473391a1ad62c489d92fbaa48ee0830caec6eabbbcca28243", + "name": "logic-swf-devmode-rhel9" + }, + { + "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-management-console-rhel9@sha256:2a3849e7030e97629d23bdbb5105a7ad206fc768103ddb9dfc9d4b37c6e1c73a", + "name": "logic-management-console-rhel9" + }, + { + "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-db-migrator-tool-rhel9@sha256:7c013f0ad1c5d441c771d4cacdf34be5a23c0743203daf8303f40a29d45d8c6d", + "name": "logic-db-migrator-tool-rhel9" + }, + { + "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-kn-workflow-cli-artifacts-rhel9@sha256:ca7752e07bd37d8e58953f904d637dc036e9880e1ae3ff82c3cc45c8478d0754", + "name": "logic-kn-workflow-cli-artifacts-rhel9" + } + ] +} diff --git a/config/osl-releases/README.md b/config/osl-releases/README.md new file mode 100755 index 0000000..07830ed --- /dev/null +++ b/config/osl-releases/README.md @@ -0,0 +1,50 @@ +# OSL Release Manifests + +Each OSL pre-release should have one local manifest JSON file: + +- Path: `config/osl-releases/.json` +- Template: `config/osl-releases/example.json` + +Recommended flow: + +```bash +cp config/osl-releases/example.json config/osl-releases/1.39.0.CR1.json +# Edit with values from the pre-release email +``` + +`prepare-osl-internal.sh` reads this file to: + +- select IIB by OCP minor version +- mirror required source images (amd64 by default) +- build a rewritten internal logic-only catalog image +- create `CatalogSource/osl-custom-catalog` and wait for it to be READY +- write `.env.osl` with `OSL_*` exports + +## Schema + +```json +{ + "version": "1.39.0.CR1", + "iib": { + "4.17": "registry-proxy.engineering.redhat.com/rh-osbs/iib:123456", + "4.18": "registry-proxy.engineering.redhat.com/rh-osbs/iib:123457" + }, + "images": [ + { + "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-rhel9-operator@sha256:", + "name": "logic-rhel9-operator" + } + ] +} +``` + +Notes: + +- `version` is the full release version string (e.g. `1.39.0.CR1`). The short + major.minor (e.g. `1.39`) is derived automatically for `OSL_LOGIC_CSV`. +- `iib` must include the current cluster's `major.minor` version. +- `images[].source` should be a full digest reference from the release email. +- `iib[*]` should also be digest-pinned where possible (`...@sha256:...`). +- `images[].name` is a short identifier used as the internal registry repo name. +- Set `ENFORCE_DIGEST_PINNING=1` to fail fast when non-digest references are present. +- Manifest files are ignored by git by default (`config/osl-releases/*.json`), except `example.json`. diff --git a/config/osl-releases/example.json b/config/osl-releases/example.json new file mode 100755 index 0000000..1498f58 --- /dev/null +++ b/config/osl-releases/example.json @@ -0,0 +1,56 @@ +{ + "_comment": "OSL pre-release manifest. Copy this file to .json and fill in values from the release email.", + "version": "1.39.0.CR1", + "iib": { + "_comment": "Index Image Bundles keyed by OCP minor version. Use the IIB tag from the release email.", + "4.17": "registry-proxy.engineering.redhat.com/rh-osbs/iib:123456", + "4.18": "registry-proxy.engineering.redhat.com/rh-osbs/iib:123457" + }, + "images": [ + { + "_comment": "Each entry is a container image referenced by the operator bundle. source is the digest ref from the release email, name is a short identifier used as the internal registry repo name.", + "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-rhel9-operator@sha256:abcdef...", + "name": "logic-rhel9-operator" + }, + { + "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-data-index-ephemeral-rhel9@sha256:abcdef...", + "name": "logic-data-index-ephemeral-rhel9" + }, + { + "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-data-index-postgresql-rhel9@sha256:abcdef...", + "name": "logic-data-index-postgresql-rhel9" + }, + { + "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-jobs-service-ephemeral-rhel9@sha256:abcdef...", + "name": "logic-jobs-service-ephemeral-rhel9" + }, + { + "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-jobs-service-postgresql-rhel9@sha256:abcdef...", + "name": "logic-jobs-service-postgresql-rhel9" + }, + { + "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-swf-builder-rhel9@sha256:abcdef...", + "name": "logic-swf-builder-rhel9" + }, + { + "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-swf-devmode-rhel9@sha256:abcdef...", + "name": "logic-swf-devmode-rhel9" + }, + { + "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-management-console-rhel9@sha256:abcdef...", + "name": "logic-management-console-rhel9" + }, + { + "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-db-migrator-tool-rhel9@sha256:abcdef...", + "name": "logic-db-migrator-tool-rhel9" + }, + { + "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-operator-bundle@sha256:abcdef...", + "name": "logic-operator-bundle" + }, + { + "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-kn-workflow-cli-artifacts-rhel9@sha256:abcdef...", + "name": "logic-kn-workflow-cli-artifacts-rhel9" + } + ] +} diff --git a/config/rbac-policies.yaml b/config/rbac-policies.yaml index 6c7ee66..fede76b 100644 --- a/config/rbac-policies.yaml +++ b/config/rbac-policies.yaml @@ -10,6 +10,8 @@ data: p, role:default/admin, catalog.entity.create, create, allow g, user:default/guest, role:default/admin + g, user:default/test1, role:default/admin + g, user:default/test2, role:default/admin p, role:default/admin, catalog-entity, read, allow p, role:default/admin, catalog.entity.create, create, allow diff --git a/deploy.sh b/deploy.sh index ae93be3..16f2dab 100755 --- a/deploy.sh +++ b/deploy.sh @@ -64,7 +64,7 @@ if [[ "$installation_method" != "helm" && "$installation_method" != "operator" ] exit 1 fi -[[ "${OPENSHIFT_CI}" != "true" ]] && source .env +[[ "${OPENSHIFT_CI}" != "true" && "${SKIP_ENV_SOURCE:-}" != "1" ]] && source .env # source utils/utils.sh # Create or switch to the specified namespace @@ -121,7 +121,7 @@ else fi # Wait for the deployment to be ready -oc rollout status deployment -l 'app.kubernetes.io/instance in (redhat-developer-hub,developer-hub)' -n "$namespace" --timeout=500s || { echo "Error: Timed out waiting for deployment to be ready."; exit 1; } +oc rollout status deployment -l 'app.kubernetes.io/instance in (redhat-developer-hub,developer-hub)' -n "$namespace" --timeout=900s || { echo "Error: Timed out waiting for deployment to be ready."; exit 1; } echo " RHDH_BASE_URL : diff --git a/docs/superpowers/plans/2026-08-20-orchestrator-serviceurl-from-endpoint.md b/docs/superpowers/plans/2026-08-20-orchestrator-serviceurl-from-endpoint.md new file mode 100644 index 0000000..598bca1 --- /dev/null +++ b/docs/superpowers/plans/2026-08-20-orchestrator-serviceurl-from-endpoint.md @@ -0,0 +1,299 @@ +# Orchestrator serviceUrl-from-endpoint Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make the Orchestrator backend treat OSL 1.39 relative Data Index `serviceUrl` as the origin of `endpoint`, so execute/abort/retrigger work without `osl-di-rewrite`. + +**Architecture:** Add a pure helper `resolveWorkflowServiceUrl`, unit-test it, then apply it to every `ProcessDefinitions` mapping in `DataIndexService`. Do not change the execute URL shape (`${origin}/${id}`). + +**Tech Stack:** TypeScript, Jest via `yarn test` in `rhdh-plugins/workspaces/orchestrator`. + +**Spec:** `docs/superpowers/specs/2026-08-20-orchestrator-serviceurl-from-endpoint.md` (this worktree copy). Implement in **`rhdh-plugins`**, not in `rhdh-test-instance`. + +## Global Constraints + +- Implementation repo: `rhdh-plugins`, workspace `workspaces/orchestrator`. Create a new branch from that repo’s default (do not commit plugin code into `rhdh-test-instance`). +- Do not mix this PR with the OSL smoke-driver PR. +- Keep absolute `http://` / `https://` `serviceUrl` values unchanged (OSL ≤ 1.38). +- Do not POST to the full `endpoint` path; only copy `.origin`. +- `fetchWorkflowServiceUrls` currently queries `{ id, serviceUrl }` only — it **must** also fetch `endpoint`. + +--- + +## File map + +| File | Responsibility | +|---|---| +| `plugins/orchestrator-backend/src/service/workflowServiceUrl.ts` | `isAbsoluteHttpUrl`, `resolveWorkflowServiceUrl` | +| `plugins/orchestrator-backend/src/service/workflowServiceUrl.test.ts` | Jest cases for relative / absolute / bad endpoint | +| `plugins/orchestrator-backend/src/service/DataIndexService.ts` | Apply helper on definition reads; add `endpoint` to `fetchWorkflowServiceUrls` query | +| `plugins/orchestrator-backend/src/service/DataIndexService.test.ts` | Assert mapping when GraphQL returns relative `serviceUrl` | + +Paths are relative to `/home/rlan/redhat/rhdh-plugins/workspaces/orchestrator`. + +--- + +### Task 1: Helper + unit tests + +**Files:** +- Create: `plugins/orchestrator-backend/src/service/workflowServiceUrl.ts` +- Create: `plugins/orchestrator-backend/src/service/workflowServiceUrl.test.ts` + +**Interfaces:** +- Consumes: none +- Produces: + - `isAbsoluteHttpUrl(value?: string): boolean` + - `resolveWorkflowServiceUrl(info: { serviceUrl?: string; endpoint?: string }): string | undefined` + +- [ ] **Step 1: Confirm branch in rhdh-plugins** + +```bash +git -C /home/rlan/redhat/rhdh-plugins branch --show-current +git -C /home/rlan/redhat/rhdh-plugins status -sb +``` + +If the tree is dirty or the branch is not a new feature branch, create one: + +```bash +git -C /home/rlan/redhat/rhdh-plugins fetch origin +git -C /home/rlan/redhat/rhdh-plugins switch -c fix/orchestrator-serviceurl-from-endpoint origin/main +``` + +(Use the actual default remote/branch if it is not `origin/main`.) + +- [ ] **Step 2: Write the failing test** + +Create `plugins/orchestrator-backend/src/service/workflowServiceUrl.test.ts`: + +```typescript +import { + isAbsoluteHttpUrl, + resolveWorkflowServiceUrl, +} from './workflowServiceUrl'; + +describe('isAbsoluteHttpUrl', () => { + it('accepts http and https', () => { + expect(isAbsoluteHttpUrl('http://greeting.ns.svc')).toBe(true); + expect(isAbsoluteHttpUrl('https://greeting.example')).toBe(true); + }); + + it('rejects relative, empty, and non-http', () => { + expect(isAbsoluteHttpUrl('/greeting')).toBe(false); + expect(isAbsoluteHttpUrl('greeting.ns.svc')).toBe(false); + expect(isAbsoluteHttpUrl('')).toBe(false); + expect(isAbsoluteHttpUrl(undefined)).toBe(false); + }); +}); + +describe('resolveWorkflowServiceUrl', () => { + it('keeps an already-absolute serviceUrl', () => { + expect( + resolveWorkflowServiceUrl({ + serviceUrl: 'http://greeting.orchestrator.svc', + endpoint: 'http://other.svc/greeting/1.0.0', + }), + ).toBe('http://greeting.orchestrator.svc'); + }); + + it('uses endpoint origin when serviceUrl is relative (SRVLOGIC-1137)', () => { + expect( + resolveWorkflowServiceUrl({ + serviceUrl: '/greeting', + endpoint: 'http://greeting.orchestrator.svc.cluster.local/greeting/1.0.0', + }), + ).toBe('http://greeting.orchestrator.svc.cluster.local'); + }); + + it('uses endpoint origin when serviceUrl is missing', () => { + expect( + resolveWorkflowServiceUrl({ + endpoint: 'http://failswitch.orchestrator.svc/failswitch', + }), + ).toBe('http://failswitch.orchestrator.svc'); + }); + + it('returns undefined when neither field is a usable URL', () => { + expect(resolveWorkflowServiceUrl({ serviceUrl: '/greeting' })).toBeUndefined(); + expect(resolveWorkflowServiceUrl({})).toBeUndefined(); + }); +}); +``` + +- [ ] **Step 3: Run test to verify it fails** + +```bash +cd /home/rlan/redhat/rhdh-plugins/workspaces/orchestrator +yarn test plugins/orchestrator-backend --testPathPattern=workflowServiceUrl.test --coverage=false +``` + +Expected: FAIL (cannot resolve `./workflowServiceUrl`). + +- [ ] **Step 4: Write the helper** + +Create `plugins/orchestrator-backend/src/service/workflowServiceUrl.ts`: + +```typescript +export function isAbsoluteHttpUrl(value?: string): boolean { + if (!value) { + return false; + } + return value.startsWith('http://') || value.startsWith('https://'); +} + +export function resolveWorkflowServiceUrl(info: { + serviceUrl?: string; + endpoint?: string; +}): string | undefined { + if (isAbsoluteHttpUrl(info.serviceUrl)) { + return info.serviceUrl; + } + if (!info.endpoint) { + return undefined; + } + try { + return new URL(info.endpoint).origin; + } catch { + return undefined; + } +} +``` + +- [ ] **Step 5: Run tests to verify they pass** + +```bash +cd /home/rlan/redhat/rhdh-plugins/workspaces/orchestrator +yarn test plugins/orchestrator-backend --testPathPattern=workflowServiceUrl.test --coverage=false +``` + +Expected: PASS. + +- [ ] **Step 6: Commit in rhdh-plugins** + +```bash +cd /home/rlan/redhat/rhdh-plugins +git add workspaces/orchestrator/plugins/orchestrator-backend/src/service/workflowServiceUrl.ts \ + workspaces/orchestrator/plugins/orchestrator-backend/src/service/workflowServiceUrl.test.ts +git commit -m "$(cat <<'EOF' +feat: add workflow serviceUrl origin helper for OSL 1.39 Data Index + +EOF +)" +``` + +--- + +### Task 2: Apply the helper in DataIndexService + +**Files:** +- Modify: `plugins/orchestrator-backend/src/service/DataIndexService.ts` +- Modify: `plugins/orchestrator-backend/src/service/DataIndexService.test.ts` + +**Interfaces:** +- Consumes: `resolveWorkflowServiceUrl` from Task 1 +- Produces: every returned `WorkflowInfo` (and `fetchWorkflowServiceUrls` map values) has an absolute `serviceUrl` when `endpoint` is absolute + +- [ ] **Step 1: Write a failing DataIndexService test** + +In `DataIndexService.test.ts`, add a `describe('relative serviceUrl from OSL 1.39')` that mocks `client.query` for `fetchWorkflowInfos` (no definitionIds/filter) returning: + +```javascript +{ + data: { + ProcessDefinitions: [ + { + id: 'greeting', + name: 'Greeting', + serviceUrl: '/greeting', + endpoint: 'http://greeting.orchestrator.svc/greeting', + metadata: {}, + }, + ], + }, + error: undefined, +} +``` + +Assert `infos[0].serviceUrl === 'http://greeting.orchestrator.svc'`. + +Follow the existing `fetchWorkflowInfos` mock style in that file (`mockClient.query`, `Client` mock, `loggerMock`). Keep `filterDeletedWorkflows` behavior: `metadata.status === 'unavailable'` still dropped. + +Add a second test for `fetchWorkflowServiceUrls`: mock GraphQL data with relative `serviceUrl` + absolute `endpoint`; expect the returned map `{ greeting: 'http://greeting.orchestrator.svc' }`. This test **must fail** until the query string includes `endpoint`. + +- [ ] **Step 2: Run the new tests to verify fail** + +```bash +cd /home/rlan/redhat/rhdh-plugins/workspaces/orchestrator +yarn test plugins/orchestrator-backend --testPathPattern=DataIndexService.test --coverage=false +``` + +Expected: FAIL — `serviceUrl` still `'/greeting'`. + +- [ ] **Step 3: Implement mapping** + +At top of `DataIndexService.ts`: + +```typescript +import { resolveWorkflowServiceUrl } from './workflowServiceUrl'; +``` + +Add a private method: + +```typescript +private withResolvedServiceUrl(info: WorkflowInfo): WorkflowInfo { + return { + ...info, + serviceUrl: resolveWorkflowServiceUrl(info), + }; +} +``` + +Apply it: + +- `fetchWorkflowInfo`: `return this.withResolvedServiceUrl(processDefinitions[0]);` +- `fetchWorkflowInfos`: `return this.filterDeletedWorkflows(...).map(w => this.withResolvedServiceUrl(w));` +- `fetchWorkflowServiceUrls`: change query to `{ ProcessDefinitions { id, serviceUrl, endpoint } }`, then: + +```typescript +return processDefinitions + .map(definition => this.withResolvedServiceUrl(definition)) + .filter(definition => definition.serviceUrl) + .map(definition => ({ [definition.id]: definition.serviceUrl! })) + .reduce((acc, curr) => ({ ...acc, ...curr }), {}); +``` + +Do not change execute/abort URL builders in `SonataFlowService.ts`; they already use the resolved `serviceUrl`. + +- [ ] **Step 4: Run DataIndexService + helper tests** + +```bash +cd /home/rlan/redhat/rhdh-plugins/workspaces/orchestrator +yarn test plugins/orchestrator-backend --testPathPattern='workflowServiceUrl.test|DataIndexService.test' --coverage=false +``` + +Expected: PASS. + +- [ ] **Step 5: Commit in rhdh-plugins** + +```bash +git add workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts \ + workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.test.ts +git commit -m "$(cat <<'EOF' +fix: derive Data Index serviceUrl origin from endpoint + +EOF +)" +``` + +--- + +### Task 3: Do not remove the test-instance rewrite in this PR + +No code. After the plugin is in the RHDH `next` catalog image the smoke uses, a **later** `rhdh-test-instance` change can skip `ensure_dataindex_rewrite` and drop `--allow-relative-service-url`. Mixing that into this plugin PR will break smoke until the image exists. + +--- + +## Self-review + +1. **Spec coverage:** helper + mapping + `fetchWorkflowServiceUrls` query includes `endpoint` → Tasks 1–2. Rewrite removal → Task 3 (explicitly deferred). Execute still uses origin, not versioned endpoint → Task 2 note. +2. **Placeholders:** none. +3. **Names:** `resolveWorkflowServiceUrl` / `withResolvedServiceUrl` used consistently. diff --git a/docs/superpowers/plans/2026-08-20-osl-rc-smoke-subset.md b/docs/superpowers/plans/2026-08-20-osl-rc-smoke-subset.md index 5ab0dcf..2275dbe 100644 --- a/docs/superpowers/plans/2026-08-20-osl-rc-smoke-subset.md +++ b/docs/superpowers/plans/2026-08-20-osl-rc-smoke-subset.md @@ -1,12 +1,14 @@ # OSL RC smoke subset Implementation Plan +> **Implementation note (2026-08-20):** Do not add Python helpers. The approved architecture is the earlier **Lean OSL smoke bash** plan: `run-osl-regression.sh` only, GraphQL classification with `jq`, Playwright `--grep` as a bash constant. The task bodies below that mention `osl_smoke.py` / `test_osl_smoke.py` are obsolete. + > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Default OSL RC `--test` probes raw Data Index GraphQL, then runs four Playwright tests (Greeting + Failswitch statuses + retrigger + token-propagation). -**Architecture:** Extract classification and Playwright grep into a stdlib Python module with unittest. The existing bash driver always deploys greeting, failswitch, and token-propagation, calls the probe, then Playwright `-g` for the four titles. Do not change overlays git files; keep copying `playwright/osl-regression-smoke.spec.ts` at runtime. +**Architecture:** Keep the existing bash driver. It always deploys greeting, failswitch, and token-propagation, probes raw Data Index with `oc exec` + `jq`, then Playwright `-g` for the four titles. Do not change overlays git files; keep copying `playwright/osl-regression-smoke.spec.ts` at runtime. -**Tech Stack:** bash, Python 3 stdlib unittest, oc, Playwright (overlays e2e-tests), existing smoke wrapper. +**Tech Stack:** bash, jq, oc, Playwright (overlays e2e-tests), existing smoke wrapper. **Spec:** `docs/superpowers/specs/2026-08-20-osl-rc-smoke-subset.md` @@ -15,10 +17,10 @@ - Repo: `rhdh-test-instance` only, branch `feat/rhidp-13375-osl-smoke`, worktree `/home/rlan/redhat/rhdh-test-instance/.worktrees/rhidp-13375-osl-smoke`. - Do not edit `rhdh-plugin-export-overlays`, `rhdh-plugins`, or `rhdh-e2e-test-utils`. - Do not commit `.env`, `.env.osl`, or cluster credentials. -- Python helpers: stdlib only. No new pip packages. +- Driver is bash. Classify GraphQL with `jq`. Do not add Python helper modules. - Default Playwright titles (exact): `Run Greeting workflow and verify Workflows tab`, `Run Failswitch workflow and verify statuses`, `Rerun Failswitch from failure point`, `Execute token-propagation workflow via API`. - Probe the raw Data Index service, never `osl-di-rewrite`. -- `--full-e2e` must keep running the full overlays orchestrator project with no title grep. +- Driver `--cleanup` always includes operators/catalog/mirror. No `--full-e2e` flag. - Commit messages: conventional commits, include `#13375`. - `export PATH="/home/rlan/bin:$HOME/.local/bin:$PATH"` before any `oc` command. @@ -28,11 +30,9 @@ | File | Responsibility | |---|---| -| `utils/orchestrator/osl_smoke.py` | Smoke titles, Playwright `-g` regex, GraphQL JSON classification, `probe` CLI | -| `utils/orchestrator/test_osl_smoke.py` | unittest for titles, grep, URL classification, probe JSON | -| `run-osl-regression.sh` | `--allow-relative-service-url`, always deploy token-propagation on smoke, call probe, pass `-g` | +| `run-osl-regression.sh` | `--allow-relative-service-url`, always deploy token-propagation on smoke, probe raw Data Index with `jq`, pass `--grep` | | `playwright/osl-regression-smoke.spec.ts` | Always register token-propagation tests | -| `README.md` | Default 4-test smoke, probe, `--full-e2e` = plugin gate | +| `README.md` | Default 4-test smoke, probe, `--allow-relative-service-url` | | `Makefile` | Pass-through `ALLOW_RELATIVE_SERVICE_URL=1` | Do **not** implement Orchestrator plugin `serviceUrl` derivation here. That is `docs/superpowers/plans/2026-08-20-orchestrator-serviceurl-from-endpoint.md`. @@ -714,8 +714,7 @@ On a logged-in cluster with RHDH already up: ```bash export PATH="/home/rlan/bin:$HOME/.local/bin:$PATH" cd /home/rlan/redhat/rhdh-test-instance/.worktrees/rhidp-13375-osl-smoke -python3 utils/orchestrator/osl_smoke.py probe --namespace orchestrator; echo exit:$? -# 1.39.CR1 expected: exit 2, problems reason relative-or-missing-serviceUrl +# 1.39.CR1 expected: probe exit 2 unless ALLOW_RELATIVE_SERVICE_URL=1 ALLOW_RELATIVE_SERVICE_URL=1 ./run-osl-regression.sh --test --namespace orchestrator ``` @@ -729,4 +728,4 @@ Do not treat this cluster run as part of the git tasks; it is the human/agent ga 1. **Spec coverage:** 4-test default including token-propagation → Tasks 1, 3, 4. GraphQL probe → Task 2. `--full-e2e` as plugin suite → Task 3 README. Makefile → Task 5. Plugin `serviceUrl` productization → sibling plan, not this file. 2. **Placeholders:** none. -3. **Types:** `playwright_grep() -> str` (four titles, no `include_token` argument), `classify_definitions(...) -> dict` with `ok`/`problems`, probe exit 0/1/2, `--allow-relative` matches `ALLOW_RELATIVE_SERVICE_URL=1`. No `--include-token-propagation`. +3. **Types:** bash `SMOKE_GREP` (four titles), probe exit 0/1/2, `--allow-relative-service-url` matches `ALLOW_RELATIVE_SERVICE_URL=1`. No `--include-token-propagation`. No Python helper modules. diff --git a/docs/superpowers/specs/2026-08-20-orchestrator-serviceurl-from-endpoint.md b/docs/superpowers/specs/2026-08-20-orchestrator-serviceurl-from-endpoint.md new file mode 100644 index 0000000..7085874 --- /dev/null +++ b/docs/superpowers/specs/2026-08-20-orchestrator-serviceurl-from-endpoint.md @@ -0,0 +1,37 @@ +# Spec: Derive Orchestrator `serviceUrl` from Data Index `endpoint` + +## Problem + +OSL 1.39 Data Index `ProcessDefinitions.serviceUrl` is a relative path ([SRVLOGIC-1137](https://redhat.atlassian.net/browse/SRVLOGIC-1137)). The RHDH Orchestrator backend concatenates that value: + +- execute: `POST ${serviceUrl}/${definitionId}` +- ping/schema: `GET ${serviceUrl}/management/processes/${definitionId}` +- abort/retrigger: `${serviceUrl}/management/processes/${definitionId}/instances/...` + +A relative `serviceUrl` such as `/greeting` becomes a failed fetch on the RHDH pod. Ricardo Zanini (SRVLOGIC-1137): `endpoint` is correct; consumers should take the server origin from `endpoint`. + +`rhdh-test-instance` currently hides this with `osl-di-rewrite`. That workaround must not stay as the product fix. + +## Goal + +In `@red-hat-developer-hub/backstage-plugin-orchestrator-backend`, after every GraphQL read of a process definition, set `serviceUrl` to an absolute HTTP(S) origin: + +- If `serviceUrl` already starts with `http://` or `https://`, keep it. +- Else if `endpoint` is an absolute URL, set `serviceUrl` to `new URL(endpoint).origin`. +- Else leave `serviceUrl` undefined (existing “not available” errors). + +## In scope + +- `rhdh-plugins` workspace `workspaces/orchestrator`, plugin `orchestrator-backend` only. +- Unit tests for the helper and for `fetchWorkflowInfos` / `fetchWorkflowServiceUrls` mapping. + +## Out of scope + +- `rhdh-test-instance` rewrite removal (do that in a later PR after this plugin is in the catalog the smoke uses). +- Changing GraphQL queries beyond ensuring `endpoint` is already selected (it is, on `fetchWorkflowInfos` and `fetchWorkflowInfo`; `fetchWorkflowServiceUrls` must add `endpoint`). + +## Constraints + +- Do not break OSL ≤ 1.38 (absolute `serviceUrl` unchanged). +- Do not use the versioned path from `endpoint` for execute (plugin still posts to `{origin}/{id}`, not `{endpoint}`). SRVLOGIC-1124 is OSL-side; do not switch execute to `endpoint` in this change. +- Conventional commits; link SRVLOGIC-1137 / RHIDP-13375 in the PR description, not as a required Jira key in this repo unless the project uses GitHub issues. diff --git a/docs/superpowers/specs/2026-08-20-osl-rc-smoke-subset.md b/docs/superpowers/specs/2026-08-20-osl-rc-smoke-subset.md index 4cc9cac..aa68464 100644 --- a/docs/superpowers/specs/2026-08-20-osl-rc-smoke-subset.md +++ b/docs/superpowers/specs/2026-08-20-osl-rc-smoke-subset.md @@ -4,7 +4,7 @@ Research for this spec: RHIDP-13375, RHDH 1.10 Orchestrator docs, OSL 1.37–1.3 ## Problem -`./run-osl-regression.sh --test` (without `--full-e2e`) copies `playwright/osl-regression-smoke.spec.ts` into overlays e2e and runs **all 10** `registerOrchestratorCoreWorkflowTests` cases. That is more Playwright than an OSL CR gate needs: abort / status-detail / All Runs / suggested-link duplicate Failswitch OSL APIs and mostly assert RHDH UI. A single Greeting execute is **not** enough either: it misses Jobs Service timers, abort, switch/error, retrigger, and JWT/OpenAPI auth into the workflow runtime. +`./run-osl-regression.sh --test` used to copy `playwright/osl-regression-smoke.spec.ts` into overlays e2e and run **all 10** `registerOrchestratorCoreWorkflowTests` cases. That is more Playwright than an OSL CR gate needs: abort / status-detail / All Runs / suggested-link duplicate Failswitch OSL APIs and mostly assert RHDH UI. A single Greeting execute is **not** enough either: it misses Jobs Service timers, abort, switch/error, retrigger, and JWT/OpenAPI auth into the workflow runtime. OSL 1.39.CR1 also changed Data Index `ProcessDefinitions.serviceUrl` to a relative path (SRVLOGIC-1137). The current `osl-di-rewrite` proxy hides that from Playwright. There is no pre-Playwright check against the **raw** Data Index. @@ -14,12 +14,10 @@ Make the default OSL RC path (`--all` / `make osl-regression`) a **lean OSL cont 1. Probe raw Data Index GraphQL before Playwright. 2. Run exactly four Playwright tests (Greeting, Failswitch statuses, Failswitch retrigger, token-propagation). -3. Keep `--full-e2e` as the RHDH **plugin** regression gate, not the OSL CR default. ## In scope (this repo: `rhdh-test-instance`) -- Python helper + unit tests for smoke titles, Playwright `-g` regex, and GraphQL contract classification. -- `run-osl-regression.sh` wiring: probe, default grep, `--allow-relative-service-url`. +- `run-osl-regression.sh` wiring: raw Data Index GraphQL probe (`jq`), default Playwright `--grep` of the four titles, `--allow-relative-service-url`. - Smoke wrapper always registers token-propagation tests (no env flag). - Always deploy `sample-server` + `token-propagation` on the smoke path (same Keycloak substitutions overlays uses). - README / Makefile copy. @@ -56,16 +54,16 @@ Default `--test` / `--all` always: - Includes that title in the Playwright grep. - Waits for `deployment/token-propagation` Ready before the GraphQL probe. -There is no `--include-token-propagation` flag. +There is no `--include-token-propagation` or `--full-e2e` flag. `--cleanup` always removes operators, catalog, and mirror (the former `--include-operators` behavior). -## `--full-e2e` +## `--allow-relative-service-url` -Unchanged: runs overlays `--project=orchestrator` with no smoke wrapper and no title grep. Document as the plugin-release suite (RBAC, entity, ui:props, Loki, all workflows). +OSL 1.39.CR1 Data Index can return a relative `ProcessDefinitions.serviceUrl` (SRVLOGIC-1137). The Orchestrator plugin then cannot execute/abort/retrigger workflows. The smoke probe queries **raw** Data Index and exits 2 on that contract break. Pass `--allow-relative-service-url` or `ALLOW_RELATIVE_SERVICE_URL=1` to warn and continue so Playwright can still run behind `osl-di-rewrite`. Remove the override after the plugin derives `serviceUrl` from `endpoint`. ## Constraints - Do not commit `.env`, `.env.osl`, cluster passwords, or Keycloak secrets. - Do not edit files outside `rhdh-test-instance` for this spec. -- Python helpers: stdlib only (`unittest`, `json`, `urllib`/`json` parsing). No new pip deps. +- Driver is bash (`run-osl-regression.sh`). Classify GraphQL with `jq`. Do not add Python helper modules. - `oc` / `helm` may live in `/home/rlan/bin`; driver already assumes they are on `PATH`. - Conventional commits; reference `#13375`. diff --git a/helm/deploy.sh b/helm/deploy.sh index 84172a6..1feba6e 100755 --- a/helm/deploy.sh +++ b/helm/deploy.sh @@ -16,6 +16,11 @@ if [[ "$version" =~ ^([0-9]+(\.[0-9]+)?)$ ]]; then CV=$(curl -s "https://quay.io/api/v1/repository/rhdh/chart/tag/?onlyActiveTags=true&limit=600" | jq -r '.tags[].name' | grep "^${version}-" | sort -V | tail -n 1) elif [[ "$version" =~ CI$ ]]; then CV=$version +elif [[ "$version" == "next" ]]; then + CV=$(curl -s "https://quay.io/api/v1/repository/rhdh/chart/tag/?onlyActiveTags=true&limit=600" | jq -r '.tags[].name' | grep -- '-CI$' | sort -V | tail -n 1) + if [[ -z "$CV" ]]; then + CV="next" + fi else echo "Error: Invalid helm chart version: $version" [[ "$OPENSHIFT_CI" == "true" ]] && gh_comment "❌ **Error: Invalid helm chart version** 🚫\n\n📝 **Provided version:** \`$version\`\n\nPlease check your version and try again! 🔄" @@ -41,8 +46,37 @@ fi echo "Using ${CHART_URL} to install Helm chart" +append_to_dynamic_plugins_cm() { + local extra="$1" + local current + current="$(oc get configmap dynamic-plugins --namespace "$namespace" -o jsonpath='{.data.dynamic-plugins\.yaml}' 2>/dev/null || true)" + extra="$(printf '%s\n' "$extra" | sed '1{/^plugins:[[:space:]]*$/d;}')" + if [[ "$extra" == -* ]]; then + extra="$(printf '%s\n' "$extra" | sed 's/^/ /')" + fi + oc create configmap dynamic-plugins \ + --from-file=dynamic-plugins.yaml=<(printf '%s\n%s\n' "$current" "$extra") \ + --namespace "$namespace" --dry-run=client -o yaml \ + | oc apply -f - --namespace "$namespace" >/dev/null +} + +if [[ "${WITH_ORCHESTRATOR}" == "1" ]]; then + current_dp="$(oc get configmap dynamic-plugins --namespace "$namespace" -o jsonpath='{.data.dynamic-plugins\.yaml}' 2>/dev/null || true)" + if [[ "$current_dp" != *plugin-orchestrator* ]]; then + orch_file="config/orchestrator-dynamic-plugins.yaml" + if [[ "$version" == "next" || "$version" == *-CI ]]; then + orch_file="config/orchestrator-dynamic-plugins-next.yaml" + fi + echo "Merging orchestrator plugins from ${orch_file} into dynamic-plugins ConfigMap..." + append_to_dynamic_plugins_cm "$(cat "$orch_file")" + fi +fi + # Install orchestrator infrastructure if requested if [[ "${WITH_ORCHESTRATOR}" == "1" ]]; then + if [[ "${SKIP_ORCHESTRATOR_INFRA_INSTALL:-}" == "1" ]]; then + echo "Skipping orchestrator infrastructure chart installation (SKIP_ORCHESTRATOR_INFRA_INSTALL=1)." + else echo "Installing orchestrator infrastructure chart..." # Check if operators are already installed on the cluster (cluster-scoped, shared across namespaces) if oc get pods -n openshift-serverless --no-headers 2>/dev/null | grep -q . && \ @@ -66,6 +100,7 @@ if [[ "${WITH_ORCHESTRATOR}" == "1" ]]; then until [[ "$(oc get pods -n openshift-serverless --no-headers 2>/dev/null | wc -l)" -gt 0 ]]; do sleep 5; done until [[ "$(oc get pods -n openshift-serverless-logic --no-headers 2>/dev/null | wc -l)" -gt 0 ]]; do sleep 5; done echo "Serverless operator pods are running." + fi fi # Build dynamic plugins value file. @@ -101,6 +136,15 @@ HELM_ARGS=( if [[ "${WITH_ORCHESTRATOR}" == "1" ]]; then HELM_ARGS+=(--set orchestrator.enabled=true) + # setup-orchestrator.sh pre-installs Serverless/Logic + SonataFlowPlatform. + # Keep orchestrator plugins enabled in RHDH, but prevent chart-managed + # operator subscriptions from fighting the prepared OSL catalog. + if [[ "${SKIP_ORCHESTRATOR_INFRA_INSTALL:-}" == "1" ]]; then + HELM_ARGS+=( + --set orchestrator.serverlessLogicOperator.enabled=false + --set orchestrator.serverlessOperator.enabled=false + ) + fi fi if [[ "${IS_AUTH_ENABLED:-false}" != "true" ]]; then @@ -108,6 +152,24 @@ if [[ "${IS_AUTH_ENABLED:-false}" != "true" ]]; then --set "upstream.backstage.extraAppConfig[1].configMapRef=app-config-guest-auth" --set "upstream.backstage.extraAppConfig[1].filename=app-config-guest-auth.yaml" ) +elif [[ -n "${KEYCLOAK_BASE_URL:-}" ]]; then + echo "Applying OIDC app-config from Keycloak at ${KEYCLOAK_BASE_URL}" + oidc_tmp="$(mktemp)" + cp config/app-config-oidc.yaml "$oidc_tmp" + for key in KEYCLOAK_METADATA_URL KEYCLOAK_CLIENT_ID KEYCLOAK_CLIENT_SECRET RHDH_BASE_URL SONATAFLOW_DATA_INDEX_URL; do + val="${!key:-}" + val_esc="$(printf '%s' "$val" | sed -e 's/[&\\#]/\\&/g')" + sed -i "s#\${${key}}#${val_esc}#g" "$oidc_tmp" + done + oc create configmap app-config-oidc \ + --from-file=app-config-oidc.yaml="$oidc_tmp" \ + --namespace "$namespace" --dry-run=client -o yaml \ + | oc apply -f - --namespace "$namespace" >/dev/null + rm -f "$oidc_tmp" + HELM_ARGS+=( + --set "upstream.backstage.extraAppConfig[1].configMapRef=app-config-oidc" + --set "upstream.backstage.extraAppConfig[1].filename=app-config-oidc.yaml" + ) fi # Install or upgrade Helm chart diff --git a/playwright/osl-regression-smoke.spec.ts b/playwright/osl-regression-smoke.spec.ts index 2cf414f..93f1d89 100644 --- a/playwright/osl-regression-smoke.spec.ts +++ b/playwright/osl-regression-smoke.spec.ts @@ -77,16 +77,6 @@ test.beforeEach(async ({ page }) => { return loc; }; - const origGetByTestId = page.getByTestId.bind(page); - page.getByTestId = (testId, options) => { - if (testId === "info-card-subheader") { - return page - .getByRole("heading", { name: /^Run status$/i }) - .locator("xpath=following-sibling::*"); - } - return origGetByTestId(testId, options); - }; - const assertions = Object.getPrototypeOf(expect(page.locator("body"))); if (assertions && !assertions.__oslPatchedToHaveText && assertions.toHaveText) { const origToHaveText = assertions.toHaveText; diff --git a/prepare-osl-internal.sh b/prepare-osl-internal.sh new file mode 100755 index 0000000..6110b29 --- /dev/null +++ b/prepare-osl-internal.sh @@ -0,0 +1,538 @@ +#!/bin/bash +# +# Prepare pre-release OSL images for testing on an OpenShift cluster: +# 1) Mirror required images into the internal registry (single-arch by default) +# 2) Build a rewritten internal logic-only catalog image (hosted-compatible) +# 3) Create CatalogSource pointing at the rewritten internal catalog +# 4) Wait for CatalogSource to become READY +# 5) Write .env.osl with OSL_* exports for setup-orchestrator.sh +# +# Requires: oc, podman, skopeo, jq +# +# Usage: +# ./prepare-osl-internal.sh --release 1.39.0.CR1 +# +# Env var output chain: +# This script writes .env.osl with OSL_IIB_IMAGE, OSL_VERSION, +# OSL_LOGIC_CSV, and OSL_CATALOG_SOURCE. +# +# setup-orchestrator.sh sources .env.osl and translates these into +# --logic-operator-* flags for install-orchestrator.sh, which uses +# LOGIC_OPERATOR_SOURCE, LOGIC_OPERATOR_STARTING_CSV, etc. +# +# The overlays e2e tests (workflow-deployment-helpers.ts) read +# ORCH_E2E_LOGIC_OPERATOR_* env vars that map 1:1 to the same flags. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +RELEASES_DIR="${SCRIPT_DIR}/config/osl-releases" +ENV_OSL_FILE="${SCRIPT_DIR}/.env.osl" + +release="" +release_manifest="" +ocp_minor="" +mirror_namespace="osl-mirror" +multi_arch=false +SKOPEO_RETRY_TIMES="${SKOPEO_RETRY_TIMES:-3}" +CATALOGSOURCE_READY_TIMEOUT="${CATALOGSOURCE_READY_TIMEOUT:-600}" +ENFORCE_DIGEST_PINNING="${ENFORCE_DIGEST_PINNING:-0}" + +INTERNAL_REGISTRY_SERVICE="image-registry.openshift-image-registry.svc:5000" +CATALOGSOURCE_NAME="osl-custom-catalog" +DEST_REPOS=() +BUNDLE_DIGEST_PIN="" + +PULLER_GROUPS=( + "system:serviceaccounts:openshift-marketplace" + "system:serviceaccounts:openshift-operators" + "system:serviceaccounts:openshift-serverless" + "system:serviceaccounts:openshift-serverless-logic" +) +rhdh_namespace="orchestrator" + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- +usage() { + cat < [options] + +Required: + --release Release name (loads config/osl-releases/.json) + +Options: + --release-manifest Explicit manifest JSON path (overrides --release lookup) + --ocp-minor Override detected cluster version (e.g. 4.17) + --mirror-namespace Internal registry project (default: osl-mirror) + --namespace RHDH namespace granted image-puller on the mirror (default: orchestrator) + --multi-arch Mirror all architectures (default: amd64 only) + -h, --help Show this help +EOF +} + +log() { echo "==> $*"; } + +die() { echo "Error: $*" >&2; exit 1; } + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" +} + +ensure_cluster_access() { + oc whoami >/dev/null 2>&1 || die "Cannot reach OpenShift cluster. Run: oc login " +} + +detect_ocp_minor() { + local full + full="$(oc get clusterversion version -o jsonpath='{.status.desired.version}' 2>/dev/null || true)" + [[ -z "$full" ]] && die "Could not detect cluster version. Pass --ocp-minor manually." + echo "$full" | sed -E 's/^([0-9]+\.[0-9]+).*/\1/' +} + +ensure_internal_registry_route() { + oc patch configs.imageregistry.operator.openshift.io cluster \ + -p '{"spec":{"defaultRoute":true}}' --type=merge \ + -n openshift-image-registry >/dev/null 2>&1 + local host + host="$(oc get route default-route -n openshift-image-registry --template='{{ .spec.host }}' 2>/dev/null || true)" + [[ -z "$host" ]] && die "Could not resolve internal registry route." + echo "$host" +} + +wait_for_internal_registry_ready() { + local registry_host="$1" + local timeout_secs="${2:-300}" + local start + start="$(date +%s)" + + log "Waiting for internal registry deployment rollout..." + oc rollout status deployment/image-registry -n openshift-image-registry --timeout="${timeout_secs}s" >/dev/null + + log "Waiting for internal registry route to serve /v2/..." + while true; do + local code + code="$(curl -sk -o /dev/null -w '%{http_code}' "https://${registry_host}/v2/" || true)" + if [[ "$code" == "200" || "$code" == "401" ]]; then + return 0 + fi + if (( $(date +%s) - start >= timeout_secs )); then + die "internal registry route did not become ready (last HTTP status: ${code:-none})" + fi + sleep 5 + done +} + +login_internal_registry() { + local registry_host="$1" + local cluster_user="$2" + local token="$3" + + log "Logging into internal registry (tls-verify=true): ${registry_host}" + if podman login -u "$cluster_user" -p "$token" --tls-verify=true "$registry_host" >/dev/null 2>&1; then + return 0 + fi + + log "TLS-verified login failed; retrying with tls-verify=false for ${registry_host}" + podman login -u "$cluster_user" -p "$token" --tls-verify=false "$registry_host" >/dev/null +} + +ensure_pull_access() { + local ns="$1" + log "Granting image-puller RBAC in namespace: ${ns}" + local groups=("${PULLER_GROUPS[@]}") + if [[ -n "${rhdh_namespace}" ]]; then + groups+=("system:serviceaccounts:${rhdh_namespace}") + fi + local group + for group in "${groups[@]}"; do + oc policy add-role-to-group system:image-puller "$group" -n "$ns" >/dev/null 2>&1 || true + done +} + +update_cluster_pull_secret() { + local route_host="$1" cluster_user="$2" + local auth tmp_current tmp_updated + auth="$(printf '%s' "${cluster_user}:$(oc whoami -t)" | base64 -w0)" + tmp_current="$(mktemp)"; tmp_updated="$(mktemp)" + oc get secret pull-secret -n openshift-config -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d > "$tmp_current" + jq --arg auth "$auth" --arg rh "$route_host" --arg sh "$INTERNAL_REGISTRY_SERVICE" ' + .auths[$rh] = {"auth": $auth, "email": "unused@example.com"} | + .auths[$sh] = {"auth": $auth, "email": "unused@example.com"} + ' "$tmp_current" > "$tmp_updated" + if ! cmp -s "$tmp_current" "$tmp_updated"; then + oc set data secret/pull-secret -n openshift-config --from-file=.dockerconfigjson="$tmp_updated" >/dev/null + log "Updated cluster pull-secret with internal registry auth." + fi + rm -f "$tmp_current" "$tmp_updated" +} + +to_repo_name() { + local ref="$1" + echo "${ref%%@*}" | sed 's|.*/||' +} + +sed_escape_ere() { + printf '%s' "$1" | sed -e 's/[][(){}.^$|*+?\\]/\\&/g' +} + +sed_escape_repl() { + printf '%s' "$1" | sed -e 's/[&\\#]/\\&/g' +} + +# Rewrite OSL image refs under a directory to the internal mirror. +# If bundle_digest is sha256:..., catalog bundle images use that digest; +# all other mirrored repos are rewritten to the :mirror tag (hosted clusters +# cannot use IDMS, and internal-registry digests do not match upstream). +rewrite_osl_refs_in_dir() { + local root="$1" + local bundle_digest="${2:-}" + local internal="$INTERNAL_REGISTRY_SERVICE" + local ns="$mirror_namespace" + local prefix="${internal}/${ns}" + local prefix_esc old_esc dest_esc name_esc dest file tmp count=0 + local -a names=() + + prefix_esc="$(sed_escape_ere "$prefix")" + if ((${#DEST_REPOS[@]} > 0)); then + mapfile -t names < <(printf '%s\n' "${DEST_REPOS[@]}" | awk '{ print length, $0 }' | sort -nr | cut -d' ' -f2-) + fi + + while IFS= read -r -d '' file; do + tmp="$(mktemp)" + old_esc="$(sed_escape_ere "registry.redhat.io/openshift-serverless-1/")" + dest_esc="$(sed_escape_repl "${prefix}/openshift-serverless-1-")" + sed -E "s#${old_esc}#${dest_esc}#g" "$file" > "$tmp" + + old_esc="$(sed_escape_ere "registry.stage.redhat.io/openshift-serverless-1/")" + dest_esc="$(sed_escape_repl "${prefix}/openshift-serverless-1-")" + sed -E -i "s#${old_esc}#${dest_esc}#g" "$tmp" + + old_esc="$(sed_escape_ere "registry-proxy.engineering.redhat.com/rh-osbs/")" + dest_esc="$(sed_escape_repl "${prefix}/")" + sed -E -i "s#${old_esc}#${dest_esc}#g" "$tmp" + + for name in "${names[@]}"; do + [[ -n "$name" ]] || continue + if [[ "$bundle_digest" == sha256:* && "$name" == *bundle* ]]; then + dest="${prefix}/${name}@${bundle_digest}" + else + dest="${prefix}/${name}:mirror" + fi + name_esc="$(sed_escape_ere "$name")" + dest_esc="$(sed_escape_repl "$dest")" + sed -E -i \ + "s#${prefix_esc}/${name_esc}(@sha256:[a-fA-F0-9]+|:[A-Za-z0-9._-]+)?#${dest_esc}#g" \ + "$tmp" + done + + if ! cmp -s "$file" "$tmp"; then + cat "$tmp" > "$file" + count=$((count + 1)) + fi + rm -f "$tmp" + done < <(find "$root" -type f -print0) + + echo "rewritten files: ${count}" +} + +# --------------------------------------------------------------------------- +# Mirror a single image with retry and exponential backoff +# --------------------------------------------------------------------------- +mirror_image() { + local source_ref="$1" push_ref="$2" + + if skopeo inspect --no-tags --tls-verify=false "docker://${push_ref}" >/dev/null 2>&1; then + log " already present, skipping copy" + return 0 + fi + + local skopeo_args=(copy --preserve-digests --retry-times "$SKOPEO_RETRY_TIMES" + --dest-tls-verify=false) + if [[ "$multi_arch" == "true" ]]; then + skopeo_args+=(--all) + else + skopeo_args+=(--override-arch amd64 --override-os linux) + fi + + local attempt=0 max_attempts=3 wait_secs=10 + while (( attempt < max_attempts )); do + attempt=$((attempt + 1)) + if skopeo "${skopeo_args[@]}" "docker://${source_ref}" "docker://${push_ref}" >/dev/null 2>&1; then + return 0 + fi + if (( attempt < max_attempts )); then + log " Retry ${attempt}/${max_attempts} in ${wait_secs}s..." + sleep "$wait_secs" + wait_secs=$((wait_secs * 2)) + fi + done + die "Failed to mirror ${source_ref} after ${max_attempts} attempts" +} + +rewrite_operator_bundle_csv() { + local registry_host="$1" + local bundle_name="" + local i + for i in "${!image_names[@]}"; do + if [[ "${image_names[$i]}" == *bundle* ]]; then + bundle_name="$(to_repo_name "${image_sources[$i]}")" + break + fi + done + [[ -n "$bundle_name" ]] || { log "no operator-bundle image; skipping bundle CSV rewrite"; return 0; } + + local source="${registry_host}/${mirror_namespace}/${bundle_name}:mirror" + local workdir + workdir="$(mktemp -d)" + log "Rewriting operator-bundle CSV images in ${bundle_name}:mirror" + local cid + cid="$(podman create --tls-verify=false "$source" 2>/dev/null || podman create "$source")" + podman cp "${cid}:/manifests" "${workdir}/manifests" + podman cp "${cid}:/metadata" "${workdir}/metadata" >/dev/null 2>&1 || true + podman rm "$cid" >/dev/null + + rewrite_osl_refs_in_dir "$workdir" "" + + { + echo "FROM ${source}" + echo "COPY manifests /manifests" + [[ -d "${workdir}/metadata" ]] && echo "COPY metadata /metadata" + } > "${workdir}/Dockerfile" + podman build -t "$source" "$workdir" >/dev/null + podman push --tls-verify=false "$source" >/dev/null + BUNDLE_DIGEST_PIN="$(skopeo inspect --no-tags --tls-verify=false "docker://${source}" | jq -r '.Digest // empty')" + [[ "$BUNDLE_DIGEST_PIN" == sha256:* ]] || die "could not inspect rewritten bundle digest for ${source}" + log "Pushed rewritten operator-bundle: ${source} (${BUNDLE_DIGEST_PIN})" + rm -rf "$workdir" +} + +build_rewritten_logic_catalog() { + local registry_host="$1" + local iib_image_route="$2" + local rewritten_tag="logic-operator-catalog:rewritten" + local rewritten_route="${registry_host}/${mirror_namespace}/${rewritten_tag}" + + local workdir + workdir="$(mktemp -d)" + + log "Extracting file-based catalog configs from mirrored IIB..." + local cid + cid="$(podman create "${iib_image_route}")" + podman cp "${cid}":/configs "${workdir}/configs" + podman rm "${cid}" >/dev/null + + find "${workdir}/configs" -mindepth 1 -maxdepth 1 -type d ! -name 'logic-operator' -exec rm -rf {} + + + [[ -d "${workdir}/configs/logic-operator" ]] || die "logic-operator package not found in extracted catalog configs" + rewrite_osl_refs_in_dir "${workdir}/configs" "$BUNDLE_DIGEST_PIN" + + cat > "${workdir}/Dockerfile" <<'EOF' +FROM quay.io/operator-framework/opm:latest +COPY configs /configs +ENTRYPOINT ["/bin/opm"] +CMD ["serve", "/configs", "--cache-dir=/tmp/cache", "--cache-enforce-integrity=false"] +EOF + + log "Building rewritten logic-only catalog image..." + podman build -t "${rewritten_route}" "${workdir}" >/dev/null + podman push --tls-verify=false "${rewritten_route}" >/dev/null + log "Pushed rewritten catalog image: ${rewritten_route}" + + OSL_IIB_IMAGE="${INTERNAL_REGISTRY_SERVICE}/${mirror_namespace}/${rewritten_tag}" + rm -rf "${workdir}" +} + +# --------------------------------------------------------------------------- +# CatalogSource +# --------------------------------------------------------------------------- +create_catalogsource() { + local iib_image="$1" + log "Creating CatalogSource ${CATALOGSOURCE_NAME} -> ${iib_image}" + cat </dev/null +apiVersion: operators.coreos.com/v1alpha1 +kind: CatalogSource +metadata: + name: ${CATALOGSOURCE_NAME} + namespace: openshift-marketplace +spec: + sourceType: grpc + image: ${iib_image} + displayName: OSL Pre-release Catalog + publisher: Pre-release Testing +EOF +} + +wait_for_catalogsource_ready() { + log "Waiting for CatalogSource ${CATALOGSOURCE_NAME} to become READY (timeout ${CATALOGSOURCE_READY_TIMEOUT}s)..." + local start elapsed state + start=$(date +%s) + while true; do + state="$(oc get catalogsource "$CATALOGSOURCE_NAME" -n openshift-marketplace \ + -o jsonpath='{.status.connectionState.lastObservedState}' 2>/dev/null || true)" + if [[ "$state" == "READY" ]]; then + log "CatalogSource ${CATALOGSOURCE_NAME} is READY." + return 0 + fi + elapsed=$(( $(date +%s) - start )) + if (( elapsed >= CATALOGSOURCE_READY_TIMEOUT )); then + echo "CatalogSource status: ${state:-unknown}" >&2 + oc get catalogsource "$CATALOGSOURCE_NAME" -n openshift-marketplace -o yaml >&2 || true + die "CatalogSource ${CATALOGSOURCE_NAME} did not become READY within ${CATALOGSOURCE_READY_TIMEOUT}s" + fi + sleep 5 + done +} + +# --------------------------------------------------------------------------- +# Parse arguments +# --------------------------------------------------------------------------- +while [[ $# -gt 0 ]]; do + case "$1" in + --release) release="${2:-}"; shift 2 ;; + --release-manifest) release_manifest="${2:-}"; shift 2 ;; + --ocp-minor) ocp_minor="${2:-}"; shift 2 ;; + --mirror-namespace) mirror_namespace="${2:-}"; shift 2 ;; + --namespace) rhdh_namespace="${2:-}"; shift 2 ;; + --multi-arch) multi_arch=true; shift ;; + -h|--help) usage; exit 0 ;; + *) die "unknown option: $1" ;; + esac +done + +[[ -z "$release" && -z "$release_manifest" ]] && { usage; die "specify --release or --release-manifest."; } + +for cmd in oc podman skopeo jq; do + require_cmd "$cmd" +done + +ensure_cluster_access + +[[ -z "$ocp_minor" ]] && ocp_minor="$(detect_ocp_minor)" +[[ "$ocp_minor" =~ ^[0-9]+\.[0-9]+$ ]] || die "invalid --ocp-minor '$ocp_minor' (expected e.g. 4.17)" + +# Resolve manifest +if [[ -n "$release_manifest" ]]; then + manifest_file="$release_manifest" +else + manifest_file="${RELEASES_DIR}/${release}.json" +fi +[[ -f "$manifest_file" ]] || die "manifest not found: $manifest_file" +jq -e . "$manifest_file" >/dev/null 2>&1 || die "invalid JSON: $manifest_file" +[[ -z "$release" ]] && release="$(jq -r '.version // empty' "$manifest_file")" + +# Read manifest fields: .iib{"4.17": "..."}, .images[{source, name}] +iib_source="$(jq -r --arg ocp "$ocp_minor" '.iib[$ocp] // empty' "$manifest_file")" +[[ -z "$iib_source" ]] && die "manifest has no IIB for OCP ${ocp_minor}. Available: $(jq -r '.iib | keys | join(", ")' "$manifest_file")" + +osl_version="$(jq -r '.version // empty' "$manifest_file")" +osl_version_short="$(echo "$osl_version" | sed -E 's/^([0-9]+\.[0-9]+).*/\1/')" + +mapfile -t image_sources < <(jq -r '.images[].source' "$manifest_file") +mapfile -t image_names < <(jq -r '.images[].name' "$manifest_file") +(( ${#image_sources[@]} > 0 )) || die "manifest contains no images" + +release_slug="$(echo "$release" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//')" +iib_repo_name="osl-iib-${release_slug}-ocp-${ocp_minor//./-}" + +log "Release: ${release}" +log "OCP: ${ocp_minor}" +log "IIB: ${iib_source}" +log "Images: ${#image_sources[@]}" +log "Arch: $(if [[ "$multi_arch" == "true" ]]; then echo "multi"; else echo "amd64"; fi)" +log "Mode: rewrite-catalog (default)" + +if [[ "$iib_source" != *@sha256:* ]]; then + if [[ "$ENFORCE_DIGEST_PINNING" == "1" ]]; then + die "IIB image must be digest-pinned when ENFORCE_DIGEST_PINNING=1: ${iib_source}" + fi + log "WARNING: IIB image is not digest-pinned: ${iib_source}" +fi +for src in "${image_sources[@]}"; do + if [[ "$src" != *@sha256:* ]]; then + if [[ "$ENFORCE_DIGEST_PINNING" == "1" ]]; then + die "Manifest image is not digest-pinned while ENFORCE_DIGEST_PINNING=1: ${src}" + fi + log "WARNING: image source is not digest-pinned: ${src}" + fi +done + +# --------------------------------------------------------------------------- +# Setup registry access +# --------------------------------------------------------------------------- +registry_host="$(ensure_internal_registry_route)" +cluster_user="$(oc whoami)" +cluster_token="$(oc whoami -t)" +wait_for_internal_registry_ready "$registry_host" + +oc new-project "$mirror_namespace" >/dev/null 2>&1 || oc project "$mirror_namespace" >/dev/null 2>&1 || true +ensure_pull_access "$mirror_namespace" +update_cluster_pull_secret "$registry_host" "$cluster_user" + +login_internal_registry "$registry_host" "$cluster_user" "$cluster_token" + +# --------------------------------------------------------------------------- +# Mirror images +# --------------------------------------------------------------------------- +for i in "${!image_sources[@]}"; do + src="${image_sources[$i]}" + name="${image_names[$i]}" + # Destination repo must match the original image name so the rewritten + # catalog (registry-proxy.../rh-osbs/@sha256) can pull from osl-mirror. + repo_name="$(to_repo_name "$src")" + push_ref="${registry_host}/${mirror_namespace}/${repo_name}:mirror" + + log "Mirroring [$(( i + 1 ))/${#image_sources[@]}] ${name} -> ${repo_name}" + mirror_image "$src" "$push_ref" + DEST_REPOS+=("$repo_name") +done + +log "Mirroring IIB -> ${iib_repo_name}" +mirror_image "$iib_source" "${registry_host}/${mirror_namespace}/${iib_repo_name}:mirror" + +rewrite_operator_bundle_csv "$registry_host" + +# --------------------------------------------------------------------------- +# Hosted-compatible rewrite catalog path (default) +# --------------------------------------------------------------------------- +OSL_IIB_IMAGE="${INTERNAL_REGISTRY_SERVICE}/${mirror_namespace}/${iib_repo_name}:mirror" +build_rewritten_logic_catalog "${registry_host}" "${registry_host}/${mirror_namespace}/${iib_repo_name}:mirror" + +# --------------------------------------------------------------------------- +# CatalogSource + wait for READY +# --------------------------------------------------------------------------- +create_catalogsource "$OSL_IIB_IMAGE" +oc delete pod -n openshift-marketplace -l "olm.catalogSource=${CATALOGSOURCE_NAME}" --ignore-not-found >/dev/null 2>&1 || true +wait_for_catalogsource_ready + +if oc get csv -n openshift-operators -o name 2>/dev/null | grep -q logic-operator; then + log "Removing existing logic-operator CSV/subscription so OLM installs from the rewritten bundle" + oc get csv -n openshift-operators -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' \ + | grep '^logic-operator' \ + | xargs -r oc delete csv -n openshift-operators --ignore-not-found + oc delete subscription.operators.coreos.com logic-operator -n openshift-operators --ignore-not-found >/dev/null 2>&1 || true +fi + +# --------------------------------------------------------------------------- +# Write .env.osl +# --------------------------------------------------------------------------- +OSL_LOGIC_CSV="$(jq -r '.logic_csv // empty' "$manifest_file")" +if [[ -z "$OSL_LOGIC_CSV" ]]; then + OSL_LOGIC_CSV="logic-operator.v${osl_version_short}.0" +fi +cat > "$ENV_OSL_FILE" < RHDH version (required with --deploy / --all) @@ -55,9 +58,14 @@ Options: --osl-manifest Explicit OSL manifest path --namespace RHDH/orchestrator namespace (default: orchestrator) --overlays-dir rhdh-plugin-export-overlays checkout - --include-operators Cleanup also removes operators/catalog/mirror - --full-e2e Full overlays orchestrator Playwright project - --allow-relative-service-url Continue smoke if Data Index serviceUrl is relative + --allow-relative-service-url OSL 1.39 Data Index may return a relative + ProcessDefinitions.serviceUrl (SRVLOGIC-1137). + The GraphQL probe fails on that by default. + This flag (or ALLOW_RELATIVE_SERVICE_URL=1) + warns and continues so Playwright can run + behind the osl-di-rewrite proxy. Drop this + after the Orchestrator plugin derives + serviceUrl from endpoint. -h, --help Show this help EOF } @@ -81,8 +89,6 @@ while [[ $# -gt 0 ]]; do --osl-manifest) osl_manifest="${2:-}"; shift 2 ;; --namespace) namespace="${2:-}"; shift 2 ;; --overlays-dir) overlays_dir="${2:-}"; shift 2 ;; - --include-operators) include_operators=true; shift ;; - --full-e2e) full_e2e=true; shift ;; --allow-relative-service-url) allow_relative_service_url=true; shift ;; -h|--help) usage; exit 0 ;; *) usage; die "unknown option: $1" ;; @@ -94,7 +100,6 @@ if [[ "$run_all" == "true" ]]; then run_prepare=true run_deploy=true run_test=true - include_operators=true fi if [[ "$run_cleanup" != "true" && "$run_prepare" != "true" && "$run_deploy" != "true" && "$run_test" != "true" ]]; then @@ -127,7 +132,6 @@ preflight() { if [[ "$run_prepare" == "true" ]]; then require_cmd podman require_cmd skopeo - require_cmd python local manifest manifest="$(resolve_manifest)" [[ -n "$manifest" ]] || die "--prepare-osl requires --osl-release or --osl-manifest" @@ -301,40 +305,23 @@ ensure_token_propagation_workflow() { props_cm="${manifests_dir}/01-configmap_token-propagation-props.yaml" specs_cm="${manifests_dir}/03-configmap_02-token-propagation-resources-specs.yaml" [[ -f "$props_cm" && -f "$specs_cm" ]] || die "token-propagation manifests missing in $DEMO_WORKFLOW_REPO" - python3 - "$ns" "$props_cm" "$specs_cm" <<'PY' -from pathlib import Path -import os -import sys - -ns, props_path, specs_path = sys.argv[1], Path(sys.argv[2]), Path(sys.argv[3]) -kc = os.environ["KEYCLOAK_BASE_URL"].rstrip("/") -realm = os.environ.get("KEYCLOAK_REALM", "rhdh") -client_id = os.environ.get("KEYCLOAK_CLIENT_ID", "rhdh-client") -client_secret = os.environ.get("KEYCLOAK_CLIENT_SECRET", "rhdh-client-secret") -auth_server_url = f"{kc}/realms/{realm}" -token_url = f"{auth_server_url}/protocol/openid-connect/token" -props = props_path.read_text() -props = props.replace( - "http://example-kc-service.keycloak:8080/realms/quarkus", - auth_server_url, -) -props = props.replace("client-id=quarkus-app", f"client-id={client_id}") -props = props.replace( - "client-secret=lVGSvdaoDUem7lqeAnqXn1F92dCPbQea", - f"client-secret={client_secret}", -) -props = props.replace( - "http://sample-server-service.rhdh-operator", - f"http://sample-server-service.{ns}:8080", -) -props_path.write_text(props) -specs_path.write_text( - specs_path.read_text().replace( - "http://example-kc-service.keycloak:8080/realms/quarkus/protocol/openid-connect/token", - token_url, - ) -) -PY + local kc_base realm client_id client_secret auth_server_url token_url sample_url + kc_base="${KEYCLOAK_BASE_URL%/}" + realm="${KEYCLOAK_REALM:-rhdh}" + client_id="${KEYCLOAK_CLIENT_ID:-rhdh-client}" + client_secret="${KEYCLOAK_CLIENT_SECRET:-rhdh-client-secret}" + auth_server_url="${kc_base}/realms/${realm}" + token_url="${auth_server_url}/protocol/openid-connect/token" + sample_url="http://sample-server-service.${ns}:8080" + sed -i \ + -e "s|http://example-kc-service.keycloak:8080/realms/quarkus|${auth_server_url}|g" \ + -e "s|client-id=quarkus-app|client-id=${client_id}|g" \ + -e "s|client-secret=lVGSvdaoDUem7lqeAnqXn1F92dCPbQea|client-secret=${client_secret}|g" \ + -e "s|http://sample-server-service.rhdh-operator|${sample_url}|g" \ + "$props_cm" + sed -i \ + -e "s|http://example-kc-service.keycloak:8080/realms/quarkus/protocol/openid-connect/token|${token_url}|g" \ + "$specs_cm" oc apply -n "$ns" -f - </dev/null oidc_tmp="$(mktemp)" oc get configmap app-config-oidc -n "$ns" -o jsonpath='{.data.app-config-oidc\.yaml}' > "$oidc_tmp" - python - "$oidc_tmp" "$rewrite_url" <<'PY' -from pathlib import Path -import sys -path = Path(sys.argv[1]) -url = sys.argv[2] -text = path.read_text() -lines = [] -replaced = False -for line in text.splitlines(): - if line.strip().startswith("url:") and not replaced: - indent = line[: len(line) - len(line.lstrip())] - lines.append(f"{indent}url: {url}") - replaced = True - else: - lines.append(line) -path.write_text("\n".join(lines) + "\n") -PY + awk -v url="$rewrite_url" ' + BEGIN { done = 0 } + { + if (!done && $0 ~ /^[[:space:]]*url:/) { + match($0, /^[[:space:]]*/) + print substr($0, 1, RLENGTH) "url: " url + done = 1 + next + } + print + } + ' "$oidc_tmp" > "${oidc_tmp}.new" + mv "${oidc_tmp}.new" "$oidc_tmp" oc create configmap app-config-oidc \ --from-file=app-config-oidc.yaml="$oidc_tmp" \ -n "$ns" --dry-run=client -o yaml | oc apply -f - >/dev/null @@ -597,6 +573,39 @@ PY log "data-index rewrite proxy ready (${rewrite_url})" } +probe_raw_dataindex() { + local ns="$1" allow="$2" + local body url json count problems + body='{"query":"{ ProcessDefinitions { id serviceUrl endpoint } }"}' + url="http://sonataflow-platform-data-index-service.${ns}.svc.cluster.local/graphql" + log "probing raw Data Index GraphQL ProcessDefinitions.serviceUrl" + json="$(oc exec -n "$ns" deploy/redhat-developer-hub -- \ + curl -sS -X POST -H "Content-Type: application/json" -d "$body" "$url")" \ + || die "oc exec curl of Data Index GraphQL failed" + if ! printf '%s' "$json" | jq -e . >/dev/null 2>&1; then + die "Data Index did not return JSON: ${json:0:500}" + fi + if printf '%s' "$json" | jq -e '.errors != null and (.errors | length) > 0' >/dev/null; then + printf '%s\n' "$json" | jq '.errors' >&2 + die "Data Index GraphQL returned errors" + fi + count="$(printf '%s' "$json" | jq '.data.ProcessDefinitions | length // 0')" + if [[ "$count" -eq 0 ]]; then + printf '%s\n' '{"ok":false,"problems":[{"id":null,"serviceUrl":null,"endpoint":null,"reason":"no-process-definitions"}]}' >&2 + exit 1 + fi + problems="$(printf '%s' "$json" | jq '[.data.ProcessDefinitions[] | select((.serviceUrl | type != "string") or ((.serviceUrl | startswith("http://") or startswith("https://")) | not)) | {id, serviceUrl, endpoint, reason: "relative-or-missing-serviceUrl"}]')" + if [[ "$(printf '%s' "$problems" | jq 'length')" -gt 0 ]]; then + printf '%s\n' "$problems" | jq '{ok:false, problems:.}' >&2 + if [[ "$allow" == "true" ]]; then + log "WARNING: relative/missing serviceUrl allowed by ALLOW_RELATIVE_SERVICE_URL" + return 0 + fi + exit 2 + fi + printf '%s\n' '{"ok":true,"problems":[]}' >&2 +} + phase_deploy() { log "[deploy] RHDH ${rhdh} namespace=${namespace}" if [[ -f "${SCRIPT_DIR}/.env.osl" ]]; then @@ -613,15 +622,11 @@ phase_deploy() { phase_test() { log "[test]" overlays_dir="$(cd "$overlays_dir" && pwd)" - local e2e smoke_spec="" backup="" rc=0 smoke_grep="" - local -a probe_args + local e2e smoke_spec="" backup="" rc=0 allow_relative=false e2e="$(overlays_e2e_dir)" ensure_e2e_deps "$e2e" export K8S_CLUSTER_ROUTER_BASE RHDH_BASE_URL KEYCLOAK_BASE_URL RHDH_VERSION - export ORCH_E2E_USE_EXISTING_RHDH=true - export ORCH_E2E_SKIP_WORKFLOW_DEPLOY=false - export ORCH_E2E_SKIP_BASELINE_RBAC=false export SKIP_KEYCLOAK_DEPLOYMENT=true export SKIP_OPERATOR_INSTALLATION=true export GH_USER_ID=test1 @@ -645,31 +650,21 @@ phase_test() { } trap cleanup_test_artifacts EXIT - if [[ "$full_e2e" != "true" ]]; then - ensure_smoke_workflows "$namespace" - probe_args=(python3 "${SCRIPT_DIR}/utils/orchestrator/osl_smoke.py" probe --namespace "$namespace") - if [[ "$allow_relative_service_url" == "true" || "${ALLOW_RELATIVE_SERVICE_URL:-}" == "1" ]]; then - probe_args+=(--allow-relative) - fi - log "probing raw Data Index GraphQL ProcessDefinitions.serviceUrl" - "${probe_args[@]}" - smoke_spec="${e2e}/tests/${SMOKE_WRAPPER_NAME}" - cp -a "$SMOKE_WRAPPER_SRC" "$smoke_spec" + ensure_smoke_workflows "$namespace" + if [[ "$allow_relative_service_url" == "true" || "${ALLOW_RELATIVE_SERVICE_URL:-}" == "1" ]]; then + allow_relative=true fi + probe_raw_dataindex "$namespace" "$allow_relative" + smoke_spec="${e2e}/tests/${SMOKE_WRAPPER_NAME}" + cp -a "$SMOKE_WRAPPER_SRC" "$smoke_spec" local pw pw="$(playwright_cmd "$e2e")" log "Playwright: ${pw} (cwd=${e2e})" + log "Playwright grep: ${SMOKE_GREP}" set +e - if [[ "$full_e2e" == "true" ]]; then - # shellcheck disable=SC2086 - (cd "$e2e" && $pw test --project=orchestrator --workers=1) - else - smoke_grep="$(python3 "${SCRIPT_DIR}/utils/orchestrator/osl_smoke.py" grep)" - log "Playwright grep: ${smoke_grep}" - # shellcheck disable=SC2086 - (cd "$e2e" && $pw test --project=orchestrator --workers=1 --grep "$smoke_grep" "$smoke_spec") - fi + # shellcheck disable=SC2086 + (cd "$e2e" && $pw test --project=orchestrator --workers=1 --grep "$SMOKE_GREP" "$smoke_spec") rc=$? set -e @@ -681,7 +676,7 @@ phase_test() { log "Playwright failed (exit ${rc}); report: ${e2e}/playwright-report" exit "$rc" fi - log "Playwright smoke/full suite passed" + log "Playwright smoke passed" } preflight diff --git a/scripts/setup-resources.sh b/scripts/setup-resources.sh index c388153..6f599a6 100755 --- a/scripts/setup-resources.sh +++ b/scripts/setup-resources.sh @@ -41,11 +41,20 @@ create_rhdh_secrets() { : "${RHDH_BASE_URL:?RHDH_BASE_URL must be set before setup-resources.sh runs}" if oc get secret rhdh-secrets --namespace="${NAMESPACE}" &>/dev/null; then - # Secret already exists — only update RHDH_BASE_URL so the URL stays - # current without rotating SESSION_SECRET or clearing plugin-owned keys. - oc patch secret rhdh-secrets -n "${NAMESPACE}" --type=merge \ - -p "{\"stringData\":{\"RHDH_BASE_URL\":\"${RHDH_BASE_URL}\"}}" - echo "rhdh-secrets already exists — updated RHDH_BASE_URL only." + # Keep SESSION_SECRET stable; refresh URLs and Keycloak/orchestrator keys. + oc patch secret rhdh-secrets -n "${NAMESPACE}" --type=merge -p "{ + \"stringData\": { + \"RHDH_BASE_URL\": \"${RHDH_BASE_URL}\", + \"KEYCLOAK_BASE_URL\": \"${KEYCLOAK_BASE_URL:-}\", + \"KEYCLOAK_METADATA_URL\": \"${KEYCLOAK_METADATA_URL:-}\", + \"KEYCLOAK_LOGIN_REALM\": \"${KEYCLOAK_LOGIN_REALM:-}\", + \"KEYCLOAK_REALM\": \"${KEYCLOAK_REALM:-}\", + \"KEYCLOAK_CLIENT_ID\": \"${KEYCLOAK_CLIENT_ID:-}\", + \"KEYCLOAK_CLIENT_SECRET\": \"${KEYCLOAK_CLIENT_SECRET:-}\", + \"SONATAFLOW_DATA_INDEX_URL\": \"${SONATAFLOW_DATA_INDEX_URL:-}\" + } + }" + echo "rhdh-secrets already exists — updated URL/Keycloak/orchestrator keys." else # Generate a random session secret at deploy time so it is never hardcoded. local session_secret @@ -62,6 +71,7 @@ create_rhdh_secrets() { --from-literal=KEYCLOAK_CLIENT_SECRET="${KEYCLOAK_CLIENT_SECRET:-}" \ --from-literal=LIGHTHOUSE_URL="${LIGHTHOUSE_URL:-}" \ --from-literal=LIGHTHOUSE_SVC_URL="${LIGHTHOUSE_SVC_URL:-}" \ + --from-literal=SONATAFLOW_DATA_INDEX_URL="${SONATAFLOW_DATA_INDEX_URL:-}" \ --namespace="${NAMESPACE}" echo "rhdh-secrets created!" diff --git a/setup-orchestrator.sh b/setup-orchestrator.sh new file mode 100755 index 0000000..fe32c40 --- /dev/null +++ b/setup-orchestrator.sh @@ -0,0 +1,552 @@ +#!/bin/bash +# +# One-command setup of RHDH + orchestrator for overlays e2e. +# Deploys Keycloak, installs orchestrator prerequisites, deploys RHDH via Helm, +# and verifies the shared existing-RHDH substrate contract. +# +# Usage: +# ./setup-orchestrator.sh [--namespace ] [--prepare-internal-osl ] +# +# Examples: +# ./setup-orchestrator.sh 1.9 +# ./setup-orchestrator.sh 1.9-200-CI +# ./setup-orchestrator.sh next --namespace rhdh-test +# ./setup-orchestrator.sh 1.9 --prepare-internal-osl 1.39.0.CR1 +# ./setup-orchestrator.sh 1.10 --prepare-internal-osl 1.39.0.CR1 +# +# Options: +# --namespace Target namespace (default: orchestrator) +# --prepare-internal-osl +# Mirror pre-release OSL images into the OpenShift internal +# registry, generate a rewritten internal logic-only catalog, +# create CatalogSource, and write .env.osl with OSL_* exports +# for this run. +# Prerequisites: +# - oc logged in to the target cluster +# - helm, git, jq available on PATH +# - .env file configured (or --prepare-internal-osl to generate .env.osl) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Resolve the parent workspace directory: the main repo root's parent, even from a worktree. +_git_common="$(cd "$SCRIPT_DIR" && git rev-parse --git-common-dir 2>/dev/null)" +_main_repo_root="$(cd "$SCRIPT_DIR" && cd "$_git_common/.." 2>/dev/null && pwd)" +WORKSPACE_DIR="$(dirname "${_main_repo_root:-$SCRIPT_DIR}")" +RHDH_E2E_TEST_UTILS_DIR="${RHDH_E2E_TEST_UTILS_DIR:-${WORKSPACE_DIR}/rhdh-e2e-test-utils}" +unset _git_common _main_repo_root +SHARED_INSTALL_SCRIPT="${RHDH_E2E_TEST_UTILS_DIR}/dist/deployment/orchestrator/install-orchestrator.sh" +LOCAL_VERIFY_EXISTING_RHDH_SCRIPT="${SCRIPT_DIR}/utils/orchestrator/verify-existing-rhdh.sh" +SHARED_VERIFY_EXISTING_RHDH_SCRIPT="${SHARED_VERIFY_EXISTING_RHDH_SCRIPT:-$LOCAL_VERIFY_EXISTING_RHDH_SCRIPT}" +KEYCLOAK_NAMESPACE="${KEYCLOAK_NAMESPACE:-rhdh-keycloak}" + +# ── Argument parsing ───────────────────────────────────────────────────────── + +if [[ $# -lt 1 ]]; then + echo "Usage: $0 [--namespace ] [--prepare-internal-osl ]" + echo "" + echo "Examples:" + echo " $0 1.9 # latest 1.9.x chart" + echo " $0 1.9-200-CI # specific CI build" + echo " $0 next # latest development build" + echo " $0 1.9 --prepare-internal-osl 1.39.0.CR1" + echo " $0 1.10 --prepare-internal-osl 1.39.0.CR1" + exit 1 +fi + +version="$1" +shift + +namespace="orchestrator" +prepare_internal_osl_release="" +while [[ $# -gt 0 ]]; do + case "$1" in + --namespace) + namespace="$2" + shift 2 + ;; + --prepare-internal-osl) + prepare_internal_osl_release="${2:-}" + shift 2 + ;; + *) + echo "Error: Unknown option: $1" + exit 1 + ;; + esac +done + +# ── Validate inputs ────────────────────────────────────────────────────────── + +if ! oc whoami &>/dev/null; then + echo "Error: Cannot connect to OpenShift cluster. Is CRC running and are you logged in?" + echo " Try: crc start && oc login -u kubeadmin https://api.crc.testing:6443" + exit 1 +fi + +if [[ ! "$namespace" =~ ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ ]]; then + echo "Error: Invalid namespace name: '$namespace' (must be lowercase alphanumeric/hyphens, 1-63 chars)" + exit 1 +fi + +assert_empty_baseline() { + local ns="$1" + local keycloak_ns="$2" + local found=0 + + if [[ "${SKIP_EMPTY_BASELINE:-}" == "1" ]]; then + echo "==> Skipping empty-baseline check (SKIP_EMPTY_BASELINE=1)." + return 0 + fi + + echo "==> Verifying clean baseline (no existing RHDH/OSL components)..." + + if helm status redhat-developer-hub -n "$ns" >/dev/null 2>&1; then + echo "Error: Existing Helm release 'redhat-developer-hub' found in namespace '$ns'." + found=1 + fi + + if oc get deployment redhat-developer-hub -n "$ns" >/dev/null 2>&1; then + echo "Error: Existing deployment/redhat-developer-hub found in namespace '$ns'." + found=1 + fi + + if oc get sonataflowplatform -n "$ns" --no-headers 2>/dev/null | grep -q .; then + echo "Error: Existing SonataFlowPlatform resources found in namespace '$ns'." + found=1 + fi + + if oc get sonataflow -n "$ns" --no-headers 2>/dev/null | grep -q .; then + echo "Error: Existing SonataFlow workflow resources found in namespace '$ns'." + found=1 + fi + + if oc get subscription serverless-operator -n openshift-operators >/dev/null 2>&1; then + echo "Error: Existing Subscription/serverless-operator found in openshift-operators." + found=1 + fi + + if oc get subscription logic-operator -n openshift-operators >/dev/null 2>&1; then + echo "Error: Existing Subscription/logic-operator found in openshift-operators." + found=1 + fi + + if oc get catalogsource osl-custom-catalog -n openshift-marketplace >/dev/null 2>&1; then + if [[ -n "${OSL_CATALOG_SOURCE:-}" ]]; then + echo "==> CatalogSource/osl-custom-catalog present from prepare-osl; allowing it." + else + echo "Error: Existing CatalogSource/osl-custom-catalog found in openshift-marketplace." + found=1 + fi + fi + + if oc get statefulset keycloak -n "$keycloak_ns" >/dev/null 2>&1 || \ + oc get deployment keycloak -n "$keycloak_ns" >/dev/null 2>&1; then + echo "Error: Existing Keycloak deployment found in namespace '$keycloak_ns'." + found=1 + fi + + if [[ $found -ne 0 ]]; then + echo "" + echo "Cluster is not clean. Run cleanup first, e.g.:" + echo " ./cleanup.sh --namespace ${ns} --include-operators --delete-namespace" + echo "Then rerun setup." + exit 1 + fi +} + +# ── Helpers ────────────────────────────────────────────────────────────────── + +log() { echo "==> $*"; } +log_debug() { echo "[DEBUG $(date -u '+%Y-%m-%dT%H:%M:%SZ')] $*"; } +phase_checkpoint() { echo "[CHECKPOINT] $*"; } + +emit_diag_hints() { + local ns="$1" + echo "Diagnostics to run:" + echo " oc get pods -n ${ns}" + echo " oc get events -n ${ns} --sort-by=.lastTimestamp | tail -n 30" + echo " oc describe deployment redhat-developer-hub -n ${ns}" + echo " oc get csv -n openshift-operators" +} + +ensure_shared_scripts() { + if [[ ! -x "$SHARED_INSTALL_SCRIPT" ]]; then + if [[ -f "${RHDH_E2E_TEST_UTILS_DIR}/package.json" ]]; then + log "Building shared rhdh-e2e-test-utils artifacts..." + (cd "$RHDH_E2E_TEST_UTILS_DIR" && yarn build >/dev/null) + fi + fi + if [[ ! -x "$SHARED_INSTALL_SCRIPT" ]]; then + echo "Error: Shared install script not found: $SHARED_INSTALL_SCRIPT" + exit 1 + fi + if [[ ! -x "$SHARED_VERIFY_EXISTING_RHDH_SCRIPT" ]]; then + echo "Error: Existing-RHDH verification script not found or not executable: $SHARED_VERIFY_EXISTING_RHDH_SCRIPT" + echo "Hint: set SHARED_VERIFY_EXISTING_RHDH_SCRIPT to override, or use the local default script." + exit 1 + fi + log "Using existing-RHDH verification script: $SHARED_VERIFY_EXISTING_RHDH_SCRIPT" +} + +run_shared_orchestrator_install() { + local args=("$namespace") + + ensure_shared_scripts + + if [[ -n "${OSL_CATALOG_SOURCE:-}" ]]; then + args+=(--logic-operator-source "${OSL_CATALOG_SOURCE}") + args+=(--logic-operator-source-namespace "openshift-marketplace") + fi + [[ -n "${OSL_LOGIC_PACKAGE:-}" ]] && args+=(--logic-operator-package "${OSL_LOGIC_PACKAGE}") + [[ -n "${OSL_LOGIC_CHANNEL:-}" ]] && args+=(--logic-operator-channel "${OSL_LOGIC_CHANNEL}") + [[ -n "${OSL_LOGIC_CSV:-}" ]] && args+=(--logic-operator-starting-csv "${OSL_LOGIC_CSV}") + [[ -n "${OSL_SERVERLESS_PACKAGE:-}" ]] && args+=(--serverless-operator-package "${OSL_SERVERLESS_PACKAGE}") + [[ -n "${OSL_SERVERLESS_CHANNEL:-}" ]] && args+=(--serverless-operator-channel "${OSL_SERVERLESS_CHANNEL}") + [[ -n "${OSL_SERVERLESS_SOURCE:-}" ]] && args+=(--serverless-operator-source "${OSL_SERVERLESS_SOURCE}") + [[ -n "${OSL_SERVERLESS_SOURCE_NAMESPACE:-}" ]] && args+=(--serverless-operator-source-namespace "${OSL_SERVERLESS_SOURCE_NAMESPACE}") + + log_debug "Shared orchestrator install args: ${args[*]}" + bash "$SHARED_INSTALL_SCRIPT" "${args[@]}" + phase_checkpoint "shared-orchestrator-installed" +} + +extract_major_minor() { + local version="$1" + echo "$version" | sed -E 's/^([0-9]+\.[0-9]+).*/\1/' +} + +get_subscription_field() { + local name="$1" field="$2" + oc get subscriptions.operators.coreos.com "$name" -n openshift-operators -o "jsonpath={.spec.${field}}" 2>/dev/null || true +} + +get_operator_csv_name() { + local package="$1" + local csv_name + csv_name="$(oc get csv -n openshift-operators -l "operators.coreos.com/${package}.openshift-operators" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)" + if [[ -z "$csv_name" && "$package" == "logic-operator" ]]; then + csv_name="$(oc get csv -n openshift-operators -l "operators.coreos.com/logic-operator-rhel8.openshift-operators" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)" + fi + echo "$csv_name" +} + +get_operator_csv_version() { + local package="$1" + local csv_name + csv_name="$(get_operator_csv_name "$package")" + [[ -z "$csv_name" ]] && { echo ""; return 0; } + oc get csv "$csv_name" -n openshift-operators -o jsonpath='{.spec.version}' 2>/dev/null || true +} + +assert_operator_configuration() { + local package="$1" sub_name="$2" expected_channel="$3" expected_source="$4" expected_source_ns="$5" expected_starting_csv="$6" + local actual_channel actual_source actual_source_ns actual_starting_csv + actual_channel="$(get_subscription_field "$sub_name" channel)" + actual_source="$(get_subscription_field "$sub_name" source)" + actual_source_ns="$(get_subscription_field "$sub_name" sourceNamespace)" + actual_starting_csv="$(get_subscription_field "$sub_name" startingCSV)" + + if [[ -n "$expected_channel" && "$actual_channel" != "$expected_channel" ]]; then + echo "Error: ${package} channel mismatch. expected='${expected_channel}' actual='${actual_channel}'" + exit 1 + fi + if [[ -n "$expected_source" && "$actual_source" != "$expected_source" ]]; then + echo "Error: ${package} source mismatch. expected='${expected_source}' actual='${actual_source}'" + exit 1 + fi + if [[ -n "$expected_source_ns" && "$actual_source_ns" != "$expected_source_ns" ]]; then + echo "Error: ${package} source namespace mismatch. expected='${expected_source_ns}' actual='${actual_source_ns}'" + exit 1 + fi + if [[ -n "$expected_starting_csv" && "$actual_starting_csv" != "$expected_starting_csv" ]]; then + echo "Error: ${package} startingCSV mismatch. expected='${expected_starting_csv}' actual='${actual_starting_csv}'" + exit 1 + fi +} + +assert_pre_release_install_state() { + local expected_logic_source="${OSL_CATALOG_SOURCE:-${OSL_LOGIC_SOURCE:-}}" + local expected_logic_source_ns="${OSL_LOGIC_SOURCE_NAMESPACE:-openshift-marketplace}" + local expected_logic_channel="${OSL_LOGIC_CHANNEL:-stable}" + local expected_logic_csv="${OSL_LOGIC_CSV:-}" + + local expected_serverless_source="${OSL_SERVERLESS_SOURCE:-redhat-operators}" + local expected_serverless_source_ns="${OSL_SERVERLESS_SOURCE_NAMESPACE:-openshift-marketplace}" + local expected_serverless_channel="${OSL_SERVERLESS_CHANNEL:-stable}" + + log "Asserting installed operator subscriptions and versions..." + assert_operator_configuration "logic-operator" "logic-operator" "$expected_logic_channel" "$expected_logic_source" "$expected_logic_source_ns" "$expected_logic_csv" + assert_operator_configuration "serverless-operator" "serverless-operator" "$expected_serverless_channel" "$expected_serverless_source" "$expected_serverless_source_ns" "" + + local logic_csv logic_version serverless_version logic_mm serverless_mm + logic_csv="$(get_operator_csv_name "logic-operator")" + logic_version="$(get_operator_csv_version "logic-operator")" + serverless_version="$(get_operator_csv_version "serverless-operator")" + + if [[ -z "$logic_csv" || -z "$logic_version" ]]; then + echo "Error: Unable to resolve installed logic-operator CSV/version." + exit 1 + fi + + if [[ -n "${OSL_VERSION:-}" ]]; then + local osl_marker + osl_marker="$(echo "${OSL_VERSION}" | tr '[:upper:]' '[:lower:]')" + local csv_lc version_lc + csv_lc="$(echo "${logic_csv}" | tr '[:upper:]' '[:lower:]')" + version_lc="$(echo "${logic_version}" | tr '[:upper:]' '[:lower:]')" + if [[ "$osl_marker" == *"cr"* || "$osl_marker" == *"rc"* ]]; then + # Some pre-release catalogs publish a GA-looking CSV/version while still being + # sourced from a pre-release catalog and pinned startingCSV; accept that case. + if [[ "$csv_lc" != *"cr"* && "$csv_lc" != *"rc"* && "$version_lc" != *"cr"* && "$version_lc" != *"rc"* ]]; then + if [[ -n "${expected_logic_csv:-}" && "$logic_csv" == "$expected_logic_csv" ]]; then + log "Pre-release marker not present in CSV/version; accepted because installed CSV matches expected startingCSV (${expected_logic_csv})." + else + echo "Error: Expected pre-release OSL marker in installed logic-operator CSV/version. csv='${logic_csv}' version='${logic_version}'" + exit 1 + fi + fi + fi + fi + + logic_mm="$(extract_major_minor "$logic_version")" + serverless_mm="$(extract_major_minor "$serverless_version")" + if [[ -n "$logic_mm" && -n "$serverless_mm" && "$logic_mm" != "$serverless_mm" ]]; then + if [[ "${ALLOW_OSL_SERVERLESS_VERSION_SKEW:-0}" != "1" ]]; then + echo "Error: Serverless/Logic major.minor mismatch (serverless=${serverless_mm}, logic=${logic_mm}). Set ALLOW_OSL_SERVERLESS_VERSION_SKEW=1 to override." + exit 1 + fi + echo "Warning: Serverless/Logic major.minor mismatch allowed by ALLOW_OSL_SERVERLESS_VERSION_SKEW=1 (serverless=${serverless_mm}, logic=${logic_mm})." + fi + + log "Installed logic-operator CSV: ${logic_csv} (version=${logic_version})" + log "Installed serverless-operator version: ${serverless_version:-unknown}" + phase_checkpoint "operator-configuration-asserted" +} + +prepare_keycloak() { + # shellcheck disable=SC1091 + source "$SCRIPT_DIR/utils/keycloak/keycloak-deploy.sh" "$KEYCLOAK_NAMESPACE" +} + +sync_keycloak_runtime_env() { + local keycloak_host + keycloak_host="$(oc get route keycloak -n "$KEYCLOAK_NAMESPACE" -o jsonpath='{.spec.host}' 2>/dev/null || true)" + if [[ -z "$keycloak_host" ]]; then + echo "Error: could not resolve Keycloak route in namespace '$KEYCLOAK_NAMESPACE'." + exit 1 + fi + + export KEYCLOAK_BASE_URL="https://${keycloak_host}" + export KEYCLOAK_METADATA_URL="${KEYCLOAK_BASE_URL}/realms/rhdh" + export KEYCLOAK_REALM="${KEYCLOAK_REALM:-rhdh}" + export KEYCLOAK_LOGIN_REALM="${KEYCLOAK_LOGIN_REALM:-${KEYCLOAK_REALM}}" + export KEYCLOAK_CLIENT_ID="${KEYCLOAK_CLIENT_ID:-rhdh-client}" + export KEYCLOAK_CLIENT_SECRET="${KEYCLOAK_CLIENT_SECRET:-rhdh-client-secret}" + + if [[ -z "${KEYCLOAK_LOGIN_REALM}" ]]; then + echo "Error: KEYCLOAK_LOGIN_REALM resolved to empty value." + exit 1 + fi +} + +verify_shared_existing_rhdh_contract() { + log "Verifying shared existing-RHDH contract in ${namespace}..." + bash "$SHARED_VERIFY_EXISTING_RHDH_SCRIPT" "$namespace" --require-keycloak + phase_checkpoint "shared-existing-rhdh-verified" +} + +log_debug "Entrypoint args: version=${version}, namespace=${namespace}, prepareInternalOsl=${prepare_internal_osl_release:-none}" +phase_checkpoint "cluster-connectivity-validated" +assert_empty_baseline "$namespace" "$KEYCLOAK_NAMESPACE" + +wait_for_rhdh_auth_and_orchestrator_ready() { + local ns="$1" + local timeout_secs="${2:-240}" + local start_time + start_time=$(date +%s) + + local rhdh_host + rhdh_host="$(oc get route redhat-developer-hub -n "$ns" -o jsonpath='{.spec.host}' 2>/dev/null || true)" + if [[ -z "$rhdh_host" ]]; then + echo "Error: Could not resolve RHDH route in namespace '$ns'." + return 1 + fi + + log "Waiting for RHDH auth/backend HTTP readiness..." + while true; do + local elapsed auth_status auth_location app_health orch_health + elapsed=$(( $(date +%s) - start_time )) + if [[ $elapsed -ge $timeout_secs ]]; then + echo "Error: Timed out waiting for auth/backend HTTP readiness after ${timeout_secs}s" + echo " Last auth status: ${auth_status:-unknown}" + echo " Last auth redirect: ${auth_location:-}" + echo " Last backend health: ${app_health:-unknown}" + echo " Last orchestrator health: ${orch_health:-unknown}" + return 1 + fi + + auth_status=$(curl -sk -o /dev/null -w '%{http_code}' "https://${rhdh_host}/api/auth/oidc/start?env=production" || true) + auth_location=$(curl -sk -D - -o /dev/null "https://${rhdh_host}/api/auth/oidc/start?env=production" | \ + awk 'BEGIN{IGNORECASE=1} /^location:/ {print $2; exit}' | tr -d '\r') + app_health=$(curl -sk -o /dev/null -w '%{http_code}' "https://${rhdh_host}/api/app/health" || true) + orch_health=$(curl -sk -o /dev/null -w '%{http_code}' "https://${rhdh_host}/api/orchestrator/health" || true) + + if [[ "$app_health" == "200" && "$auth_status" == "302" && "$auth_location" =~ ^https:// && "$orch_health" == "200" ]]; then + log "RHDH auth/backend/orchestrator readiness checks passed." + return 0 + fi + + sleep 3 + done +} + +run_post_setup_workflow_smoke() { + local ns="$1" + local run_smoke="${POST_SETUP_WORKFLOW_SMOKE:-1}" + if [[ "$run_smoke" != "1" ]]; then + log "Skipping post-setup workflow smoke (POST_SETUP_WORKFLOW_SMOKE=${run_smoke})." + return 0 + fi + + local workflow_repo="${SERVERLESS_WORKFLOWS_REPO:-https://github.com/rhdhorchestrator/serverless-workflows.git}" + local workflow_ref="${SERVERLESS_WORKFLOWS_REF:-daeeee8dec16beab6d96a81774ef500081a2c2b0}" + local workflow_dir="/tmp/serverless-workflows-${RANDOM}-${RANDOM}" + local greeting_manifest_dir="${workflow_dir}/workflows/greeting/manifests" + + log "Running post-setup workflow smoke in namespace ${ns}..." + git clone --depth=1 "$workflow_repo" "$workflow_dir" >/dev/null 2>&1 + git -C "$workflow_dir" fetch --depth=1 origin "$workflow_ref" >/dev/null 2>&1 + git -C "$workflow_dir" checkout --detach "$workflow_ref" >/dev/null 2>&1 + + oc apply -n "$ns" -f "$greeting_manifest_dir" >/dev/null + oc patch sonataflow greeting -n "$ns" --type merge -p '{ + "spec": { + "persistence": { + "postgresql": { + "secretRef": { + "name": "backstage-psql-secret", + "userKey": "POSTGRES_USER", + "passwordKey": "POSTGRES_PASSWORD" + }, + "serviceRef": { + "name": "backstage-psql", + "namespace": "'"$ns"'", + "databaseName": "backstage_plugin_orchestrator" + } + } + } + } + }' >/dev/null + + oc rollout restart deployment/greeting -n "$ns" >/dev/null 2>&1 || true + oc rollout status deployment/greeting -n "$ns" --timeout=600s >/dev/null + oc exec -n "$ns" deploy/sonataflow-platform-data-index-service -- \ + curl -sf --max-time 5 "http://localhost:8080/q/health/ready" >/dev/null + + local orchestrator_host orch_health + orchestrator_host="$(oc get route redhat-developer-hub -n "$ns" -o jsonpath='{.spec.host}' 2>/dev/null || true)" + orch_health="$(curl -sk -o /dev/null -w '%{http_code}' "https://${orchestrator_host}/api/orchestrator/health" || true)" + if [[ "$orch_health" != "200" ]]; then + echo "Error: Post-smoke orchestrator health check failed (HTTP ${orch_health})." + rm -rf "$workflow_dir" + exit 1 + fi + + rm -rf "$workflow_dir" + phase_checkpoint "post-setup-workflow-smoke-passed" +} + +# ── Internal pre-release OSL preparation ────────────────────────────────────── + +if [[ -n "$prepare_internal_osl_release" ]]; then + log "Preparing internal OSL mirror for release ${prepare_internal_osl_release}..." + "${SCRIPT_DIR}/prepare-osl-internal.sh" --release "${prepare_internal_osl_release}" --namespace "${namespace}" + # shellcheck disable=SC1091 + source "${SCRIPT_DIR}/.env.osl" + log "Loaded OSL_IIB_IMAGE=${OSL_IIB_IMAGE}" + log "Loaded OSL_VERSION=${OSL_VERSION}" + log "Loaded OSL_LOGIC_CSV=${OSL_LOGIC_CSV}" + log "Loaded OSL_CATALOG_SOURCE=${OSL_CATALOG_SOURCE}" + phase_checkpoint "internal-mirror-prep-complete" +elif [[ -f "${SCRIPT_DIR}/.env.osl" ]]; then + # shellcheck disable=SC1091 + source "${SCRIPT_DIR}/.env.osl" + log "Loaded existing .env.osl (OSL_LOGIC_CSV=${OSL_LOGIC_CSV:-unset} OSL_CATALOG_SOURCE=${OSL_CATALOG_SOURCE:-unset})" +fi + +# ── Pre-deploy: export secrets for envsubst in helm/deploy.sh ─────────────── + +export BACKEND_SECRET="${BACKEND_SECRET:-$(openssl rand -hex 32)}" +export NODE_TLS_REJECT_UNAUTHORIZED="${NODE_TLS_REJECT_UNAUTHORIZED:-1}" + +# ── Pre-deploy: shared orchestrator install spine ─────────────────────────── + +if [[ -n "${OSL_VERSION:-}" && -n "${OSL_IIB_IMAGE:-}" && -z "${OSL_LOGIC_CSV:-}" ]]; then + OSL_LOGIC_CSV="logic-operator.v$(extract_major_minor "${OSL_VERSION}").0" +fi + +log "Preparing Keycloak before shared orchestrator install..." +prepare_keycloak +sync_keycloak_runtime_env + +run_shared_orchestrator_install +assert_pre_release_install_state + +# ── Deploy RHDH + orchestrator ────────────────────────────────────────────── + +export SONATAFLOW_DATA_INDEX_URL="http://sonataflow-platform-data-index-service.${namespace}.svc.cluster.local" +export IS_AUTH_ENABLED="true" + +log "Deploying RHDH $version with shared orchestrator support" +cd "$SCRIPT_DIR" +SKIP_ENV_SOURCE=1 \ +SKIP_ORCHESTRATOR_INFRA_INSTALL=1 \ +./deploy.sh helm "$version" --namespace "$namespace" --with-orchestrator +phase_checkpoint "rhdh-deployed" + +# ── Verify overlays existing-RHDH contract ─────────────────────────────────── + +verify_shared_existing_rhdh_contract +phase_checkpoint "overlays-existing-rhdh-prepared" + +# ── Wait for RHDH readiness ───────────────────────────────────────────────── + +log "Waiting for RHDH to become ready..." +oc rollout status deployment/redhat-developer-hub -n "$namespace" --timeout=600s || { + echo "Warning: RHDH did not become ready within timeout" + emit_diag_hints "$namespace" +} +wait_for_rhdh_auth_and_orchestrator_ready "$namespace" +run_post_setup_workflow_smoke "$namespace" + +# ── Summary ────────────────────────────────────────────────────────────────── + +rhdh_host="$(oc get route redhat-developer-hub -n "$namespace" -o jsonpath='{.spec.host}' 2>/dev/null || true)" +keycloak_host="$(oc get route keycloak -n "$KEYCLOAK_NAMESPACE" -o jsonpath='{.spec.host}' 2>/dev/null || true)" +RHDH_URL="${rhdh_host:+https://${rhdh_host}}" +KEYCLOAK_URL="${keycloak_host:+https://${keycloak_host}}" + +echo "" +echo "===========================================" +echo " Setup Complete" +echo "===========================================" +echo "" +echo "RHDH URL: $RHDH_URL" +echo "Keycloak URL: $KEYCLOAK_URL" +echo "Keycloak Admin: admin / admin123" +echo "Test Users: test1 / test1@123, test2 / test2@123" +echo "" +DEPLOYED_CV=$(helm list -n "$namespace" -f redhat-developer-hub -o json 2>/dev/null | jq -r '.[0].chart // empty' | sed 's/^redhat-developer-hub-//') +echo "Namespace: $namespace" +echo "Chart Version: ${DEPLOYED_CV:-unknown}" +echo "" +echo "Pod status:" +oc get pods -n "$namespace" --no-headers 2>/dev/null | sed 's/^/ /' +echo "" +echo "SonataFlow workflows:" +oc get sonataflow -n "$namespace" --no-headers 2>/dev/null | sed 's/^/ /' || echo " (none)" +echo "" +echo "OSL operator versions:" +oc get csv -n openshift-operators --no-headers -o custom-columns='NAME:.metadata.name,VERSION:.spec.version' 2>/dev/null | sed 's/^/ /' || true +echo "" diff --git a/utils/keycloak/groups.json b/utils/keycloak/groups.json new file mode 100755 index 0000000..9d8bd91 --- /dev/null +++ b/utils/keycloak/groups.json @@ -0,0 +1,5 @@ +[ + {"name": "developers"}, + {"name": "admins"}, + {"name": "viewers"} +] diff --git a/utils/keycloak/keycloak-deploy.sh b/utils/keycloak/keycloak-deploy.sh new file mode 100755 index 0000000..a8a9e6d --- /dev/null +++ b/utils/keycloak/keycloak-deploy.sh @@ -0,0 +1,240 @@ +#!/bin/bash +set -e + +# Check for required dependencies +command -v jq >/dev/null 2>&1 || { echo "Error: jq is required but not installed"; exit 1; } +command -v oc >/dev/null 2>&1 || { echo "Error: oc (OpenShift CLI) is required but not installed"; exit 1; } + +NAMESPACE=${1:-rhdh-keycloak} +USERS_FILE=${2:-utils/keycloak/users.json} +GROUPS_FILE=${3:-utils/keycloak/groups.json} +CLIENT_FILE="utils/keycloak/rhdh-client.json" +KEYCLOAK_RELEASE_NAME="keycloak" + +# Helper function for API calls with error checking +api_call() { + local method=$1 + local url=$2 + local data=$3 + local description=$4 + + if [ -n "$data" ]; then + RESPONSE=$(curl -sk -w "\n%{http_code}" -X "$method" "$url" \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + -H "Content-Type: application/json" \ + -d "$data") + else + RESPONSE=$(curl -sk -w "\n%{http_code}" -X "$method" "$url" \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + -H "Content-Type: application/json") + fi + + HTTP_CODE=$(echo "$RESPONSE" | tail -1) + BODY=$(echo "$RESPONSE" | sed '$d') + + if [ "$method" = "GET" ] || [ "$HTTP_CODE" -lt 400 ]; then + echo "$BODY" + return 0 + fi + + # 409 Conflict is acceptable for create operations (already exists) + if [ "$HTTP_CODE" = "409" ]; then + echo "Warning: $description - already exists (continuing)" >&2 + echo "$BODY" + return 0 + fi + + echo "Error: $description failed (HTTP $HTTP_CODE): $BODY" >&2 + return 1 +} + +# Validate JSON files exist and are valid +[ ! -f "$CLIENT_FILE" ] && echo "Error: Client configuration file not found: $CLIENT_FILE" && exit 1 +jq empty "$CLIENT_FILE" 2>/dev/null || { echo "Error: Invalid JSON in $CLIENT_FILE"; exit 1; } +[ -f "$USERS_FILE" ] && { jq empty "$USERS_FILE" 2>/dev/null || { echo "Error: Invalid JSON in $USERS_FILE"; exit 1; }; } +[ -f "$GROUPS_FILE" ] && { jq empty "$GROUPS_FILE" 2>/dev/null || { echo "Error: Invalid JSON in $GROUPS_FILE"; exit 1; }; } + +# Create namespace and deploy Keycloak +echo "Creating namespace $NAMESPACE..." +oc create namespace $NAMESPACE --dry-run=client -o yaml | oc apply -f - + +echo "Adding Bitnami Helm repository..." +helm repo add bitnami https://charts.bitnami.com/bitnami +helm repo update + +echo "Deploying Keycloak..." +helm upgrade --install $KEYCLOAK_RELEASE_NAME bitnami/keycloak \ + --namespace $NAMESPACE \ + --values utils/keycloak/keycloak-values.yaml + +echo "Waiting for Keycloak rollout..." +oc rollout status statefulset/keycloak -n $NAMESPACE --timeout=5m + +# Detect TLS based on cluster route configuration +if oc get route console -n openshift-console -o=jsonpath='{.spec.tls.termination}' 2>/dev/null | grep -q .; then + KEYCLOAK_PROTOCOL="https" +else + KEYCLOAK_PROTOCOL="http" +fi + +# Create OpenShift Route +echo "Creating OpenShift Route (protocol: $KEYCLOAK_PROTOCOL)..." +if [ "$KEYCLOAK_PROTOCOL" = "https" ]; then +cat </dev/null || echo "000") + if [ "$HTTP_STATUS" = "200" ]; then + break + fi + sleep 5 + ELAPSED=$((ELAPSED + 5)) + if [ $ELAPSED -ge $TIMEOUT ]; then + echo "Error: Keycloak API not ready after 5 minutes (last status: $HTTP_STATUS)" + exit 1 + fi + echo " Waiting... (status: $HTTP_STATUS)" +done + +# Get admin token +TOKEN_RESPONSE=$(curl -sk -w "\n%{http_code}" -X POST "$KEYCLOAK_URL/realms/master/protocol/openid-connect/token" \ + -d "username=admin&password=admin123&grant_type=password&client_id=admin-cli") +TOKEN_HTTP_CODE=$(echo "$TOKEN_RESPONSE" | tail -1) +TOKEN_BODY=$(echo "$TOKEN_RESPONSE" | sed '$d') +[ "$TOKEN_HTTP_CODE" -ge 400 ] && echo "Error: Failed to get admin token (HTTP $TOKEN_HTTP_CODE): $TOKEN_BODY" && exit 1 +ADMIN_TOKEN=$(echo "$TOKEN_BODY" | jq -r '.access_token // empty') +[ -z "$ADMIN_TOKEN" ] && echo "Error: Failed to parse admin token" && exit 1 + +# Create realm and client +echo "Creating realm 'rhdh'..." +api_call POST "$KEYCLOAK_URL/admin/realms" \ + '{"realm":"rhdh","enabled":true,"displayName":"RHDH Realm"}' \ + "Create realm" >/dev/null + +echo "Creating client..." +api_call POST "$KEYCLOAK_URL/admin/realms/rhdh/clients" \ + "$(jq -c '.' "$CLIENT_FILE")" \ + "Create client" >/dev/null + +# Get IDs for role assignment +SERVICE_ACCOUNT_ID=$(api_call GET "$KEYCLOAK_URL/admin/realms/rhdh/users?username=service-account-rhdh-client" "" "Get service account" | \ + jq -r '.[0].id // empty') +[ -z "$SERVICE_ACCOUNT_ID" ] && echo "Error: Service account not found" && exit 1 + +REALM_MGMT_ID=$(api_call GET "$KEYCLOAK_URL/admin/realms/rhdh/clients?clientId=realm-management" "" "Get realm-management client" | \ + jq -r '.[0].id // empty') +[ -z "$REALM_MGMT_ID" ] && echo "Error: realm-management client not found" && exit 1 + +ROLES=$(api_call GET "$KEYCLOAK_URL/admin/realms/rhdh/clients/$REALM_MGMT_ID/roles" "" "Get roles" | \ + jq -c '[.[] | select(.name == "view-authorization" or .name == "manage-authorization" or .name == "view-users")]') +[ -z "$ROLES" ] || [ "$ROLES" = "[]" ] && echo "Error: Required roles not found" && exit 1 + +echo "Assigning service account roles..." +api_call POST "$KEYCLOAK_URL/admin/realms/rhdh/users/$SERVICE_ACCOUNT_ID/role-mappings/clients/$REALM_MGMT_ID" \ + "$ROLES" \ + "Assign roles" >/dev/null + +# Create groups +if [ -f "$GROUPS_FILE" ]; then + echo "Creating groups..." + jq -r '.[].name' "$GROUPS_FILE" | while read -r group; do + api_call POST "$KEYCLOAK_URL/admin/realms/rhdh/groups" \ + "{\"name\":\"$group\"}" \ + "Create group '$group'" >/dev/null && echo " Created group: $group" || echo " Warning: Failed to create group: $group" + done +fi + +# Create users +if [ -f "$USERS_FILE" ]; then + echo "Creating users..." + + jq -c '.[]' "$USERS_FILE" | while read -r user_json; do + username=$(echo "$user_json" | jq -r '.username') + groups=$(echo "$user_json" | jq -r '.groups // [] | join(",")') + user_payload=$(echo "$user_json" | jq -c 'del(.groups)') + + if ! api_call POST "$KEYCLOAK_URL/admin/realms/rhdh/users" "$user_payload" "Create user '$username'" >/dev/null; then + echo " Warning: Failed to create user: $username" + continue + fi + echo " Created user: $username" + + # Add user to groups + if [ -n "$groups" ]; then + USER_ID=$(api_call GET "$KEYCLOAK_URL/admin/realms/rhdh/users?username=$username" "" "Get user ID" | \ + jq -r '.[0].id // empty') + [ -z "$USER_ID" ] && echo " Warning: Could not get user ID, skipping groups" && continue + + for group in $(echo "$groups" | tr ',' ' '); do + GROUP_ID=$(api_call GET "$KEYCLOAK_URL/admin/realms/rhdh/groups?search=$group" "" "Get group ID" | \ + jq -r '.[0].id // empty') + [ -z "$GROUP_ID" ] && echo " Warning: Group '$group' not found" && continue + api_call PUT "$KEYCLOAK_URL/admin/realms/rhdh/users/$USER_ID/groups/$GROUP_ID" "" "Add to group" >/dev/null \ + && echo " Added to group: $group" || echo " Warning: Failed to add to group: $group" + done + fi + done +fi + +echo "" +echo "=========================================" +echo "Keycloak deployment complete" +echo "=========================================" +echo "URL: $KEYCLOAK_URL" +echo "Admin: admin/admin123" +echo "Realm: rhdh" + +export KEYCLOAK_CLIENT_SECRET="rhdh-client-secret" +export KEYCLOAK_CLIENT_ID="rhdh-client" +export KEYCLOAK_REALM="rhdh" +export KEYCLOAK_LOGIN_REALM="rhdh" +export KEYCLOAK_METADATA_URL="$KEYCLOAK_URL/realms/rhdh" +export KEYCLOAK_BASE_URL="$KEYCLOAK_URL" diff --git a/utils/keycloak/keycloak-values.yaml b/utils/keycloak/keycloak-values.yaml new file mode 100755 index 0000000..83a17e2 --- /dev/null +++ b/utils/keycloak/keycloak-values.yaml @@ -0,0 +1,104 @@ +global: + security: + allowInsecureImages: true + +replicaCount: 1 + +# Use Bitnami legacy repository (Bitnami images moved to bitnamilegacy as of Aug 2025) +# Note: Legacy images are not updated/maintained. Consider migrating to official Keycloak image for long-term. +image: + registry: docker.io + repository: bitnamilegacy/keycloak + tag: "26.3.3-debian-12-r0" + pullPolicy: IfNotPresent + +auth: + adminUser: admin + adminPassword: admin123 + +service: + type: ClusterIP + port: 8080 + +# OpenShift Route configuration +route: + enabled: true + host: "" # Will be auto-generated by OpenShift + tls: + enabled: false + +ingress: + enabled: false + +postgresql: + enabled: true + image: + registry: docker.io + repository: bitnamilegacy/postgresql + tag: "17.6.0-debian-12-r4" + pullPolicy: IfNotPresent + auth: + postgresPassword: postgres123 + username: keycloak + password: keycloak123 + database: keycloak + primary: + resources: + limits: + cpu: 1000m + memory: 1Gi + requests: + cpu: 100m + memory: 256Mi + persistence: + enabled: true + size: 1Gi + +resources: + limits: + cpu: 1000m + memory: 1Gi + requests: + cpu: 100m + memory: 256Mi + +extraEnvVars: + - name: KEYCLOAK_ADMIN + value: admin + - name: KEYCLOAK_ADMIN_PASSWORD + value: admin123 + - name: KC_HOSTNAME_STRICT + value: "false" + - name: KC_HOSTNAME_STRICT_HTTPS + value: "false" + - name: KC_HTTP_ENABLED + value: "true" + - name: KC_PROXY_HEADERS + value: "xforwarded" + - name: JAVA_OPTS_APPEND + value: "-Djava.net.preferIPv4Stack=true -Xms256m -Xmx512m" + +# Increase probe timeouts for slower startup on resource-constrained clusters +livenessProbe: + enabled: true + initialDelaySeconds: 120 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + successThreshold: 1 + +readinessProbe: + enabled: true + initialDelaySeconds: 60 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + successThreshold: 1 + +# Remove the custom command to use Bitnami defaults +# command: +# - /opt/keycloak/bin/kc.sh +# - start-dev + +# Configuration is now handled by our REST API job in the deployment script +# No keycloakConfigCli needed \ No newline at end of file diff --git a/utils/keycloak/rhdh-client.json b/utils/keycloak/rhdh-client.json new file mode 100755 index 0000000..2f5ed39 --- /dev/null +++ b/utils/keycloak/rhdh-client.json @@ -0,0 +1,86 @@ +{ + "clientId": "rhdh-client", + "name": "RHDH Client", + "description": "", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "rhdh-client-secret", + "redirectUris": [ + "*" + ], + "webOrigins": [ + "*" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": true, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "request.object.signature.alg": "any", + "saml.force.post.binding": "false", + "saml.multivalued.roles": "false", + "oauth2.device.authorization.grant.enabled": "true", + "backchannel.logout.revoke.offline.tokens": "false", + "saml.server.signature.keyinfo.ext": "false", + "use.refresh.tokens": "true", + "realm_client": "false", + "oidc.ciba.grant.enabled": "true", + "backchannel.logout.session.required": "true", + "client_credentials.use_refresh_token": "false", + "require.pushed.authorization.requests": "false", + "saml.client.signature": "false", + "request.object.encryption.enc": "any", + "saml.assertion.signature": "false", + "request.object.encryption.alg": "any", + "client.introspection.response.allow.jwt.claim.enabled": "false", + "saml.encrypt": "false", + "standard.token.exchange.enabled": "true", + "login_theme": "keycloak", + "saml.server.signature": "false", + "exclude.session.state.from.auth.response": "false", + "client.use.lightweight.access.token.enabled": "false", + "request.object.required": "not required", + "access.token.header.type.rfc9068": "false", + "saml_force_name_id_format": "false", + "acr.loa.map": "{}", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "token.response.type.bearer.lower-case": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "service_account", + "web-origins", + "roles", + "profile", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "access": { + "view": true, + "configure": true, + "manage": true + } +} \ No newline at end of file diff --git a/utils/keycloak/users.json b/utils/keycloak/users.json new file mode 100755 index 0000000..9e0e34a --- /dev/null +++ b/utils/keycloak/users.json @@ -0,0 +1,22 @@ +[ + { + "username": "test1", + "enabled": true, + "email": "test1@example.com", + "firstName": "Test", + "lastName": "User1", + "emailVerified": true, + "credentials": [{"type": "password", "value": "test1@123", "temporary": false}], + "groups": ["developers"] + }, + { + "username": "test2", + "enabled": true, + "email": "test2@example.com", + "firstName": "Test", + "lastName": "User2", + "emailVerified": true, + "credentials": [{"type": "password", "value": "test2@123", "temporary": false}], + "groups": ["developers"] + } +] diff --git a/utils/orchestrator/osl-di-rewrite.js b/utils/orchestrator/osl-di-rewrite.js new file mode 100644 index 0000000..62a0077 --- /dev/null +++ b/utils/orchestrator/osl-di-rewrite.js @@ -0,0 +1,104 @@ +const http = require("http"); +const { URL } = require("url"); + +const UPSTREAM = process.env.OSL_DI_UPSTREAM || "http://sonataflow-platform-data-index-service.orchestrator.svc.cluster.local"; +const PORT = Number(process.env.PORT || 8080); + +function originFromEndpoint(endpoint) { + try { + return new URL(endpoint).origin; + } catch { + return null; + } +} + +function rewritePayload(obj) { + const defs = obj && obj.data && obj.data.ProcessDefinitions; + if (!Array.isArray(defs)) return; + for (const def of defs) { + if (!def || !def.endpoint) continue; + const origin = originFromEndpoint(def.endpoint); + if (origin) def.serviceUrl = origin; + } +} + +function augmentQuery(query) { + if (typeof query !== "string") return query; + if (!query.includes("ProcessDefinitions") || !query.includes("serviceUrl")) return query; + if (/\bProcessDefinitions\s*\{[^}]*\bendpoint\b/.test(query)) return query; + return query.replace( + /ProcessDefinitions(\s*\{[^}]*\bserviceUrl\b)/, + "ProcessDefinitions$1 endpoint", + ); +} + +function proxy(req, res) { + const chunks = []; + req.on("data", (c) => chunks.push(c)); + req.on("end", () => { + let body = Buffer.concat(chunks); + const contentType = req.headers["content-type"] || ""; + if (contentType.includes("json") && body.length) { + try { + const parsed = JSON.parse(body.toString("utf8")); + if (parsed && parsed.query) { + parsed.query = augmentQuery(parsed.query); + body = Buffer.from(JSON.stringify(parsed)); + } + } catch (_err) { + /* forward unmodified */ + } + } else if (body.length && contentType.includes("graphql")) { + const q = augmentQuery(body.toString("utf8")); + body = Buffer.from(q); + } + const target = new URL(req.url || "/", UPSTREAM); + if (target.searchParams.has("query")) { + target.searchParams.set("query", augmentQuery(target.searchParams.get("query") || "")); + } + const headers = { ...req.headers, host: target.host }; + delete headers["accept-encoding"]; + headers["content-length"] = Buffer.byteLength(body); + const preq = http.request( + { + protocol: target.protocol, + hostname: target.hostname, + port: target.port || 80, + path: `${target.pathname}${target.search}`, + method: req.method, + headers, + }, + (pres) => { + const out = []; + pres.on("data", (c) => out.push(c)); + pres.on("end", () => { + let buf = Buffer.concat(out); + const ct = pres.headers["content-type"] || ""; + if (ct.includes("json") && buf.length) { + try { + const parsed = JSON.parse(buf.toString("utf8")); + rewritePayload(parsed); + buf = Buffer.from(JSON.stringify(parsed)); + } catch (_err) { + /* forward unmodified */ + } + } + const hdrs = { ...pres.headers, "content-length": Buffer.byteLength(buf) }; + delete hdrs["content-encoding"]; + delete hdrs["transfer-encoding"]; + res.writeHead(pres.statusCode || 502, hdrs); + res.end(buf); + }); + }, + ); + preq.on("error", (err) => { + res.writeHead(502, { "content-type": "text/plain" }); + res.end(String(err)); + }); + preq.end(body.length ? body : undefined); + }); +} + +http.createServer(proxy).listen(PORT, "0.0.0.0", () => { + console.log(`osl-di-rewrite listening on ${PORT} -> ${UPSTREAM}`); +}); diff --git a/utils/orchestrator/osl_smoke.py b/utils/orchestrator/osl_smoke.py deleted file mode 100644 index f37d6ef..0000000 --- a/utils/orchestrator/osl_smoke.py +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env python3 -"""OSL RC smoke helpers: Playwright grep and Data Index serviceUrl contract.""" -from __future__ import annotations - -import argparse -import json -import os -import subprocess -import sys -from typing import Any, Optional - -GRAPHQL_QUERY = "{ ProcessDefinitions { id serviceUrl endpoint } }" - -SMOKE_TITLES = [ - "Run Greeting workflow and verify Workflows tab", - "Run Failswitch workflow and verify statuses", - "Rerun Failswitch from failure point", - "Execute token-propagation workflow via API", -] - - -def playwright_grep() -> str: - return "|".join(SMOKE_TITLES) - - -def is_absolute_http_url(value: Optional[str]) -> bool: - if not value: - return False - return value.startswith("http://") or value.startswith("https://") - - -def classify_definitions(definitions: list) -> dict[str, Any]: - if not definitions: - return { - "ok": False, - "problems": [ - { - "id": None, - "serviceUrl": None, - "endpoint": None, - "reason": "no-process-definitions", - } - ], - } - problems = [] - for item in definitions: - service_url = item.get("serviceUrl") - if not is_absolute_http_url(service_url): - problems.append( - { - "id": item.get("id"), - "serviceUrl": service_url, - "endpoint": item.get("endpoint"), - "reason": "relative-or-missing-serviceUrl", - } - ) - return {"ok": not problems, "problems": problems} - - -def graphql_query() -> str: - return GRAPHQL_QUERY - - -def curl_probe_argv(namespace: str) -> list[str]: - body = json.dumps({"query": graphql_query()}) - url = ( - f"http://sonataflow-platform-data-index-service.{namespace}" - ".svc.cluster.local/graphql" - ) - return [ - "oc", - "exec", - "-n", - namespace, - "deploy/redhat-developer-hub", - "--", - "curl", - "-sS", - "-X", - "POST", - "-H", - "Content-Type: application/json", - "-d", - body, - url, - ] - - -def _cmd_probe(namespace: str, allow_relative: bool) -> int: - argv = curl_probe_argv(namespace) - proc = subprocess.run(argv, capture_output=True, text=True, check=False) - if proc.returncode != 0: - sys.stderr.write(proc.stderr or proc.stdout or "oc exec curl failed\n") - return 1 - try: - payload = json.loads(proc.stdout) - except json.JSONDecodeError: - sys.stderr.write(f"Data Index did not return JSON: {proc.stdout[:500]}\n") - return 1 - if payload.get("errors"): - sys.stderr.write(json.dumps(payload["errors"]) + "\n") - return 1 - definitions = (payload.get("data") or {}).get("ProcessDefinitions") or [] - result = classify_definitions(definitions) - json.dump(result, sys.stderr, indent=2) - sys.stderr.write("\n") - if result["ok"]: - return 0 - if allow_relative: - sys.stderr.write( - "WARNING: relative/missing serviceUrl allowed by ALLOW_RELATIVE_SERVICE_URL\n" - ) - return 0 - if result["problems"] and result["problems"][0]["reason"] == "no-process-definitions": - return 1 - return 2 - - -def _cmd_grep() -> int: - print(playwright_grep()) - return 0 - - -def _cmd_classify() -> int: - payload = json.load(sys.stdin) - definitions = (payload.get("data") or {}).get("ProcessDefinitions") or [] - result = classify_definitions(definitions) - json.dump(result, sys.stdout, indent=2) - sys.stdout.write("\n") - if result["ok"]: - return 0 - if result["problems"] and result["problems"][0]["reason"] == "no-process-definitions": - return 1 - return 2 - - -def main(argv: Optional[list[str]] = None) -> int: - parser = argparse.ArgumentParser(prog="osl_smoke.py") - sub = parser.add_subparsers(dest="cmd", required=True) - sub.add_parser("grep") - sub.add_parser("classify") - probe_p = sub.add_parser("probe") - probe_p.add_argument("--namespace", required=True) - probe_p.add_argument("--allow-relative", action="store_true") - args = parser.parse_args(argv) - if args.cmd == "grep": - return _cmd_grep() - if args.cmd == "probe": - allow = args.allow_relative or os.environ.get("ALLOW_RELATIVE_SERVICE_URL") == "1" - return _cmd_probe(args.namespace, allow) - return _cmd_classify() - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/utils/orchestrator/test_osl_smoke.py b/utils/orchestrator/test_osl_smoke.py deleted file mode 100644 index aaf26a8..0000000 --- a/utils/orchestrator/test_osl_smoke.py +++ /dev/null @@ -1,151 +0,0 @@ -#!/usr/bin/env python3 -import json -import subprocess -import sys -import unittest -from pathlib import Path - -HERE = Path(__file__).resolve().parent -sys.path.insert(0, str(HERE)) - -import osl_smoke # noqa: E402 - - -class TestPlaywrightGrep(unittest.TestCase): - def test_default_titles(self): - self.assertEqual( - osl_smoke.SMOKE_TITLES, - [ - "Run Greeting workflow and verify Workflows tab", - "Run Failswitch workflow and verify statuses", - "Rerun Failswitch from failure point", - "Execute token-propagation workflow via API", - ], - ) - - def test_grep_joins_four_escaped_titles(self): - pattern = osl_smoke.playwright_grep() - self.assertIn("Run Greeting workflow and verify Workflows tab", pattern) - self.assertIn("Run Failswitch workflow and verify statuses", pattern) - self.assertIn("Rerun Failswitch from failure point", pattern) - self.assertIn("Execute token-propagation workflow via API", pattern) - self.assertNotIn("Verify Workflow All Runs", pattern) - - -class TestServiceUrl(unittest.TestCase): - def test_absolute_http(self): - self.assertTrue( - osl_smoke.is_absolute_http_url( - "http://greeting.orchestrator.svc.cluster.local" - ) - ) - - def test_absolute_https(self): - self.assertTrue(osl_smoke.is_absolute_http_url("https://example.example")) - - def test_relative_path(self): - self.assertFalse(osl_smoke.is_absolute_http_url("/greeting")) - - def test_empty_and_none(self): - self.assertFalse(osl_smoke.is_absolute_http_url("")) - self.assertFalse(osl_smoke.is_absolute_http_url(None)) - - -class TestClassifyDefinitions(unittest.TestCase): - def test_all_absolute_ok(self): - result = osl_smoke.classify_definitions( - [ - { - "id": "greeting", - "serviceUrl": "http://greeting.orchestrator.svc", - "endpoint": "http://greeting.orchestrator.svc/greeting", - } - ] - ) - self.assertTrue(result["ok"]) - self.assertEqual(result["problems"], []) - - def test_relative_service_url_is_problem(self): - result = osl_smoke.classify_definitions( - [ - { - "id": "greeting", - "serviceUrl": "/greeting", - "endpoint": "http://greeting.orchestrator.svc/greeting", - } - ] - ) - self.assertFalse(result["ok"]) - self.assertEqual(result["problems"][0]["id"], "greeting") - self.assertEqual(result["problems"][0]["reason"], "relative-or-missing-serviceUrl") - - def test_empty_list_not_ok(self): - result = osl_smoke.classify_definitions([]) - self.assertFalse(result["ok"]) - self.assertEqual(result["problems"][0]["reason"], "no-process-definitions") - - -class TestClassifyCli(unittest.TestCase): - def test_classify_stdin_exit_2_on_relative(self): - payload = json.dumps( - { - "data": { - "ProcessDefinitions": [ - {"id": "greeting", "serviceUrl": "/greeting", "endpoint": "http://x/greeting"} - ] - } - } - ) - proc = subprocess.run( - [sys.executable, str(HERE / "osl_smoke.py"), "classify"], - input=payload, - text=True, - capture_output=True, - check=False, - ) - self.assertEqual(proc.returncode, 2) - - -class TestProbeArgv(unittest.TestCase): - def test_curl_targets_raw_data_index_not_rewrite(self): - argv = osl_smoke.curl_probe_argv("orchestrator") - joined = " ".join(argv) - self.assertIn("sonataflow-platform-data-index-service.orchestrator.svc.cluster.local/graphql", joined) - self.assertNotIn("osl-di-rewrite", joined) - self.assertIn("ProcessDefinitions", joined) - - def test_graphql_query_asks_for_service_url_and_endpoint(self): - q = osl_smoke.graphql_query() - self.assertIn("serviceUrl", q) - self.assertIn("endpoint", q) - self.assertIn("ProcessDefinitions", q) - - -class TestDriverGrepWiring(unittest.TestCase): - def test_run_script_mentions_osl_smoke_grep(self): - text = Path(__file__).resolve().parents[2].joinpath("run-osl-regression.sh").read_text() - self.assertIn("osl_smoke.py", text) - self.assertIn("grep", text) - self.assertIn("--grep", text) - - -class TestTokenSmokeWiring(unittest.TestCase): - def test_driver_always_deploys_token_propagation(self): - text = Path(__file__).resolve().parents[2].joinpath("run-osl-regression.sh").read_text() - self.assertIn("ensure_token_propagation_workflow", text) - self.assertIn("token-propagation", text) - self.assertNotIn("--include-token-propagation", text) - self.assertNotIn("OSL_SMOKE_TOKEN_PROPAGATION", text) - - def test_smoke_wrapper_always_registers_token_tests(self): - text = ( - Path(__file__).resolve().parents[2] - / "playwright" - / "osl-regression-smoke.spec.ts" - ).read_text() - self.assertIn("registerTokenPropagationWorkflowTests", text) - self.assertNotIn("OSL_SMOKE_TOKEN_PROPAGATION", text) - - -if __name__ == "__main__": - unittest.main() diff --git a/utils/orchestrator/verify-existing-rhdh.sh b/utils/orchestrator/verify-existing-rhdh.sh new file mode 100755 index 0000000..9f13c31 --- /dev/null +++ b/utils/orchestrator/verify-existing-rhdh.sh @@ -0,0 +1,98 @@ +#!/bin/bash +# +# Verify that an existing RHDH namespace satisfies the orchestrator substrate +# contract expected by this repository's setup flow. +# + +set -euo pipefail + +namespace="orchestrator" +if [[ $# -gt 0 && "$1" != --* ]]; then + namespace="$1" + shift +fi + +POSTGRES_SECRET="${POSTGRES_SECRET:-backstage-psql-secret}" +POSTGRES_SERVICE="${POSTGRES_SERVICE:-backstage-psql}" +REQUIRE_KEYCLOAK=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --postgres-secret) + POSTGRES_SECRET="$2" + shift 2 + ;; + --postgres-service) + POSTGRES_SERVICE="$2" + shift 2 + ;; + --require-keycloak) + REQUIRE_KEYCLOAK=true + shift + ;; + *) + echo "Error: Unknown option: $1" >&2 + exit 1 + ;; + esac +done + +log() { + echo "==> $*" +} + +require_resource() { + local kind="$1" name="$2" ns="$3" + if ! oc get "$kind" "$name" -n "$ns" >/dev/null 2>&1; then + echo "Error: Missing required ${kind}/${name} in namespace ${ns}" >&2 + exit 1 + fi +} + +require_route() { + local name="$1" ns="$2" + if ! oc get route "$name" -n "$ns" >/dev/null 2>&1; then + echo "Error: Missing required route/${name} in namespace ${ns}" >&2 + exit 1 + fi +} + +resolve_keycloak_route() { + local host + for ns in "$namespace" "rhdh-keycloak"; do + host="$(oc get route keycloak -n "$ns" -o jsonpath='{.spec.host}' 2>/dev/null || true)" + if [[ -n "$host" ]]; then + echo "$host" + return 0 + fi + done + return 1 +} + +main() { + if ! oc whoami >/dev/null 2>&1; then + echo "Error: Cannot connect to OpenShift cluster." >&2 + exit 1 + fi + + require_resource "secret" "$POSTGRES_SECRET" "$namespace" + require_resource "service" "$POSTGRES_SERVICE" "$namespace" + require_resource "deployment" "sonataflow-platform-data-index-service" "$namespace" + require_resource "deployment" "sonataflow-platform-jobs-service" "$namespace" + require_route "redhat-developer-hub" "$namespace" + + if [[ "$REQUIRE_KEYCLOAK" == "true" ]]; then + if [[ -n "${KEYCLOAK_BASE_URL:-}" ]]; then + log "Using KEYCLOAK_BASE_URL from environment." + elif ! resolve_keycloak_route >/dev/null; then + echo "Error: Missing required Keycloak route (checked ${namespace} and rhdh-keycloak)." >&2 + exit 1 + fi + fi + + log "Verified existing-RHDH orchestrator prerequisites in namespace ${namespace}." + log "PostgreSQL secret: ${POSTGRES_SECRET}" + log "PostgreSQL service: ${POSTGRES_SERVICE}" +} + +main "$@" From c86f04329b18f9af3b2206ac7fc00fabcad22fb0 Mon Sep 17 00:00:00 2001 From: Rostislav Lan Date: Thu, 20 Aug 2026 12:14:02 +0200 Subject: [PATCH 07/13] chore: drop agent plans and stop tracking OSL release JSON #13375 Keep only example.json in git; ignore copied CR manifests such as 1.39.0.CR1.json. Co-authored-by: Cursor --- .gitignore | 2 + config/osl-releases/1.39.0.CR1.json | 64 -- ...0-orchestrator-serviceurl-from-endpoint.md | 299 ------- .../plans/2026-08-20-osl-rc-smoke-subset.md | 731 ------------------ ...0-orchestrator-serviceurl-from-endpoint.md | 37 - .../specs/2026-08-20-osl-rc-smoke-subset.md | 69 -- 6 files changed, 2 insertions(+), 1200 deletions(-) delete mode 100644 config/osl-releases/1.39.0.CR1.json delete mode 100644 docs/superpowers/plans/2026-08-20-orchestrator-serviceurl-from-endpoint.md delete mode 100644 docs/superpowers/plans/2026-08-20-osl-rc-smoke-subset.md delete mode 100644 docs/superpowers/specs/2026-08-20-orchestrator-serviceurl-from-endpoint.md delete mode 100644 docs/superpowers/specs/2026-08-20-osl-rc-smoke-subset.md diff --git a/.gitignore b/.gitignore index 9d11e3d..dc36d55 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ .env .env.osl config/image-mirrors.conf +config/osl-releases/*.json +!config/osl-releases/example.json install-rhdh-catalog-source.sh plugin-infra.sh .DS_Store diff --git a/config/osl-releases/1.39.0.CR1.json b/config/osl-releases/1.39.0.CR1.json deleted file mode 100644 index 42de232..0000000 --- a/config/osl-releases/1.39.0.CR1.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "version": "1.39.0.CR1", - "logic_csv": "logic-operator.v1.39.0", - "iib": { - "4.13": "registry-proxy.engineering.redhat.com/rh-osbs/iib:1196310", - "4.14": "registry-proxy.engineering.redhat.com/rh-osbs/iib:1196311", - "4.15": "registry-proxy.engineering.redhat.com/rh-osbs/iib:1196312", - "4.16": "registry-proxy.engineering.redhat.com/rh-osbs/iib:1196313", - "4.17": "registry-proxy.engineering.redhat.com/rh-osbs/iib:1196315", - "4.18": "registry-proxy.engineering.redhat.com/rh-osbs/iib:1196314", - "4.19": "registry-proxy.engineering.redhat.com/rh-osbs/iib:1196319", - "4.20": "registry-proxy.engineering.redhat.com/rh-osbs/iib:1196321", - "4.21": "registry-proxy.engineering.redhat.com/rh-osbs/iib:1196320", - "4.22": "registry-proxy.engineering.redhat.com/rh-osbs/iib:1196317", - "4.23": "registry-proxy.engineering.redhat.com/rh-osbs/iib:1196316", - "5.0": "registry-proxy.engineering.redhat.com/rh-osbs/iib:1196318" - }, - "images": [ - { - "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-rhel9-operator@sha256:9c74bdc7b62309e781790af0041566d606325fb8903a6ab578a23dbcfcc1f26b", - "name": "logic-rhel9-operator" - }, - { - "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-operator-bundle@sha256:43154da5e7fd40d339f41e329475ff54350f082d6307d94e9da49c84917f1a4b", - "name": "logic-operator-bundle" - }, - { - "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-data-index-ephemeral-rhel9@sha256:6b6f43a4df8ebde1f0bbbd164075a585f6d43ea3da23ca5928530b3973e5a53b", - "name": "logic-data-index-ephemeral-rhel9" - }, - { - "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-data-index-postgresql-rhel9@sha256:b3d4e22ddce6acd4c88cbed634b3f6dfeb564f8748d887ea9170e26eb4d99a77", - "name": "logic-data-index-postgresql-rhel9" - }, - { - "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-jobs-service-ephemeral-rhel9@sha256:0eaf021f4af2b9c12f550201344ee68afac50a60c173d84b155eb2adab808c4f", - "name": "logic-jobs-service-ephemeral-rhel9" - }, - { - "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-jobs-service-postgresql-rhel9@sha256:2b11e2aa32f298693e7ce064e79b21becfd9ee52a89865f2fba2f8f0c871e46f", - "name": "logic-jobs-service-postgresql-rhel9" - }, - { - "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-swf-builder-rhel9@sha256:6f9a032f51de85568114797d98b9270cd48de4437365c05473605acc4e74160e", - "name": "logic-swf-builder-rhel9" - }, - { - "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-swf-devmode-rhel9@sha256:158469391dec4e3473391a1ad62c489d92fbaa48ee0830caec6eabbbcca28243", - "name": "logic-swf-devmode-rhel9" - }, - { - "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-management-console-rhel9@sha256:2a3849e7030e97629d23bdbb5105a7ad206fc768103ddb9dfc9d4b37c6e1c73a", - "name": "logic-management-console-rhel9" - }, - { - "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-db-migrator-tool-rhel9@sha256:7c013f0ad1c5d441c771d4cacdf34be5a23c0743203daf8303f40a29d45d8c6d", - "name": "logic-db-migrator-tool-rhel9" - }, - { - "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-kn-workflow-cli-artifacts-rhel9@sha256:ca7752e07bd37d8e58953f904d637dc036e9880e1ae3ff82c3cc45c8478d0754", - "name": "logic-kn-workflow-cli-artifacts-rhel9" - } - ] -} diff --git a/docs/superpowers/plans/2026-08-20-orchestrator-serviceurl-from-endpoint.md b/docs/superpowers/plans/2026-08-20-orchestrator-serviceurl-from-endpoint.md deleted file mode 100644 index 598bca1..0000000 --- a/docs/superpowers/plans/2026-08-20-orchestrator-serviceurl-from-endpoint.md +++ /dev/null @@ -1,299 +0,0 @@ -# Orchestrator serviceUrl-from-endpoint Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Make the Orchestrator backend treat OSL 1.39 relative Data Index `serviceUrl` as the origin of `endpoint`, so execute/abort/retrigger work without `osl-di-rewrite`. - -**Architecture:** Add a pure helper `resolveWorkflowServiceUrl`, unit-test it, then apply it to every `ProcessDefinitions` mapping in `DataIndexService`. Do not change the execute URL shape (`${origin}/${id}`). - -**Tech Stack:** TypeScript, Jest via `yarn test` in `rhdh-plugins/workspaces/orchestrator`. - -**Spec:** `docs/superpowers/specs/2026-08-20-orchestrator-serviceurl-from-endpoint.md` (this worktree copy). Implement in **`rhdh-plugins`**, not in `rhdh-test-instance`. - -## Global Constraints - -- Implementation repo: `rhdh-plugins`, workspace `workspaces/orchestrator`. Create a new branch from that repo’s default (do not commit plugin code into `rhdh-test-instance`). -- Do not mix this PR with the OSL smoke-driver PR. -- Keep absolute `http://` / `https://` `serviceUrl` values unchanged (OSL ≤ 1.38). -- Do not POST to the full `endpoint` path; only copy `.origin`. -- `fetchWorkflowServiceUrls` currently queries `{ id, serviceUrl }` only — it **must** also fetch `endpoint`. - ---- - -## File map - -| File | Responsibility | -|---|---| -| `plugins/orchestrator-backend/src/service/workflowServiceUrl.ts` | `isAbsoluteHttpUrl`, `resolveWorkflowServiceUrl` | -| `plugins/orchestrator-backend/src/service/workflowServiceUrl.test.ts` | Jest cases for relative / absolute / bad endpoint | -| `plugins/orchestrator-backend/src/service/DataIndexService.ts` | Apply helper on definition reads; add `endpoint` to `fetchWorkflowServiceUrls` query | -| `plugins/orchestrator-backend/src/service/DataIndexService.test.ts` | Assert mapping when GraphQL returns relative `serviceUrl` | - -Paths are relative to `/home/rlan/redhat/rhdh-plugins/workspaces/orchestrator`. - ---- - -### Task 1: Helper + unit tests - -**Files:** -- Create: `plugins/orchestrator-backend/src/service/workflowServiceUrl.ts` -- Create: `plugins/orchestrator-backend/src/service/workflowServiceUrl.test.ts` - -**Interfaces:** -- Consumes: none -- Produces: - - `isAbsoluteHttpUrl(value?: string): boolean` - - `resolveWorkflowServiceUrl(info: { serviceUrl?: string; endpoint?: string }): string | undefined` - -- [ ] **Step 1: Confirm branch in rhdh-plugins** - -```bash -git -C /home/rlan/redhat/rhdh-plugins branch --show-current -git -C /home/rlan/redhat/rhdh-plugins status -sb -``` - -If the tree is dirty or the branch is not a new feature branch, create one: - -```bash -git -C /home/rlan/redhat/rhdh-plugins fetch origin -git -C /home/rlan/redhat/rhdh-plugins switch -c fix/orchestrator-serviceurl-from-endpoint origin/main -``` - -(Use the actual default remote/branch if it is not `origin/main`.) - -- [ ] **Step 2: Write the failing test** - -Create `plugins/orchestrator-backend/src/service/workflowServiceUrl.test.ts`: - -```typescript -import { - isAbsoluteHttpUrl, - resolveWorkflowServiceUrl, -} from './workflowServiceUrl'; - -describe('isAbsoluteHttpUrl', () => { - it('accepts http and https', () => { - expect(isAbsoluteHttpUrl('http://greeting.ns.svc')).toBe(true); - expect(isAbsoluteHttpUrl('https://greeting.example')).toBe(true); - }); - - it('rejects relative, empty, and non-http', () => { - expect(isAbsoluteHttpUrl('/greeting')).toBe(false); - expect(isAbsoluteHttpUrl('greeting.ns.svc')).toBe(false); - expect(isAbsoluteHttpUrl('')).toBe(false); - expect(isAbsoluteHttpUrl(undefined)).toBe(false); - }); -}); - -describe('resolveWorkflowServiceUrl', () => { - it('keeps an already-absolute serviceUrl', () => { - expect( - resolveWorkflowServiceUrl({ - serviceUrl: 'http://greeting.orchestrator.svc', - endpoint: 'http://other.svc/greeting/1.0.0', - }), - ).toBe('http://greeting.orchestrator.svc'); - }); - - it('uses endpoint origin when serviceUrl is relative (SRVLOGIC-1137)', () => { - expect( - resolveWorkflowServiceUrl({ - serviceUrl: '/greeting', - endpoint: 'http://greeting.orchestrator.svc.cluster.local/greeting/1.0.0', - }), - ).toBe('http://greeting.orchestrator.svc.cluster.local'); - }); - - it('uses endpoint origin when serviceUrl is missing', () => { - expect( - resolveWorkflowServiceUrl({ - endpoint: 'http://failswitch.orchestrator.svc/failswitch', - }), - ).toBe('http://failswitch.orchestrator.svc'); - }); - - it('returns undefined when neither field is a usable URL', () => { - expect(resolveWorkflowServiceUrl({ serviceUrl: '/greeting' })).toBeUndefined(); - expect(resolveWorkflowServiceUrl({})).toBeUndefined(); - }); -}); -``` - -- [ ] **Step 3: Run test to verify it fails** - -```bash -cd /home/rlan/redhat/rhdh-plugins/workspaces/orchestrator -yarn test plugins/orchestrator-backend --testPathPattern=workflowServiceUrl.test --coverage=false -``` - -Expected: FAIL (cannot resolve `./workflowServiceUrl`). - -- [ ] **Step 4: Write the helper** - -Create `plugins/orchestrator-backend/src/service/workflowServiceUrl.ts`: - -```typescript -export function isAbsoluteHttpUrl(value?: string): boolean { - if (!value) { - return false; - } - return value.startsWith('http://') || value.startsWith('https://'); -} - -export function resolveWorkflowServiceUrl(info: { - serviceUrl?: string; - endpoint?: string; -}): string | undefined { - if (isAbsoluteHttpUrl(info.serviceUrl)) { - return info.serviceUrl; - } - if (!info.endpoint) { - return undefined; - } - try { - return new URL(info.endpoint).origin; - } catch { - return undefined; - } -} -``` - -- [ ] **Step 5: Run tests to verify they pass** - -```bash -cd /home/rlan/redhat/rhdh-plugins/workspaces/orchestrator -yarn test plugins/orchestrator-backend --testPathPattern=workflowServiceUrl.test --coverage=false -``` - -Expected: PASS. - -- [ ] **Step 6: Commit in rhdh-plugins** - -```bash -cd /home/rlan/redhat/rhdh-plugins -git add workspaces/orchestrator/plugins/orchestrator-backend/src/service/workflowServiceUrl.ts \ - workspaces/orchestrator/plugins/orchestrator-backend/src/service/workflowServiceUrl.test.ts -git commit -m "$(cat <<'EOF' -feat: add workflow serviceUrl origin helper for OSL 1.39 Data Index - -EOF -)" -``` - ---- - -### Task 2: Apply the helper in DataIndexService - -**Files:** -- Modify: `plugins/orchestrator-backend/src/service/DataIndexService.ts` -- Modify: `plugins/orchestrator-backend/src/service/DataIndexService.test.ts` - -**Interfaces:** -- Consumes: `resolveWorkflowServiceUrl` from Task 1 -- Produces: every returned `WorkflowInfo` (and `fetchWorkflowServiceUrls` map values) has an absolute `serviceUrl` when `endpoint` is absolute - -- [ ] **Step 1: Write a failing DataIndexService test** - -In `DataIndexService.test.ts`, add a `describe('relative serviceUrl from OSL 1.39')` that mocks `client.query` for `fetchWorkflowInfos` (no definitionIds/filter) returning: - -```javascript -{ - data: { - ProcessDefinitions: [ - { - id: 'greeting', - name: 'Greeting', - serviceUrl: '/greeting', - endpoint: 'http://greeting.orchestrator.svc/greeting', - metadata: {}, - }, - ], - }, - error: undefined, -} -``` - -Assert `infos[0].serviceUrl === 'http://greeting.orchestrator.svc'`. - -Follow the existing `fetchWorkflowInfos` mock style in that file (`mockClient.query`, `Client` mock, `loggerMock`). Keep `filterDeletedWorkflows` behavior: `metadata.status === 'unavailable'` still dropped. - -Add a second test for `fetchWorkflowServiceUrls`: mock GraphQL data with relative `serviceUrl` + absolute `endpoint`; expect the returned map `{ greeting: 'http://greeting.orchestrator.svc' }`. This test **must fail** until the query string includes `endpoint`. - -- [ ] **Step 2: Run the new tests to verify fail** - -```bash -cd /home/rlan/redhat/rhdh-plugins/workspaces/orchestrator -yarn test plugins/orchestrator-backend --testPathPattern=DataIndexService.test --coverage=false -``` - -Expected: FAIL — `serviceUrl` still `'/greeting'`. - -- [ ] **Step 3: Implement mapping** - -At top of `DataIndexService.ts`: - -```typescript -import { resolveWorkflowServiceUrl } from './workflowServiceUrl'; -``` - -Add a private method: - -```typescript -private withResolvedServiceUrl(info: WorkflowInfo): WorkflowInfo { - return { - ...info, - serviceUrl: resolveWorkflowServiceUrl(info), - }; -} -``` - -Apply it: - -- `fetchWorkflowInfo`: `return this.withResolvedServiceUrl(processDefinitions[0]);` -- `fetchWorkflowInfos`: `return this.filterDeletedWorkflows(...).map(w => this.withResolvedServiceUrl(w));` -- `fetchWorkflowServiceUrls`: change query to `{ ProcessDefinitions { id, serviceUrl, endpoint } }`, then: - -```typescript -return processDefinitions - .map(definition => this.withResolvedServiceUrl(definition)) - .filter(definition => definition.serviceUrl) - .map(definition => ({ [definition.id]: definition.serviceUrl! })) - .reduce((acc, curr) => ({ ...acc, ...curr }), {}); -``` - -Do not change execute/abort URL builders in `SonataFlowService.ts`; they already use the resolved `serviceUrl`. - -- [ ] **Step 4: Run DataIndexService + helper tests** - -```bash -cd /home/rlan/redhat/rhdh-plugins/workspaces/orchestrator -yarn test plugins/orchestrator-backend --testPathPattern='workflowServiceUrl.test|DataIndexService.test' --coverage=false -``` - -Expected: PASS. - -- [ ] **Step 5: Commit in rhdh-plugins** - -```bash -git add workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts \ - workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.test.ts -git commit -m "$(cat <<'EOF' -fix: derive Data Index serviceUrl origin from endpoint - -EOF -)" -``` - ---- - -### Task 3: Do not remove the test-instance rewrite in this PR - -No code. After the plugin is in the RHDH `next` catalog image the smoke uses, a **later** `rhdh-test-instance` change can skip `ensure_dataindex_rewrite` and drop `--allow-relative-service-url`. Mixing that into this plugin PR will break smoke until the image exists. - ---- - -## Self-review - -1. **Spec coverage:** helper + mapping + `fetchWorkflowServiceUrls` query includes `endpoint` → Tasks 1–2. Rewrite removal → Task 3 (explicitly deferred). Execute still uses origin, not versioned endpoint → Task 2 note. -2. **Placeholders:** none. -3. **Names:** `resolveWorkflowServiceUrl` / `withResolvedServiceUrl` used consistently. diff --git a/docs/superpowers/plans/2026-08-20-osl-rc-smoke-subset.md b/docs/superpowers/plans/2026-08-20-osl-rc-smoke-subset.md deleted file mode 100644 index 2275dbe..0000000 --- a/docs/superpowers/plans/2026-08-20-osl-rc-smoke-subset.md +++ /dev/null @@ -1,731 +0,0 @@ -# OSL RC smoke subset Implementation Plan - -> **Implementation note (2026-08-20):** Do not add Python helpers. The approved architecture is the earlier **Lean OSL smoke bash** plan: `run-osl-regression.sh` only, GraphQL classification with `jq`, Playwright `--grep` as a bash constant. The task bodies below that mention `osl_smoke.py` / `test_osl_smoke.py` are obsolete. - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Default OSL RC `--test` probes raw Data Index GraphQL, then runs four Playwright tests (Greeting + Failswitch statuses + retrigger + token-propagation). - -**Architecture:** Keep the existing bash driver. It always deploys greeting, failswitch, and token-propagation, probes raw Data Index with `oc exec` + `jq`, then Playwright `-g` for the four titles. Do not change overlays git files; keep copying `playwright/osl-regression-smoke.spec.ts` at runtime. - -**Tech Stack:** bash, jq, oc, Playwright (overlays e2e-tests), existing smoke wrapper. - -**Spec:** `docs/superpowers/specs/2026-08-20-osl-rc-smoke-subset.md` - -## Global Constraints - -- Repo: `rhdh-test-instance` only, branch `feat/rhidp-13375-osl-smoke`, worktree `/home/rlan/redhat/rhdh-test-instance/.worktrees/rhidp-13375-osl-smoke`. -- Do not edit `rhdh-plugin-export-overlays`, `rhdh-plugins`, or `rhdh-e2e-test-utils`. -- Do not commit `.env`, `.env.osl`, or cluster credentials. -- Driver is bash. Classify GraphQL with `jq`. Do not add Python helper modules. -- Default Playwright titles (exact): `Run Greeting workflow and verify Workflows tab`, `Run Failswitch workflow and verify statuses`, `Rerun Failswitch from failure point`, `Execute token-propagation workflow via API`. -- Probe the raw Data Index service, never `osl-di-rewrite`. -- Driver `--cleanup` always includes operators/catalog/mirror. No `--full-e2e` flag. -- Commit messages: conventional commits, include `#13375`. -- `export PATH="/home/rlan/bin:$HOME/.local/bin:$PATH"` before any `oc` command. - ---- - -## File map - -| File | Responsibility | -|---|---| -| `run-osl-regression.sh` | `--allow-relative-service-url`, always deploy token-propagation on smoke, probe raw Data Index with `jq`, pass `--grep` | -| `playwright/osl-regression-smoke.spec.ts` | Always register token-propagation tests | -| `README.md` | Default 4-test smoke, probe, `--allow-relative-service-url` | -| `Makefile` | Pass-through `ALLOW_RELATIVE_SERVICE_URL=1` | - -Do **not** implement Orchestrator plugin `serviceUrl` derivation here. That is `docs/superpowers/plans/2026-08-20-orchestrator-serviceurl-from-endpoint.md`. - ---- - -### Task 1: Python smoke helper + unit tests - -**Files:** -- Create: `utils/orchestrator/osl_smoke.py` -- Create: `utils/orchestrator/test_osl_smoke.py` - -**Interfaces:** -- Consumes: none -- Produces: - - `SMOKE_TITLES: list[str]` (four titles, including token-propagation) - - `playwright_grep() -> str` - - `is_absolute_http_url(value: str | None) -> bool` - - `classify_definitions(definitions: list) -> dict` with keys `ok` (bool), `problems` (list of `{id, serviceUrl, endpoint, reason}`) - - CLI: `python utils/orchestrator/osl_smoke.py grep` prints the regex to stdout - - CLI: `python utils/orchestrator/osl_smoke.py classify` reads GraphQL JSON on stdin, exit 0/2 - -- [ ] **Step 1: Write the failing tests** - -Create `utils/orchestrator/test_osl_smoke.py`: - -```python -#!/usr/bin/env python3 -import json -import subprocess -import sys -import unittest -from pathlib import Path - -HERE = Path(__file__).resolve().parent -sys.path.insert(0, str(HERE)) - -import osl_smoke # noqa: E402 - - -class TestPlaywrightGrep(unittest.TestCase): - def test_default_titles(self): - self.assertEqual( - osl_smoke.SMOKE_TITLES, - [ - "Run Greeting workflow and verify Workflows tab", - "Run Failswitch workflow and verify statuses", - "Rerun Failswitch from failure point", - "Execute token-propagation workflow via API", - ], - ) - - def test_grep_joins_four_escaped_titles(self): - pattern = osl_smoke.playwright_grep() - self.assertIn("Run Greeting workflow and verify Workflows tab", pattern) - self.assertIn("Run Failswitch workflow and verify statuses", pattern) - self.assertIn("Rerun Failswitch from failure point", pattern) - self.assertIn("Execute token-propagation workflow via API", pattern) - self.assertNotIn("Verify Workflow All Runs", pattern) - - -class TestServiceUrl(unittest.TestCase): - def test_absolute_http(self): - self.assertTrue( - osl_smoke.is_absolute_http_url( - "http://greeting.orchestrator.svc.cluster.local" - ) - ) - - def test_absolute_https(self): - self.assertTrue(osl_smoke.is_absolute_http_url("https://example.example")) - - def test_relative_path(self): - self.assertFalse(osl_smoke.is_absolute_http_url("/greeting")) - - def test_empty_and_none(self): - self.assertFalse(osl_smoke.is_absolute_http_url("")) - self.assertFalse(osl_smoke.is_absolute_http_url(None)) - - -class TestClassifyDefinitions(unittest.TestCase): - def test_all_absolute_ok(self): - result = osl_smoke.classify_definitions( - [ - { - "id": "greeting", - "serviceUrl": "http://greeting.orchestrator.svc", - "endpoint": "http://greeting.orchestrator.svc/greeting", - } - ] - ) - self.assertTrue(result["ok"]) - self.assertEqual(result["problems"], []) - - def test_relative_service_url_is_problem(self): - result = osl_smoke.classify_definitions( - [ - { - "id": "greeting", - "serviceUrl": "/greeting", - "endpoint": "http://greeting.orchestrator.svc/greeting", - } - ] - ) - self.assertFalse(result["ok"]) - self.assertEqual(result["problems"][0]["id"], "greeting") - self.assertEqual(result["problems"][0]["reason"], "relative-or-missing-serviceUrl") - - def test_empty_list_not_ok(self): - result = osl_smoke.classify_definitions([]) - self.assertFalse(result["ok"]) - self.assertEqual(result["problems"][0]["reason"], "no-process-definitions") - - -class TestClassifyCli(unittest.TestCase): - def test_classify_stdin_exit_2_on_relative(self): - payload = json.dumps( - { - "data": { - "ProcessDefinitions": [ - {"id": "greeting", "serviceUrl": "/greeting", "endpoint": "http://x/greeting"} - ] - } - } - ) - proc = subprocess.run( - [sys.executable, str(HERE / "osl_smoke.py"), "classify"], - input=payload, - text=True, - capture_output=True, - check=False, - ) - self.assertEqual(proc.returncode, 2) - - -if __name__ == "__main__": - unittest.main() -``` - -- [ ] **Step 2: Run tests to verify they fail** - -```bash -export PATH="/home/rlan/bin:$HOME/.local/bin:$PATH" -cd /home/rlan/redhat/rhdh-test-instance/.worktrees/rhidp-13375-osl-smoke -python3 utils/orchestrator/test_osl_smoke.py -``` - -Expected: FAIL with `ModuleNotFoundError: No module named 'osl_smoke'` or import error. - -- [ ] **Step 3: Write minimal implementation** - -Create `utils/orchestrator/osl_smoke.py`: - -```python -#!/usr/bin/env python3 -"""OSL RC smoke helpers: Playwright grep and Data Index serviceUrl contract.""" -from __future__ import annotations - -import argparse -import json -import re -import sys -from typing import Any, Optional - -SMOKE_TITLES = [ - "Run Greeting workflow and verify Workflows tab", - "Run Failswitch workflow and verify statuses", - "Rerun Failswitch from failure point", - "Execute token-propagation workflow via API", -] - - -def playwright_grep() -> str: - return "|".join(re.escape(t) for t in SMOKE_TITLES) - - -def is_absolute_http_url(value: Optional[str]) -> bool: - if not value: - return False - return value.startswith("http://") or value.startswith("https://") - - -def classify_definitions(definitions: list) -> dict[str, Any]: - if not definitions: - return { - "ok": False, - "problems": [ - { - "id": None, - "serviceUrl": None, - "endpoint": None, - "reason": "no-process-definitions", - } - ], - } - problems = [] - for item in definitions: - service_url = item.get("serviceUrl") - if not is_absolute_http_url(service_url): - problems.append( - { - "id": item.get("id"), - "serviceUrl": service_url, - "endpoint": item.get("endpoint"), - "reason": "relative-or-missing-serviceUrl", - } - ) - return {"ok": not problems, "problems": problems} - - -def _cmd_grep() -> int: - print(playwright_grep()) - return 0 - - -def _cmd_classify() -> int: - payload = json.load(sys.stdin) - definitions = (payload.get("data") or {}).get("ProcessDefinitions") or [] - result = classify_definitions(definitions) - json.dump(result, sys.stdout, indent=2) - sys.stdout.write("\n") - if result["ok"]: - return 0 - if result["problems"] and result["problems"][0]["reason"] == "no-process-definitions": - return 1 - return 2 - - -def main(argv: Optional[list[str]] = None) -> int: - parser = argparse.ArgumentParser(prog="osl_smoke.py") - sub = parser.add_subparsers(dest="cmd", required=True) - sub.add_parser("grep") - sub.add_parser("classify") - args = parser.parse_args(argv) - if args.cmd == "grep": - return _cmd_grep() - return _cmd_classify() - - -if __name__ == "__main__": - sys.exit(main()) -``` - -- [ ] **Step 4: Run tests to verify they pass** - -```bash -cd /home/rlan/redhat/rhdh-test-instance/.worktrees/rhidp-13375-osl-smoke -python3 utils/orchestrator/test_osl_smoke.py -``` - -Expected: PASS (all tests). - -- [ ] **Step 5: Commit** - -```bash -git add utils/orchestrator/osl_smoke.py utils/orchestrator/test_osl_smoke.py \ - docs/superpowers/specs/2026-08-20-osl-rc-smoke-subset.md \ - docs/superpowers/plans/2026-08-20-osl-rc-smoke-subset.md -git commit -m "$(cat <<'EOF' -test: add OSL smoke grep and serviceUrl classifiers #13375 - -EOF -)" -``` - ---- - -### Task 2: Probe raw Data Index from the cluster - -**Files:** -- Modify: `utils/orchestrator/osl_smoke.py` (add `probe` subcommand) -- Modify: `utils/orchestrator/test_osl_smoke.py` (build curl argv; no live cluster) -- Modify: `run-osl-regression.sh` (`phase_test` after `ensure_smoke_workflows`) - -**Interfaces:** -- Consumes: `classify_definitions` from Task 1 -- Produces: - - `graphql_query() -> str` returning `{ ProcessDefinitions { id serviceUrl endpoint } }` - - `curl_probe_argv(namespace: str) -> list[str]` for `oc exec` - - CLI `probe --namespace ` runs oc, pipes JSON to classify, honors `ALLOW_RELATIVE_SERVICE_URL=1` - -- [ ] **Step 1: Write the failing tests** - -Append to `utils/orchestrator/test_osl_smoke.py`: - -```python -class TestProbeArgv(unittest.TestCase): - def test_curl_targets_raw_data_index_not_rewrite(self): - argv = osl_smoke.curl_probe_argv("orchestrator") - joined = " ".join(argv) - self.assertIn("sonataflow-platform-data-index-service.orchestrator.svc.cluster.local/graphql", joined) - self.assertNotIn("osl-di-rewrite", joined) - self.assertIn("ProcessDefinitions", joined) - - def test_graphql_query_asks_for_service_url_and_endpoint(self): - q = osl_smoke.graphql_query() - self.assertIn("serviceUrl", q) - self.assertIn("endpoint", q) - self.assertIn("ProcessDefinitions", q) -``` - -- [ ] **Step 2: Run the new tests to verify they fail** - -```bash -python3 utils/orchestrator/test_osl_smoke.py TestProbeArgv -v -``` - -Expected: FAIL with `AttributeError: module 'osl_smoke' has no attribute 'curl_probe_argv'`. - -- [ ] **Step 3: Implement probe helpers and CLI** - -Add to `osl_smoke.py` (keep existing functions). `curl_probe_argv` must be a list `oc` can consume: - -```python -GRAPHQL_QUERY = "{ ProcessDefinitions { id serviceUrl endpoint } }" - - -def graphql_query() -> str: - return GRAPHQL_QUERY - - -def curl_probe_argv(namespace: str) -> list[str]: - body = json.dumps({"query": graphql_query()}) - url = ( - f"http://sonataflow-platform-data-index-service.{namespace}" - ".svc.cluster.local/graphql" - ) - return [ - "oc", - "exec", - "-n", - namespace, - "deploy/redhat-developer-hub", - "--", - "curl", - "-sS", - "-X", - "POST", - "-H", - "Content-Type: application/json", - "-d", - body, - url, - ] -``` - -Add `probe` subparser: - -```python -def _cmd_probe(namespace: str, allow_relative: bool) -> int: - import subprocess - - argv = curl_probe_argv(namespace) - proc = subprocess.run(argv, capture_output=True, text=True, check=False) - if proc.returncode != 0: - sys.stderr.write(proc.stderr or proc.stdout or "oc exec curl failed\n") - return 1 - try: - payload = json.loads(proc.stdout) - except json.JSONDecodeError: - sys.stderr.write(f"Data Index did not return JSON: {proc.stdout[:500]}\n") - return 1 - if payload.get("errors"): - sys.stderr.write(json.dumps(payload["errors"]) + "\n") - return 1 - definitions = (payload.get("data") or {}).get("ProcessDefinitions") or [] - result = classify_definitions(definitions) - json.dump(result, sys.stderr, indent=2) - sys.stderr.write("\n") - if result["ok"]: - return 0 - if allow_relative: - sys.stderr.write( - "WARNING: relative/missing serviceUrl allowed by ALLOW_RELATIVE_SERVICE_URL\n" - ) - return 0 - if result["problems"] and result["problems"][0]["reason"] == "no-process-definitions": - return 1 - return 2 -``` - -Wire argparse: `probe --namespace` required; `--allow-relative` flag **or** env `ALLOW_RELATIVE_SERVICE_URL=1`. - -- [ ] **Step 4: Run unit tests** - -```bash -python3 utils/orchestrator/test_osl_smoke.py -``` - -Expected: PASS. - -- [ ] **Step 5: Call probe from `phase_test`** - -In `run-osl-regression.sh`, add a `run_all=false` style flag: - -```bash -allow_relative_service_url=false -``` - -Parse: - -```bash ---allow-relative-service-url) allow_relative_service_url=true; shift ;; -``` - -After `ensure_smoke_workflows` (smoke path only, not `--full-e2e`), before Playwright: - -```bash -probe_args=(python3 "${SCRIPT_DIR}/utils/orchestrator/osl_smoke.py" probe --namespace "$namespace") -if [[ "$allow_relative_service_url" == "true" || "${ALLOW_RELATIVE_SERVICE_URL:-}" == "1" ]]; then - probe_args+=(--allow-relative) -fi -log "probing raw Data Index GraphQL ProcessDefinitions.serviceUrl" -"${probe_args[@]}" -``` - -Also document in `usage()`. - -- [ ] **Step 6: Commit** - -```bash -git add utils/orchestrator/osl_smoke.py utils/orchestrator/test_osl_smoke.py run-osl-regression.sh -git commit -m "$(cat <<'EOF' -feat: probe raw Data Index serviceUrl before OSL Playwright #13375 - -EOF -)" -``` - ---- - -### Task 3: Default Playwright grep to the four OSL tests - -**Files:** -- Modify: `run-osl-regression.sh` (`phase_test` Playwright invocation) -- Modify: `README.md` OSL RC smoke section - -**Interfaces:** -- Consumes: `osl_smoke.py grep` from Task 1 -- Produces: default `--test` runs four titles; `--full-e2e` unchanged - -- [ ] **Step 1: Write a failing driver assertion (script check)** - -Add to `utils/orchestrator/test_osl_smoke.py`: - -```python -class TestDriverGrepWiring(unittest.TestCase): - def test_run_script_mentions_osl_smoke_grep(self): - text = Path(__file__).resolve().parents[2].joinpath("run-osl-regression.sh").read_text() - self.assertIn("osl_smoke.py", text) - self.assertIn("grep", text) - self.assertIn("--grep", text) -``` - -Playwright CLI flag is `-g` / `--grep`. The driver must pass `--grep "$(python3 ... grep)"`. - -- [ ] **Step 2: Run the wiring test to see it fail** - -```bash -python3 utils/orchestrator/test_osl_smoke.py TestDriverGrepWiring -v -``` - -Expected: FAIL (`--grep` not in `run-osl-regression.sh`). - -- [ ] **Step 3: Change the smoke Playwright invocation** - -In `phase_test`, replace the smoke branch: - -```bash - else - smoke_grep="$(python3 "${SCRIPT_DIR}/utils/orchestrator/osl_smoke.py" grep)" - log "Playwright grep: ${smoke_grep}" - # shellcheck disable=SC2086 - (cd "$e2e" && $pw test --project=orchestrator --workers=1 --grep "$smoke_grep" "$smoke_spec") - fi -``` - -Leave the `--full-e2e` branch **without** `--grep`. - -- [ ] **Step 4: Run unit tests** - -```bash -python3 utils/orchestrator/test_osl_smoke.py -``` - -Expected: PASS, including `TestDriverGrepWiring`. - -- [ ] **Step 5: Update README** - -Replace the OSL RC smoke paragraph in `README.md` so it states: - -- Default smoke is four tests (list the titles, including token-propagation). -- Token-propagation workflow + sample-server are always deployed on the smoke path. -- A GraphQL probe runs first against raw Data Index. -- `--allow-relative-service-url` continues after SRVLOGIC-1137-class relative `serviceUrl` (needed on 1.39.CR1 until the plugin fix ships). -- `--full-e2e` is the RHDH plugin suite (RBAC, entity, ui:props, Loki, all workflows), not the OSL CR default. - -Example block: - -```bash -./run-osl-regression.sh --all --rhdh next --osl-release 1.39.0.CR1 --namespace orchestrator -# 1.39.CR1 currently needs the DI contract override plus rewrite proxy: -ALLOW_RELATIVE_SERVICE_URL=1 ./run-osl-regression.sh --test --namespace orchestrator -./run-osl-regression.sh --test --full-e2e --namespace orchestrator -``` - -- [ ] **Step 6: Commit** - -```bash -git add run-osl-regression.sh README.md utils/orchestrator/test_osl_smoke.py -git commit -m "$(cat <<'EOF' -feat: limit OSL RC Playwright to greeting, failswitch, retrigger, token-propagation #13375 - -EOF -)" -``` - ---- - -### Task 4: Always deploy and register token-propagation on smoke - -**Files:** -- Modify: `run-osl-regression.sh` (`ensure_token_propagation_workflow`, call it from `ensure_smoke_workflows`, wait for the deployment) -- Modify: `playwright/osl-regression-smoke.spec.ts` -- Modify: `README.md` (if Task 3 copy still called token-propagation optional) -- Modify: `utils/orchestrator/test_osl_smoke.py` (driver and wrapper wiring) - -**Interfaces:** -- Consumes: overlays token test module (read-only at runtime); Keycloak env already exported in `phase_test` -- Produces: every smoke `--test` deploys sample-server + token-propagation, registers `Execute token-propagation workflow via API`, waits Ready before the GraphQL probe - -- [ ] **Step 1: Write failing wiring tests** - -Append: - -```python -class TestTokenSmokeWiring(unittest.TestCase): - def test_driver_always_deploys_token_propagation(self): - text = Path(__file__).resolve().parents[2].joinpath("run-osl-regression.sh").read_text() - self.assertIn("ensure_token_propagation_workflow", text) - self.assertIn("token-propagation", text) - self.assertNotIn("--include-token-propagation", text) - self.assertNotIn("OSL_SMOKE_TOKEN_PROPAGATION", text) - - def test_smoke_wrapper_always_registers_token_tests(self): - text = ( - Path(__file__).resolve().parents[2] - / "playwright" - / "osl-regression-smoke.spec.ts" - ).read_text() - self.assertIn("registerTokenPropagationWorkflowTests", text) - self.assertNotIn("OSL_SMOKE_TOKEN_PROPAGATION", text) -``` - -- [ ] **Step 2: Run to verify fail** - -```bash -python3 utils/orchestrator/test_osl_smoke.py TestTokenSmokeWiring -v -``` - -Expected: FAIL (deploy function / import missing). - -- [ ] **Step 3: Patch the smoke wrapper** - -Add imports at the top of `playwright/osl-regression-smoke.spec.ts` with the other imports: - -```typescript -import { registerTokenPropagationWorkflowTests } from "./specs/orchestrator-token-propagation.tests.js"; -import { requireEnvVar } from "./support/utils/orchestrator-workflow-helpers.js"; -``` - -After `registerOrchestratorCoreWorkflowTests(ensureDataIndexOrSkip);` always call: - -```typescript -registerTokenPropagationWorkflowTests(requireEnvVar); -``` - -Do not gate this on an env var. - -- [ ] **Step 4: Always deploy token-propagation from `ensure_smoke_workflows`** - -Add `ensure_token_propagation_workflow` modeled on overlays `deployTokenPropagationWorkflow` in `rhdh-plugin-export-overlays/workspaces/orchestrator/e2e-tests/tests/support/utils/workflow-deployment-helpers.ts` (function starts ~line 460). Required behavior: - -1. Require `KEYCLOAK_BASE_URL` (already exported in `phase_test` before `ensure_smoke_workflows`). If `ensure_smoke_workflows` runs before Keycloak env is set, set `KEYCLOAK_BASE_URL` first — `phase_test` already does this before `ensure_dataindex_rewrite`; move `ensure_smoke_workflows` so it runs **after** `KEYCLOAK_BASE_URL` is exported (it already does today). -2. `authServerUrl="${KEYCLOAK_BASE_URL}/realms/${KEYCLOAK_REALM}"` with realm `rhdh`. -3. `tokenUrl="${authServerUrl}/protocol/openid-connect/token"`. -4. Clone `https://github.com/rhdhorchestrator/orchestrator-demo.git` shallow into a temp dir. -5. Rewrite `09_token_propagation/manifests/01-configmap_token-propagation-props.yaml`: - - `http://example-kc-service.keycloak:8080/realms/quarkus` → `$authServerUrl` - - `client-id=quarkus-app` → `client-id=rhdh-client` - - `client-secret=lVGSvdaoDUem7lqeAnqXn1F92dCPbQea` → `client-secret=rhdh-client-secret` - - `http://sample-server-service.rhdh-operator` → `http://sample-server-service.${ns}:8080` -6. Rewrite `09_token_propagation/manifests/03-configmap_02-token-propagation-resources-specs.yaml` token URL to `$tokenUrl`. -7. Apply the sample-server Deployment/Service YAML from that overlays function (image `quay.io/orchestrator/sample-server:latest`), wait Available 120s. -8. `oc apply -n "$ns" -f "$manifestsDir"`. -9. Extend `patch_smoke_workflow` so `name=token-propagation` patches **persistence only** (do not change `podTemplate.container.image`; demo manifests already set it). Persistence JSON must match greeting: `backstage-psql-secret` / `POSTGRES_USER` / `POSTGRES_PASSWORD`, `serviceRef.name=backstage-psql`, `databaseName=backstage_plugin_orchestrator`, `databaseSchema=token-propagation`. -10. Include `token-propagation` in `wait_smoke_workflows_ready` alongside `greeting` and `failswitch` (all three must report `readyReplicas=1`). -11. `rm -rf` the clone. - -At the end of `ensure_smoke_workflows`, after greeting/failswitch apply+patch, call: - -```bash -ensure_token_propagation_workflow "$ns" -``` - -Keep a single wait loop that includes all three deployments. Probe (Task 2) stays after `ensure_smoke_workflows`, so token-propagation is Ready before GraphQL classify. - -- [ ] **Step 5: README** - -State that default smoke always deploys and runs token-propagation (JWT/OpenAPI into the workflow). Do not document `--include-token-propagation`. - -- [ ] **Step 6: Run unit tests** - -```bash -python3 utils/orchestrator/test_osl_smoke.py -``` - -Expected: PASS. - -- [ ] **Step 7: Commit** - -```bash -git add run-osl-regression.sh playwright/osl-regression-smoke.spec.ts README.md \ - utils/orchestrator/test_osl_smoke.py -git commit -m "$(cat <<'EOF' -feat: include token-propagation in default OSL RC smoke #13375 - -EOF -)" -``` - ---- - -### Task 5: Makefile pass-through and verification notes - -**Files:** -- Modify: `Makefile` (`osl-regression` target) - -**Interfaces:** -- Consumes: `--allow-relative-service-url` from Task 2 -- Produces: `make osl-regression` can pass `ALLOW_RELATIVE_SERVICE_URL=1` - -- [ ] **Step 1: Extend `osl-regression`** - -```make -osl-regression: ## Cleanup + prepare OSL + deploy + 4-test smoke (VERSION, OSL_RELEASE) -ifndef OSL_RELEASE - $(error OSL_RELEASE is required, e.g. make osl-regression VERSION=next OSL_RELEASE=1.39.0.CR1) -endif - ./run-osl-regression.sh --all --rhdh $(VERSION) --osl-release $(OSL_RELEASE) --namespace $(ORCH_NAMESPACE) \ - $(if $(filter 1,$(ALLOW_RELATIVE_SERVICE_URL)),--allow-relative-service-url,) -``` - -- [ ] **Step 2: Dry-run help** - -```bash -./run-osl-regression.sh --help -``` - -Expected: usage lists `--allow-relative-service-url`. It must **not** list `--include-token-propagation`. - -- [ ] **Step 3: Commit** - -```bash -git add Makefile README.md -git commit -m "$(cat <<'EOF' -docs: document OSL RC 4-test smoke including token-propagation #13375 - -EOF -)" -``` - ---- - -## Cluster verification (after Tasks 1–5, not a code task) - -On a logged-in cluster with RHDH already up: - -```bash -export PATH="/home/rlan/bin:$HOME/.local/bin:$PATH" -cd /home/rlan/redhat/rhdh-test-instance/.worktrees/rhidp-13375-osl-smoke -# 1.39.CR1 expected: probe exit 2 unless ALLOW_RELATIVE_SERVICE_URL=1 -ALLOW_RELATIVE_SERVICE_URL=1 ./run-osl-regression.sh --test --namespace orchestrator -``` - -Expected Playwright: **4 passed** (not 10). The report must include `Execute token-propagation workflow via API` and must not list `Verify Workflow All Runs` as executed. - -Do not treat this cluster run as part of the git tasks; it is the human/agent gate after the commits. - ---- - -## Self-review - -1. **Spec coverage:** 4-test default including token-propagation → Tasks 1, 3, 4. GraphQL probe → Task 2. `--full-e2e` as plugin suite → Task 3 README. Makefile → Task 5. Plugin `serviceUrl` productization → sibling plan, not this file. -2. **Placeholders:** none. -3. **Types:** bash `SMOKE_GREP` (four titles), probe exit 0/1/2, `--allow-relative-service-url` matches `ALLOW_RELATIVE_SERVICE_URL=1`. No `--include-token-propagation`. No Python helper modules. diff --git a/docs/superpowers/specs/2026-08-20-orchestrator-serviceurl-from-endpoint.md b/docs/superpowers/specs/2026-08-20-orchestrator-serviceurl-from-endpoint.md deleted file mode 100644 index 7085874..0000000 --- a/docs/superpowers/specs/2026-08-20-orchestrator-serviceurl-from-endpoint.md +++ /dev/null @@ -1,37 +0,0 @@ -# Spec: Derive Orchestrator `serviceUrl` from Data Index `endpoint` - -## Problem - -OSL 1.39 Data Index `ProcessDefinitions.serviceUrl` is a relative path ([SRVLOGIC-1137](https://redhat.atlassian.net/browse/SRVLOGIC-1137)). The RHDH Orchestrator backend concatenates that value: - -- execute: `POST ${serviceUrl}/${definitionId}` -- ping/schema: `GET ${serviceUrl}/management/processes/${definitionId}` -- abort/retrigger: `${serviceUrl}/management/processes/${definitionId}/instances/...` - -A relative `serviceUrl` such as `/greeting` becomes a failed fetch on the RHDH pod. Ricardo Zanini (SRVLOGIC-1137): `endpoint` is correct; consumers should take the server origin from `endpoint`. - -`rhdh-test-instance` currently hides this with `osl-di-rewrite`. That workaround must not stay as the product fix. - -## Goal - -In `@red-hat-developer-hub/backstage-plugin-orchestrator-backend`, after every GraphQL read of a process definition, set `serviceUrl` to an absolute HTTP(S) origin: - -- If `serviceUrl` already starts with `http://` or `https://`, keep it. -- Else if `endpoint` is an absolute URL, set `serviceUrl` to `new URL(endpoint).origin`. -- Else leave `serviceUrl` undefined (existing “not available” errors). - -## In scope - -- `rhdh-plugins` workspace `workspaces/orchestrator`, plugin `orchestrator-backend` only. -- Unit tests for the helper and for `fetchWorkflowInfos` / `fetchWorkflowServiceUrls` mapping. - -## Out of scope - -- `rhdh-test-instance` rewrite removal (do that in a later PR after this plugin is in the catalog the smoke uses). -- Changing GraphQL queries beyond ensuring `endpoint` is already selected (it is, on `fetchWorkflowInfos` and `fetchWorkflowInfo`; `fetchWorkflowServiceUrls` must add `endpoint`). - -## Constraints - -- Do not break OSL ≤ 1.38 (absolute `serviceUrl` unchanged). -- Do not use the versioned path from `endpoint` for execute (plugin still posts to `{origin}/{id}`, not `{endpoint}`). SRVLOGIC-1124 is OSL-side; do not switch execute to `endpoint` in this change. -- Conventional commits; link SRVLOGIC-1137 / RHIDP-13375 in the PR description, not as a required Jira key in this repo unless the project uses GitHub issues. diff --git a/docs/superpowers/specs/2026-08-20-osl-rc-smoke-subset.md b/docs/superpowers/specs/2026-08-20-osl-rc-smoke-subset.md deleted file mode 100644 index aa68464..0000000 --- a/docs/superpowers/specs/2026-08-20-osl-rc-smoke-subset.md +++ /dev/null @@ -1,69 +0,0 @@ -# Spec: OSL RC smoke subset (RHIDP-13375) - -Research for this spec: RHIDP-13375, RHDH 1.10 Orchestrator docs, OSL 1.37–1.38 release notes, SRVLOGIC-1137 / SRVLOGIC-1124, and the Orchestrator backend execute path (`POST {serviceUrl}/{id}` plus `/management/processes/...`). - -## Problem - -`./run-osl-regression.sh --test` used to copy `playwright/osl-regression-smoke.spec.ts` into overlays e2e and run **all 10** `registerOrchestratorCoreWorkflowTests` cases. That is more Playwright than an OSL CR gate needs: abort / status-detail / All Runs / suggested-link duplicate Failswitch OSL APIs and mostly assert RHDH UI. A single Greeting execute is **not** enough either: it misses Jobs Service timers, abort, switch/error, retrigger, and JWT/OpenAPI auth into the workflow runtime. - -OSL 1.39.CR1 also changed Data Index `ProcessDefinitions.serviceUrl` to a relative path (SRVLOGIC-1137). The current `osl-di-rewrite` proxy hides that from Playwright. There is no pre-Playwright check against the **raw** Data Index. - -## Goal - -Make the default OSL RC path (`--all` / `make osl-regression`) a **lean OSL contract + workflow gate**: - -1. Probe raw Data Index GraphQL before Playwright. -2. Run exactly four Playwright tests (Greeting, Failswitch statuses, Failswitch retrigger, token-propagation). - -## In scope (this repo: `rhdh-test-instance`) - -- `run-osl-regression.sh` wiring: raw Data Index GraphQL probe (`jq`), default Playwright `--grep` of the four titles, `--allow-relative-service-url`. -- Smoke wrapper always registers token-propagation tests (no env flag). -- Always deploy `sample-server` + `token-propagation` on the smoke path (same Keycloak substitutions overlays uses). -- README / Makefile copy. - -## Out of scope (separate plan) - -- Changing Orchestrator plugin code to derive `serviceUrl` origin from `endpoint`. -- Removing `osl-di-rewrite` (only after the plugin ships). -- Editing `rhdh-plugin-export-overlays` test files in git (runtime copy of the smoke wrapper stays). - -## Default Playwright titles - -Exact strings from overlays specs: - -1. `Run Greeting workflow and verify Workflows tab` -2. `Run Failswitch workflow and verify statuses` -3. `Rerun Failswitch from failure point` -4. `Execute token-propagation workflow via API` - -## GraphQL probe - -- Query **raw** `http://sonataflow-platform-data-index-service..svc.cluster.local/graphql` from inside the cluster (RHDH pod `curl`), **not** `osl-di-rewrite`. -- Query body: `{ ProcessDefinitions { id serviceUrl endpoint } }`. -- A `serviceUrl` is valid only if it starts with `http://` or `https://`. -- If any definition has a missing or relative `serviceUrl`, exit **2** unless `ALLOW_RELATIVE_SERVICE_URL=1` / `--allow-relative-service-url` (then print a warning and continue so Playwright can still run behind the rewrite proxy). -- If the query fails or returns zero definitions after smoke workflows are Ready, exit **1**. - -## Token-propagation (always on for smoke) - -Default `--test` / `--all` always: - -- Deploys `sample-server` and `token-propagation` from `https://github.com/rhdhorchestrator/orchestrator-demo.git` path `09_token_propagation/manifests`, with the same Keycloak URL substitutions overlays uses. -- Registers overlays `Execute token-propagation workflow via API` in the smoke wrapper (unconditional). -- Includes that title in the Playwright grep. -- Waits for `deployment/token-propagation` Ready before the GraphQL probe. - -There is no `--include-token-propagation` or `--full-e2e` flag. `--cleanup` always removes operators, catalog, and mirror (the former `--include-operators` behavior). - -## `--allow-relative-service-url` - -OSL 1.39.CR1 Data Index can return a relative `ProcessDefinitions.serviceUrl` (SRVLOGIC-1137). The Orchestrator plugin then cannot execute/abort/retrigger workflows. The smoke probe queries **raw** Data Index and exits 2 on that contract break. Pass `--allow-relative-service-url` or `ALLOW_RELATIVE_SERVICE_URL=1` to warn and continue so Playwright can still run behind `osl-di-rewrite`. Remove the override after the plugin derives `serviceUrl` from `endpoint`. - -## Constraints - -- Do not commit `.env`, `.env.osl`, cluster passwords, or Keycloak secrets. -- Do not edit files outside `rhdh-test-instance` for this spec. -- Driver is bash (`run-osl-regression.sh`). Classify GraphQL with `jq`. Do not add Python helper modules. -- `oc` / `helm` may live in `/home/rlan/bin`; driver already assumes they are on `PATH`. -- Conventional commits; reference `#13375`. From 3ee9a8afa5e214fbad501c0d0f83f7a7fe457493 Mon Sep 17 00:00:00 2001 From: Rostislav Lan Date: Thu, 20 Aug 2026 12:37:52 +0200 Subject: [PATCH 08/13] fix: narrow OSL cleanup and harden Keycloak smoke client #13375 Limit operator teardown to logic/serverless CSVs, fail workflow patches instead of ignoring them, pin token-propagation fixtures, and restrict the Keycloak OIDC redirects after RHDH is up. Co-authored-by: Cursor --- README.md | 2 +- cleanup.sh | 113 ++++++++++++++++-------------- run-osl-regression.sh | 12 +++- setup-orchestrator.sh | 33 +++++++-- utils/keycloak/keycloak-deploy.sh | 39 +++++++++-- utils/keycloak/rhdh-client.json | 10 +-- 6 files changed, 133 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index e677538..f4146e8 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ Pin an OSL pre-release against a chosen RHDH version, deploy, and run the defaul 3. `Rerun Failswitch from failure point` 4. `Execute token-propagation workflow via API` -Smoke always deploys greeting, failswitch, token-propagation, and `sample-server`, then runs token-propagation (JWT/OpenAPI into the workflow). `--cleanup` (and the cleanup phase of `--all`) always removes OSL/Serverless operators, the custom catalog, and the mirror namespace as well as the RHDH namespace contents. +Smoke always deploys greeting, failswitch, token-propagation, and `sample-server`, then runs token-propagation (JWT/OpenAPI into the workflow). `--cleanup` (and the cleanup phase of `--all`) always removes OSL/Serverless operators (`logic-operator` / `serverless-operator` only), the custom catalog, and the mirror namespace, and cleans the RHDH namespace contents. It does not delete a leftover `rhdh` namespace unless you pass `--delete-namespace` (`make cleanup-full`). Other operators in `openshift-operators` are left in place. Before Playwright, a GraphQL probe hits the **raw** Data Index (`sonataflow-platform-data-index-service`), not the `osl-di-rewrite` proxy. OSL 1.39.CR1 can return a relative `ProcessDefinitions.serviceUrl` (SRVLOGIC-1137). The Orchestrator plugin then cannot `POST` to execute/abort/retrigger. The probe exits 2 on that unless you pass `--allow-relative-service-url` or `ALLOW_RELATIVE_SERVICE_URL=1`, which prints a warning and continues so the four tests can still run behind the rewrite proxy. Drop that override after the plugin derives `serviceUrl` from `endpoint`. diff --git a/cleanup.sh b/cleanup.sh index 9de97ae..f6909d0 100755 --- a/cleanup.sh +++ b/cleanup.sh @@ -8,8 +8,11 @@ # # Options: # --namespace Target namespace (default: rhdh) -# --include-operators Also remove OSL/Serverless operators (cluster-scoped) -# --delete-namespace Delete the namespace itself at the end +# --include-operators Also remove OSL/Serverless operators (logic-operator +# and serverless-operator only; other CSVs in +# openshift-operators are left in place) +# --delete-namespace Delete the target namespace itself at the end +# (required before leftover namespaces like rhdh fail verify) # # All commands are idempotent -- safe to run multiple times. @@ -128,13 +131,19 @@ delete_knative_webhooks() { --ignore-not-found 2>/dev/null || true } -delete_olm_subscriptions() { - local ns="$1" - local sub +OSL_OLM_MATCH='logic-operator|serverless-operator' + +delete_osl_olm_resources() { + local kind="$1" + local ns="$2" + local resource name - for sub in $(oc get subscriptions.operators.coreos.com -n "$ns" -o name 2>/dev/null); do - echo " Deleting $sub in $ns" - oc delete "$sub" -n "$ns" --ignore-not-found 2>/dev/null || true + for resource in $(oc get "$kind" -n "$ns" -o name 2>/dev/null); do + name="${resource##*/}" + if [[ "$name" =~ $OSL_OLM_MATCH ]]; then + echo " Deleting $resource in $ns" + oc delete "$resource" -n "$ns" --ignore-not-found 2>/dev/null || true + fi done } @@ -174,47 +183,53 @@ wait_for_namespace_gone() { post_cleanup_verify() { local failures=0 + local ns remaining_subs remaining_csvs knative_webhooks echo "--- Post-clean verification ---" - local target_ns='rhdh|orchestrator|rhdh-keycloak|knative-serving|knative-eventing|knative-serving-ingress|openshift-serverless|openshift-serverless-logic|orchestrator-infra|osl-mirror' - local remaining_ns - remaining_ns="$(oc get ns -o name 2>/dev/null | awk "tolower(\$0) ~ /${target_ns}/" || true)" - if [[ -n "$remaining_ns" ]]; then - echo " Remaining namespaces:" - echo "$remaining_ns" | sed 's/^/ /' - failures=1 - fi - local remaining_subs - remaining_subs="$(oc get subscriptions.operators.coreos.com -A -o name 2>/dev/null | awk 'tolower($0) ~ /logic-operator|serverless-operator/' || true)" - if [[ -n "$remaining_subs" ]]; then - echo " Remaining subscriptions:" - echo "$remaining_subs" | sed 's/^/ /' - failures=1 - fi + if [[ "$include_operators" == "true" ]]; then + for ns in knative-serving knative-eventing knative-serving-ingress \ + openshift-serverless openshift-serverless-logic orchestrator-infra \ + orchestrator orchestrator-e2e rhdh-keycloak osl-mirror; do + if oc get namespace "$ns" &>/dev/null; then + echo " Remaining namespace: $ns" + failures=1 + fi + done - local remaining_csvs - remaining_csvs="$(oc get csv -A -o name 2>/dev/null | awk 'tolower($0) ~ /logic-operator|serverless-operator/' || true)" - if [[ -n "$remaining_csvs" ]]; then - echo " Remaining CSVs:" - echo "$remaining_csvs" | sed 's/^/ /' - failures=1 - fi + remaining_subs="$(oc get subscriptions.operators.coreos.com -A -o name 2>/dev/null | awk 'tolower($0) ~ /logic-operator|serverless-operator/' || true)" + if [[ -n "$remaining_subs" ]]; then + echo " Remaining subscriptions:" + echo "$remaining_subs" | sed 's/^/ /' + failures=1 + fi - if oc get catalogsource osl-custom-catalog -n openshift-marketplace &>/dev/null; then - echo " Remaining catalogsource: openshift-marketplace/osl-custom-catalog" - failures=1 - fi - if oc get imagedigestmirrorset osl-bundle-mirror &>/dev/null; then - echo " Remaining IDMS: osl-bundle-mirror" - failures=1 + remaining_csvs="$(oc get csv -A -o name 2>/dev/null | awk 'tolower($0) ~ /logic-operator|serverless-operator/' || true)" + if [[ -n "$remaining_csvs" ]]; then + echo " Remaining CSVs:" + echo "$remaining_csvs" | sed 's/^/ /' + failures=1 + fi + + if oc get catalogsource osl-custom-catalog -n openshift-marketplace &>/dev/null; then + echo " Remaining catalogsource: openshift-marketplace/osl-custom-catalog" + failures=1 + fi + if oc get imagedigestmirrorset osl-bundle-mirror &>/dev/null; then + echo " Remaining IDMS: osl-bundle-mirror" + failures=1 + fi + + knative_webhooks="$(oc get validatingwebhookconfigurations,mutatingwebhookconfigurations -o name 2>/dev/null | awk 'tolower($0) ~ /knative/' || true)" + if [[ -n "$knative_webhooks" ]]; then + echo " Remaining Knative webhooks:" + echo "$knative_webhooks" | sed 's/^/ /' + failures=1 + fi fi - local knative_webhooks - knative_webhooks="$(oc get validatingwebhookconfigurations,mutatingwebhookconfigurations -o name 2>/dev/null | awk 'tolower($0) ~ /knative/' || true)" - if [[ -n "$knative_webhooks" ]]; then - echo " Remaining Knative webhooks:" - echo "$knative_webhooks" | sed 's/^/ /' + if [[ "$delete_namespace" == "true" ]] && oc get namespace "$namespace" &>/dev/null; then + echo " Remaining target namespace: $namespace" failures=1 fi @@ -224,7 +239,7 @@ post_cleanup_verify() { exit 1 fi - echo " Verification passed: no known orchestrator/serverless leftovers found." + echo " Verification passed: no known leftovers for this cleanup mode." } # --------------------------------------------------------------------------- @@ -261,17 +276,9 @@ if [[ "$include_operators" == "true" ]]; then # Custom CatalogSource oc delete catalogsource osl-custom-catalog -n openshift-marketplace --ignore-not-found 2>/dev/null || true - # Subscriptions - for ns in openshift-serverless-logic openshift-serverless openshift-operators; do - delete_olm_subscriptions "$ns" - done - - # CSVs for ns in openshift-serverless-logic openshift-serverless openshift-operators; do - for csv in $(oc get csv -n "$ns" -o name 2>/dev/null); do - echo " Deleting $csv in $ns" - oc delete "$csv" -n "$ns" --ignore-not-found 2>/dev/null || true - done + delete_osl_olm_resources subscriptions.operators.coreos.com "$ns" + delete_osl_olm_resources csv "$ns" done # ImageDigestMirrorSet diff --git a/run-osl-regression.sh b/run-osl-regression.sh index c6bcbc6..ac518d2 100755 --- a/run-osl-regression.sh +++ b/run-osl-regression.sh @@ -28,6 +28,8 @@ SMOKE_GREP='Run Greeting workflow and verify Workflows tab|Run Failswitch workfl WORKFLOW_REPO="${SERVERLESS_WORKFLOWS_REPO:-https://github.com/rhdhorchestrator/serverless-workflows.git}" WORKFLOW_REPO_REF="${SERVERLESS_WORKFLOWS_REF:-daeeee8dec16beab6d96a81774ef500081a2c2b0}" DEMO_WORKFLOW_REPO="${ORCHESTRATOR_DEMO_REPO:-https://github.com/rhdhorchestrator/orchestrator-demo.git}" +DEMO_WORKFLOW_REF="${ORCHESTRATOR_DEMO_REF:-c6e59bab65bd584ede5fde7610bbc6187e70206c}" +SAMPLE_SERVER_IMAGE="${SAMPLE_SERVER_IMAGE:-quay.io/orchestrator/sample-server@sha256:67e694c65bdff0b256590ac32aaad1eeb2045ffbe6923b140d4e022acf8c8993}" run_all=false run_cleanup=false @@ -235,7 +237,7 @@ patch_smoke_workflow() { greeting) image="quay.io/orchestrator/serverless-workflow-greeting:osl_${tag}" ;; failswitch) image="quay.io/orchestrator/fail-switch:osl_${tag}" ;; token-propagation) - oc -n "$ns" patch sonataflow "$name" --type merge -p "$persistence" >/dev/null || true + oc -n "$ns" patch sonataflow "$name" --type merge -p "$persistence" >/dev/null return 0 ;; *) die "unknown smoke workflow: $name" ;; @@ -265,7 +267,7 @@ patch_smoke_workflow() { } } } - }" >/dev/null || true + }" >/dev/null } wait_smoke_workflows_ready() { @@ -301,6 +303,8 @@ ensure_token_propagation_workflow() { _osl_token_demo_cleanup() { rm -rf "$demo_dir"; trap - RETURN; } trap _osl_token_demo_cleanup RETURN git clone --depth 1 "$DEMO_WORKFLOW_REPO" "$demo_dir" >/dev/null + git -C "$demo_dir" fetch --depth 1 origin "$DEMO_WORKFLOW_REF" >/dev/null + git -C "$demo_dir" checkout --detach "$DEMO_WORKFLOW_REF" >/dev/null manifests_dir="${demo_dir}/09_token_propagation/manifests" props_cm="${manifests_dir}/01-configmap_token-propagation-props.yaml" specs_cm="${manifests_dir}/03-configmap_02-token-propagation-resources-specs.yaml" @@ -341,7 +345,7 @@ spec: spec: containers: - name: sample-server - image: quay.io/orchestrator/sample-server:latest + image: ${SAMPLE_SERVER_IMAGE} ports: - containerPort: 8080 livenessProbe: @@ -438,6 +442,7 @@ write_overlays_dotenv() { K8S_CLUSTER_ROUTER_BASE=${K8S_CLUSTER_ROUTER_BASE} RHDH_BASE_URL=${RHDH_BASE_URL} RHDH_VERSION=${RHDH_VERSION:-} +NAME_SPACE=${namespace} SKIP_KEYCLOAK_DEPLOYMENT=true SKIP_OPERATOR_INSTALLATION=true GH_USER_ID=test1 @@ -629,6 +634,7 @@ phase_test() { export K8S_CLUSTER_ROUTER_BASE RHDH_BASE_URL KEYCLOAK_BASE_URL RHDH_VERSION export SKIP_KEYCLOAK_DEPLOYMENT=true export SKIP_OPERATOR_INSTALLATION=true + export NAME_SPACE="$namespace" export GH_USER_ID=test1 export GH_USER_PASS=test1@123 export KEYCLOAK_REALM=rhdh diff --git a/setup-orchestrator.sh b/setup-orchestrator.sh index fe32c40..e620979 100755 --- a/setup-orchestrator.sh +++ b/setup-orchestrator.sh @@ -76,6 +76,8 @@ while [[ $# -gt 0 ]]; do esac done +cd "$SCRIPT_DIR" + # ── Validate inputs ────────────────────────────────────────────────────────── if ! oc whoami &>/dev/null; then @@ -330,14 +332,20 @@ prepare_keycloak() { } sync_keycloak_runtime_env() { - local keycloak_host + local keycloak_host keycloak_proto keycloak_host="$(oc get route keycloak -n "$KEYCLOAK_NAMESPACE" -o jsonpath='{.spec.host}' 2>/dev/null || true)" if [[ -z "$keycloak_host" ]]; then echo "Error: could not resolve Keycloak route in namespace '$KEYCLOAK_NAMESPACE'." exit 1 fi - export KEYCLOAK_BASE_URL="https://${keycloak_host}" + if [[ -z "${KEYCLOAK_BASE_URL:-}" ]]; then + keycloak_proto="http" + if oc get route keycloak -n "$KEYCLOAK_NAMESPACE" -o jsonpath='{.spec.tls.termination}' 2>/dev/null | grep -q .; then + keycloak_proto="https" + fi + export KEYCLOAK_BASE_URL="${keycloak_proto}://${keycloak_host}" + fi export KEYCLOAK_METADATA_URL="${KEYCLOAK_BASE_URL}/realms/rhdh" export KEYCLOAK_REALM="${KEYCLOAK_REALM:-rhdh}" export KEYCLOAK_LOGIN_REALM="${KEYCLOAK_LOGIN_REALM:-${KEYCLOAK_REALM}}" @@ -358,6 +366,11 @@ verify_shared_existing_rhdh_contract() { log_debug "Entrypoint args: version=${version}, namespace=${namespace}, prepareInternalOsl=${prepare_internal_osl_release:-none}" phase_checkpoint "cluster-connectivity-validated" +if [[ -f "${SCRIPT_DIR}/.env.osl" ]]; then + # shellcheck disable=SC1091 + source "${SCRIPT_DIR}/.env.osl" + log "Loaded existing .env.osl before baseline (OSL_CATALOG_SOURCE=${OSL_CATALOG_SOURCE:-unset})" +fi assert_empty_baseline "$namespace" "$KEYCLOAK_NAMESPACE" wait_for_rhdh_auth_and_orchestrator_ready() { @@ -499,12 +512,21 @@ export SONATAFLOW_DATA_INDEX_URL="http://sonataflow-platform-data-index-service. export IS_AUTH_ENABLED="true" log "Deploying RHDH $version with shared orchestrator support" -cd "$SCRIPT_DIR" SKIP_ENV_SOURCE=1 \ SKIP_ORCHESTRATOR_INFRA_INSTALL=1 \ ./deploy.sh helm "$version" --namespace "$namespace" --with-orchestrator phase_checkpoint "rhdh-deployed" +rhdh_host="$(oc get route redhat-developer-hub -n "$namespace" -o jsonpath='{.spec.host}' 2>/dev/null || true)" +if [[ -z "$rhdh_host" ]]; then + echo "Error: Could not resolve RHDH route after deploy." + exit 1 +fi +export RHDH_BASE_URL="https://${rhdh_host}" +if declare -F update_rhdh_client_redirects >/dev/null; then + update_rhdh_client_redirects "$RHDH_BASE_URL" +fi + # ── Verify overlays existing-RHDH contract ─────────────────────────────────── verify_shared_existing_rhdh_contract @@ -523,9 +545,8 @@ run_post_setup_workflow_smoke "$namespace" # ── Summary ────────────────────────────────────────────────────────────────── rhdh_host="$(oc get route redhat-developer-hub -n "$namespace" -o jsonpath='{.spec.host}' 2>/dev/null || true)" -keycloak_host="$(oc get route keycloak -n "$KEYCLOAK_NAMESPACE" -o jsonpath='{.spec.host}' 2>/dev/null || true)" -RHDH_URL="${rhdh_host:+https://${rhdh_host}}" -KEYCLOAK_URL="${keycloak_host:+https://${keycloak_host}}" +RHDH_URL="${RHDH_BASE_URL:-${rhdh_host:+https://${rhdh_host}}}" +KEYCLOAK_URL="${KEYCLOAK_BASE_URL:-}" echo "" echo "===========================================" diff --git a/utils/keycloak/keycloak-deploy.sh b/utils/keycloak/keycloak-deploy.sh index a8a9e6d..448b3a6 100755 --- a/utils/keycloak/keycloak-deploy.sh +++ b/utils/keycloak/keycloak-deploy.sh @@ -6,10 +6,12 @@ command -v jq >/dev/null 2>&1 || { echo "Error: jq is required but not installed command -v oc >/dev/null 2>&1 || { echo "Error: oc (OpenShift CLI) is required but not installed"; exit 1; } NAMESPACE=${1:-rhdh-keycloak} -USERS_FILE=${2:-utils/keycloak/users.json} -GROUPS_FILE=${3:-utils/keycloak/groups.json} -CLIENT_FILE="utils/keycloak/rhdh-client.json" +KEYCLOAK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +USERS_FILE=${2:-"${KEYCLOAK_DIR}/users.json"} +GROUPS_FILE=${3:-"${KEYCLOAK_DIR}/groups.json"} +CLIENT_FILE="${KEYCLOAK_DIR}/rhdh-client.json" KEYCLOAK_RELEASE_NAME="keycloak" +KEYCLOAK_VALUES="${KEYCLOAK_DIR}/keycloak-values.yaml" # Helper function for API calls with error checking api_call() { @@ -65,7 +67,7 @@ helm repo update echo "Deploying Keycloak..." helm upgrade --install $KEYCLOAK_RELEASE_NAME bitnami/keycloak \ --namespace $NAMESPACE \ - --values utils/keycloak/keycloak-values.yaml + --values "$KEYCLOAK_VALUES" echo "Waiting for Keycloak rollout..." oc rollout status statefulset/keycloak -n $NAMESPACE --timeout=5m @@ -80,7 +82,7 @@ fi # Create OpenShift Route echo "Creating OpenShift Route (protocol: $KEYCLOAK_PROTOCOL)..." if [ "$KEYCLOAK_PROTOCOL" = "https" ]; then -cat </dev/null + echo "Pinned rhdh-client redirectUris/webOrigins to ${redirect}" +} diff --git a/utils/keycloak/rhdh-client.json b/utils/keycloak/rhdh-client.json index 2f5ed39..fca8515 100755 --- a/utils/keycloak/rhdh-client.json +++ b/utils/keycloak/rhdh-client.json @@ -10,17 +10,13 @@ "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", "secret": "rhdh-client-secret", - "redirectUris": [ - "*" - ], - "webOrigins": [ - "*" - ], + "redirectUris": [], + "webOrigins": [], "notBefore": 0, "bearerOnly": false, "consentRequired": false, "standardFlowEnabled": true, - "implicitFlowEnabled": true, + "implicitFlowEnabled": false, "directAccessGrantsEnabled": true, "serviceAccountsEnabled": true, "authorizationServicesEnabled": true, From 692c4aa3f18efd13dba79277be5c9b4764904782 Mon Sep 17 00:00:00 2001 From: Rostislav Lan Date: Thu, 20 Aug 2026 13:03:24 +0200 Subject: [PATCH 09/13] fix: install DI rewrite from setup and harden OSL smoke #13375 setup-orchestrator now deploys osl-di-rewrite and uses the RHDH route scheme. Mirror skip compares source/dest digests; token-propagation and opm images are pinned. Co-authored-by: Cursor --- Makefile | 2 +- README.md | 12 +- playwright/osl-regression-smoke.spec.ts | 4 +- prepare-osl-internal.sh | 14 +- run-osl-regression.sh | 130 ++---------------- setup-orchestrator.sh | 65 ++++++--- utils/keycloak/keycloak-deploy.sh | 5 +- .../orchestrator/ensure-dataindex-rewrite.sh | 110 +++++++++++++++ 8 files changed, 192 insertions(+), 150 deletions(-) create mode 100755 utils/orchestrator/ensure-dataindex-rewrite.sh diff --git a/Makefile b/Makefile index 996ee31..bc21de2 100644 --- a/Makefile +++ b/Makefile @@ -89,7 +89,7 @@ cleanup: ## Clean RHDH/orchestrator/OSL resources and operators from ORCH_NAMESP cleanup-full: ## Full cleanup: operators + related namespaces ./cleanup.sh --namespace $(ORCH_NAMESPACE) --include-operators --delete-namespace -osl-regression: ## Cleanup + prepare OSL + deploy + 4-test smoke (VERSION, OSL_RELEASE) +osl-regression: ## Cleanup + prepare OSL + deploy + 4-test smoke (VERSION, OSL_RELEASE; ORCH_NAMESPACE must be orchestrator) ifndef OSL_RELEASE $(error OSL_RELEASE is required, e.g. make osl-regression VERSION=next OSL_RELEASE=1.39.0.CR1) endif diff --git a/README.md b/README.md index f4146e8..b7b6215 100644 --- a/README.md +++ b/README.md @@ -188,9 +188,9 @@ Pin an OSL pre-release against a chosen RHDH version, deploy, and run the defaul 3. `Rerun Failswitch from failure point` 4. `Execute token-propagation workflow via API` -Smoke always deploys greeting, failswitch, token-propagation, and `sample-server`, then runs token-propagation (JWT/OpenAPI into the workflow). `--cleanup` (and the cleanup phase of `--all`) always removes OSL/Serverless operators (`logic-operator` / `serverless-operator` only), the custom catalog, and the mirror namespace, and cleans the RHDH namespace contents. It does not delete a leftover `rhdh` namespace unless you pass `--delete-namespace` (`make cleanup-full`). Other operators in `openshift-operators` are left in place. +Smoke always deploys greeting, failswitch, token-propagation, and `sample-server`, then runs token-propagation (JWT/OpenAPI into the workflow). `--test` requires `--namespace orchestrator` (the default): overlays Playwright uses the project name as the Kubernetes namespace for Data Index and Failswitch retrigger. `--cleanup` (and the cleanup phase of `--all`) always removes OSL/Serverless operators (`logic-operator` / `serverless-operator` only), the custom catalog, and the mirror namespace, and cleans the RHDH namespace contents. It does not delete a leftover `rhdh` namespace unless you pass `--delete-namespace` (`make cleanup-full`). Other operators in `openshift-operators` are left in place. -Before Playwright, a GraphQL probe hits the **raw** Data Index (`sonataflow-platform-data-index-service`), not the `osl-di-rewrite` proxy. OSL 1.39.CR1 can return a relative `ProcessDefinitions.serviceUrl` (SRVLOGIC-1137). The Orchestrator plugin then cannot `POST` to execute/abort/retrigger. The probe exits 2 on that unless you pass `--allow-relative-service-url` or `ALLOW_RELATIVE_SERVICE_URL=1`, which prints a warning and continues so the four tests can still run behind the rewrite proxy. Drop that override after the plugin derives `serviceUrl` from `endpoint`. +`make setup-orchestrator` (and the driver's `--deploy` phase) installs `osl-di-rewrite` in front of Data Index so OSL 1.39 relative `serviceUrl` values still work from RHDH. The GraphQL probe before Playwright still hits the **raw** Data Index (`sonataflow-platform-data-index-service`), not that proxy. OSL 1.39.CR1 can return a relative `ProcessDefinitions.serviceUrl` (SRVLOGIC-1137). The Orchestrator plugin then cannot `POST` to execute/abort/retrigger. The probe exits 2 on that unless you pass `--allow-relative-service-url` or `ALLOW_RELATIVE_SERVICE_URL=1`, which prints a warning and continues so the four tests can still run behind the rewrite proxy. Drop that override after the plugin derives `serviceUrl` from `endpoint`. ```bash # One-shot: full cleanup (including operators) -> mirror OSL -> deploy -> smoke @@ -413,6 +413,7 @@ rhdh-test-instance/ │ ├── app-config-rhdh.yaml # Main RHDH configuration (guest auth by default) │ ├── dynamic-plugins.yaml # Base dynamic plugins configuration │ ├── orchestrator-dynamic-plugins.yaml # Orchestrator plugins (merged when ORCH=true) +│ ├── osl-releases/ # Local OSL pre-release JSON (gitignored except example) │ ├── rbac-policies.yaml # RBAC policy ConfigMap │ └── rhdh-secrets.yaml # Reference template for rhdh-secrets Secret ├── helm/ @@ -439,7 +440,14 @@ rhdh-test-instance/ │ └── plugins/ │ ├── config-keycloak-plugin.sh # Keycloak deploy, realm/client/user setup │ └── config-lighthouse-plugin.sh # Lighthouse deploy and URL injection +├── utils/ +│ ├── keycloak/ # Shared Keycloak deploy used by setup-orchestrator +│ └── orchestrator/ # Data Index rewrite proxy and existing-RHDH checks +├── cleanup.sh # Orchestrator/OSL teardown (operators optional) ├── deploy.sh # Main deploy entry point +├── prepare-osl-internal.sh # Mirror pre-release OSL into the internal registry +├── run-osl-regression.sh # OSL RC smoke driver (cleanup → prepare → deploy → test) +├── setup-orchestrator.sh # RHDH + orchestrator + Keycloak + rewrite proxy ├── teardown.sh # Main teardown entry point ├── Makefile # Make targets ├── OWNERS # Project maintainers diff --git a/playwright/osl-regression-smoke.spec.ts b/playwright/osl-regression-smoke.spec.ts index 93f1d89..8c1e4dc 100644 --- a/playwright/osl-regression-smoke.spec.ts +++ b/playwright/osl-regression-smoke.spec.ts @@ -90,6 +90,8 @@ test.beforeEach(async ({ page }) => { } }); -const ensureDataIndexOrSkip = createDataIndexGuard(); +const innerDataIndexGuard = createDataIndexGuard(); +const ensureDataIndexOrSkip = (ns: string, testObj: { skip: (condition: boolean, reason: string) => void }) => + innerDataIndexGuard(process.env.NAME_SPACE || ns, testObj); registerOrchestratorCoreWorkflowTests(ensureDataIndexOrSkip); registerTokenPropagationWorkflowTests(requireEnvVar); diff --git a/prepare-osl-internal.sh b/prepare-osl-internal.sh index 6110b29..33fcb15 100755 --- a/prepare-osl-internal.sh +++ b/prepare-osl-internal.sh @@ -241,10 +241,16 @@ rewrite_osl_refs_in_dir() { # --------------------------------------------------------------------------- mirror_image() { local source_ref="$1" push_ref="$2" + local dest_digest src_digest - if skopeo inspect --no-tags --tls-verify=false "docker://${push_ref}" >/dev/null 2>&1; then - log " already present, skipping copy" - return 0 + dest_digest="$(skopeo inspect --no-tags --tls-verify=false "docker://${push_ref}" 2>/dev/null | jq -r '.Digest // empty')" + if [[ "$dest_digest" == sha256:* ]]; then + src_digest="$(skopeo inspect --no-tags --tls-verify=false "docker://${source_ref}" 2>/dev/null | jq -r '.Digest // empty')" + if [[ -n "$src_digest" && "$src_digest" == "$dest_digest" ]]; then + log " already present (${dest_digest}), skipping copy" + return 0 + fi + log " dest digest ${dest_digest} differs from source ${src_digest:-unknown}; recopying" fi local skopeo_args=(copy --preserve-digests --retry-times "$SKOPEO_RETRY_TIMES" @@ -328,7 +334,7 @@ build_rewritten_logic_catalog() { rewrite_osl_refs_in_dir "${workdir}/configs" "$BUNDLE_DIGEST_PIN" cat > "${workdir}/Dockerfile" <<'EOF' -FROM quay.io/operator-framework/opm:latest +FROM quay.io/operator-framework/opm@sha256:3bbabf4be41d2d071ce5dd2fe35040139848331c95dfb23ff06f5ba47fd13203 COPY configs /configs ENTRYPOINT ["/bin/opm"] CMD ["serve", "/configs", "--cache-dir=/tmp/cache", "--cache-enforce-integrity=false"] diff --git a/run-osl-regression.sh b/run-osl-regression.sh index ac518d2..3173b14 100755 --- a/run-osl-regression.sh +++ b/run-osl-regression.sh @@ -30,6 +30,7 @@ WORKFLOW_REPO_REF="${SERVERLESS_WORKFLOWS_REF:-daeeee8dec16beab6d96a81774ef50008 DEMO_WORKFLOW_REPO="${ORCHESTRATOR_DEMO_REPO:-https://github.com/rhdhorchestrator/orchestrator-demo.git}" DEMO_WORKFLOW_REF="${ORCHESTRATOR_DEMO_REF:-c6e59bab65bd584ede5fde7610bbc6187e70206c}" SAMPLE_SERVER_IMAGE="${SAMPLE_SERVER_IMAGE:-quay.io/orchestrator/sample-server@sha256:67e694c65bdff0b256590ac32aaad1eeb2045ffbe6923b140d4e022acf8c8993}" +TOKEN_PROPAGATION_IMAGE="${TOKEN_PROPAGATION_IMAGE:-quay.io/orchestrator/demo-token-propagation@sha256:8b35f7aeafde48deed2700ab9bb247f77d1322d0a3c26005b51aaac782d55302}" run_all=false run_cleanup=false @@ -58,7 +59,9 @@ Options: --rhdh RHDH version (required with --deploy / --all) --osl-release Load config/osl-releases/.json --osl-manifest Explicit OSL manifest path - --namespace RHDH/orchestrator namespace (default: orchestrator) + --namespace RHDH/orchestrator namespace (default: orchestrator). + --test requires orchestrator because overlays + Playwright uses the project name as the k8s ns. --overlays-dir rhdh-plugin-export-overlays checkout --allow-relative-service-url OSL 1.39 Data Index may return a relative ProcessDefinitions.serviceUrl (SRVLOGIC-1137). @@ -212,34 +215,11 @@ workflow_osl_image_tag() { } patch_smoke_workflow() { - local ns="$1" name="$2" tag="${3:-}" image persistence - persistence="{ - \"spec\": { - \"persistence\": { - \"dbMigrationStrategy\": \"job\", - \"postgresql\": { - \"secretRef\": { - \"name\": \"backstage-psql-secret\", - \"userKey\": \"POSTGRES_USER\", - \"passwordKey\": \"POSTGRES_PASSWORD\" - }, - \"serviceRef\": { - \"name\": \"backstage-psql\", - \"namespace\": \"${ns}\", - \"databaseName\": \"backstage_plugin_orchestrator\", - \"databaseSchema\": \"${name}\" - } - } - } - } - }" + local ns="$1" name="$2" tag="${3:-}" image case "$name" in greeting) image="quay.io/orchestrator/serverless-workflow-greeting:osl_${tag}" ;; failswitch) image="quay.io/orchestrator/fail-switch:osl_${tag}" ;; - token-propagation) - oc -n "$ns" patch sonataflow "$name" --type merge -p "$persistence" >/dev/null - return 0 - ;; + token-propagation) image="${TOKEN_PROPAGATION_IMAGE}" ;; *) die "unknown smoke workflow: $name" ;; esac oc -n "$ns" patch sonataflow "$name" --type merge -p "{ @@ -397,8 +377,8 @@ ensure_smoke_workflows() { patch_smoke_workflow "$ns" failswitch "$tag" ensure_token_propagation_workflow "$ns" wait_smoke_workflows_ready "$ns" 600 - oc rollout restart "deploy/sonataflow-platform-data-index-service" -n "$ns" >/dev/null 2>&1 || true - oc rollout status "deploy/sonataflow-platform-data-index-service" -n "$ns" --timeout=180s >/dev/null 2>&1 || true + oc rollout restart "deploy/sonataflow-platform-data-index-service" -n "$ns" + oc rollout status "deploy/sonataflow-platform-data-index-service" -n "$ns" --timeout=180s } ensure_e2e_deps() { @@ -486,96 +466,7 @@ phase_prepare() { ensure_dataindex_rewrite() { local ns="$1" - local name="osl-di-rewrite" - local image rewrite_url oidc_tmp - image="$(oc get deploy redhat-developer-hub -n "$ns" -o jsonpath='{.spec.template.spec.containers[0].image}' 2>/dev/null || true)" - [[ -n "$image" ]] || die "cannot resolve RHDH image for data-index rewrite proxy" - rewrite_url="http://${name}.${ns}.svc.cluster.local" - log "ensuring data-index rewrite proxy ${name} -> sonataflow-platform-data-index-service" - oc create configmap "$name" \ - --from-file=osl-di-rewrite.js="${SCRIPT_DIR}/utils/orchestrator/osl-di-rewrite.js" \ - -n "$ns" --dry-run=client -o yaml | oc apply -f - >/dev/null - oc apply -f - >/dev/null </dev/null - oidc_tmp="$(mktemp)" - oc get configmap app-config-oidc -n "$ns" -o jsonpath='{.data.app-config-oidc\.yaml}' > "$oidc_tmp" - awk -v url="$rewrite_url" ' - BEGIN { done = 0 } - { - if (!done && $0 ~ /^[[:space:]]*url:/) { - match($0, /^[[:space:]]*/) - print substr($0, 1, RLENGTH) "url: " url - done = 1 - next - } - print - } - ' "$oidc_tmp" > "${oidc_tmp}.new" - mv "${oidc_tmp}.new" "$oidc_tmp" - oc create configmap app-config-oidc \ - --from-file=app-config-oidc.yaml="$oidc_tmp" \ - -n "$ns" --dry-run=client -o yaml | oc apply -f - >/dev/null - rm -f "$oidc_tmp" - oc rollout restart "deploy/redhat-developer-hub" -n "$ns" >/dev/null - oc rollout status "deploy/redhat-developer-hub" -n "$ns" --timeout=300s >/dev/null - log "data-index rewrite proxy ready (${rewrite_url})" + "${SCRIPT_DIR}/utils/orchestrator/ensure-dataindex-rewrite.sh" "$ns" } probe_raw_dataindex() { @@ -626,6 +517,9 @@ phase_deploy() { phase_test() { log "[test]" + if [[ "$namespace" != "orchestrator" ]]; then + die "OSL Playwright smoke requires --namespace orchestrator (overlays tests use Playwright project name as the k8s namespace)" + fi overlays_dir="$(cd "$overlays_dir" && pwd)" local e2e smoke_spec="" backup="" rc=0 allow_relative=false e2e="$(overlays_e2e_dir)" diff --git a/setup-orchestrator.sh b/setup-orchestrator.sh index e620979..89f347a 100755 --- a/setup-orchestrator.sh +++ b/setup-orchestrator.sh @@ -2,7 +2,8 @@ # # One-command setup of RHDH + orchestrator for overlays e2e. # Deploys Keycloak, installs orchestrator prerequisites, deploys RHDH via Helm, -# and verifies the shared existing-RHDH substrate contract. +# installs osl-di-rewrite in front of Data Index, and verifies the shared +# existing-RHDH substrate contract. # # Usage: # ./setup-orchestrator.sh [--namespace ] [--prepare-internal-osl ] @@ -373,20 +374,35 @@ if [[ -f "${SCRIPT_DIR}/.env.osl" ]]; then fi assert_empty_baseline "$namespace" "$KEYCLOAK_NAMESPACE" +route_scheme() { + local name="$1" ns="$2" + if oc get route "$name" -n "$ns" -o jsonpath='{.spec.tls.termination}' 2>/dev/null | grep -q .; then + echo https + else + echo http + fi +} + +rhdh_public_url() { + local ns="$1" + local host scheme + host="$(oc get route redhat-developer-hub -n "$ns" -o jsonpath='{.spec.host}' 2>/dev/null || true)" + [[ -n "$host" ]] || return 1 + scheme="$(route_scheme redhat-developer-hub "$ns")" + echo "${scheme}://${host}" +} + wait_for_rhdh_auth_and_orchestrator_ready() { local ns="$1" local timeout_secs="${2:-240}" - local start_time + local start_time rhdh_url start_time=$(date +%s) - - local rhdh_host - rhdh_host="$(oc get route redhat-developer-hub -n "$ns" -o jsonpath='{.spec.host}' 2>/dev/null || true)" - if [[ -z "$rhdh_host" ]]; then + rhdh_url="$(rhdh_public_url "$ns")" || { echo "Error: Could not resolve RHDH route in namespace '$ns'." return 1 - fi + } - log "Waiting for RHDH auth/backend HTTP readiness..." + log "Waiting for RHDH auth/backend HTTP readiness at ${rhdh_url}..." while true; do local elapsed auth_status auth_location app_health orch_health elapsed=$(( $(date +%s) - start_time )) @@ -399,13 +415,13 @@ wait_for_rhdh_auth_and_orchestrator_ready() { return 1 fi - auth_status=$(curl -sk -o /dev/null -w '%{http_code}' "https://${rhdh_host}/api/auth/oidc/start?env=production" || true) - auth_location=$(curl -sk -D - -o /dev/null "https://${rhdh_host}/api/auth/oidc/start?env=production" | \ + auth_status=$(curl -sk -o /dev/null -w '%{http_code}' "${rhdh_url}/api/auth/oidc/start?env=production" || true) + auth_location=$(curl -sk -D - -o /dev/null "${rhdh_url}/api/auth/oidc/start?env=production" | \ awk 'BEGIN{IGNORECASE=1} /^location:/ {print $2; exit}' | tr -d '\r') - app_health=$(curl -sk -o /dev/null -w '%{http_code}' "https://${rhdh_host}/api/app/health" || true) - orch_health=$(curl -sk -o /dev/null -w '%{http_code}' "https://${rhdh_host}/api/orchestrator/health" || true) + app_health=$(curl -sk -o /dev/null -w '%{http_code}' "${rhdh_url}/api/app/health" || true) + orch_health=$(curl -sk -o /dev/null -w '%{http_code}' "${rhdh_url}/api/orchestrator/health" || true) - if [[ "$app_health" == "200" && "$auth_status" == "302" && "$auth_location" =~ ^https:// && "$orch_health" == "200" ]]; then + if [[ "$app_health" == "200" && "$auth_status" == "302" && "$auth_location" =~ ^https?:// && "$orch_health" == "200" ]]; then log "RHDH auth/backend/orchestrator readiness checks passed." return 0 fi @@ -457,9 +473,13 @@ run_post_setup_workflow_smoke() { oc exec -n "$ns" deploy/sonataflow-platform-data-index-service -- \ curl -sf --max-time 5 "http://localhost:8080/q/health/ready" >/dev/null - local orchestrator_host orch_health - orchestrator_host="$(oc get route redhat-developer-hub -n "$ns" -o jsonpath='{.spec.host}' 2>/dev/null || true)" - orch_health="$(curl -sk -o /dev/null -w '%{http_code}' "https://${orchestrator_host}/api/orchestrator/health" || true)" + local orchestrator_url orch_health + orchestrator_url="$(rhdh_public_url "$ns")" || { + echo "Error: Could not resolve RHDH route for post-setup smoke." + rm -rf "$workflow_dir" + exit 1 + } + orch_health="$(curl -sk -o /dev/null -w '%{http_code}' "${orchestrator_url}/api/orchestrator/health" || true)" if [[ "$orch_health" != "200" ]]; then echo "Error: Post-smoke orchestrator health check failed (HTTP ${orch_health})." rm -rf "$workflow_dir" @@ -517,12 +537,11 @@ SKIP_ORCHESTRATOR_INFRA_INSTALL=1 \ ./deploy.sh helm "$version" --namespace "$namespace" --with-orchestrator phase_checkpoint "rhdh-deployed" -rhdh_host="$(oc get route redhat-developer-hub -n "$namespace" -o jsonpath='{.spec.host}' 2>/dev/null || true)" -if [[ -z "$rhdh_host" ]]; then +RHDH_BASE_URL="$(rhdh_public_url "$namespace")" || { echo "Error: Could not resolve RHDH route after deploy." exit 1 -fi -export RHDH_BASE_URL="https://${rhdh_host}" +} +export RHDH_BASE_URL if declare -F update_rhdh_client_redirects >/dev/null; then update_rhdh_client_redirects "$RHDH_BASE_URL" fi @@ -540,12 +559,14 @@ oc rollout status deployment/redhat-developer-hub -n "$namespace" --timeout=600s emit_diag_hints "$namespace" } wait_for_rhdh_auth_and_orchestrator_ready "$namespace" +log "Installing osl-di-rewrite in front of Data Index (SRVLOGIC-1137 relative serviceUrl)" +"${SCRIPT_DIR}/utils/orchestrator/ensure-dataindex-rewrite.sh" "$namespace" +wait_for_rhdh_auth_and_orchestrator_ready "$namespace" run_post_setup_workflow_smoke "$namespace" # ── Summary ────────────────────────────────────────────────────────────────── -rhdh_host="$(oc get route redhat-developer-hub -n "$namespace" -o jsonpath='{.spec.host}' 2>/dev/null || true)" -RHDH_URL="${RHDH_BASE_URL:-${rhdh_host:+https://${rhdh_host}}}" +RHDH_URL="${RHDH_BASE_URL}" KEYCLOAK_URL="${KEYCLOAK_BASE_URL:-}" echo "" diff --git a/utils/keycloak/keycloak-deploy.sh b/utils/keycloak/keycloak-deploy.sh index 448b3a6..ae9cecb 100755 --- a/utils/keycloak/keycloak-deploy.sh +++ b/utils/keycloak/keycloak-deploy.sh @@ -261,7 +261,8 @@ update_rhdh_client_redirects() { [ -z "$client_uuid" ] && echo "Error: rhdh-client UUID not found" && return 1 payload=$(api_call GET "$KEYCLOAK_URL/admin/realms/rhdh/clients/$client_uuid" "" "Get rhdh-client representation" | \ - jq -c --arg uri "$redirect" '.redirectUris = [$uri] | .webOrigins = [$uri] | .implicitFlowEnabled = false') + jq -c --arg uri "$redirect" --arg origin "${rhdh_url%/}" \ + '.redirectUris = [$uri] | .webOrigins = [$origin] | .implicitFlowEnabled = false') api_call PUT "$KEYCLOAK_URL/admin/realms/rhdh/clients/$client_uuid" "$payload" "Pin rhdh-client redirects" >/dev/null - echo "Pinned rhdh-client redirectUris/webOrigins to ${redirect}" + echo "Pinned rhdh-client redirectUris to ${redirect} webOrigins to ${rhdh_url%/}" } diff --git a/utils/orchestrator/ensure-dataindex-rewrite.sh b/utils/orchestrator/ensure-dataindex-rewrite.sh new file mode 100755 index 0000000..50b2239 --- /dev/null +++ b/utils/orchestrator/ensure-dataindex-rewrite.sh @@ -0,0 +1,110 @@ +#!/bin/bash +# +# Deploy osl-di-rewrite in front of Data Index and point app-config-oidc at it. +# Usage: ./utils/orchestrator/ensure-dataindex-rewrite.sh +# +set -euo pipefail + +ns="${1:-}" +[[ -n "$ns" ]] || { echo "Error: namespace required" >&2; exit 1; } + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +name="osl-di-rewrite" +rewrite_url="http://${name}.${ns}.svc.cluster.local" +js="${SCRIPT_DIR}/utils/orchestrator/osl-di-rewrite.js" +[[ -f "$js" ]] || { echo "Error: missing ${js}" >&2; exit 1; } + +image="$(oc get deploy redhat-developer-hub -n "$ns" -o jsonpath='{.spec.template.spec.containers[0].image}' 2>/dev/null || true)" +[[ -n "$image" ]] || { echo "Error: cannot resolve RHDH image for data-index rewrite proxy" >&2; exit 1; } + +echo "==> ensuring data-index rewrite proxy ${name} -> sonataflow-platform-data-index-service" +current_url="$(oc get configmap app-config-oidc -n "$ns" -o jsonpath='{.data.app-config-oidc\.yaml}' 2>/dev/null | awk '/url:/ {print $2; exit}')" +if oc get deploy "$name" -n "$ns" >/dev/null 2>&1 && [[ "$current_url" == "$rewrite_url" ]]; then + oc rollout status "deploy/${name}" -n "$ns" --timeout=180s >/dev/null + echo "==> data-index rewrite proxy already configured (${rewrite_url})" + exit 0 +fi +oc create configmap "$name" \ + --from-file=osl-di-rewrite.js="$js" \ + -n "$ns" --dry-run=client -o yaml | oc apply -f - >/dev/null +oc apply -f - >/dev/null </dev/null +oidc_tmp="$(mktemp)" +oc get configmap app-config-oidc -n "$ns" -o jsonpath='{.data.app-config-oidc\.yaml}' > "$oidc_tmp" +awk -v url="$rewrite_url" ' + BEGIN { done = 0 } + { + if (!done && $0 ~ /^[[:space:]]*url:/) { + match($0, /^[[:space:]]*/) + print substr($0, 1, RLENGTH) "url: " url + done = 1 + next + } + print + } +' "$oidc_tmp" > "${oidc_tmp}.new" +mv "${oidc_tmp}.new" "$oidc_tmp" +oc create configmap app-config-oidc \ + --from-file=app-config-oidc.yaml="$oidc_tmp" \ + -n "$ns" --dry-run=client -o yaml | oc apply -f - >/dev/null +rm -f "$oidc_tmp" +oc rollout restart "deploy/redhat-developer-hub" -n "$ns" >/dev/null +oc rollout status "deploy/redhat-developer-hub" -n "$ns" --timeout=300s >/dev/null +echo "==> data-index rewrite proxy ready (${rewrite_url})" From 77c9de9c3f5628fd2a2b85d54364817be13c02f6 Mon Sep 17 00:00:00 2001 From: Rostislav Lan Date: Thu, 20 Aug 2026 16:00:58 +0200 Subject: [PATCH 10/13] fix: wire next Orchestrator UI via Legacy and stop smoke aborts #13375 Orchestrator 6.x exposes OrchestratorPage on Legacy, not PluginRoot, so next dynamicRoutes must set module: Legacy. Also keep image mirror inspect from aborting under set -e and drop the demo_dir RETURN trap that unbound the scratch dir after workflows were ready. Co-authored-by: Cursor --- config/orchestrator-dynamic-plugins-next.yaml | 8 +++++++- helm/deploy.sh | 13 +++++++++++++ prepare-osl-internal.sh | 12 ++++++++---- run-osl-regression.sh | 3 +-- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/config/orchestrator-dynamic-plugins-next.yaml b/config/orchestrator-dynamic-plugins-next.yaml index 8e88ccd..79e58d1 100644 --- a/config/orchestrator-dynamic-plugins-next.yaml +++ b/config/orchestrator-dynamic-plugins-next.yaml @@ -5,13 +5,19 @@ plugins: dynamicPlugins: frontend: red-hat-developer-hub.backstage-plugin-orchestrator: - pluginModule: Alpha + # Orchestrator 6.x colocates NFS at PluginRoot (no Alpha module). + # RHDH next still serves packages/app unless APP_CONFIG_app_packageName=app-next. + # pluginModule only preloads a scalprum module; dynamicRoutes/appIcons + # default to PluginRoot, which does not export OrchestratorPage. + pluginModule: Legacy appIcons: - name: orchestratorIcon importName: OrchestratorIcon + module: Legacy dynamicRoutes: - path: /orchestrator importName: OrchestratorPage + module: Legacy menuItem: icon: orchestratorIcon text: Orchestrator diff --git a/helm/deploy.sh b/helm/deploy.sh index 1feba6e..e1b6596 100755 --- a/helm/deploy.sh +++ b/helm/deploy.sh @@ -147,6 +147,19 @@ if [[ "${WITH_ORCHESTRATOR}" == "1" ]]; then fi fi +# New Frontend System (NFS / app-next). Off by default: RHDH next 2.0 still +# serves packages/app unless these env vars are set, and overlay smoke locators +# are written for the legacy shell. Set ENABLE_RHDH_NFS=1 to opt in. +if [[ "${ENABLE_RHDH_NFS:-0}" == "1" ]]; then + echo "Enabling RHDH new frontend system (app-next + standard Module Federation)" + HELM_ARGS+=( + --set-string "upstream.backstage.extraEnvVars[4].name=APP_CONFIG_app_packageName" + --set-string "upstream.backstage.extraEnvVars[4].value=app-next" + --set-string "upstream.backstage.extraEnvVars[5].name=ENABLE_STANDARD_MODULE_FEDERATION" + --set-string "upstream.backstage.extraEnvVars[5].value=true" + ) +fi + if [[ "${IS_AUTH_ENABLED:-false}" != "true" ]]; then HELM_ARGS+=( --set "upstream.backstage.extraAppConfig[1].configMapRef=app-config-guest-auth" diff --git a/prepare-osl-internal.sh b/prepare-osl-internal.sh index 33fcb15..c5f535b 100755 --- a/prepare-osl-internal.sh +++ b/prepare-osl-internal.sh @@ -241,11 +241,15 @@ rewrite_osl_refs_in_dir() { # --------------------------------------------------------------------------- mirror_image() { local source_ref="$1" push_ref="$2" - local dest_digest src_digest + local dest_digest="" src_digest="" dest_json src_json - dest_digest="$(skopeo inspect --no-tags --tls-verify=false "docker://${push_ref}" 2>/dev/null | jq -r '.Digest // empty')" + if dest_json="$(skopeo inspect --no-tags --tls-verify=false "docker://${push_ref}" 2>/dev/null)"; then + dest_digest="$(printf '%s' "$dest_json" | jq -r '.Digest // empty')" + fi if [[ "$dest_digest" == sha256:* ]]; then - src_digest="$(skopeo inspect --no-tags --tls-verify=false "docker://${source_ref}" 2>/dev/null | jq -r '.Digest // empty')" + if src_json="$(skopeo inspect --no-tags --tls-verify=false "docker://${source_ref}" 2>/dev/null)"; then + src_digest="$(printf '%s' "$src_json" | jq -r '.Digest // empty')" + fi if [[ -n "$src_digest" && "$src_digest" == "$dest_digest" ]]; then log " already present (${dest_digest}), skipping copy" return 0 @@ -264,7 +268,7 @@ mirror_image() { local attempt=0 max_attempts=3 wait_secs=10 while (( attempt < max_attempts )); do attempt=$((attempt + 1)) - if skopeo "${skopeo_args[@]}" "docker://${source_ref}" "docker://${push_ref}" >/dev/null 2>&1; then + if skopeo "${skopeo_args[@]}" "docker://${source_ref}" "docker://${push_ref}"; then return 0 fi if (( attempt < max_attempts )); then diff --git a/run-osl-regression.sh b/run-osl-regression.sh index 3173b14..49e6073 100755 --- a/run-osl-regression.sh +++ b/run-osl-regression.sh @@ -280,8 +280,6 @@ ensure_token_propagation_workflow() { [[ -n "${KEYCLOAK_BASE_URL:-}" ]] || die "KEYCLOAK_BASE_URL is required for token-propagation smoke" log "deploying token-propagation workflow and sample-server" demo_dir="$(mktemp -d /tmp/osl-token-demo-XXXXXX)" - _osl_token_demo_cleanup() { rm -rf "$demo_dir"; trap - RETURN; } - trap _osl_token_demo_cleanup RETURN git clone --depth 1 "$DEMO_WORKFLOW_REPO" "$demo_dir" >/dev/null git -C "$demo_dir" fetch --depth 1 origin "$DEMO_WORKFLOW_REF" >/dev/null git -C "$demo_dir" checkout --detach "$DEMO_WORKFLOW_REF" >/dev/null @@ -358,6 +356,7 @@ EOF oc wait deployment/sample-server -n "$ns" --for=condition=Available --timeout=120s oc apply -n "$ns" -f "$manifests_dir" patch_smoke_workflow "$ns" token-propagation + rm -rf "$demo_dir" } ensure_smoke_workflows() { From e1ac82d3cc2d021c2cba29b767eae7e5c8b4fd2e Mon Sep 17 00:00:00 2001 From: Rostislav Lan Date: Wed, 2 Sep 2026 10:09:33 +0200 Subject: [PATCH 11/13] feat: align OSL smoke with NFS-only orchestrator-app-next #13375 Overlays e2e is NFS-only; drop Legacy routing, default to orchestrator-app-next, enable app-next via rhdh-secrets, and pin OIDC from GHCR with a supported resolver. Co-authored-by: Cursor --- CLAUDE.md | 4 +- Makefile | 4 +- README.md | 23 +++--- cleanup.sh | 7 +- config/app-config-oidc.yaml | 4 +- config/orchestrator-dynamic-plugins-next.yaml | 32 ++------ deploy.sh | 6 ++ helm/deploy.sh | 41 ++++++---- playwright/osl-regression-smoke.spec.ts | 81 ++----------------- prepare-osl-internal.sh | 4 +- resources/keycloak/dynamic-plugins.yaml | 2 +- run-osl-regression.sh | 46 +++++++---- scripts/setup-resources.sh | 17 +++- setup-orchestrator.sh | 4 +- utils/orchestrator/verify-existing-rhdh.sh | 2 +- 15 files changed, 122 insertions(+), 155 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 67fec8c..9959434 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -95,8 +95,8 @@ deploy.sh (entry point) ### Deployment flow 1. Keycloak is deployed first (Bitnami Helm chart) with an OIDC client (`rhdh-client`) and test users (`test1`/`test2`, password: `test1@123`/`test2@123`). -2. Environment variables (Keycloak URLs, credentials) are exported and substituted into `config/rhdh-secrets.yaml`. -3. `config/app-config-rhdh.yaml` configures RHDH with OIDC auth pointing to Keycloak and catalog entity locations from GitHub. +2. Environment variables (Keycloak URLs, credentials) are written into the `rhdh-secrets` Secret by `scripts/setup-resources.sh` (`create_rhdh_secrets`). For `next` / `*-CI`, that also sets NFS (`APP_CONFIG_app_packageName=app-next`, `ENABLE_STANDARD_MODULE_FEDERATION`). `config/rhdh-secrets.yaml` is a reference template only. +3. `config/app-config-oidc.yaml` (merged into the app-config ConfigMap) configures RHDH with OIDC auth pointing to Keycloak; catalog entity locations come from `config/app-config-rhdh.yaml`. 4. For Helm: the RHDH chart is installed with dynamic plugins config. For Operator: a Backstage CR is applied referencing the ConfigMaps. 5. When `ORCH=true`, orchestrator plugins are merged into `dynamic-plugins.yaml` and serverless operators are installed. diff --git a/Makefile b/Makefile index bc21de2..25bcb2b 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ USE_CONTAINER ?= false CATALOG_INDEX_TAG ?= RUNNER_IMAGE ?= quay.io/rhdh-community/rhdh-e2e-runner:main OSL_RELEASE ?= -ORCH_NAMESPACE ?= orchestrator +ORCH_NAMESPACE ?= orchestrator-app-next export CATALOG_INDEX_TAG @@ -89,7 +89,7 @@ cleanup: ## Clean RHDH/orchestrator/OSL resources and operators from ORCH_NAMESP cleanup-full: ## Full cleanup: operators + related namespaces ./cleanup.sh --namespace $(ORCH_NAMESPACE) --include-operators --delete-namespace -osl-regression: ## Cleanup + prepare OSL + deploy + 4-test smoke (VERSION, OSL_RELEASE; ORCH_NAMESPACE must be orchestrator) +osl-regression: ## Cleanup + prepare OSL + deploy + 4-test smoke (VERSION, OSL_RELEASE; ORCH_NAMESPACE must match overlays Playwright project, default orchestrator-app-next) ifndef OSL_RELEASE $(error OSL_RELEASE is required, e.g. make osl-regression VERSION=next OSL_RELEASE=1.39.0.CR1) endif diff --git a/README.md b/README.md index b7b6215..6005880 100644 --- a/README.md +++ b/README.md @@ -188,33 +188,33 @@ Pin an OSL pre-release against a chosen RHDH version, deploy, and run the defaul 3. `Rerun Failswitch from failure point` 4. `Execute token-propagation workflow via API` -Smoke always deploys greeting, failswitch, token-propagation, and `sample-server`, then runs token-propagation (JWT/OpenAPI into the workflow). `--test` requires `--namespace orchestrator` (the default): overlays Playwright uses the project name as the Kubernetes namespace for Data Index and Failswitch retrigger. `--cleanup` (and the cleanup phase of `--all`) always removes OSL/Serverless operators (`logic-operator` / `serverless-operator` only), the custom catalog, and the mirror namespace, and cleans the RHDH namespace contents. It does not delete a leftover `rhdh` namespace unless you pass `--delete-namespace` (`make cleanup-full`). Other operators in `openshift-operators` are left in place. +Smoke always deploys greeting, failswitch, token-propagation, and `sample-server`, then runs token-propagation (JWT/OpenAPI into the workflow). Overlays orchestrator e2e is **NFS-only** (`orchestrator-app-next`): `--test` requires `--namespace` to match that Playwright project (default `orchestrator-app-next`). Deploying `next` / `*-CI` always enables the app-next shell (`APP_CONFIG_app_packageName=app-next` + `ENABLE_STANDARD_MODULE_FEDERATION` on `rhdh-secrets`). Point `--overlays-dir` at an overlays checkout that includes the NFS lane. `--cleanup` (and the cleanup phase of `--all`) always removes OSL/Serverless operators (`logic-operator` / `serverless-operator` only), the custom catalog, and the mirror namespace, and cleans the RHDH namespace contents. It does not delete a leftover `rhdh` namespace unless you pass `--delete-namespace` (`make cleanup-full`). Other operators in `openshift-operators` are left in place. `make setup-orchestrator` (and the driver's `--deploy` phase) installs `osl-di-rewrite` in front of Data Index so OSL 1.39 relative `serviceUrl` values still work from RHDH. The GraphQL probe before Playwright still hits the **raw** Data Index (`sonataflow-platform-data-index-service`), not that proxy. OSL 1.39.CR1 can return a relative `ProcessDefinitions.serviceUrl` (SRVLOGIC-1137). The Orchestrator plugin then cannot `POST` to execute/abort/retrigger. The probe exits 2 on that unless you pass `--allow-relative-service-url` or `ALLOW_RELATIVE_SERVICE_URL=1`, which prints a warning and continues so the four tests can still run behind the rewrite proxy. Drop that override after the plugin derives `serviceUrl` from `endpoint`. ```bash # One-shot: full cleanup (including operators) -> mirror OSL -> deploy -> smoke -make osl-regression VERSION=next OSL_RELEASE=1.39.0.CR1 ORCH_NAMESPACE=orchestrator +make osl-regression VERSION=next OSL_RELEASE=1.39.0.CR1 ORCH_NAMESPACE=orchestrator-app-next # 1.39.CR1 currently needs the relative-serviceUrl override: -ALLOW_RELATIVE_SERVICE_URL=1 make osl-regression VERSION=next OSL_RELEASE=1.39.0.CR1 ORCH_NAMESPACE=orchestrator +ALLOW_RELATIVE_SERVICE_URL=1 make osl-regression VERSION=next OSL_RELEASE=1.39.0.CR1 # Or call the driver directly -./run-osl-regression.sh --all --rhdh next --osl-release 1.39.0.CR1 --namespace orchestrator -./run-osl-regression.sh --cleanup --namespace orchestrator +./run-osl-regression.sh --all --rhdh next --osl-release 1.39.0.CR1 +./run-osl-regression.sh --cleanup --namespace orchestrator-app-next ./run-osl-regression.sh --cleanup --prepare-osl --deploy --rhdh next --osl-release 1.39.0.CR1 -ALLOW_RELATIVE_SERVICE_URL=1 ./run-osl-regression.sh --test --namespace orchestrator -./run-osl-regression.sh --test --overlays-dir ../rhdh-plugin-export-overlays +ALLOW_RELATIVE_SERVICE_URL=1 ./run-osl-regression.sh --test --rhdh next +./run-osl-regression.sh --test --rhdh next --overlays-dir ../rhdh-plugin-export-overlays ``` Individual pieces: ```bash make prepare-osl OSL_RELEASE=1.39.0.CR1 -make setup-orchestrator VERSION=next ORCH_NAMESPACE=orchestrator OSL_RELEASE=1.39.0.CR1 -make cleanup-full ORCH_NAMESPACE=orchestrator +make setup-orchestrator VERSION=next ORCH_NAMESPACE=orchestrator-app-next OSL_RELEASE=1.39.0.CR1 +make cleanup-full ORCH_NAMESPACE=orchestrator-app-next ``` -Requires `oc` logged in, `helm`, `skopeo`, `podman`, and a sibling `rhdh-plugin-export-overlays` checkout for `--test`. Manifests live in `config/osl-releases/`. +Requires `oc` logged in, `helm`, `skopeo`, `podman`, and a sibling `rhdh-plugin-export-overlays` checkout for `--test` (NFS `orchestrator-app-next` project). Manifests live in `config/osl-releases/`. #### Status and Debugging @@ -238,7 +238,7 @@ All make commands accept these variables: | `CATALOG_INDEX_TAG` | auto | Catalog index image tag (defaults to major.minor from version, or `next`) | | `RUNNER_IMAGE` | `quay.io/rhdh-community/rhdh-e2e-runner:main` | Container image for `install-operator` | | `OSL_RELEASE` | _(empty)_ | OSL pre-release id for `prepare-osl` / `osl-regression` | -| `ORCH_NAMESPACE` | `orchestrator` | Namespace used by orchestrator/OSL setup and cleanup | +| `ORCH_NAMESPACE` | `orchestrator-app-next` | Namespace used by orchestrator/OSL setup and cleanup (NFS Playwright project) | | `ALLOW_RELATIVE_SERVICE_URL` | _(unset)_ | Set to `1` to continue smoke after a relative Data Index `serviceUrl` | > **Note:** `install-operator` requires you to be logged into the cluster via `oc login` on your host. @@ -413,6 +413,7 @@ rhdh-test-instance/ │ ├── app-config-rhdh.yaml # Main RHDH configuration (guest auth by default) │ ├── dynamic-plugins.yaml # Base dynamic plugins configuration │ ├── orchestrator-dynamic-plugins.yaml # Orchestrator plugins (merged when ORCH=true) +│ ├── orchestrator-dynamic-plugins-next.yaml # next/CI: NFS PluginRoot + OIDC auth module │ ├── osl-releases/ # Local OSL pre-release JSON (gitignored except example) │ ├── rbac-policies.yaml # RBAC policy ConfigMap │ └── rhdh-secrets.yaml # Reference template for rhdh-secrets Secret diff --git a/cleanup.sh b/cleanup.sh index f6909d0..43a2320 100755 --- a/cleanup.sh +++ b/cleanup.sh @@ -254,9 +254,12 @@ helm uninstall orch-infra -n orchestrator-infra 2>/dev/null || true # --------------------------------------------------------------------------- # 2. Clean namespaces created by orchestrator e2e tests # (rhdh-plugin-export-overlays/workspaces/orchestrator/e2e-tests) -# Tests deploy into "orchestrator" or "orchestrator-e2e" ns and Keycloak into -# "rhdh-keycloak" ns. +# Tests deploy into "orchestrator-app-next" (NFS) or older "orchestrator" / +# "orchestrator-e2e" namespaces, and Keycloak into "rhdh-keycloak". # --------------------------------------------------------------------------- +if [[ "$namespace" != "orchestrator-app-next" ]]; then + clean_namespace "orchestrator-app-next" +fi if [[ "$namespace" != "orchestrator" ]]; then clean_namespace "orchestrator" fi diff --git a/config/app-config-oidc.yaml b/config/app-config-oidc.yaml index fd87a47..04ead55 100644 --- a/config/app-config-oidc.yaml +++ b/config/app-config-oidc.yaml @@ -10,7 +10,9 @@ auth: callbackUrl: '${RHDH_BASE_URL}/api/auth/oidc/handler/frame' signIn: resolvers: - - resolver: preferredUsernameMatchingUserEntityName + # Upstream oidc-provider (GHCR pin) exposes emailLocalPart / emailMatching + # only. preferredUsernameMatchingUserEntityName is not in that module. + - resolver: emailLocalPartMatchingUserEntityName dangerouslyAllowSignInWithoutUserInCatalog: true guest: dangerouslyAllowOutsideDevelopment: false diff --git a/config/orchestrator-dynamic-plugins-next.yaml b/config/orchestrator-dynamic-plugins-next.yaml index 79e58d1..0ccc8ee 100644 --- a/config/orchestrator-dynamic-plugins-next.yaml +++ b/config/orchestrator-dynamic-plugins-next.yaml @@ -1,30 +1,14 @@ plugins: + # RHDH 2.0 disables the OIDC auth backend by default; smoke signs in via + # Keycloak OIDC (config/app-config-oidc.yaml). quay.io/rhdh {{inherit}} has no + # prior entry in dynamic-plugins.default.yaml for this package (InstallException). + # Use the public overlays OCI pin (same approach as overlays e2e for auth). + - package: 'oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-plugin-auth-backend-module-oidc-provider:bs_1.52.0__0.4.17' + disabled: false + # Orchestrator 6.x colocates NFS at Scalprum PluginRoot (PageBlueprint path + # /orchestrator). Do not set Legacy dynamicRoutes — they 404 under app-next. - package: 'oci://quay.io/rhdh/red-hat-developer-hub-backstage-plugin-orchestrator:{{inherit}}' disabled: false - pluginConfig: - dynamicPlugins: - frontend: - red-hat-developer-hub.backstage-plugin-orchestrator: - # Orchestrator 6.x colocates NFS at PluginRoot (no Alpha module). - # RHDH next still serves packages/app unless APP_CONFIG_app_packageName=app-next. - # pluginModule only preloads a scalprum module; dynamicRoutes/appIcons - # default to PluginRoot, which does not export OrchestratorPage. - pluginModule: Legacy - appIcons: - - name: orchestratorIcon - importName: OrchestratorIcon - module: Legacy - dynamicRoutes: - - path: /orchestrator - importName: OrchestratorPage - module: Legacy - menuItem: - icon: orchestratorIcon - text: Orchestrator - textKey: menuItem.orchestrator - menuItems: - orchestrator: - icon: orchestratorIcon - package: 'oci://quay.io/rhdh/red-hat-developer-hub-backstage-plugin-orchestrator-backend:{{inherit}}' disabled: false dependencies: diff --git a/deploy.sh b/deploy.sh index 16f2dab..e66da70 100755 --- a/deploy.sh +++ b/deploy.sh @@ -30,6 +30,12 @@ installation_method="$1" version="$2" shift 2 +# NFS (app-next) is required for next / *-CI. Overlays orchestrator e2e is +# NFS-only; keys land on rhdh-secrets (see scripts/setup-resources.sh). +if [[ "$version" == "next" || "$version" == *-CI ]]; then + export ENABLE_RHDH_NFS=1 + echo "NFS enabled (app-next + standard Module Federation via rhdh-secrets)" +fi # Parse optional flags while [[ $# -gt 0 ]]; do case "$1" in diff --git a/helm/deploy.sh b/helm/deploy.sh index e1b6596..8d2de69 100755 --- a/helm/deploy.sh +++ b/helm/deploy.sh @@ -62,11 +62,28 @@ append_to_dynamic_plugins_cm() { if [[ "${WITH_ORCHESTRATOR}" == "1" ]]; then current_dp="$(oc get configmap dynamic-plugins --namespace "$namespace" -o jsonpath='{.data.dynamic-plugins\.yaml}' 2>/dev/null || true)" - if [[ "$current_dp" != *plugin-orchestrator* ]]; then - orch_file="config/orchestrator-dynamic-plugins.yaml" - if [[ "$version" == "next" || "$version" == *-CI ]]; then - orch_file="config/orchestrator-dynamic-plugins-next.yaml" + orch_file="config/orchestrator-dynamic-plugins.yaml" + merge_orch=false + if [[ "$version" == "next" || "$version" == *-CI ]]; then + orch_file="config/orchestrator-dynamic-plugins-next.yaml" + # Always ensure NFS next plugins (OIDC re-enable + orchestrator) are + # present. Full deploy.sh resets the ConfigMap from + # config/dynamic-plugins.yaml; also catch Legacy leftovers if this + # script is re-run alone. + if [[ "$current_dp" == *pluginModule:\ Legacy* ]] || [[ "$current_dp" == *OrchestratorPage* ]]; then + echo "Replacing Legacy orchestrator plugin wiring with NFS next config..." + oc create configmap dynamic-plugins \ + --from-file=config/dynamic-plugins.yaml \ + --namespace "$namespace" --dry-run=client -o yaml \ + | oc apply -f - --namespace "$namespace" >/dev/null + merge_orch=true + elif [[ "$current_dp" != *auth-backend-module-oidc-provider* ]]; then + merge_orch=true fi + elif [[ "$current_dp" != *plugin-orchestrator* ]]; then + merge_orch=true + fi + if [[ "$merge_orch" == "true" ]]; then echo "Merging orchestrator plugins from ${orch_file} into dynamic-plugins ConfigMap..." append_to_dynamic_plugins_cm "$(cat "$orch_file")" fi @@ -147,19 +164,9 @@ if [[ "${WITH_ORCHESTRATOR}" == "1" ]]; then fi fi -# New Frontend System (NFS / app-next). Off by default: RHDH next 2.0 still -# serves packages/app unless these env vars are set, and overlay smoke locators -# are written for the legacy shell. Set ENABLE_RHDH_NFS=1 to opt in. -if [[ "${ENABLE_RHDH_NFS:-0}" == "1" ]]; then - echo "Enabling RHDH new frontend system (app-next + standard Module Federation)" - HELM_ARGS+=( - --set-string "upstream.backstage.extraEnvVars[4].name=APP_CONFIG_app_packageName" - --set-string "upstream.backstage.extraEnvVars[4].value=app-next" - --set-string "upstream.backstage.extraEnvVars[5].name=ENABLE_STANDARD_MODULE_FEDERATION" - --set-string "upstream.backstage.extraEnvVars[5].value=true" - ) -fi - +# NFS env for next/*-CI is set on rhdh-secrets by deploy.sh + setup-resources.sh +# (APP_CONFIG_app_packageName / ENABLE_STANDARD_MODULE_FEDERATION) and mounted +# via extraEnvVarsSecrets. Do not duplicate those keys as Helm extraEnvVars. if [[ "${IS_AUTH_ENABLED:-false}" != "true" ]]; then HELM_ARGS+=( --set "upstream.backstage.extraAppConfig[1].configMapRef=app-config-guest-auth" diff --git a/playwright/osl-regression-smoke.spec.ts b/playwright/osl-regression-smoke.spec.ts index 8c1e4dc..46b654c 100644 --- a/playwright/osl-regression-smoke.spec.ts +++ b/playwright/osl-regression-smoke.spec.ts @@ -1,93 +1,24 @@ // Smoke entry for run-osl-regression.sh (copied into overlays e2e tests/ at runtime). // Overlays orchestrator-workflow-core.tests.ts only exports a register function; // orchestrator.spec.ts beforeAll would reinstall operators / Helm-redeploy RHDH. -// NFS Alpha copy also drifts from e2e-utils locators. +// +// NFS lane (orchestrator-app-next) drives most UI via overlays OrchestratorPO. +// Grep still hits a few e2e-utils OrchestratorPage helpers that match "Run" +// without exact:true (ambiguous with "Run again"). // @ts-nocheck -import { test, expect } from "@red-hat-developer-hub/e2e-test-utils/test"; -import { OrchestratorPage } from "@red-hat-developer-hub/e2e-test-utils/pages"; +import { test } from "@red-hat-developer-hub/e2e-test-utils/test"; import { createDataIndexGuard, requireEnvVar } from "./support/utils/orchestrator-workflow-helpers.js"; import { registerOrchestratorCoreWorkflowTests } from "./specs/orchestrator-workflow-core.tests.js"; import { registerTokenPropagationWorkflowTests } from "./specs/orchestrator-token-propagation.tests.js"; -import { ORCHESTRATOR_COMPONENTS } from "./support/pages/orchestrator-obj.js"; - -ORCHESTRATOR_COMPONENTS.workflowsHeading = (page) => - page.getByRole("heading", { name: /Workflows|Workflow Orchestrator/ }); -ORCHESTRATOR_COMPONENTS.runButton = (page) => - page.getByRole("button", { name: "Run", exact: true }); - -OrchestratorPage.prototype.validateGreetingWorkflow = async function () { - const page = this.page; - await page.getByRole("tab", { name: /Workflows/ }).click(); - await expect( - page.getByRole("heading", { name: /Workflows|Workflow Orchestrator/ }), - ).toBeVisible(); - await expect(page.locator('input[aria-label="Filter"]')).toHaveAttribute( - "placeholder", - "Filter", - ); - for (const name of ["Name", "Workflow Status", "Actions"]) { - await expect( - page.getByRole("columnheader", { name, exact: true }), - ).toBeVisible(); - } - const row = page.locator('tr:has-text("Greeting workflow")'); - await expect(row.locator("td").nth(0)).toHaveText("Greeting workflow"); - await expect(row.locator("td").nth(1)).toHaveText("Available"); - await expect( - row.getByRole("button", { name: "Run", exact: true }).first(), - ).toBeVisible(); - await expect(row.getByRole("button", { name: "View runs" }).first()).toBeVisible(); -}; test.beforeEach(async ({ page }) => { const origGetByRole = page.getByRole.bind(page); page.getByRole = (role, options) => { - if (role === "button" && options && options.name === "Run") { + if (role === "button" && options && options.name === "Run" && !options.exact) { return origGetByRole(role, { ...options, exact: true }); } - if (role === "heading" && options && options.name === "Workflows") { - return origGetByRole(role, { - ...options, - name: /^(Workflows|Workflow Orchestrator)$/, - }); - } - if (role === "columnheader" && options && options.name === "Run Status") { - return origGetByRole(role, { name: /^(Run Status|Status)$/ }); - } - if (role === "columnheader" && options && options.name === "Duration") { - return origGetByRole(role, { name: /^(Duration|Version)$/ }); - } return origGetByRole(role, options); }; - - const origGetByText = page.getByText.bind(page); - page.getByText = (text, options) => { - if (text === "Run has aborted") { - return origGetByText(/Run (has|was) aborted/); - } - const loc = origGetByText(text, options); - if ( - options && - options.exact && - typeof text === "string" && - ["Completed", "Failed", "Running"].includes(text) - ) { - return loc.first(); - } - return loc; - }; - - const assertions = Object.getPrototypeOf(expect(page.locator("body"))); - if (assertions && !assertions.__oslPatchedToHaveText && assertions.toHaveText) { - const origToHaveText = assertions.toHaveText; - assertions.toHaveText = async function (expected, options) { - if (expected === "Workflows") { - expected = /^(Workflows|Workflow Orchestrator)$/; - } - return origToHaveText.call(this, expected, options); - }; - assertions.__oslPatchedToHaveText = true; - } }); const innerDataIndexGuard = createDataIndexGuard(); diff --git a/prepare-osl-internal.sh b/prepare-osl-internal.sh index c5f535b..c4085db 100755 --- a/prepare-osl-internal.sh +++ b/prepare-osl-internal.sh @@ -49,7 +49,7 @@ PULLER_GROUPS=( "system:serviceaccounts:openshift-serverless" "system:serviceaccounts:openshift-serverless-logic" ) -rhdh_namespace="orchestrator" +rhdh_namespace="orchestrator-app-next" # --------------------------------------------------------------------------- # Helpers @@ -65,7 +65,7 @@ Options: --release-manifest Explicit manifest JSON path (overrides --release lookup) --ocp-minor Override detected cluster version (e.g. 4.17) --mirror-namespace Internal registry project (default: osl-mirror) - --namespace RHDH namespace granted image-puller on the mirror (default: orchestrator) + --namespace RHDH namespace granted image-puller on the mirror (default: orchestrator-app-next) --multi-arch Mirror all architectures (default: amd64 only) -h, --help Show this help EOF diff --git a/resources/keycloak/dynamic-plugins.yaml b/resources/keycloak/dynamic-plugins.yaml index 3c50755..a7c601c 100644 --- a/resources/keycloak/dynamic-plugins.yaml +++ b/resources/keycloak/dynamic-plugins.yaml @@ -26,7 +26,7 @@ callbackUrl: '${RHDH_BASE_URL}/api/auth/oidc/handler/frame' signIn: resolvers: - - resolver: preferredUsernameMatchingUserEntityName + - resolver: emailLocalPartMatchingUserEntityName dangerouslyAllowSignInWithoutUserInCatalog: true guest: dangerouslyAllowOutsideDevelopment: false diff --git a/run-osl-regression.sh b/run-osl-regression.sh index 49e6073..fe0208b 100755 --- a/run-osl-regression.sh +++ b/run-osl-regression.sh @@ -7,7 +7,7 @@ # # Usage: # ./run-osl-regression.sh --all --rhdh next --osl-release 1.39.0.CR1 -# ./run-osl-regression.sh --cleanup --namespace orchestrator +# ./run-osl-regression.sh --cleanup --namespace orchestrator-app-next # ./run-osl-regression.sh --test --overlays-dir ../rhdh-plugin-export-overlays # set -euo pipefail @@ -25,6 +25,8 @@ RHDH_RELEASE="redhat-developer-hub" SMOKE_WRAPPER_SRC="${SCRIPT_DIR}/playwright/osl-regression-smoke.spec.ts" SMOKE_WRAPPER_NAME="osl-regression-smoke.spec.ts" SMOKE_GREP='Run Greeting workflow and verify Workflows tab|Run Failswitch workflow and verify statuses|Rerun Failswitch from failure point|Execute token-propagation workflow via API' +# Overlays NFS lane (upstream): Playwright project name == k8s namespace. +DEFAULT_NAMESPACE="orchestrator-app-next" WORKFLOW_REPO="${SERVERLESS_WORKFLOWS_REPO:-https://github.com/rhdhorchestrator/serverless-workflows.git}" WORKFLOW_REPO_REF="${SERVERLESS_WORKFLOWS_REF:-daeeee8dec16beab6d96a81774ef500081a2c2b0}" DEMO_WORKFLOW_REPO="${ORCHESTRATOR_DEMO_REPO:-https://github.com/rhdhorchestrator/orchestrator-demo.git}" @@ -41,7 +43,7 @@ allow_relative_service_url=false rhdh="" osl_release="" osl_manifest="" -namespace="orchestrator" +namespace="$DEFAULT_NAMESPACE" overlays_dir="$DEFAULT_OVERLAYS" usage() { @@ -56,13 +58,13 @@ Phases (any subset; always run in this order): cleanup, prepare-osl, deploy, tes --test Probe Data Index, then run the four Playwright smoke tests Options: - --rhdh RHDH version (required with --deploy / --all) + --rhdh RHDH version (required with --deploy / --test / --all) --osl-release Load config/osl-releases/.json --osl-manifest Explicit OSL manifest path - --namespace RHDH/orchestrator namespace (default: orchestrator). - --test requires orchestrator because overlays - Playwright uses the project name as the k8s ns. - --overlays-dir rhdh-plugin-export-overlays checkout + --namespace RHDH/orchestrator namespace (default: ${DEFAULT_NAMESPACE}). + --test requires the namespace to match the overlays + Playwright project (NFS: orchestrator-app-next). + --overlays-dir rhdh-plugin-export-overlays checkout (NFS lane) --allow-relative-service-url OSL 1.39 Data Index may return a relative ProcessDefinitions.serviceUrl (SRVLOGIC-1137). The GraphQL probe fails on that by default. @@ -116,6 +118,18 @@ overlays_e2e_dir() { echo "${overlays_dir}/workspaces/orchestrator/e2e-tests" } +# NFS Playwright project (upstream overlays). Namespace must match. +resolve_playwright_project() { + local cfg + cfg="$(overlays_e2e_dir)/playwright.config.ts" + [[ -f "$cfg" ]] || die "overlays playwright.config.ts not found: $cfg" + if grep -Eq 'name:[[:space:]]*["'\'']orchestrator-app-next["'\'']' "$cfg"; then + echo "orchestrator-app-next" + return + fi + die "overlays checkout lacks NFS Playwright project 'orchestrator-app-next' in $cfg (pass --overlays-dir to an NFS lane checkout)" +} + resolve_manifest() { if [[ -n "$osl_manifest" ]]; then echo "$osl_manifest" @@ -151,6 +165,7 @@ preflight() { fi if [[ "$run_test" == "true" ]]; then + [[ -n "$rhdh" ]] || die "--rhdh is required when --test is selected" require_cmd git local pkg pkg="$(overlays_e2e_dir)/package.json" @@ -507,21 +522,23 @@ phase_deploy() { # shellcheck disable=SC1091 source "${SCRIPT_DIR}/.env.osl" fi + # NFS env for next/*-CI is set inside deploy.sh before secrets/Helm. + # Data Index rewrite is installed by setup-orchestrator.sh after Helm. POST_SETUP_WORKFLOW_SMOKE=0 \ SKIP_EMPTY_BASELINE=1 \ ALLOW_OSL_SERVERLESS_VERSION_SKEW=1 \ "${SCRIPT_DIR}/setup-orchestrator.sh" "$rhdh" --namespace "$namespace" - ensure_dataindex_rewrite "$namespace" } phase_test() { log "[test]" - if [[ "$namespace" != "orchestrator" ]]; then - die "OSL Playwright smoke requires --namespace orchestrator (overlays tests use Playwright project name as the k8s namespace)" - fi overlays_dir="$(cd "$overlays_dir" && pwd)" - local e2e smoke_spec="" backup="" rc=0 allow_relative=false + local e2e smoke_spec="" backup="" rc=0 allow_relative=false pw_project e2e="$(overlays_e2e_dir)" + pw_project="$(resolve_playwright_project)" + if [[ "$namespace" != "$pw_project" ]]; then + die "OSL Playwright smoke requires --namespace ${pw_project} (overlays project name is the k8s namespace; got '${namespace}')" + fi ensure_e2e_deps "$e2e" export K8S_CLUSTER_ROUTER_BASE RHDH_BASE_URL KEYCLOAK_BASE_URL RHDH_VERSION @@ -537,7 +554,7 @@ phase_test() { K8S_CLUSTER_ROUTER_BASE="$(cluster_router_base)" RHDH_BASE_URL="$(route_url "$RHDH_RELEASE" "$namespace")" KEYCLOAK_BASE_URL="$(route_url "$KEYCLOAK_RELEASE" "$KEYCLOAK_NS" http)" - RHDH_VERSION="${rhdh}" + RHDH_VERSION="$rhdh" ensure_dataindex_rewrite "$namespace" backup="$(write_overlays_dotenv "$e2e")" @@ -560,10 +577,11 @@ phase_test() { local pw pw="$(playwright_cmd "$e2e")" log "Playwright: ${pw} (cwd=${e2e})" + log "Playwright project: ${pw_project}" log "Playwright grep: ${SMOKE_GREP}" set +e # shellcheck disable=SC2086 - (cd "$e2e" && $pw test --project=orchestrator --workers=1 --grep "$SMOKE_GREP" "$smoke_spec") + (cd "$e2e" && $pw test --project="$pw_project" --workers=1 --grep "$SMOKE_GREP" "$smoke_spec") rc=$? set -e diff --git a/scripts/setup-resources.sh b/scripts/setup-resources.sh index 6f599a6..b1f440a 100755 --- a/scripts/setup-resources.sh +++ b/scripts/setup-resources.sh @@ -34,6 +34,20 @@ ROOT_DIR="$(dirname "$DIR")" # Secret always has a consistent shape. Empty values are replaced by plugin # scripts as they configure their respective services. # ============================================================================= +# NFS keys on rhdh-secrets (mounted via extraEnvVarsSecrets). Required for +# next/*-CI — overlays orchestrator e2e is NFS-only (no legacy packages/app). +patch_nfs_secrets() { + if [[ "${ENABLE_RHDH_NFS:-0}" != "1" ]]; then + return 0 + fi + oc patch secret rhdh-secrets -n "${NAMESPACE}" --type=merge -p '{ + "stringData": { + "APP_CONFIG_app_packageName": "app-next", + "ENABLE_STANDARD_MODULE_FEDERATION": "true" + } + }' +} + create_rhdh_secrets() { echo "" echo "Creating rhdh-secrets Secret..." @@ -54,6 +68,7 @@ create_rhdh_secrets() { \"SONATAFLOW_DATA_INDEX_URL\": \"${SONATAFLOW_DATA_INDEX_URL:-}\" } }" + patch_nfs_secrets echo "rhdh-secrets already exists — updated URL/Keycloak/orchestrator keys." else # Generate a random session secret at deploy time so it is never hardcoded. @@ -73,7 +88,7 @@ create_rhdh_secrets() { --from-literal=LIGHTHOUSE_SVC_URL="${LIGHTHOUSE_SVC_URL:-}" \ --from-literal=SONATAFLOW_DATA_INDEX_URL="${SONATAFLOW_DATA_INDEX_URL:-}" \ --namespace="${NAMESPACE}" - + patch_nfs_secrets echo "rhdh-secrets created!" fi } diff --git a/setup-orchestrator.sh b/setup-orchestrator.sh index 89f347a..a6d25e9 100755 --- a/setup-orchestrator.sh +++ b/setup-orchestrator.sh @@ -16,7 +16,7 @@ # ./setup-orchestrator.sh 1.10 --prepare-internal-osl 1.39.0.CR1 # # Options: -# --namespace Target namespace (default: orchestrator) +# --namespace Target namespace (default: orchestrator-app-next) # --prepare-internal-osl # Mirror pre-release OSL images into the OpenShift internal # registry, generate a rewritten internal logic-only catalog, @@ -58,7 +58,7 @@ fi version="$1" shift -namespace="orchestrator" +namespace="orchestrator-app-next" prepare_internal_osl_release="" while [[ $# -gt 0 ]]; do case "$1" in diff --git a/utils/orchestrator/verify-existing-rhdh.sh b/utils/orchestrator/verify-existing-rhdh.sh index 9f13c31..9c5bd86 100755 --- a/utils/orchestrator/verify-existing-rhdh.sh +++ b/utils/orchestrator/verify-existing-rhdh.sh @@ -6,7 +6,7 @@ set -euo pipefail -namespace="orchestrator" +namespace="orchestrator-app-next" if [[ $# -gt 0 && "$1" != --* ]]; then namespace="$1" shift From 98e19150340dfd11b1b2449800b6bfddd7f8969a Mon Sep 17 00:00:00 2001 From: Rostislav Lan Date: Wed, 2 Sep 2026 14:35:06 +0200 Subject: [PATCH 12/13] fix: share smoke workflow deploy and probe osl-di-rewrite #13375 Deduplicate greeting/failswitch/token-propagation apply, stop sourcing keycloak-deploy.sh, probe GraphQL through the rewrite proxy, and simplify Helm NFS plugin merge. Co-authored-by: Cursor --- Makefile | 4 +- README.md | 8 +- config/osl-releases/README.md | 2 +- config/osl-releases/example.json | 2 +- helm/deploy.sh | 11 +- prepare-osl-internal.sh | 8 +- run-osl-regression.sh | 225 +--------------- setup-orchestrator.sh | 43 +--- utils/keycloak/keycloak-deploy.sh | 30 +-- .../keycloak/update-rhdh-client-redirects.sh | 69 +++++ utils/orchestrator/deploy-smoke-workflows.sh | 242 ++++++++++++++++++ 11 files changed, 343 insertions(+), 301 deletions(-) create mode 100755 utils/keycloak/update-rhdh-client-redirects.sh create mode 100755 utils/orchestrator/deploy-smoke-workflows.sh diff --git a/Makefile b/Makefile index 25bcb2b..d129f5d 100644 --- a/Makefile +++ b/Makefile @@ -83,10 +83,10 @@ endif setup-orchestrator: ## Full RHDH + orchestrator setup (VERSION, ORCH_NAMESPACE, OSL_RELEASE) ./setup-orchestrator.sh $(VERSION) --namespace $(ORCH_NAMESPACE) $(if $(filter-out ,$(OSL_RELEASE)),--prepare-internal-osl $(OSL_RELEASE)) -cleanup: ## Clean RHDH/orchestrator/OSL resources and operators from ORCH_NAMESPACE +cleanup: ## Empty ORCH_NAMESPACE; remove OSL/Serverless operators, knative, osl-mirror, leftover orchestrator ns. Does not delete ORCH_NAMESPACE. ./cleanup.sh --namespace $(ORCH_NAMESPACE) --include-operators -cleanup-full: ## Full cleanup: operators + related namespaces +cleanup-full: ## Same as cleanup, then delete ORCH_NAMESPACE ./cleanup.sh --namespace $(ORCH_NAMESPACE) --include-operators --delete-namespace osl-regression: ## Cleanup + prepare OSL + deploy + 4-test smoke (VERSION, OSL_RELEASE; ORCH_NAMESPACE must match overlays Playwright project, default orchestrator-app-next) diff --git a/README.md b/README.md index 6005880..9c35f0d 100644 --- a/README.md +++ b/README.md @@ -190,19 +190,17 @@ Pin an OSL pre-release against a chosen RHDH version, deploy, and run the defaul Smoke always deploys greeting, failswitch, token-propagation, and `sample-server`, then runs token-propagation (JWT/OpenAPI into the workflow). Overlays orchestrator e2e is **NFS-only** (`orchestrator-app-next`): `--test` requires `--namespace` to match that Playwright project (default `orchestrator-app-next`). Deploying `next` / `*-CI` always enables the app-next shell (`APP_CONFIG_app_packageName=app-next` + `ENABLE_STANDARD_MODULE_FEDERATION` on `rhdh-secrets`). Point `--overlays-dir` at an overlays checkout that includes the NFS lane. `--cleanup` (and the cleanup phase of `--all`) always removes OSL/Serverless operators (`logic-operator` / `serverless-operator` only), the custom catalog, and the mirror namespace, and cleans the RHDH namespace contents. It does not delete a leftover `rhdh` namespace unless you pass `--delete-namespace` (`make cleanup-full`). Other operators in `openshift-operators` are left in place. -`make setup-orchestrator` (and the driver's `--deploy` phase) installs `osl-di-rewrite` in front of Data Index so OSL 1.39 relative `serviceUrl` values still work from RHDH. The GraphQL probe before Playwright still hits the **raw** Data Index (`sonataflow-platform-data-index-service`), not that proxy. OSL 1.39.CR1 can return a relative `ProcessDefinitions.serviceUrl` (SRVLOGIC-1137). The Orchestrator plugin then cannot `POST` to execute/abort/retrigger. The probe exits 2 on that unless you pass `--allow-relative-service-url` or `ALLOW_RELATIVE_SERVICE_URL=1`, which prints a warning and continues so the four tests can still run behind the rewrite proxy. Drop that override after the plugin derives `serviceUrl` from `endpoint`. +`make setup-orchestrator` (and the driver's `--deploy` phase) installs `osl-di-rewrite` in front of Data Index so OSL 1.39 relative `serviceUrl` values still work from RHDH. The GraphQL probe before Playwright hits that rewrite proxy (`osl-di-rewrite`), not raw Data Index. OSL 1.39.CR1 can return a relative `ProcessDefinitions.serviceUrl` (SRVLOGIC-1137); the rewrite fills `serviceUrl` from `endpoint`. If the probe still sees a relative URL, it exits 2 unless you pass `--allow-relative-service-url` or `ALLOW_RELATIVE_SERVICE_URL=1`. Drop that override after the Orchestrator plugin derives `serviceUrl` from `endpoint`. ```bash # One-shot: full cleanup (including operators) -> mirror OSL -> deploy -> smoke make osl-regression VERSION=next OSL_RELEASE=1.39.0.CR1 ORCH_NAMESPACE=orchestrator-app-next -# 1.39.CR1 currently needs the relative-serviceUrl override: -ALLOW_RELATIVE_SERVICE_URL=1 make osl-regression VERSION=next OSL_RELEASE=1.39.0.CR1 # Or call the driver directly ./run-osl-regression.sh --all --rhdh next --osl-release 1.39.0.CR1 ./run-osl-regression.sh --cleanup --namespace orchestrator-app-next ./run-osl-regression.sh --cleanup --prepare-osl --deploy --rhdh next --osl-release 1.39.0.CR1 -ALLOW_RELATIVE_SERVICE_URL=1 ./run-osl-regression.sh --test --rhdh next +./run-osl-regression.sh --test --rhdh next ./run-osl-regression.sh --test --rhdh next --overlays-dir ../rhdh-plugin-export-overlays ``` @@ -239,7 +237,7 @@ All make commands accept these variables: | `RUNNER_IMAGE` | `quay.io/rhdh-community/rhdh-e2e-runner:main` | Container image for `install-operator` | | `OSL_RELEASE` | _(empty)_ | OSL pre-release id for `prepare-osl` / `osl-regression` | | `ORCH_NAMESPACE` | `orchestrator-app-next` | Namespace used by orchestrator/OSL setup and cleanup (NFS Playwright project) | -| `ALLOW_RELATIVE_SERVICE_URL` | _(unset)_ | Set to `1` to continue smoke after a relative Data Index `serviceUrl` | +| `ALLOW_RELATIVE_SERVICE_URL` | _(unset)_ | Set to `1` to continue smoke if the rewrite probe still sees a relative `serviceUrl` | > **Note:** `install-operator` requires you to be logged into the cluster via `oc login` on your host. > It automatically passes the session token to the e2e-runner container (needs Linux tools like `umoci`, `opm`, `skopeo`). diff --git a/config/osl-releases/README.md b/config/osl-releases/README.md index 07830ed..f2b1e3b 100755 --- a/config/osl-releases/README.md +++ b/config/osl-releases/README.md @@ -45,6 +45,6 @@ Notes: - `iib` must include the current cluster's `major.minor` version. - `images[].source` should be a full digest reference from the release email. - `iib[*]` should also be digest-pinned where possible (`...@sha256:...`). -- `images[].name` is a short identifier used as the internal registry repo name. +- `images[].name` is a short label for logs and to detect a `*bundle*` image. The internal registry repo name is derived from `source` (path after the last `/`, before `@`). - Set `ENFORCE_DIGEST_PINNING=1` to fail fast when non-digest references are present. - Manifest files are ignored by git by default (`config/osl-releases/*.json`), except `example.json`. diff --git a/config/osl-releases/example.json b/config/osl-releases/example.json index 1498f58..ef0197c 100755 --- a/config/osl-releases/example.json +++ b/config/osl-releases/example.json @@ -8,7 +8,7 @@ }, "images": [ { - "_comment": "Each entry is a container image referenced by the operator bundle. source is the digest ref from the release email, name is a short identifier used as the internal registry repo name.", + "_comment": "Each entry is a container image referenced by the operator bundle. source is the digest ref from the release email. name is a short label for logs and bundle detection; the internal registry repo name is derived from source.", "source": "registry-proxy.engineering.redhat.com/rh-osbs/openshift-serverless-1-logic-rhel9-operator@sha256:abcdef...", "name": "logic-rhel9-operator" }, diff --git a/helm/deploy.sh b/helm/deploy.sh index 8d2de69..cfa560b 100755 --- a/helm/deploy.sh +++ b/helm/deploy.sh @@ -66,19 +66,14 @@ if [[ "${WITH_ORCHESTRATOR}" == "1" ]]; then merge_orch=false if [[ "$version" == "next" || "$version" == *-CI ]]; then orch_file="config/orchestrator-dynamic-plugins-next.yaml" - # Always ensure NFS next plugins (OIDC re-enable + orchestrator) are - # present. Full deploy.sh resets the ConfigMap from - # config/dynamic-plugins.yaml; also catch Legacy leftovers if this - # script is re-run alone. - if [[ "$current_dp" == *pluginModule:\ Legacy* ]] || [[ "$current_dp" == *OrchestratorPage* ]]; then - echo "Replacing Legacy orchestrator plugin wiring with NFS next config..." + # Full deploy.sh reseeds this ConfigMap from config/dynamic-plugins.yaml. + # On a helm-only re-run, reset to that base when the NFS oidc pin is missing. + if [[ "$current_dp" != *auth-backend-module-oidc-provider* ]]; then oc create configmap dynamic-plugins \ --from-file=config/dynamic-plugins.yaml \ --namespace "$namespace" --dry-run=client -o yaml \ | oc apply -f - --namespace "$namespace" >/dev/null merge_orch=true - elif [[ "$current_dp" != *auth-backend-module-oidc-provider* ]]; then - merge_orch=true fi elif [[ "$current_dp" != *plugin-orchestrator* ]]; then merge_orch=true diff --git a/prepare-osl-internal.sh b/prepare-osl-internal.sh index c4085db..4774602 100755 --- a/prepare-osl-internal.sh +++ b/prepare-osl-internal.sh @@ -19,9 +19,6 @@ # setup-orchestrator.sh sources .env.osl and translates these into # --logic-operator-* flags for install-orchestrator.sh, which uses # LOGIC_OPERATOR_SOURCE, LOGIC_OPERATOR_STARTING_CSV, etc. -# -# The overlays e2e tests (workflow-deployment-helpers.ts) read -# ORCH_E2E_LOGIC_OPERATOR_* env vars that map 1:1 to the same flags. set -euo pipefail @@ -450,7 +447,7 @@ log "OCP: ${ocp_minor}" log "IIB: ${iib_source}" log "Images: ${#image_sources[@]}" log "Arch: $(if [[ "$multi_arch" == "true" ]]; then echo "multi"; else echo "amd64"; fi)" -log "Mode: rewrite-catalog (default)" +log "Mode: rewrite-catalog" if [[ "$iib_source" != *@sha256:* ]]; then if [[ "$ENFORCE_DIGEST_PINNING" == "1" ]]; then @@ -503,9 +500,8 @@ mirror_image "$iib_source" "${registry_host}/${mirror_namespace}/${iib_repo_name rewrite_operator_bundle_csv "$registry_host" # --------------------------------------------------------------------------- -# Hosted-compatible rewrite catalog path (default) +# Hosted-compatible rewrite catalog # --------------------------------------------------------------------------- -OSL_IIB_IMAGE="${INTERNAL_REGISTRY_SERVICE}/${mirror_namespace}/${iib_repo_name}:mirror" build_rewritten_logic_catalog "${registry_host}" "${registry_host}/${mirror_namespace}/${iib_repo_name}:mirror" # --------------------------------------------------------------------------- diff --git a/run-osl-regression.sh b/run-osl-regression.sh index fe0208b..dfdec3f 100755 --- a/run-osl-regression.sh +++ b/run-osl-regression.sh @@ -27,12 +27,6 @@ SMOKE_WRAPPER_NAME="osl-regression-smoke.spec.ts" SMOKE_GREP='Run Greeting workflow and verify Workflows tab|Run Failswitch workflow and verify statuses|Rerun Failswitch from failure point|Execute token-propagation workflow via API' # Overlays NFS lane (upstream): Playwright project name == k8s namespace. DEFAULT_NAMESPACE="orchestrator-app-next" -WORKFLOW_REPO="${SERVERLESS_WORKFLOWS_REPO:-https://github.com/rhdhorchestrator/serverless-workflows.git}" -WORKFLOW_REPO_REF="${SERVERLESS_WORKFLOWS_REF:-daeeee8dec16beab6d96a81774ef500081a2c2b0}" -DEMO_WORKFLOW_REPO="${ORCHESTRATOR_DEMO_REPO:-https://github.com/rhdhorchestrator/orchestrator-demo.git}" -DEMO_WORKFLOW_REF="${ORCHESTRATOR_DEMO_REF:-c6e59bab65bd584ede5fde7610bbc6187e70206c}" -SAMPLE_SERVER_IMAGE="${SAMPLE_SERVER_IMAGE:-quay.io/orchestrator/sample-server@sha256:67e694c65bdff0b256590ac32aaad1eeb2045ffbe6923b140d4e022acf8c8993}" -TOKEN_PROPAGATION_IMAGE="${TOKEN_PROPAGATION_IMAGE:-quay.io/orchestrator/demo-token-propagation@sha256:8b35f7aeafde48deed2700ab9bb247f77d1322d0a3c26005b51aaac782d55302}" run_all=false run_cleanup=false @@ -65,14 +59,11 @@ Options: --test requires the namespace to match the overlays Playwright project (NFS: orchestrator-app-next). --overlays-dir rhdh-plugin-export-overlays checkout (NFS lane) - --allow-relative-service-url OSL 1.39 Data Index may return a relative - ProcessDefinitions.serviceUrl (SRVLOGIC-1137). - The GraphQL probe fails on that by default. - This flag (or ALLOW_RELATIVE_SERVICE_URL=1) - warns and continues so Playwright can run - behind the osl-di-rewrite proxy. Drop this - after the Orchestrator plugin derives - serviceUrl from endpoint. + --allow-relative-service-url Continue if the osl-di-rewrite GraphQL probe + still sees a relative ProcessDefinitions.serviceUrl + (SRVLOGIC-1137). Default probe hits the rewrite + proxy, not raw Data Index. Also: + ALLOW_RELATIVE_SERVICE_URL=1 -h, --help Show this help EOF } @@ -201,200 +192,6 @@ route_url() { echo "${scheme}://${host}" } -csv_mm_for_package() { - local package="$1" - local version - version="$(oc get csv -n openshift-operators -o json 2>/dev/null | jq -r --arg p "$package" ' - .items[] - | select(.status.phase == "Succeeded") - | select((.spec.name == $p) or ((.metadata.name // "") | startswith($p + "."))) - | .spec.version // empty - ' | head -n 1)" - echo "$version" | grep -oE '^[0-9]+\.[0-9]+' || true -} - -workflow_osl_image_tag() { - local os_mm osl_mm chosen - os_mm="$(csv_mm_for_package serverless-operator)" - osl_mm="$(csv_mm_for_package logic-operator)" - if [[ -n "$os_mm" && -n "$osl_mm" ]]; then - if [[ "$(printf '%s\n%s\n' "$os_mm" "$osl_mm" | sort -V | head -n 1)" == "$os_mm" ]]; then - chosen="$os_mm" - else - chosen="$osl_mm" - fi - else - chosen="${os_mm:-${osl_mm:-1.37}}" - fi - echo "${chosen//./_}" -} - -patch_smoke_workflow() { - local ns="$1" name="$2" tag="${3:-}" image - case "$name" in - greeting) image="quay.io/orchestrator/serverless-workflow-greeting:osl_${tag}" ;; - failswitch) image="quay.io/orchestrator/fail-switch:osl_${tag}" ;; - token-propagation) image="${TOKEN_PROPAGATION_IMAGE}" ;; - *) die "unknown smoke workflow: $name" ;; - esac - oc -n "$ns" patch sonataflow "$name" --type merge -p "{ - \"spec\": { - \"persistence\": { - \"dbMigrationStrategy\": \"job\", - \"postgresql\": { - \"secretRef\": { - \"name\": \"backstage-psql-secret\", - \"userKey\": \"POSTGRES_USER\", - \"passwordKey\": \"POSTGRES_PASSWORD\" - }, - \"serviceRef\": { - \"name\": \"backstage-psql\", - \"namespace\": \"${ns}\", - \"databaseName\": \"backstage_plugin_orchestrator\", - \"databaseSchema\": \"${name}\" - } - } - }, - \"podTemplate\": { - \"container\": { - \"image\": \"${image}\", - \"env\": [{\"name\": \"KOGITO_SERVICE_URL\", \"value\": \"http://${name}.${ns}.svc.cluster.local\"}] - } - } - } - }" >/dev/null -} - -wait_smoke_workflows_ready() { - local ns="$1" timeout_secs="${2:-600}" start elapsed ready - start="$(date +%s)" - while true; do - ready=true - for name in greeting failswitch token-propagation; do - local replicas - replicas="$(oc get deployment "$name" -n "$ns" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || true)" - if [[ "$replicas" != "1" ]]; then - ready=false - fi - done - if [[ "$ready" == "true" ]]; then - log "smoke workflows greeting/failswitch/token-propagation are ready" - return 0 - fi - elapsed=$(( $(date +%s) - start )) - if (( elapsed >= timeout_secs )); then - die "timeout waiting for greeting/failswitch/token-propagation deployments in $ns" - fi - sleep 10 - done -} - -ensure_token_propagation_workflow() { - local ns="$1" - local demo_dir manifests_dir props_cm specs_cm - [[ -n "${KEYCLOAK_BASE_URL:-}" ]] || die "KEYCLOAK_BASE_URL is required for token-propagation smoke" - log "deploying token-propagation workflow and sample-server" - demo_dir="$(mktemp -d /tmp/osl-token-demo-XXXXXX)" - git clone --depth 1 "$DEMO_WORKFLOW_REPO" "$demo_dir" >/dev/null - git -C "$demo_dir" fetch --depth 1 origin "$DEMO_WORKFLOW_REF" >/dev/null - git -C "$demo_dir" checkout --detach "$DEMO_WORKFLOW_REF" >/dev/null - manifests_dir="${demo_dir}/09_token_propagation/manifests" - props_cm="${manifests_dir}/01-configmap_token-propagation-props.yaml" - specs_cm="${manifests_dir}/03-configmap_02-token-propagation-resources-specs.yaml" - [[ -f "$props_cm" && -f "$specs_cm" ]] || die "token-propagation manifests missing in $DEMO_WORKFLOW_REPO" - local kc_base realm client_id client_secret auth_server_url token_url sample_url - kc_base="${KEYCLOAK_BASE_URL%/}" - realm="${KEYCLOAK_REALM:-rhdh}" - client_id="${KEYCLOAK_CLIENT_ID:-rhdh-client}" - client_secret="${KEYCLOAK_CLIENT_SECRET:-rhdh-client-secret}" - auth_server_url="${kc_base}/realms/${realm}" - token_url="${auth_server_url}/protocol/openid-connect/token" - sample_url="http://sample-server-service.${ns}:8080" - sed -i \ - -e "s|http://example-kc-service.keycloak:8080/realms/quarkus|${auth_server_url}|g" \ - -e "s|client-id=quarkus-app|client-id=${client_id}|g" \ - -e "s|client-secret=lVGSvdaoDUem7lqeAnqXn1F92dCPbQea|client-secret=${client_secret}|g" \ - -e "s|http://sample-server-service.rhdh-operator|${sample_url}|g" \ - "$props_cm" - sed -i \ - -e "s|http://example-kc-service.keycloak:8080/realms/quarkus/protocol/openid-connect/token|${token_url}|g" \ - "$specs_cm" - oc apply -n "$ns" -f - </dev/null - git -C "$workflow_dir" fetch --depth 1 origin "$WORKFLOW_REPO_REF" >/dev/null - git -C "$workflow_dir" checkout --detach "$WORKFLOW_REPO_REF" >/dev/null - oc apply -n "$ns" -f "${workflow_dir}/workflows/greeting/manifests" - oc apply -n "$ns" -f "${workflow_dir}/workflows/fail-switch/src/main/resources/manifests" - rm -rf "$workflow_dir" - patch_smoke_workflow "$ns" greeting "$tag" - patch_smoke_workflow "$ns" failswitch "$tag" - ensure_token_propagation_workflow "$ns" - wait_smoke_workflows_ready "$ns" 600 - oc rollout restart "deploy/sonataflow-platform-data-index-service" -n "$ns" - oc rollout status "deploy/sonataflow-platform-data-index-service" -n "$ns" --timeout=180s -} - ensure_e2e_deps() { local e2e="$1" if [[ -d "${e2e}/node_modules" ]]; then @@ -483,15 +280,15 @@ ensure_dataindex_rewrite() { "${SCRIPT_DIR}/utils/orchestrator/ensure-dataindex-rewrite.sh" "$ns" } -probe_raw_dataindex() { +probe_dataindex() { local ns="$1" allow="$2" local body url json count problems body='{"query":"{ ProcessDefinitions { id serviceUrl endpoint } }"}' - url="http://sonataflow-platform-data-index-service.${ns}.svc.cluster.local/graphql" - log "probing raw Data Index GraphQL ProcessDefinitions.serviceUrl" + url="http://osl-di-rewrite.${ns}.svc.cluster.local/graphql" + log "probing Data Index GraphQL via osl-di-rewrite ProcessDefinitions.serviceUrl" json="$(oc exec -n "$ns" deploy/redhat-developer-hub -- \ curl -sS -X POST -H "Content-Type: application/json" -d "$body" "$url")" \ - || die "oc exec curl of Data Index GraphQL failed" + || die "oc exec curl of Data Index GraphQL (osl-di-rewrite) failed" if ! printf '%s' "$json" | jq -e . >/dev/null 2>&1; then die "Data Index did not return JSON: ${json:0:500}" fi @@ -566,11 +363,11 @@ phase_test() { } trap cleanup_test_artifacts EXIT - ensure_smoke_workflows "$namespace" + bash "${SCRIPT_DIR}/utils/orchestrator/deploy-smoke-workflows.sh" "$namespace" if [[ "$allow_relative_service_url" == "true" || "${ALLOW_RELATIVE_SERVICE_URL:-}" == "1" ]]; then allow_relative=true fi - probe_raw_dataindex "$namespace" "$allow_relative" + probe_dataindex "$namespace" "$allow_relative" smoke_spec="${e2e}/tests/${SMOKE_WRAPPER_NAME}" cp -a "$SMOKE_WRAPPER_SRC" "$smoke_spec" diff --git a/setup-orchestrator.sh b/setup-orchestrator.sh index a6d25e9..be2290f 100755 --- a/setup-orchestrator.sh +++ b/setup-orchestrator.sh @@ -328,8 +328,7 @@ assert_pre_release_install_state() { } prepare_keycloak() { - # shellcheck disable=SC1091 - source "$SCRIPT_DIR/utils/keycloak/keycloak-deploy.sh" "$KEYCLOAK_NAMESPACE" + bash "$SCRIPT_DIR/utils/keycloak/keycloak-deploy.sh" "$KEYCLOAK_NAMESPACE" } sync_keycloak_runtime_env() { @@ -438,55 +437,23 @@ run_post_setup_workflow_smoke() { return 0 fi - local workflow_repo="${SERVERLESS_WORKFLOWS_REPO:-https://github.com/rhdhorchestrator/serverless-workflows.git}" - local workflow_ref="${SERVERLESS_WORKFLOWS_REF:-daeeee8dec16beab6d96a81774ef500081a2c2b0}" - local workflow_dir="/tmp/serverless-workflows-${RANDOM}-${RANDOM}" - local greeting_manifest_dir="${workflow_dir}/workflows/greeting/manifests" - log "Running post-setup workflow smoke in namespace ${ns}..." - git clone --depth=1 "$workflow_repo" "$workflow_dir" >/dev/null 2>&1 - git -C "$workflow_dir" fetch --depth=1 origin "$workflow_ref" >/dev/null 2>&1 - git -C "$workflow_dir" checkout --detach "$workflow_ref" >/dev/null 2>&1 - - oc apply -n "$ns" -f "$greeting_manifest_dir" >/dev/null - oc patch sonataflow greeting -n "$ns" --type merge -p '{ - "spec": { - "persistence": { - "postgresql": { - "secretRef": { - "name": "backstage-psql-secret", - "userKey": "POSTGRES_USER", - "passwordKey": "POSTGRES_PASSWORD" - }, - "serviceRef": { - "name": "backstage-psql", - "namespace": "'"$ns"'", - "databaseName": "backstage_plugin_orchestrator" - } - } - } - } - }' >/dev/null - - oc rollout restart deployment/greeting -n "$ns" >/dev/null 2>&1 || true - oc rollout status deployment/greeting -n "$ns" --timeout=600s >/dev/null + bash "$SCRIPT_DIR/utils/orchestrator/deploy-smoke-workflows.sh" "$ns" greeting + oc exec -n "$ns" deploy/sonataflow-platform-data-index-service -- \ curl -sf --max-time 5 "http://localhost:8080/q/health/ready" >/dev/null local orchestrator_url orch_health orchestrator_url="$(rhdh_public_url "$ns")" || { echo "Error: Could not resolve RHDH route for post-setup smoke." - rm -rf "$workflow_dir" exit 1 } orch_health="$(curl -sk -o /dev/null -w '%{http_code}' "${orchestrator_url}/api/orchestrator/health" || true)" if [[ "$orch_health" != "200" ]]; then echo "Error: Post-smoke orchestrator health check failed (HTTP ${orch_health})." - rm -rf "$workflow_dir" exit 1 fi - rm -rf "$workflow_dir" phase_checkpoint "post-setup-workflow-smoke-passed" } @@ -542,9 +509,7 @@ RHDH_BASE_URL="$(rhdh_public_url "$namespace")" || { exit 1 } export RHDH_BASE_URL -if declare -F update_rhdh_client_redirects >/dev/null; then - update_rhdh_client_redirects "$RHDH_BASE_URL" -fi +bash "$SCRIPT_DIR/utils/keycloak/update-rhdh-client-redirects.sh" "$KEYCLOAK_NAMESPACE" "$RHDH_BASE_URL" # ── Verify overlays existing-RHDH contract ─────────────────────────────────── diff --git a/utils/keycloak/keycloak-deploy.sh b/utils/keycloak/keycloak-deploy.sh index ae9cecb..293e1d3 100755 --- a/utils/keycloak/keycloak-deploy.sh +++ b/utils/keycloak/keycloak-deploy.sh @@ -50,6 +50,11 @@ api_call() { return 1 } +if [[ "${BASH_SOURCE[0]}" != "$0" ]]; then + echo "Error: run $0; do not source it (see update-rhdh-client-redirects.sh for redirects)" >&2 + return 1 2>/dev/null || exit 1 +fi + # Validate JSON files exist and are valid [ ! -f "$CLIENT_FILE" ] && echo "Error: Client configuration file not found: $CLIENT_FILE" && exit 1 jq empty "$CLIENT_FILE" 2>/dev/null || { echo "Error: Invalid JSON in $CLIENT_FILE"; exit 1; } @@ -241,28 +246,3 @@ export KEYCLOAK_LOGIN_REALM="rhdh" export KEYCLOAK_METADATA_URL="$KEYCLOAK_URL/realms/rhdh" export KEYCLOAK_BASE_URL="$KEYCLOAK_URL" export KEYCLOAK_PROTOCOL - -update_rhdh_client_redirects() { - local rhdh_url="${1:-}" - local redirect client_uuid payload token_response - [[ -n "$rhdh_url" ]] || { echo "Error: RHDH URL required to pin Keycloak redirects"; return 1; } - redirect="${rhdh_url%/}/api/auth/oidc/handler/frame" - - token_response=$(curl -sk -w "\n%{http_code}" -X POST "$KEYCLOAK_URL/realms/master/protocol/openid-connect/token" \ - -d "username=admin&password=admin123&grant_type=password&client_id=admin-cli") - TOKEN_HTTP_CODE=$(echo "$token_response" | tail -1) - TOKEN_BODY=$(echo "$token_response" | sed '$d') - [ "$TOKEN_HTTP_CODE" -ge 400 ] && echo "Error: Failed to refresh admin token (HTTP $TOKEN_HTTP_CODE): $TOKEN_BODY" && return 1 - ADMIN_TOKEN=$(echo "$TOKEN_BODY" | jq -r '.access_token // empty') - [ -z "$ADMIN_TOKEN" ] && echo "Error: Failed to parse refreshed admin token" && return 1 - - client_uuid=$(api_call GET "$KEYCLOAK_URL/admin/realms/rhdh/clients?clientId=rhdh-client" "" "Get rhdh-client" | \ - jq -r '.[0].id // empty') - [ -z "$client_uuid" ] && echo "Error: rhdh-client UUID not found" && return 1 - - payload=$(api_call GET "$KEYCLOAK_URL/admin/realms/rhdh/clients/$client_uuid" "" "Get rhdh-client representation" | \ - jq -c --arg uri "$redirect" --arg origin "${rhdh_url%/}" \ - '.redirectUris = [$uri] | .webOrigins = [$origin] | .implicitFlowEnabled = false') - api_call PUT "$KEYCLOAK_URL/admin/realms/rhdh/clients/$client_uuid" "$payload" "Pin rhdh-client redirects" >/dev/null - echo "Pinned rhdh-client redirectUris to ${redirect} webOrigins to ${rhdh_url%/}" -} diff --git a/utils/keycloak/update-rhdh-client-redirects.sh b/utils/keycloak/update-rhdh-client-redirects.sh new file mode 100755 index 0000000..ec95e03 --- /dev/null +++ b/utils/keycloak/update-rhdh-client-redirects.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# Pin rhdh-client redirectUris and webOrigins to the live RHDH URL. +# Usage: update-rhdh-client-redirects.sh +set -euo pipefail + +command -v jq >/dev/null 2>&1 || { echo "Error: jq is required" >&2; exit 1; } +command -v oc >/dev/null 2>&1 || { echo "Error: oc is required" >&2; exit 1; } + +NAMESPACE="${1:-}" +RHDH_URL="${2:-}" +[[ -n "$NAMESPACE" && -n "$RHDH_URL" ]] || { + echo "Usage: $0 " >&2 + exit 1 +} + +if oc get route console -n openshift-console -o=jsonpath='{.spec.tls.termination}' 2>/dev/null | grep -q .; then + KEYCLOAK_PROTOCOL="https" +else + KEYCLOAK_PROTOCOL="http" +fi +host="$(oc get route keycloak -n "$NAMESPACE" -o jsonpath='{.spec.host}' 2>/dev/null || true)" +[[ -n "$host" ]] || { echo "Error: Keycloak route not found in $NAMESPACE" >&2; exit 1; } +KEYCLOAK_URL="${KEYCLOAK_PROTOCOL}://${host}" +redirect="${RHDH_URL%/}/api/auth/oidc/handler/frame" + +api_call() { + local method=$1 url=$2 data=$3 description=$4 + local RESPONSE HTTP_CODE BODY + if [ -n "$data" ]; then + RESPONSE=$(curl -sk -w "\n%{http_code}" -X "$method" "$url" \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + -H "Content-Type: application/json" \ + -d "$data") + else + RESPONSE=$(curl -sk -w "\n%{http_code}" -X "$method" "$url" \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + -H "Content-Type: application/json") + fi + HTTP_CODE=$(echo "$RESPONSE" | tail -1) + BODY=$(echo "$RESPONSE" | sed '$d') + if [ "$method" = "GET" ] || [ "$HTTP_CODE" -lt 400 ]; then + echo "$BODY" + return 0 + fi + if [ "$HTTP_CODE" = "409" ]; then + echo "$BODY" + return 0 + fi + echo "Error: $description failed (HTTP $HTTP_CODE): $BODY" >&2 + return 1 +} + +token_response=$(curl -sk -w "\n%{http_code}" -X POST "$KEYCLOAK_URL/realms/master/protocol/openid-connect/token" \ + -d "username=admin&password=admin123&grant_type=password&client_id=admin-cli") +TOKEN_HTTP_CODE=$(echo "$token_response" | tail -1) +TOKEN_BODY=$(echo "$token_response" | sed '$d') +[ "$TOKEN_HTTP_CODE" -ge 400 ] && echo "Error: Failed to refresh admin token (HTTP $TOKEN_HTTP_CODE): $TOKEN_BODY" >&2 && exit 1 +ADMIN_TOKEN=$(echo "$TOKEN_BODY" | jq -r '.access_token // empty') +[ -z "$ADMIN_TOKEN" ] && echo "Error: Failed to parse refreshed admin token" >&2 && exit 1 + +client_uuid=$(api_call GET "$KEYCLOAK_URL/admin/realms/rhdh/clients?clientId=rhdh-client" "" "Get rhdh-client" | \ + jq -r '.[0].id // empty') +[ -z "$client_uuid" ] && echo "Error: rhdh-client UUID not found" >&2 && exit 1 + +payload=$(api_call GET "$KEYCLOAK_URL/admin/realms/rhdh/clients/$client_uuid" "" "Get rhdh-client representation" | \ + jq -c --arg uri "$redirect" --arg origin "${RHDH_URL%/}" \ + '.redirectUris = [$uri] | .webOrigins = [$origin] | .implicitFlowEnabled = false') +api_call PUT "$KEYCLOAK_URL/admin/realms/rhdh/clients/$client_uuid" "$payload" "Pin rhdh-client redirects" >/dev/null +echo "Pinned rhdh-client redirectUris to ${redirect} webOrigins to ${RHDH_URL%/}" diff --git a/utils/orchestrator/deploy-smoke-workflows.sh b/utils/orchestrator/deploy-smoke-workflows.sh new file mode 100755 index 0000000..7ec54dd --- /dev/null +++ b/utils/orchestrator/deploy-smoke-workflows.sh @@ -0,0 +1,242 @@ +#!/bin/bash +# +# Deploy OSL smoke SonataFlow workloads into a namespace. +# Usage: deploy-smoke-workflows.sh [greeting] [failswitch] [token-propagation] +# Default workflows: greeting failswitch token-propagation +# +set -euo pipefail + +log() { echo "==> $*"; } +die() { echo "Error: $*" >&2; exit 1; } + +ns="${1:-}" +[[ -n "$ns" ]] || die "namespace required" +shift || true + +if [[ $# -eq 0 ]]; then + set -- greeting failswitch token-propagation +fi + +WORKFLOW_REPO="${SERVERLESS_WORKFLOWS_REPO:-https://github.com/rhdhorchestrator/serverless-workflows.git}" +WORKFLOW_REPO_REF="${SERVERLESS_WORKFLOWS_REF:-daeeee8dec16beab6d96a81774ef500081a2c2b0}" +DEMO_WORKFLOW_REPO="${ORCHESTRATOR_DEMO_REPO:-https://github.com/rhdhorchestrator/orchestrator-demo.git}" +DEMO_WORKFLOW_REF="${ORCHESTRATOR_DEMO_REF:-c6e59bab65bd584ede5fde7610bbc6187e70206c}" +SAMPLE_SERVER_IMAGE="${SAMPLE_SERVER_IMAGE:-quay.io/orchestrator/sample-server@sha256:67e694c65bdff0b256590ac32aaad1eeb2045ffbe6923b140d4e022acf8c8993}" +TOKEN_PROPAGATION_IMAGE="${TOKEN_PROPAGATION_IMAGE:-quay.io/orchestrator/demo-token-propagation@sha256:8b35f7aeafde48deed2700ab9bb247f77d1322d0a3c26005b51aaac782d55302}" + +want_greeting=false +want_failswitch=false +want_token=false +for name in "$@"; do + case "$name" in + greeting) want_greeting=true ;; + failswitch) want_failswitch=true ;; + token-propagation) want_token=true ;; + *) die "unknown smoke workflow: $name" ;; + esac +done + +csv_mm_for_package() { + local package="$1" + local version + version="$(oc get csv -n openshift-operators -o json 2>/dev/null | jq -r --arg p "$package" ' + .items[] + | select(.status.phase == "Succeeded") + | select((.spec.name == $p) or ((.metadata.name // "") | startswith($p + "."))) + | .spec.version // empty + ' | head -n 1)" + echo "$version" | grep -oE '^[0-9]+\.[0-9]+' || true +} + +workflow_osl_image_tag() { + local os_mm osl_mm chosen + os_mm="$(csv_mm_for_package serverless-operator)" + osl_mm="$(csv_mm_for_package logic-operator)" + if [[ -n "$os_mm" && -n "$osl_mm" ]]; then + if [[ "$(printf '%s\n%s\n' "$os_mm" "$osl_mm" | sort -V | head -n 1)" == "$os_mm" ]]; then + chosen="$os_mm" + else + chosen="$osl_mm" + fi + else + chosen="${os_mm:-${osl_mm:-1.37}}" + fi + echo "${chosen//./_}" +} + +patch_smoke_workflow() { + local name="$1" tag="${2:-}" image + case "$name" in + greeting) image="quay.io/orchestrator/serverless-workflow-greeting:osl_${tag}" ;; + failswitch) image="quay.io/orchestrator/fail-switch:osl_${tag}" ;; + token-propagation) image="${TOKEN_PROPAGATION_IMAGE}" ;; + *) die "unknown smoke workflow: $name" ;; + esac + oc -n "$ns" patch sonataflow "$name" --type merge -p "{ + \"spec\": { + \"persistence\": { + \"dbMigrationStrategy\": \"job\", + \"postgresql\": { + \"secretRef\": { + \"name\": \"backstage-psql-secret\", + \"userKey\": \"POSTGRES_USER\", + \"passwordKey\": \"POSTGRES_PASSWORD\" + }, + \"serviceRef\": { + \"name\": \"backstage-psql\", + \"namespace\": \"${ns}\", + \"databaseName\": \"backstage_plugin_orchestrator\", + \"databaseSchema\": \"${name}\" + } + } + }, + \"podTemplate\": { + \"container\": { + \"image\": \"${image}\", + \"env\": [{\"name\": \"KOGITO_SERVICE_URL\", \"value\": \"http://${name}.${ns}.svc.cluster.local\"}] + } + } + } + }" >/dev/null +} + +wait_named_workflows_ready() { + local timeout_secs="$1" + shift + local -a names=("$@") + local start elapsed ready name replicas + start="$(date +%s)" + while true; do + ready=true + for name in "${names[@]}"; do + replicas="$(oc get deployment "$name" -n "$ns" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || true)" + if [[ "$replicas" != "1" ]]; then + ready=false + fi + done + if [[ "$ready" == "true" ]]; then + log "smoke workflows ready: ${names[*]}" + return 0 + fi + elapsed=$(( $(date +%s) - start )) + if (( elapsed >= timeout_secs )); then + die "timeout waiting for workflow deployments in $ns: ${names[*]}" + fi + sleep 10 + done +} + +ensure_token_propagation_workflow() { + local demo_dir manifests_dir props_cm specs_cm + [[ -n "${KEYCLOAK_BASE_URL:-}" ]] || die "KEYCLOAK_BASE_URL is required for token-propagation smoke" + log "deploying token-propagation workflow and sample-server" + demo_dir="$(mktemp -d /tmp/osl-token-demo-XXXXXX)" + git clone --depth 1 "$DEMO_WORKFLOW_REPO" "$demo_dir" >/dev/null + git -C "$demo_dir" fetch --depth 1 origin "$DEMO_WORKFLOW_REF" >/dev/null + git -C "$demo_dir" checkout --detach "$DEMO_WORKFLOW_REF" >/dev/null + manifests_dir="${demo_dir}/09_token_propagation/manifests" + props_cm="${manifests_dir}/01-configmap_token-propagation-props.yaml" + specs_cm="${manifests_dir}/03-configmap_02-token-propagation-resources-specs.yaml" + [[ -f "$props_cm" && -f "$specs_cm" ]] || die "token-propagation manifests missing in $DEMO_WORKFLOW_REPO" + local kc_base realm client_id client_secret auth_server_url token_url sample_url + kc_base="${KEYCLOAK_BASE_URL%/}" + realm="${KEYCLOAK_REALM:-rhdh}" + client_id="${KEYCLOAK_CLIENT_ID:-rhdh-client}" + client_secret="${KEYCLOAK_CLIENT_SECRET:-rhdh-client-secret}" + auth_server_url="${kc_base}/realms/${realm}" + token_url="${auth_server_url}/protocol/openid-connect/token" + sample_url="http://sample-server-service.${ns}:8080" + sed -i \ + -e "s|http://example-kc-service.keycloak:8080/realms/quarkus|${auth_server_url}|g" \ + -e "s|client-id=quarkus-app|client-id=${client_id}|g" \ + -e "s|client-secret=lVGSvdaoDUem7lqeAnqXn1F92dCPbQea|client-secret=${client_secret}|g" \ + -e "s|http://sample-server-service.rhdh-operator|${sample_url}|g" \ + "$props_cm" + sed -i \ + -e "s|http://example-kc-service.keycloak:8080/realms/quarkus/protocol/openid-connect/token|${token_url}|g" \ + "$specs_cm" + oc apply -n "$ns" -f - </dev/null + git -C "$workflow_dir" fetch --depth 1 origin "$WORKFLOW_REPO_REF" >/dev/null + git -C "$workflow_dir" checkout --detach "$WORKFLOW_REPO_REF" >/dev/null + if [[ "$want_greeting" == "true" ]]; then + oc apply -n "$ns" -f "${workflow_dir}/workflows/greeting/manifests" + patch_smoke_workflow greeting "$tag" + ready_names+=(greeting) + fi + if [[ "$want_failswitch" == "true" ]]; then + oc apply -n "$ns" -f "${workflow_dir}/workflows/fail-switch/src/main/resources/manifests" + patch_smoke_workflow failswitch "$tag" + ready_names+=(failswitch) + fi + rm -rf "$workflow_dir" +fi + +if [[ "$want_token" == "true" ]]; then + ensure_token_propagation_workflow + ready_names+=(token-propagation) +fi + +wait_named_workflows_ready 600 "${ready_names[@]}" +oc rollout restart "deploy/sonataflow-platform-data-index-service" -n "$ns" >/dev/null 2>&1 || true +oc rollout status "deploy/sonataflow-platform-data-index-service" -n "$ns" --timeout=180s From 73f877b04022d0494a458a4ecd9cefc698e10592 Mon Sep 17 00:00:00 2001 From: Rostislav Lan Date: Wed, 2 Sep 2026 15:41:43 +0200 Subject: [PATCH 13/13] refactor: dedupe OSL smoke scripts and extract shared helpers - Share Keycloak REST helpers via utils/keycloak/lib.sh - Use rhdh-e2e-test-utils verify-existing-rhdh.sh; remove local copy - Add utils/shell (common, openshift, workspace) and orchestrator probe/assert scripts - Deduplicate cleanup namespace lists, Playwright env, and route URL helpers Co-authored-by: Cursor --- Makefile | 2 +- README.md | 14 +- cleanup.sh | 64 +++--- prepare-osl-internal.sh | 18 +- run-osl-regression.sh | 135 +++--------- setup-orchestrator.sh | 202 ++---------------- utils/keycloak/keycloak-deploy.sh | 82 ++----- utils/keycloak/lib.sh | 96 +++++++++ .../keycloak/update-rhdh-client-redirects.sh | 59 ++--- utils/orchestrator/assert-osl-operators.sh | 121 +++++++++++ utils/orchestrator/deploy-smoke-workflows.sh | 5 +- utils/orchestrator/probe-dataindex-rewrite.sh | 50 +++++ utils/orchestrator/verify-existing-rhdh.sh | 98 --------- utils/shell/common.sh | 10 + utils/shell/openshift.sh | 62 ++++++ utils/shell/workspace.sh | 10 + 16 files changed, 486 insertions(+), 542 deletions(-) create mode 100644 utils/keycloak/lib.sh create mode 100644 utils/orchestrator/assert-osl-operators.sh create mode 100644 utils/orchestrator/probe-dataindex-rewrite.sh delete mode 100755 utils/orchestrator/verify-existing-rhdh.sh create mode 100644 utils/shell/common.sh create mode 100644 utils/shell/openshift.sh create mode 100644 utils/shell/workspace.sh diff --git a/Makefile b/Makefile index d129f5d..38c49bc 100644 --- a/Makefile +++ b/Makefile @@ -78,7 +78,7 @@ prepare-osl: ## Mirror pre-release OSL images (OSL_RELEASE=1.39.0.CR1) ifndef OSL_RELEASE $(error OSL_RELEASE is required, e.g. make prepare-osl OSL_RELEASE=1.39.0.CR1) endif - ./prepare-osl-internal.sh --release $(OSL_RELEASE) + ./prepare-osl-internal.sh --release $(OSL_RELEASE) --namespace $(ORCH_NAMESPACE) setup-orchestrator: ## Full RHDH + orchestrator setup (VERSION, ORCH_NAMESPACE, OSL_RELEASE) ./setup-orchestrator.sh $(VERSION) --namespace $(ORCH_NAMESPACE) $(if $(filter-out ,$(OSL_RELEASE)),--prepare-internal-osl $(OSL_RELEASE)) diff --git a/README.md b/README.md index 9c35f0d..d0e9a8d 100644 --- a/README.md +++ b/README.md @@ -440,8 +440,18 @@ rhdh-test-instance/ │ ├── config-keycloak-plugin.sh # Keycloak deploy, realm/client/user setup │ └── config-lighthouse-plugin.sh # Lighthouse deploy and URL injection ├── utils/ -│ ├── keycloak/ # Shared Keycloak deploy used by setup-orchestrator -│ └── orchestrator/ # Data Index rewrite proxy and existing-RHDH checks +│ ├── shell/ +│ │ ├── common.sh # log, die, require_cmd +│ │ ├── openshift.sh # oc login, namespace validation, route helpers +│ │ └── workspace.sh # resolve_workspace_dir +│ ├── keycloak/ +│ │ ├── lib.sh # Shared Keycloak REST + runtime env helpers +│ │ ├── keycloak-deploy.sh +│ │ └── update-rhdh-client-redirects.sh +│ └── orchestrator/ +│ ├── assert-osl-operators.sh # OSL operator subscription/CSV asserts +│ ├── probe-dataindex-rewrite.sh # Data Index GraphQL probe via osl-di-rewrite +│ └── deploy-smoke-workflows.sh ├── cleanup.sh # Orchestrator/OSL teardown (operators optional) ├── deploy.sh # Main deploy entry point ├── prepare-osl-internal.sh # Mirror pre-release OSL into the internal registry diff --git a/cleanup.sh b/cleanup.sh index 43a2320..87d92b1 100755 --- a/cleanup.sh +++ b/cleanup.sh @@ -18,6 +18,10 @@ set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 +source "${SCRIPT_DIR}/utils/shell/openshift.sh" + namespace="rhdh" include_operators=false delete_namespace=false @@ -45,17 +49,10 @@ while [[ $# -gt 0 ]]; do done # Verify cluster connectivity -if ! oc whoami &>/dev/null; then - echo "Error: Cannot connect to OpenShift cluster. Is CRC running and are you logged in?" - echo " Try: crc start && oc login -u kubeadmin https://api.crc.testing:6443" - exit 1 -fi +require_oc_login # Validate namespace -if [[ ! "$namespace" =~ ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ ]]; then - echo "Error: Invalid namespace name: '$namespace' (must be lowercase alphanumeric/hyphens, 1-63 chars)" - exit 1 -fi +validate_k8s_namespace "$namespace" echo "===========================================" echo " RHDH / Orchestrator Cleanup" @@ -65,6 +62,28 @@ echo "Include operators: $include_operators" echo "Delete namespace: $delete_namespace" echo "" +# Orchestrator e2e namespaces (NFS + legacy lanes) and shared Keycloak. +RELATED_NAMESPACES=( + orchestrator-app-next + orchestrator + orchestrator-e2e + rhdh-keycloak +) + +# Operator-created and mirror namespaces removed with --include-operators. +OPERATOR_NAMESPACES=( + knative-serving + knative-eventing + knative-serving-ingress + openshift-serverless + openshift-serverless-logic + orchestrator-infra + orchestrator + orchestrator-e2e + rhdh-keycloak + osl-mirror +) + # --------------------------------------------------------------------------- # Helper: clean RHDH/orchestrator resources from a given namespace # --------------------------------------------------------------------------- @@ -188,9 +207,7 @@ post_cleanup_verify() { echo "--- Post-clean verification ---" if [[ "$include_operators" == "true" ]]; then - for ns in knative-serving knative-eventing knative-serving-ingress \ - openshift-serverless openshift-serverless-logic orchestrator-infra \ - orchestrator orchestrator-e2e rhdh-keycloak osl-mirror; do + for ns in "${OPERATOR_NAMESPACES[@]}"; do if oc get namespace "$ns" &>/dev/null; then echo " Remaining namespace: $ns" failures=1 @@ -254,21 +271,12 @@ helm uninstall orch-infra -n orchestrator-infra 2>/dev/null || true # --------------------------------------------------------------------------- # 2. Clean namespaces created by orchestrator e2e tests # (rhdh-plugin-export-overlays/workspaces/orchestrator/e2e-tests) -# Tests deploy into "orchestrator-app-next" (NFS) or older "orchestrator" / -# "orchestrator-e2e" namespaces, and Keycloak into "rhdh-keycloak". # --------------------------------------------------------------------------- -if [[ "$namespace" != "orchestrator-app-next" ]]; then - clean_namespace "orchestrator-app-next" -fi -if [[ "$namespace" != "orchestrator" ]]; then - clean_namespace "orchestrator" -fi -if [[ "$namespace" != "orchestrator-e2e" ]]; then - clean_namespace "orchestrator-e2e" -fi -if [[ "$namespace" != "rhdh-keycloak" ]]; then - clean_namespace "rhdh-keycloak" -fi +for ns in "${RELATED_NAMESPACES[@]}"; do + if [[ "$namespace" != "$ns" ]]; then + clean_namespace "$ns" + fi +done # --------------------------------------------------------------------------- # 3. Cluster-scoped: operators and related resources @@ -298,9 +306,7 @@ if [[ "$include_operators" == "true" ]]; then # All related namespaces (operator-created + alternative deployment patterns) echo "--- Removing operator and related namespaces ---" - for ns in knative-serving knative-eventing knative-serving-ingress \ - openshift-serverless openshift-serverless-logic orchestrator-infra \ - orchestrator orchestrator-e2e rhdh-keycloak osl-mirror; do + for ns in "${OPERATOR_NAMESPACES[@]}"; do oc delete project "$ns" --ignore-not-found --timeout=60s 2>/dev/null || true wait_for_namespace_gone "$ns" 120 done diff --git a/prepare-osl-internal.sh b/prepare-osl-internal.sh index 4774602..d34b764 100755 --- a/prepare-osl-internal.sh +++ b/prepare-osl-internal.sh @@ -23,6 +23,10 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 +source "${SCRIPT_DIR}/utils/shell/common.sh" +source "${SCRIPT_DIR}/utils/shell/openshift.sh" + RELEASES_DIR="${SCRIPT_DIR}/config/osl-releases" ENV_OSL_FILE="${SCRIPT_DIR}/.env.osl" @@ -68,18 +72,6 @@ Options: EOF } -log() { echo "==> $*"; } - -die() { echo "Error: $*" >&2; exit 1; } - -require_cmd() { - command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" -} - -ensure_cluster_access() { - oc whoami >/dev/null 2>&1 || die "Cannot reach OpenShift cluster. Run: oc login " -} - detect_ocp_minor() { local full full="$(oc get clusterversion version -o jsonpath='{.status.desired.version}' 2>/dev/null || true)" @@ -413,7 +405,7 @@ for cmd in oc podman skopeo jq; do require_cmd "$cmd" done -ensure_cluster_access +require_oc_login "Cannot reach OpenShift cluster. Run: oc login " [[ -z "$ocp_minor" ]] && ocp_minor="$(detect_ocp_minor)" [[ "$ocp_minor" =~ ^[0-9]+\.[0-9]+$ ]] || die "invalid --ocp-minor '$ocp_minor' (expected e.g. 4.17)" diff --git a/run-osl-regression.sh b/run-osl-regression.sh index dfdec3f..12865bd 100755 --- a/run-osl-regression.sh +++ b/run-osl-regression.sh @@ -13,10 +13,11 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -_git_common="$(cd "$SCRIPT_DIR" && git rev-parse --git-common-dir 2>/dev/null)" -_main_repo_root="$(cd "$SCRIPT_DIR" && cd "$_git_common/.." 2>/dev/null && pwd)" -WORKSPACE_DIR="$(dirname "${_main_repo_root:-$SCRIPT_DIR}")" -unset _git_common _main_repo_root +# shellcheck disable=SC1091 +source "${SCRIPT_DIR}/utils/shell/common.sh" +source "${SCRIPT_DIR}/utils/shell/workspace.sh" +source "${SCRIPT_DIR}/utils/shell/openshift.sh" +WORKSPACE_DIR="$(resolve_workspace_dir "$SCRIPT_DIR")" DEFAULT_OVERLAYS="${WORKSPACE_DIR}/rhdh-plugin-export-overlays" KEYCLOAK_NS="rhdh-keycloak" @@ -68,13 +69,6 @@ Options: EOF } -log() { echo "==> $*"; } -die() { echo "Error: $*" >&2; exit 1; } - -require_cmd() { - command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" -} - while [[ $# -gt 0 ]]; do case "$1" in --all) run_all=true; shift ;; @@ -137,7 +131,7 @@ preflight() { require_cmd oc require_cmd helm require_cmd jq - oc whoami >/dev/null 2>&1 || die "oc whoami failed; log into a cluster first" + require_oc_login "oc whoami failed; log into a cluster first" if [[ "$run_prepare" == "true" ]]; then require_cmd podman @@ -168,30 +162,6 @@ preflight() { fi } -cluster_router_base() { - local domain - domain="$(oc get ingresses.config/cluster -o jsonpath='{.spec.domain}' 2>/dev/null || true)" - if [[ -n "$domain" ]]; then - echo "$domain" - return - fi - local host - host="$(oc get route console -n openshift-console -o jsonpath='{.spec.host}' 2>/dev/null || true)" - [[ "$host" == *.* ]] || die "could not discover cluster router base" - echo "${host#*.}" -} - -route_url() { - local name="$1" ns="$2" default_scheme="${3:-https}" - local host tls scheme - host="$(oc get route "$name" -n "$ns" -o jsonpath='{.spec.host}' 2>/dev/null || true)" - [[ -n "$host" ]] || die "route $name in $ns has no host" - tls="$(oc get route "$name" -n "$ns" -o jsonpath='{.spec.tls.termination}' 2>/dev/null || true)" - scheme="$default_scheme" - [[ -n "$tls" ]] && scheme="https" - echo "${scheme}://${host}" -} - ensure_e2e_deps() { local e2e="$1" if [[ -d "${e2e}/node_modules" ]]; then @@ -221,6 +191,22 @@ playwright_cmd() { echo "yarn playwright" } +populate_osl_playwright_env() { + export K8S_CLUSTER_ROUTER_BASE="$(openshift_cluster_router_base)" + export RHDH_BASE_URL="$(openshift_route_url "$RHDH_RELEASE" "$namespace")" + export KEYCLOAK_BASE_URL="$(openshift_route_url "$KEYCLOAK_RELEASE" "$KEYCLOAK_NS" http)" + export RHDH_VERSION="$rhdh" + export SKIP_KEYCLOAK_DEPLOYMENT=true + export SKIP_OPERATOR_INSTALLATION=true + export NAME_SPACE="$namespace" + export GH_USER_ID=test1 + export GH_USER_PASS=test1@123 + export KEYCLOAK_REALM=rhdh + export KEYCLOAK_LOGIN_REALM=rhdh + export KEYCLOAK_CLIENT_ID=rhdh-client + export KEYCLOAK_CLIENT_SECRET=rhdh-client-secret +} + write_overlays_dotenv() { local e2e="$1" local path="${e2e}/.env" @@ -233,16 +219,16 @@ write_overlays_dotenv() { K8S_CLUSTER_ROUTER_BASE=${K8S_CLUSTER_ROUTER_BASE} RHDH_BASE_URL=${RHDH_BASE_URL} RHDH_VERSION=${RHDH_VERSION:-} -NAME_SPACE=${namespace} -SKIP_KEYCLOAK_DEPLOYMENT=true -SKIP_OPERATOR_INSTALLATION=true -GH_USER_ID=test1 -GH_USER_PASS=test1@123 +NAME_SPACE=${NAME_SPACE} +SKIP_KEYCLOAK_DEPLOYMENT=${SKIP_KEYCLOAK_DEPLOYMENT} +SKIP_OPERATOR_INSTALLATION=${SKIP_OPERATOR_INSTALLATION} +GH_USER_ID=${GH_USER_ID} +GH_USER_PASS=${GH_USER_PASS} KEYCLOAK_BASE_URL=${KEYCLOAK_BASE_URL} -KEYCLOAK_REALM=rhdh -KEYCLOAK_LOGIN_REALM=rhdh -KEYCLOAK_CLIENT_ID=rhdh-client -KEYCLOAK_CLIENT_SECRET=rhdh-client-secret +KEYCLOAK_REALM=${KEYCLOAK_REALM} +KEYCLOAK_LOGIN_REALM=${KEYCLOAK_LOGIN_REALM} +KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID} +KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET} EOF echo "$backup" } @@ -275,52 +261,12 @@ phase_prepare() { "${SCRIPT_DIR}/prepare-osl-internal.sh" "${args[@]}" } -ensure_dataindex_rewrite() { - local ns="$1" - "${SCRIPT_DIR}/utils/orchestrator/ensure-dataindex-rewrite.sh" "$ns" -} - -probe_dataindex() { - local ns="$1" allow="$2" - local body url json count problems - body='{"query":"{ ProcessDefinitions { id serviceUrl endpoint } }"}' - url="http://osl-di-rewrite.${ns}.svc.cluster.local/graphql" - log "probing Data Index GraphQL via osl-di-rewrite ProcessDefinitions.serviceUrl" - json="$(oc exec -n "$ns" deploy/redhat-developer-hub -- \ - curl -sS -X POST -H "Content-Type: application/json" -d "$body" "$url")" \ - || die "oc exec curl of Data Index GraphQL (osl-di-rewrite) failed" - if ! printf '%s' "$json" | jq -e . >/dev/null 2>&1; then - die "Data Index did not return JSON: ${json:0:500}" - fi - if printf '%s' "$json" | jq -e '.errors != null and (.errors | length) > 0' >/dev/null; then - printf '%s\n' "$json" | jq '.errors' >&2 - die "Data Index GraphQL returned errors" - fi - count="$(printf '%s' "$json" | jq '.data.ProcessDefinitions | length // 0')" - if [[ "$count" -eq 0 ]]; then - printf '%s\n' '{"ok":false,"problems":[{"id":null,"serviceUrl":null,"endpoint":null,"reason":"no-process-definitions"}]}' >&2 - exit 1 - fi - problems="$(printf '%s' "$json" | jq '[.data.ProcessDefinitions[] | select((.serviceUrl | type != "string") or ((.serviceUrl | startswith("http://") or startswith("https://")) | not)) | {id, serviceUrl, endpoint, reason: "relative-or-missing-serviceUrl"}]')" - if [[ "$(printf '%s' "$problems" | jq 'length')" -gt 0 ]]; then - printf '%s\n' "$problems" | jq '{ok:false, problems:.}' >&2 - if [[ "$allow" == "true" ]]; then - log "WARNING: relative/missing serviceUrl allowed by ALLOW_RELATIVE_SERVICE_URL" - return 0 - fi - exit 2 - fi - printf '%s\n' '{"ok":true,"problems":[]}' >&2 -} - phase_deploy() { log "[deploy] RHDH ${rhdh} namespace=${namespace}" if [[ -f "${SCRIPT_DIR}/.env.osl" ]]; then # shellcheck disable=SC1091 source "${SCRIPT_DIR}/.env.osl" fi - # NFS env for next/*-CI is set inside deploy.sh before secrets/Helm. - # Data Index rewrite is installed by setup-orchestrator.sh after Helm. POST_SETUP_WORKFLOW_SMOKE=0 \ SKIP_EMPTY_BASELINE=1 \ ALLOW_OSL_SERVERLESS_VERSION_SKEW=1 \ @@ -338,21 +284,8 @@ phase_test() { fi ensure_e2e_deps "$e2e" - export K8S_CLUSTER_ROUTER_BASE RHDH_BASE_URL KEYCLOAK_BASE_URL RHDH_VERSION - export SKIP_KEYCLOAK_DEPLOYMENT=true - export SKIP_OPERATOR_INSTALLATION=true - export NAME_SPACE="$namespace" - export GH_USER_ID=test1 - export GH_USER_PASS=test1@123 - export KEYCLOAK_REALM=rhdh - export KEYCLOAK_LOGIN_REALM=rhdh - export KEYCLOAK_CLIENT_ID=rhdh-client - export KEYCLOAK_CLIENT_SECRET=rhdh-client-secret - K8S_CLUSTER_ROUTER_BASE="$(cluster_router_base)" - RHDH_BASE_URL="$(route_url "$RHDH_RELEASE" "$namespace")" - KEYCLOAK_BASE_URL="$(route_url "$KEYCLOAK_RELEASE" "$KEYCLOAK_NS" http)" - RHDH_VERSION="$rhdh" - ensure_dataindex_rewrite "$namespace" + populate_osl_playwright_env + "${SCRIPT_DIR}/utils/orchestrator/ensure-dataindex-rewrite.sh" "$namespace" backup="$(write_overlays_dotenv "$e2e")" cleanup_test_artifacts() { @@ -367,7 +300,7 @@ phase_test() { if [[ "$allow_relative_service_url" == "true" || "${ALLOW_RELATIVE_SERVICE_URL:-}" == "1" ]]; then allow_relative=true fi - probe_dataindex "$namespace" "$allow_relative" + bash "${SCRIPT_DIR}/utils/orchestrator/probe-dataindex-rewrite.sh" "$namespace" "$allow_relative" smoke_spec="${e2e}/tests/${SMOKE_WRAPPER_NAME}" cp -a "$SMOKE_WRAPPER_SRC" "$smoke_spec" diff --git a/setup-orchestrator.sh b/setup-orchestrator.sh index be2290f..6513ae2 100755 --- a/setup-orchestrator.sh +++ b/setup-orchestrator.sh @@ -30,15 +30,16 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# Resolve the parent workspace directory: the main repo root's parent, even from a worktree. -_git_common="$(cd "$SCRIPT_DIR" && git rev-parse --git-common-dir 2>/dev/null)" -_main_repo_root="$(cd "$SCRIPT_DIR" && cd "$_git_common/.." 2>/dev/null && pwd)" -WORKSPACE_DIR="$(dirname "${_main_repo_root:-$SCRIPT_DIR}")" +# shellcheck disable=SC1091 +source "${SCRIPT_DIR}/utils/shell/common.sh" +source "${SCRIPT_DIR}/utils/shell/workspace.sh" +source "${SCRIPT_DIR}/utils/shell/openshift.sh" +source "${SCRIPT_DIR}/utils/keycloak/lib.sh" +source "${SCRIPT_DIR}/utils/orchestrator/assert-osl-operators.sh" +WORKSPACE_DIR="$(resolve_workspace_dir "$SCRIPT_DIR")" RHDH_E2E_TEST_UTILS_DIR="${RHDH_E2E_TEST_UTILS_DIR:-${WORKSPACE_DIR}/rhdh-e2e-test-utils}" -unset _git_common _main_repo_root SHARED_INSTALL_SCRIPT="${RHDH_E2E_TEST_UTILS_DIR}/dist/deployment/orchestrator/install-orchestrator.sh" -LOCAL_VERIFY_EXISTING_RHDH_SCRIPT="${SCRIPT_DIR}/utils/orchestrator/verify-existing-rhdh.sh" -SHARED_VERIFY_EXISTING_RHDH_SCRIPT="${SHARED_VERIFY_EXISTING_RHDH_SCRIPT:-$LOCAL_VERIFY_EXISTING_RHDH_SCRIPT}" +SHARED_VERIFY_EXISTING_RHDH_SCRIPT="${SHARED_VERIFY_EXISTING_RHDH_SCRIPT:-${RHDH_E2E_TEST_UTILS_DIR}/dist/deployment/orchestrator/verify-existing-rhdh.sh}" KEYCLOAK_NAMESPACE="${KEYCLOAK_NAMESPACE:-rhdh-keycloak}" # ── Argument parsing ───────────────────────────────────────────────────────── @@ -81,16 +82,8 @@ cd "$SCRIPT_DIR" # ── Validate inputs ────────────────────────────────────────────────────────── -if ! oc whoami &>/dev/null; then - echo "Error: Cannot connect to OpenShift cluster. Is CRC running and are you logged in?" - echo " Try: crc start && oc login -u kubeadmin https://api.crc.testing:6443" - exit 1 -fi - -if [[ ! "$namespace" =~ ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ ]]; then - echo "Error: Invalid namespace name: '$namespace' (must be lowercase alphanumeric/hyphens, 1-63 chars)" - exit 1 -fi +require_oc_login +validate_k8s_namespace "$namespace" assert_empty_baseline() { local ns="$1" @@ -160,7 +153,6 @@ assert_empty_baseline() { # ── Helpers ────────────────────────────────────────────────────────────────── -log() { echo "==> $*"; } log_debug() { echo "[DEBUG $(date -u '+%Y-%m-%dT%H:%M:%SZ')] $*"; } phase_checkpoint() { echo "[CHECKPOINT] $*"; } @@ -174,7 +166,7 @@ emit_diag_hints() { } ensure_shared_scripts() { - if [[ ! -x "$SHARED_INSTALL_SCRIPT" ]]; then + if [[ ! -x "$SHARED_INSTALL_SCRIPT" || ! -x "$SHARED_VERIFY_EXISTING_RHDH_SCRIPT" ]]; then if [[ -f "${RHDH_E2E_TEST_UTILS_DIR}/package.json" ]]; then log "Building shared rhdh-e2e-test-utils artifacts..." (cd "$RHDH_E2E_TEST_UTILS_DIR" && yarn build >/dev/null) @@ -186,7 +178,7 @@ ensure_shared_scripts() { fi if [[ ! -x "$SHARED_VERIFY_EXISTING_RHDH_SCRIPT" ]]; then echo "Error: Existing-RHDH verification script not found or not executable: $SHARED_VERIFY_EXISTING_RHDH_SCRIPT" - echo "Hint: set SHARED_VERIFY_EXISTING_RHDH_SCRIPT to override, or use the local default script." + echo "Hint: set SHARED_VERIFY_EXISTING_RHDH_SCRIPT to override, or build rhdh-e2e-test-utils (yarn build)." exit 1 fi log "Using existing-RHDH verification script: $SHARED_VERIFY_EXISTING_RHDH_SCRIPT" @@ -214,150 +206,10 @@ run_shared_orchestrator_install() { phase_checkpoint "shared-orchestrator-installed" } -extract_major_minor() { - local version="$1" - echo "$version" | sed -E 's/^([0-9]+\.[0-9]+).*/\1/' -} - -get_subscription_field() { - local name="$1" field="$2" - oc get subscriptions.operators.coreos.com "$name" -n openshift-operators -o "jsonpath={.spec.${field}}" 2>/dev/null || true -} - -get_operator_csv_name() { - local package="$1" - local csv_name - csv_name="$(oc get csv -n openshift-operators -l "operators.coreos.com/${package}.openshift-operators" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)" - if [[ -z "$csv_name" && "$package" == "logic-operator" ]]; then - csv_name="$(oc get csv -n openshift-operators -l "operators.coreos.com/logic-operator-rhel8.openshift-operators" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)" - fi - echo "$csv_name" -} - -get_operator_csv_version() { - local package="$1" - local csv_name - csv_name="$(get_operator_csv_name "$package")" - [[ -z "$csv_name" ]] && { echo ""; return 0; } - oc get csv "$csv_name" -n openshift-operators -o jsonpath='{.spec.version}' 2>/dev/null || true -} - -assert_operator_configuration() { - local package="$1" sub_name="$2" expected_channel="$3" expected_source="$4" expected_source_ns="$5" expected_starting_csv="$6" - local actual_channel actual_source actual_source_ns actual_starting_csv - actual_channel="$(get_subscription_field "$sub_name" channel)" - actual_source="$(get_subscription_field "$sub_name" source)" - actual_source_ns="$(get_subscription_field "$sub_name" sourceNamespace)" - actual_starting_csv="$(get_subscription_field "$sub_name" startingCSV)" - - if [[ -n "$expected_channel" && "$actual_channel" != "$expected_channel" ]]; then - echo "Error: ${package} channel mismatch. expected='${expected_channel}' actual='${actual_channel}'" - exit 1 - fi - if [[ -n "$expected_source" && "$actual_source" != "$expected_source" ]]; then - echo "Error: ${package} source mismatch. expected='${expected_source}' actual='${actual_source}'" - exit 1 - fi - if [[ -n "$expected_source_ns" && "$actual_source_ns" != "$expected_source_ns" ]]; then - echo "Error: ${package} source namespace mismatch. expected='${expected_source_ns}' actual='${actual_source_ns}'" - exit 1 - fi - if [[ -n "$expected_starting_csv" && "$actual_starting_csv" != "$expected_starting_csv" ]]; then - echo "Error: ${package} startingCSV mismatch. expected='${expected_starting_csv}' actual='${actual_starting_csv}'" - exit 1 - fi -} - -assert_pre_release_install_state() { - local expected_logic_source="${OSL_CATALOG_SOURCE:-${OSL_LOGIC_SOURCE:-}}" - local expected_logic_source_ns="${OSL_LOGIC_SOURCE_NAMESPACE:-openshift-marketplace}" - local expected_logic_channel="${OSL_LOGIC_CHANNEL:-stable}" - local expected_logic_csv="${OSL_LOGIC_CSV:-}" - - local expected_serverless_source="${OSL_SERVERLESS_SOURCE:-redhat-operators}" - local expected_serverless_source_ns="${OSL_SERVERLESS_SOURCE_NAMESPACE:-openshift-marketplace}" - local expected_serverless_channel="${OSL_SERVERLESS_CHANNEL:-stable}" - - log "Asserting installed operator subscriptions and versions..." - assert_operator_configuration "logic-operator" "logic-operator" "$expected_logic_channel" "$expected_logic_source" "$expected_logic_source_ns" "$expected_logic_csv" - assert_operator_configuration "serverless-operator" "serverless-operator" "$expected_serverless_channel" "$expected_serverless_source" "$expected_serverless_source_ns" "" - - local logic_csv logic_version serverless_version logic_mm serverless_mm - logic_csv="$(get_operator_csv_name "logic-operator")" - logic_version="$(get_operator_csv_version "logic-operator")" - serverless_version="$(get_operator_csv_version "serverless-operator")" - - if [[ -z "$logic_csv" || -z "$logic_version" ]]; then - echo "Error: Unable to resolve installed logic-operator CSV/version." - exit 1 - fi - - if [[ -n "${OSL_VERSION:-}" ]]; then - local osl_marker - osl_marker="$(echo "${OSL_VERSION}" | tr '[:upper:]' '[:lower:]')" - local csv_lc version_lc - csv_lc="$(echo "${logic_csv}" | tr '[:upper:]' '[:lower:]')" - version_lc="$(echo "${logic_version}" | tr '[:upper:]' '[:lower:]')" - if [[ "$osl_marker" == *"cr"* || "$osl_marker" == *"rc"* ]]; then - # Some pre-release catalogs publish a GA-looking CSV/version while still being - # sourced from a pre-release catalog and pinned startingCSV; accept that case. - if [[ "$csv_lc" != *"cr"* && "$csv_lc" != *"rc"* && "$version_lc" != *"cr"* && "$version_lc" != *"rc"* ]]; then - if [[ -n "${expected_logic_csv:-}" && "$logic_csv" == "$expected_logic_csv" ]]; then - log "Pre-release marker not present in CSV/version; accepted because installed CSV matches expected startingCSV (${expected_logic_csv})." - else - echo "Error: Expected pre-release OSL marker in installed logic-operator CSV/version. csv='${logic_csv}' version='${logic_version}'" - exit 1 - fi - fi - fi - fi - - logic_mm="$(extract_major_minor "$logic_version")" - serverless_mm="$(extract_major_minor "$serverless_version")" - if [[ -n "$logic_mm" && -n "$serverless_mm" && "$logic_mm" != "$serverless_mm" ]]; then - if [[ "${ALLOW_OSL_SERVERLESS_VERSION_SKEW:-0}" != "1" ]]; then - echo "Error: Serverless/Logic major.minor mismatch (serverless=${serverless_mm}, logic=${logic_mm}). Set ALLOW_OSL_SERVERLESS_VERSION_SKEW=1 to override." - exit 1 - fi - echo "Warning: Serverless/Logic major.minor mismatch allowed by ALLOW_OSL_SERVERLESS_VERSION_SKEW=1 (serverless=${serverless_mm}, logic=${logic_mm})." - fi - - log "Installed logic-operator CSV: ${logic_csv} (version=${logic_version})" - log "Installed serverless-operator version: ${serverless_version:-unknown}" - phase_checkpoint "operator-configuration-asserted" -} - prepare_keycloak() { bash "$SCRIPT_DIR/utils/keycloak/keycloak-deploy.sh" "$KEYCLOAK_NAMESPACE" } -sync_keycloak_runtime_env() { - local keycloak_host keycloak_proto - keycloak_host="$(oc get route keycloak -n "$KEYCLOAK_NAMESPACE" -o jsonpath='{.spec.host}' 2>/dev/null || true)" - if [[ -z "$keycloak_host" ]]; then - echo "Error: could not resolve Keycloak route in namespace '$KEYCLOAK_NAMESPACE'." - exit 1 - fi - - if [[ -z "${KEYCLOAK_BASE_URL:-}" ]]; then - keycloak_proto="http" - if oc get route keycloak -n "$KEYCLOAK_NAMESPACE" -o jsonpath='{.spec.tls.termination}' 2>/dev/null | grep -q .; then - keycloak_proto="https" - fi - export KEYCLOAK_BASE_URL="${keycloak_proto}://${keycloak_host}" - fi - export KEYCLOAK_METADATA_URL="${KEYCLOAK_BASE_URL}/realms/rhdh" - export KEYCLOAK_REALM="${KEYCLOAK_REALM:-rhdh}" - export KEYCLOAK_LOGIN_REALM="${KEYCLOAK_LOGIN_REALM:-${KEYCLOAK_REALM}}" - export KEYCLOAK_CLIENT_ID="${KEYCLOAK_CLIENT_ID:-rhdh-client}" - export KEYCLOAK_CLIENT_SECRET="${KEYCLOAK_CLIENT_SECRET:-rhdh-client-secret}" - - if [[ -z "${KEYCLOAK_LOGIN_REALM}" ]]; then - echo "Error: KEYCLOAK_LOGIN_REALM resolved to empty value." - exit 1 - fi -} - verify_shared_existing_rhdh_contract() { log "Verifying shared existing-RHDH contract in ${namespace}..." bash "$SHARED_VERIFY_EXISTING_RHDH_SCRIPT" "$namespace" --require-keycloak @@ -373,30 +225,12 @@ if [[ -f "${SCRIPT_DIR}/.env.osl" ]]; then fi assert_empty_baseline "$namespace" "$KEYCLOAK_NAMESPACE" -route_scheme() { - local name="$1" ns="$2" - if oc get route "$name" -n "$ns" -o jsonpath='{.spec.tls.termination}' 2>/dev/null | grep -q .; then - echo https - else - echo http - fi -} - -rhdh_public_url() { - local ns="$1" - local host scheme - host="$(oc get route redhat-developer-hub -n "$ns" -o jsonpath='{.spec.host}' 2>/dev/null || true)" - [[ -n "$host" ]] || return 1 - scheme="$(route_scheme redhat-developer-hub "$ns")" - echo "${scheme}://${host}" -} - wait_for_rhdh_auth_and_orchestrator_ready() { local ns="$1" local timeout_secs="${2:-240}" local start_time rhdh_url start_time=$(date +%s) - rhdh_url="$(rhdh_public_url "$ns")" || { + rhdh_url="$(openshift_route_url redhat-developer-hub "$ns")" || { echo "Error: Could not resolve RHDH route in namespace '$ns'." return 1 } @@ -444,7 +278,7 @@ run_post_setup_workflow_smoke() { curl -sf --max-time 5 "http://localhost:8080/q/health/ready" >/dev/null local orchestrator_url orch_health - orchestrator_url="$(rhdh_public_url "$ns")" || { + orchestrator_url="$(openshift_route_url redhat-developer-hub "$ns")" || { echo "Error: Could not resolve RHDH route for post-setup smoke." exit 1 } @@ -469,10 +303,6 @@ if [[ -n "$prepare_internal_osl_release" ]]; then log "Loaded OSL_LOGIC_CSV=${OSL_LOGIC_CSV}" log "Loaded OSL_CATALOG_SOURCE=${OSL_CATALOG_SOURCE}" phase_checkpoint "internal-mirror-prep-complete" -elif [[ -f "${SCRIPT_DIR}/.env.osl" ]]; then - # shellcheck disable=SC1091 - source "${SCRIPT_DIR}/.env.osl" - log "Loaded existing .env.osl (OSL_LOGIC_CSV=${OSL_LOGIC_CSV:-unset} OSL_CATALOG_SOURCE=${OSL_CATALOG_SOURCE:-unset})" fi # ── Pre-deploy: export secrets for envsubst in helm/deploy.sh ─────────────── @@ -488,7 +318,7 @@ fi log "Preparing Keycloak before shared orchestrator install..." prepare_keycloak -sync_keycloak_runtime_env +export_keycloak_runtime_env "$KEYCLOAK_NAMESPACE" run_shared_orchestrator_install assert_pre_release_install_state @@ -504,7 +334,7 @@ SKIP_ORCHESTRATOR_INFRA_INSTALL=1 \ ./deploy.sh helm "$version" --namespace "$namespace" --with-orchestrator phase_checkpoint "rhdh-deployed" -RHDH_BASE_URL="$(rhdh_public_url "$namespace")" || { +RHDH_BASE_URL="$(openshift_route_url redhat-developer-hub "$namespace")" || { echo "Error: Could not resolve RHDH route after deploy." exit 1 } diff --git a/utils/keycloak/keycloak-deploy.sh b/utils/keycloak/keycloak-deploy.sh index 293e1d3..f06daa6 100755 --- a/utils/keycloak/keycloak-deploy.sh +++ b/utils/keycloak/keycloak-deploy.sh @@ -7,49 +7,14 @@ command -v oc >/dev/null 2>&1 || { echo "Error: oc (OpenShift CLI) is required b NAMESPACE=${1:-rhdh-keycloak} KEYCLOAK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 +source "${KEYCLOAK_DIR}/lib.sh" USERS_FILE=${2:-"${KEYCLOAK_DIR}/users.json"} GROUPS_FILE=${3:-"${KEYCLOAK_DIR}/groups.json"} CLIENT_FILE="${KEYCLOAK_DIR}/rhdh-client.json" KEYCLOAK_RELEASE_NAME="keycloak" KEYCLOAK_VALUES="${KEYCLOAK_DIR}/keycloak-values.yaml" -# Helper function for API calls with error checking -api_call() { - local method=$1 - local url=$2 - local data=$3 - local description=$4 - - if [ -n "$data" ]; then - RESPONSE=$(curl -sk -w "\n%{http_code}" -X "$method" "$url" \ - -H "Authorization: Bearer $ADMIN_TOKEN" \ - -H "Content-Type: application/json" \ - -d "$data") - else - RESPONSE=$(curl -sk -w "\n%{http_code}" -X "$method" "$url" \ - -H "Authorization: Bearer $ADMIN_TOKEN" \ - -H "Content-Type: application/json") - fi - - HTTP_CODE=$(echo "$RESPONSE" | tail -1) - BODY=$(echo "$RESPONSE" | sed '$d') - - if [ "$method" = "GET" ] || [ "$HTTP_CODE" -lt 400 ]; then - echo "$BODY" - return 0 - fi - - # 409 Conflict is acceptable for create operations (already exists) - if [ "$HTTP_CODE" = "409" ]; then - echo "Warning: $description - already exists (continuing)" >&2 - echo "$BODY" - return 0 - fi - - echo "Error: $description failed (HTTP $HTTP_CODE): $BODY" >&2 - return 1 -} - if [[ "${BASH_SOURCE[0]}" != "$0" ]]; then echo "Error: run $0; do not source it (see update-rhdh-client-redirects.sh for redirects)" >&2 return 1 2>/dev/null || exit 1 @@ -77,12 +42,7 @@ helm upgrade --install $KEYCLOAK_RELEASE_NAME bitnami/keycloak \ echo "Waiting for Keycloak rollout..." oc rollout status statefulset/keycloak -n $NAMESPACE --timeout=5m -# Detect TLS based on cluster route configuration -if oc get route console -n openshift-console -o=jsonpath='{.spec.tls.termination}' 2>/dev/null | grep -q .; then - KEYCLOAK_PROTOCOL="https" -else - KEYCLOAK_PROTOCOL="http" -fi +KEYCLOAK_PROTOCOL="$(keycloak_console_protocol)" # Create OpenShift Route echo "Creating OpenShift Route (protocol: $KEYCLOAK_PROTOCOL)..." @@ -129,8 +89,9 @@ spec: EOF fi -KEYCLOAK_URL="${KEYCLOAK_PROTOCOL}://$(oc get route keycloak -n $NAMESPACE -o jsonpath='{.spec.host}')" -[ -z "$KEYCLOAK_URL" ] || [ "$KEYCLOAK_URL" = "${KEYCLOAK_PROTOCOL}://" ] && echo "Error: Failed to get Keycloak route" && exit 1 +KEYCLOAK_URL="$(keycloak_route_url "$NAMESPACE" "$KEYCLOAK_RELEASE_NAME")" || { + echo "Error: Failed to get Keycloak route" && exit 1 +} echo "Keycloak URL: $KEYCLOAK_URL" # Wait for Keycloak API to be ready (check for HTTP 200, not just connection) @@ -151,41 +112,34 @@ while true; do echo " Waiting... (status: $HTTP_STATUS)" done -# Get admin token -TOKEN_RESPONSE=$(curl -sk -w "\n%{http_code}" -X POST "$KEYCLOAK_URL/realms/master/protocol/openid-connect/token" \ - -d "username=admin&password=admin123&grant_type=password&client_id=admin-cli") -TOKEN_HTTP_CODE=$(echo "$TOKEN_RESPONSE" | tail -1) -TOKEN_BODY=$(echo "$TOKEN_RESPONSE" | sed '$d') -[ "$TOKEN_HTTP_CODE" -ge 400 ] && echo "Error: Failed to get admin token (HTTP $TOKEN_HTTP_CODE): $TOKEN_BODY" && exit 1 -ADMIN_TOKEN=$(echo "$TOKEN_BODY" | jq -r '.access_token // empty') -[ -z "$ADMIN_TOKEN" ] && echo "Error: Failed to parse admin token" && exit 1 +ADMIN_TOKEN="$(keycloak_admin_token "$KEYCLOAK_URL")" # Create realm and client echo "Creating realm 'rhdh'..." -api_call POST "$KEYCLOAK_URL/admin/realms" \ +keycloak_api_call POST "$KEYCLOAK_URL/admin/realms" \ '{"realm":"rhdh","enabled":true,"displayName":"RHDH Realm"}' \ "Create realm" >/dev/null echo "Creating client..." -api_call POST "$KEYCLOAK_URL/admin/realms/rhdh/clients" \ +keycloak_api_call POST "$KEYCLOAK_URL/admin/realms/rhdh/clients" \ "$(jq -c '.' "$CLIENT_FILE")" \ "Create client" >/dev/null # Get IDs for role assignment -SERVICE_ACCOUNT_ID=$(api_call GET "$KEYCLOAK_URL/admin/realms/rhdh/users?username=service-account-rhdh-client" "" "Get service account" | \ +SERVICE_ACCOUNT_ID=$(keycloak_api_call GET "$KEYCLOAK_URL/admin/realms/rhdh/users?username=service-account-rhdh-client" "" "Get service account" | \ jq -r '.[0].id // empty') [ -z "$SERVICE_ACCOUNT_ID" ] && echo "Error: Service account not found" && exit 1 -REALM_MGMT_ID=$(api_call GET "$KEYCLOAK_URL/admin/realms/rhdh/clients?clientId=realm-management" "" "Get realm-management client" | \ +REALM_MGMT_ID=$(keycloak_api_call GET "$KEYCLOAK_URL/admin/realms/rhdh/clients?clientId=realm-management" "" "Get realm-management client" | \ jq -r '.[0].id // empty') [ -z "$REALM_MGMT_ID" ] && echo "Error: realm-management client not found" && exit 1 -ROLES=$(api_call GET "$KEYCLOAK_URL/admin/realms/rhdh/clients/$REALM_MGMT_ID/roles" "" "Get roles" | \ +ROLES=$(keycloak_api_call GET "$KEYCLOAK_URL/admin/realms/rhdh/clients/$REALM_MGMT_ID/roles" "" "Get roles" | \ jq -c '[.[] | select(.name == "view-authorization" or .name == "manage-authorization" or .name == "view-users")]') [ -z "$ROLES" ] || [ "$ROLES" = "[]" ] && echo "Error: Required roles not found" && exit 1 echo "Assigning service account roles..." -api_call POST "$KEYCLOAK_URL/admin/realms/rhdh/users/$SERVICE_ACCOUNT_ID/role-mappings/clients/$REALM_MGMT_ID" \ +keycloak_api_call POST "$KEYCLOAK_URL/admin/realms/rhdh/users/$SERVICE_ACCOUNT_ID/role-mappings/clients/$REALM_MGMT_ID" \ "$ROLES" \ "Assign roles" >/dev/null @@ -193,7 +147,7 @@ api_call POST "$KEYCLOAK_URL/admin/realms/rhdh/users/$SERVICE_ACCOUNT_ID/role-ma if [ -f "$GROUPS_FILE" ]; then echo "Creating groups..." jq -r '.[].name' "$GROUPS_FILE" | while read -r group; do - api_call POST "$KEYCLOAK_URL/admin/realms/rhdh/groups" \ + keycloak_api_call POST "$KEYCLOAK_URL/admin/realms/rhdh/groups" \ "{\"name\":\"$group\"}" \ "Create group '$group'" >/dev/null && echo " Created group: $group" || echo " Warning: Failed to create group: $group" done @@ -208,7 +162,7 @@ if [ -f "$USERS_FILE" ]; then groups=$(echo "$user_json" | jq -r '.groups // [] | join(",")') user_payload=$(echo "$user_json" | jq -c 'del(.groups)') - if ! api_call POST "$KEYCLOAK_URL/admin/realms/rhdh/users" "$user_payload" "Create user '$username'" >/dev/null; then + if ! keycloak_api_call POST "$KEYCLOAK_URL/admin/realms/rhdh/users" "$user_payload" "Create user '$username'" >/dev/null; then echo " Warning: Failed to create user: $username" continue fi @@ -216,15 +170,15 @@ if [ -f "$USERS_FILE" ]; then # Add user to groups if [ -n "$groups" ]; then - USER_ID=$(api_call GET "$KEYCLOAK_URL/admin/realms/rhdh/users?username=$username" "" "Get user ID" | \ + USER_ID=$(keycloak_api_call GET "$KEYCLOAK_URL/admin/realms/rhdh/users?username=$username" "" "Get user ID" | \ jq -r '.[0].id // empty') [ -z "$USER_ID" ] && echo " Warning: Could not get user ID, skipping groups" && continue for group in $(echo "$groups" | tr ',' ' '); do - GROUP_ID=$(api_call GET "$KEYCLOAK_URL/admin/realms/rhdh/groups?search=$group" "" "Get group ID" | \ + GROUP_ID=$(keycloak_api_call GET "$KEYCLOAK_URL/admin/realms/rhdh/groups?search=$group" "" "Get group ID" | \ jq -r '.[0].id // empty') [ -z "$GROUP_ID" ] && echo " Warning: Group '$group' not found" && continue - api_call PUT "$KEYCLOAK_URL/admin/realms/rhdh/users/$USER_ID/groups/$GROUP_ID" "" "Add to group" >/dev/null \ + keycloak_api_call PUT "$KEYCLOAK_URL/admin/realms/rhdh/users/$USER_ID/groups/$GROUP_ID" "" "Add to group" >/dev/null \ && echo " Added to group: $group" || echo " Warning: Failed to add to group: $group" done fi diff --git a/utils/keycloak/lib.sh b/utils/keycloak/lib.sh new file mode 100644 index 0000000..8feeb30 --- /dev/null +++ b/utils/keycloak/lib.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# Shared Keycloak REST helpers for orchestrator smoke setup. +# Source from keycloak-deploy.sh, update-rhdh-client-redirects.sh, and setup-orchestrator.sh. + +_KEYCLOAK_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 +source "${_KEYCLOAK_LIB_DIR}/../shell/openshift.sh" + +keycloak_console_protocol() { + if oc get route console -n openshift-console -o=jsonpath='{.spec.tls.termination}' 2>/dev/null | grep -q .; then + echo https + else + echo http + fi +} + +keycloak_route_url() { + local namespace="$1" + local release_name="${2:-keycloak}" + openshift_route_url "$release_name" "$namespace" http +} + +keycloak_admin_token() { + local keycloak_url="$1" + local admin_password="${2:-admin123}" + local token_response token_http_code token_body admin_token + token_response=$(curl -sk -w "\n%{http_code}" -X POST "$keycloak_url/realms/master/protocol/openid-connect/token" \ + -d "username=admin&password=${admin_password}&grant_type=password&client_id=admin-cli") + token_http_code=$(echo "$token_response" | tail -1) + token_body=$(echo "$token_response" | sed '$d') + if [[ "$token_http_code" -ge 400 ]]; then + echo "Error: Failed to get admin token (HTTP $token_http_code): $token_body" >&2 + return 1 + fi + admin_token=$(echo "$token_body" | jq -r '.access_token // empty') + if [[ -z "$admin_token" ]]; then + echo "Error: Failed to parse admin token" >&2 + return 1 + fi + echo "$admin_token" +} + +keycloak_api_call() { + local method=$1 + local url=$2 + local data=$3 + local description=$4 + local response http_code body + + if [[ -n "$data" ]]; then + response=$(curl -sk -w "\n%{http_code}" -X "$method" "$url" \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + -H "Content-Type: application/json" \ + -d "$data") + else + response=$(curl -sk -w "\n%{http_code}" -X "$method" "$url" \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + -H "Content-Type: application/json") + fi + + http_code=$(echo "$response" | tail -1) + body=$(echo "$response" | sed '$d') + + if [[ "$method" == "GET" ]] || [[ "$http_code" -lt 400 ]]; then + echo "$body" + return 0 + fi + + if [[ "$http_code" == "409" ]]; then + echo "Warning: $description - already exists (continuing)" >&2 + echo "$body" + return 0 + fi + + echo "Error: $description failed (HTTP $http_code): $body" >&2 + return 1 +} + +export_keycloak_runtime_env() { + local ns="$1" + local url + url="$(keycloak_route_url "$ns")" || { + echo "Error: could not resolve Keycloak route in namespace '$ns'." >&2 + return 1 + } + export KEYCLOAK_BASE_URL="${KEYCLOAK_BASE_URL:-$url}" + export KEYCLOAK_METADATA_URL="${KEYCLOAK_BASE_URL}/realms/rhdh" + export KEYCLOAK_REALM="${KEYCLOAK_REALM:-rhdh}" + export KEYCLOAK_LOGIN_REALM="${KEYCLOAK_LOGIN_REALM:-${KEYCLOAK_REALM}}" + export KEYCLOAK_CLIENT_ID="${KEYCLOAK_CLIENT_ID:-rhdh-client}" + export KEYCLOAK_CLIENT_SECRET="${KEYCLOAK_CLIENT_SECRET:-rhdh-client-secret}" + if [[ -z "${KEYCLOAK_LOGIN_REALM}" ]]; then + echo "Error: KEYCLOAK_LOGIN_REALM resolved to empty value." >&2 + return 1 + fi +} diff --git a/utils/keycloak/update-rhdh-client-redirects.sh b/utils/keycloak/update-rhdh-client-redirects.sh index ec95e03..ff46a9f 100755 --- a/utils/keycloak/update-rhdh-client-redirects.sh +++ b/utils/keycloak/update-rhdh-client-redirects.sh @@ -6,6 +6,10 @@ set -euo pipefail command -v jq >/dev/null 2>&1 || { echo "Error: jq is required" >&2; exit 1; } command -v oc >/dev/null 2>&1 || { echo "Error: oc is required" >&2; exit 1; } +KEYCLOAK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 +source "${KEYCLOAK_DIR}/lib.sh" + NAMESPACE="${1:-}" RHDH_URL="${2:-}" [[ -n "$NAMESPACE" && -n "$RHDH_URL" ]] || { @@ -13,57 +17,20 @@ RHDH_URL="${2:-}" exit 1 } -if oc get route console -n openshift-console -o=jsonpath='{.spec.tls.termination}' 2>/dev/null | grep -q .; then - KEYCLOAK_PROTOCOL="https" -else - KEYCLOAK_PROTOCOL="http" -fi -host="$(oc get route keycloak -n "$NAMESPACE" -o jsonpath='{.spec.host}' 2>/dev/null || true)" -[[ -n "$host" ]] || { echo "Error: Keycloak route not found in $NAMESPACE" >&2; exit 1; } -KEYCLOAK_URL="${KEYCLOAK_PROTOCOL}://${host}" -redirect="${RHDH_URL%/}/api/auth/oidc/handler/frame" - -api_call() { - local method=$1 url=$2 data=$3 description=$4 - local RESPONSE HTTP_CODE BODY - if [ -n "$data" ]; then - RESPONSE=$(curl -sk -w "\n%{http_code}" -X "$method" "$url" \ - -H "Authorization: Bearer $ADMIN_TOKEN" \ - -H "Content-Type: application/json" \ - -d "$data") - else - RESPONSE=$(curl -sk -w "\n%{http_code}" -X "$method" "$url" \ - -H "Authorization: Bearer $ADMIN_TOKEN" \ - -H "Content-Type: application/json") - fi - HTTP_CODE=$(echo "$RESPONSE" | tail -1) - BODY=$(echo "$RESPONSE" | sed '$d') - if [ "$method" = "GET" ] || [ "$HTTP_CODE" -lt 400 ]; then - echo "$BODY" - return 0 - fi - if [ "$HTTP_CODE" = "409" ]; then - echo "$BODY" - return 0 - fi - echo "Error: $description failed (HTTP $HTTP_CODE): $BODY" >&2 - return 1 +KEYCLOAK_URL="$(keycloak_route_url "$NAMESPACE")" || { + echo "Error: Keycloak route not found in $NAMESPACE" >&2 + exit 1 } +redirect="${RHDH_URL%/}/api/auth/oidc/handler/frame" -token_response=$(curl -sk -w "\n%{http_code}" -X POST "$KEYCLOAK_URL/realms/master/protocol/openid-connect/token" \ - -d "username=admin&password=admin123&grant_type=password&client_id=admin-cli") -TOKEN_HTTP_CODE=$(echo "$token_response" | tail -1) -TOKEN_BODY=$(echo "$token_response" | sed '$d') -[ "$TOKEN_HTTP_CODE" -ge 400 ] && echo "Error: Failed to refresh admin token (HTTP $TOKEN_HTTP_CODE): $TOKEN_BODY" >&2 && exit 1 -ADMIN_TOKEN=$(echo "$TOKEN_BODY" | jq -r '.access_token // empty') -[ -z "$ADMIN_TOKEN" ] && echo "Error: Failed to parse refreshed admin token" >&2 && exit 1 +ADMIN_TOKEN="$(keycloak_admin_token "$KEYCLOAK_URL")" -client_uuid=$(api_call GET "$KEYCLOAK_URL/admin/realms/rhdh/clients?clientId=rhdh-client" "" "Get rhdh-client" | \ +client_uuid=$(keycloak_api_call GET "$KEYCLOAK_URL/admin/realms/rhdh/clients?clientId=rhdh-client" "" "Get rhdh-client" | \ jq -r '.[0].id // empty') -[ -z "$client_uuid" ] && echo "Error: rhdh-client UUID not found" >&2 && exit 1 +[[ -n "$client_uuid" ]] || { echo "Error: rhdh-client UUID not found" >&2; exit 1; } -payload=$(api_call GET "$KEYCLOAK_URL/admin/realms/rhdh/clients/$client_uuid" "" "Get rhdh-client representation" | \ +payload=$(keycloak_api_call GET "$KEYCLOAK_URL/admin/realms/rhdh/clients/$client_uuid" "" "Get rhdh-client representation" | \ jq -c --arg uri "$redirect" --arg origin "${RHDH_URL%/}" \ '.redirectUris = [$uri] | .webOrigins = [$origin] | .implicitFlowEnabled = false') -api_call PUT "$KEYCLOAK_URL/admin/realms/rhdh/clients/$client_uuid" "$payload" "Pin rhdh-client redirects" >/dev/null +keycloak_api_call PUT "$KEYCLOAK_URL/admin/realms/rhdh/clients/$client_uuid" "$payload" "Pin rhdh-client redirects" >/dev/null echo "Pinned rhdh-client redirectUris to ${redirect} webOrigins to ${RHDH_URL%/}" diff --git a/utils/orchestrator/assert-osl-operators.sh b/utils/orchestrator/assert-osl-operators.sh new file mode 100644 index 0000000..a03682b --- /dev/null +++ b/utils/orchestrator/assert-osl-operators.sh @@ -0,0 +1,121 @@ +#!/bin/bash +# +# Assert OSL/Serverless operator subscriptions and CSV versions after install. +# Requires OSL_* env vars when sourced or invoked. Usage: +# source utils/orchestrator/assert-osl-operators.sh +# assert_pre_release_install_state +# Or: bash utils/orchestrator/assert-osl-operators.sh +# +set -euo pipefail + +_ASSERT_OSL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +# shellcheck disable=SC1091 +source "${_ASSERT_OSL_DIR}/utils/shell/common.sh" + +extract_major_minor() { + local version="$1" + echo "$version" | sed -E 's/^([0-9]+\.[0-9]+).*/\1/' +} + +get_subscription_field() { + local name="$1" field="$2" + oc get subscriptions.operators.coreos.com "$name" -n openshift-operators -o "jsonpath={.spec.${field}}" 2>/dev/null || true +} + +get_operator_csv_name() { + local package="$1" + local csv_name + csv_name="$(oc get csv -n openshift-operators -l "operators.coreos.com/${package}.openshift-operators" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)" + if [[ -z "$csv_name" && "$package" == "logic-operator" ]]; then + csv_name="$(oc get csv -n openshift-operators -l "operators.coreos.com/logic-operator-rhel8.openshift-operators" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)" + fi + echo "$csv_name" +} + +get_operator_csv_version() { + local package="$1" + local csv_name + csv_name="$(get_operator_csv_name "$package")" + [[ -z "$csv_name" ]] && { echo ""; return 0; } + oc get csv "$csv_name" -n openshift-operators -o jsonpath='{.spec.version}' 2>/dev/null || true +} + +assert_operator_configuration() { + local package="$1" sub_name="$2" expected_channel="$3" expected_source="$4" expected_source_ns="$5" expected_starting_csv="$6" + local actual_channel actual_source actual_source_ns actual_starting_csv + actual_channel="$(get_subscription_field "$sub_name" channel)" + actual_source="$(get_subscription_field "$sub_name" source)" + actual_source_ns="$(get_subscription_field "$sub_name" sourceNamespace)" + actual_starting_csv="$(get_subscription_field "$sub_name" startingCSV)" + + if [[ -n "$expected_channel" && "$actual_channel" != "$expected_channel" ]]; then + die "${package} channel mismatch. expected='${expected_channel}' actual='${actual_channel}'" + fi + if [[ -n "$expected_source" && "$actual_source" != "$expected_source" ]]; then + die "${package} source mismatch. expected='${expected_source}' actual='${actual_source}'" + fi + if [[ -n "$expected_source_ns" && "$actual_source_ns" != "$expected_source_ns" ]]; then + die "${package} source namespace mismatch. expected='${expected_source_ns}' actual='${actual_source_ns}'" + fi + if [[ -n "$expected_starting_csv" && "$actual_starting_csv" != "$expected_starting_csv" ]]; then + die "${package} startingCSV mismatch. expected='${expected_starting_csv}' actual='${actual_starting_csv}'" + fi +} + +assert_pre_release_install_state() { + local expected_logic_source="${OSL_CATALOG_SOURCE:-${OSL_LOGIC_SOURCE:-}}" + local expected_logic_source_ns="${OSL_LOGIC_SOURCE_NAMESPACE:-openshift-marketplace}" + local expected_logic_channel="${OSL_LOGIC_CHANNEL:-stable}" + local expected_logic_csv="${OSL_LOGIC_CSV:-}" + + local expected_serverless_source="${OSL_SERVERLESS_SOURCE:-redhat-operators}" + local expected_serverless_source_ns="${OSL_SERVERLESS_SOURCE_NAMESPACE:-openshift-marketplace}" + local expected_serverless_channel="${OSL_SERVERLESS_CHANNEL:-stable}" + + log "Asserting installed operator subscriptions and versions..." + assert_operator_configuration "logic-operator" "logic-operator" "$expected_logic_channel" "$expected_logic_source" "$expected_logic_source_ns" "$expected_logic_csv" + assert_operator_configuration "serverless-operator" "serverless-operator" "$expected_serverless_channel" "$expected_serverless_source" "$expected_serverless_source_ns" "" + + local logic_csv logic_version serverless_version logic_mm serverless_mm + logic_csv="$(get_operator_csv_name "logic-operator")" + logic_version="$(get_operator_csv_version "logic-operator")" + serverless_version="$(get_operator_csv_version "serverless-operator")" + + if [[ -z "$logic_csv" || -z "$logic_version" ]]; then + die "Unable to resolve installed logic-operator CSV/version." + fi + + if [[ -n "${OSL_VERSION:-}" ]]; then + local osl_marker + osl_marker="$(echo "${OSL_VERSION}" | tr '[:upper:]' '[:lower:]')" + local csv_lc version_lc + csv_lc="$(echo "${logic_csv}" | tr '[:upper:]' '[:lower:]')" + version_lc="$(echo "${logic_version}" | tr '[:upper:]' '[:lower:]')" + if [[ "$osl_marker" == *"cr"* || "$osl_marker" == *"rc"* ]]; then + if [[ "$csv_lc" != *"cr"* && "$csv_lc" != *"rc"* && "$version_lc" != *"cr"* && "$version_lc" != *"rc"* ]]; then + if [[ -n "${expected_logic_csv:-}" && "$logic_csv" == "$expected_logic_csv" ]]; then + log "Pre-release marker not present in CSV/version; accepted because installed CSV matches expected startingCSV (${expected_logic_csv})." + else + die "Expected pre-release OSL marker in installed logic-operator CSV/version. csv='${logic_csv}' version='${logic_version}'" + fi + fi + fi + fi + + logic_mm="$(extract_major_minor "$logic_version")" + serverless_mm="$(extract_major_minor "$serverless_version")" + if [[ -n "$logic_mm" && -n "$serverless_mm" && "$logic_mm" != "$serverless_mm" ]]; then + if [[ "${ALLOW_OSL_SERVERLESS_VERSION_SKEW:-0}" != "1" ]]; then + die "Serverless/Logic major.minor mismatch (serverless=${serverless_mm}, logic=${logic_mm}). Set ALLOW_OSL_SERVERLESS_VERSION_SKEW=1 to override." + fi + echo "Warning: Serverless/Logic major.minor mismatch allowed by ALLOW_OSL_SERVERLESS_VERSION_SKEW=1 (serverless=${serverless_mm}, logic=${logic_mm})." + fi + + log "Installed logic-operator CSV: ${logic_csv} (version=${logic_version})" + log "Installed serverless-operator version: ${serverless_version:-unknown}" + echo "[CHECKPOINT] operator-configuration-asserted" +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + assert_pre_release_install_state +fi diff --git a/utils/orchestrator/deploy-smoke-workflows.sh b/utils/orchestrator/deploy-smoke-workflows.sh index 7ec54dd..8c738f2 100755 --- a/utils/orchestrator/deploy-smoke-workflows.sh +++ b/utils/orchestrator/deploy-smoke-workflows.sh @@ -6,8 +6,9 @@ # set -euo pipefail -log() { echo "==> $*"; } -die() { echo "Error: $*" >&2; exit 1; } +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +# shellcheck disable=SC1091 +source "${SCRIPT_DIR}/utils/shell/common.sh" ns="${1:-}" [[ -n "$ns" ]] || die "namespace required" diff --git a/utils/orchestrator/probe-dataindex-rewrite.sh b/utils/orchestrator/probe-dataindex-rewrite.sh new file mode 100644 index 0000000..fc1e594 --- /dev/null +++ b/utils/orchestrator/probe-dataindex-rewrite.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# +# Probe Data Index GraphQL via osl-di-rewrite for absolute ProcessDefinitions.serviceUrl. +# Usage: probe-dataindex-rewrite.sh [allow-relative] +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +# shellcheck disable=SC1091 +source "${SCRIPT_DIR}/utils/shell/common.sh" +require_cmd jq + +ns="${1:-}" +allow="${2:-false}" +[[ -n "$ns" ]] || die "namespace required" + +if [[ "$allow" == "true" || "$allow" == "1" ]]; then + allow=true +else + allow=false +fi + +body='{"query":"{ ProcessDefinitions { id serviceUrl endpoint } }"}' +url="http://osl-di-rewrite.${ns}.svc.cluster.local/graphql" +log "probing Data Index GraphQL via osl-di-rewrite ProcessDefinitions.serviceUrl" +json="$(oc exec -n "$ns" deploy/redhat-developer-hub -- \ + curl -sS -X POST -H "Content-Type: application/json" -d "$body" "$url")" \ + || die "oc exec curl of Data Index GraphQL (osl-di-rewrite) failed" +if ! printf '%s' "$json" | jq -e . >/dev/null 2>&1; then + die "Data Index did not return JSON: ${json:0:500}" +fi +if printf '%s' "$json" | jq -e '.errors != null and (.errors | length) > 0' >/dev/null; then + printf '%s\n' "$json" | jq '.errors' >&2 + die "Data Index GraphQL returned errors" +fi +count="$(printf '%s' "$json" | jq '.data.ProcessDefinitions | length // 0')" +if [[ "$count" -eq 0 ]]; then + printf '%s\n' '{"ok":false,"problems":[{"id":null,"serviceUrl":null,"endpoint":null,"reason":"no-process-definitions"}]}' >&2 + exit 1 +fi +problems="$(printf '%s' "$json" | jq '[.data.ProcessDefinitions[] | select((.serviceUrl | type != "string") or ((.serviceUrl | startswith("http://") or startswith("https://")) | not)) | {id, serviceUrl, endpoint, reason: "relative-or-missing-serviceUrl"}]')" +if [[ "$(printf '%s' "$problems" | jq 'length')" -gt 0 ]]; then + printf '%s\n' "$problems" | jq '{ok:false, problems:.}' >&2 + if [[ "$allow" == "true" ]]; then + log "WARNING: relative/missing serviceUrl allowed by ALLOW_RELATIVE_SERVICE_URL" + exit 0 + fi + exit 2 +fi +printf '%s\n' '{"ok":true,"problems":[]}' >&2 diff --git a/utils/orchestrator/verify-existing-rhdh.sh b/utils/orchestrator/verify-existing-rhdh.sh deleted file mode 100755 index 9c5bd86..0000000 --- a/utils/orchestrator/verify-existing-rhdh.sh +++ /dev/null @@ -1,98 +0,0 @@ -#!/bin/bash -# -# Verify that an existing RHDH namespace satisfies the orchestrator substrate -# contract expected by this repository's setup flow. -# - -set -euo pipefail - -namespace="orchestrator-app-next" -if [[ $# -gt 0 && "$1" != --* ]]; then - namespace="$1" - shift -fi - -POSTGRES_SECRET="${POSTGRES_SECRET:-backstage-psql-secret}" -POSTGRES_SERVICE="${POSTGRES_SERVICE:-backstage-psql}" -REQUIRE_KEYCLOAK=false - -while [[ $# -gt 0 ]]; do - case "$1" in - --postgres-secret) - POSTGRES_SECRET="$2" - shift 2 - ;; - --postgres-service) - POSTGRES_SERVICE="$2" - shift 2 - ;; - --require-keycloak) - REQUIRE_KEYCLOAK=true - shift - ;; - *) - echo "Error: Unknown option: $1" >&2 - exit 1 - ;; - esac -done - -log() { - echo "==> $*" -} - -require_resource() { - local kind="$1" name="$2" ns="$3" - if ! oc get "$kind" "$name" -n "$ns" >/dev/null 2>&1; then - echo "Error: Missing required ${kind}/${name} in namespace ${ns}" >&2 - exit 1 - fi -} - -require_route() { - local name="$1" ns="$2" - if ! oc get route "$name" -n "$ns" >/dev/null 2>&1; then - echo "Error: Missing required route/${name} in namespace ${ns}" >&2 - exit 1 - fi -} - -resolve_keycloak_route() { - local host - for ns in "$namespace" "rhdh-keycloak"; do - host="$(oc get route keycloak -n "$ns" -o jsonpath='{.spec.host}' 2>/dev/null || true)" - if [[ -n "$host" ]]; then - echo "$host" - return 0 - fi - done - return 1 -} - -main() { - if ! oc whoami >/dev/null 2>&1; then - echo "Error: Cannot connect to OpenShift cluster." >&2 - exit 1 - fi - - require_resource "secret" "$POSTGRES_SECRET" "$namespace" - require_resource "service" "$POSTGRES_SERVICE" "$namespace" - require_resource "deployment" "sonataflow-platform-data-index-service" "$namespace" - require_resource "deployment" "sonataflow-platform-jobs-service" "$namespace" - require_route "redhat-developer-hub" "$namespace" - - if [[ "$REQUIRE_KEYCLOAK" == "true" ]]; then - if [[ -n "${KEYCLOAK_BASE_URL:-}" ]]; then - log "Using KEYCLOAK_BASE_URL from environment." - elif ! resolve_keycloak_route >/dev/null; then - echo "Error: Missing required Keycloak route (checked ${namespace} and rhdh-keycloak)." >&2 - exit 1 - fi - fi - - log "Verified existing-RHDH orchestrator prerequisites in namespace ${namespace}." - log "PostgreSQL secret: ${POSTGRES_SECRET}" - log "PostgreSQL service: ${POSTGRES_SERVICE}" -} - -main "$@" diff --git a/utils/shell/common.sh b/utils/shell/common.sh new file mode 100644 index 0000000..91d4497 --- /dev/null +++ b/utils/shell/common.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# Shared logging and command helpers for OSL smoke scripts. + +log() { echo "==> $*"; } + +die() { echo "Error: $*" >&2; exit 1; } + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" +} diff --git a/utils/shell/openshift.sh b/utils/shell/openshift.sh new file mode 100644 index 0000000..e065060 --- /dev/null +++ b/utils/shell/openshift.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# OpenShift cluster preflight and route URL helpers. + +_SHELL_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if ! declare -f die >/dev/null 2>&1; then + # shellcheck disable=SC1091 + source "${_SHELL_LIB_DIR}/common.sh" +fi + +require_oc_login() { + if oc whoami &>/dev/null; then + return 0 + fi + if [[ -n "${1:-}" ]]; then + die "$1" + fi + echo "Error: Cannot connect to OpenShift cluster. Is CRC running and are you logged in?" >&2 + echo " Try: crc start && oc login -u kubeadmin https://api.crc.testing:6443" >&2 + exit 1 +} + +validate_k8s_namespace() { + local ns="$1" + if [[ ! "$ns" =~ ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ ]]; then + die "Invalid namespace name: '$ns' (must be lowercase alphanumeric/hyphens, 1-63 chars)" + fi +} + +openshift_route_scheme() { + local name="$1" + local ns="$2" + if oc get route "$name" -n "$ns" -o jsonpath='{.spec.tls.termination}' 2>/dev/null | grep -q .; then + echo https + else + echo http + fi +} + +openshift_route_url() { + local name="$1" + local ns="$2" + local default_scheme="${3:-https}" + local host tls scheme + host="$(oc get route "$name" -n "$ns" -o jsonpath='{.spec.host}' 2>/dev/null || true)" + [[ -n "$host" ]] || die "route $name in $ns has no host" + tls="$(oc get route "$name" -n "$ns" -o jsonpath='{.spec.tls.termination}' 2>/dev/null || true)" + scheme="$default_scheme" + [[ -n "$tls" ]] && scheme="https" + echo "${scheme}://${host}" +} + +openshift_cluster_router_base() { + local domain host + domain="$(oc get ingresses.config/cluster -o jsonpath='{.spec.domain}' 2>/dev/null || true)" + if [[ -n "$domain" ]]; then + echo "$domain" + return 0 + fi + host="$(oc get route console -n openshift-console -o jsonpath='{.spec.host}' 2>/dev/null || true)" + [[ "$host" == *.* ]] || die "could not discover cluster router base" + echo "${host#*.}" +} diff --git a/utils/shell/workspace.sh b/utils/shell/workspace.sh new file mode 100644 index 0000000..05e8327 --- /dev/null +++ b/utils/shell/workspace.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# Resolve the parent workspace directory (main repo root's parent), even from a worktree. + +resolve_workspace_dir() { + local script_dir="$1" + local git_common main_repo_root + git_common="$(cd "$script_dir" && git rev-parse --git-common-dir 2>/dev/null)" + main_repo_root="$(cd "$script_dir" && cd "$git_common/.." 2>/dev/null && pwd)" + dirname "${main_repo_root:-$script_dir}" +}