From 5ea12f5471cdfd6f4caa7433fd3fdd964db4b688 Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Tue, 26 Aug 2025 12:03:44 +0200 Subject: [PATCH 01/40] add support for management of keycloak localizations --- .../identity/keycloak/keycloak.py | 78 ++++ .../modules/keycloak_realm_localization.py | 375 ++++++++++++++++++ 2 files changed, 453 insertions(+) create mode 100644 plugins/modules/keycloak_realm_localization.py diff --git a/plugins/module_utils/identity/keycloak/keycloak.py b/plugins/module_utils/identity/keycloak/keycloak.py index 70cf627e33a..a42241e3d70 100644 --- a/plugins/module_utils/identity/keycloak/keycloak.py +++ b/plugins/module_utils/identity/keycloak/keycloak.py @@ -21,6 +21,9 @@ URL_REALM = "{url}/admin/realms/{realm}" URL_REALM_KEYS_METADATA = "{url}/admin/realms/{realm}/keys" +URL_LOCALIZATIONS = "{url}/admin/realms/{realm}/localization/{locale}" +URL_LOCALIZATION = "{url}/admin/realms/{realm}/localization/{locale}/{key}" + URL_TOKEN = "{url}/realms/{realm}/protocol/openid-connect/token" URL_CLIENT = "{url}/admin/realms/{realm}/clients/{id}" URL_CLIENTS = "{url}/admin/realms/{realm}/clients" @@ -568,6 +571,81 @@ def delete_realm(self, realm="master"): self.fail_request(e, msg='Could not delete realm %s: %s' % (realm, str(e)), exception=traceback.format_exc()) + def get_localization_values(self, locale, realm="master"): + """ + Get all localization overrides for a given realm and locale. + + Parameters: + locale (str): Locale code (for example, 'en', 'fi', 'de'). + realm (str): Realm name. Defaults to 'master'. + + Returns: + dict[str, str]: Mapping of localization keys to override values. + + Raises: + KeycloakError: Wrapped HTTP/JSON error with context via fail_open_url(). + """ + realm_url = URL_LOCALIZATIONS.format(url=self.baseurl, realm=realm, locale=locale) + + try: + return json.loads(to_native(open_url(realm_url, method='GET', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, + validate_certs=self.validate_certs).read())) + except Exception as e: + self.fail_open_url(e, msg='Could not read localization overrides for realm %s, locale %s: %s' % (realm, locale, str(e)), + exception=traceback.format_exc()) + + def set_localization_value(self, locale, key, value, realm="master"): + """ + Create or update a single localization override for the given key. + + Parameters: + locale (str): Locale code (for example, 'en'). + key (str): Localization message key to set. + value (str): Override value to set. + realm (str): Realm name. Defaults to 'master'. + + Returns: + HTTPResponse: Response object on success. + + Raises: + KeycloakError: Wrapped HTTP error with context via fail_open_url(). + """ + realm_url = URL_LOCALIZATION.format(url=self.baseurl, realm=realm, locale=locale, key=key) + + headers = self.restheaders.copy() + headers['Content-Type'] = 'text/plain; charset=utf-8' + + try: + return open_url(realm_url, method='PUT', http_agent=self.http_agent, headers=headers, timeout=self.connection_timeout, + data=to_native(value), validate_certs=self.validate_certs) + except Exception as e: + self.fail_open_url(e, msg='Could not set localization value in realm %s, locale %s: %s=%s: %s' % (realm, locale, key, value, str(e)), + exception=traceback.format_exc()) + + def delete_localization_value(self, locale, key, realm="master"): + """ + Delete a single localization override key for the given locale. + + Parameters: + locale (str): Locale code (for example, 'en'). + key (str): Localization message key to delete. + realm (str): Realm name. Defaults to 'master'. + + Returns: + HTTPResponse: Response object on success. + + Raises: + KeycloakError: Wrapped HTTP error with context via fail_open_url(). + """ + realm_url = URL_LOCALIZATION.format(url=self.baseurl, realm=realm, locale=locale, key=key) + + try: + return open_url(realm_url, method='DELETE', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, + validate_certs=self.validate_certs) + except Exception as e: + self.fail_open_url(e, msg='Could not delete localization value in realm %s, locale %s, key %s: %s' % (realm, locale, key, str(e)), + exception=traceback.format_exc()) + def get_clients(self, realm='master', filter=None): """ Obtains client representations for clients in a realm diff --git a/plugins/modules/keycloak_realm_localization.py b/plugins/modules/keycloak_realm_localization.py new file mode 100644 index 00000000000..d963fc56a9d --- /dev/null +++ b/plugins/modules/keycloak_realm_localization.py @@ -0,0 +1,375 @@ +# Python +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# This Ansible module manages realm localization overrides in Keycloak. +# It ensures the set of message key/value overrides for a given locale +# either matches the provided list (state=present) or is fully removed (state=absent). + +# Copyright (c) 2025, Jakub Danek +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or +# https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: keycloak_realm_localization + +short_description: Manage Keycloak realm localization overrides via the Keycloak API + +version_added: 10.5.0 + +description: + - Manage per-locale message overrides for a Keycloak realm using the Keycloak Admin REST API. + Requires access via OpenID Connect; the connecting user/client must have sufficient privileges. + - The names of module options are snake_cased versions of the names found in the Keycloak API. + +attributes: + check_mode: + support: full + diff_mode: + support: full + +options: + locale: + description: + - Locale code for which the overrides apply (for example, C(en), C(fi), C(de)). + type: str + required: true + parent_id: + description: + - Name of the realm that owns the locale overrides. + type: str + required: true + state: + description: + - Desired state of localization overrides for the given locale. + - On C(present), the set of overrides for the locale will be made to match C(overrides) exactly: + keys not listed in C(overrides) will be removed, and listed keys will be created or updated. + - On C(absent), all overrides for the locale will be removed. + type: str + choices: [present, absent] + default: present + overrides: + description: + - List of overrides to ensure for the locale when C(state=present). Each item is a mapping with + the message C(key) and its C(value). + - Ignored when C(state=absent). + type: list + elements: dict + default: [] + suboptions: + key: + description: + - The message key to override. + type: str + required: true + value: + description: + - The override value for the message key. + type: str + required: true + +extends_documentation_fragment: + - community.general.keycloak + - community.general.attributes + +author: + - Jakub Danek (@danekja) +''' + +EXAMPLES = ''' +- name: Replace all overrides for locale "en" (credentials auth) + community.general.keycloak_realm_localization: + auth_client_id: admin-cli + auth_keycloak_url: https://auth.example.com/auth + auth_realm: master + auth_username: USERNAME + auth_password: PASSWORD + parent_id: my-realm + locale: en + state: present + overrides: + - key: greeting + value: "Hello" + - key: farewell + value: "Bye" + delegate_to: localhost + +- name: Ensure only one override exists for locale "fi" (token auth) + community.general.keycloak_realm_localization: + auth_client_id: admin-cli + auth_keycloak_url: https://auth.example.com/auth + token: TOKEN + parent_id: my-realm + locale: fi + state: present + overrides: + - key: app.title + value: "Sovellukseni" + delegate_to: localhost + +- name: Remove all overrides for locale "de" + community.general.keycloak_realm_localization: + auth_client_id: admin-cli + auth_keycloak_url: https://auth.example.com/auth + auth_realm: master + auth_username: USERNAME + auth_password: PASSWORD + parent_id: my-realm + locale: de + state: absent + delegate_to: localhost + +- name: Dry run: see what would change for locale "en" + community.general.keycloak_realm_localization: + auth_client_id: admin-cli + auth_keycloak_url: https://auth.example.com/auth + auth_realm: master + auth_username: USERNAME + auth_password: PASSWORD + parent_id: my-realm + locale: en + state: present + overrides: + - key: greeting + value: "Hello again" + check_mode: true + delegate_to: localhost +''' + +RETURN = ''' +msg: + description: Human-readable message about what action was taken. + returned: always + type: str +end_state: + description: + - Final state of localization overrides for the locale after module execution. + - Contains the C(locale) and the list of C(overrides) as key/value items. + returned: on success + type: dict + contains: + locale: + description: The locale code affected. + type: str + sample: en + overrides: + description: The list of overrides that exist after execution. + type: list + elements: dict + sample: + - key: greeting + value: Hello + - key: farewell + value: Bye +diff: + description: + - When run with C(--diff), shows the before and after structures + for the locale and its overrides. + returned: when supported and requested + type: dict + contains: + before: + type: dict + after: + type: dict +''' + + +from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, \ + keycloak_argument_spec, get_token, KeycloakError +from ansible.module_utils.basic import AnsibleModule +from copy import deepcopy + +def _normalize_overrides_from_api(current): + """ + Accept either: + - dict: {'k1': 'v1', ...} + - list of dicts: [{'key': 'k1', 'value': 'v1'}, ...] + Return a sorted list of {'key', 'value'}. + + This helper provides a consistent shape for downstream comparison/diff logic. + """ + if not current: + return [] + + if isinstance(current, dict): + # Convert mapping to list of key/value dicts + items = [{'key': k, 'value': v} for k, v in current.items()] + else: + # Assume a list of dicts with keys 'key' and 'value' + items = [{'key': o['key'], 'value': o.get('value')} for o in current] + + # Sort for stable comparisons and diff output + return sorted(items, key=lambda x: x['key']) + + +def main(): + """ + Module execution + + :return: + """ + # Base Keycloak auth/spec fragment common across Keycloak modules + argument_spec = keycloak_argument_spec() + + # Describe a single override record + overrides_spec = dict( + key=dict(type='str', required=True), + value=dict(type='str', required=True), + ) + + # Module-specific arguments + meta_args = dict( + locale=dict(type='str', required=True), + 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=[]), + ) + + argument_spec.update(meta_args) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True, + # Require token OR full credential set. This mirrors other Keycloak modules. + required_one_of=([['token', 'auth_realm', 'auth_username', 'auth_password']]), + required_together=([['auth_realm', 'auth_username', 'auth_password']])) + + # Initialize the result object used by Ansible + result = dict(changed=False, msg='', end_state={}, diff=dict(before={}, after={})) + + # Obtain access token, initialize API + try: + connection_header = get_token(module.params) + except KeycloakError as e: + module.fail_json(msg=str(e)) + + kc = KeycloakAPI(module, connection_header) + + # Convenience locals for frequently used parameters + locale = module.params.get('locale') + state = module.params.get('state') + parent_id = module.params.get('parent_id') + + # Desired overrides: deduplicate by key via dict (last wins), then sort + desired_raw = module.params.get('overrides') or [] + if state == 'present': + # Validate that all keys have a value in present mode + missing_values = [r['key'] for r in desired_raw if r.get('value') is None] + if missing_values: + module.fail_json(msg="state=present requires 'value' for keys: %s" % ", ".join(missing_values)) + + desired_map = {r['key']: r.get('value') for r in desired_raw} + desired_overrides = [{'key': k, 'value': v} for k, v in sorted(desired_map.items())] + + # Fetch current overrides and normalize to comparable structure + old_overrides = _normalize_overrides_from_api(kc.get_localization_values(locale, parent_id) or {}) + before = { + 'locale': locale, + 'overrides': deepcopy(old_overrides), + } + + # Proposed state used for diff reporting + changeset = { + 'locale': locale, + 'overrides': [], + } + + # Default to no change; flip to True when updates/deletes are needed + result['changed'] = False + + if state == 'present': + + changeset['overrides'] = deepcopy(desired_overrides) + + # Compute two sets: + # - to_update: keys missing or with different values + # - to_remove: keys existing in current state but not in desired + to_update = [] + to_remove = old_overrides.copy() + + # Mark updates and remove matched ones from to_remove + for record in desired_overrides: + override_found = False + + for override in to_remove: + + if override['key'] == record['key']: + override_found = True + + # Value differs -> update needed + if override['value'] != record['value']: + result['changed'] = True + to_update.append(record) + + # Remove processed item so what's left in to_remove are deletions + to_remove.remove(override) + break + + if not override_found: + # New key, must be created + to_update.append(record) + result['changed'] = True + + # Any leftovers in to_remove must be deleted + if len(to_remove) > 0: + result['changed'] = True + + if result['changed']: + if module._diff: + result['diff'] = dict(before=before, after=changeset) + + if module.check_mode: + # Dry-run: report intent without side effects + result['msg'] = "Locale %s overrides would be updated." % (locale) + + else: + + for override in to_remove: + kc.delete_localization_value(locale, override['key'], parent_id) + + for override in to_update: + kc.set_localization_value(locale, override['key'], override['value'], parent_id) + + result['msg'] = "Locale %s overrides have been updated." % (locale) + + else: + result['msg'] = "Locale %s overrides are in sync." % (locale) + + # For accurate end_state, read back from API unless we are in check_mode + final_overrides = _normalize_overrides_from_api(kc.get_localization_values(locale, parent_id) or {}) if not module.check_mode else changeset['overrides'] + result['end_state'] = {'locale': locale, 'overrides': final_overrides} + + elif state == 'absent': + # Full removal of locale overrides + + if module._diff: + result['diff'] = dict(before=before, after=changeset) + + if module.check_mode: + + if len(old_overrides) > 0: + result['changed'] = True + result['msg'] = "All 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: + kc.delete_localization_value(locale, override['key'], parent_id) + result['changed'] = True + + result['msg'] = "Locale %s has no overrides." % (locale) + + result['end_state'] = changeset + + + module.exit_json(**result) + + +if __name__ == '__main__': + main() From 3ab68566dd53356c582116fcee31cadad5717137 Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 19 Sep 2025 12:55:13 +0200 Subject: [PATCH 02/40] unit test for keycloak localization support --- .../test_keycloak_realm_localization.py | 262 ++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 tests/unit/plugins/modules/test_keycloak_realm_localization.py diff --git a/tests/unit/plugins/modules/test_keycloak_realm_localization.py b/tests/unit/plugins/modules/test_keycloak_realm_localization.py new file mode 100644 index 00000000000..6849a84fbdc --- /dev/null +++ b/tests/unit/plugins/modules/test_keycloak_realm_localization.py @@ -0,0 +1,262 @@ +# Python +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from contextlib import contextmanager + +from ansible_collections.community.internal_test_tools.tests.unit.compat import unittest +from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch +from ansible_collections.community.internal_test_tools.tests.unit.plugins.modules.utils import ( + AnsibleExitJson, + AnsibleFailJson, + ModuleTestCase, + set_module_args, +) + +from ansible_collections.community.general.plugins.modules import keycloak_realm_localization + +from itertools import count + +from ansible.module_utils.six import StringIO + + +@contextmanager +def patch_keycloak_api(get_localization_values=None, set_localization_value=None, delete_localization_value=None): + """ + Patch KeycloakAPI methods used by the module under test. + """ + obj = keycloak_realm_localization.KeycloakAPI + with patch.object(obj, 'get_localization_values', side_effect=get_localization_values) as mock_get_values: + with patch.object(obj, 'set_localization_value', side_effect=set_localization_value) as mock_set_value: + with patch.object(obj, 'delete_localization_value', side_effect=delete_localization_value) as mock_del_value: + yield mock_get_values, mock_set_value, mock_del_value + + +def get_response(object_with_future_response, method, get_id_call_count): + if callable(object_with_future_response): + return object_with_future_response() + if isinstance(object_with_future_response, dict): + return get_response( + object_with_future_response[method], method, get_id_call_count) + if isinstance(object_with_future_response, list): + call_number = next(get_id_call_count) + return get_response( + object_with_future_response[call_number], method, get_id_call_count) + return object_with_future_response + + +def build_mocked_request(get_id_user_count, response_dict): + def _mocked_requests(*args, **kwargs): + url = args[0] + method = kwargs['method'] + future_response = response_dict.get(url, None) + return get_response(future_response, method, get_id_user_count) + return _mocked_requests + + +def create_wrapper(text_as_string): + """Allow to mock many times a call to one address. + Without this function, the StringIO is empty for the second call. + """ + def _create_wrapper(): + return StringIO(text_as_string) + return _create_wrapper + + +def mock_good_connection(): + token_response = { + 'http://keycloak.url/auth/realms/master/protocol/openid-connect/token': create_wrapper('{"access_token": "alongtoken"}'), + } + return patch( + 'ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak.open_url', + side_effect=build_mocked_request(count(), token_response), + autospec=True + ) + + +class TestKeycloakRealmLocalization(ModuleTestCase): + def setUp(self): + super(TestKeycloakRealmLocalization, self).setUp() + self.module = keycloak_realm_localization + + def test_present_no_change_in_sync(self): + """Desired overrides already match, no change.""" + module_args = { + 'auth_keycloak_url': 'http://keycloak.url/auth', + 'token': '{{ access_token }}', + 'parent_id': 'my-realm', + 'locale': 'en', + 'state': 'present', + 'overrides': [ + {'key': 'greeting', 'value': 'Hello'}, + {'key': 'farewell', 'value': 'Bye'}, + ], + } + # get_localization_values is called twice: before and after + return_value_get_localization_values = [ + {'greeting': 'Hello', 'farewell': 'Bye'}, + {'greeting': 'Hello', 'farewell': 'Bye'}, + ] + + with set_module_args(module_args): + with mock_good_connection(): + with patch_keycloak_api(get_localization_values=return_value_get_localization_values) \ + 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) + self.assertEqual(mock_set_value.call_count, 0) + 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 = { + 'auth_keycloak_url': 'http://keycloak.url/auth', + 'token': '{{ access_token }}', + 'parent_id': 'my-realm', + 'locale': 'en', + 'state': 'present', + 'overrides': [ + {'key': 'x', 'value': '1'}, # change + {'key': 'y', 'value': '2'}, # create + ], + '_ansible_check_mode': True, # signal for readers; set_module_args is what matters + } + return_value_get_localization_values = [ + {'x': '0'}, + ] + + with set_module_args(module_args): + with mock_good_connection(): + with patch_keycloak_api(get_localization_values=return_value_get_localization_values) \ + as (mock_get_values, mock_set_value, mock_del_value): + with self.assertRaises(AnsibleExitJson) as exec_info: + self.module.main() + + # Only read current values + self.assertEqual(mock_get_values.call_count, 1) + self.assertEqual(mock_set_value.call_count, 0) + self.assertEqual(mock_del_value.call_count, 0) + 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 = { + 'auth_keycloak_url': 'http://keycloak.url/auth', + 'token': '{{ access_token }}', + 'parent_id': 'my-realm', + 'locale': 'en', + 'state': 'absent', + } + return_value_get_localization_values = [ + {}, + ] + + with set_module_args(module_args): + with mock_good_connection(): + with patch_keycloak_api(get_localization_values=return_value_get_localization_values) \ + 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, 0) + self.assertEqual(mock_set_value.call_count, 0) + self.assertIs(exec_info.exception.args[0]['changed'], False) + + def test_present_missing_value_validation(self): + """Validation error when state=present and value is missing.""" + module_args = { + 'auth_keycloak_url': 'http://keycloak.url/auth', + 'token': '{{ access_token }}', + 'parent_id': 'my-realm', + 'locale': 'en', + 'state': 'present', + 'overrides': [ + {'key': 'greeting', 'value': None}, + ], + } + + with set_module_args(module_args): + with mock_good_connection(): + with patch_keycloak_api() \ + as (_mock_get_values, _mock_set_value, _mock_del_value): + with self.assertRaises(AnsibleFailJson) as exec_info: + self.module.main() + + self.assertIn("requires 'value' for keys", exec_info.exception.args[0]['msg']) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From a5f27850a7df970216579549e9926777871695b2 Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 19 Sep 2025 13:27:32 +0200 Subject: [PATCH 03/40] keycloak_realm_localization botmeta record --- .github/BOTMETA.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index b498cdf9ea1..d5e3482f9ed 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -851,6 +851,8 @@ files: maintainers: fynncfchen $modules/keycloak_realm_key.py: maintainers: mattock + $modules/keycloak_realm_localization.py: + maintainers: danekja $modules/keycloak_role.py: maintainers: laurpaum $modules/keycloak_user.py: From c144001b871cb9ae9f5bb32e786eae8b3e3ba26e Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 19 Sep 2025 13:55:49 +0200 Subject: [PATCH 04/40] update implementation to latest version of community.general --- .../identity/keycloak/keycloak.py | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/plugins/module_utils/identity/keycloak/keycloak.py b/plugins/module_utils/identity/keycloak/keycloak.py index a42241e3d70..e7235bc50de 100644 --- a/plugins/module_utils/identity/keycloak/keycloak.py +++ b/plugins/module_utils/identity/keycloak/keycloak.py @@ -370,7 +370,7 @@ def __init__(self, module, connection_header): self.restheaders = connection_header self.http_agent = self.module.params.get('http_agent') - def _request(self, url, method, data=None): + def _request(self, url, method, data=None, headers=None): """ Makes a request to Keycloak and returns the raw response. If a 401 is returned, attempts to re-authenticate using first the module's refresh_token (if provided) @@ -381,12 +381,16 @@ def _request(self, url, method, data=None): :param url: request path :param method: request method (e.g., 'GET', 'POST', etc.) :param data: (optional) data for request + :param headers headers to be sent with request, defaults to self.restheaders :return: raw API response """ def make_request_catching_401(): try: + if headers is None: + headers = self.restheaders + return open_url(url, method=method, data=data, - http_agent=self.http_agent, headers=self.restheaders, + http_agent=self.http_agent, headers=headers, timeout=self.connection_timeout, validate_certs=self.validate_certs) except HTTPError as e: @@ -583,15 +587,14 @@ def get_localization_values(self, locale, realm="master"): dict[str, str]: Mapping of localization keys to override values. Raises: - KeycloakError: Wrapped HTTP/JSON error with context via fail_open_url(). + KeycloakError: Wrapped HTTP/JSON error with context """ realm_url = URL_LOCALIZATIONS.format(url=self.baseurl, realm=realm, locale=locale) try: - return json.loads(to_native(open_url(realm_url, method='GET', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) + return self._request_and_deserialize(realm_url, method='GET') except Exception as e: - self.fail_open_url(e, msg='Could not read localization overrides for realm %s, locale %s: %s' % (realm, locale, str(e)), + self.fail_request(e, msg='Could not read localization overrides for realm %s, locale %s: %s' % (realm, locale, str(e)), exception=traceback.format_exc()) def set_localization_value(self, locale, key, value, realm="master"): @@ -608,7 +611,7 @@ def set_localization_value(self, locale, key, value, realm="master"): HTTPResponse: Response object on success. Raises: - KeycloakError: Wrapped HTTP error with context via fail_open_url(). + KeycloakError: Wrapped HTTP error with context """ realm_url = URL_LOCALIZATION.format(url=self.baseurl, realm=realm, locale=locale, key=key) @@ -616,10 +619,9 @@ def set_localization_value(self, locale, key, value, realm="master"): headers['Content-Type'] = 'text/plain; charset=utf-8' try: - return open_url(realm_url, method='PUT', http_agent=self.http_agent, headers=headers, timeout=self.connection_timeout, - data=to_native(value), validate_certs=self.validate_certs) + return self._request(realm_url, method='PUT', data=to_native(value), headers=headers) except Exception as e: - self.fail_open_url(e, msg='Could not set localization value in realm %s, locale %s: %s=%s: %s' % (realm, locale, key, value, str(e)), + self.fail_request(e, msg='Could not set localization value in realm %s, locale %s: %s=%s: %s' % (realm, locale, key, value, str(e)), exception=traceback.format_exc()) def delete_localization_value(self, locale, key, realm="master"): @@ -635,15 +637,14 @@ def delete_localization_value(self, locale, key, realm="master"): HTTPResponse: Response object on success. Raises: - KeycloakError: Wrapped HTTP error with context via fail_open_url(). + KeycloakError: Wrapped HTTP error with context """ realm_url = URL_LOCALIZATION.format(url=self.baseurl, realm=realm, locale=locale, key=key) try: - return open_url(realm_url, method='DELETE', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - validate_certs=self.validate_certs) + return self._request(realm_url, method='DELETE') except Exception as e: - self.fail_open_url(e, msg='Could not delete localization value in realm %s, locale %s, key %s: %s' % (realm, locale, key, str(e)), + self.fail_request(e, msg='Could not delete localization value in realm %s, locale %s, key %s: %s' % (realm, locale, key, str(e)), exception=traceback.format_exc()) def get_clients(self, realm='master', filter=None): From 07ba82a4ca568ee3a0095e581eb660dfc5f4c7a2 Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 19 Sep 2025 13:56:25 +0200 Subject: [PATCH 05/40] changelog for module_utils/identity/keycloak - KeycloakAPI._request signature change --- .../fragments/10841-keycloak-add-optional-headers-request.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/10841-keycloak-add-optional-headers-request.yml diff --git a/changelogs/fragments/10841-keycloak-add-optional-headers-request.yml b/changelogs/fragments/10841-keycloak-add-optional-headers-request.yml new file mode 100644 index 00000000000..356c266cb3e --- /dev/null +++ b/changelogs/fragments/10841-keycloak-add-optional-headers-request.yml @@ -0,0 +1,2 @@ +minor_changes: + - keycloak - adds optional headers parameter override for _request method in module_utils/identity/keycloak (https://github.com/ansible-collections/community.general/pull/10841). \ No newline at end of file From 8936806152046fd658950810ead0b3d13a676246 Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Thu, 25 Sep 2025 14:58:41 +0200 Subject: [PATCH 06/40] fix indentation, documentation and other sanity test findings --- meta/runtime.yml | 1 + .../identity/keycloak/keycloak.py | 6 +-- .../modules/keycloak_realm_localization.py | 52 ++++++++++++------- .../test_keycloak_realm_localization.py | 2 +- 4 files changed, 39 insertions(+), 22 deletions(-) diff --git a/meta/runtime.yml b/meta/runtime.yml index f5cb6892871..a90840f7dcd 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -40,6 +40,7 @@ action_groups: - keycloak_realm - keycloak_realm_key - keycloak_realm_keys_metadata_info + - keycloak_realm_localization - keycloak_realm_rolemapping - keycloak_role - keycloak_user diff --git a/plugins/module_utils/identity/keycloak/keycloak.py b/plugins/module_utils/identity/keycloak/keycloak.py index e7235bc50de..05accaf0d35 100644 --- a/plugins/module_utils/identity/keycloak/keycloak.py +++ b/plugins/module_utils/identity/keycloak/keycloak.py @@ -595,7 +595,7 @@ def get_localization_values(self, locale, realm="master"): return self._request_and_deserialize(realm_url, method='GET') except Exception as e: self.fail_request(e, msg='Could not read localization overrides for realm %s, locale %s: %s' % (realm, locale, str(e)), - exception=traceback.format_exc()) + exception=traceback.format_exc()) def set_localization_value(self, locale, key, value, realm="master"): """ @@ -622,7 +622,7 @@ def set_localization_value(self, locale, key, value, realm="master"): return self._request(realm_url, method='PUT', data=to_native(value), headers=headers) except Exception as e: self.fail_request(e, msg='Could not set localization value in realm %s, locale %s: %s=%s: %s' % (realm, locale, key, value, str(e)), - exception=traceback.format_exc()) + exception=traceback.format_exc()) def delete_localization_value(self, locale, key, realm="master"): """ @@ -645,7 +645,7 @@ def delete_localization_value(self, locale, key, realm="master"): return self._request(realm_url, method='DELETE') except Exception as e: self.fail_request(e, msg='Could not delete localization value in realm %s, locale %s, key %s: %s' % (realm, locale, key, str(e)), - exception=traceback.format_exc()) + exception=traceback.format_exc()) def get_clients(self, realm='master', filter=None): """ Obtains client representations for clients in a realm diff --git a/plugins/modules/keycloak_realm_localization.py b/plugins/modules/keycloak_realm_localization.py index d963fc56a9d..c2179ebff0d 100644 --- a/plugins/modules/keycloak_realm_localization.py +++ b/plugins/modules/keycloak_realm_localization.py @@ -1,5 +1,5 @@ # Python -#!/usr/bin/python +# !/usr/bin/python # -*- coding: utf-8 -*- # This Ansible module manages realm localization overrides in Keycloak. @@ -13,8 +13,7 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -DOCUMENTATION = ''' ---- +DOCUMENTATION = r""" module: keycloak_realm_localization short_description: Manage Keycloak realm localization overrides via the Keycloak API @@ -23,7 +22,7 @@ description: - Manage per-locale message overrides for a Keycloak realm using the Keycloak Admin REST API. - Requires access via OpenID Connect; the connecting user/client must have sufficient privileges. + - Requires access via OpenID Connect; the connecting user/client must have sufficient privileges. - The names of module options are snake_cased versions of the names found in the Keycloak API. attributes: @@ -46,16 +45,16 @@ state: description: - Desired state of localization overrides for the given locale. - - On C(present), the set of overrides for the locale will be made to match C(overrides) exactly: - keys not listed in C(overrides) will be removed, and listed keys will be created or updated. + - On C(present), the set of overrides for the locale will be made to match C(overrides) exactly + - keys not listed in C(overrides) will be removed, and listed keys will be created or updated. - On C(absent), all overrides for the locale will be removed. type: str - choices: [present, absent] + choices: ['present', 'absent'] default: present overrides: description: - List of overrides to ensure for the locale when C(state=present). Each item is a mapping with - the message C(key) and its C(value). + - the message C(key) and its C(value). - Ignored when C(state=absent). type: list elements: dict @@ -74,13 +73,14 @@ extends_documentation_fragment: - community.general.keycloak + - community.general.keycloak.actiongroup_keycloak - community.general.attributes author: - Jakub Danek (@danekja) -''' +""" -EXAMPLES = ''' +EXAMPLES = r""" - name: Replace all overrides for locale "en" (credentials auth) community.general.keycloak_realm_localization: auth_client_id: admin-cli @@ -123,7 +123,7 @@ state: absent delegate_to: localhost -- name: Dry run: see what would change for locale "en" +- name: Dry run - see what would change for locale "en" community.general.keycloak_realm_localization: auth_client_id: admin-cli auth_keycloak_url: https://auth.example.com/auth @@ -138,9 +138,9 @@ value: "Hello again" check_mode: true delegate_to: localhost -''' +""" -RETURN = ''' +RETURN = r""" msg: description: Human-readable message about what action was taken. returned: always @@ -173,17 +173,29 @@ type: dict contains: before: + description: State of localization overrides before execution type: dict + sample: + - key: greeting + value: Hello + - key: farewell + value: Bye after: + description: State of localization overrides after execution type: dict -''' - + sample: + - key: greeting + value: Hello + - key: farewell + value: Bye +""" from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, \ keycloak_argument_spec, get_token, KeycloakError from ansible.module_utils.basic import AnsibleModule from copy import deepcopy + def _normalize_overrides_from_api(current): """ Accept either: @@ -218,7 +230,7 @@ def main(): # Describe a single override record overrides_spec = dict( - key=dict(type='str', required=True), + key=dict(type='str', no_log=False, required=True), value=dict(type='str', required=True), ) @@ -340,7 +352,12 @@ def main(): result['msg'] = "Locale %s overrides are in sync." % (locale) # For accurate end_state, read back from API unless we are in check_mode - final_overrides = _normalize_overrides_from_api(kc.get_localization_values(locale, parent_id) or {}) if not module.check_mode else changeset['overrides'] + if not module.check_mode: + final_overrides = _normalize_overrides_from_api(kc.get_localization_values(locale, parent_id) or {}) + + else: + final_overrides = ['overrides'] + result['end_state'] = {'locale': locale, 'overrides': final_overrides} elif state == 'absent': @@ -367,7 +384,6 @@ def main(): result['end_state'] = changeset - module.exit_json(**result) diff --git a/tests/unit/plugins/modules/test_keycloak_realm_localization.py b/tests/unit/plugins/modules/test_keycloak_realm_localization.py index 6849a84fbdc..5ff4abe05c7 100644 --- a/tests/unit/plugins/modules/test_keycloak_realm_localization.py +++ b/tests/unit/plugins/modules/test_keycloak_realm_localization.py @@ -259,4 +259,4 @@ def test_present_missing_value_validation(self): if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() From 1a433beb057168d7beb542b81bead6434aebc6d9 Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Thu, 25 Sep 2025 15:29:44 +0200 Subject: [PATCH 07/40] fix test_keycloak_realm_localization.py for run with newer ansible --- .../plugins/modules/test_keycloak_realm_localization.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/plugins/modules/test_keycloak_realm_localization.py b/tests/unit/plugins/modules/test_keycloak_realm_localization.py index 5ff4abe05c7..7814d088f4d 100644 --- a/tests/unit/plugins/modules/test_keycloak_realm_localization.py +++ b/tests/unit/plugins/modules/test_keycloak_realm_localization.py @@ -19,7 +19,7 @@ from itertools import count -from ansible.module_utils.six import StringIO +from io import StringIO @contextmanager @@ -244,7 +244,7 @@ def test_present_missing_value_validation(self): 'locale': 'en', 'state': 'present', 'overrides': [ - {'key': 'greeting', 'value': None}, + {'key': 'greeting'}, ], } @@ -255,7 +255,7 @@ def test_present_missing_value_validation(self): with self.assertRaises(AnsibleFailJson) as exec_info: self.module.main() - self.assertIn("requires 'value' for keys", exec_info.exception.args[0]['msg']) + self.assertIn("missing required arguments: value", exec_info.exception.args[0]['msg']) if __name__ == '__main__': From 803c526b275221a4fa7b85d9e79de90939fbcc90 Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Thu, 25 Sep 2025 16:04:31 +0200 Subject: [PATCH 08/40] fix closure of _request optional http headers parameter --- plugins/module_utils/identity/keycloak/keycloak.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/module_utils/identity/keycloak/keycloak.py b/plugins/module_utils/identity/keycloak/keycloak.py index 05accaf0d35..c967bb77a95 100644 --- a/plugins/module_utils/identity/keycloak/keycloak.py +++ b/plugins/module_utils/identity/keycloak/keycloak.py @@ -384,11 +384,8 @@ def _request(self, url, method, data=None, headers=None): :param headers headers to be sent with request, defaults to self.restheaders :return: raw API response """ - def make_request_catching_401(): + def make_request_catching_401(headers): try: - if headers is None: - headers = self.restheaders - return open_url(url, method=method, data=data, http_agent=self.http_agent, headers=headers, timeout=self.connection_timeout, @@ -398,7 +395,10 @@ def make_request_catching_401(): raise e return e - r = make_request_catching_401() + if headers is None: + headers = self.restheaders + + r = make_request_catching_401(headers) if isinstance(r, Exception): # Try to refresh token and retry, if available From e9194841215d5e65fcdc9049d8feff119d50c2fe Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Thu, 25 Sep 2025 16:08:44 +0200 Subject: [PATCH 09/40] add copyright and license to test_keycloak_realm_localization.py --- .../unit/plugins/modules/test_keycloak_realm_localization.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/unit/plugins/modules/test_keycloak_realm_localization.py b/tests/unit/plugins/modules/test_keycloak_realm_localization.py index 7814d088f4d..b0fca33d1e3 100644 --- a/tests/unit/plugins/modules/test_keycloak_realm_localization.py +++ b/tests/unit/plugins/modules/test_keycloak_realm_localization.py @@ -1,6 +1,11 @@ # Python # -*- coding: utf-8 -*- +# Copyright (c) 2025, Jakub Danek +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or +# https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + from __future__ import absolute_import, division, print_function __metaclass__ = type From 367c9577d17e702659a1e036055d118fa20229d2 Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Thu, 25 Sep 2025 16:17:02 +0200 Subject: [PATCH 10/40] fix documentation indentation --- .../modules/keycloak_realm_localization.py | 174 +++++++++--------- 1 file changed, 87 insertions(+), 87 deletions(-) diff --git a/plugins/modules/keycloak_realm_localization.py b/plugins/modules/keycloak_realm_localization.py index c2179ebff0d..8a48f5db083 100644 --- a/plugins/modules/keycloak_realm_localization.py +++ b/plugins/modules/keycloak_realm_localization.py @@ -21,63 +21,63 @@ version_added: 10.5.0 description: - - Manage per-locale message overrides for a Keycloak realm using the Keycloak Admin REST API. - - Requires access via OpenID Connect; the connecting user/client must have sufficient privileges. - - The names of module options are snake_cased versions of the names found in the Keycloak API. + - Manage per-locale message overrides for a Keycloak realm using the Keycloak Admin REST API. + - Requires access via OpenID Connect; the connecting user/client must have sufficient privileges. + - The names of module options are snake_cased versions of the names found in the Keycloak API. attributes: - check_mode: - support: full - diff_mode: - support: full + check_mode: + support: full + diff_mode: + support: full options: - locale: + locale: + description: + - Locale code for which the overrides apply (for example, C(en), C(fi), C(de)). + type: str + required: true + parent_id: + description: + - Name of the realm that owns the locale overrides. + type: str + required: true + state: + description: + - Desired state of localization overrides for the given locale. + - On C(present), the set of overrides for the locale will be made to match C(overrides) exactly + - keys not listed in C(overrides) will be removed, and listed keys will be created or updated. + - On C(absent), all overrides for the locale will be removed. + type: str + choices: ['present', 'absent'] + default: present + overrides: + description: + - List of overrides to ensure for the locale when C(state=present). Each item is a mapping with + - the message C(key) and its C(value). + - Ignored when C(state=absent). + type: list + elements: dict + default: [] + suboptions: + key: description: - - Locale code for which the overrides apply (for example, C(en), C(fi), C(de)). + - The message key to override. type: str required: true - parent_id: + value: description: - - Name of the realm that owns the locale overrides. + - The override value for the message key. type: str required: true - state: - description: - - Desired state of localization overrides for the given locale. - - On C(present), the set of overrides for the locale will be made to match C(overrides) exactly - - keys not listed in C(overrides) will be removed, and listed keys will be created or updated. - - On C(absent), all overrides for the locale will be removed. - type: str - choices: ['present', 'absent'] - default: present - overrides: - description: - - List of overrides to ensure for the locale when C(state=present). Each item is a mapping with - - the message C(key) and its C(value). - - Ignored when C(state=absent). - type: list - elements: dict - default: [] - suboptions: - key: - description: - - The message key to override. - type: str - required: true - value: - description: - - The override value for the message key. - type: str - required: true extends_documentation_fragment: - - community.general.keycloak - - community.general.keycloak.actiongroup_keycloak - - community.general.attributes + - community.general.keycloak + - community.general.keycloak.actiongroup_keycloak + - community.general.attributes author: - - Jakub Danek (@danekja) + - Jakub Danek (@danekja) """ EXAMPLES = r""" @@ -142,52 +142,52 @@ RETURN = r""" msg: - description: Human-readable message about what action was taken. - returned: always - type: str + description: Human-readable message about what action was taken. + returned: always + type: str end_state: - description: - - Final state of localization overrides for the locale after module execution. - - Contains the C(locale) and the list of C(overrides) as key/value items. - returned: on success - type: dict - contains: - locale: - description: The locale code affected. - type: str - sample: en - overrides: - description: The list of overrides that exist after execution. - type: list - elements: dict - sample: - - key: greeting - value: Hello - - key: farewell - value: Bye + description: + - Final state of localization overrides for the locale after module execution. + - Contains the C(locale) and the list of C(overrides) as key/value items. + returned: on success + type: dict + contains: + locale: + description: The locale code affected. + type: str + sample: en + overrides: + description: The list of overrides that exist after execution. + type: list + elements: dict + sample: + - key: greeting + value: Hello + - key: farewell + value: Bye diff: - description: - - When run with C(--diff), shows the before and after structures - for the locale and its overrides. - returned: when supported and requested - type: dict - contains: - before: - description: State of localization overrides before execution - type: dict - sample: - - key: greeting - value: Hello - - key: farewell - value: Bye - after: - description: State of localization overrides after execution - type: dict - sample: - - key: greeting - value: Hello - - key: farewell - value: Bye + description: + - When run with C(--diff), shows the before and after structures + for the locale and its overrides. + returned: when supported and requested + type: dict + contains: + before: + description: State of localization overrides before execution + type: dict + sample: + - key: greeting + value: Hello + - key: farewell + value: Bye + after: + description: State of localization overrides after execution + type: dict + sample: + - key: greeting + value: Hello + - key: farewell + value: Bye """ from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, \ From 7607ac854fc6157ce908cb76eb30095ddf1ad8bb Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Thu, 25 Sep 2025 16:18:50 +0200 Subject: [PATCH 11/40] replace list.copy with 2.x compatible syntax --- plugins/modules/keycloak_realm_localization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/keycloak_realm_localization.py b/plugins/modules/keycloak_realm_localization.py index 8a48f5db083..b3dfdf2b5a6 100644 --- a/plugins/modules/keycloak_realm_localization.py +++ b/plugins/modules/keycloak_realm_localization.py @@ -301,7 +301,7 @@ def main(): # - to_update: keys missing or with different values # - to_remove: keys existing in current state but not in desired to_update = [] - to_remove = old_overrides.copy() + to_remove = list(old_overrides) # Mark updates and remove matched ones from to_remove for record in desired_overrides: From 3d43d2ba09c1b7ac3e3c0ecbc419dace3435f7c7 Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Thu, 25 Sep 2025 16:19:27 +0200 Subject: [PATCH 12/40] replace list.copy with deepcopy --- plugins/modules/keycloak_realm_localization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/keycloak_realm_localization.py b/plugins/modules/keycloak_realm_localization.py index b3dfdf2b5a6..d4dbb8dd4c0 100644 --- a/plugins/modules/keycloak_realm_localization.py +++ b/plugins/modules/keycloak_realm_localization.py @@ -301,7 +301,7 @@ def main(): # - to_update: keys missing or with different values # - to_remove: keys existing in current state but not in desired to_update = [] - to_remove = list(old_overrides) + to_remove = deepcopy(old_overrides) # Mark updates and remove matched ones from to_remove for record in desired_overrides: From 188bfd8e1e1c37019c9fcab1365ce89472ca833a Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 26 Sep 2025 08:41:32 +0200 Subject: [PATCH 13/40] Update plugins/modules/keycloak_realm_localization.py Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> --- plugins/modules/keycloak_realm_localization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/keycloak_realm_localization.py b/plugins/modules/keycloak_realm_localization.py index d4dbb8dd4c0..55c2e5a47e2 100644 --- a/plugins/modules/keycloak_realm_localization.py +++ b/plugins/modules/keycloak_realm_localization.py @@ -18,7 +18,7 @@ short_description: Manage Keycloak realm localization overrides via the Keycloak API -version_added: 10.5.0 +version_added: 11.4.0 description: - Manage per-locale message overrides for a Keycloak realm using the Keycloak Admin REST API. From 194b03beeafa1451db9693f76bc8b3a13fe97c3a Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 26 Sep 2025 08:41:01 +0200 Subject: [PATCH 14/40] rev: remove uncessary changelog fragment --- .../fragments/10841-keycloak-add-optional-headers-request.yml | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 changelogs/fragments/10841-keycloak-add-optional-headers-request.yml diff --git a/changelogs/fragments/10841-keycloak-add-optional-headers-request.yml b/changelogs/fragments/10841-keycloak-add-optional-headers-request.yml deleted file mode 100644 index 356c266cb3e..00000000000 --- a/changelogs/fragments/10841-keycloak-add-optional-headers-request.yml +++ /dev/null @@ -1,2 +0,0 @@ -minor_changes: - - keycloak - adds optional headers parameter override for _request method in module_utils/identity/keycloak (https://github.com/ansible-collections/community.general/pull/10841). \ No newline at end of file From 1cc06a08418f6057ac8bb629aabd95f95a1b004c Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 26 Sep 2025 08:41:15 +0200 Subject: [PATCH 15/40] rev: remove uncessary code remnants --- .../modules/keycloak_realm_localization.py | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/plugins/modules/keycloak_realm_localization.py b/plugins/modules/keycloak_realm_localization.py index 55c2e5a47e2..86db6e7ff8a 100644 --- a/plugins/modules/keycloak_realm_localization.py +++ b/plugins/modules/keycloak_realm_localization.py @@ -198,9 +198,8 @@ def _normalize_overrides_from_api(current): """ - Accept either: + Accepts: - dict: {'k1': 'v1', ...} - - list of dicts: [{'key': 'k1', 'value': 'v1'}, ...] Return a sorted list of {'key', 'value'}. This helper provides a consistent shape for downstream comparison/diff logic. @@ -208,12 +207,8 @@ def _normalize_overrides_from_api(current): if not current: return [] - if isinstance(current, dict): - # Convert mapping to list of key/value dicts - items = [{'key': k, 'value': v} for k, v in current.items()] - else: - # Assume a list of dicts with keys 'key' and 'value' - items = [{'key': o['key'], 'value': o.get('value')} for o in current] + # Convert mapping to list of key/value dicts + items = [{'key': k, 'value': v} for k, v in current.items()] # Sort for stable comparisons and diff output return sorted(items, key=lambda x: x['key']) @@ -266,14 +261,7 @@ def main(): state = module.params.get('state') parent_id = module.params.get('parent_id') - # Desired overrides: deduplicate by key via dict (last wins), then sort desired_raw = module.params.get('overrides') or [] - if state == 'present': - # Validate that all keys have a value in present mode - missing_values = [r['key'] for r in desired_raw if r.get('value') is None] - if missing_values: - module.fail_json(msg="state=present requires 'value' for keys: %s" % ", ".join(missing_values)) - desired_map = {r['key']: r.get('value') for r in desired_raw} desired_overrides = [{'key': k, 'value': v} for k, v in sorted(desired_map.items())] @@ -327,7 +315,7 @@ def main(): result['changed'] = True # Any leftovers in to_remove must be deleted - if len(to_remove) > 0: + if to_remove: result['changed'] = True if result['changed']: From 7d9767a93177f294589ef8bfddd196d5dbca781d Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 26 Sep 2025 09:23:06 +0200 Subject: [PATCH 16/40] rev: update file header with copyright according to guidelines --- plugins/modules/keycloak_realm_localization.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/plugins/modules/keycloak_realm_localization.py b/plugins/modules/keycloak_realm_localization.py index 86db6e7ff8a..3614c9e229b 100644 --- a/plugins/modules/keycloak_realm_localization.py +++ b/plugins/modules/keycloak_realm_localization.py @@ -2,11 +2,7 @@ # !/usr/bin/python # -*- coding: utf-8 -*- -# This Ansible module manages realm localization overrides in Keycloak. -# It ensures the set of message key/value overrides for a given locale -# either matches the provided list (state=present) or is fully removed (state=absent). - -# Copyright (c) 2025, Jakub Danek +# Copyright: Contributors to the Ansible project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or # https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later From 40a75d2b90963f25e8febe9720c43537a1e1836b Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 26 Sep 2025 09:23:22 +0200 Subject: [PATCH 17/40] rev: update documentation according to guidelines --- .../modules/keycloak_realm_localization.py | 50 +++++-------------- 1 file changed, 13 insertions(+), 37 deletions(-) diff --git a/plugins/modules/keycloak_realm_localization.py b/plugins/modules/keycloak_realm_localization.py index 3614c9e229b..b4e01c95e2d 100644 --- a/plugins/modules/keycloak_realm_localization.py +++ b/plugins/modules/keycloak_realm_localization.py @@ -30,7 +30,7 @@ options: locale: description: - - Locale code for which the overrides apply (for example, C(en), C(fi), C(de)). + - Locale code for which the overrides apply (for example, V(en), V(fi), V(de)). type: str required: true parent_id: @@ -41,17 +41,17 @@ state: description: - Desired state of localization overrides for the given locale. - - On C(present), the set of overrides for the locale will be made to match C(overrides) exactly - - keys not listed in C(overrides) will be removed, and listed keys will be created or updated. - - On C(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) 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. type: str choices: ['present', 'absent'] default: present overrides: description: - - List of overrides to ensure for the locale when C(state=present). Each item is a mapping with - - the message C(key) and its C(value). - - Ignored when C(state=absent). + - List of overrides to ensure for the locale when O(state=present). Each item is a mapping with + the record's O(overrides.key) and its O(overrides.value). + - Ignored when O(state=absent). type: list elements: dict default: [] @@ -67,13 +67,16 @@ type: str required: true +seealso: + - module: community.general.keycloak_realm + description: Keycloak module which can be used specify list of supported locales using O(plugin.community.keycloak.general.keycloak_realm#module:supported_locales). + extends_documentation_fragment: - community.general.keycloak - community.general.keycloak.actiongroup_keycloak - community.general.attributes -author: - - Jakub Danek (@danekja) +author: Jakub Danek (@danekja) """ EXAMPLES = r""" @@ -137,14 +140,10 @@ """ RETURN = r""" -msg: - description: Human-readable message about what action was taken. - returned: always - type: str end_state: description: - Final state of localization overrides for the locale after module execution. - - Contains the C(locale) and the list of C(overrides) as key/value items. + - Contains the O(locale) and the list of O(overrides) as key/value items. returned: on success type: dict contains: @@ -161,29 +160,6 @@ value: Hello - key: farewell value: Bye -diff: - description: - - When run with C(--diff), shows the before and after structures - for the locale and its overrides. - returned: when supported and requested - type: dict - contains: - before: - description: State of localization overrides before execution - type: dict - sample: - - key: greeting - value: Hello - - key: farewell - value: Bye - after: - description: State of localization overrides after execution - type: dict - sample: - - key: greeting - value: Hello - - key: farewell - value: Bye """ from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, \ From a93417a2b0aa53881d2fcc1e56a04f6d5e5e5af3 Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 26 Sep 2025 09:29:35 +0200 Subject: [PATCH 18/40] rev: fix intermodule doc reference --- plugins/modules/keycloak_realm_localization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/keycloak_realm_localization.py b/plugins/modules/keycloak_realm_localization.py index b4e01c95e2d..7f72269c004 100644 --- a/plugins/modules/keycloak_realm_localization.py +++ b/plugins/modules/keycloak_realm_localization.py @@ -69,7 +69,7 @@ seealso: - module: community.general.keycloak_realm - description: Keycloak module which can be used specify list of supported locales using O(plugin.community.keycloak.general.keycloak_realm#module:supported_locales). + description: Keycloak module which can be used specify list of supported locales using O(community.general.keycloak_realm#module:supported_locales). extends_documentation_fragment: - community.general.keycloak From 0d52bc777eacb41230dc32bc824d8812fe03c641 Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 26 Sep 2025 09:33:55 +0200 Subject: [PATCH 19/40] rev: fix copyright --- plugins/modules/keycloak_realm_localization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/keycloak_realm_localization.py b/plugins/modules/keycloak_realm_localization.py index 7f72269c004..b315f4f5587 100644 --- a/plugins/modules/keycloak_realm_localization.py +++ b/plugins/modules/keycloak_realm_localization.py @@ -2,7 +2,7 @@ # !/usr/bin/python # -*- coding: utf-8 -*- -# Copyright: Contributors to the Ansible project +# Copyright: Jakub Danek # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or # https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later From c00c7b918edaff407c85f8d470af665ebacbe2c4 Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 26 Sep 2025 09:34:32 +0200 Subject: [PATCH 20/40] rev: list reference in docs --- plugins/modules/keycloak_realm_localization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/keycloak_realm_localization.py b/plugins/modules/keycloak_realm_localization.py index b315f4f5587..d6a9e08c1c5 100644 --- a/plugins/modules/keycloak_realm_localization.py +++ b/plugins/modules/keycloak_realm_localization.py @@ -50,7 +50,7 @@ overrides: description: - List of overrides to ensure for the locale when O(state=present). Each item is a mapping with - the record's O(overrides.key) and its O(overrides.value). + the record's O(overrides[].key) and its O(overrides[].value). - Ignored when O(state=absent). type: list elements: dict From 538cd52b40ccda9de1144c6202943dbcf031f877 Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 26 Sep 2025 09:35:35 +0200 Subject: [PATCH 21/40] rev: line too long in docs --- plugins/modules/keycloak_realm_localization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/keycloak_realm_localization.py b/plugins/modules/keycloak_realm_localization.py index d6a9e08c1c5..7e700f3a339 100644 --- a/plugins/modules/keycloak_realm_localization.py +++ b/plugins/modules/keycloak_realm_localization.py @@ -69,7 +69,7 @@ seealso: - module: community.general.keycloak_realm - description: Keycloak module which can be used specify list of supported locales using O(community.general.keycloak_realm#module:supported_locales). + description: You can specify list of supported locales using O(community.general.keycloak_realm#module:supported_locales). extends_documentation_fragment: - community.general.keycloak From 37c815c29b4e24aa5246867573d8c4507d504a91 Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 26 Sep 2025 09:36:30 +0200 Subject: [PATCH 22/40] rev: remove year from copyright per guidelines --- tests/unit/plugins/modules/test_keycloak_realm_localization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/plugins/modules/test_keycloak_realm_localization.py b/tests/unit/plugins/modules/test_keycloak_realm_localization.py index b0fca33d1e3..e93f7c6e3c9 100644 --- a/tests/unit/plugins/modules/test_keycloak_realm_localization.py +++ b/tests/unit/plugins/modules/test_keycloak_realm_localization.py @@ -1,7 +1,7 @@ # Python # -*- coding: utf-8 -*- -# Copyright (c) 2025, Jakub Danek +# Copyright Jakub Danek # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or # https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later From e1c726d67534f9f705bc1953da99ff8b3b8af0d3 Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 26 Sep 2025 09:39:34 +0200 Subject: [PATCH 23/40] rev: maybe this is valid copyright line? --- plugins/modules/keycloak_realm_localization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/keycloak_realm_localization.py b/plugins/modules/keycloak_realm_localization.py index 7e700f3a339..2eb73fb593c 100644 --- a/plugins/modules/keycloak_realm_localization.py +++ b/plugins/modules/keycloak_realm_localization.py @@ -2,7 +2,7 @@ # !/usr/bin/python # -*- coding: utf-8 -*- -# Copyright: Jakub Danek +# Copyright Jakub Danek # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or # https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later From e340bfbd9206d5f636a274b2823c35dd3f34abb9 Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 3 Oct 2025 13:23:39 +0200 Subject: [PATCH 24/40] Update plugins/modules/keycloak_realm_localization.py Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> --- plugins/modules/keycloak_realm_localization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/keycloak_realm_localization.py b/plugins/modules/keycloak_realm_localization.py index 2eb73fb593c..02bcc8cea47 100644 --- a/plugins/modules/keycloak_realm_localization.py +++ b/plugins/modules/keycloak_realm_localization.py @@ -328,7 +328,7 @@ def main(): if module.check_mode: - if len(old_overrides) > 0: + if old_overrides: result['changed'] = True result['msg'] = "All overrides for locale %s would be deleted." % (locale) else: From 72eb31f580582435e4988b6f000b2e22ff4f6719 Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 3 Oct 2025 13:25:30 +0200 Subject: [PATCH 25/40] rev: cleaner sorting in _normalize_overrides_from_api --- plugins/modules/keycloak_realm_localization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/keycloak_realm_localization.py b/plugins/modules/keycloak_realm_localization.py index 02bcc8cea47..db3beac55ea 100644 --- a/plugins/modules/keycloak_realm_localization.py +++ b/plugins/modules/keycloak_realm_localization.py @@ -180,10 +180,10 @@ def _normalize_overrides_from_api(current): return [] # Convert mapping to list of key/value dicts - items = [{'key': k, 'value': v} for k, v in current.items()] + items = [{'key': k, 'value': v} for k, v in sorted(current.items())] # Sort for stable comparisons and diff output - return sorted(items, key=lambda x: x['key']) + return items def main(): From 8a0b96228b7fcd02d905c9c7f4744586c8f00e57 Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 31 Oct 2025 08:30:39 +0100 Subject: [PATCH 26/40] python use improvements Co-authored-by: Felix Fontein --- plugins/module_utils/identity/keycloak/keycloak.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/identity/keycloak/keycloak.py b/plugins/module_utils/identity/keycloak/keycloak.py index c967bb77a95..0ea43aaf903 100644 --- a/plugins/module_utils/identity/keycloak/keycloak.py +++ b/plugins/module_utils/identity/keycloak/keycloak.py @@ -594,7 +594,7 @@ def get_localization_values(self, locale, realm="master"): try: return self._request_and_deserialize(realm_url, method='GET') except Exception as e: - self.fail_request(e, msg='Could not read localization overrides for realm %s, locale %s: %s' % (realm, locale, str(e)), + self.fail_request(e, msg=f'Could not read localization overrides for realm {realm}, locale {locale}: {e}', exception=traceback.format_exc()) def set_localization_value(self, locale, key, value, realm="master"): From 25b213acd1b319cd07de546d35c5a5ad03c1a98e Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 31 Oct 2025 08:30:51 +0100 Subject: [PATCH 27/40] Update plugins/module_utils/identity/keycloak/keycloak.py Co-authored-by: Felix Fontein --- plugins/module_utils/identity/keycloak/keycloak.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/identity/keycloak/keycloak.py b/plugins/module_utils/identity/keycloak/keycloak.py index 0ea43aaf903..e54bce6454e 100644 --- a/plugins/module_utils/identity/keycloak/keycloak.py +++ b/plugins/module_utils/identity/keycloak/keycloak.py @@ -621,7 +621,7 @@ def set_localization_value(self, locale, key, value, realm="master"): try: return self._request(realm_url, method='PUT', data=to_native(value), headers=headers) except Exception as e: - self.fail_request(e, msg='Could not set localization value in realm %s, locale %s: %s=%s: %s' % (realm, locale, key, value, str(e)), + self.fail_request(e, msg=f'Could not set localization value in realm {realm}, locale {locale}: {key}={value}: {e}', exception=traceback.format_exc()) def delete_localization_value(self, locale, key, realm="master"): From d129cfcd09bf220ab92fff6c34a62858d76da311 Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 31 Oct 2025 08:30:59 +0100 Subject: [PATCH 28/40] Update plugins/module_utils/identity/keycloak/keycloak.py Co-authored-by: Felix Fontein --- plugins/module_utils/identity/keycloak/keycloak.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/identity/keycloak/keycloak.py b/plugins/module_utils/identity/keycloak/keycloak.py index e54bce6454e..5cea9914fef 100644 --- a/plugins/module_utils/identity/keycloak/keycloak.py +++ b/plugins/module_utils/identity/keycloak/keycloak.py @@ -644,7 +644,7 @@ def delete_localization_value(self, locale, key, realm="master"): try: return self._request(realm_url, method='DELETE') except Exception as e: - self.fail_request(e, msg='Could not delete localization value in realm %s, locale %s, key %s: %s' % (realm, locale, key, str(e)), + self.fail_request(e, msg=f'Could not delete localization value in realm {realm}, locale {locale}, key {key}: {e}', exception=traceback.format_exc()) def get_clients(self, realm='master', filter=None): From 009cfd8ef372614eceb9178dd25cb1facc877593 Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 31 Oct 2025 08:31:09 +0100 Subject: [PATCH 29/40] Update plugins/modules/keycloak_realm_localization.py Co-authored-by: Felix Fontein --- plugins/modules/keycloak_realm_localization.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/modules/keycloak_realm_localization.py b/plugins/modules/keycloak_realm_localization.py index db3beac55ea..3adeb44fef7 100644 --- a/plugins/modules/keycloak_realm_localization.py +++ b/plugins/modules/keycloak_realm_localization.py @@ -6,8 +6,7 @@ # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or # https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later -from __future__ import absolute_import, division, print_function -__metaclass__ = type +from __future__ import annotations DOCUMENTATION = r""" module: keycloak_realm_localization From 65937f09072aedc492cac977b75cb801521cb644 Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 31 Oct 2025 08:31:20 +0100 Subject: [PATCH 30/40] Update plugins/modules/keycloak_realm_localization.py Co-authored-by: Felix Fontein --- plugins/modules/keycloak_realm_localization.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/modules/keycloak_realm_localization.py b/plugins/modules/keycloak_realm_localization.py index 3adeb44fef7..d9fb959c32c 100644 --- a/plugins/modules/keycloak_realm_localization.py +++ b/plugins/modules/keycloak_realm_localization.py @@ -1,6 +1,5 @@ # Python # !/usr/bin/python -# -*- coding: utf-8 -*- # Copyright Jakub Danek # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or From a3e26ab50f87fab15d81fec95e9eefb23d461149 Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 31 Oct 2025 08:31:29 +0100 Subject: [PATCH 31/40] Update plugins/modules/keycloak_realm_localization.py Co-authored-by: Felix Fontein --- plugins/modules/keycloak_realm_localization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/keycloak_realm_localization.py b/plugins/modules/keycloak_realm_localization.py index d9fb959c32c..c2594ead36d 100644 --- a/plugins/modules/keycloak_realm_localization.py +++ b/plugins/modules/keycloak_realm_localization.py @@ -338,7 +338,7 @@ def main(): kc.delete_localization_value(locale, override['key'], parent_id) result['changed'] = True - result['msg'] = "Locale %s has no overrides." % (locale) + result['msg'] = f"Locale {locale} has no overrides." result['end_state'] = changeset From 03dc127e1ba5a0a45d8d4f2d2269ec9d0fa2d654 Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 31 Oct 2025 08:31:42 +0100 Subject: [PATCH 32/40] Update plugins/modules/keycloak_realm_localization.py Co-authored-by: Felix Fontein --- plugins/modules/keycloak_realm_localization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/keycloak_realm_localization.py b/plugins/modules/keycloak_realm_localization.py index c2594ead36d..fde884ca2de 100644 --- a/plugins/modules/keycloak_realm_localization.py +++ b/plugins/modules/keycloak_realm_localization.py @@ -12,7 +12,7 @@ short_description: Manage Keycloak realm localization overrides via the Keycloak API -version_added: 11.4.0 +version_added: 12.0.0 description: - Manage per-locale message overrides for a Keycloak realm using the Keycloak Admin REST API. From 7006137a42e185feded707c843c0da33b569cf6b Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 31 Oct 2025 08:31:52 +0100 Subject: [PATCH 33/40] Update tests/unit/plugins/modules/test_keycloak_realm_localization.py Co-authored-by: Felix Fontein --- tests/unit/plugins/modules/test_keycloak_realm_localization.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/plugins/modules/test_keycloak_realm_localization.py b/tests/unit/plugins/modules/test_keycloak_realm_localization.py index e93f7c6e3c9..81405468fe0 100644 --- a/tests/unit/plugins/modules/test_keycloak_realm_localization.py +++ b/tests/unit/plugins/modules/test_keycloak_realm_localization.py @@ -1,5 +1,4 @@ # Python -# -*- coding: utf-8 -*- # Copyright Jakub Danek # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or From ebcfcb51efb1ad7851b25e4ad71ae0a392fab84a Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 31 Oct 2025 08:32:03 +0100 Subject: [PATCH 34/40] Update plugins/modules/keycloak_realm_localization.py Co-authored-by: Felix Fontein --- plugins/modules/keycloak_realm_localization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/keycloak_realm_localization.py b/plugins/modules/keycloak_realm_localization.py index fde884ca2de..aaa34e9b847 100644 --- a/plugins/modules/keycloak_realm_localization.py +++ b/plugins/modules/keycloak_realm_localization.py @@ -294,7 +294,7 @@ def main(): if module.check_mode: # Dry-run: report intent without side effects - result['msg'] = "Locale %s overrides would be updated." % (locale) + result['msg'] = f"Locale {locale} overrides would be updated." else: From 09df74653788e6537a6592684fa784fcf5ace777 Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 31 Oct 2025 08:32:15 +0100 Subject: [PATCH 35/40] Update plugins/modules/keycloak_realm_localization.py Co-authored-by: Felix Fontein --- plugins/modules/keycloak_realm_localization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/keycloak_realm_localization.py b/plugins/modules/keycloak_realm_localization.py index aaa34e9b847..43255fc8a11 100644 --- a/plugins/modules/keycloak_realm_localization.py +++ b/plugins/modules/keycloak_realm_localization.py @@ -304,7 +304,7 @@ def main(): for override in to_update: kc.set_localization_value(locale, override['key'], override['value'], parent_id) - result['msg'] = "Locale %s overrides have been updated." % (locale) + result['msg'] = f"Locale {locale} overrides have been updated." else: result['msg'] = "Locale %s overrides are in sync." % (locale) From d173219593c86a79186576993aa2726cc9900c55 Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 31 Oct 2025 08:32:25 +0100 Subject: [PATCH 36/40] Update plugins/modules/keycloak_realm_localization.py Co-authored-by: Felix Fontein --- plugins/modules/keycloak_realm_localization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/keycloak_realm_localization.py b/plugins/modules/keycloak_realm_localization.py index 43255fc8a11..4005bba1a5d 100644 --- a/plugins/modules/keycloak_realm_localization.py +++ b/plugins/modules/keycloak_realm_localization.py @@ -307,7 +307,7 @@ def main(): result['msg'] = f"Locale {locale} overrides have been updated." else: - result['msg'] = "Locale %s overrides are in sync." % (locale) + result['msg'] = f"Locale {locale} overrides are in sync." # For accurate end_state, read back from API unless we are in check_mode if not module.check_mode: From 26bc3041168469c4914ec7b3e81fc1255c67ad78 Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 31 Oct 2025 08:32:35 +0100 Subject: [PATCH 37/40] Update plugins/modules/keycloak_realm_localization.py Co-authored-by: Felix Fontein --- plugins/modules/keycloak_realm_localization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/keycloak_realm_localization.py b/plugins/modules/keycloak_realm_localization.py index 4005bba1a5d..bacd1769f40 100644 --- a/plugins/modules/keycloak_realm_localization.py +++ b/plugins/modules/keycloak_realm_localization.py @@ -328,9 +328,9 @@ def main(): if old_overrides: result['changed'] = True - result['msg'] = "All overrides for locale %s would be deleted." % (locale) + result['msg'] = f"All overrides for locale {locale} would be deleted." else: - result['msg'] = "No overrides for locale %s to be deleted." % (locale) + result['msg'] = f"No overrides for locale {locale} to be deleted." else: From 1c985026777b25357acc3f12d65df886a547209c Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 31 Oct 2025 08:32:45 +0100 Subject: [PATCH 38/40] Update tests/unit/plugins/modules/test_keycloak_realm_localization.py Co-authored-by: Felix Fontein --- tests/unit/plugins/modules/test_keycloak_realm_localization.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/plugins/modules/test_keycloak_realm_localization.py b/tests/unit/plugins/modules/test_keycloak_realm_localization.py index 81405468fe0..441070934c1 100644 --- a/tests/unit/plugins/modules/test_keycloak_realm_localization.py +++ b/tests/unit/plugins/modules/test_keycloak_realm_localization.py @@ -5,8 +5,7 @@ # https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later -from __future__ import absolute_import, division, print_function -__metaclass__ = type +from __future__ import annotations from contextlib import contextmanager From fd26fb9cb445b6b188b82706ac203d30ed540dcb Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 31 Oct 2025 08:32:53 +0100 Subject: [PATCH 39/40] Update tests/unit/plugins/modules/test_keycloak_realm_localization.py Co-authored-by: Felix Fontein --- .../unit/plugins/modules/test_keycloak_realm_localization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/plugins/modules/test_keycloak_realm_localization.py b/tests/unit/plugins/modules/test_keycloak_realm_localization.py index 441070934c1..e2285464f37 100644 --- a/tests/unit/plugins/modules/test_keycloak_realm_localization.py +++ b/tests/unit/plugins/modules/test_keycloak_realm_localization.py @@ -9,8 +9,8 @@ from contextlib import contextmanager -from ansible_collections.community.internal_test_tools.tests.unit.compat import unittest -from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch +import unittest +from unittest.mock import patch from ansible_collections.community.internal_test_tools.tests.unit.plugins.modules.utils import ( AnsibleExitJson, AnsibleFailJson, From c25497e3d7ccd8d5a22404ce1d163bc0cde8405b Mon Sep 17 00:00:00 2001 From: Jakub Danek Date: Fri, 31 Oct 2025 08:33:01 +0100 Subject: [PATCH 40/40] Update tests/unit/plugins/modules/test_keycloak_realm_localization.py Co-authored-by: Felix Fontein --- tests/unit/plugins/modules/test_keycloak_realm_localization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/plugins/modules/test_keycloak_realm_localization.py b/tests/unit/plugins/modules/test_keycloak_realm_localization.py index e2285464f37..09a35fce179 100644 --- a/tests/unit/plugins/modules/test_keycloak_realm_localization.py +++ b/tests/unit/plugins/modules/test_keycloak_realm_localization.py @@ -81,7 +81,7 @@ def mock_good_connection(): class TestKeycloakRealmLocalization(ModuleTestCase): def setUp(self): - super(TestKeycloakRealmLocalization, self).setUp() + super().setUp() self.module = keycloak_realm_localization def test_present_no_change_in_sync(self):