.. _configure-12-factor-charms:

Manage configurations for a 12-factor app charm
===============================================

Add a new configuration
-----------------------

A charm configuration can be added if your 12-factor app
requires environment variables, for example, to pass a
token for a service. Add the configuration in ``charmcraft.yaml``:

.. code-block:: yaml

    config:
      options:
        token:
          description: The token for the service.
          type: string

.. tabs::

    .. group-tab:: Django

        A user-defined configuration option will correspond to an
        environment variable generated by the ``paas-charm`` project
        to expose the configuration to the Django workload. In general,
        a configuration option ``config-option-name`` will be mapped as
        ``DJANGO_CONFIG_OPTION_NAME`` where the option name will be
        converted to upper case, dashes will be converted to underscores
        and the ``DJANGO_`` prefix will be added. In the example above,
        the ``token`` configuration will be mapped as the ``DJANGO_TOKEN``
        environment variable.

            `How to add configuration to a charm <https://juju.is/docs/sdk/config>`_

    .. group-tab:: Express

        A user-defined configuration option will correspond to an environment
        variable generated by the ``paas-charm`` project to expose the
        configuration to the Express workload. In general, a configuration option
        ``config-option-name`` will be mapped as ``APP_CONFIG_OPTION_NAME``
        where the option name will be converted to upper case, dashes will be
        converted to underscores and the ``APP_`` prefix will be added. In the
        example above, the ``token`` configuration will be mapped as the
        ``APP_TOKEN`` environment variable.

            `How to add configuration to a charm <https://juju.is/docs/sdk/config>`_

    .. group-tab:: FastAPI

        A user-defined configuration option will correspond to an environment
        variable generated by the ``paas-charm`` project to expose the
        configuration to the FastAPI workload. In general, a configuration option
        called ``config-option-name`` will be mapped as ``APP_CONFIG_OPTION_NAME``
        where the option name will be converted to upper case, dashes will be
        converted to underscores and the ``APP_`` prefix will be added. In the
        example above, the ``token`` configuration will be mapped as the
        ``APP_TOKEN`` environment variable.

            `How to add configuration to a charm <https://juju.is/docs/sdk/config>`_

    .. group-tab:: Flask

        A user-defined configuration option will correspond to
        an environment variable generated by the ``paas-charm``
        project to expose the configuration to the Flask workload.
        In general, a configuration option ``config-option-name``
        will be mapped as ``FLASK_CONFIG_OPTION_NAME`` where the
        option name will be converted to upper case, dashes will be
        converted to underscores and the ``FLASK_`` prefix will be
        added. In the example above, the ``token`` configuration will
        be mapped as the ``FLASK_TOKEN`` environment variable. In
        addition to the environment variable, the configuration is
        also available in the Flask variable ``app.config`` without
        the ``FLASK_`` prefix.

            See also: :external+ops:ref:`Ops | Manage configuration
            <manage-configuration>`, `Configuration Handling -- Flask Documentation
            <https://flask.palletsprojects.com/en/3.0.x/config/>`_

    .. group-tab:: Go

        A user-defined configuration option will correspond to an environment
        variable generated by the ``paas-charm`` project to expose the
        configuration to the Go workload. In general, a configuration option
        ``config-option-name`` will be mapped as ``APP_CONFIG_OPTION_NAME``
        where the option name will be converted to upper case, dashes will be
        converted to underscores and the ``APP_`` prefix will be added. In the
        example above, the ``token`` configuration will be mapped as the
        ``APP_TOKEN`` environment variable.

            `How to add configuration to a charm <https://juju.is/docs/sdk/config>`_

    .. group-tab:: Spring Boot

        A user-defined configuration option will correspond to an environment
        variable generated by the ``paas-charm`` project to expose the
        configuration to the Spring Boot workload. In general, a configuration option
        ``config-option-name`` will be mapped as ``APP_CONFIG_OPTION_NAME``
        where the option name will be converted to upper case, dashes will be
        converted to underscores and the ``APP_`` prefix will be added. In the
        example above, the ``token`` configuration will be mapped as the
        ``APP_TOKEN`` environment variable.

            `How to add configuration to a charm <https://juju.is/docs/sdk/config>`_

