Skip to content

feat: Kafka, Backup and DR, APIM and CDN control planes; ECS tag, VPC endpoint and PSC fidelity fixes - #1354

Merged
NitinKumar004 merged 25 commits into
stackshy:developmentfrom
aryanmehrotra:feat/control-planes-kafka-backupdr-cdn-apim
Sep 28, 2026
Merged

NitinKumar004 merged 25 commits into
stackshy:developmentfrom
aryanmehrotra:feat/control-planes-kafka-backupdr-cdn-apim

Conversation

@aryanmehrotra

Copy link
Copy Markdown
Contributor

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

  • GCP Managed Service for Apache Kafka: managedkafka.googleapis.com v1
  • GCP Backup and DR backup vaults: backupdr.googleapis.com v1
  • Azure API Management service: Microsoft.ApiManagement/service
  • GCP Cloud CDN backendBuckets, on the compute load-balancing handler
  • GCP compute addresses / globalAddresses setLabels, with labelFingerprint and label list filters

Fidelity fixes

  • ECS: Describe* with include=TAGS now reflects TagResource / UntagResource. The ARN-keyed tag store is the single authority, so the describe calls and ListTagsForResource no longer disagree after the first tag write.
  • EC2: VPC endpoint groupSet items carry <groupId>, and <groupName> when the group exists. The bare-string form decoded as an empty GroupId in the AWS SDK. One dangling group id no longer strips the names of the others.
  • GCP LB: Private Service Connect consumer forwarding rules:
    • network and subnetwork are stored and returned
    • a PSC rule sent without a scheme reports none, instead of a synthesized EXTERNAL
    • pscConnectionStatus and a stable pscConnectionId are returned
    • a Google APIs bundle target (all-apis / vpc-sc) on a regional rule is refused, as GCP does

