Skip to content

Djong1 adaptive apps microhack - #487

Open
Dylan de Jong (djong1) wants to merge 41 commits into
microsoft:feature-adaptive-appsfrom
djong1:djong1-adaptive-apps-microhack
Open

Djong1 adaptive apps microhack#487
Dylan de Jong (djong1) wants to merge 41 commits into
microsoft:feature-adaptive-appsfrom
djong1:djong1-adaptive-apps-microhack

Conversation

@djong1

Copy link
Copy Markdown

Issue #428.

This pull request introduces the Adaptive Apps MicroHack content and supporting developer environment for the Azure App Innovation scenario.

Created and validated challenges:

Challenge 00: universal prerequisite

Complete Challenge 00 before starting a challenge in any bucket, regardless of
which audience path you follow.

Bucket 1: Infrastructure setup

Audience: platform engineer. Prepare the target platforms and deploy the
Radius control plane.

Note

Challenges 01 - 03 can be pre-provisioned automatically through the Hack
Console. Work through them manually to learn how the platform is built, or
use the provisioned environment and continue at Challenge 04 or 05.

Bucket 2: Exploring Radius

Audience: platform engineer (Challenge 04 is also the optional entry point
for application developers). Define portable platform capabilities and
implement them with recipes.

Bucket 3: Portable apps across platforms

Audience: application developer (Challenge 05 also closes out the platform
engineer path). Deploy the application across environments and adapt its
identity, communication, and AI capabilities.

Bucket 4: Advanced challenges

Audience: application developer. Bring the application model into the
developer inner loop and apply the Adaptive Apps approach to an existing
application.

Additional documentation

Adaptive Apps

Dylan de Jong (djong1) and others added 30 commits August 19, 2026 13:20
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Validated challenges 00-04 end to end from the devcontainer against a live
Azure subscription, on both the AKS and K3s VM paths. Five blocking script
bugs were found and fixed, plus the prerequisite and capacity gaps that made
first-run failures hard to diagnose.

Blocking fixes:

- prepare-k3s-azure-vm.sh: "az network bastion wait --custom
  provisioningState=='Succeeded'" never matches in the bastion extension, so
  every run burned the full 30-minute --timeout before continuing. Use the
  built-in --created condition, which returns as soon as Bastion is ready.

- prepare-k3s-azure-vm.sh: disconnect only stopped the az Bash wrapper.
  /usr/bin/az runs its Python entry point as a child instead of exec'ing it,
  so the process that owns localhost:16443 survived. disconnect reported a
  false success and every later connect aborted with "Local port already in
  use", breaking the reconnect workflow used by challenges 02-08. Match both
  processes by full signature, stop all of them, and reclaim orphans in
  ensure_tunnel.

- prepare-k3s-azure-vm.sh: register the k3s-azure-vm context in the default
  kubeconfig during provision and connect. rad 0.60.0 honours KUBECONFIG for
  the Helm phase but reads ~/.kube/config when configuring the Contour
  gateway, so deploy-radius-k3s.sh failed with "context was not found". The
  active context is left unchanged.

- deploy-radius-aks.sh and deploy-radius-k3s.sh: the health check queried
  the CRD applications.radapp.io, which Radius 0.60.0 does not install. Both
  scripts exited 1 despite a healthy control plane. Check the CRDs Radius
  actually creates: recipes, deploymenttemplates, and deploymentresources.

- configure-resource-types-aks.sh and configure-resource-types-k3s.sh:
  "kubectl rollout status deployment --all" is not valid, since kubectl
  rollout status takes a single object. Both scripts failed with "unknown
  flag: --all" right after the Helm install. Iterate over the objects.

VM capacity handling:

- prepare-aks.sh and prepare-k3s-azure-vm.sh resolve the node or VM size
  before creating anything, using one az vm list-skus call per run. Zone-only
  restrictions are ignored because the deployment is regional. If allocation
  still fails on capacity or quota, the failed resource is removed and the
  next candidate is tried; authorization failures are not retried. Override
  the ordered list with AKS_NODE_VM_SIZE_CANDIDATES or
  K3S_VM_SIZE_CANDIDATES.

