From 54dde5c82417aadad69c614490e499169cda87bb Mon Sep 17 00:00:00 2001 From: Oleksandr Andriienko Date: Tue, 17 Mar 2026 02:15:34 +0200 Subject: [PATCH 1/9] feat(e2e): share orchestrator installation script Signed-off-by: Oleksandr Andriienko Co-authored-by: Yona First Co-authored-by: Gustavo Lira e Silva --- package.json | 1 + scripts/install-orchestrator.sh | 543 ++++++++++++++++++++++++++++++++ 2 files changed, 544 insertions(+) create mode 100755 scripts/install-orchestrator.sh diff --git a/package.json b/package.json index 42dda34..de96b7b 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ }, "files": [ "dist", + "scripts", "tsconfig.base.json" ], "scripts": { diff --git a/scripts/install-orchestrator.sh b/scripts/install-orchestrator.sh new file mode 100755 index 0000000..477f68a --- /dev/null +++ b/scripts/install-orchestrator.sh @@ -0,0 +1,543 @@ +#!/bin/bash +# +# Standalone script to install the orchestrator (Serverless Logic / SonataFlow) +# on OpenShift. +# +# Usage: ./install-orchestrator.sh [namespace] +# Default namespace: orchestrator +# + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +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 +} + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- +# Escape double quotes for yq string values (yq v4 mikefarah) +escape_yq() { + local input="$1" + printf '%s' "$input" | sed 's/"/\\"/g' + 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 + log::warn "Recreating namespace: $project" + delete_namespace "$project" + oc create namespace "${project}" || { log::error "Failed to create namespace ${project}"; exit 1; } + 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 pqsl_secret_name pqsl_svc_name + pqsl_secret_name=$(oc get secrets -n "$namespace" -o name 2>/dev/null | grep "backstage-psql" | grep "secret" | head -1 | sed 's|secret\/||') + pqsl_svc_name='backstage-psql' + + log::info "PostgreSQL secret: $pqsl_secret_name, service: $pqsl_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" "$pqsl_secret_name" "$pqsl_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 +} + +deploy_workflows() { + local namespace=$1 + + local pqsl_secret_name pqsl_svc_name pqsl_user_key pqsl_password_key sonataflow_db + pqsl_secret_name=$(oc get secrets -n "$namespace" -o name 2>/dev/null | grep "backstage-psql" | grep "secret" | head -1 | sed 's|secret\/||') + pqsl_svc_name='backstage-psql' + pqsl_user_key="POSTGRES_USER" + pqsl_password_key="POSTGRES_PASSWORD" + sonataflow_db="backstage_plugin_orchestrator" + + local workflow_repo="https://github.com/AndrienkoAleksandr/serverless-workflows.git" + local workflow_dir="/tmp/serverless-workflows" + local local_manifests="${SCRIPT_DIR}/yaml" + + # Prefer local yaml/ if it exists and has content + if [[ -d "${local_manifests}" ]] && [[ -n "$(ls -A "${local_manifests}" 2>/dev/null)" ]]; then + log::info "Using local workflow manifests from ${local_manifests}" + # Apply all YAMLs in yaml/ with correct namespace + for f in "${local_manifests}"/*.yaml "${local_manifests}"/*.yml; do + [[ -e "$f" ]] && oc apply -f "$f" -n "$namespace" && log::info "Applied $(basename "$f")" + done + else + log::info "Cloning workflow repo..." + rm -rf "${workflow_dir}" + git clone --single-branch --branch bulk-import-workflow-sample "${workflow_repo}" "${workflow_dir}" + local workflow_manifests="${workflow_dir}/workflows/experimentals/bulk-import-git-repos/manifests" + if [[ -d "${workflow_manifests}" ]]; then + log::info "Applying workflow manifests from repo..." + + snToDbPatch="${workflow_manifests}/04-sonataflow_universal-pr.yaml" + yq eval -i '.spec.persistence.postgresql.secretRef.name = "'"$(escape_yq "$pqsl_secret_name")"'"' "$snToDbPatch" + yq eval -i '.spec.persistence.postgresql.secretRef.userKey = "'"$(escape_yq "$pqsl_user_key")"'"' "$snToDbPatch" + yq eval -i '.spec.persistence.postgresql.secretRef.passwordKey = "'"$(escape_yq "$pqsl_password_key")"'"' "$snToDbPatch" + yq eval -i '.spec.persistence.postgresql.serviceRef.name = "'"$(escape_yq "$pqsl_svc_name")"'"' "$snToDbPatch" + yq eval -i '.spec.persistence.postgresql.serviceRef.namespace = "'"$(escape_yq "$namespace")"'"' "$snToDbPatch" + yq eval -i '.spec.persistence.postgresql.serviceRef.databaseName = "'"$(escape_yq "$sonataflow_db")"'"' "$snToDbPatch" + + oc apply -f "${workflow_manifests}" -n "$namespace" + else + log::warn "Manifests path not found in repo: ${workflow_manifests}" + fi + fi + + log::info "Waiting for SonataFlow resources..." + timeout 30s bash -c " + until [[ \$(oc get sf -n $namespace --no-headers 2>/dev/null | wc -l) -ge 1 ]]; do + echo \"Waiting for sf resources... \$(oc get sf -n $namespace --no-headers 2>/dev/null | wc -l)\" + sleep 5 + done + " + + wait_for_deployment "$namespace" universal-pr 5 || true + log::info "Orchestrator workflows deployment done." +} + + + +# --------------------------------------------------------------------------- +# 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}" + deploy_workflows "${NAME_SPACE}" + print_orchestrator_connection_info "${NAME_SPACE}" + + log::success "Orchestrator deployment completed successfully!" +} + +main "$@" \ No newline at end of file From b1bac48511218172d891a0e8e2c7f69c23a03938 Mon Sep 17 00:00:00 2001 From: Oleksandr Andriienko Date: Tue, 17 Mar 2026 02:23:49 +0200 Subject: [PATCH 2/9] feat(e2e): remove workflow installation Signed-off-by: Oleksandr Andriienko --- scripts/install-orchestrator.sh | 68 --------------------------------- 1 file changed, 68 deletions(-) diff --git a/scripts/install-orchestrator.sh b/scripts/install-orchestrator.sh index 477f68a..9289ebd 100755 --- a/scripts/install-orchestrator.sh +++ b/scripts/install-orchestrator.sh @@ -102,16 +102,6 @@ log::success() { return 0 } -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- -# Escape double quotes for yq string values (yq v4 mikefarah) -escape_yq() { - local input="$1" - printf '%s' "$input" | sed 's/"/\\"/g' - return 0 -} - # --------------------------------------------------------------------------- # Operator subscription and status # --------------------------------------------------------------------------- @@ -443,63 +433,6 @@ deploy_orchestrator_workflows_operator() { fi } -deploy_workflows() { - local namespace=$1 - - local pqsl_secret_name pqsl_svc_name pqsl_user_key pqsl_password_key sonataflow_db - pqsl_secret_name=$(oc get secrets -n "$namespace" -o name 2>/dev/null | grep "backstage-psql" | grep "secret" | head -1 | sed 's|secret\/||') - pqsl_svc_name='backstage-psql' - pqsl_user_key="POSTGRES_USER" - pqsl_password_key="POSTGRES_PASSWORD" - sonataflow_db="backstage_plugin_orchestrator" - - local workflow_repo="https://github.com/AndrienkoAleksandr/serverless-workflows.git" - local workflow_dir="/tmp/serverless-workflows" - local local_manifests="${SCRIPT_DIR}/yaml" - - # Prefer local yaml/ if it exists and has content - if [[ -d "${local_manifests}" ]] && [[ -n "$(ls -A "${local_manifests}" 2>/dev/null)" ]]; then - log::info "Using local workflow manifests from ${local_manifests}" - # Apply all YAMLs in yaml/ with correct namespace - for f in "${local_manifests}"/*.yaml "${local_manifests}"/*.yml; do - [[ -e "$f" ]] && oc apply -f "$f" -n "$namespace" && log::info "Applied $(basename "$f")" - done - else - log::info "Cloning workflow repo..." - rm -rf "${workflow_dir}" - git clone --single-branch --branch bulk-import-workflow-sample "${workflow_repo}" "${workflow_dir}" - local workflow_manifests="${workflow_dir}/workflows/experimentals/bulk-import-git-repos/manifests" - if [[ -d "${workflow_manifests}" ]]; then - log::info "Applying workflow manifests from repo..." - - snToDbPatch="${workflow_manifests}/04-sonataflow_universal-pr.yaml" - yq eval -i '.spec.persistence.postgresql.secretRef.name = "'"$(escape_yq "$pqsl_secret_name")"'"' "$snToDbPatch" - yq eval -i '.spec.persistence.postgresql.secretRef.userKey = "'"$(escape_yq "$pqsl_user_key")"'"' "$snToDbPatch" - yq eval -i '.spec.persistence.postgresql.secretRef.passwordKey = "'"$(escape_yq "$pqsl_password_key")"'"' "$snToDbPatch" - yq eval -i '.spec.persistence.postgresql.serviceRef.name = "'"$(escape_yq "$pqsl_svc_name")"'"' "$snToDbPatch" - yq eval -i '.spec.persistence.postgresql.serviceRef.namespace = "'"$(escape_yq "$namespace")"'"' "$snToDbPatch" - yq eval -i '.spec.persistence.postgresql.serviceRef.databaseName = "'"$(escape_yq "$sonataflow_db")"'"' "$snToDbPatch" - - oc apply -f "${workflow_manifests}" -n "$namespace" - else - log::warn "Manifests path not found in repo: ${workflow_manifests}" - fi - fi - - log::info "Waiting for SonataFlow resources..." - timeout 30s bash -c " - until [[ \$(oc get sf -n $namespace --no-headers 2>/dev/null | wc -l) -ge 1 ]]; do - echo \"Waiting for sf resources... \$(oc get sf -n $namespace --no-headers 2>/dev/null | wc -l)\" - sleep 5 - done - " - - wait_for_deployment "$namespace" universal-pr 5 || true - log::info "Orchestrator workflows deployment done." -} - - - # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- @@ -534,7 +467,6 @@ main() { configure_namespace "${NAME_SPACE}" log::info "Deploying orchestrator workflows..." deploy_orchestrator_workflows_operator "${NAME_SPACE}" - deploy_workflows "${NAME_SPACE}" print_orchestrator_connection_info "${NAME_SPACE}" log::success "Orchestrator deployment completed successfully!" From 3a5407b6eaeecd4d3681933ea0c22b0825f2245f Mon Sep 17 00:00:00 2001 From: Oleksandr Andriienko Date: Thu, 19 Mar 2026 14:11:14 +0200 Subject: [PATCH 3/9] Update scripts/install-orchestrator.sh Co-authored-by: Subhash Khileri --- scripts/install-orchestrator.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/install-orchestrator.sh b/scripts/install-orchestrator.sh index 9289ebd..1a480ab 100755 --- a/scripts/install-orchestrator.sh +++ b/scripts/install-orchestrator.sh @@ -472,4 +472,4 @@ main() { log::success "Orchestrator deployment completed successfully!" } -main "$@" \ No newline at end of file +main "$@" From ce35e4b0289bbd593da19e4c8a0e5e8ea6c1bb32 Mon Sep 17 00:00:00 2001 From: Oleksandr Andriienko Date: Thu, 19 Mar 2026 14:32:07 +0200 Subject: [PATCH 4/9] feat(e2e): fix typos Signed-off-by: Oleksandr Andriienko --- scripts/install-orchestrator.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/install-orchestrator.sh b/scripts/install-orchestrator.sh index 1a480ab..0174293 100755 --- a/scripts/install-orchestrator.sh +++ b/scripts/install-orchestrator.sh @@ -413,14 +413,14 @@ deploy_orchestrator_workflows_operator() { fi fi - local pqsl_secret_name pqsl_svc_name - pqsl_secret_name=$(oc get secrets -n "$namespace" -o name 2>/dev/null | grep "backstage-psql" | grep "secret" | head -1 | sed 's|secret\/||') - pqsl_svc_name='backstage-psql' + 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: $pqsl_secret_name, service: $pqsl_svc_name" + 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" "$pqsl_secret_name" "$pqsl_svc_name" + 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 From 4ed06dfc883e2bec9983fb1b846b520a2ed39f90 Mon Sep 17 00:00:00 2001 From: Oleksandr Andriienko Date: Thu, 19 Mar 2026 14:38:17 +0200 Subject: [PATCH 5/9] feat(e2e): remove namespace deletion Signed-off-by: Oleksandr Andriienko --- scripts/install-orchestrator.sh | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/install-orchestrator.sh b/scripts/install-orchestrator.sh index 0174293..086f06a 100755 --- a/scripts/install-orchestrator.sh +++ b/scripts/install-orchestrator.sh @@ -9,7 +9,6 @@ set -e -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" export NAME_SPACE="${1:-${NAME_SPACE:-orchestrator}}" LOWER_CASE_CLASS='[:lower:]' @@ -183,9 +182,12 @@ delete_namespace() { configure_namespace() { local project=$1 - log::warn "Recreating namespace: $project" - delete_namespace "$project" - oc create namespace "${project}" || { log::error "Failed to create namespace ${project}"; exit 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 From 992378094eadae5755d4c09ed484058914f7e50f Mon Sep 17 00:00:00 2001 From: Oleksandr Andriienko Date: Thu, 19 Mar 2026 15:01:34 +0200 Subject: [PATCH 6/9] feat(e2e): use typescript wrapper Signed-off-by: Oleksandr Andriienko --- package.json | 7 +++++-- src/deployment/orchestrator/index.ts | 8 ++++++++ .../deployment/orchestrator}/install-orchestrator.sh | 0 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 src/deployment/orchestrator/index.ts rename {scripts => src/deployment/orchestrator}/install-orchestrator.sh (100%) diff --git a/package.json b/package.json index de96b7b..bbf5272 100644 --- a/package.json +++ b/package.json @@ -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": { @@ -55,11 +59,10 @@ }, "files": [ "dist", - "scripts", "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..d22dd14 --- /dev/null +++ b/src/deployment/orchestrator/index.ts @@ -0,0 +1,8 @@ +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}`; +} diff --git a/scripts/install-orchestrator.sh b/src/deployment/orchestrator/install-orchestrator.sh similarity index 100% rename from scripts/install-orchestrator.sh rename to src/deployment/orchestrator/install-orchestrator.sh From b5f6a80dfd3bbe6705b9ee3b6f83c5cb0d83a689 Mon Sep 17 00:00:00 2001 From: Oleksandr Andriienko Date: Thu, 19 Mar 2026 15:24:19 +0200 Subject: [PATCH 7/9] feat(e2e): bump package.json Signed-off-by: Oleksandr Andriienko --- docs/changelog.md | 8 +++++++- package.json | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) 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/package.json b/package.json index bbf5272..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": { From 0b9777f30723842ee224159890e2365008e5b2f8 Mon Sep 17 00:00:00 2001 From: Oleksandr Andriienko Date: Thu, 19 Mar 2026 16:36:23 +0200 Subject: [PATCH 8/9] feat(e2e): export installOrchestrator Signed-off-by: Oleksandr Andriienko --- src/deployment/orchestrator/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/deployment/orchestrator/index.ts b/src/deployment/orchestrator/index.ts index d22dd14..a56a70f 100644 --- a/src/deployment/orchestrator/index.ts +++ b/src/deployment/orchestrator/index.ts @@ -6,3 +6,5 @@ const scriptPath = resolve(import.meta.dirname, "install-orchestrator.sh"); export async function installOrchestrator(namespace = "orchestrator") { await $`bash ${scriptPath} ${namespace}`; } + +export default installOrchestrator; From a4252a2d724158d0e308b196a1a995895f82764e Mon Sep 17 00:00:00 2001 From: Oleksandr Andriienko Date: Thu, 19 Mar 2026 16:52:49 +0200 Subject: [PATCH 9/9] feat(e2e): add docs Signed-off-by: Oleksandr Andriienko --- docs/.vitepress/config.ts | 5 ++ docs/api/deployment/orchestrator.md | 47 ++++++++++++ docs/guide/deployment/index.md | 1 + .../deployment/orchestrator-deployment.md | 74 +++++++++++++++++++ 4 files changed, 127 insertions(+) create mode 100644 docs/api/deployment/orchestrator.md create mode 100644 docs/guide/deployment/orchestrator-deployment.md 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/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)