Skip to content

update - #4

Merged
NitinKumar004 merged 2 commits into
masterfrom
development
Mar 1, 2026
Merged

update #4
NitinKumar004 merged 2 commits into
masterfrom
development

Conversation

@NitinKumar004

Copy link
Copy Markdown
Collaborator

No description provided.

@NitinKumar004
NitinKumar004 merged commit f1ab543 into master Mar 1, 2026
aryanmehrotra added a commit to aryanmehrotra/cloudemu that referenced this pull request Jul 27, 2026
PDB discovery advertised watch and patch, neither of which pdb.go implements —
a reflector would open a watch that returns a list and never streams, and
kubectl or helm would send a PATCH that 405s. Both failures land in the caller,
far from the document that promised them. It now advertises only what it serves.

DeleteCluster resolved the endpoint after deregistering the data plane, so the
delete response described the cluster with the not-implemented sentinel rather
than the endpoint it had been reachable on. Resolved before, since the response
describes the cluster as it was.

Two error codes said one thing and sent another. The unsupported-capability
error named InvalidAction in its message while the generic mapping emitted
InvalidParameterValue; SendCommand to a rejected target named InvalidInstanceId
while surfacing the parameter-store ParameterNotFound. Callers match on the
code, so both now emit what they claim.

A malformed NumCacheClusters was coerced to a default instead of rejected,
quietly building something other than what was asked for. Absent still means
"unspecified"; present-but-unparseable is a ValidationException.

NOT fixed — the k8s CA mismatch (stackshy#4). The data plane is HTTP while the cluster
advertises a parseable CA, so a caller building a rest.Config from Endpoint plus
CertificateAuthority alone fails the handshake. Clearing the CA was implemented
and reverted: callers that build a kubeconfig need a parseable CA to construct
one at all, and a full provisioning run broke at the first step that uses it.
Resolving it properly means serving the data plane over TLS with this CA. The
mismatch is now documented where the endpoint is set rather than left implicit.
thzgajendra pushed a commit that referenced this pull request Jul 28, 2026
…296)

* fix(serve): advertise the real Kubernetes data-plane endpoint

`cloudemu serve` started the shared Kubernetes data plane and routed
/k8s/{uid}/... to it, but the managed-Kubernetes control planes still
advertised the Wave 1 sentinel:

    "endpoint": "https://EKS-DATAPLANE-NOT-IMPLEMENTED.cloudemu.local"

so any tool that derives a kubeconfig from DescribeCluster / GetCluster —
which is what real EKS/GKE/AKS clients do — got an address that does not
resolve. The data plane was reachable the whole time; nothing told the
control planes where it was.

Two things were missing, and each alone is insufficient:

1. `APIServer.SetBaseURL` was never called, so `BaseURL()` returned "" and
   every provider's withK8sEndpoint took its early-return back to the
   sentinel. The listener binds c.host:c.k8sPort, so the base URL is
   derived from the same pair.

2. `Drivers.K8sAPI` is only the SERVER's path routing for /k8s/{uid}/...
   (see the comment on awsserver.Drivers.K8sAPI). The control-plane mocks
   hold their own reference and expose SetK8sAPI for it. Setting the
   drivers field without calling SetK8sAPI leaves m.k8sAPI nil, so no
   ClusterState is registered and no UID is recorded — the sentinel again.
   Applied to all three: cloud.EKS, cloud.GKE, cloud.AKS.

Backward compatible:
  - No API change. SetBaseURL/SetK8sAPI already existed and are documented
    for exactly this ("providers read this back when rendering kubeconfigs").
  - No flag change. Behaviour is unchanged when the data plane is disabled
    (-k8s-port ""), because k8sBackend stays nil and no BaseURL is set.
  - Library users are untouched: a caller that never calls SetBaseURL still
    gets the sentinel, which is what the existing fixtures assert.

Verified: with the patch, `cloudemu serve` + `aws eks create-cluster`
returns http://127.0.0.1:4570/k8s/<uid>, and POSTing a Namespace to that
URL creates one. Suites for cmd/cloudemu, services/kubernetes and all three
providers pass.

* fix(eks): return a real self-signed CA, not an unparseable placeholder

DescribeCluster advertised:

    -----BEGIN CERTIFICATE-----
    MIICloudemuStubCertificate
    -----END CERTIFICATE-----

The code comment justified it as "SDK clients only base64-decode it for the
kubeconfig, so a deterministic stub is enough". That holds for the raw SDK
and breaks for anything that then builds a TLS config — which is what a
kubeconfig is FOR. client-go calls AppendCertsFromPEM and fails with
"unable to load root certificates: unable to parse bytes as PEM block" at
kubernetes.NewForConfig, an error naming neither the cluster nor the CA.

So any tool following the documented path to reach an EKS cluster — derive
a kubeconfig from DescribeCluster — hit a wall as soon as it tried to use
it. The emulator looked correct right up to the point of being used.

Now generates a genuine self-signed CA certificate. Cached with sync.Once:
generation is not free, and a stable CA across calls matches real EKS,
where a cluster's CA does not change between DescribeCluster invocations.

Generation failure falls back to an empty string rather than panicking. An
empty CA makes client-go use the system roots, which is recoverable; a
panic would take down an emulator whose entire purpose is keeping tests
running.

No API or behavioural contract change: the field is still a base64 PEM blob
in the same place, it is now merely valid. No test pinned the placeholder
value. Provider and cmd suites pass.

* feat(kubernetes): serve API discovery, and reject protobuf with 415

The data plane implemented Namespace/Pod/Service/ConfigMap/Secret/Deployment
and served them correctly — but only to callers that hit a resource path
directly. Anything that NEGOTIATES first, which kubectl and helm both do,
died at startup with:

    couldn't get current server API group list: the server could not find
    the requested resource

so it never reached the resources that worked. Discovery is therefore not a
nicety; it is what makes the existing data plane reachable by real tooling.

Adds /api, /apis, /api/v1, /apis/apps/v1 and /version. The advertised set is
derived from what ClusterState.ServeHTTP actually dispatches, so discovery
can never promise a resource the emulator does not serve — a tool that
trusts discovery and then 404s is worse than one that cannot start. Verbs
are limited to what the handlers support, for the same reason. shortNames
are included so `kubectl get ns` / `get deploy` resolve rather than
reporting "the server doesn't have a resource type", which reads like the
resource is missing rather than un-aliased.

Serving discovery then exposed a second issue: kubectl negotiates PROTOBUF
for built-in types once they are advertised, and the JSON decoder reported
the frame's "k8s\x00" magic prefix as `invalid character 'k'` — a message
that reads like a malformed request rather than an encoding the server does
not speak. Writes failed while reads succeeded, which is a confusing place
to leave a user.

Protobuf bodies now return 415 with a clear message. 415 is the status the
Kubernetes client libraries are built to handle; 400 made clients give up.
Full protobuf decoding is a larger piece of work and is not attempted here.

Verified against a live emulator: `kubectl get ns` and `kubectl get
deployments -A` work, and `helm install` gets past discovery to chart
rendering. Two follow-ups are now visible and were not before: /openapi/v2
(helm validates manifests against it unless --disable-openapi-validation)
and the networking.k8s.io/policy API groups that common charts require.

services/kubernetes, cmd/cloudemu and providers/aws/eks suites pass.

* feat(kubernetes): PodDisruptionBudget (policy/v1) and an /openapi/v2 stub

PDB is here because real charts render one as a matter of course — a chart
producing a Deployment and a Service almost always produces a PDB beside
them. Without the resource, `helm install` dies at object-building with "no
matches for kind PodDisruptionBudget" BEFORE any of the workload kinds the
emulator does support are reached. Supporting the workloads but not the PDB
that ships with them means a realistic chart still cannot be installed.

Full CRUD under /apis/policy/v1, mirroring the deployments handler, and
advertised in discovery with the `pdb` shortName. Status reports only
ObservedGeneration: there is no disruption controller here, so inventing
eviction counts would assert semantics the emulator cannot honour.

/openapi/v2 returns a minimal valid Swagger 2.0 envelope with no
definitions. helm validates rendered manifests against it unless the caller
passes --disable-openapi-validation, and its absence surfaces as "failed to
download openapi" AFTER the chart renders — which reads like a chart fault
rather than a missing server capability. Empty definitions means "nothing
to contradict" (helm skips kinds it finds no schema for) rather than
"everything invalid"; publishing hand-written partial schemas would be
worse, since a subtly wrong schema rejects valid manifests and would have
the emulator asserting API shapes it does not enforce.

KNOWN INCOMPLETE, and the reason this is a stub: helm requests OpenAPI as
PROTOBUF (application/com.github.proto-openapi.spec.v2+protobuf) and parses
the response as such regardless of the Content-Type sent back, so a JSON
body fails with "proto: cannot parse invalid wire-format data". Serving it
properly needs the protobuf encoding, not another JSON shape. The endpoint
is still an improvement — it is now the encoding that is missing rather
than the route — but `helm install` does not yet complete against the
emulator without --disable-openapi-validation.

services/kubernetes and cmd/cloudemu suites pass.

* feat(ec2): DescribeAvailabilityZones

Provisioning a VPC is the first step of nearly every datastore plan, and
choosing subnets requires knowing the region's zones. Without this action a
datastore job fails at its very first step with "unknown action" — before
any of the VPC/subnet/RDS behaviour the emulator does implement is reached,
so the implemented surface was unreachable for that whole class of job.

Reports three zones. Three is the meaningful number rather than an
arbitrary one: it is the minimum real regions offer, and a subnet group
spanning two AZs is a hard RDS requirement, so a region reporting fewer
would make datastore provisioning untestable rather than merely
approximate.

Zones are derived from the requested region (us-east-1 -> us-east-1a/b/c)
instead of a fixed table, so any region a caller uses behaves consistently
rather than only the ones someone remembered to enumerate. The region is
read from the SigV4 credential scope because the EC2 query API carries no
region parameter — the signature is the only place the caller states which
region it believes it is addressing.

Verified against a real provisioner: a MySQL provisioning job that
previously failed at "unknown action: DescribeAvailabilityZones" now
advances past network AZ selection to the next unimplemented action
(AllocateAddress), confirming the handler is reached and its response
parsed.

