Skip to content

Commit a56bf90

Browse files
add mattermost notification
1 parent 62a0fc4 commit a56bf90

4 files changed

Lines changed: 149 additions & 86 deletions

File tree

k8s/deployment.yaml

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,40 @@ spec:
2121
image: service-monitor:latest # Update this with your registry/image:tag
2222
imagePullPolicy: Always
2323
env:
24-
- name: GITHUB_TOKEN
24+
- name: SEMAPHORE_URL
2525
valueFrom:
2626
secretKeyRef:
27-
name: github-token
28-
key: GITHUB_TOKEN
27+
name: semaphore-credentials
28+
key: SEMAPHORE_URL
29+
- name: SEMAPHORE_AUTH_TOKEN
30+
valueFrom:
31+
secretKeyRef:
32+
name: semaphore-credentials
33+
key: SEMAPHORE_AUTH_TOKEN
34+
- name: TENANT
35+
valueFrom:
36+
secretKeyRef:
37+
name: semaphore-credentials
38+
key: TENANT
39+
optional: true
40+
- name: PROJECT
41+
valueFrom:
42+
secretKeyRef:
43+
name: semaphore-credentials
44+
key: PROJECT
45+
optional: true
46+
- name: MATTERMOST_WEBHOOK_URL
47+
valueFrom:
48+
secretKeyRef:
49+
name: semaphore-credentials
50+
key: MATTERMOST_WEBHOOK_URL
51+
optional: true
52+
- name: K8S_CLUSTER_NAME
53+
valueFrom:
54+
secretKeyRef:
55+
name: semaphore-credentials
56+
key: K8S_CLUSTER_NAME
57+
optional: true
2958
resources:
3059
requests:
3160
cpu: "100m"

k8s/secret.yaml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
apiVersion: v1
22
kind: Secret
33
metadata:
4-
name: github-token
4+
name: semaphore-credentials
55
namespace: monitoring
66
type: Opaque
77
data:
8-
# Replace this with your base64 encoded GitHub token
9-
# Example: echo -n "your-token" | base64
10-
GITHUB_TOKEN: "YOUR_BASE64_ENCODED_TOKEN"
8+
# Replace these with your base64 encoded values
9+
# Example: echo -n "your-value" | base64
10+
SEMAPHORE_URL: "YOUR_BASE64_ENCODED_URL"
11+
SEMAPHORE_AUTH_TOKEN: "YOUR_BASE64_ENCODED_TOKEN"
12+
TENANT: "YOUR_BASE64_ENCODED_TENANT"
13+
PROJECT: "YOUR_BASE64_ENCODED_PROJECT"
14+
# Optional: Mattermost notifications
15+
MATTERMOST_WEBHOOK_URL: "YOUR_BASE64_ENCODED_MATTERMOST_WEBHOOK"
16+
K8S_CLUSTER_NAME: "YOUR_BASE64_ENCODED_CLUSTER_NAME"

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
kubernetes==29.0.0
2-
PyGithub==2.2.0
2+
requests==2.31.0

service_monitor_v2.py

Lines changed: 106 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
import sys
99
from typing import Dict, Optional
1010
from pathlib import Path
11+
import requests
1112
from kubernetes import client, config, watch
1213
from kubernetes.client.exceptions import ApiException
13-
from github import Github
1414
from datetime import datetime, timedelta
1515

1616
# Configure logging
@@ -20,13 +20,16 @@
2020
)
2121
logger = logging.getLogger(__name__)
2222

23-
# GitHub configuration
24-
GITHUB_TOKEN = os.environ.get('GITHUB_TOKEN')
25-
GITHUB_REPO = os.environ.get('GITHUB_REPO')
26-
WORKFLOW_FILE = os.environ.get('WORKFLOW_FILE')
23+
# Semaphore configuration
24+
SEMAPHORE_URL = os.environ.get('SEMAPHORE_URL')
25+
SEMAPHORE_AUTH_TOKEN = os.environ.get('SEMAPHORE_AUTH_TOKEN')
2726
TENANT = os.environ.get('TENANT')
2827
PROJECT = os.environ.get('PROJECT')
2928

29+
# Mattermost configuration
30+
MATTERMOST_WEBHOOK_URL = os.environ.get('MATTERMOST_WEBHOOK_URL')
31+
K8S_CLUSTER_NAME = os.environ.get('K8S_CLUSTER_NAME', 'unknown')
32+
3033
# Store the last trigger time globally
3134
last_trigger_global = 0
3235
DEBOUNCE_INTERVAL = 180 # 3 minutes in seconds
@@ -108,93 +111,118 @@ def get_initial_resource_version(v1) -> Optional[str]:
108111
logger.error(f"Failed to get initial resourceVersion: {e}")
109112
return None
110113

