def to_image_name(context, param, value: str) -> ImageName: """Converts an docker image name to an ImageName.""" if isinstance(value, str): result = ImageName.parse(value) else: result = [ImageName.parse(v) for v in value] return result
def test_get_tag(archive_repositories: ArchiveRepositories, image_name: ImageName): """Test repository tag retrieval.""" tag = archive_repositories.get_tag(image_name) assert tag assert FormattedSHA256(tag) == image_name.digest assert not archive_repositories.get_tag(ImageName("does_not_exist")) assert not archive_repositories.get_tag(ImageName("does_not", tag="exist"))
def test_get_manifest(archive_changeset: ArchiveChangeset, image_name: ImageName): """Test manifest retrieval.""" manifest = archive_changeset.get_manifest(image_name) if image_name.resolve_digest(): assert manifest.get_config_digest() == image_name.resolve_digest() if image_name.tag: assert ArchiveChangeset.get_repository_tag( image_name) in manifest.get_tags()
async def put_image_config(self, image_name: ImageName, image_config: ImageConfig, **kwargs): image_name = image_name.clone() if image_name.resolve_digest(): image_name.digest = None LOGGER.debug( "It is not possible to store an image configuration to a non-deterministic digest!" " Adjusted destination: %s", image_name.resolve_name(), ) image_config_digest = image_config.get_digest() name = f"{image_config_digest.sha256}.json" if not await self._file_exists(name): with open(self.archive, "rb+") as file_out: tar_add_file(file_out, name, image_config.get_bytes())
def test_get_config_digest(registry_v2_manifest: RegistryV2Manifest): """Test image configuration digest retrieval.""" formattedsha256 = FormattedSHA256( "8f1196ff19e7b5c5861de192ae77e8d7a692fcbca2dd3174d324980f72ab49bf") assert registry_v2_manifest.get_config_digest() == formattedsha256 assert (registry_v2_manifest.get_config_digest( ImageName.parse("ignored")) == formattedsha256)
def test___init__(image_data: TypingGetTestData): """Test that image name can be instantiated.""" assert ImageName( digest=image_data["digest"], endpoint=image_data["endpoint"], image=image_data["image"], tag=image_data["tag"], )
async def sign_image( self, signer: Signer, src_image_name: ImageName, dest_image_source: ImageSource, dest_image_name: ImageName, signature_type: SignatureTypes = SignatureTypes.SIGN, **kwargs, ) -> ImageSourceSignImage: LOGGER.debug( "%s: %s ...", "Endorsing" if signature_type == SignatureTypes.ENDORSE else "Signing" if signature_type == SignatureTypes.SIGN else "Resigning", src_image_name.resolve_name(), ) dest_image_name = dest_image_name.clone() if dest_image_name.resolve_digest(): dest_image_name.digest = None LOGGER.warning( "It is not possible to store a signed image to a predetermined digest! Adjusted destination: %s", dest_image_name.resolve_name(), ) # Generate a signed image configuration ... data = await self._sign_image_config(signer, src_image_name, signature_type, **kwargs) LOGGER.debug(" Signature:\n%s", data.signature_value) image_config = data.image_config config_digest = image_config.get_digest() LOGGER.debug(" config digest (signed): %s", config_digest) # Generate a new registry manifest ... manifest = data.verify_image_data.manifest.clone() manifest.set_config_digest(config_digest, len(image_config.get_bytes())) data = ImageSourceSignImage( image_config=data.image_config, manifest_signed=manifest, signature_value=data.signature_value, verify_image_data=data.verify_image_data, ) await dest_image_source.put_image( self, dest_image_name, manifest, image_config, data.verify_image_data.compressed_layer_files, **kwargs, ) dest_image_name.digest = manifest.get_digest() if not self.dry_run: LOGGER.debug("Created new image: %s", dest_image_name.resolve_name()) return data
def known_good_image_local(request, pytest_registry: str) -> TypingKnownGoodImage: """Provides 'known good' metadata for a local image that can be modified.""" request.param["image"] = request.param["image"].format(pytest_registry) manifest_digest = request.param["digests"][ DockerMediaTypes.DISTRIBUTION_MANIFEST_V2] request.param["image_name"] = ImageName.parse( f"{request.param['image']}:{request.param['tag']}@{manifest_digest}") return request.param
def test_get_layers(registry_v2_manifest: RegistryV2Manifest): """Test manifest layer retrieval.""" layers = [ "sha256:6c8c72249e560701aa1da4cd40192274a8c0419ddb8e4a553aa02b5a1acdb863", "sha256:1403b179e2c9df4f57e9ea94e32882739c6b3d75ed756d4e67fcc424288c29cc", ] assert registry_v2_manifest.get_layers() == layers assert registry_v2_manifest.get_layers( ImageName.parse("ignored")) == layers
def known_good_image_remote(request) -> TypingKnownGoodImage: """Provides 'known good' metadata for a remote image that is readonly.""" request.param["image"] = request.param["image"].format( request.param["original_endpoint"]) manifest_digest = request.param["digests"][ DockerMediaTypes.DISTRIBUTION_MANIFEST_V2] request.param["image_name"] = ImageName.parse( f"{request.param['image']}:{request.param['tag']}@{manifest_digest}") return request.param
def test_parse(image_data: TypingGetTestData): """Test initialization via parsed strings.""" image_name = ImageName.parse(image_data["string"]) assert image_name.digest == image_data["digest"] if image_data["digest"]: assert isinstance(image_name.digest, FormattedSHA256) assert image_name.endpoint == image_data["endpoint"] if image_data["endpoint"]: assert ImageName.DEFAULT_ENDPOINT not in image_name.endpoint assert image_name.image == image_data["image"] assert ImageName.DEFAULT_NAMESPACE not in image_name.image assert image_name.tag == image_data["tag"] if image_data["tag"]: assert ImageName.DEFAULT_TAG not in image_name.tag with pytest.raises(ValueError) as exception: ImageName.parse("a:b:c:d") assert str(exception.value).startswith("Unable to parse string:")
def test_get_config_digest( devicemapper_repository_manifest: DeviceMapperRepositoryManifest, img_name: str, config_digest: str, ): """Test image configuration digest retrieval.""" image_name = ImageName.parse(img_name) assert (devicemapper_repository_manifest.get_config_digest(image_name) == config_digest)
async def _verify_image_config( self, image_name: ImageName, **kwargs ) -> ImageSourceVerifyImageConfig: """ Verifies the integration of an image configuration against metadata contained within a manifest. Args: image_name: The image name for which to retrieve the configuration. Returns: NamedTuple: image_config: The image configuration. image_layers: The listing of image layer identifiers. manifest: The image-source specific manifest. manifest_layers: The listing of manifest layer identifiers. """ # Retrieve the image configuration digest and layers identifiers from the manifest ... LOGGER.debug("Verifying Integrity: %s ...", image_name.resolve_name()) manifest = await self.get_manifest(image_name, **kwargs) LOGGER.debug(" manifest digest: %s", xellipsis(manifest.get_digest())) config_digest = manifest.get_config_digest(image_name) LOGGER.debug(" config digest: %s", xellipsis(config_digest)) manifest_layers = manifest.get_layers(image_name) LOGGER.debug(" manifest layers:") for layer in manifest_layers: LOGGER.debug(" %s", xellipsis(layer)) # Retrieve the image configuration ... image_config = await self.get_image_config(image_name, **kwargs) config_digest_canonical = image_config.get_digest_canonical() LOGGER.debug( " config digest (canonical): %s", xellipsis(config_digest_canonical) ) must_be_equal( config_digest, image_config.get_digest(), "Image config digest mismatch", ) # Retrieve the image layers from the image configuration ... image_layers = image_config.get_image_layers() LOGGER.debug(" image layers:") for layer in image_layers: LOGGER.debug(" %s", xellipsis(layer)) # Quick check: Ensure that the layer counts are consistent must_be_equal(len(manifest_layers), len(image_layers), "Layer count mismatch") return ImageSourceVerifyImageConfig( image_config=image_config, image_layers=image_layers, manifest=manifest, manifest_layers=manifest_layers, )
def test_set_tag(archive_repositories: ArchiveRepositories, image_name: ImageName, name: str): """Test repository tag assignment.""" tag = archive_repositories.get_tag(image_name) assert tag assert FormattedSHA256(tag) == image_name.digest digest = FormattedSHA256.calculate(name.encode("utf-8")) name = ImageName.parse(name) archive_repositories.set_tag(name, digest) assert FormattedSHA256(archive_repositories.get_tag(name)) == digest
def test_override_config( devicemapper_repository_manifest: DeviceMapperRepositoryManifest, formattedsha256: FormattedSHA256, img_name: str, ): """Test overriding manifest values.""" image_name = ImageName.parse(img_name) devicemapper_repository_manifest.override_config(formattedsha256, image_name) assert (devicemapper_repository_manifest.get_config_digest(image_name) == formattedsha256)
def get_tag(self, image_name: ImageName): """ Retrieves a repository tag for a given image. Args: image_name: The image for which to assign the tag Returns: The repository tag, or None. """ image = ArchiveChangeset.normalize_tags([image_name.image])[0] return self.get_json().get(image, {}).get(image_name.resolve_tag(), None)
def _get_repository_key(image_name: ImageName) -> str: """ Retrieves the repository key for a given image name. Args: image_name: Image name for which to retrieve the key. Returns: The corresponding repository key. """ key_name = image_name.clone() key_name.tag = None return str(key_name)
def test_append_config( archive_manifest: ArchiveManifest, sha256_archive_layer: str, formattedsha256: FormattedSHA256, repotag, ): """Test appending configurations.""" layers = [formattedsha256] archive_manifest.append_config(formattedsha256, layers, repotag) image_name = ImageName("ignored", digest=formattedsha256) config = archive_manifest.get_config(image_name) assert config["Config"] == "{0}.json".format(formattedsha256.sha256) assert config["Layers"] == [sha256_archive_layer] assert config.get("RepoTags", None) == repotag
def set_tag(self, image_name: ImageName, digests: FormattedSHA256): """ Assigns a repository tag. Args: image_name: The image for which to assign the tag digests: The value to be assigned to the tag """ _json = self.get_json() image = ArchiveChangeset.normalize_tags([image_name.image])[0] if not image in _json: _json[image] = {} _json[image][image_name.resolve_tag()] = digests.sha256 self._set_json(_json)
def test_parse_string(image_data: TypingGetTestData): """Test string parsing for complex image names.""" result = ImageName._parse_string(image_data["string"]) assert result["digest"] == image_data["digest"] if image_data["digest"]: assert isinstance(result["digest"], FormattedSHA256) assert result["endpoint"] == image_data["endpoint"] if image_data["endpoint"]: assert ImageName.DEFAULT_ENDPOINT not in str(result["endpoint"]) assert result["image"] == image_data["image"] assert ImageName.DEFAULT_NAMESPACE not in str(result["image"]) assert result["tag"] == image_data["tag"] if image_data["tag"]: assert ImageName.DEFAULT_TAG not in str(result["tag"])
async def test_get_manifest_list( docker_registry_secure: DockerRegistrySecure, image: TypingGetTestData, registry_v2_image_source: RegistryV2ImageSource, **kwargs, ): """Test manifest retrieval.""" if "tag_resolves_to_manifest_list" not in image: pytest.skip( f"Image {image['image']} does not reference a manifest list.") image_name = ImageName( image["image"], digest=image["digests"][ DockerMediaTypes.DISTRIBUTION_MANIFEST_LIST_V2], endpoint=docker_registry_secure.endpoint, tag=image["tag"], ) LOGGER.debug("Retrieving manifest list for: %s ...", image_name) manifest = await registry_v2_image_source.get_manifest( image_name, **kwargs) assert isinstance(manifest, RegistryV2Manifest) assert manifest.get_digest() == image_name.resolve_digest()
def test_remove_tags(archive_changeset: ArchiveChangeset): """Test repository tag removal""" image_name = ImageName( "base", digest=FormattedSHA256( "adecf4209bb9dd67d96393774cbd7f8bd2bad3596da42cde33daa0c41b14ac62" ), tag="7.2", ) tag = ArchiveChangeset.get_repository_tag(image_name) manifest = archive_changeset.get_manifest(image_name) assert tag in manifest.get_tags() archive_changeset.remove_tags(["dummy"]) manifest = archive_changeset.get_manifest(image_name) assert tag in manifest.get_tags() archive_changeset.remove_tags([tag]) manifest = archive_changeset.get_manifest(image_name) assert not manifest.get_tags()
async def replicate_manifest_lists( docker_registry_secure: DockerRegistrySecure): """Replicates manifests lists to the secure docker registry for testing.""" # pylint: disable=protected-access LOGGER.debug("Replicating manifest lists into %s ...", docker_registry_secure.service_name) async with DockerRegistryClientAsync() as docker_registry_client_async: for image in get_test_data_registryv2(): if "tag_resolves_to_manifest_list" not in image: continue digest = image["digests"][ DockerMediaTypes.DISTRIBUTION_MANIFEST_LIST_V2] image_name = ImageName(image["image"], digest=digest, tag=image["tag"]) LOGGER.debug("- %s", image_name) scope = DockerAuthentication.SCOPE_REPOSITORY_PULL_PATTERN.format( image_name.image) auth_header_src = await docker_registry_client_async._get_request_headers( image_name, scope=scope) if not auth_header_src: LOGGER.warning( "Unable to retrieve authentication headers for: %s", image_name) pdrf_image_name = PDRFImageName( image_name.resolve_image(), digest=image_name.resolve_digest(), endpoint=image_name.resolve_endpoint(), tag=image_name.resolve_tag(), ) try: replicate_manifest_list( pdrf_image_name, docker_registry_secure.endpoint, auth_header_dest=docker_registry_secure.auth_header, auth_header_src=auth_header_src, ssl_context_dest=docker_registry_secure.ssl_context, ) except Exception as exception: # pylint: disable=broad-except LOGGER.warning( "Unable to replicate manifest list '%s': %s", image_name, exception, exc_info=True, )
def test_get_layers(archive_manifest: ArchiveManifest, image_name: ImageName): """Test manifest layer retrieval.""" if (image_name.resolve_digest( ) == "sha256:a51f3f9281a1a3d89dce25fec8acffbe9f59ddb67d98e04245c4c886e32d3782" ): assert archive_manifest.get_layers(image_name) == [ "sha256:137120c8596a15ab42c39c0c5cf83ef864b6b65b5516887c895915e87292bd07", "sha256:755520f73bc74ae73b12f53229e401e8d4c584b74f5704d2d36ba7c45e2657cf", "sha256:13fb089903a5e0e9b00d78ba48496da528ce8d81e08a1042ebeced8c35d714cb", "sha256:f86d68f70ca006025a7f7013f69898f78d1d9272c4d3909e3ec4c7f9958da20e", "sha256:7b4a4edd704242cec1710679a088be8aabff25c3a79f4eecbe8d11d57c53a20b", "sha256:ef4724d42630f3022ef67c3f6749e85a13e81b8efcf98fbd517476499f10e5ab", ] else: assert archive_manifest.get_layers(image_name) == [ "sha256:2c2e149ae9a88ae6bee1583459b2d3e5e317877b795c08781fab36eab4b4329f", "sha256:83419ef1d0ad0520c9fc44da4345637e5c05e34fa564ddf2fb6d6f94a6b2d205", "sha256:c2d494c64683fb7edac60aaffc570c514c1c80c797aafcf25b8a9438690da4df", "sha256:d3b1a8ce509767258045f6cc050dfc8cff27f66f9fa8c61c9dc46733e492e0af", "sha256:0a0084e273d71f3b39100a5c209a3208fafdb90e3dd038cded5e2a54265d23fa", ]
def get_manifest(self, image_name: ImageName) -> "ArchiveManifest": """ Retrieves the archive manifest for a given image name from the archive changeset. Args: image_name: The image name. Returns: The corresponding archive manifest. """ if image_name.digest: for manifest in self.get_manifests(): if manifest.get_config_digest() == image_name.resolve_digest(): return manifest else: tag = ArchiveChangeset.get_repository_tag(image_name) for manifest in self.get_manifests(): tags = manifest.get_tags() if tags and tag in manifest.get_tags(): return manifest raise RuntimeError( f"Unable to locate configuration in archive manifest for: {image_name.resolve_name()}" )
def get_test_data() -> Generator[TypingGetTestData, None, None]: """Dynamically initializes test data.""" for endpoint in ["endpoint.io", "endpoint:port", None]: for image in [ "image", "ns0/image", "ns0/ns1/image", "ns0/ns1/ns2/image" ]: for tag in ["tag", None]: for digest in [FormattedSHA256.calculate(b""), None]: # Construct a complex string ... string = image if tag: string = f"{string}:{tag}" if digest: string = f"{string}@{digest}" if endpoint: string = f"{endpoint}/{string}" yield { "digest": digest, "endpoint": endpoint, "image": image, "object": ImageName.parse(string), "string": string, "tag": tag, }
def test_get_config_digest(archive_manifest: ArchiveManifest, image_name: ImageName): """Test configuration digest retrieval.""" assert archive_manifest.get_config_digest( image_name) == image_name.resolve_digest()
def image_name() -> ImageName: """Provides a 'known good' image name.""" yield ImageName.parse( "ignored@sha256:a51f3f9281a1a3d89dce25fec8acffbe9f59ddb67d98e04245c4c886e32d3782" )
async def sign_image( self, signer: Signer, src_image_name: ImageName, dest_image_source: ImageSource, dest_image_name: ImageName, signature_type: SignatureTypes = SignatureTypes.SIGN, **kwargs, ): LOGGER.debug( "%s: %s ...", "Endorsing" if signature_type == SignatureTypes.ENDORSE else "Signing" if signature_type == SignatureTypes.SIGN else "Resigning", src_image_name.resolve_name(), ) # Generate a signed image configuration ... data = await self._sign_image_config(signer, src_image_name, signature_type) manifest = data.verify_image_data.manifest LOGGER.debug(" Signature:\n%s", data.signature_value) image_config = data.image_config # Replicate all of the image layers ... LOGGER.debug(" Replicating image layers ...") repository_layers = manifest.get_layers(src_image_name) for i, repository_layer in enumerate(repository_layers): if not dest_image_source.layer_exists(dest_image_name, repository_layer): await dest_image_source.put_image_layer_from_disk( dest_image_name, data.verify_image_data.compressed_layer_files[i], ) # TODO: We we need to track the layer translations here? Is this possible for DM repos? # Push the new image configuration ... config_digest_signed = image_config.get_digest() LOGGER.debug(" config digest (signed): %s", config_digest_signed) await dest_image_source.put_image_config(dest_image_name, image_config) # Generate a new repository manifest, and push ... if type(dest_image_source).__name__ == "ArchiveImageSource": raise NotImplementedError elif type(dest_image_source).__name__ == "RegistryV2ImageSource": raise NotImplementedError elif isinstance(dest_image_source, DeviceMapperRepositoryImageSource): manifest_signed = ( await dest_image_source.get_manifest() ) # type: DeviceMapperRepositoryManifest if dest_image_name.tag: manifest_signed.override_config(config_digest_signed, dest_image_name) data.manifest_signed = manifest_signed await dest_image_source.put_manifest(manifest_signed, dest_image_name) else: raise RuntimeError(f"Unknown derived class: {type(dest_image_source)}") if not self.dry_run: LOGGER.debug("Created new image: %s", dest_image_name.resolve_name()) return data
async def verify_image_signatures( self, image_name: ImageName, **kwargs ) -> ImageSourceVerifyImageSignatures: """ Verifies that signatures contained within the image source data format are valid (that the image has not been modified since they were created) Args: image_name: The image name. Returns: NamedTuple: compressed_layer_files: The list of compressed layer files on disk (optional). image config: The image configuration. manifest: The image source-specific manifest file (archive, registry, repository). signatures: as defined by :func:~docker_sign_verify.ImageConfig.verify_signatures. uncompressed_layer_files: The list of uncompressed layer files on disk. """ # Verify image integrity (we use the verified values from this point on) data = await self.verify_image_integrity(image_name, **kwargs) # Verify image signatures ... try: LOGGER.debug("Verifying Signature(s): %s ...", image_name.resolve_name()) LOGGER.debug( " config digest (signed): %s", xellipsis(data.image_config.get_digest()), ) signatures = await data.image_config.verify_signatures( signer_kwargs=self.signer_kwargs ) data = ImageSourceVerifyImageSignatures( compressed_layer_files=data.compressed_layer_files, image_config=data.image_config, manifest=data.manifest, signatures=signatures, uncompressed_layer_files=data.uncompressed_layer_files, ) # List the image signatures ... LOGGER.debug(" signatures:") for result in data.signatures.results: if not hasattr(result, "valid"): raise UnsupportedSignatureTypeError( f"Unsupported signature type: {type(result)}!" ) if hasattr(result, "signer_short") and hasattr(result, "signer_long"): if not result.valid: raise SignatureMismatchError( f"Verification failed for signature; {result.signer_short}" ) for line in result.signer_long.splitlines(): LOGGER.debug(line) # Try to be friendly ... else: if not result.valid: raise SignatureMismatchError( f"Verification failed for signature; unknown type: {type(result)}!" ) LOGGER.debug(" Signature of unknown type: %s", type(result)) except Exception: for file in data.compressed_layer_files + data.uncompressed_layer_files: file.close() raise LOGGER.debug("Signature check passed.") return data