mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
An engine of its own keeps its passwords in its data directory, written once
when that directory was initialised. When the deploy rotates the credential the
new value reaches the container's environment and never the engine, so the
health check authenticates with a password nothing has ever accepted. The
application user and root come from the same value, so nothing is left that
could log in and change it either.
mariadbd reads --init-file on every start, before it opens the socket, and runs
the statements with full privileges. That is the one point where a password can
be reset without knowing the previous one.
The SQL is rendered next to the env file that already holds the same secret in
plaintext, so it adds no new exposure. It reaches the container as a config
rather than a bind mount because a swarm task may be scheduled on a node the
rendered path does not exist on; the config name carries a hash of the file
content, so a rotated password produces a new name and swarm replaces the
immutable old one.
The guard is mariadb-and-not-shared, which matches the `local` guard the service
template already renders on, so the block and the top-level entry appear
together or not at all.
3831c8f804 fixed the other half of this failure: matomo, moodle and nextcloud
keep the same password in a config file on their data volume. Same rotation,
opposite end of the connection.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
76 lines
2.1 KiB
Python
76 lines
2.1 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Mapping
|
|
|
|
RDBMS_SERVICE_KEYS = ("mariadb", "postgres")
|
|
|
|
REALIGN_CONFIG_KEY = "database_realign"
|
|
REALIGN_TARGET = "/etc/mysql/realign.sql"
|
|
|
|
|
|
def _as_mapping(value: Any) -> dict[str, Any]:
|
|
return value if isinstance(value, dict) else {}
|
|
|
|
|
|
def get_compose_services(
|
|
applications: Mapping[str, Any],
|
|
application_id: str,
|
|
) -> dict[str, Any]:
|
|
"""Return the role's services map.
|
|
|
|
The materialised application payload exposes services under the
|
|
bare ``services`` key (no ``compose.services`` wrapper). The
|
|
function name is preserved for backwards compatibility with
|
|
existing call sites.
|
|
"""
|
|
application = _as_mapping(applications.get(application_id))
|
|
return _as_mapping(application.get("services"))
|
|
|
|
|
|
def get_database_service_config(
|
|
applications: Mapping[str, Any],
|
|
application_id: str,
|
|
) -> dict[str, Any]:
|
|
service_key = resolve_database_service_key(applications, application_id)
|
|
if not service_key:
|
|
return {}
|
|
return _as_mapping(
|
|
get_compose_services(applications, application_id).get(service_key)
|
|
)
|
|
|
|
|
|
def resolve_database_service_key(
|
|
applications: Mapping[str, Any],
|
|
application_id: str,
|
|
) -> str:
|
|
services = get_compose_services(applications, application_id)
|
|
enabled_keys = [
|
|
service_key
|
|
for service_key in RDBMS_SERVICE_KEYS
|
|
if _as_mapping(services.get(service_key)).get("enabled") is True
|
|
]
|
|
|
|
if len(enabled_keys) > 1:
|
|
raise ValueError(
|
|
f"{application_id}: multiple direct database services are enabled: "
|
|
+ ", ".join(enabled_keys)
|
|
)
|
|
|
|
return enabled_keys[0] if enabled_keys else ""
|
|
|
|
|
|
def has_single_database_service(
|
|
applications: Mapping[str, Any],
|
|
application_id: str,
|
|
) -> bool:
|
|
services = get_compose_services(applications, application_id)
|
|
enabled_count = sum(
|
|
1
|
|
for service_key in RDBMS_SERVICE_KEYS
|
|
if _as_mapping(services.get(service_key)).get("enabled") is True
|
|
)
|
|
return enabled_count == 1
|