Verification (this branch's tip, rebased on development @ dffcf737)

Gate Result
go build ./... + go vet ./... ✅
go test -count=1 ./... ✅ 501 packages ok. 4 cmd/cloudemu crash/persist tests timed out under a parallel full run; they pass in isolation on both this tip and development (load flake, not this diff)
golangci-lint, --new-from-rev origin/development ✅ 0 issues
zopnight private-endpoint + container-service lifecycles through real SDKs ✅ 95/95 and 75/75 (together with #1335)

Related: #1335 (EC2 tags on Elastic IPs and VPC endpoints) is still needed for the AWS private-endpoint lifecycle.

…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 NitinKumar004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_management cannot 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 on serve: GET returned gke1, then after creating Kafka cluster k9 the same GET returned only .../clusters/k9 and gke1 was gone. That breaks gcloud 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 like SetBucketLister) 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.mode is decoded only as a string. The official cloud.google.com/go/managedkafka v1.2.0 REST client sends enums as numbers (UseEnumNumbers), so CreateCluster with RebalanceConfig{Mode: AUTO_REBALANCE_ON_SCALE_UP} fails with 400 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 as server/gcp/privateca/enums.go normalizeEnumNumbers does, 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=labels after a delete returns GKE's 405 METHOD_NOT_ALLOWED instead of 404 NOT_FOUND, and GET and DELETE return GKE's cluster "k1" not found in "us-central1" rather than the Kafka error. A PATCH whose body is Kafka-shaped (capacityConfig, gcpConfig, rebalanceConfig, or labels with no cluster wrapper) should be claimed.
  • CreateCluster succeeds 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:212 and operations.go:188: the library GetOperation reports any unknown name as done, where the API returns 404. The standalone poll also returns done:true with no response. The driver's operation store grows without bound, and in the assembled server the shared lro.Registry owns polls, so this store is unused there.
  • Outputs the API returns are missing:
    • kafkaVersion, which defaults to 3.7.x. It is also listed in clusterFixedPaths though the API marks it an optional input.
    • LRO metadata (OperationMetadata).
    • The rebalanceConfig.mode default of NO_REBALANCE.
  • tlsConfig, updateOptions and brokerCapacityConfig are silently dropped.
  • --async-settle isn'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, formatTime and decodeBody duplicate helpers in sibling packages. decodeBody differs from gcprest.DecodeJSON only 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 no response. The cloud.google.com/go/backupdr/apiv1 REST client's DeleteBackupVaultOperation.Wait fails with unsupported result type <nil> (reproduced against serve). The shared poller replays the same shape because the handler registers nil. Return response: {"@type":"type.googleapis.com/google.protobuf.Empty"} as server/gcp/managedkafka/wire.go:18 does, and register that value. The tests only use the discovery client (google.golang.org/api/backupdr/v1), which ignores response, 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_ID and a/bc are accepted. a/bc is 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 whose effectiveTime has passed (locked), PATCH lowered backupMinimumEnforcedRetentionDuration from 86400s to 1s and moved effectiveTime, and both succeeded. On GCP a locked vault's retention can only increase and the lock time cannot change. checkRetention (:123) also accepts 0s and 1s; the documented range is 1 day to 99 years.
  • M3 Parent/child rules: only force versus backupCount is modelled. A vault referenced by a backup plan deletes without an ignoreBackupPlanReferences check. Reproduced with TF google_backup_dr_backup_plan: the plan request was served by the existing gkebackup handler (it answered with a google.cloud.gkebackup.v1.BackupPlan type), 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: SetUsage is a production symbol used only by tests (backupdr_test.go, sdk_roundtrip_test.go), and its doc comment says so. Move it to export_test.go or a test seam.

Low

  • L1 operations.go:75: filter and orderBy are silently ignored, so a filtered list returns every vault.
  • L2 wire.go writeErr: errors[].reason is upper-case (ABORTED, FAILED_PRECONDITION) where the rest are lower-case, and the etag error drops the vault name because cerrors.Message strips the %w prefix.
  • L3 A validateOnly create 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.addSignedUrlKey and deleteSignedUrlKey return 501, so Terraform google_compute_backend_bucket_signed_url_key fails with "Error 501: backendBuckets.addSignedUrlKey is not implemented", and cdnPolicy.signedUrlKeyNames is never populated. Both actions need adding (take keyName and keyValue, list the name under cdnPolicy.signedUrlKeyNames on GET, never echo the value), plus the same pair on backendServices, which returns 405 today.
  • POST .../urlMaps/{m}/invalidateCache returns 405. serviceAttachments does not exist at all: POST .../regions/r/serviceAttachments returns 501 and google_compute_service_attachment fails. So consumerAcceptLists, connectionPreference, connectionLimit and connectedEndpoints are not modelled.

Medium

  • forwardingrules_psc.go:28 and :58: every PSC consumer rule reports pscConnectionStatus: 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:260 and operations.go:1041: a PSC rule with no network is accepted and gets an external 34.x IPAddress. GCP requires the consumer network and an internal address. An explicit loadBalancingScheme: EXTERNAL with a service attachment target is also accepted, but PSC requires an empty scheme. Both should be rejected in validatePSCTarget.
  • backendbuckets_validate.go:137: the cdnPolicy cross-field rules are missing. These are all accepted but rejected by GCP: USE_ORIGIN_HEADERS with defaultTtl, maxTtl or clientTtl; FORCE_CACHE_ALL with maxTtl; and clientTtl > maxTtl (maxTtl 100 with clientTtl 5000 inserts fine). defaultTtl is 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:937 numericID (pre-existing): this returns the full uint64 FNV hash, which can be around 1.6e19 and so above int64. Terraform google_compute_forwarding_rule fails 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 and labelFingerprint live in the server-side addressStore. They are not in the provider and not in /_cloudemu/snapshot, so they are lost on restore, and they diverge between the library and serve. 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 at backendbuckets.go:303 races 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:14 says azurerm_api_management sees no drift, but Terraform can't create one. With azurerm v4, apply fails on GET /providers/Microsoft.ApiManagement/locations/eastus/deletedServices/<name> → 501. The provider makes this soft-delete check even with recover_soft_deleted = false. Returning 404 there only moves the failure on: create then fails on GET .../service/<n>/apis, because azurerm lists and deletes the default Echo API after create, and refresh and destroy fail on portalsettings/signin. The minimum needed is deletedServices GET/DELETE (404 when nothing is soft-deleted), the apis list, portalsettings signin/signup, tenant/access and 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. Developer with 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. svc1 is created in both rg1 and rg2 with the same svc1.azure-api.net gatewayUrl. Azure returns 409 on the second one. checkNameAvailability returns 501.

  • A PUT with a different location returns 200 and silently keeps eastus. ARM rejects this with 409 InvalidResourceLocation.

  • Architecture: server/azure/apimanagement/types.go:141-200 builds 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:32 puts sku and skuCapacity in Properties instead of Attrs.SKU, SKUCapacity and Zones. The Resource Graph row has no top-level sku{name,capacity} and shows properties.sku as a string.
  • Validation errors use InvalidParameter where APIM returns ValidationError. List responses have no nextLink paging.

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): serviceTaskSpec gives tasks the service's create-time svc.Tags and ignores propagateTags. After untagging s and tagging s2, a --force-new-deployment still launched tasks tagged s=1, and a service with the default propagateTags NONE still put its tags on its tasks. With NONE the tasks should get no tags. SERVICE should use m.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=1 is dropped, and DescribeContainerInstances --include TAGS returns null even after a TagResource.
    • Capacity providers: Create/DescribeCapacityProviders return "unknown ECS operation", yet TagResource on capacity-provider/FARGATE succeeds.

