feat(azure): Network gateway, Virtual WAN and Bastion family (7 resource types) - #127
Merged
Conversation
Adds the Virtual WAN container plus its two directly-attached children on Microsoft.Network API version 2023-05-01: - AZURE::Network::VirtualWan - Microsoft.Network/virtualWans - AZURE::Network::VirtualHub - Microsoft.Network/virtualHubs - AZURE::Network::VpnSite - Microsoft.Network/vpnSites All three are LRO on create, update and delete and follow the existing LocalNetworkGateway shape: re-PUT on update, idempotent delete, resume-token Status, and the pending-poller branch pinning the ARM path so an in-flight create cannot orphan the resource. VirtualHub and VpnSite take the WAN by resolvable ARM ID, so a single forma expresses the 1 -> 2 and 1 -> 4 orderings without an explicit dependency. Conformance fixtures: virtual-wan, virtual-hub, vpn-site (each with -update).
- AZURE::Network::VpnGateway - Microsoft.Network/vpnGateways (2023-05-01) - AZURE::Network::BastionHost - Microsoft.Network/bastionHosts (2023-05-01) Both are LRO on create, update and delete and follow the existing LocalNetworkGateway shape, including the pending-poller branch that pins the ARM path while the operation runs - the gateway takes ~30 minutes to create and the Bastion host ~10, so a mis-pinned native ID would orphan a billed resource. VpnGateway is the Virtual-WAN-scoped gateway: it takes its hub by resolvable ARM ID, models bgpSettings as ASN plus peer weight only (Azure allocates the peering addresses out of the hub prefix and they are dropped on read), and models connections with the caller-owned fields only - Azure seeds one vpnLinkConnections entry per remote link and reports status and byte counters, all discarded. sharedKey is write-only and never surfaced. BastionHost validates up front that its subnet is named exactly AzureBastionSubnet, turning a ten-minute ARM rejection into an immediate error. Conformance fixtures: vpn-gateway (rg -> WAN -> hub -> gateway, plus a free VPN site for the connection) and bastion-host (rg -> vnet -> AzureBastionSubnet + Standard static public IP -> host), each with -update.
…Connection - AZURE::Network::VirtualNetworkGateway - Microsoft.Network/virtualNetworkGateways (2023-05-01) - AZURE::Network::VirtualNetworkGatewayConnection - Microsoft.Network/connections (2023-05-01) The classic vnet-scoped gateway family, completing hybrid connectivity alongside the existing LocalNetworkGateway. Both are LRO on create, update and delete and copy the LocalNetworkGateway shape verbatim, including the pending-poller branch that pins the ARM path while the operation runs - a gateway create takes 30-45 minutes, so a mis-pinned native ID would orphan a billed resource for that whole window. VirtualNetworkGateway validates up front that its subnet is named exactly GatewaySubnet and that activeActive comes with two ipConfigurations. bgpSettings models ASN plus peer weight only: Azure allocates the peering addresses out of the GatewaySubnet and they are dropped on read, as is sku.capacity. vpnClientConfiguration covers the point-to-site surface, with radiusServerSecret write-only. VirtualNetworkGatewayConnection sends the two gateway peers as ARM models carrying only their id (ARM types them as whole gateway objects) and reads back only the id, so the inflated bodies ARM returns never reach state. sharedKey and authorizationKey are write-only - ARM does return the shared key on Get and the plugin never surfaces it. connectionType is checked against the peer field it requires, and ipsecPolicies is capped at Azure's one-entry limit. Conformance fixtures: virtual-network-gateway (rg -> vnet -> GatewaySubnet + public IP -> gateway) and virtual-network-gateway-connection (the full site-to-site chain, gateway + LocalNetworkGateway + connection with a custom IPsec/IKE policy), each with -update.
naxty
force-pushed
the
naxty/wave1-plumbing
branch
from
August 28, 2026 14:44
9d658c6 to
61383de
Compare
naxty
force-pushed
the
naxty/wave1-netgw
branch
from
August 28, 2026 14:44
8802dd3 to
00002b9
Compare
…tures The bastion-host conformance fixture omitted ipConfigurations[0].privateIpAllocationMethod. Azure populates it unconditionally, and hasProviderDefault is only honoured on top-level fields, so the value ARM returned read as drift in every comparison phase: Property ipConfigurations[0].privateIpAllocationMethod is not expected and not a provider default (after create / extract / sync / update) The host itself provisioned fine - this was purely a fixture omission, not a provisioning or marshalling bug. Declaring the value is the established fix rather than dropping the field: dns-resolver-inbound-endpoint models the same nested field, spells it out in its fixture, and passes conformance today. Keeping it in the schema also leaves it settable, which Static allocation needs.
The virtual-hub conformance fixture failed on Destroy. Every earlier phase
passed, so the bug was in the delete path. The provider error never reached the
workflow log; the Azure activity log for the failing run has it:
{"status":"Failed","error":{"code":"ResourceOperationFailure","message":"The
resource operation completed with terminal provisioning state 'Failed'.",
"details":[{"code":"InvalidOperation","message":"The specified operation
'DeleteVirtualHub' is not supported. Deletion is not supported when
RoutingStatus on Hub is 'Provisioning'. Retry when state is not
Provisioning."}]}}
A virtual hub has two independent states. `provisioningState` covers the ARM
resource and is what the create/update LRO reports on; `routingState` covers the
hub router behind it, and the service keeps programming that for another ten to
twelve minutes after the LRO says Succeeded. ARM refuses DeleteVirtualHub for
the whole of that window, and the fixture's destroy landed squarely inside it.
Measured on a bare hub in eastus with no gateway attached: create accepted at
T+0, provisioningState Succeeded at T+5m15s, routingState Provisioned only at
T+16m45s, and a delete issued at that point completed in 14m46s.
Delete now reads the hub first and parks the operation when it cannot succeed:
routingState Provisioning, provisioningState Deleting (a delete is already in
flight, this plugin's or the resource group's), or provisioningState Updating (a
write would collide). A parked delete carries the virtual-hub-only
lroOpVirtualHubAwaitRouting operation type and no resume token, and Status
re-evaluates it on the same rules until ARM will take the DELETE, at which point
it hands back a normal lroOpDelete request ID and the operation joins the usual
poller path. Delete and the parked branch share one function, so the two cannot
drift apart. Delete failures now also carry StatusMessage, so the next provider
error of this class shows up without a trip to the activity log.
Local conformance run against subscription ddfe7ebf, one clean run, all phases:
Resource Create Verify Extract Sync Update Replace Destroy OOB Del Duration
WAVE1-NETGW::virtual-hub [+] [+] [+] [+] [+] [~] [+] [+] 68m 50s
1 passed, 0 failed, 0 skipped (68m 50s)
The Destroy step took 27m10s of the 30 minute FORMAE_TEST_TIMEOUT the
virtual-hub arm of scripts/ci/run-conformance-phase.sh grants it: roughly 11
minutes parked waiting for the router, then 15 minutes of delete. That is a pass
with little headroom, and raising that arm to 45 is worth doing separately.
Splits virtual-hub out of the shared virtual-hub|bastion-host arm and raises its per-command timeout from 30 to 45 minutes. virtual-hub's Destroy measured 27m10s against the 30 minute budget - three minutes of headroom, which will flake. The wait is irreducible: ARM refuses DeleteVirtualHub while routingState is still Provisioning, which runs ~11 min past the point the create LRO reports Succeeded, and the delete itself takes ~15 min. The plugin now parks the delete until ARM will accept it rather than failing, so the time is spent waiting instead of erroring. The go-test cap stays at 90: the full lifecycle measured 69 min. bastion-host keeps 30/90 - its lifecycle measured ~57 min with no single phase close to the limit.
naxty
added a commit
that referenced
this pull request
Aug 31, 2026
The 25 resource types from PRs #126, #127 and #128 landed on main with no ongoing test coverage and no documentation. Their pull-request conformance runs proved them once, but nothing guarded them afterwards: none of their fixtures were in the curated matrix, so neither main nor nightly exercised any of them. Matrix: adds the 15 fixtures that passed CRUD *and* discovery on their own PR, which is the bar this file asks for - static-site, virtual-wan, vpn-site, bastion-host and all eleven cosmos-*. 113 active entries becomes 128. Ten fixtures stay out, each with its reason recorded inline: - app-service-plan, web-app, function-app, web-app-slot - the subscription has a dedicated-VM quota of 0 and an App Service plan of any sku runs on dedicated VMs. Verified against both B1 and F1, refused identically. - app-service-certificate, custom-hostname-binding - need a real verified custom domain. - virtual-hub - passes locally end to end (68m50s) but its CRUD phase measures ~59 min, longer than the OIDC client assertion stays valid, so the run dies on AADSTS700024 in the final phase. A credential-lifetime limit, not a plugin bug. - vpn-gateway, virtual-network-gateway, virtual-network-gateway-connection - 60-90 min lifecycles, past the job's timeout-minutes: 120. virtual-hub also joins conformance-pr-skip.txt. It was excluded from the matrix but not from PR scope, so a later PR touching its fixture would have spent an hour to fail on the same expired assertion. README: the resource table was 148 rows against 173 registered types. Adds the 25 missing rows, re-sorted; the table and the schemas now agree exactly.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds the Microsoft.Network gateway / Virtual WAN / Bastion family — 7 resource
types, all on ARM API version
2023-05-01viaarmnetwork/v4. This is the plugin's first hybrid connectivity: before thisthere was no VPN, no Virtual WAN and no Bastion, only the standalone
LocalNetworkGateway.Base is
naxty/wave1-plumbing(PR #125), which carries the seven clients theseresources bind to. No shared file was touched —
pkg/client/client.go,common.go,armid.go,go.mod,README.md,.github/**andpkg/registry/registry.goare all untouched; the resources self-register viainit().Resource types
AZURE::Network::VirtualWanMicrosoft.Network/virtualWansVirtualWansClientAZURE::Network::VirtualHubMicrosoft.Network/virtualHubsVirtualHubsClientAZURE::Network::VpnSiteMicrosoft.Network/vpnSitesVPNSitesClientAZURE::Network::VpnGatewayMicrosoft.Network/vpnGatewaysVPNGatewaysClientAZURE::Network::BastionHostMicrosoft.Network/bastionHostsBastionHostsClientAZURE::Network::VirtualNetworkGatewayMicrosoft.Network/virtualNetworkGatewaysVirtualNetworkGatewaysClientAZURE::Network::VirtualNetworkGatewayConnectionMicrosoft.Network/connectionsVirtualNetworkGatewayConnectionsClientEvery one copies the
NetworkLocalNetworkGatewaytemplate verbatim: afooAPIinterface over the SDK client,
buildPropertiesFromResult,armIDParts,normalizeAzureLocation+canonicalizeEnumon the read path,isDeleteSuccessErrorfor idempotent delete,encodeLROStart/statusLRO/statusDeleteLROfor resume tokens, and the pending-poller branchthat pins the expected ARM path while the operation is in flight. Update is a
re-PUT everywhere (
UpdateTagscannot reach any of the interesting fields).Fixture runtime — please read before adding these to the matrix
These are the slowest resources in the plugin.
FORMAE_TEST_TIMEOUTdefaults to5 minutes, which is not close to enough for four of the seven.
virtual-wanvpn-sitevirtual-hubbastion-hostvpn-gatewayvirtual-network-gatewayvirtual-network-gateway-connectionRecommendation for
.github/conformance-matrix.txt(orchestrator's call, I didnot edit the file):
virtual-wan,vpn-site— free and fast.virtual-hub,bastion-host, in the stylethe Front Door entries already use.
vpn-gateway,virtual-network-gateway,virtual-network-gateway-connection. Suggested wording, matching thecdn-*andcertificateexclusions already in the file:The
conformance-testsjob'stimeout-minutes: 120would also need raising ifvirtual-network-gateway-connectionwere ever added.Correctness notes worth reviewing
Native-ID pinning. The highest-consequence path here. Every write returns
expectedNativeIDon the in-progress branch, built fromconfig.SubscriptionId+ resource group + name against the real ARM providerpath, and each resource has a
PendingCreateReportsRealNativeIDtest assertingthe pending native ID equals the ID ARM actually assigns. A mismatch on a
45-minute gateway create orphans a billed resource for the whole window.
Load-bearing subnet names. Azure only accepts
GatewaySubnetfor aVirtualNetworkGatewayandAzureBastionSubnetfor aBastionHost, andrejects anything else after minutes of provisioning. Both resources check the
subnet ID's last segment before the request goes out and fail with a specific
message; both are documented on the
subnetIdfield in the schema; bothfixtures create the subnet with the exact required name.
lastARMSegmentis asmall shared helper defined in
bastionhost.go.Service-populated values dropped on read. Nested
hasProviderDefaultdoesnot work, so anything Azure assigns inside a nested object is dropped rather
than compared:
bgpSettings.bgpPeeringAddress/.bgpPeeringAddresseson both gateways —Azure allocates these out of the hub prefix / GatewaySubnet.
sku.capacityonVirtualNetworkGateway.ipConfigurations[].privateIPAddress, and the per-childid/etag/type/provisioningStateon every nested list (vpnSiteLinks,connections,ipConfigurations,vpnClientRootCertificates).VpnGateway.connections[].vpnLinkConnections(Azure seeds one per remotelink),
connectionStatus, byte counters.VirtualWan'svirtualHubs/vpnSitesback-references — the hub and siteown their side of the reference.
VirtualHub'sroutingState,virtualRouterAsn,virtualRouterIPsand theazureFirewall/vpnGateway/p2sVpnGateway/bgpConnectionspointers.BastionHost.dnsName.VirtualNetworkGatewayConnection, ARM typesvirtualNetworkGateway1,virtualNetworkGateway2andlocalNetworkGateway2as whole gatewayobjects, not
SubResources, and inflates them on read. The plugin sends onlytheir
idand reads back only theirid.Each is covered by a
Read_drops_service_statesubtest.Secrets.
VirtualNetworkGatewayConnection.sharedKeyand.authorizationKey,VpnGateway.connections[].sharedKeyandVirtualNetworkGateway.vpnClientConfiguration.radiusServerSecretare allwriteOnly = trueand typed(formae.Value|String). ARM does return theconnection's shared key on Get; the read path never emits it, and the tests
assert the literal key text is absent from the properties JSON.
Renamed fields.
VirtualWan.virtualWanTiermaps to ARM'sproperties.type; renamed because the flattened property map already carriesthe resource's own
type. Cross-resource references use the repo's existingfooIdconvention (virtualWanId,virtualHubId,subnetId,publicIpAddressId,virtualNetworkGateway1Id, ...), matchingfirewallPolicyId/publicIPAddressIdonApplicationGateway.createOnlycalls.gatewayType,vpnType,vpnGatewayGeneration,connectionType, all three connection peer fields,virtualWanId,virtualHubId,addressPrefix,VirtualHub.sku,BastionHost.sku,VpnGateway.bgpSettings(ASN is immutable once set),VpnGateway.isRoutingPreferenceInternet, and both gateways'ipConfigurations(ARM cannot swap a front end in place). SKU onVirtualNetworkGatewayis notcreateOnly— resize within a family is a realin-place update, exercised by the unit test.
Deliberately left out
VpnGateway.natRulesandVirtualHubroute tables / hub connections.Separate ARM child types with their own clients; out of scope for this batch.
vpnClientConfigurationin thevirtual-network-gatewayfixture.Point-to-site needs real root-certificate data or a RADIUS secret
(write-only), neither of which the conformance harness round-trips. The full
marshalling path is asserted in
TestVirtualNetworkGateway_CRUD.connections[].sharedKeyin thevpn-gatewayfixture. Write-only nestedinside an array — the harness cannot strip those, the same limitation the
application-gatewayfixture documents forsslCertificates[].data. Azuregenerates a key when it is omitted. The top-level
VirtualNetworkGatewayConnection.sharedKeyis exercised, since top-levelwrite-only fields are handled (as
acr-webhookshows).VirtualWan.allowVnetToVnetTrafficandVirtualHub.allowBranchToBranchTrafficin the fixtures. Both are in theschema, but Virtual WAN does not honour them in the way ARM echoes them back,
so declaring them would compare against a value the service ignores.
connectionType = "ExpressRoute"andpeerIdaremodelled and validated, but no
ExpressRouteCircuitresource type exists yetand creating one requires a provider order, so there is no fixture.
networkAcls,virtualNetwork) — not in thearmnetwork/v4SKU enum.Verification
Run in the worktree on the final commit:
155 resource types = the 148 on the base branch plus these 7. All 14 fixture
files also evaluate cleanly under
pkl eval --project-dir testdata.Conformance was not run: no command in this branch touched real Azure.