Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions console/src/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// after a confirmed successful provision, never before or speculatively.

import type { Source } from "./source";
import { appendMember, appendFleetBlock } from "./fleetToml";
import { appendMember, appendFleetBlock, fleetBlockExists } from "./fleetToml";
import { appendK8sFleetBlock } from "./fleetsK8sToml";

type Invoke = <T>(cmd: string, args?: Record<string, unknown>) => Promise<T>;
Expand Down Expand Up @@ -488,12 +488,35 @@ export function initDeployPanel(deps: DeployPanelDeps): DeployPanelHandle | null

// Step 1 (new-fleet only): collect the fleet identity, then reveal the
// shared Compose step — 7.5.1's "Next: first instance →".
identityForm.addEventListener("submit", (ev) => {
identityForm.addEventListener("submit", async (ev) => {
ev.preventDefault();
if (!nameInput.value.trim()) {
const fleetName = nameInput.value.trim();
if (!fleetName) {
setStatus(identityStatusEl, "fleet name is required", "err");
return;
}
// Reject a colliding name here, before Step 2 provisions anything —
// appendFleetBlock/appendK8sFleetBlock always append a brand-new
// `[fleet.<name>]` block, so reusing an existing name would otherwise
// only surface as a duplicate-key TOML parse error *after* the instance
// was already deployed (it has no partial/merge fallback; "add instance
// to an existing k8s fleet" isn't wired through this wizard yet either —
// see the isK8s check in the deploy submit handler below).
const isK8s = providerSel.value === "k8s";
try {
const current = await (isK8s ? deps.source.k8sFleetConfig() : deps.source.fleetConfig());
if (fleetBlockExists(current.text, fleetName)) {
setStatus(
identityStatusEl,
`a fleet named "${fleetName}" already exists — use "Add instance" on that fleet instead`,
"err",
);
return;
}
} catch (e) {
setStatus(identityStatusEl, `fleet name check unavailable: ${errText(e)}`, "err");
return;
}
identityForm.hidden = true;
composeSection.hidden = false;
if (composeHeading) composeHeading.textContent = "Step 2 — first instance";
Expand Down
22 changes: 21 additions & 1 deletion console/src/fleetToml.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from "vitest";
import { appendMember, appendFleetBlock } from "./fleetToml";
import { appendMember, appendFleetBlock, fleetBlockExists } from "./fleetToml";

describe("appendMember", () => {
const text = `default_cluster = "oab"
Expand Down Expand Up @@ -51,6 +51,26 @@ members = ["oab-default-mira-1"]
});
});

describe("fleetBlockExists", () => {
const text = `default_cluster = "oab"

[fleet.oab-prod-orca]
members = ["oab-default-agent-1"]
`;

it("is true when a [fleet.<name>] block is present", () => {
expect(fleetBlockExists(text, "oab-prod-orca")).toBe(true);
});

it("is false when the name isn't present", () => {
expect(fleetBlockExists(text, "no-such-fleet")).toBe(false);
});

it("is false against an empty file", () => {
expect(fleetBlockExists("", "oab-prod-orca")).toBe(false);
});
});

describe("appendFleetBlock", () => {
it("appends a new [fleet.<name>] block with the given fields", () => {
const out = appendFleetBlock("default_cluster = \"oab\"\n", {
Expand Down
10 changes: 10 additions & 0 deletions console/src/fleetToml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ function findFleetBlock(
return { start, end, headerEnd };
}

// Whether a `[fleet.<name>]` block already exists — used by the "New fleet"
// wizard (deploy.ts) to reject a colliding name *before* provisioning an
// instance, rather than discovering the collision only when appendFleetBlock/
// appendK8sFleetBlock's blind append produces a second `[fleet.<name>]`
// header and the resulting TOML fails to parse (studio: duplicate-key crash
// after the instance was already deployed).
export function fleetBlockExists(text: string, name: string): boolean {
return findFleetBlock(text, name) !== null;
}

// Append `member` to an existing fleet's `members = [...]` array (single-line
// TOML array — the only form fleets.toml is written in today, per the ADR's
// mockups). A no-op if the member is already listed or the fleet isn't found.
Expand Down
Loading