in cli/azd/pkg/apphost/generate.go [1472:1811]
func (b infraGenerator) evalBindingRef(v string, emitType inputEmitType) (string, error) {
parts := strings.SplitN(v, ".", 2)
if len(parts) != 2 {
return "", fmt.Errorf("malformed binding expression, expected <resourceName>.<propertyPath> but was: %s", v)
}
resource, prop := parts[0], parts[1]
if resource == "" {
// empty resource name means is used for global properties like outputs (currently only outputs is supported)
if !strings.HasPrefix(prop, "outputs.") {
return "", fmt.Errorf("unsupported global property referenced in binding expression: %s", prop)
}
output := prop[len("outputs."):]
return fmt.Sprintf(`{{ .Env.%s }}`, output), nil
}
targetType, ok := b.resourceTypes[resource]
if !ok {
return "", fmt.Errorf("unknown resource referenced in binding expression: %s", resource)
}
if connectionString, has := b.connectionStrings[resource]; has && prop == "connectionString" {
// The connection string can be a expression itself, so we need to evaluate it.
res, err := EvalString(connectionString, func(s string) (string, error) {
return b.evalBindingRef(s, emitType)
})
if err != nil {
return "", fmt.Errorf("evaluating connection string for %s: %w", resource, err)
}
return res, nil
}
if valueString, has := b.valueStrings[resource]; has && prop == "value" {
// The value string can be a expression itself, so we need to evaluate it.
res, err := EvalString(valueString, func(s string) (string, error) {
return b.evalBindingRef(s, emitType)
})
if err != nil {
return "", fmt.Errorf("evaluating value.v0's value string for %s: %w", resource, err)
}
return res, nil
}
if strings.HasPrefix(prop, "inputs.") {
parts := strings.Split(prop[len("inputs."):], ".")
if len(parts) != 1 {
return "", fmt.Errorf("malformed binding expression, expected inputs.<input-name> but was: %s", v)
}
switch emitType {
case inputEmitTypeBicep:
return fmt.Sprintf("${inputs['%s']['%s']}", resource, parts[0]), nil
case inputEmitTypeYaml:
return fmt.Sprintf("{{ index .Inputs `%s` `%s` }}", resource, parts[0]), nil
default:
panic(fmt.Sprintf("unexpected inputEmitType %s", string(emitType)))
}
}
switch targetType {
case "project.v0",
"container.v0",
"container.v1",
"dockerfile.v0",
"project.v1":
if strings.HasPrefix(prop, "containerImage") {
return `{{ .Image }}`, nil
}
if strings.HasPrefix(prop, "containerPort") {
return `{{ containerPort }}`, nil
}
if strings.HasPrefix(prop, "bindMounts.") {
parts := strings.Split(prop[len("bindMounts."):], ".")
if len(parts) != 2 {
return "", fmt.Errorf("malformed binding expression, expected "+
"bindMounts.<index>.<property> but was: %s", v)
}
index, property := parts[0], parts[1]
if property == "storage" {
return fmt.Sprintf(
`{{ .Env.SERVICE_%s_VOLUME_%s_NAME }}`,
scaffold.AlphaSnakeUpper(scaffold.RemoveDotAndDash(resource)),
fmt.Sprintf("BM%s", index)),
nil
}
return "", fmt.Errorf("unsupported property referenced in binding expression: %s for %s", prop, targetType)
}
if strings.HasPrefix(prop, "volumes.") {
parts := strings.Split(prop[len("volumes."):], ".")
if len(parts) != 2 {
return "", fmt.Errorf("malformed binding expression, expected "+
"volumes.<index>.<property> but was: %s", v)
}
index, property := parts[0], parts[1]
if property == "storage" {
// find the name of the volume
// convert index string to integer
indexInt, err := strconv.Atoi(index)
if err != nil {
return "", fmt.Errorf("malformed binding expression, expected "+
"volumes.<index>.<property> but was: %s", v)
}
volName := b.buildContainers[resource].Volumes[indexInt].Name
return fmt.Sprintf(
`{{ .Env.SERVICE_%s_VOLUME_%s_NAME }}`,
scaffold.AlphaSnakeUpper(resource),
scaffold.AlphaSnakeUpper(scaffold.RemoveDotAndDash(volName))),
nil
}
return "", fmt.Errorf("unsupported property referenced in binding expression: %s for %s", prop, targetType)
}
if !strings.HasPrefix(prop, "bindings.") {
return "", fmt.Errorf("unsupported property referenced in binding expression: %s for %s", prop, targetType)
}
parts := strings.Split(prop[len("bindings."):], ".")
if len(parts) != 2 {
return "", fmt.Errorf("malformed binding expression, expected "+
"bindings.<binding-name>.<property> but was: %s", v)
}
var binding *Binding
var has bool
bindingName := parts[0]
bindingProperty := parts[1]
if targetType == "project.v0" || targetType == "project.v1" {
bindings := b.projects[resource].Bindings
binding, has = bindings.Get(bindingName)
} else if targetType == "container.v0" || targetType == "container.v1" || targetType == "dockerfile.v0" {
bindings := b.buildContainers[resource].Bindings
binding, has = bindings.Get(bindingName)
}
if !has {
return "", fmt.Errorf("unknown binding referenced in binding expression: %s for resource %s", parts[0], resource)
}
bindingDetails, exists := b.allServicesIngress[resource]
if !exists {
return "", fmt.Errorf("binding reference to resource %s without ingress", resource)
}
var bindingMappedToMainIngress bool
if slices.Contains(bindingDetails.ingressBindings, bindingName) {
bindingMappedToMainIngress = true
}
hostNameSuffix := func(external bool) string {
var suffix string
switch emitType {
case inputEmitTypeYaml:
suffix = "{{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN }}"
case inputEmitTypeBicep:
suffix = "${resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}"
default:
panic(fmt.Sprintf("unexpected inputEmitType %s", string(emitType)))
}
if !external {
suffix = "internal." + suffix
}
return suffix
}
switch bindingProperty {
case "scheme":
return binding.Scheme, nil
case "protocol":
return binding.Protocol, nil
case "transport":
return binding.Scheme, nil
case "external":
return fmt.Sprintf("%t", binding.External), nil
case "host":
// If the binding is mapped to the main ingress (internal or external) and it is http/https, resolution
// expects full domain name, like `resource.internal.FQDN` or `resource.FQDN`.
if bindingMappedToMainIngress &&
(binding.Scheme == acaIngressSchemaHttp || binding.Scheme == acaIngressSchemaHttps) {
return fmt.Sprintf("%s.%s", resource, hostNameSuffix(binding.External)), nil
}
return resource, nil
case "targetPort":
if binding.TargetPort != nil {
return fmt.Sprintf("%d", *binding.TargetPort), nil
}
return acaTemplatedTargetPort, nil
case "port":
return bindingPort(binding, bindingMappedToMainIngress)
case "url":
var urlFormatString string
if bindingMappedToMainIngress {
urlFormatString = "%s://%s." + hostNameSuffix(binding.External) + "%s"
} else {
urlFormatString = "%s://%s%s"
}
var port string
resolvedPort, err := urlPort(binding, bindingMappedToMainIngress)
if err != nil {
return "", err
}
if resolvedPort != "" {
port = fmt.Sprintf(":%s", resolvedPort)
}
return fmt.Sprintf(urlFormatString, binding.Scheme, resource, port), nil
default:
return "",
fmt.Errorf("malformed binding expression, expected "+
"bindings.<binding-name>.[scheme|protocol|transport|external|host|targetPort|port|url] but was: %s", v)
}
case "azure.bicep.v0", "azure.bicep.v1":
if !strings.HasPrefix(prop, "outputs.") &&
!strings.HasPrefix(prop, "secretOutputs") &&
!strings.HasPrefix(prop, "secrets") {
return "", fmt.Errorf("unsupported property referenced in binding expression: %s for %s", prop, targetType)
}
replaceDash := strings.ReplaceAll(resource, "-", "_")
outputParts := strings.SplitN(prop, ".", 2)
var outputType string
var outputName string
noOutputName := len(outputParts) == 1
if noOutputName {
outputType = outputParts[0]
} else {
outputType = outputParts[0]
outputName = outputParts[1]
}
if outputType == "outputs" {
if emitType == inputEmitTypeYaml {
return fmt.Sprintf("{{ .Env.%s_%s }}", strings.ToUpper(replaceDash), strings.ToUpper(outputName)), nil
}
if emitType == inputEmitTypeBicep {
// using `{{ }}` helps to check if the result of evaluating a string is a complex expression or not.
return fmt.Sprintf("{{%s.outputs.%s}}", replaceDash, outputName), nil
}
return "", fmt.Errorf("unexpected output type %s", string(emitType))
} else if outputType == "secrets" {
// resource.secrets.<secret-name> was introduced after resource.outputs and resource.secretOutputs
// (few releases after).
// It enables Aspire to control the Key Vault used to save secrets.
// Before this, `secretOutputs` would:
// - Create a KeyVault, Assigned read-role for azd-user, output Key Vault Endpoint and resolve the secret during
// deployment
// Now, using `secrets`, Aspire owns creating the Key Vault and assigning the read-role to it.
if emitType == inputEmitTypeYaml {
// Get the ENV VAR Name for the Keyvault URL from the resource.connectionString
kvConnString, hasConString := b.connectionStrings[resource]
if !hasConString {
return "", fmt.Errorf(
"expecting to find connectionString for resource %s because it provides secrets", resource)
}
kvConnString = strings.TrimRight(strings.TrimLeft(kvConnString, "{"), "}")
if hasOutputs := strings.Contains(kvConnString, "outputs."); !hasOutputs {
return "", fmt.Errorf(
"expecting connectionString for resource %s to contains outputs reference", resource)
}
parts := strings.Split(kvConnString, ".outputs.")
if len(parts) != 2 {
return "", fmt.Errorf(
"unexpected connectionString for resource %s. Expecting the form of resource.outputs.name", resource)
}
if parts[0] != resource {
return "", fmt.Errorf(
"expecting to find connectionString for resource %s to auto-referenced itself"+
", like %s.outputs.name. But found: %s",
resource,
resource,
kvConnString,
)
}
envNarName := strings.ToUpper(parts[1])
return fmt.Sprintf(
"{{ secretOutput {{ .Env.%s_%s }}secrets/%s }}",
strings.ToUpper(replaceDash),
envNarName,
outputName), nil
}
if emitType == inputEmitTypeBicep {
return "", fmt.Errorf("secretOutputs not supported as inputs for bicep modules")
}
return "", fmt.Errorf("unexpected output type %s", string(emitType))
} else {
if emitType == inputEmitTypeYaml {
if noOutputName {
return fmt.Sprintf(
"{{ .Env.SERVICE_BINDING_%s_NAME }}",
strings.ToUpper("kv"+uniqueFnvNumber(resource))), nil
}
return fmt.Sprintf(
"{{ secretOutput {{ .Env.SERVICE_BINDING_%s_ENDPOINT }}secrets/%s }}",
strings.ToUpper("kv"+uniqueFnvNumber(resource)),
outputName), nil
}
if emitType == inputEmitTypeBicep {
return "", fmt.Errorf("secretOutputs not supported as inputs for bicep modules")
}
return "", fmt.Errorf("unexpected output type %s", string(emitType))
}
case "parameter.v0":
param := b.bicepContext.InputParameters[resource]
inputType := "parameter"
if param.Secret {
inputType = "securedParameter"
}
replaceDash := strings.ReplaceAll(resource, "-", "_")
switch emitType {
case inputEmitTypeBicep:
return fmt.Sprintf("{{%s}}", replaceDash), nil
case inputEmitTypeYaml:
if param.Default != nil && param.Default.Value != nil {
if param.Secret {
return "", fmt.Errorf("default value for secured parameter %s is not supported", resource)
}
inputType = "parameterWithDefault"
// parameter with default value will either use the default value or the value passed in the environment
return fmt.Sprintf(`{{ %s "%s" "%s"}}`, inputType, replaceDash, *param.Default.Value), nil
}
// parameter without default value
return fmt.Sprintf(`{{ %s "%s" }}`, inputType, replaceDash), nil
default:
panic(fmt.Sprintf("unexpected parameter %s", string(emitType)))
}
default:
ignore, err := strconv.ParseBool(os.Getenv("AZD_DEBUG_DOTNET_APPHOST_IGNORE_UNSUPPORTED_RESOURCES"))
if err == nil && ignore {
log.Printf("ignoring binding reference to resource of type %s since "+
"AZD_DEBUG_DOTNET_APPHOST_IGNORE_UNSUPPORTED_RESOURCES is set", targetType)
return fmt.Sprintf("!!! expression '%s' to type '%s' unsupported by azd !!!", v, targetType), nil
}
return "", fmt.Errorf("unsupported resource type %s referenced in binding expression", targetType)
}
}