1
0
mirror of https://github.com/opencontainers/umoci.git synced 2026-02-05 18:45:08 +01:00
Files
umoci/test/helpers.bash
Aleksa Sarai 712f78763a *: update license headers
Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
2026-01-10 11:14:14 +01:00

385 lines
10 KiB
Bash

#!/bin/bash
# SPDX-License-Identifier: Apache-2.0
# umoci: Umoci Modifies Open Containers' Images
# Copyright (C) 2016-2025 SUSE LLC
# Copyright (C) 2026 Aleksa Sarai <cyphar@cyphar.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -u
source "$(dirname "$BASH_SOURCE")/../hack/readlinkf.sh"
# Root directory of integration tests.
INTEGRATION_ROOT=$(dirname "$(readlinkf_posix "$BASH_SOURCE")")
# Binary paths.
UMOCI="${UMOCI:-${INTEGRATION_ROOT}/../umoci}"
# For some reason $(whence ...) and $(where ...) are broken.
RUNC="/usr/bin/runc"
# Used as a poor man's oci-image-tool validate.
DOCKER_METASCRIPT_DIR="${INTEGRATION_ROOT}/../hack/docker-meta-scripts"
# The source OCI image path, which we will make a copy of for each test.
SOURCE_IMAGE="${SOURCE_IMAGE:-/image}"
SOURCE_TAG="${SOURCE_TAG:-latest}"
# For "go build -cover" build umoci binaries.
GOCOVERDIR="${GOCOVERDIR:-}"
# Are we rootless?
IS_ROOTLESS="$(id -u)"
# Let's not store everything in /tmp -- that would just be messy.
TESTDIR_TMPDIR="$BATS_TMPDIR/umoci-integration"
mkdir -p "$TESTDIR_TMPDIR"
# Stores the set of tmpdirs that still have to be cleaned up. Calling
# teardown_tmpdirs will set this to an empty array (and all the tmpdirs
# contained within are removed).
export TESTDIR_LIST="$(mktemp "$TESTDIR_TMPDIR/umoci-integration-tmpdirs.XXXXXX")"
# INVALID_TAG is a sample invalid tag as per the OCI spec.
INVALID_TAG=".AZ94n18s"
# setup_tmpdir creates a new temporary directory and returns its name. Note
# that if "$IS_ROOTLESS" is true, then removing this tmpdir might be harder
# than expected -- so tests should not really attempt to clean up tmpdirs.
function setup_tmpdir() {
[[ -n "${UMOCI_TMPDIR:-}" ]] || UMOCI_TMPDIR="$TESTDIR_TMPDIR"
mktemp -d "$UMOCI_TMPDIR/umoci-integration-tmpdir.XXXXXXXX" | tee -a "$TESTDIR_LIST"
}
# setup_tmpdirs just sets up the "built-in" tmpdirs.
function setup_tmpdirs() {
declare -g UMOCI_TMPDIR="$(setup_tmpdir)"
}
# teardown_tmpdirs removes all tmpdirs created with setup_tmpdir.
function teardown_tmpdirs() {
# Do nothing if TESTDIR_LIST doesn't exist.
[ -e "$TESTDIR_LIST" ] || return
# Remove all of the tmpdirs.
while IFS= read -r tmpdir; do
[ -e "$tmpdir" ] || continue
chmod -R 0777 "$tmpdir"
rm -rf "$tmpdir"
done < "$TESTDIR_LIST"
# Clear tmpdir list.
rm -f "$TESTDIR_LIST"
}
# Where we're going to copy the images and bundle to.
IMAGE="$(setup_tmpdir)/image"
TAG="${SOURCE_TAG}"
function fail() {
echo "FAILURE:" "$@" >&2
false
}
function umoci-is-test-binary() {
"$UMOCI" --version | \
grep "== THIS UMOCI BINARY IS COMPILED IN TEST MODE ==" >/dev/null
}
# Allows a test to specify what things it requires. If the environment can't
# support it, the test is skipped with a message.
function requires() {
for var in "$@"; do
case $var in
root)
if [ "$IS_ROOTLESS" -ne 0 ]; then
skip "test requires ${var}"
fi
;;
test-binary)
if ! umoci-is-test-binary; then
skip "test requires ${var} (make umoci.cover)"
fi
;;
*)
fail "BUG: Invalid requires ${var}."
;;
esac
done
}
function image-verify() {
local ocidir="$1"
# Smoke-test for our blobs.
while IFS= read -r blob; do
# Make sure tar layers are valid according to gnutar.
mimetype="$(file --mime "$blob")"
cat=
case "$mimetype" in
*application/x-tar*)
cat="cat"
;;
*application/zstd*)
cat="zstdcat"
;;
*application/gzip*)
cat="zcat"
;;
*)
continue
;;
esac
"$cat" <"$blob" | file --mime - | grep "application/x-tar" || {
fail "blob $blob is compressed but is not a tar archive?"
}
"$cat" <"$blob" | tar tvf - >/dev/null || {
rc=$?
file "$blob"
echo "error untarring $blob: $rc"
return "$rc"
}
echo "$blob: valid tar archive"
done
# Validate that all inodes are owned by the same uid:gid as the root
# directory, which is the correct behaviour for us.
owner="$(stat -c "%u:%g" "$ocidir")"
allowners="$(find "$ocidir" -print0 | xargs -0 stat -c "%u:%g %n")"
if grep -v "^$owner" <<<"$allowners" ; then
echo "$allowners"
fail "image $ocidir contains subpaths with incorrect owners"
fi
# Use the Docker meta-scripts (which use jq internally) to do some basic
# validation of our image.
BASHBREW_META_SCRIPTS="$DOCKER_METASCRIPT_DIR" \
"$DOCKER_METASCRIPT_DIR/helpers/oci-validate.sh" "$ocidir"
# oci-spec validation
# FIXME: oci-image-tool broke due to image-spec repo changes, and so we
# have to skip this for now. The eventual plan is to move the
# validation to umoci itself.
#oci-image-tool validate --type "imageLayout" "$ocidir"
return $?
}
function bundle-verify() {
args=()
for arg in "$@"; do
args+=( --path="$arg" )
done
# FIXME: oci-runtime-tool has some incorrect validation logic (it disallows
# certain org.opencontianers.* annotations) and there has not yet
# been a release with go.mod. As such, using it causes issues and
# has blocked us from being able to update runtime-spec versions for
# years.
#
# Ultimately, we do some smoke tests with runc (which does its own
# specconv-based validation) and so our config.json does get some
# validation in our tests.
#oci-runtime-tool validate "${args[@]}"
return $?
}
function umoci() {
local args=()
if [[ "$1" == "raw" ]]; then
args+=("$1")
shift 1
fi
# Set the first argument (the subcommand).
# TODO: This doesn't correctly handle any global arguments which go before
# the subcommand. We should probably switch to getopt here.
args+=("$1")
# Set --rootless for commands that require it when we're rootless.
if [[ "$IS_ROOTLESS" != 0 && "$1" =~ unpack|insert|mtree-validate ]]; then
args+=("--rootless")
fi
shift
args+=("$@")
sane_run "$UMOCI" "${args[@]}"
}
function mtree-validate() {
umoci raw mtree-validate --umoci-keywords "$@"
local umoci_status="$status"
if [[ "$IS_ROOTLESS" == 0 ]]; then
# In the non-rootless case, we should cross-check that umoci's mtree is
# not accidentally ignoring mtree errors. Upstream gomtree doesn't
# support rootless mode.
sane_run gomtree validate --strict -K sha256digest "$@"
[[ "$status" == "$umoci_status" ]] || fail "umoci raw mtree-validate and gomtree have different results"
fi
}
function runc() {
sane_run "$RUNC" --root "$RUNC_ROOT" "$@"
}
function sane_run() {
local cmd="$1"
shift
run "$cmd" "$@"
# Some debug information to make life easier.
echo "$(basename "$cmd") $@ (status=$status)" >&2
echo "$output" >&2
}
function setup_image() {
cp -r "${SOURCE_IMAGE}" "${IMAGE}"
image-verify "${IMAGE}"
# These are just used for diagnostics, so we ignore the status.
sane_run df
sane_run du -h -d 2 "$UMOCI_TMPDIR"
}
function teardown_image() {
rm_rf "${IMAGE}"
}
function setup_runc() {
declare -g RUNC_ROOT="$(setup_tmpdir)"
}
function retry() {
local attempts="$1" delay="$2"
shift 2
local i
for ((i = 0; i < attempts; i++)); do
sane_run "$@"
if [[ "$status" -eq 0 ]]; then
return 0
fi
sleep "$delay"
done
echo "command \"$*\" failed $attempts times" >&2
false
}
function is_container_dead() {
runc state "$1"
[ "$status" -ne 0 ] || [[ "$output" = *stopped* ]]
}
function teardown_runc() {
for ctr in $(runc list -q)
do
runc kill "$ctr" KILL
retry 10 1 eval "is_container_dead '$ctr'"
runc delete -f "$ctr"
done
}
# Generate a new $BUNDLE and $ROOTFS combination.
function new_bundle_rootfs() {
declare -g BUNDLE="$(setup_tmpdir)"
declare -g ROOTFS="$BUNDLE/rootfs"
}
# _getfattr is a sane wrapper around getfattr(1) which only extracts the value
# of the requested xattr (and removes any of the other crap that it spits out).
# The usage is "sane_run _getfattr <xattr name> <path>" and outputs the hex
# representation in a single line. Exit status is non-zero if the xattr isn't
# set.
function _getfattr() {
# We only support single-file inputs.
[ "$#" -eq 2 ] || return 1
local xattr="$1"
local path="$2"
# Run getfattr.
(
set -o pipefail
getfattr -e hex -n "$xattr" "$path" 2>/dev/null \
| grep "^$xattr=" | sed "s|^$xattr=||g"
)
return $?
}
# A wrapper around "rm -rf" which works for unprivileged users (namely cases
# where a directory has mode 000 which causes a DAC refusal even though the
# user is the owner of the directory).
function rm_rf() {
for path in "$@"
do
[ -e "$path" ] || continue
dir="$(dirname "$path")"
# We need to make sure the parent directory is searchable and writable
# but we want to recover the original mode after we do the delete.
old_mode="$(stat -c '%a' "$dir")"
sane_run chmod 0777 "$dir"
[ "$status" -eq 0 ]
[ -d "$path" ] && find "$path" -type d -exec chmod +rwx {} \;
rm -rf "$path"
sane_run chmod "$old_mode" "$dir"
[ "$status" -eq 0 ]
done
}
# blob-path <hash> returns the subpath inside an OCI layout for the hash.
function blob-path() {
local blobhash="$1"
sed -E 's|^([^:]*):(.*)$|blobs/\1/\2|' <<<"$blobhash"
}
# insert-blob <media-type>
#
# Inserts the data read from stdin into the layout and returns a descriptor for
# this blob. Use blob-path on the digest of the descriptor to get a path to the
# blob.
function insert-blob() {
local mediatype="$1"
# Get the input into a file.
local blob_file
blob_file="$(mktemp "$IMAGE/.umoci-test-blob.XXXXXXXX")"
cat - >"$blob_file"
# Get the size.
local blob_size
blob_size="$(wc -c "$blob_file" | awk '{ print $1 }')"
local hash_type="sha256" # TODO: Make configurable.
local hashcmd="${hash_type}sum"
# Compute the hash.
local blob_hash
blob_hash="${hash_type}:$("$hashcmd" "$blob_file" | awk '{ print $1 }')"
# Move the blob to the right path and return the hash.
local blob_path
blob_path="$IMAGE/$(blob-path "$blob_hash")"
mkdir -p "$(dirname "$blob_path")"
mv -n "$blob_file" "$blob_path"
# Construct a digest.
echo '{"mediaType":"'"$mediatype"'","digest":"'"$blob_hash"'","size":'"$blob_size"'}'
}