Skip to content

Commit

Permalink
docs: port Flask tutorial from Juju.is (canonical#1976)
Browse files Browse the repository at this point in the history
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 <michael.dubelko@gmail.com>
Co-authored-by: Alex Lowe <alex.lowe@canonical.com>
  • Loading branch information
3 people committed Dec 19, 2024
1 parent 3d9db19 commit 3e68507
Show file tree
Hide file tree
Showing 13 changed files with 940 additions and 2 deletions.
1 change: 0 additions & 1 deletion docs/common/craft-parts

This file was deleted.

70 changes: 70 additions & 0 deletions docs/reuse/tutorial/setup_stable.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
First, `install Multipass <https://multipass.run/docs/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
14 changes: 14 additions & 0 deletions docs/tutorial/code/flask/app.py
Original file line number Diff line number Diff line change
@@ -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()
16 changes: 16 additions & 0 deletions docs/tutorial/code/flask/greeting_app.py
Original file line number Diff line number Diff line change
@@ -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()
9 changes: 9 additions & 0 deletions docs/tutorial/code/flask/greeting_charmcraft.yaml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions docs/tutorial/code/flask/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Flask
220 changes: 220 additions & 0 deletions docs/tutorial/code/flask/task.yaml
Original file line number Diff line number Diff line change
@@ -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]
42 changes: 42 additions & 0 deletions docs/tutorial/code/flask/visitors_app.py
Original file line number Diff line number Diff line change
@@ -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()
6 changes: 6 additions & 0 deletions docs/tutorial/code/flask/visitors_charmcraft.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# requires snippet for Flask application with a database

requires:
postgresql:
interface: postgresql_client
optional: false
Loading

0 comments on commit 3e68507

Please sign in to comment.