Azure / Azure/bicep

Forwarding outputs from one module to another skips unwrapping/transformation step

Open
#16,792 0 comments 0 reactions 0 assignees View on GitHub
Quality Sprint
Dominant language
Bicep
Stars
3.6k
Forks
830
Avg merge
1d 21m
Merged PRs (30d)
79

Description

Normally, Bicep compiles module output access to include a `.value` property access since outputs have a wrapper object in the response from ARM. E.g., `mod.outputs.foo` is compiled into an ARM expression like `[reference('mod').outputs.foo.value]`. When outputting the entire `outputs` property of a module, however, this step is skipped. For example, in the following template:

```bicep
module mod ...

output modOutputs object = mod.outputs
```

The `modOutputs` output will have a value like `{ foo: { type: 'String', value: < foo value > }, bar: { type: 'String', value: < bar value > } }` instead of the expected `{foo: , bar: }`.

Bicep doesn't raise any diagnostics since both match the declared output type of `object`. It would be helpful if Bicep handled this automatically, or at least raised a diagnostic with a code fix that replaced `mod.outputs` with something like `toObject(items(mod.outputs), x => x.key, x => x.value.value)`.

Originally reported by @stewartadam in #16745:

> I saw the validation failure due to lack of type inference when I tried to refactor a module which deployed a couple resources and had many flattened properties as outputs, such as:
>
> ```
> output fooName string = ...
> output fooResourceId string = ...
> // etc
> output barName string = ...
> output barResourceId string = ...
> // etc
> ```
>
> into objects of related properties:
> ```
> output foo object: {
> name: ...
> resourceId: ...
> }
>
> output bar object: {
> name: ...
> resourceId: ...
> }
> ```
>
> After doing so, in `main.bicep` when I called this module completion stopped working for `module.outputs.foo` and attempting to access a property would produce a template validation failure, even if the object property's type when resolved was correct.
>
> Context on my particular use case: I'm using the AVM Bicep registry and don't want to manually flatten and re-define each output from the individual resources as a top-level outputs for the module; it makes more sense to pass the AVM resource outputs up as module outputs so that the caller of my module can easily lookup the naming for output fields and leverage the well-designed AVM outputs.
>
> Here is the specific bits of code that triggred the validation failure:
>
> monitoring.bicep
>
> param logAnalyticsName string
> param applicationInsightsName string
> param location string = resourceGroup().location
> param tags object = {}
> @allowed(['Enabled', 'Disabled'])
> param publicNetworkAccess string = 'Enabled'
>
> module logAnalytics 'br/public:avm/res/operational-insights/workspace:0.11.1' = {
> name: 'monitoringLogAnalytics'
> params: {
> name: logAnalyticsName
> location: location
> tags: tags
> skuName: 'PerGB2018'
> dataRetention: 30
> publicNetworkAccessForIngestion: publicNetworkAccess
> publicNetworkAccessForQuery: publicNetworkAccess
> features: {
> enableLogAccessUsingOnlyResourcePermissions: true
> }
> }
> }
>
> module applicationInsights 'br/public:avm/res/insights/component:0.6.0' = {
> name: 'monitoringAppInsights'
> params: {
> name: applicationInsightsName
> location: location
> tags: tags
> workspaceResourceId: logAnalytics.outputs.resourceId
> publicNetworkAccessForIngestion: publicNetworkAccess
> publicNetworkAccessForQuery: publicNetworkAccess
> }
> }
>
> output logAnalyticsAvmOutputs object = logAnalytics.outputs
> output appInsightsAvmOutputs object = applicationInsights.outputs
>
>
>
> main.bicep
>
> module managedEnvironment 'br/public:avm/res/app/managed-environment:0.10.1' = {
> name: 'managedEnvironmentDeployment'
> params: {
> // This fails with a validation error claiming it received type 'object' even though logAnalyticsWorkspaceId property is a string
> // it fails during validation, not deployment - so a deployment is not even attempted
> logAnalyticsWorkspaceResourceId: monitoring.outputs.logAnalytics.logAnalyticsWorkspaceId
> name: managedEnvironmentName
> // Non-required parameters
> dockerBridgeCidr: ''
> infrastructureResourceGroupName: resourceGroup().name
> infrastructureSubnetId: ''
> internal: false
> platformReservedCidr: ''
> platformReservedDnsIP: ''
> publicNetworkAccess: 'Enabled'
> workloadProfiles: []
> zoneRedundant: false
> }
> }
>
>
>
> If this is as-design, then the workaround of user-defined types requires end-users re-implement type definitions matching the output structure of the underlying AVM modules, then manually map each property one-by-one from the AVM outputs to the user-defined type fields... just avoid a validation error that wouldn't fail at runtime and get completions working on outputs:
>
> ```
> @export()
> type logAnalyticsOutputs = {
> resourceId: string
> resourceGroupName: string
> name: string
> logAnalyticsWorkspaceId: string
> location: string
> systemAssignedMIPrincipalId: string?
> }
>
> @export()
> type applicationInsightsOutputs = {
> name: string
> resourceId: string
> resourceGroupName: string
> applicationId: string
> location: string
> instrumentationKey: string
> connectionString: string
> }
>
> // Instead of assigning 'output foo = foo.outputs' directly, we have to manually
> // map property-by-property for now - see Azure/bicep#13947
> output applicationInsights applicationInsightsOutputs = {
> name: applicationInsights.outputs.name
> resourceId: applicationInsights.outputs.resourceId
> resourceGroupName: applicationInsights.outputs.resourceGroupName
> applicationId: applicationInsights.outputs.applicationId
> location: applicationInsights.outputs.location
> instrumentationKey: applicationInsights.outputs.instrumentationKey
> connectionString: applicationInsights.outputs.connectionString
> }
> output logAnalytics logAnalyticsOutputs = {
> resourceId: logAnalytics.outputs.resourceId
> resourceGroupName: logAnalytics.outputs.resourceGroupName
> name: logAnalytics.outputs.name
> logAnalyticsWorkspaceId: logAnalytics.outputs.logAnalyticsWorkspaceId
> location: logAnalytics.outputs.location
> systemAssignedMIPrincipalId: logAnalytics.outputs.?systemAssignedMIPrincipalId
> }
> ```
>
> As noted in the comment, even if I were to manually re-define the output types and try to keep that in sync with the AVM outputs, I still can't do a simple `output fooResource ResourceOutputType = avmResource.outputs`. I have to manually map each field one-by-one.
>
> Putting myself in an end-user's shoes, Bicep should be already aware of the types (since it knows the object property names and types within the child module, just not when they're passed up). Re-implementing user-defined types for object outputs is brittle and a needless waste of time for end-users.

_Originally posted by @stewartadam in [#16745](https://github.com/Azure/bicep/issues/16745#issuecomment-2767126092)_

Contributor guide

Open the contributing guide

Research direction

Reproduce the behavior using the monitoring.bicep and main.bicep examples, focusing on assignments of module.outputs and subsequent property access. Compare the generated output shape with the expected unwrapped object, then trace the compiler path handling whole-module output forwarding. Done means forwarded outputs are transformed correctly or a diagnostic and code fix clearly address the case.

Written by the indexing model from the issue text.

Assessment

Tech stack
azure
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.