Files
computer-playbook/docs/contributing/code/services.md
T
kevinveenbirkenbachandClaude Sonnet 4.6 be6a310d59 refactor(services): implement role-local service registry (req-001)
- Delete group_vars/all/20_services.yml; all service metadata now lives
  in each role's config/main.yml under compose.services.<name>
- Rename generic 'database' service key to explicit 'mariadb'/'postgres'
  across all app roles; remove legacy type: field
- Add utils/service_registry.py and plugins/lookup/service_registry.py
  as the single source of truth for service discovery
- Add utils/database_service.py to centralize DB service resolution
- Delete service_load_enabled filter and service_should_load lookup;
  replaced by service lookup plugin
- Update sys-utils-service-loader to run in constructor stage and use
  generic load_service.yml; remove load_frontend_service.yml
- Add _service_loader_preloading guard to sys-svc-compose,
  sys-svc-webserver-core and sys-ctl-alm-compose to prevent
  health-checks/alarms from firing during preload phase
- Replace manual load_app.yml calls for shared DB/Mailu with assert-
  based preload validation in sys-svc-rdbms and sys-svc-mail
- Update networks.yml.j2 templates to resolve LDAP/AI networks via
  service_registry instead of hardcoded role names
- Migrate all tests to new service discovery API; delete obsolete tests
- Update docs/contributing/code/services.md to reflect role-local model

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 19:33:20 +02:00

232 lines
7.6 KiB
Markdown

# Service Management
This page is the SPOT for 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
`compose.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
[`config/main.yml`](../../../roles) under `compose.services.<entity_name>`.
## Role-Local Service Metadata
Service providers are self-describing. The provider role owns:
- `enabled`
- `shared`
- optional `provides`
- optional `canonical`
Example:
```yaml
compose:
services:
keycloak:
enabled: false
shared: true
provides: oidc
```
Canonical aliases are also role-local:
```yaml
compose:
services:
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:
- [`utils/service_registry.py`](../../../utils/service_registry.py)
- [`plugins/lookup/service_registry.py`](../../../plugins/lookup/service_registry.py)
The discovery layer:
- scans role configs from [`roles`](../../../roles)
- discovers provider entries from `compose.services`
- derives deploy type and loader bucket from the role name
- resolves `provides:` and `canonical:`
- validates and applies `run_after:` from `meta/main.yml`
## Load Order
[`roles/sys-utils-service-loader/tasks/main.yml`](../../../roles/sys-utils-service-loader/tasks/main.yml)
is the single loader SPOT for all shared services.
It runs from [`tasks/stages/01_constructor.yml`](../../../tasks/stages/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:` in the provider role's
[`meta/main.yml`](../../../roles).
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.
[`roles/sys-utils-service-loader/tasks/main.yml`](../../../roles/sys-utils-service-loader/tasks/main.yml):
- queries the ordered discovered service list
- checks `lookup('service', service_key).needed`
- skips roles already protected by `run_once_*`
- loads services through [`tasks/utils/load_app.yml`](../../../tasks/utils/load_app.yml)
Frontend service probe/load helper:
- [`roles/sys-utils-service-loader/tasks/load_service.yml`](../../../roles/sys-utils-service-loader/tasks/load_service.yml)
### Injection
Injection decides whether a deployed app gets extra nginx integration such as
dashboard, logout, CSS, or JavaScript hooks.
This stays in:
- [`roles/sys-front-inj-all/tasks/main.yml`](../../../roles/sys-front-inj-all/tasks/main.yml)
- [`roles/sys-front-inj-all/filter_plugins/inj_enabled.py`](../../../roles/sys-front-inj-all/filter_plugins/inj_enabled.py)
Injection still reads the current app's `compose.services.<feature>.enabled` flags.
It does not load provider roles.
## Lookup Plugins
### `service`
File:
[`plugins/lookup/service.py`](../../../plugins/lookup/service.py)
Examples:
```yaml
lookup('service', 'matomo')
lookup('service', 'oidc')
lookup('service', 'web-svc-cdn')
```
Returns:
- `id`
- `role`
- `enabled`
- `shared`
- `needed`
Behavior:
- accepts either a service key or a provider role name
- resolves aliases through `canonical`
- resolves provider roles through discovered primary service keys
- computes `needed` transitively from enabled shared services
### `service_registry`
File:
[`plugins/lookup/service_registry.py`](../../../plugins/lookup/service_registry.py)
Examples:
```yaml
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:
[`plugins/lookup/applications_current_play.py`](../../../plugins/lookup/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 now:
- `svc-db-mariadb` provides `mariadb`
- `svc-db-postgres` provides `postgres`
Applications express database choice directly via:
```yaml
compose:
services:
mariadb:
enabled: true
shared: true
```
or:
```yaml
compose:
services:
postgres:
enabled: true
shared: false
```
The `lookup('database', ...)` API remains as the convenience accessor for database
connection values, but it now resolves the active direct database service from those
role-local keys instead of `compose.services.database.type`.
## Related Files
| File | Purpose |
|---|---|
| [`utils/service_registry.py`](../../../utils/service_registry.py) | Service discovery, `provides`, `canonical`, bucket detection, `run_after` ordering |
| [`plugins/lookup/service_registry.py`](../../../plugins/lookup/service_registry.py) | Exposes the discovered registry and ordered provider list to Ansible |
| [`plugins/lookup/service.py`](../../../plugins/lookup/service.py) | Resolves service flags and transitive need |
| [`plugins/lookup/applications_current_play.py`](../../../plugins/lookup/applications_current_play.py) | Builds the current play app graph with shared service deps |
| [`roles/sys-utils-service-loader/tasks/main.yml`](../../../roles/sys-utils-service-loader/tasks/main.yml) | Single shared-service loader SPOT |
| [`roles/sys-utils-service-loader/tasks/load_service.yml`](../../../roles/sys-utils-service-loader/tasks/load_service.yml) | Per-service load helper used by the central service loader |
| [`tasks/stages/01_constructor.yml`](../../../tasks/stages/01_constructor.yml) | Calls the service loader during constructor |
| [`tasks/utils/load_app.yml`](../../../tasks/utils/load_app.yml) | Run-once role loader |
| [`tests/unit/utils/test_service_registry.py`](../../../tests/unit/utils/test_service_registry.py) | Unit tests for discovery, buckets, and `run_after` ordering |
| [`tests/unit/plugins/lookup/test_service.py`](../../../tests/unit/plugins/lookup/test_service.py) | Unit tests for `lookup('service', ...)` |
| [`tests/integration/test_services_resolvable.py`](../../../tests/integration/test_services_resolvable.py) | Integration checks for discovered service resolution |
| [`tests/integration/test_services_canonical.py`](../../../tests/integration/test_services_canonical.py) | Canonical alias consistency checks |
| [`tests/integration/test_service_transitive_dependencies.py`](../../../tests/integration/test_service_transitive_dependencies.py) | Integration coverage for transitive dependency resolution |