Low (pre-existing)

  • server/aws/ecs/taskdefs.go:153 and DescribeServices return tags without include=TAGS. ECS only returns them when asked.
  • tags.go:23 TagResource accepts a nonexistent ARN, 51 tags and aws:-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).
@aryanmehrotra

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review, and for reproducing everything on serve. All four blockers are fixed in b0ebd3d3..db12f2ff, and so are most of the medium and low findings. Where I went a different way than the review suggested, or left something out, I say so below.

Every fix has a test that fails on b0ebd3d3 and passes now. Where you pointed out that the discovery client hid a bug, the new test goes through the official gapic / SDK client.

Blockers

Finding Fix Proof
Kafka list takes over GKE/AlloyDB /clusters aca0460: the handler gets a read-only ClusterSibling probe, wired the way SetBucketLister is. Kafka claims the list only when it owns clusters there and the sibling owns none. I didn't merge the lists, because a GKE ListClustersResponse client would read Kafka entries as broken GKE clusters. TestFullServerManagedKafkaSharesClustersWithGKE now checks the GKE list after a Kafka create; also …WithAlloyDB
Kafka rejects numeric enums aca0460: every enum on the wire accepts a number or a name TestGAPICClusterWithNumericEnums (the cloud.google.com/go/managedkafka REST client, AUTO_REBALANCE_ON_SCALE_UP, Wait)
Backup and DR delete + Wait fails 4686c13: the delete LRO, the standalone poll and the registered value all return google.protobuf.Empty TestGAPICDeleteWaitSharedPoller, …Standalone (cloud.google.com/go/backupdr/apiv1)
APIM + Terraform db12f2f: serves deletedServices (soft delete, purge, restore), the apis list/get/delete with the default Echo API, portalsettings signin/signup, tenant/access and the service policy TestSDKTerraformCreateReadDestroy, which replays azurerm v4's request sequence (api_management_resource.go @ 3cfd078) through armapimanagement v3

⚠️ APIM: I could not run a real terraform apply here, because the emulator's TLS cert can't be trusted from Go on macOS. So the package doc now claims only what the replayed sequence proves, and "no drift" is gone. An empty plan against a real terraform binary is still unproven, and docs/coverage/nongoals/apimanagement.md says so.

Managed Kafka (aca0460, 956451e)

  • Missing-cluster fall-through: a Kafka-shaped PATCH now gets 404, not GKE's 405, and a Kafka create with an id GKE or AlloyDB already uses gets 409 (both directions).
    • Partial: GET/DELETE of a cluster nobody owns still gets GKE's 404. It has the same google.rpc.Status shape but GKE's message. A bodiless request carries no hint of which service it is for, and claiming it would give GKE clients Kafka's error.
  • Operations: GetOperation 404s an unknown name, and a finished poll carries response and metadata. The library's op store is capped at 1000 entries; the server polls through lro.Registry.
  • Outputs and validation:
    • kafkaVersion defaults to 3.7.x and is optional input.
    • rebalanceConfig.mode defaults to NO_REBALANCE.
    • tlsConfig, updateOptions and brokerCapacityConfig are stored.
    • --async-settle gives CREATING then ACTIVE.
    • The subnet region is checked against the cluster location.
  • Coverage: provider 97.6%, wire 97.5%. TypedAny, FormatTime and DecodeOptionalJSON are now shared in gcprest.
  • The test dependency is managedkafka v1.0.0, not v1.2.0, because v1.2.0 needs go 1.26 and CI is on 1.25. It encodes enums the same way.
  • ⚠️ kafkaVersion: the discovery doc this repo pins (v0.291.0) says output-only, and v0.298.0 says optional with default 3.7.x. I went with the newer one, as you suggested.

