Skip to content

Commit 3a9f965

Browse files
authored
Feat/decouple incident creation functions (#933)
* chore: add scripts to reset DynamoDB tables for incidents and webhooks * feat: add development environment configuration settings * feat: add success_creating message to incident localization files * feat: refactor channel creation logic * fix: re-enable log to sentinel * feat: implement create_incident_conversation function
1 parent 5711095 commit 3a9f965

File tree

9 files changed

+339
-73
lines changed

9 files changed

+339
-73
lines changed
File renamed without changes.

app/bin/reset_dev_webhooks_db.sh

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/bin/bash
2+
3+
delete_incident_table() {
4+
local TABLE_NAME=$1
5+
6+
# Check if table exists
7+
if aws dynamodb describe-table --table-name $TABLE_NAME --endpoint-url http://dynamodb-local:8000 >/dev/null 2>&1; then
8+
# Delete table
9+
aws dynamodb delete-table --table-name $TABLE_NAME --endpoint-url http://dynamodb-local:8000
10+
else
11+
echo "Table $TABLE_NAME does not exist, skipping deletion"
12+
fi
13+
}
14+
15+
create_incident_table() {
16+
local TABLE_NAME=$1
17+
18+
# Check if table exists
19+
if aws dynamodb describe-table --table-name $TABLE_NAME --endpoint-url http://dynamodb-local:8000 >/dev/null 2>&1; then
20+
echo "Table $TABLE_NAME already exists, skipping creation"
21+
else
22+
# Create table
23+
aws dynamodb create-table \
24+
--table-name $TABLE_NAME \
25+
--attribute-definitions AttributeName=id,AttributeType=S \
26+
--key-schema AttributeName=id,KeyType=HASH \
27+
--provisioned-throughput ReadCapacityUnits=2,WriteCapacityUnits=2 \
28+
--endpoint-url http://dynamodb-local:8000 \
29+
--no-cli-pager
30+
fi
31+
}
32+
33+
34+
delete_incident_table "webhooks"
35+
create_incident_table "webhooks"

app/core/config.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,17 @@ def validate_issuer_config(cls, v: Optional[Dict[str, Dict[str, Any]]]) -> Any:
271271
)
272272

273273

274+
class DevSettings(BaseSettings):
275+
"""Development environment configuration settings."""
276+
277+
SLACK_DEV_MSG_CHANNEL: str = Field(default="", alias="SLACK_DEV_MSG_CHANNEL")
278+
model_config = SettingsConfigDict(
279+
env_file=".env",
280+
case_sensitive=True,
281+
extra="ignore",
282+
)
283+
284+
274285
class FrontEndSettings(BaseSettings):
275286
FRONTEND_URL: str = Field(default="http://127.0.0.1:3000", alias="FRONTEND_URL")
276287
model_config = SettingsConfigDict(
@@ -308,6 +319,9 @@ class Settings(BaseSettings):
308319
aws_feature: AWSFeatureSettings
309320
feat_incident: IncidentFeatureSettings
310321

322+
# Development settings
323+
dev: DevSettings
324+
311325
@property
312326
def is_production(self) -> bool:
313327
"""Check if the application is running in production."""
@@ -330,6 +344,7 @@ def __init__(self, **kwargs):
330344
"reports": ReportsSettings,
331345
"aws_feature": AWSFeatureSettings,
332346
"feat_incident": IncidentFeatureSettings,
347+
"dev": DevSettings,
333348
}
334349

335350
for setting_name, setting_class in settings_map.items():

app/locales/incident.en-US.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ en-US:
1818
security_yes: "Yes"
1919
security_no: "No"
2020
success: "Incident successfully created"
21+
success_creating: "*Please wait while we are creating the incident channel*"
2122
user_added: "You have been added to the incident's conversation:\n"
2223
next_steps: "Wait! What's next?"
2324
next_steps_instructions: "Ask yourself these questions:"

app/locales/incident.fr-FR.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ fr-FR:
1818
security_yes: "Oui"
1919
security_no: "Non"
2020
success: "Incident créé avec succès"
21+
success_creating: "*Veuillez patienter pendant que nous créons le canal de l'incident*"
2122
user_added: "Vous avez ajouté à la discussion de cet incident:\n"
2223
next_steps: "Attendez! Que faire ensuite?"
2324
next_steps_instructions: "Posez-vous ces questions:"

app/modules/incident/incident.py

Lines changed: 67 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import re
2-
import datetime
32
import i18n # type: ignore
43
from slack_sdk import WebClient
5-
from slack_bolt import Ack
4+
from slack_bolt import Ack, App
65

76
from integrations import opsgenie
87
from integrations.slack import users as slack_users
9-
from integrations.sentinel import log_to_sentinel
108
from integrations.google_workspace import meet
9+
from integrations.sentinel import log_to_sentinel
1110

