mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-30 12:33:02 +02:00
Shell script: - Factor out big portions of code into more logical helper functions - Simplify calling setup script by checking for remote image first - Simplify meta/sha up-to-date handling and clarify terminology - Consistent use of -f instead of -e in file-exists checks - Fix unsafe use of 'mktemp -u' C code: - Clarify meta/sha terminology: rename meta-sha256 -> meta-image-sha256 - Refactor weird archive_offset() function to local_path() helper - Factor out helper function calc_sha() - Check len of sha256 >= 64 Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
1291 lines
33 KiB
Bash
Executable File
1291 lines
33 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.
|
|
#
|
|
# 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 # 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 -- "$*"
|
|
}
|
|
|
|
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"
|
|
}
|
|
|
|
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
|
|
|
|
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 [ -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
|
|
|
|
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
|
|
# 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
|
|
# 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 [ ! -f "$file" ]; then
|
|
if [ -f "$DOWNLOADS/$file" ]; then
|
|
file="$DOWNLOADS/$file"
|
|
elif [ -f "$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=$(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"
|
|
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)
|
|
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
|
|
|
|
# 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-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 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-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()
|
|
{
|
|
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.
|
|
#
|
|
# 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")
|
|
|
|
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")
|
|
|
|
# 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
|
|
|
|
# Only the 'podman stop' command takes -i and --timeout
|
|
args="-i --timeout $timeout"
|
|
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, 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
|
|
-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."
|
|
|
|
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"
|
|
|
|
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
|
|
|
|
# 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"
|
|
|
|
# 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
|
|
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)
|
|
if [ -z "$name" ]; then
|
|
name="$1"
|
|
fi
|
|
script=/run/containers/${name}.sh
|
|
|
|
# Verify container exists
|
|
if ! podman container exists "$name"; then
|
|
echo "No such container: $name"
|
|
exit 1
|
|
fi
|
|
|
|
# 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
|
|
[ -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
|