Skip to content

Commit d07b95e

Browse files
committed
Update migrations 39-45 to perform the manifest data migration for new fields
1 parent 0bf4a2e commit d07b95e

4 files changed

Lines changed: 249 additions & 12 deletions

File tree

pulp_container/app/migrations/0039_manifest_data.py

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,60 @@
11
# Generated by Django 4.2.10 on 2024-03-05 11:22
2+
import json
23
import warnings
34

45
from django.db import migrations, models
56

7+
from pulp_container.constants import (
8+
MEDIA_TYPE,
9+
)
610

7-
def print_warning_for_initializing_manifest_data(apps, schema_editor):
8-
warnings.warn(
9-
"Run 'pulpcore-manager container-handle-image-data' to move the manifests' "
10-
"data from artifacts to the new 'data' database field."
11-
)
11+
def get_content_data(artifact):
12+
with artifact.file.storage.open(artifact.file.name, mode="rb") as file:
13+
raw_data = file.read()
14+
content_data = json.loads(raw_data)
15+
return content_data, raw_data
1216

17+
def migrate_manifest_data(apps, schema_editor):
18+
"""
19+
Migrate the backing artifact manifest to the new 'data' field.
20+
Also, initialize the 'annotations' and 'labels' fields.
21+
22+
'is_bootable' and 'is_flatpak' fields are not initialized since they require annotations and
23+
labels to be initialized first.
24+
"""
25+
Manifest = apps.get_model("container", "Manifest")
26+
to_update = []
27+
manifests = Manifest.objects.filter(
28+
contentartifact__artifact__isnull=False
29+
).distinct().prefetch_related("_artifacts").select_related("config_blob")
30+
for manifest in manifests.iterator():
31+
manifest_artifact = manifest._artifacts.get()
32+
manifest_data, raw_bytes_data = get_content_data(manifest_artifact)
33+
manifest.data = raw_bytes_data.decode("utf-8")
34+
if not manifest.annotations:
35+
init_annotations(manifest, manifest_data)
36+
if not manifest.labels:
37+
init_labels(manifest)
38+
39+
to_update.append(manifest)
40+
if len(to_update) > 1000:
41+
Manifest.objects.bulk_update(to_update, ["data", "annotations", "labels"])
42+
to_update.clear()
43+
if to_update:
44+
Manifest.objects.bulk_update(to_update, ["data", "annotations", "labels"])
45+
46+
def init_annotations(manifest, manifest_data):
47+
# annotations are part of OCI only
48+
if manifest.media_type not in (MEDIA_TYPE.MANIFEST_OCI, MEDIA_TYPE.INDEX_OCI):
49+
return False
50+
51+
manifest.annotations = manifest_data.get("annotations", {})
52+
53+
def init_labels(manifest):
54+
if manifest.config_blob:
55+
config_artifact = manifest.config_blob._artifacts.get()
56+
config_data, _ = get_content_data(config_artifact)
57+
manifest.labels = config_data.get("config", {}).get("Labels") or {}
1358

1459
class Migration(migrations.Migration):
1560

@@ -24,7 +69,7 @@ class Migration(migrations.Migration):
2469
field=models.TextField(null=True),
2570
),
2671
migrations.RunPython(
27-
print_warning_for_initializing_manifest_data,
72+
migrate_manifest_data,
2873
reverse_code=migrations.RunPython.noop,
2974
elidable=True,
3075
),

pulp_container/app/migrations/0042_add_manifest_nature_field.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,142 @@
11
# Generated by Django 4.2.16 on 2024-10-21 19:14
22

3+
import json
34
from django.db import migrations, models
45

