Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cli/azd/.vscode/cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ overrides:
words:
- GHSA
- gjrc
- filename: pkg/apphost/generate.go
words:
- alue
ignorePaths:
- "**/*_test.go"
- "**/mock*.go"
108 changes: 77 additions & 31 deletions cli/azd/pkg/apphost/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,10 @@ func BicepTemplate(name string, manifest *Manifest, options AppHostOptions) (*me
// Note: azd is not checking or validating that Default.Generate and Default.Value are not both set.
// The AppHost prevents this from happening by not allowing both to be set at the same time.
}
if parameter.scope != nil {
metadataType = azure.AzdMetadataTypeResourceGroup
parameterMetadata = "{}"
}
input := genInput{Name: key, Secret: parameter.Secret, Type: parameter.Type, Value: parameterDefaultValue}
parameters = append(parameters, autoGenInput{
genInput: input,
Expand Down Expand Up @@ -677,7 +681,7 @@ func (b *infraGenerator) LoadManifest(m *Manifest) error {
if err := b.addInputParameter(name, comp); err != nil {
return fmt.Errorf("adding bicep parameter from resource %s (%s): %w", name, comp.Type, err)
}
case "azure.bicep.v0":
case "azure.bicep.v0", "azure.bicep.v1":
if err := b.addBicep(name, comp); err != nil {
return fmt.Errorf("adding bicep resource %s: %w", name, err)
}
Expand Down Expand Up @@ -827,7 +831,18 @@ func (b *infraGenerator) addBicep(name string, comp *Resource) error {
stringParams["location"] = "location"
}

b.bicepContext.BicepModules[name] = genBicepModules{Path: *comp.Path, Params: stringParams}
bicepScope := defaultBicepModuleScope
if comp.Scope != nil {
if comp.Scope.ResourceGroup == nil {
return fmt.Errorf("bicep resource %s has a scope without a resource group", name)
}
paramValue, _, err := injectValueForBicepParameter(name, "scope", *comp.Scope.ResourceGroup)
if err != nil {
return err
}
bicepScope = paramValue
}
b.bicepContext.BicepModules[name] = genBicepModules{Path: *comp.Path, Params: stringParams, Scope: bicepScope}
return nil
}

Expand All @@ -846,6 +861,8 @@ const (
knownInjectedValueLogAnalytics string = "resources.outputs.AZURE_LOG_ANALYTICS_WORKSPACE_ID"
knownInjectedValueContainerEnvName string = "resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_NAME"
knownInjectedValueContainerEnvId string = "resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID"

defaultBicepModuleScope string = "rg"
)

// injectValueForBicepParameter checks for aspire-manifest and azd conventions rules for auto injecting values for
Expand Down Expand Up @@ -1138,6 +1155,7 @@ func (b *infraGenerator) addDaprStateStoreComponent(name string) {
var singleQuotedStringRegex = regexp.MustCompile(`'[^']*'`)
var propertyNameRegex = regexp.MustCompile(`'([^']*)':`)
var jsonSimpleKeyRegex = regexp.MustCompile(`"([a-zA-Z0-9]*)":`)
var resourceValueOnlyRefRegex = regexp.MustCompile(`^"{([a-zA-Z0-9\-]+)\.[vV]alue}"$`)

type ingressDetails struct {
// aca ingress definition
Expand Down Expand Up @@ -1284,38 +1302,32 @@ func (b *infraGenerator) Compile() error {

for moduleName, module := range b.bicepContext.BicepModules {
for paramName, paramValue := range module.Params {
// bicep uses ' instead of " for strings, so we need to replace all " with '
singleQuoted := strings.ReplaceAll(paramValue, "\"", "'")

var evaluationError error
evaluatedString := singleQuotedStringRegex.ReplaceAllStringFunc(singleQuoted, func(s string) string {
evaluatedString, err := EvalString(s, func(s string) (string, error) {
return b.evalBindingRef(s, inputEmitTypeBicep)
})
if err != nil {
evaluationError = fmt.Errorf("evaluating bicep module %s parameter %s: %w", moduleName, paramName, err)
value, err := b.resolveBicepReference(paramValue)
if err != nil {
return fmt.Errorf(
"resolving bicep module %s, param: %s, reference %s: %w", moduleName, paramName, paramValue, err)
}
module.Params[paramName] = value
}
if module.Scope != defaultBicepModuleScope {
rgScope := "resourceGroup"
if matches := resourceValueOnlyRefRegex.FindStringSubmatch(module.Scope); len(matches) == 2 {
manifestResourceName := matches[1]
// Check if the scope is an input
input, hasInput := b.bicepContext.InputParameters[manifestResourceName]
if hasInput {
input.scope = &rgScope
b.bicepContext.InputParameters[manifestResourceName] = input
}
return evaluatedString
})
if evaluationError != nil {
return evaluationError
}

// quick check to know if evaluatedString is only holding one only reference. If so, we don't need to use
// the form of '%{ref}' and we can directly use ref alone.
if isComplexExp, val := isComplexExpression(evaluatedString); !isComplexExp {
module.Params[paramName] = val
continue
scope, err := b.resolveBicepReference(module.Scope)
if err != nil {
return fmt.Errorf("resolving bicep module %s scope %s: %w", moduleName, module.Scope, err)
}

// Property names that are valid identifiers should be declared without quotation marks and accessed
// using dot notation.
evaluatedString = propertyNameRegex.ReplaceAllString(evaluatedString, "${1}:")

// restore double {{ }} to single { } for bicep output
// we used double only during the evaluation to scape single brackets
module.Params[paramName] = strings.ReplaceAll(strings.ReplaceAll(evaluatedString, "'{{", "${"), "}}'", "}")
scope = fmt.Sprintf("%s(%s)", rgScope, scope)
module.Scope = scope
}
b.bicepContext.BicepModules[moduleName] = module
}

for _, kv := range b.bicepContext.KeyVaults {
Expand All @@ -1328,6 +1340,40 @@ func (b *infraGenerator) Compile() error {
return nil
}

// resolve a reference for bicep types. The reference can be from a parameter or from the scope
func (b *infraGenerator) resolveBicepReference(ref string) (string, error) {
// bicep uses ' instead of " for strings, so we need to replace all " with '
singleQuoted := strings.ReplaceAll(ref, "\"", "'")

var evaluationError error
evaluatedString := singleQuotedStringRegex.ReplaceAllStringFunc(singleQuoted, func(s string) string {
evaluatedString, err := EvalString(s, func(s string) (string, error) {
return b.evalBindingRef(s, inputEmitTypeBicep)
})
if err != nil {
evaluationError = err
}
return evaluatedString
})
if evaluationError != nil {
return "", evaluationError
}

// quick check to know if evaluatedString is only holding one only reference. If so, we don't need to use
// the form of '%{ref}' and we can directly use ref alone.
if isComplexExp, val := isComplexExpression(evaluatedString); !isComplexExp {
return val, nil
}

// Property names that are valid identifiers should be declared without quotation marks and accessed
// using dot notation.
evaluatedString = propertyNameRegex.ReplaceAllString(evaluatedString, "${1}:")

// restore double {{ }} to single { } for bicep output
// we used double only during the evaluation to scape single brackets
return strings.ReplaceAll(strings.ReplaceAll(evaluatedString, "'{{", "${"), "}}'", "}"), nil
}

// isComplexExpression checks if the evaluatedString is in the form of '{{ expr }}' or if it is a complex expression like
// 'foo {{ expr }} bar {{ expr2 }}' and returns true if it is a complex expression.
// When the expression is not complex, it returns false and the evaluatedString without the special characters.
Expand Down Expand Up @@ -1561,7 +1607,7 @@ func (b infraGenerator) evalBindingRef(v string, emitType inputEmitType) (string
fmt.Errorf("malformed binding expression, expected "+
"bindings.<binding-name>.[scheme|protocol|transport|external|host|targetPort|port|url] but was: %s", v)
}
case targetType == "azure.bicep.v0":
case targetType == "azure.bicep.v0" || targetType == "azure.bicep.v1":
if !strings.HasPrefix(prop, "outputs.") && !strings.HasPrefix(prop, "secretOutputs") {
return "", fmt.Errorf("unsupported property referenced in binding expression: %s for %s", prop, targetType)
}
Expand Down
1 change: 1 addition & 0 deletions cli/azd/pkg/apphost/generate_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ type genOutputParameter struct {
type genBicepModules struct {
Path string
Params map[string]string
Scope string
}

type genBicepTemplateContext struct {
Expand Down
17 changes: 14 additions & 3 deletions cli/azd/pkg/apphost/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@ type Resource struct {

// project.v1 and container.v1 uses deployment when the AppHost owns the ACA bicep definitions.
Deployment *DeploymentMetadata `json:"deployment,omitempty"`

// Present on bicep modules to control the scope of the module.
Scope *BicepModuleScope `json:"scope,omitempty"`
}

// BicepModuleScope is the scope of a bicep module.
type BicepModuleScope struct {
ResourceGroup *string `json:"resourceGroup,omitempty"`
}

type DeploymentMetadata struct {
Expand Down Expand Up @@ -182,6 +190,9 @@ type Input struct {
Type string `json:"type"`
Secret bool `json:"secret"`
Default *InputDefault `json:"default,omitempty"`
// When the input is used to set a bicep module scope, the scope is set here.
// This allows generation to add azdMetadata to the bicep parameter.
scope *string
}

type InputDefaultGenerate struct {
Expand Down Expand Up @@ -242,7 +253,7 @@ func ManifestFromAppHost(

for resourceName, res := range manifest.Resources {
if res.Path != nil {
if res.Type == "azure.bicep.v0" {
if res.Type == "azure.bicep.v0" || res.Type == "azure.bicep.v1" {
e := manifest.BicepFiles.MkdirAll(resourceName, osutil.PermissionDirectory)
if e != nil {
return nil, e
Expand Down Expand Up @@ -271,9 +282,9 @@ func ManifestFromAppHost(
}

if res.Deployment != nil {
if res.Deployment.Type != "azure.bicep.v0" {
if res.Deployment.Type != "azure.bicep.v0" && res.Deployment.Type != "azure.bicep.v1" {
return nil, fmt.Errorf(
"unexpected deployment type %q. Supported types: [azure.bicep.v0]", res.Deployment.Type)
"unexpected deployment type %q. Supported types: [azure.bicep.v0, azure.bicep.v1]", res.Deployment.Type)
}
// use a folder with the name of the resource
e := manifest.BicepFiles.MkdirAll(resourceName, osutil.PermissionDirectory)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ param administrator_login string
@secure()
param administratorLoginPassword string
param parameter string
@metadata({azd: {
type: 'resourceGroup'
config: {}
}
})
param rg_scope string

var tags = {
'azd-env-name': environmentName
Expand Down Expand Up @@ -85,7 +91,7 @@ module sql 'sql/aspire.hosting.azure.bicep.sql.bicep' = {
}
module test 'test/test.bicep' = {
name: 'test'
scope: rg
scope: resourceGroup(rg_scope)
params: {
host: 'frontend.internal.${resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}'
location: location
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
"parameter": {
"value": "${AZURE_PARAMETER}"
},
"rg_scope": {
"value": "${AZURE_RG_SCOPE}"
},
"environmentName": {
"value": "${AZURE_ENV_NAME}"
},
Expand Down
13 changes: 12 additions & 1 deletion cli/azd/pkg/apphost/testdata/aspire-bicep.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,16 @@
}
}
},
"rg-scope": {
"type": "parameter.v0",
"value": "{rg-scope.inputs.value}",
"inputs": {
"value": {
}
}
},
"test": {
"type": "azure.bicep.v0",
"type": "azure.bicep.v1",
"path": "test.bicep",
"params": {
"test": "{parameter.value}",
Expand All @@ -19,6 +27,9 @@
"one",
"two"
]
},
"scope": {
"resourceGroup": "{rg-scope.value}"
}
},
"administrator-login": {
Expand Down
1 change: 1 addition & 0 deletions cli/azd/pkg/azure/arm_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ const AzdMetadataTypeLocation AzdMetadataType = "location"
const AzdMetadataTypeGenerate AzdMetadataType = "generate"
const AzdMetadataTypeGenerateOrManual AzdMetadataType = "generateOrManual"
const AzdMetadataTypeNeedForDeploy AzdMetadataType = "needForDeploy"
const AzdMetadataTypeResourceGroup AzdMetadataType = "resourceGroup"

type AzdMetadata struct {
Type *AzdMetadataType `json:"type,omitempty"`
Expand Down
2 changes: 1 addition & 1 deletion cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ func (p *BicepProvider) EnsureEnv(ctx context.Context) error {

if scope == azure.DeploymentScopeResourceGroup {
if p.env.Getenv(environment.ResourceGroupEnvVarName) == "" {
rgName, err := p.prompters.PromptResourceGroup(ctx)
rgName, err := p.prompters.PromptResourceGroup(ctx, prompt.PromptResourceOptions{})
if err != nil {
return err
}
Expand Down
14 changes: 14 additions & 0 deletions cli/azd/pkg/infra/provisioning/bicep/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/input"
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/azure/azure-dev/cli/azd/pkg/password"
"github.com/azure/azure-dev/cli/azd/pkg/prompt"

"github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning"
)
Expand Down Expand Up @@ -102,6 +103,19 @@ func (p *BicepProvider) promptForParameter(
return nil, err
}
value = location
} else if paramType == provisioning.ParameterTypeString &&
azdMetadata.Type != nil &&
*azdMetadata.Type == azure.AzdMetadataTypeResourceGroup {

p.console.Message(ctx, fmt.Sprintf(
"Parameter %s requires an %s resource group.", output.WithUnderline("%s", key), output.WithBold("existing")))
rgName, err := p.prompters.PromptResourceGroup(ctx, prompt.PromptResourceOptions{
DisableCreateNew: true,
})
if err != nil {
return nil, err
}
value = rgName
} else if paramType == provisioning.ParameterTypeString &&
azdMetadata.Type != nil &&
*azdMetadata.Type == azure.AzdMetadataTypeGenerateOrManual {
Expand Down
Loading