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:
parent
1661120e41
commit
460ea43f8e
2 changed files with 196 additions and 79 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue