private links: model links as rows with controller-observed status#3063
Open
jshearer wants to merge 1 commit into
Open
private links: model links as rows with controller-observed status#3063jshearer wants to merge 1 commit into
jshearer wants to merge 1 commit into
Conversation
21cd0a4 to
2a55fa9
Compare
2a55fa9 to
be57daf
Compare
be57daf to
eb61a36
Compare
ef01fac to
1c4737b
Compare
…ved status Private links were a flat `data_planes.private_links` JSON array edited by hand, with the controller's provisioning results in separate `*_link_endpoints` columns and no way to tie an endpoint back to the link that produced it or to see a link's status. Model each link as a row in `internal.data_plane_private_links` with a stable id, its desired `config`, and controller-owned observed `status`/`details`/`error`, so the API reads status rather than reverse-engineering it from opaque endpoint blobs. * New `internal.data_plane_private_links` table, one row per link, unique on `(data_plane_id, provider, service_identity)` so a same-provider duplicate cannot collide on a Pulumi resource name and wedge the converge. A backfill, guarded by a pre-flight that aborts on legacy data the table cannot represent, seeds it from the existing column. * A `generation` counter, bumped by a `before update of config, provider` trigger that also clears the observation columns, versions the desired config. The controller pins each link's `(id, generation)` when it reads desired state and records a converge's status only against a matching generation, so an edit racing a converge is never stamped with a status computed from the pre-edit config. * A wake-only change trigger sends the controller a prompt `converge` on any link change. The controller reads desired links from the table and, after a converge, records each link's status by matching provisioned endpoints on `(provider, service_identity)`: matched is `provisioned`, unmatched is `pending`. * agent-api replaces the wholesale `updateDataPlanePrivateLinks` mutation with per-link `add`/`update`/`remove`, gated by `ModifyDataPlanePrivateNetworking`, and surfaces per-link status. The legacy `private_links` and `*_link_endpoints` columns are left in place so a controller on the pre-deploy binary keeps working during the rolling deploy; link edits are paused for the deploy window so the frozen column and the table cannot disagree while both binaries run, and a follow-up drops the columns and recreates `data_planes_overview`. Real `failed` status awaits an est-dry-dock change that emits a per-link result addressed by the id.
4a4fae1 to
b783360
Compare
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.
Reworks private networking so each private link is a first-class row with a system-assigned id and a controller-owned observed status, replacing the flat
data_planes.private_linksJSON array paired with the separate*_link_endpointsoutput columns.The controller now sends each link's id to est-dry-dock and records each link's status from the
link_resultsexport it echoes back, with every result recorded against the row its id addresses. The est-dry-dock counterpart adds the optionalidfield, thelink_resultsexport, and per-link handling of input validation errors; its fleet rollout gates this deploy (see Rollout).Background
The original API exposed the raw primitives we had: the
private_linkscolumn as desired config, and the three*_link_endpointscolumns as the controller's output. Building a UI on top surfaced two problems.service_name/service_attachmentinside a blob whose shape est-dry-dock owns. The correspondence is exact at apply time, discarded, and reconstructed downstream by every consumer; if the shape drifts, a healthy link reads as unprovisioned.The fix is the usual reconcile split: user-owned desired config next to a controller-owned observed status, per link. The id travels the full loop: controller to est-dry-dock in the stack model, echoed back per link in
control.link_results, recorded against the row it addresses.What changed
DB (new migration). New
internal.data_plane_private_linkstable, one row per link.id,data_plane_id,provider, the polymorphicconfig, agenerationcounter, a generatedservice_identity, controller-ownedstatus/details/error/observed_at, and timestamps.generationversions the desired config: abefore update of config, providertrigger bumps it and resets the observation columns (statustopending) on any desired edit, whatever the writer. The controller pins each link's(id, generation)when it reads desired state and writes status only against a matching generation, so an edit racing a converge is never stamped with a pre-edit status.(data_plane_id, provider, service_identity): a same-provider duplicate would collide on a Pulumi resource name and wedge the converge. The old column path had no guard.convergeon any desired change (insert, delete, orconfig/provideredit). The status write touches only observation columns, so it fires neither trigger and cannot loop.internal, unreachable via PostgREST, with no RLS or grants: every reader connects aspostgres, and the resolvers gate on the private-networking capabilities. Mirrorsinternal.invite_links.data-plane-controller.
created_at, id. Each modelprivate_linksentry carries its row id as a sibling field of the link config (PrivateLinkEntry); est-dry-dock parses it as optional, treats it as opaque, and echoes it back incontrol.link_results.link_results, matched to the pinned links by id:provisionedwith the result's details,failedwith its error.link_resultsis optional inControlExports: outputs from an est-dry-dock which pre-dates the export parse asNoneand write no statuses. Pinned by a parse test.configcolumn write-back stripsprivate_linksalongside the existingname/fqdnstripping: links are re-hydrated from the table on every read, so a persisted copy is dead data that drifts.control-plane-api.
privateLinksreturns{ id, config, status, details, error, observedAt }from the table; the link's cloud is theconfigunion variant.addDataPlanePrivateLink/updateDataPlanePrivateLink/removeDataPlanePrivateLinkreplace the wholesale mutation, gated byModifyDataPlanePrivateNetworking, with a clear duplicate-identity error.PrivateLinkConfig; the new row type takes thePrivateLinkname.Rollout
A single deploy with two coordination steps: the est-dry-dock fleet gate and an edit pause.
deploy_branchmust contain the est-dry-dock change before the new controller deploys. Sendingidto an older checkout fails config parsing (extra="forbid") and wedges that plane until its branch updates. Planes trackingmainpick it up automatically (the worker checks out freshorigin/<branch>every action); auditselect distinct deploy_branch from data_planesand rebase or retire divergent branches first. The reverse order is safe: without incoming ids, est-dry-dock emits null-id results, which both old and new controllers ignore.private_linkscolumn is left frozen: an old controller keeps reading it until replaced, and a new controller reads the table. The pause keeps the two from disagreeing.private_linksand*_link_endpointscolumns, recreatedata_planes_overview, remove the endpoint resolvers.Because
idis now part of the composed stack config, each plane with links gets a one-time config-diff converge on deploy. It produces no resource changes, and the stack-history check short-circuits the DNS waits.A deploy landing mid-converge is safe: the new binary may read outputs from the old binary's
pulumi up, whose results carry null ids, which is exactly the no-observation case. Rollback is redeploying the old binaries: the additive migration stays, and est-dry-dock keeps exporting the legacy endpoint arrays the old controller consumes.Status semantics
pendingmeans not yet observed for the current config: the initial state, and what any desired edit resets to.failedtoday covers per-link input validation errors est-dry-dock catches in its Pulumi program (for example an invalid AWS availability-zone id). These no longer abort the update: the failed link registers no resources while the rest of the plane converges.provisionedis est-dry-dock's program-level claim, trusted because the controller only reads stack outputs after a successfulpulumi up. Provider-side errors during the update still fail the wholepulumi upand the converge retries as before, writing no statuses; attributing those engine-level errors to links is tracked follow-up work.Editing or deleting a link is allowed in any status: the desired-edit trigger re-pends it and bumps its generation, and the generation guard makes an edit that races a converge safe, so there is no remove-and-add-only correction path.
Testing
service_identity()andprovider_str().privateLinks, plus add/update/remove and authorization tests including the duplicate-identity guard and updating a link that is alreadypending; the full suite is green.#[sqlx::test]onwrite_private_link_statusescovering a provisioned result, a failed result, no-observation for an absent link, skipped entries (null id, unknown status), and both sides of the generation guard. Parse tests pin thePrivateLinkEntrywire shape and thatControlExportswithoutlink_resultsparses asNone. Integration snapshots show the id in the serialized model andlink_resultsin the consumed exports..sqlxoffline cache.Follow-ups (not in this PR)
private_linksand*_link_endpointscolumns and recreatedata_planes_overview.pulumi up --continue-on-error, attribute provider errors to links from the engine event log, and converge the rest of the plane past a failing link.pendingAcceptance, GCPpsc_connection_status, Azure connection approval) indetails, since endpoint creation succeeds while a manual-accept connection is still pending.