From 69c697ed4e206903d75464509d201cbcadc4f2a0 Mon Sep 17 00:00:00 2001 From: ali ugur Date: Tue, 12 Nov 2024 06:10:07 +0300 Subject: [PATCH 01/13] Feat(extension): Added option to enable async workers in Flask and Django --- charmcraft/extensions/app.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/charmcraft/extensions/app.py b/charmcraft/extensions/app.py index f7a6eb46d..14dfa78c2 100644 --- a/charmcraft/extensions/app.py +++ b/charmcraft/extensions/app.py @@ -202,6 +202,10 @@ def get_image_name(self) -> str: "type": "int", "description": "The number of webserver worker processes for handling requests.", }, + "webserver-worker-class": { + "type": "str", + "description": "The method of webserver worker processes for handling requests. Can be either 'gevent' or 'sync'.", + }, } From a4d82c0b265f0aeefcc425cf5f982b962ee5b47d Mon Sep 17 00:00:00 2001 From: ali ugur Date: Mon, 2 Dec 2024 15:08:01 +0300 Subject: [PATCH 02/13] Chore(doc): Add Spread test/tutorial doc --- charmcraft/extensions/app.py | 2 +- docs/tutorial/code/flask-async/app.py | 19 + .../code/flask-async/requirements.txt | 1 + docs/tutorial/code/flask-async/task.yaml | 227 ++++++++++ docs/tutorial/flask-async.rst | 412 ++++++++++++++++++ docs/tutorial/index.rst | 9 + spread.yaml | 6 +- 7 files changed, 674 insertions(+), 2 deletions(-) create mode 100644 docs/tutorial/code/flask-async/app.py create mode 100644 docs/tutorial/code/flask-async/requirements.txt create mode 100644 docs/tutorial/code/flask-async/task.yaml create mode 100644 docs/tutorial/flask-async.rst create mode 100644 docs/tutorial/index.rst diff --git a/charmcraft/extensions/app.py b/charmcraft/extensions/app.py index 14dfa78c2..57f2e7ef1 100644 --- a/charmcraft/extensions/app.py +++ b/charmcraft/extensions/app.py @@ -203,7 +203,7 @@ def get_image_name(self) -> str: "description": "The number of webserver worker processes for handling requests.", }, "webserver-worker-class": { - "type": "str", + "type": "string", "description": "The method of webserver worker processes for handling requests. Can be either 'gevent' or 'sync'.", }, } diff --git a/docs/tutorial/code/flask-async/app.py b/docs/tutorial/code/flask-async/app.py new file mode 100644 index 000000000..2f1b61c7d --- /dev/null +++ b/docs/tutorial/code/flask-async/app.py @@ -0,0 +1,19 @@ +from time import sleep + +import flask + +app = flask.Flask(__name__) + + +@app.route("/") +def index(): + return "Hello, world!\n" + + +@app.route("/io") +def pseudo_io(): + sleep(2) + return "ok\n" + +if __name__ == "__main__": + app.run() diff --git a/docs/tutorial/code/flask-async/requirements.txt b/docs/tutorial/code/flask-async/requirements.txt new file mode 100644 index 000000000..e3e9a71d9 --- /dev/null +++ b/docs/tutorial/code/flask-async/requirements.txt @@ -0,0 +1 @@ +Flask diff --git a/docs/tutorial/code/flask-async/task.yaml b/docs/tutorial/code/flask-async/task.yaml new file mode 100644 index 000000000..badd91a5f --- /dev/null +++ b/docs/tutorial/code/flask-async/task.yaml @@ -0,0 +1,227 @@ +########################################### +# 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: 90m + +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) + # snap install rockcraft --channel=latest/edge --classic + + # Install the latest rockcraft snap + # (This can be removed after the Rockcraft PR is merged) + # The PR: https://github.com/canonical/rockcraft/pull/747 + snap install snapcraft --channel=latest/edge --classic + # Download rockcraft async-workers branch and alithethird fork + git clone -b flask-django-extention-async-workers https://github.com/alithethird/rockcraft + cd rockcraft + snapcraft pack + snap install --dangerous --classic rockcraft_* + + + snap install lxd + lxd init --auto + snap refresh charmcraft --channel=latest/edge --amend + snap install microk8s --channel=1.31-strict/stable + snap install juju --channel=3.5/stable + + # 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] + + # [docs:curl-flask-async-app] + curl localhost:8000/io + # [docs:curl-flask-async-app-end] + + kill $! + + # [docs:create-rockcraft-yaml] + rockcraft init --profile flask-framework + # [docs:create-rockcraft-yaml-end] + + sed -i "s/name: .*/name: flask-async-app/g" rockcraft.yaml + sed -i "s/amd64/$(dpkg --print-architecture)/g" rockcraft.yaml + + # uncomment the parts main section + sed -i "s/# parts:/parts:/g" rockcraft.yaml + # uncomment the async-dependencies part + awk -i inplace -v block_key="flask-framework/async-dependencies" ' + BEGIN { + in_block = 0; + comment_pattern = "^#[[:space:]]"; + uncommented_line = ""; + } + + /^#[[:space:]]/ { + # Check if the line contains the block key + if (in_block == 0 && $0 ~ block_key) { + in_block = 1; + } + } + + { + # If in_block is active, uncomment lines + if (in_block == 1) { + uncommented_line = gensub(comment_pattern, "", 1, $0); + if (uncommented_line == $0) { + in_block = 0; + } + print uncommented_line; + } else { + print $0; + } + }' 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-async-app_0.1_$(dpkg --print-architecture).rock \ + docker://localhost:32000/flask-async-app: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-async-app + # [docs:charm-init-end] + + sed -i "s/paas-charm.*/https:\/\/github.com\/canonical\/paas-charm\/archive\/async-workers.tar.gz/g" requirements.txt + + # [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-async-app + # [docs:add-juju-model-end] + + juju set-model-constraints -m flask-async-app arch=$(dpkg --print-architecture) + + # [docs:deploy-juju-model] + juju deploy ./flask-async-app_ubuntu-22.04-$(dpkg --print-architecture).charm \ + flask-async-app --resource \ + flask-app-image=localhost:32000/flask-async-app:0.1 + # [docs:deploy-juju-model-end] + + # [docs:deploy-nginx] + juju deploy nginx-ingress-integrator --channel=latest/edge --revision 122 + juju integrate nginx-ingress-integrator flask-async-app + # [docs:deploy-nginx-end] + + # [docs:config-nginx] + juju config nginx-ingress-integrator \ + service-hostname=flask-async-app path-routes=/ + # [docs:config-nginx-end] + + # give Juju some time to deploy the apps + juju wait-for application flask-async-app --query='status=="active"' --timeout 10m + juju wait-for application nginx-ingress-integrator --query='status=="active"' --timeout 10m + + # [docs:curl-init-deployment] + curl http://flask-async-app --resolve flask-async-app:80:127.0.0.1 + # [docs:curl-init-deployment-end] + + # [docs:config-async] + juju config flask-async-app webserver-worker-class=gevent + # [docs:config-async-end] + + # test the async flask service + NUM_REQUESTS=15 + ASYNC_RESULT='TRUE' + + echo "Firing $NUM_REQUESTS requests to $URL..." + + overall_start_time=$(date +%s) + + for i in $(seq 1 $NUM_REQUESTS); do + ( + start_time=$(date +%s) + echo "Request $i start time: $start_time" + + curl -s http://flask-async-app/io --resolve flask-async-app:80:127.0.0.1 + + end_time=$(date +%s) + pass_time=$((end_time - start_time)) + echo "Request $i end time: $end_time == $pass_time" + ) & + done + + wait + end_time=$(date +%s) + overall_passtime=$((end_time - overall_start_time)) + echo "Total pass time: $overall_passtime" + if [ $((3 < overall_passtime)) -eq 1 ]; then + echo "Error!" + ASYNC_RESULT='FALSE' + exit 2 + fi + [ "$ASYNC_RESULT" == 'TRUE' ] + + # 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-async-app_0.1_$(dpkg --print-architecture).rock flask-async-app_0.2_$(dpkg --print-architecture).rock \ + flask-async-app_0.3_$(dpkg --print-architecture).rock rockcraft.yaml app.py \ + requirements.txt migrate.py + # Remove the juju model + juju destroy-model flask-async-app --destroy-storage --no-prompt --force + # [docs:clean-environment-end] diff --git a/docs/tutorial/flask-async.rst b/docs/tutorial/flask-async.rst new file mode 100644 index 000000000..ae36d5b08 --- /dev/null +++ b/docs/tutorial/flask-async.rst @@ -0,0 +1,412 @@ +==================================================== +Write your a Kubernetes charm for an Async Flask app +==================================================== + +Imagine you have a Flask application that has endpoints +run for quite some time and need to deploy it. In a traditional setup, +this can be quite a challenge, but with Juju you’ll find yourself +deploying, configuring, scaling, integrating, monitoring, etc., +your Flask application in no time. Let’s get started! + +In this tutorial we will build a rock and Kubernetes charm for a +Flask application using the charm SDK, so we can have a Flask +application up and running with Juju in about 90 minutes. We will +also configure this application to use asynchronous Gunicorn workers +to be able to serve to multiple users easily. + +.. note:: + + **rock**: An Ubuntu LTS-based OCI compatible + container image designed to meet security, stability, and + reliability requirements for cloud-native software. + + **charm**: A package consisting of YAML files + Python code that will + automate every aspect of an application's lifecycle so it can + be easily orchestrated with Juju. + + **Juju**: An orchestration engine for software + operators that enables the deployment, integration and lifecycle + management of applications using charms. + +**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 +- Create the charm +- Deploy the Flask application and expose via ingress +- Enable ``juju config flask-async-app webserver-worker-class=gevent`` +- Clean up environment + +.. hint:: + + Don't hesitate to get in touch on + `Matrix `_ or + `Discourse `_ (or follow the + "Edit this page on GitHub" on the bottom of + this document to comment directly on the document). + + +Set things up +============= + +.. include:: /reuse/tutorial/setup.rst + +Finally, create a new directory for this tutorial and go inside it: + +.. code-block:: bash + + mkdir flask-async-app + cd flask-async-app + +Create the Flask application +============================ + +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-async/requirements.txt + +In the same directory, copy and save the following into a text file +called ``app.py``: + +.. literalinclude:: code/flask-async/app.py + :language: python + +Run the Flask application locally +================================= + +Install ``python3-venv`` and create a virtual environment: + +.. literalinclude:: code/flask-async/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 may need a new terminal for this; if you are using Multipass +use ``multipass shell charm-dev`` to get another terminal: + +.. literalinclude:: code/flask-async/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!``. + +Test the long running endpoint by sending a request to ``/io``: + +.. literalinclude:: code/flask-async/task.yaml + :language: bash + :start-after: [docs:curl-flask-async] + :end-before: [docs:curl-flask-async-end] + :dedent: 2 + +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-async/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. Open the file in a text editor and check +that the ``name`` is ``flask-async-app``. Ensure that ``platforms`` +includes the architecture of your host. For example, if your host uses the +ARM architecture, include ``arm64`` in ``platforms``. Make sure to uncomment +the ``parts:`` line and the following lines to enable async workers: + +.. code-block:: + flask-framework/async-dependencies: + python-packages: + - gunicorn[gevent] + +.. note:: + + For this tutorial, we'll use the ``name`` "flask-async-app" and assume + you are on the ``amd64`` platform. Check the architecture of your system + using ``dpkg --print-architecture``. Choosing a different name or + running on a different platform will influence the names of the files + generated by Rockcraft. + +Pack the rock: + +.. literalinclude:: code/flask-async/task.yaml + :language: bash + :start-after: [docs:pack] + :end-before: [docs:pack-end] + :dedent: 2 + +.. note:: + + 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-async/task.yaml + :language: bash + :start-after: [docs:ls-rock] + :end-before: [docs:ls-rock-end] + :dedent: 2 + +.. note:: + + If you changed the ``name`` or ``version`` in ``rockcraft.yaml`` or are + not on an ``amd64`` platform, the name of the ``.rock`` file will be + different for you. + +The rock needs to be copied to the MicroK8s registry so that it can be +deployed in the Kubernetes cluster: + +.. literalinclude:: code/flask-async/task.yaml + :language: bash + :start-after: [docs:skopeo-copy] + :end-before: [docs:skopeo-copy-end] + :dedent: 2 + +.. seealso:: + + See more: `skopeo `_ + +Create the charm +================ + +Create a new directory for the charm and go inside it: + +.. literalinclude:: code/flask-async/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-async/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. +Pack the charm: + +.. literalinclude:: code/flask-async/task.yaml + :language: bash + :start-after: [docs:charm-pack] + :end-before: [docs:charm-pack-end] + :dedent: 2 + +.. note:: + + 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-async/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-async/task.yaml + :language: bash + :start-after: [docs:add-juju-model] + :end-before: [docs:add-juju-model-end] + :dedent: 2 + +.. warning:: + + 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. For example, for + the arm64 architecture, use + ``juju set-model-constraints -m flask-async-app arch=arm64``. + Check the architecture of your system using ``dpkg --print-architecture``. + +Now the Flask application can be deployed using `Juju `_: + +.. literalinclude:: code/flask-async/task.yaml + :language: bash + :start-after: [docs:deploy-juju-model] + :end-before: [docs:deploy-juju-model-end] + :dedent: 2 + +.. note:: + + 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`. + + 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: + +.. code-block:: + + Model Controller Cloud/Region Version SLA Timestamp + flask-async-app dev-controller microk8s/localhost 3.1.8 unsupported 17:04:11+10:00 + + App Version Status Scale Charm Channel Rev Address Exposed Message + flask-async-app active 1 flask-async-app 0 10.152.183.166 no + + Unit Workload Agent Address Ports Message + flask-async-app/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-async/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-async/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-async-app --resolve flask-async-app: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-async-app: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 enable async Gunicorn workers using a configuration option. We will +expect this configuration option to be available in the Flask app configuration under the +keyword ``webserver-worker-class``. Verify that +the new configuration has been added using +``juju config flask-async-app | grep -A 6 webserver-worker-class:`` 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. + +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 + +Now you can run +``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 `` +in they will all return at the same time. + +.. note:: + + It might take a short time for the configuration to take effect. + +Clean up environment +==================== + +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! + +If you'd like to reset your working environment, you can run the following +in the root directory for the tutorial: + +.. literalinclude:: code/flask-async/task.yaml + :language: bash + :start-after: [docs:clean-environment] + :end-before: [docs:clean-environment-end] + :dedent: 2 + +If you created an instance using Multipass, you can also clean it up. +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 + +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 `_ + * - "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..d14c1dd67 --- /dev/null +++ b/docs/tutorial/index.rst @@ -0,0 +1,9 @@ +.. _tutorial: + +Tutorial +******** + +.. toctree:: + :maxdepth: 2 + + flask-async diff --git a/spread.yaml b/spread.yaml index 4083ee765..5254492b3 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 50G --memory 8G --name "${instance_name}" "${multipass_image}" # Enable PasswordAuthentication for root over SSH. multipass exec "$instance_name" -- \ @@ -129,6 +129,10 @@ prepare: | install_charmcraft suites: + docs/tutorial/code/: + summary: tests tutorial from the docs + systems: + - ubuntu-22.04-64 tests/spread/commands/: summary: simple charmcraft commands tests/spread/charms/: From 7fa50ec3775d6c3e4e8f8006fd4ee505e780dd17 Mon Sep 17 00:00:00 2001 From: ali ugur Date: Mon, 2 Dec 2024 15:15:34 +0300 Subject: [PATCH 03/13] Chore(docs): Lint docs --- docs/tutorial/flask-async.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/tutorial/flask-async.rst b/docs/tutorial/flask-async.rst index ae36d5b08..0c82f9a17 100644 --- a/docs/tutorial/flask-async.rst +++ b/docs/tutorial/flask-async.rst @@ -333,11 +333,11 @@ Configure the Flask application =============================== Now let's enable async Gunicorn workers using a configuration option. We will -expect this configuration option to be available in the Flask app configuration under the -keyword ``webserver-worker-class``. Verify that -the new configuration has been added using -``juju config flask-async-app | grep -A 6 webserver-worker-class:`` which should show -the configuration option. +expect this configuration option to be available in the Flask app configuration +under the keyword ``webserver-worker-class``. Verify that the new configuration +has been added using +``juju config flask-async-app | grep -A 6 webserver-worker-class:`` which should +show the configuration option. .. note:: @@ -355,7 +355,7 @@ The worker class can be changed using Juju: Now you can run ``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 `` +http://flask-async-app/io http://flask-async-app/io`` in they will all return at the same time. .. note:: From 364e3cc7960cef4c7980aba819cf7c3696d888ae Mon Sep 17 00:00:00 2001 From: ali ugur Date: Mon, 2 Dec 2024 18:10:20 +0300 Subject: [PATCH 04/13] Run CI From a5c872f46836075a49f09a4d0ddef0a63a3370f2 Mon Sep 17 00:00:00 2001 From: ali ugur Date: Tue, 3 Dec 2024 12:57:05 +0300 Subject: [PATCH 05/13] Chore(test): Fix spread test --- docs/tutorial/code/flask-async/task.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/tutorial/code/flask-async/task.yaml b/docs/tutorial/code/flask-async/task.yaml index badd91a5f..865257aa6 100644 --- a/docs/tutorial/code/flask-async/task.yaml +++ b/docs/tutorial/code/flask-async/task.yaml @@ -55,11 +55,13 @@ execute: | # Bootstrap controller juju bootstrap microk8s dev-controller + cd $HOME # [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 + pip install Flask # [docs:create-venv-end] flask run -p 8000 & From 5000827ab4e06e1d82a8e92fb9f074f5cf91ec82 Mon Sep 17 00:00:00 2001 From: ali ugur Date: Tue, 3 Dec 2024 13:20:09 +0300 Subject: [PATCH 06/13] Chore(test): Fix charmcraft version in spread test --- docs/tutorial/code/flask-async/task.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/code/flask-async/task.yaml b/docs/tutorial/code/flask-async/task.yaml index 865257aa6..05bd118d6 100644 --- a/docs/tutorial/code/flask-async/task.yaml +++ b/docs/tutorial/code/flask-async/task.yaml @@ -38,7 +38,7 @@ execute: | snap install lxd lxd init --auto - snap refresh charmcraft --channel=latest/edge --amend + # snap refresh charmcraft --channel=latest/edge --amend snap install microk8s --channel=1.31-strict/stable snap install juju --channel=3.5/stable From 5d7739e9db109c1d0bdf5e86e49f9785413cbcfc Mon Sep 17 00:00:00 2001 From: ali ugur Date: Mon, 9 Dec 2024 09:11:23 +0300 Subject: [PATCH 07/13] Chore(docs): Changed tutorial to how-to. Updated spread test. --- .../code/flask-async/app.py | 0 docs/howto/code/flask-async/requirements.txt | 2 + .../code/flask-async/task.yaml | 47 +- docs/howto/flask-async.rst | 48 ++ docs/howto/index.rst | 1 + .../code/flask-async/requirements.txt | 1 - docs/tutorial/flask-async.rst | 412 ------------------ docs/tutorial/index.rst | 9 - spread.yaml | 8 +- 9 files changed, 62 insertions(+), 466 deletions(-) rename docs/{tutorial => howto}/code/flask-async/app.py (100%) create mode 100644 docs/howto/code/flask-async/requirements.txt rename docs/{tutorial => howto}/code/flask-async/task.yaml (81%) create mode 100644 docs/howto/flask-async.rst delete mode 100644 docs/tutorial/code/flask-async/requirements.txt delete mode 100644 docs/tutorial/flask-async.rst delete mode 100644 docs/tutorial/index.rst diff --git a/docs/tutorial/code/flask-async/app.py b/docs/howto/code/flask-async/app.py similarity index 100% rename from docs/tutorial/code/flask-async/app.py rename to docs/howto/code/flask-async/app.py diff --git a/docs/howto/code/flask-async/requirements.txt b/docs/howto/code/flask-async/requirements.txt new file mode 100644 index 000000000..ea9f5b21c --- /dev/null +++ b/docs/howto/code/flask-async/requirements.txt @@ -0,0 +1,2 @@ +Flask +gevent diff --git a/docs/tutorial/code/flask-async/task.yaml b/docs/howto/code/flask-async/task.yaml similarity index 81% rename from docs/tutorial/code/flask-async/task.yaml rename to docs/howto/code/flask-async/task.yaml index 05bd118d6..acc96ccec 100644 --- a/docs/tutorial/code/flask-async/task.yaml +++ b/docs/howto/code/flask-async/task.yaml @@ -5,7 +5,7 @@ # markers for including said instructions # as snippets in the docs. ########################################### -summary: Getting started with Flask tutorial +summary: How to create async Flask Charm kill-timeout: 90m @@ -35,15 +35,10 @@ execute: | snapcraft pack snap install --dangerous --classic rockcraft_* - - snap install lxd - lxd init --auto # snap refresh charmcraft --channel=latest/edge --amend - snap install microk8s --channel=1.31-strict/stable - snap install juju --channel=3.5/stable + snap install microk8s --channel=1.32-strict/stable + snap install juju --channel=3/stable - # Juju config setup - lxc network set lxdbr0 ipv6.address none mkdir -p ~/.local/share # MicroK8s config setup @@ -61,7 +56,6 @@ execute: | python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt - pip install Flask # [docs:create-venv-end] flask run -p 8000 & @@ -84,36 +78,6 @@ execute: | sed -i "s/name: .*/name: flask-async-app/g" rockcraft.yaml sed -i "s/amd64/$(dpkg --print-architecture)/g" rockcraft.yaml - # uncomment the parts main section - sed -i "s/# parts:/parts:/g" rockcraft.yaml - # uncomment the async-dependencies part - awk -i inplace -v block_key="flask-framework/async-dependencies" ' - BEGIN { - in_block = 0; - comment_pattern = "^#[[:space:]]"; - uncommented_line = ""; - } - - /^#[[:space:]]/ { - # Check if the line contains the block key - if (in_block == 0 && $0 ~ block_key) { - in_block = 1; - } - } - - { - # If in_block is active, uncomment lines - if (in_block == 1) { - uncommented_line = gensub(comment_pattern, "", 1, $0); - if (uncommented_line == $0) { - in_block = 0; - } - print uncommented_line; - } else { - print $0; - } - }' rockcraft.yaml - # [docs:pack] rockcraft pack # [docs:pack-end] @@ -181,6 +145,8 @@ execute: | juju config flask-async-app webserver-worker-class=gevent # [docs:config-async-end] + juju wait-for application flask-async-app --query='status=="active"' --timeout 10m + # test the async flask service NUM_REQUESTS=15 ASYNC_RESULT='TRUE' @@ -221,8 +187,7 @@ execute: | deactivate rm -rf charm .venv __pycache__ # delete all the files created during the tutorial - rm flask-async-app_0.1_$(dpkg --print-architecture).rock flask-async-app_0.2_$(dpkg --print-architecture).rock \ - flask-async-app_0.3_$(dpkg --print-architecture).rock rockcraft.yaml app.py \ + rm flask-async-app_0.1_$(dpkg --print-architecture).rock rockcraft.yaml app.py \ requirements.txt migrate.py # Remove the juju model juju destroy-model flask-async-app --destroy-storage --no-prompt --force diff --git a/docs/howto/flask-async.rst b/docs/howto/flask-async.rst new file mode 100644 index 000000000..4d0776056 --- /dev/null +++ b/docs/howto/flask-async.rst @@ -0,0 +1,48 @@ +====================================================== +How to write a Kubernetes charm for an Async Flask app +====================================================== + +In this how to we will configure 12 Factor Flask application +to use asynchronous Gunicorn workers to be able to serve +to multiple users easily. + +Make the rock async +=================== + +Before packing the rock make sure to put the following in ``requirements.txt`` +file: + +.. literalinclude:: code/flask-async/requirements.txt + +Configure the async application +=============================== + +Now let's enable async Gunicorn workers using a configuration option. We will +expect this configuration option to be available in the Flask app configuration +under the keyword ``webserver-worker-class``. Verify that the new configuration +has been added using +``juju config flask-async-app | grep -A 6 webserver-worker-class:`` 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. + +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 + +Now you can run +``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`` +in they will all return at the same time. + +.. note:: + + It might take a short time for the configuration to take effect. diff --git a/docs/howto/index.rst b/docs/howto/index.rst index 9bf000898..fc641d4a8 100644 --- a/docs/howto/index.rst +++ b/docs/howto/index.rst @@ -9,3 +9,4 @@ How-To charm-to-poetry charm-to-python shared-cache + flask-async diff --git a/docs/tutorial/code/flask-async/requirements.txt b/docs/tutorial/code/flask-async/requirements.txt deleted file mode 100644 index e3e9a71d9..000000000 --- a/docs/tutorial/code/flask-async/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -Flask diff --git a/docs/tutorial/flask-async.rst b/docs/tutorial/flask-async.rst deleted file mode 100644 index 0c82f9a17..000000000 --- a/docs/tutorial/flask-async.rst +++ /dev/null @@ -1,412 +0,0 @@ -==================================================== -Write your a Kubernetes charm for an Async Flask app -==================================================== - -Imagine you have a Flask application that has endpoints -run for quite some time and need to deploy it. In a traditional setup, -this can be quite a challenge, but with Juju you’ll find yourself -deploying, configuring, scaling, integrating, monitoring, etc., -your Flask application in no time. Let’s get started! - -In this tutorial we will build a rock and Kubernetes charm for a -Flask application using the charm SDK, so we can have a Flask -application up and running with Juju in about 90 minutes. We will -also configure this application to use asynchronous Gunicorn workers -to be able to serve to multiple users easily. - -.. note:: - - **rock**: An Ubuntu LTS-based OCI compatible - container image designed to meet security, stability, and - reliability requirements for cloud-native software. - - **charm**: A package consisting of YAML files + Python code that will - automate every aspect of an application's lifecycle so it can - be easily orchestrated with Juju. - - **Juju**: An orchestration engine for software - operators that enables the deployment, integration and lifecycle - management of applications using charms. - -**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 -- Create the charm -- Deploy the Flask application and expose via ingress -- Enable ``juju config flask-async-app webserver-worker-class=gevent`` -- Clean up environment - -.. hint:: - - Don't hesitate to get in touch on - `Matrix `_ or - `Discourse `_ (or follow the - "Edit this page on GitHub" on the bottom of - this document to comment directly on the document). - - -Set things up -============= - -.. include:: /reuse/tutorial/setup.rst - -Finally, create a new directory for this tutorial and go inside it: - -.. code-block:: bash - - mkdir flask-async-app - cd flask-async-app - -Create the Flask application -============================ - -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-async/requirements.txt - -In the same directory, copy and save the following into a text file -called ``app.py``: - -.. literalinclude:: code/flask-async/app.py - :language: python - -Run the Flask application locally -================================= - -Install ``python3-venv`` and create a virtual environment: - -.. literalinclude:: code/flask-async/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 may need a new terminal for this; if you are using Multipass -use ``multipass shell charm-dev`` to get another terminal: - -.. literalinclude:: code/flask-async/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!``. - -Test the long running endpoint by sending a request to ``/io``: - -.. literalinclude:: code/flask-async/task.yaml - :language: bash - :start-after: [docs:curl-flask-async] - :end-before: [docs:curl-flask-async-end] - :dedent: 2 - -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-async/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. Open the file in a text editor and check -that the ``name`` is ``flask-async-app``. Ensure that ``platforms`` -includes the architecture of your host. For example, if your host uses the -ARM architecture, include ``arm64`` in ``platforms``. Make sure to uncomment -the ``parts:`` line and the following lines to enable async workers: - -.. code-block:: - flask-framework/async-dependencies: - python-packages: - - gunicorn[gevent] - -.. note:: - - For this tutorial, we'll use the ``name`` "flask-async-app" and assume - you are on the ``amd64`` platform. Check the architecture of your system - using ``dpkg --print-architecture``. Choosing a different name or - running on a different platform will influence the names of the files - generated by Rockcraft. - -Pack the rock: - -.. literalinclude:: code/flask-async/task.yaml - :language: bash - :start-after: [docs:pack] - :end-before: [docs:pack-end] - :dedent: 2 - -.. note:: - - 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-async/task.yaml - :language: bash - :start-after: [docs:ls-rock] - :end-before: [docs:ls-rock-end] - :dedent: 2 - -.. note:: - - If you changed the ``name`` or ``version`` in ``rockcraft.yaml`` or are - not on an ``amd64`` platform, the name of the ``.rock`` file will be - different for you. - -The rock needs to be copied to the MicroK8s registry so that it can be -deployed in the Kubernetes cluster: - -.. literalinclude:: code/flask-async/task.yaml - :language: bash - :start-after: [docs:skopeo-copy] - :end-before: [docs:skopeo-copy-end] - :dedent: 2 - -.. seealso:: - - See more: `skopeo `_ - -Create the charm -================ - -Create a new directory for the charm and go inside it: - -.. literalinclude:: code/flask-async/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-async/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. -Pack the charm: - -.. literalinclude:: code/flask-async/task.yaml - :language: bash - :start-after: [docs:charm-pack] - :end-before: [docs:charm-pack-end] - :dedent: 2 - -.. note:: - - 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-async/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-async/task.yaml - :language: bash - :start-after: [docs:add-juju-model] - :end-before: [docs:add-juju-model-end] - :dedent: 2 - -.. warning:: - - 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. For example, for - the arm64 architecture, use - ``juju set-model-constraints -m flask-async-app arch=arm64``. - Check the architecture of your system using ``dpkg --print-architecture``. - -Now the Flask application can be deployed using `Juju `_: - -.. literalinclude:: code/flask-async/task.yaml - :language: bash - :start-after: [docs:deploy-juju-model] - :end-before: [docs:deploy-juju-model-end] - :dedent: 2 - -.. note:: - - 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`. - - 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: - -.. code-block:: - - Model Controller Cloud/Region Version SLA Timestamp - flask-async-app dev-controller microk8s/localhost 3.1.8 unsupported 17:04:11+10:00 - - App Version Status Scale Charm Channel Rev Address Exposed Message - flask-async-app active 1 flask-async-app 0 10.152.183.166 no - - Unit Workload Agent Address Ports Message - flask-async-app/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-async/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-async/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-async-app --resolve flask-async-app: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-async-app: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 enable async Gunicorn workers using a configuration option. We will -expect this configuration option to be available in the Flask app configuration -under the keyword ``webserver-worker-class``. Verify that the new configuration -has been added using -``juju config flask-async-app | grep -A 6 webserver-worker-class:`` 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. - -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 - -Now you can run -``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`` -in they will all return at the same time. - -.. note:: - - It might take a short time for the configuration to take effect. - -Clean up environment -==================== - -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! - -If you'd like to reset your working environment, you can run the following -in the root directory for the tutorial: - -.. literalinclude:: code/flask-async/task.yaml - :language: bash - :start-after: [docs:clean-environment] - :end-before: [docs:clean-environment-end] - :dedent: 2 - -If you created an instance using Multipass, you can also clean it up. -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 - -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 `_ - * - "What is...?" - - `SDK Reference docs `_ - * - "Why...?", "So what?" - - `SDK Explanation docs `_ - -------------------------- diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst deleted file mode 100644 index d14c1dd67..000000000 --- a/docs/tutorial/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -.. _tutorial: - -Tutorial -******** - -.. toctree:: - :maxdepth: 2 - - flask-async diff --git a/spread.yaml b/spread.yaml index 5254492b3..257d055aa 100644 --- a/spread.yaml +++ b/spread.yaml @@ -82,6 +82,8 @@ backends: workers: 1 - ubuntu-22.04-64: workers: 4 + - ubuntu-24.04-64: + workers: 4 prepare: | set -e @@ -129,10 +131,10 @@ prepare: | install_charmcraft suites: - docs/tutorial/code/: - summary: tests tutorial from the docs + docs/howto/code/: + summary: tests howto from the docs systems: - - ubuntu-22.04-64 + - ubuntu-24.04-64 tests/spread/commands/: summary: simple charmcraft commands tests/spread/charms/: From 0a83b2a08a0889075e36945f6bae1fa67cba5bc4 Mon Sep 17 00:00:00 2001 From: ali ugur Date: Mon, 9 Dec 2024 09:17:37 +0300 Subject: [PATCH 08/13] Chore(): Fix spread test --- docs/howto/code/flask-async/task.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/howto/code/flask-async/task.yaml b/docs/howto/code/flask-async/task.yaml index acc96ccec..2302b53be 100644 --- a/docs/howto/code/flask-async/task.yaml +++ b/docs/howto/code/flask-async/task.yaml @@ -151,7 +151,7 @@ execute: | NUM_REQUESTS=15 ASYNC_RESULT='TRUE' - echo "Firing $NUM_REQUESTS requests to $URL..." + echo "Firing $NUM_REQUESTS requests to http://flask-async-app/io..." overall_start_time=$(date +%s) From a69f8dba7322b82b550a0bf05d08181e274d7a9e Mon Sep 17 00:00:00 2001 From: ali ugur Date: Mon, 9 Dec 2024 10:26:06 +0300 Subject: [PATCH 09/13] Chore(): Change microk8s version in spread test --- docs/howto/code/flask-async/task.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/howto/code/flask-async/task.yaml b/docs/howto/code/flask-async/task.yaml index 2302b53be..00d0c979b 100644 --- a/docs/howto/code/flask-async/task.yaml +++ b/docs/howto/code/flask-async/task.yaml @@ -36,7 +36,7 @@ execute: | snap install --dangerous --classic rockcraft_* # snap refresh charmcraft --channel=latest/edge --amend - snap install microk8s --channel=1.32-strict/stable + snap install microk8s --channel=1.31-strict/stable snap install juju --channel=3/stable mkdir -p ~/.local/share From b8e39a6ac1a10d92cb367bb24c55def5def380c5 Mon Sep 17 00:00:00 2001 From: ali ugur Date: Wed, 11 Dec 2024 13:25:10 +0300 Subject: [PATCH 10/13] chore(doc): Applied comments --- docs/howto/code/flask-async/task.yaml | 2 +- docs/howto/flask-async.rst | 21 +++++++++++++-------- spread.yaml | 2 +- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/docs/howto/code/flask-async/task.yaml b/docs/howto/code/flask-async/task.yaml index 00d0c979b..ddf3de74b 100644 --- a/docs/howto/code/flask-async/task.yaml +++ b/docs/howto/code/flask-async/task.yaml @@ -124,7 +124,7 @@ execute: | # [docs:deploy-juju-model-end] # [docs:deploy-nginx] - juju deploy nginx-ingress-integrator --channel=latest/edge --revision 122 + juju deploy nginx-ingress-integrator --channel=latest/edge --base ubuntu@20.04 juju integrate nginx-ingress-integrator flask-async-app # [docs:deploy-nginx-end] diff --git a/docs/howto/flask-async.rst b/docs/howto/flask-async.rst index 4d0776056..9cb40a27a 100644 --- a/docs/howto/flask-async.rst +++ b/docs/howto/flask-async.rst @@ -2,9 +2,9 @@ How to write a Kubernetes charm for an Async Flask app ====================================================== -In this how to we will configure 12 Factor Flask application -to use asynchronous Gunicorn workers to be able to serve -to multiple users easily. +In this how-to guide we will configure a 12-factor Flask +application to use asynchronous Gunicorn workers to be +able to serve to multiple users easily. Make the rock async =================== @@ -24,11 +24,6 @@ has been added using ``juju config flask-async-app | grep -A 6 webserver-worker-class:`` 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. - The worker class can be changed using Juju: .. literalinclude:: code/flask-async/task.yaml @@ -43,6 +38,16 @@ 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`` in they will all return at the same time. +Output will be similar to following: + +.. code-block:: bash + + ok + ok + ok + ok + ok + .. note:: It might take a short time for the configuration to take effect. diff --git a/spread.yaml b/spread.yaml index 257d055aa..493306dca 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 4 --disk 50G --memory 8G --name "${instance_name}" "${multipass_image}" + multipass launch --cpus 4 --disk 40G --memory 8G --name "${instance_name}" "${multipass_image}" # Enable PasswordAuthentication for root over SSH. multipass exec "$instance_name" -- \ From 40eaa15dce3176a41415a525ee0bedd3f7ee992f Mon Sep 17 00:00:00 2001 From: ali ugur Date: Thu, 19 Dec 2024 10:43:33 +0300 Subject: [PATCH 11/13] chore(test): Update spread test to use rockcraft latest/edge --- docs/howto/code/flask-async/task.yaml | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/docs/howto/code/flask-async/task.yaml b/docs/howto/code/flask-async/task.yaml index ddf3de74b..c4088fa0c 100644 --- a/docs/howto/code/flask-async/task.yaml +++ b/docs/howto/code/flask-async/task.yaml @@ -22,20 +22,8 @@ execute: | unset CHARMCRAFT_REGISTRY_URL # Add setup instructions - # (Ran into issues in prepare section) - # snap install rockcraft --channel=latest/edge --classic - - # Install the latest rockcraft snap - # (This can be removed after the Rockcraft PR is merged) - # The PR: https://github.com/canonical/rockcraft/pull/747 - snap install snapcraft --channel=latest/edge --classic - # Download rockcraft async-workers branch and alithethird fork - git clone -b flask-django-extention-async-workers https://github.com/alithethird/rockcraft - cd rockcraft - snapcraft pack - snap install --dangerous --classic rockcraft_* - - # snap refresh charmcraft --channel=latest/edge --amend + snap install rockcraft --channel=latest/edge --classic + snap install microk8s --channel=1.31-strict/stable snap install juju --channel=3/stable From 0852a04994a91b0e069c8ddab54b7e1200ce6c81 Mon Sep 17 00:00:00 2001 From: ali ugur Date: Thu, 19 Dec 2024 13:18:43 +0300 Subject: [PATCH 12/13] chore(doc): Update howto --- docs/howto/flask-async.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/howto/flask-async.rst b/docs/howto/flask-async.rst index 9cb40a27a..5f1a6240a 100644 --- a/docs/howto/flask-async.rst +++ b/docs/howto/flask-async.rst @@ -36,7 +36,7 @@ Now you can run ``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`` -in they will all return at the same time. +and they will all return at the same time. Output will be similar to following: @@ -50,4 +50,5 @@ Output will be similar to following: .. note:: + When the configuration is done the charm will be in ``active`` state. It might take a short time for the configuration to take effect. From 361d1650d7df6517ff4dce9c2cdc9921d75f0da7 Mon Sep 17 00:00:00 2001 From: ali ugur Date: Fri, 20 Dec 2024 09:09:29 +0300 Subject: [PATCH 13/13] chore(doc): Update docs --- docs/howto/flask-async.rst | 51 +++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/docs/howto/flask-async.rst b/docs/howto/flask-async.rst index 5f1a6240a..eefc5408c 100644 --- a/docs/howto/flask-async.rst +++ b/docs/howto/flask-async.rst @@ -1,28 +1,36 @@ -====================================================== -How to write a Kubernetes charm for an Async Flask app +.. _write-a-kubernetes-charm-for-an-async-flask-app: + +How to write a Kubernetes charm for an async Flask app ====================================================== -In this how-to guide we will configure a 12-factor Flask +In this how-to guide you will configure a 12-factor Flask application to use asynchronous Gunicorn workers to be able to serve to multiple users easily. Make the rock async =================== -Before packing the rock make sure to put the following in ``requirements.txt`` +To make the rock async, make sure to put the following in its ``requirements.txt`` file: .. literalinclude:: code/flask-async/requirements.txt +Pack the rock using ``rockcraft pack`` and redeploy the charm with the new rock using +[``juju refresh``](https://juju.is/docs/juju/juju-refresh). + Configure the async application -=============================== +------------------------------- -Now let's enable async Gunicorn workers using a configuration option. We will +Now let's enable async Gunicorn workers. We will expect this configuration option to be available in the Flask app configuration -under the keyword ``webserver-worker-class``. Verify that the new configuration -has been added using -``juju config flask-async-app | grep -A 6 webserver-worker-class:`` which should -show the configuration option. +under the ``webserver-worker-class`` key. 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: @@ -32,15 +40,20 @@ The worker class can be changed using Juju: :end-before: [docs:config-async-end] :dedent: 2 -Now you can run -``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`` +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. -Output will be similar to following: +The results should arrive simultaneously and contain five instances of ``ok``: -.. code-block:: bash +.. terminal:: ok ok @@ -48,7 +61,5 @@ Output will be similar to following: ok ok -.. note:: - - When the configuration is done the charm will be in ``active`` state. - It might take a short time for the configuration to take effect. +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.