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