Skip to content

Commit 1b7e1f3

Browse files
author
Gabriel Monroy
committed
publish every release as a tagged docker image (ex: gabrtv/myapp:v23)
1 parent cf4f174 commit 1b7e1f3

3 files changed

Lines changed: 193 additions & 1 deletion

File tree

api/docker.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import cStringIO
2+
import hashlib
3+
import json
4+
import requests
5+
import tarfile
6+
import urlparse
7+
import uuid
8+
9+
from deis import settings
10+
11+
12+
def publish_release(repository_path, config, tag):
13+
"""
14+
Publish a new release as a Docker image
15+
16+
Given a source repository path, a dictionary of environment variables
17+
and a target tag, create a new lightweight Docker image on the registry.
18+
19+
For example, publish_release('gabrtv/myapp', {'ENVVAR': 'values'}, 'v23')
20+
results in a new Docker image at: <registry_url>/gabrtv/myapp:v23
21+
which contains the new configuration as ENV entries.
22+
"""
23+
image_id = _get_tag(repository_path, 'latest')
24+
image = _get_image(image_id)
25+
# construct the new image
26+
image['parent'] = image['id']
27+
image['id'] = _new_id()
28+
image['config']['Env'] = _construct_env(image['config']['Env'], config)
29+
# update and tag the new image
30+
_put_image(image)
31+
cookies = _put_layer(image['id'], _empty_tar_archive())
32+
_put_checksum(image, cookies)
33+
_put_tag(image['id'], repository_path, tag)
34+
35+
36+
# registry access
37+
38+
39+
def _get_tag(repository, tag):
40+
path = "/v1/repositories/{repository}/tags/{tag}".format(**locals())
41+
url = urlparse.urljoin(settings.REGISTRY_URL, path)
42+
r = requests.get(url)
43+
if not r.status_code == 200:
44+
raise RuntimeError("GET Image Error ({}: {})".format(r.status_code, r.text))
45+
print r.text
46+
return r.json()
47+
48+
49+
def _get_image(image_id):
50+
path = "/v1/images/{image_id}/json".format(**locals())
51+
url = urlparse.urljoin(settings.REGISTRY_URL, path)
52+
r = requests.get(url)
53+
if not r.status_code == 200:
54+
raise RuntimeError("GET Image Error ({}: {})".format(r.status_code, r.text))
55+
return r.json()
56+
57+
58+
def _put_image(image):
59+
path = "/v1/images/{id}/json".format(**image)
60+
url = urlparse.urljoin(settings.REGISTRY_URL, path)
61+
r = requests.put(url, data=json.dumps(image))
62+
if not r.status_code == 200:
63+
raise RuntimeError("PUT Image Error ({}: {})".format(r.status_code, r.text))
64+
return r.json()
65+
66+
67+
def _put_layer(image_id, layer_fileobj):
68+
path = "/v1/images/{image_id}/layer".format(**locals())
69+
url = urlparse.urljoin(settings.REGISTRY_URL, path)
70+
r = requests.put(url, data=layer_fileobj.read())
71+
if not r.status_code == 200:
72+
raise RuntimeError("PUT Layer Error ({}: {})".format(r.status_code, r.text))
73+
return r.cookies
74+
75+
76+
def _put_checksum(image, cookies):
77+
path = "/v1/images/{id}/checksum".format(**image)
78+
url = urlparse.urljoin(settings.REGISTRY_URL, path)
79+
tarsum = TarSum(json.dumps(image)).compute()
80+
headers = {'X-Docker-Checksum': tarsum}
81+
r = requests.put(url, headers=headers, cookies=cookies)
82+
if not r.status_code == 200:
83+
raise RuntimeError("PUT Checksum Error ({}: {})".format(r.status_code, r.text))
84+
print r.json()
85+
86+
87+
def _put_tag(image_id, repository_path, tag):
88+
path = "/v1/repositories/{repository_path}/tags/{tag}".format(**locals())
89+
url = urlparse.urljoin(settings.REGISTRY_URL, path)
90+
r = requests.put(url, data=json.dumps(image_id))
91+
if not r.status_code == 200:
92+
raise RuntimeError("PUT Tag Error ({}: {})".format(r.status_code, r.text))
93+
print r.json()
94+
95+
96+
# utility functions
97+
98+
99+
def _construct_env(env, config):
100+
"Update current environment with latest config"
101+
new_env = []
102+
# see if we need to update existing ENV vars
103+
for e in env:
104+
k, v = e.split('=', 1)
105+
if k in config:
106+
# update values defined by config
107+
v = config.pop(k)
108+
new_env.append("{}={}".format(k, v))
109+
# add other config ENV items
110+
for k, v in config.items():
111+
new_env.append("{}={}".format(k, v))
112+
return new_env
113+
114+
115+
def _new_id():
116+
"Return 64-char UUID for use as Image ID"
117+
return ''.join(uuid.uuid4().hex * 2)
118+
119+
120+
def _empty_tar_archive():
121+
"Return an empty tar archive (in memory)"
122+
data = cStringIO.StringIO()
123+
tar = tarfile.open(mode="w", fileobj=data)
124+
tar.close()
125+
data.seek(0)
126+
return data
127+
128+
129+
#
130+
# Below adapted from https://github.com/dotcloud/docker-registry/blob/master/lib/checksums.py
131+
#
132+
133+
def sha256_file(fp, data=None):
134+
h = hashlib.sha256(data or '')
135+
if not fp:
136+
return h.hexdigest()
137+
while True:
138+
buf = fp.read(4096)
139+
if not buf:
140+
break
141+
h.update(buf)
142+
return h.hexdigest()
143+
144+
145+
def sha256_string(s):
146+
return hashlib.sha256(s).hexdigest()
147+
148+
149+
class TarSum(object):
150+
151+
def __init__(self, json_data):
152+
self.json_data = json_data
153+
self.hashes = []
154+
self.header_fields = ('name', 'mode', 'uid', 'gid', 'size', 'mtime',
155+
'type', 'linkname', 'uname', 'gname', 'devmajor',
156+
'devminor')
157+
158+
def append(self, member, tarobj):
159+
header = ''
160+
for field in self.header_fields:
161+
value = getattr(member, field)
162+
if field == 'type':
163+
field = 'typeflag'
164+
elif field == 'name':
165+
if member.isdir() and not value.endswith('/'):
166+
value += '/'
167+
header += '{0}{1}'.format(field, value)
168+
h = None
169+
try:
170+
if member.size > 0:
171+
f = tarobj.extractfile(member)
172+
h = sha256_file(f, header)
173+
else:
174+
h = sha256_string(header)
175+
except KeyError:
176+
h = sha256_string(header)
177+
self.hashes.append(h)
178+
179+
def compute(self):
180+
self.hashes.sort()
181+
data = self.json_data + ''.join(self.hashes)
182+
tarsum = 'tarsum+sha256:{0}'.format(sha256_string(data))
183+
return tarsum

