From 5660b1a886413c49173754076c9b7458ed2a8568 Mon Sep 17 00:00:00 2001 From: Paolo Salvatori Date: Fri, 23 Oct 2026 15:51:32 +0200 Subject: [PATCH 1/5] Run the scripts from Git Bash and MSYS2 with MSYS_NO_PATHCONV=1 Git Bash and MSYS2 shells rewrite every argument that looks like an absolute POSIX path before starting a Windows program, so Azure values such as the Cosmos DB partition key path /username and the resource IDs passed to --scope or --vnet-subnet-id reached az as Windows paths (SMF-963). The README now asks Windows users of these shells to export MSYS_NO_PATHCONV=1, which turns the rewriting off, and explains why. With the rewriting off, the local files the scripts hand to Windows programs need their Windows form: the SSH public key of the cluster scripts, the blobs the Blob Storage and managed identity samples upload, the host folder the Azure Files sample mounts into its local container, and the cookie jar of the end-to-end test. A native_path helper in each of these scripts returns that form through cygpath when it exists and the path unchanged otherwise, so Linux, macOS and WSL behave exactly as before. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DhZ2ojsEXd2SGMo7s9uiZh --- README.md | 27 +++++++++++++++++++ .../dotnet/scripts/01-deploy-resources.sh | 13 ++++++++- .../python/scripts/01-deploy-resources.sh | 13 ++++++++- .../dotnet/scripts/03-run-docker-container.sh | 13 ++++++++- .../python/scripts/03-run-docker-container.sh | 13 ++++++++- .../dotnet/scripts/01-deploy-resources.sh | 13 ++++++++- .../python/scripts/01-deploy-resources.sh | 13 ++++++++- .../01-system-assigned-managed-identity.sh | 13 ++++++++- scripts/01-user-assigned-managed-identity.sh | 13 ++++++++- tests/test-vacation-planner.sh | 13 ++++++++- 10 files changed, 135 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 6604e8c..2b1c102 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,33 @@ For more information, see [Azure CLI interception](https://docs.localstack.cloud > The first deployment against the emulator downloads and builds container images, which takes a few minutes. Later deployments reuse them and are much faster. +### Windows: Git Bash and MSYS2 + +The scripts are written for Bash and run unchanged on Linux, macOS and WSL. +They also run on Windows from Git Bash or from an MSYS2 shell (for example MINGW64 or UCRT64), provided you first turn off the automatic path conversion of these shells: + +```bash +export MSYS_NO_PATHCONV=1 +``` + +When Git Bash or an MSYS2 shell starts a Windows program such as the Azure CLI, `kubectl` or `docker`, it rewrites every argument that looks like an absolute POSIX path into a Windows path. +Azure values that only look like paths are rewritten too, and the commands that receive them fail: + +- `--partition-key-path /username` in the Cosmos DB sample reaches `az` as `C:/Program Files/Git/username`, so creating the container fails with an invalid partition key path error. +- Resource IDs such as `/subscriptions//resourceGroups/...`, passed to `--scope`, `--vnet-subnet-id` and similar options, become paths under the shell's installation folder, for example `C:/Program Files/Git/subscriptions/...`. + +With `MSYS_NO_PATHCONV=1` set, these values reach the Azure CLI exactly as written. +The scripts convert the paths of the local files they hand to Windows programs, such as the SSH public key of the cluster scripts or the blobs the samples upload, with `cygpath` when it is available, so they keep working with the variable set. +On Linux and macOS `cygpath` does not exist, and the paths are left unchanged. + +Tips: + +- To set the variable in every new Git Bash window, add `export MSYS_NO_PATHCONV=1` to `~/.bash_profile`, or to `~/.bashrc` if your `.bash_profile` sources it. +- `echo $MSYSTEM` prints a value such as `MINGW64` only in these shells. +- Any value turns the conversion off, `0` included; run `unset MSYS_NO_PATHCONV` to turn it back on. +- WSL runs Linux and needs no variable. +- When you write a script that passes a local file to `az`, `kubectl`, `curl`, `jq`, `python` or `docker`, convert its path with the `native_path` helper the existing scripts define. + ## Create an Azure Kubernetes Service (AKS) cluster Before deploying any sample you must first provision an AKS cluster. Four scripts under [scripts/](scripts/) create it; they all produce the `local-aks-test` cluster the samples expect and differ in how much they provision around it: diff --git a/samples/web-app-blob-storage/dotnet/scripts/01-deploy-resources.sh b/samples/web-app-blob-storage/dotnet/scripts/01-deploy-resources.sh index 82cb220..f9c7c2e 100755 --- a/samples/web-app-blob-storage/dotnet/scripts/01-deploy-resources.sh +++ b/samples/web-app-blob-storage/dotnet/scripts/01-deploy-resources.sh @@ -3,6 +3,17 @@ # Variables source ./00-variables.sh +# On Windows, Git Bash and MSYS2 shells pass a path such as /tmp/... unconverted to Windows programs +# (az, curl, jq, python, docker) when MSYS_NO_PATHCONV=1 is set, as the README asks. cygpath, which +# those shells provide, returns the C:/... form those programs can open; elsewhere the path is unchanged. +native_path() { + if command -v cygpath >/dev/null 2>&1; then + cygpath -m "$1" + else + echo "$1" + fi +} + # Change the current directory to the script's directory cd "$CURRENT_DIR" || exit @@ -131,7 +142,7 @@ SAMPLE_ACTIVITIES=( "2026-01-05-09-05-00-activity.txt|Sunset hike to the Cape lighthouse" "2026-01-05-09-10-00-activity.txt|Cooking class: pasta from scratch" ) -SAMPLE_ACTIVITIES_DIR=$(mktemp -d) +SAMPLE_ACTIVITIES_DIR=$(native_path "$(mktemp -d)") for SAMPLE_ACTIVITY in "${SAMPLE_ACTIVITIES[@]}"; do BLOB_NAME="${SAMPLE_ACTIVITY%%|*}" diff --git a/samples/web-app-blob-storage/python/scripts/01-deploy-resources.sh b/samples/web-app-blob-storage/python/scripts/01-deploy-resources.sh index 82cb220..f9c7c2e 100755 --- a/samples/web-app-blob-storage/python/scripts/01-deploy-resources.sh +++ b/samples/web-app-blob-storage/python/scripts/01-deploy-resources.sh @@ -3,6 +3,17 @@ # Variables source ./00-variables.sh +# On Windows, Git Bash and MSYS2 shells pass a path such as /tmp/... unconverted to Windows programs +# (az, curl, jq, python, docker) when MSYS_NO_PATHCONV=1 is set, as the README asks. cygpath, which +# those shells provide, returns the C:/... form those programs can open; elsewhere the path is unchanged. +native_path() { + if command -v cygpath >/dev/null 2>&1; then + cygpath -m "$1" + else + echo "$1" + fi +} + # Change the current directory to the script's directory cd "$CURRENT_DIR" || exit @@ -131,7 +142,7 @@ SAMPLE_ACTIVITIES=( "2026-01-05-09-05-00-activity.txt|Sunset hike to the Cape lighthouse" "2026-01-05-09-10-00-activity.txt|Cooking class: pasta from scratch" ) -SAMPLE_ACTIVITIES_DIR=$(mktemp -d) +SAMPLE_ACTIVITIES_DIR=$(native_path "$(mktemp -d)") for SAMPLE_ACTIVITY in "${SAMPLE_ACTIVITIES[@]}"; do BLOB_NAME="${SAMPLE_ACTIVITY%%|*}" diff --git a/samples/web-app-file-storage/dotnet/scripts/03-run-docker-container.sh b/samples/web-app-file-storage/dotnet/scripts/03-run-docker-container.sh index 071a1a9..dcc718b 100755 --- a/samples/web-app-file-storage/dotnet/scripts/03-run-docker-container.sh +++ b/samples/web-app-file-storage/dotnet/scripts/03-run-docker-container.sh @@ -3,6 +3,17 @@ # Variables source ./00-variables.sh +# On Windows, Git Bash and MSYS2 shells pass a path such as /tmp/... unconverted to Windows programs +# (az, curl, jq, python, docker) when MSYS_NO_PATHCONV=1 is set, as the README asks. cygpath, which +# those shells provide, returns the C:/... form those programs can open; elsewhere the path is unchanged. +native_path() { + if command -v cygpath >/dev/null 2>&1; then + cygpath -m "$1" + else + echo "$1" + fi +} + # Change the current directory to the script's directory cd "$CURRENT_DIR" || exit @@ -13,7 +24,7 @@ cd "$CURRENT_DIR" || exit # Under the user's cache directory rather than /tmp: the directory has to be world-writable for the # container's non-root user to write to it, and /tmp is shared with every other user on the machine. # A private cache directory is also shared with the Docker daemon by default on Docker Desktop. -ACTIVITIES_HOST_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/${IMAGE_NAME}/activities" +ACTIVITIES_HOST_DIR="$(native_path "${XDG_CACHE_HOME:-$HOME/.cache}/${IMAGE_NAME}/activities")" echo "Creating the local activities directory [$ACTIVITIES_HOST_DIR]..." mkdir -p "$ACTIVITIES_HOST_DIR" diff --git a/samples/web-app-file-storage/python/scripts/03-run-docker-container.sh b/samples/web-app-file-storage/python/scripts/03-run-docker-container.sh index 071a1a9..dcc718b 100755 --- a/samples/web-app-file-storage/python/scripts/03-run-docker-container.sh +++ b/samples/web-app-file-storage/python/scripts/03-run-docker-container.sh @@ -3,6 +3,17 @@ # Variables source ./00-variables.sh +# On Windows, Git Bash and MSYS2 shells pass a path such as /tmp/... unconverted to Windows programs +# (az, curl, jq, python, docker) when MSYS_NO_PATHCONV=1 is set, as the README asks. cygpath, which +# those shells provide, returns the C:/... form those programs can open; elsewhere the path is unchanged. +native_path() { + if command -v cygpath >/dev/null 2>&1; then + cygpath -m "$1" + else + echo "$1" + fi +} + # Change the current directory to the script's directory cd "$CURRENT_DIR" || exit @@ -13,7 +24,7 @@ cd "$CURRENT_DIR" || exit # Under the user's cache directory rather than /tmp: the directory has to be world-writable for the # container's non-root user to write to it, and /tmp is shared with every other user on the machine. # A private cache directory is also shared with the Docker daemon by default on Docker Desktop. -ACTIVITIES_HOST_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/${IMAGE_NAME}/activities" +ACTIVITIES_HOST_DIR="$(native_path "${XDG_CACHE_HOME:-$HOME/.cache}/${IMAGE_NAME}/activities")" echo "Creating the local activities directory [$ACTIVITIES_HOST_DIR]..." mkdir -p "$ACTIVITIES_HOST_DIR" diff --git a/samples/web-app-managed-identity/dotnet/scripts/01-deploy-resources.sh b/samples/web-app-managed-identity/dotnet/scripts/01-deploy-resources.sh index 645a0c1..723f8a8 100755 --- a/samples/web-app-managed-identity/dotnet/scripts/01-deploy-resources.sh +++ b/samples/web-app-managed-identity/dotnet/scripts/01-deploy-resources.sh @@ -3,6 +3,17 @@ # Variables source ./00-variables.sh +# On Windows, Git Bash and MSYS2 shells pass a path such as /tmp/... unconverted to Windows programs +# (az, curl, jq, python, docker) when MSYS_NO_PATHCONV=1 is set, as the README asks. cygpath, which +# those shells provide, returns the C:/... form those programs can open; elsewhere the path is unchanged. +native_path() { + if command -v cygpath >/dev/null 2>&1; then + cygpath -m "$1" + else + echo "$1" + fi +} + # Change the current directory to the script's directory cd "$CURRENT_DIR" || exit @@ -146,7 +157,7 @@ SAMPLE_ACTIVITIES=( "2026-01-05-09-05-00-activity.txt|Sunset hike to the Cape lighthouse" "2026-01-05-09-10-00-activity.txt|Cooking class: pasta from scratch" ) -SAMPLE_ACTIVITIES_DIR=$(mktemp -d) +SAMPLE_ACTIVITIES_DIR=$(native_path "$(mktemp -d)") for SAMPLE_ACTIVITY in "${SAMPLE_ACTIVITIES[@]}"; do BLOB_NAME="${SAMPLE_ACTIVITY%%|*}" diff --git a/samples/web-app-managed-identity/python/scripts/01-deploy-resources.sh b/samples/web-app-managed-identity/python/scripts/01-deploy-resources.sh index 645a0c1..723f8a8 100755 --- a/samples/web-app-managed-identity/python/scripts/01-deploy-resources.sh +++ b/samples/web-app-managed-identity/python/scripts/01-deploy-resources.sh @@ -3,6 +3,17 @@ # Variables source ./00-variables.sh +# On Windows, Git Bash and MSYS2 shells pass a path such as /tmp/... unconverted to Windows programs +# (az, curl, jq, python, docker) when MSYS_NO_PATHCONV=1 is set, as the README asks. cygpath, which +# those shells provide, returns the C:/... form those programs can open; elsewhere the path is unchanged. +native_path() { + if command -v cygpath >/dev/null 2>&1; then + cygpath -m "$1" + else + echo "$1" + fi +} + # Change the current directory to the script's directory cd "$CURRENT_DIR" || exit @@ -146,7 +157,7 @@ SAMPLE_ACTIVITIES=( "2026-01-05-09-05-00-activity.txt|Sunset hike to the Cape lighthouse" "2026-01-05-09-10-00-activity.txt|Cooking class: pasta from scratch" ) -SAMPLE_ACTIVITIES_DIR=$(mktemp -d) +SAMPLE_ACTIVITIES_DIR=$(native_path "$(mktemp -d)") for SAMPLE_ACTIVITY in "${SAMPLE_ACTIVITIES[@]}"; do BLOB_NAME="${SAMPLE_ACTIVITY%%|*}" diff --git a/scripts/01-system-assigned-managed-identity.sh b/scripts/01-system-assigned-managed-identity.sh index 705bd8c..daaee28 100755 --- a/scripts/01-system-assigned-managed-identity.sh +++ b/scripts/01-system-assigned-managed-identity.sh @@ -20,6 +20,17 @@ if ! command -v kubelogin >/dev/null 2>&1; then "(install it with: az aks install-cli). The LocalStack emulator does not need it." fi +# On Windows, Git Bash and MSYS2 shells pass a path such as /tmp/... unconverted to Windows programs +# (az, curl, jq, python, docker) when MSYS_NO_PATHCONV=1 is set, as the README asks. cygpath, which +# those shells provide, returns the C:/... form those programs can open; elsewhere the path is unchanged. +native_path() { + if command -v cygpath >/dev/null 2>&1; then + cygpath -m "$1" + else + echo "$1" + fi +} + # Variables # Azure Kubernetes Service (AKS) cluster @@ -66,7 +77,7 @@ log_analytics_sku="PerGB2018" # Node count, node size, and ssh key location for AKS nodes node_size="Standard_D4ds_v5" # Standard_D4ds_v4 -ssh_key_value="$HOME/.ssh/id_rsa.pub" +ssh_key_value="$(native_path "$HOME/.ssh/id_rsa.pub")" # Windows nodes credentials windows_admin_username="azadmin" diff --git a/scripts/01-user-assigned-managed-identity.sh b/scripts/01-user-assigned-managed-identity.sh index 7e2d4fe..86be2be 100755 --- a/scripts/01-user-assigned-managed-identity.sh +++ b/scripts/01-user-assigned-managed-identity.sh @@ -20,6 +20,17 @@ if ! command -v kubelogin >/dev/null 2>&1; then "(install it with: az aks install-cli). The LocalStack emulator does not need it." fi +# On Windows, Git Bash and MSYS2 shells pass a path such as /tmp/... unconverted to Windows programs +# (az, curl, jq, python, docker) when MSYS_NO_PATHCONV=1 is set, as the README asks. cygpath, which +# those shells provide, returns the C:/... form those programs can open; elsewhere the path is unchanged. +native_path() { + if command -v cygpath >/dev/null 2>&1; then + cygpath -m "$1" + else + echo "$1" + fi +} + # Variables # Azure Kubernetes Service (AKS) cluster @@ -67,7 +78,7 @@ log_analytics_sku="PerGB2018" # Node count, node size, and ssh key location for AKS nodes node_size="Standard_D4ds_v5" # Standard_D4ds_v4 -ssh_key_value="$HOME/.ssh/id_rsa.pub" +ssh_key_value="$(native_path "$HOME/.ssh/id_rsa.pub")" # Windows nodes credentials windows_admin_username="azadmin" diff --git a/tests/test-vacation-planner.sh b/tests/test-vacation-planner.sh index 0bd3c78..294db71 100755 --- a/tests/test-vacation-planner.sh +++ b/tests/test-vacation-planner.sh @@ -15,10 +15,21 @@ LOCAL_PORT=${LOCAL_PORT:-8080} MAX_ATTEMPTS=${MAX_ATTEMPTS:-60} BASE_URL="http://localhost:$LOCAL_PORT" +# On Windows, Git Bash and MSYS2 shells pass a path such as /tmp/... unconverted to Windows programs +# (az, curl, jq, python, docker) when MSYS_NO_PATHCONV=1 is set, as the README asks. cygpath, which +# those shells provide, returns the C:/... form those programs can open; elsewhere the path is unchanged. +native_path() { + if command -v cygpath >/dev/null 2>&1; then + cygpath -m "$1" + else + echo "$1" + fi +} + kubectl rollout status "deployment/$DEPLOYMENT" --namespace "$NAMESPACE" --timeout=600s PF_LOG=$(mktemp) -COOKIES=$(mktemp) +COOKIES=$(native_path "$(mktemp)") kubectl port-forward --namespace "$NAMESPACE" "service/$SERVICE" "$LOCAL_PORT:80" > "$PF_LOG" 2>&1 & PF_PID=$! trap 'kill "$PF_PID" 2>/dev/null || true; rm -f "$PF_LOG" "$COOKIES"' EXIT From bb9901082eddb98f6a9f6d96d60fb7db60a90aa5 Mon Sep 17 00:00:00 2001 From: Paolo Salvatori Date: Fri, 23 Oct 2026 16:42:37 +0200 Subject: [PATCH 2/5] Hand Windows paths to az and docker in the scripts the first pass missed With MSYS_NO_PATHCONV=1, az could not open the SSH public key that scripts/01-basic-cluster.sh and scripts/01-configurable-cluster.sh pass to --ssh-key-value, and docker could not mount the LocalStack CA file the Cosmos DB container scripts write next to themselves. Both now go through native_path, as in the other scripts. The end-to-end test assigns mktemp's result on its own line again, so a mktemp failure still stops it under set -e, and the helper follows the two-space indentation of the files that use it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DhZ2ojsEXd2SGMo7s9uiZh --- .../dotnet/scripts/03-run-docker-container.sh | 13 ++++++++++++- .../python/scripts/03-run-docker-container.sh | 13 ++++++++++++- scripts/01-basic-cluster.sh | 13 ++++++++++++- scripts/01-configurable-cluster.sh | 13 ++++++++++++- scripts/01-system-assigned-managed-identity.sh | 10 +++++----- tests/test-vacation-planner.sh | 13 +++++++------ 6 files changed, 60 insertions(+), 15 deletions(-) diff --git a/samples/web-app-cosmosdb-nosql-api/dotnet/scripts/03-run-docker-container.sh b/samples/web-app-cosmosdb-nosql-api/dotnet/scripts/03-run-docker-container.sh index fb0dd98..bb88222 100755 --- a/samples/web-app-cosmosdb-nosql-api/dotnet/scripts/03-run-docker-container.sh +++ b/samples/web-app-cosmosdb-nosql-api/dotnet/scripts/03-run-docker-container.sh @@ -3,6 +3,17 @@ # Variables source ./00-variables.sh +# On Windows, Git Bash and MSYS2 shells pass a path such as /tmp/... unconverted to Windows programs +# (az, curl, jq, python, docker) when MSYS_NO_PATHCONV=1 is set, as the README asks. cygpath, which +# those shells provide, returns the C:/... form those programs can open; elsewhere the path is unchanged. +native_path() { + if command -v cygpath >/dev/null 2>&1; then + cygpath -m "$1" + else + echo "$1" + fi +} + # Retrieve the document endpoint echo "Retrieving document endpoint for Cosmos DB account [$COSMOSDB_ACCOUNT_NAME]..." AZURECOSMOSDB_ENDPOINT=$(az cosmosdb show \ @@ -61,7 +72,7 @@ if [ -n "$LOCALSTACK_URL" ]; then # on rootless / Docker Desktop / WSL2 setups, /tmp may be outside the daemon's reachable # filesystem — in that case `-v` silently creates an empty directory at the destination # instead of bind-mounting the file, and SSL_CERT_FILE ends up pointing at a dir. - HOST_CA_PATH="$CURRENT_DIR/.localstack-ca.crt" + HOST_CA_PATH="$(native_path "$CURRENT_DIR/.localstack-ca.crt")" # Remove the CA file on script exit (Ctrl-C, normal exit, error) so we don't leave # it lying next to the script after the container stops. trap 'rm -f "$HOST_CA_PATH"' EXIT diff --git a/samples/web-app-cosmosdb-nosql-api/python/scripts/03-run-docker-container.sh b/samples/web-app-cosmosdb-nosql-api/python/scripts/03-run-docker-container.sh index 83238fd..daa7ddc 100755 --- a/samples/web-app-cosmosdb-nosql-api/python/scripts/03-run-docker-container.sh +++ b/samples/web-app-cosmosdb-nosql-api/python/scripts/03-run-docker-container.sh @@ -3,6 +3,17 @@ # Variables source ./00-variables.sh +# On Windows, Git Bash and MSYS2 shells pass a path such as /tmp/... unconverted to Windows programs +# (az, curl, jq, python, docker) when MSYS_NO_PATHCONV=1 is set, as the README asks. cygpath, which +# those shells provide, returns the C:/... form those programs can open; elsewhere the path is unchanged. +native_path() { + if command -v cygpath >/dev/null 2>&1; then + cygpath -m "$1" + else + echo "$1" + fi +} + # Retrieve the document endpoint echo "Retrieving document endpoint for Cosmos DB account [$COSMOSDB_ACCOUNT_NAME]..." AZURECOSMOSDB_ENDPOINT=$(az cosmosdb show \ @@ -61,7 +72,7 @@ if [ -n "$LOCALSTACK_URL" ]; then # on rootless / Docker Desktop / WSL2 setups, /tmp may be outside the daemon's reachable # filesystem — in that case `-v` silently creates an empty directory at the destination # instead of bind-mounting the file, and REQUESTS_CA_BUNDLE ends up pointing at a dir. - HOST_CA_PATH="$CURRENT_DIR/.localstack-ca.crt" + HOST_CA_PATH="$(native_path "$CURRENT_DIR/.localstack-ca.crt")" # Remove the CA file on script exit (Ctrl-C, normal exit, error) so we don't leave # it lying next to the script after the container stops. trap 'rm -f "$HOST_CA_PATH"' EXIT diff --git a/scripts/01-basic-cluster.sh b/scripts/01-basic-cluster.sh index 78db0ad..3d7e5ba 100755 --- a/scripts/01-basic-cluster.sh +++ b/scripts/01-basic-cluster.sh @@ -19,6 +19,17 @@ set -euo pipefail +# On Windows, Git Bash and MSYS2 shells pass a path such as /tmp/... unconverted to Windows programs +# (az, curl, jq, python, docker) when MSYS_NO_PATHCONV=1 is set, as the README asks. cygpath, which +# those shells provide, returns the C:/... form those programs can open; elsewhere the path is unchanged. +native_path() { + if command -v cygpath >/dev/null 2>&1; then + cygpath -m "$1" + else + echo "$1" + fi +} + # Variables: the same names as the other cluster scripts, which every sample relies on # Azure Kubernetes Service (AKS) cluster @@ -61,7 +72,7 @@ max_count=3 max_pods=100 node_size="Standard_D4ds_v5" os_type="Linux" -ssh_key_value="$HOME/.ssh/id_rsa.pub" +ssh_key_value="$(native_path "$HOME/.ssh/id_rsa.pub")" # Command-line options, also settable as DRY_RUN=true and ASSUME_YES=true DRY_RUN=${DRY_RUN:-false} diff --git a/scripts/01-configurable-cluster.sh b/scripts/01-configurable-cluster.sh index 532b4ac..f4f4f5b 100755 --- a/scripts/01-configurable-cluster.sh +++ b/scripts/01-configurable-cluster.sh @@ -12,6 +12,17 @@ set -euo pipefail +# On Windows, Git Bash and MSYS2 shells pass a path such as /tmp/... unconverted to Windows programs +# (az, curl, jq, python, docker) when MSYS_NO_PATHCONV=1 is set, as the README asks. cygpath, which +# those shells provide, returns the C:/... form those programs can open; elsewhere the path is unchanged. +native_path() { + if command -v cygpath >/dev/null 2>&1; then + cygpath -m "$1" + else + echo "$1" + fi +} + # Variables # Azure Kubernetes Service (AKS) cluster @@ -67,7 +78,7 @@ log_analytics_sku="PerGB2018" # Node size and ssh key location for AKS nodes. Without the key file the cluster is created with --no-ssh-key: # az aks create refuses to run with neither. node_size="Standard_D4ds_v5" -ssh_key_value="$HOME/.ssh/id_rsa.pub" +ssh_key_value="$(native_path "$HOME/.ssh/id_rsa.pub")" # Node count variables min_count=1 diff --git a/scripts/01-system-assigned-managed-identity.sh b/scripts/01-system-assigned-managed-identity.sh index daaee28..81d766e 100755 --- a/scripts/01-system-assigned-managed-identity.sh +++ b/scripts/01-system-assigned-managed-identity.sh @@ -24,11 +24,11 @@ fi # (az, curl, jq, python, docker) when MSYS_NO_PATHCONV=1 is set, as the README asks. cygpath, which # those shells provide, returns the C:/... form those programs can open; elsewhere the path is unchanged. native_path() { - if command -v cygpath >/dev/null 2>&1; then - cygpath -m "$1" - else - echo "$1" - fi + if command -v cygpath >/dev/null 2>&1; then + cygpath -m "$1" + else + echo "$1" + fi } # Variables diff --git a/tests/test-vacation-planner.sh b/tests/test-vacation-planner.sh index 294db71..fdeab9d 100755 --- a/tests/test-vacation-planner.sh +++ b/tests/test-vacation-planner.sh @@ -19,17 +19,18 @@ BASE_URL="http://localhost:$LOCAL_PORT" # (az, curl, jq, python, docker) when MSYS_NO_PATHCONV=1 is set, as the README asks. cygpath, which # those shells provide, returns the C:/... form those programs can open; elsewhere the path is unchanged. native_path() { - if command -v cygpath >/dev/null 2>&1; then - cygpath -m "$1" - else - echo "$1" - fi + if command -v cygpath >/dev/null 2>&1; then + cygpath -m "$1" + else + echo "$1" + fi } kubectl rollout status "deployment/$DEPLOYMENT" --namespace "$NAMESPACE" --timeout=600s PF_LOG=$(mktemp) -COOKIES=$(native_path "$(mktemp)") +COOKIES=$(mktemp) +COOKIES=$(native_path "$COOKIES") kubectl port-forward --namespace "$NAMESPACE" "service/$SERVICE" "$LOCAL_PORT:80" > "$PF_LOG" 2>&1 & PF_PID=$! trap 'kill "$PF_PID" 2>/dev/null || true; rm -f "$PF_LOG" "$COOKIES"' EXIT From 780184995c1e2624c1105d2c41a97d41bfa7ffca Mon Sep 17 00:00:00 2001 From: Paolo Salvatori Date: Fri, 23 Oct 2026 17:05:38 +0200 Subject: [PATCH 3/5] Discard curl's responses through the NUL device in Git Bash and MSYS2 With MSYS_NO_PATHCONV=1 these shells no longer turn a /dev/null argument into nul for Windows programs, so a Windows curl given -o /dev/null cannot open its output and exits with code 23 as soon as a response has a body. The end-to-end test, which runs under set -e, would stop at its first form post, and the Gateway API tutorial would report a failed routing check. A null_device helper returns NUL for a Windows curl in these shells and /dev/null everywhere else, MSYS2's own curl included, and the three curl calls that discard a body use it. The README says so, and tells script authors to do the same. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DhZ2ojsEXd2SGMo7s9uiZh --- README.md | 2 ++ tests/test-vacation-planner.sh | 13 ++++++++++++- tutorials/gateway-api/scripts/04-test-routing.sh | 15 +++++++++++++-- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2b1c102..2934734 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ Azure values that only look like paths are rewritten too, and the commands that With `MSYS_NO_PATHCONV=1` set, these values reach the Azure CLI exactly as written. The scripts convert the paths of the local files they hand to Windows programs, such as the SSH public key of the cluster scripts or the blobs the samples upload, with `cygpath` when it is available, so they keep working with the variable set. +They also hand `curl` the Windows `NUL` device instead of `/dev/null` for the responses they discard, because the variable stops these shells from making that conversion too. On Linux and macOS `cygpath` does not exist, and the paths are left unchanged. Tips: @@ -119,6 +120,7 @@ Tips: - Any value turns the conversion off, `0` included; run `unset MSYS_NO_PATHCONV` to turn it back on. - WSL runs Linux and needs no variable. - When you write a script that passes a local file to `az`, `kubectl`, `curl`, `jq`, `python` or `docker`, convert its path with the `native_path` helper the existing scripts define. +- To discard what a Windows program writes to a file, such as `curl -o`, pass it `"$(null_device)"` rather than `/dev/null`, as the existing scripts do. Shell redirections such as `> /dev/null` need no change. ## Create an Azure Kubernetes Service (AKS) cluster diff --git a/tests/test-vacation-planner.sh b/tests/test-vacation-planner.sh index fdeab9d..a55383d 100755 --- a/tests/test-vacation-planner.sh +++ b/tests/test-vacation-planner.sh @@ -26,6 +26,17 @@ native_path() { fi } +# Where curl discards a response body. Git Bash and MSYS2 shells hand a Windows curl /dev/null +# unchanged when MSYS_NO_PATHCONV=1 is set, as the README asks, and it cannot open that path, so it +# gets the NUL device instead; MSYS2's own curl, under /usr/bin, opens /dev/null as on Linux and macOS. +null_device() { + if command -v cygpath >/dev/null 2>&1 && [[ "$(command -v curl)" != /usr/bin/* ]]; then + echo NUL + else + echo /dev/null + fi +} + kubectl rollout status "deployment/$DEPLOYMENT" --namespace "$NAMESPACE" --timeout=600s PF_LOG=$(mktemp) @@ -68,7 +79,7 @@ get_page() { post_form() { local path=$1 field shift - local args=(-sS -m 30 -o /dev/null -w '%{http_code}' -b "$COOKIES" -c "$COOKIES" -X POST) + local args=(-sS -m 30 -o "$(null_device)" -w '%{http_code}' -b "$COOKIES" -c "$COOKIES" -X POST) for field in "$@"; do args+=(--data-urlencode "$field") done diff --git a/tutorials/gateway-api/scripts/04-test-routing.sh b/tutorials/gateway-api/scripts/04-test-routing.sh index 0e52041..4797047 100755 --- a/tutorials/gateway-api/scripts/04-test-routing.sh +++ b/tutorials/gateway-api/scripts/04-test-routing.sh @@ -6,6 +6,17 @@ # A request for the claimed hostname must return 200 from the echo-server; a request for an # unclaimed hostname must NOT return 200 (proving the gateway makes the routing decision). +# Where curl discards a response body. Git Bash and MSYS2 shells hand a Windows curl /dev/null +# unchanged when MSYS_NO_PATHCONV=1 is set, as the README asks, and it cannot open that path, so it +# gets the NUL device instead; MSYS2's own curl, under /usr/bin, opens /dev/null as on Linux and macOS. +null_device() { + if command -v cygpath >/dev/null 2>&1 && [[ "$(command -v curl)" != /usr/bin/* ]]; then + echo NUL + else + echo /dev/null + fi +} + # Variables source ./00-variables.sh @@ -55,7 +66,7 @@ echo "Sending a request for host [$SAMPLE_HOSTNAME] through the gateway..." HTTP_CODE="" ELAPSED=0 while true; do - HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -H "Host: $SAMPLE_HOSTNAME" "http://127.0.0.1:${PORT_FORWARD_LOCAL_PORT}/" 2>/dev/null) + HTTP_CODE=$(curl -s -o "$(null_device)" -w '%{http_code}' -H "Host: $SAMPLE_HOSTNAME" "http://127.0.0.1:${PORT_FORWARD_LOCAL_PORT}/" 2>/dev/null) if [[ "$HTTP_CODE" == "200" ]]; then break fi @@ -75,7 +86,7 @@ fi # An unclaimed hostname must not reach the backend UNCLAIMED_HOSTNAME="unclaimed.local" echo "Sending a request for the unclaimed host [$UNCLAIMED_HOSTNAME] through the gateway..." -HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -H "Host: $UNCLAIMED_HOSTNAME" "http://127.0.0.1:${PORT_FORWARD_LOCAL_PORT}/" 2>/dev/null) +HTTP_CODE=$(curl -s -o "$(null_device)" -w '%{http_code}' -H "Host: $UNCLAIMED_HOSTNAME" "http://127.0.0.1:${PORT_FORWARD_LOCAL_PORT}/" 2>/dev/null) if [[ "$HTTP_CODE" != "200" ]]; then echo "PASS: unclaimed host [$UNCLAIMED_HOSTNAME] returned HTTP [$HTTP_CODE] (not routed to the backend)" else From 7dc9d0baf053098e5b234d0566654cf34a4007e0 Mon Sep 17 00:00:00 2001 From: Paolo Salvatori Date: Fri, 23 Oct 2026 17:27:31 +0200 Subject: [PATCH 4/5] Tell Git Bash and MSYS2 users to drop the carriage returns of Windows programs Windows programs such as the Azure CLI and jq end every line with a carriage return, which Bash keeps in $(...) and read, so a name or an ID read from az --output tsv carries a trailing \r. In the Windows CI run, the App Configuration sample's private endpoint failed twice with ResourceNotFound for a key vault that existed, its name ending in that \r. MSYS2's bash, which Git Bash ships, drops carriage returns from command substitution and read when its igncr option is on, and an exported SHELLOPTS passes the option to the scripts, so the README asks for set -o igncr and export SHELLOPTS next to MSYS_NO_PATHCONV=1. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DhZ2ojsEXd2SGMo7s9uiZh --- README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2934734..504ad20 100644 --- a/README.md +++ b/README.md @@ -96,10 +96,12 @@ For more information, see [Azure CLI interception](https://docs.localstack.cloud ### Windows: Git Bash and MSYS2 The scripts are written for Bash and run unchanged on Linux, macOS and WSL. -They also run on Windows from Git Bash or from an MSYS2 shell (for example MINGW64 or UCRT64), provided you first turn off the automatic path conversion of these shells: +They also run on Windows from Git Bash or from an MSYS2 shell (for example MINGW64 or UCRT64), provided you first turn off the automatic path conversion of these shells and make them drop the carriage returns Windows programs print: ```bash export MSYS_NO_PATHCONV=1 +set -o igncr +export SHELLOPTS ``` When Git Bash or an MSYS2 shell starts a Windows program such as the Azure CLI, `kubectl` or `docker`, it rewrites every argument that looks like an absolute POSIX path into a Windows path. @@ -113,12 +115,16 @@ The scripts convert the paths of the local files they hand to Windows programs, They also hand `curl` the Windows `NUL` device instead of `/dev/null` for the responses they discard, because the variable stops these shells from making that conversion too. On Linux and macOS `cygpath` does not exist, and the paths are left unchanged. +Windows programs such as the Azure CLI and `jq` also end every line they print with a carriage return, which Bash keeps when a script captures their output with `$(...)` or `read`. +A name or an ID read from `az ... --output tsv` then carries an invisible `\r`, and the next command that receives it fails, for example with `ResourceNotFound` for a resource that exists. +`set -o igncr` makes these shells drop carriage returns from captured output, and `export SHELLOPTS` passes the option on to the scripts you start. + Tips: -- To set the variable in every new Git Bash window, add `export MSYS_NO_PATHCONV=1` to `~/.bash_profile`, or to `~/.bashrc` if your `.bash_profile` sources it. +- To apply these settings in every new Git Bash window, add the three lines to `~/.bash_profile`, or to `~/.bashrc` if your `.bash_profile` sources it. - `echo $MSYSTEM` prints a value such as `MINGW64` only in these shells. -- Any value turns the conversion off, `0` included; run `unset MSYS_NO_PATHCONV` to turn it back on. -- WSL runs Linux and needs no variable. +- Any value turns the conversion off, `0` included; run `unset MSYS_NO_PATHCONV` to turn it back on, and `set +o igncr` to keep carriage returns again. +- WSL runs Linux and needs none of these settings. - When you write a script that passes a local file to `az`, `kubectl`, `curl`, `jq`, `python` or `docker`, convert its path with the `native_path` helper the existing scripts define. - To discard what a Windows program writes to a file, such as `curl -o`, pass it `"$(null_device)"` rather than `/dev/null`, as the existing scripts do. Shell redirections such as `> /dev/null` need no change. From 803253ad83543a2f7503ec717c03a128de807706 Mon Sep 17 00:00:00 2001 From: Paolo Salvatori Date: Fri, 23 Oct 2026 18:44:54 +0200 Subject: [PATCH 5/5] Strip the carriage return of the Azure CLI from the arrays mapfile fills On Windows the Azure CLI ends every line with a carriage return, and bash's igncr option drops it from $(...) and read but not from mapfile. The App Configuration samples read several fields at once with mapfile, so in the Windows CI the key vault ID kept its \r and the private endpoint failed with ResourceNotFound, and the AKS sample read the cluster's OIDC and workload identity flags as "true\r". Every mapfile that reads az output, in those samples and in the AKS cluster scripts, now reads it through tr -d '\r', which changes nothing on Linux and macOS, and the README says igncr does not cover mapfile. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DhZ2ojsEXd2SGMo7s9uiZh --- README.md | 3 ++- .../dotnet/scripts/01-deploy-resources.sh | 3 ++- .../python/scripts/01-deploy-resources.sh | 3 ++- scripts/01-basic-cluster.sh | 3 ++- scripts/01-configurable-cluster.sh | 9 ++++++--- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 504ad20..5559b9d 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,8 @@ On Linux and macOS `cygpath` does not exist, and the paths are left unchanged. Windows programs such as the Azure CLI and `jq` also end every line they print with a carriage return, which Bash keeps when a script captures their output with `$(...)` or `read`. A name or an ID read from `az ... --output tsv` then carries an invisible `\r`, and the next command that receives it fails, for example with `ResourceNotFound` for a resource that exists. -`set -o igncr` makes these shells drop carriage returns from captured output, and `export SHELLOPTS` passes the option on to the scripts you start. +`set -o igncr` makes these shells drop carriage returns from `$(...)` and `read`, and `export SHELLOPTS` passes the option on to the scripts you start. +`mapfile` keeps them even then, so the scripts that fill an array from the Azure CLI strip them with `tr -d '\r'`. Tips: diff --git a/samples/web-app-app-configuration/dotnet/scripts/01-deploy-resources.sh b/samples/web-app-app-configuration/dotnet/scripts/01-deploy-resources.sh index 6658d3d..44a8122 100755 --- a/samples/web-app-app-configuration/dotnet/scripts/01-deploy-resources.sh +++ b/samples/web-app-app-configuration/dotnet/scripts/01-deploy-resources.sh @@ -787,12 +787,13 @@ echo "Checking that the [$AKS_CLUSTER_NAME] cluster has the OIDC issuer and Micr # `--output tsv` prints one value per line for a list of scalars, so the two flags are read into an # array rather than split on a tab. An absent securityProfile leaves the second element unset, which the # comparison below treats as disabled. +# tr drops the carriage return the Azure CLI ends each line with on Windows: mapfile keeps it, even with bash's igncr option mapfile -t CLUSTER_FEATURES < <(az aks show \ --name "$AKS_CLUSTER_NAME" \ --resource-group "$RESOURCE_GROUP_NAME" \ --query "[oidcIssuerProfile.enabled, securityProfile.workloadIdentity.enabled]" \ --output tsv \ - --only-show-errors 2>/dev/null) + --only-show-errors 2>/dev/null | tr -d '\r') if [[ ${#CLUSTER_FEATURES[@]} == 0 ]]; then echo "Failed to read the [$AKS_CLUSTER_NAME] cluster in the [$RESOURCE_GROUP_NAME] resource group." diff --git a/samples/web-app-app-configuration/python/scripts/01-deploy-resources.sh b/samples/web-app-app-configuration/python/scripts/01-deploy-resources.sh index 6658d3d..44a8122 100755 --- a/samples/web-app-app-configuration/python/scripts/01-deploy-resources.sh +++ b/samples/web-app-app-configuration/python/scripts/01-deploy-resources.sh @@ -787,12 +787,13 @@ echo "Checking that the [$AKS_CLUSTER_NAME] cluster has the OIDC issuer and Micr # `--output tsv` prints one value per line for a list of scalars, so the two flags are read into an # array rather than split on a tab. An absent securityProfile leaves the second element unset, which the # comparison below treats as disabled. +# tr drops the carriage return the Azure CLI ends each line with on Windows: mapfile keeps it, even with bash's igncr option mapfile -t CLUSTER_FEATURES < <(az aks show \ --name "$AKS_CLUSTER_NAME" \ --resource-group "$RESOURCE_GROUP_NAME" \ --query "[oidcIssuerProfile.enabled, securityProfile.workloadIdentity.enabled]" \ --output tsv \ - --only-show-errors 2>/dev/null) + --only-show-errors 2>/dev/null | tr -d '\r') if [[ ${#CLUSTER_FEATURES[@]} == 0 ]]; then echo "Failed to read the [$AKS_CLUSTER_NAME] cluster in the [$RESOURCE_GROUP_NAME] resource group." diff --git a/scripts/01-basic-cluster.sh b/scripts/01-basic-cluster.sh index 3d7e5ba..1c1c6ee 100755 --- a/scripts/01-basic-cluster.sh +++ b/scripts/01-basic-cluster.sh @@ -360,12 +360,13 @@ check_existing_cluster() { fi cluster_exists=true + # tr drops the carriage return the Azure CLI ends each line with on Windows: mapfile keeps it, even with bash's igncr option mapfile -t fields < <(query "" az aks show \ --name "$aks_cluster_name" \ --resource-group "$resource_group_name" \ --query "[identity.type, networkProfile.networkPlugin, networkProfile.networkPluginMode, networkProfile.networkPolicy, networkProfile.networkDataplane, agentPoolProfiles[?name=='$system_node_pool_name'] | [0].vnetSubnetId, agentPoolProfiles[?name=='$system_node_pool_name'] | [0].enableAutoScaling, addonProfiles.azureKeyvaultSecretsProvider.enabled, addonProfiles.omsagent.enabled, oidcIssuerProfile.enabled, securityProfile.workloadIdentity.enabled, workloadAutoScalerProfile.keda.enabled, ingressProfile.gatewayApi.installation, aadProfile.managed, aadProfile.enableAzureRbac, networkProfile.advancedNetworking.enabled, length(agentPoolProfiles[?name=='$user_node_pool_name']), length(agentPoolProfiles[?name=='$user_node_pool_name'] | [0].availabilityZones || \`[]\`), storageProfile.fileCsiDriver.enabled, storageProfile.diskCsiDriver.enabled]" \ --output tsv \ - --only-show-errors) + --only-show-errors | tr -d '\r') ((${#fields[@]} == 20)) || die "Failed to read the configuration of the existing [$aks_cluster_name] AKS cluster" case ${fields[0]} in diff --git a/scripts/01-configurable-cluster.sh b/scripts/01-configurable-cluster.sh index f4f4f5b..1830afd 100755 --- a/scripts/01-configurable-cluster.sh +++ b/scripts/01-configurable-cluster.sh @@ -712,12 +712,13 @@ compare_admin_groups() { # count, and its node count when the autoscaler is off compare_pool_size() { local size=() autoscaler + # tr drops the carriage return the Azure CLI ends each line with on Windows: mapfile keeps it, even with bash's igncr option mapfile -t size < <(query "" az aks show \ --name "$aks_cluster_name" \ --resource-group "$resource_group_name" \ --query "agentPoolProfiles[?name=='$2'] | [0].[count, minCount, maxCount, enableAutoScaling]" \ --output tsv \ - --only-show-errors) + --only-show-errors | tr -d '\r') ((${#size[@]} == 4)) || die "Failed to read the size of the [$2] node pool of the existing [$aks_cluster_name] AKS cluster" autoscaler=$(cluster_bool "${size[3]}") if [[ $autoscaler != "$(yes_no "$ENABLE_CLUSTER_AUTOSCALER")" ]]; then @@ -740,12 +741,13 @@ check_existing_cluster() { fi cluster_exists=true + # tr drops the carriage return the Azure CLI ends each line with on Windows: mapfile keeps it, even with bash's igncr option mapfile -t fields < <(query "" az aks show \ --name "$aks_cluster_name" \ --resource-group "$resource_group_name" \ --query "[identity.type, networkProfile.networkPlugin, networkProfile.networkPluginMode, networkProfile.networkPolicy, networkProfile.networkDataplane, agentPoolProfiles[?name=='$system_node_pool_name'] | [0].vnetSubnetId, agentPoolProfiles[?name=='$system_node_pool_name'] | [0].enableAutoScaling, addonProfiles.azureKeyvaultSecretsProvider.enabled, addonProfiles.omsagent.enabled, oidcIssuerProfile.enabled, securityProfile.workloadIdentity.enabled, workloadAutoScalerProfile.keda.enabled, ingressProfile.gatewayApi.installation, aadProfile.managed, aadProfile.enableAzureRbac, networkProfile.advancedNetworking.enabled, length(agentPoolProfiles[?name=='$user_node_pool_name']), length(agentPoolProfiles[?name=='$user_node_pool_name'] | [0].availabilityZones || \`[]\`), storageProfile.fileCsiDriver.enabled, storageProfile.diskCsiDriver.enabled]" \ --output tsv \ - --only-show-errors) + --only-show-errors | tr -d '\r') ((${#fields[@]} == 20)) || die "Failed to read the configuration of the existing [$aks_cluster_name] AKS cluster" case ${fields[0]} in @@ -952,13 +954,14 @@ ensure_subnet() { # from a virtual network, so the Azure Files CSI driver needs this endpoint to mount an NFS volume on a node. ensure_storage_service_endpoint() { local subnet=$1 current_service_endpoints=() service + # tr drops the carriage return the Azure CLI ends each line with on Windows: mapfile keeps it, even with bash's igncr option mapfile -t current_service_endpoints < <(query "" az network vnet subnet show \ --name "$subnet" \ --vnet-name "$virtual_network_name" \ --resource-group "$resource_group_name" \ --query "serviceEndpoints[].service" \ --output tsv \ - --only-show-errors 2>/dev/null) + --only-show-errors 2>/dev/null | tr -d '\r') for service in "${current_service_endpoints[@]}"; do if [[ $service == Microsoft.Storage ]]; then log "The [Microsoft.Storage] service endpoint is already enabled on the [$subnet] subnet"