2014-09-19 16:22:54 +00:00
|
|
|
from util.gzipwrap import GzipWrap
|
|
|
|
from util.streamlayerformat import StreamLayerMerger
|
|
|
|
from app import app
|
|
|
|
|
|
|
|
import copy
|
|
|
|
import json
|
|
|
|
import tarfile
|
|
|
|
|
2014-09-29 16:54:22 +00:00
|
|
|
class FileEstimationException(Exception):
|
|
|
|
""" Exception raised by build_docker_load_stream if the estimated size of the layer TAR
|
|
|
|
was lower than the actual size. This means the sent TAR header is wrong, and we have
|
|
|
|
to fail.
|
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2014-09-19 16:22:54 +00:00
|
|
|
def build_docker_load_stream(namespace, repository, tag, synthetic_image_id,
|
|
|
|
layer_json, get_image_iterator, get_layer_iterator):
|
|
|
|
""" Builds and streams a synthetic .tar.gz that represents a squashed version
|
|
|
|
of the given layers, in `docker load` V1 format.
|
|
|
|
"""
|
|
|
|
return GzipWrap(_import_format_generator(namespace, repository, tag,
|
|
|
|
synthetic_image_id, layer_json,
|
|
|
|
get_image_iterator, get_layer_iterator))
|
|
|
|
|
|
|
|
|
|
|
|
def _import_format_generator(namespace, repository, tag, synthetic_image_id,
|
|
|
|
layer_json, get_image_iterator, get_layer_iterator):
|
|
|
|
# Docker import V1 Format (.tar):
|
|
|
|
# repositories - JSON file containing a repo -> tag -> image map
|
|
|
|
# {image ID folder}:
|
|
|
|
# json - The layer JSON
|
|
|
|
# layer.tar - The TARed contents of the layer
|
|
|
|
# VERSION - The docker import version: '1.0'
|
|
|
|
layer_merger = StreamLayerMerger(get_layer_iterator)
|
|
|
|
|
|
|
|
# Yield the repositories file:
|
|
|
|
synthetic_layer_info = {}
|
|
|
|
synthetic_layer_info[tag + '.squash'] = synthetic_image_id
|
|
|
|
|
|
|
|
hostname = app.config['SERVER_HOSTNAME']
|
|
|
|
repositories = {}
|
|
|
|
repositories[hostname + '/' + namespace + '/' + repository] = synthetic_layer_info
|
|
|
|
|
|
|
|
yield _tar_file('repositories', json.dumps(repositories))
|
|
|
|
|
|
|
|
# Yield the image ID folder.
|
|
|
|
yield _tar_folder(synthetic_image_id)
|
|
|
|
|
|
|
|
# Yield the JSON layer data.
|
|
|
|
layer_json = _build_layer_json(layer_json, synthetic_image_id)
|
|
|
|
yield _tar_file(synthetic_image_id + '/json', json.dumps(layer_json))
|
|
|
|
|
|
|
|
# Yield the VERSION file.
|
|
|
|
yield _tar_file(synthetic_image_id + '/VERSION', '1.0')
|
|
|
|
|
|
|
|
# Yield the merged layer data's header.
|
|
|
|
estimated_file_size = 0
|
|
|
|
for image in get_image_iterator():
|
2014-09-29 19:33:26 +00:00
|
|
|
estimated_file_size += image.storage.uncompressed_size
|
2014-09-19 16:22:54 +00:00
|
|
|
|
|
|
|
yield _tar_file_header(synthetic_image_id + '/layer.tar', estimated_file_size)
|
|
|
|
|
|
|
|
# Yield the contents of the merged layer.
|
|
|
|
yielded_size = 0
|
|
|
|
for entry in layer_merger.get_generator():
|
|
|
|
yield entry
|
|
|
|
yielded_size += len(entry)
|
|
|
|
|
2014-09-29 16:54:22 +00:00
|
|
|
# If the yielded size is more than the estimated size (which is unlikely but possible), then
|
2014-10-08 17:43:12 +00:00
|
|
|
# raise an exception since the tar header will be wrong.
|
2014-09-29 16:54:22 +00:00
|
|
|
if yielded_size > estimated_file_size:
|
|
|
|
raise FileEstimationException()
|
|
|
|
|
2014-09-19 16:22:54 +00:00
|
|
|
# If the yielded size is less than the estimated size (which is likely), fill the rest with
|
|
|
|
# zeros.
|
|
|
|
if yielded_size < estimated_file_size:
|
|
|
|
yield '\0' * (estimated_file_size - yielded_size)
|
|
|
|
|
|
|
|
# Yield any file padding to 512 bytes that is necessary.
|
|
|
|
yield _tar_file_padding(estimated_file_size)
|
|
|
|
|
|
|
|
# Last two records are empty in TAR spec.
|
|
|
|
yield '\0' * 512
|
|
|
|
yield '\0' * 512
|
|
|
|
|
|
|
|
|
|
|
|
def _build_layer_json(layer_json, synthetic_image_id):
|
|
|
|
updated_json = copy.deepcopy(layer_json)
|
|
|
|
updated_json['id'] = synthetic_image_id
|
|
|
|
|
|
|
|
if 'parent' in updated_json:
|
|
|
|
del updated_json['parent']
|
|
|
|
|
|
|
|
if 'config' in updated_json and 'Image' in updated_json['config']:
|
|
|
|
updated_json['config']['Image'] = synthetic_image_id
|
|
|
|
|
|
|
|
if 'container_config' in updated_json and 'Image' in updated_json['container_config']:
|
|
|
|
updated_json['container_config']['Image'] = synthetic_image_id
|
|
|
|
|
|
|
|
return updated_json
|
|
|
|
|
|
|
|
|
|
|
|
def _tar_file(name, contents):
|
|
|
|
length = len(contents)
|
|
|
|
tar_data = _tar_file_header(name, length)
|
|
|
|
tar_data += contents
|
|
|
|
tar_data += _tar_file_padding(length)
|
|
|
|
return tar_data
|
|
|
|
|
|
|
|
|
|
|
|
def _tar_file_padding(length):
|
|
|
|
if length % 512 != 0:
|
|
|
|
return '\0' * (512 - (length % 512))
|
|
|
|
|
2014-10-08 17:43:12 +00:00
|
|
|
return ''
|
2014-09-19 16:22:54 +00:00
|
|
|
|
|
|
|
def _tar_file_header(name, file_size):
|
|
|
|
info = tarfile.TarInfo(name=name)
|
|
|
|
info.type = tarfile.REGTYPE
|
|
|
|
info.size = file_size
|
|
|
|
return info.tobuf()
|
|
|
|
|
|
|
|
|
|
|
|
def _tar_folder(name):
|
|
|
|
info = tarfile.TarInfo(name=name)
|
|
|
|
info.type = tarfile.DIRTYPE
|
2014-09-29 19:33:26 +00:00
|
|
|
return info.tobuf()
|