Merakit-Deploy/gitea/gitea_deployer/webhooks.py

200 lines
5.9 KiB
Python

"""
Webhook notifications module
Send deployment event notifications with retry logic
"""
import logging
import time
from dataclasses import asdict, dataclass
from datetime import datetime
from typing import Any, Dict, Optional
import requests
logger = logging.getLogger(__name__)
@dataclass
class WebhookEvent:
"""Webhook event data"""
event_type: str # deployment_started, deployment_success, etc.
timestamp: str
subdomain: str
url: str
message: str
metadata: Dict[str, Any]
class WebhookNotifier:
"""Send webhook notifications with retry logic"""
def __init__(
self,
webhook_url: Optional[str],
timeout: int,
max_retries: int
):
"""
Initialize webhook notifier
Args:
webhook_url: Webhook URL to send notifications to (None to disable)
timeout: Request timeout in seconds
max_retries: Maximum number of retry attempts
"""
self._webhook_url = webhook_url
self._timeout = timeout
self._max_retries = max_retries
self._logger = logging.getLogger(f"{__name__}.WebhookNotifier")
if not webhook_url:
self._logger.debug("Webhook notifications disabled (no URL configured)")
def notify(self, event: WebhookEvent) -> None:
"""
Send webhook notification with retry
Args:
event: WebhookEvent to send
Note:
Failures are logged but don't raise exceptions to avoid
failing deployments due to webhook issues
"""
if not self._webhook_url:
return
payload = asdict(event)
self._logger.debug(f"Sending webhook: {event.event_type}")
for attempt in range(1, self._max_retries + 1):
try:
response = requests.post(
self._webhook_url,
json=payload,
timeout=self._timeout
)
response.raise_for_status()
self._logger.debug(
f"Webhook sent successfully: {event.event_type} "
f"(attempt {attempt})"
)
return
except requests.RequestException as e:
self._logger.warning(
f"Webhook delivery failed (attempt {attempt}/{self._max_retries}): {e}"
)
if attempt < self._max_retries:
# Exponential backoff: 1s, 2s, 4s, etc.
backoff = 2 ** (attempt - 1)
self._logger.debug(f"Retrying in {backoff}s...")
time.sleep(backoff)
self._logger.error(
f"Failed to deliver webhook after {self._max_retries} attempts: "
f"{event.event_type}"
)
def deployment_started(self, subdomain: str, url: str) -> None:
"""
Convenience method for deployment_started event
Args:
subdomain: Subdomain being deployed
url: Full URL being deployed
"""
event = WebhookEvent(
event_type="deployment_started",
timestamp=datetime.utcnow().isoformat() + "Z",
subdomain=subdomain,
url=url,
message=f"Deployment started for {url}",
metadata={}
)
self.notify(event)
def deployment_success(
self,
subdomain: str,
url: str,
duration: float
) -> None:
"""
Convenience method for deployment_success event
Args:
subdomain: Subdomain that was deployed
url: Full URL that was deployed
duration: Deployment duration in seconds
"""
event = WebhookEvent(
event_type="deployment_success",
timestamp=datetime.utcnow().isoformat() + "Z",
subdomain=subdomain,
url=url,
message=f"Deployment successful for {url}",
metadata={"duration": round(duration, 2)}
)
self.notify(event)
def deployment_failed(self, subdomain: str, error: str, url: str = "") -> None:
"""
Convenience method for deployment_failed event
Args:
subdomain: Subdomain that failed to deploy
error: Error message
url: Full URL (may be empty if deployment failed early)
"""
event = WebhookEvent(
event_type="deployment_failed",
timestamp=datetime.utcnow().isoformat() + "Z",
subdomain=subdomain,
url=url,
message=f"Deployment failed: {error}",
metadata={"error": error}
)
self.notify(event)
def dns_added(self, hostname: str, ip: str) -> None:
"""
Convenience method for dns_added event
Args:
hostname: Hostname that was added to DNS
ip: IP address the hostname points to
"""
event = WebhookEvent(
event_type="dns_added",
timestamp=datetime.utcnow().isoformat() + "Z",
subdomain=hostname.split('.')[0], # Extract subdomain
url=hostname,
message=f"DNS record added for {hostname}",
metadata={"ip": ip}
)
self.notify(event)
def health_check_passed(self, url: str, duration: float) -> None:
"""
Convenience method for health_check_passed event
Args:
url: URL that passed health check
duration: Time taken for health check in seconds
"""
event = WebhookEvent(
event_type="health_check_passed",
timestamp=datetime.utcnow().isoformat() + "Z",
subdomain=url.split('.')[0].replace('https://', '').replace('http://', ''),
url=url,
message=f"Health check passed for {url}",
metadata={"duration": round(duration, 2)}
)
self.notify(event)