From a9b0f4faada5fa93a3afba13f4ee15efe78fc6c0 Mon Sep 17 00:00:00 2001 From: Blai Peidro Date: Fri, 9 Oct 2026 03:01:18 +0200 Subject: [PATCH] feat: run the playbooks on an RTE container AnsibleForms 7 runs no playbook in the app, so every playbook form failed with "No runner to run this playbook". An rte service now runs ghcr.io/ansibleforms/ansibleforms-rte:7 on the same data folder, with the ansible settings, roles, collections and PYTHONPATH moved over from the app. data/seed.yaml registers it as the default runner with RTE_TOKEN from .env. It starts once the app is healthy: an RTE that finds no schema stops working without exiting. CI runs the demo playbook on it. --- .env | 8 ++++++++ .github/workflows/ci.yml | 40 +++++++++++++++++++++++++++++++++--- CONTRIBUTING.md | 4 ++-- README.md | 12 ++++++++++- data/seed.yaml | 12 +++++++++++ docker-compose.yml | 44 ++++++++++++++++++++++++++++++++++------ 6 files changed, 108 insertions(+), 12 deletions(-) create mode 100644 data/seed.yaml diff --git a/.env b/.env index 4706f5b..770e8ec 100644 --- a/.env +++ b/.env @@ -164,6 +164,14 @@ ADMIN_PASSWORD=AnsibleForms!123 # REST_ALLOWED_HOSTS= # REST_DENIED_HOSTS= +# ============================================================================= +# RTE (RUNTIME ENVIRONMENT) +# ============================================================================= + +# Token the app sends with every call to the RTE, at least 16 characters. Both containers +# read it from here, and data/seed.yaml hands it to the app's runner on every start. +RTE_TOKEN=AnsibleForms!RTE!Token!123 + # ============================================================================= # CONFIG AND FORMS # ============================================================================= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f59ec4f..ef15dda 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,10 @@ name: CI # Starts the stack exactly as a user does (docker compose up with the committed .env and -# data/) and logs in through the API. A green run means the compose file is valid, the -# pinned images pull and start together, the app reaches its database without waiting for -# it and creates its schema, and the sample data does not stop it from booting. That is +# data/), logs in through the API and runs the demo playbook on the RTE. A green run means +# the compose file is valid, the pinned images pull and start together, the app reaches +# its database without waiting for it and creates its schema, the sample data does not stop +# it from booting, and the seed hands playbooks to the RTE. That is # what a Dependabot bump of the MySQL image has to prove before it is merged. on: @@ -41,6 +42,39 @@ jobs: echo "::error::The app did not accept the admin login within 5 minutes." exit 1 + # AnsibleForms 7 runs no playbook itself: the rte container does, registered as the + # default runner by data/seed.yaml. Launch the demo playbook form and wait for its output. + - name: A playbook runs on the RTE + run: | + set -euo pipefail + URL=https://localhost + token="$(curl -fsSk -X POST -u 'admin:AnsibleForms!123' "$URL/api/v2/auth/login" | jq -r .token)" + auth=(-H "Authorization: Bearer $token" -H "Content-Type: application/json") + runner="$(curl -fsSk "${auth[@]}" "$URL/api/v2/runner" | jq -c '.records[] | select(.type == "rte" and .is_default == 1)')" + [ -n "$runner" ] || { echo "::error::data/seed.yaml registered no default rte runner."; exit 1; } + echo "Runner: $runner" + # the RTE starts once the app is healthy, so give it a moment to listen + check="" + for i in $(seq 1 30); do + check="$(curl -fsSk "${auth[@]}" -X POST "$URL/api/v2/runner/$(jq -r .id <<<"$runner")/check" || true)" + [ -n "$check" ] && break + sleep 5 + done + [ -n "$check" ] || { echo "::error::The app cannot reach the RTE."; exit 1; } + echo "Test connection: $check" + id="$(curl -fsSk "${auth[@]}" -X POST "$URL/api/v2/job" -d '{"formName":"Demo Form Ansible No input","extravars":{}}' | jq -r .id)" + for i in $(seq 1 60); do + job="$(curl -fsSk "${auth[@]}" "$URL/api/v2/job/$id")" + status="$(jq -r .status <<<"$job")" + case "$status" in running|approve) sleep 3 ;; *) break ;; esac + done + output="$(jq -r .output <<<"$job")" + echo "Job $id ended $status" + [ "$status" = success ] || { echo "::error::The playbook job ended $status."; echo "$output"; exit 1; } + grep -q "Running on RTE" <<<"$output" || { echo "::error::The job did not run on the RTE."; exit 1; } + grep -q "Hi there and welcome to ansible" <<<"$output" || { echo "::error::The playbook output is missing."; exit 1; } + echo "The playbook ran on the RTE." + - name: The app never had to wait for its database run: | # the app only starts once MySQL is healthy, so it connects on the first try diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e029d45..f65363d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,9 +19,9 @@ The setup is a compose file, its settings and the data the containers start with | Path | Holds | |---|---| -| `docker-compose.yml` | the AnsibleForms and MySQL containers and their volumes | +| `docker-compose.yml` | the AnsibleForms, RTE and MySQL containers and their volumes | | `.env` | the settings, with sample values that work out of the box | -| `data/` | what AnsibleForms and MySQL start with: config, sample forms and playbooks, custom functions, ansible and git settings | +| `data/` | what AnsibleForms and MySQL start with: config, sample forms and playbooks, custom functions, ansible and git settings, and the config seed that registers the RTE | What the containers write at runtime (the database, logs, backups, keys) also lands in `data/`, and `.gitignore` keeps it out of git. diff --git a/README.md b/README.md index 09d79e6..5292f55 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,17 @@ Everything under `data/` is mounted into the containers and survives a restart o - A demo `config.yaml` with categories, roles and constants, and sample forms - Sample maintenance playbooks and a `dummy.yaml` playbook - Sample custom JavaScript functions and jq definitions to extend AnsibleForms -- Ansible, Python 3 and a set of Galaxy collections inside the image +- An RTE container that runs the playbooks, with ansible, Python 3 and a set of Galaxy collections +- A config seed, `data/seed.yaml`, that registers the RTE as the default runner + +## Running playbooks + +AnsibleForms 7 runs no playbook in the app: the `rte` container runs them, from +`ghcr.io/ansibleforms/ansibleforms-rte:7`. The app reaches it on `http://rte:8000` with `RTE_TOKEN` from `.env`. + +`data/seed.yaml` registers it as the default runner on every start, so it is read-only under Connections > Runners. +Ansible settings, roles and collections under `data/ansible/` are mounted into the RTE, not the app. When your +playbooks need more, build an image from the app's `Dockerfile.rte` and point the `rte` service at it. ## Kubernetes diff --git a/data/seed.yaml b/data/seed.yaml new file mode 100644 index 0000000..08205fb --- /dev/null +++ b/data/seed.yaml @@ -0,0 +1,12 @@ +# Config seed, applied by the app at every start (CONFIG_SEED_PATH in docker-compose.yml). +# It registers the rte container as the default runner, so playbook forms run there. Objects +# declared here are read-only in the interface; see https://ansibleforms.com/seed. +version: 1 +runners: + items: + - name: rte + type: rte + description: The rte container of docker-compose.yml + uri: http://rte:8000 + token: ${RTE_TOKEN} + is_default: true diff --git a/docker-compose.yml b/docker-compose.yml index 3fc023b..58ccf18 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,8 +47,8 @@ services: - DB_PASSWORD=$MYSQLDB_PASSWORD - DB_PORT=$MYSQLDB_DOCKER_PORT - PORT=$WEBAPP_DOCKER_PORT - # adding python path to add our custom persistent lib location - - PYTHONPATH=/usr/lib/python311.zip:/usr/lib/python3.11:/usr/lib/python3.11/lib-dynload:/usr/lib/python3.11/site-packages:/app/dist/persistent/python/lib + # The config seed registers the RTE below as the default runner (data/seed.yaml) + - CONFIG_SEED_PATH=/app/dist/persistent/seed.yaml # allow interactive shell stdin_open: true # allow terminal @@ -58,12 +58,44 @@ services: - ./data:/app/dist/persistent # persistent app data - ./data/ssh:$HOME_DIR/.ssh # persistent ssh keys - ./data/git/.gitconfig:$HOME_DIR/.gitconfig # custom git config - - ./data/ansible/ansible.cfg:/etc/ansible/ansible.cfg # custom ansible.cfg - - ./data/ansible/roles:/etc/ansible/roles # custom ansible roles - - ./data/ansible/collections:/etc/ansible/collections # custom ansible collections # Custom functions: the app reads these from its source folder, not from the persistent # folder above, so they are mounted over the copies in the image. - ./data/functions/custom.js:/app/dist/src/functions/custom.js # custom JavaScript functions (fnc.*) - ./data/functions/jq.custom.definitions:/app/dist/src/functions/jq.custom.definitions # custom jq definitions # - ./data/assets/images:/app/dist/views/img # 6 custom images (login_background_%theme%.jpg and logo_%theme%.svg | 2 for each theme = light, dark and color) - + # AnsibleForms RTE (runtime environment): runs the playbooks. AnsibleForms 7 runs none in + # the app itself, so this is where ansible, the Python libraries and the collections live. + # The app reaches it on http://rte:8000 with RTE_TOKEN; it is not published on the host. + rte: + # Only start once the app is healthy: it creates the schema on the first start, and an + # RTE that finds no schema stops working without exiting, so a restart would not help + depends_on: + mysqldb: + condition: service_healthy + app: + condition: service_healthy + image: ghcr.io/ansibleforms/ansibleforms-rte:7 + container_name: af_rte + restart: unless-stopped + # Load extra environment variables from file (the database, ENCRYPTION_SECRET, RTE_TOKEN) + env_file: + - ./.env + # Set environment variables + environment: + - DB_HOST=mysqldb + - DB_USER=$MYSQLDB_USER + - DB_PASSWORD=$MYSQLDB_PASSWORD + - DB_PORT=$MYSQLDB_DOCKER_PORT + - PORT=8000 + # Only the app calls it, inside the compose network + - HTTPS=0 + # The libraries the ansibleforms_pip_install playbook installs into the persistent folder + - PYTHONPATH=/app/dist/persistent/python/lib + volumes: + # The same folders as the app: playbooks, repositories, uploads and the SSH key + - ./data:/app/dist/persistent # persistent app data + - ./data/ssh:$HOME_DIR/.ssh # persistent ssh keys + - ./data/git/.gitconfig:$HOME_DIR/.gitconfig # custom git config + - ./data/ansible/ansible.cfg:/etc/ansible/ansible.cfg # custom ansible.cfg + - ./data/ansible/roles:/etc/ansible/roles # custom ansible roles + - ./data/ansible/collections:/etc/ansible/collections # custom ansible collections