111-
def trigger_github_workflow(gh_token: str, event_type: str, service_key: str) -> bool:
114+
def send_mattermost_notification(event_type: str, namespace: str, service_name: str) -> bool:
112115
"""
113-
Trigger GitHub Actions workflow with debouncing and detailed error logging
114-
116+
Send a notification to Mattermost about the triggered Semaphore webhook.
117+
118+
Args:
119+
event_type: Type of the Kubernetes event
120+
namespace: Kubernetes namespace of the service
121+
service_name: Name of the service
122+
123+
Returns:
124+
bool: True if notification was sent, False otherwise
125+
"""
126+
if not MATTERMOST_WEBHOOK_URL:
127+
logger.debug("MATTERMOST_WEBHOOK_URL not set, skipping notification")
128+
return False
129+
130+
message = (
131+
f":rocket: **Loadbalancer update triggered**\n"
132+
f"**Cluster** `{K8S_CLUSTER_NAME}` // **Namespace** `{namespace}`\n"
133+
f"**Service** `{service_name}` // **Event** `{event_type}`"
134+
)
135+
136+
payload = {"text": message}
137+
138+
try:
139+
response = requests.post(
140+
MATTERMOST_WEBHOOK_URL,
141+
json=payload,
142+
timeout=10
143+
)
144+
145+
if response.status_code >= 200 and response.status_code < 300:
146+
logger.info(f"Mattermost notification sent for {namespace}/{service_name}")
147+
return True
148+
else:
149+
logger.warning(f"Mattermost notification failed with status {response.status_code}: {response.text}")
150+
return False
151+
152+
except requests.exceptions.RequestException as e:
153+
logger.warning(f"Failed to send Mattermost notification: {str(e)}")
154+
return False
155+
156+
def trigger_semaphore_webhook(event_type: str, service_key: str) -> bool:
157+
"""
158+
Trigger Semaphore webhook with debouncing and detailed error logging
159+
115160
Args:
116-
gh_token: GitHub authentication token
117161
event_type: Type of the Kubernetes event
118162
service_key: Unique identifier for the service (namespace/name)
119-
163+
120164
Returns:
121-
bool: True if workflow was triggered, False otherwise
165+
bool: True if webhook was triggered, False otherwise
122166
"""
123-
if not GITHUB_REPO:
124-
logger.error("GITHUB_REPO environment variable not set")
167+
if not SEMAPHORE_URL:
168+
logger.error("SEMAPHORE_URL environment variable not set")
125169
return False
126-
127-
if not WORKFLOW_FILE:
128-
logger.error("WORKFLOW_FILE environment variable not set")
170+
171+
if not SEMAPHORE_AUTH_TOKEN:
172+
logger.error("SEMAPHORE_AUTH_TOKEN environment variable not set")
129173
return False
130174

131175
current_time = time.time()
132176
last_trigger_time = get_last_trigger()
133-
177+
134178
# Check if enough time has passed since the last trigger
135179
time_since_last = current_time - last_trigger_time
136180
if time_since_last < DEBOUNCE_INTERVAL:
137-
logger.info(f"Skipping workflow trigger for {service_key} due to debouncing (last trigger was {int(time_since_last)} seconds ago)")
181+
logger.info(f"Skipping webhook trigger for {service_key} due to debouncing (last trigger was {int(time_since_last)} seconds ago)")
138182
return False
139183

184+
# Prepare payload
185+
payload = {
186+
"tenant": TENANT or "",
187+
"project": PROJECT or "",
188+
"branch": "main",
189+
"type": "ansible_workload"
190+
}
191+
192+
headers = {
193+
"SEMAPHORE-AUTH": SEMAPHORE_AUTH_TOKEN,
194+
"Content-Type": "application/json"
195+
}
196+
140197
try:
141-
logger.info(f"Connecting to GitHub repo: {GITHUB_REPO}")
142-
g = Github(gh_token)
143-
repo = g.get_repo(GITHUB_REPO)
144-
145-
# Get all workflows and log them for debugging
146-
workflows = list(repo.get_workflows())
147-
logger.info(f"Found {len(workflows)} workflows in repository")
148-
for wf in workflows:
149-
logger.info(f"Available workflow: {wf.path} (ID: {wf.id})")
150-
151-
# Get the workflow by filename
152-
workflow = None
153-
for wf in workflows:
154-
if wf.path.endswith(WORKFLOW_FILE):
155-
workflow = wf
156-
logger.info(f"Found matching workflow: {wf.path} (ID: {wf.id})")
157-
break
158-
159-
if workflow is None:
160-
logger.error(f"Workflow {WORKFLOW_FILE} not found in repository {GITHUB_REPO}")
161-
return False
198+
logger.info(f"Triggering Semaphore webhook at {SEMAPHORE_URL} with payload: {payload}")
162199

