3
0
This commit is contained in:
Arne Teuke
2025-10-23 20:18:11 +02:00
parent f8d861cbca
commit fc224074d2
5 changed files with 14 additions and 274 deletions

View File

@@ -1,4 +0,0 @@
#fastapi==0.115.0
#uvicorn==0.30.6
#pydantic==2.8.2
r10k-webhook

View File

@@ -1,198 +0,0 @@
#!/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"
)