Skip to content

Commit 7031daa

Browse files
committed
Reuse and extend standardized OCI annotations
Signed-off-by: Tobias Wolf <[email protected]> On-behalf-of: SAP <[email protected]>
1 parent ca8aaa9 commit 7031daa

File tree

6 files changed

+184
-75
lines changed

6 files changed

+184
-75
lines changed

src/gardenlinux/oci/container.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ def push_index_for_tags(self, index: Index, tags: List[str]) -> None:
319319
:since: 0.7.0
320320
"""
321321

322-
# For each additional tag, push the manifest using Registry.upload_manifest
322+
# For each additional tag, push the index using Registry.upload_index
323323
for tag in tags:
324324
self._check_200_response(self._upload_index(index, tag))
325325

@@ -626,9 +626,22 @@ def _upload_index(self, index: Index, reference: Optional[str] = None) -> Respon
626626
f"{self.prefix}://{self.hostname}/v2/{self._container_name}/manifests/{reference}",
627627
"PUT",
628628
headers={"Content-Type": OCI_IMAGE_INDEX_MEDIA_TYPE},
629-
data=index.json,
629+
json=index.extended_dict,
630630
)
631631

632+
def upload_manifest(self, manifest: Manifest, container: OrasContainer) -> Response:
633+
"""
634+
oras-project.github.io: Read a manifest file and upload it.
635+
636+
:param manifest: manifest to upload
637+
:param container: parsed container URI
638+
639+
:return: (object) OCI manifest put response
640+
:since: 1.0.0
641+
"""
642+
643+
return Registry.upload_manifest(self, manifest.extended_dict, container) # type: ignore[no-any-return]
644+
632645
@staticmethod
633646
def get_artifacts_metadata_from_files(
634647
files: List[str], arch: str

src/gardenlinux/oci/image_manifest.py

Lines changed: 101 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
from pathlib import Path
77
from typing import Any, Dict
88

9-
from oras.oci import Layer
9+
from oras.defaults import annotation_title as ANNOTATION_TITLE
1010

11+
from ..constants import GL_DISTRIBUTION_NAME, GL_REPOSITORY_URL
1112
from ..features import CName
13+
from .layer import Layer
1214
from .manifest import Manifest
1315
from .platform import NewPlatform
1416
from .schemas import EmptyManifestMetadata
@@ -27,6 +29,31 @@ class ImageManifest(Manifest):
2729
Apache License, Version 2.0
2830
"""
2931

32+
ANNOTATION_ARCH_KEY = "org.opencontainers.image.architecture"
33+
"""
34+
OCI image manifest architecture annotation
35+
"""
36+
ANNOTATION_CNAME_KEY = "cname"
37+
"""
38+
OCI image manifest GardenLinux canonical name annotation
39+
"""
40+
ANNOTATION_DESCRIPTION_KEY = "org.opencontainers.image.description"
41+
"""
42+
OCI image manifest description annotation
43+
"""
44+
ANNOTATION_FEATURE_SET_KEY = "feature_set"
45+
"""
46+
OCI image manifest GardenLinux feature set annotation
47+
"""
48+
ANNOTATION_SOURCE_REPO_KEY = "org.opencontainers.image.source"
49+
"""
50+
OCI image manifest GardenLinux source repository URL annotation
51+
"""
52+
ANNOTATION_TITLE_KEY = ANNOTATION_TITLE
53+
"""
54+
OCI image manifest title annotation
55+
"""
56+
3057
@property
3158
def arch(self) -> str:
3259
"""
@@ -36,12 +63,12 @@ def arch(self) -> str:
3663
:since: 0.7.0
3764
"""
3865

39-
if "architecture" not in self.get("annotations", {}):
66+
if ImageManifest.ANNOTATION_ARCH_KEY not in self.get("annotations", {}):
4067
raise RuntimeError(
41-
"Unexpected manifest with missing config annotation 'architecture' found"
68+
f"Unexpected manifest with missing config annotation '{ImageManifest.ANNOTATION_ARCH_KEY}' found"
4269
)
4370

