Fix GCP Parity Audit Findings Across 19 Services - #322
Conversation
…services (stackshy#321) High-severity (silent-wrong-data reads, broken flows, missing surface): - compute: echo networkInterfaces on instance read; image-from-disk (no phantom instance); operations list - networks: persist+echo firewall rules; autoCreateSubnetworks; subnet parent-network link - pubsub: subscription registry — distinct sub/topic names, ackDeadline/labels round-trip, effective delete, publishTime - monitoring: real alert-policy conditions/combiner/enabled/labels round-trip + alertPolicies.patch - clouddns: delete+add of the same rrset no longer AlreadyExists; unique change id; dnsName FQDN - iam: SA getIamPolicy/setIamPolicy, signBlob/signJwt/generateAccessToken, enable/disable - artifactregistry, eventarc: /operations/{op} routing so GAPIC .Wait() resolves (was 404) Medium fidelity: - artifactregistry: non-DOCKER format + description; eventarc: serviceAccount + labels - gcs: bucket PATCH wiring versioning + labels - cloudsql: getBackupRun surfaces real errors instead of masking as 404 - alloydb: report the driver's real lifecycle state (not hardcoded READY) - gke: apply desiredMaster/NodeVersion on UpdateCluster - cloudfunctions: emit httpsTrigger.url; add :generateUploadUrl - firestore: CreateDocument returns ALREADY_EXISTS on explicit-id collision - memorystore: memorySizeGb/redisVersion/displayName round-trip + instances.patch (Update) - cloudlogging: severity/jsonPayload/labels/insertId round-trip + orderBy desc - secretmanager: create makes an empty container (no phantom seeded version) - vertexai: endpoint :countTokens + stream routing - loadbalancer: backendService description/portName/healthChecks round-trip Each fix is backed by a real-SDK regression test. GCP fields the portable drivers can't model are round-tripped via reserved tags or handler-local state, keeping the shared driver interfaces (and the AWS/Azure providers) untouched.
…oken flows (stackshy#321) Standalone-server E2E (driving cloudemu serve --providers gcp with all handlers registered) surfaced bugs the single-driver package tests can't see: - Shared location-operations handler (server/gcp/lro): owns /v1/projects/{p}/locations/{l}/operations/{op}, registered first, returning a done operation (superset: longrunning `done` + GKE `status`). Fixes artifactregistry/eventarc/memorystore operation polls that alloydb/gke previously shadowed with a 404. - compute: instances.delete now removes the instance (GET-after-delete is 404) via a GCP-local hard-delete, instead of leaving a TERMINATED tombstone. - firestore: writes auto-create the collection (real Firestore behavior); a first document write no longer 404s "collection not found". - gcs: Matches no longer swallows unclaimed API-version paths (/v1, /sql, …) as bogus bucket lookups. - cloudlogging: entries:list sorts by timestamp for orderBy (was reverse-only, mis-ordering out-of-order writes). - pubsub: createTopic tolerates an empty body; networks: duplicate network create returns 409. Adds server/gcp/fullserver_test.go — a full-server harness exercising the collision paths so these regressions are caught in-suite.
- fcm: reject a message setting more than one target (token/topic/condition) — real FCM returns INVALID_ARGUMENT. - cloudasset: searchAllResources honors assetTypes and both search/assets.list emit nextPageToken so paged callers don't silently miss truncated results. (bigtable's InvalidArgument-on-missing-parent is intentional and tested, so it is left as-is; searchAllIamPolicies stays empty — it needs a cross-service IAM policy index the engine doesn't expose.)
NitinKumar004
left a comment
There was a problem hiding this comment.
Deep review — GCP parity (closes #321) — request changes
Reviewed at 9af10fbc (isolated worktree, whole-repo go test + -race, four adversarial lenses, verification against the vendored Go client structs). (The latest commit 9af10fb closes deferred cloudasset/fcm low nits — it does not touch any of the findings below, which still stand.)
The core of this PR is strong — clean dispatch with no collisions, 6 of 9 High findings fully fixed with real assertions, and the reserved-tag round-trip approach is leak-free and thread-safe. I'm requesting changes for two fixes that reintroduce the exact silent-wrong-data class #321 set out to eliminate, one headline High fix that looks undelivered for its target client, and three "fixed" claims with no test.
Gates — all green
build / vet / gofmt / go mod tidy clean · go test ./... 225 ok, 0 fail (no cascade) · -race clean on the full-server GCP harness. Honest scope: 58 GCP files, +2,167/−274; shared drivers untouched (verified).
Solid (verified, not just claimed)
- Dispatch / no cascade / no duplicate: the shared
lrohandler is tightly scoped (GET-only, 5-segmentlocations/…/operations/{op}) and does not steal real-data polls (every mutation inlines its result in the create response); gcsMatchesnow excludes reserved API prefixes; zero duplicate claims, no registration regression — guarded byfullserver_test.go. - 6/9 High fully fixed + asserted: compute NIC subnet+IP, networks firewall (all 6 fields + autoCreateSubnetworks + subnet parent link), clouddns delete+add, IAM verbs (all 7), AR format+description, eventarc serviceAccount+labels.
Blockers — correctness / wire
- monitoring
alertPolicies.patchsilently disables the policy.server/gcp/monitoring/handler.go:214setscur.Enabled = body.Enabledunconditionally while every other field is nil-guarded; withenabled bool json:"enabled,omitempty", a PATCH that omitsenableddecodes it tofalse. Createenabled=true→PATCH {"combiner":"OR"}→ policy silently disabled. This is the #321 silent-wrong-data class, reintroduced (real GCP honorsupdateMask). (inline) - pubsub
subscriptions.listignores the new registry.server/gcp/pubsub/handler.go:174-181emits one phantom subscription per driver queue, named after the topic, never consultingh.subs. After creatingbilling-subon topicevents, List returns a phantomeventssub and omitsbilling-sub— Get was fixed, List wasn't, and it re-violates the "distinct sub/topic names" High. (inline) - AR/eventarc GAPIC LRO
.Wait()looks undelivered + unverified.operation.responseis emitted without@type(server/gcp/artifactregistry/operations.go:101doneOperation), whereas the codebase's own convention elsewhere (cloudfunctions, vertexai, cloudasset) includes it — vertexai's comment explicitly says done LROs must carry@typefor SDK Any-decoding. The AR/eventarc regression tests usegoogle.golang.org/apiREST clients, not theapiv1GAPIC clients that findings #8/#9 targeted, so the "GAPIC.Wait()resolves (was 404)" headline is unproven and likely still broken (now a decode error instead of a 404). (inline) - monitoring drops non-threshold conditions.
server/gcp/monitoring/types.gomodels onlyconditionThreshold;conditionAbsent/ MQL / PromQL / matchedLog are discarded on decode and never echoed — silent data loss for those policy types.
Blockers — test coverage (the "every finding backed by a real-SDK regression test" claim is overstated)
- gke
desiredMasterVersion/desiredNodeVersion— implemented, but no test sets or asserts them (the SDK Update test asserts logging service instead). - loadbalancer
description/portName/healthChecks— round-trip implemented (operations.go:246-247), but the test asserts only Name+Protocol; the three named fields are never set or read back. - cloudfunctions
httpsTrigger.url+:generateUploadUrl— implemented, but no test exercises either endpoint.
Low (non-blocking)
- gcs
isReservedAPIPrefixfalse-rejects buckets namedv+digit (e.g.v2-assets) on the direct-media path. - monitoring/pubsub handler-local maps aren't project-scoped (cross-project name collision); monitoring is keyed by
displayName, so a duplicate displayName in one project 409s (real GCP allows it). - compute network-only NIC (only
network, nosubnetwork) reads backnetwork:"". - Weak coverage: gcs labels PATCH, cloudlogging
insertId, cloudsqlgetBackupRunerror path, alloydb state mapping — asserted partially or not at all. - Lint hygiene (PR-introduced, CLAUDE.md local 0-issue gate): gocyclo in cloudlogging/clouddns/iam changed funcs,
analoguemisspell in eventarc; the gcs:719errshadow is benign.
Net: excellent groundwork and clean dispatch. Fixing #1–#2 (guard Enabled / honor updateMask; make List read h.subs), delivering #3 (emit @type, and test with the GAPIC apiv1 client the finding names), modeling #4's condition types, and adding the three missing field-assertion tests (#5–#7) would get this to a clean approve.
| cur.NotificationChannels = body.NotificationChannels | ||
| } | ||
|
|
||
| cur.Enabled = body.Enabled |
There was a problem hiding this comment.
Medium (correctness) — PATCH silently disables the policy. Unlike every other field here (Conditions, UserLabels, NotificationChannels are all nil-guarded), cur.Enabled = body.Enabled is unconditional. Enabled is bool with json:"enabled,omitempty", so a partial PATCH that omits enabled decodes it as false: create a policy enabled=true, then PATCH {"combiner":"OR"} → the policy is silently disabled. This is exactly the #321 silent-wrong-data class the PR set out to kill. Real GCP alertPolicies.patch applies only the paths in updateMask; at minimum make Enabled a *bool (or gate it on updateMask) so an omitted field is left unchanged. The test at monitoring_test.go sends enabled:false alongside combiner, so it never exercises the omitted-field path.
There was a problem hiding this comment.
Fixed in 52bb2a4. patchPolicy now decodes enabled as *bool, so an omitted field leaves the policy's state unchanged (only an explicit enabled is applied) — a partial PATCH no longer silently disables it. The semantics test now PATCHes {"combiner":"OR"} (no enabled) and asserts the policy stays enabled.
…RO, missing tests (stackshy#321) Blockers: - monitoring alertPolicies.patch: decode enabled as *bool so an omitted field leaves it unchanged instead of silently disabling the policy. - pubsub subscriptions.list: read the subscription registry (distinct name + ackDeadline/labels) instead of emitting one phantom sub per topic queue. - artifactregistry/eventarc: emit the LRO response with an @type so a GAPIC apiv1 .Wait() can Any-decode it; add real apiv1 CreateX(...).Wait() tests. - monitoring: round-trip non-threshold conditions (conditionAbsent / MQL / PromQL / matchedLog) instead of modelling only conditionThreshold. Test coverage the earlier claim overstated: - gke Update now asserts desiredMasterVersion/desiredNodeVersion apply. - loadbalancer asserts description/portName/healthChecks round-trip. - cloudfunctions asserts httpsTrigger.url + generateUploadUrl. Low: - gcs Matches only reserves whole-segment API versions (v1, v1beta4) — a bucket like "v2-assets" is no longer false-rejected on the direct-media path. - fix "analogue" misspell in eventarc comments. Deferred (documented, non-blocking): monitoring/pubsub project-scoped keying (monitoring keyed by displayName), compute network-only NIC network:"" echo.
|
Thanks for the deep review — all blockers + the flagged Low items are addressed in Blockers
Missing test coverage (5–7) — added field assertions: gke Low — gcs Deferred (non-blocking, documented): monitoring/pubsub project-scoped keying (monitoring keyed by displayName → dup-displayName-across-projects 409) needs a key redesign the current tests depend on; compute network-only NIC reads |
NitinKumar004
left a comment
There was a problem hiding this comment.
Re-review — GCP parity (closes #321) — one change requested
Re-reviewed at 52bb2a4, the commit addressing my earlier review. All seven of my earlier findings are resolved, each with a passing test — this is very close. I'm holding on one remaining determinism nit before approve.
Gates — green
build / vet / gofmt / go mod tidy clean · go test ./... 225 ok, 0 fail (no cascade) · -race clean on all changed GCP packages.
Earlier findings — all fixed + verified
- monitoring
alertPolicies.patchsilent-disable →Enabledis now*bool; an omittedenabledis left unchanged. ✅ - pubsub
subscriptions.listphantom subs → now lists theh.subsregistry with real name/topic/ackDeadline/labels (TestSDKPubSubSubscriptionMetadata). ✅ - AR/eventarc GAPIC LRO →
operation.responsenow carries the correct@type, and — the part I most wanted — there are now realapiv1GAPIC tests (TestGAPICCreateRepositoryWait,TestGAPICCreateTriggerWait) drivingNewRESTClient(...).Create(...).Wait(). Both pass. ✅ - monitoring non-threshold conditions →
conditionAbsent/matchedLog/MQL/PromQL now round-trip (TestMonitoringNonThresholdCondition). ✅ - gke / loadbalancer / cloudfunctions untested claims → now set + assert the fields (
CurrentMaster/NodeVersion,TestSDKGCPBackendServiceRoundTrip,HttpsTrigger.Url+GenerateUploadUrl). ✅ - gcs
v+digit false-reject → fixed withreserved_prefix_test.go. ✅
Requested change (the only blocker)
Determinism — subscriptions.list returns subscriptions in nondeterministic order. The new List iterates the h.subs map, so with two or more subscriptions the response order varies run-to-run. This is the repo's determinism pillar (elsewhere lists sort before emitting), and the test only creates one subscription so it doesn't surface. Please sort the subscriptions (by name) before writing the response, and extend the test to two subs to lock the order. (inline)
Everything else is done — once the list is deterministic this is an approve.
| h.mu.RLock() | ||
| out := listSubscriptionsResponse{Subscriptions: make([]subscription, 0, len(h.subs))} | ||
|
|
||
| for subName, meta := range h.subs { |
There was a problem hiding this comment.
Low/determinism — nondeterministic list order. Iterating h.subs (a map) means subscriptions.list returns subs in a different order each run once there are 2+. The repo sorts list output elsewhere for determinism, and the test only creates one sub so it doesn't catch this. Collect into a slice and sort.Slice(...) by subscription name before writing the response; add a second subscription to the test to lock the order.
There was a problem hiding this comment.
Fixed in 91c238d. subscriptions.list now collects the subscription names, sort.Strings them, and emits in sorted order, so the response is deterministic regardless of map-iteration order — consistent with the repo's other list endpoints. TestSDKPubSubSubscriptionMetadata now creates a second subscription (analytics-sub, created after billing-sub but sorting before it) and asserts List returns [analytics-sub, billing-sub]; passes deterministically across -count=3.
…to feat/gcp-parity-audit
subscriptions.list iterated the h.subs map directly, so with two or more subscriptions the response order varied run-to-run. Collect the names, sort them, and emit in order — matching the repo's deterministic list pillar. Extend the SDK test to two subscriptions (created out of sort order) and assert the sorted result.
|
Addressed the one remaining blocker (determinism of
Local gates on the change: build / vet / |
NitinKumar004
left a comment
There was a problem hiding this comment.
Re-review — GCP parity (closes #321) — approve
The last remaining item from my previous review is now fixed. 91c238d sorts subscriptions.list output by name for determinism, and the test now creates a second subscription (analytics-sub, which sorts before billing-sub) and asserts len==2 in sorted order — a real regression guard proving the list sorts by name rather than echoing map/insertion order. Verified green (build + server/gcp/pubsub tests pass at this head).
That closes the loop on everything: all seven earlier findings were resolved in 52bb2a4 (monitoring PATCH *bool, pubsub subscription registry, AR/eventarc @type with real apiv1 GAPIC .Wait() tests, monitoring non-threshold conditions, and the gke/loadbalancer/cloudfunctions field-asserting tests), and this commit resolves the determinism nit I held on.
Nothing outstanding. LGTM.
Objective / Issue
Fixes the GCP parity audit findings in #321. The audit drove real
cloud.google.com/go/google.golang.org/apiclients againstcloudemu serve --providers gcpacross all 23 GCP handlers and catalogued bugs / gaps / enhancements. This PR resolves every High and every Medium finding (19 services), each backed by a real-SDK regression test.What we found (blast radius)
The emulator's GCP surface was strong on dispatch but had a recurring class of silent-wrong-data bugs: a resource created with configuration X read back as a hardcoded default, because the portable driver couldn't model the GCP-specific field and the handler dropped it. Plus two GAPIC-breaking LRO routing gaps and a few broken standard flows.
How we fixed it
/operations/{op}route that returns a done operation.High
networkInterfaces(subnet + assigned IP); image create no longer needs a phantom instance; operations listallowed/denied/sourceRanges/direction/priority/targetTags) persist + round-trip;autoCreateSubnetworks; subnet parent-network linkackDeadline/labelsround-trip, effective delete,publishTimeconditions/combiner/enabled/userLabelsround-trip;alertPolicies.patchAlreadyExists; unique change id;dnsNameFQDN:getIamPolicy/:setIamPolicy,:signBlob/:signJwt/:generateAccessToken,:enable/:disable/operations/{op}routing so GAPIC.Wait()resolves (was 404)Medium
artifactregistry non-DOCKER
format+description· eventarcserviceAccount+labels· gcs bucketPATCH(versioning + labels) · cloudsqlgetBackupRunsurfaces real errors · alloydb real lifecyclestate· gkedesiredMasterVersion/desiredNodeVersionapplied · cloudfunctionshttpsTrigger.url+:generateUploadUrl· firestoreALREADY_EXISTSon explicit-id create · memorystorememorySizeGb/redisVersion/displayNameround-trip +instances.patch· cloudloggingseverity/jsonPayload/labels/insertIdround-trip +orderBy desc· secretmanager create makes an empty container (no phantom version) · loadbalancer backend-servicedescription/portName/healthChecksround-trip · vertexai endpoint:countTokens+ stream routing.Alternatives not taken
md5Hash— deliberately left empty: the driver stores a SHA-256 ETag, not the object bytes, so any synthesizedmd5Hashwould mismatch the content and make checksum-validating clients fail — worse than omitting it.Test plan
go build ./...,go vet ./...,gofmt,go mod tidycleango test ./...green (full suite); new real-SDK round-trip tests per fixed service assert field valuesgolangci-lint --new-from-rev→ 0 new issuesDeferred follow-ups (Low-value long tail / needs infrastructure)
Tracked, not in this PR: monitoring
notificationChannelsREST surface; iam:undelete+ org-level roles (/v1/organizations/) +updateMaskon updateSA/updateRole; cloudsql PUT-vs-PATCH replace semantics (SDK Update sends the full resource, so partial-merge is observationally equivalent); firestore fabricated timestamps + RESTdocuments.listpagination; vertexai multimodalgenerateContentparts (driver is text-only); cloudassetsearchAllIamPolicies(no cross-service policy index exists) +assetTypesfilter; bigtable/fcm Low fidelity nits; cross-cutting list pagination (pageSize/pageToken/nextPageToken) — emulator datasets are small, low practical impact.Conclusion
All High + Medium GCP parity findings resolved across 19 services with regression coverage; shared drivers and other cloud providers untouched.
Closes #321
Standalone-server E2E round (68608ea)
Follow-up: drove the compiled
cloudemu serve --providers gcpbinary (all 23 handlers registered at once) with real REST lifecycles per service — not just the single-driver package tests. This surfaced full-server-only issues that package tests structurally can't (dispatch collisions between handlers sharing a path prefix, greedy fallbacks). All fixed and now guarded by a new full-server harness (server/gcp/fullserver_test.go):server/gcp/lro) — location-scoped operation polls (/v1/projects/{p}/locations/{l}/operations/{op}) were greedily claimed by alloydb/gke (registered first) and 404'd artifactregistry/eventarc/memorystore ops. One shared handler now owns them, returning a done operation (superset of longrunningdone+ GKEstatus).instances.deletenow removes the instance (GET-after-delete → 404) instead of leaving a TERMINATED tombstone.Matchesno longer swallows unclaimed API-version paths (/v1,/sql, …) as bogus bucket lookups.entries:listsorts by timestamp fororderBy(was reverse-only, mis-ordering out-of-order writes).createTopictolerates an empty body; networks duplicate network create → 409.Verified false positive: the agent-reported vertexai endpoint
:countTokens/:generateContent405 was a shell/curl artifact mangling the:verbsuffix when interpolated from a variable — a literal URL returns 200. The fix was already correct.Additional deferrals from this round: compute requested
networkIP/disksecho; regional forwardingRuleselfLink/portRange/target; unscoped/v1/roles+ org roles (now a clean not-found rather than a bogus bucket error); cloudasset export-op project-scoped collision with Cloud SQL; alloydb is shadowed by GKE in the shipped binary (they share/clustersand are mutually exclusive by design — AlloyDB needs a custom build); bigtable/fcm Low nits.