Three docs still referenced the pre-collapse service keys:
* docs/contributing/artefact/files/role/playwright.specs.js.md — biber
and administrator auth-chain diagrams listed
`skipUnlessServiceEnabled('oidc' | 'oauth2' | 'ldap')`; the unified gate
is `'sso' | 'ldap'`.
* docs/contributing/design/role/services/base.md — the `lookup('service',
...)` example block carried the now-invalid `'oidc'` key; replaced
with `'sso'`.
* docs/requirements/019-playwright-meta-services-parity.md — the
prometheus, matomo, and mailu rows still spelled
`safeSkipUnlessEnabled("oauth2"|"oidc")` and
`isServiceEnabled("oauth2")`; all rewritten to `"sso"`.
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-cdnprovides the primary service keycdnweb-app-keycloakprovidesoidcsvc-db-mariadbprovidesmariadb
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:
enabledshared- 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.frontendvs.backendis 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:andcanonical: - validates and applies
run_after:frommeta/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:
universalworkstationserverweb-svcweb-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.
- 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:
idroleenabledsharedrequiredis True when some deployed app has the service with bothenabled: trueANDshared: 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 plainenabledflag 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
requiredtransitively 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-mariadbprovidesmariadbsvc-db-postgresprovidespostgres
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.
Related Files 📁
| 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 |