Skip to content

Commit 122532f

Browse files
feat(inspect): implement python-native skopeo inspect
Signed-off-by: whatsacomputertho <[email protected]>
1 parent 6b8b068 commit 122532f

File tree

6 files changed

+551
-16
lines changed

6 files changed

+551
-16
lines changed

examples/image-inspect.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
######
2+
# Hack
3+
#
4+
# Make sibling modules visible to this nested executable
5+
import os, sys
6+
sys.path.insert(
7+
0,
8+
os.path.dirname(
9+
os.path.dirname(
10+
os.path.realpath(__file__)
11+
)
12+
)
13+
)
14+
# End Hack
15+
######
16+
17+
from image.containerimage import ContainerImage
18+
19+
# Initialize a ContainerImage given a tag reference
20+
my_image = ContainerImage("registry.k8s.io/pause:3.5")
21+
22+
# Display the inspect information for the container image
23+
my_image_inspect = my_image.inspect(auth={})
24+
print(
25+
f"Inspect of {str(my_image)}: \n" + \
26+
str(my_image_inspect)
27+
)

image/config.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
configuration for a container image.
44
"""
55

6-
from typing import Dict, Any, Tuple, Type, Union
6+
from typing import Dict, Any, Tuple, Type, Union, List
77
from jsonschema import validate, ValidationError
88
from image.configschema import CONTAINER_IMAGE_CONFIG_SCHEMA
99
from image.platform import ContainerImagePlatform
@@ -105,4 +105,50 @@ def get_platform(self) -> Type[ContainerImagePlatform]:
105105
variant = self.get_variant()
106106
if variant != None:
107107
platform_dict["variant"] = variant
108-
return ContainerImagePlatform(platform_dict)
108+
return ContainerImagePlatform(platform_dict)
109+
110+
def get_labels(self) -> Dict[str, str]:
111+
"""
112+
Returns the container image labels from the config
113+
114+
Returns:
115+
Dict[str, str]: The labels from the config
116+
"""
117+
return self.config.get("Labels", {})
118+
119+
def get_created_date(self) -> str:
120+
"""
121+
Returns the created date of the container image from the config
122+
123+
Returns:
124+
str: The created date, as a string
125+
"""
126+
return self.config.get("created", "")
127+
128+
def get_runtime_config(self) -> Dict[str, Any]:
129+
"""
130+
Returns the runtime config for the container image from its config
131+
132+
Returns:
133+
Dict[str, Any]: The container image runtime config
134+
"""
135+
return self.config.get("config", {})
136+
137+
def get_env(self) -> List[str]:
138+
"""
139+
Returns the list of environment variables set for the container image
140+
at build time from the container image runtime config
141+
142+
Returns:
143+
List[str]: The list of environment variables
144+
"""
145+
return self.get_runtime_config().get("Env", [])
146+
147+
def get_author(self) -> str:
148+
"""
149+
Returns the author of the container image from its config
150+
151+
Returns:
152+
str: The container image author
153+
"""
154+
return self.config.get("Author", "")

image/containerimage.py

Lines changed: 221 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,21 @@
99
from __future__ import annotations
1010
import json
1111
import requests
12-
from typing import List, Dict, Any, \
13-
Union, Type, Iterator
14-
from image.byteunit import ByteUnit
15-
from image.client import ContainerImageRegistryClient
16-
from image.config import ContainerImageConfig
17-
from image.errors import ContainerImageError
18-
from image.manifestfactory import ContainerImageManifestFactory
19-
from image.manifestlist import ContainerImageManifestList
20-
from image.oci import ContainerImageManifestOCI, \
21-
ContainerImageIndexOCI
22-
from image.platform import ContainerImagePlatform
23-
from image.reference import ContainerImageReference
24-
from image.v2s2 import ContainerImageManifestV2S2, \
25-
ContainerImageManifestListV2S2
12+
from typing import List, Dict, Any, \
13+
Union, Type, Iterator
14+
from image.byteunit import ByteUnit
15+
from image.client import ContainerImageRegistryClient
16+
from image.config import ContainerImageConfig
17+
from image.containerimageinspect import ContainerImageInspect
18+
from image.errors import ContainerImageError
19+
from image.manifestfactory import ContainerImageManifestFactory
20+
from image.manifestlist import ContainerImageManifestList
21+
from image.oci import ContainerImageManifestOCI, \
22+
ContainerImageIndexOCI
23+
from image.platform import ContainerImagePlatform
24+
from image.reference import ContainerImageReference
25+
from image.v2s2 import ContainerImageManifestV2S2, \
26+
ContainerImageManifestListV2S2
2627

2728
#########################################
2829
# Classes for managing container images #
@@ -85,6 +86,103 @@ def is_oci_static(
8586
return isinstance(manifest, ContainerImageManifestOCI) or \
8687
isinstance(manifest, ContainerImageIndexOCI)
8788

89+
@staticmethod
90+
def get_host_platform_manifest_static(
91+
ref: ContainerImageReference,
92+
manifest: Union[
93+
ContainerImageManifestV2S2,
94+
ContainerImageManifestListV2S2,
95+
ContainerImageManifestOCI,
96+
ContainerImageIndexOCI
97+
],
98+
auth: Dict[str, Any]
99+
) -> Union[
100+
ContainerImageManifestV2S2,
101+
ContainerImageManifestOCI
102+
]:
103+
"""
104+
Given an image's reference and manifest, this static method checks if
105+
the manifest is a manifest list, and attempts to get the manifest from
106+
the list matching the host platform.
107+
108+
Args:
109+
ref (ContainerImageReference): The image reference corresponding to the manifest
110+
manifest (Union[ContainerImageManifestV2S2,ContainerImageManifestListV2S2,ContainerImageManifestOCI,ContainerImageIndexOCI]): The manifest object, generally from get_manifest method
111+
auth (Dict[str, Any]): A valid docker config JSON with auth into the ref's registry
112+
113+
Returns:
114+
Union[ContainerImageManifestV2S2,ContainerImageManifestOCI]: The manifest response from the registry API
115+
116+
Raises:
117+
ContainerImageError: Error if the image is a manifest list without a manifest matching the host platform
118+
"""
119+
host_manifest = manifest
120+
121+
# If manifest list, get the manifest matching the host platform
122+
if ContainerImage.is_manifest_list_static(manifest):
123+
found = False
124+
host_entry_digest = None
125+
host_plt = ContainerImagePlatform.get_host_platform()
126+
entries = manifest.get_entries()
127+
for entry in entries:
128+
if entry.get_platform() == host_plt:
129+
found = True
130+
host_entry_digest = entry.get_digest()
131+
if not found:
132+
raise ContainerImageError(
133+
"no image found in manifest list for platform: " + \
134+
f"{str(host_plt)}"
135+
)
136+
host_ref = ContainerImage(
137+
f"{ref.get_name()}@{host_entry_digest}"
138+
)
139+
host_manifest = host_ref.get_manifest(auth=auth)
140+
141+
# Return the manifest matching the host platform
142+
return host_manifest
143+
144+
@staticmethod
145+
def get_config_static(
146+
ref: ContainerImageReference,
147+
manifest: Union[
148+
ContainerImageManifestV2S2,
149+
ContainerImageManifestListV2S2,
150+
ContainerImageManifestOCI,
151+
ContainerImageIndexOCI
152+
],
153+
auth: Dict[str, Any]
154+
) -> ContainerImageConfig:
155+
"""
156+
Given an image's manifest, this static method fetches that image's
157+
config from the distribution registry API. If the image is a manifest
158+
list, then it gets the config corresponding to the manifest matching
159+
the host platform.
160+
161+
Args:
162+
ref (ContainerImageReference): The image reference corresponding to the manifest
163+
manifest (Union[ContainerImageManifestV2S2,ContainerImageManifestListV2S2,ContainerImageManifestOCI,ContainerImageIndexOCI]): The manifest object, generally from get_manifest method
164+
auth (Dict[str, Any]): A valid docker config JSON with auth into this image's registry
165+
166+
Returns:
167+
ContainerImageConfig: The config for this image
168+
169+
Raises:
170+
ContainerImageError: Error if the image is a manifest list without a manifest matching the host platform
171+
"""
172+
# If manifest list, get the manifest matching the host platform
173+
manifest = ContainerImage.get_host_platform_manifest_static(
174+
ref, manifest, auth
175+
)
176+
177+
# Get the image's config
178+
return ContainerImageConfig(
179+
ContainerImageRegistryClient.get_config(
180+
ref,
181+
manifest.get_config_descriptor(),
182+
auth=auth
183+
)
184+
)
185+
88186
def __init__(self, ref: str):
89187
"""
90188
Constructor for the ContainerImage class
@@ -179,6 +277,61 @@ def get_manifest(self, auth: Dict[str, Any]) -> Union[
179277
ContainerImageRegistryClient.get_manifest(self, auth)
180278
)
181279