server/aws/ec2 suite passes.

* feat(ec2): Elastic IP query-protocol handlers (Allocate/Release/Describe)

The Networking driver already implemented AllocateAddress, ReleaseAddress,
DescribeAddresses, AssociateAddress and DisassociateAddress — only the wire
layer was missing, so callers got "unknown action" for behaviour that
existed underneath. This is a wire gap, not a feature gap.

It is the first hard stop in every VPC-with-private-subnets plan: a NAT
gateway cannot be created without allocating an EIP first, so a datastore
provisioning job fails at step 1 (create_network) before reaching any of the
VPC/subnet/NAT behaviour the emulator does implement.

ReleaseAddress reads AllocationId or PublicIp — only the former is
meaningful after EC2-Classic's 2022 retirement, but SDK versions differ in
which they send. Domain is reported as "vpc" unconditionally, matching real
AWS post-Classic, so callers need not branch on a value that can no longer
vary.

Associate/Disassociate are deliberately NOT wired here: the provisioner does
not call them (NAT gateways take an allocation id directly), and wiring
actions no caller exercises would add untested surface. They remain
available on the driver when a caller needs them.

server/aws/ec2 suite passes.

* feat(ec2): route-table association lifecycle and network delete actions

A caller tearing down a VPC learns association ids only from
DescribeRouteTables, so AssociateRouteTable on its own is not enough: the
association has to survive the projection back out. driver.RouteTable grows an
Associations field, joined in DescribeRouteTables from the association store,
and the wire layer emits it as associationSet so the AWS SDK populates
RouteTable.Associations[].RouteTableAssociationId.

Wires the remaining network teardown actions, all of which the driver already
implemented and only the HTTP layer was missing: DeleteRoute, DeleteRouteTable,
DeleteInternetGateway, AssociateRouteTable, DisassociateRouteTable.

RouteTableAssociation grows a Main flag: callers skip the main association
during teardown and let it die with the VPC, so the distinction has to be
visible.

* feat(ec2): model elastic network interfaces and their teardown drain

A VPC delete is refused while interfaces remain attached, so callers drain
ENIs before deleting the network. Emulating that needs interfaces that
actually exist: a NAT gateway now holds an ENI in its subnet for as long as it
lives, and releases it on delete.

DeleteNetworkInterface refuses an attached interface, which is what tells a
draining caller its work is not finished rather than letting it report a clean
teardown over a network it never drained.

DescribeNetworkInterfaces honours vpc-id, subnet-id and status filters. An
unrecognised filter narrows to nothing rather than matching everything — a
caller draining by vpc-id must never be handed interfaces from another VPC.

Interfaces are modelled in all three providers so the driver contract stays
uniform.

* feat(rds): DB subnet groups

Managed databases are placed into a named set of subnets, and a caller
provisioning one creates that group first — without it the provision stops at
its first step.

VpcId is derived from the member subnets rather than taken as input, matching
real RDS. This is not cosmetic: a VPC teardown lists subnet groups and matches
on VpcId to decide which are its own, so a blank value there leaks every group
on every delete. The RDS mock resolves it through the networking mock, wired
in the AWS composition root alongside SetMonitoring.

A duplicate name reports DBSubnetGroupAlreadyExists. Callers re-running a
provision treat that specific code as "already there, carry on", so folding it
into a generic error would turn a safe re-run into a hard failure.

Subnet groups are an AWS concept — Azure and GCP use vnet integration — so
this is an optional capability discovered by type assertion rather than a new
method on the RelationalDB interface. Drivers that do not model them answer
InvalidAction, which is the truthful response for a cloud with no such
resource, and no other provider had to change.

* feat(elasticache): cache subnet groups

Mirrors the RDS subnet-group work: VpcId is derived from the member subnets,
because a VPC teardown lists cache subnet groups and deletes the ones whose
VpcId matches the network going away. An empty value there strands every group.

Needed even by callers that never provision a cache — a teardown sweeps RDS and
ElastiCache subnet groups in the same step, so an unimplemented
DeleteCacheSubnetGroup fails the whole step and leaves the VPC standing.

Same optional-capability shape as RDS: discovered by type assertion, so no
other cache driver had to change.

* feat(elasticache): replication groups

Redis provisioning with more than one node creates a replication group rather
than a bare cache cluster, so without this the redis path stops at its create
step.

The primary endpoint is the point of the resource: callers read
NodeGroups[0].PrimaryEndpoint.Address to build a connection string, and a group
that reports success without one hands back an unusable cache. The node group
is therefore always present, even for a single-node group.

ReplicationGroupAlreadyExists and ReplicationGroupNotFoundFault are string-
matched by callers to make a re-run idempotent and to treat an absent group as
already deleted, so both codes are reproduced verbatim.

* feat(iam): materialise AWS-managed policies on first reference

AWS publishes hundreds of managed policies and every account already has all
of them, so callers attach them — AmazonSSMManagedInstanceCore for an instance
profile, AmazonEKSClusterPolicy for a cluster role — without a preceding
CreatePolicy. The emulator required one, turning an ordinary AttachRolePolicy
into NoSuchEntity and stopping any flow that grants a role SSM or EKS access.

Any well-formed arn:aws:iam::aws:policy/ ARN is honoured rather than a seeded
list, which would only move the failure to the first policy nobody thought to
enumerate. Pathed ARNs keep their path.

Customer-managed ARNs are untouched: attaching one that was never created is
still NotFound, because there the error is a real caller bug worth surfacing.

The stored document is an explicit empty-statement placeholder. The emulator
does not evaluate policy documents, and inventing the real contents of these
policies would be fiction presented as fact.

* feat(ssm): resolve AWS-published AMI parameters on first read

Callers resolve "current image for this distro" through the public
/aws/service/.../ami-id parameters rather than pinning an id that goes stale
every couple of weeks. AWS publishes them in every account, so requiring a
PutParameter first made any instance launch fail with ParameterNotFound.

The id is derived from the parameter name, so repeated reads are stable (a
reconciling caller must not see the AMI change under it) and two distros never
collide on one image.

Deliberately narrow: only the ami-id family is synthesised. Other
/aws/service/ parameters carry payloads that cannot be derived — the
ECS-optimised family holds a JSON blob — and returning an invented value would
be worse than reporting it absent, so those stay NotFound. Unset user
parameters are likewise untouched.

* feat(ssm): Run Command send and invocation polling

Bootstrapping an instance goes through SendCommand plus a GetCommandInvocation
poll, so without them a VM provision stops after the instances are already
running — the most expensive place to fail.

LIMITATION, stated plainly: nothing executes. An emulated instance has no guest
operating system, so invocations report Success with empty output. This
exercises a caller's send/poll orchestration — that it waits for a terminal
status, reads the response code, handles failure — but it does NOT validate the
script. A caller whose bootstrap script is wrong still sees success here.

One send registers an invocation per target instance, because callers poll per
instance and would hang on any that were dropped. Polling a command that was
never sent is InvocationDoesNotExist rather than a fabricated success: that is
a real caller bug and answering Success would bury it.

* feat(elbv2): load balancer attributes and DescribeTags

Enabling cross-zone load balancing is part of bringing an NLB up, so without
ModifyLoadBalancerAttributes the load balancer step fails after the instances
behind it are already running.

LBAttributes grows an Extra map. AWS models attributes as open key/value pairs
and adds new ones over time, so the typed struct silently dropped anything it
had not been taught — cross-zone among them — and a caller reading back its own
write would get a wrong answer. Unrecognised keys are now stored rather than
echoed.

Modify merges rather than replaces, matching AWS: a caller enabling cross-zone
must not silently clear an idle timeout it set earlier.

DescribeTags is added alongside because a sweep for orphaned infrastructure
identifies its own load balancers by tag, and an empty answer reads as "not
mine" — leaving the orphan standing. Both were wire-only gaps; the driver
already stored attributes and tags.

* fix(elbv2): describing a missing load balancer by ARN is LoadBalancerNotFound

Real ELBv2 answers LoadBalancerNotFound when an explicitly named ARN does not
exist. The emulator returned an empty list and no error, so a caller waiting
for a delete to settle — which polls DescribeLoadBalancers with the ARN until
it errors — kept polling to its timeout over a load balancer that was already
gone, then reported the teardown as failed with everything downstream of it
untouched.

BEHAVIOUR CHANGE: an existing test asserted the empty-list answer. It encoded
the emulator's behaviour rather than AWS's, and is updated here. An unfiltered
describe is untouched and still returns whatever exists, including nothing.

* feat(ec2): ModifyVpcAttribute

Enabling DNS hostnames is part of standing a VPC up, and the emulator answered
InvalidAction. Callers that discard the result never noticed; callers that
check it could not proceed.

A nil pointer means "unchanged". The real API accepts exactly one attribute per
call, so treating an absent parameter as false would make a caller enabling DNS
hostnames silently switch DNS support off. VPCs default to DNS support on and
hostnames off, matching EC2.

* fix: AWS fidelity corrections and vendor-neutral fixtures

Three behaviours did not match the real services:

DescribeNetworkInterfaces silently returned nothing for a filter it did not
implement. Real EC2 answers InvalidParameterValue. Returning nothing is the
worst of the options: a caller draining a VPC reads it as "nothing left to
drain" and proceeds to a delete that then fails with DependencyViolation.
Matching everything is equally bad — it hands back interfaces the caller never
asked for and may delete. An explicit error cannot be mistaken for a result.
network-interface-id and description are now supported alongside vpc-id,
subnet-id and status.

SendCommand reported the command as Success. Real SSM reports Pending — it has
been accepted, not finished — and the caller learns the outcome from
GetCommandInvocation. Reporting Success invites a caller to skip the poll it
would need against the real service.

DescribeTags resolved every ARN as a load balancer, so a target group ARN came
back empty. Combined with the LoadBalancerNotFound fix in 3ec47d2 it became
worse: a target group ARN reported LoadBalancerNotFound instead of its tags.
Real DescribeTags takes both kinds in one call and does not care which is
which, so ARNs are now resolved against both.

Also documents a known gap: real EC2 gives every VPC a main route table whose
association carries main=true. This emulator creates none, so no association
is ever main. Self-consistent, but callers expecting a main association will
not find one.

