Add manifest creation to new registry data model interface

This commit is contained in:
Joseph Schorr 2018-09-20 17:49:00 -04:00
parent 818ed32f87
commit 0ae062be62
9 changed files with 195 additions and 5 deletions

View file

@ -1,6 +1,7 @@
import logging import logging
from calendar import timegm from calendar import timegm
from datetime import datetime
from uuid import uuid4 from uuid import uuid4
from peewee import IntegrityError, JOIN, fn from peewee import IntegrityError, JOIN, fn
@ -757,6 +758,16 @@ def change_repository_tag_expiration(namespace_name, repo_name, tag_name, expira
return (None, False) return (None, False)
def set_tag_expiration_for_manifest(tag_manifest, expiration_sec):
"""
Changes the expiration of the tag that point to the given manifest to be its lifetime start +
the expiration seconds.
"""
expiration_time_in_seconds = tag_manifest.tag.lifetime_start_ts + expiration_sec
expiration_date = datetime.utcfromtimestamp(expiration_time_in_seconds)
return change_tag_expiration(tag_manifest.tag, expiration_date)
def change_tag_expiration(tag, expiration_date): def change_tag_expiration(tag, expiration_date):
""" Changes the expiration of the given tag to the given expiration datetime. If """ Changes the expiration of the given tag to the given expiration datetime. If
the expiration datetime is None, then the tag is marked as not expiring. the expiration datetime is None, then the tag is marked as not expiring.

View file

@ -35,6 +35,19 @@ class RegistryDataInterface(object):
""" Looks up the manifest with the given digest under the given repository and returns it """ Looks up the manifest with the given digest under the given repository and returns it
or None if none. """ or None if none. """
@abstractmethod
def create_manifest_and_retarget_tag(self, repository_ref, manifest_interface_instance, tag_name):
""" Creates a manifest in a repository, adding all of the necessary data in the model.
The `manifest_interface_instance` parameter must be an instance of the manifest
interface as returned by the image/docker package.
Note that all blobs referenced by the manifest must exist under the repository or this
method will fail and return None.
Returns a reference to the (created manifest, tag) or (None, None) on error.
"""
@abstractmethod @abstractmethod
def get_legacy_images(self, repository_ref): def get_legacy_images(self, repository_ref):
""" """
@ -255,3 +268,9 @@ class RegistryDataInterface(object):
operations if an existing blob from another repositroy is being pushed. Returns False if operations if an existing blob from another repositroy is being pushed. Returns False if
the mounting fails. the mounting fails.
""" """
@abstractmethod
def set_tags_expiration_for_manifest(self, manifest, expiration_sec):
"""
Sets the expiration on all tags that point to the given manifest to that specified.
"""

View file

@ -0,0 +1,28 @@
import logging
from util.timedeltastring import convert_to_timedelta
logger = logging.getLogger(__name__)
def _expires_after(label_dict, manifest, model):
""" Sets the expiration of a manifest based on the quay.expires-in label. """
try:
timedelta = convert_to_timedelta(label_dict['value'])
except ValueError:
logger.exception('Could not convert %s to timedeltastring', label_dict['value'])
return
total_seconds = timedelta.total_seconds()
logger.debug('Labeling manifest %s with expiration of %s', manifest, total_seconds)
model.set_tags_expiration_for_manifest(manifest, total_seconds)
_LABEL_HANDLES = {
'quay.expires-after': _expires_after,
}
def apply_label_to_manifest(label_dict, manifest, model):
""" Runs the handler defined, if any, for the given label. """
handler = _LABEL_HANDLES.get(label_dict['key'])
if handler is not None:
handler(label_dict, manifest, model)

View file

@ -8,11 +8,15 @@ from peewee import IntegrityError
from data import database from data import database
from data import model from data import model
from data.database import db_transaction
from data.registry_model.interface import RegistryDataInterface from data.registry_model.interface import RegistryDataInterface
from data.registry_model.datatypes import (Tag, RepositoryReference, Manifest, LegacyImage, Label, from data.registry_model.datatypes import (Tag, RepositoryReference, Manifest, LegacyImage, Label,
SecurityScanStatus, ManifestLayer, Blob, DerivedImage, SecurityScanStatus, ManifestLayer, Blob, DerivedImage,
TorrentInfo, BlobUpload) TorrentInfo, BlobUpload)
from image.docker.schema1 import DockerSchema1ManifestBuilder, ManifestException from data.registry_model.label_handlers import apply_label_to_manifest
from image.docker.schema1 import (DockerSchema1ManifestBuilder, ManifestException,
DockerSchema1Manifest)
from util.validation import is_json
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -81,6 +85,75 @@ class PreOCIModel(RegistryDataInterface):
return Manifest.for_tag_manifest(tag_manifest, legacy_image) return Manifest.for_tag_manifest(tag_manifest, legacy_image)
def create_manifest_and_retarget_tag(self, repository_ref, manifest_interface_instance, tag_name):
""" Creates a manifest in a repository, adding all of the necessary data in the model.
The `manifest_interface_instance` parameter must be an instance of the manifest
interface as returned by the image/docker package.
Note that all blobs referenced by the manifest must exist under the repository or this
method will fail and return None.
Returns a reference to the (created manifest, tag) or (None, None) on error.
"""
# NOTE: Only Schema1 is supported by the pre_oci_model.
assert isinstance(manifest_interface_instance, DockerSchema1Manifest)
if not manifest_interface_instance.layers:
return None, None
# Ensure all the blobs in the manifest exist.
digests = manifest_interface_instance.checksums
query = model.storage.lookup_repo_storages_by_content_checksum(repository_ref._db_id, digests)
blob_map = {s.content_checksum: s.id for s in query}
for layer in manifest_interface_instance.layers:
digest_str = str(layer.digest)
if digest_str not in blob_map:
return None, None
# Lookup all the images and their parent images (if any) inside the manifest.
# This will let us know which v1 images we need to synthesize and which ones are invalid.
docker_image_ids = list(manifest_interface_instance.legacy_image_ids)
images_query = model.image.lookup_repository_images(repository_ref._db_id, docker_image_ids)
images_map = {i.docker_image_id: i.storage for i in images_query}
# Rewrite any v1 image IDs that do not match the checksum in the database.
try:
rewritten_images = list(manifest_interface_instance.rewrite_invalid_image_ids(images_map))
for rewritten_image in rewritten_images:
if not rewritten_image.image_id in images_map:
model.image.synthesize_v1_image(
repository_ref._db_id,
blob_map[rewritten_image.content_checksum],
rewritten_image.image_id,
rewritten_image.created,
rewritten_image.comment,
rewritten_image.command,
rewritten_image.compat_json,
rewritten_image.parent_image_id,
)
except ManifestException:
logger.exception("exception when rewriting v1 metadata")
return None, None
# Store the manifest pointing to the tag.
leaf_layer_id = rewritten_images[-1].image_id
tag_manifest, newly_created = model.tag.store_tag_manifest_for_repo(repository_ref._db_id,
tag_name,
manifest_interface_instance,
leaf_layer_id,
blob_map)
manifest = Manifest.for_tag_manifest(tag_manifest)
# Save the labels on the manifest.
if newly_created:
with self.batch_create_manifest_labels(manifest) as add_label:
for key, value in manifest.layers[-1].v1_metadata.labels.iteritems():
media_type = 'application/json' if is_json(value) else 'text/plain'
add_label(key, value, 'manifest', media_type)
return manifest, Tag.for_repository_tag(tag_manifest.tag)
def get_legacy_images(self, repository_ref): def get_legacy_images(self, repository_ref):
""" """
Returns an iterator of all the LegacyImage's defined in the matching repository. Returns an iterator of all the LegacyImage's defined in the matching repository.
@ -135,8 +208,17 @@ class PreOCIModel(RegistryDataInterface):
except database.TagManifest.DoesNotExist: except database.TagManifest.DoesNotExist:
return None return None
label = model.label.create_manifest_label(tag_manifest, key, value, source_type_name, label_data = dict(key=key, value=value, source_type_name=source_type_name,
media_type_name) media_type_name=media_type_name)
with db_transaction():
# Create the label itself.
label = model.label.create_manifest_label(tag_manifest, key, value, source_type_name,
media_type_name)
# Apply any changes to the manifest that the label prescribes.
apply_label_to_manifest(label_data, manifest, self)
return Label.for_label(label) return Label.for_label(label)
@contextmanager @contextmanager
@ -164,7 +246,12 @@ class PreOCIModel(RegistryDataInterface):
# TODO: make this truly batch once we've fully transitioned to V2_2 and no longer need # TODO: make this truly batch once we've fully transitioned to V2_2 and no longer need
# the mapping tables. # the mapping tables.
for label in labels_to_add: for label in labels_to_add:
model.label.create_manifest_label(tag_manifest, **label) with db_transaction():
# Create the label itself.
model.label.create_manifest_label(tag_manifest, **label)
# Apply any changes to the manifest that the label prescribes.
apply_label_to_manifest(label, manifest, self)
def list_manifest_labels(self, manifest, key_prefix=None): def list_manifest_labels(self, manifest, key_prefix=None):
""" Returns all labels found on the manifest. If specified, the key_prefix will filter the """ Returns all labels found on the manifest. If specified, the key_prefix will filter the
@ -708,4 +795,15 @@ class PreOCIModel(RegistryDataInterface):
expiration_sec) expiration_sec)
return bool(storage) return bool(storage)
def set_tags_expiration_for_manifest(self, manifest, expiration_sec):
"""
Sets the expiration on all tags that point to the given manifest to that specified.
"""
try:
tag_manifest = database.TagManifest.get(id=manifest._db_id)
except database.TagManifest.DoesNotExist:
return None
model.tag.set_tag_expiration_for_manifest(tag_manifest, expiration_sec)
pre_oci_model = PreOCIModel() pre_oci_model = PreOCIModel()

