Repository navigation
fix(go-tc): vendor HardwareID/NetworkID/CacheSoftwareType — stale struct silently NULLs orc8r gateway identity (BLO-33496) - #3
Conversation
The vendored go-tc ServerV40/ServerV50 had drifted behind trafficcontrol's,
missing three fields. Any caller doing a full-object read-modify-write through
this client silently NULLed two columns it never intended to touch:
1. GetServers returns hardwareId/networkId
2. the stale struct has no field for them, so encoding/json discards them
3. the caller mutates one unrelated field and PUTs the whole object
4. TO's updateQuery writes hardware_id=:hardware_id unconditionally, so the
absent keys bind SQL NULL
Both columns are nullable with no DEFAULT, so this fails silently -- no error,
no log line. They are the orc8r gateway-identity columns, and the known live
caller is a magma service: beacon's public_address_indexer mutates only
Interfaces[0].IPAddresses and writes back the full object.
cache_software_type is not in updateQuery, so it survived; it is vendored here
anyway because the root cause is the drift, not this one field set.
Struct declarations are copied verbatim from upstream so both ServerV40 and
ServerV50 are now byte-identical to trafficcontrol's, and Upgrade()/Downgrade()
copy the new fields -- TO round-trips the decoded server through both before
writing, so a conversion that dropped them would erase just as silently.
Tests:
- v5-client round-trip test drives the real client against a Traffic Ops
stand-in whose PUT handler mirrors updateQuery. It is deliberately written
against an untyped row so it compiles -- and fails -- against the unfixed
struct, reporting the erasure rather than a build error.
- go-tc conversion test covers Downgrade/Upgrade preservation, which the
existing TestServerV5DowngradeUpgrade cannot catch (its fixture leaves the
fields nil, so reflect.DeepEqual passes either way).
Drift prevention: tools/structdrift compares struct fields between the two
trees and gates new drift. Against the unfixed struct it reports exactly the 6
fields above. A whole-file diff was measured and rejected as a gate -- 38 of
the 88 shared files differ, because this repo is a selective extraction.
36 pre-existing missing fields are baselined in allowed-drift.txt, untriaged
and explicitly not asserted to be harmless.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 8ee91d8
Critical Issues (0)
Important Issues (1)
- [native-codex]
.github/workflows/struct-drift.yml:23-25— the workflow does not trigger whentools/structdrift/allowed-drift.txtchanges. A PR can add or remove baseline exemptions without runninggo run ./tools/structdrift, so the new drift policy can be weakened silently until the scheduled run.- Include
tools/structdrift/allowed-drift.txtin the pull-requestpathslist, or otherwise ensure baseline changes execute and are reviewed by the same check.
- Include
Suggestions (0)
Strengths
- The regression test models the client/server JSON boundary and verifies both preservation of gateway identity and the intended interface mutation.
- The conversion test covers the otherwise easy-to-miss
ServerV4/ServerV50field propagation. - The drift tool intentionally compares fields rather than whole files, matching this repository's selective vendoring model.
Recommended Action
- Address the Important issue this cycle.
- Re-run the drift check after the workflow path change.
… gate (BLO-33506)
Follow-up to BLO-33496. Triaged all 36 baselined missing fields against
trafficcontrol master c4ccaffe. 13 are erasing, 23 are inert.
Erasing -- vendored here. Each column is written by a Traffic Ops UPDATE, bound
straight from the decoded request struct, and this client has both a read and a
write path for the type, so a full-object read-modify-write silently NULLs it:
DeliveryServiceV41 extCDNEnabled, extCDNAttributionSource
DeliveryServiceV50 extCDNEnabled, extCDNAttributionSource, acmeCertKeyType,
acmeCertDurationDays, acmeProfile
(updateDSQuery() sets all five, $62-$66)
CDNFederation/V5 provider (cdnfederations.go UpdateQuery + updateQuery)
UserServiceSession serviceClass, transitClass, footprintId, bandwidthKbps
(transport_session.go updateQuery(), $12-$15)
Also vendors the ExtCDNAttributionSource type, its constants and its validator,
which the DS fields need.
go-tc/roundtrip_drift_test.go is the regression test: it decodes a body carrying
each key, re-encodes, and asserts the key survived. All 13 cases failed before
this change and pass after. It runs in the existing ci.yml with no secrets.
Inert -- kept in allowed-drift.txt, now with a per-entry reason naming the test
each one fails. Notable findings:
- TenantV50's auth-retired set passes both tests but is safe anyway: the
tenant UPDATE is a CASE whose ELSE branch is the column itself, so an
omitted key preserves the stored value. That third condition is now
documented in the file.
- UserServiceSession.Delivery is a structdrift false positive -- the field is
promoted from the embedded models.Session, which the AST walk cannot see.
- RoleV40.PrivLevel is immutable; no UPDATE has ever written priv_level.
Removes .github/workflows/struct-drift.yml. It could never run: the job-scoped
GITHUB_TOKEN cannot read the private upstream, so it reported success while
having checked nothing. Rather than put a Blockcast/trafficcontrol credential in
a public repository's Actions secrets, the check moves to trafficcontrol, which
has lib/go-tc natively and can read this public repository with no secret at
all. Deleting an inert job loses no coverage and removes a misleading green tick.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
|
@ally re-review at head Re: Important —
|
…O-33506)
Dispositions Ally's Important finding on this PR. The finding was that
struct-drift.yml's `paths` list omitted tools/structdrift/allowed-drift.txt, so a
PR could add a baseline exemption without the check running -- weakening the
policy silently until the scheduled run.
That workflow is deleted in this PR (it was inert -- it could never read the
private upstream, and reported success anyway), so the exact line the finding
names is gone. The concern is not gone, and moving the check to trafficcontrol
makes it slightly worse: an exemption added here is now caught by the weekly run
there, not at PR time here.
This closes the gap as far as it can be closed without a credential:
- This test fails if any entry lacks a reason comment. It cannot prove a
reason is true -- nothing mechanical can -- but it makes adding an exemption
require writing the claim into the diff, where a reviewer reads it. Silent
weakening becomes visible weakening, which is what the finding asked for.
- go-tc/roundtrip_drift_test.go pins the 13 fields measured to be erasing and
does not consult allowed-drift.txt at all, so no edit to that file can
weaken it. The highest-risk set is guarded at PR time regardless.
Both run in the existing ci.yml with no secrets.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Disposition of Ally's Important finding — head 048c8adFinding (reviewed head Accepted. Dispositioned as follows. The file it names is deleted in this PR
The concern survives that, and moving the check makes it slightly worseAn exemption added here is now caught by the weekly run in trafficcontrol, not at PR time here.
Both run in the existing Residual, stated plainlyA newly drifted field that is not one of the 13 is caught by the weekly trafficcontrol run rather Also in this PR13 fields triaged as erasing are vendored (see |
|
@ally re-review at head Why this is being re-requested rather than chased. The previous request lost a race, and the
So the review ran against
No code change accompanies this comment; State at this head. Finding disposition, restated for the re-review. Ally's Important finding was that The check moves to trafficcontrol#1818, Merge order. This PR first — it adds |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 048c8ad
Prior Findings Dispositioned (1)
- prior:8ee91d8 important 1 — no-longer-applicable —
tools/structdrift/main.go:566— the.github/workflows/struct-drift.ymlcall site cited by the prior finding is absent at this head; the drift command is documented to run from the upstream repository instead.
Critical Issues (0)
Important Issues (0)
Suggestions (0)
Strengths
- The server round-trip regression test exercises JSON decode, an unrelated interface mutation, and the full-object PUT boundary that caused the data loss.
- The V4/V5 conversion test verifies that all three newly preserved server fields survive both conversion directions.
- The field-level drift tool matches the repository's selective-vendoring model, while the allowlist test requires a reason for every baseline entry.
Recommended Action
- No Critical or Important issues found; this review is clean.
Issue: https://paperclip.blockcast.net/BLO/issues/BLO-33496
The defect
The vendored
go-tcServerV40/ServerV50had drifted behindBlockcast/trafficcontrol's, missingHardwareID,NetworkIDandCacheSoftwareType. Any caller doing a full-object read-modify-write through this client silently NULLs two columns it never intended to touch:GetServers→ TO returnshardwareId/networkId.encoding/jsondiscards them.updateQuerywriteshardware_id=:hardware_id, network_id=:network_idunconditionally → the absent keys bind SQL NULL.Both columns are nullable with no DEFAULT, so this fails silently — no error, no log line. Verified end-to-end: TO's
Updatehandler declaresvar server tc.ServerV5zero-valued beforeDecodeBody, so an omitted key stays nil all the way toNamedQuery.cache_software_typeis not inupdateQuery, so it survived. It is vendored anyway because the root cause is the drift, not this one field set.Why it matters
HardwareIDis the orc8r hardware UUID tying a TO server row to its magma gateway. The known live caller is a magma service —cdn/cloud/go/services/beacon/servicers/indexer/public_address_indexer.goimportsv5-client, mutates onlyInterfaces[0].IPAddresses, and writes back the full object. Verified againstBlockcast/magmaat time of writing.Changes
go-tc/servers.go— the three fields onServerV40andServerV50, copied verbatim from upstream; both structs are now byte-identical totrafficcontrol's.Upgrade()/Downgrade()copy them too: TO round-trips the decoded server through both before writing, so a conversion that dropped them would erase just as silently.v5-client/server_roundtrip_test.go— drives the real client against a TO stand-in whose PUT handler mirrorsupdateQuery. Its row is an untyped JSON object on purpose, so the test compiles and fails behaviourally against the unfixed struct rather than failing to build.go-tc/servers_drift_test.go—Downgrade/Upgradepreservation. The existingTestServerV5DowngradeUpgradecannot catch this: its fixture leaves these fields nil, soreflect.DeepEqualpasses either way.tools/structdrift+.github/workflows/struct-drift.yml— the recurrence gate.UPSTREAM.md— documents the check. Deliberately does not set the unknown last-synced base; no selective sync was performed.Evidence
Round-trip test against the unfixed struct (test unchanged, only
servers.goreverted):Drift gate against the unfixed struct — exactly the 6 fields, exit 1:
Post-fix:
go build ./...,go vet ./...,go test ./...all pass; both structs diff clean against upstream.Notes for review
DeliveryServiceV50,CRConfig,TenantV50,UserServiceSessionand others. They are baselined inallowed-drift.txtso the gate can block new drift instead of failing on a backlog it cannot fix. They are untriaged and not asserted to be harmless — a missing field only erases data if its column is in an UPDATE and some caller does a full-object write, and read-only types likeCRConfiglikely fail the second. Triage is filed separately.Blockcast/trafficcontrolis private and this repo is public, so the job-scopedGITHUB_TOKENcannot read upstream. The job skips whenUPSTREAM_RO_TOKENis absent and emits a::warningplus a step-summary note saying the check did not run — a green tick alone must not be read as "no drift". Granting that token is a separate decision and is not made in this PR.HTTPSPort/TCPPortare present and pointer-typed on both sides and round-trip correctly. This is a separate defect found while falsifying that hypothesis.go-tc/broadcast.gois a pre-existinggofmtoffender, left untouched.🤖 Generated with Claude Code