diff --git a/src/aap_eda/settings/default.py b/src/aap_eda/settings/default.py index 30cd9da02..678ace7b0 100644 --- a/src/aap_eda/settings/default.py +++ b/src/aap_eda/settings/default.py @@ -688,3 +688,7 @@ def get_rulebook_process_log_level() -> RulebookProcessLogLevel: "SAFE_PLUGINS_FOR_PORT_FORWARD", ["ansible.eda.webhook", "ansible.eda.alertmanager"], ) + +API_PATH_TO_UI_PATH_MAP = settings.get( + "API_PATH_UI_PATH_MAP", {"/api/controller": "/execution", "/": "/#"} +) diff --git a/src/aap_eda/wsapi/consumers.py b/src/aap_eda/wsapi/consumers.py index f4d956328..0801a55da 100644 --- a/src/aap_eda/wsapi/consumers.py +++ b/src/aap_eda/wsapi/consumers.py @@ -4,6 +4,7 @@ import typing as tp from datetime import datetime from enum import Enum +from urllib.parse import urlparse, urlunparse import yaml from channels.db import database_sync_to_async @@ -224,10 +225,11 @@ def insert_audit_rule_data(self, message: ActionMessage) -> None: audit_rule = models.AuditRule.objects.filter( rule_uuid=message.rule_uuid, fired_at=message.rule_run_at ).first() + + activation_instance = models.RulebookProcess.objects.filter( + id=message.activation_id + ).first() if audit_rule is None: - activation_instance = models.RulebookProcess.objects.filter( - id=message.activation_id - ).first() activation_org = models.Organization.objects.filter( id=activation_instance.organization.id ).first() @@ -259,11 +261,27 @@ def insert_audit_rule_data(self, message: ActionMessage) -> None: ).first() if audit_action is None: + inputs = {} + aap_credential_type = models.CredentialType.objects.filter( + name=DefaultCredentialType.AAP + ) + if aap_credential_type: + credentials = ( + activation_instance.get_parent().eda_credentials.filter( + credential_type_id=aap_credential_type[0].id + ) + ) + if credentials: + inputs = yaml.safe_load( + credentials[0].inputs.get_secret_value() + ) + + url = self._get_url(message, inputs) audit_action = models.AuditAction.objects.create( id=message.action_uuid, fired_at=message.run_at, name=message.action, - url=message.url, + url=url, status=message.status, rule_fired_at=message.rule_run_at, audit_rule_id=audit_rule.id, @@ -417,3 +435,37 @@ def get_eda_system_vault_passwords( ) return vault_passwords + + def _get_url(self, message: ActionMessage, inputs: dict) -> str: + if message.action not in ("run_job_template", "run_workflow_template"): + return "" + url = message.url + if not message.controller_job_id: + return url + + api_url = inputs["host"] + urlparts = urlparse(api_url) + + path = urlparts.path.rstrip("/") + if path == "": + path = "/" + if path in settings.API_PATH_TO_UI_PATH_MAP: + path = settings.API_PATH_TO_UI_PATH_MAP[path] + + if message.action == "run_job_template": + slug = f"{path}/jobs/playbook/{message.controller_job_id}/details/" + elif message.action == "run_workflow_template": + slug = f"{path}/jobs/workflow/{message.controller_job_id}/details/" + + result = urlunparse( + [ + urlparts.scheme, + urlparts.netloc, + slug, + urlparts.params, + urlparts.query, + urlparts.fragment, + ] + ) + logger.debug("Updated Job URL %s", result) + return result diff --git a/src/aap_eda/wsapi/messages.py b/src/aap_eda/wsapi/messages.py index d153fc117..cf15e17c3 100644 --- a/src/aap_eda/wsapi/messages.py +++ b/src/aap_eda/wsapi/messages.py @@ -46,6 +46,7 @@ class ActionMessage(Message): delay: Optional[float] message: Optional[str] kind: Optional[str] + controller_job_id: Optional[str] class AnsibleEventMessage(Message): diff --git a/tests/integration/wsapi/test_consumer.py b/tests/integration/wsapi/test_consumer.py index a6cf9d89b..c03f52ec1 100644 --- a/tests/integration/wsapi/test_consumer.py +++ b/tests/integration/wsapi/test_consumer.py @@ -448,6 +448,119 @@ async def test_multiple_rules_for_one_event( assert await get_audit_event_action_count(event) == 2 +test_data = [ + ( + "run_job_template", + "55", + "http://gw/api/controller", + "http://controller.com/jobs/1/", + "http://gw/execution/jobs/playbook/55/details/", + ), + ( + "run_workflow_template", + "55", + "http://gw/api/controller", + "http://controller.com/jobs/workflow/55/", + "http://gw/execution/jobs/workflow/55/details/", + ), + ( + "run_job_template", + "55", + "http://gw/api/controller/", + "http://controller.com/jobs/1/", + "http://gw/execution/jobs/playbook/55/details/", + ), + ( + "run_workflow_template", + "55", + "http://gw/api/controller/", + "http://controller.com/jobs/workflow/55/", + "http://gw/execution/jobs/workflow/55/details/", + ), + ( + "run_job_template", + "55", + "http://controller.com", + "http://controller.com/jobs/playbook/2/", + "http://controller.com/#/jobs/playbook/55/details/", + ), + ( + "run_workflow_template", + "55", + "http://controller.com", + "http://controller.com/jobs/workflow/2/", + "http://controller.com/#/jobs/workflow/55/details/", + ), + ( + "run_job_template", + "55", + "http://controller.com/", + "http://controller.com/jobs/playbook/2/", + "http://controller.com/#/jobs/playbook/55/details/", + ), + ( + "run_workflow_template", + "55", + "http://controller.com/", + "http://controller.com/jobs/workflow/2/", + "http://controller.com/#/jobs/workflow/55/details/", + ), + ( + "run_workflow_template", + "", + "http://controller.com", + "http://controller.com/jobs/workflow/2/", + "http://controller.com/jobs/workflow/2/", + ), +] + + +@pytest.mark.parametrize( + "action_type, controller_job_id, api_url, old_url, new_url", test_data +) +@pytest.mark.django_db(transaction=True) +async def test_controller_job_url( + ws_communicator: WebsocketCommunicator, + preseed_credential_types, + action_type, + controller_job_id, + api_url, + old_url, + new_url, +): + my_aap_inputs = { + "host": api_url, + "username": "adam", + "password": "secret", + "ssl_verify": "no", + "oauth_token": "", + } + rulebook_process_id = await _prepare_activation_with_controller_info( + my_aap_inputs + ) + job_instance = await _prepare_job_instance() + + assert (await get_audit_rule_count()) == 0 + payload = create_action_payload( + DUMMY_UUID, + rulebook_process_id, + job_instance.uuid, + DUMMY_UUID, + "2023-03-29T15:00:17.260803Z", + _matching_events(), + "successful", + action_type, + old_url, + controller_job_id, + ) + await ws_communicator.send_json_to(payload) + await ws_communicator.wait() + + assert (await get_audit_action_count()) == 1 + action = await get_audit_action_first() + assert action.url == new_url + + @database_sync_to_async def get_rulebook_process(instance_id): return models.RulebookProcess.objects.get(pk=instance_id) @@ -461,6 +574,16 @@ def get_audit_events(): ) +@database_sync_to_async +def get_audit_events_first(): + return models.AuditEvent.objects.first() + + +@database_sync_to_async +def get_audit_action_first(): + return models.AuditAction.objects.first() + + @database_sync_to_async def get_audit_event_count(): return models.AuditEvent.objects.count() @@ -557,7 +680,7 @@ def _prepare_activation_instance_with_eda_system_vault_credential(): @database_sync_to_async -def _prepare_activation_with_controller_info(): +def _prepare_activation_with_controller_info(inputs=AAP_INPUTS): project, _ = models.Project.objects.get_or_create( name="test-project", url="https://github.com/test/project", @@ -589,7 +712,7 @@ def _prepare_activation_with_controller_info(): credential = models.EdaCredential.objects.create( name="eda_credential", - inputs=AAP_INPUTS, + inputs=inputs, managed=False, credential_type=aap_credential_type, ) @@ -804,10 +927,13 @@ def create_action_payload( rule_run_at, matching_events, action_status="successful", + action_name="run_playbook", + action_url="https://www.example.com/", + controller_job_id="55", ): return { "type": "Action", - "action": "run_playbook", + "action": action_name, "action_uuid": action_uuid, "activation_id": activation_instance_id, "job_id": job_instance_uuid, @@ -820,6 +946,8 @@ def create_action_payload( "matching_events": matching_events, "status": action_status, "message": "Action run successfully", + "url": action_url, + "controller_job_id": controller_job_id, }