Compare commits

...

26 Commits

Author SHA1 Message Date
whatsacomputertho b692c2be09
Merge pull request #42 from containers/inspect-quick-example
doc(inspect): add inspect to quick example
2025-05-25 21:00:02 -04:00
whatsacomputertho f3fed327bb doc(inspect): add inspect to quick example
Signed-off-by: whatsacomputertho <ebalcik71@gmail.com>
2025-05-25 20:57:23 -04:00
whatsacomputertho 1228925026
Merge pull request #40 from containers/inspect-doc
fix(doc): add inspect modules to docs
2025-05-25 20:33:26 -04:00
whatsacomputertho c88d87055e fix(doc): add inspect modules to docs
Signed-off-by: whatsacomputertho <ebalcik71@gmail.com>
2025-05-25 20:29:11 -04:00
whatsacomputertho 9474295fbc
Merge pull request #34 from containers/inspect
feat(inspect): implement python-native skopeo inspect
2025-05-25 20:17:47 -04:00
whatsacomputertho 08c7858bd1 test(inspect): add inspect and config unit tests
Signed-off-by: whatsacomputertho <ebalcik71@gmail.com>
2025-05-25 20:14:16 -04:00
whatsacomputertho 532fdbb8fa test(inspect): write basic unit test for inspect
Signed-off-by: whatsacomputertho <ebalcik71@gmail.com>
2025-05-24 21:07:52 -04:00
whatsacomputertho 540fdca826 test(tags): add very basic unit test for listing tags
Signed-off-by: whatsacomputertho <ebalcik71@gmail.com>
2025-05-24 20:17:46 -04:00
whatsacomputertho d6067ee49e feat(tags): implement basic list tags functionality
Signed-off-by: whatsacomputertho <ebalcik71@gmail.com>
2025-05-20 12:30:31 -04:00
whatsacomputertho 1002360e64
Merge pull request #37 from containers/list-entry-mediatypes
fix(validation): do not enforce entry mediatypes
2025-05-14 21:49:59 -04:00
whatsacomputertho f0e0c2b046 fix(tests): remove irrelevant test case after softening validation
Signed-off-by: whatsacomputertho <ebalcik71@gmail.com>
2025-05-13 22:30:55 -04:00
whatsacomputertho c74ded2a9f fix(validation): do not enforce entry mediatypes
Signed-off-by: whatsacomputertho <ebalcik71@gmail.com>
2025-05-13 22:17:54 -04:00
whatsacomputertho 122532f4d7 feat(inspect): implement python-native skopeo inspect
Signed-off-by: whatsacomputertho <ebalcik71@gmail.com>
2025-05-11 21:06:12 -04:00
whatsacomputertho 6b8b068b95
Merge pull request #31 from containers/auth-fix
fix(auth): do auth dance even when basic auth creds are not found
2025-05-10 09:28:13 -04:00
whatsacomputertho 62eaed0b6f refac(auth): only add auth headers for auth server if auth is non empty
Signed-off-by: whatsacomputertho <ebalcik71@gmail.com>
2025-05-07 08:20:54 -04:00
whatsacomputertho 80af3ae997 fix(auth): do auth dance even when basic auth creds are not found
Signed-off-by: whatsacomputertho <ebalcik71@gmail.com>
2025-05-07 08:11:17 -04:00
whatsacomputertho a4cfa50daf
Merge pull request #25 from containers/remove-pip-note
doc(release): remove pip install note
2025-04-29 21:06:11 -04:00
whatsacomputertho 6d26f9ecde doc(release): remove pip install note
Signed-off-by: whatsacomputertho <ebalcik71@gmail.com>
2025-04-29 21:02:36 -04:00
whatsacomputertho 43e692749f
Merge pull request #24 from containers/release-make-target
feat(release): develop publish and release make targets
2025-04-28 22:02:14 -04:00
whatsacomputertho ed931091a7 chore(ci): only trigger release workflow on release
Signed-off-by: whatsacomputertho <ebalcik71@gmail.com>
2025-04-28 21:58:02 -04:00
whatsacomputertho 5e42e18f6a test(ci): test reverted pyproject changes with limited twine version
Signed-off-by: whatsacomputertho <ebalcik71@gmail.com>
2025-04-28 21:54:14 -04:00
whatsacomputertho a9f8d54b6b fix(ci): set upper bound on twine version
Signed-off-by: whatsacomputertho <ebalcik71@gmail.com>
2025-04-28 21:51:42 -04:00
whatsacomputertho 97ba84a2d6 fix(ci): attempt to fix malformed license metadata in packaging
Signed-off-by: whatsacomputertho <ebalcik71@gmail.com>
2025-04-28 21:47:34 -04:00
whatsacomputertho 064f98b861 fix(ci): attempt to fix failing publish in gh actions workflow
Signed-off-by: whatsacomputertho <ebalcik71@gmail.com>
2025-04-28 21:35:29 -04:00
whatsacomputertho 14a289b942 feat(ci): develop release workflow, run doc publish on release publish
Signed-off-by: whatsacomputertho <ebalcik71@gmail.com>
2025-04-28 21:27:12 -04:00
whatsacomputertho eb69e879f2 feat(release): develop publish and release make targets
Signed-off-by: whatsacomputertho <ebalcik71@gmail.com>
2025-04-27 21:27:25 -04:00
26 changed files with 1238 additions and 89 deletions

View File

@ -1,6 +1,10 @@
name: Doc
on: [push, workflow_dispatch]
on:
release:
types: [published]
push:
branches: '**'
permissions:
contents: write
@ -19,7 +23,7 @@ jobs:
make doc
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
if: ${{ github.event_name == 'release' && github.event.release.prerelease == false }}
with:
publish_branch: doc
github_token: ${{ secrets.GITHUB_TOKEN }}

39
.github/workflows/release.yaml vendored Normal file
View File

@ -0,0 +1,39 @@
name: Release
on:
release:
types: [published]
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Test Credentials
if: github.event.release.prerelease == true
run: |
touch $HOME/.pypirc
echo "[testpypi]" >> $HOME/.pypirc
echo " username = __token__" >> $HOME/.pypirc
echo " password = ${{ secrets.TEST_PYPI_TOKEN }}" >> $HOME/.pypirc
- name: Setup Credentials
if: github.event.release.prerelease == false
run: |
touch $HOME/.pypirc
echo "[pypi]" >> $HOME/.pypirc
echo " username = __token__" >> $HOME/.pypirc
echo " password = ${{ secrets.PYPI_TOKEN }}" >> $HOME/.pypirc
- name: Install Dependencies
run: |
make release-dependencies
- name: Test Release Distribution
if: github.event.release.prerelease == true
run: |
make release
- name: Release Distribution
if: github.event.release.prerelease == false
run: |
PYPI=pypi make release

