From 8c1eb8e999c1c44a7800e4f2c7f01709f15ea1d7 Mon Sep 17 00:00:00 2001 From: Brett Chien Date: Thu, 27 Aug 2026 21:51:35 +0800 Subject: [PATCH] feat(console): add fleetsK8sToml.ts, the client-side write helper for fleets-k8s.toml (studio#104) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sixth sub-item of #104, stacked on #109. Mirrors fleetToml.ts for fleets-k8s.toml, same rationale: k8s_fleet_config_write (#107) has no partial/append primitive, so the client computes the new/edited TOML text and calls it with the whole file. `findFleetBlock`/`appendMember` are identical between the two files (same `[fleet.]` table shape, same `members = [...]` array, neither references any AWS-specific field) — reused via import from fleetToml.ts rather than duplicated. Exported `quote()` from fleetToml.ts (was a private helper) so both modules share the one implementation. Only `appendK8sFleetBlock` (creating a brand-new fleet block) is new code — required/optional fields differ from AWS's appendFleetBlock: `context` (optional) + `namespace` (required, always written) instead of region+profile, same `expected_principal` (optional) as AWS. **Not wired into deploy.ts's submit flow yet** — the k8s identity form still can't submit (see #108: deploy_provision has no k8s dispatch, architecture question still open in #104). This PR is the write-path helper only, same "build the piece, wire it once its dependency resolves" pattern as #107's k8s_fleet_config_write tool (also not yet called by anything). Verified locally: npm run typecheck clean, npm test 106/106 (4 new tests), npm run build succeeds. Ref: studio#104. --- console/src/fleetToml.ts | 5 ++- console/src/fleetsK8sToml.test.ts | 57 +++++++++++++++++++++++++++++++ console/src/fleetsK8sToml.ts | 39 +++++++++++++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 console/src/fleetsK8sToml.test.ts create mode 100644 console/src/fleetsK8sToml.ts 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; +}