View file

@ -167,6 +167,23 @@ def test_manifest_labels(pre_oci_model):
assert created not in pre_oci_model.list_manifest_labels(found_manifest) assert created not in pre_oci_model.list_manifest_labels(found_manifest)
def test_manifest_label_handlers(pre_oci_model):
repo = model.repository.get_repository('devtable', 'simple')
repository_ref = RepositoryReference.for_repo_obj(repo)
found_tag = pre_oci_model.get_repo_tag(repository_ref, 'latest')
found_manifest = pre_oci_model.get_manifest_for_tag(found_tag)
# Ensure the tag has no expiration.
assert found_tag.lifetime_end_ts is None
# Create a new label with an expires-after.
pre_oci_model.create_manifest_label(found_manifest, 'quay.expires-after', '2h', 'api')
# Ensure the tag now has an expiration.
updated_tag = pre_oci_model.get_repo_tag(repository_ref, 'latest')
assert updated_tag.lifetime_end_ts == (updated_tag.lifetime_start_ts + (60 * 60 * 2))
def test_batch_labels(pre_oci_model): def test_batch_labels(pre_oci_model):
repo = model.repository.get_repository('devtable', 'history') repo = model.repository.get_repository('devtable', 'history')
repository_ref = RepositoryReference.for_repo_obj(repo) repository_ref = RepositoryReference.for_repo_obj(repo)
@ -554,7 +571,6 @@ def test_torrent_info(pre_oci_model):
assert torrent_info.pieces == 'foo' assert torrent_info.pieces == 'foo'
<<<<<<< HEAD
def test_blob_uploads(pre_oci_model): def test_blob_uploads(pre_oci_model):
repository_ref = pre_oci_model.lookup_repository('devtable', 'simple') repository_ref = pre_oci_model.lookup_repository('devtable', 'simple')

