Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,42 @@ 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 and make them drop the carriage returns Windows programs print:

```bash
export MSYS_NO_PATHCONV=1
Comment thread
paolosalvatori marked this conversation as resolved.
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.
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/<subscription-id>/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.
Comment thread
paolosalvatori marked this conversation as resolved.
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 `$(...)` 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:

- 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, 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.

## 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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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%%|*}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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%%|*}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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%%|*}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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%%|*}"
Expand Down
16 changes: 14 additions & 2 deletions scripts/01-basic-cluster.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -349,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
Expand Down
Loading
Loading