-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
release: Add resources and webhook for running in OpenShift
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
1 parent
ca57680
commit 5732e22
Showing
5 changed files
with
252 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') |