diff --git a/.vscode/cspell-schemas.txt b/.vscode/cspell-schemas.txt index aecf4847bc9..3f5c11bada9 100644 --- a/.vscode/cspell-schemas.txt +++ b/.vscode/cspell-schemas.txt @@ -1,3 +1,4 @@ +eventhubs postdeploy postdown postinfracreate @@ -12,3 +13,4 @@ preinfradelete preprovision prerestore preup +servicebus diff --git a/cli/azd/.vscode/cspell-azd-dictionary.txt b/cli/azd/.vscode/cspell-azd-dictionary.txt index 3e086c0dfff..487e32da36b 100644 --- a/cli/azd/.vscode/cspell-azd-dictionary.txt +++ b/cli/azd/.vscode/cspell-azd-dictionary.txt @@ -104,6 +104,7 @@ envsubst errcheck errorinfo errorlint +eventhubs executil flexconsumption Frontends diff --git a/cli/azd/internal/cmd/add/add_configure.go b/cli/azd/internal/cmd/add/add_configure.go index ff7812ec84e..9c541f937d9 100644 --- a/cli/azd/internal/cmd/add/add_configure.go +++ b/cli/azd/internal/cmd/add/add_configure.go @@ -44,6 +44,10 @@ func Configure( case project.ResourceTypeDbPostgres, project.ResourceTypeDbMongo: return fillDatabaseName(ctx, r, console, p) + case project.ResourceTypeMessagingEventHubs: + return fillEventHubs(ctx, r, console, p) + case project.ResourceTypeMessagingServiceBus: + return fillServiceBus(ctx, r, console, p) case project.ResourceTypeDbRedis: if _, exists := p.PrjConfig.Resources["redis"]; exists { return nil, fmt.Errorf("only one Redis resource is allowed at this time") diff --git a/cli/azd/internal/cmd/add/add_configure_messaging.go b/cli/azd/internal/cmd/add/add_configure_messaging.go new file mode 100644 index 00000000000..d227d2c2f99 --- /dev/null +++ b/cli/azd/internal/cmd/add/add_configure_messaging.go @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package add + +import ( + "context" + "fmt" + + "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/project" +) + +func fillEventHubs( + ctx context.Context, + r *project.ResourceConfig, + console input.Console, + p PromptOptions) (*project.ResourceConfig, error) { + r.Name = "event-hubs" + + if _, exists := p.PrjConfig.Resources["event-hubs"]; exists { + return nil, fmt.Errorf("only one event hubs resource is allowed at this time") + } + + for { + topicName, err := console.Prompt(ctx, input.ConsoleOptions{ + Message: "Input the event hub name:", + Help: "Event hub name\n\n" + + "Name of the event hub that the app connects to. " + + "Also known as a Kafka topic.", + }) + if err != nil { + return r, err + } + + if err := names.ValidateLabelName(topicName); err != nil { + console.Message(ctx, err.Error()) + continue + } + + r.Props = project.EventHubsProps{ + Hubs: []string{topicName}, + } + break + } + + return r, nil +} + +func fillServiceBus( + ctx context.Context, + r *project.ResourceConfig, + console input.Console, + p PromptOptions) (*project.ResourceConfig, error) { + r.Name = "service-bus" + + if _, exists := p.PrjConfig.Resources["service-bus"]; exists { + return nil, fmt.Errorf("only one service bus resource is allowed at this time") + } + + for { + queueName, err := console.Prompt(ctx, input.ConsoleOptions{ + Message: "Input the queue name:", + Help: "Service Bus queue name\n\n" + + "Name of the queue that the app connects to. ", + }) + if err != nil { + return r, err + } + + if err := names.ValidateLabelName(queueName); err != nil { + console.Message(ctx, err.Error()) + continue + } + + r.Props = project.ServiceBusProps{ + Queues: []string{queueName}, + } + break + } + + 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 index 742b32fa503..5d1b670294f 100644 --- a/cli/azd/internal/cmd/add/add_configure_storage.go +++ b/cli/azd/internal/cmd/add/add_configure_storage.go @@ -28,14 +28,12 @@ func fillStorageDetails( r *project.ResourceConfig, console input.Console, p PromptOptions) (*project.ResourceConfig, error) { + r.Name = "storage" + 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") diff --git a/cli/azd/internal/cmd/add/add_preview.go b/cli/azd/internal/cmd/add/add_preview.go index b7bd49dd98b..eb5343f5681 100644 --- a/cli/azd/internal/cmd/add/add_preview.go +++ b/cli/azd/internal/cmd/add/add_preview.go @@ -67,6 +67,18 @@ func Metadata(r *project.ResourceConfig) resourceMeta { res.UseEnvVars = []string{ "AZURE_OPENAI_ENDPOINT", } + case project.ResourceTypeMessagingEventHubs: + res.AzureResourceType = "Microsoft.EventHub/namespaces" + res.UseEnvVars = []string{ + "AZURE_EVENT_HUBS_HOST", + "AZURE_EVENT_HUBS_NAME", + } + case project.ResourceTypeMessagingServiceBus: + res.AzureResourceType = "Microsoft.ServiceBus/namespaces" + res.UseEnvVars = []string{ + "AZURE_SERVICE_BUS_HOST", + "AZURE_SERVICE_BUS_NAME", + } case project.ResourceTypeStorage: res.AzureResourceType = "Microsoft.Storage/storageAccounts" res.UseEnvVars = []string{ diff --git a/cli/azd/internal/cmd/add/add_select.go b/cli/azd/internal/cmd/add/add_select.go index 97ea06bacdc..b2cf088c2ba 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: "messaging", Label: "Messaging", SelectResource: selectMessaging}, {Namespace: "storage", Label: "Storage account", SelectResource: selectStorage}, } } @@ -61,6 +62,31 @@ func selectDatabase( return r, nil } +func selectMessaging( + console input.Console, + ctx context.Context, + p PromptOptions) (*project.ResourceConfig, error) { + resourceTypesDisplayMap := make(map[string]project.ResourceType) + for _, resourceType := range project.AllResourceTypes() { + if strings.HasPrefix(string(resourceType), "messaging.") { + resourceTypesDisplayMap[resourceType.String()] = resourceType + } + } + + r := &project.ResourceConfig{} + resourceTypesDisplay := slices.Sorted(maps.Keys(resourceTypesDisplayMap)) + dbOption, err := console.Select(ctx, input.ConsoleOptions{ + Message: "Which type of messaging service?", + Options: resourceTypesDisplay, + }) + if err != nil { + return nil, err + } + + r.Type = resourceTypesDisplayMap[resourceTypesDisplay[dbOption]] + return r, nil +} + func selectStorage( console input.Console, ctx context.Context, diff --git a/cli/azd/internal/scaffold/scaffold_test.go b/cli/azd/internal/scaffold/scaffold_test.go index 898229219ec..a620d8c507a 100644 --- a/cli/azd/internal/scaffold/scaffold_test.go +++ b/cli/azd/internal/scaffold/scaffold_test.go @@ -90,6 +90,8 @@ func TestExecInfra(t *testing.T) { DatabaseName: "appdb", }, DbRedis: &DatabaseRedis{}, + ServiceBus: &ServiceBus{}, + EventHubs: &EventHubs{}, StorageAccount: &StorageAccount{}, Services: []ServiceSpec{ { @@ -111,6 +113,8 @@ func TestExecInfra(t *testing.T) { DbPostgres: &DatabaseReference{ DatabaseName: "appdb", }, + ServiceBus: &ServiceBus{}, + EventHubs: &EventHubs{}, StorageAccount: &StorageReference{}, }, { diff --git a/cli/azd/internal/scaffold/spec.go b/cli/azd/internal/scaffold/spec.go index 9cdbc5ecff5..4f9518ac4b8 100644 --- a/cli/azd/internal/scaffold/spec.go +++ b/cli/azd/internal/scaffold/spec.go @@ -17,6 +17,11 @@ type InfraSpec struct { DbCosmosMongo *DatabaseCosmosMongo DbRedis *DatabaseRedis + // Messaging services + ServiceBus *ServiceBus + EventHubs *EventHubs + + // Storage account StorageAccount *StorageAccount // ai models @@ -56,6 +61,15 @@ type AIModelModel struct { Version string } +type ServiceBus struct { + Queues []string + Topics []string +} + +type EventHubs struct { + Hubs []string +} + type StorageAccount struct { Containers []string } @@ -81,6 +95,10 @@ type ServiceSpec struct { // AI model connections AIModels []AIModelReference + + // Messaging services + ServiceBus *ServiceBus + EventHubs *EventHubs } type Frontend struct { diff --git a/cli/azd/pkg/azapi/azure_resource_types.go b/cli/azd/pkg/azapi/azure_resource_types.go index 8479d0682bd..ede7f47c137 100644 --- a/cli/azd/pkg/azapi/azure_resource_types.go +++ b/cli/azd/pkg/azapi/azure_resource_types.go @@ -12,6 +12,7 @@ const ( AzureResourceTypeCacheForRedis AzureResourceType = "Microsoft.Cache/redis" AzureResourceTypeCDNProfile AzureResourceType = "Microsoft.Cdn/profiles" AzureResourceTypeCosmosDb AzureResourceType = "Microsoft.DocumentDB/databaseAccounts" + AzureResourceTypeEventHubsNamespace AzureResourceType = "Microsoft.EventHub/namespaces" AzureResourceTypeContainerApp AzureResourceType = "Microsoft.App/containerApps" AzureResourceTypeSpringApp AzureResourceType = "Microsoft.AppPlatform/Spring" AzureResourceTypeContainerAppEnvironment AzureResourceType = "Microsoft.App/managedEnvironments" @@ -79,6 +80,8 @@ func GetResourceTypeDisplayName(resourceType AzureResourceType) string { return "Container Apps Environment" case AzureResourceTypeServiceBusNamespace: return "Service Bus Namespace" + case AzureResourceTypeEventHubsNamespace: + return "Event Hubs Namespace" case AzureResourceTypeServicePlan: return "App Service plan" case AzureResourceTypeCosmosDb: diff --git a/cli/azd/pkg/project/resources.go b/cli/azd/pkg/project/resources.go index d631c893f5b..4145853abcb 100644 --- a/cli/azd/pkg/project/resources.go +++ b/cli/azd/pkg/project/resources.go @@ -18,17 +18,21 @@ func AllResourceTypes() []ResourceType { ResourceTypeDbMongo, ResourceTypeHostContainerApp, ResourceTypeOpenAiModel, + ResourceTypeMessagingEventHubs, + ResourceTypeMessagingServiceBus, ResourceTypeStorage, } } const ( - ResourceTypeDbRedis ResourceType = "db.redis" - ResourceTypeDbPostgres ResourceType = "db.postgres" - ResourceTypeDbMongo ResourceType = "db.mongo" - ResourceTypeHostContainerApp ResourceType = "host.containerapp" - ResourceTypeOpenAiModel ResourceType = "ai.openai.model" - ResourceTypeStorage ResourceType = "storage" + ResourceTypeDbRedis ResourceType = "db.redis" + ResourceTypeDbPostgres ResourceType = "db.postgres" + ResourceTypeDbMongo ResourceType = "db.mongo" + ResourceTypeHostContainerApp ResourceType = "host.containerapp" + ResourceTypeOpenAiModel ResourceType = "ai.openai.model" + ResourceTypeMessagingEventHubs ResourceType = "messaging.eventhubs" + ResourceTypeMessagingServiceBus ResourceType = "messaging.servicebus" + ResourceTypeStorage ResourceType = "storage" ) func (r ResourceType) String() string { @@ -43,6 +47,10 @@ func (r ResourceType) String() string { return "Container App" case ResourceTypeOpenAiModel: return "Open AI Model" + case ResourceTypeMessagingEventHubs: + return "Event Hubs" + case ResourceTypeMessagingServiceBus: + return "Service Bus" case ResourceTypeStorage: return "Storage Account" } @@ -82,22 +90,22 @@ func (r *ResourceConfig) MarshalYAML() (interface{}, error) { return nil } + var errMarshal error switch raw.Type { case ResourceTypeOpenAiModel: - err := marshalRawProps(raw.Props.(AIModelProps)) - if err != nil { - return nil, err - } + errMarshal = marshalRawProps(raw.Props.(AIModelProps)) case ResourceTypeHostContainerApp: - err := marshalRawProps(raw.Props.(ContainerAppProps)) - if err != nil { - return nil, err - } + errMarshal = marshalRawProps(raw.Props.(ContainerAppProps)) + case ResourceTypeMessagingEventHubs: + errMarshal = marshalRawProps(raw.Props.(EventHubsProps)) + case ResourceTypeMessagingServiceBus: + errMarshal = marshalRawProps(raw.Props.(ServiceBusProps)) case ResourceTypeStorage: - err := marshalRawProps(raw.Props.(StorageProps)) - if err != nil { - return nil, err - } + errMarshal = marshalRawProps(raw.Props.(StorageProps)) + } + + if errMarshal != nil { + return nil, errMarshal } return raw, nil @@ -137,6 +145,18 @@ func (r *ResourceConfig) UnmarshalYAML(value *yaml.Node) error { return err } raw.Props = cap + case ResourceTypeMessagingEventHubs: + ehp := EventHubsProps{} + if err := unmarshalProps(&ehp); err != nil { + return err + } + raw.Props = ehp + case ResourceTypeMessagingServiceBus: + sbp := ServiceBusProps{} + if err := unmarshalProps(&sbp); err != nil { + return err + } + raw.Props = sbp case ResourceTypeStorage: sp := StorageProps{} if err := unmarshalProps(&sp); err != nil { @@ -171,6 +191,15 @@ type AIModelPropsModel struct { Version string `yaml:"version,omitempty"` } +type ServiceBusProps struct { + Queues []string `yaml:"queues,omitempty"` + Topics []string `yaml:"topics,omitempty"` +} + +type EventHubsProps struct { + Hubs []string `yaml:"hubs,omitempty"` +} + type StorageProps struct { Containers []string `yaml:"containers,omitempty"` } diff --git a/cli/azd/pkg/project/scaffold_gen.go b/cli/azd/pkg/project/scaffold_gen.go index 674bc6f01c5..27fec5c1637 100644 --- a/cli/azd/pkg/project/scaffold_gen.go +++ b/cli/azd/pkg/project/scaffold_gen.go @@ -152,6 +152,7 @@ func infraSpec(projectConfig *ProjectConfig) (*scaffold.InfraSpec, error) { svcSpec := scaffold.ServiceSpec{ Name: res.Name, Port: -1, + Env: map[string]string{}, } err := mapContainerApp(res, &svcSpec, &infraSpec) @@ -182,6 +183,23 @@ func infraSpec(projectConfig *ProjectConfig) (*scaffold.InfraSpec, error) { Version: props.Model.Version, }, }) + case ResourceTypeMessagingEventHubs: + if infraSpec.EventHubs != nil { + return nil, fmt.Errorf("only one event hubs resource is currently allowed") + } + props := res.Props.(EventHubsProps) + infraSpec.EventHubs = &scaffold.EventHubs{ + Hubs: props.Hubs, + } + case ResourceTypeMessagingServiceBus: + if infraSpec.ServiceBus != nil { + return nil, fmt.Errorf("only one service bus resource is currently allowed") + } + props := res.Props.(ServiceBusProps) + infraSpec.ServiceBus = &scaffold.ServiceBus{ + Queues: props.Queues, + Topics: props.Topics, + } case ResourceTypeStorage: if infraSpec.StorageAccount != nil { return nil, fmt.Errorf("only one storage account resource is currently allowed") @@ -281,6 +299,10 @@ func mapHostUses( backendMapping[use] = res.Name // record the backend -> frontend mapping case ResourceTypeOpenAiModel: svcSpec.AIModels = append(svcSpec.AIModels, scaffold.AIModelReference{Name: use}) + case ResourceTypeMessagingEventHubs: + svcSpec.EventHubs = &scaffold.EventHubs{} + case ResourceTypeMessagingServiceBus: + svcSpec.ServiceBus = &scaffold.ServiceBus{} case ResourceTypeStorage: svcSpec.StorageAccount = &scaffold.StorageReference{} } diff --git a/cli/azd/resources/scaffold/templates/main.bicept b/cli/azd/resources/scaffold/templates/main.bicept index 16ec886317a..b354d13ec6a 100644 --- a/cli/azd/resources/scaffold/templates/main.bicept +++ b/cli/azd/resources/scaffold/templates/main.bicept @@ -70,4 +70,10 @@ output AZURE_RESOURCE_{{alphaSnakeUpper .DbPostgres.DatabaseName}}_ID string = r {{- if .StorageAccount }} output AZURE_RESOURCE_STORAGE_ID string = resources.outputs.AZURE_RESOURCE_STORAGE_ID {{- end}} +{{- if .EventHubs}} +output AZURE_RESOURCE_EVENT_HUBS_ID string = resources.outputs.AZURE_RESOURCE_EVENT_HUBS_ID +{{- end}} +{{- if .ServiceBus}} +output AZURE_RESOURCE_SERVICE_BUS_ID string = resources.outputs.AZURE_RESOURCE_SERVICE_BUS_ID +{{- end}} {{ end}} diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index 09ec14f2163..4888e1c7c35 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -38,7 +38,6 @@ module containerRegistry 'br/public:avm/res/container-registry/registry:0.1.1' = params: { name: '${abbrs.containerRegistryRegistries}${resourceToken}' location: location - acrAdminUserEnabled: true tags: tags publicNetworkAccess: 'Enabled' roleAssignments:[ @@ -216,6 +215,77 @@ resource localUserOpenAIIdentity 'Microsoft.Authorization/roleAssignments@2022-0 } {{- end}} +{{- if .EventHubs }} +module eventHubNamespace 'br/public:avm/res/event-hub/namespace:0.8.0' = { + name: 'eventHubNamespace' + params: { + name: '${abbrs.eventHubNamespaces}${resourceToken}' + location: location + roleAssignments: [ + {{- range .Services}} + { + principalId: {{bicepName .Name}}Identity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f526a384-b230-433a-b45c-95f59c4a2dec') + } + {{- end}} + { + principalId: principalId + principalType: 'User' + roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f526a384-b230-433a-b45c-95f59c4a2dec') + } + ] + disableLocalAuth: true + eventhubs: [ + {{- range $hub := .EventHubs.Hubs}} + { + name: '{{ $hub }}' + } + {{- end}} + ] + } +} +{{- end}} + +{{- if .ServiceBus }} +module serviceBusNamespace 'br/public:avm/res/service-bus/namespace:0.11.2' = { + name: 'serviceBusNamespace' + params: { + name: '${abbrs.serviceBusNamespaces}${resourceToken}' + location: location + roleAssignments: [ + {{- range .Services}} + { + principalId: {{bicepName .Name}}Identity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '090c5cfd-751d-490a-894a-3ce6f1109419') + } + {{- end}} + { + principalId: principalId + principalType: 'User' + roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '090c5cfd-751d-490a-894a-3ce6f1109419') + } + ] + disableLocalAuth: true + queues: [ + {{- range $queue := .ServiceBus.Queues}} + { + name: '{{ $queue }}' + } + {{- end}} + ] + topics: [ + {{- range $topic := .ServiceBus.Topics}} + { + name: '{{ $topic }}' + } + {{- end}} + ] + } +} +{{- end}} + {{- range .Services}} module {{bicepName .Name}}Identity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { @@ -385,6 +455,26 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { secretRef: 'redis-pass' } {{- end}} + {{- if .EventHubs}} + { + name: 'AZURE_EVENT_HUBS_NAME' + value: eventHubNamespace.outputs.name + } + { + name: 'AZURE_EVENT_HUBS_HOST' + value: '${eventHubNamespace.outputs.name}.servicebus.windows.net' + } + {{- end}} + {{- if .ServiceBus}} + { + name: 'AZURE_SERVICE_BUS_NAME' + value: serviceBusNamespace.outputs.name + } + { + name: 'AZURE_SERVICE_BUS_HOST' + value: '${serviceBusNamespace.outputs.name}.servicebus.windows.net' + } + {{- end}} {{- if .StorageAccount}} { name: 'AZURE_STORAGE_ACCOUNT_NAME' @@ -519,4 +609,10 @@ output AZURE_RESOURCE_{{alphaSnakeUpper .DbPostgres.DatabaseName}}_ID string = ' {{- if .StorageAccount }} output AZURE_RESOURCE_STORAGE_ID string = storageAccount.outputs.resourceId {{- end}} +{{- if .EventHubs}} +output AZURE_RESOURCE_EVENT_HUBS_ID string = eventHubNamespace.outputs.resourceId +{{- end}} +{{- if .ServiceBus}} +output AZURE_RESOURCE_SERVICE_BUS_ID string = serviceBusNamespace.outputs.resourceId +{{- end}} {{ end}} diff --git a/schemas/alpha/azure.yaml.json b/schemas/alpha/azure.yaml.json index edb8dc2e673..8117ee9324f 100644 --- a/schemas/alpha/azure.yaml.json +++ b/schemas/alpha/azure.yaml.json @@ -370,6 +370,8 @@ "db.mongo", "ai.openai.model", "host.containerapp", + "messaging.eventhubs", + "messaging.servicebus", "storage" ] }, @@ -388,6 +390,8 @@ { "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": "messaging.eventhubs" }}}, "then": { "$ref": "#/definitions/eventHubsResource" } }, + { "if": { "properties": { "type": { "const": "messaging.servicebus" }}}, "then": { "$ref": "#/definitions/serviceBusResource" } }, { "if": { "properties": { "type": { "const": "storage" }}}, "then": { "$ref": "#/definitions/storageAccountResource"} } ] } @@ -1317,6 +1321,51 @@ } } }, + "eventHubsResource": { + "type": "object", + "description": "An Azure Event Hubs namespace.", + "additionalProperties": false, + "properties": { + "type": true, + "uses": true, + "hubs": { + "type": "array", + "title": "Hubs to create in the Event Hubs namespace", + "additionalProperties": false, + "items": { + "type": "string" + }, + "uniqueItems": true + } + } + }, + "serviceBusResource": { + "type": "object", + "description": "An Azure Service Bus namespace.", + "additionalProperties": false, + "properties": { + "type": true, + "uses": true, + "queues": { + "type": "array", + "title": "Queues to create in the Service Bus namespace", + "additionalProperties": false, + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "topics": { + "type": "array", + "title": "Topics to create in the Service Bus namespace", + "additionalProperties": false, + "items": { + "type": "string" + }, + "uniqueItems": true + } + } + }, "storageAccountResource": { "type": "object", "description": "A deployed, ready-to-use Azure Storage Account.",