Skip to content

Instantly share code, notes, and snippets.

@ytimocin
Last active April 17, 2025 06:52
Show Gist options
  • Select an option

  • Save ytimocin/8887d95ab1409562f4646fd30edb101c to your computer and use it in GitHub Desktop.

Select an option

Save ytimocin/8887d95ab1409562f4646fd30edb101c to your computer and use it in GitHub Desktop.
Radius on Air-gapped Cluster
#!/bin/bash
set -e
# =====================================
# Configuration
# =====================================
CLUSTER_NAME="air-gapped-cluster"
REGISTRY_NAME="registry.localhost"
REGISTRY_SERVER="localhost"
REGISTRY_PORT="6060"
NETWORK_NAME="kind-network"
CERTS_DIR="$(pwd)/certs"
CA_CERT_PATH="/etc/ssl/certs/local-ca.crt"
# Registry URLs
REGISTRY_HOST="${REGISTRY_NAME}:${REGISTRY_PORT}"
LOCALHOST_REGISTRY="localhost:${REGISTRY_PORT}"
# Radius version
RADIUS_VERSION="0.45"
# Chart paths
CHARTS_DIR="$(pwd)/charts"
mkdir -p "${CHARTS_DIR}"
RADIUS_CHART="${CHARTS_DIR}/radius-${RADIUS_VERSION}.0.tgz"
CONTOUR_CHART="${CHARTS_DIR}/contour-11.1.1.tgz"
DAPR_CHART="${CHARTS_DIR}/dapr-1.14.4.tgz"
# =====================================
# Helper Functions
# =====================================
log() {
local level=$1
shift
echo "$(date '+%Y-%m-%d %H:%M:%S') [${level}] $*"
}
info() {
log "INFO" "$@"
}
warn() {
log "WARN" "$@" >&2
}
error() {
log "ERROR" "$@" >&2
exit 1
}
check_prerequisites() {
info "Checking prerequisites..."
for cmd in docker kind kubectl helm mkcert; do
if ! command -v "${cmd}" &>/dev/null; then
error "${cmd} is required but not found. Please install it and try again."
fi
done
# Verify mkcert CA is installed
if ! mkcert -CAROOT &>/dev/null; then
error "mkcert CA not installed. Please run 'mkcert -install'"
fi
info "All prerequisites are installed."
}
# =====================================
# Cleanup Function
# =====================================
cleanup() {
info "Cleaning up existing resources..."
# Delete kind cluster if it exists
if kind get clusters | grep -q "^${CLUSTER_NAME}\$"; then
info "Deleting existing Kind cluster: ${CLUSTER_NAME}"
kind delete cluster --name "${CLUSTER_NAME}"
fi
# Delete registry container if it exists
if docker ps -a --format '{{.Names}}' | grep -q "^${REGISTRY_NAME}\$"; then
info "Deleting existing registry container: ${REGISTRY_NAME}"
docker rm -f "${REGISTRY_NAME}" >/dev/null 2>&1 || true
fi
# Remove the network if it exists
if docker network ls --format '{{.Name}}' | grep -q "^${NETWORK_NAME}\$"; then
info "Deleting existing Docker network: ${NETWORK_NAME}"
docker network rm "${NETWORK_NAME}" >/dev/null 2>&1 || true
fi
# Remove certificate directory
if [ -d "${CERTS_DIR}" ]; then
info "Removing certificates directory: ${CERTS_DIR}"
rm -rf "${CERTS_DIR}"
fi
info "Cleanup completed."
}
# =====================================
# Setup Certificate Authority
# =====================================
setup_certificates() {
info "Setting up certificates..."
mkdir -p "${CERTS_DIR}"
# Generate certificates with mkcert for the registry
info "Generating TLS certificates for registry"
mkcert -cert-file "${CERTS_DIR}/tls.crt" -key-file "${CERTS_DIR}/tls.key" \
"${REGISTRY_NAME}" "localhost" "127.0.0.1"
# Copy the CA certificate for later use
cp "$(mkcert -CAROOT)/rootCA.pem" "${CERTS_DIR}/ca.crt"
info "Certificates generated successfully."
}
# =====================================
# Create and Configure Secure Registry
# =====================================
setup_registry() {
info "Setting up secure registry..."
# Create a Docker network that the registry and Kind cluster will use
info "Creating Docker network: ${NETWORK_NAME}"
docker network create "${NETWORK_NAME}" || true
# Start a secure Docker registry with TLS enabled
info "Starting secure registry: ${REGISTRY_NAME}:${REGISTRY_PORT}"
docker run -d \
--name "${REGISTRY_NAME}" \
--network "${NETWORK_NAME}" \
--network-alias "${REGISTRY_NAME}" \
-p "${REGISTRY_PORT}:6060" \
-v "${CERTS_DIR}:/certs" \
-e REGISTRY_HTTP_ADDR=0.0.0.0:6060 \
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/tls.crt \
-e REGISTRY_HTTP_TLS_KEY=/certs/tls.key \
--restart always \
registry:2
# Wait for the registry to start
info "Waiting for registry to initialize..."
for _ in $(seq 1 10); do
if curl -s --cacert "${CERTS_DIR}/ca.crt" "https://${LOCALHOST_REGISTRY}/v2/" >/dev/null; then
info "Registry is ready."
return 0
fi
sleep 2
done
error "Registry failed to initialize after 20 seconds."
}
# =====================================
# Download Required Helm Charts
# =====================================
download_charts() {
info "Downloading required Helm charts..."
mkdir -p "${CHARTS_DIR}"
# Download Radius chart
info "Downloading Radius chart..."
helm pull oci://ghcr.io/radius-project/helm-chart/radius --version "${RADIUS_VERSION}.0" -d "${CHARTS_DIR}"
# Download Contour chart
info "Downloading Contour chart..."
helm pull oci://registry-1.docker.io/bitnamicharts/contour --version 11.1.1 -d "${CHARTS_DIR}"
# Dapr (classic repo)
info "Adding Dapr Helm repo and pulling Dapr chart..."
helm repo add dapr https://dapr.github.io/helm-charts/ &&
helm repo update &&
helm pull dapr/dapr --version 1.14.4 -d "${CHARTS_DIR}"
info "Helm charts downloaded successfully."
}
# =====================================
# Pull and Push Required Images
# =====================================
mirror_images() {
info "Mirroring container images to local registry..."
# List of images to mirror
declare -a IMAGES=(
# Radius images
"ghcr.io/radius-project/ucpd:${RADIUS_VERSION}"
"ghcr.io/radius-project/dynamic-rp:${RADIUS_VERSION}"
"ghcr.io/radius-project/mirror/postgres:latest"
"ghcr.io/radius-project/dashboard:${RADIUS_VERSION}"
"ghcr.io/radius-project/controller:${RADIUS_VERSION}"
"ghcr.io/radius-project/deployment-engine:${RADIUS_VERSION}"
"ghcr.io/radius-project/applications-rp:${RADIUS_VERSION}"
"ghcr.io/radius-project/bicep:${RADIUS_VERSION}"
# Contour images
"docker.io/bitnami/contour:1.24.2-debian-11-r1"
"docker.io/bitnami/envoy:1.24.3-debian-11-r4"
# Dapr images
"ghcr.io/dapr/operator:1.14.4"
"ghcr.io/dapr/placement:1.14.4"
"ghcr.io/dapr/scheduler:1.14.4"
"ghcr.io/dapr/sentry:1.14.4"
"ghcr.io/dapr/injector:1.14.4"
# Other images
"docker.io/rancher/mirrored-pause:3.6"
)
# Pull and push each image
local success_count=0
local total_images=${#IMAGES[@]}
for image in "${IMAGES[@]}"; do
info "Processing image: ${image}"
# Extract repository name and tag properly
local image_repo
image_repo=$(echo "${image}" | cut -d ':' -f1 | sed -e 's/^[^\/]*\///')
local tag
tag=$(echo "${image}" | cut -d ':' -f2)
if [[ -z "${tag}" ]]; then
tag="latest"
fi
# Special case for mirror/postgres
if [[ "${image}" == *"mirror/postgres"* ]]; then
image_repo="mirror/postgres"
fi
# Pull the image
if ! docker pull "${image}"; then
warn "Failed to pull ${image}, skipping..."
continue
fi
# Tag for local registry (preserving proper tag structure)
local local_image="${LOCALHOST_REGISTRY}/${image_repo}:${tag}"
docker tag "${image}" "${local_image}"
# Push to local registry
info "Pushing to local registry: ${local_image}"
if docker push "${local_image}"; then
success_count=$((success_count + 1))
else
warn "Failed to push ${local_image} to local registry."
fi
done
info "Successfully mirrored ${success_count}/${total_images} images."
if [[ "${success_count}" -eq 0 ]]; then
error "No images were mirrored. Cannot continue."
fi
}
# =====================================
# Create Kind Cluster with Registry
# =====================================
create_cluster() {
info "Creating Kind cluster with secure registry..."
cat <<EOF | kind create cluster --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: ${CLUSTER_NAME}
nodes:
- role: control-plane
extraMounts:
- containerPath: "${CA_CERT_PATH}"
hostPath: "${CERTS_DIR}/ca.crt"
containerdConfigPatches:
- |-
[plugins."io.containerd.grpc.v1.cri".registry]
config_path = "/etc/containerd/certs.d"
EOF
# Get the kind network name (usually kind-${CLUSTER_NAME})
KIND_NETWORK="kind"
if docker network ls | grep -q "kind-${CLUSTER_NAME}"; then
KIND_NETWORK="kind-${CLUSTER_NAME}"
info "Using Kind network: ${KIND_NETWORK}"
fi
# Connect registry container to the kind network
info "Connecting registry to the Kind network: ${KIND_NETWORK}"
if docker network connect "${KIND_NETWORK}" "${REGISTRY_NAME}" 2>/dev/null; then
info "Registry connected to Kind network: ${KIND_NETWORK}"
else
info "Registry was already connected to the network or connection failed"
fi
# Get the IP address of the registry in the kind network - Fixed command with proper quoting
REGISTRY_IP=$(docker inspect --format="{{range .NetworkSettings.Networks}}{{if eq .NetworkID \"$(docker network inspect ${KIND_NETWORK} --format='{{.Id}}')\"}}{{.IPAddress}}{{end}}{{end}}" "${REGISTRY_NAME}")
if [ -z "${REGISTRY_IP}" ]; then
# Fallback method to get IP if the first approach fails
REGISTRY_IP=$(docker inspect -f "{{range .NetworkSettings.Networks}}{{if eq .NetworkName \"${KIND_NETWORK}\"}}{{.IPAddress}}{{end}}{{end}}" "${REGISTRY_NAME}")
if [ -z "${REGISTRY_IP}" ]; then
# Last resort - just get any IP address
REGISTRY_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "${REGISTRY_NAME}" | head -n1)
if [ -z "${REGISTRY_IP}" ]; then
error "Failed to get registry IP address in any network"
fi
fi
fi
info "Registry IP in ${KIND_NETWORK} network: ${REGISTRY_IP}"
# Create the directory for the certificates and add the certificate to the system trust store
LOCALHOST_DIR="/etc/containerd/certs.d/${REGISTRY_SERVER}:${REGISTRY_PORT}"
REGISTRY_DIR="/etc/containerd/certs.d/${REGISTRY_NAME}:${REGISTRY_PORT}"
for node in $(kind get nodes --name "${CLUSTER_NAME}"); do
# Add registry hostname to /etc/hosts in the Kind node
info "Adding ${REGISTRY_NAME} -> ${REGISTRY_IP} to /etc/hosts in ${node}"
docker exec "${node}" sh -c "echo '${REGISTRY_IP} ${REGISTRY_NAME}' >> /etc/hosts"
docker exec "${node}" sh -c "cat /etc/hosts | grep ${REGISTRY_NAME}" || true
# Configure containerd for localhost:PORT
docker exec "${node}" mkdir -p "${LOCALHOST_DIR}"
cat <<EOF | docker exec -i "${node}" sh -c "cat > ${LOCALHOST_DIR}/hosts.toml"
[host."https://${REGISTRY_SERVER}:${REGISTRY_PORT}"]
capabilities = ["pull", "resolve", "push"]
ca = "${CA_CERT_PATH}"
EOF
# Configure containerd for registry.localhost:PORT
docker exec "${node}" mkdir -p "${REGISTRY_DIR}"
cat <<EOF | docker exec -i "${node}" sh -c "cat > ${REGISTRY_DIR}/hosts.toml"
[host."https://${REGISTRY_NAME}:${REGISTRY_PORT}"]
capabilities = ["pull", "resolve", "push"]
ca = "${CA_CERT_PATH}"
EOF
# Restart containerd for changes to take effect
info "Restarting containerd on ${node}"
docker exec "${node}" sh -c "if command -v systemctl > /dev/null; then systemctl restart containerd; else killall -SIGHUP containerd; fi"
done
# Document the local registry
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
name: local-registry-hosting
namespace: kube-public
data:
localRegistryHosting.v1: |
host: "${REGISTRY_NAME}:${REGISTRY_PORT}"
help: "https://kind.sigs.k8s.io/docs/user/local-registry/"
secure: true
EOF
info "Kind cluster created successfully."
# Wait to make sure everything is stable
info "Waiting a moment for registry connection to stabilize..."
sleep 15
}
# =====================================
# Verify Registry Connectivity
# =====================================
verify_registry() {
info "Verifying connectivity to the registry from the cluster..."
kubectl run registry-test \
--image="${REGISTRY_HOST}/mirror/postgres:latest" \
--restart=Never \
--command -- echo "Registry connectivity test successful"
# Wait for the pod to complete
local retries=0
local max_retries=30
while [[ "${retries}" -lt "${max_retries}" ]]; do
if kubectl get pod registry-test -o jsonpath='{.status.phase}' 2>/dev/null | grep -q "Succeeded"; then
info "Registry connectivity test successful!"
kubectl delete pod registry-test --wait=false
return 0
fi
if kubectl get pod registry-test -o jsonpath='{.status.phase}' 2>/dev/null | grep -q "Failed"; then
error "Registry connectivity test failed. Check pod logs: kubectl logs registry-test"
fi
retries=$((retries + 1))
sleep 2
done
kubectl describe pod registry-test
error "Registry connectivity test timed out after ${max_retries} tries"
}
# =====================================
# Generate Installation Script
# =====================================
generate_install_script() {
info "Generating Radius installation script..."
cat >install-radius-airgapped.sh <<EOF
#!/bin/bash
set -e
echo "Installing Radius in air-gapped environment..."
# Optional: Verify internet is disabled for truly air-gapped testing
# if ping -c 1 google.com &> /dev/null; then
# echo "Warning: Internet connection detected. For a true air-gapped test, disconnect your network."
# read -p "Continue anyway? (y/n) " -n 1 -r
# echo
# if [[ ! \$REPLY =~ ^[Yy]$ ]]; then
# exit 1
# fi
# fi
# Install Radius with local charts and registry
rad install kubernetes \
--chart ${RADIUS_CHART} \
--contour-chart ${CONTOUR_CHART} \
--dapr-chart ${DAPR_CHART} \
--set rp.image=${REGISTRY_HOST}/radius-project/applications-rp,rp.tag=${RADIUS_VERSION} \
--set dynamicrp.image=${REGISTRY_HOST}/radius-project/dynamic-rp,dynamicrp.tag=${RADIUS_VERSION} \
--set controller.image=${REGISTRY_HOST}/radius-project/controller,controller.tag=${RADIUS_VERSION} \
--set ucp.image=${REGISTRY_HOST}/radius-project/ucpd,ucp.tag=${RADIUS_VERSION} \
--set bicep.image=${REGISTRY_HOST}/radius-project/bicep,bicep.tag=${RADIUS_VERSION} \
--set de.image=${REGISTRY_HOST}/radius-project/deployment-engine,de.tag=${RADIUS_VERSION} \
--set dashboard.image=${REGISTRY_HOST}/radius-project/dashboard,dashboard.tag=${RADIUS_VERSION} \
--set database.image=${REGISTRY_HOST}/mirror/postgres,database.tag=latest \
--dapr-set global.registry=${REGISTRY_HOST}/dapr \
--contour-set global.imageRegistry=${REGISTRY_HOST}
echo "Radius installation complete."
EOF
chmod +x install-radius-airgapped.sh
info "Installation script generated: install-radius-airgapped.sh"
}
# =====================================
# Main Execution
# =====================================
main() {
info "Starting Air-Gapped Radius Setup"
# Step 1: Check prerequisites
check_prerequisites
# Step 2: Clean up existing resources
cleanup
# Step 3: Setup certificates
setup_certificates
# Step 4: Create secure registry
setup_registry
# Step 5: Download Helm charts
download_charts
# Step 6: Mirror images to local registry
mirror_images
# Step 7: Create Kind cluster with registry configuration
create_cluster
# Step 8: Verify registry connectivity
verify_registry
# Step 9: Generate installation script
generate_install_script
info "==============================================="
info "Air-Gapped Radius Environment Setup Complete!"
info "==============================================="
info "To install Radius:"
info "1. Ensure no internet connection if desired for testing"
info "2. Run: ./install-radius-airgapped.sh"
}
# Execute main function
main "$@"
@ytimocin
Copy link
Author

Line 91 includes all the images that should be available to the air-gapped cluster.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment