Azure / bicep

Bicep is a declarative language for describing and deploying Azure resources
MIT License
3.16k stars 728 forks source link

Unhandled exception. System.NotImplementedException: Cannot emit unexpected expression of type ResourceFunctionCallExpression #14357

Open Meertman opened 1 week ago

Meertman commented 1 week ago

Bicep version Bicep CLI version 0.28.1 (ba1e9f8c1e)

Describe the bug When building the bicep template in which an existing KeyVault is used in order to retrieve secrets that are passed to a module via parameters, an exception with the following message: Unhandled exception. System.NotImplementedException: Cannot emit unexpected expression of type ResourceFunctionCallExpression is thrown.

To Reproduce

  1. Define the module, for example the module in zipped attachment (for a deployment of APIM).
    
    @minLength(1)
    @maxLength(50)
    @description('Optional: The name of the API.')
    param name string

@description('Mandatory: The location of the resource.') param location string

@description('Optional: The URL and SSL certificate of the API management service. If not provided, the API management service will not have any custom URL.') param customUrlInfo urlInfo?

@description('Optional: The URL and SSL certificate of the developer portal of the API management service. If not provided, the developer portal will not have any custom URL.') param developerPortalUrlInfo urlInfo?

var proxyConfigurations = customUrlInfo != null ? [{ type: 'Proxy' hostName: customUrlInfo!.url encodedCertificate: customUrlInfo!.sslInfo.certificate certificatePassword: customUrlInfo!.sslInfo.certificatePassword }] : [] var portalConfigurations = developerPortalUrlInfo != null ? [{ type: 'DeveloperPortal' hostName: developerPortalUrlInfo!.url encodedCertificate: developerPortalUrlInfo!.sslInfo.certificate certificatePassword: developerPortalUrlInfo!.sslInfo.certificatePassword }] : [] var hostnameConfigurations = union(proxyConfigurations, portalConfigurations)

resource apim 'Microsoft.ApiManagement/service@2022-08-01' = { name: name location: location sku: { name: 'Developer' capacity: 1 } properties: { publisherEmail: 'issue@github.com' publisherName: 'Github' hostnameConfigurations: hostnameConfigurations virtualNetworkType: 'External' disableGateway: false natGatewayState: 'Disabled' publicNetworkAccess: 'Disabled' } }

@description('The name of the API management service (APIM).') output name string = apim.name @description('The identifier of the service principal of the system-assigned managed identity of APIM.') output principalId string = apim.identity.principalId

@export() type urlInfo = { url: string sslInfo: certificateInfo }

type certificateInfo = { @secure() @description('Base64 encoded certificate') certificate: string @secure() certificatePassword: string }


2. Use that module, for example the bicep template in zipped attachment.
```bicep
resource KeyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
#disable-next-line use-stable-resource-identifiers
  name: 'some-existing-key-vault-deployment-name'
}

module APIM './minimal-apim.bicep' = {
  name: 'some-apim-deployment-name'
  params: {
    name: 'some-apim-name'
    location: 'some-location'
    customUrlInfo: {
      url: 'https://api-dev.github.com'
      sslInfo: {
        certificate: KeyVault.getSecret('api-dev.github.com-ssl-certificate')
        certificatePassword: KeyVault.getSecret('api-dev.github.com-ssl-certificate-password')
      }
    }
    developerPortalUrlInfo: {
      url: 'https://apiportal-dev.github.com'
      sslInfo: {
        certificate: KeyVault.getSecret('apiportal-dev.github.com-ssl-certificate')
        certificatePassword: KeyVault.getSecret('apiportal-dev.github.com-ssl-certificate')
      }
    }
  }
}

output apim object = APIM.outputs
  1. Execute the command az bicep build -f .bicep

Additional context Exception with message and complete stacktrace:

Unhandled exception. System.NotImplementedException: Cannot emit unexpected expression of type ResourceFunctionCallExpression
   at Bicep.Core.Emit.ExpressionConverter.ConvertExpression(Expression expression)
   at Bicep.Core.Emit.ExpressionEmitter.EmitLanguageExpression(Expression expression)
   at Bicep.Core.Emit.ExpressionEmitter.EmitExpression(Expression expression)
   at Bicep.Core.Emit.ExpressionEmitter.<>c__DisplayClass22_0.<EmitProperty>b__0()
   at Bicep.Core.Emit.PositionTrackingJsonTextWriter.WritePropertyWithPosition(IPositionable keyPosition, String name, Action valueFunc)
   at Bicep.Core.Emit.ExpressionEmitter.EmitPropertyInternal(LanguageExpression expressionKey, Action valueFunc, IPositionable location, Boolean skipCopyCheck)
   at Bicep.Core.Emit.ExpressionEmitter.EmitProperty(Expression name, Expression expression)
   at Bicep.Core.Emit.ExpressionEmitter.EmitProperty(String name, Expression expression)
   at Bicep.Core.Emit.ExpressionEmitter.EmitObjectProperties(ObjectExpression object)
   at Bicep.Core.Emit.ExpressionEmitter.EmitExpression(Expression expression)
   at Bicep.Core.Emit.ExpressionEmitter.<>c__DisplayClass22_0.<EmitProperty>b__0()
   at Bicep.Core.Emit.PositionTrackingJsonTextWriter.WritePropertyWithPosition(IPositionable keyPosition, String name, Action valueFunc)
   at Bicep.Core.Emit.ExpressionEmitter.EmitPropertyInternal(LanguageExpression expressionKey, Action valueFunc, IPositionable location, Boolean skipCopyCheck)
   at Bicep.Core.Emit.ExpressionEmitter.EmitProperty(Expression name, Expression expression)
   at Bicep.Core.Emit.ExpressionEmitter.EmitProperty(String name, Expression expression)
   at Bicep.Core.Emit.ExpressionEmitter.EmitObjectProperties(ObjectExpression object)
   at Bicep.Core.Emit.ExpressionEmitter.EmitExpression(Expression expression)
   at Bicep.Core.Emit.ExpressionEmitter.<>c__DisplayClass22_0.<EmitProperty>b__0()
   at Bicep.Core.Emit.PositionTrackingJsonTextWriter.WritePropertyWithPosition(IPositionable keyPosition, String name, Action valueFunc)
   at Bicep.Core.Emit.ExpressionEmitter.EmitPropertyInternal(LanguageExpression expressionKey, Action valueFunc, IPositionable location, Boolean skipCopyCheck)
   at Bicep.Core.Emit.ExpressionEmitter.EmitProperty(Expression name, Expression expression)
   at Bicep.Core.Emit.ExpressionEmitter.EmitProperty(String name, Expression expression)
   at Bicep.Core.Emit.ExpressionEmitter.EmitObjectProperties(ObjectExpression object)
   at Bicep.Core.Emit.ExpressionEmitter.EmitExpression(Expression expression)
   at Bicep.Core.Emit.ExpressionEmitter.<>c__DisplayClass22_0.<EmitProperty>b__0()
   at Bicep.Core.Emit.PositionTrackingJsonTextWriter.WritePropertyWithPosition(IPositionable keyPosition, String name, Action valueFunc)
   at Bicep.Core.Emit.ExpressionEmitter.EmitPropertyInternal(LanguageExpression expressionKey, Action valueFunc, IPositionable location, Boolean skipCopyCheck)
   at Bicep.Core.Emit.ExpressionEmitter.EmitProperty(Expression name, Expression expression)
   at Bicep.Core.Emit.ExpressionEmitter.EmitProperty(String name, Expression expression)
   at Bicep.Core.Emit.TemplateWriter.<>c__DisplayClass67_0.<EmitModuleParameters>b__0()
   at Bicep.Core.Emit.PositionTrackingJsonTextWriter.WriteObjectWithPosition(IPositionable sourcePosition, Action propertiesFunc)
   at Bicep.Core.Emit.ExpressionEmitter.EmitObject(Action writePropertiesFunc, IPositionable position)
   at Bicep.Core.Emit.ExpressionEmitter.<>c__DisplayClass32_0.<EmitObjectProperty>b__0()
   at Bicep.Core.Emit.PositionTrackingJsonTextWriter.WritePropertyWithPosition(IPositionable keyPosition, String name, Action valueFunc)
   at Bicep.Core.Emit.ExpressionEmitter.EmitProperty(String propertyName, Action writeValueFunc, IPositionable position)
   at Bicep.Core.Emit.ExpressionEmitter.EmitObjectProperty(String propertyName, Action writePropertiesFunc, IPositionable position)
   at Bicep.Core.Emit.TemplateWriter.EmitModuleParameters(ExpressionEmitter emitter, DeclaredModuleExpression module)
   at Bicep.Core.Emit.TemplateWriter.<>c__DisplayClass68_0.<EmitModule>b__2()
   at Bicep.Core.Emit.PositionTrackingJsonTextWriter.WriteObjectWithPosition(IPositionable sourcePosition, Action propertiesFunc)
   at Bicep.Core.Emit.ExpressionEmitter.EmitObject(Action writePropertiesFunc, IPositionable position)
   at Bicep.Core.Emit.ExpressionEmitter.<>c__DisplayClass32_0.<EmitObjectProperty>b__0()
   at Bicep.Core.Emit.PositionTrackingJsonTextWriter.WritePropertyWithPosition(IPositionable keyPosition, String name, Action valueFunc)
   at Bicep.Core.Emit.ExpressionEmitter.EmitProperty(String propertyName, Action writeValueFunc, IPositionable position)
   at Bicep.Core.Emit.ExpressionEmitter.EmitObjectProperty(String propertyName, Action writePropertiesFunc, IPositionable position)
   at Bicep.Core.Emit.TemplateWriter.<>c__DisplayClass68_0.<EmitModule>b__0()
   at Bicep.Core.Emit.PositionTrackingJsonTextWriter.WriteObjectWithPosition(IPositionable sourcePosition, Action propertiesFunc)
   at Bicep.Core.Emit.ExpressionEmitter.EmitObject(Action writePropertiesFunc, IPositionable position)
   at Bicep.Core.Emit.TemplateWriter.EmitModule(PositionTrackingJsonTextWriter jsonWriter, DeclaredModuleExpression module, ExpressionEmitter emitter)
   at Bicep.Core.Emit.TemplateWriter.<>c__DisplayClass64_0.<EmitResources>b__0()
   at Bicep.Core.Emit.PositionTrackingJsonTextWriter.WriteArrayWithPosition(IPositionable sourcePosition, Action itemsFunc)
   at Bicep.Core.Emit.ExpressionEmitter.EmitArray(Action writeItemsFunc, IPositionable position)
   at Bicep.Core.Emit.ExpressionEmitter.<>c__DisplayClass33_0.<EmitArrayProperty>b__0()
   at Bicep.Core.Emit.PositionTrackingJsonTextWriter.WritePropertyWithPosition(IPositionable keyPosition, String name, Action valueFunc)
   at Bicep.Core.Emit.ExpressionEmitter.EmitProperty(String propertyName, Action writeValueFunc, IPositionable position)
   at Bicep.Core.Emit.ExpressionEmitter.EmitArrayProperty(String propertyName, Action writeItemsFunc, IPositionable position)
   at Bicep.Core.Emit.TemplateWriter.EmitResources(PositionTrackingJsonTextWriter jsonWriter, ExpressionEmitter emitter, ImmutableArray`1 resources, ImmutableArray`1 modules)
   at Bicep.Core.Emit.TemplateWriter.GenerateTemplateWithoutHash(PositionTrackingJsonTextWriter jsonWriter)
   at Bicep.Core.Emit.TemplateWriter.Write(SourceAwareJsonTextWriter writer)
   at Bicep.Core.Emit.TemplateEmitter.<>c__DisplayClass8_0.<Emit>b__0()
   at Bicep.Core.Emit.TemplateEmitter.EmitOrFail(Func`1 write)
   at Bicep.Core.Emit.TemplateEmitter.Emit(TextWriter textWriter)
   at Bicep.Core.Emit.CompilationEmitter.Template(SemanticModel model)
   at Bicep.Core.Emit.CompilationEmitter.Template()
   at Bicep.Cli.Services.OutputWriter.TemplateToFile(Compilation compilation, Uri outputUri)
   at Bicep.Cli.Commands.BuildCommand.RunAsync(BuildArguments args)
   at Bicep.Cli.Program.RunAsync(String[] args, CancellationToken cancellationToken)
   at Bicep.Cli.Program.<>c__DisplayClass3_0.<<Main>b__0>d.MoveNext()
--- End of stack trace from previous location ---
   at Bicep.Cli.Program.RunWithCancellationAsync(Func`2 runFunc)
   at Bicep.Cli.Program.Main(String[] args)
   at Bicep.Cli.Program.<Main>(String[] args)
jeskew commented 1 week ago

The error message could certainly use some improvement/be a diagnostic and not an unhandled exception, but I believe the root cause is using <key vault>.getSecret(...) within a parameter value. This expression can only be used as the complete parameter value due to a limitation of the ARM platform.

Meertman commented 1 week ago

@jeskew, I can confirm that the following work-around does not throw the exception:

Module:

@minLength(1)
@maxLength(50)
@description('Optional: The name of the API.')
param name string

@description('Mandatory: The location of the resource.')
param location string

@description('Optional: A custom URL for the API management service. If not provided, the API management service will not have any custom URL.')
param customUrl string?

@secure()
@description('Optional: A certificate for the custom URL of the API management service. If not provided, the API management service will not have any custom URL.')
param customUrlCertificate string?

@secure()
@description('Optional: The password of the certificate for the custom URL of the API management service. If not provided, the API management service will not have any custom URL.')
param customUrlCertificatePassword string?

@description('Optional: A custom URL of the developer portal of the API management service. If not provided, the developer portal will not have any custom URL.')
param developerPortalUrl string?

@secure()
@description('Optional: A certificate for the custom URL of the developer portal of the API management service. If not provided, the developer portal will not have any custom URL.')
param developerPortalUrlCertificate string?

@secure()
@description('Optional: The password of the certificate for the custom URL of the developer portal of the API management service. If not provided, the developer portal will not have any custom URL.')
param developerPortalUrlCertificatePassword string?

var proxyConfigurations = customUrl != null && customUrlCertificate != null && customUrlCertificatePassword != null ? [{
      type: 'Proxy'
      hostName: customUrl
      encodedCertificate: customUrlCertificate
      certificatePassword: customUrlCertificatePassword
    }] : []
var portalConfigurations = developerPortalUrl != null && developerPortalUrlCertificate != null && developerPortalUrlCertificatePassword != null ? [{
    type: 'DeveloperPortal'
    hostName: developerPortalUrl
    encodedCertificate: developerPortalUrlCertificate
    certificatePassword: developerPortalUrlCertificatePassword
  }] : []
var hostnameConfigurations = union(proxyConfigurations, portalConfigurations)

resource apim 'Microsoft.ApiManagement/service@2022-08-01' = {
  name: name
  location: location
  sku: {
    name: 'Developer'
    capacity: 1
  }
  properties: {
    publisherEmail: 'issue@github.com'
    publisherName: 'Github'
    hostnameConfigurations: hostnameConfigurations
    virtualNetworkType: 'External'
    disableGateway: false
    natGatewayState: 'Disabled'
    publicNetworkAccess: 'Disabled'
  }
}

@description('The name of the API management service (APIM).')
output name string = apim.name
@description('The identifier of the service principal of the system-assigned managed identity of APIM.')
output principalId string = apim.identity.principalId

Usage.

resource KeyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
#disable-next-line use-stable-resource-identifiers
  name: 'some-existing-key-vault-deployment-name'
}

module APIM './minimal-apim.bicep' = {
  name: 'some-apim-deployment-name'
  params: {
    name: 'some-apim-name'
    location: 'some-location'
    customUrl: 'https://api-dev.github.com'
    customUrlCertificate: KeyVault.getSecret('api-dev.github.com-ssl-certificate')
    customUrlCertificatePassword: KeyVault.getSecret('api-dev.github.com-ssl-certificate-password')
    developerPortalUrl: 'https://apiportal-dev.github.com'
    developerPortalUrlCertificate: KeyVault.getSecret('apiportal-dev.github.com-ssl-certificate')
    developerPortalUrlCertificatePassword: KeyVault.getSecret('apiportal-dev.github.com-ssl-certificate')
  }
}

output apim object = APIM.outputs

But it feels less clear from a consumer perspective in using the module. You need to know that you need to provide the multiple parameters.

Meertman commented 1 week ago

Additionaly, I've also found that it is impossible to define multiple custom URLs with a different certificate each in the bicep module, so if you define this in the usage of the original module (supposing that the customUrlInfo is an array):

...
    customUrlInfo: [for customUrl in customUrls : {
      url: customUrl.url
      sslInfo: {
        certificate: KeyVault.getSecret(customUrl.sslInfo.certificateName)
        certificatePassword: KeyVault.getSecret(customUrl.sslInfo.certificatePasswordName)
      }
    }]
...

It would complain that the output of the KeyVault.getSecret method can only be used for properties that have the secure attribute on them and because we are just creating JSON objects here (that match the type definition of urlInfo, but are not instances of that type) the secure attribute is not on them.

Do you know if there is a better way for providing multiple custom URLs to APIM with different certificates?