diff --git a/docs/demos/private-registries-recipes/PREREQUISITES.md b/docs/demos/private-registries-recipes/PREREQUISITES.md new file mode 100644 index 0000000000..042d507caa --- /dev/null +++ b/docs/demos/private-registries-recipes/PREREQUISITES.md @@ -0,0 +1,229 @@ +# Prerequisites: Setting up private registries for the demo + +This guide covers the **high-level steps to stand up the private registries** used +by the [Private Registries & Repositories demo](./README.md). The demo proves that +Radius can pull recipes from registries that require authentication, so you need +**at least one private registry you control** for each scenario you want to run: + +- **Scenario 1 (Bicep):** a private **OCI** registry that hosts Bicep artifacts. +- **Scenario 2 (Terraform):** a private **Terraform** module registry/repository + that authenticates with a token. +- **Scenario 3 (Combined):** both of the above. + +You do **not** need every provider listed below - pick whichever one you already +have access to. Each section is a short, provider-agnostic checklist plus one +concrete example. Once a registry is ready, return to the +[demo README](./README.md) and supply the values it produced. + +> These steps are for **demoing/testing only**. They favor the fastest path to a +> working private registry, not production hardening. Use throwaway/scoped +> credentials and tear them down afterward (see [Cleanup](#cleanup)). + +--- + +## Base tooling + +Before setting up any registry, make sure you have: + +1. A Kubernetes cluster and `kubectl` configured to talk to it. +2. The [`rad` CLI](https://docs.radapp.io/installation/) installed + (`rad version` works). The Bicep tooling for `rad bicep publish` is bundled. +3. Radius installed on the cluster with the `Radius.Core` resource types + available (`rad install kubernetes`). +4. The CLI for whichever registry provider you choose (e.g. `az`, `gh`, `aws`, + or `terraform`). + +--- + +## Part A - Private Bicep (OCI) registry + +**Goal:** a private OCI registry you can `rad bicep publish` to, and a +username/password pair the cluster can use to pull from it (the demo's Bicep +scenarios use `BasicAuth`). + +### High-level steps (any OCI provider) + +1. **Create a private OCI registry.** Any registry that speaks the OCI artifact + spec works - Azure Container Registry (ACR), GitHub Container Registry (GHCR), + Amazon ECR, Google Artifact Registry, Docker Hub (private repo), Harbor, etc. + Ensure it is **private** (not anonymously pullable) so the demo actually + exercises authentication. +2. **Create scoped pull credentials.** Generate a username + password/token that + has at least **pull** rights on the recipe repository (and **push** rights if + the *same* identity will publish the recipe). Prefer a scoped token over an + admin account. +3. **Authenticate your local tooling once** so you can push the recipe artifact + (e.g. `az acr login`, `docker login`, `gh auth token | docker login`). +4. **Record these values** - you'll pass them to the demo: + - `BICEP_REGISTRY` - the registry hostname, e.g. `myregistry.azurecr.io`. + - `BICEP_RECIPE` - the full artifact reference, e.g. + `myregistry.azurecr.io/recipes/redis:latest`. + - `BICEP_REGISTRY_USERNAME` / `BICEP_REGISTRY_PASSWORD` - the pull credentials. + +### Concrete example - Azure Container Registry (ACR) + +```bash +# 1. Create a private ACR (Basic SKU is fine for a demo). +az group create --name radius-demo-rg --location eastus +az acr create --resource-group radius-demo-rg --name myregistry --sku Basic + +# 2. Create a scoped, repository-limited token for pulling the recipe. +az acr token create \ + --registry myregistry \ + --name radius-demo-pull \ + --repository recipes/redis content/read content/write + +# The command prints a token password - capture it now (it is shown once). + +# 3. Log in locally so you can publish the recipe. +az acr login --name myregistry +``` + +Then record: + +```bash +export BICEP_REGISTRY="myregistry.azurecr.io" +export BICEP_RECIPE="${BICEP_REGISTRY}/recipes/redis:latest" +export BICEP_REGISTRY_USERNAME="radius-demo-pull" +export BICEP_REGISTRY_PASSWORD="" +``` + +### Concrete example - GitHub Container Registry (GHCR) + +```bash +# 1/2. Create a classic PAT with write:packages + read:packages scope at +# https://github.com/settings/tokens, then log in. +echo "" | docker login ghcr.io -u --password-stdin +``` + +```bash +export BICEP_REGISTRY="ghcr.io/" +export BICEP_RECIPE="${BICEP_REGISTRY}/recipes/redis:latest" +export BICEP_REGISTRY_USERNAME="" +export BICEP_REGISTRY_PASSWORD="" +``` + +> GHCR packages are private by default. Leave the package private so the demo +> exercises authentication. + +The demo's [Scenario 1](./README.md#scenario-1--private-bicep-recipe-registry-oci) +publishes [`recipes/redis-recipe.bicep`](./recipes/redis-recipe.bicep) to +`BICEP_RECIPE` and feeds the credentials into a `Radius.Core/bicepSettings`. + +--- + +## Part B - Private Terraform module registry / repository + +**Goal:** a private Terraform module source that authenticates with a **token**, +plus the module address Radius should fetch. The demo's +`Radius.Core/terraformSettings` renders a `.terraformrc` `credentials` block for +that host. + +### High-level steps (any token-authenticated registry) + +1. **Choose a private module source** that authenticates over HTTPS with a bearer + token. You have two broad options: + - **Cloud / hosted (official):** Terraform Cloud / HCP Terraform + (`app.terraform.io`). + - **Self-hosted OSS:** run your own private module registry. Lightweight, + open-source options implement the + [Terraform Module Registry protocol](https://developer.hashicorp.com/terraform/internals/module-registry-protocol) + and authenticate with a token - for example + [Terralist](https://www.terralist.io/) (also acts as a provider registry), + or an Artifactory/Harbor Terraform registry. These are a good fit when you + want a fully private registry without a SaaS account. +2. **Publish (or identify) a module** that provisions the demo resource - a + Kubernetes Redis cache matches the sample. Note its full module address. +3. **Create an API token** with permission to read the module from that registry. +4. **Record these values** - you'll pass them to the demo: + - `TF_REGISTRY_HOST` - the registry hostname, e.g. `app.terraform.io`. + - `TF_RECIPE_LOCATION` - the module source address, e.g. + `app.terraform.io/my-org/redis/kubernetes`. + - `TF_REGISTRY_TOKEN` - the API token. + +### Concrete example - HCP Terraform (Terraform Cloud) + +1. Sign in at and create (or use) an organization. +2. Publish a private module to the organization's **Registry** (for a demo, you + can publish from a Git repo containing a small Kubernetes Redis module, or use + an existing private module). +3. Create a token under **User settings → Tokens** (or a team/organization token). + +```bash +export TF_REGISTRY_HOST="app.terraform.io" +export TF_RECIPE_LOCATION="app.terraform.io/my-org/redis/kubernetes" +export TF_REGISTRY_TOKEN="" +``` + +### Concrete example - self-hosted OSS registry ([Terralist](https://www.terralist.io/)) + +For a fully private setup without a SaaS account, run an open-source registry +such as [Terralist](https://www.terralist.io/), which implements the Terraform +module (and provider) registry protocol and supports token-based auth. + +1. **Deploy Terralist** following its + [documentation](https://www.terralist.io/docs/) (it ships as a single binary / + container and uses a SQL database plus object storage for module/provider + artifacts). Expose it over HTTPS at a hostname you control, e.g. + `registry.mycompany.com`. +2. **Publish a module** (a small Kubernetes Redis module works for the demo) to + your Terralist instance and note its address. +3. **Create an API token** in Terralist for Radius to pull the module. + +```bash +export TF_REGISTRY_HOST="registry.mycompany.com" +export TF_RECIPE_LOCATION="registry.mycompany.com/my-org/redis/kubernetes" +export TF_REGISTRY_TOKEN="" +``` + +> Any registry that implements the Terraform module registry protocol and +> authenticates with a token works the same way - Radius only needs the host, the +> module address, and the token. + +> `TF_RECIPE_LOCATION` is whatever module source your private recipe lives at - a +> private registry module address or an HTTP module archive URL. The demo's +> [Scenario 2](./README.md#scenario-2--private-terraform-module-registry--repository) +> stores the token in a `Radius.Security/secrets` and references it from a +> `Radius.Core/terraformSettings`. + +> **Git-based private modules (PAT auth):** authenticating to a private **Git** +> module source with a personal access token is a separate path that the new +> `Radius.Core/terraformSettings` resource does not cover yet. For Git PAT auth +> today, use the legacy `Applications.Core/environments` `recipeConfig` path - +> see [Known limitations](./README.md#known-limitations-as-of-this-demo). + +--- + +## Verify and continue + +Once your registries are ready and the variables are set, verify the base setup +and continue with the [demo README](./README.md): + +```bash +rad version +kubectl get nodes +``` + +The README's [Prerequisites](./README.md#prerequisites) section creates the +Radius group and namespaces; its scenarios then publish the recipe and deploy +using the values you recorded above. The +[automated E2E runner](./README.md#automated-e2e-runner) reads the **same** +environment variables. + +--- + +## Cleanup + +Tear down the throwaway registries and credentials when you're done: + +- **ACR:** `az group delete --name radius-demo-rg --yes --no-wait` + (or just `az acr token delete --registry myregistry --name radius-demo-pull`). +- **GHCR:** delete the package from the repository/org **Packages** UI and revoke + the PAT at . +- **HCP Terraform:** delete the module from the registry and revoke the token in + **User settings → Tokens**. +- **Self-hosted (e.g. Terralist):** revoke the API token and remove the module + (and the instance itself if it was stood up only for the demo). + +See the demo README's [Cleanup](./README.md#cleanup) section to remove the Radius +applications, group, and namespaces. diff --git a/docs/demos/private-registries-recipes/README.md b/docs/demos/private-registries-recipes/README.md new file mode 100644 index 0000000000..8b2b119586 --- /dev/null +++ b/docs/demos/private-registries-recipes/README.md @@ -0,0 +1,485 @@ +# E2E Demo: Private Registries & Repositories with Terraform and Bicep settings resource types + +This demo validates the work delivered in +[radius-project/radius#11798 - *Private registries and repositories support for +compute extensibility*](https://github.com/radius-project/radius/issues/11798), +updated for +[radius-project/radius#12303](https://github.com/radius-project/radius/pull/12303), +which renamed these resource types to match the feature spec and added support +for `Radius.Security/secrets` as the credential store. + +With compute extensibility, Radius introduced the `Radius.Core/environments` +resource. Private recipe registry authentication - which used to live inline on +`Applications.Core/environments` under `recipeConfig` - is now expressed as +**standalone, reusable resources**: + +| Resource | Purpose | +| --- | --- | +| `Radius.Core/terraformSettings` | Private Terraform registry credentials, provider installation (mirror/direct), and Terraform env vars. Radius renders a `.terraformrc` from it at recipe-execution time. | +| `Radius.Core/bicepSettings` | Private Bicep (OCI) registry authentication, keyed by registry hostname. | + +Registry credentials are supplied through a secret resource that these settings +reference by ID. As of #12303 the secret can be a **`Radius.Security/secrets`** +(recommended) or an `Applications.Core/secretStores`. This demo uses +`Radius.Security/secrets`. + +A `Radius.Core/environments` resource references these settings by resource ID, so +a platform team can define registry authentication **once** and reuse it across +many environments. + +This walkthrough proves the feature works end-to-end against **real private +registries in the cloud**, across three scenarios: + +1. **Private Bicep recipe registry** (OCI, e.g. Azure Container Registry) - `Radius.Core/bicepSettings` with `BasicAuth`. +2. **Private Terraform module registry/repository** - `Radius.Core/terraformSettings` with a `credentials` token. +3. **Combined** - a single environment that references *both* settings resources. + +> Design references: +> [Terraform & Bicep settings feature spec - User Story 8 (Private Bicep Recipes)](https://github.com/radius-project/design-notes/blob/main/features/2025-08-14-terraform-bicep-settings.md#user-story-8--private-bicep-recipes), +> [Available Terraform settings](https://github.com/radius-project/design-notes/blob/main/features/2025-08-14-terraform-bicep-settings.md#available-terraform-settings), +> [Reusable Terraform and Bicep settings architecture doc](../../architecture/terraform-bicep-settings.md). +> +> Product docs for the equivalent (legacy) feature, for additional context: +> [Private Terraform registry](https://docs.radapp.io/guides/recipes/terraform/howto-private-registry/), +> [Private Bicep registry](https://docs.radapp.io/guides/recipes/howto-private-bicep-registry/). + +--- + +## Cross-platform note (Linux / macOS / Windows) + +Every `rad`, `kubectl`, and `git` command in this guide is **identical on all +platforms**. Only the way you set shell variables differs. Where a step needs a +variable, both forms are shown. The PowerShell form uses +[`pwsh`](https://learn.microsoft.com/powershell/scripting/install/installing-powershell) +(PowerShell 7+), which runs the same on Windows, Linux, and macOS: + +- **Linux / macOS (bash / zsh):** + ```bash + export REGISTRY="myregistry.azurecr.io" + ``` +- **PowerShell (pwsh, cross-platform):** + ```powershell + $env:REGISTRY = "myregistry.azurecr.io" + ``` + +In commands below, `$REGISTRY` (bash) and `$env:REGISTRY` (PowerShell) refer to +the same value. Substitute your own values throughout. + +> The generated `.terraformrc` is written **inside the Radius control plane** and +> selected via `TF_CLI_CONFIG_FILE`, so Terraform behavior is identical +> regardless of the OS you run `rad` from. You do **not** need a local +> `.terraformrc` / `terraform.rc`. + +--- + +## Prerequisites + +1. A Kubernetes cluster and `kubectl` configured to talk to it. +2. The [`rad` CLI](https://docs.radapp.io/installation/) installed. +3. Radius installed on the cluster with the `Radius.Core` resource types + available (`rad install kubernetes`). +4. At least one **private** registry you control: + - For Scenario 1: a private **OCI** registry that can host Bicep artifacts + (e.g. Azure Container Registry, GitHub Container Registry, Amazon ECR). + - For Scenario 2: a private **Terraform** module registry or repository that + authenticates with a token (e.g. Terraform Cloud `app.terraform.io`, or a + self-hosted private module registry). + - **Don't have one yet?** See [PREREQUISITES.md](./PREREQUISITES.md) for + high-level steps to set up a private Bicep (OCI) and Terraform registry for + demoing. +5. The Bicep tooling for `rad bicep publish` (bundled with `rad`). + +Verify your setup: + +```bash +rad version +kubectl get nodes +rad group create demo-private-registries +rad group switch demo-private-registries +``` + +Create the namespaces the environments deploy into (the `Radius.Core/environments` +resource requires the target namespace to already exist). Each scenario uses a +dedicated **secrets** namespace for the environment that provisions its +`Radius.Security/secrets` resources - Radius rejects two environments that share a +namespace, so the secrets environment must be separate from the application one: + +```bash +kubectl create namespace private-bicep-demo +kubectl create namespace private-bicep-demo-secrets +kubectl create namespace private-tf-demo +kubectl create namespace private-tf-demo-secrets +kubectl create namespace private-combined-demo +kubectl create namespace private-combined-demo-secrets +``` + +All Bicep templates referenced below live in [`./bicep`](./bicep) and the sample +recipe in [`./recipes`](./recipes). Run the commands from this demo directory. + +> **Prefer to automate it?** The [`./scripts`](./scripts) folder contains an +> end-to-end runner that performs every step below (group + namespace setup, +> recipe publish, deploy, and verify) for a chosen scenario. See +> [Automated E2E runner](#automated-e2e-runner). The manual steps that follow +> document exactly what those scripts do. + +--- + +## Scenario 1 - Private Bicep recipe registry (OCI) + +**Goal:** publish a Bicep recipe to a *private* OCI registry and have Radius pull +it using credentials supplied through a `Radius.Core/bicepSettings` resource. + +### 1.1 Publish the sample recipe to your private registry + +Set your registry details. + +- **Linux / macOS:** + ```bash + export BICEP_REGISTRY="myregistry.azurecr.io" + export BICEP_RECIPE="${BICEP_REGISTRY}/recipes/redis:latest" + ``` +- **PowerShell (pwsh, cross-platform):** + ```powershell + $env:BICEP_REGISTRY = "myregistry.azurecr.io" + $env:BICEP_RECIPE = "$($env:BICEP_REGISTRY)/recipes/redis:latest" + ``` + +Authenticate your local tooling to the registry once so you can push the +artifact (example for Azure Container Registry - use the equivalent for your +provider): + +```bash +az acr login --name myregistry +``` + +Publish the recipe: + +- **Linux / macOS:** + ```bash + rad bicep publish --file ./recipes/redis-recipe.bicep --target "br:${BICEP_RECIPE}" + ``` +- **PowerShell (pwsh, cross-platform):** + ```powershell + rad bicep publish --file ./recipes/redis-recipe.bicep --target "br:$($env:BICEP_RECIPE)" + ``` + +### 1.2 Create registry credentials + +For `BasicAuth`, you need a username and password the cluster can use to pull +from the registry. For ACR this can be an +[ACR token](https://learn.microsoft.com/azure/container-registry/container-registry-repository-scoped-permissions) +or a service principal. Capture them: + +- **Linux / macOS:** + ```bash + export BICEP_REGISTRY_USERNAME="" + export BICEP_REGISTRY_PASSWORD="" + ``` +- **PowerShell (pwsh, cross-platform):** + ```powershell + $env:BICEP_REGISTRY_USERNAME = "" + $env:BICEP_REGISTRY_PASSWORD = "" + ``` + +### 1.3 Deploy the environment + app + +The template [`bicep/bicep-private-registry.bicep`](./bicep/bicep-private-registry.bicep) +creates a `Radius.Security/secrets` (holding the registry username/password), +the `Radius.Core/bicepSettings` that references it, a `Radius.Core/recipePacks` +pointing at the private recipe, the `Radius.Core/environments` that references the +bicepSettings, and an app that runs the recipe. It also creates a small **secrets +environment** whose default recipe pack materializes the backing Kubernetes Secret +that the Bicep driver reads at recipe-execution time. + +> **Self-hosted insecure registries.** This demo targets HTTPS registries. If your +> private OCI registry only serves plain HTTP (for example a locally hosted dev +> registry), set `plainHttp: true` on the recipe entry in the `recipePacks` +> resource so Radius pulls over HTTP instead of HTTPS. + +- **Linux / macOS:** + ```bash + rad deploy ./bicep/bicep-private-registry.bicep \ + --parameters registryHostname="$BICEP_REGISTRY" \ + --parameters recipeLocation="$BICEP_RECIPE" \ + --parameters registryUsername="$BICEP_REGISTRY_USERNAME" \ + --parameters registryPassword="$BICEP_REGISTRY_PASSWORD" + ``` +- **PowerShell (pwsh, cross-platform):** + ```powershell + rad deploy ./bicep/bicep-private-registry.bicep ` + --parameters registryHostname="$env:BICEP_REGISTRY" ` + --parameters recipeLocation="$env:BICEP_RECIPE" ` + --parameters registryUsername="$env:BICEP_REGISTRY_USERNAME" ` + --parameters registryPassword="$env:BICEP_REGISTRY_PASSWORD" + ``` + +### 1.4 Verify + +```bash +rad resource list Applications.Core/extenders +kubectl get pods -n private-bicep-demo +``` + +A successful deployment means Radius authenticated to your **private** OCI +registry, pulled the Bicep recipe, and executed it - the Redis pod should be +`Running` in `private-bicep-demo`. + +--- + +## Scenario 2 - Private Terraform module registry / repository + +**Goal:** authenticate to a *private* Terraform module source via a token carried +by a `Radius.Core/terraformSettings` resource. Radius renders a `.terraformrc` +`credentials` block for that registry at execution time. + +### 2.1 Gather registry details and token + +- **Linux / macOS:** + ```bash + export TF_REGISTRY_HOST="app.terraform.io" + export TF_RECIPE_LOCATION="app.terraform.io/my-org/redis/kubernetes" + export TF_REGISTRY_TOKEN="" + ``` +- **PowerShell (pwsh, cross-platform):** + ```powershell + $env:TF_REGISTRY_HOST = "app.terraform.io" + $env:TF_RECIPE_LOCATION = "app.terraform.io/my-org/redis/kubernetes" + $env:TF_REGISTRY_TOKEN = "" + ``` + +> `TF_RECIPE_LOCATION` is whatever module source your private recipe lives at - +> a private registry module address or an HTTP module archive URL. + +### 2.2 Deploy the environment + app + +The template [`bicep/terraform-private-registry.bicep`](./bicep/terraform-private-registry.bicep) +creates a `Radius.Security/secrets` (with a `token` key), a `Radius.Core/terraformSettings` +that references it under `terraformrc.credentials` (and raises `TF_LOG`), a recipe +pack pointing at the private module, the environment, and an app. A separate +**secrets environment** provisions the secret so its backing Kubernetes Secret +exists before the Terraform driver resolves it. + +- **Linux / macOS:** + ```bash + rad deploy ./bicep/terraform-private-registry.bicep \ + --parameters terraformRegistryHostname="$TF_REGISTRY_HOST" \ + --parameters recipeLocation="$TF_RECIPE_LOCATION" \ + --parameters terraformRegistryToken="$TF_REGISTRY_TOKEN" + ``` +- **PowerShell (pwsh, cross-platform):** + ```powershell + rad deploy ./bicep/terraform-private-registry.bicep ` + --parameters terraformRegistryHostname="$env:TF_REGISTRY_HOST" ` + --parameters recipeLocation="$env:TF_RECIPE_LOCATION" ` + --parameters terraformRegistryToken="$env:TF_REGISTRY_TOKEN" + ``` + +### 2.3 Verify + +```bash +rad resource list Applications.Core/extenders +kubectl get pods -n private-tf-demo +``` + +A successful deployment means Radius rendered a `.terraformrc` with your token, +authenticated to the **private** Terraform registry, downloaded the module, and +ran `terraform apply`. + +> **Tip - confirm the credentials were applied.** Because the terraformSettings sets +> `TF_LOG: INFO`, the recipe execution logs include the Terraform run. Tail the +> recipe engine logs to observe the module download from your private host. The +> recipe runs in the resource provider that handles the environment - for +> `Radius.Core` environments this is typically `dynamic-rp`, while legacy +> `Applications.Core` paths run in `applications-rp`. Pick whichever pod is +> executing your recipe: +> ```bash +> # Radius.Core (this demo) +> kubectl logs -n radius-system deploy/dynamic-rp -f +> # Legacy Applications.Core path +> kubectl logs -n radius-system deploy/applications-rp -f +> ``` + +--- + +## Scenario 3 - Combined (one environment, both private registries) + +**Goal:** demonstrate the reusability goal of #11798 - a single +`Radius.Core/environments` that references **both** a `terraformSettings` and a +`bicepSettings` resource. + +The template [`bicep/combined.bicep`](./bicep/combined.bicep) wires both settings +into one environment. + +- **Linux / macOS:** + ```bash + rad deploy ./bicep/combined.bicep \ + --parameters terraformRegistryHostname="$TF_REGISTRY_HOST" \ + --parameters terraformRecipeLocation="$TF_RECIPE_LOCATION" \ + --parameters terraformRegistryToken="$TF_REGISTRY_TOKEN" \ + --parameters bicepRegistryHostname="$BICEP_REGISTRY" \ + --parameters bicepRegistryUsername="$BICEP_REGISTRY_USERNAME" \ + --parameters bicepRegistryPassword="$BICEP_REGISTRY_PASSWORD" + ``` +- **PowerShell (pwsh, cross-platform):** + ```powershell + rad deploy ./bicep/combined.bicep ` + --parameters terraformRegistryHostname="$env:TF_REGISTRY_HOST" ` + --parameters terraformRecipeLocation="$env:TF_RECIPE_LOCATION" ` + --parameters terraformRegistryToken="$env:TF_REGISTRY_TOKEN" ` + --parameters bicepRegistryHostname="$env:BICEP_REGISTRY" ` + --parameters bicepRegistryUsername="$env:BICEP_REGISTRY_USERNAME" ` + --parameters bicepRegistryPassword="$env:BICEP_REGISTRY_PASSWORD" + ``` + +Inspect the environment to confirm both settings references resolved: + +```bash +rad resource show Radius.Core/environments combined-env +rad resource list Radius.Core/terraformSettings +rad resource list Radius.Core/bicepSettings +``` + +--- + +## How it maps to the new resource types + +```mermaid +graph LR + subgraph "Platform team (rad deploy)" + BC["bicepSettings
(registryAuthentications)"] + TC["terraformSettings
(terraformrc.credentials + env)"] + SS["Radius.Security/secrets"] + end + ENV["Radius.Core/environments
terraformSettings / bicepSettings refs"] + DRV["Terraform / Bicep recipe driver"] + + SS --> BC + SS --> TC + BC -->|resource ID| ENV + TC -->|resource ID| ENV + ENV -->|resolved at execution time| DRV + DRV -->|authenticated pull| REG["Private registries"] +``` + +- **PUT-time validation:** the environment controller validates the referenced + `terraformSettings` / `bicepSettings` IDs exist and returns `400 Bad Request` if not. +- **Execution-time resolution:** the config loader fetches the referenced settings + and bridges them into the shared `RecipeConfig` the existing drivers already + consume, so legacy `Applications.Core/environments` users are unaffected. +- **Secrets are never persisted** in the settings resources; the driver resolves the + referenced secret at execution time. For `Radius.Security/secrets`, whose sensitive + `data` is redacted from the database once provisioned, the loader reads the plaintext + from the backing Kubernetes Secret the secret's recipe materialized. + +### Property mapping (legacy → new) + +| Legacy `Applications.Core/environments.recipeConfig` | New resource | +| --- | --- | +| `terraform.authentication` (registry token) | `Radius.Core/terraformSettings` → `terraformrc.credentials` | +| `terraform.providers` provider config | `Radius.Core/terraformSettings` → `terraformrc.providerInstallation` | +| `env` (Terraform env vars) | `Radius.Core/terraformSettings` → `env` | +| `bicep.authentication..secret` | `Radius.Core/bicepSettings` → `registryAuthentications.` | + +--- + +## Known limitations (as of this demo) + +These are intentional follow-ups tracked in the +[architecture doc](../../architecture/terraform-bicep-settings.md#status-and-known-limitations); +the demo focuses on the paths that are wired end-to-end: + +- **Bicep auth method:** only `BasicAuth` (`basicAuthSecretId`) is threaded into + the driver today. `AzureWI` and `AwsIrsa` are accepted by the API and validated, + but are no-ops at execution time until the corresponding driver work lands. +- **Terraform `credentials`** is for HTTP-based Terraform CLI registry auth + (rendered as native `credentials "host" {}` blocks with a `token`). Git module + source PAT auth from `Radius.Core/environments` is a separate follow-up; for + Git PAT auth today, use the legacy `Applications.Core/environments` + `recipeConfig` path. +- **Delete protection / `referencedBy`** are not yet enforced/populated for these + settings resources. + +--- + +## Automated E2E runner + +The [`./scripts`](./scripts) folder provides a cross-platform runner that +executes the whole walkthrough non-interactively - ideal for E2E validation: + +| Platform | Script | +| --- | --- | +| Linux / macOS (bash) | [`scripts/run-e2e.sh`](./scripts/run-e2e.sh) | +| Any OS (PowerShell 7+ / pwsh) | [`scripts/run-e2e.ps1`](./scripts/run-e2e.ps1) | + +> The PowerShell runner requires [`pwsh`](https://learn.microsoft.com/powershell/scripting/install/installing-powershell) +> (PowerShell 7+) and runs identically on Windows, Linux, and macOS. + +Both scripts read the **same environment variables** documented in the scenarios +above, create the Radius group and namespaces, publish the sample Bicep recipe +(for the Bicep/combined scenarios), deploy the selected scenario, and verify it. + +Set the variables for the scenario(s) you want, then run: + +- **Linux / macOS:** + ```bash + export BICEP_REGISTRY="myregistry.azurecr.io" + export BICEP_RECIPE="${BICEP_REGISTRY}/recipes/redis:latest" + export BICEP_REGISTRY_USERNAME="" + export BICEP_REGISTRY_PASSWORD="" + export TF_REGISTRY_HOST="app.terraform.io" + export TF_RECIPE_LOCATION="app.terraform.io/my-org/redis/kubernetes" + export TF_REGISTRY_TOKEN="" + + ./scripts/run-e2e.sh --scenario bicep # or terraform | combined | all + ./scripts/run-e2e.sh --cleanup # tear everything down + ``` +- **PowerShell (pwsh, cross-platform):** + ```powershell + $env:BICEP_REGISTRY = "myregistry.azurecr.io" + $env:BICEP_RECIPE = "$($env:BICEP_REGISTRY)/recipes/redis:latest" + $env:BICEP_REGISTRY_USERNAME = "" + $env:BICEP_REGISTRY_PASSWORD = "" + $env:TF_REGISTRY_HOST = "app.terraform.io" + $env:TF_RECIPE_LOCATION = "app.terraform.io/my-org/redis/kubernetes" + $env:TF_REGISTRY_TOKEN = "" + + pwsh ./scripts/run-e2e.ps1 -Scenario bicep # or terraform | combined | all + pwsh ./scripts/run-e2e.ps1 -Cleanup # tear everything down + ``` + +Useful flags: `--scenario` / `-Scenario` selects `bicep`, `terraform`, +`combined`, or `all`; `--skip-publish` / `-SkipPublish` reuses an already-pushed +recipe; `--cleanup` / `-Cleanup` deletes all demo resources. Pass `--help` (bash) +or `Get-Help ./scripts/run-e2e.ps1` (pwsh) for full details. + +--- + +## Cleanup + +```bash +rad app delete private-bicep-demo --yes +rad app delete private-tf-demo --yes +rad app delete private-combined-demo --yes + +rad group switch default +rad group delete demo-private-registries --yes + +kubectl delete namespace \ + private-bicep-demo private-bicep-demo-secrets \ + private-tf-demo private-tf-demo-secrets \ + private-combined-demo private-combined-demo-secrets +``` + +--- + +## Files in this demo + +| Path | Description | +| --- | --- | +| [`PREREQUISITES.md`](./PREREQUISITES.md) | High-level steps to set up the private Bicep (OCI) and Terraform registries the demo needs. | +| [`bicep/bicep-private-registry.bicep`](./bicep/bicep-private-registry.bicep) | Scenario 1 - private Bicep (OCI) registry via `bicepSettings` (BasicAuth) + `Radius.Security/secrets`. | +| [`bicep/terraform-private-registry.bicep`](./bicep/terraform-private-registry.bicep) | Scenario 2 - private Terraform registry via `terraformSettings` (credentials token) + `Radius.Security/secrets`. | +| [`bicep/combined.bicep`](./bicep/combined.bicep) | Scenario 3 - one environment referencing both settings. | +| [`recipes/redis-recipe.bicep`](./recipes/redis-recipe.bicep) | Sample Bicep recipe to publish to your private OCI registry for Scenario 1. | +| [`scripts/run-e2e.sh`](./scripts/run-e2e.sh) | Linux / macOS E2E runner that automates the full walkthrough. | +| [`scripts/run-e2e.ps1`](./scripts/run-e2e.ps1) | Cross-platform PowerShell (pwsh 7+) E2E runner that automates the full walkthrough. | diff --git a/docs/demos/private-registries-recipes/bicep/bicep-private-registry.bicep b/docs/demos/private-registries-recipes/bicep/bicep-private-registry.bicep new file mode 100644 index 0000000000..a26bad18db --- /dev/null +++ b/docs/demos/private-registries-recipes/bicep/bicep-private-registry.bicep @@ -0,0 +1,147 @@ +// ----------------------------------------------------------------------------- +// Scenario 1 - Private Bicep recipe registry (OCI, e.g. Azure Container Registry) +// +// Demonstrates pulling a Bicep Recipe from a *private* OCI registry by attaching +// a Radius.Core/bicepSettings resource (BasicAuth) to a Radius.Core/environments. +// +// Registry credentials are stored in a Radius.Security/secrets resource. The +// bicepSettings resource references that secret by ID; at recipe-execution time +// the Bicep driver resolves the referenced secret and authenticates to the +// registry using the method configured on bicepSettings (BasicAuth here). +// +// Replace the parameters below with the details of your own private registry. +// See ../README.md for the full step-by-step walkthrough. +// ----------------------------------------------------------------------------- + +extension radius + +@description('Name of the Radius Application to create.') +param appName string = 'private-bicep-demo' + +@description('Kubernetes namespace the application environment deploys into. Must already exist.') +param kubernetesNamespace string = 'private-bicep-demo' + +@description('Kubernetes namespace the secrets environment deploys into. Must already exist and differ from kubernetesNamespace.') +param secretsNamespace string = 'private-bicep-demo-secrets' + +@description('Hostname of the private OCI registry that stores the Bicep recipe, e.g. "myregistry.azurecr.io".') +param registryHostname string + +@description('Full OCI path to the published Bicep recipe, e.g. "myregistry.azurecr.io/recipes/myredis:latest".') +param recipeLocation string + +@description('Username used for BasicAuth against the private registry (for ACR this can be an ACR token name or a service principal app ID).') +@secure() +param registryUsername string + +@description('Password used for BasicAuth against the private registry (for ACR this can be an ACR token password or a service principal secret).') +@secure() +param registryPassword string + +// Secrets environment. It carries NO recipePacks, so `rad deploy` injects the +// cluster's default recipe pack, which registers the Radius.Security/secrets +// Kubernetes recipe. That recipe materializes the backing Kubernetes Secret the +// Bicep driver later reads. It uses a separate namespace from the application +// environment because Radius rejects two environments that share a namespace. +resource secretsEnv 'Radius.Core/environments@2025-08-01-preview' = { + name: 'private-bicep-secrets-env' + location: 'global' + properties: { + providers: { + kubernetes: { + namespace: secretsNamespace + } + } + } +} + +// Radius.Security/secrets holding the username/password used to authenticate to +// the private OCI registry. Provisioning it runs the secrets recipe, which +// creates a same-named Kubernetes Secret (with 'username' and 'password' keys) +// that the Bicep driver reads at recipe-execution time. +resource registrySecret 'Radius.Security/secrets@2025-08-01-preview' = { + name: 'private-bicep-registry-secret' + location: 'global' + properties: { + environment: secretsEnv.id + kind: 'generic' + data: { + username: { + value: registryUsername + } + password: { + value: registryPassword + } + } + } +} + +// BicepSettings carrying the registry authentication. The map key is the registry +// hostname; the driver uses these credentials when pulling Bicep recipes hosted +// on that registry. The authentication method is selected here (BasicAuth), not +// from the secret's kind. +resource bicepSettings 'Radius.Core/bicepSettings@2025-08-01-preview' = { + name: 'private-bicep-settings' + location: 'global' + properties: { + registryAuthentications: { + '${registryHostname}': { + authenticationMethod: 'BasicAuth' + basicAuthSecretId: registrySecret.id + } + } + } +} + +// RecipePack pointing at the *private* Bicep recipe. Because the environment +// references the bicepSettings above, Radius authenticates to the registry when +// the recipe is executed. +resource recipePack 'Radius.Core/recipePacks@2025-08-01-preview' = { + name: 'private-bicep-recipe-pack' + location: 'global' + properties: { + recipes: { + 'Applications.Core/extenders': { + kind: 'bicep' + source: recipeLocation + } + } + } +} + +// Environment that references both the recipe pack and the bicep registry settings. +resource env 'Radius.Core/environments@2025-08-01-preview' = { + name: 'private-bicep-env' + location: 'global' + properties: { + recipePacks: [ + recipePack.id + ] + providers: { + kubernetes: { + namespace: kubernetesNamespace + } + } + bicepSettings: bicepSettings.id + } +} + +resource app 'Radius.Core/applications@2025-08-01-preview' = { + name: appName + location: 'global' + properties: { + environment: env.id + } +} + +// Application resource that triggers the private Bicep recipe. +resource demo 'Applications.Core/extenders@2023-10-01-preview' = { + name: '${appName}-resource' + properties: { + application: app.id + environment: env.id + recipe: { + name: 'default' + } + } +} diff --git a/docs/demos/private-registries-recipes/bicep/combined.bicep b/docs/demos/private-registries-recipes/bicep/combined.bicep new file mode 100644 index 0000000000..605136bd93 --- /dev/null +++ b/docs/demos/private-registries-recipes/bicep/combined.bicep @@ -0,0 +1,193 @@ +// ----------------------------------------------------------------------------- +// Scenario 3 - Combined: one environment, private Terraform AND private Bicep +// +// A single Radius.Core/environments references BOTH a Radius.Core/terraformSettings +// (private Terraform registry credentials + env vars) and a Radius.Core/bicepSettings +// (private OCI/Bicep registry BasicAuth). This mirrors a platform team that +// defines registry authentication once and reuses it across recipe kinds. +// +// Both registry credentials are stored as Radius.Security/secrets resources, +// provisioned into a dedicated secrets environment so their backing Kubernetes +// Secrets exist before the recipe drivers resolve them. +// +// Replace the parameters below with the details of your own private registries. +// See ../README.md for the full step-by-step walkthrough. +// ----------------------------------------------------------------------------- + +extension radius + +@description('Name of the Radius Application to create.') +param appName string = 'private-combined-demo' + +@description('Kubernetes namespace the application environment deploys into. Must already exist.') +param kubernetesNamespace string = 'private-combined-demo' + +@description('Kubernetes namespace the secrets environment deploys into. Must already exist and differ from kubernetesNamespace.') +param secretsNamespace string = 'private-combined-demo-secrets' + +// --- Private Terraform registry inputs --- +@description('Hostname of the private Terraform registry that requires a token.') +param terraformRegistryHostname string + +@description('URL of the private Terraform module source (recipeLocation).') +param terraformRecipeLocation string + +@description('Token used to authenticate to the private Terraform registry.') +@secure() +param terraformRegistryToken string + +@description('Name of the Redis cache the example Terraform recipe provisions.') +param redisCacheName string = 'tf-combined-redis' + +// --- Private Bicep (OCI) registry inputs --- +@description('Hostname of the private OCI registry that stores Bicep recipes.') +param bicepRegistryHostname string + +@description('Username used for BasicAuth against the private OCI registry.') +@secure() +param bicepRegistryUsername string + +@description('Password used for BasicAuth against the private OCI registry.') +@secure() +param bicepRegistryPassword string + +// Secrets environment. It carries NO recipePacks, so `rad deploy` injects the +// cluster's default recipe pack, which registers the Radius.Security/secrets +// Kubernetes recipe. Both credential secrets below are provisioned here so their +// backing Kubernetes Secrets exist before the recipe drivers resolve them. It +// uses a separate namespace from the application environment because Radius +// rejects two environments that share a namespace. +resource secretsEnv 'Radius.Core/environments@2025-08-01-preview' = { + name: 'combined-secrets-env' + location: 'global' + properties: { + providers: { + kubernetes: { + namespace: secretsNamespace + } + } + } +} + +// Radius.Security/secrets for the Terraform registry token (key: token). +resource terraformTokenSecret 'Radius.Security/secrets@2025-08-01-preview' = { + name: 'combined-tf-token-secret' + location: 'global' + properties: { + environment: secretsEnv.id + kind: 'generic' + data: { + token: { + value: terraformRegistryToken + } + } + } +} + +// Radius.Security/secrets for the Bicep OCI registry BasicAuth (keys: username, password). +resource bicepRegistrySecret 'Radius.Security/secrets@2025-08-01-preview' = { + name: 'combined-bicep-registry-secret' + location: 'global' + properties: { + environment: secretsEnv.id + kind: 'generic' + data: { + username: { + value: bicepRegistryUsername + } + password: { + value: bicepRegistryPassword + } + } + } +} + +resource tfSettings 'Radius.Core/terraformSettings@2025-08-01-preview' = { + name: 'combined-tf-settings' + location: 'global' + properties: { + terraformrc: { + providerInstallation: { + direct: { + include: [ + '*/*' + ] + } + } + credentials: { + '${terraformRegistryHostname}': { + secret: terraformTokenSecret.id + } + } + } + env: { + TF_LOG: 'INFO' + } + } +} + +resource bicepSettings 'Radius.Core/bicepSettings@2025-08-01-preview' = { + name: 'combined-bicep-settings' + location: 'global' + properties: { + registryAuthentications: { + '${bicepRegistryHostname}': { + authenticationMethod: 'BasicAuth' + basicAuthSecretId: bicepRegistrySecret.id + } + } + } +} + +resource recipePack 'Radius.Core/recipePacks@2025-08-01-preview' = { + name: 'combined-recipe-pack' + location: 'global' + properties: { + recipes: { + 'Applications.Core/extenders': { + kind: 'terraform' + source: terraformRecipeLocation + } + } + } +} + +// One environment, two settings resources, both reused from a single definition. +resource env 'Radius.Core/environments@2025-08-01-preview' = { + name: 'combined-env' + location: 'global' + properties: { + recipePacks: [ + recipePack.id + ] + providers: { + kubernetes: { + namespace: kubernetesNamespace + } + } + terraformSettings: tfSettings.id + bicepSettings: bicepSettings.id + } +} + +resource app 'Radius.Core/applications@2025-08-01-preview' = { + name: appName + location: 'global' + properties: { + environment: env.id + } +} + +resource demo 'Applications.Core/extenders@2023-10-01-preview' = { + name: '${appName}-resource' + properties: { + application: app.id + environment: env.id + recipe: { + name: 'default' + parameters: { + redis_cache_name: redisCacheName + } + } + } +} diff --git a/docs/demos/private-registries-recipes/bicep/terraform-private-registry.bicep b/docs/demos/private-registries-recipes/bicep/terraform-private-registry.bicep new file mode 100644 index 0000000000..394d050226 --- /dev/null +++ b/docs/demos/private-registries-recipes/bicep/terraform-private-registry.bicep @@ -0,0 +1,161 @@ +// ----------------------------------------------------------------------------- +// Scenario 2 - Private Terraform module registry / repository +// +// Demonstrates authenticating to a *private* Terraform module source by attaching +// a Radius.Core/terraformSettings resource to a Radius.Core/environments. The +// terraformSettings resource renders a .terraformrc (credentials + provider +// installation) at recipe execution time and points Terraform at it via +// TF_CLI_CONFIG_FILE. +// +// The registry token is stored in a Radius.Security/secrets resource. The +// terraformSettings credentials block references that secret by ID; the +// Terraform driver resolves the secret's 'token' key and renders it into the +// generated .terraformrc. +// +// Replace the parameters below with the details of your own private registry. +// See ../README.md for the full step-by-step walkthrough. +// ----------------------------------------------------------------------------- + +extension radius + +@description('Name of the Radius Application to create.') +param appName string = 'private-tf-demo' + +@description('Kubernetes namespace the application environment deploys into. Must already exist.') +param kubernetesNamespace string = 'private-tf-demo' + +@description('Kubernetes namespace the secrets environment deploys into. Must already exist and differ from kubernetesNamespace.') +param secretsNamespace string = 'private-tf-demo-secrets' + +@description('Hostname of the private Terraform registry that requires a token, e.g. "app.terraform.io" or "registry.mycompany.com".') +param terraformRegistryHostname string + +@description('URL of the private Terraform module source (recipeLocation). For example a module published to a private registry or an HTTP module archive.') +param recipeLocation string + +@description('Token used to authenticate to the private Terraform registry.') +@secure() +param terraformRegistryToken string + +@description('Name of the Redis cache the example Terraform recipe provisions.') +param redisCacheName string = 'tf-private-redis' + +// Secrets environment. It carries NO recipePacks, so `rad deploy` injects the +// cluster's default recipe pack, which registers the Radius.Security/secrets +// Kubernetes recipe. That recipe materializes the backing Kubernetes Secret the +// Terraform driver later reads. It uses a separate namespace from the +// application environment because Radius rejects two environments that share a +// namespace. +resource secretsEnv 'Radius.Core/environments@2025-08-01-preview' = { + name: 'private-tf-secrets-env' + location: 'global' + properties: { + providers: { + kubernetes: { + namespace: secretsNamespace + } + } + } +} + +// Radius.Security/secrets holding the Terraform registry token. The +// terraformSettings credentials block references this by ID; the backing +// Kubernetes Secret must expose a 'token' key, which the driver reads. +resource registryTokenSecret 'Radius.Security/secrets@2025-08-01-preview' = { + name: 'private-tf-token-secret' + location: 'global' + properties: { + environment: secretsEnv.id + kind: 'generic' + data: { + token: { + value: terraformRegistryToken + } + } + } +} + +// TerraformSettings that: +// * authenticates to the private Terraform registry (credentials block), and +// * keeps provider installation on the default "direct" path (fetch providers +// from the public registry). Swap "direct" for "networkMirror" if your +// providers also live behind a private mirror. +// * injects env vars into the Terraform process (here, raise the log level). +resource tfSettings 'Radius.Core/terraformSettings@2025-08-01-preview' = { + name: 'private-tf-settings' + location: 'global' + properties: { + terraformrc: { + providerInstallation: { + direct: { + include: [ + '*/*' + ] + } + } + credentials: { + '${terraformRegistryHostname}': { + secret: registryTokenSecret.id + } + } + } + env: { + TF_LOG: 'INFO' + TF_REGISTRY_CLIENT_TIMEOUT: '15' + } + } +} + +// RecipePack pointing at the Terraform module hosted in the private registry. +resource recipePack 'Radius.Core/recipePacks@2025-08-01-preview' = { + name: 'private-tf-recipe-pack' + location: 'global' + properties: { + recipes: { + 'Applications.Core/extenders': { + kind: 'terraform' + source: recipeLocation + } + } + } +} + +// Environment that references both the recipe pack and the Terraform settings. +resource env 'Radius.Core/environments@2025-08-01-preview' = { + name: 'private-tf-env' + location: 'global' + properties: { + recipePacks: [ + recipePack.id + ] + providers: { + kubernetes: { + namespace: kubernetesNamespace + } + } + terraformSettings: tfSettings.id + } +} + +resource app 'Radius.Core/applications@2025-08-01-preview' = { + name: appName + location: 'global' + properties: { + environment: env.id + } +} + +// Application resource that triggers the private Terraform recipe. +resource demo 'Applications.Core/extenders@2023-10-01-preview' = { + name: '${appName}-resource' + properties: { + application: app.id + environment: env.id + recipe: { + name: 'default' + parameters: { + redis_cache_name: redisCacheName + } + } + } +} diff --git a/docs/demos/private-registries-recipes/recipes/redis-recipe.bicep b/docs/demos/private-registries-recipes/recipes/redis-recipe.bicep new file mode 100644 index 0000000000..2c7eed512d --- /dev/null +++ b/docs/demos/private-registries-recipes/recipes/redis-recipe.bicep @@ -0,0 +1,88 @@ +// ----------------------------------------------------------------------------- +// Sample Bicep recipe for the private-registry demo (Scenario 1). +// +// Publish this to your *private* OCI registry with `rad bicep publish`, then +// point the RecipePack's recipe `source` at the published artifact. See +// ../README.md for the publishing commands. +// +// The recipe provisions a small Redis Deployment + Service on Kubernetes and +// returns its connection values, so the consuming Applications.Core/extenders +// resource gets a working result back. +// ----------------------------------------------------------------------------- + +extension kubernetes with { + kubeConfig: '' + namespace: context.runtime.kubernetes.namespace +} as kubernetes + +@description('Information about the resource calling the Recipe. Provided by Radius.') +param context object + +@description('Container image to run for the Redis cache.') +param image string = 'redis:7' + +resource redis 'apps/Deployment@v1' = { + metadata: { + name: 'redis-${uniqueString(context.resource.id)}' + } + spec: { + selector: { + matchLabels: { + app: 'redis' + resource: context.resource.name + } + } + template: { + metadata: { + labels: { + app: 'redis' + resource: context.resource.name + } + } + spec: { + containers: [ + { + name: 'redis' + image: image + ports: [ + { + containerPort: 6379 + } + ] + } + ] + } + } + } +} + +resource svc 'core/Service@v1' = { + metadata: { + name: 'redis-${uniqueString(context.resource.id)}' + } + spec: { + type: 'ClusterIP' + selector: { + app: 'redis' + resource: context.resource.name + } + ports: [ + { + port: 6379 + } + ] + } +} + +output result object = { + // The deployment engine omits Kubernetes resources from its output, so we + // surface them explicitly here. + resources: [ + '/planes/kubernetes/local/namespaces/${svc.metadata.namespace}/providers/core/Service/${svc.metadata.name}' + '/planes/kubernetes/local/namespaces/${redis.metadata.namespace}/providers/apps/Deployment/${redis.metadata.name}' + ] + values: { + host: '${svc.metadata.name}.${svc.metadata.namespace}.svc.cluster.local' + port: 6379 + } +} diff --git a/docs/demos/private-registries-recipes/scripts/run-e2e.ps1 b/docs/demos/private-registries-recipes/scripts/run-e2e.ps1 new file mode 100644 index 0000000000..371dd9b31e --- /dev/null +++ b/docs/demos/private-registries-recipes/scripts/run-e2e.ps1 @@ -0,0 +1,227 @@ +#Requires -Version 7.0 + +<# +.SYNOPSIS + E2E runner for the "Private Registries & Repositories" demo (cross-platform + PowerShell / pwsh). + +.DESCRIPTION + Automates the manual walkthrough in ..\README.md: creates the Radius group + and namespaces, optionally publishes the sample Bicep recipe, deploys the + environment + app for the selected scenario, and verifies the result. + + Configuration is supplied through environment variables (see .PARAMETER notes + and ..\README.md) so secrets stay off the command line. + +.PARAMETER Scenario + Scenario to run: bicep, terraform, combined, or all. Default: all. + +.PARAMETER SkipPublish + Skip 'rad bicep publish' (recipe already pushed). + +.PARAMETER Cleanup + Delete all demo resources and exit. + +.EXAMPLE + pwsh ./run-e2e.ps1 -Scenario bicep + +.EXAMPLE + pwsh ./run-e2e.ps1 -Scenario terraform -SkipPublish + +.EXAMPLE + pwsh ./run-e2e.ps1 -Cleanup + +.NOTES + Environment variables: + Scenario 1 (bicep / combined): + BICEP_REGISTRY, BICEP_RECIPE, + BICEP_REGISTRY_USERNAME, BICEP_REGISTRY_PASSWORD + Scenario 2 (terraform / combined): + TF_REGISTRY_HOST, TF_RECIPE_LOCATION, TF_REGISTRY_TOKEN +#> + +[CmdletBinding()] +param( + [ValidateSet('bicep', 'terraform', 'combined', 'all')] + [string]$Scenario = 'all', + + [switch]$SkipPublish, + + [switch]$Cleanup +) + +$ErrorActionPreference = 'Stop' + +$DemoDir = Split-Path -Parent $PSScriptRoot +$BicepDir = Join-Path $DemoDir 'bicep' +$RecipesDir = Join-Path $DemoDir 'recipes' + +$RadGroup = 'demo-private-registries' +$BicepNamespace = 'private-bicep-demo' +$TfNamespace = 'private-tf-demo' +$CombinedNamespace = 'private-combined-demo' +# Each scenario provisions its Radius.Security/secrets into a dedicated secrets +# environment, which needs its own namespace (Radius rejects two environments +# that share a namespace). +$BicepSecretsNamespace = 'private-bicep-demo-secrets' +$TfSecretsNamespace = 'private-tf-demo-secrets' +$CombinedSecretsNamespace = 'private-combined-demo-secrets' + +function Test-Tools { + foreach ($tool in @('rad', 'kubectl')) { + if (-not (Get-Command $tool -ErrorAction SilentlyContinue)) { + throw "Required tool '$tool' not found on PATH" + } + } +} + +function Assert-Vars { + param([string[]]$Names) + $missing = @() + foreach ($name in $Names) { + $value = [Environment]::GetEnvironmentVariable($name) + if ([string]::IsNullOrEmpty($value)) { + $missing += $name + } + } + if ($missing.Count -gt 0) { + throw "Missing required variables: $($missing -join ', ')" + } +} + +function Get-Var { + param([string]$Name) + return [Environment]::GetEnvironmentVariable($Name) +} + +function Confirm-Namespace { + param([string]$Namespace) + kubectl get namespace $Namespace 2>$null | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Host "Creating namespace $Namespace" + kubectl create namespace $Namespace + } +} + +function Initialize-Group { + # Group may already exist; allow that but let 'rad group switch' validate. + rad group create $RadGroup | Out-Null + rad group switch $RadGroup +} + +function Invoke-Bicep { + Assert-Vars @('BICEP_REGISTRY', 'BICEP_RECIPE', + 'BICEP_REGISTRY_USERNAME', 'BICEP_REGISTRY_PASSWORD') + Confirm-Namespace $BicepNamespace + Confirm-Namespace $BicepSecretsNamespace + + if (-not $SkipPublish) { + Write-Host "Publishing Bicep recipe to $(Get-Var 'BICEP_RECIPE')" + rad bicep publish ` + --file (Join-Path $RecipesDir 'redis-recipe.bicep') ` + --target "br:$(Get-Var 'BICEP_RECIPE')" + } + + Write-Host 'Deploying Scenario 1 (private Bicep registry)' + rad deploy (Join-Path $BicepDir 'bicep-private-registry.bicep') ` + --parameters registryHostname="$(Get-Var 'BICEP_REGISTRY')" ` + --parameters recipeLocation="$(Get-Var 'BICEP_RECIPE')" ` + --parameters registryUsername="$(Get-Var 'BICEP_REGISTRY_USERNAME')" ` + --parameters registryPassword="$(Get-Var 'BICEP_REGISTRY_PASSWORD')" + + Write-Host 'Verifying Scenario 1' + rad resource list Applications.Core/extenders + kubectl get pods -n $BicepNamespace +} + +function Invoke-Terraform { + Assert-Vars @('TF_REGISTRY_HOST', 'TF_RECIPE_LOCATION', 'TF_REGISTRY_TOKEN') + Confirm-Namespace $TfNamespace + Confirm-Namespace $TfSecretsNamespace + + Write-Host 'Deploying Scenario 2 (private Terraform registry)' + rad deploy (Join-Path $BicepDir 'terraform-private-registry.bicep') ` + --parameters terraformRegistryHostname="$(Get-Var 'TF_REGISTRY_HOST')" ` + --parameters recipeLocation="$(Get-Var 'TF_RECIPE_LOCATION')" ` + --parameters terraformRegistryToken="$(Get-Var 'TF_REGISTRY_TOKEN')" + + Write-Host 'Verifying Scenario 2' + rad resource list Applications.Core/extenders + kubectl get pods -n $TfNamespace +} + +function Invoke-Combined { + Assert-Vars @('BICEP_REGISTRY', 'BICEP_RECIPE', + 'BICEP_REGISTRY_USERNAME', 'BICEP_REGISTRY_PASSWORD', + 'TF_REGISTRY_HOST', 'TF_RECIPE_LOCATION', 'TF_REGISTRY_TOKEN') + Confirm-Namespace $CombinedNamespace + Confirm-Namespace $CombinedSecretsNamespace + + if (-not $SkipPublish) { + Write-Host "Publishing Bicep recipe to $(Get-Var 'BICEP_RECIPE')" + rad bicep publish ` + --file (Join-Path $RecipesDir 'redis-recipe.bicep') ` + --target "br:$(Get-Var 'BICEP_RECIPE')" + } + + Write-Host 'Deploying Scenario 3 (combined)' + rad deploy (Join-Path $BicepDir 'combined.bicep') ` + --parameters terraformRegistryHostname="$(Get-Var 'TF_REGISTRY_HOST')" ` + --parameters terraformRecipeLocation="$(Get-Var 'TF_RECIPE_LOCATION')" ` + --parameters terraformRegistryToken="$(Get-Var 'TF_REGISTRY_TOKEN')" ` + --parameters bicepRegistryHostname="$(Get-Var 'BICEP_REGISTRY')" ` + --parameters bicepRegistryUsername="$(Get-Var 'BICEP_REGISTRY_USERNAME')" ` + --parameters bicepRegistryPassword="$(Get-Var 'BICEP_REGISTRY_PASSWORD')" + + Write-Host 'Verifying Scenario 3' + rad resource show Radius.Core/environments combined-env + rad resource list Radius.Core/terraformSettings + rad resource list Radius.Core/bicepSettings +} + +function Remove-Demo { + Write-Host 'Cleaning up demo resources' + rad group switch $RadGroup 2>$null + rad app delete $BicepNamespace --yes 2>$null + rad app delete $TfNamespace --yes 2>$null + rad app delete $CombinedNamespace --yes 2>$null + rad group switch default 2>$null + rad group delete $RadGroup --yes 2>$null + kubectl delete namespace ` + $BicepNamespace $TfNamespace $CombinedNamespace ` + $BicepSecretsNamespace $TfSecretsNamespace $CombinedSecretsNamespace ` + --ignore-not-found + Write-Host 'Cleanup complete' +} + +function Main { + Test-Tools + + if ($Cleanup) { + Remove-Demo + return + } + + Write-Host '============================================================' + Write-Host "Private registries E2E demo - scenario: $Scenario" + Write-Host '============================================================' + + Initialize-Group + + switch ($Scenario) { + 'bicep' { Invoke-Bicep } + 'terraform' { Invoke-Terraform } + 'combined' { Invoke-Combined } + 'all' { + Invoke-Bicep + Invoke-Terraform + Invoke-Combined + } + } + + Write-Host '============================================================' + Write-Host "Done. Run 'pwsh ./run-e2e.ps1 -Cleanup' to remove demo resources." + Write-Host '============================================================' +} + +Main diff --git a/docs/demos/private-registries-recipes/scripts/run-e2e.sh b/docs/demos/private-registries-recipes/scripts/run-e2e.sh new file mode 100755 index 0000000000..416808199b --- /dev/null +++ b/docs/demos/private-registries-recipes/scripts/run-e2e.sh @@ -0,0 +1,260 @@ +#!/bin/bash + +# ============================================================================ +# E2E runner for the "Private Registries & Repositories" demo (Linux / macOS). +# +# Automates the manual walkthrough in ../README.md: it creates the Radius group +# and namespaces, optionally publishes the sample Bicep recipe, deploys the +# environment + app for the selected scenario, and verifies the result. +# +# Configuration is supplied through environment variables (see usage). This +# keeps secrets out of the command line and mirrors the variables used in the +# README. +# ============================================================================ + +set -euo pipefail + +SCRIPT_NAME="$(basename "$0")" +readonly SCRIPT_NAME +# Demo root is the parent of the directory holding this script. +DEMO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +readonly DEMO_DIR +readonly BICEP_DIR="${DEMO_DIR}/bicep" +readonly RECIPES_DIR="${DEMO_DIR}/recipes" + +readonly RAD_GROUP="demo-private-registries" +readonly BICEP_NAMESPACE="private-bicep-demo" +readonly TF_NAMESPACE="private-tf-demo" +readonly COMBINED_NAMESPACE="private-combined-demo" +# Each scenario provisions its Radius.Security/secrets into a dedicated secrets +# environment, which needs its own namespace (Radius rejects two environments +# that share a namespace). +readonly BICEP_SECRETS_NAMESPACE="private-bicep-demo-secrets" +readonly TF_SECRETS_NAMESPACE="private-tf-demo-secrets" +readonly COMBINED_SECRETS_NAMESPACE="private-combined-demo-secrets" + +# Default values +SCENARIO="all" +DO_CLEANUP="false" +PUBLISH_RECIPE="true" + +usage() { + cat < Scenario to run: bicep | terraform | combined | all + (default: all) + --skip-publish Skip 'rad bicep publish' (recipe already pushed) + -c, --cleanup Delete all demo resources and exit + -h, --help Show this help + +Environment variables: + Scenario 1 (bicep / combined): + BICEP_REGISTRY Private OCI registry host, e.g. myreg.azurecr.io + BICEP_RECIPE Full OCI path to the recipe, e.g. + myreg.azurecr.io/recipes/redis:latest + BICEP_REGISTRY_USERNAME BasicAuth username + BICEP_REGISTRY_PASSWORD BasicAuth password + + Scenario 2 (terraform / combined): + TF_REGISTRY_HOST Private Terraform registry host + TF_RECIPE_LOCATION Terraform module source (recipeLocation) + TF_REGISTRY_TOKEN Terraform registry token + +Examples: + ${SCRIPT_NAME} --scenario bicep + ${SCRIPT_NAME} --scenario terraform --skip-publish + ${SCRIPT_NAME} --cleanup +EOF + exit 0 +} + +require_tools() { + local tool + for tool in rad kubectl; do + if ! command -v "${tool}" >/dev/null 2>&1; then + echo "Error: required tool '${tool}' not found on PATH" >&2 + exit 1 + fi + done +} + +require_vars() { + local name + local missing=() + for name in "$@"; do + if [[ -z "${!name:-}" ]]; then + missing+=("${name}") + fi + done + if [[ ${#missing[@]} -gt 0 ]]; then + echo "Error: missing required variables: ${missing[*]}" >&2 + exit 1 + fi +} + +ensure_namespace() { + local ns="$1" + if ! kubectl get namespace "${ns}" >/dev/null 2>&1; then + echo "Creating namespace ${ns}" + kubectl create namespace "${ns}" + fi +} + +setup_group() { + # Group may already exist; suppress only the duplicate notice on stdout + # while letting real errors surface. 'rad group switch' below validates it. + rad group create "${RAD_GROUP}" >/dev/null || true + rad group switch "${RAD_GROUP}" +} + +run_bicep() { + require_vars BICEP_REGISTRY BICEP_RECIPE \ + BICEP_REGISTRY_USERNAME BICEP_REGISTRY_PASSWORD + ensure_namespace "${BICEP_NAMESPACE}" + ensure_namespace "${BICEP_SECRETS_NAMESPACE}" + + if [[ "${PUBLISH_RECIPE}" == "true" ]]; then + echo "Publishing Bicep recipe to ${BICEP_RECIPE}" + rad bicep publish \ + --file "${RECIPES_DIR}/redis-recipe.bicep" \ + --target "br:${BICEP_RECIPE}" + fi + + echo "Deploying Scenario 1 (private Bicep registry)" + rad deploy "${BICEP_DIR}/bicep-private-registry.bicep" \ + --parameters registryHostname="${BICEP_REGISTRY}" \ + --parameters recipeLocation="${BICEP_RECIPE}" \ + --parameters registryUsername="${BICEP_REGISTRY_USERNAME}" \ + --parameters registryPassword="${BICEP_REGISTRY_PASSWORD}" + + echo "Verifying Scenario 1" + rad resource list Applications.Core/extenders + kubectl get pods -n "${BICEP_NAMESPACE}" +} + +run_terraform() { + require_vars TF_REGISTRY_HOST TF_RECIPE_LOCATION TF_REGISTRY_TOKEN + ensure_namespace "${TF_NAMESPACE}" + ensure_namespace "${TF_SECRETS_NAMESPACE}" + + echo "Deploying Scenario 2 (private Terraform registry)" + rad deploy "${BICEP_DIR}/terraform-private-registry.bicep" \ + --parameters terraformRegistryHostname="${TF_REGISTRY_HOST}" \ + --parameters recipeLocation="${TF_RECIPE_LOCATION}" \ + --parameters terraformRegistryToken="${TF_REGISTRY_TOKEN}" + + echo "Verifying Scenario 2" + rad resource list Applications.Core/extenders + kubectl get pods -n "${TF_NAMESPACE}" +} + +run_combined() { + require_vars BICEP_REGISTRY BICEP_RECIPE \ + BICEP_REGISTRY_USERNAME BICEP_REGISTRY_PASSWORD \ + TF_REGISTRY_HOST TF_RECIPE_LOCATION TF_REGISTRY_TOKEN + ensure_namespace "${COMBINED_NAMESPACE}" + ensure_namespace "${COMBINED_SECRETS_NAMESPACE}" + + if [[ "${PUBLISH_RECIPE}" == "true" ]]; then + echo "Publishing Bicep recipe to ${BICEP_RECIPE}" + rad bicep publish \ + --file "${RECIPES_DIR}/redis-recipe.bicep" \ + --target "br:${BICEP_RECIPE}" + fi + + echo "Deploying Scenario 3 (combined)" + rad deploy "${BICEP_DIR}/combined.bicep" \ + --parameters terraformRegistryHostname="${TF_REGISTRY_HOST}" \ + --parameters terraformRecipeLocation="${TF_RECIPE_LOCATION}" \ + --parameters terraformRegistryToken="${TF_REGISTRY_TOKEN}" \ + --parameters bicepRegistryHostname="${BICEP_REGISTRY}" \ + --parameters bicepRegistryUsername="${BICEP_REGISTRY_USERNAME}" \ + --parameters bicepRegistryPassword="${BICEP_REGISTRY_PASSWORD}" + + echo "Verifying Scenario 3" + rad resource show Radius.Core/environments combined-env + rad resource list Radius.Core/terraformSettings + rad resource list Radius.Core/bicepSettings +} + +cleanup_demo() { + echo "Cleaning up demo resources" + rad group switch "${RAD_GROUP}" 2>/dev/null || true + rad app delete "${BICEP_NAMESPACE}" --yes 2>/dev/null || true + rad app delete "${TF_NAMESPACE}" --yes 2>/dev/null || true + rad app delete "${COMBINED_NAMESPACE}" --yes 2>/dev/null || true + rad group switch default 2>/dev/null || true + rad group delete "${RAD_GROUP}" --yes 2>/dev/null || true + kubectl delete namespace \ + "${BICEP_NAMESPACE}" "${TF_NAMESPACE}" "${COMBINED_NAMESPACE}" \ + "${BICEP_SECRETS_NAMESPACE}" "${TF_SECRETS_NAMESPACE}" \ + "${COMBINED_SECRETS_NAMESPACE}" \ + --ignore-not-found + echo "Cleanup complete" +} + +main() { + require_tools + + if [[ "${DO_CLEANUP}" == "true" ]]; then + cleanup_demo + exit 0 + fi + + echo "============================================================" + echo "Private registries E2E demo - scenario: ${SCENARIO}" + echo "============================================================" + + setup_group + + case "${SCENARIO}" in + bicep) run_bicep ;; + terraform) run_terraform ;; + combined) run_combined ;; + all) + run_bicep + run_terraform + run_combined + ;; + *) + echo "Error: unknown scenario '${SCENARIO}'" >&2 + exit 1 + ;; + esac + + echo "============================================================" + echo "Done. Run '${SCRIPT_NAME} --cleanup' to remove demo resources." + echo "============================================================" +} + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + -s | --scenario) + SCENARIO="$2" + shift 2 + ;; + --skip-publish) + PUBLISH_RECIPE="false" + shift + ;; + -c | --cleanup) + DO_CLEANUP="true" + shift + ;; + -h | --help) + usage + ;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; + esac +done + +main