mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 15:27:56 +01:00
pack builds its archive in two tar passes plus gzip, which bsdtar and GNU tar both accept, with COPYFILE_DISABLE=1 so Apple's tar adds no ._* files. vm_rows skips the blank row Ubicloud returns for a project with no VMs. usage parenthesizes its ternary for the one-true-awk and exits 1 on an unreadable list or size; the quota preflight logs that and defers to the API. Co-authored-by: Jared <182125291+yolo-jared@users.noreply.github.com> Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
5ba425af4b
commit
eb07dbb3af
4 files changed
+236
-10
No files matched your search
@@ -0,0 +1,4 @@
|
||||
### Fixed
|
||||
- **`gstack-ubi pack` works on macOS again.** macOS's built-in tar read pack's options as file names, so `sync`, `run` and `verify` failed after the VM was already created, shipping an archive holding only `.git`. Pack now builds the archive in two steps that work with both macOS and GNU tar, and keeps macOS tar from adding hidden `._*` copies of files that carry extended attributes. (#3680, reported by Jared)
|
||||
- **`gstack-ubi usage` works on macOS, and the vCPU quota check runs there again.** One line of the usage report was a syntax error for macOS's awk, which also switched off the quota check before every create. `usage` now exits 1 instead of printing a partial count when Ubicloud can't list the VMs or read a size, and the quota check says so in the log and leaves the decision to Ubicloud. (#3679, reported by Jared)
|
||||
- **`reap` and `up` work on a Ubicloud project with no VMs.** An empty VM list was read as one VM named `-`, so `reap` and every `up` died with `invalid VM name '-'`. (#3678, reported by Jared)
|
||||
@@ -740,6 +740,7 @@ run_tiers() {
|
||||
vm_rows() {
|
||||
local loc name owner created expires
|
||||
cli vm list -N -f location,name | while read -r loc name; do
|
||||
[ -n "$name" ] || continue
|
||||
owner=. created="" expires=-
|
||||
if [[ "$name" =~ ^${PREFIX}-([a-z][a-z0-9]{0,11})-([0-9]{10})- ]]; then
|
||||
owner=${BASH_REMATCH[1]} created=${BASH_REMATCH[2]}
|
||||
@@ -804,21 +805,31 @@ cmd_list() {
|
||||
}
|
||||
|
||||
# usage: VMs and vCPUs per owner, from each VM's size (standard-16 = 16 vCPUs).
|
||||
# Exits 1 with no table when the VM list or any VM's size cannot be read, so a
|
||||
# partial count is never taken for the project's usage.
|
||||
cmd_usage() {
|
||||
local tmp loc name owner i=0
|
||||
local tmp rows loc name owner f i=0 unread=0
|
||||
rows=$(vm_rows) || { log "usage: could not list the project's VMs"; return 1; }
|
||||
tmp=$(mktemp -d)
|
||||
while read -r loc name owner _; do
|
||||
[ -n "$name" ] || continue
|
||||
i=$(( i + 1 ))
|
||||
cli vm "$loc/$name" show 2>/dev/null | sed -n "s/^size: /$owner /p" >"$tmp/$i" &
|
||||
[ $(( i % 8 )) -ne 0 ] || wait
|
||||
done < <(vm_rows)
|
||||
done <<<"$rows"
|
||||
wait
|
||||
cat "$tmp"/* 2>/dev/null | awk -v me="$OWNER" '
|
||||
for f in "$tmp"/*; do [ ! -e "$f" ] || [ -s "$f" ] || unread=$(( unread + 1 )); done
|
||||
if [ "$unread" -gt 0 ]; then
|
||||
rm -rf "$tmp"
|
||||
log "usage: could not read the size of $unread of $i VMs"
|
||||
return 1
|
||||
fi
|
||||
{ [ "$i" -eq 0 ] || cat "$tmp"/*; } | awk -v me="$OWNER" '
|
||||
{ n = $2; sub(/^.*-/, "", n); o = $1 == "-" ? "(untagged)" : $1 == "." ? "(other)" : $1
|
||||
vms[o]++; cpu[o] += n; total += n; count++ }
|
||||
END {
|
||||
printf "%-14s %4s %6s\n", "OWNER", "VMS", "VCPUS"
|
||||
for (o in cpu) printf "%-14s %4d %6d%s\n", o, vms[o], cpu[o], o == me ? " (you)" : "" | "sort -k3,3nr"
|
||||
for (o in cpu) printf "%-14s %4d %6d%s\n", o, vms[o], cpu[o], (o == me ? " (you)" : "") | "sort -k3,3nr"
|
||||
close("sort -k3,3nr")
|
||||
printf "%-14s %4d %6d\n", "total", count, total
|
||||
}'
|
||||
@@ -881,8 +892,10 @@ quota_preflight() {
|
||||
# between them: a slow first usage read must not shave the printed budget.
|
||||
remaining=$QUOTA_WAIT
|
||||
while :; do
|
||||
used=$(cmd_usage 2>/dev/null | awk '$1=="total" {print $3}')
|
||||
[[ "$used" =~ ^[0-9]+$ ]] || return 0
|
||||
if ! used=$(cmd_usage 2>/dev/null | awk '$1=="total" {print $3}') || ! [[ "$used" =~ ^[0-9]+$ ]]; then
|
||||
log "quota preflight: could not read the project's vCPU usage; the API's own answer decides"
|
||||
return 0
|
||||
fi
|
||||
[ $(( used + need )) -gt "$max" ] || return 0
|
||||
if [ "$remaining" -le 0 ]; then
|
||||
log "quota preflight refused $location/$name ($size): it needs $need vCPUs and the project's VMs already use $used of $max (ceiling from $source; other usage such as managed runners counts on top)"
|
||||
@@ -1074,7 +1087,12 @@ cmd_down() {
|
||||
# pointer into the main repository, useless on the VM; the same snapshot
|
||||
# replaces it, on the worktree's own branch and index. Not carried: submodule
|
||||
# repositories (.git/modules) and a partial clone's promisor (the fetch pulls
|
||||
# every missing blob first); gstack has neither.
|
||||
# every missing blob first); gstack has neither. The archive is built in two
|
||||
# passes inside the snapshot directory because macOS's bsdtar stops reading
|
||||
# options at the first operand and resolves -T names after a -C, so no single
|
||||
# tar command line works on both it and GNU tar (#3680). COPYFILE_DISABLE=1
|
||||
# keeps Apple's tar from adding ._* AppleDouble entries for files with
|
||||
# extended attributes, which the VM would see as untracked files.
|
||||
cmd_pack() {
|
||||
local src tmp gitdir branch head entry
|
||||
src=$(cd "${1:-.}" && pwd)
|
||||
@@ -1118,15 +1136,18 @@ cmd_pack() {
|
||||
fi
|
||||
(
|
||||
cd "$src"
|
||||
export COPYFILE_DISABLE=1
|
||||
git ls-files -z -co --exclude-standard \
|
||||
| while IFS= read -r -d '' f; do
|
||||
if [ -e "$f" ] || [ -L "$f" ]; then printf '%s\0' "$f"; fi
|
||||
done \
|
||||
| tar --null -T - -c -C "$tmp" .git -zf -
|
||||
| tar --null -T - -cf "$tmp/files.tar" \
|
||||
&& tar -rf "$tmp/files.tar" -C "$tmp" .git \
|
||||
&& gzip -c "$tmp/files.tar"
|
||||
)
|
||||
rm -rf "$tmp"; trap - EXIT
|
||||
else
|
||||
tar -C "$src" -czf - .
|
||||
COPYFILE_DISABLE=1 tar -C "$src" -czf - .
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Regression tests for #3680 and #3679: scripts/ubicloud/ubi-runner.sh on the
|
||||
* BSD userland macOS ships.
|
||||
*
|
||||
* #3680 pack built its archive with `tar --null -T - -c -C "$tmp" .git -zf -`.
|
||||
* bsdtar stops reading options at the first operand (so -zf and - were
|
||||
* file names) and resolves -T names after the -C (so the work-tree
|
||||
* files were looked up in the temp dir): exit 1 and an archive holding
|
||||
* only .git, after the VM was already created.
|
||||
* #3679 usage's END block printed `o == me ? " (you)" : ""` unparenthesized
|
||||
* inside a printf list, a syntax error in the one-true-awk, so usage
|
||||
* exited 2 and the vCPU quota preflight read no total and let every
|
||||
* create through.
|
||||
*
|
||||
* Both need the real tools: macOS's /usr/bin/tar and /usr/bin/awk, or on Linux
|
||||
* `bsdtar` (libarchive-tools) and `original-awk`. Where a tool is missing its
|
||||
* cases are skipped, and the skip names the package. No network, no VM.
|
||||
*/
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, resolve } from 'node:path';
|
||||
|
||||
const ROOT = resolve(import.meta.dir, '..');
|
||||
const RUNNER = join(ROOT, 'scripts/ubicloud/ubi-runner.sh');
|
||||
const darwin = process.platform === 'darwin';
|
||||
const bsdTar = darwin ? '/usr/bin/tar' : Bun.which('bsdtar');
|
||||
const bwkAwk = darwin ? '/usr/bin/awk' : Bun.which('original-awk');
|
||||
|
||||
/** A directory to put first on PATH whose `tar` or `awk` is the BSD one. */
|
||||
function bsdPath(root: string, tools: { tar?: string | null; awk?: string | null }) {
|
||||
const bin = join(root, 'bsd-bin');
|
||||
mkdirSync(bin, { recursive: true });
|
||||
if (tools.tar) symlinkSync(tools.tar, join(bin, 'tar'));
|
||||
if (tools.awk) symlinkSync(tools.awk, join(bin, 'awk'));
|
||||
return `${bin}:${process.env.PATH}`;
|
||||
}
|
||||
|
||||
const gitEnv = { GIT_AUTHOR_NAME: 't', GIT_AUTHOR_EMAIL: 't@t', GIT_COMMITTER_NAME: 't', GIT_COMMITTER_EMAIL: 't@t' };
|
||||
|
||||
describe(`ubi-runner pack under bsdtar (#3680)${bsdTar ? '' : ': skipped, no bsdtar (install libarchive-tools)'}`, () => {
|
||||
// Value: protects=pack writes a whole checkout under bsdtar (macOS's default tar), not an archive of .git alone; fails_when=pack passes tar options after an operand or resolves its -T list after a -C again; why_new=test/ubicloud-runner.test.ts runs pack only under GNU tar on Linux, so #3680 shipped green; seam=PATH (tar resolves to bsdtar); no_claim=the VM side's tar -xzf (GNU tar on Ubuntu, exercised by ubicloud-runner.test.ts)
|
||||
test.skipIf(!bsdTar)('pack ships tracked, staged, untracked, symlinked, spaced and option-like names, a linked worktree and an empty file list', () => {
|
||||
const d = mkdtempSync(join(tmpdir(), 'ubi-bsd-pack-'));
|
||||
try {
|
||||
const PATH = bsdPath(d, { tar: bsdTar });
|
||||
const sh = (cmd: string, cwd: string) => {
|
||||
const r = Bun.spawnSync(['bash', '-c', cmd], { cwd, timeout: 30_000, env: { ...process.env, ...gitEnv, PATH } });
|
||||
expect(r.exitCode, `${cmd}\n${r.stderr.toString()}`).toBe(0);
|
||||
return r.stdout.toString();
|
||||
};
|
||||
expect(sh('tar --version', d)).toContain('bsdtar');
|
||||
const pack = (src: string, out: string) => sh(`bash ${JSON.stringify(RUNNER)} pack ${JSON.stringify(src)} > ${JSON.stringify(out)}`, d);
|
||||
const unpack = (tgz: string, dir: string) => { mkdirSync(join(d, dir)); sh(`tar -xzf ${JSON.stringify(tgz)} -C ${JSON.stringify(dir)}`, d); return join(d, dir); };
|
||||
const status = (cwd: string) => sh('git status --short', cwd).split('\n').filter(Boolean).sort();
|
||||
|
||||
sh('git init -q plain && cd plain && echo a > a && echo b > gone && echo s > "with space" && ln -s a link && echo o > ./-C && echo o > ./--files-from=a && git add -A && git commit -qm one && rm gone && echo st > staged && git add staged && echo u > untracked && echo x > "untracked space"', d);
|
||||
pack('plain', 'plain.tgz');
|
||||
const listing = sh('tar -tzf plain.tgz', d).split('\n');
|
||||
expect(listing.some(l => l.endsWith('files.tar'))).toBe(false);
|
||||
const vm = unpack('plain.tgz', 'vm-plain');
|
||||
expect(status(vm)).toEqual(status(join(d, 'plain')));
|
||||
expect(status(vm)).toEqual([' D gone', '?? "untracked space"', '?? untracked', 'A staged']);
|
||||
expect(readFileSync(join(vm, 'with space'), 'utf8')).toBe('s\n');
|
||||
expect(sh('readlink link', vm).trim()).toBe('a');
|
||||
expect(existsSync(join(vm, '-C')) && existsSync(join(vm, '--files-from=a'))).toBe(true);
|
||||
|
||||
sh('git init -q main && cd main && echo a > a && git add a && git commit -qm one && git branch feat && git worktree add -q ../wt feat', d);
|
||||
sh('echo b > b && git add b && git commit -qm two && echo u > u && echo ignored > ig && echo ig > .gitignore', join(d, 'wt'));
|
||||
pack('wt', 'wt.tgz');
|
||||
const vmWt = unpack('wt.tgz', 'vm-wt');
|
||||
expect(existsSync(join(vmWt, 'ig'))).toBe(false);
|
||||
expect(sh('git rev-parse --abbrev-ref HEAD && git status --short && git log --oneline | wc -l', vmWt).replace(/\s+/g, ' ').trim()).toBe('feat ?? .gitignore ?? u 2');
|
||||
|
||||
sh('git init -q empty && cd empty && echo a > a && git add a && git commit -qm one && rm a', d);
|
||||
pack('empty', 'empty.tgz');
|
||||
expect(status(unpack('empty.tgz', 'vm-empty'))).toEqual([' D a']);
|
||||
} finally { rmSync(d, { recursive: true, force: true }); }
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
describe(`ubi-runner usage and the quota preflight under the one-true-awk (#3679)${bwkAwk ? '' : ': skipped, no one-true-awk (install original-awk)'}`, () => {
|
||||
/** Fake Ubicloud API: two VMs, a standard-4 owned by lane7 and a standard-16 another tool named; every call body goes to calls.log. */
|
||||
function fakeApi() {
|
||||
const root = mkdtempSync(join(tmpdir(), 'ubi-bsd-awk-'));
|
||||
const bin = join(root, 'bin'), log = join(root, 'calls.log');
|
||||
mkdirSync(bin); mkdirSync(join(root, 'state'));
|
||||
writeFileSync(join(bin, 'curl'), `#!/usr/bin/env bash
|
||||
out=""; while [ $# -gt 0 ]; do [ "$1" = -o ] && out=$2; shift; done
|
||||
body=$(cat); echo "$body" >> ${JSON.stringify(log)}
|
||||
case "$body" in
|
||||
*'"list"'*) printf 'eu-central-h1 ubirun-lane7-1761900000-ab12-t3\\neu-north-h1 someone-elses-vm\\n' > "$out" ;;
|
||||
*lane7*'"show"'*) printf 'state: running\\nsize: standard-4\\n' > "$out" ;;
|
||||
*'"show"'*) printf 'state: running\\nsize: standard-16\\n' > "$out" ;;
|
||||
*) : > "$out" ;;
|
||||
esac
|
||||
printf 200
|
||||
`);
|
||||
chmodSync(join(bin, 'curl'), 0o755);
|
||||
const PATH = bsdPath(root, { awk: bwkAwk });
|
||||
const env = { ...process.env, PATH: `${bin}:${PATH}`, UBICLOUD_API_KEY: 'offline', UBI_RUNNER_STATE: join(root, 'state'), UBI_VM_RECORD: join(root, 'vms.tsv'), UBI_OWNER: 'lane7', UBI_REAP: '0', UBI_POLL_SECONDS: '1' };
|
||||
const run = (args: string[], extra: Record<string, string> = {}) => Bun.spawnSync(['bash', RUNNER, ...args], { env: { ...env, ...extra }, timeout: 60_000 });
|
||||
return { root, run, env, calls: () => (existsSync(log) ? readFileSync(log, 'utf8') : '') };
|
||||
}
|
||||
|
||||
// Value: protects=usage runs under macOS's awk and the vCPU preflight refuses over its ceiling there; fails_when=an awk program in the runner uses syntax the one-true-awk rejects, so usage dies and the preflight silently passes; why_new=the runner's awk ran only under mawk/gawk in CI, so #3679 shipped green; seam=PATH (awk resolves to the one-true-awk) and a fake curl; no_claim=real Ubicloud sizes and quota
|
||||
test.skipIf(!bwkAwk)('usage prints the per-owner table, and up refuses a create that would pass UBI_QUOTA_VCPUS', () => {
|
||||
const f = fakeApi();
|
||||
try {
|
||||
expect(Bun.spawnSync(['awk', '--version'], { env: f.env }).stdout.toString()).toContain('awk version');
|
||||
const usage = f.run(['usage']);
|
||||
expect(usage.stderr.toString()).toBe('');
|
||||
expect(usage.exitCode).toBe(0);
|
||||
expect(usage.stdout.toString()).toBe('OWNER VMS VCPUS\n(other) 1 16\nlane7 1 4 (you)\ntotal 2 20\n');
|
||||
const up = f.run(['up', '-s', 'standard-4'], { UBI_QUOTA_VCPUS: '16' });
|
||||
expect(up.exitCode).toBe(1);
|
||||
expect(up.stderr.toString()).toContain('it needs 4 vCPUs and the project\'s VMs already use 20 of 16');
|
||||
expect(up.stderr.toString()).toContain('create refused before any request: vCPU quota exhausted');
|
||||
expect(f.calls()).not.toContain('"create"');
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
}, 60_000);
|
||||
});
|
||||
@@ -250,6 +250,25 @@ esac
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
}, 30_000);
|
||||
|
||||
// Value: protects=a project with no VMs (Ubicloud answers vm list with a bare newline) reads as empty everywhere vm_rows feeds; fails_when=a blank list row becomes a VM named "-" again and reap dies on it or usage asks to show it; why_new=every fake list in this file had at least one VM, so #3678 shipped green; seam=none; no_claim=the real API's empty-body shape (taken from #3678's reading of ubi_cli.rb)
|
||||
test('an empty project (vm list answers a bare or blank line) has no VMs: reap destroys nothing and exits 0, usage prints total 0 0, list --mine prints nothing (#3678)', () => {
|
||||
for (const listed of ['\n', ' \n']) {
|
||||
const f = fakeApi({ destroy: 200, listed });
|
||||
try {
|
||||
const reap = f.run('reap');
|
||||
expect(reap.stderr.toString()).toContain('reap: no expired VMs');
|
||||
expect(reap.stderr.toString()).not.toContain('invalid VM name');
|
||||
expect(reap.exitCode).toBe(0);
|
||||
const usage = f.run('usage');
|
||||
expect(usage.exitCode).toBe(0);
|
||||
expect(usage.stdout.toString()).toBe('OWNER VMS VCPUS\ntotal 0 0\n');
|
||||
expect(f.run('list', '--mine').stdout.toString()).toBe('');
|
||||
expect(f.calls()).not.toContain('destroy');
|
||||
expect(f.calls()).not.toContain('"show"');
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
}
|
||||
});
|
||||
|
||||
test('up and run refuse without UBI_OWNER before any API call; list, usage, reap and down still work without it', () => {
|
||||
const f = fakeApi({ destroy: 200, listed: 'eu-central-h1 vm-a\n', owner: '' });
|
||||
try {
|
||||
@@ -376,6 +395,25 @@ test('pack ships a linked worktree with a self-contained .git on the checked-out
|
||||
} finally { rmSync(d, { recursive: true, force: true }); }
|
||||
}, 90_000);
|
||||
|
||||
// Value: protects=pack never lets macOS tar add AppleDouble ._* entries (files with extended attributes), which the VM's tree check would count as untracked files and fail the run with UBI_VM_TREE_MISMATCH; fails_when=a tar call in pack runs without COPYFILE_DISABLE=1; why_new=only Apple's tar reads the variable, so no Linux run can see the ._* entries themselves; seam=PATH (a tar shim records its environment, then runs the real tar); no_claim=Apple tar's metadata behavior itself (checked by hand on a Mac)
|
||||
test('pack runs every tar with COPYFILE_DISABLE=1, for a checkout and for a plain directory', () => {
|
||||
const d = mkdtempSync(join(tmpdir(), 'ubi-pack-copyfile-'));
|
||||
try {
|
||||
const realTar = Bun.which('tar');
|
||||
expect(realTar).toBeTruthy();
|
||||
mkdirSync(join(d, 'bin'));
|
||||
writeFileSync(join(d, 'bin/tar'), `#!/usr/bin/env bash\necho "COPYFILE_DISABLE=${'$'}{COPYFILE_DISABLE:-unset}" >> ${JSON.stringify(join(d, 'tar.log'))}\nexec ${JSON.stringify(realTar)} "$@"\n`);
|
||||
chmodSync(join(d, 'bin/tar'), 0o755);
|
||||
const env = { ...process.env, PATH: `${join(d, 'bin')}:${process.env.PATH}`, GIT_AUTHOR_NAME: 't', GIT_AUTHOR_EMAIL: 't@t', GIT_COMMITTER_NAME: 't', GIT_COMMITTER_EMAIL: 't@t' };
|
||||
const sh = (cmd: string) => { const r = Bun.spawnSync(['bash', '-c', cmd], { cwd: d, env, timeout: 30_000 }); expect(r.exitCode, r.stderr.toString()).toBe(0); };
|
||||
sh(`git init -q repo && cd repo && echo a > a && git add a && git commit -qm one && bash ${JSON.stringify(join(DIR, 'ubi-runner.sh'))} pack . > ../repo.tgz`);
|
||||
sh(`mkdir plain && echo p > plain/p && bash ${JSON.stringify(join(DIR, 'ubi-runner.sh'))} pack plain > plain.tgz`);
|
||||
const lines = readFileSync(join(d, 'tar.log'), 'utf8').trim().split('\n');
|
||||
expect(lines.length).toBe(3);
|
||||
expect(lines.every(l => l === 'COPYFILE_DISABLE=1')).toBe(true);
|
||||
} finally { rmSync(d, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('up records the VM in the scratch file before the create request is sent', () => {
|
||||
const runner = read('scripts/ubicloud/ubi-runner.sh');
|
||||
const create = runner.indexOf('cli "${args[@]}" >/dev/null 2>"$dir/create.err"');
|
||||
@@ -416,7 +454,7 @@ describe('ubi-runner run, verify and quota against a fake API and a fake ssh', (
|
||||
' *\'"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' +
|
||||
' *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" > "$out"; printf 200; fi ;;\n' +
|
||||
' *\'"show"\'*) [ -z "${FAKE_SHOW_DELAY:-}" ] || sleep "$FAKE_SHOW_DELAY"; if 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.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' +
|
||||
'esac\n');
|
||||
writeFileSync(join(bin, 'ssh'), '#!/usr/bin/env bash\n' +
|
||||
@@ -711,6 +749,47 @@ describe('ubi-runner run, verify and quota against a fake API and a fake ssh', (
|
||||
}, 90_000);
|
||||
|
||||
// Value: protects=the quota wait budget counts seconds slept, not usage reads: a slow usage read keeps the printed budget, a wait above 30 s counts down in 30 s steps, and the wait releases when room appears; fails_when=the budget goes back to a wall-clock deadline taken before the first usage read (the first log line shrinks to 1s), the step cap or the decrement is lost, or room found mid-wait still refuses; why_new=the WAIT=3 case above passes under both the old deadline and the new countdown because its fake usage read is instant and it never sees a second step; seam=none (the fixture's bin/ is already first on PATH, so sleep and curl are stubbed there); no_claim=real elapsed time of the long wait (the 70 s case stubs sleep)
|
||||
// Value: protects=the first up on a fresh project reaches its create with the default reap on; fails_when=the reap before up dies on an empty list again (#3678); why_new=no up case ran against a project with no VMs; seam=none; no_claim=real create and boot (fake API and ssh)
|
||||
test('up on an empty project reaps nothing, creates its VM and comes up (#3678)', () => {
|
||||
const f = fakeVm({ listed: '\n' });
|
||||
try {
|
||||
const up = run(f, ['up']);
|
||||
expect(up.stderr.toString()).toContain('reap: no expired VMs');
|
||||
expect(up.exitCode).toBe(0);
|
||||
expect(f.calls()).toContain('"create"');
|
||||
expect(f.calls()).not.toContain('destroy');
|
||||
const name = up.stdout.toString().trim();
|
||||
expect(name).toMatch(/^ubirun-thrd1-\d{10}-[0-9a-f]{8}-t3$/);
|
||||
expect(run(f, ['down', name]).exitCode).toBe(0);
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
}, 90_000);
|
||||
|
||||
// Value: protects=usage never reports a partial or empty count as the project's real usage, and the quota preflight says when it cannot read usage instead of passing silently; fails_when=a failed vm list or vm show prints a total again (the empty-project fix's `|| true` did that) or the preflight drops its log line; why_new=only successful reads were tested, and #3679 showed an unreadable usage switching the preflight off without a word; seam=none; no_claim=the real API's failure bodies
|
||||
test('usage exits 1 without a total when the VM list or a size read fails; the quota preflight logs that and leaves the call to the API', () => {
|
||||
const f = fakeVm({ listed: 'eu-central-h1 ubirun-peer-9999999999-cccc\n' });
|
||||
const listed = join(f.root, 'listed');
|
||||
try {
|
||||
writeFileSync(`${listed}.list-fails`, '');
|
||||
const noList = run(f, ['usage']);
|
||||
expect(noList.exitCode).toBe(1);
|
||||
expect(noList.stdout.toString()).not.toContain('total');
|
||||
expect(noList.stderr.toString()).toContain("usage: could not list the project's VMs");
|
||||
const up = run(f, ['up'], { UBI_REAP: '0', UBI_QUOTA_VCPUS: '16' });
|
||||
expect(up.stderr.toString()).toContain("quota preflight: could not read the project's vCPU usage; the API's own answer decides");
|
||||
expect(f.calls()).toContain('"create"');
|
||||
rmSync(`${listed}.list-fails`);
|
||||
const name = up.stdout.toString().trim();
|
||||
expect(name).toMatch(/^ubirun-thrd1-\d{10}-[0-9a-f]{8}-t3$/);
|
||||
expect(run(f, ['down', name]).exitCode).toBe(0);
|
||||
|
||||
writeFileSync(`${listed}.show-fails`, '');
|
||||
const noShow = run(f, ['usage']);
|
||||
expect(noShow.exitCode).toBe(1);
|
||||
expect(noShow.stdout.toString()).not.toContain('total');
|
||||
expect(noShow.stderr.toString()).toContain('usage: could not read the size of 1 of 1 VMs');
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
}, 90_000);
|
||||
|
||||
test('quota wait: a slow usage read does not shave the printed budget; a long wait counts down in 30s steps and releases when room appears', () => {
|
||||
const slow = fakeVm({ listed: 'eu-central-h1 ubirun-peer-9999999999-cccc\n' });
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user