Skip to content

Commit 16f5855

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

4 files changed

Lines changed: 256 additions & 12 deletions

File tree

pulp_container/app/migrations/0039_manifest_data.py

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,61 @@
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
6+
from django.core.files.storage import default_storage
57

8+
from pulp_container.constants import (
9+
MEDIA_TYPE,
10+
)
611

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-
)
12+
def get_content_data(artifact):
13+
with default_storage.open(artifact.file.name, mode="rb") as file:
14+
raw_data = file.read()
15+
content_data = json.loads(raw_data)
16+
return content_data, raw_data
1217

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

1460
class Migration(migrations.Migration):
1561

@@ -24,7 +70,7 @@ class Migration(migrations.Migration):
2470
field=models.TextField(null=True),
2571
),
2672
migrations.RunPython(
27-
print_warning_for_initializing_manifest_data,
73+
migrate_manifest_data,
2874
reverse_code=migrations.RunPython.noop,
2975
elidable=True,
3076
),

pulp_container/app/migrations/0042_add_manifest_nature_field.py

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

6136
class Migration(migrations.Migration):
7137

@@ -15,4 +145,5 @@ class Migration(migrations.Migration):
15145
name='type',
16146
field=models.CharField(null=True),
17147
),
148+
migrations.RunPython(migrate_manifest_nature, reverse_code=migrations.RunPython.noop, elidable=True),
18149
]

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: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,73 @@
11
# Generated by Django 4.2.16 on 2025-03-06 22:37
2-
2+
import json
3+
import warnings
34
from django.db import migrations, models
5+
from django.core.files.storage import default_storage
6+
7+
from pulp_container.constants import MEDIA_TYPE
8+
9+
def get_content_data(artifact):
10+
with default_storage.open(artifact.file.name, mode="rb") as file:
11+
raw_data = file.read()
12+
content_data = json.loads(raw_data)
13+
return content_data, raw_data
14+
15+
def migrate_os_arch_image_size(apps, schema_editor):
16+
"""
17+
Populate the os, architecture, and compressed_image_size fields for the manifests.
18+
19+
This migration can only handle manifests within the default domain.
20+
"""
21+
Manifest = apps.get_model("container", "Manifest")
22+
to_update = []
23+
manifests = Manifest.objects.filter(data__isnull=False, pulp_domain__name="default").select_related("config_blob")
24+
for manifest in manifests.iterator():
25+
if needs_os_arch_size_update(manifest):
26+
manifest.json_manifest = json.loads(manifest.data)
27+
init_architecture_and_os(manifest)
28+
init_compressed_image_size(manifest)
29+
to_update.append(manifest)
30+
31+
if len(to_update) > 1000:
32+
Manifest.objects.bulk_update(to_update, ["os", "architecture", "compressed_image_size"])
33+
to_update.clear()
34+
if to_update:
35+
Manifest.objects.bulk_update(to_update, ["os", "architecture", "compressed_image_size"])
436

37+
if Manifest.objects.exclude(pulp_domain__name="default").exists():
38+
warnings.warn(
39+
"Run 'pulpcore-manager container-handle-image-data' to update the manifests' "
40+
"os, architecture, and compressed_image_size fields."
41+
)
42+
43+
def needs_os_arch_size_update(manifest):
44+
return manifest.media_type not in [MEDIA_TYPE.MANIFEST_LIST, MEDIA_TYPE.INDEX_OCI] and not (
45+
manifest.architecture or manifest.os or manifest.compressed_image_size
46+
)
47+
48+
def init_architecture_and_os(manifest):
49+
# schema1 has the architecture/os definition in the Manifest (not in the ConfigBlob)
50+
# and none of these fields are required
51+
if manifest.json_manifest.get("architecture", None) or manifest.json_manifest.get("os", None):
52+
manifest.architecture = manifest.json_manifest.get("architecture", None)
53+
manifest.os = manifest.json_manifest.get("os", None)
54+
elif manifest.config_blob:
55+
config_artifact = manifest.config_blob._artifacts.get()
56+
config_data, _ = get_content_data(config_artifact)
57+
manifest.architecture = config_data.get("architecture", None)
58+
manifest.os = config_data.get("os", None)
59+
60+
def init_compressed_image_size(manifest):
61+
# manifestv2 schema1 has only blobSum definition for each layer
62+
if manifest.json_manifest.get("fsLayers", None):
63+
manifest.compressed_image_size = 0
64+
return
65+
66+
layers = manifest.json_manifest.get("layers")
67+
compressed_size = 0
68+
for layer in layers:
69+
compressed_size += layer.get("size")
70+
manifest.compressed_image_size = compressed_size
571

672
class Migration(migrations.Migration):
773

@@ -15,4 +81,9 @@ class Migration(migrations.Migration):
1581
name="compressed_image_size",
1682
field=models.BigIntegerField(null=True),
1783
),
84+
migrations.RunPython(
85+
migrate_os_arch_image_size,
86+
reverse_code=migrations.RunPython.noop,
87+
elidable=True,
88+
),
1889
]

0 commit comments

Comments
 (0)