mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-11-05 20:58:21 +00:00
Improve get_app_conf robustness and add skip_missing_app parameter support
- Added new optional parameter 'skip_missing_app' to get_app_conf() in module_utils/config_utils.py to safely return defaults when applications are missing. - Updated group_vars/all/00_general.yml and roles/web-app-nextcloud/config/main.yml to include skip_missing_app=True in all Nextcloud-related calls. - Added comprehensive unit tests under tests/unit/module_utils/test_config_utils.py covering missing app handling, schema enforcement, nested lists, and index edge cases. Ref: https://chatgpt.com/share/68ee6b5c-6db0-800f-bc20-d51470d7b39f
This commit is contained in:
@@ -76,8 +76,9 @@ _applications_nextcloud_oidc_flavor: >-
|
|||||||
False,
|
False,
|
||||||
'oidc_login'
|
'oidc_login'
|
||||||
if applications
|
if applications
|
||||||
| get_app_conf('web-app-nextcloud','features.ldap',False, True)
|
| get_app_conf('web-app-nextcloud','features.ldap',False, True, True)
|
||||||
else 'sociallogin'
|
else 'sociallogin',
|
||||||
|
True
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class ConfigEntryNotSetError(AppConfigKeyError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_app_conf(applications, application_id, config_path, strict=True, default=None):
|
def get_app_conf(applications, application_id, config_path, strict=True, default=None, skip_missing_app=False):
|
||||||
# Path to the schema file for this application
|
# Path to the schema file for this application
|
||||||
schema_path = os.path.join('roles', application_id, 'schema', 'main.yml')
|
schema_path = os.path.join('roles', application_id, 'schema', 'main.yml')
|
||||||
|
|
||||||
@@ -133,6 +133,9 @@ def get_app_conf(applications, application_id, config_path, strict=True, default
|
|||||||
try:
|
try:
|
||||||
obj = applications[application_id]
|
obj = applications[application_id]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
if skip_missing_app:
|
||||||
|
# Simply return default instead of failing
|
||||||
|
return default if default is not None else False
|
||||||
raise AppConfigKeyError(
|
raise AppConfigKeyError(
|
||||||
f"Application ID '{application_id}' not found in applications dict.\n"
|
f"Application ID '{application_id}' not found in applications dict.\n"
|
||||||
f"path_trace: {path_trace}\n"
|
f"path_trace: {path_trace}\n"
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ docker:
|
|||||||
mem_reservation: "128m"
|
mem_reservation: "128m"
|
||||||
mem_limit: "512m"
|
mem_limit: "512m"
|
||||||
pids_limit: 256
|
pids_limit: 256
|
||||||
enabled: "{{ applications | get_app_conf('web-app-nextcloud', 'features.oidc', False) }}" # Activate OIDC for Nextcloud
|
enabled: "{{ applications | get_app_conf('web-app-nextcloud', 'features.oidc', False, True, True) }}" # Activate OIDC for Nextcloud
|
||||||
# floavor decides which OICD plugin should be used.
|
# floavor decides which OICD plugin should be used.
|
||||||
# Available options: oidc_login, sociallogin
|
# Available options: oidc_login, sociallogin
|
||||||
# @see https://apps.nextcloud.com/apps/oidc_login
|
# @see https://apps.nextcloud.com/apps/oidc_login
|
||||||
@@ -194,7 +194,7 @@ plugins:
|
|||||||
enabled: false
|
enabled: false
|
||||||
fileslibreofficeedit:
|
fileslibreofficeedit:
|
||||||
# Nextcloud LibreOffice integration: allows online editing of documents with LibreOffice (https://apps.nextcloud.com/apps/fileslibreofficeedit)
|
# Nextcloud LibreOffice integration: allows online editing of documents with LibreOffice (https://apps.nextcloud.com/apps/fileslibreofficeedit)
|
||||||
enabled: "{{ not (applications | get_app_conf('web-app-nextcloud', 'plugins.richdocuments.enabled', False, True)) }}"
|
enabled: "{{ not (applications | get_app_conf('web-app-nextcloud', 'plugins.richdocuments.enabled', False, True, True)) }}"
|
||||||
forms:
|
forms:
|
||||||
# Nextcloud forms: facilitates creation of forms and surveys (https://apps.nextcloud.com/apps/forms)
|
# Nextcloud forms: facilitates creation of forms and surveys (https://apps.nextcloud.com/apps/forms)
|
||||||
enabled: true
|
enabled: true
|
||||||
@@ -292,13 +292,13 @@ plugins:
|
|||||||
# enabled: false
|
# enabled: false
|
||||||
twofactor_nextcloud_notification:
|
twofactor_nextcloud_notification:
|
||||||
# Nextcloud two-factor notification: sends notifications for two-factor authentication events (https://apps.nextcloud.com/apps/twofactor_nextcloud_notification)
|
# Nextcloud two-factor notification: sends notifications for two-factor authentication events (https://apps.nextcloud.com/apps/twofactor_nextcloud_notification)
|
||||||
enabled: "{{ not applications | get_app_conf('web-app-nextcloud', 'features.oidc', False, True) }}" # Deactivate 2FA if oidc is active
|
enabled: "{{ not applications | get_app_conf('web-app-nextcloud', 'features.oidc', False, True, True) }}" # Deactivate 2FA if oidc is active
|
||||||
twofactor_totp:
|
twofactor_totp:
|
||||||
# Nextcloud two-factor TOTP: provides time-based one-time password authentication (https://apps.nextcloud.com/apps/twofactor_totp)
|
# Nextcloud two-factor TOTP: provides time-based one-time password authentication (https://apps.nextcloud.com/apps/twofactor_totp)
|
||||||
enabled: "{{ not applications | get_app_conf('web-app-nextcloud', 'features.oidc', False, True) }}" # Deactivate 2FA if oidc is active
|
enabled: "{{ not applications | get_app_conf('web-app-nextcloud', 'features.oidc', False, True, True) }}" # Deactivate 2FA if oidc is active
|
||||||
user_ldap:
|
user_ldap:
|
||||||
# Nextcloud user LDAP: integrates LDAP for user management and authentication (https://apps.nextcloud.com/apps/user_ldap)
|
# Nextcloud user LDAP: integrates LDAP for user management and authentication (https://apps.nextcloud.com/apps/user_ldap)
|
||||||
enabled: "{{ applications | get_app_conf('web-app-nextcloud', 'features.ldap', False, True) }}"
|
enabled: "{{ applications | get_app_conf('web-app-nextcloud', 'features.ldap', False, True, True) }}"
|
||||||
user_directory:
|
user_directory:
|
||||||
enabled: true # Enables the LDAP User Directory Search
|
enabled: true # Enables the LDAP User Directory Search
|
||||||
user_oidc:
|
user_oidc:
|
||||||
|
|||||||
125
tests/unit/module_utils/test_config_utils.py
Normal file
125
tests/unit/module_utils/test_config_utils.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from module_utils.config_utils import (
|
||||||
|
get_app_conf,
|
||||||
|
AppConfigKeyError,
|
||||||
|
ConfigEntryNotSetError,
|
||||||
|
)
|
||||||
|
|
||||||
|
class TestGetAppConf(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
# Isolate working directory so that schema files can be discovered
|
||||||
|
self._cwd = os.getcwd()
|
||||||
|
self.tmpdir = tempfile.mkdtemp(prefix="cfgutilstest_")
|
||||||
|
os.chdir(self.tmpdir)
|
||||||
|
|
||||||
|
# Minimal schema structure:
|
||||||
|
# roles/web-app-demo/schema/main.yml
|
||||||
|
os.makedirs(os.path.join("roles", "web-app-demo", "schema"), exist_ok=True)
|
||||||
|
with open(os.path.join("roles", "web-app-demo", "schema", "main.yml"), "w") as f:
|
||||||
|
f.write(
|
||||||
|
# Defines 'features.defined_but_unset' in schema (without a value in applications),
|
||||||
|
# plus 'features.oidc' and 'features.nested.list'
|
||||||
|
"features:\n"
|
||||||
|
" oidc: {}\n"
|
||||||
|
" defined_but_unset: {}\n"
|
||||||
|
" nested:\n"
|
||||||
|
" list:\n"
|
||||||
|
" - {}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Example configuration with actual values
|
||||||
|
self.applications = {
|
||||||
|
"web-app-demo": {
|
||||||
|
"features": {
|
||||||
|
"oidc": True,
|
||||||
|
"nested": {
|
||||||
|
"list": ["first", "second"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
os.chdir(self._cwd)
|
||||||
|
shutil.rmtree(self.tmpdir, ignore_errors=True)
|
||||||
|
|
||||||
|
# --- Tests ---
|
||||||
|
|
||||||
|
def test_missing_app_with_skip_missing_app_returns_default_true(self):
|
||||||
|
"""If app ID is missing and skip_missing_app=True, it should return the default (True)."""
|
||||||
|
apps = {"some-other-app": {}}
|
||||||
|
val = get_app_conf(apps, "web-app-nextcloud", "features.oidc",
|
||||||
|
strict=True, default=True, skip_missing_app=True)
|
||||||
|
self.assertTrue(val)
|
||||||
|
|
||||||
|
def test_missing_app_with_skip_missing_app_returns_default_false(self):
|
||||||
|
"""If app ID is missing and skip_missing_app=True, it should return the default (False)."""
|
||||||
|
apps = {"svc-bkp-rmt-2-loc": {}}
|
||||||
|
val = get_app_conf(apps, "web-app-nextcloud", "features.oidc",
|
||||||
|
strict=True, default=False, skip_missing_app=True)
|
||||||
|
self.assertFalse(val)
|
||||||
|
|
||||||
|
def test_missing_app_without_skip_missing_app_and_strict_true_raises(self):
|
||||||
|
"""Missing app ID without skip_missing_app and strict=True should raise."""
|
||||||
|
apps = {}
|
||||||
|
with self.assertRaises(AppConfigKeyError):
|
||||||
|
get_app_conf(apps, "web-app-nextcloud", "features.oidc",
|
||||||
|
strict=True, default=True, skip_missing_app=False)
|
||||||
|
|
||||||
|
def test_missing_app_without_skip_missing_app_and_strict_false_raises(self):
|
||||||
|
apps = {}
|
||||||
|
with self.assertRaises(AppConfigKeyError):
|
||||||
|
get_app_conf(apps, "web-app-nextcloud", "features.oidc",
|
||||||
|
strict=False, default=True, skip_missing_app=False)
|
||||||
|
|
||||||
|
def test_existing_app_returns_expected_value(self):
|
||||||
|
"""Existing app and key should return the configured value."""
|
||||||
|
val = get_app_conf(self.applications, "web-app-demo", "features.oidc",
|
||||||
|
strict=True, default=False, skip_missing_app=False)
|
||||||
|
self.assertTrue(val)
|
||||||
|
|
||||||
|
def test_nested_list_index_access(self):
|
||||||
|
"""Accessing list indices should work correctly."""
|
||||||
|
val0 = get_app_conf(self.applications, "web-app-demo", "features.nested.list[0]",
|
||||||
|
strict=True, default=None, skip_missing_app=False)
|
||||||
|
val1 = get_app_conf(self.applications, "web-app-demo", "features.nested.list[1]",
|
||||||
|
strict=True, default=None, skip_missing_app=False)
|
||||||
|
self.assertEqual(val0, "first")
|
||||||
|
self.assertEqual(val1, "second")
|
||||||
|
|
||||||
|
def test_schema_defined_but_unset_raises_in_strict_mode(self):
|
||||||
|
"""Schema-defined but unset value should raise in strict mode."""
|
||||||
|
with self.assertRaises(ConfigEntryNotSetError):
|
||||||
|
get_app_conf(self.applications, "web-app-demo", "features.defined_but_unset",
|
||||||
|
strict=True, default=False, skip_missing_app=False)
|
||||||
|
|
||||||
|
def test_schema_defined_but_unset_strict_false_returns_default(self):
|
||||||
|
"""Schema-defined but unset value should return default when strict=False."""
|
||||||
|
val = get_app_conf(self.applications, "web-app-demo", "features.defined_but_unset",
|
||||||
|
strict=False, default=True, skip_missing_app=False)
|
||||||
|
self.assertTrue(val)
|
||||||
|
|
||||||
|
def test_invalid_key_format_raises(self):
|
||||||
|
"""Invalid key format in path should raise AppConfigKeyError."""
|
||||||
|
with self.assertRaises(AppConfigKeyError):
|
||||||
|
get_app_conf(self.applications, "web-app-demo", "features.nested.list[not-an-int]",
|
||||||
|
strict=True, default=None, skip_missing_app=False)
|
||||||
|
|
||||||
|
def test_index_out_of_range_respects_strict(self):
|
||||||
|
"""Out-of-range index should respect strict parameter."""
|
||||||
|
# strict=False returns default
|
||||||
|
val = get_app_conf(self.applications, "web-app-demo", "features.nested.list[99]",
|
||||||
|
strict=False, default="fallback", skip_missing_app=False)
|
||||||
|
self.assertEqual(val, "fallback")
|
||||||
|
# strict=True raises
|
||||||
|
with self.assertRaises(AppConfigKeyError):
|
||||||
|
get_app_conf(self.applications, "web-app-demo", "features.nested.list[99]",
|
||||||
|
strict=True, default=None, skip_missing_app=False)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user