in cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go [1531:1638]
func (p *BicepProvider) loadParameters(ctx context.Context) (loadParametersResult, error) {
parametersFilename := fmt.Sprintf("%s.parameters.json", p.options.Module)
parametersRoot := p.options.Path
if !filepath.IsAbs(parametersRoot) {
parametersRoot = filepath.Join(p.projectPath, parametersRoot)
}
paramFilePath := filepath.Join(parametersRoot, parametersFilename)
parametersBytes, err := os.ReadFile(paramFilePath)
// if the file does not exist, we return an empty parameters map
// This makes AZD to support deploying bicep modules without parameters file, assuming AZD prompts for all required
// parameters.
if os.IsNotExist(err) {
log.Printf("parameters file %s does not exist, using empty parameters", paramFilePath)
return loadParametersResult{}, nil
}
if err != nil {
return loadParametersResult{}, fmt.Errorf("reading parameters.json: %w", err)
}
principalId, err := p.curPrincipal.CurrentPrincipalId(ctx)
if err != nil {
return loadParametersResult{}, fmt.Errorf("fetching current principal id: %w", err)
}
var decodedParamsFile azure.ArmParameterFile
if err := json.Unmarshal(parametersBytes, &decodedParamsFile); err != nil {
return loadParametersResult{}, fmt.Errorf("error unmarshalling Bicep template parameters: %w", err)
}
parametersMappedToAzureLocation := []string{}
resolvedParams := map[string]azure.ArmParameter{}
// resolving each parameter to keep track of the name during the resolution.
// We used to resolve all the file before, supporting env var substitution at any part of the file.
// We want to support substitution only for the parameter value.
// We also need to identify which parameters are mapped to AZURE_LOCATION (if any).
// We also want to exclude parameters mapped to env vars which env var is not set (instead of using empty string).
for paramName, param := range decodedParamsFile.Parameters {
paramBytes, err := json.Marshal(param)
if err != nil {
return loadParametersResult{}, fmt.Errorf("error decoding deployment parameter %s: %w", paramName, err)
}
var hasUnsetEnvVar bool
// envsubst.Eval handles env var substitution and default values like ${VAR=default}
replaced, err := envsubst.Eval(string(paramBytes), func(name string) string {
if name == environment.PrincipalIdEnvVarName {
return principalId
}
if name == environment.LocationEnvVarName {
parametersMappedToAzureLocation = append(parametersMappedToAzureLocation, paramName)
}
if _, isDefined := p.env.LookupEnv(name); !isDefined {
hasUnsetEnvVar = true
}
return p.env.Getenv(name)
})
if err != nil {
return loadParametersResult{}, fmt.Errorf("substituting environment variables for %s: %w", paramName, err)
}
// resolve `secretOrRandomPassword` -> this is a way to ask AZD to generate a password for the user and
// store it in a Key Vault. But if the Key Vault and secret exists, AZD just takes the secret from there.
if cmdsubst.ContainsCommandInvocation(replaced, cmdsubst.SecretOrRandomPasswordCommandName) {
cmdExecutor := cmdsubst.NewSecretOrRandomPasswordExecutor(p.keyvaultService, p.env.GetSubscriptionId())
replaced, err = cmdsubst.Eval(ctx, replaced, cmdExecutor)
if err != nil {
return loadParametersResult{}, fmt.Errorf("substituting command output inside parameter file: %w", err)
}
}
var resolvedParam azure.ArmParameter
if err := json.Unmarshal([]byte(replaced), &resolvedParam); err != nil {
return loadParametersResult{}, fmt.Errorf("error unmarshalling Bicep template parameters: %w", err)
}
if resolvedParam.Value == nil && resolvedParam.KeyVaultReference == nil {
// ignore parameters that are not set
continue
}
if resolvedParam.Value != nil && resolvedParam.KeyVaultReference != nil {
return loadParametersResult{}, fmt.Errorf(
"parameter %s has both a value and a keyvault reference: %w", paramName, err)
}
if resolvedParam.KeyVaultReference != nil {
// parameter defined using a key vault reference. AZD does not validate the key vault reference
// if there is an issue with it, the deployment will fail.
resolvedParams[paramName] = resolvedParam
continue
}
stringValue, isString := resolvedParam.Value.(string)
if !isString {
continue
}
// After previous checks, we know resolvedParam.Value is not nil
if stringValue == "" && hasUnsetEnvVar {
// parameter is empty and has an unset env var
continue
}
// all other cases here represent a valid resolved parameter
resolvedParams[paramName] = resolvedParam
}
return loadParametersResult{
parameters: resolvedParams,
locationParams: parametersMappedToAzureLocation,
}, nil
}