From 9c5460074a9d239d90df6b2b8a14eaa8c3d58689 Mon Sep 17 00:00:00 2001 From: Ahmed Abdalla Date: Fri, 12 Dec 2025 01:59:27 +0100 Subject: [PATCH] feat(azure): add ARO-HCP Taskfile automation for dev workflows Adds Taskfile-based automation for ARO-HCP development under hack/aro-hcp/. Provides modular tasks for managing Azure infrastructure, AKS clusters, HyperShift operator deployment, and hosted cluster lifecycle. Key features: - Modular task structure with prereq, keyvault, oidc, dataplane, aks, dns, operator, and cluster task files - Example configuration files for credentials and environment setup - Comprehensive README with prerequisites and workflow documentation Commit-Message-Assisted-by: Claude (via Claude Code) Co-Authored-By: Claude Opus 4.5 Signed-off-by: Ahmed Abdalla --- hack/aro-hcp/.gitignore | 32 ++ hack/aro-hcp/README.md | 336 ++++++++++++++ hack/aro-hcp/Taskfile.yml | 192 ++++++++ hack/aro-hcp/azure-credentials.json.example | 6 + hack/aro-hcp/config.example.env | 53 +++ hack/aro-hcp/managed-identities.json.example | 60 +++ .../azure-ad-pod-identity-webhook-config.yaml | 8 + .../cluster-authentication-02-config.yaml | 6 + hack/aro-hcp/tasks/aks.yml | 250 ++++++++++ hack/aro-hcp/tasks/cluster.yml | 245 ++++++++++ hack/aro-hcp/tasks/dataplane.yml | 216 +++++++++ hack/aro-hcp/tasks/dns.yml | 245 ++++++++++ hack/aro-hcp/tasks/keyvault.yml | 431 ++++++++++++++++++ hack/aro-hcp/tasks/oidc.yml | 99 ++++ hack/aro-hcp/tasks/operator.yml | 159 +++++++ hack/aro-hcp/tasks/prereq.yml | 169 +++++++ 16 files changed, 2507 insertions(+) create mode 100644 hack/aro-hcp/.gitignore create mode 100644 hack/aro-hcp/README.md create mode 100644 hack/aro-hcp/Taskfile.yml create mode 100644 hack/aro-hcp/azure-credentials.json.example create mode 100644 hack/aro-hcp/config.example.env create mode 100644 hack/aro-hcp/managed-identities.json.example create mode 100644 hack/aro-hcp/manifests/azure-ad-pod-identity-webhook-config.yaml create mode 100644 hack/aro-hcp/manifests/cluster-authentication-02-config.yaml create mode 100644 hack/aro-hcp/tasks/aks.yml create mode 100644 hack/aro-hcp/tasks/cluster.yml create mode 100644 hack/aro-hcp/tasks/dataplane.yml create mode 100644 hack/aro-hcp/tasks/dns.yml create mode 100644 hack/aro-hcp/tasks/keyvault.yml create mode 100644 hack/aro-hcp/tasks/oidc.yml create mode 100644 hack/aro-hcp/tasks/operator.yml create mode 100644 hack/aro-hcp/tasks/prereq.yml diff --git a/hack/aro-hcp/.gitignore b/hack/aro-hcp/.gitignore new file mode 100644 index 00000000000..93968c5bc0e --- /dev/null +++ b/hack/aro-hcp/.gitignore @@ -0,0 +1,32 @@ +# Generated credentials and keys +cp-output.json +dp-output.json +serviceaccount-signer.public +serviceaccount-signer.private +external-dns-creds.json +azure-credentials.json +pull-secret.json + +# Temporary credential files +creds-tmp/ + +# Kubeconfig files +kubeconfig-* +mgmt-kubeconfig + +# OIDC files +jwks +openid-configuration + +# TLS keys +tls/ + +# Screenshots +*.png + +# Environment configuration (contains secrets) +.envrc + +# SP output files +*.sp-output.json +*.reset-output.json diff --git a/hack/aro-hcp/README.md b/hack/aro-hcp/README.md new file mode 100644 index 00000000000..ec64f01f599 --- /dev/null +++ b/hack/aro-hcp/README.md @@ -0,0 +1,336 @@ +# ARO-HCP Development Environment + +This directory contains Taskfiles for setting up an AKS management cluster with ARO-HCP (Azure Red Hat OpenShift Hosted Control Plane). + +## Prerequisites + +- [Task](https://taskfile.dev/) - Install with `brew install go-task/tap/go-task` +- [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) +- [ccoctl](https://github.com/openshift/cloud-credential-operator) - Cloud Credential Operator CLI +- [kubectl](https://kubernetes.io/docs/tasks/tools/) +- [jq](https://stedolan.github.io/jq/) +- [gum](https://github.com/charmbracelet/gum) - For styled terminal output +- [hypershift CLI](https://hypershift-docs.netlify.app/) - Either in PATH or set `HYPERSHIFT_BINARY_PATH` +- An Azure subscription with appropriate permissions +- A pull secret from [console.redhat.com](https://console.redhat.com/openshift/install/pull-secret) + +## Quick Start + +1. **Create Azure credentials file:** + ```bash + cp azure-credentials.json.example azure-credentials.json + # Edit azure-credentials.json with your SP credentials + ``` + +2. **Configure environment:** + ```bash + cp config.example.env .envrc + # Edit .envrc with your values (PREFIX, OIDC_ISSUER_NAME, RELEASE_IMAGE) + direnv allow # or source .envrc + ``` + +3. **Login to Azure:** + ```bash + task prereq:login + ``` + +4. **Create management cluster (first time):** + ```bash + task mgmt:create + ``` + +5. **Create hosted cluster:** + ```bash + task cluster:create + ``` + +6. **Destroy hosted cluster:** + ```bash + task cluster:destroy + ``` + +7. **Destroy management cluster:** + ```bash + task mgmt:destroy + ``` + +## Usage Pattern + +The typical workflow is: + +1. **Once every few months:** `task mgmt:create` - Creates a long-lived AKS management cluster +2. **Every few days:** `task cluster:create` / `task cluster:destroy` - Iterate on hosted clusters +3. **Rarely:** `task mgmt:destroy` - When done with the environment + +## Primary Tasks + +| Task | Description | +|------|-------------| +| `task mgmt:create` | Create management cluster (AKS) with all dependencies | +| `task mgmt:destroy` | Destroy management cluster | +| `task cluster:create` | Create hosted cluster (most frequent operation) | +| `task cluster:destroy` | Destroy hosted cluster | + +## Utility Tasks + +| Task | Description | +|------|-------------| +| `task prereq:login` | Login to Azure using azure-credentials.json | +| `task prereq:whoami` | Show current Azure identity vs credentials file | +| `task prereq:validate` | Validate all prerequisites including Azure identity | +| `task prereq:show-config` | Display current configuration | +| `task first-time` | One-time setup only (Key Vault, OIDC, identities) | +| `task teardown-all` | Complete teardown including one-time resources | +| `task status` | Show status of all components | + +## Standard Workflow (Recommended) + +For most users, these commands are all you need: + +**First-time setup:** +```bash +task prereq:login # Login to Azure +task mgmt:create # Create everything (~20 min) +``` + +**Daily use:** +```bash +task cluster:create # Create hosted cluster +task cluster:destroy # Destroy hosted cluster +``` + +**Cleanup:** +```bash +task mgmt:destroy # Destroy management cluster +task teardown-all # Complete teardown including persistent resources +``` + +## Step-by-Step Workflow (For Debugging) + +Use this when you need granular control for debugging or testing individual steps. + +**Legend:** +| Symbol | Meaning | +|--------|---------| +| `○` | Aggregator - only orchestrates subtasks, can skip if you run all children manually | +| `●` | Does work - has actual commands/logic, must run this task | +| `⚠` | Has internal subtasks - CANNOT skip, must use this parent task | + +``` +● prereq:login # Login using azure-credentials.json +● prereq:whoami # Verify identity matches +● prereq:validate # Validate all prerequisites + +○ mgmt:create # Aggregator - orchestrates all setup +├── ● prereq:validate +├── ○ keyvault:setup # Aggregator +│ ├── ● keyvault:create +│ ├── ● keyvault:create-sps ⚠ has internal create-sp +│ ├── ● keyvault:generate-sp-jsons +│ ├── ● keyvault:store-creds +│ └── ● keyvault:generate-cp-json +├── ● oidc:create ⚠ has internal create-issuer +│ └── ● oidc:create-keypair +├── ○ dataplane:create # Aggregator +│ ├── ● dataplane:create-identities ⚠ has internal +│ ├── ● dataplane:create-federated-creds ⚠ has internal +│ └── ● dataplane:generate-dp-json +├── ● aks:create-identities +├── ○ aks:create # Aggregator +│ ├── ● aks:create-rg +│ ├── ● aks:create-cluster +│ ├── ● aks:get-kubeconfig +│ └── ● aks:assign-kv-role +├── ○ dns:setup # Aggregator +│ ├── ● dns:create-zone +│ ├── ● dns:delegate-zone +│ ├── ● dns:create-sp +│ └── ● dns:create-secret +└── ● operator:install + └── ● operator:apply-crds + +● operator:wait # Wait for operator (standalone) +● operator:verify # Verify operator status (standalone) +● operator:logs # Show operator logs (standalone) + +● status # Show status of all components + +○ cluster:create # Aggregator +├── ● cluster:create-rgs +├── ● cluster:create-network ⚠ has internal create-nsg, create-vnet +└── ● cluster:create-hc + +● cluster:wait # Wait for cluster ready +● cluster:get-kubeconfig # Get kubeconfig +● cluster:show # Show cluster status + +○ cluster:destroy # Aggregator +├── ● cluster:destroy-hc +└── ● cluster:delete-rgs + +○ mgmt:destroy # Aggregator +├── ● operator:uninstall +├── ● dns:delete +└── ● aks:delete + +○ teardown-all # Aggregator +├── ○ cluster:destroy +├── ○ mgmt:destroy +├── ● dataplane:delete +├── ● oidc:delete +├── ● keyvault:delete +└── ● aks:delete-identities +``` + +**Important:** +- Tasks marked with `⚠` have internal subtasks that you CANNOT run directly +- Example: `oidc:create` calls both `create-keypair` (public) AND `create-issuer` (internal) +- Running only `oidc:create-keypair` will NOT create the OIDC issuer - you must run `oidc:create` + +## Task Namespaces + +### prereq: - Prerequisites and Azure Authentication +- `task prereq:login` - Login to Azure using azure-credentials.json +- `task prereq:whoami` - Show current Azure identity and verify it matches credentials file +- `task prereq:validate` - Validate tools, environment variables, and Azure identity +- `task prereq:show-config` - Display current configuration + +### keyvault: - Key Vault and Control Plane SPs +- `task keyvault:setup` - Complete Key Vault setup (idempotent) +- `task keyvault:rotate-creds` - Rotate all SP credentials +- `task keyvault:delete` - Delete Key Vault and SPs + +### oidc: - OIDC Provider +- `task oidc:create` - Create OIDC provider (idempotent) +- `task oidc:delete` - Delete OIDC issuer + +### dataplane: - Data Plane Managed Identities +- `task dataplane:create` - Complete data plane setup (idempotent) +- `task dataplane:delete` - Delete data plane identities + +### aks: - AKS Management Cluster +- `task aks:create` - Complete AKS setup +- `task aks:get-kubeconfig` - Get/restore AKS kubeconfig (re-run if file is lost) +- `task aks:delete` - Delete AKS cluster +- `task aks:show` - Show AKS status + +### dns: - External DNS +- `task dns:setup` - Complete DNS setup (idempotent) +- `task dns:delete` - Delete DNS resources + +### operator: - HyperShift Operator +- `task operator:install` - Install HyperShift operator (ARO-HCP mode) +- `task operator:verify` - Verify operator installation +- `task operator:uninstall` - Uninstall operator + +### cluster: - Hosted Cluster +- `task cluster:create-hc` - Create hosted cluster +- `task cluster:destroy-hc` - Destroy hosted cluster +- `task cluster:get-kubeconfig` - Get hosted cluster kubeconfig +- `task cluster:show` - Show hosted cluster status +- `task cluster:wait` - Wait for cluster to be ready + +## Required Configuration + +| File/Variable | Description | +|---------------|-------------| +| `AZURE_CREDS` | Path to azure-credentials.json (contains subscriptionId, tenantId, clientId, clientSecret) | +| `PULL_SECRET` | Path to pull secret file | +| `PREFIX` | Unique prefix for all resources | +| `OIDC_ISSUER_NAME` | Unique name for OIDC storage account | +| `RELEASE_IMAGE` | OpenShift release image | + +## Optional Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `LOCATION` | `eastus` | Azure region for resources | +| `PERSISTENT_RG_NAME` | `os4-common` | Shared resource group | +| `PARENT_DNS_ZONE` | `hypershift.azure.devcluster.openshift.com` | Parent DNS zone | +| `AKS_NODE_COUNT` | `3` | Number of AKS nodes | +| `AKS_NODE_VM_SIZE` | `Standard_D4s_v4` | VM size for AKS nodes | +| `NODE_POOL_REPLICAS` | `2` | Number of worker nodes | +| `HYPERSHIFT_IMAGE` | (none) | Override HyperShift operator image | +| `HYPERSHIFT_BINARY_PATH` | (none) | Path to hypershift binary | +| `KUBECONFIG` | `./mgmt-kubeconfig` | Path where mgmt cluster kubeconfig will be saved | + +## Generated Files + +The following files are generated during setup: + +| File | Description | +|------|-------------| +| `mgmt-kubeconfig` | Management (AKS) cluster kubeconfig - created by `task aks:get-kubeconfig` | +| `cp-output.json` | Control plane managed identities | +| `dp-output.json` | Data plane managed identities | +| `serviceaccount-signer.public` | SA token issuer public key | +| `serviceaccount-signer.private` | SA token issuer private key | +| `external-dns-creds.json` | External DNS credentials | +| `kubeconfig-` | Hosted cluster kubeconfig | + +**Note:** The `KUBECONFIG` environment variable is set in `.envrc` to point to `mgmt-kubeconfig`. With direnv, all `kubectl`, `hypershift`, and `oc` commands automatically use this file. If the file is lost, run `task aks:get-kubeconfig` to restore it. + +## Architecture + +This setup uses the MIv3 (Managed Identity v3) pattern: + +1. **Control Plane Components** use Service Principals with certificates stored in Azure Key Vault +2. **Data Plane Components** use Managed Identities with federated credentials +3. **AKS** uses the Key Vault Secrets Provider addon to mount certificates + +## Migrating from Shell Scripts + +If you were using the shell scripts in `contrib/managed-azure/`: + +1. Install Task: `brew install go-task/tap/go-task` +2. Copy your `user-vars.sh` values to `.envrc` +3. Run `task mgmt:create` (equivalent to `setup_all.sh --first-time`) +4. Run `task cluster:create` (equivalent to `create_basic_hosted_cluster.sh`) + +## Troubleshooting + +### Azure identity mismatch +If you see "Identity mismatch" or "Forbidden" errors, your Azure CLI is logged in as a different service principal than the one in your credentials file: +```bash +# Check current identity +task prereq:whoami + +# Login with correct credentials +task prereq:login + +# Verify +task prereq:validate +``` + +### Clean up after failed setup +If a task fails partway through (e.g., Key Vault created but SPs failed): +```bash +# Clean up Key Vault resources +task keyvault:delete + +# Fix the issue (e.g., login correctly) +task prereq:login + +# Retry +task keyvault:setup +``` + +### Check operator logs +```bash +task operator:logs +``` + +### Check hosted cluster status +```bash +task cluster:show +``` + +### Verify all components +```bash +task status +``` + +### Re-run with verbose output +```bash +task -v mgmt:create +``` diff --git a/hack/aro-hcp/Taskfile.yml b/hack/aro-hcp/Taskfile.yml new file mode 100644 index 00000000000..4445ab39006 --- /dev/null +++ b/hack/aro-hcp/Taskfile.yml @@ -0,0 +1,192 @@ +version: '3' + +vars: + # === Credentials (primary source of Azure config) === + AZURE_CREDS: '{{.AZURE_CREDS | default "azure-credentials.json"}}' + PULL_SECRET: '{{.PULL_SECRET | default "pull-secret.json"}}' + HYPERSHIFT_BINARY_PATH: '{{.HYPERSHIFT_BINARY_PATH | default ""}}' + + # === Azure Configuration (derived from AZURE_CREDS or env vars) === + SUBSCRIPTION_ID: + sh: 'jq -r ".subscriptionId // empty" {{.AZURE_CREDS}} 2>/dev/null || echo "${SUBSCRIPTION_ID:-}"' + TENANT_ID: + sh: 'jq -r ".tenantId // empty" {{.AZURE_CREDS}} 2>/dev/null || echo "${TENANT_ID:-}"' + LOCATION: '{{.LOCATION | default "eastus"}}' + + # === Naming === + PREFIX: '{{.PREFIX | default ""}}' + CLUSTER_NAME: '{{.PREFIX}}-hc' + + # === Resource Groups === + PERSISTENT_RG_NAME: '{{.PERSISTENT_RG_NAME | default "os4-common"}}' + PERSISTENT_RG_LOCATION: '{{.PERSISTENT_RG_LOCATION | default "centralus"}}' + AKS_RG: '{{.PREFIX}}-aks-rg' + AKS_CLUSTER_NAME: '{{.PREFIX}}-aks-cluster' + + # === Key Vault === + KV_NAME: '{{.PREFIX}}' + + # === OIDC Configuration === + OIDC_ISSUER_NAME: '{{.OIDC_ISSUER_NAME | default ""}}' + OIDC_ISSUER_URL: 'https://{{.OIDC_ISSUER_NAME}}.blob.core.windows.net/{{.OIDC_ISSUER_NAME}}' + + # === DNS === + PARENT_DNS_ZONE: '{{.PARENT_DNS_ZONE | default "hypershift.azure.devcluster.openshift.com"}}' + MGMT_DNS_ZONE_NAME: '{{.PREFIX}}.{{.PARENT_DNS_ZONE}}' + + # === Managed Identities JSON (MIv3 pattern) === + CP_OUTPUT_FILE: '{{.CP_OUTPUT_FILE | default "./cp-output.json"}}' + DP_OUTPUT_FILE: '{{.DP_OUTPUT_FILE | default "./dp-output.json"}}' + SA_TOKEN_ISSUER_PUBLIC_KEY_PATH: '{{.SA_TOKEN_ISSUER_PUBLIC_KEY_PATH | default "./serviceaccount-signer.public"}}' + SA_TOKEN_ISSUER_PRIVATE_KEY_PATH: '{{.SA_TOKEN_ISSUER_PRIVATE_KEY_PATH | default "./serviceaccount-signer.private"}}' + EXTERNAL_DNS_CREDS_FILE: '{{.EXTERNAL_DNS_CREDS_FILE | default "./external-dns-creds.json"}}' + + # === Image Overrides === + HYPERSHIFT_IMAGE: '{{.HYPERSHIFT_IMAGE | default ""}}' + RELEASE_IMAGE: '{{.RELEASE_IMAGE | default ""}}' + + # === AKS Configuration === + AKS_NODE_COUNT: '{{.AKS_NODE_COUNT | default "3"}}' + AKS_NODE_VM_SIZE: '{{.AKS_NODE_VM_SIZE | default "Standard_D4s_v4"}}' + AKS_K8S_VERSION: '{{.AKS_K8S_VERSION | default "1.30"}}' + MGMT_KUBECONFIG: '{{.KUBECONFIG | default "./mgmt-kubeconfig"}}' + + # === Hosted Cluster Configuration === + NODE_POOL_REPLICAS: '{{.NODE_POOL_REPLICAS | default "2"}}' + MANAGED_RG_NAME: '{{.PREFIX}}-managed-rg' + CUSTOMER_VNET_RG_NAME: '{{.PREFIX}}-customer-vnet-rg' + CUSTOMER_NSG_RG_NAME: '{{.PREFIX}}-customer-nsg-rg' + CUSTOMER_VNET_NAME: '{{.PREFIX}}-customer-vnet' + CUSTOMER_VNET_SUBNET1: '{{.PREFIX}}-customer-subnet-1' + CUSTOMER_NSG: '{{.PREFIX}}-customer-nsg' + + # === Control Plane SP Names === + AZURE_DISK_SP_NAME: 'azure-disk-{{.PREFIX}}' + AZURE_FILE_SP_NAME: 'azure-file-{{.PREFIX}}' + CLOUD_PROVIDER_SP_NAME: 'cloud-provider-{{.PREFIX}}' + CONTROL_PLANE_SP_NAME: 'cpo-{{.PREFIX}}' + IMAGE_REGISTRY_SP_NAME: 'ciro-{{.PREFIX}}' + INGRESS_SP_NAME: 'ingress-{{.PREFIX}}' + CNCC_SP_NAME: 'cncc-{{.PREFIX}}' + NODEPOOL_MGMT_SP_NAME: 'nodepool-mgmt-{{.PREFIX}}' + VELERO_SP_NAME: 'velero-{{.PREFIX}}' + + # === Data Plane MI Names === + AZURE_DISK_MI_NAME: 'azure-disk-MI-{{.PREFIX}}' + AZURE_FILE_MI_NAME: 'azure-file-MI-{{.PREFIX}}' + IMAGE_REGISTRY_MI_NAME: 'image-registry-MI-{{.PREFIX}}' + + # === AKS MI Names === + AKS_CP_MI_NAME: '{{.PREFIX}}-aks-mi' + AKS_KUBELET_MI_NAME: '{{.PREFIX}}-aks-kubelet-mi' + +includes: + prereq: + taskfile: ./tasks/prereq.yml + keyvault: + taskfile: ./tasks/keyvault.yml + oidc: + taskfile: ./tasks/oidc.yml + dataplane: + taskfile: ./tasks/dataplane.yml + aks: + taskfile: ./tasks/aks.yml + dns: + taskfile: ./tasks/dns.yml + operator: + taskfile: ./tasks/operator.yml + cluster: + taskfile: ./tasks/cluster.yml + +tasks: + default: + desc: Show available tasks + cmds: + - task --list + + # ============================================================ + # PRIMARY TASKS (the 4 most-used operations) + # ============================================================ + + mgmt:create: + desc: Create management cluster (AKS) with all dependencies + cmds: + - task: prereq:validate + # One-time setup (idempotent via status checks) + - task: keyvault:setup + - task: oidc:create + - task: dataplane:create + - task: aks:create-identities + # Per-mgmt-cluster setup + - task: aks:create + - task: dns:setup + - task: operator:install + + mgmt:destroy: + desc: Destroy management cluster (AKS) + cmds: + - task: operator:uninstall + - task: dns:delete + - task: aks:delete + + cluster:create: + desc: Create hosted cluster (most frequent operation) + preconditions: + - sh: kubectl get deployment operator -n hypershift < /dev/null 2>/dev/null + msg: "Management cluster not ready. Run 'task mgmt:create' first." + - sh: test -f {{.CP_OUTPUT_FILE}} + msg: "Control plane identities file not found at {{.CP_OUTPUT_FILE}}. Run 'task mgmt:create' first." + - sh: test -f {{.DP_OUTPUT_FILE}} + msg: "Data plane identities file not found at {{.DP_OUTPUT_FILE}}. Run 'task mgmt:create' first." + cmds: + - task: cluster:create-rgs + - task: cluster:create-network + - task: cluster:create-hc + + cluster:destroy: + desc: Destroy hosted cluster + cmds: + - task: cluster:destroy-hc + - task: cluster:delete-rgs + + # ============================================================ + # UTILITY TASKS + # ============================================================ + + first-time: + desc: One-time setup only (Key Vault, OIDC, identities) + cmds: + - task: prereq:validate + - task: keyvault:setup + - task: oidc:create + - task: dataplane:create + - task: aks:create-identities + + teardown-all: + desc: Complete teardown (including one-time resources) + cmds: + - task: cluster:destroy + - task: mgmt:destroy + - task: dataplane:delete + - task: oidc:delete + - task: keyvault:delete + - task: aks:delete-identities + + status: + desc: Show status of all components + vars: + KV_STATUS: + sh: az keyvault show --name {{.KV_NAME}} -o none 2>/dev/null && echo "Key Vault,✓ Ready,{{.KV_NAME}}" || echo "Key Vault,✗ Not Found,{{.KV_NAME}}" + OIDC_STATUS: + sh: az storage account show --name {{.OIDC_ISSUER_NAME}} -g {{.PERSISTENT_RG_NAME}} -o none 2>/dev/null && echo "OIDC Issuer,✓ Ready,{{.OIDC_ISSUER_NAME}}" || echo "OIDC Issuer,✗ Not Found,{{.OIDC_ISSUER_NAME}}" + AKS_STATUS: + sh: az aks show --name {{.AKS_CLUSTER_NAME}} -g {{.AKS_RG}} -o none 2>/dev/null && echo "AKS Cluster,✓ Ready,{{.AKS_CLUSTER_NAME}}" || echo "AKS Cluster,✗ Not Found,{{.AKS_CLUSTER_NAME}}" + OPERATOR_STATUS: + sh: kubectl get deployment operator -n hypershift < /dev/null 2>/dev/null | grep -q operator && echo "HyperShift Operator,✓ Ready,-" || echo "HyperShift Operator,✗ Not Deployed,-" + CLUSTERS_STATUS: + sh: 'HC=$(kubectl get hostedclusters -A -o name < /dev/null 2>/dev/null | wc -l | tr -d " "); [ "$HC" -gt 0 ] && echo "Hosted Clusters,✓ $HC cluster(s),-" || echo "Hosted Clusters,○ None,-"' + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "ARO-HCP Environment Status" + silent: true + - cmd: 'echo -e "Component,Status,Details\n{{.KV_STATUS}}\n{{.OIDC_STATUS}}\n{{.AKS_STATUS}}\n{{.OPERATOR_STATUS}}\n{{.CLUSTERS_STATUS}}" | gum table --print --border rounded' + silent: true diff --git a/hack/aro-hcp/azure-credentials.json.example b/hack/aro-hcp/azure-credentials.json.example new file mode 100644 index 00000000000..3dcf83ab8d2 --- /dev/null +++ b/hack/aro-hcp/azure-credentials.json.example @@ -0,0 +1,6 @@ +{ + "subscriptionId": "5f99720c-6823-4792-8a28-b0719eea0000", + "tenantId": "520cff00-44ed-0000-a731-abd623e73b09", + "clientId": "054ac5d2-0000-94b1-0000-53233016ce18", + "clientSecret": "~Jh8Q~LWGYG45nvwmzn2fnT4Laun0000" # notsecret +} diff --git a/hack/aro-hcp/config.example.env b/hack/aro-hcp/config.example.env new file mode 100644 index 00000000000..1849a629936 --- /dev/null +++ b/hack/aro-hcp/config.example.env @@ -0,0 +1,53 @@ +# ARO-HCP Configuration +# Copy this file to .envrc and fill in your values +# Then run: direnv allow + +# === REQUIRED: Azure Credentials === +# Create this file with your SP credentials: +# { +# "subscriptionId": "your-subscription-id", +# "tenantId": "your-tenant-id", +# "clientId": "your-client-id", +# "clientSecret": "your-client-secret" +# } +export AZURE_CREDS="./azure-credentials.json" + +# === REQUIRED: Naming === +export PREFIX="" # Unique prefix for all resources (e.g., "yourname") + +# === REQUIRED: OIDC === +export OIDC_ISSUER_NAME="" # Unique name for OIDC storage account (must be globally unique) + +# === REQUIRED: Images === +export RELEASE_IMAGE="" # OpenShift release image (e.g., quay.io/openshift-release-dev/ocp-release:4.17.0-x86_64) + +# === REQUIRED: Pull Secret === +export PULL_SECRET="./pull-secret.json" # Path to pull secret file + +# === OPTIONAL: Location === +export LOCATION="eastus" # Azure region for resources + +# === OPTIONAL: Resource Groups === +export PERSISTENT_RG_NAME="os4-common" # Shared/persistent resource group +export PERSISTENT_RG_LOCATION="centralus" # Location for persistent RG + +# === OPTIONAL: DNS === +export PARENT_DNS_ZONE="hypershift.azure.devcluster.openshift.com" # Parent DNS zone + +# === OPTIONAL: AKS Configuration === +export AKS_NODE_COUNT="3" # Number of AKS nodes +export AKS_NODE_VM_SIZE="Standard_D4s_v4" # VM size for AKS nodes +export AKS_K8S_VERSION="1.30" # Kubernetes version + +# === OPTIONAL: Hosted Cluster === +export NODE_POOL_REPLICAS="2" # Number of worker nodes + +# === OPTIONAL: Image Overrides === +export HYPERSHIFT_IMAGE="" # Override HyperShift operator image +export HYPERSHIFT_BINARY_PATH="" # Path to hypershift binary (if not in PATH) + +# === OPTIONAL: Kubeconfig === +# This file WILL BE CREATED by 'task aks:get-kubeconfig' after the AKS cluster is created. +# You don't need to create it manually - just set the path where you want it saved. +# With direnv, all kubectl/hypershift/oc commands will automatically use this file. +export KUBECONFIG="./mgmt-kubeconfig" diff --git a/hack/aro-hcp/managed-identities.json.example b/hack/aro-hcp/managed-identities.json.example new file mode 100644 index 00000000000..4227fcfbf42 --- /dev/null +++ b/hack/aro-hcp/managed-identities.json.example @@ -0,0 +1,60 @@ +{ + "managedIdentitiesKeyVault": { + "name": "", + "tenantID": "" + }, + "cloudProvider": { + "certificateName": "cloud-provider-", + "clientID": "", + "credentialsSecretName": "cloud-provider--json", + "objectEncoding": "utf-8" + }, + "controlPlaneOperator": { + "certificateName": "cpo-", + "clientID": "", + "credentialsSecretName": "cpo--json", + "objectEncoding": "utf-8" + }, + "disk": { + "certificateName": "azure-disk-", + "clientID": "", + "credentialsSecretName": "azure-disk--json", + "objectEncoding": "utf-8" + }, + "file": { + "certificateName": "azure-file-", + "clientID": "", + "credentialsSecretName": "azure-file--json", + "objectEncoding": "utf-8" + }, + "imageRegistry": { + "certificateName": "ciro-", + "clientID": "", + "credentialsSecretName": "ciro--json", + "objectEncoding": "utf-8" + }, + "ingress": { + "certificateName": "ingress-", + "clientID": "", + "credentialsSecretName": "ingress--json", + "objectEncoding": "utf-8" + }, + "network": { + "certificateName": "cncc-", + "clientID": "", + "credentialsSecretName": "cncc--json", + "objectEncoding": "utf-8" + }, + "nodePoolManagement": { + "certificateName": "nodepool-mgmt-", + "clientID": "", + "credentialsSecretName": "nodepool-mgmt--json", + "objectEncoding": "utf-8" + }, + "velero": { + "certificateName": "velero-", + "clientID": "", + "credentialsSecretName": "velero--json", + "objectEncoding": "utf-8" + } +} diff --git a/hack/aro-hcp/manifests/azure-ad-pod-identity-webhook-config.yaml b/hack/aro-hcp/manifests/azure-ad-pod-identity-webhook-config.yaml new file mode 100644 index 00000000000..a672f219995 --- /dev/null +++ b/hack/aro-hcp/manifests/azure-ad-pod-identity-webhook-config.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +stringData: + azure_tenant_id: 520cf09d-78ff-44ed-a731-abd623e73b09 +kind: Secret +metadata: + name: azure-credentials + namespace: openshift-cloud-credential-operator +type: Opaque \ No newline at end of file diff --git a/hack/aro-hcp/manifests/cluster-authentication-02-config.yaml b/hack/aro-hcp/manifests/cluster-authentication-02-config.yaml new file mode 100644 index 00000000000..cfbf296bcae --- /dev/null +++ b/hack/aro-hcp/manifests/cluster-authentication-02-config.yaml @@ -0,0 +1,6 @@ +apiVersion: config.openshift.io/v1 +kind: Authentication +metadata: + name: cluster +spec: + serviceAccountIssuer: https://aroahmedabdalla.blob.core.windows.net/aroahmedabdalla \ No newline at end of file diff --git a/hack/aro-hcp/tasks/aks.yml b/hack/aro-hcp/tasks/aks.yml new file mode 100644 index 00000000000..3dd7fa71eae --- /dev/null +++ b/hack/aro-hcp/tasks/aks.yml @@ -0,0 +1,250 @@ +version: '3' + +# This file handles AKS management cluster creation + +tasks: + # === AKS MANAGED IDENTITIES (one-time setup) === + + create-identities: + status: + - az identity show --name {{.AKS_CP_MI_NAME}} --resource-group {{.PERSISTENT_RG_NAME}} 2>/dev/null + - az identity show --name {{.AKS_KUBELET_MI_NAME}} --resource-group {{.PERSISTENT_RG_NAME}} 2>/dev/null + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Creating AKS Managed Identities" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - az identity create --name {{.AKS_CP_MI_NAME}} --resource-group {{.PERSISTENT_RG_NAME}} --output none + - az identity create --name {{.AKS_KUBELET_MI_NAME}} --resource-group {{.PERSISTENT_RG_NAME}} --output none + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,AKS Managed Identities + Control Plane MI,{{.AKS_CP_MI_NAME}} + Kubelet MI,{{.AKS_KUBELET_MI_NAME}} + Status,✓ Created" | gum table --print --border rounded + silent: true + + # === RESOURCE GROUP === + + create-rg: + status: + - az group show --name {{.AKS_RG}} 2>/dev/null + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Creating AKS Resource Group" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - az group create --name {{.AKS_RG}} --location {{.LOCATION}} --output none + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,AKS Resource Group + Name,{{.AKS_RG}} + Location,{{.LOCATION}} + Status,✓ Created" | gum table --print --border rounded + silent: true + + # === AKS CLUSTER === + + create-cluster: + vars: + AKS_CP_MI_ID: + sh: 'az identity show --name {{.AKS_CP_MI_NAME}} --resource-group {{.PERSISTENT_RG_NAME}} --query id -o tsv' + AKS_KUBELET_MI_ID: + sh: 'az identity show --name {{.AKS_KUBELET_MI_NAME}} --resource-group {{.PERSISTENT_RG_NAME}} --query id -o tsv' + status: + - az aks show --name {{.AKS_CLUSTER_NAME}} --resource-group {{.AKS_RG}} 2>/dev/null + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Creating AKS Cluster" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - | + az aks create \ + --resource-group {{.AKS_RG}} \ + --name {{.AKS_CLUSTER_NAME}} \ + --node-count {{.AKS_NODE_COUNT}} \ + --generate-ssh-keys \ + --load-balancer-sku standard \ + --os-sku AzureLinux \ + --node-vm-size {{.AKS_NODE_VM_SIZE}} \ + --enable-fips-image \ + --kubernetes-version {{.AKS_K8S_VERSION}} \ + --enable-addons azure-keyvault-secrets-provider \ + --enable-secret-rotation \ + --rotation-poll-interval 1m \ + --assign-identity {{.AKS_CP_MI_ID}} \ + --assign-kubelet-identity {{.AKS_KUBELET_MI_ID}} \ + --network-plugin azure \ + --network-policy azure \ + --max-pods 250 + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,AKS Cluster + Name,{{.AKS_CLUSTER_NAME}} + Resource Group,{{.AKS_RG}} + K8s Version,{{.AKS_K8S_VERSION}} + Node Count,{{.AKS_NODE_COUNT}} + VM Size,{{.AKS_NODE_VM_SIZE}} + Network Plugin,azure + Network Policy,azure + Status,✓ Created" | gum table --print --border rounded + silent: true + + get-kubeconfig: + desc: Get AKS cluster kubeconfig (re-run if file is lost) + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Getting AKS Kubeconfig" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - az aks get-credentials --resource-group {{.AKS_RG}} --name {{.AKS_CLUSTER_NAME}} --file {{.MGMT_KUBECONFIG}} --overwrite-existing + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,AKS Kubeconfig + Cluster,{{.AKS_CLUSTER_NAME}} + File,{{.MGMT_KUBECONFIG}} + Status,✓ Saved" | gum table --print --border rounded + silent: true + - cmd: gum format "○ KUBECONFIG is set via .envrc - all kubectl/oc commands will use this file" + silent: true + + assign-kv-role: + vars: + KV_OBJECT_ID: + sh: 'az aks show -n {{.AKS_CLUSTER_NAME}} -g {{.AKS_RG}} --query "addonProfiles.azureKeyvaultSecretsProvider.identity.objectId" -o tsv' + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Assigning Key Vault Role" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - | + az role assignment create \ + --assignee-object-id "{{.KV_OBJECT_ID}}" \ + --role "Key Vault Secrets User" \ + --scope /subscriptions/{{.SUBSCRIPTION_ID}}/resourceGroups/{{.PERSISTENT_RG_NAME}} \ + --assignee-principal-type ServicePrincipal + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,Key Vault Role Assignment + Role,Key Vault Secrets User + Key Vault RG,{{.PERSISTENT_RG_NAME}} + Assignee,azureKeyvaultSecretsProvider MI + Status,✓ Assigned" | gum table --print --border rounded + silent: true + + # === ORCHESTRATION === + + create: + cmds: + - task: create-rg + - task: create-cluster + - task: get-kubeconfig + - task: assign-kv-role + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,AKS Setup + Cluster,{{.AKS_CLUSTER_NAME}} + Resource Group,{{.AKS_RG}} + K8s Version,{{.AKS_K8S_VERSION}} + Node Count,{{.AKS_NODE_COUNT}} + Key Vault Role,{{.KV_NAME}} + Status,✓ Complete" | gum table --print --border rounded + silent: true + - kubectl cluster-info + + # === CLEANUP === + + delete-cluster: + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Deleting AKS Cluster" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - cmd: az aks delete --name {{.AKS_CLUSTER_NAME}} --resource-group {{.AKS_RG}} --yes --no-wait + ignore_error: true + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,Delete AKS Cluster + Cluster,{{.AKS_CLUSTER_NAME}} + Status,✓ Deletion initiated" | gum table --print --border rounded + silent: true + + delete-rg: + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Deleting AKS Resource Group" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - cmd: az group delete --name {{.AKS_RG}} --yes --no-wait + ignore_error: true + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,Delete Resource Group + Name,{{.AKS_RG}} + Status,✓ Deletion initiated" | gum table --print --border rounded + silent: true + + delete: + cmds: + - task: delete-cluster + - task: delete-rg + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,AKS Cleanup + Cluster,{{.AKS_CLUSTER_NAME}} + Resource Group,{{.AKS_RG}} + Status,✓ Complete" | gum table --print --border rounded + silent: true + + delete-identities: + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Deleting AKS Managed Identities" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - cmd: az identity delete --name {{.AKS_CP_MI_NAME}} --resource-group {{.PERSISTENT_RG_NAME}} --output none + ignore_error: true + - cmd: az identity delete --name {{.AKS_KUBELET_MI_NAME}} --resource-group {{.PERSISTENT_RG_NAME}} --output none + ignore_error: true + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,Delete AKS Identities + Control Plane MI,{{.AKS_CP_MI_NAME}} + Kubelet MI,{{.AKS_KUBELET_MI_NAME}} + Status,✓ Deleted" | gum table --print --border rounded + silent: true + + # === STATUS === + + show: + desc: Show AKS cluster status + vars: + CLUSTER_INFO: + sh: 'az aks show --name {{.AKS_CLUSTER_NAME}} --resource-group {{.AKS_RG}} --query "{name:name, powerState:powerState.code, kubernetesVersion:kubernetesVersion}" -o tsv 2>/dev/null || echo "Not found - -"' + NODEPOOL_INFO: + sh: 'az aks nodepool list --cluster-name {{.AKS_CLUSTER_NAME}} --resource-group {{.AKS_RG}} --query "[0].{name:name, count:count, vmSize:vmSize}" -o tsv 2>/dev/null || echo "Not found - -"' + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "AKS Cluster Status" + silent: true + - cmd: az aks show --name {{.AKS_CLUSTER_NAME}} --resource-group {{.AKS_RG}} -o table + ignore_error: true diff --git a/hack/aro-hcp/tasks/cluster.yml b/hack/aro-hcp/tasks/cluster.yml new file mode 100644 index 00000000000..58fc31d3c4d --- /dev/null +++ b/hack/aro-hcp/tasks/cluster.yml @@ -0,0 +1,245 @@ +version: '3' + +# This file handles hosted cluster lifecycle + +tasks: + # === RESOURCE GROUPS === + + create-rgs: + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Creating Hosted Cluster Resource Groups" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - az group create --name {{.MANAGED_RG_NAME}} --location {{.LOCATION}} --output none + - az group create --name {{.CUSTOMER_VNET_RG_NAME}} --location {{.LOCATION}} --output none + - az group create --name {{.CUSTOMER_NSG_RG_NAME}} --location {{.LOCATION}} --output none + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,Hosted Cluster RGs + Managed RG,{{.MANAGED_RG_NAME}} + VNet RG,{{.CUSTOMER_VNET_RG_NAME}} + NSG RG,{{.CUSTOMER_NSG_RG_NAME}} + Status,✓ Created" | gum table --print --border rounded + silent: true + + # === NETWORKING === + + create-nsg: + internal: true + cmds: + - az network nsg create --resource-group {{.CUSTOMER_NSG_RG_NAME}} --name {{.CUSTOMER_NSG}} --output none + + create-vnet: + internal: true + vars: + NSG_ID: + sh: 'az network nsg list --query "[?name==''{{.CUSTOMER_NSG}}''].id" -o tsv' + cmds: + - | + az network vnet create \ + --name {{.CUSTOMER_VNET_NAME}} \ + --resource-group {{.CUSTOMER_VNET_RG_NAME}} \ + --address-prefix 10.0.0.0/16 \ + --subnet-name {{.CUSTOMER_VNET_SUBNET1}} \ + --subnet-prefixes 10.0.0.0/24 \ + --nsg "{{.NSG_ID}}" \ + --output none + + create-network: + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Creating Network Infrastructure" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - task: create-nsg + - task: create-vnet + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,Network Infrastructure + NSG,{{.CUSTOMER_NSG}} + VNet,{{.CUSTOMER_VNET_NAME}} + Subnet,{{.CUSTOMER_VNET_SUBNET1}} + Status,✓ Created" | gum table --print --border rounded + silent: true + + # === HOSTED CLUSTER === + + create-hc: + vars: + VNET_ID: + sh: 'az network vnet list --query "[?name==''{{.CUSTOMER_VNET_NAME}}''].id" -o tsv' + SUBNET_ID: + sh: 'az network vnet subnet show --vnet-name {{.CUSTOMER_VNET_NAME}} --name {{.CUSTOMER_VNET_SUBNET1}} --resource-group {{.CUSTOMER_VNET_RG_NAME}} --query id --output tsv' + NSG_ID: + sh: 'az network nsg list --query "[?name==''{{.CUSTOMER_NSG}}''].id" -o tsv' + HYPERSHIFT_CMD: + sh: 'test -n "{{.HYPERSHIFT_BINARY_PATH}}" && echo "{{.HYPERSHIFT_BINARY_PATH}}/hypershift" || echo "hypershift"' + preconditions: + - sh: test -f {{.CP_OUTPUT_FILE}} + msg: "Control plane identities file not found at {{.CP_OUTPUT_FILE}}" + - sh: test -f {{.DP_OUTPUT_FILE}} + msg: "Data plane identities file not found at {{.DP_OUTPUT_FILE}}" + - sh: test -f {{.PULL_SECRET}} + msg: "Pull secret not found at {{.PULL_SECRET}}" + - sh: test -f {{.SA_TOKEN_ISSUER_PRIVATE_KEY_PATH}} + msg: "SA token issuer private key not found at {{.SA_TOKEN_ISSUER_PRIVATE_KEY_PATH}}" + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Creating Hosted Cluster {{.CLUSTER_NAME}}" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - | + {{.HYPERSHIFT_CMD}} create cluster azure \ + --name {{.CLUSTER_NAME}} \ + --azure-creds {{.AZURE_CREDS}} \ + --location {{.LOCATION}} \ + --node-pool-replicas {{.NODE_POOL_REPLICAS}} \ + --base-domain {{.PARENT_DNS_ZONE}} \ + --pull-secret {{.PULL_SECRET}} \ + --generate-ssh \ + --release-image {{.RELEASE_IMAGE}} \ + --external-dns-domain {{.MGMT_DNS_ZONE_NAME}} \ + --resource-group-name {{.MANAGED_RG_NAME}} \ + --vnet-id "{{.VNET_ID}}" \ + --subnet-id "{{.SUBNET_ID}}" \ + --network-security-group-id "{{.NSG_ID}}" \ + --sa-token-issuer-private-key-path {{.SA_TOKEN_ISSUER_PRIVATE_KEY_PATH}} \ + --oidc-issuer-url {{.OIDC_ISSUER_URL}} \ + --annotations hypershift.openshift.io/pod-security-admission-label-override=baseline \ + --marketplace-publisher azureopenshift \ + --marketplace-offer aro4 \ + --marketplace-sku aro_417 \ + --marketplace-version 417.94.20240701 \ + --dns-zone-rg-name {{.PERSISTENT_RG_NAME}} \ + --assign-service-principal-roles \ + --managed-identities-file {{.CP_OUTPUT_FILE}} \ + --data-plane-identities-file {{.DP_OUTPUT_FILE}} \ + --diagnostics-storage-account-type Managed + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,Create Hosted Cluster + Name,{{.CLUSTER_NAME}} + Location,{{.LOCATION}} + Release Image,{{.RELEASE_IMAGE}} + Base Domain,{{.PARENT_DNS_ZONE}} + External DNS,{{.MGMT_DNS_ZONE_NAME}} + OIDC Issuer,{{.OIDC_ISSUER_URL}} + Node Replicas,{{.NODE_POOL_REPLICAS}} + Managed RG,{{.MANAGED_RG_NAME}} + Kubeconfig,./kubeconfig-{{.CLUSTER_NAME}} + Status,✓ Created" | gum table --print --border rounded + silent: true + - cmd: gum format "○ Run 'task cluster:wait' to wait for it to be ready" + silent: true + + # === CLUSTER STATUS === + + wait: + desc: Wait for hosted cluster to be ready + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Waiting for Hosted Cluster" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - kubectl wait --for=condition=Available hostedcluster/{{.CLUSTER_NAME}} -n clusters --timeout=1800s + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,Wait for Cluster + Name,{{.CLUSTER_NAME}} + Namespace,clusters + Status,✓ Ready" | gum table --print --border rounded + silent: true + + get-kubeconfig: + desc: Get hosted cluster kubeconfig + vars: + HYPERSHIFT_CMD: + sh: 'test -n "{{.HYPERSHIFT_BINARY_PATH}}" && echo "{{.HYPERSHIFT_BINARY_PATH}}/hypershift" || echo "hypershift"' + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Getting Hosted Cluster Kubeconfig" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - '{{.HYPERSHIFT_CMD}} create kubeconfig --name {{.CLUSTER_NAME}} --namespace clusters > ./kubeconfig-{{.CLUSTER_NAME}}' + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,Get Kubeconfig + Cluster,{{.CLUSTER_NAME}} + File,./kubeconfig-{{.CLUSTER_NAME}} + Status,✓ Saved" | gum table --print --border rounded + silent: true + - cmd: 'gum format "○ To use: export KUBECONFIG=./kubeconfig-{{.CLUSTER_NAME}}"' + silent: true + + show: + desc: Show hosted cluster status + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Hosted Cluster Status" + silent: true + - cmd: kubectl get hostedcluster {{.CLUSTER_NAME}} -n clusters + ignore_error: true + - cmd: kubectl get nodepools -n clusters + ignore_error: true + + # === DESTRUCTION === + + destroy-hc: + vars: + HYPERSHIFT_CMD: + sh: 'test -n "{{.HYPERSHIFT_BINARY_PATH}}" && echo "{{.HYPERSHIFT_BINARY_PATH}}/hypershift" || echo "hypershift"' + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Destroying Hosted Cluster" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - cmd: | + {{.HYPERSHIFT_CMD}} destroy cluster azure \ + --name {{.CLUSTER_NAME}} \ + --azure-creds {{.AZURE_CREDS}} \ + --resource-group-name {{.MANAGED_RG_NAME}} \ + --location {{.LOCATION}} + ignore_error: true + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,Destroy Hosted Cluster + Name,{{.CLUSTER_NAME}} + Status,✓ Destroyed" | gum table --print --border rounded + silent: true + + delete-rgs: + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Deleting Hosted Cluster Resource Groups" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - cmd: az group delete --name {{.CUSTOMER_VNET_RG_NAME}} --yes --no-wait + ignore_error: true + - cmd: az group delete --name {{.CUSTOMER_NSG_RG_NAME}} --yes --no-wait + ignore_error: true + - cmd: az group delete --name {{.MANAGED_RG_NAME}} --yes --no-wait + ignore_error: true + - rm -f ./kubeconfig-{{.CLUSTER_NAME}} + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,Delete Resource Groups + Managed RG,{{.MANAGED_RG_NAME}} + VNet RG,{{.CUSTOMER_VNET_RG_NAME}} + NSG RG,{{.CUSTOMER_NSG_RG_NAME}} + Kubeconfig,./kubeconfig-{{.CLUSTER_NAME}} + Status,✓ Deletion initiated" | gum table --print --border rounded + silent: true diff --git a/hack/aro-hcp/tasks/dataplane.yml b/hack/aro-hcp/tasks/dataplane.yml new file mode 100644 index 00000000000..be0587cfc72 --- /dev/null +++ b/hack/aro-hcp/tasks/dataplane.yml @@ -0,0 +1,216 @@ +version: '3' + +# This file handles Data Plane Managed Identities for workload identity + +vars: + # Data plane managed identity names + DP_MI_NAMES: + - '{{.AZURE_DISK_MI_NAME}}' + - '{{.AZURE_FILE_MI_NAME}}' + - '{{.IMAGE_REGISTRY_MI_NAME}}' + + # Federated credential configurations + FEDERATED_CREDS: + - {identity: '{{.AZURE_DISK_MI_NAME}}', name: '{{.AZURE_DISK_MI_NAME}}-fed-id', subject: 'system:serviceaccount:openshift-cluster-csi-drivers:azure-disk-csi-driver-node-sa'} + - {identity: '{{.AZURE_FILE_MI_NAME}}', name: '{{.AZURE_FILE_MI_NAME}}-fed-id', subject: 'system:serviceaccount:openshift-cluster-csi-drivers:azure-file-csi-driver-node-sa'} + - {identity: '{{.IMAGE_REGISTRY_MI_NAME}}', name: '{{.IMAGE_REGISTRY_MI_NAME}}-fed-id', subject: 'system:serviceaccount:openshift-image-registry:registry'} + +tasks: + # === MANAGED IDENTITY CREATION === + + create-identity: + internal: true + requires: + vars: [MI_NAME] + cmds: + - az identity create --name {{.MI_NAME}} --resource-group {{.PERSISTENT_RG_NAME}} --output none + + create-identities: + status: + - az identity show --name {{.AZURE_DISK_MI_NAME}} --resource-group {{.PERSISTENT_RG_NAME}} 2>/dev/null + - az identity show --name {{.AZURE_FILE_MI_NAME}} --resource-group {{.PERSISTENT_RG_NAME}} 2>/dev/null + - az identity show --name {{.IMAGE_REGISTRY_MI_NAME}} --resource-group {{.PERSISTENT_RG_NAME}} 2>/dev/null + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Creating Data Plane Managed Identities" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - for: {var: DP_MI_NAMES, as: mi_name} + task: create-identity + vars: + MI_NAME: '{{.mi_name}}' + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,Data Plane Identities + Disk MI,{{.AZURE_DISK_MI_NAME}} + File MI,{{.AZURE_FILE_MI_NAME}} + Registry MI,{{.IMAGE_REGISTRY_MI_NAME}} + Resource Group,{{.PERSISTENT_RG_NAME}} + Status,✓ Created" | gum table --print --border rounded + silent: true + + # === FEDERATED CREDENTIALS === + + create-federated-cred: + internal: true + requires: + vars: [IDENTITY_NAME, CRED_NAME, SUBJECT] + cmds: + - | + az identity federated-credential create \ + --name {{.CRED_NAME}} \ + --identity-name {{.IDENTITY_NAME}} \ + --resource-group {{.PERSISTENT_RG_NAME}} \ + --issuer {{.OIDC_ISSUER_URL}} \ + --subject {{.SUBJECT}} \ + --audience openshift \ + --output none + + create-federated-creds: + status: + - az identity federated-credential show --name {{.AZURE_DISK_MI_NAME}}-fed-id --identity-name {{.AZURE_DISK_MI_NAME}} --resource-group {{.PERSISTENT_RG_NAME}} 2>/dev/null + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Creating Federated Credentials" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - for: {var: FEDERATED_CREDS, as: cred} + task: create-federated-cred + vars: + IDENTITY_NAME: '{{.cred.identity}}' + CRED_NAME: '{{.cred.name}}' + SUBJECT: '{{.cred.subject}}' + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,Federated Credentials + Disk Fed ID,{{.AZURE_DISK_MI_NAME}}-fed-id + File Fed ID,{{.AZURE_FILE_MI_NAME}}-fed-id + Registry Fed ID,{{.IMAGE_REGISTRY_MI_NAME}}-fed-id + OIDC Issuer,{{.OIDC_ISSUER_URL}} + Status,✓ Created" | gum table --print --border rounded + silent: true + + # === DP-OUTPUT.JSON GENERATION === + + generate-dp-json: + vars: + DISK_CLIENT_ID: + sh: 'az identity show --name {{.AZURE_DISK_MI_NAME}} --resource-group {{.PERSISTENT_RG_NAME}} --query clientId -o tsv' + FILE_CLIENT_ID: + sh: 'az identity show --name {{.AZURE_FILE_MI_NAME}} --resource-group {{.PERSISTENT_RG_NAME}} --query clientId -o tsv' + IMAGE_REGISTRY_CLIENT_ID: + sh: 'az identity show --name {{.IMAGE_REGISTRY_MI_NAME}} --resource-group {{.PERSISTENT_RG_NAME}} --query clientId -o tsv' + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Generating Data Plane Output" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - | + cat > {{.DP_OUTPUT_FILE}} </dev/null || echo "Not found"' + FILE_STATUS: + sh: 'az identity show --name {{.AZURE_FILE_MI_NAME}} --resource-group {{.PERSISTENT_RG_NAME}} --query clientId -o tsv 2>/dev/null || echo "Not found"' + REGISTRY_STATUS: + sh: 'az identity show --name {{.IMAGE_REGISTRY_MI_NAME}} --resource-group {{.PERSISTENT_RG_NAME}} --query clientId -o tsv 2>/dev/null || echo "Not found"' + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Data Plane Managed Identities" + silent: true + - cmd: | + echo "Identity,Client ID + {{.AZURE_DISK_MI_NAME}},{{.DISK_STATUS}} + {{.AZURE_FILE_MI_NAME}},{{.FILE_STATUS}} + {{.IMAGE_REGISTRY_MI_NAME}},{{.REGISTRY_STATUS}}" | gum table --print --border rounded + silent: true diff --git a/hack/aro-hcp/tasks/dns.yml b/hack/aro-hcp/tasks/dns.yml new file mode 100644 index 00000000000..55dd8e5adc2 --- /dev/null +++ b/hack/aro-hcp/tasks/dns.yml @@ -0,0 +1,245 @@ +version: '3' + +# This file handles External DNS setup + +vars: + EXTERNAL_DNS_SP_NAME: '{{.PREFIX}}-ExternalDnsServicePrincipal' + +tasks: + # === DNS ZONE === + + create-zone: + status: + - az network dns zone show --resource-group {{.PERSISTENT_RG_NAME}} --name {{.MGMT_DNS_ZONE_NAME}} 2>/dev/null + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Creating DNS Zone" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - az network dns zone create --resource-group {{.PERSISTENT_RG_NAME}} --name {{.MGMT_DNS_ZONE_NAME}} + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,DNS Zone + Name,{{.MGMT_DNS_ZONE_NAME}} + Resource Group,{{.PERSISTENT_RG_NAME}} + Status,✓ Created" | gum table --print --border rounded + silent: true + + add-ns-record: + internal: true + requires: + vars: [NS_NAME] + cmds: + - az network dns record-set ns add-record --resource-group {{.PERSISTENT_RG_NAME}} --zone-name {{.PARENT_DNS_ZONE}} --record-set-name {{.PREFIX}} --nsdname {{.NS_NAME}} + + delegate-zone: + vars: + NAME_SERVERS: + sh: 'az network dns zone show --resource-group {{.PERSISTENT_RG_NAME}} --name {{.MGMT_DNS_ZONE_NAME}} --query nameServers -o tsv | tr "\n" " "' + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Delegating DNS Zone" + silent: true + - cmd: gum style --faint "Output:" + silent: true + # Delete existing record set if any + - cmd: az network dns record-set ns delete --resource-group {{.PERSISTENT_RG_NAME}} --zone-name {{.PARENT_DNS_ZONE}} --name {{.PREFIX}} -y + ignore_error: true + # Add name servers to parent zone + - for: { var: NAME_SERVERS } + task: add-ns-record + vars: + NS_NAME: "{{.ITEM}}" + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,DNS Zone Delegation + Zone,{{.MGMT_DNS_ZONE_NAME}} + Parent Zone,{{.PARENT_DNS_ZONE}} + NS Record,{{.PREFIX}} + Status,✓ Delegated" | gum table --print --border rounded + silent: true + + # === SERVICE PRINCIPAL === + + create-sp: + status: + - test -f {{.EXTERNAL_DNS_CREDS_FILE}} + vars: + DNS_ZONE_ID: + sh: 'az network dns zone show --name {{.MGMT_DNS_ZONE_NAME}} --resource-group {{.PERSISTENT_RG_NAME}} --query "id" --output tsv 2>/dev/null || echo ""' + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Creating External DNS Service Principal" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - | + DNS_SP=$(az ad sp create-for-rbac --name {{.EXTERNAL_DNS_SP_NAME}}) + EXTERNAL_DNS_SP_APP_ID=$(echo "$DNS_SP" | jq -r '.appId') + EXTERNAL_DNS_SP_PASSWORD=$(echo "$DNS_SP" | jq -r '.password') + + az role assignment create \ + --role "Reader" \ + --assignee "${EXTERNAL_DNS_SP_APP_ID}" \ + --scope "{{.DNS_ZONE_ID}}" + + az role assignment create \ + --role "Contributor" \ + --assignee "${EXTERNAL_DNS_SP_APP_ID}" \ + --scope "{{.DNS_ZONE_ID}}" + + cat > {{.EXTERNAL_DNS_CREDS_FILE}} </dev/null || echo ""' + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Deleting External DNS Service Principal" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - cmd: | + if [ -n "{{.APP_ID}}" ]; then + az ad app delete --id {{.APP_ID}} + fi + ignore_error: true + - rm -f {{.EXTERNAL_DNS_CREDS_FILE}} + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,Delete DNS SP + Name,{{.EXTERNAL_DNS_SP_NAME}} + Status,✓ Deleted" | gum table --print --border rounded + silent: true + + delete-zone: + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Deleting DNS Zone" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - cmd: az network dns record-set ns delete --resource-group {{.PERSISTENT_RG_NAME}} --zone-name {{.PARENT_DNS_ZONE}} --name {{.PREFIX}} -y + ignore_error: true + - cmd: az network dns zone delete --resource-group {{.PERSISTENT_RG_NAME}} --name {{.MGMT_DNS_ZONE_NAME}} --yes + ignore_error: true + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,Delete DNS Zone + Zone,{{.MGMT_DNS_ZONE_NAME}} + Status,✓ Deleted" | gum table --print --border rounded + silent: true + + delete: + cmds: + - task: delete-secret + - task: delete-sp + - task: delete-zone + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,External DNS Cleanup + Zone,{{.MGMT_DNS_ZONE_NAME}} + NS Record,{{.PREFIX}} (from {{.PARENT_DNS_ZONE}}) + SP,{{.EXTERNAL_DNS_SP_NAME}} + Secret,azure-config-file + Status,✓ Complete" | gum table --print --border rounded + silent: true + + # === STATUS === + + show: + vars: + ZONE_EXISTS: + sh: 'az network dns zone show --resource-group {{.PERSISTENT_RG_NAME}} --name {{.MGMT_DNS_ZONE_NAME}} -o none 2>/dev/null && echo "✓ Exists" || echo "✗ Not found"' + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "DNS Zone Status" + silent: true + - cmd: az network dns zone show --resource-group {{.PERSISTENT_RG_NAME}} --name {{.MGMT_DNS_ZONE_NAME}} -o table + ignore_error: true diff --git a/hack/aro-hcp/tasks/keyvault.yml b/hack/aro-hcp/tasks/keyvault.yml new file mode 100644 index 00000000000..589b0135ff3 --- /dev/null +++ b/hack/aro-hcp/tasks/keyvault.yml @@ -0,0 +1,431 @@ +version: '3' + +# This file handles Key Vault and Control Plane Service Principals (MIv3 pattern) +# The SPs are created with certificates stored in Key Vault, which are then +# mounted onto pods via SecretProviderClass. + +vars: + # List of control plane SP names for iteration + CP_SP_NAMES: + - '{{.AZURE_DISK_SP_NAME}}' + - '{{.AZURE_FILE_SP_NAME}}' + - '{{.CLOUD_PROVIDER_SP_NAME}}' + - '{{.CONTROL_PLANE_SP_NAME}}' + - '{{.IMAGE_REGISTRY_SP_NAME}}' + - '{{.INGRESS_SP_NAME}}' + - '{{.CNCC_SP_NAME}}' + - '{{.NODEPOOL_MGMT_SP_NAME}}' + - '{{.VELERO_SP_NAME}}' + +tasks: + # === KEY VAULT CREATION === + + create: + status: + - az keyvault show --name {{.KV_NAME}} --resource-group {{.PERSISTENT_RG_NAME}} 2>/dev/null + vars: + # Get the SP's object ID from the clientId in azure-credentials.json + SP_CLIENT_ID: + sh: 'jq -r ".clientId // empty" {{.AZURE_CREDS}}' + SP_OBJECT_ID: + sh: 'az ad sp show --id $(jq -r ".clientId" {{.AZURE_CREDS}}) --query id -o tsv' + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Creating Key Vault" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - az keyvault create --name {{.KV_NAME}} --resource-group {{.PERSISTENT_RG_NAME}} --location {{.PERSISTENT_RG_LOCATION}} --enable-rbac-authorization + - | + az role assignment create \ + --assignee {{.SP_OBJECT_ID}} \ + --scope /subscriptions/{{.SUBSCRIPTION_ID}}/resourceGroups/{{.PERSISTENT_RG_NAME}}/providers/Microsoft.KeyVault/vaults/{{.KV_NAME}} \ + --role "Key Vault Administrator" + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,Key Vault + Name,{{.KV_NAME}} + Resource Group,{{.PERSISTENT_RG_NAME}} + Location,{{.PERSISTENT_RG_LOCATION}} + Status,✓ Created" | gum table --print --border rounded + silent: true + + # === SERVICE PRINCIPAL CREATION === + + create-sp: + desc: Create a single service principal with certificate in Key Vault + internal: true + requires: + vars: [SP_NAME] + cmds: + - | + az ad sp create-for-rbac \ + --name "{{.SP_NAME}}" \ + --create-cert \ + --cert "{{.SP_NAME}}" \ + --keyvault "{{.KV_NAME}}" \ + --query "{clientID: appId, certificateName: '{{.SP_NAME}}'}" \ + -o json > ./creds-tmp/{{.SP_NAME}}.sp-output.json + + create-sps: + status: + - test -f ./creds-tmp/{{.CLOUD_PROVIDER_SP_NAME}}.sp-output.json + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Creating Control Plane Service Principals" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - mkdir -p ./creds-tmp + - for: {var: CP_SP_NAMES, as: sp_name} + task: create-sp + vars: + SP_NAME: '{{.sp_name}}' + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,Control Plane SPs + cloudProvider,{{.CLOUD_PROVIDER_SP_NAME}} + controlPlane,{{.CONTROL_PLANE_SP_NAME}} + disk,{{.AZURE_DISK_SP_NAME}} + file,{{.AZURE_FILE_SP_NAME}} + imageRegistry,{{.IMAGE_REGISTRY_SP_NAME}} + ingress,{{.INGRESS_SP_NAME}} + network,{{.CNCC_SP_NAME}} + nodePoolMgmt,{{.NODEPOOL_MGMT_SP_NAME}} + velero,{{.VELERO_SP_NAME}} + Key Vault,{{.KV_NAME}} + Status,✓ Created" | gum table --print --border rounded + silent: true + + # === CREDENTIAL JSON GENERATION === + + generate-sp-json: + desc: Generate JSON credentials file for a single SP + internal: true + requires: + vars: [SP_NAME] + cmds: + - | + CERT_DETAILS=$(az keyvault secret show \ + --vault-name {{.KV_NAME}} \ + --name {{.SP_NAME}} \ + --query "{value: value, notBefore: attributes.notBefore, expires: attributes.expires}" \ + -o json) + CLIENT_SECRET=$(echo $CERT_DETAILS | jq -r '.value') + NOT_BEFORE=$(echo $CERT_DETAILS | jq -r '.notBefore') + NOT_AFTER=$(echo $CERT_DETAILS | jq -r '.expires') + + SP_DETAILS=$(az ad sp list \ + --display-name {{.SP_NAME}} \ + --query "[0].{client_id: appId, tenant_id: appOwnerOrganizationId}" \ + -o json) + CLIENT_ID=$(echo $SP_DETAILS | jq -r '.client_id') + TENANT_ID=$(echo $SP_DETAILS | jq -r '.tenant_id') + + cat > ./creds-tmp/{{.SP_NAME}}.json < /dev/null + + store-creds: + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Storing Credentials to Key Vault" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - for: {var: CP_SP_NAMES, as: sp_name} + task: store-sp-to-vault + vars: + SP_NAME: '{{.sp_name}}' + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,Store Credentials + Key Vault,{{.KV_NAME}} + Secrets,*-json (9 total) + Example,{{.CLOUD_PROVIDER_SP_NAME}}-json + Status,✓ Stored" | gum table --print --border rounded + silent: true + + # === CP-OUTPUT.JSON GENERATION === + + generate-cp-json: + vars: + CLOUD_PROVIDER_CLIENT_ID: + sh: 'jq -r ".clientID" ./creds-tmp/{{.CLOUD_PROVIDER_SP_NAME}}.sp-output.json' + CPO_CLIENT_ID: + sh: 'jq -r ".clientID" ./creds-tmp/{{.CONTROL_PLANE_SP_NAME}}.sp-output.json' + DISK_CLIENT_ID: + sh: 'jq -r ".clientID" ./creds-tmp/{{.AZURE_DISK_SP_NAME}}.sp-output.json' + FILE_CLIENT_ID: + sh: 'jq -r ".clientID" ./creds-tmp/{{.AZURE_FILE_SP_NAME}}.sp-output.json' + IMAGE_REGISTRY_CLIENT_ID: + sh: 'jq -r ".clientID" ./creds-tmp/{{.IMAGE_REGISTRY_SP_NAME}}.sp-output.json' + INGRESS_CLIENT_ID: + sh: 'jq -r ".clientID" ./creds-tmp/{{.INGRESS_SP_NAME}}.sp-output.json' + NETWORK_CLIENT_ID: + sh: 'jq -r ".clientID" ./creds-tmp/{{.CNCC_SP_NAME}}.sp-output.json' + NODEPOOL_MGMT_CLIENT_ID: + sh: 'jq -r ".clientID" ./creds-tmp/{{.NODEPOOL_MGMT_SP_NAME}}.sp-output.json' + VELERO_CLIENT_ID: + sh: 'jq -r ".clientID" ./creds-tmp/{{.VELERO_SP_NAME}}.sp-output.json' + KV_TENANT_ID: + sh: 'az account show --query tenantId -o tsv' + preconditions: + - sh: test -d ./creds-tmp + msg: "creds-tmp directory not found. Run create-sps first." + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Generating Control Plane Output" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - | + cat > {{.CP_OUTPUT_FILE}} < ./creds-tmp/{{.SP_NAME}}.reset-output.json + - task: generate-sp-json + vars: + SP_NAME: '{{.SP_NAME}}' + - task: store-sp-to-vault + vars: + SP_NAME: '{{.SP_NAME}}' + + rotate-creds: + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Rotating SP Credentials" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - for: {var: CP_SP_NAMES, as: sp_name} + task: rotate-sp-creds + vars: + SP_NAME: '{{.sp_name}}' + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,Credential Rotation + Key Vault,{{.KV_NAME}} + SPs Rotated,cloudProvider controlPlane disk file imageRegistry ingress network nodePoolMgmt velero + Status,✓ Rotated" | gum table --print --border rounded + silent: true + + # === CLEANUP === + + delete-sp: + desc: Delete a single service principal + internal: true + requires: + vars: [SP_NAME] + vars: + APP_ID: + sh: 'az ad sp list --display-name {{.SP_NAME}} --query "[0].appId" -o tsv 2>/dev/null || echo ""' + cmds: + - cmd: | + if [ -n "{{.APP_ID}}" ]; then + az ad app delete --id {{.APP_ID}} + fi + ignore_error: true + + delete: + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Deleting Key Vault Resources" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - for: {var: CP_SP_NAMES, as: sp_name} + task: delete-sp + vars: + SP_NAME: '{{.sp_name}}' + - cmd: az keyvault delete --name {{.KV_NAME}} --resource-group {{.PERSISTENT_RG_NAME}} + ignore_error: true + - cmd: az keyvault purge --name {{.KV_NAME}} + ignore_error: true + - rm -rf ./creds-tmp + - rm -f {{.CP_OUTPUT_FILE}} + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,Key Vault Deletion + Key Vault,{{.KV_NAME}} + SPs Deleted,cloudProvider controlPlane disk file imageRegistry ingress network nodePoolMgmt velero + Status,✓ Deleted" | gum table --print --border rounded + silent: true + + cleanup-local: + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Cleaning Up Local Files" + silent: true + - rm -rf ./creds-tmp + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,Local Cleanup + Directory,./creds-tmp + Status,✓ Removed" | gum table --print --border rounded + silent: true diff --git a/hack/aro-hcp/tasks/oidc.yml b/hack/aro-hcp/tasks/oidc.yml new file mode 100644 index 00000000000..37d71af2be3 --- /dev/null +++ b/hack/aro-hcp/tasks/oidc.yml @@ -0,0 +1,99 @@ +version: '3' + +# This file handles OIDC provider setup using ccoctl + +tasks: + create-keypair: + status: + - test -f {{.SA_TOKEN_ISSUER_PRIVATE_KEY_PATH}} + - test -f {{.SA_TOKEN_ISSUER_PUBLIC_KEY_PATH}} + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Creating OIDC Key Pair" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - ccoctl azure create-key-pair --output-dir . + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,OIDC Key Pair + Public Key,{{.SA_TOKEN_ISSUER_PUBLIC_KEY_PATH}} + Private Key,{{.SA_TOKEN_ISSUER_PRIVATE_KEY_PATH}} + Status,✓ Created" | gum table --print --border rounded + silent: true + + create-issuer: + internal: true + preconditions: + - sh: test -f {{.SA_TOKEN_ISSUER_PUBLIC_KEY_PATH}} + msg: "Service account public key not found. Run 'task oidc:create-keypair' first." + status: + - az storage account show --name {{.OIDC_ISSUER_NAME}} --resource-group {{.PERSISTENT_RG_NAME}} 2>/dev/null + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Creating OIDC Issuer" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - | + ccoctl azure create-oidc-issuer \ + --oidc-resource-group-name {{.PERSISTENT_RG_NAME}} \ + --tenant-id {{.TENANT_ID}} \ + --region {{.PERSISTENT_RG_LOCATION}} \ + --name {{.OIDC_ISSUER_NAME}} \ + --subscription-id {{.SUBSCRIPTION_ID}} \ + --public-key-file {{.SA_TOKEN_ISSUER_PUBLIC_KEY_PATH}} + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,OIDC Issuer + Name,{{.OIDC_ISSUER_NAME}} + URL,{{.OIDC_ISSUER_URL}} + Resource Group,{{.PERSISTENT_RG_NAME}} + Status,✓ Created" | gum table --print --border rounded + silent: true + + create: + cmds: + - task: create-keypair + - task: create-issuer + + show: + vars: + STORAGE_EXISTS: + sh: 'az storage account show --name {{.OIDC_ISSUER_NAME}} --resource-group {{.PERSISTENT_RG_NAME}} -o none 2>/dev/null && echo "✓ Exists" || echo "✗ Not found"' + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "OIDC Issuer Information" + silent: true + - cmd: | + echo "Setting,Value + Issuer Name,{{.OIDC_ISSUER_NAME}} + Issuer URL,{{.OIDC_ISSUER_URL}} + Resource Group,{{.PERSISTENT_RG_NAME}} + Storage Account,{{.STORAGE_EXISTS}}" | gum table --print --border rounded + silent: true + + delete: + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Deleting OIDC Issuer" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - cmd: | + ccoctl azure delete \ + --storage-account-name {{.OIDC_ISSUER_NAME}} \ + --region {{.PERSISTENT_RG_LOCATION}} \ + --oidc-resource-group-name {{.PERSISTENT_RG_NAME}} \ + --subscription-id {{.SUBSCRIPTION_ID}} \ + --name {{.OIDC_ISSUER_NAME}} + ignore_error: true + - rm -f {{.SA_TOKEN_ISSUER_PUBLIC_KEY_PATH}} {{.SA_TOKEN_ISSUER_PRIVATE_KEY_PATH}} + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,OIDC Issuer + Name,{{.OIDC_ISSUER_NAME}} + Status,✓ Deleted" | gum table --print --border rounded + silent: true diff --git a/hack/aro-hcp/tasks/operator.yml b/hack/aro-hcp/tasks/operator.yml new file mode 100644 index 00000000000..c85e82138f7 --- /dev/null +++ b/hack/aro-hcp/tasks/operator.yml @@ -0,0 +1,159 @@ +version: '3' + +# This file handles HyperShift operator installation on AKS (ARO-HCP mode) + +tasks: + # === CRD INSTALLATION === + + apply-crds: + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Applying Missing CRDs" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - kubectl apply -f https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/main/example/prometheus-operator-crd/monitoring.coreos.com_servicemonitors.yaml + - kubectl apply -f https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/main/example/prometheus-operator-crd/monitoring.coreos.com_prometheusrules.yaml + - kubectl apply -f https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/main/example/prometheus-operator-crd/monitoring.coreos.com_podmonitors.yaml + - kubectl apply -f https://raw.githubusercontent.com/openshift/api/6bababe9164ea6c78274fd79c94a3f951f8d5ab2/route/v1/zz_generated.crd-manifests/routes.crd.yaml + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,Apply CRDs + CRDs,ServiceMonitors PrometheusRules PodMonitors Routes + Status,✓ Applied" | gum table --print --border rounded + silent: true + + # === OPERATOR INSTALLATION === + + install: + desc: Install HyperShift operator (ARO-HCP mode) + status: + - kubectl get deployment operator -n hypershift < /dev/null 2>/dev/null + vars: + KV_CLIENT_ID: + sh: 'az aks show -n {{.AKS_CLUSTER_NAME}} -g {{.AKS_RG}} --query "addonProfiles.azureKeyvaultSecretsProvider.identity.clientId" -o tsv' + HYPERSHIFT_CMD: + sh: 'test -n "{{.HYPERSHIFT_BINARY_PATH}}" && echo "{{.HYPERSHIFT_BINARY_PATH}}/hypershift" || echo "hypershift"' + preconditions: + - sh: test -f {{.PULL_SECRET}} + msg: "Pull secret not found at {{.PULL_SECRET}}" + - sh: test -f {{.EXTERNAL_DNS_CREDS_FILE}} + msg: "External DNS credentials not found at {{.EXTERNAL_DNS_CREDS_FILE}}" + cmds: + - task: apply-crds + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Installing HyperShift Operator" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - | + IMAGE_FLAG="" + if [ -n "{{.HYPERSHIFT_IMAGE}}" ]; then + IMAGE_FLAG="--hypershift-image {{.HYPERSHIFT_IMAGE}}" + fi + + {{.HYPERSHIFT_CMD}} install \ + --enable-conversion-webhook=false \ + --external-dns-provider=azure \ + --external-dns-credentials {{.EXTERNAL_DNS_CREDS_FILE}} \ + --pull-secret {{.PULL_SECRET}} \ + --external-dns-domain-filter {{.MGMT_DNS_ZONE_NAME}} \ + --managed-service ARO-HCP \ + --aro-hcp-key-vault-users-client-id "{{.KV_CLIENT_ID}}" \ + --limit-crd-install Azure \ + --tech-preview-no-upgrade \ + $IMAGE_FLAG + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,Install HyperShift Operator + Namespace,hypershift + Mode,ARO-HCP + DNS Zone,{{.MGMT_DNS_ZONE_NAME}} + Key Vault Client,{{.KV_CLIENT_ID}} + CRD Platform,Azure + Status,✓ Installed" | gum table --print --border rounded + silent: true + + # === VERIFICATION === + + verify: + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "HyperShift Operator Status" + silent: true + - kubectl get pods -n hypershift + - kubectl get deployment operator -n hypershift + - kubectl get deployment external-dns -n hypershift 2>/dev/null || gum format "○ External DNS not deployed" + + wait: + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Waiting for Operator" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - kubectl wait --for=condition=available deployment/operator -n hypershift --timeout=300s + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,Wait for Operator + Deployment,operator + Namespace,hypershift + Status,✓ Ready" | gum table --print --border rounded + silent: true + + logs: + desc: Show operator logs + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Operator Logs" + silent: true + - kubectl logs -n hypershift deployment/operator --tail=50 + + # === UNINSTALLATION === + + uninstall: + desc: Uninstall HyperShift operator + vars: + HC_COUNT: + sh: 'kubectl get hostedclusters --all-namespaces -o name < /dev/null 2>/dev/null | wc -l | tr -d " "' + KV_CLIENT_ID: + sh: 'az aks show -n {{.AKS_CLUSTER_NAME}} -g {{.AKS_RG}} --query "addonProfiles.azureKeyvaultSecretsProvider.identity.clientId" -o tsv 2>/dev/null || echo ""' + HYPERSHIFT_CMD: + sh: 'test -n "{{.HYPERSHIFT_BINARY_PATH}}" && echo "{{.HYPERSHIFT_BINARY_PATH}}/hypershift" || echo "hypershift"' + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Uninstalling HyperShift Operator" + silent: true + - | + if [ "{{.HC_COUNT}}" -gt 0 ]; then + gum format "✗ {{.HC_COUNT}} HostedCluster(s) exist. Delete them first with 'task cluster:destroy'" + kubectl get hostedclusters --all-namespaces + exit 1 + fi + - cmd: | + IMAGE_FLAG="" + if [ -n "{{.HYPERSHIFT_IMAGE}}" ]; then + IMAGE_FLAG="--hypershift-image {{.HYPERSHIFT_IMAGE}}" + fi + + {{.HYPERSHIFT_CMD}} install render \ + --enable-conversion-webhook=false \ + --external-dns-provider=azure \ + --external-dns-credentials {{.EXTERNAL_DNS_CREDS_FILE}} \ + --pull-secret {{.PULL_SECRET}} \ + --external-dns-domain-filter {{.MGMT_DNS_ZONE_NAME}} \ + --managed-service ARO-HCP \ + --aro-hcp-key-vault-users-client-id "{{.KV_CLIENT_ID}}" \ + --limit-crd-install Azure \ + --tech-preview-no-upgrade \ + $IMAGE_FLAG | kubectl delete --ignore-not-found -f - + ignore_error: true + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,Uninstall Operator + Namespace,hypershift + CRDs,Preserved (to prevent data loss) + Status,✓ Uninstalled" | gum table --print --border rounded + silent: true diff --git a/hack/aro-hcp/tasks/prereq.yml b/hack/aro-hcp/tasks/prereq.yml new file mode 100644 index 00000000000..5f506f06efb --- /dev/null +++ b/hack/aro-hcp/tasks/prereq.yml @@ -0,0 +1,169 @@ +version: '3' + +tasks: + validate: + desc: Validate all required environment variables and tools + vars: + # Query current subscription for comparison + CURRENT_SUB: + sh: 'az account show --query id -o tsv 2>/dev/null || echo ""' + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Validating Prerequisites" + silent: true + - cmd: gum style --faint "Output:" + silent: true + + # Check credentials files + - test -f "{{.AZURE_CREDS}}" || (gum format "✗ AZURE_CREDS file not found at {{.AZURE_CREDS}}" && exit 1) + - test -f "{{.PULL_SECRET}}" || (gum format "✗ PULL_SECRET file not found at {{.PULL_SECRET}}" && exit 1) + + # Validate azure-creds has required fields + - 'jq -e ".subscriptionId" {{.AZURE_CREDS}} >/dev/null || (gum format "✗ subscriptionId missing in {{.AZURE_CREDS}}" && exit 1)' + - 'jq -e ".tenantId" {{.AZURE_CREDS}} >/dev/null || (gum format "✗ tenantId missing in {{.AZURE_CREDS}}" && exit 1)' + - 'jq -e ".clientId" {{.AZURE_CREDS}} >/dev/null || (gum format "✗ clientId missing in {{.AZURE_CREDS}}" && exit 1)' + - 'jq -e ".clientSecret" {{.AZURE_CREDS}} >/dev/null || (gum format "✗ clientSecret missing in {{.AZURE_CREDS}}" && exit 1)' + + # Check required variables + - test -n "{{.PREFIX}}" || (gum format "✗ PREFIX not set" && exit 1) + - test -n "{{.OIDC_ISSUER_NAME}}" || (gum format "✗ OIDC_ISSUER_NAME not set" && exit 1) + - test -n "{{.RELEASE_IMAGE}}" || (gum format "✗ RELEASE_IMAGE not set" && exit 1) + - test -n "{{.LOCATION}}" || (gum format "✗ LOCATION not set" && exit 1) + + # Check required CLIs + - command -v gum >/dev/null || (echo "gum not found - install from https://github.com/charmbracelet/gum" && exit 1) + - command -v az >/dev/null || (gum format "✗ az CLI not found" && exit 1) + - command -v kubectl >/dev/null || (gum format "✗ kubectl not found" && exit 1) + - command -v jq >/dev/null || (gum format "✗ jq not found" && exit 1) + - command -v ccoctl >/dev/null || (gum format "✗ ccoctl not found - install from https://github.com/openshift/cloud-credential-operator" && exit 1) + + # Check hypershift CLI + - | + if [ -n "{{.HYPERSHIFT_BINARY_PATH}}" ]; then + test -x "{{.HYPERSHIFT_BINARY_PATH}}/hypershift" || (gum format "✗ hypershift binary not found at {{.HYPERSHIFT_BINARY_PATH}}/hypershift" && exit 1) + else + command -v hypershift >/dev/null || (gum format "✗ hypershift CLI not found (set HYPERSHIFT_BINARY_PATH or add to PATH)" && exit 1) + fi + + # Check Azure login + - az account show >/dev/null 2>&1 || (gum format "✗ Not logged into Azure. Run 'task prereq:login' first." && exit 1) + + # Verify logged-in identity matches credentials file + - | + LOGGED_IN_CLIENT=$(az account show --query user.name -o tsv 2>/dev/null) + CREDS_CLIENT=$(jq -r '.clientId' {{.AZURE_CREDS}}) + if [ "$LOGGED_IN_CLIENT" != "$CREDS_CLIENT" ]; then + gum format "✗ **Identity mismatch!**" + gum format " Logged in as: $LOGGED_IN_CLIENT" + gum format " Credentials file: $CREDS_CLIENT" + gum format " Run: **task prereq:login**" + exit 1 + fi + + # Verify subscription match + - | + if [ "{{.CURRENT_SUB}}" != "{{.SUBSCRIPTION_ID}}" ]; then + gum format "✗ **Subscription mismatch!**" + gum format " Current: {{.CURRENT_SUB}}" + gum format " Expected: {{.SUBSCRIPTION_ID}}" + gum format " Run: **task prereq:login**" + exit 1 + fi + + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,Prerequisites Validation + Credentials,{{.AZURE_CREDS}} + Pull Secret,{{.PULL_SECRET}} + Status,✓ Validated" | gum table --print --border rounded + silent: true + + login: + desc: Login to Azure using credentials from azure-credentials.json + vars: + CREDS_CLIENT_ID: + sh: 'jq -r ".clientId" {{.AZURE_CREDS}}' + CREDS_CLIENT_SECRET: + sh: 'jq -r ".clientSecret" {{.AZURE_CREDS}}' + CREDS_TENANT_ID: + sh: 'jq -r ".tenantId" {{.AZURE_CREDS}}' + CREDS_SUBSCRIPTION_ID: + sh: 'jq -r ".subscriptionId" {{.AZURE_CREDS}}' + preconditions: + - sh: test -f "{{.AZURE_CREDS}}" + msg: "Credentials file not found: {{.AZURE_CREDS}}" + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Azure Login" + silent: true + - cmd: gum style --faint "Output:" + silent: true + - | + az login --service-principal \ + -u "{{.CREDS_CLIENT_ID}}" \ + -p "{{.CREDS_CLIENT_SECRET}}" \ + --tenant "{{.CREDS_TENANT_ID}}" \ + --output none + - az account set --subscription "{{.CREDS_SUBSCRIPTION_ID}}" + - cmd: gum style --border normal --padding "0 1" --border-foreground 10 "Task Summary" + silent: true + - cmd: | + echo "Property,Value + Task,Azure Login + Client ID,{{.CREDS_CLIENT_ID}} + Subscription,{{.CREDS_SUBSCRIPTION_ID}} + Status,✓ Logged in" | gum table --print --border rounded + silent: true + + whoami: + desc: Show current Azure identity and compare with credentials file + vars: + LOGGED_IN_CLIENT: + sh: 'az account show --query user.name -o tsv 2>/dev/null || echo "not logged in"' + LOGGED_IN_TYPE: + sh: 'az account show --query user.type -o tsv 2>/dev/null || echo "n/a"' + LOGGED_IN_SUB: + sh: 'az account show --query id -o tsv 2>/dev/null || echo "n/a"' + CREDS_CLIENT: + sh: 'jq -r ".clientId // \"n/a\"" {{.AZURE_CREDS}} 2>/dev/null || echo "file not found"' + CREDS_SUB: + sh: 'jq -r ".subscriptionId // \"n/a\"" {{.AZURE_CREDS}} 2>/dev/null || echo "file not found"' + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Azure Identity" + silent: true + - cmd: | + MATCH="✗ MISMATCH" + if [ "{{.LOGGED_IN_CLIENT}}" = "{{.CREDS_CLIENT}}" ]; then + MATCH="✓ Match" + fi + echo "Source,Client ID,Subscription + Logged in ({{.LOGGED_IN_TYPE}}),{{.LOGGED_IN_CLIENT}},{{.LOGGED_IN_SUB}} + Credentials file,{{.CREDS_CLIENT}},{{.CREDS_SUB}} + Status,$MATCH,-" | gum table --print --border rounded + silent: true + + show-config: + desc: Display current configuration + cmds: + - cmd: gum style --border normal --padding "0 1" --border-foreground 212 "Current Configuration" + silent: true + - cmd: | + echo "Setting,Value + PREFIX,{{.PREFIX}} + CLUSTER_NAME,{{.CLUSTER_NAME}} + LOCATION,{{.LOCATION}} + SUBSCRIPTION_ID,{{.SUBSCRIPTION_ID}} + TENANT_ID,{{.TENANT_ID}} + PERSISTENT_RG_NAME,{{.PERSISTENT_RG_NAME}} + AKS_RG,{{.AKS_RG}} + KV_NAME,{{.KV_NAME}} + OIDC_ISSUER_NAME,{{.OIDC_ISSUER_NAME}} + OIDC_ISSUER_URL,{{.OIDC_ISSUER_URL}} + PARENT_DNS_ZONE,{{.PARENT_DNS_ZONE}} + MGMT_DNS_ZONE_NAME,{{.MGMT_DNS_ZONE_NAME}} + PULL_SECRET,{{.PULL_SECRET}} + CP_OUTPUT_FILE,{{.CP_OUTPUT_FILE}} + DP_OUTPUT_FILE,{{.DP_OUTPUT_FILE}} + RELEASE_IMAGE,{{.RELEASE_IMAGE}} + HYPERSHIFT_IMAGE,{{.HYPERSHIFT_IMAGE}}" | gum table --print --border rounded + silent: true