Subplan 06, first slice. src/server/admin.sx server-renders the
operator console the same way the index and install pages are built —
sx string rendering, no client framework, no build step:
/admin apps overview (platform coverage, channel and
release counts, latest release)
/admin/apps/<slug> metadata + iOS install-mode chip, channels with
policy/rollout/retention, releases newest-first
/admin/releases/<id> artifacts with validation chips and download
links, channels serving the release, audit
timeline (by target, by metadata, by artifact
id prefix)
/admin/tokens lifecycle status via token_status; never a
secret or hash
/admin/audit the full log, newest first
Reads are public like every distd GET in v0; mutations stay on the
token-gated POST endpoints (UI actions are a later slice). Unknown
slug/release/route answer 404 JSON errors with stable codes
(admin.unknown_app / admin.unknown_release / http.not_found). The
module is self-contained (adm_-prefixed helpers) so distd can import it
without a cycle; timestamps render through a local civil-from-days
formatter. The public index footer links the console.
tests/server_admin.sx drives the built binary over curl and pins every
screen, the no-secret-material guarantee, and the 404 codes.
make test 24/24 green.
218 lines
11 KiB
Plaintext
218 lines
11 KiB
Plaintext
// Pinned acceptance for P6.1 (subplan 06) — the read-only admin console
|
|
// served by distd at /admin.
|
|
//
|
|
// Publishes two releases into a fresh store, mints a token, sets a
|
|
// retention policy, cross-promotes onto a second channel, then starts
|
|
// the BUILT `build/dist server run` and asserts every admin screen over
|
|
// curl:
|
|
//
|
|
// * /admin apps overview: app link, iOS mode chip,
|
|
// platform coverage, latest release
|
|
// * /admin/apps/<slug> channels with policy + retention
|
|
// ("keep 2" / "keep all"), install links,
|
|
// releases table with both releases
|
|
// * /admin/releases/<id> artifact row (file, size, sha prefix,
|
|
// validation chip), serving channels,
|
|
// audit timeline (artifact.upload +
|
|
// release.publish + the cli promote)
|
|
// * /admin/tokens token name + lifecycle status; the
|
|
// page NEVER contains the secret or any
|
|
// hash material
|
|
// * /admin/audit newest-first log naming every actor
|
|
// and action recorded so far
|
|
// * unknown slug / release / route → 404 JSON errors with stable codes
|
|
// * / (public index) links the admin console
|
|
#import "modules/std.sx";
|
|
#import "modules/std/json.sx";
|
|
process :: #import "modules/std/process.sx";
|
|
|
|
STORE :: ".sx-tmp/server_admin";
|
|
MDIR :: ".sx-tmp/server_admin_m";
|
|
PORT :: "18799";
|
|
BASE :: "http://127.0.0.1:18799";
|
|
|
|
MANIFEST_A :: "{\"app\":\"acme-app\",\"version\":\"1.2.3\",\"channel\":\"stable\",\"artifacts\":[{\"platform\":\"android_apk\",\"path\":\"../../examples/fixtures/acme-1.2.3-android.apk\"}]}";
|
|
MANIFEST_B :: "{\"app\":\"acme-app\",\"version\":\"1.2.4\",\"channel\":\"stable\",\"artifacts\":[{\"platform\":\"android_apk\",\"path\":\"../../examples/fixtures/acme-1.2.3-android.apk\"}]}";
|
|
|
|
REL_A :: "rel-acme-app-1.2.3";
|
|
REL_B :: "rel-acme-app-1.2.4";
|
|
|
|
get :: (o: Object, key: string) -> Value {
|
|
i := 0;
|
|
while i < o.len {
|
|
if o.items[i].key == key { return o.items[i].val; }
|
|
i += 1;
|
|
}
|
|
process.assert(false, concat("missing json key: ", key));
|
|
dummy : Value = .null_;
|
|
return dummy;
|
|
}
|
|
get_str :: (o: Object, key: string) -> string { return get(o, key).str; }
|
|
get_obj :: (o: Object, key: string) -> Object { return get(o, key).object; }
|
|
|
|
write_file :: (path: string, body: string) {
|
|
cmd := concat(concat(concat("printf '%s' '", body), "' > "), path);
|
|
process.run(cmd);
|
|
}
|
|
|
|
// True iff `needle` occurs in `hay` (plain scan; bodies are small).
|
|
contains :: (hay: string, needle: string) -> bool {
|
|
if needle.len == 0 { return true; }
|
|
if needle.len > hay.len { return false; }
|
|
i := 0;
|
|
while i + needle.len <= hay.len {
|
|
j := 0;
|
|
ok := true;
|
|
while j < needle.len {
|
|
if hay[i + j] != needle[j] { ok = false; break; }
|
|
j += 1;
|
|
}
|
|
if ok { return true; }
|
|
i += 1;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
fetch :: (path: string) -> string {
|
|
r := process.run(concat(concat("curl -s -m 2 ", BASE), path));
|
|
process.assert(r != null, concat("curl spawn failed: ", path));
|
|
return r!.stdout;
|
|
}
|
|
|
|
fetch_code :: (path: string) -> string {
|
|
r := process.run(concat(concat("curl -s -m 2 -o /dev/null -w '%{http_code}' ", BASE), path));
|
|
process.assert(r != null, concat("curl spawn failed: ", path));
|
|
return r!.stdout;
|
|
}
|
|
|
|
parse_body :: (body: string, what: string, scratch: Allocator) -> Object {
|
|
v, e := parse(body, scratch);
|
|
if e {
|
|
process.assert(false, concat("response must be valid JSON: ", what));
|
|
dummy : Object = .{};
|
|
return dummy;
|
|
}
|
|
return v.object;
|
|
}
|
|
|
|
publish_cmd :: (mpath: string) -> string {
|
|
c := concat("build/dist ci publish --manifest ", mpath);
|
|
c = concat(c, concat(" --local-store ", STORE));
|
|
return concat(c, " --json 2>/dev/null >/dev/null");
|
|
}
|
|
|
|
main :: () -> i32 {
|
|
gpa := GPA.init();
|
|
arena := Arena.init(xx gpa, 1 << 20);
|
|
defer arena.deinit();
|
|
|
|
process.run("pkill -f 'dist server run --local-store .sx-tmp/server_admin' 2>/dev/null");
|
|
process.run(concat("rm -rf ", STORE));
|
|
process.run(concat("rm -rf ", MDIR));
|
|
process.run(concat("mkdir -p ", MDIR));
|
|
write_file(path_join(MDIR, "a.json"), MANIFEST_A);
|
|
write_file(path_join(MDIR, "b.json"), MANIFEST_B);
|
|
|
|
// ── seed: two releases, a token, a retention policy, a cross-promote ─
|
|
ra := process.run(publish_cmd(path_join(MDIR, "a.json")));
|
|
process.assert(ra != null and ra!.exit_code == 0, "publish A must exit 0");
|
|
rb := process.run(publish_cmd(path_join(MDIR, "b.json")));
|
|
process.assert(rb != null and rb!.exit_code == 0, "publish B must exit 0");
|
|
|
|
tc := process.run(concat(concat("build/dist token create --name ci-main --local-store ", STORE), " --json 2>/dev/null"));
|
|
process.assert(tc != null and tc!.exit_code == 0, "token create must exit 0");
|
|
tco := parse_body(tc!.stdout, "token create", xx arena);
|
|
secret := get_str(get_obj(tco, "token"), "secret");
|
|
process.assert(secret.len > 0, "token create returned the secret");
|
|
|
|
cs := process.run(concat(concat("build/dist channel set --app acme-app --channel stable --retention-keep 2 --local-store ", STORE), " --json 2>/dev/null"));
|
|
process.assert(cs != null and cs!.exit_code == 0, "channel set must exit 0");
|
|
|
|
pr := process.run(concat(concat(concat("build/dist release promote --app acme-app --channel beta --release ", REL_A), concat(" --local-store ", STORE)), " --json 2>/dev/null"));
|
|
process.assert(pr != null and pr!.exit_code == 0, "promote A onto beta must exit 0");
|
|
|
|
// ── start the server, poll /healthz ──────────────────────────────
|
|
sp := process.run(concat(concat(concat("sh -c 'build/dist server run --local-store ", STORE), concat(concat(" --port ", PORT), " >/dev/null 2>&1 & echo $!'")), ""));
|
|
process.assert(sp != null, "server spawn failed");
|
|
pid := sp!.stdout;
|
|
|
|
ready := false;
|
|
tries := 0;
|
|
while tries < 50 {
|
|
c := fetch_code("/healthz");
|
|
if c == "200" { ready = true; break; }
|
|
process.run("sleep 0.2");
|
|
tries += 1;
|
|
}
|
|
process.assert(ready, "server must answer /healthz within 10s");
|
|
print(" server up\n");
|
|
|
|
// ── /admin — apps overview ────────────────────────────────────────
|
|
r := process.run(concat(concat("curl -s -m 2 -o /dev/null -w '%{http_code} %{content_type}' ", BASE), "/admin"));
|
|
process.assert(r != null and contains(r!.stdout, "200 text/html"), "/admin is a 200 HTML page");
|
|
ov := fetch("/admin");
|
|
process.assert(contains(ov, "href=\"/admin/apps/acme-app\""), "overview links the app detail");
|
|
process.assert(contains(ov, "ios: artifact-only"), "overview shows the iOS mode chip");
|
|
process.assert(contains(ov, "android_apk"), "overview shows platform coverage");
|
|
process.assert(contains(ov, "1.2.4"), "overview shows the latest release version");
|
|
print(" /admin overview ok\n");
|
|
|
|
// ── /admin/apps/<slug> — channels + retention + releases ─────────
|
|
ad := fetch("/admin/apps/acme-app");
|
|
process.assert(contains(ad, "keep 2"), "stable shows its retention policy");
|
|
process.assert(contains(ad, "keep all"), "beta shows the keep-everything default");
|
|
process.assert(contains(ad, "href=\"/install/acme-app/stable\""), "channels link their install pages");
|
|
process.assert(contains(ad, REL_A) and contains(ad, REL_B), "releases table lists both releases");
|
|
process.assert(contains(ad, "manual"), "channels show their rollout policy");
|
|
print(" /admin/apps/acme-app ok\n");
|
|
|
|
// ── /admin/releases/<id> — artifacts, serving, timeline ──────────
|
|
rd := fetch(concat("/admin/releases/", REL_A));
|
|
process.assert(contains(rd, "acme-1.2.3-android.apk"), "release detail names the artifact file");
|
|
process.assert(contains(rd, ">valid</span>") or contains(rd, ">pending</span>"), "artifact carries a validation chip");
|
|
process.assert(contains(rd, "href=\"/download/"), "artifact links its download");
|
|
process.assert(contains(rd, "<tr><th>serving</th><td>beta</td></tr>"), "release detail names the channel serving it");
|
|
process.assert(contains(rd, "artifact.upload"), "timeline shows the upload event");
|
|
process.assert(contains(rd, "release.publish"), "timeline shows the publish event");
|
|
process.assert(contains(rd, "channel.promote"), "timeline shows the cli promote (metadata names this release)");
|
|
print(" /admin/releases/<id> ok\n");
|
|
|
|
// ── /admin/tokens — status without secret material ────────────────
|
|
tk := fetch("/admin/tokens");
|
|
process.assert(contains(tk, "ci-main"), "tokens screen names the token");
|
|
process.assert(contains(tk, ">active</span>"), "tokens screen shows lifecycle status");
|
|
process.assert(!contains(tk, secret), "tokens screen NEVER contains the secret");
|
|
process.assert(!contains(tk, "token_hash"), "tokens screen never mentions hash material");
|
|
print(" /admin/tokens ok\n");
|
|
|
|
// ── /admin/audit — every recorded actor/action ────────────────────
|
|
au := fetch("/admin/audit");
|
|
process.assert(contains(au, "token.create"), "audit shows token.create");
|
|
process.assert(contains(au, "channel.update"), "audit shows the retention change");
|
|
process.assert(contains(au, "channel.promote"), "audit shows the promote");
|
|
process.assert(contains(au, "release.publish"), "audit shows the publishes");
|
|
process.assert(contains(au, ">cli</td>") and contains(au, ">ci</td>"), "audit names both actors");
|
|
print(" /admin/audit ok\n");
|
|
|
|
// ── 404s with stable codes ────────────────────────────────────────
|
|
process.assert(fetch_code("/admin/apps/nope") == "404", "unknown slug is 404");
|
|
na := parse_body(fetch("/admin/apps/nope"), "unknown slug body", xx arena);
|
|
process.assert(get_str(get_obj(na, "error"), "code") == "admin.unknown_app", "unknown slug names admin.unknown_app");
|
|
process.assert(fetch_code("/admin/releases/nope") == "404", "unknown release is 404");
|
|
nr := parse_body(fetch("/admin/releases/nope"), "unknown release body", xx arena);
|
|
process.assert(get_str(get_obj(nr, "error"), "code") == "admin.unknown_release", "unknown release names admin.unknown_release");
|
|
process.assert(fetch_code("/admin/bogus") == "404", "unknown admin route is 404");
|
|
print(" admin 404s ok\n");
|
|
|
|
// ── the public index links the console ────────────────────────────
|
|
idx := fetch("/");
|
|
process.assert(contains(idx, "href=\"/admin\""), "public index links /admin");
|
|
|
|
// ── teardown ──────────────────────────────────────────────────────
|
|
process.run(concat("kill ", pid));
|
|
process.run(concat("rm -rf ", STORE));
|
|
process.run(concat("rm -rf ", MDIR));
|
|
print("server_admin: ALL CASES PASS\n");
|
|
return 0;
|
|
}
|