Skip to content

fix: Add typed CIDR placeholders for unresolved address prefixes - #3908

Open
Jerome Brown (oWretch) wants to merge 4 commits into
Azure:mainfrom
oWretch:bugfix/cidr-placeholders-unresolved-address-prefixes
Open

Jerome Brown (oWretch) wants to merge 4 commits into
Azure:mainfrom
oWretch:bugfix/cidr-placeholders-unresolved-address-prefixes

Conversation

@oWretch

@oWretch Jerome Brown (oWretch) commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #3907

Problem

When address prefixes are allocated at deployment time — for example by Azure Virtual Network Manager IPAM pools — reference() cannot resolve them during Bicep expansion. The unresolved property came back empty, and the empty value was then passed straight into cidrHost() / cidrSubnet():

cidrSubnet(reference('networkManager::platformPool').addressPrefixes[0], 23, 0)
> The specified CIDR '' is not valid.

cidrHost(reference('vnet::subnet').addressPrefixes[0], 3)
> The specified CIDR '' is not valid.

This makes it impossible to validate otherwise-correct templates that use IPAM-allocated address space.

Approach

Add a source-aware placeholder table in Mock.cs, keyed on resource type + normalized property path. When a property cannot be resolved, expansion now returns a typed mock value appropriate for that property rather than an empty one.

Covered today:

Resource type Property path Placeholder
Microsoft.Network/virtualNetworks addressSpace.addressPrefixes ["192.0.2.0/24"]
Microsoft.Network/virtualNetworks/subnets addressPrefix "192.0.2.0/28"
Microsoft.Network/virtualNetworks/subnets addressPrefixes ["192.0.2.0/28"]
Microsoft.Network/networkManagers/ipamPools addressPrefixes ["192.0.2.0/24"]

Placeholders use RFC 5737 TEST-NET-1 (192.0.2.0/24) so they are obvious in output and cannot collide with real customer address space.

The table is the extension point — other resources with inferable properties (network interfaces, firewalls, DNS resolver endpoints, and so on) can be added later by adding rows, without further changes to the expansion code.

To make this work for both existing and deployed resources, resource type context is now carried through symbol lookup:

  • IDeploymentSymbol gains TryGetResource, implemented by ObjectDeploymentSymbol and ArrayDeploymentSymbol, so symbol-only (existing) resources retain their type.
  • MockResourceObject supplies typed placeholders for missing properties on a known resource while keeping normal object semantics, so partially-known properties objects don't lose resource type context.
  • TemplateContext.TryGetResource keeps the _ResourceIds lookup authoritative (so module/deployment references still return a DeploymentValue), falling back to the symbol-attached resource.

The cidr*() functions stay strict

This deliberately does not relax cidrHost() / cidrSubnet(). Only indexed access into a placeholder array yields a CIDR string — MockResourcePropertyArray remains a MockArray. So genuine authoring mistakes still fail exactly as they do today:

  • passing the wrong property, e.g. reference('vnet').id
  • forgetting the index, e.g. addressPrefixes instead of addressPrefixes[0]
  • singleton addressPrefix vs array addressPrefixes confusion

Additional fix: tryGet() on arrays

While validating against a wider set of real templates, found a related expansion failure with a different shape:

coalesce(
  tryGet(lambdaVariables('env').value, 'addressPrefixes'),
  lambdaVariables('env').value
)

Here env.value may be either an object with an addressPrefixes property, or an array of CIDR strings directly. ARM/Bicep tryGet(array, 'propertyName') should return null for an invalid property lookup so coalesce() falls back to the array, but PSRule threw:

Accessed JArray values with invalid key value: "addressPrefixes". Int32 array index expected.

ExpressionHelpers.TryPropertyOrField treated any non-JValue JToken, including JArray, as a property bag. Excluded JArray from that branch so it returns false for an array + string property, and Functions.TryGet() returns null as intended.