Documentation:

- challenge-00.md: complete zero-experience host setup. WSL 2 enablement,
  three supported container runtimes including a Docker Desktop licence note
  and a Docker Engine in WSL 2 alternative, the Dev Containers extension ID,
  host Git, runtime sizing, arm64 support, and device-code sign-in.
- challenge-00.md: resource provider registration and a public IP preflight,
  with the repair for the SubscriptionNotRegisteredForFeature
  AllowBringYourOwnPublicIpAddress error seen on a fresh subscription.
- challenge-00.md: regional VM capacity and quota preflight commands.
- solution-00.md, solution-01.md: runtime options, arm64 support, tunnel
  lifecycle, kubeconfig registration, and size fallback behaviour.
- solution-04.md: state that challenge 03 must run first, because the
  environment templates register recipes for the types it creates.
- challenge-02.md, solution-02.md, solution-03.md: correct the CRD and
  rollout commands shown to participants.
- post-create.sh: "local readonly VAR=" does not make a variable readonly
  (shellcheck SC2316).
- .gitignore: ignore the generated devcontainer-lock.json.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fix Adaptive Apps platform deployment for AKS and K3s (#1, #2)
The private K3s VM has no inbound exposure but still needs outbound access
to download the K3s installer and pull images. It relied on implicit default
outbound access, which Azure is retiring: for API versions released after
2026-03-31, new virtual networks default to private subnets with no outbound
path. The failure therefore depends on the Azure CLI version that creates the
subnet rather than on the calendar date, and would have appeared as a silent
regression on a future CLI.

Measured on 2026-08-21 against a live subscription:

- a subnet created by az 2.89.1 has defaultOutboundAccess=null, so the
  platform default still applies and outbound works
- a private subnet blocks the installer: curl https://get.k3s.io fails
- setting defaultOutboundAccess=true does not take effect on a running VM;
  outbound is restored only after a deallocate and start
- a NAT gateway restores outbound on a private subnet within seconds and
  needs no restart

prepare-k3s-azure-vm.sh now sets the outbound posture explicitly instead of
inheriting the platform default:

- by default it creates snet-k3s with defaultOutboundAccess=true, which keeps
  the current zero-cost topology working under any CLI version
- K3S_ENABLE_NAT_GATEWAY=true leaves the subnet private and attaches
  natgw-adaptive-apps with a Standard public IP, which is required when policy
  forbids default outbound access and is the durable long-term option
- an existing private subnet without a NAT gateway is reopened, and the VM is
  deallocated and started so the change applies
- the guest installer reports a dedicated no-outbound marker, so the script
  fails with actionable guidance instead of a generic K3s error

Documented the postures, their cost, and NAT gateway cleanup in
docs/prepare-k3s.md and walkthrough/challenge-01/solution-01.md.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Handle Azure default outbound access retirement for the K3s VM
Default outbound access is itself scheduled for retirement, so relying on it
only defers the problem. The NAT gateway is now the default: it is explicit,
survives the retirement, and gives a deterministic egress IP that a firewall
can allowlist. K3S_ENABLE_NAT_GATEWAY=false keeps the previous zero-cost
posture where policy still permits default outbound access.

The K3s subnet is now kept explicitly private when the NAT gateway is used.
Azure can still assign a platform-owned fallback outbound IP to a nonprivate
subnet even when a NAT gateway is attached, which would defeat the
deterministic egress address. Reconciling an existing nonprivate subnet also
flags the VM for a deallocate and start, because removing an already-assigned
fallback IP only takes effect after that cycle.

Verified against a live subscription:

- default path creates the subnet private, attaches the NAT gateway, and
  needs no restart
- re-running is idempotent
- a nonprivate subnet is converted to private and flagged for restart
- opting out with an existing NAT gateway leaves it in place rather than
  removing an in-use egress path
- egress resolves to the NAT public IP

Updated the prerequisites, walkthrough, and K3s reference for the new default,
including NAT gateway permissions, the second Standard public IP, and cleanup.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Make the NAT gateway the default outbound path for the K3s VM
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Initialize Azure PostgreSQL for the trading app
Removed instructions for upgrading an existing workshop and redeploying Challenge 05.
`az network public-ip show --query "[ipAddress,publicIPAllocationMethod,sku.name]"
--output tsv` returns a JMESPath multi-select list, which Azure CLI renders as one
value per line rather than as tab-separated columns. `read -r ip allocation sku`
therefore consumed only the first line, leaving `allocation` and `sku` empty, and a
valid Standard/Static AKS outbound IP was rejected with "must be a Standard,
statically allocated public IP".

The failure aborted configure-recipes.sh after the recipes were published to ACR but
before `rad deploy iac/aks-env.bicep`, so env-azure-prod was left with no recipes and
Challenge 05 failed with RecipeNotFoundFailure for every portable resource type.

- Read the egress IP fields with mapfile (one value per row) and strip CR so CRLF
  `az` output cannot reintroduce the same class of failure. Apply the same handling
  to the outboundType and outbound resource ID lookups.
- Include the observed sku and allocation in the rejection message so a future
  failure is self-diagnosing.
- Replace the informational `rad recipe list` with an assert_recipes helper that
  fails the run when an expected type has no default recipe. `rad recipe list`
  prints an empty table and exits 0, so a broken run previously reported success.
- Apply the identical parsing fix to the manual tutorial in solution-04.md, which
  carried the same defect.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Azure rejects tag names containing '/', so the 'radapp.io/environment',
'radapp.io/resource' and 'radapp.io/application' tags applied to the
PostgreSQL flexible server and the AVM SQL server caused challenge 05 to
fail with InvalidTagNameCharacters when the postgreSqlDatabases recipe ran.

Switch those Azure tag names to a hyphenated form. Kubernetes label keys
legitimately allow '/', so the labels in postgres-kubernetes.bicep and the
Kubernetes resources inside postgres-azure-flex.bicep are left unchanged.

Because the recipes are published to ACR under immutable version tags,
correcting the source alone is not enough. Bump the default
WORKSHOP_RECIPE_VERSION to 1.0.1 so the corrected recipes are republished
and re-registered, and stop sql-server from hardcoding :1.0.0 in the
publish and register paths so it can never drift from the other recipes.

Add an assertion to validate-postgres-recipes.sh so this class of defect
cannot return.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Fix AKS egress IP parsing that silently skipped Challenge 04 recipe registration
Fix InvalidTagNameCharacters on Azure resources in Radius recipes
Azure Database for PostgreSQL Flexible Server serializes control-plane
operations per server, but Bicep infers dependencies only from `parent:`.
The database, both server configurations and the firewall rules therefore
depended solely on the server, so ARM fanned all of them out in parallel as
soon as it was created and the losers failed with:

  ServerIsBusy: Cannot complete operation while server 'pg-...' is busy
  processing another operation. Try again later.

Which operation loses the race is non-deterministic, so challenge 05 failed
intermittently and a retry could appear to succeed.

Chain the children explicitly - database, then require_secure_transport,
then ssl_min_protocol_version, then the firewall rules with @batchsize(1)
so the loop is applied one rule at a time. This also fixes a latent bug: the
schema initializer job never depended on the database resource, and now does
so transitively.

Bump WORKSHOP_RECIPE_VERSION to 1.0.2 so the corrected recipe is republished
and re-registered, and assert the serialization in validate-postgres-recipes.sh.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Serialize PostgreSQL flexible server child resources
Radius surfaces a recipe's `values` map as the resource's properties. The three
database recipes returned `secrets` as a sibling of `values` instead of nested
inside it, which is not part of that contract - so dynamic-rp discarded the
entire output payload. Deploying the application then failed with:

  InvalidTemplate: The language expression property 'secrets' doesn't exist,
  available properties are 'application, environment, provisioningState,
  recipe, size, status'.

Note that host, port, database and username were missing too, not just the
password: the unexpected sibling key cost every output, not only the secret.

Confirmed against a working control in the same deployment: backend-identity,
provisioned by an upstream recipe, surfaced all seven of its read-only outputs,
and the registered postgreSqlDatabases schema already declared host, port,
database, username and secrets.password correctly. The two ai-model recipes in
this repository already nested secrets inside values, so the three database
recipes were the outliers.

Move `secrets` inside `values` in postgres-azure-flex, postgres-kubernetes and
sql-server, matching both the upstream recipes and the ai-model recipes here.

Bump WORKSHOP_RECIPE_VERSION to 1.0.3 so the corrected recipes are republished
and re-registered, and assert the output shape in validate-postgres-recipes.sh.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Nest recipe secrets inside the values map
The Azure PostgreSQL recipe returned every value the postgreSqlDatabases type
declares, yet trading-db came back with only application, environment,
provisioningState, recipe, size and status. app.bicep then failed with
InvalidTemplate: the language expression property 'secrets' doesn't exist.

The result output could not be evaluated. Bicep compiles
map(aksFirewallRules, rule => rule.id) to references('aksFirewallRules', 'full'),
which the Radius deployment engine resolves to the template's resource metadata.
That object exposes resourceId, not id, so evaluation threw:

  Azure.Deployments.Core.Exceptions.ExpressionException: The language expression
  property 'id' doesn't exist, available properties are '..., resourceId,
  symbolicName, ...'

bicep-de logs that only at WARN. The deployment still reported Succeeded, but it
returned no outputs, and prepareRecipeResponse skips the whole block when the
outputs map is empty, so Radius stored none of the recipe's values without
surfacing an error. The missing Kubernetes entries in status.outputResources were
the same failure: non-ARM resources arrive only via result.resources.

Precompute the firewall rule IDs with resourceId() and build the Kubernetes IDs
from the compile-time name variables, leaving the server FQDN as the only runtime
reference in the result. Verified against the compiled template: references() is
gone entirely and the values map still carries host, port, database, username and
secrets.

Bump the recipe version so the corrected template is republished, since ACR tags
are immutable, and guard the result block in validate-postgres-recipes.sh.

Also correct the rationale on the nested-secrets guard. Nesting is required
because a schema-declared secrets property is populated from the values map,
whereas a top-level sibling is materialized into a managed Radius.Security/secrets
resource that leaves only properties.secrets.name behind.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Keep the PostgreSQL recipe result evaluable so Radius records its values
Dylan de Jong (djong1) and others added 10 commits September 1, 2026 18:31
`rad deploy iac/app.bicep` failed at the backend container with:

    InvalidTemplate: Unable to process template language expressions for
    resource 'Applications.Core/containers/backend' ... 'The language
    expression property 'secrets' doesn't exist, available properties are
    'application, database, environment, host, port, provisioningState,
    recipe, size, status, username'.'

The preceding fix made the recipe's `values` land correctly (host, port,
database and username are now present), which isolated the remaining failure
to `secrets` alone.