Test fixtures renamed off a specific vendor's naming; no production code
referenced it.

* feat(ec2): main route tables

EC2 creates a route table alongside every VPC, carrying the local route and an
implicit association with main=true and no subnet. The emulator created none,
so every VPC looked like it had one fewer route table than it does and the
RouteTableAssociation.Main flag could never be true — a flag no producer ever
set.

It behaves as the real one does: it cannot be deleted on its own and its
association cannot be disassociated, because both are implicit in the VPC
rather than things a caller made. A caller sweeping a VPC's route tables has
to skip it, and that refusal is how it learns to. Both disappear with the VPC,
which is the only way they can be removed.

Caller-created route tables are unaffected: still deletable, associations
still disassociable, never main.

* fix: replace permissive stand-ins with the real published sets

Three places accepted more than the real services do. Each traded a false
failure for a false success, which is the worse of the two: the emulator
agreed with a caller that AWS would have rejected.

IAM managed policies were materialised for any well-formed
arn:aws:iam::aws:policy/ ARN. AWS publishes a finite set, so an ARN outside it
is NoSuchEntity — and accepting anything meant a typo like
AmazonEKSClusterPolicyy attached cleanly here and failed in production.
Replaced with a catalogue of real policy names, pathed ones included; unknown
names are rejected, which makes a missing entry a one-line fix rather than a
silent divergence.

SSM public parameters resolved any path ending in /ami-id. Restricted to the
trees AWS actually publishes (amazon-linux, windows, canonical, debian, suse,
marketplace, bottlerocket, eks-optimized). An invented distro now answers
ParameterNotFound instead of handing back an image id that exists nowhere.

SendCommand accepted any instance id. Real SSM answers InvalidInstanceId for a
target that is not a managed instance, and that is the most common Run Command
failure during bring-up — precisely the one worth surfacing early. Targets are
now checked against the compute mock, wired in the AWS composition root.

The AMI id itself stays synthesised: a real one changes as the publisher
rebuilds the image, so there is no fixed value to copy. It is derived from the
parameter name, so it is stable per parameter and distinct across distros.

* fix(topology): resolve a subnet's route table by association, not by order

findRouteTableForVPC returned whichever table was listed first for the VPC.
That was only ever right while a VPC had exactly one; once main route tables
existed alongside caller-created ones the arbitrary choice could land on a
table whose routes do not govern the subnet, and every routing answer derived
from it was wrong — TraceRoute reported a reachable destination as blocked.

Resolution now follows the cloud: an explicit subnet association wins, and a
subnet without one falls back to the VPC's main route table. Drivers that
model no main table still get an answer rather than the subnet reading as
unroutable.

TestTraceRoute created a route table and never associated it with the subnet,
then asserted traffic routed through it. That only passed because the emulator
picked the sole table available; in the real cloud that subnet uses the main
table and 8.8.8.8 is genuinely unreachable. The test now associates the table,
which is what the scenario requires, and a new case pins the unassociated
behaviour.

* fix: report the AWS error code for the resource, not a generic one

A duplicate DB subnet group answered ResourceAlreadyExistsFault and a
duplicate replication group answered CacheClusterAlreadyExists. The messages
read correctly, which is why string-matching callers were unaffected — but the
SDK surfaces the CODE as a typed error, so a caller doing
errors.As(&types.DBSubnetGroupAlreadyExistsFault{}) never matched and its
idempotent re-run became a hard failure.

RDS now distinguishes db subnet groups on both the not-found and already-exists
paths. ElastiCache gained the same per-resource mapping, which it did not have
at all: every error was reported as a cache cluster, so a missing replication
group was indistinguishable from a missing cluster.

Both are covered by tests that match the SDK's typed errors rather than
grepping the message, since the message was never the part that was broken.

* docs(services): document the new operations and optional capabilities

Adds the network interface, VPC attribute and main-route-table behaviour to
the networking reference, and the subnet-group, replication-group and load
balancer attribute operations to their sections.

The optional capabilities are marked as such and counted separately, because a
driver without them is still complete — folding them into the core totals would
imply every provider must implement an AWS-only concept.

* feat(gcp): Cloud Routers

A caller building a network with private subnets creates a router and then
patches Cloud NAT onto it, so without routers the network step stops after the
subnets exist and before anything on them can reach out. The whole step failed
with methodNotAllowed, which named the method rather than the missing
resource — the error sends an investigation in the wrong direction.

Routers are held in the handler rather than the networking driver: the driver
models the portable subset shared across clouds, and a router carrying embedded
NAT blocks is specific to this provider's REST shape.

The body is stored verbatim and echoed back, so a caller that patches a field
this handler does not model still reads it. Patch replaces rather than merges,
which is what makes a read agree with the write a caller just sent.

* feat(gcp,azure): Service Networking connections and the subscriptions list

Two endpoints a caller reaches before it can finish, both of which returned
methodNotAllowed and stopped the work that followed.

Service Networking backs private services access — a VPC reaching a
Google-managed service over internal addresses. A caller sets a connection up
while building a network and removes it while tearing one down, so its absence
blocked teardown rather than just the feature. Connections are records here;
nothing routes the peering they stand for.

The Azure subscriptions collection is what a caller lists to verify a
credential before connecting an account. The list is empty on purpose: this
emulator has no tenant model, so it cannot say which subscriptions a credential
reaches, and inventing some would fabricate an authorization boundary. A
well-formed empty answer lets the caller complete the request and decide.

* feat(gcp): global and regional addresses

Private services access reserves an address range to carve out the block a
managed service is peered into, and releases it when the network goes away.
Without the resource the release fails, and every teardown step behind it is
abandoned — the network, and the IAM the network depends on.

Addresses are keyed by the scope they were reserved in, so a global address and
a regional one sharing a name stay distinct. Held in the handler rather than
the networking driver for the same reason as routers: a reserved range with a
purpose and prefix length is this provider's shape, not the portable subset.

With this a GCP cluster provisions AND tears down end to end against the
emulator: all six create steps and all five delete steps.

* feat(azure): resource groups

Every Azure resource lives in a resource group, so a caller creates one before
anything else and deletes it last. The API was unimplemented, so the first step
of any Azure provisioning run failed and nothing behind it was reachable.

A group reports provisioningState Succeeded immediately: it is usable the
moment it exists, and a caller that polls would otherwise never see it leave a
pending state. Deleting one that is already gone succeeds, because that is the
caller's desired end state and a teardown retry must not fail on its second
pass.

The collection segment is matched case-insensitively — callers spell it both
resourcegroups and resourceGroups — and a path continuing into /providers/...
is a resource inside the group, left to the handler that owns it.

With this an Azure cluster provisions and tears down end to end against the
emulator.

* fix: address review — enforce the ENI drain, and stop silent-empty describes

Three things the review was right about.

The ENI drain was described but never enforced. attachManagedENI and the test
comments claimed a VPC delete is refused while an interface is attached, and
DeleteVPC/DeleteSubnet deleted unconditionally — the emulator accepting what
EC2 rejects, which is the class this branch set out to fix. Both now refuse
with DependencyViolation while an attached interface remains, and the refusal
is recoverable: detach, delete, and the delete succeeds. A test covers both the
refusal and the recovery, because a refusal that cannot be cleared is just a
wall.

DescribeTargetGroups and DescribeNetworkInterfaces answered an empty list for a
resource that does not exist — the same anti-pattern already fixed for load
balancers. Both now report not-found for an explicitly named ARN or ID.

Also: gofmt on three files (a CI blocker), stale "Phase 2" comments where the
work has since landed, a nolint that named a case count now wrong, and a
doubled error-code prefix in two elb messages — the code is added by the wire
layer, so the driver naming it too produced "TargetGroupNotFound:
TargetGroupNotFound: ...".

* docs(services): document the handler-level resources and reconcile counts

Five resources added on this branch have no driver behind them — routers,
addresses, Service Networking connections, resource groups, the subscriptions
list — so they had no place in the per-service tables and went undocumented.
They are part of the emulated surface regardless, and a reader checking whether
something is supported should not have to grep the server packages.

A new section lists them with the reason each has no driver, alongside SSM Run
Command and the materialised AWS-managed policies and published AMI parameters.
Run Command gets its own table and repeats, in the place a reader will look,
that nothing executes.

Load Balancer moves to 21: the two attribute operations were always in the
driver but unlisted, and the ELBv2 handler now exposes them. Handler-level
resources are excluded from the totals — they have no driver operations to
count — and that exclusion is stated rather than left to be inferred.

* style: US spelling, and constants for values this branch duplicated

The project lints for US spelling and this branch was written in British
throughout — behaviour, recognise, materialise, catalogue, synthesised and
friends across fourteen files. Mechanical, but it was a lint failure in every
file the branch touched.

Two goconst hits were caused by the new code rather than pre-existing: the
replication-group path repeated the engine, node-type and status defaults the
cache-cluster path already used. They are constants now, shared by both, so the
two cannot drift apart.

The duplicate-code warning on the two describe handlers is suppressed the way
the codebase already suppresses it for per-resource describe pairs: the shape
is duplicate by design, each reading its own collection.

* fix: address the remaining review items

PDB discovery advertised watch and patch, neither of which pdb.go implements —
a reflector would open a watch that returns a list and never streams, and
kubectl or helm would send a PATCH that 405s. Both failures land in the caller,
far from the document that promised them. It now advertises only what it serves.

DeleteCluster resolved the endpoint after deregistering the data plane, so the
delete response described the cluster with the not-implemented sentinel rather
than the endpoint it had been reachable on. Resolved before, since the response
describes the cluster as it was.

Two error codes said one thing and sent another. The unsupported-capability
error named InvalidAction in its message while the generic mapping emitted
InvalidParameterValue; SendCommand to a rejected target named InvalidInstanceId
while surfacing the parameter-store ParameterNotFound. Callers match on the
code, so both now emit what they claim.

A malformed NumCacheClusters was coerced to a default instead of rejected,
quietly building something other than what was asked for. Absent still means
"unspecified"; present-but-unparseable is a ValidationException.

