|
8 | 8 | import sys |
9 | 9 | from typing import Dict, Optional |
10 | 10 | from pathlib import Path |
| 11 | +import requests |
11 | 12 | from kubernetes import client, config, watch |
12 | 13 | from kubernetes.client.exceptions import ApiException |
13 | | -from github import Github |
14 | 14 | from datetime import datetime, timedelta |
15 | 15 |
|
16 | 16 | # Configure logging |
|
20 | 20 | ) |
21 | 21 | logger = logging.getLogger(__name__) |
22 | 22 |
|
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') |
27 | 26 | TENANT = os.environ.get('TENANT') |
28 | 27 | PROJECT = os.environ.get('PROJECT') |
29 | 28 |
|
| 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 | + |
30 | 33 | # Store the last trigger time globally |
31 | 34 | last_trigger_global = 0 |
32 | 35 | DEBOUNCE_INTERVAL = 180 # 3 minutes in seconds |
@@ -108,93 +111,118 @@ def get_initial_resource_version(v1) -> Optional[str]: |
108 | 111 | logger.error(f"Failed to get initial resourceVersion: {e}") |
109 | 112 | return None |
110 | 113 |
|
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: |
112 | 115 | """ |
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 | +
|
115 | 160 | Args: |
116 | | - gh_token: GitHub authentication token |
117 | 161 | event_type: Type of the Kubernetes event |
118 | 162 | service_key: Unique identifier for the service (namespace/name) |
119 | | - |
| 163 | +
|
120 | 164 | Returns: |
121 | | - bool: True if workflow was triggered, False otherwise |
| 165 | + bool: True if webhook was triggered, False otherwise |
122 | 166 | """ |
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") |
125 | 169 | 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") |
129 | 173 | return False |
130 | 174 |
|
131 | 175 | current_time = time.time() |
132 | 176 | last_trigger_time = get_last_trigger() |
133 | | - |
| 177 | + |
134 | 178 | # Check if enough time has passed since the last trigger |
135 | 179 | time_since_last = current_time - last_trigger_time |
136 | 180 | 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)") |
138 | 182 | return False |
139 | 183 |
|
| 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 | + |
140 | 197 | 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}") |
162 | 199 |
|
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: |
177 | 208 | # Update the last trigger time only on successful dispatch |
178 | 209 | 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 | + |
180 | 216 | 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}") |
189 | 219 | 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)}") |
198 | 226 | return False |
199 | 227 |
|
200 | 228 | def initialize_kubernetes_client(): |
@@ -263,13 +291,10 @@ def watch_services(timeout_seconds: Optional[int] = None): |
263 | 291 | if service.spec.type == 'LoadBalancer': |
264 | 292 | logger.info(f"LoadBalancer service event: {event_type} - {service.metadata.namespace}/{service.metadata.name}") |
265 | 293 |
|
266 | | - # Trigger workflow for relevant events |
| 294 | + # Trigger webhook for relevant events |
267 | 295 | 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) |
273 | 298 |
|
274 | 299 | # Check if we've been watching for too long and should restart |
275 | 300 | if time.time() - start_time > MAX_WATCH_TIME: |
@@ -321,8 +346,11 @@ def main(): |
321 | 346 | """ |
322 | 347 | Main function to start the service monitor with resilience |
323 | 348 | """ |
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") |
326 | 354 | exit(1) |
327 | 355 |
|
328 | 356 | # Initialize Kubernetes client and get initial resourceVersion |
|
0 commit comments