6+
from pulp_container.constants import MEDIA_TYPE, MANIFEST_TYPE, MANIFEST_MEDIA_TYPES, COSIGN_MEDIA_TYPES, COSIGN_MEDIA_TYPES_MANIFEST_TYPE_MAPPING
7+
8+
9+
def migrate_manifest_nature(apps, schema_editor):
10+
"""
11+
Populate the `type` field, and also the `is_bootable` and `is_flatpak` fields even though they are deprecated.
12+
"""
13+
Manifest = apps.get_model("container", "Manifest")
14+
to_update = []
15+
# First, update the manifests that are not manifest lists or index manifests.
16+
manifests = Manifest.objects.filter(data__isnull=False).exclude(media_type__in=[MEDIA_TYPE.MANIFEST_LIST, MEDIA_TYPE.INDEX_OCI])
17+
for manifest in manifests.iterator():
18+
if init_manifest_nature(manifest):
19+
to_update.append(manifest)
20+
if len(to_update) > 1000:
21+
Manifest.objects.bulk_update(to_update, ["type", "is_bootable", "is_flatpak"])
22+
to_update.clear()
23+
if to_update:
24+
Manifest.objects.bulk_update(to_update, ["type", "is_bootable", "is_flatpak"])
25+
to_update.clear()
26+
# Then, update the manifest lists and index manifests.
27+
manifest_lists = Manifest.objects.filter(
28+
data__isnull=False, media_type__in=[MEDIA_TYPE.MANIFEST_LIST, MEDIA_TYPE.INDEX_OCI]
29+
).prefetch_related("listed_manifests")
30+
for manifest_list in manifest_lists.iterator():
31+
if init_manifest_list_nature(manifest_list):
32+
to_update.append(manifest_list)
33+
if len(to_update) > 1000:
34+
Manifest.objects.bulk_update(to_update, ["type", "is_bootable", "is_flatpak"])
35+
to_update.clear()
36+
if to_update:
37+
Manifest.objects.bulk_update(to_update, ["type", "is_bootable", "is_flatpak"])
38+
to_update.clear()
39+
40+
def init_image_nature(manifest):
41+
manifest.json_manifest = json.loads(manifest.data)
42+
if manifest.media_type in [MEDIA_TYPE.INDEX_OCI, MEDIA_TYPE.MANIFEST_LIST]:
43+
return manifest.init_manifest_list_nature()
44+
else:
45+
return manifest.init_manifest_nature()
46+
47+
def init_manifest_list_nature(manifest_list):
48+
updated_type = False
49+
if not manifest_list.type:
50+
manifest_list.type = MANIFEST_TYPE.INDEX
51+
updated_type = True
52+
53+
for manifest in manifest_list.listed_manifests.all():
54+
# it suffices just to have a single manifest of a specific nature;
55+
# there is no case where the manifest is both bootable and flatpak-based
56+
if manifest.type == MANIFEST_TYPE.BOOTABLE:
57+
manifest_list.is_bootable = True
58+
return True
59+
elif manifest.type == MANIFEST_TYPE.FLATPAK:
60+
manifest_list.is_flatpak = True
61+
return True
62+
63+
return updated_type
64+
65+
def init_manifest_nature(manifest):
66+
if is_bootable_image(manifest):
67+
# DEPRECATED: is_bootable is deprecated and will be removed in a future release.
68+
manifest.is_bootable = True
69+
manifest.type = MANIFEST_TYPE.BOOTABLE
70+
return True
71+
elif is_flatpak_image(manifest):
72+
# DEPRECATED: is_flatpak is deprecated and will be removed in a future release.
73+
manifest.is_flatpak = True
74+
manifest.type = MANIFEST_TYPE.FLATPAK
75+
return True
76+
elif is_helm_chart(manifest):
77+
manifest.type = MANIFEST_TYPE.HELM
78+
return True
79+
elif media_type := is_cosign(manifest):
80+
manifest.type = get_cosign_type(media_type)
81+
return True
82+
elif is_artifact(manifest):
83+
manifest.type = MANIFEST_TYPE.ARTIFACT
84+
return True
85+
elif is_manifest_image(manifest):
86+
manifest.type = MANIFEST_TYPE.IMAGE
87+
return True
88+
89+
return False
90+
91+
def is_bootable_image(manifest):
92+
return (
93+
manifest.annotations.get("containers.bootc") == "1"
94+
or manifest.labels.get("containers.bootc") == "1"
95+
)
96+
97+
def is_flatpak_image(manifest):
98+
return True if manifest.labels.get("org.flatpak.ref") else False
99+
100+
def is_manifest_image(manifest):
101+
return manifest.media_type in MANIFEST_MEDIA_TYPES.IMAGE
102+
103+
def is_cosign(manifest):
104+
try:
105+
# layers is not a mandatory field
106+
layers = manifest.json_manifest["layers"]
107+
except KeyError:
108+
return False
109+
110+
for layer in layers:
111+
if layer["mediaType"] in COSIGN_MEDIA_TYPES:
112+
return layer["mediaType"]
113+
return False
114+
115+
def get_cosign_type(media_type):
116+
if media_type in MEDIA_TYPE.COSIGN_SBOM:
117+
return MANIFEST_TYPE.COSIGN_SBOM
118+
return COSIGN_MEDIA_TYPES_MANIFEST_TYPE_MAPPING.get(media_type, MANIFEST_TYPE.UNKNOWN)
119+
120+
def is_helm_chart(manifest):
121+
try:
122+
return manifest.json_manifest["config"]["mediaType"] == MEDIA_TYPE.CONFIG_BLOB_HELM
123+
except KeyError:
124+
return False
125+
126+
def is_artifact(manifest):
127+
# artifact is valid only for OCI spec
128+
if manifest.media_type != MEDIA_TYPE.MANIFEST_OCI:
129+
return False
130+
131+
if manifest.json_manifest.get("artifactType", None):
132+
return True
133+
134+
manifest_config_media_type = manifest.json_manifest["config"]["mediaType"]
135+
return (
136+
manifest_config_media_type == MEDIA_TYPE.OCI_EMPTY_JSON
137+
or manifest_config_media_type not in vars(MEDIA_TYPE).values()
138+
)
139+
5140

