Skip to content

Commit 534480b

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

File tree

5 files changed

+319
-219
lines changed

5 files changed

+319
-219
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/container.py

Lines changed: 50 additions & 24 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

@@ -117,17 +118,7 @@ def __init__(
117118
except Exception as login_error:
118119
self._logger.error(f"Login error: {str(login_error)}")
119120

120-
def generate_index(self):
121-
"""
122-
Generates an OCI image index
123-
124-
:return: (object) OCI image index
125-
:since: 0.7.0
126-
"""
127-
128-
return Index()
129-
130-
def generate_manifest(
121+
def generate_image_manifest(
131122
self,
132123
cname: str,
133124
architecture: Optional[str] = None,
@@ -145,7 +136,7 @@ def generate_manifest(
145136
:param feature_set: The expanded list of the included features of this manifest
146137
147138
:return: (object) OCI image manifest
148-
:since: 0.7.0
139+
:since: 0.9.2
149140
"""
150141

151142
cname_object = CName(cname, architecture, version)
@@ -162,15 +153,13 @@ def generate_manifest(
162153
if commit is None:
163154
commit = ""
164155

165-
manifest = Manifest()
156+
manifest = ImageManifest()
166157

167-
manifest["annotations"] = {}
168-
manifest["annotations"]["version"] = version
169-
manifest["annotations"]["cname"] = cname
170-
manifest["annotations"]["architecture"] = architecture
171-
manifest["annotations"]["feature_set"] = feature_set
172-
manifest["annotations"]["flavor"] = f"{cname_object.flavor}-{architecture}"
173-
manifest["annotations"]["commit"] = commit
158+
manifest.version = version
159+
manifest.cname = cname
160+
manifest.arch = architecture
161+
manifest.feature_set = feature_set
162+
manifest.commit = commit
174163

175164
description = (
176165
f"Image: {cname} "
@@ -189,6 +178,41 @@ def generate_manifest(
189178

190179
return manifest
191180

181+
def generate_index(self):
182+
"""
183+
Generates an OCI image index
184+
185+
:return: (object) OCI image index
186+
:since: 0.7.0
187+
"""
188+
189+
return Index()
190+
191+
def generate_manifest(
192+
self,
193+
version: Optional[str] = None,
194+
commit: Optional[str] = None,
195+
):
196+
"""
197+
Generates an OCI manifest
198+
199+
:param cname: Canonical name of the manifest
200+
:param architecture: Target architecture of the manifest
201+
:param version: Artifacts version of the manifest
202+
:param commit: The commit hash of the manifest
203+
:param feature_set: The expanded list of the included features of this manifest
204+
205+
:return: (object) OCI manifest
206+
:since: 0.9.2
207+
"""
208+
209+
manifest = Manifest()
210+
211+
manifest.version = version
212+
manifest.commit = commit
213+
214+
return manifest
215+
192216
def _get_index_without_response_parsing(self):
193217
"""
194218
Return the response of an OCI image index request.
@@ -540,23 +564,25 @@ def read_or_generate_manifest(
540564
"""
541565

542566
if cname is None:
567+
manifest_type = Manifest
543568
response = self._get_manifest_without_response_parsing(self._container_version)
544569
else:
570+
manifest_type = ImageManifest
571+
545572
if architecture is None:
546573
architecture = CName(cname, architecture, version).arch
547574

548575
response = self._get_manifest_without_response_parsing(
549576
f"{self._container_version}-{cname}-{architecture}"
550577
)
551-
#
552578

553579
if response.ok:
554-
manifest = Manifest(**response.json())
580+
manifest = manifest_type(**response.json())
555581
elif response.status_code == 404:
556582
if cname is None:
557-
manifest = Manifest()
583+
manifest = self.generate_manifest(version, commit)
558584
else:
559-
manifest = self.generate_manifest(
585+
manifest = self.generate_image_manifest(
560586
cname, architecture, version, commit, feature_set
561587
)
562588
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)