From e3fd2387b2d975f55c02f9ce22aa14e511a5cbfd Mon Sep 17 00:00:00 2001 From: Randy Bruno Piverger <21374229+Randy424@users.noreply.github.com> Date: Tue, 7 Apr 2026 22:58:38 -0700 Subject: [PATCH 1/6] ACM-30906: Add AAP automation script for rhacmstackem integration Add install-aap.sh for automated Ansible Automation Platform provisioning on weekly clusters. Supports idempotent installation, operator deployment, and automated subscription management via Red Hat offline token. Signed-off-by: Randy Bruno Piverger <21374229+Randy424@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 --- scripts/aap-automation/install-aap.sh | 324 ++++++++++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100755 scripts/aap-automation/install-aap.sh diff --git a/scripts/aap-automation/install-aap.sh b/scripts/aap-automation/install-aap.sh new file mode 100755 index 00000000000..ceab8a781e3 --- /dev/null +++ b/scripts/aap-automation/install-aap.sh @@ -0,0 +1,324 @@ +#!/usr/bin/env bash +# Copyright Contributors to the Open Cluster Management project +# Idempotent script for installing Ansible Automation Platform (AAP) on OpenShift +# Safe to run repeatedly - checks for existing installation before proceeding + +set -e + +# Configuration variables +AAP_NAMESPACE=${AAP_NAMESPACE:-"ansible-automation-platform"} +PLATFORM_NAME=${PLATFORM_NAME:-"aap-platform"} +OPERATOR_CHANNEL=${OPERATOR_CHANNEL:-"stable-2.5-cluster-scoped"} +OPERATOR_SOURCE=${OPERATOR_SOURCE:-"redhat-operators"} +RH_OFFLINE_TOKEN=${RH_OFFLINE_TOKEN:-""} +ENABLE_HUB=${ENABLE_HUB:-"false"} +ENABLE_EDA=${ENABLE_EDA:-"false"} + +# Color output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# Check prerequisites +if ! command -v oc &> /dev/null; then + log_error "oc CLI not found. Please install the OpenShift CLI." + exit 1 +fi + +if ! oc whoami &> /dev/null; then + log_error "Not logged into an OpenShift cluster. Please run 'oc login' first." + exit 1 +fi + +# --- Idempotency check --- +check_aap_status() { + if ! oc get namespace "$AAP_NAMESPACE" &> /dev/null; then + echo "not_installed"; return + fi + if ! oc get ansibleautomationplatform "$PLATFORM_NAME" -n "$AAP_NAMESPACE" &> /dev/null; then + echo "namespace_only"; return + fi + PLATFORM_STATUS=$(oc get ansibleautomationplatform "$PLATFORM_NAME" -n "$AAP_NAMESPACE" \ + -o jsonpath='{.status.conditions[?(@.type=="Running")].status}' 2>/dev/null || echo "Unknown") + GATEWAY_RUNNING=$(oc get pods -n "$AAP_NAMESPACE" -l app.kubernetes.io/component=aap-gateway \ + --no-headers 2>/dev/null | grep -c "Running" || echo "0") + if [ "$PLATFORM_STATUS" = "True" ] || [ "$GATEWAY_RUNNING" -gt "0" ]; then + echo "healthy" + else + echo "unhealthy" + fi +} + +AAP_STATUS=$(check_aap_status) +log_info "Current AAP status: $AAP_STATUS" + +case "$AAP_STATUS" in + "healthy") + ROUTE_URL=$(oc get route ${PLATFORM_NAME} -n $AAP_NAMESPACE -o jsonpath='{.spec.host}' 2>/dev/null || echo "N/A") + log_info "AAP is already installed and healthy at https://${ROUTE_URL}" + exit 0 + ;; + "not_installed") + log_info "AAP is not installed. Starting installation..." + ;; + "namespace_only") + log_info "Namespace exists but AnsibleAutomationPlatform not found. Continuing installation..." + ;; + "unhealthy") + log_warn "AAP is installed but not healthy. Attempting repair..." + ;; + *) + log_error "Unknown AAP status: $AAP_STATUS" + exit 1 + ;; +esac + +# --- Installation --- +log_info "Starting AAP installation on OpenShift cluster: $(oc whoami --show-server)" + +# Create namespace +log_info "Creating namespace: $AAP_NAMESPACE" +oc create namespace $AAP_NAMESPACE --dry-run=client -o yaml | oc apply -f - + +# Install AAP Operator +log_info "Installing AAP Operator via OperatorHub" +cat </dev/null | grep -c "Succeeded" || echo "0") + if [ "$CSV_CHECK" -gt "0" ]; then + log_info "AAP Operator is ready" + break + fi + sleep $INTERVAL + ELAPSED=$((ELAPSED + INTERVAL)) + log_info "Waiting... ($ELAPSED/$TIMEOUT seconds)" +done + +if [ $ELAPSED -ge $TIMEOUT ]; then + log_warn "Timeout waiting for AAP Operator CSV, checking if operator is already installed..." + OPERATOR_PODS=$(oc get pods -n $AAP_NAMESPACE -l app.kubernetes.io/component=operator --no-headers 2>/dev/null | wc -l) + if [ "$OPERATOR_PODS" -gt "0" ]; then + log_info "Operator pods found, continuing with installation..." + else + log_error "AAP Operator is not ready" + exit 1 + fi +fi + +CSV_VERSION=$(oc get csv -n $AAP_NAMESPACE -o jsonpath='{.items[*].metadata.name}' 2>/dev/null | grep -o 'aap-operator[^ ]*' | head -1 || echo "unknown") +log_info "AAP Operator CSV: $CSV_VERSION" + +# Create AnsibleAutomationPlatform CR +log_info "Creating AnsibleAutomationPlatform: $PLATFORM_NAME" + +PLATFORM_SPEC="apiVersion: aap.ansible.com/v1alpha1 +kind: AnsibleAutomationPlatform +metadata: + name: $PLATFORM_NAME + namespace: $AAP_NAMESPACE +spec: + controller: + replicas: 1" + +if [ "$ENABLE_HUB" = "true" ]; then + PLATFORM_SPEC="$PLATFORM_SPEC + hub: + replicas: 1" +fi + +if [ "$ENABLE_EDA" = "true" ]; then + PLATFORM_SPEC="$PLATFORM_SPEC + eda: + replicas: 1" +fi + +echo "$PLATFORM_SPEC" | oc apply -f - + +# Wait for platform to be ready +log_info "Waiting for AnsibleAutomationPlatform to be ready..." +log_info "This may take several minutes as multiple components are deployed..." +TIMEOUT=900 +ELAPSED=0 + +while [ $ELAPSED -lt $TIMEOUT ]; do + PLATFORM_STATUS=$(oc get ansibleautomationplatform $PLATFORM_NAME -n $AAP_NAMESPACE \ + -o jsonpath='{.status.conditions[?(@.type=="Running")].status}' 2>/dev/null || echo "Unknown") + if [ "$PLATFORM_STATUS" = "True" ]; then + log_info "AnsibleAutomationPlatform is ready" + break + fi + sleep $INTERVAL + ELAPSED=$((ELAPSED + INTERVAL)) + log_info "Waiting for platform... ($ELAPSED/$TIMEOUT seconds) - Status: $PLATFORM_STATUS" +done + +if [ $ELAPSED -ge $TIMEOUT ]; then + log_warn "Timeout waiting for full platform deployment" + GATEWAY_PODS=$(oc get pods -n $AAP_NAMESPACE -l app.kubernetes.io/name=aap-platform,app.kubernetes.io/component=gateway --no-headers 2>/dev/null | grep -c "Running" || echo "0") + if [ "$GATEWAY_PODS" -gt "0" ]; then + log_info "Gateway is running, platform may be partially ready" + else + log_error "Platform deployment failed" + log_info "Check status with: oc get ansibleautomationplatform $PLATFORM_NAME -n $AAP_NAMESPACE -o yaml" + exit 1 + fi +fi + +# Get admin password +log_info "Retrieving admin password from secret..." +ADMIN_PASSWORD=$(oc get secret ${PLATFORM_NAME}-admin-password -n $AAP_NAMESPACE -o jsonpath='{.data.password}' 2>/dev/null | base64 -d) + +if [ -z "$ADMIN_PASSWORD" ]; then + log_error "Failed to retrieve admin password" + exit 1 +fi + +# Get route URL +log_info "Retrieving AAP Platform route..." +ROUTE_URL=$(oc get route ${PLATFORM_NAME} -n $AAP_NAMESPACE -o jsonpath='{.spec.host}' 2>/dev/null || echo "") + +if [ -z "$ROUTE_URL" ]; then + log_warn "Route not found yet. Waiting for route to be created..." + sleep 30 + ROUTE_URL=$(oc get route ${PLATFORM_NAME} -n $AAP_NAMESPACE -o jsonpath='{.spec.host}' 2>/dev/null || echo "") +fi + +# --- Subscription management --- +if [ -n "$RH_OFFLINE_TOKEN" ]; then + log_info "Configuring subscription using offline token..." + + RHSM_API="https://api.access.redhat.com/management/v1" + ALLOCATION_NAME="AAP-Automation-$(date +%Y%m%d)" + MANIFEST_FILE="/tmp/manifest-${ALLOCATION_NAME}.zip" + AAP_URL="https://${ROUTE_URL}" + + # Exchange offline token for access token + log_info "Obtaining access token from Red Hat SSO..." + ACCESS_TOKEN=$(curl -sk -X POST \ + "https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/token" \ + -d "grant_type=refresh_token" \ + -d "client_id=rhsm-api" \ + -d "refresh_token=${RH_OFFLINE_TOKEN}" | jq -r '.access_token') + + if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" = "null" ]; then + log_warn "Failed to obtain access token - skipping subscription setup" + else + # Check for existing allocation or create new one + ALLOCATION_UUID=$(curl -sk -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + "${RHSM_API}/allocations" | jq -r '.body[0].uuid // empty') + + if [ -z "$ALLOCATION_UUID" ]; then + log_info "Creating new subscription allocation: $ALLOCATION_NAME" + ALLOCATION_UUID=$(curl -sk -X POST \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"name\":\"${ALLOCATION_NAME}\",\"type\":\"Satellite\",\"version\":\"6.11\"}" \ + "${RHSM_API}/allocations" | jq -r '.body.uuid // empty') + + if [ -n "$ALLOCATION_UUID" ]; then + POOL_ID=$(curl -sk -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + "${RHSM_API}/allocations/${ALLOCATION_UUID}/subscriptions/available" | \ + jq -r '[.body[] | select(.product_name | contains("Ansible"))][0].pool_id // empty') + + if [ -n "$POOL_ID" ]; then + curl -sk -X POST \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"pool_id\":\"${POOL_ID}\",\"quantity\":1}" \ + "${RHSM_API}/allocations/${ALLOCATION_UUID}/subscriptions" > /dev/null + log_info "Subscription attached" + else + log_warn "No AAP subscription pool found" + fi + else + log_warn "Failed to create allocation - skipping subscription setup" + fi + fi + + if [ -n "$ALLOCATION_UUID" ]; then + log_info "Downloading subscription manifest..." + curl -sk -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + "${RHSM_API}/allocations/${ALLOCATION_UUID}/export" \ + -o "${MANIFEST_FILE}" + + if [ -f "$MANIFEST_FILE" ] && [ -s "$MANIFEST_FILE" ]; then + log_info "Uploading manifest to AAP..." + UPLOAD_RESPONSE=$(curl -sk -X POST \ + -u "admin:${ADMIN_PASSWORD}" \ + -F "manifest=@${MANIFEST_FILE}" \ + "${AAP_URL}/api/controller/v2/config/subscriptions/" \ + -w "\nHTTP_CODE:%{http_code}") + + HTTP_CODE=$(echo "$UPLOAD_RESPONSE" | grep "HTTP_CODE:" | cut -d: -f2) + if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ]; then + log_info "Subscription manifest uploaded successfully" + else + log_warn "Manifest upload returned HTTP $HTTP_CODE" + fi + rm -f "$MANIFEST_FILE" + else + log_warn "Failed to download manifest" + fi + fi + fi +else + log_info "No RH_OFFLINE_TOKEN set - skipping automated subscription setup" +fi + +# --- Summary --- +echo "" +log_info "==================================================" +log_info "AAP Installation Complete!" +log_info "==================================================" +echo "" +log_info "Namespace: $AAP_NAMESPACE" +log_info "Platform Name: $PLATFORM_NAME" +log_info "URL: https://${ROUTE_URL}" +log_info "Username: admin" +log_info "Password: $ADMIN_PASSWORD" +echo "" +log_info "Deployed Components:" +log_info " - Gateway (UI)" +log_info " - Controller (Automation Engine)" +if [ "$ENABLE_HUB" = "true" ]; then + log_info " - Automation Hub (Content Management)" +fi +if [ "$ENABLE_EDA" = "true" ]; then + log_info " - Event-Driven Ansible" +fi +echo "" +log_info "To retrieve the password later, run:" +log_info " oc get secret ${PLATFORM_NAME}-admin-password -n $AAP_NAMESPACE -o jsonpath='{.data.password}' | base64 -d" From 2a8bc968411eda6804d801d0509c0b835f848b37 Mon Sep 17 00:00:00 2001 From: Randy Bruno Piverger <21374229+Randy424@users.noreply.github.com> Date: Tue, 7 Apr 2026 23:47:00 -0700 Subject: [PATCH 2/6] ACM-30906: Address CodeRabbit review findings - Add prerequisite checks for curl, jq, base64 - Centralize curl TLS options with opt-in CURL_INSECURE flag - Unify gateway pod label selectors for consistent health checks - Filter allocation lookup by name instead of selecting first result - Remove admin password from log output - Use mktemp + trap for manifest file cleanup - Use awk for pod status matching to avoid false positives - Add AAP_MODE support for platform vs controller (legacy) deployment Signed-off-by: Randy Bruno Piverger <21374229+Randy424@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 --- scripts/aap-automation/install-aap.sh | 157 +++++++++++++++++--------- 1 file changed, 104 insertions(+), 53 deletions(-) diff --git a/scripts/aap-automation/install-aap.sh b/scripts/aap-automation/install-aap.sh index ceab8a781e3..dcaa83c5c6b 100755 --- a/scripts/aap-automation/install-aap.sh +++ b/scripts/aap-automation/install-aap.sh @@ -11,6 +11,11 @@ PLATFORM_NAME=${PLATFORM_NAME:-"aap-platform"} OPERATOR_CHANNEL=${OPERATOR_CHANNEL:-"stable-2.5-cluster-scoped"} OPERATOR_SOURCE=${OPERATOR_SOURCE:-"redhat-operators"} RH_OFFLINE_TOKEN=${RH_OFFLINE_TOKEN:-""} +AAP_MODE=${AAP_MODE:-"platform"} # "platform" (AnsibleAutomationPlatform) or "controller" (AutomationController) +# Optional platform-mode components (ignored in controller mode): +# Hub = private repo for Ansible collections and execution environments +# EDA = event-driven automation (trigger playbooks from webhooks, alerts, etc.) +# Both require significant extra cluster resources; only enable if needed. ENABLE_HUB=${ENABLE_HUB:-"false"} ENABLE_EDA=${ENABLE_EDA:-"false"} @@ -25,9 +30,19 @@ log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; } # Check prerequisites -if ! command -v oc &> /dev/null; then - log_error "oc CLI not found. Please install the OpenShift CLI." - exit 1 +for cmd in oc curl jq base64; do + if ! command -v "$cmd" &> /dev/null; then + log_error "$cmd not found. Please install $cmd." + exit 1 + fi +done + +# Curl options: TLS verification enabled by default; set CURL_INSECURE=true to +# skip verification (e.g., for self-signed certs on internal AAP routes). +CURL_OPTS=(-s) +if [ "${CURL_INSECURE:-false}" = "true" ]; then + CURL_OPTS+=(-k) + log_warn "CURL_INSECURE=true: TLS certificate verification is disabled" fi if ! oc whoami &> /dev/null; then @@ -40,14 +55,24 @@ check_aap_status() { if ! oc get namespace "$AAP_NAMESPACE" &> /dev/null; then echo "not_installed"; return fi - if ! oc get ansibleautomationplatform "$PLATFORM_NAME" -n "$AAP_NAMESPACE" &> /dev/null; then - echo "namespace_only"; return + if [ "$AAP_MODE" = "controller" ]; then + if ! oc get automationcontroller "$PLATFORM_NAME" -n "$AAP_NAMESPACE" &> /dev/null; then + echo "namespace_only"; return + fi + CR_STATUS=$(oc get automationcontroller "$PLATFORM_NAME" -n "$AAP_NAMESPACE" \ + -o jsonpath='{.status.conditions[?(@.type=="Running")].status}' 2>/dev/null || echo "Unknown") + PODS_RUNNING=$(oc get pods -n "$AAP_NAMESPACE" -l app.kubernetes.io/managed-by=automationcontroller-operator \ + --no-headers 2>/dev/null | awk '$3=="Running"{c++} END{print c+0}') + else + if ! oc get ansibleautomationplatform "$PLATFORM_NAME" -n "$AAP_NAMESPACE" &> /dev/null; then + echo "namespace_only"; return + fi + CR_STATUS=$(oc get ansibleautomationplatform "$PLATFORM_NAME" -n "$AAP_NAMESPACE" \ + -o jsonpath='{.status.conditions[?(@.type=="Running")].status}' 2>/dev/null || echo "Unknown") + PODS_RUNNING=$(oc get pods -n "$AAP_NAMESPACE" -l app.kubernetes.io/name=aap-platform,app.kubernetes.io/component=gateway \ + --no-headers 2>/dev/null | awk '$3=="Running"{c++} END{print c+0}') fi - PLATFORM_STATUS=$(oc get ansibleautomationplatform "$PLATFORM_NAME" -n "$AAP_NAMESPACE" \ - -o jsonpath='{.status.conditions[?(@.type=="Running")].status}' 2>/dev/null || echo "Unknown") - GATEWAY_RUNNING=$(oc get pods -n "$AAP_NAMESPACE" -l app.kubernetes.io/component=aap-gateway \ - --no-headers 2>/dev/null | grep -c "Running" || echo "0") - if [ "$PLATFORM_STATUS" = "True" ] || [ "$GATEWAY_RUNNING" -gt "0" ]; then + if [ "$CR_STATUS" = "True" ] || [ "$PODS_RUNNING" -gt "0" ]; then echo "healthy" else echo "unhealthy" @@ -141,10 +166,22 @@ fi CSV_VERSION=$(oc get csv -n $AAP_NAMESPACE -o jsonpath='{.items[*].metadata.name}' 2>/dev/null | grep -o 'aap-operator[^ ]*' | head -1 || echo "unknown") log_info "AAP Operator CSV: $CSV_VERSION" -# Create AnsibleAutomationPlatform CR -log_info "Creating AnsibleAutomationPlatform: $PLATFORM_NAME" +# Create CR based on mode +if [ "$AAP_MODE" = "controller" ]; then + log_info "Creating AutomationController (legacy mode): $PLATFORM_NAME" + cat </dev/null || echo "Unknown") - if [ "$PLATFORM_STATUS" = "True" ]; then - log_info "AnsibleAutomationPlatform is ready" + if [ "$AAP_MODE" = "controller" ]; then + CR_STATUS=$(oc get automationcontroller $PLATFORM_NAME -n $AAP_NAMESPACE \ + -o jsonpath='{.status.conditions[?(@.type=="Running")].status}' 2>/dev/null || echo "Unknown") + else + CR_STATUS=$(oc get ansibleautomationplatform $PLATFORM_NAME -n $AAP_NAMESPACE \ + -o jsonpath='{.status.conditions[?(@.type=="Running")].status}' 2>/dev/null || echo "Unknown") + fi + if [ "$CR_STATUS" = "True" ]; then + log_info "AAP is ready" break fi sleep $INTERVAL ELAPSED=$((ELAPSED + INTERVAL)) - log_info "Waiting for platform... ($ELAPSED/$TIMEOUT seconds) - Status: $PLATFORM_STATUS" + log_info "Waiting... ($ELAPSED/$TIMEOUT seconds) - Status: $CR_STATUS" done if [ $ELAPSED -ge $TIMEOUT ]; then - log_warn "Timeout waiting for full platform deployment" - GATEWAY_PODS=$(oc get pods -n $AAP_NAMESPACE -l app.kubernetes.io/name=aap-platform,app.kubernetes.io/component=gateway --no-headers 2>/dev/null | grep -c "Running" || echo "0") - if [ "$GATEWAY_PODS" -gt "0" ]; then - log_info "Gateway is running, platform may be partially ready" + log_warn "Timeout waiting for deployment" + if [ "$AAP_MODE" = "controller" ]; then + RUNNING_PODS=$(oc get pods -n $AAP_NAMESPACE -l app.kubernetes.io/managed-by=automationcontroller-operator --no-headers 2>/dev/null | awk '$3=="Running"{c++} END{print c+0}') else - log_error "Platform deployment failed" - log_info "Check status with: oc get ansibleautomationplatform $PLATFORM_NAME -n $AAP_NAMESPACE -o yaml" + RUNNING_PODS=$(oc get pods -n $AAP_NAMESPACE -l app.kubernetes.io/name=aap-platform,app.kubernetes.io/component=gateway --no-headers 2>/dev/null | awk '$3=="Running"{c++} END{print c+0}') + fi + if [ "$RUNNING_PODS" -gt "0" ]; then + log_info "Pods are running, deployment may be partially ready" + else + log_error "Deployment failed" exit 1 fi fi @@ -222,12 +268,13 @@ if [ -n "$RH_OFFLINE_TOKEN" ]; then RHSM_API="https://api.access.redhat.com/management/v1" ALLOCATION_NAME="AAP-Automation-$(date +%Y%m%d)" - MANIFEST_FILE="/tmp/manifest-${ALLOCATION_NAME}.zip" + MANIFEST_FILE=$(mktemp /tmp/manifest-XXXXXX.zip) + trap 'rm -f "$MANIFEST_FILE"' EXIT AAP_URL="https://${ROUTE_URL}" # Exchange offline token for access token log_info "Obtaining access token from Red Hat SSO..." - ACCESS_TOKEN=$(curl -sk -X POST \ + ACCESS_TOKEN=$(curl "${CURL_OPTS[@]}" -X POST \ "https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/token" \ -d "grant_type=refresh_token" \ -d "client_id=rhsm-api" \ @@ -236,25 +283,25 @@ if [ -n "$RH_OFFLINE_TOKEN" ]; then if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" = "null" ]; then log_warn "Failed to obtain access token - skipping subscription setup" else - # Check for existing allocation or create new one - ALLOCATION_UUID=$(curl -sk -H "Authorization: Bearer ${ACCESS_TOKEN}" \ - "${RHSM_API}/allocations" | jq -r '.body[0].uuid // empty') + # Check for existing allocation by name, or create new one + ALLOCATION_UUID=$(curl "${CURL_OPTS[@]}" -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + "${RHSM_API}/allocations" | jq -r --arg name "$ALLOCATION_NAME" '[.body[] | select(.name == $name)][0].uuid // empty') if [ -z "$ALLOCATION_UUID" ]; then log_info "Creating new subscription allocation: $ALLOCATION_NAME" - ALLOCATION_UUID=$(curl -sk -X POST \ + ALLOCATION_UUID=$(curl "${CURL_OPTS[@]}" -X POST \ -H "Authorization: Bearer ${ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ -d "{\"name\":\"${ALLOCATION_NAME}\",\"type\":\"Satellite\",\"version\":\"6.11\"}" \ "${RHSM_API}/allocations" | jq -r '.body.uuid // empty') if [ -n "$ALLOCATION_UUID" ]; then - POOL_ID=$(curl -sk -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + POOL_ID=$(curl "${CURL_OPTS[@]}" -H "Authorization: Bearer ${ACCESS_TOKEN}" \ "${RHSM_API}/allocations/${ALLOCATION_UUID}/subscriptions/available" | \ jq -r '[.body[] | select(.product_name | contains("Ansible"))][0].pool_id // empty') if [ -n "$POOL_ID" ]; then - curl -sk -X POST \ + curl "${CURL_OPTS[@]}" -X POST \ -H "Authorization: Bearer ${ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ -d "{\"pool_id\":\"${POOL_ID}\",\"quantity\":1}" \ @@ -270,13 +317,13 @@ if [ -n "$RH_OFFLINE_TOKEN" ]; then if [ -n "$ALLOCATION_UUID" ]; then log_info "Downloading subscription manifest..." - curl -sk -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + curl "${CURL_OPTS[@]}" -H "Authorization: Bearer ${ACCESS_TOKEN}" \ "${RHSM_API}/allocations/${ALLOCATION_UUID}/export" \ -o "${MANIFEST_FILE}" if [ -f "$MANIFEST_FILE" ] && [ -s "$MANIFEST_FILE" ]; then log_info "Uploading manifest to AAP..." - UPLOAD_RESPONSE=$(curl -sk -X POST \ + UPLOAD_RESPONSE=$(curl "${CURL_OPTS[@]}" -X POST \ -u "admin:${ADMIN_PASSWORD}" \ -F "manifest=@${MANIFEST_FILE}" \ "${AAP_URL}/api/controller/v2/config/subscriptions/" \ @@ -288,7 +335,6 @@ if [ -n "$RH_OFFLINE_TOKEN" ]; then else log_warn "Manifest upload returned HTTP $HTTP_CODE" fi - rm -f "$MANIFEST_FILE" else log_warn "Failed to download manifest" fi @@ -305,19 +351,24 @@ log_info "AAP Installation Complete!" log_info "==================================================" echo "" log_info "Namespace: $AAP_NAMESPACE" +log_info "Mode: $AAP_MODE" log_info "Platform Name: $PLATFORM_NAME" log_info "URL: https://${ROUTE_URL}" log_info "Username: admin" -log_info "Password: $ADMIN_PASSWORD" +log_info "Password: (retrieve via command below)" echo "" log_info "Deployed Components:" -log_info " - Gateway (UI)" -log_info " - Controller (Automation Engine)" -if [ "$ENABLE_HUB" = "true" ]; then - log_info " - Automation Hub (Content Management)" -fi -if [ "$ENABLE_EDA" = "true" ]; then - log_info " - Event-Driven Ansible" +if [ "$AAP_MODE" = "controller" ]; then + log_info " - Controller (Standalone)" +else + log_info " - Gateway (UI)" + log_info " - Controller (Automation Engine)" + if [ "$ENABLE_HUB" = "true" ]; then + log_info " - Automation Hub (Content Management)" + fi + if [ "$ENABLE_EDA" = "true" ]; then + log_info " - Event-Driven Ansible" + fi fi echo "" log_info "To retrieve the password later, run:" From af0e341ac45a1b4dd2860afe7ddf96e5c52d9bf0 Mon Sep 17 00:00:00 2001 From: Randy Bruno Piverger <21374229+Randy424@users.noreply.github.com> Date: Wed, 8 Apr 2026 00:03:46 -0700 Subject: [PATCH 3/6] ACM-30906: Address second round of CodeRabbit findings - Validate AAP_MODE rejects unknown values instead of silent fallback - Guard subscription management against empty ROUTE_URL Signed-off-by: Randy Bruno Piverger <21374229+Randy424@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 --- scripts/aap-automation/install-aap.sh | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/aap-automation/install-aap.sh b/scripts/aap-automation/install-aap.sh index dcaa83c5c6b..8035aeded4d 100755 --- a/scripts/aap-automation/install-aap.sh +++ b/scripts/aap-automation/install-aap.sh @@ -19,6 +19,15 @@ AAP_MODE=${AAP_MODE:-"platform"} # "platform" (AnsibleAutomationPlatform) or "c ENABLE_HUB=${ENABLE_HUB:-"false"} ENABLE_EDA=${ENABLE_EDA:-"false"} +# Validate AAP_MODE +case "$AAP_MODE" in + platform|controller) ;; + *) + echo "ERROR: Invalid AAP_MODE: $AAP_MODE. Expected 'platform' or 'controller'." + exit 1 + ;; +esac + # Color output RED='\033[0;31m' GREEN='\033[0;32m' @@ -263,7 +272,9 @@ if [ -z "$ROUTE_URL" ]; then fi # --- Subscription management --- -if [ -n "$RH_OFFLINE_TOKEN" ]; then +if [ -z "$ROUTE_URL" ]; then + log_warn "Route is unavailable; skipping automated subscription setup" +elif [ -n "$RH_OFFLINE_TOKEN" ]; then log_info "Configuring subscription using offline token..." RHSM_API="https://api.access.redhat.com/management/v1" From 26d28a741be1ef8aa8d78401e24f97fefb138c40 Mon Sep 17 00:00:00 2001 From: Randy Bruno Piverger <21374229+Randy424@users.noreply.github.com> Date: Wed, 8 Apr 2026 12:00:20 -0700 Subject: [PATCH 4/6] ACM-30906: Validate subscription attach HTTP response Check HTTP status code when attaching subscription to allocation instead of discarding the response. Signed-off-by: Randy Bruno Piverger <21374229+Randy424@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 --- scripts/aap-automation/install-aap.sh | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/aap-automation/install-aap.sh b/scripts/aap-automation/install-aap.sh index 8035aeded4d..6282aed5b4c 100755 --- a/scripts/aap-automation/install-aap.sh +++ b/scripts/aap-automation/install-aap.sh @@ -312,12 +312,18 @@ elif [ -n "$RH_OFFLINE_TOKEN" ]; then jq -r '[.body[] | select(.product_name | contains("Ansible"))][0].pool_id // empty') if [ -n "$POOL_ID" ]; then - curl "${CURL_OPTS[@]}" -X POST \ + ATTACH_RESPONSE=$(curl "${CURL_OPTS[@]}" -X POST \ -H "Authorization: Bearer ${ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ -d "{\"pool_id\":\"${POOL_ID}\",\"quantity\":1}" \ - "${RHSM_API}/allocations/${ALLOCATION_UUID}/subscriptions" > /dev/null - log_info "Subscription attached" + "${RHSM_API}/allocations/${ALLOCATION_UUID}/subscriptions" \ + -w "\nHTTP_CODE:%{http_code}") + ATTACH_HTTP=$(echo "$ATTACH_RESPONSE" | grep "HTTP_CODE:" | cut -d: -f2) + if [ "$ATTACH_HTTP" = "200" ] || [ "$ATTACH_HTTP" = "201" ]; then + log_info "Subscription attached" + else + log_warn "Subscription attach returned HTTP $ATTACH_HTTP" + fi else log_warn "No AAP subscription pool found" fi From 25d2b2c9dbc90083d806c722e968cb33d62597bd Mon Sep 17 00:00:00 2001 From: Randy Bruno Piverger <21374229+Randy424@users.noreply.github.com> Date: Wed, 8 Apr 2026 12:51:01 -0700 Subject: [PATCH 5/6] ACM-30906: Validate manifest download HTTP response Check HTTP status on manifest download and clean up partial file on failure instead of proceeding with an invalid manifest. Signed-off-by: Randy Bruno Piverger <21374229+Randy424@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 --- scripts/aap-automation/install-aap.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/aap-automation/install-aap.sh b/scripts/aap-automation/install-aap.sh index 6282aed5b4c..331b5a8e901 100755 --- a/scripts/aap-automation/install-aap.sh +++ b/scripts/aap-automation/install-aap.sh @@ -334,9 +334,14 @@ elif [ -n "$RH_OFFLINE_TOKEN" ]; then if [ -n "$ALLOCATION_UUID" ]; then log_info "Downloading subscription manifest..." - curl "${CURL_OPTS[@]}" -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + DOWNLOAD_HTTP=$(curl "${CURL_OPTS[@]}" -H "Authorization: Bearer ${ACCESS_TOKEN}" \ "${RHSM_API}/allocations/${ALLOCATION_UUID}/export" \ - -o "${MANIFEST_FILE}" + -o "${MANIFEST_FILE}" -w "%{http_code}") + + if [ "$DOWNLOAD_HTTP" != "200" ]; then + log_warn "Manifest download returned HTTP $DOWNLOAD_HTTP" + rm -f "$MANIFEST_FILE" + fi if [ -f "$MANIFEST_FILE" ] && [ -s "$MANIFEST_FILE" ]; then log_info "Uploading manifest to AAP..." From 5ed00477600f972c17f80a719b521441403a8c98 Mon Sep 17 00:00:00 2001 From: Randy Bruno Piverger <21374229+Randy424@users.noreply.github.com> Date: Tue, 28 Apr 2026 20:51:25 -0700 Subject: [PATCH 6/6] ACM-30906: Fix install-aap.sh bugs found during live cluster testing - Fix CSV_CHECK integer comparison failure (tr + grep -c pipeline) - Replace CR Running=True readiness check with secret+route existence wait - Handle async RHSM export API (follow body.href redirect) - Switch manifest upload from multipart to JSON with base64 encoding - Add API readiness ping loop before subscription and token operations - Use /api/controller/v2/config/ for platform mode ping (gateway /ping/ returns 503 while Hub is unhealthy) - Add OAuth2 token generation and storage as k8s secret - Quote all variable expansions in oc commands (SC2086) Signed-off-by: Randy Bruno Piverger <21374229+Randy424@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 --- scripts/aap-automation/install-aap.sh | 290 ++++++++++++++++---------- 1 file changed, 184 insertions(+), 106 deletions(-) diff --git a/scripts/aap-automation/install-aap.sh b/scripts/aap-automation/install-aap.sh index 331b5a8e901..5e6c86e964e 100755 --- a/scripts/aap-automation/install-aap.sh +++ b/scripts/aap-automation/install-aap.sh @@ -93,7 +93,7 @@ log_info "Current AAP status: $AAP_STATUS" case "$AAP_STATUS" in "healthy") - ROUTE_URL=$(oc get route ${PLATFORM_NAME} -n $AAP_NAMESPACE -o jsonpath='{.spec.host}' 2>/dev/null || echo "N/A") + ROUTE_URL=$(oc get route "${PLATFORM_NAME}" -n "$AAP_NAMESPACE" -o jsonpath='{.spec.host}' 2>/dev/null || echo "N/A") log_info "AAP is already installed and healthy at https://${ROUTE_URL}" exit 0 ;; @@ -117,7 +117,7 @@ log_info "Starting AAP installation on OpenShift cluster: $(oc whoami --show-ser # Create namespace log_info "Creating namespace: $AAP_NAMESPACE" -oc create namespace $AAP_NAMESPACE --dry-run=client -o yaml | oc apply -f - +oc create namespace "$AAP_NAMESPACE" --dry-run=client -o yaml | oc apply -f - # Install AAP Operator log_info "Installing AAP Operator via OperatorHub" @@ -151,7 +151,9 @@ ELAPSED=0 INTERVAL=10 while [ $ELAPSED -lt $TIMEOUT ]; do - CSV_CHECK=$(oc get csv -n $AAP_NAMESPACE -o jsonpath='{.items[*].status.phase}' 2>/dev/null | grep -c "Succeeded" || echo "0") + CSV_CHECK=$(oc get csv -n "$AAP_NAMESPACE" -o jsonpath='{.items[*].status.phase}' 2>/dev/null \ + | tr ' ' '\n' | grep -c "Succeeded" || true) + CSV_CHECK=${CSV_CHECK:-0} if [ "$CSV_CHECK" -gt "0" ]; then log_info "AAP Operator is ready" break @@ -163,7 +165,7 @@ done if [ $ELAPSED -ge $TIMEOUT ]; then log_warn "Timeout waiting for AAP Operator CSV, checking if operator is already installed..." - OPERATOR_PODS=$(oc get pods -n $AAP_NAMESPACE -l app.kubernetes.io/component=operator --no-headers 2>/dev/null | wc -l) + OPERATOR_PODS=$(oc get pods -n "$AAP_NAMESPACE" -l app.kubernetes.io/component=operator --no-headers 2>/dev/null | wc -l) if [ "$OPERATOR_PODS" -gt "0" ]; then log_info "Operator pods found, continuing with installation..." else @@ -172,7 +174,7 @@ if [ $ELAPSED -ge $TIMEOUT ]; then fi fi -CSV_VERSION=$(oc get csv -n $AAP_NAMESPACE -o jsonpath='{.items[*].metadata.name}' 2>/dev/null | grep -o 'aap-operator[^ ]*' | head -1 || echo "unknown") +CSV_VERSION=$(oc get csv -n "$AAP_NAMESPACE" -o jsonpath='{.items[*].metadata.name}' 2>/dev/null | grep -o 'aap-operator[^ ]*' | head -1 || echo "unknown") log_info "AAP Operator CSV: $CSV_VERSION" # Create CR based on mode @@ -214,64 +216,59 @@ spec: echo "$PLATFORM_SPEC" | oc apply -f - fi -# Wait for CR to be ready -log_info "Waiting for AAP to be ready (mode: $AAP_MODE)..." +# Wait for deployment to be ready (secret + route must exist before proceeding) +log_info "Waiting for AAP deployment to be ready (mode: $AAP_MODE)..." log_info "This may take several minutes as components are deployed..." TIMEOUT=900 ELAPSED=0 +INTERVAL=15 while [ $ELAPSED -lt $TIMEOUT ]; do - if [ "$AAP_MODE" = "controller" ]; then - CR_STATUS=$(oc get automationcontroller $PLATFORM_NAME -n $AAP_NAMESPACE \ - -o jsonpath='{.status.conditions[?(@.type=="Running")].status}' 2>/dev/null || echo "Unknown") - else - CR_STATUS=$(oc get ansibleautomationplatform $PLATFORM_NAME -n $AAP_NAMESPACE \ - -o jsonpath='{.status.conditions[?(@.type=="Running")].status}' 2>/dev/null || echo "Unknown") - fi - if [ "$CR_STATUS" = "True" ]; then - log_info "AAP is ready" + ADMIN_SECRET=$(oc get secret "${PLATFORM_NAME}-admin-password" -n "$AAP_NAMESPACE" \ + -o jsonpath='{.data.password}' 2>/dev/null || true) + ROUTE_HOST=$(oc get route "${PLATFORM_NAME}" -n "$AAP_NAMESPACE" \ + -o jsonpath='{.spec.host}' 2>/dev/null || true) + + if [ -n "$ADMIN_SECRET" ] && [ -n "$ROUTE_HOST" ]; then + log_info "AAP deployment is ready (secret + route available)" break fi + sleep $INTERVAL ELAPSED=$((ELAPSED + INTERVAL)) - log_info "Waiting... ($ELAPSED/$TIMEOUT seconds) - Status: $CR_STATUS" + HAS_SECRET="no"; [ -n "$ADMIN_SECRET" ] && HAS_SECRET="yes" + HAS_ROUTE="no"; [ -n "$ROUTE_HOST" ] && HAS_ROUTE="yes" + log_info "Waiting... (${ELAPSED}/${TIMEOUT}s) secret=${HAS_SECRET} route=${HAS_ROUTE}" done if [ $ELAPSED -ge $TIMEOUT ]; then - log_warn "Timeout waiting for deployment" - if [ "$AAP_MODE" = "controller" ]; then - RUNNING_PODS=$(oc get pods -n $AAP_NAMESPACE -l app.kubernetes.io/managed-by=automationcontroller-operator --no-headers 2>/dev/null | awk '$3=="Running"{c++} END{print c+0}') - else - RUNNING_PODS=$(oc get pods -n $AAP_NAMESPACE -l app.kubernetes.io/name=aap-platform,app.kubernetes.io/component=gateway --no-headers 2>/dev/null | awk '$3=="Running"{c++} END{print c+0}') - fi - if [ "$RUNNING_PODS" -gt "0" ]; then - log_info "Pods are running, deployment may be partially ready" - else - log_error "Deployment failed" + if [ -z "$ADMIN_SECRET" ]; then + log_error "Timeout: admin password secret not found" exit 1 fi + if [ -z "$ROUTE_HOST" ]; then + log_warn "Timeout: route not found, continuing without URL" + fi fi -# Get admin password -log_info "Retrieving admin password from secret..." -ADMIN_PASSWORD=$(oc get secret ${PLATFORM_NAME}-admin-password -n $AAP_NAMESPACE -o jsonpath='{.data.password}' 2>/dev/null | base64 -d) +ADMIN_PASSWORD=$(echo "$ADMIN_SECRET" | base64 -d) +ROUTE_URL="$ROUTE_HOST" if [ -z "$ADMIN_PASSWORD" ]; then - log_error "Failed to retrieve admin password" + log_error "Failed to decode admin password" exit 1 fi -# Get route URL -log_info "Retrieving AAP Platform route..." -ROUTE_URL=$(oc get route ${PLATFORM_NAME} -n $AAP_NAMESPACE -o jsonpath='{.spec.host}' 2>/dev/null || echo "") - -if [ -z "$ROUTE_URL" ]; then - log_warn "Route not found yet. Waiting for route to be created..." - sleep 30 - ROUTE_URL=$(oc get route ${PLATFORM_NAME} -n $AAP_NAMESPACE -o jsonpath='{.spec.host}' 2>/dev/null || echo "") +# --- Determine API base URL --- +AAP_URL="https://${ROUTE_URL}" +if [ "$AAP_MODE" = "controller" ]; then + API_BASE="${AAP_URL}/api/v2" + API_PING_PATH="/api/v2/ping/" +else + API_BASE="${AAP_URL}/api/controller/v2" + API_PING_PATH="/api/controller/v2/config/" fi -# --- Subscription management --- if [ -z "$ROUTE_URL" ]; then log_warn "Route is unavailable; skipping automated subscription setup" elif [ -n "$RH_OFFLINE_TOKEN" ]; then @@ -281,84 +278,117 @@ elif [ -n "$RH_OFFLINE_TOKEN" ]; then ALLOCATION_NAME="AAP-Automation-$(date +%Y%m%d)" MANIFEST_FILE=$(mktemp /tmp/manifest-XXXXXX.zip) trap 'rm -f "$MANIFEST_FILE"' EXIT - AAP_URL="https://${ROUTE_URL}" - - # Exchange offline token for access token - log_info "Obtaining access token from Red Hat SSO..." - ACCESS_TOKEN=$(curl "${CURL_OPTS[@]}" -X POST \ - "https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/token" \ - -d "grant_type=refresh_token" \ - -d "client_id=rhsm-api" \ - -d "refresh_token=${RH_OFFLINE_TOKEN}" | jq -r '.access_token') - - if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" = "null" ]; then - log_warn "Failed to obtain access token - skipping subscription setup" - else - # Check for existing allocation by name, or create new one - ALLOCATION_UUID=$(curl "${CURL_OPTS[@]}" -H "Authorization: Bearer ${ACCESS_TOKEN}" \ - "${RHSM_API}/allocations" | jq -r --arg name "$ALLOCATION_NAME" '[.body[] | select(.name == $name)][0].uuid // empty') - - if [ -z "$ALLOCATION_UUID" ]; then - log_info "Creating new subscription allocation: $ALLOCATION_NAME" - ALLOCATION_UUID=$(curl "${CURL_OPTS[@]}" -X POST \ - -H "Authorization: Bearer ${ACCESS_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "{\"name\":\"${ALLOCATION_NAME}\",\"type\":\"Satellite\",\"version\":\"6.11\"}" \ - "${RHSM_API}/allocations" | jq -r '.body.uuid // empty') - if [ -n "$ALLOCATION_UUID" ]; then - POOL_ID=$(curl "${CURL_OPTS[@]}" -H "Authorization: Bearer ${ACCESS_TOKEN}" \ - "${RHSM_API}/allocations/${ALLOCATION_UUID}/subscriptions/available" | \ - jq -r '[.body[] | select(.product_name | contains("Ansible"))][0].pool_id // empty') + # Wait for API readiness before attempting subscription upload + log_info "Waiting for AAP API to accept requests..." + API_TIMEOUT=600 + API_ELAPSED=0 + while [ $API_ELAPSED -lt $API_TIMEOUT ]; do + API_STATUS=$(curl "${CURL_OPTS[@]}" -o /dev/null -w "%{http_code}" \ + -u "admin:${ADMIN_PASSWORD}" "${AAP_URL}${API_PING_PATH}" 2>/dev/null) + if [ "$API_STATUS" = "200" ]; then + log_info "AAP API is ready" + break + fi + sleep 15 + API_ELAPSED=$((API_ELAPSED + 15)) + log_info "Waiting for API... (${API_ELAPSED}/${API_TIMEOUT}s) - HTTP ${API_STATUS}" + done - if [ -n "$POOL_ID" ]; then - ATTACH_RESPONSE=$(curl "${CURL_OPTS[@]}" -X POST \ - -H "Authorization: Bearer ${ACCESS_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "{\"pool_id\":\"${POOL_ID}\",\"quantity\":1}" \ - "${RHSM_API}/allocations/${ALLOCATION_UUID}/subscriptions" \ - -w "\nHTTP_CODE:%{http_code}") - ATTACH_HTTP=$(echo "$ATTACH_RESPONSE" | grep "HTTP_CODE:" | cut -d: -f2) - if [ "$ATTACH_HTTP" = "200" ] || [ "$ATTACH_HTTP" = "201" ]; then - log_info "Subscription attached" + if [ "$API_STATUS" != "200" ]; then + log_warn "AAP API not responding (HTTP ${API_STATUS}) - skipping subscription setup" + else + # Exchange offline token for access token + log_info "Obtaining access token from Red Hat SSO..." + ACCESS_TOKEN=$(curl "${CURL_OPTS[@]}" -X POST \ + "https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/token" \ + -d "grant_type=refresh_token" \ + -d "client_id=rhsm-api" \ + -d "refresh_token=${RH_OFFLINE_TOKEN}" | jq -r '.access_token') + + if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" = "null" ]; then + log_warn "Failed to obtain access token - skipping subscription setup" + else + # Check for existing allocation by name, or create new one + ALLOCATION_UUID=$(curl "${CURL_OPTS[@]}" -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + "${RHSM_API}/allocations" | jq -r --arg name "$ALLOCATION_NAME" \ + '[.body[] | select(.name == $name)][0].uuid // empty') + + if [ -z "$ALLOCATION_UUID" ]; then + log_info "Creating new subscription allocation: $ALLOCATION_NAME" + ALLOCATION_UUID=$(curl "${CURL_OPTS[@]}" -X POST \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"name\":\"${ALLOCATION_NAME}\",\"type\":\"Satellite\",\"version\":\"6.11\"}" \ + "${RHSM_API}/allocations" | jq -r '.body.uuid // empty') + + if [ -n "$ALLOCATION_UUID" ]; then + POOL_ID=$(curl "${CURL_OPTS[@]}" -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + "${RHSM_API}/allocations/${ALLOCATION_UUID}/subscriptions/available" | \ + jq -r '[.body[] | select(.product_name | contains("Ansible"))][0].pool_id // empty') + + if [ -n "$POOL_ID" ]; then + ATTACH_RESPONSE=$(curl "${CURL_OPTS[@]}" -X POST \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"pool_id\":\"${POOL_ID}\",\"quantity\":1}" \ + "${RHSM_API}/allocations/${ALLOCATION_UUID}/subscriptions" \ + -w "\nHTTP_CODE:%{http_code}") + ATTACH_HTTP=$(echo "$ATTACH_RESPONSE" | grep "HTTP_CODE:" | cut -d: -f2) + if [ "$ATTACH_HTTP" = "200" ] || [ "$ATTACH_HTTP" = "201" ]; then + log_info "Subscription attached" + else + log_warn "Subscription attach returned HTTP $ATTACH_HTTP" + fi else - log_warn "Subscription attach returned HTTP $ATTACH_HTTP" + log_warn "No AAP subscription pool found" fi else - log_warn "No AAP subscription pool found" + log_warn "Failed to create allocation - skipping subscription setup" fi - else - log_warn "Failed to create allocation - skipping subscription setup" fi - fi - if [ -n "$ALLOCATION_UUID" ]; then - log_info "Downloading subscription manifest..." - DOWNLOAD_HTTP=$(curl "${CURL_OPTS[@]}" -H "Authorization: Bearer ${ACCESS_TOKEN}" \ - "${RHSM_API}/allocations/${ALLOCATION_UUID}/export" \ - -o "${MANIFEST_FILE}" -w "%{http_code}") + if [ -n "$ALLOCATION_UUID" ]; then + log_info "Downloading subscription manifest..." + # Export API may be async — check for redirect href + EXPORT_RESPONSE=$(curl "${CURL_OPTS[@]}" -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + "${RHSM_API}/allocations/${ALLOCATION_UUID}/export") + EXPORT_HREF=$(echo "$EXPORT_RESPONSE" | jq -r '.body.href // empty' 2>/dev/null) + + if [ -n "$EXPORT_HREF" ]; then + log_info "Export is async, downloading from redirect..." + DOWNLOAD_HTTP=$(curl "${CURL_OPTS[@]}" -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + "$EXPORT_HREF" -o "${MANIFEST_FILE}" -w "%{http_code}") + else + DOWNLOAD_HTTP=$(curl "${CURL_OPTS[@]}" -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + "${RHSM_API}/allocations/${ALLOCATION_UUID}/export" \ + -o "${MANIFEST_FILE}" -w "%{http_code}") + fi - if [ "$DOWNLOAD_HTTP" != "200" ]; then - log_warn "Manifest download returned HTTP $DOWNLOAD_HTTP" - rm -f "$MANIFEST_FILE" - fi + if [ "$DOWNLOAD_HTTP" != "200" ]; then + log_warn "Manifest download returned HTTP $DOWNLOAD_HTTP" + rm -f "$MANIFEST_FILE" + fi - if [ -f "$MANIFEST_FILE" ] && [ -s "$MANIFEST_FILE" ]; then - log_info "Uploading manifest to AAP..." - UPLOAD_RESPONSE=$(curl "${CURL_OPTS[@]}" -X POST \ - -u "admin:${ADMIN_PASSWORD}" \ - -F "manifest=@${MANIFEST_FILE}" \ - "${AAP_URL}/api/controller/v2/config/subscriptions/" \ - -w "\nHTTP_CODE:%{http_code}") - - HTTP_CODE=$(echo "$UPLOAD_RESPONSE" | grep "HTTP_CODE:" | cut -d: -f2) - if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ]; then - log_info "Subscription manifest uploaded successfully" + if [ -f "$MANIFEST_FILE" ] && [ -s "$MANIFEST_FILE" ]; then + log_info "Uploading manifest to AAP..." + MANIFEST_B64=$(base64 < "${MANIFEST_FILE}") + UPLOAD_RESPONSE=$(curl "${CURL_OPTS[@]}" -X POST \ + -u "admin:${ADMIN_PASSWORD}" \ + -H "Content-Type: application/json" \ + -d "{\"manifest\":\"${MANIFEST_B64}\",\"eula\":true}" \ + "${API_BASE}/config/subscriptions/" \ + -w "\nHTTP_CODE:%{http_code}") + + HTTP_CODE=$(echo "$UPLOAD_RESPONSE" | grep "HTTP_CODE:" | cut -d: -f2) + if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ]; then + log_info "Subscription manifest uploaded successfully" + else + log_warn "Manifest upload returned HTTP $HTTP_CODE (AAP 2.5 with SCA may not require it)" + fi else - log_warn "Manifest upload returned HTTP $HTTP_CODE" + log_warn "Failed to download manifest" fi - else - log_warn "Failed to download manifest" fi fi fi @@ -366,6 +396,47 @@ else log_info "No RH_OFFLINE_TOKEN set - skipping automated subscription setup" fi +# --- OAuth token generation --- +OAUTH_TOKEN="" +if [ -n "$ROUTE_URL" ] && [ -n "$ADMIN_PASSWORD" ]; then + log_info "Waiting for AAP API before generating token..." + API_TIMEOUT=600 + API_ELAPSED=0 + API_STATUS="" + while [ $API_ELAPSED -lt $API_TIMEOUT ]; do + API_STATUS=$(curl "${CURL_OPTS[@]}" -o /dev/null -w "%{http_code}" \ + -u "admin:${ADMIN_PASSWORD}" "${AAP_URL}${API_PING_PATH}" 2>/dev/null) + if [ "$API_STATUS" = "200" ]; then break; fi + sleep 15 + API_ELAPSED=$((API_ELAPSED + 15)) + log_info "Waiting for API... (${API_ELAPSED}/${API_TIMEOUT}s) - HTTP ${API_STATUS}" + done + + if [ "$API_STATUS" = "200" ]; then + log_info "Generating OAuth2 token for external integrations..." + TOKEN_RESPONSE=$(curl "${CURL_OPTS[@]}" -X POST \ + -u "admin:${ADMIN_PASSWORD}" \ + -H "Content-Type: application/json" \ + -d '{"scope":"write"}' \ + "${API_BASE}/tokens/") + OAUTH_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.token // empty') + else + log_warn "AAP API not ready (HTTP ${API_STATUS}) - skipping token generation" + fi + + if [ -n "$OAUTH_TOKEN" ]; then + log_info "OAuth token generated successfully" + oc create secret generic "${PLATFORM_NAME}-oauth-token" \ + -n "$AAP_NAMESPACE" \ + --from-literal=token="$OAUTH_TOKEN" \ + --from-literal=host="$ROUTE_URL" \ + --dry-run=client -o yaml | oc apply -f - + log_info "Token stored in secret: ${PLATFORM_NAME}-oauth-token" + else + log_warn "Failed to generate OAuth token" + fi +fi + # --- Summary --- echo "" log_info "==================================================" @@ -378,6 +449,9 @@ log_info "Platform Name: $PLATFORM_NAME" log_info "URL: https://${ROUTE_URL}" log_info "Username: admin" log_info "Password: (retrieve via command below)" +if [ -n "$OAUTH_TOKEN" ]; then + log_info "OAuth Secret: ${PLATFORM_NAME}-oauth-token" +fi echo "" log_info "Deployed Components:" if [ "$AAP_MODE" = "controller" ]; then @@ -395,3 +469,7 @@ fi echo "" log_info "To retrieve the password later, run:" log_info " oc get secret ${PLATFORM_NAME}-admin-password -n $AAP_NAMESPACE -o jsonpath='{.data.password}' | base64 -d" +if [ -n "$OAUTH_TOKEN" ]; then + log_info "To retrieve the OAuth token later, run:" + log_info " oc get secret ${PLATFORM_NAME}-oauth-token -n $AAP_NAMESPACE -o jsonpath='{.data.token}'" +fi