From b1e45f82b7731035a685995a3e424cb405c1dd61 Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Tue, 15 Sep 2026 22:20:31 +0700 Subject: [PATCH 01/28] docs: migrate 12-factor tutorials to Canonical Kubernetes Replace MicroK8s with Canonical Kubernetes throughout the 12-factor app tutorials so they use CK8s exclusively, following up on the how-to guide migration. - Rework the shared tutorial setup (setup_stable.rst, setup_edge.rst) to install and bootstrap Canonical Kubernetes (k8s snap), enable local-storage and ingress, and register the cluster as a Juju cloud with 'juju add-k8s' before bootstrapping. - Replace the MicroK8s registry push (rockcraft.skopeo copy to localhost:32000) with a direct containerd image import ('ctr images import') and reference the image without the registry prefix in the six framework tutorials and their task.yaml tests. - Update prose and 'juju status' sample output accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/reuse/tutorial/setup_edge.rst | 43 +++++++++---------- docs/reuse/tutorial/setup_stable.rst | 43 +++++++++---------- docs/tutorial/code/django/task.yaml | 32 ++++++++------ docs/tutorial/code/expressjs/task.yaml | 32 ++++++++------ docs/tutorial/code/fastapi/task.yaml | 32 ++++++++------ docs/tutorial/code/flask/task.yaml | 32 ++++++++------ docs/tutorial/code/go/task.yaml | 32 ++++++++------ docs/tutorial/code/spring-boot/task.yaml | 32 ++++++++------ docs/tutorial/kubernetes-charm-django.rst | 21 ++++----- docs/tutorial/kubernetes-charm-express.rst | 21 ++++----- docs/tutorial/kubernetes-charm-fastapi.rst | 21 ++++----- docs/tutorial/kubernetes-charm-flask.rst | 21 ++++----- docs/tutorial/kubernetes-charm-go.rst | 21 ++++----- .../tutorial/kubernetes-charm-spring-boot.rst | 21 ++++----- 14 files changed, 214 insertions(+), 190 deletions(-) diff --git a/docs/reuse/tutorial/setup_edge.rst b/docs/reuse/tutorial/setup_edge.rst index ce386b5ac..b9c1fb3b4 100644 --- a/docs/reuse/tutorial/setup_edge.rst +++ b/docs/reuse/tutorial/setup_edge.rst @@ -35,8 +35,8 @@ In order to create the charm, you'll need to install Charmcraft: sudo snap install charmcraft --channel latest/edge --classic -Install LXD, MicroK8s, and Juju -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Install LXD, Canonical Kubernetes, and Juju +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LXD will be required for building the rock. Make sure it is installed: @@ -53,48 +53,45 @@ Initialize LXD: lxd init --auto -MicroK8s is required to deploy the |12FactorApp| application on Kubernetes. -Let's install MicroK8s using the ``1.34-strict/stable`` channel, add the current -user to the group, and activate the changes: +Canonical Kubernetes is required to deploy the |12FactorApp| application on +Kubernetes. Let's install Canonical Kubernetes using the +``1.34-classic/stable`` channel and bootstrap the cluster: .. code-block:: text - sudo snap install microk8s --channel 1.34-strict/stable - sudo adduser $USER snap_microk8s - newgrp snap_microk8s + sudo snap install k8s --classic --channel 1.34-classic/stable + sudo k8s bootstrap -Several MicroK8s addons are required for deployment. We need -``hostpath-storage`` so Juju can provide storage volumes, -``registry`` to host the OCI image for our app, and ``ingress`` -so that we can expose and access the app. +Canonical Kubernetes needs local storage so Juju can provide storage volumes, +and ingress so that we can expose and access the app. -Enable the necessary MicroK8s addons: +Enable the necessary features: .. code-block:: bash - sudo microk8s enable hostpath-storage - sudo microk8s enable registry - sudo microk8s enable ingress + sudo k8s enable local-storage + sudo k8s enable ingress -Check the status of MicroK8s: +Check the status of Canonical Kubernetes: .. code-block:: bash - sudo microk8s status --wait-ready + sudo k8s status --wait-ready -If successful, the terminal will output ``microk8s is running`` -along with a list of enabled and disabled add-ons. +If successful, the terminal will output ``status: ready`` +along with a list of enabled and disabled features. Juju is required to deploy the |12FactorApp| application. We'll install Juju using the ``3.6/stable`` channel. Since the snap is sandboxed, we'll also manually create a directory to contain -its files. Once Juju is ready, we initialize it by bootstrapping a -development controller: +its files. Canonical Kubernetes isn't automatically registered as a Juju +cloud, so we add it before bootstrapping a development controller: .. code-block:: text sudo snap install juju --channel 3.6/stable mkdir -p ~/.local/share - juju bootstrap microk8s dev-controller + sudo k8s config | juju add-k8s k8s-cloud --client + juju bootstrap k8s-cloud dev-controller It could take a few minutes to download the images. diff --git a/docs/reuse/tutorial/setup_stable.rst b/docs/reuse/tutorial/setup_stable.rst index 6bcc778d1..e264d22f6 100644 --- a/docs/reuse/tutorial/setup_stable.rst +++ b/docs/reuse/tutorial/setup_stable.rst @@ -30,8 +30,8 @@ In order to create the charm, you'll need to install Charmcraft: sudo snap install charmcraft --channel latest/stable --classic -Install LXD, MicroK8s, and Juju -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Install LXD, Canonical Kubernetes, and Juju +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LXD will be required for building the rock. Make sure it is installed: @@ -48,49 +48,46 @@ Initialize LXD: lxd init --auto -MicroK8s is required to deploy the |12FactorApp| application on Kubernetes. -Let's install MicroK8s using the ``1.34-strict/stable`` channel, add the current -user to the group, and activate the changes: +Canonical Kubernetes is required to deploy the |12FactorApp| application on +Kubernetes. Let's install Canonical Kubernetes using the +``1.34-classic/stable`` channel and bootstrap the cluster: .. code-block:: text - sudo snap install microk8s --channel 1.34-strict/stable - sudo adduser $USER snap_microk8s - newgrp snap_microk8s + sudo snap install k8s --classic --channel 1.34-classic/stable + sudo k8s bootstrap -Several MicroK8s addons are required for deployment. We need -``hostpath-storage`` so Juju can provide storage volumes, -``registry`` to host the OCI image for our app, and ``ingress`` -so that we can expose and access the app. +Canonical Kubernetes needs local storage so Juju can provide storage volumes, +and ingress so that we can expose and access the app. -Enable the necessary MicroK8s addons: +Enable the necessary features: .. code-block:: bash - sudo microk8s enable hostpath-storage - sudo microk8s enable registry - sudo microk8s enable ingress + sudo k8s enable local-storage + sudo k8s enable ingress -Check the status of MicroK8s: +Check the status of Canonical Kubernetes: .. code-block:: bash - sudo microk8s status --wait-ready + sudo k8s status --wait-ready -If successful, the terminal will output ``microk8s is running`` -along with a list of enabled and disabled add-ons. +If successful, the terminal will output ``status: ready`` +along with a list of enabled and disabled features. Juju is required to deploy the |12FactorApp| application. We'll install Juju using the ``3.6/stable`` channel. Since the snap is sandboxed, we'll also manually create a directory to contain -its files. Once Juju is ready, we initialize it by bootstrapping a -development controller: +its files. Canonical Kubernetes isn't automatically registered as a Juju +cloud, so we add it before bootstrapping a development controller: .. code-block:: text sudo snap install juju --channel 3.6/stable mkdir -p ~/.local/share - juju bootstrap microk8s dev-controller + sudo k8s config | juju add-k8s k8s-cloud --client + juju bootstrap k8s-cloud dev-controller It could take a few minutes to download the images. diff --git a/docs/tutorial/code/django/task.yaml b/docs/tutorial/code/django/task.yaml index 0857d61f0..ed3a1f59e 100644 --- a/docs/tutorial/code/django/task.yaml +++ b/docs/tutorial/code/django/task.yaml @@ -78,11 +78,11 @@ execute: | # [docs:pack-end] # [docs:skopeo-copy] - rockcraft.skopeo copy \ - --insecure-policy \ - --dest-tls-verify=false \ - oci-archive:django-hello-world_0.1_$(dpkg --print-architecture).rock \ - docker://localhost:32000/django-hello-world:0.1 + sudo /snap/k8s/current/bin/ctr \ + --address /run/containerd/containerd.sock \ + --namespace k8s.io images import \ + --base-name docker.io/library/django-hello-world \ + ./django-hello-world_0.1_$(dpkg --print-architecture).rock # [docs:skopeo-copy-end] # [docs:create-charm-dir] @@ -114,7 +114,7 @@ execute: | juju deploy \ ./django-hello-world_$(dpkg --print-architecture).charm \ django-hello-world --resource \ - django-app-image=localhost:32000/django-hello-world:0.1 + django-app-image=django-hello-world:0.1 # [docs:deploy-django-app-end] # [docs:deploy-postgres] @@ -176,16 +176,18 @@ execute: | # [docs:repack-update] rockcraft pack - rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ - oci-archive:django-hello-world_0.2_$(dpkg --print-architecture).rock \ - docker://localhost:32000/django-hello-world:0.2 + sudo /snap/k8s/current/bin/ctr \ + --address /run/containerd/containerd.sock \ + --namespace k8s.io images import \ + --base-name docker.io/library/django-hello-world \ + ./django-hello-world_0.2_$(dpkg --print-architecture).rock # [docs:repack-update-end] # [docs:refresh-deployment] cd charm juju refresh django-hello-world \ --path=./django-hello-world_$(dpkg --print-architecture).charm \ - --resource django-app-image=localhost:32000/django-hello-world:0.2 + --resource django-app-image=django-hello-world:0.2 # [docs:refresh-deployment-end] # [docs:disable-debug-mode] @@ -206,9 +208,11 @@ execute: | # [docs:repack-2nd-update] rockcraft pack - rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ - oci-archive:django-hello-world_0.3_$(dpkg --print-architecture).rock \ - docker://localhost:32000/django-hello-world:0.3 + sudo /snap/k8s/current/bin/ctr \ + --address /run/containerd/containerd.sock \ + --namespace k8s.io images import \ + --base-name docker.io/library/django-hello-world \ + ./django-hello-world_0.3_$(dpkg --print-architecture).rock # [docs:repack-2nd-update-end] cd charm @@ -220,7 +224,7 @@ execute: | charmcraft pack juju refresh django-hello-world \ --path=./django-hello-world_$(dpkg --print-architecture).charm \ - --resource django-app-image=localhost:32000/django-hello-world:0.3 + --resource django-app-image=django-hello-world:0.3 # [docs:repack-refresh-2nd-deployment-end] # Wait for django-hello-world to be active diff --git a/docs/tutorial/code/expressjs/task.yaml b/docs/tutorial/code/expressjs/task.yaml index 7362f0e08..d5b7a4c1e 100644 --- a/docs/tutorial/code/expressjs/task.yaml +++ b/docs/tutorial/code/expressjs/task.yaml @@ -77,11 +77,11 @@ execute: | # [docs:pack-end] # [docs:skopeo-copy] - rockcraft.skopeo copy \ - --insecure-policy \ - --dest-tls-verify=false \ - oci-archive:expressjs-hello-world_0.1_$(dpkg --print-architecture).rock \ - docker://localhost:32000/expressjs-hello-world:0.1 + sudo /snap/k8s/current/bin/ctr \ + --address /run/containerd/containerd.sock \ + --namespace k8s.io images import \ + --base-name docker.io/library/expressjs-hello-world \ + ./expressjs-hello-world_0.1_$(dpkg --print-architecture).rock # [docs:skopeo-copy-end] # [docs:create-charm-dir] @@ -113,7 +113,7 @@ execute: | juju deploy \ ./expressjs-hello-world_$(dpkg --print-architecture).charm \ expressjs-hello-world \ - --resource app-image=localhost:32000/expressjs-hello-world:0.1 + --resource app-image=expressjs-hello-world:0.1 # [docs:deploy-expressjs-app-end] # [docs:deploy-nginx] @@ -142,9 +142,11 @@ execute: | # [docs:docker-update] rockcraft pack - rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ - oci-archive:expressjs-hello-world_0.2_$(dpkg --print-architecture).rock \ - docker://localhost:32000/expressjs-hello-world:0.2 + sudo /snap/k8s/current/bin/ctr \ + --address /run/containerd/containerd.sock \ + --namespace k8s.io images import \ + --base-name docker.io/library/expressjs-hello-world \ + ./expressjs-hello-world_0.2_$(dpkg --print-architecture).rock # [docs:docker-update-end] cat greeting_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -154,7 +156,7 @@ execute: | charmcraft pack juju refresh expressjs-hello-world \ --path=./expressjs-hello-world_$(dpkg --print-architecture).charm \ - --resource app-image=localhost:32000/expressjs-hello-world:0.2 + --resource app-image=expressjs-hello-world:0.2 # [docs:refresh-deployment-end] # give Juju some time to refresh the app @@ -199,9 +201,11 @@ execute: | # [docs:docker-2nd-update] rockcraft pack - rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ - oci-archive:expressjs-hello-world_0.3_$(dpkg --print-architecture).rock \ - docker://localhost:32000/expressjs-hello-world:0.3 + sudo /snap/k8s/current/bin/ctr \ + --address /run/containerd/containerd.sock \ + --namespace k8s.io images import \ + --base-name docker.io/library/expressjs-hello-world \ + ./expressjs-hello-world_0.3_$(dpkg --print-architecture).rock # [docs:docker-2nd-update-end] cat visitors_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -211,7 +215,7 @@ execute: | charmcraft pack juju refresh expressjs-hello-world \ --path=./expressjs-hello-world_$(dpkg --print-architecture).charm \ - --resource app-image=localhost:32000/expressjs-hello-world:0.3 + --resource app-image=expressjs-hello-world:0.3 # [docs:refresh-2nd-deployment-end] # In the relation with postgresql-k8s test we may hit the issue diff --git a/docs/tutorial/code/fastapi/task.yaml b/docs/tutorial/code/fastapi/task.yaml index 69280d78e..b15e23518 100644 --- a/docs/tutorial/code/fastapi/task.yaml +++ b/docs/tutorial/code/fastapi/task.yaml @@ -58,11 +58,11 @@ execute: | # [docs:pack-end] # [docs:skopeo-copy] - rockcraft.skopeo copy \ - --insecure-policy \ - --dest-tls-verify=false \ - oci-archive:fastapi-hello-world_0.1_$(dpkg --print-architecture).rock \ - docker://localhost:32000/fastapi-hello-world:0.1 + sudo /snap/k8s/current/bin/ctr \ + --address /run/containerd/containerd.sock \ + --namespace k8s.io images import \ + --base-name docker.io/library/fastapi-hello-world \ + ./fastapi-hello-world_0.1_$(dpkg --print-architecture).rock # [docs:skopeo-copy-end] # [docs:create-charm-dir] @@ -94,7 +94,7 @@ execute: | juju deploy \ ./fastapi-hello-world_$(dpkg --print-architecture).charm \ fastapi-hello-world --resource \ - app-image=localhost:32000/fastapi-hello-world:0.1 + app-image=fastapi-hello-world:0.1 # [docs:deploy-fastapi-app-end] # [docs:deploy-nginx] @@ -124,9 +124,11 @@ execute: | # [docs:docker-update] ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack - rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ - oci-archive:fastapi-hello-world_0.2_$(dpkg --print-architecture).rock \ - docker://localhost:32000/fastapi-hello-world:0.2 + sudo /snap/k8s/current/bin/ctr \ + --address /run/containerd/containerd.sock \ + --namespace k8s.io images import \ + --base-name docker.io/library/fastapi-hello-world \ + ./fastapi-hello-world_0.2_$(dpkg --print-architecture).rock # [docs:docker-update-end] cat greeting_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -136,7 +138,7 @@ execute: | CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack juju refresh fastapi-hello-world \ --path=./fastapi-hello-world_$(dpkg --print-architecture).charm \ - --resource app-image=localhost:32000/fastapi-hello-world:0.2 + --resource app-image=fastapi-hello-world:0.2 # [docs:refresh-deployment-end] # give Juju some time to refresh the app @@ -162,9 +164,11 @@ execute: | # [docs:docker-2nd-update] ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack - rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ - oci-archive:fastapi-hello-world_0.3_$(dpkg --print-architecture).rock \ - docker://localhost:32000/fastapi-hello-world:0.3 + sudo /snap/k8s/current/bin/ctr \ + --address /run/containerd/containerd.sock \ + --namespace k8s.io images import \ + --base-name docker.io/library/fastapi-hello-world \ + ./fastapi-hello-world_0.3_$(dpkg --print-architecture).rock # [docs:docker-2nd-update-end] cat visitors_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -174,7 +178,7 @@ execute: | CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack juju refresh fastapi-hello-world \ --path=./fastapi-hello-world_$(dpkg --print-architecture).charm \ - --resource app-image=localhost:32000/fastapi-hello-world:0.3 + --resource app-image=fastapi-hello-world:0.3 # [docs:refresh-2nd-deployment-end] # In the relation with postgresql-k8s test we may hit, so we have to wait before integrating. diff --git a/docs/tutorial/code/flask/task.yaml b/docs/tutorial/code/flask/task.yaml index 25cc4dba8..0aafdd6bc 100644 --- a/docs/tutorial/code/flask/task.yaml +++ b/docs/tutorial/code/flask/task.yaml @@ -58,11 +58,11 @@ execute: | # [docs:pack-end] # [docs:skopeo-copy] - rockcraft.skopeo copy \ - --insecure-policy \ - --dest-tls-verify=false \ - oci-archive:flask-hello-world_0.1_$(dpkg --print-architecture).rock \ - docker://localhost:32000/flask-hello-world:0.1 + sudo /snap/k8s/current/bin/ctr \ + --address /run/containerd/containerd.sock \ + --namespace k8s.io images import \ + --base-name docker.io/library/flask-hello-world \ + ./flask-hello-world_0.1_$(dpkg --print-architecture).rock # [docs:skopeo-copy-end] # [docs:create-charm-dir] @@ -90,7 +90,7 @@ execute: | juju deploy \ ./flask-hello-world_$(dpkg --print-architecture).charm \ flask-hello-world --resource \ - flask-app-image=localhost:32000/flask-hello-world:0.1 + flask-app-image=flask-hello-world:0.1 # [docs:deploy-flask-app-end] # [docs:deploy-nginx] @@ -120,9 +120,11 @@ execute: | # [docs:docker-update] rockcraft pack - 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 + sudo /snap/k8s/current/bin/ctr \ + --address /run/containerd/containerd.sock \ + --namespace k8s.io images import \ + --base-name docker.io/library/flask-hello-world \ + ./flask-hello-world_0.2_$(dpkg --print-architecture).rock # [docs:docker-update-end] cat greeting_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -132,7 +134,7 @@ execute: | charmcraft pack juju refresh flask-hello-world \ --path=./flask-hello-world_$(dpkg --print-architecture).charm \ - --resource flask-app-image=localhost:32000/flask-hello-world:0.2 + --resource flask-app-image=flask-hello-world:0.2 # [docs:refresh-deployment-end] # give Juju some time to refresh the app @@ -158,9 +160,11 @@ execute: | # [docs:docker-2nd-update] rockcraft pack - 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 + sudo /snap/k8s/current/bin/ctr \ + --address /run/containerd/containerd.sock \ + --namespace k8s.io images import \ + --base-name docker.io/library/flask-hello-world \ + ./flask-hello-world_0.3_$(dpkg --print-architecture).rock # [docs:docker-2nd-update-end] cat visitors_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -170,7 +174,7 @@ execute: | charmcraft pack juju refresh flask-hello-world \ --path=./flask-hello-world_$(dpkg --print-architecture).charm \ - --resource flask-app-image=localhost:32000/flask-hello-world:0.3 + --resource flask-app-image=flask-hello-world:0.3 # [docs:refresh-2nd-deployment-end] # In the relation with postgresql-k8s test we may hit, so we have to wait before integrating. diff --git a/docs/tutorial/code/go/task.yaml b/docs/tutorial/code/go/task.yaml index 0acf9516c..22f3eb85f 100644 --- a/docs/tutorial/code/go/task.yaml +++ b/docs/tutorial/code/go/task.yaml @@ -66,11 +66,11 @@ execute: | # [docs:pack-end] # [docs:skopeo-copy] - rockcraft.skopeo copy \ - --insecure-policy \ - --dest-tls-verify=false \ - oci-archive:go-hello-world_0.1_$(dpkg --print-architecture).rock \ - docker://localhost:32000/go-hello-world:0.1 + sudo /snap/k8s/current/bin/ctr \ + --address /run/containerd/containerd.sock \ + --namespace k8s.io images import \ + --base-name docker.io/library/go-hello-world \ + ./go-hello-world_0.1_$(dpkg --print-architecture).rock # [docs:skopeo-copy-end] # [docs:create-charm-dir] @@ -102,7 +102,7 @@ execute: | juju deploy \ ./go-hello-world_$(dpkg --print-architecture).charm \ go-hello-world --resource \ - app-image=localhost:32000/go-hello-world:0.1 + app-image=go-hello-world:0.1 # [docs:deploy-go-app-end] # [docs:deploy-nginx] @@ -132,9 +132,11 @@ execute: | # [docs:docker-update] ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack - rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ - oci-archive:go-hello-world_0.2_$(dpkg --print-architecture).rock \ - docker://localhost:32000/go-hello-world:0.2 + sudo /snap/k8s/current/bin/ctr \ + --address /run/containerd/containerd.sock \ + --namespace k8s.io images import \ + --base-name docker.io/library/go-hello-world \ + ./go-hello-world_0.2_$(dpkg --print-architecture).rock # [docs:docker-update-end] cat greeting_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -144,7 +146,7 @@ execute: | CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack juju refresh go-hello-world \ --path=./go-hello-world_$(dpkg --print-architecture).charm \ - --resource app-image=localhost:32000/go-hello-world:0.2 + --resource app-image=go-hello-world:0.2 # [docs:refresh-deployment-end] # give Juju some time to refresh the app @@ -181,9 +183,11 @@ execute: | # [docs:docker-2nd-update] ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack - rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ - oci-archive:go-hello-world_0.3_$(dpkg --print-architecture).rock \ - docker://localhost:32000/go-hello-world:0.3 + sudo /snap/k8s/current/bin/ctr \ + --address /run/containerd/containerd.sock \ + --namespace k8s.io images import \ + --base-name docker.io/library/go-hello-world \ + ./go-hello-world_0.3_$(dpkg --print-architecture).rock # [docs:docker-2nd-update-end] cat visitors_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -193,7 +197,7 @@ execute: | CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack juju refresh go-hello-world \ --path=./go-hello-world_$(dpkg --print-architecture).charm \ - --resource app-image=localhost:32000/go-hello-world:0.3 + --resource app-image=go-hello-world:0.3 # [docs:refresh-2nd-deployment-end] # In the relation with postgresql-k8s test we may hit, so we have to wait before integrating. diff --git a/docs/tutorial/code/spring-boot/task.yaml b/docs/tutorial/code/spring-boot/task.yaml index b7ef1cc87..4177cfbc7 100644 --- a/docs/tutorial/code/spring-boot/task.yaml +++ b/docs/tutorial/code/spring-boot/task.yaml @@ -84,11 +84,11 @@ execute: | # [docs:ls-rock-end] # [docs:skopeo-copy] - rockcraft.skopeo copy \ - --insecure-policy \ - --dest-tls-verify=false \ - oci-archive:spring-boot-hello-world_0.1_$(dpkg --print-architecture).rock \ - docker://localhost:32000/spring-boot-hello-world:0.1 + sudo /snap/k8s/current/bin/ctr \ + --address /run/containerd/containerd.sock \ + --namespace k8s.io images import \ + --base-name docker.io/library/spring-boot-hello-world \ + ./spring-boot-hello-world_0.1_$(dpkg --print-architecture).rock # [docs:skopeo-copy-end] # [docs:create-charm-dir] @@ -120,7 +120,7 @@ execute: | juju deploy \ ./spring-boot-hello-world_$(dpkg --print-architecture).charm \ spring-boot-hello-world --resource \ - app-image=localhost:32000/spring-boot-hello-world:0.1 + app-image=spring-boot-hello-world:0.1 # [docs:deploy-app-end] # [docs:deploy-nginx] @@ -147,9 +147,11 @@ execute: | # [docs:skopeo-update] rockcraft pack - rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ - oci-archive:spring-boot-hello-world_0.2_$(dpkg --print-architecture).rock \ - docker://localhost:32000/spring-boot-hello-world:0.2 + sudo /snap/k8s/current/bin/ctr \ + --address /run/containerd/containerd.sock \ + --namespace k8s.io images import \ + --base-name docker.io/library/spring-boot-hello-world \ + ./spring-boot-hello-world_0.2_$(dpkg --print-architecture).rock # [docs:skopeo-update-end] cat $HOME/tmp/greeting_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -159,7 +161,7 @@ execute: | charmcraft pack juju refresh spring-boot-hello-world \ --path=./spring-boot-hello-world_$(dpkg --print-architecture).charm \ - --resource app-image=localhost:32000/spring-boot-hello-world:0.2 + --resource app-image=spring-boot-hello-world:0.2 # [docs:refresh-deployment-end] # give Juju some time to refresh the app @@ -206,9 +208,11 @@ execute: | # [docs:skopeo-2nd-update] rockcraft pack - rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ - oci-archive:spring-boot-hello-world_0.3_$(dpkg --print-architecture).rock \ - docker://localhost:32000/spring-boot-hello-world:0.3 + sudo /snap/k8s/current/bin/ctr \ + --address /run/containerd/containerd.sock \ + --namespace k8s.io images import \ + --base-name docker.io/library/spring-boot-hello-world \ + ./spring-boot-hello-world_0.3_$(dpkg --print-architecture).rock # [docs:skopeo-2nd-update-end] cat $HOME/tmp/visitors_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -218,7 +222,7 @@ execute: | charmcraft pack juju refresh spring-boot-hello-world \ --path=./spring-boot-hello-world_$(dpkg --print-architecture).charm \ - --resource app-image=localhost:32000/spring-boot-hello-world:0.3 + --resource app-image=spring-boot-hello-world:0.3 # [docs:refresh-2nd-deployment-end] # In the relation with postgresql-k8s test we may hit the issue diff --git a/docs/tutorial/kubernetes-charm-django.rst b/docs/tutorial/kubernetes-charm-django.rst index f0aae12c9..c70a63b37 100644 --- a/docs/tutorial/kubernetes-charm-django.rst +++ b/docs/tutorial/kubernetes-charm-django.rst @@ -339,9 +339,9 @@ terminal will respond with something similar to reflects your system's architecture. After the initial pack, subsequent rock packings are faster. -The rock needs to be copied to the MicroK8s registry. This registry acts as a -temporary Docker Hub, storing OCI archives so they can be downloaded and -deployed in the Kubernetes cluster. Copy the rock: +The rock needs to be imported into the Canonical Kubernetes cluster so +that the image is available to Juju. Import the rock directly into the +cluster's containerd image store: .. literalinclude:: code/django/task.yaml :language: bash @@ -351,12 +351,13 @@ deployed in the Kubernetes cluster. Copy the rock: This command contains the following pieces: -- ``--insecure-policy``: adopts a permissive policy that - removes the need for a dedicated policy file. -- ``--dest-tls-verify=false``: disables the need for HTTPS - and verify certificates while interacting with the MicroK8s registry. -- ``oci-archive``: specifies the rock we created for our Django app. -- ``docker``: specifies the name of the image in the MicroK8s registry. +- ``--address``: points to the containerd socket that + Canonical Kubernetes uses. +- ``--namespace k8s.io``: imports the image into the containerd + namespace that Kubernetes reads from. +- ``images import``: imports the rock we created for our Django app. +- ``--base-name``: sets the image name that the charm references. The + image tag is taken from the rock's version. Create the charm @@ -504,7 +505,7 @@ following output: juju status Model Controller Cloud/Region Version SLA Timestamp - django-hello-world dev-controller microk8s/localhost 3.6.2 unsupported 16:47:01+10:00 + django-hello-world dev-controller k8s-cloud 3.6.2 unsupported 16:47:01+10:00 App Version Status Scale Charm Channel Rev Address Exposed Message django-hello-world active 1 django-hello-world 3 10.152.183.126 no diff --git a/docs/tutorial/kubernetes-charm-express.rst b/docs/tutorial/kubernetes-charm-express.rst index 5b4b1eb08..343ed2332 100644 --- a/docs/tutorial/kubernetes-charm-express.rst +++ b/docs/tutorial/kubernetes-charm-express.rst @@ -218,9 +218,9 @@ the terminal will respond with something similar to The file name reflects your system's architecture. After the initial pack, subsequent rock packings are faster. -The rock needs to be copied to the MicroK8s registry, which stores OCI -archives so they can be downloaded and deployed in the Kubernetes cluster. -Copy the rock: +The rock needs to be imported into the Canonical Kubernetes cluster so +that the image is available to Juju. Import the rock directly into the +cluster's containerd image store: .. literalinclude:: code/expressjs/task.yaml :language: bash @@ -230,12 +230,13 @@ Copy the rock: This command contains the following pieces: -- ``--insecure-policy``: adopts a permissive policy that - removes the need for a dedicated policy file. -- ``--dest-tls-verify=false``: disables the need for HTTPS - and verify certificates while interacting with the MicroK8s registry. -- ``oci-archive``: specifies the rock we created for our Express app. -- ``docker``: specifies the name of the image in the MicroK8s registry. +- ``--address``: points to the containerd socket that + Canonical Kubernetes uses. +- ``--namespace k8s.io``: imports the image into the containerd + namespace that Kubernetes reads from. +- ``images import``: imports the rock we created for our Express app. +- ``--base-name``: sets the image name that the charm references. The + image tag is taken from the rock's version. Create the charm ---------------- @@ -383,7 +384,7 @@ following output: juju status Model Controller Cloud/Region Version SLA Timestamp - expressjs-hello-world dev-controller microk8s/localhost 3.6.5 unsupported 12:24:51+03:00 + expressjs-hello-world dev-controller k8s-cloud 3.6.5 unsupported 12:24:51+03:00 App Version Status Scale Charm Channel Rev Address Exposed Message expressjs-hello-world active 1 expressjs-hello-world 0 10.152.183.38 no diff --git a/docs/tutorial/kubernetes-charm-fastapi.rst b/docs/tutorial/kubernetes-charm-fastapi.rst index 37402a3b5..54a211f18 100644 --- a/docs/tutorial/kubernetes-charm-fastapi.rst +++ b/docs/tutorial/kubernetes-charm-fastapi.rst @@ -234,9 +234,9 @@ the terminal will respond with something similar to reflects your system's architecture. After the initial pack, subsequent rock packings are faster. -The rock needs to be copied to the MicroK8s registry. This registry acts as a -temporary Docker Hub, storing OCI archives so they can be downloaded and -deployed in the Kubernetes cluster. Copy the rock: +The rock needs to be imported into the Canonical Kubernetes cluster so +that the image is available to Juju. Import the rock directly into the +cluster's containerd image store: .. literalinclude:: code/fastapi/task.yaml :language: bash @@ -246,12 +246,13 @@ deployed in the Kubernetes cluster. Copy the rock: This command contains the following pieces: -- ``--insecure-policy``: adopts a permissive policy that - removes the need for a dedicated policy file. -- ``--dest-tls-verify=false``: disables the need for HTTPS - and verify certificates while interacting with the MicroK8s registry. -- ``oci-archive``: specifies the rock we created for our FastAPI app. -- ``docker``: specifies the name of the image in the MicroK8s registry. +- ``--address``: points to the containerd socket that + Canonical Kubernetes uses. +- ``--namespace k8s.io``: imports the image into the containerd + namespace that Kubernetes reads from. +- ``images import``: imports the rock we created for our FastAPI app. +- ``--base-name``: sets the image name that the charm references. The + image tag is taken from the rock's version. Create the charm @@ -416,7 +417,7 @@ output: juju status Model Controller Cloud/Region Version SLA Timestamp - fastapi-hello-world dev-controller microk8s/localhost 3.6.2 unsupported 13:45:18+10:00 + fastapi-hello-world dev-controller k8s-cloud 3.6.2 unsupported 13:45:18+10:00 App Version Status Scale Charm Channel Rev Address Exposed Message fastapi-hello-world active 1 fastapi-hello-world 0 10.152.183.53 no diff --git a/docs/tutorial/kubernetes-charm-flask.rst b/docs/tutorial/kubernetes-charm-flask.rst index 9bcf92862..eb1143fa4 100644 --- a/docs/tutorial/kubernetes-charm-flask.rst +++ b/docs/tutorial/kubernetes-charm-flask.rst @@ -225,9 +225,9 @@ the terminal will respond with something similar to reflects your system's architecture. After the initial pack, subsequent rock packings are faster. -The rock needs to be copied to the MicroK8s registry. This registry acts as a -temporary Docker Hub, storing OCI archives so they can be downloaded and -deployed in the Kubernetes cluster. Copy the rock: +The rock needs to be imported into the Canonical Kubernetes cluster so +that the image is available to Juju. Import the rock directly into the +cluster's containerd image store: .. literalinclude:: code/flask/task.yaml :language: bash @@ -237,12 +237,13 @@ deployed in the Kubernetes cluster. Copy the rock: This command contains the following pieces: -- ``--insecure-policy``: adopts a permissive policy that - removes the need for a dedicated policy file. -- ``--dest-tls-verify=false``: disables the need for HTTPS - and verify certificates while interacting with the MicroK8s registry. -- ``oci-archive``: specifies the rock we created for our Flask app. -- ``docker``: specifies the name of the image in the MicroK8s registry. +- ``--address``: points to the containerd socket that + Canonical Kubernetes uses. +- ``--namespace k8s.io``: imports the image into the containerd + namespace that Kubernetes reads from. +- ``images import``: imports the rock we created for our Flask app. +- ``--base-name``: sets the image name that the charm references. The + image tag is taken from the rock's version. Create the charm @@ -362,7 +363,7 @@ following output: juju status Model Controller Cloud/Region Version SLA Timestamp - flask-hello-world dev-controller microk8s/localhost 3.6.2 unsupported 17:04:11+10:00 + flask-hello-world dev-controller k8s-cloud 3.6.2 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 diff --git a/docs/tutorial/kubernetes-charm-go.rst b/docs/tutorial/kubernetes-charm-go.rst index 003e53707..fd96a1d9d 100644 --- a/docs/tutorial/kubernetes-charm-go.rst +++ b/docs/tutorial/kubernetes-charm-go.rst @@ -221,9 +221,9 @@ the terminal will respond with something similar to reflects your system's architecture. After the initial pack, subsequent rock packings are faster. -The rock needs to be copied to the MicroK8s registry. This registry acts as a -temporary Docker Hub, storing OCI archives so they can be downloaded and -deployed in the Kubernetes cluster. Copy the rock: +The rock needs to be imported into the Canonical Kubernetes cluster so +that the image is available to Juju. Import the rock directly into the +cluster's containerd image store: .. literalinclude:: code/go/task.yaml :language: bash @@ -233,12 +233,13 @@ deployed in the Kubernetes cluster. Copy the rock: This command contains the following pieces: -- ``--insecure-policy``: adopts a permissive policy that - removes the need for a dedicated policy file. -- ``--dest-tls-verify=false``: disables the need for HTTPS - and verify certificates while interacting with the MicroK8s registry. -- ``oci-archive``: specifies the rock we created for our Go app. -- ``docker``: specifies the name of the image in the MicroK8s registry. +- ``--address``: points to the containerd socket that + Canonical Kubernetes uses. +- ``--namespace k8s.io``: imports the image into the containerd + namespace that Kubernetes reads from. +- ``images import``: imports the rock we created for our Go app. +- ``--base-name``: sets the image name that the charm references. The + image tag is taken from the rock's version. Create the charm @@ -399,7 +400,7 @@ following output: juju status Model Controller Cloud/Region Version SLA Timestamp - go-hello-world dev-controller microk8s/localhost 3.6.2 unsupported 14:35:07+02:00 + go-hello-world dev-controller k8s-cloud 3.6.2 unsupported 14:35:07+02:00 App Version Status Scale Charm Channel Rev Address Exposed Message go-hello-world active 1 go-hello-world 0 10.152.183.229 no diff --git a/docs/tutorial/kubernetes-charm-spring-boot.rst b/docs/tutorial/kubernetes-charm-spring-boot.rst index 9a96ee676..49e7ae3b0 100644 --- a/docs/tutorial/kubernetes-charm-spring-boot.rst +++ b/docs/tutorial/kubernetes-charm-spring-boot.rst @@ -224,9 +224,9 @@ the terminal will respond with something similar to reflects your system's architecture. After the initial pack, subsequent rock packings are faster. -The rock needs to be copied to the MicroK8s registry. This registry acts as a -temporary Docker Hub, storing OCI archives so they can be downloaded and -deployed in the Kubernetes cluster. Copy the rock: +The rock needs to be imported into the Canonical Kubernetes cluster so +that the image is available to Juju. Import the rock directly into the +cluster's containerd image store: .. literalinclude:: code/spring-boot/task.yaml :language: bash @@ -236,12 +236,13 @@ deployed in the Kubernetes cluster. Copy the rock: This command contains the following pieces: -- ``--insecure-policy``: adopts a permissive policy that - removes the need for a dedicated policy file. -- ``--dest-tls-verify=false``: disables the need for HTTPS - and verify certificates while interacting with the MicroK8s registry. -- ``oci-archive``: specifies the rock we created for our Spring Boot app. -- ``docker``: specifies the name of the image in the MicroK8s registry. +- ``--address``: points to the containerd socket that + Canonical Kubernetes uses. +- ``--namespace k8s.io``: imports the image into the containerd + namespace that Kubernetes reads from. +- ``images import``: imports the rock we created for our Spring Boot app. +- ``--base-name``: sets the image name that the charm references. The + image tag is taken from the rock's version. Create the charm ---------------- @@ -392,7 +393,7 @@ following output: juju status Model Controller Cloud/Region Version SLA Timestamp - spring-boot-hello-world dev-controller microk8s/localhost 3.6.6 unsupported 16:22:04+02:00 + spring-boot-hello-world dev-controller k8s-cloud 3.6.6 unsupported 16:22:04+02:00 App Version Status Scale Charm Channel Rev Address Exposed Message spring-boot-hello-world active 1 spring-boot-hello-world 0 10.152.183.157 no From d260d33984e2dca9153e37b4213f5d5232c65b9f Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Tue, 15 Sep 2026 22:27:21 +0700 Subject: [PATCH 02/28] docs: use Canonical K8s exclusively in web app how-to guides Remove the MicroK8s options from the manage-web-app-charms how-to guides so they use Canonical Kubernetes exclusively, matching the tutorial migration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../integrate-web-app-charm.rst | 21 +-- .../use-web-app-charm.rst | 145 +++--------------- 2 files changed, 28 insertions(+), 138 deletions(-) diff --git a/docs/howto/manage-web-app-charms/integrate-web-app-charm.rst b/docs/howto/manage-web-app-charms/integrate-web-app-charm.rst index ed312166b..62526eab0 100644 --- a/docs/howto/manage-web-app-charms/integrate-web-app-charm.rst +++ b/docs/howto/manage-web-app-charms/integrate-web-app-charm.rst @@ -104,24 +104,11 @@ COS relies on the Traefik ingress to expose, for example, Grafana. Traefik requires a load balancer to be enabled with an IP range. Provide the IP range and enable the load balancer with: -.. tab-set:: - - .. tab-item:: MicroK8s - :sync: microk8s - - .. code-block:: bash - - IPADDR=$(ip -4 -j route get 2.2.2.2 | jq -r '.[] | .prefsrc') - microk8s enable metallb:$IPADDR-$IPADDR - - .. tab-item:: Canonical K8s - :sync: canonical-k8s - - .. code-block:: bash +.. code-block:: bash - IPADDR=$(ip -4 -j route get 2.2.2.2 | jq -r '.[] | .prefsrc') - sudo k8s set load-balancer.l2-mode=true load-balancer.cidrs=$IPADDR-$IPADDR - sudo k8s enable load-balancer + IPADDR=$(ip -4 -j route get 2.2.2.2 | jq -r '.[] | .prefsrc') + sudo k8s set load-balancer.l2-mode=true load-balancer.cidrs=$IPADDR-$IPADDR + sudo k8s enable load-balancer Deploy and integrate observability to the 12-factor app with: diff --git a/docs/howto/manage-web-app-charms/use-web-app-charm.rst b/docs/howto/manage-web-app-charms/use-web-app-charm.rst index b60548743..985f737e3 100644 --- a/docs/howto/manage-web-app-charms/use-web-app-charm.rst +++ b/docs/howto/manage-web-app-charms/use-web-app-charm.rst @@ -431,42 +431,18 @@ Check Kubernetes pod services and logs Check the currently deployed Kubernetes resources in the ````, which is the same as the Juju model name: -.. tab-set:: - - .. tab-item:: MicroK8s - :sync: microk8s - - .. code-block:: bash - - microk8s.kubectl get all -n - - .. tab-item:: Canonical K8s - :sync: canonical-k8s - - .. code-block:: bash +.. code-block:: bash - sudo k8s kubectl get all -n + sudo k8s kubectl get all -n This command outputs a list of all the Kubernetes resources in the web app's Juju model. Check the logs for a specific Kubernetes pod: -.. tab-set:: - - .. tab-item:: MicroK8s - :sync: microk8s - - .. code-block:: bash - - microk8s kubectl logs -n - - .. tab-item:: Canonical K8s - :sync: canonical-k8s - - .. code-block:: bash +.. code-block:: bash - sudo k8s kubectl logs -n + sudo k8s kubectl logs -n This command outputs the logs of the sidecar container pod. To fetch logs specific to the workload of the web app, you need to specify the container @@ -484,42 +460,42 @@ name of the web app with the ``-c`` option. .. code-block:: bash - microk8s kubectl logs -n -c django-app + sudo k8s kubectl logs -n -c django-app .. tab-item:: Express :sync: express .. code-block:: bash - microk8s kubectl logs -n -c app + sudo k8s kubectl logs -n -c app .. tab-item:: FastAPI :sync: fastapi .. code-block:: bash - microk8s kubectl logs -n -c app + sudo k8s kubectl logs -n -c app .. tab-item:: Flask :sync: flask .. code-block:: bash - microk8s kubectl logs -n -c flask-app + sudo k8s kubectl logs -n -c flask-app .. tab-item:: Go :sync: go .. code-block:: bash - microk8s kubectl logs -n -c app + sudo k8s kubectl logs -n -c app .. tab-item:: Spring Boot :sync: spring-boot .. code-block:: bash - microk8s kubectl logs -n -c app + sudo k8s kubectl logs -n -c app .. tab-item:: Ubuntu 26.04 and higher :sync: base-26-plus @@ -529,122 +505,49 @@ name of the web app with the ``-c`` option. .. tab-item:: Django :sync: django - .. tab-set:: - - .. tab-item:: MicroK8s - :sync: microk8s - - .. code-block:: bash - - microk8s kubectl logs -n -c app - - .. tab-item:: Canonical K8s - :sync: canonical-k8s - - .. code-block:: bash + .. code-block:: bash - sudo k8s kubectl logs -n -c app + sudo k8s kubectl logs -n -c app .. tab-item:: Express :sync: express - .. tab-set:: - - .. tab-item:: MicroK8s - :sync: microk8s - - .. code-block:: bash - - microk8s kubectl logs -n -c app - - .. tab-item:: Canonical K8s - :sync: canonical-k8s - - .. code-block:: bash + .. code-block:: bash - sudo k8s kubectl logs -n -c app + sudo k8s kubectl logs -n -c app .. tab-item:: FastAPI :sync: fastapi - .. tab-set:: - - .. tab-item:: MicroK8s - :sync: microk8s - - .. code-block:: bash - - microk8s kubectl logs -n -c app - - .. tab-item:: Canonical K8s - :sync: canonical-k8s - - .. code-block:: bash + .. code-block:: bash - sudo k8s kubectl logs -n -c app + sudo k8s kubectl logs -n -c app .. tab-item:: Flask :sync: flask - .. tab-set:: - - .. tab-item:: MicroK8s - :sync: microk8s - - .. code-block:: bash - - microk8s kubectl logs -n -c app - - .. tab-item:: Canonical K8s - :sync: canonical-k8s - - .. code-block:: bash + .. code-block:: bash - sudo k8s kubectl logs -n -c app + sudo k8s kubectl logs -n -c app .. tab-item:: Go :sync: go - .. tab-set:: - - .. tab-item:: MicroK8s - :sync: microk8s - - .. code-block:: bash - - microk8s kubectl logs -n -c app - - .. tab-item:: Canonical K8s - :sync: canonical-k8s - - .. code-block:: bash + .. code-block:: bash - sudo k8s kubectl logs -n -c app + sudo k8s kubectl logs -n -c app .. tab-item:: Spring Boot :sync: spring-boot - .. tab-set:: - - .. tab-item:: MicroK8s - :sync: microk8s - - .. code-block:: bash - - microk8s kubectl logs -n -c app - - .. tab-item:: Canonical K8s - :sync: canonical-k8s - - .. code-block:: bash + .. code-block:: bash - sudo k8s kubectl logs -n -c app + sudo k8s kubectl logs -n -c app .. seealso:: - - `MicroK8s | Troubleshooting `_ - - `Canonical Kubernetes | Troubleshoot - `_ + `Canonical Kubernetes | Troubleshoot + `_ Check Juju logs ~~~~~~~~~~~~~~~ From 49c61e382e4f635e3db9434b06dd4560e59c60d7 Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Tue, 15 Sep 2026 23:52:40 +0700 Subject: [PATCH 03/28] tests: provision Canonical K8s for the tutorial spread suite The tutorial task.yaml files now import images with the k8s snap's containerd (`ctr`) and deploy to a `k8s-cloud` Juju cloud, but the shared docsuite only provisioned MicroK8s. Give docs/tutorial/code/ a dedicated suite that installs the k8s snap, configures the docker.io containerd mirror, bootstraps Canonical Kubernetes, enables local-storage, ingress and the load balancer, and registers k8s-cloud with Juju. The how-to code suite stays on MicroK8s. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- spread.yaml | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/spread.yaml b/spread.yaml index 6ce41c4f6..820638305 100644 --- a/spread.yaml +++ b/spread.yaml @@ -281,8 +281,69 @@ suites: --destroy-storage --no-prompt --no-wait dev-controller docs/tutorial/code/: - <<: *docsuite summary: tests tutorial from the docs + systems: + - ubuntu-24.04-64 + manual: true + prepare: | + juju_channel=3.6/stable + k8s_channel=1.34-classic/stable + + lxc network set lxdbr0 ipv6.address none + mkdir -p ~/.local/share # Workaround for Juju not being able to create the directory + + #shellcheck source=tests/spread/tools/prepare.sh + . "/charmcraft/tests/spread/tools/prepare.sh" + + refresh_or_install_snap juju "$juju_channel" + refresh_or_install_snap k8s "$k8s_channel" --classic + refresh_or_install_snap rockcraft latest/edge --classic + + # Canonical Kubernetes runs its own containerd using the standard host paths, + # so the docker.io pull-through mirror lives under /etc/containerd/hosts.d. + # It must be written before bootstrap so containerd picks it up on startup. + if [[ -v CONTAINER_REGISTRY_URL ]]; then + CONTAINER_REGISTRY_HOSTPORT=$(echo "$CONTAINER_REGISTRY_URL" | awk -F/ '{print $3}') + mkdir -p /etc/containerd/hosts.d/docker.io + echo "server = \"$CONTAINER_REGISTRY_URL\"" > /etc/containerd/hosts.d/docker.io/hosts.toml + echo "[host.\"$CONTAINER_REGISTRY_HOSTPORT\"]" >> /etc/containerd/hosts.d/docker.io/hosts.toml + echo 'capabilities = ["pull", "resolve"]' >> /etc/containerd/hosts.d/docker.io/hosts.toml + fi + + k8s bootstrap + k8s status --wait-ready --timeout 5m + + k8s enable local-storage + k8s enable ingress + + # Unlike the MicroK8s ingress addon (which binds the node's host ports), the + # Canonical Kubernetes ingress is exposed through a LoadBalancer service, so a + # load balancer with an address pool is required for the ingress to be reachable. + IPADDR=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') + k8s set load-balancer.l2-mode=true load-balancer.cidrs="$IPADDR-$IPADDR" + k8s enable load-balancer + + k8s status --wait-ready --timeout 5m + k8s kubectl wait --for=condition=Ready pods --all -n kube-system --timeout=10m + + # The tutorial reaches the app at 127.0.0.1; redirect loopback :80/:443 to the + # load balancer address so the documented commands work in the test suite. + iptables -t nat -A OUTPUT -p tcp -d 127.0.0.1 --dport 80 -j DNAT --to-destination "$IPADDR:80" + iptables -t nat -A OUTPUT -p tcp -d 127.0.0.1 --dport 443 -j DNAT --to-destination "$IPADDR:443" + + # Canonical Kubernetes isn't autodetected by Juju, so register it as a cloud. + k8s config | juju add-k8s k8s-cloud --client + juju bootstrap k8s-cloud dev-controller --config bootstrap-timeout=500 + + echo 'connect-timeout = 10' >> ~/.curlrc + echo 'max-time = 20' >> ~/.curlrc + debug-each: | + k8s kubectl get all,ingress -A + juju status --relations + juju debug-log --no-tail --limit 1000 + restore: | + juju destroy-controller --force --destroy-all-models \ + --destroy-storage --no-prompt --no-wait dev-controller tests/spread/commands/: summary: simple charmcraft commands From ce03d3638aa32ac3da0f4ef36c7ecae039d5cf63 Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Thu, 1 Oct 2026 11:56:12 +0700 Subject: [PATCH 04/28] docs: use a local registry for CK8s tutorials Address PR review feedback: avoid driving containerd directly (`ctr images import`) in the 12-factor tutorials. Instead deploy a small local OCI registry at localhost:5000 and push rocks to it with `rockcraft.skopeo copy`, mirroring the previous MicroK8s registry flow. - setup_stable.rst / setup_edge.rst: add a "Set up a local registry" section (distribution registry binary as a systemd service + containerd hosts.d config for localhost:5000). - tutorial task.yaml (6 frameworks): push rocks via skopeo and reference images as localhost:5000/:. - tutorial .rst (6 frameworks): update prose/bullets to the registry and skopeo workflow. - spread.yaml tutorial suite: deploy the registry + hosts.d config in place of the docker.io pull-through mirror block. - Trim heading underlines in the setup files (review nitpick). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/reuse/tutorial/setup_edge.rst | 51 ++++++++++++++++++- docs/reuse/tutorial/setup_stable.rst | 51 ++++++++++++++++++- docs/tutorial/code/django/task.yaml | 30 +++++------ docs/tutorial/code/expressjs/task.yaml | 30 +++++------ docs/tutorial/code/fastapi/task.yaml | 30 +++++------ docs/tutorial/code/flask/task.yaml | 30 +++++------ docs/tutorial/code/go/task.yaml | 30 +++++------ docs/tutorial/code/spring-boot/task.yaml | 30 +++++------ docs/tutorial/kubernetes-charm-django.rst | 19 ++++--- docs/tutorial/kubernetes-charm-express.rst | 19 ++++--- docs/tutorial/kubernetes-charm-fastapi.rst | 19 ++++--- docs/tutorial/kubernetes-charm-flask.rst | 19 ++++--- docs/tutorial/kubernetes-charm-go.rst | 19 ++++--- .../tutorial/kubernetes-charm-spring-boot.rst | 19 ++++--- spread.yaml | 51 +++++++++++++++---- 15 files changed, 267 insertions(+), 180 deletions(-) diff --git a/docs/reuse/tutorial/setup_edge.rst b/docs/reuse/tutorial/setup_edge.rst index b9c1fb3b4..de4cf5633 100644 --- a/docs/reuse/tutorial/setup_edge.rst +++ b/docs/reuse/tutorial/setup_edge.rst @@ -36,7 +36,7 @@ In order to create the charm, you'll need to install Charmcraft: sudo snap install charmcraft --channel latest/edge --classic Install LXD, Canonical Kubernetes, and Juju -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LXD will be required for building the rock. Make sure it is installed: @@ -81,6 +81,55 @@ Check the status of Canonical Kubernetes: If successful, the terminal will output ``status: ready`` along with a list of enabled and disabled features. +Set up a local registry +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Canonical Kubernetes pulls application images from an OCI registry. Unlike +MicroK8s, it doesn't bundle a registry, so we run a small local one. Download +the registry and start it as a service: + +.. code-block:: bash + + ARCH=$(dpkg --print-architecture) + sudo curl -sSL -o /tmp/registry.tar.gz \ + "https://github.com/distribution/distribution/releases/download/v2.8.3/registry_2.8.3_linux_${ARCH}.tar.gz" + sudo tar -xzf /tmp/registry.tar.gz -C /usr/local/bin registry + sudo mkdir -p /etc/distribution + sudo tee /etc/distribution/config.yml > /dev/null <<'EOF' + version: 0.1 + storage: + filesystem: + rootdirectory: /var/lib/registry + http: + addr: :5000 + EOF + sudo tee /etc/systemd/system/registry.service > /dev/null <<'EOF' + [Unit] + Description=OCI registry + After=network.target + + [Service] + ExecStart=/usr/local/bin/registry serve /etc/distribution/config.yml + Restart=always + + [Install] + WantedBy=multi-user.target + EOF + sudo systemctl enable --now registry + +Finally, let Canonical Kubernetes pull from this registry over plain HTTP by +adding a containerd hosts configuration: + +.. code-block:: bash + + sudo mkdir -p /etc/containerd/hosts.d/localhost:5000 + sudo tee /etc/containerd/hosts.d/localhost:5000/hosts.toml > /dev/null <<'EOF' + server = "http://localhost:5000" + + [host."http://localhost:5000"] + capabilities = ["pull", "resolve"] + EOF + Juju is required to deploy the |12FactorApp| application. We'll install Juju using the ``3.6/stable`` channel. Since the snap is sandboxed, we'll also manually create a directory to contain diff --git a/docs/reuse/tutorial/setup_stable.rst b/docs/reuse/tutorial/setup_stable.rst index e264d22f6..00aa7d933 100644 --- a/docs/reuse/tutorial/setup_stable.rst +++ b/docs/reuse/tutorial/setup_stable.rst @@ -31,7 +31,7 @@ In order to create the charm, you'll need to install Charmcraft: sudo snap install charmcraft --channel latest/stable --classic Install LXD, Canonical Kubernetes, and Juju -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LXD will be required for building the rock. Make sure it is installed: @@ -77,6 +77,55 @@ Check the status of Canonical Kubernetes: If successful, the terminal will output ``status: ready`` along with a list of enabled and disabled features. +Set up a local registry +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Canonical Kubernetes pulls application images from an OCI registry. Unlike +MicroK8s, it doesn't bundle a registry, so we run a small local one. Download +the registry and start it as a service: + +.. code-block:: bash + + ARCH=$(dpkg --print-architecture) + sudo curl -sSL -o /tmp/registry.tar.gz \ + "https://github.com/distribution/distribution/releases/download/v2.8.3/registry_2.8.3_linux_${ARCH}.tar.gz" + sudo tar -xzf /tmp/registry.tar.gz -C /usr/local/bin registry + sudo mkdir -p /etc/distribution + sudo tee /etc/distribution/config.yml > /dev/null <<'EOF' + version: 0.1 + storage: + filesystem: + rootdirectory: /var/lib/registry + http: + addr: :5000 + EOF + sudo tee /etc/systemd/system/registry.service > /dev/null <<'EOF' + [Unit] + Description=OCI registry + After=network.target + + [Service] + ExecStart=/usr/local/bin/registry serve /etc/distribution/config.yml + Restart=always + + [Install] + WantedBy=multi-user.target + EOF + sudo systemctl enable --now registry + +Finally, let Canonical Kubernetes pull from this registry over plain HTTP by +adding a containerd hosts configuration: + +.. code-block:: bash + + sudo mkdir -p /etc/containerd/hosts.d/localhost:5000 + sudo tee /etc/containerd/hosts.d/localhost:5000/hosts.toml > /dev/null <<'EOF' + server = "http://localhost:5000" + + [host."http://localhost:5000"] + capabilities = ["pull", "resolve"] + EOF + Juju is required to deploy the |12FactorApp| application. We'll install Juju using the ``3.6/stable`` channel. Since the snap is sandboxed, we'll also manually create a directory to contain diff --git a/docs/tutorial/code/django/task.yaml b/docs/tutorial/code/django/task.yaml index ed3a1f59e..d98cc7c95 100644 --- a/docs/tutorial/code/django/task.yaml +++ b/docs/tutorial/code/django/task.yaml @@ -78,11 +78,9 @@ execute: | # [docs:pack-end] # [docs:skopeo-copy] - sudo /snap/k8s/current/bin/ctr \ - --address /run/containerd/containerd.sock \ - --namespace k8s.io images import \ - --base-name docker.io/library/django-hello-world \ - ./django-hello-world_0.1_$(dpkg --print-architecture).rock + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:django-hello-world_0.1_$(dpkg --print-architecture).rock \ + docker://localhost:5000/django-hello-world:0.1 # [docs:skopeo-copy-end] # [docs:create-charm-dir] @@ -114,7 +112,7 @@ execute: | juju deploy \ ./django-hello-world_$(dpkg --print-architecture).charm \ django-hello-world --resource \ - django-app-image=django-hello-world:0.1 + django-app-image=localhost:5000/django-hello-world:0.1 # [docs:deploy-django-app-end] # [docs:deploy-postgres] @@ -176,18 +174,16 @@ execute: | # [docs:repack-update] rockcraft pack - sudo /snap/k8s/current/bin/ctr \ - --address /run/containerd/containerd.sock \ - --namespace k8s.io images import \ - --base-name docker.io/library/django-hello-world \ - ./django-hello-world_0.2_$(dpkg --print-architecture).rock + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:django-hello-world_0.2_$(dpkg --print-architecture).rock \ + docker://localhost:5000/django-hello-world:0.2 # [docs:repack-update-end] # [docs:refresh-deployment] cd charm juju refresh django-hello-world \ --path=./django-hello-world_$(dpkg --print-architecture).charm \ - --resource django-app-image=django-hello-world:0.2 + --resource django-app-image=localhost:5000/django-hello-world:0.2 # [docs:refresh-deployment-end] # [docs:disable-debug-mode] @@ -208,11 +204,9 @@ execute: | # [docs:repack-2nd-update] rockcraft pack - sudo /snap/k8s/current/bin/ctr \ - --address /run/containerd/containerd.sock \ - --namespace k8s.io images import \ - --base-name docker.io/library/django-hello-world \ - ./django-hello-world_0.3_$(dpkg --print-architecture).rock + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:django-hello-world_0.3_$(dpkg --print-architecture).rock \ + docker://localhost:5000/django-hello-world:0.3 # [docs:repack-2nd-update-end] cd charm @@ -224,7 +218,7 @@ execute: | charmcraft pack juju refresh django-hello-world \ --path=./django-hello-world_$(dpkg --print-architecture).charm \ - --resource django-app-image=django-hello-world:0.3 + --resource django-app-image=localhost:5000/django-hello-world:0.3 # [docs:repack-refresh-2nd-deployment-end] # Wait for django-hello-world to be active diff --git a/docs/tutorial/code/expressjs/task.yaml b/docs/tutorial/code/expressjs/task.yaml index d5b7a4c1e..87c0417f1 100644 --- a/docs/tutorial/code/expressjs/task.yaml +++ b/docs/tutorial/code/expressjs/task.yaml @@ -77,11 +77,9 @@ execute: | # [docs:pack-end] # [docs:skopeo-copy] - sudo /snap/k8s/current/bin/ctr \ - --address /run/containerd/containerd.sock \ - --namespace k8s.io images import \ - --base-name docker.io/library/expressjs-hello-world \ - ./expressjs-hello-world_0.1_$(dpkg --print-architecture).rock + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:expressjs-hello-world_0.1_$(dpkg --print-architecture).rock \ + docker://localhost:5000/expressjs-hello-world:0.1 # [docs:skopeo-copy-end] # [docs:create-charm-dir] @@ -113,7 +111,7 @@ execute: | juju deploy \ ./expressjs-hello-world_$(dpkg --print-architecture).charm \ expressjs-hello-world \ - --resource app-image=expressjs-hello-world:0.1 + --resource app-image=localhost:5000/expressjs-hello-world:0.1 # [docs:deploy-expressjs-app-end] # [docs:deploy-nginx] @@ -142,11 +140,9 @@ execute: | # [docs:docker-update] rockcraft pack - sudo /snap/k8s/current/bin/ctr \ - --address /run/containerd/containerd.sock \ - --namespace k8s.io images import \ - --base-name docker.io/library/expressjs-hello-world \ - ./expressjs-hello-world_0.2_$(dpkg --print-architecture).rock + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:expressjs-hello-world_0.2_$(dpkg --print-architecture).rock \ + docker://localhost:5000/expressjs-hello-world:0.2 # [docs:docker-update-end] cat greeting_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -156,7 +152,7 @@ execute: | charmcraft pack juju refresh expressjs-hello-world \ --path=./expressjs-hello-world_$(dpkg --print-architecture).charm \ - --resource app-image=expressjs-hello-world:0.2 + --resource app-image=localhost:5000/expressjs-hello-world:0.2 # [docs:refresh-deployment-end] # give Juju some time to refresh the app @@ -201,11 +197,9 @@ execute: | # [docs:docker-2nd-update] rockcraft pack - sudo /snap/k8s/current/bin/ctr \ - --address /run/containerd/containerd.sock \ - --namespace k8s.io images import \ - --base-name docker.io/library/expressjs-hello-world \ - ./expressjs-hello-world_0.3_$(dpkg --print-architecture).rock + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:expressjs-hello-world_0.3_$(dpkg --print-architecture).rock \ + docker://localhost:5000/expressjs-hello-world:0.3 # [docs:docker-2nd-update-end] cat visitors_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -215,7 +209,7 @@ execute: | charmcraft pack juju refresh expressjs-hello-world \ --path=./expressjs-hello-world_$(dpkg --print-architecture).charm \ - --resource app-image=expressjs-hello-world:0.3 + --resource app-image=localhost:5000/expressjs-hello-world:0.3 # [docs:refresh-2nd-deployment-end] # In the relation with postgresql-k8s test we may hit the issue diff --git a/docs/tutorial/code/fastapi/task.yaml b/docs/tutorial/code/fastapi/task.yaml index b15e23518..a312ee4cd 100644 --- a/docs/tutorial/code/fastapi/task.yaml +++ b/docs/tutorial/code/fastapi/task.yaml @@ -58,11 +58,9 @@ execute: | # [docs:pack-end] # [docs:skopeo-copy] - sudo /snap/k8s/current/bin/ctr \ - --address /run/containerd/containerd.sock \ - --namespace k8s.io images import \ - --base-name docker.io/library/fastapi-hello-world \ - ./fastapi-hello-world_0.1_$(dpkg --print-architecture).rock + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:fastapi-hello-world_0.1_$(dpkg --print-architecture).rock \ + docker://localhost:5000/fastapi-hello-world:0.1 # [docs:skopeo-copy-end] # [docs:create-charm-dir] @@ -94,7 +92,7 @@ execute: | juju deploy \ ./fastapi-hello-world_$(dpkg --print-architecture).charm \ fastapi-hello-world --resource \ - app-image=fastapi-hello-world:0.1 + app-image=localhost:5000/fastapi-hello-world:0.1 # [docs:deploy-fastapi-app-end] # [docs:deploy-nginx] @@ -124,11 +122,9 @@ execute: | # [docs:docker-update] ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack - sudo /snap/k8s/current/bin/ctr \ - --address /run/containerd/containerd.sock \ - --namespace k8s.io images import \ - --base-name docker.io/library/fastapi-hello-world \ - ./fastapi-hello-world_0.2_$(dpkg --print-architecture).rock + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:fastapi-hello-world_0.2_$(dpkg --print-architecture).rock \ + docker://localhost:5000/fastapi-hello-world:0.2 # [docs:docker-update-end] cat greeting_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -138,7 +134,7 @@ execute: | CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack juju refresh fastapi-hello-world \ --path=./fastapi-hello-world_$(dpkg --print-architecture).charm \ - --resource app-image=fastapi-hello-world:0.2 + --resource app-image=localhost:5000/fastapi-hello-world:0.2 # [docs:refresh-deployment-end] # give Juju some time to refresh the app @@ -164,11 +160,9 @@ execute: | # [docs:docker-2nd-update] ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack - sudo /snap/k8s/current/bin/ctr \ - --address /run/containerd/containerd.sock \ - --namespace k8s.io images import \ - --base-name docker.io/library/fastapi-hello-world \ - ./fastapi-hello-world_0.3_$(dpkg --print-architecture).rock + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:fastapi-hello-world_0.3_$(dpkg --print-architecture).rock \ + docker://localhost:5000/fastapi-hello-world:0.3 # [docs:docker-2nd-update-end] cat visitors_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -178,7 +172,7 @@ execute: | CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack juju refresh fastapi-hello-world \ --path=./fastapi-hello-world_$(dpkg --print-architecture).charm \ - --resource app-image=fastapi-hello-world:0.3 + --resource app-image=localhost:5000/fastapi-hello-world:0.3 # [docs:refresh-2nd-deployment-end] # In the relation with postgresql-k8s test we may hit, so we have to wait before integrating. diff --git a/docs/tutorial/code/flask/task.yaml b/docs/tutorial/code/flask/task.yaml index 0aafdd6bc..d967e3fa0 100644 --- a/docs/tutorial/code/flask/task.yaml +++ b/docs/tutorial/code/flask/task.yaml @@ -58,11 +58,9 @@ execute: | # [docs:pack-end] # [docs:skopeo-copy] - sudo /snap/k8s/current/bin/ctr \ - --address /run/containerd/containerd.sock \ - --namespace k8s.io images import \ - --base-name docker.io/library/flask-hello-world \ - ./flask-hello-world_0.1_$(dpkg --print-architecture).rock + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:flask-hello-world_0.1_$(dpkg --print-architecture).rock \ + docker://localhost:5000/flask-hello-world:0.1 # [docs:skopeo-copy-end] # [docs:create-charm-dir] @@ -90,7 +88,7 @@ execute: | juju deploy \ ./flask-hello-world_$(dpkg --print-architecture).charm \ flask-hello-world --resource \ - flask-app-image=flask-hello-world:0.1 + flask-app-image=localhost:5000/flask-hello-world:0.1 # [docs:deploy-flask-app-end] # [docs:deploy-nginx] @@ -120,11 +118,9 @@ execute: | # [docs:docker-update] rockcraft pack - sudo /snap/k8s/current/bin/ctr \ - --address /run/containerd/containerd.sock \ - --namespace k8s.io images import \ - --base-name docker.io/library/flask-hello-world \ - ./flask-hello-world_0.2_$(dpkg --print-architecture).rock + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:flask-hello-world_0.2_$(dpkg --print-architecture).rock \ + docker://localhost:5000/flask-hello-world:0.2 # [docs:docker-update-end] cat greeting_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -134,7 +130,7 @@ execute: | charmcraft pack juju refresh flask-hello-world \ --path=./flask-hello-world_$(dpkg --print-architecture).charm \ - --resource flask-app-image=flask-hello-world:0.2 + --resource flask-app-image=localhost:5000/flask-hello-world:0.2 # [docs:refresh-deployment-end] # give Juju some time to refresh the app @@ -160,11 +156,9 @@ execute: | # [docs:docker-2nd-update] rockcraft pack - sudo /snap/k8s/current/bin/ctr \ - --address /run/containerd/containerd.sock \ - --namespace k8s.io images import \ - --base-name docker.io/library/flask-hello-world \ - ./flask-hello-world_0.3_$(dpkg --print-architecture).rock + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:flask-hello-world_0.3_$(dpkg --print-architecture).rock \ + docker://localhost:5000/flask-hello-world:0.3 # [docs:docker-2nd-update-end] cat visitors_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -174,7 +168,7 @@ execute: | charmcraft pack juju refresh flask-hello-world \ --path=./flask-hello-world_$(dpkg --print-architecture).charm \ - --resource flask-app-image=flask-hello-world:0.3 + --resource flask-app-image=localhost:5000/flask-hello-world:0.3 # [docs:refresh-2nd-deployment-end] # In the relation with postgresql-k8s test we may hit, so we have to wait before integrating. diff --git a/docs/tutorial/code/go/task.yaml b/docs/tutorial/code/go/task.yaml index 22f3eb85f..d65deeac2 100644 --- a/docs/tutorial/code/go/task.yaml +++ b/docs/tutorial/code/go/task.yaml @@ -66,11 +66,9 @@ execute: | # [docs:pack-end] # [docs:skopeo-copy] - sudo /snap/k8s/current/bin/ctr \ - --address /run/containerd/containerd.sock \ - --namespace k8s.io images import \ - --base-name docker.io/library/go-hello-world \ - ./go-hello-world_0.1_$(dpkg --print-architecture).rock + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:go-hello-world_0.1_$(dpkg --print-architecture).rock \ + docker://localhost:5000/go-hello-world:0.1 # [docs:skopeo-copy-end] # [docs:create-charm-dir] @@ -102,7 +100,7 @@ execute: | juju deploy \ ./go-hello-world_$(dpkg --print-architecture).charm \ go-hello-world --resource \ - app-image=go-hello-world:0.1 + app-image=localhost:5000/go-hello-world:0.1 # [docs:deploy-go-app-end] # [docs:deploy-nginx] @@ -132,11 +130,9 @@ execute: | # [docs:docker-update] ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack - sudo /snap/k8s/current/bin/ctr \ - --address /run/containerd/containerd.sock \ - --namespace k8s.io images import \ - --base-name docker.io/library/go-hello-world \ - ./go-hello-world_0.2_$(dpkg --print-architecture).rock + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:go-hello-world_0.2_$(dpkg --print-architecture).rock \ + docker://localhost:5000/go-hello-world:0.2 # [docs:docker-update-end] cat greeting_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -146,7 +142,7 @@ execute: | CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack juju refresh go-hello-world \ --path=./go-hello-world_$(dpkg --print-architecture).charm \ - --resource app-image=go-hello-world:0.2 + --resource app-image=localhost:5000/go-hello-world:0.2 # [docs:refresh-deployment-end] # give Juju some time to refresh the app @@ -183,11 +179,9 @@ execute: | # [docs:docker-2nd-update] ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack - sudo /snap/k8s/current/bin/ctr \ - --address /run/containerd/containerd.sock \ - --namespace k8s.io images import \ - --base-name docker.io/library/go-hello-world \ - ./go-hello-world_0.3_$(dpkg --print-architecture).rock + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:go-hello-world_0.3_$(dpkg --print-architecture).rock \ + docker://localhost:5000/go-hello-world:0.3 # [docs:docker-2nd-update-end] cat visitors_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -197,7 +191,7 @@ execute: | CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack juju refresh go-hello-world \ --path=./go-hello-world_$(dpkg --print-architecture).charm \ - --resource app-image=go-hello-world:0.3 + --resource app-image=localhost:5000/go-hello-world:0.3 # [docs:refresh-2nd-deployment-end] # In the relation with postgresql-k8s test we may hit, so we have to wait before integrating. diff --git a/docs/tutorial/code/spring-boot/task.yaml b/docs/tutorial/code/spring-boot/task.yaml index 4177cfbc7..b6d1c2717 100644 --- a/docs/tutorial/code/spring-boot/task.yaml +++ b/docs/tutorial/code/spring-boot/task.yaml @@ -84,11 +84,9 @@ execute: | # [docs:ls-rock-end] # [docs:skopeo-copy] - sudo /snap/k8s/current/bin/ctr \ - --address /run/containerd/containerd.sock \ - --namespace k8s.io images import \ - --base-name docker.io/library/spring-boot-hello-world \ - ./spring-boot-hello-world_0.1_$(dpkg --print-architecture).rock + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:spring-boot-hello-world_0.1_$(dpkg --print-architecture).rock \ + docker://localhost:5000/spring-boot-hello-world:0.1 # [docs:skopeo-copy-end] # [docs:create-charm-dir] @@ -120,7 +118,7 @@ execute: | juju deploy \ ./spring-boot-hello-world_$(dpkg --print-architecture).charm \ spring-boot-hello-world --resource \ - app-image=spring-boot-hello-world:0.1 + app-image=localhost:5000/spring-boot-hello-world:0.1 # [docs:deploy-app-end] # [docs:deploy-nginx] @@ -147,11 +145,9 @@ execute: | # [docs:skopeo-update] rockcraft pack - sudo /snap/k8s/current/bin/ctr \ - --address /run/containerd/containerd.sock \ - --namespace k8s.io images import \ - --base-name docker.io/library/spring-boot-hello-world \ - ./spring-boot-hello-world_0.2_$(dpkg --print-architecture).rock + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:spring-boot-hello-world_0.2_$(dpkg --print-architecture).rock \ + docker://localhost:5000/spring-boot-hello-world:0.2 # [docs:skopeo-update-end] cat $HOME/tmp/greeting_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -161,7 +157,7 @@ execute: | charmcraft pack juju refresh spring-boot-hello-world \ --path=./spring-boot-hello-world_$(dpkg --print-architecture).charm \ - --resource app-image=spring-boot-hello-world:0.2 + --resource app-image=localhost:5000/spring-boot-hello-world:0.2 # [docs:refresh-deployment-end] # give Juju some time to refresh the app @@ -208,11 +204,9 @@ execute: | # [docs:skopeo-2nd-update] rockcraft pack - sudo /snap/k8s/current/bin/ctr \ - --address /run/containerd/containerd.sock \ - --namespace k8s.io images import \ - --base-name docker.io/library/spring-boot-hello-world \ - ./spring-boot-hello-world_0.3_$(dpkg --print-architecture).rock + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:spring-boot-hello-world_0.3_$(dpkg --print-architecture).rock \ + docker://localhost:5000/spring-boot-hello-world:0.3 # [docs:skopeo-2nd-update-end] cat $HOME/tmp/visitors_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -222,7 +216,7 @@ execute: | charmcraft pack juju refresh spring-boot-hello-world \ --path=./spring-boot-hello-world_$(dpkg --print-architecture).charm \ - --resource app-image=spring-boot-hello-world:0.3 + --resource app-image=localhost:5000/spring-boot-hello-world:0.3 # [docs:refresh-2nd-deployment-end] # In the relation with postgresql-k8s test we may hit the issue diff --git a/docs/tutorial/kubernetes-charm-django.rst b/docs/tutorial/kubernetes-charm-django.rst index c70a63b37..2d3c1adc4 100644 --- a/docs/tutorial/kubernetes-charm-django.rst +++ b/docs/tutorial/kubernetes-charm-django.rst @@ -339,9 +339,9 @@ terminal will respond with something similar to reflects your system's architecture. After the initial pack, subsequent rock packings are faster. -The rock needs to be imported into the Canonical Kubernetes cluster so -that the image is available to Juju. Import the rock directly into the -cluster's containerd image store: +The rock needs to be copied to the local registry. This registry acts as a +temporary Docker Hub, storing OCI archives so they can be downloaded and +deployed in the Kubernetes cluster. Copy the rock: .. literalinclude:: code/django/task.yaml :language: bash @@ -351,13 +351,12 @@ cluster's containerd image store: This command contains the following pieces: -- ``--address``: points to the containerd socket that - Canonical Kubernetes uses. -- ``--namespace k8s.io``: imports the image into the containerd - namespace that Kubernetes reads from. -- ``images import``: imports the rock we created for our Django app. -- ``--base-name``: sets the image name that the charm references. The - image tag is taken from the rock's version. +- ``--insecure-policy``: adopts a permissive policy that + removes the need for a dedicated policy file. +- ``--dest-tls-verify=false``: disables the need for HTTPS + and certificate verification while interacting with the local registry. +- ``oci-archive``: specifies the rock we created for our Django app. +- ``docker``: specifies the name of the image in the local registry. Create the charm diff --git a/docs/tutorial/kubernetes-charm-express.rst b/docs/tutorial/kubernetes-charm-express.rst index 343ed2332..dcd60f53f 100644 --- a/docs/tutorial/kubernetes-charm-express.rst +++ b/docs/tutorial/kubernetes-charm-express.rst @@ -218,9 +218,9 @@ the terminal will respond with something similar to The file name reflects your system's architecture. After the initial pack, subsequent rock packings are faster. -The rock needs to be imported into the Canonical Kubernetes cluster so -that the image is available to Juju. Import the rock directly into the -cluster's containerd image store: +The rock needs to be copied to the local registry. This registry acts as a +temporary Docker Hub, storing OCI archives so they can be downloaded and +deployed in the Kubernetes cluster. Copy the rock: .. literalinclude:: code/expressjs/task.yaml :language: bash @@ -230,13 +230,12 @@ cluster's containerd image store: This command contains the following pieces: -- ``--address``: points to the containerd socket that - Canonical Kubernetes uses. -- ``--namespace k8s.io``: imports the image into the containerd - namespace that Kubernetes reads from. -- ``images import``: imports the rock we created for our Express app. -- ``--base-name``: sets the image name that the charm references. The - image tag is taken from the rock's version. +- ``--insecure-policy``: adopts a permissive policy that + removes the need for a dedicated policy file. +- ``--dest-tls-verify=false``: disables the need for HTTPS + and certificate verification while interacting with the local registry. +- ``oci-archive``: specifies the rock we created for our Express app. +- ``docker``: specifies the name of the image in the local registry. Create the charm ---------------- diff --git a/docs/tutorial/kubernetes-charm-fastapi.rst b/docs/tutorial/kubernetes-charm-fastapi.rst index 54a211f18..d4d1cc037 100644 --- a/docs/tutorial/kubernetes-charm-fastapi.rst +++ b/docs/tutorial/kubernetes-charm-fastapi.rst @@ -234,9 +234,9 @@ the terminal will respond with something similar to reflects your system's architecture. After the initial pack, subsequent rock packings are faster. -The rock needs to be imported into the Canonical Kubernetes cluster so -that the image is available to Juju. Import the rock directly into the -cluster's containerd image store: +The rock needs to be copied to the local registry. This registry acts as a +temporary Docker Hub, storing OCI archives so they can be downloaded and +deployed in the Kubernetes cluster. Copy the rock: .. literalinclude:: code/fastapi/task.yaml :language: bash @@ -246,13 +246,12 @@ cluster's containerd image store: This command contains the following pieces: -- ``--address``: points to the containerd socket that - Canonical Kubernetes uses. -- ``--namespace k8s.io``: imports the image into the containerd - namespace that Kubernetes reads from. -- ``images import``: imports the rock we created for our FastAPI app. -- ``--base-name``: sets the image name that the charm references. The - image tag is taken from the rock's version. +- ``--insecure-policy``: adopts a permissive policy that + removes the need for a dedicated policy file. +- ``--dest-tls-verify=false``: disables the need for HTTPS + and certificate verification while interacting with the local registry. +- ``oci-archive``: specifies the rock we created for our FastAPI app. +- ``docker``: specifies the name of the image in the local registry. Create the charm diff --git a/docs/tutorial/kubernetes-charm-flask.rst b/docs/tutorial/kubernetes-charm-flask.rst index eb1143fa4..ccf686540 100644 --- a/docs/tutorial/kubernetes-charm-flask.rst +++ b/docs/tutorial/kubernetes-charm-flask.rst @@ -225,9 +225,9 @@ the terminal will respond with something similar to reflects your system's architecture. After the initial pack, subsequent rock packings are faster. -The rock needs to be imported into the Canonical Kubernetes cluster so -that the image is available to Juju. Import the rock directly into the -cluster's containerd image store: +The rock needs to be copied to the local registry. This registry acts as a +temporary Docker Hub, storing OCI archives so they can be downloaded and +deployed in the Kubernetes cluster. Copy the rock: .. literalinclude:: code/flask/task.yaml :language: bash @@ -237,13 +237,12 @@ cluster's containerd image store: This command contains the following pieces: -- ``--address``: points to the containerd socket that - Canonical Kubernetes uses. -- ``--namespace k8s.io``: imports the image into the containerd - namespace that Kubernetes reads from. -- ``images import``: imports the rock we created for our Flask app. -- ``--base-name``: sets the image name that the charm references. The - image tag is taken from the rock's version. +- ``--insecure-policy``: adopts a permissive policy that + removes the need for a dedicated policy file. +- ``--dest-tls-verify=false``: disables the need for HTTPS + and certificate verification while interacting with the local registry. +- ``oci-archive``: specifies the rock we created for our Flask app. +- ``docker``: specifies the name of the image in the local registry. Create the charm diff --git a/docs/tutorial/kubernetes-charm-go.rst b/docs/tutorial/kubernetes-charm-go.rst index fd96a1d9d..d65343d38 100644 --- a/docs/tutorial/kubernetes-charm-go.rst +++ b/docs/tutorial/kubernetes-charm-go.rst @@ -221,9 +221,9 @@ the terminal will respond with something similar to reflects your system's architecture. After the initial pack, subsequent rock packings are faster. -The rock needs to be imported into the Canonical Kubernetes cluster so -that the image is available to Juju. Import the rock directly into the -cluster's containerd image store: +The rock needs to be copied to the local registry. This registry acts as a +temporary Docker Hub, storing OCI archives so they can be downloaded and +deployed in the Kubernetes cluster. Copy the rock: .. literalinclude:: code/go/task.yaml :language: bash @@ -233,13 +233,12 @@ cluster's containerd image store: This command contains the following pieces: -- ``--address``: points to the containerd socket that - Canonical Kubernetes uses. -- ``--namespace k8s.io``: imports the image into the containerd - namespace that Kubernetes reads from. -- ``images import``: imports the rock we created for our Go app. -- ``--base-name``: sets the image name that the charm references. The - image tag is taken from the rock's version. +- ``--insecure-policy``: adopts a permissive policy that + removes the need for a dedicated policy file. +- ``--dest-tls-verify=false``: disables the need for HTTPS + and certificate verification while interacting with the local registry. +- ``oci-archive``: specifies the rock we created for our Go app. +- ``docker``: specifies the name of the image in the local registry. Create the charm diff --git a/docs/tutorial/kubernetes-charm-spring-boot.rst b/docs/tutorial/kubernetes-charm-spring-boot.rst index 49e7ae3b0..b0a761fc6 100644 --- a/docs/tutorial/kubernetes-charm-spring-boot.rst +++ b/docs/tutorial/kubernetes-charm-spring-boot.rst @@ -224,9 +224,9 @@ the terminal will respond with something similar to reflects your system's architecture. After the initial pack, subsequent rock packings are faster. -The rock needs to be imported into the Canonical Kubernetes cluster so -that the image is available to Juju. Import the rock directly into the -cluster's containerd image store: +The rock needs to be copied to the local registry. This registry acts as a +temporary Docker Hub, storing OCI archives so they can be downloaded and +deployed in the Kubernetes cluster. Copy the rock: .. literalinclude:: code/spring-boot/task.yaml :language: bash @@ -236,13 +236,12 @@ cluster's containerd image store: This command contains the following pieces: -- ``--address``: points to the containerd socket that - Canonical Kubernetes uses. -- ``--namespace k8s.io``: imports the image into the containerd - namespace that Kubernetes reads from. -- ``images import``: imports the rock we created for our Spring Boot app. -- ``--base-name``: sets the image name that the charm references. The - image tag is taken from the rock's version. +- ``--insecure-policy``: adopts a permissive policy that + removes the need for a dedicated policy file. +- ``--dest-tls-verify=false``: disables the need for HTTPS + and certificate verification while interacting with the local registry. +- ``oci-archive``: specifies the rock we created for our Spring Boot app. +- ``docker``: specifies the name of the image in the local registry. Create the charm ---------------- diff --git a/spread.yaml b/spread.yaml index 820638305..5782ba588 100644 --- a/spread.yaml +++ b/spread.yaml @@ -299,16 +299,47 @@ suites: refresh_or_install_snap k8s "$k8s_channel" --classic refresh_or_install_snap rockcraft latest/edge --classic - # Canonical Kubernetes runs its own containerd using the standard host paths, - # so the docker.io pull-through mirror lives under /etc/containerd/hosts.d. - # It must be written before bootstrap so containerd picks it up on startup. - if [[ -v CONTAINER_REGISTRY_URL ]]; then - CONTAINER_REGISTRY_HOSTPORT=$(echo "$CONTAINER_REGISTRY_URL" | awk -F/ '{print $3}') - mkdir -p /etc/containerd/hosts.d/docker.io - echo "server = \"$CONTAINER_REGISTRY_URL\"" > /etc/containerd/hosts.d/docker.io/hosts.toml - echo "[host.\"$CONTAINER_REGISTRY_HOSTPORT\"]" >> /etc/containerd/hosts.d/docker.io/hosts.toml - echo 'capabilities = ["pull", "resolve"]' >> /etc/containerd/hosts.d/docker.io/hosts.toml - fi + # The tutorial pushes the application image to a local OCI registry. Run it + # as a plain systemd service (no Docker, which would conflict with the k8s + # snap's own containerd), as done in the k8s-snap integration test harness. + ARCH=$(dpkg --print-architecture) + curl -sSL -o /tmp/registry.tar.gz \ + "https://github.com/distribution/distribution/releases/download/v2.8.3/registry_2.8.3_linux_${ARCH}.tar.gz" + tar -xzf /tmp/registry.tar.gz -C /usr/local/bin registry + mkdir -p /etc/distribution + cat <<'EOF' > /etc/distribution/config.yml + version: 0.1 + storage: + filesystem: + rootdirectory: /var/lib/registry + http: + addr: :5000 + EOF + cat <<'EOF' > /etc/systemd/system/registry.service + [Unit] + Description=OCI registry + After=network.target + + [Service] + ExecStart=/usr/local/bin/registry serve /etc/distribution/config.yml + Restart=always + + [Install] + WantedBy=multi-user.target + EOF + systemctl enable --now registry + retry -n 10 --wait 1 curl -sf http://localhost:5000/v2/ + + # Let Canonical Kubernetes pull from the insecure local registry. The k8s + # snap uses the standard containerd hosts.d path; it is read per-pull, so no + # restart is required. + mkdir -p "/etc/containerd/hosts.d/localhost:5000" + cat <<'EOF' > "/etc/containerd/hosts.d/localhost:5000/hosts.toml" + server = "http://localhost:5000" + + [host."http://localhost:5000"] + capabilities = ["pull", "resolve"] + EOF k8s bootstrap k8s status --wait-ready --timeout 5m From 7d6e8dde97f5e56ad96287fa2a5bd6ddd3e8081b Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Mon, 12 Oct 2026 19:40:15 +0700 Subject: [PATCH 05/28] docs: expose CK8s tutorial ingress via load balancer The Canonical Kubernetes ingress is fronted by a LoadBalancer service rather than binding the node's host ports like the MicroK8s add-on, so the documented `curl ... 127.0.0.1` requests could not reach the app. Enable the built-in load balancer with an L2 address pool set to the VM's own IP in the tutorial setup, and resolve the ingress host to that address (`$INGRESS_IP`) instead of the loopback address. Align the spread tutorial suite with the documented commands and drop the test-only loopback DNAT workaround. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/reuse/tutorial/setup_edge.rst | 11 ++++++++++ docs/reuse/tutorial/setup_stable.rst | 11 ++++++++++ docs/tutorial/code/django/task.yaml | 12 ++++++----- docs/tutorial/code/expressjs/task.yaml | 16 ++++++++------- docs/tutorial/code/fastapi/task.yaml | 20 ++++++++++--------- docs/tutorial/code/flask/task.yaml | 20 ++++++++++--------- docs/tutorial/code/go/task.yaml | 20 ++++++++++--------- docs/tutorial/code/spring-boot/task.yaml | 16 ++++++++------- docs/tutorial/kubernetes-charm-django.rst | 8 +++++--- docs/tutorial/kubernetes-charm-express.rst | 18 +++++++++-------- docs/tutorial/kubernetes-charm-fastapi.rst | 18 +++++++++-------- docs/tutorial/kubernetes-charm-flask.rst | 18 +++++++++-------- docs/tutorial/kubernetes-charm-go.rst | 18 +++++++++-------- .../tutorial/kubernetes-charm-spring-boot.rst | 18 +++++++++-------- spread.yaml | 9 ++------- 15 files changed, 137 insertions(+), 96 deletions(-) diff --git a/docs/reuse/tutorial/setup_edge.rst b/docs/reuse/tutorial/setup_edge.rst index de4cf5633..b81c05907 100644 --- a/docs/reuse/tutorial/setup_edge.rst +++ b/docs/reuse/tutorial/setup_edge.rst @@ -72,6 +72,17 @@ Enable the necessary features: sudo k8s enable local-storage sudo k8s enable ingress +Unlike the MicroK8s ingress add-on, which binds the node's ports directly, +Canonical Kubernetes exposes ingress through a load-balancer service. Enable +the built-in load balancer and point its address pool at the VM's own IP so +the ingress is assigned a reachable address: + +.. code-block:: bash + + sudo k8s enable load-balancer + VM_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') + sudo k8s set load-balancer.l2-mode=true load-balancer.cidrs="$VM_IP/32" + Check the status of Canonical Kubernetes: .. code-block:: bash diff --git a/docs/reuse/tutorial/setup_stable.rst b/docs/reuse/tutorial/setup_stable.rst index 00aa7d933..5c2a4fd72 100644 --- a/docs/reuse/tutorial/setup_stable.rst +++ b/docs/reuse/tutorial/setup_stable.rst @@ -68,6 +68,17 @@ Enable the necessary features: sudo k8s enable local-storage sudo k8s enable ingress +Unlike the MicroK8s ingress add-on, which binds the node's ports directly, +Canonical Kubernetes exposes ingress through a load-balancer service. Enable +the built-in load balancer and point its address pool at the VM's own IP so +the ingress is assigned a reachable address: + +.. code-block:: bash + + sudo k8s enable load-balancer + VM_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') + sudo k8s set load-balancer.l2-mode=true load-balancer.cidrs="$VM_IP/32" + Check the status of Canonical Kubernetes: .. code-block:: bash diff --git a/docs/tutorial/code/django/task.yaml b/docs/tutorial/code/django/task.yaml index d98cc7c95..4e5c5e873 100644 --- a/docs/tutorial/code/django/task.yaml +++ b/docs/tutorial/code/django/task.yaml @@ -148,10 +148,12 @@ execute: | juju status --relations # wait until the endpoint works - retry -n 5 --wait 5 curl --fail http://django-hello-world --resolve django-hello-world:80:127.0.0.1 + # Canonical Kubernetes serves the ingress at the VM's address + INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') + retry -n 5 --wait 5 curl --fail http://django-hello-world --resolve django-hello-world:80:$INGRESS_IP # curl the Django app - curl http://django-hello-world --resolve django-hello-world:80:127.0.0.1 | grep Congratulations + curl http://django-hello-world --resolve django-hello-world:80:$INGRESS_IP | grep Congratulations cd .. cd django_hello_world @@ -194,7 +196,7 @@ execute: | juju wait-for application django-hello-world --query='status=="active"' --timeout 10m # curl and check that the response is Hello, world! - retry -n 5 --wait 5 sh -c "curl http://django-hello-world --resolve django-hello-world:80:127.0.0.1 | grep Hello" + retry -n 5 --wait 5 sh -c "curl http://django-hello-world --resolve django-hello-world:80:$INGRESS_IP | grep Hello" cd .. # Update django_hello_world/greeting/views.py @@ -225,7 +227,7 @@ execute: | juju wait-for application django-hello-world --query='status=="active"' --timeout 10m # curl and check that the response is still Hello, world! - retry -n 5 --wait 5 sh -c "curl http://django-hello-world --resolve django-hello-world:80:127.0.0.1 | grep Hello" + retry -n 5 --wait 5 sh -c "curl http://django-hello-world --resolve django-hello-world:80:$INGRESS_IP | grep Hello" # [docs:change-config] juju config django-hello-world greeting='Hi!' @@ -235,7 +237,7 @@ execute: | juju wait-for application django-hello-world --query='status=="active"' --timeout 10m # curl and check that the response is now Hi - retry -n 5 --wait 5 sh -c "curl http://django-hello-world --resolve django-hello-world:80:127.0.0.1 | grep Hi" + retry -n 5 --wait 5 sh -c "curl http://django-hello-world --resolve django-hello-world:80:$INGRESS_IP | grep Hi" # [docs:clean-environment] charmcraft clean diff --git a/docs/tutorial/code/expressjs/task.yaml b/docs/tutorial/code/expressjs/task.yaml index 89ba60a72..b218d4c21 100644 --- a/docs/tutorial/code/expressjs/task.yaml +++ b/docs/tutorial/code/expressjs/task.yaml @@ -126,7 +126,9 @@ execute: | --query='status=="active"' --timeout 10m # [docs:curl-init-deployment] - curl -I http://expressjs-hello-world --resolve expressjs-hello-world:80:127.0.0.1 + # Canonical Kubernetes serves the ingress at the VM's address + INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') + curl -I http://expressjs-hello-world --resolve expressjs-hello-world:80:$INGRESS_IP # [docs:curl-init-deployment-end] cd .. @@ -156,7 +158,7 @@ execute: | # curl and check that the response is Hello retry -n 5 --wait 2 sh -c "curl http://expressjs-hello-world \ - --resolve expressjs-hello-world:80:127.0.0.1 | grep Hello" + --resolve expressjs-hello-world:80:$INGRESS_IP | grep Hello" # [docs:change-config] juju config expressjs-hello-world greeting='Hi!' @@ -168,7 +170,7 @@ execute: | # curl and check that the response is now Hi retry -n 5 --wait 2 sh -c "curl http://expressjs-hello-world \ - --resolve expressjs-hello-world:80:127.0.0.1 | grep Hi" + --resolve expressjs-hello-world:80:$INGRESS_IP | grep Hi" cd .. cat visitors_migrate.sh >> app/migrate.sh @@ -224,13 +226,13 @@ execute: | juju status --relations retry -n 5 --wait 2 sh -c "curl http://expressjs-hello-world \ - --resolve expressjs-hello-world:80:127.0.0.1 | grep Hi " + --resolve expressjs-hello-world:80:$INGRESS_IP | grep Hi " curl http://expressjs-hello-world/visitors \ - --resolve expressjs-hello-world:80:127.0.0.1 | grep 1 + --resolve expressjs-hello-world:80:$INGRESS_IP | grep 1 curl http://expressjs-hello-world \ - --resolve expressjs-hello-world:80:127.0.0.1 | grep Hi + --resolve expressjs-hello-world:80:$INGRESS_IP | grep Hi curl http://expressjs-hello-world/visitors \ - --resolve expressjs-hello-world:80:127.0.0.1 | grep 2 + --resolve expressjs-hello-world:80:$INGRESS_IP | grep 2 # [docs:clean-environment] charmcraft clean diff --git a/docs/tutorial/code/fastapi/task.yaml b/docs/tutorial/code/fastapi/task.yaml index 3c2d63ddf..3dd6f19c0 100644 --- a/docs/tutorial/code/fastapi/task.yaml +++ b/docs/tutorial/code/fastapi/task.yaml @@ -110,10 +110,12 @@ execute: | juju wait-for application nginx-ingress-integrator --query='status=="active"' --timeout 10m # wait until the endpoint works - retry -n 5 --wait 5 curl --fail http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1 + # Canonical Kubernetes serves the ingress at the VM's address + INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') + retry -n 5 --wait 5 curl --fail http://fastapi-hello-world --resolve fastapi-hello-world:80:$INGRESS_IP # [docs:curl-init-deployment] - curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1 + curl http://fastapi-hello-world --resolve fastapi-hello-world:80:$INGRESS_IP # [docs:curl-init-deployment-end] cd .. @@ -141,7 +143,7 @@ execute: | juju wait-for application fastapi-hello-world --query='status=="active"' --timeout 10m # curl and check that the response is Hello - retry -n 5 --wait 5 sh -c "curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1 | grep Hello" + retry -n 5 --wait 5 sh -c "curl http://fastapi-hello-world --resolve fastapi-hello-world:80:$INGRESS_IP | grep Hello" # [docs:change-config] juju config fastapi-hello-world greeting='Hi!' @@ -151,7 +153,7 @@ execute: | juju wait-for application fastapi-hello-world --query='status=="active"' --timeout 10m # curl and check that the response is now Hi - retry -n 5 --wait 5 sh -c "curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1 | grep Hi" + retry -n 5 --wait 5 sh -c "curl http://fastapi-hello-world --resolve fastapi-hello-world:80:$INGRESS_IP | grep Hi" cd .. cat visitors_migrate.py >> migrate.py @@ -191,12 +193,12 @@ execute: | juju status --relations # wait until the endpoint works - retry -n 5 --wait 5 curl --fail http://fastapi-hello-world/visitors --resolve fastapi-hello-world:80:127.0.0.1 + retry -n 5 --wait 5 curl --fail http://fastapi-hello-world/visitors --resolve fastapi-hello-world:80:$INGRESS_IP - curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1 | grep Hi - curl http://fastapi-hello-world/visitors --resolve fastapi-hello-world:80:127.0.0.1 | grep 1 - curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1 | grep Hi - curl http://fastapi-hello-world/visitors --resolve fastapi-hello-world:80:127.0.0.1 | grep 2 + curl http://fastapi-hello-world --resolve fastapi-hello-world:80:$INGRESS_IP | grep Hi + curl http://fastapi-hello-world/visitors --resolve fastapi-hello-world:80:$INGRESS_IP | grep 1 + curl http://fastapi-hello-world --resolve fastapi-hello-world:80:$INGRESS_IP | grep Hi + curl http://fastapi-hello-world/visitors --resolve fastapi-hello-world:80:$INGRESS_IP | grep 2 # [docs:clean-environment] charmcraft clean diff --git a/docs/tutorial/code/flask/task.yaml b/docs/tutorial/code/flask/task.yaml index d967e3fa0..173754f6f 100644 --- a/docs/tutorial/code/flask/task.yaml +++ b/docs/tutorial/code/flask/task.yaml @@ -106,10 +106,12 @@ execute: | juju wait-for application nginx-ingress-integrator --query='status=="active"' --timeout 10m # wait until the endpoint works - retry -n 5 --wait 5 curl --fail http://flask-hello-world --resolve flask-hello-world:80:127.0.0.1 + # Canonical Kubernetes serves the ingress at the VM's address + INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') + retry -n 5 --wait 5 curl --fail http://flask-hello-world --resolve flask-hello-world:80:$INGRESS_IP # [docs:curl-init-deployment] - curl http://flask-hello-world --resolve flask-hello-world:80:127.0.0.1 + curl http://flask-hello-world --resolve flask-hello-world:80:$INGRESS_IP # [docs:curl-init-deployment-end] cd .. @@ -137,7 +139,7 @@ execute: | juju wait-for application flask-hello-world --query='status=="active"' --timeout 10m # curl and check that the response is Hello - retry -n 5 --wait 5 sh -c "curl http://flask-hello-world --resolve flask-hello-world:80:127.0.0.1 | grep Hello" + retry -n 5 --wait 5 sh -c "curl http://flask-hello-world --resolve flask-hello-world:80:$INGRESS_IP | grep Hello" # [docs:change-config] juju config flask-hello-world greeting='Hi!' @@ -147,7 +149,7 @@ execute: | juju wait-for application flask-hello-world --query='status=="active"' --timeout 10m # curl and check that the response is now Hi - retry -n 5 --wait 5 sh -c "curl http://flask-hello-world --resolve flask-hello-world:80:127.0.0.1 | grep Hi" + retry -n 5 --wait 5 sh -c "curl http://flask-hello-world --resolve flask-hello-world:80:$INGRESS_IP | grep Hi" cd .. cat visitors_migrate.py >> migrate.py @@ -186,12 +188,12 @@ execute: | juju status --relations # wait until the endpoint works - retry -n 5 --wait 5 curl --fail http://flask-hello-world/visitors --resolve flask-hello-world:80:127.0.0.1 + retry -n 5 --wait 5 curl --fail http://flask-hello-world/visitors --resolve flask-hello-world:80:$INGRESS_IP - 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 + curl http://flask-hello-world --resolve flask-hello-world:80:$INGRESS_IP | grep Hi + curl http://flask-hello-world/visitors --resolve flask-hello-world:80:$INGRESS_IP | grep 1 + curl http://flask-hello-world --resolve flask-hello-world:80:$INGRESS_IP | grep Hi + curl http://flask-hello-world/visitors --resolve flask-hello-world:80:$INGRESS_IP | grep 2 # [docs:clean-environment] charmcraft clean diff --git a/docs/tutorial/code/go/task.yaml b/docs/tutorial/code/go/task.yaml index 03cc82a77..5eb9000a2 100644 --- a/docs/tutorial/code/go/task.yaml +++ b/docs/tutorial/code/go/task.yaml @@ -118,10 +118,12 @@ execute: | juju wait-for application nginx-ingress-integrator --query='status=="active"' --timeout 10m # wait until the endpoint works - retry -n 5 --wait 5 curl --fail http://go-hello-world --resolve go-hello-world:80:127.0.0.1 + # Canonical Kubernetes serves the ingress at the VM's address + INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') + retry -n 5 --wait 5 curl --fail http://go-hello-world --resolve go-hello-world:80:$INGRESS_IP # [docs:curl-init-deployment] - curl http://go-hello-world --resolve go-hello-world:80:127.0.0.1 + curl http://go-hello-world --resolve go-hello-world:80:$INGRESS_IP # [docs:curl-init-deployment-end] cd .. @@ -149,7 +151,7 @@ execute: | juju wait-for application go-hello-world --query='status=="active"' --timeout 10m # curl and check that the response is Hello - retry -n 5 --wait 5 sh -c "curl http://go-hello-world --resolve go-hello-world:80:127.0.0.1 | grep Hello" + retry -n 5 --wait 5 sh -c "curl http://go-hello-world --resolve go-hello-world:80:$INGRESS_IP | grep Hello" # [docs:change-config] juju config go-hello-world greeting='Hi!' @@ -159,7 +161,7 @@ execute: | juju wait-for application go-hello-world --query='status=="active"' --timeout 10m # curl and check that the response is now Hi - retry -n 5 --wait 5 sh -c "curl http://go-hello-world --resolve go-hello-world:80:127.0.0.1 | grep Hi" + retry -n 5 --wait 5 sh -c "curl http://go-hello-world --resolve go-hello-world:80:$INGRESS_IP | grep Hi" cd .. cat visitors_migrate.sh >> migrate.sh @@ -210,12 +212,12 @@ execute: | juju wait-for application go-hello-world --query='status=="active"' --timeout 20m # wait until the endpoint works - retry -n 5 --wait 5 curl --fail http://go-hello-world/visitors --resolve go-hello-world:80:127.0.0.1 + retry -n 5 --wait 5 curl --fail http://go-hello-world/visitors --resolve go-hello-world:80:$INGRESS_IP - curl http://go-hello-world --resolve go-hello-world:80:127.0.0.1 | grep Hi - curl http://go-hello-world/visitors --resolve go-hello-world:80:127.0.0.1 | grep 1 - curl http://go-hello-world --resolve go-hello-world:80:127.0.0.1 | grep Hi - curl http://go-hello-world/visitors --resolve go-hello-world:80:127.0.0.1 | grep 2 + curl http://go-hello-world --resolve go-hello-world:80:$INGRESS_IP | grep Hi + curl http://go-hello-world/visitors --resolve go-hello-world:80:$INGRESS_IP | grep 1 + curl http://go-hello-world --resolve go-hello-world:80:$INGRESS_IP | grep Hi + curl http://go-hello-world/visitors --resolve go-hello-world:80:$INGRESS_IP | grep 2 # [docs:clean-environment] charmcraft clean diff --git a/docs/tutorial/code/spring-boot/task.yaml b/docs/tutorial/code/spring-boot/task.yaml index 3c7b99848..efad32e30 100644 --- a/docs/tutorial/code/spring-boot/task.yaml +++ b/docs/tutorial/code/spring-boot/task.yaml @@ -136,7 +136,9 @@ execute: | juju wait-for application nginx-ingress-integrator --query='status=="active"' --timeout 10m # [docs:curl-init-deployment] - curl http://spring-boot-hello-world --resolve spring-boot-hello-world:80:127.0.0.1 + # Canonical Kubernetes serves the ingress at the VM's address + INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') + curl http://spring-boot-hello-world --resolve spring-boot-hello-world:80:$INGRESS_IP # [docs:curl-init-deployment-end] cd .. @@ -164,7 +166,7 @@ execute: | juju wait-for application spring-boot-hello-world --query='status=="active"' --timeout 10m # curl and check that the response is Hello - retry -n 10 --wait 20 sh -c "curl http://spring-boot-hello-world --resolve spring-boot-hello-world:80:127.0.0.1 | grep Hello" + retry -n 10 --wait 20 sh -c "curl http://spring-boot-hello-world --resolve spring-boot-hello-world:80:$INGRESS_IP | grep Hello" # [docs:change-config] juju config spring-boot-hello-world greeting='Hi!' @@ -173,7 +175,7 @@ execute: | juju wait-for application spring-boot-hello-world --query='status=="active"' --timeout 10m # curl and check that the response is now Hi - retry -n 10 --wait 20 sh -c "curl http://spring-boot-hello-world --resolve spring-boot-hello-world:80:127.0.0.1 | grep Hi" + retry -n 10 --wait 20 sh -c "curl http://spring-boot-hello-world --resolve spring-boot-hello-world:80:$INGRESS_IP | grep Hi" cd .. # [docs:add-generate-ddl] @@ -232,10 +234,10 @@ execute: | juju wait-for application postgresql-k8s --query='status=="active"' --timeout 20m | juju status --relations juju wait-for application spring-boot-hello-world --query='status=="active"' --timeout 20m | juju status --relations - retry -n 10 --wait 20 sh -c "curl http://spring-boot-hello-world --resolve spring-boot-hello-world:80:127.0.0.1 | grep Hi" - curl http://spring-boot-hello-world/visitors --resolve spring-boot-hello-world:80:127.0.0.1 | grep 1 - curl http://spring-boot-hello-world --resolve spring-boot-hello-world:80:127.0.0.1 | grep Hi - curl http://spring-boot-hello-world/visitors --resolve spring-boot-hello-world:80:127.0.0.1 | grep 2 + retry -n 10 --wait 20 sh -c "curl http://spring-boot-hello-world --resolve spring-boot-hello-world:80:$INGRESS_IP | grep Hi" + curl http://spring-boot-hello-world/visitors --resolve spring-boot-hello-world:80:$INGRESS_IP | grep 1 + curl http://spring-boot-hello-world --resolve spring-boot-hello-world:80:$INGRESS_IP | grep Hi + curl http://spring-boot-hello-world/visitors --resolve spring-boot-hello-world:80:$INGRESS_IP | grep 2 # [docs:clean-environment] charmcraft clean diff --git a/docs/tutorial/kubernetes-charm-django.rst b/docs/tutorial/kubernetes-charm-django.rst index 2d3c1adc4..b1721f45e 100644 --- a/docs/tutorial/kubernetes-charm-django.rst +++ b/docs/tutorial/kubernetes-charm-django.rst @@ -688,7 +688,9 @@ inside the Multipass VM run: .. code-block:: bash - curl http://django-hello-world --resolve django-hello-world:80:127.0.0.1 + # Canonical Kubernetes serves the ingress at the VM's address + INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') + curl http://django-hello-world --resolve django-hello-world:80:$INGRESS_IP Either way, the Django app should respond with ``Hello, world!``. @@ -786,7 +788,7 @@ web browser or send a request to the root endpoint using: .. code-block:: bash - curl http://django-hello-world --resolve django-hello-world:80:127.0.0.1 + curl http://django-hello-world --resolve django-hello-world:80:$INGRESS_IP Either action should result in the Django app responding with ``Hello, world!`` again. @@ -803,7 +805,7 @@ Wait for the app to restart, and then visit http://django-hello-world or use: .. code-block:: bash - curl http://django-hello-world --resolve django-hello-world:80:127.0.0.1 + curl http://django-hello-world --resolve django-hello-world:80:$INGRESS_IP The web app should now respond with ``Hi!``. diff --git a/docs/tutorial/kubernetes-charm-express.rst b/docs/tutorial/kubernetes-charm-express.rst index 4aafe77e5..92835976f 100644 --- a/docs/tutorial/kubernetes-charm-express.rst +++ b/docs/tutorial/kubernetes-charm-express.rst @@ -412,13 +412,15 @@ Send a request via the ingress: .. code-block:: bash - curl -I http://expressjs-hello-world --resolve expressjs-hello-world:80:127.0.0.1 + # Canonical Kubernetes serves the ingress at the VM's address + INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') + curl -I http://expressjs-hello-world --resolve expressjs-hello-world:80:$INGRESS_IP It should show the ``HTTP/1.1 200 OK`` status message. .. note:: - The ``--resolve expressjs-hello-world:80:127.0.0.1`` option to the ``curl`` + The ``--resolve expressjs-hello-world:80:$INGRESS_IP`` option to the ``curl`` command is a way of resolving the hostname of the request without setting a DNS record. @@ -531,7 +533,7 @@ Check that the response is still ``Hello, world!`` using: .. code-block:: bash - curl http://expressjs-hello-world --resolve expressjs-hello-world:80:127.0.0.1 + curl http://expressjs-hello-world --resolve expressjs-hello-world:80:$INGRESS_IP Now let's change the greeting: @@ -545,7 +547,7 @@ Wait for the app to restart, then check the response: .. code-block:: bash - curl http://expressjs-hello-world --resolve expressjs-hello-world:80:127.0.0.1 + curl http://expressjs-hello-world --resolve expressjs-hello-world:80:$INGRESS_IP The response should now return the updated ``Hi!`` greeting. @@ -721,7 +723,7 @@ Once the Express app is ``active``, send a request to the endpoint: .. code-block:: bash - curl http://expressjs-hello-world --resolve expressjs-hello-world:80:127.0.0.1 + curl http://expressjs-hello-world --resolve expressjs-hello-world:80:$INGRESS_IP It should still return the ``Hi!`` greeting. @@ -729,7 +731,7 @@ Check the local visitors: .. code-block:: bash - curl http://expressjs-hello-world/visitors --resolve expressjs-hello-world:80:127.0.0.1 + curl http://expressjs-hello-world/visitors --resolve expressjs-hello-world:80:$INGRESS_IP This request should return ``Number of visitors 1`` after the previous request to the root endpoint. @@ -741,7 +743,7 @@ repeat this process, the output should be as follows: :host: charm-dev :dir: ~/expressjs-hello-world/charm - curl http://expressjs-hello-world --resolve expressjs-hello-world:80:127.0.0.1 + curl http://expressjs-hello-world --resolve expressjs-hello-world:80:$INGRESS_IP Hi! @@ -750,7 +752,7 @@ repeat this process, the output should be as follows: :host: charm-dev :dir: ~/expressjs-hello-world/charm - curl http://expressjs-hello-world/visitors --resolve expressjs-hello-world:80:127.0.0.1 + curl http://expressjs-hello-world/visitors --resolve expressjs-hello-world:80:$INGRESS_IP Number of visitors 2 diff --git a/docs/tutorial/kubernetes-charm-fastapi.rst b/docs/tutorial/kubernetes-charm-fastapi.rst index c017d4869..9d7502299 100644 --- a/docs/tutorial/kubernetes-charm-fastapi.rst +++ b/docs/tutorial/kubernetes-charm-fastapi.rst @@ -438,13 +438,15 @@ Test the deployment by sending a request via the ingress: .. code-block:: bash - curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1 + # Canonical Kubernetes serves the ingress at the VM's address + INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') + curl http://fastapi-hello-world --resolve fastapi-hello-world:80:$INGRESS_IP It should return the ``{"message":"Hello, world!"}`` greeting. .. note:: - The ``--resolve fastapi-hello-world:80:127.0.0.1`` option to the ``curl`` + The ``--resolve fastapi-hello-world:80:$INGRESS_IP`` option to the ``curl`` command is a way of resolving the hostname of the request without setting a DNS record. @@ -553,7 +555,7 @@ Check that the response is still ``{"message":"Hello, world!"}`` using: .. code-block:: bash - curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1 + curl http://fastapi-hello-world --resolve fastapi-hello-world:80:$INGRESS_IP Now let's change the greeting: @@ -567,7 +569,7 @@ Wait for the app to restart, then check the response: .. code-block:: bash - curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1 + curl http://fastapi-hello-world --resolve fastapi-hello-world:80:$INGRESS_IP The response should now return the updated ``{"message":"Hi!"}`` greeting. @@ -696,7 +698,7 @@ Send a request to the endpoint: .. code-block:: bash - curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1 + curl http://fastapi-hello-world --resolve fastapi-hello-world:80:$INGRESS_IP It should still return the ``{"message":"Hi!"}`` greeting. @@ -704,7 +706,7 @@ Check the local visitors: .. code-block:: bash - curl http://fastapi-hello-world/visitors --resolve fastapi-hello-world:80:127.0.0.1 + curl http://fastapi-hello-world/visitors --resolve fastapi-hello-world:80:$INGRESS_IP This request should return ``{"count":1}`` after the previous request to the root endpoint. This should @@ -716,7 +718,7 @@ this process, the output should be as follows: :host: charm-dev :dir: ~/fastapi-hello-world/charm - curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1 + curl http://fastapi-hello-world --resolve fastapi-hello-world:80:$INGRESS_IP {"message":"Hi!"} @@ -725,7 +727,7 @@ this process, the output should be as follows: :host: charm-dev :dir: ~/fastapi-hello-world/charm - curl http://fastapi-hello-world/visitors --resolve fastapi-hello-world:80:127.0.0.1 + curl http://fastapi-hello-world/visitors --resolve fastapi-hello-world:80:$INGRESS_IP {"count":2} diff --git a/docs/tutorial/kubernetes-charm-flask.rst b/docs/tutorial/kubernetes-charm-flask.rst index ccf686540..acdead41b 100644 --- a/docs/tutorial/kubernetes-charm-flask.rst +++ b/docs/tutorial/kubernetes-charm-flask.rst @@ -394,13 +394,15 @@ Test the deployment by sending a request via the ingress: .. code-block:: bash - curl http://flask-hello-world --resolve flask-hello-world:80:127.0.0.1 + # Canonical Kubernetes serves the ingress at the VM's address + INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') + curl http://flask-hello-world --resolve flask-hello-world:80:$INGRESS_IP The request should return the ``Hello, world!`` greeting. .. note:: - The ``--resolve flask-hello-world:80:127.0.0.1`` option to the ``curl`` + The ``--resolve flask-hello-world:80:$INGRESS_IP`` option to the ``curl`` command is a way of resolving the hostname of the request without setting a DNS record. @@ -506,7 +508,7 @@ Check that the response is still ``Hello, world!`` using: .. code-block:: bash - curl http://flask-hello-world --resolve flask-hello-world:80:127.0.0.1 + curl http://flask-hello-world --resolve flask-hello-world:80:$INGRESS_IP Now let's change the greeting: @@ -520,7 +522,7 @@ Wait for the app to restart, then check the response: .. code-block:: bash - curl http://flask-hello-world --resolve flask-hello-world:80:127.0.0.1 + curl http://flask-hello-world --resolve flask-hello-world:80:$INGRESS_IP The response should now return the updated ``Hi!`` greeting. @@ -648,7 +650,7 @@ Send a request to the endpoint: .. code-block:: bash - curl http://flask-hello-world --resolve flask-hello-world:80:127.0.0.1 + curl http://flask-hello-world --resolve flask-hello-world:80:$INGRESS_IP It should still return the ``Hi!`` greeting. @@ -656,7 +658,7 @@ Check the total number of visitors: .. code-block:: bash - curl http://flask-hello-world/visitors --resolve flask-hello-world:80:127.0.0.1 + curl http://flask-hello-world/visitors --resolve flask-hello-world:80:$INGRESS_IP This request should return ``1`` after the previous request to the root endpoint. This should be incremented each time the root endpoint is requested. If we @@ -667,7 +669,7 @@ repeat this process, the output should be as follows: :host: charm-dev :dir: ~/flask-hello-world/charm - curl http://flask-hello-world --resolve flask-hello-world:80:127.0.0.1 + curl http://flask-hello-world --resolve flask-hello-world:80:$INGRESS_IP Hi! @@ -676,7 +678,7 @@ repeat this process, the output should be as follows: :host: charm-dev :dir: ~/flask-hello-world/charm - curl http://flask-hello-world/visitors --resolve flask-hello-world:80:127.0.0.1 + curl http://flask-hello-world/visitors --resolve flask-hello-world:80:$INGRESS_IP 2 diff --git a/docs/tutorial/kubernetes-charm-go.rst b/docs/tutorial/kubernetes-charm-go.rst index 1d36ac9f2..19546648a 100644 --- a/docs/tutorial/kubernetes-charm-go.rst +++ b/docs/tutorial/kubernetes-charm-go.rst @@ -427,13 +427,15 @@ Send a request via the ingress: .. code-block:: bash - curl http://go-hello-world --resolve go-hello-world:80:127.0.0.1 + # Canonical Kubernetes serves the ingress at the VM's address + INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') + curl http://go-hello-world --resolve go-hello-world:80:$INGRESS_IP The request should return the ``Hello, world!`` greeting. .. note:: - The ``--resolve go-hello-world:80:127.0.0.1`` option to the ``curl`` + The ``--resolve go-hello-world:80:$INGRESS_IP`` option to the ``curl`` command is a way of resolving the hostname of the request without setting a DNS record. @@ -540,7 +542,7 @@ Check that the response is still ``Hello, world!`` using: .. code-block:: bash - curl http://go-hello-world --resolve go-hello-world:80:127.0.0.1 + curl http://go-hello-world --resolve go-hello-world:80:$INGRESS_IP Now let's change the greeting: @@ -554,7 +556,7 @@ Wait for the app to restart, then check the response: .. code-block:: bash - curl http://go-hello-world --resolve go-hello-world:80:127.0.0.1 + curl http://go-hello-world --resolve go-hello-world:80:$INGRESS_IP The response should now return the updated ``Hi!`` greeting. @@ -713,7 +715,7 @@ Send a request to the endpoint: .. code-block:: bash - curl http://go-hello-world --resolve go-hello-world:80:127.0.0.1 + curl http://go-hello-world --resolve go-hello-world:80:$INGRESS_IP It should still return the ``Hi!`` greeting. @@ -721,7 +723,7 @@ Check the local visitors: .. code-block:: bash - curl http://go-hello-world/visitors --resolve go-hello-world:80:127.0.0.1 + curl http://go-hello-world/visitors --resolve go-hello-world:80:$INGRESS_IP This request should return ``Number of visitors 1`` after the previous request to the root endpoint. @@ -733,7 +735,7 @@ repeat this process, the output should be as follows: :host: charm-dev :dir: ~/go-hello-world/charm - curl http://go-hello-world --resolve go-hello-world:80:127.0.0.1 + curl http://go-hello-world --resolve go-hello-world:80:$INGRESS_IP Hi! @@ -742,7 +744,7 @@ repeat this process, the output should be as follows: :host: charm-dev :dir: ~/go-hello-world/charm - curl http://go-hello-world/visitors --resolve go-hello-world:80:127.0.0.1 + curl http://go-hello-world/visitors --resolve go-hello-world:80:$INGRESS_IP Number of visitors 2 diff --git a/docs/tutorial/kubernetes-charm-spring-boot.rst b/docs/tutorial/kubernetes-charm-spring-boot.rst index 2b29283f4..148b89993 100644 --- a/docs/tutorial/kubernetes-charm-spring-boot.rst +++ b/docs/tutorial/kubernetes-charm-spring-boot.rst @@ -430,13 +430,15 @@ Send a request via the ingress: .. code-block:: bash - curl http://spring-boot-hello-world --resolve spring-boot-hello-world:80:127.0.0.1 + # Canonical Kubernetes serves the ingress at the VM's address + INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') + curl http://spring-boot-hello-world --resolve spring-boot-hello-world:80:$INGRESS_IP The request should return the ``Hello, world!`` greeting. .. note:: - The ``--resolve spring-boot-hello-world:80:127.0.0.1`` option to the ``curl`` + The ``--resolve spring-boot-hello-world:80:$INGRESS_IP`` option to the ``curl`` command is a way of resolving the hostname of the request without setting a DNS record. @@ -545,7 +547,7 @@ Check that the response is still ``Hello, world!`` using: .. code-block:: bash - curl http://spring-boot-hello-world --resolve spring-boot-hello-world:80:127.0.0.1`` + curl http://spring-boot-hello-world --resolve spring-boot-hello-world:80:$INGRESS_IP`` Now let's change the greeting: @@ -559,7 +561,7 @@ Wait for the app to restart, then check the response: .. code-block:: bash - curl http://spring-boot-hello-world --resolve spring-boot-hello-world:80:127.0.0.1 + curl http://spring-boot-hello-world --resolve spring-boot-hello-world:80:$INGRESS_IP The response should now return the updated ``Hi!`` greeting. @@ -725,7 +727,7 @@ Send a request to the endpoint: .. code-block:: bash - curl http://spring-boot-hello-world --resolve spring-boot-hello-world:80:127.0.0.1 + curl http://spring-boot-hello-world --resolve spring-boot-hello-world:80:$INGRESS_IP It should still return the ``Hi!`` greeting. @@ -733,7 +735,7 @@ Check the local visitors: .. code-block:: bash - curl http://spring-boot-hello-world/visitors --resolve spring-boot-hello-world:80:127.0.0.1 + curl http://spring-boot-hello-world/visitors --resolve spring-boot-hello-world:80:$INGRESS_IP This request should return ``Number of visitors 1`` after the previous request to the root endpoint. @@ -745,7 +747,7 @@ repeat this process, the output should be as follows: :host: charm-dev :dir: ~/spring-boot-hello-world/charm - curl http://spring-boot-hello-world --resolve spring-boot-hello-world:80:127.0.0.1 + curl http://spring-boot-hello-world --resolve spring-boot-hello-world:80:$INGRESS_IP Hi! @@ -754,7 +756,7 @@ repeat this process, the output should be as follows: :host: charm-dev :dir: ~/spring-boot-hello-world/charm - curl http://spring-boot-hello-world/visitors --resolve spring-boot-hello-world:80:127.0.0.1 + curl http://spring-boot-hello-world/visitors --resolve spring-boot-hello-world:80:$INGRESS_IP Number of visitors 2 diff --git a/spread.yaml b/spread.yaml index 78d070472..40a1d4771 100644 --- a/spread.yaml +++ b/spread.yaml @@ -350,18 +350,13 @@ suites: # Unlike the MicroK8s ingress addon (which binds the node's host ports), the # Canonical Kubernetes ingress is exposed through a LoadBalancer service, so a # load balancer with an address pool is required for the ingress to be reachable. - IPADDR=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') - k8s set load-balancer.l2-mode=true load-balancer.cidrs="$IPADDR-$IPADDR" k8s enable load-balancer + IPADDR=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') + k8s set load-balancer.l2-mode=true load-balancer.cidrs="$IPADDR/32" k8s status --wait-ready --timeout 5m k8s kubectl wait --for=condition=Ready pods --all -n kube-system --timeout=10m - # The tutorial reaches the app at 127.0.0.1; redirect loopback :80/:443 to the - # load balancer address so the documented commands work in the test suite. - iptables -t nat -A OUTPUT -p tcp -d 127.0.0.1 --dport 80 -j DNAT --to-destination "$IPADDR:80" - iptables -t nat -A OUTPUT -p tcp -d 127.0.0.1 --dport 443 -j DNAT --to-destination "$IPADDR:443" - # Canonical Kubernetes isn't autodetected by Juju, so register it as a cloud. k8s config | juju add-k8s k8s-cloud --client juju bootstrap k8s-cloud dev-controller --config bootstrap-timeout=500 From ff9abce7ad3146246818666ba128c9bbf4318a0c Mon Sep 17 00:00:00 2001 From: florentianayuwono <76247368+florentianayuwono@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:12:44 +0700 Subject: [PATCH 06/28] Apply batched suggestions from code review Co-authored-by: florentianayuwono <76247368+florentianayuwono@users.noreply.github.com> Signed-off-by: florentianayuwono <76247368+florentianayuwono@users.noreply.github.com> --- docs/reuse/tutorial/setup_edge.rst | 5 ++--- docs/reuse/tutorial/setup_stable.rst | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/reuse/tutorial/setup_edge.rst b/docs/reuse/tutorial/setup_edge.rst index b81c05907..7cf805f08 100644 --- a/docs/reuse/tutorial/setup_edge.rst +++ b/docs/reuse/tutorial/setup_edge.rst @@ -72,7 +72,6 @@ Enable the necessary features: sudo k8s enable local-storage sudo k8s enable ingress -Unlike the MicroK8s ingress add-on, which binds the node's ports directly, Canonical Kubernetes exposes ingress through a load-balancer service. Enable the built-in load balancer and point its address pool at the VM's own IP so the ingress is assigned a reachable address: @@ -95,8 +94,8 @@ along with a list of enabled and disabled features. Set up a local registry ~~~~~~~~~~~~~~~~~~~~~~~~~ -Canonical Kubernetes pulls application images from an OCI registry. Unlike -MicroK8s, it doesn't bundle a registry, so we run a small local one. Download +Canonical Kubernetes pulls application images from an OCI registry. +It doesn't bundle a registry, so we run a small local one. Download the registry and start it as a service: .. code-block:: bash diff --git a/docs/reuse/tutorial/setup_stable.rst b/docs/reuse/tutorial/setup_stable.rst index 5c2a4fd72..d6542028a 100644 --- a/docs/reuse/tutorial/setup_stable.rst +++ b/docs/reuse/tutorial/setup_stable.rst @@ -68,7 +68,6 @@ Enable the necessary features: sudo k8s enable local-storage sudo k8s enable ingress -Unlike the MicroK8s ingress add-on, which binds the node's ports directly, Canonical Kubernetes exposes ingress through a load-balancer service. Enable the built-in load balancer and point its address pool at the VM's own IP so the ingress is assigned a reachable address: @@ -91,8 +90,8 @@ along with a list of enabled and disabled features. Set up a local registry ~~~~~~~~~~~~~~~~~~~~~~~~~ -Canonical Kubernetes pulls application images from an OCI registry. Unlike -MicroK8s, it doesn't bundle a registry, so we run a small local one. Download +Canonical Kubernetes pulls application images from an OCI registry. +It doesn't bundle a registry, so we run a small local one. Download the registry and start it as a service: .. code-block:: bash From f2d9a1f04eb4685dddc58264ece8ff505dedbd21 Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Mon, 12 Oct 2026 20:16:41 +0700 Subject: [PATCH 07/28] docs: fix Spring Boot curl backtick and add release note Remove a stray trailing double-backtick from a `curl` command in the Spring Boot tutorial so the rendered code block can be copied verbatim. Add a release note for the 12-factor tutorial migration to Canonical Kubernetes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/release-notes/charmcraft-4.5.rst | 9 +++++++++ docs/tutorial/kubernetes-charm-spring-boot.rst | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/charmcraft-4.5.rst b/docs/release-notes/charmcraft-4.5.rst index 0bc41b78b..ccfe4538a 100644 --- a/docs/release-notes/charmcraft-4.5.rst +++ b/docs/release-notes/charmcraft-4.5.rst @@ -108,6 +108,15 @@ Minor init profile improvements - Charms created with the ``kubernetes`` profile now use an Ubuntu image as a placeholder for the charm's real image, so that integration tests pass by default. +Documentation improvements +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Charmcraft 4.5 includes the following documentation changes: + +- The 12-factor app :ref:`tutorials ` now use Canonical Kubernetes + instead of MicroK8s, including a local OCI registry and load-balancer-backed + ingress. + Backwards-incompatible changes ------------------------------ diff --git a/docs/tutorial/kubernetes-charm-spring-boot.rst b/docs/tutorial/kubernetes-charm-spring-boot.rst index 148b89993..09f5d8e11 100644 --- a/docs/tutorial/kubernetes-charm-spring-boot.rst +++ b/docs/tutorial/kubernetes-charm-spring-boot.rst @@ -547,7 +547,7 @@ Check that the response is still ``Hello, world!`` using: .. code-block:: bash - curl http://spring-boot-hello-world --resolve spring-boot-hello-world:80:$INGRESS_IP`` + curl http://spring-boot-hello-world --resolve spring-boot-hello-world:80:$INGRESS_IP Now let's change the greeting: From 9afaba982409b161c857af444d23596f5faaa6fa Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Tue, 20 Oct 2026 02:18:04 +0700 Subject: [PATCH 08/28] docs: use in-cluster registry and robust load-balancer setup for CK8s tutorials Replace the static local-binary OCI registry and containerd hosts.d configuration with an in-cluster NodePort registry exposed on the node at localhost:32000, mirroring the MicroK8s registry addon. Wait for the load-balancer (MetalLB) webhook to become ready before configuring its address pool to avoid a validation-webhook race, and reuse a single INGRESS_IP variable defined during setup across the tutorial curls. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/reuse/tutorial/setup_edge.rst | 94 ++++++++++-------- docs/reuse/tutorial/setup_stable.rst | 94 ++++++++++-------- docs/tutorial/code/django/task.yaml | 12 +-- docs/tutorial/code/expressjs/task.yaml | 12 +-- docs/tutorial/code/fastapi/task.yaml | 12 +-- docs/tutorial/code/flask/task.yaml | 12 +-- docs/tutorial/code/go/task.yaml | 12 +-- docs/tutorial/code/spring-boot/task.yaml | 12 +-- docs/tutorial/kubernetes-charm-django.rst | 5 +- docs/tutorial/kubernetes-charm-express.rst | 5 +- docs/tutorial/kubernetes-charm-fastapi.rst | 5 +- docs/tutorial/kubernetes-charm-flask.rst | 5 +- docs/tutorial/kubernetes-charm-go.rst | 5 +- .../tutorial/kubernetes-charm-spring-boot.rst | 5 +- spread.yaml | 96 ++++++++++--------- 15 files changed, 204 insertions(+), 182 deletions(-) diff --git a/docs/reuse/tutorial/setup_edge.rst b/docs/reuse/tutorial/setup_edge.rst index 7cf805f08..103e2eee4 100644 --- a/docs/reuse/tutorial/setup_edge.rst +++ b/docs/reuse/tutorial/setup_edge.rst @@ -73,14 +73,16 @@ Enable the necessary features: sudo k8s enable ingress Canonical Kubernetes exposes ingress through a load-balancer service. Enable -the built-in load balancer and point its address pool at the VM's own IP so -the ingress is assigned a reachable address: +the built-in load balancer, wait for it to become ready, then point its +address pool at the VM's own IP so the ingress is assigned a reachable +address: .. code-block:: bash sudo k8s enable load-balancer - VM_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') - sudo k8s set load-balancer.l2-mode=true load-balancer.cidrs="$VM_IP/32" + sudo k8s status --wait-ready + INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') + sudo k8s set load-balancer.l2-mode=true load-balancer.cidrs="$INGRESS_IP/32" Check the status of Canonical Kubernetes: @@ -94,57 +96,65 @@ along with a list of enabled and disabled features. Set up a local registry ~~~~~~~~~~~~~~~~~~~~~~~~~ -Canonical Kubernetes pulls application images from an OCI registry. -It doesn't bundle a registry, so we run a small local one. Download -the registry and start it as a service: +Canonical Kubernetes pulls application images from an OCI registry. Deploy a +small registry inside the cluster, exposed on the node at ``localhost:32000``: .. code-block:: bash - ARCH=$(dpkg --print-architecture) - sudo curl -sSL -o /tmp/registry.tar.gz \ - "https://github.com/distribution/distribution/releases/download/v2.8.3/registry_2.8.3_linux_${ARCH}.tar.gz" - sudo tar -xzf /tmp/registry.tar.gz -C /usr/local/bin registry - sudo mkdir -p /etc/distribution - sudo tee /etc/distribution/config.yml > /dev/null <<'EOF' - version: 0.1 - storage: - filesystem: - rootdirectory: /var/lib/registry - http: - addr: :5000 + sudo k8s kubectl apply -f - <<'EOF' + apiVersion: v1 + kind: Namespace + metadata: + name: container-registry + --- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: registry + namespace: container-registry + spec: + replicas: 1 + selector: + matchLabels: + app: registry + template: + metadata: + labels: + app: registry + spec: + containers: + - name: registry + image: registry:2.8.3 + ports: + - containerPort: 5000 + --- + apiVersion: v1 + kind: Service + metadata: + name: registry + namespace: container-registry + spec: + type: NodePort + selector: + app: registry + ports: + - port: 5000 + targetPort: 5000 + nodePort: 32000 EOF - sudo tee /etc/systemd/system/registry.service > /dev/null <<'EOF' - [Unit] - Description=OCI registry - After=network.target - [Service] - ExecStart=/usr/local/bin/registry serve /etc/distribution/config.yml - Restart=always - - [Install] - WantedBy=multi-user.target - EOF - sudo systemctl enable --now registry - -Finally, let Canonical Kubernetes pull from this registry over plain HTTP by -adding a containerd hosts configuration: +Wait for the registry to become available: .. code-block:: bash - sudo mkdir -p /etc/containerd/hosts.d/localhost:5000 - sudo tee /etc/containerd/hosts.d/localhost:5000/hosts.toml > /dev/null <<'EOF' - server = "http://localhost:5000" - - [host."http://localhost:5000"] - capabilities = ["pull", "resolve"] - EOF + sudo k8s kubectl rollout status deployment/registry \ + -n container-registry --timeout=5m Juju is required to deploy the |12FactorApp| application. We'll install Juju using the ``3.6/stable`` channel. Since the snap is sandboxed, we'll also manually create a directory to contain its files. Canonical Kubernetes isn't automatically registered as a Juju -cloud, so we add it before bootstrapping a development controller: +cloud, so let's add it before bootstrapping a development controller: .. code-block:: text diff --git a/docs/reuse/tutorial/setup_stable.rst b/docs/reuse/tutorial/setup_stable.rst index d6542028a..06425f510 100644 --- a/docs/reuse/tutorial/setup_stable.rst +++ b/docs/reuse/tutorial/setup_stable.rst @@ -69,14 +69,16 @@ Enable the necessary features: sudo k8s enable ingress Canonical Kubernetes exposes ingress through a load-balancer service. Enable -the built-in load balancer and point its address pool at the VM's own IP so -the ingress is assigned a reachable address: +the built-in load balancer, wait for it to become ready, then point its +address pool at the VM's own IP so the ingress is assigned a reachable +address: .. code-block:: bash sudo k8s enable load-balancer - VM_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') - sudo k8s set load-balancer.l2-mode=true load-balancer.cidrs="$VM_IP/32" + sudo k8s status --wait-ready + INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') + sudo k8s set load-balancer.l2-mode=true load-balancer.cidrs="$INGRESS_IP/32" Check the status of Canonical Kubernetes: @@ -90,57 +92,65 @@ along with a list of enabled and disabled features. Set up a local registry ~~~~~~~~~~~~~~~~~~~~~~~~~ -Canonical Kubernetes pulls application images from an OCI registry. -It doesn't bundle a registry, so we run a small local one. Download -the registry and start it as a service: +Canonical Kubernetes pulls application images from an OCI registry. Deploy a +small registry inside the cluster, exposed on the node at ``localhost:32000``: .. code-block:: bash - ARCH=$(dpkg --print-architecture) - sudo curl -sSL -o /tmp/registry.tar.gz \ - "https://github.com/distribution/distribution/releases/download/v2.8.3/registry_2.8.3_linux_${ARCH}.tar.gz" - sudo tar -xzf /tmp/registry.tar.gz -C /usr/local/bin registry - sudo mkdir -p /etc/distribution - sudo tee /etc/distribution/config.yml > /dev/null <<'EOF' - version: 0.1 - storage: - filesystem: - rootdirectory: /var/lib/registry - http: - addr: :5000 + sudo k8s kubectl apply -f - <<'EOF' + apiVersion: v1 + kind: Namespace + metadata: + name: container-registry + --- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: registry + namespace: container-registry + spec: + replicas: 1 + selector: + matchLabels: + app: registry + template: + metadata: + labels: + app: registry + spec: + containers: + - name: registry + image: registry:2.8.3 + ports: + - containerPort: 5000 + --- + apiVersion: v1 + kind: Service + metadata: + name: registry + namespace: container-registry + spec: + type: NodePort + selector: + app: registry + ports: + - port: 5000 + targetPort: 5000 + nodePort: 32000 EOF - sudo tee /etc/systemd/system/registry.service > /dev/null <<'EOF' - [Unit] - Description=OCI registry - After=network.target - [Service] - ExecStart=/usr/local/bin/registry serve /etc/distribution/config.yml - Restart=always - - [Install] - WantedBy=multi-user.target - EOF - sudo systemctl enable --now registry - -Finally, let Canonical Kubernetes pull from this registry over plain HTTP by -adding a containerd hosts configuration: +Wait for the registry to become available: .. code-block:: bash - sudo mkdir -p /etc/containerd/hosts.d/localhost:5000 - sudo tee /etc/containerd/hosts.d/localhost:5000/hosts.toml > /dev/null <<'EOF' - server = "http://localhost:5000" - - [host."http://localhost:5000"] - capabilities = ["pull", "resolve"] - EOF + sudo k8s kubectl rollout status deployment/registry \ + -n container-registry --timeout=5m Juju is required to deploy the |12FactorApp| application. We'll install Juju using the ``3.6/stable`` channel. Since the snap is sandboxed, we'll also manually create a directory to contain its files. Canonical Kubernetes isn't automatically registered as a Juju -cloud, so we add it before bootstrapping a development controller: +cloud, so let's add it before bootstrapping a development controller: .. code-block:: text diff --git a/docs/tutorial/code/django/task.yaml b/docs/tutorial/code/django/task.yaml index 4e5c5e873..48f1cfc51 100644 --- a/docs/tutorial/code/django/task.yaml +++ b/docs/tutorial/code/django/task.yaml @@ -80,7 +80,7 @@ execute: | # [docs:skopeo-copy] rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ oci-archive:django-hello-world_0.1_$(dpkg --print-architecture).rock \ - docker://localhost:5000/django-hello-world:0.1 + docker://localhost:32000/django-hello-world:0.1 # [docs:skopeo-copy-end] # [docs:create-charm-dir] @@ -112,7 +112,7 @@ execute: | juju deploy \ ./django-hello-world_$(dpkg --print-architecture).charm \ django-hello-world --resource \ - django-app-image=localhost:5000/django-hello-world:0.1 + django-app-image=localhost:32000/django-hello-world:0.1 # [docs:deploy-django-app-end] # [docs:deploy-postgres] @@ -178,14 +178,14 @@ execute: | rockcraft pack rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ oci-archive:django-hello-world_0.2_$(dpkg --print-architecture).rock \ - docker://localhost:5000/django-hello-world:0.2 + docker://localhost:32000/django-hello-world:0.2 # [docs:repack-update-end] # [docs:refresh-deployment] cd charm juju refresh django-hello-world \ --path=./django-hello-world_$(dpkg --print-architecture).charm \ - --resource django-app-image=localhost:5000/django-hello-world:0.2 + --resource django-app-image=localhost:32000/django-hello-world:0.2 # [docs:refresh-deployment-end] # [docs:disable-debug-mode] @@ -208,7 +208,7 @@ execute: | rockcraft pack rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ oci-archive:django-hello-world_0.3_$(dpkg --print-architecture).rock \ - docker://localhost:5000/django-hello-world:0.3 + docker://localhost:32000/django-hello-world:0.3 # [docs:repack-2nd-update-end] cd charm @@ -220,7 +220,7 @@ execute: | charmcraft pack juju refresh django-hello-world \ --path=./django-hello-world_$(dpkg --print-architecture).charm \ - --resource django-app-image=localhost:5000/django-hello-world:0.3 + --resource django-app-image=localhost:32000/django-hello-world:0.3 # [docs:repack-refresh-2nd-deployment-end] # Wait for django-hello-world to be active diff --git a/docs/tutorial/code/expressjs/task.yaml b/docs/tutorial/code/expressjs/task.yaml index b218d4c21..f12f9cc30 100644 --- a/docs/tutorial/code/expressjs/task.yaml +++ b/docs/tutorial/code/expressjs/task.yaml @@ -74,7 +74,7 @@ execute: | # [docs:skopeo-copy] rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ oci-archive:expressjs-hello-world_0.1_$(dpkg --print-architecture).rock \ - docker://localhost:5000/expressjs-hello-world:0.1 + docker://localhost:32000/expressjs-hello-world:0.1 # [docs:skopeo-copy-end] # [docs:create-charm-dir] @@ -106,7 +106,7 @@ execute: | juju deploy \ ./expressjs-hello-world_$(dpkg --print-architecture).charm \ expressjs-hello-world \ - --resource app-image=localhost:5000/expressjs-hello-world:0.1 + --resource app-image=localhost:32000/expressjs-hello-world:0.1 # [docs:deploy-expressjs-app-end] # [docs:deploy-nginx] @@ -139,7 +139,7 @@ execute: | rockcraft pack rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ oci-archive:expressjs-hello-world_0.2_$(dpkg --print-architecture).rock \ - docker://localhost:5000/expressjs-hello-world:0.2 + docker://localhost:32000/expressjs-hello-world:0.2 # [docs:docker-update-end] cat greeting_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -149,7 +149,7 @@ execute: | charmcraft pack juju refresh expressjs-hello-world \ --path=./expressjs-hello-world_$(dpkg --print-architecture).charm \ - --resource app-image=localhost:5000/expressjs-hello-world:0.2 + --resource app-image=localhost:32000/expressjs-hello-world:0.2 # [docs:refresh-deployment-end] # give Juju some time to refresh the app @@ -196,7 +196,7 @@ execute: | rockcraft pack rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ oci-archive:expressjs-hello-world_0.3_$(dpkg --print-architecture).rock \ - docker://localhost:5000/expressjs-hello-world:0.3 + docker://localhost:32000/expressjs-hello-world:0.3 # [docs:docker-2nd-update-end] cat visitors_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -206,7 +206,7 @@ execute: | charmcraft pack juju refresh expressjs-hello-world \ --path=./expressjs-hello-world_$(dpkg --print-architecture).charm \ - --resource app-image=localhost:5000/expressjs-hello-world:0.3 + --resource app-image=localhost:32000/expressjs-hello-world:0.3 # [docs:refresh-2nd-deployment-end] # In the relation with postgresql-k8s test we may hit the issue diff --git a/docs/tutorial/code/fastapi/task.yaml b/docs/tutorial/code/fastapi/task.yaml index 3dd6f19c0..bbb6878d4 100644 --- a/docs/tutorial/code/fastapi/task.yaml +++ b/docs/tutorial/code/fastapi/task.yaml @@ -60,7 +60,7 @@ execute: | # [docs:skopeo-copy] rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ oci-archive:fastapi-hello-world_0.1_$(dpkg --print-architecture).rock \ - docker://localhost:5000/fastapi-hello-world:0.1 + docker://localhost:32000/fastapi-hello-world:0.1 # [docs:skopeo-copy-end] # [docs:create-charm-dir] @@ -92,7 +92,7 @@ execute: | juju deploy \ ./fastapi-hello-world_$(dpkg --print-architecture).charm \ fastapi-hello-world --resource \ - app-image=localhost:5000/fastapi-hello-world:0.1 + app-image=localhost:32000/fastapi-hello-world:0.1 # [docs:deploy-fastapi-app-end] # [docs:deploy-nginx] @@ -126,7 +126,7 @@ execute: | rockcraft pack rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ oci-archive:fastapi-hello-world_0.2_$(dpkg --print-architecture).rock \ - docker://localhost:5000/fastapi-hello-world:0.2 + docker://localhost:32000/fastapi-hello-world:0.2 # [docs:docker-update-end] cat greeting_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -136,7 +136,7 @@ execute: | charmcraft pack juju refresh fastapi-hello-world \ --path=./fastapi-hello-world_$(dpkg --print-architecture).charm \ - --resource app-image=localhost:5000/fastapi-hello-world:0.2 + --resource app-image=localhost:32000/fastapi-hello-world:0.2 # [docs:refresh-deployment-end] # give Juju some time to refresh the app @@ -164,7 +164,7 @@ execute: | rockcraft pack rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ oci-archive:fastapi-hello-world_0.3_$(dpkg --print-architecture).rock \ - docker://localhost:5000/fastapi-hello-world:0.3 + docker://localhost:32000/fastapi-hello-world:0.3 # [docs:docker-2nd-update-end] cat visitors_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -174,7 +174,7 @@ execute: | charmcraft pack juju refresh fastapi-hello-world \ --path=./fastapi-hello-world_$(dpkg --print-architecture).charm \ - --resource app-image=localhost:5000/fastapi-hello-world:0.3 + --resource app-image=localhost:32000/fastapi-hello-world:0.3 # [docs:refresh-2nd-deployment-end] # In the relation with postgresql-k8s test we may hit, so we have to wait before integrating. diff --git a/docs/tutorial/code/flask/task.yaml b/docs/tutorial/code/flask/task.yaml index 173754f6f..d363278fd 100644 --- a/docs/tutorial/code/flask/task.yaml +++ b/docs/tutorial/code/flask/task.yaml @@ -60,7 +60,7 @@ execute: | # [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:5000/flask-hello-world:0.1 + docker://localhost:32000/flask-hello-world:0.1 # [docs:skopeo-copy-end] # [docs:create-charm-dir] @@ -88,7 +88,7 @@ execute: | juju deploy \ ./flask-hello-world_$(dpkg --print-architecture).charm \ flask-hello-world --resource \ - flask-app-image=localhost:5000/flask-hello-world:0.1 + flask-app-image=localhost:32000/flask-hello-world:0.1 # [docs:deploy-flask-app-end] # [docs:deploy-nginx] @@ -122,7 +122,7 @@ execute: | rockcraft pack rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ oci-archive:flask-hello-world_0.2_$(dpkg --print-architecture).rock \ - docker://localhost:5000/flask-hello-world:0.2 + docker://localhost:32000/flask-hello-world:0.2 # [docs:docker-update-end] cat greeting_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -132,7 +132,7 @@ execute: | charmcraft pack juju refresh flask-hello-world \ --path=./flask-hello-world_$(dpkg --print-architecture).charm \ - --resource flask-app-image=localhost:5000/flask-hello-world:0.2 + --resource flask-app-image=localhost:32000/flask-hello-world:0.2 # [docs:refresh-deployment-end] # give Juju some time to refresh the app @@ -160,7 +160,7 @@ execute: | rockcraft pack rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ oci-archive:flask-hello-world_0.3_$(dpkg --print-architecture).rock \ - docker://localhost:5000/flask-hello-world:0.3 + docker://localhost:32000/flask-hello-world:0.3 # [docs:docker-2nd-update-end] cat visitors_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -170,7 +170,7 @@ execute: | charmcraft pack juju refresh flask-hello-world \ --path=./flask-hello-world_$(dpkg --print-architecture).charm \ - --resource flask-app-image=localhost:5000/flask-hello-world:0.3 + --resource flask-app-image=localhost:32000/flask-hello-world:0.3 # [docs:refresh-2nd-deployment-end] # In the relation with postgresql-k8s test we may hit, so we have to wait before integrating. diff --git a/docs/tutorial/code/go/task.yaml b/docs/tutorial/code/go/task.yaml index 5eb9000a2..fdddfd9cb 100644 --- a/docs/tutorial/code/go/task.yaml +++ b/docs/tutorial/code/go/task.yaml @@ -68,7 +68,7 @@ execute: | # [docs:skopeo-copy] rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ oci-archive:go-hello-world_0.1_$(dpkg --print-architecture).rock \ - docker://localhost:5000/go-hello-world:0.1 + docker://localhost:32000/go-hello-world:0.1 # [docs:skopeo-copy-end] # [docs:create-charm-dir] @@ -100,7 +100,7 @@ execute: | juju deploy \ ./go-hello-world_$(dpkg --print-architecture).charm \ go-hello-world --resource \ - app-image=localhost:5000/go-hello-world:0.1 + app-image=localhost:32000/go-hello-world:0.1 # [docs:deploy-go-app-end] # [docs:deploy-nginx] @@ -134,7 +134,7 @@ execute: | rockcraft pack rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ oci-archive:go-hello-world_0.2_$(dpkg --print-architecture).rock \ - docker://localhost:5000/go-hello-world:0.2 + docker://localhost:32000/go-hello-world:0.2 # [docs:docker-update-end] cat greeting_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -144,7 +144,7 @@ execute: | charmcraft pack juju refresh go-hello-world \ --path=./go-hello-world_$(dpkg --print-architecture).charm \ - --resource app-image=localhost:5000/go-hello-world:0.2 + --resource app-image=localhost:32000/go-hello-world:0.2 # [docs:refresh-deployment-end] # give Juju some time to refresh the app @@ -183,7 +183,7 @@ execute: | rockcraft pack rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ oci-archive:go-hello-world_0.3_$(dpkg --print-architecture).rock \ - docker://localhost:5000/go-hello-world:0.3 + docker://localhost:32000/go-hello-world:0.3 # [docs:docker-2nd-update-end] cat visitors_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -193,7 +193,7 @@ execute: | charmcraft pack juju refresh go-hello-world \ --path=./go-hello-world_$(dpkg --print-architecture).charm \ - --resource app-image=localhost:5000/go-hello-world:0.3 + --resource app-image=localhost:32000/go-hello-world:0.3 # [docs:refresh-2nd-deployment-end] # In the relation with postgresql-k8s test we may hit, so we have to wait before integrating. diff --git a/docs/tutorial/code/spring-boot/task.yaml b/docs/tutorial/code/spring-boot/task.yaml index efad32e30..5e9f7b04c 100644 --- a/docs/tutorial/code/spring-boot/task.yaml +++ b/docs/tutorial/code/spring-boot/task.yaml @@ -86,7 +86,7 @@ execute: | # [docs:skopeo-copy] rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ oci-archive:spring-boot-hello-world_0.1_$(dpkg --print-architecture).rock \ - docker://localhost:5000/spring-boot-hello-world:0.1 + docker://localhost:32000/spring-boot-hello-world:0.1 # [docs:skopeo-copy-end] # [docs:create-charm-dir] @@ -118,7 +118,7 @@ execute: | juju deploy \ ./spring-boot-hello-world_$(dpkg --print-architecture).charm \ spring-boot-hello-world --resource \ - app-image=localhost:5000/spring-boot-hello-world:0.1 + app-image=localhost:32000/spring-boot-hello-world:0.1 # [docs:deploy-app-end] # [docs:deploy-nginx] @@ -149,7 +149,7 @@ execute: | rockcraft pack rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ oci-archive:spring-boot-hello-world_0.2_$(dpkg --print-architecture).rock \ - docker://localhost:5000/spring-boot-hello-world:0.2 + docker://localhost:32000/spring-boot-hello-world:0.2 # [docs:skopeo-update-end] cat $HOME/tmp/greeting_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -159,7 +159,7 @@ execute: | charmcraft pack juju refresh spring-boot-hello-world \ --path=./spring-boot-hello-world_$(dpkg --print-architecture).charm \ - --resource app-image=localhost:5000/spring-boot-hello-world:0.2 + --resource app-image=localhost:32000/spring-boot-hello-world:0.2 # [docs:refresh-deployment-end] # give Juju some time to refresh the app @@ -208,7 +208,7 @@ execute: | rockcraft pack rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ oci-archive:spring-boot-hello-world_0.3_$(dpkg --print-architecture).rock \ - docker://localhost:5000/spring-boot-hello-world:0.3 + docker://localhost:32000/spring-boot-hello-world:0.3 # [docs:skopeo-2nd-update-end] cat $HOME/tmp/visitors_charmcraft.yaml >> ./charm/charmcraft.yaml @@ -218,7 +218,7 @@ execute: | charmcraft pack juju refresh spring-boot-hello-world \ --path=./spring-boot-hello-world_$(dpkg --print-architecture).charm \ - --resource app-image=localhost:5000/spring-boot-hello-world:0.3 + --resource app-image=localhost:32000/spring-boot-hello-world:0.3 # [docs:refresh-2nd-deployment-end] # In the relation with postgresql-k8s test we may hit the issue diff --git a/docs/tutorial/kubernetes-charm-django.rst b/docs/tutorial/kubernetes-charm-django.rst index b1721f45e..b2df24d24 100644 --- a/docs/tutorial/kubernetes-charm-django.rst +++ b/docs/tutorial/kubernetes-charm-django.rst @@ -684,12 +684,11 @@ Now that we have the greeting app, we can disable debug mode: Use ``juju status --watch 2s`` again to wait until the App is active again. You may visit http://django-hello-world from a web browser, or -inside the Multipass VM run: +inside the Multipass VM run the following, which reuses the ``INGRESS_IP`` +value you set during setup: .. code-block:: bash - # Canonical Kubernetes serves the ingress at the VM's address - INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') curl http://django-hello-world --resolve django-hello-world:80:$INGRESS_IP Either way, the Django app should respond with ``Hello, world!``. diff --git a/docs/tutorial/kubernetes-charm-express.rst b/docs/tutorial/kubernetes-charm-express.rst index 92835976f..5adbf20b0 100644 --- a/docs/tutorial/kubernetes-charm-express.rst +++ b/docs/tutorial/kubernetes-charm-express.rst @@ -408,12 +408,11 @@ the ingress. We will also set the default route to be the root endpoint: Monitor ``juju status`` until everything has a status of ``active``. -Send a request via the ingress: +Send a request via the ingress. The following commands reuse the +``INGRESS_IP`` value you set during setup: .. code-block:: bash - # Canonical Kubernetes serves the ingress at the VM's address - INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') curl -I http://expressjs-hello-world --resolve expressjs-hello-world:80:$INGRESS_IP It should show the ``HTTP/1.1 200 OK`` status message. diff --git a/docs/tutorial/kubernetes-charm-fastapi.rst b/docs/tutorial/kubernetes-charm-fastapi.rst index 9d7502299..286f4bd34 100644 --- a/docs/tutorial/kubernetes-charm-fastapi.rst +++ b/docs/tutorial/kubernetes-charm-fastapi.rst @@ -434,12 +434,11 @@ the ingress. We will also set the default route to be the root endpoint: Monitor ``juju status`` until everything has a status of ``active``. -Test the deployment by sending a request via the ingress: +Test the deployment by sending a request via the ingress. The following +commands reuse the ``INGRESS_IP`` value you set during setup: .. code-block:: bash - # Canonical Kubernetes serves the ingress at the VM's address - INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') curl http://fastapi-hello-world --resolve fastapi-hello-world:80:$INGRESS_IP It should return the ``{"message":"Hello, world!"}`` greeting. diff --git a/docs/tutorial/kubernetes-charm-flask.rst b/docs/tutorial/kubernetes-charm-flask.rst index acdead41b..1e56187cf 100644 --- a/docs/tutorial/kubernetes-charm-flask.rst +++ b/docs/tutorial/kubernetes-charm-flask.rst @@ -390,12 +390,11 @@ the ingress. We will also set the default route to be the root endpoint: Monitor ``juju status`` until everything has a status of ``active``. -Test the deployment by sending a request via the ingress: +Test the deployment by sending a request via the ingress. The following +commands reuse the ``INGRESS_IP`` value you set during setup: .. code-block:: bash - # Canonical Kubernetes serves the ingress at the VM's address - INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') curl http://flask-hello-world --resolve flask-hello-world:80:$INGRESS_IP The request should return the ``Hello, world!`` greeting. diff --git a/docs/tutorial/kubernetes-charm-go.rst b/docs/tutorial/kubernetes-charm-go.rst index 19546648a..f5f0bf6cc 100644 --- a/docs/tutorial/kubernetes-charm-go.rst +++ b/docs/tutorial/kubernetes-charm-go.rst @@ -423,12 +423,11 @@ the ingress. We will also set the default route to be the root endpoint: Monitor ``juju status`` until everything has a status of ``active``. -Send a request via the ingress: +Send a request via the ingress. The following commands reuse the +``INGRESS_IP`` value you set during setup: .. code-block:: bash - # Canonical Kubernetes serves the ingress at the VM's address - INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') curl http://go-hello-world --resolve go-hello-world:80:$INGRESS_IP The request should return the ``Hello, world!`` greeting. diff --git a/docs/tutorial/kubernetes-charm-spring-boot.rst b/docs/tutorial/kubernetes-charm-spring-boot.rst index 09f5d8e11..d6ef012ea 100644 --- a/docs/tutorial/kubernetes-charm-spring-boot.rst +++ b/docs/tutorial/kubernetes-charm-spring-boot.rst @@ -426,12 +426,11 @@ the ingress. We will also set the default route to be the root endpoint: Monitor ``juju status`` until everything has a status of ``active``. -Send a request via the ingress: +Send a request via the ingress. The following commands reuse the +``INGRESS_IP`` value you set during setup: .. code-block:: bash - # Canonical Kubernetes serves the ingress at the VM's address - INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') curl http://spring-boot-hello-world --resolve spring-boot-hello-world:80:$INGRESS_IP The request should return the ``Hello, world!`` greeting. diff --git a/spread.yaml b/spread.yaml index 40a1d4771..e1a05bde6 100644 --- a/spread.yaml +++ b/spread.yaml @@ -299,48 +299,6 @@ suites: refresh_or_install_snap k8s "$k8s_channel" --classic refresh_or_install_snap rockcraft latest/edge --classic - # The tutorial pushes the application image to a local OCI registry. Run it - # as a plain systemd service (no Docker, which would conflict with the k8s - # snap's own containerd), as done in the k8s-snap integration test harness. - ARCH=$(dpkg --print-architecture) - curl -sSL -o /tmp/registry.tar.gz \ - "https://github.com/distribution/distribution/releases/download/v2.8.3/registry_2.8.3_linux_${ARCH}.tar.gz" - tar -xzf /tmp/registry.tar.gz -C /usr/local/bin registry - mkdir -p /etc/distribution - cat <<'EOF' > /etc/distribution/config.yml - version: 0.1 - storage: - filesystem: - rootdirectory: /var/lib/registry - http: - addr: :5000 - EOF - cat <<'EOF' > /etc/systemd/system/registry.service - [Unit] - Description=OCI registry - After=network.target - - [Service] - ExecStart=/usr/local/bin/registry serve /etc/distribution/config.yml - Restart=always - - [Install] - WantedBy=multi-user.target - EOF - systemctl enable --now registry - retry -n 10 --wait 1 curl -sf http://localhost:5000/v2/ - - # Let Canonical Kubernetes pull from the insecure local registry. The k8s - # snap uses the standard containerd hosts.d path; it is read per-pull, so no - # restart is required. - mkdir -p "/etc/containerd/hosts.d/localhost:5000" - cat <<'EOF' > "/etc/containerd/hosts.d/localhost:5000/hosts.toml" - server = "http://localhost:5000" - - [host."http://localhost:5000"] - capabilities = ["pull", "resolve"] - EOF - k8s bootstrap k8s status --wait-ready --timeout 5m @@ -350,13 +308,63 @@ suites: # Unlike the MicroK8s ingress addon (which binds the node's host ports), the # Canonical Kubernetes ingress is exposed through a LoadBalancer service, so a # load balancer with an address pool is required for the ingress to be reachable. + # Wait for the load-balancer (MetalLB) webhook to come up before configuring + # the address pool, otherwise the "set" races the validating webhook. k8s enable load-balancer - IPADDR=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') - k8s set load-balancer.l2-mode=true load-balancer.cidrs="$IPADDR/32" + k8s status --wait-ready --timeout 5m + INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') + k8s set load-balancer.l2-mode=true load-balancer.cidrs="$INGRESS_IP/32" k8s status --wait-ready --timeout 5m k8s kubectl wait --for=condition=Ready pods --all -n kube-system --timeout=10m + # The tutorial pushes the application image to an in-cluster OCI registry + # exposed on the node at localhost:32000, mirroring the MicroK8s registry + # addon. Canonical Kubernetes' containerd trusts this node-local registry. + k8s kubectl apply -f - <<'EOF' + apiVersion: v1 + kind: Namespace + metadata: + name: container-registry + --- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: registry + namespace: container-registry + spec: + replicas: 1 + selector: + matchLabels: + app: registry + template: + metadata: + labels: + app: registry + spec: + containers: + - name: registry + image: registry:2.8.3 + ports: + - containerPort: 5000 + --- + apiVersion: v1 + kind: Service + metadata: + name: registry + namespace: container-registry + spec: + type: NodePort + selector: + app: registry + ports: + - port: 5000 + targetPort: 5000 + nodePort: 32000 + EOF + k8s kubectl rollout status deployment/registry -n container-registry --timeout=5m + retry -n 30 --wait 2 curl -sf http://localhost:32000/v2/ + # Canonical Kubernetes isn't autodetected by Juju, so register it as a cloud. k8s config | juju add-k8s k8s-cloud --client juju bootstrap k8s-cloud dev-controller --config bootstrap-timeout=500 From 55c58a1888bc970171d7f68da8ae6d3039a49e97 Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Tue, 20 Oct 2026 18:15:05 +0700 Subject: [PATCH 09/28] docs: wait for MetalLB controller rollout before setting LB address pool Waiting on `k8s status --wait-ready` does not guarantee that MetalLB's validating webhook is serving, so configuring the load-balancer address pool could still race the webhook and leave the feature in a Failed state. Wait for the metallb-controller deployment (which hosts the webhook) to roll out before setting the CIDRs, matching behaviour verified on a live Canonical Kubernetes cluster. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/reuse/tutorial/setup_edge.rst | 9 +++++---- docs/reuse/tutorial/setup_stable.rst | 9 +++++---- spread.yaml | 6 +++--- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/docs/reuse/tutorial/setup_edge.rst b/docs/reuse/tutorial/setup_edge.rst index 103e2eee4..85e511c21 100644 --- a/docs/reuse/tutorial/setup_edge.rst +++ b/docs/reuse/tutorial/setup_edge.rst @@ -73,14 +73,15 @@ Enable the necessary features: sudo k8s enable ingress Canonical Kubernetes exposes ingress through a load-balancer service. Enable -the built-in load balancer, wait for it to become ready, then point its -address pool at the VM's own IP so the ingress is assigned a reachable -address: +the built-in load balancer, wait for the MetalLB controller (which hosts the +load balancer's validating webhook) to roll out, then point its address pool +at the VM's own IP so the ingress is assigned a reachable address: .. code-block:: bash sudo k8s enable load-balancer - sudo k8s status --wait-ready + sudo k8s kubectl rollout status deployment/metallb-controller \ + -n metallb-system --timeout=5m INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') sudo k8s set load-balancer.l2-mode=true load-balancer.cidrs="$INGRESS_IP/32" diff --git a/docs/reuse/tutorial/setup_stable.rst b/docs/reuse/tutorial/setup_stable.rst index 06425f510..4ca3bf140 100644 --- a/docs/reuse/tutorial/setup_stable.rst +++ b/docs/reuse/tutorial/setup_stable.rst @@ -69,14 +69,15 @@ Enable the necessary features: sudo k8s enable ingress Canonical Kubernetes exposes ingress through a load-balancer service. Enable -the built-in load balancer, wait for it to become ready, then point its -address pool at the VM's own IP so the ingress is assigned a reachable -address: +the built-in load balancer, wait for the MetalLB controller (which hosts the +load balancer's validating webhook) to roll out, then point its address pool +at the VM's own IP so the ingress is assigned a reachable address: .. code-block:: bash sudo k8s enable load-balancer - sudo k8s status --wait-ready + sudo k8s kubectl rollout status deployment/metallb-controller \ + -n metallb-system --timeout=5m INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') sudo k8s set load-balancer.l2-mode=true load-balancer.cidrs="$INGRESS_IP/32" diff --git a/spread.yaml b/spread.yaml index e1a05bde6..d2d579caa 100644 --- a/spread.yaml +++ b/spread.yaml @@ -308,10 +308,10 @@ suites: # Unlike the MicroK8s ingress addon (which binds the node's host ports), the # Canonical Kubernetes ingress is exposed through a LoadBalancer service, so a # load balancer with an address pool is required for the ingress to be reachable. - # Wait for the load-balancer (MetalLB) webhook to come up before configuring - # the address pool, otherwise the "set" races the validating webhook. + # Wait for MetalLB's controller (which hosts the validating webhook) to roll out + # before configuring the address pool, otherwise the "set" races the webhook. k8s enable load-balancer - k8s status --wait-ready --timeout 5m + k8s kubectl rollout status deployment/metallb-controller -n metallb-system --timeout=5m INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') k8s set load-balancer.l2-mode=true load-balancer.cidrs="$INGRESS_IP/32" From 56b5c9a5fe6b4856eb4fa9472dd19b149c316f73 Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Tue, 20 Oct 2026 21:59:52 +0700 Subject: [PATCH 10/28] docs: target the cilium ingress class in CK8s tutorials Canonical Kubernetes exposes ingress through Cilium, whose ``cilium`` IngressClass is not the cluster default. Unlike the MicroK8s ingress addon, Cilium does not claim classless Ingress resources, so the nginx-ingress-integrator's Ingress was never assigned an address and the integrator stalled on "Waiting for ingress IP availability". Set ``ingress-class=cilium`` on the integrator so its Ingress targets the Cilium controller. Validated end to end on a clean Canonical Kubernetes VM. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/tutorial/code/django/task.yaml | 3 ++- docs/tutorial/code/expressjs/task.yaml | 3 ++- docs/tutorial/code/fastapi/task.yaml | 3 ++- docs/tutorial/code/flask/task.yaml | 3 ++- docs/tutorial/code/go/task.yaml | 3 ++- docs/tutorial/code/spring-boot/task.yaml | 3 ++- docs/tutorial/kubernetes-charm-django.rst | 3 ++- docs/tutorial/kubernetes-charm-express.rst | 3 ++- docs/tutorial/kubernetes-charm-fastapi.rst | 3 ++- docs/tutorial/kubernetes-charm-flask.rst | 3 ++- docs/tutorial/kubernetes-charm-go.rst | 3 ++- docs/tutorial/kubernetes-charm-spring-boot.rst | 3 ++- 12 files changed, 24 insertions(+), 12 deletions(-) diff --git a/docs/tutorial/code/django/task.yaml b/docs/tutorial/code/django/task.yaml index 48f1cfc51..fcd0a0884 100644 --- a/docs/tutorial/code/django/task.yaml +++ b/docs/tutorial/code/django/task.yaml @@ -138,7 +138,8 @@ execute: | # [docs:config-nginx] juju config nginx-ingress-integrator \ - service-hostname=django-hello-world path-routes=/ rewrite-enabled=false rewrite-target=/ + service-hostname=django-hello-world path-routes=/ rewrite-enabled=false rewrite-target=/ \ + ingress-class=cilium # [docs:config-nginx-end] # give Juju some time to deploy the apps diff --git a/docs/tutorial/code/expressjs/task.yaml b/docs/tutorial/code/expressjs/task.yaml index f12f9cc30..f1bfa038c 100644 --- a/docs/tutorial/code/expressjs/task.yaml +++ b/docs/tutorial/code/expressjs/task.yaml @@ -116,7 +116,8 @@ execute: | # [docs:config-nginx] juju config nginx-ingress-integrator \ - service-hostname=expressjs-hello-world path-routes=/ rewrite-enabled=false rewrite-target=/ + service-hostname=expressjs-hello-world path-routes=/ rewrite-enabled=false rewrite-target=/ \ + ingress-class=cilium # [docs:config-nginx-end] # give Juju some time to deploy the apps diff --git a/docs/tutorial/code/fastapi/task.yaml b/docs/tutorial/code/fastapi/task.yaml index bbb6878d4..1e21c1f60 100644 --- a/docs/tutorial/code/fastapi/task.yaml +++ b/docs/tutorial/code/fastapi/task.yaml @@ -102,7 +102,8 @@ execute: | # [docs:config-nginx] juju config nginx-ingress-integrator \ - service-hostname=fastapi-hello-world path-routes=/ rewrite-enabled=false rewrite-target=/ + service-hostname=fastapi-hello-world path-routes=/ rewrite-enabled=false rewrite-target=/ \ + ingress-class=cilium # [docs:config-nginx-end] # give Juju some time to deploy the apps diff --git a/docs/tutorial/code/flask/task.yaml b/docs/tutorial/code/flask/task.yaml index d363278fd..1c8a8346e 100644 --- a/docs/tutorial/code/flask/task.yaml +++ b/docs/tutorial/code/flask/task.yaml @@ -98,7 +98,8 @@ execute: | # [docs:config-nginx] juju config nginx-ingress-integrator \ - service-hostname=flask-hello-world path-routes=/ rewrite-enabled=false rewrite-target=/ + service-hostname=flask-hello-world path-routes=/ rewrite-enabled=false rewrite-target=/ \ + ingress-class=cilium # [docs:config-nginx-end] # give Juju some time to deploy the apps diff --git a/docs/tutorial/code/go/task.yaml b/docs/tutorial/code/go/task.yaml index fdddfd9cb..7eaf5c53e 100644 --- a/docs/tutorial/code/go/task.yaml +++ b/docs/tutorial/code/go/task.yaml @@ -110,7 +110,8 @@ execute: | # [docs:config-nginx] juju config nginx-ingress-integrator \ - service-hostname=go-hello-world path-routes=/ rewrite-enabled=false rewrite-target=/ + service-hostname=go-hello-world path-routes=/ rewrite-enabled=false rewrite-target=/ \ + ingress-class=cilium # [docs:config-nginx-end] # give Juju some time to deploy the apps diff --git a/docs/tutorial/code/spring-boot/task.yaml b/docs/tutorial/code/spring-boot/task.yaml index 5e9f7b04c..8cbebb259 100644 --- a/docs/tutorial/code/spring-boot/task.yaml +++ b/docs/tutorial/code/spring-boot/task.yaml @@ -128,7 +128,8 @@ execute: | # [docs:config-nginx] juju config nginx-ingress-integrator \ - service-hostname=spring-boot-hello-world path-routes=/ rewrite-enabled=false rewrite-target=/ + service-hostname=spring-boot-hello-world path-routes=/ rewrite-enabled=false rewrite-target=/ \ + ingress-class=cilium # [docs:config-nginx-end] # give Juju some time to deploy the apps diff --git a/docs/tutorial/kubernetes-charm-django.rst b/docs/tutorial/kubernetes-charm-django.rst index b2df24d24..b7f15b253 100644 --- a/docs/tutorial/kubernetes-charm-django.rst +++ b/docs/tutorial/kubernetes-charm-django.rst @@ -541,7 +541,8 @@ Let's expose the app using ingress so that we can access it. Deploy the :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: +the ingress. We also set the default route to the root endpoint and target +the ``cilium`` ingress class that Canonical Kubernetes uses for its ingress: .. literalinclude:: code/django/task.yaml :language: bash diff --git a/docs/tutorial/kubernetes-charm-express.rst b/docs/tutorial/kubernetes-charm-express.rst index 5adbf20b0..4c0f4f4e7 100644 --- a/docs/tutorial/kubernetes-charm-express.rst +++ b/docs/tutorial/kubernetes-charm-express.rst @@ -392,7 +392,8 @@ Let's expose the app using ingress so that we can access it. Deploy the :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: +the ingress. We also set the default route to the root endpoint and target +the ``cilium`` ingress class that Canonical Kubernetes uses for its ingress: .. literalinclude:: code/expressjs/task.yaml :language: bash diff --git a/docs/tutorial/kubernetes-charm-fastapi.rst b/docs/tutorial/kubernetes-charm-fastapi.rst index 286f4bd34..7617d81b0 100644 --- a/docs/tutorial/kubernetes-charm-fastapi.rst +++ b/docs/tutorial/kubernetes-charm-fastapi.rst @@ -424,7 +424,8 @@ Let's expose the app using ingress so that we can access it. Deploy the :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: +the ingress. We also set the default route to the root endpoint and target +the ``cilium`` ingress class that Canonical Kubernetes uses for its ingress: .. literalinclude:: code/fastapi/task.yaml :language: bash diff --git a/docs/tutorial/kubernetes-charm-flask.rst b/docs/tutorial/kubernetes-charm-flask.rst index 1e56187cf..bc20ceb9d 100644 --- a/docs/tutorial/kubernetes-charm-flask.rst +++ b/docs/tutorial/kubernetes-charm-flask.rst @@ -380,7 +380,8 @@ Let's expose the app using ingress so that we can access it. Deploy the :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: +the ingress. We also set the default route to the root endpoint and target +the ``cilium`` ingress class that Canonical Kubernetes uses for its ingress: .. literalinclude:: code/flask/task.yaml :language: bash diff --git a/docs/tutorial/kubernetes-charm-go.rst b/docs/tutorial/kubernetes-charm-go.rst index f5f0bf6cc..b8e980f40 100644 --- a/docs/tutorial/kubernetes-charm-go.rst +++ b/docs/tutorial/kubernetes-charm-go.rst @@ -407,7 +407,8 @@ Let's expose the app using ingress so that we can access it. Deploy the :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: +the ingress. We also set the default route to the root endpoint and target +the ``cilium`` ingress class that Canonical Kubernetes uses for its ingress: .. literalinclude:: code/go/task.yaml :language: bash diff --git a/docs/tutorial/kubernetes-charm-spring-boot.rst b/docs/tutorial/kubernetes-charm-spring-boot.rst index d6ef012ea..df25f45c0 100644 --- a/docs/tutorial/kubernetes-charm-spring-boot.rst +++ b/docs/tutorial/kubernetes-charm-spring-boot.rst @@ -410,7 +410,8 @@ Let's expose the app using ingress so that we can access it. Deploy the :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: +the ingress. We also set the default route to the root endpoint and target +the ``cilium`` ingress class that Canonical Kubernetes uses for its ingress: .. literalinclude:: code/spring-boot/task.yaml :language: bash From 37e6cafa6ebb3f9607581312f984e201f9a5e4a8 Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Tue, 20 Oct 2026 22:12:52 +0700 Subject: [PATCH 11/28] docs: fix packed charm filename in 12-factor tutorials The framework profiles generate charmcraft.yaml with ``base`` and ``platforms``, so ``charmcraft pack`` names the artifact ``_.charm``. The tutorials still showed the old ``bases``-style ``_ubuntu--.charm`` output (django even showed 22.04). Correct the example output to match. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/tutorial/kubernetes-charm-django.rst | 2 +- docs/tutorial/kubernetes-charm-express.rst | 2 +- docs/tutorial/kubernetes-charm-fastapi.rst | 2 +- docs/tutorial/kubernetes-charm-flask.rst | 2 +- docs/tutorial/kubernetes-charm-go.rst | 2 +- docs/tutorial/kubernetes-charm-spring-boot.rst | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/tutorial/kubernetes-charm-django.rst b/docs/tutorial/kubernetes-charm-django.rst index b7f15b253..8e732d599 100644 --- a/docs/tutorial/kubernetes-charm-django.rst +++ b/docs/tutorial/kubernetes-charm-django.rst @@ -415,7 +415,7 @@ minutes to finish. Once Charmcraft has finished packing the charm, the terminal will respond with something similar to -``Packed django-hello-world_ubuntu-22.04-.charm``. The file name +``Packed django-hello-world_.charm``. The file name reflects your system's architecture. After the initial pack, subsequent charm packings are faster. diff --git a/docs/tutorial/kubernetes-charm-express.rst b/docs/tutorial/kubernetes-charm-express.rst index 4c0f4f4e7..b9031ff19 100644 --- a/docs/tutorial/kubernetes-charm-express.rst +++ b/docs/tutorial/kubernetes-charm-express.rst @@ -307,7 +307,7 @@ minutes to finish. Once Charmcraft has finished packing the charm, the terminal will respond with something similar to -``Packed expressjs-hello-world_ubuntu-24.04-.charm``. +``Packed expressjs-hello-world_.charm``. The file name reflects your system's architecture. After the initial pack, subsequent charm packings are faster. diff --git a/docs/tutorial/kubernetes-charm-fastapi.rst b/docs/tutorial/kubernetes-charm-fastapi.rst index 7617d81b0..dac07a073 100644 --- a/docs/tutorial/kubernetes-charm-fastapi.rst +++ b/docs/tutorial/kubernetes-charm-fastapi.rst @@ -335,7 +335,7 @@ minutes to finish. Once Charmcraft has finished packing the charm, the terminal will respond with something similar to -``Packed fastapi-hello-world_ubuntu-24.04-.charm``. The file name +``Packed fastapi-hello-world_.charm``. The file name reflects your system's architecture. After the initial pack, subsequent charm packings are faster. diff --git a/docs/tutorial/kubernetes-charm-flask.rst b/docs/tutorial/kubernetes-charm-flask.rst index bc20ceb9d..93f2df6df 100644 --- a/docs/tutorial/kubernetes-charm-flask.rst +++ b/docs/tutorial/kubernetes-charm-flask.rst @@ -294,7 +294,7 @@ minutes to finish. Once Charmcraft has finished packing the charm, the terminal will respond with something similar to -``Packed flask-hello-world_ubuntu-24.04-.charm``. The file name +``Packed flask-hello-world_.charm``. The file name reflects your system's architecture. After the initial pack, subsequent charm packings are faster. diff --git a/docs/tutorial/kubernetes-charm-go.rst b/docs/tutorial/kubernetes-charm-go.rst index b8e980f40..0737cdc35 100644 --- a/docs/tutorial/kubernetes-charm-go.rst +++ b/docs/tutorial/kubernetes-charm-go.rst @@ -322,7 +322,7 @@ minutes to finish. Once Charmcraft has finished packing the charm, the terminal will respond with something similar to -``Packed go-hello-world_ubuntu-24.04-.charm``. The file name +``Packed go-hello-world_.charm``. The file name reflects your system's architecture. After the initial pack, subsequent charm packings are faster. diff --git a/docs/tutorial/kubernetes-charm-spring-boot.rst b/docs/tutorial/kubernetes-charm-spring-boot.rst index df25f45c0..1c5fdd79e 100644 --- a/docs/tutorial/kubernetes-charm-spring-boot.rst +++ b/docs/tutorial/kubernetes-charm-spring-boot.rst @@ -329,7 +329,7 @@ minutes to finish. Once Charmcraft has finished packing the charm, the terminal will respond with something similar to -``Packed spring-boot-hello-world_ubuntu-24.04-.charm``. The file name +``Packed spring-boot-hello-world_.charm``. The file name reflects your system's architecture. After the initial pack, subsequent charm packings are faster. From 80b564bb0d1a3e069a81cb72faf27c9cb2154268 Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Tue, 20 Oct 2026 22:18:35 +0700 Subject: [PATCH 12/28] ci: request combined runner label for docs spread tests Use the single ``self-hosted-linux-amd64-noble-large`` runner label for the docs-tests job instead of the multi-label set, which was mapping to a starved runner pool and preventing the docs spread tests from running. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/spread-docs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spread-docs.yaml b/.github/workflows/spread-docs.yaml index 5a18b9308..dd19675b1 100644 --- a/.github/workflows/spread-docs.yaml +++ b/.github/workflows/spread-docs.yaml @@ -53,7 +53,7 @@ jobs: echo systems="$(spread -list ci:docs/ | jq --raw-input . | jq --slurp --compact-output)" | tee -a "${GITHUB_OUTPUT}" docs-tests: - runs-on: [self-hosted, linux, amd64, noble, large] + runs-on: self-hosted-linux-amd64-noble-large needs: [spread-select, snap-build] timeout-minutes: 90 strategy: From e8c8995f2ce55cc363e1d9def345601d61f18964 Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Tue, 20 Oct 2026 22:19:20 +0700 Subject: [PATCH 13/28] ci: use single runner labels for docs spread jobs Switch the snap-build and spread-select jobs to the combined ``self-hosted-linux-amd64-noble-medium`` and ``spread-installed`` runner labels, matching the docs-tests job. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/spread-docs.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/spread-docs.yaml b/.github/workflows/spread-docs.yaml index dd19675b1..373575782 100644 --- a/.github/workflows/spread-docs.yaml +++ b/.github/workflows/spread-docs.yaml @@ -22,7 +22,7 @@ concurrency: jobs: snap-build: - runs-on: [self-hosted, linux, amd64, noble, medium] + runs-on: self-hosted-linux-amd64-noble-medium steps: - name: Checkout code uses: actions/checkout@v7 @@ -38,7 +38,7 @@ jobs: path: ${{ steps.charmcraft.outputs.snap }} spread-select: - runs-on: [spread-installed] + runs-on: spread-installed outputs: systems: ${{ steps.select.outputs.systems }} steps: From 675e8e1a25a6b025190b315ff0edc76c9b8af3d4 Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Tue, 20 Oct 2026 14:50:47 -0400 Subject: [PATCH 14/28] Apply suggestion from @lengau Signed-off-by: Alex Lowe --- .github/workflows/spread-docs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spread-docs.yaml b/.github/workflows/spread-docs.yaml index 373575782..e1b6ac7fb 100644 --- a/.github/workflows/spread-docs.yaml +++ b/.github/workflows/spread-docs.yaml @@ -38,7 +38,7 @@ jobs: path: ${{ steps.charmcraft.outputs.snap }} spread-select: - runs-on: spread-installed + runs-on: spread-enabled outputs: systems: ${{ steps.select.outputs.systems }} steps: From 27959ef9d4c4521b62e56d6dd064ba683b0475ad Mon Sep 17 00:00:00 2001 From: florentianayuwono <76247368+florentianayuwono@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:04:14 +0700 Subject: [PATCH 15/28] Update docs/reuse/tutorial/setup_stable.rst Co-authored-by: Erin Conley Signed-off-by: florentianayuwono <76247368+florentianayuwono@users.noreply.github.com> --- docs/reuse/tutorial/setup_stable.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/reuse/tutorial/setup_stable.rst b/docs/reuse/tutorial/setup_stable.rst index 4ca3bf140..8fe62ee0c 100644 --- a/docs/reuse/tutorial/setup_stable.rst +++ b/docs/reuse/tutorial/setup_stable.rst @@ -147,6 +147,9 @@ Wait for the registry to become available: sudo k8s kubectl rollout status deployment/registry \ -n container-registry --timeout=5m +Install Juju +~~~~~~~~~~~~ + Juju is required to deploy the |12FactorApp| application. We'll install Juju using the ``3.6/stable`` channel. Since the snap is sandboxed, we'll also manually create a directory to contain From afd03d3d67739c1823cffb031e660d875c607787 Mon Sep 17 00:00:00 2001 From: florentianayuwono <76247368+florentianayuwono@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:04:37 +0700 Subject: [PATCH 16/28] Update docs/reuse/tutorial/setup_stable.rst Co-authored-by: Erin Conley Signed-off-by: florentianayuwono <76247368+florentianayuwono@users.noreply.github.com> --- docs/reuse/tutorial/setup_stable.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reuse/tutorial/setup_stable.rst b/docs/reuse/tutorial/setup_stable.rst index 8fe62ee0c..0de30430c 100644 --- a/docs/reuse/tutorial/setup_stable.rst +++ b/docs/reuse/tutorial/setup_stable.rst @@ -30,8 +30,8 @@ In order to create the charm, you'll need to install Charmcraft: sudo snap install charmcraft --channel latest/stable --classic -Install LXD, Canonical Kubernetes, and Juju -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Install LXD and Canonical Kubernetes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LXD will be required for building the rock. Make sure it is installed: From 0ff71ae9effbdfe612e8688129516b32a71b8351 Mon Sep 17 00:00:00 2001 From: florentianayuwono <76247368+florentianayuwono@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:04:56 +0700 Subject: [PATCH 17/28] Update docs/reuse/tutorial/setup_edge.rst Co-authored-by: Erin Conley Signed-off-by: florentianayuwono <76247368+florentianayuwono@users.noreply.github.com> --- docs/reuse/tutorial/setup_edge.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reuse/tutorial/setup_edge.rst b/docs/reuse/tutorial/setup_edge.rst index 85e511c21..87caa3dc4 100644 --- a/docs/reuse/tutorial/setup_edge.rst +++ b/docs/reuse/tutorial/setup_edge.rst @@ -35,8 +35,8 @@ In order to create the charm, you'll need to install Charmcraft: sudo snap install charmcraft --channel latest/edge --classic -Install LXD, Canonical Kubernetes, and Juju -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Install LXD and Canonical Kubernetes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LXD will be required for building the rock. Make sure it is installed: From be2f426fe747368824425648062d3c0788dfc046 Mon Sep 17 00:00:00 2001 From: florentianayuwono <76247368+florentianayuwono@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:05:13 +0700 Subject: [PATCH 18/28] Update docs/reuse/tutorial/setup_edge.rst Co-authored-by: Erin Conley Signed-off-by: florentianayuwono <76247368+florentianayuwono@users.noreply.github.com> --- docs/reuse/tutorial/setup_edge.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/reuse/tutorial/setup_edge.rst b/docs/reuse/tutorial/setup_edge.rst index 87caa3dc4..a24f86f27 100644 --- a/docs/reuse/tutorial/setup_edge.rst +++ b/docs/reuse/tutorial/setup_edge.rst @@ -151,6 +151,9 @@ Wait for the registry to become available: sudo k8s kubectl rollout status deployment/registry \ -n container-registry --timeout=5m +Install Juju +~~~~~~~~~~~~ + Juju is required to deploy the |12FactorApp| application. We'll install Juju using the ``3.6/stable`` channel. Since the snap is sandboxed, we'll also manually create a directory to contain From 66d8b6bbf2dec08ea1f39d6df8a68d24cd4647de Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Wed, 21 Oct 2026 19:55:05 +0700 Subject: [PATCH 19/28] docs: bump tutorial VM to 8 GB RAM and allow "balancer's" Increase the Multipass VM memory from 4 GB to 8 GB in the shared setup pages and the per-framework prerequisites to avoid pack errors and I/O timeouts during the build steps. Add "balancer('s)?" to the custom wordlist so the load-balancer prose passes the documentation linter. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/.custom_wordlist.txt | 1 + docs/reuse/tutorial/setup_edge.rst | 2 +- docs/reuse/tutorial/setup_stable.rst | 2 +- docs/tutorial/kubernetes-charm-django.rst | 2 +- docs/tutorial/kubernetes-charm-express.rst | 2 +- docs/tutorial/kubernetes-charm-fastapi.rst | 2 +- docs/tutorial/kubernetes-charm-flask.rst | 2 +- docs/tutorial/kubernetes-charm-go.rst | 2 +- docs/tutorial/kubernetes-charm-spring-boot.rst | 2 +- 9 files changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/.custom_wordlist.txt b/docs/.custom_wordlist.txt index be17687dc..b8ec92afa 100644 --- a/docs/.custom_wordlist.txt +++ b/docs/.custom_wordlist.txt @@ -10,6 +10,7 @@ Autotools backend backends backported +balancer('s)? Bazel boolean bugfix(es)? diff --git a/docs/reuse/tutorial/setup_edge.rst b/docs/reuse/tutorial/setup_edge.rst index a24f86f27..68cb7759f 100644 --- a/docs/reuse/tutorial/setup_edge.rst +++ b/docs/reuse/tutorial/setup_edge.rst @@ -4,7 +4,7 @@ from the 26.04 blueprint: .. code-block:: text - multipass launch --cpus 4 --disk 50G --memory 4G --name charm-dev 26.04 + multipass launch --cpus 4 --disk 50G --memory 8G --name charm-dev 26.04 Once the VM is up, open a shell into it: diff --git a/docs/reuse/tutorial/setup_stable.rst b/docs/reuse/tutorial/setup_stable.rst index 0de30430c..8269b143e 100644 --- a/docs/reuse/tutorial/setup_stable.rst +++ b/docs/reuse/tutorial/setup_stable.rst @@ -4,7 +4,7 @@ from the 24.04 blueprint: .. code-block:: text - multipass launch --cpus 4 --disk 50G --memory 4G --name charm-dev 24.04 + multipass launch --cpus 4 --disk 50G --memory 8G --name charm-dev 24.04 Once the VM is up, open a shell into it: diff --git a/docs/tutorial/kubernetes-charm-django.rst b/docs/tutorial/kubernetes-charm-django.rst index 8e732d599..c6ac582df 100644 --- a/docs/tutorial/kubernetes-charm-django.rst +++ b/docs/tutorial/kubernetes-charm-django.rst @@ -36,7 +36,7 @@ What you'll need - A local system, 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. + CPUs, 8 GB RAM, and a 50 GB disk. - Familiarity with Linux. The RAM and disk space are necessary to set up all the required software and diff --git a/docs/tutorial/kubernetes-charm-express.rst b/docs/tutorial/kubernetes-charm-express.rst index b9031ff19..7ee28f917 100644 --- a/docs/tutorial/kubernetes-charm-express.rst +++ b/docs/tutorial/kubernetes-charm-express.rst @@ -31,7 +31,7 @@ What you'll need - A local system, 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. + 8 GB RAM, and a 50 GB disk. - Familiarity with Linux. The RAM and disk space are necessary to set up all the required software and diff --git a/docs/tutorial/kubernetes-charm-fastapi.rst b/docs/tutorial/kubernetes-charm-fastapi.rst index dac07a073..f3fd6390e 100644 --- a/docs/tutorial/kubernetes-charm-fastapi.rst +++ b/docs/tutorial/kubernetes-charm-fastapi.rst @@ -36,7 +36,7 @@ What you'll need - A local system, 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. + 8 GB RAM, and a 50 GB disk. - Familiarity with Linux. The RAM and disk space are necessary to set up all the required software and diff --git a/docs/tutorial/kubernetes-charm-flask.rst b/docs/tutorial/kubernetes-charm-flask.rst index 93f2df6df..8dfd5e44d 100644 --- a/docs/tutorial/kubernetes-charm-flask.rst +++ b/docs/tutorial/kubernetes-charm-flask.rst @@ -32,7 +32,7 @@ What you'll need - A local system, 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. + 8 GB RAM, and a 50 GB disk. - Familiarity with Linux. The RAM and disk space are necessary to set up all the required software and diff --git a/docs/tutorial/kubernetes-charm-go.rst b/docs/tutorial/kubernetes-charm-go.rst index 0737cdc35..6c65e7ae7 100644 --- a/docs/tutorial/kubernetes-charm-go.rst +++ b/docs/tutorial/kubernetes-charm-go.rst @@ -32,7 +32,7 @@ What you'll need - A local system, 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. + 8 GB RAM, and a 50 GB disk. - Familiarity with Linux. The RAM and disk space are necessary to set up all the required software and diff --git a/docs/tutorial/kubernetes-charm-spring-boot.rst b/docs/tutorial/kubernetes-charm-spring-boot.rst index 1c5fdd79e..6f42ccbfc 100644 --- a/docs/tutorial/kubernetes-charm-spring-boot.rst +++ b/docs/tutorial/kubernetes-charm-spring-boot.rst @@ -32,7 +32,7 @@ What you'll need - A local system, 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. + 8 GB RAM, and a 50 GB disk. - Familiarity with Linux. The RAM and disk space are necessary to set up all the required software and From dea7bbd445bc40eae66dba115061ef8d7bebcdcd Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Wed, 21 Oct 2026 20:31:52 +0700 Subject: [PATCH 20/28] fix: deploy PostgreSQL 16 in the Django tutorial Django 6.1 requires PostgreSQL 15 or later, but the tutorial deployed postgresql-k8s from the 14/edge channel (PostgreSQL 14), so the charm's startup migration failed with "PostgreSQL 15 or later is required". Deploy postgresql-k8s from 16/stable to match the Django requirement, consistent with the upstream 12-factor Django tutorial. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/tutorial/code/django/task.yaml | 2 +- docs/tutorial/kubernetes-charm-django.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/tutorial/code/django/task.yaml b/docs/tutorial/code/django/task.yaml index fcd0a0884..986cdb367 100644 --- a/docs/tutorial/code/django/task.yaml +++ b/docs/tutorial/code/django/task.yaml @@ -116,7 +116,7 @@ execute: | # [docs:deploy-django-app-end] # [docs:deploy-postgres] - juju deploy postgresql-k8s --trust --channel 14/edge + juju deploy postgresql-k8s --trust --channel 16/stable # [docs:deploy-postgres-end] # [docs:integrate-postgres] diff --git a/docs/tutorial/kubernetes-charm-django.rst b/docs/tutorial/kubernetes-charm-django.rst index c6ac582df..479202474 100644 --- a/docs/tutorial/kubernetes-charm-django.rst +++ b/docs/tutorial/kubernetes-charm-django.rst @@ -457,7 +457,7 @@ app. Deploy using Juju by specifying the OCI image name with the :end-before: [docs:deploy-django-app-end] :dedent: 2 -Now let's deploy PostgreSQL: +Now let's deploy PostgreSQL 16: .. literalinclude:: code/django/task.yaml :language: bash @@ -508,7 +508,7 @@ following output: App Version Status Scale Charm Channel Rev Address Exposed Message django-hello-world active 1 django-hello-world 3 10.152.183.126 no - postgresql-k8s 14.11 active 1 postgresql-k8s 14/stable 281 10.152.183.197 no + postgresql-k8s 16 active 1 postgresql-k8s 16/stable 957 10.152.183.197 no Unit Workload Agent Address Ports Message django-hello-world/0* active idle 10.1.157.80 From 44062044457bb90cb686660f54bae453b78778d8 Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Wed, 21 Oct 2026 20:44:39 +0700 Subject: [PATCH 21/28] docs: move tutorial registry manifest into a downloadable file The registry setup inlined the full Kubernetes manifest as a heredoc, which made the shared setup pages long and hard to read. Move the manifest into docs/reuse/tutorial/registry.yaml and download it with curl in the tutorial, mirroring the upstream 26.04 tutorials. Add a readiness probe so the registry rollout wait reflects actual readiness. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/reuse/tutorial/registry.yaml | 44 ++++++++++++++++++++++++++++ docs/reuse/tutorial/setup_edge.rst | 44 ++-------------------------- docs/reuse/tutorial/setup_stable.rst | 44 ++-------------------------- 3 files changed, 50 insertions(+), 82 deletions(-) create mode 100644 docs/reuse/tutorial/registry.yaml diff --git a/docs/reuse/tutorial/registry.yaml b/docs/reuse/tutorial/registry.yaml new file mode 100644 index 000000000..b6997ebec --- /dev/null +++ b/docs/reuse/tutorial/registry.yaml @@ -0,0 +1,44 @@ +apiVersion: v1 +kind: List +items: + - apiVersion: v1 + kind: Namespace + metadata: + name: container-registry + - apiVersion: apps/v1 + kind: Deployment + metadata: + name: registry + namespace: container-registry + spec: + replicas: 1 + selector: + matchLabels: + app: registry + template: + metadata: + labels: + app: registry + spec: + containers: + - name: registry + image: registry:2.8.3 + ports: + - containerPort: 5000 + readinessProbe: + httpGet: + path: /v2/ + port: 5000 + - apiVersion: v1 + kind: Service + metadata: + name: registry + namespace: container-registry + spec: + type: NodePort + selector: + app: registry + ports: + - port: 5000 + targetPort: 5000 + nodePort: 32000 diff --git a/docs/reuse/tutorial/setup_edge.rst b/docs/reuse/tutorial/setup_edge.rst index 68cb7759f..8f64c0f63 100644 --- a/docs/reuse/tutorial/setup_edge.rst +++ b/docs/reuse/tutorial/setup_edge.rst @@ -102,47 +102,9 @@ small registry inside the cluster, exposed on the node at ``localhost:32000``: .. code-block:: bash - sudo k8s kubectl apply -f - <<'EOF' - apiVersion: v1 - kind: Namespace - metadata: - name: container-registry - --- - apiVersion: apps/v1 - kind: Deployment - metadata: - name: registry - namespace: container-registry - spec: - replicas: 1 - selector: - matchLabels: - app: registry - template: - metadata: - labels: - app: registry - spec: - containers: - - name: registry - image: registry:2.8.3 - ports: - - containerPort: 5000 - --- - apiVersion: v1 - kind: Service - metadata: - name: registry - namespace: container-registry - spec: - type: NodePort - selector: - app: registry - ports: - - port: 5000 - targetPort: 5000 - nodePort: 32000 - EOF + curl --fail --location --output registry.yaml \ + https://raw.githubusercontent.com/canonical/charmcraft/main/docs/reuse/tutorial/registry.yaml + sudo k8s kubectl apply -f registry.yaml Wait for the registry to become available: diff --git a/docs/reuse/tutorial/setup_stable.rst b/docs/reuse/tutorial/setup_stable.rst index 8269b143e..d0bce8dbf 100644 --- a/docs/reuse/tutorial/setup_stable.rst +++ b/docs/reuse/tutorial/setup_stable.rst @@ -98,47 +98,9 @@ small registry inside the cluster, exposed on the node at ``localhost:32000``: .. code-block:: bash - sudo k8s kubectl apply -f - <<'EOF' - apiVersion: v1 - kind: Namespace - metadata: - name: container-registry - --- - apiVersion: apps/v1 - kind: Deployment - metadata: - name: registry - namespace: container-registry - spec: - replicas: 1 - selector: - matchLabels: - app: registry - template: - metadata: - labels: - app: registry - spec: - containers: - - name: registry - image: registry:2.8.3 - ports: - - containerPort: 5000 - --- - apiVersion: v1 - kind: Service - metadata: - name: registry - namespace: container-registry - spec: - type: NodePort - selector: - app: registry - ports: - - port: 5000 - targetPort: 5000 - nodePort: 32000 - EOF + curl --fail --location --output registry.yaml \ + https://raw.githubusercontent.com/canonical/charmcraft/main/docs/reuse/tutorial/registry.yaml + sudo k8s kubectl apply -f registry.yaml Wait for the registry to become available: From 772731af9af645e6030c6e134afb2324e6981e97 Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Wed, 21 Oct 2026 22:03:16 +0700 Subject: [PATCH 22/28] docs: update Django version in tutorial sample output Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/tutorial/kubernetes-charm-django.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/kubernetes-charm-django.rst b/docs/tutorial/kubernetes-charm-django.rst index 479202474..2d6db1b96 100644 --- a/docs/tutorial/kubernetes-charm-django.rst +++ b/docs/tutorial/kubernetes-charm-django.rst @@ -264,7 +264,7 @@ imports to include ``json``, ``os`` and ``secrets``. The top of the """ Django settings for django_hello_world project. - Generated by 'django-admin startproject' using Django 5.1.4. + Generated by 'django-admin startproject' using Django 6.1.1. For more information on this file, see https://docs.djangoproject.com/en/5.1/topics/settings/ From 2d3ca94b3e7b97f5ba561787278c7933290b9c04 Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Wed, 21 Oct 2026 23:21:56 +0700 Subject: [PATCH 23/28] docs: build the FastAPI tutorial rock on a bare Ubuntu 26.04 base Avoids the ca-certificates.crt staging conflict (canonical/rockcraft#1261) that occurs with the ubuntu@24.04 base. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/tutorial/code/fastapi/task.yaml | 6 +++++- docs/tutorial/kubernetes-charm-fastapi.rst | 25 +++++++++++++++++----- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/docs/tutorial/code/fastapi/task.yaml b/docs/tutorial/code/fastapi/task.yaml index 1e21c1f60..167ec394e 100644 --- a/docs/tutorial/code/fastapi/task.yaml +++ b/docs/tutorial/code/fastapi/task.yaml @@ -17,6 +17,10 @@ restore: | environment: execute: | + # [docs:export-experimental-env-vars] + export ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=1 + # [docs:export-experimental-env-vars-end] + # Move everything to $HOME so that Juju deployment works mkdir $HOME/fastapi mv *.yaml *.py *.txt $HOME/fastapi @@ -47,7 +51,7 @@ execute: | kill $! # [docs:create-rockcraft-yaml] - rockcraft init --profile fastapi-framework + rockcraft init --profile fastapi-framework --base ubuntu@26.04 # [docs:create-rockcraft-yaml-end] sed -i "s/name: .*/name: fastapi-hello-world/g" rockcraft.yaml diff --git a/docs/tutorial/kubernetes-charm-fastapi.rst b/docs/tutorial/kubernetes-charm-fastapi.rst index e51c511db..5a6ec1bdd 100644 --- a/docs/tutorial/kubernetes-charm-fastapi.rst +++ b/docs/tutorial/kubernetes-charm-fastapi.rst @@ -72,6 +72,15 @@ Set things up .. include:: /reuse/tutorial/setup_edge.rst .. |12FactorApp| replace:: FastAPI +The Ubuntu 26.04 extension for the rock is experimental. Enable it in the +terminal where you'll build the rock: + +.. literalinclude:: code/fastapi/task.yaml + :language: bash + :start-after: [docs:export-experimental-env-vars] + :end-before: [docs:export-experimental-env-vars-end] + :dedent: 2 + Let's create a directory for this tutorial and enter into it: .. code-block:: bash @@ -176,6 +185,9 @@ From the ``~/fastapi-hello-world`` directory, initialize the rock: The ``rockcraft.yaml`` file will be automatically created, with the name being set based on your working directory. +The Ubuntu 26.04 profile uses a bare runtime base and Ubuntu 26.04 as its +build environment. Keep these generated settings. + Let's verify that the project file is compatible with your host machine. Check the architecture of your system: @@ -197,7 +209,8 @@ The top of the file should look similar to the following snippet: name: fastapi-hello-world # see https://documentation.ubuntu.com/rockcraft/en/latest/explanation/bases/ # for more information about bases and using 'bare' bases for chiselled rocks - base: ubuntu@24.04 # the base environment for this FastAPI app + base: bare # as an alternative, an ubuntu base can be used + build-base: ubuntu@26.04 # build-base is required when the base is bare version: '0.1' # just for humans. Semantic versioning is recommended summary: A summary of your FastAPI app # 79 char long summary description: | @@ -487,12 +500,13 @@ top of the ``rockcraft.yaml`` file looks similar to the following: .. code-block:: yaml :caption: ~/fastapi-hello-world/rockcraft.yaml - :emphasize-lines: 5 + :emphasize-lines: 6 name: fastapi-hello-world # see https://documentation.ubuntu.com/rockcraft/en/latest/explanation/bases/ # for more information about bases and using 'bare' bases for chiselled rocks - base: ubuntu@24.04 # the base environment for this FastAPI app + base: bare # as an alternative, an ubuntu base can be used + build-base: ubuntu@26.04 # build-base is required when the base is bare version: '0.2' # just for humans. Semantic versioning is recommended summary: A summary of your FastAPI app # 79 char long summary description: | @@ -625,12 +639,13 @@ top of the ``rockcraft.yaml`` file looks similar to the following: .. code-block:: yaml :caption: ~/fastapi-hello-world/rockcraft.yaml - :emphasize-lines: 5 + :emphasize-lines: 6 name: fastapi-hello-world # see https://documentation.ubuntu.com/rockcraft/en/latest/explanation/bases/ # for more information about bases and using 'bare' bases for chiselled rocks - base: ubuntu@24.04 # the base environment for this FastAPI app + base: bare # as an alternative, an ubuntu base can be used + build-base: ubuntu@26.04 # build-base is required when the base is bare version: '0.3' # just for humans. Semantic versioning is recommended summary: A summary of your FastAPI app # 79 char long summary description: | From e806fe1eadb5e6535a787253e91d19331f420b40 Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Thu, 22 Oct 2026 00:47:27 +0700 Subject: [PATCH 24/28] ci: fix docs spread runner labels and clear containerd before k8s bootstrap Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/spread-docs.yaml | 4 ++-- spread.yaml | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/spread-docs.yaml b/.github/workflows/spread-docs.yaml index e1b6ac7fb..568bf21a8 100644 --- a/.github/workflows/spread-docs.yaml +++ b/.github/workflows/spread-docs.yaml @@ -22,7 +22,7 @@ concurrency: jobs: snap-build: - runs-on: self-hosted-linux-amd64-noble-medium + runs-on: [self-hosted, linux, amd64, noble, medium] steps: - name: Checkout code uses: actions/checkout@v7 @@ -53,7 +53,7 @@ jobs: echo systems="$(spread -list ci:docs/ | jq --raw-input . | jq --slurp --compact-output)" | tee -a "${GITHUB_OUTPUT}" docs-tests: - runs-on: self-hosted-linux-amd64-noble-large + runs-on: [self-hosted, linux, amd64, noble, large] needs: [spread-select, snap-build] timeout-minutes: 90 strategy: diff --git a/spread.yaml b/spread.yaml index d2d579caa..fc08493c4 100644 --- a/spread.yaml +++ b/spread.yaml @@ -299,6 +299,11 @@ suites: refresh_or_install_snap k8s "$k8s_channel" --classic refresh_or_install_snap rockcraft latest/edge --classic + # A pre-existing containerd (e.g. from Docker) holds /run/containerd and + # makes the k8s bootstrap pre-init checks fail. + systemctl stop docker.socket docker containerd 2>/dev/null || true + rm -rf /run/containerd + k8s bootstrap k8s status --wait-ready --timeout 5m From 05cf1edd0fc4eeb34d831cb6de7de892cee35f2a Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Thu, 22 Oct 2026 11:08:03 +0700 Subject: [PATCH 25/28] fix: route all paths through the CK8s tutorial ingress The cilium ingress class treats the ImplementationSpecific path / as an exact match, so endpoints other than / returned an empty 404. Use the /.* pattern and give the ingress more time to come up. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/tutorial/code/django/task.yaml | 4 ++-- docs/tutorial/code/expressjs/task.yaml | 2 +- docs/tutorial/code/fastapi/task.yaml | 6 +++--- docs/tutorial/code/flask/task.yaml | 6 +++--- docs/tutorial/code/go/task.yaml | 6 +++--- docs/tutorial/code/spring-boot/task.yaml | 2 +- docs/tutorial/kubernetes-charm-django.rst | 2 +- docs/tutorial/kubernetes-charm-express.rst | 2 +- docs/tutorial/kubernetes-charm-fastapi.rst | 2 +- docs/tutorial/kubernetes-charm-flask.rst | 2 +- docs/tutorial/kubernetes-charm-go.rst | 2 +- docs/tutorial/kubernetes-charm-spring-boot.rst | 2 +- 12 files changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/tutorial/code/django/task.yaml b/docs/tutorial/code/django/task.yaml index 986cdb367..f768a4903 100644 --- a/docs/tutorial/code/django/task.yaml +++ b/docs/tutorial/code/django/task.yaml @@ -138,7 +138,7 @@ execute: | # [docs:config-nginx] juju config nginx-ingress-integrator \ - service-hostname=django-hello-world path-routes=/ rewrite-enabled=false rewrite-target=/ \ + service-hostname=django-hello-world path-routes='/.*' rewrite-enabled=false rewrite-target=/ \ ingress-class=cilium # [docs:config-nginx-end] @@ -151,7 +151,7 @@ execute: | # wait until the endpoint works # Canonical Kubernetes serves the ingress at the VM's address INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') - retry -n 5 --wait 5 curl --fail http://django-hello-world --resolve django-hello-world:80:$INGRESS_IP + retry -n 24 --wait 5 curl --fail http://django-hello-world --resolve django-hello-world:80:$INGRESS_IP # curl the Django app curl http://django-hello-world --resolve django-hello-world:80:$INGRESS_IP | grep Congratulations diff --git a/docs/tutorial/code/expressjs/task.yaml b/docs/tutorial/code/expressjs/task.yaml index f1bfa038c..28f6b6219 100644 --- a/docs/tutorial/code/expressjs/task.yaml +++ b/docs/tutorial/code/expressjs/task.yaml @@ -116,7 +116,7 @@ execute: | # [docs:config-nginx] juju config nginx-ingress-integrator \ - service-hostname=expressjs-hello-world path-routes=/ rewrite-enabled=false rewrite-target=/ \ + service-hostname=expressjs-hello-world path-routes='/.*' rewrite-enabled=false rewrite-target=/ \ ingress-class=cilium # [docs:config-nginx-end] diff --git a/docs/tutorial/code/fastapi/task.yaml b/docs/tutorial/code/fastapi/task.yaml index 167ec394e..61930b370 100644 --- a/docs/tutorial/code/fastapi/task.yaml +++ b/docs/tutorial/code/fastapi/task.yaml @@ -106,7 +106,7 @@ execute: | # [docs:config-nginx] juju config nginx-ingress-integrator \ - service-hostname=fastapi-hello-world path-routes=/ rewrite-enabled=false rewrite-target=/ \ + service-hostname=fastapi-hello-world path-routes='/.*' rewrite-enabled=false rewrite-target=/ \ ingress-class=cilium # [docs:config-nginx-end] @@ -117,7 +117,7 @@ execute: | # wait until the endpoint works # Canonical Kubernetes serves the ingress at the VM's address INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') - retry -n 5 --wait 5 curl --fail http://fastapi-hello-world --resolve fastapi-hello-world:80:$INGRESS_IP + retry -n 24 --wait 5 curl --fail http://fastapi-hello-world --resolve fastapi-hello-world:80:$INGRESS_IP # [docs:curl-init-deployment] curl http://fastapi-hello-world --resolve fastapi-hello-world:80:$INGRESS_IP @@ -198,7 +198,7 @@ execute: | juju status --relations # wait until the endpoint works - retry -n 5 --wait 5 curl --fail http://fastapi-hello-world/visitors --resolve fastapi-hello-world:80:$INGRESS_IP + retry -n 24 --wait 5 curl --fail http://fastapi-hello-world/visitors --resolve fastapi-hello-world:80:$INGRESS_IP curl http://fastapi-hello-world --resolve fastapi-hello-world:80:$INGRESS_IP | grep Hi curl http://fastapi-hello-world/visitors --resolve fastapi-hello-world:80:$INGRESS_IP | grep 1 diff --git a/docs/tutorial/code/flask/task.yaml b/docs/tutorial/code/flask/task.yaml index 1c8a8346e..d683825e6 100644 --- a/docs/tutorial/code/flask/task.yaml +++ b/docs/tutorial/code/flask/task.yaml @@ -98,7 +98,7 @@ execute: | # [docs:config-nginx] juju config nginx-ingress-integrator \ - service-hostname=flask-hello-world path-routes=/ rewrite-enabled=false rewrite-target=/ \ + service-hostname=flask-hello-world path-routes='/.*' rewrite-enabled=false rewrite-target=/ \ ingress-class=cilium # [docs:config-nginx-end] @@ -109,7 +109,7 @@ execute: | # wait until the endpoint works # Canonical Kubernetes serves the ingress at the VM's address INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') - retry -n 5 --wait 5 curl --fail http://flask-hello-world --resolve flask-hello-world:80:$INGRESS_IP + retry -n 24 --wait 5 curl --fail http://flask-hello-world --resolve flask-hello-world:80:$INGRESS_IP # [docs:curl-init-deployment] curl http://flask-hello-world --resolve flask-hello-world:80:$INGRESS_IP @@ -189,7 +189,7 @@ execute: | juju status --relations # wait until the endpoint works - retry -n 5 --wait 5 curl --fail http://flask-hello-world/visitors --resolve flask-hello-world:80:$INGRESS_IP + retry -n 24 --wait 5 curl --fail http://flask-hello-world/visitors --resolve flask-hello-world:80:$INGRESS_IP curl http://flask-hello-world --resolve flask-hello-world:80:$INGRESS_IP | grep Hi curl http://flask-hello-world/visitors --resolve flask-hello-world:80:$INGRESS_IP | grep 1 diff --git a/docs/tutorial/code/go/task.yaml b/docs/tutorial/code/go/task.yaml index 7eaf5c53e..b0dba28d2 100644 --- a/docs/tutorial/code/go/task.yaml +++ b/docs/tutorial/code/go/task.yaml @@ -110,7 +110,7 @@ execute: | # [docs:config-nginx] juju config nginx-ingress-integrator \ - service-hostname=go-hello-world path-routes=/ rewrite-enabled=false rewrite-target=/ \ + service-hostname=go-hello-world path-routes='/.*' rewrite-enabled=false rewrite-target=/ \ ingress-class=cilium # [docs:config-nginx-end] @@ -121,7 +121,7 @@ execute: | # wait until the endpoint works # Canonical Kubernetes serves the ingress at the VM's address INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') - retry -n 5 --wait 5 curl --fail http://go-hello-world --resolve go-hello-world:80:$INGRESS_IP + retry -n 24 --wait 5 curl --fail http://go-hello-world --resolve go-hello-world:80:$INGRESS_IP # [docs:curl-init-deployment] curl http://go-hello-world --resolve go-hello-world:80:$INGRESS_IP @@ -213,7 +213,7 @@ execute: | juju wait-for application go-hello-world --query='status=="active"' --timeout 20m # wait until the endpoint works - retry -n 5 --wait 5 curl --fail http://go-hello-world/visitors --resolve go-hello-world:80:$INGRESS_IP + retry -n 24 --wait 5 curl --fail http://go-hello-world/visitors --resolve go-hello-world:80:$INGRESS_IP curl http://go-hello-world --resolve go-hello-world:80:$INGRESS_IP | grep Hi curl http://go-hello-world/visitors --resolve go-hello-world:80:$INGRESS_IP | grep 1 diff --git a/docs/tutorial/code/spring-boot/task.yaml b/docs/tutorial/code/spring-boot/task.yaml index 8cbebb259..1b97cf832 100644 --- a/docs/tutorial/code/spring-boot/task.yaml +++ b/docs/tutorial/code/spring-boot/task.yaml @@ -128,7 +128,7 @@ execute: | # [docs:config-nginx] juju config nginx-ingress-integrator \ - service-hostname=spring-boot-hello-world path-routes=/ rewrite-enabled=false rewrite-target=/ \ + service-hostname=spring-boot-hello-world path-routes='/.*' rewrite-enabled=false rewrite-target=/ \ ingress-class=cilium # [docs:config-nginx-end] diff --git a/docs/tutorial/kubernetes-charm-django.rst b/docs/tutorial/kubernetes-charm-django.rst index 10a7da45c..fb82de24e 100644 --- a/docs/tutorial/kubernetes-charm-django.rst +++ b/docs/tutorial/kubernetes-charm-django.rst @@ -547,7 +547,7 @@ Let's expose the app using ingress so that we can access it. Deploy the :dedent: 2 The hostname of the app needs to be defined so that it is accessible via -the ingress. We also set the default route to the root endpoint and target +the ingress. We also route all paths to the app with the ``/.*`` pattern, since a plain ``/`` only matches the root endpoint, and target the ``cilium`` ingress class that Canonical Kubernetes uses for its ingress: .. literalinclude:: code/django/task.yaml diff --git a/docs/tutorial/kubernetes-charm-express.rst b/docs/tutorial/kubernetes-charm-express.rst index d7416a23a..89e003066 100644 --- a/docs/tutorial/kubernetes-charm-express.rst +++ b/docs/tutorial/kubernetes-charm-express.rst @@ -398,7 +398,7 @@ Let's expose the app using ingress so that we can access it. Deploy the :dedent: 2 The hostname of the app needs to be defined so that it is accessible via -the ingress. We also set the default route to the root endpoint and target +the ingress. We also route all paths to the app with the ``/.*`` pattern, since a plain ``/`` only matches the root endpoint, and target the ``cilium`` ingress class that Canonical Kubernetes uses for its ingress: .. literalinclude:: code/expressjs/task.yaml diff --git a/docs/tutorial/kubernetes-charm-fastapi.rst b/docs/tutorial/kubernetes-charm-fastapi.rst index 5a6ec1bdd..636d454a6 100644 --- a/docs/tutorial/kubernetes-charm-fastapi.rst +++ b/docs/tutorial/kubernetes-charm-fastapi.rst @@ -443,7 +443,7 @@ Let's expose the app using ingress so that we can access it. Deploy the :dedent: 2 The hostname of the app needs to be defined so that it is accessible via -the ingress. We also set the default route to the root endpoint and target +the ingress. We also route all paths to the app with the ``/.*`` pattern, since a plain ``/`` only matches the root endpoint, and target the ``cilium`` ingress class that Canonical Kubernetes uses for its ingress: .. literalinclude:: code/fastapi/task.yaml diff --git a/docs/tutorial/kubernetes-charm-flask.rst b/docs/tutorial/kubernetes-charm-flask.rst index 324cca21b..b1e03a96f 100644 --- a/docs/tutorial/kubernetes-charm-flask.rst +++ b/docs/tutorial/kubernetes-charm-flask.rst @@ -386,7 +386,7 @@ Let's expose the app using ingress so that we can access it. Deploy the :dedent: 2 The hostname of the app needs to be defined so that it is accessible via -the ingress. We also set the default route to the root endpoint and target +the ingress. We also route all paths to the app with the ``/.*`` pattern, since a plain ``/`` only matches the root endpoint, and target the ``cilium`` ingress class that Canonical Kubernetes uses for its ingress: .. literalinclude:: code/flask/task.yaml diff --git a/docs/tutorial/kubernetes-charm-go.rst b/docs/tutorial/kubernetes-charm-go.rst index c406c2889..18617b05c 100644 --- a/docs/tutorial/kubernetes-charm-go.rst +++ b/docs/tutorial/kubernetes-charm-go.rst @@ -413,7 +413,7 @@ Let's expose the app using ingress so that we can access it. Deploy the :dedent: 2 The hostname of the app needs to be defined so that it is accessible via -the ingress. We also set the default route to the root endpoint and target +the ingress. We also route all paths to the app with the ``/.*`` pattern, since a plain ``/`` only matches the root endpoint, and target the ``cilium`` ingress class that Canonical Kubernetes uses for its ingress: .. literalinclude:: code/go/task.yaml diff --git a/docs/tutorial/kubernetes-charm-spring-boot.rst b/docs/tutorial/kubernetes-charm-spring-boot.rst index 3c6c22cce..595061ca0 100644 --- a/docs/tutorial/kubernetes-charm-spring-boot.rst +++ b/docs/tutorial/kubernetes-charm-spring-boot.rst @@ -416,7 +416,7 @@ Let's expose the app using ingress so that we can access it. Deploy the :dedent: 2 The hostname of the app needs to be defined so that it is accessible via -the ingress. We also set the default route to the root endpoint and target +the ingress. We also route all paths to the app with the ``/.*`` pattern, since a plain ``/`` only matches the root endpoint, and target the ``cilium`` ingress class that Canonical Kubernetes uses for its ingress: .. literalinclude:: code/spring-boot/task.yaml From 8321db9299971bfa32e573eef3ff4042bb279f5b Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Thu, 22 Oct 2026 14:19:11 +0700 Subject: [PATCH 26/28] test: give the tutorial endpoint checks more time after a refresh Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/tutorial/code/django/task.yaml | 6 +++--- docs/tutorial/code/expressjs/task.yaml | 6 +++--- docs/tutorial/code/fastapi/task.yaml | 4 ++-- docs/tutorial/code/flask/task.yaml | 4 ++-- docs/tutorial/code/go/task.yaml | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/tutorial/code/django/task.yaml b/docs/tutorial/code/django/task.yaml index f768a4903..ffb032121 100644 --- a/docs/tutorial/code/django/task.yaml +++ b/docs/tutorial/code/django/task.yaml @@ -197,7 +197,7 @@ execute: | juju wait-for application django-hello-world --query='status=="active"' --timeout 10m # curl and check that the response is Hello, world! - retry -n 5 --wait 5 sh -c "curl http://django-hello-world --resolve django-hello-world:80:$INGRESS_IP | grep Hello" + retry -n 24 --wait 5 sh -c "curl http://django-hello-world --resolve django-hello-world:80:$INGRESS_IP | grep Hello" cd .. # Update django_hello_world/greeting/views.py @@ -228,7 +228,7 @@ execute: | juju wait-for application django-hello-world --query='status=="active"' --timeout 10m # curl and check that the response is still Hello, world! - retry -n 5 --wait 5 sh -c "curl http://django-hello-world --resolve django-hello-world:80:$INGRESS_IP | grep Hello" + retry -n 24 --wait 5 sh -c "curl http://django-hello-world --resolve django-hello-world:80:$INGRESS_IP | grep Hello" # [docs:change-config] juju config django-hello-world greeting='Hi!' @@ -238,7 +238,7 @@ execute: | juju wait-for application django-hello-world --query='status=="active"' --timeout 10m # curl and check that the response is now Hi - retry -n 5 --wait 5 sh -c "curl http://django-hello-world --resolve django-hello-world:80:$INGRESS_IP | grep Hi" + retry -n 24 --wait 5 sh -c "curl http://django-hello-world --resolve django-hello-world:80:$INGRESS_IP | grep Hi" # [docs:clean-environment] charmcraft clean diff --git a/docs/tutorial/code/expressjs/task.yaml b/docs/tutorial/code/expressjs/task.yaml index 28f6b6219..983604eb8 100644 --- a/docs/tutorial/code/expressjs/task.yaml +++ b/docs/tutorial/code/expressjs/task.yaml @@ -158,7 +158,7 @@ execute: | --query='status=="active"' --timeout 10m # curl and check that the response is Hello - retry -n 5 --wait 2 sh -c "curl http://expressjs-hello-world \ + retry -n 24 --wait 5 sh -c "curl http://expressjs-hello-world \ --resolve expressjs-hello-world:80:$INGRESS_IP | grep Hello" # [docs:change-config] @@ -170,7 +170,7 @@ execute: | --query='status=="active"' --timeout 10m # curl and check that the response is now Hi - retry -n 5 --wait 2 sh -c "curl http://expressjs-hello-world \ + retry -n 24 --wait 5 sh -c "curl http://expressjs-hello-world \ --resolve expressjs-hello-world:80:$INGRESS_IP | grep Hi" cd .. @@ -226,7 +226,7 @@ execute: | --query='status=="active"' --timeout 20m juju status --relations - retry -n 5 --wait 2 sh -c "curl http://expressjs-hello-world \ + retry -n 24 --wait 5 sh -c "curl http://expressjs-hello-world \ --resolve expressjs-hello-world:80:$INGRESS_IP | grep Hi " curl http://expressjs-hello-world/visitors \ --resolve expressjs-hello-world:80:$INGRESS_IP | grep 1 diff --git a/docs/tutorial/code/fastapi/task.yaml b/docs/tutorial/code/fastapi/task.yaml index 61930b370..d9edc782c 100644 --- a/docs/tutorial/code/fastapi/task.yaml +++ b/docs/tutorial/code/fastapi/task.yaml @@ -148,7 +148,7 @@ execute: | juju wait-for application fastapi-hello-world --query='status=="active"' --timeout 10m # curl and check that the response is Hello - retry -n 5 --wait 5 sh -c "curl http://fastapi-hello-world --resolve fastapi-hello-world:80:$INGRESS_IP | grep Hello" + retry -n 24 --wait 5 sh -c "curl http://fastapi-hello-world --resolve fastapi-hello-world:80:$INGRESS_IP | grep Hello" # [docs:change-config] juju config fastapi-hello-world greeting='Hi!' @@ -158,7 +158,7 @@ execute: | juju wait-for application fastapi-hello-world --query='status=="active"' --timeout 10m # curl and check that the response is now Hi - retry -n 5 --wait 5 sh -c "curl http://fastapi-hello-world --resolve fastapi-hello-world:80:$INGRESS_IP | grep Hi" + retry -n 24 --wait 5 sh -c "curl http://fastapi-hello-world --resolve fastapi-hello-world:80:$INGRESS_IP | grep Hi" cd .. cat visitors_migrate.py >> migrate.py diff --git a/docs/tutorial/code/flask/task.yaml b/docs/tutorial/code/flask/task.yaml index d683825e6..81cd3aad0 100644 --- a/docs/tutorial/code/flask/task.yaml +++ b/docs/tutorial/code/flask/task.yaml @@ -140,7 +140,7 @@ execute: | juju wait-for application flask-hello-world --query='status=="active"' --timeout 10m # curl and check that the response is Hello - retry -n 5 --wait 5 sh -c "curl http://flask-hello-world --resolve flask-hello-world:80:$INGRESS_IP | grep Hello" + retry -n 24 --wait 5 sh -c "curl http://flask-hello-world --resolve flask-hello-world:80:$INGRESS_IP | grep Hello" # [docs:change-config] juju config flask-hello-world greeting='Hi!' @@ -150,7 +150,7 @@ execute: | juju wait-for application flask-hello-world --query='status=="active"' --timeout 10m # curl and check that the response is now Hi - retry -n 5 --wait 5 sh -c "curl http://flask-hello-world --resolve flask-hello-world:80:$INGRESS_IP | grep Hi" + retry -n 24 --wait 5 sh -c "curl http://flask-hello-world --resolve flask-hello-world:80:$INGRESS_IP | grep Hi" cd .. cat visitors_migrate.py >> migrate.py diff --git a/docs/tutorial/code/go/task.yaml b/docs/tutorial/code/go/task.yaml index b0dba28d2..e976efbb3 100644 --- a/docs/tutorial/code/go/task.yaml +++ b/docs/tutorial/code/go/task.yaml @@ -152,7 +152,7 @@ execute: | juju wait-for application go-hello-world --query='status=="active"' --timeout 10m # curl and check that the response is Hello - retry -n 5 --wait 5 sh -c "curl http://go-hello-world --resolve go-hello-world:80:$INGRESS_IP | grep Hello" + retry -n 24 --wait 5 sh -c "curl http://go-hello-world --resolve go-hello-world:80:$INGRESS_IP | grep Hello" # [docs:change-config] juju config go-hello-world greeting='Hi!' @@ -162,7 +162,7 @@ execute: | juju wait-for application go-hello-world --query='status=="active"' --timeout 10m # curl and check that the response is now Hi - retry -n 5 --wait 5 sh -c "curl http://go-hello-world --resolve go-hello-world:80:$INGRESS_IP | grep Hi" + retry -n 24 --wait 5 sh -c "curl http://go-hello-world --resolve go-hello-world:80:$INGRESS_IP | grep Hi" cd .. cat visitors_migrate.sh >> migrate.sh From 5f8f842ac56f206a3d65c1fffdc8995ffbf17c4b Mon Sep 17 00:00:00 2001 From: florentianayuwono Date: Thu, 22 Oct 2026 21:34:27 +0700 Subject: [PATCH 27/28] test: wait for the ingress in Express and Spring Boot, use PostgreSQL 16 in clearsession Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/howto/code/custom-action-clearsession/task.yaml | 2 +- docs/tutorial/code/expressjs/task.yaml | 4 ++++ docs/tutorial/code/spring-boot/task.yaml | 4 ++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/howto/code/custom-action-clearsession/task.yaml b/docs/howto/code/custom-action-clearsession/task.yaml index 39701e7b4..3f2756ffb 100644 --- a/docs/howto/code/custom-action-clearsession/task.yaml +++ b/docs/howto/code/custom-action-clearsession/task.yaml @@ -85,7 +85,7 @@ execute: | django-app-image=localhost:32000/django-hello-world:0.1 # Deploy + integrate with postgres - juju deploy postgresql-k8s --trust --channel 14/edge + juju deploy postgresql-k8s --trust --channel 16/stable juju integrate django-hello-world postgresql-k8s # Check that django-hello-world and postgres are active idle diff --git a/docs/tutorial/code/expressjs/task.yaml b/docs/tutorial/code/expressjs/task.yaml index 983604eb8..4c136ae79 100644 --- a/docs/tutorial/code/expressjs/task.yaml +++ b/docs/tutorial/code/expressjs/task.yaml @@ -126,6 +126,10 @@ execute: | juju wait-for application nginx-ingress-integrator \ --query='status=="active"' --timeout 10m + # wait until the endpoint works + INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') + retry -n 24 --wait 5 curl --fail -I http://expressjs-hello-world --resolve expressjs-hello-world:80:$INGRESS_IP + # [docs:curl-init-deployment] # Canonical Kubernetes serves the ingress at the VM's address INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') diff --git a/docs/tutorial/code/spring-boot/task.yaml b/docs/tutorial/code/spring-boot/task.yaml index 1b97cf832..d3a6d93da 100644 --- a/docs/tutorial/code/spring-boot/task.yaml +++ b/docs/tutorial/code/spring-boot/task.yaml @@ -136,6 +136,10 @@ execute: | juju wait-for application spring-boot-hello-world --query='status=="active"' --timeout 10m juju wait-for application nginx-ingress-integrator --query='status=="active"' --timeout 10m + # wait until the endpoint works + INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') + retry -n 24 --wait 5 curl --fail http://spring-boot-hello-world --resolve spring-boot-hello-world:80:$INGRESS_IP + # [docs:curl-init-deployment] # Canonical Kubernetes serves the ingress at the VM's address INGRESS_IP=$(ip -4 route get 2.2.2.2 | grep -oP 'src \K[0-9.]+') From 5d34c5647d7b3c889a21a921ff69faa61f72c556 Mon Sep 17 00:00:00 2001 From: florentianayuwono <76247368+florentianayuwono@users.noreply.github.com> Date: Fri, 23 Oct 2026 00:06:46 +0700 Subject: [PATCH 28/28] Update docs/release-notes/charmcraft-4.5.rst Co-authored-by: Alex Lowe Signed-off-by: florentianayuwono <76247368+florentianayuwono@users.noreply.github.com> --- docs/release-notes/charmcraft-4.5.rst | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/docs/release-notes/charmcraft-4.5.rst b/docs/release-notes/charmcraft-4.5.rst index e0be4826e..f10a11178 100644 --- a/docs/release-notes/charmcraft-4.5.rst +++ b/docs/release-notes/charmcraft-4.5.rst @@ -124,16 +124,6 @@ Charmcraft 4.5 includes the following documentation changes: ingress. -Backwards-incompatible changes ------------------------------- - -The following changes are incompatible with previous versions of Charmcraft. - - - -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Feature deprecations