Radius treats `secrets` as a framework-owned property. It is listed in
`pkg/resourceutil.BasicProperties`, which is documented as excluded from
generic recipe-output copying and from connection-based environment variable
injection. That means neither shape of recipe output can populate
`properties.secrets.password`:

  * nested inside `values`, the map becomes a computed value and is then
    dropped by `addComputedValuesToResourceProperties`, silently;
  * as a top-level sibling of `values`, `materializeRecipeSecrets` creates a
    managed `Radius.Security/secrets` resource and writes back only
    `properties.secrets.name`, because secret values are never persisted on
    the owner resource.

The second route is additionally unavailable here: consumers bind managed
secrets through the reserved `secrets.name` sub-property, and the pinned
upstream `postgreSqlDatabases` type declares only `secrets.password`.

Bind the Kubernetes Secret directly instead. The recipes already create a
Secret holding the password for their own schema-initializer Job, so give it
the stable name `<resource-name>-credentials` and have the backend read it
with `valueFrom.secretRef`. A container `secretRef.source` that is not an
`Applications.Core/secretStores` resource ID is passed through verbatim as a
`secretKeyRef.name`, so a plain Secret name resolves without any additional
resource.

The Secret name is derived from a new compile-time `tradingDbName` variable.
Using `tradingDb.name` would compile to `reference('tradingDb').name`, and the
resource's runtime property bag has no `name` key, which would have reproduced
the same class of failure this change fixes.

The Kubernetes recipe gets the same treatment so that both environments keep
sharing one `app.bicep`, and its `result` block is rewritten to be
reference-free while it is being touched.

Changes:
  * postgres-azure-flex.bicep: stable `credentialsName`, drop the nested
    `secrets` output.
  * postgres-kubernetes.bicep: matching `credentialsName`, add
    `initializerName`, drop the nested `secrets` output, and build the
    `result` block from compile-time variables instead of
    `<resource>.metadata.name`.
  * app.bicep: add `tradingDbName` and bind the password via
    `valueFrom.secretRef`.
  * validate-postgres-recipes.sh: replace the previous guard, which wrongly
    required a nested `secrets` block, with guards covering the new contract,
    and extend the existing output guards to both PostgreSQL recipes.
  * configure-recipes.sh, solution-04.md: bump the recipe version to 1.0.5 and
    document both `result` constraints.

Every new guard was verified to fail when its defect is reintroduced. All three
templates compile against the pinned extension, and the compiled `app.bicep`
resolves the Secret name at build time.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Fix recipe secret consumption so the backend container can deploy
…kout

The schema initializer Job has been failing since it was introduced, and the
failure is invisible: the recipe does not wait for the Job, so `rad deploy`
reports success while the trading database is left completely empty. On the
cluster the Job sits at `Failed  0/1`, having burned through its backoff limit
with seven pods that each died in seconds.

