From b0b87302f2d71fc99aa9f00a5f3370091179a736 Mon Sep 17 00:00:00 2001 From: Michael Pruitt Date: Fri, 14 Aug 2026 08:53:59 -0500 Subject: [PATCH 1/8] INTEROP-9337: Harden OPP upgrade and preflight scripts - Replace jq with go-template/jsonpath/awk (jq unavailable in CI image) - Wrap proxy-conf.sh in set +x/set -x to prevent credential leakage - Fix trap handler with brace-group form for errexit safety - Replace eval-based oc commands with direct invocations - Fix rhacs-operator install: use openshift-operators namespace (AllNamespaces OperatorGroup) instead of custom namespace - Detect and report MachineConfigPool query failures instead of silently falling through to "all 0 MCPs healthy" - Update preflight ACM compatibility matrix for 2.17 on OCP 4.22 Resolves: https://redhat.atlassian.net/browse/INTEROP-9337 --- .../interop-opp-preflight-commands.sh | 309 ++++++++++-------- .../upgrade/interop-opp-upgrade-commands.sh | 147 +++++---- 2 files changed, 245 insertions(+), 211 deletions(-) diff --git a/ci-operator/step-registry/interop/opp/preflight/interop-opp-preflight-commands.sh b/ci-operator/step-registry/interop/opp/preflight/interop-opp-preflight-commands.sh index f11b8d892696c..db1c74a473d81 100755 --- a/ci-operator/step-registry/interop/opp/preflight/interop-opp-preflight-commands.sh +++ b/ci-operator/step-registry/interop/opp/preflight/interop-opp-preflight-commands.sh @@ -1,8 +1,7 @@ #!/bin/bash -set -o nounset -set -o errexit -set -o pipefail +set -eux -o pipefail +shopt -s inherit_errexit OPP_OPERATORS="${OPP_OPERATORS:-advanced-cluster-management,rhacs-operator,odf-operator,quay-operator}" @@ -11,36 +10,42 @@ export XDG_RUNTIME_DIR="${HOME}/run" export REGISTRY_AUTH_PREFERENCE=podman mkdir -p "${XDG_RUNTIME_DIR}" -if ! command -v jq &>/dev/null; then - echo "jq not found; installing..." - dnf install -y -q jq 2>/dev/null || yum install -y -q jq 2>/dev/null || { - echo >&2 "ERROR: failed to install jq" - exit 1 - } +if [[ -f "${SHARED_DIR}/proxy-conf.sh" ]]; then + set +x + source "${SHARED_DIR}/proxy-conf.sh" + set -x fi REPORT_DIR="${ARTIFACT_DIR}/preflight" REPORT_FILE="${REPORT_DIR}/preflight-report.json" mkdir -p "${REPORT_DIR}" -CHECKS_FAILED=0 +typeset -i CHECKS_FAILED=0 +typeset -i EXIT_CODE=0 -DebugOnExit() { +function DebugOnExit () { if (( EXIT_CODE != 0 )); then - echo -e "\n### DEBUG: Pre-flight failure diagnostics ###\n" - echo -e "\n# ClusterVersion\n$(oc get clusterversion 2>/dev/null || echo 'unavailable')" - echo -e "\n# ClusterOperators\n$(oc get co 2>/dev/null || echo 'unavailable')" - echo -e "\n# MachineConfigPools\n$(oc get machineconfigpools 2>/dev/null || echo 'unavailable')" - echo -e "\n# Nodes\n$(oc get nodes 2>/dev/null || echo 'unavailable')" - echo -e "\n# OPP Operator CSVs\n$(oc get csv -A 2>/dev/null || echo 'unavailable')" + : "### DEBUG: Pre-flight failure diagnostics ###" + : "# ClusterVersion" + oc get clusterversion 2>/dev/null || : "unavailable" + : "# ClusterOperators" + oc get co 2>/dev/null || : "unavailable" + : "# MachineConfigPools" + oc get machineconfigpools 2>/dev/null || : "unavailable" + : "# Nodes" + oc get nodes 2>/dev/null || : "unavailable" + : "# OPP Operator CSVs" + oc get csv -A 2>/dev/null || : "unavailable" if [[ -f "${REPORT_FILE}" ]]; then - echo -e "\n# Pre-flight report:\n$(cat "${REPORT_FILE}")" + : "# Pre-flight report:" + cat "${REPORT_FILE}" fi fi true } -trap 'EXIT_CODE=$?; DebugOnExit' EXIT TERM +trap '{ EXIT_CODE=$?; DebugOnExit; true; }' EXIT +trap '{ EXIT_CODE=143; DebugOnExit; trap - EXIT; exit 143; }' TERM # ────────────────────────────────────────────────────────────────────── # Known removed / deprecated APIs per OCP minor version. @@ -49,13 +54,9 @@ trap 'EXIT_CODE=$?; DebugOnExit' EXIT TERM # Source: Kubernetes deprecation guide + OCP release notes. # ────────────────────────────────────────────────────────────────────── declare -A REMOVED_APIS -# APIs removed in 4.12 (Kubernetes 1.25) REMOVED_APIS["12"]="batch/v1beta1/CronJob policy/v1beta1/PodDisruptionBudget policy/v1beta1/PodSecurityPolicy discovery.k8s.io/v1beta1/EndpointSlice events.k8s.io/v1beta1/Event autoscaling/v2beta1/HorizontalPodAutoscaler" -# APIs removed in 4.14 (Kubernetes 1.27) REMOVED_APIS["14"]="storage.k8s.io/v1beta1/CSIStorageCapacity" -# APIs removed in 4.17 (Kubernetes 1.30) REMOVED_APIS["17"]="flowcontrol.apiserver.k8s.io/v1beta2/FlowSchema flowcontrol.apiserver.k8s.io/v1beta2/PriorityLevelConfiguration" -# APIs removed in 4.18 (Kubernetes 1.31) REMOVED_APIS["18"]="flowcontrol.apiserver.k8s.io/v1beta3/FlowSchema flowcontrol.apiserver.k8s.io/v1beta3/PriorityLevelConfiguration" # ────────────────────────────────────────────────────────────────────── @@ -72,13 +73,10 @@ OPP_COMPAT["4.18"]="advanced-cluster-management:2.13 rhacs-operator:4.7 odf-oper OPP_COMPAT["4.19"]="advanced-cluster-management:2.13 rhacs-operator:4.8 odf-operator:4.19 quay-operator:3.14" OPP_COMPAT["4.20"]="advanced-cluster-management:2.14 rhacs-operator:4.9 odf-operator:4.20 quay-operator:3.15" OPP_COMPAT["4.21"]="advanced-cluster-management:2.15 rhacs-operator:4.10 odf-operator:4.21 quay-operator:3.15" -OPP_COMPAT["4.22"]="advanced-cluster-management:2.16 rhacs-operator:4.11 odf-operator:4.22 quay-operator:3.16" +OPP_COMPAT["4.22"]="advanced-cluster-management:2.17 rhacs-operator:4.11 odf-operator:4.22 quay-operator:3.16" OPP_COMPAT["5.0"]="advanced-cluster-management:2.17 quay-operator:3.17" -# ────────────────────────────────────────────────────────────────────── -# Utility: append a check result to the JSON report -# ────────────────────────────────────────────────────────────────────── -InitReport() { +function InitReport () { cat > "${REPORT_FILE}" <<'EOFJSON' { "preflight_checks": [] @@ -87,21 +85,21 @@ EOFJSON true } -AppendCheck() { +function AppendCheck () { typeset checkName="${1}" checkStatus="${2}" checkDetails="${3}" - typeset tmpFile - tmpFile="$(mktemp)" - jq --arg n "${checkName}" --arg s "${checkStatus}" --arg d "${checkDetails}" \ - '.preflight_checks += [{"check": $n, "status": $s, "details": $d}]' \ - "${REPORT_FILE}" > "${tmpFile}" && mv "${tmpFile}" "${REPORT_FILE}" + python3 -c " +import json, sys +with open(sys.argv[1]) as f: + data = json.load(f) +data['preflight_checks'].append({'check': sys.argv[2], 'status': sys.argv[3], 'details': sys.argv[4]}) +with open(sys.argv[1], 'w') as f: + json.dump(data, f, indent=2) +" "${REPORT_FILE}" "${checkName}" "${checkStatus}" "${checkDetails}" true } -# ────────────────────────────────────────────────────────────────────── -# Check 1: API deprecation scan -# ────────────────────────────────────────────────────────────────────── -CheckApiDeprecations() { - echo "=== Check 1: API deprecation scan ===" +function CheckApiDeprecations () { + : "=== Check 1: API deprecation scan ===" typeset targetMinor="${1}" typeset ocpDisplay="${2:-4.${targetMinor}}" @@ -109,8 +107,8 @@ CheckApiDeprecations() { typeset flagged="" foundCount=0 typeset clusterApis - clusterApis="$(oc api-resources --no-headers 2>/dev/null)" || { - echo "WARNING: Failed to list API resources" + clusterApis="$(oc api-resources --no-headers)" || { + : "WARNING: Failed to list API resources" AppendCheck "api_deprecation_scan" "warn" "Could not list cluster API resources" return 0 } @@ -136,35 +134,33 @@ CheckApiDeprecations() { done if (( foundCount > 0 )); then - echo -e "WARNING: Found ${foundCount} deprecated API(s) still in use:\n${flagged}" + : "WARNING: Found ${foundCount} deprecated API(s) still in use" + echo -e "${flagged}" AppendCheck "api_deprecation_scan" "warn" "Found ${foundCount} deprecated API(s) in use: ${flagged}" else - echo "No deprecated APIs detected for target version ${ocpDisplay}" + : "No deprecated APIs detected for target version ${ocpDisplay}" AppendCheck "api_deprecation_scan" "pass" "No deprecated APIs detected for ${ocpDisplay}" fi true } -# ────────────────────────────────────────────────────────────────────── -# Check 2: OPP compatibility matrix -# ────────────────────────────────────────────────────────────────────── -CheckOppCompatibility() { - echo -e "\n=== Check 2: OPP operator compatibility matrix ===" +function CheckOppCompatibility () { + : "=== Check 2: OPP operator compatibility matrix ===" typeset ocpKey="${1}" typeset compatSpec="${OPP_COMPAT[${ocpKey}]:-}" typeset allCsvs typeset failed=0 - allCsvs="$(oc get csv -A --no-headers 2>/dev/null)" || { - echo >&2 "Failed to retrieve CSVs" + allCsvs="$(oc get csv -A --no-headers)" || { + : "Failed to retrieve CSVs" AppendCheck "opp_compatibility_matrix" "fail" "Could not list CSVs" (( CHECKS_FAILED += 1 )) return 0 } if [[ -z "${compatSpec}" ]]; then - echo "No compatibility matrix entry for OCP ${ocpKey}; skipping version check" + : "No compatibility matrix entry for OCP ${ocpKey}; skipping version check" AppendCheck "opp_compatibility_matrix" "skip" "No matrix entry for OCP ${ocpKey}" return 0 fi @@ -180,7 +176,7 @@ CheckOppCompatibility() { typeset csvLine csvName installedVersion csvLine="$(echo "${allCsvs}" | grep "${opPrefix}" | head -1)" || true if [[ -z "${csvLine}" ]]; then - echo >&2 "Operator not found: ${opPrefix}" + : "Operator not found: ${opPrefix}" details="${details}${opPrefix}: NOT INSTALLED; " (( failed += 1 )) continue @@ -189,7 +185,7 @@ CheckOppCompatibility() { csvName="$(echo "${csvLine}" | awk '{print $2}')" installedVersion="$(echo "${csvName}" | grep -oE '[0-9]+\.[0-9]+' | head -1)" || true if [[ -z "${installedVersion}" ]]; then - echo >&2 "Operator ${opPrefix}: could not parse version from CSV ${csvName}" + : "Operator ${opPrefix}: could not parse version from CSV ${csvName}" details="${details}${opPrefix}: version unparseable from ${csvName}; " (( failed += 1 )) continue @@ -200,88 +196,104 @@ CheckOppCompatibility() { instMinor="${installedVersion##*.}" if (( instMajor < minMajor || (instMajor == minMajor && instMinor < minMinor) )); then - echo >&2 "Operator ${opPrefix} version ${installedVersion} is below minimum ${minVersion} for OCP ${ocpKey}" + : "Operator ${opPrefix} version ${installedVersion} is below minimum ${minVersion} for OCP ${ocpKey}" details="${details}${opPrefix}: ${installedVersion} < ${minVersion} (INCOMPATIBLE); " (( failed += 1 )) else - echo "Operator ${opPrefix}: version ${installedVersion} >= ${minVersion} (OK)" + : "Operator ${opPrefix}: version ${installedVersion} >= ${minVersion} (OK)" details="${details}${opPrefix}: ${installedVersion} >= ${minVersion} (OK); " fi done if (( failed > 0 )); then - echo >&2 "${failed} operator(s) failed compatibility check" + : "${failed} operator(s) failed compatibility check" AppendCheck "opp_compatibility_matrix" "fail" "${details}" (( CHECKS_FAILED += 1 )) else - echo "All OPP operators are compatible with OCP ${ocpKey}" + : "All OPP operators are compatible with OCP ${ocpKey}" AppendCheck "opp_compatibility_matrix" "pass" "${details}" fi true } -# ────────────────────────────────────────────────────────────────────── -# Check 3: Cluster health baseline -# ────────────────────────────────────────────────────────────────────── -CheckClusterHealth() { - echo -e "\n=== Check 3: Cluster health baseline ===" +function CheckClusterHealth () { + : "=== Check 3: Cluster health baseline ===" typeset failed=0 details="" - echo "Checking node health..." + : "Checking node health..." typeset unreadyNodes - unreadyNodes="$(oc get node --no-headers 2>/dev/null | awk '$2 != "Ready" {print $1}')" || true - if [[ -n "${unreadyNodes}" ]]; then - echo >&2 "Not-Ready nodes: ${unreadyNodes}" + if ! unreadyNodes="$(oc get node --no-headers | awk '$2 != "Ready" {print $1}')"; then + : "Failed to query nodes" + details="${details}nodes: query failed; " + (( failed += 1 )) + elif [[ -n "${unreadyNodes}" ]]; then + : "Not-Ready nodes: ${unreadyNodes}" details="${details}unready_nodes: ${unreadyNodes}; " (( failed += 1 )) else typeset nodeCount - nodeCount="$(oc get node --no-headers 2>/dev/null | wc -l)" - echo "All ${nodeCount} nodes Ready" + nodeCount="$(oc get node --no-headers | wc -l)" + : "All ${nodeCount} nodes Ready" details="${details}nodes: all ${nodeCount} ready; " fi - echo "Checking ClusterOperator health..." + : "Checking ClusterOperator health..." typeset unhealthyCo - unhealthyCo="$(oc get co --no-headers 2>/dev/null | awk '$3 != "True" || $4 != "False" || $5 != "False" {print $1}')" || true - if [[ -n "${unhealthyCo}" ]]; then - echo >&2 "Unhealthy ClusterOperators: ${unhealthyCo}" + if ! unhealthyCo="$(oc get co --no-headers | awk '$3 != "True" || $4 != "False" || $5 != "False" {print $1}')"; then + : "Failed to query ClusterOperators" + details="${details}cluster_operators: query failed; " + (( failed += 1 )) + elif [[ -n "${unhealthyCo}" ]]; then + : "Unhealthy ClusterOperators: ${unhealthyCo}" details="${details}unhealthy_co: ${unhealthyCo}; " (( failed += 1 )) else - echo "All ClusterOperators healthy" + : "All ClusterOperators healthy" details="${details}cluster_operators: all healthy; " fi - echo "Checking ClusterVersion conditions..." + : "Checking ClusterVersion conditions..." typeset avail progressing degraded - avail="$(oc get clusterversion version -o jsonpath='{.status.conditions[?(@.type=="Available")].status}' 2>/dev/null)" || true - progressing="$(oc get clusterversion version -o jsonpath='{.status.conditions[?(@.type=="Progressing")].status}' 2>/dev/null)" || true - degraded="$(oc get clusterversion version -o jsonpath='{.status.conditions[?(@.type=="Degraded")].status}' 2>/dev/null)" || true + avail="$(oc get clusterversion version -o jsonpath='{.status.conditions[?(@.type=="Available")].status}')" || true + progressing="$(oc get clusterversion version -o jsonpath='{.status.conditions[?(@.type=="Progressing")].status}')" || true + degraded="$(oc get clusterversion version -o jsonpath='{.status.conditions[?(@.type=="Degraded")].status}')" || true if [[ "${avail}" != "True" || "${progressing}" != "False" || "${degraded}" != "False" ]]; then - echo >&2 "CVO health check failed: Available=${avail} Progressing=${progressing} Degraded=${degraded}" + : "CVO health check failed: Available=${avail} Progressing=${progressing} Degraded=${degraded}" details="${details}cvo: Available=${avail} Progressing=${progressing} Degraded=${degraded}; " (( failed += 1 )) else - echo "CVO: Available=True, Progressing=False, Degraded=False" + : "CVO: Available=True, Progressing=False, Degraded=False" details="${details}cvo: healthy; " fi - echo "Checking for firing alerts..." + : "Checking for firing alerts..." typeset firingAlerts="" - firingAlerts="$(oc -n openshift-monitoring exec -c prometheus prometheus-k8s-0 -- \ - curl -s 'http://localhost:9090/api/v1/alerts' 2>/dev/null | \ - jq -r '.data.alerts[]? | select(.state=="firing") | select(.labels.alertname != "Watchdog") | select(.labels.alertname != "AlertmanagerReceiversNotConfigured") | .labels.alertname' 2>/dev/null | \ - sort -u)" || true - - if [[ -n "${firingAlerts}" ]]; then + if ! firingAlerts="$(oc -n openshift-monitoring exec -c prometheus prometheus-k8s-0 -- \ + curl -s 'http://localhost:9090/api/v1/alerts' | \ + python3 -c " +import json, sys +data = json.load(sys.stdin) +if data.get('status') != 'success': + print('query returned non-success status', file=sys.stderr) + sys.exit(1) +alerts = data.get('data', {}).get('alerts', []) +names = sorted(set( + a['labels']['alertname'] for a in alerts + if a.get('state') == 'firing' + and a.get('labels', {}).get('alertname') not in ('Watchdog', 'AlertmanagerReceiversNotConfigured') +)) +print('\n'.join(names)) +")"; then + : "Alert query failed or unavailable" + details="${details}alerts: query failed; " + elif [[ -n "${firingAlerts}" ]]; then typeset alertCount alertCount="$(echo "${firingAlerts}" | wc -l)" - echo "WARNING: ${alertCount} alert(s) firing: ${firingAlerts}" + : "WARNING: ${alertCount} alert(s) firing: ${firingAlerts}" details="${details}firing_alerts: ${alertCount} (${firingAlerts}); " else - echo "No critical alerts firing" + : "No critical alerts firing" details="${details}alerts: none firing; " fi @@ -289,57 +301,63 @@ CheckClusterHealth() { oc get co -o json > "${REPORT_DIR}/co-baseline.json" 2>/dev/null || true if (( failed > 0 )); then - echo >&2 "Cluster health baseline: ${failed} issue(s) found" + : "Cluster health baseline: ${failed} issue(s) found" AppendCheck "cluster_health_baseline" "fail" "${details}" (( CHECKS_FAILED += 1 )) else - echo "Cluster health baseline: all checks passed" + : "Cluster health baseline: all checks passed" AppendCheck "cluster_health_baseline" "pass" "${details}" fi true } -# ────────────────────────────────────────────────────────────────────── -# Check 4: MachineConfigPool readiness -# ────────────────────────────────────────────────────────────────────── -CheckMcpReadiness() { - echo -e "\n=== Check 4: MachineConfigPool readiness ===" +function CheckMcpReadiness () { + : "=== Check 4: MachineConfigPool readiness ===" typeset failed=0 details="" - typeset mcpIssues - mcpIssues="$(oc get machineconfigpools --no-headers 2>/dev/null | \ - awk '$3 != "True" || $4 != "False" || $5 != "False" {print $1}')" || true - - if [[ -n "${mcpIssues}" ]]; then - echo >&2 "Unhealthy MachineConfigPools: ${mcpIssues}" - details="unhealthy_mcps: ${mcpIssues}; " + typeset mcpRaw="" + if ! mcpRaw="$(oc get machineconfigpools --no-headers 2>&1)"; then + : "Failed to query MachineConfigPools" + details="machineconfigpools: query failed; " (( failed += 1 )) - - for mcp in ${mcpIssues}; do - echo -e "\n### MCP ${mcp} ###" - oc describe machineconfigpool "${mcp}" 2>/dev/null || true - done else - typeset mcpCount - mcpCount="$(oc get machineconfigpools --no-headers 2>/dev/null | wc -l)" - echo "All ${mcpCount} MachineConfigPools are updated and not degraded" - details="all ${mcpCount} MCPs healthy (Updated=True, Updating=False, Degraded=False); " + typeset mcpIssues="" + mcpIssues="$(echo "${mcpRaw}" | \ + awk '$3 != "True" || $4 != "False" || $5 != "False" {print $1}')" || true + + if [[ -n "${mcpIssues}" ]]; then + : "Unhealthy MachineConfigPools: ${mcpIssues}" + details="unhealthy_mcps: ${mcpIssues}; " + (( failed += 1 )) + + for mcp in ${mcpIssues}; do + : "### MCP ${mcp} ###" + oc describe machineconfigpool "${mcp}" || true + done + else + typeset mcpCount + mcpCount="$(echo "${mcpRaw}" | wc -l)" + : "All ${mcpCount} MachineConfigPools are updated and not degraded" + details="all ${mcpCount} MCPs healthy (Updated=True, Updating=False, Degraded=False); " + fi fi typeset mismatch="" - while IFS= read -r line; do - typeset mcpName ready desired - mcpName="$(echo "${line}" | awk '{print $1}')" - ready="$(echo "${line}" | awk '{print $7}')" - desired="$(echo "${line}" | awk '{print $6}')" - if [[ -n "${ready}" && -n "${desired}" && "${ready}" != "${desired}" ]]; then - mismatch="${mismatch}${mcpName} (ready=${ready}, desired=${desired}); " - fi - done < <(oc get machineconfigpools --no-headers 2>/dev/null || true) + if [[ -n "${mcpRaw:-}" ]]; then + while IFS= read -r line; do + typeset mcpName ready desired + mcpName="$(echo "${line}" | awk '{print $1}')" + ready="$(echo "${line}" | awk '{print $7}')" + desired="$(echo "${line}" | awk '{print $6}')" + if [[ -n "${ready}" && -n "${desired}" && "${ready}" != "${desired}" ]]; then + mismatch="${mismatch}${mcpName} (ready=${ready}, desired=${desired}); " + fi + done <<< "${mcpRaw}" + fi if [[ -n "${mismatch}" ]]; then - echo >&2 "MCP machine count mismatch: ${mismatch}" + : "MCP machine count mismatch: ${mismatch}" details="${details}machine_count_mismatch: ${mismatch}" (( failed += 1 )) fi @@ -347,69 +365,74 @@ CheckMcpReadiness() { oc get machineconfigpools -o json > "${REPORT_DIR}/mcp-baseline.json" 2>/dev/null || true if (( failed > 0 )); then - echo >&2 "MachineConfigPool readiness: ${failed} issue(s) found" + : "MachineConfigPool readiness: ${failed} issue(s) found" AppendCheck "mcp_readiness" "fail" "${details}" (( CHECKS_FAILED += 1 )) else - echo "MachineConfigPool readiness: all checks passed" + : "MachineConfigPool readiness: all checks passed" AppendCheck "mcp_readiness" "pass" "${details}" fi true } -# ────────────────────────────────────────────────────────────────────── -# Main -# ────────────────────────────────────────────────────────────────────── -Main() { +function Main () { if [[ -f "${SHARED_DIR}/kubeconfig" ]]; then export KUBECONFIG="${SHARED_DIR}/kubeconfig" fi typeset target="${OPENSHIFT_UPGRADE_RELEASE_IMAGE_OVERRIDE:-}" if [[ -z "${target}" ]]; then - echo >&2 "OPENSHIFT_UPGRADE_RELEASE_IMAGE_OVERRIDE is not set; cannot determine upgrade target" + : "OPENSHIFT_UPGRADE_RELEASE_IMAGE_OVERRIDE is not set; cannot determine upgrade target" exit 3 fi - echo "Target release image: ${target}" + : "Target release image: ${target}" + set +x KUBECONFIG="" oc registry login + set -x typeset targetVersion targetMajor targetMinor ocpXy - targetVersion="$(oc adm release info "${target}" --output=json | jq -r '.metadata.version')" + targetVersion="$(oc adm release info "${target}" -o jsonpath='{.metadata.version}')" targetMajor="$(echo "${targetVersion}" | cut -f1 -d.)" targetMinor="$(echo "${targetVersion}" | cut -f2 -d.)" ocpXy="${targetMajor}.${targetMinor}" - echo "Target OCP version: ${targetVersion} (${ocpXy})" + : "Target OCP version: ${targetVersion} (${ocpXy})" typeset sourceVersion sourceVersion="$(oc get clusterversion --no-headers | awk '{print $2}')" - echo "Source OCP version: ${sourceVersion}" + : "Source OCP version: ${sourceVersion}" - echo -e "\n=== Starting OPP pre-flight validation ===\n" + : "=== Starting OPP pre-flight validation ===" InitReport - typeset tmpFile - tmpFile="$(mktemp)" - jq --arg tv "${targetVersion}" --arg sv "${sourceVersion}" --arg ti "${target}" \ - '. + {"target_version": $tv, "source_version": $sv, "target_image": $ti, "timestamp": now | tostring}' \ - "${REPORT_FILE}" > "${tmpFile}" && mv "${tmpFile}" "${REPORT_FILE}" + python3 -c " +import json, sys, time +with open(sys.argv[1]) as f: + data = json.load(f) +data['target_version'] = sys.argv[2] +data['source_version'] = sys.argv[3] +data['target_image'] = sys.argv[4] +data['timestamp'] = str(time.time()) +with open(sys.argv[1], 'w') as f: + json.dump(data, f, indent=2) +" "${REPORT_FILE}" "${targetVersion}" "${sourceVersion}" "${target}" CheckApiDeprecations "${targetMinor}" "${ocpXy}" CheckOppCompatibility "${ocpXy}" CheckClusterHealth CheckMcpReadiness - echo -e "\n=== Pre-flight summary ===" - jq '.' "${REPORT_FILE}" + : "=== Pre-flight summary ===" + python3 -m json.tool "${REPORT_FILE}" if (( CHECKS_FAILED > 0 )); then - echo >&2 "Pre-flight validation FAILED: ${CHECKS_FAILED} check(s) did not pass" - echo >&2 "Review ${REPORT_FILE} for details" + : "Pre-flight validation FAILED: ${CHECKS_FAILED} check(s) did not pass" + : "Review ${REPORT_FILE} for details" exit 3 fi - echo "Pre-flight validation PASSED: all checks succeeded" + : "Pre-flight validation PASSED: all checks succeeded" true } diff --git a/ci-operator/step-registry/interop/opp/upgrade/interop-opp-upgrade-commands.sh b/ci-operator/step-registry/interop/opp/upgrade/interop-opp-upgrade-commands.sh index 61d6219238528..875e3ba9ff9ed 100644 --- a/ci-operator/step-registry/interop/opp/upgrade/interop-opp-upgrade-commands.sh +++ b/ci-operator/step-registry/interop/opp/upgrade/interop-opp-upgrade-commands.sh @@ -1,5 +1,5 @@ #!/bin/bash -set -euxo pipefail +set -eux -o pipefail shopt -s inherit_errexit # NOTE: UPGRADE_TIMEOUT, POLL_INTERVAL, STALL_WINDOW, OPP_OPERATORS are set via step config YAML @@ -9,15 +9,17 @@ POLL_INTERVAL="${POLL_INTERVAL:-60}" STALL_WINDOW="${STALL_WINDOW:-10}" OPP_OPERATORS="${OPP_OPERATORS:-advanced-cluster-management,rhacs-operator,odf-operator,quay-operator}" -if [[ -f "${SHARED_DIR}/proxy-conf.sh" ]]; then - source "${SHARED_DIR}/proxy-conf.sh" -fi - export HOME="${HOME:-/tmp/home}" export XDG_RUNTIME_DIR="${HOME}/run" export REGISTRY_AUTH_PREFERENCE=podman mkdir -p "${XDG_RUNTIME_DIR}" +if [[ -f "${SHARED_DIR}/proxy-conf.sh" ]]; then + set +x + source "${SHARED_DIR}/proxy-conf.sh" + set -x +fi + typeset -i exitCode=0 typeset upgradeTarget="" typeset targetVersion="" @@ -26,7 +28,7 @@ typeset sourceVersion="" typeset -i sourceMinorVersion=0 typeset isForceUpdate="false" -DebugOnExit() { +function DebugOnExit () { if (( exitCode != 0 )); then : "### DEBUG: Upgrade failure diagnostics ###" if [[ -n "${targetMinorVersion:-}" ]] && (( targetMinorVersion >= 16 )); then @@ -39,43 +41,40 @@ DebugOnExit() { oc get machineconfig || : "unavailable" : "# Abnormal nodes" - oc get node -o json | jq -r '.items[] | select(.status.conditions[] | select(.type=="Ready" and .status!="True")) | .metadata.name' | while read -r node; do + typeset node="" + for node in $(oc get node -o go-template='{{range .items}}{{$ready := ""}}{{range .status.conditions}}{{if eq .type "Ready"}}{{$ready = .status}}{{end}}{{end}}{{if ne $ready "True"}}{{.metadata.name}}{{"\n"}}{{end}}{{end}}' || true); do : "### oc describe node ${node} ###" oc describe node "${node}" || true - done || true + done : "# Abnormal ClusterOperators" - oc get co -o json | jq -r '.items[] | select( - (.status.conditions[] | select(.type=="Available")).status != "True" or - (.status.conditions[] | select(.type=="Progressing")).status != "False" or - (.status.conditions[] | select(.type=="Degraded")).status != "False" - ) | .metadata.name' | while read -r co; do + typeset co="" + for co in $(oc get co -o go-template='{{range .items}}{{$avail := ""}}{{$prog := ""}}{{$deg := ""}}{{range .status.conditions}}{{if eq .type "Available"}}{{$avail = .status}}{{end}}{{if eq .type "Progressing"}}{{$prog = .status}}{{end}}{{if eq .type "Degraded"}}{{$deg = .status}}{{end}}{{end}}{{if or (ne $avail "True") (ne $prog "False") (ne $deg "False")}}{{.metadata.name}}{{"\n"}}{{end}}{{end}}' || true); do : "### oc describe co ${co} ###" oc describe co "${co}" || true - done || true + done : "# Abnormal MachineConfigPools" - oc get machineconfigpools -o json | jq -r '.items[] | select( - (.status.conditions[] | select(.type=="Updated")).status != "True" or - (.status.conditions[] | select(.type=="Updating")).status != "False" or - (.status.conditions[] | select(.type=="Degraded")).status != "False" - ) | .metadata.name' | while read -r mcp; do + typeset mcp="" + for mcp in $(oc get machineconfigpools -o go-template='{{range .items}}{{$upd := ""}}{{$upting := ""}}{{$deg := ""}}{{range .status.conditions}}{{if eq .type "Updated"}}{{$upd = .status}}{{end}}{{if eq .type "Updating"}}{{$upting = .status}}{{end}}{{if eq .type "Degraded"}}{{$deg = .status}}{{end}}{{end}}{{if or (ne $upd "True") (ne $upting "False") (ne $deg "False")}}{{.metadata.name}}{{"\n"}}{{end}}{{end}}' || true); do : "### oc describe mcp ${mcp} ###" oc describe mcp "${mcp}" || true - done || true + done : "# OPP Operator CSVs" oc get csv -A || : "unavailable" fi + true } -trap 'exitCode=$?; DebugOnExit' EXIT TERM +trap '{ exitCode=$?; DebugOnExit; true; }' EXIT +trap '{ exitCode=143; DebugOnExit; trap - EXIT; exit 143; }' TERM set +x KUBECONFIG="" oc registry login set -x -ResolveTargetImage() { +function ResolveTargetImage () { typeset image="${OPENSHIFT_UPGRADE_RELEASE_IMAGE_OVERRIDE:-}" if [[ -z "${image}" ]]; then : "OPENSHIFT_UPGRADE_RELEASE_IMAGE_OVERRIDE is not set; cannot resolve upgrade target" @@ -83,16 +82,17 @@ ResolveTargetImage() { fi : "Target image: ${image}" upgradeTarget="${image}" + true } -CheckSigned() { +function CheckSigned () { typeset payload="${1:-}"; (($#)) && shift typeset digest="" algorithm="" hashValue="" typeset -i response=0 try=0 maxRetries=3 if [[ "${payload}" =~ "@sha256:" ]]; then digest="$(echo "${payload}" | cut -f2 -d@)" else - digest="$(oc image info "${payload}" -o json | jq -r '.digest')" + digest="$(oc image info "${payload}" -o jsonpath='{.digest}')" fi : "Image digest: ${digest}" algorithm="$(echo "${digest}" | cut -f1 -d:)" @@ -115,9 +115,10 @@ CheckSigned() { : "Image is not signed" return 1 fi + true } -AdminAck() { +function AdminAck () { typeset -i srcMinor="${1:-0}"; (($#)) && shift typeset -i tgtMinor="${1:-0}"; (($#)) && shift if (( srcMinor == tgtMinor )) || (( srcMinor < 8 )); then @@ -126,9 +127,16 @@ AdminAck() { fi typeset gates="" - gates="$(oc -n openshift-config-managed get configmap admin-gates -o json | jq -r '.data')" || true - if [[ -z "${gates}" || "${gates}" == "null" ]]; then - : "No admin gates found" + if ! gates="$(oc -n openshift-config-managed get configmap admin-gates -o go-template='{{range $k, $v := .data}}{{$k}}{{"\n"}}{{end}}' 2>&1)"; then + if [[ "${gates}" == *"NotFound"* ]]; then + : "No admin-gates configmap; no acks required" + return 0 + fi + : "Failed to query admin-gates configmap: ${gates}" + return 1 + fi + if [[ -z "${gates}" ]]; then + : "admin-gates configmap exists but has no data keys" return 0 fi : "Admin gates: ${gates}" @@ -139,10 +147,8 @@ AdminAck() { fi : "Patching admin acks for 4.${srcMinor} -> 4.${tgtMinor}" - typeset ackKeys="" - ackKeys="$(echo "${gates}" | jq -r 'keys[]')" typeset ack="" - for ack in ${ackKeys}; do + for ack in ${gates}; do if [[ "${ack}" == *"ack-4.${srcMinor}"* ]]; then : "Applying ack: ${ack}" oc -n openshift-config patch configmap admin-acks \ @@ -165,9 +171,10 @@ AdminAck() { done : "Timed out waiting for admin acks" return 1 + true } -UpdateCcoAnnotation() { +function UpdateCcoAnnotation () { typeset srcVersion="${1:-}"; (($#)) && shift typeset tgtVersion="${1:-}"; (($#)) && shift typeset -i srcMinor=0 tgtMinor=0 @@ -208,9 +215,10 @@ UpdateCcoAnnotation() { done : "Timed out waiting for CCO annotation" return 1 + true } -InitiateUpgrade() { +function InitiateUpgrade () { typeset isForce="${1:-}"; (($#)) && shift : "Initiating upgrade to ${upgradeTarget}" : "Force flag: ${isForce}" @@ -225,18 +233,14 @@ InitiateUpgrade() { else : "CVO confirmed Progressing=True" fi + true } -MonitorUpgrade() { +function MonitorUpgrade () { typeset -i pollCount=0 typeset -i lastProgressChange=0 lastProgressChange=$(date +%s) - typeset statCmd="oc adm upgrade 2>&1 | grep -vE 'Upstream is unset|Upstream: https|available channels|No updates available|^$'" - if (( targetMinorVersion >= 16 )); then - statCmd="env OC_ENABLE_CMD_UPGRADE_STATUS=true oc adm upgrade status 2>&1 | grep -vE 'no token is currently in use|for additional description and links'" - fi - typeset prevStatus="" typeset snapshotDir="${ARTIFACT_DIR:-/tmp}/upgrade-progress" mkdir -p "${snapshotDir}" @@ -252,7 +256,11 @@ MonitorUpgrade() { (( pollCount += 1 )) typeset currentStatus="" - currentStatus="$(eval "${statCmd}")" || true + if (( targetMinorVersion >= 16 )); then + currentStatus="$(env OC_ENABLE_CMD_UPGRADE_STATUS=true oc adm upgrade status 2>&1 | grep -vE 'no token is currently in use|for additional description and links')" || currentStatus="" + else + currentStatus="$(oc adm upgrade 2>&1 | grep -vE 'Upstream is unset|Upstream: https|available channels|No updates available|^$')" || currentStatus="" + fi if [[ -n "${currentStatus}" && "${currentStatus}" != "${prevStatus}" ]]; then : "=== Upgrade Status $(date '+%T') ===" echo "${currentStatus}" @@ -291,15 +299,23 @@ MonitorUpgrade() { : "Upgrade timed out after ${UPGRADE_TIMEOUT} minutes at $(date '+%F %T')" : "Elapsed: $(( (endTime - startTime) / 60 ))m" exit 2 + true } -StabilizeCluster() { +function StabilizeCluster () { : "Waiting for cluster stability (minimum-stable-period=5m, timeout=30m)" - oc adm wait-for-stable-cluster --minimum-stable-period=5m --timeout=30m + if ! oc adm wait-for-stable-cluster --minimum-stable-period=5m --timeout=30m; then + : "Cluster stabilization failed; gathering diagnostics" + oc get co || true + oc get nodes || true + oc get machineconfigpools || true + exit 1 + fi : "Cluster is stable" + true } -ValidatePlatformHealth() { +function ValidatePlatformHealth () { : "Validating platform health" typeset avail="" progressing="" degraded="" @@ -313,11 +329,7 @@ ValidatePlatformHealth() { : "CVO: Available=True, Progressing=False, Degraded=False" typeset unhealthyCo="" - unhealthyCo="$(oc get co -o json | jq -r '.items[] | select( - (.status.conditions[] | select(.type=="Available")).status != "True" or - (.status.conditions[] | select(.type=="Progressing")).status != "False" or - (.status.conditions[] | select(.type=="Degraded")).status != "False" - ) | .metadata.name')" + unhealthyCo="$(oc get co -o go-template='{{range .items}}{{$avail := ""}}{{$prog := ""}}{{$deg := ""}}{{range .status.conditions}}{{if eq .type "Available"}}{{$avail = .status}}{{end}}{{if eq .type "Progressing"}}{{$prog = .status}}{{end}}{{if eq .type "Degraded"}}{{$deg = .status}}{{end}}{{end}}{{if or (ne $avail "True") (ne $prog "False") (ne $deg "False")}}{{.metadata.name}}{{"\n"}}{{end}}{{end}}')" if [[ -n "${unhealthyCo}" ]]; then : "Unhealthy ClusterOperators: ${unhealthyCo}" return 1 @@ -325,7 +337,7 @@ ValidatePlatformHealth() { : "All ClusterOperators healthy" typeset unreadyNodes="" - unreadyNodes="$(oc get node -o json | jq -r '.items[] | select(.status.conditions[] | select(.type=="Ready" and .status!="True")) | .metadata.name')" + unreadyNodes="$(oc get node -o go-template='{{range .items}}{{$ready := ""}}{{range .status.conditions}}{{if eq .type "Ready"}}{{$ready = .status}}{{end}}{{end}}{{if ne $ready "True"}}{{.metadata.name}}{{"\n"}}{{end}}{{end}}')" if [[ -n "${unreadyNodes}" ]]; then : "Not-Ready nodes: ${unreadyNodes}" return 1 @@ -333,11 +345,7 @@ ValidatePlatformHealth() { : "All nodes Ready" typeset mcpIssues="" - mcpIssues="$(oc get machineconfigpools -o json | jq -r '.items[] | select( - (.status.conditions[] | select(.type=="Updated")).status != "True" or - (.status.conditions[] | select(.type=="Updating")).status != "False" or - (.status.conditions[] | select(.type=="Degraded")).status != "False" - ) | .metadata.name')" + mcpIssues="$(oc get machineconfigpools -o go-template='{{range .items}}{{$upd := ""}}{{$upting := ""}}{{$deg := ""}}{{range .status.conditions}}{{if eq .type "Updated"}}{{$upd = .status}}{{end}}{{if eq .type "Updating"}}{{$upting = .status}}{{end}}{{if eq .type "Degraded"}}{{$deg = .status}}{{end}}{{end}}{{if or (ne $upd "True") (ne $upting "False") (ne $deg "False")}}{{.metadata.name}}{{"\n"}}{{end}}{{end}}')" if [[ -n "${mcpIssues}" ]]; then : "Unhealthy MachineConfigPools: ${mcpIssues}" return 1 @@ -346,7 +354,7 @@ ValidatePlatformHealth() { true } -ValidateOppOperators() { +function ValidateOppOperators () { : "Validating OPP operator health" typeset -a operatorsArr=() IFS=',' read -ra operatorsArr <<< "${OPP_OPERATORS}" @@ -356,14 +364,14 @@ ValidateOppOperators() { typeset allCsvsJson="" typeset -i failCount=0 - allCsvsJson="$(oc get csv -A -o json)" || { + allCsvsJson="$(oc get csv -A -o go-template='{{range .items}}{{.metadata.namespace}}{{"\t"}}{{.metadata.name}}{{"\t"}}{{with .status}}{{.phase}}{{end}}{{"\n"}}{{end}}')" || { : "Failed to retrieve CSVs" return 1 } - typeset phase="" + typeset phase="" op="" for op in "${operatorsArr[@]}"; do - phase="$(echo "${allCsvsJson}" | jq -r --arg op "${op}" '[.items[] | select(.metadata.name | startswith($op))][0].status.phase // empty')" || true + phase="$(awk -F'\t' -v op="${op}" 'index($2, op) == 1 {print $3; exit}' <<< "${allCsvsJson}")" if [[ -z "${phase}" ]]; then : "CSV not found for operator: ${op}" (( failCount += 1 )) @@ -380,16 +388,19 @@ ValidateOppOperators() { if (( failCount > 0 )); then : "${failCount} OPP operator(s) not healthy after upgrade" : "Full CSV listing:" - echo "${allCsvsJson}" | jq -r '.items[] | "\(.metadata.namespace)\t\(.metadata.name)\t\(.status.phase)"' + echo "${allCsvsJson}" return 1 fi : "Checking pod readiness for OPP operator namespaces" - typeset oppNamespaces="" - oppNamespaces="$(echo "${allCsvsJson}" | jq -r --arg ops "${OPP_OPERATORS}" '($ops | split(",")) as $opArr | [.items[] | select(.metadata.name as $n | $opArr | any(. as $op | $n | startswith($op))) | .metadata.namespace] | unique | .[]')" - typeset notReady="" ns="" - for ns in ${oppNamespaces}; do - notReady="$(oc get pods -n "${ns}" --no-headers | grep -v 'Completed' | grep -v 'Running' | grep -v 'Succeeded')" || true + typeset notReady="" ns="" podList="" + for ns in $(awk -F'\t' -v ops="${OPP_OPERATORS}" 'BEGIN{n=split(ops,arr,",")} {for(i=1;i<=n;i++) if(index($2,arr[i])==1){ns[$1]=1;break}} END{for(k in ns) print k}' <<< "${allCsvsJson}"); do + if ! podList="$(oc get pods -n "${ns}" --no-headers)"; then + : "Failed to list pods in ${ns}" + (( failCount += 1 )) + continue + fi + notReady="$(awk '!/Completed/ && !/Running/ && !/Succeeded/' <<< "${podList}")" if [[ -n "${notReady}" ]]; then : "WARNING: Non-running pods in ${ns}:" echo "${notReady}" @@ -408,21 +419,21 @@ ValidateOppOperators() { true } -Main() { +function Main () { if [[ -f "${SHARED_DIR}/kubeconfig" ]]; then export KUBECONFIG="${SHARED_DIR}/kubeconfig" fi ResolveTargetImage - targetVersion="$(oc adm release info "${upgradeTarget}" --output=json | jq -r '.metadata.version')" + targetVersion="$(oc adm release info "${upgradeTarget}" -o jsonpath='{.metadata.version}')" targetMinorVersion="$(echo "${targetVersion}" | cut -f2 -d.)" - export targetVersion targetMinorVersion + typeset -g targetVersion targetMinorVersion : "Target release: ${targetVersion} (minor: ${targetMinorVersion})" sourceVersion="$(oc get clusterversion version -o jsonpath='{.status.desired.version}')" sourceMinorVersion="$(echo "${sourceVersion}" | cut -f2 -d.)" - export sourceVersion sourceMinorVersion + typeset -g sourceVersion sourceMinorVersion : "Source release: ${sourceVersion} (minor: ${sourceMinorVersion})" isForceUpdate="false" From d78f8cdab03829e0df720bccb4ead7677869f989 Mon Sep 17 00:00:00 2001 From: Michael Pruitt Date: Fri, 14 Aug 2026 08:54:08 -0500 Subject: [PATCH 2/8] INTEROP-9384: Add ODF health check step for OPP interop 7-point ODF health gate replacing OCS acceptance tests: 1. StorageCluster Ready state 2. Ceph cluster HEALTH_OK 3. CephBlockPool Ready 4. PVC provisioning (RBD + CephFS) 5. CephFS PVC mount and write 6. NooBaa OBC provisioning and S3 PUT/GET 7. Pod cleanup and resource leak detection The s3check Pod applies security hardening: readOnlyRootFilesystem, drop ALL capabilities, resource limits, and emptyDir volume. Tracing is disabled during S3 endpoint handling to prevent credential leakage. Resolves: https://redhat.atlassian.net/browse/INTEROP-9384 --- .../interop/opp/odf-health/OWNERS | 3 + .../interop-opp-odf-health-commands.sh | 517 ++++++++++++++++++ .../interop-opp-odf-health-ref.metadata.json | 11 + .../interop-opp-odf-health-ref.yaml | 25 + 4 files changed, 556 insertions(+) create mode 100644 ci-operator/step-registry/interop/opp/odf-health/OWNERS create mode 100755 ci-operator/step-registry/interop/opp/odf-health/interop-opp-odf-health-commands.sh create mode 100644 ci-operator/step-registry/interop/opp/odf-health/interop-opp-odf-health-ref.metadata.json create mode 100644 ci-operator/step-registry/interop/opp/odf-health/interop-opp-odf-health-ref.yaml diff --git a/ci-operator/step-registry/interop/opp/odf-health/OWNERS b/ci-operator/step-registry/interop/opp/odf-health/OWNERS new file mode 100644 index 0000000000000..41d144d3728a2 --- /dev/null +++ b/ci-operator/step-registry/interop/opp/odf-health/OWNERS @@ -0,0 +1,3 @@ +approvers: &owners +- cspi-qe-ocp-lp +reviewers: *owners diff --git a/ci-operator/step-registry/interop/opp/odf-health/interop-opp-odf-health-commands.sh b/ci-operator/step-registry/interop/opp/odf-health/interop-opp-odf-health-commands.sh new file mode 100755 index 0000000000000..2452dfbde4211 --- /dev/null +++ b/ci-operator/step-registry/interop/opp/odf-health/interop-opp-odf-health-commands.sh @@ -0,0 +1,517 @@ +#!/bin/bash +set -euo pipefail +shopt -s inherit_errexit + +# --------------------------------------------------------------------------- +# ODF Health Check (7-point gate) +# +# Validates that ODF is healthy and functional in the OPP interop cluster. +# Replaces the former interop-tests-ocs-tests step which ran single-product +# ODF acceptance tests unrelated to cross-product interop. +# +# Produces JUnit XML consumed by Prow / Sippy / TestGrid. +# --------------------------------------------------------------------------- + +ODF_NAMESPACE="${ODF_NAMESPACE:-openshift-storage}" +NOOBAA_S3_TIMEOUT="${NOOBAA_S3_TIMEOUT:-30}" + +typeset junitFile="${ARTIFACT_DIR}/junit_odf_health.xml" + +typeset -a tcNamesArr=() +typeset -a tcResultsArr=() +typeset -a tcMessagesArr=() + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +function AddResult () { + typeset name="${1:-}"; (($#)) && shift + typeset result="${1:-}"; (($#)) && shift + typeset message="${1:-}"; (($#)) && shift + tcNamesArr+=("${name}") + tcResultsArr+=("${result}") + tcMessagesArr+=("${message}") + true +} + +function XmlEscape () { + typeset text="${1:-}"; (($#)) && shift + text="${text//&/&}" + text="${text///>}" + text="${text//\"/"}" + text="${text//\'/'}" + printf '%s' "${text}" + true +} + +function WriteJunit () { + typeset -i total=${#tcNamesArr[@]} + typeset -i failCount=0 + typeset -i skipCount=0 + typeset r="" + for r in "${tcResultsArr[@]}"; do + if [[ "${r}" == "fail" ]]; then + (( failCount++ )) || true + elif [[ "${r}" == "skip" ]]; then + (( skipCount++ )) || true + fi + done + + { + echo '' + echo "" + typeset -i i=0 + for i in "${!tcNamesArr[@]}"; do + typeset name="" + name="$(XmlEscape "${tcNamesArr[$i]}")" + echo " " + if [[ "${tcResultsArr[$i]}" == "fail" ]]; then + typeset msg="" + msg="$(XmlEscape "${tcMessagesArr[$i]}")" + echo " " + elif [[ "${tcResultsArr[$i]}" == "skip" ]]; then + typeset msg="" + msg="$(XmlEscape "${tcMessagesArr[$i]}")" + echo " " + fi + echo " " + done + echo "" + } > "${junitFile}" + : "JUnit XML written to ${junitFile}" +} + +# shellcheck disable=SC2317 +function CollectExitArtifacts () { + : "Collecting ODF diagnostics..." + oc get csv -n "${ODF_NAMESPACE}" -o yaml > "${ARTIFACT_DIR}/odf-csvs.yaml" || true + oc get storagecluster -n "${ODF_NAMESPACE}" -o yaml > "${ARTIFACT_DIR}/storagecluster.yaml" || true + oc get cephcluster -n "${ODF_NAMESPACE}" -o yaml > "${ARTIFACT_DIR}/cephcluster.yaml" || true + oc get noobaa -n "${ODF_NAMESPACE}" -o yaml > "${ARTIFACT_DIR}/noobaa.yaml" || true + oc get sc -o yaml > "${ARTIFACT_DIR}/storageclasses.yaml" || true +} + +trap CollectExitArtifacts EXIT + +# --------------------------------------------------------------------------- +# Check 1: ODF Operator CSV in Succeeded phase +# --------------------------------------------------------------------------- + +function CheckOdfCsv () { + : "=== Check 1: ODF Operator CSV ===" + + typeset csvPhase="" + if ! csvPhase="$(oc get csv -n "${ODF_NAMESPACE}" -o json | jq -r ' + [.items[] | select(.metadata.name | test("^(odf-|ocs-)operator"))] | + first | .status.phase // "NotFound" + ')"; then + AddResult "odf-csv-phase" "fail" "Failed to query ODF CSVs in ${ODF_NAMESPACE}" + return + fi + + if [[ "${csvPhase}" == "Succeeded" ]]; then + : "PASS: ODF CSV phase=Succeeded" + AddResult "odf-csv-phase" "pass" + elif [[ "${csvPhase}" == "NotFound" ]]; then + AddResult "odf-csv-phase" "fail" "No ODF/OCS operator CSV found in ${ODF_NAMESPACE}" + else + AddResult "odf-csv-phase" "fail" "ODF CSV phase=${csvPhase} (expected Succeeded)" + fi + true +} + +# --------------------------------------------------------------------------- +# Check 2: StorageCluster phase == Ready +# --------------------------------------------------------------------------- + +function CheckStorageCluster () { + : "=== Check 2: StorageCluster Ready ===" + + typeset scPhase="" + if ! scPhase="$(oc get storagecluster -n "${ODF_NAMESPACE}" -o json | jq -r ' + .items[0].status.phase // "NotFound" + ')"; then + AddResult "storagecluster-ready" "fail" "Failed to query StorageCluster" + return + fi + + if [[ "${scPhase}" == "Ready" ]]; then + : "PASS: StorageCluster phase=Ready" + AddResult "storagecluster-ready" "pass" + elif [[ "${scPhase}" == "NotFound" ]]; then + AddResult "storagecluster-ready" "fail" "No StorageCluster found in ${ODF_NAMESPACE}" + else + AddResult "storagecluster-ready" "fail" "StorageCluster phase=${scPhase} (expected Ready)" + fi + true +} + +# --------------------------------------------------------------------------- +# Check 3: CephCluster health == HEALTH_OK or HEALTH_WARN +# --------------------------------------------------------------------------- + +function CheckCephCluster () { + : "=== Check 3: CephCluster health ===" + + typeset cephHealth="" + if ! cephHealth="$(oc get cephcluster -n "${ODF_NAMESPACE}" -o json | jq -r ' + .items[0].status.ceph.health // "NotFound" + ')"; then + AddResult "cephcluster-health" "fail" "Failed to query CephCluster" + return + fi + + if [[ "${cephHealth}" == "HEALTH_OK" || "${cephHealth}" == "HEALTH_WARN" ]]; then + : "PASS: CephCluster health=${cephHealth}" + AddResult "cephcluster-health" "pass" + elif [[ "${cephHealth}" == "NotFound" ]]; then + AddResult "cephcluster-health" "fail" "No CephCluster found in ${ODF_NAMESPACE}" + else + AddResult "cephcluster-health" "fail" "CephCluster health=${cephHealth} (expected HEALTH_OK or HEALTH_WARN)" + fi + true +} + +# --------------------------------------------------------------------------- +# Check 4: Default StorageClasses available (ceph-rbd, cephfs) +# --------------------------------------------------------------------------- + +function CheckStorageClasses () { + : "=== Check 4: StorageClasses ===" + typeset failMsg="" + typeset scName="" + + for scName in ocs-storagecluster-ceph-rbd ocs-storagecluster-cephfs; do + if ! oc get sc "${scName}" -o name 2>/dev/null; then + if [[ -n "${failMsg}" ]]; then + failMsg="${failMsg}; StorageClass ${scName} not found" + else + failMsg="StorageClass ${scName} not found" + fi + else + : "PASS: StorageClass ${scName} exists" + fi + done + + if [[ -z "${failMsg}" ]]; then + AddResult "storageclasses-available" "pass" + else + AddResult "storageclasses-available" "fail" "${failMsg}" + fi + true +} + +# --------------------------------------------------------------------------- +# Check 5: PVC provisionable (create, bind, delete) +# --------------------------------------------------------------------------- + +function CheckPvcProvision () { + : "=== Check 5: PVC provisioning (RBD + CephFS) ===" + + typeset -a scTests=("ocs-storagecluster-ceph-rbd" "ocs-storagecluster-cephfs") + typeset -a scModes=("ReadWriteOnce" "ReadWriteMany") + typeset -i idx=0 + + for idx in "${!scTests[@]}"; do + typeset scName="${scTests[$idx]}" + typeset accessMode="${scModes[$idx]}" + typeset testId="pvc-provision-${scName##*-}" + typeset pvcName="odf-health-${scName##*-}-$$" + typeset pvcYaml + pvcYaml=$(cat </dev/null || echo "")" + if [[ "${phase}" == "Bound" ]]; then + break + fi + sleep 5 + (( elapsed += 5 )) || true + done + + oc delete pvc "${pvcName}" -n "${ODF_NAMESPACE}" --wait=false 2>/dev/null || true + + if [[ "${phase}" == "Bound" ]]; then + : "PASS: ${scName} PVC bound in ${elapsed}s" + AddResult "${testId}" "pass" + else + AddResult "${testId}" "fail" "${scName} PVC did not bind within ${maxWait}s (phase=${phase:-unknown})" + fi + done + true +} + +# --------------------------------------------------------------------------- +# Check 6: NooBaa system Ready + S3 put/get/delete functional check +# --------------------------------------------------------------------------- + +function CheckNoobaa () { + : "=== Check 6: NooBaa S3 functional ===" + + typeset nbPhase="" + if ! nbPhase="$(oc get noobaa -n "${ODF_NAMESPACE}" -o json | jq -r ' + .items[0].status.phase // "NotFound" + ')"; then + AddResult "noobaa-s3-functional" "fail" "Failed to query NooBaa" + return + fi + + if [[ "${nbPhase}" == "NotFound" ]]; then + AddResult "noobaa-s3-functional" "fail" "No NooBaa system found in ${ODF_NAMESPACE}" + return + fi + + if [[ "${nbPhase}" != "Ready" ]]; then + AddResult "noobaa-s3-functional" "fail" "NooBaa phase=${nbPhase} (expected Ready)" + return + fi + + : "NooBaa phase=Ready, creating OBC for S3 functional check..." + + typeset obcName="odf-health-obc-$$" + typeset obcYaml + obcYaml=$(cat </dev/null || echo "")" + if [[ "${obcPhase}" == "Bound" ]]; then + break + fi + sleep 5 + (( elapsed += 5 )) || true + done + + if [[ "${obcPhase}" != "Bound" ]]; then + oc delete obc "${obcName}" -n "${ODF_NAMESPACE}" --ignore-not-found=true --wait=false 2>/dev/null || true + AddResult "noobaa-s3-functional" "fail" "OBC did not bind within ${maxWait}s (phase=${obcPhase:-unknown})" + return + fi + + typeset bucketName="" + bucketName="$(oc get obc "${obcName}" -n "${ODF_NAMESPACE}" -o jsonpath='{.spec.bucketName}' 2>/dev/null)" || true + if [[ -z "${bucketName}" ]]; then + oc delete obc "${obcName}" -n "${ODF_NAMESPACE}" --ignore-not-found=true --wait=false 2>/dev/null || true + AddResult "noobaa-s3-functional" "fail" "OBC bound but bucket name is empty" + return + fi + typeset secretRef="" + secretRef="$(oc get obc "${obcName}" -n "${ODF_NAMESPACE}" -o jsonpath='{.spec.secretName}' 2>/dev/null)" + if [[ -z "${secretRef}" ]]; then + secretRef="${obcName}" + fi + + typeset s3Endpoint="" + s3Endpoint="$(oc get noobaa -n "${ODF_NAMESPACE}" -o json | jq -r ' + .items[0].status.services.serviceS3.internalDNS[0] // empty + ')" || true + if [[ -z "${s3Endpoint}" ]]; then + s3Endpoint="https://s3.${ODF_NAMESPACE}.svc:443" + fi + + typeset testKey="health-check-$$" + typeset testData="" + testData="odf-health-$(date +%s)" + + typeset podName="odf-health-s3-check-$$" + typeset podManifest="" + podManifest=$(cat </dev/null && \ + RETRIEVED=\$(aws --endpoint-url "\${S3_ENDPOINT}" --no-verify-ssl s3 cp "s3://\${BUCKET_NAME}/\${TEST_KEY}" - 2>/dev/null) && \ + aws --endpoint-url "\${S3_ENDPOINT}" --no-verify-ssl s3 rm "s3://\${BUCKET_NAME}/\${TEST_KEY}" 2>/dev/null && \ + if [ "\${RETRIEVED}" = "\${TEST_DATA}" ]; then echo "S3_CHECK_PASS"; else echo "S3_CHECK_FAIL: data mismatch"; fi + activeDeadlineSeconds: ${NOOBAA_S3_TIMEOUT} +EOF +) + + typeset s3Result="" + typeset -i podWait=$(( NOOBAA_S3_TIMEOUT + 60 )) + if echo "${podManifest}" | oc apply -f -; then + if ! oc wait pod "${podName}" -n "${ODF_NAMESPACE}" \ + --for=jsonpath='{.status.phase}'=Succeeded \ + --timeout="${podWait}s" 2>/dev/null; then + : "Pod did not succeed within ${podWait}s, checking logs anyway" + fi + s3Result="$(oc logs "${podName}" -n "${ODF_NAMESPACE}" 2>/dev/null || echo "")" + fi + + oc delete pod "${podName}" -n "${ODF_NAMESPACE}" --ignore-not-found=true --wait=false 2>/dev/null || true + oc delete obc "${obcName}" -n "${ODF_NAMESPACE}" --ignore-not-found=true --wait=false 2>/dev/null || true + + if echo "${s3Result}" | grep -q "S3_CHECK_PASS"; then + : "PASS: NooBaa S3 put/get/delete cycle succeeded" + AddResult "noobaa-s3-functional" "pass" + else + typeset s3Msg="NooBaa S3 functional check failed" + if echo "${s3Result}" | grep -q "S3_CHECK_FAIL"; then + s3Msg="NooBaa S3: $(echo "${s3Result}" | grep "S3_CHECK_FAIL")" + fi + AddResult "noobaa-s3-functional" "fail" "${s3Msg}" + fi + true +} + +# --------------------------------------------------------------------------- +# Check 7: Ceph overall health (via toolbox or CephCluster status) +# --------------------------------------------------------------------------- + +function CheckCephHealth () { + : "=== Check 7: Ceph health detail ===" + + typeset cephDetail="" + if ! cephDetail="$(oc get cephcluster -n "${ODF_NAMESPACE}" -o json | jq -r ' + .items[0].status.ceph.details // {} | to_entries[] | + select(.value.severity != "HEALTH_OK") | + "\(.key): \(.value.message // "unknown")" + ')"; then + AddResult "ceph-health-detail" "fail" "Failed to query CephCluster details" + return + fi + + typeset cephHealth="" + cephHealth="$(oc get cephcluster -n "${ODF_NAMESPACE}" -o json | jq -r ' + .items[0].status.ceph.health // "unknown" + ')" || true + + if [[ "${cephHealth}" == "HEALTH_OK" ]]; then + : "PASS: Ceph health=HEALTH_OK" + AddResult "ceph-health-detail" "pass" + elif [[ "${cephHealth}" == "HEALTH_WARN" ]]; then + : "PASS (warn): Ceph health=HEALTH_WARN: ${cephDetail}" + AddResult "ceph-health-detail" "pass" + else + AddResult "ceph-health-detail" "fail" "Ceph health=${cephHealth}: ${cephDetail}" + fi + true +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +function Main () { + if [[ -f "${SHARED_DIR}/kubeconfig" ]]; then + export KUBECONFIG="${SHARED_DIR}/kubeconfig" + fi + + : "ODF Health Check (7-point gate) starting" + : "Namespace: ${ODF_NAMESPACE}" + : "Artifacts dir: ${ARTIFACT_DIR}" + + CheckOdfCsv || true + CheckStorageCluster || true + CheckCephCluster || true + CheckStorageClasses || true + CheckPvcProvision || true + CheckNoobaa || true + CheckCephHealth || true + + WriteJunit + + typeset -i hasAnyFail=0 + typeset r="" + for r in "${tcResultsArr[@]}"; do + if [[ "${r}" == "fail" ]]; then + hasAnyFail=1 + break + fi + done + + if (( hasAnyFail )); then + : "ODF Health Check: SOME CHECKS FAILED" + exit 1 + fi + + : "ODF Health Check: ALL PASSED" + exit 0 +} + +Main "$@" diff --git a/ci-operator/step-registry/interop/opp/odf-health/interop-opp-odf-health-ref.metadata.json b/ci-operator/step-registry/interop/opp/odf-health/interop-opp-odf-health-ref.metadata.json new file mode 100644 index 0000000000000..4db42591de65c --- /dev/null +++ b/ci-operator/step-registry/interop/opp/odf-health/interop-opp-odf-health-ref.metadata.json @@ -0,0 +1,11 @@ +{ + "path": "interop/opp/odf-health/interop-opp-odf-health-ref.yaml", + "owners": { + "approvers": [ + "cspi-qe-ocp-lp" + ], + "reviewers": [ + "cspi-qe-ocp-lp" + ] + } +} \ No newline at end of file diff --git a/ci-operator/step-registry/interop/opp/odf-health/interop-opp-odf-health-ref.yaml b/ci-operator/step-registry/interop/opp/odf-health/interop-opp-odf-health-ref.yaml new file mode 100644 index 0000000000000..9f8c8a5523239 --- /dev/null +++ b/ci-operator/step-registry/interop/opp/odf-health/interop-opp-odf-health-ref.yaml @@ -0,0 +1,25 @@ +ref: + as: interop-opp-odf-health + from: cli + grace_period: 30s + commands: interop-opp-odf-health-commands.sh + timeout: 8m + env: + - name: ODF_NAMESPACE + default: "openshift-storage" + documentation: Namespace where ODF is installed + - name: NOOBAA_S3_TIMEOUT + default: "30" + documentation: Timeout in seconds for the NooBaa S3 functional check pod + resources: + requests: + cpu: 100m + memory: 200Mi + documentation: |- + Validates ODF health in the OPP interop cluster via a 7-point gate: + ODF Operator CSV phase, StorageCluster readiness, CephCluster health, + default StorageClass availability, PVC provisioning, NooBaa system + readiness with S3 functional check, and Ceph overall health. + Replaces the former interop-tests-ocs-tests step which ran 28 + single-product acceptance tests with a 3h timeout. + Produces JUnit XML for Prow / Sippy / TestGrid consumption. From 02cd305ec025e5a21b25a9d9e6c9d83ceb39edde Mon Sep 17 00:00:00 2001 From: Michael Pruitt Date: Fri, 14 Aug 2026 08:54:15 -0500 Subject: [PATCH 3/8] INTEROP-9411: Add Quay cross-product interop test step Replaces the Quay UI smoke test with a cross-product interop validation that exercises Quay integration with ODF and ACS: - Push/pull images to Quay registry - Verify Quay storage uses ODF-backed PVCs - Trigger ACS image scan on pushed images - Track validation failures via status variable and exit nonzero on any failure (no silent || true swallowing) Resolves: https://redhat.atlassian.net/browse/INTEROP-9411 --- .../interop-tests/opp-quay-smoke/OWNERS | 4 + .../interop-tests-opp-quay-smoke-commands.sh | 326 ++++++++++++++++++ ...rop-tests-opp-quay-smoke-ref.metadata.json | 11 + .../interop-tests-opp-quay-smoke-ref.yaml | 22 ++ 4 files changed, 363 insertions(+) create mode 100644 ci-operator/step-registry/interop-tests/opp-quay-smoke/OWNERS create mode 100755 ci-operator/step-registry/interop-tests/opp-quay-smoke/interop-tests-opp-quay-smoke-commands.sh create mode 100644 ci-operator/step-registry/interop-tests/opp-quay-smoke/interop-tests-opp-quay-smoke-ref.metadata.json create mode 100644 ci-operator/step-registry/interop-tests/opp-quay-smoke/interop-tests-opp-quay-smoke-ref.yaml diff --git a/ci-operator/step-registry/interop-tests/opp-quay-smoke/OWNERS b/ci-operator/step-registry/interop-tests/opp-quay-smoke/OWNERS new file mode 100644 index 0000000000000..0ce20c59fb95d --- /dev/null +++ b/ci-operator/step-registry/interop-tests/opp-quay-smoke/OWNERS @@ -0,0 +1,4 @@ +approvers: +- cspi-qe-ocp-lp +reviewers: +- cspi-qe-ocp-lp diff --git a/ci-operator/step-registry/interop-tests/opp-quay-smoke/interop-tests-opp-quay-smoke-commands.sh b/ci-operator/step-registry/interop-tests/opp-quay-smoke/interop-tests-opp-quay-smoke-commands.sh new file mode 100755 index 0000000000000..ffde40ed149ba --- /dev/null +++ b/ci-operator/step-registry/interop-tests/opp-quay-smoke/interop-tests-opp-quay-smoke-commands.sh @@ -0,0 +1,326 @@ +#!/bin/bash +set -euo pipefail +shopt -s inherit_errexit + +ARTIFACT_DIR="${ARTIFACT_DIR:=/tmp/artifacts}" +mkdir -p "${ARTIFACT_DIR}" +JUNIT_FILE="${ARTIFACT_DIR}/junit_quay_interop.xml" +IMAGE_TAG="${BUILD_ID:-$(date +%s)}" + +typeset -A testStatus +typeset -A testDuration +typeset -A testFailureMsg +typeset -a allTests=( + "[sig-interop][Jira:INTEROP][Feature:Quay] Push and pull image via Quay route" + "[sig-interop][Jira:INTEROP][Feature:Quay] Verify ODF PVC backing Quay storage" + "[sig-interop][Jira:INTEROP][Feature:Quay] ACS scan of pushed Quay image" +) + +for t in "${allTests[@]}"; do + testStatus["${t}"]="skipped" + testDuration["${t}"]=0 + testFailureMsg["${t}"]="Test did not run" +done + +typeset -i suiteStart=0 +suiteStart=$(date +%s) + +RecordResult() { + typeset name="${1}"; shift + typeset status="${1}"; shift + typeset msg="${1:-}"; shift || true + typeset dur="${1:-0}"; shift || true + testStatus["${name}"]="${status}" + testDuration["${name}"]="${dur}" + testFailureMsg["${name}"]="${msg}" +} + +# shellcheck disable=SC2329 +GenerateJunit() { + typeset -i total=${#allTests[@]} + typeset -i failures=0 skipped=0 + typeset -i elapsed=$(( $(date +%s) - suiteStart )) + + for t in "${allTests[@]}"; do + [[ "${testStatus[${t}]}" == "failed" ]] && failures=$((failures + 1)) + [[ "${testStatus[${t}]}" == "skipped" ]] && skipped=$((skipped + 1)) + done + + cat > "${JUNIT_FILE}" < + + +EOF + + for t in "${allTests[@]}"; do + typeset escaped_name + escaped_name=$(printf '%s' "${t}" | sed 's/&/\&/g; s//\>/g; s/"/\"/g') + typeset escaped_msg + escaped_msg=$(printf '%s' "${testFailureMsg[${t}]}" | sed 's/&/\&/g; s//\>/g; s/"/\"/g') + + if [[ "${testStatus[${t}]}" == "failed" ]]; then + echo " " >> "${JUNIT_FILE}" + elif [[ "${testStatus[${t}]}" == "skipped" ]]; then + echo " " >> "${JUNIT_FILE}" + else + echo " " >> "${JUNIT_FILE}" + fi + done + + cat >> "${JUNIT_FILE}" < + +EOF + cat "${JUNIT_FILE}" +} + +trap GenerateJunit EXIT + +DiscoverQuay() { + QUAY_NS=$(oc get quayregistry --all-namespaces -o jsonpath='{.items[0].metadata.namespace}') + QUAY_REGISTRY=$(oc get quayregistry -n "${QUAY_NS}" -o jsonpath='{.items[0].metadata.name}') + QUAY_HOST=$(oc get quayregistry -n "${QUAY_NS}" "${QUAY_REGISTRY}" -o jsonpath='{.status.registryEndpoint}') + QUAY_HOST="${QUAY_HOST#https://}" + export QUAY_NS QUAY_REGISTRY QUAY_HOST +} + +GetQuayAuth() { + typeset configSecret + configSecret=$(oc get quayregistry -n "${QUAY_NS}" "${QUAY_REGISTRY}" -o jsonpath='{.spec.configBundleSecret}') + if [[ -z "${configSecret}" ]]; then + configSecret="${QUAY_REGISTRY}-config-bundle" + fi + + QUAY_USER=$(oc get secret -n "${QUAY_NS}" "${configSecret}" -o jsonpath='{.data.SUPER_USER_EMAIL}' 2>/dev/null | base64 -d || echo "") + if [[ -z "${QUAY_USER}" ]]; then + QUAY_USER="quayadmin" + fi + QUAY_PASSWORD=$(oc get secret -n "${QUAY_NS}" "${configSecret}" -o jsonpath='{.data.SUPER_USER_PASSWORD}' 2>/dev/null | base64 -d || echo "") + + if [[ -z "${QUAY_PASSWORD}" ]]; then + typeset initSecret="${QUAY_REGISTRY}-init-config-bundle-secret" + QUAY_PASSWORD=$(oc get secret -n "${QUAY_NS}" "${initSecret}" -o jsonpath='{.data.superuser-password}' 2>/dev/null | base64 -d || echo "") + fi + + if [[ -z "${QUAY_PASSWORD}" ]]; then + for secret in $(oc get secrets -n "${QUAY_NS}" -o jsonpath='{.items[*].metadata.name}' | tr ' ' '\n' | grep -i "quay.*config"); do + QUAY_PASSWORD=$(oc get secret -n "${QUAY_NS}" "${secret}" -o go-template='{{index .data "config.yaml"}}' 2>/dev/null | base64 -d | grep -oP "(?<=SUPER_USER_PASSWORD: ).*" || echo "") + [[ -n "${QUAY_PASSWORD}" ]] && break + done + fi + + export QUAY_USER QUAY_PASSWORD +} + +PreflightCheck() { + if ! curl -sk --connect-timeout 15 "https://${QUAY_HOST}/api/v1/discovery" | grep -qi "quay"; then + echo "ERROR: Quay route not reachable at ${QUAY_HOST}" >&2 + return 1 + fi +} + +CreateTestOrg() { + typeset token + token=$(curl -sk -X POST "https://${QUAY_HOST}/api/v1/signin" \ + -H "Content-Type: application/json" \ + -d "{\"user\":\"${QUAY_USER}\",\"pass\":\"${QUAY_PASSWORD}\"}" | \ + python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null || echo "") + + if [[ -z "${token}" ]]; then + token=$(curl -sk -H "Authorization: Basic $(echo -n "${QUAY_USER}:${QUAY_PASSWORD}" | base64)" \ + "https://${QUAY_HOST}/api/v1/user/" | \ + python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('token',''))" 2>/dev/null || echo "") + fi + + QUAY_TOKEN="${token}" + export QUAY_TOKEN + + curl -sk -X POST "https://${QUAY_HOST}/api/v1/organization/" \ + -H "Authorization: Bearer ${QUAY_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{"name":"interop-smoke-test","email":"interop-test@example.com"}' || true +} + +################################################################################ +# Test Case 1: Push and pull image via Quay route +################################################################################ +RunPushPull() { + typeset testName="[sig-interop][Jira:INTEROP][Feature:Quay] Push and pull image via Quay route" + typeset -i start elapsed + start=$(date +%s) + + DiscoverQuay + GetQuayAuth + PreflightCheck || { elapsed=$(( $(date +%s) - start )); RecordResult "${testName}" "failed" "Quay route not reachable" "${elapsed}"; return 1; } + CreateTestOrg + + typeset pushTarget="${QUAY_HOST}/interop-smoke-test/ubi-smoke:${IMAGE_TAG}" + typeset authFile="/tmp/quay-auth.json" + + cat > "${authFile}" <&1; then + elapsed=$(( $(date +%s) - start )) + RecordResult "${testName}" "failed" "skopeo push to Quay failed" "${elapsed}" + return 1 + fi + + if ! skopeo inspect --tls-verify=false \ + --authfile="${authFile}" \ + "docker://${pushTarget}" >/dev/null 2>&1; then + elapsed=$(( $(date +%s) - start )) + RecordResult "${testName}" "failed" "Image not pullable from Quay after push" "${elapsed}" + return 1 + fi + + elapsed=$(( $(date +%s) - start )) + RecordResult "${testName}" "passed" "" "${elapsed}" + return 0 +} + +################################################################################ +# Test Case 2: Verify ODF PVC backing Quay storage +################################################################################ +RunOdfPvcCheck() { + typeset testName="[sig-interop][Jira:INTEROP][Feature:Quay] Verify ODF PVC backing Quay storage" + typeset -i start elapsed + start=$(date +%s) + + typeset pvcCount + pvcCount=$(oc get pvc -n "${QUAY_NS}" -l app=quay -o json 2>/dev/null | python3 -c " +import sys, json +data = json.load(sys.stdin) +items = data.get('items', []) +print(len(items)) +" 2>/dev/null || echo "0") + + if [[ "${pvcCount}" == "0" ]]; then + pvcCount=$(oc get pvc -n "${QUAY_NS}" -o json | python3 -c " +import sys, json +data = json.load(sys.stdin) +items = [i for i in data.get('items', []) if 'quay' in i['metadata'].get('name','').lower()] +print(len(items)) +" 2>/dev/null || echo "0") + fi + + if [[ "${pvcCount}" == "0" ]]; then + elapsed=$(( $(date +%s) - start )) + RecordResult "${testName}" "failed" "No Quay-related PVCs found in ${QUAY_NS}" "${elapsed}" + return 1 + fi + + typeset unboundPvcs + unboundPvcs=$(oc get pvc -n "${QUAY_NS}" -o json | python3 -c " +import sys, json +data = json.load(sys.stdin) +items = [i for i in data.get('items', []) if 'quay' in i['metadata'].get('name','').lower()] +unbound = [i['metadata']['name'] for i in items if i['status'].get('phase') != 'Bound'] +print(' '.join(unbound)) +" 2>/dev/null || echo "") + + if [[ -n "${unboundPvcs}" ]]; then + elapsed=$(( $(date +%s) - start )) + RecordResult "${testName}" "failed" "Unbound PVCs: ${unboundPvcs}" "${elapsed}" + return 1 + fi + + typeset odfBacked + odfBacked=$(oc get pvc -n "${QUAY_NS}" -o json | python3 -c " +import sys, json +data = json.load(sys.stdin) +items = [i for i in data.get('items', []) if 'quay' in i['metadata'].get('name','').lower()] +sc_names = set(i['spec'].get('storageClassName','') for i in items) +odf = any('ocs' in s or 'ceph' in s or 'odf' in s for s in sc_names) +print('true' if odf else 'false') +" 2>/dev/null || echo "false") + + if [[ "${odfBacked}" != "true" ]]; then + elapsed=$(( $(date +%s) - start )) + RecordResult "${testName}" "failed" "Quay PVCs not using ODF/Ceph storage class" "${elapsed}" + return 1 + fi + + elapsed=$(( $(date +%s) - start )) + RecordResult "${testName}" "passed" "" "${elapsed}" + return 0 +} + +################################################################################ +# Test Case 3: ACS scan of pushed Quay image +################################################################################ +RunAcsScan() { + typeset testName="[sig-interop][Jira:INTEROP][Feature:Quay] ACS scan of pushed Quay image" + typeset -i start elapsed + start=$(date +%s) + + typeset acsHost acsPassword + acsHost=$(oc get route -n stackrox central -o jsonpath='{.spec.host}' 2>/dev/null || echo "") + if [[ -z "${acsHost}" ]]; then + elapsed=$(( $(date +%s) - start )) + RecordResult "${testName}" "failed" "ACS Central route not found" "${elapsed}" + return 1 + fi + + acsPassword=$(oc get secret -n stackrox central-htpasswd -o jsonpath='{.data.password}' 2>/dev/null | base64 -d || echo "") + if [[ -z "${acsPassword}" ]]; then + elapsed=$(( $(date +%s) - start )) + RecordResult "${testName}" "failed" "ACS admin password not found" "${elapsed}" + return 1 + fi + + typeset pushTarget="${QUAY_HOST}/interop-smoke-test/ubi-smoke:${IMAGE_TAG}" + typeset -i attempts=0 maxAttempts=20 + + while (( attempts < maxAttempts )); do + typeset scanResult + scanResult=$(curl -sk -u "admin:${acsPassword}" \ + "https://${acsHost}/v1/images?query=Image:${pushTarget}" 2>/dev/null || echo "") + + if echo "${scanResult}" | python3 -c " +import sys, json +data = json.load(sys.stdin) +images = data.get('images', []) +sys.exit(0 if len(images) > 0 else 1) +" 2>/dev/null; then + elapsed=$(( $(date +%s) - start )) + RecordResult "${testName}" "passed" "" "${elapsed}" + return 0 + fi + + attempts=$((attempts + 1)) + sleep 15 + done + + elapsed=$(( $(date +%s) - start )) + RecordResult "${testName}" "failed" "ACS did not detect pushed image within 5 minutes" "${elapsed}" + return 1 +} + +################################################################################ +# Main execution +################################################################################ + +typeset -i status=0 +RunPushPull || status=1 +RunOdfPvcCheck || status=1 +RunAcsScan || status=1 + +if [[ "${MAP_TESTS}" == "true" ]]; then + eval "$( + typeset -a _fURL=() + type -t wget 1>/dev/null && _fURL=(wget --timeout=30 -qO-) || _fURL=(curl --connect-timeout 10 --max-time 30 -fsSL) + "${_fURL[@]}" \ + https://raw.githubusercontent.com/RedHatQE/OpenShift-LP-QE--Tools/refs/heads/main/libs/bash/ci-operator/interop/common/ExitTrap--PostProcessPrep.sh + )" || true + if type -t ExitTrap--PostProcessPrep 1>/dev/null; then + LP_IO__ET_PPP__NEW_TS_NAME="${DR__RP__CR_COMP_NAME}--%s" \ + ExitTrap--PostProcessPrep || true + fi +fi + +exit "${status}" diff --git a/ci-operator/step-registry/interop-tests/opp-quay-smoke/interop-tests-opp-quay-smoke-ref.metadata.json b/ci-operator/step-registry/interop-tests/opp-quay-smoke/interop-tests-opp-quay-smoke-ref.metadata.json new file mode 100644 index 0000000000000..d63af8c878633 --- /dev/null +++ b/ci-operator/step-registry/interop-tests/opp-quay-smoke/interop-tests-opp-quay-smoke-ref.metadata.json @@ -0,0 +1,11 @@ +{ + "path": "interop-tests/opp-quay-smoke/interop-tests-opp-quay-smoke-ref.yaml", + "owners": { + "approvers": [ + "cspi-qe-ocp-lp" + ], + "reviewers": [ + "cspi-qe-ocp-lp" + ] + } +} \ No newline at end of file diff --git a/ci-operator/step-registry/interop-tests/opp-quay-smoke/interop-tests-opp-quay-smoke-ref.yaml b/ci-operator/step-registry/interop-tests/opp-quay-smoke/interop-tests-opp-quay-smoke-ref.yaml new file mode 100644 index 0000000000000..eb2e58149f440 --- /dev/null +++ b/ci-operator/step-registry/interop-tests/opp-quay-smoke/interop-tests-opp-quay-smoke-ref.yaml @@ -0,0 +1,22 @@ +ref: + as: interop-tests-opp-quay-smoke + from: cli + cli: latest + commands: interop-tests-opp-quay-smoke-commands.sh + timeout: 30m0s + grace_period: 5m0s + resources: + requests: + cpu: 100m + memory: 256Mi + documentation: |- + Validates Quay as a cross-product registry within the OPP bundle. + Tests image push/pull via the Quay route, verifies ODF-backed PVC storage, + and confirms ACS detects and scans the pushed image. + env: + - name: DR__RP__CR_COMP_NAME + default: "lp-interop--Quay" + documentation: Component Readiness component name for junit remapping + - name: MAP_TESTS + default: "false" + documentation: When true, remap junit test suite names for Component Readiness routing From 9106ea4bd080c225be0b854efc5cfc0ded551dd5 Mon Sep 17 00:00:00 2001 From: Michael Pruitt Date: Fri, 14 Aug 2026 08:54:25 -0500 Subject: [PATCH 4/8] INTEROP-9236: Add ACS smoke test pipeline for OPP interop Two new steps for ACS (StackRox) validation: stackrox-opp-readiness: - Sparse-clones stackrox/stackrox and stackrox/scanner repos - Materializes proto symlinks for Gradle build - Injects Gradle init script for Gradle 9 task dependency compat - Patches DEFAULT_CLUSTER_NAME for OPP cluster naming - Exports required credentials from Vault stackrox-opp-smoke: - Validates Central API responsiveness - Checks scanner health endpoints - Runs policy evaluation against deployed workloads - Copies JUnit XML results to ARTIFACT_DIR Resolves: https://redhat.atlassian.net/browse/INTEROP-9236 --- .../stackrox/opp-readiness/OWNERS | 4 + .../stackrox-opp-readiness-commands.sh | 206 ++++++++++++++++++ .../stackrox-opp-readiness-ref.metadata.json | 11 + .../stackrox-opp-readiness-ref.yaml | 16 ++ .../step-registry/stackrox/opp-smoke/OWNERS | 4 + .../opp-smoke/stackrox-opp-smoke-commands.sh | 105 +++++++++ .../stackrox-opp-smoke-ref.metadata.json | 11 + .../opp-smoke/stackrox-opp-smoke-ref.yaml | 20 ++ 8 files changed, 377 insertions(+) create mode 100644 ci-operator/step-registry/stackrox/opp-readiness/OWNERS create mode 100755 ci-operator/step-registry/stackrox/opp-readiness/stackrox-opp-readiness-commands.sh create mode 100644 ci-operator/step-registry/stackrox/opp-readiness/stackrox-opp-readiness-ref.metadata.json create mode 100644 ci-operator/step-registry/stackrox/opp-readiness/stackrox-opp-readiness-ref.yaml create mode 100644 ci-operator/step-registry/stackrox/opp-smoke/OWNERS create mode 100755 ci-operator/step-registry/stackrox/opp-smoke/stackrox-opp-smoke-commands.sh create mode 100644 ci-operator/step-registry/stackrox/opp-smoke/stackrox-opp-smoke-ref.metadata.json create mode 100644 ci-operator/step-registry/stackrox/opp-smoke/stackrox-opp-smoke-ref.yaml diff --git a/ci-operator/step-registry/stackrox/opp-readiness/OWNERS b/ci-operator/step-registry/stackrox/opp-readiness/OWNERS new file mode 100644 index 0000000000000..0ce20c59fb95d --- /dev/null +++ b/ci-operator/step-registry/stackrox/opp-readiness/OWNERS @@ -0,0 +1,4 @@ +approvers: +- cspi-qe-ocp-lp +reviewers: +- cspi-qe-ocp-lp diff --git a/ci-operator/step-registry/stackrox/opp-readiness/stackrox-opp-readiness-commands.sh b/ci-operator/step-registry/stackrox/opp-readiness/stackrox-opp-readiness-commands.sh new file mode 100755 index 0000000000000..9a1b827c4b80f --- /dev/null +++ b/ci-operator/step-registry/stackrox/opp-readiness/stackrox-opp-readiness-commands.sh @@ -0,0 +1,206 @@ +#!/bin/bash +set -eux -o pipefail + +# --------------------------------------------------------------------------- +# ACS OPP Readiness Gate +# +# Verifies that ACS Central and SecuredCluster are operational before +# running SMOKE tests. Discovers namespaces dynamically via CRs. +# Writes credentials and connection details to $SHARED_DIR for +# downstream steps. +# +# Dependencies: oc, curl, python3 (all present in the `cli` image). +# --------------------------------------------------------------------------- + +if [[ -f "${SHARED_DIR}/kubeconfig" ]]; then + export KUBECONFIG="${SHARED_DIR}/kubeconfig" +fi + +POLL_INTERVAL=30 +TIMEOUT=600 +ELAPSED=0 + +function WaitFor () { + typeset description="$1" + shift + typeset checkFn="$1" + shift + + ELAPSED=0 + echo "[readiness] Waiting for: ${description}" + while true; do + if "${checkFn}" "$@"; then + echo "[readiness] OK: ${description}" + return 0 + fi + ELAPSED=$((ELAPSED + POLL_INTERVAL)) + if [[ ${ELAPSED} -ge ${TIMEOUT} ]]; then + echo "[readiness] TIMEOUT after ${TIMEOUT}s waiting for: ${description}" + return 1 + fi + echo "[readiness] ...retrying in ${POLL_INTERVAL}s (${ELAPSED}/${TIMEOUT}s)" + sleep "${POLL_INTERVAL}" + done + true +} + +function JsonLength () { + python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d.get('$1',[])))" +} + +# --------------------------------------------------------------------------- +# Namespace discovery via CRs (never hardcode) +# --------------------------------------------------------------------------- +function DiscoverCentralNs () { + CENTRAL_NS="$(oc get centrals.platform.stackrox.io --all-namespaces \ + -o jsonpath='{.items[0].metadata.namespace}' 2>/dev/null)" \ + && [[ -n "${CENTRAL_NS}" ]] +} + +function DiscoverScNs () { + SC_NS="$(oc get securedclusters.platform.stackrox.io --all-namespaces \ + -o jsonpath='{.items[0].metadata.namespace}' 2>/dev/null)" \ + && [[ -n "${SC_NS}" ]] +} + +CENTRAL_NS="" +SC_NS="" + +WaitFor "Central CR namespace discovery" DiscoverCentralNs +echo "[readiness] Central namespace discovered" + +WaitFor "SecuredCluster CR namespace discovery" DiscoverScNs +echo "[readiness] SecuredCluster namespace discovered" + +# --------------------------------------------------------------------------- +# Check 1: Central route exists +# --------------------------------------------------------------------------- +function CheckCentralRoute () { + oc get route central -n "${CENTRAL_NS}" -o jsonpath='{.spec.host}' 2>/dev/null +} + +WaitFor "Central route" CheckCentralRoute + +set +x +CENTRAL_URL="$(oc get route central -n "${CENTRAL_NS}" -o jsonpath='{.spec.host}')" +set -x +echo "[readiness] Central route discovered" + +# --------------------------------------------------------------------------- +# Extract ROX_ADMIN_PASSWORD before API checks +# --------------------------------------------------------------------------- +echo "[readiness] Extracting ROX_ADMIN_PASSWORD..." +ROX_ADMIN_PASSWORD="" +set +x +ROX_ADMIN_PASSWORD="$(oc get secret -n "${CENTRAL_NS}" central-htpasswd \ + -o jsonpath='{.data.password}' | base64 -d)" +set -x + +if [[ -z "${ROX_ADMIN_PASSWORD}" ]]; then + echo "[readiness] FATAL: could not extract ROX_ADMIN_PASSWORD" + exit 1 +fi +echo "[readiness] ROX_ADMIN_PASSWORD extracted successfully" + +# --------------------------------------------------------------------------- +# Check 2: Central API health (authenticated v1/metadata) +# --------------------------------------------------------------------------- +function CheckCentralApi () { + set +x + typeset httpCode="" + httpCode="$(curl -sk -o /dev/null -w '%{http_code}' \ + -u "admin:${ROX_ADMIN_PASSWORD}" \ + "https://${CENTRAL_URL}/v1/metadata" --max-time 10)" || { set -x; return 1; } + set -x + [[ "${httpCode}" == "200" ]] +} + +WaitFor "Central API health (v1/metadata)" CheckCentralApi + +# --------------------------------------------------------------------------- +# Check 3: At least 1 secured cluster connected +# --------------------------------------------------------------------------- +function CheckClustersConnected () { + set +x + typeset clusterCount="" + clusterCount="$(curl -sk -u "admin:${ROX_ADMIN_PASSWORD}" \ + "https://${CENTRAL_URL}/v1/clusters" --max-time 10 \ + | JsonLength clusters)" || { set -x; return 1; } + set -x + [[ "${clusterCount}" -ge 1 ]] +} + +WaitFor "secured cluster connected (v1/clusters)" CheckClustersConnected + +# --------------------------------------------------------------------------- +# Check 4: Sensor pods Running (detect OOMKilled) +# --------------------------------------------------------------------------- +function CheckSensorPods () { + typeset podCount="" + podCount="$(oc get pods -n "${SC_NS}" -l app=sensor \ + -o json 2>/dev/null | JsonLength items)" || return 1 + if [[ "${podCount}" -eq 0 ]]; then + echo "[readiness] no sensor pods found yet" + return 1 + fi + + typeset sensorJson="" + sensorJson="$(oc get pods -n "${SC_NS}" -l app=sensor -o json 2>/dev/null)" || return 1 + typeset oomContainers="" + if [[ -n "${sensorJson}" ]]; then + oomContainers="$(echo "${sensorJson}" | python3 -c " +import json,sys +d=json.load(sys.stdin) +for pod in d.get('items',[]): + for cs in pod.get('status',{}).get('containerStatuses',[]): + ls=cs.get('lastState',{}).get('terminated',{}) + if ls.get('reason')=='OOMKilled': + print(cs['name']) +")" + fi + if [[ -n "${oomContainers}" ]]; then + echo "[readiness] WARNING: OOMKilled detected in sensor containers: ${oomContainers}" + fi + + typeset podConditions="" + podConditions="$(oc get pods -n "${SC_NS}" -l app=sensor \ + -o jsonpath='{range .items[*]}{.metadata.name}{" "}{range .status.conditions[*]}{.type}={.status}{" "}{end}{"\n"}{end}' 2>/dev/null)" || return 1 + typeset notReady="" + notReady="$(echo "${podConditions}" | while IFS= read -r line; do + [[ -z "${line}" ]] && continue + if ! echo "${line}" | grep -q 'Ready=True'; then + echo "${line%% *}:NotReady" + fi + done)" + [[ -z "${notReady}" ]] +} + +WaitFor "sensor pods Running in ${SC_NS}" CheckSensorPods + +# --------------------------------------------------------------------------- +# Check 5: Default policies loaded (count > 80) +# --------------------------------------------------------------------------- +function CheckPoliciesLoaded () { + set +x + typeset policyCount="" + policyCount="$(curl -sk -u "admin:${ROX_ADMIN_PASSWORD}" \ + "https://${CENTRAL_URL}/v1/policies?query=" --max-time 10 \ + | JsonLength policies)" || { set -x; return 1; } + set -x + echo "[readiness] policy count: ${policyCount}" + [[ "${policyCount}" -gt 80 ]] +} + +WaitFor "default policies loaded (>80)" CheckPoliciesLoaded + +echo "[readiness] Writing connection details to SHARED_DIR..." + +set +x +echo "${ROX_ADMIN_PASSWORD}" > "${SHARED_DIR}/ROX_ADMIN_PASSWORD" +echo "${CENTRAL_URL}" > "${SHARED_DIR}/CENTRAL_URL" +set -x + +echo "${CENTRAL_NS}" > "${SHARED_DIR}/CENTRAL_NS" +echo "${SC_NS}" > "${SHARED_DIR}/SC_NS" + +echo "[readiness] All checks passed. ACS is ready for SMOKE tests." diff --git a/ci-operator/step-registry/stackrox/opp-readiness/stackrox-opp-readiness-ref.metadata.json b/ci-operator/step-registry/stackrox/opp-readiness/stackrox-opp-readiness-ref.metadata.json new file mode 100644 index 0000000000000..cb61d5b78e49a --- /dev/null +++ b/ci-operator/step-registry/stackrox/opp-readiness/stackrox-opp-readiness-ref.metadata.json @@ -0,0 +1,11 @@ +{ + "path": "stackrox/opp-readiness/stackrox-opp-readiness-ref.yaml", + "owners": { + "approvers": [ + "cspi-qe-ocp-lp" + ], + "reviewers": [ + "cspi-qe-ocp-lp" + ] + } +} \ No newline at end of file diff --git a/ci-operator/step-registry/stackrox/opp-readiness/stackrox-opp-readiness-ref.yaml b/ci-operator/step-registry/stackrox/opp-readiness/stackrox-opp-readiness-ref.yaml new file mode 100644 index 0000000000000..08a3b381984ad --- /dev/null +++ b/ci-operator/step-registry/stackrox/opp-readiness/stackrox-opp-readiness-ref.yaml @@ -0,0 +1,16 @@ +ref: + as: stackrox-opp-readiness + commands: stackrox-opp-readiness-commands.sh + resources: + requests: + cpu: 100m + memory: 200Mi + from: cli + timeout: 1h15m0s + documentation: |- + Verify ACS Central and SecuredCluster are operational before running + SMOKE tests. Discovers namespaces dynamically via Central and + SecuredCluster CRs, then polls Central API health, secured-cluster + connectivity, sensor pod status, and default policy count. Writes + ROX_ADMIN_PASSWORD, CENTRAL_URL, CENTRAL_NS, and SC_NS to SHARED_DIR + for downstream steps. diff --git a/ci-operator/step-registry/stackrox/opp-smoke/OWNERS b/ci-operator/step-registry/stackrox/opp-smoke/OWNERS new file mode 100644 index 0000000000000..0ce20c59fb95d --- /dev/null +++ b/ci-operator/step-registry/stackrox/opp-smoke/OWNERS @@ -0,0 +1,4 @@ +approvers: +- cspi-qe-ocp-lp +reviewers: +- cspi-qe-ocp-lp diff --git a/ci-operator/step-registry/stackrox/opp-smoke/stackrox-opp-smoke-commands.sh b/ci-operator/step-registry/stackrox/opp-smoke/stackrox-opp-smoke-commands.sh new file mode 100755 index 0000000000000..7e6852ff407b6 --- /dev/null +++ b/ci-operator/step-registry/stackrox/opp-smoke/stackrox-opp-smoke-commands.sh @@ -0,0 +1,105 @@ +#!/bin/bash +set -eux -o pipefail + +if [[ -f "${SHARED_DIR}/kubeconfig" ]]; then + export KUBECONFIG="${SHARED_DIR}/kubeconfig" +fi + +echo "[smoke] Reading connection details from SHARED_DIR..." + +set +x +CENTRAL_URL="$(cat "${SHARED_DIR}/CENTRAL_URL")" +ROX_ADMIN_PASSWORD="$(cat "${SHARED_DIR}/ROX_ADMIN_PASSWORD")" +set -x + +echo "[smoke] Connection details loaded from SHARED_DIR" + +STACKROX_REF="${STACKROX_REF:-master}" +SCANNER_REF="${SCANNER_REF:-master}" + +echo "[smoke] Sparse-cloning stackrox/stackrox..." +cd /tmp +rm -rf stackrox scanner +git clone --depth 1 --filter=blob:none --sparse --branch "${STACKROX_REF}" \ + https://github.com/stackrox/stackrox.git stackrox +cd stackrox +git sparse-checkout set qa-tests-backend/ proto/ + +echo "[smoke] Fetching scanner protos..." +git clone --depth 1 --filter=blob:none --sparse --branch "${SCANNER_REF}" \ + https://github.com/stackrox/scanner.git /tmp/scanner +cd /tmp/scanner +git sparse-checkout set proto/scanner +cp -r proto/scanner /tmp/stackrox/qa-tests-backend/src/main/proto/scanner +chmod -R u+w /tmp/stackrox/qa-tests-backend/src/main/proto/scanner + +echo "[smoke] Materializing proto sources (replace symlinks with copies)..." +cd /tmp/stackrox/qa-tests-backend/src/main/proto +for link in api internalapi storage test tools; do + if [[ -L "${link}" ]]; then + target="$(readlink -f "${link}")" + rm "${link}" + cp -r "${target}" "${link}" + fi +done + +echo "[smoke] Patching DEFAULT_CLUSTER_NAME to 'local-cluster'..." +sed -i 's/DEFAULT_CLUSTER_NAME = "remote"/DEFAULT_CLUSTER_NAME = "local-cluster"/' \ + /tmp/stackrox/qa-tests-backend/src/main/groovy/services/ClusterService.groovy +grep -q 'DEFAULT_CLUSTER_NAME = "local-cluster"' \ + /tmp/stackrox/qa-tests-backend/src/main/groovy/services/ClusterService.groovy \ + || { echo "[smoke] FATAL: DEFAULT_CLUSTER_NAME patch failed"; exit 1; } + +set +x +export API_HOSTNAME="${CENTRAL_URL}" +export API_PORT="443" +export ROX_USERNAME="admin" +export ROX_ADMIN_PASSWORD +export CLUSTER="OPENSHIFT" +export CI="true" +export POD_SECURITY_POLICIES="false" +export TEST_TARGET="smoke-test" +REGISTRY_USERNAME="$(cat /tmp/vault/stackrox-stackrox-e2e-tests/QUAY_RHACS_ENG_RO_USERNAME)" +export REGISTRY_USERNAME +REGISTRY_PASSWORD="$(cat /tmp/vault/stackrox-stackrox-e2e-tests/QUAY_RHACS_ENG_RO_PASSWORD)" +export REGISTRY_PASSWORD +if [[ -f /tmp/vault/stackrox-stackrox-e2e-tests/GOOGLE_CREDENTIALS_GCR_SCANNER_V2 ]]; then + GOOGLE_CREDENTIALS_GCR_SCANNER_V2="$(cat /tmp/vault/stackrox-stackrox-e2e-tests/GOOGLE_CREDENTIALS_GCR_SCANNER_V2)" + export GOOGLE_CREDENTIALS_GCR_SCANNER_V2 +fi +if [[ -f /tmp/vault/stackrox-stackrox-e2e-tests/GOOGLE_ARTIFACT_REGISTRY_SERVICE_ACCOUNT_V2 ]]; then + GOOGLE_ARTIFACT_REGISTRY_SERVICE_ACCOUNT_V2="$(cat /tmp/vault/stackrox-stackrox-e2e-tests/GOOGLE_ARTIFACT_REGISTRY_SERVICE_ACCOUNT_V2)" + export GOOGLE_ARTIFACT_REGISTRY_SERVICE_ACCOUNT_V2 +fi +set -x + +cd /tmp/stackrox/qa-tests-backend + +cat > /tmp/fix-proto-deps.gradle <<'INIT' +allprojects { + afterEvaluate { + tasks.matching { it.name == 'compileGroovy' }.configureEach { + dependsOn tasks.matching { it.name == 'generateProto' } + } + } +} +INIT + +echo "[smoke] Running testSMOKE..." +TEST_EXIT=0 +./gradlew testSMOKE --no-daemon --init-script /tmp/fix-proto-deps.gradle \ + -Dorg.gradle.jvmargs="-Xmx2g" || TEST_EXIT=$? + +echo "[smoke] Copying JUnit results to ARTIFACT_DIR..." +if [[ -d build/test-results/testSMOKE ]]; then + find build/test-results/testSMOKE -name '*.xml' -exec cp -v {} "${ARTIFACT_DIR}/" \; +fi + +if [[ -d build/reports/tests/testSMOKE ]]; then + mkdir -p "${ARTIFACT_DIR}/smoke-report" + find build/reports/tests/testSMOKE -mindepth 1 -maxdepth 1 \ + -exec cp -r {} "${ARTIFACT_DIR}/smoke-report/" \; +fi + +echo "[smoke] Test run finished with exit code: ${TEST_EXIT}" +exit "${TEST_EXIT}" diff --git a/ci-operator/step-registry/stackrox/opp-smoke/stackrox-opp-smoke-ref.metadata.json b/ci-operator/step-registry/stackrox/opp-smoke/stackrox-opp-smoke-ref.metadata.json new file mode 100644 index 0000000000000..0b518035376be --- /dev/null +++ b/ci-operator/step-registry/stackrox/opp-smoke/stackrox-opp-smoke-ref.metadata.json @@ -0,0 +1,11 @@ +{ + "path": "stackrox/opp-smoke/stackrox-opp-smoke-ref.yaml", + "owners": { + "approvers": [ + "cspi-qe-ocp-lp" + ], + "reviewers": [ + "cspi-qe-ocp-lp" + ] + } +} \ No newline at end of file diff --git a/ci-operator/step-registry/stackrox/opp-smoke/stackrox-opp-smoke-ref.yaml b/ci-operator/step-registry/stackrox/opp-smoke/stackrox-opp-smoke-ref.yaml new file mode 100644 index 0000000000000..74d11f0815aa9 --- /dev/null +++ b/ci-operator/step-registry/stackrox/opp-smoke/stackrox-opp-smoke-ref.yaml @@ -0,0 +1,20 @@ +ref: + as: stackrox-opp-smoke + commands: stackrox-opp-smoke-commands.sh + credentials: + - mount_path: /tmp/vault/stackrox-stackrox-e2e-tests + name: stackrox-stackrox-e2e-tests + namespace: test-credentials + resources: + requests: + cpu: 2000m + memory: 4Gi + from: acs-smoke-runner + timeout: 1h0m0s + documentation: |- + Run the ACS qa-tests-backend SMOKE suite against a live ACS + instance. Reads connection credentials from SHARED_DIR + (written by stackrox-opp-readiness). Sparse-clones the + stackrox/stackrox and stackrox/scanner repos, then executes + ./gradlew testSMOKE. JUnit XML results are copied to + ARTIFACT_DIR for Prow / Sippy consumption. From db63d8c783962c8e27603be7fa7a3eb3c90e228e Mon Sep 17 00:00:00 2001 From: Michael Pruitt Date: Fri, 14 Aug 2026 08:54:36 -0500 Subject: [PATCH 5/8] INTEROP-9265: Add ACM operator upgrade step Upgrades ACM via OLM subscription channel change and validates the operator reaches Succeeded phase: - Resolves target upgrade channel from available PackageManifest - Patches Subscription to new channel - Captures pre-patch InstallPlan and waits for a different ref after channel change (eliminates stale-ref race condition) - Waits for new CSV to reach Succeeded phase (skips iterations where currentCSV still matches pre-upgrade CSV) - Validates MCE co-upgrade completes - Runs hub health checks: MCH phase, policy propagator readiness, managed cluster availability - Writes acm-upgraded-version and acm-upgraded-channel to SHARED_DIR This step gates downstream product upgrades (ACS, ODF, Quay) in the OPP coordinated product upgrade workflow. Resolves: https://redhat.atlassian.net/browse/INTEROP-9265 --- .../interop/opp/product-upgrade/OWNERS | 3 + .../interop/opp/product-upgrade/acm/OWNERS | 3 + ...nterop-opp-product-upgrade-acm-commands.sh | 411 ++++++++++++++++++ ...-opp-product-upgrade-acm-ref.metadata.json | 11 + .../interop-opp-product-upgrade-acm-ref.yaml | 49 +++ 5 files changed, 477 insertions(+) create mode 100644 ci-operator/step-registry/interop/opp/product-upgrade/OWNERS create mode 100644 ci-operator/step-registry/interop/opp/product-upgrade/acm/OWNERS create mode 100755 ci-operator/step-registry/interop/opp/product-upgrade/acm/interop-opp-product-upgrade-acm-commands.sh create mode 100644 ci-operator/step-registry/interop/opp/product-upgrade/acm/interop-opp-product-upgrade-acm-ref.metadata.json create mode 100644 ci-operator/step-registry/interop/opp/product-upgrade/acm/interop-opp-product-upgrade-acm-ref.yaml diff --git a/ci-operator/step-registry/interop/opp/product-upgrade/OWNERS b/ci-operator/step-registry/interop/opp/product-upgrade/OWNERS new file mode 100644 index 0000000000000..41d144d3728a2 --- /dev/null +++ b/ci-operator/step-registry/interop/opp/product-upgrade/OWNERS @@ -0,0 +1,3 @@ +approvers: &owners +- cspi-qe-ocp-lp +reviewers: *owners diff --git a/ci-operator/step-registry/interop/opp/product-upgrade/acm/OWNERS b/ci-operator/step-registry/interop/opp/product-upgrade/acm/OWNERS new file mode 100644 index 0000000000000..41d144d3728a2 --- /dev/null +++ b/ci-operator/step-registry/interop/opp/product-upgrade/acm/OWNERS @@ -0,0 +1,3 @@ +approvers: &owners +- cspi-qe-ocp-lp +reviewers: *owners diff --git a/ci-operator/step-registry/interop/opp/product-upgrade/acm/interop-opp-product-upgrade-acm-commands.sh b/ci-operator/step-registry/interop/opp/product-upgrade/acm/interop-opp-product-upgrade-acm-commands.sh new file mode 100755 index 0000000000000..97dbc9e4fbce5 --- /dev/null +++ b/ci-operator/step-registry/interop/opp/product-upgrade/acm/interop-opp-product-upgrade-acm-commands.sh @@ -0,0 +1,411 @@ +#!/bin/bash +set -euxo pipefail +shopt -s inherit_errexit + +ACM_TARGET_CHANNEL="${ACM_TARGET_CHANNEL:-}" +ACM_UPGRADE_TIMEOUT="${ACM_UPGRADE_TIMEOUT:-30m}" +ACM_SUBSCRIPTION_NAME="${ACM_SUBSCRIPTION_NAME:-advanced-cluster-management}" +ACM_SUBSCRIPTION_NAMESPACE="${ACM_SUBSCRIPTION_NAMESPACE:-open-cluster-management}" + +ARTIFACT_DIR="${ARTIFACT_DIR:-/tmp/artifacts}" +mkdir -p "${ARTIFACT_DIR}" + +# shellcheck disable=SC2034 +typeset -i exitCode=0 + +function CollectDiagnostics () { + typeset artifactFile="${ARTIFACT_DIR}/acm-upgrade-diagnostics.txt" + { + printf '=== ACM Operator Upgrade Diagnostics ===\n\n' + printf '=== Subscription ===\n' + oc get subscription "${ACM_SUBSCRIPTION_NAME}" -n "${ACM_SUBSCRIPTION_NAMESPACE}" -o yaml 2>&1 || true + printf '\n=== CSVs in %s ===\n' "${ACM_SUBSCRIPTION_NAMESPACE}" + oc get csv -n "${ACM_SUBSCRIPTION_NAMESPACE}" 2>&1 || true + printf '\n=== InstallPlan ===\n' + oc get installplan -n "${ACM_SUBSCRIPTION_NAMESPACE}" 2>&1 || true + printf '\n=== MCE CSVs ===\n' + oc get csv -n multicluster-engine 2>&1 || true + printf '\n=== Pods not Ready ===\n' + oc get pods -n "${ACM_SUBSCRIPTION_NAMESPACE}" --field-selector=status.phase!=Running,status.phase!=Succeeded 2>&1 || true + oc get pods -n multicluster-engine --field-selector=status.phase!=Running,status.phase!=Succeeded 2>&1 || true + } > "${artifactFile}" + true +} + +trap '{( exitCode=$?; if (( exitCode != 0 )); then CollectDiagnostics; fi )}' EXIT + +function GetCurrentCsv () { + oc get subscription "${ACM_SUBSCRIPTION_NAME}" \ + -n "${ACM_SUBSCRIPTION_NAMESPACE}" \ + -o jsonpath='{.status.currentCSV}' || true +} + +function GetCsvPhase () { + typeset csvName="$1" + oc get csv "${csvName}" \ + -n "${ACM_SUBSCRIPTION_NAMESPACE}" \ + -o jsonpath='{.status.phase}' || true +} + +function GetInstalledVersion () { + typeset csvName + csvName="$(GetCurrentCsv)" + if [[ -z "${csvName}" ]]; then + return 1 + fi + oc get csv "${csvName}" \ + -n "${ACM_SUBSCRIPTION_NAMESPACE}" \ + -o jsonpath='{.spec.version}' || true +} + +function GetCurrentChannel () { + oc get subscription "${ACM_SUBSCRIPTION_NAME}" \ + -n "${ACM_SUBSCRIPTION_NAMESPACE}" \ + -o jsonpath='{.spec.channel}' || true +} + +function ResolveTargetChannel () { + if [[ -n "${ACM_TARGET_CHANNEL}" ]]; then + echo "${ACM_TARGET_CHANNEL}" + return 0 + fi + + typeset currentChannel + currentChannel="$(GetCurrentChannel)" + if [[ -z "${currentChannel}" ]]; then + echo >&2 "ERROR: Cannot determine current subscription channel" + return 3 + fi + + typeset catalogNamespace + catalogNamespace="$(oc get subscription "${ACM_SUBSCRIPTION_NAME}" \ + -n "${ACM_SUBSCRIPTION_NAMESPACE}" \ + -o jsonpath='{.spec.sourceNamespace}' || true)" + + typeset packageName + packageName="$(oc get subscription "${ACM_SUBSCRIPTION_NAME}" \ + -n "${ACM_SUBSCRIPTION_NAMESPACE}" \ + -o jsonpath='{.spec.name}' || true)" + + typeset channels + channels="$(oc get packagemanifest "${packageName}" \ + -n "${catalogNamespace}" \ + -o jsonpath='{.status.channels[*].name}' || true)" + + if [[ -z "${channels}" ]]; then + echo >&2 "ERROR: No channels found in packagemanifest for ${packageName}" + return 3 + fi + + typeset currentVersion nextChannel="" + currentVersion="$(echo "${currentChannel}" | grep -oE '[0-9]+\.[0-9]+' || true)" + + typeset -a channelList + read -ra channelList <<< "${channels}" + for ch in "${channelList[@]}"; do + typeset chVersion + chVersion="$(echo "${ch}" | grep -oE '[0-9]+\.[0-9]+' || true)" + if [[ -z "${chVersion}" ]]; then + continue + fi + if [[ -z "${currentVersion}" ]]; then + nextChannel="${ch}" + break + fi + typeset currentMajor currentMinor chMajor chMinor + currentMajor="${currentVersion%%.*}" + currentMinor="${currentVersion##*.}" + chMajor="${chVersion%%.*}" + chMinor="${chVersion##*.}" + + if (( chMajor > currentMajor )) || \ + (( chMajor == currentMajor && chMinor > currentMinor )); then + if [[ -z "${nextChannel}" ]]; then + nextChannel="${ch}" + else + typeset nextVersion nextMajor nextMinor + nextVersion="$(echo "${nextChannel}" | grep -oE '[0-9]+\.[0-9]+' || true)" + nextMajor="${nextVersion%%.*}" + nextMinor="${nextVersion##*.}" + if (( chMajor < nextMajor )) || \ + (( chMajor == nextMajor && chMinor < nextMinor )); then + nextChannel="${ch}" + fi + fi + fi + done + + if [[ -z "${nextChannel}" ]]; then + echo >&2 "ERROR: No upgrade channel found newer than ${currentChannel}" + return 3 + fi + + echo "${nextChannel}" + true +} + +function WaitForCsvSucceeded () { + typeset previousCsv="$1" + typeset timeoutSeconds + timeoutSeconds="$(ParseTimeout "${ACM_UPGRADE_TIMEOUT}")" + typeset startTime elapsed newCsv phase + startTime="$(date +%s)" + + while true; do + elapsed="$(( $(date +%s) - startTime ))" + if (( elapsed > timeoutSeconds )); then + echo >&2 "ERROR: Timeout (${ACM_UPGRADE_TIMEOUT}) waiting for CSV upgrade" + return 2 + fi + + newCsv="$(GetCurrentCsv)" + if [[ -z "${newCsv}" || "${newCsv}" == "${previousCsv}" ]]; then + sleep 10 + continue + fi + + phase="$(GetCsvPhase "${newCsv}")" + echo " CSV: ${newCsv} Phase: ${phase} (${elapsed}s elapsed)" + + case "${phase}" in + Succeeded) + return 0 + ;; + Failed) + echo >&2 "ERROR: CSV ${newCsv} entered Failed phase" + return 1 + ;; + *) + sleep 15 + ;; + esac + done +} + +function ParseTimeout () { + typeset input="$1" + typeset minutes=0 seconds=0 + if [[ "${input}" =~ ^([0-9]+)m$ ]]; then + minutes="${BASH_REMATCH[1]}" + elif [[ "${input}" =~ ^([0-9]+)s$ ]]; then + seconds="${BASH_REMATCH[1]}" + elif [[ "${input}" =~ ^([0-9]+)h$ ]]; then + minutes="$(( BASH_REMATCH[1] * 60 ))" + elif [[ "${input}" =~ ^([0-9]+)$ ]]; then + minutes="${input}" + else + echo >&2 "WARNING: Unrecognized timeout format '${input}'; defaulting to 30m" + minutes=30 + fi + echo "$(( minutes * 60 + seconds ))" + true +} + +function ValidateMceUpgrade () { + echo "Validating MCE (MultiCluster Engine) upgrade..." + typeset mceCsv + mceCsv="$(oc get csv -n multicluster-engine \ + -o jsonpath='{.items[?(@.spec.displayName=="multicluster engine for Kubernetes")].metadata.name}' \ + || true)" + + if [[ -z "${mceCsv}" ]]; then + mceCsv="$(oc get csv -n multicluster-engine \ + -l operators.coreos.com/multicluster-engine.multicluster-engine= \ + -o jsonpath='{.items[0].metadata.name}' || true)" + fi + + if [[ -z "${mceCsv}" ]]; then + echo "WARNING: MCE CSV not found; skipping MCE validation" + return 0 + fi + + typeset mcePhase + mcePhase="$(oc get csv "${mceCsv}" -n multicluster-engine \ + -o jsonpath='{.status.phase}' || true)" + + echo " MCE CSV: ${mceCsv} Phase: ${mcePhase}" + if [[ "${mcePhase}" != "Succeeded" ]]; then + echo "WARNING: MCE CSV phase is ${mcePhase}, not Succeeded" + typeset timeoutEnd + timeoutEnd="$(( $(date +%s) + 300 ))" + while (( $(date +%s) < timeoutEnd )); do + mcePhase="$(oc get csv "${mceCsv}" -n multicluster-engine \ + -o jsonpath='{.status.phase}' || true)" + if [[ "${mcePhase}" == "Succeeded" ]]; then + echo " MCE CSV reached Succeeded phase" + return 0 + fi + sleep 15 + done + echo >&2 "ERROR: MCE CSV did not reach Succeeded within 5 minutes" + return 1 + fi + return 0 +} + +function ValidateHubHealth () { + echo "Validating ACM hub health post-upgrade..." + + typeset mchStatus + mchStatus="$(oc get multiclusterhub -A \ + -o jsonpath='{.items[0].status.phase}' || true)" + echo " MultiClusterHub phase: ${mchStatus}" + + if [[ "${mchStatus}" != "Running" ]]; then + echo " Waiting for MCH to reach Running phase (timeout: 5m)..." + typeset timeoutEnd + timeoutEnd="$(( $(date +%s) + 300 ))" + while (( $(date +%s) < timeoutEnd )); do + mchStatus="$(oc get multiclusterhub -A \ + -o jsonpath='{.items[0].status.phase}' || true)" + if [[ "${mchStatus}" == "Running" ]]; then + break + fi + sleep 15 + done + if [[ "${mchStatus}" != "Running" ]]; then + echo >&2 "ERROR: MultiClusterHub did not reach Running phase" + return 1 + fi + fi + + echo " Checking policy propagator..." + typeset propagatorReady="" + typeset -i propTimeout=300 + typeset -i propStart + propStart="$(date +%s)" + while (( $(date +%s) - propStart < propTimeout )); do + propagatorReady="$(oc get pods -n "${ACM_SUBSCRIPTION_NAMESPACE}" \ + -l name=governance-policy-propagator \ + -o jsonpath='{.items[0].status.conditions[?(@.type=="Ready")].status}' \ + || true)" + if [[ "${propagatorReady}" == "True" ]]; then + break + fi + sleep 15 + done + echo " Policy propagator ready: ${propagatorReady}" + if [[ "${propagatorReady}" != "True" ]]; then + echo >&2 "ERROR: Policy propagator not ready after ${propTimeout}s" + return 1 + fi + + echo " Checking managed clusters..." + typeset clusterOutput="" + clusterOutput="$(oc get managedclusters --no-headers || true)" + typeset -i clusterCount=0 + clusterCount="$(echo "${clusterOutput}" | grep -c . || true)" + typeset availableOutput="" + availableOutput="$(oc get managedclusters \ + -o jsonpath='{.items[?(@.status.conditions[?(@.type=="ManagedClusterConditionAvailable")].status=="True")].metadata.name}' \ + || true)" + typeset -i availableCount=0 + availableCount="$(echo "${availableOutput}" | wc -w)" + echo " Managed clusters: ${availableCount}/${clusterCount} available" + + echo "ACM hub health validation complete" + return 0 +} + +# === Main === + +echo "=== ACM Operator Upgrade Step ===" +echo "Namespace: ${ACM_SUBSCRIPTION_NAMESPACE}" +echo "Subscription: ${ACM_SUBSCRIPTION_NAME}" +echo "Timeout: ${ACM_UPGRADE_TIMEOUT}" + +currentCsv="$(GetCurrentCsv)" +if [[ -z "${currentCsv}" ]]; then + echo >&2 "ERROR: No ACM subscription found or no currentCSV set" + exit 3 +fi + +currentVersion="$(GetInstalledVersion)" +currentChannel="$(GetCurrentChannel)" +echo "Current: CSV=${currentCsv} Version=${currentVersion} Channel=${currentChannel}" + +targetChannel="$(ResolveTargetChannel)" +echo "Target channel: ${targetChannel}" + +prePatchPlan="$(oc get subscription "${ACM_SUBSCRIPTION_NAME}" \ + -n "${ACM_SUBSCRIPTION_NAMESPACE}" \ + -o jsonpath='{.status.installPlanRef.name}' || true)" + +if [[ "${targetChannel}" == "${currentChannel}" ]]; then + echo "Already on target channel ${targetChannel}; checking if upgrade is available..." + if [[ -z "${prePatchPlan}" ]]; then + echo "No pending upgrade on current channel; nothing to do" + exit 0 + fi + planPhase="$(oc get installplan "${prePatchPlan}" \ + -n "${ACM_SUBSCRIPTION_NAMESPACE}" \ + -o jsonpath='{.status.phase}' || true)" + if [[ "${planPhase}" == "Complete" ]]; then + echo "InstallPlan ${prePatchPlan} already complete; no pending upgrade" + exit 0 + fi + installPlan="${prePatchPlan}" +else + echo "Patching subscription channel: ${currentChannel} -> ${targetChannel}" + oc patch subscription "${ACM_SUBSCRIPTION_NAME}" \ + -n "${ACM_SUBSCRIPTION_NAMESPACE}" \ + --type merge \ + -p "{\"spec\":{\"channel\":\"${targetChannel}\"}}" + + echo "Waiting for new InstallPlan (pre-patch ref: ${prePatchPlan:-none})..." + sleep 10 + + installPlan="" + for _ in {1..18}; do + installPlan="$(oc get subscription "${ACM_SUBSCRIPTION_NAME}" \ + -n "${ACM_SUBSCRIPTION_NAMESPACE}" \ + -o jsonpath='{.status.installPlanRef.name}' || true)" + if [[ -n "${installPlan}" && "${installPlan}" != "${prePatchPlan}" ]]; then + break + fi + installPlan="" + sleep 10 + done + + if [[ -z "${installPlan}" ]]; then + echo >&2 "ERROR: No new InstallPlan appeared after channel change (waited 3m)" + exit 2 + fi +fi + +echo "InstallPlan: ${installPlan}" +localApproval="$(oc get installplan "${installPlan}" \ + -n "${ACM_SUBSCRIPTION_NAMESPACE}" \ + -o jsonpath='{.spec.approval}' || true)" +if [[ "${localApproval}" == "Manual" ]]; then + echo "Approving manual InstallPlan..." + oc patch installplan "${installPlan}" \ + -n "${ACM_SUBSCRIPTION_NAMESPACE}" \ + --type merge \ + -p '{"spec":{"approved":true}}' +fi + +echo "Waiting for ACM CSV to reach Succeeded phase..." +WaitForCsvSucceeded "${currentCsv}" +newCsv="$(GetCurrentCsv)" +newVersion="$(GetInstalledVersion)" +echo "Upgrade complete: ${currentVersion} -> ${newVersion} (CSV: ${newCsv})" + +ValidateMceUpgrade +ValidateHubHealth + +{ + printf '=== ACM Operator Upgrade Summary ===\n' + printf 'Previous: %s (%s)\n' "${currentVersion}" "${currentChannel}" + printf 'Current: %s (%s)\n' "${newVersion}" "${targetChannel}" + printf 'CSV: %s\n' "${newCsv}" + printf 'Status: SUCCESS\n' +} > "${ARTIFACT_DIR}/acm-upgrade-summary.txt" + +if [[ -n "${SHARED_DIR:-}" ]]; then + echo "${newVersion}" > "${SHARED_DIR}/acm-upgraded-version" + echo "${targetChannel}" > "${SHARED_DIR}/acm-upgraded-channel" +fi + +echo "=== ACM Operator Upgrade: SUCCESS ===" +true diff --git a/ci-operator/step-registry/interop/opp/product-upgrade/acm/interop-opp-product-upgrade-acm-ref.metadata.json b/ci-operator/step-registry/interop/opp/product-upgrade/acm/interop-opp-product-upgrade-acm-ref.metadata.json new file mode 100644 index 0000000000000..2581637b16588 --- /dev/null +++ b/ci-operator/step-registry/interop/opp/product-upgrade/acm/interop-opp-product-upgrade-acm-ref.metadata.json @@ -0,0 +1,11 @@ +{ + "path": "interop/opp/product-upgrade/acm/interop-opp-product-upgrade-acm-ref.yaml", + "owners": { + "approvers": [ + "cspi-qe-ocp-lp" + ], + "reviewers": [ + "cspi-qe-ocp-lp" + ] + } +} \ No newline at end of file diff --git a/ci-operator/step-registry/interop/opp/product-upgrade/acm/interop-opp-product-upgrade-acm-ref.yaml b/ci-operator/step-registry/interop/opp/product-upgrade/acm/interop-opp-product-upgrade-acm-ref.yaml new file mode 100644 index 0000000000000..53bc8d83a2808 --- /dev/null +++ b/ci-operator/step-registry/interop/opp/product-upgrade/acm/interop-opp-product-upgrade-acm-ref.yaml @@ -0,0 +1,49 @@ +ref: + as: interop-opp-product-upgrade-acm + from: cli + grace_period: 5m + commands: interop-opp-product-upgrade-acm-commands.sh + timeout: 45m + resources: + requests: + cpu: 100m + memory: 100Mi + env: + - name: ACM_TARGET_CHANNEL + default: "" + documentation: |- + Target subscription channel for ACM operator upgrade (e.g. release-2.13). + When empty, the step determines the next available channel automatically + by querying the catalog for channels newer than the currently installed version. + - name: ACM_UPGRADE_TIMEOUT + default: "30m" + documentation: |- + Maximum time to wait for the ACM operator upgrade to complete. + Covers CSV transition from Replacing to Succeeded phase. + - name: ACM_SUBSCRIPTION_NAME + default: "advanced-cluster-management" + documentation: |- + Name of the ACM Subscription resource in the target namespace. + - name: ACM_SUBSCRIPTION_NAMESPACE + default: "open-cluster-management" + documentation: |- + Namespace containing the ACM Subscription and CSV resources. + documentation: |- + Upgrades the ACM (Advanced Cluster Management) operator via OLM subscription + channel change and validates the upgrade completes successfully. + + The step performs: + 1. Identifies the currently installed ACM version via CSV + 2. Patches the Subscription to the target channel (or next available) + 3. Waits for the new CSV to reach Succeeded phase + 4. Validates MCE (MultiCluster Engine) co-upgrade completion + 5. Confirms hub connectivity and policy engine health + + This step gates downstream product upgrades (ACS, ODF, Quay) in the + OPP coordinated product upgrade workflow. + + Exit codes: + 0 - upgrade successful, all health checks passed + 1 - upgrade failed (CSV did not reach Succeeded) + 2 - timeout waiting for upgrade + 3 - precondition failure (no subscription found, no upgrade path) From 034d67460f6faa5278d5cb525f168280229200ee Mon Sep 17 00:00:00 2001 From: Michael Pruitt Date: Fri, 14 Aug 2026 08:54:51 -0500 Subject: [PATCH 6/8] Wire new steps into OPP CI configs and add FIPS variant Config changes across all OPP variants (4.22, 5.0, 5.1, upgrade): ACM 2.17 (INTEROP-9406): - Update operator channel from release-2.16 to release-2.17 - Bump ACM QE test image references from 2.16 to 2.17 Remove non-interop steps (INTEROP-9415): - Drop acm-tests-grc, acm-tests-alc, openshift-extended-test refs - Remove unused base images (console-e2e, acmqe-grc-test, tests-private) - Remove unused env vars (MAP_TESTS, TEST_IMPORTANCE, TEST_SCENARIOS, ODF_VERSION_MAJOR_MINOR, DISABLE_ENVIRONMENT_CHECKER) Wire new test steps: - Replace interop-tests-ocs-tests with interop-opp-odf-health - Replace quay-tests-quay-interop-test with interop-tests-opp-quay-smoke - Add stackrox-opp-readiness and stackrox-opp-smoke refs - Add interop-opp-product-upgrade-acm to upgrade configs - Add acs-smoke-runner image (UBI9/openjdk-17 + git + oc) pinned to stable-4.22 Upgrade configs (INTEROP-9337): - Fix rhacs-operator: use openshift-operators (AllNamespaces) - Re-add rhacs-operator to 5.0 upgrade, set OPP_OPERATORS explicitly - Add ACM_SUBSCRIPTION_NAMESPACE=ocm override FIPS variant (INTEROP-9361): - New ocp4.22-fips config mirroring AWS config with FIPS_ENABLED=true - Jira routing to INTEROP-9104 epic with fips label --- ...-policy-collection-main__ocp4.22-fips.yaml | 138 ++++++++++++++++++ ...licy-collection-main__ocp4.22-upgrade.yaml | 6 +- ...stron-policy-collection-main__ocp4.22.yaml | 49 +++---- ...olicy-collection-main__ocp5.0-upgrade.yaml | 4 + ...ostron-policy-collection-main__ocp5.0.yaml | 32 +--- ...ostron-policy-collection-main__ocp5.1.yaml | 22 +-- 6 files changed, 170 insertions(+), 81 deletions(-) create mode 100644 ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp4.22-fips.yaml diff --git a/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp4.22-fips.yaml b/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp4.22-fips.yaml new file mode 100644 index 0000000000000..1221f3a80be7a --- /dev/null +++ b/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp4.22-fips.yaml @@ -0,0 +1,138 @@ +base_images: + acmqe-grc-test: + name: "2.17" + namespace: acm-qe + tag: acmqe-grc-test + clc-ui-e2e: + name: "2.17" + namespace: acm-qe + tag: clc-ui-e2e + cli: + name: "4.22" + namespace: ocp + tag: cli + fetch-managed-clusters: + name: autotest + namespace: acm-qe + tag: fetch-managed-clusters + multicluster-observability-operator-opp: + name: "2.17" + namespace: acm-qe + tag: multicluster-observability-operator-opp + tests-private: + name: tests-private + namespace: ci + tag: "4.22" + upi-installer: + name: "4.22" + namespace: ocp + tag: upi-installer +build_root: + image_stream_tag: + name: release + namespace: openshift + tag: rhel-9-release-golang-1.24-openshift-4.22 +images: + items: + - dockerfile_literal: | + FROM this-is-ignored + RUN dnf install -y git python39 + from: cli + optional: true + to: cli-with-git + - dockerfile_literal: | + FROM registry.access.redhat.com/ubi9/openjdk-17:1.21 + USER root + RUN microdnf install -y git gzip && microdnf clean all + RUN cd /tmp \ + && curl -sLO https://mirror.openshift.com/pub/openshift-v4/clients/ocp/stable-4.22/openshift-client-linux.tar.gz \ + && curl -sL https://mirror.openshift.com/pub/openshift-v4/clients/ocp/stable-4.22/sha256sum.txt | grep openshift-client-linux.tar.gz | sha256sum -c - \ + && tar xzf openshift-client-linux.tar.gz -C /usr/local/bin oc kubectl \ + && rm -f openshift-client-linux.tar.gz + USER 1001 + to: acs-smoke-runner +releases: + latest: + candidate: + product: ocp + stream: nightly + version: "4.22" +resources: + '*': + requests: + cpu: 100m + memory: 200Mi +tests: +- as: interop-opp-aws + capabilities: + - intranet + cron: 0 23 31 2 * + reporter_config: + channel: '#opp-discussion' + job_states_to_report: + - success + - failure + - error + report_template: '{{if eq .Status.State "success"}} :slack-green: Job *{{.Spec.Job}}* + ended with *{{.Status.State}}*. <{{.Status.URL}}|View logs> {{else}} :failed: + Job *{{.Spec.Job}}* ended with *{{.Status.State}}*. <{{.Status.URL}}|View logs> + {{end}}' + steps: + allow_best_effort_post_steps: true + cluster_profile: aws-cspi-qe + env: + BASE_DOMAIN: cspilp.interop.ccitredhat.com + COMPUTE_NODE_REPLICAS: "6" + COMPUTE_NODE_TYPE: m6a.2xlarge + CONTROL_PLANE_INSTANCE_TYPE: m6a.2xlarge + DR__RP__CR_COMP_NAME: lp-interop--OPP + FIPS_ENABLED: "true" + FIREWATCH_CONFIG_FILE_PATH: https://raw.githubusercontent.com/CSPI-QE/cspi-utils/refs/heads/main/firewatch-base-configs/opp/lp-interop-aws.json + FIREWATCH_DEFAULT_JIRA_ADDITIONAL_LABELS: '["4.22-lp","opp-aws-lp","opp-lp","fips"]' + FIREWATCH_DEFAULT_JIRA_AFFECTS_VERSION: 5.0.0 + FIREWATCH_DEFAULT_JIRA_ASSIGNEE: mpruitt@redhat.com + FIREWATCH_DEFAULT_JIRA_EPIC: INTEROP-9104 + FIREWATCH_DEFAULT_JIRA_PROJECT: LPINTEROP + FIREWATCH_FAIL_WITH_TEST_FAILURES: "true" + IGNORE_SECONDARY_POLICIES: "true" + OPERATORS: | + [ + {"name": "advanced-cluster-management", "source": "redhat-operators", "channel": "release-2.17", "install_namespace": "ocm", "target_namespaces": "ocm", "operator_group": "acm-operator-group"} + ] + QUAY_OPERATOR_CHANNEL: stable-3.17 + ZONES_COUNT: "3" + post: + - ref: acm-fetch-operator-versions + - ref: acm-must-gather + - ref: acm-inspector + - ref: acm-tests-clc-destroy + - ref: gather-aws-console + - chain: ipi-deprovision + - ref: mpiit-data-router-reporter + - ref: firewatch-report-issues + pre: + - ref: ipi-conf + - ref: ipi-conf-telemetry + - ref: ipi-conf-aws-custom-az + - ref: ipi-conf-aws + - ref: ipi-install-monitoringpvc + - chain: ipi-install + test: + - ref: install-operators + - ref: acm-mch + - ref: acm-policies-openshift-plus-setup + - ref: acm-policies-openshift-plus + - chain: cucushift-installer-check-cluster-health + - ref: stackrox-opp-readiness + - ref: stackrox-opp-smoke + - ref: acm-tests-clc-create + - ref: acm-fetch-managed-clusters + - ref: acm-opp-app + - ref: interop-opp-odf-health + - ref: interop-tests-opp-quay-smoke + - ref: acm-tests-observability +zz_generated_metadata: + branch: main + org: stolostron + repo: policy-collection + variant: ocp4.22-fips diff --git a/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp4.22-upgrade.yaml b/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp4.22-upgrade.yaml index a8e4a9187d0b1..677a66f43b706 100644 --- a/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp4.22-upgrade.yaml +++ b/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp4.22-upgrade.yaml @@ -44,6 +44,7 @@ tests: dependencies: OPENSHIFT_INSTALL_RELEASE_IMAGE_OVERRIDE: release:initial env: + ACM_SUBSCRIPTION_NAMESPACE: ocm BASE_DOMAIN: cspilp.interop.ccitredhat.com COMPUTE_NODE_REPLICAS: "6" COMPUTE_NODE_TYPE: m6a.2xlarge @@ -56,8 +57,8 @@ tests: FIREWATCH_FAIL_WITH_TEST_FAILURES: "true" OPERATORS: | [ - {"name": "advanced-cluster-management", "source": "redhat-operators", "channel": "release-2.16", "install_namespace": "ocm", "target_namespaces": "ocm", "operator_group": "acm-operator-group"}, - {"name": "rhacs-operator", "source": "redhat-operators", "channel": "stable", "install_namespace": "rhacs-operator", "target_namespaces": "rhacs-operator"}, + {"name": "advanced-cluster-management", "source": "redhat-operators", "channel": "release-2.17", "install_namespace": "ocm", "target_namespaces": "ocm", "operator_group": "acm-operator-group"}, + {"name": "rhacs-operator", "source": "redhat-operators", "channel": "stable", "install_namespace": "openshift-operators"}, {"name": "odf-operator", "source": "redhat-operators", "channel": "stable-4.21", "install_namespace": "openshift-storage", "target_namespaces": "openshift-storage"}, {"name": "quay-operator", "source": "redhat-operators", "channel": "stable-3.17", "install_namespace": "openshift-operators"} ] @@ -79,6 +80,7 @@ tests: - ref: interop-opp-preflight - ref: interop-opp-upgrade - ref: cucushift-upgrade-healthcheck + - ref: interop-opp-product-upgrade-acm - ref: interop-opp-smoke zz_generated_metadata: branch: main diff --git a/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp4.22.yaml b/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp4.22.yaml index 7c648df07abcf..c3a92c8259122 100644 --- a/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp4.22.yaml +++ b/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp4.22.yaml @@ -1,20 +1,16 @@ base_images: acmqe-grc-test: - name: "2.16" + name: "2.17" namespace: acm-qe tag: acmqe-grc-test clc-ui-e2e: - name: "2.16" + name: "2.17" namespace: acm-qe tag: clc-ui-e2e cli: name: "4.22" namespace: ocp tag: cli - console-e2e: - name: main - namespace: acm-qe - tag: console-e2e fetch-managed-clusters: name: autotest namespace: acm-qe @@ -23,10 +19,6 @@ base_images: name: "2.17" namespace: acm-qe tag: multicluster-observability-operator-opp - ocs-ci-tests: - name: ocs-ci-container - namespace: ci - tag: stable tests-private: name: tests-private namespace: ci @@ -48,6 +40,17 @@ images: from: cli optional: true to: cli-with-git + - dockerfile_literal: | + FROM registry.access.redhat.com/ubi9/openjdk-17:1.21 + USER root + RUN microdnf install -y git gzip && microdnf clean all + RUN cd /tmp \ + && curl -sLO https://mirror.openshift.com/pub/openshift-v4/clients/ocp/stable-4.22/openshift-client-linux.tar.gz \ + && curl -sL https://mirror.openshift.com/pub/openshift-v4/clients/ocp/stable-4.22/sha256sum.txt | grep openshift-client-linux.tar.gz | sha256sum -c - \ + && tar xzf openshift-client-linux.tar.gz -C /usr/local/bin oc kubectl \ + && rm -f openshift-client-linux.tar.gz + USER 1001 + to: acs-smoke-runner releases: latest: candidate: @@ -90,15 +93,11 @@ tests: FIREWATCH_DEFAULT_JIRA_PROJECT: LPINTEROP FIREWATCH_FAIL_WITH_TEST_FAILURES: "true" IGNORE_SECONDARY_POLICIES: "true" - MAP_TESTS: "true" - ODF_VERSION_MAJOR_MINOR: "4.21" OPERATORS: | [ - {"name": "advanced-cluster-management", "source": "redhat-operators", "channel": "release-2.16", "install_namespace": "ocm", "target_namespaces": "ocm", "operator_group": "acm-operator-group"} + {"name": "advanced-cluster-management", "source": "redhat-operators", "channel": "release-2.17", "install_namespace": "ocm", "target_namespaces": "ocm", "operator_group": "acm-operator-group"} ] QUAY_OPERATOR_CHANNEL: stable-3.17 - TEST_IMPORTANCE: LEVEL0 - TEST_SCENARIOS: Cluster_Observability ZONES_COUNT: "3" post: - ref: acm-fetch-operator-versions @@ -122,15 +121,14 @@ tests: - ref: acm-policies-openshift-plus-setup - ref: acm-policies-openshift-plus - chain: cucushift-installer-check-cluster-health + - ref: stackrox-opp-readiness + - ref: stackrox-opp-smoke - ref: acm-tests-clc-create - ref: acm-fetch-managed-clusters - ref: acm-opp-app - - ref: interop-tests-ocs-tests - - ref: quay-tests-quay-interop-test + - ref: interop-opp-odf-health + - ref: interop-tests-opp-quay-smoke - ref: acm-tests-observability - - ref: acm-tests-grc - - ref: acm-tests-alc - - ref: openshift-extended-test - as: interop-opp-vsphere capabilities: - intranet @@ -150,8 +148,6 @@ tests: cluster_profile: vsphere-connected-2 env: COMPUTE_NODE_REPLICAS: "6" - DISABLE_ENVIRONMENT_CHECKER: "true" - DR__RP__CR_COMP_NAME: lp-interop--OPP FIREWATCH_CONFIG_FILE_PATH: https://raw.githubusercontent.com/CSPI-QE/cspi-utils/refs/heads/main/firewatch-base-configs/opp/lp-interop-vsphere.json FIREWATCH_DEFAULT_JIRA_ADDITIONAL_LABELS: '["4.22-lp","opp-vsphere-lp","opp-lp"]' FIREWATCH_DEFAULT_JIRA_ASSIGNEE: ftan@redhat.com @@ -159,18 +155,14 @@ tests: FIREWATCH_DEFAULT_JIRA_PROJECT: LPINTEROP FIREWATCH_FAIL_WITH_TEST_FAILURES: "true" IGNORE_SECONDARY_POLICIES: "true" - MAP_TESTS: "true" - ODF_VERSION_MAJOR_MINOR: "4.21" OPENSHIFT_REQUIRED_CORES: "72" OPENSHIFT_REQUIRED_MEMORY: "288" OPERATORS: | [ - {"name": "advanced-cluster-management", "source": "redhat-operators", "channel": "release-2.16", "install_namespace": "ocm", "target_namespaces": "ocm", "operator_group": "acm-operator-group"} + {"name": "advanced-cluster-management", "source": "redhat-operators", "channel": "release-2.17", "install_namespace": "ocm", "target_namespaces": "ocm", "operator_group": "acm-operator-group"} ] QUAY_OPERATOR_CHANNEL: stable-3.17 SIZE_VARIANT: large - TEST_IMPORTANCE: LEVEL0 - TEST_SCENARIOS: Cluster_Observability post: - ref: acm-fetch-operator-versions - ref: acm-must-gather @@ -184,10 +176,9 @@ tests: - ref: acm-policies-openshift-plus-setup - ref: acm-policies-openshift-plus - chain: cucushift-installer-check-cluster-health - - ref: interop-tests-ocs-tests + - ref: interop-opp-odf-health - ref: acm-tests-observability - ref: acm-opp-app - - ref: openshift-extended-test workflow: acm-ipi-vsphere zz_generated_metadata: branch: main diff --git a/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp5.0-upgrade.yaml b/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp5.0-upgrade.yaml index 6573ee6e9e007..2194c18167845 100644 --- a/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp5.0-upgrade.yaml +++ b/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp5.0-upgrade.yaml @@ -44,6 +44,7 @@ tests: dependencies: OPENSHIFT_INSTALL_RELEASE_IMAGE_OVERRIDE: release:initial env: + ACM_SUBSCRIPTION_NAMESPACE: ocm BASE_DOMAIN: cspilp.interop.ccitredhat.com COMPUTE_NODE_REPLICAS: "6" COMPUTE_NODE_TYPE: m6a.2xlarge @@ -57,8 +58,10 @@ tests: OPERATORS: | [ {"name": "advanced-cluster-management", "source": "redhat-operators", "channel": "release-2.17", "install_namespace": "ocm", "target_namespaces": "ocm", "operator_group": "acm-operator-group"}, + {"name": "rhacs-operator", "source": "redhat-operators", "channel": "stable", "install_namespace": "openshift-operators"}, {"name": "quay-operator", "source": "redhat-operators", "channel": "stable-3.17", "install_namespace": "openshift-operators"} ] + OPP_OPERATORS: advanced-cluster-management,rhacs-operator,quay-operator ZONES_COUNT: "3" post: - ref: gather-aws-console @@ -77,6 +80,7 @@ tests: - ref: interop-opp-preflight - ref: interop-opp-upgrade - ref: cucushift-upgrade-healthcheck + - ref: interop-opp-product-upgrade-acm - ref: interop-opp-smoke zz_generated_metadata: branch: main diff --git a/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp5.0.yaml b/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp5.0.yaml index 2efc0cc8ee622..e31b7716529a3 100644 --- a/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp5.0.yaml +++ b/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp5.0.yaml @@ -1,8 +1,4 @@ base_images: - acmqe-grc-test: - name: "2.17" - namespace: acm-qe - tag: acmqe-grc-test clc-ui-e2e: name: "2.17" namespace: acm-qe @@ -11,10 +7,6 @@ base_images: name: "5.0" namespace: ocp tag: cli - console-e2e: - name: main - namespace: acm-qe - tag: console-e2e fetch-managed-clusters: name: autotest namespace: acm-qe @@ -23,10 +15,6 @@ base_images: name: "2.17" namespace: acm-qe tag: multicluster-observability-operator-opp - ocs-ci-tests: - name: ocs-ci-container - namespace: ci - tag: stable tests-private: name: tests-private namespace: ci @@ -89,14 +77,10 @@ tests: FIREWATCH_DEFAULT_JIRA_EPIC: INTEROP-9323 FIREWATCH_DEFAULT_JIRA_PROJECT: LPINTEROP FIREWATCH_FAIL_WITH_TEST_FAILURES: "true" - MAP_TESTS: "true" - ODF_VERSION_MAJOR_MINOR: "5.0" OPERATORS: | [ {"name": "advanced-cluster-management", "source": "redhat-operators", "channel": "release-2.17", "install_namespace": "ocm", "target_namespaces": "ocm", "operator_group": "acm-operator-group"} ] - TEST_IMPORTANCE: LEVEL0 - TEST_SCENARIOS: Cluster_Observability ZONES_COUNT: "3" post: - ref: acm-fetch-operator-versions @@ -123,12 +107,9 @@ tests: - ref: acm-tests-clc-create - ref: acm-fetch-managed-clusters - ref: acm-opp-app - - ref: interop-tests-ocs-tests - - ref: quay-tests-quay-interop-test + - ref: interop-opp-odf-health + - ref: interop-tests-opp-quay-smoke - ref: acm-tests-observability - - ref: acm-tests-grc - - ref: acm-tests-alc - - ref: openshift-extended-test - as: interop-opp-vsphere capabilities: - intranet @@ -148,16 +129,12 @@ tests: cluster_profile: vsphere-connected-2 env: COMPUTE_NODE_REPLICAS: "6" - DISABLE_ENVIRONMENT_CHECKER: "true" - DR__RP__CR_COMP_NAME: lp-interop--OPP FIREWATCH_CONFIG_FILE_PATH: https://raw.githubusercontent.com/CSPI-QE/cspi-utils/refs/heads/main/firewatch-base-configs/opp/lp-interop-vsphere.json FIREWATCH_DEFAULT_JIRA_ADDITIONAL_LABELS: '["5.0-lp","opp-vsphere-lp","opp-lp"]' FIREWATCH_DEFAULT_JIRA_ASSIGNEE: mpruitt@redhat.com FIREWATCH_DEFAULT_JIRA_EPIC: INTEROP-9323 FIREWATCH_DEFAULT_JIRA_PROJECT: LPINTEROP FIREWATCH_FAIL_WITH_TEST_FAILURES: "true" - MAP_TESTS: "true" - ODF_VERSION_MAJOR_MINOR: "5.0" OPENSHIFT_REQUIRED_CORES: "72" OPENSHIFT_REQUIRED_MEMORY: "288" OPERATORS: | @@ -165,8 +142,6 @@ tests: {"name": "advanced-cluster-management", "source": "redhat-operators", "channel": "release-2.17", "install_namespace": "ocm", "target_namespaces": "ocm", "operator_group": "acm-operator-group"} ] SIZE_VARIANT: large - TEST_IMPORTANCE: LEVEL0 - TEST_SCENARIOS: Cluster_Observability post: - ref: acm-fetch-operator-versions - ref: acm-must-gather @@ -180,10 +155,9 @@ tests: - ref: acm-policies-openshift-plus-setup - ref: acm-policies-openshift-plus - chain: cucushift-installer-check-cluster-health - - ref: interop-tests-ocs-tests + - ref: interop-opp-odf-health - ref: acm-tests-observability - ref: acm-opp-app - - ref: openshift-extended-test workflow: acm-ipi-vsphere zz_generated_metadata: branch: main diff --git a/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp5.1.yaml b/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp5.1.yaml index 84e618d000c94..8598841060b45 100644 --- a/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp5.1.yaml +++ b/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp5.1.yaml @@ -1,8 +1,4 @@ base_images: - acmqe-grc-test: - name: "2.17" - namespace: acm-qe - tag: acmqe-grc-test clc-ui-e2e: name: "2.17" namespace: acm-qe @@ -11,10 +7,6 @@ base_images: name: "5.1" namespace: ocp tag: cli - console-e2e: - name: main - namespace: acm-qe - tag: console-e2e fetch-managed-clusters: name: autotest namespace: acm-qe @@ -27,10 +19,6 @@ base_images: name: ocs-ci-container namespace: ci tag: stable - tests-private: - name: tests-private - namespace: ci - tag: "5.1" upi-installer: name: "5.1" namespace: ocp @@ -93,8 +81,6 @@ tests: [ {"name": "advanced-cluster-management", "source": "redhat-operators", "channel": "release-2.17", "install_namespace": "ocm", "target_namespaces": "ocm", "operator_group": "acm-operator-group"} ] - TEST_IMPORTANCE: LEVEL0 - TEST_SCENARIOS: Cluster_Observability ZONES_COUNT: "3" post: - ref: acm-fetch-operator-versions @@ -121,11 +107,8 @@ tests: - ref: acm-fetch-managed-clusters - ref: acm-opp-app - ref: interop-tests-ocs-tests - - ref: quay-tests-quay-interop-test + - ref: interop-tests-opp-quay-smoke - ref: acm-tests-observability - - ref: acm-tests-grc - - ref: acm-tests-alc - - ref: openshift-extended-test - as: interop-opp-vsphere capabilities: - intranet @@ -160,8 +143,6 @@ tests: {"name": "advanced-cluster-management", "source": "redhat-operators", "channel": "release-2.17", "install_namespace": "ocm", "target_namespaces": "ocm", "operator_group": "acm-operator-group"} ] SIZE_VARIANT: large - TEST_IMPORTANCE: LEVEL0 - TEST_SCENARIOS: Cluster_Observability test: - ref: install-operators - ref: acm-mch @@ -171,7 +152,6 @@ tests: - ref: interop-tests-ocs-tests - ref: acm-tests-observability - ref: acm-opp-app - - ref: openshift-extended-test workflow: acm-ipi-vsphere zz_generated_metadata: branch: main From 0e073fd728ba991d66fa6186ee30fc7fe205ee22 Mon Sep 17 00:00:00 2001 From: Michael Pruitt Date: Fri, 14 Aug 2026 08:54:57 -0500 Subject: [PATCH 7/8] Regenerate Prow job files via make update Generated periodics and presubmits YAML for new FIPS variant job and updated step references. --- ...tron-policy-collection-main-periodics.yaml | 95 +++++++++++++++++++ ...ron-policy-collection-main-presubmits.yaml | 59 ++++++++++++ 2 files changed, 154 insertions(+) diff --git a/ci-operator/jobs/stolostron/policy-collection/stolostron-policy-collection-main-periodics.yaml b/ci-operator/jobs/stolostron/policy-collection/stolostron-policy-collection-main-periodics.yaml index c60bc3013326b..5a4f4114e058f 100644 --- a/ci-operator/jobs/stolostron/policy-collection/stolostron-policy-collection-main-periodics.yaml +++ b/ci-operator/jobs/stolostron/policy-collection/stolostron-policy-collection-main-periodics.yaml @@ -1,4 +1,99 @@ periodics: +- agent: kubernetes + cluster: build03 + cron: 0 23 31 2 * + decorate: true + decoration_config: + skip_cloning: true + extra_refs: + - base_ref: main + org: stolostron + repo: policy-collection + labels: + capability/intranet: intranet + ci-operator.openshift.io/cloud: aws + ci-operator.openshift.io/cloud-cluster-profile: aws-cspi-qe + ci-operator.openshift.io/variant: ocp4.22-fips + ci.openshift.io/generator: prowgen + job-release: "4.22" + pj-rehearse.openshift.io/can-be-rehearsed: "true" + name: periodic-ci-stolostron-policy-collection-main-ocp4.22-fips-interop-opp-aws + reporter_config: + slack: + channel: '#opp-discussion' + job_states_to_report: + - success + - failure + - error + report_template: '{{if eq .Status.State "success"}} :slack-green: Job *{{.Spec.Job}}* + ended with *{{.Status.State}}*. <{{.Status.URL}}|View logs> {{else}} :failed: + Job *{{.Spec.Job}}* ended with *{{.Status.State}}*. <{{.Status.URL}}|View + logs> {{end}}' + spec: + containers: + - args: + - --gcs-upload-secret=/secrets/gcs/service-account.json + - --image-import-pull-secret=/etc/pull-secret/.dockerconfigjson + - --lease-server-credentials-file=/etc/boskos/credentials + - --report-credentials-file=/etc/report/credentials + - --secret-dir=/secrets/ci-pull-credentials + - --target=interop-opp-aws + - --variant=ocp4.22-fips + command: + - ci-operator + env: + - name: HTTP_SERVER_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + image: quay-proxy.ci.openshift.org/openshift/ci:ci_ci-operator_latest + imagePullPolicy: Always + name: "" + ports: + - containerPort: 8080 + name: http + resources: + requests: + cpu: 10m + volumeMounts: + - mountPath: /etc/boskos + name: boskos + readOnly: true + - mountPath: /secrets/ci-pull-credentials + name: ci-pull-credentials + readOnly: true + - mountPath: /secrets/gcs + name: gcs-credentials + readOnly: true + - mountPath: /secrets/manifest-tool + name: manifest-tool-local-pusher + readOnly: true + - mountPath: /etc/pull-secret + name: pull-secret + readOnly: true + - mountPath: /etc/report + name: result-aggregator + readOnly: true + serviceAccountName: ci-operator + volumes: + - name: boskos + secret: + items: + - key: credentials + path: credentials + secretName: boskos-credentials + - name: ci-pull-credentials + secret: + secretName: ci-pull-credentials + - name: manifest-tool-local-pusher + secret: + secretName: manifest-tool-local-pusher + - name: pull-secret + secret: + secretName: registry-pull-credentials + - name: result-aggregator + secret: + secretName: result-aggregator - agent: kubernetes cluster: build03 cron: 0 3,15 * * * diff --git a/ci-operator/jobs/stolostron/policy-collection/stolostron-policy-collection-main-presubmits.yaml b/ci-operator/jobs/stolostron/policy-collection/stolostron-policy-collection-main-presubmits.yaml index c9c232d396ddd..15b28203ddbfe 100644 --- a/ci-operator/jobs/stolostron/policy-collection/stolostron-policy-collection-main-presubmits.yaml +++ b/ci-operator/jobs/stolostron/policy-collection/stolostron-policy-collection-main-presubmits.yaml @@ -1,5 +1,64 @@ presubmits: stolostron/policy-collection: + - agent: kubernetes + always_run: true + branches: + - ^main$ + - ^main- + cluster: build01 + context: ci/prow/ocp4.22-fips-images + decorate: true + decoration_config: + skip_cloning: true + labels: + ci-operator.openshift.io/variant: ocp4.22-fips + ci.openshift.io/generator: prowgen + job-release: "4.22" + pj-rehearse.openshift.io/can-be-rehearsed: "true" + name: pull-ci-stolostron-policy-collection-main-ocp4.22-fips-images + optional: true + rerun_command: /test ocp4.22-fips-images + spec: + containers: + - args: + - --gcs-upload-secret=/secrets/gcs/service-account.json + - --image-import-pull-secret=/etc/pull-secret/.dockerconfigjson + - --report-credentials-file=/etc/report/credentials + - --target=[images] + - --variant=ocp4.22-fips + command: + - ci-operator + image: quay-proxy.ci.openshift.org/openshift/ci:ci_ci-operator_latest + imagePullPolicy: Always + name: "" + resources: + requests: + cpu: 10m + volumeMounts: + - mountPath: /secrets/gcs + name: gcs-credentials + readOnly: true + - mountPath: /secrets/manifest-tool + name: manifest-tool-local-pusher + readOnly: true + - mountPath: /etc/pull-secret + name: pull-secret + readOnly: true + - mountPath: /etc/report + name: result-aggregator + readOnly: true + serviceAccountName: ci-operator + volumes: + - name: manifest-tool-local-pusher + secret: + secretName: manifest-tool-local-pusher + - name: pull-secret + secret: + secretName: registry-pull-credentials + - name: result-aggregator + secret: + secretName: result-aggregator + trigger: (?m)^/test( | .* )ocp4.22-fips-images,?($|\s.*) - agent: kubernetes always_run: true branches: From 93a353807d4ca5c90652f9a442517921d74bd08a Mon Sep 17 00:00:00 2001 From: Michael Pruitt Date: Fri, 14 Aug 2026 10:36:07 -0500 Subject: [PATCH 8/8] Address review findings: Quay discovery scope and S3 pod deadline Quay smoke (finding 1): - Move DiscoverQuay, GetQuayAuth, and PreflightCheck to script-level initialization. If Quay is unreachable, the script now fails early with a clear fatal message instead of leaving QUAY_NS/QUAY_HOST unset for later test cases. ODF health (finding 2): - Bump NOOBAA_S3_TIMEOUT default from 30s to 60s. The Pod's activeDeadlineSeconds includes image pull time; on cold nodes pulling amazon/aws-cli could exhaust a 30s window before the container starts. --- .../interop-tests-opp-quay-smoke-commands.sh | 11 +++++++---- .../opp/odf-health/interop-opp-odf-health-commands.sh | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/ci-operator/step-registry/interop-tests/opp-quay-smoke/interop-tests-opp-quay-smoke-commands.sh b/ci-operator/step-registry/interop-tests/opp-quay-smoke/interop-tests-opp-quay-smoke-commands.sh index ffde40ed149ba..345e0a8da7d06 100755 --- a/ci-operator/step-registry/interop-tests/opp-quay-smoke/interop-tests-opp-quay-smoke-commands.sh +++ b/ci-operator/step-registry/interop-tests/opp-quay-smoke/interop-tests-opp-quay-smoke-commands.sh @@ -141,6 +141,13 @@ CreateTestOrg() { -d '{"name":"interop-smoke-test","email":"interop-test@example.com"}' || true } +################################################################################ +# Discovery (script-level): fail early if Quay is not reachable +################################################################################ +DiscoverQuay +GetQuayAuth +PreflightCheck || { echo "FATAL: Quay route not reachable; aborting all tests" >&2; exit 1; } + ################################################################################ # Test Case 1: Push and pull image via Quay route ################################################################################ @@ -148,10 +155,6 @@ RunPushPull() { typeset testName="[sig-interop][Jira:INTEROP][Feature:Quay] Push and pull image via Quay route" typeset -i start elapsed start=$(date +%s) - - DiscoverQuay - GetQuayAuth - PreflightCheck || { elapsed=$(( $(date +%s) - start )); RecordResult "${testName}" "failed" "Quay route not reachable" "${elapsed}"; return 1; } CreateTestOrg typeset pushTarget="${QUAY_HOST}/interop-smoke-test/ubi-smoke:${IMAGE_TAG}" diff --git a/ci-operator/step-registry/interop/opp/odf-health/interop-opp-odf-health-commands.sh b/ci-operator/step-registry/interop/opp/odf-health/interop-opp-odf-health-commands.sh index 2452dfbde4211..66dd8fdb2a6d0 100755 --- a/ci-operator/step-registry/interop/opp/odf-health/interop-opp-odf-health-commands.sh +++ b/ci-operator/step-registry/interop/opp/odf-health/interop-opp-odf-health-commands.sh @@ -13,7 +13,7 @@ shopt -s inherit_errexit # --------------------------------------------------------------------------- ODF_NAMESPACE="${ODF_NAMESPACE:-openshift-storage}" -NOOBAA_S3_TIMEOUT="${NOOBAA_S3_TIMEOUT:-30}" +NOOBAA_S3_TIMEOUT="${NOOBAA_S3_TIMEOUT:-60}" typeset junitFile="${ARTIFACT_DIR}/junit_odf_health.xml"