diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 1e00396..a6272f3 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -102,6 +102,10 @@ export default defineConfig({ text: "Operator Deployment", link: "/guide/deployment/operator-deployment", }, + { + text: "Orchestrator Deployment", + link: "/guide/deployment/orchestrator-deployment", + }, { text: "Authentication Providers", link: "/guide/deployment/authentication", @@ -203,6 +207,7 @@ export default defineConfig({ }, { text: "KeycloakHelper", link: "/api/deployment/keycloak-helper" }, { text: "Keycloak Types", link: "/api/deployment/keycloak-types" }, + { text: "installOrchestrator", link: "/api/deployment/orchestrator" }, ], }, { diff --git a/docs/api/deployment/orchestrator.md b/docs/api/deployment/orchestrator.md new file mode 100644 index 0000000..05dbd3e --- /dev/null +++ b/docs/api/deployment/orchestrator.md @@ -0,0 +1,47 @@ +# installOrchestrator + +Runs the bundled orchestrator install script in the given OpenShift/Kubernetes namespace. The script ensures the namespace exists (reuses or creates), sets the current context, and deploys PostgreSQL, operator, and orchestrator workflows. + +## Import + +```typescript +import installOrchestrator from "@red-hat-developer-hub/e2e-test-utils/orchestrator"; +``` + +Named import is also supported: + +```typescript +import { installOrchestrator } from "@red-hat-developer-hub/e2e-test-utils/orchestrator"; +``` + +## Function + +### `installOrchestrator(namespace?)` + +```typescript +function installOrchestrator(namespace?: string): Promise +``` + +| Parameter | Type | Default | Description | +| ----------- | -------- | --------------- | ------------------------------------ | +| `namespace` | `string` | `"orchestrator"` | Target OpenShift/Kubernetes namespace | + +**Returns:** `Promise` — Resolves when the script completes successfully; rejects on script failure or if not logged into a cluster. + +## Example + +```typescript +import installOrchestrator from "@red-hat-developer-hub/e2e-test-utils/orchestrator"; + +await installOrchestrator(); // uses namespace "orchestrator" +await installOrchestrator("my-e2e-orchestrator"); +``` + +## Requirements + +- Cluster access: `oc` (or `kubectl`) in `PATH` and already logged in +- The script runs in the same process (blocking until the shell script exits) + +## Related Pages + +- [Orchestrator Deployment (Guide)](/guide/deployment/orchestrator-deployment) - Usage patterns and prerequisites diff --git a/docs/changelog.md b/docs/changelog.md index 9499339..f1d70a4 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,7 +2,13 @@ All notable changes to this project will be documented in this file. -## [1.1.18] - Current +## [1.1.19] - Current + +### Added + +- **installOrchestrator(namespace?: string)**: Runs the orchestrator install script via a TypeScript wrapper; creates or reuses the given namespace (default `"orchestrator"`). Exported from `@red-hat-developer-hub/e2e-test-utils/orchestrator`. + +## [1.1.18] ### Added diff --git a/docs/guide/deployment/index.md b/docs/guide/deployment/index.md index 4c57940..e9c0c9e 100644 --- a/docs/guide/deployment/index.md +++ b/docs/guide/deployment/index.md @@ -166,4 +166,5 @@ Later configurations override earlier ones, allowing you to customize only what - [Keycloak Deployment](/guide/deployment/keycloak-deployment) - KeycloakHelper class - [Helm Deployment](/guide/deployment/helm-deployment) - Helm-specific guide - [Operator Deployment](/guide/deployment/operator-deployment) - Operator-specific guide +- [Orchestrator Deployment](/guide/deployment/orchestrator-deployment) - installOrchestrator script - [Authentication](/guide/deployment/authentication) - Auth providers diff --git a/docs/guide/deployment/orchestrator-deployment.md b/docs/guide/deployment/orchestrator-deployment.md new file mode 100644 index 0000000..d085e32 --- /dev/null +++ b/docs/guide/deployment/orchestrator-deployment.md @@ -0,0 +1,74 @@ +# Orchestrator Deployment + +The package provides a script-based installer for the orchestrator (workflows, PostgreSQL, and related resources) in an OpenShift/Kubernetes namespace. Use it when your E2E tests depend on a pre-installed orchestrator in the cluster. + +## Overview + +`installOrchestrator` runs a bundled shell script that: + +1. Ensures the target namespace exists (reuses it if present, creates it if not) +2. Sets the current `oc`/`kubectl` context to that namespace +3. Deploys PostgreSQL, operator, and orchestrator workflows as defined by the script + +The script is intended for use in global setup, `beforeAll`, or standalone tooling—not for per-test runs. + +## Prerequisites + +- OpenShift or Kubernetes cluster and `oc` (or `kubectl`) in `PATH` +- You must be logged in: `oc login` (or equivalent) +- The script expects `bash` + +## Basic Usage + +```typescript +import installOrchestrator from "@red-hat-developer-hub/e2e-test-utils/orchestrator"; + +// Use default namespace "orchestrator" +await installOrchestrator(); + +// Use a custom namespace +await installOrchestrator("my-orchestrator-ns"); +``` + +## Usage in Tests + +### Global setup + +Run once before all tests: + +```typescript +// global-setup.ts +import installOrchestrator from "@red-hat-developer-hub/e2e-test-utils/orchestrator"; + +export default async function globalSetup() { + const namespace = process.env.ORCHESTRATOR_NAMESPACE ?? "orchestrator"; + await installOrchestrator(namespace); +} +``` + +### Before all tests in a file + +```typescript +import { test } from "@red-hat-developer-hub/e2e-test-utils/test"; +import installOrchestrator from "@red-hat-developer-hub/e2e-test-utils/orchestrator"; + +test.beforeAll(async () => { + await installOrchestrator("orchestrator"); +}); + +test("uses orchestrator", async () => { + // ... +}); +``` + +## Namespace behavior + +- If the namespace **does not exist**, it is created and the script continues with deployment. +- If the namespace **already exists**, it is reused (not deleted or recreated). The script configures the context and proceeds with deployment steps that are idempotent where applicable. + +This allows reusing the same namespace across runs or sharing it with other tooling. + +## Related Pages + +- [installOrchestrator API](/api/deployment/orchestrator) - Function signature and options +- [Deployment Overview](/guide/deployment/) - Other deployment options (RHDH, Keycloak) diff --git a/package.json b/package.json index 42dda34..b0b3df1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@red-hat-developer-hub/e2e-test-utils", - "version": "1.1.18", + "version": "1.1.19", "description": "Test utilities for RHDH E2E tests", "license": "Apache-2.0", "repository": { @@ -48,6 +48,10 @@ "./teardown": { "types": "./dist/playwright/teardown-namespaces.d.ts", "default": "./dist/playwright/teardown-namespaces.js" + }, + "./orchestrator": { + "types": "./dist/deployment/orchestrator/index.d.ts", + "default": "./dist/deployment/orchestrator/index.js" } }, "publishConfig": { @@ -58,7 +62,7 @@ "tsconfig.base.json" ], "scripts": { - "build": "yarn clean && tsc -p tsconfig.build.json && cp -r src/deployment/rhdh/config dist/deployment/rhdh/ && cp -r src/deployment/keycloak/config dist/deployment/keycloak/", + "build": "yarn clean && tsc -p tsconfig.build.json && cp -r src/deployment/rhdh/config dist/deployment/rhdh/ && cp -r src/deployment/keycloak/config dist/deployment/keycloak/ && cp src/deployment/orchestrator/install-orchestrator.sh dist/deployment/orchestrator/", "prepare": "husky", "check": "yarn typecheck && yarn lint:check && yarn prettier:check", "clean": "rm -rf dist", diff --git a/src/deployment/orchestrator/index.ts b/src/deployment/orchestrator/index.ts new file mode 100644 index 0000000..a56a70f --- /dev/null +++ b/src/deployment/orchestrator/index.ts @@ -0,0 +1,10 @@ +import { resolve } from "path"; +import { $ } from "../../utils/index.js"; + +const scriptPath = resolve(import.meta.dirname, "install-orchestrator.sh"); + +export async function installOrchestrator(namespace = "orchestrator") { + await $`bash ${scriptPath} ${namespace}`; +} + +export default installOrchestrator; diff --git a/src/deployment/orchestrator/install-orchestrator.sh b/src/deployment/orchestrator/install-orchestrator.sh new file mode 100755 index 0000000..086f06a --- /dev/null +++ b/src/deployment/orchestrator/install-orchestrator.sh @@ -0,0 +1,477 @@ +#!/bin/bash +# +# Standalone script to install the orchestrator (Serverless Logic / SonataFlow) +# on OpenShift. +# +# Usage: ./install-orchestrator.sh [namespace] +# Default namespace: orchestrator +# + +set -e + +export NAME_SPACE="${1:-${NAME_SPACE:-orchestrator}}" + +LOWER_CASE_CLASS='[:lower:]' +UPPER_CASE_CLASS='[:upper:]' + +# --------------------------------------------------------------------------- +# Logging +# --------------------------------------------------------------------------- +if [[ -t 1 ]] && [[ "${TERM:-}" != "dumb" ]]; then + : "${LOG_NO_COLOR:=false}" +else + : "${LOG_NO_COLOR:=true}" +fi +: "${LOG_LEVEL:=INFO}" + +log::timestamp() { + echo "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" + return 0 +} +log::level_value() { + local input="$1" + local level + level="$(echo "$input" | tr "$LOWER_CASE_CLASS" "$UPPER_CASE_CLASS")" + case "${level}" in DEBUG) echo 0 ;; INFO) echo 1 ;; WARN|WARNING) echo 2 ;; ERROR|ERR) echo 3 ;; *) echo 1 ;; esac + return 0; +} +log::should_log() { + local input requested_level config_level + input="$1" + requested_level="$(echo "$input" | tr "$LOWER_CASE_CLASS" "$UPPER_CASE_CLASS")" + config_level="$(echo "${LOG_LEVEL}" | tr "$LOWER_CASE_CLASS" "$UPPER_CASE_CLASS")" + + [[ "$(log::level_value "${requested_level}")" -ge "$(log::level_value "${config_level}")" ]] + return $? +} +log::reset_code() { + [[ "${LOG_NO_COLOR}" == "true" ]] && printf '' || printf '\033[0m' + return 0; +} +log::color_for_level() { + [[ "${LOG_NO_COLOR}" == "true" ]] && { printf ''; return 0; } + local level input + input="$1" + level="$(echo "$input" | tr "$LOWER_CASE_CLASS" "$UPPER_CASE_CLASS")" + case "${level}" in + DEBUG) printf '\033[36m' ;; INFO) printf '\033[34m' ;; WARN|WARNING) printf '\033[33m' ;; + ERROR|ERR) printf '\033[31m' ;; SUCCESS) printf '\033[32m' ;; SECTION) printf '\033[35m\033[1m' ;; + *) printf '\033[37m' ;; + esac +} +log::icon_for_level() { + local level input + input="$1" + level="$(echo "$input" | tr "$LOWER_CASE_CLASS" "$UPPER_CASE_CLASS")" + case "${level}" in DEBUG) printf '🐞' ;; INFO) printf 'ℹ' ;; WARN|WARNING) printf '⚠' ;; ERROR|ERR) printf '❌' ;; SUCCESS) printf '✓' ;; *) printf '-' ;; esac + return 0 +} +log::emit_line() { + local level="$1" icon="$2" line="$3" color reset timestamp + log::should_log "${level}" || return 0 + timestamp="$(log::timestamp)" + color="$(log::color_for_level "${level}")" + reset="$(log::reset_code)" + printf '%s[%s] %s %s%s\n' "${color}" "${timestamp}" "${icon}" "${line}" "${reset}" >&2 +} +log::emit() { + local level="$1"; shift + local icon message; icon="$(log::icon_for_level "${level}")"; message="${*:-}" + [[ -z "${message}" ]] && return 0 + while IFS= read -r line; do log::emit_line "${level}" "${icon}" "${line}"; done <<< "${message}" +} +log::debug() { + log::emit "DEBUG" "$@" + return 0 +} +log::info() { + log::emit "INFO" "$@" + return 0 +} +log::warn() { + log::emit "WARN" "$@" + return 0 +} +log::error() { + log::emit "ERROR" "$@" + return 0 +} +log::success() { + log::emit "SUCCESS" "$@" + return 0 +} + +# --------------------------------------------------------------------------- +# Operator subscription and status +# --------------------------------------------------------------------------- +install_subscription() { + local name=$1 namespace=$2 channel=$3 package=$4 source_name=$5 source_namespace=$6 + oc apply -f - << EOD +apiVersion: operators.coreos.com/v1alpha1 +kind: Subscription +metadata: + name: $name + namespace: $namespace +spec: + channel: $channel + installPlanApproval: Automatic + name: $package + source: $source_name + sourceNamespace: $source_namespace +EOD + return 0 +} + +check_operator_status() { + local timeout=${1:-300} namespace=$2 operator_name=$3 expected_status=${4:-Succeeded} + log::info "Checking operator '${operator_name}' in '${namespace}' (timeout ${timeout}s, expected: ${expected_status})" + timeout "${timeout}" bash -c " + while true; do + CURRENT_PHASE=\$(oc get csv -n '${namespace}' -o jsonpath='{.items[?(@.spec.displayName==\"${operator_name}\")].status.phase}') + echo \"[check_operator_status] Phase: \${CURRENT_PHASE}\" >&2 + [[ \"\${CURRENT_PHASE}\" == \"${expected_status}\" ]] && echo \"[check_operator_status] Operator reached ${expected_status}\" >&2 && break + sleep 10 + done + " || { log::error "Operator did not reach ${expected_status} in time."; return 1; } +} + +install_serverless_logic_ocp_operator() { + install_subscription logic-operator-rhel8 openshift-operators alpha logic-operator-rhel8 redhat-operators openshift-marketplace + return 0 +} +waitfor_serverless_logic_ocp_operator() { + check_operator_status 500 openshift-operators "OpenShift Serverless Logic Operator (Alpha)" Succeeded + return 0 +} + +install_serverless_ocp_operator() { + install_subscription serverless-operator openshift-operators stable serverless-operator redhat-operators openshift-marketplace + return 0 +} +waitfor_serverless_ocp_operator() { + check_operator_status 300 openshift-operators "Red Hat OpenShift Serverless" Succeeded + return 0 +} + +# --------------------------------------------------------------------------- +# Namespace +# --------------------------------------------------------------------------- +force_delete_namespace() { + local project=$1 timeout_seconds=${2:-120} elapsed=0 sleep_interval=2 + log::warn "Force deleting namespace ${project}" + oc get namespace "$project" -o json | jq '.spec = {"finalizers":[]}' | oc replace --raw "/api/v1/namespaces/$project/finalize" -f - + while oc get namespace "$project" &>/dev/null; do + [[ $elapsed -ge $timeout_seconds ]] && { log::warn "Timeout deleting ${project}"; return 1; } + sleep $sleep_interval + elapsed=$((elapsed + sleep_interval)) + done + log::success "Namespace '${project}' deleted." +} + +delete_namespace() { + local project=$1 + if oc get namespace "$project" &>/dev/null; then + log::warn "Deleting namespace ${project}..." + oc delete namespace "$project" --grace-period=0 --force || true + if oc get namespace "$project" -o jsonpath='{.status.phase}' 2>/dev/null | grep -q Terminating; then + force_delete_namespace "$project" + fi + fi + return 0 +} + +configure_namespace() { + local project=$1 + if oc get namespace "$project" &>/dev/null; then + log::info "Namespace ${project} already exists, reusing it." + else + log::info "Creating namespace: ${project}" + oc create namespace "${project}" || { log::error "Failed to create namespace ${project}"; exit 1; } + fi + oc config set-context --current --namespace="${project}" || { log::error "Failed to set context"; exit 1; } + log::info "Namespace ${project} is ready." + return 0 +} + +# --------------------------------------------------------------------------- +# Deployment wait +# --------------------------------------------------------------------------- +wait_for_deployment() { + local namespace=$1 resource_name=$2 timeout_minutes=${3:-5} check_interval=${4:-10} + [[ -z "$namespace" || -z "$resource_name" ]] && { log::error "wait_for_deployment: namespace and resource_name required"; return 1; } + local max_attempts=$((timeout_minutes * 60 / check_interval)) + log::info "Waiting for '$resource_name' in '$namespace' (timeout ${timeout_minutes}m)..." + for ((i = 1; i <= max_attempts; i++)); do + local pod_name + pod_name=$(oc get pods -n "$namespace" 2>/dev/null | grep "$resource_name" | awk '{print $1}' | head -n 1) + if [[ -n "$pod_name" ]]; then + local is_ready + is_ready=$(oc get pod "$pod_name" -n "$namespace" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null) + if [[ "$is_ready" == "True" ]] && oc get pod "$pod_name" -n "$namespace" 2>/dev/null | grep -q Running; then + log::success "Pod '$pod_name' is ready" + return 0 + fi + fi + sleep "$check_interval" + done + log::error "Timeout waiting for $resource_name" + return 1 +} + +# --------------------------------------------------------------------------- +# PostgreSQL (simple deployment for orchestrator) +# --------------------------------------------------------------------------- +create_simple_postgres_deployment() { + local namespace=$1 postgres_name="backstage-psql" + if oc get deployment "$postgres_name" -n "$namespace" &>/dev/null; then + log::info "PostgreSQL '$postgres_name' already exists" + return 0 + fi + log::info "Creating PostgreSQL '$postgres_name' in '$namespace'" + oc create secret generic "${postgres_name}-secret" -n "$namespace" \ + --from-literal=POSTGRESQL_USER=postgres \ + --from-literal=POSTGRESQL_PASSWORD=postgres \ + --from-literal=POSTGRESQL_DATABASE=postgres \ + --from-literal=POSTGRES_USER=postgres \ + --from-literal=POSTGRES_PASSWORD=postgres \ + --from-literal=POSTGRES_DB=postgres \ + --dry-run=client -o yaml | oc apply -f - -n "$namespace" || true + + oc apply -f - -n "$namespace" << EOF +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: ${postgres_name}-pvc + namespace: ${namespace} +spec: + accessModes: [ReadWriteOnce] + resources: { requests: { storage: 1Gi } } +EOF + + oc apply -f - -n "$namespace" << EOF +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: ${postgres_name} + namespace: ${namespace} +spec: + serviceName: ${postgres_name} + replicas: 1 + selector: { matchLabels: { app: ${postgres_name} } } + template: + metadata: { labels: { app: ${postgres_name} } } + spec: + containers: + - name: postgres + image: registry.redhat.io/rhel9/postgresql-15:latest + env: + - name: POSTGRESQL_USER + valueFrom: { secretKeyRef: { name: ${postgres_name}-secret, key: POSTGRESQL_USER } } + - name: POSTGRESQL_PASSWORD + valueFrom: { secretKeyRef: { name: ${postgres_name}-secret, key: POSTGRESQL_PASSWORD } } + - name: POSTGRESQL_DATABASE + valueFrom: { secretKeyRef: { name: ${postgres_name}-secret, key: POSTGRESQL_DATABASE } } + ports: [ { containerPort: 5432, name: postgres } ] + volumeMounts: [ { name: postgres-data, mountPath: /var/lib/pgsql/data } ] + livenessProbe: + exec: { command: [ /usr/libexec/check-container, --live ] } + initialDelaySeconds: 120 + periodSeconds: 10 + readinessProbe: + exec: { command: [ /usr/libexec/check-container ] } + initialDelaySeconds: 5 + periodSeconds: 10 + volumes: [ { name: postgres-data, persistentVolumeClaim: { claimName: ${postgres_name}-pvc } } ] +EOF + + oc apply -f - -n "$namespace" << EOF +apiVersion: v1 +kind: Service +metadata: + name: ${postgres_name} + namespace: ${namespace} +spec: + selector: { app: ${postgres_name} } + ports: [ { name: postgres, port: 5432, targetPort: 5432 } ] + type: ClusterIP +EOF + + log::info "Waiting for PostgreSQL StatefulSet..." + oc wait statefulset "$postgres_name" -n "$namespace" --for=jsonpath='{.status.readyReplicas}'=1 --timeout=300s || true + sleep 5 + oc exec -n "$namespace" statefulset/"$postgres_name" -- psql -U postgres -c "CREATE DATABASE backstage_plugin_orchestrator;" 2>/dev/null || log::warn "Orchestrator DB may already exist" + log::success "PostgreSQL deployment created." +} + +# --------------------------------------------------------------------------- +# SonataFlow platform +# --------------------------------------------------------------------------- +create_sonataflow_platform() { + local namespace=$1 postgres_secret_name=$2 postgres_service_name=$3 + if ! oc get crd sonataflowplatforms.sonataflow.org &>/dev/null && ! oc get crd sonataflowplatform.sonataflow.org &>/dev/null; then + log::error "SonataFlowPlatform CRD not found. Install Serverless Logic Operator first." + return 1 + fi + if oc get sonataflowplatform sonataflow-platform -n "$namespace" &>/dev/null || oc get sfp sonataflow-platform -n "$namespace" &>/dev/null; then + log::info "SonataFlowPlatform already exists" + return 0 + fi + log::info "Creating SonataFlowPlatform in '$namespace'" + oc apply -f - -n "$namespace" << EOF +apiVersion: sonataflow.org/v1alpha08 +kind: SonataFlowPlatform +metadata: + name: sonataflow-platform + namespace: ${namespace} +spec: + services: + dataIndex: + persistence: + postgresql: + secretRef: { name: ${postgres_secret_name}, userKey: POSTGRES_USER, passwordKey: POSTGRES_PASSWORD } + serviceRef: { name: ${postgres_service_name}, namespace: ${namespace}, port: 5432, databaseName: backstage_plugin_orchestrator } + jobService: + persistence: + postgresql: + secretRef: { name: ${postgres_secret_name}, userKey: POSTGRES_USER, passwordKey: POSTGRES_PASSWORD } + serviceRef: { name: ${postgres_service_name}, namespace: ${namespace}, port: 5432, databaseName: backstage_plugin_orchestrator } +EOF + local attempt=0 max_attempts=60 + while [[ $attempt -lt $max_attempts ]]; do + if oc get deployment sonataflow-platform-data-index-service -n "$namespace" &>/dev/null && \ + oc get deployment sonataflow-platform-jobs-service -n "$namespace" &>/dev/null; then + log::success "SonataFlowPlatform services created" + wait_for_deployment "$namespace" sonataflow-platform-data-index-service 20 || true + wait_for_deployment "$namespace" sonataflow-platform-jobs-service 20 || true + log::success "SonataFlowPlatform ready." + return 0 + fi + attempt=$((attempt + 1)) + [[ $((attempt % 10)) -eq 0 ]] && log::info "Waiting for SonataFlowPlatform... ($attempt/$max_attempts)" + sleep 5 + done + log::warn "SonataFlowPlatform services did not appear in time." +} + +# --------------------------------------------------------------------------- +# Orchestrator connection info +# --------------------------------------------------------------------------- +print_orchestrator_connection_info() { + local namespace=$1 + local data_index_service="sonataflow-platform-data-index-service" + local service_url="http://${data_index_service}.${namespace}.svc.cluster.local" + log::info "==========================================" + log::info "Orchestrator Plugin Connection Information" + log::info "==========================================" + log::info "Namespace: ${namespace}" + log::info "Internal URL for Orchestrator Backend Plugin: ${service_url}" + log::info "dynamic-plugins.yaml: pluginConfig.orchestrator.dataIndexService.url: ${service_url}" + if oc get svc "${data_index_service}" -n "${namespace}" &>/dev/null; then + local port; port=$(oc get svc "${data_index_service}" -n "${namespace}" -o jsonpath='{.spec.ports[0].port}' 2>/dev/null || echo "8080") + log::info "Service: ${data_index_service}, port: ${port}" + else + log::warn "Service '${data_index_service}' not found yet." + fi + log::info "==========================================" + return 0 +} + +# --------------------------------------------------------------------------- +# Wait for SonataFlow CRDs +# --------------------------------------------------------------------------- +wait_for_sonataflow_crds() { + log::info "Waiting for SonataFlow CRDs..." + local attempt=0 max_attempts=60 + while [[ $attempt -lt $max_attempts ]]; do + if oc get crd sonataflows.sonataflow.org &>/dev/null; then + log::success "SonataFlow CRD is available." + return 0 + fi + attempt=$((attempt + 1)) + [[ $((attempt % 6)) -eq 0 ]] && log::info "Waiting for sonataflows.sonataflow.org... ($attempt/$max_attempts)" + sleep 5 + done + log::error "Timed out waiting for SonataFlow CRD." + return 1 +} + +# --------------------------------------------------------------------------- +# Deploy orchestrator workflows (operator path: git clone + helm greeting) +# Uses local yaml/ if present, otherwise clones repo. +# --------------------------------------------------------------------------- +deploy_orchestrator_workflows_operator() { + local namespace=$1 + + # PostgreSQL + if ! oc get statefulset backstage-psql -n "$namespace" &>/dev/null && ! oc get deployment backstage-psql -n "$namespace" &>/dev/null; then + log::info "Creating simple PostgreSQL deployment..." + create_simple_postgres_deployment "$namespace" + else + log::info "PostgreSQL found, waiting for ready..." + if oc get statefulset backstage-psql -n "$namespace" &>/dev/null; then + oc wait statefulset backstage-psql -n "$namespace" --for=jsonpath='{.status.readyReplicas}'=1 --timeout=300s || true + else + wait_for_deployment "$namespace" backstage-psql 15 || true + fi + fi + + local psql_secret_name psql_svc_name + psql_secret_name=$(oc get secrets -n "$namespace" -o name 2>/dev/null | grep "backstage-psql" | grep "secret" | head -1 | sed 's|secret\/||') + psql_svc_name='backstage-psql' + + log::info "PostgreSQL secret: $psql_secret_name, service: $psql_svc_name" + + if ! oc get sonataflowplatform sonataflow-platform -n "$namespace" &>/dev/null && ! oc get sfp sonataflow-platform -n "$namespace" &>/dev/null; then + create_sonataflow_platform "$namespace" "$psql_secret_name" "$psql_svc_name" + else + log::info "SonataFlowPlatform already exists" + wait_for_deployment "$namespace" sonataflow-platform-data-index-service 20 || true + wait_for_deployment "$namespace" sonataflow-platform-jobs-service 20 || true + fi + + if ! oc get crd sonataflows.sonataflow.org &>/dev/null; then + log::error "SonataFlow CRD not found." + return 1 + fi +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- +main() { + log::info "Starting orchestrator deployment for namespace: ${NAME_SPACE}" + + if ! oc whoami &>/dev/null && ! kubectl cluster-info &>/dev/null; then + log::error "Not logged into OpenShift/Kubernetes cluster" + return 1 + fi + + log::info "Checking Serverless operators..." + if ! oc get subscription serverless-operator -n openshift-operators &>/dev/null; then + log::info "Installing OpenShift Serverless Operator..." + install_serverless_ocp_operator + else + log::info "OpenShift Serverless Operator already installed" + fi + + if ! oc get subscription logic-operator-rhel8 -n openshift-operators &>/dev/null; then + log::info "Installing OpenShift Serverless Logic Operator..." + install_serverless_logic_ocp_operator + else + log::info "OpenShift Serverless Logic Operator already installed" + fi + + log::info "Waiting for operators to be ready..." + waitfor_serverless_ocp_operator + waitfor_serverless_logic_ocp_operator + wait_for_sonataflow_crds + + configure_namespace "${NAME_SPACE}" + log::info "Deploying orchestrator workflows..." + deploy_orchestrator_workflows_operator "${NAME_SPACE}" + print_orchestrator_connection_info "${NAME_SPACE}" + + log::success "Orchestrator deployment completed successfully!" +} + +main "$@"