Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
849de05
Add a new child action "orchestrate". And create a sample azure.yaml …
rujche Sep 6, 2024
acee60b
Get list of pom files.
rujche Sep 9, 2024
88d4d1b
add the code for azd java analyzer
saragluna Sep 10, 2024
6ba0816
Hook the java analyzer with the azd init.
saragluna Sep 11, 2024
6a4b664
Enhance
saragluna Sep 11, 2024
bafcbc4
Implement java_project_bicep_file_generator.go.
rujche Sep 11, 2024
b69a47e
Improve log: Add information about the file path.
rujche Sep 11, 2024
69bc59a
Enhance the java analyzer
saragluna Sep 13, 2024
8d67fc5
Add feature: Support add mysql when run "azd init".
rujche Sep 13, 2024
91900aa
Delete java_project_bicep_file_generator.go.
rujche Sep 13, 2024
2da438b
Add logic about accessing mysql in aca.
rujche Sep 14, 2024
f4e00ef
Merge pull request #1 from rujche/rujche/java-analyzer
saragluna Sep 14, 2024
2aa6cac
Fix typo by changing "DbMysql" to "DbMySql".
rujche Sep 14, 2024
cace7fa
Merge pull request #2 from rujche/rujche/java-analyzer
saragluna Sep 14, 2024
082b9b2
Use managed-identity instead of username and password. Now it has err…
rujche Sep 18, 2024
b0c39a6
Access MySql by managed identity instead of username&password.
rujche Sep 20, 2024
f90416f
Add Azure Deps in appdetect.go
saragluna Sep 23, 2024
84b7852
Customize the azd VS Code extension
saragluna Sep 23, 2024
6587f83
improve the java analyzer for event-driven
saragluna Sep 23, 2024
103a005
refactor the java analyzer
saragluna Sep 26, 2024
2e01347
Create service connector by bicep file.
rujche Sep 27, 2024
708681a
1. Remove duplicated 'azd extension add'.
rujche Sep 29, 2024
353a802
Update name of resources: linkerCreatorIdentity and appLinkToMySql
rujche Sep 29, 2024
6d853de
Merge remote-tracking branch 'saragluna/java-analyzer' into rujche/ja…
rujche Sep 29, 2024
85ec20b
Merge pull request #3 from rujche/rujche/java-analyzer
saragluna Sep 29, 2024
2856fb9
Merge remote-tracking branch 'saragluna/java-analyzer' into rujche/ja…
rujche Sep 29, 2024
b70878e
1. Add rule about postgresql in project_analyzer_java.go.
rujche Oct 7, 2024
437eeb6
Fix the error about CORS.
rujche Oct 7, 2024
3410c97
Merge pull request #4 from rujche/rujche/java-analyzer
saragluna Oct 8, 2024
f244293
Merge branch 'main' into saragluna/java-analyzer
rujche Oct 8, 2024
df2f5f2
add support for service bus
saragluna Oct 8, 2024
b6e6ecc
fix servicebus
saragluna Oct 8, 2024
073d857
Remove the logic of create tag after create service connector.
rujche Oct 8, 2024
ef73ce1
Merge remote-tracking branch 'saragluna/java-analyzer' into rujche/ja…
rujche Oct 8, 2024
4c84661
Merge pull request #5 from rujche/rujche/java-analyzer
rujche Oct 8, 2024
f1e2fc1
support both mi and connection string for service bus
saragluna Oct 9, 2024
23362f8
For PostgreSQL, support both password and passwordless.
rujche Oct 10, 2024
193f054
For MySQL, support both password and passwordless.
rujche Oct 11, 2024
48cbeb5
Remove logic of adding tag after creating service connector. Because …
rujche Oct 11, 2024
9f4fe27
Fix bug: create service connector only work for the first time run of…
rujche Oct 11, 2024
207ffa3
Merge pull request #6 from rujche/rujche/java-analyzer
rujche Oct 12, 2024
0594412
Add new feature: analyze project to add Mongo DB.
rujche Oct 12, 2024
c45382a
Delete unused content in main.bicept.
rujche Oct 12, 2024
dca325f
Merge pull request #7 from rujche/rujche/java-analyzer
rujche Oct 12, 2024
babf604
Fix bug: Get auth type should only be required for MySQL and PostgreSQL.
rujche Oct 14, 2024
12b169e
Merge pull request #8 from rujche/rujche/java-analyzer
rujche Oct 14, 2024
a2a3a73
Make sure app work well after deployed to ACA no matter what value "s…
rujche Oct 15, 2024
2f4d2e9
Merge pull request #9 from rujche/rujche/java-analyzer
rujche Oct 15, 2024
a83f7d7
Implement feature: detect port in Dockerfile.
rujche Oct 15, 2024
35f762c
Merge pull request #10 from rujche/rujche/java-analyzer
rujche Oct 15, 2024
de2e922
Implement feature: detect redis by analyzing pom file.
rujche Oct 15, 2024
8607043
Merge pull request #11 from rujche/rujche/java-analyzer
rujche Oct 15, 2024
a482496
Detect Mongo DB by dependency spring-boot-starter-data-mongodb-reacti…
rujche Oct 15, 2024
671902a
Merge pull request #12 from rujche/rujche/java-analyzer
rujche Oct 15, 2024
9c53e73
Support all kinds of properties file like application(-profile).yaml(…
rujche Oct 16, 2024
6e14a1b
Merge pull request #13 from rujche/rujche/java-analyzer
rujche Oct 16, 2024
6c117ee
Merge the java analyzer code from the main branch to sjad (#1)
saragluna Oct 22, 2024
ea4083f
Merge branch 'main' into feature/sjad
saragluna Oct 24, 2024
b44c961
Merge branch 'main' into feature/sjad
saragluna Oct 25, 2024
f8a10da
remove the orchestrate command (#2)
saragluna Oct 25, 2024
41cc716
Support detect Azure Event Hubs: produce message only, managed identi…
rujche Oct 25, 2024
b5d6dee
Support detect Azure Event Hubs: produce message only. Try to connect…
rujche Oct 25, 2024
241fbe0
Support detect Azure Event Hubs: produce message only, support both m…
rujche Oct 28, 2024
1ef0532
Change option from "Password" to "Connection string".
rujche Oct 28, 2024
f65ffd6
Rename "getAuthTypeByPrompt" to "chooseAuthType".
rujche Oct 28, 2024
a6e2da5
fix ut
saragluna Oct 28, 2024
d1cd151
fix ut
saragluna Oct 29, 2024
a8f91b1
Support detecting event hubs by analyzing pom.xml and application.yml…
rujche Oct 29, 2024
487cae1
Support detect Azure Event Hubs: both produce and consume, support ma…
rujche Oct 30, 2024
5a936f5
Merge remote-tracking branch 'azure-javaee/feature/sjad' into feature…
rujche Oct 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/azd/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ func NewRootCmd(
Command: logout,
ActionResolver: newLogoutAction,
})

root.Add("init", &actions.ActionDescriptorOptions{
Command: newInitCmd(),
FlagsResolver: newInitFlags,
Expand Down
33 changes: 33 additions & 0 deletions cli/azd/internal/appdetect/appdetect.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,36 @@ func (db DatabaseDep) Display() string {
return ""
}

//type AzureDep string

type AzureDep interface {
ResourceDisplay() string
}

type AzureDepServiceBus struct {
Queues []string
}

func (a AzureDepServiceBus) ResourceDisplay() string {
return "Azure Service Bus"
}

type AzureDepEventHubs struct {
Names []string
}

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
Expand All @@ -141,6 +171,9 @@ type Project struct {
// Experimental: Database dependencies inferred through heuristics while scanning dependencies in the project.
DatabaseDeps []DatabaseDep

// Experimental: Azure dependencies inferred through heuristics while scanning dependencies in the project.
AzureDeps []AzureDep

// The path to the project directory.
Path string

Expand Down
26 changes: 26 additions & 0 deletions cli/azd/internal/appdetect/appdetect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ func TestDetect(t *testing.T) {
Path: "java-multimodules/application",
DetectionRule: "Inferred by presence of: pom.xml",
DatabaseDeps: []DatabaseDep{
DbMongo,
DbMySql,
DbPostgres,
DbRedis,
},
},
{
Expand Down Expand Up @@ -130,8 +132,10 @@ func TestDetect(t *testing.T) {
Path: "java-multimodules/application",
DetectionRule: "Inferred by presence of: pom.xml",
DatabaseDeps: []DatabaseDep{
DbMongo,
DbMySql,
DbPostgres,
DbRedis,
},
},
{
Expand Down Expand Up @@ -163,8 +167,10 @@ func TestDetect(t *testing.T) {
Path: "java-multimodules/application",
DetectionRule: "Inferred by presence of: pom.xml",
DatabaseDeps: []DatabaseDep{
DbMongo,
DbMySql,
DbPostgres,
DbRedis,
},
},
{
Expand Down Expand Up @@ -199,8 +205,10 @@ func TestDetect(t *testing.T) {
Path: "java-multimodules/application",
DetectionRule: "Inferred by presence of: pom.xml",
DatabaseDeps: []DatabaseDep{
DbMongo,
DbMySql,
DbPostgres,
DbRedis,
},
},
{
Expand Down Expand Up @@ -279,6 +287,24 @@ func TestDetectNested(t *testing.T) {
})
}

func TestAnalyzeJavaSpringProject(t *testing.T) {
var properties = readProperties(filepath.Join("testdata", "java-spring", "project-one"))
require.Equal(t, "", properties["not.exist"])
require.Equal(t, "jdbc:h2:mem:testdb", properties["spring.datasource.url"])

properties = readProperties(filepath.Join("testdata", "java-spring", "project-two"))
require.Equal(t, "", properties["not.exist"])
require.Equal(t, "jdbc:h2:mem:testdb", properties["spring.datasource.url"])

properties = readProperties(filepath.Join("testdata", "java-spring", "project-three"))
require.Equal(t, "", properties["not.exist"])
require.Equal(t, "HTML", properties["spring.thymeleaf.mode"])

properties = readProperties(filepath.Join("testdata", "java-spring", "project-four"))
require.Equal(t, "", properties["not.exist"])
require.Equal(t, "mysql", properties["database"])
}

func copyTestDataDir(glob string, dst string) error {
root := "testdata"
return fs.WalkDir(testDataFs, root, func(name string, d fs.DirEntry, err error) error {
Expand Down
200 changes: 200 additions & 0 deletions cli/azd/internal/appdetect/java.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package appdetect

import (
"bufio"
"context"
"encoding/xml"
"fmt"
"github.com/azure/azure-dev/cli/azd/pkg/osutil"
"github.com/braydonk/yaml"
"io/fs"
"log"
"maps"
"os"
"path/filepath"
Expand Down Expand Up @@ -121,6 +125,24 @@ 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
isSpringBoot := false
if mavenProject.Parent.GroupId == "org.springframework.boot" && mavenProject.Parent.ArtifactId == "spring-boot-starter-parent" {
isSpringBoot = true
}
for _, dep := range mavenProject.Dependencies {
if dep.GroupId == "org.springframework.boot" && strings.HasPrefix(dep.ArtifactId, "spring-boot-starter") {
isSpringBoot = true
break
}
}
applicationProperties := make(map[string]string)
if isSpringBoot {
applicationProperties = readProperties(project.Path)
}

databaseDepMap := map[DatabaseDep]struct{}{}
for _, dep := range mavenProject.Dependencies {
if dep.GroupId == "com.mysql" && dep.ArtifactId == "mysql-connector-j" {
Expand All @@ -130,6 +152,55 @@ func detectDependencies(mavenProject *mavenProject, project *Project) (*Project,
if dep.GroupId == "org.postgresql" && dep.ArtifactId == "postgresql" {
databaseDepMap[DbPostgres] = struct{}{}
}

if dep.GroupId == "org.springframework.boot" && dep.ArtifactId == "spring-boot-starter-data-redis" {
databaseDepMap[DbRedis] = struct{}{}
}
if dep.GroupId == "org.springframework.boot" && dep.ArtifactId == "spring-boot-starter-data-redis-reactive" {
databaseDepMap[DbRedis] = struct{}{}
}

if dep.GroupId == "org.springframework.boot" && dep.ArtifactId == "spring-boot-starter-data-mongodb" {
databaseDepMap[DbMongo] = struct{}{}
}
if dep.GroupId == "org.springframework.boot" && dep.ArtifactId == "spring-boot-starter-data-mongodb-reactive" {
databaseDepMap[DbMongo] = struct{}{}
}

if dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-stream-binder-servicebus" {
bindingDestinations := findBindingDestinations(applicationProperties)
destinations := make([]string, 0, len(bindingDestinations))
for bindingName, destination := range bindingDestinations {
destinations = append(destinations, destination)
log.Printf("Service Bus queue [%s] found for binding [%s]", destination, bindingName)
}
project.AzureDeps = append(project.AzureDeps, AzureDepServiceBus{
Queues: destinations,
})
}

if dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-stream-binder-eventhubs" {
bindingDestinations := findBindingDestinations(applicationProperties)
var destinations []string
containsInBinding := false
for bindingName, destination := range bindingDestinations {
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"]},
})
}
}
}

if len(databaseDepMap) > 0 {
Expand All @@ -141,3 +212,132 @@ func detectDependencies(mavenProject *mavenProject, project *Project) (*Project,

return project, nil
}

func readProperties(projectPath string) map[string]string {
// todo: do we need to consider the bootstrap.properties
result := make(map[string]string)
readPropertiesInPropertiesFile(filepath.Join(projectPath, "/src/main/resources/application.properties"), result)
readPropertiesInYamlFile(filepath.Join(projectPath, "/src/main/resources/application.yml"), result)
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)
readPropertiesInYamlFile(filepath.Join(projectPath, "/src/main/resources/application-"+profile+".yml"), result)
readPropertiesInYamlFile(filepath.Join(projectPath, "/src/main/resources/application-"+profile+".yaml"), result)
}
return result
}

func readPropertiesInYamlFile(yamlFilePath string, result map[string]string) {
if !osutil.FileExists(yamlFilePath) {
return
}
data, err := os.ReadFile(yamlFilePath)
if err != nil {
log.Fatalf("error reading YAML file: %v", err)
return
}

// Parse the YAML into a yaml.Node
var root yaml.Node
err = yaml.Unmarshal(data, &root)
if err != nil {
log.Fatalf("error unmarshalling YAML: %v", err)
return
}

parseYAML("", &root, result)
}

// Recursively parse the YAML and build dot-separated keys into a map
func parseYAML(prefix string, node *yaml.Node, result map[string]string) {
switch node.Kind {
case yaml.DocumentNode:
// Process each document's content
for _, contentNode := range node.Content {
parseYAML(prefix, contentNode, result)
}
case yaml.MappingNode:
// Process key-value pairs in a map
for i := 0; i < len(node.Content); i += 2 {
keyNode := node.Content[i]
valueNode := node.Content[i+1]

// Ensure the key is a scalar
if keyNode.Kind != yaml.ScalarNode {
continue
}

keyStr := keyNode.Value
newPrefix := keyStr
if prefix != "" {
newPrefix = prefix + "." + keyStr
}
parseYAML(newPrefix, valueNode, result)
}
case yaml.SequenceNode:
// Process items in a sequence (list)
for i, item := range node.Content {
newPrefix := fmt.Sprintf("%s[%d]", prefix, i)
parseYAML(newPrefix, item, result)
}
case yaml.ScalarNode:
// If it's a scalar value, add it to the result map
result[prefix] = node.Value
default:
// Handle other node types if necessary
}
}

func readPropertiesInPropertiesFile(propertiesFilePath string, result map[string]string) {
if !osutil.FileExists(propertiesFilePath) {
return
}
file, err := os.Open(propertiesFilePath)
if err != nil {
log.Fatalf("error opening properties file: %v", err)
return
}
defer file.Close()

scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.TrimSpace(line) == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
result[key] = value
}
}
}

// Function to find all properties that match the pattern `spring.cloud.stream.bindings.<binding-name>.destination`
func findBindingDestinations(properties map[string]string) map[string]string {
result := make(map[string]string)

// Iterate through the properties map and look for matching keys
for key, value := range properties {
// Check if the key matches the pattern `spring.cloud.stream.bindings.<binding-name>.destination`
if strings.HasPrefix(key, "spring.cloud.stream.bindings.") && strings.HasSuffix(key, ".destination") {
// Extract the binding name
bindingName := key[len("spring.cloud.stream.bindings.") : len(key)-len(".destination")]
// Store the binding name and destination value
result[bindingName] = fmt.Sprintf("%v", value)
}
}

return result
}

func contains(array []string, str string) bool {
for _, v := range array {
if v == str {
return true
}
}
return false
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>

<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

<dependency>
<groupId>org.postgresql</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# database init, supports mysql too
database=mysql
spring.datasource.url=jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:petclinic}
spring.datasource.username=${MYSQL_USERNAME:petclinic}
spring.datasource.password=${MYSQL_PASSWORD:}
# SQL is written to be idempotent so this is safe
spring.sql.init.mode=always
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
database=postgres
spring.datasource.url=jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_HOST:5432}/${POSTGRES_DATABASE:petclinic}
spring.datasource.username=${POSTGRES_USERNAME:petclinic}
spring.datasource.password=${POSTGRES_PASSWORD:}
# SQL is written to be idempotent so this is safe
spring.sql.init.mode=always
Loading