diff --git a/lib/API.txt b/lib/API.txt index 2d3fe27..1181a97 100644 --- a/lib/API.txt +++ b/lib/API.txt @@ -18,12 +18,14 @@ _forge_asset_urls _forge_curl _forge_descriptor _forge_designated_release +_forge_disk_full _forge_explain_failure _forge_highest_version _forge_http_get _forge_is_current _forge_is_script_app _forge_keep_raw +_forge_place _forge_previous_version _forge_release_json _forge_require_jq diff --git a/lib/forge.func b/lib/forge.func index 291567c..5cb0995 100644 --- a/lib/forge.func +++ b/lib/forge.func @@ -569,6 +569,33 @@ _forge_workdir() { mktemp -d -p "${TMPDIR:-/var/tmp}" 2>/dev/null || mktemp -d -p /tmp } +# Puts the entries of into : moved where has no +# such entry yet, copied over it otherwise. Staging is on disk, so a plain +# cp -r had the release there twice and filled small containers. +_forge_place() { + local src="$1" target="$2" entry rc=0 + shopt -s dotglob nullglob + for entry in "$src"/*; do + if [[ -e "$target/${entry##*/}" || -L "$target/${entry##*/}" ]]; then + cp -r "$entry" "$target/" && rm -rf "$entry" || rc=1 + else + mv "$entry" "$target/" || rc=1 + fi + done + shopt -u dotglob nullglob + return $rc +} + +# After a failed _forge_place: says so and returns 1 when 's filesystem +# is full, returns 0 for any other reason. +_forge_disk_full() { + local avail + avail="$(df -Pk "$1" 2>/dev/null | awk 'NR==2 {print $4}')" + [[ "$avail" =~ ^[0-9]+$ ]] && ((avail < 1024)) || return 0 + msg_error "No space left on the disk while deploying to $1 - give the container a larger disk" + return 1 +} + # ------------------------------------------------------------------------------ # _deploy_source_tarball # @@ -585,9 +612,10 @@ _forge_workdir() { # that the wipe leaves alone (e.g. "geodata .env data/uploads"). # - Does NOT own : the caller creates it and is responsible for its # cleanup (typically via a RETURN trap on its tmpdir). -# - cp failures are non-fatal here, matching the previous inline behavior. +# - A failed copy is fatal only when the disk is full; otherwise it stays +# non-fatal, matching the previous inline behavior. # -# Returns: 0 on success (or non-fatal cp failure), 251 on extraction failure. +# Returns: 0 on success, 251 on extraction failure, 252 when the disk is full. # ------------------------------------------------------------------------------ _deploy_source_tarball() { local tarball="$1" target="$2" workdir="$3" @@ -610,9 +638,7 @@ _deploy_source_tarball() { _cs_clean_target_dir "$target" fi - shopt -s dotglob nullglob - cp -r "$unpack_dir"/* "$target/" - shopt -u dotglob nullglob + _forge_place "$unpack_dir" "$target" || _forge_disk_full "$target" || return 252 return 0 } @@ -715,7 +741,8 @@ _deploy_unpacked_archive() { _cs_clean_target_dir "$target" fi - if ! cp -r "$source_dir"/* "$target/"; then + if ! _forge_place "$source_dir" "$target"; then + _forge_disk_full "$target" || true if [[ -n "$inner_dir" ]]; then msg_error "Failed to copy contents from $inner_dir to $target" else diff --git a/tests/forge-clean-install.sh b/tests/forge-clean-install.sh index 4d3d4a5..8401974 100644 --- a/tests/forge-clean-install.sh +++ b/tests/forge-clean-install.sh @@ -2,7 +2,7 @@ set -euo pipefail CORE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" -eval "$(awk '/^(_cs_clean_target_dir|_deploy_source_tarball|_deploy_unpacked_archive|_forge_workdir)\(\) \{/,/^}/' "$CORE_DIR/lib/forge.func")" +eval "$(awk '/^(_cs_clean_target_dir|_deploy_source_tarball|_deploy_unpacked_archive|_forge_workdir|_forge_place|_forge_disk_full)\(\) \{/,/^}/' "$CORE_DIR/lib/forge.func")" TEST_DIR="$(mktemp -d)" trap 'rm -rf -- "$TEST_DIR"' EXIT @@ -92,5 +92,38 @@ check "workdir: defaults to /var/tmp, not /tmp" "/var/tmp" "${default_workdir%/* rm -rf -- "$default_workdir" check "no helper stages in the mktemp default" "0" "$(awk '/^(fetch_and_deploy_[a-z_]+|_forge_[a-z_]+)\(\) \{/,/^}/' "$CORE_DIR/lib/forge.func" | grep -c '_tmp="\{0,1\}$(mktemp -d)\|tmpdir="\{0,1\}$(mktemp -d)')" +# _forge_place: nothing of the release stays behind in staging, and what the +# target already has is merged, not replaced. +rm -rf "$TEST_DIR/src" "$TEST_DIR/dst" +mkdir -p "$TEST_DIR/src/lib" "$TEST_DIR/src/data" "$TEST_DIR/dst/data" +echo new >"$TEST_DIR/src/app" +echo new >"$TEST_DIR/src/lib/a" +echo new >"$TEST_DIR/src/data/schema" +echo hidden >"$TEST_DIR/src/.env.example" +echo mine >"$TEST_DIR/dst/data/db" +rc=0 +_forge_place "$TEST_DIR/src" "$TEST_DIR/dst" || rc=$? +check "place: succeeds" 0 "$rc" +check "place: staging is empty afterwards" "" "$(ls -A "$TEST_DIR/src")" +check "place: new entries arrive, dotfiles included" "new new hidden" "$(cat "$TEST_DIR/dst/app" "$TEST_DIR/dst/lib/a" "$TEST_DIR/dst/.env.example" | xargs)" +check "place: an existing directory is merged" "mine new" "$(cat "$TEST_DIR/dst/data/db" "$TEST_DIR/dst/data/schema" | xargs)" + +df() { printf 'Filesystem 1024-blocks Used Available Capacity Mounted on\n/dev/x 4000000 4000000 %s 100%% /\n' "${FAKE_AVAIL:-0}"; } +rc=0 +FAKE_AVAIL=12 _forge_disk_full "$TEST_DIR/dst" || rc=$? +check "a full disk is reported" 1 "$rc" +rc=0 +FAKE_AVAIL=900000 _forge_disk_full "$TEST_DIR/dst" || rc=$? +check "a failure with space left is not blamed on the disk" 0 "$rc" + +# The tarball mode used to return 0 whatever cp did. +setup +mkdir -p "$TEST_DIR/pkg/app-1.0" && echo v1 >"$TEST_DIR/pkg/app-1.0/app" +tar -czf "$TEST_DIR/work/app.tar.gz" -C "$TEST_DIR/pkg" app-1.0 +_forge_place() { return 1; } +rc=0 +FAKE_AVAIL=0 _deploy_source_tarball "$TEST_DIR/work/app.tar.gz" "$TEST_DIR/target" "$TEST_DIR/work" || rc=$? +check "tarball: a full disk ends the deploy" 252 "$rc" + echo "${passed} passed, ${failed} failed" ((failed == 0))