mirror of
				https://github.com/kevinveenbirkenbach/computer-playbook.git
				synced 2025-11-04 04:08:15 +00:00 
			
		
		
		
	Compare commits
	
		
			19 Commits
		
	
	
		
			78ee3e3c64
			...
			8608d89653
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8608d89653 | |||
| a4f39ac732 | |||
| 9cfb8f3a60 | |||
| 3e5344a46c | |||
| ec07d1a20b | |||
| 594d9417d1 | |||
| dc125e4843 | |||
| 39a54294dd | |||
| a57fe718de | |||
| b6aec5fe33 | |||
| de07d890dc | |||
| e27f355697 | |||
| 790762d397 | |||
| 4ce681e643 | |||
| 55cf3d0d8e | |||
| 2708b67751 | |||
| f477ee3731 | |||
| 6d70f78989 | |||
| b867a52471 | 
@@ -2,174 +2,45 @@
 | 
			
		||||
import os
 | 
			
		||||
import argparse
 | 
			
		||||
import json
 | 
			
		||||
import fnmatch
 | 
			
		||||
import re
 | 
			
		||||
from typing import Dict, Any
 | 
			
		||||
 | 
			
		||||
import yaml
 | 
			
		||||
 | 
			
		||||
from cli.build.graph import build_mappings, output_graph
 | 
			
		||||
from module_utils.role_dependency_resolver import RoleDependencyResolver
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def find_roles(roles_dir: str):
 | 
			
		||||
    """Yield (role_name, role_path) for every subfolder in roles_dir."""
 | 
			
		||||
    for entry in os.listdir(roles_dir):
 | 
			
		||||
        path = os.path.join(roles_dir, entry)
 | 
			
		||||
        if os.path.isdir(path):
 | 
			
		||||
            yield entry, path
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _is_pure_jinja_var(s: str) -> bool:
 | 
			
		||||
    """Check if string is exactly a single {{ var }} expression."""
 | 
			
		||||
    return bool(re.fullmatch(r"\s*\{\{\s*[^}]+\s*\}\}\s*", s))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _jinja_to_glob(s: str) -> str:
 | 
			
		||||
    """Convert Jinja placeholders {{ ... }} into * for fnmatch."""
 | 
			
		||||
    pattern = re.sub(r"\{\{[^}]+\}\}", "*", s)
 | 
			
		||||
    pattern = re.sub(r"\*{2,}", "*", pattern)
 | 
			
		||||
    return pattern.strip()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _list_role_dirs(roles_dir: str) -> list[str]:
 | 
			
		||||
    """Return a list of role directory names inside roles_dir."""
 | 
			
		||||
    return [
 | 
			
		||||
        d for d in os.listdir(roles_dir)
 | 
			
		||||
        if os.path.isdir(os.path.join(roles_dir, d))
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def find_include_role_dependencies(role_path: str, roles_dir: str) -> set[str]:
 | 
			
		||||
    """
 | 
			
		||||
    Scan all tasks/*.yml(.yaml) files of a role and collect include_role dependencies.
 | 
			
		||||
 | 
			
		||||
    Rules:
 | 
			
		||||
      - loop/with_items with literal strings -> add those as roles
 | 
			
		||||
      - name contains jinja AND surrounding literals -> convert to glob and match existing roles
 | 
			
		||||
      - name is a pure jinja variable only -> ignore
 | 
			
		||||
      - name is a pure literal -> add as-is
 | 
			
		||||
    """
 | 
			
		||||
    deps: set[str] = set()
 | 
			
		||||
    tasks_dir = os.path.join(role_path, "tasks")
 | 
			
		||||
    if not os.path.isdir(tasks_dir):
 | 
			
		||||
        return deps
 | 
			
		||||
 | 
			
		||||
    candidates = []
 | 
			
		||||
    for root, _, files in os.walk(tasks_dir):
 | 
			
		||||
        for f in files:
 | 
			
		||||
            if f.endswith(".yml") or f.endswith(".yaml"):
 | 
			
		||||
                candidates.append(os.path.join(root, f))
 | 
			
		||||
 | 
			
		||||
    all_roles = _list_role_dirs(roles_dir)
 | 
			
		||||
 | 
			
		||||
    def add_literal_loop_items(loop_val):
 | 
			
		||||
        if isinstance(loop_val, list):
 | 
			
		||||
            for item in loop_val:
 | 
			
		||||
                if isinstance(item, str) and item.strip():
 | 
			
		||||
                    deps.add(item.strip())
 | 
			
		||||
 | 
			
		||||
    for file_path in candidates:
 | 
			
		||||
        try:
 | 
			
		||||
            with open(file_path, "r", encoding="utf-8") as f:
 | 
			
		||||
                docs = list(yaml.safe_load_all(f))
 | 
			
		||||
        except Exception:
 | 
			
		||||
            # Be tolerant to any parsing issues; skip unreadable files
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
        for doc in docs:
 | 
			
		||||
            if not isinstance(doc, list):
 | 
			
		||||
                continue
 | 
			
		||||
            for task in doc:
 | 
			
		||||
                if not isinstance(task, dict):
 | 
			
		||||
                    continue
 | 
			
		||||
                if "include_role" not in task:
 | 
			
		||||
                    continue
 | 
			
		||||
                inc = task.get("include_role")
 | 
			
		||||
                if not isinstance(inc, dict):
 | 
			
		||||
                    continue
 | 
			
		||||
                name = inc.get("name")
 | 
			
		||||
                if not isinstance(name, str) or not name.strip():
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                # 1) Handle loop/with_items
 | 
			
		||||
                loop_val = task.get("loop", task.get("with_items"))
 | 
			
		||||
                if loop_val is not None:
 | 
			
		||||
                    add_literal_loop_items(loop_val)
 | 
			
		||||
                    # still check name for surrounding literals
 | 
			
		||||
                    if not _is_pure_jinja_var(name):
 | 
			
		||||
                        pattern = (
 | 
			
		||||
                            _jinja_to_glob(name)
 | 
			
		||||
                            if ("{{" in name and "}}" in name)
 | 
			
		||||
                            else name
 | 
			
		||||
                        )
 | 
			
		||||
                        if "*" in pattern:
 | 
			
		||||
                            for r in all_roles:
 | 
			
		||||
                                if fnmatch.fnmatch(r, pattern):
 | 
			
		||||
                                    deps.add(r)
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                # 2) No loop: evaluate name
 | 
			
		||||
                if "{{" in name and "}}" in name:
 | 
			
		||||
                    if _is_pure_jinja_var(name):
 | 
			
		||||
                        continue  # ignore pure variable
 | 
			
		||||
                    pattern = _jinja_to_glob(name)
 | 
			
		||||
                    if "*" in pattern:
 | 
			
		||||
                        for r in all_roles:
 | 
			
		||||
                            if fnmatch.fnmatch(r, pattern):
 | 
			
		||||
                                deps.add(r)
 | 
			
		||||
                        continue
 | 
			
		||||
                    else:
 | 
			
		||||
                        deps.add(pattern)
 | 
			
		||||
                else:
 | 
			
		||||
                    # pure literal
 | 
			
		||||
                    deps.add(name.strip())
 | 
			
		||||
 | 
			
		||||
    return deps
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def main():
 | 
			
		||||
    # default roles dir is ../../roles relative to this script
 | 
			
		||||
    script_dir = os.path.dirname(os.path.abspath(__file__))
 | 
			
		||||
    default_roles_dir = os.path.abspath(
 | 
			
		||||
        os.path.join(script_dir, "..", "..", "roles")
 | 
			
		||||
    )
 | 
			
		||||
    default_roles_dir = os.path.abspath(os.path.join(script_dir, "..", "..", "roles"))
 | 
			
		||||
 | 
			
		||||
    parser = argparse.ArgumentParser(
 | 
			
		||||
        description="Generate all graphs for each role and write meta/tree.json"
 | 
			
		||||
    )
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        "-d", "--role_dir",
 | 
			
		||||
        default=default_roles_dir,
 | 
			
		||||
        help=f"Path to roles directory (default: {default_roles_dir})"
 | 
			
		||||
    )
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        "-D", "--depth",
 | 
			
		||||
        type=int,
 | 
			
		||||
        default=0,
 | 
			
		||||
        help="Max recursion depth (>0) or <=0 to stop on cycle"
 | 
			
		||||
    )
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        "-o", "--output",
 | 
			
		||||
        choices=["yaml", "json", "console"],
 | 
			
		||||
        default="json",
 | 
			
		||||
        help="Output format"
 | 
			
		||||
    )
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        "-p", "--preview",
 | 
			
		||||
        action="store_true",
 | 
			
		||||
        help="Preview graphs to console instead of writing files"
 | 
			
		||||
    )
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        "-s", "--shadow-folder",
 | 
			
		||||
        type=str,
 | 
			
		||||
        default=None,
 | 
			
		||||
        help="If set, writes tree.json to this shadow folder instead of the role's actual meta/ folder"
 | 
			
		||||
    )
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        "-v", "--verbose",
 | 
			
		||||
        action="store_true",
 | 
			
		||||
        help="Enable verbose logging"
 | 
			
		||||
    )
 | 
			
		||||
    parser.add_argument("-d", "--role_dir", default=default_roles_dir,
 | 
			
		||||
                        help=f"Path to roles directory (default: {default_roles_dir})")
 | 
			
		||||
    parser.add_argument("-D", "--depth", type=int, default=0,
 | 
			
		||||
                        help="Max recursion depth (>0) or <=0 to stop on cycle")
 | 
			
		||||
    parser.add_argument("-o", "--output", choices=["yaml", "json", "console"],
 | 
			
		||||
                        default="json", help="Output format")
 | 
			
		||||
    parser.add_argument("-p", "--preview", action="store_true",
 | 
			
		||||
                        help="Preview graphs to console instead of writing files")
 | 
			
		||||
    parser.add_argument("-s", "--shadow-folder", type=str, default=None,
 | 
			
		||||
                        help="If set, writes tree.json to this shadow folder instead of the role's actual meta/ folder")
 | 
			
		||||
    parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose logging")
 | 
			
		||||
 | 
			
		||||
    # Toggles
 | 
			
		||||
    parser.add_argument("--no-include-role", action="store_true", help="Do not scan include_role")
 | 
			
		||||
    parser.add_argument("--no-import-role", action="store_true", help="Do not scan import_role")
 | 
			
		||||
    parser.add_argument("--no-dependencies", action="store_true", help="Do not read meta/main.yml dependencies")
 | 
			
		||||
    parser.add_argument("--no-run-after", action="store_true",
 | 
			
		||||
                        help="Do not read galaxy_info.run_after from meta/main.yml")
 | 
			
		||||
 | 
			
		||||
    args = parser.parse_args()
 | 
			
		||||
 | 
			
		||||
    if args.verbose:
 | 
			
		||||
@@ -179,6 +50,8 @@ def main():
 | 
			
		||||
        print(f"Preview mode: {args.preview}")
 | 
			
		||||
        print(f"Shadow folder: {args.shadow_folder}")
 | 
			
		||||
 | 
			
		||||
    resolver = RoleDependencyResolver(args.role_dir)
 | 
			
		||||
 | 
			
		||||
    for role_name, role_path in find_roles(args.role_dir):
 | 
			
		||||
        if args.verbose:
 | 
			
		||||
            print(f"Processing role: {role_name}")
 | 
			
		||||
