diff --git a/docs/tutorial/code/fastapi/app.py b/docs/tutorial/code/fastapi/app.py new file mode 100644 index 000000000..8e3dfea72 --- /dev/null +++ b/docs/tutorial/code/fastapi/app.py @@ -0,0 +1,7 @@ +from fastapi import FastAPI + +app = FastAPI() + +@app.get("/") +async def root(): + return {"message": "Hello World"} \ No newline at end of file diff --git a/docs/tutorial/code/fastapi/greeting_app.py b/docs/tutorial/code/fastapi/greeting_app.py new file mode 100644 index 000000000..9183838d1 --- /dev/null +++ b/docs/tutorial/code/fastapi/greeting_app.py @@ -0,0 +1,9 @@ +import os + +from fastapi import FastAPI + +app = FastAPI() + +@app.get("/") +async def root(): + return {"message": os.getenv("APP_GREETING", "Hello World")} \ No newline at end of file diff --git a/docs/tutorial/code/fastapi/greeting_charmcraft.yaml b/docs/tutorial/code/fastapi/greeting_charmcraft.yaml new file mode 100644 index 000000000..e737da7e0 --- /dev/null +++ b/docs/tutorial/code/fastapi/greeting_charmcraft.yaml @@ -0,0 +1,9 @@ +# configuration snippet for FastAPI application with a configuration + +config: + options: + greeting: + description: | + The greeting to be returned by the FastAPI application. + default: "Hello, world!" + type: string diff --git a/docs/tutorial/code/fastapi/requirements.txt b/docs/tutorial/code/fastapi/requirements.txt new file mode 100644 index 000000000..472fd3835 --- /dev/null +++ b/docs/tutorial/code/fastapi/requirements.txt @@ -0,0 +1,2 @@ +fastapi[standard] +psycopg2-binary diff --git a/docs/tutorial/code/fastapi/task.yaml b/docs/tutorial/code/fastapi/task.yaml new file mode 100644 index 000000000..d00d45731 --- /dev/null +++ b/docs/tutorial/code/fastapi/task.yaml @@ -0,0 +1,206 @@ +########################################### +# IMPORTANT +# Comments matter! +# The docs use the wrapping comments as +# markers for including said instructions +# as snippets in the docs. +########################################### +summary: Getting started with FastAPI tutorial + +kill-timeout: 180m + +environment: + +execute: | + # Move everything to $HOME so that Juju deployment works + mv *.yaml *.py *.txt $HOME + cd $HOME + + # Don't use the staging store for this test + unset CHARMCRAFT_STORE_API_URL + unset CHARMCRAFT_UPLOAD_URL + unset CHARMCRAFT_REGISTRY_URL + + # MicroK8s config setup + microk8s status --wait-ready + microk8s enable hostpath-storage + microk8s enable registry + microk8s enable ingress + + # Bootstrap controller + juju bootstrap microk8s dev-controller + + # [docs:create-venv] + sudo apt-get update && sudo apt-get install python3-venv -y + python3 -m venv .venv + source .venv/bin/activate + # [docs:create-venv-end] + + # [docs:install-requirements] + pip install -r requirements.txt + # [docs:install-requirements-end] + + fastapi dev app.py --port 8080 & + retry -n 5 --wait 2 curl --fail localhost:8080 + + # [docs:curl-fastapi] + curl localhost:8080 + # [docs:curl-fastapi-end] + + kill $! + + # [docs:create-rockcraft-yaml] + rockcraft init --profile fastapi-framework + # [docs:create-rockcraft-yaml-end] + + sed -i "s/name: .*/name: fastapi-hello-world/g" rockcraft.yaml + sed -i "s/amd64/$(dpkg --print-architecture)/g" rockcraft.yaml + + # [docs:pack] + ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack + # [docs:pack-end] + + # [docs:skopeo-copy] + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:fastapi-hello-world_0.1_$(dpkg --print-architecture).rock \ + docker://localhost:32000/fastapi-hello-world:0.1 + # [docs:skopeo-copy-end] + + # [docs:create-charm-dir] + mkdir charm + cd charm + # [docs:create-charm-dir-end] + + # [docs:charm-init] + charmcraft init --profile fastapi-framework --name fastapi-hello-world + # [docs:charm-init-end] + + # update platforms in charmcraft.yaml file + sed -i "s/amd64/$(dpkg --print-architecture)/g" charmcraft.yaml + + # [docs:charm-pack] + CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack + # [docs:charm-pack-end] + + # [docs:add-juju-model] + juju add-model fastapi-hello-world + # [docs:add-juju-model-end] + + #[docs:add-model-constraints] + juju set-model-constraints -m fastapi-hello-world arch=$(dpkg --print-architecture) + #[docs:add-model-constraints-end] + + # [docs:deploy-fastapi-app] + juju deploy \ + ./fastapi-hello-world_$(dpkg --print-architecture).charm \ + fastapi-hello-world --resource \ + app-image=localhost:32000/fastapi-hello-world:0.1 + # [docs:deploy-fastapi-app-end] + + # [docs:deploy-nginx] + juju deploy nginx-ingress-integrator --channel=latest/stable --trust + juju integrate nginx-ingress-integrator fastapi-hello-world + # [docs:deploy-nginx-end] + + # [docs:config-nginx] + juju config nginx-ingress-integrator \ + service-hostname=fastapi-hello-world path-routes=/ + # [docs:config-nginx-end] + + # give Juju some time to deploy the apps + juju wait-for application fastapi-hello-world --query='status=="active"' --timeout 10m + juju wait-for application nginx-ingress-integrator --query='status=="active"' --timeout 10m + + # [docs:curl-init-deployment] + curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1 + # [docs:curl-init-deployment-end] + + cd .. + cat greeting_app.py > app.py + sed -i "s/version: .*/version: 0.2/g" rockcraft.yaml + + # [docs:docker-update] + ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:fastapi-hello-world_0.2_$(dpkg --print-architecture).rock \ + docker://localhost:32000/fastapi-hello-world:0.2 + # [docs:docker-update-end] + + cat greeting_charmcraft.yaml >> ./charm/charmcraft.yaml + cd charm + + # [docs:refresh-deployment] + CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack + juju refresh fastapi-hello-world \ + --path=./fastapi-hello-world_$(dpkg --print-architecture).charm \ + --resource app-image=localhost:32000/fastapi-hello-world:0.2 + # [docs:refresh-deployment-end] + + # give Juju some time to refresh the app + juju wait-for application fastapi-hello-world --query='status=="active"' --timeout 10m + + # curl and check that the response is Hello + curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1 | grep Hello + + # [docs:change-config] + juju config fastapi-hello-world greeting='Hi!' + # [docs:change-config-end] + + # make sure that the application updates + juju wait-for application fastapi-hello-world --query='status=="maintenance"' --timeout 10m + juju wait-for application fastapi-hello-world --query='status=="active"' --timeout 10m + + # curl and check that the response is now Hi + curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1 | grep Hi + + cd .. + cat visitors_migrate.py >> migrate.py + cat visitors_app.py > app.py + sed -i "s/version: .*/version: 0.3/g" rockcraft.yaml + + # [docs:docker-2nd-update] + ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:fastapi-hello-world_0.3_$(dpkg --print-architecture).rock \ + docker://localhost:32000/fastapi-hello-world:0.3 + # [docs:docker-2nd-update-end] + + cat visitors_charmcraft.yaml >> ./charm/charmcraft.yaml + cd charm + + # [docs:refresh-2nd-deployment] + CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack + juju refresh fastapi-hello-world \ + --path=./fastapi-hello-world_$(dpkg --print-architecture).charm \ + --resource app-image=localhost:32000/fastapi-hello-world:0.3 + # [docs:refresh-2nd-deployment-end] + + # [docs:deploy-postgres] + juju deploy postgresql-k8s --trust + juju integrate fastapi-hello-world postgresql-k8s + # [docs:deploy-postgres-end] + + # give Juju some time to deploy and refresh the apps + juju wait-for application postgresql-k8s --query='status=="active"' --timeout 20m | juju status --relations + juju wait-for application fastapi-hello-world --query='status=="active"' --timeout 20m | juju status --relations + + curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1 | grep Hi + curl http://fastapi-hello-world/visitors --resolve fastapi-hello-world:80:127.0.0.1 | grep 1 + curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1 | grep Hi + curl http://fastapi-hello-world/visitors --resolve fastapi-hello-world:80:127.0.0.1 | grep 2 + + # Back out to main directory for cleanup + cd .. + + # [docs:clean-environment] + # exit and delete the virtual environment + deactivate + rm -rf charm .venv __pycache__ + # delete all the files created during the tutorial + rm fastapi-hello-world_0.1_$(dpkg --print-architecture).rock \ + fastapi-hello-world_0.2_$(dpkg --print-architecture).rock \ + fastapi-hello-world_0.3_$(dpkg --print-architecture).rock \ + rockcraft.yaml app.py requirements.txt migrate.py + # Remove the juju model + juju destroy-model fastapi-hello-world --destroy-storage --no-prompt --force + # [docs:clean-environment-end] diff --git a/docs/tutorial/code/fastapi/visitors_app.py b/docs/tutorial/code/fastapi/visitors_app.py new file mode 100644 index 000000000..d74e25bdc --- /dev/null +++ b/docs/tutorial/code/fastapi/visitors_app.py @@ -0,0 +1,34 @@ +# FastAPI application that keeps track of visitors using a database + +import datetime +import os +from typing import Annotated + +from fastapi import FastAPI, Header +import psycopg2 + +app = FastAPI() +DATABASE_URI = os.environ["POSTGRESQL_DB_CONNECT_STRING"] + + +@app.get("/") +async def root(user_agent: Annotated[str | None, Header()] = None): + with psycopg2.connect(DATABASE_URI) as conn, conn.cursor() as cur: + timestamp = datetime.datetime.now() + + cur.execute( + "INSERT INTO visitors (timestamp, user_agent) VALUES (%s, %s)", + (timestamp, user_agent) + ) + conn.commit() + + return {"message": os.getenv("APP_GREETING", "Hello World")} + + +@app.get("/visitors") +async def visitors(): + with psycopg2.connect(DATABASE_URI) as conn, conn.cursor() as cur: + cur.execute("SELECT COUNT(*) FROM visitors") + total_visitors = cur.fetchone()[0] + + return {"count": total_visitors} diff --git a/docs/tutorial/code/fastapi/visitors_charmcraft.yaml b/docs/tutorial/code/fastapi/visitors_charmcraft.yaml new file mode 100644 index 000000000..2cc0cb84e --- /dev/null +++ b/docs/tutorial/code/fastapi/visitors_charmcraft.yaml @@ -0,0 +1,6 @@ +# requires snippet for FastAPI application with a database + +requires: + postgresql: + interface: postgresql_client + optional: false diff --git a/docs/tutorial/code/fastapi/visitors_migrate.py b/docs/tutorial/code/fastapi/visitors_migrate.py new file mode 100644 index 000000000..9101e06b1 --- /dev/null +++ b/docs/tutorial/code/fastapi/visitors_migrate.py @@ -0,0 +1,21 @@ +# Adds database to FastAPI application + +import os + +import psycopg2 + +DATABASE_URI = os.environ["POSTGRESQL_DB_CONNECT_STRING"] + +def migrate(): + with psycopg2.connect(DATABASE_URI) as conn, conn.cursor() as cur: + cur.execute(""" + CREATE TABLE IF NOT EXISTS visitors ( + timestamp TIMESTAMP NOT NULL, + user_agent TEXT NOT NULL + ); + """) + conn.commit() + + +if __name__ == "__main__": + migrate()