280+
def get_host_platform_manifest(self, auth: Dict[str, Any]) -> Union[
281+
ContainerImageManifestOCI,
282+
ContainerImageManifestV2S2
283+
]:
284+
"""
285+
Fetches the manifest from the distribution registry API. If the
286+
manifest is a manifest list, then it attempts to fetch the manifest
287+
in the list matching the host platform. If not found, an exception is
288+
raised.
289+
290+
Args:
291+
auth (Dict[str, Any]): A valid docker config JSON with auth into this image's registry
292+
293+
Returns:
294+
Union[ContainerImageManifestV2S2,ContainerImageManifestOCI]: The manifest response from the registry API
295+
296+
Raises:
297+
ContainerImageError: Error if the image is a manifest list without a manifest matching the host platform
298+
"""
299+
# Get the container image's manifest
300+
manifest = self.get_manifest(auth=auth)
301+
302+
# Return the host platform manifest
303+
return ContainerImage.get_host_platform_manifest_static(
304+
self,
305+
manifest,
306+
auth
307+
)
308+
309+
def get_config(self, auth: Dict[str, Any]) -> ContainerImageConfig:
310+
"""
311+
Fetches the image's config from the distribution registry API. If the
312+
image is a manifest list, then it gets the config corresponding to the
313+
manifest matching the host platform.
314+
315+
Args:
316+
auth (Dict[str, Any]): A valid docker config JSON with auth into this image's registry
317+
318+
Returns:
319+
ContainerImageConfig: The config for this image
320+
321+
Raises:
322+
ContainerImageError: Error if the image is a manifest list without a manifest matching the host platform
323+
"""
324+
# Get the image's manifest
325+
manifest = self.get_manifest(auth=auth)
326+
327+
# Use the image's manifest to get the image's config
328+
config = ContainerImage.get_config_static(
329+
self, manifest, auth
330+
)
331+
332+
# Return the image's config
333+
return config
334+
182335
def exists(self, auth: Dict[str, Any]) -> bool:
183336
"""
184337
Determine if the image reference corresponds to an image in the remote
@@ -266,6 +419,60 @@ def get_size_formatted(self, auth: Dict[str, Any]) -> str:
266419
"""
267420
return ByteUnit.format_size_bytes(self.get_size(auth))
268421

