diff --git a/files/webhook b/files/webhook new file mode 100644 index 0000000..024455b Binary files /dev/null and b/files/webhook differ diff --git a/manifests/params.pp b/manifests/params.pp index de0641c..9ba3749 100644 --- a/manifests/params.pp +++ b/manifests/params.pp @@ -201,9 +201,6 @@ class puppet_cd::params ( $pt_puppetdb_ssl = "${pt_puppetdb_main}/ssl" $pt_puppetdb_log = '/var/log/puppetlabs/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 ## puppet @@ -230,15 +227,8 @@ class puppet_cd::params ( $pt_puppetdb_repl_ini = "${pt_puppetdb_conf_d}/repl.ini" $pt_puppetdb_repl_erb = 'puppet_cd/puppetdb/repl.ini.erb' ## r10k - $pt_r10k_file = "${pt_r10k_dir}/r10k.yaml" - $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' - + $pt_r10k_hook_file = '/usr/local/bin/webhook' + # service $pt_server_service = 'puppetserver' $pt_agent_service = 'puppet' diff --git a/manifests/r10k/webhook.pp b/manifests/r10k/webhook.pp index 3a5e9ef..bd7eb6e 100644 --- a/manifests/r10k/webhook.pp +++ b/manifests/r10k/webhook.pp @@ -7,72 +7,24 @@ class puppet_cd::r10k::webhook ( ) inherits puppet_cd::params { if ($pt_pm_fqdn == $fqdn) and ($pt_use_r10k_webhook == true) { - # install packages - package { $pt_r10k_webhook_pkg: - ensure => $pt_pkg_ensure, - } - - # create the webhook dir - file { $pt_r10k_webhook_dir: - ensure => directory, + # create the webhook binary + file { $pt_r10k_hook_file, + ensure => file, owner => 'root', group => 'root', mode => '0755', selrange => s0, selrole => object_r, - seltype => etc_t, - seluser => system_u, + seltype => bin_t, + seluser => unconfined_u, + source => 'puppet:///module/puppet_cd/webhook', } - # create the requirements file - file { $pt_r10k_req_file: - ensure => file, - owner => 'puppet', - group => 'puppet', - 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], - } +# # manage service +# service { 'r10k-webhook': +# ensure => 'running', +# enable => true, +# subscribe => File[$pt_r10k_wh_config_file], +# } } } diff --git a/templates/r10k/requirements.txt.erb b/templates/r10k/requirements.txt.erb deleted file mode 100644 index 4d50e5b..0000000 --- a/templates/r10k/requirements.txt.erb +++ /dev/null @@ -1,4 +0,0 @@ -#fastapi==0.115.0 -#uvicorn==0.30.6 -#pydantic==2.8.2 -r10k-webhook \ No newline at end of file diff --git a/templates/r10k/webhook.py.erb b/templates/r10k/webhook.py.erb deleted file mode 100644 index c5f7862..0000000 --- a/templates/r10k/webhook.py.erb +++ /dev/null @@ -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" - )