Merge pull request #1151 from kernelkit/recontain

Container Lifecycle and Upgrade Improvements

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2025-10-23 18:04:01 +02:00
committed by GitHub
85 changed files with 1631 additions and 314 deletions
@@ -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
+411 -71
View File
@@ -6,19 +6,33 @@
# NOTE: when creating/deleting containers, remember 'initctl reload' to
# activate the changes! In confd this is already handled.
#
# TODO: this script should be refactored to take a .cfg file instead for
# each container. For details, see this pull request discussion:
# https://github.com/kernelkit/infix/pull/1151#discussion_r2444851292
#
# shellcheck disable=SC1001
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=""
dbg()
{
logger -I $PPID -t container -p local1.debug -- "$*"
}
log()
{
logger -I $PPID -t container -p local1.notice -- "$*"
@@ -44,6 +58,104 @@ pidfn()
echo "/run/containers/${1}.pid"
}
calc_sha()
{
sha256sum "$1" 2>/dev/null | awk '{print $1}'
}
# Check image transport, return 0 if remote, 1 if local
is_remote()
{
image=$(awk '/^# meta-image:/ {print $3}' "$1" 2>/dev/null || echo "")
echo "$image"
case "$image" in
oci\:* | oci-archive\:* | docker-archive\:* | docker-daemon\:* | \
dir\:* | containers-storage\:* | ostree\:* | sif\:* | /*)
return 1
;;
*)
return 0
;;
esac
}
# Check loaded image in container store vs archive, as well as verifying
# the container instance vs. the configuration.
#
# This is an optimization to cut down startup times.
is_uptodate()
{
img_sha=$(awk '/^# meta-image-sha256:/ {print $3}' "$script" 2>/dev/null)
if [ -n "$img_sha" ]; then
# Local image optimization: check if archive SHA matches stored sidecar file
img=$(awk '/^# meta-image:/ {print $3}' "$script" 2>/dev/null)
if [ -n "$img" ]; then
case "$img" in
docker-archive\:*)
archive_path="${img#docker-archive:}"
;;
oci-archive\:*)
archive_path="${img#oci-archive:}"
;;
*)
archive_path="$img"
;;
esac
# Handle relative paths - check BUILTIN and DOWNLOADS
if [ ! -f "$archive_path" ]; then
if [ -f "$DOWNLOADS/$(basename "$archive_path")" ]; then
archive_path="$DOWNLOADS/$(basename "$archive_path")"
elif [ -f "$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"
if [ -f "$sha_file" ]; then
stored_sha=$(cat "$sha_file" 2>/dev/null)
current_sha=$(calc_sha "$archive_path")
# If SHA matches, check container instance
if [ "$stored_sha" = "$current_sha" ]; then
if podman container exists "$name"; then
config_sha=$(calc_sha "$script")
container_sha=$(podman inspect "$name" --format '{{index .Config.Labels "config-sha256"}}' 2>/dev/null)
container_img_sha=$(podman inspect "$name" --format '{{index .Config.Labels "meta-image-sha256"}}' 2>/dev/null)
if [ "$container_img_sha" = "$img_sha" ] && [ "$container_sha" = "$config_sha" ]; then
# Unmodified local image and configuration
return 0
fi
fi
else
# Archive changed (e.g., rootfs upgrade) - need to reload
dbg "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
config_sha=$(calc_sha "$script")
container_sha=$(podman inspect "$name" --format '{{index .Config.Labels "config-sha256"}}' 2>/dev/null)
if [ "$container_sha" = "$config_sha" ]; then
# Unmodified local image and configuration
return 0
fi
fi
fi
return 1
}
check()
{
file=$1
@@ -74,11 +186,16 @@ fetch()
dst="$DOWNLOADS/$file"
cd "$DOWNLOADS" || return
if [ -e "$file" ]; then
log "$file already available."
if check "$file"; then
echo "$dst"
return 0
if [ -f "$file" ]; then
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
@@ -118,6 +235,7 @@ load_archive()
{
uri=$1
tag=$2
tmp=$3
img=$(basename "$uri")
# Supported transports for load and create
@@ -134,7 +252,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
@@ -149,10 +268,10 @@ load_archive()
;;
esac
if [ ! -e "$file" ]; then
if [ -e "$DOWNLOADS/$file" ]; then
if [ ! -f "$file" ]; then
if [ -f "$DOWNLOADS/$file" ]; then
file="$DOWNLOADS/$file"
elif [ -e "$BUILTIN/$file" ]; then
elif [ -f "$BUILTIN/$file" ]; then
file="$BUILTIN/$file"
else
err 1 "cannot find OCI archive $file in URI $uri"
@@ -205,7 +324,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..."
@@ -230,6 +349,23 @@ load_archive()
err 1 "failed tagging image as $tag"
fi
# 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=$(calc_sha "$file")
archive_basename=$(basename "$file")
sha_file="$DOWNLOADS/${archive_basename}.sha256"
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
# Clean up after ourselves
if [ -n "$extracted" ]; then
log "Cleaning up extracted $dir"
@@ -261,10 +397,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)
if ! image=$(load_archive "$image" "" "$sha_file"); then
exit 1
fi
if [ -s "$sha_file" ]; then
img_sha=$(cat "$sha_file" 2>/dev/null || echo "")
rm "$sha_file"
fi
if [ -z "$logging" ]; then
logging="--log-driver syslog"
fi
@@ -295,9 +437,24 @@ create()
args="$args --network=none"
fi
# Add optimization labels for meta-image-sha256 and config checksum
script="/run/containers/${name}.sh"
if [ -f "$script" ]; then
if [ -z "$img_sha" ]; then
# Extract meta-image-sha256 from script if present
img_sha=$(awk '/^# meta-image-sha256:/ {print $3}' "$script" 2>/dev/null)
fi
if [ -n "$img_sha" ]; then
args="$args --label meta-image-sha256=$img_sha"
fi
# Add config checksum label
args="$args --label config-sha256=$(calc_sha "$script")"
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
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"
@@ -331,11 +488,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."
}
cnt=$(podman image prune -af | wc -l)
log "Pruned $cnt image(s)"
# Called by Finit cleanup:script when a container service is removed.
# Processes all container instance IDs found in $CLEANUP/<name>/ 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-image-sha256 from the container instance labels
# These are the image in container store and the trace to an oci-archive.tar.gz
img=$(podman inspect "$cid" 2>/dev/null | jq -r '.[].ImageName' 2>/dev/null || echo "")
sha=$(podman inspect "$cid" --format '{{index .Config.Labels "meta-image-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 checksum file from $DOWNLOADS for remote images
# I.e., $DOWNLOADS/oci-archive.tar.gz and $DOWNLOADS/oci-archive.tar.gz.sha256
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 by stripping .sha256
archive="${sha_file%.sha256}"
if [ -f "$archive" ]; then
# Safety check: verify archive SHA matches before removing
if [ "$(calc_sha "$archive")" = "$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
exit 0
}
waitfor()
@@ -375,31 +611,42 @@ 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.
#
# NOTE: containers have three phases: setup, running, and teardown.
# the setup phase is this script trying to fetch the image.
wrap()
{
name=$1
cmd=$2
args=
pidfile=$(pidfn "$name")
# Containers have three phases: setup, running, and teardown.
if [ "$cmd" = "stop" ]; then
# The setup phase may run forever in the background trying to fetch
# the image. It saves its PID in /run/containers/${name}.pid
if [ -f "$pidfile" ]; then
pid=$(cat "$pidfile")
# 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
# 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
# Only the 'podman stop' command takes -i and --timeout
args="-i --timeout $timeout"
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
@@ -430,7 +677,7 @@ netrestart()
done
}
cleanup()
atexit()
{
pidfile=$(pidfn "$name")
@@ -450,7 +697,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
@@ -460,7 +707,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
@@ -490,7 +737,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]
@@ -638,7 +885,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
@@ -646,6 +893,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 "$@"
@@ -739,7 +989,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
@@ -775,28 +1032,54 @@ case $cmd in
script=/run/containers/${name}.sh
[ -x "$script" ] || err 1 "setup: $script does not exist or is not executable."
if is_uptodate "$script"; then
log "Container $name config unchanged, skipping setup"
exit 0
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}")
echo $$ > "$pidfile"
while ! "$script"; do
log "${name}: setup failed, waiting for network changes ..."
if image=$(is_remote "$script"); then
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
# 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
# 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
elif ! "$script"; then
log "${name}: setup failed for local image $image"
rm -f "$pidfile"
exit 1
fi
rm -f "$pidfile"
cnt=$(podman image prune -f | wc -l)
log "setup: 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
@@ -888,32 +1171,89 @@ case $cmd in
podman stats -i 2
;;
upgrade)
# Start script used to initially create container
script=/run/containers/${1}.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;
if [ -z "$name" ]; then
name="$1"
fi
script=/run/containers/${name}.sh
# 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 ..."
fi
if ! "$script"; then
echo ">> Failed recreating container $1"
# Verify container exists
if ! podman container exists "$name"; then
echo "No such container: $name"
exit 1
fi
echo ">> Done."
# 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 $name"
exit 1
fi
# 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
@@ -936,8 +1276,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
+28
View File
@@ -13,12 +13,40 @@ All notable changes to the project are documented in this file.
- Extend NETCONF and RESTCONF scripting documentation with operational
data examples, discovery patterns, and common workflow examples, issue #1156
- Initial support for a zone-based firewall, based on `firewalld`, issue #448
- Add `validate` option to CLI `copy` command. This can be used before doing a
restore of a backup, or when having edited configuration files manually. With
the validate flag (`-n` from the shell) the file is only loaded and validated
against the YANG models, it is *not* rolled in if validation is successful.
Example: `copy /media/backup/old.cfg running-config validate`, issue #373
- 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 #981: copying any file, including `running-config`, to the persistent
back-end store for `startup-config`, does not take
- 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
- Fix #1203: copying any file, including `startup-config`, to `running-config`
does not take
[v25.09.0][] - 2025-09-30
-------------------------
+9 -1
View File
@@ -191,4 +191,12 @@ change that, e.g., breaks networking, it is trivial to revert back by:
admin@host-12-34-56:/> copy startup-config running-config
```
Or restarting the device.
Or restart the device, for example if the change to the configuration
caused you to lose contact with the system (it happens to the best of
us). The system will start up from the last "save gave".
> **Tip:** when restoring a backup of a configuration, or having manually
> edited a config file, you can validate it using system's YANG models,
> it is *not* applied if validation is successful:
>
> `copy /media/backup/old.cfg running-config validate`
+233 -48
View File
@@ -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
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
Infix intelligently manages container lifecycles to provide a smooth
experience while minimizing unnecessary work:
For "unversioned" images, e.g., images using a `:latest` or `:edge` tag,
use the following CLI command (`NAME` is the name of your container):
**At first setup:** When you configure a container for the first time,
Infix fetches the image (if needed) and creates the container instance.
admin@example:/> container upgrade NAME
**At boot time:** Infix checks if the container needs to be recreated by
comparing checksums for:
This stops the container, does `container pull IMAGE`, and recreates it
with the new image. Upgraded containers are automatically restarted.
- 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
When using version-specific tags, you upgrade by explicitly changing the
image reference in your configuration:
```
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
```
**What happens:**
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
```
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
**Example using local OCI archive:**
admin@example:/> container upgrade system
Upgrading container system with local archive: oci-archive:/var/tmp/curios-oci-amd64.tar.gz ...
7ab4a07ee0c6039837419b7afda4da1527a70f0c60c0f0ac21cafee05ba24b52
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:
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
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
@@ -614,6 +755,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
+1 -1
View File
@@ -11,7 +11,7 @@ BIN_LICENSE = BSD-3-Clause
BIN_LICENSE_FILES = LICENSE
BIN_REDISTRIBUTE = NO
BIN_DEPENDENCIES = sysrepo libite
BIN_CONF_OPTS = --prefix= --disable-silent-rules
BIN_CONF_OPTS = --disable-silent-rules
BIN_AUTORECONF = YES
define BIN_CONF_ENV
@@ -0,0 +1,35 @@
From 927acdbd7e19b1a9a0065ebdb88b663ec6626b88 Mon Sep 17 00:00:00 2001
From: Joachim Wiberg <troglobit@gmail.com>
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 <troglobit@gmail.com>
---
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
+6 -2
View File
@@ -3,11 +3,15 @@ ACLOCAL_AMFLAGS = -I m4
bin_PROGRAMS = copy erase files
# Bash completion
bashcompdir = $(datadir)/bash-completion/completions
dist_bashcomp_DATA = copy.bash
copy_SOURCES = copy.c util.c util.h
copy_CPPFLAGS = -D_DEFAULT_SOURCE -D_GNU_SOURCE
copy_CFLAGS = -W -Wall -Wextra
copy_CFLAGS += $(libite_CFLAGS) $(sysrepo_CFLAGS)
copy_LDADD = $(libite_LIBS) $(sysrepo_LIBS)
copy_CFLAGS += $(libite_CFLAGS) $(libyang_CFLAGS) $(sysrepo_CFLAGS)
copy_LDADD = $(libite_LIBS) $(libyang_LIBS) $(sysrepo_LIBS)
erase_SOURCES = erase.c util.c util.h
erase_CPPFLAGS = -D_DEFAULT_SOURCE -D_GNU_SOURCE
+3 -2
View File
@@ -15,6 +15,7 @@ AC_PROG_INSTALL
PKG_PROG_PKG_CONFIG
PKG_CHECK_MODULES([libite], [libite >= 2.5.0])
PKG_CHECK_MODULES([libyang], [libyang >= 3.0.0])
PKG_CHECK_MODULES([sysrepo], [sysrepo >= 2.2.36])
# Misc variable replacements for below Summary
@@ -55,8 +56,8 @@ cat <<EOF
System environment....: ${sysconfig_path:-${sysconfig}}
C Compiler............: $CC $CFLAGS $CPPFLAGS $LDFLAGS $LIBS
Linker................: $LD $LLDP_LDFLAGS $LLDP_BIN_LDFLAGS $LDFLAGS $LIBS
CFLAGS................: $libite_CFLAGS $sysrepo_CFLAGS
LIBS..................: $libite_LIBS $sysrepo_LIBS
CFLAGS................: $libite_CFLAGS $libyang_CFLAGS $sysrepo_CFLAGS
LIBS..................: $libite_LIBS $libyang_LIBS $sysrepo_LIBS
------------- Compiler version --------------
$($CC --version || true)
+93
View File
@@ -0,0 +1,93 @@
# bash completion for copy command
# SPDX-License-Identifier: ISC
_copy_completion()
{
local cur prev opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
# Options for the copy command
opts="-h -n -q -s -t -u -v"
local datastores_dst="running-config startup-config"
local datastores_src="running-config startup-config factory-config"
case "${prev}" in
-t)
# Timeout expects a number, no completion
return 0
;;
-u)
# Username, could complete with getent passwd but keep it simple
return 0
;;
esac
# If current word starts with -, complete options
if [[ ${cur} == -* ]]; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
# Determine position (source or destination)
# Count non-option arguments before current position
local arg_count=0
local i
for ((i=1; i < COMP_CWORD; i++)); do
case "${COMP_WORDS[i]}" in
-h|-n|-q|-s|-v)
# Flag without argument
;;
-t|-u)
# Flag with argument, skip next word
((i++))
;;
-*)
# Unknown flag, might have argument
;;
*)
# Non-option argument
((arg_count++))
;;
esac
done
# Helper function to add non-hidden files/dirs, filtering out dotfiles unless explicitly requested
_add_files() {
local IFS=$'\n'
local files
# If user is explicitly typing a dotfile path, include dotfiles
if [[ ${cur} == .* || ${cur} == */.* ]]; then
files=( $(compgen -f -- "${cur}") )
else
# Only show non-hidden files and directories
files=( $(compgen -f -- "${cur}" | grep -v '/\.[^/]*$' | grep -v '^\.[^/]') )
fi
COMPREPLY+=( "${files[@]}" )
}
case ${arg_count} in
0)
# First argument (source): complete with files and all datastores including factory-config
COMPREPLY=( $(compgen -W "${datastores_src}" -- ${cur}) )
_add_files
;;
1)
# Second argument (destination): complete with files and limited datastores (no factory-config)
COMPREPLY=( $(compgen -W "${datastores_dst}" -- ${cur}) )
_add_files
;;
*)
# No more arguments expected
return 0
;;
esac
return 0
}
complete -F _copy_completion copy
+173 -32
View File
@@ -34,6 +34,9 @@ struct infix_ds infix_config[] = {
static const char *prognm = "copy";
static int timeout;
static int dry_run;
static int quiet;
static int sanitize;
/*
@@ -177,11 +180,52 @@ static const char *infix_ds(const char *text, struct infix_ds **ds)
return text;
}
/*
* Load configuration from a file and replace running datastore.
* This triggers all sysrepo change callbacks, unlike sr_copy_config().
* In dry-run mode, only validates the configuration without applying.
*/
static int replace_running(sr_conn_ctx_t *conn, sr_session_ctx_t *sess,
const char *file, int timeout)
{
uint32_t flags = LYD_PARSE_NO_STATE | LYD_PARSE_ONLY | LYD_PARSE_STORE_ONLY | LYD_PARSE_STRICT;
struct lyd_node *data = NULL;
const struct ly_ctx *ctx;
LY_ERR err;
int rc;
ctx = sr_acquire_context(conn);
err = lyd_parse_data_path(ctx, file, LYD_JSON, flags, 0, &data);
sr_release_context(conn);
if (err) {
fprintf(stderr, ERRMSG "unable to load %s, error %d: %s\n", file, err, ly_errmsg(ctx));
return 1;
}
if (dry_run) {
if (!quiet) {
printf("Configuration validated, %s OK\n", file);
fflush(stdout);
}
lyd_free_all(data);
rc = 0;
} else {
/* Replace running config, assumes ownership of 'data' */
rc = sr_replace_config(sess, NULL, data, timeout * 1000);
if (rc) {
emsg(sess, ERRMSG "unable to replace configuration, err %d: %s\n",
rc, sr_strerror(rc));
lyd_free_all(data);
}
}
return rc;
}
static int copy(const char *src, const char *dst, const char *remote_user)
{
struct infix_ds *srcds = NULL, *dstds = NULL;
char temp_file[20] = "/tmp/copy.XXXXXX";
const char *tmpfn = NULL;
sr_session_ctx_t *sess;
const char *fn = NULL;
@@ -208,9 +252,8 @@ static int copy(const char *src, const char *dst, const char *remote_user)
user = getuser();
/* 1. Regular ds copy */
/* 1. Regular ds to ds copy */
if (srcds && dstds) {
/* Ensure the dst ds is writable */
if (!dstds->rw) {
fprintf(stderr, ERRMSG "not possible to write to \"%s\", skipping.\n", dst);
rc = 1;
@@ -229,12 +272,32 @@ static int copy(const char *src, const char *dst, const char *remote_user)
fprintf(stderr, ERRMSG "unable to open transaction to %s\n", dst);
} else {
sr_nacm_set_user(sess, user);
rc = sr_copy_config(sess, NULL, srcds->datastore, timeout * 1000);
if (rc)
emsg(sess, ERRMSG "unable to copy configuration, err %d: %s\n",
rc, sr_strerror(rc));
else
set_owner(dstds->path, user);
/*
* When copying TO running-config, use sr_replace_config()
* to trigger change callbacks. Otherwise use sr_copy_config()
* for direct datastore copy without callbacks.
*/
if (dstds->datastore == SR_DS_RUNNING && srcds->path) {
rc = replace_running(conn, sess, srcds->path, timeout);
if (!rc)
set_owner(dstds->path, user);
} else {
/* Direct copy for other datastores (no callbacks needed) */
rc = sr_copy_config(sess, NULL, srcds->datastore, timeout * 1000);
if (rc) {
emsg(sess, ERRMSG "unable to copy configuration, err %d: %s\n", rc, sr_strerror(rc));
} else {
/* Export datastore to backing file if it has one */
if (dstds->path) {
rc = systemf("sysrepocfg -d %s -X%s -f json", dstds->sysrepocfg, dstds->path);
if (rc)
fprintf(stderr, ERRMSG "failed saving %s to %s\n", srcds->name, dstds->path);
else
set_owner(dstds->path, user);
}
}
}
}
rc = sr_disconnect(conn);
@@ -246,12 +309,14 @@ static int copy(const char *src, const char *dst, const char *remote_user)
if (srcds) {
/* 2. Export from a datastore somewhere else */
if (strstr(dst, "://")) {
if (srcds->path)
fn = srcds->path;
else {
snprintf(adjust, sizeof(adjust), "/tmp/%s.cfg", srcds->name);
fn = tmpfn = adjust;
remove(tmpfn);
rc = systemf("sysrepocfg -d %s -X%s -f json", srcds->sysrepocfg, fn);
}
@@ -270,10 +335,10 @@ static int copy(const char *src, const char *dst, const char *remote_user)
if (dstds && dstds->path)
fn = dstds->path;
else
fn = cfg_adjust(dst, src, adjust, sizeof(adjust));
fn = cfg_adjust(dst, src, adjust, sizeof(adjust), sanitize);
if (!fn) {
fprintf(stderr, ERRMSG "invalid destination path.\n");
fprintf(stderr, ERRMSG "file not found.\n");
rc = -1;
goto err;
}
@@ -292,6 +357,8 @@ static int copy(const char *src, const char *dst, const char *remote_user)
else
set_owner(fn, user);
} else if (dstds) {
/* 3. Import from somewhere to a datastore */
if (!dstds->sysrepocfg) {
fprintf(stderr, ERRMSG "not possible to import to this datastore.\n");
rc = 1;
@@ -302,14 +369,20 @@ static int copy(const char *src, const char *dst, const char *remote_user)
goto err;
}
/* 3. Import from somewhere to a datastore */
if (strstr(src, "://")) {
tmpfn = mktemp(temp_file);
fn = tmpfn;
fn = basenm(src);
if (fn[0] == 0) {
fprintf(stderr, ERRMSG "missing filename in sorce URI.\n");
rc = 1;
goto err;
}
snprintf(adjust, sizeof(adjust), "/tmp/%s", fn);
fn = tmpfn = adjust;
remove(tmpfn);
} else {
fn = cfg_adjust(src, NULL, adjust, sizeof(adjust));
fn = cfg_adjust(src, NULL, adjust, sizeof(adjust), sanitize);
if (!fn) {
fprintf(stderr, ERRMSG "invalid source file location.\n");
fprintf(stderr, ERRMSG "file not found.\n");
rc = 1;
goto err;
}
@@ -320,20 +393,49 @@ static int copy(const char *src, const char *dst, const char *remote_user)
if (rc) {
fprintf(stderr, ERRMSG "failed downloading %s", src);
} else {
rc = systemf("sysrepocfg -d %s -I%s -f json", dstds->sysrepocfg, fn);
if (rc)
fprintf(stderr, ERRMSG "failed loading %s from %s", dst, src);
if (dstds->datastore == SR_DS_RUNNING) {
if (sr_connect(SR_CONN_DEFAULT, &conn)) {
fprintf(stderr, ERRMSG "connection to datastore failed\n");
rc = 1;
} else {
sr_log_syslog("klishd", SR_LL_WRN);
if (sr_session_start(conn, SR_DS_RUNNING, &sess)) {
fprintf(stderr, ERRMSG "unable to open transaction to %s\n", dst);
rc = 1;
} else {
sr_nacm_set_user(sess, user);
rc = replace_running(conn, sess, fn, timeout);
if (!rc)
set_owner(dstds->path, user);
}
sr_disconnect(conn);
}
} else {
rc = systemf("sysrepocfg -d %s -I%s -f json", dstds->sysrepocfg, fn);
if (rc)
fprintf(stderr, ERRMSG "failed loading %s from %s\n", dst, src);
else if (dstds->path) {
rc = systemf("sysrepocfg -d %s -X%s -f json", dstds->sysrepocfg, dstds->path);
if (rc)
fprintf(stderr, ERRMSG "failed saving %s\n", dstds->path);
else
set_owner(dstds->path, user);
}
}
}
} else {
/* 4. regular copy file -> file, either may be remote */
if (strstr(src, "://") && strstr(dst, "://")) {
fprintf(stderr, ERRMSG "copy from remote to remote is not supported.\n");
goto err;
}
if (strstr(src, "://")) {
fn = cfg_adjust(dst, src, adjust, sizeof(adjust));
fn = cfg_adjust(dst, src, adjust, sizeof(adjust), sanitize);
if (!fn) {
fprintf(stderr, ERRMSG "invalid destination file location.\n");
fprintf(stderr, ERRMSG "file not found.\n");
rc = 1;
goto err;
}
@@ -347,25 +449,42 @@ static int copy(const char *src, const char *dst, const char *remote_user)
rc = systemf("curl %s -Lo %s %s", remote_user, fn, src);
} else if (strstr(dst, "://")) {
fn = cfg_adjust(src, NULL, adjust, sizeof(adjust));
fn = cfg_adjust(src, NULL, adjust, sizeof(adjust), sanitize);
if (!fn) {
fprintf(stderr, ERRMSG "invalid source file location.\n");
fprintf(stderr, ERRMSG "file not found.\n");
rc = 1;
goto err;
}
if (access(fn, F_OK))
fprintf(stderr, ERRMSG "no such file %s, aborting.", fn);
fprintf(stderr, ERRMSG "no such file %s.", fn);
else
rc = systemf("curl %s -LT %s %s", remote_user, fn, dst);
} else {
if (!access(dst, F_OK)) {
if (!yorn("Overwrite existing file %s", dst)) {
char bufa[256], bufb[256];
const char *from, *to;
from = cfg_adjust(src, NULL, bufa, sizeof(bufa), sanitize);
if (!from) {
fprintf(stderr, ERRMSG "no such file %s.", src);
rc = 1;
goto err;
}
to = cfg_adjust(dst, src, bufb, sizeof(bufb), sanitize);
if (!to) {
fprintf(stderr, ERRMSG "no such file %s.", dst);
rc = 1;
goto err;
}
if (!access(to, F_OK)) {
if (!yorn("Overwrite existing file %s", to)) {
fprintf(stderr, "OK, aborting.\n");
return 0;
}
}
rc = systemf("cp %s %s", src, dst);
rc = systemf("cp %s %s", from, to);
}
}
@@ -384,10 +503,23 @@ static int usage(int rc)
printf("Usage: %s [OPTIONS] SRC DST\n"
"\n"
"Options:\n"
" -h This help text\n"
" -u USER Username for remote commands, like scp\n"
" -t SEEC Timeout for the operation, or default %d sec\n"
" -v Show version\n", prognm, timeout);
" -h This help text\n"
" -n Dry-run, validate configuration without applying\n"
" -q Quiet mode, suppress informational messages\n"
" -s Sanitize paths for CLI use (restrict path traversal)\n"
" -t SEC Timeout for the operation, or default %d sec\n"
" -u USER Username for remote commands, like scp\n"
" -v Show version\n"
"\n"
"Files:\n"
" SRC JSON configuration file, or a datastore\n"
" DST A file or datastore, except factory-config\n"
"\n"
"Datastores:\n"
" running-config The running datastore, current active config\n"
" startup-config The non-volatile config used at startup\n"
" factory-config The device's factory default configuration\n"
"\n", prognm, timeout);
return rc;
}
@@ -399,10 +531,19 @@ int main(int argc, char *argv[])
timeout = fgetint("/etc/default/confd", "=", "CONFD_TIMEOUT");
while ((c = getopt(argc, argv, "ht:u:v")) != EOF) {
while ((c = getopt(argc, argv, "hnqst:u:v")) != EOF) {
switch(c) {
case 'h':
return usage(0);
case 'n':
dry_run = 1;
break;
case 'q':
quiet = 1;
break;
case 's':
sanitize = 1;
break;
case 't':
timeout = atoi(optarg);
break;
+16 -18
View File
@@ -10,31 +10,25 @@
#include "util.h"
static const char *prognm = "erase";
static int sanitize;
static int do_erase(const char *path)
{
char *fn;
char buf[PATH_MAX];
const char *fn;
if (access(path, F_OK)) {
size_t len = strlen(path) + 10;
fn = cfg_adjust(path, NULL, buf, sizeof(buf), sanitize);
if (!fn) {
fprintf(stderr, ERRMSG "file not found.\n");
return 1;
}
fn = alloca(len);
if (!fn) {
fprintf(stderr, ERRMSG "failed allocating memory.\n");
return -1;
}
cfg_adjust(path, NULL, fn, len);
} else
fn = (char *)path;
if (!yorn("Remove %s, are you sure", fn))
if (!yorn("Remove %s, are you sure", path))
return 0;
if (remove(fn)) {
fprintf(stderr, ERRMSG "failed removing %s: %s\n", fn, strerror(errno));
return -1;
fprintf(stderr, ERRMSG "failed removing %s: %s\n", path, strerror(errno));
return 11;
}
return 0;
@@ -46,6 +40,7 @@ static int usage(int rc)
"\n"
"Options:\n"
" -h This help text\n"
" -s Sanitize paths for CLI use (restrict path traversal)\n"
" -v Show version\n", prognm);
return rc;
@@ -55,10 +50,13 @@ int main(int argc, char *argv[])
{
int c;
while ((c = getopt(argc, argv, "hv")) != EOF) {
while ((c = getopt(argc, argv, "hsv")) != EOF) {
switch(c) {
case 'h':
return usage(0);
case 's':
sanitize = 1;
break;
case 'v':
puts(PACKAGE_VERSION);
return 0;
+48 -26
View File
@@ -5,6 +5,7 @@
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
@@ -67,7 +68,7 @@ int has_ext(const char *fn, const char *ext)
return 0;
}
static const char *basenm(const char *fn)
const char *basenm(const char *fn)
{
const char *ptr;
@@ -75,44 +76,65 @@ static const char *basenm(const char *fn)
return "";
ptr = strrchr(fn, '/');
if (!ptr)
if (ptr)
ptr++;
else
ptr = fn;
return ptr;
}
char *cfg_adjust(const char *fn, const char *tmpl, char *buf, size_t len)
static int path_allowed(const char *path)
{
if (strstr(fn, "../"))
return NULL; /* relative paths not allowed */
const char *accepted[] = {
"/media/",
"/cfg/",
getenv("HOME"),
NULL
};
if (fn[0] == '/') {
strlcpy(buf, fn, len);
return buf; /* allow absolute paths */
for (int i = 0; accepted[i]; i++) {
if (!strncmp(path, accepted[i], strlen(accepted[i])))
return 1;
}
/* Files in /cfg must end in .cfg */
if (!strncmp(fn, "/cfg/", 5)) {
strlcpy(buf, fn, len);
if (!has_ext(fn, ".cfg"))
strlcat(buf, ".cfg", len);
return 0;
}
return buf;
char *cfg_adjust(const char *fn, const char *tmpl, char *buf, size_t len, int sanitize)
{
char tmp[256], resolved[PATH_MAX];
if (strlen(basenm(fn)) == 0) {
if (!tmpl)
return NULL;
fn = basenm(tmpl);
/* Fall through */
}
/* Files ending with .cfg belong in /cfg */
if (has_ext(fn, ".cfg")) {
snprintf(buf, len, "/cfg/%s", fn);
return buf;
if (sanitize) {
if (strstr(fn, "../"))
return NULL;
if (fn[0] == '/') {
if (!path_allowed(fn))
return NULL;
} else {
snprintf(tmp, sizeof(tmp), "/cfg/%s", fn);
if (!has_ext(tmp, ".cfg"))
strlcat(tmp, ".cfg", sizeof(tmp));
fn = tmp;
}
/* If file exists, resolve symlinks and verify still in whitelist */
if (!access(fn, F_OK) && realpath(fn, resolved)) {
if (!path_allowed(resolved))
return NULL;
fn = resolved;
}
}
if (strlen(fn) > 0 && fn[0] == '.' && tmpl) {
if (fn[1] == '/' && fn[2] != 0)
strlcpy(buf, fn, len);
else
strlcpy(buf, basenm(tmpl), len);
} else
strlcpy(buf, fn, len);
strlcpy(buf, fn, len);
return buf;
}
+5 -4
View File
@@ -7,11 +7,12 @@
#define ERRMSG "Error: "
#define INFMSG "Note: "
int yorn (const char *fmt, ...);
int yorn (const char *fmt, ...);
int files (const char *path, const char *stripext);
int files (const char *path, const char *stripext);
int has_ext (const char *fn, const char *ext);
char *cfg_adjust (const char *fn, const char *tmpl, char *buf, size_t len);
const char *basenm (const char *fn);
int has_ext (const char *fn, const char *ext);
char *cfg_adjust (const char *fn, const char *tmpl, char *buf, size_t len, int sanitize);
#endif /* BIN_UTIL_H_ */
+5 -1
View File
@@ -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;
}
+89 -15
View File
@@ -21,6 +21,46 @@
#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 file path.
* Returns NULL if not a recognized local archive format.
*/
static const char *local_path(const char *image)
{
const char *prefixes[] = {
"docker-archive:",
"oci-archive:",
NULL,
};
for (int i = 0; prefixes[i]; i++) {
size_t len = strlen(prefixes[i]);
if (!strncmp(image, prefixes[i], len))
return image + len;
}
return NULL;
}
static int calc_sha(const char *path, char *sha256, size_t len)
{
int rc = 1;
FILE *pp;
pp = popenf("r", "sha256sum \"%s\" 2>/dev/null | awk '{print $1}'", path);
if (pp) {
if (fgets(sha256, len, pp)) {
chomp(sha256);
rc = 0;
}
pclose(pp);
}
return rc;
}
/*
* Create a setup/create/upgrade script and instantiate a new instance
@@ -36,18 +76,9 @@ static int add(const char *name, struct lyd_node *cif)
const char *restart_policy, *string, *image;
struct lyd_node *node, *nets, *caps;
char script[strlen(name) + 5];
const char *path;
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) {
@@ -66,11 +97,27 @@ static int add(const char *name, struct lyd_node *cif)
* a running container instance.
*/
image = lydx_get_cattr(cif, "image");
/*
* TODO: this script should be refactored to be a .cfg file to
* the container helper script, or in the future, even a
* .cfg file to another runtime/engine. For details, see
* https://github.com/kernelkit/infix/pull/1151#discussion_r2444851292
*/
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);
path = local_path(image);
if (path) {
char sha256[65] = { 0 };
if (!calc_sha(path, sha256, sizeof(sha256)))
fprintf(fp, "# meta-image-sha256: %s\n", sha256);
}
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));
@@ -256,9 +303,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;
}
@@ -270,9 +317,35 @@ 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);
/* The return sha should be at least 64 chars long (sha256) */
if (strlen(buf) >= 64)
touchf("%s/%s", prune_dir, buf);
}
pclose(pp);
return SR_ERR_OK;
}
@@ -419,6 +492,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;
+30 -6
View File
@@ -22,6 +22,13 @@ module infix-containers {
prefix infix-sys;
}
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";
}
revision 2025-06-25 {
description "Add file mode option to content mounts, allows creating scripts.";
reference "internal";
@@ -62,6 +69,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 +143,7 @@ module infix-containers {
leaf name {
description "Name of the container";
type string;
type ident;
}
leaf id {
@@ -344,7 +359,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 +451,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 {
@@ -474,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.";
}
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
# -*- sh -*-
MODULES=(
"infix-interfaces -e containers"
"infix-containers@2025-06-25.yang"
"infix-containers@2025-10-12.yang"
)
+11 -15
View File
@@ -28,7 +28,7 @@
const uint8_t kplugin_infix_major = 1;
const uint8_t kplugin_infix_minor = 0;
static void cd_home(kcontext_t *ctx)
static const char *cd_home(kcontext_t *ctx)
{
const char *user = "root";
ksession_t *session;
@@ -43,6 +43,8 @@ static void cd_home(kcontext_t *ctx)
pw = getpwnam(user);
chdir(pw->pw_dir);
return user;
}
static int systemf(const char *fmt, ...)
@@ -118,7 +120,7 @@ int infix_erase(kcontext_t *ctx)
cd_home(ctx);
return systemf("erase %s", path);
return systemf("erase -s %s", path);
}
int infix_files(kcontext_t *ctx)
@@ -201,8 +203,8 @@ int infix_copy(kcontext_t *ctx)
{
kpargv_t *pargv = kcontext_pargv(ctx);
const char *src, *dst;
const char *username;
char user[256] = "";
char validate[8] = "";
kparg_t *parg;
src = kparg_value(kpargv_find(pargv, "src"));
@@ -214,26 +216,20 @@ int infix_copy(kcontext_t *ctx)
if (parg)
snprintf(user, sizeof(user), "-u %s", kparg_value(parg));
cd_home(ctx);
username = ksession_user(kcontext_session(ctx));
parg = kpargv_find(pargv, "validate");
if (parg)
strlcpy(validate, "-n", sizeof(validate));
return systemf("sudo -u %s copy %s %s %s", username, user, src, dst);
/* Ensure we run the copy command as the logged-in user, not root (klishd) */
return systemf("doas -u %s copy -s %s %s %s %s", cd_home(ctx), validate, user, src, dst);
}
int infix_shell(kcontext_t *ctx)
{
const char *user = "root";
ksession_t *session;
const char *user = cd_home(ctx);
pid_t pid;
int rc;
session = kcontext_session(ctx);
if (session) {
user = ksession_user(session);
if (!user)
user = "root";
}
pid = fork();
if (pid == -1)
return -1;
+7 -2
View File
@@ -102,9 +102,14 @@
</COMMAND>
<COMMAND name="remove" help="Remove a container image from local storage">
<PARAM name="name" ptype="/IMAGES" help="Image name" />
<SWITCH name="optional" min="0">
<PARAM name="name" ptype="/IMAGES" help="Image name" />
</SWITCH>
<ACTION sym="script" in="tty" out="tty" interrupt="true">
doas container remove $KLISH_PARAM_name
if [ -n "$KLISH_PARAM_name" ]; then
all=-a
fi
doas container $all remove $KLISH_PARAM_name
</ACTION>
</COMMAND>
+1
View File
@@ -188,6 +188,7 @@
<PARAM name="src" ptype="/DATASTORE" help="Source datastore or file, e.g., tftp://ip/file"/>
<PARAM name="dst" ptype="/RW_DATASTORE" help="Destination datastore or file, e.g., scp://ip/file"/>
<SWITCH name="optional" min="0">
<COMMAND name="validate" help="Validate configuration without applying"/>
<PARAM name="user" ptype="/STRING" help="Remote username (interactive password)"/>
</SWITCH>
<ACTION sym="copy@infix" in="tty" out="tty" interrupt="true"/>
+1 -1
View File
@@ -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")
+10 -9
View File
@@ -10,38 +10,39 @@ 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[]
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[]
+12 -9
View File
@@ -1,28 +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
@@ -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

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -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

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

@@ -1 +0,0 @@
../container_bridge/topology.dot
@@ -1 +0,0 @@
../container_bridge/topology.dot
@@ -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

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -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

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -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

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

@@ -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

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -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
+1
View File
@@ -0,0 +1 @@
../bridge/topology.dot

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

+40
View File
@@ -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"
@@ -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
+182
View File
@@ -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()
@@ -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> mgmt | <data> data }",
pos="0,12!",
requires="controller",
];
target [
label="{ <mgmt> mgmt | <data> data } | target",
pos="10,12!",
requires="infix",
];
host:mgmt -- target:mgmt [requires="mgmt", color=lightgrey]
host:data -- target:data [color=black]
}

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

@@ -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
+1
View File
@@ -0,0 +1 @@
../bridge/topology.dot

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

+1
View File
@@ -0,0 +1 @@
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
@@ -57,7 +57,7 @@ with infamy.Test() as test:
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}"
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Title: 1x2 Pages: 1 -->
<svg width="424pt" height="55pt"
viewBox="0.00 0.00 424.03 55.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 51)">
<title>1x2</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-51 420.03,-51 420.03,4 -4,4"/>
<!-- host -->
<g id="node1" class="node">
<title>host</title>
<polygon fill="none" stroke="black" points="0,-0.5 0,-46.5 100,-46.5 100,-0.5 0,-0.5"/>
<text text-anchor="middle" x="25" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="50,-0.5 50,-46.5 "/>
<text text-anchor="middle" x="75" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="50,-23.5 100,-23.5 "/>
<text text-anchor="middle" x="75" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
</g>
<!-- target -->
<g id="node2" class="node">
<title>target</title>
<polygon fill="none" stroke="black" points="300.03,-0.5 300.03,-46.5 416.03,-46.5 416.03,-0.5 300.03,-0.5"/>
<text text-anchor="middle" x="325.03" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="300.03,-23.5 350.03,-23.5 "/>
<text text-anchor="middle" x="325.03" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
<polyline fill="none" stroke="black" points="350.03,-0.5 350.03,-46.5 "/>
<text text-anchor="middle" x="383.03" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
</g>
<!-- host&#45;&#45;target -->
<g id="edge1" class="edge">
<title>host:mgmt&#45;&#45;target:mgmt</title>
<path fill="none" stroke="lightgrey" stroke-width="2" d="M100,-35.5C100,-35.5 300.03,-35.5 300.03,-35.5"/>
</g>
<!-- host&#45;&#45;target -->
<g id="edge2" class="edge">
<title>host:data&#45;&#45;target:data</title>
<path fill="none" stroke="black" stroke-width="2" d="M100,-11.5C100,-11.5 300.03,-11.5 300.03,-11.5"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+2 -1
View File
@@ -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
+4 -4
View File
@@ -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> mgmt | <data> data }",
pos="1,1!",
pos="10,10.95!",
requires="controller"
];
target [
label="{ <mgmt> mgmt | <data> data | <unused> unused } | target",
pos="3,1!",
pos="30,10!",
requires="infix",
];
dummy [
label="{ <link> link } | dummy",
pos="5,1!",
pos="29.8,00!",
requires="infix",
];
+36 -22
View File
@@ -1,45 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.43.0 (0)
-->
<!-- Title: 1x3 Pages: 1 -->
<svg width="440pt" height="78pt"
viewBox="0.00 0.00 440.03 78.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 74)">
<svg width="381pt" height="191pt"
viewBox="0.00 0.00 381.03 190.72" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 186.72)">
<title>1x3</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-74 436.03,-74 436.03,4 -4,4"/>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-186.72 377.03,-186.72 377.03,4 -4,4"/>
<!-- host -->
<g id="node1" class="node">
<title>host</title>
<polygon fill="none" stroke="black" points="0,-12 0,-58 100,-58 100,-12 0,-12"/>
<text text-anchor="middle" x="25" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="50,-12 50,-58 "/>
<text text-anchor="middle" x="75" y="-42.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="50,-35 100,-35 "/>
<text text-anchor="middle" x="75" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
<polygon fill="none" stroke="black" points="0,-136.22 0,-182.22 100,-182.22 100,-136.22 0,-136.22"/>
<text text-anchor="middle" x="25" y="-155.52" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="50,-136.22 50,-182.22 "/>
<text text-anchor="middle" x="75" y="-167.02" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="50,-159.22 100,-159.22 "/>
<text text-anchor="middle" x="75" y="-144.02" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
</g>
<!-- target -->
<g id="node2" class="node">
<title>target</title>
<polygon fill="none" stroke="black" points="300.03,-0.5 300.03,-69.5 432.03,-69.5 432.03,-0.5 300.03,-0.5"/>
<text text-anchor="middle" x="333.03" y="-54.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="300.03,-46.5 366.03,-46.5 "/>
<text text-anchor="middle" x="333.03" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
<polyline fill="none" stroke="black" points="300.03,-23.5 366.03,-23.5 "/>
<text text-anchor="middle" x="333.03" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">unused</text>
<polyline fill="none" stroke="black" points="366.03,-0.5 366.03,-69.5 "/>
<text text-anchor="middle" x="399.03" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
<polygon fill="none" stroke="black" points="241.03,-112.51 241.03,-181.51 373.03,-181.51 373.03,-112.51 241.03,-112.51"/>
<text text-anchor="middle" x="274.03" y="-166.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="241.03,-158.51 307.03,-158.51 "/>
<text text-anchor="middle" x="274.03" y="-143.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
<polyline fill="none" stroke="black" points="241.03,-135.51 307.03,-135.51 "/>
<text text-anchor="middle" x="274.03" y="-120.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">unused</text>
<polyline fill="none" stroke="black" points="307.03,-112.51 307.03,-181.51 "/>
<text text-anchor="middle" x="340.03" y="-143.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
</g>
<!-- host&#45;&#45;target -->
<g id="edge1" class="edge">
<title>host:mgmt&#45;&#45;target:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M100,-47C100,-47 300.03,-58 300.03,-58"/>
<path fill="none" stroke="lightgray" stroke-width="2" d="M100,-171.22C100,-171.22 241.03,-170.01 241.03,-170.01"/>
</g>
<!-- host&#45;&#45;target -->
<g id="edge2" class="edge">
<title>host:data&#45;&#45;target:data</title>
<path fill="none" stroke="black" stroke-width="2" d="M100,-23C100,-23 300.03,-35 300.03,-35"/>
<text text-anchor="middle" x="159" y="-26.8" font-family="DejaVu Serif, Book" font-size="14.00">192.168.1.42/24</text>
<path fill="none" stroke="black" stroke-width="2" d="M100,-147.22C100,-147.22 241.03,-147.01 241.03,-147.01"/>
<text text-anchor="middle" x="159" y="-151.02" font-family="DejaVu Serif, Book" font-size="14.00">192.168.1.42/24</text>
</g>
<!-- dummy -->
<g id="node3" class="node">
<title>dummy</title>
<polygon fill="none" stroke="black" points="250.46,-0.5 250.46,-36.5 358.46,-36.5 358.46,-0.5 250.46,-0.5"/>
<text text-anchor="middle" x="275.46" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">link</text>
<polyline fill="none" stroke="black" points="300.46,-0.5 300.46,-36.5 "/>
<text text-anchor="middle" x="329.46" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">dummy</text>
</g>
<!-- target&#45;&#45;dummy -->
<g id="edge3" class="edge">
<title>target:unused&#45;&#45;dummy:link</title>
<path fill="none" stroke="gray" stroke-width="2" stroke-dasharray="5,2" d="M274.03,-112.01C274.03,-112.01 275.46,-36.5 275.46,-36.5"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

+11 -5
View File
@@ -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"]
+2 -1
View File
@@ -16,7 +16,8 @@ the image, e.g., with missing Alpine packages.
here: <https://github.com/kernelkit/infix/pkgs/container/infix-test>
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`)
+8 -1
View File
@@ -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 \