From 3fc9e04e484e427a54a7b9551893121f950d55fc Mon Sep 17 00:00:00 2001 From: Paolo Salvatori Date: Wed, 11 Nov 2026 09:22:44 +0100 Subject: [PATCH] Seed the Cosmos DB samples' data in 01-deploy-resources.sh The SQL Database, MySQL, PostgreSQL, Blob Storage and Managed Identity samples seed sample activities when 01-deploy-resources.sh creates their data store, while the Cosmos DB for NoSQL and Cosmos DB for MongoDB samples started with an empty list. Their Python and .NET 01-deploy-resources.sh now insert the same nine activities the database samples seed, idempotently, and query them back: - NoSQL: items written through the Cosmos DB REST API with curl, each request signed with the primary master key (openssl), since the Azure CLI has no command for items. A 409 Conflict means the item already exists. - MongoDB: upserts with mongosh that set the fields only on insert. Testing on Azure showed that az cosmosdb create --server-version 7.0 creates a 3.6 account: the Azure CLI sends the version as a ServerVersion property, which the service ignores. PyMongo 4.17 and MongoDB.Driver 3.11, which the MongoDB sample's apps use, and mongosh refuse a 3.6 server, so the sample did not work on Azure. 01-deploy-resources.sh now sets the serverVersion property itself with az rest when the account reports another version, waits for the upgrade, and waits until the MongoDB endpoint accepts connections, which can lag several minutes behind Azure Resource Manager. The Samples CI installs mongosh on the Linux runners for the MongoDB sample; the Windows runner image already ships it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FvNuJLfY2KkMwaymfFnz7R --- .github/workflows/run-samples.yml | 15 ++ .../dotnet/README.md | 2 +- .../dotnet/scripts/01-deploy-resources.sh | 179 ++++++++++++++++++ .../python/README.md | 2 +- .../python/scripts/01-deploy-resources.sh | 179 ++++++++++++++++++ .../dotnet/README.md | 2 +- .../dotnet/scripts/01-deploy-resources.sh | 107 +++++++++++ .../python/README.md | 2 +- .../python/scripts/01-deploy-resources.sh | 107 +++++++++++ 9 files changed, 591 insertions(+), 4 deletions(-) diff --git a/.github/workflows/run-samples.yml b/.github/workflows/run-samples.yml index 7d0720c..4371cd6 100644 --- a/.github/workflows/run-samples.yml +++ b/.github/workflows/run-samples.yml @@ -307,6 +307,21 @@ jobs: unzip -o -q -j "$RUNNER_TEMP/sqlcmd.zip" sqlcmd.exe -d "$RUNNER_TEMP/bin" sqlcmd --version + - name: Install mongosh + # The Azure Cosmos DB for MongoDB sample seeds its collection with mongosh, which the Linux runner image + # does not ship (the Windows runner image does). The release and its SHA-256 are pinned. + if: matrix.os == 'linux' && matrix.sample == 'web-app-cosmosdb-mongodb-api' + env: + MONGOSH_VERSION: 2.14.1 + MONGOSH_SHA256: 8d24940ebe3675959b518fd41ea16b4e2182d6a75c09777db09cfa64e18834a1 + run: | + curl --silent --show-error --fail --location --retry 3 --output "$RUNNER_TEMP/mongosh.tgz" \ + "https://github.com/mongodb-js/mongosh/releases/download/v${MONGOSH_VERSION}/mongosh-${MONGOSH_VERSION}-linux-x64.tgz" + echo "${MONGOSH_SHA256} $RUNNER_TEMP/mongosh.tgz" | sha256sum --check + tar -xzf "$RUNNER_TEMP/mongosh.tgz" -C "$RUNNER_TEMP" + echo "$RUNNER_TEMP/mongosh-${MONGOSH_VERSION}-linux-x64/bin" >> "$GITHUB_PATH" + "$RUNNER_TEMP/mongosh-${MONGOSH_VERSION}-linux-x64/bin/mongosh" --version + - 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 diff --git a/samples/web-app-cosmosdb-mongodb-api/dotnet/README.md b/samples/web-app-cosmosdb-mongodb-api/dotnet/README.md index 356b0ab..703ad9e 100644 --- a/samples/web-app-cosmosdb-mongodb-api/dotnet/README.md +++ b/samples/web-app-cosmosdb-mongodb-api/dotnet/README.md @@ -39,7 +39,7 @@ cd scripts | File | Description | | ---- | ----------- | | [`00-variables.sh`](scripts/00-variables.sh) | Defines the variables shared across the other scripts (resource names, image tag, Cosmos DB account/database/collection, Kubernetes namespace, …). The other scripts load these values by sourcing this file. | -| [`01-deploy-resources.sh`](scripts/01-deploy-resources.sh) | Deploys the Azure resources used by this sample: the resource group, the [Azure Container Registry (ACR)](https://learn.microsoft.com/en-us/azure/container-registry/container-registry-intro), the [Azure Cosmos DB for MongoDB](https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/overview) account, the `sampledb` database, and the `activities` collection (with its shard key and indexes). | +| [`01-deploy-resources.sh`](scripts/01-deploy-resources.sh) | Deploys the Azure resources used by this sample: the resource group, the [Azure Container Registry (ACR)](https://learn.microsoft.com/en-us/azure/container-registry/container-registry-intro), the [Azure Cosmos DB for MongoDB](https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/overview) account, the `sampledb` database, and the `activities` collection (with its shard key and indexes), which it also seeds with sample data. Requires `mongosh` on the host. The script also sets the account's MongoDB server version (`MONGODB_API_VERSION`) itself, because `az cosmosdb create --server-version` does not apply it and the account would otherwise run version 3.6, which the app's MongoDB driver refuses. | | [`02-build-docker-image.sh`](scripts/02-build-docker-image.sh) | Builds the Docker image for the web app from the [`src/`](src/) folder. | | [`03-run-docker-container.sh`](scripts/03-run-docker-container.sh) | Runs the web app in a local Docker container (no Kubernetes) to validate that it starts and connects to the database as expected. | | [`04-push-docker-image.sh`](scripts/04-push-docker-image.sh) | Tags and pushes the Docker image to the Azure Container Registry, on Azure or in the LocalStack emulator. | diff --git a/samples/web-app-cosmosdb-mongodb-api/dotnet/scripts/01-deploy-resources.sh b/samples/web-app-cosmosdb-mongodb-api/dotnet/scripts/01-deploy-resources.sh index fe5199f..49bcde5 100755 --- a/samples/web-app-cosmosdb-mongodb-api/dotnet/scripts/01-deploy-resources.sh +++ b/samples/web-app-cosmosdb-mongodb-api/dotnet/scripts/01-deploy-resources.sh @@ -86,6 +86,83 @@ else echo "Cosmos DB account [$COSMOSDB_ACCOUNT_NAME] already exists in the [$RESOURCE_GROUP_NAME] resource group" fi +# Make sure the account runs the MongoDB server version the sample asks for. The Azure CLI sends the +# --server-version of az cosmosdb create and az cosmosdb update as a ServerVersion property, which the service +# ignores, so the account keeps the default server version, 3.6, which the MongoDB drivers of the app and of +# mongosh refuse. The script sets the serverVersion property itself, with the API version the Azure CLI uses, +# and waits for the upgrade, which takes about a minute. +echo "Retrieving the MongoDB server version of Cosmos DB account [$COSMOSDB_ACCOUNT_NAME]..." +SERVER_VERSION=$(az cosmosdb show \ + --name "$COSMOSDB_ACCOUNT_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --query "apiProperties.serverVersion" \ + --output tsv \ + --only-show-errors) + +if [[ -n $SERVER_VERSION ]]; then + echo "Cosmos DB account [$COSMOSDB_ACCOUNT_NAME] runs MongoDB server version [$SERVER_VERSION]" +else + echo "Failed to retrieve the MongoDB server version of Cosmos DB account [$COSMOSDB_ACCOUNT_NAME]" + exit 1 +fi + +if [[ "$SERVER_VERSION" != "$MONGODB_API_VERSION" ]]; then + echo "Retrieving the resource id of Cosmos DB account [$COSMOSDB_ACCOUNT_NAME]..." + COSMOSDB_ACCOUNT_ID=$(az cosmosdb show \ + --name "$COSMOSDB_ACCOUNT_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --query "id" \ + --output tsv \ + --only-show-errors) + + if [[ -n $COSMOSDB_ACCOUNT_ID ]]; then + echo "Resource id of Cosmos DB account [$COSMOSDB_ACCOUNT_NAME] retrieved successfully" + else + echo "Failed to retrieve the resource id of Cosmos DB account [$COSMOSDB_ACCOUNT_NAME]" + exit 1 + fi + + echo "Upgrading Cosmos DB account [$COSMOSDB_ACCOUNT_NAME] to MongoDB server version [$MONGODB_API_VERSION]..." + az rest \ + --method patch \ + --url "${COSMOSDB_ACCOUNT_ID}?api-version=2026-03-15" \ + --body "{\"properties\": {\"apiProperties\": {\"serverVersion\": \"$MONGODB_API_VERSION\"}}}" \ + --only-show-errors 1>/dev/null + + if [[ $? != 0 ]]; then + echo "Failed to upgrade Cosmos DB account [$COSMOSDB_ACCOUNT_NAME] to MongoDB server version [$MONGODB_API_VERSION]" + exit 1 + fi + + for ATTEMPT in $(seq 1 60); do + SERVER_VERSION=$(az cosmosdb show \ + --name "$COSMOSDB_ACCOUNT_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --query "apiProperties.serverVersion" \ + --output tsv \ + --only-show-errors) + PROVISIONING_STATE=$(az cosmosdb show \ + --name "$COSMOSDB_ACCOUNT_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --query "provisioningState" \ + --output tsv \ + --only-show-errors) + + if [[ "$SERVER_VERSION" == "$MONGODB_API_VERSION" && "$PROVISIONING_STATE" == "Succeeded" ]]; then + break + fi + echo "Waiting for the upgrade of Cosmos DB account [$COSMOSDB_ACCOUNT_NAME] (attempt $ATTEMPT of 60)..." + sleep 10 + done + + if [[ "$SERVER_VERSION" == "$MONGODB_API_VERSION" && "$PROVISIONING_STATE" == "Succeeded" ]]; then + echo "Cosmos DB account [$COSMOSDB_ACCOUNT_NAME] successfully upgraded to MongoDB server version [$MONGODB_API_VERSION]" + else + echo "Failed to upgrade Cosmos DB account [$COSMOSDB_ACCOUNT_NAME]: it still runs MongoDB server version [$SERVER_VERSION]" + exit 1 + fi +fi + # Create the MongoDB database echo "Checking if MongoDB database [$COSMOSDB_DATABASE_NAME] exists in account [$COSMOSDB_ACCOUNT_NAME]..." az cosmosdb mongodb database show \ @@ -159,4 +236,106 @@ else exit 1 fi +# Insert the sample activities, so the app does not start with an empty list. Each activity is a document +# {_id, username, activity, timestamp} of the user the app shows, the shape the app itself reads and writes; these +# are the activities the SQL Database, MySQL and PostgreSQL samples seed. A document id is the MD5 of the user and +# a fixed name, and each upsert sets the fields only when it creates the document, so a re-run inserts only the +# missing activities and leaves the others as they are. The filter of each upsert holds the shard key (username), +# which an update of a sharded collection requires. +# mongosh must be available on the host machine. +if ! command -v mongosh &>/dev/null; then + echo "mongosh is not installed on the host. Install the MongoDB Shell (mongosh) and re-run." + exit 1 +fi + +# After an upgrade, the MongoDB endpoint of the account can go on serving the old server version for several +# minutes after Azure Resource Manager reports the new one, and mongosh refuses a server older than 4.2. Wait until +# mongosh connects; each attempt that fails takes about 30 seconds, the time mongosh looks for a suitable server. +echo "Checking that the MongoDB endpoint of Cosmos DB account [$COSMOSDB_ACCOUNT_NAME] accepts mongosh connections..." +MONGOSH_CONNECTED=false +for ATTEMPT in $(seq 1 30); do + CONNECTION_ERROR=$(mongosh "$COSMOSDB_CONNECTION_STRING" --quiet --eval 'db.hello().ok' 2>&1 1>/dev/null) + + if [[ $? == 0 ]]; then + MONGOSH_CONNECTED=true + break + fi + echo "The MongoDB endpoint does not accept mongosh connections yet (attempt $ATTEMPT of 30): $CONNECTION_ERROR" + sleep 10 +done + +if [[ $MONGOSH_CONNECTED == true ]]; then + echo "The MongoDB endpoint of Cosmos DB account [$COSMOSDB_ACCOUNT_NAME] accepts mongosh connections" +else + echo "Failed to connect to the MongoDB endpoint of Cosmos DB account [$COSMOSDB_ACCOUNT_NAME] with mongosh" + exit 1 +fi + +SAMPLE_ACTIVITIES=( + "pisa|Visit the Leaning Tower in Pisa" + "volterra|Explore Etruscan walls in Volterra" + "san_gimignano|Climb Torre Grossa in San Gimignano" + "siena|Walk across Piazza del Campo in Siena" + "montalcino|Taste Brunello wine in Montalcino" + "pienza|Sample Pecorino cheese in Pienza" + "florence|Admire Michelangelo's David in Florence" + "viareggio_beach|Relax by the beach in Viareggio" + "viareggio_promenade|Stroll along the Viareggio promenade" +) + +# mongosh reads the values from its environment, so no activity needs quoting in the script +echo "Inserting the sample activities of [$LOGIN_NAME] into collection [$COSMOSDB_COLLECTION_NAME]..." +SEED_DATABASE="$COSMOSDB_DATABASE_NAME" \ + SEED_COLLECTION="$COSMOSDB_COLLECTION_NAME" \ + SEED_USERNAME="$LOGIN_NAME" \ + SEED_TIMESTAMP="$(date '+%Y-%m-%dT%H:%M:%S.000000')" \ + SEED_ACTIVITIES="$(printf '%s\n' "${SAMPLE_ACTIVITIES[@]}")" \ + mongosh "$COSMOSDB_CONNECTION_STRING" --quiet --eval ' + const crypto = require("crypto"); + const env = process.env; + const collection = db.getSiblingDB(env.SEED_DATABASE).getCollection(env.SEED_COLLECTION); + const operations = env.SEED_ACTIVITIES.trim().split("\n").map((line) => { + const separator = line.indexOf("|"); + const name = line.slice(0, separator); + const activity = line.slice(separator + 1); + const id = crypto.createHash("md5").update(`${env.SEED_USERNAME}_${name}_seed`).digest("hex"); + return { + updateOne: { + filter: { _id: id, username: env.SEED_USERNAME }, + update: { $setOnInsert: { activity: activity, timestamp: env.SEED_TIMESTAMP } }, + upsert: true + } + }; + }); + const result = collection.bulkWrite(operations, { ordered: false }); + print(`${result.upsertedCount} activities inserted, ${result.matchedCount} already present`); + ' + +if [ $? -eq 0 ]; then + echo "Sample activities inserted successfully into collection [$COSMOSDB_COLLECTION_NAME]." +else + echo "Failed to insert the sample activities into collection [$COSMOSDB_COLLECTION_NAME]." + exit 1 +fi + +# Query the activities of the user back +echo "Querying the activities of [$LOGIN_NAME] from collection [$COSMOSDB_COLLECTION_NAME]..." +SEED_DATABASE="$COSMOSDB_DATABASE_NAME" \ + SEED_COLLECTION="$COSMOSDB_COLLECTION_NAME" \ + SEED_USERNAME="$LOGIN_NAME" \ + mongosh "$COSMOSDB_CONNECTION_STRING" --quiet --eval ' + const env = process.env; + db.getSiblingDB(env.SEED_DATABASE) + .getCollection(env.SEED_COLLECTION) + .find({ username: env.SEED_USERNAME }, { _id: 0, activity: 1 }) + .forEach((document) => print(` - ${document.activity}`)); + ' + +if [ $? -eq 0 ]; then + echo "Activities of [$LOGIN_NAME] queried successfully." +else + echo "Failed to query the activities of [$LOGIN_NAME]." + exit 1 +fi + export COSMOSDB_CONNECTION_STRING diff --git a/samples/web-app-cosmosdb-mongodb-api/python/README.md b/samples/web-app-cosmosdb-mongodb-api/python/README.md index 48a4cea..b5d4fdd 100644 --- a/samples/web-app-cosmosdb-mongodb-api/python/README.md +++ b/samples/web-app-cosmosdb-mongodb-api/python/README.md @@ -39,7 +39,7 @@ cd scripts | File | Description | | ---- | ----------- | | [`00-variables.sh`](scripts/00-variables.sh) | Defines the variables shared across the other scripts (resource names, image tag, Cosmos DB account/database/collection, Kubernetes namespace, …). The other scripts load these values by sourcing this file. | -| [`01-deploy-resources.sh`](scripts/01-deploy-resources.sh) | Deploys the Azure resources used by this sample: the resource group, the [Azure Container Registry (ACR)](https://learn.microsoft.com/en-us/azure/container-registry/container-registry-intro), the [Azure Cosmos DB for MongoDB](https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/overview) account, the `sampledb` database, and the `activities` collection (with its shard key and indexes). | +| [`01-deploy-resources.sh`](scripts/01-deploy-resources.sh) | Deploys the Azure resources used by this sample: the resource group, the [Azure Container Registry (ACR)](https://learn.microsoft.com/en-us/azure/container-registry/container-registry-intro), the [Azure Cosmos DB for MongoDB](https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/overview) account, the `sampledb` database, and the `activities` collection (with its shard key and indexes), which it also seeds with sample data. Requires `mongosh` on the host. The script also sets the account's MongoDB server version (`MONGODB_API_VERSION`) itself, because `az cosmosdb create --server-version` does not apply it and the account would otherwise run version 3.6, which the app's MongoDB driver refuses. | | [`02-build-docker-image.sh`](scripts/02-build-docker-image.sh) | Builds the Docker image for the web app from the [`src/`](src/) folder. | | [`03-run-docker-container.sh`](scripts/03-run-docker-container.sh) | Runs the web app in a local Docker container (no Kubernetes) to validate that it starts and connects to the database as expected. | | [`04-push-docker-image.sh`](scripts/04-push-docker-image.sh) | Tags and pushes the Docker image to the Azure Container Registry, on Azure or in the LocalStack emulator. | diff --git a/samples/web-app-cosmosdb-mongodb-api/python/scripts/01-deploy-resources.sh b/samples/web-app-cosmosdb-mongodb-api/python/scripts/01-deploy-resources.sh index fe5199f..49bcde5 100755 --- a/samples/web-app-cosmosdb-mongodb-api/python/scripts/01-deploy-resources.sh +++ b/samples/web-app-cosmosdb-mongodb-api/python/scripts/01-deploy-resources.sh @@ -86,6 +86,83 @@ else echo "Cosmos DB account [$COSMOSDB_ACCOUNT_NAME] already exists in the [$RESOURCE_GROUP_NAME] resource group" fi +# Make sure the account runs the MongoDB server version the sample asks for. The Azure CLI sends the +# --server-version of az cosmosdb create and az cosmosdb update as a ServerVersion property, which the service +# ignores, so the account keeps the default server version, 3.6, which the MongoDB drivers of the app and of +# mongosh refuse. The script sets the serverVersion property itself, with the API version the Azure CLI uses, +# and waits for the upgrade, which takes about a minute. +echo "Retrieving the MongoDB server version of Cosmos DB account [$COSMOSDB_ACCOUNT_NAME]..." +SERVER_VERSION=$(az cosmosdb show \ + --name "$COSMOSDB_ACCOUNT_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --query "apiProperties.serverVersion" \ + --output tsv \ + --only-show-errors) + +if [[ -n $SERVER_VERSION ]]; then + echo "Cosmos DB account [$COSMOSDB_ACCOUNT_NAME] runs MongoDB server version [$SERVER_VERSION]" +else + echo "Failed to retrieve the MongoDB server version of Cosmos DB account [$COSMOSDB_ACCOUNT_NAME]" + exit 1 +fi + +if [[ "$SERVER_VERSION" != "$MONGODB_API_VERSION" ]]; then + echo "Retrieving the resource id of Cosmos DB account [$COSMOSDB_ACCOUNT_NAME]..." + COSMOSDB_ACCOUNT_ID=$(az cosmosdb show \ + --name "$COSMOSDB_ACCOUNT_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --query "id" \ + --output tsv \ + --only-show-errors) + + if [[ -n $COSMOSDB_ACCOUNT_ID ]]; then + echo "Resource id of Cosmos DB account [$COSMOSDB_ACCOUNT_NAME] retrieved successfully" + else + echo "Failed to retrieve the resource id of Cosmos DB account [$COSMOSDB_ACCOUNT_NAME]" + exit 1 + fi + + echo "Upgrading Cosmos DB account [$COSMOSDB_ACCOUNT_NAME] to MongoDB server version [$MONGODB_API_VERSION]..." + az rest \ + --method patch \ + --url "${COSMOSDB_ACCOUNT_ID}?api-version=2026-03-15" \ + --body "{\"properties\": {\"apiProperties\": {\"serverVersion\": \"$MONGODB_API_VERSION\"}}}" \ + --only-show-errors 1>/dev/null + + if [[ $? != 0 ]]; then + echo "Failed to upgrade Cosmos DB account [$COSMOSDB_ACCOUNT_NAME] to MongoDB server version [$MONGODB_API_VERSION]" + exit 1 + fi + + for ATTEMPT in $(seq 1 60); do + SERVER_VERSION=$(az cosmosdb show \ + --name "$COSMOSDB_ACCOUNT_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --query "apiProperties.serverVersion" \ + --output tsv \ + --only-show-errors) + PROVISIONING_STATE=$(az cosmosdb show \ + --name "$COSMOSDB_ACCOUNT_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --query "provisioningState" \ + --output tsv \ + --only-show-errors) + + if [[ "$SERVER_VERSION" == "$MONGODB_API_VERSION" && "$PROVISIONING_STATE" == "Succeeded" ]]; then + break + fi + echo "Waiting for the upgrade of Cosmos DB account [$COSMOSDB_ACCOUNT_NAME] (attempt $ATTEMPT of 60)..." + sleep 10 + done + + if [[ "$SERVER_VERSION" == "$MONGODB_API_VERSION" && "$PROVISIONING_STATE" == "Succeeded" ]]; then + echo "Cosmos DB account [$COSMOSDB_ACCOUNT_NAME] successfully upgraded to MongoDB server version [$MONGODB_API_VERSION]" + else + echo "Failed to upgrade Cosmos DB account [$COSMOSDB_ACCOUNT_NAME]: it still runs MongoDB server version [$SERVER_VERSION]" + exit 1 + fi +fi + # Create the MongoDB database echo "Checking if MongoDB database [$COSMOSDB_DATABASE_NAME] exists in account [$COSMOSDB_ACCOUNT_NAME]..." az cosmosdb mongodb database show \ @@ -159,4 +236,106 @@ else exit 1 fi +# Insert the sample activities, so the app does not start with an empty list. Each activity is a document +# {_id, username, activity, timestamp} of the user the app shows, the shape the app itself reads and writes; these +# are the activities the SQL Database, MySQL and PostgreSQL samples seed. A document id is the MD5 of the user and +# a fixed name, and each upsert sets the fields only when it creates the document, so a re-run inserts only the +# missing activities and leaves the others as they are. The filter of each upsert holds the shard key (username), +# which an update of a sharded collection requires. +# mongosh must be available on the host machine. +if ! command -v mongosh &>/dev/null; then + echo "mongosh is not installed on the host. Install the MongoDB Shell (mongosh) and re-run." + exit 1 +fi + +# After an upgrade, the MongoDB endpoint of the account can go on serving the old server version for several +# minutes after Azure Resource Manager reports the new one, and mongosh refuses a server older than 4.2. Wait until +# mongosh connects; each attempt that fails takes about 30 seconds, the time mongosh looks for a suitable server. +echo "Checking that the MongoDB endpoint of Cosmos DB account [$COSMOSDB_ACCOUNT_NAME] accepts mongosh connections..." +MONGOSH_CONNECTED=false +for ATTEMPT in $(seq 1 30); do + CONNECTION_ERROR=$(mongosh "$COSMOSDB_CONNECTION_STRING" --quiet --eval 'db.hello().ok' 2>&1 1>/dev/null) + + if [[ $? == 0 ]]; then + MONGOSH_CONNECTED=true + break + fi + echo "The MongoDB endpoint does not accept mongosh connections yet (attempt $ATTEMPT of 30): $CONNECTION_ERROR" + sleep 10 +done + +if [[ $MONGOSH_CONNECTED == true ]]; then + echo "The MongoDB endpoint of Cosmos DB account [$COSMOSDB_ACCOUNT_NAME] accepts mongosh connections" +else + echo "Failed to connect to the MongoDB endpoint of Cosmos DB account [$COSMOSDB_ACCOUNT_NAME] with mongosh" + exit 1 +fi + +SAMPLE_ACTIVITIES=( + "pisa|Visit the Leaning Tower in Pisa" + "volterra|Explore Etruscan walls in Volterra" + "san_gimignano|Climb Torre Grossa in San Gimignano" + "siena|Walk across Piazza del Campo in Siena" + "montalcino|Taste Brunello wine in Montalcino" + "pienza|Sample Pecorino cheese in Pienza" + "florence|Admire Michelangelo's David in Florence" + "viareggio_beach|Relax by the beach in Viareggio" + "viareggio_promenade|Stroll along the Viareggio promenade" +) + +# mongosh reads the values from its environment, so no activity needs quoting in the script +echo "Inserting the sample activities of [$LOGIN_NAME] into collection [$COSMOSDB_COLLECTION_NAME]..." +SEED_DATABASE="$COSMOSDB_DATABASE_NAME" \ + SEED_COLLECTION="$COSMOSDB_COLLECTION_NAME" \ + SEED_USERNAME="$LOGIN_NAME" \ + SEED_TIMESTAMP="$(date '+%Y-%m-%dT%H:%M:%S.000000')" \ + SEED_ACTIVITIES="$(printf '%s\n' "${SAMPLE_ACTIVITIES[@]}")" \ + mongosh "$COSMOSDB_CONNECTION_STRING" --quiet --eval ' + const crypto = require("crypto"); + const env = process.env; + const collection = db.getSiblingDB(env.SEED_DATABASE).getCollection(env.SEED_COLLECTION); + const operations = env.SEED_ACTIVITIES.trim().split("\n").map((line) => { + const separator = line.indexOf("|"); + const name = line.slice(0, separator); + const activity = line.slice(separator + 1); + const id = crypto.createHash("md5").update(`${env.SEED_USERNAME}_${name}_seed`).digest("hex"); + return { + updateOne: { + filter: { _id: id, username: env.SEED_USERNAME }, + update: { $setOnInsert: { activity: activity, timestamp: env.SEED_TIMESTAMP } }, + upsert: true + } + }; + }); + const result = collection.bulkWrite(operations, { ordered: false }); + print(`${result.upsertedCount} activities inserted, ${result.matchedCount} already present`); + ' + +if [ $? -eq 0 ]; then + echo "Sample activities inserted successfully into collection [$COSMOSDB_COLLECTION_NAME]." +else + echo "Failed to insert the sample activities into collection [$COSMOSDB_COLLECTION_NAME]." + exit 1 +fi + +# Query the activities of the user back +echo "Querying the activities of [$LOGIN_NAME] from collection [$COSMOSDB_COLLECTION_NAME]..." +SEED_DATABASE="$COSMOSDB_DATABASE_NAME" \ + SEED_COLLECTION="$COSMOSDB_COLLECTION_NAME" \ + SEED_USERNAME="$LOGIN_NAME" \ + mongosh "$COSMOSDB_CONNECTION_STRING" --quiet --eval ' + const env = process.env; + db.getSiblingDB(env.SEED_DATABASE) + .getCollection(env.SEED_COLLECTION) + .find({ username: env.SEED_USERNAME }, { _id: 0, activity: 1 }) + .forEach((document) => print(` - ${document.activity}`)); + ' + +if [ $? -eq 0 ]; then + echo "Activities of [$LOGIN_NAME] queried successfully." +else + echo "Failed to query the activities of [$LOGIN_NAME]." + exit 1 +fi + export COSMOSDB_CONNECTION_STRING diff --git a/samples/web-app-cosmosdb-nosql-api/dotnet/README.md b/samples/web-app-cosmosdb-nosql-api/dotnet/README.md index 6c5716c..6f4acb1 100644 --- a/samples/web-app-cosmosdb-nosql-api/dotnet/README.md +++ b/samples/web-app-cosmosdb-nosql-api/dotnet/README.md @@ -39,7 +39,7 @@ cd scripts | File | Description | | ---- | ----------- | | [`00-variables.sh`](scripts/00-variables.sh) | Defines the variables shared across the other scripts (resource names, image tag, Cosmos DB account/database/container, partition key, Kubernetes namespace, …). The other scripts load these values by sourcing this file. | -| [`01-deploy-resources.sh`](scripts/01-deploy-resources.sh) | Deploys the Azure resources used by this sample: the resource group, the [Azure Container Registry (ACR)](https://learn.microsoft.com/en-us/azure/container-registry/container-registry-intro), the [Azure Cosmos DB for NoSQL](https://learn.microsoft.com/en-us/azure/cosmos-db/overview) account, the `vacationplanner` database, and the `activities` container (partitioned by `/username`). | +| [`01-deploy-resources.sh`](scripts/01-deploy-resources.sh) | Deploys the Azure resources used by this sample: the resource group, the [Azure Container Registry (ACR)](https://learn.microsoft.com/en-us/azure/container-registry/container-registry-intro), the [Azure Cosmos DB for NoSQL](https://learn.microsoft.com/en-us/azure/cosmos-db/overview) account, the `vacationplanner` database, and the `activities` container (partitioned by `/username`), which it also seeds with sample data through the Cosmos DB REST API. Requires `curl` and `openssl` on the host. | | [`02-build-docker-image.sh`](scripts/02-build-docker-image.sh) | Builds the Docker image for the web app from the [`src/`](src/) folder. | | [`03-run-docker-container.sh`](scripts/03-run-docker-container.sh) | Runs the web app in a local Docker container (no Kubernetes) to validate that it starts and connects to the database as expected. | | [`04-push-docker-image.sh`](scripts/04-push-docker-image.sh) | Tags and pushes the Docker image to the Azure Container Registry, on Azure or in the LocalStack emulator. | diff --git a/samples/web-app-cosmosdb-nosql-api/dotnet/scripts/01-deploy-resources.sh b/samples/web-app-cosmosdb-nosql-api/dotnet/scripts/01-deploy-resources.sh index 1eb15d7..9a14a27 100755 --- a/samples/web-app-cosmosdb-nosql-api/dotnet/scripts/01-deploy-resources.sh +++ b/samples/web-app-cosmosdb-nosql-api/dotnet/scripts/01-deploy-resources.sh @@ -170,5 +170,112 @@ else exit 1 fi +# Insert the sample activities, so the app does not start with an empty list. Each activity is an item +# {id, username, activity, timestamp} of the user the app shows, the shape the app itself reads and writes; these +# are the activities the SQL Database, MySQL and PostgreSQL samples seed. The Azure CLI has no command for items, +# so the script calls the Cosmos DB REST API with curl and signs each request with the primary master key. An +# item id is the MD5 of the user and a fixed name, so a re-run inserts only the missing activities: Cosmos DB +# answers 409 Conflict for an id that already exists. +# curl and openssl must be available on the host machine. +for TOOL in curl openssl; do + if ! command -v $TOOL &>/dev/null; then + echo "$TOOL is not installed on the host. Install it and re-run." + exit 1 + fi +done + +lowercase() { + echo "$1" | tr '[:upper:]' '[:lower:]' +} + +# Print the Authorization header of a Cosmos DB REST request, signed with the primary master key as described in +# https://learn.microsoft.com/rest/api/cosmos-db/access-control-on-cosmosdb-resources +cosmosdb_authorization() { + local verb=$1 + local resource_type=$2 + local resource_link=$3 + local request_date=$4 + local key_hex + local signature + + key_hex=$(printf '%s' "$AZURECOSMOSDB_PRIMARY_KEY" | openssl base64 -d -A | od -An -v -tx1 | tr -d ' \n') + signature=$(printf '%s\n%s\n%s\n%s\n\n' \ + "$(lowercase "$verb")" \ + "$(lowercase "$resource_type")" \ + "$resource_link" \ + "$(lowercase "$request_date")" | + openssl dgst -sha256 -mac HMAC -macopt "hexkey:$key_hex" -binary | + openssl base64 -A | + sed -e 's/+/%2B/g' -e 's/\//%2F/g' -e 's/=/%3D/g') + echo "type%3Dmaster%26ver%3D1.0%26sig%3D$signature" +} + +SAMPLE_ACTIVITIES=( + "pisa|Visit the Leaning Tower in Pisa" + "volterra|Explore Etruscan walls in Volterra" + "san_gimignano|Climb Torre Grossa in San Gimignano" + "siena|Walk across Piazza del Campo in Siena" + "montalcino|Taste Brunello wine in Montalcino" + "pienza|Sample Pecorino cheese in Pienza" + "florence|Admire Michelangelo's David in Florence" + "viareggio_beach|Relax by the beach in Viareggio" + "viareggio_promenade|Stroll along the Viareggio promenade" +) +DOCUMENTS_LINK="dbs/$AZURECOSMOSDB_DATABASENAME/colls/$AZURECOSMOSDB_CONTAINERNAME" +DOCUMENTS_URL="${AZURECOSMOSDB_ENDPOINT%/}/$DOCUMENTS_LINK/docs" +TIMESTAMP=$(date '+%Y-%m-%dT%H:%M:%S.000000') + +for SAMPLE_ACTIVITY in "${SAMPLE_ACTIVITIES[@]}"; do + SAMPLE_NAME="${SAMPLE_ACTIVITY%%|*}" + ACTIVITY="${SAMPLE_ACTIVITY#*|}" + ITEM_ID=$(printf '%s' "${LOGIN_NAME}_${SAMPLE_NAME}_seed" | openssl dgst -md5 | sed 's/^.*= //') + REQUEST_DATE=$(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S GMT') + + # The response body is kept only to print it on failure; the last line of the output is the HTTP status + echo "Inserting sample activity [$ACTIVITY] as item [$ITEM_ID] into container [$AZURECOSMOSDB_CONTAINERNAME]..." + RESPONSE=$(curl --silent --show-error \ + --request POST "$DOCUMENTS_URL" \ + --header "Authorization: $(cosmosdb_authorization POST docs "$DOCUMENTS_LINK" "$REQUEST_DATE")" \ + --header "x-ms-date: $REQUEST_DATE" \ + --header "x-ms-version: 2018-12-31" \ + --header "x-ms-documentdb-partitionkey: [\"$LOGIN_NAME\"]" \ + --header "Content-Type: application/json" \ + --data "{\"id\": \"$ITEM_ID\", \"username\": \"$LOGIN_NAME\", \"activity\": \"$ACTIVITY\", \"timestamp\": \"$TIMESTAMP\"}" \ + --write-out '\n%{http_code}') + STATUS_CODE="${RESPONSE##*$'\n'}" + + if [[ $STATUS_CODE == 201 ]]; then + echo "Item [$ITEM_ID] inserted successfully." + elif [[ $STATUS_CODE == 409 ]]; then + echo "Item [$ITEM_ID] already exists." + else + echo "Failed to insert item [$ITEM_ID]: HTTP status [$STATUS_CODE] ${RESPONSE%$'\n'*}" + exit 1 + fi +done + +# Query the activities of the user back +echo "Querying the activities of [$LOGIN_NAME] from container [$AZURECOSMOSDB_CONTAINERNAME]..." +REQUEST_DATE=$(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S GMT') +RESPONSE=$(curl --silent --show-error \ + --request POST "$DOCUMENTS_URL" \ + --header "Authorization: $(cosmosdb_authorization POST docs "$DOCUMENTS_LINK" "$REQUEST_DATE")" \ + --header "x-ms-date: $REQUEST_DATE" \ + --header "x-ms-version: 2018-12-31" \ + --header "x-ms-documentdb-isquery: True" \ + --header "x-ms-documentdb-partitionkey: [\"$LOGIN_NAME\"]" \ + --header "Content-Type: application/query+json" \ + --data "{\"query\": \"SELECT c.id, c.activity FROM c WHERE c.username = @username\", \"parameters\": [{\"name\": \"@username\", \"value\": \"$LOGIN_NAME\"}]}" \ + --write-out '\n%{http_code}') +STATUS_CODE="${RESPONSE##*$'\n'}" + +if [[ $STATUS_CODE == 200 ]]; then + echo "Activities of [$LOGIN_NAME] queried successfully:" + grep -o '"activity": *"[^"]*"' <<<"${RESPONSE%$'\n'*}" | sed -e 's/^"activity": *"/ - /' -e 's/"$//' +else + echo "Failed to query the activities of [$LOGIN_NAME]: HTTP status [$STATUS_CODE] ${RESPONSE%$'\n'*}" + exit 1 +fi + export AZURECOSMOSDB_ENDPOINT export AZURECOSMOSDB_PRIMARY_KEY diff --git a/samples/web-app-cosmosdb-nosql-api/python/README.md b/samples/web-app-cosmosdb-nosql-api/python/README.md index 5ebe8e9..c74fe65 100644 --- a/samples/web-app-cosmosdb-nosql-api/python/README.md +++ b/samples/web-app-cosmosdb-nosql-api/python/README.md @@ -39,7 +39,7 @@ cd scripts | File | Description | | ---- | ----------- | | [`00-variables.sh`](scripts/00-variables.sh) | Defines the variables shared across the other scripts (resource names, image tag, Cosmos DB account/database/container, partition key, Kubernetes namespace, …). The other scripts load these values by sourcing this file. | -| [`01-deploy-resources.sh`](scripts/01-deploy-resources.sh) | Deploys the Azure resources used by this sample: the resource group, the [Azure Container Registry (ACR)](https://learn.microsoft.com/en-us/azure/container-registry/container-registry-intro), the [Azure Cosmos DB for NoSQL](https://learn.microsoft.com/en-us/azure/cosmos-db/overview) account, the `vacationplanner` database, and the `activities` container (partitioned by `/username`). | +| [`01-deploy-resources.sh`](scripts/01-deploy-resources.sh) | Deploys the Azure resources used by this sample: the resource group, the [Azure Container Registry (ACR)](https://learn.microsoft.com/en-us/azure/container-registry/container-registry-intro), the [Azure Cosmos DB for NoSQL](https://learn.microsoft.com/en-us/azure/cosmos-db/overview) account, the `vacationplanner` database, and the `activities` container (partitioned by `/username`), which it also seeds with sample data through the Cosmos DB REST API. Requires `curl` and `openssl` on the host. | | [`02-build-docker-image.sh`](scripts/02-build-docker-image.sh) | Builds the Docker image for the web app from the [`src/`](src/) folder. | | [`03-run-docker-container.sh`](scripts/03-run-docker-container.sh) | Runs the web app in a local Docker container (no Kubernetes) to validate that it starts and connects to the database as expected. | | [`04-push-docker-image.sh`](scripts/04-push-docker-image.sh) | Tags and pushes the Docker image to the Azure Container Registry, on Azure or in the LocalStack emulator. | diff --git a/samples/web-app-cosmosdb-nosql-api/python/scripts/01-deploy-resources.sh b/samples/web-app-cosmosdb-nosql-api/python/scripts/01-deploy-resources.sh index 1eb15d7..9a14a27 100755 --- a/samples/web-app-cosmosdb-nosql-api/python/scripts/01-deploy-resources.sh +++ b/samples/web-app-cosmosdb-nosql-api/python/scripts/01-deploy-resources.sh @@ -170,5 +170,112 @@ else exit 1 fi +# Insert the sample activities, so the app does not start with an empty list. Each activity is an item +# {id, username, activity, timestamp} of the user the app shows, the shape the app itself reads and writes; these +# are the activities the SQL Database, MySQL and PostgreSQL samples seed. The Azure CLI has no command for items, +# so the script calls the Cosmos DB REST API with curl and signs each request with the primary master key. An +# item id is the MD5 of the user and a fixed name, so a re-run inserts only the missing activities: Cosmos DB +# answers 409 Conflict for an id that already exists. +# curl and openssl must be available on the host machine. +for TOOL in curl openssl; do + if ! command -v $TOOL &>/dev/null; then + echo "$TOOL is not installed on the host. Install it and re-run." + exit 1 + fi +done + +lowercase() { + echo "$1" | tr '[:upper:]' '[:lower:]' +} + +# Print the Authorization header of a Cosmos DB REST request, signed with the primary master key as described in +# https://learn.microsoft.com/rest/api/cosmos-db/access-control-on-cosmosdb-resources +cosmosdb_authorization() { + local verb=$1 + local resource_type=$2 + local resource_link=$3 + local request_date=$4 + local key_hex + local signature + + key_hex=$(printf '%s' "$AZURECOSMOSDB_PRIMARY_KEY" | openssl base64 -d -A | od -An -v -tx1 | tr -d ' \n') + signature=$(printf '%s\n%s\n%s\n%s\n\n' \ + "$(lowercase "$verb")" \ + "$(lowercase "$resource_type")" \ + "$resource_link" \ + "$(lowercase "$request_date")" | + openssl dgst -sha256 -mac HMAC -macopt "hexkey:$key_hex" -binary | + openssl base64 -A | + sed -e 's/+/%2B/g' -e 's/\//%2F/g' -e 's/=/%3D/g') + echo "type%3Dmaster%26ver%3D1.0%26sig%3D$signature" +} + +SAMPLE_ACTIVITIES=( + "pisa|Visit the Leaning Tower in Pisa" + "volterra|Explore Etruscan walls in Volterra" + "san_gimignano|Climb Torre Grossa in San Gimignano" + "siena|Walk across Piazza del Campo in Siena" + "montalcino|Taste Brunello wine in Montalcino" + "pienza|Sample Pecorino cheese in Pienza" + "florence|Admire Michelangelo's David in Florence" + "viareggio_beach|Relax by the beach in Viareggio" + "viareggio_promenade|Stroll along the Viareggio promenade" +) +DOCUMENTS_LINK="dbs/$AZURECOSMOSDB_DATABASENAME/colls/$AZURECOSMOSDB_CONTAINERNAME" +DOCUMENTS_URL="${AZURECOSMOSDB_ENDPOINT%/}/$DOCUMENTS_LINK/docs" +TIMESTAMP=$(date '+%Y-%m-%dT%H:%M:%S.000000') + +for SAMPLE_ACTIVITY in "${SAMPLE_ACTIVITIES[@]}"; do + SAMPLE_NAME="${SAMPLE_ACTIVITY%%|*}" + ACTIVITY="${SAMPLE_ACTIVITY#*|}" + ITEM_ID=$(printf '%s' "${LOGIN_NAME}_${SAMPLE_NAME}_seed" | openssl dgst -md5 | sed 's/^.*= //') + REQUEST_DATE=$(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S GMT') + + # The response body is kept only to print it on failure; the last line of the output is the HTTP status + echo "Inserting sample activity [$ACTIVITY] as item [$ITEM_ID] into container [$AZURECOSMOSDB_CONTAINERNAME]..." + RESPONSE=$(curl --silent --show-error \ + --request POST "$DOCUMENTS_URL" \ + --header "Authorization: $(cosmosdb_authorization POST docs "$DOCUMENTS_LINK" "$REQUEST_DATE")" \ + --header "x-ms-date: $REQUEST_DATE" \ + --header "x-ms-version: 2018-12-31" \ + --header "x-ms-documentdb-partitionkey: [\"$LOGIN_NAME\"]" \ + --header "Content-Type: application/json" \ + --data "{\"id\": \"$ITEM_ID\", \"username\": \"$LOGIN_NAME\", \"activity\": \"$ACTIVITY\", \"timestamp\": \"$TIMESTAMP\"}" \ + --write-out '\n%{http_code}') + STATUS_CODE="${RESPONSE##*$'\n'}" + + if [[ $STATUS_CODE == 201 ]]; then + echo "Item [$ITEM_ID] inserted successfully." + elif [[ $STATUS_CODE == 409 ]]; then + echo "Item [$ITEM_ID] already exists." + else + echo "Failed to insert item [$ITEM_ID]: HTTP status [$STATUS_CODE] ${RESPONSE%$'\n'*}" + exit 1 + fi +done + +# Query the activities of the user back +echo "Querying the activities of [$LOGIN_NAME] from container [$AZURECOSMOSDB_CONTAINERNAME]..." +REQUEST_DATE=$(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S GMT') +RESPONSE=$(curl --silent --show-error \ + --request POST "$DOCUMENTS_URL" \ + --header "Authorization: $(cosmosdb_authorization POST docs "$DOCUMENTS_LINK" "$REQUEST_DATE")" \ + --header "x-ms-date: $REQUEST_DATE" \ + --header "x-ms-version: 2018-12-31" \ + --header "x-ms-documentdb-isquery: True" \ + --header "x-ms-documentdb-partitionkey: [\"$LOGIN_NAME\"]" \ + --header "Content-Type: application/query+json" \ + --data "{\"query\": \"SELECT c.id, c.activity FROM c WHERE c.username = @username\", \"parameters\": [{\"name\": \"@username\", \"value\": \"$LOGIN_NAME\"}]}" \ + --write-out '\n%{http_code}') +STATUS_CODE="${RESPONSE##*$'\n'}" + +if [[ $STATUS_CODE == 200 ]]; then + echo "Activities of [$LOGIN_NAME] queried successfully:" + grep -o '"activity": *"[^"]*"' <<<"${RESPONSE%$'\n'*}" | sed -e 's/^"activity": *"/ - /' -e 's/"$//' +else + echo "Failed to query the activities of [$LOGIN_NAME]: HTTP status [$STATUS_CODE] ${RESPONSE%$'\n'*}" + exit 1 +fi + export AZURECOSMOSDB_ENDPOINT export AZURECOSMOSDB_PRIMARY_KEY