From a69b2c9cb2d1bf7bcc9059a36b92726a95bf6ef1 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Wed, 9 Jul 2025 06:47:10 +0200 Subject: [PATCH] Solved run_after dependency bug --- Makefile | 6 +- cli/generate_playbook.py | 187 +++++---- tasks/utils/web-app-roles.yml | 372 +++++++++--------- .../integration/test_circular_dependencies.py | 36 ++ .../integration/test_run_after_references.py | 49 +++ tests/integration/test_self_dependency.py | 35 ++ 6 files changed, 428 insertions(+), 257 deletions(-) create mode 100644 tests/integration/test_circular_dependencies.py create mode 100644 tests/integration/test_run_after_references.py create mode 100644 tests/integration/test_self_dependency.py diff --git a/Makefile b/Makefile index 9f01d45b..ffd879c6 100644 --- a/Makefile +++ b/Makefile @@ -7,8 +7,8 @@ INCLUDES_OUT := ./tasks/utils/web-app-roles.yml INCLUDES_SCRIPT := ./cli/generate_playbook.py EXTRA_USERS := $(shell \ - find $(ROLES_DIR) -maxdepth 1 -type d -name 'docker*' -printf '%f\n' \ - | sed -E 's/^docker[_-]?//' \ + find $(ROLES_DIR) -maxdepth 1 -type d -name 'web-app*' -printf '%f\n' \ + | sed -E 's/^web-app[_-]?//' \ | grep -E -x '[a-z0-9]+' \ | paste -sd, - \ ) @@ -24,7 +24,7 @@ build: @echo "🔧 Generating users defaults → $(USERS_OUT) from roles in $(ROLES_DIR)…" @echo "🔧 Generating Docker role includes → $(INCLUDES_OUT)…" @mkdir -p $(dir $(INCLUDES_OUT)) - python3 $(INCLUDES_SCRIPT) $(ROLES_DIR) -o $(INCLUDES_OUT) -p web-app- + python3 $(INCLUDES_SCRIPT) $(ROLES_DIR) -o $(INCLUDES_OUT) -p web-app- -p svc-openldap -p svc-rdbms-postgres -p svc-rdbms-mariadb @echo "✅ Docker role includes written to $(INCLUDES_OUT)" install: build diff --git a/cli/generate_playbook.py b/cli/generate_playbook.py index 0e6b4976..1e9da5c8 100644 --- a/cli/generate_playbook.py +++ b/cli/generate_playbook.py @@ -1,13 +1,21 @@ +#!/usr/bin/env python3 + import os +import sys import yaml import argparse from collections import defaultdict, deque -def find_roles(roles_dir, prefix=None): - """Find all roles in the given directory.""" +def find_roles(roles_dir, prefixes=None): + """ + Find all roles in the given directory whose names start with + any of the provided prefixes. If prefixes is empty or None, + include all roles. + """ for entry in os.listdir(roles_dir): - if prefix and not entry.startswith(prefix): - continue + if prefixes: + if not any(entry.startswith(pref) for pref in prefixes): + continue path = os.path.join(roles_dir, entry) meta_file = os.path.join(path, 'meta', 'main.yml') if os.path.isdir(path) and os.path.isfile(meta_file): @@ -28,16 +36,21 @@ def load_application_id(role_path): return data.get('application_id') return None -def build_dependency_graph(roles_dir, prefix=None): - """Build a dependency graph where each role points to the roles it depends on.""" +def build_dependency_graph(roles_dir, prefixes=None): + """ + Build a dependency graph where each key is a role name and + its value is a list of roles that depend on it. + Also return in_degree counts and the roles metadata map. + """ graph = defaultdict(list) in_degree = defaultdict(int) roles = {} - for role_path, meta_file in find_roles(roles_dir, prefix): + for role_path, meta_file in find_roles(roles_dir, prefixes): run_after = load_run_after(meta_file) application_id = load_application_id(role_path) role_name = os.path.basename(role_path) + roles[role_name] = { 'role_name': role_name, 'run_after': run_after, @@ -45,37 +58,87 @@ def build_dependency_graph(roles_dir, prefix=None): 'path': role_path } - # If the role has dependencies, build the graph for dependency in run_after: graph[dependency].append(role_name) in_degree[role_name] += 1 - # Ensure roles with no dependencies have an in-degree of 0 if role_name not in in_degree: in_degree[role_name] = 0 return graph, in_degree, roles -def topological_sort(graph, in_degree): - """Perform topological sort on the dependency graph.""" - # Queue for roles with no incoming dependencies (in_degree == 0) - queue = deque([role for role, degree in in_degree.items() if degree == 0]) +def find_cycle(roles): + """ + Detect a cycle in the run_after relations: + roles: dict mapping role_name -> { 'run_after': [...], ... } + Returns a list of role_names forming the cycle (with the start repeated at end), or None. + """ + visited = set() + stack = set() + + def dfs(node, path): + visited.add(node) + stack.add(node) + path.append(node) + for dep in roles.get(node, {}).get('run_after', []): + if dep not in visited: + res = dfs(dep, path) + if res: + return res + elif dep in stack: + idx = path.index(dep) + return path[idx:] + [dep] + stack.remove(node) + path.pop() + return None + + for role in roles: + if role not in visited: + cycle = dfs(role, []) + if cycle: + return cycle + return None + +def topological_sort(graph, in_degree, roles=None): + """ + Perform topological sort on the dependency graph. + If `roles` is provided, on error it will include detailed debug info. + """ + queue = deque([r for r, d in in_degree.items() if d == 0]) sorted_roles = [] + local_in = dict(in_degree) while queue: role = queue.popleft() sorted_roles.append(role) - - # Reduce in-degree for roles dependent on the current role - for neighbor in graph[role]: - in_degree[neighbor] -= 1 - if in_degree[neighbor] == 0: - queue.append(neighbor) + for nbr in graph.get(role, []): + local_in[nbr] -= 1 + if local_in[nbr] == 0: + queue.append(nbr) if len(sorted_roles) != len(in_degree): - # If the number of sorted roles doesn't match the number of roles, - # there was a cycle in the graph (not all roles could be sorted) - raise Exception("Circular dependency detected among the roles!") + cycle = find_cycle(roles or {}) + if roles is not None: + if cycle: + header = f"Circular dependency detected: {' -> '.join(cycle)}" + else: + header = "Circular dependency detected among the roles!" + + unsorted = [r for r in in_degree if r not in sorted_roles] + detail_lines = ["Unsorted roles and their dependencies:"] + for r in unsorted: + deps = roles.get(r, {}).get('run_after', []) + detail_lines.append(f" - {r} depends on {deps!r}") + + detail_lines.append("Full dependency graph:") + detail_lines.append(f" {dict(graph)!r}") + + raise Exception("\n".join([header] + detail_lines)) + else: + if cycle: + raise Exception(f"Circular dependency detected: {' -> '.join(cycle)}") + else: + raise Exception("Circular dependency detected among the roles!") return sorted_roles @@ -83,48 +146,38 @@ def print_dependency_tree(graph): """Print the dependency tree visually on the console.""" def print_node(role, indent=0): print(" " * indent + role) - for dependency in graph[role]: - print_node(dependency, indent + 1) + for dep in graph.get(role, []): + print_node(dep, indent + 1) - # Print the tree starting from roles with no dependencies all_roles = set(graph.keys()) - dependent_roles = {role for dependencies in graph.values() for role in dependencies} - root_roles = all_roles - dependent_roles + dependent = {r for deps in graph.values() for r in deps} + roots = all_roles - dependent - printed_roles = [] + for root in roots: + print_node(root) - def collect_roles(role, indent=0): - printed_roles.append(role) - for dependency in graph[role]: - collect_roles(dependency, indent + 1) - - for root in root_roles: - collect_roles(root) - - return printed_roles - -def generate_playbook_entries(roles_dir, prefix=None): - """Generate playbook entries based on the sorted order.""" - graph, in_degree, roles = build_dependency_graph(roles_dir, prefix) - - # Detect cycles and get correct topological order - sorted_role_names = topological_sort(graph, in_degree) +def generate_playbook_entries(roles_dir, prefixes=None): + """ + Generate playbook entries based on the sorted order. + Raises a ValueError if application_id is missing. + """ + graph, in_degree, roles = build_dependency_graph(roles_dir, prefixes) + sorted_names = topological_sort(graph, in_degree, roles) entries = [] - for role_name in sorted_role_names: + for role_name in sorted_names: role = roles[role_name] - # --- new validation block --- if role.get('application_id') is None: - raise ValueError(f"Role '{role_name}' is missing an application_id") - # ---------------------------- + vars_file = os.path.join(role['path'], 'vars', 'main.yml') + raise ValueError(f"'application_id' missing in {vars_file}") app_id = role['application_id'] entries.append( f"- name: setup {app_id}\n" f" when: ('{app_id}' | application_allowed(group_names, allowed_applications))\n" f" include_role:\n" - f" name: {role['role_name']}\n" + f" name: {role_name}\n" ) entries.append( f"- name: flush handlers after {app_id}\n" @@ -137,32 +190,30 @@ def main(): parser = argparse.ArgumentParser( description='Generate an Ansible playbook include file from Docker roles, sorted by run_after order.' ) - parser.add_argument( - 'roles_dir', - help='Path to directory containing role folders' - ) + parser.add_argument('roles_dir', help='Path to directory containing role folders') parser.add_argument( '-p', '--prefix', - help='Only include roles whose names start with this prefix (e.g. web-app-, desk-)', - default=None + action='append', + help='Only include roles whose names start with any of these prefixes; can be specified multiple times' ) - parser.add_argument( - '-o', '--output', - help='Output file path (default: stdout)', - default=None - ) - parser.add_argument( - '-t', '--tree', - action='store_true', - help='Display the dependency tree of roles visually' - ) - args = parser.parse_args() + parser.add_argument('-o', '--output', default=None, + help='Output file path (default: stdout)') + parser.add_argument('-t', '--tree', action='store_true', + help='Display the dependency tree of roles and exit') - # Generate and output the playbook entries - entries = generate_playbook_entries(args.roles_dir, args.prefix) + args = parser.parse_args() + prefixes = args.prefix or [] + + if args.tree: + graph, _, _ = build_dependency_graph(args.roles_dir, prefixes) + print_dependency_tree(graph) + sys.exit(0) + + entries = generate_playbook_entries(args.roles_dir, prefixes) output = ''.join(entries) if args.output: + os.makedirs(os.path.dirname(args.output), exist_ok=True) with open(args.output, 'w') as f: f.write(output) print(f"Playbook entries written to {args.output}") diff --git a/tasks/utils/web-app-roles.yml b/tasks/utils/web-app-roles.yml index 53f5473a..56cc068b 100644 --- a/tasks/utils/web-app-roles.yml +++ b/tasks/utils/web-app-roles.yml @@ -1,32 +1,8 @@ -- name: setup presentation - when: ('presentation' | application_allowed(group_names, allowed_applications)) +- name: setup roulette-wheel + when: ('roulette-wheel' | application_allowed(group_names, allowed_applications)) include_role: - name: web-app-presentation -- name: flush handlers after presentation - meta: flush_handlers -- name: setup matrix-deprecated - when: ('matrix-deprecated' | application_allowed(group_names, allowed_applications)) - include_role: - name: web-app-matrix-deprecated -- name: flush handlers after matrix-deprecated - meta: flush_handlers -- name: setup postgres - when: ('postgres' | application_allowed(group_names, allowed_applications)) - include_role: - name: svc-rdbms-postgres -- name: flush handlers after postgres - meta: flush_handlers -- name: setup syncope - when: ('syncope' | application_allowed(group_names, allowed_applications)) - include_role: - name: web-app-syncope -- name: flush handlers after syncope - meta: flush_handlers -- name: setup elk - when: ('elk' | application_allowed(group_names, allowed_applications)) - include_role: - name: web-app-elk -- name: flush handlers after elk + name: web-app-roulette-wheel +- name: flush handlers after roulette-wheel meta: flush_handlers - name: setup ldap when: ('ldap' | application_allowed(group_names, allowed_applications)) @@ -34,17 +10,11 @@ name: svc-openldap - name: flush handlers after ldap meta: flush_handlers -- name: setup None - when: ('None' | application_allowed(group_names, allowed_applications)) +- name: setup simpleicons + when: ('simpleicons' | application_allowed(group_names, allowed_applications)) include_role: - name: docker-compose -- name: flush handlers after None - meta: flush_handlers -- name: setup collabora - when: ('collabora' | application_allowed(group_names, allowed_applications)) - include_role: - name: web-app-collabora -- name: flush handlers after collabora + name: web-app-simpleicons +- name: flush handlers after simpleicons meta: flush_handlers - name: setup sphinx when: ('sphinx' | application_allowed(group_names, allowed_applications)) @@ -52,29 +22,59 @@ name: web-app-sphinx - name: flush handlers after sphinx meta: flush_handlers +- name: setup presentation + when: ('presentation' | application_allowed(group_names, allowed_applications)) + include_role: + name: web-app-presentation +- name: flush handlers after presentation + meta: flush_handlers +- name: setup libretranslate + when: ('libretranslate' | application_allowed(group_names, allowed_applications)) + include_role: + name: web-app-libretranslate +- name: flush handlers after libretranslate + meta: flush_handlers +- name: setup postgres + when: ('postgres' | application_allowed(group_names, allowed_applications)) + include_role: + name: svc-rdbms-postgres +- name: flush handlers after postgres + meta: flush_handlers +- name: setup matrix-deprecated + when: ('matrix-deprecated' | application_allowed(group_names, allowed_applications)) + include_role: + name: web-app-matrix-deprecated +- name: flush handlers after matrix-deprecated + meta: flush_handlers +- name: setup syncope + when: ('syncope' | application_allowed(group_names, allowed_applications)) + include_role: + name: web-app-syncope +- name: flush handlers after syncope + meta: flush_handlers +- name: setup pretix + when: ('pretix' | application_allowed(group_names, allowed_applications)) + include_role: + name: web-app-pretix +- name: flush handlers after pretix + meta: flush_handlers - name: setup mariadb when: ('mariadb' | application_allowed(group_names, allowed_applications)) include_role: name: svc-rdbms-mariadb - name: flush handlers after mariadb meta: flush_handlers -- name: setup simpleicons - when: ('simpleicons' | application_allowed(group_names, allowed_applications)) +- name: setup elk + when: ('elk' | application_allowed(group_names, allowed_applications)) include_role: - name: web-app-simpleicons -- name: flush handlers after simpleicons + name: web-app-elk +- name: flush handlers after elk meta: flush_handlers -- name: setup None - when: ('None' | application_allowed(group_names, allowed_applications)) +- name: setup collabora + when: ('collabora' | application_allowed(group_names, allowed_applications)) include_role: - name: svc-rdbms-central -- name: flush handlers after None - meta: flush_handlers -- name: setup roulette-wheel - when: ('roulette-wheel' | application_allowed(group_names, allowed_applications)) - include_role: - name: web-app-roulette-wheel -- name: flush handlers after roulette-wheel + name: web-app-collabora +- name: flush handlers after collabora meta: flush_handlers - name: setup jenkins when: ('jenkins' | application_allowed(group_names, allowed_applications)) @@ -82,29 +82,35 @@ name: web-app-jenkins - name: flush handlers after jenkins meta: flush_handlers -- name: setup matomo - when: ('matomo' | application_allowed(group_names, allowed_applications)) - include_role: - name: web-app-matomo -- name: flush handlers after matomo - meta: flush_handlers - name: setup portfolio when: ('portfolio' | application_allowed(group_names, allowed_applications)) include_role: name: web-app-portfolio - name: flush handlers after portfolio meta: flush_handlers +- name: setup matomo + when: ('matomo' | application_allowed(group_names, allowed_applications)) + include_role: + name: web-app-matomo +- name: flush handlers after matomo + meta: flush_handlers - name: setup keycloak when: ('keycloak' | application_allowed(group_names, allowed_applications)) include_role: name: web-app-keycloak - name: flush handlers after keycloak meta: flush_handlers -- name: setup yourls - when: ('yourls' | application_allowed(group_names, allowed_applications)) +- name: setup mailu + when: ('mailu' | application_allowed(group_names, allowed_applications)) include_role: - name: web-app-yourls -- name: flush handlers after yourls + name: web-app-mailu +- name: flush handlers after mailu + meta: flush_handlers +- name: setup taiga + when: ('taiga' | application_allowed(group_names, allowed_applications)) + include_role: + name: web-app-taiga +- name: flush handlers after taiga meta: flush_handlers - name: setup wordpress when: ('wordpress' | application_allowed(group_names, allowed_applications)) @@ -112,11 +118,17 @@ name: web-app-wordpress - name: flush handlers after wordpress meta: flush_handlers -- name: setup pixelfed - when: ('pixelfed' | application_allowed(group_names, allowed_applications)) +- name: setup pgadmin + when: ('pgadmin' | application_allowed(group_names, allowed_applications)) include_role: - name: web-app-pixelfed -- name: flush handlers after pixelfed + name: web-app-pgadmin +- name: flush handlers after pgadmin + meta: flush_handlers +- name: setup lam + when: ('lam' | application_allowed(group_names, allowed_applications)) + include_role: + name: web-app-lam +- name: flush handlers after lam meta: flush_handlers - name: setup peertube when: ('peertube' | application_allowed(group_names, allowed_applications)) @@ -124,6 +136,48 @@ name: web-app-peertube - name: flush handlers after peertube meta: flush_handlers +- name: setup yourls + when: ('yourls' | application_allowed(group_names, allowed_applications)) + include_role: + name: web-app-yourls +- name: flush handlers after yourls + meta: flush_handlers +- name: setup phpldapadmin + when: ('phpldapadmin' | application_allowed(group_names, allowed_applications)) + include_role: + name: web-app-phpldapadmin +- name: flush handlers after phpldapadmin + meta: flush_handlers +- name: setup mastodon + when: ('mastodon' | application_allowed(group_names, allowed_applications)) + include_role: + name: web-app-mastodon +- name: flush handlers after mastodon + meta: flush_handlers +- name: setup friendica + when: ('friendica' | application_allowed(group_names, allowed_applications)) + include_role: + name: web-app-friendica +- name: flush handlers after friendica + meta: flush_handlers +- name: setup pixelfed + when: ('pixelfed' | application_allowed(group_names, allowed_applications)) + include_role: + name: web-app-pixelfed +- name: flush handlers after pixelfed + meta: flush_handlers +- name: setup bigbluebutton + when: ('bigbluebutton' | application_allowed(group_names, allowed_applications)) + include_role: + name: web-app-bigbluebutton +- name: flush handlers after bigbluebutton + meta: flush_handlers +- name: setup moodle + when: ('moodle' | application_allowed(group_names, allowed_applications)) + include_role: + name: web-app-moodle +- name: flush handlers after moodle + meta: flush_handlers - name: setup phpmyadmin when: ('phpmyadmin' | application_allowed(group_names, allowed_applications)) include_role: @@ -136,71 +190,83 @@ name: web-app-openproject - name: flush handlers after openproject meta: flush_handlers -- name: setup phpldapadmin - when: ('phpldapadmin' | application_allowed(group_names, allowed_applications)) +- name: setup mobilizon + when: ('mobilizon' | application_allowed(group_names, allowed_applications)) include_role: - name: web-app-phpldapadmin -- name: flush handlers after phpldapadmin + name: web-app-mobilizon +- name: flush handlers after mobilizon meta: flush_handlers -- name: setup friendica - when: ('friendica' | application_allowed(group_names, allowed_applications)) +- name: setup funkwhale + when: ('funkwhale' | application_allowed(group_names, allowed_applications)) include_role: - name: web-app-friendica -- name: flush handlers after friendica + name: web-app-funkwhale +- name: flush handlers after funkwhale meta: flush_handlers -- name: setup taiga - when: ('taiga' | application_allowed(group_names, allowed_applications)) +- name: setup matrix + when: ('matrix' | application_allowed(group_names, allowed_applications)) include_role: - name: web-app-taiga -- name: flush handlers after taiga + name: web-app-matrix +- name: flush handlers after matrix meta: flush_handlers -- name: setup bigbluebutton - when: ('bigbluebutton' | application_allowed(group_names, allowed_applications)) +- name: setup baserow + when: ('baserow' | application_allowed(group_names, allowed_applications)) include_role: - name: web-app-bigbluebutton -- name: flush handlers after bigbluebutton + name: web-app-baserow +- name: flush handlers after baserow meta: flush_handlers -- name: setup lam - when: ('lam' | application_allowed(group_names, allowed_applications)) +- name: setup gitea + when: ('gitea' | application_allowed(group_names, allowed_applications)) include_role: - name: web-app-lam -- name: flush handlers after lam + name: web-app-gitea +- name: flush handlers after gitea meta: flush_handlers -- name: setup mastodon - when: ('mastodon' | application_allowed(group_names, allowed_applications)) +- name: setup bluesky + when: ('bluesky' | application_allowed(group_names, allowed_applications)) include_role: - name: web-app-mastodon -- name: flush handlers after mastodon + name: web-app-bluesky +- name: flush handlers after bluesky meta: flush_handlers -- name: setup pgadmin - when: ('pgadmin' | application_allowed(group_names, allowed_applications)) +- name: setup mediawiki + when: ('mediawiki' | application_allowed(group_names, allowed_applications)) include_role: - name: web-app-pgadmin -- name: flush handlers after pgadmin + name: web-app-mediawiki +- name: flush handlers after mediawiki meta: flush_handlers -- name: setup mailu - when: ('mailu' | application_allowed(group_names, allowed_applications)) +- name: setup attendize + when: ('attendize' | application_allowed(group_names, allowed_applications)) include_role: - name: web-app-mailu -- name: flush handlers after mailu + name: web-app-attendize +- name: flush handlers after attendize meta: flush_handlers -- name: setup moodle - when: ('moodle' | application_allowed(group_names, allowed_applications)) +- name: setup snipe-it + when: ('snipe-it' | application_allowed(group_names, allowed_applications)) include_role: - name: web-app-moodle -- name: flush handlers after moodle + name: web-app-snipe-it +- name: flush handlers after snipe-it meta: flush_handlers -- name: setup discourse - when: ('discourse' | application_allowed(group_names, allowed_applications)) +- name: setup fusiondirectory + when: ('fusiondirectory' | application_allowed(group_names, allowed_applications)) include_role: - name: web-app-discourse -- name: flush handlers after discourse + name: web-app-fusiondirectory +- name: flush handlers after fusiondirectory meta: flush_handlers -- name: setup nextcloud - when: ('nextcloud' | application_allowed(group_names, allowed_applications)) +- name: setup akaunting + when: ('akaunting' | application_allowed(group_names, allowed_applications)) include_role: - name: web-app-nextcloud -- name: flush handlers after nextcloud + name: web-app-akaunting +- name: flush handlers after akaunting + meta: flush_handlers +- name: setup mybb + when: ('mybb' | application_allowed(group_names, allowed_applications)) + include_role: + name: web-app-mybb +- name: flush handlers after mybb + meta: flush_handlers +- name: setup gitlab + when: ('gitlab' | application_allowed(group_names, allowed_applications)) + include_role: + name: web-app-gitlab +- name: flush handlers after gitlab meta: flush_handlers - name: setup espocrm when: ('espocrm' | application_allowed(group_names, allowed_applications)) @@ -214,87 +280,21 @@ name: web-app-joomla - name: flush handlers after joomla meta: flush_handlers -- name: setup matrix - when: ('matrix' | application_allowed(group_names, allowed_applications)) - include_role: - name: web-app-matrix -- name: flush handlers after matrix - meta: flush_handlers -- name: setup mobilizon - when: ('mobilizon' | application_allowed(group_names, allowed_applications)) - include_role: - name: web-app-mobilizon -- name: flush handlers after mobilizon - meta: flush_handlers -- name: setup snipe-it - when: ('snipe-it' | application_allowed(group_names, allowed_applications)) - include_role: - name: web-app-snipe-it -- name: flush handlers after snipe-it - meta: flush_handlers -- name: setup mybb - when: ('mybb' | application_allowed(group_names, allowed_applications)) - include_role: - name: web-app-mybb -- name: flush handlers after mybb - meta: flush_handlers -- name: setup attendize - when: ('attendize' | application_allowed(group_names, allowed_applications)) - include_role: - name: web-app-attendize -- name: flush handlers after attendize - meta: flush_handlers -- name: setup gitlab - when: ('gitlab' | application_allowed(group_names, allowed_applications)) - include_role: - name: web-app-gitlab -- name: flush handlers after gitlab - meta: flush_handlers -- name: setup mediawiki - when: ('mediawiki' | application_allowed(group_names, allowed_applications)) - include_role: - name: web-app-mediawiki -- name: flush handlers after mediawiki - meta: flush_handlers -- name: setup funkwhale - when: ('funkwhale' | application_allowed(group_names, allowed_applications)) - include_role: - name: web-app-funkwhale -- name: flush handlers after funkwhale - meta: flush_handlers -- name: setup gitea - when: ('gitea' | application_allowed(group_names, allowed_applications)) - include_role: - name: web-app-gitea -- name: flush handlers after gitea - meta: flush_handlers -- name: setup baserow - when: ('baserow' | application_allowed(group_names, allowed_applications)) - include_role: - name: web-app-baserow -- name: flush handlers after baserow - meta: flush_handlers -- name: setup akaunting - when: ('akaunting' | application_allowed(group_names, allowed_applications)) - include_role: - name: web-app-akaunting -- name: flush handlers after akaunting - meta: flush_handlers -- name: setup bluesky - when: ('bluesky' | application_allowed(group_names, allowed_applications)) - include_role: - name: web-app-bluesky -- name: flush handlers after bluesky - meta: flush_handlers - name: setup listmonk when: ('listmonk' | application_allowed(group_names, allowed_applications)) include_role: name: web-app-listmonk - name: flush handlers after listmonk meta: flush_handlers -- name: setup fusiondirectory - when: ('fusiondirectory' | application_allowed(group_names, allowed_applications)) +- name: setup discourse + when: ('discourse' | application_allowed(group_names, allowed_applications)) include_role: - name: web-app-fusiondirectory -- name: flush handlers after fusiondirectory + name: web-app-discourse +- name: flush handlers after discourse + meta: flush_handlers +- name: setup nextcloud + when: ('nextcloud' | application_allowed(group_names, allowed_applications)) + include_role: + name: web-app-nextcloud +- name: flush handlers after nextcloud meta: flush_handlers diff --git a/tests/integration/test_circular_dependencies.py b/tests/integration/test_circular_dependencies.py new file mode 100644 index 00000000..0a38b2e3 --- /dev/null +++ b/tests/integration/test_circular_dependencies.py @@ -0,0 +1,36 @@ +import os +import unittest + +# import the functions from your CLI script +from cli.generate_playbook import build_dependency_graph, find_cycle + +class TestCircularDependencies(unittest.TestCase): + """ + Integration test: ensure there are no circular 'run_after' dependencies + among the roles in the roles/ directory. + """ + + @classmethod + def setUpClass(cls): + # Determine the path to the repo root and the roles directory + here = os.path.abspath(os.path.dirname(__file__)) + repo_root = os.path.abspath(os.path.join(here, '..', '..')) + cls.roles_dir = os.path.join(repo_root, 'roles') + + def test_no_circular_dependencies(self): + # Build the dependency graph using the real roles/ + graph, in_degree, roles = build_dependency_graph(self.roles_dir) + + # Attempt to find a cycle in the run_after mapping + cycle = find_cycle(roles) + + if cycle: + # Format cycle as "A -> B -> C -> A" + cycle_str = " -> ".join(cycle) + self.fail(f"Circular dependency detected among roles: {cycle_str}") + + # If no cycle, this assertion will pass + self.assertIsNone(cycle, "Expected no circular dependencies") + +if __name__ == '__main__': + unittest.main() diff --git a/tests/integration/test_run_after_references.py b/tests/integration/test_run_after_references.py new file mode 100644 index 00000000..c45238b0 --- /dev/null +++ b/tests/integration/test_run_after_references.py @@ -0,0 +1,49 @@ +import os +import unittest +import yaml + +class TestRunAfterReferences(unittest.TestCase): + """ + Integration test: ensure that every name listed under + galaxy_info.run_after in each role's meta/main.yml + corresponds to an existing role directory. + """ + + @classmethod + def setUpClass(cls): + here = os.path.abspath(os.path.dirname(__file__)) + repo_root = os.path.abspath(os.path.join(here, '..', '..')) + cls.roles_dir = os.path.join(repo_root, 'roles') + # collect all role names (folder names) in roles/ + cls.existing_roles = { + name for name in os.listdir(cls.roles_dir) + if os.path.isdir(os.path.join(cls.roles_dir, name)) + } + + def test_run_after_points_to_existing_roles(self): + errors = [] + for role in sorted(self.existing_roles): + meta_path = os.path.join(self.roles_dir, role, 'meta', 'main.yml') + if not os.path.isfile(meta_path): + # skip roles without a meta/main.yml + continue + + with open(meta_path, 'r') as f: + data = yaml.safe_load(f) or {} + + run_after = data.get('galaxy_info', {}).get('run_after', []) + for dep in run_after: + if dep not in self.existing_roles: + errors.append( + f"Role '{role}' declares run_after: '{dep}', " + f"but '{dep}' is not a directory under roles/" + ) + + if errors: + self.fail( + "Some run_after references are invalid:\n " + + "\n ".join(errors) + ) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/integration/test_self_dependency.py b/tests/integration/test_self_dependency.py new file mode 100644 index 00000000..907bc648 --- /dev/null +++ b/tests/integration/test_self_dependency.py @@ -0,0 +1,35 @@ +import os +import unittest +import yaml + +class TestSelfDependency(unittest.TestCase): + """ + Integration test: ensure no role lists itself in its own 'run_after' + in meta/main.yml. + """ + + @classmethod + def setUpClass(cls): + here = os.path.abspath(os.path.dirname(__file__)) + repo_root = os.path.abspath(os.path.join(here, '..', '..')) + cls.roles_dir = os.path.join(repo_root, 'roles') + + def test_no_self_in_run_after(self): + for entry in os.listdir(self.roles_dir): + role_path = os.path.join(self.roles_dir, entry) + meta_file = os.path.join(role_path, 'meta', 'main.yml') + if not os.path.isdir(role_path) or not os.path.isfile(meta_file): + continue + + with open(meta_file, 'r') as f: + data = yaml.safe_load(f) or {} + + run_after = data.get('galaxy_info', {}).get('run_after', []) + if entry in run_after: + self.fail( + f"Role '{entry}' has a self-dependency in its run_after list " + f"in {meta_file}" + ) + +if __name__ == '__main__': + unittest.main()