diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..b70efb4 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,10 @@ +ARG VARIANT=bullseye +ARG VERSION=3.10 +FROM --platform=amd64 mcr.microsoft.com/devcontainers/python:${VERSION}-${VARIANT} + +RUN export DEBIAN_FRONTEND=noninteractive \ + && apt-get update \ + && apt-get install -y xdg-utils \ + && curl -fsSL https://aka.ms/install-azd.sh | bash \ + && apt-get clean -y \ + && rm -rf /var/lib/apt/lists/* \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..79f8e0c --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,59 @@ +{ + "name": "Azure Developer CLI", + "build": { + "dockerfile": "Dockerfile", + "args": { + "VARIANT": "bullseye", + "VERSION": "3.10" + } + }, + "features": { + "ghcr.io/devcontainers/features/azure-cli:1": { + "version": "2.48.1" + }, + "ghcr.io/devcontainers/features/docker-from-docker:1": { + "version": "20.10" + }, + "ghcr.io/devcontainers/features/github-cli:1": { + "version": "2" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-azuretools.azure-dev", + "ms-azuretools.vscode-bicep", + "ms-azuretools.vscode-docker", + "ms-vscode.vscode-node-azure-pack", + "ms-vscode.js-debug", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "ms-azuretools.vscode-azurefunctions", + "github.vscode-github-actions", + "GitHub.copilot-nightly", + "ms-python.black-formatter" + ] + }, + "codespaces": { + "openFiles": [ + "main.py" + ] + } + }, + "forwardPorts": [ + 6379, + 8000 + ], + "postAttachCommand": "", + "postCreateCommand": "pip3 install --user -r requirements.txt", + "remoteUser": "vscode", + "hostRequirements": { + "memory": "8gb", + "cpus": 8 + }, + "portsAttributes": { + "8000": { + "label": "FastAPI Server" + } + } +} \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml new file mode 100644 index 0000000..bc04b02 --- /dev/null +++ b/.github/workflows/azure-dev.yml @@ -0,0 +1,64 @@ +on: + workflow_dispatch: + push: + branches: + - main + - just-the-basics + +# https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure?tabs=azure-portal%2Clinux#set-up-azure-login-with-openid-connect-authentication +permissions: + id-token: write + contents: read + +jobs: + build: + runs-on: ubuntu-latest + container: + image: mcr.microsoft.com/azure-dev-cli-apps:latest + env: + AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} + AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} + AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} + AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} + AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Log in with Azure (Federated Credentials) + if: ${{ env.AZURE_CLIENT_ID != '' }} + run: | + azd auth login ` + --client-id "$Env:AZURE_CLIENT_ID" ` + --federated-credential-provider "github" ` + --tenant-id "$Env:AZURE_TENANT_ID" + shell: pwsh + + - name: Log in with Azure (Client Credentials) + if: ${{ env.AZURE_CREDENTIALS != '' }} + run: | + $info = $Env:AZURE_CREDENTIALS | ConvertFrom-Json -AsHashtable; + Write-Host "::add-mask::$($info.clientSecret)" + + azd auth login ` + --client-id "$($info.clientId)" ` + --client-secret "$($info.clientSecret)" ` + --tenant-id "$($info.tenantId)" + shell: pwsh + env: + AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Provision Azure Resources - Azure Container Apps, Container Registry, Azure Monitor, Log Analytics + run: azd provision --no-prompt + env: + AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} + AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} + + - name: Deploy changes to Azure Container Apps + run: azd deploy --no-prompt + env: + AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} + AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..be98434 --- /dev/null +++ b/.gitignore @@ -0,0 +1,166 @@ +# environment name +demoplugin/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + + +.azure \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..99cd994 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: FastAPI", + "type": "python", + "request": "launch", + "module": "uvicorn", + "args": [ + "main:app", + "--reload" + ], + "jinja": true, + "justMyCode": true + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ae262f9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,182 @@ +{ + "python.testing.pytestArgs": ["."], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "json.schemas": [ + { + "fileMatch": ["ai-plugin.json"], + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "schema_version": { + "type": "string", + "description": "Manifest schema version", + "examples": ["v1"] + }, + "name_for_model": { + "type": "string", + "maxLength": 50, + "pattern": "^[a-zA-Z0-9]*$", + "title": "Name for model", + "description": "Name the model will use to target the plugin (no spaces allowed, only letters and numbers). 50 character max." + }, + "name_for_human": { + "type": "string", + "maxLength": 20, + "description": "Human-readable name, such as the full company name. 20 character max." + }, + "description_for_model": { + "type": "string", + "maxLength": 8000, + "description": "Description better tailored to the model, such as token context length considerations or keyword usage for improved plugin prompting. 8,000 character max." + }, + "description_for_human": { + "type": "string", + "maxLength": 100, + "description": "Human-readable description of the plugin. 100 character max." + }, + "auth": { + "type": "object", + "description": "Authentication schema", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["none"] + } + }, + "required": ["type"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["service_http"] + }, + "authorization_type": { + "type": "string", + "enum": ["bearer", "basic"] + }, + "verification_tokens": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "type", + "authorization_type", + "verification_tokens" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["user_http"] + }, + "authorization_type": { + "type": "string", + "enum": ["bearer", "basic"] + } + }, + "required": ["type", "authorization_type"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["oauth"] + }, + "client_url": { + "type": "string", + "format": "uri" + }, + "scope": { + "type": "string" + }, + "authorization_url": { + "type": "string", + "format": "uri" + }, + "authorization_content_type": { + "type": "string" + }, + "verification_tokens": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "type", + "client_url", + "scope", + "authorization_url", + "authorization_content_type", + "verification_tokens" + ] + } + ] + }, + "api": { + "type": "object", + "description": "API specification", + "properties": { + "type": { + "type": "string", + "enum": ["openapi"], + "description": "API specification type" + }, + "is_user_authenticated": { + "type": "boolean", + "description": "Indicates whether the user is authenticated" + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL of the API specification" + } + }, + "required": ["type", "is_user_authenticated", "url"] + }, + "logo_url": { + "type": "string", + "format": "uri", + "description": "URL used to fetch the logo. Suggested size: 512 x 512. Transparent backgrounds are supported." + }, + "contact_email": { + "type": "string", + "format": "email", + "description": "Email contact for safety/moderation, support, and deactivation" + }, + "legal_info_url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "schema_version", + "name_for_model", + "name_for_human", + "description_for_model", + "description_for_human", + "auth", + "api", + "logo_url", + "contact_email", + "legal_info_url" + ] + } + } + ] + } + \ No newline at end of file diff --git a/.well-known/ai-plugin.json b/.well-known/ai-plugin.json new file mode 100644 index 0000000..eb21367 --- /dev/null +++ b/.well-known/ai-plugin.json @@ -0,0 +1,18 @@ +{ + "schema_version": "v1", + "name_for_model": "contosoproducts", + "name_for_human": "Contoso Product Search", + "description_for_model": "Plugin for searching through my shop offering various outdoor and sports products. Use it whenever a user asks anything about outdoor, climbing, hiking or sports products.", + "description_for_human": "Contoso Outdoor and Sports Products Search.", + "auth": { + "type": "none" + }, + "api": { + "type": "openapi", + "is_user_authenticated": "false", + "url": "$host/openapi.json" + }, + "logo_url": "$host/assets/imgs/logo.png", + "contact_email": "noreply@microsoft.com", + "legal_info_url": "https://www.microsoft.com/en-us/legal/" +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9d06c2c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.10 + +COPY . . + +RUN pip install --no-cache-dir --upgrade -r requirements.txt + +CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port ${PORT:-${WEBSITES_PORT:-8080}}"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9ed1403 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright 2023 (c) Microsoft Corporation. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..32111b6 --- /dev/null +++ b/README.md @@ -0,0 +1,96 @@ +# AI Plugin Quickstart (Python 🐍) + +This is a quickstart for creating an AI plugin, from writing a simple API server to running it in ChatGPT. + + +[![Open in GitHub Codespaces](https://img.shields.io/static/v1?style=for-the-badge&label=GitHub+Codespaces&message=Open&color=lightgrey&logo=github)](https://codespaces.new/azure-samples/openai-plugin-fastapi) +[![Open in Dev Container](https://img.shields.io/static/v1?style=for-the-badge&label=Dev+Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/azure-samples/openai-plugin-fastapi) + +## Getting started + +1. **📤 One-click setup**: [Open a new Codespace](https://codespaces.new/azure-samples/openai-plugin-fastapi), giving you a fully configured cloud developer environment. +2. **🪄 Make an API**: Add routes in `main.py`, done in a few minutes even without [FastAPI](https://fastapi.tiangolo.com/lo/tutorial/) experience, thanks to [GitHub Copilot](https://github.com/features/copilot/). +3. **▶️ Run, one-click again**: Use VS Code's built-in *Run* command and open the forwarded port *8000* in your browser. +4. **💬 Test in ChatGPT**: Copy the URL (make sure its public) and paste it in ChatGPT's [Develop your own plugin](https://platform.openai.com/docs/plugins/getting-started/debugging) flow. +5. **🔄 Iterate quickly:** Codespaces updates the server on each save, and VS Code's debugger lets you dig into the code execution. + + + +## Run + +### Run in Codespaces +1. Click here to open in GitHub Codespaces + + [![Open in GitHub Codespaces](https://img.shields.io/static/v1?style=for-the-badge&label=GitHub+Codespaces&message=Open&color=lightgrey&logo=github)](https://codespaces.new/azure-samples/openai-plugin-fastapi) + +1. Open Codespaces Ports tab, right click 8000, and make it public. +1. Copy the Codesapces address for port 8000 +1. Open Chat GPT and add the plugin with the Codespaces address +1. Run a query for 'hiking boots' + +### Run in Dev Container + +1. Click here to open in Dev Container + + [![Open in Dev Container](https://img.shields.io/static/v1?style=for-the-badge&label=Dev+Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/azure-samples/openai-plugin-fastapi) + +1. Hit F5 to start the API +1. Open Chat GPT and add the plugin with `localhost:8000` +1. Run a query for 'hiking boots' + + +### Run Locally + +1. Clone the repo to your local machine `git clone https://github.com/azure-samples/openai-plugin-fastapi` +1. Open repo in VS Code +1. Create a new Python virtual environment and activate it +1. Hit F5 to start the API +1. Open Chat GPT and add the plugin with `localhost:8000` +1. Run a query for 'hiking boots' + +## Deploy to Azure + +> NOTE: If you are running locally, then you first need to [install the Azure Developer CLI](https://aka.ms/azd/install) + +### Deploy with Azure Developer CLI + +1. Open a terminal +1. Run `azd auth login` +1. Run `azd up` +1. Copy the endpoint printed to the terminal +1. Open Chat GPT and add the plugin with that endpoint +1. Run a query for 'hiking boots' + +### Deploy with GitHub Actions + +1. Fork this repo to your own account +1. Open your fork in Codespaces, Dev Container or Local +1. Open a terminal +1. Run `azd auth login` +1. Run `azd pipeline config` +1. Click on the printed actions link. Scroll to the bottom of the logs to find the endpoint. +1. Open Chat GPT and add the plugin with that endpoint +1. Run a query for 'hiking boots' + + +## Contributing + +This project welcomes contributions and suggestions. Most contributions require you to agree to a +Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us +the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. + +When you submit a pull request, a CLA bot will automatically determine whether you need to provide +a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions +provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## Trademarks + +This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft +trademarks or logos is subject to and must follow +[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). +Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. +Any use of third-party trademarks or logos are subject to those third-party's policies. \ No newline at end of file diff --git a/assets/imgs/logo.png b/assets/imgs/logo.png new file mode 100644 index 0000000..d57e9cd Binary files /dev/null and b/assets/imgs/logo.png differ diff --git a/azure.yaml b/azure.yaml new file mode 100644 index 0000000..cc285ec --- /dev/null +++ b/azure.yaml @@ -0,0 +1,10 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: openai-plugin-fastapi +metadata: + template: openai-plugin-fastapi@0.0.1-beta +services: + api: + project: . + host: containerapp + language: python \ No newline at end of file diff --git a/data/products.json b/data/products.json new file mode 100644 index 0000000..d685f70 --- /dev/null +++ b/data/products.json @@ -0,0 +1,443 @@ +[ + { + "name": "Contoso Climbing Shoes", + "description": "High-performance climbing shoes for advanced climbers", + "category": "Climbing", + "size": "8", + "price": 149.99 + }, + { + "name": "Contoso Hiking Boots", + "description": "Durable hiking boots for all-day comfort on the trails", + "category": "Hiking", + "size": "10", + "price": 129.99 + }, + { + "name": "Contoso Climbing Rope", + "description": "Dynamic climbing rope for lead climbing and top roping", + "category": "Climbing", + "size": "60m", + "price": 199.99 + }, + { + "name": "Contoso Hiking Backpack", + "description": "Lightweight backpack with ample storage for day hikes", + "category": "Hiking", + "size": "One Size", + "price": 79.99 + }, + { + "name": "Contoso Climbing Harness", + "description": "Comfortable and adjustable harness for all types of climbing", + "category": "Climbing", + "size": "Medium", + "price": 89.99 + }, + { + "name": "Contoso Hiking Poles", + "description": "Collapsible hiking poles for added stability on the trails", + "category": "Hiking", + "size": "One Size", + "price": 49.99 + }, + { + "name": "Contoso Climbing Chalk Bag", + "description": "Durable chalk bag with a drawstring closure for easy access", + "category": "Climbing", + "size": "One Size", + "price": 24.99 + }, + { + "name": "Contoso Hiking Hat", + "description": "Breathable and moisture-wicking hat for sunny hikes", + "category": "Hiking", + "size": "One Size", + "price": 19.99 + }, + { + "name": "Contoso Climbing Quickdraws", + "description": "Set of 6 quickdraws for sport climbing and trad climbing", + "category": "Climbing", + "size": "One Size", + "price": 119.99 + }, + { + "name": "Contoso Hiking Jacket", + "description": "Waterproof and breathable jacket for rainy hikes", + "category": "Hiking", + "size": "Large", + "price": 149.99 + }, + { + "name": "Contoso Climbing Helmet", + "description": "Lightweight and durable helmet for rock climbing and mountaineering", + "category": "Climbing", + "size": "Medium", + "price": 99.99 + }, + { + "name": "Contoso Hiking Pants", + "description": "Quick-drying and stretchy pants for hiking and backpacking", + "category": "Hiking", + "size": "Small", + "price": 69.99 + }, + { + "name": "Contoso Climbing Carabiners", + "description": "Set of 10 carabiners for climbing and rappelling", + "category": "Climbing", + "size": "One Size", + "price": 79.99 + }, + { + "name": "Contoso Hiking Socks", + "description": "Moisture-wicking and cushioned socks for long hikes", + "category": "Hiking", + "size": "Medium", + "price": 14.99 + }, + { + "name": "Contoso Climbing Pulley", + "description": "Durable and lightweight pulley for hauling gear on big walls", + "category": "Climbing", + "size": "One Size", + "price": 39.99 + }, + { + "name": "Contoso Hiking Water Bottle", + "description": "Stainless steel water bottle with a carabiner clip for easy carrying", + "category": "Hiking", + "size": "20 oz", + "price": 24.99 + }, + { + "name": "Contoso Climbing Slings", + "description": "Set of 3 slings for building anchors and extending placements", + "category": "Climbing", + "size": "One Size", + "price": 29.99 + }, + { + "name": "Contoso Hiking GPS", + "description": "Handheld GPS device with preloaded maps for navigation on the trails", + "category": "Hiking", + "size": "One Size", + "price": 199.99 + }, + { + "name": "Contoso Climbing Nut Tool", + "description": "Multi-functional tool for removing stuck nuts and cleaning gear", + "category": "Climbing", + "size": "One Size", + "price": 19.99 + }, + { + "name": "Contoso Hiking Headlamp", + "description": "Lightweight and rechargeable headlamp for night hikes and camping", + "category": "Hiking", + "size": "One Size", + "price": 49.99 + }, + { + "name": "Contoso Climbing Anchor Kit", + "description": "Complete kit for building anchors on multi-pitch climbs", + "category": "Climbing", + "size": "One Size", + "price": 299.99 + }, + { + "name": "Contoso Hiking Sleeping Bag", + "description": "Lightweight and compressible sleeping bag for backpacking", + "category": "Hiking", + "size": "Regular", + "price": 199.99 + }, + { + "name": "Contoso Climbing Camalot", + "description": "Durable and versatile camming device for rock climbing", + "category": "Climbing", + "size": "Size 2", + "price": 89.99 + }, + { + "name": "Contoso Hiking Tent", + "description": "Lightweight and waterproof tent for backpacking and camping", + "category": "Hiking", + "size": "2-Person", + "price": 299.99 + }, + { + "name": "Contoso Climbing Nut", + "description": "Durable and lightweight nut for passive protection on trad climbs", + "category": "Climbing", + "size": "Size 5", + "price": 14.99 + }, + { + "name": "Contoso Hiking Stove", + "description": "Compact and efficient stove for cooking meals on the trail", + "category": "Hiking", + "size": "One Size", + "price": 79.99 + }, + { + "name": "Contoso Climbing Ascender", + "description": "Mechanical ascender for ascending ropes on big walls and aid climbs", + "category": "Climbing", + "size": "One Size", + "price": 129.99 + }, + { + "name": "Contoso Hiking Gaiters", + "description": "Waterproof and breathable gaiters for keeping debris out of your boots", + "category": "Hiking", + "size": "Medium", + "price": 39.99 + }, + { + "name": "Contoso Climbing Hex", + "description": "Durable and versatile hex for passive protection on trad climbs", + "category": "Climbing", + "size": "Size 4", + "price": 24.99 + }, + { + "name": "Contoso Hiking Compass", + "description": "Compact and accurate compass for navigation on the trails", + "category": "Hiking", + "size": "One Size", + "price": 9.99 + }, + { + "name": "Contoso Climbing Cams", + "description": "Set of 3 camming devices for rock climbing and aid climbing", + "category": "Climbing", + "size": "Sizes 1-3", + "price": 299.99 + }, + { + "name": "Contoso Hiking First Aid Kit", + "description": "Compact and comprehensive first aid kit for emergencies on the trails", + "category": "Hiking", + "size": "One Size", + "price": 49.99 + }, + { + "name": "Contoso Climbing Piton", + "description": "Durable and versatile piton for aid climbing and big wall climbing", + "category": "Climbing", + "size": "Size 2", + "price": 19.99 + }, + { + "name": "Contoso Hiking Sunscreen", + "description": "Water-resistant and sweat-resistant sunscreen for sunny hikes", + "category": "Hiking", + "size": "3 oz", + "price": 14.99 + }, + { + "name": "Contoso Climbing Pullover", + "description": "Warm and breathable pullover for chilly days at the crag", + "category": "Climbing", + "size": "Large", + "price": 79.99 + }, + { + "name": "Contoso Hiking Map", + "description": "Waterproof and tear-resistant map for navigation on the trails", + "category": "Hiking", + "size": "One Size", + "price": 9.99 + }, + { + "name": "Contoso Climbing Nut Tool Set", + "description": "Set of 3 multi-functional nut tools for removing stuck nuts and cleaning gear", + "category": "Climbing", + "size": "One Size", + "price": 49.99 + }, + { + "name": "Contoso Hiking Insoles", + "description": "Cushioned and supportive insoles for all-day comfort on the trails", + "category": "Hiking", + "size": "Medium", + "price": 29.99 + }, + { + "name": "Contoso Climbing Rope Bag", + "description": "Durable and spacious bag for storing and transporting climbing ropes", + "category": "Climbing", + "size": "One Size", + "price": 59.99 + }, + { + "name": "Contoso Hiking Rain Poncho", + "description": "Lightweight and waterproof poncho for unexpected rain on the trails", + "category": "Hiking", + "size": "One Size", + "price": 29.99 + }, + { + "name": "Contoso Climbing Anchor Cord", + "description": "Durable and static cord for building anchors on multi-pitch climbs", + "category": "Climbing", + "size": "60m", + "price": 99.99 + }, + { + "name": "Contoso Hiking Bug Spray", + "description": "DEET-free bug spray for repelling mosquitoes and ticks on the trails", + "category": "Hiking", + "size": "4 oz", + "price": 12.99 + }, + { + "name": "Contoso Climbing Nut Set", + "description": "Set of 6 passive nuts for trad climbing and aid climbing", + "category": "Climbing", + "size": "Sizes 1-6", + "price": 99.99 + }, + { + "name": "Contoso Hiking Multitool", + "description": "Compact and versatile multitool for repairs and emergencies on the trails", + "category": "Hiking", + "size": "One Size", + "price": 39.99 + }, + { + "name": "Contoso Climbing Anchor Webbing", + "description": "Durable and versatile webbing for building anchors on multi-pitch climbs", + "category": "Climbing", + "size": "30ft", + "price": 49.99 + }, + { + "name": "Contoso Hiking Sunglasses", + "description": "Polarized and UV-resistant sunglasses for sunny hikes", + "category": "Hiking", + "size": "One Size", + "price": 59.99 + }, + { + "name": "Contoso Climbing Nut Tool Holster", + "description": "Durable and adjustable holster for carrying a nut tool on your harness", + "category": "Climbing", + "size": "One Size", + "price": 14.99 + }, + { + "name": "Contoso Hiking Camp Stove", + "description": "Compact and efficient camp stove for cooking meals at the campsite", + "category": "Hiking", + "size": "One Size", + "price": 99.99 + }, + { + "name": "Contoso Climbing Carabiner Set", + "description": "Set of 10 locking carabiners for building anchors and rappelling", + "category": "Climbing", + "size": "One Size", + "price": 149.99 + }, + { + "name": "Contoso Hiking Sleeping Pad", + "description": "Lightweight and inflatable sleeping pad for backpacking", + "category": "Hiking", + "size": "Regular", + "price": 99.99 + }, + { + "name": "Contoso Climbing Nut Tool Leash", + "description": "Durable and adjustable leash for securing a nut tool to your harness", + "category": "Climbing", + "size": "One Size", + "price": 9.99 + }, + { + "name": "Contoso Hiking Trekking Poles", + "description": "Collapsible and adjustable trekking poles for added stability on the trails", + "category": "Hiking", + "size": "One Size", + "price": 69.99 + }, + { + "name": "Contoso Climbing Anchor Hanger", + "description": "Durable and versatile hanger for building anchors on multi-pitch climbs", + "category": "Climbing", + "size": "One Size", + "price": 19.99 + }, + { + "name": "Contoso Hiking Water Filter", + "description": "Compact and efficient water filter for purifying water on the trails", + "category": "Hiking", + "size": "One Size", + "price": 79.99 + }, + { + "name": "Contoso Climbing Nut Tool Kit", + "description": "Complete kit for removing stuck nuts and cleaning gear on trad climbs", + "category": "Climbing", + "size": "One Size", + "price": 79.99 + }, + { + "name": "Contoso Hiking Camp Chair", + "description": "Lightweight and portable camp chair for relaxing at the campsite", + "category": "Hiking", + "size": "One Size", + "price": 49.99 + }, + { + "name": "Contoso Climbing Anchor Bolt", + "description": "Durable and versatile bolt for building anchors on multi-pitch climbs", + "category": "Climbing", + "size": "3/8in", + "price": 4.99 + }, + { + "name": "Contoso Hiking Camp Table", + "description": "Lightweight and portable camp table for cooking and eating at the campsite", + "category": "Hiking", + "size": "One Size", + "price": 99.99 + }, + { + "name": "Contoso Climbing Anchor Nut", + "description": "Durable and versatile nut for building anchors on multi-pitch climbs", + "category": "Climbing", + "size": "Size 3", + "price": 9.99 + }, + { + "name": "Contoso Hiking Camp Lantern", + "description": "Compact and rechargeable camp lantern for lighting up the campsite", + "category": "Hiking", + "size": "One Size", + "price": 29.99 + }, + { + "name": "Contoso Climbing Anchor Bolt Hanger", + "description": "Durable and versatile hanger for attaching bolts to anchors on multi-pitch climbs", + "category": "Climbing", + "size": "One Size", + "price": 14.99 + }, + { + "name": "Contoso Hiking Camp Cookware Set", + "description": "Compact and lightweight cookware set for cooking meals at the campsite", + "category": "Hiking", + "size": "One Size", + "price": 59.99 + }, + { + "name": "Contoso Climbing Anchor Bolt Nut", + "description": "Durable and versatile nut for attaching bolts to anchors on multi-pitch climbs", + "category": "Climbing", + "size": "3/8in", + "price": 2.99 + } +] diff --git a/infra/abbreviations.json b/infra/abbreviations.json new file mode 100644 index 0000000..703e503 --- /dev/null +++ b/infra/abbreviations.json @@ -0,0 +1,135 @@ +{ + "analysisServicesServers": "as", + "apiManagementService": "apim-", + "appConfigurationConfigurationStores": "appcs-", + "appManagedEnvironments": "cae-", + "appContainerApps": "ca-", + "authorizationPolicyDefinitions": "policy-", + "automationAutomationAccounts": "aa-", + "blueprintBlueprints": "bp-", + "blueprintBlueprintsArtifacts": "bpa-", + "cacheRedis": "redis-", + "cdnProfiles": "cdnp-", + "cdnProfilesEndpoints": "cdne-", + "cognitiveServicesAccounts": "cog-", + "cognitiveServicesFormRecognizer": "cog-fr-", + "cognitiveServicesTextAnalytics": "cog-ta-", + "computeAvailabilitySets": "avail-", + "computeCloudServices": "cld-", + "computeDiskEncryptionSets": "des", + "computeDisks": "disk", + "computeDisksOs": "osdisk", + "computeGalleries": "gal", + "computeSnapshots": "snap-", + "computeVirtualMachines": "vm", + "computeVirtualMachineScaleSets": "vmss-", + "containerInstanceContainerGroups": "ci", + "containerRegistryRegistries": "cr", + "containerServiceManagedClusters": "aks-", + "databricksWorkspaces": "dbw-", + "dataFactoryFactories": "adf-", + "dataLakeAnalyticsAccounts": "dla", + "dataLakeStoreAccounts": "dls", + "dataMigrationServices": "dms-", + "dBforMySQLServers": "mysql-", + "dBforPostgreSQLServers": "psql-", + "devicesIotHubs": "iot-", + "devicesProvisioningServices": "provs-", + "devicesProvisioningServicesCertificates": "pcert-", + "documentDBDatabaseAccounts": "cosmos-", + "eventGridDomains": "evgd-", + "eventGridDomainsTopics": "evgt-", + "eventGridEventSubscriptions": "evgs-", + "eventHubNamespaces": "evhns-", + "eventHubNamespacesEventHubs": "evh-", + "hdInsightClustersHadoop": "hadoop-", + "hdInsightClustersHbase": "hbase-", + "hdInsightClustersKafka": "kafka-", + "hdInsightClustersMl": "mls-", + "hdInsightClustersSpark": "spark-", + "hdInsightClustersStorm": "storm-", + "hybridComputeMachines": "arcs-", + "insightsActionGroups": "ag-", + "insightsComponents": "appi-", + "keyVaultVaults": "kv-", + "kubernetesConnectedClusters": "arck", + "kustoClusters": "dec", + "kustoClustersDatabases": "dedb", + "logicIntegrationAccounts": "ia-", + "logicWorkflows": "logic-", + "machineLearningServicesWorkspaces": "mlw-", + "managedIdentityUserAssignedIdentities": "id-", + "managementManagementGroups": "mg-", + "migrateAssessmentProjects": "migr-", + "networkApplicationGateways": "agw-", + "networkApplicationSecurityGroups": "asg-", + "networkAzureFirewalls": "afw-", + "networkBastionHosts": "bas-", + "networkConnections": "con-", + "networkDnsZones": "dnsz-", + "networkExpressRouteCircuits": "erc-", + "networkFirewallPolicies": "afwp-", + "networkFirewallPoliciesWebApplication": "waf", + "networkFirewallPoliciesRuleGroups": "wafrg", + "networkFrontDoors": "fd-", + "networkFrontdoorWebApplicationFirewallPolicies": "fdfp-", + "networkLoadBalancersExternal": "lbe-", + "networkLoadBalancersInternal": "lbi-", + "networkLoadBalancersInboundNatRules": "rule-", + "networkLocalNetworkGateways": "lgw-", + "networkNatGateways": "ng-", + "networkNetworkInterfaces": "nic-", + "networkNetworkSecurityGroups": "nsg-", + "networkNetworkSecurityGroupsSecurityRules": "nsgsr-", + "networkNetworkWatchers": "nw-", + "networkPrivateDnsZones": "pdnsz-", + "networkPrivateLinkServices": "pl-", + "networkPublicIPAddresses": "pip-", + "networkPublicIPPrefixes": "ippre-", + "networkRouteFilters": "rf-", + "networkRouteTables": "rt-", + "networkRouteTablesRoutes": "udr-", + "networkTrafficManagerProfiles": "traf-", + "networkVirtualNetworkGateways": "vgw-", + "networkVirtualNetworks": "vnet-", + "networkVirtualNetworksSubnets": "snet-", + "networkVirtualNetworksVirtualNetworkPeerings": "peer-", + "networkVirtualWans": "vwan-", + "networkVpnGateways": "vpng-", + "networkVpnGatewaysVpnConnections": "vcn-", + "networkVpnGatewaysVpnSites": "vst-", + "notificationHubsNamespaces": "ntfns-", + "notificationHubsNamespacesNotificationHubs": "ntf-", + "operationalInsightsWorkspaces": "log-", + "portalDashboards": "dash-", + "powerBIDedicatedCapacities": "pbi-", + "purviewAccounts": "pview-", + "recoveryServicesVaults": "rsv-", + "resourcesResourceGroups": "rg-", + "searchSearchServices": "srch-", + "serviceBusNamespaces": "sb-", + "serviceBusNamespacesQueues": "sbq-", + "serviceBusNamespacesTopics": "sbt-", + "serviceEndPointPolicies": "se-", + "serviceFabricClusters": "sf-", + "signalRServiceSignalR": "sigr", + "sqlManagedInstances": "sqlmi-", + "sqlServers": "sql-", + "sqlServersDataWarehouse": "sqldw-", + "sqlServersDatabases": "sqldb-", + "sqlServersDatabasesStretch": "sqlstrdb-", + "storageStorageAccounts": "st", + "storageStorageAccountsVm": "stvm", + "storSimpleManagers": "ssimp", + "streamAnalyticsCluster": "asa-", + "synapseWorkspaces": "syn", + "synapseWorkspacesAnalyticsWorkspaces": "synw", + "synapseWorkspacesSqlPoolsDedicated": "syndp", + "synapseWorkspacesSqlPoolsSpark": "synsp", + "timeSeriesInsightsEnvironments": "tsi-", + "webServerFarms": "plan-", + "webSitesAppService": "app-", + "webSitesAppServiceEnvironment": "ase-", + "webSitesFunctions": "func-", + "webStaticSites": "stapp-" +} diff --git a/infra/app/api.bicep b/infra/app/api.bicep new file mode 100644 index 0000000..f99be85 --- /dev/null +++ b/infra/app/api.bicep @@ -0,0 +1,48 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +param identityName string +param applicationInsightsName string +param containerAppsEnvironmentName string +param containerRegistryName string +param serviceName string = 'api' +param exists bool + +resource apiIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: identityName + location: location +} + +module app '../core/host/container-app-upsert.bicep' = { + name: '${serviceName}-container-app' + params: { + name: name + location: location + tags: union(tags, { 'azd-service-name': serviceName }) + identityName: identityName + exists: exists + containerAppsEnvironmentName: containerAppsEnvironmentName + containerRegistryName: containerRegistryName + env: [ + { + name: 'AZURE_CLIENT_ID' + value: apiIdentity.properties.clientId + } + { + name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' + value: applicationInsights.properties.ConnectionString + } + ] + targetPort: 8080 + } +} + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { + name: applicationInsightsName +} + +output SERVICE_API_IDENTITY_PRINCIPAL_ID string = apiIdentity.properties.principalId +output SERVICE_API_NAME string = app.outputs.name +output SERVICE_API_URI string = app.outputs.uri +output SERVICE_API_IMAGE_NAME string = app.outputs.imageName diff --git a/infra/core/host/container-app-upsert.bicep b/infra/core/host/container-app-upsert.bicep new file mode 100644 index 0000000..c7f83f3 --- /dev/null +++ b/infra/core/host/container-app-upsert.bicep @@ -0,0 +1,50 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +param containerAppsEnvironmentName string +param containerName string = 'main' +param containerRegistryName string +param secrets array = [] +param env array = [] +param external bool = true +param targetPort int = 80 +param exists bool + +@description('User assigned identity name') +param identityName string = '' + +@description('CPU cores allocated to a single container instance, e.g. 0.5') +param containerCpuCoreCount string = '0.5' + +@description('Memory allocated to a single container instance, e.g. 1Gi') +param containerMemory string = '1.0Gi' + +resource existingApp 'Microsoft.App/containerApps@2022-03-01' existing = if (exists) { + name: name +} + +module app 'container-app.bicep' = { + name: '${deployment().name}-update' + params: { + name: name + location: location + tags: tags + identityName: identityName + containerName: containerName + containerAppsEnvironmentName: containerAppsEnvironmentName + containerRegistryName: containerRegistryName + containerCpuCoreCount: containerCpuCoreCount + containerMemory: containerMemory + secrets: secrets + external: external + env: env + imageName: exists ? existingApp.properties.template.containers[0].image : '' + targetPort: targetPort + } +} + +output defaultDomain string = app.outputs.defaultDomain +output imageName string = app.outputs.imageName +output name string = app.outputs.name +output uri string = app.outputs.uri diff --git a/infra/core/host/container-app.bicep b/infra/core/host/container-app.bicep new file mode 100644 index 0000000..45631d7 --- /dev/null +++ b/infra/core/host/container-app.bicep @@ -0,0 +1,93 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +param containerAppsEnvironmentName string +param containerName string = 'main' +param containerRegistryName string +param secrets array = [] +param env array = [] +param external bool = true +param imageName string +param targetPort int = 80 + +@description('User assigned identity name') +param identityName string = '' + +@description('CPU cores allocated to a single container instance, e.g. 0.5') +param containerCpuCoreCount string = '0.5' + +@description('Memory allocated to a single container instance, e.g. 1Gi') +param containerMemory string = '1.0Gi' + +resource userIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { + name: identityName +} + +module containerRegistryAccess '../security/registry-access.bicep' = { + name: '${deployment().name}-registry-access' + params: { + containerRegistryName: containerRegistryName + principalId: userIdentity.properties.principalId + } +} + +resource app 'Microsoft.App/containerApps@2022-03-01' = { + name: name + location: location + tags: tags + // It is critical that the identity is granted ACR pull access before the app is created + // otherwise the container app will throw a provision error + // This also forces us to use an user assigned managed identity since there would no way to + // provide the system assigned identity with the ACR pull access before the app is created + dependsOn: [ containerRegistryAccess ] + identity: { + type: 'UserAssigned' + userAssignedIdentities: { '${userIdentity.id}': {} } + } + properties: { + managedEnvironmentId: containerAppsEnvironment.id + configuration: { + activeRevisionsMode: 'single' + ingress: { + external: external + targetPort: targetPort + transport: 'auto' + } + secrets: secrets + registries: [ + { + server: '${containerRegistry.name}.azurecr.io' + identity: userIdentity.id + } + ] + } + template: { + containers: [ + { + image: !empty(imageName) ? imageName : 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' + name: containerName + env: env + resources: { + cpu: json(containerCpuCoreCount) + memory: containerMemory + } + } + ] + } + } +} + +resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2022-03-01' existing = { + name: containerAppsEnvironmentName +} + +// 2022-02-01-preview needed for anonymousPullEnabled +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2022-02-01-preview' existing = { + name: containerRegistryName +} + +output defaultDomain string = containerAppsEnvironment.properties.defaultDomain +output imageName string = imageName +output name string = app.name +output uri string = 'https://${app.properties.configuration.ingress.fqdn}' diff --git a/infra/core/host/container-apps-environment.bicep b/infra/core/host/container-apps-environment.bicep new file mode 100644 index 0000000..dbe953d --- /dev/null +++ b/infra/core/host/container-apps-environment.bicep @@ -0,0 +1,27 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +param logAnalyticsWorkspaceName string + +resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2022-03-01' = { + name: name + location: location + tags: tags + properties: { + appLogsConfiguration: { + destination: 'log-analytics' + logAnalyticsConfiguration: { + customerId: logAnalyticsWorkspace.properties.customerId + sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey + } + } + } +} + +resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = { + name: logAnalyticsWorkspaceName +} + +output defaultDomain string = containerAppsEnvironment.properties.defaultDomain +output name string = containerAppsEnvironment.name diff --git a/infra/core/host/container-apps.bicep b/infra/core/host/container-apps.bicep new file mode 100644 index 0000000..245ed7f --- /dev/null +++ b/infra/core/host/container-apps.bicep @@ -0,0 +1,31 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +param containerAppsEnvironmentName string +param containerRegistryName string +param logAnalyticsWorkspaceName string + +module containerAppsEnvironment 'container-apps-environment.bicep' = { + name: '${name}-container-apps-environment' + params: { + name: containerAppsEnvironmentName + location: location + tags: tags + logAnalyticsWorkspaceName: logAnalyticsWorkspaceName + } +} + +module containerRegistry 'container-registry.bicep' = { + name: '${name}-container-registry' + params: { + name: containerRegistryName + location: location + tags: tags + } +} + +output defaultDomain string = containerAppsEnvironment.outputs.defaultDomain +output environmentName string = containerAppsEnvironment.outputs.name +output registryLoginServer string = containerRegistry.outputs.loginServer +output registryName string = containerRegistry.outputs.name diff --git a/infra/core/host/container-registry.bicep b/infra/core/host/container-registry.bicep new file mode 100644 index 0000000..c0ba201 --- /dev/null +++ b/infra/core/host/container-registry.bicep @@ -0,0 +1,67 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +param adminUserEnabled bool = true +param anonymousPullEnabled bool = false +param dataEndpointEnabled bool = false +param encryption object = { + status: 'disabled' +} +param networkRuleBypassOptions string = 'AzureServices' +param publicNetworkAccess string = 'Enabled' +param sku object = { + name: 'Basic' +} +param zoneRedundancy string = 'Disabled' + +@description('The log analytics workspace id used for logging & monitoring') +param workspaceId string = '' + +// 2022-02-01-preview needed for anonymousPullEnabled +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2022-02-01-preview' = { + name: name + location: location + tags: tags + sku: sku + properties: { + adminUserEnabled: adminUserEnabled + anonymousPullEnabled: anonymousPullEnabled + dataEndpointEnabled: dataEndpointEnabled + encryption: encryption + networkRuleBypassOptions: networkRuleBypassOptions + publicNetworkAccess: publicNetworkAccess + zoneRedundancy: zoneRedundancy + } +} + +// 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: 'registry-diagnostics' + scope: containerRegistry + properties: { + workspaceId: workspaceId + logs: [ + { + category: 'ContainerRegistryRepositoryEvents' + enabled: true + } + { + category: 'ContainerRegistryLoginEvents' + enabled: true + } + ] + metrics: [ + { + category: 'AllMetrics' + enabled: true + timeGrain: 'PT1M' + } + ] + } +} + +output loginServer string = containerRegistry.properties.loginServer +output name string = containerRegistry.name diff --git a/infra/core/monitor/applicationinsights-dashboard.bicep b/infra/core/monitor/applicationinsights-dashboard.bicep new file mode 100644 index 0000000..b7af2c1 --- /dev/null +++ b/infra/core/monitor/applicationinsights-dashboard.bicep @@ -0,0 +1,1235 @@ +param name string +param applicationInsightsName string +param location string = resourceGroup().location +param tags object = {} + +// 2020-09-01-preview because that is the latest valid version +resource applicationInsightsDashboard 'Microsoft.Portal/dashboards@2020-09-01-preview' = { + name: name + location: location + tags: tags + properties: { + lenses: [ + { + order: 0 + parts: [ + { + position: { + x: 0 + y: 0 + colSpan: 2 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'id' + value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + { + name: 'Version' + value: '1.0' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/AspNetOverviewPinnedPart' + asset: { + idInputName: 'id' + type: 'ApplicationInsights' + } + defaultMenuItemId: 'overview' + } + } + { + position: { + x: 2 + y: 0 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'Version' + value: '1.0' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/ProactiveDetectionAsyncPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + defaultMenuItemId: 'ProactiveDetection' + } + } + { + position: { + x: 3 + y: 0 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'ResourceId' + value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/QuickPulseButtonSmallPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + } + } + { + position: { + x: 4 + y: 0 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'TimeContext' + value: { + durationMs: 86400000 + endTime: null + createdTime: '2018-05-04T01:20:33.345Z' + isInitialTime: true + grain: 1 + useDashboardTimeRange: false + } + } + { + name: 'Version' + value: '1.0' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/AvailabilityNavButtonPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + } + } + { + position: { + x: 5 + y: 0 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'TimeContext' + value: { + durationMs: 86400000 + endTime: null + createdTime: '2018-05-08T18:47:35.237Z' + isInitialTime: true + grain: 1 + useDashboardTimeRange: false + } + } + { + name: 'ConfigurationId' + value: '78ce933e-e864-4b05-a27b-71fd55a6afad' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/AppMapButtonPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + } + } + { + position: { + x: 0 + y: 1 + colSpan: 3 + rowSpan: 1 + } + metadata: { + inputs: [] + type: 'Extension/HubsExtension/PartType/MarkdownPart' + settings: { + content: { + settings: { + content: '# Usage' + title: '' + subtitle: '' + } + } + } + } + } + { + position: { + x: 3 + y: 1 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'TimeContext' + value: { + durationMs: 86400000 + endTime: null + createdTime: '2018-05-04T01:22:35.782Z' + isInitialTime: true + grain: 1 + useDashboardTimeRange: false + } + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/UsageUsersOverviewPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + } + } + { + position: { + x: 4 + y: 1 + colSpan: 3 + rowSpan: 1 + } + metadata: { + inputs: [] + type: 'Extension/HubsExtension/PartType/MarkdownPart' + settings: { + content: { + settings: { + content: '# Reliability' + title: '' + subtitle: '' + } + } + } + } + } + { + position: { + x: 7 + y: 1 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ResourceId' + value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + { + name: 'DataModel' + value: { + version: '1.0.0' + timeContext: { + durationMs: 86400000 + createdTime: '2018-05-04T23:42:40.072Z' + isInitialTime: false + grain: 1 + useDashboardTimeRange: false + } + } + isOptional: true + } + { + name: 'ConfigurationId' + value: '8a02f7bf-ac0f-40e1-afe9-f0e72cfee77f' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/CuratedBladeFailuresPinnedPart' + isAdapter: true + asset: { + idInputName: 'ResourceId' + type: 'ApplicationInsights' + } + defaultMenuItemId: 'failures' + } + } + { + position: { + x: 8 + y: 1 + colSpan: 3 + rowSpan: 1 + } + metadata: { + inputs: [] + type: 'Extension/HubsExtension/PartType/MarkdownPart' + settings: { + content: { + settings: { + content: '# Responsiveness\r\n' + title: '' + subtitle: '' + } + } + } + } + } + { + position: { + x: 11 + y: 1 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ResourceId' + value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + { + name: 'DataModel' + value: { + version: '1.0.0' + timeContext: { + durationMs: 86400000 + createdTime: '2018-05-04T23:43:37.804Z' + isInitialTime: false + grain: 1 + useDashboardTimeRange: false + } + } + isOptional: true + } + { + name: 'ConfigurationId' + value: '2a8ede4f-2bee-4b9c-aed9-2db0e8a01865' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/CuratedBladePerformancePinnedPart' + isAdapter: true + asset: { + idInputName: 'ResourceId' + type: 'ApplicationInsights' + } + defaultMenuItemId: 'performance' + } + } + { + position: { + x: 12 + y: 1 + colSpan: 3 + rowSpan: 1 + } + metadata: { + inputs: [] + type: 'Extension/HubsExtension/PartType/MarkdownPart' + settings: { + content: { + settings: { + content: '# Browser' + title: '' + subtitle: '' + } + } + } + } + } + { + position: { + x: 15 + y: 1 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'MetricsExplorerJsonDefinitionId' + value: 'BrowserPerformanceTimelineMetrics' + } + { + name: 'TimeContext' + value: { + durationMs: 86400000 + createdTime: '2018-05-08T12:16:27.534Z' + isInitialTime: false + grain: 1 + useDashboardTimeRange: false + } + } + { + name: 'CurrentFilter' + value: { + eventTypes: [ + 4 + 1 + 3 + 5 + 2 + 6 + 13 + ] + typeFacets: {} + isPermissive: false + } + } + { + name: 'id' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'Version' + value: '1.0' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/MetricsExplorerBladePinnedPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + defaultMenuItemId: 'browser' + } + } + { + position: { + x: 0 + y: 2 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'sessions/count' + aggregationType: 5 + namespace: 'microsoft.insights/components/kusto' + metricVisualization: { + displayName: 'Sessions' + color: '#47BDF5' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'users/count' + aggregationType: 5 + namespace: 'microsoft.insights/components/kusto' + metricVisualization: { + displayName: 'Users' + color: '#7E58FF' + } + } + ] + title: 'Unique sessions and users' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + openBladeOnClick: { + openBlade: true + destinationBlade: { + extensionName: 'HubsExtension' + bladeName: 'ResourceMenuBlade' + parameters: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + menuid: 'segmentationUsers' + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 4 + y: 2 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'requests/failed' + aggregationType: 7 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Failed requests' + color: '#EC008C' + } + } + ] + title: 'Failed requests' + visualization: { + chartType: 3 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + openBladeOnClick: { + openBlade: true + destinationBlade: { + extensionName: 'HubsExtension' + bladeName: 'ResourceMenuBlade' + parameters: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + menuid: 'failures' + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 8 + y: 2 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'requests/duration' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Server response time' + color: '#00BCF2' + } + } + ] + title: 'Server response time' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + openBladeOnClick: { + openBlade: true + destinationBlade: { + extensionName: 'HubsExtension' + bladeName: 'ResourceMenuBlade' + parameters: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + menuid: 'performance' + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 12 + y: 2 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'browserTimings/networkDuration' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Page load network connect time' + color: '#7E58FF' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'browserTimings/processingDuration' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Client processing time' + color: '#44F1C8' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'browserTimings/sendDuration' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Send request time' + color: '#EB9371' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'browserTimings/receiveDuration' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Receiving response time' + color: '#0672F1' + } + } + ] + title: 'Average page load time breakdown' + visualization: { + chartType: 3 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 0 + y: 5 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'availabilityResults/availabilityPercentage' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Availability' + color: '#47BDF5' + } + } + ] + title: 'Average availability' + visualization: { + chartType: 3 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + openBladeOnClick: { + openBlade: true + destinationBlade: { + extensionName: 'HubsExtension' + bladeName: 'ResourceMenuBlade' + parameters: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + menuid: 'availability' + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 4 + y: 5 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'exceptions/server' + aggregationType: 7 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Server exceptions' + color: '#47BDF5' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'dependencies/failed' + aggregationType: 7 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Dependency failures' + color: '#7E58FF' + } + } + ] + title: 'Server exceptions and Dependency failures' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 8 + y: 5 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'performanceCounters/processorCpuPercentage' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Processor time' + color: '#47BDF5' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'performanceCounters/processCpuPercentage' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Process CPU' + color: '#7E58FF' + } + } + ] + title: 'Average processor and process CPU utilization' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 12 + y: 5 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'exceptions/browser' + aggregationType: 7 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Browser exceptions' + color: '#47BDF5' + } + } + ] + title: 'Browser exceptions' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 0 + y: 8 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'availabilityResults/count' + aggregationType: 7 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Availability test results count' + color: '#47BDF5' + } + } + ] + title: 'Availability test results count' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 4 + y: 8 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'performanceCounters/processIOBytesPerSecond' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Process IO rate' + color: '#47BDF5' + } + } + ] + title: 'Average process I/O rate' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 8 + y: 8 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'performanceCounters/memoryAvailableBytes' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Available memory' + color: '#47BDF5' + } + } + ] + title: 'Average available memory' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + ] + } + ] + } +} + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { + name: applicationInsightsName +} diff --git a/infra/core/monitor/applicationinsights.bicep b/infra/core/monitor/applicationinsights.bicep new file mode 100644 index 0000000..f76b292 --- /dev/null +++ b/infra/core/monitor/applicationinsights.bicep @@ -0,0 +1,30 @@ +param name string +param dashboardName string +param location string = resourceGroup().location +param tags object = {} + +param logAnalyticsWorkspaceId string + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { + name: name + location: location + tags: tags + kind: 'web' + properties: { + Application_Type: 'web' + WorkspaceResourceId: logAnalyticsWorkspaceId + } +} + +module applicationInsightsDashboard 'applicationinsights-dashboard.bicep' = { + name: 'application-insights-dashboard' + params: { + name: dashboardName + location: location + applicationInsightsName: applicationInsights.name + } +} + +output connectionString string = applicationInsights.properties.ConnectionString +output instrumentationKey string = applicationInsights.properties.InstrumentationKey +output name string = applicationInsights.name diff --git a/infra/core/monitor/loganalytics.bicep b/infra/core/monitor/loganalytics.bicep new file mode 100644 index 0000000..770544c --- /dev/null +++ b/infra/core/monitor/loganalytics.bicep @@ -0,0 +1,21 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = { + name: name + location: location + tags: tags + properties: any({ + retentionInDays: 30 + features: { + searchVersion: 1 + } + sku: { + name: 'PerGB2018' + } + }) +} + +output id string = logAnalytics.id +output name string = logAnalytics.name diff --git a/infra/core/monitor/monitoring.bicep b/infra/core/monitor/monitoring.bicep new file mode 100644 index 0000000..96ba11e --- /dev/null +++ b/infra/core/monitor/monitoring.bicep @@ -0,0 +1,31 @@ +param logAnalyticsName string +param applicationInsightsName string +param applicationInsightsDashboardName string +param location string = resourceGroup().location +param tags object = {} + +module logAnalytics 'loganalytics.bicep' = { + name: 'loganalytics' + params: { + name: logAnalyticsName + location: location + tags: tags + } +} + +module applicationInsights 'applicationinsights.bicep' = { + name: 'applicationinsights' + params: { + name: applicationInsightsName + location: location + tags: tags + dashboardName: applicationInsightsDashboardName + logAnalyticsWorkspaceId: logAnalytics.outputs.id + } +} + +output applicationInsightsConnectionString string = applicationInsights.outputs.connectionString +output applicationInsightsInstrumentationKey string = applicationInsights.outputs.instrumentationKey +output applicationInsightsName string = applicationInsights.outputs.name +output logAnalyticsWorkspaceId string = logAnalytics.outputs.id +output logAnalyticsWorkspaceName string = logAnalytics.outputs.name diff --git a/infra/core/security/registry-access.bicep b/infra/core/security/registry-access.bicep new file mode 100644 index 0000000..e17e404 --- /dev/null +++ b/infra/core/security/registry-access.bicep @@ -0,0 +1,18 @@ +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(subscription().id, resourceGroup().id, principalId, acrPullRole) + properties: { + roleDefinitionId: acrPullRole + principalType: 'ServicePrincipal' + principalId: principalId + } +} + +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2022-02-01-preview' existing = { + name: containerRegistryName +} diff --git a/infra/main.bicep b/infra/main.bicep new file mode 100644 index 0000000..821cfe8 --- /dev/null +++ b/infra/main.bicep @@ -0,0 +1,87 @@ +targetScope = 'subscription' + +@minLength(1) +@maxLength(64) +@description('Name of the the environment which is used to generate a short unique hash used in all resources.') +param environmentName string + +@minLength(1) +@description('Primary location for all resources') +param location string + +param resourceGroupName string = '' +param containerAppsEnvironmentName string = '' +param containerRegistryName string = '' +param apiContainerAppName string = '' +param applicationInsightsDashboardName string = '' +param applicationInsightsName string = '' +param logAnalyticsName string = '' + +param apiAppExists bool = false + +@description('Id of the user or app to assign application roles') +param principalId string = '' + +var abbrs = loadJsonContent('./abbreviations.json') +var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) +var tags = { 'azd-env-name': environmentName } + +// Organize resources in a resource group +resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: !empty(resourceGroupName) ? resourceGroupName : '${abbrs.resourcesResourceGroups}${environmentName}' + location: location + tags: tags +} + +// Container apps host (including container registry) +module containerApps './core/host/container-apps.bicep' = { + name: 'container-apps' + scope: resourceGroup + params: { + name: 'app' + containerAppsEnvironmentName: !empty(containerAppsEnvironmentName) ? containerAppsEnvironmentName : '${abbrs.appManagedEnvironments}${resourceToken}' + containerRegistryName: !empty(containerRegistryName) ? containerRegistryName : '${abbrs.containerRegistryRegistries}${resourceToken}' + location: location + logAnalyticsWorkspaceName: monitoring.outputs.logAnalyticsWorkspaceName + } +} + +// API +module api './app/api.bicep' = { + name: 'api' + scope: resourceGroup + params: { + name: !empty(apiContainerAppName) ? apiContainerAppName : '${abbrs.appContainerApps}api-${resourceToken}' + location: location + tags: tags + identityName: '${abbrs.managedIdentityUserAssignedIdentities}api-${resourceToken}' + applicationInsightsName: monitoring.outputs.applicationInsightsName + containerAppsEnvironmentName: containerApps.outputs.environmentName + containerRegistryName: containerApps.outputs.registryName + exists: apiAppExists + } +} + +// Monitor application with Azure Monitor +module monitoring './core/monitor/monitoring.bicep' = { + name: 'monitoring' + scope: resourceGroup + params: { + location: location + tags: tags + logAnalyticsName: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' + applicationInsightsName: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' + applicationInsightsDashboardName: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}' + } +} + +output AZURE_LOCATION string = location +output AZURE_TENANT_ID string = tenant().tenantId +output AZURE_RESOURCE_GROUP string = resourceGroup.name + +output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString +output APPLICATIONINSIGHTS_NAME string = monitoring.outputs.applicationInsightsName +output AZURE_CONTAINER_ENVIRONMENT_NAME string = containerApps.outputs.environmentName +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerApps.outputs.registryLoginServer +output AZURE_CONTAINER_REGISTRY_NAME string = containerApps.outputs.registryName +output SERVICE_API_NAME string = api.outputs.SERVICE_API_NAME diff --git a/infra/main.parameters.json b/infra/main.parameters.json new file mode 100644 index 0000000..2a1c1f1 --- /dev/null +++ b/infra/main.parameters.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "environmentName": { + "value": "${AZURE_ENV_NAME}" + }, + "location": { + "value": "${AZURE_LOCATION}" + }, + "principalId": { + "value": "${AZURE_PRINCIPAL_ID}" + }, + "apiAppExists": { + "value": "${SERVICE_API_RESOURCE_EXISTS=true}" + } + } +} diff --git a/main.py b/main.py new file mode 100644 index 0000000..ff9e51f --- /dev/null +++ b/main.py @@ -0,0 +1,23 @@ +from fastapi import FastAPI +from routers.wellknown import wellknown +from fastapi.middleware.cors import CORSMiddleware + +import json + +app = FastAPI() +app.include_router(wellknown) +app.add_middleware(CORSMiddleware, allow_origins=["https://chat.openai.com"]) + +# Load fake products from product.json +with open("./data/products.json", "r") as f: + products = json.load(f) + + +@app.get("/products") +async def get_products(query): + """Get products from the fake database""" + if query: + return [ + product for product in products if query.lower() in product["description"] + ] + return products diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d6777f2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +fastapi +uvicorn +pydantic +pytest +httpx \ No newline at end of file diff --git a/routers/__init__.py b/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routers/wellknown.py b/routers/wellknown.py new file mode 100644 index 0000000..7eca999 --- /dev/null +++ b/routers/wellknown.py @@ -0,0 +1,24 @@ +from string import Template +from fastapi import APIRouter, Request +from fastapi.responses import FileResponse, Response + +wellknown = APIRouter(prefix="/.well-known", tags=["well-known"]) + + +@wellknown.get("/logo.png", include_in_schema=False) +async def logo(): + return FileResponse(".well-known/logo.png", media_type="image/png") + + +@wellknown.get("/ai-plugin.json", include_in_schema=False) +async def manifest(request: Request): + host_header = request.headers.get("X-Forwarded-Host") or request.headers.get("Host") + protocol = request.headers.get("X-Forwarded-Proto") or request.url.scheme + + with open(".well-known/ai-plugin.json", encoding="utf-8") as file: + return Response( + content=Template(file.read()).substitute( + host=f"{protocol}://{host_header}" + ), + media_type="application/json", + ) diff --git a/scripts/load_env.sh b/scripts/load_env.sh new file mode 100644 index 0000000..aaa5745 --- /dev/null +++ b/scripts/load_env.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +echo "" +echo "Loading azd .env file from current environment" +echo "" + +while IFS='=' read -r key value; do + value=$(echo "$value" | sed 's/^"//' | sed 's/"$//') + export "$key=$value" +done <