1211
from modules.incident import (
12+
incident_conversation,
1313
incident_folder,
1414
incident_document,
1515
db_operations,
@@ -31,14 +31,20 @@
3131
i18n.set("fallback", "en-US")
3232

3333

34-
def register(bot):
34+
def register(bot: App):
3535
bot.command(f"/{PREFIX}incident")(open_create_incident_modal)
3636
bot.view("incident_view")(submit)
3737
bot.action("incident_change_locale")(handle_change_locale_button)
3838

3939

40-
def open_create_incident_modal(client, ack, command, body):
40+
def open_create_incident_modal(client: WebClient, ack, command, body):
4141
ack()
42+
# private_metadata = json.dumps(
43+
# {
44+
# "channel_id": body["channel"]["id"],
45+
# "message_ts": body["message_ts"],
46+
# }
47+
# )
4248
logger.info(
4349
"incident_command_called",
4450
command=command,
@@ -73,7 +79,7 @@ def open_create_incident_modal(client, ack, command, body):
7379
}
7480
for i in folders
7581
]
76-
loaded_view = generate_incident_modal_view(command, options, locale)
82+
loaded_view = generate_incident_modal_view(command, options, None, locale)
7783
client.views_update(view_id=view["id"], view=loaded_view)
7884

7985

@@ -96,7 +102,7 @@ def handle_change_locale_button(ack, client, body):
96102
command = {"text": body["view"]["state"]["values"]["name"]["name"]["value"]}
97103
if command["text"] is None:
98104
command["text"] = ""
99-
view = generate_incident_modal_view(command, options, locale)
105+
view = generate_incident_modal_view(command, options, None, locale)
100106
client.views_update(view_id=body["view"]["id"], view=view)
101107

102108

@@ -126,6 +132,53 @@ def submit(ack: Ack, view, say, body, client: WebClient): # noqa: C901
126132
ack(response_action="errors", errors=errors)
127133
return
128134

135+
channel_id = None
136+
channel_name = None
137+
slug = None
138+
139+
# private_metadata = json.loads(body["view"].get("private_metadata"))
140+
# source_channel_id = private_metadata.get("channel_id")
141+
# source_message_ts = private_metadata.get("message_ts")
142+
try:
143+
channel_created = incident_conversation.create_incident_conversation(
144+
client, name
145+
)
146+
channel_id = channel_created["channel_id"]
147+
channel_name = channel_created["channel_name"]
148+
slug = channel_created["slug"]
149+
150+
except Exception as e:
151+
logger.error(
152+
"incident_channel_creation_failed",
153+
error=str(e),
154+
incident_name=name,
155+
)
156+
say(
157+
text=":warning: Channel creation failed. Please contact the SRE team.",
158+
channel=body["user"]["id"],
159+
)
160+
return
161+
162+
# if private_metadata:
163+
# # private_metadata = json.loads(private_metadata)
164+
# logger.info("private_metadata_found", private_metadata=private_metadata)
165+
# incident_alert.update_alert_with_channel_link(
166+
# client,
167+
# source_channel_id,
168+
# source_message_ts,
169+
# incident_details={"channel_id": channel_id, "channel_name": channel_name},
170+
# )
171+
172+
logger.info(
173+
"incident_channel_created",
174+
channel_id=channel_id,
175+
channel_name=channel_name,
176+
slug=slug,
177+
)
178+
179+
view = generate_success_modal(body, channel_id, channel_name)
180+
client.views_open(trigger_id=body["trigger_id"], view=view)
181+
129182
logger.info(
130183
"incident_modal_submitted",
131184
name=name,
@@ -150,42 +203,16 @@ def submit(ack: Ack, view, say, body, client: WebClient): # noqa: C901
150203
if r.get("ok"):
151204
oncall.append(r["user"])
152205

153-
date = datetime.datetime.now().strftime("%Y-%m-%d")
154-
slug = f"{date} {name}".replace(" ", "-").lower()
155-
156206
# Create channel
157-
# if we are testing ie PREFIX is "dev" then create the channel with name incident-dev-{slug}. Otherwise create the channel with name incident-{slug}
158207
environment = "prod"
159-
channel_to_create = f"incident-{slug}"
160208
if PREFIX == "dev-":
161209
environment = "dev"
162-
channel_to_create = f"incident-dev-{slug}"
163-
try:
164-
if len(channel_to_create) > 80:
165-
channel_to_create = channel_to_create[:80]
166-
response = client.conversations_create(name=channel_to_create)
167-
except Exception as e:
168-
logger.error(
169-
"incident_channel_creation_failed",
170-
error=str(e),
171-
channel_name=channel_to_create,
172-
)
173-
say(
174-
text=":warning: Channel creation failed. Please contact the SRE team.",
175-
channel=body["user"]["id"],
176-
)
177-
return
178-
channel_id = response["channel"]["id"]
179-
channel_name = response["channel"]["name"]
180210
logger.info(
181211
"incident_channel_created",
182212
channel_id=channel_id,
183213
channel_name=channel_name,
184214
)
185215

186-
view = generate_success_modal(body, channel_id, channel_name)
187-
client.views_open(trigger_id=body["trigger_id"], view=view)
188-
189216
channel_url = f"https://gcdigital.slack.com/archives/{channel_id}"
190217

191218
# Set topic
@@ -318,13 +345,19 @@ def submit(ack: Ack, view, say, body, client: WebClient): # noqa: C901
318345
)
319346

