/
index.py
440 lines (377 loc) · 14 KB
/
index.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
"""
Implements validation and conversion for the OCI Index JSON.
See: https://github.com/opencontainers/image-spec/blob/master/image-index.md
Example:
{
"schemaVersion": 2,
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 7143,
"digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f",
"platform": {
"architecture": "ppc64le",
"os": "linux"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 7682,
"digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
"platform": {
"architecture": "amd64",
"os": "linux"
}
}
],
"annotations": {
"com.example.key1": "value1",
"com.example.key2": "value2"
}
}
"""
import logging
import json
from cachetools.func import lru_cache
from jsonschema import validate as validate_schema, ValidationError
from digest import digest_tools
from image.shared import ManifestException
from image.shared.interfaces import ManifestListInterface
from image.shared.schemautil import LazyManifestLoader
from image.docker.schema2 import (
DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE,
DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE,
)
from image.docker.schema2.manifest import DockerSchema2Manifest
from image.docker.schema2.list import DockerSchema2ManifestList
from image.oci import OCI_IMAGE_INDEX_CONTENT_TYPE, OCI_IMAGE_MANIFEST_CONTENT_TYPE
from image.oci.descriptor import get_descriptor_schema
from image.oci.manifest import OCIManifest
from util.bytes import Bytes
logger = logging.getLogger(__name__)
# Keys.
INDEX_VERSION_KEY = "schemaVersion"
INDEX_MEDIATYPE_KEY = "mediaType"
INDEX_SIZE_KEY = "size"
INDEX_DIGEST_KEY = "digest"
INDEX_URLS_KEY = "urls"
INDEX_MANIFESTS_KEY = "manifests"
INDEX_PLATFORM_KEY = "platform"
INDEX_ARCHITECTURE_KEY = "architecture"
INDEX_OS_KEY = "os"
INDEX_OS_VERSION_KEY = "os.version"
INDEX_OS_FEATURES_KEY = "os.features"
INDEX_FEATURES_KEY = "features"
INDEX_VARIANT_KEY = "variant"
INDEX_ANNOTATIONS_KEY = "annotations"
ALLOWED_MEDIA_TYPES = [
OCI_IMAGE_MANIFEST_CONTENT_TYPE,
OCI_IMAGE_INDEX_CONTENT_TYPE,
DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE,
DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE,
]
class MalformedIndex(ManifestException):
"""
Raised when a index fails an assertion that should be true according to the OCI Index spec.
"""
pass
class OCIIndex(ManifestListInterface):
METASCHEMA = {
"type": "object",
"properties": {
INDEX_VERSION_KEY: {
"type": "number",
"description": "The version of the index. Must always be `2`.",
"minimum": 2,
"maximum": 2,
},
INDEX_MEDIATYPE_KEY: {
"type": "string",
"description": "The media type of the index.",
"enum": [OCI_IMAGE_INDEX_CONTENT_TYPE],
},
INDEX_MANIFESTS_KEY: {
"type": "array",
"description": "The manifests field contains a list of manifests for specific platforms",
"items": get_descriptor_schema(
allowed_media_types=ALLOWED_MEDIA_TYPES,
additional_properties={
INDEX_PLATFORM_KEY: {
"type": "object",
"description": "The platform object describes the platform which the image in "
+ "the manifest runs on",
"properties": {
INDEX_ARCHITECTURE_KEY: {
"type": "string",
"description": "Specifies the CPU architecture, for example amd64 or ppc64le.",
},
INDEX_OS_KEY: {
"type": "string",
"description": "Specifies the operating system, for example linux or windows",
},
INDEX_OS_VERSION_KEY: {
"type": "string",
"description": "Specifies the operating system version, for example 10.0.10586",
},
INDEX_OS_FEATURES_KEY: {
"type": "array",
"description": "specifies an array of strings, each listing a required OS "
+ "feature (for example on Windows win32k)",
"items": {
"type": "string",
},
},
INDEX_VARIANT_KEY: {
"type": "string",
"description": "Specifies a variant of the CPU, for example armv6l to specify "
+ "a particular CPU variant of the ARM CPU",
},
INDEX_FEATURES_KEY: {
"type": "array",
"description": "specifies an array of strings, each listing a required CPU "
+ "feature (for example sse4 or aes).",
"items": {
"type": "string",
},
},
},
"required": [
INDEX_ARCHITECTURE_KEY,
INDEX_OS_KEY,
],
},
},
additional_required=[INDEX_PLATFORM_KEY],
),
},
INDEX_ANNOTATIONS_KEY: {
"type": "object",
"description": "The annotations, if any, on this index",
"additionalProperties": True,
},
},
"required": [
INDEX_VERSION_KEY,
INDEX_MANIFESTS_KEY,
],
}
def __init__(self, manifest_bytes):
assert isinstance(manifest_bytes, Bytes)
self._layers = None
self._manifest_bytes = manifest_bytes
try:
self._parsed = json.loads(manifest_bytes.as_unicode())
except ValueError as ve:
raise MalformedIndex("malformed manifest data: %s" % ve)
try:
validate_schema(self._parsed, OCIIndex.METASCHEMA)
except ValidationError as ve:
raise MalformedIndex("manifest data does not match schema: %s" % ve)
@property
def is_manifest_list(self):
"""
Returns whether this manifest is a list.
"""
return True
@property
def schema_version(self):
return 2
@property
def digest(self):
"""
The digest of the manifest, including type prefix.
"""
return digest_tools.sha256_digest(self._manifest_bytes.as_encoded_str())
@property
def media_type(self):
"""
The media type of the schema.
"""
return OCI_IMAGE_INDEX_CONTENT_TYPE
@property
def manifest_dict(self):
"""
Returns the manifest as a dictionary ready to be serialized to JSON.
"""
return self._parsed
@property
def bytes(self):
return self._manifest_bytes
@property
def filesystem_layers(self):
return None
def get_layers(self, content_retriever):
"""
Returns the layers of this manifest, from base to leaf or None if this kind of manifest does
not support layers.
"""
return None
@property
def blob_digests(self):
# Manifest lists have no blob digests, since everything is stored as a manifest.
return []
@property
def local_blob_digests(self):
return self.blob_digests
def get_blob_digests_for_translation(self):
return self.blob_digests
@property
def layers_compressed_size(self):
return None
@property
def config_media_type(self):
return None
@property
def config(self):
return None
@lru_cache(maxsize=1)
def manifests(self, content_retriever):
"""
Returns the manifests in the list.
"""
manifests = self._parsed[INDEX_MANIFESTS_KEY]
supported_types = {}
supported_types[DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE] = DockerSchema2Manifest
supported_types[DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE] = DockerSchema2ManifestList
supported_types[OCI_IMAGE_MANIFEST_CONTENT_TYPE] = OCIManifest
supported_types[OCI_IMAGE_INDEX_CONTENT_TYPE] = OCIIndex
return [
LazyManifestLoader(
m,
content_retriever,
supported_types,
INDEX_DIGEST_KEY,
INDEX_SIZE_KEY,
INDEX_MEDIATYPE_KEY,
)
for m in manifests
]
def validate(self, content_retriever):
"""
Performs validation of required assertions about the manifest.
Raises a ManifestException on failure.
"""
# Nothing to validate.
def child_manifests(self, content_retriever):
return self.manifests(content_retriever)
def child_manifest_digests(self):
return [m[INDEX_DIGEST_KEY] for m in self._parsed[INDEX_MANIFESTS_KEY]]
def get_manifest_labels(self, content_retriever):
return None
def get_leaf_layer_v1_image_id(self, content_retriever):
return None
def get_legacy_image_ids(self, content_retriever):
return None
@property
def has_legacy_image(self):
return False
@property
def amd64_linux_manifest_digest(self):
"""Returns the digest of the AMD64+Linux manifest in this list, if any, or None
if none.
"""
for manifest_ref in self._parsed[INDEX_MANIFESTS_KEY]:
platform = manifest_ref[INDEX_PLATFORM_KEY]
architecture = platform.get(INDEX_ARCHITECTURE_KEY, None)
os = platform.get(INDEX_OS_KEY, None)
if architecture == "amd64" and os == "linux":
return manifest_ref[INDEX_DIGEST_KEY]
return None
def get_requires_empty_layer_blob(self, content_retriever):
return False
def get_schema1_manifest(self, namespace_name, repo_name, tag_name, content_retriever):
"""
Returns the manifest that is compatible with V1, by virtue of being `amd64` and `linux`.
If none, returns None.
"""
legacy_manifest = self._get_legacy_manifest(content_retriever)
if legacy_manifest is None:
return None
return legacy_manifest.get_schema1_manifest(
namespace_name, repo_name, tag_name, content_retriever
)
def convert_manifest(
self, allowed_mediatypes, namespace_name, repo_name, tag_name, content_retriever
):
if self.media_type in allowed_mediatypes:
return self
legacy_manifest = self._get_legacy_manifest(content_retriever)
if legacy_manifest is None:
return None
return legacy_manifest.convert_manifest(
allowed_mediatypes, namespace_name, repo_name, tag_name, content_retriever
)
def _get_legacy_manifest(self, content_retriever):
"""
Returns the manifest under this list with architecture amd64 and os linux, if any, or None
if none or error.
"""
for manifest_ref in self.manifests(content_retriever):
platform = manifest_ref._manifest_data[INDEX_PLATFORM_KEY]
architecture = platform[INDEX_ARCHITECTURE_KEY]
os = platform[INDEX_OS_KEY]
if architecture != "amd64" or os != "linux":
continue
try:
return manifest_ref.manifest_obj
except (ManifestException, IOError):
logger.exception("Could not load child manifest")
return None
return None
def unsigned(self):
return self
def generate_legacy_layers(self, images_map, content_retriever):
return None
class OCIIndexBuilder(object):
"""
A convenient abstraction around creating new OCIIndex's.
"""
def __init__(self):
self.manifests = []
def add_manifest(self, manifest, architecture, os):
"""
Adds a manifest to the list.
"""
assert manifest.media_type in ALLOWED_MEDIA_TYPES
self.add_manifest_digest(
manifest.digest,
len(manifest.bytes.as_encoded_str()),
manifest.media_type,
architecture,
os,
)
def add_manifest_digest(self, manifest_digest, manifest_size, media_type, architecture, os):
"""
Adds a manifest to the list.
"""
self.manifests.append(
(
manifest_digest,
manifest_size,
media_type,
{
INDEX_ARCHITECTURE_KEY: architecture,
INDEX_OS_KEY: os,
},
)
)
def build(self):
"""
Builds and returns the DockerSchema2ManifestList.
"""
assert self.manifests
manifest_list_dict = {
INDEX_VERSION_KEY: 2,
INDEX_MEDIATYPE_KEY: OCI_IMAGE_INDEX_CONTENT_TYPE,
INDEX_MANIFESTS_KEY: [
{
INDEX_MEDIATYPE_KEY: manifest[2],
INDEX_DIGEST_KEY: manifest[0],
INDEX_SIZE_KEY: manifest[1],
INDEX_PLATFORM_KEY: manifest[3],
}
for manifest in self.manifests
],
}
json_str = Bytes.for_string_or_unicode(json.dumps(manifest_list_dict, indent=3))
return OCIIndex(json_str)