Backup and DR (4686c13)

  • M1: the id format follows the backup vault naming doc: ^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$. A leading digit is allowed and a trailing hyphen is refused, which differs slightly from the regex in the review.
  • M2: the retention lock is modelled. Once effectiveTime has passed, the minimum retention can only grow and effectiveTime is frozen. The range is 1 day to 99 years.
    • The error code for a locked vault isn't documented. I used FAILED_PRECONDITION.
  • M3: the /backupPlans misroute into gkebackup predates this PR, so I left it. The handler docs now list what's unimplemented and where /backupPlans goes today. Plans and ignoreBackupPlanReferences need that routing fixed first, so they're a follow-up.
  • M4: SetUsage moved to export_test.go.
  • L1: filter and orderBy work, following artifactregistry/memorystore precedent. Unlike those siblings, an unsupported expression here gets 400 rather than matching everything.
  • L2: reasons are lower-case, and the etag error keeps the vault name.
  • L3: validateOnly records no operation.
  • Also: gcprest now maps aborted to ABORTED, which also fixes kms, where the status was missing.

CDN, LB and PSC (9f7b22e … cefe314)

  • Missing operations:
    • signed URL keys on backendBuckets and backendServices: validated, never stored, never echoed; names go in cdnPolicy.signedUrlKeyNames
    • urlMaps.invalidateCache
    • regional serviceAttachments, with consumerAcceptLists / RejectLists, connectionPreference, connectionLimit and connectedEndpoints
  • PSC:
    • pscConnectionStatus now comes from the attachment: ACCEPTED, PENDING, REJECTED, or CLOSED once the attachment is deleted.
    • A rule targeting an attachment that doesn't exist is refused.
    • A rule with no network, or with any explicit scheme, is refused.
    • A rule sent without an address gets an internal one.
  • cdnPolicy: the cross-field TTL rules are enforced, and the defaults are filled in per cacheMode (3600 / 86400 / 3600 for CACHE_ALL_STATIC).
  • numericID: masked to 63 bits. ⚠️ The same defect is in server/gcp/compute/instances.go and server/gcp/vpc/handler.go, which are separate copies I haven't changed. Happy to fold them into a follow-up.
  • Address labels: the whole address record now lives in the provider behind GCPAddressStore, because the addresses themselves were missing from the snapshot too. Snapshots taken before this change restore with no addresses.
  • Not fixed: the url-map in-use delete race. It's the same pattern backendServices already has, so it's better fixed for both together.

APIM (db12f2f)

  • The etag rotates on every write, and a stale If-Match gets 412 on the service and its children.
  • Capacity ceilings per SKU, and zones only on Premium.
  • The service name is global: 409 across resource groups and subscriptions, a soft-deleted name also blocks it, and checkNameAvailability is served.
  • A location change gets 409 InvalidResourceLocation.
  • Defaults and computed fields moved into the provider, so the library and serve return the same resource.
  • Discovery sets Attrs.SKU, SKUCapacity and Zones.
  • Validation errors use ValidationError, and lists page with nextLink through a new shared azurearm.Paginate.
  • Two error codes are best guesses: ServiceAlreadyExists and ServiceAlreadyExistsInSoftDeletedState.

ECS (5996269, 676e000)

  • propagateTags: NONE (the default) propagates nothing, SERVICE uses the service's live tags, and TASK_DEFINITION uses the task definition's.
  • Tags on more resources: container-instance tags are stored and returned. Capacity providers get Create/Describe/Update/Delete, with FARGATE and FARGATE_SPOT built in.
  • include=TAGS: DescribeServices, DescribeTaskDefinition and DescribeTasks return tags only when asked.
  • TagResource validation: a nonexistent ARN, more than 50 tags, and aws:-prefixed keys or values are all refused.
  • Two places where AWS differs from the review:
    • The TagResource reference says tagging a service by its short ARN is an InvalidParameterException, so TagResource/Untag/List now refuse it. Describe and Update already resolve short ARNs, and a test pins that.
    • The docs say the predefined FARGATE / FARGATE_SPOT capacity providers cannot be tagged, so TagResource on capacity-provider/FARGATE now fails. Before, it succeeded, which was a bug.
  • ⚠️ Which exception fires for a nonexistent ARN isn't documented. I used InvalidParameterException.