The cause is line endings. `.gitattributes` pins `eol=lf` through a per-file
allowlist that covers `app.bicep`, `ai.bicep`, `ai-model-*.bicep` and `**/*.sh`
but omits `iac/recipes/postgres-*.bicep`. Those files are stored as LF and
checked out as CRLF on Windows. Bicep preserves on-disk line endings verbatim
through `loadTextContent()` and `'''…'''` multi-line strings, so the carriage
returns end up inside the Job's shell script. `sh` does not treat CR as
whitespace, so `do<CR>` stops being the `do` keyword and the container exits
immediately:

    sh: syntax error: unexpected word (expecting "do")

Linux and macOS checkouts are unaffected, which is why this went unnoticed.

Fix the root cause by replacing the allowlist with globs covering `*.bicep`,
`*.sql`, `*.yaml` and `*.yml`, so files added later are pinned by default rather
than silently omitted. Additionally strip carriage returns in the recipes
themselves with `replace(..., '\r', '')`, which keeps them correct on a checkout
configured any other way. ARM evaluates that at deployment time; a probe
deployment confirmed `length('a\r\nb')` is 4 while the replaced value is 3.

Fixing the script alone is not deployable, because it exposes a second defect.
A Job's `spec.template` is immutable, so a Job must be replaced rather than
updated when its pod template changes, and that only happens if its name
changes. The name was derived from `uniqueString(schemaSql)`, which tracks the
schema and nothing else. Renaming the credentials Secret in the previous change
therefore altered the pod template while leaving the name identical, and the
deployment failed with:

    Job.batch "postgres-...-schema-c6pwzgiu" is invalid:
    spec.template: Invalid value: ...: field is immutable

Changing the script would have hit exactly the same wall. Hash every input the
pod template embeds — the script, the credentials Secret name, the user, the
database, the port and the client image tag — so any pod template change yields
a new Job name. Radius creates the new Job and garbage-collects the old one
through `result.resources`.

Both PostgreSQL recipes get the same treatment, since `app.bicep` is shared by
the AKS and K3s environments.

Changes:
  * .gitattributes: replace the per-file bicep allowlist with globs.
  * postgres-azure-flex.bicep, postgres-kubernetes.bicep: strip CR from the
    schema SQL and the initializer script, and hash the whole pod template into
    the Job name.
  * validate-postgres-recipes.sh: guard CR stripping, the use of the extracted
    script variable, the Job name hash inputs, and the .gitattributes globs.
  * configure-recipes.sh, solution-04.md: bump the recipe version to 1.0.6 and
    document both constraints, including how to verify the Job actually
    completed rather than trusting the deployment result.

