Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions .github/extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,9 @@ The dispatcher routes to the matching provider workflow, which runs on `ubuntu-l
9. **Restore persisted state (`rad startup`).** Restores the control-plane databases and the Terraform recipe-state Secrets saved by the previous run, so `rad deploy` plans against prior state rather than an empty backend. A no-op on the first run.
10. **Register cloud credentials.** Registers the cloud identity with `rad credential register azure wi` / `aws irsa` so Radius holds the identity selector and reads the projected token at runtime.
11. **Create the Radius environment and recipe pack.** `rad deploy`s a `radius-env.bicep` that defines a `Radius.Core/recipePacks` resource and the `Radius.Core/environments` resource that references it. Azure downloads the `azure-avm` pack (Azure Verified Modules) from [resource-types-contrib](https://github.com/radius-project/resource-types-contrib); AWS generates an inline `aws-terraform` pack. `radius-env.bicep` is written to the app file's directory (e.g. `.radius/`) and deployed from there, so `rad deploy` resolves the repo's own `bicepconfig.json` (which declares the `radius` extension) — bicep resolves the config nearest the `.bicep` file. The `Radius.Compute/containerImages` type ships with the Radius extension, so no separate resource-type registration is needed.
12. **Provision registry credentials on the control plane.** Creates the `ghcr-registry-creds` secret from `github.actor` and the built-in `GITHUB_TOKEN` so the containerImages recipe's in-pod BuildKit can push the application image.
13. **Run the requested rad commands.** Validates each command in `rad_commands` against the allowed-command set, then runs them in order (stopping on the first failure) and writes a combined `rad-commands-result` artifact. When `rad_commands` is empty it runs the default `rad deploy <app-file> --environment <env>`, passing the `image` parameter (the `image` input, defaulting to `github.sha`) and any application parameters from the `RADIUS_DEPLOY_PARAMS` secret.
14. **Persist state (`rad shutdown`).** Backs the control-plane databases and Terraform recipe-state Secrets up to the `radius-state` git orphan branch. This runs even when the deploy fails (`if: always()`), so a partially-applied Terraform run is not lost.
15. **Tear down.** Runs `rad app list`, and always deletes the ephemeral `radius-cp` cluster. On failure, Radius and application logs are collected and uploaded as the `radius-logs` artifact (three-day retention).
12. **Run the requested rad commands.** Validates each command in `rad_commands` against the allowed-command set, then runs them in order (stopping on the first failure) and writes a combined `rad-commands-result` artifact. When `rad_commands` is empty it runs the default `rad deploy <app-file> --environment <env>`, passing the `image` parameter (the `image` input, defaulting to `github.sha`), any application parameters from the `RADIUS_DEPLOY_PARAMS` secret, and the registry push/pull credentials as `registryUsername` (`github.actor`) and `registryPassword` (the built-in `GITHUB_TOKEN`). Those feed the app's `Radius.Security/secrets` resource (`ghcr-registry-creds`), which materializes the registry Secret on the target cluster so the containerImages recipe's in-pod BuildKit can push the application image. The secret value is passed via an argv array and never written into the recorded command string.
13. **Persist state (`rad shutdown`).** Backs the control-plane databases and Terraform recipe-state Secrets up to the `radius-state` git orphan branch. This runs even when the deploy fails (`if: always()`), so a partially-applied Terraform run is not lost.
14. **Tear down.** Runs `rad app list`, and always deletes the ephemeral `radius-cp` cluster. On failure, Radius and application logs are collected and uploaded as the `radius-logs` artifact (three-day retention).

### Triggers and permissions

Expand Down
158 changes: 106 additions & 52 deletions .github/extension/actions/run-rad-commands/action.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Provider-agnostic deploy shared by run-rad-commands-aws.yml and
# run-rad-commands-azure.yml. Provisions registry credentials on the control plane
# and runs the requested rad commands (deploying by default), writing the combined
# rad-commands-result artifact. Teardown (rad shutdown, log collection, k3d delete)
# lives in the separate `teardown` action so it can run unconditionally.
# run-rad-commands-azure.yml. Runs the requested rad commands (deploying by
# default), writing the combined rad-commands-result artifact. Teardown (rad
# shutdown, log collection, k3d delete) lives in the separate `teardown` action
# so it can run unconditionally.
name: Radius - Run rad commands
description: Provision registry credentials and run the requested rad commands (deploying by default).
description: Run the requested rad commands (deploying by default).

inputs:
environment:
Expand All @@ -30,10 +30,10 @@ inputs:
required: false
default: ""
registry-username:
description: Username for the containerImages recipe's image-push registry secret.
description: Username passed to the app deploy as the registryUsername parameter (feeds the app's Radius.Security/secrets registry Secret).
required: true
registry-password:
description: Password/token for the containerImages recipe's image-push registry secret.
description: Password/token passed to the app deploy as the registryPassword parameter (feeds the app's Radius.Security/secrets registry Secret).
required: true

runs:
Expand All @@ -54,45 +54,22 @@ runs:
kubectl --kubeconfig "$TARGET_KUBECONFIG" create namespace "$APP_NS"
fi

- name: Provision registry credentials on control plane
shell: bash
env:
# The Radius.Compute/containerImages recipe authenticates its BuildKit
# image push by loading the Kubernetes Secret named by registrySecretName
# via the in-cluster provider -- i.e. from the CONTROL PLANE cluster where
# the dynamic-rp/BuildKit pods run, NOT the target cluster. Provision that
# secret here directly from the default GitHub token so no registry
# credentials need to live in the application bicep.
REGISTRY_USERNAME: ${{ inputs.registry-username }}
REGISTRY_PASSWORD: ${{ inputs.registry-password }}
ENV_NS: ${{ inputs.namespace }}
run: |
set -eu
REGISTRY_SECRET_NAME="ghcr-registry-creds"
BICEP_APP_NAME=$(grep -oP "name:\s*'\K[^']+" ".radius/app.bicep" 2>/dev/null | head -1)
[ -z "$BICEP_APP_NAME" ] && BICEP_APP_NAME="app"
APP_NS="default-$BICEP_APP_NAME"
# The recipe reads the secret from context.runtime.kubernetes.namespace on
# the control plane. Materialize it in both the environment namespace and
# the application namespace so the read resolves regardless of scope. These
# kubectl calls run against the default kubeconfig (the control plane), not
# the target cluster.
for NS in "$ENV_NS" "$APP_NS"; do
kubectl get namespace "$NS" >/dev/null 2>&1 || kubectl create namespace "$NS"
kubectl create secret generic "$REGISTRY_SECRET_NAME" \
--namespace "$NS" \
--from-literal=username="$REGISTRY_USERNAME" \
--from-literal=password="$REGISTRY_PASSWORD" \
--dry-run=client -o yaml | kubectl apply -f -
echo "Provisioned $REGISTRY_SECRET_NAME in control-plane namespace $NS"
done

- name: Run rad commands
shell: bash
env:
ENVIRONMENT: ${{ inputs.environment }}
APP_FILE: ${{ inputs.app-file }}
APP_IMAGE: ${{ inputs.app-image }}
# Registry push/pull credentials for the app deploy. These are passed to
# the app file's `rad deploy` as the registryUsername/registryPassword
# parameters, which feed the app's Radius.Security/secrets resource
# (`ghcr-registry-creds`). That resource materializes the registry Secret
# on the TARGET cluster, where the containerImages recipe reads it by the
# name the recipe pack registers. Read via the environment (not inlined
# into the script) so the secret value can't break the shell or be
# word-split, and is never written into the recorded command string.
REGISTRY_USERNAME: ${{ inputs.registry-username }}
REGISTRY_PASSWORD: ${{ inputs.registry-password }}
# Pass caller input through the environment to avoid command injection.
# Caller-supplied rad commands (falls back to the RADIUS_RAD_COMMANDS
# variable upstream) so the command applies on both an explicit dispatch
Expand Down Expand Up @@ -155,6 +132,10 @@ runs:
code=${PIPESTATUS[0]}
echo "::endgroup::"
Comment thread
sk593 marked this conversation as resolved.
RAN=$((RAN + 1))
# Redact any known secret values from the captured output before it is
# written into the uploaded artifact. GitHub's log masking does not apply
# to artifact file contents, so scrub them ourselves.
scrub_secrets "$outfile"
COMMANDS_JSON=$(jq \
--argjson index "$index" \
--arg cmd "$display" \
Expand All @@ -167,6 +148,49 @@ runs:
return "$code"
}

# True when RADIUS_DEPLOY_PARAMS (a JSON object) already carries the given
# key. Used to avoid passing a --parameters twice when an older environment
# still supplies registryUsername/registryPassword in the params secret.
deploy_params_has_key() {
[ -n "${RADIUS_DEPLOY_PARAMS//[[:space:]]/}" ] || return 1
printf '%s' "$RADIUS_DEPLOY_PARAMS" | jq -e --arg k "$1" 'has($k)' >/dev/null 2>&1
}
Comment thread
sk593 marked this conversation as resolved.

# Secret values that must never reach the logs or the uploaded artifact:
# the registry password (the runner GITHUB_TOKEN) and every value in the
# RADIUS_DEPLOY_PARAMS secret. Register each with ::add-mask:: so GitHub
# redacts it from the live logs (defense in depth -- registered secrets are
# already masked, but values sourced from vars are not), and keep the list
# for scrub_secrets to strip from the captured output.
SECRET_VALUES=()
add_secret() {
[ -n "$1" ] || return 0
SECRET_VALUES+=("$1")
echo "::add-mask::$1"
}
Comment thread
sk593 marked this conversation as resolved.
Comment thread
sk593 marked this conversation as resolved.
add_secret "$REGISTRY_PASSWORD"
if [ -n "${RADIUS_DEPLOY_PARAMS//[[:space:]]/}" ]; then
while IFS= read -r _sname; do
[ -z "$_sname" ] && continue
add_secret "$(printf '%s' "$RADIUS_DEPLOY_PARAMS" | jq -r --arg k "$_sname" '.[$k]')"
done < <(printf '%s' "$RADIUS_DEPLOY_PARAMS" | jq -r 'keys_unsorted[]')
fi
Comment thread
sk593 marked this conversation as resolved.

# Replace every known secret value in the given file with *** in place, so
# the artifact never carries a credential that `rad` happened to echo. Uses
# bash literal substitution (no regex) so values with special characters are
# matched verbatim.
scrub_secrets() {
local file="$1" content value
[ ${#SECRET_VALUES[@]} -eq 0 ] && return 0
content=$(cat "$file")
for value in "${SECRET_VALUES[@]}"; do
[ -n "$value" ] || continue
content=${content//"$value"/***}
done
printf '%s' "$content" > "$file"
}
Comment thread
sk593 marked this conversation as resolved.
Outdated

if [ -n "${RAD_COMMANDS//[[:space:]]/}" ]; then
# Caller-supplied commands: a single command string or a JSON array, run in
# order with the `rad` prefix omitted.
Expand Down Expand Up @@ -200,22 +224,41 @@ runs:
for cmd in "${CLEAN[@]}"; do
verb="${cmd%% *}"
if [ "$verb" = "deploy" ]; then
# For deploy commands, append the image and the secret application
# parameters. Build an argv array so secret values with special
# characters are never word-split, and keep them out of the
# For deploy commands, build an argv array so secret values with
# special characters are never word-split, and keep them out of the
# recorded command string (only the non-secret `$cmd` is recorded).
# shellcheck disable=SC2086
read -ra CMD_ARGV <<< "$cmd"
EXTRA_PARAMS=()
if [ -n "$APP_IMAGE" ]; then
EXTRA_PARAMS+=(--parameters "image=$APP_IMAGE")
fi
if [ -n "${RADIUS_DEPLOY_PARAMS//[[:space:]]/}" ]; then
while IFS= read -r _pname; do
[ -z "$_pname" ] && continue
_pval=$(printf '%s' "$RADIUS_DEPLOY_PARAMS" | jq -r --arg k "$_pname" '.[$k]')
EXTRA_PARAMS+=(--parameters "$_pname=$_pval")
done < <(printf '%s' "$RADIUS_DEPLOY_PARAMS" | jq -r 'keys_unsorted[]')
# The image, application, and registry parameters belong to the app
# only. `rad deploy` rejects unknown parameters, so a custom command
# that deploys a different template (e.g. an environment/recipe-pack
# bicep) must not receive them. The deploy target is the first
# positional argument after the `deploy` verb; inject the app-only
# parameters only when it is APP_FILE.
DEPLOY_TARGET="${CMD_ARGV[1]:-}"
if [ "$DEPLOY_TARGET" = "$APP_FILE" ]; then
Comment thread
sk593 marked this conversation as resolved.
Outdated
Comment thread
sk593 marked this conversation as resolved.
Outdated
if [ -n "$APP_IMAGE" ]; then
EXTRA_PARAMS+=(--parameters "image=$APP_IMAGE")
fi
if [ -n "${RADIUS_DEPLOY_PARAMS//[[:space:]]/}" ]; then
while IFS= read -r _pname; do
[ -z "$_pname" ] && continue
_pval=$(printf '%s' "$RADIUS_DEPLOY_PARAMS" | jq -r --arg k "$_pname" '.[$k]')
EXTRA_PARAMS+=(--parameters "$_pname=$_pval")
done < <(printf '%s' "$RADIUS_DEPLOY_PARAMS" | jq -r 'keys_unsorted[]')
fi
# Registry credentials for the app's Radius.Security/secrets
# resource. Appended to the argv array so the secret value is never
# word-split or written into the recorded command string. Skipped
# when the params secret already supplies the key, so the same
# --parameters is never passed twice.
if [ -n "$REGISTRY_USERNAME" ] && ! deploy_params_has_key registryUsername; then
EXTRA_PARAMS+=(--parameters "registryUsername=$REGISTRY_USERNAME")
fi
if [ -n "$REGISTRY_PASSWORD" ] && ! deploy_params_has_key registryPassword; then
EXTRA_PARAMS+=(--parameters "registryPassword=$REGISTRY_PASSWORD")
fi
fi
if ! record "$idx" "$cmd" rad "${CMD_ARGV[@]}" "${EXTRA_PARAMS[@]}"; then
OVERALL_OUTCOME="command_failed"
Expand Down Expand Up @@ -255,6 +298,17 @@ runs:
DEPLOY_PARAMS+=(--parameters "$_pname=$_pval")
done < <(printf '%s' "$RADIUS_DEPLOY_PARAMS" | jq -r 'keys_unsorted[]')
fi
# Registry credentials for the app's Radius.Security/secrets resource
# (`ghcr-registry-creds`). Appended to the argv array so the secret
# value is never word-split or written into the recorded command string.
# Skipped when the params secret already supplies the key, so the same
# --parameters is never passed twice.
if [ -n "$REGISTRY_USERNAME" ] && ! deploy_params_has_key registryUsername; then
DEPLOY_PARAMS+=(--parameters "registryUsername=$REGISTRY_USERNAME")
fi
if [ -n "$REGISTRY_PASSWORD" ] && ! deploy_params_has_key registryPassword; then
DEPLOY_PARAMS+=(--parameters "registryPassword=$REGISTRY_PASSWORD")
fi
# The recorded command string omits the parameters so secret values are
# not written into the result artifact.
if ! record 0 "deploy $APP_FILE --environment $ENVIRONMENT" \
Expand Down
8 changes: 4 additions & 4 deletions .github/extension/run-rad-commands-aws.yml
Original file line number Diff line number Diff line change
Expand Up @@ -232,10 +232,10 @@ jobs:
# Registry and credentials for the Radius.Compute/containerImages recipe.
# The recipe builds images with the in-pod BuildKit and pushes them to
# $BUILD_REGISTRY, authenticating with the Kubernetes Secret named
# $REGISTRY_SECRET_NAME. Because the recipe loads that Secret via the
# in-cluster provider (the control-plane cluster where dynamic-rp/BuildKit
# run), the secret is provisioned directly onto the control plane by the
# run-and-teardown action. Only the secret NAME is wired here via the
# $REGISTRY_SECRET_NAME. That Secret is created on the target cluster by
# the app's Radius.Security/secrets resource during `rad deploy` (fed by
# the registryUsername/registryPassword parameters injected by the
# run-rad-commands action). Only the secret NAME is wired here via the
# recipe pack's registrySecretName. $BUILD_REGISTRY is ghcr.io/<owner>/<repo>
# so images land under the repository's package namespace; it must be lowercase.
BUILD_REGISTRY=$(echo "${{ vars.RADIUS_BUILD_REGISTRY || format('ghcr.io/{0}', github.repository) }}" | tr '[:upper:]' '[:lower:]')
Expand Down
8 changes: 4 additions & 4 deletions .github/extension/run-rad-commands-azure.yml
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,10 @@ jobs:
# Registry and credentials for the Radius.Compute/containerImages recipe.
# The recipe builds images with the in-pod BuildKit and pushes them to
# $BUILD_REGISTRY, authenticating with the Kubernetes Secret named
# $REGISTRY_SECRET_NAME. Because the recipe loads that Secret via the
# in-cluster provider (the control-plane cluster where dynamic-rp/BuildKit
# run), the secret is provisioned directly onto the control plane by the
# run-and-teardown action. Only the secret NAME is wired here via the
# $REGISTRY_SECRET_NAME. That Secret is created on the target cluster by
# the app's Radius.Security/secrets resource during `rad deploy` (fed by
# the registryUsername/registryPassword parameters injected by the
# run-rad-commands action). Only the secret NAME is wired here via the
# recipe pack's containerImagesRegistrySecretName. $BUILD_REGISTRY is
# ghcr.io/<owner>/<repo> so images land under the repository's package
# namespace; it must be lowercase.
Expand Down
Loading