NOT fixed — the k8s CA mismatch (#4). The data plane is HTTP while the cluster
advertises a parseable CA, so a caller building a rest.Config from Endpoint plus
CertificateAuthority alone fails the handshake. Clearing the CA was implemented
and reverted: callers that build a kubeconfig need a parseable CA to construct
one at all, and a full provisioning run broke at the first step that uses it.
Resolving it properly means serving the data plane over TLS with this CA. The
mismatch is now documented where the endpoint is set rather than left implicit.

* refactor(networking): make interfaces and VPC attributes optional capabilities

Both were added to the core Networking interface, which forced every provider
to implement them. Azure and GCP model neither, so they carried byte-identical
copies of the AWS implementation purely to satisfy the interface — three
versions of the same logic to keep in sync, and two of them serving no caller.

They are optional capabilities now, discovered by type assertion, matching the
pattern already used for subnet groups, replication groups and Run Command. The
duplicated Azure and GCP implementations are deleted. A driver that does not
model them answers InvalidAction, which is the truthful response for a cloud
with no such resource.

ModifyVPCAttribute takes a VPCAttributeUpdate rather than positional pointers,
so a new attribute can be added without changing every implementation and every
call site.

The service wrapper forwards both capabilities, since a caller holding a
*Networking must see the same capability set as the driver beneath it —
otherwise wrapping would hide them and every optional capability would look
unimplemented.

Compile-time assertions now cover the optional interfaces as well. Without
them a signature drifting out of shape stops satisfying the interface silently
and every call answers InvalidAction at runtime instead of failing the build.

* feat(eks): make the advertised certificate authority certify the data plane

DescribeCluster returned a self-signed CA whose private key was discarded the
moment it was generated, so it could never sign anything. A caller building a
rest.Config from Endpoint plus CertificateAuthority — the ordinary way to reach
a cluster — presented that CA to a plain-HTTP listener and failed the
handshake. Advertising a valid CA for a server it does not certify is worse
than advertising none, because it reads as working.

The CA key is retained now, and ServingTLSConfig mints a leaf signed by it for
whoever serves the data plane. Served that way, the CA is true: a client
validating the endpoint against it succeeds, with no skip-verify anywhere.

Clearing the field instead was tried first and is worse — callers that build a
kubeconfig need a parseable CA to construct one at all, and a full provisioning
run broke at the first step that used it. The fix had to make the CA correct,
not absent.

* feat(discovery): surface interfaces and elastic IPs, and pin the PDB verbs

The resources this branch added were invisible to Resource Explorer, Resource
Graph and Cloud Asset. That is fine for a teardown driven by Describe against
ids it already holds, but a discovery-driven sweep would walk past an elastic
IP that is still costing money and an interface that is still blocking a VPC
delete.

Interfaces are gathered through the optional capability, so a driver that does
not model them contributes nothing rather than failing the whole walk — a cloud
with no interfaces has none to discover, which is not an error.

Adds the tests the review found missing. Availability zones: that more than one
is returned, since callers place subnets one per zone and a single-zone answer
collapses a multi-AZ network into one that fails much later. Elastic IPs: the
full reserve/read/release cycle, because an allocation outliving its teardown
keeps accruing charges. PodDisruptionBudgets: that the advertised verbs match
the implemented ones, so adding a verb to the advertisement without writing it
fails here rather than inside whatever client trusted the document.

* style(ec2): reuse the existing constants this branch duplicated

The new interface code introduced a third "attached" and a second "true",
the latter alongside a formTrue constant that already existed. Both now use
the shared value.

The dispatch functions are annotated the way the file already annotates its
describe pairs: an action switch has this shape by design, and the duplication
warning between two of them is not something to restructure away.

* docs(services): file the provider-specific resources under what they are

The section added earlier was organized around which layer holds a resource's
state — "no driver" — and that is an implementation detail no reader of a
resource reference is looking for. Someone asking whether Cloud Routers are
supported looks under GCP networking, not under a section about driver absence.
The "why no driver" column was design justification rather than reference
material.

It was also wrong in one row: SSM Run Command has a driver interface, so
listing it as having none contradicted the code. It is documented now as the
optional capability it is, next to the other two.

Resources are grouped by provider and service, with what each supports. The
materialized IAM policies and published AMI parameters are described as
behavior of existing resources, because that is what they are — not resources
in their own right.

* fix(serve): serve the Kubernetes data plane over TLS, and surface CA failures

ServingTLSConfig had no call sites. The certificate authority was made able to
certify the data plane, and then the data plane carried on being served over
plain HTTP — so the claim that the advertised CA certifies the endpoint was
still false for anyone running `cloudemu serve`. A rest.Config built from
Endpoint plus CertificateAuthority failed the handshake exactly as before.

The listener now serves with a leaf signed by that CA and advertises https, so
the two agree. Verified against the built binary: the presented certificate is
CN=cloudemu-k8s issued by CN=cloudemu-eks-ca, and plain HTTP to the port is
refused.

initCA also swallowed generation failures. It ran under sync.Once and returned
early on error, leaving the CA empty forever while stubCertificate handed that
empty value to every cluster — the silent-placeholder failure this work exists
to remove, reappearing one level down. It returns an error through
sync.OnceValues now, and ServingTLSConfig propagates it.

* fix: address the second review — a crash, a data-loss delete, and a race

Three of these are defects in code this branch added.

LBAttributes.Extra was shared, not copied. GetLBAttributes returned a struct
copy whose map still aliased the stored one, and the attribute handler then
wrote into it outside the lock while doing a read-modify-write. Two overlapping
ModifyLoadBalancerAttributes on one load balancer crash the process with a
concurrent map write. The map is copied on the way out and on the way in, so
neither side can reach the other's state.

A Service Networking delete with no network parameter erased every connection
in the process, not just the caller's — the parameter defaulted to the
match-everything wildcard. A delete now has to name its target.

Interface fields were mutated without a lock. memstore copies the map on All()
but the values are pointers, so a detach raced every concurrent read of the
same interface. Guarded, and the suite passes under -race.

Also: a subnet group whose member subnets do not resolve now leaves VpcId blank
rather than silently deriving one, which is the teardown leak this branch set
out to close; ReleaseAddress resolves a PublicIp back to its allocation id
instead of passing it through as one, which always missed while the comment
claimed both forms worked; the EKS-optimized AMI tree names its leaf image_id,
so matching only ami-id left every EKS image lookup unresolved; and the new GCP
handlers decode through the bounded reader their siblings use.

* fix(discovery): surface interface-listing failures, and cover the new walkers

walkNetworkInterfaces returned nil on any error, so a driver that models
interfaces and then failed to list them produced a complete-looking inventory
missing whatever could not be read — the opposite of what a sweep for orphans
is for. Errors propagate now; a driver that does not implement the capability
still contributes nothing, which is a different thing and stays silent.

Adds the tests the re-review found missing: that a walk surfaces elastic IPs
and interfaces at all, and that a failing listing surfaces rather than being
swallowed.

* fix(aws): complete the pointer-mutation race sweep, atomic LB attributes, delete-in-use

The ENI race fix guarded one site. memstore hands out the stored pointer for
every `Store[*T]`, so the same read-modify-write existed everywhere else that
pattern is used — guarding ENIs alone left the class open.

Swept the rest: VPC attributes, route creation/deletion, and elastic-IP
association all mutate a shared pointer, and their describe paths read it.
The mutex moves from a package global onto Mock, which also stops it being
shared across emulator instances.

Load-balancer attributes gain an atomic update. Modification is a
read-modify-write, and doing it as Get-then-Put drops the lock in between, so
two overlapping updates each read the same base and the second write discards
the first. Exposed as an optional capability so other providers need no
implementation, with the Get/Put path kept as the fallback.

Subnet groups now refuse to delete while something is still placed in them
(InvalidDBSubnetGroupStateFault / CacheSubnetGroupInUse), matching real AWS —
a teardown that got a success here would strand a live instance in a group
that no longer exists.

Also: AssociateAddress/DisassociateAddress get the wire handlers the file
header already claimed they had; IAM managed-policy materialization uses
SetIfAbsent so two concurrent first-references cannot hand out mismatched
ARNs; two bare body decodes get the cap their siblings use; and the dead
findRouteTableForVPC wrapper is gone.

Tests pin each fix rather than describe it — the race tests were confirmed to
fail (10 race/lost-update reports) with the guards removed, and the EKS TLS
test completes a real handshake trusting only the advertised CA, with a
vacuity guard so an empty pool cannot make it pass.
thzgajendra added a commit to thzgajendra/cloudemu that referenced this pull request Jul 29, 2026
Address review MEDIUM stackshy#2/stackshy#3/stackshy#4 (Azure update paths) and stackshy#7 (MI discovery):

- PUT on an existing Azure SQL server / database / managed instance now applies
  the request body (upsert) instead of returning the stale record.
- PATCH is a genuine merge — elastic pools, failover groups and managed
  instances gain Update* capability methods that overlay only the fields the
  request supplied, so a partial PATCH no longer wipes unspecified fields; MI
  PATCH now decodes and applies its body instead of being a no-op.
- Managed instances are projected into cross-service discovery
  (microsoft.sql/managedinstances) via sqlDiscovery + the Resource Graph type
  map, so a created MI appears in inventory.

Covered by SDK PATCH-merge round-trip tests (elastic pool keeps its SKU when
only maxSizeBytes is patched; MI keeps administratorLogin when only vCores is
patched) and a managed-instance type-map test.
thzgajendra added a commit that referenced this pull request Jul 30, 2026
…xible Server, Cloud SQL) (#303)

* feat(azure): full parity for Azure MySQL Flexible Server sub-resources

Add databases, firewall rules and server configurations plus the server
failover action to Azure Database for MySQL Flexible Server, bringing it to
native parity with the ARM surface real armmysqlflexibleservers clients use.

Introduce Databases, FirewallRules, Configurations and Failover as optional
relationaldb driver capabilities (mirroring SubnetGroups), so the same
interfaces are reusable by the other managed-SQL services. The mock stores each
family per server and cascade-deletes children on server delete; the ARM handler
routes databases/firewallRules/configurations, updateConfigurations (batch) and
the failover action. Covered by real-SDK round-trip tests and mock-level
error-path/cascade tests.

* feat(azure): full parity for Azure PostgreSQL Flexible Server sub-resources

Add databases, firewall rules and server configurations to Azure Database for
PostgreSQL Flexible Server, reusing the Databases/FirewallRules/Configurations
optional relationaldb capabilities introduced for MySQL Flex. Postgres Flex has
no failover action and no batch-configuration endpoint, and its configuration
resource accepts both PUT and PATCH; the handler and mock reflect that. The mock
cascade-deletes children on server delete. Covered by real-SDK round-trip tests
(the SDK has no ClientFactory, so each client is built from shared options) plus
mock-level default/error/cascade tests.

* feat(azure): full parity for Azure SQL server sub-resources

Add firewall rules, virtual-network rules, elastic pools, failover groups and
the Azure AD administrator to Azure SQL (Microsoft.Sql). Firewall rules reuse
the shared FirewallRules capability; the other four are added as optional
relationaldb capabilities (VNetRules, ElasticPools, FailoverGroups, AADAdmins)
alongside the existing SubnetGroups pattern. Failover-group failover flips the
local replication role between Primary and Secondary. The mock cascade-deletes
all child resources on server delete and returns isolated copies of the
slice-bearing failover-group state. Covered by real-SDK (armsql) round-trip
tests across all five families plus mock-level error/cascade/aliasing tests.

* feat(gcp): full parity for Cloud SQL databases, users, certs and instance ops

Add databases (via the shared Databases capability), users and client SSL certs
as instance child resources, plus the clone, failover, promote-replica and
start/stop-replica instance actions to GCP Cloud SQL. Users and SSL certs are
new optional relationaldb capabilities (Users, SslCerts); clone and replica
promotion are Clonable and ReplicaPromotion; failover reuses the shared Failover
capability. The REST path parser now recognizes the databases/users/sslCerts
sub-collections, and the users route honors Cloud SQL's ?name= query quirk for
delete and update. The mock cascade-deletes children on instance delete. Tiers
and flags catalogs are intentionally out of scope (separate path shapes, static
data). Covered by real sqladmin SDK round-trip tests plus mock-level
clone/cascade/error tests.

* feat(discovery): surface managed SQL servers in Resource Graph and Cloud Asset

Add a RelationalDatabases discovery capability to the resource-discovery engine
and a walkRelationalDB walker, mirroring the Kubernetes adapter pattern. Azure
wires an adapter that projects Azure SQL logical servers plus MySQL/PostgreSQL
Flexible Servers, and GCP projects Cloud SQL instances, so managed relational
databases appear in cross-service inventory. Resource Graph maps the portable
types to microsoft.sql/servers, microsoft.dbformysql/flexibleservers and
microsoft.dbforpostgresql/flexibleservers; Cloud Asset maps Cloud SQL to
sqladmin.googleapis.com/Instance. Both type-map switches become lookup tables to
stay under the cyclomatic-complexity gate. Covered by walker and type-map tests.

* feat(cost): add managed relational-database rates

Add relationaldb:* entries to the cost rate catalog so provisioning a managed
database server/instance (RDS, Azure SQL, Azure MySQL/PostgreSQL Flexible
Server, Cloud SQL) is billed per instance-hour, clusters per cluster-hour, and
restores reuse the instance-hour, while snapshots and lifecycle actions are
free. The portable "relationaldb" service name means one catalog covers every
cloud. Covered by a cost-tracker test.

* docs: document full managed-SQL parity (sub-resources, discovery, cost)

Document the native sub-resource capabilities added to Azure SQL, the Azure
MySQL/PostgreSQL Flexible Servers and Cloud SQL — databases, users, firewall/
vnet rules, configurations, elastic pools, failover groups, AAD admins, SSL
certs, clone/failover/replica actions — along with their discovery surfacing and
cost rates. Refresh the relational-database operation totals and the discovery
driver list.

* feat(sql): Cloud SQL tiers/flags catalogs + elastic-pool metrics

Close the remaining managed-SQL parity gaps: serve the Cloud SQL machine-tier
catalog (GET /v1/projects/{p}/tiers) and the database-flag catalog
(GET /v1/flags, which is project-less) as static reference data, and emit
Microsoft.Sql/servers/elasticpools metrics (cpu/storage/workers percent) when an
Azure SQL elastic pool is created. Also annotate the ModifyInstance/ModifyCluster
methods across the four SQL mocks with the driver-interface hugeParam nolint now
that the shared ModifyInstanceInput grew. Covered by SDK tiers/flags round-trip
and an elastic-pool metric-emission test.

* feat(azure): add SQL Managed Instance family to Azure SQL

Add the Microsoft.Sql/managedInstances resource type and its managed databases
as a ManagedInstances optional relationaldb capability: managed-instance CRUD +
list + start/stop/failover actions, and managed-database CRUD + list. The
handler now matches the managedInstances resource type alongside servers and
routes both, and the mock cascade-deletes managed databases when their instance
is removed. Covered by an armsql managed-instance/database SDK round-trip test
(create → get → list → failover → cascade delete) and a mock-level lifecycle
test.

* docs: document managed instances, tiers/flags and elastic-pool metrics

Update the relational-database section for the now-complete managed-SQL parity:
add the ManagedInstances capability row, note Cloud SQL's tiers/flags reference
catalogs and Azure SQL's Managed Instance family, and record the elastic-pool
metric namespace. Refresh the optional-operation totals (25 capability
interfaces, +109 relational / +118 grand-total optional).

* fix(sql): clone ManagedInstance tags on read; deterministic list ordering

Address review MEDIUM #1 and the list-ordering LOW: GetManagedInstance /
ListManagedInstances now clone the stored Tags map so a caller mutating the
returned map can't corrupt the store (the copy-on-read hole flagged as the
concurrent-map panic class), and every List* mock method iterates
memstore.SortedValues() instead of All() for deterministic SDK list ordering,
matching the documented convention. Large-value list loops use index iteration
to avoid per-element copies.

