Files
kevinveenbirkenbachandClaude Opus 5 575a5a87ac refactor(layout): sort shipped scripts by language and split the unit suite
Every script a role ships now lives under `files/<language>/`, and the unit
suite is split the same way: `tests/unit/python/` beside new sibling trees for
the other languages. `tests/lint/filesystem/test_role_files_layout.py` keeps it
that way; directories whose shape is dictated from outside carry a `.nocheck`
naming the constraint (Docker build contexts, WordPress mu-plugins, the Joomla
extension, Odoo addons, Playwright suites, the `files/test/test.sh` harness
that `test-e2e-cli` resolves generically).

This cannot be applied in halves. Moving a file and repointing what names it
are the same change: 14 references resolve only at deploy time, so the tree is
green either before or after, never between.

Those 14 were found by static resolution over every reference class, not by the
test suite -- a full `make quality-high` was green while all of them were
broken:

  * 7 `src:` values built from a role variable holding a bare filename
    (svc-bkp-remote-2-local, decidim, espocrm, friendica, joomla, pixelfed x2).
    The variable doubles as the destination name inside the container, so the
    language segment belongs at the call site, not in the variable.
  * `sys-service/tasks/04_base.yml` resolved every consumer's systemd script
    through `first_found(..., errors='strict')` over `files/script.{sh,py}`.
    18 roles ship exactly that and have no template fallback: the play would
    have died on each.
  * `sys-front-inj-javascript` fell back to `<role>/files/javascript.js`; the
    interface now reads `files/javascript/javascript.js`.
  * `sys-front-inj-logout/templates/head_sub.j2` built the cache-busting mtime
    path through `path_absolute(... 'files', ...)`.
  * 4 `ansible.builtin.script:` invocations naming a bare command
    (pull.py, prune.sh, set_postgres_superuser_password.sh, ganesha-serve.sh).
  * `dev-python/files/shell/install.sh` pointed at a sibling role's script.

Depth-dependent paths inside the moved trees were bumped accordingly: 59
`parents[N]` computations, 12 relative climbs, and the per-role recover CLI,
whose path `cli/administration/recover/recoverers.py` still assembled from the
pre-move location.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 01:31:09 +02:00

7.8 KiB

Service Management 🧱

This page describes how shared services are declared, discovered, ordered, loaded, and injected.

What Is a Service? 🧩

A service is a reusable dependency that an application enables via services.<service_key>.

Examples:

  • web-svc-cdn provides the primary service key cdn
  • web-app-keycloak provides oidc
  • svc-db-mariadb provides mariadb

Each service entry lives in the provider role's own meta/services.yml, where the file root IS the services map keyed by <entity_name> (no compose: and no services: wrapper). See layout.md for the full per-role meta layout.

Role-Local Service Metadata 🏷️

Service providers are self-describing. The provider role owns:

  • enabled
  • shared
  • optional provides
  • optional canonical

Example (file root of meta/services.yml):

keycloak:
  enabled: false
  shared: true
  provides: oidc

Canonical aliases are also role-local:

cdn:
  enabled: false
  shared: true
css:
  enabled: true
  shared: true
  canonical: cdn
javascript:
  enabled: true
  shared: true
  canonical: cdn

Rules:

  • The primary service entry is the role entity name returned by get_entity_name.
  • provides: is only used when the public service name differs from the entity name.
  • canonical: is only used on alias entries that resolve back to the primary service key.
  • frontend vs. backend is derived from the role name prefix, not stored in config.

Service Discovery 🔍

Service discovery is built from role configs, not from a central registry file.

Primary implementation files:

The discovery layer:

  • scans role configs from roles/
  • discovers provider entries from services
  • derives deploy type and loader bucket from the role name
  • resolves provides: and canonical:
  • validates and applies run_after: from meta/services.yml.<primary_entity>

Load Order 🧮

main.yml is the single loader entry point for all shared services.

It runs from 01_constructor.yml before the normal application stage.

Global bucket order:

  1. universal
  2. workstation
  3. server
  4. web-svc
  5. web-app

Within the same bucket, ordering is refined by run_after: declared on the provider role's primary entity in meta/services.yml (i.e. services.<primary_entity>.run_after).

Rules:

  • run_after: entries are role names, not service keys.
  • Cross-type run_after: is invalid and fails hard.
  • Later-bucket dependencies are invalid and fail hard.
  • Cross-type service dependencies do not need run_after: because constructor-stage loading already brings backend services up before normal app deployment.

Loading vs Injection 🔀

Service loading and frontend injection are separate mechanisms.

Loading 📥

Loading decides whether the provider role is deployed at all.

main.yml:

  • queries the ordered discovered service list
  • checks lookup('service', service_key).required
  • skips roles already protected by run_once_*
  • loads services through load/app.yml

Frontend service probe/load helper:

Injection 🔌

Injection decides whether a deployed app gets extra nginx integration such as dashboard, logout, CSS, or JavaScript hooks.

This stays in:

Injection still reads the current app's services.<feature>.enabled flags. It does not load provider roles.

Lookup Plugins 🔎

service 🧷

File: service.py

Examples:

lookup('service', 'matomo')
lookup('service', 'sso')
lookup('service', 'web-svc-cdn')

Returns:

  • id
  • role
  • enabled
  • shared
  • required is True when some deployed app has the service with both enabled: true AND shared: true, directly or transitively through its own enabled service dependencies. This is the flag consumers should gate on when deciding whether to load, integrate with, or configure the service. A plain enabled flag is not enough on its own, because a service that is enabled but never shared with another app is not actually contractually required by anyone.

Behavior:

  • accepts either a service key or a provider role name
  • resolves aliases through canonical
  • resolves provider roles through discovered primary service keys
  • computes required transitively from enabled shared services

service_registry 📚

File: service_registry.py

Examples:

query('service_registry') | first
query('service_registry', 'ordered') | first

Modes:

  • default: full discovered registry mapping
  • ordered: ordered primary service entries for the service loader

applications_current_play 🧭

File: applications_current_play.py

Builds the current-play application set including:

  • group-selected roles
  • transitive shared service dependencies
  • meta dependencies

Database Services 🗄️

Relational databases are regular services:

  • svc-db-mariadb provides mariadb
  • svc-db-postgres provides postgres

Applications express database choice directly via the file root of meta/services.yml:

mariadb:
  enabled: true
  shared: true

or:

postgres:
  enabled: true
  shared: false

The lookup('database', ...) API is the convenience accessor for database connection values; it resolves the active direct database service from the role-local keys above.

File Purpose
service_registry.py Service discovery, provides, canonical, bucket detection, run_after ordering
service_registry.py lookup Exposes the discovered registry and ordered provider list to Ansible
service.py Resolves service flags and transitive need
applications_current_play.py Builds the current play app graph with shared service deps
main.yml Single shared-service loader entry point
load_service.yml Per-service load helper used by the central service loader
01_constructor.yml Calls the service loader during constructor
load/app.yml Run-once role loader
test_service_registry.py Unit tests for discovery, buckets, and run_after ordering
test_service.py Unit tests for lookup('service', ...)
test_resolvable.py Integration checks for discovered service resolution
test_canonical.py Canonical alias consistency checks
test_transitive_dependencies.py Integration coverage for transitive dependency resolution