1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2026-02-04 07:51:50 +00:00

feat: add support for append parameter in keycloak_realm_localization module

This commit is contained in:
Jakub Danek 2026-02-03 16:12:03 +01:00
parent 1661120e41
commit 460ea43f8e
2 changed files with 196 additions and 79 deletions

View file

@ -29,6 +29,14 @@ attributes:
support: full
options:
append:
description:
- If V(true), only the keys listed in the O(overrides) will be modified by this module. Any other pre-existing
keys will be ignored.
- If V(false), all locale overrides will be made to match configuration of this module. I.e. any keys
missing from the O(overrides) will be removed regardless of O(state) value.
type: bool
default: true
locale:
description:
- Locale code for which the overrides apply (for example, V(en), V(fi), V(de)).
@ -42,9 +50,13 @@ options:
state:
description:
- Desired state of localization overrides for the given locale.
- On V(present), the set of overrides for the locale will be made to match O(overrides) exactly.
- Keys not listed in O(overrides) will be removed, and the listed keys will be created or updated.
- On V(absent), all overrides for the locale will be removed.
- On V(present), the set of overrides for the locale will be made to match O(overrides).
If O(append) is V(false) keys not listed in O(overrides) will be removed,
and the listed keys will be created or updated.
If O(append) is V(true) keys not listed in O(overrides) will be ignored,
and the listed keys will be created or updated.
- On V(absent), overrides for the locale will be removed. If O(append) is V(false), all keys will be removed.
If O(append) is V(true), only the keys listed in O(overrides) will be removed.
type: str
choices: ['present', 'absent']
default: present
@ -163,14 +175,16 @@ end_state:
value: Bye
"""
from copy import deepcopy
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import (
KeycloakAPI,
keycloak_argument_spec,
get_token,
KeycloakError,
get_token,
keycloak_argument_spec,
)
from ansible.module_utils.basic import AnsibleModule
from copy import deepcopy
def _normalize_overrides_from_api(current):
@ -212,6 +226,7 @@ def main():
parent_id=dict(type="str", required=True),
state=dict(type="str", default="present", choices=["present", "absent"]),
overrides=dict(type="list", elements="dict", options=overrides_spec, default=[]),
append=dict(type="bool", default=True),
)
argument_spec.update(meta_args)
@ -239,6 +254,7 @@ def main():
locale = module.params.get("locale")
state = module.params.get("state")
parent_id = module.params.get("parent_id")
append = module.params.get("append")
desired_raw = module.params.get("overrides") or []
desired_map = {r["key"]: r.get("value") for r in desired_raw}
@ -291,6 +307,11 @@ def main():
to_update.append(record)
result["changed"] = True
# ignore any left-overs in to_remove, append is true
if append:
changeset["overrides"].extend(to_remove)
to_remove = []
# Any leftovers in to_remove must be deleted
if to_remove:
result["changed"] = True
@ -325,22 +346,43 @@ def main():
result["end_state"] = {"locale": locale, "overrides": final_overrides}
elif state == "absent":
# Full removal of locale overrides
# touch only overrides listed in parameters, leave the rest be
if append:
to_remove = deepcopy(desired_overrides)
to_keep = deepcopy(old_overrides)
for override in to_remove:
found = False
for keep in to_keep:
if override["key"] == keep["key"]:
to_keep.remove(keep)
found = True
break
# not present
if not found:
to_remove.remove(override)
changeset["overrides"] = to_keep
else:
to_remove = old_overrides
if to_remove:
result["changed"] = True
if module._diff:
result["diff"] = dict(before=before, after=changeset)
if module.check_mode:
if old_overrides:
result["changed"] = True
result["msg"] = "All overrides for locale %s would be deleted." % (locale)
if result["changed"]:
result["msg"] = "Overrides for locale %s would be deleted." % (locale)
else:
result["msg"] = "No overrides for locale %s to be deleted." % (locale)
else:
for override in old_overrides:
for override in to_remove:
kc.delete_localization_value(locale, override["key"], parent_id)
result["changed"] = True
result["msg"] = "Locale %s has no overrides." % (locale)

View file

@ -124,44 +124,6 @@ class TestKeycloakRealmLocalization(ModuleTestCase):
self.assertEqual(mock_del_value.call_count, 0)
self.assertIs(exec_info.exception.args[0]["changed"], False)
def test_present_creates_updates_and_deletes(self):
"""Create missing, update differing, and delete extra overrides."""
module_args = {
"auth_keycloak_url": "http://keycloak.url/auth",
"token": "{{ access_token }}",
"parent_id": "my-realm",
"locale": "en",
"state": "present",
"overrides": [
{"key": "a", "value": "1-new"}, # update
{"key": "c", "value": "3"}, # create
],
}
# Before: a=1, b=2; After: a=1-new, c=3
return_value_get_localization_values = [
{"a": "1", "b": "2"},
{"a": "1-new", "c": "3"},
]
return_value_set = [None, None]
return_value_delete = [None]
with set_module_args(module_args):
with mock_good_connection():
with patch_keycloak_api(
get_localization_values=return_value_get_localization_values,
set_localization_value=return_value_set,
delete_localization_value=return_value_delete,
) as (mock_get_values, mock_set_value, mock_del_value):
with self.assertRaises(AnsibleExitJson) as exec_info:
self.module.main()
self.assertEqual(mock_get_values.call_count, 2)
# One delete for 'b'
self.assertEqual(mock_del_value.call_count, 1)
# Two set calls: update 'a', create 'c'
self.assertEqual(mock_set_value.call_count, 2)
self.assertIs(exec_info.exception.args[0]["changed"], True)
def test_present_check_mode_only_reports(self):
"""Check mode: report changes, do not call API mutators."""
module_args = {
@ -197,34 +159,6 @@ class TestKeycloakRealmLocalization(ModuleTestCase):
self.assertIs(exec_info.exception.args[0]["changed"], True)
self.assertIn("would be updated", exec_info.exception.args[0]["msg"])
def test_absent_deletes_all(self):
"""Remove all overrides when present."""
module_args = {
"auth_keycloak_url": "http://keycloak.url/auth",
"token": "{{ access_token }}",
"parent_id": "my-realm",
"locale": "en",
"state": "absent",
}
return_value_get_localization_values = [
{"k1": "v1", "k2": "v2"},
]
return_value_delete = [None, None]
with set_module_args(module_args):
with mock_good_connection():
with patch_keycloak_api(
get_localization_values=return_value_get_localization_values,
delete_localization_value=return_value_delete,
) as (mock_get_values, mock_set_value, mock_del_value):
with self.assertRaises(AnsibleExitJson) as exec_info:
self.module.main()
self.assertEqual(mock_get_values.call_count, 1)
self.assertEqual(mock_del_value.call_count, 2)
self.assertEqual(mock_set_value.call_count, 0)
self.assertIs(exec_info.exception.args[0]["changed"], True)
def test_absent_idempotent_when_nothing_to_delete(self):
"""No change when locale has no overrides."""
module_args = {
@ -274,6 +208,147 @@ class TestKeycloakRealmLocalization(ModuleTestCase):
self.assertIn("missing required arguments: value", exec_info.exception.args[0]["msg"])
def test_present_append_true_preserves_unspecified_keys(self):
"""With append=True, only modify specified keys, preserve others."""
module_args = {
"auth_keycloak_url": "http://keycloak.url/auth",
"token": "{{ access_token }}",
"parent_id": "my-realm",
"locale": "en",
"state": "present",
"append": True,
"overrides": [
{"key": "a", "value": "1-updated"}, # update existing
{"key": "c", "value": "3"}, # create new
],
}
# Before: a=1, b=2; After: a=1-updated, b=2, c=3 (b is preserved)
return_value_get_localization_values = [
{"a": "1", "b": "2"},
{"a": "1-updated", "b": "2", "c": "3"},
]
return_value_set = [None, None]
with set_module_args(module_args):
with mock_good_connection():
with patch_keycloak_api(
get_localization_values=return_value_get_localization_values,
set_localization_value=return_value_set,
) as (mock_get_values, mock_set_value, mock_del_value):
with self.assertRaises(AnsibleExitJson) as exec_info:
self.module.main()
self.assertEqual(mock_get_values.call_count, 2)
# No deletes - key 'b' should be preserved
self.assertEqual(mock_del_value.call_count, 0)
# Two set calls: update 'a', create 'c'
self.assertEqual(mock_set_value.call_count, 2)
self.assertIs(exec_info.exception.args[0]["changed"], True)
def test_present_append_false_removes_unspecified_keys(self):
"""With append=False, create new, update existing, and delete unspecified keys."""
module_args = {
"auth_keycloak_url": "http://keycloak.url/auth",
"token": "{{ access_token }}",
"parent_id": "my-realm",
"locale": "en",
"state": "present",
"append": False,
"overrides": [
{"key": "a", "value": "1-updated"}, # update
{"key": "c", "value": "3"}, # create
],
}
# Before: a=1, b=2; After: a=1-updated, c=3 (b is removed)
return_value_get_localization_values = [
{"a": "1", "b": "2"},
{"a": "1-updated", "c": "3"},
]
return_value_set = [None, None]
return_value_delete = [None]
with set_module_args(module_args):
with mock_good_connection():
with patch_keycloak_api(
get_localization_values=return_value_get_localization_values,
set_localization_value=return_value_set,
delete_localization_value=return_value_delete,
) as (mock_get_values, mock_set_value, mock_del_value):
with self.assertRaises(AnsibleExitJson) as exec_info:
self.module.main()
self.assertEqual(mock_get_values.call_count, 2)
# One delete for 'b'
self.assertEqual(mock_del_value.call_count, 1)
# Two set calls: update 'a', create 'c'
self.assertEqual(mock_set_value.call_count, 2)
self.assertIs(exec_info.exception.args[0]["changed"], True)
def test_absent_append_true_removes_only_specified_keys(self):
"""With state=absent and append=True, remove only specified keys."""
module_args = {
"auth_keycloak_url": "http://keycloak.url/auth",
"token": "{{ access_token }}",
"parent_id": "my-realm",
"locale": "en",
"state": "absent",
"append": True,
"overrides": [
{"key": "a"},
],
}
# Before: a=1, b=2; Remove only 'a', keep 'b'
return_value_get_localization_values = [
{"a": "1", "b": "2"},
]
return_value_delete = [None]
with set_module_args(module_args):
with mock_good_connection():
with patch_keycloak_api(
get_localization_values=return_value_get_localization_values,
delete_localization_value=return_value_delete,
) as (mock_get_values, mock_set_value, mock_del_value):
with self.assertRaises(AnsibleExitJson) as exec_info:
self.module.main()
self.assertEqual(mock_get_values.call_count, 1)
# One delete for 'a' only
self.assertEqual(mock_del_value.call_count, 1)
self.assertEqual(mock_set_value.call_count, 0)
self.assertIs(exec_info.exception.args[0]["changed"], True)
def test_absent_append_false_removes_all_keys(self):
"""With state=absent and append=False, remove all keys."""
module_args = {
"auth_keycloak_url": "http://keycloak.url/auth",
"token": "{{ access_token }}",
"parent_id": "my-realm",
"locale": "en",
"state": "absent",
"append": False,
}
# Before: a=1, b=2; Remove all
return_value_get_localization_values = [
{"a": "1", "b": "2"},
]
return_value_delete = [None, None]
with set_module_args(module_args):
with mock_good_connection():
with patch_keycloak_api(
get_localization_values=return_value_get_localization_values,
delete_localization_value=return_value_delete,
) as (mock_get_values, mock_set_value, mock_del_value):
with self.assertRaises(AnsibleExitJson) as exec_info:
self.module.main()
self.assertEqual(mock_get_values.call_count, 1)
# Two deletes for 'a' and 'b'
self.assertEqual(mock_del_value.call_count, 2)
self.assertEqual(mock_set_value.call_count, 0)
self.assertIs(exec_info.exception.args[0]["changed"], True)
if __name__ == "__main__":
unittest.main()