From 78cd8c9156644420117396649244008d691c6c7c Mon Sep 17 00:00:00 2001 From: Harsh Mishra Date: Tue, 13 Oct 2026 19:12:19 +0530 Subject: [PATCH 1/5] Test two samples end to end on the LocalStack Azure emulator Add a workflow that creates the AKS cluster on LocalStack for Azure, deploys the in-cluster PostgreSQL and Blob Storage samples, and checks each web app stores, lists and deletes an activity. Start the emulator with lstk in the README, since lstk does not ship the localstack CLI. Claude-Session: https://claude.ai/code/session_01DZfDmzUBPqNAu3yFKyfCNM --- .github/workflows/samples.yml | 84 ++++++++++++++++++++++++++++++++++ README.md | 16 ++++++- tests/test-vacation-planner.sh | 62 +++++++++++++++++++++++++ 3 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/samples.yml create mode 100755 tests/test-vacation-planner.sh diff --git a/.github/workflows/samples.yml b/.github/workflows/samples.yml new file mode 100644 index 0000000..45c45f2 --- /dev/null +++ b/.github/workflows/samples.yml @@ -0,0 +1,84 @@ +name: Test samples on LocalStack for Azure + +# Creates the AKS cluster on the LocalStack for Azure emulator, deploys each sample's +# Python web app with its numbered scripts, and checks the app end to end with +# tests/test-vacation-planner.sh. + +on: + push: + branches: + - main + paths-ignore: + - '**.md' + pull_request: + branches: + - main + workflow_dispatch: + +permissions: {} + +jobs: + sample: + name: ${{ matrix.sample }} + runs-on: ubuntu-latest + timeout-minutes: 60 + permissions: + contents: read + strategy: + fail-fast: false + matrix: + sample: + # Data in a StatefulSet inside the cluster: exercises AKS, ACR and the workload itself + - web-app-in-cluster-postgresql + # Data in an emulated Azure service: exercises a pod calling Azure Blob Storage + - web-app-blob-storage + env: + SAMPLE: ${{ matrix.sample }} + + steps: + - name: Check out repo + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Install lstk + run: npm install -g @localstack/lstk + + - name: Create the SSH key the cluster script provisions the node pools with + run: ssh-keygen -t rsa -b 4096 -N '' -f ~/.ssh/id_rsa -q + + - name: Start the LocalStack for Azure emulator + env: + LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} + run: | + lstk start --type azure --non-interactive + lstk az start-interception + + - name: Create the AKS cluster + working-directory: scripts + run: ./01-system-assigned-managed-identity.sh + + - name: Deploy the sample + working-directory: samples/${{ matrix.sample }}/python/scripts + run: | + ./01-deploy-resources.sh + ./02-build-docker-image.sh + ./04-push-docker-image.sh + ./05-deploy-app.sh + + - name: Test the web app + run: | + namespace=$(sed -n "s/^NAMESPACE=\"\(.*\)\"/\1/p" "samples/$SAMPLE/python/scripts/00-variables.sh") + service=$(sed -n "s/^SERVICE_NAME=\"\(.*\)\"/\1/p" "samples/$SAMPLE/python/scripts/00-variables.sh") + ./tests/test-vacation-planner.sh "$namespace" "$service" + + - name: Show cluster state and emulator logs + if: failure() + run: | + kubectl get pods --all-namespaces -o wide || true + kubectl get events --all-namespaces --sort-by=.lastTimestamp | tail -50 || true + for ns in $(kubectl get namespaces -o name | sed 's|namespace/||' | grep vacation-planner); do + kubectl describe pods --namespace "$ns" | tail -80 || true + kubectl logs --namespace "$ns" --all-containers --tail=100 -l "app=$ns" || true + done + lstk logs | tail -300 || true diff --git a/README.md b/README.md index 87ba587..a0fb1aa 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,7 @@ Start the emulator and point the Azure CLI at it: export LOCALSTACK_AUTH_TOKEN= # Start the LocalStack Azure emulator -IMAGE_NAME=localstack/localstack-azure localstack start -d -localstack wait -t 60 +lstk start --type azure # Route all Azure CLI calls to the emulator lstk az start-interception @@ -148,6 +147,19 @@ Both versions of the app also expose `GET /health`, the endpoint their Kubernete Alternatively, you can use a terminal UI such as [k9s](https://k9scli.io/) to select the service and start a port-forward interactively (press `` on a selected service or pod). +### Testing a deployed sample + +[tests/test-vacation-planner.sh](tests/test-vacation-planner.sh) checks a deployed web app end to end through its Kubernetes service: it waits for the Deployment to roll out, checks that `GET /health` reports the data service as reachable, then adds an activity through the web form, checks that the page lists it, deletes it, and checks that it is gone. Pass the sample's namespace and service name, for example: + +```bash +./tests/test-vacation-planner.sh vacation-planner-blob vacation-planner-blob +``` + +The [Test samples on LocalStack for Azure](.github/workflows/samples.yml) workflow runs this on every pull request against the emulator. It creates the cluster with `scripts/01-system-assigned-managed-identity.sh`, then deploys and tests the Python version of two samples: + +- **Web App with In-Cluster PostgreSQL StatefulSet**, which covers AKS, Azure Container Registry and the workload itself without a managed data service. +- **Web App with Azure Blob Storage**, which covers a pod calling an emulated Azure data service. + ## Tutorials Beyond the Vacation Planner samples, the repository includes standalone tutorials that exercise specific AKS capabilities. Unlike the samples above, they do not deploy the web app. diff --git a/tests/test-vacation-planner.sh b/tests/test-vacation-planner.sh new file mode 100755 index 0000000..d036be5 --- /dev/null +++ b/tests/test-vacation-planner.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# Check a deployed Vacation Planner sample end to end through its Kubernetes service: +# 1. the Deployment rolls out and GET /health reports the data service as reachable +# 2. adding an activity through the web form stores it, and the page lists it +# 3. deleting it through the web form removes it from the page +# +# Usage: tests/test-vacation-planner.sh [deployment] +set -euo pipefail + +NAMESPACE=$1 +SERVICE=$2 +DEPLOYMENT=${3:-$2} +LOCAL_PORT=${LOCAL_PORT:-8080} +MAX_ATTEMPTS=${MAX_ATTEMPTS:-60} +BASE_URL="http://localhost:$LOCAL_PORT" + +kubectl rollout status "deployment/$DEPLOYMENT" --namespace "$NAMESPACE" --timeout=600s + +kubectl port-forward --namespace "$NAMESPACE" "service/$SERVICE" "$LOCAL_PORT:80" > /dev/null 2>&1 & +PF_PID=$! +trap 'kill "$PF_PID" 2>/dev/null' EXIT + +health="" +for attempt in $(seq 1 "$MAX_ATTEMPTS"); do + health=$(curl -s -m 10 "$BASE_URL/health" || true) + echo "$health" | jq -e '.status == "ok"' > /dev/null 2>&1 && break + echo "Attempt $attempt/$MAX_ATTEMPTS: /health returned '$health'" + sleep 5 +done +if ! echo "$health" | jq -e '.status == "ok"' > /dev/null 2>&1; then + echo "Test failed: /health never reported ok" + exit 1 +fi +echo "PASS: /health reports the data service as reachable" + +ACTIVITY="CI check $(date +%s)" +status=$(curl -s -m 30 -o /dev/null -w '%{http_code}' --data-urlencode "activity=$ACTIVITY" "$BASE_URL/") +if [ "$status" != "302" ]; then + echo "Test failed: adding an activity returned HTTP $status, expected a 302 redirect" + exit 1 +fi + +# Each row carries the activity id and text: +row_id=$(curl -s -m 30 "$BASE_URL/" | grep -o "data-id=\"[^\"]*\" data-activity=\"$ACTIVITY\"" | sed 's/^data-id="\([^"]*\)".*/\1/' || true) +if [ -z "$row_id" ]; then + echo "Test failed: the page does not list the activity '$ACTIVITY'" + exit 1 +fi +echo "PASS: added activity '$ACTIVITY' (id $row_id) and the page lists it" + +status=$(curl -s -m 30 -o /dev/null -w '%{http_code}' -X POST "$BASE_URL/delete/$row_id") +if [ "$status" != "302" ]; then + echo "Test failed: deleting the activity returned HTTP $status, expected a 302 redirect" + exit 1 +fi +if curl -s -m 30 "$BASE_URL/" | grep -q "data-activity=\"$ACTIVITY\""; then + echo "Test failed: the activity is still listed after deleting it" + exit 1 +fi +echo "PASS: deleted the activity and the page no longer lists it" + +echo "Test passed: the Vacation Planner in [$NAMESPACE] stores, lists and deletes activities" From cd4f68197b073366d9e19cb288ddb3e2c564e6c8 Mon Sep 17 00:00:00 2001 From: Harsh Mishra Date: Tue, 13 Oct 2026 19:15:12 +0530 Subject: [PATCH 2/5] Answer the cluster script prompts in CI The script asks for the network configuration and node pool sizes. With no input the menu left the network settings empty and az aks create failed. Also fix the menu heading, which was copied from a Docker script. Claude-Session: https://claude.ai/code/session_01DZfDmzUBPqNAu3yFKyfCNM --- .github/workflows/samples.yml | 4 +++- scripts/01-system-assigned-managed-identity.sh | 2 +- scripts/01-user-assigned-managed-identity.sh | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/samples.yml b/.github/workflows/samples.yml index 45c45f2..89169e3 100644 --- a/.github/workflows/samples.yml +++ b/.github/workflows/samples.yml @@ -54,9 +54,11 @@ jobs: lstk start --type azure --non-interactive lstk az start-interception + # The script prompts for the network configuration and both node pool sizes: + # pick option 1 (Azure network plugin and policy) and keep the default sizes - name: Create the AKS cluster working-directory: scripts - run: ./01-system-assigned-managed-identity.sh + run: printf '1\n\n\n' | ./01-system-assigned-managed-identity.sh - name: Deploy the sample working-directory: samples/${{ matrix.sample }}/python/scripts diff --git a/scripts/01-system-assigned-managed-identity.sh b/scripts/01-system-assigned-managed-identity.sh index 63204fb..1923b80 100755 --- a/scripts/01-system-assigned-managed-identity.sh +++ b/scripts/01-system-assigned-managed-identity.sh @@ -83,7 +83,7 @@ ok=0 # Print the menu echo "====================================" -echo "Run Docker Container (1-3): " +echo "Select the network configuration (1-3): " echo "====================================" options=( "Azure Network Plugin + Azure Network Policy" diff --git a/scripts/01-user-assigned-managed-identity.sh b/scripts/01-user-assigned-managed-identity.sh index f2c87cc..1273e2a 100755 --- a/scripts/01-user-assigned-managed-identity.sh +++ b/scripts/01-user-assigned-managed-identity.sh @@ -84,7 +84,7 @@ ok=0 # Print the menu echo "====================================" -echo "Run Docker Container (1-3): " +echo "Select the network configuration (1-3): " echo "====================================" options=( "Azure Network Plugin + Azure Network Policy" From 4a6e4dc8927591ccc14ad77af0caaf2e27954e7d Mon Sep 17 00:00:00 2001 From: Harsh Mishra Date: Thu, 15 Oct 2026 10:20:48 +0530 Subject: [PATCH 3/5] Test every sample in Python and .NET, and harden the test Apply the review on #41: - The workflow builds its matrix from every samples// app. A pull request or push runs the apps it changes, and every app when the cluster script, the test or the workflow changes. A manual run picks run_mode changed (default) or all. Fork PRs are skipped, lstk is pinned to 1.4.0, and superseded runs are cancelled. - The test keeps cookies and sends the Razor Pages antiforgery token, so it drives the .NET apps too, and posts the Azure Files delete to /delete with activity_id. - The delete check keeps a second activity listed, so a page that lists nothing no longer passes. - The test stops when kubectl port-forward cannot bind instead of testing whatever owns the port. - The in-cluster PostgreSQL sample uses PREFIX local like the other samples, so it pulls from the registry attached to the cluster. Claude-Session: https://claude.ai/code/session_01DZfDmzUBPqNAu3yFKyfCNM --- .github/workflows/samples.yml | 124 ++++++++++++++++-- README.md | 7 +- .../dotnet/scripts/00-variables.sh | 2 +- .../python/scripts/00-variables.sh | 2 +- tests/test-vacation-planner.sh | 81 ++++++++++-- 5 files changed, 187 insertions(+), 29 deletions(-) diff --git a/.github/workflows/samples.yml b/.github/workflows/samples.yml index 89169e3..80b559f 100644 --- a/.github/workflows/samples.yml +++ b/.github/workflows/samples.yml @@ -1,8 +1,10 @@ name: Test samples on LocalStack for Azure -# Creates the AKS cluster on the LocalStack for Azure emulator, deploys each sample's -# Python web app with its numbered scripts, and checks the app end to end with -# tests/test-vacation-planner.sh. +# Creates the AKS cluster on the LocalStack for Azure emulator, deploys a sample's Python or .NET web +# app with its numbered scripts, one job per app, and checks the app end to end with +# tests/test-vacation-planner.sh. As in the Samples CI of localstack-azure-samples, a pull request or a +# push runs the apps it changes (every app when a file all of them use changes), and a manual run can +# choose to run every app. on: push: @@ -14,26 +16,103 @@ on: branches: - main workflow_dispatch: + inputs: + run_mode: + description: "'all' runs every sample, 'changed' runs only the samples with modified files" + required: false + type: choice + options: + - all + - changed + default: changed permissions: {} +# A new push to the same pull request cancels the run it supersedes: with one job per sample and +# language, a stale run would otherwise hold up to twenty runners for the length of a deployment +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + jobs: + setup: + name: Select samples + # Pull requests from forks get no secrets, so lstk could not start the emulator there + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + matrix: ${{ steps.select.outputs.matrix }} + has_samples: ${{ steps.select.outputs.has_samples }} + + steps: + - name: Check out repo + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + # The full history, so that 'changed' mode can compare with the base commit + fetch-depth: 0 + + - name: Select the samples to run + id: select + env: + # Pull requests and pushes have no inputs: they run the samples they change + RUN_MODE: ${{ inputs.run_mode || 'changed' }} + EVENT_NAME: ${{ github.event_name }} + PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + PUSH_BEFORE_SHA: ${{ github.event.before }} + run: | + # Every app a job can deploy, as samples//, so a new sample needs no change here + apps=$(printf '%s\n' samples/*/*/scripts/05-deploy-app.sh | sed 's|/scripts/05-deploy-app\.sh$||') + selected=$apps + + if [ "$RUN_MODE" = changed ]; then + case $EVENT_NAME in + pull_request) base=$PR_BASE_SHA ;; + push) base=$PUSH_BEFORE_SHA ;; + *) base=origin/main ;; + esac + if git rev-parse --quiet --verify "$base^{commit}" > /dev/null; then + changed=$(git diff --name-only "$base...HEAD") + echo "Files changed since $base:" + echo "$changed" + # Every job creates the cluster with the same script and checks the app with the same test, so a + # change to either, or to this workflow, runs every app; otherwise an app runs when its folder changes + shared='^(scripts/01-system-assigned-managed-identity\.sh|tests/test-vacation-planner\.sh|\.github/workflows/samples\.yml)$' + if ! grep -q -E "$shared" <<< "$changed"; then + selected=$(while read -r app; do + if grep -q "^$app/" <<< "$changed"; then echo "$app"; fi + done <<< "$apps") + fi + else + # A push that creates the branch or rewrites its history leaves no base commit to compare with + echo "Base commit '$base' not found: running every sample" + fi + fi + + matrix=$(jq -n -R -c '[inputs | select(length > 0) | split("/") | {sample: .[1], language: .[2]}] | {include: .}' <<< "$selected") + echo "Samples to run: $(jq -r '[.include[] | "\(.sample) (\(.language))"] | if length > 0 then join(", ") else "none" end' <<< "$matrix")" + { + echo "matrix=$matrix" + echo "has_samples=$(jq '.include | length > 0' <<< "$matrix")" + } >> "$GITHUB_OUTPUT" + sample: - name: ${{ matrix.sample }} + name: ${{ matrix.sample }} (${{ matrix.language }}) + needs: setup + # Skip the job when the run changes no sample + if: needs.setup.outputs.has_samples == 'true' runs-on: ubuntu-latest timeout-minutes: 60 permissions: contents: read strategy: fail-fast: false - matrix: - sample: - # Data in a StatefulSet inside the cluster: exercises AKS, ACR and the workload itself - - web-app-in-cluster-postgresql - # Data in an emulated Azure service: exercises a pod calling Azure Blob Storage - - web-app-blob-storage + matrix: ${{ fromJSON(needs.setup.outputs.matrix) }} env: SAMPLE: ${{ matrix.sample }} + LANGUAGE: ${{ matrix.language }} steps: - name: Check out repo @@ -42,7 +121,19 @@ jobs: persist-credentials: false - name: Install lstk - run: npm install -g @localstack/lstk + # Pinned like actions/checkout, so an lstk release cannot change every run at once: bump it deliberately + run: npm install -g @localstack/lstk@1.4.0 + + - name: Install sqlcmd + # The Azure SQL Database sample seeds its database with sqlcmd, which the runner image does not ship + if: matrix.sample == 'web-app-sql-database' + run: | + sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list + curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | sudo tee /etc/apt/trusted.gpg.d/microsoft.asc > /dev/null + curl -fsSL "https://packages.microsoft.com/config/ubuntu/$(lsb_release -rs)/prod.list" | sudo tee /etc/apt/sources.list.d/mssql-release.list > /dev/null + sudo apt-get update + sudo ACCEPT_EULA=Y apt-get install -y mssql-tools18 + echo "/opt/mssql-tools18/bin" >> "$GITHUB_PATH" - name: Create the SSH key the cluster script provisions the node pools with run: ssh-keygen -t rsa -b 4096 -N '' -f ~/.ssh/id_rsa -q @@ -61,7 +152,11 @@ jobs: run: printf '1\n\n\n' | ./01-system-assigned-managed-identity.sh - name: Deploy the sample - working-directory: samples/${{ matrix.sample }}/python/scripts + working-directory: samples/${{ matrix.sample }}/${{ matrix.language }}/scripts + env: + # Answer the Azure Files sample's two provisioning menus; the other samples do not read these + PROVISIONING_MODE: static + FILE_SHARE_PROTOCOL: smb run: | ./01-deploy-resources.sh ./02-build-docker-image.sh @@ -70,8 +165,9 @@ jobs: - name: Test the web app run: | - namespace=$(sed -n "s/^NAMESPACE=\"\(.*\)\"/\1/p" "samples/$SAMPLE/python/scripts/00-variables.sh") - service=$(sed -n "s/^SERVICE_NAME=\"\(.*\)\"/\1/p" "samples/$SAMPLE/python/scripts/00-variables.sh") + variables="samples/$SAMPLE/$LANGUAGE/scripts/00-variables.sh" + namespace=$(sed -n "s/^NAMESPACE=\"\(.*\)\"/\1/p" "$variables") + service=$(sed -n "s/^SERVICE_NAME=\"\(.*\)\"/\1/p" "$variables") ./tests/test-vacation-planner.sh "$namespace" "$service" - name: Show cluster state and emulator logs diff --git a/README.md b/README.md index a0fb1aa..4db3102 100644 --- a/README.md +++ b/README.md @@ -149,16 +149,13 @@ Alternatively, you can use a terminal UI such as [k9s](https://k9scli.io/) to se ### Testing a deployed sample -[tests/test-vacation-planner.sh](tests/test-vacation-planner.sh) checks a deployed web app end to end through its Kubernetes service: it waits for the Deployment to roll out, checks that `GET /health` reports the data service as reachable, then adds an activity through the web form, checks that the page lists it, deletes it, and checks that it is gone. Pass the sample's namespace and service name, for example: +[tests/test-vacation-planner.sh](tests/test-vacation-planner.sh) checks a deployed web app end to end through its Kubernetes service: it waits for the Deployment to roll out, checks that `GET /health` reports the data service as reachable, then adds an activity through the web form, checks that the page lists it, deletes it, and checks that it is gone while a second activity stays listed. It drives both the Python (Flask) and the .NET (Razor Pages) apps. Pass the sample's namespace and service name, for example: ```bash ./tests/test-vacation-planner.sh vacation-planner-blob vacation-planner-blob ``` -The [Test samples on LocalStack for Azure](.github/workflows/samples.yml) workflow runs this on every pull request against the emulator. It creates the cluster with `scripts/01-system-assigned-managed-identity.sh`, then deploys and tests the Python version of two samples: - -- **Web App with In-Cluster PostgreSQL StatefulSet**, which covers AKS, Azure Container Registry and the workload itself without a managed data service. -- **Web App with Azure Blob Storage**, which covers a pod calling an emulated Azure data service. +The [Test samples on LocalStack for Azure](.github/workflows/samples.yml) workflow runs this against the emulator, one job per sample app, in Python and .NET. Each job creates the cluster with `scripts/01-system-assigned-managed-identity.sh`, deploys the app with its numbered scripts, and runs the test. A pull request or a push to `main` runs the apps whose folders it changes, and all 20 when it changes the cluster script, the test or the workflow. A manual run tests the changed apps by default, or all 20 with `run_mode: all`. ## Tutorials diff --git a/samples/web-app-in-cluster-postgresql/dotnet/scripts/00-variables.sh b/samples/web-app-in-cluster-postgresql/dotnet/scripts/00-variables.sh index 2a7562a..26de57e 100644 --- a/samples/web-app-in-cluster-postgresql/dotnet/scripts/00-variables.sh +++ b/samples/web-app-in-cluster-postgresql/dotnet/scripts/00-variables.sh @@ -1,7 +1,7 @@ # Variables # Azure Resources -PREFIX='zeus' +PREFIX='local' SUFFIX='test' LOCATION='italynorth' RESOURCE_GROUP_NAME="${PREFIX}-rg" diff --git a/samples/web-app-in-cluster-postgresql/python/scripts/00-variables.sh b/samples/web-app-in-cluster-postgresql/python/scripts/00-variables.sh index 511c161..77786cd 100644 --- a/samples/web-app-in-cluster-postgresql/python/scripts/00-variables.sh +++ b/samples/web-app-in-cluster-postgresql/python/scripts/00-variables.sh @@ -1,7 +1,7 @@ # Variables # Azure Resources -PREFIX='zeus' +PREFIX='local' SUFFIX='test' LOCATION='italynorth' RESOURCE_GROUP_NAME="${PREFIX}-rg" diff --git a/tests/test-vacation-planner.sh b/tests/test-vacation-planner.sh index d036be5..0bd3c78 100755 --- a/tests/test-vacation-planner.sh +++ b/tests/test-vacation-planner.sh @@ -3,6 +3,7 @@ # 1. the Deployment rolls out and GET /health reports the data service as reachable # 2. adding an activity through the web form stores it, and the page lists it # 3. deleting it through the web form removes it from the page +# It drives both versions of every sample: the Python (Flask) and the .NET (Razor Pages) web app. # # Usage: tests/test-vacation-planner.sh [deployment] set -euo pipefail @@ -16,9 +17,21 @@ BASE_URL="http://localhost:$LOCAL_PORT" kubectl rollout status "deployment/$DEPLOYMENT" --namespace "$NAMESPACE" --timeout=600s -kubectl port-forward --namespace "$NAMESPACE" "service/$SERVICE" "$LOCAL_PORT:80" > /dev/null 2>&1 & +PF_LOG=$(mktemp) +COOKIES=$(mktemp) +kubectl port-forward --namespace "$NAMESPACE" "service/$SERVICE" "$LOCAL_PORT:80" > "$PF_LOG" 2>&1 & PF_PID=$! -trap 'kill "$PF_PID" 2>/dev/null' EXIT +trap 'kill "$PF_PID" 2>/dev/null || true; rm -f "$PF_LOG" "$COOKIES"' EXIT + +# Wait until kubectl listens on LOCAL_PORT. If it exits instead (the port is taken, for example by a +# port-forward left running for another sample), stop rather than test whatever answers on that port. +until grep -q '^Forwarding from' "$PF_LOG"; do + if ! kill -0 "$PF_PID" 2>/dev/null; then + echo "Test failed: kubectl port-forward exited: $(cat "$PF_LOG")" + exit 1 + fi + sleep 1 +done health="" for attempt in $(seq 1 "$MAX_ATTEMPTS"); do @@ -33,30 +46,82 @@ if ! echo "$health" | jq -e '.status == "ok"' > /dev/null 2>&1; then fi echo "PASS: /health reports the data service as reachable" +# Talk to the app the way a browser does. The .NET apps are Razor Pages, which reject a form post without the +# antiforgery token the page renders and the cookie sent with it; the Python apps ignore both. +get_page() { + curl -sS -m 30 -b "$COOKIES" -c "$COOKIES" "$BASE_URL/" +} + +# Post a form and print the HTTP status: post_form [=...] +post_form() { + local path=$1 field + shift + local args=(-sS -m 30 -o /dev/null -w '%{http_code}' -b "$COOKIES" -c "$COOKIES" -X POST) + for field in "$@"; do + args+=(--data-urlencode "$field") + done + if [ -n "$token" ]; then + args+=(--data-urlencode "__RequestVerificationToken=$token") + fi + curl "${args[@]}" "$BASE_URL$path" +} + +# Print the id of the row that lists an activity on the last page fetched, or nothing. +# Each row carries the activity id and text: +row_id_of() { + grep -o "data-id=\"[^\"]*\" data-activity=\"$1\"" <<< "$page" | sed 's/^data-id="\([^"]*\)".*/\1/' || true +} + +# Most samples delete through POST /delete/; the Azure Files sample posts the id to /delete +delete_activity() { + if grep -q 'name="activity_id"' <<< "$page"; then + post_form /delete "activity_id=$1" + else + post_form "/delete/$1" + fi +} + +page=$(get_page) +token=$(sed -n '/name="__RequestVerificationToken"/{s/.*name="__RequestVerificationToken" type="hidden" value="\([^"]*\)".*/\1/p;q;}' <<< "$page") + ACTIVITY="CI check $(date +%s)" -status=$(curl -s -m 30 -o /dev/null -w '%{http_code}' --data-urlencode "activity=$ACTIVITY" "$BASE_URL/") +status=$(post_form / "activity=$ACTIVITY") if [ "$status" != "302" ]; then echo "Test failed: adding an activity returned HTTP $status, expected a 302 redirect" exit 1 fi - -# Each row carries the activity id and text: -row_id=$(curl -s -m 30 "$BASE_URL/" | grep -o "data-id=\"[^\"]*\" data-activity=\"$ACTIVITY\"" | sed 's/^data-id="\([^"]*\)".*/\1/' || true) +page=$(get_page) +row_id=$(row_id_of "$ACTIVITY") if [ -z "$row_id" ]; then echo "Test failed: the page does not list the activity '$ACTIVITY'" exit 1 fi echo "PASS: added activity '$ACTIVITY' (id $row_id) and the page lists it" -status=$(curl -s -m 30 -o /dev/null -w '%{http_code}' -X POST "$BASE_URL/delete/$row_id") +# Keep a second activity listed while the first is deleted. An app that swallows a failed read renders an +# empty list rather than an error (the in-cluster and blob apps do), so without it the absence check below +# also passes on a page that lists nothing. The blob and Azure Files samples name each activity after the +# current second, so wait one first. +sleep 1 +KEPT_ACTIVITY="CI keep $(date +%s)" +post_form / "activity=$KEPT_ACTIVITY" > /dev/null + +status=$(delete_activity "$row_id") if [ "$status" != "302" ]; then echo "Test failed: deleting the activity returned HTTP $status, expected a 302 redirect" exit 1 fi -if curl -s -m 30 "$BASE_URL/" | grep -q "data-activity=\"$ACTIVITY\""; then +page=$(get_page) +kept_id=$(row_id_of "$KEPT_ACTIVITY") +if [ -z "$kept_id" ]; then + echo "Test failed: the page no longer lists '$KEPT_ACTIVITY', so it cannot show the deleted activity is gone" + exit 1 +fi +if [ -n "$(row_id_of "$ACTIVITY")" ]; then echo "Test failed: the activity is still listed after deleting it" exit 1 fi echo "PASS: deleted the activity and the page no longer lists it" +delete_activity "$kept_id" > /dev/null || true echo "Test passed: the Vacation Planner in [$NAMESPACE] stores, lists and deletes activities" From fd81c280b167df62395539ba18ffeabcecc746cc Mon Sep 17 00:00:00 2001 From: Harsh Mishra Date: Thu, 15 Oct 2026 10:38:14 +0530 Subject: [PATCH 4/5] Accept the SQL Server EULA in CI and trust the emulator's SQL certificate The SQL Database jobs failed with EulaNotAccepted on az sql server create. Set LOCALSTACK_MSSQL_ACCEPT_EULA=Y when starting the emulator, as the sample's README and the localstack-azure-samples CI do. mssql-tools18 encrypts by default and validates the server certificate, so pass -N -C to every sqlcmd call, as the localstack-azure-samples version of this sample does. Claude-Session: https://claude.ai/code/session_01DZfDmzUBPqNAu3yFKyfCNM --- .github/workflows/samples.yml | 3 +++ .../dotnet/scripts/01-deploy-resources.sh | 6 ++++++ .../python/scripts/01-deploy-resources.sh | 6 ++++++ 3 files changed, 15 insertions(+) diff --git a/.github/workflows/samples.yml b/.github/workflows/samples.yml index 80b559f..4244e4f 100644 --- a/.github/workflows/samples.yml +++ b/.github/workflows/samples.yml @@ -141,6 +141,9 @@ jobs: - name: Start the LocalStack for Azure emulator env: LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} + # lstk forwards LOCALSTACK_-prefixed variables to the emulator. The Azure SQL Database sample + # needs the SQL Server EULA accepted, as its README and the localstack-azure-samples CI do + LOCALSTACK_MSSQL_ACCEPT_EULA: "Y" run: | lstk start --type azure --non-interactive lstk az start-interception diff --git a/samples/web-app-sql-database/dotnet/scripts/01-deploy-resources.sh b/samples/web-app-sql-database/dotnet/scripts/01-deploy-resources.sh index a305d58..aa241c2 100755 --- a/samples/web-app-sql-database/dotnet/scripts/01-deploy-resources.sh +++ b/samples/web-app-sql-database/dotnet/scripts/01-deploy-resources.sh @@ -143,6 +143,7 @@ fi echo "Creating SQL login [$DATABASE_USER_NAME] on server [$SQL_SERVER_FQDN]..." sqlcmd -S "$SQL_SERVER_FQDN" \ + -N -C \ -U "$ADMIN_USER" \ -P "$ADMIN_PASSWORD" \ -d master \ @@ -157,6 +158,7 @@ fi echo "Creating database user [$DATABASE_USER_NAME] in database [$SQL_DATABASE_NAME]..." sqlcmd -S "$SQL_SERVER_FQDN" \ + -N -C \ -U "$ADMIN_USER" \ -P "$ADMIN_PASSWORD" \ -d "$SQL_DATABASE_NAME" \ @@ -171,6 +173,7 @@ fi echo "Granting roles db_datareader, db_datawriter, db_ddladmin to [$DATABASE_USER_NAME]..." sqlcmd -S "$SQL_SERVER_FQDN" \ + -N -C \ -U "$ADMIN_USER" \ -P "$ADMIN_PASSWORD" \ -d "$SQL_DATABASE_NAME" \ @@ -185,6 +188,7 @@ fi echo "Creating table dbo.Activities in database [$SQL_DATABASE_NAME]..." sqlcmd -S "$SQL_SERVER_FQDN" \ + -N -C \ -U "$ADMIN_USER" \ -P "$ADMIN_PASSWORD" \ -d "$SQL_DATABASE_NAME" \ @@ -200,6 +204,7 @@ fi # Insert data echo "Inserting test data into [Activities] table..." sqlcmd -S "$SQL_SERVER_FQDN" \ + -N -C \ -d "$SQL_DATABASE_NAME" \ -U "$DATABASE_USER_NAME" \ -P "$DATABASE_USER_PASSWORD" \ @@ -228,6 +233,7 @@ fi # Query data echo "Querying test data from [Activities] table..." sqlcmd -S "$SQL_SERVER_FQDN" \ + -N -C \ -d "$SQL_DATABASE_NAME" \ -U "$DATABASE_USER_NAME" \ -P "$DATABASE_USER_PASSWORD" \ diff --git a/samples/web-app-sql-database/python/scripts/01-deploy-resources.sh b/samples/web-app-sql-database/python/scripts/01-deploy-resources.sh index a305d58..aa241c2 100755 --- a/samples/web-app-sql-database/python/scripts/01-deploy-resources.sh +++ b/samples/web-app-sql-database/python/scripts/01-deploy-resources.sh @@ -143,6 +143,7 @@ fi echo "Creating SQL login [$DATABASE_USER_NAME] on server [$SQL_SERVER_FQDN]..." sqlcmd -S "$SQL_SERVER_FQDN" \ + -N -C \ -U "$ADMIN_USER" \ -P "$ADMIN_PASSWORD" \ -d master \ @@ -157,6 +158,7 @@ fi echo "Creating database user [$DATABASE_USER_NAME] in database [$SQL_DATABASE_NAME]..." sqlcmd -S "$SQL_SERVER_FQDN" \ + -N -C \ -U "$ADMIN_USER" \ -P "$ADMIN_PASSWORD" \ -d "$SQL_DATABASE_NAME" \ @@ -171,6 +173,7 @@ fi echo "Granting roles db_datareader, db_datawriter, db_ddladmin to [$DATABASE_USER_NAME]..." sqlcmd -S "$SQL_SERVER_FQDN" \ + -N -C \ -U "$ADMIN_USER" \ -P "$ADMIN_PASSWORD" \ -d "$SQL_DATABASE_NAME" \ @@ -185,6 +188,7 @@ fi echo "Creating table dbo.Activities in database [$SQL_DATABASE_NAME]..." sqlcmd -S "$SQL_SERVER_FQDN" \ + -N -C \ -U "$ADMIN_USER" \ -P "$ADMIN_PASSWORD" \ -d "$SQL_DATABASE_NAME" \ @@ -200,6 +204,7 @@ fi # Insert data echo "Inserting test data into [Activities] table..." sqlcmd -S "$SQL_SERVER_FQDN" \ + -N -C \ -d "$SQL_DATABASE_NAME" \ -U "$DATABASE_USER_NAME" \ -P "$DATABASE_USER_PASSWORD" \ @@ -228,6 +233,7 @@ fi # Query data echo "Querying test data from [Activities] table..." sqlcmd -S "$SQL_SERVER_FQDN" \ + -N -C \ -d "$SQL_DATABASE_NAME" \ -U "$DATABASE_USER_NAME" \ -P "$DATABASE_USER_PASSWORD" \ From 77ba4c443b23e6a5820d29f60d1836c41e4a087e Mon Sep 17 00:00:00 2001 From: Harsh Mishra Date: Thu, 15 Oct 2026 11:02:37 +0530 Subject: [PATCH 5/5] Authenticate the emulator's GitHub API calls in CI A cosmosdb-nosql-api job failed to create its AKS cluster: the emulator looks up the k3d release on api.github.com, and its anonymous calls hit the runner IP's 60 requests an hour with twenty jobs starting at once. Pass the run's read-only token as LOCALSTACK_GITHUB_API_TOKEN, as the localstack-azure-samples CI does. Claude-Session: https://claude.ai/code/session_01DZfDmzUBPqNAu3yFKyfCNM --- .github/workflows/samples.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/samples.yml b/.github/workflows/samples.yml index 4244e4f..99996f0 100644 --- a/.github/workflows/samples.yml +++ b/.github/workflows/samples.yml @@ -144,6 +144,10 @@ jobs: # lstk forwards LOCALSTACK_-prefixed variables to the emulator. The Azure SQL Database sample # needs the SQL Server EULA accepted, as its README and the localstack-azure-samples CI do LOCALSTACK_MSSQL_ACCEPT_EULA: "Y" + # The emulator looks up the k3d release behind each AKS cluster on api.github.com. Anonymous + # calls share the runner IP's 60 requests an hour, which twenty parallel jobs can exhaust, so pass + # the run's own read-only token, as the localstack-azure-samples CI does + LOCALSTACK_GITHUB_API_TOKEN: ${{ github.token }} run: | lstk start --type azure --non-interactive lstk az start-interception