Skip to content

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

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

fix: Add typed CIDR placeholders for unresolved address prefixes#3908
Jerome Brown (oWretch) wants to merge 2 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

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>
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

1 participant