Merge pull request #1490 from coreos-inc/aci-reproduce

Make ACI generation consistent across calls
This commit is contained in:
josephschorr 2016-05-26 19:37:01 -04:00
commit 47afbb65dc
5 changed files with 60 additions and 16 deletions

View file

@ -1,5 +1,6 @@
import json import json
import re import re
import calendar
from uuid import uuid4 from uuid import uuid4
@ -17,16 +18,21 @@ class ACIImage(TarImageFormatter):
def stream_generator(self, namespace, repository, tag, synthetic_image_id, def stream_generator(self, namespace, repository, tag, synthetic_image_id,
layer_json, get_image_iterator, get_layer_iterator, get_image_json): layer_json, get_image_iterator, get_layer_iterator, get_image_json):
image_mtime = 0
created = next(get_image_iterator()).created
if created is not None:
image_mtime = calendar.timegm(created.utctimetuple())
# ACI Format (.tar): # ACI Format (.tar):
# manifest - The JSON manifest # manifest - The JSON manifest
# rootfs - The root file system # rootfs - The root file system
# Yield the manifest. # Yield the manifest.
yield self.tar_file('manifest', self._build_manifest(namespace, repository, tag, layer_json, manifest = self._build_manifest(namespace, repository, tag, layer_json, synthetic_image_id)
synthetic_image_id)) yield self.tar_file('manifest', manifest, mtime=image_mtime)
# Yield the merged layer dtaa. # Yield the merged layer dtaa.
yield self.tar_folder('rootfs') yield self.tar_folder('rootfs', mtime=image_mtime)
layer_merger = StreamLayerMerger(get_layer_iterator, path_prefix='rootfs/') layer_merger = StreamLayerMerger(get_layer_iterator, path_prefix='rootfs/')
for entry in layer_merger.get_generator(): for entry in layer_merger.get_generator():
@ -187,7 +193,6 @@ class ACIImage(TarImageFormatter):
env_vars.append(pieces) env_vars.append(pieces)
manifest = { manifest = {
"acKind": "ImageManifest", "acKind": "ImageManifest",
"acVersion": "0.6.1", "acVersion": "0.6.1",

View file

@ -6,6 +6,7 @@ from formats.tarimageformatter import TarImageFormatter
import copy import copy
import json import json
import math import math
import calendar
class FileEstimationException(Exception): class FileEstimationException(Exception):
""" Exception raised by build_docker_load_stream if the estimated size of the layer TAR """ Exception raised by build_docker_load_stream if the estimated size of the layer TAR
@ -28,6 +29,11 @@ class SquashedDockerImage(TarImageFormatter):
def stream_generator(self, namespace, repository, tag, synthetic_image_id, def stream_generator(self, namespace, repository, tag, synthetic_image_id,
layer_json, get_image_iterator, get_layer_iterator, get_image_json): layer_json, get_image_iterator, get_layer_iterator, get_image_json):
image_mtime = 0
created = next(get_image_iterator()).created
if created is not None:
image_mtime = calendar.timegm(created.utctimetuple())
# Docker import V1 Format (.tar): # Docker import V1 Format (.tar):
# repositories - JSON file containing a repo -> tag -> image map # repositories - JSON file containing a repo -> tag -> image map
@ -45,17 +51,17 @@ class SquashedDockerImage(TarImageFormatter):
repositories = {} repositories = {}
repositories[hostname + '/' + namespace + '/' + repository] = synthetic_layer_info repositories[hostname + '/' + namespace + '/' + repository] = synthetic_layer_info
yield self.tar_file('repositories', json.dumps(repositories)) yield self.tar_file('repositories', json.dumps(repositories), mtime=image_mtime)
# Yield the image ID folder. # Yield the image ID folder.
yield self.tar_folder(synthetic_image_id) yield self.tar_folder(synthetic_image_id, mtime=image_mtime)
# Yield the JSON layer data. # Yield the JSON layer data.
layer_json = SquashedDockerImage._build_layer_json(layer_json, synthetic_image_id) layer_json = SquashedDockerImage._build_layer_json(layer_json, synthetic_image_id)
yield self.tar_file(synthetic_image_id + '/json', json.dumps(layer_json)) yield self.tar_file(synthetic_image_id + '/json', json.dumps(layer_json), mtime=image_mtime)
# Yield the VERSION file. # Yield the VERSION file.
yield self.tar_file(synthetic_image_id + '/VERSION', '1.0') yield self.tar_file(synthetic_image_id + '/VERSION', '1.0', mtime=image_mtime)
# Yield the merged layer data's header. # Yield the merged layer data's header.
estimated_file_size = 0 estimated_file_size = 0
@ -72,7 +78,8 @@ class SquashedDockerImage(TarImageFormatter):
# Make sure the estimated file size is an integer number of bytes. # Make sure the estimated file size is an integer number of bytes.
estimated_file_size = int(math.ceil(estimated_file_size)) estimated_file_size = int(math.ceil(estimated_file_size))
yield self.tar_file_header(synthetic_image_id + '/layer.tar', estimated_file_size) yield self.tar_file_header(synthetic_image_id + '/layer.tar', estimated_file_size,
mtime=image_mtime)
# Yield the contents of the merged layer. # Yield the contents of the merged layer.
yielded_size = 0 yielded_size = 0

View file

@ -18,10 +18,10 @@ class TarImageFormatter(object):
layer_json, get_image_iterator, get_layer_iterator, get_image_json): layer_json, get_image_iterator, get_layer_iterator, get_image_json):
raise NotImplementedError raise NotImplementedError
def tar_file(self, name, contents): def tar_file(self, name, contents, mtime=None):
""" Returns the TAR binary representation for a file with the given name and file contents. """ """ Returns the TAR binary representation for a file with the given name and file contents. """
length = len(contents) length = len(contents)
tar_data = self.tar_file_header(name, length) tar_data = self.tar_file_header(name, length, mtime=mtime)
tar_data += contents tar_data += contents
tar_data += self.tar_file_padding(length) tar_data += self.tar_file_padding(length)
return tar_data return tar_data
@ -33,17 +33,24 @@ class TarImageFormatter(object):
return '' return ''
def tar_file_header(self, name, file_size): def tar_file_header(self, name, file_size, mtime=None):
""" Returns TAR file header data for a file with the given name and size. """ """ Returns TAR file header data for a file with the given name and size. """
info = tarfile.TarInfo(name=name) info = tarfile.TarInfo(name=name)
info.type = tarfile.REGTYPE info.type = tarfile.REGTYPE
info.size = file_size info.size = file_size
if mtime is not None:
info.mtime = mtime
return info.tobuf() return info.tobuf()
def tar_folder(self, name): def tar_folder(self, name, mtime=None):
""" Returns TAR file header data for a folder with the given name. """ """ Returns TAR file header data for a folder with the given name. """
info = tarfile.TarInfo(name=name) info = tarfile.TarInfo(name=name)
info.type = tarfile.DIRTYPE info.type = tarfile.DIRTYPE
if mtime is not None:
info.mtime = mtime
# allow the directory to be readable by non-root users # allow the directory to be readable by non-root users
info.mode = 0755 info.mode = 0755
return info.tobuf() return info.tobuf()

