Repository navigation
fix(edit): keep flags explicitly set to their zero value - #235
Denis-hamon wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes edit commands so explicitly set zero values survive omitempty serialization.
Changes:
- Adds address-based flag-to-field restoration.
- Integrates restoration into
EditResource. - Adds unit and baremetal integration tests.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
internal/services/common/explicit_flags.go |
Adds explicit zero-value restoration. |
internal/services/common/explicit_flags_test.go |
Tests restoration behavior. |
internal/services/common/common.go |
Integrates restoration into edits. |
internal/cmd/baremetal_test.go |
Tests disabling monitoring end-to-end. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| value := reflect.ValueOf(cliParams) | ||
| if value.Kind() != reflect.Ptr || value.IsNil() { | ||
| return |
There was a problem hiding this comment.
Confirmed, and fixed in 367aff1 → cc4f6a8.
I measured it on this tree before changing anything: of the 64 EditResource call sites, exactly one passed a pointer — baremetal, the one I wrote the patch and the test against. The other 63 handed over a copy, the guard clause returned immediately, and the mechanism did nothing at all. Silent, which is worse than broken.
56 call sites now pass a pointer. EditResource only marshals that argument to JSON, and a pointer to a struct marshals exactly like the struct, so nothing else moves.
Seven keep their current argument: nil for vrackServices, three map literals where every key is present by construction, the container registry OIDC payload which already reads Flags().Changed, and the two managed-database user specs rebuilt per engine — those two keep the defect, and matching by address cannot reach them.
Added explicit_flags_callsites_test.go, which walks the services tree and fails on any call site handing over a value, naming the file and the line. Reverting one site makes it fail, so it gates the behaviour and not its own presence.
There was a problem hiding this comment.
Fixed: the value-style callers now pass an address, and
explicit_flags_callsites_test.go walks the AST of every EditResource call
site and fails on any that passes a struct by value. One call site that this
test catches only once the stack is assembled (internal/services/ip/admin.go,
from #257) has just been fixed on that branch.
367aff1 to
cc4f6a8
Compare
|
@copilot Perform the review again on the two commits pushed since your last pass: the call sites now hand the parameters struct over by pointer, and a new AST test guards them. Golang files only. |
ec18ae1 to
7467d79
Compare
EditIpService handed EditResource a copy of IPServiceSpec. On this branch alone that changes nothing, because the flag-to-field matching does not exist here yet; once ovh#235 is in, flags match their field by address, and a copy holds none of those addresses. --sla-monitoring=false and every other flag set to its zero value would then be dropped silently. EditIpServiceInfo built the renewal payload inline. Same reasoning: the AST check ovh#235 adds only recognises the payload through a named variable, as vps.go already does. Both are no-ops today and stop a real defect from appearing the moment the two branches meet. Found by assembling the whole review stack, not by reading either branch. Signed-off-by: Denis Hamon <denis.hamon@ovhcloud.com>
Edit commands marshal their parameters struct, which uses omitempty. A flag set to false, 0 or the empty string therefore disappears from the body; the body is then merged into the resource fetched from the API, which restores the remote value. The call succeeds, nothing changes, and the CLI prints 'Resource updated successfully'. On a dedicated server, 'baremetal edit --monitoring=false' reports success while monitoring stays on — the operator finds out when the alerts fire during the maintenance window they thought they had silenced. Values the user set explicitly are now put back into the body before the merge. Flags are matched to struct fields by address rather than by name: pflag keeps the pointer it was given, so a flag value and the field it writes to share an address. Matching by name would have missed the commands that expose a field under an unrelated flag name, for instance PrivateNetworkRoutingAsDefault behind --routing-as-default. Nested structs are supported and siblings already present in the body are preserved. Fields the user did not mention stay out: an edit remains a partial update. This is shared code: the 64 edit commands across 28 products benefit. Refs: BM audit BM-03 (M3) Signed-off-by: Denis Hamon <denis.hamon@ovhcloud.com>
The review was right: the previous commit fixed one command, not seventy-one.
addExplicitlySetFlags matches flags to struct fields by address, because pflag
keeps the pointer it was given. EditResource received a copy of the parameters
struct from every service but BareMetal, and a copy holds none of the addresses
the flags were registered against. The guard clause on a non-pointer argument
then returned immediately, so the whole mechanism was a silent no-op — it fixed
nothing and reported nothing, which is the worst of both.
Sixty-three call sites now pass a pointer, joining the one that already did.
Nothing else changes: EditResource only marshals that argument to JSON, and a
pointer to a struct marshals exactly like the struct.
Seven call sites keep their current argument, each for its own reason:
- vrackServices passes nil, having no editable parameter;
- three commands build their body as a map literal, where every key is
present by construction and nothing can be dropped by omitempty;
- container registry OIDC already assembles its body from Flags().Changed;
- managed database and analytics rebuild a per-engine struct, so no flag
points into the value being sent. These two keep the defect, on the name
and roles of a database user; matching by address cannot reach them.
A test walks the services tree and fails on any call site that hands over a
value, naming the file, the line and the fix. Reverting a single site to its
previous form makes it fail, so it gates the behaviour rather than its own
presence.
The commit message of 17b106e overstated the reach of the change. It holds
from this commit on.
Refs: BM audit BM-03 (M3)
Signed-off-by: Denis Hamon <denis.hamon@ovhcloud.com>
A cross-review of the patch found two ways the guard could pass while the
mechanism stayed a no-op:
- a struct literal passed by value was accepted as "a body built by hand",
a rule that only holds for map literals: no flag points into a value
built on the spot, so nothing would be restored;
- the call was matched on the qualifier "common", so an aliased import of
the package made the call site invisible.
Both now fail, and a call with fewer than four arguments is reported instead
of being skipped in silence. Each addition carries its positive control: a
struct literal and an aliased call each make the test fail on the exact line.
Signed-off-by: Denis Hamon <denis.hamon@ovhcloud.com>
The guard requires a pointer because a flag stores the address it was given, so a copy of the parameters struct silently matches nothing. That reasoning does not apply to a body assembled from `cmd.Flags().Changed`: no flag needs to point into it, because it already holds exactly what the operator set. The allow-list carries one such entry for the container registry OIDC payload. A second one is coming with the service-info fix, which builds its renewal body the same way. Naming it here rather than when it lands keeps both merge orders green: an allow-list entry for an absent call site is inert, while a missing one fails the guard on whichever pull request arrives second. Signed-off-by: Denis Hamon <denis.hamon@ovhcloud.com>
95ff8d5 to
25e0cb1
Compare
EditIpService handed EditResource a copy of IPServiceSpec. On this branch alone that changes nothing, because the flag-to-field matching does not exist here yet; once ovh#235 is in, flags match their field by address, and a copy holds none of those addresses. --sla-monitoring=false and every other flag set to its zero value would then be dropped silently. EditIpServiceInfo built the renewal payload inline. Same reasoning: the AST check ovh#235 adds only recognises the payload through a named variable, as vps.go already does. Both are no-ops today and stop a real defect from appearing the moment the two branches meet. Found by assembling the whole review stack, not by reading either branch. Signed-off-by: Denis Hamon <denis.hamon@ovhcloud.com>
The end-to-end test so far, baremetal edit --monitoring=false, goes through a call site that already handed its parameters over by pointer, so it would pass whether or not the by-value call sites had been changed. vps edit is one of those that had: addExplicitlySetFlags matches flags to fields by address, and a copy holds none of the addresses the flags were bound to. TestVpsEditDisablesSlaMonitoring fails when VpsSpec is passed by value again (the PUT body has no slaMonitoring, so the matcher finds no responder) and passes with &VpsSpec. Signed-off-by: Denis Hamon <denis.hamon@ovhcloud.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EditIpService handed EditResource a copy of IPServiceSpec. On this branch alone that changes nothing, because the flag-to-field matching does not exist here yet; once ovh#235 is in, flags match their field by address, and a copy holds none of those addresses. --sla-monitoring=false and every other flag set to its zero value would then be dropped silently. EditIpServiceInfo built the renewal payload inline. Same reasoning: the AST check ovh#235 adds only recognises the payload through a named variable, as vps.go already does. Both are no-ops today and stop a real defect from appearing the moment the two branches meet. Found by assembling the whole review stack, not by reading either branch. Signed-off-by: Denis Hamon <denis.hamon@ovhcloud.com>
Updated 2026-10-01. As asked in review, there is now an end-to-end test on a call site this PR had to change, not only on one that already passed a pointer.
vps edit --sla-monitoring=falsemust send"slaMonitoring": false. The test fails whenVpsSpecis passed by value again and passes with&VpsSpec. The sentence on what is preserved is also corrected below: the flag's own value overwrites whatever the body held at that path, and siblings are what is kept.Description
Edit commands marshal their parameters struct, which uses
omitempty. A flag set tofalse,0or""therefore disappears from the request body. That body is then merged into the resource fetched from the API — which restores the remote value. The call succeeds, nothing changes, and the CLI prints✅ Resource updated successfully.Concretely, on a dedicated server:
The operator finds out during the maintenance window they thought they had silenced.
This PR puts explicitly-set values back into the body before the merge.
Flags are matched to struct fields by address, not by name. pflag keeps the pointer it was given (
type boolValue bool,newBoolValue(val, p)returns(*boolValue)(p)), so a flag value and the field it writes to share an address. Matching by name would have silently missed the commands that expose a field under an unrelated flag name —PrivateNetworkRoutingAsDefaultbehind--routing-as-default, or nested flags using dots such as--network.private.create.subnet-enable-dhcp— that is, it would have reproduced the same bug elsewhere.Nested structs are supported, values already present next to the restored field are preserved (the field itself takes the flag's value), and fields the user did not mention stay out: an edit remains a partial update.
Correction — the reach of the first commit was overstated
The automated review was right, and I had it wrong. The first commit claimed every edit command benefited. They did not.
Matching by address only works on the struct the flags were registered against.
EditResourcereceived a copy from every service but BareMetal, and a copy holds none of those addresses, so the guard clause on a non-pointer argument returned immediately. The mechanism was a silent no-op everywhere else — it fixed nothing and reported nothing, which is the worst of the two.Measured on this tree before the second commit, rebased on main at bdfc23b:
The second commit hands the struct over by pointer at 63 further call sites.
EditResourceonly marshals that argument to JSON, and a pointer to a struct marshals exactly like the struct, so nothing else changes.Seven call sites keep their current argument, each for its own reason:
nilomitemptycannot drop anythingpayloadFlags().ChangeduserSpecThe last two keep the defect, on the name and roles of a database user. Matching by address cannot reach them, and I would rather leave that visible than paper over it.
Rebased on main (bdfc23b, 2026-10-01). Main gained seven edit call sites since the first version of this PR. One of them,
EditShareSnapshotfrom #284, handed its struct over by value; the call-site test failed on it after the rebase, naming the line, and the second commit now covers it. The managed-database site from #283 keeps its newnetworkFieldsargument and passes its struct by pointer.Type of change
Checklist:
go mod tidySix unit tests in
internal/services/common/explicit_flags_test.go— false boolean kept, untouched fields left out, zero values of every kind, nested field reached through an unrelated flag name, siblings preserved, nil inputs tolerated — plus one end-to-end test asserting thatbaremetal edit --monitoring=falsesends"monitoring": falsein the PUT body.explicit_flags_callsites_test.gowalks the services tree and fails on any call site handing over a value, naming the file, the line and the fix. It carries a positive control: reverting a single site to its previous form makes it fail, so it gates the behaviour rather than its own presence. It also refuses to pass on an empty walk.go build ./...,GOOS=js GOARCH=wasm go build ./...,go vet ./...and the fullgo test ./...pass.make docproduces no change: the behaviour of existing flags is fixed, not their surface.