44-
return self["annotations"]["architecture"] # type: ignore[no-any-return]
71+
return self["annotations"][ImageManifest.ANNOTATION_ARCH_KEY] # type: ignore[no-any-return]
4572

4673
@arch.setter
4774
def arch(self, value: str) -> None:
@@ -54,7 +81,7 @@ def arch(self, value: str) -> None:
5481
"""
5582

5683
self._ensure_annotations_dict()
57-
self["annotations"]["architecture"] = value
84+
self["annotations"][ImageManifest.ANNOTATION_ARCH_KEY] = value
5885

5986
@property
6087
def cname(self) -> str:
@@ -65,12 +92,12 @@ def cname(self) -> str:
6592
:since: 0.7.0
6693
"""
6794

68-
if "cname" not in self.get("annotations", {}):
95+
if ImageManifest.ANNOTATION_CNAME_KEY not in self.get("annotations", {}):
6996
raise RuntimeError(
70-
"Unexpected manifest with missing config annotation 'cname' found"
97+
f"Unexpected manifest with missing config annotation '{ImageManifest.ANNOTATION_CNAME_KEY}' found"
7198
)
7299

73-
return self["annotations"]["cname"] # type: ignore[no-any-return]
100+
return self["annotations"][ImageManifest.ANNOTATION_CNAME_KEY] # type: ignore[no-any-return]
74101

75102
@cname.setter
76103
def cname(self, value: str) -> None:
@@ -83,7 +110,7 @@ def cname(self, value: str) -> None:
83110
"""
84111

85112
self._ensure_annotations_dict()
86-
self["annotations"]["cname"] = value
113+
self["annotations"][ImageManifest.ANNOTATION_CNAME_KEY] = value
87114

88115
@property
89116
def feature_set(self) -> str:
@@ -94,12 +121,12 @@ def feature_set(self) -> str:
94121
:since: 0.7.0
95122
"""
96123

97-
if "feature_set" not in self.get("annotations", {}):
124+
if ImageManifest.ANNOTATION_FEATURE_SET_KEY not in self.get("annotations", {}):
98125
raise RuntimeError(
99-
"Unexpected manifest with missing config annotation 'feature_set' found"
126+
f"Unexpected manifest with missing config annotation '{ImageManifest.ANNOTATION_FEATURE_SET_KEY}' found"
100127
)
101128

102-
return self["annotations"]["feature_set"] # type: ignore[no-any-return]
129+
return self["annotations"][ImageManifest.ANNOTATION_FEATURE_SET_KEY] # type: ignore[no-any-return]
103130

104131
@feature_set.setter
105132
def feature_set(self, value: str) -> None:
@@ -112,7 +139,7 @@ def feature_set(self, value: str) -> None:
112139
"""
113140

114141
self._ensure_annotations_dict()
115-
self["annotations"]["feature_set"] = value
142+
self["annotations"][ImageManifest.ANNOTATION_FEATURE_SET_KEY] = value
116143

117144
@property
118145
def flavor(self) -> str:
@@ -126,56 +153,75 @@ def flavor(self) -> str:
126153
return CName(self.cname).flavor
127154

128155
@property
129-
def layers_as_dict(self) -> Dict[str, Any]:
156+
def extended_dict(self) -> Dict[str, Any]:
130157
"""
131-
Returns the OCI image manifest layers as a dictionary.
158+
Returns the final parsed and extended OCI manifest dictionary
132159
133-
:return: (dict) OCI image manifest layers with title as key
134-
:since: 0.7.0
160+
:return: (dict) OCI manifest dictionary
161+
:since: 1.0.0
135162
"""
136163

137-
layers = {}
164+
manifest_dict = Manifest(self).extended_dict
165+
manifest_annotations = manifest_dict["annotations"]
138166

139-
for layer in self["layers"]:
140-
if "org.opencontainers.image.title" not in layer.get("annotations", {}):
141-
raise RuntimeError(
142-
"Unexpected layer with missing annotation 'org.opencontainers.image.title' found"
143-
)
167+
if ImageManifest.ANNOTATION_TITLE_KEY not in manifest_annotations:
168+
manifest_annotations[ImageManifest.ANNOTATION_TITLE_KEY] = (
169+
GL_DISTRIBUTION_NAME
170+
)
144171

145-
layers[layer["annotations"]["org.opencontainers.image.title"]] = layer
172+
manifest_description = manifest_annotations[ImageManifest.ANNOTATION_TITLE_KEY]
146173

147-
return layers
174+
if ImageManifest.ANNOTATION_SOURCE_REPO_KEY not in manifest_annotations:
175+
manifest_annotations[ImageManifest.ANNOTATION_SOURCE_REPO_KEY] = (
176+
GL_REPOSITORY_URL
177+
)
148178

149-
@property
150-
def version(self) -> str:
151-
"""
152-
Returns the GardenLinux version of the OCI image manifest.
179+
if ImageManifest.ANNOTATION_ARCH_KEY in manifest_annotations:
180+
manifest_annotations["architecture"] = self.arch
181+
manifest_description += f" ({self.arch})"
153182

