def test_pull_placeholder_manifest_updates_manifest_bytes( self, test_name, proxy_manifest_response): """ it's not possible to connect a sub-manifest to a manifest list on a subsequent pull, since a regular manifest request has no pointer to the manifest list it belongs to. to connect the sub-manifests with the manifest list being at pull time, we create a placeholder manifest. placeholder manifests are caracterized by having an empty manifest_bytes. """ test_params = storage_test_cases[test_name] if test_params["manifest_type"] in [ DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE, OCI_IMAGE_INDEX_CONTENT_TYPE, ]: pytest.skip( "manifest list detected, skipping 'flat' manifest specific test." ) # we only have the manifest list json for the hello-world # (because it's significantly smaller). if test_params["image_name"] == "busybox": pytest.skip( "skipping test for busybox image - we do not have its manifest list json." ) if test_params["manifest_type"] == OCI_IMAGE_MANIFEST_CONTENT_TYPE: pytest.skip( "skipping OCI content type - manifest list specifies docker schema v2." ) if test_params["ref_type"] == "tag": pytest.skip( "skipping manifest fetch by tag - pull for a specific architecture is made by digest", ) parsed = parse_manifest_from_bytes( Bytes.for_string_or_unicode(HELLO_WORLD_MANIFEST_LIST_JSON), DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE, sparse_manifest_support=True, ) # first create the manifest list and its placeholders repo = f"{self.orgname}/{test_params['image_name']}" params = { "repository": repo, "manifest_ref": parsed.digest, } proxy_mock = proxy_manifest_response( parsed.digest, HELLO_WORLD_MANIFEST_LIST_JSON, DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE, ) with patch("data.registry_model.registry_proxy_model.Proxy", MagicMock(return_value=proxy_mock)): headers = _get_auth_headers(self.sub, self.ctx, repo) headers["Accept"] = ", ".join( DOCKER_SCHEMA2_CONTENT_TYPES.union(OCI_CONTENT_TYPES).union( DOCKER_SCHEMA1_CONTENT_TYPES)) conduct_call( self.client, test_params["view_name"], url_for, "GET", params, expected_code=200, headers=headers, ) # now fetch one of the sub manifests from the manifest list test_manifest = parse_manifest_from_bytes( Bytes.for_string_or_unicode(test_params["manifest_json"]), test_params["manifest_type"], sparse_manifest_support=True, ) params = { "repository": repo, "manifest_ref": test_params["manifest_ref"], } proxy_mock = proxy_manifest_response( test_params["manifest_ref"], test_params["manifest_json"], test_params["manifest_type"], ) with patch("data.registry_model.registry_proxy_model.Proxy", MagicMock(return_value=proxy_mock)): headers = _get_auth_headers(self.sub, self.ctx, repo) headers["Accept"] = ", ".join( DOCKER_SCHEMA2_CONTENT_TYPES.union(OCI_CONTENT_TYPES).union( DOCKER_SCHEMA1_CONTENT_TYPES)) resp = conduct_call( self.client, test_params["view_name"], url_for, "GET", params, expected_code=200, headers=headers, ) sub_manifest = Manifest.filter( Manifest.digest == test_params["manifest_ref"]).get() assert sub_manifest.manifest_bytes != "" output_manifest = parse_manifest_from_bytes( Bytes.for_string_or_unicode(sub_manifest.manifest_bytes), sub_manifest.media_type.name, sparse_manifest_support=True, ) input_manifest = parse_manifest_from_bytes( Bytes.for_string_or_unicode(test_params["manifest_json"]), test_params["manifest_type"], sparse_manifest_support=True, ) assert output_manifest.schema_version == input_manifest.schema_version assert output_manifest.media_type == input_manifest.media_type assert output_manifest.is_manifest_list == input_manifest.is_manifest_list assert output_manifest.digest == input_manifest.digest assert output_manifest.manifest_dict == input_manifest.manifest_dict
def test_create_placeholder_blobs_on_first_pull(self, test_name, proxy_manifest_response): test_params = storage_test_cases[test_name] # no blob placeholders are created for manifest lists - we don't have # the sub-manifests at manifest list creation time, so there's no way # to know which blobs the sub-manifest has. if test_params["manifest_type"] in [ DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE, OCI_IMAGE_INDEX_CONTENT_TYPE, ]: pytest.skip( "manifest list detected - skipping blob placeholder test") repo = f"{self.orgname}/{test_params['image_name']}" params = { "repository": repo, "manifest_ref": test_params["manifest_ref"], } proxy_mock = proxy_manifest_response(test_params["manifest_ref"], test_params["manifest_json"], test_params["manifest_type"]) with patch("data.registry_model.registry_proxy_model.Proxy", MagicMock(return_value=proxy_mock)): headers = _get_auth_headers(self.sub, self.ctx, repo) headers["Accept"] = ", ".join( DOCKER_SCHEMA2_CONTENT_TYPES.union(OCI_CONTENT_TYPES).union( DOCKER_SCHEMA1_CONTENT_TYPES)) conduct_call( self.client, test_params["view_name"], url_for, "GET", params, expected_code=200, headers=headers, ) parsed = parse_manifest_from_bytes( Bytes.for_string_or_unicode(test_params["manifest_json"]), test_params["manifest_type"], sparse_manifest_support=True, ) manifest = Manifest.filter(Manifest.digest == parsed.digest).get() mdict = parsed.manifest_dict layers = mdict.get("layers", mdict.get("fsLayers")) mblobs = ManifestBlob.filter(ManifestBlob.manifest == manifest) expected_count = len(layers) # schema 2 manifests have an extra config blob which we need to take into # consideration in the total count config_digest = "" if parsed.schema_version == 2: config_digest = parsed.config.digest expected_count += 1 assert mblobs.count() == expected_count for mblob in mblobs: blob = None layer = None # don't assert if digest belongs to a config blob if mblob.blob.content_checksum == config_digest: continue for layer in layers: digest = layer.get("digest", layer.get("blobSum")) if mblob.blob.content_checksum == digest: blob = mblob.blob layer = layer break assert blob is not None assert blob.image_size == layer.get("size", None) # the absence of an image storage placement for a blob indicates that it's # a placeholder blob, not yet downloaded from the upstream registry. placements = ImageStoragePlacement.filter( ImageStoragePlacement.storage == blob) assert placements.count() == 0
def test_manifest_list_create_manifest_with_sub_manifests_and_connect_them( self, test_name, proxy_manifest_response): test_params = storage_test_cases[test_name] if test_params["manifest_type"] not in [ DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE, OCI_IMAGE_INDEX_CONTENT_TYPE, ]: pytest.skip( "regular manifest detected, skipping manifest list specific test." ) repo = f"{self.orgname}/{test_params['image_name']}" params = { "repository": repo, "manifest_ref": test_params["manifest_ref"], } proxy_mock = proxy_manifest_response( test_params["manifest_ref"], test_params["manifest_json"], test_params["manifest_type"], ) with patch("data.registry_model.registry_proxy_model.Proxy", MagicMock(return_value=proxy_mock)): headers = _get_auth_headers(self.sub, self.ctx, repo) headers["Accept"] = ", ".join( DOCKER_SCHEMA2_CONTENT_TYPES.union(OCI_CONTENT_TYPES).union( DOCKER_SCHEMA1_CONTENT_TYPES)) conduct_call( self.client, test_params["view_name"], url_for, "GET", params, expected_code=200, headers=headers, ) manifest_list = parse_manifest_from_bytes( Bytes.for_string_or_unicode(test_params["manifest_json"]), test_params["manifest_type"], sparse_manifest_support=True, ) try: manifest = Manifest.filter( Manifest.digest == manifest_list.digest).get() except Manifest.DoesNotExist: assert False, "failed to create manifest list" input_digests = [ manifest["digest"] for manifest in manifest_list.manifest_dict["manifests"] ] manifest_links = ManifestChild.select( ManifestChild.child_manifest).where( ManifestChild.manifest == manifest) sub_digests = [ml.child_manifest.digest for ml in manifest_links] assert input_digests == sub_digests for link in manifest_links: mbytes = link.child_manifest.manifest_bytes assert mbytes == "", f"child manifest bytes expected empty, but got {mbytes}"