Skip to content

Commit cd17b0b

Browse files
septatrixDaanDeMeyer
authored andcommitted
Implement creation of OCI images
1 parent 248dfb4 commit cd17b0b

File tree

4 files changed

+152
-4
lines changed

4 files changed

+152
-4
lines changed

mkosi/__init__.py

+108
Original file line numberDiff line numberDiff line change
@@ -3260,6 +3260,103 @@ def make_disk(
32603260
return make_image(context, msg=msg, skip=skip, split=split, tabs=tabs, root=context.root, definitions=definitions)
32613261

32623262

3263+
def make_oci(context: Context, root_layer: Path, dst: Path) -> None:
3264+
ca_store = dst / "blobs" / "sha256"
3265+
with umask(~0o755):
3266+
ca_store.mkdir(parents=True)
3267+
3268+
layer_diff_digest = hash_file(root_layer)
3269+
maybe_compress(
3270+
context,
3271+
context.config.compress_output,
3272+
context.staging / "rootfs.layer",
3273+
# Pass explicit destination to suppress adding an extension
3274+
context.staging / "rootfs.layer",
3275+
)
3276+
layer_digest = hash_file(root_layer)
3277+
root_layer.rename(ca_store / layer_digest)
3278+
3279+
creation_time = (
3280+
datetime.datetime.fromtimestamp(context.config.source_date_epoch, tz=datetime.timezone.utc)
3281+
if context.config.source_date_epoch
3282+
else datetime.datetime.now(tz=datetime.timezone.utc)
3283+
).isoformat()
3284+
3285+
oci_config = {
3286+
"created": creation_time,
3287+
"architecture": context.config.architecture.to_oci(),
3288+
# Name of the operating system which the image is built to run on as defined by
3289+
# https://github.com/opencontainers/image-spec/blob/v1.0.2/config.md#properties.
3290+
"os": "linux",
3291+
"rootfs": {
3292+
"type": "layers",
3293+
"diff_ids": [f"sha256:{layer_diff_digest}"],
3294+
},
3295+
"config": {
3296+
"Cmd": [
3297+
"/sbin/init",
3298+
*context.config.kernel_command_line,
3299+
],
3300+
},
3301+
"history": [
3302+
{
3303+
"created": creation_time,
3304+
"comment": "Created by mkosi",
3305+
},
3306+
],
3307+
}
3308+
oci_config_blob = json.dumps(oci_config)
3309+
oci_config_digest = hashlib.sha256(oci_config_blob.encode()).hexdigest()
3310+
with umask(~0o644):
3311+
(ca_store / oci_config_digest).write_text(oci_config_blob)
3312+
3313+
layer_suffix = context.config.compress_output.oci_media_type_suffix()
3314+
oci_manifest = {
3315+
"schemaVersion": 2,
3316+
"mediaType": "application/vnd.oci.image.manifest.v1+json",
3317+
"config": {
3318+
"mediaType": "application/vnd.oci.image.config.v1+json",
3319+
"digest": f"sha256:{oci_config_digest}",
3320+
"size": (ca_store / oci_config_digest).stat().st_size,
3321+
},
3322+
"layers": [
3323+
{
3324+
"mediaType": f"application/vnd.oci.image.layer.v1.tar{layer_suffix}",
3325+
"digest": f"sha256:{layer_digest}",
3326+
"size": (ca_store / layer_digest).stat().st_size,
3327+
}
3328+
],
3329+
"annotations": {
3330+
"io.systemd.mkosi.version": __version__,
3331+
**({
3332+
"org.opencontainers.image.version": context.config.image_version,
3333+
} if context.config.image_version else {}),
3334+
}
3335+
}
3336+
oci_manifest_blob = json.dumps(oci_manifest)
3337+
oci_manifest_digest = hashlib.sha256(oci_manifest_blob.encode()).hexdigest()
3338+
with umask(~0o644):
3339+
(ca_store / oci_manifest_digest).write_text(oci_manifest_blob)
3340+
3341+
(dst / "index.json").write_text(
3342+
json.dumps(
3343+
{
3344+
"schemaVersion": 2,
3345+
"mediaType": "application/vnd.oci.image.index.v1+json",
3346+
"manifests": [
3347+
{
3348+
"mediaType": "application/vnd.oci.image.manifest.v1+json",
3349+
"digest": f"sha256:{oci_manifest_digest}",
3350+
"size": (ca_store / oci_manifest_digest).stat().st_size,
3351+
}
3352+
],
3353+
}
3354+
)
3355+
)
3356+
3357+
(dst / "oci-layout").write_text(json.dumps({"imageLayoutVersion": "1.0.0"}))
3358+
3359+
32633360
def make_esp(context: Context, uki: Path) -> list[Partition]:
32643361
if not (arch := context.config.architecture.to_efi()):
32653362
die(f"Architecture {context.config.architecture} does not support UEFI")
@@ -3561,6 +3658,17 @@ def build_image(context: Context) -> None:
35613658
tools=context.config.tools(),
35623659
sandbox=context.sandbox,
35633660
)
3661+
elif context.config.output_format == OutputFormat.oci:
3662+
make_tar(
3663+
context.root, context.staging / "rootfs.layer",
3664+
tools=context.config.tools(),
3665+
sandbox=context.sandbox,
3666+
)
3667+
make_oci(
3668+
context,
3669+
context.staging / "rootfs.layer",
3670+
context.staging / context.config.output_with_format,
3671+
)
35643672
elif context.config.output_format == OutputFormat.cpio:
35653673
make_cpio(
35663674
context.root, context.staging / context.config.output_with_format,

mkosi/config.py

+38
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ class OutputFormat(StrEnum):
173173
sysext = enum.auto()
174174
tar = enum.auto()
175175
uki = enum.auto()
176+
oci = enum.auto()
176177

177178
def extension(self) -> str:
178179
return {
@@ -205,6 +206,7 @@ class Compression(StrEnum):
205206
xz = enum.auto()
206207
bz2 = enum.auto()
207208
gz = enum.auto()
209+
gzip = "gz"
208210
lz4 = enum.auto()
209211
lzma = enum.auto()
210212

@@ -216,6 +218,18 @@ def extension(self) -> str:
216218
Compression.zstd: ".zst"
217219
}.get(self, f".{self}")
218220

221+
def oci_media_type_suffix(self) -> str:
222+
suffix = {
223+
Compression.none: "",
224+
Compression.gz: "+gzip",
225+
Compression.zstd: "+zstd",
226+
}.get(self)
227+
228+
if not suffix:
229+
die(f"Compression {self} not supported for OCI layers")
230+
231+
return suffix
232+
219233

220234
class DocFormat(StrEnum):
221235
auto = enum.auto()
@@ -379,6 +393,28 @@ def to_qemu(self) -> str:
379393

380394
return a
381395

396+
def to_oci(self) -> str:
397+
a = {
398+
Architecture.arm : "arm",
399+
Architecture.arm64 : "arm64",
400+
Architecture.loongarch64 : "loong64",
401+
Architecture.mips64_le : "mips64le",
402+
Architecture.mips_le : "mipsle",
403+
Architecture.ppc : "ppc",
404+
Architecture.ppc64 : "ppc64",
405+
Architecture.ppc64_le : "ppc64le",
406+
Architecture.riscv32 : "riscv",
407+
Architecture.riscv64 : "riscv64",
408+
Architecture.s390x : "s390x",
409+
Architecture.x86 : "386",
410+
Architecture.x86_64 : "amd64",
411+
}.get(self)
412+
413+
if not a:
414+
die(f"Architecture {self} not supported by OCI")
415+
416+
return a
417+
382418
def default_serial_tty(self) -> str:
383419
return {
384420
Architecture.arm : "ttyAMA0",
@@ -634,6 +670,8 @@ def config_default_compression(namespace: argparse.Namespace) -> Compression:
634670
return Compression.xz
635671
else:
636672
return Compression.zstd
673+
elif namespace.output_format == OutputFormat.oci:
674+
return Compression.gzip
637675
else:
638676
return Compression.none
639677

mkosi/resources/mkosi.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -667,7 +667,8 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`,
667667
archive is generated), `disk` (a block device OS image with a GPT
668668
partition table), `uki` (a unified kernel image with the OS image in
669669
the `.initrd` PE section), `esp` (`uki` but wrapped in a disk image
670-
with only an ESP partition), `sysext`, `confext`, `portable` or `none`
670+
with only an ESP partition), `oci` (a directory compatible with the
671+
OCI image specification), `sysext`, `confext`, `portable` or `none`
671672
(the OS image is solely intended as a build image to produce another
672673
artifact).
673674

@@ -705,11 +706,12 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`,
705706
: Configure compression for the resulting image or archive. The argument can be
706707
either a boolean or a compression algorithm (`xz`, `zstd`). `zstd`
707708
compression is used by default, except CentOS and derivatives up to version
708-
8, which default to `xz`. Note that when applied to block device image types,
709+
8, which default to `xz`, and OCI images, which default to `gzip`.
710+
Note that when applied to block device image types,
709711
compression means the image cannot be started directly but needs to be
710712
decompressed first. This also means that the `shell`, `boot`, `qemu` verbs
711713
are not available when this option is used. Implied for `tar`, `cpio`, `uki`,
712-
and `esp`.
714+
`esp`, and `oci`.
713715

714716
`CompressLevel=`, `--compress-level=`
715717

tests/test_boot.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def test_format(config: Image.Config, format: OutputFormat) -> None:
5757
if image.config.distribution == Distribution.rhel_ubi:
5858
return
5959

60-
if format in (OutputFormat.tar, OutputFormat.none) or format.is_extension_image():
60+
if format in (OutputFormat.tar, OutputFormat.oci, OutputFormat.none) or format.is_extension_image():
6161
return
6262

6363
if format == OutputFormat.directory and not find_virtiofsd():

0 commit comments

Comments
 (0)