diff --git a/src/dispatch/database/revisions/tenant/versions/2024-10-25_24322617ce9a.py b/src/dispatch/database/revisions/tenant/versions/2024-10-25_24322617ce9a.py new file mode 100644 index 000000000000..efd161033e6e --- /dev/null +++ b/src/dispatch/database/revisions/tenant/versions/2024-10-25_24322617ce9a.py @@ -0,0 +1,99 @@ +"""Adds configuration to the Dispatch Ticket PluginInstance + +Revision ID: 24322617ce9a +Revises: 3c49f62d7914 +Create Date: 2024-10-25 15:15:38.078421 + +""" + +from alembic import op +from pydantic import SecretStr, ValidationError +from pydantic.json import pydantic_encoder + +from sqlalchemy import Column, Integer, ForeignKey, String +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import relationship, Session +from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy_utils import StringEncryptedType +from sqlalchemy_utils.types.encrypted.encrypted_type import AesEngine +from dispatch.config import DISPATCH_ENCRYPTION_KEY + +# revision identifiers, used by Alembic. +revision = "24322617ce9a" +down_revision = "3c49f62d7914" +branch_labels = None +depends_on = None + +Base = declarative_base() + + +def show_secrets_encoder(obj): + if isinstance(obj, SecretStr): + return obj.get_secret_value() + else: + return pydantic_encoder(obj) + + +def migrate_config(instances, slug, config): + for instance in instances: + if slug == instance.plugin.slug: + instance.configuration = config + + +class Plugin(Base): + __tablename__ = "plugin" + __table_args__ = {"schema": "dispatch_core"} + id = Column(Integer, primary_key=True) + slug = Column(String, unique=True) + + +class PluginInstance(Base): + __tablename__ = "plugin_instance" + id = Column(Integer, primary_key=True) + _configuration = Column( + StringEncryptedType(key=str(DISPATCH_ENCRYPTION_KEY), engine=AesEngine, padding="pkcs5") + ) + plugin_id = Column(Integer, ForeignKey(Plugin.id)) + plugin = relationship(Plugin, backref="instances") + + @hybrid_property + def configuration(self): + """Property that correctly returns a plugins configuration object.""" + pass + + @configuration.setter + def configuration(self, configuration): + """Property that correctly sets a plugins configuration object.""" + if configuration: + self._configuration = configuration.json(encoder=show_secrets_encoder) + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + from dispatch.plugins.dispatch_core.config import DispatchTicketConfiguration + + bind = op.get_bind() + session = Session(bind=bind) + + instances = session.query(PluginInstance).all() + + try: + dispatch_ticket_config = DispatchTicketConfiguration( + use_incident_name=False, + ) + + migrate_config(instances, "dispatch-ticket", dispatch_ticket_config) + + except ValidationError: + print( + "Skipping automatic migration of Dispatch ticket plugin, if you are using the Dispatch ticket plugin, please manually migrate." + ) + + session.commit() + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/src/dispatch/plugins/dispatch_core/config.py b/src/dispatch/plugins/dispatch_core/config.py index 5d7efd6f9e0d..76f21661f94b 100644 --- a/src/dispatch/plugins/dispatch_core/config.py +++ b/src/dispatch/plugins/dispatch_core/config.py @@ -1,9 +1,20 @@ import logging +from dispatch.config import BaseConfigurationModel from starlette.config import Config - +from pydantic import Field log = logging.getLogger(__name__) config = Config(".env") + + +class DispatchTicketConfiguration(BaseConfigurationModel): + """Dispatch ticket configuration""" + + use_incident_name: bool = Field( + True, + title="Use Incident Name", + description="Use the incident name as the ticket title.", + ) diff --git a/src/dispatch/plugins/dispatch_core/plugin.py b/src/dispatch/plugins/dispatch_core/plugin.py index f8e881f160aa..5971b905f59a 100644 --- a/src/dispatch/plugins/dispatch_core/plugin.py +++ b/src/dispatch/plugins/dispatch_core/plugin.py @@ -61,6 +61,8 @@ from dispatch.service.models import Service, ServiceRead from dispatch.team import service as team_service from dispatch.team.models import TeamContact, TeamContactRead +from dispatch.plugins.dispatch_core.config import DispatchTicketConfiguration +from dispatch.plugins.dispatch_core.service import create_resource_id log = logging.getLogger(__name__) @@ -175,6 +177,9 @@ class DispatchTicketPlugin(TicketPlugin): author = "Netflix" author_url = "https://github.com/netflix/dispatch.git" + def __init__(self): + self.configuration_schema = DispatchTicketConfiguration + def create( self, incident_id: int, @@ -187,9 +192,11 @@ def create( """Creates a Dispatch incident ticket.""" incident = incident_service.get(db_session=db_session, incident_id=incident_id) - resource_id = ( - f"dispatch-{incident.project.organization.slug}-{incident.project.slug}-{incident.id}" - ) + if self.configuration and self.configuration.use_incident_name: + resource_id = create_resource_id(f"{incident.project.slug}-{title}-{incident.id}") + else: + resource_id = f"dispatch-{incident.project.organization.slug}-{incident.project.slug}-{incident.id}" + return { "resource_id": resource_id, "weblink": f"{DISPATCH_UI_URL}/{incident.project.organization.name}/incidents/{resource_id}?project={incident.project.name}", diff --git a/src/dispatch/plugins/dispatch_core/service.py b/src/dispatch/plugins/dispatch_core/service.py new file mode 100644 index 000000000000..b349849b53d7 --- /dev/null +++ b/src/dispatch/plugins/dispatch_core/service.py @@ -0,0 +1,20 @@ +import re + + +def create_resource_id(title: str) -> str: + """Creates a Slack-friendly resource id from the incident title.""" + resource_id = title.lower() + + # Replace any character that is not a lowercase letter or number with a hyphen + resource_id = re.sub(r"[^a-z0-9]", "-", resource_id) + + # Replace multiple consecutive hyphens with a single hyphen + resource_id = re.sub(r"-+", "-", resource_id) + + # Ensure the channel name is not longer than 80 characters + resource_id = resource_id[:80] + + # Remove leading or trailing hyphens + resource_id = resource_id.strip("-") + + return resource_id