154-
:return: (str) OCI image GardenLinux version
155-
:since: 0.7.0
156-
"""
183+
if ImageManifest.ANNOTATION_VERSION_KEY in manifest_annotations:
184+
manifest_annotations["org.opencontainers.image.version"] = self.version
185+
manifest_description += " " + self.version
157186

158-
if "version" not in self.get("annotations", {}):
159-
raise RuntimeError(
160-
"Unexpected manifest with missing config annotation 'version' found"
187+
if ImageManifest.ANNOTATION_COMMIT_KEY in manifest_annotations:
188+
manifest_annotations["org.opencontainers.image.revision"] = self.commit
189+
manifest_description += f" ({self.commit})"
190+
191+
if ImageManifest.ANNOTATION_FEATURE_SET_KEY in manifest_annotations:
192+
manifest_description += (
193+
" - " + manifest_annotations[ImageManifest.ANNOTATION_FEATURE_SET_KEY]
194+
)
195+
196+
if ImageManifest.ANNOTATION_DESCRIPTION_KEY not in manifest_annotations:
197+
manifest_annotations[ImageManifest.ANNOTATION_DESCRIPTION_KEY] = (
198+
manifest_description
161199
)
162200

163-
return self["annotations"]["version"] # type: ignore[no-any-return]
201+
return manifest_dict
164202

165-
@version.setter
166-
def version(self, value: str) -> None:
203+
@property
204+
def layers_as_dict(self) -> Dict[str, Any]:
167205
"""
168-
Sets the GardenLinux version of the OCI image manifest.
169-
170-
:param value: OCI image GardenLinux version
206+
Returns the OCI image manifest layers as a dictionary.
171207
172-
:since: 0.7.0
208+
:return: (dict) OCI image manifest layers with title as key
209+
:since: 0.7.0
173210
"""
174211

175-
self._ensure_annotations_dict()
176-
self["annotations"]["version"] = value
212+
layers = {}
213+
214+
for layer in self["layers"]:
215+
if ImageManifest.ANNOTATION_TITLE_KEY not in layer.get("annotations", {}):
216+
raise RuntimeError(
217+
f"Unexpected layer with missing annotation '{ImageManifest.ANNOTATION_TITLE_KEY}' found"
218+
)
219+
220+
layers[layer["annotations"][ImageManifest.ANNOTATION_TITLE_KEY]] = layer
177221

178-
def append_layer(self, layer: Layer) -> None:
222+
return layers
223+
224+
def append_layer(self, layer: Layer | Dict[str, Any]) -> None:
179225
"""
180226
Appends the given OCI image manifest layer to the manifest
181227
@@ -184,30 +230,30 @@ def append_layer(self, layer: Layer) -> None:
184230
:since: 0.7.0
185231
"""
186232

187-
if not isinstance(layer, Layer):
188-
raise RuntimeError("Unexpected layer type given")
189-
190-
layer_dict = layer.dict
233+
if isinstance(layer, Layer):
234+
layer_dict = layer.dict
235+
else:
236+
layer_dict = layer
191237

192-
if "org.opencontainers.image.title" not in layer_dict.get("annotations", {}):
238+
if ImageManifest.ANNOTATION_TITLE_KEY not in layer_dict.get("annotations", {}):
193239
raise RuntimeError(
194-
"Unexpected layer with missing annotation 'org.opencontainers.image.title' found"
240+
f"Unexpected layer with missing annotation '{ImageManifest.ANNOTATION_TITLE_KEY}' found"
195241
)
196242

197-
image_title = layer_dict["annotations"]["org.opencontainers.image.title"]
243+
image_title = layer_dict["annotations"][ImageManifest.ANNOTATION_TITLE_KEY]
198244
existing_layer_index = 0
199245

200246
for existing_layer in self["layers"]:
201-
if "org.opencontainers.image.title" not in existing_layer.get(
247+
if ImageManifest.ANNOTATION_TITLE_KEY not in existing_layer.get(
202248
"annotations", {}
203249
):
204250
raise RuntimeError(
205-
"Unexpected layer with missing annotation 'org.opencontainers.image.title' found"
251+
f"Unexpected layer with missing annotation '{ImageManifest.ANNOTATION_TITLE_KEY}' found"
206252
)
207253

208254
if (
209255
image_title
210-
== existing_layer["annotations"]["org.opencontainers.image.title"]
256+
== existing_layer["annotations"][ImageManifest.ANNOTATION_TITLE_KEY]
211257
):
212258
break
213259

src/gardenlinux/oci/index.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,17 @@ def __init__(self, *args: Any, **kwargs: Any):
3333
self.update(*args)
3434
self.update(**kwargs)
3535

36+
@property
37+
def extended_dict(self) -> Dict[str, Any]:
38+
"""
39+
Returns the final parsed and extended OCI image index dictionary
40+
41+
:return: (dict) OCI image index dictionary
42+
:since: 1.0.0
43+
"""
44+
45+
return self.copy()
46+
3647
@property
3748
def json(self) -> bytes:
3849
"""
@@ -42,7 +53,7 @@ def json(self) -> bytes:
4253
:since: 0.7.0
4354
"""
4455

45-
return json.dumps(self).encode("utf-8")
56+
return json.dumps(self.extended_dict).encode("utf-8")
4657

4758
@property
4859
def manifests_as_dict(self) -> Dict[str, Dict[str, Any]]:

src/gardenlinux/oci/layer.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from pathlib import Path
66
from typing import Any, Dict, Iterator, Optional
77

8-
from oras.defaults import annotation_title as ANNOTATION_TITLE
98
from oras.oci import Layer as _Layer
109

1110
from ..constants import GL_MEDIA_TYPE_LOOKUP, GL_MEDIA_TYPES
@@ -26,6 +25,15 @@ class Layer(_Layer, Mapping): # type: ignore[misc, type-arg]
2625
Apache License, Version 2.0
2726
"""
2827

28+
ANNOTATION_ARCH_KEY = "io.gardenlinux.image.layer.architecture"
29+
"""
30+
OCI image layer architecture annotation
31+
"""
32+
ANNOTATION_TITLE_KEY = "org.opencontainers.image.title"
33+
"""
34+
OCI image layer title annotation
35+
"""
36+
2937
def __init__(
3038
self,
3139
blob_path: PathLike[str] | str,
@@ -48,7 +56,7 @@ def __init__(
4856
_Layer.__init__(self, blob_path, media_type, is_dir)
4957

5058
self._annotations = {
51-
ANNOTATION_TITLE: blob_path.name, # type: ignore[attr-defined]
59+
Layer.ANNOTATION_TITLE_KEY: blob_path.name, # type: ignore[attr-defined]
5260
}
5361

5462
@property
@@ -157,7 +165,7 @@ def generate_metadata_from_file_name(
157165
return {
158166
"file_name": file_name.name, # type: ignore[attr-defined]
159167
"media_type": media_type,
160-
"annotations": {"io.gardenlinux.image.layer.architecture": arch},
168+
"annotations": {Layer.ANNOTATION_ARCH_KEY: arch},
161169
}
162170

163171
@staticmethod

0 commit comments

Comments
 (0)