@@ -189,13 +62,26 @@ def main():
 | 
			
		||||
            max_depth=args.depth
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # add include_role dependencies from tasks
 | 
			
		||||
        include_deps = find_include_role_dependencies(role_path, args.role_dir)
 | 
			
		||||
        if include_deps:
 | 
			
		||||
        # Direct deps (depth=1) – getrennt erfasst für buckets
 | 
			
		||||
        inc_roles, imp_roles = resolver._scan_tasks(role_path)
 | 
			
		||||
        meta_deps = resolver._extract_meta_dependencies(role_path)
 | 
			
		||||
        run_after = set()
 | 
			
		||||
        if not args.no_run_after:
 | 
			
		||||
            run_after = resolver._extract_meta_run_after(role_path)
 | 
			
		||||
 | 
			
		||||
        if any([not args.no_include_role and inc_roles,
 | 
			
		||||
                not args.no_import_role and imp_roles,
 | 
			
		||||
                not args.no_dependencies and meta_deps,
 | 
			
		||||
                not args.no_run_after and run_after]):
 | 
			
		||||
            deps_root = graphs.setdefault("dependencies", {})
 | 
			
		||||
            inc_list = set(deps_root.get("include_role", []))
 | 
			
		||||
            inc_list.update(include_deps)
 | 
			
		||||
            deps_root["include_role"] = sorted(inc_list)
 | 
			
		||||
            if not args.no_include_role and inc_roles:
 | 
			
		||||
                deps_root["include_role"] = sorted(inc_roles)
 | 
			
		||||
            if not args.no_import_role and imp_roles:
 | 
			
		||||
                deps_root["import_role"] = sorted(imp_roles)
 | 
			
		||||
            if not args.no_dependencies and meta_deps:
 | 
			
		||||
                deps_root["dependencies"] = sorted(meta_deps)
 | 
			
		||||
            if not args.no_run_after and run_after:
 | 
			
		||||
                deps_root["run_after"] = sorted(run_after)
 | 
			
		||||
            graphs["dependencies"] = deps_root
 | 
			
		||||
 | 
			
		||||
        if args.preview:
 | 
			
		||||
@@ -205,13 +91,11 @@ def main():
 | 
			
		||||
                output_graph(data, "console", role_name, key)
 | 
			
		||||
        else:
 | 
			
		||||
            if args.shadow_folder:
 | 
			
		||||
                tree_file = os.path.join(
 | 
			
		||||
                    args.shadow_folder, role_name, "meta", "tree.json"
 | 
			
		||||
                )
 | 
			
		||||
                tree_file = os.path.join(args.shadow_folder, role_name, "meta", "tree.json")
 | 
			
		||||
            else:
 | 
			
		||||
                tree_file = os.path.join(role_path, "meta", "tree.json")
 | 
			
		||||
            os.makedirs(os.path.dirname(tree_file), exist_ok=True)
 | 
			
		||||
            with open(tree_file, "w") as f:
 | 
			
		||||
            with open(tree_file, "w", encoding="utf-8") as f:
 | 
			
		||||
                json.dump(graphs, f, indent=2)
 | 
			
		||||
            print(f"Wrote {tree_file}")
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,31 +4,71 @@ import os
 | 
			
		||||
 | 
			
		||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
 | 
			
		||||
from module_utils.entity_name_utils import get_entity_name
 | 
			
		||||
from module_utils.role_dependency_resolver import RoleDependencyResolver
 | 
			
		||||
