mirror of
https://github.com/andrewyng/openworker.git
synced 2026-11-04 21:37:21 +00:00
224 lines
8.1 KiB
JavaScript
224 lines
8.1 KiB
JavaScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { run, repo } from "./local.mjs";
|
|
const helm = process.env.HELM || "helm",
|
|
digest = "sha256:" + "a".repeat(64);
|
|
const values = {
|
|
image: "example/harness@" + digest,
|
|
config: {
|
|
database_env: "OW_SERVER_DATABASE_URL",
|
|
schema: "ow_test",
|
|
database_ca: "/database-ca/ca.crt",
|
|
state_root: "/state",
|
|
bundle: "/opt/openworker/bundle",
|
|
runner: "/opt/openworker/openworker",
|
|
model: "fixture",
|
|
scripted: true,
|
|
api_token_file: "/bootstrap/admin",
|
|
actor: "test",
|
|
openshell: {
|
|
identity: "test",
|
|
endpoint: "https://gateway.example:8080",
|
|
certificates: "/openshell",
|
|
workspace: "default",
|
|
image: "example/runner@" + digest,
|
|
},
|
|
},
|
|
secrets: {
|
|
database: "db",
|
|
databaseCA: "db-ca",
|
|
openshell: "shell",
|
|
apiTLS: "tls",
|
|
probe: "probe",
|
|
},
|
|
};
|
|
async function render(v) {
|
|
const root = await mkdtemp(join(tmpdir(), "ow-chart-"));
|
|
try {
|
|
const p = join(root, "values.json");
|
|
await writeFile(p, JSON.stringify(v));
|
|
return await run(helm, [
|
|
"template",
|
|
"test",
|
|
join(repo, "deploy/helm/openworker"),
|
|
"-f",
|
|
p,
|
|
]);
|
|
} finally {
|
|
await rm(root, { recursive: true });
|
|
}
|
|
}
|
|
test("separate hardened roles and TLS-only service; chart embeds no credentials or databases", async () => {
|
|
const out = await render(values);
|
|
assert.equal((out.match(/kind: Deployment/g) || []).length, 2);
|
|
assert.equal(
|
|
(out.match(/automountServiceAccountToken: false/g) || []).length,
|
|
2,
|
|
);
|
|
assert.equal((out.match(/readOnlyRootFilesystem: true/g) || []).length, 3);
|
|
assert.match(out, /127\.0\.0\.1:8080/);
|
|
assert.match(out, /targetPort: https/);
|
|
assert.match(out, /subPath: reference.json/);
|
|
assert.match(out, /proxy_buffering off/);
|
|
assert.match(out, /items: \[\{key: ca.crt, path: ca.crt\}\]/);
|
|
assert.match(out, /kind: NetworkPolicy/);
|
|
assert.doesNotMatch(
|
|
out,
|
|
/kind: Secret|kind: StatefulSet|hostPath:|privileged: true|NodePort|LoadBalancer/,
|
|
);
|
|
assert.doesNotMatch(out, /mountPath: \/bootstrap/);
|
|
});
|
|
test("chart fails closed on missing secrets, mutable images, plaintext DB and invalid capacity", async () => {
|
|
for (const change of [
|
|
(v) => (v.secrets.database = ""),
|
|
(v) => (v.image = "example:latest"),
|
|
(v) => (v.config.database_loopback_plaintext = true),
|
|
(v) => (v.config.openshell.endpoint = "http://gateway:8080"),
|
|
(v) => (v.worker = { replicas: 2, slots: 0 }),
|
|
(v) => (v.config.artifacts = { kind: "filesystem" }),
|
|
(v) => (v.unknown = "typo"),
|
|
]) {
|
|
const v = structuredClone(values);
|
|
change(v);
|
|
await assert.rejects(render(v));
|
|
}
|
|
});
|
|
test("planned drain has an explicit rolling policy and sufficient termination budget", async () => {
|
|
const out = await render(values);
|
|
assert.match(out, /maxUnavailable: 0, maxSurge: 1/);
|
|
assert.match(out, /terminationGracePeriodSeconds: 360/);
|
|
assert.match(out, /--drain-timeout-seconds.*300/);
|
|
assert.match(out, /--health-listen.*127\.0\.0\.1:8081/);
|
|
assert.match(out, /http:\/\/127\.0\.0\.1:8081\/readyz/);
|
|
const v = structuredClone(values);
|
|
v.terminationGracePeriodSeconds = 60;
|
|
await assert.rejects(render(v));
|
|
v.worker = {
|
|
replicas: 2,
|
|
slots: 1,
|
|
drainTimeoutSeconds: 10,
|
|
shutdownTimeoutSeconds: 30,
|
|
};
|
|
assert.match(await render(v), /terminationGracePeriodSeconds: 60/);
|
|
v.worker.drainTimeoutSeconds = 0;
|
|
await assert.rejects(render(v));
|
|
v.worker = { replicas: 0, slots: 1 };
|
|
v.config.recovery_read_only = true;
|
|
assert.match(await render(v), /terminationGracePeriodSeconds: 60/);
|
|
});
|
|
test("operational sampling and deployment identity are optional and bounded", async () => {
|
|
const v = structuredClone(values);
|
|
v.config.telemetry = {
|
|
endpoint: "https://collector.example:4318",
|
|
deployment: "evaluation",
|
|
};
|
|
v.config.operational_metrics = {
|
|
sample_interval_seconds: 10,
|
|
stalled_after_seconds: 300,
|
|
recent_window_seconds: 300,
|
|
};
|
|
const out = await render(v);
|
|
assert.match(out, /operational_metrics/);
|
|
assert.match(out, /evaluation/);
|
|
v.config.operational_metrics.sample_interval_seconds = 0;
|
|
await assert.rejects(render(v));
|
|
v.config.operational_metrics.sample_interval_seconds = 10;
|
|
v.config.telemetry.deployment = "secret@example.com";
|
|
await assert.rejects(render(v));
|
|
});
|
|
test("API S3 credentials and worker model credentials are not shared", async () => {
|
|
const v = structuredClone(values);
|
|
v.secrets.objectStore = "artifact-credential";
|
|
v.secrets.model = "model-credential";
|
|
const out = await render(v),
|
|
api = out.slice(
|
|
out.indexOf("name: test-openworker-api"),
|
|
out.indexOf("name: test-openworker-worker"),
|
|
),
|
|
worker = out.slice(out.indexOf("name: test-openworker-worker"));
|
|
assert.match(api, /artifact-credential/);
|
|
assert.doesNotMatch(api, /model-credential/);
|
|
assert.match(worker, /model-credential/);
|
|
assert.doesNotMatch(worker, /artifact-credential/);
|
|
});
|
|
test("provider connections carry identities and secret references, never inline keys", async () => {
|
|
const v = structuredClone(values);
|
|
v.config.model_provider = "openai";
|
|
v.config.model_connections = [
|
|
{id: "openai", provider: "openai", identity: "deployment-openai-v1", key_env: "OPENAI_API_KEY"},
|
|
{id: "anthropic", provider: "anthropic", identity: "deployment-anthropic-v1", key_env: "ANTHROPIC_API_KEY"},
|
|
];
|
|
assert.match(await render(v), /deployment-anthropic-v1/);
|
|
v.config.model_connections[0].api_key = "synthetic-not-a-real-key";
|
|
await assert.rejects(render(v));
|
|
delete v.config.model_connections[0].api_key;
|
|
v.config.model_connections[1].provider = "unsupported";
|
|
await assert.rejects(render(v));
|
|
});
|
|
test("Task Desk is optional, private, single-replica and has no service token or admin key", async () => {
|
|
const v = structuredClone(values);
|
|
v.api = { replicas: 2, serviceAccountName: "artifact-api" };
|
|
v.taskDesk = {
|
|
enabled: true,
|
|
image: "example/taskdesk@" + digest,
|
|
accessSecret: "desk-access",
|
|
apiCASecret: "api-ca",
|
|
config: {
|
|
port: 54350,
|
|
origin: "http://127.0.0.1:54351",
|
|
api_token_file: "/access/operator",
|
|
access_key_file: "/access/access",
|
|
},
|
|
};
|
|
const out = await render(v);
|
|
assert.match(out, /serviceAccountName: "artifact-api"/);
|
|
assert.equal((out.match(/kind: Deployment/g) || []).length, 3);
|
|
assert.match(out, /http:\/\/127.0.0.1:54350/);
|
|
assert.match(out, /http:\/\/127.0.0.1:54351/);
|
|
assert.match(out, /headers:\{host\}/); // Probe uses configured Host, connects only to loopback.
|
|
assert.doesNotMatch(out, /kind: Ingress|NodePort|LoadBalancer/);
|
|
const app = out.slice(
|
|
out.indexOf(
|
|
"name: test-openworker-taskdesk",
|
|
out.indexOf("kind: Deployment"),
|
|
),
|
|
);
|
|
assert.match(app, /secretName: desk-access/);
|
|
assert.doesNotMatch(app, /secretName: bootstrap/);
|
|
v.taskDesk.config.private_network = true;
|
|
await assert.rejects(render(v));
|
|
});
|
|
test("optional bounded database readiness has CA verification and no database/model password", async () => {
|
|
const v = structuredClone(values);
|
|
v.databaseReadiness = {
|
|
enabled: true,
|
|
host: "database.internal",
|
|
image: "example/pg@" + digest,
|
|
};
|
|
const out = await render(v);
|
|
assert.equal((out.match(/name: database-ready/g) || []).length, 2);
|
|
const init = out.slice(
|
|
out.indexOf("initContainers:"),
|
|
out.indexOf(" containers:", out.indexOf("initContainers:")),
|
|
);
|
|
assert.match(init, /verify-full/);
|
|
assert.doesNotMatch(init, /PGPASSWORD|secretKeyRef|envFrom/);
|
|
v.databaseReadiness.host = "";
|
|
await assert.rejects(render(v));
|
|
});
|
|
test("recovery rendering requires zero workers and no model or gateway credential mount", async () => {
|
|
const v = structuredClone(values);
|
|
v.config.recovery_read_only = true;
|
|
await assert.rejects(render(v));
|
|
v.worker = { replicas: 0, slots: 1 };
|
|
v.secrets.openshell = "";
|
|
const out = await render(v);
|
|
assert.match(out, /"recovery_read_only": true/);
|
|
assert.doesNotMatch(out, /mountPath: \/openshell|name: openshell/);
|
|
v.secrets.model = "must-not-mount";
|
|
await assert.rejects(render(v));
|
|
});
|