api/models.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from json_field.fields import JSONField # @UnusedImport
2525

2626
from api import fields, tasks
27+
from docker import publish_release
2728
from provider import import_provider_module
2829
from utils import dict_diff, fingerprint
2930

@@ -499,7 +500,7 @@ def calculate(self):
499500
release = releases[0]
500501
d['release']['version'] = release.version
501502
d['release']['config'] = release.config.values
502-
d['release']['build'] = {'image': release.build.image}
503+
d['release']['build'] = {'image': release.build.image + ":v{}".format(release.version)}
503504
if release.build.url:
504505
d['release']['build']['url'] = release.build.url
505506
d['release']['build']['procfile'] = release.build.procfile
@@ -858,6 +859,11 @@ def new_release(sender, **kwargs):
858859
release = Release.objects.create(
859860
owner=user, app=app, config=config,
860861
build=build, version=new_version)
862+
# publish release to registry as new docker image
863+
if settings.REGISTRY_URL:
864+
repository_path = "{}/{}".format(user.username, app.id)
865+
tag = 'v{}'.format(new_version)
866+
publish_release(repository_path, config.values, tag)
861867
return release
862868

863869

deis/settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,9 @@
288288
SECRET_KEY = os.environ.get('DEIS_SECRET_KEY', 'CHANGEME_sapm$s%upvsw5l_zuy_&29rkywd^78ff(qi')
289289
BUILDER_KEY = os.environ.get('DEIS_BUILDER_KEY', 'CHANGEME_sapm$s%upvsw5l_zuy_&29rkywd^78ff(qi')
290290

291+
# registry settings
292+
REGISTRY_URL = os.environ.get('DEIS_REGISTRY_URL', None)
293+
291294
# the config management module to use in api.models
292295
CM_MODULE = os.environ.get('DEIS_CM_MODULE', 'cm.mock')
293296

0 commit comments

Comments
 (0)