Each of the nine new guards was verified to fail when its defect is
reintroduced. All three templates still compile, and they were compiled from a
CRLF working tree so the compiled output exercises the stripping path.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…-endings

Fix the schema initializer Job, which has never run on a Windows checkout
Challenge 05 could not finish on AKS. After the schema initializer was fixed the
backend still crash-looped, and `rad deploy` failed before the frontend container was
ever created:

    "code": "Internal",
    "message": "Container state is 'Waiting' Reason: CrashLoopBackOff, ..."

The backend reached PostgreSQL fine; `/api/accounts` returned data. It died on MQTT.
`MqttOrderListener` called `SubscribeAsync` on a client that never connected, which
threw `MqttClientNotConnectedException`, and .NET's default
`BackgroundServiceExceptionBehavior.StopHost` stopped the host.

The connection was refused because Azure Event Grid accepts a Microsoft Entra token
only through the MQTT v5 enhanced-authentication fields: `Authentication Method` set
to `OAUTH2-JWT` and the bearer token in `Authentication Data`. The published Trading
backend sends the token as an ordinary CONNECT password, which Event Grid never
inspects. This is a defect in the application image, not in the workshop's Azure
setup, so provisioning topic spaces, federated credentials and Event Grid data-plane
roles would all have been correct work and the broker would still have refused every
connection.

Register the in-cluster Mosquitto recipe for `Radius.Resources/mqttBrokers` on AKS,
behind a `mqttRecipeTemplatePath` override so the Event Grid recipe can be restored in
one parameter once the application supports enhanced authentication. This keeps the
change where it belongs. `iac/app.bicep` still requests the portable contract and names
no broker; only the platform team's recipe registration differs.

Swapping the recipe alone was not enough, which exposed a second defect. `app.bicep`
read `MQTT_AUTH_METHOD` and `MQTT_TOKEN_AUDIENCE` from the workload identity, and the
Azure workload-identity recipe hard-codes `authMethod: 'OAUTH2-JWT'`. The application
therefore attempted token authentication regardless of which broker recipe the
environment had registered, so the identity silently overrode the platform team's
choice. Read both from the broker instead, which is what the `mqttBrokers` resource
type documents. The expression is identical in both environments and each recipe
supplies its own answer, so K3s is unaffected; its no-op identity recipe already
reported `none`.

While here, derive `AZURE_TENANT_ID` from the identity resource rather than from the
bare override parameter. The parameter defaults to empty, so the containers were given
an empty tenant even though the recipe returns the correct one.

Guard all of this in `validate-postgres-recipes.sh`: the AKS environment must not
default `mqttBrokers` to the Event Grid recipe, and `app.bicep` must read the MQTT auth
method and audience from the broker and the tenant from the identity.

