Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff62770bcc | ||
|
|
2ad4aa63c6 |
@@ -163,8 +163,15 @@ libfprint, the udev rule, `plugdev` and the systemd service. On a field laptop:
|
||||
```sh
|
||||
curl -fsSL https://tools.yrka.app/fpagent/install.sh | bash -s -- --enable-service
|
||||
curl -fsSL https://tools.yrka.app/fpagent/install.sh | bash -s -- --check # report only
|
||||
curl -fsSL https://tools.yrka.app/fpagent/install.sh | bash -s -- --tag v1.0.0 --enable-service
|
||||
```
|
||||
|
||||
Rerunning it is the upgrade path: it installs the newest release (or `--tag`
|
||||
for a pinned one), the bundle installer reports the version change, and with
|
||||
`--enable-service` it restarts a running service so the new binary takes effect.
|
||||
The version is the git hash carried in the asset name, stamped next to the
|
||||
binary (`/usr/local/bin/fpagent.version`) and compared on the next run.
|
||||
|
||||
The forge is private, so the operator's laptop needs no credentials: the app
|
||||
reads the release with `GITEA_RELEASE_INSTALL_KEY` and proxies it through
|
||||
`/fpagent/releases/*`. Without that key the installer answers `503`.
|
||||
|
||||
@@ -27,8 +27,12 @@ usage() {
|
||||
cat <<EOF
|
||||
Usage: curl -fsSL ${BASE}/install.sh | bash -s -- [installer options]
|
||||
|
||||
Downloads the latest fpagent release, verifies it, and runs its installer.
|
||||
Options are forwarded to that installer:
|
||||
Downloads the fpagent release, verifies it, and runs its installer.
|
||||
|
||||
Options:
|
||||
--tag TAG release tag to install (default: latest)
|
||||
|
||||
Options below are forwarded to the bundled installer:
|
||||
--check preflight and report only; change nothing
|
||||
--offline never touch the network; fail if dependencies are missing
|
||||
--enable-service install and enable a systemd service (starts at boot)
|
||||
@@ -42,13 +46,26 @@ Environment:
|
||||
EOF
|
||||
}
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
RELEASE_TAG="latest"
|
||||
FORWARD=()
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--tag)
|
||||
RELEASE_TAG="${2:?--tag needs a release tag}"
|
||||
shift
|
||||
;;
|
||||
--tag=*)
|
||||
RELEASE_TAG="${1#--tag=}"
|
||||
;;
|
||||
*)
|
||||
FORWARD+=("$1")
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
for cmd in curl tar sha256sum python3 find; do
|
||||
@@ -68,11 +85,17 @@ find "$tmpbase" -maxdepth 1 -mindepth 1 -type d -uid "$(id -u)" \
|
||||
# online bundle (whose installer apt-get's the dependency).
|
||||
codename="$(sed -n 's/^VERSION_CODENAME=//p' /etc/os-release 2>/dev/null | tr -d '"')"
|
||||
|
||||
say "Fetching the latest Fingerprint Agent release..."
|
||||
meta="$(curl -fsSL "${BASE}/releases/latest")" || die "could not fetch the release list from ${BASE}"
|
||||
if [ "$RELEASE_TAG" = "latest" ]; then
|
||||
meta_url="${BASE}/releases/latest"
|
||||
else
|
||||
meta_url="${BASE}/releases/tags/${RELEASE_TAG}"
|
||||
fi
|
||||
|
||||
say "Fetching the Fingerprint Agent release (${RELEASE_TAG})..."
|
||||
meta="$(curl -fsSL "$meta_url")" || die "could not fetch the release list from ${BASE}"
|
||||
|
||||
readout="$(python3 -c '
|
||||
import json, sys
|
||||
import json, re, sys
|
||||
codename = sys.argv[1]
|
||||
release = json.loads(sys.argv[2])
|
||||
assets = [a["name"] for a in release.get("assets", [])]
|
||||
@@ -83,15 +106,19 @@ def pick(suffix):
|
||||
name = pick("-ubuntu-" + codename + "-x86_64.tar.gz") if codename else ""
|
||||
if not name:
|
||||
name = pick("-linux-x86_64.tar.gz")
|
||||
print(release.get("tag", "") + "\t" + name)
|
||||
# The git hash is baked into the asset name (bundle.sh --version) and into
|
||||
# the binary itself; it is the version we record and later compare against.
|
||||
m = re.match(r"^fpagent-(.+)-(?:linux-x86_64|ubuntu-[a-z0-9]+-x86_64)\.tar\.gz$", name)
|
||||
version = m.group(1) if m else ""
|
||||
print(release.get("tag", "") + "\t" + name + "\t" + version)
|
||||
' "$codename" "$meta")" || die "could not parse the release metadata"
|
||||
|
||||
tag="${readout%%$'\t'*}"
|
||||
asset="${readout#*$'\t'}"
|
||||
[ -n "$tag" ] && [ -n "$asset" ] || die "no compatible bundle in the latest release (x86_64 required)"
|
||||
IFS=$'\t' read -r tag asset version <<<"$readout"
|
||||
[ -n "$tag" ] && [ -n "$asset" ] || die "no compatible bundle in release ${RELEASE_TAG} (x86_64 required)"
|
||||
|
||||
say " release ${tag}"
|
||||
say " bundle ${asset}"
|
||||
[ -n "$version" ] && say " version ${version}"
|
||||
|
||||
tmp="$(mktemp -d "$tmpbase/fpagent-install.XXXXXXXX")"
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
@@ -106,5 +133,10 @@ get "${BASE}/releases/download/${tag}/${asset}.sha256" "${tmp}/${asset}.sha256"
|
||||
tar xzf "${tmp}/${asset}" -C "$tmp" || die "could not extract ${asset}"
|
||||
[ -x "${tmp}/install.sh" ] || die "the bundle does not contain install.sh"
|
||||
|
||||
# Hand the version to the bundle installer so it can record it and report an
|
||||
# upgrade. Bundles built by bundle.sh already carry VERSION; this also covers
|
||||
# older bundles and other packagers, using the hash from the asset name.
|
||||
[ -n "$version" ] && printf '%s\n' "$version" >"${tmp}/VERSION"
|
||||
|
||||
say "Running the installer (pass --enable-service to start at boot)..."
|
||||
"${tmp}/install.sh" "$@"
|
||||
"${tmp}/install.sh" "${FORWARD[@]}"
|
||||
|
||||
+18
-5
@@ -345,12 +345,9 @@ export function createApp(overrides = {}) {
|
||||
message: 'The Fingerprint Agent installer is not configured on this deployment.',
|
||||
});
|
||||
|
||||
app.get('/fpagent/releases/latest', async (req, res) => {
|
||||
if (!cfg.giteaToken) return installerNotConfigured(res);
|
||||
const releasePayload = async (res, giteaPath) => {
|
||||
try {
|
||||
const r = await cfg.giteaFetch(`${giteaBase()}/api/v1/repos/${cfg.fpagentRepo}/releases/latest`, {
|
||||
headers: giteaHeaders(),
|
||||
});
|
||||
const r = await cfg.giteaFetch(`${giteaBase()}${giteaPath}`, { headers: giteaHeaders() });
|
||||
if (!r.ok) {
|
||||
return res.status(502).json({ ok: false, message: 'Could not reach the Fingerprint Agent releases.' });
|
||||
}
|
||||
@@ -372,6 +369,22 @@ export function createApp(overrides = {}) {
|
||||
} catch {
|
||||
res.status(502).json({ ok: false, message: 'Could not reach the Fingerprint Agent releases.' });
|
||||
}
|
||||
};
|
||||
|
||||
app.get('/fpagent/releases/latest', (req, res) => {
|
||||
if (!cfg.giteaToken) return installerNotConfigured(res);
|
||||
return releasePayload(res, `/api/v1/repos/${cfg.fpagentRepo}/releases/latest`);
|
||||
});
|
||||
|
||||
app.get('/fpagent/releases/tags/:tag', (req, res) => {
|
||||
if (!cfg.giteaToken) return installerNotConfigured(res);
|
||||
if (!SAFE_ASSET.test(req.params.tag)) {
|
||||
return res.status(400).json({ ok: false, message: 'Unknown release tag.' });
|
||||
}
|
||||
return releasePayload(
|
||||
res,
|
||||
`/api/v1/repos/${cfg.fpagentRepo}/releases/tags/${encodeURIComponent(req.params.tag)}`
|
||||
);
|
||||
});
|
||||
|
||||
app.get('/fpagent/releases/download/:tag/:name', async (req, res) => {
|
||||
|
||||
@@ -3,14 +3,17 @@
|
||||
// laptop never needs credentials.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { execFile, execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { promisify } from 'node:util';
|
||||
import { createApp, FPAGENT_REPO } from '../server.js';
|
||||
import { startServer, stopServer, urlOf } from './helpers.js';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const SCRIPT = path.join(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'..',
|
||||
@@ -49,6 +52,11 @@ async function startFakeGitea({ releaseStatus = 200, downloadStatus = 200 } = {}
|
||||
res.end(JSON.stringify(RELEASE));
|
||||
return;
|
||||
}
|
||||
if (req.url.startsWith(`/api/v1/repos/${FPAGENT_REPO}/releases/tags/`)) {
|
||||
res.writeHead(releaseStatus, { 'content-type': 'application/json' });
|
||||
res.end(JSON.stringify(RELEASE));
|
||||
return;
|
||||
}
|
||||
if (req.url.startsWith(`/${FPAGENT_REPO}/releases/download/`)) {
|
||||
res.writeHead(downloadStatus, { 'content-type': 'application/gzip' });
|
||||
res.end(Buffer.from('bundle-bytes'));
|
||||
@@ -98,6 +106,70 @@ test('latest release is proxied as tools-relative URLs, without the token', asyn
|
||||
}
|
||||
});
|
||||
|
||||
test('a pinned tag is proxied from the forge tag endpoint', async () => {
|
||||
const gitea = await startFakeGitea();
|
||||
const { server, base } = await bootFpagentApp(urlOf(gitea.server));
|
||||
try {
|
||||
const r = await fetch(`${base}/fpagent/releases/tags/v1.0.0`);
|
||||
assert.equal(r.status, 200);
|
||||
assert.equal((await r.json()).tag, TAG);
|
||||
assert.ok(gitea.calls[0].url.endsWith('/releases/tags/v1.0.0'));
|
||||
} finally {
|
||||
await stopServer(server);
|
||||
await stopServer(gitea.server);
|
||||
}
|
||||
});
|
||||
|
||||
test('an unsafe release tag is rejected before reaching the forge', async () => {
|
||||
const gitea = await startFakeGitea();
|
||||
const { server, base } = await bootFpagentApp(urlOf(gitea.server));
|
||||
try {
|
||||
const r = await fetch(`${base}/fpagent/releases/tags/-bad`);
|
||||
assert.equal(r.status, 400);
|
||||
assert.equal(gitea.calls.length, 0);
|
||||
} finally {
|
||||
await stopServer(server);
|
||||
await stopServer(gitea.server);
|
||||
}
|
||||
});
|
||||
|
||||
test('install.sh --tag asks for that tag, not latest', async () => {
|
||||
const calls = [];
|
||||
const gateway = await startServer((req, res) => {
|
||||
calls.push(req.url);
|
||||
if (req.url === '/releases/tags/v7.7.7') {
|
||||
res.writeHead(200, { 'content-type': 'application/json' });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
tag: 'v7.7.7',
|
||||
assets: [{ name: 'fpagent-cafebabe-linux-x86_64.tar.gz' }],
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
});
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'fpagent-tag-test-'));
|
||||
try {
|
||||
let failed = false;
|
||||
try {
|
||||
// Async so the in-process gateway can answer while the child runs.
|
||||
await execFileAsync('bash', [SCRIPT, '--tag', 'v7.7.7', '--check'], {
|
||||
env: { ...process.env, TMPDIR: tmp, FPAGENT_BASE_URL: urlOf(gateway) },
|
||||
});
|
||||
} catch {
|
||||
failed = true; // the asset download 404s; the metadata request is what matters
|
||||
}
|
||||
assert.equal(failed, true);
|
||||
assert.ok(calls.includes('/releases/tags/v7.7.7'), 'should request the pinned tag');
|
||||
assert.ok(!calls.includes('/releases/latest'), 'should not fall back to latest');
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
await stopServer(gateway);
|
||||
}
|
||||
});
|
||||
|
||||
test('bundles stream through this app; the client sends no credentials', async () => {
|
||||
const gitea = await startFakeGitea();
|
||||
const { server, base } = await bootFpagentApp(urlOf(gitea.server));
|
||||
|
||||
Reference in New Issue
Block a user