Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
62 changes: 62 additions & 0 deletions .github/extension/actions/run-rad-commands/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,24 @@ inputs:
registry-password:
description: Password/token passed when the app declares the registryPassword parameter (feeds its Radius.Security/secrets registry Secret).
required: true
build-arch-mode:
description: >-
Target-cluster architecture mode for Radius.Compute/containerImages builds.
'detect' probes the target cluster node architectures; a single-arch cluster
builds only that platform, a mixed or undetermined cluster falls back to
build-fallback-platforms. An explicit platform list (any value containing
'/', e.g. 'linux/amd64') is honored verbatim. Empty or an unsubstituted
'{{TARGET_CLUSTER_ARCH_MODE}}' placeholder disables the feature and preserves
the recipe's default platforms.
required: false
default: ""
build-fallback-platforms:
description: >-
Comma-separated platform list used when build-arch-mode is 'detect' and the
target cluster is mixed-arch or its architecture cannot be determined. Empty
defaults to 'linux/amd64,linux/arm64'.
required: false
default: ""

runs:
using: composite
Expand Down Expand Up @@ -64,6 +82,50 @@ runs:
kubectl --kubeconfig "$TARGET_KUBECONFIG" create namespace "$APP_NS"
fi

- name: Compute container build platforms from target cluster architecture
shell: bash
env:
BUILD_ARCH_MODE: ${{ inputs.build-arch-mode }}
BUILD_FALLBACK_PLATFORMS: ${{ inputs.build-fallback-platforms }}
run: |
set -euo pipefail

# Feature off when the mode is empty or still an unsubstituted template
# placeholder: emit nothing and preserve the recipe's default platforms.
case "$BUILD_ARCH_MODE" in
"" | "{{TARGET_CLUSTER_ARCH_MODE}}")
echo "Architecture-aware container builds disabled; using recipe default platforms."
exit 0
;;
esac

# Only 'detect' consults the cluster; an explicit platform list is honored
# verbatim by the helper without any kubectl call.
ARCHES=""
if [ "$BUILD_ARCH_MODE" = "detect" ]; then
# Default-expand so an unset RADIUS_TARGET_KUBECONFIG does not trip
# `set -u`; the empty case is handled below and degrades to fallback.
TARGET_KUBECONFIG="${RADIUS_TARGET_KUBECONFIG:-}"
if [ -n "$TARGET_KUBECONFIG" ] && [ -f "$TARGET_KUBECONFIG" ]; then
# One architecture token per node; failures degrade to empty, which the
# helper treats as "undetermined" and resolves to the fallback list.
ARCHES=$(kubectl --kubeconfig "$TARGET_KUBECONFIG" get nodes \
-o jsonpath='{range .items[*]}{.status.nodeInfo.architecture}{"\n"}{end}' \
2>/dev/null || true)
echo "Detected target cluster node architectures: $(echo "$ARCHES" | tr '\n' ' ')"
else
echo "No target kubeconfig; cluster architecture undetermined, will use fallback."
fi
fi

EFFECTIVE=$(bash "$GITHUB_ACTION_PATH/compute-build-platforms.sh" \
"$BUILD_ARCH_MODE" "$BUILD_FALLBACK_PLATFORMS" "$ARCHES")

if [ -n "$EFFECTIVE" ]; then
echo "RADIUS_EFFECTIVE_BUILD_PLATFORMS=$EFFECTIVE" >> "$GITHUB_ENV"
echo "Effective container build platforms: $EFFECTIVE"
fi

- name: Run rad commands
shell: bash
env:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#!/bin/bash

# Computes the effective container image build platforms for a Radius deploy,
# given the requested arch mode, a fallback platform list, and the set of node
# architectures detected on the target cluster.
#
# This exists because Radius.Compute/containerImages builds run in an in-cluster
# BuildKit that is compiled for the runner's architecture (amd64 on standard
# GitHub-hosted runners). When an app leaves build.platforms unset, the recipe
# defaults to a multi-arch build (linux/amd64,linux/arm64), so the arm64 half is
# produced under QEMU emulation -- an order of magnitude slower and prone to
# emulation crashes. When the target cluster is single-arch, building only that
# one platform avoids emulation entirely; when it is mixed (or we cannot tell),
# a multi-arch fallback preserves portability.
#
# Contract (kept intentionally small and explicit):
# MODE
# - '' Feature disabled: emit nothing so the recipe
# default platforms apply (existing behavior).
# - '{{TARGET_CLUSTER_ARCH_MODE}}'
# An unsubstituted template placeholder is
# treated the same as empty (disabled).
# - 'detect' Probe the target cluster node architectures.
# - an explicit platform list Any value containing '/', e.g. 'linux/amd64'
# or 'linux/amd64,linux/arm64', is honored
# verbatim with no detection.
# FALLBACK
# - comma-separated platform list used when detection is inconclusive
# (mixed-arch or undetermined). Empty or an unsubstituted placeholder
# defaults to 'linux/amd64,linux/arm64'.
# ARCHES
# - whitespace/newline-separated node architecture tokens as reported by
# kubectl (.status.nodeInfo.architecture), e.g. 'amd64' or 'amd64 arm64'.
#
# Output: the effective comma-separated platform list on stdout, or nothing when
# the feature is disabled. Detection outcomes:
# - exactly one recognized arch -> that single platform (no emulation)
# - multiple archs, or none/unknown -> the FALLBACK list
#
# The script is both sourceable (exposes compute_build_platforms) and directly
# executable (compute-build-platforms.sh MODE FALLBACK ARCHES).

