diff --git a/console/index.html b/console/index.html index f3321b1..6329d00 100644 --- a/console/index.html +++ b/console/index.html @@ -92,17 +92,43 @@ - - +
+ + + +
+
diff --git a/console/src/deploy.ts b/console/src/deploy.ts index 0e181bf..5f3dc0d 100644 --- a/console/src/deploy.ts +++ b/console/src/deploy.ts @@ -37,6 +37,19 @@ function fillOptions(sel: HTMLSelectElement, names: string[], keepNoneFirst: boo sel.innerHTML = opts.join(""); } +// list_k8s_contexts / list_namespaces response shapes (oab-mcp, studio#104) — +// kept minimal (just what this panel reads), not the tools' full contract. +interface K8sContextsResponse { + contexts: { name: string }[]; + current_context: string | null; +} +interface K8sNamespacesResponse { + namespaces: string[]; +} +interface K8sServiceAccountsResponse { + service_accounts: string[]; +} + export type DeployMode = { kind: "new-fleet" } | { kind: "add-instance"; fleetName: string }; // What the panel reports back once a deploy + fleets.toml write both succeed — @@ -75,9 +88,22 @@ export function initDeployPanel(deps: DeployPanelDeps): DeployPanelHandle | null const cancelBtn = document.getElementById("deploy-cancel") as HTMLButtonElement | null; const identityForm = document.getElementById("deploy-identity-form") as HTMLFormElement | null; const nameInput = document.getElementById("deploy-fleet-name") as HTMLInputElement | null; + const providerSel = document.getElementById("deploy-provider") as HTMLSelectElement | null; + const awsFieldsEl = document.getElementById("deploy-aws-fields"); const regionInput = document.getElementById("deploy-region") as HTMLInputElement | null; const profileInput = document.getElementById("deploy-profile") as HTMLInputElement | null; const principalInput = document.getElementById("deploy-principal") as HTMLInputElement | null; + const k8sFieldsEl = document.getElementById("deploy-k8s-fields"); + const k8sContextSel = document.getElementById("deploy-k8s-context") as HTMLSelectElement | null; + // Namespace is a text input with a of what already exists (not a + // + // is the right shape here, no free-text escape hatch needed. + const k8sServiceAccountSel = document.getElementById("deploy-k8s-service-account") as HTMLSelectElement | null; const identityStatusEl = document.getElementById("deploy-identity-status"); const composeSection = document.getElementById("deploy-compose"); const composeHeading = document.getElementById("deploy-compose-heading"); @@ -97,9 +123,16 @@ export function initDeployPanel(deps: DeployPanelDeps): DeployPanelHandle | null !cancelBtn || !identityForm || !nameInput || + !providerSel || + !awsFieldsEl || !regionInput || !profileInput || !principalInput || + !k8sFieldsEl || + !k8sContextSel || + !k8sNamespaceInput || + !k8sNamespaceOptions || + !k8sServiceAccountSel || !composeSection || !tmplSel || !ovlSel || @@ -130,8 +163,98 @@ export function initDeployPanel(deps: DeployPanelDeps): DeployPanelHandle | null setStatus(identityStatusEl, ""); setStatus(previewStatusEl, ""); setStatus(deployStatusEl, ""); + // identityForm.reset() puts (with a manual-entry fallback for a namespace that doesn't exist yet — this can only list what's already there).", + as_map(json!({ + "type": "object", + "properties": { + "context": { "type": "string", "description": "Kubeconfig context name. Omit to use the kubeconfig's current-context." } + } + })), + ), + Tool::new( + "list_service_accounts", + "List service accounts in one namespace of a kubeconfig context. Read-only; backs the New Fleet wizard's optional service-account `. + async fn t_list_namespaces(&self, args: &Map) -> Result { + let context = args.get("context").and_then(Value::as_str); + let namespaces = scp::list_namespaces(context).await?; + Ok(json!({ "namespaces": namespaces })) + } + + /// Service-account discovery (studio#104): backs the New Fleet wizard's + /// optional service-account `` — a manual-entry fallback covers a namespace that +/// doesn't exist yet (this can only list what's already there). +pub async fn list_namespaces(context: Option<&str>) -> anyhow::Result> { + use k8s_openapi::api::core::v1::Namespace; + use kube::api::{Api, ListParams}; + + let client = k8s_client_for(context).await?; + let api: Api = Api::all(client); + let list = api + .list(&ListParams::default()) + .await + .map_err(|e| anyhow::anyhow!("failed to list namespaces: {e}"))?; + Ok(list.items.into_iter().filter_map(|ns| ns.metadata.name).collect()) +} + +/// List service accounts in one namespace of the given kubeconfig context. +/// Backs the New Fleet wizard's (optional) service-account `