diff --git a/COVERAGE.md b/COVERAGE.md index d69e539..8ac0c2c 100644 --- a/COVERAGE.md +++ b/COVERAGE.md @@ -71,6 +71,31 @@ The gate stays at 99. The stub `tests/layer` puts where `bt-layer` expects `generate-signature` is excluded by its scratch path, so the stub's 100 percent cannot stand in for the real script's number. +## 2026-09-27: units in the layer manifest + +`bt-layer` records the components a layer carries as fab units, and a child +layer subtracts the ones its parent applied instead of applying them again. +Ten new functions in `bin/layer-lib` (`layer_unit_content`, +`layer_unit_digest`, `layer_unit_version`, `layer_unit_check`, `layer_units`, +`layer_units_version`, `layer_child_units`, `layer_units_merge`, +`layer_unit_paths`, `layer_manifest_opt`) and six cases in `tests/layer` +covering a rootfs layer with a unit, a child that composes the same unit, a +child that adds one, a child with none of its own, a version conflict and a +unit whose `conf` fab would skip. + +| File | Lines covered | Cover | +|------|---------------|-------| +| bin/layer-lib | 331 of 332 | 99 percent | +| bt-layer | 110 of 111 | 99 percent | +| bin/aplinfo-lib | 186 of 186 | 100 percent | +| bt-aplinfo | 47 of 47 | 100 percent | +| bin/signature-lib | 76 of 76 | 100 percent | +| bin/generate-signature | 87 of 87 | 100 percent | + +The gate stays at 99. Both missing lines are the continuation of a +multi-line command, which kcov cannot attribute; every function added here +is fully covered, so nothing new was written in that shape. + ## 2026-09-27: the cipher list of a child layer `layer_needs_ssl_ciphers` decides whether a child layer has to run diff --git a/bin/layer-lib b/bin/layer-lib index 58b89c9..780d45b 100755 --- a/bin/layer-lib +++ b/bin/layer-lib @@ -36,6 +36,31 @@ LAYER_SUBMOUNTS="run dev/pts dev proc sys" LAYER_ZSTD_LEVEL=19 +# A unit is a component supplied to fab as a directory under the product's +# UNIT_DIRS, carrying any of these four inputs. fab resolves the plan, applies +# the overlay, runs the conf script and applies the removelist. +LAYER_UNIT_INPUTS="plan overlay conf removelist" + +# What the manifest says when a layer has no unit, and what a field missing +# from an older manifest is read as. Never a valid unit name, since a name +# has to be followed by @version. +LAYER_UNITS_NONE=none + +# A unit token is name@version. The name is the directory name, under the +# same rule as a layer name; the version has no @ and no space, so the two +# halves of a token are never ambiguous. +LAYER_UNIT_NAME_RE='^[a-z0-9][a-z0-9._-]*$' +LAYER_UNIT_VERSION_RE='^[A-Za-z0-9][A-Za-z0-9._+~-]*$' +LAYER_UNIT_VERSION_MAX=64 + +# The first line of a unit's version file that is neither blank nor a +# comment, trimmed of the whitespace around it +LAYER_UNIT_VERSION_AWK='{ sub(/#.*/, ""); gsub(/^[[:space:]]+|[[:space:]]+$/, ""); if ($0 != "") { print; exit } }' + +# How many hex digits of the content digest a unit version keeps when the +# unit pins itself neither by a version file nor by being its own checkout. +LAYER_UNIT_DIGEST_LENGTH=16 + layer_error() { echo "ERROR [layer-lib]: $*" 1>&2 return 1 @@ -169,6 +194,214 @@ layer_child_overlays() { echo "$LAYER_CHILD_OVERLAYS $child" | sed 's/ $//' } +# layer_unit_content DIR: what the unit is made of, one line per path with +# its type, mode and symlink target, then the sha256 of every file. Sorted in +# the C locale, so the listing does not move with the caller's environment; +# .git is left out, so a unit that is its own checkout reads the same as the +# same files unpacked from a tarball. +layer_unit_content() { + local dir=$1 + ( cd "$dir" && find . -path ./.git -prune -o -printf '%y %m %P %l\n' | LC_ALL=C sort ) || return 1 + ( cd "$dir" && find . -path ./.git -prune -o -type f -printf '%P\0' | LC_ALL=C sort -z | xargs -0 -r sha256sum ) || return 1 +} + +# layer_unit_digest DIR: a version for a unit that pins itself no other way, +# derived from what the directory holds. +layer_unit_digest() { + local dir=$1 + local digest + [[ -d "$dir" ]] || { layer_error "not a unit directory: $dir"; return 1; } + digest=$(layer_unit_content "$dir" | sha256sum | cut -d ' ' -f 1) || return 1 + echo "sha256-${digest:0:$LAYER_UNIT_DIGEST_LENGTH}" +} + +# layer_unit_version DIR: the version of one unit, from the first of three +# sources that answers. +# +# a version file the pin the recipe declared, written by whatever +# materialised unit.d; authoritative when present +# its own git HEAD a component checked out as its own repository, the +# same pin common_commit records for the shared tree; +# only when DIR is the top of that work tree, so the +# commit of an enclosing product repository is never +# passed off as the component's +# the content digest so that an assembled or scratch unit still has an +# identity, and two different contents never record +# the same version +layer_unit_version() { + local dir=$1 + local name version toplevel + name=$(basename "$dir") + if [[ -f "$dir/version" ]]; then + version=$(awk "$LAYER_UNIT_VERSION_AWK" "$dir/version") + if [[ ! "$version" =~ $LAYER_UNIT_VERSION_RE ]] || [[ ${#version} -gt $LAYER_UNIT_VERSION_MAX ]]; then + layer_error "unit '$name': $dir/version is not a version: '$version'" + return 1 + fi + echo "$version" + return 0 + fi + toplevel=$(git -C "$dir" rev-parse --show-toplevel 2>/dev/null) || toplevel= + if [[ -n "$toplevel" ]] && [[ "$(readlink -f "$toplevel")" == "$(readlink -f "$dir")" ]]; then + version=$(git -C "$dir" rev-parse HEAD 2>/dev/null) || version= + if [[ -n "$version" ]]; then + echo "$version" + return 0 + fi + fi + layer_unit_digest "$dir" +} + +# layer_unit_check DIR: refuse a unit directory fab would misread. An empty +# directory contributes nothing and means the assembly step went wrong, and a +# conf script without the execute bit is skipped by fab without a word, which +# is the same silent no-op the manifest work exists to end. +layer_unit_check() { + local dir=$1 + local name input found= + name=$(basename "$dir") + for input in $LAYER_UNIT_INPUTS; do + [[ ! -e "$dir/$input" ]] || found=yes + done + if [[ -z "$found" ]]; then + layer_error "unit '$name' carries none of: $LAYER_UNIT_INPUTS" + return 1 + fi + if [[ -e "$dir/conf" && ! -x "$dir/conf" ]]; then + layer_error "unit '$name': conf is not executable, so fab would skip it" + return 1 + fi + if [[ -e "$dir/overlay" && ! -d "$dir/overlay" ]]; then + layer_error "unit '$name': overlay is not a directory" + return 1 + fi + return 0 +} + +# layer_units PRODUCT_DIR UNIT_DIRS: the units the product supplies, as +# name@version sorted by name, or none. UNIT_DIRS is the product-relative +# directory fab reads them from; empty means the product's fab has no unit +# support, which is not an error. +layer_units() { + local product_dir=$1 + local unit_dirs=$2 + local base=$product_dir/$unit_dirs + local path name version out=() + if [[ -z "$unit_dirs" ]] || [[ ! -d "$base" ]]; then + echo "$LAYER_UNITS_NONE" + return 0 + fi + for path in "$base"/*; do + [[ -e "$path" ]] || continue + name=$(basename "$path") + if [[ ! -d "$path" ]]; then + layer_error "$unit_dirs/$name is not a directory" + return 1 + fi + if [[ ! "$name" =~ $LAYER_UNIT_NAME_RE ]]; then + layer_error "invalid unit name '$name' in $base" + return 1 + fi + layer_unit_check "$path" || return 1 + version=$(layer_unit_version "$path") || return 1 + out+=("$name@$version") + done + if [[ ${#out[@]} -eq 0 ]]; then + echo "$LAYER_UNITS_NONE" + return 0 + fi + printf '%s\n' "${out[@]}" | LC_ALL=C sort | tr '\n' ' ' | sed 's/ $//' +} + +# layer_units_version UNITS NAME: the version UNITS records for NAME, empty +# when it records none +layer_units_version() { + local units=$1 + local wanted=$2 + local token + for token in $units; do + [[ "$token" != "$LAYER_UNITS_NONE" ]] || continue + if [[ "${token%@*}" == "$wanted" ]]; then + echo "${token##*@}" + return 0 + fi + done + return 0 +} + +# layer_child_units OWN PARENT: the units this build has to apply, which are +# the ones the parent layer did not. +# +# A unit the parent applied at another version is refused rather than +# applied: a conf script runs once, so there is no move from one version of a +# component to another inside a child layer, and re-running the script on top +# of the old version is what breaks (conf/mysql symlinks /etc/init.d/mysql +# under bash -e and fails the second time). The parent is the thing to +# rebuild. +layer_child_units() { + local own=$1 + local parent=$2 + local out=() token name version seen + for token in $own; do + [[ "$token" != "$LAYER_UNITS_NONE" ]] || continue + name=${token%@*} + version=${token##*@} + seen=$(layer_units_version "$parent" "$name") + if [[ -z "$seen" ]]; then + out+=("$token") + elif [[ "$seen" != "$version" ]]; then + layer_error "unit '$name' is $version here and $seen in the parent" \ + "layer; a conf script runs once, so rebuild the parent instead" + return 1 + fi + done + if [[ ${#out[@]} -eq 0 ]]; then + echo "$LAYER_UNITS_NONE" + return 0 + fi + echo "${out[*]}" +} + +# layer_units_merge PARENT OWN: every unit the finished layer carries, the +# parent's and this build's, sorted by name. +# +# The field is cumulative on purpose. A layer built on this one subtracts +# what this one records, so a component must stay recorded even when the +# recipe of this layer does not declare it: a child that carries no database +# content of its own still sits on the database, and its own child has to +# know that. Where both name the same unit the versions agree, because +# layer_child_units refuses the build otherwise. +layer_units_merge() { + local parent=$1 + local own=$2 + local out=() token name + for token in $own $parent; do + [[ "$token" != "$LAYER_UNITS_NONE" ]] || continue + name=${token%@*} + [[ -z "$(layer_units_version "${out[*]}" "$name")" ]] || continue + out+=("$token") + done + if [[ ${#out[@]} -eq 0 ]]; then + echo "$LAYER_UNITS_NONE" + return 0 + fi + printf '%s\n' "${out[@]}" | LC_ALL=C sort | tr '\n' ' ' | sed 's/ $//' +} + +# layer_unit_paths UNIT_DIRS UNITS: the directories fab has to apply, the +# value of its UNITS variable. Empty for none, which is what a child layer +# that inherits every one of its components passes. +layer_unit_paths() { + local unit_dirs=$1 + local units=$2 + local out=() token + for token in $units; do + [[ "$token" != "$LAYER_UNITS_NONE" ]] || continue + out+=("$unit_dirs/${token%@*}") + done + echo "${out[*]}" +} + layer_manifest_render() { local key value if [[ $(( $# % 2 )) -ne 0 ]]; then @@ -203,6 +436,22 @@ layer_manifest_get() { echo "$value" } +# layer_manifest_opt MANIFEST KEY DEFAULT: the value of KEY, or DEFAULT when +# the manifest does not carry that field. Manifests written before a field +# existed stay usable as parents; a missing file is still an error. +layer_manifest_opt() { + local manifest=$1 + local key=$2 + local default=$3 + local value + if [[ ! -f "$manifest" ]]; then + layer_error "manifest not found: $manifest" + return 1 + fi + value=$(sed -n "s|^$key ||p" "$manifest" | head -n 1) + echo "${value:-$default}" +} + layer_tar_flags() { local epoch=$1 local pattern diff --git a/bt-layer b/bt-layer index 1bfa244..cbab812 100755 --- a/bt-layer +++ b/bt-layer @@ -30,8 +30,9 @@ Options:: -p|--parent LAYER - build on top of layer LAYER (exported earlier by $APP on this host) instead of the bootstrap; only - the overlays and conf scripts the parent did not - run are applied, plus the ones every layer needs + the overlays, conf scripts and units the parent did + not apply are applied, plus the ones every layer + needs -u|--use-existing - skip the build and export the existing build/root.patched (fails if it is missing) @@ -52,7 +53,11 @@ Output, in \$BT_BUILDS/layers/:: layer.tar.zst full rootfs without --parent, the root.build and root.patched deck upper directories with --parent layer.tar.zst.sha256 sha256sum of the tarball - layer.manifest plain text, one "key value" per line + layer.manifest plain text, one "key value" per line; "units" + lists every component this layer carries as a fab + unit, as name@version, the parent's and this + build's, and "build_units" the ones this build + applied layer.rootfs/ copy of root.patched, the altstrap for children layer.log build log @@ -113,8 +118,14 @@ full_conf=$(layer_make_var "$product_dir" COMMON_CONF) \ || fatal "cannot read COMMON_CONF of $layer" full_overlays=$(layer_make_var "$product_dir" COMMON_OVERLAYS) \ || fatal "cannot read COMMON_OVERLAYS of $layer" +unit_dirs=$(layer_make_var "$product_dir" UNIT_DIRS) \ + || fatal "cannot read UNIT_DIRS of $layer" +own_units=$(layer_units "$product_dir" "$unit_dirs") \ + || fatal "cannot read the units of $layer" +full_units=$own_units build_conf=default build_overlays=default +build_units=default make_args=() if [[ -n "$parent" ]]; then parent_manifest=$layers_dir/$parent.manifest @@ -122,11 +133,18 @@ if [[ -n "$parent" ]]; then || fatal "parent manifest is incomplete" parent_overlays=$(layer_manifest_get "$parent_manifest" common_overlays) \ || fatal "parent manifest is incomplete" + parent_units=$(layer_manifest_opt "$parent_manifest" units "$LAYER_UNITS_NONE") \ + || fatal "parent manifest is unreadable" build_overlays=$(layer_child_overlays "$full_overlays" "$parent_overlays") build_conf=$(layer_child_conf "$full_conf" "$parent_conf" "$FAB_PATH/common/conf" "$FAB_PATH/common/overlays" "$build_overlays") - make_args=("COMMON_OVERLAYS=$build_overlays" "COMMON_CONF=$build_conf") + build_units=$(layer_child_units "$own_units" "$parent_units") \ + || fatal "cannot work out the units of $layer" + full_units=$(layer_units_merge "$parent_units" "$own_units") + build_unit_paths=$(layer_unit_paths "$unit_dirs" "$build_units") + make_args=("COMMON_OVERLAYS=$build_overlays" "COMMON_CONF=$build_conf" "UNITS=$build_unit_paths") info "overlays for this layer: $build_overlays" info "conf for this layer: $build_conf" + info "units for this layer: $build_units (of $full_units)" fi altstrap= @@ -197,8 +215,10 @@ layer_manifest_render \ source_date_epoch "$SOURCE_DATE_EPOCH" \ common_overlays "$full_overlays" \ common_conf "$full_conf" \ + units "$full_units" \ build_overlays "$build_overlays" \ build_conf "$build_conf" \ + build_units "$build_units" \ tarball "$(basename "$tarball")" \ sha256 "$sha256" \ size "$size" \ diff --git a/tests/layer b/tests/layer index e09d7d3..0b4d2fd 100755 --- a/tests/layer +++ b/tests/layer @@ -2,14 +2,26 @@ # Test bt-layer and bin/layer-lib against scratch directories. Nothing is # built: products are fake Makefiles, fab and the signature helper are stubs. # -# Code paths covered (42 of 42): +# Code paths covered: # layer-lib: layer_kind (rootfs, delta); layer_altstrap_path; layer_epoch # (env, env invalid, git fallback, no git); layer_git_commit (git, none); # layer_make_var (ok, failure); layer_words_has; layer_words_minus; # layer_child_conf (stack on core with ssl ciphers, app on stack, no # turnkey.d, overlay carrying the ciphers mark, overlay without it); -# layer_needs_ssl_ciphers; layer_child_overlays; layer_manifest_render (ok, odd args, +# layer_needs_ssl_ciphers; layer_child_overlays; layer_unit_content and +# layer_unit_digest (same content, file content, directory mode, symlink, +# symlink target, .git ignored, missing directory); layer_unit_version +# (version file, comments and whitespace, not a version, an @ in it, too +# long, own checkout, a unit inside another repository, digest); +# layer_unit_check (every input, plan only, empty, conf not executable, +# overlay not a directory); layer_units (no unit.d, no UNIT_DIRS, empty +# unit.d, sorted, not a directory, bad name, hollow unit); +# layer_units_version (found, absent, none); layer_child_units (parent +# has none, has one of two, has both, own none, version conflict); +# layer_units_merge (none, own only, parent only, union, overlap); +# layer_unit_paths (none, two); layer_manifest_render (ok, odd args, # bad key); layer_manifest_get (ok, missing file, missing key); +# layer_manifest_opt (present, absent, missing file); # layer_tar_flags; layer_tar_members (rootfs, delta, unknown); # layer_audit_packages (clean, unconfigured package); # layer_check_build (ok, no stamp, not mounted, no upper); @@ -23,7 +35,11 @@ # altstrap created and removed, hash file with BT_GPGKEY); repeated # export with -u is byte identical; -u without a build; unknown parent; # stale altstrap symlink; parent manifest without conf list; product -# without a debug target; failing make removes the altstrap; TERM default. +# without a debug target; failing make removes the altstrap; TERM default; +# a rootfs layer with a unit; a child that composes the same unit; a child +# that adds one; a child with no unit of its own; a unit pinned to another +# version than the parent applied; a unit whose conf fab would skip; a +# parent manifest written before the units field existed. fatal() { echo "FATAL [$(basename "$0")]: $*" 1>&2; exit 1; } info() { echo "INFO [$(basename "$0")]: $*"; } @@ -79,6 +95,10 @@ write_product() { COMMON_CONF := $conf COMMON_OVERLAYS := $overlays +# as share/product.mk defines them +UNIT_DIRS ?= unit.d +UNITS ?= \$(sort \$(wildcard \$(UNIT_DIRS)/*)) + debug: \$(foreach v,\$(V),\$(warning \$(v) = \$(\$(v)))) @true @@ -101,6 +121,7 @@ build/stamps/root.patched: echo conf > build/.deck/root.patched/upper/etc/x.conf echo "\$(COMMON_OVERLAYS)" > build/overlays.used echo "\$(COMMON_CONF)" > build/conf.used + echo "\$(UNITS)" > build/units.used echo "\$\$TERM" > build/term.used readlink "\$(FAB_PATH)/altstraps/trixie-amd64.core" > build/altstrap.used || true touch build/stamps/root.patched @@ -175,6 +196,136 @@ assert_eq child-overlays "$(layer_child_overlays "turnkey.d apache php" "turnkey assert_eq child-overlays-none "$(layer_child_overlays "turnkey.d" "turnkey.d")" \ "turnkey.d/tkl-bashlib turnkey.d/systemd-chroot" +info "8b: layer_unit_digest" +units=$tmp/units +write_unit() { + local dir=$units/$1 + mkdir -p "$dir/overlay/etc" + echo "${2:-body}" > "$dir/overlay/etc/file" + printf '#!/bin/sh\ntrue\n' > "$dir/conf" + chmod 755 "$dir/conf" + : > "$dir/plan" +} +write_unit alpha +write_unit beta +digest_alpha=$(layer_unit_digest "$units/alpha") +[[ "$digest_alpha" =~ ^sha256-[0-9a-f]{16}$ ]] \ + || fatal "digest is not sha256- plus 16 hex digits: $digest_alpha" +assert_eq digest-same "$(layer_unit_digest "$units/beta")" "$digest_alpha" +assert_eq digest-stable "$(layer_unit_digest "$units/alpha")" "$digest_alpha" +echo other > "$units/beta/overlay/etc/file" +[[ "$(layer_unit_digest "$units/beta")" != "$digest_alpha" ]] \ + || fatal "digest ignores file content" +echo "${digest_alpha}" > /dev/null +write_unit gamma +chmod 700 "$units/gamma/overlay/etc" +[[ "$(layer_unit_digest "$units/gamma")" != "$digest_alpha" ]] \ + || fatal "digest ignores the mode of a directory" +write_unit delta +ln -s file "$units/delta/overlay/etc/link" +digest_delta=$(layer_unit_digest "$units/delta") +[[ "$digest_delta" != "$digest_alpha" ]] || fatal "digest ignores a symlink" +ln -sfn other "$units/delta/overlay/etc/link" +[[ "$(layer_unit_digest "$units/delta")" != "$digest_delta" ]] \ + || fatal "digest ignores a symlink target" +write_unit epsilon +git -C "$units/epsilon" init -q +assert_eq digest-ignores-git "$(layer_unit_digest "$units/epsilon")" "$digest_alpha" +expect_fail digest-missing layer_unit_digest "$tmp/no-such-unit" + +info "8c: layer_unit_version" +echo '1.2.0' > "$units/alpha/version" +assert_eq version-file "$(layer_unit_version "$units/alpha")" 1.2.0 +printf '# the pin\n\n 2.0.0~rc1 \n' > "$units/alpha/version" +assert_eq version-file-comment "$(layer_unit_version "$units/alpha")" "2.0.0~rc1" +echo 'not a version' > "$units/alpha/version" +expect_fail version-file-bad layer_unit_version "$units/alpha" +echo 'a@b' > "$units/alpha/version" +expect_fail version-file-at layer_unit_version "$units/alpha" +printf 'v%065d\n' 1 > "$units/alpha/version" +expect_fail version-file-long layer_unit_version "$units/alpha" +rm "$units/alpha/version" +assert_eq version-digest "$(layer_unit_version "$units/alpha")" "$digest_alpha" +own=$tmp/own-checkout +mkdir -p "$own" +write_unit_at() { + mkdir -p "$1/overlay" + echo body > "$1/overlay/file" +} +write_unit_at "$own" +git -C "$own" init -q +git -C "$own" -c user.name=t -c user.email=t@[2001:db8::1] add -A +git -C "$own" -c user.name=t -c user.email=t@[2001:db8::1] commit -q -m unit +assert_eq version-own-git "$(layer_unit_version "$own")" "$(git -C "$own" rev-parse HEAD)" +inner=$own/unit.d/nested +write_unit_at "$inner" +[[ "$(layer_unit_version "$inner")" != "$(git -C "$own" rev-parse HEAD)" ]] \ + || fatal "a unit inside a repository reported the repository's commit" +assert_eq version-nested-digest "$(layer_unit_version "$inner")" \ + "$(layer_unit_digest "$inner")" + +info "8d: layer_unit_check" +expect_ok unit-check-full layer_unit_check "$units/beta" +mkdir -p "$units/plan-only" +: > "$units/plan-only/plan" +expect_ok unit-check-plan-only layer_unit_check "$units/plan-only" +mkdir -p "$units/empty" +expect_fail unit-check-empty layer_unit_check "$units/empty" +mkdir -p "$units/notexec/overlay" +printf '#!/bin/sh\ntrue\n' > "$units/notexec/conf" +chmod 644 "$units/notexec/conf" +expect_fail unit-check-notexec layer_unit_check "$units/notexec" +mkdir -p "$units/flatoverlay" +: > "$units/flatoverlay/overlay" +expect_fail unit-check-overlay-file layer_unit_check "$units/flatoverlay" + +info "8e: layer_units" +prod=$tmp/unitprod +mkdir -p "$prod" +assert_eq units-no-dir "$(layer_units "$prod" unit.d)" none +assert_eq units-no-var "$(layer_units "$prod" "")" none +mkdir -p "$prod/unit.d" +assert_eq units-empty-dir "$(layer_units "$prod" unit.d)" none +cp -a "$units/beta" "$prod/unit.d/mariadb" +echo '1.0.0' > "$prod/unit.d/mariadb/version" +cp -a "$units/plan-only" "$prod/unit.d/adminer" +echo '0.9' > "$prod/unit.d/adminer/version" +assert_eq units-sorted "$(layer_units "$prod" unit.d)" "adminer@0.9 mariadb@1.0.0" +: > "$prod/unit.d/README" +expect_fail units-not-a-dir layer_units "$prod" unit.d +rm "$prod/unit.d/README" +mkdir -p "$prod/unit.d/Bad_Name" +: > "$prod/unit.d/Bad_Name/plan" +expect_fail units-bad-name layer_units "$prod" unit.d +rm -rf "$prod/unit.d/Bad_Name" +mkdir -p "$prod/unit.d/hollow" +expect_fail units-hollow layer_units "$prod" unit.d +rm -rf "$prod/unit.d/hollow" +assert_eq units-after-cleanup "$(layer_units "$prod" unit.d)" "adminer@0.9 mariadb@1.0.0" + +info "8f: layer_units_version, layer_child_units, layer_units_merge, layer_unit_paths" +assert_eq units-version "$(layer_units_version "adminer@0.9 mariadb@1.0.0" mariadb)" 1.0.0 +assert_eq units-version-missing "$(layer_units_version "adminer@0.9" mariadb)" "" +assert_eq units-version-none "$(layer_units_version none mariadb)" "" +assert_eq child-units-all "$(layer_child_units "adminer@0.9 mariadb@1.0.0" none)" \ + "adminer@0.9 mariadb@1.0.0" +assert_eq child-units-partial \ + "$(layer_child_units "adminer@0.9 mariadb@1.0.0" "mariadb@1.0.0")" "adminer@0.9" +assert_eq child-units-none \ + "$(layer_child_units "mariadb@1.0.0" "adminer@0.9 mariadb@1.0.0")" none +assert_eq child-units-own-none "$(layer_child_units none "mariadb@1.0.0")" none +expect_fail child-units-conflict layer_child_units "mariadb@2.0.0" "mariadb@1.0.0" +assert_eq merge-none "$(layer_units_merge none none)" none +assert_eq merge-own "$(layer_units_merge none "mariadb@1.0.0")" "mariadb@1.0.0" +assert_eq merge-parent "$(layer_units_merge "mariadb@1.0.0" none)" "mariadb@1.0.0" +assert_eq merge-union "$(layer_units_merge "mariadb@1.0.0" "adminer@0.9")" \ + "adminer@0.9 mariadb@1.0.0" +assert_eq merge-overlap "$(layer_units_merge "mariadb@1.0.0" "mariadb@1.0.0")" \ + "mariadb@1.0.0" +assert_eq paths-none "$(layer_unit_paths unit.d none)" "" +assert_eq paths-two "$(layer_unit_paths unit.d "adminer@0.9 mariadb@1.0.0")" \ + "unit.d/adminer unit.d/mariadb" + info "9: layer_manifest_render and layer_manifest_get" manifest=$tmp/test.manifest layer_manifest_render layer core sha256 abc common_conf "turnkey.d php" > "$manifest" @@ -184,6 +335,9 @@ expect_fail manifest-odd layer_manifest_render layer core sha256 expect_fail manifest-badkey layer_manifest_render "Bad Key" x expect_fail manifest-nofile layer_manifest_get "$tmp/none.manifest" sha256 expect_fail manifest-nokey layer_manifest_get "$manifest" size +assert_eq manifest-opt "$(layer_manifest_opt "$manifest" common_conf none)" "turnkey.d php" +assert_eq manifest-opt-default "$(layer_manifest_opt "$manifest" units none)" none +expect_fail manifest-opt-nofile layer_manifest_opt "$tmp/none.manifest" units none info "10: layer_tar_flags and layer_tar_members" flags=$(layer_tar_flags 1700000000) @@ -408,5 +562,81 @@ expect_fail make-fails "$sbt/bt-layer" broken --parent core [[ ! -e "$FAB_PATH/altstraps/trixie-amd64.core" ]] || fatal "altstrap left behind after failed make" grep -q 'test ! -f FAIL' "$L/broken.log" || fatal "build log not written on failure" +info "22: bt-layer with units (a component supplied as a fab unit)" +# add_unit PRODUCT NAME VERSION: a unit fab would apply, pinned by a version +# file the way an assembly step would write it +add_unit() { + local dir=$tmp/products/$1/unit.d/$2 + mkdir -p "$dir/overlay/etc" + echo "$2" > "$dir/overlay/etc/$2.conf" + printf '#!/bin/sh\ntrue\n' > "$dir/conf" + chmod 755 "$dir/conf" + : > "$dir/plan" + echo "$3" > "$dir/version" +} +assert_eq core-units "$(layer_manifest_get "$L/core.manifest" units)" none +assert_eq core-build-units "$(layer_manifest_get "$L/core.manifest" build_units)" default +assert_eq lamp-units "$(layer_manifest_get "$L/lamp.manifest" units)" none +assert_eq lamp-build-units "$(layer_manifest_get "$L/lamp.manifest" build_units)" none +assert_eq lamp-units-used "$(cat "$tmp/products/lamp/build/units.used")" "" + +write_product "$tmp/products/db" "turnkey.d mysql" "turnkey.d mysql" +add_unit db mariadb 1.0.0 +"$sbt/bt-layer" db >/dev/null +assert_eq db-units "$(layer_manifest_get "$L/db.manifest" units)" "mariadb@1.0.0" +assert_eq db-build-units "$(layer_manifest_get "$L/db.manifest" build_units)" default +assert_eq db-units-used "$(cat "$tmp/products/db/build/units.used")" "unit.d/mariadb" + +info "22a: a child that composes the same unit does not apply it again" +write_product "$tmp/products/app" "turnkey.d mysql" "turnkey.d mysql" +add_unit app mariadb 1.0.0 +out=$("$sbt/bt-layer" app --parent db) +grep -q 'units for this layer: none' <<<"$out" || fatal "expected no units for app" +assert_eq app-units "$(layer_manifest_get "$L/app.manifest" units)" "mariadb@1.0.0" +assert_eq app-build-units "$(layer_manifest_get "$L/app.manifest" build_units)" none +assert_eq app-units-used "$(cat "$tmp/products/app/build/units.used")" "" + +info "22b: a child adds only the unit the parent has not applied" +write_product "$tmp/products/app2" "turnkey.d mysql" "turnkey.d mysql" +add_unit app2 mariadb 1.0.0 +add_unit app2 adminer 0.9 +"$sbt/bt-layer" app2 --parent db >/dev/null +assert_eq app2-units "$(layer_manifest_get "$L/app2.manifest" units)" \ + "adminer@0.9 mariadb@1.0.0" +assert_eq app2-build-units "$(layer_manifest_get "$L/app2.manifest" build_units)" \ + "adminer@0.9" +assert_eq app2-units-used "$(cat "$tmp/products/app2/build/units.used")" "unit.d/adminer" + +info "22c: a child with no unit of its own still records the parent's" +write_product "$tmp/products/app3" "turnkey.d" "turnkey.d" +"$sbt/bt-layer" app3 --parent db >/dev/null +assert_eq app3-units "$(layer_manifest_get "$L/app3.manifest" units)" "mariadb@1.0.0" +assert_eq app3-build-units "$(layer_manifest_get "$L/app3.manifest" build_units)" none +assert_eq app3-units-used "$(cat "$tmp/products/app3/build/units.used")" "" + +info "22d: a unit pinned to another version than the parent applied is refused" +write_product "$tmp/products/app4" "turnkey.d mysql" "turnkey.d mysql" +add_unit app4 mariadb 2.0.0 +out=$("$sbt/bt-layer" app4 --parent db 2>&1 || true) +grep -q "unit 'mariadb' is 2.0.0 here and 1.0.0 in the parent" <<<"$out" \ + || fatal "version conflict not reported: $out" +[[ ! -e "$L/app4.manifest" ]] || fatal "app4 layer exported despite the conflict" +[[ ! -e "$FAB_PATH/altstraps/trixie-amd64.core" ]] || fatal "altstrap left behind" + +info "22e: a unit fab would skip, and a manifest older than the units field" +mkdir -p "$tmp/products/app5" +write_product "$tmp/products/app5" "turnkey.d" "turnkey.d" +add_unit app5 broken 1.0 +chmod 644 "$tmp/products/app5/unit.d/broken/conf" +out=$("$sbt/bt-layer" app5 --parent db 2>&1 || true) +grep -q "conf is not executable" <<<"$out" || fatal "non-executable conf not caught: $out" +chmod 755 "$tmp/products/app5/unit.d/broken/conf" +sed -i '/^units /d' "$L/db.manifest" +"$sbt/bt-layer" app5 --parent db >/dev/null +assert_eq app5-units "$(layer_manifest_get "$L/app5.manifest" units)" "broken@1.0" +assert_eq app5-build-units "$(layer_manifest_get "$L/app5.manifest" build_units)" \ + "broken@1.0" +assert_eq app5-units-used "$(cat "$tmp/products/app5/build/units.used")" "unit.d/broken" + echo info "all tests passed"