draft(gcp): DiskAsyncReplication, and the plan-time reference gap that blocks it - #166
Draft
naxty wants to merge 2 commits into
Draft
draft(gcp): DiskAsyncReplication, and the plan-time reference gap that blocks it#166naxty wants to merge 2 commits into
naxty wants to merge 2 commits into
Conversation
…nly pair The nightly's disk-async-replication case fails the extract re-apply while CI on the same commit passes: CI runs the released formae, the nightly builds formae main, and formae main narrowed the strip these two fields relied on. A field hinted writeOnly + createOnly used to be dropped from the desired state unconditionally; it is now dropped only when the stored document holds no value at that path, so a genuine change to one is no longer silently ignored. DiskAsyncReplication's Read reported primaryDisk and secondaryDisk - both declared writeOnly - back as disk URLs, because the native ID already carries them. That put a value at both paths, so the new condition kept the fields in the comparison, and the extracted forma writes them as unresolved references to the two disks. Unresolved reference against a stored URL, on a createOnly path, plans a replacement of the pair already in place: delete plugin-sdk-test-disk-async-replication; create plugin-sdk-test-... Write-only promises the agent that a Read never returns the field, and the agent keeps the last-applied value in state on that promise. Honour it: Read reports no properties and establishes only what a read of a relationship has to, that the pair is live. Same treatment vpnTunnel's sharedSecret already gets. A discovered pair now carries no properties, so extracting one no longer names its disks - the native ID still does. That is the cost writeOnly always carries, and the discovery case checks the pair is found, unmanaged and correctly typed, none of which changes. Also adds a formae_branch input to the debug-conformance workflow. Without it a debug run installs the released formae and cannot reproduce a nightly-only failure at all; with it the run builds formae from the given branch exactly as the nightly does.
Read reported an empty object, and the agent validates required fields before it persists a resource: primaryDisk and secondaryDisk are both required, so every discovered pair was rejected and the discovery case timed out waiting for it to appear. Absent properties skip that validation - what the pair is, is still in its native ID. Measured, not reasoned: run 33153366449 has crud 8/8 green against formae main and discovery failing on "missing required fields: [primaryDisk secondaryDisk]". A probe branch that drops the write-only hint entirely (run 33154860464) still fails the extract re-apply, so the suppression is still needed and this is the narrow way to keep discovery.
naxty
marked this pull request as draft
August 28, 2026 08:53
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.
The resource
GCP::Compute::DiskAsyncReplicationis not a GCP API resource — noGETreturns one. It is the relationship between two ordinary disks in different regions, started and stopped with thestartAsyncReplication/stopAsyncReplicationverbs on the primary. The plugin invents the resource, its two properties and its native ID:Its two properties are the disks the pair joins. Both are immutable — which disks a pair joins is fixed at creation, and is exactly what the native ID is made of — and both are declared as references, because that is how a forma names another resource:
The defect
The extract step exports the live resource to a forma and re-applies it. That must be a zero-operation apply:
An extracted forma writes each field as a reference that is unresolved at plan time:
So the planner compares a
$refwith no$valueagainst the URL in state, on acreateOnlypath, and correctly concludes an immutable field changed. The replacement is the planner behaving properly on a comparison it should never have been asked to make.Why it surfaced on 2026-08-27
6d13459hints both fieldswriteOnlyalongsidecreateOnly. formae dropped such a field from the desired side before diffing, unconditionally, so nothing was compared.f0b5e61enarrows that strip: dropped only when the stored document holds no value at that path, so a genuine change to such a field is no longer silently ignored. Sound change — 386 fields across published schemas carry both hints.Readreported both fields back as disk URLs (the native ID has them). That put a value at both paths, so the fields stayed in the comparison — and the plain string also displaced the stored reference envelope whose$appliedrecord is how the planner recognises an unresolved reference as unchanged.c14ab919. CI runs released formae 0.89.0, the nightly builds formaemain.The bind, measured
writeOnly,Readreports the paircreateOnlyonly,Readreports the pairwriteOnly,Readreports{}writeOnly,Readreports nothingAll four against formae
main. Two findings:createOnlyalone is not enough. Run 33154860464 drops thewriteOnlyhint entirely and still fails the extract re-apply, so plan-time reference resolution does not reach this case even after the Aug 27 resolver work (085e8181"plan-time references resolve against effective desired state",274e782b"follow reference chains to the value the chain root will hold").Reporting nothing at all rather than
{}does not dodge that either — the resource still arrives at the persister with an empty property set (internal/metastructure/resource_persister/resource_persister.go:181).A run with
phases=bothis onemakeinvocation with two targets, so a CRUD failure stops the discovery phase before it starts — which is why the first two rows have no discovery result.What the agent needs to do
1. Resolve a plan-time reference to the value the target already holds. The desired side is a
$refwith no$value; state holds the URL that same reference resolved to when the resource was created. Resolve it and no plugin hint is needed at all: plaincreateOnly,Readreports the pair, discovery keeps its properties, and extract → re-apply is a zero-op by ordinary comparison. This is the fix that makes the type shippable.2. Stop requiring a
writeOnlyfield on a resource that came from a Read. AwriteOnlyfield can never be returned by a Read, so requiring one makes every resource carrying it undiscoverable — today that reads as a two-minute discovery timeout with the reason only in a debug line. This is smaller than (1), is not GCP-specific (386 fields across the published plugin schemas carrywriteOnly), and would unblock the suppression route as a stopgap.Either one lands and this branch becomes mergeable; (1) makes it a two-line hint removal instead.
What is on this branch
2b13349—Readstops echoing the write-only pair, honouring the hint rather than contradicting it. CRUD 8/8 against formaemain.c15bc04—Readreports absent properties rather than{}, an attempt at the discovery half that the run above shows is not sufficient.formae_branchinput fordebug-conformance, which lands separately via feat(gcp)!: withdraw DiskAsyncReplication until the agent can resolve a reference at plan time #172. Without it a debug run installs the released formae and cannot reproduce a nightly-only failure at all.The honest framing for whatever eventually lands in the plugin: it suppresses a diff the planner cannot compute, not a field the API declines to return. GCP does report the pairing — the secondary disk carries
asyncPrimaryDisk.diskandresourceStatus.asyncPrimaryDisk.state, andReadparses both to decide the pair is live.primaryDiskandsecondaryDiskare plugin-invented names whose values were synthesised from the native ID. We are choosing not to report them.