* fix(azure): real update semantics + discoverable managed instances

Address review MEDIUM #2/#3/#4 (Azure update paths) and #7 (MI discovery):

- PUT on an existing Azure SQL server / database / managed instance now applies
  the request body (upsert) instead of returning the stale record.
- PATCH is a genuine merge — elastic pools, failover groups and managed
  instances gain Update* capability methods that overlay only the fields the
  request supplied, so a partial PATCH no longer wipes unspecified fields; MI
  PATCH now decodes and applies its body instead of being a no-op.
- Managed instances are projected into cross-service discovery
  (microsoft.sql/managedinstances) via sqlDiscovery + the Resource Graph type
  map, so a created MI appears in inventory.

Covered by SDK PATCH-merge round-trip tests (elastic pool keeps its SKU when
only maxSizeBytes is patched; MI keeps administratorLogin when only vCores is
patched) and a managed-instance type-map test.

* fix(azure): model Azure SQL database elastic-pool membership

Address review MEDIUM #5. Databases now carry an ElasticPoolID (added to the
shared InstanceConfig/Instance/ModifyInstanceInput and surfaced as the
elasticPoolId ARM database property), so an SDK client placing a database into a
pool round-trips instead of silently dropping it. DeleteElasticPool now returns
a precondition error while the pool still contains databases, matching real
Azure's 409. Covered by a membership/delete-guard test.

* fix(gcp): real Cloud SQL read-replica and failover semantics

Address review MEDIUM #6. A Cloud SQL insert with masterInstanceName now creates
an actual read replica: the replica records its master (ReadReplicaSource) and
the primary lists it (replicaNames), both surfaced in the instance body. Replica
lifecycle is faithful — start/stopReplica require the target to be a replica and
leave it RUNNABLE (no SUSPENDED state change); promoteReplica detaches it from
the primary (and rejects a non-replica); failover is a primary-only operation
and rejects a replica. Covered by SDK and mock replica-lifecycle tests.

* fix(sql): review LOW items — determinism, validation, replica fidelity tests

- Cloud SQL backup-run IDs are generated by the mock from the clock + a
  monotonic counter (was time.Now().UnixNano(), violating the Clock determinism
  rule and collision-prone); CloneInstance now clones the source's databases and
  resets replica linkage; UpdateUser doc corrected.
- Firewall rules validate IPv4 start/end (Azure SQL + both Flexible Servers);
  Azure SQL Managed Instance requires subnetId, as real Azure does.
- Added a real-SDK Resource Graph indexing test that drives sqlDiscovery
  end-to-end (logical server + managed instance appear and filter), and a
  -race concurrency test that mutates a returned managed-instance Tags map to
  pin the copy-on-read fix.

* fix(sql): address second review — MI lifecycle, in-place restore, validation

- Cloud SQL DeleteInstance now unlinks replica<->master before cascading
  children, so no dangling ReadReplicaSource/ReadReplicaTargets remain.
- Managed Instance lifecycle gains a state guard (transitionManagedInstance):
  Start/Stop respect current state, Failover requires Ready; MI create emits
  representative metrics and clones returned Tags; adds CreateManagedInstance
  cost rate.
- Cloud SQL RestoreBackup restores in place onto the existing instance via a
  new optional BackupRestorer capability (was create-new -> 409).
- Failover-group failover requires a partner server; firewall rules validate
  Start <= End; Azure Flex SetConfiguration rejects unknown params and empty
  values; ARM parser captures the action verb so forced vs planned failover
  and unknown POST verbs are distinguished; Cloud SQL selfLink/targetLink use
  the served /v1 prefix.
- Broaden new-package test coverage (sub-resource CRUD, MI lifecycle, Users
  update, backup get, error paths, server PATCH, raw MI start/stop).

* fix(sql): address third review — output aliasing, config catalog, validation

- Deep-copy Tags/slice fields on every Instance/Cluster/Snapshot return path in
  azuresql and cloudsql (Describe/Create/Modify/Restore/Clone), so a returned
  value never aliases the memstore — fixes a potential concurrent-map crash and
  matches the managedinstance/FailoverGroup discipline.
- Azure Flex GetConfiguration/ListConfigurations return catalog defaults for a
  known-but-unset parameter (real Azure behavior) instead of NotFound/empty; the
  catalog now maps names to defaults.
- MySQL Flex batch config update is atomic: all entries are validated before any
  is applied, via a new optional BatchConfigurations capability.
- Azure SQL database create/modify validates the referenced elastic pool exists
  (symmetry with the pool-delete member guard).
