Files
infix/board/common/rootfs/usr/sbin/container
T
Joachim Wiberg 7fbc2eba4f 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 <troglobit@gmail.com>
2025-10-23 15:23:54 +02:00

1183 lines
30 KiB
Bash
Executable File

#!/bin/bash
# This script can be used to start, stop, create, and delete containers.
# It is what confd use, with the Finit container@.conf template, to set
# up, run, and delete containers.
#
# 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 # 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 -- "$*"
}
err()
{
rc=$1; shift
logger -I $PPID -t container -p local1.err -- "Error: $*"
if [ -n "$extracted" ] && [ -n "$tmpdir" ]; then
if [ -d "$tmpdir" ]; then
log "Cleaning up temporary directory $tmpdir"
rm -rf "$tmpdir"
fi
fi
[ "$rc" -eq 0 ] || exit "$rc"
}
pidfn()
{
echo "/run/containers/${1}.pid"
}
check()
{
file=$1
if [ -z "$checksum" ]; then
log "no checksum to verify $file against, continuing."
return 0
fi
if echo "${checksum} ${file}" | "$cmdsum" -c -s; then
log "$file checksum verified OK."
return 0
fi
got=$("$cmdsum" "${file}" | awk '{print $1}')
log "$file checksum mismatch, got $got, expected $checksum, removing file."
rm -f "$file"
return 1
}
# Fetch an OCI image over ftp/http/https. Use wget for FTP, which curl
# empirically does not work well with. Log progress+ & error to syslog.
fetch()
{
url=$1
file=$(basename "$url")
dst="$DOWNLOADS/$file"
cd "$DOWNLOADS" || return
if [ -e "$file" ]; then
log "$file already available."
if check "$file"; then
echo "$dst"
return 0
fi
fi
log "Fetching $url"
if echo "$url" | grep -qE "^ftp://"; then
cmd="wget -q $url"
elif echo "$url" | grep -qE "^https?://"; then
cmd="curl $creds -sSL --fail -o \"$file\" $url"
else
log "Unsupported URL scheme: $url"
return 1
fi
if out=$(eval "$cmd" 2>&1); then
log "$file downloaded successfully."
if check "$file"; then
echo "$dst"
return 0
fi
fi
# log error message from backend
while IFS= read -r line; do
log "$line"
done <<EOF
$out
EOF
return 1
}
# Extracts an oci-archive.tar[.gz] in a temporary directory. Finds and
# sanity checks that at least one index.json exist in the archive. This
# is the OCI directory fed to `podman load` and also used as repo name.
# NOTE: if there are >1 index.json, this function does not handle them.
load_archive()
{
uri=$1
tag=$2
tmp=$3
img=$(basename "$uri")
# Supported transports for load and create
case "$uri" in
oci:*) # Unpacked OCI image
file=${uri#oci:}
;;
oci-archive:*) # Packed OCI image, .tar or .tar.gz format
file=${uri#oci-archive:}
;;
ftp://* | http://* | https://*)
if ! file=$(fetch "$uri"); then
return 1
fi
;;
*) # docker://*, docker-archive:*, or URL
if podman image exists "$img"; then
echo "$img"
return 0
fi
# XXX: use --retry=0 with Podman 5.0 or later.
if ! id=$(podman pull --quiet "$uri"); then
log "Failed pulling $uri"
return 1
fi
# Echo image tag to caller
podman images --filter id="$id" --format "{{.Repository}}:{{.Tag}}"
return 0
;;
esac
if [ ! -e "$file" ]; then
if [ -e "$DOWNLOADS/$file" ]; then
file="$DOWNLOADS/$file"
elif [ -e "$BUILTIN/$file" ]; then
file="$BUILTIN/$file"
else
err 1 "cannot find OCI archive $file in URI $uri"
fi
fi
file=$(realpath "$file")
if [ -d "$file" ]; then
index=$(find "$file" -name index.json)
if [ -z "$index" ]; then
err 1 "cannot find index.json in OCI image $file"
fi
else
# Extract files in a temporary directory, because most OCI
# archives are flat/bare, all files in the root w/o a dir/
tmpdir=$(mktemp -d -p "$BASEDIR") || err 1 "failed creating temporary directory"
cd "$tmpdir" || err 1 "failed cd to temporary directory $tmpdir"
index="$tmpdir/$(tar tf "$file" |grep index.json)"
if [ -z "$index" ]; then
err 1 "invalid OCI archive, cannot find index.json in $file"
fi
[ -n "$quiet" ] || log "Extracting OCI archive $file ..."
tar xf "$file" || err 1 "failed unpacking $file in $tmpdir"
extracted=true
cd - >/dev/null || err 0 "failed cd -"
fi
dir=$(dirname "$index")
# Handle flat tarballs without a sub-directory, because
# the $dir name is used as fallback when retagging below.
if [ -n "$extracted" ] && [ "$dir" = "$tmpdir" ]; then
parent=$(dirname "$dir")
dirnam=$(echo "$img" | sed 's/\(.*\)\.tar.*/\1/')
tmpdir="${parent}/${dirnam}"
mv "$dir" "$tmpdir"
dir="$tmpdir"
fi
if basename "$dir" | grep -q ":"; then
if [ -z "$tag" ]; then
tag=$(basename "$dir")
fi
sanitized_dir=$(echo "$dir" | cut -d':' -f1)
mv "$dir" "$sanitized_dir" || err 1 "failed renaming $dir to $sanitized_dir"
dir="$sanitized_dir"
fi
[ -n "$quiet" ] || log "Loading OCI image $dir ..."
output=$(nice podman load -qi "$dir")
# Extract image ID from podman load output:
# "Loaded image: sha256:cd9d0aaf81be..."
if echo "$output" | grep -q "sha256:"; then
img_id="${output##*sha256:}"
else
# Fallback to directory name if no SHA found
img_id="$dir"
fi
# On podman < 4.7.0 we had to retag images from default $dir:latest
# From >= 4.7.0 we always tag since loads come in as <none>:<none>
if [ -z "$tag" ]; then
tag=$(basename "$dir")
fi
# Repo names must be lowercase, and only '[a-z0-9._/-]+' and ':tag'
tag=$(printf "%s" "$tag" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9._/:-' '-')
[ -n "$quiet" ] || log "Tagging loaded image $img_id as $tag"
if ! podman tag "$img_id" "$tag"; then
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=$(sha256sum "$file" | cut -f1 -d' ')
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"
rm -rf "$tmpdir"
fi
echo "$tag"
}
running()
{
status=$(podman inspect -f '{{.State.Status}}' "$1" 2>/dev/null)
[ "$status" = "running" ] && return 0
return 1
}
# shellcheck disable=SC2086
create()
{
name=$1
image=$2
shift 2
if [ -z "$name" ] || [ -z "$image" ]; then
echo "Usage:"
echo " container create NAME IMAGE"
exit 1
fi
# Unpack and load docker-archive/oci/oci-archive, returning image
# name, or return docker:// URL for download.
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
# When we get here we've already fetched, or pulled, the image
args="$args --read-only --replace --quiet --cgroup-parent=containers $caps"
args="$args --restart=$restart --systemd=false --tz=local $privileged"
args="$args $vol $mount $hostname $entrypoint $env $port $logging"
pidfile=/run/container:${name}.pid
[ -n "$quiet" ] || log "---------------------------------------"
[ -n "$quiet" ] || log "Got name: $name image: $image"
[ -n "$quiet" ] || log "Got networks: $network"
if [ -n "$network" ]; then
for net in $network; do
args="$args --net=$net"
done
for srv in $dns; do
args="$args --dns=$srv"
done
for domain in $search; do
args="$args --dns-search=$domain"
done
else
args="$args --network=none"
fi
# Add optimization labels for meta-sha256 and config checksum
script="/run/containers/${name}.sh"
if [ -f "$script" ]; then
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
# 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 nice podman create --name "$name" --conmon-pidfile="$pidfile" $args "$image" $*; then
[ -n "$quiet" ] || log "Successfully created container $name from $image"
[ -n "$manual" ] || start "$name"
# Should already be enabled by confd (this is for manual use)
initctl -bnq enable "container@${name}.conf"
exit 0
fi
err 1 "failed creating container $name, please check the configuration."
}
delete()
{
name=$1
if [ -z "$name" ]; then
echo "Usage:"
echo " container delete NAME"
exit 1
fi
# Should already be stopped, but if not ...
container stop "$name" >/dev/null
while running "$name"; do
log "$name: still running, waiting for it to stop ..."
_=$((timeout -= 1))
if [ $timeout -le 0 ]; then
err 1 "timed out waiting for container $1 to stop before deleting it."
fi
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/<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-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
exit 0
}
waitfor()
{
while [ ! -f "$1" ]; do
_=$((timeout -= 1))
if [ $timeout -le 0 ]; then
err 1 "timed out waiting for $1, aborting!"
fi
sleep 1;
done
}
start()
{
name=$1
if running "$name"; then
[ -n "$quiet" ] || echo "$name: already running."
return
fi
initctl start container:$name
# Real work is done by wrap() courtesy of finit sysv emulation
}
stop()
{
name=$1
if ! running "$name"; then
[ -n "$quiet" ] || echo "$name: not running."
return
fi
initctl stop container:$name
# 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
cmd=$2
pidfile=$(pidfn "$name")
# Containers have three phases: setup, running, and teardown.
# 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" ]; 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
args="-i --timeout $timeout"
else
args=
fi
# Skip "echo $name" from podman start in log
# shellcheck disable=SC2086
podman "$cmd" $args "$name" >/dev/null
}
# Removes network $1 from all containers
netwrm()
{
net=$1
for c in $(podman ps $all --format "{{.Names}}"); do
for n in $(podman inspect "$c" |jq -r '.[].NetworkSettings.Networks | keys[]'); do
if [ "$n" = "$net" ]; then
podman network disconnect $force "$n" "$c" >/dev/null
fi
done
done
}
# Schedule restart of (any) container using network $1 to activate network changes
netrestart()
{
net=$1
for c in $(podman ps $all --format "{{.Names}}"); do
for n in $(podman inspect "$c" |jq -r '.[].NetworkSettings.Networks | keys[]'); do
if [ "$n" = "$net" ]; then
initctl -nbq touch "container@$c"
fi
done
done
}
atexit()
{
pidfile=$(pidfn "$name")
log "Received signal, exiting."
if [ -n "$name" ] && [ -f "$pidfile" ]; then
log "$name: in setup phase, removing $pidfile ..."
rm -f "$pidfile"
fi
exit 1
}
usage()
{
cat <<EOF
usage:
container [opt] cmd [arg]
options:
-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
--cap-drop CAP Drop capability, for privileged containter
--checksum TYPE:SUM Use md5/sha256/sha512 to verify ftp/http/https archives
-c, --creds USR[:PWD] Credentials to pass to curl -u for remote ops
-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
-h, --help Show this help text
--hostname NAME Set hostname when creating container
--net NETWORK Network interface(s) when creating or finding container
-l, --log-driver DRV Log driver to use
--log-opt OPT Logging options to log driver
--log-path PATH Path for k8s-file log pipe
-m, --mount HOST:DEST Bind mount a read-only file inside a container
--manual Do not start container automatically after creation
-n, --name NAME Alternative way of supplying name to start/stop/restart
--privileged Give container extended privileges
-p, --publish PORT Publish ports when creating container
Syntax: [[ip:][hostPort]:]containerPort[/protocol]
-q, --quiet Quiet operation, called from confd
-r, --restart POLICY One of "no", "always", or "on-failure:NUM"
-s, --simple Show output in simplified format
-t, --timeout SEC Set timeout for delete/restart commands, default: 30
-v, --volume NAME:PATH Create named volume mounted inside container on PATH
commands:
create NAME IMAGE NET Create container NAME using IMAGE with networks NET
delete [network] NAME Remove container NAME or network NAME from all containers
exec NAME CMD Run a command inside a container
flush Clean up lingering containers and associated anonymous volumes
find [ifname PID] Find PID of container where '--net IFNAME' currently lives
or, find the name of our IFNAME inside the container @PID
help Show this help text
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 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]
setup NAME Create and set up container as a Finit task
shell [CMD] Start a shell, or run CMD, inside a container
show [image | volume] Show containers, images, or volumes
stat Show continuous stats about containers (Ctrl-C aborts)
start [NAME] Start a container, see -n
stop [NAME] Stop a container, see -n
upgrade NAME Upgrade a running container (stop, pull, restart)
volume [prune] Prune unused volumes
EOF
}
while [ "$1" != "" ]; do
case $1 in
-a | --all)
all="-a"
;;
--cap-add)
shift
caps="$caps --cap-add=$1"
;;
--cap-drop)
shift
caps="$caps --cap-drop=$1"
;;
--checksum)
shift
type="${1%%:*}"
checksum="${1#*:}"
case "$type" in
md5)
cmdsum=md5sum
;;
sha256)
cmdsum=sha256sum
;;
sha512)
cmdsum=sha512sum
;;
*)
err 1 "Unsupported checksum type: $type"
;;
esac
;;
-c | --creds)
shift
creds="-u $1"
;;
-d | --detach)
detach="-d"
;;
--dns)
shift
dns="$dns $1"
;;
--dns-search)
shift
search="$search $1"
;;
-e | --env)
shift
env="$env --env-file=$1"
;;
--entrypoint)
entrypoint="--entrypoint=\"\""
;;
-f | --force)
force="-f"
;;
-h | --help)
usage
exit 0
;;
--hostname)
shift
hostname="--hostname $1"
;;
-l | --log-driver)
shift
logging=" --log-driver=$1"
;;
--log-opt)
shift
logging="$logging --log-opt $1"
;;
--log-path)
shift
logging="$logging --log-opt path=$1"
;;
-m | --mount)
shift
mount="$mount --mount=$1"
;;
--manual)
manual=true
;;
-n | --name)
shift
name="$1"
;;
--net)
shift
if [ -n "$network" ]; then
network="$network $1"
else
network=$1
fi
;;
--privileged)
privileged="--privileged=true"
;;
-p | --publish)
shift
port="$port -p $1"
;;
-q | --quiet)
quiet="-q"
;;
-r | --restart)
shift
restart=$1
;;
-s | --simple)
simple=true
;;
-t | --timeout)
shift
timeout=$1
;;
-v | --volume)
shift
vol="$vol -v $1"
;;
*)
break
;;
esac
shift
done
cmd=$1
if [ -n "$cmd" ]; then
shift
fi
trap atexit INT HUP TERM
case $cmd in
# Does not work atm., cannot attach to TTY because
# we monitor 'podman start -ai foo' with Finit.
# attach)
# podman attach "$1"
# ;;
cleanup) # Hidden from public view, use remove or flush commands instead
cleanup "$@"
;;
create)
[ -n "$quiet" ] || log "Got create args: $*"
create "$@"
;;
delete)
cmd=$1
[ -n "$name" ] || name=$2
if [ "$cmd" = "network" ] && [ -n "$name" ]; then
netwrm "$name"
else
[ -n "$name" ] || name=$1
delete "$name"
fi
;;
exec)
if [ -z "$name" ]; then
name="$1"
shift
fi
podman exec -i "$name" "$@"
;;
flush)
echo "Cleaning up any lingering containers";
podman rm -av $force
;;
find)
cmd=$1
pid=$2
if [ "$cmd" = "ifname" ] && [ -n "$pid" ]; then
nsenter -t "$pid" -n ip -d -j link | \
jq --arg ifname "$network" -r '.[] | select(.ifalias==$ifname) | .ifname'
else
containers=$(podman ps $all --format "{{.Names}}")
for c in $containers; do
json=$(podman inspect "$c")
nets=$(echo "$json" |jq -r '.[].NetworkSettings.Networks | keys[]' 2>/dev/null)
for n in $nets; do
if [ "$network" = "$n" ]; then
pid=$(echo "$json" | jq .[].State.Pid)
echo "$pid"
exit 0
fi
done
done
fi
;;
help)
usage
;;
load)
# shellcheck disable=SC2086
name=$(load_archive "$1" $2)
[ -n "$name" ] || exit 1
# Show resulting image(s) matching $name
podman images -n "$name"
;;
locate) # Find where the host's ifname lives
if [ -z "$network" ]; then
echo "Missing --net IFNAME option."
exit 1
fi
containers=$(podman ps $all --format "{{.Names}}")
for c in $containers; do
json=$(podman inspect "$c")
nets=$(echo "$json" |jq -r '.[].NetworkSettings.Networks | keys[]' 2>/dev/null)
for n in $nets; do
if [ "$network" = "$n" ]; then
echo "$c"
exit 0;
fi
done
done
;;
ls | list)
cmd=$1
[ -n "$cmd" ] && shift
case $cmd in
image*)
podman images $all --format "{{.Repository}}:{{.Tag}}"
;;
oci)
find $BUILTIN $DOWNLOADS -type f 2>/dev/null
;;
*)
podman ps $all --format "{{.Names}}"
;;
esac
;;
pull)
podman pull "$@"
;;
remove)
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
cmd=$2
[ -n "$port" ] || port="-P"
if [ -n "$cmd" ]; then
shift 2
[ -n "$detach" ] || echo "Starting $img ENTRYPOINT $cmd :: use Ctrl-p Ctrl-q to detach"
podman run -it --rm $detach $port --entrypoint="$cmd" "$img" "$@"
else
[ -n "$detach" ] || echo "Starting $img :: use Ctrl-p Ctrl-q to detach"
podman run -it --rm $detach $port "$img"
fi
;;
save)
name=$1
file=$2
if echo "$file" | grep -q ".gz"; then
file=${file%%.gz}
gzip=true
fi
if ! echo "$file" | grep -q ".tar"; then
file=${file}.tar
gzip=true
fi
podman save -o "$file" "$name"
if [ -s "$file" ] && [ -n "$gzip" ]; then
gzip "$file"
fi
;;
setup)
[ -n "$name" ] || err 1 "setup: missing container name."
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}")
echo $$ > "$pidfile"
# 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)
log "Pruned $cnt image(s)"
;;
shell)
if [ -z "$name" ]; then
name="$1"
shift
fi
if [ $# -gt 0 ]; then
podman exec -i "$name" sh -c "$*"
else
podman exec -it "$name" sh -l
fi
;;
show)
cmd=$1
[ -n "$cmd" ] && shift
case $cmd in
image*)
if [ -n "$simple" ]; then
podman images $all --format "{{.Names}} {{.Size}}" \
| sed 's/\[\(.*\)\] /\1 /g' \
| awk '{ printf "%-60s %s %s\n", $1, $2, $3}'
else
podman images $all
fi
;;
volume*)
printf "%-20s CONTAINER\n" "VOLUME"
for v in $(podman volume ls --format "{{.Name}}"); do
printf "%-20s" "$v"
podman ps -a --filter volume="$v" --format '{{.Names}}' | sed 's/^/ /'
done
;;
*)
if [ -n "$simple" ]; then
podman ps $all --format "{{.ID}} {{.Names}} {{.Image}}" \
| awk '{ printf "%s %-30s %s\n", $1, $2, $3}'
else
podman ps $all
fi
;;
esac
;;
start)
if [ -n "$name" ]; then
wrap "$name" start
elif [ -n "$1" ]; then
start "$1"
else
usage
exit 1
fi
;;
restart)
if [ -n "$name" ]; then
wrap "$name" restart
elif [ -n "$1" ]; then
cmd=$1
name=$2
if [ "$cmd" = "network" ] && [ -n "$name" ]; then
netrestart "$name"
else
name=$1
stop "$name"
while running "$name"; do
_=$((timeout -= 1))
if [ $timeout -le 0 ]; then
err 1 "timed out waiting for container $1 to stop before restarting it."
fi
sleep 1
done
start "$name"
fi
else
usage
exit 1
fi
;;
stop)
if [ -n "$name" ]; then
wrap "$name" stop
elif [ -n "$1" ]; then
stop "$1"
else
usage
exit 1
fi
;;
stat*)
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;
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 ..."
fi
if ! "$script"; then
echo ">> Failed recreating container $1"
exit 1
fi
echo ">> Done."
;;
volume)
cmd=$1
[ -n "$cmd" ] && shift
case $cmd in
prune)
podman volume prune $force
;;
*)
false
;;
esac
;;
*)
if [ -n "$SERVICE_SCRIPT_TYPE" ] && [ -n "$SERVICE_ID" ]; then
case "$SERVICE_SCRIPT_TYPE" in
pre)
# Called as pre-script from Finit service
exec $container -q -n "$SERVICE_ID" setup
;;
cleanup)
# Called as cleanup-script from Finit service
log "Calling $container -n $SERVICE_ID cleanup"
exec $container -q -n "$SERVICE_ID" cleanup
;;
*)
false
;;
esac
fi
usage
exit 1
;;
esac