diff --git a/src/dispatch/incident/service.py b/src/dispatch/incident/service.py index 5aa2a3f0049f..6667c3e107ac 100644 --- a/src/dispatch/incident/service.py +++ b/src/dispatch/incident/service.py @@ -29,6 +29,7 @@ from dispatch.project import service as project_service from dispatch.tag import service as tag_service from dispatch.term import service as term_service +from dispatch.ticket import flows as ticket_flows from .enums import IncidentStatus from .models import Incident, IncidentCreate, IncidentRead, IncidentUpdate @@ -384,6 +385,16 @@ def update(*, db_session: Session, incident: Incident, incident_in: IncidentUpda incident_cost_service.update_incident_response_cost( incident_id=incident.id, db_session=db_session ) + # if the new incident type has plugin metadata and the + # project key of the ticket is the same, also update the ticket with the new metadata + if incident_type.plugin_metadata: + ticket_flows.update_incident_ticket_metadata( + db_session=db_session, + ticket_id=incident.ticket.resource_id, + project_id=incident.project.id, + incident_id=incident.id, + incident_type=incident_type, + ) update_data = incident_in.dict( skip_defaults=True, diff --git a/src/dispatch/plugins/dispatch_aws/plugin.py b/src/dispatch/plugins/dispatch_aws/plugin.py index 750fbbc8a047..9c120b01896d 100644 --- a/src/dispatch/plugins/dispatch_aws/plugin.py +++ b/src/dispatch/plugins/dispatch_aws/plugin.py @@ -74,15 +74,19 @@ def consume(self, db_session: Session, project: Project) -> None: entries: list[SqsEntries] = [] for message in response["Messages"]: - message_attributes = message.get("MessageAttributes", {}) - message_body = message["Body"] + try: + message_body = json.loads(message["Body"]) + message_body_message = message_body.get("Message") + message_attributes = message_body.get("MessageAttributes", {}) - if message_attributes.get("compressed", {}).get("StringValue") == "zlib": - # Message is compressed, decompress it - message_body = decompress_json(message_body) + if message_attributes.get("compressed", {}).get("Value") == "zlib": + # Message is compressed, decompress it + message_body_message = decompress_json(message_body_message) - message_body = json.loads(message_body) - signal_data = json.loads(message_body["Message"]) + signal_data = json.loads(message_body_message) + except Exception as e: + log.exception(f"Unable to extract signal data from SQS message: {e}") + continue try: signal_instance_in = SignalInstanceCreate( @@ -90,7 +94,7 @@ def consume(self, db_session: Session, project: Project) -> None: ) except ValidationError as e: log.warning( - f"Received a signal instance that does not conform to the `SignalInstanceCreate` structure. Skipping creation: {e}" + f"Received a signal instance that does not conform to the SignalInstanceCreate pydantic model. Skipping creation: {e}" ) continue @@ -100,7 +104,7 @@ def consume(self, db_session: Session, project: Project) -> None: db_session=db_session, signal_instance_id=signal_instance_in.raw["id"] ): log.info( - f"Received a signal instance that already exists in the database. Skipping creation: {signal_instance_in.raw['id']}" + f"Received a signal that already exists in the database. Skipping signal instance creation: {signal_instance_in.raw['id']}" ) continue @@ -113,15 +117,17 @@ def consume(self, db_session: Session, project: Project) -> None: except IntegrityError as e: if isinstance(e.orig, UniqueViolation): log.info( - f"Received a signal instance that already exists in the database. Skipping creation: {e}" + f"Received a signal that already exists in the database. Skipping signal instance creation: {e}" ) else: log.exception( - f"Encountered an Integrity error when trying to create a signal instance: {e}" + f"Encountered an integrity error when trying to create a signal instance: {e}" ) continue except Exception as e: - log.exception(f"Unable to create signal instance: {e}") + log.exception( + f"Unable to create signal instance. Signal name/variant: {signal_instance_in.raw['name'] if signal_instance_in.raw and signal_instance_in.raw['name'] else signal_instance_in.raw['variant']}. Error: {e}" + ) db_session.rollback() continue else: diff --git a/src/dispatch/plugins/dispatch_core/plugin.py b/src/dispatch/plugins/dispatch_core/plugin.py index f309690152e0..050e2626596e 100644 --- a/src/dispatch/plugins/dispatch_core/plugin.py +++ b/src/dispatch/plugins/dispatch_core/plugin.py @@ -312,6 +312,14 @@ def create_case_ticket( "resource_type": "dispatch-internal-ticket", } + def update_metadata( + self, + ticket_id: str, + metadata: dict, + ): + """Updates the metadata of a Dispatch ticket.""" + return + def update_case_ticket( self, ticket_id: str, diff --git a/src/dispatch/plugins/dispatch_jira/plugin.py b/src/dispatch/plugins/dispatch_jira/plugin.py index d66d1b848f66..222dbda7d774 100644 --- a/src/dispatch/plugins/dispatch_jira/plugin.py +++ b/src/dispatch/plugins/dispatch_jira/plugin.py @@ -315,6 +315,7 @@ def create( reporter = get_user_field(client, self.configuration, reporter_email) project_id, issue_type_name = process_plugin_metadata(incident_type_plugin_metadata) + other_fields = create_dict_from_plugin_metadata(incident_type_plugin_metadata) if not project_id: project_id = self.configuration.default_project_id @@ -335,6 +336,7 @@ def create( "assignee": assignee, "reporter": reporter, "summary": title, + **other_fields, } ticket = create(self.configuration, client, issue_fields) @@ -401,6 +403,31 @@ def update( return update(self.configuration, client, issue, issue_fields, status) + def update_metadata( + self, + ticket_id: str, + metadata: dict, + ): + """Updates the metadata of a Jira issue.""" + client = create_client(self.configuration) + issue = client.issue(ticket_id) + + # check to make sure project id matches metadata + project_id, issue_type_name = process_plugin_metadata(metadata) + if project_id and issue.fields.project.key != project_id: + log.warning( + f"Project key mismatch between Jira issue {issue.fields.project.key} and metadata {project_id} for ticket {ticket_id}" + ) + return + other_fields = create_dict_from_plugin_metadata(metadata) + issue_fields = { + **other_fields, + } + if issue_type_name: + issue_fields["issuetype"] = {"name": issue_type_name} + + issue.update(fields=issue_fields) + def create_case_ticket( self, case_id: int, diff --git a/src/dispatch/signal/service.py b/src/dispatch/signal/service.py index 751201594093..ba158d9e7b0e 100644 --- a/src/dispatch/signal/service.py +++ b/src/dispatch/signal/service.py @@ -544,9 +544,18 @@ def delete(*, db_session: Session, signal_id: int): return signal_id -def is_valid_uuid(val): +def is_valid_uuid(value) -> bool: + """ + Checks if the provided value is a valid UUID. + + Args: + val: The value to be checked. + + Returns: + bool: True if the value is a valid UUID, False otherwise. + """ try: - uuid.UUID(str(val), version=4) + uuid.UUID(str(value), version=4) return True except ValueError: return False @@ -587,7 +596,7 @@ def create_instance( signal_instance.id = signal_instance_in.raw["id"] if signal_instance.id and not is_valid_uuid(signal_instance.id): - msg = f"Invalid signal id format. Expecting UUID format. Received {signal_instance.id}." + msg = f"Invalid signal id format. Expecting UUIDv4 format. Signal id: {signal_instance.id}. Signal name/variant: {signal_instance.raw['name'] if signal_instance and signal_instance.raw and signal_instance.raw.get('name') else signal_instance.raw['variant']}" log.warn(msg) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, diff --git a/src/dispatch/static/dispatch/src/incident/type/IncidentTypeSelect.vue b/src/dispatch/static/dispatch/src/incident/type/IncidentTypeSelect.vue index 992e3012ce7c..199cc4c2f642 100644 --- a/src/dispatch/static/dispatch/src/incident/type/IncidentTypeSelect.vue +++ b/src/dispatch/static/dispatch/src/incident/type/IncidentTypeSelect.vue @@ -120,7 +120,6 @@ export default { sortBy: ["name"], descending: [false], itemsPerPage: this.numItems, - enabled: ["true"], } if (this.project) { @@ -132,6 +131,8 @@ export default { } } + filterOptions.filters["enabled"] = ["true"] + filterOptions = SearchUtils.createParametersFromTableOptions( { ...filterOptions }, "IncidentType" diff --git a/src/dispatch/ticket/flows.py b/src/dispatch/ticket/flows.py index 32e0ede24181..3ed09124c312 100644 --- a/src/dispatch/ticket/flows.py +++ b/src/dispatch/ticket/flows.py @@ -9,6 +9,7 @@ from dispatch.event import service as event_service from dispatch.incident import service as incident_service from dispatch.incident.models import Incident +from dispatch.incident.type.models import IncidentType from dispatch.incident.type import service as incident_type_service from dispatch.participant import service as participant_service from dispatch.plugin import service as plugin_service @@ -334,3 +335,38 @@ def create_task_ticket(task: Task, db_session: Session): db_session.commit() return external_ticket + + +def update_incident_ticket_metadata( + db_session: Session, + ticket_id: str, + project_id: int, + incident_id: int, + incident_type: IncidentType, +): + """ + Updates the metadata of an incident ticket. + """ + plugin = plugin_service.get_active_instance( + db_session=db_session, project_id=project_id, plugin_type="ticket" + ) + if not plugin: + log.warning("Incident ticket metadata not updated. No ticket plugin enabled.") + return + + # we update the external incident ticket + try: + plugin.instance.update_metadata( + ticket_id=ticket_id, + metadata=incident_type.get_meta(plugin.plugin.slug), + ) + except Exception as e: + log.exception(e) + return + + event_service.log_incident_event( + db_session=db_session, + source=plugin.plugin.title, + description="Incident ticket metadata updated", + incident_id=incident_id, + )