NodeLink Filestore and Azure Files adapters
## Requirement
Original requirement (epic, verbatim): "Add support for GCP and Azure with the same functionality as AWS:
- Your Cloud (former Your AWS)
- NodeLink may work on GCP and Azure
- What else we have AWS specific
Out of Scope: Jaah Hosted (still remains AWS only)"
Acceptance this child owns (PRD, verbatim):
- AC6.1 NodeLink › Storage offers, per provider the org has ON, "New GCP Filestore", "Existing GCP Filestore", "New Azure Files (NFS)", "Existing Azure Files (NFS)" beside the AWS EFS choices; create provisions in the customer's account, existing adopts; failures show the provider's text. → verify: create one storage per provider on dev CP.
- AC6.2 (CP half) A NodeLink on a GCP storage attaches to a GCP node (Azure → Azure node) live, mounts fail closed and the node reports its real mount state; tokens shared through `{jaah-home}/auth/.shared` work across two nodes of that provider, exactly as `docs/policies/nodelink.md` states for EFS. → verify: attach to two nodes, log an account in on one, observe it usable on the other.
- AC6.3 The UI shows the minimum size/cost implication before creating (Filestore ≥1 TiB; Azure Files NFS needs Premium) so an operator is not surprised by the bill. → verify: read the create form.
Architect deviation D7a (recorded, binding): current provider floors are lower — Filestore **Regional** 100 GiB–10,239 GiB; `Microsoft.FileShares` NFS SSD 32 GiB–262,144 GiB; the form shows the REAL floor and monthly cost line from `min_size_note()`. Fallback if the spike finds `Microsoft.FileShares` not GA/reachable: classic `Microsoft.Storage` FileStorage `Premium_LRS` + NFS share (100 GiB floor), note "Premium, ≥100 GiB".
- AC7.3 (storage half) Every resource Jaah creates in GCP/Azure carries labels/tags equivalent to AWS's `ManagedBy=jaah` plus its node/org, and the "What Jaah creates" table lists them per provider. → verify: inspect the VM, disk, firewall/NSG and storage in each provider console.
- AC8.3 (storage doors) Every server door (… storage create) enforces the provider switch. → verify: API test per door returns 403 with the switch off.
## Scope (Architecture D6, D7, D7a, risk 3)
0. Step 1 — Azure network spike on the dev subscription (risk 3): can `Microsoft.FileShares` be reached via service endpoint from the connection subnet? If not, the adapter creates a private endpoint in the connection subnet (cost in the min-size line). If `jaah/test/azure` is not yet filled, decide from provider docs, record the decision, and leave the live confirmation to "Dev oracle run".
1. GCP Filestore `SharedStorageProvider` (on the pinned `app.gcp.auth.token` / `app.gcp.rest.call`): one **Regional** Filestore per storage (smallest tier with NFSv4.1); export `ipRanges` = the connection subnet CIDR (from the connection's resolved `network`), `NO_ROOT_SQUASH`; a NodeLink = subdirectory `/nodelinks/<id>`; `mount_spec` → `{"type":"nfs","server","export":"/share1/nodelinks/<id>","root_export":"/share1"}`; adopt an existing Filestore; state in `org_shared_stores.provider_state` / `nodelinks.provider_state`.
2. Azure Files adapter (on `app.azure.auth.token` / `app.azure.rest.call`): storage row = location (subscription, RG, region, subnet); one `Microsoft.FileShares` NFS share per NodeLink, 32 GiB min, SSD, `encryptionInTransitRequired=Enabled`, `rootSquash=NoRootSquash`, access limited to the connection subnet; `mount_spec` → `{"type":"aznfs",...}`; "Existing Azure Files (NFS)" adopts a share as a NodeLink.
3. Labels via `provider.labels(purpose="nodelink")` on the Filestore instance, Azure share and any private endpoint; add their rows to the "What Jaah creates" table.
4. `require_provider` on GCP/Azure storage create/adopt.
5. Console `apps/web/src/pages/nodes/nodeLinkStore/StorageSourceFields.tsx` (`:119-130`): the four new choices shown only for providers ON for the org, each create form shows `min_size_note()` text before Create.
6. `docs/policies/nodelink.md` GCP section stating both exceptions (no TLS — plain NFSv4.1 inside the customer VPC; no server-side isolation between NodeLinks on one Filestore — a node's root can see sibling NodeLinks of the same org) and an Azure section (share per link, TLS via aznfs).
Credentials & reach: Unit/integration tests | none beyond the worktree (`httpx.MockTransport` fixtures) | **verified**: nothing external. Live storage create + two-node token share (AC6.1, AC6.2, AC7.3) | `jaah/test/gcp` (incl. `roles/file.editor`, `file.googleapis.com` enabled) and `jaah/test/azure` (`Contributor`, `Microsoft.FileShares` registered) | **deferred**: not on the roster; operator BA D6 "You'll add creds later — create stubs for creds and tell me to fill in" — runs in "Dev oracle run".
Out of scope: Do NOT touch Jaah Cloud / Jaah Hosted (N1) or platform AWS usage (N2). No GCP/Azure control-plane infra (N3). No cross-cloud NodeLink (N6). No change to AWS EFS NodeLink behaviour (N7). No migration. Do NOT change the node-side mount code beyond fixes the adapters strictly need (the previous NodeLink child owns it). No cloud SDK (D1).
Done means:
- AC6.1 — verify: MockTransport tests for create/adopt/delete of storage and link on both providers assert the exact requests (Regional tier, `ipRanges` = subnet CIDR, `NO_ROOT_SQUASH`; FileShares NFS with `encryptionInTransitRequired=Enabled`, subnet-only access) and that a provider refusal surfaces its text; e2e: the four choices appear only for switched-ON providers. Live create per provider (deferred — runs in "Dev oracle run").
- AC6.2 CP half — verify: wire v2 test: a GCP link yields `mount.type == "nfs"` with `root_export`, an Azure link `mount.type == "aznfs"`. Live two-node token share (deferred).
- AC6.3 — verify: e2e reads the create forms: GCP shows the Regional ≥100 GiB floor with a monthly cost line, Azure the ≥32 GiB SSD floor (or the documented fallback note); text readable at 1280px and 390px (screenshots in the PR).
- AC7.3 storage — verify: tests assert labels/tags on Filestore, share and private endpoint; the docstring table lists them.
- AC8.3 — verify: with GCP (Azure) OFF, storage create and adopt return 403 (one test per door per provider).
- AC9.1 — verify: no provider branching in the shared NodeLink routes/orchestration (grep as in the previous NodeLink child).
- `docs/policies/nodelink.md` has GCP and Azure sections; `python3 tools/check_context.py` green.
- Spike decision recorded in the PR body. `./dc check quick` green; merge lane `./dc check full` green; Jaah Deploy succeeds.
Outcome
GCP Filestore + Azure Files NFS NodeLink adapters, the 4 console choices with cost notes, switch 403s, docs. Quick gate green, review lanes pass, UI checked at 1280/390. PR #6565 queued in lane. Azure spike: service endpoint, no private endpoint. Live runs deferred to Dev oracle run. Memory: proposed 1 gotcha (mem-23120).
Artifacts
Activity
- Draft
- Ready
- Runner started
- Runner ended (done)