Skip to content

Commit

Permalink
release: Add resources and webhook for running in OpenShift
Browse files Browse the repository at this point in the history
Add a script to turn a home directory with all the necessary credentials
into a Kubernetes secrets volume.

Add a GitHub webhook (using only Python 3 included modules) that can
release any project with the Cockpituous release script. The git repo
URL is taken from the requests's GitHub metadata, and the path to the
release script is taken from the URL path.

Document how to deploy this.

Also install the `oc` client into the container. It is not currently
being used, but it makes it easier to experiment with different
architectures, such as creating `Job` objects in a separate webhook
container.

Closes #183
  • Loading branch information
martinpitt committed Jul 12, 2018
1 parent ca57680 commit 5732e22
Show file tree
Hide file tree
Showing 5 changed files with 252 additions and 2 deletions.
4 changes: 3 additions & 1 deletion release/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ gnupg \
hardlink \
krb5-workstation \
nc \
npm tar \
npm \
origin-clients \
psmisc \
python \
python-irclib \
reprepro \
rpm-build \
tar \
which \
yum-utils \
&& \
Expand Down
34 changes: 33 additions & 1 deletion release/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ This is the container for the Cockpit release runner. It normally gets
activated through a HTTP request: <http://host:8090/cockpit>. The "/cockpit"
path specifies the systemd service name to start (<name>-release.service).

## How to deploy
## Deploying on a host

Setup a 'cockpit' user:

Expand Down Expand Up @@ -71,3 +71,35 @@ Start the container manually (without a webhook):

# systemctl start cockpit-release

# Deploying on OpenShift

On some host `$SECRETHOST`, set up all necessary credentials as above, plus an
extra `~/.config/github-webhook-token` with a shared secret.

Then build a Kubernetes secret volume definition on that host, copy that to a
machine that administers your OpenShift cluster, and deploy that secret volume:

ssh $SECRETHOST release/build-secrets | oc create -f -

Then deploy the other objects:

oc create -f release/cockpit-release.yaml