set -euo pipefail

readonly DEFAULT_FALLBACK_PLATFORMS="linux/amd64,linux/arm64"
readonly MODE_PLACEHOLDER="{{TARGET_CLUSTER_ARCH_MODE}}"
readonly FALLBACK_PLACEHOLDER="{{TARGET_CLUSTER_ARCH_FALLBACK_PLATFORMS}}"

# Map a node architecture token to an OCI platform, or empty when unrecognized.
_arch_to_platform() {
case "$1" in
amd64 | x86_64) printf 'linux/amd64' ;;
arm64 | aarch64) printf 'linux/arm64' ;;
*) printf '' ;;
esac
}

# Normalize a comma/whitespace-separated platform list: trim each entry, drop
# blanks, de-duplicate, and re-join with commas in sorted (deterministic) order.
_normalize_platforms() {
# Turn commas into spaces, then let unquoted expansion word-split on all IFS
# whitespace (space, tab, newline) so mixed separators collapse to one token
# per line without relying on multi-character tr sets.
# shellcheck disable=SC2086
printf '%s\n' ${1//,/ } |
sed '/^$/d' |
sort -u |
paste -sd, -
}

# Resolve the fallback list, applying the default when empty or a placeholder.
_resolve_fallback() {
local fallback="$1"
if [[ -z "$fallback" || "$fallback" == "$FALLBACK_PLACEHOLDER" ]]; then
fallback="$DEFAULT_FALLBACK_PLATFORMS"
fi
_normalize_platforms "$fallback"
}

# compute_build_platforms MODE FALLBACK ARCHES -> effective platforms on stdout.
compute_build_platforms() {
local mode="${1:-}"
local fallback="${2:-}"
local arches="${3:-}"

# Feature disabled: empty or an unsubstituted placeholder. Emit nothing so
# callers inject no platform parameter and the recipe default applies.
if [[ -z "$mode" || "$mode" == "$MODE_PLACEHOLDER" ]]; then
return 0
fi

# Explicit override: any value that looks like a platform list (contains a
# '/') is honored verbatim, no detection.
if [[ "$mode" == */* ]]; then
_normalize_platforms "$mode"
return 0
fi

if [[ "$mode" != "detect" ]]; then
# Unrecognized mode keyword. Fail safe to the portable fallback rather
# than guessing, and warn so the misconfiguration is visible.
echo "compute-build-platforms: unrecognized mode '${mode}', using fallback platforms" >&2
_resolve_fallback "$fallback"
return 0
fi

# detect: collect the distinct recognized platforms across all nodes.
local platforms=()
local unknown=0
local token platform
for token in $arches; do
platform=$(_arch_to_platform "$token")
if [[ -z "$platform" ]]; then
unknown=1
continue
fi
platforms+=("$platform")
done

local distinct
distinct=$(_normalize_platforms "$(printf '%s\n' "${platforms[@]:-}")")

# Single recognized arch across the whole cluster, and nothing unrecognized:
# build just that platform and skip emulation.
if [[ "$unknown" -eq 0 && -n "$distinct" && "$distinct" != *,* ]]; then
printf '%s' "$distinct"
return 0
fi

# Mixed-arch, empty, or anything we could not classify confidently.
_resolve_fallback "$fallback"
}

# Run directly when invoked as a script (not when sourced by tests).
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
compute_build_platforms "${1:-}" "${2:-}" "${3:-}"
fi
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/bin/bash

# Tests for compute-build-platforms.sh (the effective container build platform
# resolver) and for the workflow/action wiring that feeds it. Run directly or via
# `make test-build-platforms`.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_DIR
readonly SCRIPT="${SCRIPT_DIR}/compute-build-platforms.sh"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../../.." && pwd)"
readonly REPO_ROOT
readonly AZURE_WF="${REPO_ROOT}/.github/extension/run-rad-commands-azure.yml"
readonly AWS_WF="${REPO_ROOT}/.github/extension/run-rad-commands-aws.yml"
readonly ACTION="${SCRIPT_DIR}/action.yml"

fail() {
echo "FAIL: $*" >&2
exit 1
}

# assert_platforms MODE FALLBACK ARCHES EXPECTED
assert_platforms() {
local mode="$1" fallback="$2" arches="$3" expected="$4"
local actual
actual="$(bash "${SCRIPT}" "${mode}" "${fallback}" "${arches}")"
if [[ "${actual}" != "${expected}" ]]; then
fail "compute('${mode}','${fallback}','${arches}') = '${actual}', expected '${expected}'"
fi
}

DEFAULT="linux/amd64,linux/arm64"

# --- Feature disabled -------------------------------------------------------
# Empty mode and an unsubstituted placeholder both emit nothing so the recipe's
# default platforms apply (existing behavior preserved).
assert_platforms "" "" "" ""
assert_platforms "" "${DEFAULT}" "amd64" ""
assert_platforms "{{TARGET_CLUSTER_ARCH_MODE}}" "${DEFAULT}" "amd64 arm64" ""

# --- detect: single-arch cluster -> that single platform, no emulation ------
assert_platforms "detect" "${DEFAULT}" "amd64" "linux/amd64"
assert_platforms "detect" "${DEFAULT}" "arm64" "linux/arm64"
assert_platforms "detect" "${DEFAULT}" "x86_64" "linux/amd64"
assert_platforms "detect" "${DEFAULT}" "aarch64" "linux/arm64"
# Multiple nodes, all the same arch.
assert_platforms "detect" "${DEFAULT}" "amd64
amd64
amd64" "linux/amd64"

# --- detect: mixed / undetermined -> fallback -------------------------------
assert_platforms "detect" "${DEFAULT}" "amd64 arm64" "${DEFAULT}"
assert_platforms "detect" "${DEFAULT}" "" "${DEFAULT}"
# An unknown architecture is not classified confidently -> fallback.
assert_platforms "detect" "${DEFAULT}" "ppc64le" "${DEFAULT}"
# One known plus one unknown is still not confidently single-arch -> fallback.
assert_platforms "detect" "${DEFAULT}" "amd64 ppc64le" "${DEFAULT}"

# --- detect: custom / placeholder fallback ----------------------------------
assert_platforms "detect" "linux/arm64" "amd64 arm64" "linux/arm64"
assert_platforms "detect" "{{TARGET_CLUSTER_ARCH_FALLBACK_PLATFORMS}}" "amd64 arm64" "${DEFAULT}"

# --- Explicit override: honored verbatim, no detection ----------------------
assert_platforms "linux/amd64" "${DEFAULT}" "arm64" "linux/amd64"
assert_platforms "linux/amd64,linux/arm64" "${DEFAULT}" "" "${DEFAULT}"
# Whitespace and duplicates are normalized (trimmed, de-duplicated, sorted).
assert_platforms "linux/amd64, linux/amd64 , linux/arm64" "${DEFAULT}" "" "${DEFAULT}"

# --- Unrecognized mode -> fail safe to fallback, with a warning -------------
unrecognized_err="$(bash "${SCRIPT}" "bogus" "${DEFAULT}" "amd64" 2>&1 >/dev/null)"
Comment thread
sylvainsf marked this conversation as resolved.
assert_platforms "bogus" "${DEFAULT}" "amd64" "${DEFAULT}"
echo "${unrecognized_err}" | grep -q "unrecognized mode" ||
fail "expected a warning on stderr for an unrecognized mode"

# --- Workflow / action wiring ------------------------------------------------
for wf in "${AZURE_WF}" "${AWS_WF}"; do
grep -q "{{TARGET_CLUSTER_ARCH_MODE}}" "${wf}" ||
fail "expected TARGET_CLUSTER_ARCH_MODE placeholder in $(basename "${wf}")"
grep -q "{{TARGET_CLUSTER_ARCH_FALLBACK_PLATFORMS}}" "${wf}" ||
fail "expected TARGET_CLUSTER_ARCH_FALLBACK_PLATFORMS placeholder in $(basename "${wf}")"
grep -q "build-arch-mode:" "${wf}" ||
fail "expected build-arch-mode wired to run-rad-commands in $(basename "${wf}")"
grep -q "build-fallback-platforms:" "${wf}" ||
fail "expected build-fallback-platforms wired to run-rad-commands in $(basename "${wf}")"
done

grep -q "build-arch-mode:" "${ACTION}" ||
fail "expected build-arch-mode input declared in run-rad-commands action.yml"
grep -q "build-fallback-platforms:" "${ACTION}" ||
fail "expected build-fallback-platforms input declared in run-rad-commands action.yml"
grep -q "compute-build-platforms.sh" "${ACTION}" ||
fail "expected the detection step to invoke compute-build-platforms.sh"
grep -q "RADIUS_EFFECTIVE_BUILD_PLATFORMS" "${ACTION}" ||
fail "expected the detection step to export RADIUS_EFFECTIVE_BUILD_PLATFORMS"

echo "compute-build-platforms tests passed"
14 changes: 14 additions & 0 deletions .github/extension/actions/run-rad-commands/deploy-parameters.sh
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,18 @@ append_generated_app_params() {
--parameters "registryPassword=${REGISTRY_PASSWORD}"
)
fi
# Target-cluster architecture-aware container builds. When the deploy computed
# an effective platform list (see compute-build-platforms.sh) and the app
# declares a `platforms` parameter, pass it so Radius.Compute/containerImages
# builds only the needed platform(s) instead of the recipe's multi-arch
# default (which builds arm64 under QEMU emulation on an amd64 runner). Apps
# that do not declare `platforms` are unaffected, and a value already supplied
# via RADIUS_DEPLOY_PARAMS is not overridden.
if [[ -n "${RADIUS_EFFECTIVE_BUILD_PLATFORMS:-}" ]] &&
app_declares_parameter "platforms" &&
! deploy_params_has_key "platforms"; then
GENERATED_APP_PARAMS+=(
--parameters "platforms=${RADIUS_EFFECTIVE_BUILD_PLATFORMS}"
)
fi
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,30 @@ assert_param_absent "registryUsername=${REGISTRY_USERNAME}"
assert_param_present "registryPassword=${REGISTRY_PASSWORD}"
DEPLOY_PARAMS_JSON=""

# Architecture-aware build platforms: injected only when the app declares a
# `platforms` parameter and the deploy computed an effective platform list.
RADIUS_EFFECTIVE_BUILD_PLATFORMS="linux/amd64"

run_generated_params_case '{"platforms":{}}'
assert_param_present "platforms=linux/amd64"

# App does not declare `platforms`: nothing injected.
run_generated_params_case '{"environment":{}}'
assert_param_absent "platforms=linux/amd64"

# No effective platforms computed (feature off/undetermined): nothing injected.
RADIUS_EFFECTIVE_BUILD_PLATFORMS=""
run_generated_params_case '{"platforms":{}}'
assert_param_absent "platforms=linux/amd64"

# A value supplied via RADIUS_DEPLOY_PARAMS is not overridden by the computed one.
RADIUS_EFFECTIVE_BUILD_PLATFORMS="linux/amd64"
DEPLOY_PARAMS_JSON='{"platforms":"linux/arm64"}'
run_generated_params_case '{"platforms":{}}'
assert_param_absent "platforms=linux/amd64"
DEPLOY_PARAMS_JSON=""
RADIUS_EFFECTIVE_BUILD_PLATFORMS=""

reset_discovery
write_template '{"image":{}}'
load_declared_app_params "${APP_FILE}"
Expand Down
10 changes: 10 additions & 0 deletions .github/extension/run-rad-commands-aws.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ env:
ENVIRONMENT: ${{ inputs.environment }}
APP_FILE: '{{APP_FILE}}'
APP_IMAGE: ${{ inputs.image || github.sha || 'latest' }}
# Target-cluster architecture-aware container image builds. Rendered by the
# Radius extension: TARGET_CLUSTER_ARCH_MODE -> ${{ vars.RADIUS_BUILD_ARCH_MODE
# || 'detect' }} and TARGET_CLUSTER_ARCH_FALLBACK_PLATFORMS ->
# ${{ vars.RADIUS_BUILD_PLATFORMS || 'linux/amd64,linux/arm64' }}. When a
# template does not opt in, the unsubstituted placeholder disables the feature
# and the containerImages recipe's default platforms apply (existing behavior).
TARGET_CLUSTER_ARCH_MODE: '{{TARGET_CLUSTER_ARCH_MODE}}'
TARGET_CLUSTER_ARCH_FALLBACK_PLATFORMS: '{{TARGET_CLUSTER_ARCH_FALLBACK_PLATFORMS}}'

jobs:
deploy:
Expand Down Expand Up @@ -387,6 +395,8 @@ jobs:
deploy-params: ${{ secrets.RADIUS_DEPLOY_PARAMS }}
registry-username: ${{ github.actor }}
registry-password: ${{ secrets.GITHUB_TOKEN }}
build-arch-mode: ${{ env.TARGET_CLUSTER_ARCH_MODE }}
build-fallback-platforms: ${{ env.TARGET_CLUSTER_ARCH_FALLBACK_PLATFORMS }}

# Publish status on failure too: an empty Deployed tab is least helpful
# exactly when a deploy failed and the user is trying to see which
Expand Down
Loading
Loading