Correct the documentation that described this as a topic-space and permission-binding
gap, since that was never the cause, and that promised a reachable frontend with only
MQTT features degraded, since the deployment actually failed outright.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
… failure modes (#21)

* docs(challenge-05): place orders in step 5 and verify them in Azure PostgreSQL

Step 5 stopped at "open localhost:3000", so step 6 had no application data to
check and the managed database was never actually queried.

Step 5 now covers sign-in and asks the attendee to place several BUY orders
(and optionally an uncoverable SELL) so both `processed` and `rejected` rows
exist. Orders reach PostgreSQL over the backend's /api/orders endpoint.

Step 6 is split per platform, because app.bicep is identical but the database
it resolves to is not:

- K3s keeps the in-cluster verification pod.
- AKS gets an Azure-native check that reads the orders back out of Azure
  Database for PostgreSQL Flexible Server, which is the actual proof the data
  left the cluster.

Verified end to end against a live Flexible Server. Fixes found while doing so:

- `az postgres flexible-server firewall-rule` takes `--server-name` for the
  server and `--name` for the rule; `--rule-name` does not exist.
- `az postgres flexible-server execute` splits `--querytext` on newlines and
  runs only the first fragment, failing with a misleading
  `column "..." does not exist`. All queries are now single-line.
- Its table renderer drops a bare `id` column and native timestamp columns, so
  the orders query aliases `id` and casts `created_at::text`.
- The recipe allowlists only AKS egress IPs, so the section opens a temporary
  client firewall rule and closes it again, with an in-cluster psql fallback
  that needs no firewall change at all.
- The PowerShell K3s block piped `kubectl get job` into ConvertFrom-Json
  without `--output json`, which always threw.

Also records that Flexible Server has no portal query editor, so the portal
route is Cloud Shell running the same CLI.

* docs(challenge-05): document the two failure modes that cost the most time

Both were hit repeatedly while validating this challenge and neither was
written down anywhere.

A deployment can report a container failure that already succeeded. Radius
polls the pods that already exist, so a pod left crash-looping by an earlier
attempt is still inside its five-minute back-off window and reports a stale
status while the replacement ReplicaSet rolls out. The give-away is that the
pod hash in the error belongs to the previous attempt. Records how to confirm
it from the live pods and that re-running is the fix.

The AKS cluster and the PostgreSQL Flexible Server can both be stopped between
sessions, and each fails unrecognisably: the cluster stops resolving its API
server name, so every rad and kubectl command dies with NXDOMAIN before
reaching Radius, while the database surfaces as a bare InternalServerError
nested inside RecipeDeploymentFailed. Neither looks like a stopped resource.
Adds the state checks, the start commands, and the kubeconfig refresh for a
stale context pointing at a deleted cluster.

Also links the Mosquitto explanation to the upstream defect it depends on,
microsoft/adaptive-apps#44, so readers can tell when Event Grid becomes usable.
* Add Challenge 09: model, review, and deploy with Radius Canvas

Replaces the custom-resources scaffold with a full student challenge and coach
walkthrough built around the Radius Canvas public preview for the GitHub Copilot
app.

The challenge contrasts the Canvas developer path (source-derived .radius/app.bicep,
recipe packs, ephemeral control plane in GitHub Actions, OIDC trust) with the
platform path built in Challenges 03-08, and requires an honest assessment of what
the preview does and does not solve for the non-Azure port.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Correct Challenge 09 factual errors found in review

- OIDC: the federated credential subject binds repository and environment, not
  branch. Replace the incorrect branch-isolation claim with a three-way table and
  point at GitHub Environment deployment-branch rules as the actual control.
- Diff analysis: modeled properties such as env vars, image references and probes
  are reported as modified, not unchanged. Replace the two-way table with the
  correct three-way split of reported, reported-but-not-judged, and not-compared.
- Do not run rad on the host after stating no host rad is required; move the
  control-plane isolation checks into the devcontainer.
- Compare AI against iac/ai.bicep, not iac/app.bicep, which keeps AI disabled.
- Expect and teach the unresolved-recipe case: a generated custom type has no
  entry in the azure-avm pack, so plan and deploy are expected to surface it.
- A shared Radius.Resources namespace is not a shared contract; require a
  schema-compatibility judgement rather than implying interchangeability.
- Prove isolation with Azure resource snapshots and a Challenge 05 health check,
  not namespace separation and resource counts alone.
- Scope the secret criterion to newly introduced values and add a scan of the
  generated model, since upstream docker-compose.yml already carries dev values.
- Drop the immutable-tag and functionally-similar-recipe overclaims, add coach
  pre-staging guidance, and raise the estimate to 90-150 minutes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ve Apps readme (#23)

* Target platform engineers and app developers in the Adaptive Apps readme

Split the challenge sequence into two audience paths, call out that challenges 01-03 can be pre-provisioned via the Hack Console, and add diagrams for the one-application/many-platforms model.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Anchor the readme to microsoft/adaptive-apps and the Simplified Trading App

Add a 'What this MicroHack builds on' table mapping every building block to its upstream source, name the Simplified Trading App as the running example, and add an Additional documentation section including the Azure Architecture Center article.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Removed references to resource types, recipe artifacts, and Radius from the Readme.
@djong1

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overall structure and documentation looks very good

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants