From 68bb01545c31c04117e81f63e5039e32273dcc4a Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 26 Sep 2025 11:20:35 +0200 Subject: [PATCH] container: optimize startup of preexisting containers This commit adds metadata to track loaded OCI archives to allow skipping 'delete + load' of OCI images when restarting either the container or the system as a whole. The sha256 of all loaded OCI archives is stored in a sidecar file in our downloads directory. Then we verify the checksum of the OCI archives against their same-named sidecar to determine if the OCI archive is already loaded or not. Additionally, the instance using the image is labled with metadata to detect changes in the container configuration. This in turn allow skipping the delete + create phase also of the instance. Signed-off-by: Joachim Wiberg --- board/common/rootfs/usr/sbin/container | 98 ++++++++++++++++++++++++++ src/confd/src/infix-containers.c | 48 ++++++++++++- 2 files changed, 143 insertions(+), 3 deletions(-) diff --git a/board/common/rootfs/usr/sbin/container b/board/common/rootfs/usr/sbin/container index d39ad217..c3a5bda4 100755 --- a/board/common/rootfs/usr/sbin/container +++ b/board/common/rootfs/usr/sbin/container @@ -230,6 +230,18 @@ load_archive() err 1 "failed tagging image as $tag" fi + # Save archive SHA256 to sidecar file for local archives to enable optimization + if [ -f "$file" ]; then + img_sha256=$(sha256sum "$file" | cut -f1 -d' ') + archive_basename=$(basename "$file") + sha_file="$DOWNLOADS/${archive_basename}.sha256" + + mkdir -p "$DOWNLOADS" + + echo "$img_sha256" > "$sha_file" + log "Saved archive checksum to $sha_file" + fi + # Clean up after ourselves if [ -n "$extracted" ]; then log "Cleaning up extracted $dir" @@ -295,6 +307,20 @@ create() args="$args --network=none" fi + # Add optimization labels for meta-sha256 and config checksum + script="/run/containers/${name}.sh" + if [ -f "$script" ]; then + # Extract meta-sha256 from script if present + meta_sha=$(awk '/^# meta-sha256:/ {print $3}' "$script" 2>/dev/null) + if [ -n "$meta_sha" ]; then + args="$args --label meta-sha256=$meta_sha" + fi + + # Add config checksum label + config_sha=$(sha256sum "$script" | cut -f1 -d' ') + args="$args --label config-sha256=$config_sha" + fi + # shellcheck disable=SC2048 log "podman create --name $name --conmon-pidfile=$pidfile $args $image $*" if podman create --name "$name" --conmon-pidfile="$pidfile" $args "$image" $*; then @@ -785,6 +811,78 @@ case $cmd in script=/run/containers/${name}.sh [ -x "$script" ] || err 1 "setup: $script does not exist or is not executable." + # Early exit optimization: check if container is already up-to-date + meta_sha=$(awk '/^# meta-sha256:/ {print $3}' "$script" 2>/dev/null) + if [ -n "$meta_sha" ]; then + # Local image optimization: check if archive SHA matches stored sidecar file + # Extract the image path from the script + img=$(awk '/^# meta-image:/ {print $3}' "$script" 2>/dev/null) + if [ -n "$img" ]; then + # Determine the archive file path + case "$img" in + oci-archive:*) + archive_path="${img#oci-archive:}" + ;; + *) + archive_path="$img" + ;; + esac + + # Handle relative paths - check BUILTIN and DOWNLOADS + if [ ! -e "$archive_path" ]; then + if [ -e "$DOWNLOADS/$(basename "$archive_path")" ]; then + archive_path="$DOWNLOADS/$(basename "$archive_path")" + elif [ -e "$BUILTIN/$(basename "$archive_path")" ]; then + archive_path="$BUILTIN/$(basename "$archive_path")" + fi + fi + + # Check if the archive exists and compare SHA with sidecar file + if [ -f "$archive_path" ]; then + archive_basename=$(basename "$archive_path") + sha_file="$DOWNLOADS/${archive_basename}.sha256" + + # Check if sidecar file exists + if [ -f "$sha_file" ]; then + stored_sha=$(cat "$sha_file" 2>/dev/null) + current_sha=$(sha256sum "$archive_path" | cut -f1 -d' ') + + # If SHA matches, check container instance + if [ "$stored_sha" = "$current_sha" ]; then + if podman container exists "$name"; then + # Check if container has matching labels + config_sha=$(sha256sum "$script" | cut -f1 -d' ') + container_meta=$(podman inspect "$name" --format '{{index .Config.Labels "meta-sha256"}}' 2>/dev/null) + container_config=$(podman inspect "$name" --format '{{index .Config.Labels "config-sha256"}}' 2>/dev/null) + + if [ "$container_meta" = "$meta_sha" ] && [ "$container_config" = "$config_sha" ]; then + log "Container $name is up-to-date (archive and config unchanged), skipping setup" + exit 0 + fi + fi + else + # Archive changed (e.g., rootfs upgrade) - need to reload + log "Archive SHA changed, will reload image and recreate container" + fi + fi + fi + fi + else + # Remote image optimization: check config-sha256 only + if podman container exists "$name"; then + # Get current config checksum from script + config_sha=$(sha256sum "$script" | cut -f1 -d' ') + + # Get container's stored config checksum + container_config=$(podman inspect "$name" --format '{{index .Config.Labels "config-sha256"}}' 2>/dev/null) + + if [ -n "$container_config" ] && [ "$container_config" = "$config_sha" ]; then + log "Container $name config unchanged, skipping setup" + exit 0 + fi + fi + fi + # Save our PID in case we get stuck here and someone wants to # stop us, e.g., due to reconfiguration or reboot. pidfile=$(pidfn "${name}") diff --git a/src/confd/src/infix-containers.c b/src/confd/src/infix-containers.c index 2a108109..b4e37a89 100644 --- a/src/confd/src/infix-containers.c +++ b/src/confd/src/infix-containers.c @@ -22,6 +22,30 @@ #define _PATH_CONT "/run/containers" +/* + * Check if image is a local archive and return the offset to the file path. + * Returns 0 if not a recognized local archive format. + */ +static int archive_offset(const char *image) +{ + static const struct { + const char *prefix; + int offset; + } prefixes[] = { + { "docker-archive:", 15 }, + { "oci-archive:", 12 }, + { NULL, 0 } + }; + int i; + + for (i = 0; prefixes[i].prefix; i++) { + if (!strncmp(image, prefixes[i].prefix, prefixes[i].offset)) + return prefixes[i].offset; + } + + return 0; +} + /* * Create a setup/create/upgrade script and instantiate a new instance * that Finit will start when all networking and other dependencies are @@ -37,6 +61,7 @@ static int add(const char *name, struct lyd_node *cif) struct lyd_node *node, *nets, *caps; char script[strlen(name) + 5]; FILE *fp, *ap; + int offset; snprintf(script, sizeof(script), "%s.sh", name); fp = fopenf("w", "%s/%s", _PATH_CONT, script); @@ -58,9 +83,26 @@ static int add(const char *name, struct lyd_node *cif) image = lydx_get_cattr(cif, "image"); fprintf(fp, "#!/bin/sh\n" "# meta-name: %s\n" - "# meta-image: %s\n" - "container --quiet delete %s >/dev/null\n" - "container --quiet", name, image, name); + "# meta-image: %s\n", name, image); + + offset = archive_offset(image); + if (offset) { + const char *path = image + offset; + char sha256[65] = { 0 }; + FILE *pp; + + pp = popenf("r", "sha256sum %s | cut -f1 -d' '", path); + if (pp) { + if (fgets(sha256, sizeof(sha256), pp)) { + chomp(sha256); + fprintf(fp, "# meta-sha256: %s\n", sha256); + } + pclose(pp); + } + } + + fprintf(fp, "container --quiet delete %s >/dev/null\n" + "container --quiet", name); LYX_LIST_FOR_EACH(lyd_child(cif), node, "dns") fprintf(fp, " --dns %s", lyd_get_value(node));