diff --git a/cli/azd/internal/appdetect/appdetect.go b/cli/azd/internal/appdetect/appdetect.go index d01a8b6f1ce..bb8053061a3 100644 --- a/cli/azd/internal/appdetect/appdetect.go +++ b/cli/azd/internal/appdetect/appdetect.go @@ -145,6 +145,14 @@ func (a AzureDepServiceBus) ResourceDisplay() string { return "Azure Service Bus" } +type AzureDepEventHubs struct { + Names []string +} + +func (a AzureDepEventHubs) ResourceDisplay() string { + return "Azure Event Hubs" +} + type Project struct { // The language associated with the project. Language Language diff --git a/cli/azd/internal/appdetect/java.go b/cli/azd/internal/appdetect/java.go index be9989a0baf..6d7bbd4dd05 100644 --- a/cli/azd/internal/appdetect/java.go +++ b/cli/azd/internal/appdetect/java.go @@ -178,6 +178,18 @@ func detectDependencies(mavenProject *mavenProject, project *Project) (*Project, Queues: destinations, }) } + + if dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-stream-binder-eventhubs" { + bindingDestinations := findBindingDestinations(applicationProperties) + destinations := make([]string, 0, len(bindingDestinations)) + for bindingName, destination := range bindingDestinations { + destinations = append(destinations, destination) + log.Printf("Event Hubs [%s] found for binding [%s]", destination, bindingName) + } + project.AzureDeps = append(project.AzureDeps, AzureDepEventHubs{ + Names: destinations, + }) + } } if len(databaseDepMap) > 0 { diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index ef5e2f5e9f6..f4b098b4440 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -41,6 +41,7 @@ var dbMap = map[appdetect.DatabaseDep]struct{}{ var azureDepMap = map[string]struct{}{ appdetect.AzureDepServiceBus{}.ResourceDisplay(): {}, + appdetect.AzureDepEventHubs{}.ResourceDisplay(): {}, } // InitFromApp initializes the infra directory and project file from the current existing app. diff --git a/cli/azd/internal/repository/infra_confirm.go b/cli/azd/internal/repository/infra_confirm.go index 35550c8de11..f41f55029e6 100644 --- a/cli/azd/internal/repository/infra_confirm.go +++ b/cli/azd/internal/repository/infra_confirm.go @@ -212,6 +212,8 @@ func (i *Initializer) infraSpecFromDetect( switch azureDep.(type) { case appdetect.AzureDepServiceBus: serviceSpec.AzureServiceBus = spec.AzureServiceBus + case appdetect.AzureDepEventHubs: + serviceSpec.AzureEventHubs = spec.AzureEventHubs } } spec.Services = append(spec.Services, serviceSpec) @@ -344,41 +346,51 @@ azureDepPrompt: } } - authType := scaffold.AuthType(0) switch azureDep.(type) { case appdetect.AzureDepServiceBus: - _authType, err := i.console.Prompt(ctx, input.ConsoleOptions{ - Message: fmt.Sprintf("Input the authentication type you want for (%s), 1 for connection string, 2 for managed identity", azureDep.ResourceDisplay()), - Help: "Authentication type:\n\n" + - "Enter 1 if you want to use connection string to connect to the Service Bus.\n" + - "Enter 2 if you want to use user assigned managed identity to connect to the Service Bus.", - }) + authType, err := i.chooseAuthType(ctx, azureDepName) if err != nil { return err } - - if _authType != "1" && _authType != "2" { - i.console.Message(ctx, "Invalid authentication type. Please enter 0 or 1.") - continue azureDepPrompt - } - if _authType == "1" { - authType = scaffold.AuthType_PASSWORD - } else { - authType = scaffold.AuthType_TOKEN_CREDENTIAL - } - } - - switch azureDep.(type) { - case appdetect.AzureDepServiceBus: spec.AzureServiceBus = &scaffold.AzureDepServiceBus{ Name: azureDepName, Queues: azureDep.(appdetect.AzureDepServiceBus).Queues, AuthUsingConnectionString: authType == scaffold.AuthType_PASSWORD, AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, } + case appdetect.AzureDepEventHubs: + authType, err := i.chooseAuthType(ctx, azureDepName) + if err != nil { + return err + } + spec.AzureEventHubs = &scaffold.AzureDepEventHubs{ + Name: azureDepName, + EventHubNames: azureDep.(appdetect.AzureDepEventHubs).Names, + AuthUsingConnectionString: authType == scaffold.AuthType_PASSWORD, + AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, + } break azureDepPrompt } break azureDepPrompt } return nil } + +func (i *Initializer) chooseAuthType(ctx context.Context, serviceName string) (scaffold.AuthType, error) { + portOptions := []string{ + "User assigned managed identity", + "Connection string", + } + selection, err := i.console.Select(ctx, input.ConsoleOptions{ + Message: "Choose auth type for '" + serviceName + "'?", + Options: portOptions, + }) + if err != nil { + return scaffold.AUTH_TYPE_UNSPECIFIED, err + } + if selection == 0 { + return scaffold.AuthType_TOKEN_CREDENTIAL, nil + } else { + return scaffold.AuthType_PASSWORD, nil + } +} diff --git a/cli/azd/internal/scaffold/spec.go b/cli/azd/internal/scaffold/spec.go index 44664e1918e..cb8b7d921a3 100644 --- a/cli/azd/internal/scaffold/spec.go +++ b/cli/azd/internal/scaffold/spec.go @@ -17,6 +17,8 @@ type InfraSpec struct { // Azure Service Bus AzureServiceBus *AzureDepServiceBus + // Azure EventHubs + AzureEventHubs *AzureDepEventHubs } type Parameter struct { @@ -55,6 +57,13 @@ type AzureDepServiceBus struct { AuthUsingManagedIdentity bool } +type AzureDepEventHubs struct { + Name string + EventHubNames []string + AuthUsingConnectionString bool + AuthUsingManagedIdentity bool +} + // AuthType defines different authentication types. type AuthType int32 @@ -84,6 +93,8 @@ type ServiceSpec struct { // Azure Service Bus AzureServiceBus *AzureDepServiceBus + // Azure Service Bus + AzureEventHubs *AzureDepEventHubs } type Frontend struct { diff --git a/cli/azd/resources/scaffold/base/modules/set-event-hubs-namespace-connection-string.bicep b/cli/azd/resources/scaffold/base/modules/set-event-hubs-namespace-connection-string.bicep new file mode 100644 index 00000000000..82d13f9a83d --- /dev/null +++ b/cli/azd/resources/scaffold/base/modules/set-event-hubs-namespace-connection-string.bicep @@ -0,0 +1,20 @@ +param eventHubsNamespaceName string +param connectionStringSecretName string +param keyVaultName string + +resource eventHubsNamespace 'Microsoft.EventHub/namespaces@2024-01-01' existing = { + name: eventHubsNamespaceName +} + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { + name: keyVaultName +} + +resource connectionStringSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { + name: connectionStringSecretName + parent: keyVault + properties: { + value: listKeys(concat(resourceId('Microsoft.EventHub/namespaces', eventHubsNamespaceName), '/AuthorizationRules/RootManageSharedAccessKey'), '2024-01-01').primaryConnectionString + } +} + diff --git a/cli/azd/resources/scaffold/templates/main.bicept b/cli/azd/resources/scaffold/templates/main.bicept index 6a574ab55e1..527b5d08c48 100644 --- a/cli/azd/resources/scaffold/templates/main.bicept +++ b/cli/azd/resources/scaffold/templates/main.bicept @@ -59,4 +59,7 @@ output AZURE_CACHE_REDIS_ID string = resources.outputs.AZURE_CACHE_REDIS_ID {{- if .DbPostgres}} output AZURE_POSTGRES_FLEXIBLE_SERVER_ID string = resources.outputs.AZURE_POSTGRES_FLEXIBLE_SERVER_ID {{- end}} +{{- if .AzureEventHubs }} +output AZURE_EVENT_HUBS_ID string = resources.outputs.AZURE_EVENT_HUBS_ID +{{- end}} {{ end}} diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index 7d39c83c534..5d1ab06e69b 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -126,7 +126,47 @@ module postgreServer 'br/public:avm/res/db-for-postgre-sql/flexible-server:0.1.4 } } {{- end}} +{{- if .AzureEventHubs }} +module eventHubNamespace 'br/public:avm/res/event-hub/namespace:0.7.1' = { + name: 'eventHubNamespace' + params: { + name: '${abbrs.eventHubNamespaces}${resourceToken}' + location: location + roleAssignments: [ + {{- if (and .AzureEventHubs .AzureEventHubs.AuthUsingManagedIdentity) }} + {{- range .Services}} + { + principalId: {{bicepName .Name}}Identity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f526a384-b230-433a-b45c-95f59c4a2dec') + } + {{- end}} + {{- end}} + ] + {{- if (and .AzureEventHubs .AzureEventHubs.AuthUsingConnectionString) }} + disableLocalAuth: false + {{- end}} + eventhubs: [ + {{- range $eventHubName := .AzureEventHubs.EventHubNames}} + { + name: '{{ $eventHubName }}' + } + {{- end}} + ] + } +} +{{- if (and .AzureEventHubs .AzureEventHubs.AuthUsingConnectionString) }} +module eventHubsConnectionString './modules/set-event-hubs-namespace-connection-string.bicep' = { + name: 'eventHubsConnectionString' + params: { + eventHubsNamespaceName: eventHubNamespace.outputs.name + connectionStringSecretName: 'EVENT-HUBS-CONNECTION-STRING' + keyVaultName: keyVault.outputs.name + } +} +{{end}} +{{end}} {{- range .Services}} module {{bicepName .Name}}Identity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { @@ -205,6 +245,13 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { keyVaultUrl: '${keyVault.outputs.uri}secrets/REDIS-URL' } {{- end}} + {{- if (and .AzureEventHubs .AzureEventHubs.AuthUsingConnectionString) }} + { + name: 'event-hubs-connection-string' + identity:{{bicepName .Name}}Identity.outputs.resourceId + keyVaultUrl: '${keyVault.outputs.uri}secrets/EVENT-HUBS-CONNECTION-STRING' + } + {{- end}} ], map({{bicepName .Name}}Secrets, secret => { name: secret.secretRef @@ -282,6 +329,40 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { secretRef: 'redis-pass' } {{- end}} + {{- if .AzureEventHubs }} + { + name: 'SPRING_CLOUD_AZURE_EVENTHUBS_NAMESPACE' + value: eventHubNamespace.outputs.name + } + {{- end}} + {{- if (and .AzureEventHubs .AzureEventHubs.AuthUsingManagedIdentity) }} + { + name: 'SPRING_CLOUD_AZURE_EVENTHUBS_CONNECTION_STRING' + value: '' + } + { + name: 'SPRING_CLOUD_AZURE_EVENTHUBS_CREDENTIAL_MANAGEDIDENTITYENABLED' + value: 'true' + } + { + name: 'SPRING_CLOUD_AZURE_EVENTHUBS_CREDENTIAL_CLIENTID' + value: {{bicepName .Name}}Identity.outputs.clientId + } + {{- end}} + {{- if (and .AzureEventHubs .AzureEventHubs.AuthUsingConnectionString) }} + { + name: 'SPRING_CLOUD_AZURE_EVENTHUBS_CONNECTION_STRING' + secretRef: 'event-hubs-connection-string' + } + { + name: 'SPRING_CLOUD_AZURE_EVENTHUBS_CREDENTIAL_MANAGEDIDENTITYENABLED' + value: 'false' + } + { + name: 'SPRING_CLOUD_AZURE_EVENTHUBS_CREDENTIAL_CLIENTID' + value: '' + } + {{- end}} {{- if .Frontend}} {{- range $i, $e := .Frontend.Backends}} { @@ -392,4 +473,7 @@ output AZURE_CACHE_REDIS_ID string = redis.outputs.resourceId {{- if .DbPostgres}} output AZURE_POSTGRES_FLEXIBLE_SERVER_ID string = postgreServer.outputs.resourceId {{- end}} +{{- if .AzureEventHubs }} +output AZURE_EVENT_HUBS_ID string = eventHubNamespace.outputs.resourceId +{{- end}} {{ end}}