diff --git a/README.md b/README.md index 38a34c4..7adc93e 100644 --- a/README.md +++ b/README.md @@ -33,18 +33,35 @@ does. ## Layout ``` -playbook.yml Ansible deployment (hosts: webservers) -inventory.ini Inventory (localhost + commented remote example) -trade-hub/ - podman-compose.yml Local three-service stack (db + backend + frontend) - backend/ FastAPI app, tests, data fetch script, Dockerfile - frontend/ nginx config + static inventory UI +playbook.yml Example play using the role (hosts: webservers) +inventory.ini Inventory (localhost + commented remote example) +teardown.yml Removes this project's containers and data +roles/trade_hub/ + README.md Role usage and variables + defaults/main.yml Configurable defaults + tasks/main.yml Stage source, build images, run containers + files/trade-hub/ + podman-compose.yml Local three-service stack (db + backend + frontend) + backend/ FastAPI app, tests, data fetch script, Dockerfile + frontend/ nginx config + static inventory UI ``` +The Ansible deployment is a self-contained role. To deploy it from an existing +playbook, copy `roles/trade_hub` into that repository's `roles/` directory and +add the role to the play: + +```yaml + - role: trade_hub + tags: [trade_hub] +``` + +See `roles/trade_hub/README.md` for the variables. The play-level `become: true` +used by a typical server play makes podman run as root. + ## Run locally with Podman Compose ```bash -cd trade-hub +cd roles/trade_hub/files/trade-hub podman-compose up -d --build # or: podman compose up -d --build ``` @@ -60,7 +77,8 @@ POSTGRES_PASSWORD='choose-something-strong' podman-compose up -d --build ## Deploy with Ansible -Deploys to every host in the `webservers` group (defaults to localhost): +The example play deploys the role to every host in the `webservers` group +(defaults to localhost): ```bash POSTGRES_PASSWORD='choose-something-strong' ansible-playbook playbook.yml @@ -73,16 +91,22 @@ To deploy to a remote server, edit `inventory.ini`: server1 ansible_host=203.0.113.10 ansible_user=deploy ``` -Then run the same command from this directory. The playbook: +Then run the same command from this directory. The role: -1. installs podman if missing, -2. removes the legacy `my-apache-server` container if present, -3. builds `sc-inventory-backend` / `sc-inventory-frontend` images (skipped when - they already exist; set `-e force_rebuild=true` to rebuild), +1. installs podman if missing (only when `trade_hub_install_podman: true`), +2. optionally removes the legacy `my-apache-server` container + (`trade_hub_remove_legacy_apache: true`), +3. stages the application source in `/opt/trade-hub` and builds the + `sc-inventory-backend` / `sc-inventory-frontend` images (skipped when the + source is unchanged and the images already exist; set + `-e trade_hub_force_rebuild=true` to force it), 4. creates the `sc-inventory` podman network and the `sc-inventory-data` volume, 5. runs PostgreSQL, the backend, and the frontend containers (only the frontend publishes a port: 8080), -6. verifies `http://localhost:8080/healthz` returns 200. +6. verifies `http://127.0.0.1:8080/healthz` returns 200. + +The old standalone playbook's variables (`force_rebuild`, `cookie_secure`, …) +became `trade_hub_*` variables; see `roles/trade_hub/README.md`. ## Reset for testing (remove everything) @@ -165,7 +189,8 @@ The stack publishes plain HTTP on port 8080. Session cookies therefore travel in cleartext, and `COOKIE_SECURE` defaults to `false` so local development works. Before putting this on the open internet, terminate TLS in front of the frontend (Caddy, nginx with certificates, or a load balancer) and set -`COOKIE_SECURE=true` (compose env var) or `-e cookie_secure=true` (Ansible). +`COOKIE_SECURE=true` (compose env var) or +`-e trade_hub_cookie_secure=true` (Ansible). Also change `POSTGRES_PASSWORD` from its development default. ## Verify @@ -195,7 +220,7 @@ curl -fsS -b cookies.txt http://localhost:8080/api/v1/me/inventory ## Backend development ```bash -cd trade-hub/backend +cd roles/trade_hub/files/trade-hub/backend python3 -m venv .venv .venv/bin/pip install -r requirements.txt -r requirements-dev.txt .venv/bin/python -m pytest tests/ -q # 83 tests, no database needed diff --git a/playbook.yml b/playbook.yml index caa90c1..95f6f15 100644 --- a/playbook.yml +++ b/playbook.yml @@ -1,126 +1,23 @@ --- +# Example play that exercises the trade_hub role against the local inventory. +# To use the role in an existing playbook, copy roles/trade_hub into that +# repository's roles/ directory and add: +# +# roles: +# - role: trade_hub +# tags: [trade_hub] +# - name: Deploy Star Citizen Inventory (trade-hub) hosts: webservers vars: - backend_image: sc-inventory-backend - frontend_image: sc-inventory-frontend - postgres_image: docker.io/library/postgres:16-alpine - image_tag: latest - network_name: sc-inventory - frontend_port: "8080:80" - postgres_volume: sc-inventory-data - postgres_password: "{{ lookup('env', 'POSTGRES_PASSWORD') | default('sc_inventory_dev', true) }}" - database_url: "postgresql+psycopg://sc_inventory:{{ postgres_password }}@db:5432/sc_inventory" - # Set to true once the site is served over TLS. - cookie_secure: "false" - # Set to true to force image rebuilds even when the images already exist - force_rebuild: false + # The standalone play used to install podman; in a larger playbook the + # "packages" role normally provides it instead. + trade_hub_install_podman: true + trade_hub_postgres_password: "{{ lookup('env', 'POSTGRES_PASSWORD') | default('sc_inventory_dev', true) }}" + trade_hub_cookie_secure: "false" + trade_hub_force_rebuild: false - tasks: - - name: Check whether podman is installed - ansible.builtin.command: podman --version - register: podman_check - failed_when: false - changed_when: false - - - name: Install podman if missing - become: true - ansible.builtin.package: - name: podman - state: present - when: podman_check.rc != 0 - - - name: Remove legacy Apache container if present - containers.podman.podman_container: - name: my-apache-server - state: absent - - - name: Check if backend image exists - ansible.builtin.command: podman image exists {{ backend_image }}:{{ image_tag }} - register: backend_image_check - failed_when: false - changed_when: false - - - name: Check if frontend image exists - ansible.builtin.command: podman image exists {{ frontend_image }}:{{ image_tag }} - register: frontend_image_check - failed_when: false - changed_when: false - - - name: Build backend image - ansible.builtin.command: > - podman build -t {{ backend_image }}:{{ image_tag }} - {{ playbook_dir }}/trade-hub/backend - when: backend_image_check.rc != 0 or force_rebuild | bool - - - name: Build frontend image - ansible.builtin.command: > - podman build -t {{ frontend_image }}:{{ image_tag }} - {{ playbook_dir }}/trade-hub/frontend - when: frontend_image_check.rc != 0 or force_rebuild | bool - - - name: Create podman network - containers.podman.podman_network: - name: "{{ network_name }}" - state: present - - - name: Create PostgreSQL data volume - containers.podman.podman_volume: - name: "{{ postgres_volume }}" - state: present - - - name: Run PostgreSQL container (internal only) - containers.podman.podman_container: - name: sc-inventory-db - image: "{{ postgres_image }}" - state: started - network: "{{ network_name }}" - network_aliases: - - db - env: - POSTGRES_USER: sc_inventory - POSTGRES_PASSWORD: "{{ postgres_password }}" - POSTGRES_DB: sc_inventory - volumes: - - "{{ postgres_volume }}:/var/lib/postgresql/data:Z" - restart_policy: unless-stopped - - - name: Run backend container (internal only) - containers.podman.podman_container: - name: sc-inventory-backend - image: "{{ backend_image }}:{{ image_tag }}" - state: started - # podman does not recreate a container when its image tag resolves to - # a new ID, so without this a rebuilt image would be ignored. - recreate: true - network: "{{ network_name }}" - # Network alias "backend" matches the compose service name, so the - # nginx upstream (http://backend:8000) resolves in both deployments. - network_aliases: - - backend - env: - DATABASE_URL: "{{ database_url }}" - DB_WAIT_SECONDS: "60" - COOKIE_SECURE: "{{ cookie_secure }}" - restart_policy: unless-stopped - - - name: Run frontend container (published port) - containers.podman.podman_container: - name: sc-inventory-frontend - image: "{{ frontend_image }}:{{ image_tag }}" - state: started - recreate: true - network: "{{ network_name }}" - ports: - - "{{ frontend_port }}" - restart_policy: unless-stopped - - - name: Verify the stack responds - ansible.builtin.uri: - url: http://localhost:8080/healthz - status_code: 200 - register: health_result - until: health_result.status == 200 - retries: 8 - delay: 10 + roles: + - role: trade_hub + tags: [trade_hub] diff --git a/roles/trade_hub/README.md b/roles/trade_hub/README.md new file mode 100644 index 0000000..d1b4b16 --- /dev/null +++ b/roles/trade_hub/README.md @@ -0,0 +1,82 @@ +# trade_hub + +Deploys the Free People of the Verse trade hub: a FastAPI backend, an nginx +frontend and PostgreSQL, all run by podman on the target host. The role stages +its own copy of the application source from `files/trade-hub/` and builds the +backend and frontend images locally, so it does not need a registry. + +## Usage + +Add the role to an existing play. It is safe to run after a `packages` role that +already installs podman: + +```yaml +- hosts: internal-apps + become: true + vars_files: + - ./vars.yml + roles: + - role: packages + tags: [packages] + # ... + - role: trade_hub + tags: [trade_hub] +``` + +The play-level `become: true` makes podman run as root. If you run the role +without become, podman runs rootless and the containers belong to the connecting +user. + +The playbook's `Pull images` task is unrelated to this role: the role builds its +own backend/frontend images from the staged source. The PostgreSQL image is +pulled by the role if it is not already present. + +## Required variables + +Set a real database password in `vars.yml` (or the environment of the control +node): + +```yaml +trade_hub_postgres_password: "choose-something-strong" +``` + +If it is omitted, the role falls back to `POSTGRES_PASSWORD` from the control +node environment, then to the development default `sc_inventory_dev`. + +## Common variables + +| Variable | Default | Purpose | +| --- | --- | --- | +| `trade_hub_app_dir` | `/opt/trade-hub` | Where the build context is staged | +| `trade_hub_install_podman` | `false` | Install podman if it is missing | +| `trade_hub_build_images` | `true` | Build backend/frontend images on the target | +| `trade_hub_force_rebuild` | `false` | Rebuild images and restart containers | +| `trade_hub_image_tag` | `latest` | Tag applied to built images | +| `trade_hub_backend_image` | `localhost/sc-inventory-backend` | Backend image name | +| `trade_hub_frontend_image` | `localhost/sc-inventory-frontend` | Frontend image name | +| `trade_hub_postgres_image` | `docker.io/library/postgres:16-alpine` | Database image | +| `trade_hub_postgres_user` | `sc_inventory` | Database user | +| `trade_hub_postgres_db` | `sc_inventory` | Database name | +| `trade_hub_postgres_password` | env `POSTGRES_PASSWORD` or `sc_inventory_dev` | Database password | +| `trade_hub_network` | `sc-inventory` | Podman network name | +| `trade_hub_volume` | `sc-inventory-data` | PostgreSQL data volume | +| `trade_hub_frontend_bind` | `0.0.0.0` | Address the frontend publishes on | +| `trade_hub_frontend_port` | `8080` | Host port for the frontend | +| `trade_hub_cookie_secure` | `false` | Mark session cookies `Secure` (enable behind TLS) | +| `trade_hub_healthcheck_url` | `http://127.0.0.1:/healthz` | Post-deploy check | +| `trade_hub_remove_legacy_apache` | `false` | Remove a container named `my-apache-server` | +| `trade_hub_enable_restart_service` | `false` | Enable `podman-restart.service` | + +The container names also default to `sc-inventory-db`, `sc-inventory-backend` +and `sc-inventory-frontend`, matching the previous standalone playbook so an +existing installation is upgraded in place. + +## Example deployment + +```bash +ansible-playbook site.yml --tags trade_hub -e trade_hub_postgres_password='...' +``` + +The role verifies `http://127.0.0.1:8080/healthz` after starting the containers. +Only the frontend publishes a port; the database and backend stay on the +internal podman network. diff --git a/roles/trade_hub/defaults/main.yml b/roles/trade_hub/defaults/main.yml new file mode 100644 index 0000000..3dc7ce9 --- /dev/null +++ b/roles/trade_hub/defaults/main.yml @@ -0,0 +1,51 @@ +--- +# Where the application build context is staged on the target host. +trade_hub_app_dir: /opt/trade-hub + +# Install podman as part of this role. Leave false when another role (for +# example "packages") already provides podman. +trade_hub_install_podman: false + +# Images. These are built locally by default; point them at a registry if you +# prefer to pull prebuilt images. +trade_hub_image_tag: latest +trade_hub_backend_image: localhost/sc-inventory-backend +trade_hub_frontend_image: localhost/sc-inventory-frontend +trade_hub_postgres_image: docker.io/library/postgres:16-alpine +trade_hub_build_images: true +trade_hub_force_rebuild: false + +# Container names. Kept compatible with the previous standalone playbook so an +# existing installation is upgraded in place. +trade_hub_db_container: sc-inventory-db +trade_hub_backend_container: sc-inventory-backend +trade_hub_frontend_container: sc-inventory-frontend + +# Database. +trade_hub_postgres_user: sc_inventory +trade_hub_postgres_db: sc_inventory +trade_hub_postgres_password: "{{ lookup('env', 'POSTGRES_PASSWORD') | default('sc_inventory_dev', true) }}" +trade_hub_database_url: "postgresql+psycopg://{{ trade_hub_postgres_user }}:{{ trade_hub_postgres_password }}@db:5432/{{ trade_hub_postgres_db }}" +trade_hub_db_wait_seconds: 60 + +# Networking. +trade_hub_network: sc-inventory +trade_hub_volume: sc-inventory-data +trade_hub_frontend_bind: 0.0.0.0 +trade_hub_frontend_port: 8080 +# Set to true (or "true") once the site is served over TLS. +trade_hub_cookie_secure: "false" + +# Post-deploy health check. +trade_hub_healthcheck_url: "http://127.0.0.1:{{ trade_hub_frontend_port }}/healthz" +trade_hub_healthcheck_retries: 12 +trade_hub_healthcheck_delay: 5 + +# The original standalone playbook removed an ad-hoc Apache container that +# used the same host. Disabled by default so the role never touches unrelated +# containers. +trade_hub_remove_legacy_apache: false +trade_hub_legacy_apache_container: my-apache-server + +# Enable podman-restart.service so the containers come back after a reboot. +trade_hub_enable_restart_service: false diff --git a/trade-hub/backend/Dockerfile b/roles/trade_hub/files/trade-hub/backend/Dockerfile similarity index 100% rename from trade-hub/backend/Dockerfile rename to roles/trade_hub/files/trade-hub/backend/Dockerfile diff --git a/trade-hub/backend/app/__init__.py b/roles/trade_hub/files/trade-hub/backend/app/__init__.py similarity index 100% rename from trade-hub/backend/app/__init__.py rename to roles/trade_hub/files/trade-hub/backend/app/__init__.py diff --git a/trade-hub/backend/app/api.py b/roles/trade_hub/files/trade-hub/backend/app/api.py similarity index 100% rename from trade-hub/backend/app/api.py rename to roles/trade_hub/files/trade-hub/backend/app/api.py diff --git a/trade-hub/backend/app/auth.py b/roles/trade_hub/files/trade-hub/backend/app/auth.py similarity index 100% rename from trade-hub/backend/app/auth.py rename to roles/trade_hub/files/trade-hub/backend/app/auth.py diff --git a/trade-hub/backend/app/available.py b/roles/trade_hub/files/trade-hub/backend/app/available.py similarity index 100% rename from trade-hub/backend/app/available.py rename to roles/trade_hub/files/trade-hub/backend/app/available.py diff --git a/trade-hub/backend/app/catalog.py b/roles/trade_hub/files/trade-hub/backend/app/catalog.py similarity index 100% rename from trade-hub/backend/app/catalog.py rename to roles/trade_hub/files/trade-hub/backend/app/catalog.py diff --git a/trade-hub/backend/app/config.py b/roles/trade_hub/files/trade-hub/backend/app/config.py similarity index 100% rename from trade-hub/backend/app/config.py rename to roles/trade_hub/files/trade-hub/backend/app/config.py diff --git a/trade-hub/backend/app/db.py b/roles/trade_hub/files/trade-hub/backend/app/db.py similarity index 100% rename from trade-hub/backend/app/db.py rename to roles/trade_hub/files/trade-hub/backend/app/db.py diff --git a/trade-hub/backend/app/deps.py b/roles/trade_hub/files/trade-hub/backend/app/deps.py similarity index 100% rename from trade-hub/backend/app/deps.py rename to roles/trade_hub/files/trade-hub/backend/app/deps.py diff --git a/trade-hub/backend/app/inventory.py b/roles/trade_hub/files/trade-hub/backend/app/inventory.py similarity index 100% rename from trade-hub/backend/app/inventory.py rename to roles/trade_hub/files/trade-hub/backend/app/inventory.py diff --git a/trade-hub/backend/app/loader.py b/roles/trade_hub/files/trade-hub/backend/app/loader.py similarity index 100% rename from trade-hub/backend/app/loader.py rename to roles/trade_hub/files/trade-hub/backend/app/loader.py diff --git a/trade-hub/backend/app/models.py b/roles/trade_hub/files/trade-hub/backend/app/models.py similarity index 100% rename from trade-hub/backend/app/models.py rename to roles/trade_hub/files/trade-hub/backend/app/models.py diff --git a/trade-hub/backend/app/orders.py b/roles/trade_hub/files/trade-hub/backend/app/orders.py similarity index 100% rename from trade-hub/backend/app/orders.py rename to roles/trade_hub/files/trade-hub/backend/app/orders.py diff --git a/trade-hub/backend/app/schemas.py b/roles/trade_hub/files/trade-hub/backend/app/schemas.py similarity index 100% rename from trade-hub/backend/app/schemas.py rename to roles/trade_hub/files/trade-hub/backend/app/schemas.py diff --git a/trade-hub/backend/app/security.py b/roles/trade_hub/files/trade-hub/backend/app/security.py similarity index 100% rename from trade-hub/backend/app/security.py rename to roles/trade_hub/files/trade-hub/backend/app/security.py diff --git a/trade-hub/backend/data/samples/commodities.json b/roles/trade_hub/files/trade-hub/backend/data/samples/commodities.json similarity index 100% rename from trade-hub/backend/data/samples/commodities.json rename to roles/trade_hub/files/trade-hub/backend/data/samples/commodities.json diff --git a/trade-hub/backend/data/samples/fps-items.json b/roles/trade_hub/files/trade-hub/backend/data/samples/fps-items.json similarity index 100% rename from trade-hub/backend/data/samples/fps-items.json rename to roles/trade_hub/files/trade-hub/backend/data/samples/fps-items.json diff --git a/trade-hub/backend/data/samples/manufacturers.json b/roles/trade_hub/files/trade-hub/backend/data/samples/manufacturers.json similarity index 100% rename from trade-hub/backend/data/samples/manufacturers.json rename to roles/trade_hub/files/trade-hub/backend/data/samples/manufacturers.json diff --git a/trade-hub/backend/data/samples/ship-items.json b/roles/trade_hub/files/trade-hub/backend/data/samples/ship-items.json similarity index 100% rename from trade-hub/backend/data/samples/ship-items.json rename to roles/trade_hub/files/trade-hub/backend/data/samples/ship-items.json diff --git a/trade-hub/backend/data/samples/ships.json b/roles/trade_hub/files/trade-hub/backend/data/samples/ships.json similarity index 100% rename from trade-hub/backend/data/samples/ships.json rename to roles/trade_hub/files/trade-hub/backend/data/samples/ships.json diff --git a/trade-hub/backend/main.py b/roles/trade_hub/files/trade-hub/backend/main.py similarity index 100% rename from trade-hub/backend/main.py rename to roles/trade_hub/files/trade-hub/backend/main.py diff --git a/trade-hub/backend/requirements-dev.txt b/roles/trade_hub/files/trade-hub/backend/requirements-dev.txt similarity index 100% rename from trade-hub/backend/requirements-dev.txt rename to roles/trade_hub/files/trade-hub/backend/requirements-dev.txt diff --git a/trade-hub/backend/requirements.txt b/roles/trade_hub/files/trade-hub/backend/requirements.txt similarity index 100% rename from trade-hub/backend/requirements.txt rename to roles/trade_hub/files/trade-hub/backend/requirements.txt diff --git a/trade-hub/backend/scripts/fetch_data.py b/roles/trade_hub/files/trade-hub/backend/scripts/fetch_data.py similarity index 100% rename from trade-hub/backend/scripts/fetch_data.py rename to roles/trade_hub/files/trade-hub/backend/scripts/fetch_data.py diff --git a/trade-hub/backend/tests/conftest.py b/roles/trade_hub/files/trade-hub/backend/tests/conftest.py similarity index 100% rename from trade-hub/backend/tests/conftest.py rename to roles/trade_hub/files/trade-hub/backend/tests/conftest.py diff --git a/trade-hub/backend/tests/fixtures/commodities.json b/roles/trade_hub/files/trade-hub/backend/tests/fixtures/commodities.json similarity index 100% rename from trade-hub/backend/tests/fixtures/commodities.json rename to roles/trade_hub/files/trade-hub/backend/tests/fixtures/commodities.json diff --git a/trade-hub/backend/tests/fixtures/fps-items.json b/roles/trade_hub/files/trade-hub/backend/tests/fixtures/fps-items.json similarity index 100% rename from trade-hub/backend/tests/fixtures/fps-items.json rename to roles/trade_hub/files/trade-hub/backend/tests/fixtures/fps-items.json diff --git a/trade-hub/backend/tests/fixtures/manufacturers.json b/roles/trade_hub/files/trade-hub/backend/tests/fixtures/manufacturers.json similarity index 100% rename from trade-hub/backend/tests/fixtures/manufacturers.json rename to roles/trade_hub/files/trade-hub/backend/tests/fixtures/manufacturers.json diff --git a/trade-hub/backend/tests/fixtures/ship-items.json b/roles/trade_hub/files/trade-hub/backend/tests/fixtures/ship-items.json similarity index 100% rename from trade-hub/backend/tests/fixtures/ship-items.json rename to roles/trade_hub/files/trade-hub/backend/tests/fixtures/ship-items.json diff --git a/trade-hub/backend/tests/fixtures/ships.json b/roles/trade_hub/files/trade-hub/backend/tests/fixtures/ships.json similarity index 100% rename from trade-hub/backend/tests/fixtures/ships.json rename to roles/trade_hub/files/trade-hub/backend/tests/fixtures/ships.json diff --git a/trade-hub/backend/tests/test_api.py b/roles/trade_hub/files/trade-hub/backend/tests/test_api.py similarity index 100% rename from trade-hub/backend/tests/test_api.py rename to roles/trade_hub/files/trade-hub/backend/tests/test_api.py diff --git a/trade-hub/backend/tests/test_auth.py b/roles/trade_hub/files/trade-hub/backend/tests/test_auth.py similarity index 100% rename from trade-hub/backend/tests/test_auth.py rename to roles/trade_hub/files/trade-hub/backend/tests/test_auth.py diff --git a/trade-hub/backend/tests/test_available.py b/roles/trade_hub/files/trade-hub/backend/tests/test_available.py similarity index 100% rename from trade-hub/backend/tests/test_available.py rename to roles/trade_hub/files/trade-hub/backend/tests/test_available.py diff --git a/trade-hub/backend/tests/test_inventory.py b/roles/trade_hub/files/trade-hub/backend/tests/test_inventory.py similarity index 100% rename from trade-hub/backend/tests/test_inventory.py rename to roles/trade_hub/files/trade-hub/backend/tests/test_inventory.py diff --git a/trade-hub/backend/tests/test_orders.py b/roles/trade_hub/files/trade-hub/backend/tests/test_orders.py similarity index 100% rename from trade-hub/backend/tests/test_orders.py rename to roles/trade_hub/files/trade-hub/backend/tests/test_orders.py diff --git a/trade-hub/frontend/Dockerfile b/roles/trade_hub/files/trade-hub/frontend/Dockerfile similarity index 100% rename from trade-hub/frontend/Dockerfile rename to roles/trade_hub/files/trade-hub/frontend/Dockerfile diff --git a/trade-hub/frontend/nginx.conf b/roles/trade_hub/files/trade-hub/frontend/nginx.conf similarity index 100% rename from trade-hub/frontend/nginx.conf rename to roles/trade_hub/files/trade-hub/frontend/nginx.conf diff --git a/trade-hub/frontend/src/app.js b/roles/trade_hub/files/trade-hub/frontend/src/app.js similarity index 100% rename from trade-hub/frontend/src/app.js rename to roles/trade_hub/files/trade-hub/frontend/src/app.js diff --git a/trade-hub/frontend/src/index.html b/roles/trade_hub/files/trade-hub/frontend/src/index.html similarity index 100% rename from trade-hub/frontend/src/index.html rename to roles/trade_hub/files/trade-hub/frontend/src/index.html diff --git a/trade-hub/frontend/src/style.css b/roles/trade_hub/files/trade-hub/frontend/src/style.css similarity index 100% rename from trade-hub/frontend/src/style.css rename to roles/trade_hub/files/trade-hub/frontend/src/style.css diff --git a/trade-hub/podman-compose.yml b/roles/trade_hub/files/trade-hub/podman-compose.yml similarity index 100% rename from trade-hub/podman-compose.yml rename to roles/trade_hub/files/trade-hub/podman-compose.yml diff --git a/roles/trade_hub/meta/main.yml b/roles/trade_hub/meta/main.yml new file mode 100644 index 0000000..37b93a8 --- /dev/null +++ b/roles/trade_hub/meta/main.yml @@ -0,0 +1,16 @@ +--- +galaxy_info: + role_name: trade_hub + author: boxswapper + description: Deploy the Free People of the Verse trade hub (FastAPI, nginx, PostgreSQL) with Podman. + license: MIT + min_ansible_version: "2.15" + platforms: + - name: GenericLinux + versions: + - all + galaxy_tags: + - podman + - fastapi + - postgresql +dependencies: [] diff --git a/roles/trade_hub/tasks/main.yml b/roles/trade_hub/tasks/main.yml new file mode 100644 index 0000000..e8e7752 --- /dev/null +++ b/roles/trade_hub/tasks/main.yml @@ -0,0 +1,217 @@ +--- +- name: Check whether podman is installed + ansible.builtin.command: podman --version + register: trade_hub_podman_check + changed_when: false + failed_when: false + check_mode: false + +- name: Install podman when requested + ansible.builtin.package: + name: podman + state: present + become: true + when: + - trade_hub_podman_check.rc != 0 + - trade_hub_install_podman | bool + +- name: Re-check podman after installation + ansible.builtin.command: podman --version + register: trade_hub_podman_recheck + changed_when: false + failed_when: false + check_mode: false + when: trade_hub_podman_check.rc != 0 + +- name: Require podman to be available + ansible.builtin.assert: + that: + - trade_hub_podman_check.rc == 0 or (trade_hub_podman_recheck.rc | default(1)) == 0 + fail_msg: >- + podman is not installed on {{ inventory_hostname }}. Install it first + (for example with the "packages" role) or run this role with + -e trade_hub_install_podman=true. + quiet: true + +- name: Remove the legacy Apache container when requested + containers.podman.podman_container: + name: "{{ trade_hub_legacy_apache_container }}" + state: absent + when: trade_hub_remove_legacy_apache | bool + +- name: Create the application directory + ansible.builtin.file: + path: "{{ trade_hub_app_dir }}" + state: directory + mode: "0755" + +- name: Stage the application build context + ansible.builtin.copy: + src: "{{ role_path }}/files/trade-hub/{{ item.src }}" + dest: "{{ trade_hub_app_dir }}/{{ item.dest }}" + mode: preserve + loop: + # Only the files needed by the two Dockerfiles are staged. Copying the + # whole tree would also ship local development artefacts such as .venv. + - { src: "backend/Dockerfile", dest: "backend/Dockerfile" } + - { src: "backend/requirements.txt", dest: "backend/requirements.txt" } + - { src: "backend/main.py", dest: "backend/main.py" } + - { src: "backend/app/", dest: "backend/app/" } + - { src: "backend/scripts/", dest: "backend/scripts/" } + - { src: "frontend/Dockerfile", dest: "frontend/Dockerfile" } + - { src: "frontend/nginx.conf", dest: "frontend/nginx.conf" } + - { src: "frontend/src/", dest: "frontend/src/" } + register: trade_hub_source + +- name: Check whether the backend image exists + ansible.builtin.command: + argv: + - podman + - image + - exists + - "{{ trade_hub_backend_image }}:{{ trade_hub_image_tag }}" + register: trade_hub_backend_image_check + changed_when: false + failed_when: false + check_mode: false + +- name: Check whether the frontend image exists + ansible.builtin.command: + argv: + - podman + - image + - exists + - "{{ trade_hub_frontend_image }}:{{ trade_hub_image_tag }}" + register: trade_hub_frontend_image_check + changed_when: false + failed_when: false + check_mode: false + +- name: Build the backend image + ansible.builtin.command: + argv: + - podman + - build + - --tag + - "{{ trade_hub_backend_image }}:{{ trade_hub_image_tag }}" + - "{{ trade_hub_app_dir }}/backend" + when: + - trade_hub_build_images | bool + - trade_hub_force_rebuild | bool or trade_hub_source.changed or trade_hub_backend_image_check.rc != 0 + register: trade_hub_backend_build + +- name: Build the frontend image + ansible.builtin.command: + argv: + - podman + - build + - --tag + - "{{ trade_hub_frontend_image }}:{{ trade_hub_image_tag }}" + - "{{ trade_hub_app_dir }}/frontend" + when: + - trade_hub_build_images | bool + - trade_hub_force_rebuild | bool or trade_hub_source.changed or trade_hub_frontend_image_check.rc != 0 + register: trade_hub_frontend_build + +- name: Require prebuilt images when image builds are disabled + ansible.builtin.assert: + that: + - trade_hub_backend_image_check.rc == 0 + - trade_hub_frontend_image_check.rc == 0 + fail_msg: >- + trade_hub_build_images is false but {{ trade_hub_backend_image }}:{{ + trade_hub_image_tag }} and/or {{ trade_hub_frontend_image }}:{{ + trade_hub_image_tag }} are missing on {{ inventory_hostname }}. + quiet: true + when: not trade_hub_build_images | bool + +- name: Record whether the application containers need to be recreated + ansible.builtin.set_fact: + trade_hub_recreate_containers: >- + {{ (trade_hub_force_rebuild | bool) + or ((trade_hub_build_images | bool) and (trade_hub_source.changed | default(false))) + or (trade_hub_backend_build.changed | default(false)) + or (trade_hub_frontend_build.changed | default(false)) }} + +- name: Ensure the PostgreSQL image is present + containers.podman.podman_image: + name: "{{ trade_hub_postgres_image }}" + state: present + when: not ansible_check_mode + +- name: Create the podman network + containers.podman.podman_network: + name: "{{ trade_hub_network }}" + state: present + +- name: Create the PostgreSQL data volume + containers.podman.podman_volume: + name: "{{ trade_hub_volume }}" + state: present + +- name: Run the PostgreSQL container + containers.podman.podman_container: + name: "{{ trade_hub_db_container }}" + image: "{{ trade_hub_postgres_image }}" + state: started + recreate: "{{ trade_hub_force_rebuild | bool }}" + network: "{{ trade_hub_network }}" + network_aliases: + - db + env: + POSTGRES_USER: "{{ trade_hub_postgres_user }}" + POSTGRES_PASSWORD: "{{ trade_hub_postgres_password }}" + POSTGRES_DB: "{{ trade_hub_postgres_db }}" + volumes: + - "{{ trade_hub_volume }}:/var/lib/postgresql/data:Z" + restart_policy: unless-stopped + when: not ansible_check_mode + +- name: Run the backend container + containers.podman.podman_container: + name: "{{ trade_hub_backend_container }}" + image: "{{ trade_hub_backend_image }}:{{ trade_hub_image_tag }}" + state: started + # podman does not recreate a container when its image tag resolves to a new + # ID, so recreate whenever the image was rebuilt or the app changed. + recreate: "{{ trade_hub_recreate_containers | bool }}" + network: "{{ trade_hub_network }}" + # The nginx upstream is http://backend:8000, so this alias must match. + network_aliases: + - backend + env: + DATABASE_URL: "{{ trade_hub_database_url }}" + DB_WAIT_SECONDS: "{{ trade_hub_db_wait_seconds }}" + COOKIE_SECURE: "{{ trade_hub_cookie_secure | bool | lower }}" + restart_policy: unless-stopped + when: not ansible_check_mode + +- name: Run the frontend container + containers.podman.podman_container: + name: "{{ trade_hub_frontend_container }}" + image: "{{ trade_hub_frontend_image }}:{{ trade_hub_image_tag }}" + state: started + recreate: "{{ trade_hub_recreate_containers | bool }}" + network: "{{ trade_hub_network }}" + ports: + - "{{ trade_hub_frontend_bind }}:{{ trade_hub_frontend_port }}:80" + restart_policy: unless-stopped + when: not ansible_check_mode + +- name: Enable podman-restart so containers survive a reboot + ansible.builtin.systemd: + name: podman-restart.service + enabled: true + state: started + when: trade_hub_enable_restart_service | bool + +- name: Wait for the application to answer health checks + ansible.builtin.uri: + url: "{{ trade_hub_healthcheck_url }}" + status_code: 200 + register: trade_hub_health + until: trade_hub_health.status == 200 + retries: "{{ trade_hub_healthcheck_retries }}" + delay: "{{ trade_hub_healthcheck_delay }}" + changed_when: false + when: not ansible_check_mode