6141
class Migration(migrations.Migration):
7142

pulp_container/app/migrations/0043_add_os_arch_image_size_manifest_fields.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,5 @@ class Migration(migrations.Migration):
4141
name='os',
4242
field=models.TextField(blank=True, default=''),
4343
),
44-
migrations.RunPython(
45-
print_warning_for_updating_manifest_fields,
46-
reverse_code=migrations.RunPython.noop,
47-
elidable=True,
48-
),
44+
# Will update the os, architecture, and compressed_image_size in migration 45
4945
]

pulp_container/app/migrations/0045_alter_manifest_compressed_image_size.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,63 @@
11
# Generated by Django 4.2.16 on 2025-03-06 22:37
2-
2+
import json
33
from django.db import migrations, models
44

5+
from pulp_container.constants import MEDIA_TYPE
6+
7+
def get_content_data(artifact):
8+
with artifact.file.storage.open(artifact.file.name, mode="rb") as file:
9+
raw_data = file.read()
10+
content_data = json.loads(raw_data)
11+
return content_data, raw_data
12+
13+
def migrate_os_arch_image_size(apps, schema_editor):
14+
"""
15+
Populate the os, architecture, and compressed_image_size fields for the manifests.
16+
"""
17+
Manifest = apps.get_model("container", "Manifest")
18+
to_update = []
19+
manifests = Manifest.objects.filter(data__isnull=False).select_related("config_blob")
20+
for manifest in manifests.iterator():
21+
if needs_os_arch_size_update(manifest):
22+
manifest.json_manifest = json.loads(manifest.data)
23+
init_architecture_and_os(manifest)
24+
init_compressed_image_size(manifest)
25+
to_update.append(manifest)
26+
27+
if len(to_update) > 1000:
28+
Manifest.objects.bulk_update(to_update, ["os", "architecture", "compressed_image_size"])
29+
to_update.clear()
30+
if to_update:
31+
Manifest.objects.bulk_update(to_update, ["os", "architecture", "compressed_image_size"])
32+
33+
def needs_os_arch_size_update(manifest):
34+
return manifest.media_type not in [MEDIA_TYPE.MANIFEST_LIST, MEDIA_TYPE.INDEX_OCI] and not (
35+
manifest.architecture or manifest.os or manifest.compressed_image_size
36+
)
37+
38+
def init_architecture_and_os(manifest):
39+
# schema1 has the architecture/os definition in the Manifest (not in the ConfigBlob)
40+
# and none of these fields are required
41+
if manifest.json_manifest.get("architecture", None) or manifest.json_manifest.get("os", None):
42+
manifest.architecture = manifest.json_manifest.get("architecture", None)
43+
manifest.os = manifest.json_manifest.get("os", None)
44+
elif manifest.config_blob:
45+
config_artifact = manifest.config_blob._artifacts.get()
46+
config_data, _ = get_content_data(config_artifact)
47+
manifest.architecture = config_data.get("architecture", None)
48+
manifest.os = config_data.get("os", None)
49+
50+
def init_compressed_image_size(manifest):
51+
# manifestv2 schema1 has only blobSum definition for each layer
52+
if manifest.json_manifest.get("fsLayers", None):
53+
manifest.compressed_image_size = 0
54+
return
55+
56+
layers = manifest.json_manifest.get("layers")
57+
compressed_size = 0
58+
for layer in layers:
59+
compressed_size += layer.get("size")
60+
manifest.compressed_image_size = compressed_size
561

662
class Migration(migrations.Migration):
763

@@ -15,4 +71,9 @@ class Migration(migrations.Migration):
1571
name="compressed_image_size",
1672
field=models.BigIntegerField(null=True),
1773
),
74+
migrations.RunPython(
75+
migrate_os_arch_image_size,
76+
reverse_code=migrations.RunPython.noop,
77+
elidable=True,
78+
),
1879
]

0 commit comments

Comments
 (0)