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..dc36d55 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,10 @@ .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 -.claude/ \ No newline at end of file +.claude/ +.worktrees/ 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 dc0c6ff..38c49bc 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-app-next 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) --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)) + +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: ## 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) +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 58576a1..d0e9a8d 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,41 @@ 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`, 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 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 + +# 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 +./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-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` (NFS `orchestrator-app-next` project). Manifests live in `config/osl-releases/`. + #### Status and Debugging ```bash @@ -200,6 +235,9 @@ 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-app-next` | Namespace used by orchestrator/OSL setup and cleanup (NFS Playwright project) | +| `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`). @@ -373,6 +411,8 @@ 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 ├── helm/ @@ -399,7 +439,24 @@ rhdh-test-instance/ │ └── plugins/ │ ├── config-keycloak-plugin.sh # Keycloak deploy, realm/client/user setup │ └── config-lighthouse-plugin.sh # Lighthouse deploy and URL injection +├── utils/ +│ ├── 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 +├── 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/cleanup.sh b/cleanup.sh new file mode 100755 index 0000000..87d92b1 --- /dev/null +++ b/cleanup.sh @@ -0,0 +1,329 @@ +#!/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 (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. + +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 + +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 +require_oc_login + +# Validate namespace +validate_k8s_namespace "$namespace" + +echo "===========================================" +echo " RHDH / Orchestrator Cleanup" +echo "===========================================" +echo "Namespace: $namespace" +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 +# --------------------------------------------------------------------------- +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 +} + +OSL_OLM_MATCH='logic-operator|serverless-operator' + +delete_osl_olm_resources() { + local kind="$1" + local ns="$2" + local resource name + + 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 +} + +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 + local ns remaining_subs remaining_csvs knative_webhooks + + echo "--- Post-clean verification ---" + + if [[ "$include_operators" == "true" ]]; then + for ns in "${OPERATOR_NAMESPACES[@]}"; do + if oc get namespace "$ns" &>/dev/null; then + echo " Remaining namespace: $ns" + failures=1 + fi + done + + 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 + + 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 + + if [[ "$delete_namespace" == "true" ]] && oc get namespace "$namespace" &>/dev/null; then + echo " Remaining target namespace: $namespace" + 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 leftovers for this cleanup mode." +} + +# --------------------------------------------------------------------------- +# 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) +# --------------------------------------------------------------------------- +for ns in "${RELATED_NAMESPACES[@]}"; do + if [[ "$namespace" != "$ns" ]]; then + clean_namespace "$ns" + fi +done + +# --------------------------------------------------------------------------- +# 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 + + for ns in openshift-serverless-logic openshift-serverless openshift-operators; do + delete_osl_olm_resources subscriptions.operators.coreos.com "$ns" + delete_osl_olm_resources csv "$ns" + 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 "${OPERATOR_NAMESPACES[@]}"; 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..04ead55 --- /dev/null +++ b/config/app-config-oidc.yaml @@ -0,0 +1,22 @@ +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: + # Upstream oidc-provider (GHCR pin) exposes emailLocalPart / emailMatching + # only. preferredUsernameMatchingUserEntityName is not in that module. + - resolver: emailLocalPartMatchingUserEntityName + 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..0ccc8ee --- /dev/null +++ b/config/orchestrator-dynamic-plugins-next.yaml @@ -0,0 +1,19 @@ +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 + - 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/README.md b/config/osl-releases/README.md new file mode 100755 index 0000000..f2b1e3b --- /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 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 new file mode 100755 index 0000000..ef0197c --- /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 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" + }, + { + "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..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 @@ -64,7 +70,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 +127,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/helm/deploy.sh b/helm/deploy.sh index 84172a6..cfa560b 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,49 @@ 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)" + orch_file="config/orchestrator-dynamic-plugins.yaml" + merge_orch=false + if [[ "$version" == "next" || "$version" == *-CI ]]; then + orch_file="config/orchestrator-dynamic-plugins-next.yaml" + # 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 + 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 +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 +112,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,13 +148,43 @@ 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 +# 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" --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 new file mode 100644 index 0000000..46b654c --- /dev/null +++ b/playwright/osl-regression-smoke.spec.ts @@ -0,0 +1,28 @@ +// 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 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 } 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"; + +test.beforeEach(async ({ page }) => { + const origGetByRole = page.getByRole.bind(page); + page.getByRole = (role, options) => { + if (role === "button" && options && options.name === "Run" && !options.exact) { + return origGetByRole(role, { ...options, exact: true }); + } + return origGetByRole(role, options); + }; +}); + +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 new file mode 100755 index 0000000..d34b764 --- /dev/null +++ b/prepare-osl-internal.sh @@ -0,0 +1,536 @@ +#!/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. + +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" + +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-app-next" + +# --------------------------------------------------------------------------- +# 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-app-next) + --multi-arch Mirror all architectures (default: amd64 only) + -h, --help Show this help +EOF +} + +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" + local dest_digest="" src_digest="" dest_json src_json + + 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 + 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 + 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" + --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}"; 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@sha256:3bbabf4be41d2d071ce5dd2fe35040139848331c95dfb23ff06f5ba47fd13203 +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 + +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)" + +# 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" + +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 +# --------------------------------------------------------------------------- +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" < prepare-osl -> deploy -> test. +# 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 +# ./run-osl-regression.sh --cleanup --namespace orchestrator-app-next +# ./run-osl-regression.sh --test --overlays-dir ../rhdh-plugin-export-overlays +# +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/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" +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" +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" + +run_all=false +run_cleanup=false +run_prepare=false +run_deploy=false +run_test=false +allow_relative_service_url=false +rhdh="" +osl_release="" +osl_manifest="" +namespace="$DEFAULT_NAMESPACE" +overlays_dir="$DEFAULT_OVERLAYS" + +usage() { + cat < 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: ${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 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 +} + +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 ;; + --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 +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" +} + +# 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" + 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 + require_oc_login "oc whoami failed; log into a cluster first" + + if [[ "$run_prepare" == "true" ]]; then + require_cmd podman + require_cmd skopeo + 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 + [[ -n "$rhdh" ]] || die "--rhdh is required when --test is selected" + 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 +} + +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" +} + +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" + local backup="" + if [[ -f "$path" ]]; then + backup="${e2e}/.env.osl-regression.bak" + cp -a "$path" "$backup" + fi + cat > "$path" </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:-}\" + } + }" + 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. local session_secret @@ -62,8 +86,9 @@ 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}" - + patch_nfs_secrets echo "rhdh-secrets created!" fi } diff --git a/setup-orchestrator.sh b/setup-orchestrator.sh new file mode 100755 index 0000000..6513ae2 --- /dev/null +++ b/setup-orchestrator.sh @@ -0,0 +1,389 @@ +#!/bin/bash +# +# One-command setup of RHDH + orchestrator for overlays e2e. +# Deploys Keycloak, installs orchestrator prerequisites, deploys RHDH via Helm, +# 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 ] +# +# 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-app-next) +# --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)" +# 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}" +SHARED_INSTALL_SCRIPT="${RHDH_E2E_TEST_UTILS_DIR}/dist/deployment/orchestrator/install-orchestrator.sh" +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 ───────────────────────────────────────────────────────── + +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-app-next" +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 + +cd "$SCRIPT_DIR" + +# ── Validate inputs ────────────────────────────────────────────────────────── + +require_oc_login +validate_k8s_namespace "$namespace" + +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_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" || ! -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) + 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 build rhdh-e2e-test-utils (yarn build)." + 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" +} + +prepare_keycloak() { + bash "$SCRIPT_DIR/utils/keycloak/keycloak-deploy.sh" "$KEYCLOAK_NAMESPACE" +} + +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" +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() { + local ns="$1" + local timeout_secs="${2:-240}" + local start_time rhdh_url + start_time=$(date +%s) + rhdh_url="$(openshift_route_url redhat-developer-hub "$ns")" || { + echo "Error: Could not resolve RHDH route in namespace '$ns'." + return 1 + } + + 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 )) + 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}' "${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}' "${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 + 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 + + log "Running post-setup workflow smoke in namespace ${ns}..." + 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="$(openshift_route_url redhat-developer-hub "$ns")" || { + echo "Error: Could not resolve RHDH route for post-setup smoke." + 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})." + exit 1 + fi + + 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" +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 +export_keycloak_runtime_env "$KEYCLOAK_NAMESPACE" + +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" +SKIP_ENV_SOURCE=1 \ +SKIP_ORCHESTRATOR_INFRA_INSTALL=1 \ +./deploy.sh helm "$version" --namespace "$namespace" --with-orchestrator +phase_checkpoint "rhdh-deployed" + +RHDH_BASE_URL="$(openshift_route_url redhat-developer-hub "$namespace")" || { + echo "Error: Could not resolve RHDH route after deploy." + exit 1 +} +export RHDH_BASE_URL +bash "$SCRIPT_DIR/utils/keycloak/update-rhdh-client-redirects.sh" "$KEYCLOAK_NAMESPACE" "$RHDH_BASE_URL" + +# ── 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" +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_URL="${RHDH_BASE_URL}" +KEYCLOAK_URL="${KEYCLOAK_BASE_URL:-}" + +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..f06daa6 --- /dev/null +++ b/utils/keycloak/keycloak-deploy.sh @@ -0,0 +1,202 @@ +#!/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} +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" + +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; } +[ -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 "$KEYCLOAK_VALUES" + +echo "Waiting for Keycloak rollout..." +oc rollout status statefulset/keycloak -n $NAMESPACE --timeout=5m + +KEYCLOAK_PROTOCOL="$(keycloak_console_protocol)" + +# 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 + +ADMIN_TOKEN="$(keycloak_admin_token "$KEYCLOAK_URL")" + +# Create realm and client +echo "Creating realm 'rhdh'..." +keycloak_api_call POST "$KEYCLOAK_URL/admin/realms" \ + '{"realm":"rhdh","enabled":true,"displayName":"RHDH Realm"}' \ + "Create realm" >/dev/null + +echo "Creating client..." +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=$(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=$(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=$(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..." +keycloak_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 + 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 +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 ! 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 + echo " Created user: $username" + + # Add user to groups + if [ -n "$groups" ]; then + 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=$(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 + 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 + 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" +export KEYCLOAK_PROTOCOL 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/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/rhdh-client.json b/utils/keycloak/rhdh-client.json new file mode 100755 index 0000000..fca8515 --- /dev/null +++ b/utils/keycloak/rhdh-client.json @@ -0,0 +1,82 @@ +{ + "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": false, + "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/update-rhdh-client-redirects.sh b/utils/keycloak/update-rhdh-client-redirects.sh new file mode 100755 index 0000000..ff46a9f --- /dev/null +++ b/utils/keycloak/update-rhdh-client-redirects.sh @@ -0,0 +1,36 @@ +#!/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; } + +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" ]] || { + echo "Usage: $0 " >&2 + exit 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" + +ADMIN_TOKEN="$(keycloak_admin_token "$KEYCLOAK_URL")" + +client_uuid=$(keycloak_api_call GET "$KEYCLOAK_URL/admin/realms/rhdh/clients?clientId=rhdh-client" "" "Get rhdh-client" | \ + jq -r '.[0].id // empty') +[[ -n "$client_uuid" ]] || { echo "Error: rhdh-client UUID not found" >&2; exit 1; } + +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') +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/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/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 new file mode 100755 index 0000000..8c738f2 --- /dev/null +++ b/utils/orchestrator/deploy-smoke-workflows.sh @@ -0,0 +1,243 @@ +#!/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 + +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" +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 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})" 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/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/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}" +}