mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
The custom flag moves from 33 hardcoded template kwargs into the merged service config: 'custom: true' names the shared <entity>_custom build, a string names a second distinct '<string>_custom' image inside one app (nextcloud whiteboard). The image lookup reads the declaration as a fallback, so config consumers (e.g. backup image matching) resolve the exact deployed reference from one declaration point. nextcloud cron now runs the nextcloud_custom build like the app service instead of the upstream image. A new services lint couples the declaration to reality in both directions: custom requires a role Dockerfile, and a role Dockerfile requires a custom declaration. The eight legacy roles deploying local builds under upstream tags or ad-hoc image= overrides are ratcheted via '# nocheck: dockerfile-custom'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
101 lines
3.5 KiB
Python
101 lines
3.5 KiB
Python
"""Couple role Dockerfiles to ``services.<key>.custom`` declarations.
|
|
|
|
Rule
|
|
====
|
|
Both directions must hold per role:
|
|
|
|
* A service declaring ``custom:`` (locally-built image) requires a
|
|
``Dockerfile``/``Dockerfile.j2`` in the role — otherwise the resolved
|
|
``*_custom`` reference points at an image nothing ever builds.
|
|
* A role shipping a ``Dockerfile`` must declare ``custom:`` on at least
|
|
one service in ``meta/services.yml`` — otherwise the local build is
|
|
deployed under an upstream tag (shadowing the real upstream image on
|
|
the host) or through an ad-hoc ``image=`` override, both of which hide
|
|
the build from config consumers such as the backup image matching.
|
|
|
|
Per-role opt-out
|
|
================
|
|
Add ``# nocheck: dockerfile-custom`` anywhere in the role's
|
|
``meta/services.yml``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
from utils.cache.files import iter_project_files, read_text
|
|
from utils.cache.yaml import load_yaml_any
|
|
from utils.roles.mapping import ROLE_FILE_META_SERVICES
|
|
|
|
from . import PROJECT_ROOT
|
|
|
|
_RULE = "dockerfile-custom"
|
|
_ROLES_PREFIX = "roles/"
|
|
|
|
|
|
def _custom_services(services_path):
|
|
try:
|
|
data = load_yaml_any(services_path, default_if_missing={})
|
|
except yaml.YAMLError:
|
|
return []
|
|
if not isinstance(data, dict):
|
|
return []
|
|
return sorted(
|
|
key
|
|
for key, entry in data.items()
|
|
if isinstance(entry, dict) and entry.get("custom")
|
|
)
|
|
|
|
|
|
class TestCustomDockerfileCoupling(unittest.TestCase):
|
|
def test_dockerfile_and_custom_declarations_match(self) -> None:
|
|
dockerfiles: dict[str, list[str]] = {}
|
|
services_files: dict[str, str] = {}
|
|
for path in iter_project_files(exclude_tests=True):
|
|
rel = Path(path).relative_to(PROJECT_ROOT).as_posix()
|
|
if not rel.startswith(_ROLES_PREFIX):
|
|
continue
|
|
parts = rel.split("/")
|
|
if len(parts) < 3:
|
|
continue
|
|
role = parts[1]
|
|
if Path(rel).name.startswith("Dockerfile"):
|
|
dockerfiles.setdefault(role, []).append("/".join(parts[2:]))
|
|
elif rel == f"{_ROLES_PREFIX}{role}/{ROLE_FILE_META_SERVICES}":
|
|
services_files[role] = path
|
|
|
|
findings: list[str] = []
|
|
for role in sorted(set(dockerfiles) | set(services_files)):
|
|
services_path = services_files.get(role)
|
|
if services_path and f"# nocheck: {_RULE}" in read_text(services_path):
|
|
continue
|
|
role_dockerfiles = sorted(dockerfiles.get(role, []))
|
|
custom = _custom_services(services_path) if services_path else []
|
|
rel = f"{_ROLES_PREFIX}{role}"
|
|
if custom and not role_dockerfiles:
|
|
findings.append(
|
|
f"- {rel}: services {custom} declare custom: but the role "
|
|
"ships no Dockerfile"
|
|
)
|
|
if role_dockerfiles and not custom:
|
|
findings.append(
|
|
f"- {rel}: role ships {role_dockerfiles} but no service in "
|
|
"meta/services.yml declares custom:"
|
|
)
|
|
|
|
if findings:
|
|
self.fail(
|
|
"Dockerfile/custom coupling violated (locally-built images "
|
|
"must be declared via services.<key>.custom, and every "
|
|
"custom declaration needs a Dockerfile). Opt out with "
|
|
f"`# nocheck: {_RULE}` in the role's meta/services.yml:\n"
|
|
+ "\n".join(sorted(findings))
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|