Solved collection dependency bug

This commit is contained in:
Kevin Veen-Birkenbach 2025-07-12 13:42:45 +02:00
parent b2e32aacf3
commit e174523fc6
No known key found for this signature in database
GPG Key ID: 44D8F11FD62F878E
52 changed files with 148 additions and 69 deletions

View File

@ -9,12 +9,12 @@ defaults_service_provider:
city: "Cybertown" city: "Cybertown"
postal_code: "00001" postal_code: "00001"
country: "Nexusland" country: "Nexusland"
logo: "{{ applications['asset'].url ~ '/img/logo.png' }}" logo: "{{ applications['web-svc-asset'].url ~ '/img/logo.png' }}"
platform: platform:
titel: "CyMaIS" titel: "CyMaIS"
subtitel: "One login. Infinite applications." subtitel: "One login. Infinite applications."
logo: "{{ applications['asset'].url ~ '/img/logo.png' }}" logo: "{{ applications['web-svc-asset'].url ~ '/img/logo.png' }}"
favicon: "{{ applications['asset'].url ~ '/img/favicon.ico' }}" favicon: "{{ applications['web-svc-asset'].url ~ '/img/favicon.ico' }}"
contact: contact:
bluesky: >- bluesky: >-
{{ ('@' ~ users.contact.username ~ '.' ~ domains.bluesky.api) {{ ('@' ~ users.contact.username ~ '.' ~ domains.bluesky.api)
@ -30,4 +30,4 @@ defaults_service_provider:
legal: legal:
editorial_responsible: "Johannes Gutenberg" editorial_responsible: "Johannes Gutenberg"
source_code: "https://github.com/kevinveenbirkenbach/cymais" source_code: "https://github.com/kevinveenbirkenbach/cymais"
imprint: "{{web_protocol}}://{{ domains | get_domain('html') }}/imprint.html" imprint: "{{web_protocol}}://{{ domains | get_domain('web-svc-html') }}/imprint.html"

View File

@ -1 +1 @@
application_id: bluray-player application_id: desk-bluray-player

View File

@ -1,5 +1,5 @@
--- ---
application_id: "chromium" application_id: "desk-chromium"
chromium_package: "{{ 'chromium-browser' if ansible_os_family == 'Debian' else 'chromium' }}" chromium_package: "{{ 'chromium-browser' if ansible_os_family == 'Debian' else 'chromium' }}"

View File

@ -1,7 +1,7 @@
--- ---
# Default variables for the pc-firefox role # Default variables for the pc-firefox role
application_id: "firefox" application_id: "desk-firefox"
# Package name for Firefox on Arch Linux # Package name for Firefox on Arch Linux
firefox_package: firefox firefox_package: firefox

View File

@ -1,2 +1,2 @@
auto_start_directory: /home/{{users.client.username}}/.config/autostart/ auto_start_directory: /home/{{users.client.username}}/.config/autostart/
application_id: gnome-caffeine application_id: desk-gnome-caffeine

View File

@ -1 +1 @@
application_id: gnome-extensions application_id: desk-gnome-extensions

View File

@ -1 +1 @@
application_id: gnome-terminal application_id: desk-gnome-terminal

View File

@ -1 +1 @@
application_id: gnome application_id: desk-gnome

View File

@ -1 +1 @@
application_id: gnucash application_id: desk-gnucash

View File

@ -1 +1 @@
application_id: jrnl application_id: desk-jrnl

View File

@ -1 +1 @@
application_id: keepassxc application_id: desk-keepassxc

View File

@ -2,11 +2,11 @@
community.general.pacman: community.general.pacman:
name: name:
- ttf-liberation - ttf-liberation
- "libreoffice-{{ applications.libreoffice.flavor }}" - "libreoffice-{{ applications['desk-libreoffice'].flavor }}"
state: present state: present
- name: install libreoffice language packages - name: install libreoffice language packages
community.general.pacman: community.general.pacman:
name: "libreoffice-{{ applications.libreoffice.flavor }}-{{ item }}" name: "libreoffice-{{ applications['desk-libreoffice'].flavor }}-{{ item }}"
state: present state: present
loop: "{{libreoffice_languages}}" loop: "{{libreoffice_languages}}"

View File

@ -1 +1 @@
application_id: "libreoffice" application_id: "desk-libreoffice"

View File

@ -1,3 +1,3 @@
user_home_directory: /home/{{users.client.username}}/ user_home_directory: /home/{{users.client.username}}/
cloud_directory: '{{user_home_directory}}Clouds/{{cloud_fqdn}}/{{users.client.username}}/' cloud_directory: '{{user_home_directory}}Clouds/{{cloud_fqdn}}/{{users.client.username}}/'
application_id: nextcloud-client application_id: desk-nextcloud-client

View File

@ -1 +1 @@
application_id: obs application_id: desk-obs

View File

@ -1 +1 @@
application_id: qbittorrent application_id: desk-qbittorrent

View File

@ -2,4 +2,4 @@ retroarch_packages:
- retroarch - retroarch
- retroarch-assets-xmb - retroarch-assets-xmb
- retroarch-assets-ozone - retroarch-assets-ozone
application_id: retroarch application_id: desk-retroarch

View File

@ -1 +1 @@
application_id: spotify application_id: desk-spotify

View File

@ -1 +1 @@
application_id: ssh application_id: desk-ssh

View File

@ -1 +1 @@
application_id: torbrowser application_id: desk-torbrowser

View File

@ -1 +1 @@
application_id: virtual-box application_id: desk-virtual-box

View File

@ -1 +1 @@
application_id: zoom application_id: desk-zoom

View File

@ -1 +1 @@
application_id: java application_id: dev-java

View File

@ -1 +1 @@
application_id: make application_id: dev-make

View File

@ -1 +1 @@
application_id: hunspell application_id: gen-hunspell

View File

@ -1 +1 @@
application_id: wireguard-core application_id: net-wireguard-core

View File

@ -1 +1 @@
application_id: wireguard-firewalled application_id: net-wireguard-firewalled

View File

@ -1 +1 @@
application_id: wireguard-plain application_id: net-wireguard-plain

View File

@ -1 +1 @@
application_id: browser application_id: util-desk-browser

View File

@ -1 +1 @@
application_id: design application_id: util-desk-design

View File

@ -1 +1 @@
application_id: dev-arduino application_id: util-desk-dev-arduino

View File

@ -1 +1 @@
application_id: dev-core application_id: util-desk-dev-core

View File

@ -1 +1 @@
application_id: dev-java application_id: util-desk-dev-java

View File

@ -1 +1 @@
application_id: dev-php application_id: util-desk-dev-php

View File

@ -1 +1 @@
application_id: dev-python application_id: util-desk-dev-python

View File

@ -1 +1 @@
application_id: dev-shell application_id: util-desk-dev-shell

View File

@ -1 +1 @@
application_id: game-compose application_id: util-desk-game-compose

View File

@ -5,4 +5,4 @@ gamer_default_games:
- gnuchess - gnuchess
- sauerbraten - sauerbraten
- mari0 - mari0
application_id: game-os application_id: util-desk-game-os

View File

@ -1 +1 @@
application_id: game-windows application_id: util-desk-game-windows

View File

@ -1 +1 @@
application_id: office-tools application_id: util-desk-office-tools

View File

@ -1 +1 @@
application_id: dev-admin-network application_id: util-dev-admin-network

View File

@ -1 +1 @@
application_id: dev-admin application_id: util-dev-admin

View File

@ -1 +1 @@
application_id: corporate-identity application_id: util-srv-corporate-identity

View File

@ -1,4 +1,4 @@
application_id: "portfolio" application_id: "web-app-portfolio"
docker_repository_address: "https://github.com/kevinveenbirkenbach/portfolio" docker_repository_address: "https://github.com/kevinveenbirkenbach/portfolio"
config_inventory_path: "{{ inventory_dir }}/files/{{ inventory_hostname }}/docker/portfolio/config.yaml.j2" config_inventory_path: "{{ inventory_dir }}/files/{{ inventory_hostname }}/docker/portfolio/config.yaml.j2"
docker_repository: true docker_repository: true

View File

@ -1,3 +1,3 @@
application_id: "asset" # Application identifier application_id: "web-svc-asset" # Application identifier
source_directory: "{{ applications[application_id].source_directory }}/" # Source directory from which the files are coming from source_directory: "{{ applications[application_id].source_directory }}/" # Source directory from which the files are coming from
target_directory: "{{ nginx.directories.data.files }}assets" # Directory to which the files will be copied target_directory: "{{ nginx.directories.data.files }}assets" # Directory to which the files will be copied

View File

@ -4,4 +4,6 @@ features:
portfolio_iframe: true portfolio_iframe: true
domains: domains:
canonical: canonical:
- "file.{{ primary_domain }}"
alias:
- "files.{{ primary_domain }}" - "files.{{ primary_domain }}"

View File

@ -1,2 +1,2 @@
application_id: "file" application_id: "web-svc-file"
domain: "{{ domains | get_domain(application_id) }}" domain: "{{ domains | get_domain(application_id) }}"

View File

@ -1,4 +1,2 @@
application_id: "html" application_id: "web-svc-html"
domain: "{{domains | get_domain(application_id)}}" domain: "{{domains | get_domain(application_id)}}"
features:
portfolio_iframe: true # Necessary for imprint loading

View File

@ -1 +1 @@
application_id: "legal" application_id: "web-svc-legal"

View File

@ -48,7 +48,7 @@
# The following mapping is necessary to define the exceptions for domains which are created, but which aren't used # The following mapping is necessary to define the exceptions for domains which are created, but which aren't used
redirect_domain_mappings: "{{ redirect_domain_mappings: "{{
[] | [] |
add_redirect_if_group('asset', domains | get_domain('asset'), domains | get_domain('file'), group_names) | add_redirect_if_group('web-svc-asset', domains | get_domain('web-svc-asset'), domains | get_domain('web-svc-file'), group_names) |
merge_mapping(redirect_domain_mappings| default([]), 'source') merge_mapping(redirect_domain_mappings| default([]), 'source')
}}" }}"

View File

@ -0,0 +1,79 @@
import os
import unittest
import yaml
import glob
class TestDependencyApplicationId(unittest.TestCase):
def test_application_id_matches_folder_for_dependent_roles(self):
"""
For any role A that depends on role B (listed under A/meta/main.yml dependencies),
if both A and B have vars/main.yml with application_id defined,
then application_id must equal the role's folder name for both.
"""
base_dir = os.path.dirname(__file__)
roles_dir = os.path.abspath(os.path.join(base_dir, '..', '..', 'roles'))
violations = []
# Helper to load application_id if present
def load_app_id(role_path):
vars_file = os.path.join(role_path, 'vars', 'main.yml')
if not os.path.isfile(vars_file):
return None
data = yaml.safe_load(open(vars_file, encoding='utf-8')) or {}
return data.get('application_id')
# Iterate all roles
for role_path in glob.glob(os.path.join(roles_dir, '*')):
if not os.path.isdir(role_path):
continue
role_name = os.path.basename(role_path)
meta_file = os.path.join(role_path, 'meta', 'main.yml')
if not os.path.isfile(meta_file):
continue
meta = yaml.safe_load(open(meta_file, encoding='utf-8')) or {}
deps = meta.get('dependencies', [])
if not isinstance(deps, list):
continue
# collect just the role names this role depends on
dep_names = []
for item in deps:
if isinstance(item, dict) and 'role' in item:
dep_names.append(item['role'])
elif isinstance(item, str):
dep_names.append(item)
# load application_id for this role once
app_id_role = load_app_id(role_path)
for dep_name in dep_names:
dep_path = os.path.join(roles_dir, dep_name)
if not os.path.isdir(dep_path):
continue
app_id_dep = load_app_id(dep_path)
# only check if both app_ids are defined
if app_id_role is None or app_id_dep is None:
continue
# check role A
if app_id_role != role_name:
violations.append(
f"{role_name}: application_id='{app_id_role}' ≠ folder name '{role_name}'"
)
# check role B
if app_id_dep != dep_name:
violations.append(
f"{dep_name}: application_id='{app_id_dep}' ≠ folder name '{dep_name}'"
)
if violations:
self.fail(
"application_id mismatches between role folder names and vars/main.yml:\n"
+ "\n".join(violations)
)
if __name__ == '__main__':
unittest.main()

View File

@ -18,25 +18,25 @@ class TestApplicationsIfGroupAndDeps(unittest.TestCase):
def setUp(self): def setUp(self):
self.filter = FilterModule().applications_if_group_and_deps self.filter = FilterModule().applications_if_group_and_deps
self.sample_apps = { self.sample_apps = {
'html': {}, 'web-svc-html': {},
'legal': {}, 'web-svc-legal': {},
'file': {}, 'web-svc-file': {},
'asset': {}, 'web-svc-asset': {},
'portfolio': {}, 'web-app-portfolio': {},
'corporate-identity': {}, 'util-srv-corporate-identity': {},
} }
def test_direct_group(self): def test_direct_group(self):
result = self.filter(self.sample_apps, ['html']) result = self.filter(self.sample_apps, ['web-svc-html'])
self.assertIn('html', result) self.assertIn('web-svc-html', result)
self.assertNotIn('legal', result) self.assertNotIn('web-svc-legal', result)
def test_recursive_deps(self): def test_recursive_deps(self):
# html -> depends on none, but corporate-identity pulls in web-svc-legal -> web-svc-html -> legal # html -> depends on none, but util-srv-corporate-identity pulls in web-svc-legal -> web-svc-html -> legal
result = self.filter(self.sample_apps, ['util-srv-corporate-identity']) result = self.filter(self.sample_apps, ['util-srv-corporate-identity'])
self.assertIn('corporate-identity', result) self.assertIn('util-srv-corporate-identity', result)
self.assertIn('legal', result) # via web-svc-legal self.assertIn('web-svc-legal', result) # via web-svc-legal
self.assertIn('html', result) # via web-svc-legal -> html self.assertIn('web-svc-html', result) # via web-svc-legal -> html
def test_real_vars_files(self): def test_real_vars_files(self):
# load real vars to get application_id # load real vars to get application_id