Blame assemblers/org.osbuild.oci-archive

Packit Service 863627
#!/usr/bin/python3
Packit Service 863627
"""
Packit Service 863627
Assemble an OCI image archive
Packit Service 863627
Packit Service 863627
Assemble an Open Container Initiative[1] image[2] archive, i.e. a
Packit Service 863627
tarball whose contents is in the OCI image layout.
Packit Service 863627
Packit Service 863627
Currently the only required options are `filename` and `architecture`.
Packit Service 863627
The execution parameters for the image, which then should form the base
Packit Service 863627
for the container, can be given via `config`. They have the same format
Packit Service 863627
as the `config` option for the "OCI Image Configuration" (see [2]),
Packit Service 863627
except those that map to the "Go type map[string]struct{}", which are
Packit Service 863627
represented as array of strings.
Packit Service 863627
Packit Service 863627
The final resulting tarball, aka a "orci-archive", can be imported via
Packit Service 863627
podman[3] with `podman pull oci-archive:<archive>`.
Packit Service 863627
Packit Service 863627
[1] https://www.opencontainers.org/
Packit Service 863627
[2] https://github.com/opencontainers/image-spec/
Packit Service 863627
[3] https://podman.io/
Packit Service 863627
"""
Packit Service 863627
Packit Service 863627
Packit Service 863627
import datetime
Packit Service 863627
import json
Packit Service 863627
import os
Packit Service 863627
import subprocess
Packit Service 863627
import sys
Packit Service 863627
import tempfile
Packit Service 863627
Packit Service 863627
Packit Service 863627
DEFAULT_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
Packit Service 863627
Packit Service 863627
Packit Service 863627
SCHEMA = """
Packit Service 863627
"additionalProperties": false,
Packit Service 863627
"required": ["architecture", "filename"],
Packit Service 863627
"properties": {
Packit Service 863627
  "architecture": {
Packit Service 863627
    "description": "The CPU architecture of the image",
Packit Service 863627
    "type": "string"
Packit Service 863627
  },
Packit Service 863627
  "filename": {
Packit Service 863627
    "description": "Resulting image filename",
Packit Service 863627
    "type": "string"
Packit Service 863627
  },
Packit Service 863627
  "config": {
Packit Service 863627
    "description": "The execution parameters",
Packit Service 863627
    "type": "object",
Packit Service 863627
    "additionalProperties": false,
Packit Service 863627
    "properties": {
Packit Service 863627
      "Cmd": {
Packit Service 863627
        "type": "array",
Packit Service 863627
        "default": ["sh"],
Packit Service 863627
        "items": {
Packit Service 863627
          "type": "string"
Packit Service 863627
        }
Packit Service 863627
      },
Packit Service 863627
      "Env": {
Packit Service 863627
        "type": "array",
Packit Service 863627
        "default": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],
Packit Service 863627
        "items": {
Packit Service 863627
          "type": "string"
Packit Service 863627
        }
Packit Service 863627
      },
Packit Service 863627
      "ExposedPorts": {
Packit Service 863627
        "type": "array",
Packit Service 863627
        "items": {
Packit Service 863627
          "type": "string"
Packit Service 863627
        }
Packit Service 863627
      },
Packit Service 863627
      "User": {
Packit Service 863627
        "type": "string"
Packit Service 863627
      },
Packit Service 863627
      "Labels": {
Packit Service 863627
        "type": "object",
Packit Service 863627
        "additionalProperties": true
Packit Service 863627
      },
Packit Service 863627
      "StopSiganl": {
Packit Service 863627
        "type": "string"
Packit Service 863627
      },
Packit Service 863627
      "Volumes": {
Packit Service 863627
        "type": "array",
Packit Service 863627
        "items": {
Packit Service 863627
          "type": "string"
Packit Service 863627
        }
Packit Service 863627
      },
Packit Service 863627
      "WorkingDir": {
Packit Service 863627
        "type": "string"
Packit Service 863627
      }
Packit Service 863627
    }
Packit Service 863627
  }
Packit Service 863627
}
Packit Service 863627
"""
Packit Service 863627
Packit Service 863627
Packit Service 863627
MEDIA_TYPES = {
Packit Service 863627
    "layer": "application/vnd.oci.image.layer.v1.tar",
Packit Service 863627
    "manifest": "application/vnd.oci.image.manifest.v1+json",
Packit Service 863627
    "config": "application/vnd.oci.image.config.v1+json"
Packit Service 863627
}
Packit Service 863627
Packit Service 863627
Packit Service 863627
def sha256sum(path: str) -> str:
Packit Service 863627
    ret = subprocess.run(["sha256sum", path],
Packit Service 863627
                         stdout=subprocess.PIPE,
Packit Service 863627
                         encoding="utf-8",
Packit Service 863627
                         check=True)
