diff --git a/console/src/fleetToml.ts b/console/src/fleetToml.ts index 824da9c..dc4a348 100644 --- a/console/src/fleetToml.ts +++ b/console/src/fleetToml.ts @@ -7,7 +7,10 @@ // regex-based (not a full TOML parser) so it's unit-testable and only ever // touches the one array/block it means to. -function quote(s: string): string { +// Exported so fleetsK8sToml.ts (fleets-k8s.toml's client-side edits, same +// `[fleet.]` shape) can reuse it instead of duplicating a one-line +// helper. +export function quote(s: string): string { return JSON.stringify(s); } diff --git a/console/src/fleetsK8sToml.test.ts b/console/src/fleetsK8sToml.test.ts new file mode 100644 index 0000000..8b42b82 --- /dev/null +++ b/console/src/fleetsK8sToml.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect } from "vitest"; +import { appendMember, appendK8sFleetBlock } from "./fleetsK8sToml"; + +describe("appendMember (reused from fleetToml.ts)", () => { + it("works against fleets-k8s.toml's [fleet.] shape too", () => { + const text = `[fleet.orbstack-dev] +context = "orbstack" +namespace = "dev" +members = ["scratch-agent"] +`; + const out = appendMember(text, "orbstack-dev", "scratch-agent-2"); + expect(out).toContain('members = ["scratch-agent", "scratch-agent-2"]'); + expect(out).toContain('context = "orbstack"'); + expect(out).toContain('namespace = "dev"'); + }); +}); + +describe("appendK8sFleetBlock", () => { + it("appends a new [fleet.] block with context, namespace, members, expected_principal", () => { + const out = appendK8sFleetBlock('default_cluster = "oab"\n', { + name: "orbstack-dev", + member: "oab-dev-scratch-agent", + context: "orbstack", + namespace: "dev", + expectedPrincipal: "system:serviceaccount:dev:oab-agent", + }); + expect(out).toContain("[fleet.orbstack-dev]"); + expect(out).toContain('context = "orbstack"'); + expect(out).toContain('namespace = "dev"'); + expect(out).toContain('members = ["oab-dev-scratch-agent"]'); + expect(out).toContain('expected_principal = "system:serviceaccount:dev:oab-agent"'); + }); + + it("omits context and expected_principal when not provided, but always writes namespace", () => { + const out = appendK8sFleetBlock("", { + name: "orca-k8s", + member: "oab-prod-orca", + context: null, + namespace: "prod", + expectedPrincipal: null, + }); + expect(out).not.toContain("context ="); + expect(out).not.toContain("expected_principal ="); + expect(out).toContain('namespace = "prod"'); + }); + + it("separates the new block from existing content with exactly one blank line", () => { + const out = appendK8sFleetBlock('default_cluster = "oab"\n', { + name: "x", + member: "m", + context: null, + namespace: "ns", + expectedPrincipal: null, + }); + expect(out).toBe('default_cluster = "oab"\n\n[fleet.x]\nnamespace = "ns"\nmembers = ["m"]\n'); + }); +}); diff --git a/console/src/fleetsK8sToml.ts b/console/src/fleetsK8sToml.ts new file mode 100644 index 0000000..37552b2 --- /dev/null +++ b/console/src/fleetsK8sToml.ts @@ -0,0 +1,39 @@ +// Pure text-level edits to fleets-k8s.toml's `[fleet.]` blocks — the +// k8s counterpart to fleetToml.ts, same rationale (studio#104: k8s_fleet_ +// config_write has no partial/append primitive, so the client computes the +// new/edited TOML and calls it with the full updated text). +// +// `[fleet.]` block lookup/append-member is identical between fleets. +// toml and fleets-k8s.toml (same table shape, same `members = [...]` array — +// neither `findFleetBlock` nor `appendMember` reference any AWS-specific +// field), so this module reuses fleetToml.ts's `appendMember` rather than +// duplicating it. Only "create a brand-new fleet block" differs, since the +// two files' required/optional fields differ (context+namespace vs +// region+profile). + +import { quote, appendMember } from "./fleetToml"; + +export { appendMember }; + +export interface NewK8sFleetEntry { + name: string; + member: string; + context: string | null; + namespace: string; + expectedPrincipal: string | null; +} + +// Append a brand-new `[fleet.]` block to the end of the file, with the +// one member — the first instance just deployed. `context` and +// `expected_principal` are optional fields, omitted rather than written as +// empty strings (mirrors fleetToml.ts's appendFleetBlock). +export function appendK8sFleetBlock(text: string, entry: NewK8sFleetEntry): string { + const lines = [`[fleet.${entry.name}]`]; + if (entry.context) lines.push(`context = ${quote(entry.context)}`); + lines.push(`namespace = ${quote(entry.namespace)}`); + lines.push(`members = [${quote(entry.member)}]`); + if (entry.expectedPrincipal) lines.push(`expected_principal = ${quote(entry.expectedPrincipal)}`); + const block = `${lines.join("\n")}\n`; + const trimmed = text.replace(/\s*$/, ""); + return trimmed.length ? `${trimmed}\n\n${block}` : block; +}