3
0
Arne Teuke
2025-10-21 20:58:41 +02:00
parent 20f767283b
commit 4da12ae2f8
9 changed files with 311 additions and 12 deletions

19
.vscode/settings.json vendored
View File

@@ -1,11 +1,28 @@
{
"cSpell.words": [
"appender",
"asctime",
"basedirt",
"cachedir",
"devel",
"fastapi",
"getenv",
"hashlib",
"hmac",
"httpx",
"isoformat",
"kahadb",
"levelname",
"logappender",
"pydantic",
"pylint",
"pytest",
"requestlogging",
"springframework",
"startswith",
"Supress",
"trapperkeeper"
"trapperkeeper",
"utcnow",
"uvicorn"
]
}

View File

@@ -11,4 +11,9 @@ class puppet_cd::main::config (
if $pt_use_puppetdb == true {
include puppet_cd::puppetdb::service
}
if $pt_use_r10k == true {
include puppet_cd::r10k::install
include puppet_cd::r10k::webhook
}
}

View File

@@ -10,10 +10,10 @@
# @param [String] pt_agent_pkg the packages for agents to install
# @param [String] pt_server_pkg the server packages to install
# @param [Array] pt_db_pkg the packages for puppetdb
# @param [Array] pt_r10k_pkg the packages for r10k to install
# @param [String] pt_no_ssl_port non-ssl port number for puppetdb
# @param [String] pt_ssl_port ssl port for puppetdb
# @param [Boolean] pt_use_ssl_only whether to use ssl only.
# @param [Boolean] pt_manage_user whether to manage the puppet user
# @param [String] pt_user the puppet user
# @param [String] pt_user_comment the user comment
# @param [String] pt_user_home the user home
@@ -77,6 +77,11 @@
# @param [Boolean] pt_enable_repl whether to allow puppetdb replication
# @param [String] pt_repl_port the replication port
# @param [String] pt_repl_host the replication host
# @param [Boolean] pt_use_r10k whether to use r10k service
# @param [Boolean] pt_use_r10k_webhook whether to use r10k webhook service
# @param [String] pt_r10k_remote the remote url for the r10k control repo
# @param [Boolean] pt_r10k_prefix the r10k prefix. defaults to false
# @param [String] pt_r10k_basedir the base directory for r10k.yaml
###############################################################################
class puppet_cd::params (
@@ -89,6 +94,7 @@ class puppet_cd::params (
String $pt_agent_pkg = 'puppet-agent',
String $pt_server_pkg = 'puppetserver',
Array $pt_db_pkg = ['puppetdb','puppetdb-termini'],
Array $pt_r10k_pkg = ['ruby','ruby-devel'],
# user settings
## puppet user
@@ -132,7 +138,7 @@ class puppet_cd::params (
String $pt_storeconfigs_backend = 'puppetdb',
String $pt_parser = 'current',
Boolean $pt_cert_revocation = true,
## puppetdb
## puppetdb
Boolean $pt_use_puppetdb = false,
String $pt_logging_max_file_size = '200MB',
String $pt_logging_max_history = '90',
@@ -160,8 +166,19 @@ class puppet_cd::params (
String $pt_repl_port = '8082',
String $pt_repl_host = '127.0.0.1',
# r10k
Boolean $pt_use_r10k = false,
Boolean $pt_use_r10k_webhook = false,
String $pt_r10k_remote = 'git@gitlab.example.net/repo.git',
Boolean $pt_r10k_prefix = false,
String $pt_r10k_basedir = '/etc/puppetlabs/code/environments',
) {
# facts
$fqdn = $facts['networking']['fqdn']
$domain = $facts['networking']['domain']
$os_name = $facts['os']['name']
$os_release = $facts['os']['release']['major']
# directories
## puppet
@@ -181,6 +198,8 @@ 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"
# files
## puppet
@@ -190,22 +209,25 @@ class puppet_cd::params (
$pt_hiera_config = "${pt_puppetdir}/hiera.yaml"
## puppetdb
$pt_bootstrap_conf = "${pt_puppetdb_main}/bootstrap.cfg"
$pt_bootstrap_erb = 'cd_puppet/puppetdb/bootstrap.cfg.erb'
$pt_bootstrap_erb = 'puppet_cd/puppetdb/bootstrap.cfg.erb'
$pt_puppetdb_access_log = "${pt_puppetdb_log}/puppetdb-access"
$pt_request_logging_conf = "${pt_puppetdb_main}/request-logging.xml"
$pt_request_logging_erb = 'cd_puppet/puppetdb/request_logging.xml.erb'
$pt_request_logging_erb = 'puppet_cd/puppetdb/request_logging.xml.erb'
$pt_logback_conf = "${pt_puppetdb_main}/logback.xml"
$pt_logback_erb = 'cd_puppet/puppetdb/logback.xml.erb'
$pt_logback_erb = 'puppet_cd/puppetdb/logback.xml.erb'
$pt_puppetdb_config_ini = "${pt_puppetdb_conf_d}/config.ini"
$pt_puppetdb_config_erb = 'cd_puppet/puppetdb/config.ini.erb'
$pt_puppetdb_config_erb = 'puppet_cd/puppetdb/config.ini.erb'
$pt_puppetdb_database_ini = "${pt_puppetdb_conf_d}/database.ini"
$pt_puppetdb_database_erb = 'cd_puppet/puppetdb/database.ini.erb'
$pt_puppetdb_database_erb = 'puppet_cd/puppetdb/database.ini.erb'
$pt_puppetdb_jetty_ini = "${pt_puppetdb_conf_d}/jetty.ini"
$pt_puppetdb_jetty_erb = 'cd_puppet/puppetdb/jetty.ini.erb'
$pt_puppetdb_jetty_erb = 'puppet_cd/puppetdb/jetty.ini.erb'
$pt_puppetdb_conf_file = "${pt_puppetdir}/puppetdb.conf"
$pt_puppetdb_conf_erb = 'cd_puppet/puppetdb/puppetdb.conf.erb'
$pt_puppetdb_conf_erb = 'puppet_cd/puppetdb/puppetdb.conf.erb'
$pt_puppetdb_repl_ini = "${pt_puppetdb_conf_d}/repl.ini"
$pt_puppetdb_repl_erb = 'cd_puppet/puppetdb/repl.ini.erb'
$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'
# service
$pt_server_service = 'puppetserver'

35
manifests/r10k/install.pp Normal file
View File

@@ -0,0 +1,35 @@
## puppet_cd::r10k::install.pp
# Module name: puppet_cd
# Author: Arne Teuke (arne_teuke@confdroid)
# @summary Class manages r10k installation for the puppet_cd module.
###############################################################################
class puppet_cd::r10k::install (
) inherits puppet_cd::params {
if ($pt_pm_fqdn == $fqdn) and ($pt_use_r10k == true) {
# install required packages
package { $pt_r10k_pkg:
ensure => $pt_pkg_ensure,
before => Package['r10k'],
}
# install r10k via gem
package { 'r10k':
ensure => $pt_pkg_ensure,
provider => gem,
}
# configure r10k.yaml
file { $pt_r10k_file:
ensure => file,
owner => 'root',
group => 'root',
mode => '0440',
selrange => s0,
selrole => object_r,
seltype => puppet_etc_t,
seluser => unconfined_u,
content => template($pt_r10k_erb),
}
}
}

10
manifests/r10k/webhook.pp Normal file
View File

@@ -0,0 +1,10 @@
## puppet_cd::r10k::webhook.pp
# Module name: puppet_cd
# Author: Arne Teuke (arne_teuke@confdroid)
# @summary Class manages r10k webhook settings for the puppet_cd module.
###############################################################################
class puppet_cd::r10k::webhook (
) inherits puppet_cd::params {
}

0
templates/puppetdb/logback.xml.erb Executable file → Normal file
View File

View File

@@ -0,0 +1,7 @@
:cachedir: /var/cache/r10k
:sources:
:puppet:
remote: <%= @pt_r10k_remote %>
prefix: <&= @pt_r10k_prefix %>
basedir: '<%= @pt_r10k_basedir %>'

View File

@@ -0,0 +1,5 @@
pytest==7.4.3
pytest-cov==4.1.0
httpx==0.25.2
flake8==6.1.0
pylint==3.0.1

View File

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