diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index cef876c..327eb28 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -9,13 +9,4 @@ RUN export DEBIAN_FRONTEND=noninteractive \ && apt-get clean -y && rm -rf /var/lib/apt/lists/* RUN curl -fsSL https://aka.ms/install-azd.sh | bash -RUN python3 -m pip install --upgrade pip - -# Copy requirements.txt (if found) to a temp location so we update the environment. Also -# copy "noop.txt" so the COPY instruction does not fail if no requirements.txt exists. -COPY requirements.txt* .devcontainer/noop.txt /tmp/pip/ -RUN if [ -f "/tmp/pip/requirements.txt" ]; then umask 0002 && python3 -m pip install -r /tmp/pip/requirements.txt && sudo rm -rf /tmp/pip; fi - -# sudo apt update -# sudo apt-get -y install ufw -# sudo ufw allow 8000 \ No newline at end of file +RUN python3 -m pip install --upgrade pip \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 284eed8..fc81ea2 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,7 +3,7 @@ { "name": "FastAPI", "build": { - "context": "..", + "context": ".", "dockerfile": "Dockerfile" }, @@ -19,7 +19,6 @@ // Set *default* container specific settings.json values on container create. "settings": { "[python]": { - "defaultInterpreterPath": "/opt/conda/envs/myenv/bin/python", "editor.formatOnType": true, "editor.formatOnSave": true } @@ -45,7 +44,7 @@ "memory": "8gb" }, - "postAttachCommand": "export DATASTORE=redis && export BEARER_TOKEN=footoken && export OPENAI_API_KEY='' && docker compose -f ./docker-compose.yml up -d" + "postAttachCommand": "chmod +x .devcontainer/setup.sh && .devcontainer/setup.sh" // Use 'postCreateCommand' to run commands after the container is created. // "postCreateCommand": "pip3 install --user -r requirements.txt", diff --git a/.devcontainer/noop.txt b/.devcontainer/noop.txt deleted file mode 100644 index afbd696..0000000 --- a/.devcontainer/noop.txt +++ /dev/null @@ -1 +0,0 @@ -This file is in place so that Dockerfile's COPY instruction does not fail if no requirements.txt exists. \ No newline at end of file diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh new file mode 100755 index 0000000..53b06d3 --- /dev/null +++ b/.devcontainer/setup.sh @@ -0,0 +1,33 @@ +#!/bin/sh +# set -eu + +# Install libraries from requirements.txt +chmod +x ./requirements.txt && pip install -r ./requirements.txt +echo + +# Check if the "redis" container is running +if ! docker ps --filter "status=running" --format "{{.Names}}" | grep -q "redis"; then + # If the "redis" container is not running, start it using docker-compose + docker-compose -f ./docker-compose.yml up -d +else + echo "The 'redis' container is already running." +fi + +echo +echo "Let's set up your development environment..." +echo +echo "Please enter your OpenAI API key found here: https://platform.openai.com/account/api-keys:" +read -r OPENAI_API_KEY + +# Export the OPENAI_API_KEY environment variable +export OPENAI_API_KEY +export DATASTORE=redis +export BEARER_TOKEN=footoken +export PLUGIN_HOSTNAME=https://$CODESPACE_NAME-8000.$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN + +echo +echo "Setting host configuration (from ./hostconfig.sh)..." +chmod +x ./hostconfig.sh && ./hostconfig.sh + +echo +echo "Enter 'footoken' if OpenAI prompts you for a Bearer Token" diff --git a/.vscode/json.code-snippets b/.vscode/json.code-snippets index 4ef38d1..d04add9 100644 --- a/.vscode/json.code-snippets +++ b/.vscode/json.code-snippets @@ -1,7 +1,7 @@ { "OpenAPI Manifest": { "prefix": "manifest-openapi", - "body": ["{\n\t\"schema_version\": \"${1:v1}\",\n\t\"name_for_human\": \"${2}\",\n\t\"name_for_model\": \"${3}\",\n\t\"description_for_human\": \"${4}\",\n\t\"description_for_model\": \"${5}\",\n\t\"auth\": {\n\t\t\"type\": \"${6:none}\"\n\t},\n\t\"api\": {\n\t\t\"type\": \"openapi\",\n\t\t\"url\": \"${7:https://your-app-url.com/openapi.yaml}\",\n\t\t\"is_user_authenticated\": \"${8:false}\"\n\t},\n\t\"logo_url\": \"${9:https://example.com/logo.png}\",\n\t\"contact_email\": \"${10:example@company.com}\",\n\t\"legal_info_url\": \"${11:https://example.com/legal}\"\n}$0"], + "body": ["{\n\t\"schema_version\": \"${1:v1}\",\n\t\"name_for_human\": \"${2}\",\n\t\"name_for_model\": \"${3}\",\n\t\"description_for_human\": \"${4}\",\n\t\"description_for_model\": \"${5}\",\n\t\"auth\": {\n\t\t\"type\": \"${6:none}\"\n\t},\n\t\"api\": {\n\t\t\"type\": \"openapi\",\n\t\t\"url\": \"${7:https://your-app-url.com/openapi.yaml}\",\n\t\t\"is_user_authenticated\": \"${8:false}\"\n\t},\n\t\"logo_url\": \"${9:https://your-app-url.com/logo.png}\",\n\t\"contact_email\": \"${10:example@company.com}\",\n\t\"legal_info_url\": \"${11:https://example.com/legal}\"\n}$0"], "description": "OpenAI manifest" } } \ No newline at end of file diff --git a/ai-plugin.json b/.well-known/ai-plugin.json similarity index 57% rename from ai-plugin.json rename to .well-known/ai-plugin.json index 751cdbc..71b27df 100644 --- a/ai-plugin.json +++ b/.well-known/ai-plugin.json @@ -5,15 +5,16 @@ "description_for_human": "Todo app for managing your tasks", "description_for_model": "Todo app for managing your tasks", "auth": { - "type": "user_http", - "authorization_type": "bearer" + "type": "user_http", + "authorization_type": "bearer" }, "api": { - "type": "openapi", - "url": "https://your-app-url.com/openapi.yaml", - "is_user_authenticated": "false" + "type": "openapi", + "url": "https://your-app-url.com/.well-known/openapi.yaml", + "is_user_authenticated": "false" }, - "logo_url": "https://example.com/logo.png", + "logo_url": "https://your-app-url.com/.well-known/logo.png", "contact_email": "example@company.com", "legal_info_url": "https://example.com/legal" -} \ No newline at end of file + } + \ No newline at end of file diff --git a/.well-known/logo.png b/.well-known/logo.png new file mode 100644 index 0000000..e6e4811 Binary files /dev/null and b/.well-known/logo.png differ diff --git a/openapi.yaml b/.well-known/openapi.yaml similarity index 53% rename from openapi.yaml rename to .well-known/openapi.yaml index 77b753f..3494062 100644 --- a/openapi.yaml +++ b/.well-known/openapi.yaml @@ -1,7 +1,7 @@ -openapi: 3.0.0 +openapi: 3.0.2 info: - title: TODO app - description: Todo app for managing your tasks + title: OpenAI plugin for a simple todo app + description: Todo app for managing your tasks on ChatGPT version: 1.0.0 servers: - url: https://your-app-url.com @@ -9,39 +9,40 @@ paths: /todos: post: summary: Create a new TODO item - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/TodoItem' + description: Accepts a string and adds as new TODO item + operationId: create_todo + parameters: + - in: query + name: todo + schema: + type: string + required: true + description: The description of the TODO item responses: - '200': + "200": description: OK content: application/json: schema: - type: object - properties: - item_id: - type: integer - format: int64 + $ref: "#/components/schemas/TodoItem" get: summary: Get a list of all TODO items + operationId: list_todos responses: - '200': + "200": description: OK content: application/json: schema: type: array items: - $ref: '#/components/schemas/TodoItem' - /todos/{item_id}: + $ref: "#/components/schemas/TodoList" + /todos/{todo_id}: get: summary: Get a TODO item by ID + operationId: get_todo parameters: - - name: item_id + - name: todo_id in: path required: true description: ID of the TODO item to retrieve @@ -49,16 +50,19 @@ paths: type: integer format: int64 responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/TodoItem' + $ref: "#/components/schemas/TodoItem" + "404": + description: Todo not found delete: summary: Delete a TODO item by ID + operationId: delete_todo parameters: - - name: item_id + - name: todo_id in: path required: true description: ID of the TODO item to delete @@ -66,17 +70,20 @@ paths: type: integer format: int64 responses: - '204': - description: No Content + "204": + description: Todo deleted + "404": + description: Todo not found components: schemas: TodoItem: type: object properties: - title: - type: string - description: + todo: type: string + todo_id: + type: integer + format: int32 + readOnly: true required: - - title - - description \ No newline at end of file + - todo \ No newline at end of file diff --git a/README.md b/README.md index 3c978aa..8603e5b 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,8 @@ This is a sample repo for developing OpenAI plugin using the FastAPI framework. - Code for the FastAPI app - Code to help deploy the app on the cloud -## Code to help setup development environment +## 📦 Code to help setup development environment +Create a Codespaces by clicking **<> Code** -> **Codespaces** -> **Create codespaces on {branch}**, and a containerized development environment will be set up for you on the cloud based on the contents of the following files. ### **.devcontainer** The `.devcontainer` folder contains files for defining a containerized development environment, specific to building this FastAPI app. It's set up in a way that makes it easy for you to use with GitHub Codespaces as well: launch a Codespace using this template, and you're ready to start developing! Learn more about devcontainers [here](https://containers.dev/). @@ -16,8 +17,8 @@ The `.vscode` folder contains: - `settings.json` file that helps to validate the manifest file (`ai-plugin.json`) against [this schema](https://github.com/minsa110/ai-plugin-schema/blob/main/ai-plugin-schema.json). - `launch.json` file that helps to customize **Run and Debug**. -## Code for the FastAPI app -To test the app, run `uvicorn main:app` in the integrated terminal, or press `F5`, and debug CRUD operations at .../docs. +## 💻 Code for the FastAPI app +If you have [access](https://code.visualstudio.com/blogs/2023/03/30/vscode-copilot#_getting-started-today) to [GitHub Copilot](https://github.com/features/copilot), try it out to help you write code faster. To test the app, run `uvicorn main:app` in the integrated terminal, or press `F5`, and debug CRUD operations at .../docs. - `main.py` was the plugin code generated by Copilot. Learn more about Copilot here. The prompt used here is: ```markdown @@ -29,7 +30,7 @@ To test the app, run `uvicorn main:app` in the integrated terminal, or press `F5 ``` - `ai-plugin.json` is a JSON manifest file that defines relevant metadata for the plugin. -## Code to help deploy the app on the cloud +## ☁️ Code to help deploy the app on the cloud This repo uses Azure Developer CLI to create two Azure Container Apps: one for the API and the other for the vector database (using Redis), then deploys the app. You can use the following commands to invoke the deployment flow: ```bash azd auth login # for now, use azd login @@ -41,3 +42,6 @@ azd up - `azure.yaml` describes the application for Azure Developer CLI (`azd`). Learn more about Azure Developer CLI [here](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/overview). - `infra` folder contains infra-as-code files (Bicep) needed to provision the Azure resource. + +## 💬 Register the app on ChatGPT +- Copy the container app link and paste it to ChatGPT plugin diff --git a/entrypoint.sh b/entrypoint.sh index 9813a71..91d2d28 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,5 +1,14 @@ #!/bin/sh PLUGIN_HOSTNAME=$(echo "$PLUGIN_HOSTNAME" | sed 's/^"//' | sed 's/"$//') -sed -i 's|https://your-app-url.com|'$PLUGIN_HOSTNAME'|g' ./ai-plugin.json ./openapi.yaml +sed -i 's|https://your-app-url.com|'$PLUGIN_HOSTNAME'|g' ./.well-known/ai-plugin.json ./.well-known/openapi.yaml ./main.py -exec uvicorn main:app --host 0.0.0.0 --port "${PORT:-${WEBSITES_PORT:-8080}}" \ No newline at end of file +exec uvicorn main:app --host 0.0.0.0 --port "${PORT:-${WEBSITES_PORT:-8080}}" + + +#!/bin/sh +set -eu + +./hostconfig.sh + +# Heroku uses PORT, Azure App Services uses WEBSITES_PORT, Fly.io uses 8080 by default +exec uvicorn server.main:app --host 0.0.0.0 --port "${PORT:-${WEBSITES_PORT:-8080}}" diff --git a/hostconfig.sh b/hostconfig.sh new file mode 100755 index 0000000..e86ed56 --- /dev/null +++ b/hostconfig.sh @@ -0,0 +1,69 @@ +#!/bin/sh +set -eu + +# # Set the plugin hostname for Codespaces +# # Check if CODESPACES environment variable is set to true +# if [ "$CODESPACES" = "true" ]; then +# # If CODESPACES is true and PLUGIN_HOSTNAME is undefined or empty, set PLUGIN_HOSTNAME +# if [ -z "$PLUGIN_HOSTNAME" ]; then +# # Check if CODESPACE_NAME and GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN are set +# if [ -z "$CODESPACE_NAME" ] || [ -z "$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN" ]; then +# echo "CODESPACE_NAME and/or GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN environment variables are not set." +# exit 1 +# fi +# # Set PLUGIN_HOSTNAME to the expanded version of the URL +# PLUGIN_HOSTNAME="https://$CODESPACE_NAME-8000.$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN" +# fi +# else +# # If CODESPACES is not true, check if PLUGIN_HOSTNAME is set +# if [ -z "$PLUGIN_HOSTNAME" ]; then +# echo "PLUGIN_HOSTNAME environment variable is not set." +# exit 1 +# fi +# fi + +# Set the plugin hostname for Azure in the azd environment +if [ "$SERVICE_API_URI" = "true" ]; then + PLUGIN_HOSTNAME=$(echo "$SERVICE_API_URI" | sed 's/^"//' | sed 's/"$//') + echo "PLUGIN_HOSTNAME environment variable is successfully set to Azure SERVICE_API_URI." +else + echo "PLUGIN_HOSTNAME environment variable is not set." + exit 1 +fi + +# Input JSON file +json_input_file="./.well-known/ai-plugin.json" + +# Input YAML file +yaml_input_file="./.well-known/openapi.yaml" + +# Create temporary files to store the modified JSON and YAML +temp_json_file=$(mktemp) +temp_yaml_file=$(mktemp) + +# Read the JSON file and perform the substitutions using jq +jq --arg plugin_hostname "$PLUGIN_HOSTNAME" ' + .api.url = ($plugin_hostname + "/.well-known/openapi.yaml") | + .logo_url = ($plugin_hostname + "/.well-known/logo.png") +' "$json_input_file" > "$temp_json_file" + +# Find the line number where the "servers:" key is located in the YAML file +servers_line_number=$(grep -n "servers:" "$yaml_input_file" | cut -d: -f1) + +# Update the YAML file using sed and awk +awk -v line_number="$servers_line_number" -v plugin_hostname="$PLUGIN_HOSTNAME" ' + NR == line_number + 1 { + sub(/url: .*/, "url: " plugin_hostname) + } + { print } +' "$yaml_input_file" > "$temp_yaml_file" + +# Overwrite the original JSON file with the modified contents +mv "$temp_json_file" "$json_input_file" + +# Overwrite the original YAML file with the modified contents +mv "$temp_yaml_file" "$yaml_input_file" + +# Print success messages +echo "$json_input_file has been updated successfully." +echo "$yaml_input_file file has been updated successfully." diff --git a/infra/core/cache/redis-enterprise.bicep b/infra/core/cache/redis-enterprise.bicep deleted file mode 100644 index c9f843c..0000000 --- a/infra/core/cache/redis-enterprise.bicep +++ /dev/null @@ -1,56 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -@description('Specify the pricing tier of the new Azure Redis Cache.') -@allowed([ - 'EnterpriseFlash_F1500' - 'EnterpriseFlash_F300' - 'EnterpriseFlash_F700' - 'Enterprise_E10' - 'Enterprise_E100' - 'Enterprise_E20' - 'Enterprise_E50' -]) -param skuName string = 'Enterprise_E10' - -@description('Specify the size of the new Azure Redis Cache instance. Valid values: for C (Basic/Standard) family (0, 1, 2, 3, 4, 5, 6), for P (Premium) family (1, 2, 3, 4)') -@allowed([ - 0 - 1 - 2 - 3 - 4 - 5 - 6 -]) -param skuCapacity int = contains(skuName, 'Flash') ? 3 : 2 - -param zones array = [] - -@allowed([ - '1.0' - '1.1' - '1.2' -]) -param minimumTlsVersion string = '1.2' - -resource redis 'Microsoft.Cache/redisEnterprise@2023-03-01-preview' = { - name: name - location: location - tags: tags - sku: { - capacity: skuCapacity - name: skuName - } - properties: { - minimumTlsVersion: minimumTlsVersion - } - zones: zones -} - -output endpoint string = redis.properties.hostName -output id string = redis.id -output name string = redis.name -output port int = redis.properties.port -output sslPort int = redis.properties.sslPort diff --git a/infra/core/cache/redis.bicep b/infra/core/cache/redis.bicep deleted file mode 100644 index 3c53b6c..0000000 --- a/infra/core/cache/redis.bicep +++ /dev/null @@ -1,54 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -@description('Specify the pricing tier of the new Azure Redis Cache.') -@allowed([ - 'Basic' - 'Standard' - 'Premium' -]) -param redisCacheSKU string = 'Basic' - -@description('Specify the family for the sku. C = Basic/Standard, P = Premium.') -@allowed([ - 'C' - 'P' -]) -param redisCacheFamily string = 'C' - -@description('Specify the size of the new Azure Redis Cache instance. Valid values: for C (Basic/Standard) family (0, 1, 2, 3, 4, 5, 6), for P (Premium) family (1, 2, 3, 4)') -@allowed([ - 0 - 1 - 2 - 3 - 4 - 5 - 6 -]) -param redisCacheCapacity int = 1 - -@description('Specify a boolean value that indicates whether to allow access via non-SSL ports.') -param enableNonSslPort bool = false - -resource redis 'Microsoft.Cache/redis@2022-06-01' = { - name: name - location: location - tags: tags - properties: { - enableNonSslPort: enableNonSslPort - minimumTlsVersion: '1.2' - sku: { - capacity: redisCacheCapacity - family: redisCacheFamily - name: redisCacheSKU - } - } -} - -output endpoint string = redis.properties.hostName -output id string = redis.id -output name string = redis.name -output port int = redis.properties.port -output sslPort int = redis.properties.sslPort diff --git a/infra/core/database/cosmos/cosmos-account.bicep b/infra/core/database/cosmos/cosmos-account.bicep deleted file mode 100644 index 6bc1f2e..0000000 --- a/infra/core/database/cosmos/cosmos-account.bicep +++ /dev/null @@ -1,48 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -param connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING' -param keyVaultName string - -@allowed([ 'GlobalDocumentDB', 'MongoDB', 'Parse' ]) -param kind string - -resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2022-08-15' = { - name: name - kind: kind - location: location - tags: tags - properties: { - consistencyPolicy: { defaultConsistencyLevel: 'Session' } - locations: [ - { - locationName: location - failoverPriority: 0 - isZoneRedundant: false - } - ] - databaseAccountOfferType: 'Standard' - enableAutomaticFailover: false - enableMultipleWriteLocations: false - apiProperties: (kind == 'MongoDB') ? { serverVersion: '4.0' } : {} - capabilities: [ { name: 'EnableServerless' } ] - } -} - -resource cosmosConnectionString 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { - parent: keyVault - name: connectionStringKey - properties: { - value: cosmos.listConnectionStrings().connectionStrings[0].connectionString - } -} - -resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { - name: keyVaultName -} - -output connectionStringKey string = connectionStringKey -output endpoint string = cosmos.properties.documentEndpoint -output id string = cosmos.id -output name string = cosmos.name diff --git a/infra/core/database/cosmos/mongo/cosmos-mongo-account.bicep b/infra/core/database/cosmos/mongo/cosmos-mongo-account.bicep deleted file mode 100644 index bd2a2b5..0000000 --- a/infra/core/database/cosmos/mongo/cosmos-mongo-account.bicep +++ /dev/null @@ -1,22 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -param keyVaultName string -param connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING' - -module cosmos '../../cosmos/cosmos-account.bicep' = { - name: 'cosmos-account' - params: { - name: name - location: location - connectionStringKey: connectionStringKey - keyVaultName: keyVaultName - kind: 'MongoDB' - tags: tags - } -} - -output connectionStringKey string = cosmos.outputs.connectionStringKey -output endpoint string = cosmos.outputs.endpoint -output id string = cosmos.outputs.id diff --git a/infra/core/database/cosmos/mongo/cosmos-mongo-db.bicep b/infra/core/database/cosmos/mongo/cosmos-mongo-db.bicep deleted file mode 100644 index 2c9688e..0000000 --- a/infra/core/database/cosmos/mongo/cosmos-mongo-db.bicep +++ /dev/null @@ -1,46 +0,0 @@ -param accountName string -param databaseName string -param location string = resourceGroup().location -param tags object = {} - -param collections array = [] -param connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING' -param keyVaultName string - -module cosmos 'cosmos-mongo-account.bicep' = { - name: 'cosmos-mongo-account' - params: { - name: accountName - location: location - keyVaultName: keyVaultName - tags: tags - connectionStringKey: connectionStringKey - } -} - -resource database 'Microsoft.DocumentDB/databaseAccounts/mongodbDatabases@2022-08-15' = { - name: '${accountName}/${databaseName}' - tags: tags - properties: { - resource: { id: databaseName } - } - - resource list 'collections' = [for collection in collections: { - name: collection.name - properties: { - resource: { - id: collection.id - shardKey: { _id: collection.shardKey } - indexes: [ { key: { keys: [ collection.indexKey ] } } ] - } - } - }] - - dependsOn: [ - cosmos - ] -} - -output connectionStringKey string = connectionStringKey -output databaseName string = databaseName -output endpoint string = cosmos.outputs.endpoint diff --git a/infra/core/database/cosmos/sql/cosmos-sql-account.bicep b/infra/core/database/cosmos/sql/cosmos-sql-account.bicep deleted file mode 100644 index e8b030f..0000000 --- a/infra/core/database/cosmos/sql/cosmos-sql-account.bicep +++ /dev/null @@ -1,21 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -param keyVaultName string - -module cosmos '../../cosmos/cosmos-account.bicep' = { - name: 'cosmos-account' - params: { - name: name - location: location - tags: tags - keyVaultName: keyVaultName - kind: 'GlobalDocumentDB' - } -} - -output connectionStringKey string = cosmos.outputs.connectionStringKey -output endpoint string = cosmos.outputs.endpoint -output id string = cosmos.outputs.id -output name string = cosmos.outputs.name diff --git a/infra/core/database/cosmos/sql/cosmos-sql-db.bicep b/infra/core/database/cosmos/sql/cosmos-sql-db.bicep deleted file mode 100644 index 5a4de20..0000000 --- a/infra/core/database/cosmos/sql/cosmos-sql-db.bicep +++ /dev/null @@ -1,73 +0,0 @@ -param accountName string -param databaseName string -param location string = resourceGroup().location -param tags object = {} - -param containers array = [] -param keyVaultName string -param principalIds array = [] - -module cosmos 'cosmos-sql-account.bicep' = { - name: 'cosmos-sql-account' - params: { - name: accountName - location: location - tags: tags - keyVaultName: keyVaultName - } -} - -resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2022-05-15' = { - name: '${accountName}/${databaseName}' - properties: { - resource: { id: databaseName } - } - - resource list 'containers' = [for container in containers: { - name: container.name - properties: { - resource: { - id: container.id - partitionKey: { paths: [ container.partitionKey ] } - } - options: {} - } - }] - - dependsOn: [ - cosmos - ] -} - -module roleDefintion 'cosmos-sql-role-def.bicep' = { - name: 'cosmos-sql-role-definition' - params: { - accountName: accountName - } - dependsOn: [ - cosmos - database - ] -} - -// We need batchSize(1) here because sql role assignments have to be done sequentially -@batchSize(1) -module userRole 'cosmos-sql-role-assign.bicep' = [for principalId in principalIds: if (!empty(principalId)) { - name: 'cosmos-sql-user-role-${uniqueString(principalId)}' - params: { - accountName: accountName - roleDefinitionId: roleDefintion.outputs.id - principalId: principalId - } - dependsOn: [ - cosmos - database - ] -}] - -output accountId string = cosmos.outputs.id -output accountName string = cosmos.outputs.name -output connectionStringKey string = cosmos.outputs.connectionStringKey -output databaseName string = databaseName -output endpoint string = cosmos.outputs.endpoint -output roleDefinitionId string = roleDefintion.outputs.id diff --git a/infra/core/database/cosmos/sql/cosmos-sql-role-assign.bicep b/infra/core/database/cosmos/sql/cosmos-sql-role-assign.bicep deleted file mode 100644 index 6855edf..0000000 --- a/infra/core/database/cosmos/sql/cosmos-sql-role-assign.bicep +++ /dev/null @@ -1,18 +0,0 @@ -param accountName string - -param roleDefinitionId string -param principalId string = '' - -resource role 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2022-05-15' = { - parent: cosmos - name: guid(roleDefinitionId, principalId, cosmos.id) - properties: { - principalId: principalId - roleDefinitionId: roleDefinitionId - scope: cosmos.id - } -} - -resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2022-08-15' existing = { - name: accountName -} diff --git a/infra/core/database/cosmos/sql/cosmos-sql-role-def.bicep b/infra/core/database/cosmos/sql/cosmos-sql-role-def.bicep deleted file mode 100644 index cfb4033..0000000 --- a/infra/core/database/cosmos/sql/cosmos-sql-role-def.bicep +++ /dev/null @@ -1,29 +0,0 @@ -param accountName string - -resource roleDefinition 'Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions@2022-08-15' = { - parent: cosmos - name: guid(cosmos.id, accountName, 'sql-role') - properties: { - assignableScopes: [ - cosmos.id - ] - permissions: [ - { - dataActions: [ - 'Microsoft.DocumentDB/databaseAccounts/readMetadata' - 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*' - 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*' - ] - notDataActions: [] - } - ] - roleName: 'Reader Writer' - type: 'CustomRole' - } -} - -resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2022-08-15' existing = { - name: accountName -} - -output id string = roleDefinition.id diff --git a/infra/core/database/postgresql/flexibleserver.bicep b/infra/core/database/postgresql/flexibleserver.bicep deleted file mode 100644 index 1aaa584..0000000 --- a/infra/core/database/postgresql/flexibleserver.bicep +++ /dev/null @@ -1,64 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -param sku object -param storage object -param administratorLogin string -@secure() -param administratorLoginPassword string -param databaseNames array = [] -param allowAzureIPsFirewall bool = false -param allowAllIPsFirewall bool = false -param allowedSingleIPs array = [] - -// PostgreSQL version -param version string - -// Latest official version 2022-12-01 does not have Bicep types available -resource postgresServer 'Microsoft.DBforPostgreSQL/flexibleServers@2022-12-01' = { - location: location - tags: tags - name: name - sku: sku - properties: { - version: version - administratorLogin: administratorLogin - administratorLoginPassword: administratorLoginPassword - storage: storage - highAvailability: { - mode: 'Disabled' - } - } - - resource database 'databases' = [for name in databaseNames: { - name: name - }] - - resource firewall_all 'firewallRules' = if (allowAllIPsFirewall) { - name: 'allow-all-IPs' - properties: { - startIpAddress: '0.0.0.0' - endIpAddress: '255.255.255.255' - } - } - - resource firewall_azure 'firewallRules' = if (allowAzureIPsFirewall) { - name: 'allow-all-azure-internal-IPs' - properties: { - startIpAddress: '0.0.0.0' - endIpAddress: '0.0.0.0' - } - } - - resource firewall_single 'firewallRules' = [for ip in allowedSingleIPs: { - name: 'allow-single-${replace(ip, '.', '')}' - properties: { - startIpAddress: ip - endIpAddress: ip - } - }] - -} - -output POSTGRES_DOMAIN_NAME string = postgresServer.properties.fullyQualifiedDomainName diff --git a/infra/core/database/sqlserver/sqlserver.bicep b/infra/core/database/sqlserver/sqlserver.bicep deleted file mode 100644 index 64477a7..0000000 --- a/infra/core/database/sqlserver/sqlserver.bicep +++ /dev/null @@ -1,129 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -param appUser string = 'appUser' -param databaseName string -param keyVaultName string -param sqlAdmin string = 'sqlAdmin' -param connectionStringKey string = 'AZURE-SQL-CONNECTION-STRING' - -@secure() -param sqlAdminPassword string -@secure() -param appUserPassword string - -resource sqlServer 'Microsoft.Sql/servers@2022-05-01-preview' = { - name: name - location: location - tags: tags - properties: { - version: '12.0' - minimalTlsVersion: '1.2' - publicNetworkAccess: 'Enabled' - administratorLogin: sqlAdmin - administratorLoginPassword: sqlAdminPassword - } - - resource database 'databases' = { - name: databaseName - location: location - } - - resource firewall 'firewallRules' = { - name: 'Azure Services' - properties: { - // Allow all clients - // Note: range [0.0.0.0-0.0.0.0] means "allow all Azure-hosted clients only". - // This is not sufficient, because we also want to allow direct access from developer machine, for debugging purposes. - startIpAddress: '0.0.0.1' - endIpAddress: '255.255.255.254' - } - } -} - -resource sqlDeploymentScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = { - name: '${name}-deployment-script' - location: location - kind: 'AzureCLI' - properties: { - azCliVersion: '2.37.0' - retentionInterval: 'PT1H' // Retain the script resource for 1 hour after it ends running - timeout: 'PT5M' // Five minutes - cleanupPreference: 'OnSuccess' - environmentVariables: [ - { - name: 'APPUSERNAME' - value: appUser - } - { - name: 'APPUSERPASSWORD' - secureValue: appUserPassword - } - { - name: 'DBNAME' - value: databaseName - } - { - name: 'DBSERVER' - value: sqlServer.properties.fullyQualifiedDomainName - } - { - name: 'SQLCMDPASSWORD' - secureValue: sqlAdminPassword - } - { - name: 'SQLADMIN' - value: sqlAdmin - } - ] - - scriptContent: ''' -wget https://github.com/microsoft/go-sqlcmd/releases/download/v0.8.1/sqlcmd-v0.8.1-linux-x64.tar.bz2 -tar x -f sqlcmd-v0.8.1-linux-x64.tar.bz2 -C . - -cat < ./initDb.sql -drop user ${APPUSERNAME} -go -create user ${APPUSERNAME} with password = '${APPUSERPASSWORD}' -go -alter role db_owner add member ${APPUSERNAME} -go -SCRIPT_END - -./sqlcmd -S ${DBSERVER} -d ${DBNAME} -U ${SQLADMIN} -i ./initDb.sql - ''' - } -} - -resource sqlAdminPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { - parent: keyVault - name: 'sqlAdminPassword' - properties: { - value: sqlAdminPassword - } -} - -resource appUserPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { - parent: keyVault - name: 'appUserPassword' - properties: { - value: appUserPassword - } -} - -resource sqlAzureConnectionStringSercret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { - parent: keyVault - name: connectionStringKey - properties: { - value: '${connectionString}; Password=${appUserPassword}' - } -} - -resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { - name: keyVaultName -} - -var connectionString = 'Server=${sqlServer.properties.fullyQualifiedDomainName}; Database=${sqlServer::database.name}; User=${appUser}' -output connectionStringKey string = connectionStringKey -output databaseName string = sqlServer::database.name diff --git a/infra/core/gateway/apim.bicep b/infra/core/gateway/apim.bicep deleted file mode 100644 index 64c958c..0000000 --- a/infra/core/gateway/apim.bicep +++ /dev/null @@ -1,78 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -@description('The email address of the owner of the service') -@minLength(1) -param publisherEmail string = 'noreply@microsoft.com' - -@description('The name of the owner of the service') -@minLength(1) -param publisherName string = 'n/a' - -@description('The pricing tier of this API Management service') -@allowed([ - 'Consumption' - 'Developer' - 'Standard' - 'Premium' -]) -param sku string = 'Consumption' - -@description('The instance size of this API Management service.') -@allowed([ 0, 1, 2 ]) -param skuCount int = 0 - -@description('Azure Application Insights Name') -param applicationInsightsName string - -resource apimService 'Microsoft.ApiManagement/service@2021-08-01' = { - name: name - location: location - tags: union(tags, { 'azd-service-name': name }) - sku: { - name: sku - capacity: (sku == 'Consumption') ? 0 : ((sku == 'Developer') ? 1 : skuCount) - } - properties: { - publisherEmail: publisherEmail - publisherName: publisherName - // Custom properties are not supported for Consumption SKU - customProperties: sku == 'Consumption' ? {} : { - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_128_GCM_SHA256': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_256_CBC_SHA256': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_128_CBC_SHA256': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_256_CBC_SHA': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_128_CBC_SHA': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TripleDes168': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls10': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls11': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Ssl30': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls10': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls11': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Ssl30': 'false' - } - } -} - -resource apimLogger 'Microsoft.ApiManagement/service/loggers@2021-12-01-preview' = if (!empty(applicationInsightsName)) { - name: 'app-insights-logger' - parent: apimService - properties: { - credentials: { - instrumentationKey: applicationInsights.properties.InstrumentationKey - } - description: 'Logger to Azure Application Insights' - isBuffered: false - loggerType: 'applicationInsights' - resourceId: applicationInsights.id - } -} - -resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!empty(applicationInsightsName)) { - name: applicationInsightsName -} - -output apimServiceName string = apimService.name diff --git a/infra/core/host/aks-agent-pool.bicep b/infra/core/host/aks-agent-pool.bicep deleted file mode 100644 index 56c7a32..0000000 --- a/infra/core/host/aks-agent-pool.bicep +++ /dev/null @@ -1,17 +0,0 @@ -param clusterName string - -@description('The agent pool name') -param name string - -@description('The agent pool configuration') -param config object - -resource aksCluster 'Microsoft.ContainerService/managedClusters@2022-11-02-preview' existing = { - name: clusterName -} - -resource nodePool 'Microsoft.ContainerService/managedClusters/agentPools@2022-11-02-preview' = { - parent: aksCluster - name: name - properties: config -} diff --git a/infra/core/host/aks-managed-cluster.bicep b/infra/core/host/aks-managed-cluster.bicep deleted file mode 100644 index e608030..0000000 --- a/infra/core/host/aks-managed-cluster.bicep +++ /dev/null @@ -1,139 +0,0 @@ -@description('The name for the AKS managed cluster') -param name string - -@description('The name of the resource group for the managed resources of the AKS cluster') -param nodeResourceGroupName string = '' - -@description('The Azure region/location for the AKS resources') -param location string = resourceGroup().location - -@description('Custom tags to apply to the AKS resources') -param tags object = {} - -@description('Kubernetes Version') -param kubernetesVersion string = '1.25.5' - -@description('Whether RBAC is enabled for local accounts') -param enableRbac bool = true - -// Add-ons -@description('Whether web app routing (preview) add-on is enabled') -param webAppRoutingAddon bool = true - -// AAD Integration -@description('Enable Azure Active Directory integration') -param enableAad bool = false - -@description('Enable RBAC using AAD') -param enableAzureRbac bool = false - -@description('The Tenant ID associated to the Azure Active Directory') -param aadTenantId string = '' - -@description('The load balancer SKU to use for ingress into the AKS cluster') -@allowed([ 'basic', 'standard' ]) -param loadBalancerSku string = 'standard' - -@description('Network plugin used for building the Kubernetes network.') -@allowed([ 'azure', 'kubenet', 'none' ]) -param networkPlugin string = 'azure' - -@description('Network policy used for building the Kubernetes network.') -@allowed([ 'azure', 'calico' ]) -param networkPolicy string = 'azure' - -@description('If set to true, getting static credentials will be disabled for this cluster.') -param disableLocalAccounts bool = false - -@description('The managed cluster SKU.') -@allowed([ 'Paid', 'Free' ]) -param sku string = 'Free' - -@description('Configuration of AKS add-ons') -param addOns object = {} - -@description('The log analytics workspace id used for logging & monitoring') -param workspaceId string = '' - -@description('The node pool configuration for the System agent pool') -param systemPoolConfig object - -@description('The DNS prefix to associate with the AKS cluster') -param dnsPrefix string = '' - -resource aks 'Microsoft.ContainerService/managedClusters@2022-11-02-preview' = { - name: name - location: location - tags: tags - identity: { - type: 'SystemAssigned' - } - sku: { - name: 'Basic' - tier: sku - } - properties: { - nodeResourceGroup: !empty(nodeResourceGroupName) ? nodeResourceGroupName : 'rg-mc-${name}' - kubernetesVersion: kubernetesVersion - dnsPrefix: empty(dnsPrefix) ? '${name}-dns' : dnsPrefix - enableRBAC: enableRbac - aadProfile: enableAad ? { - managed: true - enableAzureRBAC: enableAzureRbac - tenantID: aadTenantId - } : null - agentPoolProfiles: [ - systemPoolConfig - ] - networkProfile: { - loadBalancerSku: loadBalancerSku - networkPlugin: networkPlugin - networkPolicy: networkPolicy - } - disableLocalAccounts: disableLocalAccounts && enableAad - addonProfiles: addOns - ingressProfile: { - webAppRouting: { - enabled: webAppRoutingAddon - } - } - } -} - -var aksDiagCategories = [ - 'cluster-autoscaler' - 'kube-controller-manager' - 'kube-audit-admin' - 'guard' -] - -// TODO: Update diagnostics to be its own module -// Blocking issue: https://github.com/Azure/bicep/issues/622 -// Unable to pass in a `resource` scope or unable to use string interpolation in resource types -resource diagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(workspaceId)) { - name: 'aks-diagnostics' - scope: aks - properties: { - workspaceId: workspaceId - logs: [for category in aksDiagCategories: { - category: category - enabled: true - }] - metrics: [ - { - category: 'AllMetrics' - enabled: true - } - ] - } -} - -@description('The resource name of the AKS cluster') -output clusterName string = aks.name - -@description('The AKS cluster identity') -output clusterIdentity object = { - clientId: aks.properties.identityProfile.kubeletidentity.clientId - objectId: aks.properties.identityProfile.kubeletidentity.objectId - resourceId: aks.properties.identityProfile.kubeletidentity.resourceId -} diff --git a/infra/core/host/aks.bicep b/infra/core/host/aks.bicep deleted file mode 100644 index f2f4206..0000000 --- a/infra/core/host/aks.bicep +++ /dev/null @@ -1,213 +0,0 @@ -@description('The name for the AKS managed cluster') -param name string - -@description('The name for the Azure container registry (ACR)') -param containerRegistryName string - -@description('The name of the connected log analytics workspace') -param logAnalyticsName string = '' - -@description('The name of the keyvault to grant access') -param keyVaultName string - -@description('The Azure region/location for the AKS resources') -param location string = resourceGroup().location - -@description('Custom tags to apply to the AKS resources') -param tags object = {} - -@description('AKS add-ons configuration') -param addOns object = { - azurePolicy: { - enabled: true - config: { - version: 'v2' - } - } - keyVault: { - enabled: true - config: { - enableSecretRotation: 'true' - rotationPollInterval: '2m' - } - } - openServiceMesh: { - enabled: false - config: {} - } - omsAgent: { - enabled: true - config: {} - } - applicationGateway: { - enabled: false - config: {} - } -} - -@allowed([ - 'CostOptimised' - 'Standard' - 'HighSpec' - 'Custom' -]) -@description('The System Pool Preset sizing') -param systemPoolType string = 'CostOptimised' - -@allowed([ - '' - 'CostOptimised' - 'Standard' - 'HighSpec' - 'Custom' -]) -@description('The User Pool Preset sizing') -param agentPoolType string = '' - -// Configure system / user agent pools -@description('Custom configuration of system node pool') -param systemPoolConfig object = {} -@description('Custom configuration of user node pool') -param agentPoolConfig object = {} - -// Configure AKS add-ons -var omsAgentConfig = (!empty(logAnalyticsName) && !empty(addOns.omsAgent) && addOns.omsAgent.enabled) ? union( - addOns.omsAgent, - { - config: { - logAnalyticsWorkspaceResourceID: logAnalytics.id - } - } -) : {} - -var addOnsConfig = union( - (!empty(addOns.azurePolicy) && addOns.azurePolicy.enabled) ? { azurepolicy: addOns.azurePolicy } : {}, - (!empty(addOns.keyVault) && addOns.keyVault.enabled) ? { azureKeyvaultSecretsProvider: addOns.keyVault } : {}, - (!empty(addOns.openServiceMesh) && addOns.openServiceMesh.enabled) ? { openServiceMesh: addOns.openServiceMesh } : {}, - (!empty(addOns.omsAgent) && addOns.omsAgent.enabled) ? { omsagent: omsAgentConfig } : {}, - (!empty(addOns.applicationGateway) && addOns.applicationGateway.enabled) ? { ingressApplicationGateway: addOns.applicationGateway } : {} -) - -// Link to existing log analytics workspace when available -resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' existing = if (!empty(logAnalyticsName)) { - name: logAnalyticsName -} - -var systemPoolSpec = !empty(systemPoolConfig) ? systemPoolConfig : nodePoolPresets[systemPoolType] - -// Create the primary AKS cluster resources and system node pool -module managedCluster 'aks-managed-cluster.bicep' = { - name: 'managed-cluster' - params: { - name: name - location: location - tags: tags - systemPoolConfig: union( - { name: 'npsystem', mode: 'System' }, - nodePoolBase, - systemPoolSpec - ) - addOns: addOnsConfig - workspaceId: !empty(logAnalyticsName) ? logAnalytics.id : '' - } -} - -var hasAgentPool = !empty(agentPoolConfig) || !empty(agentPoolType) -var agentPoolSpec = hasAgentPool && !empty(agentPoolConfig) ? agentPoolConfig : empty(agentPoolType) ? {} : nodePoolPresets[agentPoolType] - -// Create additional user agent pool when specified -module agentPool 'aks-agent-pool.bicep' = if (hasAgentPool) { - name: 'aks-node-pool' - params: { - clusterName: managedCluster.outputs.clusterName - name: 'npuserpool' - config: union({ name: 'npuser', mode: 'User' }, nodePoolBase, agentPoolSpec) - } -} - -// Creates container registry (ACR) -module containerRegistry 'container-registry.bicep' = { - name: 'container-registry' - params: { - name: containerRegistryName - location: location - tags: tags - workspaceId: !empty(logAnalyticsName) ? logAnalytics.id : '' - } -} - -// Grant ACR Pull access from cluster managed identity to container registry -module containerRegistryAccess '../security/registry-access.bicep' = { - name: 'cluster-container-registry-access' - params: { - containerRegistryName: containerRegistry.outputs.name - principalId: managedCluster.outputs.clusterIdentity.objectId - } -} - -// Give the AKS Cluster access to KeyVault -module clusterKeyVaultAccess '../security/keyvault-access.bicep' = { - name: 'cluster-keyvault-access' - params: { - keyVaultName: keyVaultName - principalId: managedCluster.outputs.clusterIdentity.objectId - } -} - -// Helpers for node pool configuration -var nodePoolBase = { - osType: 'Linux' - maxPods: 30 - type: 'VirtualMachineScaleSets' - upgradeSettings: { - maxSurge: '33%' - } -} - -var nodePoolPresets = { - CostOptimised: { - vmSize: 'Standard_B4ms' - count: 1 - minCount: 1 - maxCount: 3 - enableAutoScaling: true - availabilityZones: [] - } - Standard: { - vmSize: 'Standard_DS2_v2' - count: 3 - minCount: 3 - maxCount: 5 - enableAutoScaling: true - availabilityZones: [ - '1' - '2' - '3' - ] - } - HighSpec: { - vmSize: 'Standard_D4s_v3' - count: 3 - minCount: 3 - maxCount: 5 - enableAutoScaling: true - availabilityZones: [ - '1' - '2' - '3' - ] - } -} - -// Module outputs -@description('The resource name of the AKS cluster') -output clusterName string = managedCluster.outputs.clusterName - -@description('The AKS cluster identity') -output clusterIdentity object = managedCluster.outputs.clusterIdentity - -@description('The resource name of the ACR') -output containerRegistryName string = containerRegistry.outputs.name - -@description('The login server for the container registry') -output containerRegistryLoginServer string = containerRegistry.outputs.loginServer diff --git a/infra/core/host/appservice.bicep b/infra/core/host/appservice.bicep deleted file mode 100644 index c65f2b8..0000000 --- a/infra/core/host/appservice.bicep +++ /dev/null @@ -1,101 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -// Reference Properties -param applicationInsightsName string = '' -param appServicePlanId string -param keyVaultName string = '' -param managedIdentity bool = !empty(keyVaultName) - -// Runtime Properties -@allowed([ - 'dotnet', 'dotnetcore', 'dotnet-isolated', 'node', 'python', 'java', 'powershell', 'custom' -]) -param runtimeName string -param runtimeNameAndVersion string = '${runtimeName}|${runtimeVersion}' -param runtimeVersion string - -// Microsoft.Web/sites Properties -param kind string = 'app,linux' - -// Microsoft.Web/sites/config -param allowedOrigins array = [] -param alwaysOn bool = true -param appCommandLine string = '' -param appSettings object = {} -param clientAffinityEnabled bool = false -param enableOryxBuild bool = contains(kind, 'linux') -param functionAppScaleLimit int = -1 -param linuxFxVersion string = runtimeNameAndVersion -param minimumElasticInstanceCount int = -1 -param numberOfWorkers int = -1 -param scmDoBuildDuringDeployment bool = false -param use32BitWorkerProcess bool = false -param ftpsState string = 'FtpsOnly' -param healthCheckPath string = '' - -resource appService 'Microsoft.Web/sites@2022-03-01' = { - name: name - location: location - tags: tags - kind: kind - properties: { - serverFarmId: appServicePlanId - siteConfig: { - linuxFxVersion: linuxFxVersion - alwaysOn: alwaysOn - ftpsState: ftpsState - minTlsVersion: '1.2' - appCommandLine: appCommandLine - numberOfWorkers: numberOfWorkers != -1 ? numberOfWorkers : null - minimumElasticInstanceCount: minimumElasticInstanceCount != -1 ? minimumElasticInstanceCount : null - use32BitWorkerProcess: use32BitWorkerProcess - functionAppScaleLimit: functionAppScaleLimit != -1 ? functionAppScaleLimit : null - healthCheckPath: healthCheckPath - cors: { - allowedOrigins: union([ 'https://portal.azure.com', 'https://ms.portal.azure.com' ], allowedOrigins) - } - } - clientAffinityEnabled: clientAffinityEnabled - httpsOnly: true - } - - identity: { type: managedIdentity ? 'SystemAssigned' : 'None' } - - resource configAppSettings 'config' = { - name: 'appsettings' - properties: union(appSettings, - { - SCM_DO_BUILD_DURING_DEPLOYMENT: string(scmDoBuildDuringDeployment) - ENABLE_ORYX_BUILD: string(enableOryxBuild) - }, - !empty(applicationInsightsName) ? { APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsights.properties.ConnectionString } : {}, - !empty(keyVaultName) ? { AZURE_KEY_VAULT_ENDPOINT: keyVault.properties.vaultUri } : {}) - } - - resource configLogs 'config' = { - name: 'logs' - properties: { - applicationLogs: { fileSystem: { level: 'Verbose' } } - detailedErrorMessages: { enabled: true } - failedRequestsTracing: { enabled: true } - httpLogs: { fileSystem: { enabled: true, retentionInDays: 1, retentionInMb: 35 } } - } - dependsOn: [ - configAppSettings - ] - } -} - -resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = if (!(empty(keyVaultName))) { - name: keyVaultName -} - -resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!empty(applicationInsightsName)) { - name: applicationInsightsName -} - -output identityPrincipalId string = managedIdentity ? appService.identity.principalId : '' -output name string = appService.name -output uri string = 'https://${appService.properties.defaultHostName}' diff --git a/infra/core/host/appserviceplan.bicep b/infra/core/host/appserviceplan.bicep deleted file mode 100644 index 69c35d7..0000000 --- a/infra/core/host/appserviceplan.bicep +++ /dev/null @@ -1,20 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -param kind string = '' -param reserved bool = true -param sku object - -resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = { - name: name - location: location - tags: tags - sku: sku - kind: kind - properties: { - reserved: reserved - } -} - -output id string = appServicePlan.id diff --git a/infra/core/host/functions.bicep b/infra/core/host/functions.bicep deleted file mode 100644 index 28a581b..0000000 --- a/infra/core/host/functions.bicep +++ /dev/null @@ -1,82 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -// Reference Properties -param applicationInsightsName string = '' -param appServicePlanId string -param keyVaultName string = '' -param managedIdentity bool = !empty(keyVaultName) -param storageAccountName string - -// Runtime Properties -@allowed([ - 'dotnet', 'dotnetcore', 'dotnet-isolated', 'node', 'python', 'java', 'powershell', 'custom' -]) -param runtimeName string -param runtimeNameAndVersion string = '${runtimeName}|${runtimeVersion}' -param runtimeVersion string - -// Function Settings -@allowed([ - '~4', '~3', '~2', '~1' -]) -param extensionVersion string = '~4' - -// Microsoft.Web/sites Properties -param kind string = 'functionapp,linux' - -// Microsoft.Web/sites/config -param allowedOrigins array = [] -param alwaysOn bool = true -param appCommandLine string = '' -param appSettings object = {} -param clientAffinityEnabled bool = false -param enableOryxBuild bool = contains(kind, 'linux') -param functionAppScaleLimit int = -1 -param linuxFxVersion string = runtimeNameAndVersion -param minimumElasticInstanceCount int = -1 -param numberOfWorkers int = -1 -param scmDoBuildDuringDeployment bool = true -param use32BitWorkerProcess bool = false - -module functions 'appservice.bicep' = { - name: '${name}-functions' - params: { - name: name - location: location - tags: tags - allowedOrigins: allowedOrigins - alwaysOn: alwaysOn - appCommandLine: appCommandLine - applicationInsightsName: applicationInsightsName - appServicePlanId: appServicePlanId - appSettings: union(appSettings, { - AzureWebJobsStorage: 'DefaultEndpointsProtocol=https;AccountName=${storage.name};AccountKey=${storage.listKeys().keys[0].value};EndpointSuffix=${environment().suffixes.storage}' - FUNCTIONS_EXTENSION_VERSION: extensionVersion - FUNCTIONS_WORKER_RUNTIME: runtimeName - }) - clientAffinityEnabled: clientAffinityEnabled - enableOryxBuild: enableOryxBuild - functionAppScaleLimit: functionAppScaleLimit - keyVaultName: keyVaultName - kind: kind - linuxFxVersion: linuxFxVersion - managedIdentity: managedIdentity - minimumElasticInstanceCount: minimumElasticInstanceCount - numberOfWorkers: numberOfWorkers - runtimeName: runtimeName - runtimeVersion: runtimeVersion - runtimeNameAndVersion: runtimeNameAndVersion - scmDoBuildDuringDeployment: scmDoBuildDuringDeployment - use32BitWorkerProcess: use32BitWorkerProcess - } -} - -resource storage 'Microsoft.Storage/storageAccounts@2021-09-01' existing = { - name: storageAccountName -} - -output identityPrincipalId string = managedIdentity ? functions.outputs.identityPrincipalId : '' -output name string = functions.outputs.name -output uri string = functions.outputs.uri diff --git a/infra/core/host/staticwebapp.bicep b/infra/core/host/staticwebapp.bicep deleted file mode 100644 index 91c2d0d..0000000 --- a/infra/core/host/staticwebapp.bicep +++ /dev/null @@ -1,21 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -param sku object = { - name: 'Free' - tier: 'Free' -} - -resource web 'Microsoft.Web/staticSites@2022-03-01' = { - name: name - location: location - tags: tags - sku: sku - properties: { - provider: 'Custom' - } -} - -output name string = web.name -output uri string = 'https://${web.properties.defaultHostname}' diff --git a/infra/core/security/keyvault-access.bicep b/infra/core/security/keyvault-access.bicep deleted file mode 100644 index 96c9cf7..0000000 --- a/infra/core/security/keyvault-access.bicep +++ /dev/null @@ -1,21 +0,0 @@ -param name string = 'add' - -param keyVaultName string = '' -param permissions object = { secrets: [ 'get', 'list' ] } -param principalId string - -resource keyVaultAccessPolicies 'Microsoft.KeyVault/vaults/accessPolicies@2022-07-01' = { - parent: keyVault - name: name - properties: { - accessPolicies: [ { - objectId: principalId - tenantId: subscription().tenantId - permissions: permissions - } ] - } -} - -resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { - name: keyVaultName -} diff --git a/infra/core/security/keyvault-secret.bicep b/infra/core/security/keyvault-secret.bicep deleted file mode 100644 index 5f786ce..0000000 --- a/infra/core/security/keyvault-secret.bicep +++ /dev/null @@ -1,30 +0,0 @@ -param name string -param tags object = {} -param keyVaultName string -param contentType string = 'string' -@description('The value of the secret. Provide only derived values like blob storage access, but do not hard code any secrets in your templates') -@secure() -param secretValue string - -param enabled bool = true -param exp int = 0 -param nbf int = 0 - -resource keyVaultSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { - name: name - tags: tags - parent: keyVault - properties: { - attributes: { - enabled: enabled - exp: exp - nbf: nbf - } - contentType: contentType - value: secretValue - } -} - -resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { - name: keyVaultName -} diff --git a/infra/core/security/keyvault.bicep b/infra/core/security/keyvault.bicep deleted file mode 100644 index 0eb4a86..0000000 --- a/infra/core/security/keyvault.bicep +++ /dev/null @@ -1,25 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -param principalId string = '' - -resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = { - name: name - location: location - tags: tags - properties: { - tenantId: subscription().tenantId - sku: { family: 'A', name: 'standard' } - accessPolicies: !empty(principalId) ? [ - { - objectId: principalId - permissions: { secrets: [ 'get', 'list' ] } - tenantId: subscription().tenantId - } - ] : [] - } -} - -output endpoint string = keyVault.properties.vaultUri -output name string = keyVault.name diff --git a/infra/core/security/registry-access.bicep b/infra/core/security/registry-access.bicep deleted file mode 100644 index 056bd6c..0000000 --- a/infra/core/security/registry-access.bicep +++ /dev/null @@ -1,18 +0,0 @@ -param containerRegistryName string -param principalId string - -var acrPullRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') - -resource aksAcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - scope: containerRegistry // Use when specifying a scope that is different than the deployment scope - name: guid(principalId, 'Acr', acrPullRole) - properties: { - roleDefinitionId: acrPullRole - principalType: 'ServicePrincipal' - principalId: principalId - } -} - -resource containerRegistry 'Microsoft.ContainerRegistry/registries@2022-02-01-preview' existing = { - name: containerRegistryName -} diff --git a/infra/core/storage/storage-account.bicep b/infra/core/storage/storage-account.bicep deleted file mode 100644 index a41972c..0000000 --- a/infra/core/storage/storage-account.bicep +++ /dev/null @@ -1,38 +0,0 @@ -param name string -param location string = resourceGroup().location -param tags object = {} - -param allowBlobPublicAccess bool = false -param containers array = [] -param kind string = 'StorageV2' -param minimumTlsVersion string = 'TLS1_2' -param sku object = { name: 'Standard_LRS' } - -resource storage 'Microsoft.Storage/storageAccounts@2022-05-01' = { - name: name - location: location - tags: tags - kind: kind - sku: sku - properties: { - minimumTlsVersion: minimumTlsVersion - allowBlobPublicAccess: allowBlobPublicAccess - networkAcls: { - bypass: 'AzureServices' - defaultAction: 'Allow' - } - } - - resource blobServices 'blobServices' = if (!empty(containers)) { - name: 'default' - resource container 'containers' = [for container in containers: { - name: container.name - properties: { - publicAccess: contains(container, 'publicAccess') ? container.publicAccess : 'None' - } - }] - } -} - -output name string = storage.name -output primaryEndpoints object = storage.properties.primaryEndpoints diff --git a/infra/main.bicep b/infra/main.bicep index f409f64..1a92ded 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -13,18 +13,21 @@ param apiContainerAppName string = '' param apiImageName string = '' param applicationInsightsDashboardName string = '' param applicationInsightsName string = '' + @secure() param bearerToken string param containerAppsEnvironmentName string = '' param containerRegistryName string = '' param datastore string = 'redis' param logAnalyticsName string = '' + @secure() param openAiApiKey string = '' param redisContainerAppName string = '' param redisContainerPort int = 80 param redisHost string = '' param redisImageName string = '' + @secure() param redisPassword string = '' param redisPort int = 6379 @@ -55,7 +58,7 @@ module containerApps './core/host/container-apps.bicep' = { } } -// api frontend +// Container app for API frontend module api './app/api.bicep' = { name: 'api' scope: rg @@ -77,6 +80,7 @@ module api './app/api.bicep' = { } } +// Container app for Redis datastore module redis './app/redis.bicep' = { name: 'redis' scope: rg diff --git a/main.py b/main.py index d8fe94c..6b69e5b 100644 --- a/main.py +++ b/main.py @@ -1,65 +1,55 @@ -import json from fastapi import FastAPI, HTTPException -from fastapi.responses import JSONResponse -from fastapi.openapi.utils import get_openapi -from pydantic import BaseModel -from typing import Optional, Dict, List +from fastapi.staticfiles import StaticFiles import os +import redis -class TodoItem(BaseModel): - title: str - description: Optional[str] = None +# Set Redis parameters +redis_host = os.environ.get('REDIS_HOST') +redis_port = int(os.environ.get('REDIS_PORT', 6379)) # Use default port 6379 if not specified +redis_password = os.environ.get('REDIS_PASSWORD') -app = FastAPI() - -todos: Dict[int, TodoItem] = {} -next_id: int = 1 - -def load_manifest(): - with open("./ai-plugin.json", "r") as f: - return json.load(f) - -@app.get("/.well-known/ai-plugin.json", include_in_schema=False) -async def ai_plugin(): - manifest = load_manifest() - return JSONResponse(content=manifest) +# Create a Redis connection +redis_client = redis.Redis(host=redis_host, port=redis_port, password=redis_password) -@app.get("/todos/", response_model=List[TodoItem]) -async def list_todos(): - return list(todos.values()) - -@app.get("/todos/{todo_id}", response_model=TodoItem) -async def get_todo(todo_id: int): - if todo_id not in todos: +app = FastAPI() +app.mount("/.well-known", StaticFiles(directory=".well-known"), name="static") + +# Route to list all TODOs +@app.get("/todos") +def list_todos(): + todos = {} + for key in redis_client.keys(): + k = str(key.decode('utf-8')) + if k != 'todo_id': + todo = redis_client.get(key) + if todo is not None: + todos[key] = "["+k+"] " + str(todo.decode('utf-8')) + return todos + +# Route to list a specific TODO +@app.get("/todos/{todo_id}") +def list_todo(todo_id: int): + todo = redis_client.get(str(todo_id)) + if todo: + return {"todo_id": todo_id, "todo": todo} + else: raise HTTPException(status_code=404, detail="Todo not found") - return todos[todo_id] - -@app.post("/todos/") -async def create_todo(todo: TodoItem): - global next_id - todos[next_id] = todo - next_id += 1 - return next_id -1 -@app.delete("/todos/{todo_id}", response_model=None) -async def delete_todo(todo_id: int): - if todo_id not in todos: +# Route to add a TODO +@app.post("/todos") +def add_todo(todo: str): + # Generate a unique todo_id + todo_id = redis_client.incr('todo_id') + redis_client.set(str(todo_id), todo) + return {"todo_id": todo_id, "todo": todo} + +# Route to delete a TODO +@app.delete("/todos/{todo_id}") +def delete_todo(todo_id: int): + if not redis_client.exists(str(todo_id)): raise HTTPException(status_code=404, detail="Todo not found") - del todos[todo_id] - -def custom_openapi(): - if app.openapi_schema: - return app.openapi_schema - openapi_schema = get_openapi( - title="TODO App", - version="1.0.0", - description="A simple TODO app with FastAPI", - routes=app.routes, - ) - app.openapi_schema = openapi_schema - return app.openapi_schema - -app.openapi = custom_openapi + redis_client.delete(str(todo_id)) + return {"result": "Todo deleted"} if __name__ == "__main__": import uvicorn diff --git a/requirements.txt b/requirements.txt old mode 100644 new mode 100755 index a9ac093..8d8432f --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ fastapi uvicorn -pydantic \ No newline at end of file +redis +pytest +httpx \ No newline at end of file