From 2f648d0366d49f431476650bfc22e48697d4d37d Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 14 Oct 2025 11:52:56 +0200 Subject: [PATCH 01/19] package/finit: backport support for longer service identifiers Fixes #1146 Signed-off-by: Joachim Wiberg --- ...LEN-to-support-longer-service-identi.patch | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 package/finit/0001-Increase-MAX_ID_LEN-to-support-longer-service-identi.patch diff --git a/package/finit/0001-Increase-MAX_ID_LEN-to-support-longer-service-identi.patch b/package/finit/0001-Increase-MAX_ID_LEN-to-support-longer-service-identi.patch new file mode 100644 index 000000000..98bc19b81 --- /dev/null +++ b/package/finit/0001-Increase-MAX_ID_LEN-to-support-longer-service-identi.patch @@ -0,0 +1,35 @@ +From 927acdbd7e19b1a9a0065ebdb88b663ec6626b88 Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Tue, 14 Oct 2025 11:49:57 +0200 +Subject: [PATCH] Increase MAX_ID_LEN to support longer service identifiers +Organization: Wires + +Allow service IDs up to 64 characters to support SHA-256 hashes, +UUIDs, and other long unique identifiers. This increases memory +usage by ~98 bytes per service instance, which is negligible for +typical deployments. + +The IDENT column width in initctl output adapts dynamically based +on actual ID lengths in use, so short IDs remain unaffected. + +Signed-off-by: Joachim Wiberg +--- + src/svc.h | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/svc.h b/src/svc.h +index 74cc3ca..1a7e6f2 100644 +--- a/src/svc.h ++++ b/src/svc.h +@@ -94,7 +94,7 @@ typedef enum { + SVC_NOTIFY_S6, + } svc_notify_t; + +-#define MAX_ID_LEN 16 ++#define MAX_ID_LEN 65 + #define MAX_ARG_LEN 64 + #define MAX_CMD_LEN 256 + #define MAX_IDENT_LEN (MAX_ARG_LEN + MAX_ID_LEN + 1) +-- +2.43.0 + From 273757025313af5b4c47222bfb331fdf79a18b20 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 9 Sep 2025 17:37:43 +0200 Subject: [PATCH 02/19] confd: add new container and volume name type This rectifies an omission from the initial yang model. Not all charachters are supported in container and volume names. E.g., simply attempting to create a volume or container with a space in the name causes this error message from podman: podman: Error: running volume create option: names must match [a-zA-Z0-9][a-zA-Z0-9_.-]*: invalid argument In addition to the regexp, the new 'ident' type also enforces a minimum and maximum length. Sure, technically a single char is allowed, but let's be reasonable, and who in their right mind wants an identifier > 64 chars? We have description for that. Signed-off-by: Joachim Wiberg --- src/confd/yang/confd/infix-containers.yang | 22 ++++++++++++++----- ....yang => infix-containers@2025-09-09.yang} | 0 src/confd/yang/containers.inc | 2 +- 3 files changed, 17 insertions(+), 7 deletions(-) rename src/confd/yang/confd/{infix-containers@2025-06-25.yang => infix-containers@2025-09-09.yang} (100%) diff --git a/src/confd/yang/confd/infix-containers.yang b/src/confd/yang/confd/infix-containers.yang index 9226bc243..b4c90adaf 100644 --- a/src/confd/yang/confd/infix-containers.yang +++ b/src/confd/yang/confd/infix-containers.yang @@ -22,6 +22,11 @@ module infix-containers { prefix infix-sys; } + revision 2025-09-09 { + description "Add dedicated 'ident' type for container and volume names."; + reference "internal"; + } + revision 2025-06-25 { description "Add file mode option to content mounts, allows creating scripts."; reference "internal"; @@ -62,6 +67,14 @@ module infix-containers { * Typedefs */ + typedef ident { + description "Container name or volume identifiers may not contain spaces."; + type string { + length "2..64"; + pattern '[a-zA-Z0-9][a-zA-Z0-9_.-]+'; + } + } + typedef mount-type { type enumeration { enum bind { @@ -128,7 +141,7 @@ module infix-containers { leaf name { description "Name of the container"; - type string; + type ident; } leaf id { @@ -344,7 +357,7 @@ module infix-containers { For persistent writable directories, *volumes* may be a better fit for your container and easier to set up."; - type string; + type ident; } leaf type { @@ -436,10 +449,7 @@ module infix-containers { with the contents of the container's file system on first use. Hence, upgrading the container image will not update the volume if the image has new/removed files at 'path'."; - type string { - pattern '[a-zA-Z_][a-zA-Z0-9_]*'; - length "1..64"; - } + type ident; } leaf target { diff --git a/src/confd/yang/confd/infix-containers@2025-06-25.yang b/src/confd/yang/confd/infix-containers@2025-09-09.yang similarity index 100% rename from src/confd/yang/confd/infix-containers@2025-06-25.yang rename to src/confd/yang/confd/infix-containers@2025-09-09.yang diff --git a/src/confd/yang/containers.inc b/src/confd/yang/containers.inc index 4b2e3f30c..b9199119b 100644 --- a/src/confd/yang/containers.inc +++ b/src/confd/yang/containers.inc @@ -1,5 +1,5 @@ # -*- sh -*- MODULES=( "infix-interfaces -e containers" - "infix-containers@2025-06-25.yang" + "infix-containers@2025-09-09.yang" ) From 6678cc352d739e5c35ef8b721e718ab53f6087bf Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 9 Sep 2025 18:08:32 +0200 Subject: [PATCH 03/19] confd: create container script even if disabled Not only great for debugging, but also allows users to start their containers manually in another way. But yeah, mostly for debug. Signed-off-by: Joachim Wiberg --- src/confd/src/infix-containers.c | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/confd/src/infix-containers.c b/src/confd/src/infix-containers.c index 22736992f..2a108109a 100644 --- a/src/confd/src/infix-containers.c +++ b/src/confd/src/infix-containers.c @@ -38,16 +38,6 @@ static int add(const char *name, struct lyd_node *cif) char script[strlen(name) + 5]; FILE *fp, *ap; - /* - * If running already, disable the service, keeping the created - * container and any volumes for later if the user re-enables - * it again. - */ - if (!lydx_is_enabled(cif, "enabled")) { - systemf("initctl -bnq disable container@%s.conf", name); - return 0; - } - snprintf(script, sizeof(script), "%s.sh", name); fp = fopenf("w", "%s/%s", _PATH_CONT, script); if (!fp) { @@ -256,9 +246,9 @@ static int add(const char *name, struct lyd_node *cif) fchmod(fileno(fp), 0700); fclose(fp); - /* Enable, or update, container -- both trigger container setup. */ - systemf("initctl -bnq enable container@%s.conf", name); systemf("initctl -bnq touch container@%s.conf", name); + systemf("initctl -bnq %s container@%s.conf", lydx_is_enabled(cif, "enabled") + ? "enable" : "disable", name); return 0; } From e91f71e31e1f85922436c75fb3dd57fe49b2ab5d Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 9 Sep 2025 18:05:37 +0200 Subject: [PATCH 04/19] container: increase podman stop timeout The default timeout for 'podman stop foo' is 10 seconds, which for heavily loaded systems with intricate shutdown process is *waaaay* too short. Increase it to the container script default 30s, which coincidentally is also the container@.conf template's kill delay. Fixes #1149 Signed-off-by: Joachim Wiberg --- board/common/rootfs/usr/sbin/container | 32 +++++++++++++++++--------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/board/common/rootfs/usr/sbin/container b/board/common/rootfs/usr/sbin/container index fe68c85a8..79033cf91 100755 --- a/board/common/rootfs/usr/sbin/container +++ b/board/common/rootfs/usr/sbin/container @@ -375,6 +375,9 @@ stop() # Real work is done by wrap() courtesy of finit sysv emulation } +# When called by Finit: `initctl stop foo`, the container script +# is called as `container -n $foo stop`. For the stop command +# we want to bypass the default 10 second timeout. wrap() { name=$1 @@ -385,21 +388,28 @@ wrap() # The setup phase may run forever in the background trying to fetch # the image. It saves its PID in /run/containers/${name}.pid - if [ "$cmd" = "stop" ] && [ -f "$pidfile" ]; then - pid=$(cat "$pidfile") - - # Check if setup is still running ... - if kill -0 "$pid" 2>/dev/null; then - kill "$pid" - wait "$pid" 2>/dev/null - fi + if [ "$cmd" = "stop" ]; then + if [ -f "$pidfile" ]; then + pid=$(cat "$pidfile") + + # Check if setup is still running ... + if kill -0 "$pid" 2>/dev/null; then + kill "$pid" + wait "$pid" 2>/dev/null + fi + + rm -f "$pidfile" + return 0 + fi - rm -f "$pidfile" - return 0 + args="-i --timeout $timeout" + else + args= fi # Skip "echo $name" from podman start in log - podman "$cmd" "$name" >/dev/null + # shellcheck disable=SC2086 + podman "$cmd" $args "$name" >/dev/null } # Removes network $1 from all containers From 144806637cbd36b21177c5230f831ad834f57c9d Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 9 Sep 2025 18:52:23 +0200 Subject: [PATCH 05/19] container: only retry remote images on network changes Fixes #1148 Signed-off-by: Joachim Wiberg --- board/common/rootfs/usr/sbin/container | 42 ++++++++++++++++++-------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/board/common/rootfs/usr/sbin/container b/board/common/rootfs/usr/sbin/container index 79033cf91..d39ad2173 100755 --- a/board/common/rootfs/usr/sbin/container +++ b/board/common/rootfs/usr/sbin/container @@ -790,19 +790,35 @@ case $cmd in pidfile=$(pidfn "${name}") echo $$ > "$pidfile" - while ! "$script"; do - log "${name}: setup failed, waiting for network changes ..." - - # Timeout and retry after 60 seconds, on SIGTERM, or when - # any network event is caught. - timeout -s TERM -k 1 60 sh -c \ - 'ip monitor address route 2>/dev/null | head -n1 >/dev/null' || true - - # On IP address/route changes, wait a few seconds more to ensure - # the system has ample time to react and set things up for us. - log "${name}: retrying ..." - sleep 2 - done + # Try setup once first + if ! "$script"; then + # Get image transport to decide if retries make sense + image=$(awk '/^# meta-image:/ {print $3}' "$script" 2>/dev/null || echo "") + case "$image" in + oci:* | oci-archive:* | docker-archive:* | docker-daemon:* | dir:* | containers-storage:* | ostree:* | sif:* | /*) + # Local transport - exit immediately on failure + log "${name}: setup failed for local image $image" + rm -f "$pidfile" + exit 1 + ;; + *) + # Remote transport (docker://, ftp://, http://, https://, etc.) - retry on network changes + while ! "$script"; do + log "${name}: setup failed, waiting for network changes ..." + + # Timeout and retry after 60 seconds, on SIGTERM, or when + # any network event is caught. + timeout -s TERM -k 1 60 sh -c \ + 'ip monitor address route 2>/dev/null | head -n1 >/dev/null' || true + + # On IP address/route changes, wait a few seconds more to ensure + # the system has ample time to react and set things up for us. + log "${name}: retrying ..." + sleep 2 + done + ;; + esac + fi rm -f "$pidfile" cnt=$(podman image prune -f | wc -l) From 8a83bbe99043c1e311da3df4a0547a0e589b9eba Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 26 Sep 2025 11:20:35 +0200 Subject: [PATCH 06/19] 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 d39ad2173..c3a5bda4c 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 2a108109a..b4e37a896 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)); From f2ad1063baed19204bcacb60afe5f9a22f6f63ee Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 9 Sep 2025 17:51:29 +0200 Subject: [PATCH 07/19] container: refactor cleanup on instance removal This commit reverts 477f7ae and bb19d06, which intended to fix an issue with lingering old images, see #1098. However, as detailed in #1147, this caused severe side effects while working with multiple larger containers. Basically, the prune operation of one container removed images of other containers that are just being created in parallel. Instead of using the podman prune command we can use the meta datain the start script to pinpoint exactly which image(s) to remove, including any downloaded OCI archives when the container instance is removed. Fixes #1147 Signed-off-by: Joachim Wiberg --- .../etc/finit.d/available/container@.conf | 9 +- board/common/rootfs/usr/sbin/container | 125 ++++++++++++++++-- src/confd/src/core.c | 6 +- src/confd/src/infix-containers.c | 26 ++++ 4 files changed, 149 insertions(+), 17 deletions(-) diff --git a/board/common/rootfs/etc/finit.d/available/container@.conf b/board/common/rootfs/etc/finit.d/available/container@.conf index 7a494c600..4e6b813cc 100644 --- a/board/common/rootfs/etc/finit.d/available/container@.conf +++ b/board/common/rootfs/etc/finit.d/available/container@.conf @@ -1,7 +1,8 @@ # Start a container instance (%i) and redirect logs to /log/container -# Give podman enough time to properly shut down the container, kill:30 -# The pre:script, which is responsibe for fetching a remote image, must -# not have a timeout. The cleanup should take no longer than a minute. +# Give podman enough time to properly shut down the container, kill:30, +# which is also matched in the container script (podman default is 10!) +# The pre:script, responsibe for fetching a remote image and calling on +# 'podman load', must not have a timeout. sysv log:prio:local1,tag:%i kill:30 pid:!/run/container:%i.pid \ - pre:0,/usr/sbin/container cleanup:60,/usr/sbin/container \ + pre:0,/usr/sbin/container cleanup:0,/usr/sbin/container \ [2345] :%i container -n %i -- container %i diff --git a/board/common/rootfs/usr/sbin/container b/board/common/rootfs/usr/sbin/container index c3a5bda4c..bbebb43b1 100755 --- a/board/common/rootfs/usr/sbin/container +++ b/board/common/rootfs/usr/sbin/container @@ -6,19 +6,24 @@ # NOTE: when creating/deleting containers, remember 'initctl reload' to # activate the changes! In confd this is already handled. # +CLEANUP=/var/lib/containers/cleanup DOWNLOADS=/var/lib/containers/oci BUILTIN=/lib/oci BASEDIR=/var/tmp container=$0 checksum="" extracted= -timeout=30 +timeout=30 # NOTE: matched with container@.conf dir="" all="" env="" port="" force= +# Variable shared across subshells +export meta_sha="" + + log() { logger -I $PPID -t container -p local1.notice -- "$*" @@ -118,6 +123,7 @@ load_archive() { uri=$1 tag=$2 + tmp=$3 img=$(basename "$uri") # Supported transports for load and create @@ -230,7 +236,8 @@ load_archive() err 1 "failed tagging image as $tag" fi - # Save archive SHA256 to sidecar file for local archives to enable optimization + # Save archive SHA256 to sidecar file to enable optimization + # This applies to both local archives and downloaded remote archives if [ -f "$file" ]; then img_sha256=$(sha256sum "$file" | cut -f1 -d' ') archive_basename=$(basename "$file") @@ -239,6 +246,10 @@ load_archive() mkdir -p "$DOWNLOADS" echo "$img_sha256" > "$sha_file" + if [ -n "$tmp" ]; then + echo "$img_sha256" > "$tmp" + fi + log "Saved archive checksum to $sha_file" fi @@ -273,10 +284,16 @@ create() # Unpack and load docker-archive/oci/oci-archive, returning image # name, or return docker:// URL for download. - if ! image=$(load_archive "$image"); then + sha_file=$(mktemp -u) + if ! image=$(load_archive "$image" "" "$sha_file"); then exit 1 fi + if [ -f "$sha_file" ]; then + meta_sha=$(cat "$sha_file" 2>/dev/null || echo "") + rm "$sha_file" + fi + if [ -z "$logging" ]; then logging="--log-driver syslog" fi @@ -310,8 +327,10 @@ create() # 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 [ -z "$meta_sha" ]; then + # Extract meta-sha256 from script if present + meta_sha=$(awk '/^# meta-sha256:/ {print $3}' "$script" 2>/dev/null) + fi if [ -n "$meta_sha" ]; then args="$args --label meta-sha256=$meta_sha" fi @@ -357,11 +376,90 @@ delete() sleep 1 done + # NOTE: -v does not remove *named volumes* podman rm -vif "$name" >/dev/null 2>&1 [ -n "$quiet" ] || log "Container $name has been removed." +} + +# Called by Finit cleanup:script when a container service is removed. +# Processes all container instance IDs found in $CLEANUP// directory +# and removes them. This handles the case where a container with the same +# name but different configuration replaces an old one. +# +# Note: Volumes are NOT automatically removed to prevent accidental data loss. +# Use 'admin-exec container prune' to clean up unused volumes manually. +cleanup() +{ + if [ -z "$name" ]; then + log "cleanup: missing container name" + return 1 + fi + + cleanup_dir="$CLEANUP/$name" + if [ ! -d "$cleanup_dir" ]; then + log "cleanup: no cleanup directory for $name, nothing to do" + return 0 + fi + + log "Cleaning up container instances for: $name" + + # Remove all container instances by ID from the cleanup directory + for id_file in "$cleanup_dir"/*; do + [ -f "$id_file" ] || continue + cid=$(basename "$id_file") + + # Extract image name and meta-sha256 from the container instance labels + img=$(podman inspect "$cid" 2>/dev/null | jq -r '.[].ImageName' 2>/dev/null || echo "") + sha=$(podman inspect "$cid" --format '{{index .Config.Labels "meta-sha256"}}' 2>/dev/null || echo "") + + log "Removing container instance with ID: ${cid:0:12}..." + podman rm -vif "$cid" >/dev/null 2>&1 + rm -f "$id_file" + + # Clean up the image if it exists and is not used by other containers + if [ -n "$img" ] && [ "$img" != "null" ]; then + if ! podman ps -a --format "{{.Image}}" | grep -q "^${img}$"; then + log "Removing unused image: $img" + podman rmi "$img" 2>/dev/null || true + + # Also remove archive and sidecar file from $DOWNLOADS for remote images + if [ -n "$sha" ] && [ -d "$DOWNLOADS" ]; then + # Search for matching sidecar file containing this SHA + for sha_file in "$DOWNLOADS"/*.sha256; do + [ -f "$sha_file" ] || continue + stored_sha=$(cat "$sha_file" 2>/dev/null || echo "") + if [ "$stored_sha" = "$sha" ]; then + # Found matching sidecar - derive archive filename + archive="${sha_file%.sha256}" + + if [ -f "$archive" ]; then + # Safety check: verify archive SHA matches before removing + archive_sha=$(sha256sum "$archive" 2>/dev/null | awk '{print $1}') + if [ "$archive_sha" = "$sha" ]; then + log "Removing archive: $archive" + rm -f "$archive" + else + log "Warning: archive SHA mismatch for $archive, not removing" + fi + fi + + log "Removing sidecar file: $sha_file" + rm -f "$sha_file" + break + fi + done + fi + else + log "Image $img still in use by other containers, not removing" + fi + fi + done + + # Remove the cleanup directory for this container, and parent if empty + rmdir "$cleanup_dir" 2>/dev/null + rmdir "$CLEANUP" 2>/dev/null - cnt=$(podman image prune -af | wc -l) - log "Pruned $cnt image(s)" + exit 0 } waitfor() @@ -466,7 +564,7 @@ netrestart() done } -cleanup() +atexit() { pidfile=$(pidfn "$name") @@ -674,7 +772,7 @@ if [ -n "$cmd" ]; then shift fi -trap cleanup INT HUP TERM +trap atexit INT HUP TERM case $cmd in # Does not work atm., cannot attach to TTY because @@ -682,6 +780,9 @@ case $cmd in # attach) # podman attach "$1" # ;; + cleanup) # Hidden from public view, use remove or flush commands instead + cleanup "$@" + ;; create) [ -n "$quiet" ] || log "Got create args: $*" create "$@" @@ -920,7 +1021,7 @@ case $cmd in rm -f "$pidfile" cnt=$(podman image prune -f | wc -l) - log "setup: pruned $cnt image(s)" + log "Pruned $cnt image(s)" ;; shell) if [ -z "$name" ]; then @@ -1060,8 +1161,8 @@ case $cmd in ;; cleanup) # Called as cleanup-script from Finit service - log "Calling $container -n $SERVICE_ID delete" - exec $container -q -n "$SERVICE_ID" delete + log "Calling $container -n $SERVICE_ID cleanup" + exec $container -q -n "$SERVICE_ID" cleanup ;; *) false diff --git a/src/confd/src/core.c b/src/confd/src/core.c index 49fb59fe7..719d62288 100644 --- a/src/confd/src/core.c +++ b/src/confd/src/core.c @@ -82,8 +82,12 @@ int core_post_hook(sr_session_ctx_t *session, uint32_t sub_id, const char *modul return SR_ERR_OK; } - if (systemf("initctl -b reload")) + if (systemf("initctl -b reload")) { + EMERG("initctl reload: failed applying new configuration!"); return SR_ERR_SYS; + } + + AUDIT("The new configuration has been applied."); return SR_ERR_OK; } diff --git a/src/confd/src/infix-containers.c b/src/confd/src/infix-containers.c index b4e37a896..61d777ea7 100644 --- a/src/confd/src/infix-containers.c +++ b/src/confd/src/infix-containers.c @@ -21,6 +21,7 @@ #define CFG_XPATH "/infix-containers:containers" #define _PATH_CONT "/run/containers" +#define _PATH_CLEAN "/var/lib/containers/cleanup" /* * Check if image is a local archive and return the offset to the file path. @@ -302,9 +303,34 @@ static int add(const char *name, struct lyd_node *cif) */ static int del(const char *name) { + char prune_dir[sizeof(_PATH_CLEAN) + strlen(name) + 3]; + char buf[256]; + FILE *pp; + erasef("%s/%s.sh", _PATH_CONT, name); systemf("initctl -bnq disable container@%s.conf", name); + /* Schedule a cleanup job for this container as soon as it has stopped */ + snprintf(prune_dir, sizeof(prune_dir), "%s/%s", _PATH_CLEAN, name); + systemf("mkdir -p %s", prune_dir); + + /* Finit cleanup:script runs when container is deleted, it will remove any image by-ID */ + pp = popenf("r", "podman inspect %s 2>/dev/null | jq -r '.[].Id' 2>/dev/null", name); + if (!pp) { + /* Nothing to do, if we can't get the Id we cannot safely remove anything */ + ERROR("Cannot find any container instance named '%s' to delete", name); + rmdir(prune_dir); + return SR_ERR_OK; + } + + if (fgets(buf, sizeof(buf), pp)) { + chomp(buf); + if (strlen(buf) > 2) + touchf("%s/%s", prune_dir, buf); + } + + pclose(pp); + return SR_ERR_OK; } From 096a26748700860eae06aa5f19a1f2b72c5810a0 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 10 Oct 2025 17:40:24 +0200 Subject: [PATCH 08/19] container: use 'nice' for podman load and create When loading big OCI images at boot the `podman load` process completely monopolizes all cores of an Arm Cortex-A72. It blocks on I/O, sure, but with 'nice' we can get some attention at least to more critical services at boot. Signed-off-by: Joachim Wiberg --- board/common/rootfs/usr/sbin/container | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/board/common/rootfs/usr/sbin/container b/board/common/rootfs/usr/sbin/container index bbebb43b1..1ee8ae7a5 100755 --- a/board/common/rootfs/usr/sbin/container +++ b/board/common/rootfs/usr/sbin/container @@ -211,7 +211,7 @@ load_archive() fi [ -n "$quiet" ] || log "Loading OCI image $dir ..." - output=$(podman load -qi "$dir") + output=$(nice podman load -qi "$dir") # Extract image ID from podman load output: # "Loaded image: sha256:cd9d0aaf81be..." @@ -342,7 +342,7 @@ create() # shellcheck disable=SC2048 log "podman create --name $name --conmon-pidfile=$pidfile $args $image $*" - if podman create --name "$name" --conmon-pidfile="$pidfile" $args "$image" $*; then + if nice podman create --name "$name" --conmon-pidfile="$pidfile" $args "$image" $*; then [ -n "$quiet" ] || log "Successfully created container $name from $image" [ -n "$manual" ] || start "$name" From 8b67d057ce1a99df8589274a199a367903fb0815 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 12 Oct 2025 10:18:49 +0200 Subject: [PATCH 09/19] container: make 'container remove' cli command slightly more useful Usually, when your system is up and running properly, you want to clean up anything unused from your previous experiments. This change alllows that by calling the interactive 'podman image prune -a -f' command from the CLI command 'container remove all' Signed-off-by: Joachim Wiberg --- board/common/rootfs/usr/sbin/container | 13 ++++++++++--- src/klish-plugin-infix/xml/containers.xml | 9 +++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/board/common/rootfs/usr/sbin/container b/board/common/rootfs/usr/sbin/container index 1ee8ae7a5..a8be65d3d 100755 --- a/board/common/rootfs/usr/sbin/container +++ b/board/common/rootfs/usr/sbin/container @@ -584,7 +584,7 @@ usage: container [opt] cmd [arg] options: - -a, --all Show all, of something + -a, --all Show all, do all, remove all, of something --dns NAMESERVER Set nameserver(s) when creating a container --dns-search LIST Set host lookup search list when creating container --cap-add CAP Add capability to unprivileged container @@ -624,7 +624,7 @@ commands: list [image | oci] List names (only) of containers, images, or OCI archives load [NAME | URL] NM Load OCI tarball fileNAME or URL to image NM locate Find container that currently owns '--net IFNAME' - remove IMAGE Remove an (unused) container image + remove [IMAGE] Remove an image, or prune unused images including OCI archives restart [network] NAME Restart a (crashed) container or container(s) using network run NAME [CMD] Run a container interactively, with an optional command save IMAGE FILE Save a container image to an OCI tarball FILE[.tar.gz] @@ -876,7 +876,14 @@ case $cmd in podman pull "$@" ;; remove) - podman rmi $all $force -i "$1" + if [ -n "$all" ]; then + log "Removing all OCI archives from $DOWNLOADS directory ..." + find "${DOWNLOADS:?}" -mindepth 1 -exec rm -rf {} + + log "Removing all unused container images ..." + podman image prune $all $force + else + podman rmi $force -i "$1" + fi ;; run) img=$1 diff --git a/src/klish-plugin-infix/xml/containers.xml b/src/klish-plugin-infix/xml/containers.xml index 23becbae7..4d697c8a6 100644 --- a/src/klish-plugin-infix/xml/containers.xml +++ b/src/klish-plugin-infix/xml/containers.xml @@ -102,9 +102,14 @@ - + + + - doas container remove $KLISH_PARAM_name + if [ -n "$KLISH_PARAM_name" ]; then + all=-a + fi + doas container $all remove $KLISH_PARAM_name From f40668364687fafbe761a6bfdfc8fa363f1c0636 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 12 Oct 2025 10:34:49 +0200 Subject: [PATCH 10/19] container: upgrade fixes for mutable images Container instances that run with mutable images, e.g., tagged with `:latest` or similar non-versioned tags, can be upgraded without changing the config. This commit fixes two issues found with this support: - force container image re-fetch on upgrade, even if the file exists locally - surgically remove old image from container store after upgrade Signed-off-by: Joachim Wiberg --- board/common/rootfs/usr/sbin/container | 113 ++++++++++++++---- .../infix_containers/container_volume/test.py | 2 +- 2 files changed, 89 insertions(+), 26 deletions(-) diff --git a/board/common/rootfs/usr/sbin/container b/board/common/rootfs/usr/sbin/container index a8be65d3d..77d9f9d55 100755 --- a/board/common/rootfs/usr/sbin/container +++ b/board/common/rootfs/usr/sbin/container @@ -80,10 +80,15 @@ fetch() cd "$DOWNLOADS" || return if [ -e "$file" ]; then - log "$file already available." - if check "$file"; then - echo "$dst" - return 0 + if [ -n "$force" ]; then + log "Force flag set, removing cached $file and re-downloading." + rm -f "$file" + else + log "$file already available." + if check "$file"; then + echo "$dst" + return 0 + fi fi fi @@ -140,7 +145,8 @@ load_archive() fi ;; *) # docker://*, docker-archive:*, or URL - if podman image exists "$img"; then + # Skip existence check if force flag is set (e.g., for upgrade command) + if [ -z "$force" ] && podman image exists "$img"; then echo "$img" return 0 fi @@ -594,7 +600,7 @@ options: -d, --detach Detach a container started with 'run IMG [CMD]' -e, --env FILE Environment variables when creating container --entrypoint Disable container image's ENTRYPOINT, run cmd + arg - -f, --force Force operation, e.g. remove + -f, --force Force operation, e.g. remove, or force image re-fetch -h, --help Show this help text --hostname NAME Set hostname when creating container --net NETWORK Network interface(s) when creating or finding container @@ -1120,32 +1126,89 @@ case $cmd in podman stats -i 2 ;; upgrade) - # Start script used to initially create container - script=/run/containers/${1}.sh + if [ -z "$name" ]; then + name="$1" + fi + script=/run/containers/${name}.sh - # Find container image - img=$(podman inspect "$1" | jq -r .[].ImageName) - if [ -z "$img" ]; then - echo "No such container ($1), or invalid ImageName. Cannot upgrade." - exit 1; + # Verify container exists + if ! podman container exists "$name"; then + echo "No such container: $name" + exit 1 fi - # Likely an OCI archive, or local directory, assume user has updated image. - if echo "$img" | grep -Eq '^localhost/'; then - file=$(awk '/^# meta-image:/ {print $3}' "$script") - echo ">> Upgrading container $1 using $file ..." - else - printf ">> Stopping ... " - podman stop "$1" - printf ">> " - podman pull "$img" || (echo "Failed fetching $img, check your network (settings)."; exit 1) - echo ">> Starting $1 ..." + # Check if container uses a mutable tag (e.g., :latest) + # Get the actual image name from the running container + image_name=$(podman inspect "$name" | jq -r '.[].ImageName') + if [ -z "$image_name" ]; then + echo "Cannot determine ImageName for container $name" + exit 1 + fi + + # Check if image uses :latest or other mutable tag + # Images with immutable digests (@sha256:...) don't benefit from upgrade + if echo "$image_name" | grep -qE '@sha256:'; then + echo "Container $name uses an immutable image digest ($image_name)" + echo "Upgrade will have no effect. Update the configuration to use a mutable tag." + exit 1 + fi + + # Get image URI from the container script + img=$(awk '/^# meta-image:/ {print $3}' "$script") + if [ -z "$img" ]; then + echo "Cannot determine image for container $name" + exit 1 fi + + echo ">> Upgrading container $name from image $img ..." + + # Capture the current image ID before upgrade so we can remove it after + old_image_id=$(podman inspect "$name" --format '{{.ImageID}}' 2>/dev/null) + + # Stop container using proper command (goes through Finit) + printf ">> Stopping container ... " + container stop "$name" + echo "done" + + # Set force flag to ensure fresh pull/fetch of image + force="-f" + + # For remote images, force re-pull + case "$img" in + docker://* | ftp://* | http://* | https://*) + printf ">> Pulling latest image ... " + if ! load_archive "$img" >/dev/null 2>&1; then + echo "failed" + echo "Failed fetching $img, check your network settings." + exit 1 + fi + echo "done" + ;; + *) + # Local archives - user must update the file manually + echo ">> Using local image $img (ensure file is updated) ..." + ;; + esac + + # Recreate container by running the script + echo ">> Recreating container ..." if ! "$script"; then - echo ">> Failed recreating container $1" + echo ">> Failed recreating container $name" exit 1 fi - echo ">> Done." + + # Remove the old image if it's not used by any other containers + if [ -n "$old_image_id" ]; then + # Check if the old image is still in use by any containers + if ! podman ps -a --format '{{.ImageID}}' | grep -q "^${old_image_id}$"; then + log "Removing old image $old_image_id" + podman rmi "$old_image_id" 2>/dev/null || true + else + log "Old image $old_image_id still in use by other containers, keeping it" + fi + fi + + echo ">> Container $name upgraded successfully." ;; volume) cmd=$1 diff --git a/test/case/infix_containers/container_volume/test.py b/test/case/infix_containers/container_volume/test.py index 95be8fe07..896b8ea85 100755 --- a/test/case/infix_containers/container_volume/test.py +++ b/test/case/infix_containers/container_volume/test.py @@ -57,7 +57,7 @@ with test.step("Upgrade container"): out = tgtssh.runsh(f"sudo container upgrade {NAME}") - if ">> Done." not in out.stdout: + if f">> Container {NAME} upgraded successfully." not in out.stdout: msg = f"Failed upgrading container {NAME}:\n" \ f"STDOUT:\n{out.stdout}\n" \ f"STDERR:\n{out.stderr}" From b4239b568d2cccefedb46975d519009b5a97d0b0 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 12 Oct 2025 11:02:47 +0200 Subject: [PATCH 11/19] container: remove old image when upgrading at startup One unique feature of Infix OS is that you can embed your OCI archive in the rootfs.squashfs. This way your container instance does not need to download anything from the network. To upgrade you drop in a new image and rebuild Infix, when the new Infix boots the container script 'setup' command will recognize that the OCI archive has changed and will reload it into the container store. This patch is an improvement of the way too generic 'podman image prune' command used previously. Instead of looking for any dangling image, we now surgically remove the old image. Signed-off-by: Joachim Wiberg --- board/common/rootfs/usr/sbin/container | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/board/common/rootfs/usr/sbin/container b/board/common/rootfs/usr/sbin/container index 77d9f9d55..9579b2510 100755 --- a/board/common/rootfs/usr/sbin/container +++ b/board/common/rootfs/usr/sbin/container @@ -997,6 +997,12 @@ case $cmd in fi fi + # Capture old image ID before recreation (for surgical cleanup after) + old_image_id="" + if podman container exists "$name"; then + old_image_id=$(podman inspect "$name" --format '{{.ImageID}}' 2>/dev/null) + 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}") @@ -1033,8 +1039,17 @@ case $cmd in fi rm -f "$pidfile" - cnt=$(podman image prune -f | wc -l) - log "Pruned $cnt image(s)" + + # Remove the old image if it's not used by any other containers + if [ -n "$old_image_id" ]; then + # Check if the old image is still in use by any containers + if ! podman ps -a --format '{{.ImageID}}' | grep -q "^${old_image_id}$"; then + log "Removing old image $old_image_id" + podman rmi "$old_image_id" 2>/dev/null || true + else + log "Old image $old_image_id still in use by other containers, keeping it" + fi + fi ;; shell) if [ -z "$name" ]; then From a89910bdb6b648c9be99ec554207d6301d117aab Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 12 Oct 2025 23:06:55 +0200 Subject: [PATCH 12/19] confd: add support for upgrade action (rpc) Already supported in the CLI. This makes it official, and quite handy for users that run mutable containers. Signed-off-by: Joachim Wiberg --- src/confd/src/infix-containers.c | 1 + src/confd/yang/confd/infix-containers.yang | 18 ++++++++++++++++-- ...9.yang => infix-containers@2025-10-12.yang} | 0 src/confd/yang/containers.inc | 2 +- 4 files changed, 18 insertions(+), 3 deletions(-) rename src/confd/yang/confd/{infix-containers@2025-09-09.yang => infix-containers@2025-10-12.yang} (100%) diff --git a/src/confd/src/infix-containers.c b/src/confd/src/infix-containers.c index 61d777ea7..655d487be 100644 --- a/src/confd/src/infix-containers.c +++ b/src/confd/src/infix-containers.c @@ -477,6 +477,7 @@ int infix_containers_init(struct confd *confd) REGISTER_RPC(confd->session, CFG_XPATH "/container/start", action, NULL, &confd->sub); REGISTER_RPC(confd->session, CFG_XPATH "/container/stop", action, NULL, &confd->sub); REGISTER_RPC(confd->session, CFG_XPATH "/container/restart", action, NULL, &confd->sub); + REGISTER_RPC(confd->session, CFG_XPATH "/container/upgrade", action, NULL, &confd->sub); REGISTER_RPC(confd->session, "/infix-containers:oci-load", oci_load, NULL, &confd->sub); return SR_ERR_OK; diff --git a/src/confd/yang/confd/infix-containers.yang b/src/confd/yang/confd/infix-containers.yang index b4c90adaf..07746d0eb 100644 --- a/src/confd/yang/confd/infix-containers.yang +++ b/src/confd/yang/confd/infix-containers.yang @@ -22,8 +22,10 @@ module infix-containers { prefix infix-sys; } - revision 2025-09-09 { - description "Add dedicated 'ident' type for container and volume names."; + revision 2025-10-12 { + description "Two major changes: + - Add dedicated 'ident' type for container and volume names. + - New upgrade action (RPC) for containers with :latest image."; reference "internal"; } @@ -484,6 +486,18 @@ module infix-containers { action restart { description "Restart a running, or start, a stopped container."; } + + action upgrade { + description "Upgrade container to latest version of its image. + + This action fetches the latest version of the container's image + and recreates the container with the new image. Any named volumes + are preserved. + + Note: This action is primarily for containers using mutable tags + like ':latest'. For containers using immutable tags (e.g., + ':25.06.0'), the upgrade action will have no effect."; + } } } diff --git a/src/confd/yang/confd/infix-containers@2025-09-09.yang b/src/confd/yang/confd/infix-containers@2025-10-12.yang similarity index 100% rename from src/confd/yang/confd/infix-containers@2025-09-09.yang rename to src/confd/yang/confd/infix-containers@2025-10-12.yang diff --git a/src/confd/yang/containers.inc b/src/confd/yang/containers.inc index b9199119b..11dfc5bc5 100644 --- a/src/confd/yang/containers.inc +++ b/src/confd/yang/containers.inc @@ -1,5 +1,5 @@ # -*- sh -*- MODULES=( "infix-interfaces -e containers" - "infix-containers@2025-09-09.yang" + "infix-containers@2025-10-12.yang" ) From 3e01df9e471368da2399d883611dea3426844178 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 13 Oct 2025 08:19:24 +0200 Subject: [PATCH 13/19] test: add support for 'make V=1 test-spec' to debug Signed-off-by: Joachim Wiberg --- test/test.mk | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/test.mk b/test/test.mk index 78e74a5bc..85f3f4bba 100644 --- a/test/test.mk +++ b/test/test.mk @@ -36,11 +36,18 @@ test: test-sh: $(test-dir)/env $(base) $(mode) $(binaries) $(pkg-$(ARCH)) -i /bin/sh +SPEC_DEBUG := +SPEC_Q := @ +ifeq ($(V),1) +SPEC_DEBUG := -d +SPEC_Q := +endif + test-spec: @esc_infix_name="$(echo $(INFIX_NAME) | sed 's/\//\\\//g')"; \ sed 's/{REPLACE}/$(subst ",,$(esc_infix_name)) $(INFIX_VERSION)/' \ $(spec-dir)/Readme.adoc.in > $(spec-dir)/Readme.adoc - @$(spec-dir)/generate_spec.py -s $(test-dir)/case/all.yaml -r $(BR2_EXTERNAL_INFIX_PATH) + $(SPEC_Q)$(spec-dir)/generate_spec.py -s $(test-dir)/case/all.yaml -r $(BR2_EXTERNAL_INFIX_PATH) $(SPEC_DEBUG) @asciidoctor-pdf --failure-level INFO --theme $(spec-dir)/theme.yml \ -a logo="image:$(LOGO)" \ -a pdf-fontsdir=$(spec-dir)/fonts \ From 08b2a7e1dd5c2cab3d4b501ae174e21b364fe128 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 12 Oct 2025 23:08:26 +0200 Subject: [PATCH 14/19] test: verify container upgrade This commit adds four (small) container images to the Infamy test container which are used in the new container upgrade test. The test verifies that a mutable container can be upgraded and that old images are properly cleaned up from the container store. Fixes #624 Signed-off-by: Joachim Wiberg --- test/.env | 2 +- test/case/infix_containers/Readme.adoc | 1 + test/case/infix_containers/all.yaml | 3 + .../case/infix_containers/upgrade/Readme.adoc | 1 + .../case/infix_containers/upgrade/download.sh | 40 ++++ test/case/infix_containers/upgrade/test.adoc | 34 ++++ test/case/infix_containers/upgrade/test.py | 182 ++++++++++++++++++ .../infix_containers/upgrade/topology.dot | 24 +++ .../infix_containers/upgrade/topology.svg | 42 ++++ test/docker/Dockerfile | 16 +- test/docker/README.md | 3 +- 11 files changed, 341 insertions(+), 7 deletions(-) create mode 120000 test/case/infix_containers/upgrade/Readme.adoc create mode 100755 test/case/infix_containers/upgrade/download.sh create mode 100644 test/case/infix_containers/upgrade/test.adoc create mode 100755 test/case/infix_containers/upgrade/test.py create mode 100644 test/case/infix_containers/upgrade/topology.dot create mode 100644 test/case/infix_containers/upgrade/topology.svg diff --git a/test/.env b/test/.env index 962afdf37..b6968427f 100644 --- a/test/.env +++ b/test/.env @@ -2,7 +2,7 @@ # shellcheck disable=SC2034,SC2154 # Current container image -INFIX_TEST=ghcr.io/kernelkit/infix-test:2.5 +INFIX_TEST=ghcr.io/kernelkit/infix-test:2.6 ixdir=$(readlink -f "$testdir/..") logdir=$(readlink -f "$testdir/.log") diff --git a/test/case/infix_containers/Readme.adoc b/test/case/infix_containers/Readme.adoc index 726d2c1a1..7fe48f874 100644 --- a/test/case/infix_containers/Readme.adoc +++ b/test/case/infix_containers/Readme.adoc @@ -10,6 +10,7 @@ Tests verifying Infix Docker container support: - Connecting containers with VETH pairs to standard Linux bridges - Assigning physical Ethernet interfaces to containers - Container upgrades with persistent volume data + - Container upgrade using RPC with cleanup of old image - Firewall container running in host network mode with full privileges include::container_basic/Readme.adoc[] diff --git a/test/case/infix_containers/all.yaml b/test/case/infix_containers/all.yaml index 53ec29154..41a0a7585 100644 --- a/test/case/infix_containers/all.yaml +++ b/test/case/infix_containers/all.yaml @@ -21,6 +21,9 @@ - name: Container Volume Persistence case: container_volume/test.py +- name: Container Upgrade + case: upgrade/test.py + - name: Basic Firewall Container case: container_firewall_basic/test.py diff --git a/test/case/infix_containers/upgrade/Readme.adoc b/test/case/infix_containers/upgrade/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/infix_containers/upgrade/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/infix_containers/upgrade/download.sh b/test/case/infix_containers/upgrade/download.sh new file mode 100755 index 000000000..7bd28b1a5 --- /dev/null +++ b/test/case/infix_containers/upgrade/download.sh @@ -0,0 +1,40 @@ +#!/bin/sh +# Download curios-httpd container images for upgrade testing +# This script is called during Docker image build to pre-populate +# the test container with the necessary OCI archives. + +set -e + +DEST_DIR="${1:-/srv}" +IMAGE_BASE="ghcr.io/kernelkit/curios-httpd" +VERSIONS="24.05.0 24.11.0" +ARCHS="linux/amd64 linux/arm64" + +echo "Downloading curios-httpd images to $DEST_DIR..." +mkdir -p "$DEST_DIR" + +for ver in $VERSIONS; do + for arch in $ARCHS; do + # Create architecture-specific filename + arch_suffix=$(echo "$arch" | sed 's|linux/||') + output="$DEST_DIR/curios-httpd-${ver}-${arch_suffix}.tar" + + echo "Fetching ${IMAGE_BASE}:${ver} for ${arch}..." + skopeo copy --override-arch "${arch#linux/}" \ + "docker://${IMAGE_BASE}:${ver}" \ + "oci-archive:${output}" + + # Check if already gzipped and compress if needed + output_gz="${output}.gz" + if file "$output" | grep -q "gzip compressed"; then + echo "File ${output} is already gzipped, renaming..." + mv "$output" "$output_gz" + else + echo "Compressing ${output}..." + gzip "${output}" + fi + done +done + +echo "Download complete!" +ls -lh "$DEST_DIR" diff --git a/test/case/infix_containers/upgrade/test.adoc b/test/case/infix_containers/upgrade/test.adoc new file mode 100644 index 000000000..59c249406 --- /dev/null +++ b/test/case/infix_containers/upgrade/test.adoc @@ -0,0 +1,34 @@ +=== Container Upgrade + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/upgrade] + +==== Description + +Verify container upgrade functionality by testing the optimization +that skips recreation when the container configuration hasn't changed. + +This test uses two versions of curios-httpd (24.05.0 and 24.11.0) served +over HTTP with :latest tag. The test verifies that: +1. Container starts successfully from the :latest image +2. When the :latest tag points to a new version, upgrade is triggered +3. The container runs the new version after upgrade + +==== Topology + +image::topology.svg[Container Upgrade topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUT +. Detect target architecture +. Set up isolated network and file server +. Create symlink for curios-httpd:latest -> 24.05.0 +. Create container 'web' from curios-httpd:latest (24.05.0) +. Verify container 'web' has started with version 24.05.0 +. Update symlink to point to curios-httpd:24.11.0 +. Trigger container upgrade by calling upgrade action +. Wait for container 'web' to complete uprgade +. Verify container 'web' is running new version 24.11.0 +. Verify old image was pruned and disk usage is reasonable + + diff --git a/test/case/infix_containers/upgrade/test.py b/test/case/infix_containers/upgrade/test.py new file mode 100755 index 000000000..7193c87ee --- /dev/null +++ b/test/case/infix_containers/upgrade/test.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""Container Upgrade + +Verify container upgrade functionality by testing the optimization +that skips recreation when the container configuration hasn't changed. + +This test uses two versions of curios-httpd (24.05.0 and 24.11.0) served +over HTTP with :latest tag. The test verifies that: +1. Container starts successfully from the :latest image +2. When the :latest tag points to a new version, upgrade is triggered +3. The container runs the new version after upgrade +""" +import os +import time +import infamy +import infamy.file_server as srv +from infamy.util import until + +SRVPORT = 8008 +SRVDIR = "/srv" + +with infamy.Test() as test: + NAME = "web" + + with test.step("Set up topology and attach to target DUT"): + env = infamy.Env() + target = env.attach("target", "mgmt") + tgtssh = env.attach("target", "mgmt", "ssh") + + if not target.has_model("infix-containers"): + test.skip() + + _, hport = env.ltop.xlate("host", "data") + _, tport = env.ltop.xlate("target", "data") + + with test.step("Detect target architecture"): + # Query operational datastore for machine architecture + system_state = target.get_data("/ietf-system:system-state") + arch = system_state["system-state"]["platform"]["machine"] + + # Map kernel arch to our image naming + arch_map = { + "x86_64": "amd64", + "aarch64": "arm64", + "armv7l": "arm64", # Fallback for ARM variants + } + image_arch = arch_map.get(arch, "amd64") + print(f"Detected architecture: {arch} -> using {image_arch} images") + + with test.step("Set up isolated network and file server"): + netns = infamy.IsolatedMacVlan(hport).start() + netns.addip("192.168.0.1") + + target.put_config_dicts({ + "ietf-interfaces": { + "interfaces": { + "interface": [ + { + "name": tport, + "ipv4": { + "address": [ + { + "ip": "192.168.0.2", + "prefix-length": 24 + } + ] + } + } + ] + } + } + }) + netns.must_reach("192.168.0.2") + + with srv.FileServer(netns, "192.168.0.1", SRVPORT, SRVDIR): + with test.step("Create symlink for curios-httpd:latest -> 24.05.0"): + # Create symlink in the file server directory + old_img = f"{SRVDIR}/curios-httpd-24.05.0-{image_arch}.tar.gz" + new_img = f"{SRVDIR}/curios-httpd-24.11.0-{image_arch}.tar.gz" + latest_link = f"{SRVDIR}/curios-httpd-latest.tar.gz" + + # Remove any existing symlink + if os.path.exists(latest_link): + os.unlink(latest_link) + + # Point to old version + os.symlink(old_img, latest_link) + print(f"Created symlink: {latest_link} -> {old_img}") + + with test.step("Create container 'web' from curios-httpd:latest (24.05.0)"): + target.put_config_dict("infix-containers", { + "containers": { + "container": [ + { + "name": f"{NAME}", + "image": f"http://192.168.0.1:{SRVPORT}/curios-httpd-latest.tar.gz", + "command": "/usr/sbin/httpd -f -v -p 91", + "network": { + "host": True + } + } + ] + } + }) + + with test.step("Verify container 'web' has started with version 24.05.0"): + c = infamy.Container(target) + until(lambda: c.running(NAME), attempts=60) + + # Get initial operational data to capture container ID and image ID + containers_data = target.get_data("/infix-containers:containers") + container = containers_data["containers"]["container"][NAME] + initial_container_id = container["id"] + initial_image_id = container["image-id"] + + print(f"Container started with container-id: {initial_container_id[:12]}...") + print(f" and image-id: {initial_image_id[:12]}...") + + # Get baseline disk usage for /var/lib/containers + result = tgtssh.runsh("doas du -s /var/lib/containers") + initial_disk_usage = int(result.stdout.split()[0]) + print(f"Disk usage after initial creation: {initial_disk_usage} KiB") + + with test.step("Update symlink to point to curios-httpd:24.11.0"): + # Remove old symlink and point to new version + os.unlink(latest_link) + os.symlink(new_img, latest_link) + print(f"Updated symlink: {latest_link} -> {new_img}") + + with test.step("Trigger container upgrade by calling upgrade action"): + c = infamy.Container(target) + c.action(NAME, "upgrade") + + with test.step("Wait for container 'web' to complete uprgade"): + time.sleep(3) + until(lambda: c.running(NAME), attempts=30) + + with test.step("Verify container 'web' is running new version 24.11.0"): + c = infamy.Container(target) + # Wait for upgrade to complete and container to restart + until(lambda: c.running(NAME), attempts=60) + + # Get operational data after upgrade + containers_data = target.get_data("/infix-containers:containers") + container = containers_data["containers"]["container"][NAME] + new_container_id = container["id"] + new_image_id = container["image-id"] + + print(f"After upgrade container-id: {new_container_id[:12]}...") + print(f" image-id: {new_image_id[:12]}...") + + # Verify that both IDs have changed + if new_container_id == initial_container_id: + test.fail("Container ID did not change after upgrade!") + if new_image_id == initial_image_id: + test.fail("Image ID did not change after upgrade!") + + print("✓ Both container ID and image ID changed after upgrade") + + with test.step("Verify old image was pruned and disk usage is reasonable"): + # We expect minimal growth — the new image replaces old + # image, and they are each around 500-600 KiB. We allow for + # some overhead, but should be < 200 KiB. If the old image + # is not pruned properly, we'll see ~600 KiB extra growth. + MAX_ACCEPTABLE_GROWTH = 1000 + + # Get disk usage after upgrade + result = tgtssh.runsh("doas du -s /var/lib/containers") + final_disk_usage = int(result.stdout.split()[0]) + print(f"Disk usage after upgrade: {final_disk_usage} KiB") + + # Calculate the difference + disk_growth = final_disk_usage - initial_disk_usage + print(f"Disk usage growth: {disk_growth} KiB") + + if disk_growth > MAX_ACCEPTABLE_GROWTH: + test.fail(f"Disk usage grew by {disk_growth} KiB (expected < {MAX_ACCEPTABLE_GROWTH} KiB). " + f"Old image may not have been pruned!") + + print(f"✓ Disk usage growth is acceptable ({disk_growth} KiB < {MAX_ACCEPTABLE_GROWTH} KiB)") + + test.succeed() diff --git a/test/case/infix_containers/upgrade/topology.dot b/test/case/infix_containers/upgrade/topology.dot new file mode 100644 index 000000000..ebb673d5f --- /dev/null +++ b/test/case/infix_containers/upgrade/topology.dot @@ -0,0 +1,24 @@ +graph "1x2" { + layout="neato"; + overlap="false"; + esep="+80"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="host | { mgmt | data }", + pos="0,12!", + requires="controller", + ]; + + target [ + label="{ mgmt | data } | target", + pos="10,12!", + + requires="infix", + ]; + + host:mgmt -- target:mgmt [requires="mgmt", color=lightgrey] + host:data -- target:data [color=black] +} diff --git a/test/case/infix_containers/upgrade/topology.svg b/test/case/infix_containers/upgrade/topology.svg new file mode 100644 index 000000000..ff3d246be --- /dev/null +++ b/test/case/infix_containers/upgrade/topology.svg @@ -0,0 +1,42 @@ + + + + + + +1x2 + + + +host + +host + +mgmt + +data + + + +target + +mgmt + +data + +target + + + +host:mgmt--target:mgmt + + + + +host:data--target:data + + + + diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile index 81a07e081..78aa3a8be 100644 --- a/test/docker/Dockerfile +++ b/test/docker/Dockerfile @@ -8,6 +8,7 @@ RUN apk add --no-cache \ e2tools \ ethtool \ fakeroot \ + file \ gcc \ git \ graphviz \ @@ -25,6 +26,7 @@ RUN apk add --no-cache \ qemu-img \ qemu-system-x86_64 \ ruby-mustache \ + skopeo \ socat \ squashfs-tools \ sshpass \ @@ -38,20 +40,24 @@ RUN cd /tmp/mtools-$MTOOL_VERSION && make && make install # Alpine's QEMU package does not bundle this for some reason, copied # from Ubuntu -COPY qemu-ifup /etc +COPY docker/qemu-ifup /etc # Needed to let qeneth find mustache(1) ENV PATH="${PATH}:/usr/lib/ruby/gems/3.2.0/bin" # Install all python packages used by the tests -COPY init-venv.sh /root -COPY pip-requirements.txt /root +COPY docker/init-venv.sh /root +COPY docker/pip-requirements.txt /root # Add bootstrap YANG models, the rest will be downloaded from the device -ADD yang /root/yang +ADD docker/yang /root/yang RUN ~/init-venv.sh ~/pip-requirements.txt -COPY entrypoint.sh /entrypoint.sh +# Download container images for upgrade testing +COPY case/infix_containers/upgrade/download.sh /tmp/download.sh +RUN /tmp/download.sh /srv && rm /tmp/download.sh + +COPY docker/entrypoint.sh /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] CMD ["/bin/sh"] diff --git a/test/docker/README.md b/test/docker/README.md index 83b09004a..c28b084c6 100644 --- a/test/docker/README.md +++ b/test/docker/README.md @@ -16,7 +16,8 @@ the image, e.g., with missing Alpine packages. here: in this example we use version 0.4: - docker build -t ghcr.io/kernelkit/infix-test:0.4 . + cd test/ + docker build -f docker/Dockerfile -t ghcr.io/kernelkit/infix-test:0.4 . 3. Update the `test/.env` file to use the new version 4. Verify your new image works properly (remember to remove your `~/.infix/venv`) From 5bf35b5a3f20711de64e532b9c0908dffc95a208 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 13 Oct 2025 06:43:34 +0200 Subject: [PATCH 15/19] test: remove redundant container_ prefix from test directories Rename test directories in infix_containers/ to remove the redundant 'container_' prefix since they already live under infix_containers/: container_basic -> basic container_bridge -> bridge container_enabled -> enabled container_environment -> environment container_firewall_basic -> firewall_basic container_host_commands -> host_commands container_phys -> phys container_veth -> veth container_volume -> volume Also update references in all.yaml and Readme.adoc files. Signed-off-by: Joachim Wiberg --- test/case/infix_containers/Readme.adoc | 18 +++++++++--------- test/case/infix_containers/all.yaml | 18 +++++++++--------- .../{container_basic => basic}/Readme.adoc | 0 .../{container_basic => basic}/test.adoc | 2 +- .../{container_basic => basic}/test.py | 0 .../{container_basic => basic}/topology.dot | 0 .../{container_basic => basic}/topology.svg | 0 .../{container_bridge => bridge}/Readme.adoc | 0 .../{container_bridge => bridge}/test.adoc | 2 +- .../{container_bridge => bridge}/test.py | 0 .../{container_bridge => bridge}/topology.dot | 0 .../{container_bridge => bridge}/topology.svg | 0 .../container_phys/topology.dot | 1 - .../container_veth/topology.dot | 1 - .../{container_enabled => enabled}/Readme.adoc | 0 .../{container_enabled => enabled}/test.adoc | 2 +- .../{container_enabled => enabled}/test.py | 0 .../topology.dot | 0 .../topology.svg | 0 .../Readme.adoc | 0 .../test.adoc | 2 +- .../test.py | 0 .../topology.dot | 0 .../topology.svg | 0 .../Readme.adoc | 0 .../test.adoc | 2 +- .../test.py | 0 .../topology.dot | 0 .../topology.svg | 0 .../Readme.adoc | 0 .../test.adoc | 2 +- .../test.py | 0 .../topology.dot | 0 .../topology.svg | 0 .../{container_phys => phys}/Readme.adoc | 0 .../{container_phys => phys}/test.adoc | 2 +- .../{container_phys => phys}/test.py | 0 test/case/infix_containers/phys/topology.dot | 1 + .../{container_phys => phys}/topology.svg | 0 .../{container_veth => veth}/Readme.adoc | 0 .../network_diagram.svg | 0 .../{container_veth => veth}/test.adoc | 2 +- .../{container_veth => veth}/test.py | 0 test/case/infix_containers/veth/topology.dot | 1 + .../{container_veth => veth}/topology.svg | 0 .../{container_volume => volume}/Readme.adoc | 0 .../{container_volume => volume}/test.adoc | 2 +- .../{container_volume => volume}/test.py | 0 .../{container_volume => volume}/topology.dot | 0 .../{container_volume => volume}/topology.svg | 0 50 files changed, 29 insertions(+), 29 deletions(-) rename test/case/infix_containers/{container_basic => basic}/Readme.adoc (100%) rename test/case/infix_containers/{container_basic => basic}/test.adoc (97%) rename test/case/infix_containers/{container_basic => basic}/test.py (100%) rename test/case/infix_containers/{container_basic => basic}/topology.dot (100%) rename test/case/infix_containers/{container_basic => basic}/topology.svg (100%) rename test/case/infix_containers/{container_bridge => bridge}/Readme.adoc (100%) rename test/case/infix_containers/{container_bridge => bridge}/test.adoc (97%) rename test/case/infix_containers/{container_bridge => bridge}/test.py (100%) rename test/case/infix_containers/{container_bridge => bridge}/topology.dot (100%) rename test/case/infix_containers/{container_bridge => bridge}/topology.svg (100%) delete mode 120000 test/case/infix_containers/container_phys/topology.dot delete mode 120000 test/case/infix_containers/container_veth/topology.dot rename test/case/infix_containers/{container_enabled => enabled}/Readme.adoc (100%) rename test/case/infix_containers/{container_enabled => enabled}/test.adoc (97%) rename test/case/infix_containers/{container_enabled => enabled}/test.py (100%) rename test/case/infix_containers/{container_enabled => enabled}/topology.dot (100%) rename test/case/infix_containers/{container_enabled => enabled}/topology.svg (100%) rename test/case/infix_containers/{container_environment => environment}/Readme.adoc (100%) rename test/case/infix_containers/{container_environment => environment}/test.adoc (96%) rename test/case/infix_containers/{container_environment => environment}/test.py (100%) rename test/case/infix_containers/{container_environment => environment}/topology.dot (100%) rename test/case/infix_containers/{container_environment => environment}/topology.svg (100%) rename test/case/infix_containers/{container_firewall_basic => firewall_basic}/Readme.adoc (100%) rename test/case/infix_containers/{container_firewall_basic => firewall_basic}/test.adoc (98%) rename test/case/infix_containers/{container_firewall_basic => firewall_basic}/test.py (100%) rename test/case/infix_containers/{container_firewall_basic => firewall_basic}/topology.dot (100%) rename test/case/infix_containers/{container_firewall_basic => firewall_basic}/topology.svg (100%) rename test/case/infix_containers/{container_host_commands => host_commands}/Readme.adoc (100%) rename test/case/infix_containers/{container_host_commands => host_commands}/test.adoc (96%) rename test/case/infix_containers/{container_host_commands => host_commands}/test.py (100%) rename test/case/infix_containers/{container_host_commands => host_commands}/topology.dot (100%) rename test/case/infix_containers/{container_host_commands => host_commands}/topology.svg (100%) rename test/case/infix_containers/{container_phys => phys}/Readme.adoc (100%) rename test/case/infix_containers/{container_phys => phys}/test.adoc (97%) rename test/case/infix_containers/{container_phys => phys}/test.py (100%) create mode 120000 test/case/infix_containers/phys/topology.dot rename test/case/infix_containers/{container_phys => phys}/topology.svg (100%) rename test/case/infix_containers/{container_veth => veth}/Readme.adoc (100%) rename test/case/infix_containers/{container_veth => veth}/network_diagram.svg (100%) rename test/case/infix_containers/{container_veth => veth}/test.adoc (98%) rename test/case/infix_containers/{container_veth => veth}/test.py (100%) create mode 120000 test/case/infix_containers/veth/topology.dot rename test/case/infix_containers/{container_veth => veth}/topology.svg (100%) rename test/case/infix_containers/{container_volume => volume}/Readme.adoc (100%) rename test/case/infix_containers/{container_volume => volume}/test.adoc (97%) rename test/case/infix_containers/{container_volume => volume}/test.py (100%) rename test/case/infix_containers/{container_volume => volume}/topology.dot (100%) rename test/case/infix_containers/{container_volume => volume}/topology.svg (100%) diff --git a/test/case/infix_containers/Readme.adoc b/test/case/infix_containers/Readme.adoc index 7fe48f874..1a4c3a6cf 100644 --- a/test/case/infix_containers/Readme.adoc +++ b/test/case/infix_containers/Readme.adoc @@ -13,36 +13,36 @@ Tests verifying Infix Docker container support: - Container upgrade using RPC with cleanup of old image - Firewall container running in host network mode with full privileges -include::container_basic/Readme.adoc[] +include::basic/Readme.adoc[] <<< -include::container_enabled/Readme.adoc[] +include::enabled/Readme.adoc[] <<< -include::container_environment/Readme.adoc[] +include::environment/Readme.adoc[] <<< -include::container_bridge/Readme.adoc[] +include::bridge/Readme.adoc[] <<< -include::container_phys/Readme.adoc[] +include::phys/Readme.adoc[] <<< -include::container_veth/Readme.adoc[] +include::veth/Readme.adoc[] <<< -include::container_volume/Readme.adoc[] +include::volume/Readme.adoc[] <<< -include::container_firewall_basic/Readme.adoc[] +include::firewall_basic/Readme.adoc[] <<< -include::container_host_commands/Readme.adoc[] +include::host_commands/Readme.adoc[] diff --git a/test/case/infix_containers/all.yaml b/test/case/infix_containers/all.yaml index 41a0a7585..d50db46ab 100644 --- a/test/case/infix_containers/all.yaml +++ b/test/case/infix_containers/all.yaml @@ -1,31 +1,31 @@ --- - name: Container basic - case: container_basic/test.py + case: basic/test.py - name: Container enabled/disabled - case: container_enabled/test.py + case: enabled/test.py - name: Container environment variables - case: container_environment/test.py + case: environment/test.py - name: Container with bridge network - case: container_bridge/test.py + case: bridge/test.py - name: Container with physical interface - case: container_phys/test.py + case: phys/test.py - name: Container with VETH pair - case: container_veth/test.py + case: veth/test.py - name: Container Volume Persistence - case: container_volume/test.py + case: volume/test.py - name: Container Upgrade case: upgrade/test.py - name: Basic Firewall Container - case: container_firewall_basic/test.py + case: firewall_basic/test.py - name: Host Command Execution from Container - case: container_host_commands/test.py + case: host_commands/test.py diff --git a/test/case/infix_containers/container_basic/Readme.adoc b/test/case/infix_containers/basic/Readme.adoc similarity index 100% rename from test/case/infix_containers/container_basic/Readme.adoc rename to test/case/infix_containers/basic/Readme.adoc diff --git a/test/case/infix_containers/container_basic/test.adoc b/test/case/infix_containers/basic/test.adoc similarity index 97% rename from test/case/infix_containers/container_basic/test.adoc rename to test/case/infix_containers/basic/test.adoc index 8205adbb8..e76cd18cd 100644 --- a/test/case/infix_containers/container_basic/test.adoc +++ b/test/case/infix_containers/basic/test.adoc @@ -1,6 +1,6 @@ === Container basic -ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/container_basic] +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/basic] ==== Description diff --git a/test/case/infix_containers/container_basic/test.py b/test/case/infix_containers/basic/test.py similarity index 100% rename from test/case/infix_containers/container_basic/test.py rename to test/case/infix_containers/basic/test.py diff --git a/test/case/infix_containers/container_basic/topology.dot b/test/case/infix_containers/basic/topology.dot similarity index 100% rename from test/case/infix_containers/container_basic/topology.dot rename to test/case/infix_containers/basic/topology.dot diff --git a/test/case/infix_containers/container_basic/topology.svg b/test/case/infix_containers/basic/topology.svg similarity index 100% rename from test/case/infix_containers/container_basic/topology.svg rename to test/case/infix_containers/basic/topology.svg diff --git a/test/case/infix_containers/container_bridge/Readme.adoc b/test/case/infix_containers/bridge/Readme.adoc similarity index 100% rename from test/case/infix_containers/container_bridge/Readme.adoc rename to test/case/infix_containers/bridge/Readme.adoc diff --git a/test/case/infix_containers/container_bridge/test.adoc b/test/case/infix_containers/bridge/test.adoc similarity index 97% rename from test/case/infix_containers/container_bridge/test.adoc rename to test/case/infix_containers/bridge/test.adoc index faf9160f4..c50ff86fa 100644 --- a/test/case/infix_containers/container_bridge/test.adoc +++ b/test/case/infix_containers/bridge/test.adoc @@ -1,6 +1,6 @@ === Container with bridge network -ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/container_bridge] +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/bridge] ==== Description diff --git a/test/case/infix_containers/container_bridge/test.py b/test/case/infix_containers/bridge/test.py similarity index 100% rename from test/case/infix_containers/container_bridge/test.py rename to test/case/infix_containers/bridge/test.py diff --git a/test/case/infix_containers/container_bridge/topology.dot b/test/case/infix_containers/bridge/topology.dot similarity index 100% rename from test/case/infix_containers/container_bridge/topology.dot rename to test/case/infix_containers/bridge/topology.dot diff --git a/test/case/infix_containers/container_bridge/topology.svg b/test/case/infix_containers/bridge/topology.svg similarity index 100% rename from test/case/infix_containers/container_bridge/topology.svg rename to test/case/infix_containers/bridge/topology.svg diff --git a/test/case/infix_containers/container_phys/topology.dot b/test/case/infix_containers/container_phys/topology.dot deleted file mode 120000 index 5130457b5..000000000 --- a/test/case/infix_containers/container_phys/topology.dot +++ /dev/null @@ -1 +0,0 @@ -../container_bridge/topology.dot \ No newline at end of file diff --git a/test/case/infix_containers/container_veth/topology.dot b/test/case/infix_containers/container_veth/topology.dot deleted file mode 120000 index 5130457b5..000000000 --- a/test/case/infix_containers/container_veth/topology.dot +++ /dev/null @@ -1 +0,0 @@ -../container_bridge/topology.dot \ No newline at end of file diff --git a/test/case/infix_containers/container_enabled/Readme.adoc b/test/case/infix_containers/enabled/Readme.adoc similarity index 100% rename from test/case/infix_containers/container_enabled/Readme.adoc rename to test/case/infix_containers/enabled/Readme.adoc diff --git a/test/case/infix_containers/container_enabled/test.adoc b/test/case/infix_containers/enabled/test.adoc similarity index 97% rename from test/case/infix_containers/container_enabled/test.adoc rename to test/case/infix_containers/enabled/test.adoc index 047b08c8b..ea10606ae 100644 --- a/test/case/infix_containers/container_enabled/test.adoc +++ b/test/case/infix_containers/enabled/test.adoc @@ -1,6 +1,6 @@ === Container enabled/disabled -ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/container_enabled] +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/enabled] ==== Description diff --git a/test/case/infix_containers/container_enabled/test.py b/test/case/infix_containers/enabled/test.py similarity index 100% rename from test/case/infix_containers/container_enabled/test.py rename to test/case/infix_containers/enabled/test.py diff --git a/test/case/infix_containers/container_enabled/topology.dot b/test/case/infix_containers/enabled/topology.dot similarity index 100% rename from test/case/infix_containers/container_enabled/topology.dot rename to test/case/infix_containers/enabled/topology.dot diff --git a/test/case/infix_containers/container_enabled/topology.svg b/test/case/infix_containers/enabled/topology.svg similarity index 100% rename from test/case/infix_containers/container_enabled/topology.svg rename to test/case/infix_containers/enabled/topology.svg diff --git a/test/case/infix_containers/container_environment/Readme.adoc b/test/case/infix_containers/environment/Readme.adoc similarity index 100% rename from test/case/infix_containers/container_environment/Readme.adoc rename to test/case/infix_containers/environment/Readme.adoc diff --git a/test/case/infix_containers/container_environment/test.adoc b/test/case/infix_containers/environment/test.adoc similarity index 96% rename from test/case/infix_containers/container_environment/test.adoc rename to test/case/infix_containers/environment/test.adoc index 5e67da5ab..2de6e5c42 100644 --- a/test/case/infix_containers/container_environment/test.adoc +++ b/test/case/infix_containers/environment/test.adoc @@ -1,6 +1,6 @@ === Container environment variables -ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/container_environment] +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/environment] ==== Description diff --git a/test/case/infix_containers/container_environment/test.py b/test/case/infix_containers/environment/test.py similarity index 100% rename from test/case/infix_containers/container_environment/test.py rename to test/case/infix_containers/environment/test.py diff --git a/test/case/infix_containers/container_environment/topology.dot b/test/case/infix_containers/environment/topology.dot similarity index 100% rename from test/case/infix_containers/container_environment/topology.dot rename to test/case/infix_containers/environment/topology.dot diff --git a/test/case/infix_containers/container_environment/topology.svg b/test/case/infix_containers/environment/topology.svg similarity index 100% rename from test/case/infix_containers/container_environment/topology.svg rename to test/case/infix_containers/environment/topology.svg diff --git a/test/case/infix_containers/container_firewall_basic/Readme.adoc b/test/case/infix_containers/firewall_basic/Readme.adoc similarity index 100% rename from test/case/infix_containers/container_firewall_basic/Readme.adoc rename to test/case/infix_containers/firewall_basic/Readme.adoc diff --git a/test/case/infix_containers/container_firewall_basic/test.adoc b/test/case/infix_containers/firewall_basic/test.adoc similarity index 98% rename from test/case/infix_containers/container_firewall_basic/test.adoc rename to test/case/infix_containers/firewall_basic/test.adoc index 1f986fa67..3b1d8e33e 100644 --- a/test/case/infix_containers/container_firewall_basic/test.adoc +++ b/test/case/infix_containers/firewall_basic/test.adoc @@ -1,6 +1,6 @@ === Basic Firewall Container -ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/container_firewall_basic] +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/firewall_basic] ==== Description diff --git a/test/case/infix_containers/container_firewall_basic/test.py b/test/case/infix_containers/firewall_basic/test.py similarity index 100% rename from test/case/infix_containers/container_firewall_basic/test.py rename to test/case/infix_containers/firewall_basic/test.py diff --git a/test/case/infix_containers/container_firewall_basic/topology.dot b/test/case/infix_containers/firewall_basic/topology.dot similarity index 100% rename from test/case/infix_containers/container_firewall_basic/topology.dot rename to test/case/infix_containers/firewall_basic/topology.dot diff --git a/test/case/infix_containers/container_firewall_basic/topology.svg b/test/case/infix_containers/firewall_basic/topology.svg similarity index 100% rename from test/case/infix_containers/container_firewall_basic/topology.svg rename to test/case/infix_containers/firewall_basic/topology.svg diff --git a/test/case/infix_containers/container_host_commands/Readme.adoc b/test/case/infix_containers/host_commands/Readme.adoc similarity index 100% rename from test/case/infix_containers/container_host_commands/Readme.adoc rename to test/case/infix_containers/host_commands/Readme.adoc diff --git a/test/case/infix_containers/container_host_commands/test.adoc b/test/case/infix_containers/host_commands/test.adoc similarity index 96% rename from test/case/infix_containers/container_host_commands/test.adoc rename to test/case/infix_containers/host_commands/test.adoc index d6f88d8a7..ac74d43e9 100644 --- a/test/case/infix_containers/container_host_commands/test.adoc +++ b/test/case/infix_containers/host_commands/test.adoc @@ -1,6 +1,6 @@ === Host Command Execution from Container -ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/container_host_commands] +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/host_commands] ==== Description diff --git a/test/case/infix_containers/container_host_commands/test.py b/test/case/infix_containers/host_commands/test.py similarity index 100% rename from test/case/infix_containers/container_host_commands/test.py rename to test/case/infix_containers/host_commands/test.py diff --git a/test/case/infix_containers/container_host_commands/topology.dot b/test/case/infix_containers/host_commands/topology.dot similarity index 100% rename from test/case/infix_containers/container_host_commands/topology.dot rename to test/case/infix_containers/host_commands/topology.dot diff --git a/test/case/infix_containers/container_host_commands/topology.svg b/test/case/infix_containers/host_commands/topology.svg similarity index 100% rename from test/case/infix_containers/container_host_commands/topology.svg rename to test/case/infix_containers/host_commands/topology.svg diff --git a/test/case/infix_containers/container_phys/Readme.adoc b/test/case/infix_containers/phys/Readme.adoc similarity index 100% rename from test/case/infix_containers/container_phys/Readme.adoc rename to test/case/infix_containers/phys/Readme.adoc diff --git a/test/case/infix_containers/container_phys/test.adoc b/test/case/infix_containers/phys/test.adoc similarity index 97% rename from test/case/infix_containers/container_phys/test.adoc rename to test/case/infix_containers/phys/test.adoc index 977854bc3..6d0390f2c 100644 --- a/test/case/infix_containers/container_phys/test.adoc +++ b/test/case/infix_containers/phys/test.adoc @@ -1,6 +1,6 @@ === Container with physical interface -ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/container_phys] +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/phys] ==== Description diff --git a/test/case/infix_containers/container_phys/test.py b/test/case/infix_containers/phys/test.py similarity index 100% rename from test/case/infix_containers/container_phys/test.py rename to test/case/infix_containers/phys/test.py diff --git a/test/case/infix_containers/phys/topology.dot b/test/case/infix_containers/phys/topology.dot new file mode 120000 index 000000000..1f2e45e2b --- /dev/null +++ b/test/case/infix_containers/phys/topology.dot @@ -0,0 +1 @@ +../bridge/topology.dot \ No newline at end of file diff --git a/test/case/infix_containers/container_phys/topology.svg b/test/case/infix_containers/phys/topology.svg similarity index 100% rename from test/case/infix_containers/container_phys/topology.svg rename to test/case/infix_containers/phys/topology.svg diff --git a/test/case/infix_containers/container_veth/Readme.adoc b/test/case/infix_containers/veth/Readme.adoc similarity index 100% rename from test/case/infix_containers/container_veth/Readme.adoc rename to test/case/infix_containers/veth/Readme.adoc diff --git a/test/case/infix_containers/container_veth/network_diagram.svg b/test/case/infix_containers/veth/network_diagram.svg similarity index 100% rename from test/case/infix_containers/container_veth/network_diagram.svg rename to test/case/infix_containers/veth/network_diagram.svg diff --git a/test/case/infix_containers/container_veth/test.adoc b/test/case/infix_containers/veth/test.adoc similarity index 98% rename from test/case/infix_containers/container_veth/test.adoc rename to test/case/infix_containers/veth/test.adoc index fa4edc703..c5de93122 100644 --- a/test/case/infix_containers/container_veth/test.adoc +++ b/test/case/infix_containers/veth/test.adoc @@ -1,6 +1,6 @@ === Container with VETH pair -ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/container_veth] +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/veth] ==== Description diff --git a/test/case/infix_containers/container_veth/test.py b/test/case/infix_containers/veth/test.py similarity index 100% rename from test/case/infix_containers/container_veth/test.py rename to test/case/infix_containers/veth/test.py diff --git a/test/case/infix_containers/veth/topology.dot b/test/case/infix_containers/veth/topology.dot new file mode 120000 index 000000000..1f2e45e2b --- /dev/null +++ b/test/case/infix_containers/veth/topology.dot @@ -0,0 +1 @@ +../bridge/topology.dot \ No newline at end of file diff --git a/test/case/infix_containers/container_veth/topology.svg b/test/case/infix_containers/veth/topology.svg similarity index 100% rename from test/case/infix_containers/container_veth/topology.svg rename to test/case/infix_containers/veth/topology.svg diff --git a/test/case/infix_containers/container_volume/Readme.adoc b/test/case/infix_containers/volume/Readme.adoc similarity index 100% rename from test/case/infix_containers/container_volume/Readme.adoc rename to test/case/infix_containers/volume/Readme.adoc diff --git a/test/case/infix_containers/container_volume/test.adoc b/test/case/infix_containers/volume/test.adoc similarity index 97% rename from test/case/infix_containers/container_volume/test.adoc rename to test/case/infix_containers/volume/test.adoc index e56587a73..b5be4eda5 100644 --- a/test/case/infix_containers/container_volume/test.adoc +++ b/test/case/infix_containers/volume/test.adoc @@ -1,6 +1,6 @@ === Container Volume Persistence -ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/container_volume] +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/volume] ==== Description diff --git a/test/case/infix_containers/container_volume/test.py b/test/case/infix_containers/volume/test.py similarity index 100% rename from test/case/infix_containers/container_volume/test.py rename to test/case/infix_containers/volume/test.py diff --git a/test/case/infix_containers/container_volume/topology.dot b/test/case/infix_containers/volume/topology.dot similarity index 100% rename from test/case/infix_containers/container_volume/topology.dot rename to test/case/infix_containers/volume/topology.dot diff --git a/test/case/infix_containers/container_volume/topology.svg b/test/case/infix_containers/volume/topology.svg similarity index 100% rename from test/case/infix_containers/container_volume/topology.svg rename to test/case/infix_containers/volume/topology.svg From 648a28425499b1b203b8ad23cf9c4f83dc36eea8 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 13 Oct 2025 08:32:34 +0200 Subject: [PATCH 16/19] test: update firewall/basic topology and doc Signed-off-by: Joachim Wiberg --- test/case/infix_firewall/basic/test.adoc | 3 +- test/case/infix_firewall/basic/topology.dot | 8 +-- test/case/infix_firewall/basic/topology.svg | 58 +++++++++++++-------- 3 files changed, 42 insertions(+), 27 deletions(-) diff --git a/test/case/infix_firewall/basic/test.adoc b/test/case/infix_firewall/basic/test.adoc index bf6aef8bf..015e9a4c9 100644 --- a/test/case/infix_firewall/basic/test.adoc +++ b/test/case/infix_firewall/basic/test.adoc @@ -11,7 +11,7 @@ image::basic.svg[align=center, scaledwidth=50%] - Single zone configuration, "public", with action=drop - Allowed services: SSH (port 22), DHCPv6-client, mySSH (custom, port 222) - All other ports (HTTP, HTTPS, Telnet, etc.) blocked -- Verifies unused interfaces automatically assigned to default zone +- Check that unused interfaces are automatically assigned to default zone ==== Topology @@ -26,6 +26,7 @@ image::topology.svg[Basic Firewall for End Devices topology, align=center, scale . Verify ICMPv6 is dropped . Verify SSH service is allowed . Verify custom mySSH service is allowed +. Verify HTTP service override (8080 allowed, 80 blocked) . Verify other ports are blocked diff --git a/test/case/infix_firewall/basic/topology.dot b/test/case/infix_firewall/basic/topology.dot index c75e3bbfc..52174355c 100644 --- a/test/case/infix_firewall/basic/topology.dot +++ b/test/case/infix_firewall/basic/topology.dot @@ -1,26 +1,26 @@ graph "1x3" { layout = "neato"; overlap = false; - esep = "+80"; + esep = "+30"; node [shape=record, fontname="DejaVu Sans Mono, Book"]; edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; host [ label="host | { mgmt | data }", - pos="1,1!", + pos="10,10.95!", requires="controller" ]; target [ label="{ mgmt | data | unused } | target", - pos="3,1!", + pos="30,10!", requires="infix", ]; dummy [ label="{ link } | dummy", - pos="5,1!", + pos="29.8,00!", requires="infix", ]; diff --git a/test/case/infix_firewall/basic/topology.svg b/test/case/infix_firewall/basic/topology.svg index 644d700d8..22a6325f7 100644 --- a/test/case/infix_firewall/basic/topology.svg +++ b/test/case/infix_firewall/basic/topology.svg @@ -1,45 +1,59 @@ - + - - + + 1x3 - + host - -host - -mgmt - -data + +host + +mgmt + +data target - -mgmt - -data - -unused - -target + +mgmt + +data + +unused + +target host:mgmt--target:mgmt - + host:data--target:data - -192.168.1.42/24 + +192.168.1.42/24 + + + +dummy + +link + +dummy + + + +target:unused--dummy:link + From a40ea28bbe34756ac8a3c38e6d2f980589df3cf1 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 12 Oct 2025 23:10:51 +0200 Subject: [PATCH 17/19] doc: add section on container volume management Since there is no safe way (unique hash/id) to identify unused named volumes, we cannot automate removal of them. Since volume data, when compared to a container image, is quite small, it was decided that we document this instead. Signed-off-by: Joachim Wiberg --- doc/container.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/doc/container.md b/doc/container.md index 1f1691851..765d8996a 100644 --- a/doc/container.md +++ b/doc/container.md @@ -614,6 +614,50 @@ empty: then rsync". > volume between containers. All the tricks possible with volumes may > be added in a later release. +### Volume Management + +Volumes are persistent storage that survive container restarts and image +upgrades, making them ideal for application data. However, this also means +they **are not automatically removed** when a container is deleted from the +configuration. + +This design choice prevents accidental data loss, especially in scenarios +where: + + - A container is temporarily removed and re-added with the same name + - A container is replaced with a different configuration but same name + - System upgrades or configuration changes affect container definitions + +To clean up unused volumes and reclaim disk space, use the admin-exec +command: + + admin@example:/> container prune + Deleted Images + ... + Deleted Volumes + ntpd-varlib + system-data + + Total reclaimed space: 45.2MB + +The `container prune` command safely removes: + + - Unused container images + - Volumes not attached to any container (running or stopped) + - Other unused container resources + +> [!TIP] +> You can monitor container resource usage with the command: +> +> admin@example:/> show container usage +> +> This displays disk space used by images, containers, and volumes, +> helping you decide when to run the prune command. +> +> To see which volumes exist and which containers use them: +> +> admin@example:/> show container volumes + ### Content Mounts Content mounts are a special type of file mount where the file contents From fd3a02e07cee4a724bfa3ca37df8a91fa87ef7da Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 14 Oct 2025 10:50:24 +0200 Subject: [PATCH 18/19] doc: explain docker image tags and rewrite upgrade section Much of the content was made obsolete by recent changes, we now optimize the experience for users, avoiding recreate at boot unless checksums for configuration or base image have changed. This was also a good time to explain the difference between mutable and immutable tags, which sometimes is a cause for great confusion. Signed-off-by: Joachim Wiberg --- doc/container.md | 243 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 192 insertions(+), 51 deletions(-) diff --git a/doc/container.md b/doc/container.md index 765d8996a..1d66166d4 100644 --- a/doc/container.md +++ b/doc/container.md @@ -246,80 +246,221 @@ archive, which helps greatly with container upgrades (see below): admin@example:/> show log ... - Nov 20 07:24:56 infix container[5040]: Fetching ftp://192.168.122.1/curios-oci-amd64-v24.05.0.tar.gz - Nov 20 07:24:56 infix container[5040]: curios-oci-amd64-v24.05.0.tar.gz downloaded successfully. - Nov 20 07:24:56 infix container[5040]: curios-oci-amd64-v24.05.0.tar.gz checksum verified OK. - Nov 20 07:24:57 infix container[5040]: Cleaning up extracted curios-oci-amd64-v24.05.0 - Nov 20 07:24:57 infix container[5040]: podman create --name sys --conmon-pidfile=/run/container:sys.pid --read-only --replace --quiet --cgroup-parent=containers --restart=always --systemd=false --tz=local --hostname sys --log-driver k8s-file --log-opt path=/run/containers/sys.fifo --network=none curios-oci-amd64-v24.05.0 - Nov 20 07:24:57 infix container[3556]: b02e945c43c9bce2c4be88e31d6f63cfdb1a3c8bdd02179376eb059a49ae05e4 + Nov 20 07:24:56 example container[5040]: Fetching ftp://192.168.122.1/curios-oci-amd64-v24.05.0.tar.gz + Nov 20 07:24:56 example container[5040]: curios-oci-amd64-v24.05.0.tar.gz downloaded successfully. + Nov 20 07:24:56 example container[5040]: curios-oci-amd64-v24.05.0.tar.gz checksum verified OK. + Nov 20 07:24:57 example container[5040]: Cleaning up extracted curios-oci-amd64-v24.05.0 + Nov 20 07:24:57 example container[5040]: podman create --name sys --conmon-pidfile=/run/container:sys.pid --read-only --replace --quiet --cgroup-parent=containers --restart=always --systemd=false --tz=local --hostname sys --log-driver k8s-file --log-opt path=/run/containers/sys.fifo --network=none curios-oci-amd64-v24.05.0 + Nov 20 07:24:57 example container[3556]: b02e945c43c9bce2c4be88e31d6f63cfdb1a3c8bdd02179376eb059a49ae05e4 -Upgrading a Container Image +Understanding Image Tags +------------------------ + +Docker images use tags to identify different versions of the same image. +Understanding the difference between *mutable* and *immutable* tags is +important for managing container upgrades effectively. + +### Mutable Tags + +Tags like `:latest`, `:edge`, or `:stable` are *mutable* — they point to +different images over time as new versions are published to the registry. + +**Advantages:** + + - Convenient: upgrade without changing configuration + - Simple: use the CLI command `container upgrade NAME` to get the latest version, + there is even a convenient RPC for controlling the remotely + - Good for: development, testing, and systems that auto-update + +**Trade-offs:** + + - Less reproducible: different systems may run different versions + - Less predictable: upgrades happen when you pull, not when you plan + - Harder to rollback: previous version may no longer be available + +**Example mutable tags:** + +``` +docker://nginx:latest # Always points to newest release +docker://myapp:edge # Development/bleeding edge version +oci-archive:/var/tmp/app.tar # Local archive that may be replaced +``` + +### Immutable Tags + +Version-specific tags like `:v1.0.1`, `:24.11.0`, or digest references +like `@sha256:abc123...` are *immutable* — they always reference the +exact same image content. + +**Advantages:** + + - Reproducible: all systems run identical versions + - Predictable: upgrades only happen when you change configuration + - Auditable: clear history of what ran when + - Good for: production, compliance, and controlled deployments + +**Trade-offs:** + + - More explicit: must update configuration to upgrade + - Requires planning: need to know which version to use + +**Example immutable tags:** + +``` +docker://nginx:1.25.3 # Specific version number +docker://myapp:v2.1.0 # Semantic version tag +docker://nginx@sha256:abc123 # Cryptographic digest (most immutable) +``` + +> [!TIP] +> **Best practice for production:** Use specific version tags (`:v1.0.1`) +> rather than mutable tags (`:latest`). This ensures all your systems run +> identical software and upgrades happen only when you decide. + +Upgrading Container Images --------------------------- + ![Up-to-date Shield](img/shield-checkmark.svg){ align=right width="100" } The applications in your container are an active part of the system as a whole, so make it a routine to keep your container images up-to-date! -Containers are created at first setup and at every boot. If the image -exists in the file system it is reused -- i.e., an image pulled from a -remote registry is not fetched again. +### How Container Lifecycle Works + +Infix intelligently manages container lifecycles to provide a smooth +experience while minimizing unnecessary work: + +**At first setup:** When you configure a container for the first time, +Infix fetches the image (if needed) and creates the container instance. + +**At boot time:** Infix checks if the container needs to be recreated by +comparing checksums for: + + - The image archive that the container was built from + - The container configuration script + +**When configuration changes:** If you modify any container settings +(network, volumes, environment, etc.), the container is automatically +recreated with the new configuration. + +**When explicitly upgraded:** Using the `container upgrade` command forces +a fresh pull of the image and recreates the container. + +This means that in most cases, **containers persist across reboots** and +are only recreated when actually necessary. Your container's state stored +in volumes is preserved across recreations. Since Infix containers use a +read-only root filesystem, any changes written outside of volumes or the +writable paths provided by Podman (`/dev`, `/dev/shm`, `/run`, `/tmp`, +`/var/tmp`) will be lost when the container is recreated. + +### Method 1: Upgrading Immutable Tags -To upgrade a versioned image: - - update your `running-config` to use the new `image:tag` - - `leave` to activate the change, if you are in the CLI - - Podman pulls the new image in the background - - Your container is recreated with the new image - - The container is started +When using version-specific tags, you upgrade by explicitly changing the +image reference in your configuration: -For "unversioned" images, e.g., images using a `:latest` or `:edge` tag, -use the following CLI command (`NAME` is the name of your container): +``` +admin@example:/> configure +admin@example:/config/> edit container web +admin@example:/config/container/web/> set image docker://nginx:1.25.3 +admin@example:/config/container/web/> leave +``` - admin@example:/> container upgrade NAME +**What happens:** -This stops the container, does `container pull IMAGE`, and recreates it -with the new image. Upgraded containers are automatically restarted. + 1. Podman pulls the new image in the background (if not already present) + 2. Your container is automatically stopped + 3. The container is recreated with the new image + 4. The container is started with your existing volumes intact + +**Example:** Upgrading from one version to another: + +``` +admin@example:/> configure +admin@example:/config/> edit container system +admin@example:/config/container/system/> show image +image ghcr.io/kernelkit/curios:v24.11.0; +admin@example:/config/container/system/> set image ghcr.io/kernelkit/curios:v24.12.0 +admin@example:/config/container/system/> leave +admin@example:/> show log +... +Dec 13 14:32:15 example container[1523]: Pulling ghcr.io/kernelkit/curios:v24.12.0... +Dec 13 14:32:18 example container[1523]: Stopping old container instance... +Dec 13 14:32:19 example container[1523]: Creating new container with updated image... +Dec 13 14:32:20 example container[1523]: Container system started successfully +``` + +### Method 2: Upgrading Mutable Tags + +For images using mutable tags like `:latest` or `:edge`, use the +`container upgrade` command: + +``` +admin@example:/> container upgrade NAME +``` + +This command: + + 1. Stops the running container + 2. Pulls the latest version of the image from the registry + 3. Recreates the container with the new image + 4. Starts the container automatically **Example using registry:** - admin@example:/> container upgrade system - system - Trying to pull ghcr.io/kernelkit/curios:edge... - Getting image source signatures - Copying blob 07bfba95fe93 done - Copying config 0cb6059c0f done - Writing manifest to image destination - Storing signatures - 0cb6059c0f4111650ddbc7dbc4880c64ab8180d4bdbb7269c08034defc348f17 - system: not running. - 59618cc3c84bef341c1f5251a62be1592e459cc990f0b8864bc0f5be70e60719 - -An OCI archive image can be upgraded in a similar manner, the first step -is of course to get the new archive onto the system (see above), and -then, provided the `oci-archive:/path/to/archive` format is used, call -the upgrade command as - - admin@example:/> container upgrade system - Upgrading container system with local archive: oci-archive:/var/tmp/curios-oci-amd64.tar.gz ... - 7ab4a07ee0c6039837419b7afda4da1527a70f0c60c0f0ac21cafee05ba24b52 - -OCI archives can also be fetched from ftp/http/https URL, in that case -the upgrade can be done the same way as a registry image (above). +``` +admin@example:/> container upgrade system +system +Trying to pull ghcr.io/kernelkit/curios:edge... +Getting image source signatures +Copying blob 07bfba95fe93 done +Copying config 0cb6059c0f done +Writing manifest to image destination +Storing signatures +0cb6059c0f4111650ddbc7dbc4880c64ab8180d4bdbb7269c08034defc348f17 +system: not running. +59618cc3c84bef341c1f5251a62be1592e459cc990f0b8864bc0f5be70e60719 +``` + +**Example using local OCI archive:** + +An OCI archive image can be upgraded in a similar manner. First, get the +new archive onto the system (see Container Images section above), then, +provided the `oci-archive:/path/to/archive` format is used in your +configuration, call the upgrade command: + +``` +admin@example:/> container upgrade system +Upgrading container system with local archive: oci-archive:/var/tmp/curios-oci-amd64.tar.gz ... +7ab4a07ee0c6039837419b7afda4da1527a70f0c60c0f0ac21cafee05ba24b52 +``` + +OCI archives can also be fetched from ftp/http/https URLs. In that case, +the upgrade works the same way as a registry image — Infix downloads the +new archive and recreates the container. + +### Embedded Container Images > [!TIP] > Containers running from OCI images embedded in the operating system, -> e.g., `/lib/oci/mycontainer.tar.gz`, always run from the version in -> the operating system. To upgrade, install the new container image at -> build time, after system upgrade the container is also upgraded. The -> system unpacks and loads the OCI images into Podman every boot, which -> ensures the running container always has known starting state. +> e.g., `/lib/oci/mycontainer.tar.gz`, are automatically kept in sync +> with the Infix system image version. +> +> **How it works:** When you build a custom Infix image with embedded OCI +> archives, those containers will be upgraded whenever you upgrade the +> Infix operating system itself. At boot, Infix checks if the embedded +> image has changed and automatically recreates the container if needed. > > **Example:** default builds of Infix include a couple of OCI images > for reference, one is `/lib/oci/curios-nftables-v24.11.0.tar.gz`, but > there is also a symlink called `curios-nftables-latest.tar.gz` in the > same directory, which is what the Infix regression tests use in the -> image configuration of the container. This is what enables easy -> upgrades of the container along with the system itself. +> image configuration of the container. When the system is upgraded and +> the embedded image changes, the test containers are automatically +> recreated with the new version. +> +> This approach ensures your embedded containers always match your system +> version without any manual intervention. Capabilities From 9a6b5c1410731fe4915a2a030050c41577627b21 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 14 Oct 2025 11:24:29 +0200 Subject: [PATCH 19/19] doc: update changelog with container fixes and new features Signed-off-by: Joachim Wiberg --- doc/ChangeLog.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index 0dbda7eb6..53bcecb84 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -13,11 +13,30 @@ All notable changes to the project are documented in this file. data examples, discovery patterns, and common workflow examples, issue #1156 - Initial support for a zone-based firewall, based on `firewalld`, issue #448 - Automatically expand `/var` partition on SD card at first boot on RPi +- New `upgrade` RPC (action) for containers using images with mutable tags +- Optimize startup of preexisting containers by adding metadata to track all + OCI archives loaded into container store, and all container configurations + used to create container instances. Instances are now only recreated when + metadata from an existing instance does not match either the configuration + or the image — because of configuration changes or image upgrades +- Updated container documentation on volumes, image tags, and image upgrade ### Fixes +- Fix #1146: Possible to set longer containers names than the system supports. + Root cause, a limit of 15 characters implicitly imposed by the service mgmt + daemon, Finit. The length has not been increased to 64 characters (min: 2) + and the YANG model now properly warns if the name is outside of these limits +- Fix #1147: Use container metadata to clean up lingering old container images + instead of using the too broad `podman image prune -af` command +- Fix #1148: Only retry container instance create on remote images +- Fix #1149: Increase `podman stop` timeout, from 10 to 30 seconds, needed with + bigger containers on heavily loaded systems - Fix #1194: CLI `text-editor` command does not do proper input sanitation - Fix #1197: RPi4 no longer boots after BPi-R3 merge, introduced in v25.09 +- Upgrade fixes for containers with mutable images, e.g., `:latest`. Infix now + always tries to fetch a new version of the OCI archive, for remote images, + regardless of the transport. After upgrade the old image is pruned [v25.09.0][] - 2025-09-30 -------------------------