starting over with hook - https://gitlab.confdroid.com/internal/confdroid_management/-/issues/284
This commit is contained in:
BIN
files/webhook
Normal file
BIN
files/webhook
Normal file
Binary file not shown.
@@ -201,9 +201,6 @@ class puppet_cd::params (
|
|||||||
$pt_puppetdb_ssl = "${pt_puppetdb_main}/ssl"
|
$pt_puppetdb_ssl = "${pt_puppetdb_main}/ssl"
|
||||||
$pt_puppetdb_log = '/var/log/puppetlabs/puppetdb'
|
$pt_puppetdb_log = '/var/log/puppetlabs/puppetdb'
|
||||||
$pt_puppetdb_var_dir = '/opt/puppetlabs/server/data/puppetdb'
|
$pt_puppetdb_var_dir = '/opt/puppetlabs/server/data/puppetdb'
|
||||||
## r10k
|
|
||||||
$pt_r10k_dir = "${pt_main_dir}/r10k"
|
|
||||||
$pt_r10k_webhook_dir = '/etc/r10k-webhook'
|
|
||||||
|
|
||||||
# files
|
# files
|
||||||
## puppet
|
## puppet
|
||||||
@@ -230,15 +227,8 @@ class puppet_cd::params (
|
|||||||
$pt_puppetdb_repl_ini = "${pt_puppetdb_conf_d}/repl.ini"
|
$pt_puppetdb_repl_ini = "${pt_puppetdb_conf_d}/repl.ini"
|
||||||
$pt_puppetdb_repl_erb = 'puppet_cd/puppetdb/repl.ini.erb'
|
$pt_puppetdb_repl_erb = 'puppet_cd/puppetdb/repl.ini.erb'
|
||||||
## r10k
|
## r10k
|
||||||
$pt_r10k_file = "${pt_r10k_dir}/r10k.yaml"
|
$pt_r10k_hook_file = '/usr/local/bin/webhook'
|
||||||
$pt_r10k_erb = 'puppet_cd/r10k/r10k.yaml.erb'
|
|
||||||
$pt_r10k_webhook_file = "${pt_r10k_webhook_dir}/webhook_server.py"
|
|
||||||
$pt_r10k_webhook_erb = 'puppet_cd/r10k/webhook.py.erb'
|
|
||||||
$pt_r10k_req_file = "${pt_r10k_webhook_dir}/requirements.txt"
|
|
||||||
$pt_r10k_req_erb = 'puppet_cd/r10k/requirements.txt.erb'
|
|
||||||
$pt_r10k_wh_config_file = "${pt_r10k_webhook_dir}/config.json"
|
|
||||||
$pt_r10k_wh_config_erb = 'puppet_cd/r10k/r10k_webhook_config.erb'
|
|
||||||
|
|
||||||
# service
|
# service
|
||||||
$pt_server_service = 'puppetserver'
|
$pt_server_service = 'puppetserver'
|
||||||
$pt_agent_service = 'puppet'
|
$pt_agent_service = 'puppet'
|
||||||
|
|||||||
@@ -7,72 +7,24 @@ class puppet_cd::r10k::webhook (
|
|||||||
|
|
||||||
) inherits puppet_cd::params {
|
) inherits puppet_cd::params {
|
||||||
if ($pt_pm_fqdn == $fqdn) and ($pt_use_r10k_webhook == true) {
|
if ($pt_pm_fqdn == $fqdn) and ($pt_use_r10k_webhook == true) {
|
||||||
# install packages
|
# create the webhook binary
|
||||||
package { $pt_r10k_webhook_pkg:
|
file { $pt_r10k_hook_file,
|
||||||
ensure => $pt_pkg_ensure,
|
ensure => file,
|
||||||
}
|
|
||||||
|
|
||||||
# create the webhook dir
|
|
||||||
file { $pt_r10k_webhook_dir:
|
|
||||||
ensure => directory,
|
|
||||||
owner => 'root',
|
owner => 'root',
|
||||||
group => 'root',
|
group => 'root',
|
||||||
mode => '0755',
|
mode => '0755',
|
||||||
selrange => s0,
|
selrange => s0,
|
||||||
selrole => object_r,
|
selrole => object_r,
|
||||||
seltype => etc_t,
|
seltype => bin_t,
|
||||||
seluser => system_u,
|
seluser => unconfined_u,
|
||||||
|
source => 'puppet:///module/puppet_cd/webhook',
|
||||||
}
|
}
|
||||||
|
|
||||||
# create the requirements file
|
# # manage service
|
||||||
file { $pt_r10k_req_file:
|
# service { 'r10k-webhook':
|
||||||
ensure => file,
|
# ensure => 'running',
|
||||||
owner => 'puppet',
|
# enable => true,
|
||||||
group => 'puppet',
|
# subscribe => File[$pt_r10k_wh_config_file],
|
||||||
mode => '0644',
|
# }
|
||||||
selrange => s0,
|
|
||||||
selrole => object_r,
|
|
||||||
seltype => etc_t,
|
|
||||||
seluser => system_u,
|
|
||||||
content => template($pt_r10k_req_erb),
|
|
||||||
}
|
|
||||||
|
|
||||||
# create the webhook config file
|
|
||||||
file { $pt_r10k_wh_config_file:
|
|
||||||
ensure => file,
|
|
||||||
owner => 'root',
|
|
||||||
group => 'root',
|
|
||||||
mode => '0644',
|
|
||||||
selrange => s0,
|
|
||||||
selrole => object_r,
|
|
||||||
seltype => etc_t,
|
|
||||||
seluser => system_u,
|
|
||||||
content => template($pt_r10k_wh_config_erb),
|
|
||||||
require => File[$pt_r10k_webhook_dir],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# install pip dependencies
|
|
||||||
exec { 'pip_install_r10k_webhook':
|
|
||||||
command => 'pip3 install --user -r /opt/r10k-webhook/requirements.txt',
|
|
||||||
user => 'puppet',
|
|
||||||
require => [Package[$pt_r10k_webhook_pkg],File[$pt_r10k_req_file]],
|
|
||||||
unless => 'pip3 show fastapi', # Idempotent check
|
|
||||||
}
|
|
||||||
|
|
||||||
# establish exec systemd reload
|
|
||||||
exec { 'systemctl_daemon_reload':
|
|
||||||
command => 'systemctl daemon-reload',
|
|
||||||
path => ['/bin', '/usr/bin'],
|
|
||||||
require => Exec['pip_install_r10k_webhook'],
|
|
||||||
refreshonly => true,
|
|
||||||
}
|
|
||||||
|
|
||||||
# manage service
|
|
||||||
service { 'r10k-webhook':
|
|
||||||
ensure => 'running',
|
|
||||||
enable => true,
|
|
||||||
subscribe => File[$pt_r10k_wh_config_file],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
#fastapi==0.115.0
|
|
||||||
#uvicorn==0.30.6
|
|
||||||
#pydantic==2.8.2
|
|
||||||
r10k-webhook
|
|
||||||
@@ -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"
|
|
||||||
)
|
|
||||||
Reference in New Issue
Block a user