diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 23e5cc81..b106d68d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -24,3 +24,18 @@ jobs: self-hosted-runner-image: "noble" working-directory: ${{ matrix.charm.working-directory }} with-uv: true + build-snap-haproxy-route-policy: + name: Build Snap + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Build Snap + id: snapcraft + uses: snapcore/action-build@v1 + with: + path: haproxy-route-policy + - name: Upload Snap Artifact + uses: actions/upload-artifact@v5 + with: + name: snap + path: ${{ steps.snapcraft.outputs.snap }} diff --git a/docs/release-notes/artifacts/pr0415.yaml b/docs/release-notes/artifacts/pr0415.yaml new file mode 100644 index 00000000..8bb7166c --- /dev/null +++ b/docs/release-notes/artifacts/pr0415.yaml @@ -0,0 +1,20 @@ +version_schema: 2 + +changes: + - title: Added snap packaging and runtime scripts for haproxy-route-policy + author: tphan025 + type: minor + description: > + Added snap packaging for the haproxy-route-policy app, including + `snap/snapcraft.yaml`, install/configure hooks, and helper scripts to run + Gunicorn and Django management commands with snap configuration values. + Added Gunicorn as a dependency and configured uv build metadata in + `pyproject.toml` for packaging. Updated the app README with a basic setup + flow for PostgreSQL and snap configuration. + urls: + pr: + - https://github.com/canonical/haproxy-operator/pull/415 + related_doc: + related_issue: + visibility: public + highlight: false diff --git a/haproxy-route-policy/README.md b/haproxy-route-policy/README.md index e69de29b..f97e7ced 100644 --- a/haproxy-route-policy/README.md +++ b/haproxy-route-policy/README.md @@ -0,0 +1,24 @@ +#### Basic setup + +Start a PostgreSQL database: + +``` +docker run -d --name postgres -p 127.0.0.1:5432:5432 -e POSTGRES_PASSWORD=postgres -e POSTGRES_USERNAME=postgres postgres:latest +``` + +Basic snap configurations: + +``` +sudo snap set haproxy-route-policy database-password=postgres +sudo snap set haproxy-route-policy database-host=localhost +sudo snap set haproxy-route-policy database-port=5432 +sudo snap set haproxy-route-policy database-user=postgres +sudo snap set haproxy-route-policy database-name=postgres +``` + +## Learn more +* [Read more](https://charmhub.io/haproxy-operator/docs) + +## Project and community +* [Issues](https://github.com/canonical/haproxy-operator/issues) +* [Matrix](https://matrix.to/#/#charmhub-charmdev:ubuntu.com) diff --git a/haproxy-route-policy/pyproject.toml b/haproxy-route-policy/pyproject.toml index 886b7f29..e076f2a5 100644 --- a/haproxy-route-policy/pyproject.toml +++ b/haproxy-route-policy/pyproject.toml @@ -8,11 +8,20 @@ dependencies = [ "django>=6.0.3", "djangorestframework>=3.16.1", "djangorestframework-simplejwt>=5.5.1", + "gunicorn>=23.0.0", "psycopg2-binary>=2.9.11", "validators>=0.35.0", "whitenoise>=6.12.0", ] +[build-system] +requires = ["uv_build>=0.7.2,<1"] +build-backend = "uv_build" + +[tool.uv.build-backend] +module-root = "" +module-name = ["haproxy_route_policy", "policy"] + [dependency-groups] auth = [ "djangorestframework-simplejwt>=5.5.1", diff --git a/haproxy-route-policy/snap/hooks/configure b/haproxy-route-policy/snap/hooks/configure new file mode 100644 index 00000000..75634aa4 --- /dev/null +++ b/haproxy-route-policy/snap/hooks/configure @@ -0,0 +1,52 @@ +#!/bin/sh + +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +DJANGO_DEBUG="$(snapctl get debug)" +export DJANGO_DEBUG + +case "$DJANGO_DEBUG" in + "true") ;; + "false") ;; + *) + >&2 echo "'$DJANGO_DEBUG is not a supported value for django_debug. Possible values are true, false" + return 1 + ;; +esac + +DJANGO_LOG_LEVEL="$(snapctl get log-level)" +export DJANGO_LOG_LEVEL + +case "$DJANGO_LOG_LEVEL" in + "debug") ;; + "info") ;; + "warning") ;; + "error") ;; + "critical") ;; + "DEBUG") ;; + "INFO") ;; + "WARNING") ;; + "ERROR") ;; + "CRITICAL") ;; + *) + >&2 echo "'$DJANGO_LOG_LEVEL is not a supported value for debug. Possible values are debug, info, warning, error, critical" + return 1 + ;; +esac + +DJANGO_ALLOWED_HOSTS="$(snapctl get allowed-hosts)" +export DJANGO_ALLOWED_HOSTS +DJANGO_DATABASE_PASSWORD="$(snapctl get database-password)" +export DJANGO_DATABASE_PASSWORD +DJANGO_DATABASE_HOST="$(snapctl get database-host)" +export DJANGO_DATABASE_HOST +DJANGO_DATABASE_PORT="$(snapctl get database-port)" +export DJANGO_DATABASE_PORT +DJANGO_DATABASE_USER="$(snapctl get database-user)" +export DJANGO_DATABASE_USER +DJANGO_DATABASE_NAME="$(snapctl get database-name)" +export DJANGO_DATABASE_NAME + +snapctl stop "$SNAP_INSTANCE_NAME" +snapctl start "$SNAP_INSTANCE_NAME" diff --git a/haproxy-route-policy/snap/hooks/install b/haproxy-route-policy/snap/hooks/install new file mode 100755 index 00000000..81c5db0c --- /dev/null +++ b/haproxy-route-policy/snap/hooks/install @@ -0,0 +1,13 @@ +#!/bin/sh + +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +set -xe + +# set default configuration values +snapctl set debug='false' +snapctl set log-level='INFO' +snapctl set allowed-hosts='["*"]' +SECRET_KEY="$(tr -dc a-zA-Z0-9 < /dev/urandom | head -c 50)" +snapctl set secret-key="$SECRET_KEY" diff --git a/haproxy-route-policy/snap/scripts/bin/gunicorn-start b/haproxy-route-policy/snap/scripts/bin/gunicorn-start new file mode 100755 index 00000000..19de8ea5 --- /dev/null +++ b/haproxy-route-policy/snap/scripts/bin/gunicorn-start @@ -0,0 +1,33 @@ +#!/bin/sh + +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +set -xe + +DJANGO_SECRET_KEY="$(snapctl get secret-key)" +export DJANGO_SECRET_KEY +DJANGO_DEBUG="$(snapctl get debug)" +export DJANGO_DEBUG +DJANGO_ALLOWED_HOSTS="$(snapctl get allowed-hosts)" +export DJANGO_ALLOWED_HOSTS +DJANGO_LOG_LEVEL="$(snapctl get log-level)" +export DJANGO_LOG_LEVEL +DJANGO_DATABASE_PASSWORD="$(snapctl get database-password)" +export DJANGO_DATABASE_PASSWORD +DJANGO_DATABASE_HOST="$(snapctl get database-host)" +export DJANGO_DATABASE_HOST +DJANGO_DATABASE_PORT="$(snapctl get database-port)" +export DJANGO_DATABASE_PORT +DJANGO_DATABASE_USER="$(snapctl get database-user)" +export DJANGO_DATABASE_USER +DJANGO_DATABASE_NAME="$(snapctl get database-name)" +export DJANGO_DATABASE_NAME + +LOG_LEVEL="info" +if [ "$DJANGO_DEBUG" = "true" ]; then + LOG_LEVEL="debug" +fi + +exec gunicorn --bind 0.0.0.0:8080 haproxy_route_policy.wsgi \ + --capture-output --log-level="$LOG_LEVEL" diff --git a/haproxy-route-policy/snap/scripts/bin/manage b/haproxy-route-policy/snap/scripts/bin/manage new file mode 100755 index 00000000..f2543820 --- /dev/null +++ b/haproxy-route-policy/snap/scripts/bin/manage @@ -0,0 +1,30 @@ +#!/bin/sh + +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +set -e + +DJANGO_SECRET_KEY="$(snapctl get secret-key)" +export DJANGO_SECRET_KEY +DJANGO_DEBUG="$(snapctl get debug)" +export DJANGO_DEBUG +DJANGO_ALLOWED_HOSTS="$(snapctl get allowed-hosts)" +if [ -z "$DJANGO_ALLOWED_HOSTS" ]; then + DJANGO_ALLOWED_HOSTS="[]" +fi +export DJANGO_ALLOWED_HOSTS +DJANGO_LOG_LEVEL="$(snapctl get log-level)" +export DJANGO_LOG_LEVEL +DJANGO_DATABASE_PASSWORD="$(snapctl get database-password)" +export DJANGO_DATABASE_PASSWORD +DJANGO_DATABASE_HOST="$(snapctl get database-host)" +export DJANGO_DATABASE_HOST +DJANGO_DATABASE_PORT="$(snapctl get database-port)" +export DJANGO_DATABASE_PORT +DJANGO_DATABASE_USER="$(snapctl get database-user)" +export DJANGO_DATABASE_USER +DJANGO_DATABASE_NAME="$(snapctl get database-name)" +export DJANGO_DATABASE_NAME + +exec $SNAP/bin/uv run $SNAP/app/manage.py "$@" --settings=haproxy_route_policy.settings diff --git a/haproxy-route-policy/snap/snapcraft.yaml b/haproxy-route-policy/snap/snapcraft.yaml new file mode 100644 index 00000000..813eac1f --- /dev/null +++ b/haproxy-route-policy/snap/snapcraft.yaml @@ -0,0 +1,54 @@ +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +name: haproxy-route-policy +base: core24 +version: "0.1" +license: Apache-2.0 +summary: HAProxy Route Policy API +description: | + This snap bundles the HAProxy Route Policy Django application to be included in the haproxy-route-policy-operator. +confinement: strict +platforms: + amd64: + build-on: [amd64] + build-for: [amd64] + +system-usernames: + _daemon_: shared + +parts: + haproxy-route-policy: + plugin: uv + source: . + build-snaps: + - astral-uv + stage-snaps: + - astral-uv + override-build: | + # Also copy the source code to the install directory for the manage.py script + cp -r . $SNAPCRAFT_PART_INSTALL/app + chown -R 584792:584792 $SNAPCRAFT_PART_INSTALL/app + craftctl default + + scripts: + plugin: dump + source: ./snap/scripts + override-prime: | + craftctl default + chmod -R +rx $CRAFT_PRIME/bin + +apps: + gunicorn: + command: bin/gunicorn-start + daemon: simple + restart-condition: always + plugs: + - network + - network-bind + + manage: + command: bin/manage + plugs: + - network + - network-bind diff --git a/haproxy-route-policy/uv.lock b/haproxy-route-policy/uv.lock index 924df501..c46a486c 100644 --- a/haproxy-route-policy/uv.lock +++ b/haproxy-route-policy/uv.lock @@ -231,14 +231,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/5f/d908ce938356b209d4d27a7fb159ab9100b8814396a69c0204bb66e38703/djangorestframework_types-0.9.0-py3-none-any.whl", hash = "sha256:5e4258fe43774d0a3d018780170bd702bf615407fe244453ea5ec6e6676b98c4", size = 54947, upload-time = "2024-10-10T00:42:02.311Z" }, ] +[[package]] +name = "gunicorn" +version = "25.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/13/dd3f8e40ea3ee907a6cbf3d1f1f81afcc3ecd0087d313baabfe95372f15c/gunicorn-25.2.0.tar.gz", hash = "sha256:10bd7adb36d44945d97d0a1fdf9a0fb086ae9c7b39e56b4dece8555a6bf4a09c", size = 632709, upload-time = "2026-03-24T22:49:54.433Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/53/fb024445837e02cd5cf989cf349bfac6f3f433c05184ea5d49c8ade751c6/gunicorn-25.2.0-py3-none-any.whl", hash = "sha256:88f5b444d0055bf298435384af7294f325e2273fd37ba9f9ff7b98e0a1e5dfdc", size = 211659, upload-time = "2026-03-24T22:49:52.528Z" }, +] + [[package]] name = "haproxy-route-policy" version = "0.1.0" -source = { virtual = "." } +source = { editable = "." } dependencies = [ { name = "django" }, { name = "djangorestframework" }, { name = "djangorestframework-simplejwt" }, + { name = "gunicorn" }, { name = "psycopg2-binary" }, { name = "validators" }, { name = "whitenoise" }, @@ -272,6 +285,7 @@ requires-dist = [ { name = "django", specifier = ">=6.0.3" }, { name = "djangorestframework", specifier = ">=3.16.1" }, { name = "djangorestframework-simplejwt", specifier = ">=5.5.1" }, + { name = "gunicorn", specifier = ">=23.0.0" }, { name = "psycopg2-binary", specifier = ">=2.9.11" }, { name = "validators", specifier = ">=0.35.0" }, { name = "whitenoise", specifier = ">=6.12.0" }, @@ -415,6 +429,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + [[package]] name = "pathspec" version = "1.0.4"