163-
# Prepare inputs
164-
inputs = {
165-
"team": TENANT or "",
166-
"project": PROJECT or ""
167-
}
168-
169-
logger.info(f"Triggering workflow dispatch for {workflow.path} with inputs: {inputs}")
170-
171-
# Create workflow dispatch event with inputs
172-
try:
173-
workflow.create_dispatch(
174-
ref="main", # You might want to make this configurable
175-
inputs=inputs
176-
)
200+
response = requests.post(
201+
SEMAPHORE_URL,
202+
json=payload,
203+
headers=headers,
204+
timeout=30
205+
)
206+
207+
if response.status_code >= 200 and response.status_code < 300:
177208
# Update the last trigger time only on successful dispatch
178209
set_last_trigger(current_time)
179-
logger.info(f"Successfully triggered workflow {workflow.path} for event: {event_type} (service: {service_key})")
210+
logger.info(f"Successfully triggered Semaphore webhook for event: {event_type} (service: {service_key}), response: {response.status_code}")
211+
212+
# Send Mattermost notification
213+
namespace, service_name = service_key.split('/', 1)
214+
send_mattermost_notification(event_type, namespace, service_name)
215+
180216
return True
181-
182-
except Exception as dispatch_error:
183-
logger.error(f"Failed to dispatch workflow: {str(dispatch_error)}")
184-
# Try to get more details about the error
185-
if hasattr(dispatch_error, 'response'):
186-
response = getattr(dispatch_error, 'response')
187-
if response:
188-
logger.error(f"GitHub API Response: {response.status_code} - {response.text}")
217+
else:
218+
logger.error(f"Semaphore webhook failed with status {response.status_code}: {response.text}")
189219
return False
190-
191-
except Exception as e:
192-
logger.error(f"Failed to trigger workflow: {str(e)}")
193-
# Try to get more details about the error
194-
if hasattr(e, 'response'):
195-
response = getattr(e, 'response')
196-
if response:
197-
logger.error(f"GitHub API Response: {response.status_code} - {response.text}")
220+
221+
except requests.exceptions.Timeout:
222+
logger.error("Semaphore webhook request timed out")
223+
return False
224+
except requests.exceptions.RequestException as e:
225+
logger.error(f"Failed to trigger Semaphore webhook: {str(e)}")
198226
return False
199227

200228
def initialize_kubernetes_client():
@@ -263,13 +291,10 @@ def watch_services(timeout_seconds: Optional[int] = None):
263291
if service.spec.type == 'LoadBalancer':
264292
logger.info(f"LoadBalancer service event: {event_type} - {service.metadata.namespace}/{service.metadata.name}")
265293

266-
# Trigger workflow for relevant events
294+
# Trigger webhook for relevant events
267295
if event_type in ['ADDED', 'MODIFIED', 'DELETED']:
268-
if GITHUB_TOKEN:
269-
service_key = f"{service.metadata.namespace}/{service.metadata.name}"
270-
trigger_github_workflow(GITHUB_TOKEN, event_type, service_key)
271-
else:
272-
logger.error("GITHUB_TOKEN environment variable not set")
296+
service_key = f"{service.metadata.namespace}/{service.metadata.name}"
297+
trigger_semaphore_webhook(event_type, service_key)
273298

274299
# Check if we've been watching for too long and should restart
275300
if time.time() - start_time > MAX_WATCH_TIME:
@@ -321,8 +346,11 @@ def main():
321346
"""
322347
Main function to start the service monitor with resilience
323348
"""
324-
if not GITHUB_TOKEN:
325-
logger.error("GITHUB_TOKEN environment variable must be set")
349+
if not SEMAPHORE_URL:
350+
logger.error("SEMAPHORE_URL environment variable must be set")
351+
exit(1)
352+
if not SEMAPHORE_AUTH_TOKEN:
353+
logger.error("SEMAPHORE_AUTH_TOKEN environment variable must be set")
326354
exit(1)
327355

328356
# Initialize Kubernetes client and get initial resourceVersion

0 commit comments

Comments
 (0)