mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-07 17:28:23 +01:00
fix(ubi-runner): an API error is never read as success on the money path (GST-1441)
A refused create disarms teardown, a 5xx/unanswered create is looked up by name, down counts a VM gone only on a readable list, pull fails on ssh failure, gc reports failed destroys, an unanswerable run record refuses instead of starting a second VM, and the API key leaves curl's argv. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
6278ee4fb7
commit
6a612065a2
5 files changed
+276
-42
No files matched your search
@@ -0,0 +1 @@
|
|||||||
|
- **The Ubicloud runner no longer reads an API error as success.** A create Ubicloud refuses (a 409 name collision, any 4xx) now disarms teardown, so `up` and `run` never destroy a VM someone else holds under that name. A 5xx or unanswered create is looked up by name before anything is decided: a VM that landed anyway is adopted, and one nobody can confirm either way keeps its record and recovery bundle and exits `UBI_CREATE_UNKNOWN`. `down` counts a VM as gone only on a readable answer, so a failed or garbled VM list exits non-zero and keeps the bundle. `pull` exits non-zero when ssh fails, and `run` fails when one of its pulls does. `gc` exits non-zero when a stale VM could not be destroyed, a run record whose VM the API cannot check refuses with `UBI_ATTACH_UNKNOWN` instead of starting a second VM, and the API key reaches curl as a header on a pipe, out of `ps`. (GST-1441)
|
||||||
+101
-35
@@ -35,7 +35,15 @@
|
|||||||
# confirmed gone or provably never existed, waiting out a create that may still
|
# confirmed gone or provably never existed, waiting out a create that may still
|
||||||
# be in flight. An interrupted `up` or `run` (EXIT, INT, TERM, QUIT, and HUP
|
# be in flight. An interrupted `up` or `run` (EXIT, INT, TERM, QUIT, and HUP
|
||||||
# until a remote phase is live) runs that `down`; further signals are ignored
|
# until a remote phase is live) runs that `down`; further signals are ignored
|
||||||
# until it finishes. A create refused
|
# until it finishes. Teardown is armed only once this call's create request is
|
||||||
|
# sent, and disarmed when Ubicloud refuses it (HTTP 4xx, a 409 name collision
|
||||||
|
# included), so it never destroys a VM this call did not create. A 5xx or no
|
||||||
|
# answer is not a refusal: `up` looks the VM up by name for UBI_CREATE_GRACE
|
||||||
|
# seconds, and when that cannot settle it either, it keeps the record and the
|
||||||
|
# recovery bundle and exits UBI_CREATE_UNKNOWN. `down` counts a VM as gone only
|
||||||
|
# on a readable answer: a failed or unreadable VM list or show is unknown, so
|
||||||
|
# down exits non-zero and keeps the recovery bundle. The API key reaches curl
|
||||||
|
# as a header file on a pipe, never in its argv. A create refused
|
||||||
# for vCPU quota prints the project's usage per owner.
|
# for vCPU quota prints the project's usage per owner.
|
||||||
#
|
#
|
||||||
# The owner-age sweep is off by default (the TTL reap below always runs). With UBI_GC_HOURS set to a positive
|
# The owner-age sweep is off by default (the TTL reap below always runs). With UBI_GC_HOURS set to a positive
|
||||||
@@ -225,7 +233,7 @@ api() {
|
|||||||
shift
|
shift
|
||||||
code=$(python3 -c 'import json,sys; print(json.dumps({"argv": sys.argv[1:]}))' "$@" \
|
code=$(python3 -c 'import json,sys; print(json.dumps({"argv": sys.argv[1:]}))' "$@" \
|
||||||
| curl -sS -o "$out" -w '%{http_code}' --max-time "$API_TIMEOUT" -X POST \
|
| curl -sS -o "$out" -w '%{http_code}' --max-time "$API_TIMEOUT" -X POST \
|
||||||
-H "Authorization: Bearer $UBICLOUD_API_KEY" \
|
-H @<(printf 'Authorization: Bearer %s\n' "$UBICLOUD_API_KEY") \
|
||||||
-H 'Accept: text/plain' -H 'Content-Type: application/json' \
|
-H 'Accept: text/plain' -H 'Content-Type: application/json' \
|
||||||
-H 'X-Ubi-Version: 1.0.0' --data @- "$API/cli") || true
|
-H 'X-Ubi-Version: 1.0.0' --data @- "$API/cli") || true
|
||||||
echo "${code:-000}"
|
echo "${code:-000}"
|
||||||
@@ -552,6 +560,8 @@ lease_busy() {
|
|||||||
|
|
||||||
# vm_alive NAME: the recorded VM still answers show. One confirmed gone is
|
# vm_alive NAME: the recorded VM still answers show. One confirmed gone is
|
||||||
# cleaned up (state dir and scratch record), so its record is never tried again.
|
# cleaned up (state dir and scratch record), so its record is never tried again.
|
||||||
|
# Returns 2 when the API gives no answer either way: the caller must not start a
|
||||||
|
# second VM beside one that may still be running.
|
||||||
vm_alive() {
|
vm_alive() {
|
||||||
local n=$1 out code
|
local n=$1 out code
|
||||||
( load "$n"; [ "$CREATED" = yes ] && [ -n "$IP" ] ) 2>/dev/null || return 1
|
( load "$n"; [ "$CREATED" = yes ] && [ -n "$IP" ] ) 2>/dev/null || return 1
|
||||||
@@ -561,7 +571,7 @@ vm_alive() {
|
|||||||
case $code in
|
case $code in
|
||||||
200) return 0 ;;
|
200) return 0 ;;
|
||||||
404) log "run record for $n: the VM is gone; starting a new one"; cmd_down "$n" >/dev/null 2>&1 || true; return 1 ;;
|
404) log "run record for $n: the VM is gone; starting a new one"; cmd_down "$n" >/dev/null 2>&1 || true; return 1 ;;
|
||||||
*) return 1 ;;
|
*) log "run record for $n: could not check the VM (HTTP $code)"; return 2 ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -603,7 +613,8 @@ find_record() {
|
|||||||
[ -f "$f" ] || continue
|
[ -f "$f" ] || continue
|
||||||
grep -qx "RUN_KEY=$RUN_KEY" "$f" || continue
|
grep -qx "RUN_KEY=$RUN_KEY" "$f" || continue
|
||||||
n=$(basename "$(dirname "$f")")
|
n=$(basename "$(dirname "$f")")
|
||||||
if vm_alive "$n"; then echo "$n"; return 0; fi
|
vm_alive "$n" && { echo "$n"; return 0; }
|
||||||
|
[ $? -ne 2 ] || return 2
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -775,8 +786,13 @@ run_tiers() {
|
|||||||
# did not name; expires is the epoch the name's -t<hours> TTL ends at, or "-";
|
# did not name; expires is the epoch the name's -t<hours> TTL ends at, or "-";
|
||||||
# created is the name's epoch, or "-".
|
# created is the name's epoch, or "-".
|
||||||
vm_rows() {
|
vm_rows() {
|
||||||
local loc name owner created expires
|
local loc name owner created expires list
|
||||||
cli vm list -N -f location,name | while read -r loc name; do
|
list=$(cli vm list -N -f location,name) || return 1
|
||||||
|
if grep -Evq '^[[:blank:]]*([a-z0-9-]+[[:blank:]]+[a-z0-9-]+)?[[:blank:]]*$' <<<"$list"; then
|
||||||
|
log "the VM list answer is unreadable (a line is not 'location name')"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
while read -r loc name; do
|
||||||
[ -n "$name" ] || continue
|
[ -n "$name" ] || continue
|
||||||
owner=. created="" expires=-
|
owner=. created="" expires=-
|
||||||
if [[ "$name" =~ ^${PREFIX}-([a-z][a-z0-9]{0,11})-([0-9]{10})- ]]; then
|
if [[ "$name" =~ ^${PREFIX}-([a-z][a-z0-9]{0,11})-([0-9]{10})- ]]; then
|
||||||
@@ -788,7 +804,7 @@ vm_rows() {
|
|||||||
expires=$(( 10#$created + 10#${BASH_REMATCH[1]} * 3600 ))
|
expires=$(( 10#$created + 10#${BASH_REMATCH[1]} * 3600 ))
|
||||||
fi
|
fi
|
||||||
echo "$loc $name $owner $expires ${created:--}"
|
echo "$loc $name $owner $expires ${created:--}"
|
||||||
done
|
done <<<"$list"
|
||||||
}
|
}
|
||||||
|
|
||||||
# reap: destroy the caller's own VMs whose TTL ended more than REAP_GRACE
|
# reap: destroy the caller's own VMs whose TTL ended more than REAP_GRACE
|
||||||
@@ -823,18 +839,20 @@ cmd_reap() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd_gc() {
|
cmd_gc() {
|
||||||
local hours=${1:-$GC_HOURS} now loc name owner
|
local hours=${1:-$GC_HOURS} now rows loc name owner failed=0
|
||||||
[[ "$hours" =~ ^[0-9]+$ ]] || die "gc: HOURS must be a whole number (or set UBI_GC_HOURS)"
|
[[ "$hours" =~ ^[0-9]+$ ]] || die "gc: HOURS must be a whole number (or set UBI_GC_HOURS)"
|
||||||
if [ "$hours" -eq 0 ]; then log "gc: no sweep (give HOURS > 0 or set UBI_GC_HOURS); nothing listed, nothing destroyed"; return 0; fi
|
if [ "$hours" -eq 0 ]; then log "gc: no sweep (give HOURS > 0 or set UBI_GC_HOURS); nothing listed, nothing destroyed"; return 0; fi
|
||||||
now=$(date +%s)
|
now=$(date +%s)
|
||||||
vm_rows | while read -r loc name owner _; do
|
rows=$(vm_rows) || { log "gc: could not list the project's VMs; nothing destroyed"; return 1; }
|
||||||
|
while read -r loc name owner _; do
|
||||||
[ "$owner" = "$OWNER" ] || continue
|
[ "$owner" = "$OWNER" ] || continue
|
||||||
[[ "$name" =~ -([0-9]{10})- ]] || continue
|
[[ "$name" =~ -([0-9]{10})- ]] || continue
|
||||||
if [ $(( (now - BASH_REMATCH[1]) / 3600 )) -ge "$hours" ]; then
|
if [ $(( (now - BASH_REMATCH[1]) / 3600 )) -ge "$hours" ]; then
|
||||||
log "destroying stale $loc/$name (owner $OWNER, older than ${hours}h)"
|
log "destroying stale $loc/$name (owner $OWNER, older than ${hours}h)"
|
||||||
cmd_down "$name" -l "$loc" || log "failed to destroy $loc/$name"
|
cmd_down "$name" -l "$loc" || { failed=$(( failed + 1 )); log "failed to destroy $loc/$name"; }
|
||||||
fi
|
fi
|
||||||
done
|
done <<<"$rows"
|
||||||
|
[ "$failed" -eq 0 ] || { log "gc: $failed stale VMs could not be destroyed"; return 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_list() {
|
cmd_list() {
|
||||||
@@ -1296,25 +1314,63 @@ cmd_up() {
|
|||||||
[ -z "$storage" ] || args+=(-S "$storage")
|
[ -z "$storage" ] || args+=(-S "$storage")
|
||||||
args+=("$(cat "$dir/key.pub")")
|
args+=("$(cat "$dir/key.pub")")
|
||||||
create_at=$(date +%s)
|
create_at=$(date +%s)
|
||||||
|
log "creating $location/$name ($size)"
|
||||||
|
# Signals are held from before the create request until its answer is
|
||||||
|
# recorded. Teardown is armed (UP_NAME) only once the request is on its way
|
||||||
|
# and disarmed when Ubicloud refuses it (HTTP 4xx): a refused create made
|
||||||
|
# nothing, and a VM that answers to the name (a 409) is not this call's.
|
||||||
|
local interrupted="" created code err refusal need used max
|
||||||
|
trap 'interrupted=1' INT TERM HUP QUIT
|
||||||
write_state "$name" "$location" "$size" "" unknown "$create_at"
|
write_state "$name" "$location" "$size" "" unknown "$create_at"
|
||||||
bundle_save "$name"
|
bundle_save "$name"
|
||||||
record_vm "$name" "$location"
|
record_vm "$name" "$location"
|
||||||
UP_NAME=$name
|
UP_NAME=$name
|
||||||
log "creating $location/$name ($size)"
|
code=$(api "$dir/create.err" "${args[@]}")
|
||||||
# Hold signals until the create answer is recorded, so teardown knows
|
case $code in
|
||||||
# whether the VM exists instead of waiting out UBI_CREATE_GRACE.
|
200) created=yes ;;
|
||||||
local interrupted="" created=yes refusal need used max
|
4[0-9][0-9]) created=no ;;
|
||||||
trap 'interrupted=1' INT TERM HUP QUIT
|
*) created=unknown ;;
|
||||||
cli "${args[@]}" >/dev/null 2>"$dir/create.err" || created=no
|
esac
|
||||||
write_state "$name" "$location" "$size" "" "$created" "$create_at"
|
if [ "$created" = no ]; then
|
||||||
[ "$created" = no ] || bundle_save "$name"
|
UP_NAME=""
|
||||||
|
err=$(cat "$dir/create.err")
|
||||||
|
rm -rf "$dir"
|
||||||
|
forget_vm "$name"
|
||||||
|
[ -z "$RECOVERY_DIR" ] || rm -f "$RECOVERY_DIR/$OWNER/$name.bundle"
|
||||||
|
else
|
||||||
|
write_state "$name" "$location" "$size" "" "$created" "$create_at"
|
||||||
|
[ "$created" = unknown ] || bundle_save "$name"
|
||||||
|
fi
|
||||||
trap 'exit 130' INT TERM HUP QUIT
|
trap 'exit 130' INT TERM HUP QUIT
|
||||||
[ -z "$interrupted" ] || exit 130
|
[ -z "$interrupted" ] || exit 130
|
||||||
|
if [ "$created" = unknown ]; then
|
||||||
|
# A 5xx or no answer (000) is not a refusal: the VM may exist. Look it up
|
||||||
|
# by name until it shows or UBI_CREATE_GRACE has passed.
|
||||||
|
log "create of $location/$name answered HTTP $code; looking it up by name before deciding"
|
||||||
|
local seen_code
|
||||||
|
while :; do
|
||||||
|
seen_code=$(api "$dir/lookup.out" vm "$location/$name" show)
|
||||||
|
[ "$seen_code" != 200 ] || break
|
||||||
|
[ $(( $(date +%s) - create_at )) -lt "$CREATE_GRACE" ] || break
|
||||||
|
sleep "$POLL"
|
||||||
|
done
|
||||||
|
rm -f "$dir/lookup.out"
|
||||||
|
case $seen_code in
|
||||||
|
200) created=yes ;;
|
||||||
|
404) created=no ;;
|
||||||
|
esac
|
||||||
|
write_state "$name" "$location" "$size" "" "$created" "$create_at"
|
||||||
|
case $created in
|
||||||
|
yes) bundle_save "$name"; log "create of $location/$name went through; continuing" ;;
|
||||||
|
no) cat "$dir/create.err" >&2; die "create failed (HTTP $code): $location/$name was still absent ${CREATE_GRACE}s after the request" ;;
|
||||||
|
unknown) die "UBI_CREATE_UNKNOWN: could not tell whether $location/$name exists (create answered HTTP $code, its lookup HTTP $seen_code); its record and recovery bundle stay, teardown destroys it if it shows up, and this owner's reap destroys it once its -t<hours> TTL ends; check with: $0 down $name" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
if [ "$created" = no ]; then
|
if [ "$created" = no ]; then
|
||||||
refusal=$(sed -n 's/.*Requested vCPU count: \([0-9]*\), currently used vCPU count: \([0-9]*\), maximum allowed vCPU count: \([0-9]*\).*/\1 \2 \3/p' "$dir/create.err")
|
refusal=$(sed -n 's/.*Requested vCPU count: \([0-9]*\), currently used vCPU count: \([0-9]*\), maximum allowed vCPU count: \([0-9]*\).*/\1 \2 \3/p' <<<"$err")
|
||||||
if [ -z "$refusal" ]; then
|
if [ -z "$refusal" ]; then
|
||||||
cat "$dir/create.err" >&2
|
printf '%s\n' "$err" >&2
|
||||||
die "create failed"
|
die "create failed (HTTP $code); nothing was created"
|
||||||
fi
|
fi
|
||||||
read -r need used max <<<"$refusal"
|
read -r need used max <<<"$refusal"
|
||||||
echo "$max" >"$STATE_ROOT/quota-max"
|
echo "$max" >"$STATE_ROOT/quota-max"
|
||||||
@@ -1380,14 +1436,20 @@ cmd_down() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local deadline=$(( $(date +%s) + DOWN_TIMEOUT )) seen=0 asked=0 out code now
|
local deadline=$(( $(date +%s) + DOWN_TIMEOUT )) seen=0 asked=0 out code now rows unlisted=0
|
||||||
out=$(mktemp)
|
out=$(mktemp)
|
||||||
while :; do
|
while :; do
|
||||||
now=$(date +%s)
|
now=$(date +%s)
|
||||||
if [ -z "$loc" ]; then
|
if [ -z "$loc" ]; then
|
||||||
loc=$(vm_rows 2>/dev/null | awk -v n="$name" '$2==n {print $1}') || loc=""
|
if rows=$(vm_rows 2>/dev/null); then
|
||||||
code=404
|
loc=$(awk -v n="$name" '$2==n {print $1}' <<<"$rows")
|
||||||
[ -z "$loc" ] || code=$(api "$out" vm "$loc/$name" show)
|
code=404
|
||||||
|
[ -z "$loc" ] || code=$(api "$out" vm "$loc/$name" show)
|
||||||
|
else
|
||||||
|
code=unlisted
|
||||||
|
[ "$unlisted" = 1 ] || log "could not read the project's VM list; $name is not counted as gone"
|
||||||
|
unlisted=1
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
code=$(api "$out" vm "$loc/$name" show)
|
code=$(api "$out" vm "$loc/$name" show)
|
||||||
fi
|
fi
|
||||||
@@ -1541,12 +1603,14 @@ cmd_sync() {
|
|||||||
# Relative remote paths start at /home/ubi. A glob that matches nothing (for
|
# Relative remote paths start at /home/ubi. A glob that matches nothing (for
|
||||||
# example an optional flake ledger) is reported and skipped, not a failure.
|
# example an optional flake ledger) is reported and skipped, not a failure.
|
||||||
cmd_pull() {
|
cmd_pull() {
|
||||||
local name=$1 from=$2 to=$3 dir
|
local name=$1 from=$2 to=$3 dir rc=0
|
||||||
dir=$(printf %q "$(dirname "$from")")
|
dir=$(printf %q "$(dirname "$from")")
|
||||||
if ! remote "$name" "cd $dir 2>/dev/null && ls -d -- $(basename "$from") >/dev/null 2>&1"; then
|
remote "$name" "cd $dir 2>/dev/null || exit 3; ls -d -- $(basename "$from") >/dev/null 2>&1 || exit 3" || rc=$?
|
||||||
log "pull: nothing matches $from"
|
case $rc in
|
||||||
return 0
|
0) ;;
|
||||||
fi
|
3) log "pull: nothing matches $from"; return 0 ;;
|
||||||
|
*) log "pull failed: could not check $from on $name (exit $rc)"; return 1 ;;
|
||||||
|
esac
|
||||||
mkdir -p "$to"
|
mkdir -p "$to"
|
||||||
remote "$name" "cd $dir && tar -czf - $(basename "$from")" | tar -xzf - -C "$to"
|
remote "$name" "cd $dir && tar -czf - $(basename "$from")" | tar -xzf - -C "$to"
|
||||||
}
|
}
|
||||||
@@ -1602,13 +1666,14 @@ PULL_ROOT=""
|
|||||||
run_teardown() {
|
run_teardown() {
|
||||||
local rc=$1 rows left others
|
local rc=$1 rows left others
|
||||||
trap "" INT TERM HUP QUIT
|
trap "" INT TERM HUP QUIT
|
||||||
cmd_down "$RUN_VM" || log "WARNING: failed to destroy $RUN_VM; run: $0 down $RUN_VM"
|
[ -z "$UP_NAME" ] || cmd_down "$RUN_VM" || log "WARNING: failed to destroy $RUN_VM; run: $0 down $RUN_VM"
|
||||||
if rows=$(vm_rows 2>/dev/null); then
|
if rows=$(vm_rows 2>/dev/null); then
|
||||||
left=$(awk -v n="$RUN_VM" '$2==n' <<<"$rows" | grep -c . || true)
|
left=$(awk -v n="$RUN_VM" '$2==n' <<<"$rows" | grep -c . || true)
|
||||||
others=$(awk -v n="$RUN_VM" -v o="$OWNER" '$3==o && $2!=n' <<<"$rows" | grep -c . || true)
|
others=$(awk -v n="$RUN_VM" -v o="$OWNER" '$3==o && $2!=n' <<<"$rows" | grep -c . || true)
|
||||||
else
|
else
|
||||||
left=unknown others=unknown
|
left=unknown others=unknown
|
||||||
fi
|
fi
|
||||||
|
[ -n "$UP_NAME" ] || left=0
|
||||||
leftover "left=$left owner=$OWNER vm=$RUN_VM others=$others"
|
leftover "left=$left owner=$OWNER vm=$RUN_VM others=$others"
|
||||||
if [ "$left" = 0 ]; then
|
if [ "$left" = 0 ]; then
|
||||||
[[ "$SINK_FLUSH_FAILED" != *" $RUN_VM "* ]] || [ "$rc" -ne 0 ] || exit 1
|
[[ "$SINK_FLUSH_FAILED" != *" $RUN_VM "* ]] || [ "$rc" -ne 0 ] || exit 1
|
||||||
@@ -1691,10 +1756,10 @@ cmd_run() {
|
|||||||
|
|
||||||
local attach=""
|
local attach=""
|
||||||
if [ -n "$FORCE_ATTACH" ]; then
|
if [ -n "$FORCE_ATTACH" ]; then
|
||||||
vm_alive "$FORCE_ATTACH" || die "attach: $FORCE_ATTACH no longer exists; rerun the original command to start fresh"
|
vm_alive "$FORCE_ATTACH" || { [ $? -ne 2 ] || die "UBI_ATTACH_UNKNOWN: could not check whether $FORCE_ATTACH still exists; nothing created, rerun"; die "attach: $FORCE_ATTACH no longer exists; rerun the original command to start fresh"; }
|
||||||
attach=$FORCE_ATTACH
|
attach=$FORCE_ATTACH
|
||||||
else
|
else
|
||||||
attach=$(find_record)
|
attach=$(find_record) || die "UBI_ATTACH_UNKNOWN: a run record for this key names a VM the Ubicloud API could not check; nothing created (a new VM would leave that one billing), rerun"
|
||||||
fi
|
fi
|
||||||
if [ "$probe" = 1 ]; then
|
if [ "$probe" = 1 ]; then
|
||||||
if [ -z "$attach" ]; then echo "UBI_ATTACH_INFO: none key=${RUN_KEY:0:12}"
|
if [ -z "$attach" ]; then echo "UBI_ATTACH_INFO: none key=${RUN_KEY:0:12}"
|
||||||
@@ -1750,6 +1815,7 @@ cmd_run() {
|
|||||||
trap 'exit 130' INT TERM QUIT
|
trap 'exit 130' INT TERM QUIT
|
||||||
trap on_hup HUP
|
trap on_hup HUP
|
||||||
if [ -n "$attach" ]; then
|
if [ -n "$attach" ]; then
|
||||||
|
UP_NAME=$name
|
||||||
load "$name"
|
load "$name"
|
||||||
log "reattached to $name created_at=$CREATE_AT key=${RUN_KEY:0:12}"
|
log "reattached to $name created_at=$CREATE_AT key=${RUN_KEY:0:12}"
|
||||||
if [ -n "$sink" ]; then
|
if [ -n "$sink" ]; then
|
||||||
@@ -1821,7 +1887,7 @@ cmd_run() {
|
|||||||
[ "$rc" -ne 0 ] || rc=1
|
[ "$rc" -ne 0 ] || rc=1
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
cmd_pull "$name" "${p%%:*}" "${p#*:}" || log "pull failed: $p"
|
cmd_pull "$name" "${p%%:*}" "${p#*:}" || { log "pull failed: $p"; [ "$rc" -ne 0 ] || rc=1; }
|
||||||
done
|
done
|
||||||
[ ${#TIER_NAMES[@]} -eq 0 ] || verify_line
|
[ ${#TIER_NAMES[@]} -eq 0 ] || verify_line
|
||||||
return "$rc"
|
return "$rc"
|
||||||
|
|||||||
@@ -13,10 +13,14 @@ export const read = (rel: string) => readFileSync(join(ROOT, rel), 'utf8');
|
|||||||
/**
|
/**
|
||||||
* Fake Ubicloud API that can create: create adds the name to the listed file
|
* Fake Ubicloud API that can create: create adds the name to the listed file
|
||||||
* (or answers the quota refusal in refuse with HTTP 400), show is running at
|
* (or answers the quota refusal in refuse with HTTP 400), show is running at
|
||||||
* 192.0.2.1 while listed, destroy unlists. The fake VM is the directory vm/:
|
* 192.0.2.1 while listed, destroy unlists. Flag files beside listed change one
|
||||||
|
* answer: listed.create-status answers every create with that HTTP status, and
|
||||||
|
* with listed.create-lands (a count N) the VM still lands, listed from the Nth
|
||||||
|
* show on; listed.list-fails, listed.show-fails and listed.destroy-fails answer
|
||||||
|
* 503 or 500; listed.list-body is the list's 200 body. The fake VM is the directory vm/:
|
||||||
* the fake ssh records every remote command in ssh.log, keeps copies of
|
* the fake ssh records every remote command in ssh.log, keeps copies of
|
||||||
* ~/.ubirun-env and ~/.ubirun-setup.sh under the fixture root, serves pulls
|
* ~/.ubirun-env and ~/.ubirun-setup.sh under the fixture root, serves pulls
|
||||||
* from remote-fs/, and runs every other command there for real (HOME=vm/,
|
* from remote-fs/ (a pull's existence check matches only what remote-fs/ holds), and runs every other command there for real (HOME=vm/,
|
||||||
* vmbin/ first on PATH), so uploads unpack, detached phases write their pid,
|
* vmbin/ first on PATH), so uploads unpack, detached phases write their pid,
|
||||||
* output and exit files, tail --pid follows them and pkill ends them. The stub
|
* output and exit files, tail --pid follows them and pkill ends them. The stub
|
||||||
* make in vmbin/ echoes `ran: make ARGS`, or runs command-mode or
|
* make in vmbin/ echoes `ran: make ARGS`, or runs command-mode or
|
||||||
@@ -37,10 +41,10 @@ export function fakeVm(opts: { listed?: string; refuse?: string } = {}) {
|
|||||||
'loc=$(printf \'%s\' "$body" | sed -n \'s/.*"vm", *"\\([^/"]*\\)\\/[^"]*".*/\\1/p\')\n' +
|
'loc=$(printf \'%s\' "$body" | sed -n \'s/.*"vm", *"\\([^/"]*\\)\\/[^"]*".*/\\1/p\')\n' +
|
||||||
'name=$(printf \'%s\' "$body" | sed -n \'s/.*"vm", *"[^/"]*\\/\\([^"]*\\)".*/\\1/p\')\n' +
|
'name=$(printf \'%s\' "$body" | sed -n \'s/.*"vm", *"[^/"]*\\/\\([^"]*\\)".*/\\1/p\')\n' +
|
||||||
'case "$body" in\n' +
|
'case "$body" in\n' +
|
||||||
' *\'"create"\'*) if [ -n ' + JSON.stringify(opts.refuse ?? '') + ' ]; then printf \'%s\' ' + JSON.stringify(opts.refuse ?? '') + ' > "$out"; printf 400; else echo "$loc $name" >> "$listed"; : > "$out"; printf 200; fi ;;\n' +
|
' *\'"create"\'*) if [ -f "$listed.create-status" ]; then : > "$out"; [ ! -f "$listed.create-lands" ] || echo "$loc $name" > "$listed.pending"; cat "$listed.create-status"; elif [ -n ' + JSON.stringify(opts.refuse ?? '') + ' ]; then printf \'%s\' ' + JSON.stringify(opts.refuse ?? '') + ' > "$out"; printf 400; else echo "$loc $name" >> "$listed"; : > "$out"; printf 200; fi ;;\n' +
|
||||||
' *destroy*) : > "$out"; if [ -f "$listed.destroy-fails" ]; then printf 500; else grep -v " $name$" "$listed" > "$listed.tmp"; mv "$listed.tmp" "$listed"; printf 200; fi ;;\n' +
|
' *destroy*) : > "$out"; if [ -f "$listed.destroy-fails" ]; then printf 500; else grep -v " $name$" "$listed" > "$listed.tmp"; mv "$listed.tmp" "$listed"; printf 200; fi ;;\n' +
|
||||||
' *\'"list"\'*) if [ -f "$listed.list-fails" ]; then echo down > "$out"; printf 503; else cat "$listed" "$listed.ghost" > "$out" 2>/dev/null; printf 200; fi ;;\n' +
|
' *\'"list"\'*) if [ -f "$listed.list-fails" ]; then echo down > "$out"; printf 503; elif [ -f "$listed.list-body" ]; then cat "$listed.list-body" > "$out"; printf 200; else cat "$listed" "$listed.ghost" > "$out" 2>/dev/null; printf 200; fi ;;\n' +
|
||||||
' *\'"show"\'*) [ -z "${FAKE_SHOW_DELAY:-}" ] || sleep "$FAKE_SHOW_DELAY"; if [ -f "$listed.show-fails" ]; then echo down > "$out"; printf 503; elif grep -q " $name$" "$listed"; then printf \'state: running\\nsize: standard-16\\nip4: 192.0.2.1\\n\' > "$out"; printf 200; else : > "$out"; printf 404; fi ;;\n' +
|
' *\'"show"\'*) [ -z "${FAKE_SHOW_DELAY:-}" ] || sleep "$FAKE_SHOW_DELAY"; if [ -f "$listed.pending" ]; then n=$(( $(cat "$listed.shows" 2>/dev/null || echo 0) + 1 )); echo "$n" > "$listed.shows"; if [ "$n" -ge "$(cat "$listed.create-lands")" ]; then cat "$listed.pending" >> "$listed"; rm -f "$listed.pending"; fi; fi; if [ -f "$listed.show-fails" ]; then echo down > "$out"; printf 503; elif grep -q " $name$" "$listed"; then printf \'state: running\\nsize: standard-16\\nip4: 192.0.2.1\\n\' > "$out"; printf 200; else : > "$out"; printf 404; fi ;;\n' +
|
||||||
' *) : > "$out"; printf 200 ;;\n' +
|
' *) : > "$out"; printf 200 ;;\n' +
|
||||||
'esac\n');
|
'esac\n');
|
||||||
writeFileSync(join(bin, 'ssh'), '#!/usr/bin/env bash\n' +
|
writeFileSync(join(bin, 'ssh'), '#!/usr/bin/env bash\n' +
|
||||||
@@ -55,7 +59,7 @@ export function fakeVm(opts: { listed?: string; refuse?: string } = {}) {
|
|||||||
' *"cat > ~/.ubirun-env"*) tee "$root/ubirun-env" > "$vm/.ubirun-env" ;;\n' +
|
' *"cat > ~/.ubirun-env"*) tee "$root/ubirun-env" > "$vm/.ubirun-env" ;;\n' +
|
||||||
' *"cat > ~/.ubirun-setup.sh"*) tee "$root/ubirun-setup.sh" > "$vm/.ubirun-setup.sh" ;;\n' +
|
' *"cat > ~/.ubirun-setup.sh"*) tee "$root/ubirun-setup.sh" > "$vm/.ubirun-setup.sh" ;;\n' +
|
||||||
' *"tar -czf -"*) base=${cmd##* }; tar -czf - -C "$root/remote-fs" "$base" ;;\n' +
|
' *"tar -czf -"*) base=${cmd##* }; tar -czf - -C "$root/remote-fs" "$base" ;;\n' +
|
||||||
' *"ls -d -- "*) : ;;\n' +
|
' *"ls -d -- "*) g=${cmd#*ls -d -- }; g=${g%% *}; cd "$root/remote-fs" 2>/dev/null && ls -d -- $g >/dev/null 2>&1 || exit 3 ;;\n' +
|
||||||
' *) exec bash -c "$cmd" ;;\n' +
|
' *) exec bash -c "$cmd" ;;\n' +
|
||||||
'esac\n');
|
'esac\n');
|
||||||
writeFileSync(join(vmbin, 'make'), '#!/usr/bin/env bash\n' +
|
writeFileSync(join(vmbin, 'make'), '#!/usr/bin/env bash\n' +
|
||||||
|
|||||||
@@ -314,3 +314,110 @@ describe('ubi-runner recovery bundle (GST-1220 b)', () => {
|
|||||||
expect(await a.exited).toBe(0);
|
expect(await a.exited).toBe(0);
|
||||||
}, 90_000);
|
}, 90_000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('ubi-runner money path: an API error is never read as success (GST-1441)', () => {
|
||||||
|
const rd = (f: FakeVm) => join(f.root, 'rd');
|
||||||
|
const bundle = (f: FakeVm, name: string) => join(rd(f), 'thrd1', name + '.bundle');
|
||||||
|
const recorded = (f: FakeVm, name: string) => existsSync(f.record) && readFileSync(f.record, 'utf8').includes(name);
|
||||||
|
const flag = (f: FakeVm, file: string, body = '') => writeFileSync(join(f.root, 'listed.' + file), body);
|
||||||
|
const NAME = 'ubirun-thrd1-1000000000-aaaaaaaa-t3';
|
||||||
|
|
||||||
|
// Value: protects=a refused create never arms teardown, so up and run never destroy a VM another caller owns under the same name; fails_when=teardown runs down on a name whose create Ubicloud refused (409); why_new=every fake create succeeded or answered the quota 400 on a name nobody held; seam=none; no_claim=the real API's 409 body
|
||||||
|
test('a refused create (409, the name is taken) disarms teardown: the VM holding the name is never destroyed', () => {
|
||||||
|
const f = make();
|
||||||
|
writeFileSync(join(f.root, 'listed'), 'eu-central-h1 ' + NAME + '\n');
|
||||||
|
flag(f, 'create-status', '409');
|
||||||
|
const repo = gitSrc(f);
|
||||||
|
for (const args of [['up', '-n', NAME, '-l', 'eu-central-h1'], ['run', '-n', NAME, '-l', 'eu-central-h1', '--src', repo, '--', 'true']]) {
|
||||||
|
const r = run(f, args, { UBI_RECOVERY_DIR: rd(f), UBI_REAP: '0' });
|
||||||
|
expect(r.exitCode, out(r)).not.toBe(0);
|
||||||
|
expect(out(r)).toContain('create failed (HTTP 409); nothing was created');
|
||||||
|
expect(f.calls()).not.toContain('"destroy"');
|
||||||
|
expect(f.listed()).toContain(NAME);
|
||||||
|
expect(existsSync(join(f.state, NAME))).toBe(false);
|
||||||
|
expect(recorded(f, NAME)).toBe(false);
|
||||||
|
expect(existsSync(bundle(f, NAME))).toBe(false);
|
||||||
|
if (args[0] === 'run') expect(out(r)).toContain('UBICLOUD: left=0 owner=thrd1 vm=' + NAME);
|
||||||
|
}
|
||||||
|
}, 60_000);
|
||||||
|
|
||||||
|
// Value: protects=down settles a VM as gone only on a readable list, so a caller settling a reserve on down's exit never leaves a live VM billing; fails_when=a failed or unreadable list counts as gone again and the bundle is deleted; why_new=down was only tested with a working list; seam=none; no_claim=the real API's error bodies
|
||||||
|
test('down with no local state: a failed or unreadable VM list is unknown, never gone; exit non-zero and the bundle stays', () => {
|
||||||
|
const f = make();
|
||||||
|
writeFileSync(join(f.root, 'listed'), 'eu-central-h1 ' + NAME + '\n');
|
||||||
|
mkdirSync(join(rd(f), 'thrd1'), { recursive: true });
|
||||||
|
writeFileSync(bundle(f, NAME), 'sealed');
|
||||||
|
for (const [file, body] of [['list-fails', ''], ['list-body', '<html>upstream error</html>\n']]) {
|
||||||
|
flag(f, file, body);
|
||||||
|
const r = run(f, ['down', NAME], { UBI_RECOVERY_DIR: rd(f), UBI_DOWN_TIMEOUT: '2' });
|
||||||
|
expect(r.exitCode, out(r)).not.toBe(0);
|
||||||
|
expect(out(r)).not.toContain('gone: ');
|
||||||
|
expect(existsSync(bundle(f, NAME))).toBe(true);
|
||||||
|
expect(f.listed()).toContain(NAME);
|
||||||
|
rmSync(join(f.root, 'listed.' + file));
|
||||||
|
}
|
||||||
|
const ok = run(f, ['down', NAME], { UBI_RECOVERY_DIR: rd(f) });
|
||||||
|
expect(ok.exitCode, out(ok)).toBe(0);
|
||||||
|
expect(existsSync(bundle(f, NAME))).toBe(false);
|
||||||
|
}, 60_000);
|
||||||
|
|
||||||
|
// Value: protects=a 5xx or timed-out create is looked up by name, so a VM Ubicloud created anyway is adopted instead of orphaned until its TTL; fails_when=a 502/503/000 create is treated as a refusal and the first 404 settles it as gone; why_new=no fake create ever failed and landed; seam=none; no_claim=how late a real 5xx create appears
|
||||||
|
test('a 5xx or unanswered create that lands anyway is found by name and adopted, not orphaned', () => {
|
||||||
|
for (const status of ['503', '000']) {
|
||||||
|
const f = make();
|
||||||
|
flag(f, 'create-status', status);
|
||||||
|
flag(f, 'create-lands', '2');
|
||||||
|
const r = run(f, ['up'], { UBI_RECOVERY_DIR: rd(f), UBI_REAP: '0', UBI_CREATE_GRACE: '20' });
|
||||||
|
expect(r.exitCode, out(r)).toBe(0);
|
||||||
|
const name = r.stdout.toString().trim();
|
||||||
|
expect(out(r)).toContain('answered HTTP ' + status + '; looking it up by name');
|
||||||
|
expect(f.listed()).toContain(name);
|
||||||
|
expect(recorded(f, name)).toBe(true);
|
||||||
|
expect(existsSync(bundle(f, name))).toBe(true);
|
||||||
|
expect(readFileSync(join(f.state, name, 'env'), 'utf8')).toContain('CREATED=yes');
|
||||||
|
}
|
||||||
|
}, 60_000);
|
||||||
|
|
||||||
|
// Value: protects=a create nobody can confirm either way keeps its record and bundle for the reaper and recovery; fails_when=an unconfirmable 5xx create is settled as refused and its bundle dropped; why_new=no fake create ever failed; seam=none; no_claim=real outage shapes
|
||||||
|
test('a 5xx create the lookup cannot settle is UBI_CREATE_UNKNOWN: record and bundle kept, exit non-zero, nothing destroyed', () => {
|
||||||
|
const f = make();
|
||||||
|
flag(f, 'create-status', '502');
|
||||||
|
flag(f, 'show-fails');
|
||||||
|
const r = run(f, ['up'], { UBI_RECOVERY_DIR: rd(f), UBI_REAP: '0', UBI_CREATE_GRACE: '2', UBI_DOWN_TIMEOUT: '2' });
|
||||||
|
expect(r.exitCode, out(r)).not.toBe(0);
|
||||||
|
expect(out(r)).toContain('UBI_CREATE_UNKNOWN');
|
||||||
|
const name = vmDirs(f)[0]!;
|
||||||
|
expect(readFileSync(join(f.state, name, 'env'), 'utf8')).toContain('CREATED=unknown');
|
||||||
|
expect(recorded(f, name)).toBe(true);
|
||||||
|
expect(existsSync(bundle(f, name))).toBe(true);
|
||||||
|
expect(f.calls()).not.toContain('"destroy"');
|
||||||
|
}, 60_000);
|
||||||
|
|
||||||
|
// Value: protects=gc reports a stale VM it could not destroy; fails_when=gc exits 0 after a failed destroy; why_new=gc's destroy failure was only logged; seam=none; no_claim=real destroy failures
|
||||||
|
test('gc exits non-zero when a stale VM of this owner could not be destroyed', () => {
|
||||||
|
const f = make();
|
||||||
|
writeFileSync(join(f.root, 'listed'), 'eu-central-h1 ' + NAME + '\n');
|
||||||
|
flag(f, 'destroy-fails');
|
||||||
|
const r = run(f, ['gc', '1'], { UBI_DOWN_TIMEOUT: '2' });
|
||||||
|
expect(r.exitCode, out(r)).not.toBe(0);
|
||||||
|
expect(out(r)).toContain('gc: 1 stale VMs could not be destroyed');
|
||||||
|
}, 60_000);
|
||||||
|
|
||||||
|
// Value: protects=a run record whose VM the API cannot check is never abandoned for a second VM; fails_when=an unanswered show reads as "VM gone" and run creates another VM beside the live one; why_new=reattach was only tested with a working show; seam=none; no_claim=real API outages
|
||||||
|
test('run: a recorded VM the API cannot check refuses with UBI_ATTACH_UNKNOWN instead of creating a second VM', async () => {
|
||||||
|
const f = make();
|
||||||
|
const repo = gitSrc(f);
|
||||||
|
writeFileSync(join(f.root, 'make-test.sh'), 'echo begin; sleep 3\n');
|
||||||
|
const args = ['run', '--src', repo, '--', 'make test'];
|
||||||
|
const a = f.spawn(args, { UBI_REAP: '0' });
|
||||||
|
await until(() => vmDirs(f).some(d => existsSync(join(f.state, d, 'command.out')) && readFileSync(join(f.state, d, 'command.out'), 'utf8').includes('begin')), 30_000);
|
||||||
|
process.kill(a.pid, 'SIGKILL');
|
||||||
|
await a.exited;
|
||||||
|
flag(f, 'show-fails');
|
||||||
|
const b = run(f, args, { UBI_REAP: '0' });
|
||||||
|
expect(b.exitCode, out(b)).not.toBe(0);
|
||||||
|
expect(out(b)).toContain('UBI_ATTACH_UNKNOWN');
|
||||||
|
expect(creates(f)).toBe(1);
|
||||||
|
}, 90_000);
|
||||||
|
});
|
||||||
|
|
||||||
@@ -86,6 +86,62 @@ describe('ubicloud free-suite runner', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Value: protects=a pull that cannot reach the VM fails, so a caller never keeps a stale copy as fresh; fails_when=an ssh failure in the existence check reads as "nothing matches" and exits 0 again; why_new=only a reachable VM was tested; seam=none; no_claim=real ssh error codes beyond 255
|
||||||
|
test('pull exits non-zero when ssh fails, and only a reachable VM with no match counts as nothing to pull (GST-1441)', () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), 'ubi-pull-'));
|
||||||
|
try {
|
||||||
|
const bin = join(root, 'bin'), state = join(root, 'state');
|
||||||
|
mkdirSync(join(state, 'fake-vm'), { recursive: true });
|
||||||
|
writeFileSync(join(state, 'fake-vm/env'), 'IP=192.0.2.1\n');
|
||||||
|
mkdirSync(bin);
|
||||||
|
writeFileSync(join(bin, 'ssh'), '#!/usr/bin/env bash\necho "ssh: connect to host 192.0.2.1 port 22: Connection timed out" >&2\nexit 255\n');
|
||||||
|
chmodSync(join(bin, 'ssh'), 0o755);
|
||||||
|
const env = { ...process.env, PATH: `${bin}:${process.env.PATH}`, UBICLOUD_API_KEY: 'offline', UBI_RUNNER_STATE: state };
|
||||||
|
const r = Bun.spawnSync(['bash', join(DIR, 'ubi-runner.sh'), 'pull', 'fake-vm', 'work/results', join(root, 'local')], { env, timeout: 10_000 });
|
||||||
|
expect(r.exitCode).not.toBe(0);
|
||||||
|
expect(r.stderr.toString()).toContain('pull failed: could not check work/results on fake-vm (exit 255)');
|
||||||
|
expect(r.stderr.toString()).not.toContain('nothing matches');
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Value: protects=the Ubicloud key never appears in curl's argv (visible to every local user in ps) and the pinned curl still sends it; fails_when=the key goes back on the command line, or -H @file stops delivering it; why_new=the key's argv exposure was never checked; seam=none; no_claim=curl builds older than 7.55, which lack -H @file
|
||||||
|
test('the API key reaches curl as a header file, never in its argv, and real curl still sends it (GST-1441)', async () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), 'ubi-key-'));
|
||||||
|
const key = 'sekrit-ubi-key-4711';
|
||||||
|
try {
|
||||||
|
const bin = join(root, 'bin'), argv = join(root, 'argv.log'), hdr = join(root, 'hdr.log');
|
||||||
|
mkdirSync(bin);
|
||||||
|
writeFileSync(join(bin, 'curl'), `#!/usr/bin/env bash
|
||||||
|
printf '%s\\n' "$*" >> ${JSON.stringify(argv)}
|
||||||
|
out=""; prev=""
|
||||||
|
for a in "$@"; do
|
||||||
|
[ "$prev" = -o ] && out=$a
|
||||||
|
[ "$prev" = -H ] && case $a in @*) cat "\${a#@}" >> ${JSON.stringify(hdr)} ;; esac
|
||||||
|
prev=$a
|
||||||
|
done
|
||||||
|
cat >/dev/null; printf '' > "$out"; printf 200
|
||||||
|
`);
|
||||||
|
chmodSync(join(bin, 'curl'), 0o755);
|
||||||
|
const base = { ...process.env, UBICLOUD_API_KEY: key, UBI_RUNNER_STATE: join(root, 'state') };
|
||||||
|
const faked = Bun.spawnSync(['bash', join(DIR, 'ubi-runner.sh'), 'cli', 'vm', 'list'], { env: { ...base, PATH: `${bin}:${process.env.PATH}` }, timeout: 10_000 });
|
||||||
|
expect(faked.exitCode, faked.stderr.toString()).toBe(0);
|
||||||
|
expect(readFileSync(argv, 'utf8')).not.toContain(key);
|
||||||
|
expect(readFileSync(hdr, 'utf8')).toBe(`Authorization: Bearer ${key}\n`);
|
||||||
|
let seen = '';
|
||||||
|
const server = Bun.serve({ port: 0, hostname: '127.0.0.1', fetch(req) { seen = req.headers.get('authorization') ?? ''; return new Response('eu-central-h1 vm-a\n'); } });
|
||||||
|
try {
|
||||||
|
const real = Bun.spawn(['bash', join(DIR, 'ubi-runner.sh'), 'cli', 'vm', 'list'], { env: { ...base, UBICLOUD_API_URL: `http://127.0.0.1:${server.port}` }, stdout: 'pipe', stderr: 'pipe', timeout: 20_000 });
|
||||||
|
expect(await real.exited).toBe(0);
|
||||||
|
expect(await new Response(real.stdout).text()).toBe('eu-central-h1 vm-a\n');
|
||||||
|
expect(seen).toBe(`Bearer ${key}`);
|
||||||
|
} finally { server.stop(true); }
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('refuses to start without UBICLOUD_API_KEY, before any network call', () => {
|
test('refuses to start without UBICLOUD_API_KEY, before any network call', () => {
|
||||||
const env = { ...process.env, UBICLOUD_API_URL: 'http://127.0.0.1:9' } as Record<string, string | undefined>;
|
const env = { ...process.env, UBICLOUD_API_URL: 'http://127.0.0.1:9' } as Record<string, string | undefined>;
|
||||||
delete env.UBICLOUD_API_KEY;
|
delete env.UBICLOUD_API_KEY;
|
||||||
@@ -461,7 +517,7 @@ test('pack ships a linked worktree with a self-contained .git on the checked-out
|
|||||||
|
|
||||||
test('up records the VM in the scratch file before the create request is sent', () => {
|
test('up records the VM in the scratch file before the create request is sent', () => {
|
||||||
const runner = read('scripts/ubicloud/ubi-runner.sh');
|
const runner = read('scripts/ubicloud/ubi-runner.sh');
|
||||||
const create = runner.indexOf('cli "${args[@]}" >/dev/null 2>"$dir/create.err"');
|
const create = runner.indexOf('code=$(api "$dir/create.err" "${args[@]}")');
|
||||||
expect(create).toBeGreaterThan(0);
|
expect(create).toBeGreaterThan(0);
|
||||||
expect(runner.indexOf('record_vm "$name" "$location"')).toBeLessThan(create);
|
expect(runner.indexOf('record_vm "$name" "$location"')).toBeLessThan(create);
|
||||||
expect(runner).toContain('$HOME/.capy/work/ubi-runner/vms.tsv');
|
expect(runner).toContain('$HOME/.capy/work/ubi-runner/vms.tsv');
|
||||||
|
|||||||
Reference in new issue
Block a user