Gates on db12f2ff

  • go build / go vet: clean
  • go mod tidy: no drift
  • go generate coverage docs and compatgen: no drift
  • golangci-lint --new-from-rev b0ebd3d3: 0 issues
  • go test ./...: 501 packages ok. TestK8sDataPlanePersistsAcrossCrash timed out once in the full parallel run and passes alone on this tip (5.8s). The same 4 crash/persist tests are flaky under load on development too.
  • zopnight's resource-plane suites through the real AWS/GCP/Azure SDKs against this branch plus fix(aws-ec2): tag Elastic IPs and VPC endpoints after creation #1335: private-endpoint 95/95, container-service 75/75

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 NitinKumar004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 gke1 and then Kafka k9 in the same region, GET .../locations/us-central1/clusters still returns only gke1. In a region with no GKE cluster, the gapic ListClusters returns the Kafka cluster.
  • Numeric enums: fixed. cloud.google.com/go/managedkafka CreateCluster with AUTO_REBALANCE_ON_SCALE_UP completes through Wait and reads the mode back. "mode":2 works, and "mode":7 gets 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 metadata with an OperationMetadata @type.
  • Missing outputs and dropped fields: fixed.
    • kafkaVersion defaults to 3.7.x.
    • rebalanceConfig.mode defaults to NO_REBALANCE.
    • tlsConfig and updateOptions read 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.Wait returns nil, and so does a rebuilt handle.

  • M1, ID format: fixed. Bad_ID, a/bc and pv get 400.

  • M2, retention lock: fixed. Once effectiveTime has passed:

    • a lower minimum retention gets 400;
    • a higher one gets 200;
    • moving effectiveTime gets 400.

    1s is refused.

  • M3, backup plans routed to gkebackup: documented in the handler. That's reasonable, since the misroute predates this PR.

  • M4: fixed. SetUsage is now only in export_test.go.

  • L1 to L3: fixed.

GCP CDN, load balancing and PSC

  • Signed URL keys: fixed. google_compute_backend_bucket_signed_url_key applies and destroys.
  • invalidateCache: served. A missing url map gets 404.
  • serviceAttachments: fixed. google_compute_service_attachment with ACCEPT_MANUAL and an accept list applies. Swapping to a reject list turns the consumer rule and the connectedEndpoints entry to REJECTED.
  • PSC rule validation: fixed. A PSC rule with no network, or with loadBalancingScheme: EXTERNAL, gets 400.
  • cdnPolicy cross-field rules: fixed.
  • numericID: fixed. forwarding_rule_id reads 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-Match gets 412.
  • Capacity ceilings: fixed.
  • Global name: fixed. It returns 409 ServiceAlreadyExists, and checkNameAvailability answers.
  • 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 carries v=2.
  • include=TAGS: fixed.
  • Container-instance tags: fixed.
  • Capacity providers: fixed.
  • TagResource validation: fixed. It refuses FARGATE, a missing ARN and an aws: key with InvalidParameterException.

New findings (Low)

  • golangci-lint --new-from-rev=origin/development reports gocyclo on server/gcp/managedkafka/handler.go:210 (Matches, 14) and server/gcp/managedkafka/wire.go:212 (toDriverCluster, 11). To fix them, pull the list and item branches of Matches into helpers, and move the optional-block conversion into a helper.
  • In a location where the sibling owns clusters, the gapic Kafka ListClusters returns gke1 and 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 to docs/coverage/nongoals.

These predate this PR and aren't blocking, but they're worth tickets:

  • server/gcp/loadbalancer/types.go forwardingRuleResponse has no region, and neither does the regional backend service. As a result, the next terraform plan replaces every regional forwarding rule and the service attachment that points at it. The same rule drops allPorts, and an INTERNAL rule gets an external 34.x address.
  • server/gcp/loadbalancer/operations.go:862 validateHealthCheckRefs only 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 vet and go test -race pass on the 25 touched packages, and on persist, server/gcp/gcs and server/grpc/..., which covers the iam and longrunning bumps. 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/google 6.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/aws 6.66.0: aws_ecs_cluster, aws_ecs_task_definition and aws_ecs_service with propagate_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.
@NitinKumar004
NitinKumar004 merged commit d4cb304 into stackshy:development Sep 28, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants