Skip to content

Commit 1604620

Browse files
committed
Split OCI image manifest from generic manifest in gardenlinux.oci
Signed-off-by: Tobias Wolf <[email protected]>
1 parent a21b7c5 commit 1604620

File tree

7 files changed

+483
-221
lines changed

7 files changed

+483
-221
lines changed

src/gardenlinux/oci/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
"""
66

77
from .container import Container
8+
from .image_manifest import ImageManifest
89
from .index import Index
910
from .layer import Layer
1011
from .manifest import Manifest
1112

12-
__all__ = ["Container", "Index", "Layer", "Manifest"]
13+
__all__ = ["Container", "ImageManifest", "Index", "Layer", "Manifest"]

src/gardenlinux/oci/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def push_manifest(
121121
required=False,
122122
type=click.Path(),
123123
default=None,
124-
help="Canonical Name of Image"
124+
help="Canonical Name of Image",
125125
)
126126
@click.option(
127127
"--arch",

src/gardenlinux/oci/container.py

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
from .index import Index
3030
from .layer import Layer
31+
from .image_manifest import ImageManifest
3132
from .manifest import Manifest
3233
from .schemas import index as IndexSchema
3334

@@ -123,17 +124,7 @@ def __init__(
123124
except Exception as login_error:
124125
self._logger.error(f"Login error: {str(login_error)}")
125126

126-
def generate_index(self):
127-
"""
128-
Generates an OCI image index
129-
130-
:return: (object) OCI image index
131-
:since: 0.7.0
132-
"""
133-
134-
return Index()
135-
136-
def generate_manifest(
127+
def generate_image_manifest(
137128
self,
138129
cname: str,
139130
architecture: Optional[str] = None,
@@ -151,7 +142,7 @@ def generate_manifest(
151142
:param feature_set: The expanded list of the included features of this manifest
152143
153144
:return: (object) OCI image manifest
154-
:since: 0.7.0
145+
:since: 0.9.2
155146
"""
156147

