Solved run_after dependency bug

This commit is contained in:
Kevin Veen-Birkenbach 2025-07-09 06:47:10 +02:00
parent 39d2e6c0fa
commit a69b2c9cb2
No known key found for this signature in database
GPG Key ID: 44D8F11FD62F878E
6 changed files with 428 additions and 257 deletions

View File

@ -7,8 +7,8 @@ INCLUDES_OUT := ./tasks/utils/web-app-roles.yml
INCLUDES_SCRIPT := ./cli/generate_playbook.py INCLUDES_SCRIPT := ./cli/generate_playbook.py
EXTRA_USERS := $(shell \ EXTRA_USERS := $(shell \
find $(ROLES_DIR) -maxdepth 1 -type d -name 'docker*' -printf '%f\n' \ find $(ROLES_DIR) -maxdepth 1 -type d -name 'web-app*' -printf '%f\n' \
| sed -E 's/^docker[_-]?//' \ | sed -E 's/^web-app[_-]?//' \
| grep -E -x '[a-z0-9]+' \ | grep -E -x '[a-z0-9]+' \
| paste -sd, - \ | paste -sd, - \
) )
@ -24,7 +24,7 @@ build:
@echo "🔧 Generating users defaults → $(USERS_OUT) from roles in $(ROLES_DIR)" @echo "🔧 Generating users defaults → $(USERS_OUT) from roles in $(ROLES_DIR)"
@echo "🔧 Generating Docker role includes → $(INCLUDES_OUT)" @echo "🔧 Generating Docker role includes → $(INCLUDES_OUT)"
@mkdir -p $(dir $(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)" @echo "✅ Docker role includes written to $(INCLUDES_OUT)"
install: build install: build

View File

@ -1,13 +1,21 @@
#!/usr/bin/env python3
import os import os
import sys
import yaml import yaml
import argparse import argparse
from collections import defaultdict, deque from collections import defaultdict, deque
def find_roles(roles_dir, prefix=None): def find_roles(roles_dir, prefixes=None):
"""Find all roles in the given directory.""" """
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): for entry in os.listdir(roles_dir):
if prefix and not entry.startswith(prefix): if prefixes:
continue if not any(entry.startswith(pref) for pref in prefixes):
continue
path = os.path.join(roles_dir, entry) path = os.path.join(roles_dir, entry)
meta_file = os.path.join(path, 'meta', 'main.yml') meta_file = os.path.join(path, 'meta', 'main.yml')
if os.path.isdir(path) and os.path.isfile(meta_file): 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 data.get('application_id')
return None return None
def build_dependency_graph(roles_dir, prefix=None): def build_dependency_graph(roles_dir, prefixes=None):
"""Build a dependency graph where each role points to the roles it depends on.""" """
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) graph = defaultdict(list)
in_degree = defaultdict(int) in_degree = defaultdict(int)
roles = {} 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) run_after = load_run_after(meta_file)
application_id = load_application_id(role_path) application_id = load_application_id(role_path)
role_name = os.path.basename(role_path) role_name = os.path.basename(role_path)
roles[role_name] = { roles[role_name] = {
'role_name': role_name, 'role_name': role_name,
'run_after': run_after, 'run_after': run_after,
@ -45,37 +58,87 @@ def build_dependency_graph(roles_dir, prefix=None):
'path': role_path 'path': role_path
} }
# If the role has dependencies, build the graph
for dependency in run_after: for dependency in run_after:
graph[dependency].append(role_name) graph[dependency].append(role_name)
in_degree[role_name] += 1 in_degree[role_name] += 1
# Ensure roles with no dependencies have an in-degree of 0
if role_name not in in_degree: if role_name not in in_degree:
in_degree[role_name] = 0 in_degree[role_name] = 0
return graph, in_degree, roles return graph, in_degree, roles
def topological_sort(graph, in_degree): def find_cycle(roles):
"""Perform topological sort on the dependency graph.""" """
# Queue for roles with no incoming dependencies (in_degree == 0) Detect a cycle in the run_after relations:
queue = deque([role for role, degree in in_degree.items() if degree == 0]) 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 = [] sorted_roles = []
local_in = dict(in_degree)
while queue: while queue:
role = queue.popleft() role = queue.popleft()
sorted_roles.append(role) sorted_roles.append(role)
for nbr in graph.get(role, []):
# Reduce in-degree for roles dependent on the current role local_in[nbr] -= 1
for neighbor in graph[role]: if local_in[nbr] == 0:
in_degree[neighbor] -= 1 queue.append(nbr)
if in_degree[neighbor] == 0:
queue.append(neighbor)
if len(sorted_roles) != len(in_degree): if len(sorted_roles) != len(in_degree):
# If the number of sorted roles doesn't match the number of roles, cycle = find_cycle(roles or {})
# there was a cycle in the graph (not all roles could be sorted) if roles is not None:
raise Exception("Circular dependency detected among the roles!") 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 return sorted_roles
@ -83,48 +146,38 @@ def print_dependency_tree(graph):
"""Print the dependency tree visually on the console.""" """Print the dependency tree visually on the console."""
def print_node(role, indent=0): def print_node(role, indent=0):
print(" " * indent + role) print(" " * indent + role)
for dependency in graph[role]: for dep in graph.get(role, []):
print_node(dependency, indent + 1) print_node(dep, indent + 1)
# Print the tree starting from roles with no dependencies
all_roles = set(graph.keys()) all_roles = set(graph.keys())
dependent_roles = {role for dependencies in graph.values() for role in dependencies} dependent = {r for deps in graph.values() for r in deps}
root_roles = all_roles - dependent_roles roots = all_roles - dependent
printed_roles = [] for root in roots:
print_node(root)
def collect_roles(role, indent=0): def generate_playbook_entries(roles_dir, prefixes=None):
printed_roles.append(role) """
for dependency in graph[role]: Generate playbook entries based on the sorted order.
collect_roles(dependency, indent + 1) Raises a ValueError if application_id is missing.
"""
for root in root_roles: graph, in_degree, roles = build_dependency_graph(roles_dir, prefixes)
collect_roles(root) sorted_names = topological_sort(graph, in_degree, roles)
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)
entries = [] entries = []
for role_name in sorted_role_names: for role_name in sorted_names:
role = roles[role_name] role = roles[role_name]
# --- new validation block ---
if role.get('application_id') is None: 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'] app_id = role['application_id']
entries.append( entries.append(
f"- name: setup {app_id}\n" f"- name: setup {app_id}\n"
f" when: ('{app_id}' | application_allowed(group_names, allowed_applications))\n" f" when: ('{app_id}' | application_allowed(group_names, allowed_applications))\n"
f" include_role:\n" f" include_role:\n"
f" name: {role['role_name']}\n" f" name: {role_name}\n"
) )
entries.append( entries.append(
f"- name: flush handlers after {app_id}\n" f"- name: flush handlers after {app_id}\n"
@ -137,32 +190,30 @@ def main():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='Generate an Ansible playbook include file from Docker roles, sorted by run_after order.' description='Generate an Ansible playbook include file from Docker roles, sorted by run_after order.'
) )
parser.add_argument( parser.add_argument('roles_dir', help='Path to directory containing role folders')
'roles_dir',
help='Path to directory containing role folders'
)
parser.add_argument( parser.add_argument(
'-p', '--prefix', '-p', '--prefix',
help='Only include roles whose names start with this prefix (e.g. web-app-, desk-)', action='append',
default=None help='Only include roles whose names start with any of these prefixes; can be specified multiple times'
) )
parser.add_argument( parser.add_argument('-o', '--output', default=None,
'-o', '--output', help='Output file path (default: stdout)')
help='Output file path (default: stdout)', parser.add_argument('-t', '--tree', action='store_true',
default=None help='Display the dependency tree of roles and exit')
)
parser.add_argument(
'-t', '--tree',
action='store_true',
help='Display the dependency tree of roles visually'
)
args = parser.parse_args()
# Generate and output the playbook entries args = parser.parse_args()
entries = generate_playbook_entries(args.roles_dir, args.prefix) 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) output = ''.join(entries)
if args.output: if args.output:
os.makedirs(os.path.dirname(args.output), exist_ok=True)
with open(args.output, 'w') as f: with open(args.output, 'w') as f:
f.write(output) f.write(output)
print(f"Playbook entries written to {args.output}") print(f"Playbook entries written to {args.output}")

View File

@ -1,32 +1,8 @@
- name: setup presentation - name: setup roulette-wheel
when: ('presentation' | application_allowed(group_names, allowed_applications)) when: ('roulette-wheel' | application_allowed(group_names, allowed_applications))
include_role: include_role:
name: web-app-presentation name: web-app-roulette-wheel
- name: flush handlers after presentation - name: flush handlers after roulette-wheel
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
meta: flush_handlers meta: flush_handlers
- name: setup ldap - name: setup ldap
when: ('ldap' | application_allowed(group_names, allowed_applications)) when: ('ldap' | application_allowed(group_names, allowed_applications))
@ -34,17 +10,11 @@
name: svc-openldap name: svc-openldap
- name: flush handlers after ldap - name: flush handlers after ldap
meta: flush_handlers meta: flush_handlers
- name: setup None - name: setup simpleicons
when: ('None' | application_allowed(group_names, allowed_applications)) when: ('simpleicons' | application_allowed(group_names, allowed_applications))
include_role: include_role:
name: docker-compose name: web-app-simpleicons
- name: flush handlers after None - name: flush handlers after simpleicons
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
meta: flush_handlers meta: flush_handlers
- name: setup sphinx - name: setup sphinx
when: ('sphinx' | application_allowed(group_names, allowed_applications)) when: ('sphinx' | application_allowed(group_names, allowed_applications))
@ -52,29 +22,59 @@
name: web-app-sphinx name: web-app-sphinx
- name: flush handlers after sphinx - name: flush handlers after sphinx
meta: flush_handlers 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 - name: setup mariadb
when: ('mariadb' | application_allowed(group_names, allowed_applications)) when: ('mariadb' | application_allowed(group_names, allowed_applications))
include_role: include_role:
name: svc-rdbms-mariadb name: svc-rdbms-mariadb
- name: flush handlers after mariadb - name: flush handlers after mariadb
meta: flush_handlers meta: flush_handlers
- name: setup simpleicons - name: setup elk
when: ('simpleicons' | application_allowed(group_names, allowed_applications)) when: ('elk' | application_allowed(group_names, allowed_applications))
include_role: include_role:
name: web-app-simpleicons name: web-app-elk
- name: flush handlers after simpleicons - name: flush handlers after elk
meta: flush_handlers meta: flush_handlers
- name: setup None - name: setup collabora
when: ('None' | application_allowed(group_names, allowed_applications)) when: ('collabora' | application_allowed(group_names, allowed_applications))
include_role: include_role:
name: svc-rdbms-central name: web-app-collabora
- name: flush handlers after None - name: flush handlers after collabora
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
meta: flush_handlers meta: flush_handlers
- name: setup jenkins - name: setup jenkins
when: ('jenkins' | application_allowed(group_names, allowed_applications)) when: ('jenkins' | application_allowed(group_names, allowed_applications))
@ -82,29 +82,35 @@
name: web-app-jenkins name: web-app-jenkins
- name: flush handlers after jenkins - name: flush handlers after jenkins
meta: flush_handlers 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 - name: setup portfolio
when: ('portfolio' | application_allowed(group_names, allowed_applications)) when: ('portfolio' | application_allowed(group_names, allowed_applications))
include_role: include_role:
name: web-app-portfolio name: web-app-portfolio
- name: flush handlers after portfolio - name: flush handlers after portfolio
meta: flush_handlers 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 - name: setup keycloak
when: ('keycloak' | application_allowed(group_names, allowed_applications)) when: ('keycloak' | application_allowed(group_names, allowed_applications))
include_role: include_role:
name: web-app-keycloak name: web-app-keycloak
- name: flush handlers after keycloak - name: flush handlers after keycloak
meta: flush_handlers meta: flush_handlers
- name: setup yourls - name: setup mailu
when: ('yourls' | application_allowed(group_names, allowed_applications)) when: ('mailu' | application_allowed(group_names, allowed_applications))
include_role: include_role:
name: web-app-yourls name: web-app-mailu
- name: flush handlers after yourls - 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 meta: flush_handlers
- name: setup wordpress - name: setup wordpress
when: ('wordpress' | application_allowed(group_names, allowed_applications)) when: ('wordpress' | application_allowed(group_names, allowed_applications))
@ -112,11 +118,17 @@
name: web-app-wordpress name: web-app-wordpress
- name: flush handlers after wordpress - name: flush handlers after wordpress
meta: flush_handlers meta: flush_handlers
- name: setup pixelfed - name: setup pgadmin
when: ('pixelfed' | application_allowed(group_names, allowed_applications)) when: ('pgadmin' | application_allowed(group_names, allowed_applications))
include_role: include_role:
name: web-app-pixelfed name: web-app-pgadmin
- name: flush handlers after pixelfed - 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 meta: flush_handlers
- name: setup peertube - name: setup peertube
when: ('peertube' | application_allowed(group_names, allowed_applications)) when: ('peertube' | application_allowed(group_names, allowed_applications))
@ -124,6 +136,48 @@
name: web-app-peertube name: web-app-peertube
- name: flush handlers after peertube - name: flush handlers after peertube
meta: flush_handlers 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 - name: setup phpmyadmin
when: ('phpmyadmin' | application_allowed(group_names, allowed_applications)) when: ('phpmyadmin' | application_allowed(group_names, allowed_applications))
include_role: include_role:
@ -136,71 +190,83 @@
name: web-app-openproject name: web-app-openproject
- name: flush handlers after openproject - name: flush handlers after openproject
meta: flush_handlers meta: flush_handlers
- name: setup phpldapadmin - name: setup mobilizon
when: ('phpldapadmin' | application_allowed(group_names, allowed_applications)) when: ('mobilizon' | application_allowed(group_names, allowed_applications))
include_role: include_role:
name: web-app-phpldapadmin name: web-app-mobilizon
- name: flush handlers after phpldapadmin - name: flush handlers after mobilizon
meta: flush_handlers meta: flush_handlers
- name: setup friendica - name: setup funkwhale
when: ('friendica' | application_allowed(group_names, allowed_applications)) when: ('funkwhale' | application_allowed(group_names, allowed_applications))
include_role: include_role:
name: web-app-friendica name: web-app-funkwhale
- name: flush handlers after friendica - name: flush handlers after funkwhale
meta: flush_handlers meta: flush_handlers
- name: setup taiga - name: setup matrix
when: ('taiga' | application_allowed(group_names, allowed_applications)) when: ('matrix' | application_allowed(group_names, allowed_applications))
include_role: include_role:
name: web-app-taiga name: web-app-matrix
- name: flush handlers after taiga - name: flush handlers after matrix
meta: flush_handlers meta: flush_handlers
- name: setup bigbluebutton - name: setup baserow
when: ('bigbluebutton' | application_allowed(group_names, allowed_applications)) when: ('baserow' | application_allowed(group_names, allowed_applications))
include_role: include_role:
name: web-app-bigbluebutton name: web-app-baserow
- name: flush handlers after bigbluebutton - name: flush handlers after baserow
meta: flush_handlers meta: flush_handlers
- name: setup lam - name: setup gitea
when: ('lam' | application_allowed(group_names, allowed_applications)) when: ('gitea' | application_allowed(group_names, allowed_applications))
include_role: include_role:
name: web-app-lam name: web-app-gitea
- name: flush handlers after lam - name: flush handlers after gitea
meta: flush_handlers meta: flush_handlers
- name: setup mastodon - name: setup bluesky
when: ('mastodon' | application_allowed(group_names, allowed_applications)) when: ('bluesky' | application_allowed(group_names, allowed_applications))
include_role: include_role:
name: web-app-mastodon name: web-app-bluesky
- name: flush handlers after mastodon - name: flush handlers after bluesky
meta: flush_handlers meta: flush_handlers
- name: setup pgadmin - name: setup mediawiki
when: ('pgadmin' | application_allowed(group_names, allowed_applications)) when: ('mediawiki' | application_allowed(group_names, allowed_applications))
include_role: include_role:
name: web-app-pgadmin name: web-app-mediawiki
- name: flush handlers after pgadmin - name: flush handlers after mediawiki
meta: flush_handlers meta: flush_handlers
- name: setup mailu - name: setup attendize
when: ('mailu' | application_allowed(group_names, allowed_applications)) when: ('attendize' | application_allowed(group_names, allowed_applications))
include_role: include_role:
name: web-app-mailu name: web-app-attendize
- name: flush handlers after mailu - name: flush handlers after attendize
meta: flush_handlers meta: flush_handlers
- name: setup moodle - name: setup snipe-it
when: ('moodle' | application_allowed(group_names, allowed_applications)) when: ('snipe-it' | application_allowed(group_names, allowed_applications))
include_role: include_role:
name: web-app-moodle name: web-app-snipe-it
- name: flush handlers after moodle - name: flush handlers after snipe-it
meta: flush_handlers meta: flush_handlers
- name: setup discourse - name: setup fusiondirectory
when: ('discourse' | application_allowed(group_names, allowed_applications)) when: ('fusiondirectory' | application_allowed(group_names, allowed_applications))
include_role: include_role:
name: web-app-discourse name: web-app-fusiondirectory
- name: flush handlers after discourse - name: flush handlers after fusiondirectory
meta: flush_handlers meta: flush_handlers
- name: setup nextcloud - name: setup akaunting
when: ('nextcloud' | application_allowed(group_names, allowed_applications)) when: ('akaunting' | application_allowed(group_names, allowed_applications))
include_role: include_role:
name: web-app-nextcloud name: web-app-akaunting
- name: flush handlers after nextcloud - 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 meta: flush_handlers
- name: setup espocrm - name: setup espocrm
when: ('espocrm' | application_allowed(group_names, allowed_applications)) when: ('espocrm' | application_allowed(group_names, allowed_applications))
@ -214,87 +280,21 @@
name: web-app-joomla name: web-app-joomla
- name: flush handlers after joomla - name: flush handlers after joomla
meta: flush_handlers 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 - name: setup listmonk
when: ('listmonk' | application_allowed(group_names, allowed_applications)) when: ('listmonk' | application_allowed(group_names, allowed_applications))
include_role: include_role:
name: web-app-listmonk name: web-app-listmonk
- name: flush handlers after listmonk - name: flush handlers after listmonk
meta: flush_handlers meta: flush_handlers
- name: setup fusiondirectory - name: setup discourse
when: ('fusiondirectory' | application_allowed(group_names, allowed_applications)) when: ('discourse' | application_allowed(group_names, allowed_applications))
include_role: include_role:
name: web-app-fusiondirectory name: web-app-discourse
- name: flush handlers after fusiondirectory - 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 meta: flush_handlers

View File

@ -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()

View File

@ -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()

View File

@ -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()