The configuration can be set on the deployed charm using:

.. code-block:: bash

    juju config <app name> token=<token>

Add a custom action
-------------------

For commands and processes that see shared or frequent use, custom Juju actions
can expedite your workflow. Actions are handled in the Ops
library, and Juju manages the action's workflow via tasks and operations. To
implement a custom action, you need to declare the action in
``charmcraft.yaml`` and add an event handler to the Ops framework of your
charm.

.. seealso::

    :external+ops:ref:`Ops | Manage actions <manage-actions>`

The custom action must be defined in the project file under the
``actions`` section. You should provide a name, a description, and any
associated parameters.

Add the custom action to your charm code by modifying the ``src/charm.py``
file. You need to add the action under the class constructor function
(``__init__``) as well as a dedicated function for the action. When you define
the action, follow the convention of appending ``_action`` onto the
action name.

Once your web app is updated and deployed with Juju, you can call the custom
action using ``juju run``.

Example: clear cookies in a Django app
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

For example, let's say you want to add a custom action to your charmed Django
app that removes expired session cookies.

Add the custom action in the project file:

.. literalinclude:: ../code/custom-action-clearsession/clearsession_action_charmcraft.yaml
    :language: yaml
    :caption: charmcraft.yaml

Modify the ``src/charm.py`` to define your custom action as part of the class
and provide the function definition:

.. literalinclude:: ../code/custom-action-clearsession/clearsession_action_charm.py
    :language: python
    :caption: src/charm.py
    :lines: 20-46
    :emphasize-lines: 8-27

Build the charm using ``charmcraft pack`` and deploy the app with Juju.

Finally, call the action using:

.. code-block:: bash

    juju run <django unit name> clearsession

If successful, the terminal will output something like:

.. terminal::
    :input: juju run django-app/0 clearsession

    Running operation 1 with 1 task
      - task 2 on unit-django-app-0

    Waiting for task 2...
    result: session cleared!


Example: check Flask app status and write output to container file
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

As another example, let's say you want to add a custom action to your charmed
Flask app that checks the status of the running app and writes the status to a
log file in the app container. The action performs the following steps:

- Sends a request to the Flask app at its available port.
- If the request is successful, updates the app container with the status
  message in a logfile.
- As a consistency check, reads the contents of the log file in the app
  container and outputs the status message as part of the action result.
  In practice, you could create a separate action for reading the log file.

Add the custom action to the project file:

.. literalinclude:: ../code/custom-action-updatelogfile/updatelogfile_action_charmcraft.yaml
    :language: yaml
    :caption: charmcraft.yaml

Add ``import requests`` to the start of ``src/charm.py``, then define
your custom action as part of the class and provide the function definition:

.. literalinclude:: ../code/custom-action-updatelogfile/updatelogfile_action_charm.py
    :language: python
    :caption: src/charm.py
    :lines: 22-65
    :emphasize-lines: 8-44

Build the charm using ``charmcraft pack`` and deploy the app with Juju.

Finally, call the action using:

.. code:: bash

   juju run <flask unit name> updatelogfile logfile=<full path to logfile>

If successful, the terminal will output something like:

.. terminal::
    :input: juju run flask-app/0 updatelogfile logfile="/tmp/example.log"

    Running operation 1 with 1 task
      - task 2 on unit-flask-app-0

    Waiting for task 2...
    result: |-
      App response: Hello, world!
      Output written to file: Hello, world!

.. warning::

    Writing to a file in the app container is unstable because the Juju units
    are ephemeral, meaning that container files are not persistent in the case
    of the unit's restart or deletion.

Manage secrets
--------------

A user secret can be added to a charm and all the keys and values
in the secret will be exposed as environment variables. Add the secret
configuration option in your project file:

.. code-block:: yaml

    config:
      options:
        api-token:
          type: secret
          description: Secret needed to access some API secret information

Once the charm is deployed, you can add a juju secret to the model:

