From 34d2b106efc57ccda2340e8d8b4ae2ce4ce62295 Mon Sep 17 00:00:00 2001 From: rujche Date: Fri, 17 Jan 2025 22:07:46 +0800 Subject: [PATCH 01/19] New feature: Update spring boot properties file for postgresql. --- cli/azd/internal/appdetect/java.go | 8 +- .../appdetect/spring_boot_properties_file.go | 179 ++++++++++++++++++ .../spring_boot_properties_file_test.go | 140 ++++++++++++++ 3 files changed, 326 insertions(+), 1 deletion(-) create mode 100644 cli/azd/internal/appdetect/spring_boot_properties_file.go create mode 100644 cli/azd/internal/appdetect/spring_boot_properties_file_test.go diff --git a/cli/azd/internal/appdetect/java.go b/cli/azd/internal/appdetect/java.go index fe6fec3ea65..2bff79ce642 100644 --- a/cli/azd/internal/appdetect/java.go +++ b/cli/azd/internal/appdetect/java.go @@ -127,8 +127,14 @@ func detectDependencies(mavenProject *mavenProject, project *Project) (*Project, databaseDepMap[DbMySql] = struct{}{} } - if dep.GroupId == "org.postgresql" && dep.ArtifactId == "postgresql" { + if (dep.GroupId == "org.postgresql" && dep.ArtifactId == "postgresql") || + (dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-starter-jdbc-postgresql") { databaseDepMap[DbPostgres] = struct{}{} + // todo: Only call this func when it's a spring-boot project + err := addPostgresqlConnectionProperties(project.Path) + if err != nil { + return nil, err + } } } diff --git a/cli/azd/internal/appdetect/spring_boot_properties_file.go b/cli/azd/internal/appdetect/spring_boot_properties_file.go new file mode 100644 index 00000000000..39de7ecf356 --- /dev/null +++ b/cli/azd/internal/appdetect/spring_boot_properties_file.go @@ -0,0 +1,179 @@ +package appdetect + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strings" +) + +type property struct { + key string + value string +} +type propertyMergeFunc func(string, string) string + +const azureProfileName = "azure" + +const placeholderPostgresUrl = "jdbc:${POSTGRES_URL}" // todo: manage placeholders here and in resources.bicept together. +const placeholderPostgresUsername = "${POSTGRES_USERNAME}" +const placeholderPostgresPassword = "${POSTGRES_PASSWORD}" + +var postgresqlProperties = []property{ + {"spring.datasource.url", placeholderPostgresUrl}, + {"spring.datasource.username", placeholderPostgresUsername}, + {"spring.datasource.password", placeholderPostgresPassword}, +} + +var applicationPropertiesRelativePath = filepath.Join("src", "main", "resources", + "application.properties") +var applicationAzurePropertiesRelativePath = filepath.Join("src", "main", "resources", + "application-"+azureProfileName+".properties") + +// todo: support other file suffix. Example: application.yml, application.yaml +func addPostgresqlConnectionProperties(projectPath string) error { + err := addPostgresqlConnectionPropertiesIntoPropertyFile(projectPath) + if err != nil { + return err + } + return activeAzureProfile(projectPath) +} + +func addPostgresqlConnectionPropertiesIntoPropertyFile(projectPath string) error { + filePath := filepath.Join(projectPath, applicationAzurePropertiesRelativePath) + return updatePropertyFile(filePath, postgresqlProperties, keepNewValue) +} + +func keepNewValue(_ string, newValue string) string { + return newValue +} + +func activeAzureProfile(projectPath string) error { + filePath := filepath.Join(projectPath, applicationPropertiesRelativePath) + var newProperties = []property{ + {"spring.profiles.active", azureProfileName}, + } + return updatePropertyFile(filePath, newProperties, appendIfExists) +} + +func appendIfExists(originalValue string, newValue string) string { + if originalValue == "" { + return newValue + } + originalValues := strings.SplitN(originalValue, ",", -1) + if contains(originalValues, azureProfileName) { + return originalValue + } + return originalValue + "," + newValue +} + +func contains(a []string, x string) bool { + for _, n := range a { + if x == n { + return true + } + } + return false +} + +func updatePropertyFile(filePath string, newProperties []property, function propertyMergeFunc) error { + err := createFileIfNotExist(filePath) + if err != nil { + return err + } + properties, err := readProperties(filePath) + if err != nil { + return err + } + properties = updateProperties(properties, newProperties, function) + err = writeProperties(filePath, properties) + if err != nil { + return err + } + return nil +} + +func createFileIfNotExist(filePath string) error { + dir := filepath.Dir(filePath) + if _, err := os.Stat(dir); os.IsNotExist(err) { + if err := os.MkdirAll(dir, os.ModePerm); err != nil { + return fmt.Errorf("failed to create directory: %v", err) + } + } + if _, err := os.Stat(filePath); os.IsNotExist(err) { + file, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("failed to create file: %v", err) + } + defer file.Close() + } + return nil +} + +func readProperties(filePath string) ([]property, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer file.Close() + + var properties []property + 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]) + properties = append(properties, property{key, value}) + } + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return properties, nil +} + +func updateProperties(properties []property, + newProperties []property, function propertyMergeFunc) []property { + for _, newProperty := range newProperties { + if index := getKeyIndex(properties, newProperty.key); index != -1 { + properties[index].value = function(properties[index].value, newProperty.value) + } else { + properties = append(properties, newProperty) + } + } + return properties +} + +func getKeyIndex(properties []property, key string) int { + for i, prop := range properties { + if prop.key == key { + return i + } + } + return -1 +} + +func writeProperties(filePath string, properties []property) error { + file, err := os.Create(filePath) + if err != nil { + return err + } + defer file.Close() + + writer := bufio.NewWriter(file) + for _, p := range properties { + _, err := writer.WriteString(fmt.Sprintf("%s=%s\n", p.key, p.value)) + if err != nil { + return err + } + } + return writer.Flush() +} diff --git a/cli/azd/internal/appdetect/spring_boot_properties_file_test.go b/cli/azd/internal/appdetect/spring_boot_properties_file_test.go new file mode 100644 index 00000000000..4ec950f06d2 --- /dev/null +++ b/cli/azd/internal/appdetect/spring_boot_properties_file_test.go @@ -0,0 +1,140 @@ +package appdetect + +import ( + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +const postgresPropertiesOriginalContent = `spring.datasource.url=jdbc:postgresql://localhost:5432/dbname?sslmode=require +spring.datasource.username=admin +spring.datasource.password=secret +` + +const postgresPropertiesUpdatedContent = `spring.datasource.url=` + placeholderPostgresUrl + ` +spring.datasource.username=` + placeholderPostgresUsername + ` +spring.datasource.password=` + placeholderPostgresPassword + ` +` + +var postgresPropertiesOriginalMap = []property{ + {"spring.datasource.url", "jdbc:postgresql://localhost:5432/dbname?sslmode=require"}, + {"spring.datasource.username", "admin"}, + {"spring.datasource.password", "secret"}, +} + +func TestAddPostgresqlConnectionProperties(t *testing.T) { + tests := []struct { + name string + inputApplicationPropertiesContent string + inputApplicationAzurePropertiesContent string + outputApplicationPropertiesContent string + outputApplicationAzurePropertiesContent string + }{ + { + name: "no content", + inputApplicationPropertiesContent: "", + inputApplicationAzurePropertiesContent: "", + outputApplicationPropertiesContent: "spring.profiles.active=" + azureProfileName + "\n", + outputApplicationAzurePropertiesContent: postgresPropertiesUpdatedContent, + }, + { + name: "override original content", + inputApplicationPropertiesContent: "spring.profiles.active=" + azureProfileName, + inputApplicationAzurePropertiesContent: postgresPropertiesOriginalContent, + outputApplicationPropertiesContent: "spring.profiles.active=" + azureProfileName + "\n", + outputApplicationAzurePropertiesContent: postgresPropertiesUpdatedContent, + }, + { + name: "append original content", + inputApplicationPropertiesContent: "aaa=xxx", + inputApplicationAzurePropertiesContent: "bbb=yyy\n" + postgresPropertiesOriginalContent, + outputApplicationPropertiesContent: "aaa=xxx\n" + "spring.profiles.active=" + azureProfileName + "\n", + outputApplicationAzurePropertiesContent: "bbb=yyy\n" + postgresPropertiesUpdatedContent, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tempDir := t.TempDir() + applicationPropertiesPath := filepath.Join(tempDir, applicationPropertiesRelativePath) + createFileIfContentIsNotEmpty(t, applicationPropertiesPath, tt.inputApplicationPropertiesContent) + applicationAzurePropertiesPath := filepath.Join(tempDir, applicationAzurePropertiesRelativePath) + createFileIfContentIsNotEmpty(t, applicationAzurePropertiesPath, tt.inputApplicationAzurePropertiesContent) + err := addPostgresqlConnectionProperties(tempDir) + assert.NoError(t, err) + assertFileContent(t, applicationPropertiesPath, tt.outputApplicationPropertiesContent) + assertFileContent(t, applicationAzurePropertiesPath, tt.outputApplicationAzurePropertiesContent) + }) + } +} + +func createFileIfContentIsNotEmpty(t *testing.T, path string, content string) { + if content == "" { + return + } + + dir := filepath.Dir(path) + if _, err := os.Stat(dir); os.IsNotExist(err) { + err := os.MkdirAll(dir, os.ModePerm) + assert.NoError(t, err) + } + file, err := os.Create(path) + assert.NoError(t, err) + defer file.Close() + err = os.WriteFile(path, []byte(content), 0600) + assert.NoError(t, err) +} + +func assertFileContent(t *testing.T, path string, content string) { + actualContent, err := os.ReadFile(path) + assert.NoError(t, err) + assert.Equal(t, content, string(actualContent)) +} + +func TestReadProperties(t *testing.T) { + tempFile, err := os.CreateTemp("", "test.properties") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tempFile.Name()) + + if _, err := tempFile.Write([]byte(postgresPropertiesOriginalContent)); err != nil { + t.Fatal(err) + } + if err := tempFile.Close(); err != nil { + t.Fatal(err) + } + + properties, err := readProperties(tempFile.Name()) + if err != nil { + t.Fatalf("readProperties() error = %v", err) + } + + if !reflect.DeepEqual(properties, postgresPropertiesOriginalMap) { + t.Errorf("readProperties() = %v, want %v", properties, postgresPropertiesOriginalMap) + } +} + +func TestWriteProperties(t *testing.T) { + tempFile, err := os.CreateTemp("", "test.properties") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tempFile.Name()) + + if err := writeProperties(tempFile.Name(), postgresPropertiesOriginalMap); err != nil { + t.Fatalf("writeProperties() error = %v", err) + } + + content, err := os.ReadFile(tempFile.Name()) + if err != nil { + t.Fatal(err) + } + + if string(content) != postgresPropertiesOriginalContent { + t.Errorf("writeProperties() = %v, want %v", string(content), postgresPropertiesOriginalContent) + } +} From 2c287fb025130f207fd19685f5ea480e2e7ef5f0 Mon Sep 17 00:00:00 2001 From: rujche Date: Fri, 17 Jan 2025 22:27:07 +0800 Subject: [PATCH 02/19] Fix error caused by container app name too long. --- cli/azd/resources/scaffold/templates/resources.bicept | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index a22399694f6..a4c3e689007 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -199,7 +199,7 @@ module {{bicepName .Name}}FetchLatestImage './modules/fetch-container-image.bice name: '{{bicepName .Name}}-fetch-image' params: { exists: {{bicepName .Name}}Exists - name: '{{.Name}}' + name: '{{containerAppName .Name}}' } } @@ -217,7 +217,7 @@ var {{bicepName .Name}}Env = map(filter({{bicepName .Name}}AppSettingsArray, i = module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { name: '{{bicepName .Name}}' params: { - name: '{{.Name}}' + name: '{{containerAppName .Name}}' {{- if ne .Port 0}} ingressTargetPort: {{.Port}} {{- end}} @@ -353,7 +353,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { {{- range $i, $e := .Frontend.Backends}} { name: '{{upper .Name}}_BASE_URL' - value: 'https://{{.Name}}.${containerAppsEnvironment.outputs.defaultDomain}' + value: 'https://{{containerAppName .Name}}.${containerAppsEnvironment.outputs.defaultDomain}' } {{- end}} {{- end}} From cba79ffba9b8ad65855cc42e736bc281cccc1d10 Mon Sep 17 00:00:00 2001 From: rujche Date: Fri, 17 Jan 2025 22:29:12 +0800 Subject: [PATCH 03/19] Fix lint error. --- cli/azd/.vscode/cspell.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/azd/.vscode/cspell.yaml b/cli/azd/.vscode/cspell.yaml index a10ce775337..2ee283f639f 100644 --- a/cli/azd/.vscode/cspell.yaml +++ b/cli/azd/.vscode/cspell.yaml @@ -16,6 +16,8 @@ words: - runcontext - unmarshals - usgovcloudapi + - jdbc + - bicept languageSettings: - languageId: go ignoreRegExpList: From fe86ba7b07a6f340c1c4cba33780e7097ec7b6ff Mon Sep 17 00:00:00 2001 From: rujche Date: Fri, 17 Jan 2025 23:14:08 +0800 Subject: [PATCH 04/19] Fix error about jdbc url. --- .../internal/appdetect/spring_boot_properties_file.go | 10 ++++++++-- .../appdetect/spring_boot_properties_file_test.go | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/cli/azd/internal/appdetect/spring_boot_properties_file.go b/cli/azd/internal/appdetect/spring_boot_properties_file.go index 39de7ecf356..0bb82d5f198 100644 --- a/cli/azd/internal/appdetect/spring_boot_properties_file.go +++ b/cli/azd/internal/appdetect/spring_boot_properties_file.go @@ -16,12 +16,18 @@ type propertyMergeFunc func(string, string) string const azureProfileName = "azure" -const placeholderPostgresUrl = "jdbc:${POSTGRES_URL}" // todo: manage placeholders here and in resources.bicept together. +// todo: manage following placeholders and together with resources.bicept. + +const placeholderPostgresHost = "${POSTGRES_HOST}" +const placeholderPostgresPort = "${POSTGRES_PORT}" +const placeholderPostgresDatabase = "${POSTGRES_DATABASE}" const placeholderPostgresUsername = "${POSTGRES_USERNAME}" const placeholderPostgresPassword = "${POSTGRES_PASSWORD}" +const placeholderPostgresJdbcUrl = "jdbc:postgresql://" + placeholderPostgresHost + ":" + placeholderPostgresPort + + "/" + placeholderPostgresDatabase var postgresqlProperties = []property{ - {"spring.datasource.url", placeholderPostgresUrl}, + {"spring.datasource.url", placeholderPostgresJdbcUrl}, {"spring.datasource.username", placeholderPostgresUsername}, {"spring.datasource.password", placeholderPostgresPassword}, } diff --git a/cli/azd/internal/appdetect/spring_boot_properties_file_test.go b/cli/azd/internal/appdetect/spring_boot_properties_file_test.go index 4ec950f06d2..45e39badc96 100644 --- a/cli/azd/internal/appdetect/spring_boot_properties_file_test.go +++ b/cli/azd/internal/appdetect/spring_boot_properties_file_test.go @@ -14,7 +14,7 @@ spring.datasource.username=admin spring.datasource.password=secret ` -const postgresPropertiesUpdatedContent = `spring.datasource.url=` + placeholderPostgresUrl + ` +const postgresPropertiesUpdatedContent = `spring.datasource.url=` + placeholderPostgresJdbcUrl + ` spring.datasource.username=` + placeholderPostgresUsername + ` spring.datasource.password=` + placeholderPostgresPassword + ` ` From ed61fa08bfc1661dc5582f7f9e7ca0b24899a893 Mon Sep 17 00:00:00 2001 From: rujche Date: Fri, 17 Jan 2025 23:17:13 +0800 Subject: [PATCH 05/19] Fix error about lint. --- cli/azd/internal/appdetect/spring_boot_properties_file.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/azd/internal/appdetect/spring_boot_properties_file.go b/cli/azd/internal/appdetect/spring_boot_properties_file.go index 0bb82d5f198..eb7ab3ac05b 100644 --- a/cli/azd/internal/appdetect/spring_boot_properties_file.go +++ b/cli/azd/internal/appdetect/spring_boot_properties_file.go @@ -104,13 +104,13 @@ func createFileIfNotExist(filePath string) error { dir := filepath.Dir(filePath) if _, err := os.Stat(dir); os.IsNotExist(err) { if err := os.MkdirAll(dir, os.ModePerm); err != nil { - return fmt.Errorf("failed to create directory: %v", err) + return fmt.Errorf("failed to create directory: %w", err) } } if _, err := os.Stat(filePath); os.IsNotExist(err) { file, err := os.Create(filePath) if err != nil { - return fmt.Errorf("failed to create file: %v", err) + return fmt.Errorf("failed to create file: %w", err) } defer file.Close() } From 2a656b8e99165561c892d5bee676e8712768355f Mon Sep 17 00:00:00 2001 From: rujche Date: Fri, 17 Jan 2025 23:51:05 +0800 Subject: [PATCH 06/19] Fix "G101: Potential hardcoded credentials (gosec)" by splitting string value. --- cli/azd/internal/appdetect/spring_boot_properties_file.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cli/azd/internal/appdetect/spring_boot_properties_file.go b/cli/azd/internal/appdetect/spring_boot_properties_file.go index eb7ab3ac05b..3fec47b24fb 100644 --- a/cli/azd/internal/appdetect/spring_boot_properties_file.go +++ b/cli/azd/internal/appdetect/spring_boot_properties_file.go @@ -22,7 +22,9 @@ const placeholderPostgresHost = "${POSTGRES_HOST}" const placeholderPostgresPort = "${POSTGRES_PORT}" const placeholderPostgresDatabase = "${POSTGRES_DATABASE}" const placeholderPostgresUsername = "${POSTGRES_USERNAME}" -const placeholderPostgresPassword = "${POSTGRES_PASSWORD}" + +// Split to fix this problem: "G101: Potential hardcoded credentials (gosec)" +const placeholderPostgresPassword = "${POSTGRES_PASS" + "WORD}" const placeholderPostgresJdbcUrl = "jdbc:postgresql://" + placeholderPostgresHost + ":" + placeholderPostgresPort + "/" + placeholderPostgresDatabase From 15d518200dab136909c2a303043e070558896ad4 Mon Sep 17 00:00:00 2001 From: rujche Date: Sun, 19 Jan 2025 19:31:54 +0800 Subject: [PATCH 07/19] Improve func: appendToCommaSeperatedValues Improve func: appendToCommaSeperatedValues --- .../appdetect/spring_boot_properties_file.go | 20 ++++++++++++------- .../spring_boot_properties_file_test.go | 9 +++++---- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/cli/azd/internal/appdetect/spring_boot_properties_file.go b/cli/azd/internal/appdetect/spring_boot_properties_file.go index 3fec47b24fb..98339f016b9 100644 --- a/cli/azd/internal/appdetect/spring_boot_properties_file.go +++ b/cli/azd/internal/appdetect/spring_boot_properties_file.go @@ -62,18 +62,24 @@ func activeAzureProfile(projectPath string) error { var newProperties = []property{ {"spring.profiles.active", azureProfileName}, } - return updatePropertyFile(filePath, newProperties, appendIfExists) + return updatePropertyFile(filePath, newProperties, appendToCommaSeperatedValues) } -func appendIfExists(originalValue string, newValue string) string { - if originalValue == "" { +func appendToCommaSeperatedValues(commaSeperatedValues string, newValue string) string { + if commaSeperatedValues == "" { return newValue } - originalValues := strings.SplitN(originalValue, ",", -1) - if contains(originalValues, azureProfileName) { - return originalValue + var values []string + for _, value := range strings.SplitN(commaSeperatedValues, ",", -1) { + value = strings.TrimSpace(value) + if value != "" { + values = append(values, value) + } + } + if !contains(values, azureProfileName) { + values = append(values, azureProfileName) } - return originalValue + "," + newValue + return strings.Join(values, ",") } func contains(a []string, x string) bool { diff --git a/cli/azd/internal/appdetect/spring_boot_properties_file_test.go b/cli/azd/internal/appdetect/spring_boot_properties_file_test.go index 45e39badc96..92b2f0d0a7c 100644 --- a/cli/azd/internal/appdetect/spring_boot_properties_file_test.go +++ b/cli/azd/internal/appdetect/spring_boot_properties_file_test.go @@ -48,10 +48,11 @@ func TestAddPostgresqlConnectionProperties(t *testing.T) { outputApplicationAzurePropertiesContent: postgresPropertiesUpdatedContent, }, { - name: "append original content", - inputApplicationPropertiesContent: "aaa=xxx", - inputApplicationAzurePropertiesContent: "bbb=yyy\n" + postgresPropertiesOriginalContent, - outputApplicationPropertiesContent: "aaa=xxx\n" + "spring.profiles.active=" + azureProfileName + "\n", + name: "append original content", + inputApplicationPropertiesContent: "aaa=xxx\n" + "spring.profiles.active=production , cloud,,", + inputApplicationAzurePropertiesContent: "bbb=yyy\n" + postgresPropertiesOriginalContent, + outputApplicationPropertiesContent: "aaa=xxx\n" + "spring.profiles.active=production,cloud," + + azureProfileName + "\n", outputApplicationAzurePropertiesContent: "bbb=yyy\n" + postgresPropertiesUpdatedContent, }, } From 9c486794d215d484bf0688239986b6aba3373d15 Mon Sep 17 00:00:00 2001 From: rujche Date: Sun, 19 Jan 2025 19:46:16 +0800 Subject: [PATCH 08/19] Fix typo. --- cli/azd/internal/appdetect/spring_boot_properties_file.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/azd/internal/appdetect/spring_boot_properties_file.go b/cli/azd/internal/appdetect/spring_boot_properties_file.go index 98339f016b9..a0baf6c5b75 100644 --- a/cli/azd/internal/appdetect/spring_boot_properties_file.go +++ b/cli/azd/internal/appdetect/spring_boot_properties_file.go @@ -62,15 +62,15 @@ func activeAzureProfile(projectPath string) error { var newProperties = []property{ {"spring.profiles.active", azureProfileName}, } - return updatePropertyFile(filePath, newProperties, appendToCommaSeperatedValues) + return updatePropertyFile(filePath, newProperties, appendToCommaSeparatedValues) } -func appendToCommaSeperatedValues(commaSeperatedValues string, newValue string) string { - if commaSeperatedValues == "" { +func appendToCommaSeparatedValues(commaSeparatedValues string, newValue string) string { + if commaSeparatedValues == "" { return newValue } var values []string - for _, value := range strings.SplitN(commaSeperatedValues, ",", -1) { + for _, value := range strings.SplitN(commaSeparatedValues, ",", -1) { value = strings.TrimSpace(value) if value != "" { values = append(values, value) From cdc9acddd069570d8841a7fb4e2fb1046e34baa9 Mon Sep 17 00:00:00 2001 From: rujche Date: Thu, 23 Jan 2025 16:05:16 +0800 Subject: [PATCH 09/19] Put all env name into one place. --- cli/azd/cmd/show.go | 6 +++-- .../appdetect/spring_boot_properties_file.go | 19 +++++-------- .../spring_boot_properties_file_test.go | 7 ++--- cli/azd/internal/cmd/add/add_preview.go | 27 ++++++++++--------- cli/azd/internal/env.go | 24 +++++++++++++++++ 5 files changed, 52 insertions(+), 31 deletions(-) create mode 100644 cli/azd/internal/env.go diff --git a/cli/azd/cmd/show.go b/cli/azd/cmd/show.go index 498e8022585..2662e9c44cc 100644 --- a/cli/azd/cmd/show.go +++ b/cli/azd/cmd/show.go @@ -418,11 +418,13 @@ func showModelDeployment( if account.Properties.Endpoint != nil { console.Message(ctx, color.HiMagentaString("%s (Azure AI Services Model Deployment)", id.Name)) console.Message(ctx, " Endpoint:") - console.Message(ctx, color.HiBlueString(fmt.Sprintf(" AZURE_OPENAI_ENDPOINT=%s", *account.Properties.Endpoint))) + console.Message(ctx, + color.HiBlueString(fmt.Sprintf(" %s=%s", internal.EnvNameAzureOpenAiUrl, *account.Properties.Endpoint))) console.Message(ctx, " Access:") console.Message(ctx, " Keyless (Microsoft Entra ID)") //nolint:lll - console.Message(ctx, output.WithGrayFormat(" Hint: To access locally, use DefaultAzureCredential. To learn more, visit https://learn.microsoft.com/en-us/azure/ai-services/openai/supported-languages")) + console.Message(ctx, + output.WithGrayFormat(" Hint: To access locally, use DefaultAzureCredential. To learn more, visit https://learn.microsoft.com/en-us/azure/ai-services/openai/supported-languages")) console.Message(ctx, "") } diff --git a/cli/azd/internal/appdetect/spring_boot_properties_file.go b/cli/azd/internal/appdetect/spring_boot_properties_file.go index a0baf6c5b75..fbcf87953c6 100644 --- a/cli/azd/internal/appdetect/spring_boot_properties_file.go +++ b/cli/azd/internal/appdetect/spring_boot_properties_file.go @@ -6,6 +6,8 @@ import ( "os" "path/filepath" "strings" + + "github.com/azure/azure-dev/cli/azd/internal" ) type property struct { @@ -16,22 +18,13 @@ type propertyMergeFunc func(string, string) string const azureProfileName = "azure" -// todo: manage following placeholders and together with resources.bicept. - -const placeholderPostgresHost = "${POSTGRES_HOST}" -const placeholderPostgresPort = "${POSTGRES_PORT}" -const placeholderPostgresDatabase = "${POSTGRES_DATABASE}" -const placeholderPostgresUsername = "${POSTGRES_USERNAME}" - -// Split to fix this problem: "G101: Potential hardcoded credentials (gosec)" -const placeholderPostgresPassword = "${POSTGRES_PASS" + "WORD}" -const placeholderPostgresJdbcUrl = "jdbc:postgresql://" + placeholderPostgresHost + ":" + placeholderPostgresPort + - "/" + placeholderPostgresDatabase +var placeholderPostgresJdbcUrl = "jdbc:postgresql://" + internal.ToEnvPlaceHolder(internal.EnvNamePostgresHost) + + ":" + internal.ToEnvPlaceHolder(internal.EnvNamePostgresPort) + "/" + internal.ToEnvPlaceHolder(internal.EnvNamePostgresDatabase) var postgresqlProperties = []property{ {"spring.datasource.url", placeholderPostgresJdbcUrl}, - {"spring.datasource.username", placeholderPostgresUsername}, - {"spring.datasource.password", placeholderPostgresPassword}, + {"spring.datasource.username", internal.ToEnvPlaceHolder(internal.EnvNamePostgresUsername)}, + {"spring.datasource.password", internal.ToEnvPlaceHolder(internal.EnvNamePostgresPassword)}, } var applicationPropertiesRelativePath = filepath.Join("src", "main", "resources", diff --git a/cli/azd/internal/appdetect/spring_boot_properties_file_test.go b/cli/azd/internal/appdetect/spring_boot_properties_file_test.go index 92b2f0d0a7c..c3bc8f4f101 100644 --- a/cli/azd/internal/appdetect/spring_boot_properties_file_test.go +++ b/cli/azd/internal/appdetect/spring_boot_properties_file_test.go @@ -6,6 +6,7 @@ import ( "reflect" "testing" + "github.com/azure/azure-dev/cli/azd/internal" "github.com/stretchr/testify/assert" ) @@ -14,9 +15,9 @@ spring.datasource.username=admin spring.datasource.password=secret ` -const postgresPropertiesUpdatedContent = `spring.datasource.url=` + placeholderPostgresJdbcUrl + ` -spring.datasource.username=` + placeholderPostgresUsername + ` -spring.datasource.password=` + placeholderPostgresPassword + ` +var postgresPropertiesUpdatedContent = `spring.datasource.url=` + placeholderPostgresJdbcUrl + ` +spring.datasource.username=` + internal.ToEnvPlaceHolder(internal.EnvNamePostgresUsername) + ` +spring.datasource.password=` + internal.ToEnvPlaceHolder(internal.EnvNamePostgresPassword) + ` ` var postgresPropertiesOriginalMap = []property{ diff --git a/cli/azd/internal/cmd/add/add_preview.go b/cli/azd/internal/cmd/add/add_preview.go index 94a40197bba..ea8f19a900e 100644 --- a/cli/azd/internal/cmd/add/add_preview.go +++ b/cli/azd/internal/cmd/add/add_preview.go @@ -9,6 +9,7 @@ import ( "strings" "text/tabwriter" + "github.com/azure/azure-dev/cli/azd/internal" "github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning" "github.com/azure/azure-dev/cli/azd/pkg/input" "github.com/azure/azure-dev/cli/azd/pkg/output" @@ -38,31 +39,31 @@ func Metadata(r *project.ResourceConfig) resourceMeta { case project.ResourceTypeDbRedis: res.AzureResourceType = "Microsoft.Cache/redis" res.UseEnvVars = []string{ - "REDIS_HOST", - "REDIS_PORT", - "REDIS_ENDPOINT", - "REDIS_PASSWORD", - "REDIS_URL", + internal.EnvNameRedisHost, + internal.EnvNameRedisPort, + internal.EnvNameRedisEndpoint, + internal.EnvNameRedisPassword, + internal.EnvNameRedisUrl, } case project.ResourceTypeDbPostgres: res.AzureResourceType = "Microsoft.DBforPostgreSQL/flexibleServers/databases" res.UseEnvVars = []string{ - "POSTGRES_HOST", - "POSTGRES_USERNAME", - "POSTGRES_DATABASE", - "POSTGRES_PASSWORD", - "POSTGRES_PORT", - "POSTGRES_URL", + internal.EnvNamePostgresHost, + internal.EnvNamePostgresUsername, + internal.EnvNamePostgresDatabase, + internal.EnvNamePostgresPassword, + internal.EnvNamePostgresPort, + internal.EnvNamePostgresUrl, } case project.ResourceTypeDbMongo: res.AzureResourceType = "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases" res.UseEnvVars = []string{ - "MONGODB_URL", + internal.EnvNameMongoDbUrl, } case project.ResourceTypeOpenAiModel: res.AzureResourceType = "Microsoft.CognitiveServices/accounts/deployments" res.UseEnvVars = []string{ - "AZURE_OPENAI_ENDPOINT", + internal.EnvNameAzureOpenAiUrl, } } return res diff --git a/cli/azd/internal/env.go b/cli/azd/internal/env.go new file mode 100644 index 00000000000..bb79ad2b4cf --- /dev/null +++ b/cli/azd/internal/env.go @@ -0,0 +1,24 @@ +package internal + +// todo: keep single source for env name in go lang code and resources.bicept + +const EnvNamePostgresHost = "POSTGRES_HOST" +const EnvNamePostgresPort = "POSTGRES_PORT" +const EnvNamePostgresUrl = "POSTGRES_URL" +const EnvNamePostgresDatabase = "POSTGRES_DATABASE" +const EnvNamePostgresUsername = "POSTGRES_USERNAME" +const EnvNamePostgresPassword = "POSTGRES_PASSWORD" + +const EnvNameRedisHost = "REDIS_HOST" +const EnvNameRedisPort = "REDIS_PORT" +const EnvNameRedisEndpoint = "REDIS_ENDPOINT" +const EnvNameRedisPassword = "REDIS_PASSWORD" +const EnvNameRedisUrl = "REDIS_URL" + +const EnvNameMongoDbUrl = "MONGODB_URL" + +const EnvNameAzureOpenAiUrl = "AZURE_OPENAI_ENDPOINT" + +func ToEnvPlaceHolder(envName string) string { + return "${" + envName + "}" +} From 519564711d2b2394be6bdeedd77f4efa9781189f Mon Sep 17 00:00:00 2001 From: rujche Date: Thu, 23 Jan 2025 16:12:30 +0800 Subject: [PATCH 10/19] Fix problem: the line is 133 characters long, which exceeds the maximum of 125 characters. (lll) --- cli/azd/internal/appdetect/spring_boot_properties_file.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/azd/internal/appdetect/spring_boot_properties_file.go b/cli/azd/internal/appdetect/spring_boot_properties_file.go index fbcf87953c6..1a260240755 100644 --- a/cli/azd/internal/appdetect/spring_boot_properties_file.go +++ b/cli/azd/internal/appdetect/spring_boot_properties_file.go @@ -19,7 +19,8 @@ type propertyMergeFunc func(string, string) string const azureProfileName = "azure" var placeholderPostgresJdbcUrl = "jdbc:postgresql://" + internal.ToEnvPlaceHolder(internal.EnvNamePostgresHost) + - ":" + internal.ToEnvPlaceHolder(internal.EnvNamePostgresPort) + "/" + internal.ToEnvPlaceHolder(internal.EnvNamePostgresDatabase) + ":" + internal.ToEnvPlaceHolder(internal.EnvNamePostgresPort) + + "/" + internal.ToEnvPlaceHolder(internal.EnvNamePostgresDatabase) var postgresqlProperties = []property{ {"spring.datasource.url", placeholderPostgresJdbcUrl}, From b16f2ec9635b65cd508af2333881da0f0ea1157b Mon Sep 17 00:00:00 2001 From: rujche Date: Thu, 23 Jan 2025 16:15:34 +0800 Subject: [PATCH 11/19] Fix this problem: // G101 : Potential hardcoded credentials (gosec) --- cli/azd/internal/env.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/azd/internal/env.go b/cli/azd/internal/env.go index bb79ad2b4cf..91b7844d555 100644 --- a/cli/azd/internal/env.go +++ b/cli/azd/internal/env.go @@ -7,6 +7,8 @@ const EnvNamePostgresPort = "POSTGRES_PORT" const EnvNamePostgresUrl = "POSTGRES_URL" const EnvNamePostgresDatabase = "POSTGRES_DATABASE" const EnvNamePostgresUsername = "POSTGRES_USERNAME" + +// nolint:gosec const EnvNamePostgresPassword = "POSTGRES_PASSWORD" const EnvNameRedisHost = "REDIS_HOST" From a3f83b7f1139d29a454faf37cbe15c07a8fcade2 Mon Sep 17 00:00:00 2001 From: rujche Date: Fri, 24 Jan 2025 22:15:34 +0800 Subject: [PATCH 12/19] Move updating application property file related code from appdetect.go to app_init.go. --- cli/azd/internal/appdetect/appdetect.go | 17 ++ cli/azd/internal/appdetect/java.go | 30 ++- .../appdetect/spring_boot_properties_file.go | 187 ---------------- .../spring_boot_properties_file_test.go | 142 ------------ cli/azd/internal/repository/app_init.go | 202 ++++++++++++++++++ cli/azd/internal/repository/app_init_test.go | 133 ++++++++++++ 6 files changed, 371 insertions(+), 340 deletions(-) delete mode 100644 cli/azd/internal/appdetect/spring_boot_properties_file.go delete mode 100644 cli/azd/internal/appdetect/spring_boot_properties_file_test.go diff --git a/cli/azd/internal/appdetect/appdetect.go b/cli/azd/internal/appdetect/appdetect.go index e6103d3cbd7..38ea64830cf 100644 --- a/cli/azd/internal/appdetect/appdetect.go +++ b/cli/azd/internal/appdetect/appdetect.go @@ -102,6 +102,20 @@ func (f Dependency) IsWebUIFramework() bool { return false } +type RawDependency struct { + Kind RawDependencyKind + Name string + Version string +} + +type RawDependencyKind string + +const RawDependencyKindMaven RawDependencyKind = "maven" +const MavenDependencyNameMySqlConnectorJ = "com.mysql:mysql-connector-j" +const MavenDependencyNamePostgresql = "org.postgresql:postgresql" +const MavenDependencyNameSpringCloudAzureStarterJdbcPostgresql = "com.azure.spring:spring-cloud-azure-starter-jdbc-postgresql" +const MavenDependencyNameSpringBootMavenPlugin = "org.springframework.boot:spring-boot-maven-plugin" + // A type of database that is inferred through heuristics while scanning project information. type DatabaseDep string @@ -138,6 +152,9 @@ type Project struct { // Dependencies scanned in the project. Dependencies []Dependency + // RawDependencies scanned in the project. + RawDependencies []RawDependency + // Experimental: Database dependencies inferred through heuristics while scanning dependencies in the project. DatabaseDeps []DatabaseDep diff --git a/cli/azd/internal/appdetect/java.go b/cli/azd/internal/appdetect/java.go index 2bff79ce642..53b3bcdbbea 100644 --- a/cli/azd/internal/appdetect/java.go +++ b/cli/azd/internal/appdetect/java.go @@ -123,27 +123,35 @@ func readMavenProject(filePath string) (*mavenProject, error) { func detectDependencies(mavenProject *mavenProject, project *Project) (*Project, error) { databaseDepMap := map[DatabaseDep]struct{}{} for _, dep := range mavenProject.Dependencies { - if dep.GroupId == "com.mysql" && dep.ArtifactId == "mysql-connector-j" { + name := toDependencyName(dep.GroupId, dep.ArtifactId) + switch name { + case MavenDependencyNameMySqlConnectorJ: databaseDepMap[DbMySql] = struct{}{} - } - - if (dep.GroupId == "org.postgresql" && dep.ArtifactId == "postgresql") || - (dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-starter-jdbc-postgresql") { + project.RawDependencies = append(project.RawDependencies, + RawDependency{RawDependencyKindMaven, name, dep.Version}) + case MavenDependencyNamePostgresql, MavenDependencyNameSpringCloudAzureStarterJdbcPostgresql: databaseDepMap[DbPostgres] = struct{}{} - // todo: Only call this func when it's a spring-boot project - err := addPostgresqlConnectionProperties(project.Path) - if err != nil { - return nil, err - } + project.RawDependencies = append(project.RawDependencies, + RawDependency{RawDependencyKindMaven, name, dep.Version}) } } - if len(databaseDepMap) > 0 { project.DatabaseDeps = slices.SortedFunc(maps.Keys(databaseDepMap), func(a, b DatabaseDep) int { return strings.Compare(string(a), string(b)) }) } + for _, dep := range mavenProject.Build.Plugins { + name := toDependencyName(dep.GroupId, dep.ArtifactId) + if name == MavenDependencyNameSpringBootMavenPlugin { + project.RawDependencies = append(project.RawDependencies, + RawDependency{RawDependencyKindMaven, name, dep.Version}) + } + } return project, nil } + +func toDependencyName(groupId string, artifactId string) string { + return groupId + ":" + artifactId +} diff --git a/cli/azd/internal/appdetect/spring_boot_properties_file.go b/cli/azd/internal/appdetect/spring_boot_properties_file.go deleted file mode 100644 index 1a260240755..00000000000 --- a/cli/azd/internal/appdetect/spring_boot_properties_file.go +++ /dev/null @@ -1,187 +0,0 @@ -package appdetect - -import ( - "bufio" - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/azure/azure-dev/cli/azd/internal" -) - -type property struct { - key string - value string -} -type propertyMergeFunc func(string, string) string - -const azureProfileName = "azure" - -var placeholderPostgresJdbcUrl = "jdbc:postgresql://" + internal.ToEnvPlaceHolder(internal.EnvNamePostgresHost) + - ":" + internal.ToEnvPlaceHolder(internal.EnvNamePostgresPort) + - "/" + internal.ToEnvPlaceHolder(internal.EnvNamePostgresDatabase) - -var postgresqlProperties = []property{ - {"spring.datasource.url", placeholderPostgresJdbcUrl}, - {"spring.datasource.username", internal.ToEnvPlaceHolder(internal.EnvNamePostgresUsername)}, - {"spring.datasource.password", internal.ToEnvPlaceHolder(internal.EnvNamePostgresPassword)}, -} - -var applicationPropertiesRelativePath = filepath.Join("src", "main", "resources", - "application.properties") -var applicationAzurePropertiesRelativePath = filepath.Join("src", "main", "resources", - "application-"+azureProfileName+".properties") - -// todo: support other file suffix. Example: application.yml, application.yaml -func addPostgresqlConnectionProperties(projectPath string) error { - err := addPostgresqlConnectionPropertiesIntoPropertyFile(projectPath) - if err != nil { - return err - } - return activeAzureProfile(projectPath) -} - -func addPostgresqlConnectionPropertiesIntoPropertyFile(projectPath string) error { - filePath := filepath.Join(projectPath, applicationAzurePropertiesRelativePath) - return updatePropertyFile(filePath, postgresqlProperties, keepNewValue) -} - -func keepNewValue(_ string, newValue string) string { - return newValue -} - -func activeAzureProfile(projectPath string) error { - filePath := filepath.Join(projectPath, applicationPropertiesRelativePath) - var newProperties = []property{ - {"spring.profiles.active", azureProfileName}, - } - return updatePropertyFile(filePath, newProperties, appendToCommaSeparatedValues) -} - -func appendToCommaSeparatedValues(commaSeparatedValues string, newValue string) string { - if commaSeparatedValues == "" { - return newValue - } - var values []string - for _, value := range strings.SplitN(commaSeparatedValues, ",", -1) { - value = strings.TrimSpace(value) - if value != "" { - values = append(values, value) - } - } - if !contains(values, azureProfileName) { - values = append(values, azureProfileName) - } - return strings.Join(values, ",") -} - -func contains(a []string, x string) bool { - for _, n := range a { - if x == n { - return true - } - } - return false -} - -func updatePropertyFile(filePath string, newProperties []property, function propertyMergeFunc) error { - err := createFileIfNotExist(filePath) - if err != nil { - return err - } - properties, err := readProperties(filePath) - if err != nil { - return err - } - properties = updateProperties(properties, newProperties, function) - err = writeProperties(filePath, properties) - if err != nil { - return err - } - return nil -} - -func createFileIfNotExist(filePath string) error { - dir := filepath.Dir(filePath) - if _, err := os.Stat(dir); os.IsNotExist(err) { - if err := os.MkdirAll(dir, os.ModePerm); err != nil { - return fmt.Errorf("failed to create directory: %w", err) - } - } - if _, err := os.Stat(filePath); os.IsNotExist(err) { - file, err := os.Create(filePath) - if err != nil { - return fmt.Errorf("failed to create file: %w", err) - } - defer file.Close() - } - return nil -} - -func readProperties(filePath string) ([]property, error) { - file, err := os.Open(filePath) - if err != nil { - return nil, err - } - defer file.Close() - - var properties []property - 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]) - properties = append(properties, property{key, value}) - } - } - - if err := scanner.Err(); err != nil { - return nil, err - } - - return properties, nil -} - -func updateProperties(properties []property, - newProperties []property, function propertyMergeFunc) []property { - for _, newProperty := range newProperties { - if index := getKeyIndex(properties, newProperty.key); index != -1 { - properties[index].value = function(properties[index].value, newProperty.value) - } else { - properties = append(properties, newProperty) - } - } - return properties -} - -func getKeyIndex(properties []property, key string) int { - for i, prop := range properties { - if prop.key == key { - return i - } - } - return -1 -} - -func writeProperties(filePath string, properties []property) error { - file, err := os.Create(filePath) - if err != nil { - return err - } - defer file.Close() - - writer := bufio.NewWriter(file) - for _, p := range properties { - _, err := writer.WriteString(fmt.Sprintf("%s=%s\n", p.key, p.value)) - if err != nil { - return err - } - } - return writer.Flush() -} diff --git a/cli/azd/internal/appdetect/spring_boot_properties_file_test.go b/cli/azd/internal/appdetect/spring_boot_properties_file_test.go deleted file mode 100644 index c3bc8f4f101..00000000000 --- a/cli/azd/internal/appdetect/spring_boot_properties_file_test.go +++ /dev/null @@ -1,142 +0,0 @@ -package appdetect - -import ( - "os" - "path/filepath" - "reflect" - "testing" - - "github.com/azure/azure-dev/cli/azd/internal" - "github.com/stretchr/testify/assert" -) - -const postgresPropertiesOriginalContent = `spring.datasource.url=jdbc:postgresql://localhost:5432/dbname?sslmode=require -spring.datasource.username=admin -spring.datasource.password=secret -` - -var postgresPropertiesUpdatedContent = `spring.datasource.url=` + placeholderPostgresJdbcUrl + ` -spring.datasource.username=` + internal.ToEnvPlaceHolder(internal.EnvNamePostgresUsername) + ` -spring.datasource.password=` + internal.ToEnvPlaceHolder(internal.EnvNamePostgresPassword) + ` -` - -var postgresPropertiesOriginalMap = []property{ - {"spring.datasource.url", "jdbc:postgresql://localhost:5432/dbname?sslmode=require"}, - {"spring.datasource.username", "admin"}, - {"spring.datasource.password", "secret"}, -} - -func TestAddPostgresqlConnectionProperties(t *testing.T) { - tests := []struct { - name string - inputApplicationPropertiesContent string - inputApplicationAzurePropertiesContent string - outputApplicationPropertiesContent string - outputApplicationAzurePropertiesContent string - }{ - { - name: "no content", - inputApplicationPropertiesContent: "", - inputApplicationAzurePropertiesContent: "", - outputApplicationPropertiesContent: "spring.profiles.active=" + azureProfileName + "\n", - outputApplicationAzurePropertiesContent: postgresPropertiesUpdatedContent, - }, - { - name: "override original content", - inputApplicationPropertiesContent: "spring.profiles.active=" + azureProfileName, - inputApplicationAzurePropertiesContent: postgresPropertiesOriginalContent, - outputApplicationPropertiesContent: "spring.profiles.active=" + azureProfileName + "\n", - outputApplicationAzurePropertiesContent: postgresPropertiesUpdatedContent, - }, - { - name: "append original content", - inputApplicationPropertiesContent: "aaa=xxx\n" + "spring.profiles.active=production , cloud,,", - inputApplicationAzurePropertiesContent: "bbb=yyy\n" + postgresPropertiesOriginalContent, - outputApplicationPropertiesContent: "aaa=xxx\n" + "spring.profiles.active=production,cloud," + - azureProfileName + "\n", - outputApplicationAzurePropertiesContent: "bbb=yyy\n" + postgresPropertiesUpdatedContent, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tempDir := t.TempDir() - applicationPropertiesPath := filepath.Join(tempDir, applicationPropertiesRelativePath) - createFileIfContentIsNotEmpty(t, applicationPropertiesPath, tt.inputApplicationPropertiesContent) - applicationAzurePropertiesPath := filepath.Join(tempDir, applicationAzurePropertiesRelativePath) - createFileIfContentIsNotEmpty(t, applicationAzurePropertiesPath, tt.inputApplicationAzurePropertiesContent) - err := addPostgresqlConnectionProperties(tempDir) - assert.NoError(t, err) - assertFileContent(t, applicationPropertiesPath, tt.outputApplicationPropertiesContent) - assertFileContent(t, applicationAzurePropertiesPath, tt.outputApplicationAzurePropertiesContent) - }) - } -} - -func createFileIfContentIsNotEmpty(t *testing.T, path string, content string) { - if content == "" { - return - } - - dir := filepath.Dir(path) - if _, err := os.Stat(dir); os.IsNotExist(err) { - err := os.MkdirAll(dir, os.ModePerm) - assert.NoError(t, err) - } - file, err := os.Create(path) - assert.NoError(t, err) - defer file.Close() - err = os.WriteFile(path, []byte(content), 0600) - assert.NoError(t, err) -} - -func assertFileContent(t *testing.T, path string, content string) { - actualContent, err := os.ReadFile(path) - assert.NoError(t, err) - assert.Equal(t, content, string(actualContent)) -} - -func TestReadProperties(t *testing.T) { - tempFile, err := os.CreateTemp("", "test.properties") - if err != nil { - t.Fatal(err) - } - defer os.Remove(tempFile.Name()) - - if _, err := tempFile.Write([]byte(postgresPropertiesOriginalContent)); err != nil { - t.Fatal(err) - } - if err := tempFile.Close(); err != nil { - t.Fatal(err) - } - - properties, err := readProperties(tempFile.Name()) - if err != nil { - t.Fatalf("readProperties() error = %v", err) - } - - if !reflect.DeepEqual(properties, postgresPropertiesOriginalMap) { - t.Errorf("readProperties() = %v, want %v", properties, postgresPropertiesOriginalMap) - } -} - -func TestWriteProperties(t *testing.T) { - tempFile, err := os.CreateTemp("", "test.properties") - if err != nil { - t.Fatal(err) - } - defer os.Remove(tempFile.Name()) - - if err := writeProperties(tempFile.Name(), postgresPropertiesOriginalMap); err != nil { - t.Fatalf("writeProperties() error = %v", err) - } - - content, err := os.ReadFile(tempFile.Name()) - if err != nil { - t.Fatal(err) - } - - if string(content) != postgresPropertiesOriginalContent { - t.Errorf("writeProperties() = %v, want %v", string(content), postgresPropertiesOriginalContent) - } -} diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index cb211bc14b1..d2734b57c32 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -1,6 +1,7 @@ package repository import ( + "bufio" "context" "fmt" "maps" @@ -301,6 +302,31 @@ func (i *Initializer) InitFromApp( }) } + for _, proj := range projects { + isSpringBootProject := false + for _, dep := range proj.RawDependencies { + if dep.Kind == appdetect.RawDependencyKindMaven && dep.Name == appdetect.MavenDependencyNameSpringBootMavenPlugin { + isSpringBootProject = true + break + } + } + if !isSpringBootProject { + continue + } + for _, dep := range proj.RawDependencies { + if dep.Kind != appdetect.RawDependencyKindMaven { + continue + } + switch dep.Name { + case appdetect.MavenDependencyNamePostgresql, appdetect.MavenDependencyNameSpringCloudAzureStarterJdbcPostgresql: + err := addPostgresqlConnectionProperties(proj.Path) + if err != nil { + return err + } + } + } + } + return nil } @@ -578,3 +604,179 @@ func ServiceFromDetect( return svc, nil } + +type property struct { + key string + value string +} +type propertyMergeFunc func(string, string) string + +const azureProfileName = "azure" + +var placeholderPostgresJdbcUrl = "jdbc:postgresql://" + internal.ToEnvPlaceHolder(internal.EnvNamePostgresHost) + + ":" + internal.ToEnvPlaceHolder(internal.EnvNamePostgresPort) + + "/" + internal.ToEnvPlaceHolder(internal.EnvNamePostgresDatabase) + +var postgresqlProperties = []property{ + {"spring.datasource.url", placeholderPostgresJdbcUrl}, + {"spring.datasource.username", internal.ToEnvPlaceHolder(internal.EnvNamePostgresUsername)}, + {"spring.datasource.password", internal.ToEnvPlaceHolder(internal.EnvNamePostgresPassword)}, +} + +var applicationPropertiesRelativePath = filepath.Join("src", "main", "resources", + "application.properties") +var applicationAzurePropertiesRelativePath = filepath.Join("src", "main", "resources", + "application-"+azureProfileName+".properties") + +// todo: support other file suffix. Example: application.yml, application.yaml +func addPostgresqlConnectionProperties(projectPath string) error { + err := addPostgresqlConnectionPropertiesIntoPropertyFile(projectPath) + if err != nil { + return err + } + return activeAzureProfile(projectPath) +} + +func addPostgresqlConnectionPropertiesIntoPropertyFile(projectPath string) error { + filePath := filepath.Join(projectPath, applicationAzurePropertiesRelativePath) + return updatePropertyFile(filePath, postgresqlProperties, keepNewValue) +} + +func keepNewValue(_ string, newValue string) string { + return newValue +} + +func activeAzureProfile(projectPath string) error { + filePath := filepath.Join(projectPath, applicationPropertiesRelativePath) + var newProperties = []property{ + {"spring.profiles.active", azureProfileName}, + } + return updatePropertyFile(filePath, newProperties, appendToCommaSeparatedValues) +} + +func appendToCommaSeparatedValues(commaSeparatedValues string, newValue string) string { + if commaSeparatedValues == "" { + return newValue + } + var values []string + for _, value := range strings.SplitN(commaSeparatedValues, ",", -1) { + value = strings.TrimSpace(value) + if value != "" { + values = append(values, value) + } + } + if !contains(values, azureProfileName) { + values = append(values, azureProfileName) + } + return strings.Join(values, ",") +} + +func contains(a []string, x string) bool { + for _, n := range a { + if x == n { + return true + } + } + return false +} + +func updatePropertyFile(filePath string, newProperties []property, function propertyMergeFunc) error { + err := createFileIfNotExist(filePath) + if err != nil { + return err + } + properties, err := readProperties(filePath) + if err != nil { + return err + } + properties = updateProperties(properties, newProperties, function) + err = writeProperties(filePath, properties) + if err != nil { + return err + } + return nil +} + +func createFileIfNotExist(filePath string) error { + dir := filepath.Dir(filePath) + if _, err := os.Stat(dir); os.IsNotExist(err) { + if err := os.MkdirAll(dir, os.ModePerm); err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + } + if _, err := os.Stat(filePath); os.IsNotExist(err) { + file, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("failed to create file: %w", err) + } + defer file.Close() + } + return nil +} + +func readProperties(filePath string) ([]property, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer file.Close() + + var properties []property + 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]) + properties = append(properties, property{key, value}) + } + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return properties, nil +} + +func updateProperties(properties []property, + newProperties []property, function propertyMergeFunc) []property { + for _, newProperty := range newProperties { + if index := getKeyIndex(properties, newProperty.key); index != -1 { + properties[index].value = function(properties[index].value, newProperty.value) + } else { + properties = append(properties, newProperty) + } + } + return properties +} + +func getKeyIndex(properties []property, key string) int { + for i, prop := range properties { + if prop.key == key { + return i + } + } + return -1 +} + +func writeProperties(filePath string, properties []property) error { + file, err := os.Create(filePath) + if err != nil { + return err + } + defer file.Close() + + writer := bufio.NewWriter(file) + for _, p := range properties { + _, err := writer.WriteString(fmt.Sprintf("%s=%s\n", p.key, p.value)) + if err != nil { + return err + } + } + return writer.Flush() +} diff --git a/cli/azd/internal/repository/app_init_test.go b/cli/azd/internal/repository/app_init_test.go index 6a0b7a7dac6..71823cf3540 100644 --- a/cli/azd/internal/repository/app_init_test.go +++ b/cli/azd/internal/repository/app_init_test.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "reflect" "strings" "testing" @@ -12,6 +13,7 @@ import ( "github.com/azure/azure-dev/cli/azd/internal/appdetect" "github.com/azure/azure-dev/cli/azd/pkg/input" "github.com/azure/azure-dev/cli/azd/pkg/project" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -319,3 +321,134 @@ func TestInitializer_prjConfigFromDetect(t *testing.T) { }) } } + +const postgresPropertiesOriginalContent = `spring.datasource.url=jdbc:postgresql://localhost:5432/dbname?sslmode=require +spring.datasource.username=admin +spring.datasource.password=secret +` + +var postgresPropertiesUpdatedContent = `spring.datasource.url=` + placeholderPostgresJdbcUrl + ` +spring.datasource.username=` + internal.ToEnvPlaceHolder(internal.EnvNamePostgresUsername) + ` +spring.datasource.password=` + internal.ToEnvPlaceHolder(internal.EnvNamePostgresPassword) + ` +` + +var postgresPropertiesOriginalMap = []property{ + {"spring.datasource.url", "jdbc:postgresql://localhost:5432/dbname?sslmode=require"}, + {"spring.datasource.username", "admin"}, + {"spring.datasource.password", "secret"}, +} + +func TestAddPostgresqlConnectionProperties(t *testing.T) { + tests := []struct { + name string + inputApplicationPropertiesContent string + inputApplicationAzurePropertiesContent string + outputApplicationPropertiesContent string + outputApplicationAzurePropertiesContent string + }{ + { + name: "no content", + inputApplicationPropertiesContent: "", + inputApplicationAzurePropertiesContent: "", + outputApplicationPropertiesContent: "spring.profiles.active=" + azureProfileName + "\n", + outputApplicationAzurePropertiesContent: postgresPropertiesUpdatedContent, + }, + { + name: "override original content", + inputApplicationPropertiesContent: "spring.profiles.active=" + azureProfileName, + inputApplicationAzurePropertiesContent: postgresPropertiesOriginalContent, + outputApplicationPropertiesContent: "spring.profiles.active=" + azureProfileName + "\n", + outputApplicationAzurePropertiesContent: postgresPropertiesUpdatedContent, + }, + { + name: "append original content", + inputApplicationPropertiesContent: "aaa=xxx\n" + "spring.profiles.active=production , cloud,,", + inputApplicationAzurePropertiesContent: "bbb=yyy\n" + postgresPropertiesOriginalContent, + outputApplicationPropertiesContent: "aaa=xxx\n" + "spring.profiles.active=production,cloud," + + azureProfileName + "\n", + outputApplicationAzurePropertiesContent: "bbb=yyy\n" + postgresPropertiesUpdatedContent, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tempDir := t.TempDir() + applicationPropertiesPath := filepath.Join(tempDir, applicationPropertiesRelativePath) + createFileIfContentIsNotEmpty(t, applicationPropertiesPath, tt.inputApplicationPropertiesContent) + applicationAzurePropertiesPath := filepath.Join(tempDir, applicationAzurePropertiesRelativePath) + createFileIfContentIsNotEmpty(t, applicationAzurePropertiesPath, tt.inputApplicationAzurePropertiesContent) + err := addPostgresqlConnectionProperties(tempDir) + assert.NoError(t, err) + assertFileContent(t, applicationPropertiesPath, tt.outputApplicationPropertiesContent) + assertFileContent(t, applicationAzurePropertiesPath, tt.outputApplicationAzurePropertiesContent) + }) + } +} + +func createFileIfContentIsNotEmpty(t *testing.T, path string, content string) { + if content == "" { + return + } + + dir := filepath.Dir(path) + if _, err := os.Stat(dir); os.IsNotExist(err) { + err := os.MkdirAll(dir, os.ModePerm) + assert.NoError(t, err) + } + file, err := os.Create(path) + assert.NoError(t, err) + defer file.Close() + err = os.WriteFile(path, []byte(content), 0600) + assert.NoError(t, err) +} + +func assertFileContent(t *testing.T, path string, content string) { + actualContent, err := os.ReadFile(path) + assert.NoError(t, err) + assert.Equal(t, content, string(actualContent)) +} + +func TestReadProperties(t *testing.T) { + tempFile, err := os.CreateTemp("", "test.properties") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tempFile.Name()) + + if _, err := tempFile.Write([]byte(postgresPropertiesOriginalContent)); err != nil { + t.Fatal(err) + } + if err := tempFile.Close(); err != nil { + t.Fatal(err) + } + + properties, err := readProperties(tempFile.Name()) + if err != nil { + t.Fatalf("readProperties() error = %v", err) + } + + if !reflect.DeepEqual(properties, postgresPropertiesOriginalMap) { + t.Errorf("readProperties() = %v, want %v", properties, postgresPropertiesOriginalMap) + } +} + +func TestWriteProperties(t *testing.T) { + tempFile, err := os.CreateTemp("", "test.properties") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tempFile.Name()) + + if err := writeProperties(tempFile.Name(), postgresPropertiesOriginalMap); err != nil { + t.Fatalf("writeProperties() error = %v", err) + } + + content, err := os.ReadFile(tempFile.Name()) + if err != nil { + t.Fatal(err) + } + + if string(content) != postgresPropertiesOriginalContent { + t.Errorf("writeProperties() = %v, want %v", string(content), postgresPropertiesOriginalContent) + } +} From d46ed232cbb343315334e9c608512fdc59fe4f01 Mon Sep 17 00:00:00 2001 From: rujche Date: Fri, 24 Jan 2025 22:32:49 +0800 Subject: [PATCH 13/19] Fix lint error. --- cli/azd/internal/appdetect/appdetect.go | 3 ++- cli/azd/internal/repository/app_init.go | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cli/azd/internal/appdetect/appdetect.go b/cli/azd/internal/appdetect/appdetect.go index 38ea64830cf..7e890f0fba3 100644 --- a/cli/azd/internal/appdetect/appdetect.go +++ b/cli/azd/internal/appdetect/appdetect.go @@ -113,7 +113,8 @@ type RawDependencyKind string const RawDependencyKindMaven RawDependencyKind = "maven" const MavenDependencyNameMySqlConnectorJ = "com.mysql:mysql-connector-j" const MavenDependencyNamePostgresql = "org.postgresql:postgresql" -const MavenDependencyNameSpringCloudAzureStarterJdbcPostgresql = "com.azure.spring:spring-cloud-azure-starter-jdbc-postgresql" +const MavenDependencyNameSpringCloudAzureStarterJdbcPostgresql = "com.azure.spring:" + + "spring-cloud-azure-starter-jdbc-postgresql" const MavenDependencyNameSpringBootMavenPlugin = "org.springframework.boot:spring-boot-maven-plugin" // A type of database that is inferred through heuristics while scanning project information. diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index d08d33d1169..f6898bd6d84 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -304,7 +304,8 @@ func (i *Initializer) InitFromApp( continue } switch dep.Name { - case appdetect.MavenDependencyNamePostgresql, appdetect.MavenDependencyNameSpringCloudAzureStarterJdbcPostgresql: + case appdetect.MavenDependencyNamePostgresql, + appdetect.MavenDependencyNameSpringCloudAzureStarterJdbcPostgresql: err := addPostgresqlConnectionProperties(proj.Path) if err != nil { return err From 4f48b256a35fc37e70ff2bbef8a0d338a5f5ed1d Mon Sep 17 00:00:00 2001 From: rujche Date: Fri, 24 Jan 2025 22:34:17 +0800 Subject: [PATCH 14/19] Adding "springframework" in cspell.yaml. --- cli/azd/.vscode/cspell.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/azd/.vscode/cspell.yaml b/cli/azd/.vscode/cspell.yaml index 2ee283f639f..57c3fd1144a 100644 --- a/cli/azd/.vscode/cspell.yaml +++ b/cli/azd/.vscode/cspell.yaml @@ -18,6 +18,7 @@ words: - usgovcloudapi - jdbc - bicept + - springframework languageSettings: - languageId: go ignoreRegExpList: From e8f1de1563e7b0cd101702eea612ef4edcd710f4 Mon Sep 17 00:00:00 2001 From: rujche Date: Sun, 26 Jan 2025 12:58:11 +0800 Subject: [PATCH 15/19] Fix lint error. --- cli/azd/cmd/show.go | 5 ++--- cli/azd/internal/repository/app_init.go | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/azd/cmd/show.go b/cli/azd/cmd/show.go index 2662e9c44cc..9860a93c7c8 100644 --- a/cli/azd/cmd/show.go +++ b/cli/azd/cmd/show.go @@ -423,9 +423,8 @@ func showModelDeployment( console.Message(ctx, " Access:") console.Message(ctx, " Keyless (Microsoft Entra ID)") //nolint:lll - console.Message(ctx, - output.WithGrayFormat(" Hint: To access locally, use DefaultAzureCredential. To learn more, visit https://learn.microsoft.com/en-us/azure/ai-services/openai/supported-languages")) - + console.Message(ctx, output.WithGrayFormat(" Hint: To access locally, use DefaultAzureCredential. "+ + "To learn more, visit https://learn.microsoft.com/en-us/azure/ai-services/openai/supported-languages")) console.Message(ctx, "") } diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index f6898bd6d84..c7b25e07916 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -291,7 +291,8 @@ func (i *Initializer) InitFromApp( for _, proj := range projects { isSpringBootProject := false for _, dep := range proj.RawDependencies { - if dep.Kind == appdetect.RawDependencyKindMaven && dep.Name == appdetect.MavenDependencyNameSpringBootMavenPlugin { + if dep.Kind == appdetect.RawDependencyKindMaven && + dep.Name == appdetect.MavenDependencyNameSpringBootMavenPlugin { isSpringBootProject = true break } From eafe0be27a772a146e5384515242f82ac6466a2b Mon Sep 17 00:00:00 2001 From: rujche Date: Sun, 26 Jan 2025 13:08:35 +0800 Subject: [PATCH 16/19] Fix test error. --- cli/azd/internal/appdetect/appdetect_test.go | 68 ++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/cli/azd/internal/appdetect/appdetect_test.go b/cli/azd/internal/appdetect/appdetect_test.go index b356a151ace..0fcd7c33666 100644 --- a/cli/azd/internal/appdetect/appdetect_test.go +++ b/cli/azd/internal/appdetect/appdetect_test.go @@ -49,6 +49,23 @@ func TestDetect(t *testing.T) { DbMySql, DbPostgres, }, + RawDependencies: []RawDependency{ + { + Kind: RawDependencyKindMaven, + Name: "com.mysql:mysql-connector-j", + Version: "", + }, + { + Kind: RawDependencyKindMaven, + Name: "org.postgresql:postgresql", + Version: "", + }, + { + Kind: RawDependencyKindMaven, + Name: "org.springframework.boot:spring-boot-maven-plugin", + Version: "", + }, + }, }, { Language: Java, @@ -133,6 +150,23 @@ func TestDetect(t *testing.T) { DbMySql, DbPostgres, }, + RawDependencies: []RawDependency{ + { + Kind: RawDependencyKindMaven, + Name: "com.mysql:mysql-connector-j", + Version: "", + }, + { + Kind: RawDependencyKindMaven, + Name: "org.postgresql:postgresql", + Version: "", + }, + { + Kind: RawDependencyKindMaven, + Name: "org.springframework.boot:spring-boot-maven-plugin", + Version: "", + }, + }, }, { Language: Java, @@ -166,6 +200,23 @@ func TestDetect(t *testing.T) { DbMySql, DbPostgres, }, + RawDependencies: []RawDependency{ + { + Kind: RawDependencyKindMaven, + Name: "com.mysql:mysql-connector-j", + Version: "", + }, + { + Kind: RawDependencyKindMaven, + Name: "org.postgresql:postgresql", + Version: "", + }, + { + Kind: RawDependencyKindMaven, + Name: "org.springframework.boot:spring-boot-maven-plugin", + Version: "", + }, + }, }, { Language: Java, @@ -202,6 +253,23 @@ func TestDetect(t *testing.T) { DbMySql, DbPostgres, }, + RawDependencies: []RawDependency{ + { + Kind: RawDependencyKindMaven, + Name: "com.mysql:mysql-connector-j", + Version: "", + }, + { + Kind: RawDependencyKindMaven, + Name: "org.postgresql:postgresql", + Version: "", + }, + { + Kind: RawDependencyKindMaven, + Name: "org.springframework.boot:spring-boot-maven-plugin", + Version: "", + }, + }, }, { Language: Java, From 2df79beacc3218ff17e6222f3835a4d87c336d51 Mon Sep 17 00:00:00 2001 From: rujche Date: Sun, 26 Jan 2025 13:16:29 +0800 Subject: [PATCH 17/19] Fix test error: Version filled by effective pom feature. --- cli/azd/internal/appdetect/appdetect_test.go | 24 ++++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/cli/azd/internal/appdetect/appdetect_test.go b/cli/azd/internal/appdetect/appdetect_test.go index fd50d2dfe95..b6066bee80c 100644 --- a/cli/azd/internal/appdetect/appdetect_test.go +++ b/cli/azd/internal/appdetect/appdetect_test.go @@ -56,17 +56,17 @@ func TestDetect(t *testing.T) { { Kind: RawDependencyKindMaven, Name: "com.mysql:mysql-connector-j", - Version: "", + Version: "8.3.0", }, { Kind: RawDependencyKindMaven, Name: "org.postgresql:postgresql", - Version: "", + Version: "42.7.3", }, { Kind: RawDependencyKindMaven, Name: "org.springframework.boot:spring-boot-maven-plugin", - Version: "", + Version: "3.3.0", }, }, }, @@ -157,17 +157,17 @@ func TestDetect(t *testing.T) { { Kind: RawDependencyKindMaven, Name: "com.mysql:mysql-connector-j", - Version: "", + Version: "8.3.0", }, { Kind: RawDependencyKindMaven, Name: "org.postgresql:postgresql", - Version: "", + Version: "42.7.3", }, { Kind: RawDependencyKindMaven, Name: "org.springframework.boot:spring-boot-maven-plugin", - Version: "", + Version: "3.3.0", }, }, }, @@ -207,17 +207,17 @@ func TestDetect(t *testing.T) { { Kind: RawDependencyKindMaven, Name: "com.mysql:mysql-connector-j", - Version: "", + Version: "8.3.0", }, { Kind: RawDependencyKindMaven, Name: "org.postgresql:postgresql", - Version: "", + Version: "42.7.3", }, { Kind: RawDependencyKindMaven, Name: "org.springframework.boot:spring-boot-maven-plugin", - Version: "", + Version: "3.3.0", }, }, }, @@ -260,17 +260,17 @@ func TestDetect(t *testing.T) { { Kind: RawDependencyKindMaven, Name: "com.mysql:mysql-connector-j", - Version: "", + Version: "8.3.0", }, { Kind: RawDependencyKindMaven, Name: "org.postgresql:postgresql", - Version: "", + Version: "42.7.3", }, { Kind: RawDependencyKindMaven, Name: "org.springframework.boot:spring-boot-maven-plugin", - Version: "", + Version: "3.3.0", }, }, }, From c94105a97cc1a1e656ce60641766b4bce932f746 Mon Sep 17 00:00:00 2001 From: rujche Date: Sun, 26 Jan 2025 13:44:01 +0800 Subject: [PATCH 18/19] Fix check error by adding copyright. --- cli/azd/internal/appdetect/java_test.go | 3 +++ cli/azd/internal/env.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/cli/azd/internal/appdetect/java_test.go b/cli/azd/internal/appdetect/java_test.go index 0d62858ae19..174d26a8968 100644 --- a/cli/azd/internal/appdetect/java_test.go +++ b/cli/azd/internal/appdetect/java_test.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package appdetect import ( diff --git a/cli/azd/internal/env.go b/cli/azd/internal/env.go index 91b7844d555..2648bd98a94 100644 --- a/cli/azd/internal/env.go +++ b/cli/azd/internal/env.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package internal // todo: keep single source for env name in go lang code and resources.bicept From 633a488e7dd6283eabadc96523176ac13fdd5990 Mon Sep 17 00:00:00 2001 From: rujche Date: Wed, 5 Feb 2025 15:41:01 +0800 Subject: [PATCH 19/19] Change RawDependency to RawProject in appdetect.go. --- cli/azd/internal/appdetect/appdetect.go | 18 +---- cli/azd/internal/appdetect/appdetect_test.go | 77 ++------------------ cli/azd/internal/appdetect/java.go | 38 +++------- cli/azd/internal/repository/app_init.go | 22 +++--- 4 files changed, 35 insertions(+), 120 deletions(-) diff --git a/cli/azd/internal/appdetect/appdetect.go b/cli/azd/internal/appdetect/appdetect.go index 06fb994f980..5a1d9663db9 100644 --- a/cli/azd/internal/appdetect/appdetect.go +++ b/cli/azd/internal/appdetect/appdetect.go @@ -103,21 +103,9 @@ func (f Dependency) IsWebUIFramework() bool { return false } -type RawDependency struct { - Kind RawDependencyKind - Name string - Version string +type RawProject interface { } -type RawDependencyKind string - -const RawDependencyKindMaven RawDependencyKind = "maven" -const MavenDependencyNameMySqlConnectorJ = "com.mysql:mysql-connector-j" -const MavenDependencyNamePostgresql = "org.postgresql:postgresql" -const MavenDependencyNameSpringCloudAzureStarterJdbcPostgresql = "com.azure.spring:" + - "spring-cloud-azure-starter-jdbc-postgresql" -const MavenDependencyNameSpringBootMavenPlugin = "org.springframework.boot:spring-boot-maven-plugin" - // A type of database that is inferred through heuristics while scanning project information. type DatabaseDep string @@ -154,8 +142,8 @@ type Project struct { // Dependencies scanned in the project. Dependencies []Dependency - // RawDependencies scanned in the project. - RawDependencies []RawDependency + // RawProject scanned in the project. + RawProject RawProject // Experimental: Database dependencies inferred through heuristics while scanning dependencies in the project. DatabaseDeps []DatabaseDep diff --git a/cli/azd/internal/appdetect/appdetect_test.go b/cli/azd/internal/appdetect/appdetect_test.go index b6066bee80c..e9c7d920b3b 100644 --- a/cli/azd/internal/appdetect/appdetect_test.go +++ b/cli/azd/internal/appdetect/appdetect_test.go @@ -52,23 +52,6 @@ func TestDetect(t *testing.T) { DbMySql, DbPostgres, }, - RawDependencies: []RawDependency{ - { - Kind: RawDependencyKindMaven, - Name: "com.mysql:mysql-connector-j", - Version: "8.3.0", - }, - { - Kind: RawDependencyKindMaven, - Name: "org.postgresql:postgresql", - Version: "42.7.3", - }, - { - Kind: RawDependencyKindMaven, - Name: "org.springframework.boot:spring-boot-maven-plugin", - Version: "3.3.0", - }, - }, }, { Language: Java, @@ -153,23 +136,6 @@ func TestDetect(t *testing.T) { DbMySql, DbPostgres, }, - RawDependencies: []RawDependency{ - { - Kind: RawDependencyKindMaven, - Name: "com.mysql:mysql-connector-j", - Version: "8.3.0", - }, - { - Kind: RawDependencyKindMaven, - Name: "org.postgresql:postgresql", - Version: "42.7.3", - }, - { - Kind: RawDependencyKindMaven, - Name: "org.springframework.boot:spring-boot-maven-plugin", - Version: "3.3.0", - }, - }, }, { Language: Java, @@ -203,23 +169,6 @@ func TestDetect(t *testing.T) { DbMySql, DbPostgres, }, - RawDependencies: []RawDependency{ - { - Kind: RawDependencyKindMaven, - Name: "com.mysql:mysql-connector-j", - Version: "8.3.0", - }, - { - Kind: RawDependencyKindMaven, - Name: "org.postgresql:postgresql", - Version: "42.7.3", - }, - { - Kind: RawDependencyKindMaven, - Name: "org.springframework.boot:spring-boot-maven-plugin", - Version: "3.3.0", - }, - }, }, { Language: Java, @@ -256,23 +205,6 @@ func TestDetect(t *testing.T) { DbMySql, DbPostgres, }, - RawDependencies: []RawDependency{ - { - Kind: RawDependencyKindMaven, - Name: "com.mysql:mysql-connector-j", - Version: "8.3.0", - }, - { - Kind: RawDependencyKindMaven, - Name: "org.postgresql:postgresql", - Version: "42.7.3", - }, - { - Kind: RawDependencyKindMaven, - Name: "org.springframework.boot:spring-boot-maven-plugin", - Version: "3.3.0", - }, - }, }, { Language: Java, @@ -297,7 +229,14 @@ func TestDetect(t *testing.T) { tt.want[i].Path = filepath.Join(dir, tt.want[i].Path) } - require.Equal(t, tt.want, projects) + require.Equal(t, len(tt.want), len(projects)) + for i := range tt.want { + require.Equal(t, tt.want[i].Language, projects[i].Language) + require.Equal(t, tt.want[i].Path, projects[i].Path) + require.Equal(t, tt.want[i].DetectionRule, projects[i].DetectionRule) + require.Equal(t, tt.want[i].Dependencies, projects[i].Dependencies) + require.Equal(t, tt.want[i].DatabaseDeps, projects[i].DatabaseDeps) + } }) } } diff --git a/cli/azd/internal/appdetect/java.go b/cli/azd/internal/appdetect/java.go index d51577b559e..e3b6c639de7 100644 --- a/cli/azd/internal/appdetect/java.go +++ b/cli/azd/internal/appdetect/java.go @@ -18,7 +18,7 @@ import ( type javaDetector struct { mvnCli *maven.Cli - rootProjects []mavenProject + rootProjects []MavenProject } func (jd *javaDetector) Language() Language { @@ -41,7 +41,7 @@ func (jd *javaDetector) DetectProject(ctx context.Context, path string, entries return nil, nil } - var currentRoot *mavenProject + var currentRoot *MavenProject for _, rootProject := range jd.rootProjects { // we can say that the project is in the root project if the path is under the project if inRoot := strings.HasPrefix(pomFile, rootProject.path); inRoot { @@ -66,8 +66,8 @@ func (jd *javaDetector) DetectProject(ctx context.Context, path string, entries return nil, nil } -// mavenProject represents the top-level structure of a Maven POM file. -type mavenProject struct { +// MavenProject represents the top-level structure of a Maven POM file. +type MavenProject struct { XmlName xml.Name `xml:"project"` Parent parent `xml:"parent"` Modules []string `xml:"modules>module"` // Capture the modules @@ -109,12 +109,12 @@ type plugin struct { Version string `xml:"version"` } -func readMavenProject(ctx context.Context, mvnCli *maven.Cli, filePath string) (*mavenProject, error) { +func readMavenProject(ctx context.Context, mvnCli *maven.Cli, filePath string) (*MavenProject, error) { effectivePom, err := mvnCli.EffectivePom(ctx, filePath) if err != nil { return nil, err } - var project mavenProject + var project MavenProject if err := xml.Unmarshal([]byte(effectivePom), &project); err != nil { return nil, fmt.Errorf("parsing xml: %w", err) } @@ -122,19 +122,16 @@ func readMavenProject(ctx context.Context, mvnCli *maven.Cli, filePath string) ( return &project, nil } -func detectDependencies(mavenProject *mavenProject, project *Project) (*Project, error) { +func detectDependencies(mavenProject *MavenProject, project *Project) (*Project, error) { databaseDepMap := map[DatabaseDep]struct{}{} for _, dep := range mavenProject.Dependencies { - name := toDependencyName(dep.GroupId, dep.ArtifactId) + name := dep.GroupId + ":" + dep.ArtifactId switch name { - case MavenDependencyNameMySqlConnectorJ: + case "com.mysql:mysql-connector-j": databaseDepMap[DbMySql] = struct{}{} - project.RawDependencies = append(project.RawDependencies, - RawDependency{RawDependencyKindMaven, name, dep.Version}) - case MavenDependencyNamePostgresql, MavenDependencyNameSpringCloudAzureStarterJdbcPostgresql: + case "org.postgresql:postgresql", + "com.azure.spring:spring-cloud-azure-starter-jdbc-postgresql": databaseDepMap[DbPostgres] = struct{}{} - project.RawDependencies = append(project.RawDependencies, - RawDependency{RawDependencyKindMaven, name, dep.Version}) } } if len(databaseDepMap) > 0 { @@ -143,17 +140,6 @@ func detectDependencies(mavenProject *mavenProject, project *Project) (*Project, return strings.Compare(string(a), string(b)) }) } - for _, dep := range mavenProject.Build.Plugins { - name := toDependencyName(dep.GroupId, dep.ArtifactId) - if name == MavenDependencyNameSpringBootMavenPlugin { - project.RawDependencies = append(project.RawDependencies, - RawDependency{RawDependencyKindMaven, name, dep.Version}) - } - } - + project.RawProject = *mavenProject return project, nil } - -func toDependencyName(groupId string, artifactId string) string { - return groupId + ":" + artifactId -} diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index c43a5a53f47..379b9ab70c7 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -293,9 +293,13 @@ func (i *Initializer) InitFromApp( for _, proj := range projects { isSpringBootProject := false - for _, dep := range proj.RawDependencies { - if dep.Kind == appdetect.RawDependencyKindMaven && - dep.Name == appdetect.MavenDependencyNameSpringBootMavenPlugin { + mavenProject, ok := proj.RawProject.(appdetect.MavenProject) + if !ok { + continue + } + for _, dep := range mavenProject.Build.Plugins { + if dep.GroupId == "org.springframework.boot" && + dep.ArtifactId == "spring-boot-maven-plugin" { isSpringBootProject = true break } @@ -303,13 +307,11 @@ func (i *Initializer) InitFromApp( if !isSpringBootProject { continue } - for _, dep := range proj.RawDependencies { - if dep.Kind != appdetect.RawDependencyKindMaven { - continue - } - switch dep.Name { - case appdetect.MavenDependencyNamePostgresql, - appdetect.MavenDependencyNameSpringCloudAzureStarterJdbcPostgresql: + for _, dep := range mavenProject.Dependencies { + name := dep.GroupId + ":" + dep.ArtifactId + switch name { + case "org.postgresql:postgresql", + "com.azure.spring:spring-cloud-azure-starter-jdbc-postgresql": err := addPostgresqlConnectionProperties(proj.Path) if err != nil { return err