from typing import Iterable
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FilterModule(object):
 | 
			
		||||
    def filters(self):
 | 
			
		||||
        return {'canonical_domains_map': self.canonical_domains_map}
 | 
			
		||||
 | 
			
		||||
    def canonical_domains_map(self, apps, PRIMARY_DOMAIN):
 | 
			
		||||
    def canonical_domains_map(
 | 
			
		||||
        self,
 | 
			
		||||
        apps,
 | 
			
		||||
        PRIMARY_DOMAIN,
 | 
			
		||||
        *,
 | 
			
		||||
        recursive: bool = False,
 | 
			
		||||
        roles_base_dir: str | None = None,
 | 
			
		||||
        seed: Iterable[str] | None = None,
 | 
			
		||||
    ):
 | 
			
		||||
        """
 | 
			
		||||
        Maps applications to their canonical domains, checking for conflicts 
 | 
			
		||||
        and ensuring all domains are valid and unique across applications.
 | 
			
		||||
        Build { app_id: [canonical domains...] }.
 | 
			
		||||
 | 
			
		||||
        Rekursiv werden nur include_role, import_role und meta/main.yml:dependencies verfolgt.
 | 
			
		||||
        'run_after' wird hier absichtlich ignoriert.
 | 
			
		||||
        """
 | 
			
		||||
        if not isinstance(apps, dict):
 | 
			
		||||
            raise AnsibleFilterError(f"'apps' must be a dict, got {type(apps).__name__}")
 | 
			
		||||
 | 
			
		||||
        app_keys  = set(apps.keys())
 | 
			
		||||
        seed_keys = set(seed) if seed is not None else app_keys
 | 
			
		||||
 | 
			
		||||
        if recursive:
 | 
			
		||||
            roles_base_dir = roles_base_dir or os.path.join(os.getcwd(), "roles")
 | 
			
		||||
            if not os.path.isdir(roles_base_dir):
 | 
			
		||||
                raise AnsibleFilterError(
 | 
			
		||||
                    f"roles_base_dir '{roles_base_dir}' not found or not a directory."
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            resolver = RoleDependencyResolver(roles_base_dir)
 | 
			
		||||
            discovered_roles = resolver.resolve_transitively(
 | 
			
		||||
                start_roles=seed_keys,
 | 
			
		||||
                resolve_include_role=True,
 | 
			
		||||
                resolve_import_role=True,
 | 
			
		||||
                resolve_dependencies=True,
 | 
			
		||||
                resolve_run_after=False,
 | 
			
		||||
                max_depth=None,
 | 
			
		||||
            )
 | 
			
		||||
            # all discovered roles that actually have config entries in `apps`
 | 
			
		||||
            target_apps = discovered_roles & app_keys
 | 
			
		||||
        else:
 | 
			
		||||
            target_apps = seed_keys
 | 
			
		||||
 | 
			
		||||
        result = {}
 | 
			
		||||
        seen_domains = {}
 | 
			
		||||
 | 
			
		||||
        for app_id, cfg in apps.items():
 | 
			
		||||
            if app_id.startswith((
 | 
			
		||||
                    "web-",
 | 
			
		||||
                    "svc-db-"   # Database services can also be exposed to the internet. It is just listening to the port, but the domain is used for port mapping
 | 
			
		||||
                    )):
 | 
			
		||||
        for app_id in sorted(target_apps):
 | 
			
		||||
            cfg = apps.get(app_id)
 | 
			
		||||
            if cfg is None:
 | 
			
		||||
                continue
 | 
			
		||||
            if not str(app_id).startswith(("web-", "svc-db-")):
 | 
			
		||||
                continue
 | 
			
		||||
            if not isinstance(cfg, dict):
 | 
			
		||||
                raise AnsibleFilterError(
 | 
			
		||||
                    f"Invalid configuration for application '{app_id}': "
 | 
			
		||||
                    f"expected a dict, got {cfg!r}"
 | 
			
		||||
                    f"Invalid configuration for application '{app_id}': expected dict, got {cfg!r}"
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                domains_cfg = cfg.get('server',{}).get('domains',{})
 | 
			
		||||
            domains_cfg = cfg.get('server', {}).get('domains', {})
 | 
			
		||||
            if not domains_cfg or 'canonical' not in domains_cfg:
 | 
			
		||||
                self._add_default_domain(app_id, PRIMARY_DOMAIN, seen_domains, result)
 | 
			
		||||
                continue
 | 
			
		||||
@@ -39,10 +79,6 @@ class FilterModule(object):
 | 
			
		||||
        return result
 | 
			
		||||
 | 
			
		||||
    def _add_default_domain(self, app_id, PRIMARY_DOMAIN, seen_domains, result):
 | 
			
		||||
        """
 | 
			
		||||
        Add the default domain for an application if no canonical domains are defined.
 | 
			
		||||
        Ensures the domain is unique across applications.
 | 
			
		||||
        """
 | 
			
		||||
        entity_name = get_entity_name(app_id)
 | 
			
		||||
        default_domain = f"{entity_name}.{PRIMARY_DOMAIN}"
 | 
			
		||||
        if default_domain in seen_domains:
 | 
			
		||||
@@ -54,40 +90,21 @@ class FilterModule(object):
 | 
			
		||||
        result[app_id] = [default_domain]
 | 
			
		||||
 | 
			
		||||
    def _process_canonical_domains(self, app_id, canonical_domains, seen_domains, result):
 | 
			
		||||
        """
 | 
			
		||||
        Process the canonical domains for an application, handling both lists and dicts,
 | 
			
		||||
        and ensuring each domain is unique.
 | 
			
		||||
        """
 | 
			
		||||
        if isinstance(canonical_domains, dict):
 | 
			
		||||
            self._process_canonical_domains_dict(app_id, canonical_domains, seen_domains, result)
 | 
			
		||||
            for _, domain in canonical_domains.items():
 | 
			
		||||
                self._validate_and_check_domain(app_id, domain, seen_domains)
 | 
			
		||||
            result[app_id] = canonical_domains.copy()
 | 
			
		||||
        elif isinstance(canonical_domains, list):
 | 
			
		||||
            self._process_canonical_domains_list(app_id, canonical_domains, seen_domains, result)
 | 
			
		||||
            for domain in canonical_domains:
 | 
			
		||||
                self._validate_and_check_domain(app_id, domain, seen_domains)
 | 
			
		||||
            result[app_id] = list(canonical_domains)
 | 
			
		||||
        else:
 | 
			
		||||
            raise AnsibleFilterError(
 | 
			
		||||
                f"Unexpected type for 'server.domains.canonical' in application '{app_id}': "
 | 
			
		||||
                f"{type(canonical_domains).__name__}"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    def _process_canonical_domains_dict(self, app_id, domains_dict, seen_domains, result):
 | 
			
		||||
        """
 | 
			
		||||
        Process a dictionary of canonical domains for an application.
 | 
			
		||||
        """
 | 
			
		||||
        for name, domain in domains_dict.items():
 | 
			
		||||
            self._validate_and_check_domain(app_id, domain, seen_domains)
 | 
			
		||||
        result[app_id] = domains_dict.copy()
 | 
			
		||||
 | 
			
		||||
    def _process_canonical_domains_list(self, app_id, domains_list, seen_domains, result):
 | 
			
		||||
        """
 | 
			
		||||
        Process a list of canonical domains for an application.
 | 
			
		||||
        """
 | 
			
		||||
        for domain in domains_list:
 | 
			
		||||
            self._validate_and_check_domain(app_id, domain, seen_domains)
 | 
			
		||||
        result[app_id] = list(domains_list)
 | 
			
		||||
 | 
			
		||||
    def _validate_and_check_domain(self, app_id, domain, seen_domains):
 | 
			
		||||
        """
 | 
			
		||||
        Validate the domain and check if it has already been assigned to another application.
 | 
			
		||||
        """
 | 
			
		||||
        if not isinstance(domain, str) or not domain.strip():
 | 
			
		||||
            raise AnsibleFilterError(
 | 
			
		||||
                f"Invalid domain entry in 'canonical' for application '{app_id}': {domain!r}"
 | 
			
		||||
 
 | 
			
		||||
@@ -6,3 +6,4 @@ PATH_SYSTEMCTL_SCRIPTS:         "{{ [ PATH_ADMINISTRATOR_SCRIPTS, 'systemctl' ]
 | 
			
		||||
PATH_DOCKER_COMPOSE_INSTANCES:      "/opt/docker/"
 | 
			
		||||
PATH_SYSTEM_LOCK_SCRIPT:            "/opt/scripts/sys-lock.py"
 | 
			
		||||
PATH_SYSTEM_SERVICE_DIR:            "/etc/systemd/system"
 | 
			
		||||
PATH_DOCKER_COMPOSE_PULL_LOCK_DIR:  "/run/ansible/compose-pull/"
 | 
			
		||||
@@ -88,7 +88,7 @@ defaults_networks:
 | 
			
		||||
      subnet: 192.168.103.96/28
 | 
			
		||||
    web-svc-simpleicons:
 | 
			
		||||
      subnet: 192.168.103.112/28
 | 
			
		||||
    web-app-libretranslate:
 | 
			
		||||
    web-svc-libretranslate:
 | 
			
		||||
      subnet: 192.168.103.128/28
 | 
			
		||||
    web-app-pretix:
 | 
			
		||||
      subnet: 192.168.103.144/28
 | 
			
		||||
 
 | 
			
		||||
@@ -66,7 +66,7 @@ ports:
 | 
			
		||||
      web-svc-collabora: 8042
 | 
			
		||||
      web-app-mobilizon: 8043
 | 
			
		||||
      web-svc-simpleicons: 8044
 | 
			
		||||
      web-app-libretranslate: 8045
 | 
			
		||||
      web-svc-libretranslate: 8045
 | 
			
		||||
      web-app-pretix: 8046
 | 
			
		||||
      web-app-mig: 8047
 | 
			
		||||
      web-svc-logout: 8048
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										296
									
								
								module_utils/role_dependency_resolver.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										296
									
								
								module_utils/role_dependency_resolver.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,296 @@
 | 
			
		||||
import os
 | 
			
		||||
import fnmatch
 | 
			
		||||
import re
 | 
			
		||||
from typing import Dict, Set, Iterable, Tuple, Optional
 | 
			
		||||
 | 
			
		||||
import yaml
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RoleDependencyResolver:
 | 
			
		||||
    _RE_PURE_JINJA = re.compile(r"\s*\{\{\s*[^}]+\s*\}\}\s*$")
 | 
			
		||||
 | 
			
		||||
    def __init__(self, roles_dir: str):
 | 
			
		||||
        self.roles_dir = roles_dir
 | 
			
		||||
 | 
			
		||||
    # -------------------------- public API --------------------------
 | 
			
		||||
 | 
			
		||||
    def resolve_transitively(
 | 
			
		||||
        self,
 | 
			
		||||
        start_roles: Iterable[str],
 | 
			
		||||
        *,
 | 
			
		||||
        resolve_include_role: bool = True,
 | 
			
		||||
        resolve_import_role: bool = True,
 | 
			
		||||
        resolve_dependencies: bool = True,
 | 
			
		||||
        resolve_run_after: bool = False,
 | 
			
		||||
        max_depth: Optional[int] = None,
 | 
			
		||||
    ) -> Set[str]:
 | 
			
		||||
        to_visit = list(dict.fromkeys(start_roles))
 | 
			
		||||
        visited: Set[str] = set()
 | 
			
		||||
        depth: Dict[str, int] = {}
 | 
			
		||||
 | 
			
		||||
        for r in to_visit:
 | 
			
		||||
            depth[r] = 0
 | 
			
		||||
 | 
			
		||||
        while to_visit:
 | 
			
		||||
            role = to_visit.pop()
 | 
			
		||||
            cur_d = depth.get(role, 0)
 | 
			
		||||
            if role in visited:
 | 
			
		||||
                continue
 | 
			
		||||
            visited.add(role)
 | 
			
		||||
 | 
			
		||||
            if max_depth is not None and cur_d >= max_depth:
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            for dep in self.get_role_dependencies(
 | 
			
		||||
                role,
 | 
			
		||||
                resolve_include_role=resolve_include_role,
 | 
			
		||||
                resolve_import_role=resolve_import_role,
 | 
			
		||||
                resolve_dependencies=resolve_dependencies,
 | 
			
		||||
                resolve_run_after=resolve_run_after,
 | 
			
		||||
            ):
 | 
			
		||||
                if dep not in visited:
 | 
			
		||||
                    to_visit.append(dep)
 | 
			
		||||
                    depth[dep] = cur_d + 1
 | 
			
		||||
 | 
			
		||||
        return visited
 | 
			
		||||
 | 
			
		||||
    def get_role_dependencies(
 | 
			
		||||
        self,
 | 
			
		||||
        role_name: str,
 | 
			
		||||
        *,
 | 
			
		||||
        resolve_include_role: bool = True,
 | 
			
		||||
        resolve_import_role: bool = True,
 | 
			
		||||
        resolve_dependencies: bool = True,
 | 
			
		||||
        resolve_run_after: bool = False,
 | 
			
		||||
    ) -> Set[str]:
 | 
			
		||||
        role_path = os.path.join(self.roles_dir, role_name)
 | 
			
		||||
        if not os.path.isdir(role_path):
 | 
			
		||||
            return set()
 | 
			
		||||
 | 
			
		||||
        deps: Set[str] = set()
 | 
			
		||||
 | 
			
		||||
        if resolve_include_role or resolve_import_role:
 | 
			
		||||
            includes, imports = self._scan_tasks(role_path)
 | 
			
		||||
            if resolve_include_role:
 | 
			
		||||
                deps |= includes
 | 
			
		||||
            if resolve_import_role:
 | 
			
		||||
                deps |= imports
 | 
			
		||||
 | 
			
		||||
        if resolve_dependencies:
 | 
			
		||||
            deps |= self._extract_meta_dependencies(role_path)
 | 
			
		||||
 | 
			
		||||
        if resolve_run_after:
 | 
			
		||||
            deps |= self._extract_meta_run_after(role_path)
 | 
			
		||||
 | 
			
		||||
        return deps
 | 
			
		||||
 | 
			
		||||
    # -------------------------- scanning helpers --------------------------
 | 
			
		||||
 | 
			
		||||
    def _scan_tasks(self, role_path: str) -> Tuple[Set[str], Set[str]]:
 | 
			
		||||
        tasks_dir = os.path.join(role_path, "tasks")
 | 
			
		||||
        include_roles: Set[str] = set()
 | 
			
		||||
        import_roles: Set[str] = set()
 | 
			
		||||
 | 
			
		||||
        if not os.path.isdir(tasks_dir):
 | 
			
		||||
            return include_roles, import_roles
 | 
			
		||||
 | 
			
		||||
        all_roles = self._list_role_dirs(self.roles_dir)
 | 
			
		||||
 | 
			
		||||
        candidates = []
 | 
			
		||||
        for root, _, files in os.walk(tasks_dir):
 | 
			
		||||
            for f in files:
 | 
			
		||||
                if f.endswith(".yml") or f.endswith(".yaml"):
 | 
			
		||||
                    candidates.append(os.path.join(root, f))
 | 
			
		||||
 | 
			
		||||
        for file_path in candidates:
 | 
			
		||||
            try:
 | 
			
		||||
                with open(file_path, "r", encoding="utf-8") as f:
 | 
			
		||||
                    docs = list(yaml.safe_load_all(f))
 | 
			
		||||
            except Exception:
 | 
			
		||||
                inc, imp = self._tolerant_scan_file(file_path, all_roles)
 | 
			
		||||
                include_roles |= inc
 | 
			
		||||
                import_roles |= imp
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            for doc in docs or []:
 | 
			
		||||
                if not isinstance(doc, list):
 | 
			
		||||
                    continue
 | 
			
		||||
                for task in doc:
 | 
			
		||||
                    if not isinstance(task, dict):
 | 
			
		||||
                        continue
 | 
			
		||||
                    if "include_role" in task:
 | 
			
		||||
                        include_roles |= self._extract_from_task(task, "include_role", all_roles)
 | 
			
		||||
                    if "import_role" in task:
 | 
			
		||||
                        import_roles |= self._extract_from_task(task, "import_role", all_roles)
 | 
			
		||||
 | 
			
		||||
        return include_roles, import_roles
 | 
			
		||||
 | 
			
		||||
    def _extract_from_task(self, task: dict, key: str, all_roles: Iterable[str]) -> Set[str]:
 | 
			
		||||
        roles: Set[str] = set()
 | 
			
		||||
        spec = task.get(key)
 | 
			
		||||
        if not isinstance(spec, dict):
 | 
			
		||||
            return roles
 | 
			
		||||
 | 
			
		||||
        name = spec.get("name")
 | 
			
		||||
        loop_val = self._collect_loop_values(task)
 | 
			
		||||
 | 
			
		||||
        if loop_val is not None:
 | 
			
		||||
            for item in self._iter_flat(loop_val):
 | 
			
		||||
                cand = self._role_from_loop_item(item, name_template=name)
 | 
			
		||||
                if cand:
 | 
			
		||||
                    roles.add(cand)
 | 
			
		||||
 | 
			
		||||
            if isinstance(name, str) and name.strip() and not self._is_pure_jinja_var(name):
 | 
			
		||||
                pattern = self._jinja_to_glob(name) if ("{{" in name and "}}" in name) else name
 | 
			
		||||
                self._match_glob_into(pattern, all_roles, roles)
 | 
			
		||||
            return roles
 | 
			
		||||
 | 
			
		||||
        if isinstance(name, str) and name.strip():
 | 
			
		||||
            if "{{" in name and "}}" in name:
 | 
			
		||||
                if self._is_pure_jinja_var(name):
 | 
			
		||||
                    return roles
 | 
			
		||||
                pattern = self._jinja_to_glob(name)
 | 
			
		||||
                self._match_glob_into(pattern, all_roles, roles)
 | 
			
		||||
            else:
 | 
			
		||||
                roles.add(name.strip())
 | 
			
		||||
 | 
			
		||||
        return roles
 | 
			
		||||
 | 
			
		||||
    def _collect_loop_values(self, task: dict):
 | 
			
		||||
        for k in ("loop", "with_items", "with_list", "with_flattened"):
 | 
			
		||||
            if k in task:
 | 
			
		||||
                return task[k]
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    def _iter_flat(self, value):
 | 
			
		||||
        if isinstance(value, list):
 | 
			
		||||
            for v in value:
 | 
			
		||||
                if isinstance(v, list):
 | 
			
		||||
                    for x in v:
 | 
			
		||||
                        yield x
 | 
			
		||||
                else:
 | 
			
		||||
                    yield v
 | 
			
		||||
 | 
			
		||||
    def _role_from_loop_item(self, item, name_template=None) -> Optional[str]:
 | 
			
		||||
        tmpl = (name_template or "").strip() if isinstance(name_template, str) else ""
 | 
			
		||||
 | 
			
		||||
        if isinstance(item, str):
 | 
			
		||||
            if tmpl in ("{{ item }}", "{{item}}") or not tmpl or "item" in tmpl:
 | 
			
		||||
                return item.strip()
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        if isinstance(item, dict):
 | 
			
		||||
            for k in ("role", "name"):
 | 
			
		||||
                v = item.get(k)
 | 
			
		||||
                if isinstance(v, str) and v.strip():
 | 
			
		||||
                    if tmpl in (f"{{{{ item.{k} }}}}", f"{{{{item.{k}}}}}") or not tmpl or "item" in tmpl:
 | 
			
		||||
                        return v.strip()
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    def _match_glob_into(self, pattern: str, all_roles: Iterable[str], out: Set[str]):
 | 
			
		||||
        if "*" in pattern or "?" in pattern or "[" in pattern:
 | 
			
		||||
            for r in all_roles:
 | 
			
		||||
                if fnmatch.fnmatch(r, pattern):
 | 
			
		||||
                    out.add(r)
 | 
			
		||||
        else:
 | 
			
		||||
            out.add(pattern)
 | 
			
		||||
 | 
			
		||||
    def test_jinja_mixed_name_glob_matching(self):
 | 
			
		||||
        """
 | 
			
		||||
        include_role:
 | 
			
		||||
        name: "prefix-{{ item }}-suffix"
 | 
			
		||||
        loop: [x, y]
 | 
			
		||||
        Existing roles: prefix-x-suffix, prefix-y-suffix, prefix-z-suffix
 | 
			
		||||
 | 
			
		||||
        Expectation:
 | 
			
		||||
        - NO raw loop items ('x', 'y') end up as roles
 | 
			
		||||
        - Glob matching resolves to all three concrete roles
 | 
			
		||||
        """
 | 
			
		||||
        make_role(self.roles_dir, "A")
 | 
			
		||||
        for rn in ["prefix-x-suffix", "prefix-y-suffix", "prefix-z-suffix"]:
 | 
			
		||||
            make_role(self.roles_dir, rn)
 | 
			
		||||
 | 
			
		||||
        write(
 | 
			
		||||
            os.path.join(self.roles_dir, "A", "tasks", "main.yml"),
 | 
			
		||||
            """
 | 
			
		||||
            - name: jinja-mixed glob
 | 
			
		||||
            include_role:
 | 
			
		||||
                name: "prefix-{{ item }}-suffix"
 | 
			
		||||
            loop:
 | 
			
		||||
                - x
 | 
			
		||||
                - y
 | 
			
		||||
            """
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        r = RoleDependencyResolver(self.roles_dir)
 | 
			
		||||
        deps = r.get_role_dependencies("A")
 | 
			
		||||
 | 
			
		||||
        # ensure no raw loop items leak into the results
 | 
			
		||||
        self.assertNotIn("x", deps)
 | 
			
		||||
        self.assertNotIn("y", deps)
 | 
			
		||||
 | 
			
		||||
        # only the resolved role names should be present
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            deps,
 | 
			
		||||
            {"prefix-x-suffix", "prefix-y-suffix", "prefix-z-suffix"},
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    # -------------------------- meta helpers --------------------------
 | 
			
		||||
 | 
			
		||||
    def _extract_meta_dependencies(self, role_path: str) -> Set[str]:
 | 
			
		||||
        deps: Set[str] = set()
 | 
			
		||||
        meta_main = os.path.join(role_path, "meta", "main.yml")
 | 
			
		||||
        if not os.path.isfile(meta_main):
 | 
			
		||||
            return deps
 | 
			
		||||
        try:
 | 
			
		||||
            with open(meta_main, "r", encoding="utf-8") as f:
 | 
			
		||||
                meta = yaml.safe_load(f) or {}
 | 
			
		||||
            raw_deps = meta.get("dependencies", [])
 | 
			
		||||
            if isinstance(raw_deps, list):
 | 
			
		||||
                for item in raw_deps:
 | 
			
		||||
                    if isinstance(item, str):
 | 
			
		||||
                        deps.add(item.strip())
 | 
			
		||||
                    elif isinstance(item, dict):
 | 
			
		||||
                        r = item.get("role")
 | 
			
		||||
                        if isinstance(r, str) and r.strip():
 | 
			
		||||
                            deps.add(r.strip())
 | 
			
		||||
        except Exception:
 | 
			
		||||
            pass
 | 
			
		||||
        return deps
 | 
			
		||||
 | 
			
		||||
    def _extract_meta_run_after(self, role_path: str) -> Set[str]:
 | 
			
		||||
        deps: Set[str] = set()
 | 
			
		||||
        meta_main = os.path.join(role_path, "meta", "main.yml")
 | 
			
		||||
        if not os.path.isfile(meta_main):
 | 
			
		||||
            return deps
 | 
			
		||||
        try:
 | 
			
		||||
            with open(meta_main, "r", encoding="utf-8") as f:
 | 
			
		||||
                meta = yaml.safe_load(f) or {}
 | 
			
		||||
            galaxy_info = meta.get("galaxy_info", {})
 | 
			
		||||
            run_after = galaxy_info.get("run_after", [])
 | 
			
		||||
            if isinstance(run_after, list):
 | 
			
		||||
                for item in run_after:
 | 
			
		||||
                    if isinstance(item, str) and item.strip():
 | 
			
		||||
                        deps.add(item.strip())
 | 
			
		||||
        except Exception:
 | 
			
		||||
            pass
 | 
			
		||||
        return deps
 | 
			
		||||
 | 
			
		||||
    # -------------------------- small utils --------------------------
 | 
			
		||||
 | 
			
		||||
    def _list_role_dirs(self, roles_dir: str) -> list[str]:
 | 
			
		||||
        return [
 | 
			
		||||
            d for d in os.listdir(roles_dir)
 | 
			
		||||
            if os.path.isdir(os.path.join(roles_dir, d))
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def _is_pure_jinja_var(cls, s: str) -> bool:
 | 
			
		||||
        return bool(cls._RE_PURE_JINJA.fullmatch(s or ""))
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def _jinja_to_glob(s: str) -> str:
 | 
			
		||||
        pattern = re.sub(r"\{\{[^}]+\}\}", "*", s or "")
 | 
			
		||||
        pattern = re.sub(r"\*{2,}", "*", pattern)
 | 
			
		||||
        return pattern.strip()
 | 
			
		||||
@@ -8,4 +8,4 @@ This role builds on `cmp-db-docker` by adding a reverse-proxy frontend for HTTP
 | 
			
		||||
  Leverages the `cmp-db-docker` role to stand up your containerized database (PostgreSQL, MariaDB, etc.) with backups and user management.
 | 
			
		||||
 | 
			
		||||
- **Reverse Proxy**  
 | 
			
		||||
  Includes the `srv-proxy-6-6-domain` role to configure a proxy (e.g. nginx) for routing HTTP(S) traffic to your database UI or management endpoint.
 | 
			
		||||
  Includes the `srv-domain-provision` role to configure a proxy (e.g. nginx) for routing HTTP(S) traffic to your database UI or management endpoint.
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
galaxy_info:
 | 
			
		||||
  author: "Kevin Veen-Birkenbach"
 | 
			
		||||
  description: >
 | 
			
		||||
    Extends cmp-db-docker by adding an HTTP reverse proxy via srv-proxy-6-6-domain.
 | 
			
		||||
    Extends cmp-db-docker by adding an HTTP reverse proxy via srv-domain-provision.
 | 
			
		||||
  company: |
 | 
			
		||||
    Kevin Veen-Birkenbach
 | 
			
		||||
    Consulting & Coaching Solutions
 | 
			
		||||
 
 | 
			
		||||
@@ -8,9 +8,9 @@
 | 
			
		||||
  include_role:
 | 
			
		||||
    name: cmp-db-docker
 | 
			
		||||
 | 
			
		||||
- name: "For '{{ application_id }}': include role srv-proxy-6-6-domain"
 | 
			
		||||
- name: "For '{{ application_id }}': include role srv-domain-provision"
 | 
			
		||||
  include_role:
 | 
			
		||||
    name: srv-proxy-6-6-domain
 | 
			
		||||
    name: srv-domain-provision
 | 
			
		||||
  vars:
 | 
			
		||||
    domain:     "{{ domains | get_domain(application_id) }}"
 | 
			
		||||
    http_port:  "{{ ports.localhost.http[application_id] }}"
 | 
			
		||||
 
 | 
			
		||||
@@ -8,4 +8,4 @@ This role combines the standard Docker Compose setup with a reverse-proxy for an
 | 
			
		||||
  Brings up containers, networks, and volumes via the `docker-compose` role.
 | 
			
		||||
 | 
			
		||||
- **Reverse Proxy**  
 | 
			
		||||
  Uses the `srv-proxy-6-6-domain` role to expose your application under a custom domain and port.
 | 
			
		||||
  Uses the `srv-domain-provision` role to expose your application under a custom domain and port.
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
galaxy_info:
 | 
			
		||||
  author: "Kevin Veen-Birkenbach"
 | 
			
		||||
  description: >
 | 
			
		||||
    Combines the docker-compose role with srv-proxy-6-6-domain to
 | 
			
		||||
    Combines the docker-compose role with srv-domain-provision to
 | 
			
		||||
    deploy applications behind a reverse proxy.
 | 
			
		||||
  company: |
 | 
			
		||||
    Kevin Veen-Birkenbach
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
# run_once_cmp_docker_proxy: deactivated
 | 
			
		||||
 | 
			
		||||
# Load the proxy first, so that openresty handlers are flushed before the main docker compose
 | 
			
		||||
- name: "For '{{ application_id }}': include role srv-proxy-6-6-domain"
 | 
			
		||||
- name: "For '{{ application_id }}': include role srv-domain-provision"
 | 
			
		||||
  include_role:
 | 
			
		||||
    name: srv-proxy-6-6-domain
 | 
			
		||||
    name: srv-domain-provision
 | 
			
		||||
  vars:
 | 
			
		||||
    domain:   "{{ domains | get_domain(application_id) }}"
 | 
			
		||||
    http_port:   "{{ ports.localhost.http[application_id] }}"
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ To offer a centralized, extensible system for managing containerized application
 | 
			
		||||
- **Reset Logic:** Cleans previous Compose project files and data when `MODE_RESET` is enabled.
 | 
			
		||||
- **Handlers for Runtime Control:** Automatically builds, sets up, or restarts containers based on handlers.
 | 
			
		||||
- **Template-ready Service Files:** Predefined service base and health check templates.
 | 
			
		||||
- **Integration Support:** Compatible with `srv-proxy-7-4-core` and other Infinito.Nexus service roles.
 | 
			
		||||
- **Integration Support:** Compatible with `srv-proxy-core` and other Infinito.Nexus service roles.
 | 
			
		||||
 | 
			
		||||
## Administration Tips
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,30 @@
 | 
			
		||||
    - docker compose restart
 | 
			
		||||
    - docker compose just up
 | 
			
		||||
 | 
			
		||||
- name: docker compose pull
 | 
			
		||||
  shell: |
 | 
			
		||||
    set -euo pipefail
 | 
			
		||||
    lock="{{ [ PATH_DOCKER_COMPOSE_PULL_LOCK_DIR | docker_compose.directories.instance ] path_join | hash('sha1') }}"
 | 
			
		||||
    if [ ! -e "$lock" ]; then
 | 
			
		||||
      mkdir -p "$(dirname "$lock")"
 | 
			
		||||
      docker compose pull
 | 
			
		||||
      : > "$lock"
 | 
			
		||||
      echo "pulled"
 | 
			
		||||
    fi
 | 
			
		||||
  args:
 | 
			
		||||
    chdir: "{{ docker_compose.directories.instance }}"
 | 
			
		||||
    executable: /bin/bash
 | 
			
		||||
  register: compose_pull
 | 
			
		||||
  changed_when: "'pulled' in compose_pull.stdout"
 | 
			
		||||
  environment:
 | 
			
		||||
    COMPOSE_HTTP_TIMEOUT: 600
 | 
			
		||||
    DOCKER_CLIENT_TIMEOUT: 600
 | 
			
		||||
  when: MODE_UPDATE | bool
 | 
			
		||||
  listen:
 | 
			
		||||
    - docker compose up
 | 
			
		||||
    - docker compose restart
 | 
			
		||||
    - docker compose just up
 | 
			
		||||
 | 
			
		||||
- name: Build docker compose 
 | 
			
		||||
  shell: |
 | 
			
		||||
    set -euo pipefail
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,8 @@
 | 
			
		||||
- name: Remove all docker compose pull locks
 | 
			
		||||
  file:
 | 
			
		||||
    path: "{{ PATH_DOCKER_COMPOSE_PULL_LOCK_DIR }}"
 | 
			
		||||
    state: absent
 | 
			
		||||
 | 
			
		||||
- name: "Load docker container role"
 | 
			
		||||
  include_role:
 | 
			
		||||
    name: docker-container
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
# Role: srv-web-7-6-composer
 | 
			
		||||
# Role: srv-composer
 | 
			
		||||
 | 
			
		||||
This Ansible role composes and orchestrates all necessary HTTPS-layer tasks and HTML-content injections for your webserver domains. It integrates two key sub-roles into a unified workflow:
 | 
			
		||||
 | 
			
		||||
1. **`sys-srv-web-inj-compose`**
 | 
			
		||||
   Injects global HTML snippets (CSS, Matomo tracking, iFrame notifier, custom JavaScript) into responses using Nginx `sub_filter`.
 | 
			
		||||
2. **`srv-web-6-6-tls-core`**
 | 
			
		||||
2. **`srv-tls-core`**
 | 
			
		||||
   Handles issuing, renewing, and managing TLS certificates via ACME/Certbot.
 | 
			
		||||
 | 
			
		||||
By combining encryption setup with content enhancements, this role streamlines domain provisioning for secure, fully-featured HTTP/HTTPS delivery.
 | 
			
		||||
@@ -16,7 +16,7 @@ By combining encryption setup with content enhancements, this role streamlines d
 | 
			
		||||
* **Content Injection**
 | 
			
		||||
  Adds global theming, analytics, and custom scripts before `</head>` and tracking noscript tags before `</body>`.
 | 
			
		||||
* **Certificate Management**
 | 
			
		||||
  Automates cert issuance and renewal via `srv-web-6-6-tls-core`.
 | 
			
		||||
  Automates cert issuance and renewal via `srv-tls-core`.
 | 
			
		||||
* **Idempotent Workflow**
 | 
			
		||||
  Ensures each component runs only once per domain.
 | 
			
		||||
* **Simplified Playbooks**
 | 
			
		||||
@@ -27,4 +27,4 @@ galaxy_info:
 | 
			
		||||
    - orchestration
 | 
			
		||||
  repository: "https://s.infinito.nexus/code"
 | 
			
		||||
  issue_tracker_url: "https://s.infinito.nexus/issues"
 | 
			
		||||
  documentation: "https://s.infinito.nexus/code/roles/srv-web-7-6-composer"
 | 
			
		||||
  documentation: "https://s.infinito.nexus/code/roles/srv-composer"
 | 
			
		||||
							
								
								
									
										9
									
								
								roles/srv-composer/tasks/main.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								roles/srv-composer/tasks/main.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
# run_once_srv_composer: deactivated
 | 
			
		||||
 | 
			
		||||
- name: "include role sys-srv-web-inj-compose for '{{ domain }}'"
 | 
			
		||||
  include_role: 
 | 
			
		||||
    name: sys-srv-web-inj-compose
 | 
			
		||||
 | 
			
		||||
- name: "include role srv-tls-core for '{{ domain }}'"
 | 
			
		||||
  include_role: 
 | 
			
		||||
    name: srv-tls-core
 | 
			
		||||
@@ -18,4 +18,4 @@ galaxy_info:
 | 
			
		||||
    - performance
 | 
			
		||||
  repository: "https://s.infinito.nexus/code"
 | 
			
		||||
  issue_tracker_url: "https://s.infinito.nexus/issues"
 | 
			
		||||
  documentation: "https://s.infinito.nexus/code/roles/srv-web-7-4-core"
 | 
			
		||||
  documentation: "https://s.infinito.nexus/code/roles/srv-core"
 | 
			
		||||
@@ -2,4 +2,4 @@
 | 
			
		||||
- block:
 | 
			
		||||
  - include_tasks: 01_core.yml
 | 
			
		||||
  - include_tasks: utils/run_once.yml
 | 
			
		||||
  when: run_once_srv_web_7_4_core is not defined
 | 
			
		||||
  when: run_once_srv_core is not defined
 | 
			
		||||
@@ -6,11 +6,11 @@ This role bootstraps **per-domain Nginx configuration**: it requests TLS certifi
 | 
			
		||||
 | 
			
		||||
## Overview
 | 
			
		||||
 | 
			
		||||
A higher-level orchestration wrapper, *srv-proxy-6-6-domain* ties together several lower-level roles:
 | 
			
		||||
A higher-level orchestration wrapper, *srv-domain-provision* ties together several lower-level roles:
 | 
			
		||||
 | 
			
		||||
1. **`sys-srv-web-inj-compose`** – applies global tweaks and includes.  
 | 
			
		||||
2. **`srv-web-6-6-tls-core`** – obtains Let’s Encrypt certificates.  
 | 
			
		||||
3. **Domain template deployment** – copies a Jinja2 vHost from *srv-proxy-7-4-core*.  
 | 
			
		||||
2. **`srv-tls-core`** – obtains Let’s Encrypt certificates.  
 | 
			
		||||
3. **Domain template deployment** – copies a Jinja2 vHost from *srv-proxy-core*.  
 | 
			
		||||
4. **`web-app-oauth2-proxy`** *(optional)* – protects the site with OAuth2.
 | 
			
		||||
 | 
			
		||||
The result is a complete, reproducible domain rollout in a single playbook task.
 | 
			
		||||
@@ -2,4 +2,4 @@
 | 
			
		||||
vhost_flavour:        "basic"               # valid: basic | ws_generic
 | 
			
		||||
 | 
			
		||||
# build the full template path from the flavour
 | 
			
		||||
vhost_template_src:   "roles/srv-proxy-7-4-core/templates/vhost/{{ vhost_flavour }}.conf.j2"
 | 
			
		||||
vhost_template_src:   "roles/srv-proxy-core/templates/vhost/{{ vhost_flavour }}.conf.j2"
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
# roles/srv-proxy-6-6-domain/tasks/02_enable_cf_dev_mode.yml
 | 
			
		||||
# roles/srv-domain-provision/tasks/02_enable_cf_dev_mode.yml
 | 
			
		||||
---
 | 
			
		||||
# Enables Cloudflare Development Mode (bypasses cache for ~3 hours).
 | 
			
		||||
# Uses the same auth token as in 01_cleanup.yml: CLOUDFLARE_API_TOKEN
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
- block:
 | 
			
		||||
  - name: Include dependency 'srv-proxy-7-4-core'
 | 
			
		||||
  - name: Include dependency 'srv-proxy-core'
 | 
			
		||||
    include_role:
 | 
			
		||||
      name: srv-proxy-7-4-core
 | 
			
		||||
    when: run_once_srv_proxy_7_4_core is not defined
 | 
			
		||||
      name: srv-proxy-core
 | 
			
		||||
    when: run_once_srv_proxy_core is not defined
 | 
			
		||||
  - include_tasks: utils/run_once.yml
 | 
			
		||||
  when: run_once_srv_proxy_6_6_domain is not defined
 | 
			
		||||
  when: run_once_srv_domain_provision is not defined
 | 
			
		||||
 | 
			
		||||
- include_tasks: "01_cloudflare.yml"
 | 
			
		||||
  when: DNS_PROVIDER == "cloudflare"
 | 
			
		||||
@@ -15,7 +15,7 @@
 | 
			
		||||
 | 
			
		||||
- name: "include role for {{ domain }} to receive certificates and do the modification routines"
 | 
			
		||||
  include_role:
 | 
			
		||||
    name: srv-web-7-6-composer
 | 
			
		||||
    name: srv-composer
 | 
			
		||||
 | 
			
		||||
- name: "Copy nginx config to {{ configuration_destination }}"
 | 
			
		||||
  template:
 | 
			
		||||
@@ -1,23 +1,23 @@
 | 
			
		||||
# Webserver HTTPS Provisioning 🚀
 | 
			
		||||
 | 
			
		||||
## Description
 | 
			
		||||
The **srv-web-7-6-https** role extends a basic Nginx installation by wiring in everything you need to serve content over HTTPS:
 | 
			
		||||
The **srv-https-stack** role extends a basic Nginx installation by wiring in everything you need to serve content over HTTPS:
 | 
			
		||||
 | 
			
		||||
1. Ensures your Nginx server is configured for SSL/TLS.
 | 
			
		||||
2. Pulls in Let’s Encrypt ACME challenge handling.
 | 
			
		||||
3. Applies global cleanup of unused domain configs.
 | 
			
		||||
 | 
			
		||||
This role is built on top of your existing `srv-web-7-4-core` role, and it automates the end-to-end process of turning HTTP sites into secure HTTPS sites.
 | 
			
		||||
This role is built on top of your existing `srv-core` role, and it automates the end-to-end process of turning HTTP sites into secure HTTPS sites.
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Overview
 | 
			
		||||
 | 
			
		||||
When you apply **srv-web-7-6-https**, it will:
 | 
			
		||||
When you apply **srv-https-stack**, it will:
 | 
			
		||||
 | 
			
		||||
1. **Include** the `srv-web-7-4-core` role to install and configure Nginx.  
 | 
			
		||||
1. **Include** the `srv-core` role to install and configure Nginx.  
 | 
			
		||||
2. **Clean up** any stale vHost files under `sys-svc-cln-domains`.  
 | 
			
		||||
3. **Deploy** the Let’s Encrypt challenge-and-redirect snippet from `srv-web-7-7-letsencrypt`.  
 | 
			
		||||
3. **Deploy** the Let’s Encrypt challenge-and-redirect snippet from `srv-letsencrypt`.  
 | 
			
		||||
4. **Reload** Nginx automatically when any template changes.
 | 
			
		||||
 | 
			
		||||
All tasks are idempotent—once your certificates are in place and your configuration is set, Ansible will skip unchanged steps on subsequent runs.
 | 
			
		||||
@@ -42,7 +42,7 @@ All tasks are idempotent—once your certificates are in place and your configur
 | 
			
		||||
 | 
			
		||||
## Requirements
 | 
			
		||||
 | 
			
		||||
- A working `srv-web-7-4-core` setup.
 | 
			
		||||
- A working `srv-core` setup.
 | 
			
		||||
- DNS managed via Cloudflare (for CAA record tasks) or equivalent ACME DNS flow.
 | 
			
		||||
- Variables:
 | 
			
		||||
  - `LETSENCRYPT_WEBROOT_PATH`  
 | 
			
		||||
@@ -3,8 +3,8 @@
 | 
			
		||||
    include_role:
 | 
			
		||||
      name: '{{ item }}'
 | 
			
		||||
    loop:
 | 
			
		||||
    - srv-web-7-4-core
 | 
			
		||||
    - srv-core
 | 
			
		||||
    - sys-svc-cln-domains
 | 
			
		||||
    - srv-web-7-7-letsencrypt
 | 
			
		||||
    - srv-letsencrypt
 | 
			
		||||
  - include_tasks: utils/run_once.yml
 | 
			
		||||
  when: run_once_srv_web_7_6_https is not defined
 | 
			
		||||
  when: run_once_srv_https_stack is not defined
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
- block:
 | 
			
		||||
  - include_tasks: 01_core.yml
 | 
			
		||||
  - include_tasks: utils/run_once.yml
 | 
			
		||||
  when: run_once_srv_web_7_7_letsencrypt is not defined
 | 
			
		||||
  when: run_once_srv_letsencrypt is not defined
 | 
			
		||||
@@ -12,4 +12,4 @@ ssl_session_tickets on;
 | 
			
		||||
add_header Strict-Transport-Security max-age=15768000;
 | 
			
		||||
ssl_stapling on;
 | 
			
		||||
ssl_stapling_verify on;
 | 
			
		||||
{% include 'roles/srv-web-7-7-letsencrypt/templates/ssl_credentials.j2' %}
 | 
			
		||||
{% include 'roles/srv-letsencrypt/templates/ssl_credentials.j2' %}
 | 
			
		||||
@@ -16,7 +16,7 @@ The goal of this role is to deliver a **hassle-free, production-ready reverse pr
 | 
			
		||||
 | 
			
		||||
## Features
 | 
			
		||||
 | 
			
		||||
- **Automatic TLS & HSTS** — integrates with the *srv-web-7-6-https* role for certificate management.  
 | 
			
		||||
- **Automatic TLS & HSTS** — integrates with the *srv-https-stack* role for certificate management.  
 | 
			
		||||
- **Flexible vHost templates** — *basic* and *ws_generic* flavours cover standard HTTP and WebSocket applications.  
 | 
			
		||||
- **Security headers** — sensible defaults plus optional X-Frame-Options / CSP based on application settings.  
 | 
			
		||||
- **WebSocket & HTTP/2 aware** — upgrades, keep-alive tuning, and gzip already configured.  
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
    include_role:
 | 
			
		||||
      name: '{{ item }}'
 | 
			
		||||
    loop:
 | 
			
		||||
    - srv-web-7-6-https
 | 
			
		||||
    - srv-web-7-4-core
 | 
			
		||||
    - srv-https-stack
 | 
			
		||||
    - srv-core
 | 
			
		||||
  - include_tasks: utils/run_once.yml
 | 
			
		||||
  when: run_once_srv_proxy_7_4_core is not defined
 | 
			
		||||
  when: run_once_srv_proxy_core is not defined
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
# Nginx Location Templates
 | 
			
		||||
 | 
			
		||||
This directory contains Jinja2 templates for different Nginx `location` blocks, each designed to proxy and optimize different types of web traffic. These templates are used by the `srv-proxy-7-4-core` role to modularize and standardize reverse proxy configuration across a wide variety of applications.
 | 
			
		||||
This directory contains Jinja2 templates for different Nginx `location` blocks, each designed to proxy and optimize different types of web traffic. These templates are used by the `srv-proxy-core` role to modularize and standardize reverse proxy configuration across a wide variety of applications.
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
@@ -16,7 +16,7 @@ location {{location}}
 | 
			
		||||
  proxy_set_header X-Forwarded-Proto $scheme;
 | 
			
		||||
  proxy_set_header X-Forwarded-Port {{ WEB_PORT }};
 | 
			
		||||
 | 
			
		||||
  {% include 'roles/srv-proxy-7-4-core/templates/headers/content_security_policy.conf.j2' %}
 | 
			
		||||
  {% include 'roles/srv-proxy-core/templates/headers/content_security_policy.conf.j2' %}
 | 
			
		||||
 | 
			
		||||
  # WebSocket specific header
 | 
			
		||||
  proxy_http_version 1.1;
 | 
			
		||||
@@ -13,7 +13,7 @@ server
 | 
			
		||||
    {{ proxy_extra_configuration }}
 | 
			
		||||
  {% endif %}
 | 
			
		||||
 | 
			
		||||
  {% include 'roles/srv-web-7-7-letsencrypt/templates/ssl_header.j2' %}
 | 
			
		||||
  {% include 'roles/srv-letsencrypt/templates/ssl_header.j2' %}
 | 
			
		||||
 | 
			
		||||
  {% if applications | get_app_conf(application_id, 'features.oauth2', False) %}
 | 
			
		||||
    {% set acl = applications | get_app_conf(application_id, 'oauth2_proxy.acl', False, {}) %}
 | 
			
		||||
@@ -22,38 +22,38 @@ server
 | 
			
		||||
      {# 1. Expose everything by default, then protect blacklisted paths #}
 | 
			
		||||
      {% set oauth2_proxy_enabled = false %}
 | 
			
		||||
      {% set location = "/" %}
 | 
			
		||||
      {% include 'roles/srv-proxy-7-4-core/templates/location/html.conf.j2' %}
 | 
			
		||||
      {% include 'roles/srv-proxy-core/templates/location/html.conf.j2' %}
 | 
			
		||||
 | 
			
		||||
      {% for loc in acl.blacklist %}
 | 
			
		||||
        {% set oauth2_proxy_enabled = true %}
 | 
			
		||||
        {% set location = loc %}
 | 
			
		||||
        {% include 'roles/srv-proxy-7-4-core/templates/location/html.conf.j2' %}
 | 
			
		||||
        {% include 'roles/srv-proxy-core/templates/location/html.conf.j2' %}
 | 
			
		||||
      {% endfor %}
 | 
			
		||||
 | 
			
		||||
    {% elif acl.whitelist is defined %}
 | 
			
		||||
      {# 2. Protect everything by default, then expose whitelisted paths #}
 | 
			
		||||
      {% set oauth2_proxy_enabled = true %}
 | 
			
		||||
      {% set location = "/" %}
 | 
			
		||||
      {% include 'roles/srv-proxy-7-4-core/templates/location/html.conf.j2' %}
 | 
			
		||||
      {% include 'roles/srv-proxy-core/templates/location/html.conf.j2' %}
 | 
			
		||||
 | 
			
		||||
      {% for loc in acl.whitelist %}
 | 
			
		||||
        {% set oauth2_proxy_enabled = false %}
 | 
			
		||||
        {% set location = loc %}
 | 
			
		||||
        {% include 'roles/srv-proxy-7-4-core/templates/location/html.conf.j2' %}
 | 
			
		||||
        {% include 'roles/srv-proxy-core/templates/location/html.conf.j2' %}
 | 
			
		||||
      {% endfor %}
 | 
			
		||||
 | 
			
		||||
    {% else %}
 | 
			
		||||
      {# 3. OAuth2 enabled but no (or empty) ACL — protect all #}
 | 
			
		||||
      {% set oauth2_proxy_enabled = true %}
 | 
			
		||||
      {% set location = "/" %}
 | 
			
		||||
      {% include 'roles/srv-proxy-7-4-core/templates/location/html.conf.j2' %}
 | 
			
		||||
      {% include 'roles/srv-proxy-core/templates/location/html.conf.j2' %}
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
  {% else %}
 | 
			
		||||
    {# 4. OAuth2 completely disabled — expose all #}
 | 
			
		||||
    {% set oauth2_proxy_enabled = false %}
 | 
			
		||||
    {% set location = "/" %}
 | 
			
		||||
    {% include 'roles/srv-proxy-7-4-core/templates/location/html.conf.j2' %}
 | 
			
		||||
    {% include 'roles/srv-proxy-core/templates/location/html.conf.j2' %}
 | 
			
		||||
  {% endif %}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -6,7 +6,7 @@ map $http_upgrade $connection_upgrade {
 | 
			
		||||
server {
 | 
			
		||||
  server_name {{ domain }};
 | 
			
		||||
 | 
			
		||||
  {% include 'roles/srv-web-7-7-letsencrypt/templates/ssl_header.j2' %}
 | 
			
		||||
  {% include 'roles/srv-letsencrypt/templates/ssl_header.j2' %}
 | 
			
		||||
 | 
			
		||||
  {% include 'roles/sys-srv-web-inj-compose/templates/server.conf.j2' %}
 | 
			
		||||
 | 
			
		||||
@@ -25,10 +25,10 @@ server {
 | 
			
		||||
 | 
			
		||||
  add_header Strict-Transport-Security "max-age=31536000";
 | 
			
		||||
 | 
			
		||||
  {% include 'roles/srv-proxy-7-4-core/templates/location/html.conf.j2' %}
 | 
			
		||||
  {% include 'roles/srv-proxy-core/templates/location/html.conf.j2' %}
 | 
			
		||||
 | 
			
		||||
  {% if location_ws is defined %}
 | 
			
		||||
    {% include 'roles/srv-proxy-7-4-core/templates/location/ws.conf.j2' %}
 | 
			
		||||
    {% include 'roles/srv-proxy-core/templates/location/ws.conf.j2' %}
 | 
			
		||||
  {% endif %}
 | 
			
		||||
 | 
			
		||||
  error_page 500 501 502 503 504 /500.html;
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
- block:
 | 
			
		||||
  - name: Include dependency 'srv-web-7-6-https'
 | 
			
		||||
  - name: Include dependency 'srv-https-stack'
 | 
			
		||||
    include_role:
 | 
			
		||||
      name: srv-web-7-6-https
 | 
			
		||||
    when: run_once_srv_web_7_6_https is not defined
 | 
			
		||||
      name: srv-https-stack
 | 
			
		||||
    when: run_once_srv_https_stack is not defined
 | 
			
		||||
  - include_tasks: utils/run_once.yml
 | 
			
		||||
  when: run_once_srv_web_6_6_tls_core is not defined
 | 
			
		||||
  when: run_once_srv_tls_core is not defined
 | 
			
		||||
 | 
			
		||||
- name: "Include flavor '{{ CERTBOT_FLAVOR }}' for '{{ domain }}'"
 | 
			
		||||
  include_tasks: "{{ role_path }}/tasks/flavors/{{ CERTBOT_FLAVOR }}.yml"
 | 
			
		||||
@@ -1,9 +0,0 @@
 | 
			
		||||
# run_once_srv_web_7_6_composer: deactivated
 | 
			
		||||
 | 
			
		||||
- name: "include role sys-srv-web-inj-compose for '{{ domain }}'"
 | 
			
		||||
  include_role: 
 | 
			
		||||
    name: sys-srv-web-inj-compose
 | 
			
		||||
 | 
			
		||||
- name: "include role srv-web-6-6-tls-core for '{{ domain }}'"
 | 
			
		||||
  include_role: 
 | 
			
		||||
    name: srv-web-6-6-tls-core
 | 
			
		||||
@@ -6,4 +6,3 @@ OnFailure={{ SYS_SERVICE_ON_FAILURE_COMPOSE }} {{ SYS_SERVICE_CLEANUP_BACKUPS_FA
 | 
			
		||||
Type=oneshot
 | 
			
		||||
ExecStartPre=/usr/bin/python {{ PATH_SYSTEM_LOCK_SCRIPT }} {{ SYS_SERVICE_GROUP_MANIPULATION | join(' ')  }} --ignore {{ SYS_SERVICE_GROUP_BACKUPS | join(' ') }} --timeout "{{ SYS_TIMEOUT_BACKUP_SERVICES }}"
 | 
			
		||||
ExecStart={{ system_service_script_exec }}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,5 +2,5 @@ server {
 | 
			
		||||
    listen {{ ports.public.ldaps['svc-db-openldap'] }}ssl;
 | 
			
		||||
    proxy_pass 127.0.0.1:{{ ports.localhost.ldap['svc-db-openldap'] }};
 | 
			
		||||
    
 | 
			
		||||
    {% include 'roles/srv-web-7-7-letsencrypt/templates/ssl_credentials.j2' %}
 | 
			
		||||
    {% include 'roles/srv-letsencrypt/templates/ssl_credentials.j2' %}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,9 +14,10 @@
 | 
			
		||||
  include_tasks: 03_reset.yml
 | 
			
		||||
  when: MODE_RESET | bool
 | 
			
		||||
 | 
			
		||||
- include_role:
 | 
			
		||||
- name: "Execute system service setup for '{{ system_service_id }}'"
 | 
			
		||||
  include_role:
 | 
			
		||||
    name: sys-service
 | 
			
		||||
  vars:
 | 
			
		||||
    system_service_copy_files:     false
 | 
			
		||||
    system_service_timer_enabled:  false
 | 
			
		||||
    system_service_timer_enabled:  true
 | 
			
		||||
    system_service_on_calendar:    "{{ SYS_SCHEDULE_BACKUP_DOCKER_TO_LOCAL }}"
 | 
			
		||||
 
 | 
			
		||||
@@ -8,3 +8,4 @@
 | 
			
		||||
  vars:
 | 
			
		||||
    system_service_on_calendar:   "{{SYS_SCHEDULE_HEALTH_BTRFS}}"
 | 
			
		||||
    system_service_timer_enabled:  true
 | 
			
		||||
    system_service_tpl_on_failure: "{{ SYS_SERVICE_ON_FAILURE_COMPOSE }}"
 | 
			
		||||
@@ -15,3 +15,4 @@
 | 
			
		||||
  vars:
 | 
			
		||||
    system_service_on_calendar:     "{{ SYS_SCHEDULE_HEALTH_CSP_CRAWLER }}"
 | 
			
		||||
    system_service_timer_enabled:   true
 | 
			
		||||
    system_service_tpl_on_failure:  "{{ SYS_SERVICE_ON_FAILURE_COMPOSE }}"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								roles/sys-ctl-hlth-disc-space/tasks/01_core.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								roles/sys-ctl-hlth-disc-space/tasks/01_core.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
- name: Include dependency 'sys-ctl-alm-compose'
 | 
			
		||||
  include_role:
 | 
			
		||||
    name: sys-ctl-alm-compose
 | 
			
		||||
  when: run_once_sys_ctl_alm_compose is not defined
 | 
			
		||||
 | 
			
		||||
- include_role:
 | 
			
		||||
    name: sys-service
 | 
			
		||||
  vars:
 | 
			
		||||
    system_service_on_calendar:    "{{ SYS_SCHEDULE_HEALTH_DISC_SPACE }}"
 | 
			
		||||
    system_service_timer_enabled:  true
 | 
			
		||||
    system_service_tpl_on_failure: "{{ SYS_SERVICE_ON_FAILURE_COMPOSE }}"
 | 
			
		||||
@@ -1,13 +1,4 @@
 | 
			
		||||
- block:
 | 
			
		||||
  - name: Include dependency 'sys-ctl-alm-compose'
 | 
			
		||||
    include_role:
 | 
			
		||||
      name: sys-ctl-alm-compose
 | 
			
		||||
    when: run_once_sys_ctl_alm_compose is not defined
 | 
			
		||||
    - include_tasks: 01_core.yml
 | 
			
		||||
    - include_tasks: utils/run_once.yml
 | 
			
		||||
  when: run_once_sys_ctl_hlth_disc_space is not defined
 | 
			
		||||
 | 
			
		||||
- include_role:
 | 
			
		||||
    name: sys-service
 | 
			
		||||
  vars:
 | 
			
		||||
    system_service_on_calendar:    "{{ SYS_SCHEDULE_HEALTH_DISC_SPACE }}"
 | 
			
		||||
    system_service_timer_enabled:  true
 | 
			
		||||
 
 | 
			
		||||
@@ -8,3 +8,4 @@
 | 
			
		||||
  vars:
 | 
			
		||||
    system_service_timer_enabled:  true
 | 
			
		||||
    system_service_on_calendar:    "{{ SYS_SCHEDULE_HEALTH_DOCKER_CONTAINER }}"
 | 
			
		||||
    system_service_tpl_on_failure: "{{ SYS_SERVICE_ON_FAILURE_COMPOSE }}"
 | 
			
		||||
 
 | 
			
		||||
@@ -8,3 +8,4 @@
 | 
			
		||||
  vars:
 | 
			
		||||
    system_service_on_calendar:     "{{SYS_SCHEDULE_HEALTH_DOCKER_VOLUMES}}"
 | 
			
		||||
    system_service_timer_enabled:   true
 | 
			
		||||
    system_service_tpl_on_failure:  "{{ SYS_SERVICE_ON_FAILURE_COMPOSE }}"
 | 
			
		||||
 
 | 
			
		||||
@@ -8,3 +8,4 @@
 | 
			
		||||
  vars:
 | 
			
		||||
    system_service_on_calendar:   "{{SYS_SCHEDULE_HEALTH_JOURNALCTL}}"
 | 
			
		||||
    system_service_timer_enabled:  true
 | 
			
		||||
    system_service_tpl_on_failure: "{{ SYS_SERVICE_ON_FAILURE_COMPOSE }}"
 | 
			
		||||
 
 | 
			
		||||
@@ -6,5 +6,6 @@
 | 
			
		||||
- include_role:
 | 
			
		||||
    name: sys-service
 | 
			
		||||
  vars:
 | 
			
		||||
    system_service_tpl_on_failure: "{{ SYS_SERVICE_ON_FAILURE_COMPOSE }}"
 | 
			
		||||
    system_service_on_calendar:    "{{ SYS_SCHEDULE_HEALTH_MSMTP }}"
 | 
			
		||||
    system_service_timer_enabled:  true
 | 
			
		||||
@@ -1,20 +1,20 @@
 | 
			
		||||
- block:
 | 
			
		||||
  - name: Include dependencies
 | 
			
		||||
- name: Include dependencies
 | 
			
		||||
  include_role:
 | 
			
		||||
    name: '{{ item }}'
 | 
			
		||||
  loop:
 | 
			
		||||
  - dev-python-pip
 | 
			
		||||
  - sys-ctl-alm-compose
 | 
			
		||||
  - include_tasks: utils/run_once.yml
 | 
			
		||||
  when: run_once_sys_ctl_hlth_webserver is not defined
 | 
			
		||||
 | 
			
		||||
- name: Install required Python modules
 | 
			
		||||
  community.general.pacman:
 | 
			
		||||
    name: python-requests
 | 
			
		||||
    state: present
 | 
			
		||||
 | 
			
		||||
- meta: flush_handlers
 | 
			
		||||
 | 
			
		||||
- include_role:
 | 
			
		||||
    name: sys-service
 | 
			
		||||
  vars:
 | 
			
		||||
    system_service_on_calendar: "{{SYS_SCHEDULE_HEALTH_NGINX}}"
 | 
			
		||||
    system_service_on_calendar: "{{ SYS_SCHEDULE_HEALTH_NGINX }}"
 | 
			
		||||
    system_service_timer_enabled:  true
 | 
			
		||||
    system_service_tpl_on_failure: "{{ SYS_SERVICE_ON_FAILURE_COMPOSE }}"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
- name: Include dependency 'sys-ctl-alm-compose'
 | 
			
		||||
  include_role:
 | 
			
		||||
    name: sys-ctl-alm-compose
 | 
			
		||||
  vars:
 | 
			
		||||
    flush_handlers: true
 | 
			
		||||
  when: run_once_sys_ctl_alm_compose is not defined
 | 
			
		||||
 | 
			
		||||
- include_role:
 | 
			
		||||
@@ -10,3 +12,4 @@
 | 
			
		||||
    system_service_on_calendar:   "{{ SYS_SCHEDULE_MAINTANANCE_LETSENCRYPT_DEPLOY }}"
 | 
			
		||||
    persistent:                    "true"
 | 
			
		||||
    system_service_timer_enabled:  true
 | 
			
		||||
    system_service_tpl_on_failure: "{{ SYS_SERVICE_ON_FAILURE_COMPOSE }}"
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
    name: '{{ item }}'
 | 
			
		||||
  loop:
 | 
			
		||||
  - sys-svc-certbot
 | 
			
		||||
  - srv-web-7-4-core
 | 
			
		||||
  - srv-core
 | 
			
		||||
  - sys-ctl-alm-compose
 | 
			
		||||
 | 
			
		||||
- name: install certbot
 | 
			
		||||
@@ -19,3 +19,4 @@
 | 
			
		||||
    system_service_on_calendar:     "{{ SYS_SCHEDULE_MAINTANANCE_LETSENCRYPT_RENEW }}"
 | 
			
		||||
    persistent:                     true
 | 
			
		||||
    system_service_timer_enabled:   true
 | 
			
		||||
    system_service_tpl_on_failure:  "{{ SYS_SERVICE_ON_FAILURE_COMPOSE }}"
 | 
			
		||||
 
 | 
			
		||||
@@ -15,3 +15,4 @@
 | 
			
		||||
    system_service_copy_files:    false
 | 
			
		||||
    system_service_on_calendar:   "{{SYS_SCHEDULE_REPAIR_BTRFS_AUTO_BALANCER}}"
 | 
			
		||||
    system_service_timer_enabled:  true
 | 
			
		||||
    system_service_tpl_on_failure: "{{ SYS_SERVICE_ON_FAILURE_COMPOSE }}"
 | 
			
		||||
 
 | 
			
		||||
@@ -8,3 +8,4 @@
 | 
			
		||||
  vars:
 | 
			
		||||
    system_service_on_calendar:   "{{SYS_SCHEDULE_REPAIR_DOCKER_SOFT}}"
 | 
			
		||||
    system_service_timer_enabled:  true
 | 
			
		||||
    system_service_tpl_on_failure: "{{ SYS_SERVICE_ON_FAILURE_COMPOSE }}"
 | 
			
		||||
 
 | 
			
		||||
@@ -7,8 +7,6 @@
 | 
			
		||||
  block:
 | 
			
		||||
    - name: "Load base routine for '{{ system_service_id }}'"
 | 
			
		||||
      include_tasks: 03_base.yml
 | 
			
		||||
  - include_tasks: utils/run_once.yml
 | 
			
		||||
    vars:
 | 
			
		||||
      # Necessary to flush after every service which uses an 'system_service_id' otherwise wrong one will be used
 | 
			
		||||
      flush_handlers: true
 | 
			
		||||
    - name: "Flush system handlers for '{{ system_service_id }}'"
 | 
			
		||||
      meta: flush_handlers
 | 
			
		||||
  when: system_service_id is defined
 | 
			
		||||
@@ -3,10 +3,10 @@
 | 
			
		||||
    inj_enabled: "{{ applications | inj_enabled(application_id, SRV_WEB_INJ_COMP_FEATURES_ALL) }}"
 | 
			
		||||
 | 
			
		||||
- block:
 | 
			
		||||
  - name: Include dependency 'srv-web-7-4-core'
 | 
			
		||||
  - name: Include dependency 'srv-core'
 | 
			
		||||
    include_role:
 | 
			
		||||
      name: srv-web-7-4-core
 | 
			
		||||
    when: run_once_srv_web_7_4_core is not defined
 | 
			
		||||
      name: srv-core
 | 
			
		||||
    when: run_once_srv_core is not defined
 | 
			
		||||
  - include_tasks: utils/run_once.yml
 | 
			
		||||
  when: run_once_sys_srv_web_inj_compose is not defined
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
- name: Include dependency 'srv-web-7-4-core'
 | 
			
		||||
- name: Include dependency 'srv-core'
 | 
			
		||||
  include_role:
 | 
			
		||||
    name: srv-web-7-4-core
 | 
			
		||||
  when: run_once_srv_web_7_4_core is not defined
 | 
			
		||||
    name: srv-core
 | 
			
		||||
  when: run_once_srv_core is not defined
 | 
			
		||||
 | 
			
		||||
- name: Generate color palette with colorscheme-generator
 | 
			
		||||
  set_fact:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
- block:
 | 
			
		||||
  - name: Include dependency 'srv-web-7-4-core'
 | 
			
		||||
  - name: Include dependency 'srv-core'
 | 
			
		||||
    include_role:
 | 
			
		||||
      name: srv-web-7-4-core
 | 
			
		||||
    when: run_once_srv_web_7_4_core is not defined
 | 
			
		||||
      name: srv-core
 | 
			
		||||
    when: run_once_srv_core is not defined
 | 
			
		||||
  - include_tasks: 01_deploy.yml
 | 
			
		||||
  - include_tasks: utils/run_once.yml
 | 
			
		||||
  when: run_once_sys_srv_web_inj_desktop is not defined
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
- block:
 | 
			
		||||
 | 
			
		||||
  - name: Include dependency 'srv-web-7-4-core'
 | 
			
		||||
  - name: Include dependency 'srv-core'
 | 
			
		||||
    include_role:
 | 
			
		||||
      name: srv-web-7-4-core
 | 
			
		||||
    when: run_once_srv_web_7_4_core is not defined
 | 
			
		||||
      name: srv-core
 | 
			
		||||
    when: run_once_srv_core is not defined
 | 
			
		||||
  - include_tasks: utils/run_once.yml
 | 
			
		||||
  when: run_once_sys_srv_web_inj_javascript is not defined
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
- name: Include dependency 'srv-web-7-4-core'
 | 
			
		||||
- name: Include dependency 'srv-core'
 | 
			
		||||
  include_role:
 | 
			
		||||
    name: srv-web-7-4-core
 | 
			
		||||
    name: srv-core
 | 
			
		||||
  when: 
 | 
			
		||||
    - run_once_srv_web_7_4_core is not defined
 | 
			
		||||
    - run_once_srv_core is not defined
 | 
			
		||||
  
 | 
			
		||||
- name: "deploy the logout.js"
 | 
			
		||||
  include_tasks: "02_deploy.yml"
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
- block:
 | 
			
		||||
  - name: Include dependency 'srv-web-7-4-core'
 | 
			
		||||
  - name: Include dependency 'srv-core'
 | 
			
		||||
    include_role:
 | 
			
		||||
      name: srv-web-7-4-core
 | 
			
		||||
    when: run_once_srv_web_7_4_core is not defined
 | 
			
		||||
      name: srv-core
 | 
			
		||||
    when: run_once_srv_core is not defined
 | 
			
		||||
  - include_tasks: utils/run_once.yml
 | 
			
		||||
  when: run_once_sys_srv_web_inj_matomo is not defined
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
    include_role:
 | 
			
		||||
      name: '{{ item }}'
 | 
			
		||||
    loop:
 | 
			
		||||
    - srv-web-7-4-core
 | 
			
		||||
    - srv-core
 | 
			
		||||
 | 
			
		||||
  - name: Include task to remove deprecated nginx configs
 | 
			
		||||
    include_tasks: remove_deprecated_nginx_configs.yml
 | 
			
		||||
 
 | 
			
		||||
@@ -160,23 +160,6 @@ def upgrade_listmonk():
 | 
			
		||||
    run_command('echo "y" | docker compose run -T application ./listmonk --upgrade')
 | 
			
		||||
    print("Upgrade complete.")
 | 
			
		||||
 | 
			
		||||
def update_nextcloud():
 | 
			
		||||
    """
 | 
			
		||||
    Performs the necessary Nextcloud update procedures, including maintenance and app updates.
 | 
			
		||||
    """
 | 
			
		||||
    print("Start Nextcloud upgrade procedure.")
 | 
			
		||||
    update_procedure("docker-compose exec -T -u www-data application /var/www/html/occ upgrade")
 | 
			
		||||
    print("Start Nextcloud repairing procedure.")
 | 
			
		||||
    update_procedure("docker-compose exec -T -u www-data application /var/www/html/occ maintenance:repair --include-expensive")
 | 
			
		||||
    print("Start Nextcloud update procedure.")
 | 
			
		||||
    update_procedure("docker-compose exec -T -u www-data application /var/www/html/occ app:update --all")
 | 
			
		||||
    print("Start Nextcloud add-missing procedure.")
 | 
			
		||||
    update_procedure("docker-compose exec -T -u www-data application /var/www/html/occ db:add-missing-columns")
 | 
			
		||||
    update_procedure("docker-compose exec -T -u www-data application /var/www/html/occ db:add-missing-indices")
 | 
			
		||||
    update_procedure("docker-compose exec -T -u www-data application /var/www/html/occ db:add-missing-primary-keys")
 | 
			
		||||
    print("Deactivate Maintanance Mode")
 | 
			
		||||
    update_procedure("docker-compose exec -T -u www-data application /var/www/html/occ maintenance:mode --off")
 | 
			
		||||
 | 
			
		||||
def update_procedure(command):
 | 
			
		||||
    """
 | 
			
		||||
    Attempts to execute a command up to a maximum number of retries.
 | 
			
		||||
@@ -239,8 +222,6 @@ if __name__ == "__main__":
 | 
			
		||||
                    upgrade_listmonk()
 | 
			
		||||
                elif os.path.basename(dir_path) == "mastodon":
 | 
			
		||||
                    update_mastodon()
 | 
			
		||||
                elif os.path.basename(dir_path) == "nextcloud":
 | 
			
		||||
                    update_nextcloud()
 | 
			
		||||
                    
 | 
			
		||||
                # @todo implement dedicated procedure for bluesky
 | 
			
		||||
                # @todo implement dedicated procedure for taiga
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
---
 | 
			
		||||
- name: "For '{{ application_id }}': include role to receive certs & do modification routines"
 | 
			
		||||
  include_role:
 | 
			
		||||
    name: srv-web-7-6-composer
 | 
			
		||||
    name: srv-composer
 | 
			
		||||
  vars:
 | 
			
		||||
    domain: "{{ item }}"
 | 
			
		||||
    http_port:   "{{ ports.localhost.http[application_id] }}"
 | 
			
		||||
@@ -17,7 +17,7 @@
 | 
			
		||||
    
 | 
			
		||||
- name: "For '{{ application_id }}': configure {{ domains | get_domain(application_id) }}.conf"
 | 
			
		||||
  template: 
 | 
			
		||||
    src: roles/srv-proxy-7-4-core/templates/vhost/basic.conf.j2 
 | 
			
		||||
    src: roles/srv-proxy-core/templates/vhost/basic.conf.j2 
 | 
			
		||||
    dest: "{{ NGINX.DIRECTORIES.HTTP.SERVERS }}{{ domains | get_domain(application_id) }}.conf"
 | 
			
		||||
  notify: restart openresty
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,7 @@ By default, BigBlueButton is deployed with best-practice hardening, modular secr
 | 
			
		||||
## System Requirements
 | 
			
		||||
 | 
			
		||||
- Arch Linux with Docker, Compose, and Nginx roles pre-installed
 | 
			
		||||
- DNS and reverse proxy configuration using `srv-proxy-7-4-core`
 | 
			
		||||
- DNS and reverse proxy configuration using `srv-proxy-core`
 | 
			
		||||
- Functional email system for Greenlight SMTP
 | 
			
		||||
 | 
			
		||||
## Important Resources
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
  set_fact:
 | 
			
		||||
    proxy_extra_configuration: >-
 | 
			
		||||
      {{ lookup('ansible.builtin.template',
 | 
			
		||||
                playbook_dir ~ '/roles/srv-proxy-7-4-core/templates/location/html.conf.j2') | trim }}
 | 
			
		||||
                playbook_dir ~ '/roles/srv-proxy-core/templates/location/html.conf.j2') | trim }}
 | 
			
		||||
  vars:
 | 
			
		||||
    location: '^~ /html5client'
 | 
			
		||||
    oauth2_proxy_enabled: false
 | 
			
		||||
 
 | 
			
		||||
@@ -2,9 +2,9 @@
 | 
			
		||||
  include_role: 
 | 
			
		||||
    name: docker-compose
 | 
			
		||||
 | 
			
		||||
- name: "include role srv-proxy-6-6-domain for {{ application_id }}"
 | 
			
		||||
- name: "include role srv-domain-provision for {{ application_id }}"
 | 
			
		||||
  include_role:
 | 
			
		||||
    name: srv-proxy-6-6-domain
 | 
			
		||||
    name: srv-domain-provision
 | 
			
		||||
  vars:
 | 
			
		||||
    domain: "{{ item.domain }}"
 | 
			
		||||
    http_port: "{{ item.http_port }}"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,2 +0,0 @@
 | 
			
		||||
application_id:     "web-app-coturn"
 | 
			
		||||
container_port:     3000
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
- name: "include role srv-proxy-6-6-domain for {{ application_id }}"
 | 
			
		||||
- name: "include role srv-domain-provision for {{ application_id }}"
 | 
			
		||||
  include_role:
 | 
			
		||||
    name: srv-proxy-6-6-domain
 | 
			
		||||
    name: srv-domain-provision
 | 
			
		||||
  vars:
 | 
			
		||||
    domain:   "{{ domains | get_domain(application_id) }}"
 | 
			
		||||
    http_port:   "{{ ports.localhost.http[application_id] }}"
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user