diff --git a/cli/azd/.vscode/cspell.yaml b/cli/azd/.vscode/cspell.yaml index 9364122e8f6..e886a88f93c 100644 --- a/cli/azd/.vscode/cspell.yaml +++ b/cli/azd/.vscode/cspell.yaml @@ -35,6 +35,13 @@ overrides: - cloudapp - mediaservices - msecnd + - filename: internal/tracing/fields/fields.go + words: + - azuredeps + - filename: internal/appdetect/java.go + words: + - springframework + - eventhubs - filename: docs/docgen.go words: - alexwolf diff --git a/cli/azd/cmd/root.go b/cli/azd/cmd/root.go index e397aa61fc4..14c98cb7beb 100644 --- a/cli/azd/cmd/root.go +++ b/cli/azd/cmd/root.go @@ -177,7 +177,7 @@ func NewRootCmd( Command: logout, ActionResolver: newLogoutAction, }) - + root.Add("init", &actions.ActionDescriptorOptions{ Command: newInitCmd(), FlagsResolver: newInitFlags, diff --git a/cli/azd/internal/appdetect/appdetect.go b/cli/azd/internal/appdetect/appdetect.go index bb8053061a3..aff6c7a3328 100644 --- a/cli/azd/internal/appdetect/appdetect.go +++ b/cli/azd/internal/appdetect/appdetect.go @@ -153,6 +153,14 @@ func (a AzureDepEventHubs) ResourceDisplay() string { return "Azure Event Hubs" } +type AzureDepStorageAccount struct { + ContainerNames []string +} + +func (a AzureDepStorageAccount) ResourceDisplay() string { + return "Azure Storage Account" +} + 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 6d7bbd4dd05..71ce8936335 100644 --- a/cli/azd/internal/appdetect/java.go +++ b/cli/azd/internal/appdetect/java.go @@ -127,9 +127,11 @@ func readMavenProject(filePath string) (*mavenProject, error) { func detectDependencies(mavenProject *mavenProject, project *Project) (*Project, error) { // how can we tell it's a Spring Boot project? // 1. It has a parent with a groupId of org.springframework.boot and an artifactId of spring-boot-starter-parent - // 2. It has a dependency with a groupId of org.springframework.boot and an artifactId that starts with spring-boot-starter + // 2. It has a dependency with a groupId of org.springframework.boot and an artifactId that starts with + // spring-boot-starter isSpringBoot := false - if mavenProject.Parent.GroupId == "org.springframework.boot" && mavenProject.Parent.ArtifactId == "spring-boot-starter-parent" { + if mavenProject.Parent.GroupId == "org.springframework.boot" && + mavenProject.Parent.ArtifactId == "spring-boot-starter-parent" { isSpringBoot = true } for _, dep := range mavenProject.Dependencies { @@ -181,14 +183,26 @@ func detectDependencies(mavenProject *mavenProject, project *Project) (*Project, if dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-stream-binder-eventhubs" { bindingDestinations := findBindingDestinations(applicationProperties) - destinations := make([]string, 0, len(bindingDestinations)) + var destinations []string + containsInBinding := false for bindingName, destination := range bindingDestinations { - destinations = append(destinations, destination) - log.Printf("Event Hubs [%s] found for binding [%s]", destination, bindingName) + if strings.Contains(bindingName, "-in-") { // Example: consume-in-0 + containsInBinding = true + } + if !contains(destinations, destination) { + destinations = append(destinations, destination) + log.Printf("Event Hubs [%s] found for binding [%s]", destination, bindingName) + } } project.AzureDeps = append(project.AzureDeps, AzureDepEventHubs{ Names: destinations, }) + if containsInBinding { + project.AzureDeps = append(project.AzureDeps, AzureDepStorageAccount{ + ContainerNames: []string{ + applicationProperties["spring.cloud.azure.eventhubs.processor.checkpoint-store.container-name"]}, + }) + } } } @@ -210,7 +224,8 @@ func readProperties(projectPath string) map[string]string { readPropertiesInYamlFile(filepath.Join(projectPath, "/src/main/resources/application.yaml"), result) profile, profileSet := result["spring.profiles.active"] if profileSet { - readPropertiesInPropertiesFile(filepath.Join(projectPath, "/src/main/resources/application-"+profile+".properties"), result) + readPropertiesInPropertiesFile( + filepath.Join(projectPath, "/src/main/resources/application-"+profile+".properties"), result) readPropertiesInYamlFile(filepath.Join(projectPath, "/src/main/resources/application-"+profile+".yml"), result) readPropertiesInYamlFile(filepath.Join(projectPath, "/src/main/resources/application-"+profile+".yaml"), result) } @@ -321,3 +336,12 @@ func findBindingDestinations(properties map[string]string) map[string]string { return result } + +func contains(array []string, str string) bool { + for _, v := range array { + if v == str { + return true + } + } + return false +} diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index f4b098b4440..2ea0c0b942c 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -40,8 +40,9 @@ var dbMap = map[appdetect.DatabaseDep]struct{}{ } var azureDepMap = map[string]struct{}{ - appdetect.AzureDepServiceBus{}.ResourceDisplay(): {}, - appdetect.AzureDepEventHubs{}.ResourceDisplay(): {}, + appdetect.AzureDepServiceBus{}.ResourceDisplay(): {}, + appdetect.AzureDepEventHubs{}.ResourceDisplay(): {}, + appdetect.AzureDepStorageAccount{}.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 f41f55029e6..342b2815ffa 100644 --- a/cli/azd/internal/repository/infra_confirm.go +++ b/cli/azd/internal/repository/infra_confirm.go @@ -214,6 +214,8 @@ func (i *Initializer) infraSpecFromDetect( serviceSpec.AzureServiceBus = spec.AzureServiceBus case appdetect.AzureDepEventHubs: serviceSpec.AzureEventHubs = spec.AzureEventHubs + case appdetect.AzureDepStorageAccount: + serviceSpec.AzureStorageAccount = spec.AzureStorageAccount } } spec.Services = append(spec.Services, serviceSpec) @@ -346,26 +348,36 @@ azureDepPrompt: } } + authType := scaffold.AuthType(0) switch azureDep.(type) { case appdetect.AzureDepServiceBus: - authType, err := i.chooseAuthType(ctx, azureDepName) + _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.", + }) if err != nil { return err } - spec.AzureServiceBus = &scaffold.AzureDepServiceBus{ - Name: azureDepName, - Queues: azureDep.(appdetect.AzureDepServiceBus).Queues, - AuthUsingConnectionString: authType == scaffold.AuthType_PASSWORD, - AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, + + if _authType != "1" && _authType != "2" { + i.console.Message(ctx, "Invalid authentication type. Please enter 0 or 1.") + continue azureDepPrompt } - case appdetect.AzureDepEventHubs: - authType, err := i.chooseAuthType(ctx, azureDepName) - if err != nil { - return err + if _authType == "1" { + authType = scaffold.AuthType_PASSWORD + } else { + authType = scaffold.AuthType_TOKEN_CREDENTIAL } - spec.AzureEventHubs = &scaffold.AzureDepEventHubs{ + } + + switch dependency := azureDep.(type) { + case appdetect.AzureDepServiceBus: + spec.AzureServiceBus = &scaffold.AzureDepServiceBus{ Name: azureDepName, - EventHubNames: azureDep.(appdetect.AzureDepEventHubs).Names, + Queues: dependency.Queues, AuthUsingConnectionString: authType == scaffold.AuthType_PASSWORD, AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, } @@ -375,22 +387,3 @@ 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 cb8b7d921a3..7d60cb22680 100644 --- a/cli/azd/internal/scaffold/spec.go +++ b/cli/azd/internal/scaffold/spec.go @@ -15,10 +15,9 @@ type InfraSpec struct { DbCosmosMongo *DatabaseCosmosMongo DbRedis *DatabaseRedis - // Azure Service Bus - AzureServiceBus *AzureDepServiceBus - // Azure EventHubs - AzureEventHubs *AzureDepEventHubs + AzureServiceBus *AzureDepServiceBus + AzureEventHubs *AzureDepEventHubs + AzureStorageAccount *AzureDepStorageAccount } type Parameter struct { @@ -64,6 +63,13 @@ type AzureDepEventHubs struct { AuthUsingManagedIdentity bool } +type AzureDepStorageAccount struct { + Name string + ContainerNames []string + AuthUsingConnectionString bool + AuthUsingManagedIdentity bool +} + // AuthType defines different authentication types. type AuthType int32 @@ -91,10 +97,9 @@ type ServiceSpec struct { DbCosmosMongo *DatabaseReference DbRedis *DatabaseReference - // Azure Service Bus - AzureServiceBus *AzureDepServiceBus - // Azure Service Bus - AzureEventHubs *AzureDepEventHubs + AzureServiceBus *AzureDepServiceBus + AzureEventHubs *AzureDepEventHubs + AzureStorageAccount *AzureDepStorageAccount } type Frontend struct { diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index 5d1ab06e69b..b1b655333ee 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -167,6 +167,41 @@ module eventHubsConnectionString './modules/set-event-hubs-namespace-connection- } {{end}} {{end}} +{{- if .AzureStorageAccount }} +var storageAccountName = '${abbrs.storageStorageAccounts}${resourceToken}' +module storageAccount 'br/public:avm/res/storage/storage-account:0.14.3' = { + name: 'storageAccount' + params: { + name: storageAccountName + publicNetworkAccess: 'Enabled' + blobServices: { + containers: [ + {{- range $index, $element := .AzureStorageAccount.ContainerNames}} + { + name: '{{ $element }}' + } + {{- end}} + ] + } + location: location + roleAssignments: [ + {{- if (and .AzureStorageAccount .AzureStorageAccount.AuthUsingManagedIdentity) }} + {{- range .Services}} + { + principalId: {{bicepName .Name}}Identity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b') + } + {{- end}} + {{- end}} + ] + networkAcls: { + defaultAction: 'Allow' + } + tags: tags + } +} +{{end}} {{- range .Services}} module {{bicepName .Name}}Identity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { @@ -363,6 +398,22 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { value: '' } {{- end}} + {{- if .AzureStorageAccount }} + { + name: 'SPRING_CLOUD_AZURE_EVENTHUBS_PROCESSOR_CHECKPOINTSTORE_ACCOUNTNAME' + value: storageAccountName + } + {{- end}} + {{- if (and .AzureStorageAccount .AzureStorageAccount.AuthUsingManagedIdentity) }} + { + name: 'SPRING_CLOUD_AZURE_STORAGE_CREDENTIAL_MANAGEDIDENTITYENABLED' + value: 'true' + } + { + name: 'SPRING_CLOUD_AZURE_STORAGE_CREDENTIAL_CLIENTID' + value: {{bicepName .Name}}Identity.outputs.clientId + } + {{- end}} {{- if .Frontend}} {{- range $i, $e := .Frontend.Backends}} {