View file

@ -19,7 +19,7 @@ from cryptography.x509 import load_pem_x509_certificate
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from app import app, storage from app import app, storage
from data.database import close_db_filter, configure from data.database import close_db_filter, configure, DerivedStorageForImage
from data import model from data import model
from endpoints.v1 import v1_bp from endpoints.v1 import v1_bp
from endpoints.v2 import v2_bp from endpoints.v2 import v2_bp
@ -83,6 +83,12 @@ def set_feature(feature_name):
return jsonify({'old_value': old_value}) return jsonify({'old_value': old_value})
@testbp.route('/clearderivedcache', methods=['POST'])
def clearderivedcache():
DerivedStorageForImage.delete().execute()
return 'OK'
@testbp.route('/removeuncompressed/<image_id>', methods=['POST']) @testbp.route('/removeuncompressed/<image_id>', methods=['POST'])
def removeuncompressed(image_id): def removeuncompressed(image_id):
image = model.image.get_image_by_id('devtable', 'newrepo', image_id) image = model.image.get_image_by_id('devtable', 'newrepo', image_id)
@ -1449,7 +1455,7 @@ class ACIConversionTests(RegistryTestCaseMixin, V1RegistryPushMixin, LiveServerT
def get_converted_signature(self): def get_converted_signature(self):
# Give time for the signature to be written before continuing. # Give time for the signature to be written before continuing.
time.sleep(1) time.sleep(2)
response = self.conduct('GET', '/c1/aci/localhost:5000/devtable/newrepo/latest/aci.asc/linux/amd64/', auth='sig') response = self.conduct('GET', '/c1/aci/localhost:5000/devtable/newrepo/latest/aci.asc/linux/amd64/', auth='sig')
return response.content return response.content
@ -1485,6 +1491,7 @@ class ACIConversionTests(RegistryTestCaseMixin, V1RegistryPushMixin, LiveServerT
# Pull the squashed version of the tag. # Pull the squashed version of the tag.
tar, converted = self.get_converted_image() tar, converted = self.get_converted_image()
signature = self.get_converted_signature() signature = self.get_converted_signature()
first_hash = hashlib.sha256(converted).hexdigest()
# Verify the manifest. # Verify the manifest.
self.assertEquals(['manifest', 'rootfs', 'rootfs/contents'], tar.getnames()) self.assertEquals(['manifest', 'rootfs', 'rootfs/contents'], tar.getnames())
@ -1523,6 +1530,24 @@ class ACIConversionTests(RegistryTestCaseMixin, V1RegistryPushMixin, LiveServerT
# Verify the signature. # Verify the signature.
self._verify_signature(signature, converted) self._verify_signature(signature, converted)
# Clear the cache and pull again, ensuring that the hash does not change even for a completely
# new generation of the image.
self.conduct('POST', '/__test/clearderivedcache')
_, converted_again = self.get_converted_image()
second_hash = hashlib.sha256(converted_again).hexdigest()
self.assertEquals(second_hash, first_hash)
# Ensure we have a different signature (and therefore the cache was broken).
signature_again = self.get_converted_signature()
self.assertNotEquals(signature_again, signature)
# Ensure *both* signatures work for both images.
self._verify_signature(signature, converted_again)
self._verify_signature(signature_again, converted)
self._verify_signature(signature_again, converted_again)
def test_multilayer_conversion(self): def test_multilayer_conversion(self):
images = [ images = [
{ {

View file

@ -7,7 +7,7 @@ class GzipWrap(object):
def __init__(self, input, filename=None, compresslevel=1): def __init__(self, input, filename=None, compresslevel=1):
self.input = iter(input) self.input = iter(input)
self.buffer = '' self.buffer = ''
self.zipper = GzipFile(filename, mode='wb', fileobj=self, compresslevel=compresslevel) self.zipper = GzipFile(filename, mode='wb', fileobj=self, compresslevel=compresslevel, mtime=0)
self.is_done = False self.is_done = False
def read(self, size=-1): def read(self, size=-1):