This will create a POD that runs a simple HTTP server that acts as a
[GitHub webhook](https://developer.github.com/webhooks/). Set this up as a
webhook in GitHub, using an URL like

http://release-cockpit.apps.ci.centos.org/bots/major-cockpit-release

using the path to the release script of the corresponding project's git tree
(the git repository URL will be taken from the POST data that GitHub sends).
Use the same secret as in `~/.config/github-webhook-token` above. Make sure to
change the Content Type to `application/json`.

To remove the deployment:

oc delete service cockpit-release
oc delete route release
oc delete pod release
oc delete secrets cockpit-release-secrets

16 changes: 16 additions & 0 deletions release/build-secrets
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/sh
# Run this in a home directory with all credentials; write OpenShift secrets
# volume JSON definition to stdout
# https://docs.openshift.com/container-platform/3.9/dev_guide/secrets.html
set -eu

printf '{ "apiVersion": "v1", "kind": "Secret", "metadata": { "name": "cockpit-release-secrets" }, "data": {\n'

first=yes
cd ~
for f in $(find .ssh .gnupg .config .pki .fedora* .gitconfig -type f); do
[ -n "$first" ] || printf ',\n'
printf '\t"%s": "%s"' "$(echo $f | sed "s!/!--!g")" "$(base64 --wrap=0 $f)"
first=''
done
printf '\n} }\n'
48 changes: 48 additions & 0 deletions release/cockpit-release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
apiVersion: v1
kind: List
items:
- kind: Pod
apiVersion: v1
metadata:
name: release
labels:
infra: cockpit-release
spec:
containers:
- name: release
image: cockpit/release
ports:
- containerPort: 8080
protocol: TCP
command: [ "webhook" ]
volumeMounts:
- name: secrets
mountPath: /run/secrets/release
readOnly: true
volumes:
- name: secrets
secret:
secretName: cockpit-release-secrets

- kind: Service
apiVersion: v1
metadata:
name: cockpit-release
spec:
clusterIP: None
selector:
infra: cockpit-release
ports:
- name: webhook
port: 8080
protocol: TCP

- kind: Route
apiVersion: v1
metadata:
name: release
spec:
to:
kind: Service
name: cockpit-release
152 changes: 152 additions & 0 deletions release/webhook
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
#!/usr/bin/python3

import os
import hmac
import logging
import json
import subprocess
import shutil
import http.server

project = None
release_script = None

HOME_DIR = '/tmp/home'
BUILD_DIR = os.path.join(HOME_DIR, 'build')
# FIXME: make this a request parameter
SINK = 'fedorapeople.org'
SECRETS = '/run/secrets/release'


def setup():
'''Prepare container for running release scripts'''

if os.path.isdir(HOME_DIR):
return
logging.debug('Initializing %s', HOME_DIR)
os.makedirs(HOME_DIR)

# ensure we have a passwd entry for random UIDs
# https://docs.openshift.com/container-platform/3.7/creating_images/guidelines.html
subprocess.check_call('''
if ! whoami &> /dev/null && [ -w /etc/passwd ]; then
echo "randuser:x:$(id -u):0:random uid:%s:/sbin/nologin" >> /etc/passwd
fi''' % HOME_DIR, shell=True)

# install credentials from secrets volume; copy to avoid world-readable files
# (which e. g. ssh complains about), and to make them owned by our random UID.
old_umask = os.umask(0o77)
for f in os.listdir(SECRETS):
if f.startswith('..'):
continue # secrets volume internal files
src = os.path.join(SECRETS, f)
dest = os.path.join(HOME_DIR, f.replace('--', '/'))
os.makedirs(os.path.dirname(dest), exist_ok=True)
shutil.copyfile(src, dest)
os.umask(old_umask)


class GithubHandler(http.server.BaseHTTPRequestHandler):
def check_sig(self, request):
'''Validate github signature of request.
See https://developer.github.com/webhooks/securing/
'''
# load key
keyfile = os.path.join(HOME_DIR, '.config/github-webhook-token')
try:
with open(keyfile, 'rb') as f:
key = f.read().strip()
except IOError as e:
logging.error('Failed to load GitHub key: %s', e)
return False

sig_sha1 = self.headers.get('X-Hub-Signature', '')
payload_sha1 = 'sha1=' + hmac.new(key, request, 'sha1').hexdigest()
if hmac.compare_digest(sig_sha1, payload_sha1):
return True
logging.error('GitHub signature mismatch! received: %s calculated: %s',
sig_sha1, payload_sha1)
return False

def fail(self, reason, code=404):
logging.error(reason)
self.send_response(code)
self.send_header('Content-type', 'text/plain')
self.end_headers()
self.wfile.write(reason.encode())
self.wfile.write(b'\n')

def success(self):
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
self.wfile.write(b'OK\n')

def do_POST(self):
global project, release_script

content_length = int(self.headers.get('Content-Length', 0))
request = self.rfile.read(content_length)

if not self.check_sig(request):
self.send_response(403)
self.end_headers()
return

event = self.headers.get('X-GitHub-Event')

logging.debug('event: %s, path: %s', event, self.path)
logging.debug(request.decode())

request = json.loads(request)

try:
project = request['repository']['clone_url']
except KeyError:
self.fail('Request misses repository clone_url')
return

if event == 'ping':
self.success()
return
elif event != 'create':
self.fail('unsupported event ' + event)
return

if self.path[0] != '/':
self.fail('Invalid path, should start with /: ' + self.path)
return

release_script = self.path[1:]
self.success()


def release(project, script):
logging.info('Releasing project %s, script %s', project, script)
shutil.rmtree(BUILD_DIR, ignore_errors=True)
subprocess.check_call(['git', 'clone', project, BUILD_DIR])
e = os.environ.copy()
e['HOME'] = HOME_DIR
e['RELEASE_SINK'] = SINK
subprocess.check_call(['/usr/local/bin/release-runner', '-r', project, os.path.join(BUILD_DIR, script)],
cwd=BUILD_DIR, env=e)


#
# main
#

logging.basicConfig(level=logging.DEBUG) # INFO

setup()
httpd = http.server.HTTPServer(('', 8080), GithubHandler)

# we can't do the long-running release() within the request, that blocks the client
# run a loop, as kubernetes does not seem to have on-demand pod launching from a service
while True:
httpd.handle_request()
if project and release_script:
release(project, release_script)
else:
logging.error('Did not get project and script')

0 comments on commit 5732e22

Please sign in to comment.