diff --git a/cpo/lib/ansible/modules/abstract_module.py b/cpo/lib/ansible/modules/abstract_module.py index 5b7252d..74fad80 100644 --- a/cpo/lib/ansible/modules/abstract_module.py +++ b/cpo/lib/ansible/modules/abstract_module.py @@ -1,4 +1,4 @@ -# Copyright 2022 IBM Corporation +# Copyright 2022, 2024 IBM Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -87,13 +87,13 @@ def _wait_for_custom_resource( self, kind_metadata: KindMetadata, log_callback: Callable[[int, str], None], - success_callback: Callable[..., bool], + success_callback: Callable[..., Optional[CustomResourceEventResult]], **kwargs, - ): + ) -> CustomResourceEventResult: custom_objects_api = client.CustomObjectsApi() - succeeded = False + custom_resource_event_result: Optional[CustomResourceEventResult] = None - while not succeeded: + while custom_resource_event_result is None: try: resource_version: Optional[str] = None w = watch.Watch() @@ -106,9 +106,9 @@ def _wait_for_custom_resource( resource_version=resource_version, ): resource_version = cpo.lib.jmespath.get_jmespath_string("object.metadata.resourceVersion", event) - succeeded = success_callback(event, **kwargs) + custom_resource_event_result = success_callback(event, kind_metadata=kind_metadata, **kwargs) - if succeeded: + if custom_resource_event_result is not None: w.stop() except client.ApiException as exception: self._handle_api_exception(exception, log_callback) @@ -116,6 +116,8 @@ def _wait_for_custom_resource( except urllib3.exceptions.ProtocolError as exception: self._handle_protocol_error(exception, log_callback) + return custom_resource_event_result + def _wait_for_namespaced_custom_resource( self, project: str, diff --git a/cpo/lib/ansible/modules/wait_for_custom_resource.py b/cpo/lib/ansible/modules/wait_for_custom_resource.py new file mode 100644 index 0000000..b51bbed --- /dev/null +++ b/cpo/lib/ansible/modules/wait_for_custom_resource.py @@ -0,0 +1,130 @@ +# Copyright 2024 IBM Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from dataclasses import dataclass +from typing import Any, Optional + +from ansible.module_utils.basic import AnsibleModule + +import cpo.lib.jmespath + +from cpo.lib.ansible.modules.abstract_module import AbstractModule +from cpo.lib.ansible.modules.custom_resource_event_result import CustomResourceEventResult +from cpo.lib.jmespath import JmespathPathExpressionNotFoundException +from cpo.lib.openshift.types.kind_metadata import KindMetadata + + +@dataclass +class CustomResourceEventData: + custom_resource_name: str + + +class WaitForCustomResourceModule(AbstractModule): + def __init__(self): + argument_spec = { + "custom_resource_name": { + "type": "str", + "required": True, + }, + "group": { + "type": "str", + "required": True, + }, + "jmespath_expression": { + "type": "str", + "required": False, + }, + "kind": { + "type": "str", + "required": True, + }, + "kubeconfig": { + "type": "dict", + "required": True, + }, + "plural": { + "type": "str", + "required": True, + }, + "version": { + "type": "str", + "required": True, + }, + } + + self._module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + super().__init__(self._module.params["kubeconfig"]) # type: ignore + + self._custom_resource_name: str = self._module.params["custom_resource_name"] # type: ignore + self._jmespath_expression: Optional[str] = self._module.params["jmespath_expression"] # type: ignore + self._kind_metadata = KindMetadata( + self._module.params["group"], # type: ignore + self._module.params["kind"], # type: ignore + self._module.params["plural"], # type: ignore + self._module.params["version"], # type: ignore + ) + + # override + def get_module(self) -> AnsibleModule: + return self._module + + # override + def run(self): + result: Optional[dict] + + try: + self._wait_for_custom_resource( + self._kind_metadata, + self._log, + self._success_callback, + # passed to _add_event_indicates_custom_resource_definitions_are_created + custom_resource_event_data=CustomResourceEventData(custom_resource_name=self._custom_resource_name), + ) + + result = {"changed": False} + except Exception as exception: + self._log(logging.ERROR, str(exception)) + + result = {"changed": False, "failed": True} + + self._module.exit_json(**result) + + def _success_callback( + self, event: Any, kind_metadata: KindMetadata, custom_resource_event_data: CustomResourceEventData + ) -> Optional[CustomResourceEventResult]: + custom_resource_event_result: Optional[CustomResourceEventResult] = None + + if (event["type"] == "ADDED") or (event["type"] == "MODIFIED"): + resource_name = cpo.lib.jmespath.get_jmespath_string("object.metadata.name", event) + + try: + if resource_name == custom_resource_event_data.custom_resource_name: + if (self._jmespath_expression is None) or cpo.lib.jmespath.get_jmespath_bool( + self._jmespath_expression, event + ): + custom_resource_event_result = CustomResourceEventResult(True) + except JmespathPathExpressionNotFoundException: + pass + + return custom_resource_event_result + + +def main(): + WaitForCustomResourceModule().run() + + +if __name__ == "__main__": + main() diff --git a/cpo/lib/ansible/modules/wait_for_custom_resource_definitions.py b/cpo/lib/ansible/modules/wait_for_custom_resource_definitions.py index 04fcdcc..08e2447 100644 --- a/cpo/lib/ansible/modules/wait_for_custom_resource_definitions.py +++ b/cpo/lib/ansible/modules/wait_for_custom_resource_definitions.py @@ -1,4 +1,4 @@ -# Copyright 2022, 2023 IBM Corporation +# Copyright 2022, 2024 IBM Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import cpo.lib.jmespath from cpo.lib.ansible.modules.abstract_module import AbstractModule +from cpo.lib.ansible.modules.custom_resource_event_result import CustomResourceEventResult from cpo.lib.openshift.types.kind_metadata import KindMetadata @@ -77,14 +78,19 @@ def run(self): ) result = {"changed": False} - except Exception: + except Exception as exception: + self._log(logging.ERROR, str(exception)) + result = {"changed": False, "failed": True} self._module.exit_json(**result) def _add_event_indicates_custom_resource_definitions_are_created( - self, event: Any, custom_resource_definitions_event_data: CustomResourceDefinitionsEventData - ) -> bool: + self, + event: Any, + kind_metadata: KindMetadata, + custom_resource_definitions_event_data: CustomResourceDefinitionsEventData, + ) -> Optional[CustomResourceEventResult]: """Callback for checking whether the given set of expected custom resource definitions was created @@ -106,7 +112,7 @@ def _add_event_indicates_custom_resource_definitions_are_created( created """ - custom_resource_definitions_are_created = False + custom_resource_event_result: Optional[CustomResourceEventResult] = None if event["type"] == "ADDED": encountered_crd_kind = cpo.lib.jmespath.get_jmespath_string("object.spec.names.kind", event) @@ -115,12 +121,13 @@ def _add_event_indicates_custom_resource_definitions_are_created( self._log(logging.DEBUG, f"Detected creation of custom resource definition '{encountered_crd_kind}'") custom_resource_definitions_event_data.encountered_crd_kinds.add(encountered_crd_kind) - custom_resource_definitions_are_created = ( + if ( custom_resource_definitions_event_data.encountered_crd_kinds == custom_resource_definitions_event_data.expected_crd_kinds - ) + ): + custom_resource_event_result = CustomResourceEventResult(True) - return custom_resource_definitions_are_created + return custom_resource_event_result def main():