mirror of
https://github.com/andrewyng/openworker.git
synced 2026-11-04 21:37:21 +00:00
324 lines
9.4 KiB
JavaScript
324 lines
9.4 KiB
JavaScript
// Bounded HTTP/Kubernetes helpers for the explicitly owned AWS experiment.
|
|
// No browser automation, model credential access or automatic mutation retries.
|
|
import assert from "node:assert/strict";
|
|
import https from "node:https";
|
|
import { spawn } from "node:child_process";
|
|
import { readFile, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { randomUUID } from "node:crypto";
|
|
import { repo, kubectl, apply } from "../helm/local.mjs";
|
|
import { name } from "./templates.mjs";
|
|
import { account } from "./client.mjs";
|
|
export const root = join(repo, ".ow-aws");
|
|
// Qualification-specific filters, not replacements for server-side admission.
|
|
export const deploymentPods = (items) =>
|
|
items.filter((p) =>
|
|
p.metadata.ownerReferences?.some((r) => r.kind === "ReplicaSet"),
|
|
);
|
|
export const hasFixtureWorker = (pool) =>
|
|
pool.workers.some(
|
|
(w) =>
|
|
!w.draining &&
|
|
w.selector.scripted === true &&
|
|
w.selector.fallback_model === "fixture",
|
|
);
|
|
export function postgresEnvironment(connection) {
|
|
const u = new URL(connection);
|
|
assert.equal(u.protocol, "postgresql:");
|
|
assert.equal(u.pathname, "/openworker");
|
|
assert.ok(u.hostname && u.username && u.password && !u.search && !u.hash);
|
|
return {
|
|
PGHOST: u.hostname,
|
|
PGPORT: u.port || "5432",
|
|
PGDATABASE: "openworker",
|
|
PGUSER: decodeURIComponent(u.username),
|
|
PGPASSWORD: decodeURIComponent(u.password),
|
|
PGCONNECT_TIMEOUT: "10",
|
|
};
|
|
}
|
|
export async function context() {
|
|
const m = JSON.parse(await readFile(join(root, "kubernetes.json"), "utf8"));
|
|
const plan = JSON.parse(await readFile(join(root, "plan.json"), "utf8"));
|
|
assert.equal(m.context, name);
|
|
assert.equal(plan.name, name);
|
|
assert.equal(plan.account, account);
|
|
assert.ok(Date.now() < Date.parse(plan.expiry));
|
|
const ns = JSON.parse(
|
|
await kubectl(m, ["get", "namespace", "ow-app", "-o", "json"]),
|
|
);
|
|
assert.equal(ns.metadata.labels["openworker.dev/experiment"], name);
|
|
return m;
|
|
}
|
|
export const save = (file, data) =>
|
|
writeFile(join(root, file), JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
export async function wait(fn, ms = 120000) {
|
|
const end = Date.now() + ms;
|
|
while (Date.now() < end) {
|
|
const result = await fn();
|
|
if (result) return result;
|
|
await new Promise((r) => setTimeout(r, 500));
|
|
}
|
|
throw Error("Qualification state deadline");
|
|
}
|
|
export async function pods(m, role, release = "ow") {
|
|
return JSON.parse(
|
|
await kubectl(m, [
|
|
"-n",
|
|
"ow-app",
|
|
"get",
|
|
"pods",
|
|
"-l",
|
|
`app.kubernetes.io/instance=${release},app.kubernetes.io/component=${role}`,
|
|
"-o",
|
|
"json",
|
|
]),
|
|
).items.filter(
|
|
(p) =>
|
|
!p.metadata.deletionTimestamp &&
|
|
p.status.containerStatuses?.every((c) => c.ready),
|
|
);
|
|
}
|
|
export async function forward(m, pod, remote = 8443) {
|
|
const child = spawn(
|
|
"kubectl",
|
|
[
|
|
"--kubeconfig",
|
|
m.kubeconfig,
|
|
"--context",
|
|
m.context,
|
|
"-n",
|
|
"ow-app",
|
|
"port-forward",
|
|
"--address",
|
|
"127.0.0.1",
|
|
"pod/" + pod,
|
|
`0:${remote}`,
|
|
],
|
|
{ stdio: ["ignore", "pipe", "pipe"] },
|
|
);
|
|
child.stderr.resume();
|
|
const port = await new Promise((resolve, reject) => {
|
|
let out = "";
|
|
const timer = setTimeout(() => {
|
|
child.kill("SIGTERM");
|
|
reject(Error("Forward deadline"));
|
|
}, 20000);
|
|
child.on("error", reject);
|
|
child.on("exit", () => {
|
|
clearTimeout(timer);
|
|
reject(Error("Forward ended"));
|
|
});
|
|
child.stdout.on("data", (b) => {
|
|
out += b;
|
|
const match = out.match(/127\.0\.0\.1:(\d+) ->/);
|
|
if (match) {
|
|
clearTimeout(timer);
|
|
resolve(Number(match[1]));
|
|
}
|
|
});
|
|
});
|
|
return { port, close: () => child.kill("SIGTERM") };
|
|
}
|
|
export async function client(port, key = "operator") {
|
|
const token = (await readFile(join(root, key), "utf8")).trim(),
|
|
ca = await readFile(join(root, "ca.crt"));
|
|
async function request(path, { body, raw, method, stream = false } = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const bytes =
|
|
raw ??
|
|
(body === undefined ? undefined : Buffer.from(JSON.stringify(body)));
|
|
const r = https.request(
|
|
{
|
|
hostname: "127.0.0.1",
|
|
port,
|
|
servername: "localhost",
|
|
ca,
|
|
path,
|
|
method: method || (body ? "POST" : "GET"),
|
|
headers: {
|
|
Authorization: "Bearer " + token,
|
|
...(bytes
|
|
? {
|
|
"Content-Type": raw
|
|
? "application/octet-stream"
|
|
: "application/json",
|
|
"Content-Length": bytes.length,
|
|
}
|
|
: {}),
|
|
},
|
|
},
|
|
(res) => {
|
|
if (stream) {
|
|
resolve(res);
|
|
return;
|
|
}
|
|
const chunks = [];
|
|
let size = 0;
|
|
res.on("data", (b) => {
|
|
size += b.length;
|
|
if (size > 4 * 1024 * 1024) {
|
|
res.destroy(Error("Bounded response exceeded"));
|
|
return;
|
|
}
|
|
chunks.push(b);
|
|
});
|
|
res.on("error", reject);
|
|
res.on("end", () =>
|
|
resolve({ status: res.statusCode, bytes: Buffer.concat(chunks) }),
|
|
);
|
|
},
|
|
);
|
|
r.on("error", reject);
|
|
r.setTimeout(30000, () => r.destroy(Error("HTTP deadline")));
|
|
r.end(bytes);
|
|
});
|
|
}
|
|
async function json(path, body) {
|
|
const r = await request(path, { body });
|
|
assert.ok(r.status >= 200 && r.status < 300, `${path}: ${r.status}`);
|
|
return JSON.parse(r.bytes);
|
|
}
|
|
const snapshot = (sid) => json("/sessions/" + sid);
|
|
async function state(sid, status) {
|
|
return wait(async () => {
|
|
const s = await snapshot(sid);
|
|
assert.ok(
|
|
!["failed", "truncated"].includes(s.turn?.status),
|
|
`unexpected ${s.turn?.status}`,
|
|
);
|
|
return s.turn?.status === status && s;
|
|
});
|
|
}
|
|
async function events(sid, through) {
|
|
const res = await request(`/sessions/${sid}/events?after=0`, {
|
|
stream: true,
|
|
});
|
|
assert.equal(res.statusCode, 200);
|
|
const out = [];
|
|
let buffer = "";
|
|
const timer = setTimeout(
|
|
() => res.destroy(Error("Replay deadline")),
|
|
30000,
|
|
);
|
|
try {
|
|
for await (const chunk of res) {
|
|
buffer += chunk.toString();
|
|
let i;
|
|
while ((i = buffer.indexOf("\n\n")) >= 0) {
|
|
const frame = buffer.slice(0, i);
|
|
buffer = buffer.slice(i + 2);
|
|
const line = frame.split("\n").find((l) => l.startsWith("data: "));
|
|
if (!line) continue;
|
|
const e = JSON.parse(line.slice(6));
|
|
assert.equal(e.event_id, out.length + 1);
|
|
out.push(e);
|
|
if (e.event_id >= through) return out;
|
|
}
|
|
}
|
|
throw Error("Replay incomplete");
|
|
} finally {
|
|
clearTimeout(timer);
|
|
res.destroy();
|
|
}
|
|
}
|
|
async function approve(sid, s, expectedName, response = "once") {
|
|
const i = s.interactions.find((i) => !i.response);
|
|
assert.ok(i);
|
|
assert.equal(i.kind, "approval");
|
|
assert.equal(i.call.name, expectedName);
|
|
return json(`/sessions/${sid}/controls`, {
|
|
request_id: randomUUID(),
|
|
turn_id: i.turn_id,
|
|
action: {
|
|
kind: "response",
|
|
interaction_id: i.id,
|
|
response,
|
|
binding: Object.fromEntries(
|
|
[
|
|
"bundle_digest",
|
|
"schema_digest",
|
|
"policy_digest",
|
|
"resource_policy_digest",
|
|
].map((k) => [k, i[k]]),
|
|
),
|
|
},
|
|
});
|
|
}
|
|
return { request, json, snapshot, state, events, approve };
|
|
}
|
|
export async function admin(m, args, release = "ow") {
|
|
const dep = JSON.parse(
|
|
await kubectl(m, [
|
|
"-n",
|
|
"ow-app",
|
|
"get",
|
|
"deployment",
|
|
release + "-openworker-worker",
|
|
"-o",
|
|
"json",
|
|
]),
|
|
);
|
|
const spec = dep.spec.template.spec,
|
|
c = spec.containers[0];
|
|
c.args = ["--config", "/config/reference.json", ...args];
|
|
c.name = "admin";
|
|
delete c.envFrom;
|
|
delete c.readinessProbe;
|
|
delete c.livenessProbe;
|
|
c.env = c.env.filter((e) => e.name !== "POD_NAME");
|
|
c.volumeMounts = c.volumeMounts.filter((v) => v.name !== "openshell");
|
|
spec.volumes = spec.volumes.filter((v) => v.name !== "openshell");
|
|
spec.restartPolicy = "Never";
|
|
const id = "ow-current-" + randomUUID().slice(0, 8);
|
|
await apply(m, [
|
|
{
|
|
apiVersion: "batch/v1",
|
|
kind: "Job",
|
|
metadata: {
|
|
name: id,
|
|
namespace: "ow-app",
|
|
labels: { "openworker.dev/experiment": name },
|
|
},
|
|
spec: {
|
|
backoffLimit: 0,
|
|
template: {
|
|
metadata: { labels: dep.spec.template.metadata.labels },
|
|
spec,
|
|
},
|
|
},
|
|
},
|
|
]);
|
|
await kubectl(m, [
|
|
"-n",
|
|
"ow-app",
|
|
"wait",
|
|
"--for=condition=complete",
|
|
"job/" + id,
|
|
"--timeout=120s",
|
|
]);
|
|
return kubectl(m, ["-n", "ow-app", "logs", "job/" + id, "-c", "admin"]);
|
|
}
|
|
export const planTool = (name, args) =>
|
|
"TEST_RESOURCE_GRANTS " + JSON.stringify([{ name, arguments: args }]);
|
|
export async function permitPodIdentity(m, release) {
|
|
assert.ok(["variant", "restore", "live"].includes(release));
|
|
const p = JSON.parse(
|
|
await kubectl(m, [
|
|
"-n",
|
|
"ow-app",
|
|
"get",
|
|
"networkpolicy",
|
|
"api-pod-identity",
|
|
"-o",
|
|
"json",
|
|
]),
|
|
);
|
|
p.metadata = {
|
|
name: release + "-pod-identity",
|
|
namespace: "ow-app",
|
|
labels: { "openworker.dev/experiment": name },
|
|
};
|
|
p.spec.podSelector.matchLabels["app.kubernetes.io/instance"] = release;
|
|
await apply(m, [p]);
|
|
}
|