320347

321-
def generate_incident_modal_view(command, options=[], locale="en-US"):
348+
def generate_incident_modal_view(
349+
command, options=None, private_metadata=None, locale="en-US"
350+
):
351+
"""Generate the incident creation modal view."""
352+
if options is None:
353+
options = []
322354
handbook_string = f"For more details on what constitutes a security incident, visit our <{INCIDENT_HANDBOOK_URL}|Incident Management Handbook>"
323355
return {
324356
"type": "modal",
325357
"callback_id": "incident_view",
326358
"title": {"type": "plain_text", "text": i18n.t("incident.modal.title")},
327359
"submit": {"type": "plain_text", "text": i18n.t("incident.submit")},
360+
"private_metadata": private_metadata,
328361
"blocks": [
329362
{
330363
"type": "actions",

app/modules/incident/incident_conversation.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytz # type: ignore
44
from slack_sdk import WebClient # type: ignore
55
from slack_sdk.errors import SlackApiError # type: ignore
6+
from slack_sdk.web import SlackResponse # type: ignore
67
from integrations.google_workspace import google_docs
78
from integrations.slack import users as slack_users
89
from integrations.sentinel import log_to_sentinel
@@ -13,14 +14,72 @@
1314
)
1415
from modules.incident import incident_helper, schedule_retro
1516
from core.logging import get_module_logger
17+
from core.config import settings
1618

1719

20+
PREFIX = settings.PREFIX
21+
1822
START_HEADING = "DO NOT REMOVE this line as the SRE bot needs it as a placeholder."
1923
END_HEADING = "Trigger"
2024

2125
logger = get_module_logger()
2226

2327

28+
def create_incident_conversation(client: WebClient, incident_name: str):
29+
"""Create an incident conversation channel.
30+
31+
Args:
32+
client (WebClient): The Slack WebClient instance.
33+
incident_name (str): The name of the incident.
34+
35+
Returns:
36+
dict: A dictionary containing the channel ID, channel name, and slug.
37+
"""
38+
date_now = datetime.now().strftime("%Y-%m-%d")
39+
slug = f"{date_now} {incident_name}".strip().replace(" ", "-").lower()
40+
channel_name_prefix = "incident-dev-" if PREFIX == "dev-" else "incident-"
41+
base_channel_name = channel_name_prefix + slug
42+
# Ensure base_channel_name is at most 80 chars
43+
if len(base_channel_name) > 80:
44+
base_channel_name = base_channel_name[:80]
45+
i = 0
46+
while True:
47+
if i == 0:
48+
attempt_channel_name = base_channel_name
49+
else:
50+
# Calculate the max length for the base part so that the suffix fits in 80 chars
51+
suffix = f"-{i}"
52+
max_base_length = 80 - len(suffix)
53+
truncated_base = base_channel_name[:max_base_length]
54+
attempt_channel_name = f"{truncated_base}{suffix}"
55+
try:
56+
response: SlackResponse = client.conversations_create(
57+
name=attempt_channel_name
58+
)
59+
if response.get("ok"):
60+
if i > 0:
61+
slug = f"{slug}{suffix}"
62+
channel_details = response.get("channel")
63+
if not isinstance(channel_details, dict):
64+
raise SlackApiError("Error creating the channel", response)
65+
channel_id = channel_details.get("id", "")
66+
channel_name = attempt_channel_name
67+
break
68+
elif response.get("error") != "name_taken":
69+
raise SlackApiError("Error creating the channel", response)
70+
except SlackApiError as e:
71+
# If the error is name_taken, continue to next attempt; otherwise, raise
72+
if (
73+
hasattr(e, "response")
74+
and getattr(e.response, "data", {}).get("error") == "name_taken"
75+
):
76+
pass # Try next channel name
77+
else:
78+
raise
79+
i += 1
80+
return {"channel_id": channel_id, "channel_name": channel_name, "slug": slug}
81+
82+
2483
# Make sure that we are listening only on floppy disk reaction
2584
def is_floppy_disk(event: dict) -> bool:
2685
return event["reaction"] == "floppy_disk"

0 commit comments

Comments
 (0)