Merge pull request #1490 from coreos-inc/aci-reproduce
Make ACI generation consistent across calls
This commit is contained in:
commit
47afbb65dc
5 changed files with 60 additions and 16 deletions
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 = [
|
||||||
{
|
{
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Reference in a new issue