422+
def inspect(self, auth: Dict[str, Any]) -> ContainerImageInspect:
423+
"""
424+
Returns a collection of basic information about the image, equivalent
425+
to skopeo inspect.
426+
427+
Args:
428+
auth (Dict[str, Any]): A valid docker config JSON loaded into a dict
429+
430+
Returns:
431+
ContainerImageInspect: A collection of information about the image
432+
"""
433+
# Get the image's manifest
434+
manifest = self.get_host_platform_manifest(auth=auth)
435+
436+
# Use the image's manifest to get the image's config
437+
config = ContainerImage.get_config_static(
438+
self, manifest, auth
439+
)
440+
441+
# Format the inspect dictionary
442+
inspect = {
443+
"Name": self.get_name(),
444+
"Digest": self.get_digest(auth=auth),
445+
# TODO: Implement v2s1 manifest extension - only v2s1 manifests use this value
446+
"DockerVersion": "",
447+
"Created": config.get_created_date(),
448+
"Labels": config.get_labels(),
449+
"Architecture": config.get_architecture(),
450+
"Variant": config.get_variant() or "",
451+
"Os": config.get_os(),
452+
"Layers": [
453+
layer.get_digest() \
454+
for layer \
455+
in manifest.get_layer_descriptors()
456+
],
457+
"LayersData": [
458+
{
459+
"MIMEType": layer.get_media_type(),
460+
"Digest": layer.get_digest(),
461+
"Size": layer.get_size(),
462+
"Annotations": layer.get_annotations() or {}
463+
} for layer in manifest.get_layer_descriptors()
464+
],
465+
"Env": config.get_env(),
466+
"Author": config.get_author()
467+
}
468+
469+
# Set the tag in the inspect dict
470+
if self.is_tag_ref():
471+
inspect["Tag"] = self.get_identifier()
472+
473+
# TODO: Get the RepoTags for the image
474+
return ContainerImageInspect(inspect)
475+
269476
def delete(self, auth: Dict[str, Any]):
270477
"""
271478
Deletes the image from the registry.

0 commit comments

Comments
 (0)