Packit Service 863627
Packit Service 863627
    return ret.stdout.strip().split(" ")[0]
Packit Service 863627
Packit Service 863627
Packit Service 863627
def blobs_add_file(blobs: str, path: str, mtype: str):
Packit Service 863627
    digest = sha256sum(path)
Packit Service 863627
    size = os.stat(path).st_size
Packit Service 863627
Packit Service 863627
    os.rename(path, os.path.join(blobs, digest))
Packit Service 863627
    info = {
Packit Service 863627
        "digest": "sha256:" + digest,
Packit Service 863627
        "size": size,
Packit Service 863627
        "mediaType": MEDIA_TYPES[mtype]
Packit Service 863627
    }
Packit Service 863627
Packit Service 863627
    print(f"blobs: +{mtype} ({size}, {digest})")
Packit Service 863627
    return info
Packit Service 863627
Packit Service 863627
Packit Service 863627
def blobs_add_json(blobs: str, js: str, mtype: str):
Packit Service 863627
    js_file = os.path.join(blobs, "temporary.js")
Packit Service 863627
    with open(js_file, "w") as f:
Packit Service 863627
        json.dump(js, f)
Packit Service 863627
Packit Service 863627
    return blobs_add_file(blobs, js_file, mtype)
Packit Service 863627
Packit Service 863627
Packit Service 863627
def blobs_add_layer(blobs: str, tree: str):
Packit Service 863627
    compression = "gzip"
Packit Service 863627
Packit Service 863627
    layer_file = os.path.join(blobs, "layer.tar")
Packit Service 863627
Packit Service 863627
    command = [
Packit Service 863627
        "tar",
Packit Service 863627
        "--selinux",
Packit Service 863627
        "--acls",
Packit Service 863627
        "--xattrs",
Packit Service 863627
        "-cf", layer_file,
Packit Service 863627
        "-C", tree,
Packit Service 863627
    ] + os.listdir(tree)
Packit Service 863627
Packit Service 863627
    print("creating layer")
Packit Service 863627
    subprocess.run(command,
Packit Service 863627
                   stdout=subprocess.DEVNULL,
Packit Service 863627
                   check=True)
Packit Service 863627
Packit Service 863627
    digest = "sha256:" + sha256sum(layer_file)
Packit Service 863627
Packit Service 863627
    print("compressing layer")
Packit Service 863627
    suffix = ".compressed"
Packit Service 863627
    subprocess.run([compression,
Packit Service 863627
                    "-S", suffix,
Packit Service 863627
                    layer_file],
Packit Service 863627
                   stdout=subprocess.DEVNULL,
Packit Service 863627
                   check=True)
Packit Service 863627
Packit Service 863627
    layer_file += suffix
Packit Service 863627
Packit Service 863627
    info = blobs_add_file(blobs, layer_file, "layer")
Packit Service 863627
    info["mediaType"] += compression
Packit Service 863627
Packit Service 863627
    return digest, info
Packit Service 863627
Packit Service 863627
Packit Service 863627
def config_from_options(options):
Packit Service 863627
    command = options.get("Cmd", ["sh"])
Packit Service 863627
    env = options.get("Env", ["PATH=" + DEFAULT_PATH])
Packit Service 863627
Packit Service 863627
    config = {
Packit Service 863627
        "Env": env,
Packit Service 863627
        "Cmd": command
Packit Service 863627
    }
Packit Service 863627
Packit Service 863627
    for name in ["User", "Labels", "StopSignal", "WorkingDir"]:
Packit Service 863627
        item = options.get(name)
Packit Service 863627
        if item:
Packit Service 863627
            config[name] = item
