diff --git a/docs/ai-and-plans/PRs/653-local-quickstart-design/description.md b/docs/ai-and-plans/PRs/653-local-quickstart-design/description.md new file mode 100644 index 000000000..a3f5c24b5 --- /dev/null +++ b/docs/ai-and-plans/PRs/653-local-quickstart-design/description.md @@ -0,0 +1,79 @@ +# PR #653: Local DocumentDB Quick Start — Design Decisions + +**Status:** Open +**Branch:** `guanzhou/local-quickstart-design` +**Base:** `main` +**Date:** 2026-06-15 + +## Why this note + +This PR adds a design doc only (no shipping code). Before implementation we +benchmarked the design against a proven, shipping reference — the +**PostgreSQL extension's "Local Docker Server"** flow (`ms-ossdata.vscode-pgsql`, +read at the source level). This note records the **non-obvious decisions**: +where we deliberately diverge from that reference and why, where we match it, +and the deviations that override shared/base behavior inside our own codebase. +It exists so human reviewers and review agents don't have to re-derive the +rationale — or mistake a deliberate deviation for an oversight. + +Full design: [`../../local-quickstart/local-quickstart-v2.md`](../../local-quickstart/local-quickstart-v2.md) +(iteration 2, supersedes iteration 1). Review-resolution map is in v2 §18. + +## Reference: PostgreSQL "Local Docker Server" + +A 3-page webview (Home → Prereqs → Create form) that checks Docker (CLI + +daemon) via VS Code tasks, runs a **required-field** form, creates a container +through `@microsoft/vscode-container-client`, passes credentials via a temp +`--env-file`, waits for `pg_isready` **inside** the container, then saves + +reveals the connection and auto-closes the webview. It is **create-and-connect +only** — no persistent volume, no lifecycle (stop/start/delete), no +labels/adopt. The "Easy Management" welcome copy is marketing; no such +commands exist in its source. + +## Deliberate deviations from the reference (non-obvious — keep) + +| We do | PG does | Why we deviate | +| ----------------------------------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------- | +| **Zero required fields** (generate creds/names) | 3 required fields | First-run friction is the thing to remove; PG even shipped a "password required before start" bug. | +| **Persistent named volume** | No volume (data lost on `rm`) | A local dev DB that silently loses data on container removal is a footgun. | +| **Wire-protocol readiness, 60 s** | `pg_isready` in-container, 5 s | DocumentDB has no in-image readiness CLI we can rely on; PG's 5 s is fragile on a cold `initdb`. | +| **Full lifecycle (7 states; stop/start/delete/logs)** | None (create-only) | The managed-instance tree is our core value; PG only _markets_ lifecycle. | +| **tRPC + FluentUI** | mssql-fork custom RPC | Matches this repo's webview stack (CollectionView/DocumentView); we are not a fork. | +| **Stricter telemetry** (resolved semver only) | sends registry/image/tag | Avoid leaking image/registry identifiers. | +| **Docker labels + adopt** | name-only refuse | Lets us recognize and re-attach containers we created. | + +## Changes folded back into v2 from the PG study + +- **v1.0 create-progress = terminal task + button spinner + auto-close** (the + in-webview multi-step progress card is descoped to v1.1). PG proves the + terminal-task model ships without streaming `docker pull` % into a webview. +- **Adopt `@microsoft/vscode-container-client`** (the runtime layer PG uses; + ships both `DockerClient` and `PodmanClient`) instead of a hand-rolled + abstraction → makes "OCI/podman later" a driver swap, not a rewrite. +- **Port rule:** only auto-fallback the _default_ port; never silently relocate + a port the user typed in Advanced (match PG's intent-respecting behavior). +- **Read the real bound host port from `docker inspect`** before composing the + saved connection string (don't trust the requested port; matters with fallback). +- **Distinguish failed-to-create vs failed-to-start** in error copy (cheap, via inspect). +- **Pre-create duplicate check on both connection name and container name.** + +## Deviations that override shared/base behavior (flag for reviewers) + +These are the easy-to-miss ones — we extend or override shared infrastructure +rather than add isolated code: + +1. **TLS exception folded into the shared new-connection wizard** (gated to + localhost / private hosts) instead of a separate emulator wizard. This + overrides the shared wizard with a conditional step, and the old + `New Local Connection...` entry point is removed. +2. **Canonical port `10260` overrides the shared wizard's hardcoded `10255`** + (`PromptConnectionTypeStep.ts`, `PromptPortStep.ts`). This is a pre-ship fix + that changes existing manual-connection defaults — not Quick-Start-local. +3. **Legacy migration mutates the shared `ConnectionType.Emulators` storage + zone** (moves entries into a `Local Connections (Legacy)` folder; the old + zone is kept read-only for one release before removal as a rollback path). + +## Status + +Design only; no implementation in this PR. Open questions are tracked in +v2 §17; the full reviewer-comment → resolution map is in v2 §18. diff --git a/docs/ai-and-plans/PRs/local-quickstart-poc/description.md b/docs/ai-and-plans/PRs/local-quickstart-poc/description.md new file mode 100644 index 000000000..20fa25251 --- /dev/null +++ b/docs/ai-and-plans/PRs/local-quickstart-poc/description.md @@ -0,0 +1,162 @@ +# Local Quick Start — POC: Design Decisions & Scope + +**Status:** Planning — **reviewed by 5 agents over two rounds; revised to consensus (rev. 3), all five APPROVE, no blocking issues**. No implementation yet. +**Branch:** `feature/local-quickstart/POC` +**Base:** `guanzhou/local-quickstart-design` +**Date:** 2026-06-22 +**Companions:** [`poc-implementation-plan.md`](./poc-implementation-plan.md) (the *how*) · +[`review-and-resolutions.md`](./review-and-resolutions.md) (the 5-agent review + every resolution). +**Folder note:** rename to `-local-quickstart-poc` once a PR is opened (matches the +existing `653-local-quickstart-design` convention). + +## Why this note + +This document records the **decisions and rationale** behind a **proof-of-concept (POC)** +for the Local Quick Start feature. The POC exists to be **demoed**, not shipped. Its job is +to prove the end-to-end value of the full design ([`local-quickstart-v2.md`](../../local-quickstart/local-quickstart-v2.md)) +with the **smallest credible vertical slice**, and to de-risk the parts the design leaves as +"architecture, not an implementation plan." + +It is the companion "why" to the step-by-step "how" in +[`poc-implementation-plan.md`](./poc-implementation-plan.md). Read this first. + +## One-sentence goal (unchanged from the design) + +> From an empty machine-with-Docker to an **open, browsable** local DocumentDB connection, +> in one click, without leaving VS Code. + +## What the POC proves (the demo narrative) + +1. User opens the **DocumentDB Local – Quick Start** entry (tree rocket or command). +2. A **card-based webview** (same design language as the Query Insights tab) shows Docker + readiness and a "what we'll do" summary. +3. User clicks **Start DocumentDB Local**. The real container is pulled, created, started. +4. The webview shows **lightweight staged progress** (Checking → Pulling → Creating → + Starting → Waiting for readiness → Done), driven by a tRPC subscription. +5. A **wire-protocol readiness probe** confirms the DB accepts connections. +6. The connection is **saved and revealed in the Connections view**; the webview auto-closes. +7. User **expands the connection and browses real databases/collections** — using the + extension's existing tree/browse code, end to end. + +If steps 1–7 work live, the POC has proven the design. + +## Scope: what the POC focuses on vs. leaves out + +The split is driven by one question: **does it move the demo?** + +| Area | In POC (focus) | Deferred (left out) | Why | +| ---- | :---: | :---: | ---- | +| Quick Start webview (Review → Progress → Success) | ✅ | | The visual centerpiece of the demo | +| Real container provisioning via `@microsoft/vscode-container-client` | ✅ | | The design-sanctioned runtime; proves it works | +| **Lightweight in-webview staged progress** | ✅ | | See "Key deviation 1" — it's what the manager singled out | +| Wire-protocol readiness probe (180 s, POC) | ✅ | | "Running ≠ ready"; the design's core correctness contract | +| **Inline managed instance** under the Quick Start node + **browse** | ✅ | | The payoff + the design's signature "webview closes, tree takes over" handoff; cheap via `DocumentDBClusterItem` | +| Quick Start tree node + rocket empty state (incl. **fresh-machine** empty state) | ✅ | | The design's entry point | +| Lifecycle actions (Stop / Start / Delete) | ▲ Stretch | | Not needed to prove the provisioning flow | +| Storage persistence across reload + named data volume | ▲ Stretch | | Demo is single-session; volume needs the image data path (OPEN-2) | +| Auto-generated credentials in SecretStorage (masked in all logs) | ✅ | | Zero-friction is the whole point; never leak secrets | +| Legacy emulator migration (§4) | | ✅ | Invisible in a fresh-machine demo | +| TLS-exception wizard step (§7) | | ✅ | Separate slice; POC hardcodes `emulatorConfiguration` | +| Full 7-state machine + complete action matrix (§6) | | ✅ | POC uses a reduced state set | +| Port fallback band (§8.3) | | ✅ | POC pre-checks 10260 and errors clearly if busy (no random band) | +| Container adoption / label-conflict resolution (§10) | | ✅ | POC uses a fixed name + simple message | +| Multi-window coordination / Docker events (§12) | | ✅ | Single-window demo | +| Advanced panel (custom creds/image/seed data) (§5.2, §8.4) | | ✅ | Happy path only | +| Categorized Docker diagnosis (§9, v1.2) | | ✅ | Basic "Docker not ready" message only | +| Telemetry (§14) | | ✅ | Not demo-visible | +| `10255 → 10260` manual-wizard fix (§13.5) | | ✅ | Quick Start uses its own port; unrelated to the POC | + +## Key deliberate deviations (POC vs. the v1.0 shipping design) + +These are intentional. They make the POC a better demo while staying true to the design's +intent. They are **not** proposals to change the shipping plan. + +1. **Include lightweight staged progress (the design's v1.1 "prefer to ship").** + The shipping design makes v1.0 *terminal-first / spinner-only* and pushes lightweight + in-webview progress to v1.1. The POC pulls v1.1's lightweight progress **forward**, because: + (a) a demo must *show* the value, and a silent spinner shows nothing; and (b) this is + precisely the slice the manager (Tomaz) carved out and singled out as worth shipping + (commit `ce0224f8`). We still honor the hard constraint: **no `docker pull` percentage + streaming** — stage-level transitions only. + +2. **OutputChannel transparency is a deliberate POC *compromise*, not parity.** The design's + "terminal-first transparency" runs `docker` as VS Code *terminal tasks*. The repo has **no + VS Code terminal-task integration** (`vscode.Task`/`ShellExecution`) — though it *does* have a + general `Task` service framework (`src/services/taskService/`, which the POC reuses for + lifecycle/cancellation). For the POC we stream the runtime's stdout/stderr to a dedicated + **Output channel**. This is **not equivalent** to the integrated-terminal experience the design + anchors on; it is a cheaper stand-in that still lets a viewer see the **real docker commands and + live output**. The generated password is **masked** in everything written to the channel. Full + terminal-task transparency is a shipping-time follow-up. + +3. **Service-owned instance, rendered inline (no double-appearance).** `QuickStartService` owns the + managed instance; the **DocumentDB Local - Quick Start** tree node renders it **inline** as a + read-only `DocumentDBClusterItem` built from the instance's connection string (with + `emulatorConfiguration = { isEmulator: true, disableEmulatorSecurity: true }`). This buys + TLS-allow-invalid and **full browse for free**, and — because nothing is written to the shared + Emulators storage zone in the Core path — the instance shows up **only** under the Quick Start + node, matching the iteration-2 tree shape (§2/§3.2) and avoiding the duplicated/legacy-zone + appearance the design was redesigned to remove. **Storage persistence is Stretch (WI-8).** + +4. **Ephemeral data, honestly labeled.** The documented `docker run` mounts no volume, and the + image's internal data path is unverified (OPEN-2). The POC therefore runs **ephemeral**, and the + webview's **Data card reads "Ephemeral (POC)"** (never "Persistent"/"Persisted") so the demo UI + does not claim a property the build lacks. A named volume is Stretch (WI-8). + +## Codebase reuse strategy (the leverage points) + +The POC is small **because** it stands on existing infrastructure (all verified in source): + +| Need | Reuse | Path | +| ---- | ----- | ---- | +| Webview panel + tRPC + React | `WebviewControllerBase`, `appRouter`, `WebviewRegistry` | `src/webviews/_integration/*` | +| Webview UI vocabulary | `MetricsRow` / `MetricBase` / `SummaryCard` / `Card`,`Badge`,`Button` | `.../queryInsightsTab/components/*` | +| Streaming progress pattern | subscription generator + `AbortSignal` | `.../queryInsights/queryInsightsEventsRouter.ts` (`streamStage3`) | +| Inline instance + browse (the payoff) | `DocumentDBClusterItem` from a `TreeCluster`; primes `CredentialCache` from the connection string on expand | `src/tree/connections-view/LocalEmulators/LocalEmulatorsItem.ts` (template) | +| Lifecycle / cancellation / state | the `Task` base class (state machine + `AbortSignal` + `updateProgress`) | `src/services/taskService/` | +| Persist a connection (Stretch only) | `ConnectionStorageService.save(ConnectionType.Emulators, …)` + reveal helpers | `src/commands/newLocalConnection/ExecuteStep.ts:177-201` | +| TLS-allow-invalid behavior | `emulatorConfiguration.disableEmulatorSecurity` → `tlsAllowInvalidCertificates` | `src/documentdb/connectToClient.ts` | +| Conn-string composition (already percent-encodes) | `DocumentDBConnectionString` | `src/documentdb/utils/DocumentDBConnectionString.ts` | +| New tree node + refresh | `ConnectionsBranchDataProvider`, `ext.state.notifyChildrenChanged` | `src/tree/connections-view/*` | +| Command + menu registration | `ClustersExtension.activateClustersSupport()` + `package.json` contributes | `src/documentdb/ClustersExtension.ts` | + +## Real-world findings that shape implementation + +From the **official DocumentDB image** (`github.com/microsoft/documentdb` README): + +- **Image:** `ghcr.io/documentdb/documentdb/documentdb-local:latest` +- **Run:** `docker run -dt -p 10260:10260 --name --username --password

` +- **Port `10260`** is the documented default — the design's canonical port is already correct. +- **Connection string:** `mongodb://:

@localhost:10260/?tls=true&tlsAllowInvalidCertificates=true` + → maps cleanly onto `emulatorConfiguration`/TLS-allow-invalid. + +**Tension to flag:** the image takes credentials as **container CLI args** (`--username` / +`--password` after the image name), **not** as environment variables. This means the password +lands on the host `docker run` command line — re-introducing exactly the `ps -ef` / process-audit +exposure the design's §8.2 `--env-file` was meant to avoid (separate from the `docker inspect` +exposure the design already accepts). **POC decision:** use the documented `--username/--password` +args **and mask the password in all Output-channel writes**; record for the shipping design a +two-part open question — *does the gateway accept env-var credentials*, and if not, *how do we +avoid CLI-arg exposure?* + +## Open questions / risks + +1. **Credential transport** — does the image accept env-var credentials, or only the + documented CLI args? (Affects §8.2 hardening. POC uses CLI args.) +2. **Data persistence** — the documented `docker run` mounts no volume; the in-container data + path for a persistent named volume is unknown. POC treats persistence as a **stretch**; + ephemeral data is acceptable for a demo. +3. **Readiness probe reuse** — confirm we can drive a one-shot `ping`/`hello` over the wire + protocol through the existing connect path (`connectToClient.ts` / `ClustersClient`) with + `tlsAllowInvalidCertificates`, in a 60 s retry loop. +4. **Container client ergonomics** — `@microsoft/vscode-container-client@0.5.4` is installable; + confirm the `DockerClient` + command-runner API for pull/create/start/inspect/stop and for + appending post-image args (`--username/--password`). +5. **Double-appearance cosmetic** — the saved Emulators connection may show under both the new + Quick Start node and the existing **DocumentDB Local** node. Acceptable for the POC. + +## Status + +Planning only. The detailed work breakdown, acceptance checks, and demo script live in +[`poc-implementation-plan.md`](./poc-implementation-plan.md). The multi-agent review of this +plan and its resolutions will be recorded in `review-and-resolutions.md`. diff --git a/docs/ai-and-plans/PRs/local-quickstart-poc/poc-implementation-plan.md b/docs/ai-and-plans/PRs/local-quickstart-poc/poc-implementation-plan.md new file mode 100644 index 000000000..feeb6527b --- /dev/null +++ b/docs/ai-and-plans/PRs/local-quickstart-poc/poc-implementation-plan.md @@ -0,0 +1,353 @@ +# Local Quick Start — POC: Implementation Plan + +> Companion to [`description.md`](./description.md) (read that first for the *why*). +> Full design: [`local-quickstart-v2.md`](../../local-quickstart/local-quickstart-v2.md). +> Review history & resolutions: [`review-and-resolutions.md`](./review-and-resolutions.md). +> +> **Audience:** an implementation agent (Opus/Sonnet-class) or a developer. +> **Status:** **Implemented (WI-0…WI-6) and reviewed by 5 agents against the running code — all APPROVE, 12 findings fixed.** See review-and-resolutions.md "Implementation review". +> **Goal of this plan:** a demoable POC, built as small focused work items, grounded in +> the existing codebase so it is *easy to implement*. + +--- + +## 0. How the implementing agent must work (process contract) + +1. **Work item by work item.** Numbered **Work Items (WI-n)**. Do one at a time. +2. **Commit per work item** (e.g. `feat(quickstart): add container runtime wrapper (WI-0)`). +3. **Report status** before/after each WI with what changed and check results. +4. **This plan is the source of truth.** After each WI, tick its checkbox + append a one-line outcome. +5. **Confidence gate.** If confidence in any non-obvious decision is **< 80%**, stop and ask. +6. **Phase gating.** A→D ordered. **Phase A+B+C = the demo.** Phase D is stretch. +7. **PR checklist before declaring a phase done:** `npm run l10n` (if user-facing strings + changed) → `npm run prettier-fix` → `npm run lint` → `npx jest --no-coverage` → + `npm run build`. All must pass. (`npm run build`, never `npm run compile`.) +8. **Two kinds of acceptance checks.** Each WI marks checks as **[unit]** (must pass under + `npx jest`, Docker-free — e.g. credential generation, connection-string composition, + stage-event ordering, password-masking) or **[manual/integration]** (requires a live Docker + daemon — e.g. pull/create/start/inspect). The §0.7 gate runs the **[unit]** checks; + **[manual/integration]** checks are run by hand and reported, never assumed. +9. **Terminology:** "DocumentDB" for the service; "MongoDB API"/"DocumentDB API" for the wire + protocol. Never "MongoDB" alone. All user-facing strings via `vscode.l10n.t()`. +10. **No `any`.** `unknown` + type guards. Explicit return types. `instanceof Error` in catch. + +--- + +## 1. Goal, demo narrative, and non-goals + +### Goal +Prove the Local Quick Start vertical slice end-to-end, live: one click → provisioned local +DocumentDB container → **browsable connection rendered inline under the Quick Start node**. + +### Demo narrative +Rocket/command → webview (readiness + summary) → **Start** → lightweight staged progress → +wire-protocol readiness → webview **auto-closes** → the **DocumentDB Local - Quick Start** node +shows a **Running** instance **inline**; expand it to browse real databases/collections. (Full +script: §6.) + +### Non-goals (explicitly out — see `description.md` scope table) +Legacy migration; TLS-exception wizard; full 7-state machine + complete action matrix; port +**fallback band** (we still detect a busy port and error cleanly); container adoption/label +**conflict** resolution; multi-window coordination/Docker events; Advanced panel; categorized +Docker diagnosis; telemetry; the `10255→10260` manual-wizard fix; init-script seed feature. +**Storage persistence across reloads and a named data volume are Stretch (Phase D).** + +--- + +## 2. Architecture map (where it plugs in — all paths verified against source) + +**New code (POC owns these):** + +``` +src/services/localQuickStart/ + ContainerRuntime.ts # wrapper over @microsoft/vscode-container-client (Docker) + QuickStartService.ts # singleton: orchestration, state, credentials, readiness + quickStartTypes.ts # InstanceState, StageEvent union, InstanceMetadata +src/commands/localQuickStart/ + openLocalQuickStart.ts # command → opens the webview +src/webviews/documentdb/localQuickStart/ + localQuickStartController.ts # extends WebviewControllerBase + localQuickStartRouter.ts # tRPC: getDockerStatus / startQuickStart (sub) / cancel + LocalQuickStart.tsx # React entry (Review → Progress → Success) + components/... # cards reusing query-insights vocabulary +src/tree/connections-view/LocalQuickStart/ + LocalQuickStartItem.ts # "DocumentDB Local - Quick Start" node + rocket empty state + inline instance + QuickStartActionItem.ts # empty-state "Quick Start..." row (opens webview) +``` + +**Existing code to modify (small, surgical):** + +| File | Change | +| ---- | ------ | +| `package.json` | add `@microsoft/vscode-container-client`; command + menu contributions; (empty-state) viewsWelcome option | +| `src/webviews/_integration/appRouter.ts` | mount `localQuickStart` router (import primitives from `./trpc`, not here) | +| `src/webviews/_integration/WebviewRegistry.ts` | register the React component (auto-bundles — no esbuild change) | +| `src/documentdb/ClustersExtension.ts` | register command(s) + create/attach `QuickStartService` | +| `src/tree/connections-view/ConnectionsBranchDataProvider.ts` | render `LocalQuickStartItem` **including the zero-connections empty state** (§WI-6); fix `savedConnections` count | + +**Reuse verbatim (verified reusable):** `WebviewControllerBase`, `useTrpcClient`, +**`MetricsRow`/`MetricBase`/`SummaryCard` (confirmed pure-presentational, NOT coupled to +CollectionView context)**, the subscription-generator pattern from +`queryInsightsEventsRouter.streamStage3` (with its rethrow-in-`catch` / `finally`-cleanup / +abort-listener gotchas), `DocumentDBClusterItem` + the `TreeCluster` +recipe in `LocalEmulatorsItem.ts` (browse works because `DocumentDBClusterItem` primes +`CredentialCache` from the model's `connectionString` on expand), `DocumentDBConnectionString` +(already percent-encodes), `connectToClient.ts` (TLS-allow-invalid), and the **`Task` base +class in `src/services/taskService/`** (see D13). + +> **Correction (from review):** the repo **does** have a task framework +> (`src/services/taskService/`: a `Task` state machine with `AbortSignal`, `updateProgress`, +> telemetry, and a `TaskService` singleton). What it lacks is **VS Code *terminal-task*** +> integration (`vscode.Task`/`ShellExecution`). D2/D13 are worded accordingly. + +--- + +## 3. Confirmed design decisions + +| # | Decision | +| - | -------- | +| D1 | **Runtime = `@microsoft/vscode-container-client` `DockerClient`** (v0.5.4, confirmed installable, not yet a dep). No hand-rolled `docker` strings *as the primary path*. Podman/OCI later = a client swap (§13.8). | +| D2 | **Transparency = a dedicated VS Code OutputChannel** streaming runtime stdout/stderr. **This is a deliberate POC compromise, *not* parity with the design's terminal-task transparency** (the repo has no VS Code terminal-task integration). The demo must still let a viewer see the **real docker commands + live output**. Full terminal-task transparency is a shipping follow-up. | +| D3 | **Progress = lightweight in-webview staged checklist** via a tRPC **subscription** fed by the service-level `StageEvent` sink (D13; template: `streamStage3`). Stage-level only — **no pull-% streaming**. (Pulls design v1.1 forward for the demo; manager-emphasized.) | +| D4 | **Image** = `ghcr.io/documentdb/documentdb/documentdb-local:latest`; **port 10260**; credentials passed as **container args** `--username/--password`; conn-string `mongodb://U:P@localhost:10260/?tls=true&tlsAllowInvalidCertificates=true`. Run detached+tty (`-dt`). | +| D5 | **Service-owned instance, rendered inline.** `QuickStartService` owns the managed instance (state + credentials + connection string). `LocalQuickStartItem` renders it **inline** as a read-only `DocumentDBClusterItem` built from the instance's `connectionString` → browse works for free and **only under the Quick Start node** (no Emulators-zone save in Core → **no double-appearance**, matches §2/§3.2). Storage persistence is **Stretch (WI-8)**. | +| D6 | **Credentials auto-generated** from a URL-safe alphabet `[A-Za-z0-9]`; held in SecretStorage. `DocumentDBConnectionString` already `encodeURIComponent`s them (belt-and-suspenders, §8.1). | +| D7 | **Readiness = wire-protocol `ping`** through the existing connect path with TLS-allow-invalid, retry loop with backoff, **timeout 180 s for the POC** (first cold start generates TLS certs + initializes Postgres; 60 s is too tight). On timeout: keep-waiting / logs / cancel. "Running" only on probe success (§9.1). | +| D8 | **Reduced state set for the POC:** `NotInstalled → Provisioning → Running` (+ `Error`). Stretch adds `Stopped`/`Starting`/`Stopping`. | +| D9 | **Fixed container name** `vscode-documentdb-local` + Docker labels `vscode.documentdb.quickstart=1`, `vscode.documentdb.alias=` (§10.1). **All destructive/inspect ops act on the stored `containerId`, never the name; any name-collision branch verifies the `vscode.documentdb.quickstart` label before touching a container** (§10.1/§13.1). If an unlabeled container owns the name: show a simple message (no adopt flow in POC). | +| D10 | **Entry point = a tree node + a command.** The node `DocumentDB Local - Quick Start` shows a rocket "Quick Start…" empty-state row that opens the webview; **the node renders even with zero saved connections** (WI-6). Command `vscode-documentdb.command.localQuickStart.open` is the palette/fallback launch. | +| D11 | **Bound port from `docker inspect`** (`NetworkSettings.Ports`) is the source of truth for the saved connection string (§8.3), even though the POC requests a fixed 10260. | +| D12 | **Cancel via `AbortSignal`.** `provision()` accepts an `AbortSignal` threaded into every runtime call. **Pull-phase cancel** aborts the pull — **no container exists to remove**. **Create/Start-phase cancel** removes the container *by `containerId`* and releases the port (§5.6 provisioning rows). Lifecycle-transition cancel is out of POC scope. | +| D13 | **`QuickStartService` (singleton) uses the `Task` framework by *composition*, not inheritance** (`src/services/taskService/`). `Task` is single-use (`start()` throws if state ≠ Pending; `delete()` disposes its emitters), so the singleton owns **a fresh internal `Task` per provisioning attempt** — this is what makes **Retry** (WI-4) and re-provision after Delete (WI-7) work. Reuse `Task` for `doWork(signal)` / `stop()` / `AbortSignal`. **Do not** use `TaskProgressReportingService` (numeric 0-100 → a VS Code *notification*, conflicting with D3's in-webview stage model). Stage progress flows through a **service-level `EventEmitter` `StageEvent` sink** that `doWork` pushes into; the tRPC subscription drains it into an async-iterable (the `streamStage3` pattern). The **tree change-event lives on the service**, not the per-attempt `Task` (whose emitters are disposed on `delete()`). | +| D14 | **Never write secrets to the OutputChannel.** All runtime stdout/stderr is **line-buffered** (split on newlines *before* masking, so a chunk boundary can't split the secret) and passed through a `writeMasked()` helper that redacts the generated password (and any connection string containing it) to `***` in every command echo, stdout, and stderr line. Unit-tested. | + +### Still open (resolve at the relevant WI; ask if confidence < 80%) +- **OPEN-1:** credential transport for shipping. The image takes credentials as **CLI args**, + which puts the password on the host `docker run` command line (`ps -ef`) — re-introducing the + exact exposure §8.2's `--env-file` avoids. Shipping question is two-fold: *does the image + accept env-var credentials*, and if not, *how do we avoid CLI-arg exposure*? POC uses CLI args + + D14 masking. **Note:** D14 masks only the *OutputChannel*; the host process-table (`ps -ef`) + exposure genuinely **remains** in the POC and is part of the shipping question, not solved by it. +- **OPEN-2:** persistent volume data path inside the image (needed for WI-8 / honest "persisted"). +- **OPEN-3:** the `DockerClient` call surface — **WI-0 must validate**: pull-with-streaming, + create-with-**post-image args** (`--username/--password` after the image), inspect bound port, + start/stop/remove, list-by-label. **If the client cannot append post-image args, fall back to + a raw `docker` spawn via `src/utils/cp.ts`** (still centralized, still masked per D14). + +--- + +## 4. Work items + +### Phase A — Runtime foundations (no UI) + +- [ ] **WI-0 — Container runtime wrapper + API validation.** + Add `@microsoft/vscode-container-client` to `package.json`. **First, validate OPEN-3** against + the installed package (a throwaway spike is fine); if post-image args aren't supported, switch + `ContainerRuntime` to a raw `docker` spawn via `src/utils/cp.ts`. Then implement + `ContainerRuntime.ts`: `isDockerReady()` (CLI on PATH + daemon reachable), + `isPortFree(10260)` (pre-check → friendly "Port 10260 is in use" instead of raw Docker stderr), + `pullImage(ref, onLine)`, `createContainer(opts)` (name, labels, `10260:10260`, post-image args + `--username/--password`, detached+tty), `startContainer(id)`, **`followLogs(id, onLine)`** + (because `-dt` detaches, the readiness wait must stream container logs explicitly or the + channel goes silent), `inspectContainer(id)` (state + bound host port), `stopContainer(id)`, + `removeContainer(id)`, `listByLabel(label)`. Stream everything to an OutputChannel + **through a single `writeMasked()` helper that redacts the password (D14)**. + - *Acceptance:* **[unit]** `writeMasked()` never emits the password; port-busy maps to the + friendly message. **[manual/integration]** version logs through the wrapper; a hand-created + container is `inspect`ed for its bound port; `followLogs` shows live output. + - *Files:* `package.json`, `ContainerRuntime.ts`, `quickStartTypes.ts`. + +- [ ] **WI-1 — QuickStartService (orchestration + state + readiness + reconciliation).** + `QuickStartService.ts` singleton using the `Task` framework **by composition (D13)** — it owns a + **fresh internal `Task` per provisioning attempt** (so Retry/re-provision work cleanly). Generate + credentials (D6). The `Task`'s `doWork(signal)` runs the steps and **pushes `StageEvent`s into a + service-level `EventEmitter` sink** (`checking → pulling → creating → starting → waiting → + done|error`); the tRPC subscription drains that sink into an async-iterable for the webview (D3). + Hold `InstanceState` + `InstanceMetadata` (`containerId`, alias, boundPort, clusterId, + connectionString). Readiness probe per D7 (reuse the connect path; 180 s; backoff). Cancel per + D12 via the `Task`'s `AbortSignal` (pull-cancel = abort, no removal; create/start-cancel = remove + by `containerId`). Emit a **service-level** tree change event (not on the per-attempt `Task`). + **Activation reconciliation:** on init, `listByLabel('vscode.documentdb.quickstart=1')`; if a + labeled container exists with no in-memory state (e.g. after a window reload), **adopt it** + (rehydrate `containerId`/port/state from `inspect` + SecretStorage so the inline node reappears), + or if its credentials can't be recovered, offer a one-click **Reset** (remove). This prevents an + orphaned container from silently blocking the next `isPortFree(10260)`. + - *Acceptance:* **[unit]** stage-event ordering; credential alphabet; connection-string + composition; cancel-during-pull performs **no** `removeContainer`; a second attempt after a + failed one starts cleanly (fresh `Task`). **[manual/integration]** `provision()` brings the + container up and resolves `Running`; reload → reconciliation re-shows the running instance; + cancel during create removes the container by id. + - *Files:* `QuickStartService.ts`, `quickStartTypes.ts`, `ClustersExtension.ts` (attach). + +### Phase B — The Quick Start webview (demo centerpiece) + +- [ ] **WI-2 — Webview scaffold (controller + router + React + wiring).** + `localQuickStartController.ts` (extends `WebviewControllerBase`, mirrors + `documentsViewController.ts`); **pass `closePanel: () => this.panel.dispose()` into the trpc + context** (use `this.panel.dispose()`, **not** `this.dispose()` — the framework deliberately does + not close the panel from `dispose()` to avoid a circular chain; disposing the panel fires + `onDidDispose → dispose()`, so cleanup still runs). `localQuickStartRouter.ts` + with `getDockerStatus` (query), `startQuickStart` (subscription → yields `StageEvent`s), + `cancelQuickStart` (mutation) — **import `publicProcedure*`/`router` from `./trpc`, not + `appRouter.ts` (circular-import trap)**. Mount under `appRouter`; register the React entry in + `WebviewRegistry`; `openLocalQuickStart.ts` opens it; register the command in `ClustersExtension` + + `package.json`. + - *Acceptance:* **[manual]** the command opens a webview that calls `getDockerStatus` and renders it. + +- [ ] **WI-3 — Review & Start view.** + `LocalQuickStart.tsx` Review state: 4 metric cards (**Docker / Port / Data / Security**) reusing + `MetricsRow`+`MetricBase`; a "What we'll do" `SummaryCard` (image = the official + `ghcr.io/documentdb/...` ref, host, credentials, lifetime). **The Data card reads + "Ephemeral (POC)"** (the POC has no volume — do not claim "Persistent"). **Start** + **Cancel**. + Basic **Docker-not-ready** variant (single message + Retry; no categorized diagnosis; honors + opt-in — never auto-starts Docker). + - *Acceptance:* **[manual]** cards reflect real `getDockerStatus`; Start disabled when not ready; + Data card says Ephemeral. + +- [ ] **WI-4 — Progress + Success (staged progress, D3).** + On **Start**, subscribe to `startQuickStart`; render the **lightweight staged checklist** + (done/active/pending) + elapsed timer; **Start** shows a spinner while running. **Inherit the + subscription gotchas:** rethrow in the router `catch` so `onError` reaches the webview, clean up + in `finally`, manage the abort listener. On failure: inline error + **Retry** (detail in the + Output channel via a **"View Docker output"** link). On success: brief Success card (if it shows + a Data card, it also reads **"Ephemeral (POC)"**) → **auto-close by calling `closePanel` + (which disposes the panel, per WI-2)** → hand off to the tree. + - *Acceptance:* **[manual]** Start runs the real flow with live stage transitions; failure shows + Retry; success auto-closes and the instance appears inline in the tree (WI-5). + +### Phase C — Inline instance + entry (the payoff, design-faithful) + +- [ ] **WI-5 — Inline managed instance + browse.** + After readiness success, compose the connection string (D4) from the **inspected bound port** + (D11) with name **"DocumentDB Local"** (§8 default). `LocalQuickStartItem.getChildren()` returns + a read-only `DocumentDBClusterItem` built from a `TreeCluster` (reuse the + `LocalEmulatorsItem.ts:60-80` recipe) carrying `emulatorConfiguration { isEmulator:true, + disableEmulatorSecurity:true }` and the `connectionString`. Browse works via + `CredentialCache`-from-connection-string (verified). Give the inline row a **static + `description = 'Running · localhost:'`** so the demo's "Running" row exists in Core (the + full state-aware/colored-dot description is WI-7). **Sample data:** programmatically insert one + sample doc (1 db / 1 collection / 1 doc) — a single driver call, *not* the init-script seed + feature. Optional polish **unless** the fresh image exposes no browsable database/collection, in + which case it becomes a **demo-prep requirement** (so the final beat isn't an empty tree). + - *Acceptance:* **[manual]** after success the instance shows **inline** under the Quick Start + node (only there), with a `Running · localhost:10260` description, and **expands to real + databases/collections**; the sample doc is visible. + +- [ ] **WI-6 — "DocumentDB Local - Quick Start" node + rocket + empty state.** + `LocalQuickStartItem.ts` (mirror `LocalEmulatorsItem.ts`): with no managed instance, + `getChildren()` returns a rocket **"Quick Start — Install & try DocumentDB locally"** row (opens + the webview). Wire into `ConnectionsBranchDataProvider`. **Critical: handle the zero-connections + empty state** — `getRootItems()` currently `return null` when there are 0 clusters **and** 0 + emulators (`:111-116`), which renders the *welcome screen* and hides all root nodes. + **Mandate: `LocalQuickStartItem` must render as a root tree item *unconditionally* — independent of + the stored-connection count *and* of whether the instance is persisted** (return it before/instead + of the `null` early-return). **Do not** rely on a `viewsWelcome` button as the fix: because the + Core instance is in-memory (unsaved, D5), a fresh machine stays at 0 *stored* connections **even + after a successful provision**, so a `viewsWelcome` view would render empty and the running inline + instance (WI-5) would be hidden behind the welcome screen — breaking demo §6 step 4. (A welcome + button may be added *in addition*, for the pre-provision state only.) **Also fix** the + `savedConnections = rootItems.length - 2` telemetry (`:61`) so the extra always-present node + doesn't skew the count. + - *Acceptance:* **[manual]** on a **fresh machine (0 connections)** the node + rocket appear and + open the webview; telemetry count is correct. + +### Phase D — Stretch (only if time before the demo) + +- [ ] **WI-7 (stretch) — Minimal lifecycle actions.** + Inline **Stop / Start / Delete Container** actions wired to `QuickStartService`, **all acting on + the stored `containerId` and verifying the quickstart label first (D9)**; state-aware row + description (`Running · localhost:10260`); refresh via `ext.state.notifyChildrenChanged(this.id)`. +- [ ] **WI-8 (stretch) — Persistence + named volume.** + Persist the instance/connection to storage so it survives reload (mirror + `src/commands/newLocalConnection/ExecuteStep.ts:177-201` — note ~15 files share the name + `ExecuteStep.ts`; this is the **`newLocalConnection`** one: build a `ConnectionItem`, call + `ConnectionStorageService.save(...)`, reveal helpers) and mount a named volume + `vscode-documentdb-local-data` (resolve OPEN-2) so the Data card can honestly say "Persistent." + If persisting into the Emulators zone, **filter the managed instance out of `LocalEmulatorsItem` + rendering** to preserve the single-location UX (D5). + +--- + +## 5. Risks & mitigations + +| Risk | Mitigation (in-plan) | +| ---- | -------- | +| **Secret leak to OutputChannel** | D14 `writeMasked()`, unit-tested (WI-0) | +| **`-dt` detach → silent channel during wait** | `followLogs()` streams container logs explicitly (WI-0) | +| **Cold-start readiness > 60 s** | D7: 180 s + backoff + keep-waiting | +| **Cancel orphans a container / crashes on pull-cancel** | D12: `AbortSignal`; pull-cancel removes nothing; create/start-cancel removes by id | +| **`DockerClient` lacks post-image args** | OPEN-3 validated in WI-0; raw-`cp.ts` fallback | +| **Port 10260 busy (no fallback band)** | `isPortFree` pre-check → friendly message (WI-0) | +| **Fresh-machine node hidden by welcome screen** | WI-6 empty-state handling | +| **Image pull slow/blocked on demo network** | Pre-pull in demo prep (§6); already-present image skips the pull stage | +| **Destructive op hits a user's container** | D9: act on `containerId` + verify label | +| **Orphaned container after a VS Code reload blocks the next run** | WI-1 activation reconciliation (adopt-or-reset by label) | +| **`Task` is single-use → Retry / re-provision break; generator-vs-`Task` impedance** | D13 composition: a fresh `Task` per attempt + `EventEmitter`→subscription bridge | +| **Masking misses a secret split across a stream chunk** | D14: line-buffer before `writeMasked()` | + +## 6. Demo script + +**Prep (do before the demo):** Docker running; **run in a VS Code profile *without* the Docker VS +Code extension** (so "no Docker-extension dependency" is *shown*, not just claimed); **pre-pull** +the image; **verify no stale `vscode-documentdb-local` container** (`docker rm -f` if present); +**verify port 10260 is free**; if the fresh image exposes **no** default browsable database/ +collection, ensure the **WI-5 sample-doc seed** runs (so step 5 isn't an empty tree); keep the +command palette (`DocumentDB: Local Quick Start`) ready as a fallback launch. + +1. Fresh VS Code, 0 connections. Connections view shows **DocumentDB Local - Quick Start** with + the **rocket** row (proves the empty-state entry, WI-6). +2. Click the rocket → webview. Point out: **Docker ✅**, **Port 10260**, the **official + `ghcr.io/documentdb/...` image** in "What we'll do", **Data = Ephemeral (POC)**. Mention it + needs **no Docker VS Code extension**. +3. **Start DocumentDB Local** → watch staged progress: Checking ✅ → Pulling → Creating → + Starting → Waiting → Done. (Click **View Docker output** to show the real commands/output.) +4. Webview **auto-closes**; the **Running** instance appears **inline** under the Quick Start node + (the "webview closes, tree takes over" handoff). +5. Expand it → **admin** → a database → a collection (and, if the optional seed ran, open it to + show a document). +6. (Stretch) **Stop** / **Start** the instance live. + +**Three manager-checkpoints to call out:** official ghcr image; **works without the Docker +extension (running in a profile that doesn't have it — shown, not just claimed)**; the auto-close → +inline-tree handoff. + +## 7. Deviation log + +- **WI-1 — `QuickStartService` is a standalone service, not built on the `Task` base class + (deviates from D13).** Rationale: the `Task` base class is single-use (`start()` throws once + it has run; terminal states don't reset) which conflicts with the **Retry**/re-provision + requirement, and its progress model is numeric `0-100` driving a VS Code *notification* — at + odds with D3's in-webview stage checklist. A standalone singleton with a per-attempt + `AbortSignal` + a `vscode.EventEmitter` status sink satisfies every functional requirement the + reviewers raised (cancellation threaded to docker, fresh-per-attempt, no single-use breakage) + with less ceremony. D13 explicitly permits a standalone service; provisioning is an async + generator consumed directly by the tRPC subscription. +- **Windows arg-quoting fix (manual-testing finding).** The container-client's + `ShellStreamCommandRunnerFactory` must be given a **`shellProvider`** (`Cmd` on Windows, `Bash` + elsewhere); without one it drops each argument's quoting and sets `windowsVerbatimArguments` on + Windows, splitting Go-template `--format {{json .}}` args and breaking `info`/`inspect`/`list`. + Added the `@microsoft/vscode-processutils` dependency. `-dt` is achieved with `detached: true` + alone (the client adds `--tty` automatically) — no `customOptions` needed. +- **Sample data uses the image's native `--init-data true`** (decision from reviewing the + DocumentDB source), not a bespoke driver-side seed. This matches §8.4 ("use the image's standard + init-script convention") and loads the rich `sampledb` (users/products/orders/analytics). A + capped, non-fatal `waitForSampleData()` waits for `sampledb` to appear so the browse step + reliably shows data. The old single-document driver seed was removed. + +--- + +## Appendix — equivalent raw Docker (reference; we call this via the client, not as strings) + +``` +docker pull ghcr.io/documentdb/documentdb/documentdb-local:latest +docker run -dt -p 10260:10260 \ + --name vscode-documentdb-local \ + --label vscode.documentdb.quickstart=1 \ + --label vscode.documentdb.alias=vscode-documentdb-local \ + ghcr.io/documentdb/documentdb/documentdb-local:latest \ + --username --password # masked to *** in all logs (D14) +docker logs -f vscode-documentdb-local # stream during readiness wait (D2/-dt) +docker inspect vscode-documentdb-local # NetworkSettings.Ports → bound host port +# connection string (creds percent-encoded by DocumentDBConnectionString): +# mongodb://:

@localhost:10260/?tls=true&tlsAllowInvalidCertificates=true +``` diff --git a/docs/ai-and-plans/PRs/local-quickstart-poc/review-and-resolutions.md b/docs/ai-and-plans/PRs/local-quickstart-poc/review-and-resolutions.md new file mode 100644 index 000000000..35687c1d2 --- /dev/null +++ b/docs/ai-and-plans/PRs/local-quickstart-poc/review-and-resolutions.md @@ -0,0 +1,369 @@ +# Local Quick Start POC — Plan Review & Resolutions + +**Artifact under review:** [`poc-implementation-plan.md`](./poc-implementation-plan.md) + +[`description.md`](./description.md) (rev. 1 → rev. 2). +**Branch:** `feature/local-quickstart/POC` +**Date:** 2026-06-22 + +## What this document is + +The POC plan was reviewed by **5 independent agents**, each on a different model and a distinct +lens, before any code was written. This file records each lens's verdict, every finding, and how +the plan was changed in response (the `> ✅ RESOLVED` notes). It exists so the manager, a +co-worker, or a future agent can see *why the plan is shaped the way it is* and continue without +re-deriving the rationale. + +## Reviewers and verdicts + +| Lens | Agent / model | Initial verdict | +| ---- | ------------- | --------------- | +| Design fidelity vs `local-quickstart-v2.md` | rubber-duck / Opus | APPROVE-WITH-CHANGES | +| Manager (Tomaz) perspective & expectations | rubber-duck / GPT-5.4 | APPROVE-WITH-CHANGES | +| Implementability against the real codebase | rubber-duck / Opus | APPROVE-WITH-CHANGES | +| POC scope & demo effectiveness | rubber-duck / GPT-5.4 | APPROVE-WITH-CHANGES | +| Technical risk & live-demo correctness | rubber-duck / Gemini 3.1 Pro | **NEEDS-WORK** | + +**Convergent signal:** 3 of 5 lenses independently flagged the same top issue — the design's +signature **inline managed instance under the Quick Start node** was wrongly relegated to Stretch. +The risk lens (the lone NEEDS-WORK) surfaced two P0 demo-breakers the others didn't. + +--- + +## Findings & resolutions (severity-sorted) + +### P0 — Credential leak to the OutputChannel *(risk)* +The plan streams runtime stdout/stderr to an OutputChannel **and** passes `--password` as a CLI +arg, which an entrypoint can echo → the plaintext password could persist in the Output tab. +> ✅ **RESOLVED.** Added **D14** ("never write secrets to the OutputChannel") and a `writeMasked()` +> helper in **WI-0** that redacts the password in every command echo / stdout / stderr line, with +> a **[unit]** acceptance check. `description.md` deviation 2 now states the password is masked. + +### P0 — Detached `-dt` makes the OutputChannel silent during the wait *(risk)* +A detached container returns immediately; the stream closes after the container ID, so the channel +shows nothing during the readiness wait → the demo looks frozen. +> ✅ **RESOLVED.** Added `followLogs(id, onLine)` to **WI-0** (stream `docker logs -f` after start); +> noted in **D2** and the §-appendix. Risk table row added. + +### P0 — Fresh-machine empty state hides the entry node *(scope + impl; verified first-hand)* +`ConnectionsBranchDataProvider.getRootItems()` returns `null` when there are 0 clusters **and** 0 +emulators (`:111-116`), which renders the VS Code *welcome screen* and hides **all** root nodes — +so a naively-inserted Quick Start node never appears in the demo's "fresh machine" scenario. +> ✅ **RESOLVED.** **WI-6** now explicitly handles the zero-connections state (render +> `[LocalQuickStartItem]` instead of `null`, or add a `viewsWelcome` button) and its acceptance +> check is "on a fresh machine (0 connections) the node + rocket appear." Demo §6 step 1 calls it out. + +### P1 — Signature inline instance was Stretch *(design + manager + scope — 3-way consensus)* +Iteration 2's headline is "the managed cluster is **inline**… no separate entry" (§2/§3.2). The +plan saved a **separate** connection in the legacy Emulators zone and pushed the inline view to +Stretch — showcasing the exact shape the redesign removed, with possible double-appearance under +the legacy "DocumentDB Local" node. +> ✅ **RESOLVED.** **Promoted to Core.** **D5** rewritten: `QuickStartService` **owns** the +> instance; `LocalQuickStartItem` renders it **inline** as a read-only `DocumentDBClusterItem` +> (browse via `CredentialCache`-from-connection-string, verified by the impl lens). **Nothing is +> written to the Emulators zone in Core → no double-appearance.** Only Stop/Start/Delete stay +> Stretch (WI-7); storage persistence is Stretch (WI-8, which filters the instance out of +> `LocalEmulatorsItem` if it persists there). Scope table + WI-5 updated. + +### P1 — "No Tasks infrastructure" is factually wrong; WI-1 over-scoped *(impl)* +`src/services/taskService/` is a full `Task` framework (state machine, `AbortSignal`, +`updateProgress`, telemetry, `TaskService` singleton). The repo only lacks **VS Code +*terminal-task*** integration. WI-1 rebuilt orchestration/state/cancel from scratch. +> ✅ **RESOLVED.** Added **D13**: build `QuickStartService` on the `Task` base class for +> lifecycle/state/cancellation; do **not** use `TaskProgressReportingService` (its numeric +> notification progress conflicts with the in-webview stage model). Corrected the wording in +> `description.md` deviation 2 ("no VS Code *terminal-task* integration"). WI-1 + §2 updated. + +### P1 — Readiness 60 s too short for a cold start *(risk)* +First run generates TLS certs + initializes a Postgres-backed gateway; 60 s can time out. +> ✅ **RESOLVED.** **D7** bumped to **180 s + backoff + keep-waiting**; risk table updated. + +### P1 — Cancellation lacked `AbortSignal`; pull-cancel "removes a container" is wrong *(risk)* +Without a threaded signal a cancelled provision keeps running and orphans a container; and a pull +creates no container to remove. +> ✅ **RESOLVED.** **D12** rewritten: `provision()` takes an `AbortSignal` threaded into every +> runtime call; **pull-cancel removes nothing**; **create/start-cancel removes by `containerId`**. +> WI-1 acceptance adds a unit check that pull-cancel performs no `removeContainer`. + +### P1 — Webview "Data" card claims Persistent, but the POC is ephemeral *(design + manager)* +WI-3/WI-4 mirrored §5.1/§5.5 cards verbatim ("Persistent volume" / "Persisted") while persistence +is deferred — a UI claim the build can't back. +> ✅ **RESOLVED.** Added **deviation 4** (ephemeral, honestly labeled); **WI-3** now specifies the +> Data card reads **"Ephemeral (POC)"**; volume + "Persistent" is Stretch (WI-8). + +### P1 — Demo script ends at documents, but Core seeds none *(scope)* +> ✅ **RESOLVED.** Demo §6 now ends at **databases/collections** by default; **WI-5** adds an +> **optional** one-document programmatic seed (a single driver call, *not* the init-script feature) +> for a richer ending if time allows. + +### P1 — OutputChannel framed as equivalent to terminal-first *(manager)* +> ✅ **RESOLVED.** **D2** + deviation 2 reworded as a **deliberate compromise, not parity**; the +> demo must still expose the real docker commands/output ("View Docker output", §6 step 3). + +### P2 — Destructive ops keyed on name, not id/label *(design)* +> ✅ **RESOLVED.** **D9**/**D12**/**WI-7** state all stop/remove/inspect act on the stored +> `containerId` and verify the `vscode.documentdb.quickstart` label first (§10.1/§13.1). + +### P2 — `savedConnections` telemetry off-by-one *(impl)* +`getRootItems` computes `rootItems.length - 2`; a third always-present node skews it. +> ✅ **RESOLVED.** **WI-6** includes fixing the count/comment. + +### P2 — Circular-import trap mounting the new router *(impl)* +> ✅ **RESOLVED.** **WI-2** states the router imports tRPC primitives from `./trpc`, not `appRouter`. + +### P2 — Webview auto-close mechanism unspecified *(impl)* +> ✅ **RESOLVED (corrected in rev. 3).** **WI-2** passes `closePanel: () => this.panel.dispose()` +> into the trpc context; **WI-4** calls it on success. *Note:* an earlier draft used +> `this.dispose()`, which the impl re-review correctly flagged as wrong — the framework +> deliberately does **not** close the panel from `dispose()` (circular-chain guard); disposing the +> **panel** fires `onDidDispose → dispose()`, so cleanup still runs. + +### P2 — Saved connection label would be `user@host` *(design)* +> ✅ **RESOLVED.** **WI-5** sets the name to **"DocumentDB Local"** (§8 default). + +### P2 — `DockerClient` post-image-arg support unproven *(risk + impl)* +> ✅ **RESOLVED.** **OPEN-3** + **WI-0** front-load API validation; **fallback to a raw `docker` +> spawn via `src/utils/cp.ts`** (still masked) if the client can't append post-image args. + +### P2 — Port 10260 busy → raw Docker error *(risk)* +> ✅ **RESOLVED.** **WI-0** adds an `isPortFree()` pre-check → friendly "Port 10260 is in use". + +### P2 — Acceptance checks are Docker-dependent, not CI-runnable *(impl)* +> ✅ **RESOLVED.** §0.8 splits checks into **[unit]** (jest gate) vs **[manual/integration]**; +> each WI tags its checks. + +### P2 — Credential-transport open question understated *(design)* +> ✅ **RESOLVED.** **OPEN-1** + the "Tension to flag" note now capture both halves (env support +> *and* avoiding CLI-arg `ps -ef` exposure). + +### P2 — Demo resilience too network-only; dirty-machine failures more likely *(scope + risk)* +> ✅ **RESOLVED.** §6 "Prep" now also: verify no stale `vscode-documentdb-local` container, verify +> 10260 free, keep the command-palette launch as a fallback. + +--- + +## Confirmed-accurate (verified by the implementability lens against source — no change needed) + +- New-webview file set is correct; the `WebviewRegistry` key **auto-bundles** the component (no + esbuild/entry wiring). `revealToForeground()` public, `setupTrpc` protected. +- The `streamStage3` subscription generator is a sound template (with rethrow-in-`catch`, + `finally` cleanup, abort-listener add/remove). +- `MetricsRow` / `MetricBase` / `SummaryCard` are **pure presentational** — not coupled to + CollectionView context — so the "reuse the query-insights vocabulary" claim holds. +- The TLS / encoding / browse chain is exactly as claimed; **browse works after a plain model + build** because `DocumentDBClusterItem` primes `CredentialCache` from the connection string. +- `@microsoft/vscode-container-client` is genuinely absent (dep + lockfile + `node_modules`), so + WI-0 must add it and the API is genuinely unproven — making WI-0's front-loaded validation the + correct first move. + +## Status + +Plan revised to **rev. 3** after **two rounds** of 5-agent review. **Consensus reached: all five +lenses approve, no blocking issues.** The round-2 outcomes and the full rev.-3 change list are +recorded immediately above. Ready to implement starting at WI-0. + +### Re-review outcomes (rev. 2 → rev. 3) + +All five lenses re-reviewed rev. 2. **Result: unanimous approval, no blocking issues.** The prior +NEEDS-WORK (risk) flipped after confirming its P0/P1 fixes. The re-reviews surfaced a focused set of +**non-blocking** refinements, all folded into **rev. 3**: + +| Lens | Round 1 | Round 2 (on rev. 2) | +| ---- | ------- | ------------------- | +| POC scope & demo | APPROVE-WITH-CHANGES | **APPROVE** | +| Manager perspective | APPROVE-WITH-CHANGES | **APPROVE-WITH-CHANGES** — P0/P1 resolved | +| Design fidelity | APPROVE-WITH-CHANGES | **APPROVE-WITH-CHANGES** — all resolved, no blocking | +| Implementability | APPROVE-WITH-CHANGES | **APPROVE-WITH-CHANGES** — `Task` API validated | +| Technical risk | **NEEDS-WORK** | **APPROVE-WITH-CHANGES** — prior blockers resolved | + +**Validated, not just claimed:** the impl lens verified against source that **every `Task`-API +assumption in D13 is real** (`doWork(signal)`, `stop()→abort`, threaded `AbortSignal`, +`updateProgress`, `onDidChangeState/Status`), that the empty-state fix point and telemetry line are +correct, and that the circular-import / registry-auto-bundle facts hold. + +**Rev. 3 changes (from the re-reviews):** + +1. **`closePanel` correction (impl, important).** `this.dispose()` does **not** close the panel + (framework circular-chain guard). → `closePanel: () => this.panel.dispose()` (WI-2, WI-4). +2. **`Task` by composition, not inheritance (impl + risk).** `Task` is single-use (`start()` throws + if state ≠ Pending; `delete()` disposes emitters), so a singleton that inherits it breaks **Retry** + and re-provision. → **D13/WI-1** rewritten: the singleton owns a **fresh `Task` per attempt**; a + service-level `EventEmitter` `StageEvent` sink feeds the tRPC subscription; the tree change-event + lives on the service. This also resolves the async-generator-vs-`doWork` impedance. +3. **Activation reconciliation (risk).** Persistence is Stretch, so a VS Code reload would orphan the + running container and block the next `isPortFree(10260)`. → **WI-1** adds reconcile-on-init + (`listByLabel` → adopt/rehydrate or Reset). +4. **Line-buffered masking (risk).** A secret split across a stdout chunk could evade redaction. → + **D14** masks **after** line-buffering. +5. **WI-6 must mandate the always-render root node (design).** The `viewsWelcome`-button alternative + is incompatible with the in-memory Core instance (a fresh machine stays at 0 *stored* connections + even after provision, hiding the running inline instance behind the welcome screen). → WI-6 now + **mandates** `LocalQuickStartItem` renders unconditionally; the welcome button is at most an + addition for the pre-provision state. +6. **Static "Running" description in Core (design).** The demo promises a `Running · localhost:10260` + row, but the state-aware description was Stretch. → **WI-5** adds a static description in Core. +7. **Honest "Ephemeral" on the Success card (design).** → **WI-4** Data card (if shown) reads + "Ephemeral (POC)". +8. **OPEN-1 crispness (design).** Clarified that D14 masks only the OutputChannel — the `ps -ef` + process-table exposure genuinely remains in the POC. +9. **Explicit "no Docker-extension" demo proof (manager).** → §6 prep runs in a VS Code profile + without the Docker extension, so the checkpoint is *shown*, not just spoken. +10. **Seed fallback for a non-empty final beat (scope).** → §6 prep + WI-5: if the fresh image + exposes no browsable database/collection, the 1-doc seed becomes a prep requirement. +11. **`ExecuteStep.ts` path disambiguation (impl).** → WI-8 names the `newLocalConnection` file + (15 files share the name). + +**Residual (intentionally deferred, logged for shipping):** OPEN-1 (credential transport), +OPEN-2 (volume data path), OPEN-3 (validated in WI-0). No reviewer considers any of these a blocker +for a POC demo. + +**Consensus reached.** The plan is consistent with the design, aligned with the manager's +perspective, and judged implementable. Ready to start at WI-0. + +--- + +## Implementation review (POC code, 2026-06-22) + +After implementing WI-0…WI-6, the working POC was reviewed by **5 more agents** against the +running code (functional correctness · design fidelity · webview/tRPC · tree+browse · secret +masking/robustness). **All five returned APPROVE-WITH-CHANGES — no P0, no NEEDS-WORK.** The +security agent **ran the real image** and confirmed no actual password leak today. The core demo +path (provision → masked output → wire-protocol readiness → inline browse) was verified sound. + +**12 findings, all fixed** (commit `fix(quickstart): address 5-agent POC review`): + +| Sev | Finding | Resolution | +| --- | ------- | ---------- | +| P1 | `followLogs` masked per-chunk, not line-buffered (D14 split-secret gap; design+security) | Route container logs through `MaskingLineBuffer` | +| P1 | `followLogs` leaked on success — `cts` disposed but never cancelled, so `docker logs -f` ran forever (functional) | `cts.cancel()` in `finally` (all outcomes) | +| P1 | Container orphaned if cancelled in the create window (id not yet captured) (functional) | `createAttempted` flag + label-based sweep in `finally` | +| P1 | Re-provision reused a stale `ClustersClient` cached by id (tree) | `ClustersClient.deleteClient(clusterId)` before publishing new creds | +| P1 | Webview could hang in `provisioning` on a busy/empty stream (webview) | `provision()` emits a terminal error when busy + `onComplete` handler recovers to review | +| P2 | Subscription leak on double-click Start (webview) | Unsubscribe-before-resubscribe + null the ref on terminal callbacks | +| P2 | Cancel deferred up to ~30s during an in-flight readiness connect (functional) | Direct `MongoClient` with `serverSelectionTimeoutMS: 3000` | +| P2 | Redundant `-t` in `customOptions` — `detached` already adds `--tty` (functional) | Removed `customOptions` | +| P2 | `savedConnections` telemetry undercounted (tree) | Count real connections/folders by contextValue, excluding synthetic nodes | +| P2 | "Learn more…" row missing from the empty state (design) | Added (opens the DocumentDB repo) | +| P2 | Review screen lacked a Cancel button (design) | Added (closes the panel) | +| P2 | WI-5 sample seed not implemented (design) | Best-effort 1-doc seed after readiness so the tree isn't empty to browse | + +**Verified correct by the reviewers (no change needed):** the cancellation plumbing +(`ctx.signal` → mirror → `provision` → `cts` → tree-kill) and `return()` propagation through the +nested generator; the browse/cache-key path (`CredentialCache.setAuthCredentials` under the same +`clusterId` the tree item uses); no double-appearance (nothing written to the Emulators zone); +webview mount/typing/auto-close/bundle-purity; and split-safe masking on the primary paths. + +**Post-fix gates:** `npm run lint` ✅ · `npx jest --no-coverage` (2055/2055) ✅ · `npm run build` ✅ +· webview webpack bundle ✅. + +--- + +## Manual testing (live on Windows, 2026-06-23) + +Running the POC end-to-end surfaced two issues — one launch-recipe gotcha (not a code bug) and one +genuine Windows bug — both resolved. + +### 1. Blank webview (launch recipe, not a code bug) + +A `webpack-dev` build bakes `DEVSERVER='true'` (via `webpack.config.ext.js` `EnvironmentPlugin`), +so the extension fetches the webview script from the dev server at `http://localhost:18080`. A +one-shot `code --extensionDevelopmentPath=dist` launch does **not** start that dev server → the +webview HTML had no script to load → blank page. + +**How to run a standalone manual test (no dev server, no `Watch` task, no problem-matcher +extension):** + +```powershell +npm run webpack-prod # bakes DEVSERVER='' + IS_BUNDLE='true' → loads dist/views.js from disk +code --extensionDevelopmentPath="\dist" --profile=noExtensionsProfile +``` + +(Or press **F5**, which starts the dev server via the `Watch` task — but that task references the +`amodio.tsl-problem-matcher` extension, absent in `--profile=noExtensionsProfile`, so install it +first: `code --install-extension amodio.tsl-problem-matcher`.) + +### 2. P1 (real bug) — "Docker daemon not reachable" on Windows even when Docker is running + +**Symptom:** `isDockerReady()` reported the daemon unreachable; `docker info` worked fine from a +shell. + +**Root cause:** `ShellStreamCommandRunnerFactory` **without a `shellProvider`** discards each +argument's quoting metadata (`args.map(a => a.value)`) and sets `windowsVerbatimArguments` on +Windows. Go-template arguments like `--format {{json .}}` were therefore split on the space, so +`docker info` (and `inspect`/`list`) failed — breaking readiness and, latently, the whole flow. + +**Fix (commit `fix(quickstart): pass shell provider …`):** provide a platform shell provider — +`Cmd` on Windows, `Bash` elsewhere — to every runner; switch `makeRunner` to `strict:false` +(non-zero exit still rejects, harmless stderr warnings don't). Added the +`@microsoft/vscode-processutils` dependency. + +**Live verification (real Docker on Windows):** a full end-to-end run of the exact provision +sequence passed — Docker readiness (the previously-broken `info`), `runContainer` with credential +args + labels, `inspect` bound port, wire-protocol `ping`, sample seed, browse +(`dbs=[quickstart]`), and cleanup. The official image also ships a default `sampledb`, so the demo's +final browse step is never an empty tree. + +--- + +## v1.0 management layer (2026-06-23) + +After a tier-by-tier gap analysis against the design's §15, the biggest v1.0 gap was the +**managed-instance management surface** (§6.2 action matrix / §11 lifecycle vocabulary) — the POC +could provision and browse but not manage the instance. Implemented: + +- **Lifecycle actions:** Start · Stop · Restart · Delete Container · Copy Connection String · + Copy Password · View Logs (inline icons + context menu, gated by state). +- **State machine completed:** added `Starting` / `Stopping`; `Stopped` is now reachable; each + state renders a distinct row (icon + `Running · localhost:` style description). +- **`Missing` badge (§6.1):** when the extension holds metadata but Docker has no matching + container, the row shows `Missing · click to recreate` (primary action re-opens Quick Start; + Delete clears the stale metadata). +- **Live freshness (§12, partial):** the tree re-checks live Docker state on expand + (`refreshLiveState`), so external/other-window changes (and the Missing case) are reflected + without a full polling subsystem. +- **Safety (§9/§13.1):** every destructive op (`stop`/`start`/`remove`) verifies the + `vscode.documentdb.quickstart` **label** on the container before acting; Delete uses a one-line + modal confirm (§11); View Logs masks the password (D14). + +The instance row carries a Quick-Start-specific `contextValue` +(`treeItem_quickStartInstance` + a `state_*` token) so it shows Quick Start actions instead of the +generic cluster menus, while still browsing via the pre-populated `CredentialCache`. + +**Live verification (real Docker on Windows):** run → stop (`exited`) → start (`running`) → remove, +with the label-ownership check confirmed. Gates: `lint` ✅ · `jest` (2055/2055) ✅ · `build` ✅ · +`webpack-prod` ✅. + +**Still open in v1.0** (readiness polish, not yet implemented): platform-supported check (§9), +port-fallback random band (§8.3), "Start Docker Desktop" action (§5.3/§9), and success-card action +buttons (§5.5, lower value since the webview auto-closes and the tree is now the control surface). +Full multi-window *polling* (§12) remains beyond the on-expand refresh. + +--- + +## Restart-safe sample data — `docker start` bug fix (2026-06-23) + +**Symptom (found in manual test action 5 "Start"):** after a Stop, clicking **Start** showed the +row flip to *Running* in the tree, but the Docker backend showed the container had **exited**. + +**Root cause (confirmed via live container logs):** provisioning baked `--init-data true` into the +container's *run args*. That flag is **not restart-safe** — the image's entrypoint re-runs the +sample-data init on **every** `docker start`, the second run hits +`Duplicate key violation Index '_id_'` on `01-users.js`, the init script exits non-zero, and +`set -e` tears the whole entrypoint (and container) down. A secondary bug: `start()` inspected the +container *immediately* after `docker start`, catching the ~3 s window where it still reports +"running" before it dies — hence the false *Running* badge. + +**Fix:** +- **Drop the baked flag.** `provision()` now runs the container with only + `--username/--password` (restart-safe), and seeds the built-in sample data **once**, after + readiness, by running the image's *native* init script via `docker exec`: + `/home/documentdb/gateway/scripts/init_documentdb_data.sh -H localhost -P 10260 -u … -p … -d /home/documentdb/gateway/sample-data` + (paths verified against `Dockerfile_documentdb_local`). New `ContainerRuntime.execInContainer` + streams + masks the exec output (D14); seeding is best-effort/non-fatal. +- **Confirm-stays-running.** `start()`/`restart()` now poll `confirmStaysRunning` (3 × 1.5 s) and + only declare *Running* if the container is still up after the settle window; otherwise they set + *Error* with a "exited shortly after — check the logs" message instead of a false *Running*. + +**Live verification (real Docker on Windows):** run (no `--init-data`) → `exec` native init → +`sampledb` loaded (users 5 · products 5 · orders 4 · analytics 2) → **stop → start stays +`running`** at +1/5/10/15 s and the **sample data persists** (identical counts). Gates: `build` ✅ · +`lint` ✅ · `jest` (2055/2055) ✅ · `webpack-prod` ✅. diff --git a/docs/ai-and-plans/local-quickstart/decision-instance-model.md b/docs/ai-and-plans/local-quickstart/decision-instance-model.md new file mode 100644 index 000000000..7ffee8fcc --- /dev/null +++ b/docs/ai-and-plans/local-quickstart/decision-instance-model.md @@ -0,0 +1,125 @@ +# Decision: Instance model — single managed instance, ownership-bounded (v1) + +**Status:** Accepted (production v1) · **Date:** 2026-06-25 +**Scope:** Production v1 of Local Quick Start (not the POC). +**Design doc:** [`local-quickstart-v2.md`](./local-quickstart-v2.md) (§10.1 labels, §10.2 +existing-container conflict, §13.10 attach, §15 roadmap, decision log). +**Raised by:** German Eichberger (xgerman) in design review — "manage multiple +containers / versions" and "connect to a retained test container." + +## Questions + +1. Should Quick Start manage **multiple** local DocumentDB containers (e.g. several + instances, or multiple image versions side by side)? +2. If the user already created DocumentDB containers **another way** (CLI, `docker run`, + a test harness), should Quick Start **list / adopt / manage** them? + +## Decision + +For **v1**, Quick Start manages **exactly one** instance and only ever touches +containers **it created**, recognized by the Docker label `vscode.documentdb.quickstart=1` +(§10.1). Concretely: + +| Topic | v1 decision | Deferred to | +| ----- | ----------- | ----------- | +| Multiple **managed** instances | **No.** One managed instance; the rocket entry hides after setup. | v1.2 (§15) | +| Multiple **image versions** side by side | **No.** | v1.2 (§15) | +| Listing the user's **own** (unlabelled) containers inside Quick Start | **No.** They connect via the **regular new-connection wizard** at `localhost:` — "Attach stays first-class" (§13.10). Quick Start does not own them. | — | +| **Adopt-existing-container** flow | **No** as a general feature. The *only* adoption v1 performs is re-recognizing **its own labelled** container after a reload (reconcile). | v1.2 (§15) | +| **Auto-discovery** of unmanaged DocumentDB containers | **No** — and when built, it belongs to the **generic connections** experience, not Quick Start. | v1.2 (§15) | +| **Name / port collision safety** | **Yes — required in v1.** See "What v1 must do" below (sharpens §10.2). | — | + +This ratifies the design doc's existing position (§15: "Single managed instance"; +decision log: "Single instance in v1; labels keep the model forward-compatible; +multi-instance + multi-version are v1.2") and records the reasoning below. + +### Re-affirmed 2026-06-30 (manual-testing review) + +Revisited during hands-on manual testing, framed by user personas, and **held**: + +- **Newbie / trial** and **typical app dev** want *one* decision-free instance; a second + instance only re-introduces the "which one / alias / port" choices Quick Start removes. +- The **advanced "validate before deploying to k8s / on-prem"** persona is the strongest case + *for* multi-version — but their genuine need is met more cheaply and correctly by + **(a) image-tag / version selection on the single managed instance (the Advanced panel, P1-4)** + and **(b) attaching their own side-by-side `docker run` containers via the regular wizard** — + not by Quick Start managing N containers. +- Known papercut accepted for v1: switching versions today = **Delete (loses data) → re-provision**. + P1-4 (pick image tag → recreate) smooths this *without* going multi-instance. + +Net: single-instance stays the v1 model; multi-instance / multi-version remain the additive +v1.2 features the label model already makes free to add. + +## Rationale + +1. **The value proposition is "zero decisions."** Quick Start exists to go from an empty + machine to an open, browsable local DB in one click. Supporting N instances + re-introduces exactly the decisions it removes (which one? alias? port?) and multiplies + port allocation, credential storage, tree shape, reconciliation, and multi-window + coordination by N. Users who genuinely need N custom containers are already well served + by their own `docker run` + the regular wizard. + +2. **Ownership boundary = trust + safety.** The clean, defensible mental model is *Quick + Start only manages containers it created (label-gated).* The moment it lists or acts on + containers it did not create, a stray Stop/Delete can destroy something the user cares + about, and it must guess "is this even DocumentDB? what port? what TLS?" That ambiguity + is a support and trust liability. Recognition is therefore **label-based, never** + name/image/port-based (§10.1). + +3. **Credentials make adoption hollow anyway.** Quick Start auto-generates and stores the + container's credentials. For a hand-run container it cannot know the `--username` / + `--password` the user chose, so it could never populate a working connection. "Listing" + such a container degrades to "here's a thing, go type your own creds" — which **is** the + regular new-connection wizard. So discovery rightly lives in the generic connections + experience, not here. + +4. **Deferring is cheap because the model is already forward-compatible.** Because + recognition is by label (not by the fixed name/port), adding multi-instance or + adopt-existing in v1.2 needs **no data migration** — it is purely additive. That is the + whole reason the design chose labels. + +## What v1 must do (the one concrete work item) + +Even with a single instance, v1 must handle a pre-existing container that holds the planned +name **or** the planned port, without clobbering it (§10.2): + +- **Labelled as ours** (`vscode.documentdb.quickstart=1`) → re-adopt / reconcile it (the + managed instance reappears in the tree). This is *not* general adoption — only our own + container. +- **Unlabelled** (someone else's container holds the name, or the port is taken) → **never + recreate over it.** Validate **both** identifiers up front (the connection/cluster name in + the Connections view **and** the Docker container name), reject with a **clear inline + error**, and point the user to the regular wizard / a port change. Matches the PostgreSQL + reference, which refuses on a duplicate of either identifier. + +This is the only part of this topic that is in-scope for v1 implementation. + +## v1.2 extension shape (recorded so deferral is provably safe) + +- **Discovered (unmanaged) containers:** a **read-only** section populated by `docker ps` + filtered on the DocumentDB image; each row shows name / port / status; the only action is + **Connect**, which opens the regular wizard pre-filled with `localhost:` (user + supplies credentials). No Stop/Delete — these are not owned. +- **Multiple managed instances:** the Quick Start node becomes a parent of N label-tagged + rows, each carrying a unique `vscode.documentdb.alias`; the provision flow gains an + alias + port step; credentials are keyed per-alias in SecretStorage; tree / lifecycle / + reconcile iterate the labelled set instead of taking the first match. + +Both are additive on top of today's label model. + +## Current implementation note (starting point) + +Today's code (POC) is strictly single-instance and assumes the first labelled match: +fixed container name/alias `vscode-documentdb-local`, a singleton cache key +`QUICK_START_CLUSTER_ID = 'quickstart-local-documentdb'`, and +`findManagedContainer()` returns `list[0]`. The unlabelled-collision safety above is **not +yet implemented** and is the concrete v1 hardening item this decision identifies. + +## Consequences + +- **Users:** one-click path stays decision-free; power users attach their own containers via + the regular wizard; nobody's hand-run container is ever modified by Quick Start. +- **Engineering:** v1 surface stays small; the label model makes multi-instance / adopt / + discovery clean v1.2 additions with no migration. +- **Review:** answers German's points with a documented rationale and a forward path + (scheduled to v1.2; labels make it free to add) — see the design-doc decision log. diff --git a/docs/ai-and-plans/local-quickstart/local-quickstart-v2.md b/docs/ai-and-plans/local-quickstart/local-quickstart-v2.md new file mode 100644 index 000000000..2a81ddc01 --- /dev/null +++ b/docs/ai-and-plans/local-quickstart/local-quickstart-v2.md @@ -0,0 +1,902 @@ +# Local Quick Start — Revised Design (Iteration 2) + +> **Supersedes:** [Iteration 1](./local-quickstart.md) — kept as reference +> for original rationale and edge-case analysis. +> +> **What changed:** Simplified tree architecture, removed dedicated local +> connection subtree, moved container creation to a webview, unified TLS +> exception handling into the regular connection wizard. +> +> **Scope:** UX design and architecture. Not an implementation plan. + +--- + +## 1. One-sentence goal + +From an empty machine-with-Docker to an open local DocumentDB connection, +without leaving VS Code. + +--- + +## 2. Key decisions (what changed from iteration 1) + +| Decision | Iteration 1 | Iteration 2 | +| ---------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| Tree root | `DocumentDB Local` with `Quick Start` + `Manual connections` groups | `DocumentDB Local - Quick Start` — container management only | +| Quick Start connection | Separate connection entry in the tree | Cluster is inline — expand the Quick Start node to browse databases | +| User-created localhost connections | Live inside the local subtree | Live alongside regular clusters in the Connections view | +| Emulator templates | Kept ("MongoDB Emulator RU", "DocumentDB Local", "Custom") | Dropped. Regular new-connection wizard only. | +| TLS exception | Gated to emulator wizard | Gated step in regular new-connection wizard (localhost + private IPs + local domains) | +| Container creation UI | Notification-based interstitial | Webview (tRPC router, card-based, same design language as query insights tab) | +| Legacy migration | Not addressed | First launch: existing emulator connections → `Local Connections (Legacy)` folder | +| Container runtime | Docker-specific | Docker-first (v1). OCI/podman follow-up issues tracked separately. | + +--- + +## 3. Tree shape + +### 3.1 Before Quick Start (first activation) + +``` +v Connections + > my-cloud-cluster + > another-cluster + v DocumentDB Local - Quick Start [rocket] + o Quick Start — Install & try DocumentDB locally + o Learn more... +``` + +### 3.2 After a successful Quick Start + +``` +v Connections + > my-cloud-cluster + > another-cluster + > my-manual-localhost (user added this themselves) + v Local Connections (Legacy) (one-time migration, if any existed) + > old-emulator-conn + v DocumentDB Local - Quick Start [rocket] + v DocumentDB Local Running · localhost:10260 + v admin + > mydb +``` + +The managed cluster is **inline** — expanding the Quick Start node is how +users browse databases and collections. No separate connection entry. + +### 3.3 Node glossary + +| Node | Label | Description | Icon | +| ------------------ | ------------------------------------------------ | ------------------------- | -------------------------- | +| Section header | `DocumentDB Local - Quick Start` | n/a | DocumentDB icon | +| Managed instance | `DocumentDB Local` | ` · :` | Colored state dot (see §6) | +| Empty-state action | `Quick Start — Install & try DocumentDB locally` | n/a | Rocket | +| Empty-state link | `Learn more...` | n/a | Link icon | + +The rocket `[rocket]` icon on the section header is the primary entry +point. Hidden once a managed instance exists (v1 is single-instance). + +--- + +## 4. Legacy migration + +On first activation after the update: + +1. Read all connections stored under `ConnectionType.Emulators`. +2. Create a folder named `Local Connections (Legacy)` in the regular + Connections tree. If that name already exists, use the existing + duplicate-suffix logic (e.g., `Local Connections (Legacy) (2)`). +3. Move each emulator connection into that folder as a regular cluster. +4. Preserve credentials, auth config, and `emulatorConfiguration` on + each moved connection. +5. Keep the old `ConnectionType.Emulators` storage zone for one release as + a deprecated, read-only rollback path; remove it in a follow-up release. + Do **not** delete it in the same release that performs the migration, so + a migration bug can never orphan a user's existing local connections. +6. Show a one-time toast: "Your local connections have been moved to + 'Local Connections (Legacy)' in the Connections view." + +The `LocalEmulatorsItem` tree node and the `New Local Connection...` +wizard entry point are removed. + +--- + +## 5. Container creation webview + +When the user clicks **Quick Start**, a webview opens. The webview uses the +existing tRPC/webview infrastructure (router + React + FluentUI). Design +language follows the query insights tab: **card-based layout** with +responsive columns, metric cards, and clear action buttons. + +### 5.1 Webview: Review & Start (happy path — Docker ready) + +``` ++======================================================================+ +| DocumentDB Local - Quick Start [x] | ++======================================================================+ +| | +| ┌────────────────────────────────────────────────────────────────┐ | +| │ 🚀 Start DocumentDB Local │ | +| │ │ | +| │ Get a working local DocumentDB instance in one click. │ | +| │ No terminal commands needed. │ | +| └────────────────────────────────────────────────────────────────┘ | +| | +| ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ | +| │ Docker │ │ Port │ │ Data │ │ Security │ | +| │ ✅ Ready │ │ 10260 │ │ Persistent │ │ TLS local │ | +| │ │ │ │ │ volume │ │ self-sign │ | +| └──────────────┘ └──────────────┘ └──────────────┘ └────────────┘ | +| | +| ┌────────────────────────────────────────────────────────────────┐ | +| │ What we'll do │ | +| │ │ | +| │ Image ghcr.io/documentdb/...:latest │ | +| │ Runs on This machine │ | +| │ Credentials Auto-generated, stored securely │ | +| │ Lifetime Keeps running after VS Code closes │ | +| └────────────────────────────────────────────────────────────────┘ | +| | +| ▸ Advanced | +| | +| [ Start DocumentDB Local ] [ Cancel ] | +| | ++======================================================================+ +``` + +The four **metric cards** at the top (Docker / Port / Data / Security) +follow the same responsive grid pattern as the query insights metrics row: +1 column on narrow views, 2 on medium, 4 on wide. + +The **"What we'll do"** summary is a card with a two-column data grid +(same as the query insights SummaryCard). + +### 5.2 Webview: Advanced panel (expanded) + +``` +| v Advanced | +| ┌────────────────────────────────────────────────────────────────┐ | +| │ Container name [ vscode-documentdb-local ] │ | +| │ Port [ 10260 ] │ | +| │ Data volume Persistent local volume │ | +| │ Credentials (•) Generate strong password │ | +| │ ( ) Use these: │ | +| │ user [ admin ] │ | +| │ pass [ .......................... ] │ | +| │ Image tag [ latest ] │ | +| │ Seed sample data [ ] Load sample documents on start │ | +| └────────────────────────────────────────────────────────────────┘ | +``` + +### 5.3 Webview: Docker not ready + +When a blocking check fails, the metric cards turn into a diagnosis view +instead of the start flow: + +``` ++======================================================================+ +| DocumentDB Local - Quick Start [x] | ++======================================================================+ +| | +| ┌────────────────────────────────────────────────────────────────┐ | +| │ ⚠️ Docker is required │ | +| │ │ | +| │ Local Quick Start needs Docker to run DocumentDB on your │ | +| │ machine. The extension does not install Docker for you. │ | +| └────────────────────────────────────────────────────────────────┘ | +| | +| ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ | +| │ Docker CLI │ │ Docker │ │ Registry │ │ Platform │ | +| │ ✅ Found │ │ daemon │ │ ⚠️ Not │ │ ✅ amd64 │ | +| │ v1.27.0 │ │ ❌ Stopped │ │ reached │ │ │ | +| └──────────────┘ └──────────────┘ └──────────────┘ └────────────┘ | +| | +| ┌────────────────────────────────────────────────────────────────┐ | +| │ How to fix │ | +| │ │ | +| │ • Start Docker Desktop and sign in │ | +| │ • Check your corporate proxy settings │ | +| │ • Test reachability: ghcr.io │ | +| │ │ | +| │ [ Start Docker Desktop ] [ Troubleshooting ] │ | +| └────────────────────────────────────────────────────────────────┘ | +| | +| [ Retry ] [ Cancel ] | +| | ++======================================================================+ +``` + +### 5.4 Webview: Progress + +After the user clicks **Start DocumentDB Local**, the webview transitions +to a progress view. The user can keep working — the webview is not modal +(yet — modal webview API is expected from VS Code; once available we +switch). + +The heavy container work (pull / create / start) runs as **VS Code +terminal tasks**, so the raw `docker` commands and their streaming output +are visible in the integrated terminal. This mirrors the PostgreSQL +"Local Docker Server" reference (see §16): the webview shows friendly step +status while the terminal provides full command transparency. The +webview's **View Docker output** expander surfaces the same stream inline +for users who prefer not to switch to the terminal. + +**v1.0 keeps the webview side minimal** — matching the PostgreSQL reference, +which shows _no_ in-webview progress at all. While the container work runs, +the **Start** button is disabled and shows a spinner, and a failure renders +as a single inline error message with a **Retry** (the terminal carries the +detail). The staged progress list, per-stage percentages, and per-step +inline expansion shown below are the **v1.2** enriched view (§15); the +diagram illustrates that target, not the v1.0 surface. + +``` ++======================================================================+ +| DocumentDB Local - Quick Start [x] | ++======================================================================+ +| | +| ┌────────────────────────────────────────────────────────────────┐ | +| │ Setting up DocumentDB Local... 00:18 │ | +| └────────────────────────────────────────────────────────────────┘ | +| | +| ┌────────────────────────────────────────────────────────────────┐ | +| │ [✅] Checking Docker │ | +| │ [✅] Reserving port 10260 │ | +| │ [🔄] Pulling official image 42% │ | +| │ [ ] Creating container │ | +| │ [ ] Starting container │ | +| │ [ ] Waiting for DocumentDB to accept connections │ | +| └────────────────────────────────────────────────────────────────┘ | +| | +| ┌────────────────────────────────────────────────────────────────┐ | +| │ ▸ View Docker output │ | +| └────────────────────────────────────────────────────────────────┘ | +| | +| [ Cancel ] | +| | ++======================================================================+ +``` + +On failure (v1.2 enriched view), the failed step expands with guidance; in +v1.0 the same guidance is a single inline error message with **Retry** (the +terminal carries the detail). When `docker run` fails, distinguish the cause +via `docker inspect` — if the container exists it is a **start** failure, +otherwise a **create** failure — and word the message accordingly (matching +the PostgreSQL reference): + +``` +| │ [✅] Checking Docker │ | +| │ [✅] Reserving port 10260 │ | +| │ [❌] Pulling official image Failed │ | +| │ We couldn't pull the image from ghcr.io. │ | +| │ Check your network connection or proxy settings. │ | +| │ │ | +| │ [ Retry ] [ Troubleshooting ] │ | +| │ [ ] Creating container │ | +| │ [ ] Starting container │ | +| │ [ ] Waiting for DocumentDB to accept connections │ | +``` + +### 5.5 Webview: Success + +``` ++======================================================================+ +| DocumentDB Local - Quick Start [x] | ++======================================================================+ +| | +| ┌────────────────────────────────────────────────────────────────┐ | +| │ ✅ DocumentDB Local is running on localhost:10260 │ | +| └────────────────────────────────────────────────────────────────┘ | +| | +| ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ | +| │ Status │ │ Endpoint │ │ Image │ │ Data │ | +| │ ✅ Running │ │ localhost │ │ v1.2.3 │ │ Persisted │ | +| │ │ │ :10260 │ │ (latest) │ │ │ | +| └──────────────┘ └──────────────┘ └──────────────┘ └────────────┘ | +| | +| ┌────────────────────────────────────────────────────────────────┐ | +| │ [ Open Connection ] [ Copy Connection String ] │ | +| │ [ Load Sample Data ] [ View Logs ] │ | +| └────────────────────────────────────────────────────────────────┘ | +| | ++======================================================================+ +``` + +On readiness success the webview **auto-closes** and hands off to the +tree, which becomes the persistent control surface (this matches the +prototype: "progress in the terminal, and the webview closes +automatically"). The success card above is shown only briefly. +**Open Connection** expands the managed cluster in the tree (user browses +databases/collections from the Quick Start subtree). + +**Load Sample Data** is rendered only when a seed dataset is available at +ship time (see §8.4); otherwise it appears disabled with a "Coming soon" +tooltip, since it is a v1.2 item (§15). + +### 5.6 Cancel rules + +| Cancelled during | Rollback behavior | +| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| Pull | Abort pull, no container created | +| Create | Remove partially created container | +| Start (first run) | Stop and remove container, release port | +| Waiting for readiness | Stop and remove container, surface last connection error | +| Starting (existing row) | Non-destructive: let the in-flight `docker start` finish and leave the instance Running. Cancel only detaches the UI from the spinner. | +| Stopping (existing row) | Non-destructive: let the in-flight `docker stop` finish; the instance lands in Stopped. Cancel never aborts the stop. | + +The first four rows are the **Provisioning** path, where destructive +rollback is safe because the instance is not yet established. The last two +are the lifecycle **Starting** / **Stopping** transitions of an +already-provisioned instance: there, Cancel is non-destructive — it +detaches the UI from a transition Docker will complete anyway, and never +deletes the container or its data. + +Generated credentials are kept in SecretStorage so a retry reuses them. + +--- + +## 6. Lifecycle states + +``` + NotInstalled ──(Quick Start)──▸ Provisioning ──(success)──▸ Running + │ │ ▲ + │ failure │ │ + ▼ ▼ │ + Error ◂── error ──── Stopping │ + ▲ │ │ + │ ▼ │ + Starting ◂──(start)── Stopped │ + │ │ + └────────(success)───────────┘ +``` + +### 6.1 State presentation + +| State | Icon | Color | Tree description | +| ------------ | ---------------- | ------ | ----------------------------------- | +| NotInstalled | n/a | n/a | (no row — empty state) | +| Provisioning | `loading~spin` | yellow | `Provisioning... · localhost:10260` | +| Starting | `loading~spin` | yellow | `Starting... · localhost:10260` | +| Running | `circle-filled` | green | `Running · localhost:10260` | +| Stopping | `loading~spin` | yellow | `Stopping... · localhost:10260` | +| Stopped | `circle-outline` | gray | `Stopped · localhost:10260` | +| Error | `warning` | red | `Error · click for details` | + +Badges (overlay any state): + +- **`Missing`** — extension has metadata but Docker has no matching + container. Shows `Missing · click to recreate`. Available actions on a + `Missing` instance: **Quick Start** (recreate the container, reusing the + stored credentials and data volume if present) and + **Delete Container...** (clear the stale metadata). No other lifecycle + actions apply. +- **`UpdateAvailable`** _(v1.2)_ — newer image detected. Shows + `Running · localhost:10260 · update available`. + +### 6.2 Action matrix + +| Action | NotInstalled | Provisioning | Running | Stopping | Stopped | Starting | Error | +| ---------------------- | :----------: | :----------: | :-----: | :------: | :-----: | :------: | :---: | +| Quick Start | v | | | | | | | +| Open Connection | | | v | | | | | +| Start | | | | | v | | v | +| Stop | | | v | | | | | +| Cancel | | v | | v | | v | | +| Restart | | | v | | | | v | +| View Logs | | v | v | v | v | v | v | +| Copy Connection String | | | v | | v | | | +| Copy Password | | | v | | v | | | +| Delete Container... | | | | | v | | v | + +Inline icon positions are fixed so buttons don't shift: + +``` +Position 1: primary [open] when Running, blank otherwise +Position 2: power [start] or [stop] or [cancel] +Position 3: overflow [...] +``` + +--- + +## 7. TLS exception in the regular connection wizard + +The emulator-specific `New Local Connection...` wizard is removed. TLS +exception handling moves to the **regular new connection wizard** as a +conditional step. + +### 7.1 Gating rules + +The "Allow invalid TLS certificates" step appears **only** when the +parsed host from the connection string matches any of: + +- Loopback: `localhost`, `127.0.0.0/8`, `::1`, `*.localhost` +- IPv4 private ranges (RFC 1918): + - `10.0.0.0/8` + - `172.16.0.0/12` + - `192.168.0.0/16` +- IPv4 link-local: `169.254.0.0/16` +- IPv6 unique-local and link-local: `fc00::/7`, `fe80::/10` +- Single-word hostnames (no dots): `home`, `devbox`, etc. +- `*.local` mDNS names + +All other hosts: no TLS exception step shown. + +> **Caveat — `.local` and single-word hosts can be corporate infra.** +> Many corporate Active Directory domains use a `.local` suffix (e.g. +> `db.corp.local`), and single-word names can resolve to real internal +> servers via DNS search domains. These are **not** the "self-signed is +> expected" case. The gate therefore only decides whether to **offer** the +> step; the step itself always **defaults to Enable TLS** (§7.2), and its +> copy warns when the host may be a managed internal host rather than the +> developer's own machine. + +### 7.2 Wizard step + +When the gate matches, a new step appears after the connection string / +host prompt: + +``` ++---- TLS Certificate Validation ----+ +| | +| This connection targets a local | +| or private network host. | +| | +| (•) Enable TLS (default) | +| ( ) Allow invalid certificates | +| (self-signed / untrusted CA) | +| | +| [Back] [Continue] | ++-------------------------------------+ +``` + +### 7.3 Future: connection edit dialog + +For hosts outside the gate, the TLS exception will be configurable via a +**connection edit / advanced settings dialog** (to be built as part of this +project — tracked as a separate issue). + +--- + +## 8. Defaults + +| Setting | Default | Notes | +| --------------- | -------------------------------- | ---------------------------------------------------------------- | +| Connection name | `DocumentDB Local` | Readable tree label | +| Container alias | `vscode-documentdb-local` | Visible in `docker ps`, in tooltip | +| Port | `10260` | Canonical port for both Quick Start and manual connections | +| Credentials | Auto-generated username/password | Stored in SecretStorage, passed via `--env-file` (not CLI flags) | +| Data volume | `vscode-documentdb-local-data` | Persistent; survives stop/restart/update | +| Image | Official DocumentDB local image | `ghcr.io/documentdb/...` | +| TLS | Self-signed local certificate | `tlsAllowInvalidCertificates=true` | + +### 8.1 Password generation + +Generated passwords must be safe to embed in a connection string. Apply +**both** defenses (belt-and-suspenders), never just one: + +1. **Generate from a curated safe alphabet** — `[A-Za-z0-9]` plus a small + set of unambiguous, URL-safe symbols. Never emit characters that have + meaning in a URI (`@ : / ? # [ ] %`). +2. **Percent-encode at composition time** — always `encodeURIComponent` + the username and password when building the connection string, even + though step 1 should make this a no-op. Relying on the alphabet alone is + fragile: a future change to the generator, or a user-supplied password + from the Advanced panel, can reintroduce unsafe characters. + +The same encoding rule applies to any user-entered credentials in the +Advanced panel and to the migrated legacy connections (§4). + +### 8.2 Credential transport + +Credentials are passed to the container via a temporary `--env-file` +(written to `os.tmpdir()`, deleted in a `finally` block). This keeps +passwords out of `ps -ef`, shell history, and process audit logs. + +Note: the password is still visible inside the container runtime +environment (`docker inspect`, `docker exec env`) to anyone with Docker +access on the host. This matches the trust boundary the user accepts by +running Docker locally. + +### 8.3 Port fallback + +Fallback applies **only to the default port** (one the user did not choose): + +1. Try `10260`. +2. If busy, try up to 10 random ports in `[10260, 10360)`. +3. If still no free port, show `Change port...` dialog. + +When a fallback is used, the webview shows a yellow banner: + +``` +⚠ Port 10260 is in use. We'll use port 10273 instead. + [ Change port... ] [ Use 10273 ] +``` + +**Explicit ports are never silently relocated.** If the user sets a port in +Advanced and it is busy, surface an error and let them change it — do not +move them to a different port behind their back. (This matches the +PostgreSQL reference, which only auto-allocates when the port field is left +blank or invalid.) + +**Use the actually-bound port, not the requested one.** After the container +starts, read the bound host port back from `docker inspect` +(`NetworkSettings.Ports`) and use _that_ value when composing and saving the +connection string. The requested and bound ports can differ (a race between +the free-port check and the bind), so the inspected value is the source of +truth. + +### 8.4 Container initialization and seed data + +Initialization uses the container image's **standard init-script +convention**, not a bespoke VS Code mechanism. This keeps the behavior +portable (it works identically when the user runs the image by hand) and +testable outside the extension. + +- On create, Quick Start mounts a host directory into the image's + documented init directory. Scripts placed there run once, the first time + the data volume is initialized. +- **Seed sample data** (the Advanced toggle and the success-card button) + simply drops a known, bundled init script into that directory before the + first start. It is therefore the same mechanism as user init scripts, not + a special path. +- **Init-script development.** The Advanced panel lets the user point at a + local scripts folder, which is mounted into the init directory. Editing a + script and resetting the data volume re-runs it, so users can iterate on + their own initialization without leaving VS Code. + +Because init scripts run only on first volume initialization, re-running +them requires a **Reset** (drops the data volume, §11), never a plain +restart. Seed and init scripts must never embed the generated password; +they receive credentials through the same `--env-file` the container uses +(§8.2). + +--- + +## 9. Prereq checks + +Run before showing the Review & Start webview. Results populate the +metric cards. + +| Check | Scope | Pass | Fail | +| ------------------------ | ----- | ------------------------ | -------------------------------- | +| Docker CLI on PATH | v1.0 | ✅ Found (version shown) | ❌ "Install Docker" link | +| Docker daemon reachable | v1.0 | ✅ Ready | ❌ "Start Docker Desktop" action | +| Port available | v1.0 | ✅ Free | ⚠️ Fallback port (see §8.3) | +| Platform supported | v1.0 | ✅ amd64/arm64 | ⚠️ "Use x86_64 emulation?" | +| Image registry reachable | v1.2 | ✅ OK | ⚠️ "Check proxy settings" | + +v1.0 ships the same minimal readiness the PostgreSQL reference ships (CLI +present + daemon reachable + a generic troubleshooting link, §15) plus two +cheap local checks — port-free and platform. The categorized +registry/proxy/Apple-Silicon diagnosis is v1.2. + +Platform check should detect unsupported CPU architectures per +[Azure emulator Docker issue #254](https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/254#issuecomment-4515601488). + +### 9.1 Readiness contract + +Quick Start declares success only when the database accepts connections, +not when the container starts. Readiness is probed by issuing a +`hello`/`ping` command over the wire protocol against +`localhost:`. Timeout: 60 seconds (fixed in v1, setting later). + +On timeout: failure toast with `Wait longer`, `Logs`, `Reset`. + +--- + +## 10. Container recognition and adoption + +### 10.1 Labels + +Quick Start applies these Docker labels at container creation: + +- `vscode.documentdb.quickstart=1` +- `vscode.documentdb.alias=` + +These are the **only** way a container is recognized as a Quick Start +instance. Name, image, or port alone are never sufficient. + +### 10.2 Existing container conflict + +When a container with the planned name already exists: + +Before any image pull or container creation, Quick Start validates **both** +identifiers up front (matching the PostgreSQL reference, which refuses on a +duplicate of either): the **connection/cluster name** in the Connections +view and the **container name** in Docker. A duplicate connection name is +rejected with an inline error before any Docker work; a container-name +collision is resolved as follows. + +**Recognized** (has Quick Start labels): + +``` ++--- Existing Quick Start container found ---+ +| | +| Container vscode-documentdb-local | +| Status Exited 12 days ago | +| | +| (•) Adopt as managed instance | +| ( ) Reset and recreate | +| ( ) Cancel | +| | +| [ Continue ] [ Cancel ] | ++--------------------------------------------+ +``` + +**Unrecognized** (no labels): + +``` ++--- Container name already in use ---+ +| | +| Another container is using the | +| name 'vscode-documentdb-local'. | +| | +| (•) Create a manual connection | +| ( ) Reset and recreate | +| ( ) Cancel | +| | +| [ Continue ] [ Cancel ] | ++-------------------------------------+ +``` + +--- + +## 11. Lifecycle vocabulary + +| Verb | Container | Data volume | Credentials | Tree row | +| ---------------------------- | --------------- | ----------- | ----------- | ------------------------ | +| **Start** | Starts existing | Unchanged | Unchanged | → Running | +| **Stop** | Stops | Unchanged | Unchanged | → Stopped | +| **Restart** | Stop + start | Unchanged | Unchanged | → Running | +| **Delete Container...** | Removed | Kept | Kept | → NotInstalled (Missing) | +| **Update Image...** _(v1.2)_ | Recreated | Kept | Kept | → Running | +| **Move Port...** _(v1.2)_ | Recreated | Kept | Kept | → Running | +| **Reset...** _(v1.2)_ | Removed | **Dropped** | **Dropped** | → NotInstalled | + +Confirmations: + +- Stop / Restart: none (reversible) +- Delete Container: one-line confirm +- Reset: two-step confirm, user must type container alias + +--- + +## 12. Multi-window coordination + +The container is shared machine state. No window "owns" it. + +- v1: **polling only** (on activation, on view refresh, on overflow-menu + open). Docker event subscription deferred to v1.2. +- Destructive actions re-check live state before executing. +- If state changed, show: "The instance is now Stopping (from another + window). Action is no longer available." + +--- + +## 13. Cross-cutting rules + +1. **Opt-in only.** Never install Docker, never start Docker silently, + never modify containers the extension didn't create. +2. **Explicit Docker start.** If Docker is stopped, offer a user-clicked + `Start Docker Desktop` action where supported. +3. **No background pulls.** Image pulled only inside user-initiated flows. +4. **No required form fields** in the happy path. +5. **Canonical port `10260`** for both Quick Start and manual connections. + The manual wizard's hardcoded `10255` (in `PromptConnectionTypeStep.ts` + and `PromptPortStep.ts`) must be fixed before Quick Start ships. +6. **No nag toasts.** Updates and warnings stay in the tree description. +7. **Uninstalling the extension does not remove the container.** +8. **Docker-first, OCI later.** v1 targets Docker. Podman/OCI follow-up + issues are tracked separately. The container runtime is accessed through + **`@microsoft/vscode-container-client`** — the Microsoft-maintained library + the PostgreSQL extension uses, which ships both a `DockerClient` and a + `PodmanClient` behind one interface (with mount/label/platform/port arg + helpers). Adding podman/OCI later is therefore a client swap, not a + rewrite, and we avoid hand-rolling `docker` CLI strings. +9. **No dependency on the Docker VS Code extension.** Quick Start manages + the container itself, in-tree. The Docker extension is optional and + aimed at advanced users; a hard dependency would break the zero-friction + goal and contradicts going beyond Docker. (Reviewer request to evaluate + reuse — decided out of scope.) +10. **Attach stays first-class.** Any locally reachable container — + including a retained test container — can be connected to through the + regular new-connection wizard at its `localhost:`; Quick Start + does not need to own it. Auto-discovery of unmanaged DocumentDB + containers is a v1.2 item. + +--- + +## 14. Telemetry + +``` +quickstart.review_shown source=tree|menu|command|welcome +quickstart.docker_readiness result=ok|cli_missing|daemon_stopped|... +quickstart.start_begin source=... +quickstart.start_stage stage=pull|create|start|connect + duration_ms, success=bool +quickstart.start_end result=success|cancelled|failed + elapsed_ms, port_fallback=bool + image_resolved_version=semver|unknown +quickstart.lifecycle action=start|stop|restart|delete + duration_ms, success=bool +quickstart.error stage=..., reason=... +quickstart.dismiss_welcome from=welcome_view|empty_state +quickstart.report_issue stage=..., from=error_state|readiness_timeout +``` + +Never send: container names, raw image tags, registry URLs, hostnames, +ports, credentials, or image digests. The **resolved semantic version** +(`image_resolved_version`, e.g. `1.2.3`) is the one allowed image +identifier — it is needed to correlate "version X has a bug" reports and is +not user-identifying, unlike a raw tag string or digest. + +--- + +## 15. Scope split + +### v1.0 (must ship) + +- Quick Start webview (review, success, Docker diagnosis); create progress + is terminal-first (spinner + inline error), not an in-webview step list +- Pull / create / start / readiness / connect / reveal +- Tree node with inline cluster (expand to browse) +- Seven lifecycle states + `Missing` badge +- v1.0 actions: Open, Start, Stop, Restart, View Logs, Copy Connection + String, Copy Password, Delete Container +- Single managed instance (rocket hidden after setup) +- Port fallback with random port band +- Credentials via `--env-file` +- Docker labels for recognition +- Legacy migration of existing emulator connections +- TLS exception in regular connection wizard (gated) +- Polling-only multi-window coordination +- Docker readiness: CLI present + daemon reachable + port-free + platform + supported, plus a generic troubleshooting link (categorized + registry/proxy/Apple-Silicon diagnosis is v1.2; see §9) +- Connection edit dialog (needed for TLS exception on non-gated hosts) +- Container initialization via the image's init-script convention (§8.4) + +### v1.1 (prefer to ship) + +- Goal: ship v1.1 with meaningful but lightweight webview progress + visibility +- Lightweight in-webview stage progress notification while create/start is + running (for example: current stage + completed stages + failure stage), + without full per-stage percentages or per-step inline retry controls +- Keep terminal-first transparency: integrated terminal remains the source + of detailed Docker command output +- Maintain v1.0 constraints for simplicity: no `docker pull` percentage + streaming into the webview + +### v1.2 (deferred) + +- Adopt-existing-container flow +- Update Image with version/digest diff +- Move to a different port +- Reset (drop data + credentials) +- In-webview staged progress card (per-stage percentages + per-step inline + retry); v1.1 ships lightweight stage notification and v1.0 remains + terminal-first (§5.4) +- Categorized Docker readiness (Apple Silicon, WSL2, sudo group, + proxy, Windows engine, etc.) +- Docker event subscription (replaces polling) +- Remote VS Code banner (SSH / WSL / dev container) +- Load Sample Data dataset (if not bundled in v1.0) +- Multiple managed instances (and multiple image versions side by side) +- Auto-discovery of unmanaged / retained test containers (§13 rule 10) +- Report Issue action on Error / readiness timeout — pre-filled GitHub + issue with sanitized diagnostics (no creds, hostnames, or ports per §14) +- OCI/podman support +- View Logs + tracing integration + +--- + +## 16. Webview implementation notes + +The Quick Start webview follows existing extension patterns: + +- **tRPC router** — same pattern as CollectionView/DocumentView. + Procedures: `getDockerStatus`, `startQuickStart`, `cancelQuickStart`, + `getInstanceState`, etc. +- **React + FluentUI v9** — card-based layout using the same component + vocabulary as the query insights tab (`Card`, `Text`, `Badge`, + `Button`, responsive grid with CSS grid/flexbox). +- **Design reference**: the query insights tab's `metricsRow` (4 metric + cards in responsive grid), `SummaryCard` (2-column data grid), and + `AnimatedCardList` (step transitions) are the closest starting points. +- **Modal webview** — the VS Code API is expected to support modal + webviews in the future. Once available, the Quick Start webview should + use it. Until then, it opens as a regular editor tab. +- **Flow reference: the PostgreSQL "Local Docker Server" webview.** Its + shipped flow is the closest working model for ours: + - A Docker-branded header and a benefits panel (One-Click creation, + Fully automated setup, Easy management, Code without distractions) + frame the welcome and form steps. + - Prereq checks render as a vertical list of expandable status cards + ("Checking if Docker is installed", "Checking if Docker is running"), + each backed by a real `docker` command. + - The actual `docker` commands execute as **VS Code terminal tasks**, so + their output is visible in the integrated terminal (full transparency) + while the webview shows friendly step status. + - When the server is up, the **webview closes automatically** and the + tree takes over as the control surface. + + Our happy path diverges in one way (§13 rule 4): PostgreSQL's form has + several required fields; ours has **none** — credentials and names are + generated, and everything optional lives under Advanced. + +- **Container runtime client** — use **`@microsoft/vscode-container-client`** + (§13.8) instead of hand-rolling `docker` commands, and run each operation + as a **VS Code terminal task** so the raw commands and output stay visible + in the integrated terminal (the PostgreSQL-proven model). +- **v1.0 progress is terminal-first** — the webview shows a spinner + inline + error (with **Retry**); lightweight stage progress notification is the + v1.1 target, while the staged in-webview progress card (§5.4) remains a + v1.2 enrichment, avoiding `docker pull` percentage streaming into the + webview for v1.0/v1.1. + +--- + +## 17. Open questions + +1. **Persistent volume naming.** Should the user-chosen alias in Advanced + be reflected in the volume name? Pro: discoverable in `docker volume ls`. + Con: renaming breaks the link. **Leaning:** keep a stable, alias-derived + name fixed at creation and never rename it on a later alias change, so + the container↔volume link cannot break. +2. **Self-signed cert trust.** Long term, the image's local CA could be + auto-trusted in Node's trust store. Out of scope for v1. +3. **Welcome card scope.** Show only when `DocumentDB Local - Quick Start` + is empty, or when the entire Connections view is empty? **Leaning:** + scope to the empty Quick Start section, and store the dismissal in a + user **Setting** (e.g. `documentdb.quickStart.welcomeDismissed`) rather + than `globalState`, which is wiped on uninstall/reinstall. + +--- + +## 18. Review resolutions (iteration 2) + +This revision folds in the PR review feedback (notably @xgerman's +comments), the design intent shown in @tnaum-ms's prototype screenshots, +and the gaps surfaced in design review. Each row links a comment to where +it is resolved. + +| Source / finding | Resolution | Section | +| ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | +| OCI / podman / Apple containers (xgerman) | Docker-first for v1; runtime behind a thin abstraction so an OCI driver is a later swap; follow-up issues tracked | §2, §13.8 | +| Platform / CPU not supported (xgerman, Azure emulator #254) | Platform prereq check with the unsupported-CPU reference | §9 | +| URL-encode generated passwords (xgerman) | Belt-and-suspenders: safe alphabet **and** percent-encode at composition; applies to Advanced + migrated creds | §8.1 | +| Check a custom (Advanced) port is free (xgerman) | "Port available" prereq check + fallback band | §8.3, §9 | +| Reuse the Docker VS Code extension? (xgerman) | Out of scope — no hard dependency; in-tree management only | §13.9 | +| View logs + tracing (xgerman) | Deferred to v1.2 | §15 | +| Connect to a retained test container (xgerman) | Attach via the regular wizard at `localhost:`; auto-discovery is v1.2 | §13.10, §15 | +| Manage multiple containers / versions / other DBs (xgerman) | Single instance in v1; labels keep the model forward-compatible; multi-instance + multi-version are v1.2 | §10.1, §15 | +| Container initialization & init-script dev (xgerman) | Use the image's standard init-script convention; Seed = a bundled init script; Advanced can mount a local scripts folder | §8.4 | +| New image available — notify or auto-update? (xgerman) | Notify only via the `UpdateAvailable` badge; never auto-update (no-surprises rule) | §6.1, §11 | +| Help file a DocumentDB issue on failure (xgerman) | Report Issue action (pre-filled, sanitized) on Error / readiness timeout — v1.2 | §14, §15 | +| Progress location / webview lifetime (screenshots, tnaum-ms) | Heavy work runs as terminal tasks (transparency); webview auto-closes on success; tree takes over | §5.4, §5.5, §16 | +| TLS gate over-matched local/private hosts (gap) | Tightened ranges (added IPv6 ULA/link-local, IPv4 link-local, loopback block); `.local`/single-word only _offer_ the step and default to Enable TLS | §7.1, §7.2 | +| Cancel undefined for Starting/Stopping (gap) | Defined as non-destructive for already-provisioned instances | §5.6 | +| Legacy storage deleted in the migration release (gap) | Retain the old zone read-only for one release as a rollback path | §4 | +| `Missing` badge actions undefined (gap) | Specified: Quick Start (recreate) and Delete Container (clear metadata) | §6.1 | +| `Load Sample Data` shown but is v1.2 (gap) | Rendered disabled with "Coming soon" until a dataset ships | §5.5, §15 | +| Telemetry: version-vs-tag tension (gap) | Resolved semantic version is allowed; raw tags and digests are not | §14 | + +### 18.1 PostgreSQL source-benchmark learnings + +The design was benchmarked against the PostgreSQL extension's "Local Docker +Server" flow (`ms-ossdata.vscode-pgsql`, read at the source level). Changes +folded in from that study: + +| Learning from the reference | Change | +| ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| Runs container work as terminal tasks with **no in-webview progress**; webview auto-closes on success | v1.0 create progress is terminal-first (spinner + inline error); lightweight stage notification is v1.1, and the staged card is v1.2 — §5.4, §15, §16 | +| Uses **`@microsoft/vscode-container-client`** (`DockerClient` + `PodmanClient`) | Adopt the same library instead of a hand-rolled abstraction — §13.8, §16 | +| Auto-allocates a port **only when the field is blank/invalid**; never relocates an explicit user port | Fallback applies to the default port only; explicit Advanced ports error instead of moving — §8.3 | +| Reads the bound port from `docker inspect` | Use the inspected bound port when composing the connection string — §8.3 | +| Distinguishes **failed-to-create vs failed-to-start** after a failed run | Same distinction in error copy — §5.4 | +| Checks duplicate **connection name and container name** before side effects | Validate both up front — §10.2 | +| Ships only CLI + daemon prereqs | v1.0 prereqs labeled (CLI/daemon/port/platform); registry/proxy diagnosis is v1.2 — §9, §15 | +| (Kept **better in v2**) | Zero required fields, persistent volume, 60 s wire-protocol readiness, full lifecycle, labels+adopt, stricter telemetry | + +A reviewer-facing decision note for this PR lives at +[`../PRs/653-local-quickstart-design/description.md`](../PRs/653-local-quickstart-design/description.md). diff --git a/docs/ai-and-plans/local-quickstart/local-quickstart.md b/docs/ai-and-plans/local-quickstart/local-quickstart.md new file mode 100644 index 000000000..01da1aabc --- /dev/null +++ b/docs/ai-and-plans/local-quickstart/local-quickstart.md @@ -0,0 +1,1538 @@ +# Local Quick Start — UX Design Reference (Iteration 1) + +> **⚠️ This is Iteration 1.** This document was the initial comprehensive UX +> exploration. It has been superseded by +> **[Iteration 2 — Revised Design](./local-quickstart-v2.md)**, which +> incorporates PR review feedback, simplifies the tree architecture, and +> removes the dedicated local connection subtree. Keep this document as +> reference for the original rationale and edge-case analysis. + +> **What this is:** A user-facing design specification for the proposed +> _Local Quick Start_ feature — "install and try DocumentDB locally from +> inside VS Code". ASCII flows show what the user sees at every step. +> +> **Audience:** Maintainers, reviewers, QA, technical writers. Not an +> implementation plan. +> +> **Scope:** End-to-end user experience and lifecycle. Implementation details +> (process orchestration, Docker SDK calls, container labels, retries) are +> intentionally omitted. +> +> **Related docs:** +> +> - User manual entry that will replace the current +> `docs/user-manual/local-connection-documentdb-local.md` page once shipped. +> - `docs/user-manual/local-connection.md` — manual connection wizard that +> continues to exist side-by-side. + +--- + +## 0. UX references from adjacent database extensions + +### 0.1 Azure Cosmos DB emulator flow + +The closest in-repo-adjacent reference is the Azure Cosmos DB extension's +emulator flow. It is useful mostly as a baseline to improve on: it helps +users **attach** to an emulator, but it does not install, start, stop, or +clean up the emulator for them. + +``` +Azure Cosmos DB extension today + +v Cosmos DB Accounts + v Local Emulators + o New Emulator Connection... + | + v + Select emulator type + | + v + Enter or confirm port + | + v + Save attached emulator connection + | + v + User can browse only if emulator was already installed and running +``` + +Observed UX patterns worth keeping: + +| Pattern from vscode-cosmosdb | Keep / adapt for DocumentDB Local Quick Start | +| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| Dedicated `Local Emulators` tree section | Keep a dedicated `DocumentDB Local` section so local resources are visually separate from cloud connections. | +| "New Emulator Connection..." helper row | Keep a simple helper row, but add a stronger `Quick Start` primary action before manual attach. | +| Preconfigured vs custom connection choices | Keep manual `New Local Connection...` for users who already run their own container. | +| Port is visible and configurable | Keep port visible in the review screen and tree description; never hide fallback ports. | +| Secrets stored outside tree IDs | Keep connection strings and generated passwords out of labels, IDs, telemetry, and logs. | +| Newly attached connection is revealed in the tree | After Quick Start succeeds, expand the `Quick Start` group and focus the managed instance row. | +| Learn-more escape hatch | Keep a `Learn more...` entry, but it must not be the main path. | + +The UX gap this design closes: + +``` +Cosmos DB attach flow: + "I already installed and started the emulator. Help me connect." + +DocumentDB Local Quick Start: + "I do not have anything installed. Give me a safe local DocumentDB I can use now." +``` + +Design implication: Quick Start owns the **local lifecycle** (download image, +create container, start, connect, stop, reset), while the existing manual +connection wizard remains the attach-only path. + +### 0.2 Primary reference: PostgreSQL local Docker server flow + +The PostgreSQL extension already lets users create a local Docker PostgreSQL +server from the extension. Its flow is closer to the DocumentDB Quick Start +goal than the Cosmos DB emulator flow because it owns creation, readiness, +connection save, and reveal: + +``` +PostgreSQL extension local Docker flow + +Create local Docker PostgreSQL server + | + v +Home page: benefits of local Docker server + | + v +Prerequisite checks + [ ] Docker installed + [ ] Docker service running + | + v +Create form + required: connection name, container name, user, password, database + advanced: port, registry, image name, image version, platform + | + v +Run detached container + | + v +Wait for database readiness + | + v +Save connection, connect, reveal in Object Explorer +``` + +Observed UX patterns worth adapting: + +| Pattern from vs-code-postgresql | Keep / adapt for DocumentDB Local Quick Start | +| ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Creates the container and connection in one guided flow | Quick Start owns create -> wait -> connect -> reveal, so the user never needs to run Docker commands or paste a connection string. | +| Starts with value, not Docker mechanics | Welcome / Review copy should say "try DocumentDB locally now"; Docker details stay secondary. | +| Keeps common fields separate from advanced image / port / platform fields | The default DocumentDB path should require **zero form fields**; alias, port, image tag, credentials, platform, and sample data live under Advanced. | +| Defaults most fields before the user acts | Pre-fill alias, port, username, password, image, volume, security mode, and connection name. | +| Validates duplicate connection and container names before launch | Detect conflicts before pull/create so errors are explained before side effects. | +| Allocates a free random port when the user did **not** supply a port | DocumentDB Quick Start goes further: when the default port is busy, also pick a free random port from a small band and show the visible port-fallback banner. (PostgreSQL only allocates a random port when the input is empty/invalid — it does not auto-fall-back from a valid-but-busy port. See sec. 7.4.) | +| Waits for database readiness before connecting | Do not declare success at "container started"; success means DocumentDB accepts connections. PostgreSQL uses `pg_isready` inside the container; DocumentDB has no equivalent baked-in CLI we can rely on, so readiness is defined over the wire protocol (see sec. 4 and sec. 7.2). | +| Saves the connection and reveals it after readiness succeeds | Keep "success means opened/revealed usable connection," not merely "container exists." | + +What **not** to copy literally from PostgreSQL: + +``` +PostgreSQL has: Home page -> Prereq page -> Create form -> Create + +DocumentDB Quick Start should compress this to: + Quick Start click -> Review & Start -> Progress -> Open connection + +No required home page. +No required prerequisite page when Docker is ready. +No required form fields in the default path. +No separate "create connection" step after the container starts. +``` + +### 0.3 Secondary reference: MSSQL local container deployment flow + +The MSSQL extension reinforces a few useful failure and progress patterns, but +its multi-page wizard should **not** become the default DocumentDB Quick Start +shape. + +``` +Use from MSSQL: + - explicit Docker start action if Docker is installed but stopped + - visible provisioning steps for long-running work + - retry at the failed step + - expandable full Docker output + +Do not copy from MSSQL: + - mandatory info page + - mandatory multi-page wizard + - required version/password/profile form before the user can start +``` + +### 0.4 Design updates after comparing all three references + +The final direction is PostgreSQL-inspired, but simpler: + +``` +Cosmos DB teaches: keep local resources visually separate and preserve manual attach. +PostgreSQL teaches: create, connect, and reveal can be one easy managed flow. +MSSQL teaches: long Docker work needs optional details, retry, and full-error text. + +DocumentDB Quick Start should therefore be: + attach-compatible like Cosmos DB, + creation-capable like PostgreSQL, + but with fewer required screens than both PostgreSQL and MSSQL. +``` + +> **Scope note.** This design is a **strict superset** of the PostgreSQL Docker +> creation slice. PostgreSQL only implements create -> wait -> save -> connect -> +> reveal; everything else in this doc (the seven-state lifecycle in sec. 6, the +> categorized Docker readiness diagnosis in sec. 7.1, adopt-existing-container in +> sec. 9.1, multi-window coordination in sec. 9.3, Update Image / Move Port / +> Reset / Forget verbs in sec. 11) is genuinely new work that has no equivalent +> in the PostgreSQL extension. The v1 scope cut in sec. 17.4 makes the v1.0 / +> v1.1 split explicit. + +--- + +## 1. What the user can do + +Local Quick Start gives a developer who has never used DocumentDB a working +local instance and an open Collection View from one entry point, one +one-screen review, and one progress flow — without leaving VS Code and +without touching a terminal. + +The default path is intentionally **not a setup wizard**. Quick Start borrows +PostgreSQL's "create local Docker server and reveal the connection" outcome, +but removes the required form by generating safe defaults. + +Key capabilities: + +- **Install, run, and connect** to an official DocumentDB local container by + clicking _Quick Start_ in the Connections view. +- **Review before action.** A single one-screen interstitial summarizes what + the extension is about to do to the user's machine (image, port, data + persistence, security, lifetime) before anything is downloaded or started. +- **Zero required fields.** Container name, connection name, username, + password, port, image tag, data volume, and TLS behavior all have defaults. + The user opens Advanced only when they want to override them. +- **Manage** the resulting instance directly from the tree — start, stop, + restart, view logs, copy connection string, copy password, update the + image, delete the container, or reset everything. +- **Recover** from common breakage: existing container with the same name, + port already in use, Docker not installed or not running, missing + credentials, multi-window conflicts. +- **Coexist** with the existing _New Local Connection..._ wizard. Users who + prefer to run Docker themselves can still attach a manual local + connection; both paths show up in the same `DocumentDB Local` section + with clear "managed" vs "manual" badges. + +Prerequisite promise: + +- Quick Start installs and starts the **DocumentDB local container image**. +- Quick Start does **not** install Docker. +- If Docker is installed but stopped, Quick Start may offer an explicit + `Start Docker Desktop` / `Start Docker` action on platforms where that can + be done without privilege escalation. It never starts Docker silently. +- If Docker is missing or stopped, the user gets a readiness screen with + next actions instead of a failed mystery operation. + +Local Quick Start is **opt-in**. The extension never installs, starts, or +modifies a container without an explicit user gesture. + +--- + +## 2. Entry-point map + +``` ++------------------ DocumentDB activity bar ------------------+ +| | +| Connections view | +| | | +| v | +| v DocumentDB Local | +| | | +| +-- (empty section) | +| | | "Try DocumentDB locally" welcome card | +| | | [Quick Start] [New Local Connection...] | +| | | [Don't show again] | +| | v | +| | Empty-state child rows: | +| | o Quick Start - Install & try DocumentDB locally | +| | o New Local Connection... | +| | o Learn more... | +| | | +| +-- (populated section) | +| Inline icon on "DocumentDB Local" header: | +| [rocket] Quick Start - Install local instance | +| Right-click menu on header: | +| - Quick Start - Install & try DocumentDB ... | +| - New Local Connection... | +| - Learn more... | +| | +| Command Palette: | +| > DocumentDB: Quick Start - Install Local DocumentDB | +| | +| Walkthrough / Welcome (first activation only): | +| Card "Try DocumentDB locally" | +| [Quick Start] [Open docs] [Skip] | +| | ++-------------------------------------------------------------+ +``` + +All four entry points open the **same Review & Start interstitial** described +in section 4. They differ only in where the user enters from. + +The welcome card and the empty-state copy share a single dismissal flag — +dismissing in one place dismisses both. The flag is stored as a user +**Setting** (`documentdb.quickStart.welcomeDismissed`), not in `globalState`, +so the dismissed state survives an extension uninstall/reinstall and roams +with Settings Sync. Re-installing the extension does **not** revive the +dismissed state. + +--- + +## 3. Tree shape (before and after) + +### 3.1 Before Quick Start exists (today) + +``` +v Connections + v DocumentDB Local + > New Local Connection... +``` + +### 3.2 After Quick Start exists, before user ever ran it + +``` +v Connections + v DocumentDB Local [rocket] [+] + +-- (empty area) + o Quick Start - Install & try DocumentDB locally + o New Local Connection... + o Learn more... +``` + +Inline icons on the `DocumentDB Local` header row, left to right: + +| Icon | Action | Surfaces | +| ---------- | ----------------------- | ------------------------- | +| `[rocket]` | Quick Start | inline + right-click menu | +| `[+]` | New Local Connection... | inline + right-click menu | + +### 3.3 After a successful Quick Start + +``` +v Connections + v DocumentDB Local [rocket] [+] + v Quick Start [group header] + v DocumentDB Local Running . localhost:10260 + | description: Quick Start . official local image . v1.2.3 + | inline: [open] [stop] [...] + v admin + > _vscode_quickstart_seed (optional) + v Manual connections [group header] + > my-laptop Manual . localhost:27017 + > local-dev Manual . same target as Quick Start + > New Local Connection... [+] +``` + +Per-row glossary: + +| Node | Label | Description suffix | Icon | +| ------------------------- | ------------------------- | ----------------------------------------------- | -------------------------------------- | +| Section header | `DocumentDB Local` | n/a | DocumentDB icon | +| Group: Quick Start | `Quick Start` | n/a | `rocket` | +| Group: Manual connections | `Manual connections` | n/a | `plug` | +| Managed instance | `DocumentDB Local` | ` . : [. update available]` | colored disk state icon (see sec. 8.1) | +| Manual connection | user name | `Manual . : [. same target as ...]` | existing connection icon | +| New Local Connection... | `New Local Connection...` | n/a | `plus` | + +The tree row label is the **human connection name** (`DocumentDB Local`), +not the docker container alias. The container alias +(`vscode-documentdb-local`) and the resolved image digest live in the +tooltip (see sec. 8.2). This keeps the tree readable while still letting +the user correlate the row with `docker ps` output. + +The two groups (`Quick Start`, `Manual connections`) appear only when there +is at least one child of either kind. With no children of either kind, the +section falls back to the **empty-state child rows** in section 3.2. + +In v1 a user has **exactly one managed Quick Start instance** at any time +(see sec. 14). The `Quick Start` group exists for forward compatibility +with a future multi-instance flow but always contains a single row in v1. + +### 3.4 Duplicate-target indicator + +When a manual connection points at the same `host:port` as a managed Quick +Start instance, the manual row gets a soft description suffix: + +``` +> local-dev Manual . same target as Quick Start +``` + +The Quick Start row does **not** get a reverse marker — Quick Start is the +authoritative actor for that endpoint. + +--- + +## 4. First-time happy path + +The first time a user clicks Quick Start, they see **one confirmation +screen** and then **one compact progress surface**. There is no required +multi-page wizard in the happy path, but the user can open detailed step +cards when the pull is slow or something fails. + +PostgreSQL's local Docker creation proves the value of `create -> wait -> +connect -> reveal`; DocumentDB keeps that outcome but avoids PostgreSQL's +required create form by pre-filling every setup choice. + +``` +[Quick Start clicked anywhere] + | + v ++--------------------- Review & Start ---------------------+ +| | +| Start DocumentDB Local? | +| | +| Docker Required; start offered if stopped | +| Runs on This machine | +| Image ghcr.io/documentdb/...:latest | +| version shown after pull | +| Port 10260 | +| same default as New Local Connection | +| Data Persistent local volume | +| (kept until you choose "Reset") | +| Security TLS with self-signed local certificate | +| Credentials Auto-generated and stored securely | +| Lifetime Keeps running after VS Code closes | +| Sample data Optional after start | +| | +| [Start DocumentDB Local] [Advanced] [Cancel] | ++----------------------------------------------------------+ + | + v ++----------- Background progress notification -------------+ +| | +| Starting DocumentDB Local... | +| | +| [x] Checking Docker | +| [x] Reserving port 10260 | +| [>] Pulling official image 42% | +| [ ] Creating container | +| [ ] Starting container | +| [ ] Waiting for the database to accept connections | +| | +| [Show Details] [Cancel] | ++----------------------------------------------------------+ + | + v ++------------------ Success notification ------------------+ +| | +| DocumentDB Local is running on localhost:10260. | +| | +| [Open Connection] [Load Sample Data] | +| [Copy Connection String] [Logs] | +| | ++----------------------------------------------------------+ + | + v + Tree refreshes, Quick Start group is expanded, + focus lands on the new managed instance row. +``` + +### 4.1 Easy setup defaults + +These defaults are visible in Review & Start, but they are not prompts. They +exist so the user can start without thinking about Docker, credentials, or +connection-string shape. + +| Setup choice | Default the user sees | Why this keeps setup easy | +| --------------- | ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Connection name | `DocumentDB Local` | The tree row label is understandable immediately. The docker alias `vscode-documentdb-local` is hidden in the tooltip. | +| Container alias | `vscode-documentdb-local` | Stable name for lifecycle actions and adoption; visible in `docker ps` and in the row tooltip, not in the row label. | +| Port | `10260` | Matches DocumentDB Local documentation **and** the `documentDB.local.port` setting used by the manual wizard. Both paths must agree (sec. 13). | +| Credentials | Generated username/password | No password decision before first run. | +| Secret storage | Store generated password in SecretStorage; pass to the container via `--env-file` (temp file, deleted after run) | The password is **not on the host shell command line** (so it does not appear in `ps -ef`, shell history, or process-audit logs). It IS in the container's runtime environment and remains visible via `docker inspect` / `docker exec env` to anyone with Docker access on the host — this matches the security boundary the user already accepts by running Docker locally. Copy password is available later if needed. | +| Data volume | Persistent local volume | Data survives stop/restart/update by default. | +| Image | Official DocumentDB local image | No registry/image decision in the happy path. | +| TLS | Local self-signed setup | Works for local development without cert setup. | +| Sample data | Offered after start (may be deferred in v1; see sec. 14) | Keeps the first run fast and still discoverable. | + +**Readiness contract.** Quick Start declares success only when the database +accepts connections, not when the container starts. Readiness is probed by +issuing a `hello` (or `ping`) command over the wire protocol against +`localhost:` using the generated credentials. The v1.0 timeout is a +fixed **60 seconds** (not user-configurable); a future v1.x release may +expose it as a setting. On timeout the user sees a non-modal failure +toast offering `Wait longer`, `Logs`, and `Reset`. This is the DocumentDB +equivalent of PostgreSQL's `pg_isready` probe; we cannot rely on a +baked-in CLI inside the image. + +If the user clicks `[Open Connection]`, the standard Collection View opens. +If the database is empty, the Collection View shows a first-run callout: + +``` ++-------------------- Empty local database --------------------+ +| | +| DocumentDB Local is ready. | +| | +| Create your first database, or load sample documents to | +| try queries immediately. | +| | +| [Load Sample Data] [Create Database] [Learn More] | +| | ++-------------------------------------------------------------+ +``` + +The "first delightful query" (sample data) is **not** auto-loaded in v1, +but it is one action away from the success card and empty Collection View. + +If the user clicks `[Cancel]` from the progress notification, the extension +rolls back: the container is removed and the port reservation is released. +Generated credentials are kept in storage so a retry can reuse them or +discard them at the user's choice. + +### 4.2 The Advanced panel + +`[Advanced]` opens a single inline expansion on the Review screen — not a +new dialog — so the user never loses the original review context. + +``` ++--------------------- Review & Start ---------------------+ +| | +| ... summary unchanged ... | +| | +| v Advanced | +| Container name [vscode-documentdb-local ] | +| Port [10260 ] | +| Data volume Persistent local volume | +| Credentials (*) Generate strong password | +| ( ) Use these: | +| user [admin ] | +| pass [..........] [show] | +| Image tag [latest ] | +| Seed sample data [ ] Load sample documents on | +| first start | +| | +| [Start DocumentDB Local] [Advanced ^] [Cancel] | ++----------------------------------------------------------+ +``` + +The Advanced panel is sticky per workspace — if the user opens it once, the +next Review screen opens with it expanded. + +Ephemeral data volumes are out of scope for v1. The only v1 data mode is a +persistent local volume that survives Stop, Restart, Update Image, Move Port, +and Delete Container, and is removed only by Reset. + +### 4.3 Remote-session review banner + +When VS Code is connected to SSH, WSL, a dev container, or another remote +extension host, "local" means local to that remote context. The Review screen +adds a banner before the user starts: + +``` +! This will run DocumentDB Local on: ssh://devbox-01 + It will not run on your laptop unless Docker is available in this remote + environment. + + [Start on devbox-01] [Cancel] +``` + +The tree row continues to show the reachable endpoint from the extension's +point of view, but the tooltip includes the remote context. + +--- + +## 5. Subsequent starts (true one-click after setup) + +Once a managed instance exists (even if currently stopped), the entry +points change behavior: + +- The `[Quick Start]` rocket icon on the section header is **hidden** in + v1; with a single managed instance, the rocket would otherwise either + re-trigger the Review screen or silently restart something the user + did not click on. Start is initiated from the row (inline `[start]` + icon, right-click `Start`) or from the command palette + (`DocumentDB: Start Local DocumentDB`, `DocumentDB: Stop Local DocumentDB`). +- Right-click on the managed row offers `Start` / `Stop` / `Restart`. +- Command palette gains `DocumentDB: Start Local DocumentDB` and + `DocumentDB: Stop Local DocumentDB`. + +``` +Managed instance is "Stopped" + | + click row [start] / palette Start + | + v ++--- Background progress (compact, status-bar) ---+ +| Starting DocumentDB Local... [Cancel] | ++--------------------------------------------------+ + | + v + Tree row flips to "Running . localhost:10260". + No notification toast on routine start/stop. +``` + +The Review screen is **never re-shown** during routine start/stop. It only +re-appears when: + +1. The user explicitly creates a **new** managed instance via Command + Palette `DocumentDB: Quick Start - Install Local DocumentDB`. +2. The existing managed instance no longer exists at the Docker level (it + was removed outside the extension) and the user clicks any Start + action — the Review screen returns in "recreate" mode (see section 9.5). + +--- + +## 6. Lifecycle states and the action matrix + +### 6.1 Seven states and two badges + +Each managed instance occupies exactly one of these **seven states**. The +state is shown by an icon color and an inline status word in the +description. + +``` + +---------------+ +---------------+ +---------------+ + | NotInstalled | click QS | Provisioning | success | Running | + | (no row) | -----------> | (spinner) | -----------> | (green dot) | + +---------------+ +---------------+ +---------------+ + | | ^ + | failure | | + v v | + +---------------+ user stop +---------------+ + | Error | <--- error - | Stopping | + | (red dot) | | (yellow dot) | + +---------------+ +---------------+ + ^ ^ | + | | v + +---------------+ user start +---------------+ + | Starting | <----------- | Stopped | + | (yellow dot) | | (gray dot) | + +---------------+ +---------------+ + --- success --> Running +``` + +Running -> Stopping is explicit and is initiated by the user's Stop action; +the diagram should be read end-to-end without implicit edges. + +Two **soft badges** overlay any state without changing the state itself: + +| Badge | Meaning | Tree presentation | +| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +| `UpdateAvailable` | A newer image tag was detected (see sec. 9.4) | `Running . localhost:10260 . update available` | +| `Missing` | The extension has metadata for this instance but Docker has no matching container (e.g., removed in a terminal). Effective hard state is `NotInstalled`. | `Missing . click to recreate` (see sec. 9.5) | + +Badges are not states. `Missing` is the _label_ applied to a `NotInstalled` +row when prior metadata still exists; the row's underlying state, action +matrix, and recovery flow are the `NotInstalled` ones. + +### 6.2 Action matrix + +`v` = action exposed for that state. Inline columns are always shown in +the same position so icons never shift under the cursor (see review finding +R10). + +| Action | NotInstalled (incl. `Missing` badge) | Provisioning | Running | Stopping | Stopped | Starting | Error | Where it shows | +| -------------------------------- | :----------------------------------: | :----------: | :-----: | :------: | :-----: | :------: | :---: | -------------------- | +| Quick Start | v | | | | | | | rocket inline + menu | +| Open Connection | | | v | | | | | inline `[open]` | +| Start | | | | | v | | v | inline `[start]` | +| Stop | | | v | | | | | inline `[stop]` | +| Cancel | | v | | v | | v | | inline `[cancel]` | +| Restart | | | v | | | | v | overflow `[...]` | +| View Logs | | v | v | v | v | v | v | overflow `[...]` | +| Copy Connection String | | | v | | v | | | overflow `[...]` | +| Copy Password | | | v | | v | | | overflow `[...]` | +| Reveal in Docker | | v | v | v | v | v | v | overflow `[...]` | +| Delete Container... | | | | | v | | v | overflow `[...]` | +| Check for Image Update _(v1.1)_ | | | v | | v | | v | overflow `[...]` | +| Rename Alias... _(v1.1)_ | | | v | | v | | v | overflow `[...]` | +| Reset DocumentDB Local… _(v1.1)_ | | | v | | v | | v | overflow `[...]` | +| Forget Quick Start... _(v1.1)_ | | | v | | v | | v | overflow `[...]` | + +Actions marked _(v1.1)_ are documented here for completeness but are +**not shipped in v1.0** — see sec. 17.4 for the v1.0 / v1.1 split. The +v1.0 overflow menu contains only Restart, View Logs, Copy Connection +String, Copy Password, Reveal in Docker, and Delete Container. + +Inline icons reserve fixed positions so the layout is stable across state +transitions: + +``` +Position 1: primary action ([open] when Running, blank otherwise) +Position 2: power action ([start] or [stop] or [cancel]) +Position 3: overflow ([...]) +``` + +`Delete Container` is intentionally **not** offered while the instance is +running — the user must `Stop` first. This avoids an accidental delete on a +hover misclick. + +--- + +## 7. Detailed screens + +### 7.1 Docker readiness diagnosis (shown only if a check fails) + +``` ++------------------ Docker readiness ----------------------+ +| | +| Local Quick Start needs Docker to run DocumentDB on | +| your machine. | +| | +| [x] Docker CLI found v1.27.0 | +| [!] Docker daemon reachable stopped | +| [!] Image registry not reached (proxy or offline?) | +| [?] Image architecture unknown until pull | +| | +| How to fix | +| - Start Docker Desktop and sign in | +| - Check your corporate proxy settings | +| - Test reachability: ghcr.io | +| | +| [Start Docker Desktop] [Troubleshooting] [Retry] | +| | ++----------------------------------------------------------+ +``` + +The diagnosis screen replaces the Review & Start screen when any _blocking_ +check fails. Non-blocking warnings (e.g., insufficient free disk) appear as +a yellow banner **inside** the Review screen instead, and the user can +proceed. + +Categorized failure messages cover, at minimum: + +| Symptom | Action surfaced | +| --------------------------------------- | ------------------------------------------------------------------------- | +| Docker CLI not on PATH | "Install Docker" link, "Already installed? Open settings" link | +| Daemon socket not reachable | "Start Docker Desktop" where supported; otherwise platform-specific setup | +| Linux user not in `docker` group | "Open setup guide for Linux" | +| Windows engine is Windows containers | "Switch to Linux containers?" confirmation, or setup guide | +| Windows Home / WSL2 missing | "Open WSL2 setup guide" | +| Apple Silicon, but image lacks arm64 | "Use x86_64 emulation? (slower)" choice | +| Authenticated proxy blocks registry | "Configure registry credentials" link | +| Docker Desktop resource limits too low | "Open Docker resources" link | +| Remote VS Code session, no local daemon | Explanation + "Use SSH-host Docker" link | + +### 7.2 Progress notification + +Always rendered as a single VS Code progress notification (not a modal) so +the user can keep working. Cancel is always available and rolls back. + +``` ++--------- Starting DocumentDB Local ---------+ +| | +| [x] Checking Docker | +| [x] Reserving port 10260 | +| [>] Pulling official image 42% | +| [ ] Creating container | +| [ ] Starting container | +| [ ] Waiting for connection | +| | +| Elapsed 00:18 [Show Details] [Cancel] | ++---------------------------------------------+ +``` + +`[Show Details]` opens a lightweight details panel with the same step list, +friendly error summaries, and expandable full Docker output: + +``` ++---------------- Quick Start details ----------------+ +| | +| Setting up vscode-documentdb-local | +| | +| [x] Checking Docker | +| [x] Reserving port 10260 | +| [>] Pulling official image | +| This might take a few minutes. | +| [ ] Creating container | +| [ ] Starting container | +| [ ] Waiting for DocumentDB to accept connections | +| | +| [View logs] [Cancel] | ++------------------------------------------------------+ +``` + +On failure, the current step expands automatically: + +``` ++---------------- Quick Start details ----------------+ +| | +| Pulling official image Failed | +| | +| We couldn't pull the image from ghcr.io. | +| Check your network connection or proxy settings. | +| | +| [Show full Docker output] | +| [Retry] [Troubleshooting] [Cancel] | ++------------------------------------------------------+ +``` + +Cancel rules: + +- Cancel during pull -> abort pull, no container created. +- Cancel after pull but before container start -> no container created. +- Cancel during start -> container is created, then stopped and removed, + port released. Generated credentials are kept in storage; the user is + told they will be reused on retry or can be discarded. +- Cancel during "Waiting for connection" -> same as above, plus surface + the most recent connection error in the failure toast. + +### 7.3 Success card + +``` ++---- DocumentDB Local is running on localhost:10260 -----+ +| | +| [Open Connection] [Copy Connection String] | +| | +| [Logs] [Load Sample Data] [Don't show again] | +| | ++----------------------------------------------------------+ +``` + +`[Load Sample Data]` is offered only when the Advanced panel had +_Load sample data on first start_ unchecked. If the user opted in, the +seed is already loaded and this button is hidden. + +`[Don't show again]` mutes the success card for routine starts (it +already isn't shown for non-first starts; this is for users who installed +multiple managed instances). + +### 7.4 Visible port fallback + +The canonical local port is `10260`. When that port is busy, the extension +allocates a free port from a small band — it does **not** silently pick +`10261`, which is commonly also taken on developer machines: + +1. Try `10260`. +2. If busy, try up to **N=10 random ports** in the band `[10260, 10360)`. +3. If still no free port is found, surface the **Change port...** dialog + instead of auto-picking. The user then types a port and the same + conflict check repeats. + +(The PostgreSQL extension only allocates a random port when the input is +empty or invalid; it does **not** auto-fall-back from a valid-but-busy +user-supplied port. The DocumentDB design is intentionally stronger here so +that the zero-form happy path keeps working when the default is occupied.) + +When a fallback is used, the user sees it explicitly in two places: + +1. A yellow banner in the Review screen: + + ``` + ! Port 10260 is in use. We'll use port 10273 instead. + [Change port...] [Use 10273] + ``` + +2. A persistent description on the tree row: + + ``` + v DocumentDB Local Running . localhost:10273 + description: 10260 was already in use + ``` + +The connection string everywhere always reflects the **actual** port. + +--- + +## 8. Managed-instance presentation + +### 8.1 Status icons and colors + +| State | Icon glyph | Color | Tree row example | +| ------------ | ---------------- | ------ | --------------------------------------------- | +| NotInstalled | n/a | n/a | (no row, empty state instead) | +| Provisioning | `loading~spin` | yellow | `Provisioning... . localhost:10260` | +| Starting | `loading~spin` | yellow | `Starting... . localhost:10260` | +| Running | `circle-filled` | green | `Running . localhost:10260` | +| Stopping | `loading~spin` | yellow | `Stopping... . localhost:10260` | +| Stopped | `circle-outline` | gray | `Stopped . localhost:10260` | +| Error | `warning` | red | `Error . click for details . localhost:10260` | + +A small `UpdateAvailable` badge on Running / Stopped: + +``` +v DocumentDB Local Running . localhost:10260 . update available +``` + +The `Missing` badge applies when prior metadata exists but Docker has no +matching container (sec. 6.1, sec. 9.5): + +``` +v DocumentDB Local Missing . click to recreate +``` + +### 8.2 Description format + +``` + . localhost: [. ] +``` + +`` is reserved for the most important contextual fact: + +- `update available` +- `10260 was already in use` +- `same target as a manual connection` +- `stopped from another VS Code window` (transient, see section 9) + +Only one secondary note at a time. Tooltip lists all applicable notes. + +Tooltip example for the managed row: + +``` +DocumentDB Local +Container alias: vscode-documentdb-local + +State: Running +Endpoint: localhost:10260 +Image: ghcr.io/documentdb/...:latest +Resolved version: v1.2.3 (if the image carries a version label; otherwise "unknown") +Resolved digest: sha256:12ab...90ef +Data volume: vscode-documentdb-local-data +Runs on: This machine +``` + +The tree row keeps the UI simple; the tooltip carries the container alias +(for `docker ps` correlation) plus the resolved image digest and — when +the image carries a version label — the resolved version. The digest is +always available; the semver version is image-label-dependent and may +read `unknown`. + +--- + +## 9. Conflict resolution + +### 9.1 An existing container with the same name + +When the user clicks Quick Start but a Docker container already exists +under the planned name (`vscode-documentdb-local`), the extension first +decides whether it is a recognized DocumentDB Local Quick Start resource. +Only recognized containers can be adopted as managed Quick Start instances. + +**Recognition contract.** A container is recognized as a Quick Start +instance if and only if it carries the labels +`vscode.documentdb.quickstart=1` and `vscode.documentdb.alias=`. +These labels are applied at creation time by Quick Start itself; they are +not derived from name, image, or port. `docker container update` does not +support label modification — labels can only be changed by recreating the +container, so a user who wants to manually opt out of adoption must +recreate the container without the labels. The extension also maintains a +local **forgotten-instances list** (sec. 11) that suppresses adoption for +specific container IDs even when the labels still match. Image name, +container name, and port are never sufficient on their own to recognize a +container as managed. + +Recognized container: + +``` ++----- Existing container 'vscode-documentdb-local' found -----+ +| | +| We found a recognized DocumentDB Local container. | +| | +| Container name vscode-documentdb-local | +| Image ghcr.io/documentdb/...:latest | +| Recognized as DocumentDB Local Quick Start | +| Port binding 0.0.0.0:10260 -> 10260 | +| Status Exited 12 days ago | +| Volume vscode-documentdb-local-data | +| | +| What would you like to do? | +| | +| ( ) Adopt as managed Quick Start instance | +| Existing data and credentials are kept where possible. | +| ( ) Reset and recreate | +| Removes the container and its data volume. | +| ( ) Cancel | +| | +| [Continue] [Cancel] | ++--------------------------------------------------------------+ +``` + +Unrecognized container: + +``` ++---- Container name 'vscode-documentdb-local' is already used ----+ +| | +| A container already uses the Quick Start name, but we cannot | +| verify that it is a DocumentDB Local Quick Start container. | +| | +| Container name vscode-documentdb-local | +| Image unknown-or-custom-image | +| Status Running | +| | +| To avoid taking over the wrong container, Quick Start will not | +| adopt it automatically. | +| | +| ( ) Create a manual local connection to this endpoint | +| ( ) Reset and recreate as DocumentDB Local | +| Removes this container and its data volume. | +| ( ) Cancel | +| | +| [Continue] [Cancel] | ++------------------------------------------------------------------+ +``` + +Adopt path resolves credentials in this order: + +1. Local SecretStorage entry from a previous Quick Start. +2. If missing, the user is offered: + ``` + We can't find the saved credentials for this container. + ( ) Reset credentials and recreate the container + ( ) Delete the container and start fresh + ( ) Cancel + ``` + "Adopt without credentials" is intentionally not offered because the + resulting row could not actually open a connection. + +### 9.2 Same-target manual connection already exists + +When the new managed instance points at the same `host:port` as an existing +manual connection, the Review screen shows a soft warning, not a block: + +``` +i You already have a manual connection to localhost:10260. + After Quick Start finishes, both will appear in the tree. + The Quick Start instance owns lifecycle actions. +``` + +Tree presentation rule from section 3.4 then applies. + +### 9.3 Multi-window coordination + +The container is **shared machine state**. The extension never assumes a +window "owns" it. + +UX rules: + +- All windows reflect state changes within a few seconds. **v1 implements + polling only** (on activation, on overflow-menu open, and on the + Connections view refresh tick). Subscription to the Docker event stream + is deferred to v1.x; under polling-only, cross-window latency is bounded + by the poll interval. +- Every destructive action (Stop, Delete, Reset) re-checks the live state + immediately before executing. If the state changed under the user, the + confirmation dialog is replaced: + ``` + ! The instance is now Stopping (from another VS Code window). + The action is no longer available. + [OK] + ``` +- A transient secondary note appears for ~10 seconds when an action was + initiated by a different window: + ``` + DocumentDB Local Running . stopped from another VS Code window + ``` + +### 9.4 Image is outdated + +Discovery is **passive**: the extension does not check for image updates on +activation, and does not show a toast. The check runs when: + +- The user opens the overflow menu on the managed row (lazy). +- The user clicks `Check for Image Update` explicitly. +- The user restarts the instance after at least 7 days. (The 7-day + threshold uses a `lastUpdateCheckAt` timestamp persisted with the + managed-instance metadata.) + +If an update is found, the `update available` badge appears (section 8.1) +and the overflow menu offers: + +``` +Overflow menu + Update Image... opens a Review-style dialog with diff + View Current Version + Ignore This Version +``` + +The Update dialog is the only place the user is asked to confirm an image +change. It restates the data implications: + +``` +Update DocumentDB Local Image? + + Current image ghcr.io/documentdb/...:latest + Current version v1.2.3 (if available) sha256:12ab...90ef + New image ghcr.io/documentdb/...:latest + New version v1.3.0 (if available) sha256:45cd...67ab + Container will be recreated + Data volume will be kept + Credentials will be kept + + [Update] [Cancel] +``` + +When the image does not carry a version label, the `Current version` / +`New version` rows read `unknown`; the digest rows remain authoritative. + +### 9.5 Container disappeared outside the extension + +If the user removed the container in a terminal, the tree row enters the +`NotInstalled` state with the `Missing` badge applied (see sec. 6.1 for the +state/badge distinction) and changes its label to: + +``` +v DocumentDB Local Missing . click to recreate +``` + +Click triggers the Review screen in **recreate** mode (pre-filled with the +last known port, alias, persistence choice). + +### 9.6 Port already in use after a Quick Start once worked + +The same rules as section 7.4 apply. Additionally, the overflow menu gains +a one-shot `Move to a different port...` action that: + +``` ++-------- Move DocumentDB Local to a different port --------+ +| | +| Current port 10260 | +| New port [10261 ] | +| | +| The container will be recreated. Data is kept. | +| The saved connection string will be updated. | +| | +| [Move] [Cancel] | ++-----------------------------------------------------------+ +``` + +--- + +## 10. Error and edge cases + +| Category | What the user sees | +| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| Docker not installed | Docker readiness screen (sec. 7.1) | +| Docker not running | Docker readiness screen with "Open Docker Desktop" | +| Permission denied (Linux group) | Docker readiness screen with platform-specific guidance | +| Apple Silicon, no arm64 image | Readiness warning + opt-in to amd64 emulation | +| Disk space below 2 GB | Yellow banner in Review screen, not a block | +| Network offline / proxy blocked | Readiness "Image registry not reached" + retry | +| Pull aborted mid-way | Failure toast: "Pull failed. [Retry] [View logs]" | +| Container fails to start | Failure toast: "Container failed to start. [Logs] [Reset]" | +| Health check timeout (default 60s) | Failure toast: "Database didn't accept connections in time. [Wait longer] [Logs] [Reset]" | +| SecretStorage cleared | Adopt flow with credential-reset path (sec. 9.1) | +| Quick Start invoked on an unsupported OS | Toast: "Local Quick Start is supported on Windows, macOS, and Linux." (Same gate as the manual emulator wizard already uses.) | +| User clicks Open Connection while still Provisioning | Action is hidden until Running | +| Remote VS Code (SSH / WSL / dev container) | Readiness explains where the container will live and asks the user to confirm | + +All **post-readiness** errors (pull, create, start, health-check timeout, +container fails to start, etc.) render in the same shape: a single VS Code +toast with at most three actions, and never block the editor. +**Pre-start** readiness failures (Docker not installed, daemon stopped, +permission denied, registry unreachable, remote-host ambiguity) render in +the Docker readiness screen (sec. 7.1), not as toasts, because they +require categorized guidance and a Retry that re-runs the check +sequence. + +--- + +## 11. Lifecycle vocabulary (definitions the UI strictly follows) + +Wording mistakes here cause data loss. The UI uses these exact verbs and +never mixes them. + +| Verb | Effect on container | Effect on data volume | Effect on credentials | Effect on `quickstart.*` Docker labels | Effect on extension's local management metadata | Effect on tree row | +| ---------------------------------------- | ------------------------------------------------------------------- | --------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | ------------------------------------------------ | +| **Start** | starts existing | unchanged | unchanged | unchanged | unchanged | -> Running | +| **Stop** | stops | unchanged | unchanged | unchanged | unchanged | -> Stopped | +| **Restart** | stop + start | unchanged | unchanged | unchanged | unchanged | -> Running | +| **Rename Alias...** _(v1.1)_ | `docker rename` to new alias; container is not stopped or recreated | unchanged | unchanged | `vscode.documentdb.alias` updated to new value (via container recreation, since labels are immutable); other labels re-applied | alias field updated; container ID unchanged unless recreation is required for label rewrite | label and tooltip update; row position unchanged | +| **Update Image...** _(v1.1)_ | recreate | kept | kept | re-applied on the new container | updated (new digest, new container ID) | -> Running | +| **Move to a different port...** _(v1.1)_ | recreate | kept | kept | re-applied on the new container | updated (new port, new container ID) | -> Running | +| **Delete Container...** | removes container | kept | kept | n/a (no container) | kept (re-create reuses alias, port, volume) | -> NotInstalled with `Missing` badge (sec. 9.5) | +| **Reset DocumentDB Local...** _(v1.1)_ | removes container | **dropped** | **dropped** | n/a (no container) | **dropped** | -> NotInstalled (row removed) | +| **Forget Quick Start...** _(v1.1)_ | **unchanged** | unchanged | kept and re-keyed to a Manual connection ID | **unchanged** (see implementation note below) | **dropped** | row converted to a Manual connection | + +Note on `Rename Alias...`: because Docker labels are immutable on an +existing container, changing the `vscode.documentdb.alias` label requires +container recreation. The v1.1 implementation does this transparently +(stop, `docker commit` not used; just recreate with same image digest + +new alias + persistent volume), so the user-visible effect is just "the +name changed." The data volume is intentionally **not** renamed to follow +the alias — the volume keeps its original name to preserve the +metadata-to-volume link if the user renames repeatedly. This trade-off is +also flagged in sec. 15 open question 1. + +`Forget Quick Start` deliberately keeps the credentials (the verb forgets +the **management relationship**, not the secret). After Forget: + +- The container keeps running with its Docker labels intact. + `docker container update` does **not** support label removal (it only + changes resource limits and restart policy), and the design + intentionally avoids recreating the container just to drop labels, + because Forget must be non-destructive. +- The extension drops its local management metadata for this container ID + and adds the container ID to a local **forgotten-instances list**. + This list is consulted whenever the recognition contract (sec. 9.1) + runs: a container whose ID appears in the forgotten list is **not** + offered as an Adopt candidate even though its labels still match. The + user can clear the list from the command palette + (`DocumentDB: Reconsider Forgotten Local Instances`). +- The stored credentials are re-keyed from the Quick Start + SecretStorage namespace into the Manual connection SecretStorage + namespace so the converted row still opens. +- The row moves out of the `Quick Start` group and into the + `Manual connections` group; lifecycle actions + (Start/Stop/Update/Move/Reset) disappear from the overflow menu because + the extension no longer owns the container. + +Confirmation phrasing: + +- _Stop_ — no confirmation. Reversible. +- _Restart_ — no confirmation. Reversible. +- _Delete Container_ — one-line confirm: "Delete the container? Your data + is kept and will be re-attached if you Quick Start again." +- _Reset DocumentDB Local_ — two-step confirm. User must type the + container alias to confirm. Names the volume that will be deleted and + warns "Data cannot be recovered." +- _Forget Quick Start_ — one-line confirm: "Stop managing this container + from the extension? The container keeps running and the saved + credentials are kept so the Manual connection still works. The + extension will stop offering lifecycle actions for it and will not + re-adopt it automatically. You can re-adopt it from the command + palette later." + +--- + +## 12. Telemetry hints (informational; no PII) + +Per the existing telemetry conventions of the extension. Listed here for +UX completeness so reviewers can see what we plan to measure. + +``` +event: quickstart.review_shown prop: source=tree|menu|command|welcome +event: quickstart.review_advanced prop: opened_first_time=bool +event: quickstart.docker_readiness prop: result=ok|cli_missing|...|unknown, + os=win|mac-x64|mac-arm|linux|... +event: quickstart.start_begin prop: source=... +event: quickstart.start_stage prop: stage=pull|create|start|connect, + duration_ms, success=bool +event: quickstart.start_end prop: result=success|cancelled|failed, + elapsed_ms, + port_fallback=bool, + recreate=bool, adopted=bool, + image_resolved_version=semver|unknown +event: quickstart.lifecycle prop: action=start|stop|restart|delete|reset|move|update|forget, + initiated_by=user|other_window, + duration_ms, success=bool +event: quickstart.error prop: stage=..., reason=... +event: quickstart.dismiss_welcome prop: from=welcome_view|empty_state +``` + +Container name, user-edited image tag, registry URL, hostnames, ports, +credentials, and image digest are never sent. The **resolved image +version** (semver from the image label, or `unknown`) IS sent so that +"v1.2.3 has a bug" can be correlated with telemetry. Whether the user +opted into sample data IS sent. + +--- + +## 13. Cross-cutting rules + +- **Opt-in only.** The extension never installs Docker, never starts Docker + silently, and never modifies a container that wasn't created by Quick Start + unless the user explicitly chooses Adopt. +- **Explicit Docker start.** If Docker is installed but stopped, the extension + can offer `Start Docker Desktop` / `Start Docker` as a user-clicked action + where supported. It does not invoke `sudo` or perform privileged daemon + setup. +- **No background pulls.** Image is pulled only inside a user-initiated + Quick Start or Update Image flow. +- **No required form in the happy path.** The default Quick Start path has no + mandatory fields. Any setting that would otherwise become a setup step must + either have a safe default or move to Advanced. +- **Canonical local port.** Quick Start and the manual DocumentDB Local path + use `10260` by default. The current manual wizard hardcodes `10255` for + the preconfigured DocumentDB/MongoRU paths + (`PromptConnectionTypeStep.ts` and `PromptPortStep.ts`); this is a + pre-ship bug and must be fixed so both paths agree on the + `documentDB.local.port` setting (default `10260`) before Quick Start + lands. +- **No nag toasts.** Updates and warnings stay in the tree row description + unless the user opens the overflow menu. +- **Reversibility.** Stop, Restart, and Cancel are always safe. +- **Symmetry with the manual wizard.** Manual connections continue to work + exactly as today. Nothing is removed. +- **Uninstalling the extension does not remove the container.** A separate + "Clean Up Quick Start Resources..." command is offered for that, before + uninstall, in the command palette and in the overflow menu. + +--- + +## 14. Non-goals for v1 + +- **Multiple concurrent managed instances per user.** v1 ships strictly + single-managed-instance. The `Quick Start` group in the tree exists for + forward compatibility but always contains exactly one row in v1. The + rocket icon on the section header is hidden once a managed instance + exists; a future v1.x release will reintroduce it as + `Create another local instance...`. +- **Bundled sample data.** The `[Load Sample Data]` action is described + throughout this document, but if no curated dataset can be bundled in + the extension (extension-size impact) or fetched safely on demand + (offline behavior, proxy, signature verification) by ship time, the + action ships **disabled with a "Coming soon" affordance** in v1 and is + enabled in v1.x. The success card and empty Collection View callout + still render the button so the discovery surface is preserved. +- Auto-loading sample data by default. The flag is in Advanced; the + separate `Load Sample Data` command on the managed row remains the + primary path. +- Ephemeral data volumes. Persistent local storage is the only v1 mode so + Stop, Restart, Update, Move Port, and Delete Container have predictable + data behavior. +- Resource usage charts (CPU, memory, disk) in the tree row. +- Authentication beyond username / password. No client certs in v1. +- Bring-your-own-image. The image tag is editable in Advanced, but only + for the official image. Custom images are deferred. +- Managing non-managed containers as Quick Start. The Adopt path requires + the container to be recognized as a previous DocumentDB Local Quick Start + resource via the labels contract in sec. 9.1; a matching name alone is + not enough. + +--- + +## 15. Remaining open questions + +1. **Persistent volume naming.** Default `vscode-documentdb-local-data` is + easy to find in `docker volume ls`. Should the alias the user picks in + Advanced be reflected in the volume name? Pro: discoverable. Con: + renaming the instance breaks the link. +2. **Self-signed certificate trust.** Today both wizards either skip TLS + verification (`tlsAllowInvalidCertificates=true`) or use a global + `disableEmulatorSecurity` flag. Quick Start uses the same approach. + Long term, the official image's local CA could be auto-trusted in the + user's Node trust store, which would let the connection string drop + the allow-invalid-certs flag. Out of scope for v1; worth tracking. +3. **Welcome card scope.** Should the welcome card appear only when the + Connections view is empty _overall_, or whenever the DocumentDB Local + section is empty? Current draft says the latter; some reviewers may + prefer the former to avoid showing the card to users who already have + many cloud connections. +4. **Linux + sudo Docker.** Linux machines without the user in the docker + group need `sudo`. Quick Start does **not** invoke sudo. The Docker + readiness diagnosis surfaces the fix instead. Confirm this matches the + extension's existing posture on privilege escalation. + +--- + +## 16. Out of scope (for this design doc) + +The following implementation-detail topics intentionally do not belong +here, but each has a downstream implication that the companion +implementation plan must address: + +- **Choice of orchestration mechanism (Docker SDK vs. `docker` CLI).** The + cancellation contract in sec. 4 and sec. 7.2 means + `vscode.ShellExecution` (the PostgreSQL approach) is insufficient, + because it cannot abort an in-flight `docker pull`. The implementation + must use a cancellable process surface (e.g., + `child_process.spawn` with kill-on-cancel, or a streaming API from + `@microsoft/vscode-container-client`), and the cancellation handler + must explicitly remove any container that was created and release any + port that was reserved before declaring the operation aborted. +- **How healthchecks are implemented.** The user-visible contract is in + sec. 4.1 (a `hello`/`ping` over the wire protocol). The implementation + decides how to issue that probe (raw socket, driver, `mongosh` if + available, etc.), the back-off curve, and the cancellation behavior. +- **`LocalEmulatorsItem` migration contract.** The current + `LocalEmulatorsItem` (`src/tree/connections-view/LocalEmulators/LocalEmulatorsItem.ts`) + renders a `DocumentDB Local` row with a single `New Local Connection...` + child when empty. Quick Start replaces this empty state with three + child rows plus inline header icons (sec. 3.2). Existing manual + connections continue to render at the top level; the + `Quick Start` / `Manual connections` grouping (sec. 3.3) is applied + only once at least one Quick Start instance exists. The implementation + plan must call out this migration explicitly so existing users with + many manual emulator connections do not regress. +- **Credential transport.** The implementation must pass generated + credentials to the container via a temp `--env-file` (written under + `os.tmpdir()` and removed in a `finally` block), not as repeated `-e` + flags. `-e` flags appear on the host CLI command line and therefore in + `ps -ef` and shell history; `--env-file` does not. Note the precise + security boundary: `--env-file` removes the **host-side** exposure + (CLI, history, process audit), but the password is still present in + the container's runtime environment and is therefore visible via + `docker inspect ` (Config.Env) and `docker exec env` + to anyone with Docker access on the host. This matches the trust + boundary the user already accepts by running Docker locally. PostgreSQL + already does this (see `dockerCreateWebviewController.ts:280-287`); + DocumentDB must match. +- **Docker labels for the recognition contract.** The user-facing rule is + in sec. 9.1. The exact label names (`vscode.documentdb.quickstart=1`, + `vscode.documentdb.alias=`) are stable wire format; the + implementation plan should treat them as a versioned contract and not + rename them silently. +- **Welcome-card dismissal storage.** Stored in the user Setting + `documentdb.quickStart.welcomeDismissed` (sec. 2), not in + `globalState`. This is the only way the dismissal survives an extension + uninstall/reinstall. +- **Telemetry property data types or sampling rules.** +- **Localization of strings** (handled at implementation time via + `vscode.l10n.t()`, per repo convention). +- **Tests, build wiring, or settings keys.** + +These belong in a companion implementation plan that references this +document. + +--- + +## 17. Design review: comments, findings, and suggestions + +### 17.1 Review outcome + +**Approve the UX direction for implementation planning.** + +The design correctly moves beyond the Cosmos DB extension's attach-only +emulator pattern and uses the PostgreSQL local Docker creation flow as the +primary UX reference. The strongest parts are the explicit Review & Start +screen, zero required fields in the default path, visible Docker/readiness +progress when needed, the tree as the persistent control surface, the +separation between managed Quick Start and manual connections, and the careful +lifecycle vocabulary for Stop, Delete, Reset, and Forget. + +The initial review findings below have been folded back into this draft. The +remaining open questions are intentionally limited to follow-up product or +implementation decisions that should not block the core v1 workflow. + +### 17.2 Findings + +#### First-round findings (folded into the draft) + +| ID | Severity | Original finding | Resolution in this draft | +| --- | ------------ | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| R1 | Must fix | "One click" could conflict with first-run review. | Product copy now uses **Quick Start**; true one-click is scoped to subsequent starts after setup. | +| R2 | Must fix | Manual DocumentDB Local and Quick Start could disagree on default port. | `10260` is now a cross-cutting UX rule for both Quick Start and manual local connection; mismatch is called a pre-ship bug. | +| R3 | Must fix | Users could think the extension installs Docker. | The Review screen and prerequisite promise say Docker is required and not installed by the extension; if Docker is stopped, start is explicit. | +| R4 | Must fix | Ephemeral data mode was ambiguous. | Ephemeral volumes are removed from v1; persistent local volume is the only data mode. | +| R5 | Should fix | Empty database after success may not feel like "try DocumentDB." | `Load Sample Data` is promoted on the success card and empty Collection View callout. | +| R6 | Should fix | Existing-container adoption could take over the wrong container. | Adopt is offered only for recognized DocumentDB Local Quick Start resources; name-only matches get manual connection/reset/cancel choices. | +| R7 | Should fix | Remote VS Code makes "local" ambiguous. | Remote-session Review banner names the actual target context before start. | +| R8 | Should fix | `latest` makes image version hard to reason about. | Managed-row tooltip and update dialog show resolved version and image digest. | +| R9 | Nice to have | Multi-window coordination may expand v1 implementation scope. | User-facing rule remains; v1.0 ships polling-only (sec. 9.3, sec. 17.4); event subscription deferred to v1.1. | +| R10 | Nice to have | Inline actions should not shift under the cursor. | Three fixed action slots are retained as UX contract: primary, power, overflow. | +| R11 | Nice to have | Welcome card could annoy users with cloud connections but no local ones. | Empty `DocumentDB Local` section remains the default scope; dismissal is shared with empty-state card. | +| R12 | Should fix | PostgreSQL shows that local Docker creation should finish by saving, connecting, and revealing the new resource. | The success definition now requires DocumentDB readiness plus a usable revealed connection, not just a running container. | +| R13 | Should fix | PostgreSQL uses a form, but DocumentDB can be easier because its defaults are known. | The happy path now has zero required fields; all setup choices are generated or moved to Advanced. | +| R14 | Should fix | MSSQL reduces friction by starting Docker Desktop when possible, but that can feel surprising. | The design allows an explicit user-clicked Docker start action where supported, while preserving the no-silent-start rule. | + +#### Second-round findings (combined external review, folded into this revision) + +| ID | Severity | Original finding | Resolution in this revision | +| --- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| R15 | Must fix | §6.1 said "six states" but showed seven; the seventh "soft" badge sentence didn't reconcile the count. | Heading renamed to **"Seven states and two badges"** (sec. 6.1). The `Missing` badge is now a documented second badge alongside `UpdateAvailable`. Action matrix in §6.2 unchanged (already had seven cols). | +| R16 | Must fix | `Missing` was used inconsistently across §9.5 (label on NotInstalled) and §11 (apparent distinct state). | `Missing` is a **badge** over `NotInstalled` everywhere. §6.1, §8.1, §9.5, §11 updated. Action matrix column header is "NotInstalled (incl. `Missing` badge)". | +| R17 | Must fix | `Forget Quick Start` (§11) dropped credentials yet converted the row to a Manual connection, which §9.1 explicitly says cannot open without credentials. | Forget now **keeps** credentials; it drops only the `quickstart.*` Docker labels and the management relationship. Updated §11 verb table and confirmation copy. | +| R18 | Must fix | "Waiting for the database to accept connections" had no defined probe (PostgreSQL uses `pg_isready`; DocumentDB has no equivalent CLI baked into the image). | New "Readiness contract" paragraph in §4.1 defines a `hello`/`ping` over the wire protocol with a 60s default timeout. §10 references stay consistent. | +| R19 | Must fix | §7.4 deterministic `+1` fallback was fragile and misrepresented PostgreSQL. | §7.4 rewritten to random free port in `[10260, 10360)` with up to 10 attempts, then escalate to `Change port...`. §0.2 row corrected to state that PostgreSQL does **not** do this for valid-but-busy ports. | +| R20 | Must fix | Tree label inconsistent: §3.3 used `vscode-documentdb-local`, §4.1 said `DocumentDB Local`. | Tree row label is **`DocumentDB Local`** (sec. 3.3, sec. 4.1). The container alias `vscode-documentdb-local` lives in the tooltip (sec. 8.2). | +| R21 | Must fix | The manual wizard currently hardcodes port **10255** (`PromptConnectionTypeStep.ts:97,101`, `PromptPortStep.ts:23,25`) despite the `documentDB.local.port` setting defaulting to **10260**. The doc only treated this as hypothetical. | §13 now names the specific files and calls out the fix as a hard pre-ship dependency for Quick Start. | +| R22 | Should fix | Adopt-recognition contract was deferred to §16 even though it has user-visible consequences (whether Adopt is offered). | Recognition contract is now an explicit paragraph in §9.1 (labels `vscode.documentdb.quickstart=1` and `vscode.documentdb.alias=`, applied at creation, never inferred from name/image/port). | +| R23 | Should fix | §16 listed "out of scope" items that have hard downstream constraints (cancellation, env-file credential transport, process orchestration). | §16 rewritten to keep each item but call out its downstream implication explicitly so the implementation plan does not silently regress against them. | +| R24 | Should fix | §17.4 had no v1.0 / v1.1 split; readiness diagnosis (§7.1) and the Forget/Update/Move/Reset verbs would balloon v1. | §17.4 rewritten as an explicit **v1.0 / v1.1** split. v1.0 = PostgreSQL-parity slice + DocumentDB UX wins; v1.1 = adopt, update, move, reset, forget, event subscription, remote banner, categorized diagnosis. | +| R25 | Should fix | §3.3 enabled multi-instance ("every managed instance is listed") while §14 declared it a non-goal. | §3.3 now states v1 is single-managed-instance. §14 updated to make this concrete. Multi-instance moves to v1.1 (sec. 17.4). | +| R26 | Should fix | Welcome-card dismissal across uninstall (§2) needed an explicit storage commitment because `globalState` is wiped on uninstall. | §2 now states dismissal is stored in user Setting `documentdb.quickStart.welcomeDismissed`. §16 reiterates. | +| R27 | Should fix | `LocalEmulatorsItem` migration contract was implicit. | §16 calls out the file path and the v1 invariant: manual connections continue to render at the top level; grouping only kicks in once a Quick Start instance exists. | +| R28 | Should fix | `Load Sample Data` was treated as borrowed from PostgreSQL (it isn't) and committed to without a delivery mode. | §14 marks Load Sample Data as v1.0-if-feasible / v1.1-otherwise, with the button rendered disabled-with-"Coming soon" if no dataset can be bundled or fetched safely by ship time. | +| R29 | Nice fix | `Resolved version` was promised in the tooltip, but the image may not carry a version label. | §8.2 and §9.4 phrase the field as "version if available, digest always." | +| R30 | Nice fix | Telemetry omitted resolved image version (useful for "v1.2.3 has a bug" correlations) and included nothing about Forget. | §12 adds `image_resolved_version=semver\|unknown` to `quickstart.start_end` and adds `forget` to the lifecycle action enum. | +| R31 | Nice fix | §6.1 diagram had no explicit `Running → Stopping` arrow. | Diagram updated; "user stop" edge from Running to Stopping is now drawn explicitly, and a callout under the diagram repeats the rule. | + +#### Third-round findings (independent fresh-context review by a different model, folded into this revision) + +| ID | Severity | Original finding | Resolution in this revision | +| --- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| R32 | Must fix | §5 said the header rocket "turns into a direct start" of the existing instance after setup, while §14 and §17.4 said the rocket is hidden once a managed instance exists. The two readings conflicted. | §5 rewritten: with v1 single-managed-instance the rocket is **hidden** once a managed instance exists; Start happens from the row's inline `[start]`, the row's right-click menu, or the command palette. The hidden behavior is consistent with §14 and §17.4. | +| R33 | Should fix | §10 declared "all errors render as a single toast," but Docker readiness failures (§7.1) explicitly render as a screen with a Retry that re-runs the check sequence. | §10 closing paragraph now scopes the toast rule to **post-readiness** failures (pull, create, start, healthcheck timeout, etc.) and explicitly excludes pre-start readiness diagnosis, which continues to render via §7.1. | +| R34 | Must fix | §11 said `Forget Quick Start` removes labels from the existing container, but `docker container update` does **not** support label removal (only resource limits and restart policy). | §11 rewritten: Forget does **not** touch the container or its labels. Instead it drops the extension's local management metadata, adds the container ID to a local **forgotten-instances list** consulted by the recognition contract, and re-keys credentials into the Manual connection SecretStorage namespace. A new command `DocumentDB: Reconsider Forgotten Local Instances` clears the list. | +| R35 | Must fix | §4.1 secret-storage row and §16 credential-transport bullet both claimed `--env-file` keeps the password out of `docker inspect`. That is not true — `Config.Env` from `--env-file` is inspectable via `docker inspect` and `docker exec env`. | Both passages rewritten: `--env-file` removes the **host-side** exposure (CLI command line, `ps -ef`, shell history) but the password remains visible inside the container's runtime environment to anyone with Docker access on the host. This matches the trust boundary the user already accepts by running Docker. | + +### 17.3 UX principles to carry into implementation planning + +1. **Be transparent before side effects.** Downloading an image, creating a + container, binding a port, and persisting a volume are machine-level + changes. The Review screen must stay mandatory on first run. +2. **Do not turn Quick Start into a setup form.** PostgreSQL's flow is a good + creation reference, but DocumentDB should be easier: generate defaults and + use Advanced for overrides. +3. **Keep routine actions quiet.** After setup, Start and Stop should update + the tree and status bar without celebratory toasts. +4. **Keep manual attach first-class.** Quick Start should not replace users + who already run DocumentDB themselves. +5. **Use the tree as source of truth.** Status, port, update availability, + and lifecycle actions should be discoverable from the managed instance row. +6. **Prefer reversible defaults.** Persistent data, explicit reset, and no + automatic cleanup on extension uninstall are the safest defaults. +7. **Avoid terminal language in the happy path.** Docker details belong in + Review, Advanced, logs, and troubleshooting, not in the main success flow. + +### 17.4 Suggested v1.0 / v1.1 scope split + +The full design above is the multi-release roadmap. The shippable +**v1.0** matches what PostgreSQL actually demonstrates plus the +DocumentDB-specific UX wins; everything else is **v1.1**. + +#### v1.0 (must ship) + +Surface and behavior: + +- Entry points: rocket icon on the `DocumentDB Local` header, child row + in the empty state, command palette entry, walkthrough card. +- One Review & Start screen with zero required fields. +- Docker readiness: same two checks as PostgreSQL (CLI present, daemon + reachable) plus a single generic `See Troubleshooting` link. (The nine + categorized failure modes in sec. 7.1 — Apple Silicon arm64, WSL2 + missing, sudo group, Windows-vs-Linux engine, authenticated proxy, + etc. — are v1.1.) +- Progress notification with `Show Details` and `Cancel`. Cancel must + actually abort the in-flight `docker pull`, remove any created + container, and release the reserved port (sec. 16). +- Pull / create / start / wait-for-readiness / save / connect / reveal. +- **Readiness** is a `hello`/`ping` over the wire protocol, 60s + timeout (sec. 4.1). +- **Port fallback** picks a random free port in `[10260, 10360)` with up + to 10 attempts; falls through to the `Change port...` dialog (sec. + 7.4). Both Quick Start and the manual wizard use `10260` as the + canonical default (sec. 13). The current manual-wizard 10255 hardcode + is fixed. +- **Credentials** stored in SecretStorage, passed via `--env-file` only + (sec. 16). +- **Labels** `vscode.documentdb.quickstart=1` and + `vscode.documentdb.alias=` applied at creation (sec. 9.1). +- **Tree row** label is `DocumentDB Local`; alias and digest live in the + tooltip (sec. 3.3, sec. 8.2). +- **Lifecycle actions** in v1.0: Open Connection, Start, Stop, Restart, + View Logs, Copy Connection String, Copy Password, Reveal in Docker, + Delete Container. All other overflow actions are v1.1. +- **States** are the seven defined in sec. 6.1. `UpdateAvailable` and + `Missing` badges are wired through the rendering layer but only + `Missing` is reachable in v1.0 (Update is a v1.1 feature; the badge + renders no-op until then). +- **Multi-instance** is single-only in v1.0; the rocket icon is hidden + once a managed row exists (sec. 14). +- **Multi-window coordination** is polling-only on activation, + Connections-view refresh, and overflow-menu open (sec. 9.3). Docker + event subscription is v1.1. +- **Welcome card dismissal** stored in user Setting + `documentdb.quickStart.welcomeDismissed` (sec. 2). +- **Sample data** is v1.0 if a curated dataset can be bundled or fetched + safely by ship time; otherwise the button renders disabled with a + "Coming soon" affordance (sec. 14). + +#### v1.1 (deferred) + +- Adopt-existing-container flow (sec. 9.1) — the recognition contract + still ships in v1.0 (labels are applied on creation) so v1.1 can use + them immediately. +- Update Image with version/digest diff (sec. 9.4). +- Move to a different port (sec. 9.6). +- Reset DocumentDB Local and Forget Quick Start (sec. 11). +- Categorized Docker readiness diagnosis (sec. 7.1, rows beyond the two + baseline checks). +- Docker event subscription for multi-window coordination (sec. 9.3). +- Remote VS Code (SSH/WSL/dev container) banner (sec. 4.3). +- Load Sample Data if not bundled in v1.0. +- Multiple managed instances via Command Palette + `DocumentDB: Quick Start - Install Local DocumentDB`. + +The v1.0 user promise: **from an empty machine-with-Docker to an open +local DocumentDB connection, without leaving VS Code.** Everything in +v1.1 polishes lifecycle ownership on top of that promise without +changing the promise itself. diff --git a/docs/ai-and-plans/local-quickstart/v1-readiness-gaps.md b/docs/ai-and-plans/local-quickstart/v1-readiness-gaps.md new file mode 100644 index 000000000..14d43b798 --- /dev/null +++ b/docs/ai-and-plans/local-quickstart/v1-readiness-gaps.md @@ -0,0 +1,284 @@ +# Local Quick Start — v1 production-readiness gaps + +**Status:** In progress · **Date:** 2026-06-26 +**Scope:** Gap analysis between the production design +([`local-quickstart-v2.md`](./local-quickstart-v2.md) §15 v1.0 "must ship" + UX sections +§3–§12) and the current implementation (the POC on branch `feature/local-quickstart/POC`). +**Companion:** [`decision-instance-model.md`](./decision-instance-model.md) (single-instance v1). + +This is the ranked work list to take Local Quick Start from a demo-POC to a shippable v1. +Ranking is by **user stakes**, not by effort. Each row cites the design section and the current +state. Checked rows are implemented in this branch. + +## ✅ Already implemented (POC baseline) + +Provision → 180 s wire-protocol readiness → inline browse · full 7-state machine + `Missing` +badge · all 8 lifecycle actions (Open/Start/Stop/Restart/View Logs/Copy Conn String/Copy +Password/Delete) · label-gated ownership (`vscode.documentdb.quickstart=1`) · restart-safe sample +data via `docker exec` of the image's native init script · masked OutputChannel · single managed +instance. + +## 🔴 P0 — Correctness & data loss (will bite real users) + +| # | Gap | Design | Current | What's needed | +| - | --- | ------ | ------- | ------------- | +| P0‑1 | **Persistent data volume** | §8 defaults; §11 | **Ephemeral** (no volume) | Named volume `vscode-documentdb-local-data` mounted at **`/data`** (verified `DATA_PATH=/data`). Make sample-seeding **idempotent** (skip if `sampledb` exists). Align Delete to §11 (keep volume + creds → Missing); recreate reuses both. | +| P0‑2 | **Port-conflict fallback** | §8.3 | Pre-checks 10260, hard-errors if busy | Try 10260, then up to 10 random ports in `[10260,10360)`; yellow "using 10273 instead" banner; **use the bound port from `docker inspect`** when composing/saving the conn string. Explicit (Advanced) ports are never relocated — error instead. | +| P0‑3 | **Credential transport via env-file** | §8.2 | Password on `--username/--password` **CLI args** (leaks to `ps`/history) | Pass creds as `USERNAME`/`PASSWORD` via a temp `--env-file` (deleted in `finally`). **Verified the image reads these env vars** (entrypoint `${USERNAME:-}/${PASSWORD:-}`; CLI args only override) — resolves OPEN‑1. | + +## 🟠 P1 — First-run UX (where impressions are made) + +| # | Gap | Design | Current | What's needed | +| - | --- | ------ | ------- | ------------- | +| P1‑1 | **Docker-not-ready diagnosis** | §5.3, §9 | One-line message + Retry | Per-check cards (CLI / daemon / platform), a **"Start Docker Desktop"** action (§13.2), and a Troubleshooting link. Docker-stopped is the most common first-run failure — a dead-end one-liner loses users. | +| P1‑2 | **Platform-supported check** | §9 | Not implemented | Detect unsupported CPU arch (amd64/arm64 ok); warn otherwise. | +| P1‑3 | **Success → tree handoff** | §5.5 | Auto-closes, no card buttons | Brief success card with **Open Connection** (reveal + expand the tree node) so the instance doesn't just "disappear". | +| P1‑4 | **Advanced panel** | §5.2 | ✅ **Done** | Collapsible Advanced panel: custom port (explicit-port branch of P0‑2), custom credentials, image tag, sample-data toggle. On reuse the creds/image fields hide (volume kept). 4-round 5-agent review (security + data-safety). | + +## 🟡 P2 — Ecosystem integration (upgrade trust) + +| # | Gap | Design | Current | What's needed | +| - | --- | ------ | ------- | ------------- | +| P2‑0 | **Decouple storage-zone from `isEmulator`** (prerequisite) | §7 | ✅ **Done** | Explicit `storageZone` on the model + `resolveStorageZone`; route all ops by it. Unblocks P2‑1/P2‑2. | +| P2‑1 | **Legacy emulator migration** | §4 | ✅ **Done** | One-time copy of `Emulators` → "Local Connections (Legacy)" folder (creds/auth/`emulatorConfiguration` preserved), keep Emulators as rollback, toast, retire `LocalEmulatorsItem`. 3-round 5-agent review; create-if-missing + race reconciliation. | +| P2‑2 | **TLS-exception step in the regular wizard** + connection edit dialog | §7, §7.3 | ✅ **Done** (step); §7.3 edit dialog deferred | The emulator wizard is being removed; this is its replacement. Gated host step defaulting to *Enable TLS*; TLS-allow-invalid now keyed off `disableEmulatorSecurity` alone and host-gated to local/private hosts only. | +| P2‑3 | **Manual-wizard `10255`→`10260`** | §13.5 | ✅ **Done** | Design: *"must be fixed before Quick Start ships."* DocumentDB-local default is now `10260`; `10255` retained only for the Cosmos Mongo‑RU experience. | + +## 🔵 P3 — Observability & robustness + +| # | Gap | Design | Current | What's needed | +| - | --- | ------ | ------- | ------------- | +| P3‑1 | **Telemetry** | §14 | None | Event taxonomy (`quickstart.*`); never send names/ports/creds, only resolved semver. Expected for production. | +| P3‑2 | **Multi-window coordination** | §12 | Refresh-on-expand only | Destructive actions re-check live state; *"now Stopping from another window"* message. | +| P3‑3 | **Terminal-first transparency** | §5.4 | OutputChannel stream | Design runs docker as VS Code **terminal tasks** (Tomaz emphasized this). Confirm v1 decision vs. accepting the OutputChannel. | +| P3‑4 | **Accessibility** | — | ✅ **Done (v1.1)** | Per-field validation, live regions for staged progress + terminal states, list semantics, focus management (committed `8a08c2c3`). | +| P3‑5 | **Readiness on-timeout actions** | §9.1 | ✅ **Done (v1.1)** | On a readiness timeout the container is KEPT running and the webview offers **Wait longer** (re-probe, no re-pull) / **View logs** / **Start over** (discard; data-safe). Retain-and-resume state machine; 3-round 5-agent review. | + +## Recommended v1 cut line + +- **Must-have:** P0‑1, P0‑2, P0‑3 · P1‑1, P1‑2 · P2‑1, P2‑3. +- **Strongly-want:** P1‑3 · P3‑1 · P3‑2 · P2‑2. +- **Can slip to v1.1:** P3‑3 terminal-first (**in progress** — decided to align with the design) · P3‑4 a11y (**done**) · P3‑5 on-timeout (**done**). + +## Implementation log + +- _2026-06-26_: doc created; image facts verified (`/data` volume mount, env-var creds, init dir + `/init_doc_db.d`). Starting P0. +- _2026-06-26_: **P0 complete + verified live.** + - **P0‑1 volume:** named volume `vscode-documentdb-local-data` at `/data`; seeding made + **idempotent** (skip if `sampledb` exists); **Missing→recreate reuses stored creds + volume** + (verified: data persists across container removal+recreate); **explicit Delete** now also drops + the volume (honest clean slate — the data-preserving Reset split is v1.2). + - **P0‑2 port fallback:** `findAvailablePort` (10260 → up to 10 random in band) + bound-port + readback; substitution surfaced in the `checking` stage message. (Interactive + "Change port" banner needs the Advanced panel, P1‑4.) + - **P0‑3 env-file:** creds now pass via a temp `--env-file` (mode 600, deleted in `finally`) as + `USERNAME`/`PASSWORD`; **verified live** the image authenticates with env-file creds and nothing + lands on the docker CLI. Resolves OPEN‑1. +- _2026-06-26_: **P1‑1 + P1‑2 complete (build-verified).** + - **P1‑2 platform:** `DockerReadiness` gains `arch`/`platformSupported` (host arch x64/arm64). + - **P1‑1 diagnosis:** Docker-not-ready view rebuilt into per-check cards (CLI / daemon / platform) + + **"Start Docker Desktop"** action (`startDockerDesktop`, best-effort per-OS launch) + Install / + Troubleshooting links. Review "Data" card corrected to **Persistent volume**. + - Gates green: l10n · prettier · lint · jest (2055/2055) · build · webpack-prod. +- _Remaining:_ P1‑4 Advanced panel, P2 (migration / TLS wizard / 10255), P3‑4 a11y, P3‑5 + readiness on-timeout actions. + +- _2026-06-26_: **P1‑3 + P3‑1 + P3‑2 complete (build + gates verified).** + - **P1‑3 success handoff:** success card now shows **Open Connection** (focuses the Connections + view, then closes) + **Copy Connection String**; auto-close delay extended so the buttons are + usable. New router mutations `openConnection` / `copyConnectionString`. + - **P3‑2 multi-window:** `start/stop/restart` now re-check live Docker state via `liveStateGuard` + immediately before acting; if another window already changed it, the tree refreshes and the user + is told *"changed in another window (now …)"* instead of acting on stale state (§12). + - **P3‑1 telemetry:** `documentDB.quickstart.provision` event (result · reused · portFallback · + provisionMs); `getDockerStatus` now reports `dockerReadiness` + `platformSupported`; lifecycle + commands tag `action`. No names/ports/creds sent (§14). + - Gates green: l10n · prettier · lint · jest (2055/2055) · build · webpack-prod. + +- _2026-06-26_: **P2‑1 attempted → REVERTED (architectural blocker found by 5-agent review).** + - A first cut (copy each `Emulators`-zone connection into a "Local Connections (Legacy)" folder + in the `Clusters` zone, keep the Emulators zone as rollback, gate the legacy node on a + completion flag) was implemented and passed all gates (build/lint/jest 2055). + - The mandatory 5-agent rubber-duck review **caught a release blocker** (GPT‑5.4 + GPT‑5.5 REJECT; + Opus 4.6/4.7 missed it). **Verified directly in code:** `emulatorConfiguration.isEmulator` is + **overloaded** — it is the **storage-zone selector** in connect/rename/delete/move/ + update-credentials/update-connection-string paths and in `DocumentDBClusterItem` + (`isEmulator ? Emulators : Clusters`), *and* `connectToClient.ts:25` **requires** + `isEmulator && disableEmulatorSecurity` for local TLS-allow-invalid. So a migrated connection + living in the `Clusters` zone cannot be made correct: keep `isEmulator=true` → all operations + look it up in the **wrong zone** (broken connect/delete/rename); set `isEmulator=false` → + **TLS-allow-invalid breaks** (can't reach the self-signed local server). The completion flag would + then hide the working originals → effectively unreachable. + - Citations: `src/commands/removeConnection/removeConnection.ts:81`, + `src/commands/connections-view/moveItems/moveItems.ts:129`, + `src/commands/updateCredentials/updateCredentials.ts:53`, + `src/commands/updateConnectionString/updateConnectionString.ts:42`, + `src/commands/connections-view/renameConnection/renameConnection.ts:24`, + `src/tree/connections-view/DocumentDBClusterItem.ts:61,101,173`, + `src/documentdb/connectToClient.ts:25`. + - Secondary findings (also valid): `getAll()` triggers storage bootstrap **cleanup that iterates + the Emulators zone** (weakens the "untouched rollback" guarantee); a snapshot **race** if emulator + data changes during the migration window; corrupt/folder items skipped by the storage wrapper + become invisible once the node is gated off. + - **Conclusion:** P2‑1 has a hard **prerequisite (P2‑0)** — decouple *storage-zone selection* from + `emulatorConfiguration.isEmulator` (add an explicit `storageZone`/`connectionType` on the + connection model and route all operations by it; make TLS-allow-invalid depend on + `disableEmulatorSecurity` alone). This is essentially the **§7** "move emulator/TLS handling out + of a dedicated zone" work, and P2‑2 (TLS wizard) shares the same root cause. Reverted the cut; + branch left clean. + +- _2026-06-26_: **P2‑0 decoupling + P2‑1 migration — DONE (3-round 5-agent review, consensus on correctness).** + - **P2‑0 (decouple zone from `isEmulator`):** added `storageZone?: StorageZone` to + `ConnectionClusterModel` + a `resolveStorageZone(cluster)` helper (prefers explicit zone, falls + back to the old `isEmulator` inference for safety). Stamped `storageZone` at the 3 construction + sites (`ConnectionsBranchDataProvider`→Clusters, `FolderItem`→`_connectionType`, + `LocalEmulatorsItem`→Emulators) and routed every zone decision through the helper + (`DocumentDBClusterItem` ×3, `removeConnection`, `moveItems`, rename/updateCredentials/ + updateConnectionString wizards). `isEmulator` is kept ONLY for behaviour (TLS/timeouts/icons). + +`resolveStorageZone` unit tests. + - **P2‑1 (migration), now correct on the decoupled arch:** copies keep `isEmulator:true` for TLS + and are rendered by `FolderItem` with `storageZone:Clusters`, so all operations route to Clusters. + - **3 review rounds (GPT‑5.4/5.5 xhigh, Opus 4.6/4.7/4.8 max):** + - R1 → caught the architectural blocker (above) → led to P2‑0. + - R2 on P2‑0+P2‑1 → **blocker resolved (5/5)**; found a partial-retry **BLOCKER** (overwrite could + revert user edits) + a URI-handler **MAJOR** (deep-link saves to the hidden Emulators zone). + Fixed: migration is now **create-if-missing** (never overwrites); URI handler routes new local + connections to Clusters once retired. + - R3 on the fixes → **unanimous the core is correct & data-safe; blocker stays resolved; no + regression.** Applied the reviewers' remaining hardening: a **reconciliation re-scan** before the + completion flag (closes the activation-window race), explicit `overwrite:false` (defense-in-depth), + an `isFolder` guard on the reused legacy folder, and telemetry refinement. + - **Known follow-up (pre-existing, documented):** the URI handler's deep-link **reveal** uses a flat + tree path, so auto-reveal of a connection *nested in a folder* (incl. a migrated one) can fail; the + connection is still found and navigable. Fix = folder-aware reveal via `buildFullTreePath` + + recursive `findNodeById` (tracked, not a regression). + - Gates green throughout: build · lint · jest (2058/2058, +3 tests) · l10n · prettier. + +- _2026-06-26_: **P2‑3 manual-wizard default port `10255`→`10260` — DONE (committed `441d9bda`).** + - `newLocalConnection/PromptConnectionTypeStep.ts`: the **DocumentDB** local branch now defaults to + `10260`; the **Cosmos Mongo‑RU** branch legitimately keeps `10255` (its real emulator port). + `PromptPortStep.ts` default is now experience-aware. Resolves the §13.5 "must fix before ship". + - Gates green: build · lint · jest · l10n. + +- _2026-06-26_: **P2‑2 TLS-exception wizard (§7) — DONE (3-round 5-agent review, consensus on correctness).** + - **Decouple TLS from `isEmulator`:** TLS-allow-invalid is now keyed off + `emulatorConfiguration.disableEmulatorSecurity` **alone** at all five option-builder sites + (`connectToClient`, `NativeAuthHandler`, `MicrosoftEntraIDAuthHandler`, `PlaygroundEvaluator`, + `ShellSessionManager`); the fail-fast `serverSelectionTimeoutMS=4000` and the `ClustersClient` + friendly-error messages were broadened from `isEmulator` to `isEmulator || disableEmulatorSecurity`. + All 5 reviewers confirmed this weakens **no** existing connection (only emulator paths set the flag). + Tree UX (`DocumentDBClusterItem`) keys its TLS description/tooltip off `disableEmulatorSecurity`. + - **Single source of truth canonicalizer:** new `tlsException.ts` (`canonicalizeTlsException`, + `stripTlsBypassParams`, `areAllHostsLocal`, `resolveAllowInvalidCertificates`). At **write time** it + strips every TLS-bypass URL param (`tls/sslAllowInvalidCertificates`, `tlsInsecure`, + `tls/sslAllowInvalidHostnames`, **and** `rejectUnauthorized` — inverse semantics, case-insensitive) + from the **stored** string and host-gates the exception, so the wizard/deep-link/update can never + create an accidental allow-invalid exception for a public host. Applied at all four write paths + (PromptConnectionStringStep, newConnection/ExecuteStep, updateConnectionString/ExecuteStep, + vscodeUriHandler). + - **Host classifier hardening (§7.1):** `isLocalOrPrivateHost` now IDNA-normalizes the host + (`normalizeHostForClassification`: maps the Unicode full-stop homographs U+3002/U+FF0E/U+FF61 → `.`, + then `domainToASCII`) so a public domain (e.g. `example。com`, which DNS resolves as `example.com`) + can't masquerade as a single-word local host. + - **Hybrid runtime policy:** `resolveAllowInvalidCertificates(disableEmulatorSecurity, cs)` returns + `true` only when `disableEmulatorSecurity && areAllHostsLocal(cs)`, else `undefined` (**never** + `false`). The 5 builders honor the stored flag **only for local/private hosts**; for a public host a + bare orphaned flag is **not** activated (so an old connection whose `tlsAllowInvalidCertificates` + param was later edited out can't silently disable validation), while an explicit URL param is still + honored by the driver (a self-hosted DB on a public hostname keeps working). + - **7 review rounds (GPT‑5.4/5.5 xhigh, Opus 4.6/4.7/4.8 max):** + - R1 → confirmed the decoupling is safe; found a connection-string second-source-of-truth, a + mixed-seed-list `.some` gap, the EntraID handler missing the flag, and `ClustersClient`/timeouts + still keyed off `isEmulator`. Fixed via the shared canonicalizer + `.every` gating. + - R2/R3 → caught + fixed a **latching BLOCKER** (the flag only ever *upgraded*), the **hostname-bypass** + strip gap, and a shell **`isEmulator` mislabel**; both ExecuteSteps now authoritatively host-gate. + - R4/R5 → caught the **Unicode-dot homograph** classifier bypass (fixed via IDNA normalization) and a + `rejectUnauthorized` hygiene gap (now stripped). A runtime "force-validate public hosts" attempt was + explored, then **rejected by the product owner** (it broke self-hosted public-host DBs and blocked the + future §7.3 public-exception dialog). + - R6/R7 → caught the **orphaned-flag** edge (a pre-existing public connection whose bypass param was + edited out keeps an inert flag the decoupling would activate) → resolved with the **hybrid runtime + policy** above, which honors explicit params but not bare flags on public hosts. + - R8 → **consistency pass**: extended the same host-gate (`resolveAllowInvalidCertificates`) to every + remaining flag-driven runtime surface — the 4s fail-fast `serverSelectionTimeoutMS` + (NativeAuthHandler/PlaygroundEvaluator/ShellSessionManager), the `ClustersClient` "local instance" + friendly-error copy, and the `DocumentDBClusterItem` "⚠ TLS/SSL Disabled" tree badge/tooltip — so an + orphaned public-host flag is now **fully inert** (no allow-invalid, no fast-fail, no mislabel). + - **§7.3 connection edit dialog deferred** — the design itself tracks it as a separate issue. + - Gates green: build · lint · jest (2135/2135) · l10n · prettier · production webpack. + +- _2026-06-30_: **P1‑4 Advanced panel (§5.2) — DONE (4-round 5-agent review, unanimous consensus).** + - **Feature:** collapsible FluentUI Advanced accordion on the review screen — custom **host port** + (feeds the explicit-port branch of P0‑2: a conflict errors, never auto-relocates), custom + **username/password**, **image tag**, and a **Load sample data** toggle. A new + `advancedOptionsSchema` (zod) validates on the wire; the summary + review cards reflect the + effective port/image/credentials; a failed provision gets an **Edit settings** button back to the + form. New `AdvancedQuickStartOptions`, `resolveQuickStartImage(tag)`, `StageEvent.boundPort`. + - **Security (creds off the host shell, §8.2):** sample-data seeding runs the image's init script via + `ContainerRuntime.execShellInContainer`, which references `"$USERNAME"`/`"$PASSWORD"` from the + **container's own** env (set by the `--env-file` at run) inside a **`ShellQuoting.Strong`**-quoted + `sh -c`. Verified end-to-end (WSL bash + lib trace + 5 agents): the host shell never expands the + refs (single-quoted on bash, escaped-double-quoted on cmd; cmd ignores `$`), so credentials never + hit the host argv/process list on **either** platform. This let the earlier `%`-in-password band-aid + be removed (creds are validated control-char-only now — the env-file newline-injection vector stays + blocked at zod + `writeEnvFile`; `%` round-trips safely via the env-file + percent-encoded conn + string). _A first cut used the default array-arg quoting (`Escape`), which leaks/empties the refs on + POSIX and word-splits on Windows — caught by the review and fixed to `Strong`._ + - **Data safety:** the reuse decision is now keyed on **stored credentials existing** (live + SecretStorage), not the in-memory `Missing` flag, so re-running setup can **never** silently wipe a + reusable data volume (e.g. after a window reload + external container removal). A fresh, + volume-wiping provision is strictly the explicit **Delete**-then-recreate path. + - **Recreate fidelity:** `InstanceMetadata.imageRef` + a durable `globalState` record + (`documentdb.quickstart.imageRef`, written on provision, **backfilled on reconcile/adopt**, cleared + on Delete) so a recreate — even across a reload — reuses the **original** image, not `latest` + (the "image is kept" promise is now true). `getDockerStatus` surfaces a `willReuse` flag computed + from the **same** predicate `provision()` uses, and the webview derives `isRecreate` strictly from + it — so the credential/image inputs are hidden (and the summary relabels "Reused/Kept from the + existing instance") whenever, and only when, the service will actually reuse. + - **Also:** server-side both-or-neither credential `.refine()` (parity with the client); whitespace + trim consistent client↔zod↔service; custom port preserved in the success message/conn string via a + `chosenPort` inspect fallback; telemetry stays booleans-only (`customPort/customCreds/customImage/ + sampleData`). + - **Review (GPT‑5.4/5.5 xhigh, Opus 4.6/4.7/4.8 max):** R1 → env-file newline + client/server + validation gaps. R2 → recreate image-tag loss + whitespace divergence + hardcoded review-card port. + R3 → **the `Escape`→`Strong` seed-quoting fix** (independently reproduced by 3 agents) + durable + reuse/UI-divergence blockers (GPT‑5.4/5.5/Opus‑4.8). R4 → durable `imageRef` + `willReuse` parity + landed; GPT‑5.4's last two refinements (reconcile backfill, `isRecreate` strictly `= willReuse`) + applied and re-confirmed → **unanimous APPROVE**. + - Gates green: build · lint · jest (2139/2139) · l10n · prettier. + +- _2026-06-30_: **P3‑4 Accessibility (v1.1) — DONE (committed `8a08c2c3`, 3-agent review).** + - Advanced inputs now surface errors via FluentUI `Field` `validationState`/`validationMessage` + (programmatically associated, `aria-invalid`) instead of one detached message; a polite live + region streams the current provisioning stage; failed / Docker-not-ready states are announced; + the stage list is `role="list"`/`listitem` with natural row `aria-label`s and decorative icons + hidden; on failure the active stage flips to error (no stuck "in progress"); focus moves to the + primary result action when provisioning ends. WCAG 2.4.3 / 3.3.1 / 1.1.1 / 4.1.3. + - Gates green: build · lint · jest (2139/2139) · l10n · prettier. + +- _2026-06-30_: **P3‑5 Readiness on-timeout actions (§9.1, v1.1) — DONE (3-round 5-agent review).** + - **Behavior:** a readiness timeout no longer tears the container down. It is KEPT running (it may + just be slow to initialize) and the webview offers **Wait longer** (re-probe the same container + for another window — no re-pull), **View Docker output** (the §9.1 "Logs"), and **Start over** + (discard; a fresh attempt's half-initialized volume is wiped, a reusing attempt's real data is + kept). New `ReadinessTimeoutError`, `PendingReadiness` retained state, `resumeReadiness` generator, + `discardTimedOutInstance`, `waitLonger` subscription + `discardTimedOut` mutation, and a + `canResumeReadiness` status flag so reopening the panel rehydrates the recovery actions. + - **Data safety (the hard part):** the provision `try/catch/finally` was refactored to extract a + shared non-yielding `finalizeReadyInstance` while preserving the `setStatus(Running)→success=true` + ordering (so an unsubscribe after the terminal event can never tear down a healthy container). + Terminal events are **buffered and yielded after `finally`** so the flags are clean before the + webview shows Wait longer / Retry (no fast-click race). A webview stream-generation guard ignores + trailing callbacks from a superseded/cancelled stream. + - **Reviewed 3 rounds (GPT‑5.4/5.5 xhigh, Opus 4.6/4.7/4.8 max):** R1 → ~12 findings (races, cancel + handling, resumable-state, telemetry, log streaming, focus) → hardening pass. R2 → **unanimous 5**; + caught a **data-loss BLOCKER** (a well-meaning `reconcile` volume-removal that `!stored` doesn't + prove is disposable — reverted to container-only cleanup). R3 (focused) → confirmed; the one + late-callback concern was **empirically disproven** (the webview tRPC client unregisters its + handler synchronously on unsubscribe, `vscodeLink.ts:187-199`) and additionally guarded. + - **Known limitation (deferred to v1.2, all reviewers non-blocking):** after a window reload, + `reconcile()` adopts a *reusing* timed-out container as `Running` without re-probing readiness — + it could briefly show a not-yet-ready connection as healthy (no data loss; recoverable via + Restart/Delete). Fix = a bounded `ping` before promoting to `Running`, or a durable pending marker. + - Gates green: build · lint · jest (2139/2139) · l10n · prettier. \ No newline at end of file diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 4873e4e11..fb989672e 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -103,6 +103,7 @@ "[StreamingWriter] Throttle: wrote {0} docs, {1} remaining in batch": "[StreamingWriter] Throttle: wrote {0} docs, {1} remaining in batch", "[StreamingWriter] Writing {0} documents to target (may take a moment)...": "[StreamingWriter] Writing {0} documents to target (may take a moment)...", "{0}\n\nThe query did not complete successfully. Performance metrics shown are partial and measured up to the failure point.": "{0}\n\nThe query did not complete successfully. Performance metrics shown are partial and measured up to the failure point.", + "{0} (auto)": "{0} (auto)", "{0} (Emulator)": "{0} (Emulator)", "{0} completed successfully": "{0} completed successfully", "{0} connections": "{0} connections", @@ -123,6 +124,7 @@ "{0} tenants available ({1} signed in)": "{0} tenants available ({1} signed in)", "{0} was stopped": "{0} was stopped", "{0}: v{1}": "{0}: v{1}", + "{0}…": "{0}…", "{0}/{1} documents": "{0}/{1} documents", "{0}s": "{0}s", "{countMany} documents have been deleted.": "{countMany} documents have been deleted.", @@ -131,6 +133,12 @@ "{experienceName} Emulator": "{experienceName} Emulator", "**No public IP or FQDN available for direct connection.**": "**No public IP or FQDN available for direct connection.**", "/ (Root)": "/ (Root)", + "• Copy Connection String — use it from a Query Playground, your app, or mongosh (localhost:{0}).": "• Copy Connection String — use it from a Query Playground, your app, or mongosh (localhost:{0}).", + "• If you use a corporate proxy, check that ghcr.io is reachable.": "• If you use a corporate proxy, check that ghcr.io is reachable.", + "• Install Docker Desktop, then reopen Quick Start.": "• Install Docker Desktop, then reopen Quick Start.", + "• Open Connection — browse your databases in the Connections view, under “DocumentDB Local”.": "• Open Connection — browse your databases in the Connections view, under “DocumentDB Local”.", + "• Start Docker Desktop and wait for it to report “running”.": "• Start Docker Desktop and wait for it to report “running”.", + "• The container keeps running after VS Code closes. Manage it with Stop / Restart / Delete in the Connections view.": "• The container keeps running after VS Code closes. Manage it with Stop / Restart / Delete in the Connections view.", "^C": "^C", "■ Task '{taskName}' was stopped. {message}": "■ Task '{taskName}' was stopped. {message}", "► Task '{taskName}' starting...": "► Task '{taskName}' starting...", @@ -169,6 +177,7 @@ "Abort entire operation on first write error. Recommended for safe data copy operations.": "Abort entire operation on first write error. Recommended for safe data copy operations.", "Abort on first error": "Abort on first error", "About (v{0})": "About (v{0})", + "Accept a self-signed or untrusted certificate. Only choose this for a host you trust — a “.local” or single-word name can also be managed corporate infrastructure.": "Accept a self-signed or untrusted certificate. Only choose this for a host you trust — a “.local” or single-word name can also be managed corporate infrastructure.", "Acceptable execution time": "Acceptable execution time", "Account information is incomplete.": "Account information is incomplete.", "Account Management Completed": "Account Management Completed", @@ -178,12 +187,14 @@ "Additional write and storage overhead for maintaining a new index.": "Additional write and storage overhead for maintaining a new index.", "Adjust Filters": "Adjust Filters", "Advanced": "Advanced", + "Advanced (optional)": "Advanced (optional)", "AI is analyzing…": "AI is analyzing…", "AI Performance Insights": "AI Performance Insights", "AI recommendations": "AI recommendations", "AI responses may be inaccurate": "AI responses may be inaccurate", "All {count} connections have been removed.": "All {count} connections have been removed.", "All available providers have been added already.": "All available providers have been added already.", + "Allow invalid certificates": "Allow invalid certificates", "Always upload": "Always upload", "An element with the following id already exists: {id}": "An element with the following id already exists: {id}", "An error has occurred. Check output window for more details.": "An error has occurred. Check output window for more details.", @@ -214,6 +225,8 @@ "Authentication is required to run this action.": "Authentication is required to run this action.", "Authentication is required to use this migration provider.": "Authentication is required to use this migration provider.", "Authentication: {0} | Database: {1}": "Authentication: {0} | Database: {1}", + "auto": "auto", + "Auto-generated, stored securely": "Auto-generated, stored securely", "Azure account added successfully.": "Azure account added successfully.", "Azure account management failed: {0}": "Azure account management failed: {0}", "Azure account management was cancelled by user.": "Azure account management was cancelled by user.", @@ -251,6 +264,8 @@ "Change display batch size (currently {0}) in settings": "Change display batch size (currently {0}) in settings", "Change page size": "Change page size", "Changelog": "Changelog", + "Checking Docker": "Checking Docker", + "Checking Docker…": "Checking Docker…", "Checking for conflicts…": "Checking for conflicts…", "Choose a cluster…": "Choose a cluster…", "Choose a different folder": "Choose a different folder", @@ -268,6 +283,7 @@ "Click to view resource": "Click to view resource", "Clipboard does not contain a recognizable find() query.": "Clipboard does not contain a recognizable find() query.", "Clipboard is empty.": "Clipboard is empty.", + "Close": "Close", "Cluster metadata not initialized. Client may not be properly connected.": "Cluster metadata not initialized. Client may not be properly connected.", "Cluster support unknown $(info)": "Cluster support unknown $(info)", "collection \"{0}\"": "collection \"{0}\"", @@ -308,6 +324,7 @@ "connection \"{0}\"": "connection \"{0}\"", "Connection String": "Connection String", "Connection string cannot be empty.": "Connection string cannot be empty.", + "Connection string copied to clipboard.": "Connection string copied to clipboard.", "Connection string is not set": "Connection string is not set", "Connection to \"{cluster}\" was cancelled.": "Connection to \"{cluster}\" was cancelled.", "Connection updated successfully.": "Connection updated successfully.", @@ -318,6 +335,7 @@ "Copied to clipboard": "Copied to clipboard", "Copy": "Copy", "Copy \"{sourceCollection}\" from \"{sourceDatabase}\" to \"{targetDatabase}/{targetCollection}\"": "Copy \"{sourceCollection}\" from \"{sourceDatabase}\" to \"{targetDatabase}/{targetCollection}\"", + "Copy Connection String": "Copy Connection String", "Copy current query to clipboard": "Copy current query to clipboard", "Copy index definitions from source collection?": "Copy index definitions from source collection?", "Copy index definitions from source to target collection.": "Copy index definitions from source to target collection.", @@ -359,11 +377,15 @@ "Created new folder: {folderName} in folder with ID {parentFolderId}": "Created new folder: {folderName} in folder with ID {parentFolderId}", "Creating \"{nodeName}\"…": "Creating \"{nodeName}\"…", "Creating {0}...": "Creating {0}...", + "Creating container": "Creating container", "Creating index \"{indexName}\" on collection: {collection}": "Creating index \"{indexName}\" on collection: {collection}", "Creating resource group \"{0}\" in location \"{1}\"...": "Creating resource group \"{0}\" in location \"{1}\"...", "Creating storage account \"{0}\" in location \"{1}\" with sku \"{2}\"...": "Creating storage account \"{0}\" in location \"{1}\" with sku \"{2}\"...", "Creating user-assigned identity \"{0}\" in location \"{1}\"\"...": "Creating user-assigned identity \"{0}\" in location \"{1}\"\"...", + "Credentials": "Credentials", "Credentials updated successfully.": "Credentials updated successfully.", + "Custom, stored securely": "Custom, stored securely", + "Data": "Data", "Data shown was correct": "Data shown was correct", "Data shown was incorrect": "Data shown was incorrect", "Database": "Database", @@ -373,6 +395,9 @@ "Database name is required when collection is specified": "Database name is required when collection is specified", "Database name is required.": "Database name is required.", "Database: \"{databaseName}\"": "Database: \"{databaseName}\"", + "Default “{0}”": "Default “{0}”", + "Default {0}": "Default {0}", + "Default: auto-generated": "Default: auto-generated", "Delete": "Delete", "Delete \"{connectionName}\"?": "Delete \"{connectionName}\"?", "Delete \"{nodeName}\"?": "Delete \"{nodeName}\"?", @@ -380,6 +405,7 @@ "Delete {count} documents?": "Delete {count} documents?", "Delete collection \"{collectionId}\" and its contents?": "Delete collection \"{collectionId}\" and its contents?", "Delete database \"{databaseId}\" and its contents?": "Delete database \"{databaseId}\" and its contents?", + "Delete DocumentDB Local container?": "Delete DocumentDB Local container?", "Delete Folder": "Delete Folder", "Delete folder \"{folderName}\"?": "Delete folder \"{folderName}\"?", "Delete index \"{indexName}\" from collection \"{collectionName}\"?": "Delete index \"{indexName}\" from collection \"{collectionName}\"?", @@ -404,6 +430,11 @@ "Do not rely on case to distinguish between databases. For example, you cannot use two databases with names like, salesData and SalesData.": "Do not rely on case to distinguish between databases. For example, you cannot use two databases with names like, salesData and SalesData.", "Do not save credentials.": "Do not save credentials.", "Do you want to include the password in the connection string?": "Do you want to include the password in the connection string?", + "Docker": "Docker", + "Docker CLI": "Docker CLI", + "Docker daemon": "Docker daemon", + "Docker is required": "Docker is required", + "Docker is required and is not ready. Review the checks below.": "Docker is required and is not ready. Review the checks below.", "Document already exists (skipped)": "Document already exists (skipped)", "Document Editor: Edit the document in JSON format": "Document Editor: Edit the document in JSON format", "Document must be an object.": "Document must be an object.", @@ -412,7 +443,12 @@ "DocumentDB Documentation": "DocumentDB Documentation", "DocumentDB for VS Code has been updated. View the release notes?": "DocumentDB for VS Code has been updated. View the release notes?", "DocumentDB for VS Code is not signed in to Azure": "DocumentDB for VS Code is not signed in to Azure", + "DocumentDB is still initializing. Keep waiting, view the logs, or start over.": "DocumentDB is still initializing. Keep waiting, view the logs, or start over.", "DocumentDB Local": "DocumentDB Local", + "DocumentDB Local - Quick Start": "DocumentDB Local - Quick Start", + "DocumentDB Local container deleted.": "DocumentDB Local container deleted.", + "DocumentDB Local is ready. Next steps are shown below.": "DocumentDB Local is ready. Next steps are shown below.", + "DocumentDB Local is running.": "DocumentDB Local is running.", "DocumentDB Shell: {0}": "DocumentDB Shell: {0}", "DocumentDB: {0}@{1}/{2}": "DocumentDB: {0}@{1}/{2}", "DocumentDB: {0}/{1}": "DocumentDB: {0}/{1}", @@ -423,6 +459,8 @@ "Don't Ask Again": "Don't Ask Again", "Don't upload": "Don't upload", "Don't warn again": "Don't warn again", + "done": "done", + "Done": "Done", "Double-click to open the collection view": "Double-click to open the collection view", "Drafting…": "Drafting…", "Drop Index…": "Drop Index…", @@ -434,15 +472,18 @@ "e.g., DocumentDB, Environment, Project": "e.g., DocumentDB, Environment, Project", "Each line will be run independently.": "Each line will be run independently.", "Edit selected document": "Edit selected document", + "Edit settings": "Edit settings", "Efficient sorting": "Efficient sorting", "Element with id of {rootId} not found.": "Element with id of {rootId} not found.", "empty": "empty", + "Enable TLS (default)": "Enable TLS (default)", "Enable TLS/SSL (Default)": "Enable TLS/SSL (Default)", "Enforce TLS/SSL checks for a secure connection.": "Enforce TLS/SSL checks for a secure connection.", "Enhanced Query Configuration\n(Projection, Sort, Skip, Limit)": "Enhanced Query Configuration\n(Projection, Sort, Skip, Limit)", "Ensuring target exists...": "Ensuring target exists...", "Enter a collection name.": "Enter a collection name.", "Enter a database name.": "Enter a database name.", + "Enter both a username and a password, or leave both blank to auto-generate.": "Enter both a username and a password, or leave both blank to auto-generate.", "Enter folder name": "Enter folder name", "Enter new folder name": "Enter new folder name", "Enter the Azure VM tag key used for discovering DocumentDB instances.": "Enter the Azure VM tag key used for discovering DocumentDB instances.", @@ -458,6 +499,7 @@ "Enter the username for {experience}": "Enter the username for {experience}", "Entra ID for Azure DocumentDB": "Entra ID for Azure DocumentDB", "Error": "Error", + "Error · click for details": "Error · click for details", "Error creating index: {error}": "Error creating index: {error}", "Error creating resource: {0}": "Error creating resource: {0}", "Error deleting selected documents": "Error deleting selected documents", @@ -507,6 +549,7 @@ "Exporting…": "Exporting…", "Extension dependency with id \"{0}\" must be updated.": "Extension dependency with id \"{0}\" must be updated.", "Extension Documentation": "Extension Documentation", + "failed": "failed", "Failed to {action} index \"{indexName}\": {error} [{durationMs}ms]": "Failed to {action} index \"{indexName}\": {error} [{durationMs}ms]", "Failed to abort transaction: {0}": "Failed to abort transaction: {0}", "Failed to commit transaction: {0}": "Failed to commit transaction: {0}", @@ -586,11 +629,13 @@ "Find Query": "Find Query", "Finished importing": "Finished importing", "Folder name cannot be empty": "Folder name cannot be empty", + "Found": "Found", "Full collection scan": "Full collection scan", "Generate": "Generate", "Generate new _id values": "Generate new _id values", "Generate query with AI": "Generate query with AI", "Generating recommendation…": "Generating recommendation…", + "Get a working local DocumentDB instance in one click. No terminal commands needed.": "Get a working local DocumentDB instance in one click. No terminal commands needed.", "Get AI Performance Insights": "Get AI Performance Insights", "Get personalized recommendations to optimize your query performance. AI will analyze your cluster configuration, index usage, execution plan, and more to suggest specific improvements.": "Get personalized recommendations to optimize your query performance. AI will analyze your cluster configuration, index usage, execution plan, and more to suggest specific improvements.", "GitHub Copilot is not available. Please install the GitHub Copilot extension and ensure you have an active subscription.": "GitHub Copilot is not available. Please install the GitHub Copilot extension and ensure you have an active subscription.", @@ -614,6 +659,7 @@ "Host": "Host", "How do you want to connect?": "How do you want to connect?", "How should conflicts be handled during the copy operation?": "How should conflicts be handled during the copy operation?", + "How to fix": "How to fix", "How to process your multi-line text?": "How to process your multi-line text?", "How would you rate Query Insights?": "How would you rate Query Insights?", "I have read and agree to the ": "I have read and agree to the ", @@ -624,6 +670,9 @@ "I want to connect using a connection string.": "I want to connect using a connection string.", "Ignore": "Ignore", "Ignoring the following files that do not match the \"*.json\" file name pattern:": "Ignoring the following files that do not match the \"*.json\" file name pattern:", + "Image": "Image", + "Image tag": "Image tag", + "Image tag may contain only letters, numbers, dots, dashes, and underscores.": "Image tag may contain only letters, numbers, dots, dashes, and underscores.", "Import": "Import", "Import canceled. Inserted {0} document(s) before cancellation.": "Import canceled. Inserted {0} document(s) before cancellation.", "Import completed with errors.": "Import completed with errors.", @@ -639,6 +688,7 @@ "Importing documents…": "Importing documents…", "Importing…": "Importing…", "Improved my query performance": "Improved my query performance", + "in progress": "in progress", "In-Memory Sort": "In-Memory Sort", "In-memory sort required": "In-memory sort required", "Index \"{indexName}\" {action} successfully [{durationMs}ms]": "Index \"{indexName}\" {action} successfully [{durationMs}ms]", @@ -672,6 +722,7 @@ "Initializing…": "Initializing…", "Inserted {0} document(s).": "Inserted {0} document(s).", "Install Azure Account Extension...": "Install Azure Account Extension...", + "Install Docker": "Install Docker", "Internal error: connectionString must be defined.": "Internal error: connectionString must be defined.", "Internal error: connectionString, port, and api must be defined.": "Internal error: connectionString, port, and api must be defined.", "Internal error: Expected value to be neither null nor undefined": "Internal error: Expected value to be neither null nor undefined", @@ -707,6 +758,8 @@ "JSON View": "JSON View", "Keep-alive timeout exceeded": "Keep-alive timeout exceeded", "Keep-alive timeout exceeded: stream has been running for {0} seconds (limit: {1} seconds)": "Keep-alive timeout exceeded: stream has been running for {0} seconds (limit: {1} seconds)", + "Keeps running after VS Code closes": "Keeps running after VS Code closes", + "Kept from the existing instance": "Kept from the existing instance", "Key Definition": "Key Definition", "Keys Examined": "Keys Examined", "Large Collection Copy Operation": "Large Collection Copy Operation", @@ -720,11 +773,14 @@ "Learn more about local connections.": "Learn more about local connections.", "Learn more about the utility model used.": "Learn more about the utility model used.", "Learn more…": "Learn more…", + "Leave any field blank to keep the automatic default.": "Leave any field blank to keep the automatic default.", "Length must be greater than 1": "Length must be greater than 1", "Level up": "Level up", + "Lifetime": "Lifetime", "Limit": "Limit", "Lines will be joined into a single expression and executed.": "Lines will be joined into a single expression and executed.", "Load More...": "Load More...", + "Load sample data": "Load sample data", "Loaded {0} document(s) from \"{1}\"": "Loaded {0} document(s) from \"{1}\"", "Loading \"{0}\"...": "Loading \"{0}\"...", "Loading Azure Accounts Used for Service Discovery…": "Loading Azure Accounts Used for Service Discovery…", @@ -742,6 +798,7 @@ "Loading Tenants…": "Loading Tenants…", "Loading Virtual Machines…": "Loading Virtual Machines…", "Loading...": "Loading...", + "Local Quick Start runs DocumentDB on your machine using Docker. The extension does not install Docker for you.": "Local Quick Start runs DocumentDB on your machine using Docker. The extension does not install Docker for you.", "Location": "Location", "Low efficiency ratio": "Low efficiency ratio", "Low filter selectivity": "Low filter selectivity", @@ -754,6 +811,8 @@ "MEDIUM PRIORITY": "MEDIUM PRIORITY", "Microsoft will process the feedback data you submit on behalf of your organization in accordance with the Data Protection Addendum between your organization and Microsoft.": "Microsoft will process the feedback data you submit on behalf of your organization in accordance with the Data Protection Addendum between your organization and Microsoft.", "Migration Providers: {0}": "Migration Providers: {0}", + "missing": "missing", + "Missing · click to recreate": "Missing · click to recreate", "Missing important information": "Missing important information", "Mode: {0}": "Mode: {0}", "Moderate efficiency ratio": "Moderate efficiency ratio", @@ -779,6 +838,7 @@ "New Connection…": "New Connection…", "New Local Connection": "New Local Connection", "New Local Connection…": "New Local Connection…", + "Next steps": "Next steps", "No": "No", "No Action": "No Action", "No additional cost for most GitHub Copilot subscribers.": "No additional cost for most GitHub Copilot subscribers.", @@ -829,6 +889,8 @@ "None": "None", "None (collection scan)": "None (collection scan)", "Not connected": "Not connected", + "Not found": "Not found", + "Not ready": "Not ready", "Not signed in to {0}. Please authenticate first.": "Not signed in to {0}. Please authenticate first.", "Note: This confirmation type can be configured in the extension settings.": "Note: This confirmation type can be configured in the extension settings.", "Note: You can disable these URL handling confirmations in the extension settings.": "Note: You can disable these URL handling confirmations in the extension settings.", @@ -840,6 +902,7 @@ "Open batch size setting": "Open batch size setting", "Open Collection": "Open Collection", "Open collection \"{0}.{1}\" in Collection View": "Open collection \"{0}.{1}\" in Collection View", + "Open Connection": "Open Connection", "Open current query in a Query Playground": "Open current query in a Query Playground", "Open current query in an Interactive Shell": "Open current query in an Interactive Shell", "Open in Playground": "Open in Playground", @@ -858,15 +921,22 @@ "Overwrite existing documents": "Overwrite existing documents", "Overwrite existing documents that share the same _id; other write errors will abort the operation.": "Overwrite existing documents that share the same _id; other write errors will abort the operation.", "Parsing file {0}: {1}": "Parsing file {0}: {1}", + "Password": "Password", "Password cannot be empty": "Password cannot be empty", "Password contains characters that cannot be safely encoded.": "Password contains characters that cannot be safely encoded.", + "Password copied to clipboard.": "Password copied to clipboard.", "Password for {username_at_resource}": "Password for {username_at_resource}", + "Password must be 256 characters or fewer.": "Password must be 256 characters or fewer.", + "Password must not contain control characters.": "Password must not contain control characters.", "Paste a find query from clipboard into the editors": "Paste a find query from clipboard into the editors", "Paste Collection": "Paste Collection", "Paste Query": "Paste Query", "Pasting…": "Pasting…", + "pending": "pending", "Performance Rating": "Performance Rating", + "Persistent volume": "Persistent volume", "Pick \"{number}\" to confirm and continue.": "Pick \"{number}\" to confirm and continue.", + "Platform": "Platform", "Playground": "Playground", "Please authenticate first by expanding the tree item of the selected cluster.": "Please authenticate first by expanding the tree item of the selected cluster.", "Please confirm by re-entering the previous value.": "Please confirm by re-entering the previous value.", @@ -880,6 +950,8 @@ "Please provide the username for \"{resource}\":": "Please provide the username for \"{resource}\":", "Please stop these tasks first before proceeding.": "Please stop these tasks first before proceeding.", "Poor": "Poor", + "Port": "Port", + "Port must be a whole number between 1024 and 65535.": "Port must be a whole number between 1024 and 65535.", "Port number is required": "Port number is required", "Port number must be a number": "Port number must be a number", "Port number must be between 1 and 65535": "Port number must be between 1 and 65535", @@ -893,6 +965,8 @@ "Project": "Project", "Project: Specify which fields to include or exclude": "Project: Specify which fields to include or exclude", "Provider \"{0}\" does not have resource type \"{1}\".": "Provider \"{0}\" does not have resource type \"{1}\".", + "Provisioning… · localhost:10260": "Provisioning… · localhost:10260", + "Pulling official image": "Pulling official image", "Qualified Name": "Qualified Name", "Query completed in {0}ms.\n\nThis is acceptable for most use cases, though optimization could improve responsiveness.": "Query completed in {0}ms.\n\nThis is acceptable for most use cases, though optimization could improve responsiveness.", "Query completed in {0}ms.\n\nThis is excellent performance and provides a responsive user experience.": "Query completed in {0}ms.\n\nThis is excellent performance and provides a responsive user experience.", @@ -922,6 +996,9 @@ "Query took {0}ms to complete.\n\nThis may impact user experience.\n\nConsider adding indexes or optimizing your query structure.": "Query took {0}ms to complete.\n\nThis may impact user experience.\n\nConsider adding indexes or optimizing your query structure.", "Query took {0}s to complete.\n\nThis significantly impacts performance and user experience.\n\nImmediate optimization is recommended.": "Query took {0}s to complete.\n\nThis significantly impacts performance and user experience.\n\nImmediate optimization is recommended.", "Quick Actions": "Quick Actions", + "Quick Start — Install & try DocumentDB locally": "Quick Start — Install & try DocumentDB locally", + "Reachable": "Reachable", + "Ready": "Ready", "Receiving response.": "Receiving response.", "Receiving response…": "Receiving response…", "Recommendation: Create Index": "Recommendation: Create Index", @@ -932,6 +1009,7 @@ "Recommended Index": "Recommended Index", "Reconnect now with the updated credentials": "Reconnect now with the updated credentials", "Reconnecting...": "Reconnecting...", + "Recreating reuses the existing data volume, so the original credentials and image are kept.": "Recreating reuses the existing data volume, so the original credentials and image are kept.", "Refresh": "Refresh", "Refresh current view": "Refresh current view", "Refreshing Azure discovery tree…": "Refreshing Azure discovery tree…", @@ -959,6 +1037,7 @@ "Retry": "Retry", "Retry Error: {error}": "Retry Error: {error}", "Returns majority of collection": "Returns majority of collection", + "Reused from the existing instance": "Reused from the existing instance", "Reusing active connection for \"{cluster}\".": "Reusing active connection for \"{cluster}\".", "Revisit connection details and try again.": "Revisit connection details and try again.", "Right-click a cluster, database, or collection in the DocumentDB panel to open an interactive shell.": "Right-click a cluster, database, or collection in the DocumentDB panel to open an interactive shell.", @@ -971,8 +1050,11 @@ "Run as Is": "Run as Is", "Run the entire file ({0}+Shift+Enter)": "Run the entire file ({0}+Shift+Enter)", "Run this block ({0}+Enter)": "Run this block ({0}+Enter)", + "running": "running", + "Running · localhost:{0}": "Running · localhost:{0}", "Running query…": "Running query…", "Running…": "Running…", + "Runs on": "Runs on", "Save": "Save", "Save credentials for future connections.": "Save credentials for future connections.", "Save credentials for future use?": "Save credentials for future use?", @@ -984,6 +1066,7 @@ "Schema scan complete: {0} fields discovered in \"{1}\".": "Schema scan complete: {0} fields discovered in \"{1}\".", "Security": "Security", "See output for more details.": "See output for more details.", + "See the details below.": "See the details below.", "Select {0}": "Select {0}", "Select a location for new resources.": "Select a location for new resources.", "Select a tenant for Microsoft Entra ID authentication": "Select a tenant for Microsoft Entra ID authentication", @@ -1007,7 +1090,11 @@ "Service Discovery": "Service Discovery", "Session ID is required": "Session ID is required", "sessionId is required for query optimization": "sessionId is required for query optimization", + "Setting up DocumentDB Local…": "Setting up DocumentDB Local…", "Settings:": "Settings:", + "Setup failed.": "Setup failed.", + "Setup failed. {0}": "Setup failed. {0}", + "Setup progress": "Setup progress", "Severe multikey expansion": "Severe multikey expansion", "Shard Key": "Shard Key", "SHARD_MERGE · {0} shards": "SHARD_MERGE · {0} shards", @@ -1047,16 +1134,26 @@ "Start a discussion": "Start a discussion", "Start Copy-and-Merge": "Start Copy-and-Merge", "Start Copy-and-Paste": "Start Copy-and-Paste", + "Start Docker Desktop": "Start Docker Desktop", + "Start DocumentDB Local": "Start DocumentDB Local", + "Start over": "Start over", "Started executable: \"{command}\". Connecting to host…": "Started executable: \"{command}\". Connecting to host…", "Starting Azure account management wizard": "Starting Azure account management wizard", "Starting Azure sign-in process…": "Starting Azure sign-in process…", + "Starting container": "Starting container", + "Starting Docker Desktop…": "Starting Docker Desktop…", "Starting executable: \"{command}\"": "Starting executable: \"{command}\"", "Starting export to: {filePath}": "Starting export to: {filePath}", "Starting import of {0} file(s) into collection \"{1}\"": "Starting import of {0} file(s) into collection \"{1}\"", "Starting sign-in to tenant: {0}": "Starting sign-in to tenant: {0}", + "Starting… · localhost:{0}": "Starting… · localhost:{0}", "Starts with mongodb:// or mongodb+srv://": "Starts with mongodb:// or mongodb+srv://", + "stopped": "stopped", + "Stopped": "Stopped", + "Stopped · localhost:{0}": "Stopped · localhost:{0}", "Stopping {0}": "Stopping {0}", "Stopping task...": "Stopping task...", + "Stopping… · localhost:{0}": "Stopping… · localhost:{0}", "Submit": "Submit", "Submit Feedback": "Submit Feedback", "Submitting...": "Submitting...", @@ -1119,6 +1216,9 @@ "The connection string will include the password": "The connection string will include the password", "The connection string will not include the password": "The connection string will not include the password", "The connection will now be opened in the Connections View.": "The connection will now be opened in the Connections View.", + "The container and its data volume will be permanently removed. All data, logs, and the auto-generated credentials will be lost. This cannot be undone — you can recreate a fresh instance any time with Quick Start.": "The container and its data volume will be permanently removed. All data, logs, and the auto-generated credentials will be lost. This cannot be undone — you can recreate a fresh instance any time with Quick Start.", + "The container is currently running. It will be stopped and permanently removed. All data, logs, and the auto-generated credentials will be lost. This cannot be undone — you can recreate a fresh instance any time with Quick Start.": "The container is currently running. It will be stopped and permanently removed. All data, logs, and the auto-generated credentials will be lost. This cannot be undone — you can recreate a fresh instance any time with Quick Start.", + "The container is running, but DocumentDB has not accepted connections yet. It may still be initializing — keep waiting, view the logs, or start over.": "The container is running, but DocumentDB has not accepted connections yet. It may still be initializing — keep waiting, view the logs, or start over.", "The custom cloud choice is not configured. Please configure the setting `{0}.{1}`.": "The custom cloud choice is not configured. Please configure the setting `{0}.{1}`.", "The database \"{0}\" already exists in the MongoDB Cluster \"{1}\".": "The database \"{0}\" already exists in the MongoDB Cluster \"{1}\".", "The database did not sort data in memory.\n\nResults came back in the right order naturally, either from the index or because no sort was requested.": "The database did not sort data in memory.\n\nResults came back in the right order naturally, either from the index or because no sort was requested.", @@ -1131,7 +1231,9 @@ "The database used a bitmap index to execute this query.\n\nBitmap indexes are an internal optimization that DocumentDB applies to low-cardinality fields (fields with few distinct values, such as booleans or status codes). They are space-efficient but less selective than B-tree indexes on high-cardinality fields.\n\nThis is expected behavior and does not indicate a problem. If query performance is a concern, consider filtering on a more selective field or using a compound index.": "The database used a bitmap index to execute this query.\n\nBitmap indexes are an internal optimization that DocumentDB applies to low-cardinality fields (fields with few distinct values, such as booleans or status codes). They are space-efficient but less selective than B-tree indexes on high-cardinality fields.\n\nThis is expected behavior and does not indicate a problem. If query performance is a concern, consider filtering on a more selective field or using a compound index.", "The default port: {defaultPort}": "The default port: {defaultPort}", "The default port: 10255": "The default port: 10255", + "The default port: 10260": "The default port: 10260", "The document with the _id \"{0}\" has been saved.": "The document with the _id \"{0}\" has been saved.", + "The DocumentDB Local instance changed in another window (now {0}). The view has been refreshed.": "The DocumentDB Local instance changed in another window (now {0}). The view has been refreshed.", "The entered value does not match the original.": "The entered value does not match the original.", "The existing connection has been selected in the Connections View.\n\nSelected connection name:\n\"{0}\"": "The existing connection has been selected in the Connections View.\n\nSelected connection name:\n\"{0}\"", "The export operation was canceled.": "The export operation was canceled.", @@ -1165,10 +1267,12 @@ "The worker for this cluster is busy executing a playground": "The worker for this cluster is busy executing a playground", "These signals help us improve, but more context in a discussion, issue report, or a direct message adds even more value. ": "These signals help us improve, but more context in a discussion, issue report, or a direct message adds even more value. ", "This cannot be undone.": "This cannot be undone.", + "This connection targets a local or private network host. TLS certificate validation:": "This connection targets a local or private network host. TLS certificate validation:", "This field is not set": "This field is not set", "This functionality requires installing the Azure Account extension.": "This functionality requires installing the Azure Account extension.", "This functionality requires updating the Azure Account extension to at least version \"{0}\".": "This functionality requires updating the Azure Account extension to at least version \"{0}\".", "This index on {0} is not being used and adds unnecessary overhead to write operations.": "This index on {0} is not being used and adds unnecessary overhead to write operations.", + "This machine (Docker)": "This machine (Docker)", "This operation is not supported as it would create a circular dependency and never terminate. Please select a different target collection or database.": "This operation is not supported as it would create a circular dependency and never terminate. Please select a different target collection or database.", "This operation is not supported.": "This operation is not supported.", "This operation will copy all documents from the source to the target collection. Large collections may take several minutes to complete.": "This operation will copy all documents from the source to the target collection. Large collections may take several minutes to complete.", @@ -1188,6 +1292,7 @@ "This will execute all statements in the file against the connected cluster.": "This will execute all statements in the file against the connected cluster.", "This will prevent the query planner from using this index.": "This will prevent the query planner from using this index.", "Tip: use .maxTimeMS() to increase the time limit for this query.": "Tip: use .maxTimeMS() to increase the time limit for this query.", + "TLS · self-signed": "TLS · self-signed", "TLS/SSL Disabled": "TLS/SSL Disabled", "TLS/SSL Enabled": "TLS/SSL Enabled", "To connect to Azure resources, you need to sign in to Azure accounts.": "To connect to Azure resources, you need to sign in to Azure accounts.", @@ -1196,6 +1301,7 @@ "Total time taken to execute the query on the server": "Total time taken to execute the query on the server", "Transforming Stage 2 response to UI format": "Transforming Stage 2 response to UI format", "Tree View": "Tree View", + "Troubleshooting": "Troubleshooting", "Try with Decoded Password": "Try with Decoded Password", "Type \"help\" for available commands.": "Type \"help\" for available commands.", "Type \"it\" for more": "Type \"it\" for more", @@ -1211,6 +1317,7 @@ "Unhide index?": "Unhide index?", "Unhide Index…": "Unhide Index…", "Unhiding…": "Unhiding…", + "unknown": "unknown", "Unknown command type: {type}": "Unknown command type: {type}", "Unknown conflict resolution strategy: {0}": "Unknown conflict resolution strategy: {0}", "Unknown constructor '{0}'. Expected a BSON constructor (e.g., ObjectId, ISODate) or a known global (e.g., Date, RegExp).": "Unknown constructor '{0}'. Expected a BSON constructor (e.g., ObjectId, ISODate) or a known global (e.g., Date, RegExp).", @@ -1238,34 +1345,42 @@ "URL handling aborted. Connection was unsuccessful or the specified database/collection does not exist.": "URL handling aborted. Connection was unsuccessful or the specified database/collection does not exist.", "User": "User", "User: {0} | Authentication: {1} | Database: {2}": "User: {0} | Authentication: {1} | Database: {2}", + "Username": "Username", "Username and Password": "Username and Password", "Username cannot be empty": "Username cannot be empty", "Username contains characters that cannot be safely encoded.": "Username contains characters that cannot be safely encoded.", "Username for {resource}": "Username for {resource}", + "Username must be 128 characters or fewer.": "Username must be 128 characters or fewer.", + "Username must not contain control characters.": "Username must not contain control characters.", "Using custom prompt template for {type} query generation: {path}": "Using custom prompt template for {type} query generation: {path}", "Using custom prompt template for {type} query: {path}": "Using custom prompt template for {type} query: {path}", "Using existing resource group \"{0}\".": "Using existing resource group \"{0}\".", "Using the table navigation, you can explore deeper levels or move back and forth between them.": "Using the table navigation, you can explore deeper levels or move back and forth between them.", "Validate": "Validate", "Validate document syntax": "Validate document syntax", + "Validate the server certificate. Recommended.": "Validate the server certificate. Recommended.", "Validating source collection...": "Validating source collection...", "Verifying folder can be deleted…": "Verifying folder can be deleted…", "Verifying move operation…": "Verifying move operation…", "Very low efficiency ratio": "Very low efficiency ratio", "Very slow execution": "Very slow execution", "View conflict details in the Output panel": "View conflict details in the Output panel", + "View Docker output": "View Docker output", "View in Marketplace": "View in Marketplace", "View Raw Execution Stats": "View Raw Execution Stats", "View Raw Explain Output": "View Raw Explain Output", "View selected document": "View selected document", "Viewing Azure account information for: {0}": "Viewing Azure account information for: {0}", "VS Code: v{0}": "VS Code: v{0}", + "Wait longer": "Wait longer", "Waiting for Azure sign-in...": "Waiting for Azure sign-in...", + "Waiting for DocumentDB to accept connections": "Waiting for DocumentDB to accept connections", "WARNING: Cannot create resource group \"{0}\" because the selected subscription is a concierge subscription. Using resource group \"{1}\" instead.": "WARNING: Cannot create resource group \"{0}\" because the selected subscription is a concierge subscription. Using resource group \"{1}\" instead.", "WARNING: Provider \"{0}\" does not support location \"{1}\". Using \"{2}\" instead.": "WARNING: Provider \"{0}\" does not support location \"{1}\". Using \"{2}\" instead.", "WARNING: Resource does not support extended location \"{0}\". Using \"{1}\" instead.": "WARNING: Resource does not support extended location \"{0}\". Using \"{1}\" instead.", "We can't move items between \"DocumentDB Local\" and regular connections. Please select items from only one of those areas at a time.": "We can't move items between \"DocumentDB Local\" and regular connections. Please select items from only one of those areas at a time.", "We found {0} naming conflict(s) in \"{1}\". To move these items, please rename them or choose a different folder:": "We found {0} naming conflict(s) in \"{1}\". To move these items, please rename them or choose a different folder:", + "What we'll do": "What we'll do", "What's New": "What's New", "Where to save the exported documents?": "Where to save the exported documents?", "with Popover": "with Popover", @@ -1301,6 +1416,7 @@ "Your Cluster": "Your Cluster", "Your database stores documents with embedded fields, allowing for hierarchical data organization.": "Your database stores documents with embedded fields, allowing for hierarchical data organization.", "Your feedback helps us improve Query Insights. Tell us what could be better:": "Your feedback helps us improve Query Insights. Tell us what could be better:", + "Your local connections have been moved to '{0}' in the Connections view.": "Your local connections have been moved to '{0}' in the Connections view.", "Your positive feedback helps us understand what works well in Query Insights. Tell us more:": "Your positive feedback helps us understand what works well in Query Insights. Tell us more:", "Your query does not require sorting, which avoids additional processing overhead.": "Your query does not require sorting, which avoids additional processing overhead.", "Your query does not use an index.\n\nWhile not necessarily a problem for small collections, adding appropriate indexes can significantly improve query performance.": "Your query does not use an index.\n\nWhile not necessarily a problem for small collections, adding appropriate indexes can significantly improve query performance.", diff --git a/package-lock.json b/package-lock.json index d291d3f9d..7e2130c15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@microsoft/vscode-azext-azureutils": "~4.2.0", "@microsoft/vscode-azext-utils": "~4.1.0", "@microsoft/vscode-azureresources-api": "~2.5.0", + "@microsoft/vscode-container-client": "^0.5.4", "@microsoft/vscode-ext-react-webview": "*", "@monaco-editor/react": "~4.7.0", "@mongodb-js/explain-plan-helper": "1.4.24", @@ -5325,6 +5326,17 @@ "@azure/ms-rest-azure-env": "^2.0.0" } }, + "node_modules/@microsoft/vscode-container-client": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-container-client/-/vscode-container-client-0.5.4.tgz", + "integrity": "sha512-cFLiSNImnURifgmXkYxUvyYAs6qFJ7qegFAarUEUE5fCFgfU68Nj+e6W2WZaOienkgFOmUbACDXKTSNYL/XxbQ==", + "license": "See LICENSE in the project root for license information.", + "dependencies": { + "@microsoft/vscode-processutils": "^0.2.2", + "dayjs": "^1.11.2", + "zod": "^4.1.13" + } + }, "node_modules/@microsoft/vscode-ext-react-webview": { "resolved": "packages/vscode-ext-react-webview", "link": true diff --git a/package.json b/package.json index 7cd306bb1..4eccbef1d 100644 --- a/package.json +++ b/package.json @@ -164,12 +164,18 @@ "@azure/arm-resources": "~7.0.0", "@azure/cosmos": "~4.7.0", "@azure/identity": "~4.13.0", + "@documentdb-js/operator-registry": "*", + "@documentdb-js/schema-analyzer": "*", + "@documentdb-js/shell-runtime": "*", "@fluentui/react-components": "~9.73.3", "@fluentui/react-icons": "~2.0.320", "@microsoft/vscode-azext-azureauth": "~4.1.1", "@microsoft/vscode-azext-azureutils": "~4.2.0", "@microsoft/vscode-azext-utils": "~4.1.0", "@microsoft/vscode-azureresources-api": "~2.5.0", + "@microsoft/vscode-container-client": "^0.5.4", + "@microsoft/vscode-ext-react-webview": "*", + "@microsoft/vscode-processutils": "^0.2.2", "@monaco-editor/react": "~4.7.0", "@mongodb-js/explain-plan-helper": "1.4.24", "@mongodb-js/shell-bson-parser": "^1.5.6", @@ -180,10 +186,6 @@ "@mongosh/shell-evaluator": "^5.1.4", "@trpc/client": "~11.10.0", "@trpc/server": "~11.10.0", - "@documentdb-js/operator-registry": "*", - "@documentdb-js/shell-runtime": "*", - "@documentdb-js/schema-analyzer": "*", - "@microsoft/vscode-ext-react-webview": "*", "@vscode/l10n": "~0.0.18", "acorn": "^8.16.0", "acorn-walk": "^8.3.5", @@ -355,6 +357,62 @@ "title": "New Local Connection…", "icon": "$(add)" }, + { + "//": "[Local Quick Start] Open the Quick Start webview", + "category": "DocumentDB", + "command": "vscode-documentdb.command.localQuickStart.open", + "title": "Local Quick Start", + "icon": "$(rocket)" + }, + { + "//": "[Local Quick Start] Start the managed instance", + "category": "DocumentDB", + "command": "vscode-documentdb.command.localQuickStart.start", + "title": "Start", + "icon": "$(play)" + }, + { + "//": "[Local Quick Start] Stop the managed instance", + "category": "DocumentDB", + "command": "vscode-documentdb.command.localQuickStart.stop", + "title": "Stop", + "icon": "$(debug-stop)" + }, + { + "//": "[Local Quick Start] Restart the managed instance", + "category": "DocumentDB", + "command": "vscode-documentdb.command.localQuickStart.restart", + "title": "Restart", + "icon": "$(debug-restart)" + }, + { + "//": "[Local Quick Start] Delete the managed container", + "category": "DocumentDB", + "command": "vscode-documentdb.command.localQuickStart.delete", + "title": "Delete Container…", + "icon": "$(trash)" + }, + { + "//": "[Local Quick Start] Copy connection string", + "category": "DocumentDB", + "command": "vscode-documentdb.command.localQuickStart.copyConnectionString", + "title": "Copy Connection String", + "icon": "$(link)" + }, + { + "//": "[Local Quick Start] Copy password", + "category": "DocumentDB", + "command": "vscode-documentdb.command.localQuickStart.copyPassword", + "title": "Copy Password", + "icon": "$(key)" + }, + { + "//": "[Local Quick Start] View container logs", + "category": "DocumentDB", + "command": "vscode-documentdb.command.localQuickStart.viewLogs", + "title": "View Logs", + "icon": "$(output)" + }, { "//": "[ConnectionsView] Delete Connection", "category": "DocumentDB", @@ -682,6 +740,51 @@ "editor/context": [], "editor/title": [], "view/item/context": [ + { + "command": "vscode-documentdb.command.localQuickStart.start", + "when": "view == connectionsView && viewItem =~ /\\btreeItem_quickStartInstance\\b/i && viewItem =~ /\\bstate_stopped\\b/i", + "group": "inline@1" + }, + { + "command": "vscode-documentdb.command.localQuickStart.stop", + "when": "view == connectionsView && viewItem =~ /\\btreeItem_quickStartInstance\\b/i && viewItem =~ /\\bstate_running\\b/i", + "group": "inline@1" + }, + { + "command": "vscode-documentdb.command.localQuickStart.start", + "when": "view == connectionsView && viewItem =~ /\\btreeItem_quickStartInstance\\b/i && viewItem =~ /\\bstate_stopped\\b/i", + "group": "1_quickstart@1" + }, + { + "command": "vscode-documentdb.command.localQuickStart.stop", + "when": "view == connectionsView && viewItem =~ /\\btreeItem_quickStartInstance\\b/i && viewItem =~ /\\bstate_running\\b/i", + "group": "1_quickstart@1" + }, + { + "command": "vscode-documentdb.command.localQuickStart.restart", + "when": "view == connectionsView && viewItem =~ /\\btreeItem_quickStartInstance\\b/i && viewItem =~ /\\bstate_(running|error)\\b/i", + "group": "1_quickstart@2" + }, + { + "command": "vscode-documentdb.command.localQuickStart.viewLogs", + "when": "view == connectionsView && viewItem =~ /\\btreeItem_quickStartInstance\\b/i", + "group": "1_quickstart@3" + }, + { + "command": "vscode-documentdb.command.localQuickStart.copyConnectionString", + "when": "view == connectionsView && viewItem =~ /\\btreeItem_quickStartInstance\\b/i && viewItem =~ /\\bstate_(running|stopped)\\b/i", + "group": "2_quickstart@1" + }, + { + "command": "vscode-documentdb.command.localQuickStart.copyPassword", + "when": "view == connectionsView && viewItem =~ /\\btreeItem_quickStartInstance\\b/i && viewItem =~ /\\bstate_(running|stopped)\\b/i", + "group": "2_quickstart@2" + }, + { + "command": "vscode-documentdb.command.localQuickStart.delete", + "when": "view == connectionsView && viewItem =~ /\\btreeItem_quickStartInstance\\b/i && viewItem =~ /\\bstate_(running|stopped|error|missing)\\b/i", + "group": "3_quickstart@1" + }, { "command": "vscode-documentdb.command.connectionsView.updateConnectionString", "when": "view == connectionsView && viewItem =~ /\\btreeitem_documentdbcluster\\b/i", diff --git a/src/commands/connections-view/moveItems/moveItems.ts b/src/commands/connections-view/moveItems/moveItems.ts index 033d40e69..e3caad5a8 100644 --- a/src/commands/connections-view/moveItems/moveItems.ts +++ b/src/commands/connections-view/moveItems/moveItems.ts @@ -13,6 +13,7 @@ import { } from '../../../services/connectionStorageService'; import { DocumentDBClusterItem } from '../../../tree/connections-view/DocumentDBClusterItem'; import { FolderItem } from '../../../tree/connections-view/FolderItem'; +import { resolveStorageZone } from '../../../tree/connections-view/models/ConnectionClusterModel'; import { type TreeElement } from '../../../tree/TreeElement'; import { ConfirmMoveStep } from './ConfirmMoveStep'; import { ExecuteStep } from './ExecuteStep'; @@ -126,7 +127,7 @@ function getConnectionType(item: MovableTreeElement): ConnectionType { } if (item instanceof DocumentDBClusterItem) { - return item.cluster.emulatorConfiguration?.isEmulator ? ConnectionType.Emulators : ConnectionType.Clusters; + return resolveStorageZone(item.cluster); } // Default fallback diff --git a/src/commands/connections-view/renameConnection/ExecuteStep.ts b/src/commands/connections-view/renameConnection/ExecuteStep.ts index d9e5fe2ec..9bd1a1624 100644 --- a/src/commands/connections-view/renameConnection/ExecuteStep.ts +++ b/src/commands/connections-view/renameConnection/ExecuteStep.ts @@ -19,13 +19,12 @@ export class ExecuteStep extends AzureWizardExecuteStep { + const resourceType = + context.storageZone ?? (context.isEmulator ? ConnectionType.Emulators : ConnectionType.Clusters); // Set telemetry properties - context.telemetry.properties.connectionType = context.isEmulator - ? ConnectionType.Emulators - : ConnectionType.Clusters; + context.telemetry.properties.connectionType = resourceType; await withConnectionsViewProgress(async () => { - const resourceType = context.isEmulator ? ConnectionType.Emulators : ConnectionType.Clusters; const connection = await ConnectionStorageService.get(context.storageId, resourceType); if (connection) { diff --git a/src/commands/connections-view/renameConnection/PromptNewConnectionNameStep.ts b/src/commands/connections-view/renameConnection/PromptNewConnectionNameStep.ts index e9654e2df..d634725a3 100644 --- a/src/commands/connections-view/renameConnection/PromptNewConnectionNameStep.ts +++ b/src/commands/connections-view/renameConnection/PromptNewConnectionNameStep.ts @@ -39,7 +39,8 @@ export class PromptNewConnectionNameStep extends AzureWizardPromptStep { + context.telemetry.properties.action = 'start'; + await QuickStartService.start(); +} + +export async function stopQuickStartInstance(context: IActionContext): Promise { + context.telemetry.properties.action = 'stop'; + await QuickStartService.stop(); +} + +export async function restartQuickStartInstance(context: IActionContext): Promise { + context.telemetry.properties.action = 'restart'; + await QuickStartService.restart(); +} + +export async function deleteQuickStartInstance(context: IActionContext): Promise { + context.telemetry.properties.action = 'delete'; + + // Delete is now offered while Running too, so the container is force-stopped before removal + // (ContainerRuntime.removeContainer uses force). Warn accordingly and make the data-loss + // consequences explicit — Delete drops the data volume, so this is a permanent clean slate. + const wasRunning = QuickStartService.getStatus().state === InstanceState.Running; + context.telemetry.properties.wasRunning = String(wasRunning); + + const detail = wasRunning + ? l10n.t( + 'The container is currently running. It will be stopped and permanently removed. All data, logs, and the auto-generated credentials will be lost. This cannot be undone — you can recreate a fresh instance any time with Quick Start.', + ) + : l10n.t( + 'The container and its data volume will be permanently removed. All data, logs, and the auto-generated credentials will be lost. This cannot be undone — you can recreate a fresh instance any time with Quick Start.', + ); + + const confirmed = await getConfirmationWithClick(l10n.t('Delete DocumentDB Local container?'), detail); + if (!confirmed) { + return; + } + await QuickStartService.deleteContainer(); + showConfirmationAsInSettings(l10n.t('DocumentDB Local container deleted.')); +} + +export function copyQuickStartConnectionString(_context: IActionContext): void { + const metadata = QuickStartService.getStatus().metadata; + if (!metadata) { + return; + } + void vscode.env.clipboard.writeText(metadata.connectionString); + showConfirmationAsInSettings(l10n.t('Connection string copied to clipboard.')); +} + +export function copyQuickStartPassword(_context: IActionContext): void { + const metadata = QuickStartService.getStatus().metadata; + if (!metadata) { + return; + } + let password = ''; + try { + password = new DocumentDBConnectionString(metadata.connectionString).password; + } catch { + password = ''; + } + if (!password) { + return; + } + void vscode.env.clipboard.writeText(password); + showConfirmationAsInSettings(l10n.t('Password copied to clipboard.')); +} + +export function viewQuickStartLogs(_context: IActionContext): void { + const channel = getQuickStartOutputChannel(); + channel.show(true); + // Best-effort: stream the running container's current logs into the channel, + // masking the password (D14) in case the image ever echoes it. + const metadata = QuickStartService.getStatus().metadata; + if (metadata) { + let password = ''; + try { + password = new DocumentDBConnectionString(metadata.connectionString).password; + } catch { + password = ''; + } + void ContainerRuntime.followLogs(metadata.containerId, password ? [password] : [], undefined); + } +} diff --git a/src/commands/localQuickStart/openLocalQuickStart.ts b/src/commands/localQuickStart/openLocalQuickStart.ts new file mode 100644 index 000000000..40a17c1f1 --- /dev/null +++ b/src/commands/localQuickStart/openLocalQuickStart.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type IActionContext } from '@microsoft/vscode-azext-utils'; +import { LocalQuickStartController } from '../../webviews/documentdb/localQuickStart/localQuickStartController'; + +/** + * Opens the Local Quick Start webview. Primary entry point is the tree rocket + * row (WI-6); this command is the command-palette / fallback launch (D10). + */ +export function openLocalQuickStart(_context: IActionContext): void { + const view = new LocalQuickStartController({ id: 'localQuickStart' }); + view.revealToForeground(); +} diff --git a/src/commands/newConnection/ExecuteStep.ts b/src/commands/newConnection/ExecuteStep.ts index 5a9212032..b7e5475f6 100644 --- a/src/commands/newConnection/ExecuteStep.ts +++ b/src/commands/newConnection/ExecuteStep.ts @@ -8,6 +8,7 @@ import * as l10n from '@vscode/l10n'; import { AuthMethodId } from '../../documentdb/auth/AuthMethod'; import { redactCredentialsFromConnectionString } from '../../documentdb/utils/connectionStringHelpers'; import { DocumentDBConnectionString } from '../../documentdb/utils/DocumentDBConnectionString'; +import { areAllHostsLocal, canonicalizeTlsException } from '../../documentdb/utils/tlsException'; import { API } from '../../DocumentDBExperiences'; import { ext } from '../../extensionVariables'; @@ -37,7 +38,16 @@ export class ExecuteStep extends AzureWizardExecuteStep { @@ -64,6 +65,8 @@ export class PromptConnectionModeStep extends AzureWizardPromptStep { @@ -45,6 +46,16 @@ export class PromptConnectionStringStep extends AzureWizardPromptStep { + public async prompt(context: NewConnectionWizardContext): Promise { + const enableTls = { + id: 'enable', + label: l10n.t('Enable TLS (default)'), + detail: l10n.t('Validate the server certificate. Recommended.'), + alwaysShow: true, + }; + const allowInvalid = { + id: 'allow', + label: l10n.t('Allow invalid certificates'), + detail: l10n.t( + 'Accept a self-signed or untrusted certificate. Only choose this for a host you trust — a “.local” or single-word name can also be managed corporate infrastructure.', + ), + alwaysShow: true, + }; + + const selected = await context.ui.showQuickPick([enableTls, allowInvalid], { + placeHolder: l10n.t('This connection targets a local or private network host. TLS certificate validation:'), + stepName: 'tlsException', + suppressPersistence: true, + }); + + context.disableEmulatorSecurity = selected.id === 'allow'; + context.telemetry.properties.tlsException = context.disableEmulatorSecurity ? 'allowInvalid' : 'enabled'; + } + + public shouldPrompt(context: NewConnectionWizardContext): boolean { + // Already decided (e.g. the connection string already opted in) — don't ask again. + if (context.disableEmulatorSecurity !== undefined) { + return false; + } + if (!context.connectionString) { + return false; + } + try { + const hosts = new DocumentDBConnectionString(context.connectionString).hosts; + // Allow-invalid is client-wide, so only offer the exception when EVERY seed host is + // local/private — a mixed list (e.g. localhost + a public host) must NOT be able to + // disable certificate validation for the public host. + return hosts.length > 0 && hosts.every((host) => isLocalOrPrivateHost(host)); + } catch { + // A connection string we can't parse won't reach the gate — let later steps handle it. + return false; + } + } +} diff --git a/src/commands/newLocalConnection/PromptConnectionTypeStep.ts b/src/commands/newLocalConnection/PromptConnectionTypeStep.ts index ee98fcf5a..fc15ef60b 100644 --- a/src/commands/newLocalConnection/PromptConnectionTypeStep.ts +++ b/src/commands/newLocalConnection/PromptConnectionTypeStep.ts @@ -94,7 +94,7 @@ export class PromptConnectionTypeStep extends AzureWizardPromptStep { - if (connection.cluster.emulatorConfiguration?.isEmulator) { - await ConnectionStorageService.delete(ConnectionType.Emulators, connection.storageId); - } else { - await ConnectionStorageService.delete(ConnectionType.Clusters, connection.storageId); - } + await ConnectionStorageService.delete(resolveStorageZone(connection.cluster), connection.storageId); }); // delete cached credentials from memory using stable clusterId (not treeId) diff --git a/src/commands/updateConnectionString/ExecuteStep.ts b/src/commands/updateConnectionString/ExecuteStep.ts index 2dada7245..03486d3e2 100644 --- a/src/commands/updateConnectionString/ExecuteStep.ts +++ b/src/commands/updateConnectionString/ExecuteStep.ts @@ -5,8 +5,9 @@ import { AzureWizardExecuteStep } from '@microsoft/vscode-azext-utils'; import { l10n, window } from 'vscode'; +import { areAllHostsLocal, canonicalizeTlsException } from '../../documentdb/utils/tlsException'; import { ext } from '../../extensionVariables'; -import { ConnectionStorageService, ConnectionType } from '../../services/connectionStorageService'; +import { ConnectionStorageService, ConnectionType, isConnection } from '../../services/connectionStorageService'; import { showConfirmationAsInSettings } from '../../utils/dialogs/showConfirmation'; import { nonNullValue } from '../../utils/nonNull'; import { type UpdateCSWizardContext } from './UpdateCSWizardContext'; @@ -15,7 +16,8 @@ export class ExecuteStep extends AzureWizardExecuteStep { public priority: number = 100; public async execute(context: UpdateCSWizardContext): Promise { - const resourceType = context.isEmulator ? ConnectionType.Emulators : ConnectionType.Clusters; + const resourceType = + context.storageZone ?? (context.isEmulator ? ConnectionType.Emulators : ConnectionType.Clusters); const connection = await ConnectionStorageService.get(context.storageId, resourceType); if (!connection || !connection.secrets?.connectionString) { @@ -27,15 +29,39 @@ export class ExecuteStep extends AzureWizardExecuteStep { } try { + // Canonicalize the edited connection string: strip any TLS-bypass param and keep + // `emulatorConfiguration.disableEmulatorSecurity` as the single source of truth (§7). + // Recompute the exception host-gated against the EDITED hosts: honor a freshly-requested + // bypass OR preserve an existing exception, but ONLY while every host stays local/private. + // Editing to a public/mixed host therefore CLEARS allow-invalid so the public host + // validates certificates (it never stays latched from the old value). + const canonicalTls = canonicalizeTlsException( + nonNullValue(context.newConnectionString?.trim(), 'context.newConnectionString', 'ExecuteStep.ts'), + ); + connection.secrets = { ...connection.secrets, - connectionString: nonNullValue( - context.newConnectionString?.trim(), - 'context.newConnectionString', - 'ExecuteStep.ts', - ), + connectionString: canonicalTls.connectionString, }; + if (isConnection(connection)) { + // Host-gate BOTH emulator flags against the EDITED hosts. Editing a local emulator or + // TLS-exception connection to a public/mixed host therefore CLEARS allow-invalid (so + // the public host validates certificates) AND clears `isEmulator` in storage — neither + // flag stays latched from the old local value. A still-local edit preserves both. + // (Note: a legacy connection still rendered under the Emulators tree node is forced to + // `isEmulator:true` at runtime by LocalEmulatorsItem regardless of the stored value, so + // the "(Emulator)" label/timeout there only fully clears once the §4 migration retires + // that node; the security-relevant `disableEmulatorSecurity` is honored from storage.) + const existing = connection.properties.emulatorConfiguration; + const allLocal = areAllHostsLocal(canonicalTls.connectionString); + const disableEmulatorSecurity = + allLocal && (canonicalTls.disableEmulatorSecurity || !!existing?.disableEmulatorSecurity); + const isEmulator = allLocal && !!existing?.isEmulator; + connection.properties.emulatorConfiguration = + isEmulator || disableEmulatorSecurity ? { isEmulator, disableEmulatorSecurity } : undefined; + } + await ConnectionStorageService.save(resourceType, connection, true); showConfirmationAsInSettings(l10n.t('Connection updated successfully.')); diff --git a/src/commands/updateConnectionString/UpdateCSWizardContext.ts b/src/commands/updateConnectionString/UpdateCSWizardContext.ts index 3f748568f..5779a8678 100644 --- a/src/commands/updateConnectionString/UpdateCSWizardContext.ts +++ b/src/commands/updateConnectionString/UpdateCSWizardContext.ts @@ -4,10 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { type IActionContext } from '@microsoft/vscode-azext-utils'; +import { type StorageZone } from '../../services/connectionStorageService'; export interface UpdateCSWizardContext extends IActionContext { // target item details isEmulator: boolean; + /** Explicit storage zone of the target connection (preferred over isEmulator inference). */ + storageZone?: StorageZone; storageId: string; originalConnectionString: string; diff --git a/src/commands/updateConnectionString/updateConnectionString.ts b/src/commands/updateConnectionString/updateConnectionString.ts index adffb1967..d9c5335db 100644 --- a/src/commands/updateConnectionString/updateConnectionString.ts +++ b/src/commands/updateConnectionString/updateConnectionString.ts @@ -8,8 +8,9 @@ import * as l10n from '@vscode/l10n'; import { maskSensitiveValuesInTelemetry } from '../../documentdb/utils/connectionStringHelpers'; import { DocumentDBConnectionString } from '../../documentdb/utils/DocumentDBConnectionString'; import { Views } from '../../documentdb/Views'; -import { ConnectionStorageService, ConnectionType } from '../../services/connectionStorageService'; +import { ConnectionStorageService } from '../../services/connectionStorageService'; import { type DocumentDBClusterItem } from '../../tree/connections-view/DocumentDBClusterItem'; +import { resolveStorageZone } from '../../tree/connections-view/models/ConnectionClusterModel'; import { refreshView } from '../refreshView/refreshView'; import { ConnectionStringStep } from './ConnectionStringStep'; import { ExecuteStep } from './ExecuteStep'; @@ -39,9 +40,7 @@ export async function updateConnectionString(context: IActionContext, node: Docu // as the object is cached in the tree view, and in the 'retry/error' nodes // that's why we need to get the fresh one each time. - const resourceType = node.cluster.emulatorConfiguration?.isEmulator - ? ConnectionType.Emulators - : ConnectionType.Clusters; + const resourceType = resolveStorageZone(node.cluster); const connection = await ConnectionStorageService.get(node.storageId, resourceType); const connectionString = connection?.secrets?.connectionString || ''; @@ -57,6 +56,7 @@ export async function updateConnectionString(context: IActionContext, node: Docu ...context, originalConnectionString: parsedCS.toString(), isEmulator: Boolean(node.cluster.emulatorConfiguration?.isEmulator), + storageZone: resolveStorageZone(node.cluster), storageId: node.storageId, }; diff --git a/src/commands/updateCredentials/ExecuteStep.ts b/src/commands/updateCredentials/ExecuteStep.ts index 8be891a3c..32e247af6 100644 --- a/src/commands/updateCredentials/ExecuteStep.ts +++ b/src/commands/updateCredentials/ExecuteStep.ts @@ -27,7 +27,8 @@ export class ExecuteStep extends AzureWizardExecuteStep { - const resourceType = context.isEmulator ? ConnectionType.Emulators : ConnectionType.Clusters; + const resourceType = + context.storageZone ?? (context.isEmulator ? ConnectionType.Emulators : ConnectionType.Clusters); const connectionCredentials = await ConnectionStorageService.get(context.storageId, resourceType); if (!connectionCredentials) { diff --git a/src/commands/updateCredentials/UpdateCredentialsWizardContext.ts b/src/commands/updateCredentials/UpdateCredentialsWizardContext.ts index 05d7621e5..2ec9336dc 100644 --- a/src/commands/updateCredentials/UpdateCredentialsWizardContext.ts +++ b/src/commands/updateCredentials/UpdateCredentialsWizardContext.ts @@ -6,10 +6,13 @@ import { type IActionContext } from '@microsoft/vscode-azext-utils'; import { type EntraIdAuthConfig, type NativeAuthConfig } from '../../documentdb/auth/AuthConfig'; import { type AuthMethodId } from '../../documentdb/auth/AuthMethod'; +import { type StorageZone } from '../../services/connectionStorageService'; export interface UpdateCredentialsWizardContext extends IActionContext { // target item details isEmulator: boolean; + /** Explicit storage zone of the target connection (preferred over isEmulator inference). */ + storageZone?: StorageZone; storageId: string; availableAuthenticationMethods: AuthMethodId[]; diff --git a/src/commands/updateCredentials/updateCredentials.ts b/src/commands/updateCredentials/updateCredentials.ts index 1021e9f5a..cfbe00324 100644 --- a/src/commands/updateCredentials/updateCredentials.ts +++ b/src/commands/updateCredentials/updateCredentials.ts @@ -12,8 +12,9 @@ import { AzureDomains, hasDomainSuffix } from '../../documentdb/utils/connection import { DocumentDBConnectionString } from '../../documentdb/utils/DocumentDBConnectionString'; import { Views } from '../../documentdb/Views'; import { ext } from '../../extensionVariables'; -import { ConnectionStorageService, ConnectionType, isConnection } from '../../services/connectionStorageService'; +import { ConnectionStorageService, isConnection } from '../../services/connectionStorageService'; import { type DocumentDBClusterItem } from '../../tree/connections-view/DocumentDBClusterItem'; +import { resolveStorageZone } from '../../tree/connections-view/models/ConnectionClusterModel'; import { refreshView } from '../refreshView/refreshView'; import { PromptAuthMethodStep } from '../updateCredentials/PromptAuthMethodStep'; import { ExecuteStep } from './ExecuteStep'; @@ -50,9 +51,7 @@ export async function updateCredentials(context: IActionContext, node: DocumentD // Note to future maintainers: the node.cluster might be out of date // as the object is cached in the tree view, and in the 'retry/error' nodes // that's why we need to get the fresh one each time. - const resourceType = node.cluster.emulatorConfiguration?.isEmulator - ? ConnectionType.Emulators - : ConnectionType.Clusters; + const resourceType = resolveStorageZone(node.cluster); const storedItem = await ConnectionStorageService.get(node.storageId, resourceType); // Type guard ensures we have connection properties (not a folder) @@ -83,6 +82,7 @@ export async function updateCredentials(context: IActionContext, node: DocumentD availableAuthenticationMethods: authMethodsFromString(supportedAuthMethods), selectedAuthenticationMethod: authMethodFromString(connectionCredentials?.properties.selectedAuthMethod), isEmulator: Boolean(node.cluster.emulatorConfiguration?.isEmulator), + storageZone: resolveStorageZone(node.cluster), storageId: node.storageId, isErrorState, reconnectAfterError: false, diff --git a/src/documentdb/ClustersClient.ts b/src/documentdb/ClustersClient.ts index 5a7d83e85..c5541693d 100644 --- a/src/documentdb/ClustersClient.ts +++ b/src/documentdb/ClustersClient.ts @@ -60,6 +60,7 @@ import { SchemaStore } from './SchemaStore'; import { getHostsFromConnectionString, hasAzureDomain } from './utils/connectionStringHelpers'; import { fixupDocumentDbExplain } from './utils/fixupDocumentDbExplain'; import { getClusterMetadata, type ClusterMetadata } from './utils/getClusterMetadata'; +import { resolveAllowInvalidCertificates } from './utils/tlsException'; import { toFilterQueryObj } from './utils/toFilterQuery'; export interface DatabaseItemModel { @@ -321,13 +322,20 @@ export class ClustersClient { } const message = parseError(error).message; - if (emulatorConfiguration?.isEmulator && message.includes('ECONNREFUSED')) { + // Surface the friendly local-connection tips only for a genuinely local-ish connection + // (emulator, OR a local connection that opted into the TLS exception — host-gated the + // same way as the TLS option). An orphaned flag on a PUBLIC host must NOT make a public + // ECONNREFUSED/self-signed failure show "local instance" troubleshooting copy. + const isLocalish = + !!emulatorConfiguration?.isEmulator || + !!resolveAllowInvalidCertificates(emulatorConfiguration?.disableEmulatorSecurity, connectionString); + if (isLocalish && message.includes('ECONNREFUSED')) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access error.message = l10n.t( 'Unable to connect to the local instance. Make sure it is started correctly. See {link} for tips.', { link: Links.LocalConnectionDebuggingTips }, ); - } else if (emulatorConfiguration?.isEmulator && message.includes('self-signed certificate')) { + } else if (isLocalish && message.includes('self-signed certificate')) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access error.message = l10n.t( 'The local instance is using a self-signed certificate. To connect, you must import the appropriate TLS/SSL certificate. See {link} for tips.', diff --git a/src/documentdb/ClustersExtension.ts b/src/documentdb/ClustersExtension.ts index a6f9aa7f9..5e0aae565 100644 --- a/src/documentdb/ClustersExtension.ts +++ b/src/documentdb/ClustersExtension.ts @@ -47,6 +47,16 @@ import { dropIndex } from '../commands/index.dropIndex/dropIndex'; import { hideIndex } from '../commands/index.hideIndex/hideIndex'; import { unhideIndex } from '../commands/index.unhideIndex/unhideIndex'; import { learnMoreAboutServiceProvider } from '../commands/learnMoreAboutServiceProvider/learnMoreAboutServiceProvider'; +import { + copyQuickStartConnectionString, + copyQuickStartPassword, + deleteQuickStartInstance, + restartQuickStartInstance, + startQuickStartInstance, + stopQuickStartInstance, + viewQuickStartLogs, +} from '../commands/localQuickStart/localQuickStartCommands'; +import { openLocalQuickStart } from '../commands/localQuickStart/openLocalQuickStart'; import { newConnection } from '../commands/newConnection/newConnection'; import { newLocalConnection } from '../commands/newLocalConnection/newLocalConnection'; import { openCollectionView, openCollectionViewInternal } from '../commands/openCollectionView/openCollectionView'; @@ -82,6 +92,9 @@ import { AzureMongoRUDiscoveryProvider } from '../plugins/service-azure-mongo-ru import { AzureDiscoveryProvider } from '../plugins/service-azure-mongo-vcore/AzureDiscoveryProvider'; import { AzureVMDiscoveryProvider } from '../plugins/service-azure-vm/AzureVMDiscoveryProvider'; import { DiscoveryService } from '../services/discoveryServices'; +import { migrateLegacyEmulatorConnections } from '../services/legacyEmulatorMigration'; +import { disposeQuickStartOutputChannel } from '../services/localQuickStart/ContainerRuntime'; +import { QuickStartService } from '../services/localQuickStart/QuickStartService'; import { maybeShowReleaseNotesNotification } from '../services/releaseNotesNotification'; import { DemoTask } from '../services/taskService/tasks/DemoTask'; import { TaskService } from '../services/taskService/taskService'; @@ -234,6 +247,21 @@ export class ClustersExtension implements vscode.Disposable { const playgroundService = PlaygroundService.getInstance(); ext.context.subscriptions.push(playgroundService); + // Initialize Local Quick Start (managed local DocumentDB container). + // Reconcile detects a still-running container after a window reload. + ext.context.subscriptions.push(QuickStartService); + ext.context.subscriptions.push({ dispose: disposeQuickStartOutputChannel }); + ext.context.subscriptions.push( + QuickStartService.onDidChangeStatus(() => { + ext.connectionsBranchDataProvider?.refresh(); + }), + ); + void QuickStartService.reconcile(); + + // One-time migration of legacy emulator connections into a regular + // "Local Connections (Legacy)" folder (design §4). Non-blocking. + void migrateLegacyEmulatorConnections(); + // Register evaluator disposal for clean worker shutdown on deactivation ext.context.subscriptions.push({ dispose: disposeEvaluators }); @@ -528,6 +556,39 @@ export class ClustersExtension implements vscode.Disposable { withTreeNodeCommandCorrelation(newLocalConnection), ); + registerCommand( + 'vscode-documentdb.command.localQuickStart.open', + withCommandCorrelation(openLocalQuickStart), + ); + registerCommand( + 'vscode-documentdb.command.localQuickStart.start', + withCommandCorrelation(startQuickStartInstance), + ); + registerCommand( + 'vscode-documentdb.command.localQuickStart.stop', + withCommandCorrelation(stopQuickStartInstance), + ); + registerCommand( + 'vscode-documentdb.command.localQuickStart.restart', + withCommandCorrelation(restartQuickStartInstance), + ); + registerCommand( + 'vscode-documentdb.command.localQuickStart.delete', + withCommandCorrelation(deleteQuickStartInstance), + ); + registerCommand( + 'vscode-documentdb.command.localQuickStart.copyConnectionString', + withCommandCorrelation(copyQuickStartConnectionString), + ); + registerCommand( + 'vscode-documentdb.command.localQuickStart.copyPassword', + withCommandCorrelation(copyQuickStartPassword), + ); + registerCommand( + 'vscode-documentdb.command.localQuickStart.viewLogs', + withCommandCorrelation(viewQuickStartLogs), + ); + registerCommand( 'vscode-documentdb.command.connectionsView.refresh', withCommandCorrelation((context: IActionContext) => { diff --git a/src/documentdb/auth/MicrosoftEntraIDAuthHandler.ts b/src/documentdb/auth/MicrosoftEntraIDAuthHandler.ts index f52c25f4e..62c33f126 100644 --- a/src/documentdb/auth/MicrosoftEntraIDAuthHandler.ts +++ b/src/documentdb/auth/MicrosoftEntraIDAuthHandler.ts @@ -9,6 +9,7 @@ import * as l10n from '@vscode/l10n'; import { type MongoClientOptions, type OIDCCallbackParams, type OIDCResponse } from 'mongodb'; import { type CachedClusterCredentials } from '../CredentialCache'; import { DocumentDBConnectionString } from '../utils/DocumentDBConnectionString'; +import { resolveAllowInvalidCertificates } from '../utils/tlsException'; import { type AuthHandler, type AuthHandlerResponse } from './AuthHandler'; /** @@ -52,6 +53,19 @@ export class MicrosoftEntraIDAuthHandler implements AuthHandler { }, }; + // Honor the TLS exception (design §7) consistently with the other auth paths, with the same + // "hybrid" runtime policy (flag honored only for local/private hosts). Entra ID is only + // offered for Azure (public) hosts, so this is effectively defensive, but it keeps the policy + // uniform across all builders. + if ( + resolveAllowInvalidCertificates( + this.clusterCredentials.emulatorConfiguration?.disableEmulatorSecurity, + this.clusterCredentials.connectionString, + ) + ) { + options.tlsAllowInvalidCertificates = true; + } + return { connectionString: dbConnectionString.toString(), options, diff --git a/src/documentdb/auth/NativeAuthHandler.ts b/src/documentdb/auth/NativeAuthHandler.ts index ddab5e92a..24c048b93 100644 --- a/src/documentdb/auth/NativeAuthHandler.ts +++ b/src/documentdb/auth/NativeAuthHandler.ts @@ -6,6 +6,7 @@ import { type MongoClientOptions } from 'mongodb'; import { nonNullValue } from '../../utils/nonNull'; import { type CachedClusterCredentials } from '../CredentialCache'; +import { resolveAllowInvalidCertificates } from '../utils/tlsException'; import { type AuthHandler, type AuthHandlerResponse } from './AuthHandler'; /** @@ -17,22 +18,41 @@ export class NativeAuthHandler implements AuthHandler { public configureAuth(): Promise { const options: MongoClientOptions = {}; - // Apply emulator-specific configuration if needed - if (this.clusterCredentials.emulatorConfiguration?.isEmulator) { + const connectionString = nonNullValue( + this.clusterCredentials.connectionStringWithPassword, + 'clusterCredentials.connectionStringWithPassword', + 'NativeAuthHandler.ts', + ); + + // Emulator-specific tuning: a shorter server-selection timeout fails fast against a + // local instance that isn't up yet — also applied to a regular LOCAL connection that opted + // into the TLS exception (§7). Host-gated the same way as the TLS option, so an orphaned + // flag on a public host does NOT trigger the aggressive 4s fail-fast. + if ( + this.clusterCredentials.emulatorConfiguration?.isEmulator || + resolveAllowInvalidCertificates( + this.clusterCredentials.emulatorConfiguration?.disableEmulatorSecurity, + connectionString, + ) + ) { options.serverSelectionTimeoutMS = 4000; + } - if (this.clusterCredentials.emulatorConfiguration?.disableEmulatorSecurity) { - // Prevents self signed certificate error for emulator - options.tlsAllowInvalidCertificates = true; - } + // TLS-allow-invalid is driven by `disableEmulatorSecurity` (design §7), honored ONLY for + // local/private hosts ("hybrid" runtime policy): an orphaned flag on a public host is not + // activated, while an explicit `tlsAllowInvalidCertificates` URL param is still honored by + // the driver (we never force the option to `false`). + if ( + resolveAllowInvalidCertificates( + this.clusterCredentials.emulatorConfiguration?.disableEmulatorSecurity, + connectionString, + ) + ) { + options.tlsAllowInvalidCertificates = true; } return Promise.resolve({ - connectionString: nonNullValue( - this.clusterCredentials.connectionStringWithPassword, - 'clusterCredentials.connectionStringWithPassword', - 'NativeAuthHandler.ts', - ), + connectionString, options, }); } diff --git a/src/documentdb/connectToClient.ts b/src/documentdb/connectToClient.ts index 2f20a9d6f..56afc3743 100644 --- a/src/documentdb/connectToClient.ts +++ b/src/documentdb/connectToClient.ts @@ -7,6 +7,7 @@ import * as l10n from '@vscode/l10n'; import { MongoClient, type MongoClientOptions } from 'mongodb'; import { Links, wellKnownEmulatorPassword } from '../constants'; import { type EmulatorConfiguration } from '../utils/emulatorConfiguration'; +import { resolveAllowInvalidCertificates } from './utils/tlsException'; export async function connectToClient( connectionString: string, @@ -22,8 +23,12 @@ export async function connectToClient( useUnifiedTopology: true, }; - if (emulatorConfiguration && emulatorConfiguration.isEmulator && emulatorConfiguration.disableEmulatorSecurity) { - // Prevents self signed certificate error for emulator https://github.com/microsoft/vscode-cosmosdb/issues/1241#issuecomment-614446198 + // TLS-allow-invalid is driven by `disableEmulatorSecurity` (design §7), but the stored flag is + // honored ONLY for local/private hosts ("hybrid" runtime policy): an orphaned flag on a public + // host is not activated, while an explicit `tlsAllowInvalidCertificates` URL param a user put in + // the connection string is still honored by the driver (we never force the option to `false`). + if (resolveAllowInvalidCertificates(emulatorConfiguration?.disableEmulatorSecurity, connectionString)) { + // Prevents self signed certificate error https://github.com/microsoft/vscode-cosmosdb/issues/1241#issuecomment-614446198 options.tlsAllowInvalidCertificates = true; } diff --git a/src/documentdb/playground/PlaygroundEvaluator.ts b/src/documentdb/playground/PlaygroundEvaluator.ts index c774812c0..5fef047ff 100644 --- a/src/documentdb/playground/PlaygroundEvaluator.ts +++ b/src/documentdb/playground/PlaygroundEvaluator.ts @@ -11,6 +11,7 @@ import { ext } from '../../extensionVariables'; import { meterSilentCatch } from '../../utils/callWithAccumulatingTelemetry'; import { getBatchSizeSetting } from '../../utils/workspacUtils'; import { CredentialCache } from '../CredentialCache'; +import { resolveAllowInvalidCertificates } from '../utils/tlsException'; import { type ExecutionResult, type PlaygroundConnection } from './types'; import { WorkerSessionManager } from './WorkerSessionManager'; import { type MainToWorkerMessage, type SerializableMongoClientOptions, type WorkerToMainMessage } from './workerTypes'; @@ -230,12 +231,23 @@ export class PlaygroundEvaluator implements vscode.Disposable { // Build serializable MongoClientOptions const clientOptions: SerializableMongoClientOptions = { - serverSelectionTimeoutMS: credentials.emulatorConfiguration?.isEmulator ? 4000 : undefined, - tlsAllowInvalidCertificates: - credentials.emulatorConfiguration?.isEmulator && - credentials.emulatorConfiguration?.disableEmulatorSecurity - ? true + // Fail-fast 4s timeout for emulators / local TLS-exception connections — host-gated the + // same way as the TLS option so an orphaned flag on a public host doesn't trigger it. + serverSelectionTimeoutMS: + credentials.emulatorConfiguration?.isEmulator || + resolveAllowInvalidCertificates( + credentials.emulatorConfiguration?.disableEmulatorSecurity, + connectionString, + ) + ? 4000 : undefined, + // TLS-allow-invalid is keyed off `disableEmulatorSecurity` (design §7), honored ONLY for + // local/private hosts ("hybrid" runtime policy): an orphaned flag on a public host is not + // activated; an explicit URL param is still honored by the driver (we never force `false`). + tlsAllowInvalidCertificates: resolveAllowInvalidCertificates( + credentials.emulatorConfiguration?.disableEmulatorSecurity, + connectionString, + ), }; return { diff --git a/src/documentdb/shell/ShellSessionManager.ts b/src/documentdb/shell/ShellSessionManager.ts index 4c2654ffc..15164ec29 100644 --- a/src/documentdb/shell/ShellSessionManager.ts +++ b/src/documentdb/shell/ShellSessionManager.ts @@ -15,6 +15,7 @@ import { type SerializableMongoClientOptions, type WorkerToMainMessage, } from '../playground/workerTypes'; +import { resolveAllowInvalidCertificates } from '../utils/tlsException'; /** * Connection parameters for a shell session. @@ -172,7 +173,14 @@ export class ShellSessionManager implements vscode.Disposable { return { host: this.extractHost(initMsg.connectionString), authMechanism: initMsg.authMechanism, - isEmulator: initMsg.clientOptions.serverSelectionTimeoutMS === 4000, + // Derive emulator-ness from the authoritative credential flag, NOT from the + // fail-fast `serverSelectionTimeoutMS === 4000` proxy: that timeout now also fires + // for a regular local connection that opted into the TLS exception + // (`disableEmulatorSecurity` without `isEmulator`, design §7), which must not be + // mislabeled "(Emulator)" in the banner or inflate the emulator telemetry metric. + isEmulator: + CredentialCache.getCredentials(this._connectionInfo.clusterId)?.emulatorConfiguration?.isEmulator ?? + false, username, }; } @@ -253,12 +261,23 @@ export class ShellSessionManager implements vscode.Disposable { } const clientOptions: SerializableMongoClientOptions = { - serverSelectionTimeoutMS: credentials.emulatorConfiguration?.isEmulator ? 4000 : undefined, - tlsAllowInvalidCertificates: - credentials.emulatorConfiguration?.isEmulator && - credentials.emulatorConfiguration?.disableEmulatorSecurity - ? true + // Fail-fast 4s timeout for emulators / local TLS-exception connections — host-gated the + // same way as the TLS option so an orphaned flag on a public host doesn't trigger it. + serverSelectionTimeoutMS: + credentials.emulatorConfiguration?.isEmulator || + resolveAllowInvalidCertificates( + credentials.emulatorConfiguration?.disableEmulatorSecurity, + connectionString, + ) + ? 4000 : undefined, + // TLS-allow-invalid is keyed off `disableEmulatorSecurity` (design §7), honored ONLY for + // local/private hosts ("hybrid" runtime policy): an orphaned flag on a public host is not + // activated; an explicit URL param is still honored by the driver (we never force `false`). + tlsAllowInvalidCertificates: resolveAllowInvalidCertificates( + credentials.emulatorConfiguration?.disableEmulatorSecurity, + connectionString, + ), }; return { diff --git a/src/documentdb/utils/hostClassification.test.ts b/src/documentdb/utils/hostClassification.test.ts new file mode 100644 index 000000000..445fdab34 --- /dev/null +++ b/src/documentdb/utils/hostClassification.test.ts @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { extractHostname, isLocalOrPrivateHost } from './hostClassification'; + +describe('hostClassification (TLS-exception gating, design §7.1)', () => { + describe('extractHostname', () => { + it('strips an optional port', () => { + expect(extractHostname('localhost:10260')).toBe('localhost'); + expect(extractHostname('10.0.0.5:27017')).toBe('10.0.0.5'); + }); + + it('strips IPv6 brackets and port', () => { + expect(extractHostname('[fe80::1]:10260')).toBe('fe80::1'); + expect(extractHostname('[::1]')).toBe('::1'); + }); + + it('returns bare IPv6 unchanged', () => { + expect(extractHostname('fe80::1')).toBe('fe80::1'); + }); + + it('lowercases the host', () => { + expect(extractHostname('MyDevBox')).toBe('mydevbox'); + }); + }); + + describe('isLocalOrPrivateHost — should OFFER the TLS exception (true)', () => { + it.each([ + 'localhost', + 'localhost:10260', + 'db.localhost', + '127.0.0.1', + '127.5.6.7', + '::1', + '[::1]:10260', + '10.0.0.1', + '10.255.255.255', + '172.16.0.0', + '172.16.0.1', + '172.31.255.255', + '192.168.1.1', + '192.168.255.255', + '169.254.0.1', + 'devbox', // single-word hostname + 'home', + 'my-server.local', // mDNS + 'fc00::', // IPv6 ULA lower boundary + 'fc00::1', + 'fd12:3456::1', + 'fdff::1', // IPv6 ULA upper boundary + 'fe80::1', // IPv6 link-local + 'febf::1', // IPv6 link-local upper boundary + '[fe80::abcd]:10260', + ])('%s → true', (host) => { + expect(isLocalOrPrivateHost(host)).toBe(true); + }); + }); + + describe('isLocalOrPrivateHost — should NOT offer the TLS exception (false)', () => { + it.each([ + 'example.com', + 'cluster0.mongodb.net', + 'my-cluster.documents.azure.com', + '8.8.8.8', + '172.15.0.1', // just below the 172.16/12 range + '172.32.0.1', // just above the 172.16/12 range + '192.169.0.1', // not 192.168 + '169.255.0.1', // not 169.254 + '11.0.0.1', // not 10/8 + 'fec0::1', // just above fe80::/10 (not link-local) + '2001:db8::1', // public IPv6 + '', // empty + 'example\u3002com', // U+3002 ideographic full stop — DNS resolves as public example.com + 'example\uFF0Ecom', // U+FF0E fullwidth full stop + 'example\uFF61com', // U+FF61 halfwidth ideographic full stop + '8\u30028\u30028\u30028', // public IPv4 written with Unicode dots → 8.8.8.8 + 'cluster0\u3002mongodb\u3002net', // public host with mixed Unicode separators + ])('%s → false', (host) => { + expect(isLocalOrPrivateHost(host)).toBe(false); + }); + }); + + describe('isLocalOrPrivateHost — IDNA/homograph normalization keeps genuinely-local hosts local', () => { + it.each([ + '127\u30020\u30020\u30021', // loopback IPv4 with Unicode dots → 127.0.0.1 + '10\u30020\u30020\u30021', // private IPv4 with Unicode dots → 10.0.0.1 + 'devbox', // legitimate single-label local host (no dots at all) + ])('%s → true', (host) => { + expect(isLocalOrPrivateHost(host)).toBe(true); + }); + }); +}); diff --git a/src/documentdb/utils/hostClassification.ts b/src/documentdb/utils/hostClassification.ts new file mode 100644 index 000000000..877572576 --- /dev/null +++ b/src/documentdb/utils/hostClassification.ts @@ -0,0 +1,146 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { domainToASCII } from 'node:url'; + +/** + * Host classification for the TLS-exception gating rules (Local Quick Start design §7.1). + * + * Decides whether a connection target is a local / private-network host for which a + * self-signed or untrusted certificate is plausibly expected — i.e. whether to *offer* + * the "Allow invalid TLS certificates" step in the new-connection wizard. The step itself + * always defaults to **Enable TLS**; this gate only decides whether the step is shown. + * + * Caveat (design §7.1): `.local` suffixes and single-word names can also be corporate + * infrastructure (AD domains, DNS search domains). That is why the gate only controls + * whether the step is offered — the step defaults to keeping TLS on. + * + * Security note: classification is done on the IDNA/punycode-normalized hostname (see + * `normalizeHostForClassification`). A public domain must never be able to masquerade as a + * single-word local host by using a Unicode label separator (e.g. `example。com`, U+3002), + * which DNS resolves as `example.com` but a naive ASCII-dot check would treat as one word. + */ + +/** + * Extract the bare hostname/IP from a connection-string host entry, stripping an optional + * port and IPv6 brackets, lowercased. Handles `host`, `host:port`, `[ipv6]`, `[ipv6]:port`, + * and bare IPv6 (`fe80::1`). + */ +export function extractHostname(host: string): string { + const trimmed = host.trim(); + if (trimmed.startsWith('[')) { + // [ipv6] or [ipv6]:port + const end = trimmed.indexOf(']'); + if (end !== -1) { + return trimmed.slice(1, end).toLowerCase(); + } + } + // `host:port` has exactly one colon; bare IPv6 has several (and no port without brackets). + const colonCount = (trimmed.match(/:/g) ?? []).length; + if (colonCount === 1) { + return trimmed.slice(0, trimmed.indexOf(':')).toLowerCase(); + } + return trimmed.toLowerCase(); +} + +/** + * Normalize a bare hostname for classification so Unicode/IDNA homographs can't disguise a + * public multi-label domain as a single-word local host. IPv6 literals (containing `:`) are + * returned unchanged because `domainToASCII` rejects them. For everything else we first map the + * Unicode full-stop variants that IDNA treats as label separators (U+3002 `。`, U+FF0E `.`, + * U+FF61 `。`) to ASCII `.` (defense-in-depth), then apply IDNA via `domainToASCII` (which also + * punycodes other confusables). Falls back to the dot-normalized input if `domainToASCII` returns + * empty (a malformed domain), so the downstream IP checks still run. + */ +function normalizeHostForClassification(hostname: string): string { + if (hostname.includes(':')) { + return hostname; + } + const dotNormalized = hostname.replace(/[\u3002\uFF0E\uFF61]/g, '.'); + return domainToASCII(dotNormalized) || dotNormalized; +} + +/** Parse a dotted-quad IPv4 string into octets, or undefined if it is not a valid IPv4. */ +function ipv4Octets(value: string): number[] | undefined { + if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(value)) { + return undefined; + } + const octets = value.split('.').map((part) => Number(part)); + return octets.every((octet) => octet >= 0 && octet <= 255) ? octets : undefined; +} + +/** + * Whether a host is a loopback / private-network / local-discovery target for which a + * TLS-exception step should be offered (design §7.1): + * - Loopback: `localhost`, `*.localhost`, `127.0.0.0/8`, `::1` + * - IPv4 private (RFC 1918): `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16` + * - IPv4 link-local: `169.254.0.0/16` + * - IPv6 unique-local (`fc00::/7`) and link-local (`fe80::/10`) + * - Single-word hostnames (no dots), e.g. `home`, `devbox` + * - `*.local` mDNS names + */ +export function isLocalOrPrivateHost(host: string): boolean { + const extracted = extractHostname(host); + if (!extracted) { + return false; + } + // Normalize away IDNA/Unicode homographs so a public domain can't pose as a single-word host. + const hostname = normalizeHostForClassification(extracted); + if (!hostname) { + return false; + } + + // Loopback / mDNS / single-word names. + if (hostname === 'localhost' || hostname.endsWith('.localhost')) { + return true; + } + if (hostname === '::1') { + return true; + } + if (hostname.endsWith('.local')) { + return true; + } + // A single-word hostname has no dots and is not IPv6 (no colons). + if (!hostname.includes('.') && !hostname.includes(':')) { + return true; + } + + // IPv4 ranges. + const octets = ipv4Octets(hostname); + if (octets) { + const [a, b] = octets; + if (a === 127) { + return true; // 127.0.0.0/8 loopback + } + if (a === 10) { + return true; // 10.0.0.0/8 + } + if (a === 172 && b >= 16 && b <= 31) { + return true; // 172.16.0.0/12 + } + if (a === 192 && b === 168) { + return true; // 192.168.0.0/16 + } + if (a === 169 && b === 254) { + return true; // 169.254.0.0/16 link-local + } + return false; + } + + // IPv6 unique-local (fc00::/7) and link-local (fe80::/10). + if (hostname.includes(':')) { + const firstHextet = parseInt(hostname.split(':')[0] || '', 16); + if (!isNaN(firstHextet)) { + if ((firstHextet & 0xfe00) === 0xfc00) { + return true; // fc00::/7 + } + if ((firstHextet & 0xffc0) === 0xfe80) { + return true; // fe80::/10 + } + } + } + + return false; +} diff --git a/src/documentdb/utils/tlsException.test.ts b/src/documentdb/utils/tlsException.test.ts new file mode 100644 index 000000000..e91e3e795 --- /dev/null +++ b/src/documentdb/utils/tlsException.test.ts @@ -0,0 +1,184 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { areAllHostsLocal, canonicalizeTlsException, resolveAllowInvalidCertificates } from './tlsException'; + +describe('canonicalizeTlsException (TLS exception single-source-of-truth, design §7)', () => { + it('honors allow-invalid for a local host and strips the param', () => { + const result = canonicalizeTlsException('mongodb://localhost:10260/?tls=true&tlsAllowInvalidCertificates=true'); + expect(result.disableEmulatorSecurity).toBe(true); + expect(result.connectionString).not.toContain('tlsAllowInvalidCertificates'); + expect(result.connectionString).toContain('tls=true'); + }); + + it('honors allow-invalid for a private (RFC1918) host', () => { + const result = canonicalizeTlsException('mongodb://192.168.1.5:27017/?tlsAllowInvalidCertificates=true'); + expect(result.disableEmulatorSecurity).toBe(true); + expect(result.connectionString).not.toContain('tlsAllowInvalidCertificates'); + }); + + it('does NOT honor allow-invalid for a public host, but still strips the bypass param', () => { + const result = canonicalizeTlsException('mongodb://prod.example.com/?tlsAllowInvalidCertificates=true'); + expect(result.disableEmulatorSecurity).toBe(false); + // The param is stripped so the driver can't silently disable validation for a public host. + expect(result.connectionString).not.toContain('tlsAllowInvalidCertificates'); + }); + + it('does NOT honor allow-invalid for a Unicode-dot homograph of a public host', () => { + // `example。com` (U+3002) has no ASCII dot but DNS resolves it as the public example.com, + // so it must NOT be treated as a single-word local host. + const result = canonicalizeTlsException('mongodb://example\u3002com/?tlsAllowInvalidCertificates=true'); + expect(result.disableEmulatorSecurity).toBe(false); + expect(result.connectionString).not.toContain('tlsAllowInvalidCertificates'); + }); + + it('does NOT honor allow-invalid for a mixed seed list (one public host)', () => { + const result = canonicalizeTlsException( + 'mongodb://localhost:27017,prod.example.com:27017/?tlsAllowInvalidCertificates=true', + ); + expect(result.disableEmulatorSecurity).toBe(false); + expect(result.connectionString).not.toContain('tlsAllowInvalidCertificates'); + }); + + it('strips alias bypass params (sslAllowInvalidCertificates, tlsInsecure)', () => { + const a = canonicalizeTlsException('mongodb://localhost/?sslAllowInvalidCertificates=true'); + expect(a.disableEmulatorSecurity).toBe(true); + expect(a.connectionString.toLowerCase()).not.toContain('sslallowinvalidcertificates'); + + const b = canonicalizeTlsException('mongodb://localhost/?tlsInsecure=true'); + expect(b.disableEmulatorSecurity).toBe(true); + expect(b.connectionString.toLowerCase()).not.toContain('tlsinsecure'); + }); + + it('strips hostname-validation bypass params (tlsAllowInvalidHostnames / ssl alias) for a local host', () => { + const a = canonicalizeTlsException('mongodb://localhost/?tlsAllowInvalidHostnames=true'); + expect(a.disableEmulatorSecurity).toBe(true); + expect(a.connectionString.toLowerCase()).not.toContain('allowinvalidhostnames'); + + const b = canonicalizeTlsException('mongodb://localhost/?sslAllowInvalidHostnames=true'); + expect(b.disableEmulatorSecurity).toBe(true); + expect(b.connectionString.toLowerCase()).not.toContain('allowinvalidhostnames'); + }); + + it('does NOT honor a hostname-validation bypass for a public host, but still strips it', () => { + const result = canonicalizeTlsException('mongodb://prod.example.com/?tlsAllowInvalidHostnames=true'); + expect(result.disableEmulatorSecurity).toBe(false); + expect(result.connectionString.toLowerCase()).not.toContain('allowinvalidhostnames'); + }); + + it('does NOT honor a hostname-validation bypass for a mixed seed list, but still strips it', () => { + const result = canonicalizeTlsException( + 'mongodb://localhost:27017,prod.example.com:27017/?tlsAllowInvalidHostnames=true', + ); + expect(result.disableEmulatorSecurity).toBe(false); + expect(result.connectionString.toLowerCase()).not.toContain('allowinvalidhostnames'); + }); + + it('is case-insensitive on the hostname-validation bypass key', () => { + const result = canonicalizeTlsException('mongodb://localhost/?TLSAllowInvalidHostnames=true'); + expect(result.disableEmulatorSecurity).toBe(true); + expect(result.connectionString.toLowerCase()).not.toContain('allowinvalidhostnames'); + }); + + it('honors rejectUnauthorized=false (inverse semantics) for a local host and strips it', () => { + const result = canonicalizeTlsException('mongodb://localhost/?rejectUnauthorized=false'); + expect(result.disableEmulatorSecurity).toBe(true); + expect(result.connectionString.toLowerCase()).not.toContain('rejectunauthorized'); + }); + + it('does NOT honor rejectUnauthorized=false for a public host, but still strips it', () => { + const result = canonicalizeTlsException('mongodb://prod.example.com/?rejectUnauthorized=false'); + expect(result.disableEmulatorSecurity).toBe(false); + expect(result.connectionString.toLowerCase()).not.toContain('rejectunauthorized'); + }); + + it('strips rejectUnauthorized=true without requesting a bypass (and validates)', () => { + const result = canonicalizeTlsException('mongodb://localhost/?rejectUnauthorized=true'); + expect(result.disableEmulatorSecurity).toBe(false); + expect(result.connectionString.toLowerCase()).not.toContain('rejectunauthorized'); + }); + + it('returns false (no exception) when no bypass param is present', () => { + const result = canonicalizeTlsException('mongodb://localhost:10260/?tls=true'); + expect(result.disableEmulatorSecurity).toBe(false); + // Nothing to strip → connection string returned unchanged. + expect(result.connectionString).toBe('mongodb://localhost:10260/?tls=true'); + }); + + it('treats a bypass param set to false as no exception (and strips it)', () => { + const result = canonicalizeTlsException('mongodb://localhost/?tlsAllowInvalidCertificates=false'); + expect(result.disableEmulatorSecurity).toBe(false); + expect(result.connectionString).not.toContain('tlsAllowInvalidCertificates'); + }); + + it('is case-insensitive on the param key', () => { + const result = canonicalizeTlsException('mongodb://localhost/?TLSAllowInvalidCertificates=true'); + expect(result.disableEmulatorSecurity).toBe(true); + expect(result.connectionString.toLowerCase()).not.toContain('allowinvalidcertificates'); + }); + + it('returns the input unchanged and no exception for an unparseable string', () => { + const result = canonicalizeTlsException('not-a-connection-string'); + expect(result.disableEmulatorSecurity).toBe(false); + expect(result.connectionString).toBe('not-a-connection-string'); + }); +}); + +describe('areAllHostsLocal (host-gating a TLS exception decided elsewhere)', () => { + it('is true when every host is local/private', () => { + expect(areAllHostsLocal('mongodb://localhost:10260/')).toBe(true); + expect(areAllHostsLocal('mongodb://192.168.1.5:27017,10.0.0.1:27017/')).toBe(true); + }); + + it('is false when any host is public (mixed seed list)', () => { + expect(areAllHostsLocal('mongodb://localhost:27017,prod.example.com:27017/')).toBe(false); + }); + + it('is false for a public host', () => { + expect(areAllHostsLocal('mongodb://cluster0.mongodb.net/')).toBe(false); + }); + + it('is false for a Unicode-dot homograph of a public host', () => { + expect(areAllHostsLocal('mongodb://example\u3002com/')).toBe(false); + }); + + it('is false for an unparseable string', () => { + expect(areAllHostsLocal('not-a-connection-string')).toBe(false); + }); +}); + +describe('resolveAllowInvalidCertificates (hybrid runtime policy: honor the flag only for local hosts)', () => { + it('returns true for a local/private host with the exception flag set', () => { + expect(resolveAllowInvalidCertificates(true, 'mongodb://localhost:10260/')).toBe(true); + expect(resolveAllowInvalidCertificates(true, 'mongodb://192.168.1.5:27017/')).toBe(true); + }); + + it('returns undefined for a local host without the flag', () => { + expect(resolveAllowInvalidCertificates(false, 'mongodb://localhost:10260/')).toBeUndefined(); + expect(resolveAllowInvalidCertificates(undefined, 'mongodb://localhost:10260/')).toBeUndefined(); + }); + + it('returns undefined (NOT false) for a public host even when the orphaned flag is set', () => { + // Staying silent (undefined) — never forcing `false` — lets the driver still honor an + // explicit `tlsAllowInvalidCertificates=true` URL param, while a BARE orphaned flag on a + // public host is not activated. + expect(resolveAllowInvalidCertificates(true, 'mongodb://cluster0.mongodb.net/')).toBeUndefined(); + expect(resolveAllowInvalidCertificates(true, 'mongodb://prod.example.com/')).toBeUndefined(); + }); + + it('returns undefined for a mixed seed list with a public host even when the flag is set', () => { + expect( + resolveAllowInvalidCertificates(true, 'mongodb://localhost:27017,prod.example.com:27017/'), + ).toBeUndefined(); + }); + + it('returns undefined for a Unicode-dot homograph of a public host with the flag set', () => { + expect(resolveAllowInvalidCertificates(true, 'mongodb://example\u3002com/')).toBeUndefined(); + }); + + it('returns undefined for an unparseable connection string (fail closed: no allow-invalid)', () => { + expect(resolveAllowInvalidCertificates(true, 'not-a-connection-string')).toBeUndefined(); + }); +}); diff --git a/src/documentdb/utils/tlsException.ts b/src/documentdb/utils/tlsException.ts new file mode 100644 index 000000000..8cdc2b30e --- /dev/null +++ b/src/documentdb/utils/tlsException.ts @@ -0,0 +1,150 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DocumentDBConnectionString } from './DocumentDBConnectionString'; +import { isLocalOrPrivateHost } from './hostClassification'; + +/** + * Canonicalize the TLS exception carried by a connection string (Local Quick Start design §7). + * + * Why this exists: TLS-allow-invalid is keyed off `emulatorConfiguration.disableEmulatorSecurity` + * (a stored flag), but a user-supplied connection string can ALSO request a TLS bypass via URL + * params: certificate-validation bypass (`tlsAllowInvalidCertificates`, the legacy alias + * `sslAllowInvalidCertificates`, or `tlsInsecure`), hostname-validation bypass + * (`tlsAllowInvalidHostnames` / `sslAllowInvalidHostnames`), and the low-level Node socket toggle + * `rejectUnauthorized` (inverse semantics: `false` = skip validation). + * Two sources of truth are dangerous: the UI could show "TLS enabled" while the URL silently + * disables certificate or hostname validation. And because the option is connection-wide, a public + * host must never be able to disable validation through a pasted/deep-linked URL. + * + * This helper makes `emulatorConfiguration.disableEmulatorSecurity` the SINGLE source of truth: + * - It strips every TLS-bypass param from the connection string (case-insensitive). Local/private + * connections re-derive the relaxed TLS posture from the stored flag (the option builders set + * `tlsAllowInvalidCertificates`), so no bypass param ever needs to persist in the string. Note a + * hostname-only bypass request is intentionally promoted to the broader certificate bypass for + * local hosts: `tlsAllowInvalidCertificates` (⇒ `rejectUnauthorized: false`) is a superset that + * also relaxes hostname checking, which keeps the single-source-of-truth model to one knob. + * - It returns `disableEmulatorSecurity: true` ONLY when a bypass was requested AND **every** host + * is local/private (loopback/RFC1918/etc., per §7.1). For a public host (or a mixed seed list), + * the bypass is dropped and the connection validates certificates and hostnames. + */ + +/** TLS-bypass URL params (lower-cased keys) whose value `true` disables certificate/hostname validation. */ +const TLS_BYPASS_KEYS = new Set([ + 'tlsallowinvalidcertificates', + 'sslallowinvalidcertificates', + 'tlsinsecure', + 'tlsallowinvalidhostnames', + 'sslallowinvalidhostnames', +]); + +/** + * The low-level Node TLS socket toggle `rejectUnauthorized`, which the MongoDB driver also accepts + * as a URL param. It has INVERSE semantics (`false` = skip validation) and is the one with the + * subtlest footgun: via a URL it is parsed as the *string* `"false"`, and Node's `tls.connect` + * only treats the *boolean* `false` as "skip validation" (`rejectUnauthorized !== false`), so a + * URL `?rejectUnauthorized=false` does NOT actually disable validation today. We still strip it + * from the stored string (it is a socket-level toggle that should never persist in a user-facing + * connection string) and, for hygiene + consistency, treat `=false` as a bypass *request* so a + * local user's intent is honored through the single source of truth and a public host is gated. + */ +const REJECT_UNAUTHORIZED_KEY = 'rejectunauthorized'; + +export interface CanonicalTls { + /** The connection string with all TLS-bypass params removed. */ + readonly connectionString: string; + /** Whether allow-invalid certificates should be enabled (bypass requested AND all hosts local). */ + readonly disableEmulatorSecurity: boolean; +} + +/** + * Delete every TLS-bypass param (case-insensitive) from a parsed connection string in place. + * Returns `stripped` (any bypass param was present and removed) and `bypassRequested` (the string + * asked to skip certificate/hostname validation: a `TLS_BYPASS_KEYS` param set to `true`, or + * `rejectUnauthorized` set to `false`). + */ +export function stripTlsBypassParams(parsed: DocumentDBConnectionString): { + stripped: boolean; + bypassRequested: boolean; +} { + let stripped = false; + let bypassRequested = false; + for (const key of [...parsed.searchParams.keys()]) { + const lowerKey = key.toLowerCase(); + const value = (parsed.searchParams.get(key) ?? '').toLowerCase(); + if (TLS_BYPASS_KEYS.has(lowerKey)) { + if (value === 'true') { + bypassRequested = true; + } + parsed.searchParams.delete(key); + stripped = true; + } else if (lowerKey === REJECT_UNAUTHORIZED_KEY) { + // Inverse semantics: `rejectUnauthorized=false` is the bypass request. + if (value === 'false') { + bypassRequested = true; + } + parsed.searchParams.delete(key); + stripped = true; + } + } + return { stripped, bypassRequested }; +} + +export function canonicalizeTlsException(connectionString: string): CanonicalTls { + let parsed: DocumentDBConnectionString; + try { + parsed = new DocumentDBConnectionString(connectionString); + } catch { + // Unparseable — leave it for the regular validators; never claim an exception. + return { connectionString, disableEmulatorSecurity: false }; + } + + const { stripped, bypassRequested } = stripTlsBypassParams(parsed); + + // Allow-invalid is connection-wide, so only honor it when EVERY seed host is local/private. + const allHostsLocal = parsed.hosts.length > 0 && parsed.hosts.every((host) => isLocalOrPrivateHost(host)); + + return { + connectionString: stripped ? parsed.toString() : connectionString, + disableEmulatorSecurity: bypassRequested && allHostsLocal, + }; +} + +/** + * Whether EVERY host in a connection string is local/private (§7.1). Use this to host-gate a + * TLS exception that was decided elsewhere (e.g. a wizard choice or a previously-stored flag), + * so a connection string later changed to a public/mixed host can never keep allow-invalid. + * Returns false for an unparseable or host-less string. + */ +export function areAllHostsLocal(connectionString: string): boolean { + try { + const parsed = new DocumentDBConnectionString(connectionString); + return parsed.hosts.length > 0 && parsed.hosts.every((host) => isLocalOrPrivateHost(host)); + } catch { + return false; + } +} + +/** + * Resolve the runtime `tlsAllowInvalidCertificates` MongoClient option from the stored + * `emulatorConfiguration.disableEmulatorSecurity` flag (design §7 "hybrid" runtime policy). + * + * The stored flag is honored ONLY when every host is local/private — the case where a self-signed + * certificate is expected. For a public host a bare stored flag is deliberately NOT activated, so an + * orphaned flag left on a connection (e.g. an old shared deep link that was later edited to drop its + * `tlsAllowInvalidCertificates` URL param) can't silently disable certificate validation after the + * flag was decoupled from `isEmulator`. + * + * Returns `true` to enable allow-invalid, or `undefined` to stay silent. It NEVER returns `false`: + * staying silent (rather than forcing `tlsAllowInvalidCertificates: false`) lets the MongoDB driver + * still honor an explicit `tlsAllowInvalidCertificates=true` that a user deliberately put in their + * connection string, so self-hosted databases on public hostnames keep working. + */ +export function resolveAllowInvalidCertificates( + disableEmulatorSecurity: boolean | undefined, + connectionString: string, +): true | undefined { + return disableEmulatorSecurity && areAllHostsLocal(connectionString) ? true : undefined; +} diff --git a/src/services/legacyEmulatorMigration.ts b/src/services/legacyEmulatorMigration.ts new file mode 100644 index 000000000..7d5251149 --- /dev/null +++ b/src/services/legacyEmulatorMigration.ts @@ -0,0 +1,217 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * One-time legacy emulator migration (Local Quick Start design §4). + * + * The dedicated "DocumentDB Local" emulator storage zone and its tree node are being + * retired in favour of regular connections + the Quick Start managed instance. On the + * first activation after the update we **copy** every connection from the `Emulators` + * storage zone into a new "Local Connections (Legacy)" folder in the regular `Clusters` + * zone, preserving credentials, auth config and (normalized) `emulatorConfiguration`. + * + * This relies on the storage-zone decoupling: a connection's operations are routed by the + * tree model's `storageZone` (set to `Clusters` by `FolderItem` for the migrated copies), + * NOT by `emulatorConfiguration.isEmulator`. The copies therefore keep `isEmulator: true` + * so local TLS-allow-invalid still works, while connect/rename/delete/move correctly target + * the `Clusters` zone. + * + * Safety properties (so a migration bug can never orphan a user's local connections): + * - The original `Emulators` zone is **kept** as a deprecated, read-only rollback path + * (§4.5) — we never delete it here. + * - Folder + copied-connection ids are **deterministic**, and a retry **creates only the + * missing** copies (it never overwrites an existing one), so a partial run that retries on + * the next launch is idempotent (no duplicates) and never reverts a user's later edits. + * - The completion flag is only set after **all** connections copy successfully; until + * then the legacy emulator tree node stays visible, so nothing is ever hidden while + * still un-migrated. + * + * Scope note: per §4 ("into that folder"), Emulators-zone **sub-folders are flattened** — + * every connection is copied directly under the single legacy folder. No data is lost (the + * originals remain in the Emulators zone); only the folder organization is not reproduced. + */ + +import { callWithTelemetryAndErrorHandling } from '@microsoft/vscode-azext-utils'; +import * as l10n from '@vscode/l10n'; +import * as vscode from 'vscode'; +import { ext } from '../extensionVariables'; +import { + ConnectionStorageService, + isConnection, + isFolder, + ItemType, + StorageZone, + type StoredItem, +} from './connectionStorageService'; + +/** globalState flag recording that the one-time migration has fully completed. */ +const MIGRATION_COMPLETED_KEY = 'documentdb.localQuickStart.legacyEmulatorMigration.completed'; + +/** Stable id of the destination folder, so retries reuse it instead of duplicating. */ +const LEGACY_FOLDER_ID = 'vscode-documentdb.legacyLocalConnectionsFolder'; + +/** Base name of the destination folder (a numeric suffix is added on a name clash). */ +const LEGACY_FOLDER_BASE_NAME = 'Local Connections (Legacy)'; + +/** Whether the one-time legacy emulator migration has completed (gates the tree node). */ +export function isLegacyEmulatorMigrationComplete(): boolean { + return ext.context.globalState.get(MIGRATION_COMPLETED_KEY, false); +} + +/** Deterministic id for a copied connection, so retries overwrite rather than duplicate. */ +function legacyConnectionId(originalId: string): string { + return `legacy_${originalId}`; +} + +/** + * Pick a root-level folder name that does not clash with an existing user folder in the + * Clusters zone (excluding our own deterministic folder so retries don't keep re-suffixing). + */ +async function uniqueLegacyFolderName(): Promise { + const rootFolders = await ConnectionStorageService.getChildren(undefined, StorageZone.Clusters, ItemType.Folder); + const takenNames = new Set(rootFolders.filter((f) => f.id !== LEGACY_FOLDER_ID).map((f) => f.name)); + if (!takenNames.has(LEGACY_FOLDER_BASE_NAME)) { + return LEGACY_FOLDER_BASE_NAME; + } + for (let i = 2; ; i++) { + const candidate = `${LEGACY_FOLDER_BASE_NAME} (${i})`; + if (!takenNames.has(candidate)) { + return candidate; + } + } +} + +/** + * Run the one-time migration. Best-effort and non-blocking: wrapped in + * `callWithTelemetryAndErrorHandling` so it never throws into activation; on any failure + * the completion flag is left unset so the next launch retries (idempotently) and the + * legacy node stays visible meanwhile. + */ +export async function migrateLegacyEmulatorConnections(): Promise { + if (isLegacyEmulatorMigrationComplete()) { + return; + } + + await callWithTelemetryAndErrorHandling('documentDB.quickstart.legacyMigration', async (context) => { + context.errorHandling.suppressDisplay = true; + context.telemetry.properties.outcome = 'started'; + + const allItems = await ConnectionStorageService.getAllItems(StorageZone.Emulators); + const emulatorConnections = allItems.filter(isConnection); + context.telemetry.measurements.itemsFound = allItems.length; + context.telemetry.measurements.connectionsFound = emulatorConnections.length; + + if (emulatorConnections.length === 0) { + // Nothing to migrate — mark done so we never run again, and refresh so the + // (now retired) legacy node disappears. + await ext.context.globalState.update(MIGRATION_COMPLETED_KEY, true); + ext.connectionsBranchDataProvider?.refresh(); + context.telemetry.properties.outcome = 'nothingToMigrate'; + return; + } + + // Reuse an existing legacy folder from a prior (partial) run so a retry never + // overwrites a user rename; only create it the first time. Guard that the id still + // refers to a folder — if corruption/a bug ever made it a connection, fall back to a + // fresh folder rather than parenting copies under a non-folder (which the storage + // cleanup would later treat as orphaned and delete). + const existing = await ConnectionStorageService.get(LEGACY_FOLDER_ID, StorageZone.Clusters); + const existingFolder = existing && isFolder(existing) ? existing : undefined; + const folderName = existingFolder?.name ?? (await uniqueLegacyFolderName()); + context.telemetry.properties.folderReused = String(!!existingFolder); + if (!existingFolder) { + context.telemetry.properties.folderSuffixed = String(folderName !== LEGACY_FOLDER_BASE_NAME); + await ConnectionStorageService.saveFolder(StorageZone.Clusters, { id: LEGACY_FOLDER_ID, name: folderName }); + } + + let failed = 0; + const copyMissing = async (connections: ReadonlyArray): Promise => { + for (const connection of connections) { + if (!isConnection(connection)) { + continue; + } + const destId = legacyConnectionId(connection.id); + // Create-if-missing: never overwrite an already-copied legacy connection + // (preserves a user's later edits) and never duplicate it. + if (await ConnectionStorageService.get(destId, StorageZone.Clusters)) { + continue; + } + try { + await ConnectionStorageService.saveConnection( + StorageZone.Clusters, + { + id: destId, + name: connection.name, + properties: { + ...connection.properties, + // Re-home under the legacy folder (replaces any Emulators-zone parent). + parentId: LEGACY_FOLDER_ID, + // Normalize the emulator flag exactly like LocalEmulatorsItem renders it, + // so local TLS-allow-invalid keeps working in the Clusters zone. Zone + // routing is handled separately by the tree model's storageZone. + emulatorConfiguration: { + isEmulator: true, + disableEmulatorSecurity: + !!connection.properties.emulatorConfiguration?.disableEmulatorSecurity, + }, + }, + secrets: { + connectionString: connection.secrets.connectionString, + nativeAuthConfig: connection.secrets.nativeAuthConfig, + entraIdAuthConfig: connection.secrets.entraIdAuthConfig, + }, + }, + false /* never overwrite — the get() above guarantees this is a new copy */, + ); + } catch (error) { + failed++; + ext.outputChannel.warn( + `[LegacyMigration] Failed to migrate emulator connection "${connection.name}": ${ + error instanceof Error ? error.message : String(error) + }`, + ); + } + } + }; + + await copyMissing(emulatorConnections); + // Reconciliation pass: re-read the Emulators zone and copy anything added after the + // initial snapshot (e.g. a localhost deep-link handled during this run), so we never + // set the completion flag — and hide the legacy node — while a source connection is + // still un-copied (closes the activation-window race). + const finalConnections = (await ConnectionStorageService.getAllItems(StorageZone.Emulators)).filter( + isConnection, + ); + await copyMissing(finalConnections); + + // Converged only when every current emulator connection has a Clusters copy. + let stillMissing = 0; + for (const connection of finalConnections) { + if (!(await ConnectionStorageService.get(legacyConnectionId(connection.id), StorageZone.Clusters))) { + stillMissing++; + } + } + context.telemetry.measurements.connectionsMigrated = finalConnections.length - stillMissing; + context.telemetry.measurements.connectionsFailed = failed; + + // Refresh so the copies appear immediately. + ext.connectionsBranchDataProvider?.refresh(); + + if (failed > 0 || stillMissing > 0) { + // Leave the flag unset: retry next launch (idempotent), keep the legacy node visible. + context.telemetry.properties.outcome = 'partial'; + return; + } + + // The Emulators zone is intentionally KEPT as a read-only rollback path (§4.5). + await ext.context.globalState.update(MIGRATION_COMPLETED_KEY, true); + ext.connectionsBranchDataProvider?.refresh(); + context.telemetry.properties.outcome = 'completed'; + + void vscode.window.showInformationMessage( + l10n.t("Your local connections have been moved to '{0}' in the Connections view.", folderName), + ); + }); +} diff --git a/src/services/localQuickStart/ContainerRuntime.ts b/src/services/localQuickStart/ContainerRuntime.ts new file mode 100644 index 000000000..a602c8b79 --- /dev/null +++ b/src/services/localQuickStart/ContainerRuntime.ts @@ -0,0 +1,404 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Thin wrapper over `@microsoft/vscode-container-client` (Docker) for the Local + * Quick Start POC (WI-0). + * + * - All runtime stdout/stderr/command lines are routed through a single + * {@link MaskedChannelWritable} that **line-buffers** and **redacts secrets** + * before writing to the "DocumentDB Local Quick Start" OutputChannel (D14): + * the generated password must never reach the channel, even when a stream + * chunk splits it across a buffer boundary. + * - `docker run` is detached (D4); because a detached run streams nothing back, + * {@link ContainerRuntime.followLogs} streams `docker logs -f` so the channel + * isn't silent during the readiness wait. + * - The image takes credentials as **post-image args** (`--username/--password`), + * which the client supports via `runContainer({ command: [...] })` — validated + * in WI-0, so no raw-CLI fallback is needed. + */ + +import { + DockerClient, + type InspectContainersItem, + type ListContainersItem, + ShellStreamCommandRunnerFactory, +} from '@microsoft/vscode-container-client'; +import { Bash, Cmd, type Shell, type ShellQuotedString, ShellQuoting } from '@microsoft/vscode-processutils'; +import { spawn } from 'child_process'; +import * as fs from 'fs'; +import * as net from 'net'; +import * as path from 'path'; +import { Writable } from 'stream'; +import * as vscode from 'vscode'; +import { MaskingLineBuffer, maskSecrets } from './outputMasking'; +import { + type DockerReadiness, + QUICK_START_PORT, + QUICK_START_PORT_BAND_END, + QUICK_START_PORT_FALLBACK_ATTEMPTS, +} from './quickStartTypes'; + +/** + * Shell used to run docker commands. A shell provider is REQUIRED so the runner + * applies each argument's quoting metadata: without one it sets + * `windowsVerbatimArguments` on Windows and drops quoting, which splits Go-template + * `--format {{json .}}` arguments on the space and breaks info/inspect/list. + */ +const SHELL_PROVIDER: Shell = process.platform === 'win32' ? new Cmd() : new Bash(); + +function errMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +let outputChannel: vscode.OutputChannel | undefined; + +/** Lazily create the shared OutputChannel. */ +export function getQuickStartOutputChannel(): vscode.OutputChannel { + if (!outputChannel) { + outputChannel = vscode.window.createOutputChannel('DocumentDB Local Quick Start'); + } + return outputChannel; +} + +export function disposeQuickStartOutputChannel(): void { + outputChannel?.dispose(); + outputChannel = undefined; +} + +/** + * Writable that line-buffers incoming chunks and masks each complete line + * before appending it to the OutputChannel (delegates to {@link MaskingLineBuffer}). + */ +class MaskedChannelWritable extends Writable { + private readonly lineBuffer: MaskingLineBuffer; + + constructor(channel: vscode.OutputChannel, secrets: ReadonlyArray) { + super(); + this.lineBuffer = new MaskingLineBuffer((line) => channel.appendLine(line), secrets); + } + + public override _write(chunk: unknown, _encoding: BufferEncoding, callback: (error?: Error | null) => void): void { + this.lineBuffer.push(String(chunk)); + callback(); + } + + public override _final(callback: (error?: Error | null) => void): void { + this.lineBuffer.flush(); + callback(); + } +} + +export interface CreateContainerOptions { + readonly imageRef: string; + readonly name: string; + readonly labels: Record; + readonly hostPort: number; + readonly containerPort: number; + /** Named volume mounted at {@link dataPath} so data survives recreation (§8/§11). */ + readonly volumeName?: string; + readonly dataPath?: string; + /** Paths to `--env-file`s carrying credentials, so they stay off the CLI (§8.2). */ + readonly environmentFiles?: ReadonlyArray; + /** Post-image args appended after the image ref (optional; creds now go via env-file). */ + readonly command?: ReadonlyArray; +} + +/** + * Stateless wrapper around a single Docker {@link DockerClient}. Each call + * builds a fresh runner so its masked stdout/stderr writables don't leak state + * between commands. + */ +class ContainerRuntimeImpl { + private readonly client = new DockerClient(); + + private makeRunner(secrets: ReadonlyArray, token?: vscode.CancellationToken) { + const channel = getQuickStartOutputChannel(); + const factory = new ShellStreamCommandRunnerFactory({ + // Non-strict: a non-zero exit still rejects, but harmless stderr warnings + // (e.g. `docker info`) do not. A shellProvider is required for arg quoting. + strict: false, + shellProvider: SHELL_PROVIDER, + onCommand: (command: string) => channel.appendLine('$ ' + maskSecrets(command, secrets)), + stdOutPipe: new MaskedChannelWritable(channel, secrets), + stdErrPipe: new MaskedChannelWritable(channel, secrets), + cancellationToken: token, + }); + return factory.getCommandRunner(); + } + + /** CLI-on-PATH + daemon-reachable check (design §9 prereq cards). */ + public async isDockerReady(): Promise { + // Host CPU architecture check (design §9): x64/arm64 are supported; arm64 may + // run the amd64 image under emulation. Independent of the Docker checks. + const arch = process.arch; + const platformSupported = arch === 'x64' || arch === 'arm64'; + + let cliVersion: string | undefined; + try { + const runner = this.makeRunner([]); + cliVersion = (await runner(this.client.checkInstall({}))).trim(); + } catch (error) { + return { cliInstalled: false, daemonReachable: false, arch, platformSupported, error: errMessage(error) }; + } + + try { + const runner = this.makeRunner([]); + await runner(this.client.info({})); + } catch (error) { + return { + cliInstalled: true, + cliVersion, + daemonReachable: false, + arch, + platformSupported, + error: errMessage(error), + }; + } + + return { cliInstalled: true, cliVersion, daemonReachable: true, arch, platformSupported }; + } + + /** True if the TCP port can be bound on loopback right now (pre-check, design §8.3). */ + public isPortFree(port: number = QUICK_START_PORT): Promise { + return new Promise((resolve) => { + const server = net.createServer(); + server.once('error', () => resolve(false)); + server.once('listening', () => server.close(() => resolve(true))); + server.listen(port, '127.0.0.1'); + }); + } + + /** + * Pick an available host port (design §8.3): prefer {@link preferred}, else try + * up to {@link attempts} random ports in `[preferred, bandEnd)`. Returns + * `undefined` if none are free. Only used for the default (non-explicit) port. + */ + public async findAvailablePort( + preferred: number = QUICK_START_PORT, + bandEnd: number = QUICK_START_PORT_BAND_END, + attempts: number = QUICK_START_PORT_FALLBACK_ATTEMPTS, + ): Promise { + if (await this.isPortFree(preferred)) { + return preferred; + } + const span = Math.max(1, bandEnd - preferred); + const tried = new Set([preferred]); + for (let i = 0; i < attempts; i++) { + const candidate = preferred + Math.floor(Math.random() * span); + if (tried.has(candidate)) { + continue; + } + tried.add(candidate); + if (await this.isPortFree(candidate)) { + return candidate; + } + } + return undefined; + } + + public async pullImage(imageRef: string, token?: vscode.CancellationToken): Promise { + const runner = this.makeRunner([], token); + await runner(this.client.pullImage({ imageRef })); + } + + /** `docker run` detached, returning the new container id. */ + public async createAndRunContainer( + options: CreateContainerOptions, + secrets: ReadonlyArray, + token?: vscode.CancellationToken, + ): Promise { + const runner = this.makeRunner(secrets, token); + const mounts = + options.volumeName && options.dataPath + ? [ + { + type: 'volume' as const, + source: options.volumeName, + destination: options.dataPath, + readOnly: false, + }, + ] + : undefined; + return runner( + this.client.runContainer({ + imageRef: options.imageRef, + name: options.name, + // `detached: true` already emits `-d --tty` (the client adds --tty + // whenever detached/interactive), matching the image README's `-dt`. + detached: true, + labels: { ...options.labels }, + ports: [{ containerPort: options.containerPort, hostPort: options.hostPort }], + mounts, + environmentFiles: options.environmentFiles ? [...options.environmentFiles] : undefined, + command: options.command ? [...options.command] : undefined, + }), + ); + } + + public async inspectContainer(nameOrId: string): Promise { + try { + const runner = this.makeRunner([]); + const items = await runner(this.client.inspectContainers({ containers: [nameOrId] })); + return items?.[0]; + } catch { + return undefined; + } + } + + /** Read the host port actually bound to `containerPort` (design §8.3, D11). */ + public getBoundHostPort(item: InspectContainersItem, containerPort: number = QUICK_START_PORT): number | undefined { + const binding = item.ports?.find((p) => p.containerPort === containerPort && typeof p.hostPort === 'number'); + return binding?.hostPort; + } + + public isRunning(item: InspectContainersItem | undefined): boolean { + return !!item?.status && item.status.toLowerCase().includes('running'); + } + + public async startContainer(id: string): Promise { + const runner = this.makeRunner([]); + await runner(this.client.startContainers({ container: [id] })); + } + + public async stopContainer(id: string): Promise { + const runner = this.makeRunner([]); + await runner(this.client.stopContainers({ container: [id] })); + } + + public async removeContainer(id: string, force = true): Promise { + const runner = this.makeRunner([]); + await runner(this.client.removeContainers({ containers: [id], force })); + } + + /** Remove a named volume (best-effort; used for a clean fresh provision and on Delete). */ + public async removeVolume(name: string, force = true): Promise { + const runner = this.makeRunner([]); + await runner(this.client.removeVolumes({ volumes: [name], force })); + } + + /** + * Run a `/bin/sh -c