View file

@ -34,6 +34,12 @@ class ManifestInterface(object):
""" Returns the Docker V1 image ID for the leaf (top) layer, if any, or None if none. """ """ Returns the Docker V1 image ID for the leaf (top) layer, if any, or None if none. """
pass pass
@abstractproperty
def legacy_image_ids(self):
""" Returns the Docker V1 image IDs for the layers of this manifest or None if not applicable.
"""
pass
@abstractproperty @abstractproperty
def blob_digests(self): def blob_digests(self):
""" Returns an iterator over all the blob digests referenced by this manifest, """ Returns an iterator over all the blob digests referenced by this manifest,

View file

@ -254,6 +254,10 @@ class DockerSchema1Manifest(ManifestInterface):
def image_ids(self): def image_ids(self):
return {mdata.v1_metadata.image_id for mdata in self.layers} return {mdata.v1_metadata.image_id for mdata in self.layers}
@property
def legacy_image_ids(self):
return {mdata.v1_metadata.image_id for mdata in self.layers}
@property @property
def parent_image_ids(self): def parent_image_ids(self):
return {mdata.v1_metadata.parent_image_id for mdata in self.layers return {mdata.v1_metadata.parent_image_id for mdata in self.layers

View file

@ -172,6 +172,10 @@ class DockerSchema2Manifest(ManifestInterface):
def leaf_layer_v1_image_id(self): def leaf_layer_v1_image_id(self):
return list(self.layers_with_v1_ids)[-1].v1_id return list(self.layers_with_v1_ids)[-1].v1_id
@property
def legacy_image_ids(self):
return [l.v1_id for l in self.layers_with_v1_ids]
@property @property
def blob_digests(self): def blob_digests(self):
return [str(layer.digest) for layer in self.layers] return [str(layer.digest) for layer in self.layers]

View file

@ -51,6 +51,10 @@ class BrokenManifest(ManifestInterface):
def layers(self): def layers(self):
return [] return []
@property
def legacy_image_ids(self):
return []
@property @property
def leaf_layer_v1_image_id(self): def leaf_layer_v1_image_id(self):
return None return None