View File

@ -1,5 +1,6 @@
.PHONY: doc
PYTHON ?= /usr/bin/python3
PYPI ?= testpypi
############
# Pre-commit recipes
@ -31,6 +32,27 @@ build-dependencies:
build:
$(PYTHON) -m build
#########
# Publish recipes
#
# Install the required dependencies for the publish recipe
publish-dependencies:
$(PYTHON) -m pip install -r ci/requirements.publish.txt
# Publish the built python distribution
# Assumes the python distribution was already built using build recipe
publish:
$(PYTHON) -m twine upload --repository $(PYPI) dist/*
#########
# Release recipes
#
# Install the required dependencies for the release recipe
release-dependencies: build-dependencies publish-dependencies
# Release the python distribution by building and publishing it
release: build publish
#####
# Doc recipes
#

View File

@ -1,4 +1,4 @@
![containerimage-py](./doc/source/_static/container-image-py.png)
![containerimage-py](https://raw.githubusercontent.com/containers/containerimage-py/main/doc/source/_static/container-image-py.png)
# containerimage-py
@ -8,7 +8,7 @@ A python library for interacting with container images and container image regis
**Docs**: https://containers.github.io/containerimage-py/
**Contributing**: [CONTRIBUTING.md](CONTRIBUTING.md)
**Contributing**: [CONTRIBUTING.md](https://github.com/containers/containerimage-py/blob/main/CONTRIBUTING.md)
## Quick Example
@ -28,6 +28,12 @@ print(
f"Digest for {str(my_image)}: " + \
my_image.get_digest(auth={}) # sha256:1ff6c18fbef2045af6b9c16bf034cc421a29027b800e4f9b68ae9b1cb3e9ae07
)
# Inspect the container image image for a more consolidated summary
print(
f"Inspect for {str(my_image)}:\n" + \
str(my_image.inspect(auth={})) # Same as skopeo inspect docker://registry.k8s.io/pause:3.5
)
```
To run this example, simply execute the following from the root of this repository
@ -39,8 +45,6 @@ python3 examples/quick-example.py
### Using Pip
> IMPORTANT: This project has not yet been released on PyPi. This will not work at this point in time. Instead follow [the local install instructions](#installation).
Run the following command to install the latest version of this package
```
@ -65,4 +69,4 @@ From the root of this repository, execute
make build
```
Under the hood, this will execute `python3 -m build` and produce a `.whl` (wheel) and `.tgz` (TAR-GZip archive) file in the `dist` subdirectory. For more on this project's make recipes, see [CONTRIBUTING.md](CONTRIBUTING.md#other-make-recipes).
Under the hood, this will execute `python3 -m build` and produce a `.whl` (wheel) and `.tgz` (TAR-GZip archive) file in the `dist` subdirectory. For more on this project's make recipes, see [CONTRIBUTING.md](https://github.com/containers/containerimage-py/blob/main/CONTRIBUTING.md#other-make-recipes).

View File

@ -0,0 +1 @@
twine<=6.0.1

View File

@ -52,6 +52,14 @@ image.containerimage module
:undoc-members:
:show-inheritance:
image.containerimageinspect module
----------------------------------
.. automodule:: image.containerimageinspect
:members:
:undoc-members:
:show-inheritance:
image.descriptor module
-----------------------
@ -68,6 +76,14 @@ image.errors module
:undoc-members:
:show-inheritance:
image.inspectschema module
--------------------------
.. automodule:: image.inspectschema
:members:
:undoc-members:
:show-inheritance:
image.manifest module
---------------------

View File

@ -37,13 +37,17 @@ Here is a quick motivating example for how you might use ``containerimage-py`` i
my_image.get_digest(auth={}) # sha256:1ff6c18fbef2045af6b9c16bf034cc421a29027b800e4f9b68ae9b1cb3e9ae07
)
# Inspect the container image image for a more consolidated summary
print(
f"Inspect for {str(my_image)}:\n" + \
str(my_image.inspect(auth={})) # Same as skopeo inspect docker://registry.k8s.io/pause:3.5
)
Installation
============
**Using Pip**
IMPORTANT: This project has not yet been released on PyPi. This will not work at this point in time. Instead follow the local install instructions.
Run the following command to install the latest version of this package using pip
.. code-block:: shell

27
examples/image-inspect.py Normal file
View File

@ -0,0 +1,27 @@
######
# Hack
#
# Make sibling modules visible to this nested executable
import os, sys
sys.path.insert(
0,
os.path.dirname(
os.path.dirname(
os.path.realpath(__file__)
)
)
)
# End Hack
######
from image.containerimage import ContainerImage
# Initialize a ContainerImage given a tag reference
my_image = ContainerImage("registry.k8s.io/pause:3.5")
# Display the inspect information for the container image
my_image_inspect = my_image.inspect(auth={})
print(
f"Inspect of {str(my_image)}: \n" + \
str(my_image_inspect)
)

View File

@ -17,7 +17,7 @@ sys.path.insert(
from image.containerimage import ContainerImage
# Initialize a ContainerImage given a tag reference
my_image = ContainerImage("registry.k8s.io/pause:3.5")
my_image = ContainerImage("ghcr.io/matejvasek/builder-ubi8-base:latest")
# Display some basic information about the container image
print(
@ -28,3 +28,9 @@ print(
f"Digest for {str(my_image)}: " + \
my_image.get_digest(auth={}) # sha256:1ff6c18fbef2045af6b9c16bf034cc421a29027b800e4f9b68ae9b1cb3e9ae07
)
# Inspect the container image image for a more consolidated summary
print(
f"Inspect for {str(my_image)}:\n" + \
str(my_image.inspect(auth={})) # Same as skopeo inspect docker://registry.k8s.io/pause:3.5
)

View File

@ -1,14 +1,21 @@
import image.auth
import image.byteunit
import image.client
import image.config
import image.configschema
import image.containerimage
import image.containerimageinspect
import image.descriptor
import image.errors
import image.inspectschema
import image.manifest
import image.manifestlistentry
import image.manifestschema
import image.mediatypes
import image.oci
import image.ocischema
import image.platform
import image.reference
import image.regex
import image.v2s2
import image.v2s2schema

View File

@ -139,7 +139,7 @@ class ContainerImageRegistryClient:
response, and MUST include the www-authenticate header
Args:
res (Type[requests.Response]): The response from the registry API
res (requests.Response): The response from the registry API
reg_auth (str): The auth retrieved for the registry
Returns:
@ -168,6 +168,8 @@ class ContainerImageRegistryClient:
# Send the request to the auth service, parse the token from the
# response
headers = {}
if len(reg_auth) > 0:
headers = {
'Authorization': f"Basic {reg_auth}"
}
@ -189,11 +191,11 @@ class ContainerImageRegistryClient:
Args:
str_or_ref (Union[str, ContainerImageReference]): An image reference corresponding to the blob descriptor
desc (Type[ContainerImageDescriptor]): A blob descriptor
desc (ContainerImageDescriptor): A blob descriptor
auth (Dict[str, Any]): A valid docker config JSON loaded into a dict
Returns:
Type[requests.Response]: The registry API blob response
requests.Response: The registry API blob response
"""
# If given a str, then load as a ref
ref = str_or_ref
@ -219,7 +221,7 @@ class ContainerImageRegistryClient:
# Send the request to the distribution registry API
# If it fails with a 401 response code and auth given, do OAuth dance
res = requests.get(api_url, headers=headers)
if res.status_code == 401 and found and \
if res.status_code == 401 and \
'www-authenticate' in res.headers.keys():
# Do Oauth dance if basic auth fails
# Ref: https://distribution.github.io/distribution/spec/auth/token/
@ -264,6 +266,90 @@ class ContainerImageRegistryClient:
config = res.json()
return config
@staticmethod
def query_tags(
str_or_ref: Union[str, ContainerImageReference],
auth: Dict[str, Any]
) -> requests.Response:
"""
Fetches the list of tags for a reference from the registry API and
returns as a dict
Args:
str_or_ref (Union[str, ContainerImageReference]): An image reference
auth (Dict[str, Any]): A valid docker config JSON loaded into a dict
Returns:
requests.Response: The registry API tag list response
"""
# If given a str, then load as a ref
ref = str_or_ref
if isinstance(str_or_ref, str):
ref = ContainerImageReference(str_or_ref)
# Construct the API URL for querying the image manifest
api_base_url = ContainerImageRegistryClient.get_registry_base_url(
ref
)
image_identifier = ref.get_identifier()
api_url = f'{api_base_url}/tags/list'
# Construct the headers for querying the image manifest
headers = {
'Accept': 'application/json'
}
# Get the matching auth for the image from the docker config JSON
reg_auth, found = ContainerImageRegistryClient.get_registry_auth(
ref,
auth
)
if found:
headers['Authorization'] = f'Basic {reg_auth}'
# Send the request to the distribution registry API
# If it fails with a 401 response code and auth given, do OAuth dance
res = requests.get(api_url, headers=headers)
if res.status_code == 401 and \
'www-authenticate' in res.headers.keys():
# Do Oauth dance if basic auth fails
# Ref: https://distribution.github.io/distribution/spec/auth/token/
scheme, token = ContainerImageRegistryClient.get_auth_token(
res, reg_auth
)
headers['Authorization'] = f'{scheme} {token}'
res = requests.get(api_url, headers=headers)
# Raise exceptions on error status codes
res.raise_for_status()
return res
@staticmethod
def list_tags(
str_or_ref: Union[str, ContainerImageReference],
auth: Dict[str, Any]
) -> Dict[str, Any]:
"""
Fetches the list of tags for a reference from the registry API and
returns as a dict
Args:
str_or_ref (Union[str, ContainerImageReference]): An image reference
auth (Dict[str, Any]): A valid docker config JSON loaded into a dict
Returns:
Dict[str, Any]: The config as a dict
"""
# If given a str, then load as a ref
ref = str_or_ref
if isinstance(str_or_ref, str):
ref = ContainerImageReference(str_or_ref)
# Query the tags, get the tag list response
res = ContainerImageRegistryClient.query_tags(
ref, auth
)
# Load the tag list into a dict and return
tags = res.json()
return tags
@staticmethod
def query_manifest(
str_or_ref: Union[str, ContainerImageReference],
@ -308,7 +394,7 @@ class ContainerImageRegistryClient:
# Send the request to the distribution registry API
# If it fails with a 401 response code and auth given, do OAuth dance
res = requests.get(api_url, headers=headers)
if res.status_code == 401 and found and \
if res.status_code == 401 and \
'www-authenticate' in res.headers.keys():
# Do Oauth dance if basic auth fails
# Ref: https://distribution.github.io/distribution/spec/auth/token/
@ -433,7 +519,7 @@ class ContainerImageRegistryClient:
# Send the request to the distribution registry API
# If it fails with a 401 response code and auth given, do OAuth dance
res = requests.delete(api_url, headers=headers)
if res.status_code == 401 and found and \
if res.status_code == 401 and \
'www-authenticate' in res.headers.keys():
# Do Oauth dance if basic auth fails
# Ref: https://distribution.github.io/distribution/spec/auth/token/

View File

@ -3,7 +3,7 @@ Contains the ContainerImageConfig class, which specifies the runtime
configuration for a container image.
"""
from typing import Dict, Any, Tuple, Type, Union
from typing import Dict, Any, Tuple, Type, Union, List
from jsonschema import validate, ValidationError
from image.configschema import CONTAINER_IMAGE_CONFIG_SCHEMA
from image.platform import ContainerImagePlatform
@ -106,3 +106,49 @@ class ContainerImageConfig:
if variant != None:
platform_dict["variant"] = variant
return ContainerImagePlatform(platform_dict)
def get_labels(self) -> Dict[str, str]:
"""
Returns the container image labels from the config
Returns:
Dict[str, str]: The labels from the config
"""
return self.get_runtime_config().get("Labels", {})
def get_created_date(self) -> str:
"""
Returns the created date of the container image from the config
Returns:
str: The created date, as a string
"""
return self.config.get("created", "")
def get_runtime_config(self) -> Dict[str, Any]:
"""
Returns the runtime config for the container image from its config
Returns:
Dict[str, Any]: The container image runtime config
"""
return self.config.get("config", {})
def get_env(self) -> List[str]:
"""
Returns the list of environment variables set for the container image
at build time from the container image runtime config
Returns:
List[str]: The list of environment variables
"""
return self.get_runtime_config().get("Env", [])
def get_author(self) -> str:
"""
Returns the author of the container image from its config
Returns:
str: The container image author
"""
return self.config.get("author", "")

View File

@ -14,6 +14,7 @@ from typing import List, Dict, Any, \
from image.byteunit import ByteUnit
from image.client import ContainerImageRegistryClient
from image.config import ContainerImageConfig
from image.containerimageinspect import ContainerImageInspect
from image.errors import ContainerImageError
from image.manifestfactory import ContainerImageManifestFactory
from image.manifestlist import ContainerImageManifestList
@ -85,6 +86,103 @@ class ContainerImage(ContainerImageReference):
return isinstance(manifest, ContainerImageManifestOCI) or \
isinstance(manifest, ContainerImageIndexOCI)
@staticmethod
def get_host_platform_manifest_static(
ref: ContainerImageReference,
manifest: Union[
ContainerImageManifestV2S2,
ContainerImageManifestListV2S2,
ContainerImageManifestOCI,
ContainerImageIndexOCI
],
auth: Dict[str, Any]
) -> Union[
ContainerImageManifestV2S2,
ContainerImageManifestOCI
]:
"""
Given an image's reference and manifest, this static method checks if
the manifest is a manifest list, and attempts to get the manifest from
the list matching the host platform.
Args:
ref (ContainerImageReference): The image reference corresponding to the manifest
manifest (Union[ContainerImageManifestV2S2,ContainerImageManifestListV2S2,ContainerImageManifestOCI,ContainerImageIndexOCI]): The manifest object, generally from get_manifest method
auth (Dict[str, Any]): A valid docker config JSON with auth into the ref's registry
Returns:
Union[ContainerImageManifestV2S2,ContainerImageManifestOCI]: The manifest response from the registry API
Raises:
ContainerImageError: Error if the image is a manifest list without a manifest matching the host platform
"""
host_manifest = manifest
# If manifest list, get the manifest matching the host platform
if ContainerImage.is_manifest_list_static(manifest):
found = False
host_entry_digest = None
host_plt = ContainerImagePlatform.get_host_platform()
entries = manifest.get_entries()
for entry in entries:
if entry.get_platform() == host_plt:
found = True
host_entry_digest = entry.get_digest()
if not found:
raise ContainerImageError(
"no image found in manifest list for platform: " + \
f"{str(host_plt)}"
)
host_ref = ContainerImage(
f"{ref.get_name()}@{host_entry_digest}"
)
host_manifest = host_ref.get_manifest(auth=auth)
# Return the manifest matching the host platform
return host_manifest
@staticmethod
def get_config_static(
ref: ContainerImageReference,
manifest: Union[
ContainerImageManifestV2S2,
ContainerImageManifestListV2S2,
ContainerImageManifestOCI,
ContainerImageIndexOCI
],
auth: Dict[str, Any]
) -> ContainerImageConfig:
"""
Given an image's manifest, this static method fetches that image's
config from the distribution registry API. If the image is a manifest
list, then it gets the config corresponding to the manifest matching
the host platform.
Args:
ref (ContainerImageReference): The image reference corresponding to the manifest
manifest (Union[ContainerImageManifestV2S2,ContainerImageManifestListV2S2,ContainerImageManifestOCI,ContainerImageIndexOCI]): The manifest object, generally from get_manifest method
auth (Dict[str, Any]): A valid docker config JSON with auth into this image's registry
Returns:
ContainerImageConfig: The config for this image
Raises:
ContainerImageError: Error if the image is a manifest list without a manifest matching the host platform
"""
# If manifest list, get the manifest matching the host platform
manifest = ContainerImage.get_host_platform_manifest_static(
ref, manifest, auth
)
# Get the image's config
return ContainerImageConfig(
ContainerImageRegistryClient.get_config(
ref,
manifest.get_config_descriptor(),
auth=auth
)
)
def __init__(self, ref: str):
"""
Constructor for the ContainerImage class
@ -179,6 +277,73 @@ class ContainerImage(ContainerImageReference):
ContainerImageRegistryClient.get_manifest(self, auth)
)
def get_host_platform_manifest(self, auth: Dict[str, Any]) -> Union[
ContainerImageManifestOCI,
ContainerImageManifestV2S2
]:
"""
Fetches the manifest from the distribution registry API. If the
manifest is a manifest list, then it attempts to fetch the manifest
in the list matching the host platform. If not found, an exception is
raised.
Args:
auth (Dict[str, Any]): A valid docker config JSON with auth into this image's registry
Returns:
Union[ContainerImageManifestV2S2,ContainerImageManifestOCI]: The manifest response from the registry API
Raises:
ContainerImageError: Error if the image is a manifest list without a manifest matching the host platform
"""
# Get the container image's manifest
manifest = self.get_manifest(auth=auth)
# Return the host platform manifest
return ContainerImage.get_host_platform_manifest_static(
self,
manifest,
auth
)
def get_config(self, auth: Dict[str, Any]) -> ContainerImageConfig:
"""
Fetches the image's config from the distribution registry API. If the
image is a manifest list, then it gets the config corresponding to the
manifest matching the host platform.
Args:
auth (Dict[str, Any]): A valid docker config JSON with auth into this image's registry
Returns:
ContainerImageConfig: The config for this image
Raises:
ContainerImageError: Error if the image is a manifest list without a manifest matching the host platform
"""
# Get the image's manifest
manifest = self.get_manifest(auth=auth)
# Use the image's manifest to get the image's config
config = ContainerImage.get_config_static(
self, manifest, auth
)
# Return the image's config
return config
def list_tags(self, auth: Dict[str, Any]) -> Dict[str, Any]:
"""
Fetches the list of tags for the image from the distribution registry
API.
Args:
auth (Dict[str, Any]): A valid docker config JSON with auth into this image's registry
Returns:
Dict[str, Any]: The tag list loaded into a dict
"""
return ContainerImageRegistryClient.list_tags(self, auth)
def exists(self, auth: Dict[str, Any]) -> bool:
"""
Determine if the image reference corresponds to an image in the remote
@ -266,6 +431,67 @@ class ContainerImage(ContainerImageReference):
"""
return ByteUnit.format_size_bytes(self.get_size(auth))
def inspect(self, auth: Dict[str, Any]) -> ContainerImageInspect:
"""
Returns a collection of basic information about the image, equivalent
to skopeo inspect.
Args:
auth (Dict[str, Any]): A valid docker config JSON loaded into a dict
Returns:
ContainerImageInspect: A collection of information about the image
"""
# Get the image's manifest
manifest = self.get_host_platform_manifest(auth=auth)
# Use the image's manifest to get the image's config
config = ContainerImage.get_config_static(
self, manifest, auth
)
# List the image's tags
tags = self.list_tags(auth)
# Format the inspect dictionary
inspect = {
"Name": self.get_name(),
"Digest": self.get_digest(auth=auth),
"RepoTags": tags["tags"],
# TODO: Implement v2s1 manifest extension - only v2s1 manifests use this value
"DockerVersion": "",
"Created": config.get_created_date(),
"Labels": config.get_labels(),
"Architecture": config.get_architecture(),
"Os": config.get_os(),
"Layers": [
layer.get_digest() \
for layer \
in manifest.get_layer_descriptors()
],
"LayersData": [
{
"MIMEType": layer.get_media_type(),
"Digest": layer.get_digest(),
"Size": layer.get_size(),
"Annotations": layer.get_annotations() or {}
} for layer in manifest.get_layer_descriptors()
],
"Env": config.get_env()
}
# Set the variant in the inspect dict if found
variant = config.get_variant()
if variant:
inspect["Variant"] = variant
# Set the tag in the inspect dict
if self.is_tag_ref():
inspect["Tag"] = self.get_identifier()
# TODO: Get the RepoTags for the image
return ContainerImageInspect(inspect)
def delete(self, auth: Dict[str, Any]):
"""
Deletes the image from the registry.

View File

@ -0,0 +1,99 @@
import json
import re
from image.errors import ContainerImageError
from image.regex import ANCHORED_DIGEST, ANCHORED_NAME
from image.inspectschema import CONTAINER_IMAGE_INSPECT_SCHEMA
from jsonschema import validate
from typing import Dict, Any, Tuple
class ContainerImageInspect:
"""
Represents a collection of basic informataion about a container image.
This object is equivalent to the output of skopeo inspect.
"""
@staticmethod
def validate_static(inspect: Dict[str, Any]) -> Tuple[bool, str]:
"""
Validate a container image inspect dict using its json schema
Args:
inspect (dict): The container image inspect dict to validate
Returns:
bool: Whether the container image inspect dict was valid
str: Error message if it was invalid
"""
# Validate the container image inspect dict against its json schema
try:
validate(
instance=inspect,
schema=CONTAINER_IMAGE_INSPECT_SCHEMA
)
except Exception as e:
return False, str(e)
# Validate the name and digest
if len(inspect["Name"]) > 0 and not bool(re.match(ANCHORED_NAME, inspect["Name"])):
return False, f"Invalid Name: {inspect['Name']}"
if not bool(re.match(ANCHORED_DIGEST, inspect["Digest"])):
return False, f"Invalid Digest: {inspect['Digest']}"
# Validate the layer and layersdata digests
for digest in inspect["Layers"]:
if not bool(re.match(ANCHORED_DIGEST, digest)):
return False, f"Invalid digest in Layers: {digest}"
for layerdata in inspect["LayersData"]:
if not bool(re.match(ANCHORED_DIGEST, layerdata["Digest"])):
return False, f"Invalid digest in LayersData: {layerdata['Digest']}"
# If all pass then the inspect is valid
return True, ""
def __init__(self, inspect: Dict[str, Any]) -> "ContainerImageInspect":
"""
Constructor for the ContainerImageInspect class
Args:
inspect (dict): The container image inspect dict
Returns:
ContainerImageInspect: The ContainerImageInspect instance
"""
valid, err = ContainerImageInspect.validate_static(inspect)
if not valid:
raise ContainerImageError(f"Invalid inspect dictionary: {err}")
self.inspect = inspect
def validate(self) -> Tuple[bool, str]:
"""
Validate a container image inspect instance
Returns:
bool: Whether the container image inspect dict was valid
str: Error message if it was invalid
"""
return ContainerImageInspect.validate_static(self.inspect)
def __str__(self) -> str:
"""
Stringifies a ContainerImageInspect instance
Args:
None
Returns:
str: The stringified inspect
"""
return json.dumps(self.inspect, indent=2, sort_keys=False)
def __json__(self) -> Dict[str, Any]:
"""
JSONifies a ContainerImageInspect instance
Args:
None
Returns:
Dict[str, Any]: The JSONified descriptor
"""
return self.inspect

156
image/inspectschema.py Normal file
View File

@ -0,0 +1,156 @@
CONTAINER_IMAGE_LAYER_INSPECT_SCHEMA = {
"type": "object",
"description": "The JSON schema for a container image layer inspect " + \
"dictionary.",
"required": [ "MIMEType", "Digest", "Size" ],
"additionalProperties": False,
"properties": {
"MIMEType": {
"type": "string",
"description": "This REQUIRED property is the MIME type, or " + \
"media type, of this container image layer"
},
"Digest": {
"type": "string",
"description": "This REQUIRED property is the digest of this " + \
"container image layer"
},
"Size": {
"type": "integer",
"description": "This REQUIRED property is the size of this " + \
"container image layer measured in bytes"
},
"Annotations": {
"anyOf": [
{
"type": "object",
"description": "This OPTIONAL property is the set of " + \
"annotations belonging to this container image",
"patternProperties": {
"(^.*$)": { "type": "string" }
}
},
{
"type": "null"
}
]
}
}
}
"""
The JSON schema for validating a container image layer inspect dictionary
Ref: https://github.com/containers/image/blob/main/types/types.go#L491-L497
:meta hide-value:
"""
CONTAINER_IMAGE_INSPECT_SCHEMA = {
"type": "object",
"description": "The JSON schema for a container image inspect dictionary",
"required": [
"Digest", "Created", "DockerVersion", "Labels", "Architecture", "Os",
"Layers", "LayersData", "Env"
],
"additionalProperties": False,
"properties": {
"Name": {
"type": "string",
"description": "This OPTIONAL property is the name of this " + \
"container image"
},
"Digest": {
"type": "string",
"description": "This REQUIRED property is the digest of this " + \
"container image"
},
"Tag": {
"type": "string",
"description": "This OPTIONAL property is the tag of this " + \
"container image"
},
"RepoTags": {
"type": "array",
"description": "This OPTIONAL property is the list of tags " + \
"for this container image in the remote registry",
"items": {
"type": "string"
}
},
"Created": {
"type": "string",
"description": "This REQUIRED property is the date this " + \
"container image was built"
},
"DockerVersion": {
"type": "string",
"description": "This REQUIRED property is the version of " + \
"docker used to build this container image"
},
"Labels": {
"anyOf": [
{
"type": "object",
"description": "This REQUIRED property is the set of labels " + \
"applied to this container image at build time",
"patternProperties": {
"(^.*$)": { "type": "string" }
}
},
{
"type": "null"
}
]
},
"Architecture": {
"type": "string",
"description": "This REQUIRED property is the architecture " + \
"for which this container image was built"
},
"Variant": {
"type": "string",
"description": "This OPTIONAL property is the variant of the " + \
"OS and architecture for which this container image was built"
},
"Os": {
"type": "string",
"description": "This REQUIRED property is the operating system" + \
"for which this container image was built"
},
"Layers": {
"type": "array",
"description": "This REQUIRED property contains information " + \
"on the set of layers belonging to this container image",
"items": {
"type": "string"
}
},
"LayersData": {
"type": "array",
"description": "This REQUIRED property contains information " + \
"on the set of layers belonging to this container image",
"items": CONTAINER_IMAGE_LAYER_INSPECT_SCHEMA
},
"Env": {
"type": "array",
"description": "This REQUIRED property contains information " + \
"on the set of environment variables set at build time " + \
"in this container image",
"items": {
"type": "string"
}
},
"Author": {
"type": "string",
"description": "This OPTIONAL property is the author who " + \
"built the container image"
}
}
}
"""
The JSON schema for validating a container image inspect dictionary
Ref:
- https://github.com/containers/image/blob/main/types/types.go#L474-L489
- https://github.com/containers/image/blob/main/types/types.go#L474-L489
:meta hide-value:
"""

View File

@ -156,10 +156,6 @@ class ContainerImageIndexEntryOCI(ContainerImageManifestListEntry):
if not platform_valid:
return platform_valid, err
# If the mediaType is unsupported, then error
if entry["mediaType"] in UNSUPPORTED_OCI_MANIFEST_MEDIA_TYPES:
return False, f"Unsupported mediaType: {entry['mediaType']}"
# Valid if all of the above are valid
return True, ""

View File

@ -4,10 +4,39 @@ architecture, and optionally the variant on which the container image is built
to run
"""
import os
import platform
from typing import Dict, Any, Tuple, Union, List
from jsonschema import validate, ValidationError
from image.manifestschema import IMAGE_INDEX_ENTRY_PLATFORM_SCHEMA
PLATFORM_ARCHITECTURE_MAP = {
'x86_64': 'amd64',
'amd64': 'amd64',
'i386': '386',
'i686': '386',
'arm64': 'arm64',
'aarch64': 'arm64',
'armv7l': 'arm',
'armv6l': 'arm',
}
"""
A map used to transform the output of the platform.machine method such that it
matches the expected value of GoLang's GOARCH environment variable
"""
DEFAULT_HOST_OS = platform.system().lower()
DEFAULT_HOST_ARCH = PLATFORM_ARCHITECTURE_MAP.get(
platform.machine().lower(),
platform.machine().lower()
)
HOST_OS = os.environ.get("HOST_OS", DEFAULT_HOST_OS)
HOST_ARCH = os.environ.get("HOST_ARCH", DEFAULT_HOST_ARCH)
HOST_PLATFORM = {
"os": HOST_OS,
"architecture": HOST_ARCH
}
class ContainerImagePlatform:
"""
Represents platform metadata, which is generally specified in an OCI image
@ -17,6 +46,16 @@ class ContainerImagePlatform:
Note that the OCI and v2s2 specifications do not diverge in their schema for
platform metadata, hence we reuse this class across both scenarios.
"""
@staticmethod
def get_host_platform() -> "ContainerImagePlatform":
"""
Get the platform of the host machine
Returns:
ContainerImagePlatform: The host machine platform
"""
return ContainerImagePlatform(HOST_PLATFORM)
@staticmethod
def validate_static(platform: Dict[str, Any]) -> Tuple[bool, str]:
"""

View File

@ -18,13 +18,23 @@ from image.v2s2schema import MANIFEST_V2_SCHEMA, \
MANIFEST_LIST_V2_SCHEMA, \
MANIFEST_LIST_V2_ENTRY_SCHEMA
# A list of mediaTypes which are not supported by the v2s2 manifest spec
# See doc comments below
UNSUPPORTED_V2S2_MANIFEST_MEDIA_TYPES = [
OCI_MANIFEST_MEDIA_TYPE
]
"""
A list of mediaTypes which are not supported by the v2s2 manifest spec.
This mainly just includes the OCI manifest mediaType.
"""
# See doc comments below
UNSUPPORTED_V2S2_MANIFEST_LIST_MEDIA_TYPES = [
OCI_INDEX_MEDIA_TYPE
]
"""
A list of mediaTypes which are not supported by the v2s2 manifest list
spec. This mainly just includes the OCI index mediaType.
"""
"""
ContainerImageManifestV2S2 class
@ -146,10 +156,6 @@ class ContainerImageManifestListEntryV2S2(ContainerImageManifestListEntry):
if not platform_valid:
return platform_valid, err
# If the mediaType is unsupported, then error
if entry["mediaType"] in UNSUPPORTED_V2S2_MANIFEST_MEDIA_TYPES:
return False, f"Unsupported mediaType: {entry['mediaType']}"
# Valid if all of the above are valid
return True, ""

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "containerimage-py"
version = "0.1.0"
version = "0.1.0-6"
authors = [
{name = "Ethan Balcik", email="ethanbalcik@ibm.com" }
]
@ -17,7 +17,9 @@ classifiers = [
]
keywords = ["container","image","py","oci","docker"]
license = "Apache-2.0"
license-files = ["LICENSE"]
license-files = [
"LICENSE"
]
dependencies = [
"requests",
"jsonschema"

89
tests/config_test.py Normal file
View File

@ -0,0 +1,89 @@
import copy
import json
import os
from image.config import ContainerImageConfig
from typing import Dict, Any
# Get the directory of this file to load the JSON schemas from files
WORKDIR = os.path.dirname(os.path.realpath(__file__))
def _load_json(path: str) -> Dict[str, Any]:
"""
Helper function for loading mock manifests
"""
mock_manifest_dict = {}
with open(path) as mock_manifest_file:
mock_manifest_dict = json.load(mock_manifest_file)
return mock_manifest_dict
EXAMPLE_CONFIG = _load_json(f"{WORKDIR}/mock/configs/config.json")
def test_load_config():
"""
Ensure we can load a config from the OCI image spec
"""
exc = None
try:
inspect = ContainerImageConfig(EXAMPLE_CONFIG)
except Exception as e:
exc = e
assert exc == None
assert isinstance(inspect, ContainerImageConfig)
def test_load_config_no_platform():
"""
Ensure the config fails to load if no OS or arch given
"""
# No os
conf = copy.deepcopy(EXAMPLE_CONFIG)
conf.pop("os")
exc = None
try:
config = ContainerImageConfig(conf)
except Exception as e:
exc = e
assert exc != None
# No architecture
conf = copy.deepcopy(EXAMPLE_CONFIG)
conf.pop("architecture")
exc = None
try:
config = ContainerImageConfig(conf)
except Exception as e:
exc = e
assert exc != None
def test_load_config_no_rootfs():
"""
Ensure the config fails to load if no rootfs property given
"""
# No rootfs property at all
conf = copy.deepcopy(EXAMPLE_CONFIG)
conf.pop("rootfs")
exc = None
try:
config = ContainerImageConfig(conf)
except Exception as e:
exc = e
assert exc != None
# No type in rootfs property
conf = copy.deepcopy(EXAMPLE_CONFIG)
conf["rootfs"].pop("type")
exc = None
try:
config = ContainerImageConfig(conf)
except Exception as e:
exc = e
assert exc != None
# No diff_ids in rootfs property
conf = copy.deepcopy(EXAMPLE_CONFIG)
conf["rootfs"].pop("diff_ids")
exc = None
try:
config = ContainerImageConfig(conf)
except Exception as e:
exc = e
assert exc != None

View File

@ -6,6 +6,7 @@ from image.oci import ContainerImageManifestOCI
from image.containerimage import ContainerImage, \
ContainerImageManifestListV2S2, \
ContainerImageIndexOCI
from image.containerimageinspect import ContainerImageInspect
from tests.registryclientmock import MOCK_IMAGE_NAME, \
MOCK_REGISTRY_CREDS, \
REDHAT_MANIFEST_LIST_EXAMPLE, \
@ -18,7 +19,10 @@ from tests.registryclientmock import MOCK_IMAGE_NAME, \
ATTESTATION_S390X_MANIFEST, \
ATTESTATION_AMD64_ATTESTATION_MANIFEST, \
ATTESTATION_S390X_ATTESTATION_MANIFEST, \
mock_get_manifest
mock_get_manifest, \
mock_list_tags, \
mock_get_config, \
mock_get_digest
def test_container_image_static_validation():
# Ensure the empty string is invalid
@ -250,6 +254,18 @@ def test_container_image_get_name():
assert exc != None
assert isinstance(exc, ContainerImageError)
def test_container_image_list_tags(mocker):
mocker.patch(
"image.containerimage.ContainerImageRegistryClient.list_tags",
mock_list_tags
)
# Ensure the tag list response matches the expected
image = ContainerImage(f"{MOCK_IMAGE_NAME}:latest")
tags = image.list_tags(MOCK_REGISTRY_CREDS)
assert tags["name"] == MOCK_IMAGE_NAME
assert tags["tags"] == [ "latest", "latest-dup", "latest-attestation" ]
def test_container_image_get_manifest(mocker):
mocker.patch(
"image.containerimage.ContainerImageRegistryClient.get_manifest",
@ -290,6 +306,41 @@ def test_container_image_get_manifest(mocker):
assert exc != None
assert isinstance(exc, ContainerImageError)
def test_container_image_inspect(mocker):
mocker.patch(
"image.containerimage.ContainerImageRegistryClient.get_manifest",
mock_get_manifest
)
mocker.patch(
"image.containerimage.ContainerImageRegistryClient.list_tags",
mock_list_tags
)
mocker.patch(
"image.containerimage.ContainerImageRegistryClient.get_config",
mock_get_config
)
mocker.patch(
"image.containerimage.ContainerImageRegistryClient.get_digest",
mock_get_digest
)
image = ContainerImage(f"{MOCK_IMAGE_NAME}:latest")
# Ensure the inspect response matches the expected
exc = None
try:
inspect = image.inspect(MOCK_REGISTRY_CREDS)
except Exception as e:
exc = e
assert exc == None
assert isinstance(inspect, ContainerImageInspect)
assert inspect.inspect["Digest"] == "sha256:8f74ffc756f871ee9037fb8e0c3cd9c5cb54e92e014f92d771ab8e6bf925f372"
assert inspect.inspect["RepoTags"] == [ "latest", "latest-dup", "latest-attestation" ]
assert inspect.inspect["Created"] == "2015-10-31T22:22:56.015925234Z"
assert inspect.inspect["Labels"] == {
"com.example.project.git.url": "https://example.com/project.git",
"com.example.project.git.commit": "45a939b2999782a3f005621a8d0f29aa387e1d6b"
}
def test_container_image_is_manifest_list(mocker):
mocker.patch(
"image.containerimage.ContainerImageRegistryClient.get_manifest",

103
tests/inspect_test.py Normal file
View File

@ -0,0 +1,103 @@
import copy
import json
import os
from image.containerimageinspect import ContainerImageInspect
from typing import Dict, Any
# Get the directory of this file to load the JSON schemas from files
WORKDIR = os.path.dirname(os.path.realpath(__file__))
def _load_json(path: str) -> Dict[str, Any]:
"""
Helper function for loading mock manifests
"""
mock_manifest_dict = {}
with open(path) as mock_manifest_file:
mock_manifest_dict = json.load(mock_manifest_file)
return mock_manifest_dict
EXAMPLE_INSPECT = _load_json(f"{WORKDIR}/mock/inspect/inspect.json")
def test_load_skopeo_inspect_output():
"""
Ensure we can load example output from skopeo inspect
"""
exc = None
try:
inspect = ContainerImageInspect(EXAMPLE_INSPECT)
except Exception as e:
exc = e
assert exc == None
assert isinstance(inspect, ContainerImageInspect)
def test_load_inspect_no_labels():
"""
Ensure we can load an inspect output with null labels
"""
example = copy.deepcopy(EXAMPLE_INSPECT)
example["Labels"] = None
exc = None
try:
inspect = ContainerImageInspect(example)
except Exception as e:
exc = e
assert exc == None
assert isinstance(inspect, ContainerImageInspect)
def test_load_inspect_invalid_name():
"""
Ensure we do not load inspect output with invalid image name
"""
# Invalid name should not work
example = copy.deepcopy(EXAMPLE_INSPECT)
example["Name"] = "Not a container image"
exc = None
try:
inspect = ContainerImageInspect(example)
except Exception as e:
exc = e
assert exc != None
# Empty name should work
example["Name"] = ""
exc = None
try:
inspect = ContainerImageInspect(example)
except Exception as e:
exc = e
assert exc == None
assert isinstance(inspect, ContainerImageInspect)
def test_load_inspect_invalid_digest():
"""
Ensure we do not load inspect output with invalid digests
"""
# Top-level digest
example = copy.deepcopy(EXAMPLE_INSPECT)
example["Digest"] = "notadigest"
exc = None
try:
inspect = ContainerImageInspect(example)
except Exception as e:
exc = e
assert exc != None
# Layer digest(s)
example = copy.deepcopy(EXAMPLE_INSPECT)
example["Layers"][0] = "notadigest"
exc = None
try:
inspect = ContainerImageInspect(example)
except Exception as e:
exc = e
assert exc != None
# LayersData digest(s)
example = copy.deepcopy(EXAMPLE_INSPECT)
example["LayersData"][0]["Digest"] = "notadigest"
exc = None
try:
inspect = ContainerImageInspect(example)
except Exception as e:
exc = e
assert exc != None

View File

@ -0,0 +1,56 @@
{
"created": "2015-10-31T22:22:56.015925234Z",
"author": "Alyssa P. Hacker <alyspdev@example.com>",
"architecture": "amd64",
"os": "linux",
"config": {
"User": "alice",
"ExposedPorts": {
"8080/tcp": {}
},
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"FOO=oci_is_a",
"BAR=well_written_spec"
],
"Entrypoint": [
"/bin/my-app-binary"
],
"Cmd": [
"--foreground",
"--config",
"/etc/my-app.d/default.cfg"
],
"Volumes": {
"/var/job-result-data": {},
"/var/log/my-app-logs": {}
},
"WorkingDir": "/home/alice",
"Labels": {
"com.example.project.git.url": "https://example.com/project.git",
"com.example.project.git.commit": "45a939b2999782a3f005621a8d0f29aa387e1d6b"
}
},
"rootfs": {
"diff_ids": [
"sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1",
"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
],
"type": "layers"
},
"history": [
{
"created": "2015-10-31T22:22:54.690851953Z",
"created_by": "/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /"
},
{
"created": "2015-10-31T22:22:55.613815829Z",
"created_by": "/bin/sh -c #(nop) CMD [\"sh\"]",
"empty_layer": true
},
{
"created": "2015-10-31T22:22:56.329850019Z",
"created_by": "/bin/sh -c apk add curl"
}
]
}

View File

@ -0,0 +1,35 @@
{
"Name": "registry.fedoraproject.org/fedora",
"Digest": "sha256:0f65bee641e821f8118acafb44c2f8fe30c2fc6b9a2b3729c0660376391aa117",
"RepoTags": [
"34-aarch64",
"34",
"latest"
],
"Created": "2022-11-24T13:54:18Z",
"DockerVersion": "1.10.1",
"Labels": {
"license": "MIT",
"name": "fedora",
"vendor": "Fedora Project",
"version": "37"
},
"Architecture": "amd64",
"Os": "linux",
"Layers": [
"sha256:2a0fc6bf62e155737f0ace6142ee686f3c471c1aab4241dc3128904db46288f0"
],
"LayersData": [
{
"MIMEType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"Digest": "sha256:2a0fc6bf62e155737f0ace6142ee686f3c471c1aab4241dc3128904db46288f0",
"Size": 71355009,
"Annotations": null
}
],
"Env": [
"DISTTAG=f37container",
"FGC=f37",
"container=oci"
]
}

View File

@ -96,20 +96,6 @@ OCI_IMAGE_INDEX_EXAMPLE = {
}
}
# An example manifest list entry from the CNCF manifest v2s2 spec
CNCF_MANIFEST_LIST_ENTRY_EXAMPLE = {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
"size": 7682,
"platform": {
"architecture": "amd64",
"os": "linux",
"features": [
"sse4"
]
}
}
# An example image index entry following the OCI spec
DOCKER_BUILDX_ATTESTATION_INDEX_ENTRY = {
"mediaType": "application/vnd.oci.image.manifest.v1+json",
@ -229,14 +215,6 @@ def test_container_image_oci_image_index_entry_static_validation():
assert isinstance(err, str)
assert len(err) > 0
# CNCF (non-OCI) example should be invalid
cncf_example_valid, err = ContainerImageIndexEntryOCI.validate_static(
copy.deepcopy(CNCF_MANIFEST_LIST_ENTRY_EXAMPLE)
)
assert cncf_example_valid == False
assert isinstance(err, str)
assert len(err) > 0
def test_container_image_oci_image_index_entry_instantiation():
# Invalid entry should raise ValidationError
exc = None

View File

@ -3,6 +3,8 @@ import os
from typing import Any, Dict, Type, Union
from image.v2s2 import ContainerImageManifestV2S2
from image.containerimage import ContainerImage
from image.config import ContainerImageConfig
from image.descriptor import ContainerImageDescriptor
"""
ContainerImageRegistryClient mocks
@ -70,9 +72,24 @@ ATTESTATION_S390X_ATTESTATION_MANIFEST = _load_mock_manifest(
f"{WORKDIR}/mock/manifests/attestation-s390x-attestation-manifest.json"
)
# An example container image config
MOCK_CONFIG = _load_mock_manifest(
f"{WORKDIR}/mock/configs/config.json"
)
# Mock an image name
MOCK_IMAGE_NAME = "this.is/my/registry/and-my-image"
# Mock an image tag list response
MOCK_TAG_LIST_RESPONSE = {
"name": MOCK_IMAGE_NAME,
"tags": [
"latest",
"latest-dup",
"latest-attestation"
]
}
# Mock image registry creds for the above image name mock
MOCK_REGISTRY_CREDS = {
"auths": {
@ -135,6 +152,27 @@ def mock_get_manifest(ref_or_img: Union[str, ContainerImage], auth: Dict[str, An
else:
raise Exception(f"Unmocked reference: {ref_or_img}")
# Mock the ContainerImageRegistryClient.get_config function
def mock_get_config(
ref_or_img: Union[str, ContainerImage],
config_desc: ContainerImageDescriptor,
auth: Dict[str, Any]
) -> Dict[str, Any]:
"""
Mocks the ContainerImageRegistryClient.get_config function
Args:
ref (Union[str, ContainerImage]): The image reference
auth (Dict[str, Any]): The auth for the reference
Returns:
ContainerImageConfig: The container image config
"""
if str(ref_or_img) == f"{MOCK_IMAGE_NAME}:latest":
return MOCK_CONFIG
else:
raise Exception(f"Unmocked reference: {ref_or_img}")
def mock_get_digest(ref_or_img: Union[str, ContainerImage], auth: Dict[str, Any]) -> str:
"""
Mocks the ContainerImageRegistryClient.get_digest function
@ -150,3 +188,16 @@ def mock_get_digest(ref_or_img: Union[str, ContainerImage], auth: Dict[str, Any]
return "sha256:8f74ffc756f871ee9037fb8e0c3cd9c5cb54e92e014f92d771ab8e6bf925f372"
else:
raise Exception(f"Unmocked reference: {ref_or_img}")
def mock_list_tags(ref_or_img: Union[str, ContainerImage], auth: Dict[str, Any]) -> Dict[str, Any]:
"""
Mocks the ContainerImageRegistryClient.list_tags function
Args:
ref (Union[str, ContainerImage]): The image reference
auth (Dict[str, Any]): The auth for the reference
Returns:
Dict[str, Any]: The tag list response
"""
return MOCK_TAG_LIST_RESPONSE