From ed71ef7805817813ebcaabeb5d5e657e254deb60 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Fri, 11 Sep 2026 16:43:25 -0400 Subject: [PATCH] service_key: support editable-only API endpoints Detect POST support through OPTIONS and skip unsupported creates with a clear warning. Limit editable-only updates to supported fields, add deprecation notices for create-only parameters, and split integration coverage for creatable versus editable APIs. Co-Authored-By: Codex --- plugins/action/base_action.py | 22 ++- plugins/action/service_key.py | 96 +++++++-- .../plugin_utils/manager/platform_manager.py | 21 +- plugins/plugin_utils/manager/rpc_client.py | 4 + .../plugin_utils/platform/direct_client.py | 20 ++ .../service_keys_test/tasks/creatable.yml | 182 ++++++++++++++++++ .../service_keys_test/tasks/editable.yml | 90 +++++++++ .../targets/service_keys_test/tasks/main.yml | 174 +---------------- tests/test_completeness.py | 2 +- 9 files changed, 426 insertions(+), 185 deletions(-) create mode 100644 tests/integration/targets/service_keys_test/tasks/creatable.yml create mode 100644 tests/integration/targets/service_keys_test/tasks/editable.yml diff --git a/plugins/action/base_action.py b/plugins/action/base_action.py index eb1d244d..d72083ab 100644 --- a/plugins/action/base_action.py +++ b/plugins/action/base_action.py @@ -359,6 +359,19 @@ def _pre_execute_hook(self, ansible_data: dict, write_only_data: dict, validated operation: The resolved operation string. """ + def _handle_operation_error(self, error: Exception, operation: str, result: dict) -> bool: + """Handle a resource-specific operation failure. + + Return ``True`` after updating *result* to convert an expected API + limitation into a successful module result. The default leaves all + errors to the standard action-plugin failure path. + """ + return False + + def _skip_operation(self, operation: str, result: dict) -> bool: + """Optionally complete an operation without sending an API request.""" + return False + def _get_or_spawn_manager(self, task_vars: dict) -> Tuple[Union["DirectHTTPClient", "ManagerRPCClient"], Optional[Dict[str, Any]]]: """ Dispatcher: Get connection client from the connection plugin. @@ -1201,11 +1214,14 @@ def run(self, tmp: object = None, task_vars: Optional[dict] = None) -> dict: else: operation = "create" - # ---- check mode ------------------------------------------------ + # ---- unsupported operation / check mode ----------------------- ansible_data = self._build_ansible_data(resource, validated_params, operation) if operation == "update" and state == "enforced": ansible_data["_platform_enforced"] = True + if self._skip_operation(operation, result): + return result + if self._task.check_mode and operation in ("create", "update", "delete"): if operation == "delete": result.update( @@ -1236,7 +1252,9 @@ def run(self, tmp: object = None, task_vars: Optional[dict] = None) -> dict: module_name=self.MODULE_NAME, ansible_data=ansible_data, ) - except ValueError as exc: + except Exception as exc: + if self._handle_operation_error(exc, operation, result): + return result if operation == "find" and ("not found" in str(exc).lower() or "resource with" in str(exc).lower()): result.update( { diff --git a/plugins/action/service_key.py b/plugins/action/service_key.py index fdb4b1fc..27afec8b 100644 --- a/plugins/action/service_key.py +++ b/plugins/action/service_key.py @@ -5,6 +5,8 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type +from dataclasses import asdict + from ansible_collections.ansible.platform.plugins.action.base_action import BaseResourceActionPlugin from ansible_collections.ansible.platform.plugins.plugin_utils.ansible_models.service_key import AnsibleServiceKey @@ -14,29 +16,91 @@ class ActionModule(BaseResourceActionPlugin): MODEL_CLASS = AnsibleServiceKey # mark_previous_inactive: operation-time directive; API never returns it. # secret: write-only; API returns null/hash, not the original value. - # Including either in _should_update() causes false positives. _WRITE_ONLY_FIELDS = frozenset({"mark_previous_inactive", "secret"}) + _CREATE_ONLY_FIELDS = frozenset({"algorithm", "mark_previous_inactive", "secret", "secret_length", "service_cluster"}) + + def run(self, tmp=None, task_vars=None): + """Emit a deprecation warning when create-only fields are supplied.""" + supplied_fields = sorted(field for field in self._CREATE_ONLY_FIELDS if self._task.args.get(field) is not None) + result = super().run(tmp, task_vars) + if supplied_fields: + result.setdefault("deprecations", []).append( + { + "msg": "The service_key parameters %s are deprecated; newer AAP versions ignore them for existing service keys." + % ", ".join(supplied_fields), + "version": "4.0.0", + "collection_name": "ansible.platform", + } + ) + return result def _pre_execute_hook(self, ansible_data, write_only_data, validated_params, operation): """Re-inject write-only fields so they reach the API payload. - ``mark_previous_inactive`` and ``secret`` are excluded from the - AnsibleServiceKey dataclass (via _WRITE_ONLY_FIELDS) to prevent - false-positive idempotency checks — the API never echoes these - fields back in GET responses, so _should_update() would always - see None vs. a user-supplied value and report changed. - - For create/update operations however, both fields must still reach - the transform and ultimately the API request body. This hook puts - them back into ansible_data (from the write_only_data stash) so - the transform can include them when they are non-None. - - Note: mark_previous_inactive=False is a valid explicit value and - must not be filtered out here — only skip genuinely absent (None) - values. + Older AAP versions accept all fields on update. Newer versions + advertise no POST action and only allow name/is_active changes. """ if operation in ("create", "update"): - for field in ("mark_previous_inactive", "secret"): + for field in self._WRITE_ONLY_FIELDS: val = write_only_data.get(field) if val is not None: ansible_data[field] = val + + if operation == "update" and not self._supports_create(): + for field in self._CREATE_ONLY_FIELDS: + ansible_data.pop(field, None) + + def _supports_create(self): + """Use OPTIONS to distinguish legacy and editable-only APIs.""" + return self._client.endpoint_supports_method("/api/gateway/v1/service_keys/", "POST") + + def _should_update(self, desired_data, current_data): + """Compare all fields on legacy AAP, editable fields on newer AAP.""" + if self._supports_create(): + return super()._should_update(desired_data, current_data) + + ignored_fields = [field for field in self._CREATE_ONLY_FIELDS if self._task.args.get(field) is not None] + if ignored_fields: + self._display.warning("The AAP instance will ignore: %s when making this request." % ", ".join(sorted(ignored_fields))) + editable_data = {key: value for key, value in desired_data.items() if key not in self._CREATE_ONLY_FIELDS} + return super()._should_update(editable_data, current_data) + + def _skip_operation(self, operation, result): + """Avoid POST when OPTIONS reports that the endpoint is read-only.""" + if operation == "create" and not self._supports_create(): + return self._warn_create_unsupported(result) + return False + + def _handle_operation_error(self, error, operation, result): + """Warn when newer AAP versions reject service-key creation.""" + error_text = str(error).lower() + status_code = getattr(error, "status_code", None) + if status_code is None: + status_code = getattr(error, "details", {}).get("status_code") + if operation != "create" or (status_code != 405 and "405" not in error_text): + return False + + return self._warn_create_unsupported(result) + + def _warn_create_unsupported(self, result): + """Return a stable no-op result for an editable-only AAP instance.""" + + warning = "This version of AAP does not support creating service_keys through the API." + # Match the normal resource result shape without implying that AAP + # created the requested key. ``state`` is an input directive, not a + # returned resource attribute. + service_key = asdict(AnsibleServiceKey(name=self._task.args.get("name"))) + service_key.pop("state") + self._display.warning(warning) + result.update( + { + "changed": False, + "failed": False, + self.MODULE_NAME: service_key, + # Keep legacy flat result access stable for follow-up tasks. + **service_key, + "msg": warning, + "warnings": [warning], + } + ) + return True diff --git a/plugins/plugin_utils/manager/platform_manager.py b/plugins/plugin_utils/manager/platform_manager.py index 4cf2593b..83da431d 100644 --- a/plugins/plugin_utils/manager/platform_manager.py +++ b/plugins/plugin_utils/manager/platform_manager.py @@ -483,6 +483,23 @@ def _build_url(self, endpoint: str, query_params: Optional[Dict] = None) -> str: return url + def endpoint_supports_method(self, path: str, method: str) -> bool: + """Return whether an endpoint advertises an HTTP method via OPTIONS.""" + method = method.upper() + cache_key = f"endpoint-method:{path}:{method}" + if cache_key in self.cache: + return self.cache[cache_key] + + response = self.session.options(self._build_url(path), timeout=self.request_timeout, verify=self.requests_verify) + response.raise_for_status() + allowed = response.headers.get("Allow", "") + if allowed: + supported = method in {item.strip().upper() for item in allowed.split(",")} + else: + supported = method in response.json().get("actions", {}) + self.cache[cache_key] = supported + return supported + def execute(self, operation: str, module_name: str, ansible_data_dict: dict) -> dict: """ Execute a generic operation on any resource. @@ -660,7 +677,7 @@ def _update_resource(self, ansible_data: Any, mixin_class: type, context: dict) lookup_field = mixin_class.get_lookup_field() api_normalized_fields = {"slug"} internal_fields = {"organization_id"} - skip_fields = read_only_fields | {"state", lookup_field} | api_normalized_fields | internal_fields + skip_fields = read_only_fields | {"state", "new_name", lookup_field} | api_normalized_fields | internal_fields requested = asdict(ansible_data) for k, v in requested.items(): if k in skip_fields or v is None: @@ -703,7 +720,7 @@ def _update_resource(self, ansible_data: Any, mixin_class: type, context: dict) internal_fields = {"organization_id"} norm = self._normalize_for_compare lookup_field = mixin_class.get_lookup_field() - skip_fields = read_only_fields | {"state", lookup_field} | api_normalized_fields | internal_fields + skip_fields = read_only_fields | {"state", "new_name", lookup_field} | api_normalized_fields | internal_fields requested = asdict(ansible_data) changed = False for k, v in requested.items(): diff --git a/plugins/plugin_utils/manager/rpc_client.py b/plugins/plugin_utils/manager/rpc_client.py index ba29ba5a..e5776d4f 100644 --- a/plugins/plugin_utils/manager/rpc_client.py +++ b/plugins/plugin_utils/manager/rpc_client.py @@ -112,6 +112,10 @@ def lookup_resource_id(self, endpoint: str, lookup_field: str, lookup_value: str """ return self.service_proxy.lookup_resource_id(endpoint, lookup_field, lookup_value) + def endpoint_supports_method(self, path: str, method: str) -> bool: + """Ask the manager whether an endpoint advertises an HTTP method.""" + return self.service_proxy.endpoint_supports_method(path, method) + def search_api(self, endpoint: str, query_params: Optional[dict] = None, return_all: bool = False, max_objects: int = 1000) -> dict: """ Execute a raw GET via the manager subprocess and return the JSON response. diff --git a/plugins/plugin_utils/platform/direct_client.py b/plugins/plugin_utils/platform/direct_client.py index b56fdce9..ec1e1595 100644 --- a/plugins/plugin_utils/platform/direct_client.py +++ b/plugins/plugin_utils/platform/direct_client.py @@ -1018,3 +1018,23 @@ def direct_request(self, method: str, path: str, data=None) -> dict: return json.loads(response_body) if response_body else {} except Exception: return {} + + def endpoint_supports_method(self, path: str, method: str) -> bool: + """Return whether an endpoint advertises an HTTP method via OPTIONS.""" + method = method.upper() + cache_key = f"endpoint-method:{path}:{method}" + if cache_key in self.cache: + return self.cache[cache_key] + + response = self._make_request("OPTIONS", self._build_url(path), operation="options", resource=path) + allowed = getattr(response, "headers", {}).get("Allow", "") + if allowed: + supported = method in {item.strip().upper() for item in allowed.split(",")} + else: + try: + response_data = json.loads(response.read() or "{}") + except (TypeError, ValueError): + response_data = {} + supported = method in response_data.get("actions", {}) + self.cache[cache_key] = supported + return supported diff --git a/tests/integration/targets/service_keys_test/tasks/creatable.yml b/tests/integration/targets/service_keys_test/tasks/creatable.yml new file mode 100644 index 00000000..a25925bc --- /dev/null +++ b/tests/integration/targets/service_keys_test/tasks/creatable.yml @@ -0,0 +1,182 @@ +--- +- name: Run Createable Service Key Tests + module_defaults: + group/ansible.platform.gateway: + gateway_hostname: "{{ gateway_hostname }}" + gateway_username: "{{ gateway_username }}" + gateway_password: "{{ gateway_password }}" + gateway_validate_certs: "{{ gateway_validate_certs | bool }}" + + block: + # ---------------------------- + - name: Create Service Key 1 with check mode + ansible.platform.service_key: + name: "{{ name_prefix }}-Key 1" + is_active: true + service_cluster: "{{ controller_sc.id }}" + algorithm: HS384 + secret: "gateway-secret" + mark_previous_inactive: false + check_mode: true + + - name: Search for Service Key 1 + ansible.builtin.set_fact: + item_that_should_not_exist: "{{ lookup('ansible.platform.gateway_api', 'service_keys', + query_params={'name': '{{ name_prefix }}-Key 1'}, **connection_info) }}" + + - name: Assert that Service Key 1 does not exist + ansible.builtin.assert: + that: + - item_that_should_not_exist is not defined or item_that_should_not_exist | length == 0 + fail_msg: "Service Key '{{ name_prefix }}-Key 1' exists in the system!" + + - name: Create Service Key 1 + ansible.platform.service_key: + name: "{{ name_prefix }}-Key 1" + is_active: true + service_cluster: "{{ controller_sc.id }}" + algorithm: HS384 + secret: "gateway-secret" + mark_previous_inactive: false + register: service_key1 + + - name: Assert that we created service key 1 + ansible.builtin.assert: + that: + - service_key1 is changed + + # We have to take out the secret because its encrypted + - name: Recreate Service Key 1 + ansible.platform.service_key: + name: "{{ name_prefix }}-Key 1" + is_active: true + service_cluster: "{{ controller_sc.id }}" + algorithm: HS384 + mark_previous_inactive: false + register: recreate_service_key1 + + - name: Assert that recreate does not change the system + ansible.builtin.assert: + that: + - recreate_service_key1 is not changed + + - name: Create Service Key 2 + ansible.platform.service_key: + name: "{{ name_prefix }}-Key 2" + service_cluster: "{{ hub_sc.name }}" + secret: "gateway-secret" + mark_previous_inactive: true + register: service_key2 + + - name: Assert that we created service key 2 + ansible.builtin.assert: + that: + - service_key2 is changed + + - name: Create Service Key 3 + ansible.platform.service_key: + name: "{{ name_prefix }}-Key 3" + is_active: false + service_cluster: "{{ controller_sc.id }}" # Controller + mark_previous_inactive: false + register: service_key3 + + - name: Assert that we created service key 3 + ansible.builtin.assert: + that: + - service_key3 is changed + + - name: Create Service Key 4 + ansible.platform.service_key: + name: "{{ name_prefix }}-Key 4" + service_cluster: "{{ eda_sc.id }}" # EDA + mark_previous_inactive: false + register: service_key4 + + - name: Assert that we created service key 4 + ansible.builtin.assert: + that: + - service_key4 is changed + + - name: Create Service Key 5 + ansible.platform.service_key: + name: "{{ name_prefix }}-Key 5" + service_cluster: "{{ controller_sc.id }}" # Controller, have to set others as inactive + mark_previous_inactive: true + register: service_key5 + + - name: Assert that we created service key 5 + ansible.builtin.assert: + that: + - service_key5 is changed + + - name: Deactivate a key + ansible.platform.service_key: + name: "{{ service_key2.name }}" + is_active: false + register: change_service_key2 + + - name: Assert that we changed the existing key + ansible.builtin.assert: + that: + - change_service_key2 is changed + - change_service_key2.id == change_service_key2.id + + - name: See if a key exists + ansible.platform.service_key: + name: "{{ service_key3.id }}" + state: exists + register: exists_service_key3 + + - name: Assert that exists does not change the system + ansible.builtin.assert: + that: + - exists_service_key3 is not changed + + - name: Rename a key + ansible.platform.service_key: + name: "{{ service_key4.id }}" + new_name: "{{ service_key4.id }}-New" + register: rename_service_key4 + + - name: Assert that the rename changed an existing service key + ansible.builtin.assert: + that: + - rename_service_key4 is changed + - rename_service_key4.id == service_key4.id + + - name: Delete a non-existing service key + ansible.platform.service_key: + name: "{{ name_prefix }}-DNE" + state: absent + register: delete + + - name: Assert that delete of non-existent does not change the system + ansible.builtin.assert: + that: + - delete is not changed + + - name: Delete an actual service key + ansible.platform.service_key: + name: "{{ service_key5.id }}" + state: absent + register: delete + + - name: Assert that the delete changed the system + ansible.builtin.assert: + that: + - delete is changed + + always: + # Always Cleanup + - name: Delete Service Keys + ansible.platform.service_key: + state: absent + name: "{{ vars[item].id }}" + when: "item in vars and 'id' in vars[item]" + loop: + - "service_key1" + - "service_key2" + - "service_key3" + - "service_key4" + - "service_key5" diff --git a/tests/integration/targets/service_keys_test/tasks/editable.yml b/tests/integration/targets/service_keys_test/tasks/editable.yml new file mode 100644 index 00000000..e51a1bfb --- /dev/null +++ b/tests/integration/targets/service_keys_test/tasks/editable.yml @@ -0,0 +1,90 @@ +--- +- name: Load existing service keys + ansible.builtin.set_fact: + existing_service_keys: "{{ lookup('ansible.platform.gateway_api', 'service_keys', **connection_info) }}" + +- name: Ensure we have at least one service key + ansible.builtin.fail: + msg: "No service keys found in the system and unable to create them via the API. Please create at least one service key before running this test." + when: existing_service_keys | length == 0 + +- ansible.builtin.debug: + msg: "Existing service key we will be using: {{ existing_service_keys[0] }}" + +- name: Run Editable Service Key Tests + module_defaults: + group/ansible.platform.gateway: + gateway_hostname: "{{ gateway_hostname }}" + gateway_username: "{{ gateway_username }}" + gateway_password: "{{ gateway_password }}" + gateway_validate_certs: "{{ gateway_validate_certs | bool }}" + + block: + # ---------------------------- + - name: Try running the module with all of the fields to see what happens + ansible.platform.service_key: + name: "{{ existing_service_keys[0].name }}" + secret: "Some Secret" + algorithm: "HS512" + service_cluster: "{{ controller_sc.id }}" + mark_previous_inactive: false + is_active: "{{ existing_service_keys[0].is_active }}" + register: all_params_update + + - name: Assert that our call didn't fail + ansible.builtin.assert: + that: + - all_params_update is not changed + + - name: Attempt to update the first service key with the same values + ansible.platform.service_key: + name: "{{ existing_service_keys[0].name }}" + is_active: "{{ existing_service_keys[0].is_active }}" + register: check_mode_update + + - name: Assert that we could have changed the service key + ansible.builtin.assert: + that: + - check_mode_update is not changed + + - name: Attempt to change the first service key with check mode + ansible.platform.service_key: + name: "{{ existing_service_keys[0].name }}" + is_active: "{{ not existing_service_keys[0].is_active }}" + check_mode: true + register: check_mode_update + + - name: Assert that we would havechanged the first service key + ansible.builtin.assert: + that: + - check_mode_update is changed + + - name: Change the active state of the service key + ansible.platform.service_key: + name: "{{ existing_service_keys[0].name }}" + is_active: "{{ not existing_service_keys[0].is_active }}" + register: updated_service_key_result + + - name: Assert that we changed the service key + ansible.builtin.assert: + that: + - updated_service_key_result is changed + + - name: Reload the service key to verify the changes + ansible.builtin.set_fact: + updated_service_key: "{{ lookup('ansible.platform.gateway_api', 'service_keys/' ~ existing_service_keys[0].id, **connection_info) }}" + + - name: Assert the fields have been updated + ansible.builtin.assert: + that: + - updated_service_key.id == existing_service_keys[0].id + - updated_service_key.name == existing_service_keys[0].name + - updated_service_key.is_active != existing_service_keys[0].is_active + + always: + # Always Cleanup + - name: Reset the active state to the original value (if needed) + ansible.platform.service_key: + name: "{{ existing_service_keys[0].name }}" + is_active: "{{ existing_service_keys[0].is_active }}" + when: updated_service_key_result is defined and updated_service_key_result is changed diff --git a/tests/integration/targets/service_keys_test/tasks/main.yml b/tests/integration/targets/service_keys_test/tasks/main.yml index 0443e7a1..241f0b1c 100644 --- a/tests/integration/targets/service_keys_test/tasks/main.yml +++ b/tests/integration/targets/service_keys_test/tasks/main.yml @@ -17,7 +17,7 @@ msg: "This test works with 3 service clusters: gateway, eda and hub. It appears you might already have one or more of those, failing" when: - _sc_query | length > 1 - - _sc_query | length != 1 and _sc_query[0].type != 'gateway' + - _sc_query | length != 1 and _sc_query[0].name != 'gateway' - name: Run Test module_defaults: @@ -79,175 +79,21 @@ - name: Create Service Key 1 with check mode ansible.platform.service_key: name: "{{ name_prefix }}-Key 1" - is_active: true service_cluster: "{{ controller_sc.id }}" - algorithm: HS384 - secret: "gateway-secret" - mark_previous_inactive: false - check_mode: true + algorithm: "HS256" + mark_previous_inactive: False + register: check_mode_create - - name: Search for Service Key 1 - ansible.builtin.set_fact: - item_that_should_not_exist: "{{ lookup('ansible.platform.gateway_api', 'service_keys', - query_params={'name': '{{ name_prefix }}-Key 1'}, **connection_info) }}" + - name: Include old tests + ansible.builtin.include_tasks: "creatable.yml" + when: check_mode_create.warnings | default([]) | length == 0 - - name: Assert that Service Key 1 does not exist - ansible.builtin.assert: - that: - - item_that_should_not_exist is not defined or item_that_should_not_exist | length == 0 - fail_msg: "Service Key '{{ name_prefix }}-Key 1' exists in the system!" - - - name: Create Service Key 1 - ansible.platform.service_key: - name: "{{ name_prefix }}-Key 1" - is_active: true - service_cluster: "{{ controller_sc.id }}" - algorithm: HS384 - secret: "gateway-secret" - mark_previous_inactive: false - register: service_key1 - - - name: Assert that we created service key 1 - ansible.builtin.assert: - that: - - service_key1 is changed - - # We have to take out the secret because its encrypted - - name: Recreate Service Key 1 - ansible.platform.service_key: - name: "{{ name_prefix }}-Key 1" - is_active: true - service_cluster: "{{ controller_sc.id }}" - algorithm: HS384 - mark_previous_inactive: false - register: recreate_service_key1 - - - name: Assert that recreate does not change the system - ansible.builtin.assert: - that: - - recreate_service_key1 is not changed - - - name: Create Service Key 2 - ansible.platform.service_key: - name: "{{ name_prefix }}-Key 2" - service_cluster: "{{ hub_sc.name }}" - secret: "gateway-secret" - mark_previous_inactive: true - register: service_key2 - - - name: Assert that we created service key 2 - ansible.builtin.assert: - that: - - service_key2 is changed - - - name: Create Service Key 3 - ansible.platform.service_key: - name: "{{ name_prefix }}-Key 3" - is_active: false - service_cluster: "{{ controller_sc.id }}" # Controller - mark_previous_inactive: false - register: service_key3 - - - name: Assert that we created service key 3 - ansible.builtin.assert: - that: - - service_key3 is changed - - - name: Create Service Key 4 - ansible.platform.service_key: - name: "{{ name_prefix }}-Key 4" - service_cluster: "{{ eda_sc.id }}" # EDA - mark_previous_inactive: false - register: service_key4 - - - name: Assert that we created service key 4 - ansible.builtin.assert: - that: - - service_key4 is changed - - - name: Create Service Key 5 - ansible.platform.service_key: - name: "{{ name_prefix }}-Key 5" - service_cluster: "{{ controller_sc.id }}" # Controller, have to set others as inactive - mark_previous_inactive: true - register: service_key5 - - - name: Assert that we created service key 5 - ansible.builtin.assert: - that: - - service_key5 is changed - - - name: Deactivate a key - ansible.platform.service_key: - name: "{{ service_key2.name }}" - is_active: false - register: change_service_key2 - - - name: Assert that we changed the existing key - ansible.builtin.assert: - that: - - change_service_key2 is changed - - change_service_key2.id == change_service_key2.id - - - name: See if a key exists - ansible.platform.service_key: - name: "{{ service_key3.id }}" - state: exists - register: exists_service_key3 - - - name: Assert that exists does not change the system - ansible.builtin.assert: - that: - - exists_service_key3 is not changed - - - name: Rename a key - ansible.platform.service_key: - name: "{{ service_key4.id }}" - new_name: "{{ service_key4.id }}-New" - register: rename_service_key4 - - - name: Assert that the rename changed an existing service key - ansible.builtin.assert: - that: - - rename_service_key4 is changed - - rename_service_key4.id == service_key4.id - - - name: Delete a non-existing service key - ansible.platform.service_key: - name: "{{ name_prefix }}-DNE" - state: absent - register: delete - - - name: Assert that delete of non-existent dies not change the system - ansible.builtin.assert: - that: - - delete is not changed - - - name: Delete an actual service key - ansible.platform.service_key: - name: "{{ service_key5.id }}" - state: absent - register: delete - - - name: Assert that the delete changed the system - ansible.builtin.assert: - that: - - delete is changed + - name: Include new tests + ansible.builtin.include_tasks: "editable.yml" + when: check_mode_create.warnings | default([]) | length > 0 always: # Always Cleanup - - name: Delete Service Keys - ansible.platform.service_key: - state: absent - name: "{{ vars[item].id }}" - when: "item in vars and 'id' in vars[item]" - loop: - - "service_key1" - - "service_key2" - - "service_key3" - - "service_key4" - - "service_key5" - - name: Delete Service Clusters ansible.platform.service_cluster: name: "{{ vars[item].id }}" diff --git a/tests/test_completeness.py b/tests/test_completeness.py index b982fd19..c581eb2a 100755 --- a/tests/test_completeness.py +++ b/tests/test_completeness.py @@ -18,7 +18,7 @@ # Normally a read-only endpoint should not have a module (i.e. /api/v2/me) but sometimes we reuse a name # For example, we have a role module but /api/v2/roles is a read only endpoint. # This list indicates which read-only endpoints have associated modules with them. -read_only_endpoints_with_modules = ["settings", "authenticator_user"] +read_only_endpoints_with_modules = ["settings", "authenticator_user", "service_keys"] # If a module should not be created for an endpoint and the endpoint is not read-only add it here # THINK HARD ABOUT DOING THIS