diff --git a/cli/azd/pkg/azapi/stack_deployments.go b/cli/azd/pkg/azapi/stack_deployments.go index 8f66e1a3d1b..5cb586e155f 100644 --- a/cli/azd/pkg/azapi/stack_deployments.go +++ b/cli/azd/pkg/azapi/stack_deployments.go @@ -258,12 +258,7 @@ func (d *StackDeployments) DeployToSubscription( clonedTags := maps.Clone(tags) clonedTags[azure.TagKeyAzdDeploymentTemplateHashName] = &templateHash - stackParams := map[string]*armdeploymentstacks.DeploymentParameter{} - for k, v := range parameters { - stackParams[k] = &armdeploymentstacks.DeploymentParameter{ - Value: v.Value, - } - } + stackParams := convertToStackParams(parameters) deploymentStackOptions, err := parseDeploymentStackOptions(options) if err != nil { @@ -321,12 +316,7 @@ func (d *StackDeployments) DeployToResourceGroup( clonedTags := maps.Clone(tags) clonedTags[azure.TagKeyAzdDeploymentTemplateHashName] = &templateHash - stackParams := map[string]*armdeploymentstacks.DeploymentParameter{} - for k, v := range parameters { - stackParams[k] = &armdeploymentstacks.DeploymentParameter{ - Value: v.Value, - } - } + stackParams := convertToStackParams(parameters) deploymentStackOptions, err := parseDeploymentStackOptions(options) if err != nil { @@ -754,3 +744,26 @@ func convertFromStacksProvisioningState( return DeploymentProvisioningState("") } + +// convertToStackParams converts the given ARM parameters to deployment stack parameters +func convertToStackParams(parameters azure.ArmParameters) map[string]*armdeploymentstacks.DeploymentParameter { + stackParams := map[string]*armdeploymentstacks.DeploymentParameter{} + for k, v := range parameters { + if v.KeyVaultReference != nil { + stackParams[k] = &armdeploymentstacks.DeploymentParameter{ + Reference: &armdeploymentstacks.KeyVaultParameterReference{ + KeyVault: &armdeploymentstacks.KeyVaultReference{ + ID: &v.KeyVaultReference.KeyVault.ID, + }, + SecretName: &v.KeyVaultReference.SecretName, + SecretVersion: &v.KeyVaultReference.SecretVersion, + }, + } + } else { + stackParams[k] = &armdeploymentstacks.DeploymentParameter{ + Value: v.Value, + } + } + } + return stackParams +} diff --git a/cli/azd/pkg/azure/arm_parameters.go b/cli/azd/pkg/azure/arm_parameters.go index 773c5a9deeb..5e7724a00d4 100644 --- a/cli/azd/pkg/azure/arm_parameters.go +++ b/cli/azd/pkg/azure/arm_parameters.go @@ -4,7 +4,7 @@ package azure // ArmParameters is a map of arm template parameters to their configured values. -type ArmParameters map[string]ArmParameterValue +type ArmParameters map[string]ArmParameter // ArmParametersFile is the model type for a `.parameters.json` file. It fits the schema outlined here: // https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json @@ -14,7 +14,20 @@ type ArmParameterFile struct { Parameters ArmParameters `json:"parameters"` } -// ArmParameterValue wraps the configured value for the parameter. -type ArmParameterValue struct { - Value any `json:"value"` +// ArmParameter wraps the configured value or KV reference for the parameter. +type ArmParameter struct { + Value any `json:"value"` + KeyVaultReference *KeyVaultParameterReference `json:"reference"` +} + +// KeyVaultParameterReference is the model type for a Key Vault parameter reference. +type KeyVaultParameterReference struct { + KeyVault KeyVaultReference `json:"keyVault"` + SecretName string `json:"secretName"` + SecretVersion string `json:"secretVersion"` +} + +// KeyVaultReference represents the Key Vault resource ID. +type KeyVaultReference struct { + ID string `json:"id"` } diff --git a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go index 0dc52493283..738983d896d 100644 --- a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go +++ b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go @@ -1483,7 +1483,7 @@ func (p *BicepProvider) createOutputParameters( // loadParameters reads the parameters file template for environment/module specified by Options, // doing environment and command substitutions, and returns the values. -func (p *BicepProvider) loadParameters(ctx context.Context) (map[string]azure.ArmParameterValue, error) { +func (p *BicepProvider) loadParameters(ctx context.Context) (map[string]azure.ArmParameter, error) { parametersFilename := fmt.Sprintf("%s.parameters.json", p.options.Module) parametersRoot := p.options.Path @@ -1761,7 +1761,7 @@ func (p *BicepProvider) modulePath() string { // whether new inputs were written, and an error if any occurred during the generation of input values. func inputsParameter( existingInputs map[string]map[string]any, autoGenParameters map[string]map[string]azure.AutoGenInput) ( - inputsParameter azure.ArmParameterValue, inputsUpdated bool, err error) { + inputsParameter azure.ArmParameter, inputsUpdated bool, err error) { wroteNewInput := false for inputResource, inputResourceInfo := range autoGenParameters { @@ -1793,7 +1793,7 @@ func inputsParameter( existingInputs[inputResource] = existingRecordsForResource } - return azure.ArmParameterValue{ + return azure.ArmParameter{ Value: existingInputs, }, wroteNewInput, nil } @@ -1830,8 +1830,15 @@ func (p *BicepProvider) ensureParameters( // If a value is explicitly configured via a parameters file, use it. // unless the parameter value inference is nil/empty if v, has := parameters[key]; has { - paramValue := armParameterFileValue(parameterType, v.Value, param.DefaultValue) + // Directly pass through Key Vault references without prompting. + if v.KeyVaultReference != nil { + configuredParameters[key] = azure.ArmParameter{ + KeyVaultReference: v.KeyVaultReference, + } + continue + } + paramValue := armParameterFileValue(parameterType, v.Value, param.DefaultValue) if paramValue != nil { needForDeployParameter := hasMetadata && azdMetadata.Type != nil && @@ -1844,7 +1851,7 @@ func (p *BicepProvider) ensureParameters( paramValue = defValue } } - configuredParameters[key] = azure.ArmParameterValue{ + configuredParameters[key] = azure.ArmParameter{ Value: paramValue, } if needForDeployParameter { @@ -1871,7 +1878,7 @@ func (p *BicepProvider) ensureParameters( if v, has := p.env.Config.Get(configKey); has { if isValueAssignableToParameterType(parameterType, v) { - configuredParameters[key] = azure.ArmParameterValue{ + configuredParameters[key] = azure.ArmParameter{ Value: v, } continue @@ -1892,7 +1899,7 @@ func (p *BicepProvider) ensureParameters( if err != nil { return nil, err } - configuredParameters[key] = azure.ArmParameterValue{ + configuredParameters[key] = azure.ArmParameter{ Value: genValue, } mustSetParamAsConfig(key, genValue, p.env.Config, param.Secure()) @@ -1930,7 +1937,7 @@ func (p *BicepProvider) ensureParameters( value := values[prompt.key] mustSetParamAsConfig(key, value, p.env.Config, prompt.param.Secure()) configModified = true - configuredParameters[key] = azure.ArmParameterValue{ + configuredParameters[key] = azure.ArmParameter{ Value: value, } } @@ -1946,7 +1953,7 @@ func (p *BicepProvider) ensureParameters( mustSetParamAsConfig(key, value, p.env.Config, prompt.param.Secure()) configModified = true - configuredParameters[key] = azure.ArmParameterValue{ + configuredParameters[key] = azure.ArmParameter{ Value: value, } } diff --git a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider_test.go b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider_test.go index 08928cf3fdb..d6c322d05bc 100644 --- a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider_test.go +++ b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider_test.go @@ -63,6 +63,24 @@ func TestBicepPlan(t *testing.T) { ) } +func TestBicepPlanKeyVaultRef(t *testing.T) { + mockContext := mocks.NewMockContext(context.Background()) + prepareBicepMocks(mockContext) + infraProvider := createBicepProvider(t, mockContext) + + deploymentPlan, err := infraProvider.plan(*mockContext.Context) + + require.Nil(t, err) + + require.IsType(t, &deploymentDetails{}, deploymentPlan) + configuredParameters := deploymentPlan.CompiledBicep.Parameters + + require.NotEmpty(t, configuredParameters["kvSecret"]) + require.NotNil(t, configuredParameters["kvSecret"].KeyVaultReference) + require.Nil(t, configuredParameters["kvSecret"].Value) + require.Equal(t, "secretName", configuredParameters["kvSecret"].KeyVaultReference.SecretName) +} + const paramsArmJson = `{ "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", "contentVersion": "1.0.0.0", @@ -399,6 +417,7 @@ func prepareBicepMocks( Parameters: azure.ArmTemplateParameterDefinitions{ "environmentName": {Type: "string"}, "location": {Type: "string"}, + "kvSecret": {Type: "securestring"}, }, Outputs: azure.ArmTemplateOutputs{ "WEBSITE_URL": {Type: "string"}, diff --git a/cli/azd/test/functional/testdata/samples/storage-bicepparam/infra/main.bicep b/cli/azd/test/functional/testdata/samples/storage-bicepparam/infra/main.bicep index 19d4c1cf426..db3e2787590 100644 --- a/cli/azd/test/functional/testdata/samples/storage-bicepparam/infra/main.bicep +++ b/cli/azd/test/functional/testdata/samples/storage-bicepparam/infra/main.bicep @@ -17,6 +17,10 @@ param intTagValue int @description('Test parameter for bool-typed values.') param boolTagValue bool +@secure() +@description('Test secure parameter.') +param kvSecret string + var tags = { 'azd-env-name': environmentName DeleteAfter: deleteAfterTime diff --git a/cli/azd/test/functional/testdata/samples/storage-bicepparam/infra/main.bicepparam b/cli/azd/test/functional/testdata/samples/storage-bicepparam/infra/main.bicepparam index 2690036c522..ca04aafa102 100644 --- a/cli/azd/test/functional/testdata/samples/storage-bicepparam/infra/main.bicepparam +++ b/cli/azd/test/functional/testdata/samples/storage-bicepparam/infra/main.bicepparam @@ -4,3 +4,4 @@ param environmentName = readEnvironmentVariable('AZURE_ENV_NAME') param location = readEnvironmentVariable('AZURE_LOCATION') param intTagValue = int(readEnvironmentVariable('INT_TAG_VALUE', '678')) param boolTagValue = bool(readEnvironmentVariable('BOOL_TAG_VALUE', 'false')) +param kvSecret = getSecret('SUBSCRIPTION_ID', 'RESOURCE_GROUP', 'VAULT', 'secretName') diff --git a/cli/azd/test/functional/testdata/samples/webapp/infra/main.bicep b/cli/azd/test/functional/testdata/samples/webapp/infra/main.bicep index 69ad2cfe810..fbc188d37ad 100644 --- a/cli/azd/test/functional/testdata/samples/webapp/infra/main.bicep +++ b/cli/azd/test/functional/testdata/samples/webapp/infra/main.bicep @@ -11,6 +11,9 @@ param location string @description('A time to mark on created resource groups, so they can be cleaned up via an automated process.') param deleteAfterTime string = dateTimeAdd(utcNow('o'), 'PT1H') +@secure() +param kvSecret string + var tags = { 'azd-env-name': environmentName, DeleteAfter: deleteAfterTime } resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { diff --git a/cli/azd/test/functional/testdata/samples/webapp/infra/main.parameters.json b/cli/azd/test/functional/testdata/samples/webapp/infra/main.parameters.json index 8f7787beb16..3e2ef5b733a 100644 --- a/cli/azd/test/functional/testdata/samples/webapp/infra/main.parameters.json +++ b/cli/azd/test/functional/testdata/samples/webapp/infra/main.parameters.json @@ -7,6 +7,14 @@ }, "location": { "value": "${AZURE_LOCATION}" + }, + "kvSecret": { + "reference": { + "keyVault": { + "id": "/subscriptions/SUBSCRIPTION_ID/resourceGroups/RESOURCE_GROUP/providers/Microsoft.KeyVault/vaults/VAULT" + }, + "secretName": "secretName" + } } } } \ No newline at end of file