From 3e6850700a3a0727637286facb69f076f7e3dbfc Mon Sep 17 00:00:00 2001 From: Erin Conley Date: Wed, 11 Dec 2024 16:15:39 -0500 Subject: [PATCH] docs: port Flask tutorial from Juju.is (#1976) The current Flask tutorial: https://juju.is/docs/sdk/write-your-first-kubernetes-charm-for-a-flask-app This PR will move the tutorial off of Discourse and Charm SDK and onto the Charmcraft RTD. --------- Co-authored-by: Michael DuBelko Co-authored-by: Alex Lowe --- docs/common/craft-parts | 1 - docs/reuse/tutorial/setup_stable.rst | 70 +++ docs/tutorial/code/flask/app.py | 14 + docs/tutorial/code/flask/greeting_app.py | 16 + .../code/flask/greeting_charmcraft.yaml | 9 + docs/tutorial/code/flask/requirements.txt | 1 + docs/tutorial/code/flask/task.yaml | 220 ++++++++ docs/tutorial/code/flask/visitors_app.py | 42 ++ .../code/flask/visitors_charmcraft.yaml | 6 + docs/tutorial/code/flask/visitors_migrate.py | 23 + docs/tutorial/flask.rst | 523 ++++++++++++++++++ docs/tutorial/index.rst | 10 + spread.yaml | 7 +- 13 files changed, 940 insertions(+), 2 deletions(-) delete mode 120000 docs/common/craft-parts create mode 100644 docs/reuse/tutorial/setup_stable.rst create mode 100644 docs/tutorial/code/flask/app.py create mode 100644 docs/tutorial/code/flask/greeting_app.py create mode 100644 docs/tutorial/code/flask/greeting_charmcraft.yaml create mode 100644 docs/tutorial/code/flask/requirements.txt create mode 100644 docs/tutorial/code/flask/task.yaml create mode 100644 docs/tutorial/code/flask/visitors_app.py create mode 100644 docs/tutorial/code/flask/visitors_charmcraft.yaml create mode 100644 docs/tutorial/code/flask/visitors_migrate.py create mode 100644 docs/tutorial/flask.rst create mode 100644 docs/tutorial/index.rst diff --git a/docs/common/craft-parts b/docs/common/craft-parts deleted file mode 120000 index 4aa111d9f..000000000 --- a/docs/common/craft-parts +++ /dev/null @@ -1 +0,0 @@ -/home/dora/git/charmcraft/.tox/docs/lib/python3.12/site-packages/craft_parts_docs/craft-parts \ No newline at end of file diff --git a/docs/reuse/tutorial/setup_stable.rst b/docs/reuse/tutorial/setup_stable.rst new file mode 100644 index 000000000..7737854e9 --- /dev/null +++ b/docs/reuse/tutorial/setup_stable.rst @@ -0,0 +1,70 @@ +First, `install Multipass `_. + +Use Multipass to launch an Ubuntu VM with the name ``charm-dev`` +from the 24.04 blueprint: + +.. code-block:: bash + + multipass launch --cpus 4 --disk 50G --memory 4G --name charm-dev 24.04 + +Once the VM is up, open a shell into it: + +.. code-block:: bash + + multipass shell charm-dev + +In order to create the rock, you'll need to install Rockcraft: + +.. code-block:: bash + + sudo snap install rockcraft --classic + +``LXD`` will be required for building the rock. +Make sure it is installed and initialised: + +.. code-block:: bash + + sudo snap install lxd + lxd init --auto + +In order to create the charm, you'll need to install Charmcraft: + +.. code-block:: bash + + sudo snap install charmcraft --channel latest/stable --classic + +.. warning:: + + This tutorial requires version ``3.0.0`` or later of Charmcraft. Check the + version of Charmcraft using ``charmcraft --version`` If you have an older + version of Charmcraft installed, use + ``sudo snap refresh charmcraft --channel latest/edge`` to get the latest + edge version of Charmcraft. + +MicroK8s is required to deploy the Flask application on Kubernetes. Install MicroK8s: + +.. code-block:: bash + + sudo snap install microk8s --channel 1.31-strict/stable + sudo adduser $USER snap_microk8s + newgrp snap_microk8s + +Wait for MicroK8s to be ready using ``sudo microk8s status --wait-ready``. +Several MicroK8s add-ons are required for deployment: + +.. code-block:: bash + + sudo microk8s enable hostpath-storage + # Required to host the OCI image of the Flask application + sudo microk8s enable registry + # Required to expose the Flask application + sudo microk8s enable ingress + +Juju is required to deploy the Flask application. +Install Juju and bootstrap a development controller: + +.. code-block:: bash + + sudo snap install juju --channel 3.5/stable + mkdir -p ~/.local/share + juju bootstrap microk8s dev-controller diff --git a/docs/tutorial/code/flask/app.py b/docs/tutorial/code/flask/app.py new file mode 100644 index 000000000..c2c4f114b --- /dev/null +++ b/docs/tutorial/code/flask/app.py @@ -0,0 +1,14 @@ +# initial hello world Flask app + +import flask + +app = flask.Flask(__name__) + + +@app.route("/") +def index(): + return "Hello, world!\n" + + +if __name__ == "__main__": + app.run() diff --git a/docs/tutorial/code/flask/greeting_app.py b/docs/tutorial/code/flask/greeting_app.py new file mode 100644 index 000000000..9f659d835 --- /dev/null +++ b/docs/tutorial/code/flask/greeting_app.py @@ -0,0 +1,16 @@ +# Flask app with a greeting configuration + +import flask + +app = flask.Flask(__name__) +app.config.from_prefixed_env() + + +@app.route("/") +def index(): + greeting = app.config.get("GREETING", "Hello, world!") + return f"{greeting}\n" + + +if __name__ == "__main__": + app.run() diff --git a/docs/tutorial/code/flask/greeting_charmcraft.yaml b/docs/tutorial/code/flask/greeting_charmcraft.yaml new file mode 100644 index 000000000..bf837f42d --- /dev/null +++ b/docs/tutorial/code/flask/greeting_charmcraft.yaml @@ -0,0 +1,9 @@ +# configuration snippet for Flask application with a configuration + +config: + options: + greeting: + description: | + The greeting to be returned by the Flask application. + default: "Hello, world!" + type: string diff --git a/docs/tutorial/code/flask/requirements.txt b/docs/tutorial/code/flask/requirements.txt new file mode 100644 index 000000000..e3e9a71d9 --- /dev/null +++ b/docs/tutorial/code/flask/requirements.txt @@ -0,0 +1 @@ +Flask diff --git a/docs/tutorial/code/flask/task.yaml b/docs/tutorial/code/flask/task.yaml new file mode 100644 index 000000000..13c5504e1 --- /dev/null +++ b/docs/tutorial/code/flask/task.yaml @@ -0,0 +1,220 @@ +########################################### +# IMPORTANT +# Comments matter! +# The docs use the wrapping comments as +# markers for including said instructions +# as snippets in the docs. +########################################### +summary: Getting started with Flask 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 + + # Add setup instructions + # (Ran into issues in prepare section) + # (don't install charmcraft) + snap install rockcraft --classic + snap install lxd + lxd init --auto + snap install microk8s --channel=1.31-strict/stable + snap refresh juju --channel=3.5/stable --amend + + # Juju config setup + lxc network set lxdbr0 ipv6.address none + mkdir -p ~/.local/share + + # 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 + pip install -r requirements.txt + # [docs:create-venv-end] + + flask run -p 8000 & + retry -n 5 --wait 2 curl --fail localhost:8000 + + # [docs:curl-flask] + curl localhost:8000 + # [docs:curl-flask-end] + + kill $! + + # [docs:create-rockcraft-yaml] + rockcraft init --profile flask-framework + # [docs:create-rockcraft-yaml-end] + + sed -i "s/name: .*/name: flask-hello-world/g" rockcraft.yaml + sed -i "s/amd64/$(dpkg --print-architecture)/g" rockcraft.yaml + + # [docs:pack] + rockcraft pack + # [docs:pack-end] + + # [docs:ls-rock] + ls *.rock -l + # [docs:ls-rock-end] + + # [docs:skopeo-copy] + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:flask-hello-world_0.1_$(dpkg --print-architecture).rock \ + docker://localhost:32000/flask-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 flask-framework --name flask-hello-world + # [docs:charm-init-end] + + # [docs:charm-pack] + charmcraft pack + # [docs:charm-pack-end] + + # [docs:ls-charm] + ls *.charm -l + # [docs:ls-charm-end] + + # [docs:add-juju-model] + juju add-model flask-hello-world + # [docs:add-juju-model-end] + + juju set-model-constraints -m flask-hello-world arch=$(dpkg --print-architecture) + + # [docs:deploy-juju-model] + juju deploy ./flask-hello-world_ubuntu-22.04-$(dpkg --print-architecture).charm \ + flask-hello-world --resource \ + flask-app-image=localhost:32000/flask-hello-world:0.1 + # [docs:deploy-juju-model-end] + + # [docs:deploy-nginx] + juju deploy nginx-ingress-integrator --channel=latest/edge --base ubuntu@20.04 + juju integrate nginx-ingress-integrator flask-hello-world + # [docs:deploy-nginx-end] + + # [docs:config-nginx] + juju config nginx-ingress-integrator \ + service-hostname=flask-hello-world path-routes=/ + # [docs:config-nginx-end] + + # give Juju some time to deploy the apps + juju wait-for application flask-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://flask-hello-world --resolve flask-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 + rockcraft pack + + # [docs:docker-update] + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:flask-hello-world_0.2_$(dpkg --print-architecture).rock \ + docker://localhost:32000/flask-hello-world:0.2 + # [docs:docker-update-end] + + cat greeting_charmcraft.yaml >> ./charm/charmcraft.yaml + cd charm + charmcraft pack + + # [docs:refresh-deployment] + juju refresh flask-hello-world \ + --path=./flask-hello-world_ubuntu-22.04-$(dpkg --print-architecture).charm \ + --resource flask-app-image=localhost:32000/flask-hello-world:0.2 + # [docs:refresh-deployment-end] + + # give Juju some time to refresh the app + juju wait-for application flask-hello-world --query='status=="active"' --timeout 10m + + # curl and check that the response is Hello + curl http://flask-hello-world --resolve flask-hello-world:80:127.0.0.1 | grep Hello + + # [docs:change-config] + juju config flask-hello-world greeting='Hi!' + # [docs:change-config-end] + + # make sure that the application updates + juju wait-for application flask-hello-world --query='status=="maintenance"' --timeout 10m + juju wait-for application flask-hello-world --query='status=="active"' --timeout 10m + + # curl and check that the response is now Hi + curl http://flask-hello-world --resolve flask-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 + echo "psycopg2-binary" >> requirements.txt + rockcraft pack + + # [docs:docker-2nd-update] + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:flask-hello-world_0.3_$(dpkg --print-architecture).rock \ + docker://localhost:32000/flask-hello-world:0.3 + # [docs:docker-2nd-update-end] + + cat visitors_charmcraft.yaml >> ./charm/charmcraft.yaml + cd charm + charmcraft pack + + # [docs:refresh-2nd-deployment] + juju refresh flask-hello-world \ + --path=./flask-hello-world_ubuntu-22.04-$(dpkg --print-architecture).charm \ + --resource flask-app-image=localhost:32000/flask-hello-world:0.3 + # [docs:refresh-2nd-deployment-end] + + # [docs:deploy-postgres] + juju deploy postgresql-k8s --channel=14/stable --trust + juju integrate flask-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 10m + juju wait-for application flask-hello-world --query='status=="active"' --timeout 30m | juju status --relations + + juju status --relations + + curl http://flask-hello-world --resolve flask-hello-world:80:127.0.0.1 | grep Hi + curl http://flask-hello-world/visitors --resolve flask-hello-world:80:127.0.0.1 | grep 1 + curl http://flask-hello-world --resolve flask-hello-world:80:127.0.0.1 | grep Hi + curl http://flask-hello-world/visitors --resolve flask-hello-world:80:127.0.0.1 | grep 2 + + # Back out to main directory for clean-up + 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 flask-hello-world_0.1_$(dpkg --print-architecture).rock flask-hello-world_0.2_$(dpkg --print-architecture).rock \ + flask-hello-world_0.3_$(dpkg --print-architecture).rock rockcraft.yaml app.py \ + requirements.txt migrate.py + # Remove the juju model + juju destroy-model flask-hello-world --destroy-storage --no-prompt --force + # [docs:clean-environment-end] diff --git a/docs/tutorial/code/flask/visitors_app.py b/docs/tutorial/code/flask/visitors_app.py new file mode 100644 index 000000000..67e32ea8f --- /dev/null +++ b/docs/tutorial/code/flask/visitors_app.py @@ -0,0 +1,42 @@ +# Flask application that keeps track of visitors using a database + +import datetime +import os + +import flask +import psycopg2 + +app = flask.Flask(__name__) +app.config.from_prefixed_env() + +DATABASE_URI = os.environ["POSTGRESQL_DB_CONNECT_STRING"] + + +@app.route("/") +def index(): + with psycopg2.connect(DATABASE_URI) as conn, conn.cursor() as cur: + user_agent = flask.request.headers.get('User-Agent') + timestamp = datetime.datetime.now() + + cur.execute( + "INSERT INTO visitors (timestamp, user_agent) VALUES (%s, %s)", + (timestamp, user_agent) + ) + conn.commit() + + + greeting = app.config.get("GREETING", "Hello, world!") + return f"{greeting}\n" + + +@app.route("/visitors") +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 f"{total_visitors}\n" + + +if __name__ == "__main__": + app.run() diff --git a/docs/tutorial/code/flask/visitors_charmcraft.yaml b/docs/tutorial/code/flask/visitors_charmcraft.yaml new file mode 100644 index 000000000..757935d58 --- /dev/null +++ b/docs/tutorial/code/flask/visitors_charmcraft.yaml @@ -0,0 +1,6 @@ +# requires snippet for Flask application with a database + +requires: + postgresql: + interface: postgresql_client + optional: false diff --git a/docs/tutorial/code/flask/visitors_migrate.py b/docs/tutorial/code/flask/visitors_migrate.py new file mode 100644 index 000000000..6ef58bece --- /dev/null +++ b/docs/tutorial/code/flask/visitors_migrate.py @@ -0,0 +1,23 @@ +# Adds database to Flask 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() diff --git a/docs/tutorial/flask.rst b/docs/tutorial/flask.rst new file mode 100644 index 000000000..37bfcc6bc --- /dev/null +++ b/docs/tutorial/flask.rst @@ -0,0 +1,523 @@ +================================================= +Write your first Kubernetes charm for a Flask app +================================================= + +Imagine you have a Flask application backed up by a database +such as PostgreSQL and need to deploy it. In a traditional setup, +this can be quite a challenge, but with Charmcraft you’ll find +yourself packaging and deploying your Flask application in no time. +Let’s get started! + +In this tutorial we will build a Kubernetes charm for a Flask +application using Charmcraft, so we can have a Flask application +up and running with Juju. + +This tutorial should take 90 minutes for you to complete. + +.. note:: + If you're new to the charming world: Flask applications are + specifically supported with a coordinated pair of profiles + for an OCI container image (**rock**) and corresponding + packaged software (**charm**) that allow for the application + to be deployed, integrated and operated on a Kubernetes + cluster with the Juju orchestration engine. + +What you’ll need +================ + +- A workstation, e.g., a laptop, with amd64 or arm64 architecture which + has sufficient resources to launch a virtual machine with 4 CPUs, + 4 GB RAM, and a 50 GB disk +- Familiarity with Linux + +What you’ll do +============== + +- Set things up +- Create the Flask application +- Run the Flask application locally +- Pack the Flask application into a rock called ``flask-hello-world`` +- Create the charm called ``flask-hello-world`` +- Deploy the Flask application and expose via ingress +- Enable ``juju config flask-hello-world greeting=`` +- Integrate with a database +- Clean up environment + +Set things up +============= + +.. include:: /reuse/tutorial/setup_stable.rst + +Finally, let's create a new directory for this tutorial and go +inside it: + +.. code-block:: bash + + mkdir flask-hello-world + cd flask-hello-world + +Create the Flask application +============================ + +Let's start by creating the "Hello, world" Flask application that +will be used for this tutorial. + +Create a ``requirements.txt`` file, copy the following text into it +and then save it: + +.. literalinclude:: code/flask/requirements.txt + +In the same directory, copy and save the following into a text file +called ``app.py``: + +.. literalinclude:: code/flask/app.py + :language: python + +Run the Flask application locally +================================= + +Let's install ``python3-venv`` and create a virtual environment: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:create-venv] + :end-before: [docs:create-venv-end] + :dedent: 2 + +Now that we have a virtual environment with all the dependencies, let's +run the Flask application to verify that it works: + +.. code-block:: bash + + flask run -p 8000 + +Test the Flask application by using ``curl`` to send a request to the root +endpoint. You will need a new terminal for this; use +``multipass shell charm-dev`` to get another terminal: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:curl-flask] + :end-before: [docs:curl-flask-end] + :dedent: 2 + +The Flask application should respond with ``Hello, world!``. The Flask +application looks good, so we can stop for now using +:kbd:`Ctrl` + :kbd:`C`. + +Pack the Flask application into a rock +====================================== + +First, we'll need a ``rockcraft.yaml`` file. Rockcraft will automate its +creation and tailoring for a Flask application by using the +``flask-framework`` profile: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:create-rockcraft-yaml] + :end-before: [docs:create-rockcraft-yaml-end] + :dedent: 2 + +The ``rockcraft.yaml`` file will automatically be created and set the name +based on your working directory. Choosing a different name or running on +a platform different from ``amd64`` will influence the names of the files +generated by Rockcraft. + +Open the file in a text editor and check that the ``name`` is +``flask-hello-world``. + +Ensure that ``platforms`` includes the architecture of your host. Check +the architecture of your system: + +.. code-block:: bash + + dpkg --print-architecture + + +If your host uses the ARM architecture, include ``arm64`` in ``platforms``. + +Now let's pack the rock: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:pack] + :end-before: [docs:pack-end] + :dedent: 2 + +Depending on your system and network, this step can take a couple of +minutes to finish. + +Once Rockcraft has finished packing the Flask rock, you'll find a new file +in your working directory with the ``.rock`` extension: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:ls-rock] + :end-before: [docs:ls-rock-end] + :dedent: 2 + +The rock needs to be copied to the MicroK8s registry so that it can be +deployed in the Kubernetes cluster: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:skopeo-copy] + :end-before: [docs:skopeo-copy-end] + :dedent: 2 + +.. seealso:: + + See more: `skopeo `_ + +Create the charm +================ + +Let's create a new directory for the charm and go inside it: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:create-charm-dir] + :end-before: [docs:create-charm-dir-end] + :dedent: 2 + +We'll need a ``charmcraft.yaml``, ``requirements.txt`` and source code for +the charm. The source code contains the logic required to operate the Flask +application. Charmcraft will automate the creation of these files by using +the ``flask-framework`` profile: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:charm-init] + :end-before: [docs:charm-init-end] + :dedent: 2 + +The files will automatically be created in your working directory. +Let's pack the charm: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:charm-pack] + :end-before: [docs:charm-pack-end] + :dedent: 2 + +Depending on your system and network, this step can take a couple +of minutes to finish. + +Once Charmcraft has finished packing the charm, you'll find a new file in your +working directory with the ``.charm`` extension: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:ls-charm] + :end-before: [docs:ls-charm-end] + :dedent: 2 + +.. note:: + + If you changed the name in charmcraft.yaml or are not on the amd64 platform, + the name of the ``.charm`` file will be different for you. + +Deploy the Flask application +============================ + +A Juju model is needed to deploy the application. Let's create a new model: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:add-juju-model] + :end-before: [docs:add-juju-model-end] + :dedent: 2 + +If you are not on a host with the ``amd64`` architecture, you will need to include +a constraint to the Juju model to specify your architecture. Check the +architecture of your system using ``dpkg --print-architecture``. + +For the ``arm64`` architecture, set the model constraints using + +.. code-block:: + + juju set-model-constraints -m flask-hello-world arch=arm64 + +Now the Flask application can be deployed using Juju: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:deploy-juju-model] + :end-before: [docs:deploy-juju-model-end] + :dedent: 2 + +It will take a few minutes to deploy the Flask application. You can monitor the +progress using ``juju status --watch 5s``. Once the status of the App has gone +to ``active``, you can stop watching using :kbd:`Ctrl` + :kbd:`C`. + +.. seealso:: + + See more: `Command 'juju status' `_ + +The Flask application should now be running. We can monitor the status of the deployment +using ``juju status`` which should be similar to the following output: + +.. terminal:: + + Model Controller Cloud/Region Version SLA Timestamp + flask-hello-world dev-controller microk8s/localhost 3.1.8 unsupported 17:04:11+10:00 + + App Version Status Scale Charm Channel Rev Address Exposed Message + flask-hello-world active 1 flask-hello-world 0 10.152.183.166 no + + Unit Workload Agent Address Ports Message + flask-hello-world/0* active idle 10.1.87.213 + +The deployment is finished when the status shows ``active``. Let's expose the +application using ingress. Deploy the ``nginx-ingress-integrator`` charm and integrate +it with the Flask app: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:deploy-nginx] + :end-before: [docs:deploy-nginx-end] + :dedent: 2 + +The hostname of the app needs to be defined so that it is accessible via the ingress. +We will also set the default route to be the root endpoint: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:config-nginx] + :end-before: [docs:config-nginx-end] + :dedent: 2 + +Monitor ``juju status`` until everything has a status of ``active``. Test the +deployment using +``curl http://flask-hello-world --resolve flask-hello-world:80:127.0.0.1`` to send +a request via the ingress to the root endpoint. It should still be returning +the ``Hello, world!`` greeting. + +.. note:: + + The ``--resolve flask-hello-world:80:127.0.0.1`` option to the ``curl`` + command is a way of resolving the hostname of the request without + setting a DNS record. + +Configure the Flask application +=============================== + +Now let's customise the greeting using a configuration option. We will expect this +configuration option to be available in the Flask app configuration under the +keyword ``GREETING``. Go back out to the root directory of the project using +``cd ..`` and copy the following code into ``app.py``: + +.. literalinclude:: code/flask/greeting_app.py + :language: python + +Open ``rockcraft.yaml`` and update the version to ``0.2``. Run ``rockcraft pack`` +again, then upload the new OCI image to the MicroK8s registry: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:docker-update] + :end-before: [docs:docker-update-end] + :dedent: 2 + +Change back into the charm directory using ``cd charm``. The ``flask-framework`` +Charmcraft extension supports adding configurations to ``charmcraft.yaml`` which +will be passed as environment variables to the Flask application. Add the +following to the end of the ``charmcraft.yaml`` file: + +.. code-block:: yaml + + config: + options: + greeting: + description: | + The greeting to be returned by the Flask application. + default: "Hello, world!" + type: string + +.. note:: + + Configuration options are automatically capitalised and ``-`` are replaced + by ``_``. A ``FLASK_`` prefix will also be added which will let Flask + identify which environment variables to include when running + ``app.config.from_prefixed_env()`` in ``app.py``. + +Run ``charmcraft pack`` again. We can now refresh the deployment to +make use of the new code: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:refresh-deployment] + :end-before: [docs:refresh-deployment-end] + :dedent: 2 + +Wait for ``juju status`` to show that the App is ``active`` again. Verify that +the new configuration has been added using +``juju config flask-hello-world | grep -A 6 greeting:`` which should show +the configuration option. + +.. note:: + + The ``grep`` command extracts a portion of the configuration to make + it easier to check whether the configuration option has been added. + +Using ``curl http://flask-hello-world --resolve flask-hello-world:80:127.0.0.1`` +shows that the response is still ``Hello, world!`` as expected. +The greeting can be changed using Juju: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:change-config] + :end-before: [docs:change-config-end] + :dedent: 2 + +``curl http://flask-hello-world --resolve flask-hello-world:80:127.0.0.1`` +now returns the updated ``Hi!`` greeting. + +.. note:: + + It might take a short time for the configuration to take effect. + +Integrate with a database +========================= + +Now let's keep track of how many visitors your application has received. +This will require integration with a database to keep the visitor count. +This will require a few changes: + +* We will need to create a database migration that creates the ``visitors`` table +* We will need to keep track how many times the root endpoint has been called + in the database +* We will need to add a new endpoint to retrieve the number of visitors from the + database + +Let's start with the database migration to create the required tables. +The charm created by the ``flask-framework`` extension will execute the +``migrate.py`` script if it exists. This script should ensure that the +database is initialised and ready to be used by the application. We will +create a ``migrate.py`` file containing this logic. + +Go back out to the tutorial root directory using ``cd ..``, open the ``migrate.py`` +file using a text editor and paste the following code into it: + +.. literalinclude:: code/flask/visitors_migrate.py + :language: python + +.. note:: + + The charm will pass the Database connection string in the + ``POSTGRESQL_DB_CONNECT_STRING`` environment variable once + postgres has been integrated with the charm. + +Open the ``rockcraft.yaml`` file in a text editor and update the version to ``0.3``. + +To be able to connect to postgresql from the Flask app the ``psycopg2-binary`` +dependency needs to be added in ``requirements.txt``. The app code also needs +to be updated to keep track of the number of visitors and to include a new +endpoint to retrieve the number of visitors to the app. Open ``app.py`` in +a text editor and replace its contents with the following code: + +.. collapse:: visitors_app.py + + .. literalinclude:: code/flask/visitors_app.py + :language: python + +Run ``rockcraft pack`` and upload the newly created rock to the MicroK8s registry: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:docker-2nd-update] + :end-before: [docs:docker-2nd-update-end] + :dedent: 2 + +Go back into the charm directory using ``cd charm``. The Flask app now requires +a database which needs to be declared in the ``charmcraft.yaml`` file. Open +``charmcraft.yaml`` in a text editor and add the following section to the end: + +.. code-block:: yaml + + requires: + postgresql: + interface: postgresql_client + optional: false + +Pack the charm using ``charmcraft pack`` and refresh the deployment using Juju: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:refresh-2nd-deployment] + :end-before: [docs:refresh-2nd-deployment-end] + :dedent: 2 + +Deploy ``postgresql-k8s`` using Juju and integrate it with ``flask-hello-world``: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:deploy-postgres] + :end-before: [docs:deploy-postgres-end] + :dedent: 2 + +Wait for ``juju status`` to show that the App is ``active`` again. +Running ``curl http://flask-hello-world --resolve flask-hello-world:80:127.0.0.1`` +should still return the ``Hi!`` greeting. + +To check the total visitors, use +``curl http://flask-hello-world/visitors --resolve flask-hello-world:80:127.0.0.1`` +which should return ``1`` after the previous request to the root endpoint and +should be incremented each time the root endpoint is requested. + +If we perform another request to +``curl http://flask-hello-world --resolve flask-hello-world:80:127.0.0.1``, +``curl http://flask-hello-world/visitors --resolve flask-hello-world:80:127.0.0.1`` +will return ``2``. + +Clean up the environment +======================== + +If you'd like to reset your working environment, you can run the following +in the root directory for the tutorial: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:clean-environment] + :end-before: [docs:clean-environment-end] + :dedent: 2 + +You can also clean up the Multipass instance. +Start by exiting it: + +.. code-block:: bash + + exit + +And then you can proceed with its deletion: + +.. code-block:: bash + + multipass delete charm-dev + multipass purge + +We've reached the end of this tutorial. We have created a Flask application, +deployed it locally, exposed it via ingress and integrated it with a database! + +Next steps +========== + +.. list-table:: + :widths: 30 30 + :header-rows: 1 + + * - If you are wondering... + - Visit... + * - "How do I...?" + - `SDK How-to docs `_ + * - "How do I debug?" + - `Charm debugging tools `_ + * - "How do I get in touch?" + - `Matrix channel `_ + * - "What is...?" + - `SDK Reference docs `_ + * - "Why...?", "So what?" + - `SDK Explanation docs `_ + +------------------------- + diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst new file mode 100644 index 000000000..3965c1272 --- /dev/null +++ b/docs/tutorial/index.rst @@ -0,0 +1,10 @@ +.. _tutorial: + +Tutorial +******** + +.. toctree:: + :maxdepth: 2 + + flask + diff --git a/spread.yaml b/spread.yaml index 4083ee765..2f53cace3 100644 --- a/spread.yaml +++ b/spread.yaml @@ -52,7 +52,7 @@ backends: system=$(echo "${SPREAD_SYSTEM}" | tr . -) instance_name="spread-${SPREAD_BACKEND}-${instance_num}-${system}" - multipass launch --cpus 2 --disk 20G --memory 2G --name "${instance_name}" "${multipass_image}" + multipass launch --cpus 4 --disk 40G --memory 4G --name "${instance_name}" "${multipass_image}" # Enable PasswordAuthentication for root over SSH. multipass exec "$instance_name" -- \ @@ -129,6 +129,11 @@ prepare: | install_charmcraft suites: + docs/tutorial/code/: + summary: tests tutorial from the docs + systems: + - ubuntu-22.04-64 + manual: true tests/spread/commands/: summary: simple charmcraft commands tests/spread/charms/: