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