From d25f3243740c86875ae367fbb7772bf01125f5db Mon Sep 17 00:00:00 2001 From: "Soojin (Min) Choi" Date: Fri, 7 Apr 2023 11:54:00 -0700 Subject: [PATCH 01/17] readme --- README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) 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 From 514aa8d8661d92dbb074d87fee2845c7d6673725 Mon Sep 17 00:00:00 2001 From: "Soojin (Min) Choi" Date: Mon, 10 Apr 2023 12:33:08 -0700 Subject: [PATCH 02/17] clean up --- .devcontainer/Dockerfile | 8 +- .devcontainer/devcontainer.json | 5 +- .devcontainer/noop.txt | 1 - .devcontainer/setup.sh | 35 ++++++++ .vscode/json.code-snippets | 2 +- ai-plugin.json => .well-known/ai-plugin.json | 15 ++-- .well-known/logo.png | Bin 0 -> 8085 bytes openapi.yaml => .well-known/openapi.yaml | 50 +++++------ entrypoint.sh | 15 +++- hostconfig.sh | 59 ++++++++++++ main.py | 89 ++++++++----------- requirements.txt | 4 +- 12 files changed, 183 insertions(+), 100 deletions(-) delete mode 100644 .devcontainer/noop.txt create mode 100644 .devcontainer/setup.sh rename ai-plugin.json => .well-known/ai-plugin.json (57%) create mode 100644 .well-known/logo.png rename openapi.yaml => .well-known/openapi.yaml (60%) create mode 100644 hostconfig.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index cef876c..fdc3d00 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -12,10 +12,6 @@ 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/ +# copy "setup.sh" so the COPY instruction does not fail if no requirements.txt exists. +COPY ../requirements.txt setup.sh /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 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 100644 index 0000000..e624149 --- /dev/null +++ b/.devcontainer/setup.sh @@ -0,0 +1,35 @@ +#!/bin/sh +# set -eu + +# 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 "Click on GitHub Codespaces PORTS tab. Right click on port 8000, and set Port Visibility to Public. Once Port 8000 if Public, press Enter to continue..." +read -r placeholder_var + +echo "Once your app is running, use the following URL to use this plugin in the OpenAI Plugin store:" +echo $PLUGIN_HOSTNAME +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 0000000000000000000000000000000000000000..0f237a226583e08f89f14a15d86aa330a11151ae GIT binary patch literal 8085 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 literal 0 HcmV?d00001 diff --git a/openapi.yaml b/.well-known/openapi.yaml similarity index 60% rename from openapi.yaml rename to .well-known/openapi.yaml index 77b753f..d043e2c 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,39 @@ paths: /todos: post: summary: Create a new TODO item + description: Accepts a string and adds as new TODO item + operationId: create_todo requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/TodoItem' + $ref: "#/components/schemas/TodoItem" 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 +49,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 +69,14 @@ 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: - type: string - required: - - title - - description \ No newline at end of file + type: string \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh index 9813a71..cd2b149 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,5 +1,16 @@ #!/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' ./ai-plugin.json ./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}}" + +# EDIT ME AND DOCKERFILE & all the new ones from main to use redis \ No newline at end of file diff --git a/hostconfig.sh b/hostconfig.sh new file mode 100644 index 0000000..40542cb --- /dev/null +++ b/hostconfig.sh @@ -0,0 +1,59 @@ +#!/bin/sh +set -eu + +# 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 + +# 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/main.py b/main.py index d8fe94c..e5ca87f 100644 --- a/main.py +++ b/main.py @@ -1,65 +1,46 @@ -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 +redis_client = redis.StrictRedis(host='0.0.0.0', port=6379, db=0, decode_responses=True) 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) - -@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.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(): + if key != 'todo_id': + todos[key] = "["+key+"] "+str(redis_client.get(key)) + 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 index a9ac093..8d8432f 100644 --- 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 From 83fd4f93f3165d1498e0c075fd39062f674045d4 Mon Sep 17 00:00:00 2001 From: "Soojin (Min) Choi" Date: Mon, 10 Apr 2023 15:35:50 -0700 Subject: [PATCH 03/17] req install --- .devcontainer/Dockerfile | 7 +------ .devcontainer/setup.sh | 4 ++++ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index fdc3d00..327eb28 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -9,9 +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 "setup.sh" so the COPY instruction does not fail if no requirements.txt exists. -COPY ../requirements.txt setup.sh /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 +RUN python3 -m pip install --upgrade pip \ No newline at end of file diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh index e624149..166b53d 100644 --- a/.devcontainer/setup.sh +++ b/.devcontainer/setup.sh @@ -1,6 +1,10 @@ #!/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 From c4debe32ccd53a6585a86abb1a9217eee254bb93 Mon Sep 17 00:00:00 2001 From: "Soojin (Min) Choi" Date: Mon, 10 Apr 2023 15:39:00 -0700 Subject: [PATCH 04/17] new logo --- .well-known/logo.png | Bin 8085 -> 8690 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.well-known/logo.png b/.well-known/logo.png index 0f237a226583e08f89f14a15d86aa330a11151ae..e6e4811f29f7004ccfb1e3acff2d4614c0627976 100644 GIT binary patch literal 8690 zcmc(Ec{r5q-~K&gCTmf6Pz+LvoC`bZniv2;@F@VmJOltT z2w~;{;GR4HETRBFH4OlGyfYiFUjT^)jrGm6zyOIXylC=5Ji#+^ecrRnoJZ+A)QFtpo1wJbP_tg0pj8bG!c;eAKK&TH1Q;Y=>AkV zg&;%^15*gXso-z=KqQR_USg>P;bh{e^nVjYGKgS27{i~sDVj>hK@GrbnrJe}Mg$4P z6A0qzYx{-h8bp&xV#%PM^?iiSMo&2?0rG$v68Fo|Il*9paQcQ=@}^+Q8WOWFc)u@D z2ABob<LJYf?Y3nY@jT;csr!CC-~MvpnTQVx{wQza8cQ;EW< z2aWF|NuWZooeiPHHPQ4fx|pO5dZ)?sEP7-ri4M|r5`|+|K@B`l=YQ`&%7)BqnlPwj zUnhM7QwgHr)}_)rqW24)S2D;CQh^7te4zhe2}}Va|7E719bE>U1kBw}2jhhfwmOw4 zle|bzPuc=c6U?P+OCbuSlju_S_W}G3w$C2}?h;)sT_IQ^jb5EDAa(U%bLgs)z!LN- zbOB%?u*Wp|fzpxm6>uPo1$S>9*d0Ht|gM~moI0hU{n7JmUXdI} z605^2`$IPihlw{#V@whri(A_r*&HdqV-fc}cG4&)=Tos+Wy|%SR&3ouUFfiEzJiO* z_xzcIae*Z#pJWQ7N_#c^?)N-+h=^=w?_*MWlr|tg>gM!CWhUxdK93r?85buE1X;K&F14 zilYYXEskIA&wc0Q>`Vb$EVXqZ&1TO3!Nn;$?qtt&tLucCiO~XNM*U5-NJQg&v}ih0WwqT4#WM!34$em^*W{Aba-ut zT1$PT8++5fV2(Q|WJm22JHPNn=g8-|H%HBzTPi@JvI_Z`C+B{xqNaqd#@dJ|hFrnc zkTiCJ)-vM%Y>5q=8|PyPi6={^DROCQtDM~Mnd-|eBz>o#@4qny=+QJ1Ep{Fl)C&m z+L487(*#$;7c|R$_Y^7r8@Nd3)~C*TSZqQ|RwX>se{OmGhq8?z4YDD(KrsDj$-g^_ zR{q&LadE3XEQg9HVP&~5EUcgo^~HsKP=2{%RY0(sJ=gt1&;RdsBGa61FP5*T|3;FJ_csml*`Zf* zbNpYG7Wh*YtyC1h&qFQqG$^lX)O-A`z;MZF7Au1yhlbzpGG&MVqE zw8{A`d?DiFsc!>`_fy=)b`I52_puRsVW z%aX=0^7>l0?rBnIKh$zc0|M>*)qQoI=8*BW)SrmGh_M2wN|bC{pV~HaKo_et#yxFg z_hlX_+cr2!U5Q)x$fNPv^W`H;rSXGHhrbdK**6kyBxZn?qX|QC$fAI|Aj~PifmsoG=Brts@awm<3E{hcPYeI@5T0l z%#Et-3Krwb6l!*F`>lI7=fsw)!h+X-z{g!mpvZSWU%RZ^*u++M$&snIy1rVPWB#^Q z9sOu*C$0YMtK!nGpuX=ZX{4$&`&w+x4(>_phO44#JuS=4P!q0eK(*b}(X*4i__<_w zcypqB5eH4P_^{nQT?E+F@`z}5G|w)bGY7;<0*B6r32d)x3JzJ^n7~c>!=Nv8@90*h`}t@C2|B>aE&dVGwQI6e6?603KRJrhVL+ zI5K>=81v(u5$zQqWzp&FzP(=0@WB9XeLLE1bj-`)$&DAiiRPz z+-L?lc1t+D2>R&3SW$U1OPniE-&hQS-^P@T;O`jkgc*>-b$Qx(78Lzpqu;MPQ0$Cc zBV4mtUNO{qzQ}=!$E$1V@o@_c@55kzj?JG)lf=d3PT+fce)YooZ>Ffn*dybCmNx>~=^4 zS{Q#+fT=Di77LR^y3VG{1rg8??I?A_YXtS${~a6C@!u{L=-C}D1!G) ziXnaZM>k^IWODB4pjF$nA!FXu6gao{LUo{93eOb8W*375-5}rE!js0Sp|;wDVt~6v z;~9bx3rpK_)44nR{2xREwv5mGi>8gF$)vLwL-yKI3C)irAS^T(ddp6MI|sTQb!Kkp zQsbW&NH*-s`*Yd3c5q)bBB@pve$P<>MKK`sYQKsHf~Hzi6z)QcJs?abq=9Q``)k$+ z8oVYiC@bY;oBn`c2O%g(6v8&hU2SxXVuJy4xHOL{rqFW9^rf%o6I8E`Ap2EN9w z*V_zTcCzgpor##eG-lu|_{;&W*E@u~k0|nM#A=RxJ$O{FC!Dnaz-;sl~L}Nb~+`1|dbT-lG=;w5rhXB^&E2vK$pvj9nzl}z%okH8Wy@=L|B#7}Edt-H zINMN3TpYesCpk#ir{4=PdYCT0LwWg_Gy;0|*wpz)t&&nXzxEQz(T@wMGp&+t;KZ(e zQAo>9m|goSgX~;$^l&}q0`JMgHJM)1oKtq45!VNhT{|r5keE2ey!`MK-x<|h-akLl z;m<8DI4g@5gJM#qatV_^B2UeUZGuc5l&^8HLG|Bd~{Nfmg#9y|_{MA%h< z599V%n|)c<^th2wlU^a%1FyeeR7Q?^GcYEB;4itooA&9w@r*Dx61dhI1ym+_=PE;U zV~YW(@!c?P4yy>(i$@-Yy@E0!JA*WY{pIBR_&FL(;T&MH6VgLg&QF$u>~0Bpq5SUN zyRzj#((G)_eT!!M>B3swq*{X$%DB4Y=T6RHAr9D%NGdNACHWO#)i*xkaUsr=-AelQ zQ}l}+t2^2Tojb9Z;VtjnI5l4w{MG2UowTvnOI!W&It^Go&CuJvvKm9)pKl=B>_ zmklwmUz+l{H$PQ)Ovcm^|D0q}JC%1e)CkP)9E<133^|J%9MG9fZ z^uh3BMIVTftF(W?XAzU+22h8Fpf4ZB6_YA<#IAhr%!;Tad4qu8+hpZQ~lCrjw zR%GhqoE2H*I0@TfRFLgZ;@8=@aV&VMm4zdKqC<0UZsr4b?b*kkQe${@BQ>vqt1hBM zJ^J&ZjIoh$7LNNA+#TIFeIK1R5IIL_WT8^v@R3FIM&i_~#p6Hta<88My1S&_?5qdO z_=YawXy4QHnK2>dM)=9cY|NNY`XHT+*_e1XWZ5IIdYD`kMq91Dt%FG@+E{FHSR_p* zcQf@{lg$K)$8(0zN7Ij@B>XO=a@9a?RY_0bH8i0Z<=ao~7>MLEB8t$KL2sM=@kvD} zZ&>ok&Uqvbjp=4Wy@;?0Xa~wPt~kTwCmtgxaydAfaQe}lQH$}v+%dK^`Shd0vmKUi z4hK_CbUp)ojDD46lnpgM?q-Tp<1qbstBGm=YO28x!9po6*D$Z^Q-hkQhi~g*wiziz za#UvmAjQV?o;q@@WfYhoE0UP65Xrkl-OY=TCa(BndVjPGfUxf-^Rrb_z@{$2bn~2_ z#g%KPZ2%bt3w+;qiT-gs4j->B92VX40{G+5nk{W96j~<|d;1G#zur-)N`7e0VN@7p zuDrQ&X!4I4e-o|z4^>Q915WAVH1Vm zMwbOUtBP``MNJ%bVA1DWTK90AkXez~hbzqU1<^?s+7Jge@3XYkd?VE14c+-{jO`QM z$G^B*Trr8e8ir)pgC^>P^!Fog4HJt%3QM>=@6;?6o2VYX8}}a#I3NcSvqEc_=VaO< z%UnG6ULq$I3{HmwY~KE~$xB$ICa&nGix6S*)Zq(*&?MfewH@|N_3(CEwAOQZ6XH4j z(+|x~Hv(^8S7zaoE017l(<_{=o<4&G+(&jY735kvZ+0uLrgxN)=SJE_B;?-@O**vu zetrkMo$YZ~6~w+4NDjVgL5vS1!$gBAp~3lAX?5*Y62c7}CY6RTV~6Vys1BTopIg}5 z4Ab(zF&RAP6^IYC8e3oNDb6^fP$rLNogmw^w2roNtNA8o5B4Y;|r7Rj~x;!C)=G*4pFwcf-t50ewhQw)YwoGlb^4=w8Po z;m&A^#7Xcs0ENF!$_`E6tMWSyCvG$FwA8mO^?bJmh{1o^v!Vhiv89vHf71J*X*_8$ zi%Xj`w?T>X?3E&{sP|Oa_uAavRsQmDFa^T;k3k(sy7I zm-avt(?EhI)YDq7=7{+wBnleU>W9oQp8g?pTsM>PoztE_=%$s{&BQ^NE*(A8M12lc zs4v?1*7FTN3HsM3foLh8GF#fIDlw#|RnimC$@H0xxwjN>L(SoA%p9~FEa+#J$Alvx ztwS1CP>T~SR_Lfu=<+V@G53GGs1bZdhG<5m;2qNd+&EP6ZBhq|%%GwjKm$-k9;s)q# zzkP+g$QiwQfK&q*Nbd^m{vxOASDI9)NWQ~cxa8TE9|R#=GOW;|TiP6iM&O^!pD)*x z>{&$h(RSuJ)0`2FW|47Ee(?yWs(H_T-%+9AU3jdwm}~Gf9qET=rL2&X|ZNZ#TF7!$;wwicpelM!@{8=jhy=(s}b6 zY-82Ck1rYeE_g4aPH(!ixmaSruQy6p8>$Q8?1$&h>`7VU1fG5LeEwsJMLzW7HCHrg z_r9uBbL^Xl%Hh(pndT5dTU2kK%%A|1u{NphmZM4rblff@)}X&2?=61S4P@Ou|431) zQ5>z{P%2>`Ud~3g)Rh3l^v6!3w94G%Y2|#hy(> z&JqlXJDYhHB9Eo1u0vCS33^3a$F!#Cm(ZnZeba3GSk7-M=QA(!0S&!9s-6DH!7_-K zJEP+z+}Up1D_0>CRYrdc3PM841fId0+vV>cg~J0?vLA_7UKLRm{7NTFLLxb9qBiOx?J6ko+bwdpILe-m09bgNrP-*3;q6THd#hO$m zEPtzLqi-cBSO6lYFq7}*)%>`ujB|JSj)F%;gdWE8sz1zvDRt|`$s%R?*Bc*27LG@Q z?s2P?^^oYXkE=O-+8){-|mxdfju7+7iG9EY@$;;#iQ;15>h+wr04lAO;Xc z)_xB3$N@@&?BSLiC|-^dJBCDh;1y$v=|`)DE5>KR7dU0EJ?k&pvb(uuhPphL61tf% z4SarF0VBYkoiYS26tC%o^!p*2R9{t5!J; zYXSPqIf(kT)FIZOV+(mkIR*zl=LVe#OFUKF>~t+ZRXzNV1GhQJB7gBcLRgo2DEmV2 zzix6JGk$#cnSues?!TBh=1X!dvw&i@#BCYSft-UsZQw3 z${W2ow(@hq|9X}UhU9yvYDcY6XrO&+S*P{40}jAH9VYW)yUmwV)Rd9`MBG48#gN-gj^rJw8{+rH%CM-JwT zX*{v*PR#U6P|)uRlbn{Q_)&~^Qe82g@B5-Q;PzQgr}ZH^`mmABnL!`MOJ~@wW7`IT zK1<3DxV#UkP zYS@Fw7EK}}U2Bb%qB5w(fq7x#wm@$%cT6Mpy;x-X(TtnN+QHdwJaIvWXOyYM z0%zJmc~0Dk>h)Sy*RS}BC;Al46)18j?VMPGO}!o(nJ$kvyfmCs6rPU1w^^SjQ{c;? z;MhLK(qfA1?P$6&3H$KrOl_HlQB#B9mhHm1hh1#Dn-%%rtS`#QUXMfPZ486C6b<~tq26*pJF67-n12yjLdQQ)9*udRTb?kKHa5a zOKhX3ZR@SE96tJkM+-NszOmjPUspR~5Z`;J`k%+d z^oKSD1=Sy@1=7gmp~XMX|I7b(;QwF;?g zFp=Y7pcjiS>)$xPI-Py_P@7uHrD$lYE7Mw-Nk3)acbfvO-qgjX5Q*^7<7?#yb8)w+ z;O`4$mxJa9D>2iPnhs@|Vi+av`&U!U3aQuY;@tl*UOYpQcj zbZ1XqOs_Y3dz0V%>GiOYai|Ag?cJbIwUR~Dd_?mjR2q4wvY;U_*0J{I-F9x?CpTca z|9uB7#gA#`V|%{Pp-Y@TPcLY^dEtL=g`+C<$jaS+_=;&G53F)>1^8}a4*-6b_sc(?&dEa!Rl zt3~!IGs`=bOp4O}N;hsW-kV0+SOo}|-RLUG@cAs86idVX0_tBL?QzX9lvcU4o&=z( zt}6epWoiNfWlgmtmmPaDREg*1^L}GmFl^|99?dX|@?Eg`n0RR$a*i1JGj7Jq3sw`2 zoV2eQ`pPv{vm307lr*ujE$j-gRh*`#EY=DLRJuw?R91L-uC0E7>pB4U?m61uzgHeJ z34hdFa~npPBPCEU=kWbk*7$nbLBo_RPX;2kZ7}{Yc*lQod+AEd9>6XMvb%69QY_Uf z%B8Hy*pv6S{7dHOR2SA(FVxj!Z)wp=Z?Erq>df9e8w(#eeOST5blCPd!f=}NWc2s1 z#Lx4e#=V3uK^viq-O2ofT%TFnqor)``4wO`>}XTjBW!$O_zF{?^=CJNI-odxXYQ$9 z?!?vA!SxyP8`kLf_PX>I9xlkE1w$pcBcD;4;$R}iRh$!)k69{eIkTO$MM~ta4CZ2z z-w)|@P3&r{wGO}E+v*H>`*Ue;6%qYi>BsFr$suqdKdaL_iF7>1^D79+b;kR18q9Ho z1~@95`L{~p2iq}KHizN)cjYN+C1H610!ooCy0bTVVYN%HEiRwoGY_T@)n=KEVF7)j zEnXf{GopSr1%@HtH<1Hz;P~&87G*LTtOt)tA7`>RT6mwlWqAT35YL0|N@X|be zeHjH1tK^sZgd{FKy~G0 z>AlJ*C-_+nN-Mj5z;A;N*W$0ehtMi|?Dk)eTloDS6G>D#C4nYKY{(Hj)~sc>Z4Y$T z3kdm`-nN=efF?f~nQ;H9K>Ef@-%KNtUP)&9CabWUX72Fo0HLy!aLDz+;6N;=~i?4uO)-bZt0K8EUNa=0@wx z+P+$k3@n8ugXoE+_wSjUir)U7y>YBy&m=+ z<)&lfKBPsT?!a%H=|3{HY_!QQ!ld+ej}EFozbyE$=TXuvr|8;y{#8{f7PoY;DU#SQ z>R~mzI5A8KXjd`V3Fo+#NJ<8!sq-60f65;j>mb%>p5vv6B}vjn4s?onN#~j9 z38)6=D8uNlkLP%pvq!@u=s*=rPrV)!kl`&KpF8j>u682pH2tgE+n^t?mQ{dJ(MXqm zO_i*>&&a^;)=1;(x%Z__ax+^mkELwe8KaBGts1nXpmFS;Xy&(s)e7wV=xxplEp4cL-rPRPm-$V+#vy6xgiOdQDyqIBX8#c027 zBs*H9!4h$K1D%m zm2b_!Q3CC}o1c200^dDi_*=aEG#_sCTmKX73xa;~&9E+fe(jkp@!8ojqjBU9nyw?8 zc1*h{n_^-g>e^#&*FNC$<6G_AB#gp9oKqpL>o-c*hUOSh6*P*ai(|S4bLVL1BFzh$ z5A8)q{{F>jtu;R?@)Jn$1(VTTupDvBkEiS!8I z|7$F@s6g9QFE5*tP)H7Afo#>F;)+k8kQ>M=#q7i@59Q_mux30)Qv42G280{eP>MNJ zH6%5W&Gm_EQ|S!43I&p$B4~ATHFqeLRYFoNIYtp7k`k4Z9gH9@e&Lsm56h;C_^K{M z`ffacyx5dLzQ$%tQf>CzN2CL22{#e9y6}Rm6HAdmWd%88PK^mq|0-?X6&b^0+z5mY ze;tKtTJR$BCva)_7geQ~PM5pfsbNIyN?}4Q{<2nxKpr0zFZft zrsra-j*wk3uQaGRUu2WP724NC4sTP~EDtBHD}YVU5CsCye}2t_>hB4aJSPW(M$pSo zM0seWXShqzkfT*rfMxtme9^X{M})7w-K->u!4y6t`NKSV!72{mm}B2G6jf{geIvhe z!YU{w?KGSyo^DB~_#l_2hj{ok9BqW*+b$oy*hdpTy1W2o$;B%2J3;VO(RUzd=lE`Z zK;aphNaX>{I!1l&Wlumr*G! zA1vGVJh0_sRZI^7lj}XQ-)l*8bV)ukivV^19X-f$*$1R*h=(a=JN875?jy2pW5(15TaiRaH zB`^s`!2$rcY|v`t3%TBprdejuT=aTV)Lz~m$1Z$4`0fUww0Mm+MqaDoN{e5mH%HLB z2h#Z%0jde_jNotGP{I`#lW*{tWq_CdlxgyK8a-VvN|JRfR*0FWRO6mf7z8#z-ePld zvl|`Uej<-DkDaS2WNLJk&aU|@o}!p;8&_+88&#Sj@)z_OgI8gT&Xsx(L^M3|MYX;M z$2~qdu;yy#@iAXQtplFsDYmg2*q!NQ3nWFhY!~jqT>bWdbQET`OsX-IeSNfTZfpt@C^=NSiAvl zzik{$`f=+b&ef^p^n#8)0B`E!Ch{hpYM`ln z&}+0VBbkbMaG3TTmo#_D-S?^;0!;ej1#nj{vB4U!P@;pvD`T@>7rc_V&ep;NRIjU< z1oKmnZa0j6R55;IphbsDW!{I{1j0WwXNDIEf3VA#Uno&*mAP97t>^cs2p^vrg&KGGSA24S&NuJTdDNdO zAGi|h!C}0%fN@}uHugx=NNlOZb$JIk;CH3oh6}qdr_j0)TCeBAc^Xfb;oBzu7t_ssh-1*PbYdCvy z9ElLx1I)@!HArQ^>vvB6Dzrv0`CG@@+dAGr8(^VqHTQf=i!*;xSHvpsfF8etqb;Mx+mDI%y1O^Rj4|uK zQIzGKgN!noN%Uh5_?~@ho8My}m6Y+mpPont`sN*|2Jou!Nix&%1de=cFb9n+2iDup zK7w-*LmRa=kVFXhB@+Q&1rA!&Kr74h=gfL>b1&~$YFsnNy1s4=Pqdb`V_yeo76&+* zT2#YQ+2VaV3h0)2PJgCqw6{^W{+PAQBg)Nm8;%@bmSy4D4rK^k>A;FA4SZ3ipt23iPV@%$R}xUGh2RkQUYc{Z?Y1`{!Ql>$M!!Ew43GMpBc9le5DZw3TN504n3%z($w6@x08K z7VnGj_W2y9#a+a1bC@>6-jT3T`f_}9WcjMt4ap1DqMz3z_9g9U+Li0W_qdP>SV8m- z>PNH>HlI?B(?=jRNt_gc>wI*S{E!437isl;T^ch0q3|;3vqfMsM)zn~HNX`2|0YTr z9A?MW#x8lgn7fIn@%U6MCR|}vQ5lUi6rDSFcn|=&=!*5g!+wP9e5O#88@{u0l-rpv zg{4e?!<|B}Zn60X*IY^UFdt4qDlJYjqwH6GPr3{E7fV-SjfftPRFgsfr|;vyEUeH& zMY13h3vQI>sO0tpze!Yed`&2}+! zkhgU|v|=MR@u3#dh6%k*A6>kHUk+Rzxc*=!ujtDc>%5nh?9^!q8vNGLzpqV`p76`R zg-Ve=g~;)9(ptFgff&?8BoC;WKK6qWW~idYi!BIAO1(GgXWX|oFWRD14R%UD-2f)o zQ{cNX#4^%Bv&mMl5ojFkd99suv#OMvzZA<+CVZ>)Iw2|U4QW1b^SIoi%mp*|7U~Rl zcT6e$WNrPKU7@g4oA1+8js=oC8?Wet}kF=qevlbY{?VnC-#WknUEa&`3;Ary0U zZw2OupnSF*6T5L&Kx91+S8MO1Yj7o0){Bf?%?=5@PwIC=T*q>rAwLg=}2yKhnk9jrdFs zn2EezAv;1e72_@(v<#Nf>0q>6t4)>gXK8f5?IF=Ss|jY}qwsLvt0?u7G?>a7GS!5z zwj1W9g5=MPwNe2Pr!6~sXm?&~M%EjX(J6UHF@_uD$;5+HVy(-3$33e`J?UY|t z7h*=7EYGh?dFoC1zW1(O=cs{F^9m8(N1Z|^o>N6!E+n0Ny~_LpXIu~4L~_b3PloM? zDkjnpz0K)q_TTqQO@Fb3-2dT# z9K~a$Eo{{FWGWYuG`P**P_ZBFa9Z&=pGM)jqsj8YGLD?+cmvEl>~le6o&U``Hs$Tl zl-DQ9ztM$^%hfdjnXZzi6mLM(BQX*?NIgsQ40wI%AWeoTwK&=9jC&p#FZ*%ue(>HFFf(UL8+>pLfP%Vs2aFlXk>Qq~0>%28N~`xr7ze z;vWZjcC-HDER)=(hV{SOGo=Wte4rN|FOo=;iLcg~dDHT;o?7--6xEjnSn-&}mz;-2 zX>z4RS0k&IMOmZHSg)P(s=`MjIg>|xHhzLsQ{j5i18ej5{z{=FAG_XLe^xV+EeTgG z`=AT~9)h%Fev8`^o8?)PnpBEQ)!N;89l5m%s9?$qk2;4?D<773|JET7X7X-Hs*`{2 z?vXkcqHfBK+P`0HoY9v>ZMeWt4{cH;VBYz(mtjFPBE(J0)SacSaXyDv?qq~_1 z+PErdYAT=4trr50kr-b+|As4ip`gtl;QgZ5VDE!|0Rg4dEe}7VS*d`A$-hEw(ua-J zP(HK^N@`4a=Nehs|4N3k*}vp&C4*Xx7q|A}w{~cY|Mk9}`>{t&PfGt#ZSF-~$i_qc z*`kGp5uU*I)i8}6c!vSgcHJ{Y-R|U4DZvsM zgZOT?JF8I79_f&Yh5szQ6TFY+J1JiPZRmeY?uDz*;Ey<3s-P0(xl6(sinYT2@@=xS z9N(WkF1&mcxbA$#H)l6{T^92Wv|=pMr+w2ONf<>>kqtN=n$1e)GZ2$81DSzuik?+s zW^@=M-UTF#4{Elo#bJ*h5Z<|=JS~*-^PR7U6>UB@M<_oGWCDQ0;R}xMDi4ZDjgsT< z59ai>lg1otPeD-LGgFD7!X9lI@hAHOTTW6oq{x%aiKo_f`R9fO?e9ttt{lDfnP2!W z$=o24@gdhJwo2z(IWM4EuW%f4!3t;nwO&J7CoGe4Do@N ze0?Ll3dwo@{iYp#v(Ab@iu1V*Nd0WW%y%7Ycgm%TI%dE*^Zlzb;Uhg0A6&3A; z3$@v&ySaopv0}FWB8WeaG8=5Zylt^Q`DAnG#Ewv+qf_R=MT~yt>(o&FW*9<9bP;$PZo3SFV)wKG!23>VdH*S2$tpd9+h8 vm{h2v9Dij;CQqNNYK7N)pA@v57rGMY^ijcdQjk#p`&BhFvNEhSco6eHk%;zp From 5fe41f06a883f6dc203b681c088d3c7b85ab90d9 Mon Sep 17 00:00:00 2001 From: "Soojin (Min) Choi" Date: Mon, 10 Apr 2023 15:43:14 -0700 Subject: [PATCH 05/17] entrypoint path --- entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entrypoint.sh b/entrypoint.sh index cd2b149..927eff0 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,6 +1,6 @@ #!/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 ./main.py +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}}" From 812dc0ef91dc6ad4cd745718ea93dcaa618c4143 Mon Sep 17 00:00:00 2001 From: "Soojin (Min) Choi" Date: Mon, 10 Apr 2023 15:55:32 -0700 Subject: [PATCH 06/17] bicep comments --- infra/main.bicep | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/infra/main.bicep b/infra/main.bicep index f409f64..2c57a95 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -55,7 +55,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 +77,7 @@ module api './app/api.bicep' = { } } +// Container app for Redis datastore module redis './app/redis.bicep' = { name: 'redis' scope: rg From 1c19c5660fb5ea5f24dceef6f1cea74f23b9eb65 Mon Sep 17 00:00:00 2001 From: "Soojin (Min) Choi" Date: Mon, 10 Apr 2023 15:56:33 -0700 Subject: [PATCH 07/17] not codespaces url --- .devcontainer/setup.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh index 166b53d..33d51f6 100644 --- a/.devcontainer/setup.sh +++ b/.devcontainer/setup.sh @@ -33,7 +33,5 @@ echo echo "Click on GitHub Codespaces PORTS tab. Right click on port 8000, and set Port Visibility to Public. Once Port 8000 if Public, press Enter to continue..." read -r placeholder_var -echo "Once your app is running, use the following URL to use this plugin in the OpenAI Plugin store:" -echo $PLUGIN_HOSTNAME echo echo "Enter 'footoken' if OpenAI prompts you for a Bearer Token" From ad1d717594039fea8cf8c874017fee331312e8ca Mon Sep 17 00:00:00 2001 From: "Soojin (Min) Choi" Date: Mon, 10 Apr 2023 16:03:06 -0700 Subject: [PATCH 08/17] spaces for readability --- infra/main.bicep | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infra/main.bicep b/infra/main.bicep index 2c57a95..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 From 325c15f304d0112e07dd3279a12f95ada671f64d Mon Sep 17 00:00:00 2001 From: "Soojin (Min) Choi" Date: Mon, 10 Apr 2023 16:07:38 -0700 Subject: [PATCH 09/17] fix openapi manifest --- .well-known/openapi.yaml | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/.well-known/openapi.yaml b/.well-known/openapi.yaml index d043e2c..3494062 100644 --- a/.well-known/openapi.yaml +++ b/.well-known/openapi.yaml @@ -11,12 +11,13 @@ paths: summary: Create a new TODO item description: Accepts a string and adds as new TODO item operationId: create_todo - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/TodoItem" + parameters: + - in: query + name: todo + schema: + type: string + required: true + description: The description of the TODO item responses: "200": description: OK @@ -78,5 +79,11 @@ components: TodoItem: type: object properties: - title: - type: string \ No newline at end of file + todo: + type: string + todo_id: + type: integer + format: int32 + readOnly: true + required: + - todo \ No newline at end of file From 33dc7b1bd02c5d4e309645eb7248e5202193a833 Mon Sep 17 00:00:00 2001 From: "Soojin (Min) Choi" Date: Mon, 10 Apr 2023 23:55:20 +0000 Subject: [PATCH 10/17] use azure redis --- main.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index e5ca87f..c8e2ecb 100644 --- a/main.py +++ b/main.py @@ -3,7 +3,18 @@ import os import redis -redis_client = redis.StrictRedis(host='0.0.0.0', port=6379, db=0, decode_responses=True) +redis_host = os.environ.get("REDIS_HOST") +redis_port = os.environ.get("REDIS_PORT") +redis_password = os.environ.get("REDIS_PASSWORD") + +redis_client = redis.Redis( + host=redis_host, + port=redis_port, + password=redis_password, + ssl=True, + ssl_cert_reqs=None, + decode_responses=True, +) app = FastAPI() app.mount("/.well-known", StaticFiles(directory=".well-known"), name="static") From f74c73815561a7bd5f05dbb4cb5ace7b59cf4e63 Mon Sep 17 00:00:00 2001 From: "Soojin (Min) Choi" Date: Mon, 10 Apr 2023 20:14:54 -0700 Subject: [PATCH 11/17] connect to ca redis --- main.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/main.py b/main.py index c8e2ecb..83ad371 100644 --- a/main.py +++ b/main.py @@ -3,18 +3,13 @@ import os import redis -redis_host = os.environ.get("REDIS_HOST") -redis_port = os.environ.get("REDIS_PORT") -redis_password = os.environ.get("REDIS_PASSWORD") - -redis_client = redis.Redis( - host=redis_host, - port=redis_port, - password=redis_password, - ssl=True, - ssl_cert_reqs=None, - decode_responses=True, -) +# 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') + +# Create a Redis connection +redis_client = redis.Redis(host=redis_host, port=redis_port, password=redis_password) app = FastAPI() app.mount("/.well-known", StaticFiles(directory=".well-known"), name="static") From ccd1c6915c830e6e66420b64c3a62f88416209c0 Mon Sep 17 00:00:00 2001 From: "Soojin (Min) Choi" Date: Mon, 10 Apr 2023 22:32:16 -0700 Subject: [PATCH 12/17] clean up infra --- infra/core/cache/redis-enterprise.bicep | 56 ----- infra/core/cache/redis.bicep | 54 ----- .../core/database/cosmos/cosmos-account.bicep | 48 ---- .../cosmos/mongo/cosmos-mongo-account.bicep | 22 -- .../cosmos/mongo/cosmos-mongo-db.bicep | 46 ---- .../cosmos/sql/cosmos-sql-account.bicep | 21 -- .../database/cosmos/sql/cosmos-sql-db.bicep | 73 ------ .../cosmos/sql/cosmos-sql-role-assign.bicep | 18 -- .../cosmos/sql/cosmos-sql-role-def.bicep | 29 --- .../database/postgresql/flexibleserver.bicep | 64 ------ infra/core/database/sqlserver/sqlserver.bicep | 129 ----------- infra/core/gateway/apim.bicep | 78 ------- infra/core/host/aks-agent-pool.bicep | 17 -- infra/core/host/aks-managed-cluster.bicep | 139 ------------ infra/core/host/aks.bicep | 213 ------------------ infra/core/host/appservice.bicep | 101 --------- infra/core/host/appserviceplan.bicep | 20 -- infra/core/host/functions.bicep | 82 ------- infra/core/host/staticwebapp.bicep | 21 -- infra/core/security/keyvault-access.bicep | 21 -- infra/core/security/keyvault-secret.bicep | 30 --- infra/core/security/keyvault.bicep | 25 -- infra/core/security/registry-access.bicep | 18 -- infra/core/storage/storage-account.bicep | 38 ---- 24 files changed, 1363 deletions(-) delete mode 100644 infra/core/cache/redis-enterprise.bicep delete mode 100644 infra/core/cache/redis.bicep delete mode 100644 infra/core/database/cosmos/cosmos-account.bicep delete mode 100644 infra/core/database/cosmos/mongo/cosmos-mongo-account.bicep delete mode 100644 infra/core/database/cosmos/mongo/cosmos-mongo-db.bicep delete mode 100644 infra/core/database/cosmos/sql/cosmos-sql-account.bicep delete mode 100644 infra/core/database/cosmos/sql/cosmos-sql-db.bicep delete mode 100644 infra/core/database/cosmos/sql/cosmos-sql-role-assign.bicep delete mode 100644 infra/core/database/cosmos/sql/cosmos-sql-role-def.bicep delete mode 100644 infra/core/database/postgresql/flexibleserver.bicep delete mode 100644 infra/core/database/sqlserver/sqlserver.bicep delete mode 100644 infra/core/gateway/apim.bicep delete mode 100644 infra/core/host/aks-agent-pool.bicep delete mode 100644 infra/core/host/aks-managed-cluster.bicep delete mode 100644 infra/core/host/aks.bicep delete mode 100644 infra/core/host/appservice.bicep delete mode 100644 infra/core/host/appserviceplan.bicep delete mode 100644 infra/core/host/functions.bicep delete mode 100644 infra/core/host/staticwebapp.bicep delete mode 100644 infra/core/security/keyvault-access.bicep delete mode 100644 infra/core/security/keyvault-secret.bicep delete mode 100644 infra/core/security/keyvault.bicep delete mode 100644 infra/core/security/registry-access.bicep delete mode 100644 infra/core/storage/storage-account.bicep 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 From bff09fbd76075cf3bfdc51f60d0b5a1d35dff226 Mon Sep 17 00:00:00 2001 From: "Soojin (Min) Choi" Date: Tue, 11 Apr 2023 08:28:40 -0700 Subject: [PATCH 13/17] hostconfig for Azure --- hostconfig.sh | 44 +++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/hostconfig.sh b/hostconfig.sh index 40542cb..e86ed56 100644 --- a/hostconfig.sh +++ b/hostconfig.sh @@ -1,24 +1,34 @@ #!/bin/sh set -eu -# 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 +# # 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 - # 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 + echo "PLUGIN_HOSTNAME environment variable is not set." + exit 1 fi # Input JSON file From 52aeb077c8abbdcdf5da4a78eca6eb8c591cde50 Mon Sep 17 00:00:00 2001 From: "Soojin (Min) Choi" Date: Tue, 11 Apr 2023 09:00:53 -0700 Subject: [PATCH 14/17] stringify key --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 83ad371..71b3c77 100644 --- a/main.py +++ b/main.py @@ -20,7 +20,7 @@ def list_todos(): todos = {} for key in redis_client.keys(): if key != 'todo_id': - todos[key] = "["+key+"] "+str(redis_client.get(key)) + todos[key] = "[" + str(key) + "] "+str(redis_client.get(key)) return todos # Route to list a specific TODO From 0c34e6a2f2b66bd8afaeb1456eb0a56fcac3e090 Mon Sep 17 00:00:00 2001 From: "Soojin (Min) Choi" Date: Tue, 11 Apr 2023 09:27:21 -0700 Subject: [PATCH 15/17] setup bash --- .devcontainer/setup.sh | 4 ---- entrypoint.sh | 2 -- 2 files changed, 6 deletions(-) diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh index 33d51f6..53b06d3 100644 --- a/.devcontainer/setup.sh +++ b/.devcontainer/setup.sh @@ -29,9 +29,5 @@ echo echo "Setting host configuration (from ./hostconfig.sh)..." chmod +x ./hostconfig.sh && ./hostconfig.sh -echo -echo "Click on GitHub Codespaces PORTS tab. Right click on port 8000, and set Port Visibility to Public. Once Port 8000 if Public, press Enter to continue..." -read -r placeholder_var - echo echo "Enter 'footoken' if OpenAI prompts you for a Bearer Token" diff --git a/entrypoint.sh b/entrypoint.sh index 927eff0..91d2d28 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -12,5 +12,3 @@ set -eu # 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}}" - -# EDIT ME AND DOCKERFILE & all the new ones from main to use redis \ No newline at end of file From f4bab294eb143d3ba7ba3b83e864d7c1680742fa Mon Sep 17 00:00:00 2001 From: "Soojin (Min) Choi" Date: Tue, 11 Apr 2023 18:05:50 +0000 Subject: [PATCH 16/17] fix todo list key --- main.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 71b3c77..6b69e5b 100644 --- a/main.py +++ b/main.py @@ -19,8 +19,11 @@ def list_todos(): todos = {} for key in redis_client.keys(): - if key != 'todo_id': - todos[key] = "[" + str(key) + "] "+str(redis_client.get(key)) + 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 From af373d477b14ef6beda8f94186175d3ce0432f9f Mon Sep 17 00:00:00 2001 From: "Soojin (Min) Choi" Date: Tue, 11 Apr 2023 18:17:59 +0000 Subject: [PATCH 17/17] update with Azure config --- .devcontainer/setup.sh | 0 hostconfig.sh | 0 requirements.txt | 0 3 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .devcontainer/setup.sh mode change 100644 => 100755 hostconfig.sh mode change 100644 => 100755 requirements.txt diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh old mode 100644 new mode 100755 diff --git a/hostconfig.sh b/hostconfig.sh old mode 100644 new mode 100755 diff --git a/requirements.txt b/requirements.txt old mode 100644 new mode 100755