Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/API.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 33 additions & 6 deletions lib/forge.func
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,33 @@ _forge_workdir() {
mktemp -d -p "${TMPDIR:-/var/tmp}" 2>/dev/null || mktemp -d -p /tmp
}

# Puts the entries of <source_dir> into <target>: moved where <target> 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 <target>'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 <tarball_path> <target> <workdir>
#
Expand All @@ -585,9 +612,10 @@ _forge_workdir() {
# <target> that the wipe leaves alone (e.g. "geodata .env data/uploads").
# - Does NOT own <workdir>: 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"
Expand All @@ -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
}

Expand Down Expand Up @@ -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
Expand Down
35 changes: 34 additions & 1 deletion tests/forge-clean-install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))