From b46dd96ee8e2dc2098bdcb751f7b8ec3397c7b45 Mon Sep 17 00:00:00 2001 From: Nell Shamrell-Harrington Date: Fri, 16 Oct 2026 13:44:04 -0700 Subject: [PATCH 01/12] Document explicit plaintext PostgreSQL development demo Gate adoption on public compatible artifacts and synchronized edge samples; preserve password Secret delivery and generate the candidate TLS reference through the canonical producer. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Nell Shamrell-Harrington --- docs/content/getting-started/_index.md | 2 +- .../getting-started/add-connection/_index.md | 73 ++++++++++++++++--- .../getting-started/deploy-demo/_index.md | 24 ++++-- .../getting-started/install-radius/_index.md | 8 +- .../management/existing-recipes/_index.md | 2 + .../postgresqldatabases/index.md | 2 +- docs/static/samples/demo/app-postgresql.bicep | 13 +++- 7 files changed, 100 insertions(+), 24 deletions(-) diff --git a/docs/content/getting-started/_index.md b/docs/content/getting-started/_index.md index b6c524b9c..ef89ff0ad 100644 --- a/docs/content/getting-started/_index.md +++ b/docs/content/getting-started/_index.md @@ -12,7 +12,7 @@ aliases: - /getting-started/first-app/ --- -This guide will show you how to quickly get started with Radius. You will do a basic installation of Radius on a Kubernetes cluster, deploy the Radius Demo sample application, and add a Redis cache connection to it. When you finish, explore the hands-on labs for deeper, scenario-based projects. +This guide will show you how to quickly get started with Radius. You will install Radius on a private, disposable Kubernetes cluster, deploy the Radius Demo sample application, and connect it to a PostgreSQL database and an authored password Secret. The database demo explicitly disables TLS; it is not a production configuration. When you finish, explore the hands-on labs for deeper, scenario-based projects. ## Organization diff --git a/docs/content/getting-started/add-connection/_index.md b/docs/content/getting-started/add-connection/_index.md index f2e165285..f11d12b27 100644 --- a/docs/content/getting-started/add-connection/_index.md +++ b/docs/content/getting-started/add-connection/_index.md @@ -8,39 +8,86 @@ weight: 300 The Radius Demo application runs on its own, but most applications depend on other resources such as databases and caches. In this step you will add a PostgreSQL database and an authored Secret to the application, then connect the container to both resources. +> **Private development only:** This Kubernetes demo explicitly sets the database's `tls` policy to `disabled` and the existing PostgreSQL client's `PGSSLMODE` to `disable`. Password authentication remains enabled, but database traffic is unencrypted and credentials and queries can be exposed in transit. Use only a disposable, private development cluster, never a production database. No TLS Secret or client CA is needed for this mode. + +The Resource Type's default remains `required`. Both `required` and `optional` need operator-provided certificates; `optional` is not a certificate-free alternative. The guarded Azure Recipe rejects `disabled`. For secure deployments and migration, see the [PostgreSQL operator guide](https://github.com/radius-project/resource-types-contrib/blob/131913259f3beb40807afed9730b454128f6d108/Data/postgreSqlDatabases/README.md#kubernetes-tls-prerequisites). Complete the [compatible-artifact prerequisite]({{< ref "/getting-started/install-radius#prerequisites" >}}) before proceeding; the linked policy is a candidate, not proof of published support. + +The Kubernetes database Recipes have no explicit durable database volume. Pod replacement can lose all demo data. Do not use this tutorial to change the policy of an existing database. + ## Add a PostgreSQL database and connections -The `app-postgresql.bicep` definition builds on `app.bicep` by adding a `Radius.Data/postgreSqlDatabases` resource, a `Radius.Security/secrets` resource, a `@secure()` password parameter, and two `connections` entries on the container. The highlighted lines are the additions: +The `app-postgresql.bicep` definition builds on `app.bicep` by adding a `Radius.Data/postgreSqlDatabases` resource, a `Radius.Security/secrets` resource, a `@secure()` password parameter, and two `connections` entries on the container. It also explicitly disables TLS on the server and client. The highlighted lines are the additions: -{{< rad file="/static/samples/demo/app-postgresql.bicep" embed=true markdownConfig=` {hl_lines=["6-8","36-43","47-57","59-70"]}` >}} +{{< rad file="/static/samples/demo/app-postgresql.bicep" embed=true markdownConfig=` {hl_lines=["6-11","32-36","44-51","55-66","68-79"]}` >}} -The `postgresql` resource is provisioned by the PostgreSQL Recipe in the default Recipe Pack. The `password` parameter is declared with `@secure()`, which keeps its value out of deployment logs and history and lets you supply the password at deploy time instead of hardcoding it. The application passes that value directly to the database's `password` property. Because the property is `x-radius-sensitive`, Radius encrypts it and redacts it from reads. +The `postgresql` resource is provisioned by the PostgreSQL Recipe in the Environment's Recipe Pack. The `password` parameter is declared with `@secure()`, which keeps its value out of deployment logs and history and lets you supply the password at deploy time instead of hardcoding it. The application passes that value directly to the database's `password` property. Because the property is `x-radius-sensitive`, Radius encrypts it and redacts it from reads. The application also stores the same value under the `password` key of the authored `postgresqlCredentials` Secret instead of relying on database Recipe `result.secrets` for a developer-owned credential. The Secret has the same application and environment as the other resources, so it shares their lifecycle. Its resource name is deliberately different from the `postgresql-${environmentName}-credentials` Kubernetes Secret owned by the PostgreSQL Recipe. -The `postgresql` connection tells Radius that the container depends on the database. Radius provisions the database first, then injects its ordinary connection values as `CONNECTION_POSTGRESQL_HOST`, `CONNECTION_POSTGRESQL_PORT`, `CONNECTION_POSTGRESQL_DATABASE`, and `CONNECTION_POSTGRESQL_USERNAME`. +The `postgresql` connection tells Radius that the container depends on the database. Radius provisions the database first, then injects its ordinary connection values as `CONNECTION_POSTGRESQL_HOST`, `CONNECTION_POSTGRESQL_PORT`, `CONNECTION_POSTGRESQL_DATABASE`, `CONNECTION_POSTGRESQL_USERNAME`, and `CONNECTION_POSTGRESQL_TLS`. The `postgresqlCredentials` connection points directly to the authored Secret. With a compatible Kubernetes Container Recipe, Radius projects its `password` key through a Kubernetes `secretKeyRef` as `CONNECTION_POSTGRESQLCREDENTIALS_PASSWORD`. The secret value remains in the Secret and is not copied into Recipe output or plaintext container configuration. > **Caution:** The Radius Demo displays all `CONNECTION_*` environment variables it receives, including this password, to illustrate connection injection. Do not display secret values in a production application. > -> **Compatibility:** Automatic Secret connection projection requires a compatible Radius runtime and Kubernetes Container Recipe. In older or mixed-version environments, keep an explicit container `env.valueFrom.secretKeyRef` binding instead of changing a working model. Azure Container Instances behavior is unchanged. +> **Compatibility:** Automatic Secret connection projection requires a compatible Radius runtime and Kubernetes Container Recipe. In older or mixed-version environments, keep an explicit container `env.valueFrom.secretKeyRef` binding instead of changing a working model. This does not remove the separate transport-policy compatibility prerequisite. Azure Container Instances behavior is unchanged. ## Redeploy the application -Deploy the updated definition from its published URL. The `--parameters` argument sets the database password, and `$(openssl rand -hex 16)` generates a random value on each deploy. This works in Bash and Zsh; in PowerShell, generate the value separately and pass it in: +Deploy the updated definition from its published URL using a temporary ARM JSON parameter file. Run the following block in Bash with shell tracing disabled. It creates a private temporary directory and a file readable only by your user, generates a URI-safe hexadecimal password for the demo's existing connection-string construction, and removes the file on success or failure. Only the file path, not the password, appears in the deployment command's arguments. + +```bash +( + set -eu + set +x + umask 077 + credential_dir="$(mktemp -d)" + trap 'rm -f "$credential_dir/parameters.json"; rmdir "$credential_dir"' EXIT + trap 'exit 130' INT + trap 'exit 143' TERM + password="$(openssl rand -hex 16)" + printf '{"$schema":"https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#","contentVersion":"1.0.0.0","parameters":{"password":{"value":"%s"}}}\n' \ + "$password" > "$credential_dir/parameters.json" + unset password + rad deploy https://edge.docs.radapp.io/samples/demo/app-postgresql.bicep \ + --parameters @"$credential_dir/parameters.json" +) +``` -{{< rad-deploy path="samples/demo/app-postgresql.bicep" args=`--parameters password="$(openssl rand -hex 16)"` >}} +Do not regenerate the password to retry a deployment against an existing database. For this disposable tutorial, delete only the tutorial application before starting over. -Radius creates the database and authored Secret, then updates the container with both connections. Forward a local port to the container and open the application again: +Radius creates the database and authored Secret, then updates the container with both connections. Deployment success alone does not establish readiness. Find the database's actual runtime namespace and wait for both current rollouts: ```bash -kubectl port-forward svc/demo-default-web 3000:3000 +kubectl get deployment --all-namespaces \ + -l radapp.io/application=demo-default,radapp.io/resource=postgresql-default +DB_NAMESPACE='' +kubectl rollout status deployment/postgresql-default -n "$DB_NAMESPACE" --timeout=180s +kubectl rollout status deployment/demo-default -n "$DEMO_NAMESPACE" --timeout=180s +kubectl port-forward svc/demo-default-web -n "$DEMO_NAMESPACE" 3000:3000 ``` -Open [http://localhost:3000](http://localhost:3000). The Radius Connections section now lists the `postgresql` database connection and the `postgresqlCredentials` Secret connection. +Use `DEMO_NAMESPACE` from the previous step and replace the database namespace placeholder with the namespace shown for this tutorial's database. Stop if either rollout fails; an old pod can still be serving the previous configuration. + +Open [http://localhost:3000](http://localhost:3000). The Radius Connections section now lists the `postgresql` database connection and the `postgresqlCredentials` Secret connection. The database connection includes `appdb`, `myadmin`, port `5432`, the database's service hostname, and `tls: disabled`. The client separately uses `PGSSLMODE=disable`; the connection's TLS property is not itself a client setting. + +### Verify database-backed behavior + +Create a uniquely titled todo item in the demo, mark it complete, and refresh the page. In another terminal, set the same namespace variables and check the stored row and server transport setting: + +```bash +kubectl exec -n "$DB_NAMESPACE" deployment/postgresql-default -c postgres -- \ + sh -ec 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "SHOW ssl;" -c "SELECT title, done FROM items;"' +kubectl get pods -n "$DEMO_NAMESPACE" -l radapp.io/resource=demo-default -o wide +kubectl exec -n "$DB_NAMESPACE" deployment/postgresql-default -c postgres -- \ + sh -ec 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "SELECT a.client_addr, s.ssl FROM pg_stat_activity a JOIN pg_stat_ssl s USING (pid) WHERE a.datname = current_database() AND a.client_addr IS NOT NULL;"' +``` + +Expect `ssl` to be `off`, your completed item to appear in `items`, and the connection from the current demo pod's IP address to have `ssl = f`. If no demo connection appears, refresh the application and repeat the query; an empty result is not proof. These local administrative queries inspect the demo's connection; a separate plaintext `psql` connection would not prove how the demo connects. Delete the item in the UI and repeat the row query to confirm it was removed. Stop the port forward with `Ctrl+C`. + +These checks establish database-backed plaintext behavior, not Recipe provenance: an older plaintext Recipe can produce the same results. The compatible-artifact prerequisite still applies. ## View the connections in the Dashboard @@ -56,14 +103,16 @@ To learn more about modeling dependencies between resources, see [How to model a ## Clean up -Delete the Radius Demo application. Because the database and authored Secret belong to this application, Radius deletes them with the container: +Delete only the Radius Demo application created in this tutorial. Because the database and authored Secret belong to this application, Radius deletes them with the container: ```bash rad application delete demo-default --preview ``` -Optionally, uninstall Radius and remove all of its data: +The deployment block removes its temporary credential file even if deployment fails. If your shell was forcibly terminated before its exit trap ran, remove only the `parameters.json` file and temporary directory created by that invocation. Do not delete other applications, namespaces, or operator-owned Secrets. + +Only if this is a disposable cluster dedicated to this tutorial, optionally uninstall Radius and remove all of its data. **Do not purge a shared installation:** ```bash rad uninstall kubernetes --purge diff --git a/docs/content/getting-started/deploy-demo/_index.md b/docs/content/getting-started/deploy-demo/_index.md index 5f99b2e9f..b73e1a392 100644 --- a/docs/content/getting-started/deploy-demo/_index.md +++ b/docs/content/getting-started/deploy-demo/_index.md @@ -20,12 +20,25 @@ Deploy the Radius Demo using `rad deploy`: {{< rad-deploy path="samples/demo/app.bicep" >}} -Radius deploys the application to the `default` Environment, which is configured to use the `default` namespace of your Kubernetes cluster. The `Radius.Compute/containers` resource is provisioned by a Recipe that creates a Kubernetes Deployment and a `ClusterIP` Service. +Radius deploys the application to the `default` Environment. The `Radius.Compute/containers` resource is provisioned by a Recipe that creates a Kubernetes Deployment and a `ClusterIP` Service. Find the actual runtime namespace rather than assuming it matches the Environment's configured namespace: + +```bash +rad environment show default --preview --output json +kubectl get deployment --all-namespaces \ + -l radapp.io/application=demo-default,radapp.io/resource=demo-default +``` + +Set `DEMO_NAMESPACE` to the namespace shown for this tutorial's deployment. If more than one deployment matches, identify the one in your tutorial Environment before continuing: + +```bash +DEMO_NAMESPACE='' +kubectl rollout status deployment/demo-default -n "$DEMO_NAMESPACE" --timeout=180s +``` View the deployed resources with `kubectl`: ```bash -kubectl get pod,deployment,service +kubectl get pod,deployment,service -n "$DEMO_NAMESPACE" -l radapp.io/application=demo-default ``` Example output: @@ -39,7 +52,6 @@ deployment.apps/demo-default 1/1 1 1 3m18s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/demo-default-web ClusterIP 10.96.177.228 3000/TCP 3m18s -service/kubernetes ClusterIP 10.96.0.1 443/TCP 4m10s ``` ## Browse the Radius Demo application @@ -47,10 +59,10 @@ service/kubernetes ClusterIP 10.96.0.1 443/TCP Use `kubectl` to forward a local port to the container's Kubernetes Service: ```bash -kubectl port-forward svc/demo-default-web 3000:3000 +kubectl port-forward svc/demo-default-web -n "$DEMO_NAMESPACE" 3000:3000 ``` -Open [http://localhost:3000](http://localhost:3000) in your browser. The Radius Connections section says "No connections defined"; you will add a Redis cache connection in the next step. Press `Ctrl+C` to stop the port forward. +Open [http://localhost:3000](http://localhost:3000) in your browser. The Radius Connections section says "No connections defined"; you will add PostgreSQL and password Secret connections in the next step. Press `Ctrl+C` to stop the port forward. ## Browse the Radius Dashboard @@ -87,6 +99,6 @@ You have deployed your first application with Radius. For a deeper walkthrough o ## Next steps -In part three of this guide, you will add a Redis cache resource and a connection to the `demo-default` container. +In part three of this guide, you will add a PostgreSQL database and an authored password Secret, then connect both to the `demo-default` container. {{< button text="Next step: Add a connection" page="getting-started/add-connection" >}} diff --git a/docs/content/getting-started/install-radius/_index.md b/docs/content/getting-started/install-radius/_index.md index dd116892f..8e4913803 100644 --- a/docs/content/getting-started/install-radius/_index.md +++ b/docs/content/getting-started/install-radius/_index.md @@ -10,7 +10,11 @@ In part one, you will install Radius on an existing Kubernetes cluster. ## Prerequisites -For this guide you only need a **Kubernetes cluster**. To install Radius, your user must have the cluster-admin role. Radius }}">supports AKS, EKS, k3d, and kind clusters. Running a local cluster with k3d or kind is recommended. +Use a **private, disposable Kubernetes cluster** with no existing `demo-default` application. To install Radius, your user must have the cluster-admin role. Radius }}">supports AKS, EKS, k3d, and kind clusters. Running a local cluster with k3d or kind is recommended. Do not expose the demo or database publicly: step three deliberately uses unencrypted PostgreSQL connections. + +The commands use Bash on Linux or macOS, `kubectl`, and OpenSSL. On Windows, use WSL with a Linux filesystem for the protected temporary password file; Git Bash on a Windows filesystem does not provide the same Unix permission guarantees. Use a compatible Radius CLI/runtime, Bicep compiler and extension, registered PostgreSQL schema, and Kubernetes Recipe Pack. The schema must accept `tls: 'disabled'`, the PostgreSQL Recipe must implement that policy, and the Container and Secret Recipes must support password projection through `secretKeyRef`. Secret support alone does not establish TLS-policy compatibility. Check the Environment's assigned Recipe Pack and any overrides, not just the CLI version. + +> **Edge adoption gate:** This tutorial depends on the [PostgreSQL transport-policy change](https://github.com/radius-project/resource-types-contrib/pull/394) and the matching samples update. Do not run the database step until compatible schemas, extensions, and recipes are publicly available and the sample has landed on `edge`. Public edge artifacts are sufficient; no numbered release is required. An older plaintext Recipe, a schema warning, or an unpublished candidate is not a supported substitute. See the [PostgreSQL operator guide](https://github.com/radius-project/resource-types-contrib/blob/131913259f3beb40807afed9730b454128f6d108/Data/postgreSqlDatabases/README.md#adopting-the-change) for the candidate policy and adoption requirements. ## Install the Radius CLI @@ -25,7 +29,7 @@ mkdir radius cd radius ``` -Ensure your cluster is set as your current context with `kubectl config current-context`. If the context needs updating, change it with `kubectl config set-context `. Then install Radius with the `rad initialize` command: +Ensure your disposable cluster is set as your current context with `kubectl config current-context`. If the context needs updating, change it with `kubectl config use-context `. Then install Radius with the `rad initialize` command: ```bash diff --git a/docs/content/management/existing-recipes/_index.md b/docs/content/management/existing-recipes/_index.md index 609be9c93..396352430 100644 --- a/docs/content/management/existing-recipes/_index.md +++ b/docs/content/management/existing-recipes/_index.md @@ -96,6 +96,8 @@ resource dataRecipes 'Radius.Core/recipePacks@2025-08-01-preview' = { The `parameters` and `outputs` names come from the module you reference, so consult the module's own documentation for its input variables and outputs. Radius passes only the parameters you map; every other input uses the module's own default. +> **PostgreSQL transport policy:** The custom Azure example above calls the AVM directly. It bypasses the guarded PostgreSQL Recipe's transport-policy validation, including its rejection of `tls: 'disabled'`; it does not map the resource's `tls` request. Do not assume a custom direct-AVM Recipe enforces that contract. See the [PostgreSQL operator guide](https://github.com/radius-project/resource-types-contrib/blob/131913259f3beb40807afed9730b454128f6d108/Data/postgreSqlDatabases/README.md#using-the-resource-type) for the candidate guarded Recipe and adoption requirements. + ## Resolve developer inputs with context expressions Any `parameters` value can include a `{{context.*}}` expression. When Radius runs the recipe, it replaces the expression with a real value, such as a property the developer set on the resource, the resource's name, or a detail about the Environment or cloud provider. The [recipe context reference]({{< ref "/reference/recipes/context" >}}) lists every value you can read from `context`. For example: diff --git a/docs/content/reference/resources/radius.data/2025-08-01-preview/postgresqldatabases/index.md b/docs/content/reference/resources/radius.data/2025-08-01-preview/postgresqldatabases/index.md index 035d7de24..291175613 100644 --- a/docs/content/reference/resources/radius.data/2025-08-01-preview/postgresqldatabases/index.md +++ b/docs/content/reference/resources/radius.data/2025-08-01-preview/postgresqldatabases/index.md @@ -73,7 +73,7 @@ The connection automatically injects environment variables into the container fo | `password` | string | (Required) The administrator password for the PostgreSQL database. Marked `x-radius-sensitive`: Radius encrypts it at rest, redacts it on reads, and exposes it decrypted only to the recipe as `{{context.resource.properties.password}}`. | | `port` | integer | (Optional) The TCP port used to connect to the database. Defaults to `5432`, the standard PostgreSQL port that every Recipe in this repository provisions and the port PostgreSQL flexible server is fixed to. A Recipe that provisions the database on a different port reports the real port as an output, which overwrites this value once the deployment finishes. Setting it in an application definition changes only the value reported to connected containers, never the port the server listens on, so leave it unset. | | `size` | string | (Optional) The size of the PostgreSQL database. Defaults to `S` if not provided.
Allowed values: `L`, `M`, `S`. | -| `tls` | string | (Optional) The requested transport policy for database connections. Configure your client using `CONNECTION_POSTGRESQL_TLS`. Defaults to `required`. Use `optional` only when the server is not publicly reachable; it permits connections without TLS.
Allowed values: `optional`, `required`. | +| `tls` | string | (Optional) Transport policy, projected as CONNECTION_POSTGRESQL_TLS. Defaults to required (TLS only). On Kubernetes, optional supports TLS and plaintext; both modes require an operator-owned TLS Secret. Explicit disabled is Kubernetes-only: plaintext only, with no certificate requirement. Use optional/disabled only on isolated private networks. Configure trusted CA certificates and hostname verification separately. Azure rejects disabled; operator overrides retain precedence for required/optional.
Allowed values: `disabled`, `optional`, `required`. | | `username` | string | (Required) The administrator username for the PostgreSQL database. Provided directly on the resource and passed to the recipe as `{{context.resource.properties.username}}`. | ## Object Properties diff --git a/docs/static/samples/demo/app-postgresql.bicep b/docs/static/samples/demo/app-postgresql.bicep index 868e2846c..75b3cfcdc 100644 --- a/docs/static/samples/demo/app-postgresql.bicep +++ b/docs/static/samples/demo/app-postgresql.bicep @@ -3,10 +3,13 @@ extension radius @description('The Radius Environment ID. Injected automatically by the rad CLI.') param environment string -@description('The administrator password for the PostgreSQL database. Pass via the CLI, e.g. -p password=$(openssl rand -hex 16).') +@description('The administrator password for the PostgreSQL database. Supply a URI-safe random hexadecimal value in a protected parameter file.') @secure() param password string +// Defaults to the published image; CI overrides this to test the freshly built image. +param image string = 'ghcr.io/radius-project/samples/demo:latest' + // Environment name derived from the Environment ID, used to keep resource names // unique per environment (e.g. dev/test/prod) within the same resource group. var environmentName = last(split(environment, '/')) @@ -25,7 +28,12 @@ resource demoContainer 'Radius.Compute/containers@2025-08-01-preview' = { application: demoApp.id containers: { web: { - image: 'ghcr.io/radius-project/samples/demo:latest' + image: image + env: { + PGSSLMODE: { + value: 'disable' + } + } ports: { web: { containerPort: 3000 @@ -50,6 +58,7 @@ resource postgresql 'Radius.Data/postgreSqlDatabases@2025-08-01-preview' = { environment: environment application: demoApp.id size: 'S' + tls: 'disabled' database: 'appdb' username: 'myadmin' password: password From b82c987de9f870ef96053991efcde775cffed25d Mon Sep 17 00:00:00 2001 From: Nell Shamrell-Harrington Date: Fri, 16 Oct 2026 13:48:29 -0700 Subject: [PATCH 02/12] Avoid redundant version-specific environment command Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Nell Shamrell-Harrington --- docs/content/getting-started/deploy-demo/_index.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/content/getting-started/deploy-demo/_index.md b/docs/content/getting-started/deploy-demo/_index.md index b73e1a392..d83210ca1 100644 --- a/docs/content/getting-started/deploy-demo/_index.md +++ b/docs/content/getting-started/deploy-demo/_index.md @@ -23,7 +23,6 @@ Deploy the Radius Demo using `rad deploy`: Radius deploys the application to the `default` Environment. The `Radius.Compute/containers` resource is provisioned by a Recipe that creates a Kubernetes Deployment and a `ClusterIP` Service. Find the actual runtime namespace rather than assuming it matches the Environment's configured namespace: ```bash -rad environment show default --preview --output json kubectl get deployment --all-namespaces \ -l radapp.io/application=demo-default,radapp.io/resource=demo-default ``` From 9a89a2b1b7f39e3e30eb2657bd8d9bba6c313153 Mon Sep 17 00:00:00 2001 From: Nell Shamrell-Harrington Date: Fri, 16 Oct 2026 14:12:52 -0700 Subject: [PATCH 03/12] Fail PostgreSQL demo validation on unsupported TLS warnings Classify BCP036 and BCP037 for the demo TLS property as failures even when Bicep exits successfully. Cover diagnostics and validation flow with standard-library fixtures in the existing CI job. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Nell Shamrell-Harrington --- .github/scripts/test_validate_bicep.py | 66 ++++++++++++ .github/scripts/validate_bicep.py | 137 +++++++++++++++---------- .github/workflows/validate-bicep.yaml | 4 +- 3 files changed, 150 insertions(+), 57 deletions(-) create mode 100644 .github/scripts/test_validate_bicep.py diff --git a/.github/scripts/test_validate_bicep.py b/.github/scripts/test_validate_bicep.py new file mode 100644 index 000000000..42d08fcab --- /dev/null +++ b/.github/scripts/test_validate_bicep.py @@ -0,0 +1,66 @@ +import contextlib +import io +import subprocess +import unittest +from unittest.mock import patch + +import validate_bicep + + +SAMPLE = "docs/static/samples/demo/app-postgresql.bicep" +TLS_TYPE_WARNING = ( + 'Warning BCP036: The property "tls" expected a value of type ' + '"\'optional\' | \'required\' | null" but the provided value is of type ' + '"\'disabled\'".' +) +TLS_PROPERTY_WARNING = ( + 'Warning BCP037: The property "tls" is not allowed on objects of type ' + '"postgreSqlDatabasesProperties".' +) + + +class ValidationTests(unittest.TestCase): + def test_diagnostic_fixtures(self): + fixtures = [ + ("supported", SAMPLE, "", 0, False), + ("unsupported value", SAMPLE, TLS_TYPE_WARNING, 0, True), + ("unsupported property", SAMPLE, TLS_PROPERTY_WARNING, 0, True), + ("windows path", SAMPLE.replace("/", "\\"), TLS_TYPE_WARNING, 0, True), + ("relative windows path", ".\\" + SAMPLE.replace("/", "\\"), TLS_TYPE_WARNING, 0, True), + ("relative path", "./" + SAMPLE, TLS_TYPE_WARNING, 0, True), + ("single quotes", SAMPLE, TLS_TYPE_WARNING.replace('"tls"', "'tls'"), 0, True), + ("served copy", SAMPLE.replace("/static/", "/public/"), TLS_TYPE_WARNING, 0, True), + ("unrelated file", "other/app-postgresql.bicep", TLS_TYPE_WARNING, 0, False), + ("other sample", SAMPLE.replace("postgresql", "redis"), TLS_TYPE_WARNING, 0, False), + ("other property", SAMPLE, TLS_TYPE_WARNING.replace('"tls"', '"size"'), 0, False), + ("different diagnostic", SAMPLE, TLS_TYPE_WARNING.replace("BCP036", "BCP035"), 0, False), + ("lint warning", SAMPLE, 'Warning no-unused-vars: Variable "tls" is unused.', 0, False), + ("separate diagnostic lines", SAMPLE, 'Warning BCP036: Other property.\nWarning no-unused-vars: The property "tls" is unused.', 0, False), + ("compiler error", "other.bicep", "Error BCP001: Invalid syntax.", 0, True), + ("nonzero exit", SAMPLE, "", 1, True), + ] + for name, filename, diagnostic, exitcode, expected in fixtures: + with self.subTest(name=name): + self.assertEqual( + validate_bicep.is_validation_failure( + filename, f"{filename}(93,7) : {diagnostic}", exitcode + ), + expected, + ) + + def test_validation_path_rejects_exit_zero_warning(self): + for diagnostic, expected in [(TLS_TYPE_WARNING, 1), (TLS_PROPERTY_WARNING, 1), ("", 0)]: + with self.subTest(diagnostic=diagnostic): + result = subprocess.CompletedProcess( + args=[], returncode=0, stderr=diagnostic.encode("utf-8") + ) + output = io.StringIO() + with patch.object(validate_bicep.os, "walk", return_value=[ + ("docs/static/samples/demo", [], ["app-postgresql.bicep"]) + ]), patch.object(validate_bicep.subprocess, "run", return_value=result), contextlib.redirect_stdout(output): + self.assertEqual(validate_bicep.main(), expected) + self.assertEqual("Failed:" in output.getvalue(), expected == 1) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/scripts/validate_bicep.py b/.github/scripts/validate_bicep.py index a04729099..3039302e3 100644 --- a/.github/scripts/validate_bicep.py +++ b/.github/scripts/validate_bicep.py @@ -1,68 +1,93 @@ import os +import posixpath +import re import subprocess import concurrent.futures num_workers = 5 -# set the bicep binary name based on the OS -if os.name == 'nt': - bicep_bin = 'bicep.exe' - home_path = os.environ['USERPROFILE'] -else: - bicep_bin = 'bicep' - home_path = os.environ['HOME'] - -# set the default Bicep path based on the runner -if os.environ.get("GITHUB_ACTIONS", "false") == "false": - print("Running locally") - bicep_path = os.path.join(home_path, '.rad', 'bin') -else: - print("Running in GitHub Actions", flush=True) - bicep_path = "." - -# allow override through an environment variable -bicep_path = os.environ.get("BICEP_PATH", bicep_path) -bicep_executable = os.path.join(bicep_path, bicep_bin) - -print(f"Using Bicep binary: {bicep_executable}", flush=True) - -files = [] -failures = [] -warnings = [] - -# Walk the directory tree and find all .bicep files -for root, _, filenames in os.walk("."): - for filename in filenames: - if filename.endswith(".bicep"): - files.append(os.path.join(root, filename)) - -def validate_file(f): - print(f"Validating {f}...", flush=True) - - result = subprocess.run( - [bicep_executable, "build", f, "--stdout"], - stderr=subprocess.PIPE, - stdout=subprocess.DEVNULL, - ) - stderr = result.stderr.decode("utf-8") - exitcode = result.returncode - - if "Warning" in stderr: - warnings.append(f) - print(stderr, flush=True) +def is_validation_failure(filename, stderr, exitcode): if exitcode != 0 or "Error" in stderr: - failures.append(f) - print(stderr, flush=True) + return True + + normalized = posixpath.normpath(filename.replace("\\", "/")) + if normalized not in ( + "docs/static/samples/demo/app-postgresql.bicep", + "docs/public/samples/demo/app-postgresql.bicep", + ): + return False + + return any( + re.search(r"\bBCP03[67]:\s+The property ([\"'])tls\1(?:\s|$)", line) + for line in stderr.splitlines() + ) + + +def main(): + # set the bicep binary name based on the OS + if os.name == 'nt': + bicep_bin = 'bicep.exe' + home_path = os.environ['USERPROFILE'] + else: + bicep_bin = 'bicep' + home_path = os.environ['HOME'] + + # set the default Bicep path based on the runner + if os.environ.get("GITHUB_ACTIONS", "false") == "false": + print("Running locally") + bicep_path = os.path.join(home_path, '.rad', 'bin') + else: + print("Running in GitHub Actions", flush=True) + bicep_path = "." + + # allow override through an environment variable + bicep_path = os.environ.get("BICEP_PATH", bicep_path) + bicep_executable = os.path.join(bicep_path, bicep_bin) + + print(f"Using Bicep binary: {bicep_executable}", flush=True) + + files = [] + failures = [] + warnings = [] + + # Walk the directory tree and find all .bicep files + for root, _, filenames in os.walk("."): + for filename in filenames: + if filename.endswith(".bicep"): + files.append(os.path.join(root, filename)) + + def validate_file(f): + print(f"Validating {f}...", flush=True) + + result = subprocess.run( + [bicep_executable, "build", f, "--stdout"], + stderr=subprocess.PIPE, + stdout=subprocess.DEVNULL, + ) + stderr = result.stderr.decode("utf-8") + exitcode = result.returncode + + if "Warning" in stderr: + warnings.append(f) + print(stderr, flush=True) + + if is_validation_failure(f, stderr, exitcode): + failures.append(f) + print(stderr, flush=True) + + with concurrent.futures.ThreadPoolExecutor(max_workers=num_workers) as executor: + futures = [executor.submit(validate_file, f) for f in files] + concurrent.futures.wait(futures) + + for f in failures: + print(f"Failed: {f}", flush=True) -with concurrent.futures.ThreadPoolExecutor(max_workers=num_workers) as executor: - futures = [executor.submit(validate_file, f) for f in files] -concurrent.futures.wait(futures) + for f in warnings: + print(f"Warning: {f}", flush=True) -for f in failures: - print(f"Failed: {f}", flush=True) + return len(failures) -for f in warnings: - print(f"Warning: {f}", flush=True) -exit(len(failures)) +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/workflows/validate-bicep.yaml b/.github/workflows/validate-bicep.yaml index b3324e759..6a888d961 100644 --- a/.github/workflows/validate-bicep.yaml +++ b/.github/workflows/validate-bicep.yaml @@ -50,4 +50,6 @@ jobs: ./bicep --version - name: Verify Bicep files - run: python ./.github/scripts/validate_bicep.py + run: | + python -m unittest discover -s .github/scripts -p test_validate_bicep.py + python ./.github/scripts/validate_bicep.py From b023f6182d76e9de0410ebe21cc55bb2368c0c54 Mon Sep 17 00:00:00 2001 From: Nell Shamrell-Harrington Date: Fri, 16 Oct 2026 14:14:17 -0700 Subject: [PATCH 04/12] Scope TLS diagnostic failures to their source file Reject only diagnostics attributed to the approved demo path, preserving warnings from unrelated imported files. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Nell Shamrell-Harrington --- .github/scripts/test_validate_bicep.py | 19 +++++++++++++++- .github/scripts/validate_bicep.py | 31 +++++++++++++++++--------- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/.github/scripts/test_validate_bicep.py b/.github/scripts/test_validate_bicep.py index 42d08fcab..b1a8bf57c 100644 --- a/.github/scripts/test_validate_bicep.py +++ b/.github/scripts/test_validate_bicep.py @@ -1,5 +1,6 @@ import contextlib import io +import os import subprocess import unittest from unittest.mock import patch @@ -52,7 +53,8 @@ def test_validation_path_rejects_exit_zero_warning(self): for diagnostic, expected in [(TLS_TYPE_WARNING, 1), (TLS_PROPERTY_WARNING, 1), ("", 0)]: with self.subTest(diagnostic=diagnostic): result = subprocess.CompletedProcess( - args=[], returncode=0, stderr=diagnostic.encode("utf-8") + args=[], returncode=0, + stderr=f"{SAMPLE}(61,10) : {diagnostic}".encode("utf-8") ) output = io.StringIO() with patch.object(validate_bicep.os, "walk", return_value=[ @@ -61,6 +63,21 @@ def test_validation_path_rejects_exit_zero_warning(self): self.assertEqual(validate_bicep.main(), expected) self.assertEqual("Failed:" in output.getvalue(), expected == 1) + def test_diagnostic_must_belong_to_sample(self): + for filename, expected in [ + (os.path.abspath(SAMPLE), True), + (".\\" + SAMPLE.replace("/", "\\"), True), + ("other/app-postgresql.bicep", False), + (SAMPLE.replace("postgresql", "redis"), False), + ]: + with self.subTest(filename=filename): + diagnostic = f"{filename}(120,9) : {TLS_TYPE_WARNING}" + self.assertEqual( + validate_bicep.is_validation_failure(SAMPLE, diagnostic, 0), + expected, + ) + self.assertFalse(validate_bicep.is_validation_failure(SAMPLE, TLS_TYPE_WARNING, 0)) + if __name__ == "__main__": unittest.main() diff --git a/.github/scripts/validate_bicep.py b/.github/scripts/validate_bicep.py index 3039302e3..032f189b3 100644 --- a/.github/scripts/validate_bicep.py +++ b/.github/scripts/validate_bicep.py @@ -1,5 +1,4 @@ import os -import posixpath import re import subprocess import concurrent.futures @@ -11,17 +10,29 @@ def is_validation_failure(filename, stderr, exitcode): if exitcode != 0 or "Error" in stderr: return True - normalized = posixpath.normpath(filename.replace("\\", "/")) - if normalized not in ( - "docs/static/samples/demo/app-postgresql.bicep", - "docs/public/samples/demo/app-postgresql.bicep", - ): + sample_path = os.path.normcase(os.path.abspath(filename.replace("\\", "/"))) + if sample_path not in { + os.path.normcase(os.path.abspath(path)) + for path in ( + "docs/static/samples/demo/app-postgresql.bicep", + "docs/public/samples/demo/app-postgresql.bicep", + ) + }: return False - return any( - re.search(r"\bBCP03[67]:\s+The property ([\"'])tls\1(?:\s|$)", line) - for line in stderr.splitlines() - ) + for line in stderr.splitlines(): + diagnostic = re.match( + r"^(.+)\(\d+,\d+\)\s*:\s*(?:Warning|Error)\s+BCP03[67]:" + r"\s+The property ([\"'])tls\2(?:\s|$)", + line, + ) + if diagnostic: + diagnostic_path = os.path.normcase(os.path.abspath( + diagnostic.group(1).replace("\\", "/") + )) + if diagnostic_path == sample_path: + return True + return False def main(): From c7c2d6f9fe76067320bd1717e0ec445017355798 Mon Sep 17 00:00:00 2001 From: Nell Shamrell-Harrington Date: Fri, 16 Oct 2026 14:24:01 -0700 Subject: [PATCH 05/12] Clarify namespace release prerequisite for edge adoption Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Nell Shamrell-Harrington --- docs/content/getting-started/install-radius/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/getting-started/install-radius/_index.md b/docs/content/getting-started/install-radius/_index.md index 8e4913803..cd1c750a2 100644 --- a/docs/content/getting-started/install-radius/_index.md +++ b/docs/content/getting-started/install-radius/_index.md @@ -14,7 +14,7 @@ Use a **private, disposable Kubernetes cluster** with no existing `demo-default` The commands use Bash on Linux or macOS, `kubectl`, and OpenSSL. On Windows, use WSL with a Linux filesystem for the protected temporary password file; Git Bash on a Windows filesystem does not provide the same Unix permission guarantees. Use a compatible Radius CLI/runtime, Bicep compiler and extension, registered PostgreSQL schema, and Kubernetes Recipe Pack. The schema must accept `tls: 'disabled'`, the PostgreSQL Recipe must implement that policy, and the Container and Secret Recipes must support password projection through `secretKeyRef`. Secret support alone does not establish TLS-policy compatibility. Check the Environment's assigned Recipe Pack and any overrides, not just the CLI version. -> **Edge adoption gate:** This tutorial depends on the [PostgreSQL transport-policy change](https://github.com/radius-project/resource-types-contrib/pull/394) and the matching samples update. Do not run the database step until compatible schemas, extensions, and recipes are publicly available and the sample has landed on `edge`. Public edge artifacts are sufficient; no numbered release is required. An older plaintext Recipe, a schema warning, or an unpublished candidate is not a supported substitute. See the [PostgreSQL operator guide](https://github.com/radius-project/resource-types-contrib/blob/131913259f3beb40807afed9730b454128f6d108/Data/postgreSqlDatabases/README.md#adopting-the-change) for the candidate policy and adoption requirements. +> **Edge adoption gate:** This tutorial depends on the [PostgreSQL transport-policy change](https://github.com/radius-project/resource-types-contrib/pull/394) and the matching samples update. Do not run the database step until compatible schemas, extensions, and recipes are publicly available and the sample has landed on `edge`. This tutorial targets compatible Radius edge artifacts, not an upgrade to a stable Radius release or changes to `v0.61`. However, Radius currently pins `Radius.Data` to a stable namespace release. Advancing that pin requires a separately approved upstream namespace release containing the fix, followed by Radius adoption and publication of compatible artifacts. Publishing an edge Recipe alone does not update the schema. This tutorial does not authorize release or adoption. An older plaintext Recipe, a schema warning, or an unpublished candidate is not a supported substitute. See the [PostgreSQL operator guide](https://github.com/radius-project/resource-types-contrib/blob/131913259f3beb40807afed9730b454128f6d108/Data/postgreSqlDatabases/README.md#adopting-the-change) for the candidate policy and adoption requirements. ## Install the Radius CLI From 85e6bf29592271ad80e32bf396cadff89e2f696b Mon Sep 17 00:00:00 2001 From: Nell Shamrell-Harrington Date: Fri, 16 Oct 2026 14:29:36 -0700 Subject: [PATCH 06/12] Refresh operator guide links to signed candidate revision Use the pushed DCO-equivalent candidate without changing generated reference content or claiming published adoption. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Nell Shamrell-Harrington --- docs/content/getting-started/add-connection/_index.md | 2 +- docs/content/getting-started/install-radius/_index.md | 2 +- docs/content/management/existing-recipes/_index.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/content/getting-started/add-connection/_index.md b/docs/content/getting-started/add-connection/_index.md index f11d12b27..aa7ed9712 100644 --- a/docs/content/getting-started/add-connection/_index.md +++ b/docs/content/getting-started/add-connection/_index.md @@ -10,7 +10,7 @@ The Radius Demo application runs on its own, but most applications depend on oth > **Private development only:** This Kubernetes demo explicitly sets the database's `tls` policy to `disabled` and the existing PostgreSQL client's `PGSSLMODE` to `disable`. Password authentication remains enabled, but database traffic is unencrypted and credentials and queries can be exposed in transit. Use only a disposable, private development cluster, never a production database. No TLS Secret or client CA is needed for this mode. -The Resource Type's default remains `required`. Both `required` and `optional` need operator-provided certificates; `optional` is not a certificate-free alternative. The guarded Azure Recipe rejects `disabled`. For secure deployments and migration, see the [PostgreSQL operator guide](https://github.com/radius-project/resource-types-contrib/blob/131913259f3beb40807afed9730b454128f6d108/Data/postgreSqlDatabases/README.md#kubernetes-tls-prerequisites). Complete the [compatible-artifact prerequisite]({{< ref "/getting-started/install-radius#prerequisites" >}}) before proceeding; the linked policy is a candidate, not proof of published support. +The Resource Type's default remains `required`. Both `required` and `optional` need operator-provided certificates; `optional` is not a certificate-free alternative. The guarded Azure Recipe rejects `disabled`. For secure deployments and migration, see the [PostgreSQL operator guide](https://github.com/radius-project/resource-types-contrib/blob/67b77684cc165f069a392b7d3e9151318feab062/Data/postgreSqlDatabases/README.md#kubernetes-tls-prerequisites). Complete the [compatible-artifact prerequisite]({{< ref "/getting-started/install-radius#prerequisites" >}}) before proceeding; the linked policy is a candidate, not proof of published support. The Kubernetes database Recipes have no explicit durable database volume. Pod replacement can lose all demo data. Do not use this tutorial to change the policy of an existing database. diff --git a/docs/content/getting-started/install-radius/_index.md b/docs/content/getting-started/install-radius/_index.md index cd1c750a2..595d3ac91 100644 --- a/docs/content/getting-started/install-radius/_index.md +++ b/docs/content/getting-started/install-radius/_index.md @@ -14,7 +14,7 @@ Use a **private, disposable Kubernetes cluster** with no existing `demo-default` The commands use Bash on Linux or macOS, `kubectl`, and OpenSSL. On Windows, use WSL with a Linux filesystem for the protected temporary password file; Git Bash on a Windows filesystem does not provide the same Unix permission guarantees. Use a compatible Radius CLI/runtime, Bicep compiler and extension, registered PostgreSQL schema, and Kubernetes Recipe Pack. The schema must accept `tls: 'disabled'`, the PostgreSQL Recipe must implement that policy, and the Container and Secret Recipes must support password projection through `secretKeyRef`. Secret support alone does not establish TLS-policy compatibility. Check the Environment's assigned Recipe Pack and any overrides, not just the CLI version. -> **Edge adoption gate:** This tutorial depends on the [PostgreSQL transport-policy change](https://github.com/radius-project/resource-types-contrib/pull/394) and the matching samples update. Do not run the database step until compatible schemas, extensions, and recipes are publicly available and the sample has landed on `edge`. This tutorial targets compatible Radius edge artifacts, not an upgrade to a stable Radius release or changes to `v0.61`. However, Radius currently pins `Radius.Data` to a stable namespace release. Advancing that pin requires a separately approved upstream namespace release containing the fix, followed by Radius adoption and publication of compatible artifacts. Publishing an edge Recipe alone does not update the schema. This tutorial does not authorize release or adoption. An older plaintext Recipe, a schema warning, or an unpublished candidate is not a supported substitute. See the [PostgreSQL operator guide](https://github.com/radius-project/resource-types-contrib/blob/131913259f3beb40807afed9730b454128f6d108/Data/postgreSqlDatabases/README.md#adopting-the-change) for the candidate policy and adoption requirements. +> **Edge adoption gate:** This tutorial depends on the [PostgreSQL transport-policy change](https://github.com/radius-project/resource-types-contrib/pull/394) and the matching samples update. Do not run the database step until compatible schemas, extensions, and recipes are publicly available and the sample has landed on `edge`. This tutorial targets compatible Radius edge artifacts, not an upgrade to a stable Radius release or changes to `v0.61`. However, Radius currently pins `Radius.Data` to a stable namespace release. Advancing that pin requires a separately approved upstream namespace release containing the fix, followed by Radius adoption and publication of compatible artifacts. Publishing an edge Recipe alone does not update the schema. This tutorial does not authorize release or adoption. An older plaintext Recipe, a schema warning, or an unpublished candidate is not a supported substitute. See the [PostgreSQL operator guide](https://github.com/radius-project/resource-types-contrib/blob/67b77684cc165f069a392b7d3e9151318feab062/Data/postgreSqlDatabases/README.md#adopting-the-change) for the candidate policy and adoption requirements. ## Install the Radius CLI diff --git a/docs/content/management/existing-recipes/_index.md b/docs/content/management/existing-recipes/_index.md index 396352430..cefd7c5b0 100644 --- a/docs/content/management/existing-recipes/_index.md +++ b/docs/content/management/existing-recipes/_index.md @@ -96,7 +96,7 @@ resource dataRecipes 'Radius.Core/recipePacks@2025-08-01-preview' = { The `parameters` and `outputs` names come from the module you reference, so consult the module's own documentation for its input variables and outputs. Radius passes only the parameters you map; every other input uses the module's own default. -> **PostgreSQL transport policy:** The custom Azure example above calls the AVM directly. It bypasses the guarded PostgreSQL Recipe's transport-policy validation, including its rejection of `tls: 'disabled'`; it does not map the resource's `tls` request. Do not assume a custom direct-AVM Recipe enforces that contract. See the [PostgreSQL operator guide](https://github.com/radius-project/resource-types-contrib/blob/131913259f3beb40807afed9730b454128f6d108/Data/postgreSqlDatabases/README.md#using-the-resource-type) for the candidate guarded Recipe and adoption requirements. +> **PostgreSQL transport policy:** The custom Azure example above calls the AVM directly. It bypasses the guarded PostgreSQL Recipe's transport-policy validation, including its rejection of `tls: 'disabled'`; it does not map the resource's `tls` request. Do not assume a custom direct-AVM Recipe enforces that contract. See the [PostgreSQL operator guide](https://github.com/radius-project/resource-types-contrib/blob/67b77684cc165f069a392b7d3e9151318feab062/Data/postgreSqlDatabases/README.md#using-the-resource-type) for the candidate guarded Recipe and adoption requirements. ## Resolve developer inputs with context expressions From 8cbaf9053189acc5d4faf44e3b93c5087fa87aa6 Mon Sep 17 00:00:00 2001 From: Nell Shamrell-Harrington Date: Mon, 19 Oct 2026 13:24:57 -0700 Subject: [PATCH 07/12] Document candidate Recipe Context operation contract Describe explicit Terraform delete semantics and Secret cleanup runtime prerequisites without claiming published support. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Nell Shamrell-Harrington --- docs/content/reference/recipes/context/_index.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/content/reference/recipes/context/_index.md b/docs/content/reference/recipes/context/_index.md index f1500b770..a2748f62c 100644 --- a/docs/content/reference/recipes/context/_index.md +++ b/docs/content/reference/recipes/context/_index.md @@ -30,6 +30,7 @@ Radius automatically injects the `context` object into each recipe at deploy tim | Key | Type | Description | |-----|------|-------------| +| [`operation`](#operation) | string | **Candidate runtime contract:** `deploy` or `delete`. Defaults to `deploy`; only Terraform destroy explicitly receives `delete`. Older runtimes omit this field. | | [`resource`](#resource) | object | Metadata about the resource, defined in the application definition, being deployed including its name, ID, type, and properties. | | [`application`](#application) | object | Metadata about the application the resource belongs to. Populated only when the resource is application-scoped. | | [`environment`](#environment) | object | Metadata about the environment the resource is deployed into. | @@ -37,6 +38,16 @@ Radius automatically injects the `context` object into each recipe at deploy tim | [`azure`](#azure) | object | The Azure provider scope (subscription and resource group) for the deployment. Present only when an Azure provider is configured on the environment. | | [`aws`](#aws) | object | The AWS provider scope (account and region) for the deployment. Present only when an AWS provider is configured on the environment. | +### operation + +> **Candidate runtime requirement:** This field describes a new runtime contract, not published or deployed availability. Recipes that depend on it require adoption of a compatible Radius runtime as well as the matching Recipe. Upgrading the CLI alone does not upgrade an existing runtime. + +`context.operation` is a string with values `deploy` and `delete`. The runtime's `recipecontext.New` constructor defaults it to `deploy`. Only the Terraform destroy path explicitly sets it to `delete`, both in the wrapped `context` input and when resolving direct-module parameter expressions such as `{{context.operation}}`. Do not interpret it as a general deletion signal for every recipe backend. + +Use the explicit operation rather than inferring deletion from null or missing resource properties. Redacted stored Secret properties contain no recoverable original values. In the candidate Azure Secret Terraform Recipe, only an explicit `delete` sets the secret resource's `for_each` to `{}` so Terraform can destroy the existing state without reconstructing those values. Invalid create inputs still fail rather than silently producing an empty deployment. This preserves existing state, provider configuration, and module/resource addresses. + +Older runtimes omit `operation` and remain blocked on this Secret cleanup path; a missing field must not be treated as authorization to delete. Do not remove input validation or substitute fabricated secret values to work around the runtime prerequisite. + ### resource | Key | Type | Description | Example | From 9f5cd42e9697c66e41a7d1e044b1929de0b43993 Mon Sep 17 00:00:00 2001 From: Nell Shamrell-Harrington Date: Mon, 19 Oct 2026 15:42:43 -0700 Subject: [PATCH 08/12] Revert "Document candidate Recipe Context operation contract" This reverts commit 8cbaf9053189acc5d4faf44e3b93c5087fa87aa6. Preserve the existing plaintext tutorial and adoption work while removing the out-of-scope candidate runtime contract documentation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Nell Shamrell-Harrington --- docs/content/reference/recipes/context/_index.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/docs/content/reference/recipes/context/_index.md b/docs/content/reference/recipes/context/_index.md index a2748f62c..f1500b770 100644 --- a/docs/content/reference/recipes/context/_index.md +++ b/docs/content/reference/recipes/context/_index.md @@ -30,7 +30,6 @@ Radius automatically injects the `context` object into each recipe at deploy tim | Key | Type | Description | |-----|------|-------------| -| [`operation`](#operation) | string | **Candidate runtime contract:** `deploy` or `delete`. Defaults to `deploy`; only Terraform destroy explicitly receives `delete`. Older runtimes omit this field. | | [`resource`](#resource) | object | Metadata about the resource, defined in the application definition, being deployed including its name, ID, type, and properties. | | [`application`](#application) | object | Metadata about the application the resource belongs to. Populated only when the resource is application-scoped. | | [`environment`](#environment) | object | Metadata about the environment the resource is deployed into. | @@ -38,16 +37,6 @@ Radius automatically injects the `context` object into each recipe at deploy tim | [`azure`](#azure) | object | The Azure provider scope (subscription and resource group) for the deployment. Present only when an Azure provider is configured on the environment. | | [`aws`](#aws) | object | The AWS provider scope (account and region) for the deployment. Present only when an AWS provider is configured on the environment. | -### operation - -> **Candidate runtime requirement:** This field describes a new runtime contract, not published or deployed availability. Recipes that depend on it require adoption of a compatible Radius runtime as well as the matching Recipe. Upgrading the CLI alone does not upgrade an existing runtime. - -`context.operation` is a string with values `deploy` and `delete`. The runtime's `recipecontext.New` constructor defaults it to `deploy`. Only the Terraform destroy path explicitly sets it to `delete`, both in the wrapped `context` input and when resolving direct-module parameter expressions such as `{{context.operation}}`. Do not interpret it as a general deletion signal for every recipe backend. - -Use the explicit operation rather than inferring deletion from null or missing resource properties. Redacted stored Secret properties contain no recoverable original values. In the candidate Azure Secret Terraform Recipe, only an explicit `delete` sets the secret resource's `for_each` to `{}` so Terraform can destroy the existing state without reconstructing those values. Invalid create inputs still fail rather than silently producing an empty deployment. This preserves existing state, provider configuration, and module/resource addresses. - -Older runtimes omit `operation` and remain blocked on this Secret cleanup path; a missing field must not be treated as authorization to delete. Do not remove input validation or substitute fabricated secret values to work around the runtime prerequisite. - ### resource | Key | Type | Description | Example | From 2bd16a318078973f05de176b47b2fd6f45753dfb Mon Sep 17 00:00:00 2001 From: Nell Shamrell-Harrington Date: Wed, 21 Oct 2026 15:23:10 -0700 Subject: [PATCH 09/12] Limit PostgreSQL documentation to required TLS policy changes Restore unrelated tutorial and validation changes for separate follow-up issues. Keep explicit server/client TLS settings, generated policy reference, compatibility requirements and Azure guard caveat. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Nell Shamrell-Harrington --- .github/scripts/test_validate_bicep.py | 83 ---------- .github/scripts/validate_bicep.py | 148 +++++++----------- .github/workflows/validate-bicep.yaml | 4 +- docs/content/getting-started/_index.md | 2 +- .../getting-started/add-connection/_index.md | 73 ++------- .../getting-started/deploy-demo/_index.md | 23 +-- .../getting-started/install-radius/_index.md | 8 +- docs/static/samples/demo/app-postgresql.bicep | 7 +- 8 files changed, 85 insertions(+), 263 deletions(-) delete mode 100644 .github/scripts/test_validate_bicep.py diff --git a/.github/scripts/test_validate_bicep.py b/.github/scripts/test_validate_bicep.py deleted file mode 100644 index b1a8bf57c..000000000 --- a/.github/scripts/test_validate_bicep.py +++ /dev/null @@ -1,83 +0,0 @@ -import contextlib -import io -import os -import subprocess -import unittest -from unittest.mock import patch - -import validate_bicep - - -SAMPLE = "docs/static/samples/demo/app-postgresql.bicep" -TLS_TYPE_WARNING = ( - 'Warning BCP036: The property "tls" expected a value of type ' - '"\'optional\' | \'required\' | null" but the provided value is of type ' - '"\'disabled\'".' -) -TLS_PROPERTY_WARNING = ( - 'Warning BCP037: The property "tls" is not allowed on objects of type ' - '"postgreSqlDatabasesProperties".' -) - - -class ValidationTests(unittest.TestCase): - def test_diagnostic_fixtures(self): - fixtures = [ - ("supported", SAMPLE, "", 0, False), - ("unsupported value", SAMPLE, TLS_TYPE_WARNING, 0, True), - ("unsupported property", SAMPLE, TLS_PROPERTY_WARNING, 0, True), - ("windows path", SAMPLE.replace("/", "\\"), TLS_TYPE_WARNING, 0, True), - ("relative windows path", ".\\" + SAMPLE.replace("/", "\\"), TLS_TYPE_WARNING, 0, True), - ("relative path", "./" + SAMPLE, TLS_TYPE_WARNING, 0, True), - ("single quotes", SAMPLE, TLS_TYPE_WARNING.replace('"tls"', "'tls'"), 0, True), - ("served copy", SAMPLE.replace("/static/", "/public/"), TLS_TYPE_WARNING, 0, True), - ("unrelated file", "other/app-postgresql.bicep", TLS_TYPE_WARNING, 0, False), - ("other sample", SAMPLE.replace("postgresql", "redis"), TLS_TYPE_WARNING, 0, False), - ("other property", SAMPLE, TLS_TYPE_WARNING.replace('"tls"', '"size"'), 0, False), - ("different diagnostic", SAMPLE, TLS_TYPE_WARNING.replace("BCP036", "BCP035"), 0, False), - ("lint warning", SAMPLE, 'Warning no-unused-vars: Variable "tls" is unused.', 0, False), - ("separate diagnostic lines", SAMPLE, 'Warning BCP036: Other property.\nWarning no-unused-vars: The property "tls" is unused.', 0, False), - ("compiler error", "other.bicep", "Error BCP001: Invalid syntax.", 0, True), - ("nonzero exit", SAMPLE, "", 1, True), - ] - for name, filename, diagnostic, exitcode, expected in fixtures: - with self.subTest(name=name): - self.assertEqual( - validate_bicep.is_validation_failure( - filename, f"{filename}(93,7) : {diagnostic}", exitcode - ), - expected, - ) - - def test_validation_path_rejects_exit_zero_warning(self): - for diagnostic, expected in [(TLS_TYPE_WARNING, 1), (TLS_PROPERTY_WARNING, 1), ("", 0)]: - with self.subTest(diagnostic=diagnostic): - result = subprocess.CompletedProcess( - args=[], returncode=0, - stderr=f"{SAMPLE}(61,10) : {diagnostic}".encode("utf-8") - ) - output = io.StringIO() - with patch.object(validate_bicep.os, "walk", return_value=[ - ("docs/static/samples/demo", [], ["app-postgresql.bicep"]) - ]), patch.object(validate_bicep.subprocess, "run", return_value=result), contextlib.redirect_stdout(output): - self.assertEqual(validate_bicep.main(), expected) - self.assertEqual("Failed:" in output.getvalue(), expected == 1) - - def test_diagnostic_must_belong_to_sample(self): - for filename, expected in [ - (os.path.abspath(SAMPLE), True), - (".\\" + SAMPLE.replace("/", "\\"), True), - ("other/app-postgresql.bicep", False), - (SAMPLE.replace("postgresql", "redis"), False), - ]: - with self.subTest(filename=filename): - diagnostic = f"{filename}(120,9) : {TLS_TYPE_WARNING}" - self.assertEqual( - validate_bicep.is_validation_failure(SAMPLE, diagnostic, 0), - expected, - ) - self.assertFalse(validate_bicep.is_validation_failure(SAMPLE, TLS_TYPE_WARNING, 0)) - - -if __name__ == "__main__": - unittest.main() diff --git a/.github/scripts/validate_bicep.py b/.github/scripts/validate_bicep.py index 032f189b3..a04729099 100644 --- a/.github/scripts/validate_bicep.py +++ b/.github/scripts/validate_bicep.py @@ -1,104 +1,68 @@ import os -import re import subprocess import concurrent.futures num_workers = 5 +# set the bicep binary name based on the OS +if os.name == 'nt': + bicep_bin = 'bicep.exe' + home_path = os.environ['USERPROFILE'] +else: + bicep_bin = 'bicep' + home_path = os.environ['HOME'] + +# set the default Bicep path based on the runner +if os.environ.get("GITHUB_ACTIONS", "false") == "false": + print("Running locally") + bicep_path = os.path.join(home_path, '.rad', 'bin') +else: + print("Running in GitHub Actions", flush=True) + bicep_path = "." + +# allow override through an environment variable +bicep_path = os.environ.get("BICEP_PATH", bicep_path) +bicep_executable = os.path.join(bicep_path, bicep_bin) + +print(f"Using Bicep binary: {bicep_executable}", flush=True) + +files = [] +failures = [] +warnings = [] + +# Walk the directory tree and find all .bicep files +for root, _, filenames in os.walk("."): + for filename in filenames: + if filename.endswith(".bicep"): + files.append(os.path.join(root, filename)) + +def validate_file(f): + print(f"Validating {f}...", flush=True) + + result = subprocess.run( + [bicep_executable, "build", f, "--stdout"], + stderr=subprocess.PIPE, + stdout=subprocess.DEVNULL, + ) + stderr = result.stderr.decode("utf-8") + exitcode = result.returncode + + if "Warning" in stderr: + warnings.append(f) + print(stderr, flush=True) -def is_validation_failure(filename, stderr, exitcode): if exitcode != 0 or "Error" in stderr: - return True + failures.append(f) + print(stderr, flush=True) - sample_path = os.path.normcase(os.path.abspath(filename.replace("\\", "/"))) - if sample_path not in { - os.path.normcase(os.path.abspath(path)) - for path in ( - "docs/static/samples/demo/app-postgresql.bicep", - "docs/public/samples/demo/app-postgresql.bicep", - ) - }: - return False +with concurrent.futures.ThreadPoolExecutor(max_workers=num_workers) as executor: + futures = [executor.submit(validate_file, f) for f in files] +concurrent.futures.wait(futures) - for line in stderr.splitlines(): - diagnostic = re.match( - r"^(.+)\(\d+,\d+\)\s*:\s*(?:Warning|Error)\s+BCP03[67]:" - r"\s+The property ([\"'])tls\2(?:\s|$)", - line, - ) - if diagnostic: - diagnostic_path = os.path.normcase(os.path.abspath( - diagnostic.group(1).replace("\\", "/") - )) - if diagnostic_path == sample_path: - return True - return False +for f in failures: + print(f"Failed: {f}", flush=True) +for f in warnings: + print(f"Warning: {f}", flush=True) -def main(): - # set the bicep binary name based on the OS - if os.name == 'nt': - bicep_bin = 'bicep.exe' - home_path = os.environ['USERPROFILE'] - else: - bicep_bin = 'bicep' - home_path = os.environ['HOME'] - - # set the default Bicep path based on the runner - if os.environ.get("GITHUB_ACTIONS", "false") == "false": - print("Running locally") - bicep_path = os.path.join(home_path, '.rad', 'bin') - else: - print("Running in GitHub Actions", flush=True) - bicep_path = "." - - # allow override through an environment variable - bicep_path = os.environ.get("BICEP_PATH", bicep_path) - bicep_executable = os.path.join(bicep_path, bicep_bin) - - print(f"Using Bicep binary: {bicep_executable}", flush=True) - - files = [] - failures = [] - warnings = [] - - # Walk the directory tree and find all .bicep files - for root, _, filenames in os.walk("."): - for filename in filenames: - if filename.endswith(".bicep"): - files.append(os.path.join(root, filename)) - - def validate_file(f): - print(f"Validating {f}...", flush=True) - - result = subprocess.run( - [bicep_executable, "build", f, "--stdout"], - stderr=subprocess.PIPE, - stdout=subprocess.DEVNULL, - ) - stderr = result.stderr.decode("utf-8") - exitcode = result.returncode - - if "Warning" in stderr: - warnings.append(f) - print(stderr, flush=True) - - if is_validation_failure(f, stderr, exitcode): - failures.append(f) - print(stderr, flush=True) - - with concurrent.futures.ThreadPoolExecutor(max_workers=num_workers) as executor: - futures = [executor.submit(validate_file, f) for f in files] - concurrent.futures.wait(futures) - - for f in failures: - print(f"Failed: {f}", flush=True) - - for f in warnings: - print(f"Warning: {f}", flush=True) - - return len(failures) - - -if __name__ == "__main__": - raise SystemExit(main()) +exit(len(failures)) diff --git a/.github/workflows/validate-bicep.yaml b/.github/workflows/validate-bicep.yaml index 6a888d961..b3324e759 100644 --- a/.github/workflows/validate-bicep.yaml +++ b/.github/workflows/validate-bicep.yaml @@ -50,6 +50,4 @@ jobs: ./bicep --version - name: Verify Bicep files - run: | - python -m unittest discover -s .github/scripts -p test_validate_bicep.py - python ./.github/scripts/validate_bicep.py + run: python ./.github/scripts/validate_bicep.py diff --git a/docs/content/getting-started/_index.md b/docs/content/getting-started/_index.md index ef89ff0ad..b6c524b9c 100644 --- a/docs/content/getting-started/_index.md +++ b/docs/content/getting-started/_index.md @@ -12,7 +12,7 @@ aliases: - /getting-started/first-app/ --- -This guide will show you how to quickly get started with Radius. You will install Radius on a private, disposable Kubernetes cluster, deploy the Radius Demo sample application, and connect it to a PostgreSQL database and an authored password Secret. The database demo explicitly disables TLS; it is not a production configuration. When you finish, explore the hands-on labs for deeper, scenario-based projects. +This guide will show you how to quickly get started with Radius. You will do a basic installation of Radius on a Kubernetes cluster, deploy the Radius Demo sample application, and add a Redis cache connection to it. When you finish, explore the hands-on labs for deeper, scenario-based projects. ## Organization diff --git a/docs/content/getting-started/add-connection/_index.md b/docs/content/getting-started/add-connection/_index.md index aa7ed9712..fc37ea009 100644 --- a/docs/content/getting-started/add-connection/_index.md +++ b/docs/content/getting-started/add-connection/_index.md @@ -8,86 +8,45 @@ weight: 300 The Radius Demo application runs on its own, but most applications depend on other resources such as databases and caches. In this step you will add a PostgreSQL database and an authored Secret to the application, then connect the container to both resources. -> **Private development only:** This Kubernetes demo explicitly sets the database's `tls` policy to `disabled` and the existing PostgreSQL client's `PGSSLMODE` to `disable`. Password authentication remains enabled, but database traffic is unencrypted and credentials and queries can be exposed in transit. Use only a disposable, private development cluster, never a production database. No TLS Secret or client CA is needed for this mode. - -The Resource Type's default remains `required`. Both `required` and `optional` need operator-provided certificates; `optional` is not a certificate-free alternative. The guarded Azure Recipe rejects `disabled`. For secure deployments and migration, see the [PostgreSQL operator guide](https://github.com/radius-project/resource-types-contrib/blob/67b77684cc165f069a392b7d3e9151318feab062/Data/postgreSqlDatabases/README.md#kubernetes-tls-prerequisites). Complete the [compatible-artifact prerequisite]({{< ref "/getting-started/install-radius#prerequisites" >}}) before proceeding; the linked policy is a candidate, not proof of published support. +## Add a PostgreSQL database and connections -The Kubernetes database Recipes have no explicit durable database volume. Pod replacement can lose all demo data. Do not use this tutorial to change the policy of an existing database. +> **Private development only:** This Kubernetes demo sets the database's `tls` policy to `disabled` and the PostgreSQL client's `PGSSLMODE` to `disable`. Password authentication remains enabled, but database traffic is unencrypted and credentials and queries can be exposed in transit. Use only a private, disposable development cluster. -## Add a PostgreSQL database and connections +The Resource Type's default remains `required`. Both `required` and `optional` need operator-provided certificates; `optional` is not a certificate-free alternative. Only explicit `disabled` needs no TLS Secret or client CA. The guarded Azure Recipe rejects `disabled`. See the [PostgreSQL operator guide](https://github.com/radius-project/resource-types-contrib/blob/67b77684cc165f069a392b7d3e9151318feab062/Data/postgreSqlDatabases/README.md#kubernetes-tls-prerequisites) for secure deployments and migration. Complete the [compatible-artifact prerequisite]({{< ref "/getting-started/install-radius#prerequisites" >}}) before proceeding; the linked policy is a candidate, not proof of published support. The `app-postgresql.bicep` definition builds on `app.bicep` by adding a `Radius.Data/postgreSqlDatabases` resource, a `Radius.Security/secrets` resource, a `@secure()` password parameter, and two `connections` entries on the container. It also explicitly disables TLS on the server and client. The highlighted lines are the additions: -{{< rad file="/static/samples/demo/app-postgresql.bicep" embed=true markdownConfig=` {hl_lines=["6-11","32-36","44-51","55-66","68-79"]}` >}} +{{< rad file="/static/samples/demo/app-postgresql.bicep" embed=true markdownConfig=` {hl_lines=["6-8","29-33","41-48","52-63","65-76"]}` >}} -The `postgresql` resource is provisioned by the PostgreSQL Recipe in the Environment's Recipe Pack. The `password` parameter is declared with `@secure()`, which keeps its value out of deployment logs and history and lets you supply the password at deploy time instead of hardcoding it. The application passes that value directly to the database's `password` property. Because the property is `x-radius-sensitive`, Radius encrypts it and redacts it from reads. +The `postgresql` resource is provisioned by the PostgreSQL Recipe in the default Recipe Pack. The `password` parameter is declared with `@secure()`, which keeps its value out of deployment logs and history and lets you supply the password at deploy time instead of hardcoding it. The application passes that value directly to the database's `password` property. Because the property is `x-radius-sensitive`, Radius encrypts it and redacts it from reads. The application also stores the same value under the `password` key of the authored `postgresqlCredentials` Secret instead of relying on database Recipe `result.secrets` for a developer-owned credential. The Secret has the same application and environment as the other resources, so it shares their lifecycle. Its resource name is deliberately different from the `postgresql-${environmentName}-credentials` Kubernetes Secret owned by the PostgreSQL Recipe. -The `postgresql` connection tells Radius that the container depends on the database. Radius provisions the database first, then injects its ordinary connection values as `CONNECTION_POSTGRESQL_HOST`, `CONNECTION_POSTGRESQL_PORT`, `CONNECTION_POSTGRESQL_DATABASE`, `CONNECTION_POSTGRESQL_USERNAME`, and `CONNECTION_POSTGRESQL_TLS`. +The `postgresql` connection tells Radius that the container depends on the database. Radius provisions the database first, then injects its ordinary connection values as `CONNECTION_POSTGRESQL_HOST`, `CONNECTION_POSTGRESQL_PORT`, `CONNECTION_POSTGRESQL_DATABASE`, and `CONNECTION_POSTGRESQL_USERNAME`. The `postgresqlCredentials` connection points directly to the authored Secret. With a compatible Kubernetes Container Recipe, Radius projects its `password` key through a Kubernetes `secretKeyRef` as `CONNECTION_POSTGRESQLCREDENTIALS_PASSWORD`. The secret value remains in the Secret and is not copied into Recipe output or plaintext container configuration. > **Caution:** The Radius Demo displays all `CONNECTION_*` environment variables it receives, including this password, to illustrate connection injection. Do not display secret values in a production application. > -> **Compatibility:** Automatic Secret connection projection requires a compatible Radius runtime and Kubernetes Container Recipe. In older or mixed-version environments, keep an explicit container `env.valueFrom.secretKeyRef` binding instead of changing a working model. This does not remove the separate transport-policy compatibility prerequisite. Azure Container Instances behavior is unchanged. +> **Compatibility:** Automatic Secret connection projection requires a compatible Radius runtime and Kubernetes Container Recipe. In older or mixed-version environments, keep an explicit container `env.valueFrom.secretKeyRef` binding instead of changing a working model. Azure Container Instances behavior is unchanged. ## Redeploy the application -Deploy the updated definition from its published URL using a temporary ARM JSON parameter file. Run the following block in Bash with shell tracing disabled. It creates a private temporary directory and a file readable only by your user, generates a URI-safe hexadecimal password for the demo's existing connection-string construction, and removes the file on success or failure. Only the file path, not the password, appears in the deployment command's arguments. - -```bash -( - set -eu - set +x - umask 077 - credential_dir="$(mktemp -d)" - trap 'rm -f "$credential_dir/parameters.json"; rmdir "$credential_dir"' EXIT - trap 'exit 130' INT - trap 'exit 143' TERM - password="$(openssl rand -hex 16)" - printf '{"$schema":"https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#","contentVersion":"1.0.0.0","parameters":{"password":{"value":"%s"}}}\n' \ - "$password" > "$credential_dir/parameters.json" - unset password - rad deploy https://edge.docs.radapp.io/samples/demo/app-postgresql.bicep \ - --parameters @"$credential_dir/parameters.json" -) -``` - -Do not regenerate the password to retry a deployment against an existing database. For this disposable tutorial, delete only the tutorial application before starting over. +Deploy the updated definition from its published URL. The `--parameters` argument sets the database password, and `$(openssl rand -hex 16)` generates a random value on each deploy. This works in Bash and Zsh; in PowerShell, generate the value separately and pass it in: -Radius creates the database and authored Secret, then updates the container with both connections. Deployment success alone does not establish readiness. Find the database's actual runtime namespace and wait for both current rollouts: +{{< rad-deploy path="samples/demo/app-postgresql.bicep" args=`--parameters password="$(openssl rand -hex 16)"` >}} -```bash -kubectl get deployment --all-namespaces \ - -l radapp.io/application=demo-default,radapp.io/resource=postgresql-default -DB_NAMESPACE='' -kubectl rollout status deployment/postgresql-default -n "$DB_NAMESPACE" --timeout=180s -kubectl rollout status deployment/demo-default -n "$DEMO_NAMESPACE" --timeout=180s -kubectl port-forward svc/demo-default-web -n "$DEMO_NAMESPACE" 3000:3000 -``` - -Use `DEMO_NAMESPACE` from the previous step and replace the database namespace placeholder with the namespace shown for this tutorial's database. Stop if either rollout fails; an old pod can still be serving the previous configuration. - -Open [http://localhost:3000](http://localhost:3000). The Radius Connections section now lists the `postgresql` database connection and the `postgresqlCredentials` Secret connection. The database connection includes `appdb`, `myadmin`, port `5432`, the database's service hostname, and `tls: disabled`. The client separately uses `PGSSLMODE=disable`; the connection's TLS property is not itself a client setting. - -### Verify database-backed behavior - -Create a uniquely titled todo item in the demo, mark it complete, and refresh the page. In another terminal, set the same namespace variables and check the stored row and server transport setting: +Radius creates the database and authored Secret, then updates the container with both connections. Forward a local port to the container and open the application again: ```bash -kubectl exec -n "$DB_NAMESPACE" deployment/postgresql-default -c postgres -- \ - sh -ec 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "SHOW ssl;" -c "SELECT title, done FROM items;"' -kubectl get pods -n "$DEMO_NAMESPACE" -l radapp.io/resource=demo-default -o wide -kubectl exec -n "$DB_NAMESPACE" deployment/postgresql-default -c postgres -- \ - sh -ec 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "SELECT a.client_addr, s.ssl FROM pg_stat_activity a JOIN pg_stat_ssl s USING (pid) WHERE a.datname = current_database() AND a.client_addr IS NOT NULL;"' +kubectl port-forward svc/demo-default-web 3000:3000 ``` -Expect `ssl` to be `off`, your completed item to appear in `items`, and the connection from the current demo pod's IP address to have `ssl = f`. If no demo connection appears, refresh the application and repeat the query; an empty result is not proof. These local administrative queries inspect the demo's connection; a separate plaintext `psql` connection would not prove how the demo connects. Delete the item in the UI and repeat the row query to confirm it was removed. Stop the port forward with `Ctrl+C`. +Open [http://localhost:3000](http://localhost:3000). The Radius Connections section now lists the `postgresql` database connection and the `postgresqlCredentials` Secret connection. -These checks establish database-backed plaintext behavior, not Recipe provenance: an older plaintext Recipe can produce the same results. The compatible-artifact prerequisite still applies. +The database connection reports `tls: disabled`. The client separately uses `PGSSLMODE=disable`; the connection's TLS property does not configure the client by itself. ## View the connections in the Dashboard @@ -103,16 +62,14 @@ To learn more about modeling dependencies between resources, see [How to model a ## Clean up -Delete only the Radius Demo application created in this tutorial. Because the database and authored Secret belong to this application, Radius deletes them with the container: +Delete the Radius Demo application. Because the database and authored Secret belong to this application, Radius deletes them with the container: ```bash rad application delete demo-default --preview ``` -The deployment block removes its temporary credential file even if deployment fails. If your shell was forcibly terminated before its exit trap ran, remove only the `parameters.json` file and temporary directory created by that invocation. Do not delete other applications, namespaces, or operator-owned Secrets. - -Only if this is a disposable cluster dedicated to this tutorial, optionally uninstall Radius and remove all of its data. **Do not purge a shared installation:** +Optionally, uninstall Radius and remove all of its data: ```bash rad uninstall kubernetes --purge diff --git a/docs/content/getting-started/deploy-demo/_index.md b/docs/content/getting-started/deploy-demo/_index.md index d83210ca1..5f99b2e9f 100644 --- a/docs/content/getting-started/deploy-demo/_index.md +++ b/docs/content/getting-started/deploy-demo/_index.md @@ -20,24 +20,12 @@ Deploy the Radius Demo using `rad deploy`: {{< rad-deploy path="samples/demo/app.bicep" >}} -Radius deploys the application to the `default` Environment. The `Radius.Compute/containers` resource is provisioned by a Recipe that creates a Kubernetes Deployment and a `ClusterIP` Service. Find the actual runtime namespace rather than assuming it matches the Environment's configured namespace: - -```bash -kubectl get deployment --all-namespaces \ - -l radapp.io/application=demo-default,radapp.io/resource=demo-default -``` - -Set `DEMO_NAMESPACE` to the namespace shown for this tutorial's deployment. If more than one deployment matches, identify the one in your tutorial Environment before continuing: - -```bash -DEMO_NAMESPACE='' -kubectl rollout status deployment/demo-default -n "$DEMO_NAMESPACE" --timeout=180s -``` +Radius deploys the application to the `default` Environment, which is configured to use the `default` namespace of your Kubernetes cluster. The `Radius.Compute/containers` resource is provisioned by a Recipe that creates a Kubernetes Deployment and a `ClusterIP` Service. View the deployed resources with `kubectl`: ```bash -kubectl get pod,deployment,service -n "$DEMO_NAMESPACE" -l radapp.io/application=demo-default +kubectl get pod,deployment,service ``` Example output: @@ -51,6 +39,7 @@ deployment.apps/demo-default 1/1 1 1 3m18s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/demo-default-web ClusterIP 10.96.177.228 3000/TCP 3m18s +service/kubernetes ClusterIP 10.96.0.1 443/TCP 4m10s ``` ## Browse the Radius Demo application @@ -58,10 +47,10 @@ service/demo-default-web ClusterIP 10.96.177.228 3000/TCP Use `kubectl` to forward a local port to the container's Kubernetes Service: ```bash -kubectl port-forward svc/demo-default-web -n "$DEMO_NAMESPACE" 3000:3000 +kubectl port-forward svc/demo-default-web 3000:3000 ``` -Open [http://localhost:3000](http://localhost:3000) in your browser. The Radius Connections section says "No connections defined"; you will add PostgreSQL and password Secret connections in the next step. Press `Ctrl+C` to stop the port forward. +Open [http://localhost:3000](http://localhost:3000) in your browser. The Radius Connections section says "No connections defined"; you will add a Redis cache connection in the next step. Press `Ctrl+C` to stop the port forward. ## Browse the Radius Dashboard @@ -98,6 +87,6 @@ You have deployed your first application with Radius. For a deeper walkthrough o ## Next steps -In part three of this guide, you will add a PostgreSQL database and an authored password Secret, then connect both to the `demo-default` container. +In part three of this guide, you will add a Redis cache resource and a connection to the `demo-default` container. {{< button text="Next step: Add a connection" page="getting-started/add-connection" >}} diff --git a/docs/content/getting-started/install-radius/_index.md b/docs/content/getting-started/install-radius/_index.md index 595d3ac91..2c6cd1579 100644 --- a/docs/content/getting-started/install-radius/_index.md +++ b/docs/content/getting-started/install-radius/_index.md @@ -10,11 +10,11 @@ In part one, you will install Radius on an existing Kubernetes cluster. ## Prerequisites -Use a **private, disposable Kubernetes cluster** with no existing `demo-default` application. To install Radius, your user must have the cluster-admin role. Radius }}">supports AKS, EKS, k3d, and kind clusters. Running a local cluster with k3d or kind is recommended. Do not expose the demo or database publicly: step three deliberately uses unencrypted PostgreSQL connections. +For this guide you only need a **Kubernetes cluster**. To install Radius, your user must have the cluster-admin role. Radius }}">supports AKS, EKS, k3d, and kind clusters. Running a local cluster with k3d or kind is recommended. -The commands use Bash on Linux or macOS, `kubectl`, and OpenSSL. On Windows, use WSL with a Linux filesystem for the protected temporary password file; Git Bash on a Windows filesystem does not provide the same Unix permission guarantees. Use a compatible Radius CLI/runtime, Bicep compiler and extension, registered PostgreSQL schema, and Kubernetes Recipe Pack. The schema must accept `tls: 'disabled'`, the PostgreSQL Recipe must implement that policy, and the Container and Secret Recipes must support password projection through `secretKeyRef`. Secret support alone does not establish TLS-policy compatibility. Check the Environment's assigned Recipe Pack and any overrides, not just the CLI version. +> **PostgreSQL TLS prerequisite:** The database step explicitly disables TLS. Use a private, disposable Kubernetes cluster; do not expose the demo or database publicly. The Radius schema and Bicep extension must accept `tls: 'disabled'`, and the Environment's PostgreSQL Recipe must implement that policy. Check the assigned Recipe Pack and overrides, not just the CLI version. -> **Edge adoption gate:** This tutorial depends on the [PostgreSQL transport-policy change](https://github.com/radius-project/resource-types-contrib/pull/394) and the matching samples update. Do not run the database step until compatible schemas, extensions, and recipes are publicly available and the sample has landed on `edge`. This tutorial targets compatible Radius edge artifacts, not an upgrade to a stable Radius release or changes to `v0.61`. However, Radius currently pins `Radius.Data` to a stable namespace release. Advancing that pin requires a separately approved upstream namespace release containing the fix, followed by Radius adoption and publication of compatible artifacts. Publishing an edge Recipe alone does not update the schema. This tutorial does not authorize release or adoption. An older plaintext Recipe, a schema warning, or an unpublished candidate is not a supported substitute. See the [PostgreSQL operator guide](https://github.com/radius-project/resource-types-contrib/blob/67b77684cc165f069a392b7d3e9151318feab062/Data/postgreSqlDatabases/README.md#adopting-the-change) for the candidate policy and adoption requirements. +> **Edge adoption gate:** This tutorial depends on the [PostgreSQL transport-policy change](https://github.com/radius-project/resource-types-contrib/pull/394) and the matching samples update. Do not run the database step until compatible artifacts are publicly available and the sample has landed on `edge`. Radius currently pins `Radius.Data` to a stable namespace release. Adoption requires a separately approved upstream namespace release containing the fix, followed by Radius adoption and publication of compatible artifacts. Publishing an edge Recipe alone does not update the schema. This tutorial targets Radius edge, not changes to `v0.61`, and does not authorize release or adoption. See the [candidate operator guide](https://github.com/radius-project/resource-types-contrib/blob/67b77684cc165f069a392b7d3e9151318feab062/Data/postgreSqlDatabases/README.md#adopting-the-change). ## Install the Radius CLI @@ -29,7 +29,7 @@ mkdir radius cd radius ``` -Ensure your disposable cluster is set as your current context with `kubectl config current-context`. If the context needs updating, change it with `kubectl config use-context `. Then install Radius with the `rad initialize` command: +Ensure your cluster is set as your current context with `kubectl config current-context`. If the context needs updating, change it with `kubectl config set-context `. Then install Radius with the `rad initialize` command: ```bash diff --git a/docs/static/samples/demo/app-postgresql.bicep b/docs/static/samples/demo/app-postgresql.bicep index 75b3cfcdc..0178855a6 100644 --- a/docs/static/samples/demo/app-postgresql.bicep +++ b/docs/static/samples/demo/app-postgresql.bicep @@ -3,13 +3,10 @@ extension radius @description('The Radius Environment ID. Injected automatically by the rad CLI.') param environment string -@description('The administrator password for the PostgreSQL database. Supply a URI-safe random hexadecimal value in a protected parameter file.') +@description('The administrator password for the PostgreSQL database. Pass via the CLI, e.g. -p password=$(openssl rand -hex 16).') @secure() param password string -// Defaults to the published image; CI overrides this to test the freshly built image. -param image string = 'ghcr.io/radius-project/samples/demo:latest' - // Environment name derived from the Environment ID, used to keep resource names // unique per environment (e.g. dev/test/prod) within the same resource group. var environmentName = last(split(environment, '/')) @@ -28,7 +25,7 @@ resource demoContainer 'Radius.Compute/containers@2025-08-01-preview' = { application: demoApp.id containers: { web: { - image: image + image: 'ghcr.io/radius-project/samples/demo:latest' env: { PGSSLMODE: { value: 'disable' From be36c5a048f416b23c2ec0bd7dead2b5642c481b Mon Sep 17 00:00:00 2001 From: Nell Shamrell-Harrington Date: Wed, 21 Oct 2026 15:47:08 -0700 Subject: [PATCH 10/12] Use Azure Storage for the direct AVM recipe example Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Nell Shamrell-Harrington --- .../management/existing-recipes/_index.md | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/docs/content/management/existing-recipes/_index.md b/docs/content/management/existing-recipes/_index.md index cefd7c5b0..78002ddff 100644 --- a/docs/content/management/existing-recipes/_index.md +++ b/docs/content/management/existing-recipes/_index.md @@ -62,27 +62,39 @@ git::https://github.com/my-org/modules.git//rds?ref=v6.1.0 {{% codetab %}} -Bicep modules are referenced by their OCI registry path, with the version in the image tag. For example, reference an Azure Verified Module from the Microsoft Container Registry: +Bicep modules are referenced by their OCI registry path, with the version in the image tag. This example uses the Azure Storage Account AVM to provision a blob container for [`Radius.Storage/objectStorage`]({{< ref "/reference/resources/radius.storage/2025-08-01-preview/objectstorage" >}}): ```bicep extension radius resource dataRecipes 'Radius.Core/recipePacks@2025-08-01-preview' = { - name: 'azure-data-recipes' + name: 'azure-storage-recipes' properties: { recipes: { - 'Radius.Data/postgreSqlDatabases': { + 'Radius.Storage/objectStorage': { kind: 'bicep' - source: 'br:mcr.microsoft.com/bicep/avm/res/db-for-postgre-sql/flexible-server:0.4.0' + source: 'br:mcr.microsoft.com/bicep/avm/res/storage/storage-account:0.32.1' parameters: { - name: 'pg-{{context.resource.name}}' + name: '{{context.resource.name}}' location: 'eastus2' - skuName: 'Standard_B1ms' + kind: 'StorageV2' + skuName: 'Standard_LRS' + allowBlobPublicAccess: false + blobServices: { + containers: [ + { + name: '{{context.resource.properties.containerName}}' + } + ] + } } outputs: { - host: 'fqdn' - port: 'port' - database: 'name' + endpoint: 'primaryBlobEndpoint' + accountName: 'name' + secrets: { + accountKey: 'primaryAccessKey' + connectionString: 'primaryConnectionString' + } } } } @@ -90,14 +102,14 @@ resource dataRecipes 'Radius.Core/recipePacks@2025-08-01-preview' = { } ``` +The resource's name becomes the Azure storage account name, so choose a globally unique name with 3 to 24 lowercase letters or digits. The `containerName` property selects the blob container. The nested `secrets` mapping sends the account key and connection string to Radius-managed secret storage rather than ordinary resource properties. The module retains its default network access restrictions; configure access for your application separately. + {{% /codetab %}} {{< /tabs >}} The `parameters` and `outputs` names come from the module you reference, so consult the module's own documentation for its input variables and outputs. Radius passes only the parameters you map; every other input uses the module's own default. -> **PostgreSQL transport policy:** The custom Azure example above calls the AVM directly. It bypasses the guarded PostgreSQL Recipe's transport-policy validation, including its rejection of `tls: 'disabled'`; it does not map the resource's `tls` request. Do not assume a custom direct-AVM Recipe enforces that contract. See the [PostgreSQL operator guide](https://github.com/radius-project/resource-types-contrib/blob/67b77684cc165f069a392b7d3e9151318feab062/Data/postgreSqlDatabases/README.md#using-the-resource-type) for the candidate guarded Recipe and adoption requirements. - ## Resolve developer inputs with context expressions Any `parameters` value can include a `{{context.*}}` expression. When Radius runs the recipe, it replaces the expression with a real value, such as a property the developer set on the resource, the resource's name, or a detail about the Environment or cloud provider. The [recipe context reference]({{< ref "/reference/recipes/context" >}}) lists every value you can read from `context`. For example: From ecb2af9dedb3e5601781a1c45c4a898048b23acf Mon Sep 17 00:00:00 2001 From: Nell Shamrell-Harrington Date: Wed, 21 Oct 2026 15:57:12 -0700 Subject: [PATCH 11/12] Clarify guarded PostgreSQL recipe policy in generated reference Regenerate from resource-types-contrib 56d20315676cbee42a5939d105bb5e21096d0780 with Radius producer 59b3954f3af5702dc2a4057ac31292dbed40e35d and the existing docs importer. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Nell Shamrell-Harrington --- .../radius.data/2025-08-01-preview/postgresqldatabases/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/reference/resources/radius.data/2025-08-01-preview/postgresqldatabases/index.md b/docs/content/reference/resources/radius.data/2025-08-01-preview/postgresqldatabases/index.md index 291175613..e8c63e3ad 100644 --- a/docs/content/reference/resources/radius.data/2025-08-01-preview/postgresqldatabases/index.md +++ b/docs/content/reference/resources/radius.data/2025-08-01-preview/postgresqldatabases/index.md @@ -73,7 +73,7 @@ The connection automatically injects environment variables into the container fo | `password` | string | (Required) The administrator password for the PostgreSQL database. Marked `x-radius-sensitive`: Radius encrypts it at rest, redacts it on reads, and exposes it decrypted only to the recipe as `{{context.resource.properties.password}}`. | | `port` | integer | (Optional) The TCP port used to connect to the database. Defaults to `5432`, the standard PostgreSQL port that every Recipe in this repository provisions and the port PostgreSQL flexible server is fixed to. A Recipe that provisions the database on a different port reports the real port as an output, which overwrites this value once the deployment finishes. Setting it in an application definition changes only the value reported to connected containers, never the port the server listens on, so leave it unset. | | `size` | string | (Optional) The size of the PostgreSQL database. Defaults to `S` if not provided.
Allowed values: `L`, `M`, `S`. | -| `tls` | string | (Optional) Transport policy, projected as CONNECTION_POSTGRESQL_TLS. Defaults to required (TLS only). On Kubernetes, optional supports TLS and plaintext; both modes require an operator-owned TLS Secret. Explicit disabled is Kubernetes-only: plaintext only, with no certificate requirement. Use optional/disabled only on isolated private networks. Configure trusted CA certificates and hostname verification separately. Azure rejects disabled; operator overrides retain precedence for required/optional.
Allowed values: `disabled`, `optional`, `required`. | +| `tls` | string | (Optional) Transport policy, projected as CONNECTION_POSTGRESQL_TLS. Defaults to required (TLS only). On Kubernetes, optional supports TLS and plaintext; both modes require an operator-owned TLS Secret. Explicit disabled is Kubernetes-only: plaintext only, with no certificate requirement. Use optional/disabled only on isolated private networks. Configure trusted CA certificates and hostname verification separately. The guarded Azure PostgreSQL recipe rejects disabled; direct AVM calls bypass this validation. Azure operator overrides retain precedence for required/optional, so the projected value reports the requested policy, not the effective server setting.
Allowed values: `disabled`, `optional`, `required`. | | `username` | string | (Required) The administrator username for the PostgreSQL database. Provided directly on the resource and passed to the recipe as `{{context.resource.properties.username}}`. | ## Object Properties From 4830ca0929d0c4de661ceaaa9c229833f9cf3b5e Mon Sep 17 00:00:00 2001 From: Nell Shamrell-Harrington Date: Wed, 21 Oct 2026 15:58:12 -0700 Subject: [PATCH 12/12] Simplify PostgreSQL tutorial TLS guidance Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Nell Shamrell-Harrington --- docs/content/getting-started/add-connection/_index.md | 6 +----- docs/content/getting-started/install-radius/_index.md | 4 +--- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/docs/content/getting-started/add-connection/_index.md b/docs/content/getting-started/add-connection/_index.md index fc37ea009..342220212 100644 --- a/docs/content/getting-started/add-connection/_index.md +++ b/docs/content/getting-started/add-connection/_index.md @@ -12,9 +12,7 @@ The Radius Demo application runs on its own, but most applications depend on oth > **Private development only:** This Kubernetes demo sets the database's `tls` policy to `disabled` and the PostgreSQL client's `PGSSLMODE` to `disable`. Password authentication remains enabled, but database traffic is unencrypted and credentials and queries can be exposed in transit. Use only a private, disposable development cluster. -The Resource Type's default remains `required`. Both `required` and `optional` need operator-provided certificates; `optional` is not a certificate-free alternative. Only explicit `disabled` needs no TLS Secret or client CA. The guarded Azure Recipe rejects `disabled`. See the [PostgreSQL operator guide](https://github.com/radius-project/resource-types-contrib/blob/67b77684cc165f069a392b7d3e9151318feab062/Data/postgreSqlDatabases/README.md#kubernetes-tls-prerequisites) for secure deployments and migration. Complete the [compatible-artifact prerequisite]({{< ref "/getting-started/install-radius#prerequisites" >}}) before proceeding; the linked policy is a candidate, not proof of published support. - -The `app-postgresql.bicep` definition builds on `app.bicep` by adding a `Radius.Data/postgreSqlDatabases` resource, a `Radius.Security/secrets` resource, a `@secure()` password parameter, and two `connections` entries on the container. It also explicitly disables TLS on the server and client. The highlighted lines are the additions: +The `app-postgresql.bicep` definition builds on `app.bicep` by adding a `Radius.Data/postgreSqlDatabases` resource, a `Radius.Security/secrets` resource, a `@secure()` password parameter, and two `connections` entries on the container. The highlighted lines are the additions: @@ -46,8 +44,6 @@ kubectl port-forward svc/demo-default-web 3000:3000 Open [http://localhost:3000](http://localhost:3000). The Radius Connections section now lists the `postgresql` database connection and the `postgresqlCredentials` Secret connection. -The database connection reports `tls: disabled`. The client separately uses `PGSSLMODE=disable`; the connection's TLS property does not configure the client by itself. - ## View the connections in the Dashboard Start port forwarding for the Dashboard: diff --git a/docs/content/getting-started/install-radius/_index.md b/docs/content/getting-started/install-radius/_index.md index 2c6cd1579..2da770253 100644 --- a/docs/content/getting-started/install-radius/_index.md +++ b/docs/content/getting-started/install-radius/_index.md @@ -12,9 +12,7 @@ In part one, you will install Radius on an existing Kubernetes cluster. For this guide you only need a **Kubernetes cluster**. To install Radius, your user must have the cluster-admin role. Radius }}">supports AKS, EKS, k3d, and kind clusters. Running a local cluster with k3d or kind is recommended. -> **PostgreSQL TLS prerequisite:** The database step explicitly disables TLS. Use a private, disposable Kubernetes cluster; do not expose the demo or database publicly. The Radius schema and Bicep extension must accept `tls: 'disabled'`, and the Environment's PostgreSQL Recipe must implement that policy. Check the assigned Recipe Pack and overrides, not just the CLI version. - -> **Edge adoption gate:** This tutorial depends on the [PostgreSQL transport-policy change](https://github.com/radius-project/resource-types-contrib/pull/394) and the matching samples update. Do not run the database step until compatible artifacts are publicly available and the sample has landed on `edge`. Radius currently pins `Radius.Data` to a stable namespace release. Adoption requires a separately approved upstream namespace release containing the fix, followed by Radius adoption and publication of compatible artifacts. Publishing an edge Recipe alone does not update the schema. This tutorial targets Radius edge, not changes to `v0.61`, and does not authorize release or adoption. See the [candidate operator guide](https://github.com/radius-project/resource-types-contrib/blob/67b77684cc165f069a392b7d3e9151318feab062/Data/postgreSqlDatabases/README.md#adopting-the-change). +> **PostgreSQL TLS prerequisite:** The database step explicitly disables TLS. Use a private, disposable Kubernetes cluster; do not expose the demo or database publicly. ## Install the Radius CLI