Skip to content

Commit a1f440c

Browse files
author
Matthew Fisher
committed
ref(builder): replace python with bash
Debugging the builder script when it fails is really difficult in python. You have to import a bunch of libraries, pipe stderr on some commands to stdout, inspect the return codes and the error returned... In short, it's quite difficult to debug. Refactoring to bash allows us to debug the script quite easily by setting the -x option for bash scripts and we have instant debugging. This refactor also removes the "remote:" prefix from most of the output, which is another huge plus.
1 parent 35ff77d commit a1f440c

4 files changed

Lines changed: 190 additions & 187 deletions

File tree

builder/Dockerfile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ RUN apt-get update && apt-get install -yq \
2323
aufs-tools iptables lxc \
2424
lxc-docker-1.2.0
2525

26+
# install jq for parsing json
27+
RUN curl http://stedolan.github.io/jq/download/linux64/jq > /usr/bin/jq && chmod 755 /usr/bin/jq
28+
2629
# configure ssh server
2730
RUN rm /etc/ssh/ssh_host_*
2831
RUN dpkg-reconfigure openssh-server
@@ -60,9 +63,9 @@ WORKDIR /app
6063
ENTRYPOINT ["/app/bin/entry"]
6164
CMD ["/app/bin/boot"]
6265
EXPOSE 22
63-
6466
RUN addgroup --quiet --gid 2000 slug && useradd slug --uid=2000 --gid=2000
6567

68+
ADD templates/shim.dockerfile /home/git/
6669
ADD . /app
6770
ADD sshd_config /etc/ssh/sshd_config
6871
RUN chown -R root:root /app

builder/templates/builder

