feat: Kafka, Backup and DR, APIM and CDN control planes; ECS tag, VPC endpoint and PSC fidelity fixes - #1354
Conversation
…ka.googleapis.com v1)
Operations: clusters create/update/delete (LROs registered with the shared
location-operations poller), get, list (pageToken/pageSize); topics
create/get/list/patch/delete (synchronous; create/patch return Topic, delete
returns Empty). Deleting a cluster removes its topics. Snapshot/restore
included.
Validation: vcpuCount >= 3; memoryBytes 1-8 GiB per vCPU inclusive; 1-10
networkConfigs with projects/*/regions/*/subnetworks/* subnets; RFC 1035
clusterId; Kafka topic-name alphabet; partitionCount/replicationFactor > 0;
updateMask required, unknown/immutable/output-only paths rejected 400;
partitionCount may only increase; replicationFactor and kmsKey immutable.
Duplicates 409, missing resources 404.
Routing: the clusters path is identical to GKE's and AlloyDB's, so the
handler registers ahead of GKE and claims only Kafka-shaped creates
(capacityConfig/gcpConfig body), clusters it owns, lists where it owns a
cluster, and clusters/{c}/topics. Everything else falls through.
Out of scope: consumer groups, ACLs, Connect clusters/connectors, schema
registry, tlsConfig/updateOptions, brokerDetails/FULL view.
…apis.com v1)
Add projects.locations.backupVaults: create, get, list, patch, delete.
Mutations return done google.longrunning.Operations registered with the
shared location-scoped LRO poller, so Operations.Get replays the typed
BackupVault response and unknown operation names 404.
Validation: backupMinimumEnforcedRetentionDuration is required and must be
a non-negative Duration string (400 otherwise); vault id 3-63 chars;
accessRestriction/backupRetentionInheritance enums and RFC 3339
effectiveTime checked. Patch requires updateMask, rejects output-only or
unknown paths (400) and a stale body etag (409 ABORTED). Delete honors
force (a vault holding backups is 400 FAILED_PRECONDITION without it),
allowMissing, etag (409 on mismatch) and validateOnly; create/patch honor
validateOnly. Duplicate create 409, missing vault 404.
Output-only fields: state ACTIVE, deletable, etag rotating per update, uid,
backupCount/totalStoredBytes "0", deterministic serviceAccount
service-{12-digit number derived from project id}@gcp-sa-backupdr-pr.iam.gserviceaccount.com,
accessRestriction defaulting to WITHIN_ORGANIZATION. List supports
pageSize/pageToken and the locations/- wildcard.
Out of scope: data sources, backups, backup plans, management servers,
restores; requestId, filter, orderBy, view and
ignoreBackupPlanReferences are accepted and ignored. The provider mock's
SetUsage hook seeds usage to exercise the non-empty delete guard.
…ement/service) Service CRUD via the real armapimanagement/v3 ServiceClient: CreateOrUpdate, Get, Update (PATCH: properties merged key-by-key, tags/zones replaced when sent, sku/identity re-resolved when sent), Delete, ListByResourceGroup and List (subscription). LROs complete synchronously: PUT 201/200 and PATCH 200 carry provisioningState=Succeeded with no polling headers, DELETE is 200/204. Validation (400 InvalidParameter): location, sku.name (SDK SKUType enum), sku.capacity (Consumption = 0, other tiers >= 1), properties.publisherEmail and publisherName, service name (1-50 chars, starts with a letter, letters/ digits/hyphens, no trailing hyphen). A PATCH is re-validated after merge. Computed, stable fields: etag, createdAtUtc, provisioningState, the system-assigned identity principalId/tenantId, and the gateway / portal / developer portal / management / scm URLs derived from the name (Consumption reports only the gateway). Wired into the RG-purge cascade, snapshot/restore, resource discovery and Resource Graph. Out of scope: child resources (apis, products, subscriptions, policies, backends, ...), the gateway data plane, backup/restore, network configuration updates, soft-deleted services, and global name uniqueness.
…dler Operations (compute/v1 global/backendBuckets): insert, get, list (maxResults/pageToken, name filter), patch (RFC 7386 merge: cdnPolicy merges member-by-member), update (full replace; output-only edgeSecurityPolicy kept), delete, setEdgeSecurityPolicy. Every mutation returns a DONE global compute#operation recorded in the shared OperationRegistry and polled via global/operations. Storage: new optional driver capability GCPBackendBucketStore (services/loadbalancer/driver/gcp.go), implemented by the GCP LB mock over the existing opaque GCP resource store, so records snapshot with the other LB resources. AWS/Azure are untouched (type-asserted capability). Validation: RFC 1035 name; bucketName required and, when the GCS driver is wired (Drivers.Storage), must name an existing bucket; compressionMode and cdnPolicy.cacheMode enums; defaultTtl/maxTtl/clientTtl in 0..31622400 and defaultTtl <= maxTtl; serveWhileStale <= 604800; <= 5 bypass headers; negativeCachingPolicy requires negativeCaching. Patch/update validate the merged result under the store lock. Duplicate 409, missing 404. Reference integrity: url-map defaultService / pathMatchers[].defaultService / pathRules[].service naming a missing backend bucket is rejected (400); deleting a backend bucket a url-map routes to returns 400 resourceInUseByAnotherResource. Out of scope: signed URL keys, IAM policy, validating the edge security policy reference (no securityPolicies resource), mode-dependent TTL rules.
…Fingerprint and label list filters
POST .../regions/{r}/addresses/{name}/setLabels and
.../global/addresses/{name}/setLabels previously answered 405
methodNotAllowed. They now replace the address's label set under
labelFingerprint optimistic concurrency: the caller must send the current
fingerprint (read from Get), and a missing or stale one is rejected
412 conditionNotMet with no change applied. Success returns a DONE compute
Operation recorded in the shared gcprest OperationRegistry, so
regionOperations/globalOperations polls resolve it. Get now always carries
labelFingerprint (stamped at insert, recomputed on every setLabels), so a
new label set reads back under a new fingerprint and an empty set clears
the labels.
Address list and aggregatedList filters now understand
labels.<key>=<value> (and !=, eq, ne) in addition to name, instead of
treating a label filter as match-all.
Tested with the google.golang.org/api/compute/v1 AddressesService and
GlobalAddressesService SetLabels clients.
DescribeServices, DescribeClusters, DescribeTaskDefinition and DescribeTasks serialised the entity's own Tags field, a create-time snapshot, while TagResource/UntagResource write only the ARN-keyed tag store that ListTagsForResource reads. After the first tag write the describe calls and ListTagsForResource disagreed. Every create path (CreateCluster, CreateService, RegisterTaskDefinition, RunTask/StartTask) already seeds that store via recordTags, so it is made the single authority: a new liveTags(arn, fallback) helper reads it, and describeCluster, DescribeServices, observedTask and DescribeTaskDefinition overlay the live set onto the returned copy. The entity field is used only when the ARN was never recorded. include gating is unchanged: DescribeClusters still omits tags without include=TAGS; the service, task and task-definition describes return tags unconditionally, as they did before. Test: SDK round-trip (aws-sdk-go-v2 ecs) per resource kind: create with tags, TagResource adds one, UntagResource removes one, describe with include=TAGS and ListTagsForResource both return exactly the live set.
DescribeVpcEndpoints and CreateVpcEndpoint rendered groupSet as a list of bare strings. Real EC2 types VpcEndpoint.Groups as SecurityGroupIdentifier, so every <groupSet><item> carries <groupId> (and <groupName> when the group exists); the AWS SDK decoded the bare form as an empty GroupId. Name resolution now looks ids up one at a time when the batch lookup fails, so one dangling id no longer strips the names of the others.
A PSC consumer rule targets a producer's serviceAttachment (regional) or a Google APIs bundle, all-apis or vpc-sc (global only). The emulator dropped the rule's network and subnetwork and synthesized an EXTERNAL scheme for a rule that has none. - network and subnetwork are stored and returned - a PSC rule sent without a scheme reports none - pscConnectionStatus (ACCEPTED) and a stable pscConnectionId are returned - a bundle target on a regional rule is refused, as GCP does
NitinKumar004
left a comment
There was a problem hiding this comment.
Reviewed each area against the real APIs, and ran each one on cloudemu serve with the official SDKs, the CLIs and Terraform.
Verdict: changes requested. Four things block this:
- Managed Kafka's cluster list takes over GKE and AlloyDB lists in the same region.
- Managed Kafka rejects the numeric enums that the official Go client sends.
- Waiting on a Backup and DR vault delete fails in the official Go client.
- APIM says it works with Terraform, but
azurerm_api_managementcannot be created.
The ECS tag fix, the VPC endpoint fix and the CDN backend bucket work are correct. Every area passes build, vet, race tests, lint and the coverage doc check.
GCP Managed Kafka
High
server/gcp/managedkafka/handler.go:166-169: claiming the list "when Kafka owns at least one cluster" takes over GKE's (and AlloyDB's)GET /v1/projects/{p}/locations/{l}/clusters. Reproduced onserve:GETreturnedgke1, then after creating Kafka clusterk9the sameGETreturned only.../clusters/k9andgke1was gone. That breaksgcloud container clusters list --region, GKE SDK lists and AlloyDB lists whenever the services share a region. The path and shape are identical, so the body can't tell them apart. Options: probe GKE/AlloyDB ownership (wired likeSetBucketLister) and claim the list only when Kafka owns clusters there and the siblings own none, or merge the lists by owner.TestFullServerManagedKafkaSharesClustersWithGKE(server/gcp/operations_ownership_test.go:166) checks the GKE list only before any Kafka cluster exists, so it passes on the broken code. It should also assert the GKE list after a Kafka create.server/gcp/managedkafka/wire.go:66-67:rebalanceConfig.modeis decoded only as a string. The officialcloud.google.com/go/managedkafkav1.2.0 REST client sends enums as numbers (UseEnumNumbers), so CreateCluster withRebalanceConfig{Mode: AUTO_REBALANCE_ON_SCALE_UP}fails with400 malformed JSON body: cannot unmarshal number into ... rebalanceConfig.mode(same with curl"mode":2). Proto3 JSON allows both forms. Accept a number or a name asserver/gcp/privateca/enums.gonormalizeEnumNumbersdoes, and add a gapic-client test. The discovery-client tests send names and pass either way.
Medium
handler.go:160-164: when a Kafka cluster is missing, item requests fall through to GKE.PATCH .../clusters/k1?updateMask=labelsafter a delete returns GKE's405 METHOD_NOT_ALLOWEDinstead of404 NOT_FOUND, and GET and DELETE return GKE'scluster "k1" not found in "us-central1"rather than the Kafka error. A PATCH whose body is Kafka-shaped (capacityConfig,gcpConfig,rebalanceConfig, orlabelswith noclusterwrapper) should be claimed.CreateClustersucceeds with an id a GKE cluster already uses in that location. From then on Kafka claims every item route for that id and the GKE cluster can't be reached. This is the same missing cross-owner check as the first High.
Low
providers/gcp/managedkafka/managedkafka.go:212andoperations.go:188: the libraryGetOperationreports any unknown name as done, where the API returns 404. The standalone poll also returnsdone:truewith noresponse. The driver's operation store grows without bound, and in the assembled server the sharedlro.Registryowns polls, so this store is unused there.- Outputs the API returns are missing:
kafkaVersion, which defaults to3.7.x. It is also listed inclusterFixedPathsthough the API marks it an optional input.- LRO
metadata(OperationMetadata). - The
rebalanceConfig.modedefault ofNO_REBALANCE.
tlsConfig,updateOptionsandbrokerCapacityConfigare silently dropped.--async-settleisn't honoured: a new cluster is ACTIVE at once and never CREATING.- The subnet's region isn't checked against the cluster location, which the API requires.
- Coverage is below the 90% target: provider 80.1%, wire 78.8%.
anyWithType,formatTimeanddecodeBodyduplicate helpers in sibling packages.decodeBodydiffers fromgcprest.DecodeJSONonly in accepting an empty body.
Matches the API:
- Nesting and
int64-as-string capacity fields. - vcpu ≥ 3 and the 1:1 to 1:8 GiB ratio.
- Subnet format with 1 to 10 entries.
- A required updateMask, and rejection of immutable or output-only paths in snake and camel case.
- partitionCount can only grow and replicationFactor is immutable.
- Topic calls are synchronous, and topic delete returns
{}. - A topic's parent must exist, and cluster delete cascades to its topics.
- Errors use the google.rpc.Status shape.
Checked and fine: build, vet, race tests, lint and no coverage doc drift. On serve, curl covered create, poll, get, list, patch, topic CRUD and the error cases. The real cloud.google.com/go/managedkafka REST client passes create (Wait), get, list, update (Wait), topic create/list/delete, delete (Wait) and a 404 get-after-delete, as long as no rebalanceConfig is sent. Terraform hashicorp/google 8.4.0 google_managed_kafka_cluster and google_managed_kafka_topic apply, show a clean plan, update in place (capacityConfig,labels and partitionCount), plan clean again and destroy.
GCP Backup and DR
Scope. Only backupVaults is implemented (create, get, list, patch, delete, plus the LRO). managementServers, backupPlans, backupPlanAssociations, dataSources and backups are not. /backupPlans requests currently fall through to other handlers (see M3).
High
- H1
server/gcp/backupdr/operations.go:165(and the standalone poll at:185): the delete LRO returns{"done":true}with noresponse. Thecloud.google.com/go/backupdr/apiv1REST client'sDeleteBackupVaultOperation.Waitfails withunsupported result type <nil>(reproduced againstserve). The shared poller replays the same shape because the handler registersnil. Returnresponse: {"@type":"type.googleapis.com/google.protobuf.Empty"}asserver/gcp/managedkafka/wire.go:18does, and register that value. The tests only use the discovery client (google.golang.org/api/backupdr/v1), which ignoresresponse, so they pass on the broken code. A delete-and-Wait test with the gapic client would catch it.
Medium
- M1
providers/gcp/backupdr/validate.go:103: the vault ID check is length only.Bad_IDanda/bcare accepted.a/bcis stored as.../backupVaults/a/bc, shows up in list, but is unreachable by get or delete (the path falls through to Firestore and returns 404), leaving an orphan. Enforce^[a-z][a-z0-9-]{2,62}$. - M2
validate.go:213(applyMask) and:154: retention lock is not modelled. On a vault whoseeffectiveTimehas passed (locked), PATCH loweredbackupMinimumEnforcedRetentionDurationfrom 86400s to 1s and movedeffectiveTime, and both succeeded. On GCP a locked vault's retention can only increase and the lock time cannot change.checkRetention(:123) also accepts0sand1s; the documented range is 1 day to 99 years. - M3 Parent/child rules: only
forceversusbackupCountis modelled. A vault referenced by a backup plan deletes without anignoreBackupPlanReferencescheck. Reproduced with TFgoogle_backup_dr_backup_plan: the plan request was served by the existing gkebackup handler (it answered with agoogle.cloud.gkebackup.v1.BackupPlantype), then a plain vault DELETE succeeded. The misroute predates this PR, but it means Backup and DR plans silently land in GKE Backup. Worth a ticket or a note in the handler docs. - M4
providers/gcp/backupdr/backupdr.go:262:SetUsageis a production symbol used only by tests (backupdr_test.go,sdk_roundtrip_test.go), and its doc comment says so. Move it toexport_test.goor a test seam.
Low
- L1
operations.go:75:filterandorderByare silently ignored, so a filtered list returns every vault. - L2
wire.gowriteErr:errors[].reasonis upper-case (ABORTED,FAILED_PRECONDITION) where the rest are lower-case, and the etag error drops the vault name becausecerrors.Messagestrips the%wprefix. - L3 A
validateOnlycreate or delete still records and registers an operation.
Checked and fine: behaviour lives in the provider, RWMutex plus deep clones, Snapshottable round-trips including opSeq, dispatch yields operation polls to the shared poller. Build, vet, race tests and lint are clean and docs/coverage/gcp/backupdr.md has no drift. serve e2e (curl and the gapic REST client) covered create, get, list, patch, LRO poll, 404, duplicate and stale-etag 409, missing-mask 400, allowMissing and validateOnly. Terraform hashicorp/google 8.4.0 google_backup_dr_backup_vault apply, a clean plan, an in-place update, and destroy all pass.
GCP CDN, load balancing, PSC and dependencies
The code that is here is correct and does not change existing GCP routing. The gaps below are operations real users reach for in this area, not regressions.
High (missing operations)
server/gcp/loadbalancer/backendbuckets.go:132:backendBuckets.addSignedUrlKeyanddeleteSignedUrlKeyreturn 501, so Terraformgoogle_compute_backend_bucket_signed_url_keyfails with "Error 501: backendBuckets.addSignedUrlKey is not implemented", andcdnPolicy.signedUrlKeyNamesis never populated. Both actions need adding (take keyName and keyValue, list the name undercdnPolicy.signedUrlKeyNameson GET, never echo the value), plus the same pair on backendServices, which returns 405 today.POST .../urlMaps/{m}/invalidateCachereturns 405.serviceAttachmentsdoes not exist at all:POST .../regions/r/serviceAttachmentsreturns 501 andgoogle_compute_service_attachmentfails. So consumerAcceptLists, connectionPreference, connectionLimit and connectedEndpoints are not modelled.
Medium
forwardingrules_psc.go:28and:58: every PSC consumer rule reportspscConnectionStatus: ACCEPTED, even for a target like.../serviceAttachments/nosuch. GCP returns PENDING or REJECTED under ACCEPT_MANUAL or the accept/reject lists. Once service attachments exist, the status should come from the producer's lists and limits.refvalidation.go:260andoperations.go:1041: a PSC rule with nonetworkis accepted and gets an external34.xIPAddress. GCP requires the consumer network and an internal address. An explicitloadBalancingScheme: EXTERNALwith a service attachment target is also accepted, but PSC requires an empty scheme. Both should be rejected invalidatePSCTarget.backendbuckets_validate.go:137: the cdnPolicy cross-field rules are missing. These are all accepted but rejected by GCP:USE_ORIGIN_HEADERSwith defaultTtl, maxTtl or clientTtl;FORCE_CACHE_ALLwith maxTtl; andclientTtl > maxTtl(maxTtl 100 with clientTtl 5000 inserts fine).defaultTtlis checked only against an explicit maxTtl, not the 86400 default.applyBackendBucketDefaults(line 79) fills in cacheMode but not the documented defaultTtl, maxTtl and clientTtl of 3600, 86400 and 3600.
Low
operations.go:937numericID(pre-existing): this returns the full uint64 FNV hash, which can be around 1.6e19 and so above int64. Terraformgoogle_compute_forwarding_rulefails on read with "forwarding_rule_id: expected type 'int', got unconvertible type 'string', value: '16018893947941779668'". It affects every forwarding rule, so the PSC flow cannot complete in Terraform. Masking to 63 bits fixes it.server/gcp/vpc/address_labels.go: the labels andlabelFingerprintlive in the server-sideaddressStore. They are not in the provider and not in/_cloudemu/snapshot, so they are lost on restore, and they diverge between the library andserve. They belong in the provider.- The cdnPolicy validation and the url-map in-use delete check sit in the handler, over an opaque driver store (
providers/gcp/loadbalancer/backend_buckets.go:50). That matches the existing urlMaps and healthChecks code, but the in-use check atbackendbuckets.go:303races a concurrent url-map insert, as backendServices does.
Dependencies: the only go.mod change is a new armapimanagement/v3 v3.0.0 (MIT). go mod why shows it reached only from server/azure/apimanagement tests. It is not in go list -deps ./cmd/cloudemu, and go mod tidy is clean.
Checked and fine: build, vet, race tests on the loadbalancer, vpc, compute and persist packages, lint, and no coverage doc drift. On serve these work: backend bucket insert, get and patch (cdnPolicy merge), TTL rejection, the url-map dangling-reference 400, the in-use delete 400, snapshot persistence of backend buckets, setLabels 412 and 404, and label filters. Terraform google_compute_backend_bucket with cdn_policy applied, updated in place, gave a clean plan and destroyed.
Azure API Management
Only Microsoft.ApiManagement/service is implemented. apis, operations, products, subscriptions, backends, policies and named values return 404 InvalidResourceType (server/azure/apimanagement/handler.go:80-84), so the service, api, operation and product lifecycle and If-Match cannot be exercised yet.
High
providers/azure/apimanagement/apimanagement.go:14saysazurerm_api_managementsees no drift, but Terraform can't create one. With azurerm v4, apply fails onGET /providers/Microsoft.ApiManagement/locations/eastus/deletedServices/<name>→ 501. The provider makes this soft-delete check even withrecover_soft_deleted = false. Returning 404 there only moves the failure on: create then fails onGET .../service/<n>/apis, because azurerm lists and deletes the default Echo API after create, and refresh and destroy fail onportalsettings/signin. The minimum needed isdeletedServicesGET/DELETE (404 when nothing is soft-deleted), the apis list,portalsettingssignin/signup,tenant/accessand the service policy. Otherwise drop the Terraform claim and document the gap.
Medium
-
apimanagement.go:268: the etag is set once and never changes, and PUT and PATCH return the same value. Optimistic concurrency doesn't work, and the child resources will need If-Match on top of it. Rotate the etag on every write. -
validate.go:94: capacity has no upper bound.Developerwith capacity 5 returns 201. The Azure limits are Developer 1, Basic 2, Standard 4, Premium 12 and BasicV2/StandardV2 10. Zones are also accepted on tiers other than Premium. -
apimanagement.go:183: the service name is a global DNS label but isn't treated as one.svc1is created in both rg1 and rg2 with the samesvc1.azure-api.netgatewayUrl. Azure returns 409 on the second one.checkNameAvailabilityreturns 501. -
A PUT with a different
locationreturns 200 and silently keepseastus. ARM rejects this with 409InvalidResourceLocation. -
Architecture:
server/azure/apimanagement/types.go:141-200builds the defaults and computed properties in the server layer:- virtualNetworkType
- publicNetworkAccess
- notificationSenderEmail
- platformVersion
- the regional gateway URL
- the Consumption endpoint rules
The Go library therefore returns a different resource than
serve. These belong in the provider.
Low
apimanagement_discovery.go:32putsskuandskuCapacityin Properties instead ofAttrs.SKU,SKUCapacityandZones. The Resource Graph row has no top-levelsku{name,capacity}and showsproperties.skuas a string.- Validation errors use
InvalidParameterwhere APIM returnsValidationError. List responses have nonextLinkpaging.
AWS ECS tags and EC2 VPC endpoints
The ECS tag fix and the VPC endpoint groupSet fix are correct. groupSet items now carry groupId plus the resolved groupName (EC2 SecurityGroupIdentifier), and a dangling group still renders with only its id.
Medium
providers/aws/ecs/services.go:336(pre-existing, but it undercuts this fix):serviceTaskSpecgives tasks the service's create-timesvc.Tagsand ignorespropagateTags. After untaggingsand taggings2, a--force-new-deploymentstill launched tasks taggeds=1, and a service with the defaultpropagateTagsNONE still put its tags on its tasks. With NONE the tasks should get no tags. SERVICE should usem.liveTags(svc.ARN, …), and TASK_DEFINITION should use the task definition's live tags.- The fix covers clusters, services, tasks and task definitions only:
- Container instances:
RegisterContainerInstance --tags ci=1is dropped, andDescribeContainerInstances --include TAGSreturns null even after a TagResource. - Capacity providers:
Create/DescribeCapacityProvidersreturn "unknown ECS operation", yetTagResourceoncapacity-provider/FARGATEsucceeds.
- Container instances:
Low (pre-existing)
server/aws/ecs/taskdefs.go:153and DescribeServices return tags withoutinclude=TAGS. ECS only returns them when asked.tags.go:23TagResource accepts a nonexistent ARN, 51 tags andaws:-prefixed keys. ECS rejects all three with InvalidParameterException.- Old-format service ARNs (
service/s1) don't resolve.
No file overlap with #1335. It touches server/aws/ec2/tags.go and providers/aws/vpc/tags.go, and this PR touches endpoint.go and operations.go. They complement each other: on this branch ec2 create-tags on a VPC endpoint still fails with InvalidID.NotFound, which #1335 fixes.
Checked and fine: build, vet, race tests on 11 packages including persist, lint, and no coverage doc drift. APIM over curl: create, get, PATCH tags (replace), list, the ARG query, and idempotent delete (200 then 204). ECS tag and untag on each resource type with the aws CLI. aws_ecs_cluster, aws_ecs_task_definition and aws_ecs_service: apply, a clean plan, a tag change, a clean plan again, and destroy.
…rmat, retention lock, list filter)
H1: a delete operation came back as {"done":true} with no response, inline,
from the standalone poll and from the shared poller (the handler registered
nil). The cloud.google.com/go/backupdr/apiv1 REST client's
DeleteBackupVaultOperation.Wait rejects that with "unsupported result type
<nil>". Delete now returns and registers
{"@type":"type.googleapis.com/google.protobuf.Empty"}, as managedkafka does;
the standalone poll replays the vault for create/update ops and Empty
otherwise. Covered by delete-and-Wait tests through the GAPIC REST client
(added cloud.google.com/go/backupdr v1.16.0 as a test dependency).
M1: the vault id was only length-checked, so "Bad_ID" was accepted and
"a/bc" was stored under an unreachable name. It must now match the documented
rule: lowercase letters, digits and hyphens, starting and ending with a letter
or digit, 3-63 characters.
M2: the retention lock is modelled. Once effectiveTime has been reached,
backupMinimumEnforcedRetentionDuration may only increase and effectiveTime
cannot change or be cleared (FAILED_PRECONDITION). The retention must be
between 1 day and 99 years (previously 0s and 1s were accepted).
M3 (documented only): the handler docs now name the unimplemented
resources and note that /backupPlans is currently answered by gkebackup.
M4: SetUsage moved out of the production API into export_test.go; the
server tests seed usage through the snapshot/restore seam instead.
L1: list honours filter (name/state/description/accessRestriction/
backupRetentionInheritance/labels.<key> with = or !=, joined by AND) and
orderBy (name/createTime/updateTime, asc/desc). An unsupported expression is
400 INVALID_ARGUMENT instead of silently returning every vault.
L2: errors[].reason uses the camelCase tokens every other GCP handler uses
("aborted", "failedPrecondition"); gcprest now maps "aborted" to the
canonical ABORTED status. The etag error keeps the vault name.
L3: validateOnly create, update and delete no longer mint, record or register
an operation; they return a done operation with the inline response and no
name.
… tags, TagResource validation Follow-up to the ARN-keyed tag store becoming the single authority for Describe* include=TAGS. - Service tasks: serviceTaskSpec stamped the service's create-time tags on every task and ignored propagateTags. Tasks now get nothing for NONE (the default), the service's live tags for SERVICE, and the task definition's live tags for TASK_DEFINITION, read at launch so a TagResource/UntagResource before a forced redeploy shows up on the new tasks. The service's tags are now recorded before its first tasks launch. - Container instances: RegisterContainerInstance tags are stored in the tag store and returned by DescribeContainerInstances include=TAGS, reflecting TagResource/UntagResource. - Capacity providers: Create/Describe/Update/DeleteCapacityProvider were "unknown ECS operation". They are now implemented in the provider (driver interface + portable wrapper + wire handler): EC2_AUTOSCALING providers with the documented managedScaling defaults and ranges, MANAGED_INSTANCES providers (cluster-scoped, configuration echoed verbatim), the predefined FARGATE and FARGATE_SPOT in Describe, include=TAGS, the cluster filter, maxResults/nextToken (default page 10), and the documented name rules. FARGATE/FARGATE_SPOT cannot be updated, deleted or tagged; a provider still associated with a cluster or used by a service's strategy cannot be deleted. Capacity providers are included in snapshots. - DescribeServices, DescribeTaskDefinition and DescribeContainerInstances return tags only when include=TAGS is sent, as ECS does. - TagResource/UntagResource/ListTagsForResource resolve the ARN to the stored resource and reject, with InvalidParameterException: an unknown resource or non-ECS ARN, a short-format service ARN (the TagResource reference requires migrating to the long format before tagging), a predefined Fargate provider (write only), more than 50 tags on a resource, and aws:-prefixed keys or values (and removing aws: keys).
DescribeTasks always returned a task's tags. ECS returns them only when the caller sends include=TAGS, as DescribeServices, DescribeClusters and DescribeTaskDefinition now do. RunTask's response is unchanged. The roundtrip test now asserts both halves: no tags without include, the tags with it.
…LRO metadata replay
Location-scoped GCP services each carried a private copy of the same three
helpers (responseAny/anyWithType, formatTime, a decodeBody that tolerates an
empty body). Add one shared copy of each to server/wire/gcprest so new
services stop re-deriving them:
- TypedAny renders a value as a proto3-JSON google.protobuf.Any ("@type"
added to its object), the shape a done Operation's response and metadata
carry.
- FormatTime renders a google.protobuf.Timestamp (RFC 3339 UTC, ns) and a
zero time as "".
- DecodeOptionalJSON is DecodeJSON for requests whose body may be empty.
The shared lro.Registry only replayed an operation's response, so a service
whose operations carry OperationMetadata lost it on every poll. Add
RegisterWithMetadata; a done poll now replays metadata beside the response.
Register is unchanged for every existing caller.
…numeric enums, fill missing outputs
Managed Kafka, GKE and AlloyDB all serve /v1/projects/{p}/locations/{l}/clusters.
Real GCP separates them by hostname; the emulator cannot, and the Kafka handler
claimed the whole list as soon as it owned one cluster in a location, so
`gcloud container clusters list` and AlloyDB lists lost every cluster once a
Kafka cluster existed there.
Routing (server layer, since the conflict exists only on the shared path):
- The assembled server now hands the Kafka handler a read-only ClusterSibling
probe of whichever of GKE / AlloyDB is enabled, wired the way the load
balancer's BucketLister is. Kafka claims the list only where it owns a
cluster and the sibling owns none; where both do, the sibling keeps its list
(the shapes differ and the request says nothing about which API it is for).
- A Kafka-shaped PATCH of a cluster nobody owns is claimed, so it is Kafka's
404 instead of GKE's 405.
- A create reusing an id the other service already holds in that location is
409 ALREADY_EXISTS, in both directions, instead of making one cluster
unreachable.
Wire:
- rebalanceConfig.mode and the echoed output-only state decode as a name or a
proto3-JSON number. The official cloud.google.com/go/managedkafka REST client
sends numbers (UseEnumNumbers) and was rejected with 400 on any
RebalanceConfig.
- kafkaVersion, tlsConfig, updateOptions and brokerCapacityConfig are read,
stored and returned instead of dropped; LROs carry
google.cloud.managedkafka.v1.OperationMetadata.
- Uses the shared gcprest TypedAny/FormatTime/DecodeOptionalJSON helpers.
- A standalone package server answers operation polls from a private
lro.Registry: the typed response and metadata, and 404 for a name it never
issued (it answered done with no response for any name).
Provider:
- kafkaVersion defaults to 3.7.x and is an optional, updatable input (current
discovery doc); rebalanceConfig.mode defaults to NO_REBALANCE, and
MODE_UNSPECIFIED means unset, as proto3 has it.
- Subnets must be in the cluster's region (the project may differ);
brokerCapacityConfig.diskSizeGib >= 100; at most 10 CA pools, each a CA
Service pool name.
- --async-settle reports a new cluster CREATING before ACTIVE.
- GetOperation 404s an unknown name; the operation store is capped at 1000,
evicting the oldest.
Adds cloud.google.com/go/managedkafka v1.0.0 as a test dependency: it needs go
1.25 (what CI runs) and bumps no existing module. v1.1.0 would bump
cloud.google.com/go/longrunning v0.9.0 -> v1.2.0 and v1.2.0 needs go 1.26. Its
REST transport encodes enums as numbers, like v1.2.0.
numericID returned the full 64-bit FNV hash, so roughly half of all forwarding rules, backend services, url maps and backend buckets got an id above MaxInt64 (e.g. 16018893947941779668). The proto type is uint64, so the gapic client accepted it, but Terraform's google provider reads every compute id into an int and failed the forwarding-rule read with "forwarding_rule_id: expected type 'int', got unconvertible type 'string'", which blocked the PSC flow in Terraform entirely. Real GCP ids never set the top bit. Mask the hash to a non-zero 63-bit value (positiveID), the same fix clouddns already carries, and apply it to pscConnectionId too.
…heme A Private Service Connect consumer rule was accepted with no network and was handed an external 34.x IPAddress, and an explicit loadBalancingScheme (EXTERNAL, INTERNAL) on a service-attachment or Google APIs bundle target was stored as sent. In GCP a PSC endpoint is an internal address in the consumer's VPC: the network is required and loadBalancingScheme must be empty. validatePSCTarget now refuses both with the repo's "Invalid value for field 'resource.<f>'" 400, and a PSC rule sent without an IPAddress gets a stable internal 10.x address.
…faults backendBuckets accepted TTL combinations GCP refuses: USE_ORIGIN_HEADERS with defaultTtl/maxTtl/clientTtl, FORCE_CACHE_ALL with maxTtl, and clientTtl above maxTtl (maxTtl 100 + clientTtl 5000 inserted fine). defaultTtl was only compared with an explicit maxTtl, never the 86400 default, and applyBackendBucketDefaults filled cacheMode but none of the documented TTLs. Now: - USE_ORIGIN_HEADERS refuses any non-zero TTL; FORCE_CACHE_ALL refuses a non-zero maxTtl. - Under CACHE_ALL_STATIC, defaultTtl and clientTtl are capped by the effective maxTtl (explicit, else 86400). - Defaults: CACHE_ALL_STATIC gets defaultTtl 3600 / maxTtl 86400 / clientTtl 3600, FORCE_CACHE_ALL defaultTtl and clientTtl 3600, USE_ORIGIN_HEADERS none. A default never exceeds an explicit smaller maxTtl, so a default cannot be why a request is refused. - A merge patch that switches cacheMode drops stored TTLs the new mode forbids unless the patch itself sends them. The rules stay in the handler, next to the existing urlMaps and healthChecks validation. The lifecycle fixture used FORCE_CACHE_ALL with maxTtl 600, which GCP refuses; it now uses CACHE_ALL_STATIC with the same TTL assertions.
compute addresses / globalAddresses, and the labels and labelFingerprint setLabels gives them, lived in a server-side addressStore inside the networking handler. They were not in /_cloudemu/snapshot, so a restore lost every address (and its labels), and a Go-library caller could not see what `serve` held. The GCP networking provider now owns them through a new optional capability, driver.GCPAddressStore (insert, get, list, delete, IP allocation, setLabels). The provider stamps labelFingerprint on insert, does the fingerprint check-and-replace under its store lock, and snapshots the records together with the IP allocator counter, so a restored emulator neither loses an address nor hands its IP out again. The handler keeps only the wire shape (kind, id, selfLink, IN_USE overlay, list filters) and adapts to the capability; a networking driver without it answers addresses with 501. Coverage docs regenerated.
…ps.invalidateCache
backendBuckets.addSignedUrlKey / deleteSignedUrlKey returned 501, the
same pair on backendServices returned 405, and urlMaps.invalidateCache
returned 405. So Terraform google_compute_backend_bucket_signed_url_key
failed and cdnPolicy.signedUrlKeyNames was never populated.
- add/deleteSignedUrlKey (both backends) take {keyName, keyValue} /
?keyName=, list the name under cdnPolicy.signedUrlKeyNames on GET and
never store or echo the value. keyName follows the compute name
grammar, keyValue must be a base64url 128-bit key, a backend holds at
most 3 keys, a duplicate name is 409 and deleting an unknown one 404.
The names are output-only: a patch or update keeps them and a client
echo of the list is ignored.
- urlMaps.invalidateCache checks the url map exists and the rule names a
path starting with "/" (or cache tags), and returns a DONE operation.
…ttachment compute.serviceAttachments did not exist (POST .../regions/r/ serviceAttachments returned 501, so google_compute_service_attachment failed), and every PSC consumer rule reported pscConnectionStatus ACCEPTED, even for a target like .../serviceAttachments/nosuch. serviceAttachments now has insert, get, list, patch (JSON merge patch) and delete with DONE operations. The records and the connection decisions live in the GCP load-balancer provider behind a new optional capability, driver.GCPServiceAttachmentStore, stored with the other opaque GCP resources so they snapshot and restore. The provider validates connectionPreference (ACCEPT_AUTOMATIC / ACCEPT_MANUAL), targetService, natSubnets and consumerAcceptLists entries. A consumer rule targeting an attachment now: - is refused (400, "The referenced serviceAttachment resource cannot be found") when the attachment does not exist or is in another region; - is recorded in the attachment's connectedEndpoints with a status: ACCEPT_AUTOMATIC accepts every consumer; ACCEPT_MANUAL rejects a project or network in consumerRejectLists, accepts one matched by a consumerAcceptLists entry (projectIdOrNum, networkUrl or endpointUrl) while its connectionLimit has room, and leaves the rest PENDING; - is re-evaluated, in connection order, whenever the attachment changes or another endpoint disconnects, and reads CLOSED once the attachment is deleted. Google APIs bundle endpoints (all-apis, vpc-sc) stay ACCEPTED. The existing PSC tests targeted an attachment that was never created; they now create it first. Coverage docs regenerated.
The snapshot round-trip test now also deletes an address on the restored emulator and checks the 404s, the only path of the provider-backed address store the vpc package tests did not reach.
… lint Split routeServiceAttachments into collection and item routers and the consumerAcceptLists checks out of validateServiceAttachment (gocyclo), wrap a long PSC error string (lll), and drop two nolint directives that suppressed nothing (nolintlint). No behaviour change.
…te etags, enforce tier and name rules Review of stackshy#1354 found that azurerm_api_management could not be created and that several ARM behaviours of Microsoft.ApiManagement/service were wrong. Terraform surface. azurerm v4 (api_management_resource.go) GETs locations/{l}/deletedservices/{name} before every create (501 here), lists and deletes the sample Echo API and the Starter/Unlimited products after create, PUTs portalsettings signin/signup, reads policies/policy, portalsettings signin/signup/delegation (+ delegation/listSecrets) and tenant/access/listSecrets on refresh, and on destroy (purge_soft_delete_on_ destroy defaults to true) GETs the soft-deleted service and purges it. The provider now models soft delete as Azure does since 2020-06-01-preview: a delete (or a resource-group delete) keeps the service for 48 hours under deletedservices, where it can be read, listed, purged or recovered by a PUT with properties.restore. A new non-Consumption service is born with echo-api and the starter/unlimited products; the service policy, portal settings and tenant access (get/patch/listSecrets) are served. The sequence is replayed through the official armapimanagement clients; no terraform binary was run, so the package doc now claims only what that proves. Etag. The etag was minted once and never changed. It now changes on every PUT/PATCH (and on every child write), and a non-wildcard If-Match that does not match is 412 PreconditionFailed on the service and on the children. Tier rules. Capacity has per-tier ceilings (Developer 1, Basic 2, Standard 4, Premium 12, BasicV2/StandardV2 10) and zones are Premium-only, on create and on PATCH. Name and location. The service name is a global *.azure-api.net label: a second service of that name in any group or subscription is 409, a name held by a soft-deleted service is 409 until it is purged or recovered, and POST .../checkNameAvailability answers instead of 501. A PUT naming another location on an existing service is 409 InvalidResourceLocation instead of a 200 that silently kept the old one. Architecture. The defaults (virtualNetworkType, publicNetworkAccess, notificationSenderEmail, disableGateway, customProperties) and the computed properties (provisioningState, createdAtUtc, platformVersion, the gateway, regional gateway, portal, management and SCM URLs, the Consumption endpoint rules) moved from the server layer into the provider, so the Go library and serve return the same resource. Snapshots restore through the same path. Discovery sets Attrs.SKU/SKUCapacity/Zones so the Resource Graph row has a top-level sku{name,capacity}. Validation errors are APIM's 400 ValidationError. Lists page with $skip/$top and a nextLink via a new shared azurearm.Paginate. The server's unmodeled-property echo no longer reflects a delegation validationKey on GET (Azure serves it only via listSecrets).
|
Thanks for the thorough review, and for reproducing everything on Every fix has a test that fails on Blockers
Managed Kafka (aca0460, 956451e)
Backup and DR (4686c13)
CDN, LB and PSC (9f7b22e … cefe314)
APIM (db12f2f)
Gates on
|
The root module now requires cloud.google.com/go/longrunning v1.2.0, iam v1.11.0, managedkafka and backupdr. contrib/server's go.mod still pinned longrunning v1.0.0, so its 'go build' stopped with 'updates to go.mod needed' (the Contrib (server) CI job). dockerengine and realengine only gain the matching go.sum lines.
gcpResourceJSON sized its output map as len(res.Body)+internalFieldCount. CodeQL flags that addition as a size computation that may overflow (go/allocation-size-overflow). The hint only needs to be approximate; the map grows for the few server-injected members, so the constant is gone.
NitinKumar004
left a comment
There was a problem hiding this comment.
All four blockers are fixed, and I reproduced each fix on serve. Approving. What's left below is small or already existed before this PR.
Earlier findings
GCP Managed Kafka
- Kafka list taking over the GKE list: fixed. With GKE
gke1and then Kafkak9in the same region,GET .../locations/us-central1/clustersstill returns onlygke1. In a region with no GKE cluster, the gapicListClustersreturns the Kafka cluster. - Numeric enums: fixed.
cloud.google.com/go/managedkafkaCreateCluster withAUTO_REBALANCE_ON_SCALE_UPcompletes through Wait and reads the mode back."mode":2works, and"mode":7gets a 400. - PATCH of a missing cluster falling through to GKE: fixed. GET and DELETE of an id nobody owns still get GKE's 404. The reason you gave for that is fair.
- Create reusing an id another service owns: fixed. It returns 409 in both directions.
- Operations: fixed. An unknown operation gets 404, and create returns
metadatawith an OperationMetadata@type. - Missing outputs and dropped fields: fixed.
kafkaVersiondefaults to3.7.x.rebalanceConfig.modedefaults toNO_REBALANCE.tlsConfigandupdateOptionsread back.- A subnet in another region is refused.
- Coverage and duplicated helpers: fixed.
GCP Backup and DR
-
H1, delete and Wait: fixed. The gapic
DeleteBackupVaultOperation.Waitreturns nil, and so does a rebuilt handle. -
M1, ID format: fixed.
Bad_ID,a/bcandpvget 400. -
M2, retention lock: fixed. Once
effectiveTimehas passed:- a lower minimum retention gets 400;
- a higher one gets 200;
- moving
effectiveTimegets 400.
1sis refused. -
M3, backup plans routed to gkebackup: documented in the handler. That's reasonable, since the misroute predates this PR.
-
M4: fixed.
SetUsageis now only inexport_test.go. -
L1 to L3: fixed.
GCP CDN, load balancing and PSC
- Signed URL keys: fixed.
google_compute_backend_bucket_signed_url_keyapplies and destroys. invalidateCache: served. A missing url map gets 404.serviceAttachments: fixed.google_compute_service_attachmentwith ACCEPT_MANUAL and an accept list applies. Swapping to a reject list turns the consumer rule and theconnectedEndpointsentry toREJECTED.- PSC rule validation: fixed. A PSC rule with no
network, or withloadBalancingScheme: EXTERNAL, gets 400. - cdnPolicy cross-field rules: fixed.
numericID: fixed.forwarding_rule_idreads back in Terraform.- Address labels: fixed. They live in the provider now and survive snapshot, reset and restore.
- The url-map in-use delete race is still open. Fixing it later together with backendServices is fine.
Azure API Management
- Terraform create: fixed at the wire level. I replayed azurerm v4's create, read, update and destroy sequence with curl against
serve. That covers the soft-delete check, PUT, removing the sample APIs and products, portal settings, tenant access, the policy GET, listSecrets, the PATCH update, DELETE, and the deletedservices purge. Every call returns the status azurerm expects. Dropping the "no drift" claim until a real apply has run was the right call. - Etag: fixed. It changes on every write, and a stale
If-Matchgets 412. - Capacity ceilings: fixed.
- Global name: fixed. It returns 409
ServiceAlreadyExists, andcheckNameAvailabilityanswers. - Location change: fixed. It returns 409
InvalidResourceLocation. - Defaults moved into the provider: fixed.
- Paging: fixed.
- Persistence: the service restores with its children.
AWS ECS
- propagateTags: fixed. After a tag change and
--force-new-deployment, the new task carriesv=2. include=TAGS: fixed.- Container-instance tags: fixed.
- Capacity providers: fixed.
- TagResource validation: fixed. It refuses
FARGATE, a missing ARN and anaws:key with InvalidParameterException.
New findings (Low)
golangci-lint --new-from-rev=origin/developmentreports gocyclo onserver/gcp/managedkafka/handler.go:210(Matches, 14) andserver/gcp/managedkafka/wire.go:212(toDriverCluster, 11). To fix them, pull the list and item branches ofMatchesinto helpers, and move the optional-block conversion into a helper.- In a location where the sibling owns clusters, the gapic Kafka
ListClustersreturnsgke1and none of the Kafka clusters. You documented this, and it's the right default. A Kafka client there gets wrong data rather than an error, though, so please add a line todocs/coverage/nongoals.
These predate this PR and aren't blocking, but they're worth tickets:
server/gcp/loadbalancer/types.goforwardingRuleResponsehas noregion, and neither does the regional backend service. As a result, the nextterraform planreplaces every regional forwarding rule and the service attachment that points at it. The same rule dropsallPorts, and anINTERNALrule gets an external34.xaddress.server/gcp/loadbalancer/operations.go:862validateHealthCheckRefsonly looks in the rule's own scope. A regional INTERNAL backend service that points at a global health check is refused, but GCP allows it.
Checked
go build,go vetandgo test -racepass on the 25 touched packages, and onpersist,server/gcp/gcsandserver/grpc/..., which covers theiamandlongrunningbumps. coveragegen shows no drift.- On
serve:- the gapic Kafka and Backup and DR clients;
- curl for every earlier repro;
- snapshot, reset and restore for addresses, Kafka, vaults and APIM.
- Terraform
hashicorp/google6.50.0: Kafka cluster and topic, backup vault, backend bucket with cdn_policy, signed URL key, service attachment, PSC consumer rule and address labels. Each ran apply, update and destroy. Plans were clean except for the forwarding rules above. - Terraform
hashicorp/aws6.66.0:aws_ecs_cluster,aws_ecs_task_definitionandaws_ecs_servicewithpropagate_tags. Each ran apply, a clean plan, a tag update, a clean plan and destroy.
…n-apim Resolve conflicts with Logic Apps (stackshy#1337): API Management and Logic workflows both register as new Azure RG-scoped handlers, resource-graph types and discovery buckets, so every hunk keeps both sides. Coverage docs regenerated with go generate.
These are the control planes and fidelity fixes that zopnight's resource-plane families need to run their lifecycle tests against real AWS, GCP and Azure SDKs.
Control planes
managedkafka.googleapis.comv1backupdr.googleapis.comv1Microsoft.ApiManagement/servicebackendBuckets, on the compute load-balancing handleraddresses/globalAddressessetLabels, withlabelFingerprintand label list filtersFidelity fixes
Describe*withinclude=TAGSnow reflectsTagResource/UntagResource. The ARN-keyed tag store is the single authority, so the describe calls andListTagsForResourceno longer disagree after the first tag write.groupSetitems carry<groupId>, and<groupName>when the group exists. The bare-string form decoded as an emptyGroupIdin the AWS SDK. One dangling group id no longer strips the names of the others.networkandsubnetworkare stored and returnedpscConnectionStatusand a stablepscConnectionIdare returnedall-apis/vpc-sc) on a regional rule is refused, as GCP doesVerification (this branch's tip, rebased on
development@dffcf737)go build ./...+go vet ./...go test -count=1 ./...cmd/cloudemucrash/persist tests timed out under a parallel full run; they pass in isolation on both this tip anddevelopment(load flake, not this diff)--new-from-rev origin/developmentRelated: #1335 (EC2 tags on Elastic IPs and VPC endpoints) is still needed for the AWS private-endpoint lifecycle.