Skip to content

Commit d071f0b

Browse files
[patch] send start message and update the same message on completion
1 parent e74a5a5 commit d071f0b

2 files changed

Lines changed: 88 additions & 6 deletions

File tree

bin/mas-devops-notify-slack

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,14 @@ def notifyAnsibleStart(channels: list[str], taskName: str, instanceId: str | Non
149149
SlackUtil.buildSection(f"⏳ **{taskName}** - Started")
150150
]
151151
response = SlackUtil.postMessageBlocks(channelId, taskMessage, threadId)
152+
153+
# Save message timestamp in ConfigMap for later editing
154+
if response.data.get("ok", False):
155+
messageTs = response.data.get("ts")
156+
if messageTs:
157+
# Store with task name as key
158+
SlackUtil.updateThreadConfigMap(namespace, instanceId, {f"task_{taskName}": messageTs})
159+
152160
if isinstance(response, list):
153161
return all([res.data.get("ok", False) for res in response])
154162
return response.data.get("ok", False)
@@ -166,24 +174,61 @@ def notifyAnsibleComplete(channels: list[str], rc: int, taskName: str, instanceI
166174
if threadInfo is None:
167175
print("No thread found - creating pipeline start notification")
168176
threadInfo = notifyPipelineStart(channels, instanceId, pipelineName)
169-
# Send task completion message as thread reply
177+
170178
threadId = threadInfo.get("threadId")
171179
channelId = threadInfo.get("channelId")
180+
181+
# Get the message timestamp for this task
182+
taskMessageTs = threadInfo.get(f"task_{taskName}")
183+
184+
# Determine status
172185
if rc == 0:
173186
emoji = "✅"
174187
status = "Success"
175188
else:
176189
emoji = "❌"
177190
status = "Failed"
191+
192+
# Calculate task duration if we have the message timestamp
193+
durationText = ""
194+
if taskMessageTs:
195+
from datetime import datetime
196+
try:
197+
# Message timestamp is in format "1234567890.123456"
198+
startTime = float(taskMessageTs)
199+
endTime = datetime.utcnow().timestamp()
200+
duration = int(endTime - startTime)
201+
202+
hours, remainder = divmod(duration, 3600)
203+
minutes, seconds = divmod(remainder, 60)
204+
205+
if hours > 0:
206+
durationText = f" ({hours}h {minutes}m {seconds}s)"
207+
elif minutes > 0:
208+
durationText = f" ({minutes}m {seconds}s)"
209+
else:
210+
durationText = f" ({seconds}s)"
211+
except Exception as e:
212+
print(f"Failed to calculate duration: {e}")
213+
214+
# Build the completion message
178215
taskMessage = [
179-
SlackUtil.buildSection(f"{emoji} **{taskName}** - {status}")
216+
SlackUtil.buildSection(f"{emoji} **{taskName}** - {status}{durationText}")
180217
]
181218
if rc != 0:
182219
taskMessage.append(SlackUtil.buildSection(f"Return Code: `{rc}`\nCheck logs for details"))
183-
response = SlackUtil.postMessageBlocks(channelId, taskMessage, threadId)
184-
if isinstance(response, list):
185-
return all([res.data.get("ok", False) for res in response])
186-
return response.data.get("ok", False)
220+
221+
# If we have the original message timestamp, update it; otherwise post new message
222+
if taskMessageTs:
223+
response = SlackUtil.updateMessageBlocks(channelId, taskMessageTs, taskMessage)
224+
return response.data.get("ok", False)
225+
else:
226+
# Fallback: post new message if task start message wasn't tracked
227+
print(f"No start message found for task {taskName}, posting new completion message")
228+
response = SlackUtil.postMessageBlocks(channelId, taskMessage, threadId)
229+
if isinstance(response, list):
230+
return all([res.data.get("ok", False) for res in response])
231+
return response.data.get("ok", False)
187232

188233

189234
def notifyPipelineComplete(channels: list[str], rc: int, instanceId: str | None = None, pipelineName: str | None = None) -> bool:

src/mas/devops/slack.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,43 @@ def getThreadConfigMap(cls, namespace: str, instanceId: str) -> dict | None:
343343
logger.error(f"Failed to retrieve ConfigMap: {e}")
344344
return None
345345

346+
def updateThreadConfigMap(cls, namespace: str, instanceId: str, updates: dict) -> bool:
347+
"""
348+
Update the ConfigMap with additional data (e.g., task message timestamps).
349+
350+
Parameters:
351+
namespace (str): Kubernetes namespace containing the ConfigMap
352+
instanceId (str): Unique identifier for the pipeline run
353+
updates (dict): Dictionary of key-value pairs to add/update in the ConfigMap
354+
355+
Returns:
356+
bool: True if ConfigMap was updated successfully, False otherwise
357+
"""
358+
try:
359+
# Load Kubernetes configuration
360+
try:
361+
config.load_incluster_config()
362+
except Exception:
363+
config.load_kube_config()
364+
v1 = client.CoreV1Api()
365+
configmap_name = f"slack-thread-{instanceId}"
366+
367+
# Get existing ConfigMap
368+
configmap = v1.read_namespaced_config_map(name=configmap_name, namespace=namespace)
369+
370+
# Update data
371+
if configmap.data is None:
372+
configmap.data = {}
373+
configmap.data.update(updates)
374+
375+
# Patch the ConfigMap
376+
v1.patch_namespaced_config_map(name=configmap_name, namespace=namespace, body=configmap)
377+
logger.debug(f"Updated ConfigMap {configmap_name} in namespace {namespace}")
378+
return True
379+
except Exception as e:
380+
logger.error(f"Failed to update ConfigMap: {e}")
381+
return False
382+
346383
def deleteThreadConfigMap(cls, namespace: str, instanceId: str) -> bool:
347384
"""
348385
Delete the ConfigMap containing Slack thread information.

0 commit comments

Comments
 (0)