Lines changed: 173 additions & 185 deletions
Original file line numberDiff line numberDiff line change
@@ -1,189 +1,177 @@
1-
#!/usr/bin/env python
1+
#!/usr/bin/env bash
22
#
33
# builder hook called on every git receive-pack
44
# NOTE: this script must be run as root (for docker access)
55
#
6-
import hashlib
7-
import json
8-
import os
9-
import requests
10-
import shutil
11-
import subprocess
12-
import sys
13-
import tarfile
14-
import tempfile
15-
import uuid
16-
import yaml
17-
18-
19-
def parse_args():
20-
if len(sys.argv) < 3:
21-
print('Usage: {} [user] [repo] [branch]'.format(sys.argv[0]))
22-
sys.exit(1)
23-
user, repo, branch = sys.argv[1], sys.argv[2], sys.argv[3]
24-
app = repo.split('.')[0]
25-
return user, repo, branch, app
26-
27-
28-
def recursive_chown(path, uid, guid):
29-
os.chown(os.path.join(path), uid, guid)
30-
for root, dirs, files in os.walk(path):
31-
for d in dirs:
32-
os.chown(os.path.join(root, d), uid, guid)
33-
for f in files:
34-
os.chown(os.path.join(root, f), uid, guid)
35-
36-
37-
DOCKERFILE_SHIM = """FROM deis/slugrunner
38-
ADD slug.tgz /app
39-
"""
40-
41-
42-
if __name__ == '__main__':
43-
user, repo, branch, app = parse_args()
44-
# create required directories
45-
repo_dir = os.path.join(os.getcwd(), repo)
46-
build_dir = os.path.join(repo_dir, 'build')
47-
cache_dir = os.path.join(repo_dir, 'cache')
48-
# get sha of branch
49-
with open(os.path.join(repo_dir, branch)) as f:
50-
sha = f.read().strip('\n')
51-
short_sha = sha[:8]
52-
# define image names
53-
tmp_image = "{app}:git-{short_sha}".format(**locals())
54-
target_image = "{{ .deis_registry_host }}:{{ .deis_registry_port }}/{app}".format(**locals())
55-
image_reference = "{app}".format(**locals())
56-
for d in (cache_dir, build_dir):
57-
if not os.path.exists(d):
58-
os.mkdir(d)
59-
try:
60-
# create temporary directory
61-
temp_dir = tempfile.mkdtemp(dir=build_dir)
62-
# extract git branch
63-
p = subprocess.Popen(
64-
'git archive {branch} | tar -x -C {temp_dir}'.format(**locals()),
65-
shell=True, cwd=repo_dir)
66-
rc = p.wait()
67-
if rc != 0:
68-
raise Exception('Could not extract git archive')
69-
dockerfile = os.path.join(temp_dir, 'Dockerfile')
70-
procfile = os.path.join(temp_dir, 'Procfile')
71-
# pull config to be used during build
72-
body = {}
73-
body['receive_user'], body['receive_repo'] = user, app
74-
url = "{{ .deis_controller_protocol }}://{{ .deis_controller_host }}:{{ .deis_controller_port }}/api/hooks/config"
75-
headers = {'Content-Type': 'application/json', 'X-Deis-Builder-Auth': '{{ .deis_controller_builderKey }}'}
76-
r = requests.post(url, headers=headers, data=json.dumps(body))
77-
if r.status_code != 200:
78-
raise Exception('Config hook error: {} {}'.format(r.status_code, r.text))
79-
config_env = " ".join([ "-e {}='{}'".format(*kv) for kv in json.loads(r.json().get('values', '{}')).items()])
80-
# some applications do not have a Procfile, so only check for a Dockerfile
81-
if not os.path.exists(dockerfile):
82-
# fix permissions
83-
slug_uid = 2000
84-
slug_guid = 2000
85-
for path in (cache_dir, temp_dir):
86-
recursive_chown(path, slug_uid, slug_guid)
87-
if os.path.exists('/buildpacks'):
88-
recursive_chown('/buildpacks', slug_uid, slug_guid)
89-
build_cmd = "docker run -i -a stdin {config_env} -v {temp_dir}:/tmp/app -v {cache_dir}:/tmp/cache:rw -v /buildpacks:/tmp/buildpacks deis/slugbuilder".format(**locals())
90-
else:
91-
build_cmd = "docker run -i -a stdin {config_env} -v {temp_dir}:/tmp/app -v {cache_dir}:/tmp/cache:rw deis/slugbuilder".format(**locals())
92-
# run slugbuilder in the background
93-
p = subprocess.Popen(build_cmd, shell=True, cwd=repo_dir, stdout=subprocess.PIPE)
94-
container = p.stdout.read().strip('\n')
95-
# attach to slugbuilder output
96-
p = subprocess.Popen('docker attach {container}'.format(**locals()), shell=True, cwd=temp_dir)
97-
rc = p.wait()
98-
if rc != 0:
99-
raise Exception('Slugbuilder returned error code')
100-
# extract slug
101-
p = subprocess.Popen('docker cp {container}:/tmp/slug.tgz .'.format(**locals()), shell=True, cwd=temp_dir)
102-
rc = p.wait()
103-
if rc != 0:
104-
raise Exception('Could not extract slug from container')
105-
slug_path = os.path.join(temp_dir, 'slug.tgz')
106-
# write out a dockerfile shim for slugbuilder
107-
with open(dockerfile, 'w') as f:
108-
f.write(DOCKERFILE_SHIM)
109-
# build the docker image
110-
print('-----> Building Docker image')
111-
sys.stdout.flush(), sys.stderr.flush()
112-
p = subprocess.Popen('docker build -t {tmp_image} .'.format(**locals()), shell=True, cwd=temp_dir)
113-
rc = p.wait()
114-
if rc != 0:
115-
raise Exception('Could not build Docker image')
116-
# tag the image
117-
p = subprocess.Popen('docker tag {tmp_image} {target_image}'.format(**locals()), shell=True)
118-
rc = p.wait()
119-
if rc != 0:
120-
raise Exception('Could not tag Docker image')
121-
# push the image, output to /dev/null
122-
print('-----> Pushing image to private registry')
123-
sys.stdout.flush(), sys.stderr.flush()
124-
p = subprocess.Popen('docker push {target_image}'.format(**locals()), shell=True,
125-
stdout=open(os.devnull, 'w'), stderr=open(os.devnull, 'w'))
126-
rc = p.wait()
127-
if rc != 0:
128-
raise Exception('Could not push Docker image')
129-
# construct json body for posting to the build hook
130-
body = {}
131-
body['sha'] = sha
132-
body['receive_user'] = user
133-
body['receive_repo'] = app
134-
body['image'] = image_reference
135-
# use sha of branch
136-
with open(os.path.join(repo_dir, branch)) as f:
137-
body['sha'] = f.read().strip('\n')
138-
# extract the user-defined Procfile and any default_process_types
139-
procfile_dict = {}
140-
p = subprocess.Popen('tar --to-stdout -xzf {temp_dir}/slug.tgz ./.release'.format(**locals()), shell=True, cwd=temp_dir,
141-
stdout=subprocess.PIPE, stderr=open(os.devnull, 'w'))
142-
rc = p.wait()
143-
if rc == 0:
144-
stdout = p.stdout.read()
145-
default_process_types = yaml.safe_load(stdout).get('default_process_types', {})
146-
procfile_dict.update(default_process_types)
147-
if os.path.exists(procfile):
148-
with open(procfile) as f:
149-
raw_procfile = f.read()
150-
procfile_dict.update(yaml.safe_load(raw_procfile))
151-
if procfile_dict:
152-
body['procfile'] = json.dumps(procfile_dict)
153-
# extract Dockerfile
154-
if os.path.exists(dockerfile):
155-
with open(dockerfile) as f:
156-
body['dockerfile'] = f.read()
157-
# trigger build hook
158-
sys.stdout.write('\n Launching... ')
159-
sys.stdout.flush()
160-
url = "{{ .deis_controller_protocol }}://{{ .deis_controller_host }}:{{ .deis_controller_port }}/api/hooks/build"
161-
headers = {'Content-Type': 'application/json', 'X-Deis-Builder-Auth': '{{ .deis_controller_builderKey }}'}
162-
r = requests.post(url, headers=headers, data=json.dumps(body))
163-
if r.status_code != 200:
164-
raise Exception('Build hook error: {} {}'.format(r.status_code, r.text))
165-
# write out results for git user
166-
response = r.json()
167-
sys.stdout.write('done, v{version}\n\n'.format(**response['release']))
168-
print("-----> {app} deployed to Deis".format(**locals()))
169-
domains = response.get('domains', [])
170-
if domains:
171-
for domain in domains:
172-
print(" http://{domain}".format(**locals()))
173-
else:
174-
print(' No domains found for this application')
175-
print('\n To learn more, use `deis help` or visit http://deis.io\n')
176-
p = subprocess.Popen("git gc", shell=True, cwd=repo_dir)
177-
p.wait()
178-
except Exception as e:
179-
print(e.message)
180-
sys.exit(1)
181-
finally:
182-
l = locals()
183-
shutil.rmtree(temp_dir)
184-
if 'container' in l:
185-
subprocess.Popen('docker rm -f {container}'.format(**l), shell=True,
186-
stdout=open(os.devnull, 'w'), stderr=open(os.devnull, 'w'))
187-
if 'tmp_image' in l and 'target_image' in l:
188-
subprocess.Popen('docker rmi -f {tmp_image} {target_image}'.format(**l), shell=True,
189-
stdout=open(os.devnull, 'w'), stderr=open(os.devnull, 'w'))
6+
set -e
7+
8+
ARGS=3
9+
10+
get_app_name() {
11+
echo $1 | awk -F"." '{print $1}'
12+
}
13+
14+
get_git_sha() {
15+
repo=$1
16+
branch=$2
17+
branch_file="${repo}/${branch}"
18+
cat $branch_file
19+
}
20+
21+
indent() {
22+
echo " $@"
23+
}
24+
25+
puts-step() {
26+
echo "-----> $@"
27+
}
28+
29+
puts-warn() {
30+
echo " ! $@"
31+
}
32+
33+
usage() {
34+
echo "Usage: $0 <user> <repo> <branch>"
35+
}
36+
37+
if [ $# -ne $ARGS ]; then
38+
usage
39+
exit 1
40+
fi
41+
42+
USER=$1
43+
REPO=$2
44+
BRANCH=$3
45+
APP_NAME=$(get_app_name $REPO)
46+
47+
cd $(dirname $0) # ensure we are in the root dir
48+
49+
ROOT_DIR=$(pwd)
50+
DOCKERFILE_SHIM="$ROOT_DIR/shim.dockerfile"
51+
REPO_DIR="${ROOT_DIR}/${REPO}"
52+
BUILD_DIR="${REPO_DIR}/build"
53+
CACHE_DIR="${REPO_DIR}/cache"
54+
55+
# get git sha of branch
56+
GIT_SHA=$(get_git_sha $REPO_DIR $BRANCH)
57+
SHORT_SHA=${GIT_SHA:0:8}
58+
59+
# define image names
60+
TMP_IMAGE="${APP_NAME}:git-${SHORT_SHA}"
61+
TARGET_IMAGE="{{ .deis_registry_host }}:{{ .deis_registry_port }}/${APP_NAME}"
62+
IMAGE_REFERENCE=$APP_NAME
63+
64+
# create app directories
65+
mkdir -p $BUILD_DIR $CACHE_DIR
66+
# create temporary directory inside the build dir for this push
67+
TMP_DIR=$(mktemp -d --tmpdir=$BUILD_DIR)
68+
69+
cd $REPO_DIR
70+
# extract git branch
71+
git archive $BRANCH | tar -xC $TMP_DIR
72+
73+
# switch to app context
74+
cd $TMP_DIR
75+
76+
if [ -f Dockerfile ]; then
77+
DOCKERFILE=$(cat Dockerfile)
78+
fi
79+
80+
if [ -f Procfile ]; then
81+
PROCFILE=$(cat Procfile)
82+
fi
83+
84+
# pull config from controller to be used during build
85+
URL="{{ .deis_controller_protocol }}://{{ .deis_controller_host }}:{{ .deis_controller_port }}/api/hooks/config"
86+
RESPONSE=$(curl -s -XPOST \
87+
-H "Content-Type: application/json" \
88+
-H "X-Deis-Builder-Auth: {{ .deis_controller_builderKey }}" \
89+
--data "{\"receive_user\":\"$USER\",\"receive_repo\":\"$APP_NAME\"}" \
90+
$URL)
91+
92+
# massage response for the environment variables in form HELLO=world
93+
CONFIG=$(echo $RESPONSE | python -c 'import json,sys;obj=json.load(sys.stdin);print obj["values"]' | jq -c -M "to_entries|map(\"\(.key)=\(.value|tostring)\")|.[]")
94+
95+
# build option string to send to slugbuilder
96+
BUILD_OPTS=""
97+
for i in $CONFIG; do
98+
BUILD_OPTS+="-e $i "
99+
done
100+
101+
# if no Dockerfile is present, use slugbuilder to compile a heroku slug
102+
# and write out a Dockerfile to use that slug
103+
if [ ! -f Dockerfile ]; then
104+
if [ -f /buildpacks ]; then
105+
BUILD_OPTS+="-v /buildpacks:/tmp/buildpacks:rw "
106+
# give non-root slubuilder user R/W perms for docker volumes
107+
chown -R 2000:2000 /buildpacks
108+
fi
109+
110+
# run in the background, we'll attach to it to retrieve logs
111+
BUILD_OPTS+="-d "
112+
BUILD_OPTS+="-v $TMP_DIR:/tmp/app "
113+
BUILD_OPTS+="-v $CACHE_DIR:/tmp/cache:rw "
114+
# give non-root slubuilder user R/W perms for docker volumes
115+
chown -R 2000:2000 $TMP_DIR $CACHE_DIR
116+
117+
# build the application and attach to the process
118+
JOB=$(docker run $BUILD_OPTS deis/slugbuilder)
119+
docker attach $JOB
120+
121+
# copy out the compiled slug
122+
docker cp $JOB:/tmp/slug.tgz .
123+
# copy over the Dockerfile shim to the build dir
124+
cp $DOCKERFILE_SHIM ./Dockerfile
125+
fi
126+
127+
puts-step "Building Docker image"
128+
docker build -t $TMP_IMAGE . 2>&1
129+
docker tag $TMP_IMAGE $TARGET_IMAGE
130+
131+
puts-step "Pushing image to private registry"
132+
docker push $TARGET_IMAGE &>/dev/null
133+
134+
if [ -f $TMP_DIR/slug.tgz ]; then
135+
RELEASE_INFO=$(tar --to-stdout -xzf $TMP_DIR/slug.tgz ./.release | python -c 'import sys,yaml,json;print json.dumps(yaml.safe_load(sys.stdin).get("default_process_types", {}))')
136+
else
137+
RELEASE_INFO="{}"
138+
fi
139+
140+
if [ -f $TMP_DIR/Procfile ]; then
141+
# update release info with data from the Procfile
142+
RELEASE_INFO=$(echo $RELEASE_INFO | python -c "import sys,json,os,yaml;obj=json.load(sys.stdin);procfile=open('Procfile').read();obj.update(yaml.safe_load(procfile));print json.dumps(obj)")
143+
fi
144+
145+
if [ -f $TMP_DIR/Dockerfile ]; then
146+
DOCKERFILE=$(cat $TMP_DIR/Dockerfile)
147+
fi
148+
149+
# safely escape double quotes
150+
RELEASE_INFO=$(echo $RELEASE_INFO | sed -e 's/\"/\\\"/g')
151+
DOCKERFILE=$(echo $DOCKERFILE | sed -e 's/\"/\\\"/g')
152+
153+
indent "Launching..."
154+
URL="{{ .deis_controller_protocol }}://{{ .deis_controller_host }}:{{ .deis_controller_port }}/api/hooks/build"
155+
DATA="{\"sha\":\"$GIT_SHA\",\"receive_user\":\"$USER\",\"receive_repo\":\"$APP_NAME\",\"image\":\"$IMAGE_REFERENCE\",\"procfile\":\"$RELEASE_INFO\",\"dockerfile\":\"$DOCKERFILE\"}"
156+
157+
# notify the controller that the push was successful
158+
RESPONSE=$(curl -s -XPOST \
159+
-H "Content-Type: application/json" \
160+
-H "X-Deis-Builder-Auth: {{ .deis_controller_builderKey }}" \
161+
--data "$DATA" \
162+
$URL)
163+
164+
RELEASE=$(echo $RESPONSE | python -c 'import json,sys;obj=json.load(sys.stdin);print obj["release"]["version"]')
165+
DOMAIN=$(echo $RESPONSE | python -c 'import json,sys;obj=json.load(sys.stdin);print obj["domains"][0]')
166+
indent "done, v$RELEASE"
167+
echo
168+
puts-step "$APP_NAME deployed to Deis"
169+
indent "http://$DOMAIN"
170+
echo
171+
indent "To learn more, use \`deis help\` or visit http://deis.io"
172+
173+
# cleanup
174+
cd $REPO_DIR
175+
git gc &>/dev/null
176+
docker rm -f $JOB &>/dev/null
177+
docker rmi -f $TMP_IMAGE $TARGET_IMAGE &>/dev/null

0 commit comments

Comments
 (0)