From 849de05c61c4b1f003be6f80c35eb5d71a77456e Mon Sep 17 00:00:00 2001 From: rujche Date: Fri, 6 Sep 2024 13:49:13 +0800 Subject: [PATCH 01/39] Add a new child action "orchestrate". And create a sample azure.yaml file. --- cli/azd/cmd/orchestrate.go | 73 ++++++++++++++++++++++++++++++++++++++ cli/azd/cmd/root.go | 13 +++++++ 2 files changed, 86 insertions(+) create mode 100644 cli/azd/cmd/orchestrate.go diff --git a/cli/azd/cmd/orchestrate.go b/cli/azd/cmd/orchestrate.go new file mode 100644 index 00000000000..1e43c1d1255 --- /dev/null +++ b/cli/azd/cmd/orchestrate.go @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package cmd + +import ( + "context" + "fmt" + "github.com/azure/azure-dev/cli/azd/cmd/actions" + "github.com/azure/azure-dev/cli/azd/internal" + "github.com/azure/azure-dev/cli/azd/pkg/output" + "github.com/spf13/cobra" + "os" +) + +func newOrchestrateFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *orchestrateFlags { + flags := &orchestrateFlags{} + return flags +} + +func newOrchestrateCmd() *cobra.Command { + return &cobra.Command{ + Use: "orchestrate", + Short: "Orchestrate an existing application. (Beta)", + } +} + +type orchestrateFlags struct { + global *internal.GlobalCommandOptions +} + +type orchestrateAction struct { +} + +func (action orchestrateAction) Run(ctx context.Context) (*actions.ActionResult, error) { + azureYamlFile, err := os.Create("azure.yaml") + if err != nil { + return nil, fmt.Errorf("creating azure.yaml: %w", err) + } + defer azureYamlFile.Close() + + if _, err := azureYamlFile.WriteString("Test Content in azure.yaml\n"); err != nil { + return nil, fmt.Errorf("saving azure.yaml: %w", err) + } + + if err := azureYamlFile.Sync(); err != nil { + return nil, fmt.Errorf("saving azure.yaml: %w", err) + } + return nil, nil +} + +func newOrchestrateAction() actions.Action { + return &orchestrateAction{} +} + +func getCmdOrchestrateHelpDescription(*cobra.Command) string { + return generateCmdHelpDescription("Orchestrate an existing application in your current directory.", + []string{ + formatHelpNote( + fmt.Sprintf("Running %s without flags specified will prompt "+ + "you to orchestrate using your existing code.", + output.WithHighLightFormat("orchestrate"), + )), + }) +} + +func getCmdOrchestrateHelpFooter(*cobra.Command) string { + return generateCmdHelpSamplesBlock(map[string]string{ + "Orchestrate a existing project.": fmt.Sprintf("%s", + output.WithHighLightFormat("azd orchestrate"), + ), + }) +} diff --git a/cli/azd/cmd/root.go b/cli/azd/cmd/root.go index 7b0c9f0b501..01ae9295dac 100644 --- a/cli/azd/cmd/root.go +++ b/cli/azd/cmd/root.go @@ -178,6 +178,19 @@ func NewRootCmd( ActionResolver: newLogoutAction, }) + root.Add("orchestrate", &actions.ActionDescriptorOptions{ + Command: newOrchestrateCmd(), + FlagsResolver: newOrchestrateFlags, + ActionResolver: newOrchestrateAction, + HelpOptions: actions.ActionHelpOptions{ + Description: getCmdOrchestrateHelpDescription, + Footer: getCmdOrchestrateHelpFooter, + }, + GroupingOptions: actions.CommandGroupOptions{ + RootLevelHelp: actions.CmdGroupConfig, + }, + }) + root.Add("init", &actions.ActionDescriptorOptions{ Command: newInitCmd(), FlagsResolver: newInitFlags, From acee60b39f314bc8573df8ca1bcff0ad270160ac Mon Sep 17 00:00:00 2001 From: rujche Date: Mon, 9 Sep 2024 10:55:01 +0800 Subject: [PATCH 02/39] Get list of pom files. --- cli/azd/cmd/orchestrate.go | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/cli/azd/cmd/orchestrate.go b/cli/azd/cmd/orchestrate.go index 1e43c1d1255..d7413f93e24 100644 --- a/cli/azd/cmd/orchestrate.go +++ b/cli/azd/cmd/orchestrate.go @@ -11,6 +11,7 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/output" "github.com/spf13/cobra" "os" + "path/filepath" ) func newOrchestrateFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *orchestrateFlags { @@ -39,8 +40,16 @@ func (action orchestrateAction) Run(ctx context.Context) (*actions.ActionResult, } defer azureYamlFile.Close() - if _, err := azureYamlFile.WriteString("Test Content in azure.yaml\n"); err != nil { - return nil, fmt.Errorf("saving azure.yaml: %w", err) + files, err := findPomFiles(".") + if err != nil { + fmt.Println("Error:", err) + return nil, fmt.Errorf("find pom files: %w", err) + } + + for _, file := range files { + if _, err := azureYamlFile.WriteString(file + "\n"); err != nil { + return nil, fmt.Errorf("writing azure.yaml: %w", err) + } } if err := azureYamlFile.Sync(); err != nil { @@ -71,3 +80,17 @@ func getCmdOrchestrateHelpFooter(*cobra.Command) string { ), }) } + +func findPomFiles(root string) ([]string, error) { + var files []string + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() && filepath.Base(path) == "pom.xml" { + files = append(files, path) + } + return nil + }) + return files, err +} From 88d4d1b9d3287ded345da5f97be6ba1cfed941d7 Mon Sep 17 00:00:00 2001 From: Xiaolu Dai Date: Tue, 10 Sep 2024 14:45:39 +0800 Subject: [PATCH 03/39] add the code for azd java analyzer --- cli/azd/analyze/java_project.go | 55 +++++++++++++++++++++++++ cli/azd/analyze/main.go | 69 ++++++++++++++++++++++++++++++++ cli/azd/analyze/pom_analyzer.go | 71 +++++++++++++++++++++++++++++++++ cli/azd/analyze/rule_engine.go | 25 ++++++++++++ 4 files changed, 220 insertions(+) create mode 100644 cli/azd/analyze/java_project.go create mode 100644 cli/azd/analyze/main.go create mode 100644 cli/azd/analyze/pom_analyzer.go create mode 100644 cli/azd/analyze/rule_engine.go diff --git a/cli/azd/analyze/java_project.go b/cli/azd/analyze/java_project.go new file mode 100644 index 00000000000..a1be739a04b --- /dev/null +++ b/cli/azd/analyze/java_project.go @@ -0,0 +1,55 @@ +package main + +type JavaProject struct { + Services []ServiceConfig `json:"services"` + Resources []Resource `json:"resources"` + ServiceBindings []ServiceBinding `json:"serviceBindings"` +} + +type Resource struct { + Name string `json:"name"` + Type string `json:"type"` + BicepParameters []BicepParameter `json:"bicepParameters"` + BicepProperties []BicepProperty `json:"bicepProperties"` +} + +type BicepParameter struct { + Name string `json:"name"` + Description string `json:"description"` + Type string `json:"type"` +} + +type BicepProperty struct { + Name string `json:"name"` + Description string `json:"description"` + Type string `json:"type"` +} + +type ResourceType int32 + +const ( + RESOURCE_TYPE_MYSQL ResourceType = 0 + RESOURCE_TYPE_AZURE_STORAGE ResourceType = 1 +) + +// ServiceConfig represents a specific service's configuration. +type ServiceConfig struct { + Name string `json:"name"` + ResourceURI string `json:"resourceUri"` + Description string `json:"description"` +} + +type ServiceBinding struct { + Name string `json:"name"` + ResourceURI string `json:"resourceUri"` + AuthType AuthType `json:"authType"` +} + +type AuthType int32 + +const ( + // Authentication type not specified. + AuthType_SYSTEM_MANAGED_IDENTITY AuthType = 0 + // Username and Password Authentication. + AuthType_USER_PASSWORD AuthType = 1 +) diff --git a/cli/azd/analyze/main.go b/cli/azd/analyze/main.go new file mode 100644 index 00000000000..f180e661336 --- /dev/null +++ b/cli/azd/analyze/main.go @@ -0,0 +1,69 @@ +package main + +import ( + "fmt" + "os" +) + +// Main function. +func main() { + if len(os.Args) < 2 { + fmt.Println("Usage: go run main.go [path-to-pom.xml]") + os.Exit(1) + } + + pomPath := os.Args[1] + project, err := ParsePOM(pomPath) + if err != nil { + fmt.Printf("Failed to parse POM file: %s\n", err) + os.Exit(1) + } + + fmt.Println("Dependencies found:") + for _, dep := range project.Dependencies { + fmt.Printf("- GroupId: %s, ArtifactId: %s, Version: %s, Scope: %s\n", + dep.GroupId, dep.ArtifactId, dep.Version, dep.Scope) + } + + fmt.Println("Dependency Management:") + for _, dep := range project.DependencyManagement.Dependencies { + fmt.Printf("- GroupId: %s, ArtifactId: %s, Version: %s\n", + dep.GroupId, dep.ArtifactId, dep.Version) + } + + fmt.Println("Plugins used in Build:") + for _, plugin := range project.Build.Plugins { + fmt.Printf("- GroupId: %s, ArtifactId: %s, Version: %s\n", + plugin.GroupId, plugin.ArtifactId, plugin.Version) + } + + if project.Parent.GroupId != "" { + fmt.Printf("Parent POM: GroupId: %s, ArtifactId: %s, Version: %s\n", + project.Parent.GroupId, project.Parent.ArtifactId, project.Parent.Version) + } + + //ApplyRules(project, []Rule{ + // { + // Match: func(mavenProject MavenProject) bool { + // for _, dep := range mavenProject.Dependencies { + // if dep.GroupId == "com.mysql" && dep.ArtifactId == "mysql-connector-java" { + // return true + // } + // } + // return false + // }, + // Apply: func(javaProject *JavaProject) { + // append(javaProject.Resources, Resource{ + // Name: "mysql", + // Type: "mysql", + // BicepParameters: []BicepParameter{ + // { + // Name: "serverName", + // }, + // } + // }) + // }, + // }, + //}) + +} diff --git a/cli/azd/analyze/pom_analyzer.go b/cli/azd/analyze/pom_analyzer.go new file mode 100644 index 00000000000..9270b8595a0 --- /dev/null +++ b/cli/azd/analyze/pom_analyzer.go @@ -0,0 +1,71 @@ +package main + +import ( + "encoding/xml" + "fmt" + "io/ioutil" + "os" +) + +// MavenProject represents the top-level structure of a Maven POM file. +type MavenProject struct { + XMLName xml.Name `xml:"project"` + Parent Parent `xml:"parent"` + Dependencies []Dependency `xml:"dependencies>dependency"` + DependencyManagement DependencyManagement `xml:"dependencyManagement"` + Build Build `xml:"build"` +} + +// Parent represents the parent POM if this project is a module. +type Parent struct { + GroupId string `xml:"groupId"` + ArtifactId string `xml:"artifactId"` + Version string `xml:"version"` +} + +// Dependency represents a single Maven dependency. +type Dependency struct { + GroupId string `xml:"groupId"` + ArtifactId string `xml:"artifactId"` + Version string `xml:"version"` + Scope string `xml:"scope,omitempty"` +} + +// DependencyManagement includes a list of dependencies that are managed. +type DependencyManagement struct { + Dependencies []Dependency `xml:"dependencies>dependency"` +} + +// Build represents the build configuration which can contain plugins. +type Build struct { + Plugins []Plugin `xml:"plugins>plugin"` +} + +// Plugin represents a build plugin. +type Plugin struct { + GroupId string `xml:"groupId"` + ArtifactId string `xml:"artifactId"` + Version string `xml:"version"` + //Configuration xml.Node `xml:"configuration"` +} + +// ParsePOM Parse the POM file. +func ParsePOM(filePath string) (*MavenProject, error) { + xmlFile, err := os.Open(filePath) + if err != nil { + return nil, fmt.Errorf("error opening file: %w", err) + } + defer xmlFile.Close() + + bytes, err := ioutil.ReadAll(xmlFile) + if err != nil { + return nil, fmt.Errorf("error reading file: %w", err) + } + + var project MavenProject + if err := xml.Unmarshal(bytes, &project); err != nil { + return nil, fmt.Errorf("error parsing XML: %w", err) + } + + return &project, nil +} diff --git a/cli/azd/analyze/rule_engine.go b/cli/azd/analyze/rule_engine.go new file mode 100644 index 00000000000..6be5c709121 --- /dev/null +++ b/cli/azd/analyze/rule_engine.go @@ -0,0 +1,25 @@ +package main + +type Rule struct { + Match func(MavenProject) bool + Apply func(*JavaProject) +} + +func matchesRule(mavenProject MavenProject, rule Rule) bool { + return rule.Match(mavenProject) +} + +func applyOperation(javaProject *JavaProject, rule Rule) { + rule.Apply(javaProject) +} + +func ApplyRules(mavenProject MavenProject, rules []Rule) error { + javaProject := &JavaProject{} + + for _, rule := range rules { + if matchesRule(mavenProject, rule) { + applyOperation(javaProject, rule) + } + } + return nil +} From 6ba08169f20906d0bd287dd002d73fe91f787847 Mon Sep 17 00:00:00 2001 From: Xiaolu Dai Date: Wed, 11 Sep 2024 10:57:59 +0800 Subject: [PATCH 04/39] Hook the java analyzer with the azd init. --- cli/azd/analyze/main.go | 69 ------------------- cli/azd/internal/appdetect/appdetect.go | 16 +++++ .../appdetect/javaanalyze/java_analyzer.go | 6 ++ .../appdetect/javaanalyze}/java_project.go | 2 +- .../appdetect/javaanalyze}/pom_analyzer.go | 2 +- .../appdetect/javaanalyze}/rule_engine.go | 2 +- 6 files changed, 25 insertions(+), 72 deletions(-) delete mode 100644 cli/azd/analyze/main.go create mode 100644 cli/azd/internal/appdetect/javaanalyze/java_analyzer.go rename cli/azd/{analyze => internal/appdetect/javaanalyze}/java_project.go (98%) rename cli/azd/{analyze => internal/appdetect/javaanalyze}/pom_analyzer.go (98%) rename cli/azd/{analyze => internal/appdetect/javaanalyze}/rule_engine.go (96%) diff --git a/cli/azd/analyze/main.go b/cli/azd/analyze/main.go deleted file mode 100644 index f180e661336..00000000000 --- a/cli/azd/analyze/main.go +++ /dev/null @@ -1,69 +0,0 @@ -package main - -import ( - "fmt" - "os" -) - -// Main function. -func main() { - if len(os.Args) < 2 { - fmt.Println("Usage: go run main.go [path-to-pom.xml]") - os.Exit(1) - } - - pomPath := os.Args[1] - project, err := ParsePOM(pomPath) - if err != nil { - fmt.Printf("Failed to parse POM file: %s\n", err) - os.Exit(1) - } - - fmt.Println("Dependencies found:") - for _, dep := range project.Dependencies { - fmt.Printf("- GroupId: %s, ArtifactId: %s, Version: %s, Scope: %s\n", - dep.GroupId, dep.ArtifactId, dep.Version, dep.Scope) - } - - fmt.Println("Dependency Management:") - for _, dep := range project.DependencyManagement.Dependencies { - fmt.Printf("- GroupId: %s, ArtifactId: %s, Version: %s\n", - dep.GroupId, dep.ArtifactId, dep.Version) - } - - fmt.Println("Plugins used in Build:") - for _, plugin := range project.Build.Plugins { - fmt.Printf("- GroupId: %s, ArtifactId: %s, Version: %s\n", - plugin.GroupId, plugin.ArtifactId, plugin.Version) - } - - if project.Parent.GroupId != "" { - fmt.Printf("Parent POM: GroupId: %s, ArtifactId: %s, Version: %s\n", - project.Parent.GroupId, project.Parent.ArtifactId, project.Parent.Version) - } - - //ApplyRules(project, []Rule{ - // { - // Match: func(mavenProject MavenProject) bool { - // for _, dep := range mavenProject.Dependencies { - // if dep.GroupId == "com.mysql" && dep.ArtifactId == "mysql-connector-java" { - // return true - // } - // } - // return false - // }, - // Apply: func(javaProject *JavaProject) { - // append(javaProject.Resources, Resource{ - // Name: "mysql", - // Type: "mysql", - // BicepParameters: []BicepParameter{ - // { - // Name: "serverName", - // }, - // } - // }) - // }, - // }, - //}) - -} diff --git a/cli/azd/internal/appdetect/appdetect.go b/cli/azd/internal/appdetect/appdetect.go index b903f92dad5..4f1ba8b522c 100644 --- a/cli/azd/internal/appdetect/appdetect.go +++ b/cli/azd/internal/appdetect/appdetect.go @@ -12,6 +12,7 @@ import ( "os" "path/filepath" + "github.com/azure/azure-dev/cli/azd/internal/appdetect/javaanalyze" "github.com/azure/azure-dev/cli/azd/pkg/exec" "github.com/azure/azure-dev/cli/azd/pkg/tools/dotnet" "github.com/bmatcuk/doublestar/v4" @@ -243,6 +244,9 @@ func detectUnder(ctx context.Context, root string, config detectConfig) ([]Proje return nil, fmt.Errorf("scanning directories: %w", err) } + // call the java analyzer + analyze(projects) + return projects, nil } @@ -306,3 +310,15 @@ func walkDirectories(path string, fn walkDirFunc) error { return nil } + +func analyze(projects []Project) []Project { + for _, project := range projects { + if project.Language == Java { + fmt.Printf("Java project [%s] found\n", project.Path) + javaanalyze.Analyze(project.Path) + // analyze the java projects + } + + } + return projects +} diff --git a/cli/azd/internal/appdetect/javaanalyze/java_analyzer.go b/cli/azd/internal/appdetect/javaanalyze/java_analyzer.go new file mode 100644 index 00000000000..31f6f6653d2 --- /dev/null +++ b/cli/azd/internal/appdetect/javaanalyze/java_analyzer.go @@ -0,0 +1,6 @@ +package javaanalyze + +func Analyze(path string) []JavaProject { + + return nil +} diff --git a/cli/azd/analyze/java_project.go b/cli/azd/internal/appdetect/javaanalyze/java_project.go similarity index 98% rename from cli/azd/analyze/java_project.go rename to cli/azd/internal/appdetect/javaanalyze/java_project.go index a1be739a04b..7ff2e448d84 100644 --- a/cli/azd/analyze/java_project.go +++ b/cli/azd/internal/appdetect/javaanalyze/java_project.go @@ -1,4 +1,4 @@ -package main +package javaanalyze type JavaProject struct { Services []ServiceConfig `json:"services"` diff --git a/cli/azd/analyze/pom_analyzer.go b/cli/azd/internal/appdetect/javaanalyze/pom_analyzer.go similarity index 98% rename from cli/azd/analyze/pom_analyzer.go rename to cli/azd/internal/appdetect/javaanalyze/pom_analyzer.go index 9270b8595a0..bda6ea2fbce 100644 --- a/cli/azd/analyze/pom_analyzer.go +++ b/cli/azd/internal/appdetect/javaanalyze/pom_analyzer.go @@ -1,4 +1,4 @@ -package main +package javaanalyze import ( "encoding/xml" diff --git a/cli/azd/analyze/rule_engine.go b/cli/azd/internal/appdetect/javaanalyze/rule_engine.go similarity index 96% rename from cli/azd/analyze/rule_engine.go rename to cli/azd/internal/appdetect/javaanalyze/rule_engine.go index 6be5c709121..ac23214fbc5 100644 --- a/cli/azd/analyze/rule_engine.go +++ b/cli/azd/internal/appdetect/javaanalyze/rule_engine.go @@ -1,4 +1,4 @@ -package main +package javaanalyze type Rule struct { Match func(MavenProject) bool From 6a4b6649d74bb96872a80bf4c023c7efc9d8c799 Mon Sep 17 00:00:00 2001 From: Xiaolu Dai Date: Wed, 11 Sep 2024 17:03:10 +0800 Subject: [PATCH 05/39] Enhance --- cli/azd/internal/appdetect/appdetect.go | 2 +- .../appdetect/javaanalyze/java_analyzer.go | 32 ++++++++++++++++++- .../appdetect/javaanalyze/mysqlrule.go | 22 +++++++++++++ .../appdetect/javaanalyze/pom_analyzer.go | 1 + .../appdetect/javaanalyze/rule_engine.go | 24 +++++--------- 5 files changed, 63 insertions(+), 18 deletions(-) create mode 100644 cli/azd/internal/appdetect/javaanalyze/mysqlrule.go diff --git a/cli/azd/internal/appdetect/appdetect.go b/cli/azd/internal/appdetect/appdetect.go index 4f1ba8b522c..eadba8fa9e9 100644 --- a/cli/azd/internal/appdetect/appdetect.go +++ b/cli/azd/internal/appdetect/appdetect.go @@ -314,7 +314,7 @@ func walkDirectories(path string, fn walkDirFunc) error { func analyze(projects []Project) []Project { for _, project := range projects { if project.Language == Java { - fmt.Printf("Java project [%s] found\n", project.Path) + fmt.Printf("Java project [%s] found", project.Path) javaanalyze.Analyze(project.Path) // analyze the java projects } diff --git a/cli/azd/internal/appdetect/javaanalyze/java_analyzer.go b/cli/azd/internal/appdetect/javaanalyze/java_analyzer.go index 31f6f6653d2..198450cff3f 100644 --- a/cli/azd/internal/appdetect/javaanalyze/java_analyzer.go +++ b/cli/azd/internal/appdetect/javaanalyze/java_analyzer.go @@ -1,6 +1,36 @@ package javaanalyze +import ( + "os" +) + func Analyze(path string) []JavaProject { + result := []JavaProject{} + rules := []rule{ + &mysqlRule{}, + } + + entries, err := os.ReadDir(path) + if err == nil { + for _, entry := range entries { + if "pom.xml" == entry.Name() { + mavenProject, _ := ParsePOM(path + "/" + entry.Name()) + + // if it has submodules + if len(mavenProject.Modules) > 0 { + for _, m := range mavenProject.Modules { + // analyze the submodules + subModule, _ := ParsePOM(path + "/" + m + "/pom.xml") + javaProject, _ := ApplyRules(subModule, rules) + result = append(result, *javaProject) + } + } else { + // analyze the maven project + } + } + //fmt.Printf("\tentry: %s", entry.Name()) + } + } - return nil + return result } diff --git a/cli/azd/internal/appdetect/javaanalyze/mysqlrule.go b/cli/azd/internal/appdetect/javaanalyze/mysqlrule.go new file mode 100644 index 00000000000..20cc0157410 --- /dev/null +++ b/cli/azd/internal/appdetect/javaanalyze/mysqlrule.go @@ -0,0 +1,22 @@ +package javaanalyze + +type mysqlRule struct { +} + +func (mr *mysqlRule) Match(mavenProject *MavenProject) bool { + if mavenProject.Dependencies != nil { + for _, dep := range mavenProject.Dependencies { + if dep.GroupId == "com.mysql" && dep.ArtifactId == "mysql-connector-j" { + return true + } + } + } + return false +} + +func (mr *mysqlRule) Apply(javaProject *JavaProject) { + javaProject.Resources = append(javaProject.Resources, Resource{ + Name: "MySQL", + Type: "MySQL", + }) +} diff --git a/cli/azd/internal/appdetect/javaanalyze/pom_analyzer.go b/cli/azd/internal/appdetect/javaanalyze/pom_analyzer.go index bda6ea2fbce..8a5129ae77a 100644 --- a/cli/azd/internal/appdetect/javaanalyze/pom_analyzer.go +++ b/cli/azd/internal/appdetect/javaanalyze/pom_analyzer.go @@ -11,6 +11,7 @@ import ( type MavenProject struct { XMLName xml.Name `xml:"project"` Parent Parent `xml:"parent"` + Modules []string `xml:"modules>module"` // Capture the modules Dependencies []Dependency `xml:"dependencies>dependency"` DependencyManagement DependencyManagement `xml:"dependencyManagement"` Build Build `xml:"build"` diff --git a/cli/azd/internal/appdetect/javaanalyze/rule_engine.go b/cli/azd/internal/appdetect/javaanalyze/rule_engine.go index ac23214fbc5..173cc88096b 100644 --- a/cli/azd/internal/appdetect/javaanalyze/rule_engine.go +++ b/cli/azd/internal/appdetect/javaanalyze/rule_engine.go @@ -1,25 +1,17 @@ package javaanalyze -type Rule struct { - Match func(MavenProject) bool - Apply func(*JavaProject) +type rule interface { + Match(*MavenProject) bool + Apply(*JavaProject) } -func matchesRule(mavenProject MavenProject, rule Rule) bool { - return rule.Match(mavenProject) -} - -func applyOperation(javaProject *JavaProject, rule Rule) { - rule.Apply(javaProject) -} - -func ApplyRules(mavenProject MavenProject, rules []Rule) error { +func ApplyRules(mavenProject *MavenProject, rules []rule) (*JavaProject, error) { javaProject := &JavaProject{} - for _, rule := range rules { - if matchesRule(mavenProject, rule) { - applyOperation(javaProject, rule) + for _, r := range rules { + if r.Match(mavenProject) { + r.Apply(javaProject) } } - return nil + return javaProject, nil } From bafcbc4f4e11322181d49033e31a23d0dcdddb42 Mon Sep 17 00:00:00 2001 From: rujche Date: Wed, 11 Sep 2024 17:44:23 +0800 Subject: [PATCH 06/39] Implement java_project_bicep_file_generator.go. --- .../java_project_bicep_file_generator.go | 72 +++++++++++++++++++ .../java_project_bicep_file_generator_test.go | 26 +++++++ 2 files changed, 98 insertions(+) create mode 100644 cli/azd/internal/appdetect/javaanalyze/java_project_bicep_file_generator.go create mode 100644 cli/azd/internal/appdetect/javaanalyze/java_project_bicep_file_generator_test.go diff --git a/cli/azd/internal/appdetect/javaanalyze/java_project_bicep_file_generator.go b/cli/azd/internal/appdetect/javaanalyze/java_project_bicep_file_generator.go new file mode 100644 index 00000000000..962000a63a0 --- /dev/null +++ b/cli/azd/internal/appdetect/javaanalyze/java_project_bicep_file_generator.go @@ -0,0 +1,72 @@ +package javaanalyze + +import ( + "fmt" + "log" + "os" + "path/filepath" +) + +func GenerateBicepFilesForJavaProject(outputDirectory string, project JavaProject) error { + log.Printf("Generating bicep files for java project.") + err := GenerateMainDotBicep(outputDirectory) + if err != nil { + return err + } + for _, resource := range project.Resources { + err := GenerateBicepFileForResource(outputDirectory, resource) + if err != nil { + return err + } + } + for _, service := range project.Services { + err := GenerateBicepFileForService(outputDirectory, service) + if err != nil { + return err + } + } + for _, binding := range project.ServiceBindings { + err := GenerateBicepFileForBinding(outputDirectory, binding) + if err != nil { + return err + } + } + return nil +} + +func GenerateMainDotBicep(outputDirectory string) error { + log.Printf("Generating main.bicep.") + bicepFileName := filepath.Join(outputDirectory, "main.bicep") + return GenerateBicepFile(bicepFileName, "placeholder") +} + +func GenerateBicepFileForResource(outputDirectory string, resource Resource) error { + log.Printf("Generating bicep file for resource: %s.", resource.Name) + bicepFileName := filepath.Join(outputDirectory, resource.Name+".bicep") + return GenerateBicepFile(bicepFileName, "placeholder") +} + +func GenerateBicepFileForService(outputDirectory string, service ServiceConfig) error { + log.Printf("Generating bicep file for service config: %s.", service.Name) + bicepFileName := filepath.Join(outputDirectory, service.Name+".bicep") + return GenerateBicepFile(bicepFileName, "placeholder") +} + +func GenerateBicepFileForBinding(outputDirectory string, binding ServiceBinding) error { + log.Printf("Generating bicep file for service binding: %s.", binding.Name) + bicepFileName := filepath.Join(outputDirectory, binding.Name+".bicep") + return GenerateBicepFile(bicepFileName, "placeholder") +} + +func GenerateBicepFile(fileName string, content string) error { + bicepFile, err := os.Create(fileName) + if err != nil { + return fmt.Errorf("creating %s: %w", fileName, err) + } + defer bicepFile.Close() + if _, err := bicepFile.WriteString(content); err != nil { + return fmt.Errorf("writing %s: %w", fileName, err) + } + return nil + +} diff --git a/cli/azd/internal/appdetect/javaanalyze/java_project_bicep_file_generator_test.go b/cli/azd/internal/appdetect/javaanalyze/java_project_bicep_file_generator_test.go new file mode 100644 index 00000000000..a0b830f2616 --- /dev/null +++ b/cli/azd/internal/appdetect/javaanalyze/java_project_bicep_file_generator_test.go @@ -0,0 +1,26 @@ +package javaanalyze + +import ( + "fmt" + "github.com/stretchr/testify/require" + "testing" +) + +func TestGenerateBicepFilesForJavaProject(t *testing.T) { + javaProject := JavaProject{ + Services: []ServiceConfig{}, + Resources: []Resource{ + { + Name: "mysql_one", + Type: "mysql", + BicepParameters: nil, + BicepProperties: nil, + }, + }, + ServiceBindings: []ServiceBinding{}, + } + dir := t.TempDir() + fmt.Printf("dir:%s\n", dir) + err := GenerateBicepFilesForJavaProject(dir, javaProject) + require.NoError(t, err) +} From b69a47ed6058bf7f1adf5b20112806eb024b9ca2 Mon Sep 17 00:00:00 2001 From: rujche Date: Wed, 11 Sep 2024 17:53:36 +0800 Subject: [PATCH 07/39] Improve log: Add information about the file path. --- .../javaanalyze/java_project_bicep_file_generator.go | 5 +---- .../javaanalyze/java_project_bicep_file_generator_test.go | 2 -- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/cli/azd/internal/appdetect/javaanalyze/java_project_bicep_file_generator.go b/cli/azd/internal/appdetect/javaanalyze/java_project_bicep_file_generator.go index 962000a63a0..611586a6a01 100644 --- a/cli/azd/internal/appdetect/javaanalyze/java_project_bicep_file_generator.go +++ b/cli/azd/internal/appdetect/javaanalyze/java_project_bicep_file_generator.go @@ -35,30 +35,27 @@ func GenerateBicepFilesForJavaProject(outputDirectory string, project JavaProjec } func GenerateMainDotBicep(outputDirectory string) error { - log.Printf("Generating main.bicep.") bicepFileName := filepath.Join(outputDirectory, "main.bicep") return GenerateBicepFile(bicepFileName, "placeholder") } func GenerateBicepFileForResource(outputDirectory string, resource Resource) error { - log.Printf("Generating bicep file for resource: %s.", resource.Name) bicepFileName := filepath.Join(outputDirectory, resource.Name+".bicep") return GenerateBicepFile(bicepFileName, "placeholder") } func GenerateBicepFileForService(outputDirectory string, service ServiceConfig) error { - log.Printf("Generating bicep file for service config: %s.", service.Name) bicepFileName := filepath.Join(outputDirectory, service.Name+".bicep") return GenerateBicepFile(bicepFileName, "placeholder") } func GenerateBicepFileForBinding(outputDirectory string, binding ServiceBinding) error { - log.Printf("Generating bicep file for service binding: %s.", binding.Name) bicepFileName := filepath.Join(outputDirectory, binding.Name+".bicep") return GenerateBicepFile(bicepFileName, "placeholder") } func GenerateBicepFile(fileName string, content string) error { + log.Printf("Generating bicep file: %s.", fileName) bicepFile, err := os.Create(fileName) if err != nil { return fmt.Errorf("creating %s: %w", fileName, err) diff --git a/cli/azd/internal/appdetect/javaanalyze/java_project_bicep_file_generator_test.go b/cli/azd/internal/appdetect/javaanalyze/java_project_bicep_file_generator_test.go index a0b830f2616..7deb72f9e97 100644 --- a/cli/azd/internal/appdetect/javaanalyze/java_project_bicep_file_generator_test.go +++ b/cli/azd/internal/appdetect/javaanalyze/java_project_bicep_file_generator_test.go @@ -1,7 +1,6 @@ package javaanalyze import ( - "fmt" "github.com/stretchr/testify/require" "testing" ) @@ -20,7 +19,6 @@ func TestGenerateBicepFilesForJavaProject(t *testing.T) { ServiceBindings: []ServiceBinding{}, } dir := t.TempDir() - fmt.Printf("dir:%s\n", dir) err := GenerateBicepFilesForJavaProject(dir, javaProject) require.NoError(t, err) } From 69bc59a9009dcc94d74161b6306783f6aac609e3 Mon Sep 17 00:00:00 2001 From: Xiaolu Dai Date: Fri, 13 Sep 2024 14:23:03 +0800 Subject: [PATCH 08/39] Enhance the java analyzer --- cli/azd/internal/appdetect/appdetect.go | 35 +++++++++++++++-- .../appdetect/javaanalyze/java_analyzer.go | 6 ++- .../appdetect/javaanalyze/java_project.go | 19 ++++++--- .../appdetect/javaanalyze/pom_analyzer.go | 3 ++ .../appdetect/javaanalyze/rule_mongo.go | 27 +++++++++++++ .../{mysqlrule.go => rule_mysql.go} | 11 ++++-- .../appdetect/javaanalyze/rule_redis.go | 16 ++++++++ .../appdetect/javaanalyze/rule_service.go | 17 ++++++++ .../appdetect/javaanalyze/rule_storage.go | 39 +++++++++++++++++++ 9 files changed, 159 insertions(+), 14 deletions(-) create mode 100644 cli/azd/internal/appdetect/javaanalyze/rule_mongo.go rename cli/azd/internal/appdetect/javaanalyze/{mysqlrule.go => rule_mysql.go} (53%) create mode 100644 cli/azd/internal/appdetect/javaanalyze/rule_redis.go create mode 100644 cli/azd/internal/appdetect/javaanalyze/rule_service.go create mode 100644 cli/azd/internal/appdetect/javaanalyze/rule_storage.go diff --git a/cli/azd/internal/appdetect/appdetect.go b/cli/azd/internal/appdetect/appdetect.go index eadba8fa9e9..b7a24b137a4 100644 --- a/cli/azd/internal/appdetect/appdetect.go +++ b/cli/azd/internal/appdetect/appdetect.go @@ -245,7 +245,7 @@ func detectUnder(ctx context.Context, root string, config detectConfig) ([]Proje } // call the java analyzer - analyze(projects) + projects = analyze(projects) return projects, nil } @@ -312,13 +312,40 @@ func walkDirectories(path string, fn walkDirFunc) error { } func analyze(projects []Project) []Project { + result := []Project{} for _, project := range projects { if project.Language == Java { fmt.Printf("Java project [%s] found", project.Path) - javaanalyze.Analyze(project.Path) - // analyze the java projects + _javaProjects := javaanalyze.Analyze(project.Path) + + if len(_javaProjects) == 1 { + enrichFromJavaProject(_javaProjects[0], &project) + result = append(result, project) + } else { + for _, _project := range _javaProjects { + copiedProject := project + enrichFromJavaProject(_project, &copiedProject) + result = append(result, copiedProject) + } + } } + } + return result +} +func enrichFromJavaProject(javaProject javaanalyze.JavaProject, project *Project) { + // if there is only one project, we can safely assume that it is the main project + for _, resource := range javaProject.Resources { + if resource.Type == "Azure Storage" { + // project.DatabaseDeps = append(project.DatabaseDeps, Db) + } else if resource.Type == "MySQL" { + project.DatabaseDeps = append(project.DatabaseDeps, DbMySql) + } else if resource.Type == "PostgreSQL" { + project.DatabaseDeps = append(project.DatabaseDeps, DbPostgres) + } else if resource.Type == "SQL Server" { + project.DatabaseDeps = append(project.DatabaseDeps, DbSqlServer) + } else if resource.Type == "Redis" { + project.DatabaseDeps = append(project.DatabaseDeps, DbRedis) + } } - return projects } diff --git a/cli/azd/internal/appdetect/javaanalyze/java_analyzer.go b/cli/azd/internal/appdetect/javaanalyze/java_analyzer.go index 198450cff3f..b3b489de7bb 100644 --- a/cli/azd/internal/appdetect/javaanalyze/java_analyzer.go +++ b/cli/azd/internal/appdetect/javaanalyze/java_analyzer.go @@ -7,7 +7,9 @@ import ( func Analyze(path string) []JavaProject { result := []JavaProject{} rules := []rule{ - &mysqlRule{}, + &ruleService{}, + &ruleMysql{}, + &ruleStorage{}, } entries, err := os.ReadDir(path) @@ -26,6 +28,8 @@ func Analyze(path string) []JavaProject { } } else { // analyze the maven project + javaProject, _ := ApplyRules(mavenProject, rules) + result = append(result, *javaProject) } } //fmt.Printf("\tentry: %s", entry.Name()) diff --git a/cli/azd/internal/appdetect/javaanalyze/java_project.go b/cli/azd/internal/appdetect/javaanalyze/java_project.go index 7ff2e448d84..9b494d24426 100644 --- a/cli/azd/internal/appdetect/javaanalyze/java_project.go +++ b/cli/azd/internal/appdetect/javaanalyze/java_project.go @@ -1,7 +1,7 @@ package javaanalyze type JavaProject struct { - Services []ServiceConfig `json:"services"` + Service *Service `json:"service"` Resources []Resource `json:"resources"` ServiceBindings []ServiceBinding `json:"serviceBindings"` } @@ -32,11 +32,18 @@ const ( RESOURCE_TYPE_AZURE_STORAGE ResourceType = 1 ) -// ServiceConfig represents a specific service's configuration. -type ServiceConfig struct { - Name string `json:"name"` - ResourceURI string `json:"resourceUri"` - Description string `json:"description"` +// Service represents a specific service's configuration. +type Service struct { + Name string `json:"name"` + Path string `json:"path"` + ResourceURI string `json:"resourceUri"` + Description string `json:"description"` + Environment []Environment `json:"environment"` +} + +type Environment struct { + Name string `json:"name"` + Value string `json:"value"` } type ServiceBinding struct { diff --git a/cli/azd/internal/appdetect/javaanalyze/pom_analyzer.go b/cli/azd/internal/appdetect/javaanalyze/pom_analyzer.go index 8a5129ae77a..0c7ad862049 100644 --- a/cli/azd/internal/appdetect/javaanalyze/pom_analyzer.go +++ b/cli/azd/internal/appdetect/javaanalyze/pom_analyzer.go @@ -15,6 +15,7 @@ type MavenProject struct { Dependencies []Dependency `xml:"dependencies>dependency"` DependencyManagement DependencyManagement `xml:"dependencyManagement"` Build Build `xml:"build"` + Path string } // Parent represents the parent POM if this project is a module. @@ -68,5 +69,7 @@ func ParsePOM(filePath string) (*MavenProject, error) { return nil, fmt.Errorf("error parsing XML: %w", err) } + project.Path = filePath + return &project, nil } diff --git a/cli/azd/internal/appdetect/javaanalyze/rule_mongo.go b/cli/azd/internal/appdetect/javaanalyze/rule_mongo.go new file mode 100644 index 00000000000..78ee0999c23 --- /dev/null +++ b/cli/azd/internal/appdetect/javaanalyze/rule_mongo.go @@ -0,0 +1,27 @@ +package javaanalyze + +type ruleMongo struct { +} + +func (mr *ruleMongo) Match(mavenProject *MavenProject) bool { + if mavenProject.Dependencies != nil { + for _, dep := range mavenProject.Dependencies { + if dep.GroupId == "org.springframework.boot" && dep.ArtifactId == "spring-boot-starter-data-mongodb" { + return true + } + } + } + return false +} + +func (mr *ruleMongo) Apply(javaProject *JavaProject) { + javaProject.Resources = append(javaProject.Resources, Resource{ + Name: "MongoDB", + Type: "MongoDB", + }) + + javaProject.ServiceBindings = append(javaProject.ServiceBindings, ServiceBinding{ + Name: "MongoDB", + AuthType: AuthType_SYSTEM_MANAGED_IDENTITY, + }) +} diff --git a/cli/azd/internal/appdetect/javaanalyze/mysqlrule.go b/cli/azd/internal/appdetect/javaanalyze/rule_mysql.go similarity index 53% rename from cli/azd/internal/appdetect/javaanalyze/mysqlrule.go rename to cli/azd/internal/appdetect/javaanalyze/rule_mysql.go index 20cc0157410..1029eea1078 100644 --- a/cli/azd/internal/appdetect/javaanalyze/mysqlrule.go +++ b/cli/azd/internal/appdetect/javaanalyze/rule_mysql.go @@ -1,9 +1,9 @@ package javaanalyze -type mysqlRule struct { +type ruleMysql struct { } -func (mr *mysqlRule) Match(mavenProject *MavenProject) bool { +func (mr *ruleMysql) Match(mavenProject *MavenProject) bool { if mavenProject.Dependencies != nil { for _, dep := range mavenProject.Dependencies { if dep.GroupId == "com.mysql" && dep.ArtifactId == "mysql-connector-j" { @@ -14,9 +14,14 @@ func (mr *mysqlRule) Match(mavenProject *MavenProject) bool { return false } -func (mr *mysqlRule) Apply(javaProject *JavaProject) { +func (mr *ruleMysql) Apply(javaProject *JavaProject) { javaProject.Resources = append(javaProject.Resources, Resource{ Name: "MySQL", Type: "MySQL", }) + + javaProject.ServiceBindings = append(javaProject.ServiceBindings, ServiceBinding{ + Name: "MySQL", + AuthType: AuthType_SYSTEM_MANAGED_IDENTITY, + }) } diff --git a/cli/azd/internal/appdetect/javaanalyze/rule_redis.go b/cli/azd/internal/appdetect/javaanalyze/rule_redis.go new file mode 100644 index 00000000000..1f5d437867b --- /dev/null +++ b/cli/azd/internal/appdetect/javaanalyze/rule_redis.go @@ -0,0 +1,16 @@ +package javaanalyze + +type ruleRedis struct { +} + +func (mr *ruleRedis) Match(mavenProject *MavenProject) bool { + + return false +} + +func (mr *ruleRedis) Apply(javaProject *JavaProject) { + javaProject.Resources = append(javaProject.Resources, Resource{ + Name: "Redis", + Type: "Redis", + }) +} diff --git a/cli/azd/internal/appdetect/javaanalyze/rule_service.go b/cli/azd/internal/appdetect/javaanalyze/rule_service.go new file mode 100644 index 00000000000..8e6106d703a --- /dev/null +++ b/cli/azd/internal/appdetect/javaanalyze/rule_service.go @@ -0,0 +1,17 @@ +package javaanalyze + +type ruleService struct { + MavenProject *MavenProject +} + +func (mr *ruleService) Match(mavenProject *MavenProject) bool { + mr.MavenProject = mavenProject + return true +} + +func (mr *ruleService) Apply(javaProject *JavaProject) { + if javaProject.Service == nil { + javaProject.Service = &Service{} + } + javaProject.Service.Path = mr.MavenProject.Path +} diff --git a/cli/azd/internal/appdetect/javaanalyze/rule_storage.go b/cli/azd/internal/appdetect/javaanalyze/rule_storage.go new file mode 100644 index 00000000000..5ec5dd0999b --- /dev/null +++ b/cli/azd/internal/appdetect/javaanalyze/rule_storage.go @@ -0,0 +1,39 @@ +package javaanalyze + +type ruleStorage struct { +} + +func (mr *ruleStorage) Match(mavenProject *MavenProject) bool { + if mavenProject.Dependencies != nil { + for _, dep := range mavenProject.Dependencies { + if dep.GroupId == "com.azure" && dep.ArtifactId == "" { + return true + } + if dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-starter-storage" { + return true + } + if dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-starter-storage-blob" { + return true + } + if dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-starter-storage-file-share" { + return true + } + if dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-starter-storage-queue" { + return true + } + } + } + return false +} + +func (mr *ruleStorage) Apply(javaProject *JavaProject) { + javaProject.Resources = append(javaProject.Resources, Resource{ + Name: "Azure Storage", + Type: "Azure Storage", + }) + + javaProject.ServiceBindings = append(javaProject.ServiceBindings, ServiceBinding{ + Name: "Azure Storage", + AuthType: AuthType_SYSTEM_MANAGED_IDENTITY, + }) +} From 8d67fc5cf605a1731994f8369655130e18fd16f0 Mon Sep 17 00:00:00 2001 From: rujche Date: Fri, 13 Sep 2024 20:48:40 +0800 Subject: [PATCH 09/39] Add feature: Support add mysql when run "azd init". --- cli/azd/internal/repository/app_init.go | 1 + cli/azd/internal/repository/infra_confirm.go | 14 ++++ cli/azd/internal/scaffold/scaffold.go | 11 ++- cli/azd/internal/scaffold/spec.go | 7 ++ .../scaffold/templates/db-mysql.bicept | 74 +++++++++++++++++++ .../templates/host-containerapp.bicept | 7 ++ .../resources/scaffold/templates/main.bicept | 27 ++++++- 7 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 cli/azd/resources/scaffold/templates/db-mysql.bicept diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index 7837a40c1c1..e0dce236a6f 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -34,6 +34,7 @@ var languageMap = map[appdetect.Language]project.ServiceLanguageKind{ var dbMap = map[appdetect.DatabaseDep]struct{}{ appdetect.DbMongo: {}, appdetect.DbPostgres: {}, + appdetect.DbMySql: {}, appdetect.DbRedis: {}, } diff --git a/cli/azd/internal/repository/infra_confirm.go b/cli/azd/internal/repository/infra_confirm.go index 57488b077dc..eead3d61f7f 100644 --- a/cli/azd/internal/repository/infra_confirm.go +++ b/cli/azd/internal/repository/infra_confirm.go @@ -88,6 +88,16 @@ func (i *Initializer) infraSpecFromDetect( spec.DbPostgres = &scaffold.DatabasePostgres{ DatabaseName: dbName, } + break dbPrompt + case appdetect.DbMySql: + if dbName == "" { + i.console.Message(ctx, "Database name is required.") + continue + } + spec.DbMySql = &scaffold.DatabaseMySql{ + DatabaseName: dbName, + } + break dbPrompt } break dbPrompt } @@ -130,6 +140,10 @@ func (i *Initializer) infraSpecFromDetect( serviceSpec.DbPostgres = &scaffold.DatabaseReference{ DatabaseName: spec.DbPostgres.DatabaseName, } + case appdetect.DbMySql: + serviceSpec.DbMySql = &scaffold.DatabaseReference{ + DatabaseName: spec.DbMySql.DatabaseName, + } case appdetect.DbRedis: serviceSpec.DbRedis = &scaffold.DatabaseReference{ DatabaseName: "redis", diff --git a/cli/azd/internal/scaffold/scaffold.go b/cli/azd/internal/scaffold/scaffold.go index b0b4b838969..8a9f5fd60b4 100644 --- a/cli/azd/internal/scaffold/scaffold.go +++ b/cli/azd/internal/scaffold/scaffold.go @@ -122,6 +122,13 @@ func ExecInfra( } } + if spec.DbMySql != nil { + err = Execute(t, "db-mysql.bicep", spec.DbMySql, filepath.Join(infraApp, "db-mysql.bicep")) + if err != nil { + return fmt.Errorf("scaffolding mysql: %w", err) + } + } + if spec.DbPostgres != nil { err = Execute(t, "db-postgres.bicep", spec.DbPostgres, filepath.Join(infraApp, "db-postgres.bicep")) if err != nil { @@ -150,8 +157,8 @@ func ExecInfra( } func preExecExpand(spec *InfraSpec) { - // postgres requires specific password seeding parameters - if spec.DbPostgres != nil { + // postgres and mysql requires specific password seeding parameters + if spec.DbPostgres != nil || spec.DbMySql != nil { spec.Parameters = append(spec.Parameters, Parameter{ Name: "databasePassword", diff --git a/cli/azd/internal/scaffold/spec.go b/cli/azd/internal/scaffold/spec.go index 9788f8c247c..47d525619d4 100644 --- a/cli/azd/internal/scaffold/spec.go +++ b/cli/azd/internal/scaffold/spec.go @@ -11,6 +11,7 @@ type InfraSpec struct { // Databases to create DbPostgres *DatabasePostgres + DbMySql *DatabaseMySql DbCosmosMongo *DatabaseCosmosMongo } @@ -26,6 +27,11 @@ type DatabasePostgres struct { DatabaseName string } +type DatabaseMySql struct { + DatabaseUser string + DatabaseName string +} + type DatabaseCosmosMongo struct { DatabaseName string } @@ -42,6 +48,7 @@ type ServiceSpec struct { // Connection to a database DbPostgres *DatabaseReference + DbMySql *DatabaseReference DbCosmosMongo *DatabaseReference DbRedis *DatabaseReference } diff --git a/cli/azd/resources/scaffold/templates/db-mysql.bicept b/cli/azd/resources/scaffold/templates/db-mysql.bicept new file mode 100644 index 00000000000..9292c057b21 --- /dev/null +++ b/cli/azd/resources/scaffold/templates/db-mysql.bicept @@ -0,0 +1,74 @@ +{{define "db-mysql.bicep" -}} +param serverName string +param location string = resourceGroup().location +param tags object = {} + +param keyVaultName string + +param databaseUser string = 'mysqladmin' +param databaseName string = '{{.DatabaseName}}' +@secure() +param databasePassword string + +param allowAllIPsFirewall bool = false + +resource mysqlServer'Microsoft.DBforMySQL/flexibleServers@2023-06-30' = { + location: location + tags: tags + name: serverName + sku: { + name: 'Standard_B1ms' + tier: 'Burstable' + } + properties: { + version: '8.0.21' + administratorLogin: databaseUser + administratorLoginPassword: databasePassword + storage: { + storageSizeGB: 128 + } + backup: { + backupRetentionDays: 7 + geoRedundantBackup: 'Disabled' + } + highAvailability: { + mode: 'Disabled' + } + } + + resource firewall_all 'firewallRules' = if (allowAllIPsFirewall) { + name: 'allow-all-IPs' + properties: { + startIpAddress: '0.0.0.0' + endIpAddress: '255.255.255.255' + } + } +} + +resource database 'Microsoft.DBforMySQL/flexibleServers/databases@2023-06-30' = { + parent: mysqlServer + name: databaseName + properties: { + // Azure defaults to UTF-8 encoding, override if required. + // charset: 'string' + // collation: 'string' + } +} + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { + name: keyVaultName +} + +resource dbPasswordKey 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { + parent: keyVault + name: 'databasePassword' + properties: { + value: databasePassword + } +} + +output databaseHost string = mysqlServer.properties.fullyQualifiedDomainName +output databaseName string = databaseName +output databaseUser string = databaseUser +output databaseConnectionKey string = 'databasePassword' +{{ end}} diff --git a/cli/azd/resources/scaffold/templates/host-containerapp.bicept b/cli/azd/resources/scaffold/templates/host-containerapp.bicept index 452402f4a96..c8fe0d9b967 100644 --- a/cli/azd/resources/scaffold/templates/host-containerapp.bicept +++ b/cli/azd/resources/scaffold/templates/host-containerapp.bicept @@ -18,6 +18,13 @@ param databaseName string @secure() param databasePassword string {{- end}} +{{- if .DbMySql}} +param databaseHost string +param databaseUser string +param databaseName string +@secure() +param databasePassword string +{{- end}} {{- if .DbRedis}} param redisName string {{- end}} diff --git a/cli/azd/resources/scaffold/templates/main.bicept b/cli/azd/resources/scaffold/templates/main.bicept index b86550124c2..9cf7aa9d3f1 100644 --- a/cli/azd/resources/scaffold/templates/main.bicept +++ b/cli/azd/resources/scaffold/templates/main.bicept @@ -91,7 +91,7 @@ module appsEnv './shared/apps-env.bicep' = { } scope: rg } -{{- if (or .DbCosmosMongo .DbPostgres)}} +{{- if (or (or .DbCosmosMongo .DbPostgres) .DbMySql)}} resource vault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { name: keyVault.outputs.name @@ -111,8 +111,8 @@ module cosmosDb './app/db-cosmos-mongo.bicep' = { scope: rg } {{- end}} -{{- if .DbPostgres}} +{{- if .DbPostgres}} module postgresDb './app/db-postgres.bicep' = { name: 'postgresDb' params: { @@ -126,8 +126,23 @@ module postgresDb './app/db-postgres.bicep' = { scope: rg } {{- end}} -{{- range .Services}} +{{- if .DbMySql}} +module mysqlDb './app/db-mysql.bicep' = { + name: 'mysqlDb' + params: { + serverName: '${abbrs.dBforMySQLServers}${resourceToken}' + location: location + tags: tags + databasePassword: databasePassword + keyVaultName: keyVault.outputs.name + allowAllIPsFirewall: true + } + scope: rg +} +{{- end}} + +{{- range .Services}} module {{bicepName .Name}} './app/{{.Name}}.bicep' = { name: '{{.Name}}' params: { @@ -152,6 +167,12 @@ module {{bicepName .Name}} './app/{{.Name}}.bicep' = { databaseUser: postgresDb.outputs.databaseUser databasePassword: vault.getSecret(postgresDb.outputs.databaseConnectionKey) {{- end}} + {{- if .DbMySql}} + databaseName: mysqlDb.outputs.databaseName + databaseHost: mysqlDb.outputs.databaseHost + databaseUser: mysqlDb.outputs.databaseUser + databasePassword: vault.getSecret(mysqlDb.outputs.databaseConnectionKey) + {{- end}} {{- if (and .Frontend .Frontend.Backends)}} apiUrls: [ {{- range .Frontend.Backends}} From 91900aa6e7cc38c8f0c98530903446c11e84b2f4 Mon Sep 17 00:00:00 2001 From: rujche Date: Fri, 13 Sep 2024 20:49:48 +0800 Subject: [PATCH 10/39] Delete java_project_bicep_file_generator.go. --- .../java_project_bicep_file_generator.go | 69 ------------------- .../java_project_bicep_file_generator_test.go | 24 ------- 2 files changed, 93 deletions(-) delete mode 100644 cli/azd/internal/appdetect/javaanalyze/java_project_bicep_file_generator.go delete mode 100644 cli/azd/internal/appdetect/javaanalyze/java_project_bicep_file_generator_test.go diff --git a/cli/azd/internal/appdetect/javaanalyze/java_project_bicep_file_generator.go b/cli/azd/internal/appdetect/javaanalyze/java_project_bicep_file_generator.go deleted file mode 100644 index 611586a6a01..00000000000 --- a/cli/azd/internal/appdetect/javaanalyze/java_project_bicep_file_generator.go +++ /dev/null @@ -1,69 +0,0 @@ -package javaanalyze - -import ( - "fmt" - "log" - "os" - "path/filepath" -) - -func GenerateBicepFilesForJavaProject(outputDirectory string, project JavaProject) error { - log.Printf("Generating bicep files for java project.") - err := GenerateMainDotBicep(outputDirectory) - if err != nil { - return err - } - for _, resource := range project.Resources { - err := GenerateBicepFileForResource(outputDirectory, resource) - if err != nil { - return err - } - } - for _, service := range project.Services { - err := GenerateBicepFileForService(outputDirectory, service) - if err != nil { - return err - } - } - for _, binding := range project.ServiceBindings { - err := GenerateBicepFileForBinding(outputDirectory, binding) - if err != nil { - return err - } - } - return nil -} - -func GenerateMainDotBicep(outputDirectory string) error { - bicepFileName := filepath.Join(outputDirectory, "main.bicep") - return GenerateBicepFile(bicepFileName, "placeholder") -} - -func GenerateBicepFileForResource(outputDirectory string, resource Resource) error { - bicepFileName := filepath.Join(outputDirectory, resource.Name+".bicep") - return GenerateBicepFile(bicepFileName, "placeholder") -} - -func GenerateBicepFileForService(outputDirectory string, service ServiceConfig) error { - bicepFileName := filepath.Join(outputDirectory, service.Name+".bicep") - return GenerateBicepFile(bicepFileName, "placeholder") -} - -func GenerateBicepFileForBinding(outputDirectory string, binding ServiceBinding) error { - bicepFileName := filepath.Join(outputDirectory, binding.Name+".bicep") - return GenerateBicepFile(bicepFileName, "placeholder") -} - -func GenerateBicepFile(fileName string, content string) error { - log.Printf("Generating bicep file: %s.", fileName) - bicepFile, err := os.Create(fileName) - if err != nil { - return fmt.Errorf("creating %s: %w", fileName, err) - } - defer bicepFile.Close() - if _, err := bicepFile.WriteString(content); err != nil { - return fmt.Errorf("writing %s: %w", fileName, err) - } - return nil - -} diff --git a/cli/azd/internal/appdetect/javaanalyze/java_project_bicep_file_generator_test.go b/cli/azd/internal/appdetect/javaanalyze/java_project_bicep_file_generator_test.go deleted file mode 100644 index 7deb72f9e97..00000000000 --- a/cli/azd/internal/appdetect/javaanalyze/java_project_bicep_file_generator_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package javaanalyze - -import ( - "github.com/stretchr/testify/require" - "testing" -) - -func TestGenerateBicepFilesForJavaProject(t *testing.T) { - javaProject := JavaProject{ - Services: []ServiceConfig{}, - Resources: []Resource{ - { - Name: "mysql_one", - Type: "mysql", - BicepParameters: nil, - BicepProperties: nil, - }, - }, - ServiceBindings: []ServiceBinding{}, - } - dir := t.TempDir() - err := GenerateBicepFilesForJavaProject(dir, javaProject) - require.NoError(t, err) -} From 2da438ba2a0da23f14ca0accd2e48ab501d18665 Mon Sep 17 00:00:00 2001 From: rujche Date: Sat, 14 Sep 2024 10:18:10 +0800 Subject: [PATCH 11/39] Add logic about accessing mysql in aca. --- cli/azd/internal/repository/detect_confirm.go | 2 + cli/azd/internal/scaffold/scaffold.go | 12 ++-- .../templates/host-containerapp.bicept | 56 ++++++++++++++----- .../resources/scaffold/templates/main.bicept | 16 +++--- .../scaffold/templates/next-steps.mdt | 8 ++- 5 files changed, 65 insertions(+), 29 deletions(-) diff --git a/cli/azd/internal/repository/detect_confirm.go b/cli/azd/internal/repository/detect_confirm.go index 52fb19ad6c4..5124228ed01 100644 --- a/cli/azd/internal/repository/detect_confirm.go +++ b/cli/azd/internal/repository/detect_confirm.go @@ -207,6 +207,8 @@ func (d *detectConfirm) render(ctx context.Context) error { switch db { case appdetect.DbPostgres: recommendedServices = append(recommendedServices, "Azure Database for PostgreSQL flexible server") + case appdetect.DbMySql: + recommendedServices = append(recommendedServices, "Azure Database for MySQL flexible server") case appdetect.DbMongo: recommendedServices = append(recommendedServices, "Azure CosmosDB API for MongoDB") case appdetect.DbRedis: diff --git a/cli/azd/internal/scaffold/scaffold.go b/cli/azd/internal/scaffold/scaffold.go index 8a9f5fd60b4..ae2d876fdc2 100644 --- a/cli/azd/internal/scaffold/scaffold.go +++ b/cli/azd/internal/scaffold/scaffold.go @@ -122,17 +122,17 @@ func ExecInfra( } } - if spec.DbMySql != nil { - err = Execute(t, "db-mysql.bicep", spec.DbMySql, filepath.Join(infraApp, "db-mysql.bicep")) + if spec.DbPostgres != nil { + err = Execute(t, "db-postgres.bicep", spec.DbPostgres, filepath.Join(infraApp, "db-postgres.bicep")) if err != nil { - return fmt.Errorf("scaffolding mysql: %w", err) + return fmt.Errorf("scaffolding postgres: %w", err) } } - if spec.DbPostgres != nil { - err = Execute(t, "db-postgres.bicep", spec.DbPostgres, filepath.Join(infraApp, "db-postgres.bicep")) + if spec.DbMySql != nil { + err = Execute(t, "db-mysql.bicep", spec.DbMySql, filepath.Join(infraApp, "db-mysql.bicep")) if err != nil { - return fmt.Errorf("scaffolding postgres: %w", err) + return fmt.Errorf("scaffolding mysql: %w", err) } } diff --git a/cli/azd/resources/scaffold/templates/host-containerapp.bicept b/cli/azd/resources/scaffold/templates/host-containerapp.bicept index c8fe0d9b967..4b11c95d6f7 100644 --- a/cli/azd/resources/scaffold/templates/host-containerapp.bicept +++ b/cli/azd/resources/scaffold/templates/host-containerapp.bicept @@ -12,18 +12,18 @@ param applicationInsightsName string param cosmosDbConnectionString string {{- end}} {{- if .DbPostgres}} -param databaseHost string -param databaseUser string -param databaseName string +param postgresDatabaseHost string +param postgresDatabaseUser string +param postgresDatabaseName string @secure() -param databasePassword string +param postgresDatabasePassword string {{- end}} {{- if .DbMySql}} -param databaseHost string -param databaseUser string -param databaseName string +param mysqlDatabaseHost string +param mysqlDatabaseUser string +param mysqlDatabaseName string @secure() -param databasePassword string +param mysqlDatabasePassword string {{- end}} {{- if .DbRedis}} param redisName string @@ -149,8 +149,14 @@ resource app 'Microsoft.App/containerApps@2023-05-02-preview' = { {{- end}} {{- if .DbPostgres}} { - name: 'db-pass' - value: databasePassword + name: 'postgres-db-pass' + value: postgresDatabasePassword + } + {{- end}} + {{- if .DbMySql}} + { + name: 'mysql-db-pass' + value: mysqlDatabasePassword } {{- end}} ], @@ -178,25 +184,47 @@ resource app 'Microsoft.App/containerApps@2023-05-02-preview' = { {{- if .DbPostgres}} { name: 'POSTGRES_HOST' - value: databaseHost + value: postgresDatabaseHost } { name: 'POSTGRES_USERNAME' - value: databaseUser + value: postgresDatabaseUser } { name: 'POSTGRES_DATABASE' - value: databaseName + value: postgresDatabaseName } { name: 'POSTGRES_PASSWORD' - secretRef: 'db-pass' + secretRef: 'postgres-db-pass' } { name: 'POSTGRES_PORT' value: '5432' } {{- end}} + {{- if .DbMySql}} + { + name: 'MYSQL_HOST' + value: mysqlDatabaseHost + } + { + name: 'MYSQL_USERNAME' + value: mysqlDatabaseUser + } + { + name: 'MYSQL_DATABASE' + value: mysqlDatabaseName + } + { + name: 'MYSQL_PASSWORD' + secretRef: 'mysql-db-pass' + } + { + name: 'MYSQL_PORT' + value: '3306' + } + {{- end}} {{- if .Frontend}} {{- range $i, $e := .Frontend.Backends}} { diff --git a/cli/azd/resources/scaffold/templates/main.bicept b/cli/azd/resources/scaffold/templates/main.bicept index 9cf7aa9d3f1..c8e943aee86 100644 --- a/cli/azd/resources/scaffold/templates/main.bicept +++ b/cli/azd/resources/scaffold/templates/main.bicept @@ -162,16 +162,16 @@ module {{bicepName .Name}} './app/{{.Name}}.bicep' = { cosmosDbConnectionString: vault.getSecret(cosmosDb.outputs.connectionStringKey) {{- end}} {{- if .DbPostgres}} - databaseName: postgresDb.outputs.databaseName - databaseHost: postgresDb.outputs.databaseHost - databaseUser: postgresDb.outputs.databaseUser - databasePassword: vault.getSecret(postgresDb.outputs.databaseConnectionKey) + postgresDatabaseName: postgresDb.outputs.databaseName + postgresDatabaseHost: postgresDb.outputs.databaseHost + postgresDatabaseUser: postgresDb.outputs.databaseUser + postgresDatabasePassword: vault.getSecret(postgresDb.outputs.databaseConnectionKey) {{- end}} {{- if .DbMySql}} - databaseName: mysqlDb.outputs.databaseName - databaseHost: mysqlDb.outputs.databaseHost - databaseUser: mysqlDb.outputs.databaseUser - databasePassword: vault.getSecret(mysqlDb.outputs.databaseConnectionKey) + mysqlDatabaseName: mysqlDb.outputs.databaseName + mysqlDatabaseHost: mysqlDb.outputs.databaseHost + mysqlDatabaseUser: mysqlDb.outputs.databaseUser + mysqlDatabasePassword: vault.getSecret(mysqlDb.outputs.databaseConnectionKey) {{- end}} {{- if (and .Frontend .Frontend.Backends)}} apiUrls: [ diff --git a/cli/azd/resources/scaffold/templates/next-steps.mdt b/cli/azd/resources/scaffold/templates/next-steps.mdt index 14be7cbbbfe..0be57282e71 100644 --- a/cli/azd/resources/scaffold/templates/next-steps.mdt +++ b/cli/azd/resources/scaffold/templates/next-steps.mdt @@ -21,13 +21,16 @@ To troubleshoot any issues, see [troubleshooting](#troubleshooting). Configure environment variables for running services by updating `settings` in [main.parameters.json](./infra/main.parameters.json). {{- range .Services}} -{{- if or .DbPostgres .DbCosmosMongo .DbRedis }} +{{- if or .DbPostgres .DbMysql .DbCosmosMongo .DbRedis }} #### Database connections for `{{.Name}}` {{ end}} {{- if .DbPostgres }} - `POSTGRES_*` environment variables are configured in [{{.Name}}.bicep](./infra/app/{{.Name}}.bicep) to connect to the Postgres database. Modify these variables to match your application's needs. {{- end}} +{{- if .DbMysql }} +- `MYSQL_*` environment variables are configured in [{{.Name}}.bicep](./infra/app/{{.Name}}.bicep) to connect to the Mysql database. Modify these variables to match your application's needs. +{{- end}} {{- if .DbCosmosMongo }} - `AZURE_COSMOS_MONGODB_CONNECTION_STRING` environment variable is configured in [{{.Name}}.bicep](./infra/app/{{.Name}}.bicep) to connect to the MongoDB database. Modify this variable to match your application's needs. {{- end}} @@ -65,6 +68,9 @@ Each bicep file declares resources to be provisioned. The resources are provisio {{- if .DbPostgres}} - [app/db-postgre.bicep](./infra/app/db-postgre.bicep) - Azure Postgres Flexible Server to host the '{{.DbPostgres.DatabaseName}}' database. {{- end}} +{{- if .DbMysql}} +- [app/db-mysql.bicep](./infra/app/db-mysql.bicep) - Azure MySQL Flexible Server to host the '{{.DbMysql.DatabaseName}}' database. +{{- end}} {{- if .DbCosmosMongo}} - [app/db-cosmos.bicep](./infra/app/db-cosmos.bicep) - Azure Cosmos DB (MongoDB) to host the '{{.DbCosmosMongo.DatabaseName}}' database. {{- end}} From 2aa6cac32c41e42503ad6ecc25377232b22ac943 Mon Sep 17 00:00:00 2001 From: rujche Date: Sat, 14 Sep 2024 13:30:13 +0800 Subject: [PATCH 12/39] Fix typo by changing "DbMysql" to "DbMySql". --- cli/azd/resources/scaffold/templates/next-steps.mdt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/azd/resources/scaffold/templates/next-steps.mdt b/cli/azd/resources/scaffold/templates/next-steps.mdt index 0be57282e71..f2e46041137 100644 --- a/cli/azd/resources/scaffold/templates/next-steps.mdt +++ b/cli/azd/resources/scaffold/templates/next-steps.mdt @@ -21,14 +21,14 @@ To troubleshoot any issues, see [troubleshooting](#troubleshooting). Configure environment variables for running services by updating `settings` in [main.parameters.json](./infra/main.parameters.json). {{- range .Services}} -{{- if or .DbPostgres .DbMysql .DbCosmosMongo .DbRedis }} +{{- if or .DbPostgres .DbMySql .DbCosmosMongo .DbRedis }} #### Database connections for `{{.Name}}` {{ end}} {{- if .DbPostgres }} - `POSTGRES_*` environment variables are configured in [{{.Name}}.bicep](./infra/app/{{.Name}}.bicep) to connect to the Postgres database. Modify these variables to match your application's needs. {{- end}} -{{- if .DbMysql }} +{{- if .DbMySql }} - `MYSQL_*` environment variables are configured in [{{.Name}}.bicep](./infra/app/{{.Name}}.bicep) to connect to the Mysql database. Modify these variables to match your application's needs. {{- end}} {{- if .DbCosmosMongo }} @@ -68,8 +68,8 @@ Each bicep file declares resources to be provisioned. The resources are provisio {{- if .DbPostgres}} - [app/db-postgre.bicep](./infra/app/db-postgre.bicep) - Azure Postgres Flexible Server to host the '{{.DbPostgres.DatabaseName}}' database. {{- end}} -{{- if .DbMysql}} -- [app/db-mysql.bicep](./infra/app/db-mysql.bicep) - Azure MySQL Flexible Server to host the '{{.DbMysql.DatabaseName}}' database. +{{- if .DbMySql}} +- [app/db-mysql.bicep](./infra/app/db-mysql.bicep) - Azure MySQL Flexible Server to host the '{{.DbMySql.DatabaseName}}' database. {{- end}} {{- if .DbCosmosMongo}} - [app/db-cosmos.bicep](./infra/app/db-cosmos.bicep) - Azure Cosmos DB (MongoDB) to host the '{{.DbCosmosMongo.DatabaseName}}' database. From 082b9b2d6f3e76cd5934bd0c75e7496eb8b78924 Mon Sep 17 00:00:00 2001 From: rujche Date: Wed, 18 Sep 2024 17:46:05 +0800 Subject: [PATCH 13/39] Use managed-identity instead of username and password. Now it has error like this: "argetTypeNotSupported: Target resource type MICROSOFT.DBFORMYSQL/FLEXIBLESERVERS is not supported.". --- .../scaffold/templates/db-mysql.bicept | 1 + .../templates/host-containerapp.bicept | 20 +++++++++++++++++++ .../resources/scaffold/templates/main.bicept | 1 + 3 files changed, 22 insertions(+) diff --git a/cli/azd/resources/scaffold/templates/db-mysql.bicept b/cli/azd/resources/scaffold/templates/db-mysql.bicept index 9292c057b21..27317d195ca 100644 --- a/cli/azd/resources/scaffold/templates/db-mysql.bicept +++ b/cli/azd/resources/scaffold/templates/db-mysql.bicept @@ -71,4 +71,5 @@ output databaseHost string = mysqlServer.properties.fullyQualifiedDomainName output databaseName string = databaseName output databaseUser string = databaseUser output databaseConnectionKey string = 'databasePassword' +output mysqlServerId string = mysqlServer.id {{ end}} diff --git a/cli/azd/resources/scaffold/templates/host-containerapp.bicept b/cli/azd/resources/scaffold/templates/host-containerapp.bicept index 4b11c95d6f7..f088be601ee 100644 --- a/cli/azd/resources/scaffold/templates/host-containerapp.bicept +++ b/cli/azd/resources/scaffold/templates/host-containerapp.bicept @@ -267,6 +267,26 @@ resource app 'Microsoft.App/containerApps@2023-05-02-preview' = { } } +{{- if .DbMySql}} +resource appLinkToMySql 'Microsoft.ServiceLinker/linkers@2022-11-01-preview' = { + name: 'appLinkToMySql' + scope: app + properties: { + authInfo: { + authType: 'userAssignedIdentity' + } + clientType: 'springBoot' + targetService: { + type: 'AzureResource' + id: mysqlServerId + resourceProperties: { + type: 'KeyVault' + } + } + } +} +{{- end}} + output defaultDomain string = containerAppsEnvironment.properties.defaultDomain output name string = app.name output uri string = 'https://${app.properties.configuration.ingress.fqdn}' diff --git a/cli/azd/resources/scaffold/templates/main.bicept b/cli/azd/resources/scaffold/templates/main.bicept index c8e943aee86..73a461ee424 100644 --- a/cli/azd/resources/scaffold/templates/main.bicept +++ b/cli/azd/resources/scaffold/templates/main.bicept @@ -172,6 +172,7 @@ module {{bicepName .Name}} './app/{{.Name}}.bicep' = { mysqlDatabaseHost: mysqlDb.outputs.databaseHost mysqlDatabaseUser: mysqlDb.outputs.databaseUser mysqlDatabasePassword: vault.getSecret(mysqlDb.outputs.databaseConnectionKey) + mysqlServerId: mysqlDb.outputs.mysqlServerId {{- end}} {{- if (and .Frontend .Frontend.Backends)}} apiUrls: [ From b0c39a695c1c7566f6cd730ce1fa59869e7bc8ee Mon Sep 17 00:00:00 2001 From: rujche Date: Fri, 20 Sep 2024 15:36:14 +0800 Subject: [PATCH 14/39] Access MySql by managed identity instead of username&password. --- .../scaffold/templates/db-mysql.bicept | 6 +-- .../templates/host-containerapp.bicept | 44 +++---------------- .../resources/scaffold/templates/main.bicept | 11 ++--- 3 files changed, 11 insertions(+), 50 deletions(-) diff --git a/cli/azd/resources/scaffold/templates/db-mysql.bicept b/cli/azd/resources/scaffold/templates/db-mysql.bicept index 27317d195ca..b36f5780a2c 100644 --- a/cli/azd/resources/scaffold/templates/db-mysql.bicept +++ b/cli/azd/resources/scaffold/templates/db-mysql.bicept @@ -67,9 +67,5 @@ resource dbPasswordKey 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { } } -output databaseHost string = mysqlServer.properties.fullyQualifiedDomainName -output databaseName string = databaseName -output databaseUser string = databaseUser -output databaseConnectionKey string = 'databasePassword' -output mysqlServerId string = mysqlServer.id +output databaseId string = database.id {{ end}} diff --git a/cli/azd/resources/scaffold/templates/host-containerapp.bicept b/cli/azd/resources/scaffold/templates/host-containerapp.bicept index f088be601ee..61d2f0ac502 100644 --- a/cli/azd/resources/scaffold/templates/host-containerapp.bicept +++ b/cli/azd/resources/scaffold/templates/host-containerapp.bicept @@ -19,11 +19,7 @@ param postgresDatabaseName string param postgresDatabasePassword string {{- end}} {{- if .DbMySql}} -param mysqlDatabaseHost string -param mysqlDatabaseUser string -param mysqlDatabaseName string -@secure() -param mysqlDatabasePassword string +param mysqlDatabaseId string {{- end}} {{- if .DbRedis}} param redisName string @@ -153,12 +149,6 @@ resource app 'Microsoft.App/containerApps@2023-05-02-preview' = { value: postgresDatabasePassword } {{- end}} - {{- if .DbMySql}} - { - name: 'mysql-db-pass' - value: mysqlDatabasePassword - } - {{- end}} ], map(secrets, secret => { name: secret.secretRef @@ -203,28 +193,6 @@ resource app 'Microsoft.App/containerApps@2023-05-02-preview' = { value: '5432' } {{- end}} - {{- if .DbMySql}} - { - name: 'MYSQL_HOST' - value: mysqlDatabaseHost - } - { - name: 'MYSQL_USERNAME' - value: mysqlDatabaseUser - } - { - name: 'MYSQL_DATABASE' - value: mysqlDatabaseName - } - { - name: 'MYSQL_PASSWORD' - secretRef: 'mysql-db-pass' - } - { - name: 'MYSQL_PORT' - value: '3306' - } - {{- end}} {{- if .Frontend}} {{- range $i, $e := .Frontend.Backends}} { @@ -266,22 +234,22 @@ resource app 'Microsoft.App/containerApps@2023-05-02-preview' = { } } } - {{- if .DbMySql}} + resource appLinkToMySql 'Microsoft.ServiceLinker/linkers@2022-11-01-preview' = { name: 'appLinkToMySql' scope: app properties: { + scope: 'main' authInfo: { authType: 'userAssignedIdentity' + subscriptionId: subscription().subscriptionId + clientId: identity.properties.clientId } clientType: 'springBoot' targetService: { type: 'AzureResource' - id: mysqlServerId - resourceProperties: { - type: 'KeyVault' - } + id: mysqlDatabaseId } } } diff --git a/cli/azd/resources/scaffold/templates/main.bicept b/cli/azd/resources/scaffold/templates/main.bicept index 73a461ee424..4f33ab35651 100644 --- a/cli/azd/resources/scaffold/templates/main.bicept +++ b/cli/azd/resources/scaffold/templates/main.bicept @@ -111,8 +111,8 @@ module cosmosDb './app/db-cosmos-mongo.bicep' = { scope: rg } {{- end}} - {{- if .DbPostgres}} + module postgresDb './app/db-postgres.bicep' = { name: 'postgresDb' params: { @@ -126,8 +126,8 @@ module postgresDb './app/db-postgres.bicep' = { scope: rg } {{- end}} - {{- if .DbMySql}} + module mysqlDb './app/db-mysql.bicep' = { name: 'mysqlDb' params: { @@ -140,6 +140,7 @@ module mysqlDb './app/db-mysql.bicep' = { } scope: rg } + {{- end}} {{- range .Services}} @@ -168,11 +169,7 @@ module {{bicepName .Name}} './app/{{.Name}}.bicep' = { postgresDatabasePassword: vault.getSecret(postgresDb.outputs.databaseConnectionKey) {{- end}} {{- if .DbMySql}} - mysqlDatabaseName: mysqlDb.outputs.databaseName - mysqlDatabaseHost: mysqlDb.outputs.databaseHost - mysqlDatabaseUser: mysqlDb.outputs.databaseUser - mysqlDatabasePassword: vault.getSecret(mysqlDb.outputs.databaseConnectionKey) - mysqlServerId: mysqlDb.outputs.mysqlServerId + mysqlDatabaseId: mysqlDb.outputs.databaseId {{- end}} {{- if (and .Frontend .Frontend.Backends)}} apiUrls: [ From f90416fcade73487a61151ce75a408258b07663a Mon Sep 17 00:00:00 2001 From: Xiaolu Dai Date: Mon, 23 Sep 2024 18:16:21 +0800 Subject: [PATCH 15/39] Add Azure Deps in appdetect.go --- cli/azd/internal/appdetect/appdetect.go | 25 ++++++++++ cli/azd/internal/repository/app_init.go | 5 ++ cli/azd/internal/repository/detect_confirm.go | 48 +++++++++++++++++-- cli/azd/internal/tracing/fields/fields.go | 5 +- 4 files changed, 78 insertions(+), 5 deletions(-) diff --git a/cli/azd/internal/appdetect/appdetect.go b/cli/azd/internal/appdetect/appdetect.go index b7a24b137a4..665b71c1da3 100644 --- a/cli/azd/internal/appdetect/appdetect.go +++ b/cli/azd/internal/appdetect/appdetect.go @@ -132,6 +132,24 @@ func (db DatabaseDep) Display() string { return "" } +type AzureDep string + +const ( + AzureStorage AzureDep = "storage" + AzureServiceBus AzureDep = "servicebus" +) + +func (azureDep AzureDep) Display() string { + switch azureDep { + case AzureStorage: + return "Azure Storage" + case AzureServiceBus: + return "Azure Service Bus" + } + + return "" +} + type Project struct { // The language associated with the project. Language Language @@ -142,6 +160,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 @@ -346,6 +367,10 @@ func enrichFromJavaProject(javaProject javaanalyze.JavaProject, project *Project project.DatabaseDeps = append(project.DatabaseDeps, DbSqlServer) } else if resource.Type == "Redis" { project.DatabaseDeps = append(project.DatabaseDeps, DbRedis) + } else if resource.Type == "Azure Service Bus" { + project.AzureDeps = append(project.AzureDeps, AzureServiceBus) + } else if resource.Type == "Azure Storage" { + project.AzureDeps = append(project.AzureDeps, AzureStorage) } } } diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index e0dce236a6f..b0c24c8ef1f 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -38,6 +38,11 @@ var dbMap = map[appdetect.DatabaseDep]struct{}{ appdetect.DbRedis: {}, } +var azureDepMap = map[appdetect.AzureDep]struct{}{ + appdetect.AzureServiceBus: {}, + appdetect.AzureStorage: {}, +} + // InitFromApp initializes the infra directory and project file from the current existing app. func (i *Initializer) InitFromApp( ctx context.Context, diff --git a/cli/azd/internal/repository/detect_confirm.go b/cli/azd/internal/repository/detect_confirm.go index 5124228ed01..0f641e3fa91 100644 --- a/cli/azd/internal/repository/detect_confirm.go +++ b/cli/azd/internal/repository/detect_confirm.go @@ -47,6 +47,7 @@ type detectConfirm struct { // detected services and databases Services []appdetect.Project Databases map[appdetect.DatabaseDep]EntryKind + AzureDeps map[appdetect.AzureDep]EntryKind // the root directory of the project root string @@ -59,6 +60,7 @@ type detectConfirm struct { // Init initializes state from initial detection output func (d *detectConfirm) Init(projects []appdetect.Project, root string) { d.Databases = make(map[appdetect.DatabaseDep]EntryKind) + d.AzureDeps = make(map[appdetect.AzureDep]EntryKind) d.Services = make([]appdetect.Project, 0, len(projects)) d.modified = false d.root = root @@ -73,16 +75,24 @@ func (d *detectConfirm) Init(projects []appdetect.Project, root string) { d.Databases[dbType] = EntryKindDetected } } + + for _, azureDep := range project.AzureDeps { + if _, supported := azureDepMap[azureDep]; supported { + d.AzureDeps[azureDep] = EntryKindDetected + } + } } d.captureUsage( fields.AppInitDetectedDatabase, - fields.AppInitDetectedServices) + fields.AppInitDetectedServices, + fields.AppInitDetectedAzureDeps) } func (d *detectConfirm) captureUsage( databases attribute.Key, - services attribute.Key) { + services attribute.Key, + azureDeps attribute.Key) { names := make([]string, 0, len(d.Services)) for _, svc := range d.Services { names = append(names, string(svc.Language)) @@ -93,9 +103,15 @@ func (d *detectConfirm) captureUsage( dbNames = append(dbNames, string(db)) } + azureDepNames := make([]string, 0, len(d.AzureDeps)) + for azureDep := range d.AzureDeps { + azureDepNames = append(azureDepNames, string(azureDep)) + } + tracing.SetUsageAttributes( databases.StringSlice(dbNames), services.StringSlice(names), + azureDeps.StringSlice(azureDepNames), ) } @@ -146,7 +162,8 @@ func (d *detectConfirm) Confirm(ctx context.Context) error { case 0: d.captureUsage( fields.AppInitConfirmedDatabases, - fields.AppInitConfirmedServices) + fields.AppInitConfirmedServices, + fields.AppInitDetectedAzureDeps) return nil case 1: if err := d.remove(ctx); err != nil { @@ -203,6 +220,9 @@ func (d *detectConfirm) render(ctx context.Context) error { } } + if len(d.Databases) > 0 { + d.console.Message(ctx, "\n"+output.WithBold("Detected databases:")+"\n") + } for db, entry := range d.Databases { switch db { case appdetect.DbPostgres: @@ -226,6 +246,28 @@ func (d *detectConfirm) render(ctx context.Context) error { d.console.Message(ctx, "") } + if len(d.AzureDeps) > 0 { + d.console.Message(ctx, "\n"+output.WithBold("Detected Azure dependencies:")+"\n") + } + for azureDep, entry := range d.AzureDeps { + switch azureDep { + case appdetect.AzureStorage: + recommendedServices = append(recommendedServices, "Azure Storage") + case appdetect.AzureServiceBus: + recommendedServices = append(recommendedServices, "Azure Service Bus") + } + + status := "" + if entry == EntryKindModified { + status = " " + output.WithSuccessFormat("[Updated]") + } else if entry == EntryKindManual { + status = " " + output.WithSuccessFormat("[Added]") + } + + d.console.Message(ctx, " "+color.BlueString(azureDep.Display())+status) + d.console.Message(ctx, "") + } + displayedServices := make([]string, 0, len(recommendedServices)) for _, svc := range recommendedServices { displayedServices = append(displayedServices, color.MagentaString(svc)) diff --git a/cli/azd/internal/tracing/fields/fields.go b/cli/azd/internal/tracing/fields/fields.go index 52562e181c6..c264acafe66 100644 --- a/cli/azd/internal/tracing/fields/fields.go +++ b/cli/azd/internal/tracing/fields/fields.go @@ -240,8 +240,9 @@ const ( const ( InitMethod = attribute.Key("init.method") - AppInitDetectedDatabase = attribute.Key("appinit.detected.databases") - AppInitDetectedServices = attribute.Key("appinit.detected.services") + AppInitDetectedDatabase = attribute.Key("appinit.detected.databases") + AppInitDetectedServices = attribute.Key("appinit.detected.services") + AppInitDetectedAzureDeps = attribute.Key("appinit.detected.azuredeps") AppInitConfirmedDatabases = attribute.Key("appinit.confirmed.databases") AppInitConfirmedServices = attribute.Key("appinit.confirmed.services") From 84b785232dbd883915de1a5c2841175a3f5e9dcb Mon Sep 17 00:00:00 2001 From: Xiaolu Dai Date: Mon, 23 Sep 2024 18:16:37 +0800 Subject: [PATCH 16/39] Customize the azd VS Code extension --- ext/vscode/package.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ext/vscode/package.json b/ext/vscode/package.json index f9f06a3f6f2..83261a28432 100644 --- a/ext/vscode/package.json +++ b/ext/vscode/package.json @@ -185,11 +185,16 @@ "explorer/context": [ { "submenu": "azure-dev.explorer.submenu", - "when": "resourceFilename =~ /azure.yaml/i", + "when": "resourceFilename =~ /(azure.yaml|pom.xml)/i", "group": "azure-dev" } ], "azure-dev.explorer.submenu": [ + { + "when": "resourceFilename =~ /pom.xml/i", + "command": "azure-dev.commands.cli.init", + "group": "10provision@10" + }, { "when": "resourceFilename =~ /azure.yaml/i", "command": "azure-dev.commands.cli.provision", From 6587f83c54464a0ab0e69b47a79e51c3ec1ba8a4 Mon Sep 17 00:00:00 2001 From: Xiaolu Dai Date: Mon, 23 Sep 2024 18:17:04 +0800 Subject: [PATCH 17/39] improve the java analyzer for event-driven --- .../appdetect/javaanalyze/java_analyzer.go | 1 + .../appdetect/javaanalyze/rule_servicebus.go | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 cli/azd/internal/appdetect/javaanalyze/rule_servicebus.go diff --git a/cli/azd/internal/appdetect/javaanalyze/java_analyzer.go b/cli/azd/internal/appdetect/javaanalyze/java_analyzer.go index b3b489de7bb..cd7bf9bec00 100644 --- a/cli/azd/internal/appdetect/javaanalyze/java_analyzer.go +++ b/cli/azd/internal/appdetect/javaanalyze/java_analyzer.go @@ -10,6 +10,7 @@ func Analyze(path string) []JavaProject { &ruleService{}, &ruleMysql{}, &ruleStorage{}, + &ruleServiceBus{}, } entries, err := os.ReadDir(path) diff --git a/cli/azd/internal/appdetect/javaanalyze/rule_servicebus.go b/cli/azd/internal/appdetect/javaanalyze/rule_servicebus.go new file mode 100644 index 00000000000..ef4e52e4129 --- /dev/null +++ b/cli/azd/internal/appdetect/javaanalyze/rule_servicebus.go @@ -0,0 +1,30 @@ +package javaanalyze + +type ruleServiceBus struct { +} + +func (mr *ruleServiceBus) Match(mavenProject *MavenProject) bool { + if mavenProject.Dependencies != nil { + for _, dep := range mavenProject.Dependencies { + if dep.GroupId == "com.azure" && dep.ArtifactId == "" { + return true + } + if dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-stream-binder-servicebus" { + return true + } + } + } + return false +} + +func (mr *ruleServiceBus) Apply(javaProject *JavaProject) { + javaProject.Resources = append(javaProject.Resources, Resource{ + Name: "Azure Service Bus", + Type: "Azure Service Bus", + }) + + javaProject.ServiceBindings = append(javaProject.ServiceBindings, ServiceBinding{ + Name: "Azure Service Bus", + AuthType: AuthType_SYSTEM_MANAGED_IDENTITY, + }) +} From 103a005ae9c7df4edebc7a06dcd137b35107f5d3 Mon Sep 17 00:00:00 2001 From: Xiaolu Dai Date: Thu, 26 Sep 2024 16:59:29 +0800 Subject: [PATCH 18/39] refactor the java analyzer --- cli/azd/internal/appdetect/appdetect.go | 18 ++--- .../{java_project.go => azure_yaml.go} | 33 +++++++- .../appdetect/javaanalyze/java_analyzer.go | 41 ---------- .../javaanalyze/project_analyzer_java.go | 38 +++++++++ ..._analyzer.go => project_analyzer_maven.go} | 55 ++++++++----- .../javaanalyze/project_analyzer_spring.go | 78 +++++++++++++++++++ .../appdetect/javaanalyze/rule_engine.go | 14 ++-- .../appdetect/javaanalyze/rule_mongo.go | 12 +-- .../appdetect/javaanalyze/rule_mysql.go | 12 +-- .../appdetect/javaanalyze/rule_redis.go | 7 +- .../appdetect/javaanalyze/rule_service.go | 14 ++-- .../appdetect/javaanalyze/rule_servicebus.go | 30 ------- .../javaanalyze/rule_servicebus_scsb.go | 62 +++++++++++++++ .../appdetect/javaanalyze/rule_storage.go | 12 +-- cli/azd/internal/repository/infra_confirm.go | 70 +++++++++++++++++ cli/azd/internal/repository/infra_prompt.go | 38 +++++++++ cli/azd/internal/scaffold/spec.go | 12 +++ 17 files changed, 409 insertions(+), 137 deletions(-) rename cli/azd/internal/appdetect/javaanalyze/{java_project.go => azure_yaml.go} (69%) delete mode 100644 cli/azd/internal/appdetect/javaanalyze/java_analyzer.go create mode 100644 cli/azd/internal/appdetect/javaanalyze/project_analyzer_java.go rename cli/azd/internal/appdetect/javaanalyze/{pom_analyzer.go => project_analyzer_maven.go} (51%) create mode 100644 cli/azd/internal/appdetect/javaanalyze/project_analyzer_spring.go delete mode 100644 cli/azd/internal/appdetect/javaanalyze/rule_servicebus.go create mode 100644 cli/azd/internal/appdetect/javaanalyze/rule_servicebus_scsb.go create mode 100644 cli/azd/internal/repository/infra_prompt.go diff --git a/cli/azd/internal/appdetect/appdetect.go b/cli/azd/internal/appdetect/appdetect.go index 665b71c1da3..122ba242fa6 100644 --- a/cli/azd/internal/appdetect/appdetect.go +++ b/cli/azd/internal/appdetect/appdetect.go @@ -354,22 +354,22 @@ func analyze(projects []Project) []Project { return result } -func enrichFromJavaProject(javaProject javaanalyze.JavaProject, project *Project) { +func enrichFromJavaProject(azureYaml javaanalyze.AzureYaml, project *Project) { // if there is only one project, we can safely assume that it is the main project - for _, resource := range javaProject.Resources { - if resource.Type == "Azure Storage" { + for _, resource := range azureYaml.Resources { + if resource.GetType() == "Azure Storage" { // project.DatabaseDeps = append(project.DatabaseDeps, Db) - } else if resource.Type == "MySQL" { + } else if resource.GetType() == "MySQL" { project.DatabaseDeps = append(project.DatabaseDeps, DbMySql) - } else if resource.Type == "PostgreSQL" { + } else if resource.GetType() == "PostgreSQL" { project.DatabaseDeps = append(project.DatabaseDeps, DbPostgres) - } else if resource.Type == "SQL Server" { + } else if resource.GetType() == "SQL Server" { project.DatabaseDeps = append(project.DatabaseDeps, DbSqlServer) - } else if resource.Type == "Redis" { + } else if resource.GetType() == "Redis" { project.DatabaseDeps = append(project.DatabaseDeps, DbRedis) - } else if resource.Type == "Azure Service Bus" { + } else if resource.GetType() == "Azure Service Bus" { project.AzureDeps = append(project.AzureDeps, AzureServiceBus) - } else if resource.Type == "Azure Storage" { + } else if resource.GetType() == "Azure Storage" { project.AzureDeps = append(project.AzureDeps, AzureStorage) } } diff --git a/cli/azd/internal/appdetect/javaanalyze/java_project.go b/cli/azd/internal/appdetect/javaanalyze/azure_yaml.go similarity index 69% rename from cli/azd/internal/appdetect/javaanalyze/java_project.go rename to cli/azd/internal/appdetect/javaanalyze/azure_yaml.go index 9b494d24426..41e848c88cd 100644 --- a/cli/azd/internal/appdetect/javaanalyze/java_project.go +++ b/cli/azd/internal/appdetect/javaanalyze/azure_yaml.go @@ -1,11 +1,18 @@ package javaanalyze -type JavaProject struct { +type AzureYaml struct { Service *Service `json:"service"` - Resources []Resource `json:"resources"` + Resources []IResource `json:"resources"` ServiceBindings []ServiceBinding `json:"serviceBindings"` } +type IResource interface { + GetName() string + GetType() string + GetBicepParameters() []BicepParameter + GetBicepProperties() []BicepProperty +} + type Resource struct { Name string `json:"name"` Type string `json:"type"` @@ -13,6 +20,28 @@ type Resource struct { BicepProperties []BicepProperty `json:"bicepProperties"` } +func (r *Resource) GetName() string { + return r.Name +} + +func (r *Resource) GetType() string { + return r.Type +} + +func (r *Resource) GetBicepParameters() []BicepParameter { + return r.BicepParameters +} + +func (r *Resource) GetBicepProperties() []BicepProperty { + return r.BicepProperties +} + +type ServiceBusResource struct { + Resource + Queues []string `json:"queues"` + TopicAndSubscriptions []string `json:"topicAndSubscriptions"` +} + type BicepParameter struct { Name string `json:"name"` Description string `json:"description"` diff --git a/cli/azd/internal/appdetect/javaanalyze/java_analyzer.go b/cli/azd/internal/appdetect/javaanalyze/java_analyzer.go deleted file mode 100644 index cd7bf9bec00..00000000000 --- a/cli/azd/internal/appdetect/javaanalyze/java_analyzer.go +++ /dev/null @@ -1,41 +0,0 @@ -package javaanalyze - -import ( - "os" -) - -func Analyze(path string) []JavaProject { - result := []JavaProject{} - rules := []rule{ - &ruleService{}, - &ruleMysql{}, - &ruleStorage{}, - &ruleServiceBus{}, - } - - entries, err := os.ReadDir(path) - if err == nil { - for _, entry := range entries { - if "pom.xml" == entry.Name() { - mavenProject, _ := ParsePOM(path + "/" + entry.Name()) - - // if it has submodules - if len(mavenProject.Modules) > 0 { - for _, m := range mavenProject.Modules { - // analyze the submodules - subModule, _ := ParsePOM(path + "/" + m + "/pom.xml") - javaProject, _ := ApplyRules(subModule, rules) - result = append(result, *javaProject) - } - } else { - // analyze the maven project - javaProject, _ := ApplyRules(mavenProject, rules) - result = append(result, *javaProject) - } - } - //fmt.Printf("\tentry: %s", entry.Name()) - } - } - - return result -} diff --git a/cli/azd/internal/appdetect/javaanalyze/project_analyzer_java.go b/cli/azd/internal/appdetect/javaanalyze/project_analyzer_java.go new file mode 100644 index 00000000000..bdb0c9cf38a --- /dev/null +++ b/cli/azd/internal/appdetect/javaanalyze/project_analyzer_java.go @@ -0,0 +1,38 @@ +package javaanalyze + +import "os" + +type javaProject struct { + springProject springProject + mavenProject mavenProject +} + +func Analyze(path string) []AzureYaml { + var result []AzureYaml + rules := []rule{ + &ruleService{}, + &ruleMysql{}, + &ruleStorage{}, + &ruleServiceBusScsb{}, + } + + entries, err := os.ReadDir(path) + if err == nil { + for _, entry := range entries { + if "pom.xml" == entry.Name() { + mavenProjects, _ := analyzeMavenProject(path) + + for _, mavenProject := range mavenProjects { + javaProject := &javaProject{ + mavenProject: mavenProject, + springProject: analyzeSpringProject(mavenProject.path), + } + azureYaml, _ := applyRules(javaProject, rules) + result = append(result, *azureYaml) + } + } + } + } + + return result +} diff --git a/cli/azd/internal/appdetect/javaanalyze/pom_analyzer.go b/cli/azd/internal/appdetect/javaanalyze/project_analyzer_maven.go similarity index 51% rename from cli/azd/internal/appdetect/javaanalyze/pom_analyzer.go rename to cli/azd/internal/appdetect/javaanalyze/project_analyzer_maven.go index 0c7ad862049..6f79d0f73bd 100644 --- a/cli/azd/internal/appdetect/javaanalyze/pom_analyzer.go +++ b/cli/azd/internal/appdetect/javaanalyze/project_analyzer_maven.go @@ -5,28 +5,30 @@ import ( "fmt" "io/ioutil" "os" + "path/filepath" ) -// MavenProject represents the top-level structure of a Maven POM file. -type MavenProject struct { - XMLName xml.Name `xml:"project"` - Parent Parent `xml:"parent"` +// 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 - Dependencies []Dependency `xml:"dependencies>dependency"` - DependencyManagement DependencyManagement `xml:"dependencyManagement"` - Build Build `xml:"build"` - Path string + Dependencies []dependency `xml:"dependencies>dependency"` + DependencyManagement dependencyManagement `xml:"dependencyManagement"` + Build build `xml:"build"` + path string + spring springProject } // Parent represents the parent POM if this project is a module. -type Parent struct { +type parent struct { GroupId string `xml:"groupId"` ArtifactId string `xml:"artifactId"` Version string `xml:"version"` } // Dependency represents a single Maven dependency. -type Dependency struct { +type dependency struct { GroupId string `xml:"groupId"` ArtifactId string `xml:"artifactId"` Version string `xml:"version"` @@ -34,25 +36,40 @@ type Dependency struct { } // DependencyManagement includes a list of dependencies that are managed. -type DependencyManagement struct { - Dependencies []Dependency `xml:"dependencies>dependency"` +type dependencyManagement struct { + Dependencies []dependency `xml:"dependencies>dependency"` } // Build represents the build configuration which can contain plugins. -type Build struct { - Plugins []Plugin `xml:"plugins>plugin"` +type build struct { + Plugins []plugin `xml:"plugins>plugin"` } // Plugin represents a build plugin. -type Plugin struct { +type plugin struct { GroupId string `xml:"groupId"` ArtifactId string `xml:"artifactId"` Version string `xml:"version"` //Configuration xml.Node `xml:"configuration"` } -// ParsePOM Parse the POM file. -func ParsePOM(filePath string) (*MavenProject, error) { +func analyzeMavenProject(projectPath string) ([]mavenProject, error) { + rootProject, _ := analyze(projectPath + "/pom.xml") + var result []mavenProject + + // if it has submodules + if len(rootProject.Modules) > 0 { + for _, m := range rootProject.Modules { + subModule, _ := analyze(projectPath + "/" + m + "/pom.xml") + result = append(result, *subModule) + } + } else { + result = append(result, *rootProject) + } + return result, nil +} + +func analyze(filePath string) (*mavenProject, error) { xmlFile, err := os.Open(filePath) if err != nil { return nil, fmt.Errorf("error opening file: %w", err) @@ -64,12 +81,12 @@ func ParsePOM(filePath string) (*MavenProject, error) { return nil, fmt.Errorf("error reading file: %w", err) } - var project MavenProject + var project mavenProject if err := xml.Unmarshal(bytes, &project); err != nil { return nil, fmt.Errorf("error parsing XML: %w", err) } - project.Path = filePath + project.path = filepath.Dir(filePath) return &project, nil } diff --git a/cli/azd/internal/appdetect/javaanalyze/project_analyzer_spring.go b/cli/azd/internal/appdetect/javaanalyze/project_analyzer_spring.go new file mode 100644 index 00000000000..85047325da4 --- /dev/null +++ b/cli/azd/internal/appdetect/javaanalyze/project_analyzer_spring.go @@ -0,0 +1,78 @@ +package javaanalyze + +import ( + "fmt" + "gopkg.in/yaml.v3" + "io/ioutil" + "log" +) + +type springProject struct { + applicationProperties map[string]interface{} +} + +func analyzeSpringProject(projectPath string) springProject { + return springProject{ + applicationProperties: findSpringApplicationProperties(projectPath), + } +} + +func findSpringApplicationProperties(projectPath string) map[string]interface{} { + yamlFilePath := projectPath + "/src/main/resources/application.yml" + data, err := ioutil.ReadFile(yamlFilePath) + if err != nil { + log.Fatalf("error reading YAML file: %v", err) + } + + // 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) + } + + result := make(map[string]interface{}) + parseYAML("", &root, result) + + return result +} + +// Recursively parse the YAML and build dot-separated keys into a map +func parseYAML(prefix string, node *yaml.Node, result map[string]interface{}) { + 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 + } +} diff --git a/cli/azd/internal/appdetect/javaanalyze/rule_engine.go b/cli/azd/internal/appdetect/javaanalyze/rule_engine.go index 173cc88096b..630d2d0ebf4 100644 --- a/cli/azd/internal/appdetect/javaanalyze/rule_engine.go +++ b/cli/azd/internal/appdetect/javaanalyze/rule_engine.go @@ -1,17 +1,17 @@ package javaanalyze type rule interface { - Match(*MavenProject) bool - Apply(*JavaProject) + match(project *javaProject) bool + apply(azureYaml *AzureYaml) } -func ApplyRules(mavenProject *MavenProject, rules []rule) (*JavaProject, error) { - javaProject := &JavaProject{} +func applyRules(javaProject *javaProject, rules []rule) (*AzureYaml, error) { + azureYaml := &AzureYaml{} for _, r := range rules { - if r.Match(mavenProject) { - r.Apply(javaProject) + if r.match(javaProject) { + r.apply(azureYaml) } } - return javaProject, nil + return azureYaml, nil } diff --git a/cli/azd/internal/appdetect/javaanalyze/rule_mongo.go b/cli/azd/internal/appdetect/javaanalyze/rule_mongo.go index 78ee0999c23..5ca181970a6 100644 --- a/cli/azd/internal/appdetect/javaanalyze/rule_mongo.go +++ b/cli/azd/internal/appdetect/javaanalyze/rule_mongo.go @@ -3,9 +3,9 @@ package javaanalyze type ruleMongo struct { } -func (mr *ruleMongo) Match(mavenProject *MavenProject) bool { - if mavenProject.Dependencies != nil { - for _, dep := range mavenProject.Dependencies { +func (mr *ruleMongo) match(javaProject *javaProject) bool { + if javaProject.mavenProject.Dependencies != nil { + for _, dep := range javaProject.mavenProject.Dependencies { if dep.GroupId == "org.springframework.boot" && dep.ArtifactId == "spring-boot-starter-data-mongodb" { return true } @@ -14,13 +14,13 @@ func (mr *ruleMongo) Match(mavenProject *MavenProject) bool { return false } -func (mr *ruleMongo) Apply(javaProject *JavaProject) { - javaProject.Resources = append(javaProject.Resources, Resource{ +func (mr *ruleMongo) apply(azureYaml *AzureYaml) { + azureYaml.Resources = append(azureYaml.Resources, &Resource{ Name: "MongoDB", Type: "MongoDB", }) - javaProject.ServiceBindings = append(javaProject.ServiceBindings, ServiceBinding{ + azureYaml.ServiceBindings = append(azureYaml.ServiceBindings, ServiceBinding{ Name: "MongoDB", AuthType: AuthType_SYSTEM_MANAGED_IDENTITY, }) diff --git a/cli/azd/internal/appdetect/javaanalyze/rule_mysql.go b/cli/azd/internal/appdetect/javaanalyze/rule_mysql.go index 1029eea1078..c98d317b101 100644 --- a/cli/azd/internal/appdetect/javaanalyze/rule_mysql.go +++ b/cli/azd/internal/appdetect/javaanalyze/rule_mysql.go @@ -3,9 +3,9 @@ package javaanalyze type ruleMysql struct { } -func (mr *ruleMysql) Match(mavenProject *MavenProject) bool { - if mavenProject.Dependencies != nil { - for _, dep := range mavenProject.Dependencies { +func (mr *ruleMysql) match(javaProject *javaProject) bool { + if javaProject.mavenProject.Dependencies != nil { + for _, dep := range javaProject.mavenProject.Dependencies { if dep.GroupId == "com.mysql" && dep.ArtifactId == "mysql-connector-j" { return true } @@ -14,13 +14,13 @@ func (mr *ruleMysql) Match(mavenProject *MavenProject) bool { return false } -func (mr *ruleMysql) Apply(javaProject *JavaProject) { - javaProject.Resources = append(javaProject.Resources, Resource{ +func (mr *ruleMysql) apply(azureYaml *AzureYaml) { + azureYaml.Resources = append(azureYaml.Resources, &Resource{ Name: "MySQL", Type: "MySQL", }) - javaProject.ServiceBindings = append(javaProject.ServiceBindings, ServiceBinding{ + azureYaml.ServiceBindings = append(azureYaml.ServiceBindings, ServiceBinding{ Name: "MySQL", AuthType: AuthType_SYSTEM_MANAGED_IDENTITY, }) diff --git a/cli/azd/internal/appdetect/javaanalyze/rule_redis.go b/cli/azd/internal/appdetect/javaanalyze/rule_redis.go index 1f5d437867b..59ef290ac9b 100644 --- a/cli/azd/internal/appdetect/javaanalyze/rule_redis.go +++ b/cli/azd/internal/appdetect/javaanalyze/rule_redis.go @@ -3,13 +3,12 @@ package javaanalyze type ruleRedis struct { } -func (mr *ruleRedis) Match(mavenProject *MavenProject) bool { - +func (r *ruleRedis) match(javaProject *javaProject) bool { return false } -func (mr *ruleRedis) Apply(javaProject *JavaProject) { - javaProject.Resources = append(javaProject.Resources, Resource{ +func (r *ruleRedis) apply(azureYaml *AzureYaml) { + azureYaml.Resources = append(azureYaml.Resources, &Resource{ Name: "Redis", Type: "Redis", }) diff --git a/cli/azd/internal/appdetect/javaanalyze/rule_service.go b/cli/azd/internal/appdetect/javaanalyze/rule_service.go index 8e6106d703a..8203848830f 100644 --- a/cli/azd/internal/appdetect/javaanalyze/rule_service.go +++ b/cli/azd/internal/appdetect/javaanalyze/rule_service.go @@ -1,17 +1,17 @@ package javaanalyze type ruleService struct { - MavenProject *MavenProject + javaProject *javaProject } -func (mr *ruleService) Match(mavenProject *MavenProject) bool { - mr.MavenProject = mavenProject +func (r *ruleService) match(javaProject *javaProject) bool { + r.javaProject = javaProject return true } -func (mr *ruleService) Apply(javaProject *JavaProject) { - if javaProject.Service == nil { - javaProject.Service = &Service{} +func (r *ruleService) apply(azureYaml *AzureYaml) { + if azureYaml.Service == nil { + azureYaml.Service = &Service{} } - javaProject.Service.Path = mr.MavenProject.Path + azureYaml.Service.Path = r.javaProject.mavenProject.path } diff --git a/cli/azd/internal/appdetect/javaanalyze/rule_servicebus.go b/cli/azd/internal/appdetect/javaanalyze/rule_servicebus.go deleted file mode 100644 index ef4e52e4129..00000000000 --- a/cli/azd/internal/appdetect/javaanalyze/rule_servicebus.go +++ /dev/null @@ -1,30 +0,0 @@ -package javaanalyze - -type ruleServiceBus struct { -} - -func (mr *ruleServiceBus) Match(mavenProject *MavenProject) bool { - if mavenProject.Dependencies != nil { - for _, dep := range mavenProject.Dependencies { - if dep.GroupId == "com.azure" && dep.ArtifactId == "" { - return true - } - if dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-stream-binder-servicebus" { - return true - } - } - } - return false -} - -func (mr *ruleServiceBus) Apply(javaProject *JavaProject) { - javaProject.Resources = append(javaProject.Resources, Resource{ - Name: "Azure Service Bus", - Type: "Azure Service Bus", - }) - - javaProject.ServiceBindings = append(javaProject.ServiceBindings, ServiceBinding{ - Name: "Azure Service Bus", - AuthType: AuthType_SYSTEM_MANAGED_IDENTITY, - }) -} diff --git a/cli/azd/internal/appdetect/javaanalyze/rule_servicebus_scsb.go b/cli/azd/internal/appdetect/javaanalyze/rule_servicebus_scsb.go new file mode 100644 index 00000000000..4276527b56d --- /dev/null +++ b/cli/azd/internal/appdetect/javaanalyze/rule_servicebus_scsb.go @@ -0,0 +1,62 @@ +package javaanalyze + +import ( + "fmt" + "strings" +) + +type ruleServiceBusScsb struct { + javaProject *javaProject +} + +func (r *ruleServiceBusScsb) match(javaProject *javaProject) bool { + if javaProject.mavenProject.Dependencies != nil { + for _, dep := range javaProject.mavenProject.Dependencies { + if dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-stream-binder-servicebus" { + r.javaProject = javaProject + return true + } + } + } + return false +} + +// Function to find all properties that match the pattern `spring.cloud.stream.bindings..destination` +func findBindingDestinations(properties map[string]interface{}) 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..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 (r *ruleServiceBusScsb) apply(azureYaml *AzureYaml) { + bindingDestinations := findBindingDestinations(r.javaProject.springProject.applicationProperties) + destinations := make([]string, 0, len(bindingDestinations)) + for bindingName, destination := range bindingDestinations { + destinations = append(destinations, destination) + fmt.Printf("Service Bus queue [%s] found for binding [%s]", destination, bindingName) + } + resource := ServiceBusResource{ + Resource: Resource{ + Name: "Azure Service Bus", + Type: "Azure Service Bus", + }, + Queues: destinations, + } + azureYaml.Resources = append(azureYaml.Resources, &resource) + + azureYaml.ServiceBindings = append(azureYaml.ServiceBindings, ServiceBinding{ + Name: "Azure Service Bus", + AuthType: AuthType_SYSTEM_MANAGED_IDENTITY, + }) +} diff --git a/cli/azd/internal/appdetect/javaanalyze/rule_storage.go b/cli/azd/internal/appdetect/javaanalyze/rule_storage.go index 5ec5dd0999b..557733ebb7b 100644 --- a/cli/azd/internal/appdetect/javaanalyze/rule_storage.go +++ b/cli/azd/internal/appdetect/javaanalyze/rule_storage.go @@ -3,9 +3,9 @@ package javaanalyze type ruleStorage struct { } -func (mr *ruleStorage) Match(mavenProject *MavenProject) bool { - if mavenProject.Dependencies != nil { - for _, dep := range mavenProject.Dependencies { +func (r *ruleStorage) match(javaProject *javaProject) bool { + if javaProject.mavenProject.Dependencies != nil { + for _, dep := range javaProject.mavenProject.Dependencies { if dep.GroupId == "com.azure" && dep.ArtifactId == "" { return true } @@ -26,13 +26,13 @@ func (mr *ruleStorage) Match(mavenProject *MavenProject) bool { return false } -func (mr *ruleStorage) Apply(javaProject *JavaProject) { - javaProject.Resources = append(javaProject.Resources, Resource{ +func (r *ruleStorage) apply(azureYaml *AzureYaml) { + azureYaml.Resources = append(azureYaml.Resources, &Resource{ Name: "Azure Storage", Type: "Azure Storage", }) - javaProject.ServiceBindings = append(javaProject.ServiceBindings, ServiceBinding{ + azureYaml.ServiceBindings = append(azureYaml.ServiceBindings, ServiceBinding{ Name: "Azure Storage", AuthType: AuthType_SYSTEM_MANAGED_IDENTITY, }) diff --git a/cli/azd/internal/repository/infra_confirm.go b/cli/azd/internal/repository/infra_confirm.go index eead3d61f7f..b577fdc6e9b 100644 --- a/cli/azd/internal/repository/infra_confirm.go +++ b/cli/azd/internal/repository/infra_confirm.go @@ -103,6 +103,13 @@ func (i *Initializer) infraSpecFromDetect( } } + for azureDep := range detect.AzureDeps { + infraSpec, err := i.promptForAzureResource(ctx, azureDep, spec) + if err != nil { + return infraSpec, err + } + } + for _, svc := range detect.Services { name := filepath.Base(svc.Path) serviceSpec := scaffold.ServiceSpec{ @@ -208,3 +215,66 @@ func (i *Initializer) infraSpecFromDetect( return spec, nil } + +func (i *Initializer) promptForAzureResource( + ctx context.Context, + azureDep appdetect.AzureDep, + spec scaffold.InfraSpec) (scaffold.InfraSpec, error) { +azureDepPrompt: + for { + azureDepName, err := i.console.Prompt(ctx, input.ConsoleOptions{ + Message: fmt.Sprintf("Input the name of the Azure dependency (%s)", azureDep.Display()), + Help: "Hint: Azure dependency name\n\n" + + "Name of the Azure dependency that the app connects to. " + + "This dependency will be created after running azd provision or azd up." + + "\nYou may be able to skip this step by hitting enter, in which case the dependency will not be created.", + }) + if err != nil { + return scaffold.InfraSpec{}, err + } + + if strings.ContainsAny(azureDepName, " ") { + i.console.MessageUxItem(ctx, &ux.WarningMessage{ + Description: "Dependency name contains whitespace. This might not be allowed by the Azure service.", + }) + confirm, err := i.console.Confirm(ctx, input.ConsoleOptions{ + Message: fmt.Sprintf("Continue with name '%s'?", azureDepName), + }) + if err != nil { + return scaffold.InfraSpec{}, err + } + + if !confirm { + continue azureDepPrompt + } + } else if !wellFormedDbNameRegex.MatchString(azureDepName) { + i.console.MessageUxItem(ctx, &ux.WarningMessage{ + Description: "Dependency name contains special characters. " + + "This might not be allowed by the Azure service.", + }) + confirm, err := i.console.Confirm(ctx, input.ConsoleOptions{ + Message: fmt.Sprintf("Continue with name '%s'?", azureDepName), + }) + if err != nil { + return scaffold.InfraSpec{}, err + } + + if !confirm { + continue azureDepPrompt + } + } + + switch azureDep { + case appdetect.AzureServiceBus: + + spec.AzureServiceBus = &scaffold.AzureDepServiceBus{ + Name: azureDepName, + } + break azureDepPrompt + case appdetect.AzureStorage: + break azureDepPrompt + } + break azureDepPrompt + } + return spec, nil +} diff --git a/cli/azd/internal/repository/infra_prompt.go b/cli/azd/internal/repository/infra_prompt.go new file mode 100644 index 00000000000..69622d40073 --- /dev/null +++ b/cli/azd/internal/repository/infra_prompt.go @@ -0,0 +1,38 @@ +package repository + +import ( + "github.com/azure/azure-dev/cli/azd/internal/appdetect" + "github.com/azure/azure-dev/cli/azd/internal/scaffold" +) + +type infraPrompt interface { + Type() string + Properties() map[string]string + Apply(spec *scaffold.InfraSpec) +} + +type serviceBusPrompt struct { + name string + queues []string + topicAndSubscriptions []string +} + +func (s *serviceBusPrompt) Type() string { + return appdetect.AzureServiceBus.Display() +} + +func (s *serviceBusPrompt) Properties() map[string]string { + return map[string]string{ + "name": "Service Bus namespace name", + "queues": "Comma-separated list of queue names", + "topicAndSubscriptions": "Comma-separated list of topic names and their subscriptions, of format 'topicName:subscription1,subscription2,...'", + } +} + +func (s *serviceBusPrompt) Apply(spec *scaffold.InfraSpec) { + if spec.AzureServiceBus == nil { + spec.AzureServiceBus = &scaffold.AzureDepServiceBus{} + } + spec.AzureServiceBus.Name = s.name + spec.AzureServiceBus.Queues = s.queues +} diff --git a/cli/azd/internal/scaffold/spec.go b/cli/azd/internal/scaffold/spec.go index 47d525619d4..feac1585671 100644 --- a/cli/azd/internal/scaffold/spec.go +++ b/cli/azd/internal/scaffold/spec.go @@ -13,6 +13,9 @@ type InfraSpec struct { DbPostgres *DatabasePostgres DbMySql *DatabaseMySql DbCosmosMongo *DatabaseCosmosMongo + + // Azure Service Bus + AzureServiceBus *AzureDepServiceBus } type Parameter struct { @@ -36,6 +39,12 @@ type DatabaseCosmosMongo struct { DatabaseName string } +type AzureDepServiceBus struct { + Name string + Queues []string + TopicsAndSubscriptions map[string][]string +} + type ServiceSpec struct { Name string Port int @@ -51,6 +60,9 @@ type ServiceSpec struct { DbMySql *DatabaseReference DbCosmosMongo *DatabaseReference DbRedis *DatabaseReference + + // Azure Service Bus + AzureServiceBus *AzureDepServiceBus } type Frontend struct { From 2e01347be5a528fdd1f9377c65008fd80e2a892f Mon Sep 17 00:00:00 2001 From: rujche Date: Fri, 27 Sep 2024 20:50:19 +0800 Subject: [PATCH 19/39] Create service connector by bicep file. --- .../scaffold/templates/db-mysql.bicept | 15 ++++++- .../templates/host-containerapp.bicept | 45 +++++++++++++------ .../resources/scaffold/templates/main.bicept | 2 + 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/cli/azd/resources/scaffold/templates/db-mysql.bicept b/cli/azd/resources/scaffold/templates/db-mysql.bicept index b36f5780a2c..caac47b50db 100644 --- a/cli/azd/resources/scaffold/templates/db-mysql.bicept +++ b/cli/azd/resources/scaffold/templates/db-mysql.bicept @@ -4,6 +4,7 @@ param location string = resourceGroup().location param tags object = {} param keyVaultName string +param identityName string param databaseUser string = 'mysqladmin' param databaseName string = '{{.DatabaseName}}' @@ -12,7 +13,12 @@ param databasePassword string param allowAllIPsFirewall bool = false -resource mysqlServer'Microsoft.DBforMySQL/flexibleServers@2023-06-30' = { +resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: identityName + location: location +} + +resource mysqlServer 'Microsoft.DBforMySQL/flexibleServers@2023-06-30' = { location: location tags: tags name: serverName @@ -20,6 +26,12 @@ resource mysqlServer'Microsoft.DBforMySQL/flexibleServers@2023-06-30' = { name: 'Standard_B1ms' tier: 'Burstable' } + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${userAssignedIdentity.id}': {} + } + } properties: { version: '8.0.21' administratorLogin: databaseUser @@ -68,4 +80,5 @@ resource dbPasswordKey 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { } output databaseId string = database.id +output identityName string = userAssignedIdentity.name {{ end}} diff --git a/cli/azd/resources/scaffold/templates/host-containerapp.bicept b/cli/azd/resources/scaffold/templates/host-containerapp.bicept index 61d2f0ac502..42601ff4605 100644 --- a/cli/azd/resources/scaffold/templates/host-containerapp.bicept +++ b/cli/azd/resources/scaffold/templates/host-containerapp.bicept @@ -20,6 +20,7 @@ param postgresDatabasePassword string {{- end}} {{- if .DbMySql}} param mysqlDatabaseId string +param mysqlIdentityName string {{- end}} {{- if .DbRedis}} param redisName string @@ -236,22 +237,40 @@ resource app 'Microsoft.App/containerApps@2023-05-02-preview' = { } {{- if .DbMySql}} -resource appLinkToMySql 'Microsoft.ServiceLinker/linkers@2022-11-01-preview' = { - name: 'appLinkToMySql' - scope: app +resource linkerCreatorIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: 'linkerCreatorIdentity' + location: location +} + +resource linkerCreatorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: resourceGroup() + name: guid(subscription().id, resourceGroup().id, linkerCreatorIdentity.id, 'linkerCreatorRole') properties: { - scope: 'main' - authInfo: { - authType: 'userAssignedIdentity' - subscriptionId: subscription().subscriptionId - clientId: identity.properties.clientId - } - clientType: 'springBoot' - targetService: { - type: 'AzureResource' - id: mysqlDatabaseId + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') + principalType: 'ServicePrincipal' + principalId: linkerCreatorIdentity.properties.principalId + } +} + +resource appLinkToMySql 'Microsoft.Resources/deploymentScripts@2023-08-01' = { + dependsOn: [ linkerCreatorRole ] + name: 'appLinkToMySql' + location: location + kind: 'AzureCLI' + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${linkerCreatorIdentity.id}': {} } } + properties: { + azCliVersion: '2.63.0' + timeout: 'PT10M' + scriptContent: 'apk update; apk add g++; apk add unixodbc-dev; az extension add --name containerapp; az extension add --name serviceconnector-passwordless; az extension add --name containerapp; az extension add --name serviceconnector-passwordless --upgrade; az containerapp connection create mysql-flexible --connection \'appLinkToMySql\' --source-id ${app.id} --target-id ${mysqlDatabaseId} --client-type \'springBoot\' --user-identity client-id=${identity.properties.clientId} subs-id=${subscription().subscriptionId} user-object-id=${linkerCreatorIdentity.properties.principalId} mysql-identity-id=${mysqlIdentityName} -c main --yes 1>&2' + cleanupPreference: 'OnSuccess' + retentionInterval: 'P1D' + } } {{- end}} diff --git a/cli/azd/resources/scaffold/templates/main.bicept b/cli/azd/resources/scaffold/templates/main.bicept index 4f33ab35651..2cb3d975ca6 100644 --- a/cli/azd/resources/scaffold/templates/main.bicept +++ b/cli/azd/resources/scaffold/templates/main.bicept @@ -134,6 +134,7 @@ module mysqlDb './app/db-mysql.bicep' = { serverName: '${abbrs.dBforMySQLServers}${resourceToken}' location: location tags: tags + identityName: '${abbrs.managedIdentityUserAssignedIdentities}mysql-${resourceToken}' databasePassword: databasePassword keyVaultName: keyVault.outputs.name allowAllIPsFirewall: true @@ -170,6 +171,7 @@ module {{bicepName .Name}} './app/{{.Name}}.bicep' = { {{- end}} {{- if .DbMySql}} mysqlDatabaseId: mysqlDb.outputs.databaseId + mysqlIdentityName: mysqlDb.outputs.identityName {{- end}} {{- if (and .Frontend .Frontend.Backends)}} apiUrls: [ From 708681aadd4450cbce70f51c4311a4f2fbb361c4 Mon Sep 17 00:00:00 2001 From: rujche Date: Sun, 29 Sep 2024 13:14:33 +0800 Subject: [PATCH 20/39] 1. Remove duplicated 'azd extension add'. 2. Delete '1>&2' used for debug. 3. Add 'az tag create' to fix the problem about tag been deleted when creating service connector. --- cli/azd/resources/scaffold/templates/host-containerapp.bicept | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/azd/resources/scaffold/templates/host-containerapp.bicept b/cli/azd/resources/scaffold/templates/host-containerapp.bicept index 42601ff4605..4333fe5ef76 100644 --- a/cli/azd/resources/scaffold/templates/host-containerapp.bicept +++ b/cli/azd/resources/scaffold/templates/host-containerapp.bicept @@ -267,7 +267,7 @@ resource appLinkToMySql 'Microsoft.Resources/deploymentScripts@2023-08-01' = { properties: { azCliVersion: '2.63.0' timeout: 'PT10M' - scriptContent: 'apk update; apk add g++; apk add unixodbc-dev; az extension add --name containerapp; az extension add --name serviceconnector-passwordless; az extension add --name containerapp; az extension add --name serviceconnector-passwordless --upgrade; az containerapp connection create mysql-flexible --connection \'appLinkToMySql\' --source-id ${app.id} --target-id ${mysqlDatabaseId} --client-type \'springBoot\' --user-identity client-id=${identity.properties.clientId} subs-id=${subscription().subscriptionId} user-object-id=${linkerCreatorIdentity.properties.principalId} mysql-identity-id=${mysqlIdentityName} -c main --yes 1>&2' + scriptContent: 'apk update; apk add g++; apk add unixodbc-dev; az extension add --name containerapp; az extension add --name serviceconnector-passwordless --upgrade; az containerapp connection create mysql-flexible --connection appLinkToMySql --source-id ${app.id} --target-id ${mysqlDatabaseId} --client-type springBoot --user-identity client-id=${identity.properties.clientId} subs-id=${subscription().subscriptionId} user-object-id=${linkerCreatorIdentity.properties.principalId} mysql-identity-id=${mysqlIdentityName} -c main --yes; az tag create --resource-id ${app.id} --tags azd-service-name={{.Name}} ' cleanupPreference: 'OnSuccess' retentionInterval: 'P1D' } From 353a80222cc0c0a5d25fdc03b95cfa243f1c11b5 Mon Sep 17 00:00:00 2001 From: rujche Date: Sun, 29 Sep 2024 13:22:06 +0800 Subject: [PATCH 21/39] Update name of resources: linkerCreatorIdentity and appLinkToMySql --- cli/azd/resources/scaffold/templates/host-containerapp.bicept | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/azd/resources/scaffold/templates/host-containerapp.bicept b/cli/azd/resources/scaffold/templates/host-containerapp.bicept index 4333fe5ef76..1dc60722d5a 100644 --- a/cli/azd/resources/scaffold/templates/host-containerapp.bicept +++ b/cli/azd/resources/scaffold/templates/host-containerapp.bicept @@ -238,7 +238,7 @@ resource app 'Microsoft.App/containerApps@2023-05-02-preview' = { {{- if .DbMySql}} resource linkerCreatorIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { - name: 'linkerCreatorIdentity' + name: '${name}-linker-creator-identity' location: location } @@ -255,7 +255,7 @@ resource linkerCreatorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' resource appLinkToMySql 'Microsoft.Resources/deploymentScripts@2023-08-01' = { dependsOn: [ linkerCreatorRole ] - name: 'appLinkToMySql' + name: '${name}-deployment-script' location: location kind: 'AzureCLI' identity: { From b70878eaaea8dc30f52bc60a4d856c4af664a192 Mon Sep 17 00:00:00 2001 From: rujche Date: Mon, 7 Oct 2024 18:06:39 +0800 Subject: [PATCH 22/39] 1. Add rule about postgresql in project_analyzer_java.go. 2. Update log about "failed to read spring application properties". 3. Fix bug about can not find frontend app and backend app at the same time. 4. Add service connector from aca to postgresql. --- cli/azd/internal/appdetect/appdetect.go | 2 ++ .../javaanalyze/project_analyzer_java.go | 1 + .../javaanalyze/project_analyzer_spring.go | 3 +- .../appdetect/javaanalyze/rule_postgresql.go | 27 +++++++++++++++++ .../scaffold/base/shared/monitoring.bicep | 1 + .../scaffold/templates/db-postgres.bicept | 1 + .../templates/host-containerapp.bicept | 29 +++++++++++++++++-- .../resources/scaffold/templates/main.bicept | 8 +++-- 8 files changed, 67 insertions(+), 5 deletions(-) create mode 100644 cli/azd/internal/appdetect/javaanalyze/rule_postgresql.go diff --git a/cli/azd/internal/appdetect/appdetect.go b/cli/azd/internal/appdetect/appdetect.go index 122ba242fa6..8d45a55cc87 100644 --- a/cli/azd/internal/appdetect/appdetect.go +++ b/cli/azd/internal/appdetect/appdetect.go @@ -349,6 +349,8 @@ func analyze(projects []Project) []Project { result = append(result, copiedProject) } } + } else { + result = append(result, project) } } return result diff --git a/cli/azd/internal/appdetect/javaanalyze/project_analyzer_java.go b/cli/azd/internal/appdetect/javaanalyze/project_analyzer_java.go index bdb0c9cf38a..fe8abae659f 100644 --- a/cli/azd/internal/appdetect/javaanalyze/project_analyzer_java.go +++ b/cli/azd/internal/appdetect/javaanalyze/project_analyzer_java.go @@ -12,6 +12,7 @@ func Analyze(path string) []AzureYaml { rules := []rule{ &ruleService{}, &ruleMysql{}, + &rulePostgresql{}, &ruleStorage{}, &ruleServiceBusScsb{}, } diff --git a/cli/azd/internal/appdetect/javaanalyze/project_analyzer_spring.go b/cli/azd/internal/appdetect/javaanalyze/project_analyzer_spring.go index 85047325da4..eef378a9836 100644 --- a/cli/azd/internal/appdetect/javaanalyze/project_analyzer_spring.go +++ b/cli/azd/internal/appdetect/javaanalyze/project_analyzer_spring.go @@ -21,7 +21,8 @@ func findSpringApplicationProperties(projectPath string) map[string]interface{} yamlFilePath := projectPath + "/src/main/resources/application.yml" data, err := ioutil.ReadFile(yamlFilePath) if err != nil { - log.Fatalf("error reading YAML file: %v", err) + log.Printf("failed to read spring application properties: %s", yamlFilePath) + return nil } // Parse the YAML into a yaml.Node diff --git a/cli/azd/internal/appdetect/javaanalyze/rule_postgresql.go b/cli/azd/internal/appdetect/javaanalyze/rule_postgresql.go new file mode 100644 index 00000000000..bfe58533428 --- /dev/null +++ b/cli/azd/internal/appdetect/javaanalyze/rule_postgresql.go @@ -0,0 +1,27 @@ +package javaanalyze + +type rulePostgresql struct { +} + +func (mr *rulePostgresql) match(javaProject *javaProject) bool { + if javaProject.mavenProject.Dependencies != nil { + for _, dep := range javaProject.mavenProject.Dependencies { + if dep.GroupId == "org.postgresql" && dep.ArtifactId == "postgresql" { + return true + } + } + } + return false +} + +func (mr *rulePostgresql) apply(azureYaml *AzureYaml) { + azureYaml.Resources = append(azureYaml.Resources, &Resource{ + Name: "PostgreSQL", + Type: "PostgreSQL", + }) + + azureYaml.ServiceBindings = append(azureYaml.ServiceBindings, ServiceBinding{ + Name: "PostgreSQL", + AuthType: AuthType_SYSTEM_MANAGED_IDENTITY, + }) +} diff --git a/cli/azd/resources/scaffold/base/shared/monitoring.bicep b/cli/azd/resources/scaffold/base/shared/monitoring.bicep index 4ae9796cc3b..7b50e45ec24 100644 --- a/cli/azd/resources/scaffold/base/shared/monitoring.bicep +++ b/cli/azd/resources/scaffold/base/shared/monitoring.bicep @@ -30,5 +30,6 @@ resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { } output applicationInsightsName string = applicationInsights.name +output connectionString string = applicationInsights.properties.ConnectionString output logAnalyticsWorkspaceId string = logAnalytics.id output logAnalyticsWorkspaceName string = logAnalytics.name diff --git a/cli/azd/resources/scaffold/templates/db-postgres.bicept b/cli/azd/resources/scaffold/templates/db-postgres.bicept index 54866987449..b6ebb5a87b8 100644 --- a/cli/azd/resources/scaffold/templates/db-postgres.bicept +++ b/cli/azd/resources/scaffold/templates/db-postgres.bicept @@ -73,6 +73,7 @@ resource dbPasswordKey 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { } } +output databaseId string = database.id output databaseHost string = postgreServer.properties.fullyQualifiedDomainName output databaseName string = databaseName output databaseUser string = databaseUser diff --git a/cli/azd/resources/scaffold/templates/host-containerapp.bicept b/cli/azd/resources/scaffold/templates/host-containerapp.bicept index 1dc60722d5a..4f0d3c2cb27 100644 --- a/cli/azd/resources/scaffold/templates/host-containerapp.bicept +++ b/cli/azd/resources/scaffold/templates/host-containerapp.bicept @@ -15,6 +15,7 @@ param cosmosDbConnectionString string param postgresDatabaseHost string param postgresDatabaseUser string param postgresDatabaseName string +param postgresDatabaseId string @secure() param postgresDatabasePassword string {{- end}} @@ -235,7 +236,7 @@ resource app 'Microsoft.App/containerApps@2023-05-02-preview' = { } } } -{{- if .DbMySql}} +{{- if (or .DbMySql .DbPostgres)}} resource linkerCreatorIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { name: '${name}-linker-creator-identity' @@ -252,10 +253,12 @@ resource linkerCreatorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' principalId: linkerCreatorIdentity.properties.principalId } } +{{- end}} +{{- if .DbMySql}} resource appLinkToMySql 'Microsoft.Resources/deploymentScripts@2023-08-01' = { dependsOn: [ linkerCreatorRole ] - name: '${name}-deployment-script' + name: '${name}-link-to-mysql' location: location kind: 'AzureCLI' identity: { @@ -273,6 +276,28 @@ resource appLinkToMySql 'Microsoft.Resources/deploymentScripts@2023-08-01' = { } } {{- end}} +{{- if .DbPostgres}} + +resource appLinkToPostgres 'Microsoft.Resources/deploymentScripts@2023-08-01' = { + dependsOn: [ linkerCreatorRole ] + name: '${name}-link-to-postgres' + location: location + kind: 'AzureCLI' + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${linkerCreatorIdentity.id}': {} + } + } + properties: { + azCliVersion: '2.63.0' + timeout: 'PT10M' + scriptContent: 'apk update; apk add g++; apk add unixodbc-dev; az extension add --name containerapp; az extension add --name serviceconnector-passwordless --upgrade; az containerapp connection create postgres-flexible --connection appLinkToPostgres --source-id ${app.id} --target-id ${postgresDatabaseId} --client-type springBoot --user-identity client-id=${identity.properties.clientId} subs-id=${subscription().subscriptionId} user-object-id=${linkerCreatorIdentity.properties.principalId} -c main --yes; az tag create --resource-id ${app.id} --tags azd-service-name={{.Name}} ' + cleanupPreference: 'OnSuccess' + retentionInterval: 'P1D' + } +} +{{- end}} output defaultDomain string = containerAppsEnvironment.properties.defaultDomain output name string = app.name diff --git a/cli/azd/resources/scaffold/templates/main.bicept b/cli/azd/resources/scaffold/templates/main.bicept index 2cb3d975ca6..0d8d6fdbcaa 100644 --- a/cli/azd/resources/scaffold/templates/main.bicept +++ b/cli/azd/resources/scaffold/templates/main.bicept @@ -141,10 +141,9 @@ module mysqlDb './app/db-mysql.bicep' = { } scope: rg } - {{- end}} - {{- range .Services}} + module {{bicepName .Name}} './app/{{.Name}}.bicep' = { name: '{{.Name}}' params: { @@ -167,6 +166,7 @@ module {{bicepName .Name}} './app/{{.Name}}.bicep' = { postgresDatabaseName: postgresDb.outputs.databaseName postgresDatabaseHost: postgresDb.outputs.databaseHost postgresDatabaseUser: postgresDb.outputs.databaseUser + postgresDatabaseId: postgresDb.outputs.databaseId postgresDatabasePassword: vault.getSecret(postgresDb.outputs.databaseConnectionKey) {{- end}} {{- if .DbMySql}} @@ -195,4 +195,8 @@ module {{bicepName .Name}} './app/{{.Name}}.bicep' = { output AZURE_CONTAINER_REGISTRY_ENDPOINT string = registry.outputs.loginServer output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.endpoint +output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.connectionString +{{- range .Services}} +output {{.Name}}_uri string = {{.Name}}.outputs.uri +{{- end}} {{ end}} From 437eeb69041bb6436660f87ecae61f30839a5734 Mon Sep 17 00:00:00 2001 From: rujche Date: Mon, 7 Oct 2024 22:14:23 +0800 Subject: [PATCH 23/39] Fix the error about CORS. --- cli/azd/resources/scaffold/templates/host-containerapp.bicept | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/azd/resources/scaffold/templates/host-containerapp.bicept b/cli/azd/resources/scaffold/templates/host-containerapp.bicept index 4f0d3c2cb27..768693889da 100644 --- a/cli/azd/resources/scaffold/templates/host-containerapp.bicept +++ b/cli/azd/resources/scaffold/templates/host-containerapp.bicept @@ -128,6 +128,7 @@ resource app 'Microsoft.App/containerApps@2023-05-02-preview' = { allowedOrigins: union(allowedOrigins, [ // define additional allowed origins here ]) + allowedMethods: ['GET', 'PUT', 'POST', 'DELETE'] } {{- end}} } From df2f5f2fcf1a9e3ab20f26f017d9f41f8f9bea83 Mon Sep 17 00:00:00 2001 From: Xiaolu Dai Date: Tue, 8 Oct 2024 13:55:18 +0800 Subject: [PATCH 24/39] add support for service bus --- cli/azd/internal/appdetect/appdetect.go | 28 ++++++------- cli/azd/internal/repository/app_init.go | 5 +-- cli/azd/internal/repository/detect_confirm.go | 31 +++++++------- cli/azd/internal/repository/infra_confirm.go | 35 +++++++++------- cli/azd/internal/repository/infra_prompt.go | 2 +- cli/azd/internal/scaffold/scaffold.go | 7 ++++ .../templates/azure-service-bus.bicept | 40 +++++++++++++++++++ .../templates/host-containerapp.bicept | 10 +++++ .../resources/scaffold/templates/main.bicept | 18 ++++++++- 9 files changed, 125 insertions(+), 51 deletions(-) create mode 100644 cli/azd/resources/scaffold/templates/azure-service-bus.bicept diff --git a/cli/azd/internal/appdetect/appdetect.go b/cli/azd/internal/appdetect/appdetect.go index 8d45a55cc87..9ec2a63e05d 100644 --- a/cli/azd/internal/appdetect/appdetect.go +++ b/cli/azd/internal/appdetect/appdetect.go @@ -132,22 +132,18 @@ func (db DatabaseDep) Display() string { return "" } -type AzureDep string +//type AzureDep string -const ( - AzureStorage AzureDep = "storage" - AzureServiceBus AzureDep = "servicebus" -) +type AzureDep interface { + ResourceDisplay() string +} -func (azureDep AzureDep) Display() string { - switch azureDep { - case AzureStorage: - return "Azure Storage" - case AzureServiceBus: - return "Azure Service Bus" - } +type AzureDepServiceBus struct { + Queues []string +} - return "" +func (a AzureDepServiceBus) ResourceDisplay() string { + return "Azure Service Bus" } type Project struct { @@ -370,9 +366,9 @@ func enrichFromJavaProject(azureYaml javaanalyze.AzureYaml, project *Project) { } else if resource.GetType() == "Redis" { project.DatabaseDeps = append(project.DatabaseDeps, DbRedis) } else if resource.GetType() == "Azure Service Bus" { - project.AzureDeps = append(project.AzureDeps, AzureServiceBus) - } else if resource.GetType() == "Azure Storage" { - project.AzureDeps = append(project.AzureDeps, AzureStorage) + project.AzureDeps = append(project.AzureDeps, AzureDepServiceBus{ + Queues: resource.(*javaanalyze.ServiceBusResource).Queues, + }) } } } diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index b0c24c8ef1f..712782808b8 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -38,9 +38,8 @@ var dbMap = map[appdetect.DatabaseDep]struct{}{ appdetect.DbRedis: {}, } -var azureDepMap = map[appdetect.AzureDep]struct{}{ - appdetect.AzureServiceBus: {}, - appdetect.AzureStorage: {}, +var azureDepMap = map[string]struct{}{ + appdetect.AzureDepServiceBus{}.ResourceDisplay(): {}, } // InitFromApp initializes the infra directory and project file from the current existing app. diff --git a/cli/azd/internal/repository/detect_confirm.go b/cli/azd/internal/repository/detect_confirm.go index 0f641e3fa91..cefce4f8e6c 100644 --- a/cli/azd/internal/repository/detect_confirm.go +++ b/cli/azd/internal/repository/detect_confirm.go @@ -42,12 +42,17 @@ const ( EntryKindModified EntryKind = "modified" ) +type Pair struct { + first appdetect.AzureDep + second EntryKind +} + // detectConfirm handles prompting for confirming the detected services and databases type detectConfirm struct { // detected services and databases Services []appdetect.Project Databases map[appdetect.DatabaseDep]EntryKind - AzureDeps map[appdetect.AzureDep]EntryKind + AzureDeps map[string]Pair // the root directory of the project root string @@ -60,7 +65,7 @@ type detectConfirm struct { // Init initializes state from initial detection output func (d *detectConfirm) Init(projects []appdetect.Project, root string) { d.Databases = make(map[appdetect.DatabaseDep]EntryKind) - d.AzureDeps = make(map[appdetect.AzureDep]EntryKind) + d.AzureDeps = make(map[string]Pair) d.Services = make([]appdetect.Project, 0, len(projects)) d.modified = false d.root = root @@ -77,8 +82,8 @@ func (d *detectConfirm) Init(projects []appdetect.Project, root string) { } for _, azureDep := range project.AzureDeps { - if _, supported := azureDepMap[azureDep]; supported { - d.AzureDeps[azureDep] = EntryKindDetected + if _, supported := azureDepMap[azureDep.ResourceDisplay()]; supported { + d.AzureDeps[azureDep.ResourceDisplay()] = Pair{azureDep, EntryKindDetected} } } } @@ -104,8 +109,9 @@ func (d *detectConfirm) captureUsage( } azureDepNames := make([]string, 0, len(d.AzureDeps)) - for azureDep := range d.AzureDeps { - azureDepNames = append(azureDepNames, string(azureDep)) + + for _, pair := range d.AzureDeps { + azureDepNames = append(azureDepNames, pair.first.ResourceDisplay()) } tracing.SetUsageAttributes( @@ -250,21 +256,16 @@ func (d *detectConfirm) render(ctx context.Context) error { d.console.Message(ctx, "\n"+output.WithBold("Detected Azure dependencies:")+"\n") } for azureDep, entry := range d.AzureDeps { - switch azureDep { - case appdetect.AzureStorage: - recommendedServices = append(recommendedServices, "Azure Storage") - case appdetect.AzureServiceBus: - recommendedServices = append(recommendedServices, "Azure Service Bus") - } + recommendedServices = append(recommendedServices, azureDep) status := "" - if entry == EntryKindModified { + if entry.second == EntryKindModified { status = " " + output.WithSuccessFormat("[Updated]") - } else if entry == EntryKindManual { + } else if entry.second == EntryKindManual { status = " " + output.WithSuccessFormat("[Added]") } - d.console.Message(ctx, " "+color.BlueString(azureDep.Display())+status) + d.console.Message(ctx, " "+color.BlueString(azureDep)+status) d.console.Message(ctx, "") } diff --git a/cli/azd/internal/repository/infra_confirm.go b/cli/azd/internal/repository/infra_confirm.go index b577fdc6e9b..92386391881 100644 --- a/cli/azd/internal/repository/infra_confirm.go +++ b/cli/azd/internal/repository/infra_confirm.go @@ -103,10 +103,10 @@ func (i *Initializer) infraSpecFromDetect( } } - for azureDep := range detect.AzureDeps { - infraSpec, err := i.promptForAzureResource(ctx, azureDep, spec) + for _, azureDep := range detect.AzureDeps { + err := i.promptForAzureResource(ctx, azureDep.first, &spec) if err != nil { - return infraSpec, err + return scaffold.InfraSpec{}, err } } @@ -157,6 +157,13 @@ func (i *Initializer) infraSpecFromDetect( } } } + + for _, azureDep := range svc.AzureDeps { + switch azureDep.(type) { + case appdetect.AzureDepServiceBus: + serviceSpec.AzureServiceBus = spec.AzureServiceBus + } + } spec.Services = append(spec.Services, serviceSpec) } @@ -219,18 +226,18 @@ func (i *Initializer) infraSpecFromDetect( func (i *Initializer) promptForAzureResource( ctx context.Context, azureDep appdetect.AzureDep, - spec scaffold.InfraSpec) (scaffold.InfraSpec, error) { + spec *scaffold.InfraSpec) error { azureDepPrompt: for { azureDepName, err := i.console.Prompt(ctx, input.ConsoleOptions{ - Message: fmt.Sprintf("Input the name of the Azure dependency (%s)", azureDep.Display()), + Message: fmt.Sprintf("Input the name of the Azure dependency (%s)", azureDep.ResourceDisplay()), Help: "Hint: Azure dependency name\n\n" + "Name of the Azure dependency that the app connects to. " + "This dependency will be created after running azd provision or azd up." + "\nYou may be able to skip this step by hitting enter, in which case the dependency will not be created.", }) if err != nil { - return scaffold.InfraSpec{}, err + return err } if strings.ContainsAny(azureDepName, " ") { @@ -241,7 +248,7 @@ azureDepPrompt: Message: fmt.Sprintf("Continue with name '%s'?", azureDepName), }) if err != nil { - return scaffold.InfraSpec{}, err + return err } if !confirm { @@ -256,7 +263,7 @@ azureDepPrompt: Message: fmt.Sprintf("Continue with name '%s'?", azureDepName), }) if err != nil { - return scaffold.InfraSpec{}, err + return err } if !confirm { @@ -264,17 +271,15 @@ azureDepPrompt: } } - switch azureDep { - case appdetect.AzureServiceBus: - + switch azureDep.(type) { + case appdetect.AzureDepServiceBus: spec.AzureServiceBus = &scaffold.AzureDepServiceBus{ - Name: azureDepName, + Name: azureDepName, + Queues: azureDep.(appdetect.AzureDepServiceBus).Queues, } break azureDepPrompt - case appdetect.AzureStorage: - break azureDepPrompt } break azureDepPrompt } - return spec, nil + return nil } diff --git a/cli/azd/internal/repository/infra_prompt.go b/cli/azd/internal/repository/infra_prompt.go index 69622d40073..ed1ac1d0e77 100644 --- a/cli/azd/internal/repository/infra_prompt.go +++ b/cli/azd/internal/repository/infra_prompt.go @@ -18,7 +18,7 @@ type serviceBusPrompt struct { } func (s *serviceBusPrompt) Type() string { - return appdetect.AzureServiceBus.Display() + return appdetect.AzureDepServiceBus{}.ResourceDisplay() } func (s *serviceBusPrompt) Properties() map[string]string { diff --git a/cli/azd/internal/scaffold/scaffold.go b/cli/azd/internal/scaffold/scaffold.go index ae2d876fdc2..b89a94ce317 100644 --- a/cli/azd/internal/scaffold/scaffold.go +++ b/cli/azd/internal/scaffold/scaffold.go @@ -136,6 +136,13 @@ func ExecInfra( } } + if spec.AzureServiceBus != nil { + err = Execute(t, "azure-service-bus.bicep", spec.AzureServiceBus, filepath.Join(infraApp, "azure-service-bus.bicep")) + if err != nil { + return fmt.Errorf("scaffolding service bus: %w", err) + } + } + for _, svc := range spec.Services { err = Execute(t, "host-containerapp.bicep", svc, filepath.Join(infraApp, svc.Name+".bicep")) if err != nil { diff --git a/cli/azd/resources/scaffold/templates/azure-service-bus.bicept b/cli/azd/resources/scaffold/templates/azure-service-bus.bicept new file mode 100644 index 00000000000..ce874456060 --- /dev/null +++ b/cli/azd/resources/scaffold/templates/azure-service-bus.bicept @@ -0,0 +1,40 @@ +{{define "azure-service-bus.bicep" -}} +param serviceBusNamespaceName string +param location string +param tags object = {} + +resource serviceBusNamespace 'Microsoft.ServiceBus/namespaces@2022-10-01-preview' = { + name: serviceBusNamespaceName + location: location + tags: tags + sku: { + name: 'Standard' + tier: 'Standard' + capacity: 1 + } +} + +{{- range $index, $element := .Queues }} +resource serviceBusQueue_{{ $index }} 'Microsoft.ServiceBus/namespaces/queues@2022-01-01-preview' = { + parent: serviceBusNamespace + name: '{{ $element }}' + properties: { + lockDuration: 'PT5M' + maxSizeInMegabytes: 1024 + requiresDuplicateDetection: false + requiresSession: false + defaultMessageTimeToLive: 'P10675199DT2H48M5.4775807S' + deadLetteringOnMessageExpiration: false + duplicateDetectionHistoryTimeWindow: 'PT10M' + maxDeliveryCount: 10 + autoDeleteOnIdle: 'P10675199DT2H48M5.4775807S' + enablePartitioning: false + enableExpress: false + } +} +{{end}} + +output serviceBusNamespaceId string = serviceBusNamespace.id +output serviceBusNamespaceApiVersion string = serviceBusNamespace.apiVersion +output serviceBusConnectionString string = listKeys('${serviceBusNamespace.id}/AuthorizationRules/RootManageSharedAccessKey', serviceBusNamespace.apiVersion).primaryConnectionString +{{ end}} \ No newline at end of file diff --git a/cli/azd/resources/scaffold/templates/host-containerapp.bicept b/cli/azd/resources/scaffold/templates/host-containerapp.bicept index 768693889da..45a4bf212fa 100644 --- a/cli/azd/resources/scaffold/templates/host-containerapp.bicept +++ b/cli/azd/resources/scaffold/templates/host-containerapp.bicept @@ -23,6 +23,10 @@ param postgresDatabasePassword string param mysqlDatabaseId string param mysqlIdentityName string {{- end}} +{{- if .AzureServiceBus}} +@secure() +param azureServiceBusConnectionString string +{{- end}} {{- if .DbRedis}} param redisName string {{- end}} @@ -152,6 +156,12 @@ resource app 'Microsoft.App/containerApps@2023-05-02-preview' = { value: postgresDatabasePassword } {{- end}} + {{- if .AzureServiceBus}} + { + name: 'SPRING_CLOUD_AZURE_SERVICEBUS_CONNECTION_STRING' + value: azureServiceBusConnectionString + } + {{- end}} ], map(secrets, secret => { name: secret.secretRef diff --git a/cli/azd/resources/scaffold/templates/main.bicept b/cli/azd/resources/scaffold/templates/main.bicept index 0d8d6fdbcaa..aa1a558646e 100644 --- a/cli/azd/resources/scaffold/templates/main.bicept +++ b/cli/azd/resources/scaffold/templates/main.bicept @@ -91,7 +91,7 @@ module appsEnv './shared/apps-env.bicep' = { } scope: rg } -{{- if (or (or .DbCosmosMongo .DbPostgres) .DbMySql)}} +{{- if (or (or (or .DbCosmosMongo .DbPostgres) .DbMySql) .AzureServiceBus)}} resource vault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { name: keyVault.outputs.name @@ -142,6 +142,19 @@ module mysqlDb './app/db-mysql.bicep' = { scope: rg } {{- end}} + +{{- if .AzureServiceBus }} +module serviceBus './app/azure-service-bus.bicep' = { + name: 'serviceBus' + params: { + serviceBusNamespaceName: '${abbrs.serviceBusNamespaces}${resourceToken}' + location: location + tags: tags + } + scope: rg +} +{{- end}} + {{- range .Services}} module {{bicepName .Name}} './app/{{.Name}}.bicep' = { @@ -173,6 +186,9 @@ module {{bicepName .Name}} './app/{{.Name}}.bicep' = { mysqlDatabaseId: mysqlDb.outputs.databaseId mysqlIdentityName: mysqlDb.outputs.identityName {{- end}} + {{- if .AzureServiceBus }} + azureServiceBusConnectionString: vault.getSecret(serviceBus.outputs.serviceBusConnectionString) + {{- end}} {{- if (and .Frontend .Frontend.Backends)}} apiUrls: [ {{- range .Frontend.Backends}} From b6e6ecca1591b0d8a96ed620fc5d6ce5668e9ae1 Mon Sep 17 00:00:00 2001 From: Xiaolu Dai Date: Tue, 8 Oct 2024 17:32:29 +0800 Subject: [PATCH 25/39] fix servicebus --- .../scaffold/templates/azure-service-bus.bicept | 15 ++++++++++++++- .../scaffold/templates/host-containerapp.bicept | 8 +++++++- cli/azd/resources/scaffold/templates/main.bicept | 5 +++-- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/cli/azd/resources/scaffold/templates/azure-service-bus.bicept b/cli/azd/resources/scaffold/templates/azure-service-bus.bicept index ce874456060..01ed109bf55 100644 --- a/cli/azd/resources/scaffold/templates/azure-service-bus.bicept +++ b/cli/azd/resources/scaffold/templates/azure-service-bus.bicept @@ -1,5 +1,6 @@ {{define "azure-service-bus.bicep" -}} param serviceBusNamespaceName string +param keyVaultName string param location string param tags object = {} @@ -34,7 +35,19 @@ resource serviceBusQueue_{{ $index }} 'Microsoft.ServiceBus/namespaces/queues@20 } {{end}} +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { + name: keyVaultName +} + +resource serviceBusConnectionString 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { + parent: keyVault + name: 'serviceBusConnectionString' + properties: { + value: listKeys('${serviceBusNamespace.id}/AuthorizationRules/RootManageSharedAccessKey', serviceBusNamespace.apiVersion).primaryConnectionString + } +} + output serviceBusNamespaceId string = serviceBusNamespace.id output serviceBusNamespaceApiVersion string = serviceBusNamespace.apiVersion -output serviceBusConnectionString string = listKeys('${serviceBusNamespace.id}/AuthorizationRules/RootManageSharedAccessKey', serviceBusNamespace.apiVersion).primaryConnectionString +output serviceBusConnectionStringKey string = 'serviceBusConnectionString' {{ end}} \ No newline at end of file diff --git a/cli/azd/resources/scaffold/templates/host-containerapp.bicept b/cli/azd/resources/scaffold/templates/host-containerapp.bicept index 45a4bf212fa..7c1d91d366e 100644 --- a/cli/azd/resources/scaffold/templates/host-containerapp.bicept +++ b/cli/azd/resources/scaffold/templates/host-containerapp.bicept @@ -158,7 +158,7 @@ resource app 'Microsoft.App/containerApps@2023-05-02-preview' = { {{- end}} {{- if .AzureServiceBus}} { - name: 'SPRING_CLOUD_AZURE_SERVICEBUS_CONNECTION_STRING' + name: 'spring-cloud-azure-servicebus-connection-string' value: azureServiceBusConnectionString } {{- end}} @@ -206,6 +206,12 @@ resource app 'Microsoft.App/containerApps@2023-05-02-preview' = { value: '5432' } {{- end}} + {{- if .AzureServiceBus}} + { + name: 'SPRING_CLOUD_AZURE_SERVICEBUS_CONNECTION_STRING' + secretRef: 'spring-cloud-azure-servicebus-connection-string' + } + {{- end}} {{- if .Frontend}} {{- range $i, $e := .Frontend.Backends}} { diff --git a/cli/azd/resources/scaffold/templates/main.bicept b/cli/azd/resources/scaffold/templates/main.bicept index aa1a558646e..3659af2d310 100644 --- a/cli/azd/resources/scaffold/templates/main.bicept +++ b/cli/azd/resources/scaffold/templates/main.bicept @@ -150,6 +150,7 @@ module serviceBus './app/azure-service-bus.bicep' = { serviceBusNamespaceName: '${abbrs.serviceBusNamespaces}${resourceToken}' location: location tags: tags + keyVaultName: keyVault.outputs.name } scope: rg } @@ -187,7 +188,7 @@ module {{bicepName .Name}} './app/{{.Name}}.bicep' = { mysqlIdentityName: mysqlDb.outputs.identityName {{- end}} {{- if .AzureServiceBus }} - azureServiceBusConnectionString: vault.getSecret(serviceBus.outputs.serviceBusConnectionString) + azureServiceBusConnectionString: vault.getSecret(serviceBus.outputs.serviceBusConnectionStringKey) {{- end}} {{- if (and .Frontend .Frontend.Backends)}} apiUrls: [ @@ -213,6 +214,6 @@ output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.endpoint output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.connectionString {{- range .Services}} -output {{.Name}}_uri string = {{.Name}}.outputs.uri +output {{bicepName .Name}}_uri string = {{bicepName .Name}}.outputs.uri {{- end}} {{ end}} From 073d857f80d4bbb5596de0f308ce11c81bbb2a80 Mon Sep 17 00:00:00 2001 From: rujche Date: Tue, 8 Oct 2024 17:59:59 +0800 Subject: [PATCH 26/39] Remove the logic of create tag after create service connector. --- cli/azd/resources/scaffold/templates/host-containerapp.bicept | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/azd/resources/scaffold/templates/host-containerapp.bicept b/cli/azd/resources/scaffold/templates/host-containerapp.bicept index 768693889da..43268ac366a 100644 --- a/cli/azd/resources/scaffold/templates/host-containerapp.bicept +++ b/cli/azd/resources/scaffold/templates/host-containerapp.bicept @@ -271,7 +271,7 @@ resource appLinkToMySql 'Microsoft.Resources/deploymentScripts@2023-08-01' = { properties: { azCliVersion: '2.63.0' timeout: 'PT10M' - scriptContent: 'apk update; apk add g++; apk add unixodbc-dev; az extension add --name containerapp; az extension add --name serviceconnector-passwordless --upgrade; az containerapp connection create mysql-flexible --connection appLinkToMySql --source-id ${app.id} --target-id ${mysqlDatabaseId} --client-type springBoot --user-identity client-id=${identity.properties.clientId} subs-id=${subscription().subscriptionId} user-object-id=${linkerCreatorIdentity.properties.principalId} mysql-identity-id=${mysqlIdentityName} -c main --yes; az tag create --resource-id ${app.id} --tags azd-service-name={{.Name}} ' + scriptContent: 'apk update; apk add g++; apk add unixodbc-dev; az extension add --name containerapp; az extension add --name serviceconnector-passwordless --upgrade; az containerapp connection create mysql-flexible --connection appLinkToMySql --source-id ${app.id} --target-id ${mysqlDatabaseId} --client-type springBoot --user-identity client-id=${identity.properties.clientId} subs-id=${subscription().subscriptionId} user-object-id=${linkerCreatorIdentity.properties.principalId} mysql-identity-id=${mysqlIdentityName} -c main --yes;' cleanupPreference: 'OnSuccess' retentionInterval: 'P1D' } From f1e2fc17da05920be4642e4b48c6495f65713c94 Mon Sep 17 00:00:00 2001 From: Xiaolu Dai Date: Wed, 9 Oct 2024 20:21:13 +0800 Subject: [PATCH 27/39] support both mi and connection string for service bus --- cli/azd/internal/repository/infra_confirm.go | 32 ++++++++++- cli/azd/internal/repository/infra_prompt.go | 38 ------------- cli/azd/internal/scaffold/spec.go | 19 ++++++- .../templates/azure-service-bus.bicept | 7 +++ .../templates/host-containerapp.bicept | 55 ++++++++++++++++++- .../resources/scaffold/templates/main.bicept | 12 +++- 6 files changed, 114 insertions(+), 49 deletions(-) delete mode 100644 cli/azd/internal/repository/infra_prompt.go diff --git a/cli/azd/internal/repository/infra_confirm.go b/cli/azd/internal/repository/infra_confirm.go index 92386391881..4495f29a3c9 100644 --- a/cli/azd/internal/repository/infra_confirm.go +++ b/cli/azd/internal/repository/infra_confirm.go @@ -231,7 +231,7 @@ azureDepPrompt: for { azureDepName, err := i.console.Prompt(ctx, input.ConsoleOptions{ Message: fmt.Sprintf("Input the name of the Azure dependency (%s)", azureDep.ResourceDisplay()), - Help: "Hint: Azure dependency name\n\n" + + Help: "Azure dependency name\n\n" + "Name of the Azure dependency that the app connects to. " + "This dependency will be created after running azd provision or azd up." + "\nYou may be able to skip this step by hitting enter, in which case the dependency will not be created.", @@ -271,11 +271,37 @@ azureDepPrompt: } } + authType := scaffold.AuthType(0) + switch azureDep.(type) { + case appdetect.AzureDepServiceBus: + _authType, err := i.console.Prompt(ctx, input.ConsoleOptions{ + Message: fmt.Sprintf("Input the authentication type you want for (%s), 1 for connection string, 2 for managed identity", azureDep.ResourceDisplay()), + Help: "Authentication type:\n\n" + + "Enter 1 if you want to use connection string to connect to the Service Bus.\n" + + "Enter 2 if you want to use user assigned managed identity to connect to the Service Bus.", + }) + if err != nil { + return err + } + + if _authType != "1" && _authType != "2" { + i.console.Message(ctx, "Invalid authentication type. Please enter 0 or 1.") + continue azureDepPrompt + } + if _authType == "1" { + authType = scaffold.AuthType_PASSWORD + } else { + authType = scaffold.AuthType_TOKEN_CREDENTIAL + } + } + switch azureDep.(type) { case appdetect.AzureDepServiceBus: spec.AzureServiceBus = &scaffold.AzureDepServiceBus{ - Name: azureDepName, - Queues: azureDep.(appdetect.AzureDepServiceBus).Queues, + Name: azureDepName, + Queues: azureDep.(appdetect.AzureDepServiceBus).Queues, + AuthUsingConnectionString: authType == scaffold.AuthType_PASSWORD, + AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, } break azureDepPrompt } diff --git a/cli/azd/internal/repository/infra_prompt.go b/cli/azd/internal/repository/infra_prompt.go deleted file mode 100644 index ed1ac1d0e77..00000000000 --- a/cli/azd/internal/repository/infra_prompt.go +++ /dev/null @@ -1,38 +0,0 @@ -package repository - -import ( - "github.com/azure/azure-dev/cli/azd/internal/appdetect" - "github.com/azure/azure-dev/cli/azd/internal/scaffold" -) - -type infraPrompt interface { - Type() string - Properties() map[string]string - Apply(spec *scaffold.InfraSpec) -} - -type serviceBusPrompt struct { - name string - queues []string - topicAndSubscriptions []string -} - -func (s *serviceBusPrompt) Type() string { - return appdetect.AzureDepServiceBus{}.ResourceDisplay() -} - -func (s *serviceBusPrompt) Properties() map[string]string { - return map[string]string{ - "name": "Service Bus namespace name", - "queues": "Comma-separated list of queue names", - "topicAndSubscriptions": "Comma-separated list of topic names and their subscriptions, of format 'topicName:subscription1,subscription2,...'", - } -} - -func (s *serviceBusPrompt) Apply(spec *scaffold.InfraSpec) { - if spec.AzureServiceBus == nil { - spec.AzureServiceBus = &scaffold.AzureDepServiceBus{} - } - spec.AzureServiceBus.Name = s.name - spec.AzureServiceBus.Queues = s.queues -} diff --git a/cli/azd/internal/scaffold/spec.go b/cli/azd/internal/scaffold/spec.go index feac1585671..f9bb49751d9 100644 --- a/cli/azd/internal/scaffold/spec.go +++ b/cli/azd/internal/scaffold/spec.go @@ -40,11 +40,24 @@ type DatabaseCosmosMongo struct { } type AzureDepServiceBus struct { - Name string - Queues []string - TopicsAndSubscriptions map[string][]string + Name string + Queues []string + TopicsAndSubscriptions map[string][]string + AuthUsingConnectionString bool + AuthUsingManagedIdentity bool } +// AuthType defines different authentication types. +type AuthType int32 + +const ( + AUTH_TYPE_UNSPECIFIED AuthType = 0 + // Username and password, or key based authentication, or connection string + AuthType_PASSWORD AuthType = 1 + // Microsoft EntraID token credential + AuthType_TOKEN_CREDENTIAL AuthType = 2 +) + type ServiceSpec struct { Name string Port int diff --git a/cli/azd/resources/scaffold/templates/azure-service-bus.bicept b/cli/azd/resources/scaffold/templates/azure-service-bus.bicept index 01ed109bf55..1504934841f 100644 --- a/cli/azd/resources/scaffold/templates/azure-service-bus.bicept +++ b/cli/azd/resources/scaffold/templates/azure-service-bus.bicept @@ -1,6 +1,8 @@ {{define "azure-service-bus.bicep" -}} param serviceBusNamespaceName string +{{- if .AuthUsingConnectionString }} param keyVaultName string +{{end}} param location string param tags object = {} @@ -35,6 +37,8 @@ resource serviceBusQueue_{{ $index }} 'Microsoft.ServiceBus/namespaces/queues@20 } {{end}} +{{- if .AuthUsingConnectionString }} + resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { name: keyVaultName } @@ -46,8 +50,11 @@ resource serviceBusConnectionString 'Microsoft.KeyVault/vaults/secrets@2022-07-0 value: listKeys('${serviceBusNamespace.id}/AuthorizationRules/RootManageSharedAccessKey', serviceBusNamespace.apiVersion).primaryConnectionString } } +{{end}} output serviceBusNamespaceId string = serviceBusNamespace.id output serviceBusNamespaceApiVersion string = serviceBusNamespace.apiVersion +{{- if .AuthUsingConnectionString }} output serviceBusConnectionStringKey string = 'serviceBusConnectionString' +{{end}} {{ end}} \ No newline at end of file diff --git a/cli/azd/resources/scaffold/templates/host-containerapp.bicept b/cli/azd/resources/scaffold/templates/host-containerapp.bicept index e76d81652b2..715d0a0d9f1 100644 --- a/cli/azd/resources/scaffold/templates/host-containerapp.bicept +++ b/cli/azd/resources/scaffold/templates/host-containerapp.bicept @@ -23,10 +23,14 @@ param postgresDatabasePassword string param mysqlDatabaseId string param mysqlIdentityName string {{- end}} -{{- if .AzureServiceBus}} +{{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingConnectionString)}} @secure() param azureServiceBusConnectionString string {{- end}} +{{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingManagedIdentity)}} +@secure() +param azureServiceBusNamespace string +{{- end}} {{- if .DbRedis}} param redisName string {{- end}} @@ -156,7 +160,7 @@ resource app 'Microsoft.App/containerApps@2023-05-02-preview' = { value: postgresDatabasePassword } {{- end}} - {{- if .AzureServiceBus}} + {{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingConnectionString)}} { name: 'spring-cloud-azure-servicebus-connection-string' value: azureServiceBusConnectionString @@ -206,12 +210,30 @@ resource app 'Microsoft.App/containerApps@2023-05-02-preview' = { value: '5432' } {{- end}} - {{- if .AzureServiceBus}} + {{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingConnectionString)}} { name: 'SPRING_CLOUD_AZURE_SERVICEBUS_CONNECTION_STRING' secretRef: 'spring-cloud-azure-servicebus-connection-string' } {{- end}} + {{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingManagedIdentity)}} + { + name: 'SPRING_CLOUD_AZURE_SERVICEBUS_CONNECTION_STRING' + value: '' + } + { + name: 'SPRING_CLOUD_AZURE_SERVICEBUS_NAMESPACE' + value: azureServiceBusNamespace + } + { + name: 'SPRING_CLOUD_AZURE_SERVICEBUS_CREDENTIAL_MANAGEDIDENTITYENABLED' + value: 'true' + } + { + name: 'SPRING_CLOUD_AZURE_SERVICEBUS_CREDENTIAL_CLIENTID' + value: identity.properties.clientId + } + {{- end}} {{- if .Frontend}} {{- range $i, $e := .Frontend.Backends}} { @@ -316,6 +338,33 @@ resource appLinkToPostgres 'Microsoft.Resources/deploymentScripts@2023-08-01' = } {{- end}} +{{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingManagedIdentity) }} +resource servicebus 'Microsoft.ServiceBus/namespaces@2022-01-01-preview' existing = { + name: azureServiceBusNamespace +} + +resource serviceBusReceiverRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { + name: guid(servicebus.id, '4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0', identity.name) + scope: servicebus + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0') // Azure Service Bus Data Receiver + principalId: identity.properties.principalId + principalType: 'ServicePrincipal' + } +} + +resource serviceBusSenderRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { + name: guid(servicebus.id, '69a216fc-b8fb-44d8-bc22-1f3c2cd27a39', identity.name) + scope: servicebus + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '69a216fc-b8fb-44d8-bc22-1f3c2cd27a39') // Azure Service Bus Data Sender + principalId: identity.properties.principalId + principalType: 'ServicePrincipal' + } +} + +{{end}} + output defaultDomain string = containerAppsEnvironment.properties.defaultDomain output name string = app.name output uri string = 'https://${app.properties.configuration.ingress.fqdn}' diff --git a/cli/azd/resources/scaffold/templates/main.bicept b/cli/azd/resources/scaffold/templates/main.bicept index 3659af2d310..eb0d71eb9de 100644 --- a/cli/azd/resources/scaffold/templates/main.bicept +++ b/cli/azd/resources/scaffold/templates/main.bicept @@ -91,13 +91,14 @@ module appsEnv './shared/apps-env.bicep' = { } scope: rg } -{{- if (or (or (or .DbCosmosMongo .DbPostgres) .DbMySql) .AzureServiceBus)}} +{{- if (or (or (or .DbCosmosMongo .DbPostgres) .DbMySql) (and .AzureServiceBus .AzureServiceBus.AuthUsingConnectionString))}})))}} resource vault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { name: keyVault.outputs.name scope: rg } {{- end}} + {{- if .DbCosmosMongo}} module cosmosDb './app/db-cosmos-mongo.bicep' = { @@ -150,7 +151,9 @@ module serviceBus './app/azure-service-bus.bicep' = { serviceBusNamespaceName: '${abbrs.serviceBusNamespaces}${resourceToken}' location: location tags: tags + {{- if .AzureServiceBus.AuthUsingConnectionString}} keyVaultName: keyVault.outputs.name + {{end}} } scope: rg } @@ -187,9 +190,14 @@ module {{bicepName .Name}} './app/{{.Name}}.bicep' = { mysqlDatabaseId: mysqlDb.outputs.databaseId mysqlIdentityName: mysqlDb.outputs.identityName {{- end}} - {{- if .AzureServiceBus }} + {{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingConnectionString)}} azureServiceBusConnectionString: vault.getSecret(serviceBus.outputs.serviceBusConnectionStringKey) {{- end}} + {{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingManagedIdentity)}} + azureServiceBusNamespace: '${abbrs.serviceBusNamespaces}${resourceToken}' + {{- end}} + {{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingManagedIdentity)}} + {{- end}} {{- if (and .Frontend .Frontend.Backends)}} apiUrls: [ {{- range .Frontend.Backends}} From 23362f8181f2a3a6eb83ee3deca636436aff61c5 Mon Sep 17 00:00:00 2001 From: rujche Date: Thu, 10 Oct 2024 21:10:14 +0800 Subject: [PATCH 28/39] For PostgreSQL, support both password and passwordless. --- cli/azd/internal/repository/infra_confirm.go | 38 ++++++++++++++++--- cli/azd/internal/scaffold/spec.go | 16 +++++--- .../templates/host-containerapp.bicept | 28 +++++++------- .../resources/scaffold/templates/main.bicept | 10 +++-- 4 files changed, 64 insertions(+), 28 deletions(-) diff --git a/cli/azd/internal/repository/infra_confirm.go b/cli/azd/internal/repository/infra_confirm.go index 4495f29a3c9..924aa16adfe 100644 --- a/cli/azd/internal/repository/infra_confirm.go +++ b/cli/azd/internal/repository/infra_confirm.go @@ -72,21 +72,41 @@ func (i *Initializer) infraSpecFromDetect( } } + authType := scaffold.AuthType(0) + selection, err := i.console.Select(ctx, input.ConsoleOptions{ + Message: "Input the authentication type you want for database:", + Options: []string{ + "Use user assigned managed identity", + "Use username and password", + }, + }) + if err != nil { + return scaffold.InfraSpec{}, err + } + switch selection { + case 0: + authType = scaffold.AuthType_TOKEN_CREDENTIAL + case 1: + authType = scaffold.AuthType_PASSWORD + default: + panic("unhandled selection") + } + switch database { case appdetect.DbMongo: spec.DbCosmosMongo = &scaffold.DatabaseCosmosMongo{ DatabaseName: dbName, } - break dbPrompt case appdetect.DbPostgres: if dbName == "" { i.console.Message(ctx, "Database name is required.") continue } - spec.DbPostgres = &scaffold.DatabasePostgres{ - DatabaseName: dbName, + DatabaseName: dbName, + AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, + AuthUsingUsernamePassword: authType == scaffold.AuthType_PASSWORD, } break dbPrompt case appdetect.DbMySql: @@ -95,7 +115,9 @@ func (i *Initializer) infraSpecFromDetect( continue } spec.DbMySql = &scaffold.DatabaseMySql{ - DatabaseName: dbName, + DatabaseName: dbName, + AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, + AuthUsingUsernamePassword: authType == scaffold.AuthType_PASSWORD, } break dbPrompt } @@ -145,11 +167,15 @@ func (i *Initializer) infraSpecFromDetect( } case appdetect.DbPostgres: serviceSpec.DbPostgres = &scaffold.DatabaseReference{ - DatabaseName: spec.DbPostgres.DatabaseName, + DatabaseName: spec.DbPostgres.DatabaseName, + AuthUsingManagedIdentity: spec.DbPostgres.AuthUsingManagedIdentity, + AuthUsingUsernamePassword: spec.DbPostgres.AuthUsingUsernamePassword, } case appdetect.DbMySql: serviceSpec.DbMySql = &scaffold.DatabaseReference{ - DatabaseName: spec.DbMySql.DatabaseName, + DatabaseName: spec.DbMySql.DatabaseName, + AuthUsingManagedIdentity: spec.DbPostgres.AuthUsingManagedIdentity, + AuthUsingUsernamePassword: spec.DbPostgres.AuthUsingUsernamePassword, } case appdetect.DbRedis: serviceSpec.DbRedis = &scaffold.DatabaseReference{ diff --git a/cli/azd/internal/scaffold/spec.go b/cli/azd/internal/scaffold/spec.go index f9bb49751d9..20a9f61b57c 100644 --- a/cli/azd/internal/scaffold/spec.go +++ b/cli/azd/internal/scaffold/spec.go @@ -26,13 +26,17 @@ type Parameter struct { } type DatabasePostgres struct { - DatabaseUser string - DatabaseName string + DatabaseUser string + DatabaseName string + AuthUsingManagedIdentity bool + AuthUsingUsernamePassword bool } type DatabaseMySql struct { - DatabaseUser string - DatabaseName string + DatabaseUser string + DatabaseName string + AuthUsingManagedIdentity bool + AuthUsingUsernamePassword bool } type DatabaseCosmosMongo struct { @@ -91,7 +95,9 @@ type ServiceReference struct { } type DatabaseReference struct { - DatabaseName string + DatabaseName string + AuthUsingManagedIdentity bool + AuthUsingUsernamePassword bool } func containerAppExistsParameter(serviceName string) Parameter { diff --git a/cli/azd/resources/scaffold/templates/host-containerapp.bicept b/cli/azd/resources/scaffold/templates/host-containerapp.bicept index 715d0a0d9f1..3a0c1655cd8 100644 --- a/cli/azd/resources/scaffold/templates/host-containerapp.bicept +++ b/cli/azd/resources/scaffold/templates/host-containerapp.bicept @@ -11,11 +11,13 @@ param applicationInsightsName string @secure() param cosmosDbConnectionString string {{- end}} -{{- if .DbPostgres}} +{{- if (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity)}} +param postgresDatabaseId string +{{- end}} +{{- if (and .DbPostgres .DbPostgres.AuthUsingUsernamePassword)}} param postgresDatabaseHost string -param postgresDatabaseUser string param postgresDatabaseName string -param postgresDatabaseId string +param postgresDatabaseUser string @secure() param postgresDatabasePassword string {{- end}} @@ -154,7 +156,7 @@ resource app 'Microsoft.App/containerApps@2023-05-02-preview' = { value: cosmosDbConnectionString } {{- end}} - {{- if .DbPostgres}} + {{- if (and .DbPostgres .DbPostgres.AuthUsingUsernamePassword)}} { name: 'postgres-db-pass' value: postgresDatabasePassword @@ -188,26 +190,26 @@ resource app 'Microsoft.App/containerApps@2023-05-02-preview' = { secretRef: 'azure-cosmos-connection-string' } {{- end}} - {{- if .DbPostgres}} + {{- if (and .DbPostgres .DbPostgres.AuthUsingUsernamePassword)}} { name: 'POSTGRES_HOST' value: postgresDatabaseHost } { - name: 'POSTGRES_USERNAME' - value: postgresDatabaseUser + name: 'POSTGRES_PORT' + value: '5432' } { name: 'POSTGRES_DATABASE' value: postgresDatabaseName } { - name: 'POSTGRES_PASSWORD' - secretRef: 'postgres-db-pass' + name: 'POSTGRES_USERNAME' + value: postgresDatabaseUser } { - name: 'POSTGRES_PORT' - value: '5432' + name: 'POSTGRES_PASSWORD' + secretRef: 'postgres-db-pass' } {{- end}} {{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingConnectionString)}} @@ -275,7 +277,7 @@ resource app 'Microsoft.App/containerApps@2023-05-02-preview' = { } } } -{{- if (or .DbMySql .DbPostgres)}} +{{- if (or .DbMySql (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity))}} resource linkerCreatorIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { name: '${name}-linker-creator-identity' @@ -315,7 +317,7 @@ resource appLinkToMySql 'Microsoft.Resources/deploymentScripts@2023-08-01' = { } } {{- end}} -{{- if .DbPostgres}} +{{- if (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity)}} resource appLinkToPostgres 'Microsoft.Resources/deploymentScripts@2023-08-01' = { dependsOn: [ linkerCreatorRole ] diff --git a/cli/azd/resources/scaffold/templates/main.bicept b/cli/azd/resources/scaffold/templates/main.bicept index eb0d71eb9de..dd9089ee532 100644 --- a/cli/azd/resources/scaffold/templates/main.bicept +++ b/cli/azd/resources/scaffold/templates/main.bicept @@ -91,7 +91,7 @@ module appsEnv './shared/apps-env.bicep' = { } scope: rg } -{{- if (or (or (or .DbCosmosMongo .DbPostgres) .DbMySql) (and .AzureServiceBus .AzureServiceBus.AuthUsingConnectionString))}})))}} +{{- if (or (or (or .DbCosmosMongo .DbPostgres) .DbMySql) (and .AzureServiceBus .AzureServiceBus.AuthUsingConnectionString))}} resource vault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { name: keyVault.outputs.name @@ -179,11 +179,13 @@ module {{bicepName .Name}} './app/{{.Name}}.bicep' = { {{- if .DbCosmosMongo}} cosmosDbConnectionString: vault.getSecret(cosmosDb.outputs.connectionStringKey) {{- end}} - {{- if .DbPostgres}} - postgresDatabaseName: postgresDb.outputs.databaseName + {{- if (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity)}} + postgresDatabaseId: postgresDb.outputs.databaseId + {{- end}} + {{- if (and .DbPostgres .DbPostgres.AuthUsingUsernamePassword)}} postgresDatabaseHost: postgresDb.outputs.databaseHost + postgresDatabaseName: postgresDb.outputs.databaseName postgresDatabaseUser: postgresDb.outputs.databaseUser - postgresDatabaseId: postgresDb.outputs.databaseId postgresDatabasePassword: vault.getSecret(postgresDb.outputs.databaseConnectionKey) {{- end}} {{- if .DbMySql}} From 193f054d3484f7ad13a4b4153b5895f56c35a603 Mon Sep 17 00:00:00 2001 From: rujche Date: Fri, 11 Oct 2024 17:02:53 +0800 Subject: [PATCH 29/39] For MySQL, support both password and passwordless. --- cli/azd/internal/repository/infra_confirm.go | 4 +- .../scaffold/templates/db-mysql.bicept | 4 ++ .../templates/host-containerapp.bicept | 58 +++++++++++++++---- .../resources/scaffold/templates/main.bicept | 8 ++- 4 files changed, 59 insertions(+), 15 deletions(-) diff --git a/cli/azd/internal/repository/infra_confirm.go b/cli/azd/internal/repository/infra_confirm.go index 924aa16adfe..9b9ce62b775 100644 --- a/cli/azd/internal/repository/infra_confirm.go +++ b/cli/azd/internal/repository/infra_confirm.go @@ -174,8 +174,8 @@ func (i *Initializer) infraSpecFromDetect( case appdetect.DbMySql: serviceSpec.DbMySql = &scaffold.DatabaseReference{ DatabaseName: spec.DbMySql.DatabaseName, - AuthUsingManagedIdentity: spec.DbPostgres.AuthUsingManagedIdentity, - AuthUsingUsernamePassword: spec.DbPostgres.AuthUsingUsernamePassword, + AuthUsingManagedIdentity: spec.DbMySql.AuthUsingManagedIdentity, + AuthUsingUsernamePassword: spec.DbMySql.AuthUsingUsernamePassword, } case appdetect.DbRedis: serviceSpec.DbRedis = &scaffold.DatabaseReference{ diff --git a/cli/azd/resources/scaffold/templates/db-mysql.bicept b/cli/azd/resources/scaffold/templates/db-mysql.bicept index caac47b50db..dcd9dad0618 100644 --- a/cli/azd/resources/scaffold/templates/db-mysql.bicept +++ b/cli/azd/resources/scaffold/templates/db-mysql.bicept @@ -81,4 +81,8 @@ resource dbPasswordKey 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { output databaseId string = database.id output identityName string = userAssignedIdentity.name +output databaseHost string = mysqlServer.properties.fullyQualifiedDomainName +output databaseName string = databaseName +output databaseUser string = databaseUser +output databaseConnectionKey string = 'databasePassword' {{ end}} diff --git a/cli/azd/resources/scaffold/templates/host-containerapp.bicept b/cli/azd/resources/scaffold/templates/host-containerapp.bicept index 3a0c1655cd8..dcab264572a 100644 --- a/cli/azd/resources/scaffold/templates/host-containerapp.bicept +++ b/cli/azd/resources/scaffold/templates/host-containerapp.bicept @@ -21,10 +21,17 @@ param postgresDatabaseUser string @secure() param postgresDatabasePassword string {{- end}} -{{- if .DbMySql}} +{{- if (and .DbMySql .DbMySql.AuthUsingManagedIdentity)}} param mysqlDatabaseId string param mysqlIdentityName string {{- end}} +{{- if (and .DbMySql .DbMySql.AuthUsingUsernamePassword)}} +param mysqlDatabaseHost string +param mysqlDatabaseName string +param mysqlDatabaseUser string +@secure() +param mysqlDatabasePassword string +{{- end}} {{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingConnectionString)}} @secure() param azureServiceBusConnectionString string @@ -162,6 +169,12 @@ resource app 'Microsoft.App/containerApps@2023-05-02-preview' = { value: postgresDatabasePassword } {{- end}} + {{- if (and .DbMySql .DbMySql.AuthUsingUsernamePassword)}} + { + name: 'mysql-db-pass' + value: mysqlDatabasePassword + } + {{- end}} {{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingConnectionString)}} { name: 'spring-cloud-azure-servicebus-connection-string' @@ -212,6 +225,28 @@ resource app 'Microsoft.App/containerApps@2023-05-02-preview' = { secretRef: 'postgres-db-pass' } {{- end}} + {{- if (and .DbMySql .DbMySql.AuthUsingUsernamePassword)}} + { + name: 'MYSQL_HOST' + value: mysqlDatabaseHost + } + { + name: 'MYSQL_PORT' + value: '3306' + } + { + name: 'MYSQL_DATABASE' + value: mysqlDatabaseName + } + { + name: 'MYSQL_USERNAME' + value: mysqlDatabaseUser + } + { + name: 'MYSQL_PASSWORD' + secretRef: 'mysql-db-pass' + } + {{- end}} {{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingConnectionString)}} { name: 'SPRING_CLOUD_AZURE_SERVICEBUS_CONNECTION_STRING' @@ -277,7 +312,7 @@ resource app 'Microsoft.App/containerApps@2023-05-02-preview' = { } } } -{{- if (or .DbMySql (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity))}} +{{- if (or (and .DbMySql .DbMySql.AuthUsingManagedIdentity) (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity))}} resource linkerCreatorIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { name: '${name}-linker-creator-identity' @@ -295,11 +330,11 @@ resource linkerCreatorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' } } {{- end}} -{{- if .DbMySql}} +{{- if (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity)}} -resource appLinkToMySql 'Microsoft.Resources/deploymentScripts@2023-08-01' = { +resource appLinkToPostgres 'Microsoft.Resources/deploymentScripts@2023-08-01' = { dependsOn: [ linkerCreatorRole ] - name: '${name}-link-to-mysql' + name: '${name}-link-to-postgres' location: location kind: 'AzureCLI' identity: { @@ -311,17 +346,17 @@ resource appLinkToMySql 'Microsoft.Resources/deploymentScripts@2023-08-01' = { properties: { azCliVersion: '2.63.0' timeout: 'PT10M' - scriptContent: 'apk update; apk add g++; apk add unixodbc-dev; az extension add --name containerapp; az extension add --name serviceconnector-passwordless --upgrade; az containerapp connection create mysql-flexible --connection appLinkToMySql --source-id ${app.id} --target-id ${mysqlDatabaseId} --client-type springBoot --user-identity client-id=${identity.properties.clientId} subs-id=${subscription().subscriptionId} user-object-id=${linkerCreatorIdentity.properties.principalId} mysql-identity-id=${mysqlIdentityName} -c main --yes;' + scriptContent: 'apk update; apk add g++; apk add unixodbc-dev; az extension add --name containerapp; az extension add --name serviceconnector-passwordless --upgrade; az containerapp connection create postgres-flexible --connection appLinkToPostgres --source-id ${app.id} --target-id ${postgresDatabaseId} --client-type springBoot --user-identity client-id=${identity.properties.clientId} subs-id=${subscription().subscriptionId} user-object-id=${linkerCreatorIdentity.properties.principalId} -c main --yes; az tag create --resource-id ${app.id} --tags azd-service-name={{.Name}} ' cleanupPreference: 'OnSuccess' retentionInterval: 'P1D' } } {{- end}} -{{- if (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity)}} +{{- if (and .DbMySql .DbMySql.AuthUsingManagedIdentity)}} -resource appLinkToPostgres 'Microsoft.Resources/deploymentScripts@2023-08-01' = { +resource appLinkToMySql 'Microsoft.Resources/deploymentScripts@2023-08-01' = { dependsOn: [ linkerCreatorRole ] - name: '${name}-link-to-postgres' + name: '${name}-link-to-mysql' location: location kind: 'AzureCLI' identity: { @@ -333,14 +368,14 @@ resource appLinkToPostgres 'Microsoft.Resources/deploymentScripts@2023-08-01' = properties: { azCliVersion: '2.63.0' timeout: 'PT10M' - scriptContent: 'apk update; apk add g++; apk add unixodbc-dev; az extension add --name containerapp; az extension add --name serviceconnector-passwordless --upgrade; az containerapp connection create postgres-flexible --connection appLinkToPostgres --source-id ${app.id} --target-id ${postgresDatabaseId} --client-type springBoot --user-identity client-id=${identity.properties.clientId} subs-id=${subscription().subscriptionId} user-object-id=${linkerCreatorIdentity.properties.principalId} -c main --yes; az tag create --resource-id ${app.id} --tags azd-service-name={{.Name}} ' + scriptContent: 'apk update; apk add g++; apk add unixodbc-dev; az extension add --name containerapp; az extension add --name serviceconnector-passwordless --upgrade; az containerapp connection create mysql-flexible --connection appLinkToMySql --source-id ${app.id} --target-id ${mysqlDatabaseId} --client-type springBoot --user-identity client-id=${identity.properties.clientId} subs-id=${subscription().subscriptionId} user-object-id=${linkerCreatorIdentity.properties.principalId} mysql-identity-id=${mysqlIdentityName} -c main --yes;' cleanupPreference: 'OnSuccess' retentionInterval: 'P1D' } } {{- end}} - {{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingManagedIdentity) }} + resource servicebus 'Microsoft.ServiceBus/namespaces@2022-01-01-preview' existing = { name: azureServiceBusNamespace } @@ -364,7 +399,6 @@ resource serviceBusSenderRoleAssignment 'Microsoft.Authorization/roleAssignments principalType: 'ServicePrincipal' } } - {{end}} output defaultDomain string = containerAppsEnvironment.properties.defaultDomain diff --git a/cli/azd/resources/scaffold/templates/main.bicept b/cli/azd/resources/scaffold/templates/main.bicept index dd9089ee532..3d6f0cc2099 100644 --- a/cli/azd/resources/scaffold/templates/main.bicept +++ b/cli/azd/resources/scaffold/templates/main.bicept @@ -188,10 +188,16 @@ module {{bicepName .Name}} './app/{{.Name}}.bicep' = { postgresDatabaseUser: postgresDb.outputs.databaseUser postgresDatabasePassword: vault.getSecret(postgresDb.outputs.databaseConnectionKey) {{- end}} - {{- if .DbMySql}} + {{- if (and .DbMySql .DbMySql.AuthUsingManagedIdentity)}} mysqlDatabaseId: mysqlDb.outputs.databaseId mysqlIdentityName: mysqlDb.outputs.identityName {{- end}} + {{- if (and .DbMySql .DbMySql.AuthUsingUsernamePassword)}} + mysqlDatabaseHost: mysqlDb.outputs.databaseHost + mysqlDatabaseName: mysqlDb.outputs.databaseName + mysqlDatabaseUser: mysqlDb.outputs.databaseUser + mysqlDatabasePassword: vault.getSecret(mysqlDb.outputs.databaseConnectionKey) + {{- end}} {{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingConnectionString)}} azureServiceBusConnectionString: vault.getSecret(serviceBus.outputs.serviceBusConnectionStringKey) {{- end}} From 48cbeb562f7c98e4905b5868f9c27fec2fc5a105 Mon Sep 17 00:00:00 2001 From: rujche Date: Fri, 11 Oct 2024 17:04:26 +0800 Subject: [PATCH 30/39] Remove logic of adding tag after creating service connector. Because related bug has been fixed. --- cli/azd/resources/scaffold/templates/host-containerapp.bicept | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/azd/resources/scaffold/templates/host-containerapp.bicept b/cli/azd/resources/scaffold/templates/host-containerapp.bicept index dcab264572a..25af91147f5 100644 --- a/cli/azd/resources/scaffold/templates/host-containerapp.bicept +++ b/cli/azd/resources/scaffold/templates/host-containerapp.bicept @@ -346,7 +346,7 @@ resource appLinkToPostgres 'Microsoft.Resources/deploymentScripts@2023-08-01' = properties: { azCliVersion: '2.63.0' timeout: 'PT10M' - scriptContent: 'apk update; apk add g++; apk add unixodbc-dev; az extension add --name containerapp; az extension add --name serviceconnector-passwordless --upgrade; az containerapp connection create postgres-flexible --connection appLinkToPostgres --source-id ${app.id} --target-id ${postgresDatabaseId} --client-type springBoot --user-identity client-id=${identity.properties.clientId} subs-id=${subscription().subscriptionId} user-object-id=${linkerCreatorIdentity.properties.principalId} -c main --yes; az tag create --resource-id ${app.id} --tags azd-service-name={{.Name}} ' + scriptContent: 'apk update; apk add g++; apk add unixodbc-dev; az extension add --name containerapp; az extension add --name serviceconnector-passwordless --upgrade; az containerapp connection create postgres-flexible --connection appLinkToPostgres --source-id ${app.id} --target-id ${postgresDatabaseId} --client-type springBoot --user-identity client-id=${identity.properties.clientId} subs-id=${subscription().subscriptionId} user-object-id=${linkerCreatorIdentity.properties.principalId} -c main --yes;' cleanupPreference: 'OnSuccess' retentionInterval: 'P1D' } From 9f4fe2707919512dd9a42f667502d32a1b65a5df Mon Sep 17 00:00:00 2001 From: rujche Date: Fri, 11 Oct 2024 23:33:57 +0800 Subject: [PATCH 31/39] Fix bug: create service connector only work for the first time run of "azd up". --- cli/azd/resources/scaffold/templates/host-containerapp.bicept | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cli/azd/resources/scaffold/templates/host-containerapp.bicept b/cli/azd/resources/scaffold/templates/host-containerapp.bicept index 25af91147f5..1085bce491e 100644 --- a/cli/azd/resources/scaffold/templates/host-containerapp.bicept +++ b/cli/azd/resources/scaffold/templates/host-containerapp.bicept @@ -52,6 +52,7 @@ param allowedOrigins array param exists bool @secure() param appDefinition object +param currentTime string = utcNow() var appSettingsArray = filter(array(appDefinition.settings), i => i.name != '') var secrets = map(filter(appSettingsArray, i => i.?secret != null), i => { @@ -346,6 +347,7 @@ resource appLinkToPostgres 'Microsoft.Resources/deploymentScripts@2023-08-01' = properties: { azCliVersion: '2.63.0' timeout: 'PT10M' + forceUpdateTag: currentTime scriptContent: 'apk update; apk add g++; apk add unixodbc-dev; az extension add --name containerapp; az extension add --name serviceconnector-passwordless --upgrade; az containerapp connection create postgres-flexible --connection appLinkToPostgres --source-id ${app.id} --target-id ${postgresDatabaseId} --client-type springBoot --user-identity client-id=${identity.properties.clientId} subs-id=${subscription().subscriptionId} user-object-id=${linkerCreatorIdentity.properties.principalId} -c main --yes;' cleanupPreference: 'OnSuccess' retentionInterval: 'P1D' @@ -368,6 +370,7 @@ resource appLinkToMySql 'Microsoft.Resources/deploymentScripts@2023-08-01' = { properties: { azCliVersion: '2.63.0' timeout: 'PT10M' + forceUpdateTag: currentTime scriptContent: 'apk update; apk add g++; apk add unixodbc-dev; az extension add --name containerapp; az extension add --name serviceconnector-passwordless --upgrade; az containerapp connection create mysql-flexible --connection appLinkToMySql --source-id ${app.id} --target-id ${mysqlDatabaseId} --client-type springBoot --user-identity client-id=${identity.properties.clientId} subs-id=${subscription().subscriptionId} user-object-id=${linkerCreatorIdentity.properties.principalId} mysql-identity-id=${mysqlIdentityName} -c main --yes;' cleanupPreference: 'OnSuccess' retentionInterval: 'P1D' From 05944125d89c4e4beaaa85abd368ce4fb91a00e5 Mon Sep 17 00:00:00 2001 From: rujche Date: Sat, 12 Oct 2024 15:02:40 +0800 Subject: [PATCH 32/39] Add new feature: analyze project to add Mongo DB. --- cli/azd/internal/appdetect/appdetect.go | 2 ++ cli/azd/internal/appdetect/javaanalyze/project_analyzer_java.go | 1 + 2 files changed, 3 insertions(+) diff --git a/cli/azd/internal/appdetect/appdetect.go b/cli/azd/internal/appdetect/appdetect.go index 9ec2a63e05d..88bc4286f45 100644 --- a/cli/azd/internal/appdetect/appdetect.go +++ b/cli/azd/internal/appdetect/appdetect.go @@ -361,6 +361,8 @@ func enrichFromJavaProject(azureYaml javaanalyze.AzureYaml, project *Project) { project.DatabaseDeps = append(project.DatabaseDeps, DbMySql) } else if resource.GetType() == "PostgreSQL" { project.DatabaseDeps = append(project.DatabaseDeps, DbPostgres) + } else if resource.GetType() == "MongoDB" { + project.DatabaseDeps = append(project.DatabaseDeps, DbMongo) } else if resource.GetType() == "SQL Server" { project.DatabaseDeps = append(project.DatabaseDeps, DbSqlServer) } else if resource.GetType() == "Redis" { diff --git a/cli/azd/internal/appdetect/javaanalyze/project_analyzer_java.go b/cli/azd/internal/appdetect/javaanalyze/project_analyzer_java.go index fe8abae659f..552daa69fc3 100644 --- a/cli/azd/internal/appdetect/javaanalyze/project_analyzer_java.go +++ b/cli/azd/internal/appdetect/javaanalyze/project_analyzer_java.go @@ -13,6 +13,7 @@ func Analyze(path string) []AzureYaml { &ruleService{}, &ruleMysql{}, &rulePostgresql{}, + &ruleMongo{}, &ruleStorage{}, &ruleServiceBusScsb{}, } From c45382a61f13f7bb2ed796f70375113ea23a06cc Mon Sep 17 00:00:00 2001 From: rujche Date: Sat, 12 Oct 2024 15:02:58 +0800 Subject: [PATCH 33/39] Delete unused content in main.bicept. --- cli/azd/resources/scaffold/templates/main.bicept | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cli/azd/resources/scaffold/templates/main.bicept b/cli/azd/resources/scaffold/templates/main.bicept index 3d6f0cc2099..9e1e149c126 100644 --- a/cli/azd/resources/scaffold/templates/main.bicept +++ b/cli/azd/resources/scaffold/templates/main.bicept @@ -228,8 +228,4 @@ module {{bicepName .Name}} './app/{{.Name}}.bicep' = { output AZURE_CONTAINER_REGISTRY_ENDPOINT string = registry.outputs.loginServer output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.endpoint -output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.connectionString -{{- range .Services}} -output {{bicepName .Name}}_uri string = {{bicepName .Name}}.outputs.uri -{{- end}} {{ end}} From babf604644a2b752eb49230185e526a279de9a2a Mon Sep 17 00:00:00 2001 From: rujche Date: Mon, 14 Oct 2024 09:42:14 +0800 Subject: [PATCH 34/39] Fix bug: Get auth type should only be required for MySQL and PostgreSQL. --- cli/azd/internal/repository/infra_confirm.go | 51 ++++++++++++-------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/cli/azd/internal/repository/infra_confirm.go b/cli/azd/internal/repository/infra_confirm.go index 9b9ce62b775..10c5b21ad5b 100644 --- a/cli/azd/internal/repository/infra_confirm.go +++ b/cli/azd/internal/repository/infra_confirm.go @@ -72,26 +72,6 @@ func (i *Initializer) infraSpecFromDetect( } } - authType := scaffold.AuthType(0) - selection, err := i.console.Select(ctx, input.ConsoleOptions{ - Message: "Input the authentication type you want for database:", - Options: []string{ - "Use user assigned managed identity", - "Use username and password", - }, - }) - if err != nil { - return scaffold.InfraSpec{}, err - } - switch selection { - case 0: - authType = scaffold.AuthType_TOKEN_CREDENTIAL - case 1: - authType = scaffold.AuthType_PASSWORD - default: - panic("unhandled selection") - } - switch database { case appdetect.DbMongo: spec.DbCosmosMongo = &scaffold.DatabaseCosmosMongo{ @@ -103,6 +83,10 @@ func (i *Initializer) infraSpecFromDetect( i.console.Message(ctx, "Database name is required.") continue } + authType, err := i.getAuthType(ctx) + if err != nil { + return scaffold.InfraSpec{}, err + } spec.DbPostgres = &scaffold.DatabasePostgres{ DatabaseName: dbName, AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, @@ -114,6 +98,10 @@ func (i *Initializer) infraSpecFromDetect( i.console.Message(ctx, "Database name is required.") continue } + authType, err := i.getAuthType(ctx) + if err != nil { + return scaffold.InfraSpec{}, err + } spec.DbMySql = &scaffold.DatabaseMySql{ DatabaseName: dbName, AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, @@ -249,6 +237,29 @@ func (i *Initializer) infraSpecFromDetect( return spec, nil } +func (i *Initializer) getAuthType(ctx context.Context) (scaffold.AuthType, error) { + authType := scaffold.AuthType(0) + selection, err := i.console.Select(ctx, input.ConsoleOptions{ + Message: "Input the authentication type you want:", + Options: []string{ + "Use user assigned managed identity", + "Use username and password", + }, + }) + if err != nil { + return authType, err + } + switch selection { + case 0: + authType = scaffold.AuthType_TOKEN_CREDENTIAL + case 1: + authType = scaffold.AuthType_PASSWORD + default: + panic("unhandled selection") + } + return authType, nil +} + func (i *Initializer) promptForAzureResource( ctx context.Context, azureDep appdetect.AzureDep, From a2a3a731b5db58c8bad222afc59e89c442f2e875 Mon Sep 17 00:00:00 2001 From: rujche Date: Tue, 15 Oct 2024 09:33:17 +0800 Subject: [PATCH 35/39] Make sure app work well after deployed to ACA no matter what value "server.port" is set in application.properties. --- cli/azd/resources/scaffold/templates/host-containerapp.bicept | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cli/azd/resources/scaffold/templates/host-containerapp.bicept b/cli/azd/resources/scaffold/templates/host-containerapp.bicept index 1085bce491e..9991fdea940 100644 --- a/cli/azd/resources/scaffold/templates/host-containerapp.bicept +++ b/cli/azd/resources/scaffold/templates/host-containerapp.bicept @@ -285,6 +285,10 @@ resource app 'Microsoft.App/containerApps@2023-05-02-preview' = { name: 'PORT' value: '{{ .Port }}' } + { + name: 'SERVER_PORT' + value: '{{ .Port }}' + } {{- end}} ], env, From a83f7d75ae535fab61753a0938a617ecedcbebf9 Mon Sep 17 00:00:00 2001 From: rujche Date: Tue, 15 Oct 2024 14:26:05 +0800 Subject: [PATCH 36/39] Implement feature: detect port in Dockerfile. --- cli/azd/internal/repository/infra_confirm.go | 27 ++++++++++++++++++- .../internal/repository/infra_confirm_test.go | 24 +++++++++++++++++ .../testdata/Dockerfile/Dockerfile1 | 20 ++++++++++++++ .../testdata/Dockerfile/Dockerfile2 | 22 +++++++++++++++ .../testdata/Dockerfile/Dockerfile3 | 21 +++++++++++++++ 5 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 cli/azd/internal/repository/testdata/Dockerfile/Dockerfile1 create mode 100644 cli/azd/internal/repository/testdata/Dockerfile/Dockerfile2 create mode 100644 cli/azd/internal/repository/testdata/Dockerfile/Dockerfile3 diff --git a/cli/azd/internal/repository/infra_confirm.go b/cli/azd/internal/repository/infra_confirm.go index 10c5b21ad5b..d1b6d2ce7c0 100644 --- a/cli/azd/internal/repository/infra_confirm.go +++ b/cli/azd/internal/repository/infra_confirm.go @@ -1,8 +1,10 @@ package repository import ( + "bufio" "context" "fmt" + "os" "path/filepath" "regexp" "strconv" @@ -130,10 +132,11 @@ func (i *Initializer) infraSpecFromDetect( if svc.Docker == nil || svc.Docker.Path == "" { // default builder always specifies port 80 serviceSpec.Port = 80 - if svc.Language == appdetect.Java { serviceSpec.Port = 8080 } + } else { + serviceSpec.Port = i.detectPortInDockerfile(svc.Docker.Path) } for _, framework := range svc.Dependencies { @@ -260,6 +263,28 @@ func (i *Initializer) getAuthType(ctx context.Context) (scaffold.AuthType, error return authType, nil } +func (i *Initializer) detectPortInDockerfile( + filePath string) int { + file, err := os.Open(filePath) + if err != nil { + return -1 + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "EXPOSE") { + var port int + _, err := fmt.Sscanf(line, "EXPOSE %d", &port) + if err == nil { + return port + } + } + } + return -1 +} + func (i *Initializer) promptForAzureResource( ctx context.Context, azureDep appdetect.AzureDep, diff --git a/cli/azd/internal/repository/infra_confirm_test.go b/cli/azd/internal/repository/infra_confirm_test.go index 1cd3a28664c..21873df7c7d 100644 --- a/cli/azd/internal/repository/infra_confirm_test.go +++ b/cli/azd/internal/repository/infra_confirm_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "path/filepath" "strings" "testing" @@ -225,3 +226,26 @@ func TestInitializer_infraSpecFromDetect(t *testing.T) { }) } } + +func TestDetectPortInDockerfile(t *testing.T) { + i := &Initializer{ + console: input.NewConsole( + false, + false, + input.Writers{Output: os.Stdout}, + input.ConsoleHandles{ + Stderr: os.Stderr, + Stdin: os.Stdin, + Stdout: os.Stdout, + }, + nil, + nil), + } + var port int + port = i.detectPortInDockerfile(filepath.Join("testdata", "Dockerfile", "Dockerfile1")) + require.Equal(t, 80, port) + port = i.detectPortInDockerfile(filepath.Join("testdata", "Dockerfile", "Dockerfile2")) + require.Equal(t, 3100, port) + port = i.detectPortInDockerfile(filepath.Join("testdata", "Dockerfile", "Dockerfile3")) + require.Equal(t, -1, port) +} diff --git a/cli/azd/internal/repository/testdata/Dockerfile/Dockerfile1 b/cli/azd/internal/repository/testdata/Dockerfile/Dockerfile1 new file mode 100644 index 00000000000..0b10c650d8d --- /dev/null +++ b/cli/azd/internal/repository/testdata/Dockerfile/Dockerfile1 @@ -0,0 +1,20 @@ +FROM node:20-alpine AS build + +# make the 'app' folder the current working directory +WORKDIR /app + +COPY . . + +# install project dependencies +RUN npm ci +RUN npm run build + +FROM nginx:alpine + +WORKDIR /usr/share/nginx/html +COPY --from=build /app/dist . +COPY --from=build /app/nginx/nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["/bin/sh", "-c", "sed -i \"s|http://localhost:3100|${API_BASE_URL}|g\" -i ./**/*.js && nginx -g \"daemon off;\""] diff --git a/cli/azd/internal/repository/testdata/Dockerfile/Dockerfile2 b/cli/azd/internal/repository/testdata/Dockerfile/Dockerfile2 new file mode 100644 index 00000000000..c1925937d2d --- /dev/null +++ b/cli/azd/internal/repository/testdata/Dockerfile/Dockerfile2 @@ -0,0 +1,22 @@ +FROM mcr.microsoft.com/openjdk/jdk:17-mariner AS build + +WORKDIR /workspace/app +EXPOSE 3100 + +COPY mvnw . +COPY .mvn .mvn +COPY pom.xml . +COPY src src + +RUN chmod +x ./mvnw +RUN ./mvnw package -DskipTests +RUN mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar) + +FROM mcr.microsoft.com/openjdk/jdk:17-mariner + +ARG DEPENDENCY=/workspace/app/target/dependency +COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib +COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF +COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app + +ENTRYPOINT ["java","-noverify", "-XX:MaxRAMPercentage=70", "-XX:+UseParallelGC", "-XX:ActiveProcessorCount=2", "-cp","app:app/lib/*","com.microsoft.azure.simpletodo.SimpleTodoApplication"] \ No newline at end of file diff --git a/cli/azd/internal/repository/testdata/Dockerfile/Dockerfile3 b/cli/azd/internal/repository/testdata/Dockerfile/Dockerfile3 new file mode 100644 index 00000000000..1ecad8a32f2 --- /dev/null +++ b/cli/azd/internal/repository/testdata/Dockerfile/Dockerfile3 @@ -0,0 +1,21 @@ +FROM mcr.microsoft.com/openjdk/jdk:17-mariner AS build + +WORKDIR /workspace/app + +COPY mvnw . +COPY .mvn .mvn +COPY pom.xml . +COPY src src + +RUN chmod +x ./mvnw +RUN ./mvnw package -DskipTests +RUN mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar) + +FROM mcr.microsoft.com/openjdk/jdk:17-mariner + +ARG DEPENDENCY=/workspace/app/target/dependency +COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib +COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF +COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app + +ENTRYPOINT ["java","-noverify", "-XX:MaxRAMPercentage=70", "-XX:+UseParallelGC", "-XX:ActiveProcessorCount=2", "-cp","app:app/lib/*","com.microsoft.azure.simpletodo.SimpleTodoApplication"] \ No newline at end of file From de2e922389f53eda82345990e643843e96585960 Mon Sep 17 00:00:00 2001 From: rujche Date: Tue, 15 Oct 2024 17:26:38 +0800 Subject: [PATCH 37/39] Implement feature: detect redis by analyzing pom file. --- .../appdetect/javaanalyze/project_analyzer_java.go | 1 + cli/azd/internal/appdetect/javaanalyze/rule_redis.go | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/cli/azd/internal/appdetect/javaanalyze/project_analyzer_java.go b/cli/azd/internal/appdetect/javaanalyze/project_analyzer_java.go index 552daa69fc3..dd3fbe37665 100644 --- a/cli/azd/internal/appdetect/javaanalyze/project_analyzer_java.go +++ b/cli/azd/internal/appdetect/javaanalyze/project_analyzer_java.go @@ -14,6 +14,7 @@ func Analyze(path string) []AzureYaml { &ruleMysql{}, &rulePostgresql{}, &ruleMongo{}, + &ruleRedis{}, &ruleStorage{}, &ruleServiceBusScsb{}, } diff --git a/cli/azd/internal/appdetect/javaanalyze/rule_redis.go b/cli/azd/internal/appdetect/javaanalyze/rule_redis.go index 59ef290ac9b..7e87a57afa8 100644 --- a/cli/azd/internal/appdetect/javaanalyze/rule_redis.go +++ b/cli/azd/internal/appdetect/javaanalyze/rule_redis.go @@ -4,6 +4,16 @@ type ruleRedis struct { } func (r *ruleRedis) match(javaProject *javaProject) bool { + if javaProject.mavenProject.Dependencies != nil { + for _, dep := range javaProject.mavenProject.Dependencies { + if dep.GroupId == "org.springframework.boot" && dep.ArtifactId == "spring-boot-starter-data-redis" { + return true + } + if dep.GroupId == "org.springframework.boot" && dep.ArtifactId == "spring-boot-starter-data-redis-reactive" { + return true + } + } + } return false } From a482496f555f75c1e6bdefe98b551871a9274e4b Mon Sep 17 00:00:00 2001 From: rujche Date: Tue, 15 Oct 2024 17:56:11 +0800 Subject: [PATCH 38/39] Detect Mongo DB by dependency spring-boot-starter-data-mongodb-reactive just like spring-boot-starter-data-mongodb --- cli/azd/internal/appdetect/javaanalyze/rule_mongo.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cli/azd/internal/appdetect/javaanalyze/rule_mongo.go b/cli/azd/internal/appdetect/javaanalyze/rule_mongo.go index 5ca181970a6..74e282bde2f 100644 --- a/cli/azd/internal/appdetect/javaanalyze/rule_mongo.go +++ b/cli/azd/internal/appdetect/javaanalyze/rule_mongo.go @@ -9,6 +9,9 @@ func (mr *ruleMongo) match(javaProject *javaProject) bool { if dep.GroupId == "org.springframework.boot" && dep.ArtifactId == "spring-boot-starter-data-mongodb" { return true } + if dep.GroupId == "org.springframework.boot" && dep.ArtifactId == "spring-boot-starter-data-mongodb-reactive" { + return true + } } } return false From 9c53e733dc4e6b11d85ad9d15cd39399c464b7c3 Mon Sep 17 00:00:00 2001 From: rujche Date: Wed, 16 Oct 2024 15:46:42 +0800 Subject: [PATCH 39/39] Support all kinds of properties file like application(-profile).yaml(or yaml, properties) --- .../javaanalyze/project_analyzer_spring.go | 60 +++++++++++++++---- .../project_analyzer_spring_test.go | 26 ++++++++ .../javaanalyze/rule_servicebus_scsb.go | 2 +- .../resources/application-mysql.properties | 7 +++ .../resources/application-postgres.properties | 6 ++ .../src/main/resources/application.properties | 29 +++++++++ .../src/main/resources/application.yml | 12 ++++ .../src/main/resources/application.properties | 29 +++++++++ .../src/main/resources/application.yaml | 12 ++++ 9 files changed, 170 insertions(+), 13 deletions(-) create mode 100644 cli/azd/internal/appdetect/javaanalyze/project_analyzer_spring_test.go create mode 100644 cli/azd/internal/appdetect/javaanalyze/testdata/project-four/src/main/resources/application-mysql.properties create mode 100644 cli/azd/internal/appdetect/javaanalyze/testdata/project-four/src/main/resources/application-postgres.properties create mode 100644 cli/azd/internal/appdetect/javaanalyze/testdata/project-four/src/main/resources/application.properties create mode 100644 cli/azd/internal/appdetect/javaanalyze/testdata/project-one/src/main/resources/application.yml create mode 100644 cli/azd/internal/appdetect/javaanalyze/testdata/project-three/src/main/resources/application.properties create mode 100644 cli/azd/internal/appdetect/javaanalyze/testdata/project-two/src/main/resources/application.yaml diff --git a/cli/azd/internal/appdetect/javaanalyze/project_analyzer_spring.go b/cli/azd/internal/appdetect/javaanalyze/project_analyzer_spring.go index eef378a9836..3370477b551 100644 --- a/cli/azd/internal/appdetect/javaanalyze/project_analyzer_spring.go +++ b/cli/azd/internal/appdetect/javaanalyze/project_analyzer_spring.go @@ -1,28 +1,44 @@ package javaanalyze import ( + "bufio" "fmt" "gopkg.in/yaml.v3" - "io/ioutil" "log" + "os" + "path/filepath" + "strings" ) type springProject struct { - applicationProperties map[string]interface{} + applicationProperties map[string]string } func analyzeSpringProject(projectPath string) springProject { return springProject{ - applicationProperties: findSpringApplicationProperties(projectPath), + applicationProperties: getProperties(projectPath), } } -func findSpringApplicationProperties(projectPath string) map[string]interface{} { - yamlFilePath := projectPath + "/src/main/resources/application.yml" - data, err := ioutil.ReadFile(yamlFilePath) +func getProperties(projectPath string) map[string]string { + result := make(map[string]string) + getPropertiesInPropertiesFile(filepath.Join(projectPath, "/src/main/resources/application.properties"), result) + getPropertiesInYamlFile(filepath.Join(projectPath, "/src/main/resources/application.yml"), result) + getPropertiesInYamlFile(filepath.Join(projectPath, "/src/main/resources/application.yaml"), result) + profile, profileSet := result["spring.profiles.active"] + if profileSet { + getPropertiesInPropertiesFile(filepath.Join(projectPath, "/src/main/resources/application-"+profile+".properties"), result) + getPropertiesInYamlFile(filepath.Join(projectPath, "/src/main/resources/application-"+profile+".yml"), result) + getPropertiesInYamlFile(filepath.Join(projectPath, "/src/main/resources/application-"+profile+".yaml"), result) + } + return result +} + +func getPropertiesInYamlFile(yamlFilePath string, result map[string]string) { + data, err := os.ReadFile(yamlFilePath) if err != nil { - log.Printf("failed to read spring application properties: %s", yamlFilePath) - return nil + // Ignore the error if file not exist. + return } // Parse the YAML into a yaml.Node @@ -32,14 +48,11 @@ func findSpringApplicationProperties(projectPath string) map[string]interface{} log.Fatalf("error unmarshalling YAML: %v", err) } - result := make(map[string]interface{}) parseYAML("", &root, result) - - return result } // Recursively parse the YAML and build dot-separated keys into a map -func parseYAML(prefix string, node *yaml.Node, result map[string]interface{}) { +func parseYAML(prefix string, node *yaml.Node, result map[string]string) { switch node.Kind { case yaml.DocumentNode: // Process each document's content @@ -77,3 +90,26 @@ func parseYAML(prefix string, node *yaml.Node, result map[string]interface{}) { // Handle other node types if necessary } } + +func getPropertiesInPropertiesFile(propertiesFilePath string, result map[string]string) { + file, err := os.Open(propertiesFilePath) + if err != nil { + // Ignore the error if file not exist. + 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 + } + } +} diff --git a/cli/azd/internal/appdetect/javaanalyze/project_analyzer_spring_test.go b/cli/azd/internal/appdetect/javaanalyze/project_analyzer_spring_test.go new file mode 100644 index 00000000000..833645e4b52 --- /dev/null +++ b/cli/azd/internal/appdetect/javaanalyze/project_analyzer_spring_test.go @@ -0,0 +1,26 @@ +package javaanalyze + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAnalyzeSpringProject(t *testing.T) { + var project = analyzeSpringProject(filepath.Join("testdata", "project-one")) + require.Equal(t, "", project.applicationProperties["not.exist"]) + require.Equal(t, "jdbc:h2:mem:testdb", project.applicationProperties["spring.datasource.url"]) + + project = analyzeSpringProject(filepath.Join("testdata", "project-two")) + require.Equal(t, "", project.applicationProperties["not.exist"]) + require.Equal(t, "jdbc:h2:mem:testdb", project.applicationProperties["spring.datasource.url"]) + + project = analyzeSpringProject(filepath.Join("testdata", "project-three")) + require.Equal(t, "", project.applicationProperties["not.exist"]) + require.Equal(t, "HTML", project.applicationProperties["spring.thymeleaf.mode"]) + + project = analyzeSpringProject(filepath.Join("testdata", "project-four")) + require.Equal(t, "", project.applicationProperties["not.exist"]) + require.Equal(t, "mysql", project.applicationProperties["database"]) +} diff --git a/cli/azd/internal/appdetect/javaanalyze/rule_servicebus_scsb.go b/cli/azd/internal/appdetect/javaanalyze/rule_servicebus_scsb.go index 4276527b56d..242d22560ff 100644 --- a/cli/azd/internal/appdetect/javaanalyze/rule_servicebus_scsb.go +++ b/cli/azd/internal/appdetect/javaanalyze/rule_servicebus_scsb.go @@ -22,7 +22,7 @@ func (r *ruleServiceBusScsb) match(javaProject *javaProject) bool { } // Function to find all properties that match the pattern `spring.cloud.stream.bindings..destination` -func findBindingDestinations(properties map[string]interface{}) map[string]string { +func findBindingDestinations(properties map[string]string) map[string]string { result := make(map[string]string) // Iterate through the properties map and look for matching keys diff --git a/cli/azd/internal/appdetect/javaanalyze/testdata/project-four/src/main/resources/application-mysql.properties b/cli/azd/internal/appdetect/javaanalyze/testdata/project-four/src/main/resources/application-mysql.properties new file mode 100644 index 00000000000..33ec21d3c95 --- /dev/null +++ b/cli/azd/internal/appdetect/javaanalyze/testdata/project-four/src/main/resources/application-mysql.properties @@ -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 diff --git a/cli/azd/internal/appdetect/javaanalyze/testdata/project-four/src/main/resources/application-postgres.properties b/cli/azd/internal/appdetect/javaanalyze/testdata/project-four/src/main/resources/application-postgres.properties new file mode 100644 index 00000000000..7d9676e3aad --- /dev/null +++ b/cli/azd/internal/appdetect/javaanalyze/testdata/project-four/src/main/resources/application-postgres.properties @@ -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 diff --git a/cli/azd/internal/appdetect/javaanalyze/testdata/project-four/src/main/resources/application.properties b/cli/azd/internal/appdetect/javaanalyze/testdata/project-four/src/main/resources/application.properties new file mode 100644 index 00000000000..59d5368e73c --- /dev/null +++ b/cli/azd/internal/appdetect/javaanalyze/testdata/project-four/src/main/resources/application.properties @@ -0,0 +1,29 @@ +# database init, supports mysql too +database=h2 +spring.sql.init.schema-locations=classpath*:db/${database}/schema.sql +spring.sql.init.data-locations=classpath*:db/${database}/data.sql + +# Web +spring.thymeleaf.mode=HTML + +# JPA +spring.jpa.hibernate.ddl-auto=none +spring.jpa.open-in-view=true + +# Internationalization +spring.messages.basename=messages/messages + +spring.profiles.active=mysql + +# Actuator +management.endpoints.web.exposure.include=* + +# Logging +logging.level.org.springframework=INFO +# logging.level.org.springframework.web=DEBUG +# logging.level.org.springframework.context.annotation=TRACE + +# Maximum time static resources should be cached +spring.web.resources.cache.cachecontrol.max-age=12h + +server.port=8081 diff --git a/cli/azd/internal/appdetect/javaanalyze/testdata/project-one/src/main/resources/application.yml b/cli/azd/internal/appdetect/javaanalyze/testdata/project-one/src/main/resources/application.yml new file mode 100644 index 00000000000..09d0cc057c5 --- /dev/null +++ b/cli/azd/internal/appdetect/javaanalyze/testdata/project-one/src/main/resources/application.yml @@ -0,0 +1,12 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + jackson: + date-format: com.microsoft.azure.simpletodo.configuration.RFC3339DateFormat + serialization: + write-dates-as-timestamps: false + jpa: + hibernate: + ddl-auto: update + show-sql: true + diff --git a/cli/azd/internal/appdetect/javaanalyze/testdata/project-three/src/main/resources/application.properties b/cli/azd/internal/appdetect/javaanalyze/testdata/project-three/src/main/resources/application.properties new file mode 100644 index 00000000000..59d5368e73c --- /dev/null +++ b/cli/azd/internal/appdetect/javaanalyze/testdata/project-three/src/main/resources/application.properties @@ -0,0 +1,29 @@ +# database init, supports mysql too +database=h2 +spring.sql.init.schema-locations=classpath*:db/${database}/schema.sql +spring.sql.init.data-locations=classpath*:db/${database}/data.sql + +# Web +spring.thymeleaf.mode=HTML + +# JPA +spring.jpa.hibernate.ddl-auto=none +spring.jpa.open-in-view=true + +# Internationalization +spring.messages.basename=messages/messages + +spring.profiles.active=mysql + +# Actuator +management.endpoints.web.exposure.include=* + +# Logging +logging.level.org.springframework=INFO +# logging.level.org.springframework.web=DEBUG +# logging.level.org.springframework.context.annotation=TRACE + +# Maximum time static resources should be cached +spring.web.resources.cache.cachecontrol.max-age=12h + +server.port=8081 diff --git a/cli/azd/internal/appdetect/javaanalyze/testdata/project-two/src/main/resources/application.yaml b/cli/azd/internal/appdetect/javaanalyze/testdata/project-two/src/main/resources/application.yaml new file mode 100644 index 00000000000..09d0cc057c5 --- /dev/null +++ b/cli/azd/internal/appdetect/javaanalyze/testdata/project-two/src/main/resources/application.yaml @@ -0,0 +1,12 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + jackson: + date-format: com.microsoft.azure.simpletodo.configuration.RFC3339DateFormat + serialization: + write-dates-as-timestamps: false + jpa: + hibernate: + ddl-auto: update + show-sql: true +