Files
computer-playbook/docs/contributing/design/role/services/base.md
T
kevinveenbirkenbachandClaude Opus 5 a05071fe78 refactor(roles): let a role declare its entity name and drop the get_ prefix
The entity name keys a role's instance directory, its compose project and its container name, and was derived only by stripping the longest matching category prefix. Two roles under different categories whose names share a suffix therefore collapse onto one entity and overwrite each other's rendered stack, which no provides: can repair.

A literal entity_name in vars/main.yml now wins over the derivation. web-app-seaweedfs-console takes its shorter name back and declares the entity seaweedfs-console, which frees the seaweedfs key in its own services map for the engine consumer and removes the provides:, covers: and alias entries that stood in for it. A new lint pairs the declaration with the primary key it has to match, because a drift between them would drop the role from the provider set in silence.

Compared across all 288 roles, exactly one entity changes, and it restores what main already keeps on disk, so no instance directory moves.

The function and the filter lose their get_ prefix. Two lints read that prefix as the discriminator between the variable and the filter that fills it; both discriminate on the pipe now. Ansible's templar resolves a filter and a same-named variable independently, verified against ansible-core 2.20.2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-03 19:40:53 +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 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