From 71d7d469d8dfeb0a32f72bcbb5efb1590fed6f8f Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Wed, 5 Feb 2025 20:17:20 +0000 Subject: [PATCH 1/6] Add `azd add` support for storage accounts --- cli/azd/internal/cmd/add/add_configure.go | 2 + .../internal/cmd/add/add_configure_storage.go | 142 ++++++++++++++++++ cli/azd/internal/cmd/add/add_preview.go | 11 ++ cli/azd/internal/cmd/add/add_select.go | 11 ++ cli/azd/internal/scaffold/scaffold_test.go | 4 +- cli/azd/internal/scaffold/spec.go | 13 ++ cli/azd/pkg/project/resources.go | 24 +++ cli/azd/pkg/project/scaffold_gen.go | 11 ++ .../resources/scaffold/templates/main.bicept | 3 + .../scaffold/templates/resources.bicept | 80 ++++++++++ schemas/alpha/azure.yaml.json | 31 +++- 11 files changed, 329 insertions(+), 3 deletions(-) create mode 100644 cli/azd/internal/cmd/add/add_configure_storage.go diff --git a/cli/azd/internal/cmd/add/add_configure.go b/cli/azd/internal/cmd/add/add_configure.go index de59a5c5f8f..ff7812ec84e 100644 --- a/cli/azd/internal/cmd/add/add_configure.go +++ b/cli/azd/internal/cmd/add/add_configure.go @@ -51,6 +51,8 @@ func Configure( r.Name = "redis" return r, nil + case project.ResourceTypeStorage: + return fillStorageDetails(ctx, r, console, p) default: return r, nil } diff --git a/cli/azd/internal/cmd/add/add_configure_storage.go b/cli/azd/internal/cmd/add/add_configure_storage.go new file mode 100644 index 00000000000..0aff43d0f8c --- /dev/null +++ b/cli/azd/internal/cmd/add/add_configure_storage.go @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package add + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/azure/azure-dev/cli/azd/internal/names" + "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/project" +) + +const ( + StorageDataTypeBlob = "Blobs" +) + +func allStorageDataTypes() []string { + return []string{StorageDataTypeBlob} +} + +func fillStorageDetails( + ctx context.Context, + r *project.ResourceConfig, + console input.Console, + p PromptOptions) (*project.ResourceConfig, error) { + if _, exists := p.PrjConfig.Resources["storage"]; exists { + return nil, fmt.Errorf("only one Storage resource is allowed at this time") + } + + if r.Name == "" { + r.Name = "storage" + } + + modelProps, ok := r.Props.(project.StorageProps) + if !ok { + return nil, fmt.Errorf("invalid resource properties") + } + + selectedDataTypes, err := selectStorageDataTypes(ctx, console) + if err != nil { + return nil, err + } + + selection, err := console.Select(ctx, input.ConsoleOptions{ + Message: "Use keyless authentication?", + Options: []string{ + "Yes (recommended)", + "No", + }, + }) + if err != nil { + return nil, err + } + + modelProps.KeylessAuth = selection == 0 + + for _, option := range selectedDataTypes { + switch option { + case StorageDataTypeBlob: + if err := fillBlobDetails(ctx, console, &modelProps); err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("unsupported data type: %s", option) + } + } + + r.Props = modelProps + return r, nil +} + +func selectStorageDataTypes(ctx context.Context, console input.Console) ([]string, error) { + var selectedDataOptions []string + for { + var err error + selectedDataOptions, err = console.MultiSelect(ctx, input.ConsoleOptions{ + Message: "What type of data do you want to store?", + Options: allStorageDataTypes(), + DefaultValue: []string{StorageDataTypeBlob}, + }) + if err != nil { + return nil, err + } + + if len(selectedDataOptions) == 0 { + console.Message(ctx, output.WithErrorFormat("At least one data type must be selected")) + continue + } + break + } + return selectedDataOptions, nil +} + +func fillBlobDetails(ctx context.Context, console input.Console, modelProps *project.StorageProps) error { + for { + containerName, err := console.Prompt(ctx, input.ConsoleOptions{ + Message: "Input a blob container name to be created:", + Help: "Blob container name\n\n" + + "A blob container organizes a set of blobs, similar to a directory in a file system.", + }) + if err != nil { + return err + } + + if err := validateContainerName(containerName); err != nil { + console.Message(ctx, err.Error()) + continue + } + modelProps.Containers = append(modelProps.Containers, containerName) + break + } + return nil +} + +// validateContainerName validates storage account container names. +// Reference: +// https://learn.microsoft.com/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata +func validateContainerName(name string) error { + if len(name) < 3 { + return errors.New("name must be 3 characters or more") + } + + if strings.Contains(name, "--") { + return errors.New("name cannot contain consecutive hyphens") + } + + if strings.ToLower(name) != name { + return errors.New("name must be all lower case") + } + + err := names.ValidateLabelName(name) + if err != nil { + return err + } + + return nil +} diff --git a/cli/azd/internal/cmd/add/add_preview.go b/cli/azd/internal/cmd/add/add_preview.go index 62f95bfa6c8..6929e06ade5 100644 --- a/cli/azd/internal/cmd/add/add_preview.go +++ b/cli/azd/internal/cmd/add/add_preview.go @@ -67,6 +67,17 @@ func Metadata(r *project.ResourceConfig) resourceMeta { res.UseEnvVars = []string{ "AZURE_OPENAI_ENDPOINT", } + case project.ResourceTypeStorage: + res.AzureResourceType = "Microsoft.Storage/storageAccounts" + res.UseEnvVars = []string{ + "AZURE_STORAGE_ACCOUNT_NAME", + "AZURE_STORAGE_BLOB_ENDPOINT", + } + + if modelProps, ok := r.Props.(project.StorageProps); ok && !modelProps.KeylessAuth { + res.UseEnvVars = append(res.UseEnvVars, "AZURE_STORAGE_ACCOUNT_KEY") + res.UseEnvVars = append(res.UseEnvVars, "AZURE_STORAGE_CONNECTION_STRING") + } } return res } diff --git a/cli/azd/internal/cmd/add/add_select.go b/cli/azd/internal/cmd/add/add_select.go index 9344666ef3d..97ea06bacdc 100644 --- a/cli/azd/internal/cmd/add/add_select.go +++ b/cli/azd/internal/cmd/add/add_select.go @@ -32,6 +32,7 @@ func (a *AddAction) selectMenu() []Menu { {Namespace: "db", Label: "Database", SelectResource: selectDatabase}, {Namespace: "host", Label: "Host service"}, {Namespace: "ai.openai", Label: "Azure OpenAI", SelectResource: a.selectOpenAi}, + {Namespace: "storage", Label: "Storage account", SelectResource: selectStorage}, } } @@ -59,3 +60,13 @@ func selectDatabase( r.Type = resourceTypesDisplayMap[resourceTypesDisplay[dbOption]] return r, nil } + +func selectStorage( + console input.Console, + ctx context.Context, + p PromptOptions) (*project.ResourceConfig, error) { + r := &project.ResourceConfig{} + r.Type = project.ResourceTypeStorage + r.Props = project.StorageProps{} + return r, nil +} diff --git a/cli/azd/internal/scaffold/scaffold_test.go b/cli/azd/internal/scaffold/scaffold_test.go index dcd1c6e5c41..898229219ec 100644 --- a/cli/azd/internal/scaffold/scaffold_test.go +++ b/cli/azd/internal/scaffold/scaffold_test.go @@ -89,7 +89,8 @@ func TestExecInfra(t *testing.T) { DbCosmosMongo: &DatabaseCosmosMongo{ DatabaseName: "appdb", }, - DbRedis: &DatabaseRedis{}, + DbRedis: &DatabaseRedis{}, + StorageAccount: &StorageAccount{}, Services: []ServiceSpec{ { Name: "api", @@ -110,6 +111,7 @@ func TestExecInfra(t *testing.T) { DbPostgres: &DatabaseReference{ DatabaseName: "appdb", }, + StorageAccount: &StorageReference{}, }, { Name: "web", diff --git a/cli/azd/internal/scaffold/spec.go b/cli/azd/internal/scaffold/spec.go index 99aea10e3e0..ee50f6d8008 100644 --- a/cli/azd/internal/scaffold/spec.go +++ b/cli/azd/internal/scaffold/spec.go @@ -17,6 +17,8 @@ type InfraSpec struct { DbCosmosMongo *DatabaseCosmosMongo DbRedis *DatabaseRedis + StorageAccount *StorageAccount + // ai models AIModels []AIModel } @@ -54,6 +56,11 @@ type AIModelModel struct { Version string } +type StorageAccount struct { + Containers []string + KeylessAuth bool +} + type ServiceSpec struct { Name string Port int @@ -71,6 +78,8 @@ type ServiceSpec struct { DbCosmosMongo *DatabaseReference DbRedis *DatabaseReference + StorageAccount *StorageReference + // AI model connections AIModels []AIModelReference } @@ -95,6 +104,10 @@ type AIModelReference struct { Name string } +type StorageReference struct { + KeylessAuth bool +} + func containerAppExistsParameter(serviceName string) Parameter { return Parameter{ Name: BicepName(serviceName) + "Exists", diff --git a/cli/azd/pkg/project/resources.go b/cli/azd/pkg/project/resources.go index 9c1494ec15e..5f31f989e38 100644 --- a/cli/azd/pkg/project/resources.go +++ b/cli/azd/pkg/project/resources.go @@ -18,6 +18,7 @@ func AllResourceTypes() []ResourceType { ResourceTypeDbMongo, ResourceTypeHostContainerApp, ResourceTypeOpenAiModel, + ResourceTypeStorage, } } @@ -27,6 +28,7 @@ const ( ResourceTypeDbMongo ResourceType = "db.mongo" ResourceTypeHostContainerApp ResourceType = "host.containerapp" ResourceTypeOpenAiModel ResourceType = "ai.openai.model" + ResourceTypeStorage ResourceType = "storage" ) func (r ResourceType) String() string { @@ -41,6 +43,8 @@ func (r ResourceType) String() string { return "Container App" case ResourceTypeOpenAiModel: return "Open AI Model" + case ResourceTypeStorage: + return "Storage Account" } return "" @@ -89,6 +93,11 @@ func (r *ResourceConfig) MarshalYAML() (interface{}, error) { if err != nil { return nil, err } + case ResourceTypeStorage: + err := marshalRawProps(raw.Props.(StorageProps)) + if err != nil { + return nil, err + } } return raw, nil @@ -128,6 +137,16 @@ func (r *ResourceConfig) UnmarshalYAML(value *yaml.Node) error { return err } raw.Props = cap + case ResourceTypeStorage: + sp := StorageProps{} + if err := unmarshalProps(&sp); err != nil { + return err + } + // Default KeylessAuth to true if omitted + if _, ok := raw.RawProps["keyless"]; !ok { + sp.KeylessAuth = true + } + raw.Props = sp } *r = ResourceConfig(raw) @@ -155,3 +174,8 @@ type AIModelPropsModel struct { Name string `yaml:"name,omitempty"` Version string `yaml:"version,omitempty"` } + +type StorageProps struct { + Containers []string `yaml:"containers,omitempty"` + KeylessAuth bool `yaml:"keyless,omitempty"` +} diff --git a/cli/azd/pkg/project/scaffold_gen.go b/cli/azd/pkg/project/scaffold_gen.go index 120f1c63211..71b75f2f1f1 100644 --- a/cli/azd/pkg/project/scaffold_gen.go +++ b/cli/azd/pkg/project/scaffold_gen.go @@ -182,6 +182,15 @@ func infraSpec(projectConfig *ProjectConfig) (*scaffold.InfraSpec, error) { Version: props.Model.Version, }, }) + case ResourceTypeStorage: + if infraSpec.StorageAccount != nil { + return nil, fmt.Errorf("only one storage account resource is currently allowed") + } + props := res.Props.(StorageProps) + infraSpec.StorageAccount = &scaffold.StorageAccount{ + Containers: props.Containers, + KeylessAuth: props.KeylessAuth, + } } } @@ -273,6 +282,8 @@ func mapHostUses( backendMapping[use] = res.Name // record the backend -> frontend mapping case ResourceTypeOpenAiModel: svcSpec.AIModels = append(svcSpec.AIModels, scaffold.AIModelReference{Name: use}) + case ResourceTypeStorage: + svcSpec.StorageAccount = &scaffold.StorageReference{KeylessAuth: useRes.Props.(StorageProps).KeylessAuth} } } diff --git a/cli/azd/resources/scaffold/templates/main.bicept b/cli/azd/resources/scaffold/templates/main.bicept index 8ec2feb9f17..16ec886317a 100644 --- a/cli/azd/resources/scaffold/templates/main.bicept +++ b/cli/azd/resources/scaffold/templates/main.bicept @@ -67,4 +67,7 @@ output AZURE_RESOURCE_REDIS_ID string = resources.outputs.AZURE_RESOURCE_REDIS_I {{- if .DbPostgres}} output AZURE_RESOURCE_{{alphaSnakeUpper .DbPostgres.DatabaseName}}_ID string = resources.outputs.AZURE_RESOURCE_{{alphaSnakeUpper .DbPostgres.DatabaseName}}_ID {{- end}} +{{- if .StorageAccount }} +output AZURE_RESOURCE_STORAGE_ID string = resources.outputs.AZURE_RESOURCE_STORAGE_ID +{{- end}} {{ end}} diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index 92d9826a550..df2cc76edd9 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -134,6 +134,51 @@ module postgreServer 'br/public:avm/res/db-for-postgre-sql/flexible-server:0.1.4 } {{- end}} +{{- if .StorageAccount }} +var storageAccountName = '${abbrs.storageStorageAccounts}${resourceToken}' +module storageAccount 'br/public:avm/res/storage/storage-account:0.17.0' = { + name: 'storageAccount' + params: { + name: storageAccountName + publicNetworkAccess: 'Enabled' + blobServices: { + {{- if .StorageAccount.Containers }} + containers: [ + {{- range $index, $element := .StorageAccount.Containers}} + { + name: '{{ $element }}' + } + {{- end}} + ] + {{- end }} + } + location: location + roleAssignments: [ + {{- if .StorageAccount.KeylessAuth }} + {{- range .Services}} + { + principalId: {{bicepName .Name}}Identity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: 'Storage Blob Data Owner' + } + {{- end}} + {{- end}} + ] + networkAcls: { + defaultAction: 'Allow' + } + tags: tags + {{- if (and .Services (not .StorageAccount.KeylessAuth)) }} + secretsExportConfiguration: { + keyVaultResourceId: keyVault.outputs.resourceId + accessKey1Name: 'STORAGE-ACCOUNT-KEY' + connectionString1Name: 'STORAGE-ACCOUNT-CONNECTION-STRING' + } + {{- end}} + } +} +{{end}} + {{- if .AIModels}} var accountName = '${abbrs.cognitiveServicesAccounts}${resourceToken}' module account 'br/public:avm/res/cognitive-services/account:0.7.0' = { @@ -266,6 +311,18 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { keyVaultUrl: '${keyVault.outputs.uri}secrets/REDIS-URL' } {{- end}} + {{- if (and .StorageAccount (not .StorageAccount.KeylessAuth)) }} + { + name: 'storage-key' + identity: {{bicepName .Name}}Identity.outputs.resourceId + keyVaultUrl: '${keyVault.outputs.uri}secrets/STORAGE-ACCOUNT-KEY' + } + { + name: 'storage-connection-string' + identity: {{bicepName .Name}}Identity.outputs.resourceId + keyVaultUrl: '${keyVault.outputs.uri}secrets/STORAGE-ACCOUNT-CONNECTION-STRING' + } + {{- end}} ], map({{bicepName .Name}}Secrets, secret => { name: secret.secretRef @@ -343,6 +400,26 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { secretRef: 'redis-pass' } {{- end}} + {{- if .StorageAccount}} + { + name: 'AZURE_STORAGE_ACCOUNT_NAME' + value: storageAccount.outputs.name + } + { + name: 'AZURE_STORAGE_BLOB_ENDPOINT' + value: storageAccount.outputs.serviceEndpoints.blob + } + {{- if not .StorageAccount.KeylessAuth}} + { + name: 'AZURE_STORAGE_KEY' + secretRef: 'storage-key' + } + { + name: 'AZURE_STORAGE_CONNECTION_STRING' + secretRef: 'storage-connection-string' + } + {{- end}} + {{- end}} {{- if .AIModels}} { name: 'AZURE_OPENAI_ENDPOINT' @@ -464,4 +541,7 @@ output AZURE_RESOURCE_REDIS_ID string = redis.outputs.resourceId {{- if .DbPostgres}} output AZURE_RESOURCE_{{alphaSnakeUpper .DbPostgres.DatabaseName}}_ID string = '${postgreServer.outputs.resourceId}/databases/{{.DbPostgres.DatabaseName}}' {{- end}} +{{- if .StorageAccount }} +output AZURE_RESOURCE_STORAGE_ID string = storageAccount.outputs.resourceId +{{- end}} {{ end}} diff --git a/schemas/alpha/azure.yaml.json b/schemas/alpha/azure.yaml.json index adf9a7e81a5..732eb7c32c5 100644 --- a/schemas/alpha/azure.yaml.json +++ b/schemas/alpha/azure.yaml.json @@ -369,7 +369,8 @@ "db.redis", "db.mongo", "ai.openai.model", - "host.containerapp" + "host.containerapp", + "storage" ] }, "uses": { @@ -386,7 +387,8 @@ { "if": { "properties": { "type": { "const": "ai.openai.model" }}}, "then": { "$ref": "#/definitions/aiModelResource" } }, { "if": { "properties": { "type": { "const": "db.postgres" }}}, "then": { "$ref": "#/definitions/resource"} }, { "if": { "properties": { "type": { "const": "db.redis" }}}, "then": { "$ref": "#/definitions/resource"} }, - { "if": { "properties": { "type": { "const": "db.mongo" }}}, "then": { "$ref": "#/definitions/resource"} } + { "if": { "properties": { "type": { "const": "db.mongo" }}}, "then": { "$ref": "#/definitions/resource"} }, + { "if": { "properties": { "type": { "const": "storage" }}}, "then": { "$ref": "#/definitions/storageAccountResource"} } ] } }, @@ -1314,6 +1316,31 @@ } } } + }, + "storageAccountResource": { + "type": "object", + "description": "A deployed, ready-to-use Azure Storage Account.", + "additionalProperties": false, + "properties": { + "type": true, + "uses": true, + "keyless": { + "type": "boolean", + "title": "Keyless Authentication", + "description": "Indicates whether keyless authentication is used for Azure Storage Account.", + "default": true + }, + "containers": { + "type": "array", + "title": "Azure Storage Account container names.", + "description": "The container names of Azure Storage Account.", + "items": { + "type": "string", + "title": "Azure Storage Account container name", + "description": "The container name of Azure Storage Account." + } + } + } } } } \ No newline at end of file From 7e6da4efe376663fadca8779764aea0dd25b0640 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Thu, 6 Feb 2025 19:18:48 +0000 Subject: [PATCH 2/6] Fix RFC 1123 regex to allow one-length character --- cli/azd/internal/names/label.go | 2 +- cli/azd/internal/names/label_test.go | 35 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/cli/azd/internal/names/label.go b/cli/azd/internal/names/label.go index 6a1c5639400..29a785aeb28 100644 --- a/cli/azd/internal/names/label.go +++ b/cli/azd/internal/names/label.go @@ -9,7 +9,7 @@ import ( "strings" ) -var rfc1123LabelRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$`) +var rfc1123LabelRegex = regexp.MustCompile(`^(?:[A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])$`) // ValidateLabelName checks if the given name is a valid RFC 1123 Label name. func ValidateLabelName(name string) error { diff --git a/cli/azd/internal/names/label_test.go b/cli/azd/internal/names/label_test.go index 498465a5462..b085aceb2ac 100644 --- a/cli/azd/internal/names/label_test.go +++ b/cli/azd/internal/names/label_test.go @@ -4,6 +4,7 @@ package names import ( + "strings" "testing" ) @@ -68,4 +69,38 @@ func TestLabelNameEdgeCases(t *testing.T) { } } +func TestValidateLabelName(t *testing.T) { + tests := []struct { + name string + input string + wantErr bool + }{ + {"SingleLowercase", "a", false}, + {"MaxLength", strings.Repeat("a", 63), false}, + {"WithHyphen", "a-b-c", false}, + {"EmptyString", "", true}, + {"SingleUppercase", "Z", true}, + {"TooLong", strings.Repeat("a", 64), true}, + {"StartWithUppercase", "Abcdef", true}, + {"EndsWithUppercase", "abcdefG", true}, + {"InvalidSingleHyphen", "-", true}, + {"InvalidSingleSymbol", "!", true}, + {"LabelStartingWithHyphen", "-abc", true}, + {"LabelEndingWithHyphen", "abc-", true}, + {"LabelWithInvalidCharacters", "ab#cd", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateLabelName(tt.input) + if tt.wantErr && err == nil { + t.Errorf("expected error for input %q, got none", tt.input) + } + if !tt.wantErr && err != nil { + t.Errorf("expected no error for input %q, got %q", tt.input, err) + } + }) + } +} + //cspell:enable From b630ebe585275bcc0652912e07a7f10e57e64ebf Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Thu, 6 Feb 2025 21:18:23 +0000 Subject: [PATCH 3/6] Remove KeylessAuth property and prompt --- .../internal/cmd/add/add_configure_storage.go | 13 --------- cli/azd/internal/cmd/add/add_preview.go | 5 ---- cli/azd/internal/scaffold/spec.go | 4 +-- cli/azd/pkg/project/resources.go | 7 +---- cli/azd/pkg/project/scaffold_gen.go | 5 ++-- .../scaffold/templates/resources.bicept | 29 ------------------- schemas/alpha/azure.yaml.json | 6 ---- 7 files changed, 4 insertions(+), 65 deletions(-) diff --git a/cli/azd/internal/cmd/add/add_configure_storage.go b/cli/azd/internal/cmd/add/add_configure_storage.go index 0aff43d0f8c..742b32fa503 100644 --- a/cli/azd/internal/cmd/add/add_configure_storage.go +++ b/cli/azd/internal/cmd/add/add_configure_storage.go @@ -46,19 +46,6 @@ func fillStorageDetails( return nil, err } - selection, err := console.Select(ctx, input.ConsoleOptions{ - Message: "Use keyless authentication?", - Options: []string{ - "Yes (recommended)", - "No", - }, - }) - if err != nil { - return nil, err - } - - modelProps.KeylessAuth = selection == 0 - for _, option := range selectedDataTypes { switch option { case StorageDataTypeBlob: diff --git a/cli/azd/internal/cmd/add/add_preview.go b/cli/azd/internal/cmd/add/add_preview.go index 6929e06ade5..b7bd49dd98b 100644 --- a/cli/azd/internal/cmd/add/add_preview.go +++ b/cli/azd/internal/cmd/add/add_preview.go @@ -73,11 +73,6 @@ func Metadata(r *project.ResourceConfig) resourceMeta { "AZURE_STORAGE_ACCOUNT_NAME", "AZURE_STORAGE_BLOB_ENDPOINT", } - - if modelProps, ok := r.Props.(project.StorageProps); ok && !modelProps.KeylessAuth { - res.UseEnvVars = append(res.UseEnvVars, "AZURE_STORAGE_ACCOUNT_KEY") - res.UseEnvVars = append(res.UseEnvVars, "AZURE_STORAGE_CONNECTION_STRING") - } } return res } diff --git a/cli/azd/internal/scaffold/spec.go b/cli/azd/internal/scaffold/spec.go index ee50f6d8008..9cdbc5ecff5 100644 --- a/cli/azd/internal/scaffold/spec.go +++ b/cli/azd/internal/scaffold/spec.go @@ -57,8 +57,7 @@ type AIModelModel struct { } type StorageAccount struct { - Containers []string - KeylessAuth bool + Containers []string } type ServiceSpec struct { @@ -105,7 +104,6 @@ type AIModelReference struct { } type StorageReference struct { - KeylessAuth bool } func containerAppExistsParameter(serviceName string) Parameter { diff --git a/cli/azd/pkg/project/resources.go b/cli/azd/pkg/project/resources.go index 5f31f989e38..d631c893f5b 100644 --- a/cli/azd/pkg/project/resources.go +++ b/cli/azd/pkg/project/resources.go @@ -142,10 +142,6 @@ func (r *ResourceConfig) UnmarshalYAML(value *yaml.Node) error { if err := unmarshalProps(&sp); err != nil { return err } - // Default KeylessAuth to true if omitted - if _, ok := raw.RawProps["keyless"]; !ok { - sp.KeylessAuth = true - } raw.Props = sp } @@ -176,6 +172,5 @@ type AIModelPropsModel struct { } type StorageProps struct { - Containers []string `yaml:"containers,omitempty"` - KeylessAuth bool `yaml:"keyless,omitempty"` + Containers []string `yaml:"containers,omitempty"` } diff --git a/cli/azd/pkg/project/scaffold_gen.go b/cli/azd/pkg/project/scaffold_gen.go index 71b75f2f1f1..674bc6f01c5 100644 --- a/cli/azd/pkg/project/scaffold_gen.go +++ b/cli/azd/pkg/project/scaffold_gen.go @@ -188,8 +188,7 @@ func infraSpec(projectConfig *ProjectConfig) (*scaffold.InfraSpec, error) { } props := res.Props.(StorageProps) infraSpec.StorageAccount = &scaffold.StorageAccount{ - Containers: props.Containers, - KeylessAuth: props.KeylessAuth, + Containers: props.Containers, } } } @@ -283,7 +282,7 @@ func mapHostUses( case ResourceTypeOpenAiModel: svcSpec.AIModels = append(svcSpec.AIModels, scaffold.AIModelReference{Name: use}) case ResourceTypeStorage: - svcSpec.StorageAccount = &scaffold.StorageReference{KeylessAuth: useRes.Props.(StorageProps).KeylessAuth} + svcSpec.StorageAccount = &scaffold.StorageReference{} } } diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index df2cc76edd9..c3dec4c989c 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -168,13 +168,6 @@ module storageAccount 'br/public:avm/res/storage/storage-account:0.17.0' = { defaultAction: 'Allow' } tags: tags - {{- if (and .Services (not .StorageAccount.KeylessAuth)) }} - secretsExportConfiguration: { - keyVaultResourceId: keyVault.outputs.resourceId - accessKey1Name: 'STORAGE-ACCOUNT-KEY' - connectionString1Name: 'STORAGE-ACCOUNT-CONNECTION-STRING' - } - {{- end}} } } {{end}} @@ -311,18 +304,6 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { keyVaultUrl: '${keyVault.outputs.uri}secrets/REDIS-URL' } {{- end}} - {{- if (and .StorageAccount (not .StorageAccount.KeylessAuth)) }} - { - name: 'storage-key' - identity: {{bicepName .Name}}Identity.outputs.resourceId - keyVaultUrl: '${keyVault.outputs.uri}secrets/STORAGE-ACCOUNT-KEY' - } - { - name: 'storage-connection-string' - identity: {{bicepName .Name}}Identity.outputs.resourceId - keyVaultUrl: '${keyVault.outputs.uri}secrets/STORAGE-ACCOUNT-CONNECTION-STRING' - } - {{- end}} ], map({{bicepName .Name}}Secrets, secret => { name: secret.secretRef @@ -409,16 +390,6 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { name: 'AZURE_STORAGE_BLOB_ENDPOINT' value: storageAccount.outputs.serviceEndpoints.blob } - {{- if not .StorageAccount.KeylessAuth}} - { - name: 'AZURE_STORAGE_KEY' - secretRef: 'storage-key' - } - { - name: 'AZURE_STORAGE_CONNECTION_STRING' - secretRef: 'storage-connection-string' - } - {{- end}} {{- end}} {{- if .AIModels}} { diff --git a/schemas/alpha/azure.yaml.json b/schemas/alpha/azure.yaml.json index 732eb7c32c5..edb8dc2e673 100644 --- a/schemas/alpha/azure.yaml.json +++ b/schemas/alpha/azure.yaml.json @@ -1324,12 +1324,6 @@ "properties": { "type": true, "uses": true, - "keyless": { - "type": "boolean", - "title": "Keyless Authentication", - "description": "Indicates whether keyless authentication is used for Azure Storage Account.", - "default": true - }, "containers": { "type": "array", "title": "Azure Storage Account container names.", From d8e929ecc6fe2fa668ac60e62eb7d409d4136193 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Thu, 6 Feb 2025 21:18:47 +0000 Subject: [PATCH 4/6] Update storage account role assignments --- cli/azd/resources/scaffold/templates/resources.bicept | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index c3dec4c989c..70ec82bf1c4 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -154,15 +154,18 @@ module storageAccount 'br/public:avm/res/storage/storage-account:0.17.0' = { } location: location roleAssignments: [ - {{- if .StorageAccount.KeylessAuth }} + { + principalId: principalId + principalType: 'User' + roleDefinitionIdOrName: 'Storage Blob Data Contributor' + } {{- range .Services}} { principalId: {{bicepName .Name}}Identity.outputs.principalId principalType: 'ServicePrincipal' - roleDefinitionIdOrName: 'Storage Blob Data Owner' + roleDefinitionIdOrName: 'Storage Blob Data Contributor' } {{- end}} - {{- end}} ] networkAcls: { defaultAction: 'Allow' From b48141ed4734a9b154cbccca7307c671e87aec2d Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Thu, 6 Feb 2025 21:49:30 +0000 Subject: [PATCH 5/6] Fix test --- cli/azd/internal/repository/app_init_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/azd/internal/repository/app_init_test.go b/cli/azd/internal/repository/app_init_test.go index dad43680123..31ca10a2976 100644 --- a/cli/azd/internal/repository/app_init_test.go +++ b/cli/azd/internal/repository/app_init_test.go @@ -211,13 +211,13 @@ func TestInitializer_prjConfigFromDetect(t *testing.T) { interactions: []string{ // prompt for db -- hit multiple validation cases "my app db", - "n", + "N", "my$special$db", - "n", + "N", "mongodb", // fill in db name // prompt for db -- hit multiple validation cases "my$special$db", - "n", + "N", "postgres", // fill in db name }, want: project.ProjectConfig{ From 84501cbeec622f0aa13f8d45ae8a77a04771a7b7 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Mon, 10 Feb 2025 22:33:46 +0000 Subject: [PATCH 6/6] Disable storage account key access by default --- cli/azd/resources/scaffold/templates/resources.bicept | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index 70ec82bf1c4..09ec14f2163 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -136,10 +136,11 @@ module postgreServer 'br/public:avm/res/db-for-postgre-sql/flexible-server:0.1.4 {{- if .StorageAccount }} var storageAccountName = '${abbrs.storageStorageAccounts}${resourceToken}' -module storageAccount 'br/public:avm/res/storage/storage-account:0.17.0' = { +module storageAccount 'br/public:avm/res/storage/storage-account:0.17.2' = { name: 'storageAccount' params: { name: storageAccountName + allowSharedKeyAccess: false publicNetworkAccess: 'Enabled' blobServices: { {{- if .StorageAccount.Containers }}