- Cloud SQL rejects child (database/user) names containing '/' that would orphan
  a row; get-paths add length guards on single-ID Describe lookups.
- Flex firewall create validates start <= end (parity with Azure SQL).
- Tests: aliasing, config defaults, batch atomicity, elastic-pool validation,
  child-name rejection, FG update/list, managed-database delete, and Azure
  wire-level error-mapping (404/400) across all three Azure SQL services.
NitinKumar004 added a commit that referenced this pull request Jul 30, 2026
…ocs)

From thzgajendra's review of PR #299:
- Lint (#1): replace the version-sensitive //nolint:prealloc directives with
  real preallocation (coreResources/appsResources/registryAPIResources,
  openapi kinds, endpoint-address slice), so golangci-lint is clean regardless
  of prealloc's version-dependent placement — no dangling directives.
- Docs (#2): drop the 'pagination' claim from the k8s data-plane sentence in
  sdk-server.md; data-plane lists are unpaginated, matching services.md.
- Watch load-shedding (#3): a slow watcher that overflows its buffer now
  receives a 410 Gone (ERROR) event and the stream ends, so client-go relists
  instead of running with a permanently-divergent cache. Regression test added.
- GC comment (#4): correct the 'never mutate while ranging' wording — deleting
  the current key mid-range is legal; the BFS is what makes the cascade
  order-independent.
- Provider package headers (#8): describe the now-wired data plane instead of
  'out of scope / Wave 2'.

Deferred as documented follow-ups (all Low): clamp/silent-signal, Job shrink
reconcile, committed kubectl smoke test, provider CA-vs-sentinel misconfig
window.
NitinKumar004 added a commit that referenced this pull request Jul 30, 2026
…upport (#299)

* feat(k8s): connect parity across EKS/AKS/GKE via a shared CA (Phase A)

The data-plane serving cert and every provider's advertised CA must be the
same authority or client-go's TLS handshake fails. Extract the CA into a new
internal/k8spki package used by both the serving TLS config and all three
control planes:

- EKS: tls.go now delegates to k8spki (serve + EKS call sites unchanged).
- GKE: advertise the real CA in masterAuth.clusterCaCertificate; drop the
  unparseable dummy blob that broke the handshake outright.
- AKS: embed the real CA in the rendered kubeconfig and drop
  insecure-skip-tls-verify — parity with EKS/GKE.

Tests: AKS data-plane test now serves with the k8spki cert and validates
end-to-end (no skip-verify); new GKE real-TLS connect-parity test proves the
advertised CA certifies the endpoint (create cluster -> validate CA ->
client-go ConfigMap round-trip). RenderKubeconfig test updated to the real-CA
behavior.

First phase of the k8s runtime/parity work; registry refactor + reconcile
engine + workload kinds follow on this branch.

* feat(k8s): parse resource subresources in the router (Phase B foundation)

Add Route.Subresource and parse the /{name}/{subresource} tail for both
cluster-scoped (/api/v1/nodes/n/status) and namespaced
(/apis/apps/v1/namespaces/ns/deployments/d/scale) shapes. ServeHTTP routes
subresource requests to a dedicated dispatcher (stubbed to 404 until the
reconcile phase wires /status and /scale) so a subresource path is never
mis-parsed as a write against the parent object. Updated the parseRoute unit
test to the new (correct) cluster-subresource semantics.

* feat(k8s): generic resource registry + reconcile engine (Phases B+C)

Turn the k8s data plane from a CRUD store into a minikube-like runtime.

Registry (registry.go, registry_ops.go, registry_defs.go): a generic
unstructured-backed store + one handler serving CRUD, list (label & field
selectors), watch, patch, delete (ownerReference garbage collection), and the
/status + /scale subresources for any registered kind. New kinds are a
registration + optional reconcile hook. Registers apps/v1 ReplicaSet,
StatefulSet, DaemonSet and core/v1 PersistentVolumeClaim; discovery is derived
from the registry so it can't drift.

Reconcile engine (reconcile.go), run synchronously on every write (no
controller goroutines, so it stays deterministic):
- Pods are driven Running with a synthetic Pod IP and ready containers.
- Deployment materializes its Pods and reports real status; ReplicaSet and
  DaemonSet do likewise; StatefulSet creates stable-ordinal Pods (name-0..N-1)
  plus a Bound PVC per volumeClaimTemplate.
- The endpoints controller fills a Service's Endpoints from the Running Pods
  matching its selector, and drains them when Pods are deleted/GC'd.
- Deleting a controller cascades to its Pods; scaling (spec.replicas or the
  /scale subresource) adjusts the Pod count.

Typed handlers: Deployment now reconciles + serves /scale and /status; direct
Pod creates come up Running; Pod list honors label/field selectors; Service
create populates endpoints.

Tests: new client-go WorkloadRuntime E2E (Deployment+Service -> Running pods +
endpoints -> scale to 4 -> StatefulSet with 3 stable pods + 3 Bound PVCs ->
DaemonSet -> cascade teardown). Existing pod/cascade/provider tests updated to
the new Running/materialized behavior. Deployment /scale + /status advertised
in discovery.

Deferred to later phases: the intermediate ReplicaSet object for Deployments
(pods are owned by the Deployment directly); Job/CronJob, Ingress, RBAC, HPA,
Node, Event, NetworkPolicy, EndpointSlice; strategic-merge / server-side-apply.

* k8s: register batch/networking/rbac/storage/autoscaling/discovery + core supporting kinds; registry-driven discovery

Adds registry entries for Job/CronJob, Ingress/IngressClass/NetworkPolicy,
RBAC (Role/RoleBinding/ClusterRole/ClusterRoleBinding), StorageClass,
HorizontalPodAutoscaler, EndpointSlice, and core PVC/PV/Node/Event/
ResourceQuota/LimitRange. Reconcile hooks drive Job pods to Succeeded,
Ingress to a load-balancer IP, and PV to Available/Bound.

serveDiscovery now derives the /apis group list and every
/apis/<group>/<version> resource list from registeredResources() (seeded
with the typed apps/policy groups) instead of a hardcoded switch, so new
groups and their subresources are discoverable by kubectl and client-go
without drifting from what the server serves.

* test(k8s): e2e coverage for supporting kinds via client-go

Drives Jobs (complete to Succeeded with materialized pods), Ingresses
(get a load-balancer IP), PVCs (bind), StorageClass/RBAC/HPA/Node
round-trips, and asserts the new API groups are discoverable — the
negotiation kubectl and client-go do before any typed request.

* k8s: rolling updates replace Pods on pod-template change; advertise endpoints

- Controllers stamp a pod-template-hash label on Pods; reconcile treats a
  changed template hash as a rolling update and replaces stale-hash Pods
  (Deployment/ReplicaSet via syncScaledPods, StatefulSet via syncStablePods).
  Convergence is instant — no surge/unavailable pacing.
- buildControllerPod now copies the template label map before stamping, so it
  can't mutate the controller's shared template.
- Discovery advertises core/v1 endpoints (get/list/watch only, matching the
  read-only handler) so kubectl/client-go can resolve them.

E2E: runtime test now exercises a rolling update (image change replaces all
Pods, endpoints re-point); supporting-kinds test asserts endpoints discovery.

* docs(k8s): document the minikube-like data plane and its non-goals

Update services.md §18, sdk-server.md, and the package doc to reflect the
reconcile engine (Running Pods, Endpoints, binding PVCs, completing Jobs),
validated TLS via the shared CA, the full multi-group resource surface with
/scale and /status subresources, rolling updates, and the deliberate emulation
boundaries (no exec/logs/portforward, no scheduling, no quota/RBAC/policy
enforcement, no HPA/CronJob actuation).

* fix(k8s): merge-patch to /scale no longer silently scales to zero

applyUnstructuredPatch decoded the merged JSON with plain json.Unmarshal
into map[string]any, which turns whole-number JSON into float64.
unstructured.NestedInt64 accepts only int64, so spec.replicas read back as
0 — a 'kubectl scale --replicas=N' (a merge-patch to the /scale subresource,
or any merge-patch touching replicas) silently scaled the workload to zero.

Decode via unstructured.Unstructured.UnmarshalJSON instead, which preserves
integers as int64. This fixes every merge-patch path (object and /scale) at
the root. Regression test drives a merge-patch scale-up and asserts both the
returned Scale and the stored object carry replicas=4.

* fix(k8s): address review findings across reconcile, GC, endpoints, PKI

- Endpoints: only bump ResourceVersion / publish MODIFIED when the address set
  actually changes. resyncEndpointsForNamespaceLocked runs for every Service on
  any Pod change, so an unchanged Service was emitting a spurious watch event
  (with a climbing RV) on unrelated Pod churn. Regression test added.
- Pod field selector: support spec.nodeName. Every materialized Pod is scheduled
  to the synthetic node, so 'kubectl get pods --field-selector spec.nodeName=...'
  (node-drain/kubelet tooling) previously returned an empty list. E2E covers it.
- Garbage collection: walk the owned set breadth-first and collect UIDs before
  deleting, instead of mutating each store's map while ranging it and recursing.
  Pods owned by an intermediate controller (not just the root) are now reaped.
- Scale subresource: only bump generation when spec.replicas actually changes,
  matching registryUpdate/registryPatch (no spurious generation != observed).
- Job: ignore a non-positive spec.completions (default to 1) so a Job can't
  report Complete having run zero Pods.
- StatefulSet PVCs: deep-copy the volumeClaimTemplate spec per ordinal instead
  of aliasing one map across every PVC.
- PKI: give each serving leaf a random 128-bit serial (was fixed '2') and assert
  BasicConstraintsValid (cA=FALSE) so strict non-Go verifiers accept the leaf.
- docs: correct the data-plane list to say it is unpaginated (limit/continue are
  not honored) and scope field-selector support accurately.

* k8s: real-user kubectl parity — protobuf writes, OpenAPI v2/v3, strategic & JSON patch

Driving a running cloudemu server with real kubectl (cluster created via the
EKS/GKE/AKS SDK, then kubectl against the advertised endpoint) surfaced gaps
the JSON-forcing client-go tests masked. kubectl now works end-to-end.

- Protobuf request bodies: kubectl sends built-in kinds as protobuf on writes
  and does NOT retry as JSON on 415, so every 'kubectl create/apply/scale'
  write failed. Decode protobuf via the client-go scheme's recognizing
  deserializer (typed handlers decode in place; registry handlers convert to
  unstructured). Responses stay JSON — clients' Accept allows it.
- OpenAPI: serve a v3 discovery root + per-group docs that carry each served
  GVK (with a permissive schema) so kubectl resolves the kind and stays on the
  JSON v3 path; and serve the legacy v2 doc as protobuf bytes (mime-safe
  application/octet-stream content type) for the fallback. Without this
  'kubectl apply' died at 'failed to download openapi'. Served
  cluster-independently in APIServer.ServeHTTP so the prefix-less v3
  serverRelativeURL follow-ups resolve.
- Patch types: typed handlers now accept strategic-merge-patch (kubectl's
  default for set/edit/label — real strategic merge, so the container list
  merges by name) and JSONPatch (RFC 6902), in addition to merge-patch;
  registry handlers gain JSONPatch too.
- Discovery: advertise kubectl short names (pvc, hpa, sts, ds, rs, ing, sc, …)
  for registry kinds so 'kubectl get pvc' resolves.

Verified with real kubectl v1.36 against a standalone server: full lifecycle
(apply → scale → rolling update → statefulset/PVCs → daemonset → job → cronjob
→ ingress → hpa → pv/pvc/storageclass → rbac → networkpolicy → node → all three
patch types → cascade teardown) across EKS, GKE, and AKS connect paths.

* docs(k8s): note full kubectl parity (protobuf, OpenAPI, all patch types)

* k8s: drop len()+1 map capacity hint (clears CodeQL allocation-overflow alert)

* k8s: filter watch streams by label/field selector (review blocker)

Watch streams ignored labelSelector/fieldSelector: typed watches (watchPods,
watchDeployments, …) filtered neither initial nor streamed events, and the
registry watch filtered only the initial snapshot. A selective watch
('kubectl get pods -l app=x -w', or any informer/controller-runtime cache
built with a selector) therefore received non-matching objects — polluting
reflector caches and firing spurious reconciles, which the reconcile engine
amplifies (one Deployment emits many Pod events).

streamWatch now takes a keep(T) predicate applied to both the initial snapshot
and every streamed event; each watch handler builds it from the request's
selectors (parseListSelectors + metaFieldsMatch/podMatchesFields/matchesFields).
Also extracts field-selector-name constants (fixes goconst) and indexes
filterPods to avoid per-item Pod copies.

* k8s: cap materialized pods, bootstrap Node, mirror EndpointSlices, resync endpoints on pod update/patch

Correctness gaps from the PR review:
- Unbounded replicas/completions: the reconciler runs synchronously under the
  cluster lock, so a huge spec value would allocate/hang the whole API. Clamp
  the materialized Pod count to maxReconciledPods (500) for Deployment,
  ReplicaSet/StatefulSet (via replicasOf), and Job. reconcileJob's top-up is
  also made O(n) instead of O(n²).
- Synthetic Node: bootstrap cloudemu-node-0 (Ready, InternalIP) in
  newClusterState so 'kubectl get nodes' is non-empty and the node every Pod is
  scheduled onto actually exists.
- EndpointSlices: mirror each Service's endpoints into a discovery.k8s.io
  EndpointSlice (labelled kubernetes.io/service-name) so EndpointSlice-mode
  consumers (kube-proxy, Gateway API) see the same backends as Endpoints.
- Pod update/patch now resync endpoints (a label change matching a Service
  selector was invisible until unrelated churn) and re-drive a spec-only PUT
  back to Running so it isn't dropped out of the endpoint set.

reconcileServiceEndpointsLocked is split into matchingEndpointAddressesLocked /
writeEndpointsLocked / syncEndpointSliceLocked (also lowers its complexity).
E2E asserts the synthetic Node and populated EndpointSlices; a new watch test
asserts label-selector stream filtering.

* k8s: dedup watch handlers + resolve golangci-lint to zero (review blocker)

- All 8 typed watch handlers now share a generic serveWatch[T] helper (removes
  the dupl the near-identical subscribe/snapshot/stream blocks triggered, and
  addresses the reuse the review flagged).
- golangci-lint (repo .golangci.yml, v2.11.4) is clean on the new non-test
  files: extracted goconst constants (api/apis path segments, status/scale
  subresources, group names), named crypto/mnd magic numbers in k8spki, gave
  parseListSelectors named results, indexed the container-status range,
  lowered ServeHTTP/serveRegistry complexity via dispatchResource /
  serveRegistryItem, fixed the govet shadow, and added reasoned //nolint for
  the legitimately-global lookup tables and hugeParam k8s structs.

No behavior change; build, vet, tests, and -race remain green.

* k8s: bump Deployment generation on spec change; unify /scale patch types

- Typed Deployment now sets metadata.generation=1 on create and advances it
  only on a spec change (update/patch), matching apiserver semantics and the
  registry path so observedGeneration comparisons are meaningful.
- deploymentScale PATCH routes through the shared applyPatchBytes dispatcher,
  so the typed /scale honors merge / strategic-merge / JSONPatch like the
  registry /scale (the two paths no longer diverge).
- Tidy a stale AKS kubeconfig comment (Phase 3 -> the normal path).

* docs(k8s): note watch selector filtering, synthetic node, EndpointSlice mirroring

* k8s: address second-review findings (lint robustness, watch relist, docs)

From thzgajendra's review of PR #299:
- Lint (#1): replace the version-sensitive //nolint:prealloc directives with
  real preallocation (coreResources/appsResources/registryAPIResources,
  openapi kinds, endpoint-address slice), so golangci-lint is clean regardless
  of prealloc's version-dependent placement — no dangling directives.
- Docs (#2): drop the 'pagination' claim from the k8s data-plane sentence in
  sdk-server.md; data-plane lists are unpaginated, matching services.md.
- Watch load-shedding (#3): a slow watcher that overflows its buffer now
  receives a 410 Gone (ERROR) event and the stream ends, so client-go relists
  instead of running with a permanently-divergent cache. Regression test added.
- GC comment (#4): correct the 'never mutate while ranging' wording — deleting
  the current key mid-range is legal; the BFS is what makes the cascade
  order-independent.
- Provider package headers (#8): describe the now-wired data plane instead of
  'out of scope / Wave 2'.

Deferred as documented follow-ups (all Low): clamp/silent-signal, Job shrink
reconcile, committed kubectl smoke test, provider CA-vs-sentinel misconfig
window.
thzgajendra added a commit that referenced this pull request Jul 31, 2026
* feat(azure): full parity for Azure MySQL Flexible Server sub-resources

Add databases, firewall rules and server configurations plus the server
failover action to Azure Database for MySQL Flexible Server, bringing it to
native parity with the ARM surface real armmysqlflexibleservers clients use.

Introduce Databases, FirewallRules, Configurations and Failover as optional
relationaldb driver capabilities (mirroring SubnetGroups), so the same
interfaces are reusable by the other managed-SQL services. The mock stores each
family per server and cascade-deletes children on server delete; the ARM handler
routes databases/firewallRules/configurations, updateConfigurations (batch) and
the failover action. Covered by real-SDK round-trip tests and mock-level
error-path/cascade tests.

* feat(azure): full parity for Azure PostgreSQL Flexible Server sub-resources

Add databases, firewall rules and server configurations to Azure Database for
PostgreSQL Flexible Server, reusing the Databases/FirewallRules/Configurations
optional relationaldb capabilities introduced for MySQL Flex. Postgres Flex has
no failover action and no batch-configuration endpoint, and its configuration
resource accepts both PUT and PATCH; the handler and mock reflect that. The mock
cascade-deletes children on server delete. Covered by real-SDK round-trip tests
(the SDK has no ClientFactory, so each client is built from shared options) plus
mock-level default/error/cascade tests.

* feat(azure): full parity for Azure SQL server sub-resources

Add firewall rules, virtual-network rules, elastic pools, failover groups and
the Azure AD administrator to Azure SQL (Microsoft.Sql). Firewall rules reuse
the shared FirewallRules capability; the other four are added as optional
relationaldb capabilities (VNetRules, ElasticPools, FailoverGroups, AADAdmins)
alongside the existing SubnetGroups pattern. Failover-group failover flips the
local replication role between Primary and Secondary. The mock cascade-deletes
all child resources on server delete and returns isolated copies of the
slice-bearing failover-group state. Covered by real-SDK (armsql) round-trip
tests across all five families plus mock-level error/cascade/aliasing tests.

* feat(gcp): full parity for Cloud SQL databases, users, certs and instance ops

Add databases (via the shared Databases capability), users and client SSL certs
as instance child resources, plus the clone, failover, promote-replica and
start/stop-replica instance actions to GCP Cloud SQL. Users and SSL certs are
new optional relationaldb capabilities (Users, SslCerts); clone and replica
promotion are Clonable and ReplicaPromotion; failover reuses the shared Failover
capability. The REST path parser now recognizes the databases/users/sslCerts
sub-collections, and the users route honors Cloud SQL's ?name= query quirk for
delete and update. The mock cascade-deletes children on instance delete. Tiers
and flags catalogs are intentionally out of scope (separate path shapes, static
data). Covered by real sqladmin SDK round-trip tests plus mock-level
clone/cascade/error tests.

* feat(discovery): surface managed SQL servers in Resource Graph and Cloud Asset

Add a RelationalDatabases discovery capability to the resource-discovery engine
and a walkRelationalDB walker, mirroring the Kubernetes adapter pattern. Azure
wires an adapter that projects Azure SQL logical servers plus MySQL/PostgreSQL
Flexible Servers, and GCP projects Cloud SQL instances, so managed relational
databases appear in cross-service inventory. Resource Graph maps the portable
types to microsoft.sql/servers, microsoft.dbformysql/flexibleservers and
microsoft.dbforpostgresql/flexibleservers; Cloud Asset maps Cloud SQL to
sqladmin.googleapis.com/Instance. Both type-map switches become lookup tables to
stay under the cyclomatic-complexity gate. Covered by walker and type-map tests.

* feat(cost): add managed relational-database rates

Add relationaldb:* entries to the cost rate catalog so provisioning a managed
database server/instance (RDS, Azure SQL, Azure MySQL/PostgreSQL Flexible
Server, Cloud SQL) is billed per instance-hour, clusters per cluster-hour, and
restores reuse the instance-hour, while snapshots and lifecycle actions are
free. The portable "relationaldb" service name means one catalog covers every
cloud. Covered by a cost-tracker test.

* docs: document full managed-SQL parity (sub-resources, discovery, cost)

Document the native sub-resource capabilities added to Azure SQL, the Azure
MySQL/PostgreSQL Flexible Servers and Cloud SQL — databases, users, firewall/
vnet rules, configurations, elastic pools, failover groups, AAD admins, SSL
certs, clone/failover/replica actions — along with their discovery surfacing and
cost rates. Refresh the relational-database operation totals and the discovery
driver list.

* feat(sql): Cloud SQL tiers/flags catalogs + elastic-pool metrics

Close the remaining managed-SQL parity gaps: serve the Cloud SQL machine-tier
catalog (GET /v1/projects/{p}/tiers) and the database-flag catalog
(GET /v1/flags, which is project-less) as static reference data, and emit
Microsoft.Sql/servers/elasticpools metrics (cpu/storage/workers percent) when an
Azure SQL elastic pool is created. Also annotate the ModifyInstance/ModifyCluster
methods across the four SQL mocks with the driver-interface hugeParam nolint now
that the shared ModifyInstanceInput grew. Covered by SDK tiers/flags round-trip
and an elastic-pool metric-emission test.

* feat(azure): add SQL Managed Instance family to Azure SQL

Add the Microsoft.Sql/managedInstances resource type and its managed databases
as a ManagedInstances optional relationaldb capability: managed-instance CRUD +
list + start/stop/failover actions, and managed-database CRUD + list. The
handler now matches the managedInstances resource type alongside servers and
routes both, and the mock cascade-deletes managed databases when their instance
is removed. Covered by an armsql managed-instance/database SDK round-trip test
(create → get → list → failover → cascade delete) and a mock-level lifecycle
test.

* docs: document managed instances, tiers/flags and elastic-pool metrics

Update the relational-database section for the now-complete managed-SQL parity:
add the ManagedInstances capability row, note Cloud SQL's tiers/flags reference
catalogs and Azure SQL's Managed Instance family, and record the elastic-pool
metric namespace. Refresh the optional-operation totals (25 capability
interfaces, +109 relational / +118 grand-total optional).

* fix(sql): clone ManagedInstance tags on read; deterministic list ordering

Address review MEDIUM #1 and the list-ordering LOW: GetManagedInstance /
ListManagedInstances now clone the stored Tags map so a caller mutating the
returned map can't corrupt the store (the copy-on-read hole flagged as the
concurrent-map panic class), and every List* mock method iterates
memstore.SortedValues() instead of All() for deterministic SDK list ordering,
matching the documented convention. Large-value list loops use index iteration
to avoid per-element copies.

* fix(azure): real update semantics + discoverable managed instances

Address review MEDIUM #2/#3/#4 (Azure update paths) and #7 (MI discovery):

- PUT on an existing Azure SQL server / database / managed instance now applies
  the request body (upsert) instead of returning the stale record.
- PATCH is a genuine merge — elastic pools, failover groups and managed
  instances gain Update* capability methods that overlay only the fields the
  request supplied, so a partial PATCH no longer wipes unspecified fields; MI
  PATCH now decodes and applies its body instead of being a no-op.
- Managed instances are projected into cross-service discovery
  (microsoft.sql/managedinstances) via sqlDiscovery + the Resource Graph type
  map, so a created MI appears in inventory.

Covered by SDK PATCH-merge round-trip tests (elastic pool keeps its SKU when
only maxSizeBytes is patched; MI keeps administratorLogin when only vCores is
patched) and a managed-instance type-map test.

* fix(azure): model Azure SQL database elastic-pool membership

Address review MEDIUM #5. Databases now carry an ElasticPoolID (added to the
shared InstanceConfig/Instance/ModifyInstanceInput and surfaced as the
elasticPoolId ARM database property), so an SDK client placing a database into a
pool round-trips instead of silently dropping it. DeleteElasticPool now returns
a precondition error while the pool still contains databases, matching real
Azure's 409. Covered by a membership/delete-guard test.

* fix(gcp): real Cloud SQL read-replica and failover semantics

Address review MEDIUM #6. A Cloud SQL insert with masterInstanceName now creates
an actual read replica: the replica records its master (ReadReplicaSource) and
the primary lists it (replicaNames), both surfaced in the instance body. Replica
lifecycle is faithful — start/stopReplica require the target to be a replica and
leave it RUNNABLE (no SUSPENDED state change); promoteReplica detaches it from
the primary (and rejects a non-replica); failover is a primary-only operation
and rejects a replica. Covered by SDK and mock replica-lifecycle tests.

* fix(sql): review LOW items — determinism, validation, replica fidelity tests

- Cloud SQL backup-run IDs are generated by the mock from the clock + a
  monotonic counter (was time.Now().UnixNano(), violating the Clock determinism
  rule and collision-prone); CloneInstance now clones the source's databases and
  resets replica linkage; UpdateUser doc corrected.
- Firewall rules validate IPv4 start/end (Azure SQL + both Flexible Servers);
  Azure SQL Managed Instance requires subnetId, as real Azure does.
- Added a real-SDK Resource Graph indexing test that drives sqlDiscovery
  end-to-end (logical server + managed instance appear and filter), and a
  -race concurrency test that mutates a returned managed-instance Tags map to
  pin the copy-on-read fix.

* fix(sql): address second review — MI lifecycle, in-place restore, validation

- Cloud SQL DeleteInstance now unlinks replica<->master before cascading
  children, so no dangling ReadReplicaSource/ReadReplicaTargets remain.
- Managed Instance lifecycle gains a state guard (transitionManagedInstance):
  Start/Stop respect current state, Failover requires Ready; MI create emits
  representative metrics and clones returned Tags; adds CreateManagedInstance
  cost rate.
- Cloud SQL RestoreBackup restores in place onto the existing instance via a
  new optional BackupRestorer capability (was create-new -> 409).
- Failover-group failover requires a partner server; firewall rules validate
  Start <= End; Azure Flex SetConfiguration rejects unknown params and empty
  values; ARM parser captures the action verb so forced vs planned failover
  and unknown POST verbs are distinguished; Cloud SQL selfLink/targetLink use
  the served /v1 prefix.
- Broaden new-package test coverage (sub-resource CRUD, MI lifecycle, Users
  update, backup get, error paths, server PATCH, raw MI start/stop).

* fix(sql): address third review — output aliasing, config catalog, validation

- Deep-copy Tags/slice fields on every Instance/Cluster/Snapshot return path in
  azuresql and cloudsql (Describe/Create/Modify/Restore/Clone), so a returned
  value never aliases the memstore — fixes a potential concurrent-map crash and
  matches the managedinstance/FailoverGroup discipline.
- Azure Flex GetConfiguration/ListConfigurations return catalog defaults for a
  known-but-unset parameter (real Azure behavior) instead of NotFound/empty; the
  catalog now maps names to defaults.
- MySQL Flex batch config update is atomic: all entries are validated before any
  is applied, via a new optional BatchConfigurations capability.
- Azure SQL database create/modify validates the referenced elastic pool exists
  (symmetry with the pool-delete member guard).
- Cloud SQL rejects child (database/user) names containing '/' that would orphan
  a row; get-paths add length guards on single-ID Describe lookups.
- Flex firewall create validates start <= end (parity with Azure SQL).
- Tests: aliasing, config defaults, batch atomicity, elastic-pool validation,
  child-name rejection, FG update/list, managed-database delete, and Azure
  wire-level error-mapping (404/400) across all three Azure SQL services.

* feat(gcp): add AlloyDB full-parity support

Adds GCP AlloyDB as a first-class managed database server, reusing the
relationaldb driver.

- Provider providers/gcp/alloydb: implements RelationalDB (clusters,
  instances, cluster backups→ClusterSnapshot, restore) + Users + Databases,
  plus a new optional rdsdriver.AlloyDB capability for AlloyDB-specific
  behavior — rich cluster/instance create, cross-region secondary + promote,
  instance failover/restart, continuous/automated-backup + maintenance config,
  and *Info accessors. Cloud Monitoring metrics on instance create.
  Copy-on-read (clone Tags/slices), '/'-name and instance-type validation,
  cascade delete, and lifecycle state guards throughout.
- Server server/gcp/alloydb: alloydb.googleapis.com/v1 REST handler
  (clusters[/instances|/users], backups, LRO operations, custom methods
  :promote/:createsecondary/:restore/:failover/:restart) using the SDK types
  for wire fidelity; SDK round-trip + wire-error (404/409/400) tests.
- Wiring: providers/gcp/gcp.go (field, monitoring, combined CloudSQL+AlloyDB
  relational discovery adapter, TypeAlloyDBCluster); server/gcp Drivers gains
  an opt-in AlloyDB field (left nil in DriversFrom — its paths collide with
  GKE's, so the two are mutually exclusive on one server).
- Dedicated AlloyDB cost rates; docs/services.md updated (capabilities, GCP
  column, operation counts).

* fix(gcp): address AlloyDB review comments

- One PRIMARY per cluster enforced (base + native create); SECONDARY instance
  requires a SECONDARY cluster; READ_POOL requires nodeCount > 0.
- Detach dangling SECONDARY links when their PRIMARY cluster is deleted (the
  replica-linkage class from #303).
- FailoverInstance requires a PRIMARY target; RestartInstance stays valid on
  any type.
- UpdateUser is now reachable via Users.Patch (was 405).
- Custom methods (:promote / :failover / :restart) are POST-only and unknown
  verbs 404 instead of misrouting to CRUD; GET on :promote no longer promotes.
- Deterministic list order: DescribeClusters/DescribeInstances use SortedValues.
- patchCluster/patchInstance handle the capability assertion; toWireUser returns
  the full resource path; removed banner comments.
- Wiring: guard New against AlloyDB+GKE both enabled (they share paths) and add
  DriversFromWithAlloyDB helper (enables AlloyDB in place of GKE).
- Tests for every guard + user PATCH + GET-:promote rejection + 400 wire error +
  instance output aliasing.
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.

1 participant