157148
cname_object = CName(cname, architecture, version)
@@ -168,15 +159,13 @@ def generate_manifest(
168159
if commit is None:
169160
commit = ""
170161

171-
manifest = Manifest()
162+
manifest = ImageManifest()
172163

173-
manifest["annotations"] = {}
174-
manifest["annotations"]["version"] = version
175-
manifest["annotations"]["cname"] = cname
176-
manifest["annotations"]["architecture"] = architecture
177-
manifest["annotations"]["feature_set"] = feature_set
178-
manifest["annotations"]["flavor"] = f"{cname_object.flavor}-{architecture}"
179-
manifest["annotations"]["commit"] = commit
164+
manifest.version = version
165+
manifest.cname = cname
166+
manifest.arch = architecture
167+
manifest.feature_set = feature_set
168+
manifest.commit = commit
180169

181170
description = (
182171
f"Image: {cname} "
@@ -195,6 +184,43 @@ def generate_manifest(
195184

196185
return manifest
197186

187+
def generate_index(self):
188+
"""
189+
Generates an OCI image index
190+
191+
:return: (object) OCI image index
192+
:since: 0.7.0
193+
"""
194+
195+
return Index()
196+
197+
def generate_manifest(
198+
self,
199+
version: Optional[str] = None,
200+
commit: Optional[str] = None,
201+
):
202+
"""
203+
Generates an OCI manifest
204+
205+
:param cname: Canonical name of the manifest
206+
:param architecture: Target architecture of the manifest
207+
:param version: Artifacts version of the manifest
208+
:param commit: The commit hash of the manifest
209+
:param feature_set: The expanded list of the included features of this manifest
210+
211+
:return: (object) OCI manifest
212+
:since: 0.9.2
213+
"""
214+
215+
manifest = Manifest()
216+
217+
manifest.version = version
218+
manifest.commit = commit
219+
220+
manifest.config_from_dict({}, {})
221+
222+
return manifest
223+
198224
def _get_index_without_response_parsing(self):
199225
"""
200226
Return the response of an OCI image index request.
@@ -546,23 +572,28 @@ def read_or_generate_manifest(
546572
"""
547573

548574
if cname is None:
549-
response = self._get_manifest_without_response_parsing(self._container_version)
575+
manifest_type = Manifest
576+
577+
response = self._get_manifest_without_response_parsing(
578+
self._container_version
579+
)
550580
else:
581+
manifest_type = ImageManifest
582+
551583
if architecture is None:
552584
architecture = CName(cname, architecture, version).arch
553585

554586
response = self._get_manifest_without_response_parsing(
555587
f"{self._container_version}-{cname}-{architecture}"
556588
)
557-
#
558589

559590
if response.ok:
560-
manifest = Manifest(**response.json())
591+
manifest = manifest_type(**response.json())
561592
elif response.status_code == 404:
562593
if cname is None:
563-
manifest = Manifest()
594+
manifest = self.generate_manifest(version, commit)
564595
else:
565-
manifest = self.generate_manifest(
596+
manifest = self.generate_image_manifest(
566597
cname, architecture, version, commit, feature_set
567598
)
568599
else:
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import json
4+
from copy import deepcopy
5+
from oras.oci import Layer
6+
from os import PathLike
7+
from pathlib import Path
8+
9+
from ..features import CName
10+
11+
from .manifest import Manifest
12+
from .platform import NewPlatform
13+
from .schemas import EmptyManifestMetadata
14+
15+
16+
class ImageManifest(Manifest):
17+
"""
18+
OCI image manifest
19+
20+
:author: Garden Linux Maintainers
21+
:copyright: Copyright 2024 SAP SE
22+
:package: gardenlinux
23+
:subpackage: oci
24+
:since: 0.7.0
25+
:license: https://www.apache.org/licenses/LICENSE-2.0
26+
Apache License, Version 2.0
27+
"""
28+
29+
@property
30+
def arch(self):
31+
"""
32+
Returns the architecture of the OCI image manifest.
33+
34+
:return: (str) OCI image architecture
35+
:since: 0.7.0
36+
"""
37+
38+
if "architecture" not in self.get("annotations", {}):
39+
raise RuntimeError(
40+
"Unexpected manifest with missing config annotation 'architecture' found"
41+
)
42+
43+
return self["annotations"]["architecture"]
44+
45+
@arch.setter
46+
def arch(self, value):
47+
"""
48+
Sets the architecture of the OCI image manifest.
49+
50+
:param value: OCI image architecture
51+
52+
:since: 0.7.0
53+
"""
54+
55+
self._ensure_annotations_dict()
56+
self["annotations"]["architecture"] = value
57+
58+
@property
59+
def cname(self):
60+
"""
61+
Returns the GardenLinux canonical name of the OCI image manifest.
62+
63+
:return: (str) OCI image GardenLinux canonical name
64+
:since: 0.7.0
65+
"""
66+
67+
if "cname" not in self.get("annotations", {}):
68+
raise RuntimeError(
69+
"Unexpected manifest with missing config annotation 'cname' found"
70+
)
71+
72+
return self["annotations"]["cname"]
73+
74+
@cname.setter
75+
def cname(self, value):
76+
"""
77+
Sets the GardenLinux canonical name of the OCI image manifest.
78+
79+
:param value: OCI image GardenLinux canonical name
80+
81+
:since: 0.7.0
82+
"""
83+
84+
self._ensure_annotations_dict()
85+
self["annotations"]["cname"] = value
86+
87+
@property
88+
def feature_set(self):
89+
"""
90+
Returns the GardenLinux feature set of the OCI image manifest.
91+
92+
:return: (str) OCI image GardenLinux feature set
93+
:since: 0.7.0
94+
"""
95+
96+
if "feature_set" not in self.get("annotations", {}):
97+
raise RuntimeError(
98+
"Unexpected manifest with missing config annotation 'feature_set' found"
99+
)
100+
101+
return self["annotations"]["feature_set"]
102+
103+
@feature_set.setter
104+
def feature_set(self, value):
105+
"""
106+
Sets the GardenLinux feature set of the OCI image manifest.
107+
108+
:param value: OCI image GardenLinux feature set
109+
110+
:since: 0.7.0
111+
"""
112+
113+
self._ensure_annotations_dict()
114+
self["annotations"]["feature_set"] = value
115+
116+
@property
117+
def flavor(self):
118+
"""
119+
Returns the GardenLinux flavor of the OCI image manifest.
120+
121+
:return: (str) OCI image GardenLinux flavor
122+
:since: 0.7.0
123+
"""
124+
125+
return CName(self.cname).flavor
126+
127+
@property
128+
def layers_as_dict(self):
129+
"""
130+
Returns the OCI image manifest layers as a dictionary.
131+
132+
:return: (dict) OCI image manifest layers with title as key
133+
:since: 0.7.0
134+
"""
135+
136+
layers = {}
137+
138+
for layer in self["layers"]:
139+
if "org.opencontainers.image.title" not in layer.get("annotations", {}):
140+
raise RuntimeError(
141+
"Unexpected layer with missing annotation 'org.opencontainers.image.title' found"
142+
)
143+
144+
layers[layer["annotations"]["org.opencontainers.image.title"]] = layer
145+
146+
return layers
147+
148+
@property
149+
def version(self):
150+
"""
151+
Returns the GardenLinux version of the OCI image manifest.
152+
153+
:return: (str) OCI image GardenLinux version
154+
:since: 0.7.0
155+
"""
156+
157+
if "version" not in self.get("annotations", {}):
158+
raise RuntimeError(
159+
"Unexpected manifest with missing config annotation 'version' found"
160+
)
161+
162+
return self["annotations"]["version"]
163+
164+
@version.setter
165+
def version(self, value):
166+
"""
167+
Sets the GardenLinux version of the OCI image manifest.
168+
169+
:param value: OCI image GardenLinux version
170+
171+
:since: 0.7.0
172+
"""
173+
174+
self._ensure_annotations_dict()
175+
self["annotations"]["version"] = value
176+
177+
def append_layer(self, layer):
178+
"""
179+
Appends the given OCI image manifest layer to the manifest
180+
181+
:param layer: OCI image manifest layer
182+
183+
:since: 0.7.0
184+
"""
185+
186+
if not isinstance(layer, Layer):
187+
raise RuntimeError("Unexpected layer type given")
188+
189+
layer_dict = layer.dict
190+
191+
if "org.opencontainers.image.title" not in layer_dict.get("annotations", {}):
192+
raise RuntimeError(
193+
"Unexpected layer with missing annotation 'org.opencontainers.image.title' found"
194+
)
195+
196+
image_title = layer_dict["annotations"]["org.opencontainers.image.title"]
197+
existing_layer_index = 0
198+
199+
for existing_layer in self["layers"]:
200+
if "org.opencontainers.image.title" not in existing_layer.get(
201+
"annotations", {}
202+
):
203+
raise RuntimeError(
204+
"Unexpected layer with missing annotation 'org.opencontainers.image.title' found"
205+
)
206+
207+
if (
208+
image_title
209+
== existing_layer["annotations"]["org.opencontainers.image.title"]
210+
):
211+
break
212+
213+
existing_layer_index += 1
214+
215+
if len(self["layers"]) > existing_layer_index:
216+
self["layers"].pop(existing_layer_index)
217+
218+
self["layers"].append(layer_dict)
219+
220+
def write_metadata_file(self, manifest_file_path_name):
221+
if not isinstance(manifest_file_path_name, PathLike):
222+
manifest_file_path_name = Path(manifest_file_path_name)
223+
224+
metadata_annotations = {
225+
"cname": self.cname,
226+
"architecture": self.arch,
227+
"feature_set": self.feature_set,
228+
}
229+
230+
metadata = deepcopy(EmptyManifestMetadata)
231+
metadata["mediaType"] = "application/vnd.oci.image.manifest.v1+json"
232+
metadata["digest"] = self.digest
233+
metadata["size"] = self.size
234+
metadata["annotations"] = metadata_annotations
235+
metadata["platform"] = NewPlatform(self.arch, self.version)
236+
237+
with open(manifest_file_path_name, "w") as fp:
238+
fp.write(json.dumps(metadata))

0 commit comments

Comments
 (0)