.. terminal::
    :input: juju add-secret my-api-token value=1234 othervalue=5678

    secret:cru00lvmp25c77qa0qrg

From this output, you can get the Juju secret ID. Grant the application
access to view the value of the secret:

.. code-block:: bash

    juju grant-secret my-api-token <app name>

Add the Juju secret ID to the application:

.. code-block:: bash

    juju config <app name> api-token=secret:cru00lvmp25c77qa0qrg

.. tabs::

    .. group-tab:: Django

        The following environment variables are available for the application:

        - ``DJANGO_API_TOKEN_VALUE: "1234"``
        - ``DJANGO_API_TOKEN_OTHERVALUE: "5678"``

            See also: `How to manage secrets
            <https://juju.is/docs/juju/manage-secrets>`_

    .. group-tab:: Express

        The following environment variables are available for the application:

        - ``APP_API_TOKEN_VALUE: "1234"``
        - ``APP_API_TOKEN_OTHERVALUE: "5678"``

            See also: `How to manage secrets
            <https://juju.is/docs/juju/manage-secrets>`_

    .. group-tab:: FastAPI

        The following environment variables are available for the application:

        - ``APP_API_TOKEN_VALUE: "1234"``
        - ``APP_API_TOKEN_OTHERVALUE: "5678"``

            See also: `How to manage secrets
            <https://juju.is/docs/juju/manage-secrets>`_

    .. group-tab:: Flask

        The following environment variables are available for the application:

        - ``APP_API_TOKEN_VALUE: "1234"``
        - ``APP_API_TOKEN_OTHERVALUE: "5678"``

            See also: `How to manage secrets
            <https://juju.is/docs/juju/manage-secrets>`_

    .. group-tab:: Go

        The following environment variables are available for the application:

        - ``APP_API_TOKEN_VALUE: "1234"``
        - ``APP_API_TOKEN_OTHERVALUE: "5678"``

            See also: `How to manage secrets
            <https://juju.is/docs/juju/manage-secrets>`_

    .. group-tab:: Spring Boot

        The following environment variables are available for the application:

        - ``APP_API_TOKEN_VALUE: "1234"``
        - ``APP_API_TOKEN_OTHERVALUE: "5678"``

            See also: `How to manage secrets
            <https://juju.is/docs/juju/manage-secrets>`_

Write a Kubernetes charm for an async Flask app
-----------------------------------------------

In this guide you will configure a 12-factor Flask
app to use asynchronous Gunicorn workers to be
able to serve to multiple users easily.

.. note::

    This guide also applies to Django apps.

Make the rock async
~~~~~~~~~~~~~~~~~~~

To make the rock async, make sure to put the following in its ``requirements.txt``
file:

.. literalinclude:: ../code/flask-async/requirements.txt
    :caption: requirements.txt

Pack the rock using ``rockcraft pack`` and redeploy the charm with the new rock using
:external+juju:ref:`command-juju-refresh`.

Configure the async application
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Now let's enable async Gunicorn workers. We will
expect this configuration option to be available in the Flask app configuration
under the ``webserver-worker-class`` key. Assuming the app name is
``flask-async-app``, verify that the new configuration
has been added by running:

.. code:: bash

  juju config flask-async-app | grep -A 6 webserver-worker-class:

The result should contain the key.

The worker class can be changed using Juju:

.. literalinclude:: ../code/flask-async/task.yaml
    :language: bash
    :start-after: [docs:config-async]
    :end-before: [docs:config-async-end]
    :dedent: 2

Test that the workers are operating in parallel by sending multiple
simultaneous requests with curl:

.. code:: bash

  curl --parallel --parallel-immediate --resolve flask-async-app:80:127.0.0.1 \
  http://flask-async-app/io http://flask-async-app/io http://flask-async-app/io \
  http://flask-async-app/io http://flask-async-app/io

and they will all return at the same time.

The results should arrive simultaneously and contain five instances of ``ok``:

.. terminal::

   ok
   ok
   ok
   ok
   ok

It can take up to a minute for the configuration to take effect. When the
configuration changes, the charm will re-enter the active state, which you
can monitor using ``juju status``.
