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
|
```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 -- --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 -- --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
|
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
|
reads the release with `GITEA_RELEASE_INSTALL_KEY` and proxies it through
|
||||||
`/fpagent/releases/*`. Without that key the installer answers `503`.
|
`/fpagent/releases/*`. Without that key the installer answers `503`.
|
||||||
|
|||||||
@@ -27,8 +27,12 @@ usage() {
|
|||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: curl -fsSL ${BASE}/install.sh | bash -s -- [installer options]
|
Usage: curl -fsSL ${BASE}/install.sh | bash -s -- [installer options]
|
||||||
|
|
||||||
Downloads the latest fpagent release, verifies it, and runs its installer.
|
Downloads the fpagent release, verifies it, and runs its installer.
|
||||||
Options are forwarded to that 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
|
--check preflight and report only; change nothing
|
||||||
--offline never touch the network; fail if dependencies are missing
|
--offline never touch the network; fail if dependencies are missing
|
||||||
--enable-service install and enable a systemd service (starts at boot)
|
--enable-service install and enable a systemd service (starts at boot)
|
||||||
@@ -42,13 +46,26 @@ Environment:
|
|||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
for arg in "$@"; do
|
RELEASE_TAG="latest"
|
||||||
case "$arg" in
|
FORWARD=()
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
-h | --help)
|
-h | --help)
|
||||||
usage
|
usage
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
|
--tag)
|
||||||
|
RELEASE_TAG="${2:?--tag needs a release tag}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--tag=*)
|
||||||
|
RELEASE_TAG="${1#--tag=}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
FORWARD+=("$1")
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
|
shift
|
||||||
done
|
done
|
||||||
|
|
||||||
for cmd in curl tar sha256sum python3 find; do
|
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).
|
# online bundle (whose installer apt-get's the dependency).
|
||||||
codename="$(sed -n 's/^VERSION_CODENAME=//p' /etc/os-release 2>/dev/null | tr -d '"')"
|
codename="$(sed -n 's/^VERSION_CODENAME=//p' /etc/os-release 2>/dev/null | tr -d '"')"
|
||||||
|
|
||||||
say "Fetching the latest Fingerprint Agent release..."
|
if [ "$RELEASE_TAG" = "latest" ]; then
|
||||||
meta="$(curl -fsSL "${BASE}/releases/latest")" || die "could not fetch the release list from ${BASE}"
|
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 '
|
readout="$(python3 -c '
|
||||||
import json, sys
|
import json, re, sys
|
||||||
codename = sys.argv[1]
|
codename = sys.argv[1]
|
||||||
release = json.loads(sys.argv[2])
|
release = json.loads(sys.argv[2])
|
||||||
assets = [a["name"] for a in release.get("assets", [])]
|
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 ""
|
name = pick("-ubuntu-" + codename + "-x86_64.tar.gz") if codename else ""
|
||||||
if not name:
|
if not name:
|
||||||
name = pick("-linux-x86_64.tar.gz")
|
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"
|
' "$codename" "$meta")" || die "could not parse the release metadata"
|
||||||
|
|
||||||
tag="${readout%%$'\t'*}"
|
IFS=$'\t' read -r tag asset version <<<"$readout"
|
||||||
asset="${readout#*$'\t'}"
|
[ -n "$tag" ] && [ -n "$asset" ] || die "no compatible bundle in release ${RELEASE_TAG} (x86_64 required)"
|
||||||
[ -n "$tag" ] && [ -n "$asset" ] || die "no compatible bundle in the latest release (x86_64 required)"
|
|
||||||
|
|
||||||
say " release ${tag}"
|
say " release ${tag}"
|
||||||
say " bundle ${asset}"
|
say " bundle ${asset}"
|
||||||
|
[ -n "$version" ] && say " version ${version}"
|
||||||
|
|
||||||
tmp="$(mktemp -d "$tmpbase/fpagent-install.XXXXXXXX")"
|
tmp="$(mktemp -d "$tmpbase/fpagent-install.XXXXXXXX")"
|
||||||
trap 'rm -rf "$tmp"' EXIT
|
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}"
|
tar xzf "${tmp}/${asset}" -C "$tmp" || die "could not extract ${asset}"
|
||||||
[ -x "${tmp}/install.sh" ] || die "the bundle does not contain install.sh"
|
[ -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)..."
|
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.',
|
message: 'The Fingerprint Agent installer is not configured on this deployment.',
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/fpagent/releases/latest', async (req, res) => {
|
const releasePayload = async (res, giteaPath) => {
|
||||||
if (!cfg.giteaToken) return installerNotConfigured(res);
|
|
||||||
try {
|
try {
|
||||||
const r = await cfg.giteaFetch(`${giteaBase()}/api/v1/repos/${cfg.fpagentRepo}/releases/latest`, {
|
const r = await cfg.giteaFetch(`${giteaBase()}${giteaPath}`, { headers: giteaHeaders() });
|
||||||
headers: giteaHeaders(),
|
|
||||||
});
|
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
return res.status(502).json({ ok: false, message: 'Could not reach the Fingerprint Agent releases.' });
|
return res.status(502).json({ ok: false, message: 'Could not reach the Fingerprint Agent releases.' });
|
||||||
}
|
}
|
||||||
@@ -372,6 +369,22 @@ export function createApp(overrides = {}) {
|
|||||||
} catch {
|
} catch {
|
||||||
res.status(502).json({ ok: false, message: 'Could not reach the Fingerprint Agent releases.' });
|
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) => {
|
app.get('/fpagent/releases/download/:tag/:name', async (req, res) => {
|
||||||
|
|||||||
@@ -3,14 +3,17 @@
|
|||||||
// laptop never needs credentials.
|
// laptop never needs credentials.
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
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 fs from 'node:fs';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { promisify } from 'node:util';
|
||||||
import { createApp, FPAGENT_REPO } from '../server.js';
|
import { createApp, FPAGENT_REPO } from '../server.js';
|
||||||
import { startServer, stopServer, urlOf } from './helpers.js';
|
import { startServer, stopServer, urlOf } from './helpers.js';
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
const SCRIPT = path.join(
|
const SCRIPT = path.join(
|
||||||
path.dirname(fileURLToPath(import.meta.url)),
|
path.dirname(fileURLToPath(import.meta.url)),
|
||||||
'..',
|
'..',
|
||||||
@@ -49,6 +52,11 @@ async function startFakeGitea({ releaseStatus = 200, downloadStatus = 200 } = {}
|
|||||||
res.end(JSON.stringify(RELEASE));
|
res.end(JSON.stringify(RELEASE));
|
||||||
return;
|
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/`)) {
|
if (req.url.startsWith(`/${FPAGENT_REPO}/releases/download/`)) {
|
||||||
res.writeHead(downloadStatus, { 'content-type': 'application/gzip' });
|
res.writeHead(downloadStatus, { 'content-type': 'application/gzip' });
|
||||||
res.end(Buffer.from('bundle-bytes'));
|
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 () => {
|
test('bundles stream through this app; the client sends no credentials', async () => {
|
||||||
const gitea = await startFakeGitea();
|
const gitea = await startFakeGitea();
|
||||||
const { server, base } = await bootFpagentApp(urlOf(gitea.server));
|
const { server, base } = await bootFpagentApp(urlOf(gitea.server));
|
||||||
|
|||||||
Reference in New Issue
Block a user