Testing

  • New cases in FunctionTests.cs covering placeholder resolution, existing/deployed symbol resolution, that concrete values are still preferred over placeholders, and tryGet() returning null for an array.
  • Full test suite green: 402 passed, 0 failed.
  • Validated against real templates that previously failed expansion, including one using the tryGet/coalesce shape above; all now expand and evaluate cleanly.

When address prefixes are allocated at deployment time, such as by Azure
Virtual Network Manager IPAM pools, `reference()` cannot resolve them
during Bicep expansion. The empty result was then passed to `cidrHost()`
and `cidrSubnet()`, which failed with "The specified CIDR '' is not valid".

Add a source-aware placeholder table keyed on resource type and normalized
property path, so unresolved properties return a typed mock value instead
of an empty one. Placeholders use RFC 5737 TEST-NET-1 (192.0.2.0/24) so
they are obvious in output and cannot collide with real address space.

Covered today:

- `Microsoft.Network/virtualNetworks` - `addressSpace.addressPrefixes`
- `Microsoft.Network/virtualNetworks/subnets` - `addressPrefix`, `addressPrefixes`
- `Microsoft.Network/networkManagers/ipamPools` - `addressPrefixes`

The table is the extension point, so additional resource properties can be
added without further changes to the expansion code.

The `cidr*()` functions are deliberately left strict. Only indexed access
into a placeholder array yields a CIDR string, so genuine authoring
mistakes, such as passing `id` or an unindexed `addressPrefixes`, still
fail as before.

Fixes Azure#3907

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@oWretch
Jerome Brown (oWretch) requested a review from a team as a code owner September 8, 2026 03:46
`tryGet(array, 'propertyName')` should return null for an invalid
property lookup against an array, matching ARM/Bicep semantics, so a
surrounding `coalesce()` can fall back to another value.

`ExpressionHelpers.TryPropertyOrField` treated any non-JValue JToken,
including JArray, as a property bag and attempted an index lookup by
name, throwing:

  Accessed JArray values with invalid key value: "addressPrefixes".
  Int32 array index expected.

Exclude JArray from that branch so TryPropertyOrField returns false for
an array, and Functions.TryGet returns null as intended.

This was found expanding a template with:

  coalesce(
    tryGet(lambdaVariables('env').value, 'addressPrefixes'),
    lambdaVariables('env').value
  )

where `env.value` can be either an object with an `addressPrefixes`
property, or an array of CIDR strings.

Related to Azure#3907

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

@polatengin Engin Polat (polatengin) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for this fix Jerome Brown (@oWretch).

Keeping the CIDR checks strict makes sense 👍

I found two cases that still give the empty-CIDR error:

  • VNets with IPAM settings inside addressSpace , and Full references to deployed subnets.

The ID fallback also hides real errors, such as division by zero in a resource name.

Could you address these before we merge?

Preserve resource property context through concrete nested objects so VNet
address spaces using IPAM allocations receive typed address prefix
placeholders. Apply the same context to Full references for deployed
resources, including subnets.

Limit resource ID fallback to TemplateSymbolException so genuine
expression evaluation failures, such as division by zero in a resource
name, continue to surface.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@oWretch

Jerome Brown (oWretch) commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor Author

Engin Polat (@polatengin) addressed in 7111cfd:

  • VNet IPAM inside addressSpace: concrete nested objects now retain their resource type and normalized property path, so a missing addressSpace.addressPrefixes resolves to the typed CIDR placeholder even when addressSpace.ipamPoolPrefixAllocations is present.
  • Full references to deployed subnets: full resource mocks now wrap properties with the same source-aware context, so reference(..., 'Full').properties.addressPrefixes[0] resolves correctly.
  • Hidden resource-name errors: narrowed both ID fallbacks from a broad catch to TemplateSymbolException. Expression evaluation errors such as div(1, 0) now propagate instead of being replaced by the symbolic name.

Regression coverage was added for all three cases. Full test suite: 402 passed, 0 failed.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thank for the contribution Jerome Brown (@oWretch) LGTM.

This branch has not been deployed

No deployments
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.

[BUG] cidrHost/cidrSubnet fail during Bicep expansion when address prefixes are assigned outside the template

3 participants