199 lines
5.2 KiB
Plaintext
199 lines
5.2 KiB
Plaintext
#!/usr/bin/env python3
|
|
"""
|
|
Custom r10k Webhook Server for Puppet Control Repo
|
|
"""
|
|
|
|
from datetime import datetime
|
|
import os
|
|
import subprocess
|
|
import logging
|
|
import hmac
|
|
import hashlib
|
|
|
|
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks
|
|
from fastapi.responses import JSONResponse
|
|
import uvicorn
|
|
from pydantic import BaseModel
|
|
|
|
|
|
# Configure logging
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(levelname)s - %(message)s',
|
|
handlers=[
|
|
logging.FileHandler('/var/log/r10k-webhook.log'),
|
|
logging.StreamHandler()
|
|
]
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
app = FastAPI(title="r10k Webhook Server")
|
|
|
|
|
|
class WebhookPayload(BaseModel):
|
|
"""Data model for webhook payload"""
|
|
ref: str
|
|
project: dict
|
|
commits: list
|
|
|
|
|
|
def run_r10k_deploy() -> bool:
|
|
"""Run r10k deploy command"""
|
|
try:
|
|
cmd = [
|
|
'/usr/bin/r10k', 'deploy',
|
|
'-v',
|
|
'-c', '/etc/puppetlabs/r10k/r10k.conf'
|
|
]
|
|
|
|
logger.info("Starting r10k deploy...")
|
|
result = subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
check=True
|
|
)
|
|
|
|
logger.info("r10k deploy successful!")
|
|
logger.debug("r10k stdout: %s", result.stdout)
|
|
if result.stderr:
|
|
logger.warning("r10k stderr: %s", result.stderr)
|
|
return True
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
logger.error("r10k deploy failed: %s", e)
|
|
logger.error("stdout: %s", e.stdout)
|
|
logger.error("stderr: %s", e.stderr)
|
|
return False
|
|
except FileNotFoundError:
|
|
logger.error("r10k binary not found")
|
|
return False
|
|
except PermissionError:
|
|
logger.error("Permission denied running r10k")
|
|
return False
|
|
|
|
|
|
def validate_signature(payload: bytes, signature: str, secret: str) -> bool:
|
|
"""Validate webhook signature"""
|
|
if not secret:
|
|
return True
|
|
|
|
expected = hmac.new(
|
|
secret.encode(),
|
|
payload,
|
|
hashlib.sha256
|
|
).hexdigest()
|
|
|
|
if signature.startswith('sha256='):
|
|
return hmac.compare_digest(signature, f'sha256={expected}')
|
|
|
|
return hmac.compare_digest(signature, expected)
|
|
|
|
|
|
@app.post("/webhook")
|
|
async def webhook_handler(
|
|
request: Request,
|
|
background_tasks: BackgroundTasks
|
|
):
|
|
"""Handle incoming webhook requests"""
|
|
|
|
body = await request.body()
|
|
headers = dict(request.headers)
|
|
event_type = headers.get(
|
|
'x-gitlab-event',
|
|
headers.get('x-github-event', 'unknown')
|
|
)
|
|
signature = headers.get(
|
|
'x-gitlab-token',
|
|
headers.get('x-hub-signature-256', '')
|
|
)
|
|
|
|
print(
|
|
f"DEBUG: Received webhook: event_type={event_type}, "
|
|
f"headers={headers}"
|
|
)
|
|
logger.info(
|
|
"Received webhook: event_type=%s, headers=%s",
|
|
event_type,
|
|
headers
|
|
)
|
|
|
|
webhook_secret = os.getenv('R10K_WEBHOOK_SECRET', '')
|
|
is_valid = validate_signature(body, signature, webhook_secret)
|
|
if webhook_secret and not is_valid:
|
|
logger.warning("Invalid webhook signature")
|
|
raise HTTPException(status_code=403, detail="Invalid signature")
|
|
|
|
try:
|
|
payload = await request.json()
|
|
ref = payload.get('ref', '')
|
|
branch = ref.split('/')[-1] if '/' in ref else ref
|
|
print(
|
|
f"DEBUG: Parsed payload: ref={ref}, "
|
|
f"branch={branch}"
|
|
)
|
|
logger.info("Parsed payload: ref=%s, branch=%s", ref, branch)
|
|
|
|
if branch not in ['main', 'master']:
|
|
logger.info("Ignoring non-main branch: %s", branch)
|
|
return JSONResponse({
|
|
"status": "ignored",
|
|
"branch": branch
|
|
})
|
|
|
|
# Match GitLab event types explicitly
|
|
valid_events = [
|
|
'push hook', 'merge request hook',
|
|
'push', 'Push', 'Push Hook'
|
|
]
|
|
normalized_event = event_type.lower().strip()
|
|
print(f"DEBUG: Normalized event: {normalized_event}")
|
|
logger.info("Normalized event: %s", normalized_event)
|
|
if normalized_event in valid_events:
|
|
logger.info("Triggering r10k for %s on %s", event_type, branch)
|
|
background_tasks.add_task(run_r10k_deploy)
|
|
return JSONResponse({
|
|
"status": "accepted",
|
|
"message": "r10k deploy triggered",
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
"branch": branch
|
|
})
|
|
|
|
logger.info("Ignoring event type: %s", event_type)
|
|
return JSONResponse({
|
|
"status": "ignored",
|
|
"event": event_type
|
|
})
|
|
|
|
except ValueError as e:
|
|
logger.error("Webhook processing error: %s", e)
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="Invalid payload"
|
|
) from e
|
|
|
|
|
|
@app.get("/health")
|
|
async def health_check():
|
|
"""Health check endpoint"""
|
|
result = subprocess.run(
|
|
['r10k', '--version'],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True
|
|
)
|
|
return {
|
|
"status": "healthy",
|
|
"r10k_version": result.stdout.strip()
|
|
}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
uvicorn.run(
|
|
"webhook_server:app",
|
|
host="0.0.0.0",
|
|
port=8080,
|
|
log_level="info"
|
|
)
|