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