Skip to content

feat(provisioning): discovery endpoints for external writers#966

Draft
jghoman wants to merge 1 commit into
mainfrom
jakob/discovery-endpoints
Draft

feat(provisioning): discovery endpoints for external writers#966
jghoman wants to merge 1 commit into
mainfrom
jakob/discovery-endpoints

Conversation

@jghoman

@jghoman jghoman commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

What

Two read-only endpoints on the internal provisioning API — the machine-facing "which tenants exist and where do I write" surface for external writers (viaduck destination discovery; millpond's include-values source, shipped in millpond#107):

  • GET /api/v1/warehouses — per ready/resharding warehouse: org_id, duckling name, state, writable fence flag (false while resharding — a resharding warehouse is listed, not hidden, because vanishing reads as tenant removal downstream), teams array, metadata-store connection with a k8s SecretRef (no plaintext credentials in any payload), bucket, and config_generation (max updated_at across all warehouse+org rows regardless of state, so rows leaving the discoverable set bump it — genuinely monotonic).
  • GET /api/v1/warehouse-team-ids — bare sorted deduped JSON array of team ids, the values-only shape millpond's generic poller consumes. Deliberately not nested under /warehouses/: org ids are free-form slugs, so a static child would squat the namespace a future per-org GET /warehouses/:org_id needs.

Teams are M1-lite: derived from the org's default_team_id (one team per org, empty table suffix). The payload is shaped as a teams array from day one so the eventual per-team table (multi-team orgs, real suffixes) swaps in with no wire change.

Error contract (the load-bearing part)

An erroneous omission of a team from these responses eventually stops that team's ingestion silently (consumers damp removals, then apply them). So: transient store failures fail the whole request — a polling consumer keeps its last-known-good state; a 200 with a team silently absent is never served. Only a genuinely-missing org row / nil default_team_id degrades, per-warehouse, to an empty teams array — logged and counted on duckgres_discovery_broken_org_rows_total{reason} (sustained nonzero = a live tenant is silently unroutable; also counts cross-org duplicate-team conflicts). Both endpoints project from one shared assembly so the values projection can't drift to weaker semantics.

Known gap (documented in code, asserted as-is in the harness)

cnpg-shard rows carry only the metadata-store kind — the provision handler never writes endpoint/db/user and MetadataStoreCredentials has no production writer; the truth lives in the Duckling CR status. So cnpg tenants serve an empty connection block until the provisioner backfills the row on Ready (follow-up: sync CR status → row in the provisioner's watch, guarded to state == ready so the reshard runner keeps exclusive ownership of those columns mid-flip). Team-level consumers are unaffected; this only bites viaduck's connection-details consumption later.

Also

  • ListWarehousesByStates gains ORDER BY org_id (Postgres heap order shuffles between polls → phantom diffs for consumers).
  • configstore gains ListOrgsByNames (batch — kills the N+1 org lookup) and LatestConfigChange.
  • State-classification tripwire test: the state enum is open, and an unclassified new state would read as fleet-wide tenant removal to every discovery consumer — TestDiscoveryStateClassification forces a human decision when a state is added.

Testing

Authored with an agent; adversarial lead-QE and principal-SWE review agents ran against the initial version and their findings (transient-vs-missing error split, non-monotonic generation, cnpg row gap, ordering, namespace squat, leak-test hardening, state tripwire, broken-org observability) are incorporated. Checks actually run:

  • go test ./controlplane/provisioning ./controlplane/configstore + full just test-controlplane — pass
  • golangci-lint — 0 issues; gofmt clean
  • e2e: new discovery_endpoints assertion in tests/mw-dev/e2e/harness.sh (both provisioned orgs listed writable with their provisioned team ids, ext metadata round-trip + cnpg kind-only, no password-ish keys anywhere in the payload, bare sorted team-ids array) — runs in the per-PR mw-dev job
  • Payload secrecy pinned in unit tests with a sentinel PasswordAWSSecret value + recursive key scan

Follow-ups

  • Provisioner CR-status→row backfill for cnpg connection details (design above).
  • Org↔team mapping work (multi-team, real table suffixes — the A1 teams table) — next week.

Two read-only endpoints on the internal provisioning API, the
machine-facing 'which tenants exist and where do I write' surface for
external writers (viaduck destination discovery, millpond's
include-values source):

- GET /warehouses: per ready/resharding warehouse — org_id, duckling
  name, state, writable fence flag (false while resharding; a
  resharding warehouse is LISTED, not hidden — hiding reads as tenant
  removal downstream), teams array, metadata-store connection with a
  k8s SecretRef (no plaintext credentials in the payload), bucket, and
  config_generation (max updated_at over ALL warehouse+org rows
  regardless of state, so rows LEAVING the discoverable set bump it).
- GET /warehouse-team-ids: bare sorted deduped array of team ids —
  the values-only shape millpond's generic include-list poller
  consumes. (Not nested under /warehouses/: org ids are free-form
  slugs, so a static child would squat the namespace a per-org
  GET /warehouses/:org_id needs later.)

Teams are M1-lite: derived from the org's default_team_id (one team
per org, empty table suffix) — the payload is shaped as a teams array
from day one so a real per-team table swaps in invisibly.

Error contract: transient store failures fail the WHOLE request (a
polling consumer keeps last-known-good — the safe direction); only a
genuinely-missing org row / nil team id degrades, per warehouse, to an
empty teams array, logged and counted on
duckgres_discovery_broken_org_rows_total{reason} (sustained nonzero =
a live tenant silently unroutable). Both endpoints project from ONE
assembly so the values projection can't drift to weaker semantics.

Known gap, documented in code + asserted as-is in the harness: cnpg
rows carry only the metadata-store KIND today (endpoint/user/db +
SecretRef have no production writer; truth is the Duckling CR status),
so cnpg tenants serve an empty connection block until the provisioner
backfills the row on Ready. Team-level consumers are unaffected.

Also: ListWarehousesByStates gains ORDER BY org_id (no phantom diffs
between polls), configstore gains ListOrgsByNames (batch, kills the
N+1) and LatestConfigChange; state-classification tripwire test (the
enum is open — an unclassified new state would read as fleet-wide
removal); e2e harness discovery_endpoints assertion.
@github-actions

Copy link
Copy Markdown

Test Impact Plan

Deterministic summary of how this PR changes tests, CI runners, and coverage-risk signals.

Summary

Area Added Changed Deleted
Test files 1 2 0
E2E/journey files 0 0 0
Workflow files 0 0 0

Signals

  • Test cases: +11 / -0
  • Assertions: +46 / -0
  • Skips or known failures added: 0
  • Workflow continue-on-error added: 0
  • Workflow path filters added: 0
  • Test commands removed from justfile: 0
  • E2E/journey retry lines added: 0

Coverage risk: neutral or increased

No coverage-reduction warnings detected.

@jghoman
jghoman marked this pull request as draft July 17, 2026 16:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant