Refactor LDAP variable schema to use top-level constant LDAP and nested ALL-CAPS keys.

- Converted group_vars/all/13_ldap.yml from lower-case to ALL-CAPS nested keys.
- Updated all roles, tasks, templates, and filter_plugins to reference LDAP.* instead of ldap.*.
- Fixed Keycloak JSON templates to properly quote Jinja variables.
- Adjusted svc-db-openldap filter plugins and unit tests to handle new LDAP structure.
- Updated integration test to only check uniqueness of TOP-LEVEL ALL-CAPS constants, ignoring nested keys.

See: https://chatgpt.com/share/68b01017-efe0-800f-a508-7d7e2f1c8c8d
This commit is contained in:
2025-08-28 10:15:48 +02:00
parent b9da6908ec
commit cb66fb2978
33 changed files with 238 additions and 249 deletions

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Integration test: ensure every ALL-CAPS variable is defined only once project-wide.
Integration test: ensure every TOP-LEVEL ALL-CAPS variable is defined only once project-wide.
Scope (by design):
- group_vars/**/*.yml
@@ -8,9 +8,11 @@ Scope (by design):
- roles/*/defaults/*.yml
- roles/*/defauls/*.yml # included on purpose in case of folder typos
A variable is considered a “constant” if its key matches: ^[A-Z0-9_]+$
If a constant is declared more than once across the scanned files, the test fails
with a clear message explaining that such constants must be defined only once.
A variable is considered a “constant” if its KEY (at the top level of a YAML document)
matches: ^[A-Z0-9_]+$
Only TOP-LEVEL keys are checked for uniqueness. Nested keys are ignored to allow
namespacing like DICTIONARYA.ENTRY and DICTIONARYB.ENTRY without conflicts.
"""
import os
@@ -41,42 +43,32 @@ def _iter_yaml_files():
seen = set()
for pattern in patterns:
for path in glob.glob(pattern, recursive=True):
# Normalize and deduplicate
norm = os.path.normpath(path)
if norm not in seen and os.path.isfile(norm):
seen.add(norm)
yield norm
def _extract_uppercase_keys_from_mapping(mapping):
def _extract_top_level_uppercase_keys(docs):
"""
Recursively extract ALL-CAPS keys from any YAML mapping.
Returns a set of keys found in this mapping (deduplicated for the file).
Return a set of TOP-LEVEL ALL-CAPS keys found across all mapping documents in a file.
Nested keys are intentionally ignored.
"""
found = set()
def walk(node):
if isinstance(node, dict):
for k, v in node.items():
# Only consider string keys
for doc in docs:
if isinstance(doc, dict):
for k in doc.keys():
if isinstance(k, str) and UPPER_CONST_RE.match(k):
found.add(k)
# Recurse into values to catch nested mappings too
walk(v)
elif isinstance(node, list):
for item in node:
walk(item)
walk(mapping)
return found
class TestUppercaseConstantVarsUnique(unittest.TestCase):
def test_uppercase_constants_unique(self):
# Track where each constant is defined
# Track where each TOP-LEVEL constant is defined
constant_to_files = defaultdict(set)
# Track YAML parse errors to fail fast with a helpful message
# Track YAML parse errors to fail with a helpful message
parse_errors = []
yaml_files = list(_iter_yaml_files())
@@ -88,35 +80,32 @@ class TestUppercaseConstantVarsUnique(unittest.TestCase):
parse_errors.append(f"{yml}: {e}")
continue
# Some files may be empty or contain only comments
if not docs:
continue
# Collect ALL-CAPS keys for this file (dedup per file)
file_constants = set()
for doc in docs:
if isinstance(doc, dict):
file_constants |= _extract_uppercase_keys_from_mapping(doc)
# Non-mapping documents (e.g., lists/None) are ignored
file_constants = _extract_top_level_uppercase_keys(docs)
for const in file_constants:
constant_to_files[const].add(yml)
# Fail if YAML parsing had errors
if parse_errors:
self.fail(
"YAML parsing failed for one or more files:\n"
+ "\n".join(f"- {err}" for err in parse_errors)
)
# Find duplicates (same constant in more than one file)
duplicates = {c: sorted(files) for c, files in constant_to_files.items() if len(files) > 1}
# Duplicates are same TOP-LEVEL constant appearing in >1 files
duplicates = {
c: sorted(files)
for c, files in constant_to_files.items()
if len(files) > 1
}
if duplicates:
msg_lines = [
"Found constants defined more than once. "
"ALL-CAPS variables are treated as constants and must be defined only once project-wide.\n"
"Please consolidate each duplicated constant into a single authoritative location (e.g., one vars/defaults file).",
"Found TOP-LEVEL constants defined more than once. "
"ALL-CAPS top-level variables are treated as constants and must be defined only once project-wide.\n"
"Nested ALL-CAPS keys are allowed and ignored by this test.",
"",
]
for const, files in sorted(duplicates.items()):

View File

@@ -42,19 +42,19 @@ class TestBuildLdapRoleEntries(unittest.TestCase):
}
self.ldap = {
"dn": {
"ou": {
"users": "ou=users,dc=example,dc=org",
"roles": "ou=roles,dc=example,dc=org"
"DN": {
"OU": {
"USERS": "ou=users,dc=example,dc=org",
"ROLES": "ou=roles,dc=example,dc=org"
}
},
"user":{
"attributes": {
"id": "uid"
"USER":{
"ATTRIBUTES": {
"ID": "uid"
}
},
"rbac": {
"flavors": ["posixGroup", "groupOfNames"]
"RBAC": {
"FLAVORS": ["posixGroup", "groupOfNames"]
}
}