Packit Service 863627
Packit Service 863627
    for name in ["ExposedPorts", "Volumes"]:
Packit Service 863627
        item = options.get(name)
Packit Service 863627
        if item:
Packit Service 863627
            config[name] = {x: {} for x in item}
Packit Service 863627
Packit Service 863627
    print(config)
Packit Service 863627
    return config
Packit Service 863627
Packit Service 863627
Packit Service 863627
def create_oci_dir(tree, output_dir, options):
Packit Service 863627
    architecture = options["architecture"]
Packit Service 863627
Packit Service 863627
    config = {
Packit Service 863627
        "created": datetime.datetime.utcnow().isoformat() + "Z",
Packit Service 863627
        "architecture": architecture,
Packit Service 863627
        "os": "linux",
Packit Service 863627
        "config": config_from_options(options["config"]),
Packit Service 863627
        "rootfs": {
Packit Service 863627
            "type": "layers",
Packit Service 863627
            "diff_ids": []
Packit Service 863627
        }
Packit Service 863627
    }
Packit Service 863627
Packit Service 863627
    manifest = {
Packit Service 863627
        "schemaVersion": 2,
Packit Service 863627
        "config": None,
Packit Service 863627
        "layers": []
Packit Service 863627
    }
Packit Service 863627
Packit Service 863627
    index = {
Packit Service 863627
        "schemaVersion": 2,
Packit Service 863627
        "manifests": []
Packit Service 863627
    }
Packit Service 863627
Packit Service 863627
    blobs = os.path.join(output_dir, "blobs", "sha256")
Packit Service 863627
    os.makedirs(blobs)
Packit Service 863627
Packit Service 863627
    ## layers / rootfs
Packit Service 863627
Packit Service 863627
    digest, info = blobs_add_layer(blobs, tree)
Packit Service 863627
Packit Service 863627
    config["rootfs"]["diff_ids"] = [digest]
Packit Service 863627
    manifest["layers"].append(info)
Packit Service 863627
Packit Service 863627
    ## write config
Packit Service 863627
    info = blobs_add_json(blobs, config, "config")
Packit Service 863627
    manifest["config"] = info
Packit Service 863627
Packit Service 863627
    # manifest
Packit Service 863627
    info = blobs_add_json(blobs, manifest, "manifest")
Packit Service 863627
    index["manifests"].append(info)
Packit Service 863627
Packit Service 863627
    # index
Packit Service 863627
    print("writing index")
Packit Service 863627
    with open(os.path.join(output_dir, "index.json"), "w") as f:
Packit Service 863627
        json.dump(index, f)
Packit Service 863627
Packit Service 863627
    # oci-layout tag
Packit Service 863627
    with open(os.path.join(output_dir, "oci-layout"), "w") as f:
Packit Service 863627
        json.dump({"imageLayoutVersion": "1.0.0"}, f)
Packit Service 863627
Packit Service 863627
Packit Service 863627
def main(tree, output_dir, options):
Packit Service 863627
    filename = options["filename"]
Packit Service 863627
Packit Service 863627
    with tempfile.TemporaryDirectory(dir=output_dir) as tmpdir:
Packit Service 863627
        workdir = os.path.join(tmpdir, "output")
Packit Service 863627
        os.makedirs(workdir)
Packit Service 863627
Packit Service 863627
        create_oci_dir(tree, workdir, options)
Packit Service 863627
Packit Service 863627
        command = [
Packit Service 863627
            "tar",
Packit Service 863627
            "--remove-files",
Packit Service 863627
            "-cf", os.path.join(output_dir, filename),
Packit Service 863627
            f"--directory={workdir}",
Packit Service 863627
        ] + os.listdir(workdir)
Packit Service 863627
Packit Service 863627
        print("creating final archive")
Packit Service 863627
        subprocess.run(command,
Packit Service 863627
                       stdout=subprocess.DEVNULL,
Packit Service 863627
                       check=True)
Packit Service 863627
Packit Service 863627
Packit Service 863627
if __name__ == '__main__':
Packit Service 863627
    args = json.load(sys.stdin)
Packit Service 863627
    r = main(args["tree"], args["output_dir"], args["options"])
Packit Service 863627
    sys.exit(r)