=================================================
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=<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: `Ubuntu manpage | skopeo
    <https://manpages.ubuntu.com/manpages/noble/man1/skopeo.1.html>`_

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: :external+juju:ref:`Juju | juju status <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...?"
      - :ref:`How-to guides <how-to-guides>`,
        :external+ops:ref:`Ops | How-to guides <how-to-guides>`
    * - "How do I debug?"
      - `Charm debugging tools <https://juju.is/docs/sdk/debug-a-charm>`_
    * - "How do I get in touch?"
      - `Matrix channel <https://matrix.to/#/#12-factor-charms:ubuntu.com>`_
    * - "What is...?"
      - :ref:`reference`,
        :external+ops:ref:`Ops | Reference <reference>`,
        :external+juju:ref:`Juju | Reference <reference>`
    * - "Why...?", "So what?"
      - :external+ops:ref:`Ops | Explanation <explanation>`,
        :external+juju:ref:`Juju | Explanation <explanation>`
