feat(provisioning): discovery endpoints for external writers#966
Merged
Conversation
Test Impact PlanDeterministic summary of how this PR changes tests, CI runners, and coverage-risk signals. Summary
Signals
Coverage risk: neutral or increased No coverage-reduction warnings detected. |
jghoman
marked this pull request as draft
July 17, 2026 16:28
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, metadata-store connection with a k8s SecretRef (no plaintext credentials in the payload), bucket, and config_generation (an opaque change token: max updated_at over ALL warehouse+org+org-team rows regardless of state, read BEFORE the data so the stamp is never newer than the rows). - 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 come from duckgres_org_teams with RESOLVED table locations: events_table/persons_table = <schema>.<override-or-derived>, data_imports_schema = override-or-<schema>_data_imports. The derivation lives once (resolveTeamTables); legacy overrides are BARE identifiers, now enforced at every write surface by the new ValidateOrgTeamTableName (provisioning upsert + admin PUT), with the admin UI placeholders corrected to teach the bare form. enabled is passed through as information only — it is the per-team query-serving switch (migration 000024, not yet enforced), NOT an ingestion signal; disabled teams stay in BOTH endpoints, and the only ingestion-stop signal is row absence. Error contract: transient store failures fail the WHOLE request (a polling consumer keeps last-known-good — the safe direction); only a warehouse with zero team rows degrades, per-warehouse, to an empty teams array, counted on duckgres_discovery_broken_team_rows_total. Bugs found and fixed along the way (postgres regression tests each): - creating a team with enabled=false stored TRUE on BOTH the provisioning upsert and admin create paths (gorm omits zero-valued default-tagged fields from INSERTs; the admin path additionally had gorm's RETURNING write-back masking the intent) - deleting a non-billing team was invisible to the change token (a plain DELETE bumps no updated_at) — DeleteOrgTeamTx now touches the parent org row (DB-clock now()) in the same transaction Also: ListWarehousesByStates gains ORDER BY org_id, configstore gains ListOrgTeamsByOrgIDs + LatestConfigChange, state-classification tripwire test (an unclassified new state would read as fleet-wide removal), e2e harness discovery_endpoints assertion, CLAUDE.md discovery section.
jghoman
force-pushed
the
jakob/discovery-endpoints
branch
from
July 21, 2026 14:17
173d246 to
679ad04
Compare
jghoman
marked this pull request as ready for review
July 21, 2026 14:18
This was referenced Jul 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 poller, shipped in millpond#107):
GET /api/v1/warehouses— per ready/resharding warehouse: org_id, duckling name, state,writablefence flag (false while resharding — a resharding warehouse is listed, not hidden, because vanishing reads as tenant removal downstream), teams, metadata-store connection with a k8s SecretRef (no plaintext credentials anywhere in the payload), bucket, andconfig_generation.GET /api/v1/warehouse-team-ids— bare sorted deduped JSON array of team ids, the values-only shape millpond's poller consumes.Teams (reworked for #969/#970's
duckgres_org_teams)Teams are the org's team rows, served with resolved table locations:
events_table/persons_table=<schema>.<override-or-derived>,data_imports_schema= override-or-<schema>_data_imports. The derivation lives once, inresolveTeamTables; legacy overrides are bare identifiers (never schema-qualified), now enforced at every write surface by the newValidateOrgTeamTableName(provisioning upsert + admin PUT), with the admin UI placeholders corrected — they previously taught the qualified form the contract forbids.enabledis passed through as information only. It is the per-team query-serving switch (migration 000024's definition, not yet enforced on the serve path), not an ingestion signal — disabled teams stay in both endpoints. Deriving "stop ingesting" from "stop serving queries" would turn a serving hold into permanent event loss; the only ingestion-stop signal is row absence.is_billing_team/backfill_enabledare deliberately not served (billing internals / not a stream-start input; adding later is wire-compatible).Error contract (the load-bearing part)
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 (after consumer-side damping that would silently stop a team's ingestion). Only a warehouse with zero team rows degrades, per-warehouse, to an empty teams array, logged and counted on
duckgres_discovery_broken_team_rows_total{reason}. Both endpoints project from one shared assembly.config_generationis an opaque change token: maxupdated_atacross all warehouse+org+org-team rows regardless of state, read before the data queries. Compare for equality only.Bugs found in merged code, fixed here (postgres regression tests for each)
enabled:falsestored TRUE on both the provisioning upsert and admin create paths — gorm omits zero-valued default-tagged fields from INSERTs, so the DB default won; the admin path additionally had gorm's RETURNING write-back masking the caller's intent. Proven by mutation against real postgres.updated_atbehind) —DeleteOrgTeamTxnow touches the parent org row (DB-clocknow()) in the same transaction; the coupling is documented onOrg.UpdatedAtandLatestConfigChangeand pinned byTestLatestConfigChangeCoversTeamsPostgres.Known gap (documented in code, asserted as-is in the harness)
cnpg-shard rows carry only the metadata-store kind — endpoint/db/user + SecretRef have no production writer; truth lives in the Duckling CR status until the provisioner backfills the row on Ready (follow-up: sync in the provisioner watch, guarded to
state == readyso the reshard runner keeps exclusive column ownership mid-flip). Team-level consumers are unaffected.Testing
Authored with an agent; three adversarial review rounds (lead-QE + principal-SWE agents ×2, then a scoped final pass over the fix-round code) — findings incorporated include the error-channel split, the resolved-name contract, the state-classification tripwire (
TestDiscoveryStateClassification: the state enum is open, and an unclassified new state would read as fleet-wide tenant removal), the two merged-code bugs above, and a clock-source fix (gorm.Expr("now()")vs CP-pod time) on the generation touch. Checks actually run:just test-controlplane,just test-configstore-integration(docker postgres),just test-controlplane-k8s— all passgolangci-lint0 issues;gofmtclean; harnessbash -ncleandiscovery_endpointsassertion intests/mw-dev/e2e/harness.sh(billing team withteam_<id>schema + resolved names, ext metadata round-trip, cnpg kind-only, no password-ish keys, bare sorted team-ids array) — runs in the per-PR mw-dev jobFollow-ups