func()

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