#!/bin/env python3 # Copyright: (c) 2025, Luca Bilke <luca@bil.ke> # MIT License (see LICENSE) # ruff: noqa: T201 from __future__ import annotations import sys import textwrap from pathlib import Path from typing import TYPE_CHECKING, cast import yaml from ansible_collections.snailed.ez_docker.plugins.module_utils import ( label, service, ) if TYPE_CHECKING: from types import ModuleType GALAXY_METADATA_PATH = Path("../galaxy.yml") def get_modules(parent_module: ModuleType) -> list[tuple[str, ModuleType]]: return [ (name, getattr(parent_module, name)) for name in parent_module.__all__ if name != "common" ] def settings_format(text: str) -> str: return "\n".join( line for line in text.splitlines() if not line.strip().startswith(("default:", "required:")) ) def get_module_docs( module: ModuleType, ) -> str: ret: str = "" if docs := getattr(module, "DOCUMENTATION", None): ret = docs if ret: return ret msg = f"Module {module.__name__} has no documentation" raise ValueError(msg) def get_metadata() -> str: with GALAXY_METADATA_PATH.open("r") as f: metadata = yaml.safe_load(f) docs = """ module: compose version_added: 1.0.0 short_description: Simplify docker-compose deployments. description: Easily create docker-compose files using a single module seealso: - name: Compose file reference description: Complete reference of the compose file spec. link: https://docs.docker.com/reference/compose-file/ attributes: check_mode: support: full diff_mode: support: full """ if author := metadata.get("author"): if isinstance(author, str): docs += f"author: {author}\n" elif isinstance(author, list): docs += "author:\n" for a in cast(list[str], author): docs += f" - {a}\n" return docs def get_settings_docs() -> str: service_default_definitions = """ service_default_definitions: description: - Default definitions for each service type: dict suboptions: """ service_default_args = """ service_default_args: description: - Default arguments for each service helper. type: dict suboptions: """ label_default_args = """ label_default_args: description: - Default arguments for each label helper. type: dict suboptions: """ for name, module in get_modules(service): if module.__name__.endswith("common"): continue service_default_definitions += f""" {name}: description: - Default definitions for {name} services. type: dict """ service_default_args += textwrap.indent(settings_format(get_module_docs(module)), " " * 8) if module.__name__.endswith("custom"): for _, label_module in get_modules(label): if label_module.__name__.endswith("common"): continue service_default_args += textwrap.indent( settings_format(get_module_docs(label_module)), " " * 24 ) else: service_default_args += textwrap.indent( settings_format(service.common.BASE_DOCUMENTATION), " " * 16 ) for _, module in get_modules(label): if module.__name__.endswith("common"): continue label_default_args += textwrap.indent(settings_format(get_module_docs(module)), " " * 8) return f""" settings: description: - Settings/Defaults for the module. type: dict suboptions: external_networks: description: - Networks to mark as external. type: list elements: str external_volumes: description: - Volumes to mark as external. type: list elements: str default_definition: description: - Default definition for all containers. - Overwritten by per-service defaults. type: dict {textwrap.indent(service_default_definitions, " " * 8)} {textwrap.indent(service_default_args, " " * 8)} {textwrap.indent(label_default_args, " " * 8)} """ def get_service_docs() -> str: service_docs = """ services: description: - Services to create in the project. type: dict suboptions: """ for _, module in get_modules(service): service_docs += f""" {textwrap.indent(get_module_docs(module), " " * 8)} """ if module.__name__.endswith("custom"): for _, label_module in get_modules(label): if label_module.__name__ == "common": continue service_docs += textwrap.indent( settings_format(get_module_docs(label_module)), " " * 24 ) else: service_docs += textwrap.indent(service.common.BASE_DOCUMENTATION, " " * 16) return service_docs ret = f""" --- {get_metadata()} options: name: description: - Name of the compose project to create or modify. aliases: [project] type: str project_dir: description: - Path to store project directory under. type: path {textwrap.indent(get_settings_docs(), " " * 4)} {textwrap.indent(get_service_docs(), " " * 4)} """ ret = "\n".join(line for line in ret.splitlines() if line.strip()) print(ret) try: yaml.safe_load(ret) except yaml.YAMLError as e: print("YAML is invalid!\n", file=sys.stderr) print(e, file=sys.stderr)