Skip to content

Commit

Permalink
Implement creation of OCI images
Browse files Browse the repository at this point in the history
  • Loading branch information
septatrix committed Mar 29, 2024
1 parent 248dfb4 commit 1567bee
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 4 deletions.
107 changes: 107 additions & 0 deletions mkosi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3260,6 +3260,102 @@ def make_disk(
return make_image(context, msg=msg, skip=skip, split=split, tabs=tabs, root=context.root, definitions=definitions)


def make_oci(context: Context, root_layer: Path, dst: Path) -> None:
ca_store = dst / "blobs" / "sha256"
ca_store.mkdir(parents=True)

layer_diff_digest = hash_file(root_layer)
maybe_compress(
context,
context.config.compress_output,
context.staging / "rootfs.layer",
# Pass explicit destination to suppress adding an extension
context.staging / "rootfs.layer",
)
layer_digest = hash_file(root_layer)
root_layer.rename(ca_store / layer_digest)

creation_time = (
datetime.datetime.fromtimestamp(context.config.source_date_epoch, tz=datetime.timezone.utc)
if context.config.source_date_epoch
else datetime.datetime.now(tz=datetime.timezone.utc)
).isoformat()

oci_config = {
"created": creation_time,
"architecture": context.config.architecture.to_oci(),
# Name of the operating system which the image is built to run on as defined by
# https://github.com/opencontainers/image-spec/blob/v1.0.2/config.md#properties.
"os": "linux",
"rootfs": {
"type": "layers",
"diff_ids": [f"sha256:{layer_diff_digest}"],
},
"config": {
"Cmd": [
"/sbin/init",
*context.config.kernel_command_line,
*context.config.kernel_command_line_extra,
],
},
"history": [
{
"created": creation_time,
"comment": "Created by mkosi",
},
],
}
oci_config_blob = json.dumps(oci_config)
oci_config_digest = hashlib.sha256(oci_config_blob.encode()).hexdigest()
(ca_store / oci_config_digest).write_text(oci_config_blob)

layer_suffix = context.config.compress_output.oci_media_type_suffix()
oci_manifest = {
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": f"sha256:{oci_config_digest}",
"size": (ca_store / oci_config_digest).stat().st_size,
},
"layers": [
{
"mediaType": f"application/vnd.oci.image.layer.v1.tar{layer_suffix}",
"digest": f"sha256:{layer_digest}",
"size": (ca_store / layer_digest).stat().st_size,
}
],
"annotations": {
"io.systemd.mkosi.version": __version__,
**({
"org.opencontainers.image.version": context.config.image_version,
} if context.config.image_version else {}),
}
}
oci_manifest_blob = json.dumps(oci_manifest)
oci_manifest_digest = hashlib.sha256(oci_manifest_blob.encode()).hexdigest()
(ca_store / oci_manifest_digest).write_text(oci_manifest_blob)

with (dst / "index.json").open("w") as f:
json.dump(
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": f"sha256:{oci_manifest_digest}",
"size": (ca_store / oci_manifest_digest).stat().st_size,
}
],
},
f,
)

with (dst / "oci-layout").open("w") as f:
json.dump({"imageLayoutVersion": "1.0.0"}, f)


def make_esp(context: Context, uki: Path) -> list[Partition]:
if not (arch := context.config.architecture.to_efi()):
die(f"Architecture {context.config.architecture} does not support UEFI")
Expand Down Expand Up @@ -3561,6 +3657,17 @@ def build_image(context: Context) -> None:
tools=context.config.tools(),
sandbox=context.sandbox,
)
elif context.config.output_format == OutputFormat.oci:
make_tar(
context.root, context.staging / "rootfs.layer",
tools=context.config.tools(),
sandbox=context.sandbox,
)
make_oci(
context,
context.staging / "rootfs.layer",
context.staging / context.config.output_with_format,
)
elif context.config.output_format == OutputFormat.cpio:
make_cpio(
context.root, context.staging / context.config.output_with_format,
Expand Down
38 changes: 38 additions & 0 deletions mkosi/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ class OutputFormat(StrEnum):
sysext = enum.auto()
tar = enum.auto()
uki = enum.auto()
oci = enum.auto()

def extension(self) -> str:
return {
Expand Down Expand Up @@ -205,6 +206,7 @@ class Compression(StrEnum):
xz = enum.auto()
bz2 = enum.auto()
gz = enum.auto()
gzip = "gz"
lz4 = enum.auto()
lzma = enum.auto()

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

def oci_media_type_suffix(self) -> str:
suffix = {
Compression.none: "",
Compression.gz: "+gzip",
Compression.zstd: "+zstd",
}.get(self)

if not suffix:
die(f"Compression {self} not supported for OCI layers")

return suffix


class DocFormat(StrEnum):
auto = enum.auto()
Expand Down Expand Up @@ -379,6 +393,28 @@ def to_qemu(self) -> str:

return a

def to_oci(self) -> str:
a = {
Architecture.arm : "arm",
Architecture.arm64 : "arm64",
Architecture.loongarch64 : "loong64",
Architecture.mips64_le : "mips64le",
Architecture.mips_le : "mipsle",
Architecture.ppc : "ppc",
Architecture.ppc64 : "ppc64",
Architecture.ppc64_le : "ppc64le",
Architecture.riscv32 : "riscv",
Architecture.riscv64 : "riscv64",
Architecture.s390x : "s390x",
Architecture.x86 : "386",
Architecture.x86_64 : "amd64",
}.get(self)

if not a:
die(f"Architecture {self} not supported by OCI")

return a

def default_serial_tty(self) -> str:
return {
Architecture.arm : "ttyAMA0",
Expand Down Expand Up @@ -634,6 +670,8 @@ def config_default_compression(namespace: argparse.Namespace) -> Compression:
return Compression.xz
else:
return Compression.zstd
elif namespace.output_format == OutputFormat.oci:
return Compression.gzip
else:
return Compression.none

Expand Down
8 changes: 5 additions & 3 deletions mkosi/resources/mkosi.md
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,8 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`,
archive is generated), `disk` (a block device OS image with a GPT
partition table), `uki` (a unified kernel image with the OS image in
the `.initrd` PE section), `esp` (`uki` but wrapped in a disk image
with only an ESP partition), `sysext`, `confext`, `portable` or `none`
with only an ESP partition), `oci` (a directory compatible with the
OCI image specification), `sysext`, `confext`, `portable` or `none`
(the OS image is solely intended as a build image to produce another
artifact).

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

`CompressLevel=`, `--compress-level=`

Expand Down
2 changes: 1 addition & 1 deletion tests/test_boot.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def test_format(config: Image.Config, format: OutputFormat) -> None:
if image.config.distribution == Distribution.rhel_ubi:
return

if format in (OutputFormat.tar, OutputFormat.none) or format.is_extension_image():
if format in (OutputFormat.tar, OutputFormat.oci, OutputFormat.none) or format.is_extension_image():
return

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

0 comments on commit 1567bee

Please sign in to comment.