github.com/blystad/deis@v0.11.0/controller/registry/private.py (about) 1 import cStringIO 2 import hashlib 3 import json 4 import requests 5 import tarfile 6 import urlparse 7 import uuid 8 9 from docker.utils import utils 10 11 from django.conf import settings 12 13 14 def publish_release(source, config, target): 15 """ 16 Publish a new release as a Docker image 17 18 Given a source image and dictionary of last-mile configuration, 19 create a target Docker image on the registry. 20 21 For example publish_release('registry.local:5000/gabrtv/myapp:v22', 22 {'ENVVAR': 'values'}, 23 'registry.local:5000/gabrtv/myapp:v23',) 24 results in a new Docker image at 'registry.local:5000/gabrtv/myapp:v23' which 25 contains the new configuration as ENV entries. 26 """ 27 try: 28 repo, tag = utils.parse_repository_tag(source) 29 src_image = repo 30 src_tag = tag if tag is not None else 'latest' 31 32 nameparts = repo.rsplit('/', 1) 33 if len(nameparts) == 2: 34 if '/' in nameparts[0]: 35 # strip the hostname and just use the app name 36 src_image = '{}/{}'.format(nameparts[0].rsplit('/', 1)[1], 37 nameparts[1]) 38 elif '.' in nameparts[0]: 39 # we got a name like registry.local:5000/registry 40 src_image = nameparts[1] 41 42 target_image = target.rsplit(':', 1)[0] 43 target_tag = target.rsplit(':', 1)[1] 44 image_id = _get_tag(src_image, src_tag) 45 except RuntimeError: 46 if src_tag == 'latest': 47 # no image exists yet, so let's build one! 48 _put_first_image(src_image) 49 image_id = _get_tag(src_image, src_tag) 50 else: 51 raise 52 image = _get_image(image_id) 53 # construct the new image 54 image['parent'] = image['id'] 55 image['id'] = _new_id() 56 image['config']['Env'] = _construct_env(image['config']['Env'], config) 57 # update and tag the new image 58 _commit(target_image, image, _empty_tar_archive(), target_tag) 59 60 61 # registry access 62 63 64 def _commit(repository_path, image, layer, tag): 65 _put_image(image) 66 cookies = _put_layer(image['id'], layer) 67 _put_checksum(image, cookies) 68 _put_tag(image['id'], repository_path, tag) 69 # point latest to the new tag 70 _put_tag(image['id'], repository_path, 'latest') 71 72 73 def _put_first_image(repository_path): 74 image = { 75 'id': _new_id(), 76 'parent': '', 77 'config': { 78 'Env': [] 79 } 80 } 81 # tag as v0 in the registry 82 _commit(repository_path, image, _empty_tar_archive(), 'v0') 83 84 85 def _api_call(endpoint, data=None, headers={}, cookies=None, request_type='GET'): 86 # FIXME: update API calls for docker 0.10.0+ 87 base_headers = {'user-agent': 'docker/0.9.0'} 88 r = None 89 if len(headers) > 0: 90 for header, value in headers.iteritems(): 91 base_headers[header] = value 92 if request_type == 'GET': 93 r = requests.get(endpoint, headers=base_headers) 94 elif request_type == 'PUT': 95 r = requests.put(endpoint, data=data, headers=base_headers, cookies=cookies) 96 else: 97 raise AttributeError("request type not supported: {}".format(request_type)) 98 return r 99 100 101 def _get_tag(repository, tag): 102 path = "/v1/repositories/{repository}/tags/{tag}".format(**locals()) 103 url = urlparse.urljoin(settings.REGISTRY_URL, path) 104 r = _api_call(url) 105 if not r.status_code == 200: 106 raise RuntimeError("GET Image Error ({}: {})".format(r.status_code, r.text)) 107 return r.json() 108 109 110 def _get_image(image_id): 111 path = "/v1/images/{image_id}/json".format(**locals()) 112 url = urlparse.urljoin(settings.REGISTRY_URL, path) 113 r = _api_call(url) 114 if not r.status_code == 200: 115 raise RuntimeError("GET Image Error ({}: {})".format(r.status_code, r.text)) 116 return r.json() 117 118 119 def _put_image(image): 120 path = "/v1/images/{id}/json".format(**image) 121 url = urlparse.urljoin(settings.REGISTRY_URL, path) 122 r = _api_call(url, data=json.dumps(image), request_type='PUT') 123 if not r.status_code == 200: 124 raise RuntimeError("PUT Image Error ({}: {})".format(r.status_code, r.text)) 125 return r.json() 126 127 128 def _put_layer(image_id, layer_fileobj): 129 path = "/v1/images/{image_id}/layer".format(**locals()) 130 url = urlparse.urljoin(settings.REGISTRY_URL, path) 131 r = _api_call(url, data=layer_fileobj.read(), request_type='PUT') 132 if not r.status_code == 200: 133 raise RuntimeError("PUT Layer Error ({}: {})".format(r.status_code, r.text)) 134 return r.cookies 135 136 137 def _put_checksum(image, cookies): 138 path = "/v1/images/{id}/checksum".format(**image) 139 url = urlparse.urljoin(settings.REGISTRY_URL, path) 140 tarsum = TarSum(json.dumps(image)).compute() 141 headers = {'X-Docker-Checksum': tarsum} 142 r = _api_call(url, headers=headers, cookies=cookies, request_type='PUT') 143 if not r.status_code == 200: 144 raise RuntimeError("PUT Checksum Error ({}: {})".format(r.status_code, r.text)) 145 print r.json() 146 147 148 def _put_tag(image_id, repository_path, tag): 149 path = "/v1/repositories/{repository_path}/tags/{tag}".format(**locals()) 150 url = urlparse.urljoin(settings.REGISTRY_URL, path) 151 r = _api_call(url, data=json.dumps(image_id), request_type='PUT') 152 if not r.status_code == 200: 153 raise RuntimeError("PUT Tag Error ({}: {})".format(r.status_code, r.text)) 154 print r.json() 155 156 157 # utility functions 158 159 160 def _construct_env(env, config): 161 "Update current environment with latest config" 162 new_env = [] 163 # see if we need to update existing ENV vars 164 for e in env: 165 k, v = e.split('=', 1) 166 if k in config: 167 # update values defined by config 168 v = config.pop(k) 169 new_env.append("{}={}".format(k, v)) 170 # add other config ENV items 171 for k, v in config.items(): 172 new_env.append("{}={}".format(k, v)) 173 return new_env 174 175 176 def _new_id(): 177 "Return 64-char UUID for use as Image ID" 178 return ''.join(uuid.uuid4().hex * 2) 179 180 181 def _empty_tar_archive(): 182 "Return an empty tar archive (in memory)" 183 data = cStringIO.StringIO() 184 tar = tarfile.open(mode="w", fileobj=data) 185 tar.close() 186 data.seek(0) 187 return data 188 189 190 # 191 # Below adapted from https://github.com/dotcloud/docker-registry/blob/master/lib/checksums.py 192 # 193 194 def sha256_file(fp, data=None): 195 h = hashlib.sha256(data or '') 196 if not fp: 197 return h.hexdigest() 198 while True: 199 buf = fp.read(4096) 200 if not buf: 201 break 202 h.update(buf) 203 return h.hexdigest() 204 205 206 def sha256_string(s): 207 return hashlib.sha256(s).hexdigest() 208 209 210 class TarSum(object): 211 212 def __init__(self, json_data): 213 self.json_data = json_data 214 self.hashes = [] 215 self.header_fields = ('name', 'mode', 'uid', 'gid', 'size', 'mtime', 216 'type', 'linkname', 'uname', 'gname', 'devmajor', 217 'devminor') 218 219 def append(self, member, tarobj): 220 header = '' 221 for field in self.header_fields: 222 value = getattr(member, field) 223 if field == 'type': 224 field = 'typeflag' 225 elif field == 'name': 226 if member.isdir() and not value.endswith('/'): 227 value += '/' 228 header += '{0}{1}'.format(field, value) 229 h = None 230 try: 231 if member.size > 0: 232 f = tarobj.extractfile(member) 233 h = sha256_file(f, header) 234 else: 235 h = sha256_string(header) 236 except KeyError: 237 h = sha256_string(header) 238 self.hashes.append(h) 239 240 def compute(self): 241 self.hashes.sort() 242 data = self.json_data + ''.join(self.hashes) 243 tarsum = 'tarsum+sha256:{0}'.format(sha256_string(data)) 244 return tarsum