Fix pulling of squashed versions of the legacy image in a manifest lists
This commit is contained in:
		
							parent
							
								
									001768c043
								
							
						
					
					
						commit
						1f03fdb27e
					
				
					 10 changed files with 198 additions and 31 deletions
				
			
		|  | @ -197,6 +197,13 @@ class RegistryDataInterface(object): | |||
|         Returns None if the manifest could not be parsed and validated. | ||||
|     """ | ||||
| 
 | ||||
|   @abstractmethod | ||||
|   def list_parsed_manifest_layers(self, repository_ref, parsed_manifest, include_placements=False): | ||||
|     """ Returns an *ordered list* of the layers found in the parsed manifest, starting at the base | ||||
|         and working towards the leaf, including the associated Blob and its placements | ||||
|         (if specified). | ||||
|     """ | ||||
| 
 | ||||
|   @abstractmethod | ||||
|   def lookup_derived_image(self, manifest, verb, varying_metadata=None, include_placements=False): | ||||
|     """ | ||||
|  | @ -205,8 +212,8 @@ class RegistryDataInterface(object): | |||
|     """ | ||||
| 
 | ||||
|   @abstractmethod | ||||
|   def lookup_or_create_derived_image(self, manifest, verb, storage_location, varying_metadata=None, | ||||
|                                      include_placements=False): | ||||
|   def lookup_or_create_derived_image(self, manifest, verb, storage_location, storage, | ||||
|                                      varying_metadata=None, include_placements=False): | ||||
|     """ | ||||
|     Looks up the derived image for the given maniest, verb and optional varying metadata | ||||
|     and returns it. If none exists, a new derived image is created. | ||||
|  |  | |||
|  | @ -54,6 +54,30 @@ class OCIModel(SharedModel, RegistryDataInterface): | |||
| 
 | ||||
|     return tags_map | ||||
| 
 | ||||
|   def _get_legacy_compatible_image_for_manifest(self, manifest, storage): | ||||
|     # Check for a legacy image directly on the manifest. | ||||
|     if manifest.media_type != DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE: | ||||
|       return oci.shared.get_legacy_image_for_manifest(manifest._db_id) | ||||
|      | ||||
|     # Otherwise, lookup a legacy image associated with the v1-compatible manifest | ||||
|     # in the list. | ||||
|     try: | ||||
|       manifest_obj = database.Manifest.get(id=manifest._db_id) | ||||
|     except database.Manifest.DoesNotExist: | ||||
|       logger.exception('Could not find manifest for manifest `%s`', manifest._db_id) | ||||
|       return None | ||||
| 
 | ||||
|     # See if we can lookup a schema1 legacy image. | ||||
|     v1_compatible = self.get_schema1_parsed_manifest(manifest, '', '', '', storage) | ||||
|     if v1_compatible is None: | ||||
|       return None | ||||
| 
 | ||||
|     v1_id = v1_compatible.leaf_layer_v1_image_id | ||||
|     if v1_id is None: | ||||
|       return None | ||||
| 
 | ||||
|     return model.image.get_image(manifest_obj.repository_id, v1_id) | ||||
| 
 | ||||
|   def find_matching_tag(self, repository_ref, tag_names): | ||||
|     """ Finds an alive tag in the repository matching one of the given tag names and returns it | ||||
|         or None if none. | ||||
|  | @ -400,7 +424,13 @@ class OCIModel(SharedModel, RegistryDataInterface): | |||
|       logger.exception('Could not find manifest for manifest `%s`', manifest._db_id) | ||||
|       return None | ||||
| 
 | ||||
|     return self._list_manifest_layers(manifest, manifest_obj.repository_id, include_placements) | ||||
|     try: | ||||
|       parsed = manifest.get_parsed_manifest() | ||||
|     except ManifestException: | ||||
|       logger.exception('Could not parse and validate manifest `%s`', manifest._db_id) | ||||
|       return None | ||||
| 
 | ||||
|     return self._list_manifest_layers(manifest_obj.repository_id, parsed, include_placements) | ||||
| 
 | ||||
|   def lookup_derived_image(self, manifest, verb, varying_metadata=None, include_placements=False): | ||||
|     """ | ||||
|  | @ -414,13 +444,14 @@ class OCIModel(SharedModel, RegistryDataInterface): | |||
|     derived = model.image.find_derived_storage_for_image(legacy_image, verb, varying_metadata) | ||||
|     return self._build_derived(derived, verb, varying_metadata, include_placements) | ||||
| 
 | ||||
