Files
computer-playbook/library/stack_host_files.py
T
kevinveenbirkenbachandClaude Opus 5 d084aead95 perf(web-app-dashboard): write every port-ui catalog in one module run
lookup('i18n_content', …) already returns every language in one dict, and the
role took it apart again: one stack_host_template action per language, a file
task for the directory, a find, and a second loop dropping the catalog of a
language core no longer translates. Four tasks and two loops for what is one
mapping.

The template they looped over was not a template. i18n-content.yaml.j2 held a
single line, `{{ dashboard_i18n_content[item] | to_nice_yaml(width=100000) }}`
— serialisation wearing a costume — so it is deleted.

library/stack_host_files.py takes the whole mapping, serialises each entry,
writes only what differs, drops what the caller no longer wants and creates the
directory, in one module run. The IS_STACK_HOST gate moves to the task's `when`
because a module runs on the target rather than on the controller, where the
sibling stack_host_template and stack_host_copy action plugins gate it.

The per-language file layout stays: files/Dockerfile copies i18n-content/ into
the image, and port-ui reads one yaml per language from /app/i18n/content.

main() stays thin so render, survey, plan and apply can be unit-tested without
mocking AnsibleModule, the way library/database_query.py is tested. The cycle
test asserts the second pass finds nothing to do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-03 00:47:04 +02:00

129 lines
4.1 KiB
Python

from pathlib import Path
import yaml
from ansible.module_utils.basic import AnsibleModule
def render(documents, suffix, indent, width):
"""Serialise each document to YAML under its own file name.
Args:
documents: mapping of base name to the document to write.
suffix: file extension appended to every base name.
indent: YAML block indentation.
width: line width before the dumper wraps a scalar.
Returns:
Mapping of file name to file content.
"""
return {
f"{name}{suffix}": yaml.safe_dump(
value,
default_flow_style=False,
indent=indent,
width=width,
allow_unicode=True,
)
for name, value in documents.items()
}
def survey(directory, suffix):
"""Map every file already carrying *suffix* in *directory* to its path.
Args:
directory: the target directory, which need not exist.
suffix: file extension the caller owns.
"""
if not directory.is_dir():
return {}
return {path.name: path for path in directory.glob(f"*{suffix}")}
def plan(present, wanted, prune):
"""Decide which files to write and which to drop.
Args:
present: mapping of file name to path, as returned by ``survey``.
wanted: mapping of file name to content the caller wants on disk.
prune: whether files the caller no longer wants are removed.
Returns:
A ``(written, removed)`` pair of sorted file-name lists.
"""
def stale(name, content):
if name not in present:
return True
current = present[name].read_text("utf-8") # nocheck: cache-read no utils here
return current != content
written = sorted(name for name, content in wanted.items() if stale(name, content))
removed = sorted(name for name in present if name not in wanted) if prune else []
return written, removed
def apply(directory, present, wanted, written, removed, mode, directory_mode):
"""Write the planned files and drop the planned ones.
Args:
directory: the target directory, created when missing.
present: mapping of file name to path, as returned by ``survey``.
wanted: mapping of file name to content.
written: file names to write.
removed: file names to delete.
mode: octal string for the written files.
directory_mode: octal string for the directory.
"""
directory.mkdir(parents=True, exist_ok=True)
directory.chmod(int(directory_mode, 8))
file_mode = int(mode, 8)
for name in written:
target = directory / name
target.write_text(wanted[name], encoding="utf-8")
target.chmod(file_mode)
for name in removed:
present[name].unlink()
def main():
module_args = {
"path": {"type": "str", "required": True},
"documents": {"type": "dict", "required": True},
"suffix": {"type": "str", "required": False, "default": ".yaml"},
"prune": {"type": "bool", "required": False, "default": True},
"mode": {"type": "str", "required": False, "default": "0644"},
"directory_mode": {"type": "str", "required": False, "default": "0755"},
"indent": {"type": "int", "required": False, "default": 4},
"width": {"type": "int", "required": False, "default": 100000},
}
module = AnsibleModule(argument_spec=module_args, supports_check_mode=True)
directory = Path(module.params["path"])
suffix = module.params["suffix"]
wanted = render(
module.params["documents"],
suffix,
module.params["indent"],
module.params["width"],
)
present = survey(directory, suffix)
written, removed = plan(present, wanted, module.params["prune"])
if not module.check_mode:
apply(
directory,
present,
wanted,
written,
removed,
module.params["mode"],
module.params["directory_mode"],
)
module.exit_json(changed=bool(written or removed), written=written, removed=removed)
if __name__ == "__main__":
main()