From f3939661e4b920ee6e09853898dcb12a35343254 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Wed, 9 Jul 2025 14:52:51 +0200 Subject: [PATCH] Implemented filter functions to get roles by application_id --- README.md | 2 +- filter_plugins/role_path_by_app_id.py | 88 +++++++++++++++++++ roles/docker-compose/tasks/files.yml | 8 +- .../srv-web-injector-javascript/vars/main.yml | 2 +- .../templates/server.js.j2 | 2 +- .../test_role_path_by_app_id.py | 80 +++++++++++++++++ 6 files changed, 175 insertions(+), 7 deletions(-) create mode 100644 filter_plugins/role_path_by_app_id.py create mode 100644 tests/unit/filter_plugins/test_role_path_by_app_id.py diff --git a/README.md b/README.md index fd5bb4cf..1cd67162 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ More informations about the features you will find [here](docs/overview/Features ### Use it online ๐ŸŒ -Give CyMaIS a spin at cymais.cloud โ€“ sign up in seconds, click around, and see how easy infra magic can be! ๐Ÿš€๐Ÿ”งโœจ +Give CyMaIS a spin at [CyMaIS.cloud](httpy://cymais.cloud) โ€“ sign up in seconds, click around, and see how easy infra magic can be! ๐Ÿš€๐Ÿ”งโœจ ### Install locally ๐Ÿ’ป 1. **Install CyMaIS** via [Kevin's Package Manager](https://github.com/kevinveenbirkenbach/package-manager) diff --git a/filter_plugins/role_path_by_app_id.py b/filter_plugins/role_path_by_app_id.py new file mode 100644 index 00000000..feaf7dba --- /dev/null +++ b/filter_plugins/role_path_by_app_id.py @@ -0,0 +1,88 @@ +# filter_plugins/role_path_by_app_id.py + +import os +import glob +import yaml +from ansible.errors import AnsibleFilterError + + +def abs_role_path_by_application_id(application_id): + """ + Searches all roles/*/vars/main.yml for application_id and returns + the absolute path of the role that matches. Raises an error if + zero or more than one match is found. + """ + base_dir = os.getcwd() + pattern = os.path.join(base_dir, 'roles', '*', 'vars', 'main.yml') + matches = [] + + for filepath in glob.glob(pattern): + try: + with open(filepath, 'r') as f: + data = yaml.safe_load(f) or {} + except Exception: + continue + + if data.get('application_id') == application_id: + role_dir = os.path.dirname(os.path.dirname(filepath)) + abs_path = os.path.abspath(role_dir) + matches.append(abs_path) + + if len(matches) > 1: + raise AnsibleFilterError( + f"Multiple roles found with application_id='{application_id}': {matches}. " + "The application_id must be unique." + ) + if not matches: + raise AnsibleFilterError( + f"No role found with application_id='{application_id}'." + ) + + return matches[0] + + +def rel_role_path_by_application_id(application_id): + """ + Searches all roles/*/vars/main.yml for application_id and returns + the relative path (from the project root) of the role that matches. + Raises an error if zero or more than one match is found. + """ + base_dir = os.getcwd() + pattern = os.path.join(base_dir, 'roles', '*', 'vars', 'main.yml') + matches = [] + + for filepath in glob.glob(pattern): + try: + with open(filepath, 'r') as f: + data = yaml.safe_load(f) or {} + except Exception: + continue + + if data.get('application_id') == application_id: + role_dir = os.path.dirname(os.path.dirname(filepath)) + rel_path = os.path.relpath(role_dir, base_dir) + matches.append(rel_path) + + if len(matches) > 1: + raise AnsibleFilterError( + f"Multiple roles found with application_id='{application_id}': {matches}. " + "The application_id must be unique." + ) + if not matches: + raise AnsibleFilterError( + f"No role found with application_id='{application_id}'." + ) + + return matches[0] + + +class FilterModule(object): + """ + Provides the filters `abs_role_path_by_application_id` and + `rel_role_path_by_application_id`. + """ + def filters(self): + return { + 'abs_role_path_by_application_id': abs_role_path_by_application_id, + 'rel_role_path_by_application_id': rel_role_path_by_application_id, + } diff --git a/roles/docker-compose/tasks/files.yml b/roles/docker-compose/tasks/files.yml index 5a2d5b17..dbf1c037 100644 --- a/roles/docker-compose/tasks/files.yml +++ b/roles/docker-compose/tasks/files.yml @@ -3,8 +3,8 @@ src: "{{ item }}" dest: "{{ docker_compose.files.dockerfile }}" loop: - - "{{ playbook_dir }}/roles/web-app-{{ application_id }}/templates/Dockerfile.j2" - - "{{ playbook_dir }}/roles/web-app-{{ application_id }}/files/Dockerfile" + - "{{ application_id | abs_role_path_by_application_id }}/templates/Dockerfile.j2" + - "{{ application_id | abs_role_path_by_application_id }}/files/Dockerfile" notify: docker compose up register: create_dockerfile_result failed_when: @@ -20,8 +20,8 @@ notify: docker compose up register: env_template loop: - - "{{ playbook_dir }}/roles/web-app-{{ application_id }}/templates/env.j2" - - "{{ playbook_dir }}/roles/web-app-{{ application_id }}/files/env" + - "{{ application_id | abs_role_path_by_application_id }}/templates/env.j2" + - "{{ application_id | abs_role_path_by_application_id }}/files/env" failed_when: - env_template is failed - "'Could not find or access' not in env_template.msg" diff --git a/roles/srv-web-injector-javascript/vars/main.yml b/roles/srv-web-injector-javascript/vars/main.yml index b8a76683..1e72f9bc 100644 --- a/roles/srv-web-injector-javascript/vars/main.yml +++ b/roles/srv-web-injector-javascript/vars/main.yml @@ -1 +1 @@ -modifier_javascript_template_file: "{{ playbook_dir }}/roles/web-app-{{ application_id }}/templates/javascript.js.j2" \ No newline at end of file +modifier_javascript_template_file: "{{ application_id | abs_role_path_by_application_id }}/templates/javascript.js.j2" \ No newline at end of file diff --git a/roles/web-svc-simpleicons/templates/server.js.j2 b/roles/web-svc-simpleicons/templates/server.js.j2 index dd25980b..92772219 100644 --- a/roles/web-svc-simpleicons/templates/server.js.j2 +++ b/roles/web-svc-simpleicons/templates/server.js.j2 @@ -15,7 +15,7 @@ function getExportName(slug) { // Root: redirect to your documentation app.get('/', (req, res) => { - res.redirect('{{ domains | get_url('sphinx', web_protocol) }}/roles/web-app-{{ application_id }}/README.html'); + res.redirect('{{ domains | get_url('sphinx', web_protocol) }}/{{ application_id | rel_role_path_by_application_id}}/README.html'); }); // GET /:slug.svg diff --git a/tests/unit/filter_plugins/test_role_path_by_app_id.py b/tests/unit/filter_plugins/test_role_path_by_app_id.py new file mode 100644 index 00000000..54f1b049 --- /dev/null +++ b/tests/unit/filter_plugins/test_role_path_by_app_id.py @@ -0,0 +1,80 @@ +import os +import tempfile +import shutil +import yaml +import unittest + +from ansible.errors import AnsibleFilterError +from filter_plugins.role_path_by_app_id import ( + abs_role_path_by_application_id, + rel_role_path_by_application_id, +) + + +def write_vars_file(base_dir, role_name, app_id): + """ + Helper to create roles//vars/main.yml with application_id + """ + role_vars_dir = os.path.join(base_dir, 'roles', role_name, 'vars') + os.makedirs(role_vars_dir, exist_ok=True) + file_path = os.path.join(role_vars_dir, 'main.yml') + with open(file_path, 'w') as f: + yaml.safe_dump({'application_id': app_id}, f) + return file_path + + +class TestRolePathByApplicationId(unittest.TestCase): + def setUp(self): + # Create temporary directory for each test and switch cwd + self.tmp_dir = tempfile.mkdtemp() + self.prev_cwd = os.getcwd() + os.chdir(self.tmp_dir) + + def tearDown(self): + # Restore cwd and clean up + os.chdir(self.prev_cwd) + shutil.rmtree(self.tmp_dir) + + def test_abs_single_match(self): + write_vars_file(self.tmp_dir, 'role_one', 'app123') + write_vars_file(self.tmp_dir, 'role_two', 'other_id') + result = abs_role_path_by_application_id('app123') + expected = os.path.abspath(os.path.join(self.tmp_dir, 'roles', 'role_one')) + self.assertEqual(result, expected) + + def test_rel_single_match(self): + write_vars_file(self.tmp_dir, 'role_one', 'app123') + write_vars_file(self.tmp_dir, 'role_two', 'other_id') + result = rel_role_path_by_application_id('app123') + expected = os.path.relpath(os.path.join(self.tmp_dir, 'roles', 'role_one'), self.tmp_dir) + self.assertEqual(result, expected) + + def test_abs_no_match(self): + write_vars_file(self.tmp_dir, 'role_one', 'app123') + with self.assertRaises(AnsibleFilterError) as cm: + abs_role_path_by_application_id('nonexistent') + self.assertIn("No role found with application_id='nonexistent'", str(cm.exception)) + + def test_rel_no_match(self): + write_vars_file(self.tmp_dir, 'role_one', 'app123') + with self.assertRaises(AnsibleFilterError) as cm: + rel_role_path_by_application_id('nonexistent') + self.assertIn("No role found with application_id='nonexistent'", str(cm.exception)) + + def test_abs_multiple_match(self): + write_vars_file(self.tmp_dir, 'role_one', 'dup_id') + write_vars_file(self.tmp_dir, 'role_two', 'dup_id') + with self.assertRaises(AnsibleFilterError) as cm: + abs_role_path_by_application_id('dup_id') + self.assertIn("Multiple roles found with application_id='dup_id'", str(cm.exception)) + + def test_rel_multiple_match(self): + write_vars_file(self.tmp_dir, 'role_one', 'dup_id') + write_vars_file(self.tmp_dir, 'role_two', 'dup_id') + with self.assertRaises(AnsibleFilterError) as cm: + rel_role_path_by_application_id('dup_id') + self.assertIn("Multiple roles found with application_id='dup_id'", str(cm.exception)) + + +if __name__ == '__main__': + unittest.main()