|   def lookup_or_create_derived_image(self, manifest, verb, storage_location, varying_metadata=None, | ||||
|   def lookup_or_create_derived_image(self, manifest, verb, storage_location, storage, | ||||
|                                     varying_metadata=None, | ||||
|                                      include_placements=False): | ||||
|     """ | ||||
|     Looks up the derived image for the given maniest, verb and optional varying metadata | ||||
|     and returns it. If none exists, a new derived image is created. | ||||
|     """ | ||||
|     legacy_image = oci.shared.get_legacy_image_for_manifest(manifest._db_id) | ||||
|     legacy_image = self._get_legacy_compatible_image_for_manifest(manifest, storage) | ||||
|     if legacy_image is None: | ||||
|       return None | ||||
| 
 | ||||
|  |  | |||
|  | @ -11,7 +11,8 @@ from data import model | |||
| from data.database import db_transaction | ||||
| from data.registry_model.interface import RegistryDataInterface | ||||
| from data.registry_model.datatypes import (Tag, Manifest, LegacyImage, Label, | ||||
|                                            SecurityScanStatus, ManifestLayer, Blob, DerivedImage) | ||||
|                                            SecurityScanStatus, ManifestLayer, Blob, DerivedImage, | ||||
|                                            RepositoryReference) | ||||
| from data.registry_model.shared import SharedModel | ||||
| from data.registry_model.label_handlers import apply_label_to_manifest | ||||
| from image.docker.schema1 import (DockerSchema1ManifestBuilder, ManifestException, | ||||
|  | @ -489,7 +490,14 @@ class PreOCIModel(SharedModel, RegistryDataInterface): | |||
|       logger.exception('Could not find tag manifest for manifest `%s`', manifest._db_id) | ||||
|       return None | ||||
| 
 | ||||
|     return self._list_manifest_layers(manifest, tag_manifest.tag.repository_id, include_placements) | ||||
|     try: | ||||
|       parsed = manifest.get_parsed_manifest() | ||||
|     except ManifestException: | ||||
|       logger.exception('Could not parse and validate manifest `%s`', manifest._db_id) | ||||
|       return None | ||||
| 
 | ||||
|     repo_ref = RepositoryReference.for_id(tag_manifest.tag.repository_id) | ||||
|     return self.list_parsed_manifest_layers(repo_ref, parsed, include_placements) | ||||
| 
 | ||||
|   def lookup_derived_image(self, manifest, verb, varying_metadata=None, include_placements=False): | ||||
|     """ | ||||
|  | @ -506,8 +514,8 @@ class PreOCIModel(SharedModel, RegistryDataInterface): | |||
|     derived = model.image.find_derived_storage_for_image(repo_image, verb, varying_metadata) | ||||
|     return self._build_derived(derived, verb, varying_metadata, include_placements) | ||||
| 
 | ||||
|   def lookup_or_create_derived_image(self, manifest, verb, storage_location, varying_metadata=None, | ||||
|                                      include_placements=False): | ||||
|   def lookup_or_create_derived_image(self, manifest, verb, storage_location, storage, | ||||
|                                      varying_metadata=None, include_placements=False): | ||||
|     """ | ||||
|     Looks up the derived image for the given maniest, verb and optional varying metadata | ||||
|     and returns it. If none exists, a new derived image is created. | ||||
|  |  | |||
|  | @ -306,17 +306,18 @@ class SharedModel: | |||
| 
 | ||||
|     return LegacyImage.for_image(image, images_map=parent_images_map, blob=blob) | ||||
| 
 | ||||
|   def _list_manifest_layers(self, manifest, repo_id, include_placements=False): | ||||
|   def list_parsed_manifest_layers(self, repository_ref, parsed_manifest, include_placements=False): | ||||
|     """ Returns an *ordered list* of the layers found in the parsed manifest, starting at the base | ||||
|         and working towards the leaf, including the associated Blob and its placements | ||||
|         (if specified). | ||||
|     """ | ||||
|     return self._list_manifest_layers(repository_ref._db_id, parsed_manifest, include_placements) | ||||
| 
 | ||||
|   def _list_manifest_layers(self, repo_id, parsed, include_placements=False): | ||||
|     """ Returns an *ordered list* of the layers found in the manifest, starting at the base and | ||||
|         working towards the leaf, including the associated Blob and its placements (if specified). | ||||
|         Returns None if the manifest could not be parsed and validated. | ||||
|     """ | ||||
|     try: | ||||
|       parsed = manifest.get_parsed_manifest() | ||||
|     except ManifestException: | ||||
|       logger.exception('Could not parse and validate manifest `%s`', manifest._db_id) | ||||
|       return None | ||||
| 
 | ||||
|     storage_map = {} | ||||
|     if parsed.local_blob_digests: | ||||
|       blob_query = model.storage.lookup_repo_storages_by_content_checksum(repo_id, | ||||
|  | @ -331,11 +332,12 @@ class SharedModel: | |||
| 
 | ||||
|       digest_str = str(layer.digest) | ||||
|       if digest_str not in storage_map: | ||||
|         logger.error('Missing digest `%s` for manifest `%s`', layer.digest, manifest._db_id) | ||||
|         logger.error('Missing digest `%s` for manifest `%s`', layer.digest, parsed.digest) | ||||
|         return None | ||||
| 
 | ||||
|       image_storage = storage_map[digest_str] | ||||
|       assert image_storage.cas_path is not None | ||||
|       assert image_storage.image_size is not None | ||||
| 
 | ||||
|       placements = None | ||||
|       if include_placements: | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| import hashlib | ||||
| import os | ||||
| import tarfile | ||||
| 
 | ||||
| from io import BytesIO | ||||
| 
 | ||||
|  | @ -113,3 +114,36 @@ def test_extra_blob_stream_handlers(pre_oci_model): | |||
| 
 | ||||
|   assert ''.join(handler1_result) == 'hello world' | ||||
|   assert ''.join(handler2_result) == 'hello world' | ||||
| 
 | ||||
| 
 | ||||
| def valid_tar_gz(contents): | ||||
|   layer_data = BytesIO() | ||||
|   tar_file = tarfile.open(fileobj=layer_data, mode='w|gz') | ||||
| 
 | ||||
|   tar_file_info = tarfile.TarInfo(name='somefile') | ||||
|   tar_file_info.type = tarfile.REGTYPE | ||||
|   tar_file_info.size = len(contents) | ||||
|   tar_file_info.mtime = 1 | ||||
| 
 | ||||
|   tar_file.addfile(tar_file_info, BytesIO(contents)) | ||||
|   tar_file.close() | ||||
| 
 | ||||
|   layer_bytes = layer_data.getvalue() | ||||
|   layer_data.close() | ||||
|   return layer_bytes | ||||
| 
 | ||||
| 
 | ||||
| def test_uncompressed_size(pre_oci_model): | ||||
|   repository_ref = pre_oci_model.lookup_repository('devtable', 'complex') | ||||
|   storage = DistributedStorage({'local_us': FakeStorage(None)}, ['local_us']) | ||||
|   settings = BlobUploadSettings('1K', 512 * 1024, 3600) | ||||
|   app_config = {'TESTING': True} | ||||
| 
 | ||||
|   with upload_blob(repository_ref, storage, settings) as manager: | ||||
|     manager.upload_chunk(app_config, BytesIO(valid_tar_gz('hello world'))) | ||||
| 
 | ||||
|     blob = manager.commit_to_blob(app_config) | ||||
| 
 | ||||
|   assert blob.compressed_size is not None | ||||
|   assert blob.uncompressed_size is not None | ||||
| 
 | ||||
|  |  | |||
|  | @ -543,8 +543,10 @@ def test_derived_image(registry_model): | |||
|   assert registry_model.lookup_derived_image(manifest, 'squash', {}) is None | ||||
| 
 | ||||
|   # Create a new one. | ||||
|   squashed = registry_model.lookup_or_create_derived_image(manifest, 'squash', 'local_us', {}) | ||||
|   assert registry_model.lookup_or_create_derived_image(manifest, 'squash', 'local_us', {}) == squashed | ||||
|   squashed = registry_model.lookup_or_create_derived_image(manifest, 'squash', | ||||
|                                                            'local_us', storage, {}) | ||||
|   assert registry_model.lookup_or_create_derived_image(manifest, 'squash', | ||||
|                                                        'local_us', storage, {}) == squashed | ||||
|   assert squashed.unique_id | ||||
| 
 | ||||
|   # Check and set the size. | ||||
|  | @ -560,15 +562,15 @@ def test_derived_image(registry_model): | |||
|   assert registry_model.lookup_derived_image(manifest, 'squash', {'foo': 'bar'}) is None | ||||
| 
 | ||||
|   squashed_foo = registry_model.lookup_or_create_derived_image(manifest, 'squash', 'local_us', | ||||
|                                                                {'foo': 'bar'}) | ||||
|                                                                storage, {'foo': 'bar'}) | ||||
|   assert squashed_foo != squashed | ||||
|   assert registry_model.lookup_derived_image(manifest, 'squash', {'foo': 'bar'}) == squashed_foo | ||||
| 
 | ||||
|   assert squashed.unique_id != squashed_foo.unique_id | ||||
| 
 | ||||
|   # Lookup with placements. | ||||
|   squashed = registry_model.lookup_or_create_derived_image(manifest, 'squash', 'local_us', {}, | ||||
|                                                            include_placements=True) | ||||
|   squashed = registry_model.lookup_or_create_derived_image(manifest, 'squash', 'local_us', | ||||
|                                                            storage, {}, include_placements=True) | ||||
|   assert squashed.blob.placements | ||||
| 
 | ||||
|   # Delete the derived image. | ||||
|  |  | |||
		Reference in a new issue