From d49d3d25979ff8a1ca761e73d6fd3c5377b01013 Mon Sep 17 00:00:00 2001 From: Sylvain Niles Date: Thu, 14 May 2026 13:06:39 -0700 Subject: [PATCH] Run Azure functional tests locally against OS-process Radius Adds a workflow for running the corerp/cloud Azure functional tests against a local OS-process Radius stack (`make debug-start`) using the host's `az login` credentials, with no service-principal/workload-identity registration required. Highlights - New `build/scripts/azure-local-testenv.sh` orchestrator with `setup`, `run`, `teardown`, `all` sub-commands. `run` and `all` accept passthrough `go test` flags (e.g. `-run`, `-v`). - Auto-recovery: `run` rebuilds state from the newest `radlocal-${USER}-*` resource group when the state file is missing (e.g. after `make debug-stop`), and re-applies the Azure scope on the default rad environment that `debug-start` wipes. - Orphan GC: `teardown --all-orphans` deletes every `radlocal-${USER}-*` RG and stops the `tf-module-server` port-forward. - `tf-module-server` bootstrap: deploys the in-cluster nginx test module server and port-forwards it to `localhost:8999` automatically when not already reachable. - Terraform Azure provider falls back to `use_cli = true` when no Azure credential is registered with UCP (404), letting the host RP's `az login` session authenticate. CI workload-identity path is unchanged. - `start-radius.sh` exports `TERRAFORM_TEST_GLOBAL_DIR` so the RP no longer tries to write to read-only `/terraform`. - AWS-required tests skip cleanly via `t.Skip` when AWS env vars are unset; private-git redis test skips when `GH_TOKEN` is unset. - `recipe_terraform_test.go` now derives the resource ID from the active workspace scope so it works against any RG (CI's `kind-radius` and local debug's `default`). Tested Full `corerp/cloud/...` suite green locally: - PASS: `Test_AzureConnections`, `Test_ACI`, `Test_TerraformRecipe_AzureResourceGroup` - SKIP: AWS-only tests, `Test_TerraformPrivateGitModule_KubernetesRedis`, `Test_Storage`/`Test_PersistentVolume` (issue #7853, pre-existing) Documentation in `docs/contributing/contributing-code/contributing-code-debugging/radius-os-processes-debugging.md`. Signed-off-by: Sylvain Niles --- build/debug.mk | 59 ++- build/scripts/azure-local-testenv.sh | 460 ++++++++++++++++++ build/scripts/ensure-encryption-key.sh | 53 ++ build/scripts/start-radius.sh | 6 + build/test.mk | 38 ++ .../radius-os-processes-debugging.md | 179 ++++++- .../terraform/config/providers/azure.go | 15 +- test/createAzureTestResources.bicep | 5 +- .../corerp/cloud/resources/extender_test.go | 8 +- .../cloud/resources/recipe_terraform_test.go | 13 +- test/validation/shared.go | 34 ++ 11 files changed, 847 insertions(+), 23 deletions(-) create mode 100755 build/scripts/azure-local-testenv.sh create mode 100755 build/scripts/ensure-encryption-key.sh diff --git a/build/debug.mk b/build/debug.mk index a3f6f46670..76be7afeeb 100644 --- a/build/debug.mk +++ b/build/debug.mk @@ -209,14 +209,25 @@ debug-build-rad: ## Build rad CLI with debug symbols + create drad alias @echo "💡 Use './drad' for debug-configured CLI (preserves 'rad' for your installed version)" debug-start: debug-setup debug-build-all ## Start k3d cluster and all Radius components as OS processes - @echo "Creating k3d cluster..." - @if k3d cluster list | grep -q "radius-debug"; then \ - echo "k3d cluster 'radius-debug' already exists"; \ + @echo "Ensuring k3d cluster 'radius-debug' is running..." + @if k3d cluster list --no-headers 2>/dev/null | awk '{print $$1}' | grep -qx "radius-debug"; then \ + servers=$$(k3d cluster list --no-headers 2>/dev/null | awk '$$1=="radius-debug"{print $$2}'); \ + running=$$(echo "$$servers" | cut -d/ -f1); \ + if [ "$$running" = "0" ]; then \ + echo "k3d cluster 'radius-debug' exists but is stopped — starting it..."; \ + k3d cluster start radius-debug; \ + else \ + echo "k3d cluster 'radius-debug' already running ($$servers servers)"; \ + fi; \ else \ + echo "Creating k3d cluster 'radius-debug'..."; \ k3d cluster create radius-debug --api-port 0.0.0.0:6443 --wait --timeout 60s; \ fi @echo "Switching to k3d context..." @kubectl config use-context k3d-radius-debug + @echo "Ensuring radius-encryption-key secret exists in k3d cluster..." + @chmod +x build/scripts/ensure-encryption-key.sh 2>/dev/null || true + @build/scripts/ensure-encryption-key.sh @echo "Starting Radius components as OS processes..." @build/scripts/start-radius.sh @echo "Waiting for components to be ready..." @@ -281,17 +292,30 @@ debug-deployment-engine-pull: ## Pull latest deployment engine image from ghcr.i && echo "✅ Deployment Engine image pulled successfully" \ || echo "❌ Failed to pull Deployment Engine image" -debug-deployment-engine-start: ## Start deployment engine in k3d cluster - @echo "Installing ONLY deployment engine to k3d cluster..." - @if kubectl --context k3d-radius-debug get deployment deployment-engine >/dev/null 2>&1 && \ - kubectl --context k3d-radius-debug get deployment deployment-engine -o jsonpath='{.status.readyReplicas}' 2>/dev/null | grep -q "1" && \ - curl -s "http://localhost:5017/metrics" > /dev/null 2>&1; then \ - echo "✅ Deployment engine already running and healthy"; \ +debug-deployment-engine-start: ## Start deployment engine in k3d cluster (or reuse a local OS process on port 5017) + @echo "Checking for an existing Deployment Engine on localhost:5017..." + @listener_cmd=""; \ + if command -v lsof >/dev/null 2>&1; then \ + listener_cmd=$$(lsof -nP -iTCP:5017 -sTCP:LISTEN 2>/dev/null | awk 'NR==2 {print $$1}'); \ + fi; \ + if [ -n "$$listener_cmd" ] && [ "$$listener_cmd" != "kubectl" ] && curl -s "http://localhost:5017/metrics" > /dev/null 2>&1; then \ + echo "✅ Detected local Deployment Engine process ($$listener_cmd) on port 5017 — reusing it"; \ + echo "💡 Skipping k3d deployment-engine install and port-forward"; \ + mkdir -p $(DEBUG_DEV_ROOT)/logs; \ + echo "external" > $(DEBUG_DEV_ROOT)/logs/de-external.marker; \ else \ - $(MAKE) debug-deployment-engine-deploy; \ - $(MAKE) debug-deployment-engine-port-forward; \ + rm -f $(DEBUG_DEV_ROOT)/logs/de-external.marker 2>/dev/null || true; \ + echo "Installing ONLY deployment engine to k3d cluster..."; \ + if kubectl --context k3d-radius-debug get deployment deployment-engine >/dev/null 2>&1 && \ + kubectl --context k3d-radius-debug get deployment deployment-engine -o jsonpath='{.status.readyReplicas}' 2>/dev/null | grep -q "1" && \ + curl -s "http://localhost:5017/metrics" > /dev/null 2>&1; then \ + echo "✅ Deployment engine already running and healthy in k3d"; \ + else \ + $(MAKE) debug-deployment-engine-deploy; \ + $(MAKE) debug-deployment-engine-port-forward; \ + fi; \ + echo "✅ Deployment engine installed and ready in k3d cluster"; \ fi - @echo "✅ Deployment engine installed and ready in k3d cluster" debug-deployment-engine-deploy: ## Deploy deployment engine to k3d cluster @echo "Applying deployment engine manifest to k3d cluster..." @@ -302,7 +326,12 @@ debug-deployment-engine-deploy: ## Deploy deployment engine to k3d cluster debug-deployment-engine-port-forward: ## Set up port forwarding for deployment engine @build/scripts/setup-deployment-engine-port-forward.sh -debug-deployment-engine-stop: ## Stop deployment engine in k3d cluster +debug-deployment-engine-stop: ## Stop deployment engine in k3d cluster (leaves external local DE process alone) + @if [ -f $(DEBUG_DEV_ROOT)/logs/de-external.marker ]; then \ + echo "ℹ️ External local Deployment Engine was in use — leaving it running"; \ + rm -f $(DEBUG_DEV_ROOT)/logs/de-external.marker; \ + exit 0; \ + fi @echo "Removing deployment engine from k3d cluster..." @if [ -f $(DEBUG_DEV_ROOT)/logs/de-port-forward.pid ]; then \ kill $$(cat $(DEBUG_DEV_ROOT)/logs/de-port-forward.pid) 2>/dev/null || true; \ @@ -315,7 +344,9 @@ debug-deployment-engine-stop: ## Stop deployment engine in k3d cluster debug-deployment-engine-status: ## Check deployment engine status @echo "🚀 Deployment Engine Status:" - @if kubectl --context k3d-radius-debug get deployment deployment-engine >/dev/null 2>&1; then \ + @if [ -f $(DEBUG_DEV_ROOT)/logs/de-external.marker ] && curl -s "http://localhost:5017/metrics" > /dev/null 2>&1; then \ + echo "✅ Deployment Engine (external local process on :5017) - Running"; \ + elif kubectl --context k3d-radius-debug get deployment deployment-engine >/dev/null 2>&1; then \ replicas=$$(kubectl --context k3d-radius-debug get deployment deployment-engine -o jsonpath='{.status.readyReplicas}' 2>/dev/null || echo "0"); \ if [ "$$replicas" = "1" ]; then \ echo "✅ Deployment Engine (k3d) - Running and ready"; \ diff --git a/build/scripts/azure-local-testenv.sh b/build/scripts/azure-local-testenv.sh new file mode 100755 index 0000000000..8f8fd09f62 --- /dev/null +++ b/build/scripts/azure-local-testenv.sh @@ -0,0 +1,460 @@ +#!/usr/bin/env bash +# ------------------------------------------------------------ +# Copyright 2024 The Radius Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# ------------------------------------------------------------ +# +# Orchestrates an ephemeral Azure environment for local functional tests. +# +# Subcommands: +# setup Create an ephemeral resource group, deploy test fixtures, configure +# the current rad environment with the Azure provider scope, and write +# state to debug_files/logs/azure-local.env. +# run Source the state file and run the Test_Azure* subset of the +# corerp-cloud functional tests against the locally-running stack. +# teardown Delete the resource group (no-wait), clear the env-update on the +# current rad environment, and remove the state file. +# +# Auth model: this script assumes the caller is authenticated via `az login`. +# Radius components (RP/UCP) authenticate to Azure via the Azure CLI fallback in +# pkg/azure/armauth/auth.go. The Deployment Engine must also be running locally +# (not in a container) so it can use the same DefaultAzureCredential -> az CLI +# path. See debug_files/logs/de-external.marker for the external-DE indicator. +# +# Environment variables: +# AZURE_LOCATION Azure region (default: westus3). +# AZURE_SUBSCRIPTION_ID Subscription to use (default: az account show). +# AZURE_LOCAL_PREPROVISIONED_RG If set, reuse an existing RG and skip fixture +# deployment / teardown of that RG. The script +# will still write the state file so `run` works. +# RAD_ENV rad environment to configure (default: default). +# +# Note: AWS support is intentionally out of scope for this iteration. +# Note: Test_AzureMSSQL_* tests are not configured here; they auto-skip when the +# AZURE_MSSQL_* env vars are absent. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +STATE_DIR="${REPO_ROOT}/debug_files/logs" +STATE_FILE="${STATE_DIR}/azure-local.env" +BICEP_TEMPLATE="${REPO_ROOT}/test/createAzureTestResources.bicep" +TF_MODULE_SERVER_NS="radius-test-tf-module-server" +TF_MODULE_SERVER_PORT_FORWARD_PID_FILE="${STATE_DIR}/tf-module-server-pf.pid" +TF_MODULE_SERVER_PORT_FORWARD_LOG="${STATE_DIR}/tf-module-server-pf.log" + +AZURE_LOCATION="${AZURE_LOCATION:-westus3}" +RAD_ENV="${RAD_ENV:-default}" + +log() { printf '\033[0;34mℹ\033[0m %s\n' "$*"; } +ok() { printf '\033[0;32m✔\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m⚠\033[0m %s\n' "$*" >&2; } +err() { printf '\033[0;31m✖\033[0m %s\n' "$*" >&2; } + +require_cmd() { + for c in "$@"; do + command -v "$c" >/dev/null 2>&1 || { err "required command not found: $c"; exit 1; } + done +} + +require_az_login() { + if ! az account show >/dev/null 2>&1; then + err "not logged in to Azure. Run 'az login' first." + exit 1 + fi +} + +resolve_subscription() { + if [[ -n "${AZURE_SUBSCRIPTION_ID:-}" ]]; then + echo "${AZURE_SUBSCRIPTION_ID}" + return + fi + az account show --query id -o tsv +} + +ensure_tf_module_server() { + # Terraform-recipe tests fetch zipped modules from http://localhost:8999. + # In CI an in-cluster nginx serves them; locally we deploy the same nginx + # into the debug k3d cluster and port-forward it to localhost:8999. + require_cmd kubectl make + if curl -sf -o /dev/null -m 2 http://localhost:8999/azure-rg.zip; then + log "tf-module-server already reachable at http://localhost:8999" + return 0 + fi + if ! kubectl get ns "${TF_MODULE_SERVER_NS}" >/dev/null 2>&1 \ + || ! kubectl -n "${TF_MODULE_SERVER_NS}" get deploy tf-module-server >/dev/null 2>&1; then + log "Deploying tf-module-server into the debug cluster (publish-test-terraform-recipes)..." + (cd "${REPO_ROOT}" && make publish-test-terraform-recipes >/dev/null) \ + || { err "make publish-test-terraform-recipes failed"; exit 1; } + fi + log "Waiting for tf-module-server rollout..." + kubectl -n "${TF_MODULE_SERVER_NS}" rollout status deploy/tf-module-server --timeout=120s >/dev/null \ + || { err "tf-module-server rollout did not become ready"; exit 1; } + # Stop any stale port-forward before starting a new one. + if [[ -f "${TF_MODULE_SERVER_PORT_FORWARD_PID_FILE}" ]]; then + local old_pid + old_pid="$(cat "${TF_MODULE_SERVER_PORT_FORWARD_PID_FILE}" 2>/dev/null || true)" + if [[ -n "${old_pid}" ]] && kill -0 "${old_pid}" 2>/dev/null; then + kill "${old_pid}" 2>/dev/null || true + fi + rm -f "${TF_MODULE_SERVER_PORT_FORWARD_PID_FILE}" + fi + log "Starting kubectl port-forward svc/tf-module-server 8999:80 -n ${TF_MODULE_SERVER_NS}" + ( kubectl -n "${TF_MODULE_SERVER_NS}" port-forward svc/tf-module-server 8999:80 \ + >"${TF_MODULE_SERVER_PORT_FORWARD_LOG}" 2>&1 ) & + echo $! > "${TF_MODULE_SERVER_PORT_FORWARD_PID_FILE}" + # Wait briefly for the port-forward to come up. + local i + for i in $(seq 1 20); do + if curl -sf -o /dev/null -m 1 http://localhost:8999/azure-rg.zip; then + ok "tf-module-server reachable at http://localhost:8999 (pid $(cat "${TF_MODULE_SERVER_PORT_FORWARD_PID_FILE}"))" + return 0 + fi + sleep 0.5 + done + err "tf-module-server port-forward did not become reachable; see ${TF_MODULE_SERVER_PORT_FORWARD_LOG}" + exit 1 +} + +stop_tf_module_server_port_forward() { + if [[ -f "${TF_MODULE_SERVER_PORT_FORWARD_PID_FILE}" ]]; then + local pid + pid="$(cat "${TF_MODULE_SERVER_PORT_FORWARD_PID_FILE}" 2>/dev/null || true)" + if [[ -n "${pid}" ]] && kill -0 "${pid}" 2>/dev/null; then + log "Stopping tf-module-server port-forward (pid ${pid})" + kill "${pid}" 2>/dev/null || true + fi + rm -f "${TF_MODULE_SERVER_PORT_FORWARD_PID_FILE}" + fi +} + +cmd_setup() { + require_cmd az jq rad + require_az_login + mkdir -p "${STATE_DIR}" + + if [[ -f "${STATE_FILE}" ]]; then + err "state file already exists at ${STATE_FILE}. Run 'teardown' first or remove it manually." + exit 1 + fi + + local sub + sub="$(resolve_subscription)" + local tenant + tenant="$(az account show --query tenantId -o tsv)" + log "Subscription: ${sub}" + + local rg + if [[ -n "${AZURE_LOCAL_PREPROVISIONED_RG:-}" ]]; then + rg="${AZURE_LOCAL_PREPROVISIONED_RG}" + log "Reusing pre-provisioned resource group: ${rg}" + if ! az group show --subscription "${sub}" --name "${rg}" >/dev/null 2>&1; then + err "pre-provisioned resource group ${rg} not found in subscription ${sub}" + exit 1 + fi + else + local user_slug epoch + user_slug="$(echo "${USER:-local}" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9' '-' | sed 's/-\{2,\}/-/g;s/^-//;s/-$//')" + epoch="$(date +%s)" + rg="radlocal-${user_slug}-${epoch}" + log "Creating resource group: ${rg} in ${AZURE_LOCATION}" + az group create \ + --subscription "${sub}" \ + --location "${AZURE_LOCATION}" \ + --name "${rg}" \ + --tags creationTime="${epoch}" creator="${USER:-unknown}" purpose=radius-local-test \ + -o none + while [[ "$(az group exists --subscription "${sub}" --name "${rg}")" != "true" ]]; do + sleep 2 + done + ok "Resource group created: ${rg}" + fi + + local cosmos_id="" + if [[ -z "${AZURE_LOCAL_PREPROVISIONED_RG:-}" ]]; then + # Cosmos DB account names are globally unique. Derive a stable-but-unique + # name from the RG (which already includes user + epoch) and trim to the + # 3-44 char limit. Cosmos requires lowercase alphanumerics + hyphens, and + # disallows leading or trailing hyphens. + local cosmos_name + cosmos_name="$(echo "radlocal-${rg#radlocal-}" \ + | tr '[:upper:]' '[:lower:]' \ + | tr -c 'a-z0-9-' '-' \ + | cut -c1-44 \ + | sed -E 's/^-+//; s/-+$//')" + log "Deploying test fixtures (Cosmos Mongo account ${cosmos_name}) — this typically takes 3-5 minutes..." + local deploy_json + deploy_json="$(az deployment group create \ + --subscription "${sub}" \ + --resource-group "${rg}" \ + --template-file "${BICEP_TEMPLATE}" \ + --parameters cosmosAccountName="${cosmos_name}" \ + -o json)" + cosmos_id="$(echo "${deploy_json}" | jq -r '.properties.outputs.cosmosMongoAccountID.value')" + ok "Cosmos Mongo account deployed: ${cosmos_id}" + else + # Reuse: look up by tag/kind in the pre-provisioned RG. + cosmos_id="$(az resource list \ + --subscription "${sub}" \ + --resource-group "${rg}" \ + --resource-type Microsoft.DocumentDB/databaseAccounts \ + --query '[?kind==`MongoDB`] | [0].id' -o tsv || true)" + if [[ -z "${cosmos_id}" || "${cosmos_id}" == "null" ]]; then + warn "no Cosmos Mongo account found in ${rg}; Test_AzureConnections will fail." + else + ok "Found Cosmos Mongo account: ${cosmos_id}" + fi + fi + + log "Configuring rad environment '${RAD_ENV}' with Azure scope" + rad env update "${RAD_ENV}" \ + --azure-subscription-id "${sub}" \ + --azure-resource-group "${rg}" + + ensure_tf_module_server + + cat > "${STATE_FILE}" <-* RG owned by the current user. + local user_slug + user_slug="$(echo "${USER:-local}" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9' '-' | sed 's/-\{2,\}/-/g;s/^-//;s/-$//')" + local matches + matches="$(az group list --subscription "${sub}" --query "[?starts_with(name, 'radlocal-${user_slug}-')].name" -o tsv)" + local count + count="$(printf '%s\n' "${matches}" | grep -c . || true)" + if [[ "${count}" -eq 0 ]]; then + err "no state file at ${STATE_FILE} and no radlocal-* RG to recover from. Run 'setup' first." + exit 1 + fi + # Pick the newest RG (epoch suffix). RG names are radlocal--. + rg="$(printf '%s\n' ${matches} | sort -t- -k3 -n | tail -1)" + if [[ "${count}" -gt 1 ]]; then + warn "multiple radlocal-${user_slug}-* RGs found; using newest: ${rg}" + warn "clean up the rest with: $0 teardown --all-orphans" + fi + fi + if ! az group show --subscription "${sub}" --name "${rg}" >/dev/null 2>&1; then + err "resource group ${rg} not found in subscription ${sub}" + exit 1 + fi + cosmos_id="$(az resource list \ + --subscription "${sub}" \ + --resource-group "${rg}" \ + --resource-type Microsoft.DocumentDB/databaseAccounts \ + --query '[?kind==`MongoDB`] | [0].id' -o tsv 2>/dev/null || true)" + mkdir -p "${STATE_DIR}" + cat > "${STATE_FILE}" < ${STATE_FILE}" + ensure_tf_module_server +} + +cmd_run() { + if [[ ! -f "${STATE_FILE}" ]]; then + warn "no state file at ${STATE_FILE}; attempting to recover from an existing RG..." + recover_state + fi + # shellcheck disable=SC1090 + source "${STATE_FILE}" + ensure_tf_module_server + # Re-apply Azure scope on the rad env idempotently. `make debug-start` + # recreates the Postgres DB, which wipes the env's Azure provider config + # set during `setup`. Without this, bicep templates that use + # `resourceGroup().id` for `providers.azure.scope` (e.g. + # corerp-resources-terraform-azurerg.bicep) get an empty subscription id + # because the deployment-engine substitutes `resourceGroup().id` from the + # active env's Azure scope. + log "Ensuring rad env '${RAD_ENV}' has Azure scope (sub=${AZURE_SUBSCRIPTION_ID}, rg=${AZURE_LOCAL_TEST_RG})" + rad env update "${RAD_ENV}" \ + --azure-subscription-id "${AZURE_SUBSCRIPTION_ID}" \ + --azure-resource-group "${AZURE_LOCAL_TEST_RG}" >/dev/null + log "Running corerp/cloud functional tests against RG ${AZURE_LOCAL_TEST_RG}" + log "AWS-required tests will skip automatically (RADIUS_TEST_USE_LOCAL_CLOUD_CREDS=azure)" + cd "${REPO_ROOT}" + # Run the full corerp/cloud suite. CheckRequiredFeatures will skip tests that + # need AWS or the CSI driver; only the Azure-required tests will execute. + # + # Stream per-test output: `go test -v` buffers each package's output until + # the package finishes, which hides progress for long Azure deployments. + # Prefer gotestsum (installed via `make test-get-envtools` in CI) for + # per-test live output; fall back to `go test -json` piped through a small + # awk filter that prints each PASS/FAIL/SKIP line as it completes. + if command -v gotestsum >/dev/null 2>&1; then + CGO_ENABLED=1 gotestsum --format testname -- \ + ./test/functional-portable/corerp/cloud/... \ + -timeout "${TEST_TIMEOUT:-1h}" \ + -parallel 5 \ + ${GOTEST_OPTS:-} \ + "$@" + else + log "gotestsum not found; using 'go test -json' for streaming output. Install with: go install gotest.tools/gotestsum@latest" + CGO_ENABLED=1 go test \ + ./test/functional-portable/corerp/cloud/... \ + -json \ + -timeout "${TEST_TIMEOUT:-1h}" \ + -parallel 5 \ + ${GOTEST_OPTS:-} \ + "$@" | \ + awk -F'"' ' + /"Action":"run"/ { for (i=1;i<=NF;i++) if ($i=="Test") { print "RUN " $(i+2); break } } + /"Action":"pass"/ { for (i=1;i<=NF;i++) if ($i=="Test") { print "PASS " $(i+2); break } } + /"Action":"fail"/ { for (i=1;i<=NF;i++) if ($i=="Test") { print "FAIL " $(i+2); break } } + /"Action":"skip"/ { for (i=1;i<=NF;i++) if ($i=="Test") { print "SKIP " $(i+2); break } } + /"Action":"output"/ { for (i=1;i<=NF;i++) if ($i=="Output") { gsub(/\\n/,"",$(i+2)); if ($(i+2) != "") print " " $(i+2); break } } + ' + fi +} + +cmd_teardown() { + require_cmd az + # Optional: --all-orphans deletes every radlocal--* RG in the current + # subscription, regardless of state file. Useful after a debug-stop wiped + # state without tearing down RGs. + if [[ "${1:-}" == "--all-orphans" ]]; then + require_az_login + local sub user_slug matches + sub="$(resolve_subscription)" + user_slug="$(echo "${USER:-local}" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9' '-' | sed 's/-\{2,\}/-/g;s/^-//;s/-$//')" + matches="$(az group list --subscription "${sub}" --query "[?starts_with(name, 'radlocal-${user_slug}-')].name" -o tsv)" + if [[ -z "${matches}" ]]; then + ok "no radlocal-${user_slug}-* RGs to delete." + else + log "Deleting orphan RGs (no-wait):" + printf ' %s\n' ${matches} + for orphan in ${matches}; do + az group delete --subscription "${sub}" --name "${orphan}" --yes --no-wait \ + || warn "failed to start delete for ${orphan}" + done + fi + stop_tf_module_server_port_forward + rm -f "${STATE_FILE}" + return 0 + fi + if [[ ! -f "${STATE_FILE}" ]]; then + warn "no state file at ${STATE_FILE}; nothing to tear down. Use '$0 teardown --all-orphans' to GC stale radlocal-* RGs." + return 0 + fi + # shellcheck disable=SC1090 + source "${STATE_FILE}" + + local sub="${AZURE_SUBSCRIPTION_ID}" + local rg="${AZURE_LOCAL_TEST_RG}" + + if [[ -z "${AZURE_LOCAL_PREPROVISIONED_RG:-}" && "${rg}" == radlocal-* ]]; then + log "Deleting resource group ${rg} (no-wait)" + az group delete \ + --subscription "${sub}" \ + --name "${rg}" \ + --yes --no-wait || warn "az group delete returned non-zero" + else + warn "RG ${rg} was pre-provisioned (or not radlocal-* prefix); leaving it intact." + fi + + log "Clearing Azure scope on rad env '${RAD_ENV}'" + rad env update "${RAD_ENV}" --clear-azure 2>/dev/null || \ + warn "rad env update --clear-azure failed or unsupported; clear manually if needed." + + stop_tf_module_server_port_forward + rm -f "${STATE_FILE}" + ok "Teardown complete." +} + +cmd_all() { + cmd_setup + local rc=0 + if [[ "${AZURE_LOCAL_KEEP_ON_FAILURE:-0}" == "1" || "${AZURE_LOCAL_KEEP_ON_FAILURE:-}" =~ ^[Tt]rue$ ]]; then + log "AZURE_LOCAL_KEEP_ON_FAILURE is set; teardown will be SKIPPED if tests fail (post-mortem mode)." + cmd_run "$@" || rc=$? + if [[ "${rc}" -ne 0 ]]; then + warn "Tests exited with rc=${rc}; preserving RG ${AZURE_LOCAL_TEST_RG:-?} and state file ${STATE_FILE} for inspection." + warn "Run 'make test-functional-azure-local-teardown' when finished." + return "${rc}" + fi + cmd_teardown + return 0 + fi + # Default: ensure teardown runs even if tests fail. + trap cmd_teardown EXIT + cmd_run "$@" || rc=$? + trap - EXIT + cmd_teardown + return "${rc}" +} + +usage() { + cat >&2 < [-- extra go test args] + + setup Create RG, deploy fixtures, write state file. + run Source state file (auto-recover from existing RG if missing) and + run corerp/cloud functional tests. Extra args after the command + are passed through to 'go test' (e.g. -run '^Test_X$' -v). + teardown Delete RG and remove state file. + all setup -> run -> teardown (teardown runs even on failure). + +Examples: + # Re-run only specific failing tests against the existing RG: + $0 run -run '^(Test_TerraformRecipe_AzureResourceGroup|Test_Extender_RecipeAWS_LogGroup)\$' -v + +Environment variables: + AZURE_LOCAL_KEEP_ON_FAILURE=1 When set with 'all', skip teardown on test + failure so the RG can be inspected. The + state file is also preserved; clean up later + with 'teardown'. + AZURE_LOCAL_PREPROVISIONED_RG Reuse an existing RG (skip Cosmos deploy and + skip RG deletion). + AZURE_LOCAL_TEST_RG Force recovery from a specific RG when the + state file is missing. +EOF + exit 2 +} + +cmd="${1:-}" +shift || true +case "${cmd}" in + setup) cmd_setup "$@" ;; + run) cmd_run "$@" ;; + teardown) cmd_teardown "$@" ;; + all) cmd_all "$@" ;; + *) usage ;; +esac diff --git a/build/scripts/ensure-encryption-key.sh b/build/scripts/ensure-encryption-key.sh new file mode 100755 index 0000000000..fd9e658e89 --- /dev/null +++ b/build/scripts/ensure-encryption-key.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Ensure the radius-encryption-key Secret exists in the radius-system namespace +# of the k3d-radius-debug cluster. dynamic-rp (and other Radius components that +# use the encryption key provider) refuse to start without it. +# +# The Helm chart (deploy/Chart/templates/dynamic-rp/secret.yaml) normally +# creates this. The OS-process debug stack skips Helm, so we recreate the same +# secret format here. +# +# This script is idempotent: if the secret already exists, it does nothing. + +set -euo pipefail + +CONTEXT="${KUBE_CONTEXT:-k3d-radius-debug}" +NAMESPACE="${RADIUS_NAMESPACE:-radius-system}" +SECRET_NAME="radius-encryption-key" + +if ! command -v kubectl >/dev/null 2>&1; then + echo "❌ kubectl not found" + exit 1 +fi + +if ! kubectl --context "$CONTEXT" cluster-info >/dev/null 2>&1; then + echo "❌ cluster $CONTEXT not reachable" + exit 1 +fi + +# Ensure namespace exists +kubectl --context "$CONTEXT" get namespace "$NAMESPACE" >/dev/null 2>&1 \ + || kubectl --context "$CONTEXT" create namespace "$NAMESPACE" >/dev/null + +# If the secret already exists, leave it alone. +if kubectl --context "$CONTEXT" -n "$NAMESPACE" get secret "$SECRET_NAME" >/dev/null 2>&1; then + echo "✅ Secret $NAMESPACE/$SECRET_NAME already exists" + exit 0 +fi + +# Generate a 32-byte random key and build the JSON keystore the same way the +# Helm chart does. +now_iso="$(date -u +%Y-%m-%dT%H:%M:%SZ)" +expiry_iso="$(date -u -v+90d +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \ + || date -u -d '+90 days' +%Y-%m-%dT%H:%M:%SZ)" +key_b64="$(head -c 32 /dev/urandom | base64 | tr -d '\n')" + +keystore_json=$(cat </dev/null + +echo "✅ Created secret $NAMESPACE/$SECRET_NAME (random 32-byte key, 90-day expiry)" diff --git a/build/scripts/start-radius.sh b/build/scripts/start-radius.sh index 1cf9a3707b..9adaa0e85d 100755 --- a/build/scripts/start-radius.sh +++ b/build/scripts/start-radius.sh @@ -130,6 +130,12 @@ fi # Ensure logs directory exists mkdir -p "$DEBUG_ROOT/logs" +# Use a writable Terraform global cache directory for local OS-process runs. +# The default `/terraform` path only exists inside the Radius container image +# and would fail with "read-only file system" on host filesystems. +export TERRAFORM_TEST_GLOBAL_DIR="${TERRAFORM_TEST_GLOBAL_DIR:-$DEBUG_ROOT/terraform-global}" +mkdir -p "$TERRAFORM_TEST_GLOBAL_DIR" + # Check prerequisites check_prerequisites diff --git a/build/test.mk b/build/test.mk index c11f4a3f3f..2604dd390d 100644 --- a/build/test.mk +++ b/build/test.mk @@ -162,6 +162,44 @@ test-functional-samples: test-functional-samples-noncloud ## Runs all Samples fu test-functional-samples-noncloud: ## Runs Samples functional tests that do not require cloud resources CGO_ENABLED=1 $(GOTEST_TOOL) ./test/functional-portable/samples/noncloud/... -timeout ${TEST_TIMEOUT} -v -parallel 5 $(GOTEST_OPTS) +# ---------------------------------------------------------------------------- +# Local Azure functional tests +# +# These targets orchestrate an ephemeral Azure resource group, deploy the test +# fixtures (Cosmos Mongo for Test_AzureConnections), run the Test_Azure* subset +# of corerp-cloud tests against your locally-running Radius stack (make +# debug-start) using ambient `az login` credentials, and tear everything down. +# +# Prerequisites: +# - `az login` succeeded for the target subscription. +# - `make debug-start` is running (OS-process Radius). +# - Deployment Engine is running locally on :5017 (NOT in a container) so it +# can use the az CLI fallback. See debug_files/logs/de-external.marker. +# +# NOTE: AWS is intentionally out of scope for this iteration. +# ---------------------------------------------------------------------------- +AZURE_LOCAL_TESTENV := ./build/scripts/azure-local-testenv.sh + +.PHONY: test-functional-azure-local-setup +test-functional-azure-local-setup: ## Provision an ephemeral Azure RG and fixtures for local Azure functional tests. + @$(AZURE_LOCAL_TESTENV) setup + +.PHONY: test-functional-azure-local-run +test-functional-azure-local-run: ## Run Test_Azure* against the locally-running Radius stack using the env from setup. + @$(AZURE_LOCAL_TESTENV) run + +.PHONY: test-functional-azure-local-teardown +test-functional-azure-local-teardown: ## Delete the ephemeral Azure RG and clear local Azure test state. + @$(AZURE_LOCAL_TESTENV) teardown + +.PHONY: test-functional-azure-local +test-functional-azure-local: ## Setup -> run Test_Azure* -> teardown (teardown runs even on test failure). + @$(AZURE_LOCAL_TESTENV) all + +.PHONY: test-functional-azure-local-keep +test-functional-azure-local-keep: ## Same as test-functional-azure-local but skips teardown on failure (post-mortem). + @AZURE_LOCAL_KEEP_ON_FAILURE=1 $(AZURE_LOCAL_TESTENV) all + .PHONY: test-validate-bicep test-validate-bicep: ## Validates that all .bicep files compile cleanly BICEP_PATH="${HOME}/.rad/bin/bicep" ./build/validate-bicep.sh diff --git a/docs/contributing/contributing-code/contributing-code-debugging/radius-os-processes-debugging.md b/docs/contributing/contributing-code/contributing-code-debugging/radius-os-processes-debugging.md index 5cc938ddbc..1652befb5e 100644 --- a/docs/contributing/contributing-code/contributing-code-debugging/radius-os-processes-debugging.md +++ b/docs/contributing/contributing-code/contributing-code-debugging/radius-os-processes-debugging.md @@ -7,7 +7,8 @@ Run Radius components as OS processes with full debugger support - set breakpoin 1. [Quick Start](#quick-start) 2. [Prerequisites](#prerequisites) 3. [Debugging Workflow](#debugging-workflow) -4. [Troubleshooting](#troubleshooting) +4. [Using a Local Deployment Engine](#using-a-local-deployment-engine) +5. [Troubleshooting](#troubleshooting) ## Overview @@ -235,6 +236,182 @@ make debug-logs # Tail all component logs make debug-help # Show all debug commands ``` +## Using a Local Deployment Engine + +By default `make debug-start` runs the Deployment Engine (DE) inside the k3d +cluster from the published `ghcr.io/radius-project/deployment-engine:latest` +image. If you are working on DE itself you can run it as a local OS process +instead and have the debug stack pick it up automatically. + +### Auto-detection + +`make debug-start` checks whether something is already listening on TCP +**port 5017** (`lsof -nP -iTCP:5017 -sTCP:LISTEN`). If it is, the in-cluster DE +deployment is skipped and a marker file is written to +`debug_files/logs/de-external.marker`. `make debug-stop` honours the marker and +leaves your local DE process alone. + +There is nothing else to configure on the Radius side — UCP/Applications RP +will reach DE at `http://localhost:5017` and DE will reach UCP at +`http://localhost:9000/apis/api.ucp.dev/v1alpha3`. + +### Running DE locally + +From your `deployment-engine` checkout, in a shell where you want DE attached +to a debugger or just running with hot-reload: + +```bash +# UCP endpoint exposed by `make debug-start` +export RADIUSBACKENDURL=http://localhost:9000/apis/api.ucp.dev/v1alpha3 +export ASPNETCORE_URLS=http://+:5017 + +# Provider toggles that the in-cluster DE config sets by default +export AZURE_ENABLED=true +export AWS_ENABLED=false +export KUBERNETES_ENABLED=true + +# IMPORTANT: do NOT set ARM_AUTH_METHOD when you want DE to use ambient +# Azure credentials (az CLI / DefaultAzureCredential). Setting it to +# UCPCredential forces DE to fetch credentials from UCP, which is the +# correct value when DE runs in-cluster but defeats the local path. +unset ARM_AUTH_METHOD SKIP_ARM + +dotnet run --project src/DeploymentEngine +``` + +Then start (or restart) the rest of the stack: + +```bash +make debug-start +# Output will include: +# ℹ Detected Deployment Engine listening on localhost:5017 — using external instance +``` + +### Switching back to the in-cluster DE + +Stop your local DE process so port 5017 is free, then: + +```bash +rm -f debug_files/logs/de-external.marker +make debug-stop +make debug-start +``` + +### Running Azure functional tests against the local stack + +When DE is running locally with ambient `az login` credentials, you can run +the Azure subset of the cloud functional tests without registering any Azure +service principal in UCP. The test helper +`AssertCredentialExists` honours the `RADIUS_TEST_USE_LOCAL_CLOUD_CREDS` +environment variable as a local-dev escape hatch (set to `azure`, `aws`, +`azure,aws`, or `1` for all clouds). The container-DE / CI path is unchanged +and still requires `rad credential register azure …`. + +A make target orchestrates an ephemeral Azure resource group, deploys the +test fixtures (Cosmos Mongo for `Test_AzureConnections`), runs the entire +`corerp/cloud/...` suite (AWS-required tests skip automatically because +`RADIUS_TEST_USE_LOCAL_CLOUD_CREDS=azure` only covers Azure), and tears +everything down even if the tests fail: + +```bash +az login +az account set --subscription + +make debug-start # OS-process Radius, picks up local DE +make test-functional-azure-local # setup → run → teardown +``` + +Sub-targets if you want manual control: + +```bash +make test-functional-azure-local-setup # create RG, deploy fixtures +make test-functional-azure-local-run # run corerp/cloud tests against the stack +make test-functional-azure-local-teardown # delete RG, clear state +``` + +For post-mortem debugging of failing tests, use the `-keep` variant. It runs +setup → run → teardown as normal, but **skips the teardown step if any test +fails** so you can inspect the RG and the running stack: + +```bash +make test-functional-azure-local-keep +# On failure, RG is preserved. When done: +make test-functional-azure-local-teardown +``` + +The setup step creates a resource group named +`radlocal-${USER}-$(date +%s)` (tagged `creator`/`creationTime`/`purpose=radius-local-test`) +and writes state to `debug_files/logs/azure-local.env`. To reuse a long-lived +resource group instead of paying the ~3-5 minute Cosmos provisioning cost on +every run: + +```bash +AZURE_LOCAL_PREPROVISIONED_RG= make test-functional-azure-local-setup +``` + +The teardown step refuses to delete a pre-provisioned RG. + +#### Re-running individual tests + +`run` accepts arbitrary `go test` flags after the sub-command, so you can +quickly re-run one failing test without re-doing setup or teardown: + +```bash +./build/scripts/azure-local-testenv.sh run -run '^Test_TerraformRecipe_AzureResourceGroup$' -v +``` + +If `debug_files/logs/azure-local.env` was wiped (e.g. by `make debug-stop`), +`run` auto-recovers state by listing `radlocal-${USER}-*` resource groups in +the current subscription and picking the newest. It re-applies the Azure +scope on the `default` rad environment too — `make debug-start` resets the +embedded Postgres DB which clears the env's provider config. + +#### Cleaning up orphaned resource groups + +If a previous run left RGs behind (cancelled tests, lost state file, multiple +attempts), garbage-collect everything you own with one command: + +```bash +./build/scripts/azure-local-testenv.sh teardown --all-orphans +``` + +This deletes every `radlocal-${USER}-*` RG in the current subscription +(`--no-wait`), stops the `tf-module-server` port-forward, and removes the +state file. Pre-provisioned RGs (`AZURE_LOCAL_PREPROVISIONED_RG`) are not +touched. + +#### Terraform module server bootstrap + +`Test_TerraformRecipe_AzureResourceGroup` consumes a recipe served from +`http://localhost:8999`. Both `setup` and `run` call `ensure_tf_module_server` +which: + +1. Probes `http://localhost:8999/azure-rg.zip` and short-circuits if reachable. +2. Otherwise runs `make publish-test-terraform-recipes` (deploys the nginx + `tf-module-server` Deployment + Service into the + `radius-test-tf-module-server` namespace). +3. Starts a `kubectl port-forward svc/tf-module-server 8999:80` in the + background (PID stored under `debug_files/logs/tf-module-server-pf.pid`). +4. Waits for `/azure-rg.zip` to return 200. + +Teardown (and `--all-orphans`) stop the port-forward. + +#### Terraform recipes and Azure CLI credentials + +The Azure terraform provider configuration in +[`pkg/recipes/terraform/config/providers/azure.go`](../../../../pkg/recipes/terraform/config/providers/azure.go) +falls back to **`use_cli = true`** when no credential is registered with UCP +(404 from `/planes/azure/azurecloud/providers/System.Azure/credentials/default`). +This makes terraform pick up the same `az login` session the host RP process +already uses. No `rad credential register azure …` is required for local dev. + +In CI a workload-identity credential is registered as before; that path is +unchanged. + +> **Note:** AWS local-credentials and Azure MSSQL fixtures are intentionally +> out of scope for this flow. Tests that require `AZURE_MSSQL_*` env vars +> auto-skip when those vars are absent. + ## Troubleshooting ### Components Won't Start diff --git a/pkg/recipes/terraform/config/providers/azure.go b/pkg/recipes/terraform/config/providers/azure.go index 2a97b8f0e3..395106d074 100644 --- a/pkg/recipes/terraform/config/providers/azure.go +++ b/pkg/recipes/terraform/config/providers/azure.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" + "github.com/radius-project/radius/pkg/azure/clientv2" "github.com/radius-project/radius/pkg/azure/tokencredentials" "github.com/radius-project/radius/pkg/components/secret" "github.com/radius-project/radius/pkg/components/secret/secretprovider" @@ -128,8 +129,8 @@ func fetchAzureCredentials(ctx context.Context, azureCredentialsProvider credent logger := ucplog.FromContextOrDiscard(ctx) credentials, err := azureCredentialsProvider.Fetch(ctx, credentials.AzureCloud, "default") if err != nil { - if errors.Is(err, &secret.ErrNotFound{}) { - logger.Info("Azure credentials are not registered, skipping credentials configuration.") + if errors.Is(err, &secret.ErrNotFound{}) || clientv2.Is404Error(err) { + logger.Info("Azure credentials are not registered, falling back to Azure CLI credentials.") return nil, nil } @@ -168,6 +169,16 @@ func (p *azureProvider) generateProviderConfigMap(configMap map[string]any, cred configMap[azureSubIDParam] = subscriptionID } + // When no Radius-managed credentials are registered (e.g. a developer + // running the RP locally without `rad credential register azure ...`), + // fall back to the Azure CLI credentials available on the host process. + // `use_cli = true` is the azurerm provider default but we set it + // explicitly to make the intent clear in the generated terraform config. + if credentials == nil { + configMap[azureUseCLIParam] = true + return configMap + } + switch credentials.Kind { case ucp_datamodel.AzureServicePrincipalCredentialKind: if credentials.ServicePrincipal != nil && diff --git a/test/createAzureTestResources.bicep b/test/createAzureTestResources.bicep index 2bb507fa2f..40d542d90e 100644 --- a/test/createAzureTestResources.bicep +++ b/test/createAzureTestResources.bicep @@ -1,7 +1,10 @@ param location string = resourceGroup().location +@description('Name of the Cosmos DB (MongoDB) account. Cosmos account names are globally unique, so callers running in parallel or recreating the deployment shortly after a delete must override this with a unique value.') +param cosmosAccountName string = 'account-radiustest' + resource account 'Microsoft.DocumentDB/databaseAccounts@2020-04-01' = { - name: 'account-radiustest' + name: cosmosAccountName location: location kind: 'MongoDB' tags: { diff --git a/test/functional-portable/corerp/cloud/resources/extender_test.go b/test/functional-portable/corerp/cloud/resources/extender_test.go index a599bc6243..2bb0211dcf 100644 --- a/test/functional-portable/corerp/cloud/resources/extender_test.go +++ b/test/functional-portable/corerp/cloud/resources/extender_test.go @@ -32,10 +32,12 @@ import ( func Test_Extender_RecipeAWS_LogGroup(t *testing.T) { awsAccountID := os.Getenv("AWS_ACCOUNT_ID") awsRegion := os.Getenv("AWS_REGION") - // Error the test if the required environment variables are not set - // for running locally set the environment variables + // Skip the test if the required environment variables are not set + // (in CI these are provided alongside AWS credentials; locally the + // AWS feature gate via RequiredFeatures will also skip the test when + // AWS credentials are not registered with UCP). if awsAccountID == "" || awsRegion == "" { - t.Error("This test needs the env variables AWS_ACCOUNT_ID and AWS_REGION to be set") + t.Skip("This test needs the env variables AWS_ACCOUNT_ID and AWS_REGION to be set") } template := "testdata/corerp-resources-extender-aws-logs-recipe.bicep" diff --git a/test/functional-portable/corerp/cloud/resources/recipe_terraform_test.go b/test/functional-portable/corerp/cloud/resources/recipe_terraform_test.go index 0cb42b3cfe..479fb4ddec 100644 --- a/test/functional-portable/corerp/cloud/resources/recipe_terraform_test.go +++ b/test/functional-portable/corerp/cloud/resources/recipe_terraform_test.go @@ -25,6 +25,7 @@ package resource_test import ( "context" + "os" "strings" "testing" @@ -72,7 +73,9 @@ func Test_TerraformRecipe_AzureResourceGroup(t *testing.T) { }, SkipObjectValidation: true, PostStepVerify: func(ctx context.Context, t *testing.T, test rp.RPTest) { - resourceID := "/planes/radius/local/resourcegroups/kind-radius/providers/Applications.Core/extenders/" + name + // Use the active workspace's scope so this works against any + // resource group (CI uses 'kind-radius', local debug uses 'default'). + resourceID := test.Options.Workspace.Scope + "/providers/Applications.Core/extenders/" + name secretSuffix, err := corerp.GetSecretSuffix(resourceID, envName, appName) require.NoError(t, err) @@ -86,7 +89,7 @@ func Test_TerraformRecipe_AzureResourceGroup(t *testing.T) { }) test.PostDeleteVerify = func(ctx context.Context, t *testing.T, test rp.RPTest) { - resourceID := "/planes/radius/local/resourcegroups/kind-radius/providers/Applications.Core/extenders/" + name + resourceID := test.Options.Workspace.Scope + "/providers/Applications.Core/extenders/" + name corerp.TestSecretDeletion(t, ctx, test, appName, envName, resourceID, secretNamespace, secretPrefix) } @@ -103,6 +106,12 @@ func Test_TerraformRecipe_AzureResourceGroup(t *testing.T) { // - Upload the files from test/testrecipes/test-terraform-recipes/kubernetes-redis/modules to a private repository and update the module source in testutil.GetTerraformPrivateModuleSource() // - Create a PAT to access the private repository and update testutil.GetGitPAT() to return the generated PAT. func Test_TerraformPrivateGitModule_KubernetesRedis(t *testing.T) { + // This test pulls a Terraform module from a private GitHub repo using a + // personal access token supplied via the GH_TOKEN env var (set in CI). + // Without that secret the deployment cannot succeed, so skip locally. + if strings.TrimSpace(os.Getenv("GH_TOKEN")) == "" { + t.Skip("Test_TerraformPrivateGitModule_KubernetesRedis requires GH_TOKEN to access a private terraform module repo") + } template := "testdata/corerp-resources-terraform-private-git-repo-redis.bicep" name := "corerp-resources-terraform-private-redis" appName := "corerp-resources-terraform-private-app" diff --git a/test/validation/shared.go b/test/validation/shared.go index a09f81ca95..cad5025966 100644 --- a/test/validation/shared.go +++ b/test/validation/shared.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "net/http" + "os" "strings" "testing" @@ -197,7 +198,20 @@ func ValidateRPResources(ctx context.Context, t *testing.T, expected *RPResource } // AssertCredentialExists checks if the credential is registered in the workspace and returns a boolean value. +// +// Local-dev escape hatch: when RADIUS_TEST_USE_LOCAL_CLOUD_CREDS lists the credential +// (comma-separated; supported values: "azure", "aws", or "1"/"true" for all clouds), +// the UCP credential check is bypassed and the test is allowed to run as if the +// credential were registered. This is used by the `test-functional-azure-local` +// make target, which runs Radius components with ambient cloud credentials +// (az CLI / AWS profile) instead of credentials registered in UCP. The container-DE +// path used by CI and most contributors is unaffected. func AssertCredentialExists(t *testing.T, credential string) bool { + if localCloudCredAllowed(credential) { + t.Logf("RADIUS_TEST_USE_LOCAL_CLOUD_CREDS includes %q; bypassing UCP credential check", credential) + return true + } + ctx := testcontext.New(t) config, err := cli.LoadConfig("") @@ -216,3 +230,23 @@ func AssertCredentialExists(t *testing.T, credential string) bool { return cred.CloudProviderStatus.Enabled } + +// localCloudCredAllowed reports whether the given credential ("azure", "aws") is +// covered by the RADIUS_TEST_USE_LOCAL_CLOUD_CREDS escape hatch. Accepted values: +// - "1" / "true": all clouds. +// - comma-separated list of cloud names, e.g. "azure" or "azure,aws". +func localCloudCredAllowed(credential string) bool { + v := strings.TrimSpace(os.Getenv("RADIUS_TEST_USE_LOCAL_CLOUD_CREDS")) + if v == "" { + return false + } + if v == "1" || strings.EqualFold(v, "true") { + return true + } + for _, item := range strings.Split(v, ",") { + if strings.EqualFold(strings.TrimSpace(item), credential) { + return true + } + } + return false +}