diff --git a/cli/azd/internal/appdetect/appdetect.go b/cli/azd/internal/appdetect/appdetect.go index 787866413f2..c7f8e6ebe48 100644 --- a/cli/azd/internal/appdetect/appdetect.go +++ b/cli/azd/internal/appdetect/appdetect.go @@ -156,10 +156,30 @@ func (a AzureDepServiceBus) ResourceDisplay() string { type AzureDepEventHubs struct { EventHubsNamePropertyMap map[string]string - UseKafka bool + DependencyTypes []DependencyType SpringBootVersion string } +type DependencyType string + +const ( + SpringCloudStreamEventHubs DependencyType = "spring-cloud-azure-stream-binder-eventhubs" + SpringCloudEventHubsStarter DependencyType = "spring-cloud-azure-starter-eventhubs" + SpringIntegrationEventHubs DependencyType = "spring-cloud-azure-starter-integration-eventhubs" + SpringMessagingEventHubs DependencyType = "spring-messaging-azure-eventhubs" + SpringCloudStreamKafka DependencyType = "spring-cloud-starter-stream-kafka" + SpringKafka DependencyType = "spring-kafka" +) + +func (a AzureDepEventHubs) UseKafka() bool { + for _, dependencyType := range a.DependencyTypes { + if dependencyType == SpringCloudStreamKafka || dependencyType == SpringKafka { + return true + } + } + return false +} + func (a AzureDepEventHubs) ResourceDisplay() string { return "Azure Event Hubs" } diff --git a/cli/azd/internal/appdetect/spring_boot.go b/cli/azd/internal/appdetect/spring_boot.go index 6c8651ffe33..45660d21e55 100644 --- a/cli/azd/internal/appdetect/spring_boot.go +++ b/cli/azd/internal/appdetect/spring_boot.go @@ -180,8 +180,11 @@ func detectServiceBusAccordingToSpringCloudStreamBinderMavenDependency( func detectEventHubs(azdProject *Project, springBootProject *SpringBootProject) { // we need to figure out multiple projects are using the same event hub detectEventHubsAccordingToSpringCloudStreamBinderMavenDependency(azdProject, springBootProject) - detectEventHubsAccordingToSpringCloudEventhubsStarterDependency(azdProject, springBootProject) + detectEventHubsAccordingToSpringCloudEventhubsStarterMavenDependency(azdProject, springBootProject) + detectEventHubsAccordingToSpringIntegrationEventhubsMavenDependency(azdProject, springBootProject) + detectEventHubsAccordingToSpringMessagingEventhubsMavenDependency(azdProject, springBootProject) detectEventHubsAccordingToSpringCloudStreamKafkaMavenDependency(azdProject, springBootProject) + detectEventHubsAccordingToSpringKafkaMavenDependency(azdProject, springBootProject) } func detectEventHubsAccordingToSpringCloudStreamBinderMavenDependency( @@ -192,9 +195,9 @@ func detectEventHubsAccordingToSpringCloudStreamBinderMavenDependency( bindingDestinations := getBindingDestinationMap(springBootProject.applicationProperties) newDep := AzureDepEventHubs{ EventHubsNamePropertyMap: bindingDestinations, - UseKafka: false, + DependencyTypes: []DependencyType{SpringCloudStreamEventHubs}, } - azdProject.AzureDeps = append(azdProject.AzureDeps, newDep) + addAzureDepEventHubsIntoProject(azdProject, newDep) logServiceAddedAccordingToMavenDependency(newDep.ResourceDisplay(), targetGroupId, targetArtifactId) for bindingName, destination := range bindingDestinations { log.Printf(" Detected Event Hub [%s] for binding [%s] by analyzing property file.", @@ -203,20 +206,30 @@ func detectEventHubsAccordingToSpringCloudStreamBinderMavenDependency( } } -func detectEventHubsAccordingToSpringCloudEventhubsStarterDependency( +func detectEventHubsAccordingToSpringCloudEventhubsStarterMavenDependency( azdProject *Project, springBootProject *SpringBootProject) { var targetGroupId = "com.azure.spring" var targetArtifactId = "spring-cloud-azure-starter-eventhubs" - var targetPropertyName = "spring.cloud.azure.eventhubs.event-hub-name" + // event-hub-name can be specified in different levels, see + // https://learn.microsoft.com/azure/developer/java/spring-framework/configuration-properties-azure-event-hubs + var targetPropertyNames = []string{ + "spring.cloud.azure.eventhubs.event-hub-name", + "spring.cloud.azure.eventhubs.producer.event-hub-name", + "spring.cloud.azure.eventhubs.consumer.event-hub-name", + "spring.cloud.azure.eventhubs.processor.event-hub-name", + } if hasDependency(springBootProject, targetGroupId, targetArtifactId) { - eventHubsNamePropertyMap := map[string]string{ - targetPropertyName: springBootProject.applicationProperties[targetPropertyName], + eventHubsNamePropertyMap := map[string]string{} + for _, propertyName := range targetPropertyNames { + if propertyValue, ok := springBootProject.applicationProperties[propertyName]; ok { + eventHubsNamePropertyMap[propertyName] = propertyValue + } } newDep := AzureDepEventHubs{ EventHubsNamePropertyMap: eventHubsNamePropertyMap, - UseKafka: false, + DependencyTypes: []DependencyType{SpringCloudEventHubsStarter}, } - azdProject.AzureDeps = append(azdProject.AzureDeps, newDep) + addAzureDepEventHubsIntoProject(azdProject, newDep) logServiceAddedAccordingToMavenDependency(newDep.ResourceDisplay(), targetGroupId, targetArtifactId) for property, name := range eventHubsNamePropertyMap { log.Printf(" Detected Event Hub [%s] for [%s] by analyzing property file.", property, name) @@ -224,6 +237,36 @@ func detectEventHubsAccordingToSpringCloudEventhubsStarterDependency( } } +func detectEventHubsAccordingToSpringIntegrationEventhubsMavenDependency( + azdProject *Project, springBootProject *SpringBootProject) { + var targetGroupId = "com.azure.spring" + var targetArtifactId = "spring-cloud-azure-starter-integration-eventhubs" + if hasDependency(springBootProject, targetGroupId, targetArtifactId) { + newDep := AzureDepEventHubs{ + // eventhubs name is empty here because no configured property + EventHubsNamePropertyMap: map[string]string{}, + DependencyTypes: []DependencyType{SpringIntegrationEventHubs}, + } + addAzureDepEventHubsIntoProject(azdProject, newDep) + logServiceAddedAccordingToMavenDependency(newDep.ResourceDisplay(), targetGroupId, targetArtifactId) + } +} + +func detectEventHubsAccordingToSpringMessagingEventhubsMavenDependency( + azdProject *Project, springBootProject *SpringBootProject) { + var targetGroupId = "com.azure.spring" + var targetArtifactId = "spring-messaging-azure-eventhubs" + if hasDependency(springBootProject, targetGroupId, targetArtifactId) { + newDep := AzureDepEventHubs{ + // eventhubs name is empty here because no configured property + EventHubsNamePropertyMap: map[string]string{}, + DependencyTypes: []DependencyType{SpringMessagingEventHubs}, + } + addAzureDepEventHubsIntoProject(azdProject, newDep) + logServiceAddedAccordingToMavenDependency(newDep.ResourceDisplay(), targetGroupId, targetArtifactId) + } +} + func detectEventHubsAccordingToSpringCloudStreamKafkaMavenDependency( azdProject *Project, springBootProject *SpringBootProject) { var targetGroupId = "org.springframework.cloud" @@ -232,10 +275,10 @@ func detectEventHubsAccordingToSpringCloudStreamKafkaMavenDependency( bindingDestinations := getBindingDestinationMap(springBootProject.applicationProperties) newDep := AzureDepEventHubs{ EventHubsNamePropertyMap: bindingDestinations, - UseKafka: true, SpringBootVersion: detectSpringBootVersion(springBootProject.pom), + DependencyTypes: []DependencyType{SpringCloudStreamKafka}, } - azdProject.AzureDeps = append(azdProject.AzureDeps, newDep) + addAzureDepEventHubsIntoProject(azdProject, newDep) logServiceAddedAccordingToMavenDependency(newDep.ResourceDisplay(), targetGroupId, targetArtifactId) for bindingName, destination := range bindingDestinations { log.Printf(" Detected Kafka Topic [%s] for binding [%s] by analyzing property file.", @@ -244,15 +287,52 @@ func detectEventHubsAccordingToSpringCloudStreamKafkaMavenDependency( } } +func detectEventHubsAccordingToSpringKafkaMavenDependency(azdProject *Project, springBootProject *SpringBootProject) { + var targetGroupId = "org.springframework.kafka" + var targetArtifactId = "spring-kafka" + if hasDependency(springBootProject, targetGroupId, targetArtifactId) { + newDep := AzureDepEventHubs{ + // eventhubs name is empty here because no configured property + EventHubsNamePropertyMap: map[string]string{}, + SpringBootVersion: detectSpringBootVersion(springBootProject.pom), + DependencyTypes: []DependencyType{SpringKafka}, + } + addAzureDepEventHubsIntoProject(azdProject, newDep) + logServiceAddedAccordingToMavenDependency(newDep.ResourceDisplay(), targetGroupId, targetArtifactId) + } +} + +func addAzureDepEventHubsIntoProject( + azdProject *Project, + newDep AzureDepEventHubs) { + for index, azureDep := range azdProject.AzureDeps { + if azureDep, ok := azureDep.(AzureDepEventHubs); ok { + // already have existing dependency + for property, eventHubsName := range newDep.EventHubsNamePropertyMap { + azureDep.EventHubsNamePropertyMap[property] = eventHubsName + } + azureDep.DependencyTypes = append(azureDep.DependencyTypes, newDep.DependencyTypes...) + azureDep.SpringBootVersion = newDep.SpringBootVersion + azdProject.AzureDeps[index] = azureDep + return + } + } + + // add new dependency + azdProject.AzureDeps = append(azdProject.AzureDeps, newDep) +} + func detectStorageAccount(azdProject *Project, springBootProject *SpringBootProject) { detectStorageAccountAccordingToSpringCloudStreamBinderMavenDependencyAndProperty(azdProject, springBootProject) + detectStorageAccountAccordingToSpringIntegrationEventhubsMavenDependencyAndProperty(azdProject, springBootProject) + detectStorageAccountAccordingToSpringMessagingEventhubsMavenDependencyAndProperty(azdProject, springBootProject) } func detectStorageAccountAccordingToSpringCloudStreamBinderMavenDependencyAndProperty( azdProject *Project, springBootProject *SpringBootProject) { var targetGroupId = "com.azure.spring" var targetArtifactId = "spring-cloud-azure-stream-binder-eventhubs" - var targetPropertyName = "spring.cloud.azure.eventhubs.processor.checkpoint-store.container-name" + var targetPropertyNameSuffix = "spring.cloud.azure.eventhubs.processor.checkpoint-store.container-name" if hasDependency(springBootProject, targetGroupId, targetArtifactId) { bindingDestinations := getBindingDestinationMap(springBootProject.applicationProperties) containsInBindingName := "" @@ -263,22 +343,53 @@ func detectStorageAccountAccordingToSpringCloudStreamBinderMavenDependencyAndPro } } if containsInBindingName != "" { - containerNamePropertyMap := make(map[string]string) - for key, value := range springBootProject.applicationProperties { - if strings.HasSuffix(key, targetPropertyName) { - containerNamePropertyMap[key] = value - } - } - newDep := AzureDepStorageAccount{ - ContainerNamePropertyMap: containerNamePropertyMap, - } - azdProject.AzureDeps = append(azdProject.AzureDeps, newDep) - logServiceAddedAccordingToMavenDependencyAndExtraCondition(newDep.ResourceDisplay(), targetGroupId, - targetArtifactId, "binding name ["+containsInBindingName+"] contains '-in-'") - for property, containerName := range containerNamePropertyMap { - log.Printf(" Detected Storage container name: [%s] for [%s] by analyzing property file.", - containerName, property) - } + detectStorageAccountAccordingToProperty(azdProject, springBootProject.applicationProperties, + targetGroupId, targetArtifactId, targetPropertyNameSuffix, + "binding name ["+containsInBindingName+"] contains '-in-'") + } + } +} + +func detectStorageAccountAccordingToSpringIntegrationEventhubsMavenDependencyAndProperty( + azdProject *Project, springBootProject *SpringBootProject) { + var targetGroupId = "com.azure.spring" + var targetArtifactId = "spring-cloud-azure-starter-integration-eventhubs" + var targetPropertyNameSuffix = "spring.cloud.azure.eventhubs.processor.checkpoint-store.container-name" + if hasDependency(springBootProject, targetGroupId, targetArtifactId) { + detectStorageAccountAccordingToProperty(azdProject, springBootProject.applicationProperties, + targetGroupId, targetArtifactId, targetPropertyNameSuffix, "") + } +} + +func detectStorageAccountAccordingToSpringMessagingEventhubsMavenDependencyAndProperty( + azdProject *Project, springBootProject *SpringBootProject) { + var targetGroupId = "com.azure.spring" + var targetArtifactId = "spring-messaging-azure-eventhubs" + var targetPropertyNameSuffix = "spring.cloud.azure.eventhubs.processor.checkpoint-store.container-name" + if hasDependency(springBootProject, targetGroupId, targetArtifactId) { + detectStorageAccountAccordingToProperty(azdProject, springBootProject.applicationProperties, + targetGroupId, targetArtifactId, targetPropertyNameSuffix, "") + } +} + +func detectStorageAccountAccordingToProperty(azdProject *Project, applicationProperties map[string]string, + targetGroupId string, targetArtifactId string, targetPropertyNameSuffix string, extraCondition string) { + containerNamePropertyMap := make(map[string]string) + for key, value := range applicationProperties { + if strings.HasSuffix(key, targetPropertyNameSuffix) { + containerNamePropertyMap[key] = value + } + } + if len(containerNamePropertyMap) > 0 { + newDep := AzureDepStorageAccount{ + ContainerNamePropertyMap: containerNamePropertyMap, + } + azdProject.AzureDeps = append(azdProject.AzureDeps, newDep) + logServiceAddedAccordingToMavenDependencyAndExtraCondition(newDep.ResourceDisplay(), targetGroupId, + targetArtifactId, extraCondition) + for property, containerName := range containerNamePropertyMap { + log.Printf(" Detected Storage container name: [%s] for [%s] by analyzing property file.", + containerName, property) } } } diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index 8f612e2592f..3e4f69b427f 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -6,7 +6,9 @@ import ( "maps" "os" "path/filepath" + "regexp" "slices" + "strconv" "strings" "time" @@ -141,7 +143,7 @@ func (i *Initializer) InitFromApp( for depIndex, dep := range prj.AzureDeps { if eventHubs, ok := dep.(appdetect.AzureDepEventHubs); ok { // prompt spring boot version if not detected for kafka - if eventHubs.UseKafka { + if eventHubs.UseKafka() { hasKafkaDep = true springBootVersion := eventHubs.SpringBootVersion if springBootVersion == appdetect.UnknownSpringBootVersion { @@ -154,6 +156,10 @@ func (i *Initializer) InitFromApp( } } // prompt event hubs name if not detected + if len(eventHubs.EventHubsNamePropertyMap) == 0 { + promptMissingEventHubsNameOrExit(i.console, ctx, &eventHubs) + prj.AzureDeps[depIndex] = eventHubs + } for property, eventHubsName := range eventHubs.EventHubsNamePropertyMap { if eventHubsName == "" { promptMissingPropertyAndExit(i.console, ctx, property) @@ -699,7 +705,7 @@ func (i *Initializer) prjConfigFromDetect( }, } case appdetect.AzureDepEventHubs: - if azureDep.UseKafka { + if azureDep.UseKafka() { config.Resources["kafka"] = &project.ResourceConfig{ Type: project.ResourceTypeMessagingKafka, Props: project.KafkaProps{ @@ -770,7 +776,7 @@ func (i *Initializer) prjConfigFromDetect( case appdetect.AzureDepServiceBus: resSpec.Uses = append(resSpec.Uses, "servicebus") case appdetect.AzureDepEventHubs: - if azureDep.UseKafka { + if azureDep.UseKafka() { resSpec.Uses = append(resSpec.Uses, "kafka") } else { resSpec.Uses = append(resSpec.Uses, "eventhubs") @@ -1088,7 +1094,7 @@ func processSpringCloudAzureDepByPrompt(console input.Console, ctx context.Conte // remove Kafka Azure Dep var result []appdetect.AzureDep for _, dep := range project.AzureDeps { - if eventHubs, ok := dep.(appdetect.AzureDepEventHubs); !(ok && eventHubs.UseKafka) { + if eventHubs, ok := dep.(appdetect.AzureDepEventHubs); !(ok && eventHubs.UseKafka()) { result = append(result, dep) } } @@ -1120,12 +1126,76 @@ func promptSpringBootVersion(console input.Console, ctx context.Context) (string } } +func promptMissingEventHubsNameOrExit(console input.Console, ctx context.Context, eventHubs *appdetect.AzureDepEventHubs) { + for _, dependencyType := range eventHubs.DependencyTypes { + switch dependencyType { + case appdetect.SpringIntegrationEventHubs, appdetect.SpringMessagingEventHubs, appdetect.SpringKafka: + eventHubsNames, err := promptEventHubsNames(console, ctx) + if err != nil { + console.Message(ctx, fmt.Sprintf("Error happened when prompt eventhubs name: %s.", err)) + os.Exit(-1) + } + for i, eventHubsName := range eventHubsNames { + propertyName := string(dependencyType) + strconv.Itoa(i) + eventHubs.EventHubsNamePropertyMap[propertyName] = eventHubsName + } + case appdetect.SpringCloudStreamEventHubs, appdetect.SpringCloudStreamKafka: + promptMissingPropertyAndExit(console, ctx, "spring.cloud.stream.bindings..destination") + os.Exit(0) + case appdetect.SpringCloudEventHubsStarter: + promptMissingPropertyAndExit(console, ctx, "spring.cloud.azure.eventhubs.event-hub-name or "+ + "spring.cloud.azure.eventhubs.[producer|consumer|processor].event-hub-name") + os.Exit(0) + } + } +} + func promptMissingPropertyAndExit(console input.Console, ctx context.Context, key string) { console.Message(ctx, fmt.Sprintf("No value was provided for %s. Please update the configuration file "+ "(like application.properties or application.yaml) with a valid value.", key)) os.Exit(0) } +// todo: delete this after we implement to detect eventhubs names from code +func promptEventHubsNames(console input.Console, ctx context.Context) ([]string, error) { + for { + eventHubsNamesInput, err := console.Prompt(ctx, input.ConsoleOptions{ + Message: "Input the names of Azure Event Hubs (not the namespace name), " + + "if you have multiple ones, separate with commas:", + Help: "Hint: Azure Event Hubs Name, not the namespace name", + }) + if err != nil { + return []string{}, err + } + eventHubsNames := strings.Split(eventHubsNamesInput, ",") + allValidEventHubsNames := true + for i, eventHubsName := range eventHubsNames { + eventHubsNames[i] = strings.TrimSpace(eventHubsName) + if !isValidEventhubsName(eventHubsNames[i]) { + console.Message(ctx, "Invalid eventhubs name. it should contain letters, numbers, periods (.), "+ + "hyphens (-), underscores (_), must begin and end with a letter or number. Please choose another name:") + allValidEventHubsNames = false + break + } + } + if allValidEventHubsNames { + return eventHubsNames, nil + } + } +} + +// contain letters, numbers, periods (.), hyphens (-), and underscores (_) +// must begin and end with a letter or number +var eventHubsNameRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]$`) + +func isValidEventhubsName(name string) bool { + // up to 256 characters + if len(name) == 0 || len(name) > 256 { + return false + } + return eventHubsNameRegex.MatchString(name) +} + func appendJavaEurekaServerEnv(svc *project.ServiceConfig, eurekaServerName string) error { if eurekaServerName == "" { // eureka server not found, maybe removed when detect confirm diff --git a/cli/azd/internal/repository/infra_confirm.go b/cli/azd/internal/repository/infra_confirm.go index dec8e4303c0..e76244ec8f9 100644 --- a/cli/azd/internal/repository/infra_confirm.go +++ b/cli/azd/internal/repository/infra_confirm.go @@ -355,7 +355,7 @@ func (i *Initializer) buildInfraSpecByAzureDep( spec.AzureEventHubs = &scaffold.AzureDepEventHubs{ EventHubNames: appdetect.DistinctValues(dependency.EventHubsNamePropertyMap), AuthType: authType, - UseKafka: dependency.UseKafka, + UseKafka: dependency.UseKafka(), SpringBootVersion: dependency.SpringBootVersion, } case appdetect.AzureDepStorageAccount: