From 849de05c61c4b1f003be6f80c35eb5d71a77456e Mon Sep 17 00:00:00 2001 From: rujche Date: Fri, 6 Sep 2024 13:49:13 +0800 Subject: [PATCH 01/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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 + From 6c117ee6577ecb844dee42e6bb8b131049f451a1 Mon Sep 17 00:00:00 2001 From: Xiaolu Dai <31124698+saragluna@users.noreply.github.com> Date: Tue, 22 Oct 2024 17:14:07 +0800 Subject: [PATCH 40/92] Merge the java analyzer code from the main branch to sjad (#1) --- .vscode/cspell-github-user-aliases.txt | 1 + .vscode/cspell.global.yaml | 1 + .vscode/cspell.misc.yaml | 1 + cli/azd/.gitignore | 2 + cli/azd/.vscode/cspell-azd-dictionary.txt | 1 + cli/azd/CHANGELOG.md | 26 ++ cli/azd/cmd/root.go | 1 - cli/azd/internal/appdetect/appdetect.go | 51 ---- cli/azd/internal/appdetect/appdetect_test.go | 82 +++++ cli/azd/internal/appdetect/java.go | 289 +++++++++++++++++- .../appdetect/javaanalyze/azure_yaml.go | 91 ------ .../javaanalyze/project_analyzer_java.go | 41 --- .../javaanalyze/project_analyzer_maven.go | 92 ------ .../javaanalyze/project_analyzer_spring.go | 115 ------- .../project_analyzer_spring_test.go | 26 -- .../appdetect/javaanalyze/rule_engine.go | 17 -- .../appdetect/javaanalyze/rule_mongo.go | 30 -- .../appdetect/javaanalyze/rule_mysql.go | 27 -- .../appdetect/javaanalyze/rule_postgresql.go | 27 -- .../appdetect/javaanalyze/rule_redis.go | 25 -- .../appdetect/javaanalyze/rule_service.go | 17 -- .../javaanalyze/rule_servicebus_scsb.go | 62 ---- .../appdetect/javaanalyze/rule_storage.go | 39 --- .../java-multimodules/application/pom.xml | 69 +++++ .../application/DemoApplication.java | 27 ++ .../src/main/resources/application.properties | 1 + .../application/DemoApplicationTest.java | 23 ++ .../java-multimodules/library/pom.xml | 29 ++ .../multimodule/service/MyService.java | 19 ++ .../service/ServiceProperties.java | 20 ++ .../multimodule/service/MyServiceTest.java | 26 ++ .../testdata/java-multimodules/pom.xml | 16 + .../resources/application-mysql.properties | 0 .../resources/application-postgres.properties | 0 .../src/main/resources/application.properties | 0 .../src/main/resources/application.yml | 0 .../src/main/resources/application.properties | 0 .../src/main/resources/application.yaml | 0 cli/azd/internal/repository/app_init.go | 3 +- cli/azd/internal/repository/detect_confirm.go | 2 + cli/azd/internal/repository/infra_confirm.go | 2 +- cli/azd/internal/repository/util.go | 106 +++++++ cli/azd/internal/repository/util_test.go | 67 ++++ cli/azd/internal/scaffold/funcs.go | 51 ++-- cli/azd/internal/scaffold/funcs_test.go | 1 + cli/azd/internal/scaffold/scaffold.go | 10 +- cli/azd/pkg/ai/config.go | 2 +- cli/azd/pkg/alpha/alpha_feature.go | 2 +- cli/azd/pkg/apphost/generate.go | 2 +- cli/azd/pkg/containerapps/container_app.go | 5 +- cli/azd/pkg/infra/provisioning/manager.go | 2 +- cli/azd/pkg/osutil/expandable_string_test.go | 2 +- cli/azd/pkg/project/project.go | 2 +- cli/azd/pkg/project/project_config_test.go | 2 +- cli/azd/pkg/project/project_test.go | 2 +- .../pkg/project/service_target_aks_test.go | 2 +- cli/azd/pkg/tools/dotnet/dotnet.go | 3 + cli/azd/pkg/tools/kubectl/kube_config.go | 2 +- cli/azd/pkg/tools/kubectl/models_test.go | 2 +- cli/azd/pkg/tools/kubectl/util.go | 2 +- cli/azd/pkg/workflow/config_test.go | 2 +- cli/azd/pkg/workflow/workflow.go | 2 +- .../resources/scaffold/templates/main.bicept | 8 +- cli/azd/test/cmdrecord/cassette.go | 2 +- cli/azd/test/cmdrecord/cmdrecorder_test.go | 2 +- cli/azd/test/cmdrecord/proxy/main.go | 2 +- cli/azd/test/functional/experiment_test.go | 2 + cli/azd/test/recording/recording.go | 2 +- go.mod | 3 +- go.sum | 2 + schemas/alpha/azure.yaml.json | 111 ++++++- 71 files changed, 982 insertions(+), 724 deletions(-) delete mode 100644 cli/azd/internal/appdetect/javaanalyze/azure_yaml.go delete mode 100644 cli/azd/internal/appdetect/javaanalyze/project_analyzer_java.go delete mode 100644 cli/azd/internal/appdetect/javaanalyze/project_analyzer_maven.go delete mode 100644 cli/azd/internal/appdetect/javaanalyze/project_analyzer_spring.go delete mode 100644 cli/azd/internal/appdetect/javaanalyze/project_analyzer_spring_test.go delete mode 100644 cli/azd/internal/appdetect/javaanalyze/rule_engine.go delete mode 100644 cli/azd/internal/appdetect/javaanalyze/rule_mongo.go delete mode 100644 cli/azd/internal/appdetect/javaanalyze/rule_mysql.go delete mode 100644 cli/azd/internal/appdetect/javaanalyze/rule_postgresql.go delete mode 100644 cli/azd/internal/appdetect/javaanalyze/rule_redis.go delete mode 100644 cli/azd/internal/appdetect/javaanalyze/rule_service.go delete mode 100644 cli/azd/internal/appdetect/javaanalyze/rule_servicebus_scsb.go delete mode 100644 cli/azd/internal/appdetect/javaanalyze/rule_storage.go create mode 100644 cli/azd/internal/appdetect/testdata/java-multimodules/application/pom.xml create mode 100644 cli/azd/internal/appdetect/testdata/java-multimodules/application/src/main/java/com/example/multimodule/application/DemoApplication.java create mode 100644 cli/azd/internal/appdetect/testdata/java-multimodules/application/src/main/resources/application.properties create mode 100644 cli/azd/internal/appdetect/testdata/java-multimodules/application/src/test/java/com/example/multimodule/application/DemoApplicationTest.java create mode 100644 cli/azd/internal/appdetect/testdata/java-multimodules/library/pom.xml create mode 100644 cli/azd/internal/appdetect/testdata/java-multimodules/library/src/main/java/com/example/multimodule/service/MyService.java create mode 100644 cli/azd/internal/appdetect/testdata/java-multimodules/library/src/main/java/com/example/multimodule/service/ServiceProperties.java create mode 100644 cli/azd/internal/appdetect/testdata/java-multimodules/library/src/test/java/com/example/multimodule/service/MyServiceTest.java create mode 100644 cli/azd/internal/appdetect/testdata/java-multimodules/pom.xml rename cli/azd/internal/appdetect/{javaanalyze/testdata => testdata/java-spring}/project-four/src/main/resources/application-mysql.properties (100%) rename cli/azd/internal/appdetect/{javaanalyze/testdata => testdata/java-spring}/project-four/src/main/resources/application-postgres.properties (100%) rename cli/azd/internal/appdetect/{javaanalyze/testdata => testdata/java-spring}/project-four/src/main/resources/application.properties (100%) rename cli/azd/internal/appdetect/{javaanalyze/testdata => testdata/java-spring}/project-one/src/main/resources/application.yml (100%) rename cli/azd/internal/appdetect/{javaanalyze/testdata => testdata/java-spring}/project-three/src/main/resources/application.properties (100%) rename cli/azd/internal/appdetect/{javaanalyze/testdata => testdata/java-spring}/project-two/src/main/resources/application.yaml (100%) create mode 100644 cli/azd/internal/repository/util.go create mode 100644 cli/azd/internal/repository/util_test.go diff --git a/.vscode/cspell-github-user-aliases.txt b/.vscode/cspell-github-user-aliases.txt index 1fb61a96429..4676714fb87 100644 --- a/.vscode/cspell-github-user-aliases.txt +++ b/.vscode/cspell-github-user-aliases.txt @@ -6,6 +6,7 @@ benbjohnson blang bmatcuk bradleyjkemp +braydonk briandowns buger cobey diff --git a/.vscode/cspell.global.yaml b/.vscode/cspell.global.yaml index 94465ad0248..35f35fccca1 100644 --- a/.vscode/cspell.global.yaml +++ b/.vscode/cspell.global.yaml @@ -150,6 +150,7 @@ ignoreWords: - tfstate - tfvars - traf + - unmanage - useragent - versioncontrol - vmss diff --git a/.vscode/cspell.misc.yaml b/.vscode/cspell.misc.yaml index 2282ecb210c..b32078de70c 100644 --- a/.vscode/cspell.misc.yaml +++ b/.vscode/cspell.misc.yaml @@ -40,3 +40,4 @@ overrides: - azdev - myimage - azureai + - entra diff --git a/cli/azd/.gitignore b/cli/azd/.gitignore index 83a4d0bbddc..03fed358981 100644 --- a/cli/azd/.gitignore +++ b/cli/azd/.gitignore @@ -7,3 +7,5 @@ resource.syso versioninfo.json azd.sln +**/target + diff --git a/cli/azd/.vscode/cspell-azd-dictionary.txt b/cli/azd/.vscode/cspell-azd-dictionary.txt index acc8951751e..ae552f0dbed 100644 --- a/cli/azd/.vscode/cspell-azd-dictionary.txt +++ b/cli/azd/.vscode/cspell-azd-dictionary.txt @@ -59,6 +59,7 @@ blockblob BOOLSLICE BUILDID BUILDNUMBER +buildargs buildpacks byoi cflags diff --git a/cli/azd/CHANGELOG.md b/cli/azd/CHANGELOG.md index 6dfb1b8fe1a..dedbae143e7 100644 --- a/cli/azd/CHANGELOG.md +++ b/cli/azd/CHANGELOG.md @@ -10,6 +10,32 @@ ### Other Changes +## 1.10.3 (2024-10-16) + +### Bugs Fixed + +- [[4450]](https://github.com/Azure/azure-dev/pull/4450) fix `persistSettings` alpha feature. + +## 1.10.2 (2024-10-08) + +### Features Added + +- [[4272]](https://github.com/Azure/azure-dev/pull/4272) Supports configurable `api-version` for container app deployments. +- [[4286]](https://github.com/Azure/azure-dev/pull/4286) Adds `alpha` feature `alpha.aspire.useBicepForContainerApps` to use bicep for container app deployment. +- [[4371]](https://github.com/Azure/azure-dev/pull/4371) Adds support for `default.value` for `parameter.v0`. + +### Bugs Fixed + +- [[4375]](https://github.com/Azure/azure-dev/pull/4375) Enables remote build support for AKS. +- [[4363]](https://github.com/Azure/azure-dev/pull/4363) Fix environment variables to be evaluated too early for `main.parameters.json`. + +### Other Changes + +- [[4336]](https://github.com/Azure/azure-dev/pull/4336) Adds spinner to `azd down`. +- [[4357]](https://github.com/Azure/azure-dev/pull/4357) Updates `azure.yaml.json` for `remoteBuild`. +- [[4369]](https://github.com/Azure/azure-dev/pull/4369) Updates docker `buildargs` to expandable strings. +- [[4331]](https://github.com/Azure/azure-dev/pull/4331) Exposes configurable settings for `actionOnUnmanage` and `denySettings` for Azure Deployment Stacks (alpha). + ## 1.10.1 (2024-09-05) ### Bugs Fixed diff --git a/cli/azd/cmd/root.go b/cli/azd/cmd/root.go index 01ae9295dac..521fdd8ab4f 100644 --- a/cli/azd/cmd/root.go +++ b/cli/azd/cmd/root.go @@ -345,7 +345,6 @@ func NewRootCmd( root. UseMiddleware("debug", middleware.NewDebugMiddleware). UseMiddleware("ux", middleware.NewUxMiddleware). - UseMiddleware("experimentation", middleware.NewExperimentationMiddleware). UseMiddlewareWhen("telemetry", middleware.NewTelemetryMiddleware, func(descriptor *actions.ActionDescriptor) bool { return !descriptor.Options.DisableTelemetry }) diff --git a/cli/azd/internal/appdetect/appdetect.go b/cli/azd/internal/appdetect/appdetect.go index 88bc4286f45..2bb4b6861f8 100644 --- a/cli/azd/internal/appdetect/appdetect.go +++ b/cli/azd/internal/appdetect/appdetect.go @@ -12,7 +12,6 @@ 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" @@ -261,9 +260,6 @@ func detectUnder(ctx context.Context, root string, config detectConfig) ([]Proje return nil, fmt.Errorf("scanning directories: %w", err) } - // call the java analyzer - projects = analyze(projects) - return projects, nil } @@ -327,50 +323,3 @@ func walkDirectories(path string, fn walkDirFunc) error { return nil } - -func analyze(projects []Project) []Project { - result := []Project{} - for _, project := range projects { - if project.Language == Java { - fmt.Printf("Java project [%s] found", project.Path) - _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) - } - } - } else { - result = append(result, project) - } - } - return result -} - -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 azureYaml.Resources { - if resource.GetType() == "Azure Storage" { - // project.DatabaseDeps = append(project.DatabaseDeps, Db) - } else if resource.GetType() == "MySQL" { - 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" { - project.DatabaseDeps = append(project.DatabaseDeps, DbRedis) - } else if resource.GetType() == "Azure Service Bus" { - project.AzureDeps = append(project.AzureDeps, AzureDepServiceBus{ - Queues: resource.(*javaanalyze.ServiceBusResource).Queues, - }) - } - } -} diff --git a/cli/azd/internal/appdetect/appdetect_test.go b/cli/azd/internal/appdetect/appdetect_test.go index dcff83fdf68..83800e0057f 100644 --- a/cli/azd/internal/appdetect/appdetect_test.go +++ b/cli/azd/internal/appdetect/appdetect_test.go @@ -41,6 +41,22 @@ func TestDetect(t *testing.T) { Path: "java", DetectionRule: "Inferred by presence of: pom.xml", }, + { + Language: Java, + Path: "java-multimodules/application", + DetectionRule: "Inferred by presence of: pom.xml", + DatabaseDeps: []DatabaseDep{ + DbMongo, + DbMySql, + DbPostgres, + DbRedis, + }, + }, + { + Language: Java, + Path: "java-multimodules/library", + DetectionRule: "Inferred by presence of: pom.xml", + }, { Language: JavaScript, Path: "javascript", @@ -111,6 +127,22 @@ func TestDetect(t *testing.T) { Path: "java", DetectionRule: "Inferred by presence of: pom.xml", }, + { + Language: Java, + Path: "java-multimodules/application", + DetectionRule: "Inferred by presence of: pom.xml", + DatabaseDeps: []DatabaseDep{ + DbMongo, + DbMySql, + DbPostgres, + DbRedis, + }, + }, + { + Language: Java, + Path: "java-multimodules/library", + DetectionRule: "Inferred by presence of: pom.xml", + }, }, }, { @@ -130,6 +162,22 @@ func TestDetect(t *testing.T) { Path: "java", DetectionRule: "Inferred by presence of: pom.xml", }, + { + Language: Java, + Path: "java-multimodules/application", + DetectionRule: "Inferred by presence of: pom.xml", + DatabaseDeps: []DatabaseDep{ + DbMongo, + DbMySql, + DbPostgres, + DbRedis, + }, + }, + { + Language: Java, + Path: "java-multimodules/library", + DetectionRule: "Inferred by presence of: pom.xml", + }, }, }, { @@ -152,6 +200,22 @@ func TestDetect(t *testing.T) { Path: "java", DetectionRule: "Inferred by presence of: pom.xml", }, + { + Language: Java, + Path: "java-multimodules/application", + DetectionRule: "Inferred by presence of: pom.xml", + DatabaseDeps: []DatabaseDep{ + DbMongo, + DbMySql, + DbPostgres, + DbRedis, + }, + }, + { + Language: Java, + Path: "java-multimodules/library", + DetectionRule: "Inferred by presence of: pom.xml", + }, { Language: Python, Path: "python", @@ -222,6 +286,24 @@ func TestDetectNested(t *testing.T) { }) } +func TestAnalyzeJavaSpringProject(t *testing.T) { + var properties = readProperties(filepath.Join("testdata", "java-spring", "project-one")) + require.Equal(t, "", properties["not.exist"]) + require.Equal(t, "jdbc:h2:mem:testdb", properties["spring.datasource.url"]) + + properties = readProperties(filepath.Join("testdata", "java-spring", "project-two")) + require.Equal(t, "", properties["not.exist"]) + require.Equal(t, "jdbc:h2:mem:testdb", properties["spring.datasource.url"]) + + properties = readProperties(filepath.Join("testdata", "java-spring", "project-three")) + require.Equal(t, "", properties["not.exist"]) + require.Equal(t, "HTML", properties["spring.thymeleaf.mode"]) + + properties = readProperties(filepath.Join("testdata", "java-spring", "project-four")) + require.Equal(t, "", properties["not.exist"]) + require.Equal(t, "mysql", properties["database"]) +} + func copyTestDataDir(glob string, dst string) error { root := "testdata" return fs.WalkDir(testDataFs, root, func(name string, d fs.DirEntry, err error) error { diff --git a/cli/azd/internal/appdetect/java.go b/cli/azd/internal/appdetect/java.go index 0a5dfdac870..be9989a0baf 100644 --- a/cli/azd/internal/appdetect/java.go +++ b/cli/azd/internal/appdetect/java.go @@ -1,12 +1,23 @@ package appdetect import ( + "bufio" "context" + "encoding/xml" + "fmt" + "github.com/azure/azure-dev/cli/azd/pkg/osutil" + "github.com/braydonk/yaml" "io/fs" + "log" + "maps" + "os" + "path/filepath" + "slices" "strings" ) type javaDetector struct { + rootProjects []mavenProject } func (jd *javaDetector) Language() Language { @@ -16,13 +27,285 @@ func (jd *javaDetector) Language() Language { func (jd *javaDetector) DetectProject(ctx context.Context, path string, entries []fs.DirEntry) (*Project, error) { for _, entry := range entries { if strings.ToLower(entry.Name()) == "pom.xml" { - return &Project{ + pomFile := filepath.Join(path, entry.Name()) + project, err := readMavenProject(pomFile) + if err != nil { + return nil, fmt.Errorf("error reading pom.xml: %w", err) + } + + if len(project.Modules) > 0 { + // This is a multi-module project, we will capture the analysis, but return nil + // to continue recursing + jd.rootProjects = append(jd.rootProjects, *project) + return nil, nil + } + + var currentRoot *mavenProject + for _, rootProject := range jd.rootProjects { + // we can say that the project is in the root project if the path is under the project + if inRoot := strings.HasPrefix(pomFile, rootProject.path); inRoot { + currentRoot = &rootProject + } + } + + _ = currentRoot // use currentRoot here in the analysis + result, err := detectDependencies(project, &Project{ Language: Java, Path: path, - DetectionRule: "Inferred by presence of: " + entry.Name(), - }, nil + DetectionRule: "Inferred by presence of: pom.xml", + }) + if err != nil { + return nil, fmt.Errorf("detecting dependencies: %w", err) + } + + return result, nil } } return nil, nil } + +// 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 +} + +// 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"` +} + +func readMavenProject(filePath string) (*mavenProject, error) { + bytes, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + + var project mavenProject + if err := xml.Unmarshal(bytes, &project); err != nil { + return nil, fmt.Errorf("parsing xml: %w", err) + } + + project.path = filepath.Dir(filePath) + + return &project, nil +} + +func detectDependencies(mavenProject *mavenProject, project *Project) (*Project, error) { + // how can we tell it's a Spring Boot project? + // 1. It has a parent with a groupId of org.springframework.boot and an artifactId of spring-boot-starter-parent + // 2. It has a dependency with a groupId of org.springframework.boot and an artifactId that starts with spring-boot-starter + isSpringBoot := false + if mavenProject.Parent.GroupId == "org.springframework.boot" && mavenProject.Parent.ArtifactId == "spring-boot-starter-parent" { + isSpringBoot = true + } + for _, dep := range mavenProject.Dependencies { + if dep.GroupId == "org.springframework.boot" && strings.HasPrefix(dep.ArtifactId, "spring-boot-starter") { + isSpringBoot = true + break + } + } + applicationProperties := make(map[string]string) + if isSpringBoot { + applicationProperties = readProperties(project.Path) + } + + databaseDepMap := map[DatabaseDep]struct{}{} + for _, dep := range mavenProject.Dependencies { + if dep.GroupId == "com.mysql" && dep.ArtifactId == "mysql-connector-j" { + databaseDepMap[DbMySql] = struct{}{} + } + + if dep.GroupId == "org.postgresql" && dep.ArtifactId == "postgresql" { + databaseDepMap[DbPostgres] = struct{}{} + } + + if dep.GroupId == "org.springframework.boot" && dep.ArtifactId == "spring-boot-starter-data-redis" { + databaseDepMap[DbRedis] = struct{}{} + } + if dep.GroupId == "org.springframework.boot" && dep.ArtifactId == "spring-boot-starter-data-redis-reactive" { + databaseDepMap[DbRedis] = struct{}{} + } + + if dep.GroupId == "org.springframework.boot" && dep.ArtifactId == "spring-boot-starter-data-mongodb" { + databaseDepMap[DbMongo] = struct{}{} + } + if dep.GroupId == "org.springframework.boot" && dep.ArtifactId == "spring-boot-starter-data-mongodb-reactive" { + databaseDepMap[DbMongo] = struct{}{} + } + + if dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-stream-binder-servicebus" { + bindingDestinations := findBindingDestinations(applicationProperties) + destinations := make([]string, 0, len(bindingDestinations)) + for bindingName, destination := range bindingDestinations { + destinations = append(destinations, destination) + log.Printf("Service Bus queue [%s] found for binding [%s]", destination, bindingName) + } + project.AzureDeps = append(project.AzureDeps, AzureDepServiceBus{ + Queues: destinations, + }) + } + } + + if len(databaseDepMap) > 0 { + project.DatabaseDeps = slices.SortedFunc(maps.Keys(databaseDepMap), + func(a, b DatabaseDep) int { + return strings.Compare(string(a), string(b)) + }) + } + + return project, nil +} + +func readProperties(projectPath string) map[string]string { + // todo: do we need to consider the bootstrap.properties + result := make(map[string]string) + readPropertiesInPropertiesFile(filepath.Join(projectPath, "/src/main/resources/application.properties"), result) + readPropertiesInYamlFile(filepath.Join(projectPath, "/src/main/resources/application.yml"), result) + readPropertiesInYamlFile(filepath.Join(projectPath, "/src/main/resources/application.yaml"), result) + profile, profileSet := result["spring.profiles.active"] + if profileSet { + readPropertiesInPropertiesFile(filepath.Join(projectPath, "/src/main/resources/application-"+profile+".properties"), result) + readPropertiesInYamlFile(filepath.Join(projectPath, "/src/main/resources/application-"+profile+".yml"), result) + readPropertiesInYamlFile(filepath.Join(projectPath, "/src/main/resources/application-"+profile+".yaml"), result) + } + return result +} + +func readPropertiesInYamlFile(yamlFilePath string, result map[string]string) { + if !osutil.FileExists(yamlFilePath) { + return + } + data, err := os.ReadFile(yamlFilePath) + if err != nil { + log.Fatalf("error reading YAML file: %v", err) + return + } + + // Parse the YAML into a yaml.Node + var root yaml.Node + err = yaml.Unmarshal(data, &root) + if err != nil { + log.Fatalf("error unmarshalling YAML: %v", err) + return + } + + parseYAML("", &root, result) +} + +// Recursively parse the YAML and build dot-separated keys into a map +func parseYAML(prefix string, node *yaml.Node, result map[string]string) { + switch node.Kind { + case yaml.DocumentNode: + // Process each document's content + for _, contentNode := range node.Content { + parseYAML(prefix, contentNode, result) + } + case yaml.MappingNode: + // Process key-value pairs in a map + for i := 0; i < len(node.Content); i += 2 { + keyNode := node.Content[i] + valueNode := node.Content[i+1] + + // Ensure the key is a scalar + if keyNode.Kind != yaml.ScalarNode { + continue + } + + keyStr := keyNode.Value + newPrefix := keyStr + if prefix != "" { + newPrefix = prefix + "." + keyStr + } + parseYAML(newPrefix, valueNode, result) + } + case yaml.SequenceNode: + // Process items in a sequence (list) + for i, item := range node.Content { + newPrefix := fmt.Sprintf("%s[%d]", prefix, i) + parseYAML(newPrefix, item, result) + } + case yaml.ScalarNode: + // If it's a scalar value, add it to the result map + result[prefix] = node.Value + default: + // Handle other node types if necessary + } +} + +func readPropertiesInPropertiesFile(propertiesFilePath string, result map[string]string) { + if !osutil.FileExists(propertiesFilePath) { + return + } + file, err := os.Open(propertiesFilePath) + if err != nil { + log.Fatalf("error opening properties file: %v", err) + return + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if strings.TrimSpace(line) == "" || strings.HasPrefix(line, "#") { + continue + } + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 { + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + result[key] = value + } + } +} + +// Function to find all properties that match the pattern `spring.cloud.stream.bindings..destination` +func findBindingDestinations(properties map[string]string) map[string]string { + result := make(map[string]string) + + // Iterate through the properties map and look for matching keys + for key, value := range properties { + // Check if the key matches the pattern `spring.cloud.stream.bindings..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 +} diff --git a/cli/azd/internal/appdetect/javaanalyze/azure_yaml.go b/cli/azd/internal/appdetect/javaanalyze/azure_yaml.go deleted file mode 100644 index 41e848c88cd..00000000000 --- a/cli/azd/internal/appdetect/javaanalyze/azure_yaml.go +++ /dev/null @@ -1,91 +0,0 @@ -package javaanalyze - -type AzureYaml struct { - Service *Service `json:"service"` - 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"` - BicepParameters []BicepParameter `json:"bicepParameters"` - 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"` - 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 -) - -// 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 { - 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/internal/appdetect/javaanalyze/project_analyzer_java.go b/cli/azd/internal/appdetect/javaanalyze/project_analyzer_java.go deleted file mode 100644 index dd3fbe37665..00000000000 --- a/cli/azd/internal/appdetect/javaanalyze/project_analyzer_java.go +++ /dev/null @@ -1,41 +0,0 @@ -package javaanalyze - -import "os" - -type javaProject struct { - springProject springProject - mavenProject mavenProject -} - -func Analyze(path string) []AzureYaml { - var result []AzureYaml - rules := []rule{ - &ruleService{}, - &ruleMysql{}, - &rulePostgresql{}, - &ruleMongo{}, - &ruleRedis{}, - &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/project_analyzer_maven.go b/cli/azd/internal/appdetect/javaanalyze/project_analyzer_maven.go deleted file mode 100644 index 6f79d0f73bd..00000000000 --- a/cli/azd/internal/appdetect/javaanalyze/project_analyzer_maven.go +++ /dev/null @@ -1,92 +0,0 @@ -package javaanalyze - -import ( - "encoding/xml" - "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"` - Modules []string `xml:"modules>module"` // Capture the modules - 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 { - 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"` -} - -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) - } - 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) - } - - 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 deleted file mode 100644 index 3370477b551..00000000000 --- a/cli/azd/internal/appdetect/javaanalyze/project_analyzer_spring.go +++ /dev/null @@ -1,115 +0,0 @@ -package javaanalyze - -import ( - "bufio" - "fmt" - "gopkg.in/yaml.v3" - "log" - "os" - "path/filepath" - "strings" -) - -type springProject struct { - applicationProperties map[string]string -} - -func analyzeSpringProject(projectPath string) springProject { - return springProject{ - applicationProperties: getProperties(projectPath), - } -} - -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 { - // Ignore the error if file not exist. - return - } - - // Parse the YAML into a yaml.Node - var root yaml.Node - err = yaml.Unmarshal(data, &root) - if err != nil { - log.Fatalf("error unmarshalling YAML: %v", err) - } - - parseYAML("", &root, result) -} - -// Recursively parse the YAML and build dot-separated keys into a map -func parseYAML(prefix string, node *yaml.Node, result map[string]string) { - switch node.Kind { - case yaml.DocumentNode: - // Process each document's content - for _, contentNode := range node.Content { - parseYAML(prefix, contentNode, result) - } - case yaml.MappingNode: - // Process key-value pairs in a map - for i := 0; i < len(node.Content); i += 2 { - keyNode := node.Content[i] - valueNode := node.Content[i+1] - - // Ensure the key is a scalar - if keyNode.Kind != yaml.ScalarNode { - continue - } - - keyStr := keyNode.Value - newPrefix := keyStr - if prefix != "" { - newPrefix = prefix + "." + keyStr - } - parseYAML(newPrefix, valueNode, result) - } - case yaml.SequenceNode: - // Process items in a sequence (list) - for i, item := range node.Content { - newPrefix := fmt.Sprintf("%s[%d]", prefix, i) - parseYAML(newPrefix, item, result) - } - case yaml.ScalarNode: - // If it's a scalar value, add it to the result map - result[prefix] = node.Value - default: - // Handle other node types if necessary - } -} - -func 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 deleted file mode 100644 index 833645e4b52..00000000000 --- a/cli/azd/internal/appdetect/javaanalyze/project_analyzer_spring_test.go +++ /dev/null @@ -1,26 +0,0 @@ -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_engine.go b/cli/azd/internal/appdetect/javaanalyze/rule_engine.go deleted file mode 100644 index 630d2d0ebf4..00000000000 --- a/cli/azd/internal/appdetect/javaanalyze/rule_engine.go +++ /dev/null @@ -1,17 +0,0 @@ -package javaanalyze - -type rule interface { - match(project *javaProject) bool - apply(azureYaml *AzureYaml) -} - -func applyRules(javaProject *javaProject, rules []rule) (*AzureYaml, error) { - azureYaml := &AzureYaml{} - - for _, r := range rules { - if r.match(javaProject) { - r.apply(azureYaml) - } - } - return azureYaml, nil -} diff --git a/cli/azd/internal/appdetect/javaanalyze/rule_mongo.go b/cli/azd/internal/appdetect/javaanalyze/rule_mongo.go deleted file mode 100644 index 74e282bde2f..00000000000 --- a/cli/azd/internal/appdetect/javaanalyze/rule_mongo.go +++ /dev/null @@ -1,30 +0,0 @@ -package javaanalyze - -type ruleMongo struct { -} - -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 - } - if dep.GroupId == "org.springframework.boot" && dep.ArtifactId == "spring-boot-starter-data-mongodb-reactive" { - return true - } - } - } - return false -} - -func (mr *ruleMongo) apply(azureYaml *AzureYaml) { - azureYaml.Resources = append(azureYaml.Resources, &Resource{ - Name: "MongoDB", - Type: "MongoDB", - }) - - 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 deleted file mode 100644 index c98d317b101..00000000000 --- a/cli/azd/internal/appdetect/javaanalyze/rule_mysql.go +++ /dev/null @@ -1,27 +0,0 @@ -package javaanalyze - -type ruleMysql struct { -} - -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 - } - } - } - return false -} - -func (mr *ruleMysql) apply(azureYaml *AzureYaml) { - azureYaml.Resources = append(azureYaml.Resources, &Resource{ - Name: "MySQL", - Type: "MySQL", - }) - - azureYaml.ServiceBindings = append(azureYaml.ServiceBindings, ServiceBinding{ - Name: "MySQL", - AuthType: AuthType_SYSTEM_MANAGED_IDENTITY, - }) -} diff --git a/cli/azd/internal/appdetect/javaanalyze/rule_postgresql.go b/cli/azd/internal/appdetect/javaanalyze/rule_postgresql.go deleted file mode 100644 index bfe58533428..00000000000 --- a/cli/azd/internal/appdetect/javaanalyze/rule_postgresql.go +++ /dev/null @@ -1,27 +0,0 @@ -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/internal/appdetect/javaanalyze/rule_redis.go b/cli/azd/internal/appdetect/javaanalyze/rule_redis.go deleted file mode 100644 index 7e87a57afa8..00000000000 --- a/cli/azd/internal/appdetect/javaanalyze/rule_redis.go +++ /dev/null @@ -1,25 +0,0 @@ -package javaanalyze - -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 -} - -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 deleted file mode 100644 index 8203848830f..00000000000 --- a/cli/azd/internal/appdetect/javaanalyze/rule_service.go +++ /dev/null @@ -1,17 +0,0 @@ -package javaanalyze - -type ruleService struct { - javaProject *javaProject -} - -func (r *ruleService) match(javaProject *javaProject) bool { - r.javaProject = javaProject - return true -} - -func (r *ruleService) apply(azureYaml *AzureYaml) { - if azureYaml.Service == nil { - azureYaml.Service = &Service{} - } - azureYaml.Service.Path = r.javaProject.mavenProject.path -} diff --git a/cli/azd/internal/appdetect/javaanalyze/rule_servicebus_scsb.go b/cli/azd/internal/appdetect/javaanalyze/rule_servicebus_scsb.go deleted file mode 100644 index 242d22560ff..00000000000 --- a/cli/azd/internal/appdetect/javaanalyze/rule_servicebus_scsb.go +++ /dev/null @@ -1,62 +0,0 @@ -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]string) map[string]string { - result := make(map[string]string) - - // Iterate through the properties map and look for matching keys - for key, value := range properties { - // Check if the key matches the pattern `spring.cloud.stream.bindings..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 deleted file mode 100644 index 557733ebb7b..00000000000 --- a/cli/azd/internal/appdetect/javaanalyze/rule_storage.go +++ /dev/null @@ -1,39 +0,0 @@ -package javaanalyze - -type ruleStorage struct { -} - -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 - } - 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 (r *ruleStorage) apply(azureYaml *AzureYaml) { - azureYaml.Resources = append(azureYaml.Resources, &Resource{ - Name: "Azure Storage", - Type: "Azure Storage", - }) - - azureYaml.ServiceBindings = append(azureYaml.ServiceBindings, ServiceBinding{ - Name: "Azure Storage", - AuthType: AuthType_SYSTEM_MANAGED_IDENTITY, - }) -} diff --git a/cli/azd/internal/appdetect/testdata/java-multimodules/application/pom.xml b/cli/azd/internal/appdetect/testdata/java-multimodules/application/pom.xml new file mode 100644 index 00000000000..a63cc042486 --- /dev/null +++ b/cli/azd/internal/appdetect/testdata/java-multimodules/application/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.0 + + + com.example + application + 0.0.1-SNAPSHOT + application + Demo project for Spring Boot + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-web + + + com.example + library + ${project.version} + + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.mysql + mysql-connector-j + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + + org.postgresql + postgresql + test + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/cli/azd/internal/appdetect/testdata/java-multimodules/application/src/main/java/com/example/multimodule/application/DemoApplication.java b/cli/azd/internal/appdetect/testdata/java-multimodules/application/src/main/java/com/example/multimodule/application/DemoApplication.java new file mode 100644 index 00000000000..de6d4e0c7ce --- /dev/null +++ b/cli/azd/internal/appdetect/testdata/java-multimodules/application/src/main/java/com/example/multimodule/application/DemoApplication.java @@ -0,0 +1,27 @@ +package com.example.multimodule.application; + +import com.example.multimodule.service.MyService; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@SpringBootApplication(scanBasePackages = "com.example.multimodule") +@RestController +public class DemoApplication { + + private final MyService myService; + + public DemoApplication(MyService myService) { + this.myService = myService; + } + + @GetMapping("/") + public String home() { + return myService.message(); + } + + public static void main(String[] args) { + SpringApplication.run(DemoApplication.class, args); + } +} diff --git a/cli/azd/internal/appdetect/testdata/java-multimodules/application/src/main/resources/application.properties b/cli/azd/internal/appdetect/testdata/java-multimodules/application/src/main/resources/application.properties new file mode 100644 index 00000000000..7c40093f75e --- /dev/null +++ b/cli/azd/internal/appdetect/testdata/java-multimodules/application/src/main/resources/application.properties @@ -0,0 +1 @@ +service.message=Hello, World diff --git a/cli/azd/internal/appdetect/testdata/java-multimodules/application/src/test/java/com/example/multimodule/application/DemoApplicationTest.java b/cli/azd/internal/appdetect/testdata/java-multimodules/application/src/test/java/com/example/multimodule/application/DemoApplicationTest.java new file mode 100644 index 00000000000..7ef7bd2ad19 --- /dev/null +++ b/cli/azd/internal/appdetect/testdata/java-multimodules/application/src/test/java/com/example/multimodule/application/DemoApplicationTest.java @@ -0,0 +1,23 @@ +package com.example.multimodule.application; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.example.multimodule.service.MyService; + +@SpringBootTest +public class DemoApplicationTest { + + @Autowired + private MyService myService; + + @Test + public void contextLoads() { + assertThat(myService.message()).isNotNull(); + } + +} diff --git a/cli/azd/internal/appdetect/testdata/java-multimodules/library/pom.xml b/cli/azd/internal/appdetect/testdata/java-multimodules/library/pom.xml new file mode 100644 index 00000000000..8a2db935b0f --- /dev/null +++ b/cli/azd/internal/appdetect/testdata/java-multimodules/library/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + com.example + library + 0.0.1-SNAPSHOT + library + Demo project for Spring Boot + + + org.springframework.boot + spring-boot + + + + org.springframework.boot + spring-boot-starter-test + test + + + + diff --git a/cli/azd/internal/appdetect/testdata/java-multimodules/library/src/main/java/com/example/multimodule/service/MyService.java b/cli/azd/internal/appdetect/testdata/java-multimodules/library/src/main/java/com/example/multimodule/service/MyService.java new file mode 100644 index 00000000000..06444562963 --- /dev/null +++ b/cli/azd/internal/appdetect/testdata/java-multimodules/library/src/main/java/com/example/multimodule/service/MyService.java @@ -0,0 +1,19 @@ +package com.example.multimodule.service; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Service; + +@Service +@EnableConfigurationProperties(ServiceProperties.class) +public class MyService { + + private final ServiceProperties serviceProperties; + + public MyService(ServiceProperties serviceProperties) { + this.serviceProperties = serviceProperties; + } + + public String message() { + return this.serviceProperties.getMessage(); + } +} diff --git a/cli/azd/internal/appdetect/testdata/java-multimodules/library/src/main/java/com/example/multimodule/service/ServiceProperties.java b/cli/azd/internal/appdetect/testdata/java-multimodules/library/src/main/java/com/example/multimodule/service/ServiceProperties.java new file mode 100644 index 00000000000..7dd29b730e0 --- /dev/null +++ b/cli/azd/internal/appdetect/testdata/java-multimodules/library/src/main/java/com/example/multimodule/service/ServiceProperties.java @@ -0,0 +1,20 @@ +package com.example.multimodule.service; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("service") +public class ServiceProperties { + + /** + * A message for the service. + */ + private String message; + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/cli/azd/internal/appdetect/testdata/java-multimodules/library/src/test/java/com/example/multimodule/service/MyServiceTest.java b/cli/azd/internal/appdetect/testdata/java-multimodules/library/src/test/java/com/example/multimodule/service/MyServiceTest.java new file mode 100644 index 00000000000..0a2a07cfeef --- /dev/null +++ b/cli/azd/internal/appdetect/testdata/java-multimodules/library/src/test/java/com/example/multimodule/service/MyServiceTest.java @@ -0,0 +1,26 @@ +package com.example.multimodule.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest("service.message=Hello") +public class MyServiceTest { + + @Autowired + private MyService myService; + + @Test + public void contextLoads() { + assertThat(myService.message()).isNotNull(); + } + + @SpringBootApplication + static class TestConfiguration { + } + +} diff --git a/cli/azd/internal/appdetect/testdata/java-multimodules/pom.xml b/cli/azd/internal/appdetect/testdata/java-multimodules/pom.xml new file mode 100644 index 00000000000..fa72a1aa4c8 --- /dev/null +++ b/cli/azd/internal/appdetect/testdata/java-multimodules/pom.xml @@ -0,0 +1,16 @@ + + + 4.0.0 + + org.springframework + gs-multi-module + 0.1.0 + pom + + + library + application + + + diff --git a/cli/azd/internal/appdetect/javaanalyze/testdata/project-four/src/main/resources/application-mysql.properties b/cli/azd/internal/appdetect/testdata/java-spring/project-four/src/main/resources/application-mysql.properties similarity index 100% rename from cli/azd/internal/appdetect/javaanalyze/testdata/project-four/src/main/resources/application-mysql.properties rename to cli/azd/internal/appdetect/testdata/java-spring/project-four/src/main/resources/application-mysql.properties diff --git a/cli/azd/internal/appdetect/javaanalyze/testdata/project-four/src/main/resources/application-postgres.properties b/cli/azd/internal/appdetect/testdata/java-spring/project-four/src/main/resources/application-postgres.properties similarity index 100% rename from cli/azd/internal/appdetect/javaanalyze/testdata/project-four/src/main/resources/application-postgres.properties rename to cli/azd/internal/appdetect/testdata/java-spring/project-four/src/main/resources/application-postgres.properties diff --git a/cli/azd/internal/appdetect/javaanalyze/testdata/project-four/src/main/resources/application.properties b/cli/azd/internal/appdetect/testdata/java-spring/project-four/src/main/resources/application.properties similarity index 100% rename from cli/azd/internal/appdetect/javaanalyze/testdata/project-four/src/main/resources/application.properties rename to cli/azd/internal/appdetect/testdata/java-spring/project-four/src/main/resources/application.properties diff --git a/cli/azd/internal/appdetect/javaanalyze/testdata/project-one/src/main/resources/application.yml b/cli/azd/internal/appdetect/testdata/java-spring/project-one/src/main/resources/application.yml similarity index 100% rename from cli/azd/internal/appdetect/javaanalyze/testdata/project-one/src/main/resources/application.yml rename to cli/azd/internal/appdetect/testdata/java-spring/project-one/src/main/resources/application.yml diff --git a/cli/azd/internal/appdetect/javaanalyze/testdata/project-three/src/main/resources/application.properties b/cli/azd/internal/appdetect/testdata/java-spring/project-three/src/main/resources/application.properties similarity index 100% rename from cli/azd/internal/appdetect/javaanalyze/testdata/project-three/src/main/resources/application.properties rename to cli/azd/internal/appdetect/testdata/java-spring/project-three/src/main/resources/application.properties diff --git a/cli/azd/internal/appdetect/javaanalyze/testdata/project-two/src/main/resources/application.yaml b/cli/azd/internal/appdetect/testdata/java-spring/project-two/src/main/resources/application.yaml similarity index 100% rename from cli/azd/internal/appdetect/javaanalyze/testdata/project-two/src/main/resources/application.yaml rename to cli/azd/internal/appdetect/testdata/java-spring/project-two/src/main/resources/application.yaml diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index 712782808b8..efad238a3a1 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -350,7 +350,7 @@ func prjConfigFromDetect( root string, detect detectConfirm) (project.ProjectConfig, error) { config := project.ProjectConfig{ - Name: filepath.Base(root), + Name: LabelName(filepath.Base(root)), Metadata: &project.ProjectMetadata{ Template: fmt.Sprintf("%s@%s", InitGenTemplateId, internal.VersionInfo().Version), }, @@ -415,6 +415,7 @@ func prjConfigFromDetect( if name == "." { name = config.Name } + name = LabelName(name) config.Services[name] = &svc } diff --git a/cli/azd/internal/repository/detect_confirm.go b/cli/azd/internal/repository/detect_confirm.go index cefce4f8e6c..900ba3e8820 100644 --- a/cli/azd/internal/repository/detect_confirm.go +++ b/cli/azd/internal/repository/detect_confirm.go @@ -308,6 +308,7 @@ func (d *detectConfirm) remove(ctx context.Context) error { confirm, err := d.console.Confirm(ctx, input.ConsoleOptions{ Message: fmt.Sprintf( "Remove %s in %s?", projectDisplayName(svc), relSafe(d.root, svc.Path)), + DefaultValue: true, }) if err != nil { return err @@ -325,6 +326,7 @@ func (d *detectConfirm) remove(ctx context.Context) error { confirm, err := d.console.Confirm(ctx, input.ConsoleOptions{ Message: fmt.Sprintf( "Remove %s?", db.Display()), + DefaultValue: true, }) if err != nil { return err diff --git a/cli/azd/internal/repository/infra_confirm.go b/cli/azd/internal/repository/infra_confirm.go index d1b6d2ce7c0..74a94d1364b 100644 --- a/cli/azd/internal/repository/infra_confirm.go +++ b/cli/azd/internal/repository/infra_confirm.go @@ -123,7 +123,7 @@ func (i *Initializer) infraSpecFromDetect( } for _, svc := range detect.Services { - name := filepath.Base(svc.Path) + name := LabelName(filepath.Base(svc.Path)) serviceSpec := scaffold.ServiceSpec{ Name: name, Port: -1, diff --git a/cli/azd/internal/repository/util.go b/cli/azd/internal/repository/util.go new file mode 100644 index 00000000000..3e5f563646a --- /dev/null +++ b/cli/azd/internal/repository/util.go @@ -0,0 +1,106 @@ +package repository + +import "strings" + +//cspell:disable + +// LabelName cleans up a string to be used as a RFC 1123 Label name. +// It does not enforce the 63 character limit. +// +// RFC 1123 Label name: +// - contain only lowercase alphanumeric characters or '-' +// - start with an alphanumeric character +// - end with an alphanumeric character +// +// Examples: +// - myproject, MYPROJECT -> myproject +// - myProject, myProjecT, MyProject, MyProjecT -> my-project +// - my.project, My.Project, my-project, My-Project -> my-project +func LabelName(name string) string { + hasSeparator, n := cleanAlphaNumeric(name) + if hasSeparator { + return labelNameFromSeparators(n) + } + + return labelNameFromCasing(name) +} + +//cspell:enable + +// cleanAlphaNumeric removes non-alphanumeric characters from the name. +// +// It also returns whether the name uses word separators. +func cleanAlphaNumeric(name string) (hasSeparator bool, cleaned string) { + sb := strings.Builder{} + hasSeparator = false + for _, c := range name { + if isAsciiAlphaNumeric(c) { + sb.WriteRune(c) + } else if isSeparator(c) { + hasSeparator = true + sb.WriteRune(c) + } + } + + return hasSeparator, sb.String() +} + +func isAsciiAlphaNumeric(r rune) bool { + return ('0' <= r && r <= '9') || ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') +} + +func isSeparator(r rune) bool { + return r == '-' || r == '_' || r == '.' +} + +func lowerCase(r rune) rune { + if 'A' <= r && r <= 'Z' { + r += 'a' - 'A' + } + return r +} + +// Converts camel-cased or Pascal-cased names into lower-cased dash-separated names. +// Example: MyProject, myProject -> my-project +func labelNameFromCasing(name string) string { + result := strings.Builder{} + // previously seen upper-case character + prevUpperCase := -2 // -2 to avoid matching the first character + + for i, c := range name { + if 'A' <= c && c <= 'Z' { + if prevUpperCase == i-1 { // handle runs of upper-case word + prevUpperCase = i + result.WriteRune(lowerCase(c)) + continue + } + + if i > 0 && i != len(name)-1 { + result.WriteRune('-') + } + + prevUpperCase = i + } + + if isAsciiAlphaNumeric(c) { + result.WriteRune(lowerCase(c)) + } + } + + return result.String() +} + +// Converts all word-separated names into lower-cased dash-separated names. +// Examples: my.project, my_project, My-Project -> my-project +func labelNameFromSeparators(name string) string { + result := strings.Builder{} + for i, c := range name { + if isAsciiAlphaNumeric(c) { + result.WriteRune(lowerCase(c)) + } else if i > 0 && i != len(name)-1 && isSeparator(c) { + result.WriteRune('-') + } + } + + return result.String() +} diff --git a/cli/azd/internal/repository/util_test.go b/cli/azd/internal/repository/util_test.go new file mode 100644 index 00000000000..56a2c467756 --- /dev/null +++ b/cli/azd/internal/repository/util_test.go @@ -0,0 +1,67 @@ +package repository + +import ( + "testing" +) + +//cspell:disable + +func TestLabelName(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + {"Lowercase", "myproject", "myproject"}, + {"Uppercase", "MYPROJECT", "myproject"}, + {"MixedCase", "myProject", "my-project"}, + {"MixedCaseEnd", "myProjecT", "my-project"}, + {"TitleCase", "MyProject", "my-project"}, + {"TitleCaseEnd", "MyProjecT", "my-project"}, + {"WithDot", "my.project", "my-project"}, + {"WithDotTitleCase", "My.Project", "my-project"}, + {"WithHyphen", "my-project", "my-project"}, + {"WithHyphenTitleCase", "My-Project", "my-project"}, + {"StartWithNumber", "1myproject", "1myproject"}, + {"EndWithNumber", "myproject2", "myproject2"}, + {"MixedWithNumbers", "my2Project3", "my2-project3"}, + {"SpecialCharacters", "my_project!@#", "my-project"}, + {"EmptyString", "", ""}, + {"OnlySpecialCharacters", "@#$%^&*", ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := LabelName(tt.input) + if result != tt.expected { + t.Errorf("LabelName(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestLabelNameEdgeCases(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + {"SingleCharacter", "A", "a"}, + {"TwoCharacters", "Ab", "ab"}, + {"StartEndHyphens", "-abc-", "abc"}, + {"LongString", + "ThisIsOneVeryLongStringThatExceedsTheSixtyThreeCharacterLimitForRFC1123LabelNames", + "this-is-one-very-long-string-that-exceeds-the-sixty-three-character-limit-for-rfc1123-label-names"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := LabelName(tt.input) + if result != tt.expected { + t.Errorf("LabelName(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} + +//cspell:enable diff --git a/cli/azd/internal/scaffold/funcs.go b/cli/azd/internal/scaffold/funcs.go index 9e124a572da..22fa3d2664e 100644 --- a/cli/azd/internal/scaffold/funcs.go +++ b/cli/azd/internal/scaffold/funcs.go @@ -14,6 +14,9 @@ import ( func BicepName(name string) string { sb := strings.Builder{} separatorStart := -1 + + allUpper := isAllUpperCase(name) + for i := range name { switch name[i] { case '-', '_': @@ -24,18 +27,17 @@ func BicepName(name string) string { if !isAsciiAlphaNumeric(name[i]) { continue } - char := name[i] - if separatorStart != -1 { - if separatorStart == 0 { // first character should be lowerCase - char = lowerCase(name[i]) - } else { - char = upperCase(name[i]) - } + var char byte + if separatorStart == 0 || i == 0 { // we are at the start + char = lowerCase(name[i]) separatorStart = -1 - } - - if i == 0 { + } else if separatorStart > 0 { // end of separator, and it's not the first one + char = upperCase(name[i]) + separatorStart = -1 + } else if allUpper { // when the input is all uppercase, convert to lowercase char = lowerCase(name[i]) + } else { + char = name[i] } sb.WriteByte(char) @@ -81,6 +83,16 @@ func AlphaSnakeUpper(name string) string { return sb.String() } +func isAllUpperCase(c string) bool { + for i := range c { + if 'a' <= c[i] && c[i] <= 'z' { + return false + } + } + + return true +} + func isAsciiAlphaNumeric(c byte) bool { return ('0' <= c && c <= '9') || ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') } @@ -99,16 +111,9 @@ func lowerCase(r byte) byte { return r } -// Provide a reasonable limit for the container app infix to avoid name length issues -// This is calculated as follows: -// 1. Start with max initial length of 32 characters from the Container App name -// https://learn.microsoft.com/azure/azure-resource-manager/management/resource-name-rules#microsoftapp -// 2. Prefix abbreviation of 'ca-' from abbreviations.json (4 characters) -// 3. Bicep resource token (13 characters) + separator '-' (1 character) -- total of 14 characters +// 32 characters are allowed for the Container App name. See +// https://learn.microsoft.com/azure/azure-resource-manager/management/resource-name-rules#microsoftapp // -// Which leaves us with: 32 - 4 - 14 = 14 characters. -const containerAppNameInfixMaxLen = 12 - // We allow 2 additional characters for wiggle-room. We've seen failures when container app name is exactly at 32. const containerAppNameMaxLen = 30 @@ -173,14 +178,6 @@ func EnvFormat(src string) string { return fmt.Sprintf("${AZURE_%s}", snake) } -// ContainerAppInfix returns a suitable infix for a container app resource. -// -// The name is treated to only contain alphanumeric and dash characters, with no repeated dashes, and no dashes -// as the first or last character. -func ContainerAppInfix(name string) string { - return containerAppName(name, containerAppNameInfixMaxLen) -} - // Formats a parameter value for use in a bicep file. // If the value is a string, it is quoted inline with no indentation. // Otherwise, the value is marshaled with indentation specified by prefix and indent. diff --git a/cli/azd/internal/scaffold/funcs_test.go b/cli/azd/internal/scaffold/funcs_test.go index 68139946965..6adb8d0a24e 100644 --- a/cli/azd/internal/scaffold/funcs_test.go +++ b/cli/azd/internal/scaffold/funcs_test.go @@ -32,6 +32,7 @@ func Test_BicepName(t *testing.T) { in string want string }{ + {"alpha upper snake", "THIS_IS_MY_VAR_123", "thisIsMyVar123"}, {"uppercase separators", "this-is-my-var-123", "thisIsMyVar123"}, {"allowed characters", "myVar_!#%^", "myVar"}, {"normalize casing", "MyVar", "myVar"}, diff --git a/cli/azd/internal/scaffold/scaffold.go b/cli/azd/internal/scaffold/scaffold.go index b89a94ce317..8e856d32496 100644 --- a/cli/azd/internal/scaffold/scaffold.go +++ b/cli/azd/internal/scaffold/scaffold.go @@ -54,11 +54,11 @@ func copyFS(embedFs fs.FS, root string, target string) error { // To execute a named template, call Execute with the defined name. func Load() (*template.Template, error) { funcMap := template.FuncMap{ - "bicepName": BicepName, - "containerAppInfix": ContainerAppInfix, - "upper": strings.ToUpper, - "lower": strings.ToLower, - "formatParam": FormatParameter, + "bicepName": BicepName, + "containerAppName": ContainerAppName, + "upper": strings.ToUpper, + "lower": strings.ToLower, + "formatParam": FormatParameter, } t, err := template.New("templates"). diff --git a/cli/azd/pkg/ai/config.go b/cli/azd/pkg/ai/config.go index 3b621a237c2..b4b51104c63 100644 --- a/cli/azd/pkg/ai/config.go +++ b/cli/azd/pkg/ai/config.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/azure/azure-dev/cli/azd/pkg/osutil" - "gopkg.in/yaml.v3" + "github.com/braydonk/yaml" ) // ComponentConfig is a base configuration structure used by multiple AI components diff --git a/cli/azd/pkg/alpha/alpha_feature.go b/cli/azd/pkg/alpha/alpha_feature.go index 95dc8889415..94f8d11b84d 100644 --- a/cli/azd/pkg/alpha/alpha_feature.go +++ b/cli/azd/pkg/alpha/alpha_feature.go @@ -6,7 +6,7 @@ import ( "strings" "github.com/azure/azure-dev/cli/azd/resources" - "gopkg.in/yaml.v3" + "github.com/braydonk/yaml" ) // Feature defines the structure for a feature in alpha mode. diff --git a/cli/azd/pkg/apphost/generate.go b/cli/azd/pkg/apphost/generate.go index 4d1bececeb8..8a753bfcb54 100644 --- a/cli/azd/pkg/apphost/generate.go +++ b/cli/azd/pkg/apphost/generate.go @@ -28,8 +28,8 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/osutil" "github.com/azure/azure-dev/cli/azd/pkg/output" "github.com/azure/azure-dev/cli/azd/resources" + "github.com/braydonk/yaml" "github.com/psanford/memfs" - "gopkg.in/yaml.v3" ) const RedisContainerAppService = "redis" diff --git a/cli/azd/pkg/containerapps/container_app.go b/cli/azd/pkg/containerapps/container_app.go index 8edd230b8cc..3a15ba6b39e 100644 --- a/cli/azd/pkg/containerapps/container_app.go +++ b/cli/azd/pkg/containerapps/container_app.go @@ -20,7 +20,7 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/config" "github.com/azure/azure-dev/cli/azd/pkg/convert" "github.com/benbjohnson/clock" - "gopkg.in/yaml.v3" + "github.com/braydonk/yaml" ) const ( @@ -146,6 +146,9 @@ func (cas *containerAppService) persistSettings( aca, err := cas.getContainerApp(ctx, subscriptionId, resourceGroupName, appName, options) if err != nil { log.Printf("failed getting current aca settings: %v. No settings will be persisted.", err) + // if the container app doesn't exist, there's nothing for us to update in the desired state, + // so we can just return the existing state as is. + return obj, nil } objConfig := config.NewConfig(obj) diff --git a/cli/azd/pkg/infra/provisioning/manager.go b/cli/azd/pkg/infra/provisioning/manager.go index c31bb910b27..5979aac9a71 100644 --- a/cli/azd/pkg/infra/provisioning/manager.go +++ b/cli/azd/pkg/infra/provisioning/manager.go @@ -22,7 +22,7 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/output" "github.com/azure/azure-dev/cli/azd/pkg/output/ux" "github.com/azure/azure-dev/cli/azd/pkg/prompt" - "gopkg.in/yaml.v3" + "github.com/braydonk/yaml" ) type DefaultProviderResolver func() (ProviderKind, error) diff --git a/cli/azd/pkg/osutil/expandable_string_test.go b/cli/azd/pkg/osutil/expandable_string_test.go index 4e9832dbf50..a0f3ac2c53b 100644 --- a/cli/azd/pkg/osutil/expandable_string_test.go +++ b/cli/azd/pkg/osutil/expandable_string_test.go @@ -6,8 +6,8 @@ package osutil import ( "testing" + "github.com/braydonk/yaml" "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v3" ) func TestExpandableStringYaml(t *testing.T) { diff --git a/cli/azd/pkg/project/project.go b/cli/azd/pkg/project/project.go index 84d33f85a60..ae8f6c5c03c 100644 --- a/cli/azd/pkg/project/project.go +++ b/cli/azd/pkg/project/project.go @@ -18,7 +18,7 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning" "github.com/azure/azure-dev/cli/azd/pkg/osutil" "github.com/blang/semver/v4" - "gopkg.in/yaml.v3" + "github.com/braydonk/yaml" ) const ( diff --git a/cli/azd/pkg/project/project_config_test.go b/cli/azd/pkg/project/project_config_test.go index 3b0b1af2e4c..02f68f0a086 100644 --- a/cli/azd/pkg/project/project_config_test.go +++ b/cli/azd/pkg/project/project_config_test.go @@ -13,8 +13,8 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/osutil" "github.com/azure/azure-dev/cli/azd/test/mocks" "github.com/azure/azure-dev/cli/azd/test/snapshot" + "github.com/braydonk/yaml" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" ) // Tests invalid project configurations. diff --git a/cli/azd/pkg/project/project_test.go b/cli/azd/pkg/project/project_test.go index 3e7129a487b..f3fa036e69c 100644 --- a/cli/azd/pkg/project/project_test.go +++ b/cli/azd/pkg/project/project_test.go @@ -18,9 +18,9 @@ import ( "github.com/azure/azure-dev/cli/azd/test/mocks/mockarmresources" "github.com/azure/azure-dev/cli/azd/test/mocks/mockazcli" "github.com/azure/azure-dev/cli/azd/test/snapshot" + "github.com/braydonk/yaml" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" ) // Specifying resource name in the project file should override the default diff --git a/cli/azd/pkg/project/service_target_aks_test.go b/cli/azd/pkg/project/service_target_aks_test.go index 7c10d949a5f..18327fdd490 100644 --- a/cli/azd/pkg/project/service_target_aks_test.go +++ b/cli/azd/pkg/project/service_target_aks_test.go @@ -35,9 +35,9 @@ import ( "github.com/azure/azure-dev/cli/azd/test/mocks/mockenv" "github.com/azure/azure-dev/cli/azd/test/ostest" "github.com/benbjohnson/clock" + "github.com/braydonk/yaml" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" ) func Test_NewAksTarget(t *testing.T) { diff --git a/cli/azd/pkg/tools/dotnet/dotnet.go b/cli/azd/pkg/tools/dotnet/dotnet.go index 8361a7477be..ae5ed3a88ea 100644 --- a/cli/azd/pkg/tools/dotnet/dotnet.go +++ b/cli/azd/pkg/tools/dotnet/dotnet.go @@ -189,6 +189,9 @@ func (cli *Cli) PublishContainer( ) runArgs = runArgs.WithEnv([]string{ + fmt.Sprintf("DOTNET_CONTAINER_REGISTRY_UNAME=%s", username), + fmt.Sprintf("DOTNET_CONTAINER_REGISTRY_PWORD=%s", password), + // legacy variables for dotnet SDK version < 8.0.400 fmt.Sprintf("SDK_CONTAINER_REGISTRY_UNAME=%s", username), fmt.Sprintf("SDK_CONTAINER_REGISTRY_PWORD=%s", password), }) diff --git a/cli/azd/pkg/tools/kubectl/kube_config.go b/cli/azd/pkg/tools/kubectl/kube_config.go index c9f54b8507c..c4f4f1a4812 100644 --- a/cli/azd/pkg/tools/kubectl/kube_config.go +++ b/cli/azd/pkg/tools/kubectl/kube_config.go @@ -8,7 +8,7 @@ import ( "strings" "github.com/azure/azure-dev/cli/azd/pkg/osutil" - "gopkg.in/yaml.v3" + "github.com/braydonk/yaml" ) // Manages k8s configurations available to the k8s CLI diff --git a/cli/azd/pkg/tools/kubectl/models_test.go b/cli/azd/pkg/tools/kubectl/models_test.go index d8d3966b952..6d447c10d25 100644 --- a/cli/azd/pkg/tools/kubectl/models_test.go +++ b/cli/azd/pkg/tools/kubectl/models_test.go @@ -6,8 +6,8 @@ import ( "os" "testing" + "github.com/braydonk/yaml" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" ) func Test_Port_TargetPort_Unmarshalling(t *testing.T) { diff --git a/cli/azd/pkg/tools/kubectl/util.go b/cli/azd/pkg/tools/kubectl/util.go index 4593eaf5512..68802401ec1 100644 --- a/cli/azd/pkg/tools/kubectl/util.go +++ b/cli/azd/pkg/tools/kubectl/util.go @@ -7,8 +7,8 @@ import ( "fmt" "time" + "github.com/braydonk/yaml" "github.com/sethvargo/go-retry" - "gopkg.in/yaml.v3" ) var ( diff --git a/cli/azd/pkg/workflow/config_test.go b/cli/azd/pkg/workflow/config_test.go index 2c6aecd21ef..78294d30266 100644 --- a/cli/azd/pkg/workflow/config_test.go +++ b/cli/azd/pkg/workflow/config_test.go @@ -4,8 +4,8 @@ import ( "testing" "github.com/MakeNowJust/heredoc/v2" + "github.com/braydonk/yaml" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" ) var testWorkflow = &Workflow{ diff --git a/cli/azd/pkg/workflow/workflow.go b/cli/azd/pkg/workflow/workflow.go index 86a7111f291..ed97ec9fdc8 100644 --- a/cli/azd/pkg/workflow/workflow.go +++ b/cli/azd/pkg/workflow/workflow.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "gopkg.in/yaml.v3" + "github.com/braydonk/yaml" ) // Workflow stores a list of steps to execute diff --git a/cli/azd/resources/scaffold/templates/main.bicept b/cli/azd/resources/scaffold/templates/main.bicept index 9e1e149c126..e31f3bc4cbb 100644 --- a/cli/azd/resources/scaffold/templates/main.bicept +++ b/cli/azd/resources/scaffold/templates/main.bicept @@ -164,17 +164,17 @@ module serviceBus './app/azure-service-bus.bicep' = { module {{bicepName .Name}} './app/{{.Name}}.bicep' = { name: '{{.Name}}' params: { - name: '${abbrs.appContainerApps}{{containerAppInfix .Name}}-${resourceToken}' + name: '{{containerAppName .Name}}' location: location tags: tags - identityName: '${abbrs.managedIdentityUserAssignedIdentities}{{containerAppInfix .Name}}-${resourceToken}' + identityName: '${abbrs.managedIdentityUserAssignedIdentities}{{.Name}}-${resourceToken}' applicationInsightsName: monitoring.outputs.applicationInsightsName containerAppsEnvironmentName: appsEnv.outputs.name containerRegistryName: registry.outputs.name exists: {{bicepName .Name}}Exists appDefinition: {{bicepName .Name}}Definition {{- if .DbRedis}} - redisName: 'rd-{{containerAppInfix .Name}}-${resourceToken}' + redisName: 'rd-{{containerAppName .Name}}' {{- end}} {{- if .DbCosmosMongo}} cosmosDbConnectionString: vault.getSecret(cosmosDb.outputs.connectionStringKey) @@ -216,7 +216,7 @@ module {{bicepName .Name}} './app/{{.Name}}.bicep' = { {{- if (and .Backend .Backend.Frontends)}} allowedOrigins: [ {{- range .Backend.Frontends}} - 'https://${abbrs.appContainerApps}{{containerAppInfix .Name}}-${resourceToken}.${appsEnv.outputs.domain}' + 'https://{{containerAppName .Name}}.${appsEnv.outputs.domain}' {{- end}} ] {{- end}} diff --git a/cli/azd/test/cmdrecord/cassette.go b/cli/azd/test/cmdrecord/cassette.go index 306d4da0d18..212f44bad84 100644 --- a/cli/azd/test/cmdrecord/cassette.go +++ b/cli/azd/test/cmdrecord/cassette.go @@ -10,7 +10,7 @@ import ( "path/filepath" "strconv" - "gopkg.in/yaml.v3" + "github.com/braydonk/yaml" ) const InteractionIdFile = "int-id.txt" diff --git a/cli/azd/test/cmdrecord/cmdrecorder_test.go b/cli/azd/test/cmdrecord/cmdrecorder_test.go index 72d844de0f6..f78427cbe8f 100644 --- a/cli/azd/test/cmdrecord/cmdrecorder_test.go +++ b/cli/azd/test/cmdrecord/cmdrecorder_test.go @@ -10,9 +10,9 @@ import ( "path/filepath" "testing" + "github.com/braydonk/yaml" "github.com/stretchr/testify/require" "gopkg.in/dnaeon/go-vcr.v3/recorder" - "gopkg.in/yaml.v3" ) // Verify that record + playback work together. diff --git a/cli/azd/test/cmdrecord/proxy/main.go b/cli/azd/test/cmdrecord/proxy/main.go index 3953e5e9bff..7ade8315ebe 100644 --- a/cli/azd/test/cmdrecord/proxy/main.go +++ b/cli/azd/test/cmdrecord/proxy/main.go @@ -13,8 +13,8 @@ import ( "strings" "github.com/azure/azure-dev/cli/azd/test/cmdrecord" + "github.com/braydonk/yaml" "gopkg.in/dnaeon/go-vcr.v3/recorder" - "gopkg.in/yaml.v3" ) type ErrExitCode struct { diff --git a/cli/azd/test/functional/experiment_test.go b/cli/azd/test/functional/experiment_test.go index 59d12f13547..6731f27923c 100644 --- a/cli/azd/test/functional/experiment_test.go +++ b/cli/azd/test/functional/experiment_test.go @@ -19,6 +19,8 @@ import ( // Verifies that the assignment context returned is included in the telemetry events we capture. func Test_CLI_Experiment_AssignmentContextInTelemetry(t *testing.T) { + t.Skip("Skipping while experimentation is not enabled") + // CLI process and working directory are isolated t.Parallel() ctx, cancel := newTestContext(t) diff --git a/cli/azd/test/recording/recording.go b/cli/azd/test/recording/recording.go index 12fa8b5fd43..8d0a54c0b5a 100644 --- a/cli/azd/test/recording/recording.go +++ b/cli/azd/test/recording/recording.go @@ -26,9 +26,9 @@ import ( "time" "github.com/azure/azure-dev/cli/azd/test/cmdrecord" + "github.com/braydonk/yaml" "gopkg.in/dnaeon/go-vcr.v3/cassette" "gopkg.in/dnaeon/go-vcr.v3/recorder" - "gopkg.in/yaml.v3" ) type recordOptions struct { diff --git a/go.mod b/go.mod index 164e3b7cb42..0dca27b1057 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,7 @@ require ( github.com/blang/semver/v4 v4.0.0 github.com/bmatcuk/doublestar/v4 v4.6.0 github.com/bradleyjkemp/cupaloy/v2 v2.8.0 + github.com/braydonk/yaml v0.7.0 github.com/buger/goterm v1.0.4 github.com/cli/browser v1.1.0 github.com/drone/envsubst v1.0.3 @@ -69,7 +70,6 @@ require ( go.uber.org/multierr v1.8.0 golang.org/x/sys v0.21.0 gopkg.in/dnaeon/go-vcr.v3 v3.1.2 - gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -106,4 +106,5 @@ require ( google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect google.golang.org/grpc v1.56.3 // indirect google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 1bfc6810097..aa2cd325a7c 100644 --- a/go.sum +++ b/go.sum @@ -145,6 +145,8 @@ github.com/bmatcuk/doublestar/v4 v4.6.0 h1:HTuxyug8GyFbRkrffIpzNCSK4luc0TY3wzXvz github.com/bmatcuk/doublestar/v4 v4.6.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= +github.com/braydonk/yaml v0.7.0 h1:ySkqO7r0MGoCNhiRJqE0Xe9yhINMyvOAB3nFjgyJn2k= +github.com/braydonk/yaml v0.7.0/go.mod h1:hcm3h581tudlirk8XEUPDBAimBPbmnL0Y45hCRl47N4= github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY= github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE= github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= diff --git a/schemas/alpha/azure.yaml.json b/schemas/alpha/azure.yaml.json index f8d1f575d7f..f846f3f1fd5 100644 --- a/schemas/alpha/azure.yaml.json +++ b/schemas/alpha/azure.yaml.json @@ -35,7 +35,10 @@ "type": "object", "title": "The infrastructure configuration used for the application", "description": "Optional. Provides additional configuration for Azure infrastructure provisioning.", - "additionalProperties": true, + "additionalProperties": false, + "required": [ + "provider" + ], "properties": { "provider": { "type": "string", @@ -55,8 +58,29 @@ "type": "string", "title": "Name of the default module within the Azure provisioning templates", "description": "Optional. The name of the Azure provisioning module used when provisioning resources. (Default: main)" + }, + "deploymentStacks": { + "$ref": "#/definitions/deploymentStacksConfig" } - } + }, + "allOf": [ + { + "if": { + "not": { + "properties": { + "provider": { + "const": "bicep" + } + } + } + }, + "then": { + "properties": { + "deploymentStacks": false + } + } + } + ] }, "services": { "type": "object", @@ -1052,6 +1076,89 @@ "required": [ "deployment" ] + }, + "deploymentStacksConfig": { + "type": "object", + "title": "The deployment stack configuration used for the project.", + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "actionOnUnmanage" + ] + }, + { + "required": [ + "denySettings" + ] + } + ], + "properties": { + "actionOnUnmanage": { + "type": "object", + "title": "The action to take when when resources become unmanaged", + "description": "Defines the behavior of resources that are no longer managed after the Deployment stack is updated or deleted. Defaults to 'delete' for all resource scopes.", + "required": [ + "resourceGroups", + "resources" + ], + "properties": { + "resourceGroups": { + "type": "string", + "title": "Required. The action on unmanage setting for resource groups", + "description": "Specifies an action for a newly unmanaged resource. Delete will attempt to delete the resource from Azure. Detach will leave the resource in it's current state.", + "default": "delete", + "enum": [ + "delete", + "detach" + ] + }, + "resources": { + "type": "string", + "title": "Required. The action on unmanage setting for resources", + "description": "Specifies an action for a newly unmanaged resource. Delete will attempt to delete the resource from Azure. Detach will leave the resource in it's current state.", + "default": "delete", + "enum": [ + "delete", + "detach" + ] + } + } + }, + "denySettings": { + "type": "object", + "title": "The deny settings for the deployment stack", + "description": "Defines how resources deployed by the stack are locked. Defaults to 'none'.", + "required": [ + "mode" + ], + "properties": { + "mode": { + "type": "string", + "title": "Required. Mode that defines denied actions.", + "default": "none", + "enum": [ + "none", + "denyDelete", + "denyWriteAndDelete" + ] + }, + "applyToChildScopes": { + "type": "boolean", + "title": "Whether the deny settings apply to child scopes.", + "description": "DenySettings will be applied to child resource scopes of every managed resource with a deny assignment." + }, + "excludedActions": { + "type": "array", + "title": "List of role-based management operations that are excluded from the denySettings." + }, + "excludedPrincipals": { + "type": "array", + "title": "List of Entra ID principal IDs excluded from the lock. Up to 5 principals are permitted." + } + } + } + } } } } \ No newline at end of file From f8a10da2b64bfbda0246f11044cd07792303c547 Mon Sep 17 00:00:00 2001 From: Xiaolu Dai <31124698+saragluna@users.noreply.github.com> Date: Fri, 25 Oct 2024 15:50:43 +0800 Subject: [PATCH 41/92] remove the orchestrate command (#2) --- cli/azd/cmd/orchestrate.go | 96 -------------------------------------- cli/azd/cmd/root.go | 15 +----- 2 files changed, 1 insertion(+), 110 deletions(-) delete mode 100644 cli/azd/cmd/orchestrate.go diff --git a/cli/azd/cmd/orchestrate.go b/cli/azd/cmd/orchestrate.go deleted file mode 100644 index d7413f93e24..00000000000 --- a/cli/azd/cmd/orchestrate.go +++ /dev/null @@ -1,96 +0,0 @@ -// 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" - "path/filepath" -) - -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() - - 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 { - 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"), - ), - }) -} - -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 -} diff --git a/cli/azd/cmd/root.go b/cli/azd/cmd/root.go index 521fdd8ab4f..e397aa61fc4 100644 --- a/cli/azd/cmd/root.go +++ b/cli/azd/cmd/root.go @@ -177,20 +177,7 @@ func NewRootCmd( Command: logout, 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 a6e2da5763421581580529578ed31f9a4904e804 Mon Sep 17 00:00:00 2001 From: Xiaolu Dai Date: Mon, 28 Oct 2024 21:37:51 +0800 Subject: [PATCH 42/92] fix ut --- cli/azd/internal/repository/infra_confirm_test.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cli/azd/internal/repository/infra_confirm_test.go b/cli/azd/internal/repository/infra_confirm_test.go index 1cd3a28664c..ca9d5b51112 100644 --- a/cli/azd/internal/repository/infra_confirm_test.go +++ b/cli/azd/internal/repository/infra_confirm_test.go @@ -162,11 +162,13 @@ func TestInitializer_infraSpecFromDetect(t *testing.T) { }, }, interactions: []string{ - "myappdb", // fill in db name + "myappdb", // fill in db name + "Use user assigned managed identity", // confirm db authentication }, want: scaffold.InfraSpec{ DbPostgres: &scaffold.DatabasePostgres{ - DatabaseName: "myappdb", + DatabaseName: "myappdb", + AuthUsingManagedIdentity: true, }, Services: []scaffold.ServiceSpec{ { @@ -180,7 +182,8 @@ func TestInitializer_infraSpecFromDetect(t *testing.T) { }, }, DbPostgres: &scaffold.DatabaseReference{ - DatabaseName: "myappdb", + DatabaseName: "myappdb", + AuthUsingManagedIdentity: true, }, }, { From d1cd1513bfbab953ec3f5f93013229f026ac4c9e Mon Sep 17 00:00:00 2001 From: Xiaolu Dai Date: Tue, 29 Oct 2024 09:51:59 +0800 Subject: [PATCH 43/92] fix ut --- cli/azd/test/functional/init_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/azd/test/functional/init_test.go b/cli/azd/test/functional/init_test.go index db81819476a..48d7a46d619 100644 --- a/cli/azd/test/functional/init_test.go +++ b/cli/azd/test/functional/init_test.go @@ -199,6 +199,7 @@ func Test_CLI_Init_From_App(t *testing.T) { "Use code in the current directory\n"+ "Confirm and continue initializing my app\n"+ "appdb\n"+ + "Use user assigned managed identity\n"+ "TESTENV\n", "init", ) From a8f91b1309296faba63b450507f7c9a0ad1fe1a7 Mon Sep 17 00:00:00 2001 From: Rujun Chen <949800722@qq.com> Date: Tue, 29 Oct 2024 17:21:45 +0800 Subject: [PATCH 44/92] Support detecting event hubs by analyzing pom.xml and application.yml (#3) * Support detect Azure Event Hubs: produce message only, managed identity only. * Support detect Azure Event Hubs: produce message only. Try to connect by connection string, but failed: Cant not get connection string. Issue created: https://github.com/Azure/bicep-registry-modules/issues/3638 * Support detect Azure Event Hubs: produce message only, support both managed-identity and connection-string. * Change option from "Password" to "Connection string". * Rename "getAuthTypeByPrompt" to "chooseAuthType". --- cli/azd/internal/appdetect/appdetect.go | 8 ++ cli/azd/internal/appdetect/java.go | 12 +++ cli/azd/internal/repository/app_init.go | 1 + cli/azd/internal/repository/infra_confirm.go | 54 +++++++----- cli/azd/internal/scaffold/spec.go | 11 +++ ...ent-hubs-namespace-connection-string.bicep | 20 +++++ .../resources/scaffold/templates/main.bicept | 3 + .../scaffold/templates/resources.bicept | 84 +++++++++++++++++++ 8 files changed, 172 insertions(+), 21 deletions(-) create mode 100644 cli/azd/resources/scaffold/base/modules/set-event-hubs-namespace-connection-string.bicep diff --git a/cli/azd/internal/appdetect/appdetect.go b/cli/azd/internal/appdetect/appdetect.go index d01a8b6f1ce..bb8053061a3 100644 --- a/cli/azd/internal/appdetect/appdetect.go +++ b/cli/azd/internal/appdetect/appdetect.go @@ -145,6 +145,14 @@ func (a AzureDepServiceBus) ResourceDisplay() string { return "Azure Service Bus" } +type AzureDepEventHubs struct { + Names []string +} + +func (a AzureDepEventHubs) ResourceDisplay() string { + return "Azure Event Hubs" +} + type Project struct { // The language associated with the project. Language Language diff --git a/cli/azd/internal/appdetect/java.go b/cli/azd/internal/appdetect/java.go index be9989a0baf..6d7bbd4dd05 100644 --- a/cli/azd/internal/appdetect/java.go +++ b/cli/azd/internal/appdetect/java.go @@ -178,6 +178,18 @@ func detectDependencies(mavenProject *mavenProject, project *Project) (*Project, Queues: destinations, }) } + + if dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-stream-binder-eventhubs" { + bindingDestinations := findBindingDestinations(applicationProperties) + destinations := make([]string, 0, len(bindingDestinations)) + for bindingName, destination := range bindingDestinations { + destinations = append(destinations, destination) + log.Printf("Event Hubs [%s] found for binding [%s]", destination, bindingName) + } + project.AzureDeps = append(project.AzureDeps, AzureDepEventHubs{ + Names: destinations, + }) + } } if len(databaseDepMap) > 0 { diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index ef5e2f5e9f6..f4b098b4440 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -41,6 +41,7 @@ var dbMap = map[appdetect.DatabaseDep]struct{}{ var azureDepMap = map[string]struct{}{ appdetect.AzureDepServiceBus{}.ResourceDisplay(): {}, + appdetect.AzureDepEventHubs{}.ResourceDisplay(): {}, } // InitFromApp initializes the infra directory and project file from the current existing app. diff --git a/cli/azd/internal/repository/infra_confirm.go b/cli/azd/internal/repository/infra_confirm.go index 35550c8de11..f41f55029e6 100644 --- a/cli/azd/internal/repository/infra_confirm.go +++ b/cli/azd/internal/repository/infra_confirm.go @@ -212,6 +212,8 @@ func (i *Initializer) infraSpecFromDetect( switch azureDep.(type) { case appdetect.AzureDepServiceBus: serviceSpec.AzureServiceBus = spec.AzureServiceBus + case appdetect.AzureDepEventHubs: + serviceSpec.AzureEventHubs = spec.AzureEventHubs } } spec.Services = append(spec.Services, serviceSpec) @@ -344,41 +346,51 @@ 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.", - }) + authType, err := i.chooseAuthType(ctx, azureDepName) 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, AuthUsingConnectionString: authType == scaffold.AuthType_PASSWORD, AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, } + case appdetect.AzureDepEventHubs: + authType, err := i.chooseAuthType(ctx, azureDepName) + if err != nil { + return err + } + spec.AzureEventHubs = &scaffold.AzureDepEventHubs{ + Name: azureDepName, + EventHubNames: azureDep.(appdetect.AzureDepEventHubs).Names, + AuthUsingConnectionString: authType == scaffold.AuthType_PASSWORD, + AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, + } break azureDepPrompt } break azureDepPrompt } return nil } + +func (i *Initializer) chooseAuthType(ctx context.Context, serviceName string) (scaffold.AuthType, error) { + portOptions := []string{ + "User assigned managed identity", + "Connection string", + } + selection, err := i.console.Select(ctx, input.ConsoleOptions{ + Message: "Choose auth type for '" + serviceName + "'?", + Options: portOptions, + }) + if err != nil { + return scaffold.AUTH_TYPE_UNSPECIFIED, err + } + if selection == 0 { + return scaffold.AuthType_TOKEN_CREDENTIAL, nil + } else { + return scaffold.AuthType_PASSWORD, nil + } +} diff --git a/cli/azd/internal/scaffold/spec.go b/cli/azd/internal/scaffold/spec.go index 44664e1918e..cb8b7d921a3 100644 --- a/cli/azd/internal/scaffold/spec.go +++ b/cli/azd/internal/scaffold/spec.go @@ -17,6 +17,8 @@ type InfraSpec struct { // Azure Service Bus AzureServiceBus *AzureDepServiceBus + // Azure EventHubs + AzureEventHubs *AzureDepEventHubs } type Parameter struct { @@ -55,6 +57,13 @@ type AzureDepServiceBus struct { AuthUsingManagedIdentity bool } +type AzureDepEventHubs struct { + Name string + EventHubNames []string + AuthUsingConnectionString bool + AuthUsingManagedIdentity bool +} + // AuthType defines different authentication types. type AuthType int32 @@ -84,6 +93,8 @@ type ServiceSpec struct { // Azure Service Bus AzureServiceBus *AzureDepServiceBus + // Azure Service Bus + AzureEventHubs *AzureDepEventHubs } type Frontend struct { diff --git a/cli/azd/resources/scaffold/base/modules/set-event-hubs-namespace-connection-string.bicep b/cli/azd/resources/scaffold/base/modules/set-event-hubs-namespace-connection-string.bicep new file mode 100644 index 00000000000..82d13f9a83d --- /dev/null +++ b/cli/azd/resources/scaffold/base/modules/set-event-hubs-namespace-connection-string.bicep @@ -0,0 +1,20 @@ +param eventHubsNamespaceName string +param connectionStringSecretName string +param keyVaultName string + +resource eventHubsNamespace 'Microsoft.EventHub/namespaces@2024-01-01' existing = { + name: eventHubsNamespaceName +} + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { + name: keyVaultName +} + +resource connectionStringSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { + name: connectionStringSecretName + parent: keyVault + properties: { + value: listKeys(concat(resourceId('Microsoft.EventHub/namespaces', eventHubsNamespaceName), '/AuthorizationRules/RootManageSharedAccessKey'), '2024-01-01').primaryConnectionString + } +} + diff --git a/cli/azd/resources/scaffold/templates/main.bicept b/cli/azd/resources/scaffold/templates/main.bicept index 6a574ab55e1..527b5d08c48 100644 --- a/cli/azd/resources/scaffold/templates/main.bicept +++ b/cli/azd/resources/scaffold/templates/main.bicept @@ -59,4 +59,7 @@ output AZURE_CACHE_REDIS_ID string = resources.outputs.AZURE_CACHE_REDIS_ID {{- if .DbPostgres}} output AZURE_POSTGRES_FLEXIBLE_SERVER_ID string = resources.outputs.AZURE_POSTGRES_FLEXIBLE_SERVER_ID {{- end}} +{{- if .AzureEventHubs }} +output AZURE_EVENT_HUBS_ID string = resources.outputs.AZURE_EVENT_HUBS_ID +{{- end}} {{ end}} diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index 7d39c83c534..5d1ab06e69b 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -126,7 +126,47 @@ module postgreServer 'br/public:avm/res/db-for-postgre-sql/flexible-server:0.1.4 } } {{- end}} +{{- if .AzureEventHubs }} +module eventHubNamespace 'br/public:avm/res/event-hub/namespace:0.7.1' = { + name: 'eventHubNamespace' + params: { + name: '${abbrs.eventHubNamespaces}${resourceToken}' + location: location + roleAssignments: [ + {{- if (and .AzureEventHubs .AzureEventHubs.AuthUsingManagedIdentity) }} + {{- range .Services}} + { + principalId: {{bicepName .Name}}Identity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f526a384-b230-433a-b45c-95f59c4a2dec') + } + {{- end}} + {{- end}} + ] + {{- if (and .AzureEventHubs .AzureEventHubs.AuthUsingConnectionString) }} + disableLocalAuth: false + {{- end}} + eventhubs: [ + {{- range $eventHubName := .AzureEventHubs.EventHubNames}} + { + name: '{{ $eventHubName }}' + } + {{- end}} + ] + } +} +{{- if (and .AzureEventHubs .AzureEventHubs.AuthUsingConnectionString) }} +module eventHubsConnectionString './modules/set-event-hubs-namespace-connection-string.bicep' = { + name: 'eventHubsConnectionString' + params: { + eventHubsNamespaceName: eventHubNamespace.outputs.name + connectionStringSecretName: 'EVENT-HUBS-CONNECTION-STRING' + keyVaultName: keyVault.outputs.name + } +} +{{end}} +{{end}} {{- range .Services}} module {{bicepName .Name}}Identity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { @@ -205,6 +245,13 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { keyVaultUrl: '${keyVault.outputs.uri}secrets/REDIS-URL' } {{- end}} + {{- if (and .AzureEventHubs .AzureEventHubs.AuthUsingConnectionString) }} + { + name: 'event-hubs-connection-string' + identity:{{bicepName .Name}}Identity.outputs.resourceId + keyVaultUrl: '${keyVault.outputs.uri}secrets/EVENT-HUBS-CONNECTION-STRING' + } + {{- end}} ], map({{bicepName .Name}}Secrets, secret => { name: secret.secretRef @@ -282,6 +329,40 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { secretRef: 'redis-pass' } {{- end}} + {{- if .AzureEventHubs }} + { + name: 'SPRING_CLOUD_AZURE_EVENTHUBS_NAMESPACE' + value: eventHubNamespace.outputs.name + } + {{- end}} + {{- if (and .AzureEventHubs .AzureEventHubs.AuthUsingManagedIdentity) }} + { + name: 'SPRING_CLOUD_AZURE_EVENTHUBS_CONNECTION_STRING' + value: '' + } + { + name: 'SPRING_CLOUD_AZURE_EVENTHUBS_CREDENTIAL_MANAGEDIDENTITYENABLED' + value: 'true' + } + { + name: 'SPRING_CLOUD_AZURE_EVENTHUBS_CREDENTIAL_CLIENTID' + value: {{bicepName .Name}}Identity.outputs.clientId + } + {{- end}} + {{- if (and .AzureEventHubs .AzureEventHubs.AuthUsingConnectionString) }} + { + name: 'SPRING_CLOUD_AZURE_EVENTHUBS_CONNECTION_STRING' + secretRef: 'event-hubs-connection-string' + } + { + name: 'SPRING_CLOUD_AZURE_EVENTHUBS_CREDENTIAL_MANAGEDIDENTITYENABLED' + value: 'false' + } + { + name: 'SPRING_CLOUD_AZURE_EVENTHUBS_CREDENTIAL_CLIENTID' + value: '' + } + {{- end}} {{- if .Frontend}} {{- range $i, $e := .Frontend.Backends}} { @@ -392,4 +473,7 @@ output AZURE_CACHE_REDIS_ID string = redis.outputs.resourceId {{- if .DbPostgres}} output AZURE_POSTGRES_FLEXIBLE_SERVER_ID string = postgreServer.outputs.resourceId {{- end}} +{{- if .AzureEventHubs }} +output AZURE_EVENT_HUBS_ID string = eventHubNamespace.outputs.resourceId +{{- end}} {{ end}} From d992c707cc090291bfc1a48504f7c29117ae486f Mon Sep 17 00:00:00 2001 From: Rujun Chen <949800722@qq.com> Date: Wed, 30 Oct 2024 17:56:13 +0800 Subject: [PATCH 45/92] Support detect Azure Event Hubs - 2 (#6) --- cli/azd/.vscode/cspell.yaml | 7 +++ cli/azd/cmd/root.go | 2 +- cli/azd/internal/appdetect/appdetect.go | 8 +++ cli/azd/internal/appdetect/java.go | 36 ++++++++++-- cli/azd/internal/repository/app_init.go | 5 +- cli/azd/internal/repository/infra_confirm.go | 55 ++++++++----------- cli/azd/internal/scaffold/spec.go | 21 ++++--- .../scaffold/templates/resources.bicept | 51 +++++++++++++++++ 8 files changed, 137 insertions(+), 48 deletions(-) diff --git a/cli/azd/.vscode/cspell.yaml b/cli/azd/.vscode/cspell.yaml index 9364122e8f6..e886a88f93c 100644 --- a/cli/azd/.vscode/cspell.yaml +++ b/cli/azd/.vscode/cspell.yaml @@ -35,6 +35,13 @@ overrides: - cloudapp - mediaservices - msecnd + - filename: internal/tracing/fields/fields.go + words: + - azuredeps + - filename: internal/appdetect/java.go + words: + - springframework + - eventhubs - filename: docs/docgen.go words: - alexwolf diff --git a/cli/azd/cmd/root.go b/cli/azd/cmd/root.go index e397aa61fc4..14c98cb7beb 100644 --- a/cli/azd/cmd/root.go +++ b/cli/azd/cmd/root.go @@ -177,7 +177,7 @@ func NewRootCmd( Command: logout, ActionResolver: newLogoutAction, }) - + root.Add("init", &actions.ActionDescriptorOptions{ Command: newInitCmd(), FlagsResolver: newInitFlags, diff --git a/cli/azd/internal/appdetect/appdetect.go b/cli/azd/internal/appdetect/appdetect.go index bb8053061a3..aff6c7a3328 100644 --- a/cli/azd/internal/appdetect/appdetect.go +++ b/cli/azd/internal/appdetect/appdetect.go @@ -153,6 +153,14 @@ func (a AzureDepEventHubs) ResourceDisplay() string { return "Azure Event Hubs" } +type AzureDepStorageAccount struct { + ContainerNames []string +} + +func (a AzureDepStorageAccount) ResourceDisplay() string { + return "Azure Storage Account" +} + type Project struct { // The language associated with the project. Language Language diff --git a/cli/azd/internal/appdetect/java.go b/cli/azd/internal/appdetect/java.go index 6d7bbd4dd05..71ce8936335 100644 --- a/cli/azd/internal/appdetect/java.go +++ b/cli/azd/internal/appdetect/java.go @@ -127,9 +127,11 @@ func readMavenProject(filePath string) (*mavenProject, error) { func detectDependencies(mavenProject *mavenProject, project *Project) (*Project, error) { // how can we tell it's a Spring Boot project? // 1. It has a parent with a groupId of org.springframework.boot and an artifactId of spring-boot-starter-parent - // 2. It has a dependency with a groupId of org.springframework.boot and an artifactId that starts with spring-boot-starter + // 2. It has a dependency with a groupId of org.springframework.boot and an artifactId that starts with + // spring-boot-starter isSpringBoot := false - if mavenProject.Parent.GroupId == "org.springframework.boot" && mavenProject.Parent.ArtifactId == "spring-boot-starter-parent" { + if mavenProject.Parent.GroupId == "org.springframework.boot" && + mavenProject.Parent.ArtifactId == "spring-boot-starter-parent" { isSpringBoot = true } for _, dep := range mavenProject.Dependencies { @@ -181,14 +183,26 @@ func detectDependencies(mavenProject *mavenProject, project *Project) (*Project, if dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-stream-binder-eventhubs" { bindingDestinations := findBindingDestinations(applicationProperties) - destinations := make([]string, 0, len(bindingDestinations)) + var destinations []string + containsInBinding := false for bindingName, destination := range bindingDestinations { - destinations = append(destinations, destination) - log.Printf("Event Hubs [%s] found for binding [%s]", destination, bindingName) + if strings.Contains(bindingName, "-in-") { // Example: consume-in-0 + containsInBinding = true + } + if !contains(destinations, destination) { + destinations = append(destinations, destination) + log.Printf("Event Hubs [%s] found for binding [%s]", destination, bindingName) + } } project.AzureDeps = append(project.AzureDeps, AzureDepEventHubs{ Names: destinations, }) + if containsInBinding { + project.AzureDeps = append(project.AzureDeps, AzureDepStorageAccount{ + ContainerNames: []string{ + applicationProperties["spring.cloud.azure.eventhubs.processor.checkpoint-store.container-name"]}, + }) + } } } @@ -210,7 +224,8 @@ func readProperties(projectPath string) map[string]string { readPropertiesInYamlFile(filepath.Join(projectPath, "/src/main/resources/application.yaml"), result) profile, profileSet := result["spring.profiles.active"] if profileSet { - readPropertiesInPropertiesFile(filepath.Join(projectPath, "/src/main/resources/application-"+profile+".properties"), result) + readPropertiesInPropertiesFile( + filepath.Join(projectPath, "/src/main/resources/application-"+profile+".properties"), result) readPropertiesInYamlFile(filepath.Join(projectPath, "/src/main/resources/application-"+profile+".yml"), result) readPropertiesInYamlFile(filepath.Join(projectPath, "/src/main/resources/application-"+profile+".yaml"), result) } @@ -321,3 +336,12 @@ func findBindingDestinations(properties map[string]string) map[string]string { return result } + +func contains(array []string, str string) bool { + for _, v := range array { + if v == str { + return true + } + } + return false +} diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index f4b098b4440..2ea0c0b942c 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -40,8 +40,9 @@ var dbMap = map[appdetect.DatabaseDep]struct{}{ } var azureDepMap = map[string]struct{}{ - appdetect.AzureDepServiceBus{}.ResourceDisplay(): {}, - appdetect.AzureDepEventHubs{}.ResourceDisplay(): {}, + appdetect.AzureDepServiceBus{}.ResourceDisplay(): {}, + appdetect.AzureDepEventHubs{}.ResourceDisplay(): {}, + appdetect.AzureDepStorageAccount{}.ResourceDisplay(): {}, } // InitFromApp initializes the infra directory and project file from the current existing app. diff --git a/cli/azd/internal/repository/infra_confirm.go b/cli/azd/internal/repository/infra_confirm.go index f41f55029e6..342b2815ffa 100644 --- a/cli/azd/internal/repository/infra_confirm.go +++ b/cli/azd/internal/repository/infra_confirm.go @@ -214,6 +214,8 @@ func (i *Initializer) infraSpecFromDetect( serviceSpec.AzureServiceBus = spec.AzureServiceBus case appdetect.AzureDepEventHubs: serviceSpec.AzureEventHubs = spec.AzureEventHubs + case appdetect.AzureDepStorageAccount: + serviceSpec.AzureStorageAccount = spec.AzureStorageAccount } } spec.Services = append(spec.Services, serviceSpec) @@ -346,26 +348,36 @@ azureDepPrompt: } } + authType := scaffold.AuthType(0) switch azureDep.(type) { case appdetect.AzureDepServiceBus: - authType, err := i.chooseAuthType(ctx, azureDepName) + _authType, err := i.console.Prompt(ctx, input.ConsoleOptions{ + Message: fmt.Sprintf("Input the authentication type you want for (%s), "+ + "1 for connection string, 2 for managed identity", azureDep.ResourceDisplay()), + Help: "Authentication type:\n\n" + + "Enter 1 if you want to use connection string to connect to the Service Bus.\n" + + "Enter 2 if you want to use user assigned managed identity to connect to the Service Bus.", + }) if err != nil { return err } - spec.AzureServiceBus = &scaffold.AzureDepServiceBus{ - Name: azureDepName, - Queues: azureDep.(appdetect.AzureDepServiceBus).Queues, - AuthUsingConnectionString: authType == scaffold.AuthType_PASSWORD, - AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, + + if _authType != "1" && _authType != "2" { + i.console.Message(ctx, "Invalid authentication type. Please enter 0 or 1.") + continue azureDepPrompt } - case appdetect.AzureDepEventHubs: - authType, err := i.chooseAuthType(ctx, azureDepName) - if err != nil { - return err + if _authType == "1" { + authType = scaffold.AuthType_PASSWORD + } else { + authType = scaffold.AuthType_TOKEN_CREDENTIAL } - spec.AzureEventHubs = &scaffold.AzureDepEventHubs{ + } + + switch dependency := azureDep.(type) { + case appdetect.AzureDepServiceBus: + spec.AzureServiceBus = &scaffold.AzureDepServiceBus{ Name: azureDepName, - EventHubNames: azureDep.(appdetect.AzureDepEventHubs).Names, + Queues: dependency.Queues, AuthUsingConnectionString: authType == scaffold.AuthType_PASSWORD, AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, } @@ -375,22 +387,3 @@ azureDepPrompt: } return nil } - -func (i *Initializer) chooseAuthType(ctx context.Context, serviceName string) (scaffold.AuthType, error) { - portOptions := []string{ - "User assigned managed identity", - "Connection string", - } - selection, err := i.console.Select(ctx, input.ConsoleOptions{ - Message: "Choose auth type for '" + serviceName + "'?", - Options: portOptions, - }) - if err != nil { - return scaffold.AUTH_TYPE_UNSPECIFIED, err - } - if selection == 0 { - return scaffold.AuthType_TOKEN_CREDENTIAL, nil - } else { - return scaffold.AuthType_PASSWORD, nil - } -} diff --git a/cli/azd/internal/scaffold/spec.go b/cli/azd/internal/scaffold/spec.go index cb8b7d921a3..7d60cb22680 100644 --- a/cli/azd/internal/scaffold/spec.go +++ b/cli/azd/internal/scaffold/spec.go @@ -15,10 +15,9 @@ type InfraSpec struct { DbCosmosMongo *DatabaseCosmosMongo DbRedis *DatabaseRedis - // Azure Service Bus - AzureServiceBus *AzureDepServiceBus - // Azure EventHubs - AzureEventHubs *AzureDepEventHubs + AzureServiceBus *AzureDepServiceBus + AzureEventHubs *AzureDepEventHubs + AzureStorageAccount *AzureDepStorageAccount } type Parameter struct { @@ -64,6 +63,13 @@ type AzureDepEventHubs struct { AuthUsingManagedIdentity bool } +type AzureDepStorageAccount struct { + Name string + ContainerNames []string + AuthUsingConnectionString bool + AuthUsingManagedIdentity bool +} + // AuthType defines different authentication types. type AuthType int32 @@ -91,10 +97,9 @@ type ServiceSpec struct { DbCosmosMongo *DatabaseReference DbRedis *DatabaseReference - // Azure Service Bus - AzureServiceBus *AzureDepServiceBus - // Azure Service Bus - AzureEventHubs *AzureDepEventHubs + AzureServiceBus *AzureDepServiceBus + AzureEventHubs *AzureDepEventHubs + AzureStorageAccount *AzureDepStorageAccount } type Frontend struct { diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index 5d1ab06e69b..b1b655333ee 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -167,6 +167,41 @@ module eventHubsConnectionString './modules/set-event-hubs-namespace-connection- } {{end}} {{end}} +{{- if .AzureStorageAccount }} +var storageAccountName = '${abbrs.storageStorageAccounts}${resourceToken}' +module storageAccount 'br/public:avm/res/storage/storage-account:0.14.3' = { + name: 'storageAccount' + params: { + name: storageAccountName + publicNetworkAccess: 'Enabled' + blobServices: { + containers: [ + {{- range $index, $element := .AzureStorageAccount.ContainerNames}} + { + name: '{{ $element }}' + } + {{- end}} + ] + } + location: location + roleAssignments: [ + {{- if (and .AzureStorageAccount .AzureStorageAccount.AuthUsingManagedIdentity) }} + {{- range .Services}} + { + principalId: {{bicepName .Name}}Identity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b') + } + {{- end}} + {{- end}} + ] + networkAcls: { + defaultAction: 'Allow' + } + tags: tags + } +} +{{end}} {{- range .Services}} module {{bicepName .Name}}Identity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { @@ -363,6 +398,22 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { value: '' } {{- end}} + {{- if .AzureStorageAccount }} + { + name: 'SPRING_CLOUD_AZURE_EVENTHUBS_PROCESSOR_CHECKPOINTSTORE_ACCOUNTNAME' + value: storageAccountName + } + {{- end}} + {{- if (and .AzureStorageAccount .AzureStorageAccount.AuthUsingManagedIdentity) }} + { + name: 'SPRING_CLOUD_AZURE_STORAGE_CREDENTIAL_MANAGEDIDENTITYENABLED' + value: 'true' + } + { + name: 'SPRING_CLOUD_AZURE_STORAGE_CREDENTIAL_CLIENTID' + value: {{bicepName .Name}}Identity.outputs.clientId + } + {{- end}} {{- if .Frontend}} {{- range $i, $e := .Frontend.Backends}} { From ef8ebe205144c2d5c1541b1fa303e2eef5b4f82a Mon Sep 17 00:00:00 2001 From: Xiaolu Dai <31124698+saragluna@users.noreply.github.com> Date: Wed, 30 Oct 2024 21:43:14 +0800 Subject: [PATCH 46/92] switch to service bus avm (#7) --- ...rvicebus-namespace-connection-string.bicep | 20 ++++ .../templates/azure-service-bus.bicept | 60 ------------ .../scaffold/templates/resources.bicept | 96 ++++++++++++++++++- 3 files changed, 114 insertions(+), 62 deletions(-) create mode 100644 cli/azd/resources/scaffold/base/modules/set-servicebus-namespace-connection-string.bicep delete mode 100644 cli/azd/resources/scaffold/templates/azure-service-bus.bicept diff --git a/cli/azd/resources/scaffold/base/modules/set-servicebus-namespace-connection-string.bicep b/cli/azd/resources/scaffold/base/modules/set-servicebus-namespace-connection-string.bicep new file mode 100644 index 00000000000..42f0779bb9d --- /dev/null +++ b/cli/azd/resources/scaffold/base/modules/set-servicebus-namespace-connection-string.bicep @@ -0,0 +1,20 @@ +param serviceBusNamespaceName string +param connectionStringSecretName string +param keyVaultName string + +resource serviceBusNamespace 'Microsoft.ServiceBus/namespaces@2022-10-01-preview' existing = { + name: serviceBusNamespaceName +} + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { + name: keyVaultName +} + +resource serviceBusConnectionStringSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { + name: connectionStringSecretName + parent: keyVault + properties: { + value: listKeys(concat(resourceId('Microsoft.ServiceBus/namespaces', serviceBusNamespaceName), '/AuthorizationRules/RootManageSharedAccessKey'), serviceBusNamespace.apiVersion).primaryConnectionString + } +} + diff --git a/cli/azd/resources/scaffold/templates/azure-service-bus.bicept b/cli/azd/resources/scaffold/templates/azure-service-bus.bicept deleted file mode 100644 index 1504934841f..00000000000 --- a/cli/azd/resources/scaffold/templates/azure-service-bus.bicept +++ /dev/null @@ -1,60 +0,0 @@ -{{define "azure-service-bus.bicep" -}} -param serviceBusNamespaceName string -{{- if .AuthUsingConnectionString }} -param keyVaultName string -{{end}} -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}} - -{{- if .AuthUsingConnectionString }} - -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 - } -} -{{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/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index b1b655333ee..a5564fed40f 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -197,11 +197,57 @@ module storageAccount 'br/public:avm/res/storage/storage-account:0.14.3' = { ] networkAcls: { defaultAction: 'Allow' - } + } tags: tags } } {{end}} + +{{- if .AzureServiceBus }} + +module serviceBusNamespace 'br/public:avm/res/service-bus/namespace:0.10.0' = { + name: 'serviceBusNamespace' + params: { + // Required parameters + name: '${abbrs.serviceBusNamespaces}${resourceToken}' + // Non-required parameters + location: location + roleAssignments: [ + {{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingManagedIdentity) }} + {{- range .Services}} + { + principalId: {{bicepName .Name}}Identity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '090c5cfd-751d-490a-894a-3ce6f1109419') + } + {{- end}} + {{- end}} + ] + {{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingConnectionString) }} + disableLocalAuth: false + {{- end}} + queues: [ + {{- range $queue := .AzureServiceBus.Queues}} + { + name: '{{ $queue }}' + } + {{- end}} + ] + } +} + +{{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingConnectionString) }} +module serviceBusConnectionString './modules/set-servicebus-namespace-connection-string.bicep' = { + name: 'serviceBusConnectionString' + params: { + serviceBusNamespaceName: serviceBusNamespace.outputs.name + connectionStringSecretName: 'SERVICEBUS-CONNECTION-STRING' + keyVaultName: keyVault.outputs.name + } +} +{{end}} +{{end}} + {{- range .Services}} module {{bicepName .Name}}Identity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { @@ -234,7 +280,7 @@ var {{bicepName .Name}}Env = map(filter({{bicepName .Name}}AppSettingsArray, i = module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { name: '{{bicepName .Name}}' params: { - name: '{{.Name}}' + name: '{{containerAppName .Name}}' {{- if ne .Port 0}} ingressTargetPort: {{.Port}} {{- end}} @@ -287,6 +333,13 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { keyVaultUrl: '${keyVault.outputs.uri}secrets/EVENT-HUBS-CONNECTION-STRING' } {{- end}} + {{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingConnectionString) }} + { + name: 'servicebus-connection-string' + identity:{{bicepName .Name}}Identity.outputs.resourceId + keyVaultUrl: '${keyVault.outputs.uri}secrets/SERVICEBUS-CONNECTION-STRING' + } + {{- end}} ], map({{bicepName .Name}}Secrets, secret => { name: secret.secretRef @@ -414,6 +467,42 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { value: {{bicepName .Name}}Identity.outputs.clientId } {{- end}} + + {{- if .AzureServiceBus }} + { + name: 'SPRING_CLOUD_AZURE_SERVICEBUS_NAMESPACE' + value: serviceBusNamespace.outputs.name + } + {{- end}} + {{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingManagedIdentity) }} + { + name: 'SPRING_CLOUD_AZURE_SERVICEBUS_CONNECTION_STRING' + value: '' + } + { + name: 'SPRING_CLOUD_AZURE_SERVICEBUS_CREDENTIAL_MANAGEDIDENTITYENABLED' + value: 'true' + } + { + name: 'SPRING_CLOUD_AZURE_SERVICEBUS_CREDENTIAL_CLIENTID' + value: {{bicepName .Name}}Identity.outputs.clientId + } + {{- end}} + {{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingConnectionString) }} + { + name: 'SPRING_CLOUD_AZURE_SERVICEBUS_CONNECTION_STRING' + secretRef: 'servicebus-connection-string' + } + { + name: 'SPRING_CLOUD_AZURE_SERVICEBUS_CREDENTIAL_MANAGEDIDENTITYENABLED' + value: 'false' + } + { + name: 'SPRING_CLOUD_AZURE_EVENTHUBS_CREDENTIAL_CLIENTID' + value: '' + } + {{- end}} + {{- if .Frontend}} {{- range $i, $e := .Frontend.Backends}} { @@ -527,4 +616,7 @@ output AZURE_POSTGRES_FLEXIBLE_SERVER_ID string = postgreServer.outputs.resource {{- if .AzureEventHubs }} output AZURE_EVENT_HUBS_ID string = eventHubNamespace.outputs.resourceId {{- end}} +{{- if .AzureServiceBus }} +output AZURE_SERVICE_BUS_ID string = serviceBusNamespace.outputs.resourceId +{{- end}} {{ end}} From 39fda8ce532b1e55bdc86b33535457e9a40ffd11 Mon Sep 17 00:00:00 2001 From: Rujun Chen <949800722@qq.com> Date: Fri, 1 Nov 2024 14:08:38 +0800 Subject: [PATCH 47/92] Support detect Azure Event Hubs, connect by connection string. (#8) --- cli/azd/internal/repository/infra_confirm.go | 67 ++++++++++++------- ...ent-hubs-namespace-connection-string.bicep | 3 +- ...rvicebus-namespace-connection-string.bicep | 1 - ...et-storage-account-connection-string.bicep | 19 ++++++ .../scaffold/templates/resources.bicept | 50 ++++++++++++-- 5 files changed, 106 insertions(+), 34 deletions(-) create mode 100644 cli/azd/resources/scaffold/base/modules/set-storage-account-connection-string.bicep diff --git a/cli/azd/internal/repository/infra_confirm.go b/cli/azd/internal/repository/infra_confirm.go index 342b2815ffa..eb116dc9a61 100644 --- a/cli/azd/internal/repository/infra_confirm.go +++ b/cli/azd/internal/repository/infra_confirm.go @@ -348,42 +348,61 @@ azureDepPrompt: } } - authType := scaffold.AuthType(0) - switch azureDep.(type) { + switch dependency := 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.", - }) + authType, err := i.chooseAuthType(ctx, azureDepName) 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 dependency := azureDep.(type) { - case appdetect.AzureDepServiceBus: spec.AzureServiceBus = &scaffold.AzureDepServiceBus{ Name: azureDepName, Queues: dependency.Queues, AuthUsingConnectionString: authType == scaffold.AuthType_PASSWORD, AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, } - break azureDepPrompt + case appdetect.AzureDepEventHubs: + authType, err := i.chooseAuthType(ctx, azureDepName) + if err != nil { + return err + } + spec.AzureEventHubs = &scaffold.AzureDepEventHubs{ + Name: azureDepName, + EventHubNames: dependency.Names, + AuthUsingConnectionString: authType == scaffold.AuthType_PASSWORD, + AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, + } + case appdetect.AzureDepStorageAccount: + authType, err := i.chooseAuthType(ctx, azureDepName) + if err != nil { + return err + } + spec.AzureStorageAccount = &scaffold.AzureDepStorageAccount{ + Name: azureDepName, + ContainerNames: dependency.ContainerNames, + AuthUsingConnectionString: authType == scaffold.AuthType_PASSWORD, + AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, + } } break azureDepPrompt } return nil } + +func (i *Initializer) chooseAuthType(ctx context.Context, serviceName string) (scaffold.AuthType, error) { + portOptions := []string{ + "User assigned managed identity", + "Connection string", + } + selection, err := i.console.Select(ctx, input.ConsoleOptions{ + Message: "Choose auth type for '" + serviceName + "'?", + Options: portOptions, + }) + if err != nil { + return scaffold.AUTH_TYPE_UNSPECIFIED, err + } + if selection == 0 { + return scaffold.AuthType_TOKEN_CREDENTIAL, nil + } else { + return scaffold.AuthType_PASSWORD, nil + } +} diff --git a/cli/azd/resources/scaffold/base/modules/set-event-hubs-namespace-connection-string.bicep b/cli/azd/resources/scaffold/base/modules/set-event-hubs-namespace-connection-string.bicep index 82d13f9a83d..7eee8d73cdc 100644 --- a/cli/azd/resources/scaffold/base/modules/set-event-hubs-namespace-connection-string.bicep +++ b/cli/azd/resources/scaffold/base/modules/set-event-hubs-namespace-connection-string.bicep @@ -14,7 +14,6 @@ resource connectionStringSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = name: connectionStringSecretName parent: keyVault properties: { - value: listKeys(concat(resourceId('Microsoft.EventHub/namespaces', eventHubsNamespaceName), '/AuthorizationRules/RootManageSharedAccessKey'), '2024-01-01').primaryConnectionString + value: listKeys(concat(resourceId('Microsoft.EventHub/namespaces', eventHubsNamespaceName), '/AuthorizationRules/RootManageSharedAccessKey'), eventHubsNamespace.apiVersion).primaryConnectionString } } - diff --git a/cli/azd/resources/scaffold/base/modules/set-servicebus-namespace-connection-string.bicep b/cli/azd/resources/scaffold/base/modules/set-servicebus-namespace-connection-string.bicep index 42f0779bb9d..1152b5dcc12 100644 --- a/cli/azd/resources/scaffold/base/modules/set-servicebus-namespace-connection-string.bicep +++ b/cli/azd/resources/scaffold/base/modules/set-servicebus-namespace-connection-string.bicep @@ -17,4 +17,3 @@ resource serviceBusConnectionStringSecret 'Microsoft.KeyVault/vaults/secrets@202 value: listKeys(concat(resourceId('Microsoft.ServiceBus/namespaces', serviceBusNamespaceName), '/AuthorizationRules/RootManageSharedAccessKey'), serviceBusNamespace.apiVersion).primaryConnectionString } } - diff --git a/cli/azd/resources/scaffold/base/modules/set-storage-account-connection-string.bicep b/cli/azd/resources/scaffold/base/modules/set-storage-account-connection-string.bicep new file mode 100644 index 00000000000..2b04668f17b --- /dev/null +++ b/cli/azd/resources/scaffold/base/modules/set-storage-account-connection-string.bicep @@ -0,0 +1,19 @@ +param storageAccountName string +param connectionStringSecretName string +param keyVaultName string + +resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' existing = { + name: storageAccountName +} + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { + name: keyVaultName +} + +resource connectionStringSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { + name: connectionStringSecretName + parent: keyVault + properties: { + value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccount.listKeys().keys[0].value};EndpointSuffix=${environment().suffixes.storage}' + } +} diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index a5564fed40f..247da419c05 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -197,10 +197,21 @@ module storageAccount 'br/public:avm/res/storage/storage-account:0.14.3' = { ] networkAcls: { defaultAction: 'Allow' - } + } tags: tags } } + +{{- if (and .AzureStorageAccount .AzureStorageAccount.AuthUsingConnectionString) }} +module storageAccountConnectionString './modules/set-storage-account-connection-string.bicep' = { + name: 'storageAccountConnectionString' + params: { + storageAccountName: storageAccountName + connectionStringSecretName: 'STORAGE-ACCOUNT-CONNECTION-STRING' + keyVaultName: keyVault.outputs.name + } +} +{{end}} {{end}} {{- if .AzureServiceBus }} @@ -340,6 +351,13 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { keyVaultUrl: '${keyVault.outputs.uri}secrets/SERVICEBUS-CONNECTION-STRING' } {{- end}} + {{- if (and .AzureStorageAccount .AzureStorageAccount.AuthUsingConnectionString) }} + { + name: 'storage-account-connection-string' + identity:{{bicepName .Name}}Identity.outputs.resourceId + keyVaultUrl: '${keyVault.outputs.uri}secrets/STORAGE-ACCOUNT-CONNECTION-STRING' + } + {{- end}} ], map({{bicepName .Name}}Secrets, secret => { name: secret.secretRef @@ -425,7 +443,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { {{- end}} {{- if (and .AzureEventHubs .AzureEventHubs.AuthUsingManagedIdentity) }} { - name: 'SPRING_CLOUD_AZURE_EVENTHUBS_CONNECTION_STRING' + name: 'SPRING_CLOUD_AZURE_EVENTHUBS_CONNECTIONSTRING' value: '' } { @@ -439,7 +457,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { {{- end}} {{- if (and .AzureEventHubs .AzureEventHubs.AuthUsingConnectionString) }} { - name: 'SPRING_CLOUD_AZURE_EVENTHUBS_CONNECTION_STRING' + name: 'SPRING_CLOUD_AZURE_EVENTHUBS_CONNECTIONSTRING' secretRef: 'event-hubs-connection-string' } { @@ -459,14 +477,32 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { {{- end}} {{- if (and .AzureStorageAccount .AzureStorageAccount.AuthUsingManagedIdentity) }} { - name: 'SPRING_CLOUD_AZURE_STORAGE_CREDENTIAL_MANAGEDIDENTITYENABLED' + name: 'SPRING_CLOUD_AZURE_EVENTHUBS_PROCESSOR_CHECKPOINTSTORE_CONNECTIONSTRING' + value: '' + } + { + name: 'SPRING_CLOUD_AZURE_EVENTHUBS_PROCESSOR_CHECKPOINTSTORE_CREDENTIAL_MANAGEDIDENTITYENABLED' value: 'true' } { - name: 'SPRING_CLOUD_AZURE_STORAGE_CREDENTIAL_CLIENTID' + name: 'SPRING_CLOUD_AZURE_EVENTHUBS_PROCESSOR_CHECKPOINTSTORE_CREDENTIAL_CLIENTID' value: {{bicepName .Name}}Identity.outputs.clientId } {{- end}} + {{- if (and .AzureStorageAccount .AzureStorageAccount.AuthUsingConnectionString) }} + { + name: 'SPRING_CLOUD_AZURE_EVENTHUBS_PROCESSOR_CHECKPOINTSTORE_CONNECTIONSTRING' + secretRef: 'storage-account-connection-string' + } + { + name: 'SPRING_CLOUD_AZURE_EVENTHUBS_PROCESSOR_CHECKPOINTSTORE_CREDENTIAL_MANAGEDIDENTITYENABLED' + value: 'false' + } + { + name: 'SPRING_CLOUD_AZURE_EVENTHUBS_PROCESSOR_CHECKPOINTSTORE_CREDENTIAL_CLIENTID' + value: '' + } + {{- end}} {{- if .AzureServiceBus }} { @@ -476,7 +512,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { {{- end}} {{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingManagedIdentity) }} { - name: 'SPRING_CLOUD_AZURE_SERVICEBUS_CONNECTION_STRING' + name: 'SPRING_CLOUD_AZURE_SERVICEBUS_CONNECTIONSTRING' value: '' } { @@ -490,7 +526,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { {{- end}} {{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingConnectionString) }} { - name: 'SPRING_CLOUD_AZURE_SERVICEBUS_CONNECTION_STRING' + name: 'SPRING_CLOUD_AZURE_SERVICEBUS_CONNECTIONSTRING' secretRef: 'servicebus-connection-string' } { From 2a2111536187662e05eb3e820585d80d749b197e Mon Sep 17 00:00:00 2001 From: Rujun Chen <949800722@qq.com> Date: Mon, 4 Nov 2024 13:25:20 +0800 Subject: [PATCH 48/92] Use to AVM to support PostgreSql (#9) --- cli/azd/internal/scaffold/scaffold.go | 15 ++- .../scaffold/base/abbreviations.json | 1 + .../scaffold/templates/resources.bicept | 125 +++++++++++++++--- 3 files changed, 116 insertions(+), 25 deletions(-) diff --git a/cli/azd/internal/scaffold/scaffold.go b/cli/azd/internal/scaffold/scaffold.go index b75c93b6db6..9d49608106b 100644 --- a/cli/azd/internal/scaffold/scaffold.go +++ b/cli/azd/internal/scaffold/scaffold.go @@ -135,11 +135,20 @@ func ExecInfra( func preExecExpand(spec *InfraSpec) { // postgres and mysql requires specific password seeding parameters - if spec.DbPostgres != nil || spec.DbMySql != nil { + if spec.DbPostgres != nil { spec.Parameters = append(spec.Parameters, Parameter{ - Name: "databasePassword", - Value: "$(secretOrRandomPassword ${AZURE_KEY_VAULT_NAME} databasePassword)", + Name: "postgreSqlDatabasePassword", + Value: "$(secretOrRandomPassword ${AZURE_KEY_VAULT_NAME} postgreSqlDatabasePassword)", + Type: "string", + Secret: true, + }) + } + if spec.DbMySql != nil { + spec.Parameters = append(spec.Parameters, + Parameter{ + Name: "mysqlDatabasePassword", + Value: "$(secretOrRandomPassword ${AZURE_KEY_VAULT_NAME} mysqlDatabasePassword)", Type: "string", Secret: true, }) diff --git a/cli/azd/resources/scaffold/base/abbreviations.json b/cli/azd/resources/scaffold/base/abbreviations.json index dc62141f9da..4d4a4c62d6c 100644 --- a/cli/azd/resources/scaffold/base/abbreviations.json +++ b/cli/azd/resources/scaffold/base/abbreviations.json @@ -33,6 +33,7 @@ "dataMigrationServices": "dms-", "dBforMySQLServers": "mysql-", "dBforPostgreSQLServers": "psql-", + "deploymentScript": "dc-", "devicesIotHubs": "iot-", "devicesProvisioningServices": "provs-", "devicesProvisioningServicesCertificates": "pcert-", diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index 247da419c05..8ab889d9c23 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -61,6 +61,15 @@ module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.4.5 name: '${abbrs.appManagedEnvironments}${resourceToken}' location: location zoneRedundant: false + {{- if (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity) }} + roleAssignments: [ + { + principalId: connectionCreatorIdentity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: 'b24988ac-6180-42a0-ab88-20f7382dd24c' + } + ] + {{- end}} } } {{- end}} @@ -96,8 +105,8 @@ module cosmos 'br/public:avm/res/document-db/database-account:0.4.0' = { {{- end}} {{- if .DbPostgres}} -var databaseName = '{{ .DbPostgres.DatabaseName }}' -var databaseUser = 'psqladmin' +var postgreSqlDatabaseName = '{{ .DbPostgres.DatabaseName }}' +var postgreSqlDatabaseUser = 'psqladmin' module postgreServer 'br/public:avm/res/db-for-postgre-sql/flexible-server:0.1.4' = { name: 'postgreServer' params: { @@ -106,8 +115,8 @@ module postgreServer 'br/public:avm/res/db-for-postgre-sql/flexible-server:0.1.4 skuName: 'Standard_B1ms' tier: 'Burstable' // Non-required parameters - administratorLogin: databaseUser - administratorLoginPassword: databasePassword + administratorLogin: postgreSqlDatabaseUser + administratorLoginPassword: postgreSqlDatabasePassword geoRedundantBackup: 'Disabled' passwordAuth:'Enabled' firewallRules: [ @@ -119,13 +128,53 @@ module postgreServer 'br/public:avm/res/db-for-postgre-sql/flexible-server:0.1.4 ] databases: [ { - name: databaseName + name: postgreSqlDatabaseName + } + ] + location: location + {{- if (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity) }} + roleAssignments: [ + { + principalId: connectionCreatorIdentity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: 'b24988ac-6180-42a0-ab88-20f7382dd24c' } ] + {{- end}} + } +} +{{- end}} +{{- if (or (and .DbMySql .DbMySql.AuthUsingManagedIdentity) (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity))}} + +module connectionCreatorIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { + name: 'connectionCreatorIdentity' + params: { + name: '${abbrs.managedIdentityUserAssignedIdentities}cci-${resourceToken}' + location: location + } +} +{{- end}} +{{- if (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity) }} +{{- range .Services}} +module {{bicepName .Name}}CreateConnectionToPostgreSql 'br/public:avm/res/resources/deployment-script:0.4.0' = { + name: '{{bicepName .Name}}CreateConnectionToPostgreSql' + params: { + kind: 'AzureCLI' + name: '${abbrs.deploymentScript}{{bicepName .Name}}-connection-to-pg-${resourceToken}' + azCliVersion: '2.63.0' location: location + managedIdentities: { + userAssignedResourcesIds: [ + connectionCreatorIdentity.outputs.resourceId + ] + } + runOnce: false + retentionInterval: 'P1D' + 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 ${ {{bicepName .Name}}.outputs.resourceId} --target-id ${postgreServer.outputs.resourceId}/databases/${postgreSqlDatabaseName} --client-type springBoot --user-identity client-id=${ {{bicepName .Name}}Identity.outputs.clientId} subs-id=${subscription().subscriptionId} user-object-id=${connectionCreatorIdentity.outputs.principalId} -c main --yes;' } } {{- end}} +{{- end}} {{- if .AzureEventHubs }} module eventHubNamespace 'br/public:avm/res/event-hub/namespace:0.7.1' = { @@ -266,6 +315,15 @@ module {{bicepName .Name}}Identity 'br/public:avm/res/managed-identity/user-assi params: { name: '${abbrs.managedIdentityUserAssignedIdentities}{{bicepName .Name}}-${resourceToken}' location: location + {{- if (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity) }} + roleAssignments: [ + { + principalId: connectionCreatorIdentity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: 'b24988ac-6180-42a0-ab88-20f7382dd24c' + } + ] + {{- end}} } } @@ -315,14 +373,14 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { keyVaultUrl: '${keyVault.outputs.uri}secrets/MONGODB-URL' } {{- end}} - {{- if .DbPostgres}} + {{- if (and .DbPostgres .DbPostgres.AuthUsingUsernamePassword) }} { - name: 'db-pass' - value: databasePassword + name: 'postgresql-password' + value: postgreSqlDatabasePassword } { - name: 'db-url' - value: 'postgresql://${databaseUser}:${databasePassword}@${postgreServer.outputs.fqdn}:5432/${databaseName}' + name: 'postgresql-db-url' + value: 'postgresql://${postgreSqlDatabaseUser}:${postgreSqlDatabasePassword}@${postgreServer.outputs.fqdn}:5432/${postgreSqlDatabaseName}' } {{- end}} {{- if .DbRedis}} @@ -393,24 +451,38 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { value: postgreServer.outputs.fqdn } { - name: 'POSTGRES_USERNAME' - value: databaseUser + name: 'POSTGRES_DATABASE' + value: postgreSqlDatabaseName } { - name: 'POSTGRES_DATABASE' - value: databaseName + name: 'POSTGRES_PORT' + value: '5432' } { - name: 'POSTGRES_PASSWORD' - secretRef: 'db-pass' + name: 'SPRING_DATASOURCE_URL' + value: 'jdbc:postgresql://${postgreServer.outputs.fqdn}:5432/${postgreSqlDatabaseName}' } + {{- end}} + {{- if (and .DbPostgres .DbPostgres.AuthUsingUsernamePassword) }} { name: 'POSTGRES_URL' - secretRef: 'db-url' + secretRef: 'postgresql-db-url' } { - name: 'POSTGRES_PORT' - value: '5432' + name: 'POSTGRES_USERNAME' + value: postgreSqlDatabaseUser + } + { + name: 'POSTGRES_PASSWORD' + secretRef: 'postgresql-password' + } + { + name: 'SPRING_DATASOURCE_USERNAME' + value: postgreSqlDatabaseUser + } + { + name: 'SPRING_DATASOURCE_PASSWORD' + secretRef: 'postgresql-password' } {{- end}} {{- if .DbRedis}} @@ -574,6 +646,15 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { environmentResourceId: containerAppsEnvironment.outputs.resourceId location: location tags: union(tags, { 'azd-service-name': '{{.Name}}' }) + {{- if (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity) }} + roleAssignments: [ + { + principalId: connectionCreatorIdentity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: 'b24988ac-6180-42a0-ab88-20f7382dd24c' + } + ] + {{- end}} } } {{- end}} @@ -627,10 +708,10 @@ module keyVault 'br/public:avm/res/key-vault/vault:0.6.1' = { {{- end}} ] secrets: [ - {{- if .DbPostgres}} + {{- if (and .DbPostgres .DbPostgres.AuthUsingUsernamePassword) }} { - name: 'db-pass' - value: databasePassword + name: 'postgresql-password' + value: postgreSqlDatabasePassword } {{- end}} ] From 65cbfe1d7228881802ac3a2bade95d9b509c6eef Mon Sep 17 00:00:00 2001 From: HaoZhang Date: Mon, 4 Nov 2024 15:46:53 +0800 Subject: [PATCH 49/92] Add a span to indicate if java detector has started or finished (#10) Co-authored-by: Hao Zhang --- cli/azd/internal/appdetect/java.go | 4 ++++ cli/azd/internal/tracing/fields/fields.go | 3 +++ 2 files changed, 7 insertions(+) diff --git a/cli/azd/internal/appdetect/java.go b/cli/azd/internal/appdetect/java.go index 71ce8936335..c9046a8d6d8 100644 --- a/cli/azd/internal/appdetect/java.go +++ b/cli/azd/internal/appdetect/java.go @@ -5,6 +5,8 @@ import ( "context" "encoding/xml" "fmt" + "github.com/azure/azure-dev/cli/azd/internal/tracing" + "github.com/azure/azure-dev/cli/azd/internal/tracing/fields" "github.com/azure/azure-dev/cli/azd/pkg/osutil" "github.com/braydonk/yaml" "io/fs" @@ -27,6 +29,7 @@ func (jd *javaDetector) Language() Language { func (jd *javaDetector) DetectProject(ctx context.Context, path string, entries []fs.DirEntry) (*Project, error) { for _, entry := range entries { if strings.ToLower(entry.Name()) == "pom.xml" { + tracing.SetUsageAttributes(fields.AppInitJavaDetect.String("start")) pomFile := filepath.Join(path, entry.Name()) project, err := readMavenProject(pomFile) if err != nil { @@ -58,6 +61,7 @@ func (jd *javaDetector) DetectProject(ctx context.Context, path string, entries return nil, fmt.Errorf("detecting dependencies: %w", err) } + tracing.SetUsageAttributes(fields.AppInitJavaDetect.String("finish")) return result, nil } } diff --git a/cli/azd/internal/tracing/fields/fields.go b/cli/azd/internal/tracing/fields/fields.go index c264acafe66..6b3bf726609 100644 --- a/cli/azd/internal/tracing/fields/fields.go +++ b/cli/azd/internal/tracing/fields/fields.go @@ -250,6 +250,9 @@ const ( AppInitModifyAddCount = attribute.Key("appinit.modify_add.count") AppInitModifyRemoveCount = attribute.Key("appinit.modify_remove.count") + // AppInitJavaDetect indicates if java detector has started or finished + AppInitJavaDetect = attribute.Key("appinit.java.detect") + // The last step recorded during the app init process. AppInitLastStep = attribute.Key("appinit.lastStep") ) From 030ec5ba6540b66249cbad7491a5eca54a53e940 Mon Sep 17 00:00:00 2001 From: Rujun Chen <949800722@qq.com> Date: Tue, 5 Nov 2024 09:30:50 +0800 Subject: [PATCH 50/92] Use to AVM to support MySql: Auth by managed identity and username & password. (#11) --- .../scaffold/templates/db-mysql.bicept | 88 ---- .../scaffold/templates/db-postgres.bicept | 81 ---- .../templates/host-containerapp.bicept | 415 ------------------ .../scaffold/templates/resources.bicept | 154 ++++++- 4 files changed, 148 insertions(+), 590 deletions(-) delete mode 100644 cli/azd/resources/scaffold/templates/db-mysql.bicept delete mode 100644 cli/azd/resources/scaffold/templates/db-postgres.bicept delete mode 100644 cli/azd/resources/scaffold/templates/host-containerapp.bicept diff --git a/cli/azd/resources/scaffold/templates/db-mysql.bicept b/cli/azd/resources/scaffold/templates/db-mysql.bicept deleted file mode 100644 index dcd9dad0618..00000000000 --- a/cli/azd/resources/scaffold/templates/db-mysql.bicept +++ /dev/null @@ -1,88 +0,0 @@ -{{define "db-mysql.bicep" -}} -param serverName string -param location string = resourceGroup().location -param tags object = {} - -param keyVaultName string -param identityName string - -param databaseUser string = 'mysqladmin' -param databaseName string = '{{.DatabaseName}}' -@secure() -param databasePassword string - -param allowAllIPsFirewall bool = false - -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 - sku: { - name: 'Standard_B1ms' - tier: 'Burstable' - } - identity: { - type: 'UserAssigned' - userAssignedIdentities: { - '${userAssignedIdentity.id}': {} - } - } - 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 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/db-postgres.bicept b/cli/azd/resources/scaffold/templates/db-postgres.bicept deleted file mode 100644 index b6ebb5a87b8..00000000000 --- a/cli/azd/resources/scaffold/templates/db-postgres.bicept +++ /dev/null @@ -1,81 +0,0 @@ -{{define "db-postgres.bicep" -}} -param serverName string -param location string = resourceGroup().location -param tags object = {} - -param keyVaultName string - -param databaseUser string = 'psqladmin' -param databaseName string = '{{.DatabaseName}}' -@secure() -param databasePassword string - -param allowAllIPsFirewall bool = false - -resource postgreServer'Microsoft.DBforPostgreSQL/flexibleServers@2022-01-20-preview' = { - location: location - tags: tags - name: serverName - sku: { - name: 'Standard_B1ms' - tier: 'Burstable' - } - properties: { - version: '13' - administratorLogin: databaseUser - administratorLoginPassword: databasePassword - storage: { - storageSizeGB: 128 - } - backup: { - backupRetentionDays: 7 - geoRedundantBackup: 'Disabled' - } - highAvailability: { - mode: 'Disabled' - } - maintenanceWindow: { - customWindow: 'Disabled' - dayOfWeek: 0 - startHour: 0 - startMinute: 0 - } - } - - resource firewall_all 'firewallRules' = if (allowAllIPsFirewall) { - name: 'allow-all-IPs' - properties: { - startIpAddress: '0.0.0.0' - endIpAddress: '255.255.255.255' - } - } -} - -resource database 'Microsoft.DBforPostgreSQL/flexibleServers/databases@2022-01-20-preview' = { - parent: postgreServer - 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 databaseId string = database.id -output databaseHost string = postgreServer.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 deleted file mode 100644 index 9991fdea940..00000000000 --- a/cli/azd/resources/scaffold/templates/host-containerapp.bicept +++ /dev/null @@ -1,415 +0,0 @@ -{{define "host-containerapp.bicep" -}} -param name string -param location string = resourceGroup().location -param tags object = {} - -param identityName string -param containerRegistryName string -param containerAppsEnvironmentName string -param applicationInsightsName string -{{- if .DbCosmosMongo}} -@secure() -param cosmosDbConnectionString string -{{- end}} -{{- if (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity)}} -param postgresDatabaseId string -{{- end}} -{{- if (and .DbPostgres .DbPostgres.AuthUsingUsernamePassword)}} -param postgresDatabaseHost string -param postgresDatabaseName string -param postgresDatabaseUser string -@secure() -param postgresDatabasePassword string -{{- end}} -{{- 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 -{{- end}} -{{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingManagedIdentity)}} -@secure() -param azureServiceBusNamespace string -{{- end}} -{{- if .DbRedis}} -param redisName string -{{- end}} -{{- if (and .Frontend .Frontend.Backends)}} -param apiUrls array -{{- end}} -{{- if (and .Backend .Backend.Frontends)}} -param allowedOrigins array -{{- end}} -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 => { - name: i.name - value: i.value - secretRef: i.?secretRef ?? take(replace(replace(toLower(i.name), '_', '-'), '.', '-'), 32) -}) -var env = map(filter(appSettingsArray, i => i.?secret == null), i => { - name: i.name - value: i.value -}) - -resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { - name: identityName - location: location -} - -resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' existing = { - name: containerRegistryName -} - -resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' existing = { - name: containerAppsEnvironmentName -} - -resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { - name: applicationInsightsName -} - -resource acrPullRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - scope: containerRegistry - name: guid(subscription().id, resourceGroup().id, identity.id, 'acrPullRole') - properties: { - roleDefinitionId: subscriptionResourceId( - 'Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') - principalType: 'ServicePrincipal' - principalId: identity.properties.principalId - } -} - -module fetchLatestImage '../modules/fetch-container-image.bicep' = { - name: '${name}-fetch-image' - params: { - exists: exists - name: name - } -} -{{- if .DbRedis}} - -resource redis 'Microsoft.App/containerApps@2023-05-02-preview' = { - name: redisName - location: location - properties: { - environmentId: containerAppsEnvironment.id - configuration: { - service: { - type: 'redis' - } - } - template: { - containers: [ - { - image: 'redis' - name: 'redis' - } - ] - } - } -} -{{- end}} - -resource app 'Microsoft.App/containerApps@2023-05-02-preview' = { - name: name - location: location - tags: union(tags, {'azd-service-name': '{{.Name}}' }) - dependsOn: [ acrPullRole ] - identity: { - type: 'UserAssigned' - userAssignedIdentities: { '${identity.id}': {} } - } - properties: { - managedEnvironmentId: containerAppsEnvironment.id - configuration: { - {{- if ne .Port 0}} - ingress: { - external: true - targetPort: {{.Port}} - transport: 'auto' - {{- if (and .Backend .Backend.Frontends)}} - corsPolicy: { - allowedOrigins: union(allowedOrigins, [ - // define additional allowed origins here - ]) - allowedMethods: ['GET', 'PUT', 'POST', 'DELETE'] - } - {{- end}} - } - {{- end}} - registries: [ - { - server: '${containerRegistryName}.azurecr.io' - identity: identity.id - } - ] - secrets: union([ - {{- if .DbCosmosMongo}} - { - name: 'azure-cosmos-connection-string' - value: cosmosDbConnectionString - } - {{- end}} - {{- if (and .DbPostgres .DbPostgres.AuthUsingUsernamePassword)}} - { - name: 'postgres-db-pass' - 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' - value: azureServiceBusConnectionString - } - {{- end}} - ], - map(secrets, secret => { - name: secret.secretRef - value: secret.value - })) - } - template: { - containers: [ - { - image: fetchLatestImage.outputs.?containers[?0].?image ?? 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' - name: 'main' - env: union([ - { - name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' - value: applicationInsights.properties.ConnectionString - } - {{- if .DbCosmosMongo}} - { - name: 'AZURE_COSMOS_MONGODB_CONNECTION_STRING' - secretRef: 'azure-cosmos-connection-string' - } - {{- end}} - {{- if (and .DbPostgres .DbPostgres.AuthUsingUsernamePassword)}} - { - name: 'POSTGRES_HOST' - value: postgresDatabaseHost - } - { - name: 'POSTGRES_PORT' - value: '5432' - } - { - name: 'POSTGRES_DATABASE' - value: postgresDatabaseName - } - { - name: 'POSTGRES_USERNAME' - value: postgresDatabaseUser - } - { - name: 'POSTGRES_PASSWORD' - 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' - 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}} - { - name: '{{upper .Name}}_BASE_URL' - value: apiUrls[{{$i}}] - } - {{- end}} - {{- end}} - {{- if ne .Port 0}} - { - name: 'PORT' - value: '{{ .Port }}' - } - { - name: 'SERVER_PORT' - value: '{{ .Port }}' - } - {{- end}} - ], - env, - map(secrets, secret => { - name: secret.name - secretRef: secret.secretRef - })) - resources: { - cpu: json('1.0') - memory: '2.0Gi' - } - } - ] - {{- if .DbRedis}} - serviceBinds: [ - { - serviceId: redis.id - name: 'redis' - } - ] - {{- end}} - scale: { - minReplicas: 1 - maxReplicas: 10 - } - } - } -} -{{- if (or (and .DbMySql .DbMySql.AuthUsingManagedIdentity) (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity))}} - -resource linkerCreatorIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { - name: '${name}-linker-creator-identity' - location: location -} - -resource linkerCreatorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - scope: resourceGroup() - name: guid(subscription().id, resourceGroup().id, linkerCreatorIdentity.id, 'linkerCreatorRole') - properties: { - roleDefinitionId: subscriptionResourceId( - 'Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') - principalType: 'ServicePrincipal' - principalId: linkerCreatorIdentity.properties.principalId - } -} -{{- end}} -{{- if (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity)}} - -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' - 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' - } -} -{{- end}} -{{- if (and .DbMySql .DbMySql.AuthUsingManagedIdentity)}} - -resource appLinkToMySql 'Microsoft.Resources/deploymentScripts@2023-08-01' = { - dependsOn: [ linkerCreatorRole ] - name: '${name}-link-to-mysql' - location: location - kind: 'AzureCLI' - identity: { - type: 'UserAssigned' - userAssignedIdentities: { - '${linkerCreatorIdentity.id}': {} - } - } - 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' - } -} -{{- 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}' -output id string = app.id -{{ end}} diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index 8ab889d9c23..b87f8c4d4bd 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -61,7 +61,7 @@ module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.4.5 name: '${abbrs.appManagedEnvironments}${resourceToken}' location: location zoneRedundant: false - {{- if (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity) }} + {{- if (or (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity) (and .DbMySql .DbMySql.AuthUsingManagedIdentity))}} roleAssignments: [ { principalId: connectionCreatorIdentity.outputs.principalId @@ -103,8 +103,8 @@ module cosmos 'br/public:avm/res/document-db/database-account:0.4.0' = { } } {{- end}} - {{- if .DbPostgres}} + var postgreSqlDatabaseName = '{{ .DbPostgres.DatabaseName }}' var postgreSqlDatabaseUser = 'psqladmin' module postgreServer 'br/public:avm/res/db-for-postgre-sql/flexible-server:0.1.4' = { @@ -144,7 +144,69 @@ module postgreServer 'br/public:avm/res/db-for-postgre-sql/flexible-server:0.1.4 } } {{- end}} -{{- if (or (and .DbMySql .DbMySql.AuthUsingManagedIdentity) (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity))}} +{{- if .DbMySql}} + +var mysqlDatabaseName = '{{ .DbMySql.DatabaseName }}' +var mysqlDatabaseUser = 'mysqladmin' +{{- if (and .DbMySql .DbMySql.AuthUsingManagedIdentity) }} +module mysqlIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { + name: 'mysqlIdentity' + params: { + name: '${abbrs.managedIdentityUserAssignedIdentities}mysql-${resourceToken}' + location: location + roleAssignments: [ + { + principalId: connectionCreatorIdentity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: 'b24988ac-6180-42a0-ab88-20f7382dd24c' + } + ] + } +} +{{- end}} +module mysqlServer 'br/public:avm/res/db-for-my-sql/flexible-server:0.4.1' = { + name: 'mysqlServer' + params: { + // Required parameters + name: '${abbrs.dBforMySQLServers}${resourceToken}' + skuName: 'Standard_B1ms' + tier: 'Burstable' + // Non-required parameters + administratorLogin: mysqlDatabaseUser + administratorLoginPassword: mysqlDatabasePassword + geoRedundantBackup: 'Disabled' + firewallRules: [ + { + name: 'AllowAllIps' + startIpAddress: '0.0.0.0' + endIpAddress: '255.255.255.255' + } + ] + databases: [ + { + name: mysqlDatabaseName + } + ] + location: location + highAvailability: 'Disabled' + {{- if (and .DbMySql .DbMySql.AuthUsingManagedIdentity) }} + managedIdentities: { + userAssignedResourceIds: [ + mysqlIdentity.outputs.resourceId + ] + } + roleAssignments: [ + { + principalId: connectionCreatorIdentity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: 'b24988ac-6180-42a0-ab88-20f7382dd24c' + } + ] + {{- end}} + } +} +{{- end}} +{{- if (or (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity) (and .DbMySql .DbMySql.AuthUsingManagedIdentity))}} module connectionCreatorIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { name: 'connectionCreatorIdentity' @@ -170,7 +232,28 @@ module {{bicepName .Name}}CreateConnectionToPostgreSql 'br/public:avm/res/resour } runOnce: false retentionInterval: 'P1D' - 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 ${ {{bicepName .Name}}.outputs.resourceId} --target-id ${postgreServer.outputs.resourceId}/databases/${postgreSqlDatabaseName} --client-type springBoot --user-identity client-id=${ {{bicepName .Name}}Identity.outputs.clientId} subs-id=${subscription().subscriptionId} user-object-id=${connectionCreatorIdentity.outputs.principalId} -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 appConnectToPostgres --source-id ${ {{bicepName .Name}}.outputs.resourceId} --target-id ${postgreServer.outputs.resourceId}/databases/${postgreSqlDatabaseName} --client-type springBoot --user-identity client-id=${ {{bicepName .Name}}Identity.outputs.clientId} subs-id=${subscription().subscriptionId} user-object-id=${connectionCreatorIdentity.outputs.principalId} -c main --yes;' + } +} +{{- end}} +{{- end}} +{{- if (and .DbMySql .DbMySql.AuthUsingManagedIdentity) }} +{{- range .Services}} +module {{bicepName .Name}}CreateConnectionToMysql 'br/public:avm/res/resources/deployment-script:0.4.0' = { + name: '{{bicepName .Name}}CreateConnectionToMysql' + params: { + kind: 'AzureCLI' + name: '${abbrs.deploymentScript}{{bicepName .Name}}-connection-to-mysql-${resourceToken}' + azCliVersion: '2.63.0' + location: location + managedIdentities: { + userAssignedResourcesIds: [ + connectionCreatorIdentity.outputs.resourceId + ] + } + runOnce: false + retentionInterval: 'P1D' + 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 appConnectToMysql --source-id ${ {{bicepName .Name}}.outputs.resourceId} --target-id ${mysqlServer.outputs.resourceId}/databases/${mysqlDatabaseName} --client-type springBoot --user-identity client-id=${ {{bicepName .Name}}Identity.outputs.clientId} subs-id=${subscription().subscriptionId} user-object-id=${connectionCreatorIdentity.outputs.principalId} mysql-identity-id=${mysqlIdentity.outputs.resourceId} -c main --yes;' } } {{- end}} @@ -315,7 +398,7 @@ module {{bicepName .Name}}Identity 'br/public:avm/res/managed-identity/user-assi params: { name: '${abbrs.managedIdentityUserAssignedIdentities}{{bicepName .Name}}-${resourceToken}' location: location - {{- if (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity) }} + {{- if (or (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity) (and .DbMySql .DbMySql.AuthUsingManagedIdentity))}} roleAssignments: [ { principalId: connectionCreatorIdentity.outputs.principalId @@ -383,6 +466,16 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { value: 'postgresql://${postgreSqlDatabaseUser}:${postgreSqlDatabasePassword}@${postgreServer.outputs.fqdn}:5432/${postgreSqlDatabaseName}' } {{- end}} + {{- if (and .DbMySql .DbMySql.AuthUsingUsernamePassword) }} + { + name: 'mysql-password' + value: mysqlDatabasePassword + } + { + name: 'mysql-db-url' + value: 'mysql://${mysqlDatabaseUser}:${mysqlDatabasePassword}@${mysqlServer.outputs.fqdn}:3306/${mysqlDatabaseName}' + } + {{- end}} {{- if .DbRedis}} { name: 'redis-pass' @@ -485,6 +578,46 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { secretRef: 'postgresql-password' } {{- end}} + {{- if .DbMySql}} + { + name: 'MYSQL_HOST' + value: mysqlServer.outputs.fqdn + } + { + name: 'MYSQL_DATABASE' + value: mysqlDatabaseName + } + { + name: 'MYSQL_PORT' + value: '3306' + } + { + name: 'SPRING_DATASOURCE_URL' + value: 'jdbc:mysql://${mysqlServer.outputs.fqdn}:3306/${mysqlDatabaseName}' + } + {{- end}} + {{- if (and .DbMySql .DbMySql.AuthUsingUsernamePassword) }} + { + name: 'MYSQL_URL' + secretRef: 'mysql-db-url' + } + { + name: 'MYSQL_USERNAME' + value: mysqlDatabaseUser + } + { + name: 'MYSQL_PASSWORD' + secretRef: 'mysql-password' + } + { + name: 'SPRING_DATASOURCE_USERNAME' + value: mysqlDatabaseUser + } + { + name: 'SPRING_DATASOURCE_PASSWORD' + secretRef: 'mysql-password' + } + {{- end}} {{- if .DbRedis}} { name: 'REDIS_HOST' @@ -646,7 +779,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { environmentResourceId: containerAppsEnvironment.outputs.resourceId location: location tags: union(tags, { 'azd-service-name': '{{.Name}}' }) - {{- if (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity) }} + {{- if (or (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity) (and .DbMySql .DbMySql.AuthUsingManagedIdentity))}} roleAssignments: [ { principalId: connectionCreatorIdentity.outputs.principalId @@ -714,6 +847,12 @@ module keyVault 'br/public:avm/res/key-vault/vault:0.6.1' = { value: postgreSqlDatabasePassword } {{- end}} + {{- if (and .DbMySql .DbMySql.AuthUsingUsernamePassword) }} + { + name: 'mysql-password' + value: mysqlDatabasePassword + } + {{- end}} ] } } @@ -730,6 +869,9 @@ output AZURE_CACHE_REDIS_ID string = redis.outputs.resourceId {{- if .DbPostgres}} output AZURE_POSTGRES_FLEXIBLE_SERVER_ID string = postgreServer.outputs.resourceId {{- end}} +{{- if .DbMySql}} +output AZURE_MYSQL_FLEXIBLE_SERVER_ID string = mysqlServer.outputs.resourceId +{{- end}} {{- if .AzureEventHubs }} output AZURE_EVENT_HUBS_ID string = eventHubNamespace.outputs.resourceId {{- end}} From 41331c02550b57ebd0f6c6ea55e4f4eb0b0186f2 Mon Sep 17 00:00:00 2001 From: Xiaolu Dai <31124698+saragluna@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:28:07 +0800 Subject: [PATCH 51/92] output resources from app init (#4) --- cli/azd/internal/repository/app_init.go | 40 +++++++++++++++++++++---- cli/azd/pkg/project/project.go | 7 +++++ cli/azd/pkg/project/resources.go | 19 ++++++++++++ 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index 2ea0c0b942c..54484e41207 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -263,7 +263,7 @@ func (i *Initializer) InitFromApp( tracing.SetUsageAttributes(fields.AppInitLastStep.String("generate")) i.console.Message(ctx, "\n"+output.WithBold("Generating files to run your app on Azure:")+"\n") - err = i.genProjectFile(ctx, azdCtx, detect) + err = i.genProjectFile(ctx, azdCtx, detect, spec) if err != nil { return err } @@ -325,14 +325,15 @@ func (i *Initializer) InitFromApp( func (i *Initializer) genProjectFile( ctx context.Context, azdCtx *azdcontext.AzdContext, - detect detectConfirm) error { + detect detectConfirm, + spec scaffold.InfraSpec) error { title := "Generating " + output.WithHighLightFormat("./"+azdcontext.ProjectFileName) i.console.ShowSpinner(ctx, title, input.Step) var err error defer i.console.StopSpinner(ctx, title, input.GetStepResultFormat(err)) - config, err := prjConfigFromDetect(azdCtx.ProjectDirectory(), detect) + config, err := prjConfigFromDetect(azdCtx.ProjectDirectory(), detect, spec) if err != nil { return fmt.Errorf("converting config: %w", err) } @@ -351,13 +352,15 @@ const InitGenTemplateId = "azd-init" func prjConfigFromDetect( root string, - detect detectConfirm) (project.ProjectConfig, error) { + detect detectConfirm, + spec scaffold.InfraSpec) (project.ProjectConfig, error) { config := project.ProjectConfig{ Name: azdcontext.ProjectName(root), Metadata: &project.ProjectMetadata{ Template: fmt.Sprintf("%s@%s", InitGenTemplateId, internal.VersionInfo().Version), }, - Services: map[string]*project.ServiceConfig{}, + Services: map[string]*project.ServiceConfig{}, + Resources: map[string]*project.ResourceConfig{}, } for _, prj := range detect.Services { rel, err := filepath.Rel(root, prj.Path) @@ -414,6 +417,33 @@ func prjConfigFromDetect( } } + for _, db := range prj.DatabaseDeps { + switch db { + case appdetect.DbMongo: + config.Resources["mongo"] = &project.ResourceConfig{ + Type: project.ResourceTypeDbMongo, + Name: spec.DbCosmosMongo.DatabaseName, + } + case appdetect.DbPostgres: + config.Resources["postgres"] = &project.ResourceConfig{ + Type: project.ResourceTypeDbPostgres, + Name: spec.DbPostgres.DatabaseName, + } + case appdetect.DbMySql: + config.Resources["mysql"] = &project.ResourceConfig{ + Type: project.ResourceTypeDbMySQL, + Props: project.MySQLProps{ + DatabaseName: spec.DbMySql.DatabaseName, + AuthType: "managedIdentity", + }, + } + case appdetect.DbRedis: + config.Resources["redis"] = &project.ResourceConfig{ + Type: project.ResourceTypeDbRedis, + } + } + } + name := filepath.Base(rel) if name == "." { name = config.Name diff --git a/cli/azd/pkg/project/project.go b/cli/azd/pkg/project/project.go index 73a50a15307..3aa988937bf 100644 --- a/cli/azd/pkg/project/project.go +++ b/cli/azd/pkg/project/project.go @@ -252,6 +252,13 @@ func Save(ctx context.Context, projectConfig *ProjectConfig, projectFilePath str copy.Services[name] = &svcCopy } + for name, resource := range projectConfig.Resources { + resourceCopy := *resource + resourceCopy.Project = © + + copy.Resources[name] = &resourceCopy + } + projectBytes, err := yaml.Marshal(copy) if err != nil { return fmt.Errorf("marshalling project yaml: %w", err) diff --git a/cli/azd/pkg/project/resources.go b/cli/azd/pkg/project/resources.go index 927aa682e65..9f4bca49765 100644 --- a/cli/azd/pkg/project/resources.go +++ b/cli/azd/pkg/project/resources.go @@ -14,6 +14,7 @@ type ResourceType string const ( ResourceTypeDbRedis ResourceType = "db.redis" ResourceTypeDbPostgres ResourceType = "db.postgres" + ResourceTypeDbMySQL ResourceType = "db.mysql" ResourceTypeDbMongo ResourceType = "db.mongo" ResourceTypeHostContainerApp ResourceType = "host.containerapp" ResourceTypeOpenAiModel ResourceType = "ai.openai.model" @@ -25,6 +26,8 @@ func (r ResourceType) String() string { return "Redis" case ResourceTypeDbPostgres: return "PostgreSQL" + case ResourceTypeDbMySQL: + return "MySQL" case ResourceTypeDbMongo: return "MongoDB" case ResourceTypeHostContainerApp: @@ -79,6 +82,11 @@ func (r *ResourceConfig) MarshalYAML() (interface{}, error) { if err != nil { return nil, err } + case ResourceTypeDbMySQL: + err := marshalRawProps(raw.Props.(MySQLProps)) + if err != nil { + return nil, err + } } return raw, nil @@ -118,6 +126,12 @@ func (r *ResourceConfig) UnmarshalYAML(value *yaml.Node) error { return err } raw.Props = cap + case ResourceTypeDbMySQL: + mp := MySQLProps{} + if err := unmarshalProps(&mp); err != nil { + return err + } + raw.Props = mp } *r = ResourceConfig(raw) @@ -145,3 +159,8 @@ type AIModelPropsModel struct { Name string `yaml:"name,omitempty"` Version string `yaml:"version,omitempty"` } + +type MySQLProps struct { + DatabaseName string `yaml:"databaseName,omitempty"` + AuthType string `yaml:"authType,omitempty"` +} From b5069e5e8053ecf93f36d6445b842bd06c37e59e Mon Sep 17 00:00:00 2001 From: HaoZhang Date: Tue, 5 Nov 2024 16:38:15 +0800 Subject: [PATCH 52/92] update azure yaml schema to support `resources` (#12) Co-authored-by: Hao Zhang --- schemas/alpha/azure.yaml.json | 57 +++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/schemas/alpha/azure.yaml.json b/schemas/alpha/azure.yaml.json index f846f3f1fd5..1c98c15d473 100644 --- a/schemas/alpha/azure.yaml.json +++ b/schemas/alpha/azure.yaml.json @@ -352,6 +352,63 @@ ] } }, + "resources": { + "type": "object", + "title": "Definition of resources that the application depends on", + "description": "Optional. Provides additional configuration for Azure resources that the application depends on.", + "minProperties": 1, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "title": "Required. The type of Azure resource that the application depends on", + "description": "The Azure resource type that the application depends on", + "enum": [ + "db.mysql", + "db.redis", + "db.postgres", + "db.mongo" + ] + }, + "authType": { + "type": "string", + "title": "The authentication type of Azure resource used for the application", + "description": "The application uses this kind of authentication to connect to the Azure resource.", + "enum": [ + "managedIdentity", + "usernamePassword" + ] + }, + "databaseName": { + "type": "string", + "title": "The name of Azure resource that the application depends on", + "description": "The Azure resource that will be accessed during application runtime." + } + }, + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "db.mysql" + } + } + }, + "then": { + "required": [ + "authType", + "databaseName" + ] + } + } + ] + } + }, "pipeline": { "type": "object", "title": "Definition of continuous integration pipeline", From 31f8240eaf90966815d6d808ccdc33b166214b77 Mon Sep 17 00:00:00 2001 From: HaoZhang Date: Wed, 6 Nov 2024 11:24:58 +0800 Subject: [PATCH 53/92] Update azure.yaml schema reference for private preview (#13) * update azure yaml schema to support `resources` * small update for private preview --------- Co-authored-by: Hao Zhang --- cli/azd/pkg/project/project.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/azd/pkg/project/project.go b/cli/azd/pkg/project/project.go index 3aa988937bf..9d7dba97775 100644 --- a/cli/azd/pkg/project/project.go +++ b/cli/azd/pkg/project/project.go @@ -23,7 +23,8 @@ import ( const ( //nolint:lll - projectSchemaAnnotation = "# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json" + // todo(haozhan): update this line for sjad private preview, need to revert it when merge into azure-dev/main branch + projectSchemaAnnotation = "# yaml-language-server: $schema=https://raw.githubusercontent.com/azure-javaee/azure-dev/feature/sjad/schemas/alpha/azure.yaml.json" ) func New(ctx context.Context, projectFilePath string, projectName string) (*ProjectConfig, error) { From 08e3776f01cabe74b5a8a6935817f6ed1515dac8 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Mon, 11 Nov 2024 14:41:03 +0800 Subject: [PATCH 54/92] Support this scenario: frontend + backend + MongoDB (#14) --- .../testdata/Dockerfile/Dockerfile1 | 20 ------------- .../testdata/Dockerfile/Dockerfile2 | 22 -------------- .../testdata/Dockerfile/Dockerfile3 | 21 ------------- .../resources/scaffold/templates/main.bicept | 3 ++ .../scaffold/templates/resources.bicept | 30 ++++++++++++++----- 5 files changed, 26 insertions(+), 70 deletions(-) delete mode 100644 cli/azd/internal/repository/testdata/Dockerfile/Dockerfile1 delete mode 100644 cli/azd/internal/repository/testdata/Dockerfile/Dockerfile2 delete mode 100644 cli/azd/internal/repository/testdata/Dockerfile/Dockerfile3 diff --git a/cli/azd/internal/repository/testdata/Dockerfile/Dockerfile1 b/cli/azd/internal/repository/testdata/Dockerfile/Dockerfile1 deleted file mode 100644 index 0b10c650d8d..00000000000 --- a/cli/azd/internal/repository/testdata/Dockerfile/Dockerfile1 +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index c1925937d2d..00000000000 --- a/cli/azd/internal/repository/testdata/Dockerfile/Dockerfile2 +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 1ecad8a32f2..00000000000 --- a/cli/azd/internal/repository/testdata/Dockerfile/Dockerfile3 +++ /dev/null @@ -1,21 +0,0 @@ -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 diff --git a/cli/azd/resources/scaffold/templates/main.bicept b/cli/azd/resources/scaffold/templates/main.bicept index 527b5d08c48..6de0ef441a1 100644 --- a/cli/azd/resources/scaffold/templates/main.bicept +++ b/cli/azd/resources/scaffold/templates/main.bicept @@ -59,6 +59,9 @@ output AZURE_CACHE_REDIS_ID string = resources.outputs.AZURE_CACHE_REDIS_ID {{- if .DbPostgres}} output AZURE_POSTGRES_FLEXIBLE_SERVER_ID string = resources.outputs.AZURE_POSTGRES_FLEXIBLE_SERVER_ID {{- end}} +{{- if .DbMySql}} +output AZURE_MYSQL_FLEXIBLE_SERVER_ID string = resources.outputs.AZURE_MYSQL_FLEXIBLE_SERVER_ID +{{- end}} {{- if .AzureEventHubs }} output AZURE_EVENT_HUBS_ID string = resources.outputs.AZURE_EVENT_HUBS_ID {{- end}} diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index 69721c36086..9a4c83e4a93 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -75,9 +75,11 @@ module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.4.5 {{- end}} {{- if .DbCosmosMongo}} -module cosmos 'br/public:avm/res/document-db/database-account:0.4.0' = { +module cosmos 'br/public:avm/res/document-db/database-account:0.8.1' = { name: 'cosmos' params: { + name: '${abbrs.documentDBDatabaseAccounts}${resourceToken}' + location: location tags: tags locations: [ { @@ -86,8 +88,11 @@ module cosmos 'br/public:avm/res/document-db/database-account:0.4.0' = { locationName: location } ] - name: '${abbrs.documentDBDatabaseAccounts}${resourceToken}' - location: location + networkRestrictions: { + ipRules: [] + virtualNetworkRules: [] + publicNetworkAccess: 'Enabled' + } {{- if .DbCosmosMongo.DatabaseName}} mongodbDatabases: [ { @@ -95,8 +100,8 @@ module cosmos 'br/public:avm/res/document-db/database-account:0.4.0' = { } ] {{- end}} - secretsKeyVault: { - keyVaultName: keyVault.outputs.name + secretsExportConfiguration: { + keyVaultResourceId: keyVault.outputs.resourceId primaryWriteConnectionStringSecretName: 'MONGODB-URL' } capabilitiesToAdd: [ 'EnableServerless' ] @@ -494,6 +499,9 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { 'https://{{.Name}}.${containerAppsEnvironment.outputs.defaultDomain}' {{- end}} ] + allowedMethods: [ + '*' + ] } {{- end}} scaleMinReplicas: 1 @@ -504,7 +512,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { { name: 'mongodb-url' identity:{{bicepName .Name}}Identity.outputs.resourceId - keyVaultUrl: '${keyVault.outputs.uri}secrets/MONGODB-URL' + keyVaultUrl: cosmos.outputs.exportedSecrets['MONGODB-URL'].secretUri } {{- end}} {{- if (and .DbPostgres .DbPostgres.AuthUsingUsernamePassword) }} @@ -588,6 +596,14 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { name: 'MONGODB_URL' secretRef: 'mongodb-url' } + { + name: 'SPRING_DATA_MONGODB_URI' + secretRef: 'mongodb-url' + } + { + name: 'SPRING_DATA_MONGODB_DATABASE' + value: '{{ .DbCosmosMongo.DatabaseName }}' + } {{- end}} {{- if .DbPostgres}} { @@ -805,7 +821,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { {{- range $i, $e := .Frontend.Backends}} { name: '{{upper .Name}}_BASE_URL' - value: 'https://{{.Name}}.internal.${containerAppsEnvironment.outputs.defaultDomain}' + value: 'https://{{.Name}}.${containerAppsEnvironment.outputs.defaultDomain}' } {{- end}} {{- end}} From 209e859c8119112f75ab026da76930062449012b Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Mon, 11 Nov 2024 14:41:24 +0800 Subject: [PATCH 55/92] Azd enhancement for cosmosdb (#15) --- cli/azd/internal/appdetect/appdetect.go | 3 + cli/azd/internal/appdetect/java.go | 4 ++ cli/azd/internal/repository/app_init.go | 1 + cli/azd/internal/repository/detect_confirm.go | 2 + cli/azd/internal/repository/infra_confirm.go | 67 +++++++++++++++++ .../internal/repository/infra_confirm_test.go | 72 +++++++++++++++++++ cli/azd/internal/scaffold/spec.go | 13 ++++ .../scaffold/templates/resources.bicept | 60 ++++++++++++++++ 8 files changed, 222 insertions(+) diff --git a/cli/azd/internal/appdetect/appdetect.go b/cli/azd/internal/appdetect/appdetect.go index aff6c7a3328..ef1a9c708bb 100644 --- a/cli/azd/internal/appdetect/appdetect.go +++ b/cli/azd/internal/appdetect/appdetect.go @@ -110,6 +110,7 @@ const ( DbPostgres DatabaseDep = "postgres" DbMongo DatabaseDep = "mongo" DbMySql DatabaseDep = "mysql" + DbCosmos DatabaseDep = "cosmos" DbSqlServer DatabaseDep = "sqlserver" DbRedis DatabaseDep = "redis" ) @@ -122,6 +123,8 @@ func (db DatabaseDep) Display() string { return "MongoDB" case DbMySql: return "MySQL" + case DbCosmos: + return "Cosmos DB" case DbSqlServer: return "SQL Server" case DbRedis: diff --git a/cli/azd/internal/appdetect/java.go b/cli/azd/internal/appdetect/java.go index c9046a8d6d8..2bbde68271f 100644 --- a/cli/azd/internal/appdetect/java.go +++ b/cli/azd/internal/appdetect/java.go @@ -159,6 +159,10 @@ func detectDependencies(mavenProject *mavenProject, project *Project) (*Project, databaseDepMap[DbPostgres] = struct{}{} } + if dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-starter-data-cosmos" { + databaseDepMap[DbCosmos] = struct{}{} + } + if dep.GroupId == "org.springframework.boot" && dep.ArtifactId == "spring-boot-starter-data-redis" { databaseDepMap[DbRedis] = struct{}{} } diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index 2a87fd37e6b..82a1f76235e 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -37,6 +37,7 @@ var dbMap = map[appdetect.DatabaseDep]struct{}{ appdetect.DbMongo: {}, appdetect.DbPostgres: {}, appdetect.DbMySql: {}, + appdetect.DbCosmos: {}, appdetect.DbRedis: {}, } diff --git a/cli/azd/internal/repository/detect_confirm.go b/cli/azd/internal/repository/detect_confirm.go index 900ba3e8820..a60cfe41e3b 100644 --- a/cli/azd/internal/repository/detect_confirm.go +++ b/cli/azd/internal/repository/detect_confirm.go @@ -235,6 +235,8 @@ func (d *detectConfirm) render(ctx context.Context) error { recommendedServices = append(recommendedServices, "Azure Database for PostgreSQL flexible server") case appdetect.DbMySql: recommendedServices = append(recommendedServices, "Azure Database for MySQL flexible server") + case appdetect.DbCosmos: + recommendedServices = append(recommendedServices, "Azure Cosmos DB for NoSQL") case appdetect.DbMongo: recommendedServices = append(recommendedServices, "Azure CosmosDB API for MongoDB") case appdetect.DbRedis: diff --git a/cli/azd/internal/repository/infra_confirm.go b/cli/azd/internal/repository/infra_confirm.go index c580e81dd15..5f446fd16b4 100644 --- a/cli/azd/internal/repository/infra_confirm.go +++ b/cli/azd/internal/repository/infra_confirm.go @@ -3,6 +3,7 @@ package repository import ( "context" "fmt" + "os" "path/filepath" "regexp" "strconv" @@ -74,6 +75,24 @@ func (i *Initializer) infraSpecFromDetect( AuthUsingUsernamePassword: authType == scaffold.AuthType_PASSWORD, } break dbPrompt + case appdetect.DbCosmos: + if dbName == "" { + i.console.Message(ctx, "Database name is required.") + continue + } + containers, err := detectCosmosSqlDatabaseContainersInDirectory(detect.root) + if err != nil { + return scaffold.InfraSpec{}, err + } + spec.DbCosmos = &scaffold.DatabaseCosmosAccount{ + // todo: + // Now all services (except aca) are named by '${abbrs.xxx}${resourceToken}' + // Consider to name it by AccountName defined here. + AccountName: "not used for now", + DatabaseName: dbName, + Containers: containers, + } + break dbPrompt } break dbPrompt } @@ -128,6 +147,8 @@ func (i *Initializer) infraSpecFromDetect( AuthUsingManagedIdentity: spec.DbMySql.AuthUsingManagedIdentity, AuthUsingUsernamePassword: spec.DbMySql.AuthUsingUsernamePassword, } + case appdetect.DbCosmos: + serviceSpec.DbCosmos = spec.DbCosmos case appdetect.DbRedis: serviceSpec.DbRedis = &scaffold.DatabaseReference{ DatabaseName: "redis", @@ -434,3 +455,49 @@ func (i *Initializer) chooseAuthType(ctx context.Context, serviceName string) (s return scaffold.AuthType_PASSWORD, nil } } + +func detectCosmosSqlDatabaseContainersInDirectory(root string) ([]scaffold.CosmosSqlDatabaseContainer, error) { + var result []scaffold.CosmosSqlDatabaseContainer + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() && filepath.Ext(path) == ".java" { + container, err := detectCosmosSqlDatabaseContainerInFile(path) + if err != nil { + return err + } + if len(container.ContainerName) != 0 { + result = append(result, container) + } + } + return nil + }) + return result, err +} + +func detectCosmosSqlDatabaseContainerInFile(filePath string) (scaffold.CosmosSqlDatabaseContainer, error) { + var result scaffold.CosmosSqlDatabaseContainer + result.PartitionKeyPaths = make([]string, 0) + content, err := os.ReadFile(filePath) + if err != nil { + return result, err + } + // todo: + // 1. Maybe "@Container" is not "com.azure.spring.data.cosmos.core.mapping.Container" + // 2. Maybe "@Container" is imported by "com.azure.spring.data.cosmos.core.mapping.*" + containerRegex := regexp.MustCompile(`@Container\s*\(containerName\s*=\s*"([^"]+)"\)`) + partitionKeyRegex := regexp.MustCompile(`@PartitionKey\s*(?:\n\s*)?(?:private|public|protected)?\s*\w+\s+(\w+);`) + + matches := containerRegex.FindAllStringSubmatch(string(content), -1) + if len(matches) != 1 { + return result, nil + } + result.ContainerName = matches[0][1] + + matches = partitionKeyRegex.FindAllStringSubmatch(string(content), -1) + for _, match := range matches { + result.PartitionKeyPaths = append(result.PartitionKeyPaths, match[1]) + } + return result, nil +} diff --git a/cli/azd/internal/repository/infra_confirm_test.go b/cli/azd/internal/repository/infra_confirm_test.go index bebc7142884..0ccc91552ba 100644 --- a/cli/azd/internal/repository/infra_confirm_test.go +++ b/cli/azd/internal/repository/infra_confirm_test.go @@ -3,7 +3,10 @@ package repository import ( "context" "fmt" + "github.com/azure/azure-dev/cli/azd/pkg/osutil" + "github.com/stretchr/testify/assert" "os" + "path/filepath" "strings" "testing" @@ -232,3 +235,72 @@ func TestInitializer_infraSpecFromDetect(t *testing.T) { }) } } + +func TestDetectCosmosSqlDatabaseContainerInFile(t *testing.T) { + tests := []struct { + javaFileContent string + expectedContainers scaffold.CosmosSqlDatabaseContainer + }{ + { + javaFileContent: "", + expectedContainers: scaffold.CosmosSqlDatabaseContainer{ + ContainerName: "", + PartitionKeyPaths: []string{}, + }, + }, + { + javaFileContent: "@Container(containerName = \"users\")", + expectedContainers: scaffold.CosmosSqlDatabaseContainer{ + ContainerName: "users", + PartitionKeyPaths: []string{}, + }, + }, + { + javaFileContent: "" + + "@Container(containerName = \"users\")\n" + + "public class User {\n" + + " @Id\n " + + "private String id;\n" + + " private String firstName;\n" + + " @PartitionKey\n" + + " private String lastName;", + expectedContainers: scaffold.CosmosSqlDatabaseContainer{ + ContainerName: "users", + PartitionKeyPaths: []string{ + "/last_name", + }, + }, + }, + { + javaFileContent: "" + + "@Container(containerName = \"users\")\n" + + "public class User {\n" + + " @Id\n " + + "private String id;\n" + + " private String firstName;\n" + + " @PartitionKey private String lastName;", + expectedContainers: scaffold.CosmosSqlDatabaseContainer{ + ContainerName: "users", + PartitionKeyPaths: []string{ + "/last_name", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.javaFileContent, func(t *testing.T) { + tempDir := t.TempDir() + tempFile := filepath.Join(tempDir, "Example.java") + file, err := os.Create(tempFile) + assert.NoError(t, err) + file.Close() + + err = os.WriteFile(tempFile, []byte(tt.javaFileContent), osutil.PermissionFile) + assert.NoError(t, err) + + container, err := detectCosmosSqlDatabaseContainerInFile(tempFile) + assert.NoError(t, err) + assert.Equal(t, tt.expectedContainers, container) + }) + } +} diff --git a/cli/azd/internal/scaffold/spec.go b/cli/azd/internal/scaffold/spec.go index 59d5c29cc45..043db0c10d9 100644 --- a/cli/azd/internal/scaffold/spec.go +++ b/cli/azd/internal/scaffold/spec.go @@ -12,6 +12,7 @@ type InfraSpec struct { // Databases to create DbPostgres *DatabasePostgres DbMySql *DatabaseMySql + DbCosmos *DatabaseCosmosAccount DbCosmosMongo *DatabaseCosmosMongo DbRedis *DatabaseRedis @@ -45,6 +46,17 @@ type DatabaseMySql struct { AuthUsingUsernamePassword bool } +type CosmosSqlDatabaseContainer struct { + ContainerName string + PartitionKeyPaths []string +} + +type DatabaseCosmosAccount struct { + AccountName string + DatabaseName string + Containers []CosmosSqlDatabaseContainer +} + type DatabaseCosmosMongo struct { DatabaseName string } @@ -115,6 +127,7 @@ type ServiceSpec struct { DbPostgres *DatabaseReference DbMySql *DatabaseReference DbCosmosMongo *DatabaseReference + DbCosmos *DatabaseCosmosAccount DbRedis *DatabaseReference // AI model connections diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index 9a4c83e4a93..21b001aeab6 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -108,6 +108,56 @@ module cosmos 'br/public:avm/res/document-db/database-account:0.8.1' = { } } {{- end}} +{{- if .DbCosmos }} + +module cosmos 'br/public:avm/res/document-db/database-account:0.8.1' = { + name: 'cosmos' + params: { + name: '${abbrs.documentDBDatabaseAccounts}${resourceToken}' + tags: tags + location: location + locations: [ + { + failoverPriority: 0 + isZoneRedundant: false + locationName: location + } + ] + networkRestrictions: { + ipRules: [] + virtualNetworkRules: [] + publicNetworkAccess: 'Enabled' + } + sqlDatabases: [ + { + name: '{{ .DbCosmos.DatabaseName }}' + containers: [ + {{- range .DbCosmos.Containers}} + { + name: '{{ .ContainerName }}' + paths: [ + {{- range $path := .PartitionKeyPaths}} + '{{ $path }}' + {{- end}} + ] + } + {{- end}} + ] + } + ] + sqlRoleAssignmentsPrincipalIds: [ + {{- range .Services}} + {{bicepName .Name}}Identity.outputs.principalId + {{- end}} + ] + sqlRoleDefinitions: [ + { + name: 'service-access-cosmos-sql-role' + } + ] + } +} +{{- end}} {{- if .DbPostgres}} var postgreSqlDatabaseName = '{{ .DbPostgres.DatabaseName }}' @@ -685,6 +735,16 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { secretRef: 'mysql-password' } {{- end}} + {{- if .DbCosmos }} + { + name: 'SPRING_CLOUD_AZURE_COSMOS_ENDPOINT' + value: cosmos.outputs.endpoint + } + { + name: 'SPRING_CLOUD_AZURE_COSMOS_DATABASE' + value: '{{ .DbCosmos.DatabaseName }}' + } + {{- end}} {{- if .DbRedis}} { name: 'REDIS_HOST' From eb356d44c4d534b869f9edffe6ad07a79b114717 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Mon, 11 Nov 2024 14:42:16 +0800 Subject: [PATCH 56/92] Auto-detecting Redis in spring boot applications. (#16) --- cli/azd/internal/repository/detect_confirm.go | 2 +- cli/azd/resources/scaffold/templates/resources.bicept | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/cli/azd/internal/repository/detect_confirm.go b/cli/azd/internal/repository/detect_confirm.go index a60cfe41e3b..afe05c7ddce 100644 --- a/cli/azd/internal/repository/detect_confirm.go +++ b/cli/azd/internal/repository/detect_confirm.go @@ -240,7 +240,7 @@ func (d *detectConfirm) render(ctx context.Context) error { case appdetect.DbMongo: recommendedServices = append(recommendedServices, "Azure CosmosDB API for MongoDB") case appdetect.DbRedis: - recommendedServices = append(recommendedServices, "Azure Container Apps Redis add-on") + recommendedServices = append(recommendedServices, "Azure Cache for Redis") } status := "" diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index 21b001aeab6..fc0c9dc842a 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -766,6 +766,10 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { name: 'REDIS_PASSWORD' secretRef: 'redis-pass' } + { + name: 'SPRING_DATA_REDIS_URL' + secretRef: 'redis-url' + } {{- end}} {{- if .AIModels}} { From 9bdd8d697d3d421dfee38721c5b6051bb9daaba6 Mon Sep 17 00:00:00 2001 From: Xiaolu Dai Date: Mon, 11 Nov 2024 17:18:20 +0800 Subject: [PATCH 57/92] fix ut --- cli/azd/internal/repository/app_init.go | 48 ++++++++++--------- cli/azd/internal/repository/app_init_test.go | 2 + .../internal/repository/infra_confirm_test.go | 6 +-- .../testdata/empty/azureyaml_created.txt | 2 +- 4 files changed, 31 insertions(+), 27 deletions(-) diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index 0e2a015ea56..388117bd7c5 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -460,29 +460,31 @@ func (i *Initializer) prjConfigFromDetect( } } - for _, db := range prj.DatabaseDeps { - switch db { - case appdetect.DbMongo: - config.Resources["mongo"] = &project.ResourceConfig{ - Type: project.ResourceTypeDbMongo, - Name: spec.DbCosmosMongo.DatabaseName, - } - case appdetect.DbPostgres: - config.Resources["postgres"] = &project.ResourceConfig{ - Type: project.ResourceTypeDbPostgres, - Name: spec.DbPostgres.DatabaseName, - } - case appdetect.DbMySql: - config.Resources["mysql"] = &project.ResourceConfig{ - Type: project.ResourceTypeDbMySQL, - Props: project.MySQLProps{ - DatabaseName: spec.DbMySql.DatabaseName, - AuthType: "managedIdentity", - }, - } - case appdetect.DbRedis: - config.Resources["redis"] = &project.ResourceConfig{ - Type: project.ResourceTypeDbRedis, + if !addResources { + for _, db := range prj.DatabaseDeps { + switch db { + case appdetect.DbMongo: + config.Resources["mongo"] = &project.ResourceConfig{ + Type: project.ResourceTypeDbMongo, + Name: spec.DbCosmosMongo.DatabaseName, + } + case appdetect.DbPostgres: + config.Resources["postgres"] = &project.ResourceConfig{ + Type: project.ResourceTypeDbPostgres, + Name: spec.DbPostgres.DatabaseName, + } + case appdetect.DbMySql: + config.Resources["mysql"] = &project.ResourceConfig{ + Type: project.ResourceTypeDbMySQL, + Props: project.MySQLProps{ + DatabaseName: spec.DbMySql.DatabaseName, + AuthType: "managedIdentity", + }, + } + case appdetect.DbRedis: + config.Resources["redis"] = &project.ResourceConfig{ + Type: project.ResourceTypeDbRedis, + } } } } diff --git a/cli/azd/internal/repository/app_init_test.go b/cli/azd/internal/repository/app_init_test.go index 37652ea84e7..1721767a985 100644 --- a/cli/azd/internal/repository/app_init_test.go +++ b/cli/azd/internal/repository/app_init_test.go @@ -3,6 +3,7 @@ package repository import ( "context" "fmt" + "github.com/azure/azure-dev/cli/azd/internal/scaffold" "os" "path/filepath" "strings" @@ -308,6 +309,7 @@ func TestInitializer_prjConfigFromDetect(t *testing.T) { context.Background(), dir, tt.detect, + scaffold.InfraSpec{}, true) // Print extra newline to avoid mangling `go test -v` final test result output while waiting for final stdin, diff --git a/cli/azd/internal/repository/infra_confirm_test.go b/cli/azd/internal/repository/infra_confirm_test.go index 0ccc91552ba..0a2b8ebac3a 100644 --- a/cli/azd/internal/repository/infra_confirm_test.go +++ b/cli/azd/internal/repository/infra_confirm_test.go @@ -169,7 +169,7 @@ func TestInitializer_infraSpecFromDetect(t *testing.T) { "n", "my$special$db", "n", - "myappdb", // fill in db name + "myappdb", // fill in db name "Use user assigned managed identity", // confirm db authentication }, want: scaffold.InfraSpec{ @@ -267,7 +267,7 @@ func TestDetectCosmosSqlDatabaseContainerInFile(t *testing.T) { expectedContainers: scaffold.CosmosSqlDatabaseContainer{ ContainerName: "users", PartitionKeyPaths: []string{ - "/last_name", + "lastName", }, }, }, @@ -282,7 +282,7 @@ func TestDetectCosmosSqlDatabaseContainerInFile(t *testing.T) { expectedContainers: scaffold.CosmosSqlDatabaseContainer{ ContainerName: "users", PartitionKeyPaths: []string{ - "/last_name", + "lastName", }, }, }, diff --git a/cli/azd/internal/repository/testdata/empty/azureyaml_created.txt b/cli/azd/internal/repository/testdata/empty/azureyaml_created.txt index 7318d2a5007..5443f055e86 100644 --- a/cli/azd/internal/repository/testdata/empty/azureyaml_created.txt +++ b/cli/azd/internal/repository/testdata/empty/azureyaml_created.txt @@ -1,3 +1,3 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json +# yaml-language-server: $schema=https://raw.githubusercontent.com/azure-javaee/azure-dev/feature/sjad/schemas/alpha/azure.yaml.json name: "" From 9ba423ae415f0d081e236bf8e63860e09b60c0f2 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Tue, 12 Nov 2024 08:45:43 +0800 Subject: [PATCH 58/92] Delete service names because they are not used. (#17) --- cli/azd/internal/repository/infra_confirm.go | 111 +++++-------------- cli/azd/internal/scaffold/spec.go | 5 - 2 files changed, 29 insertions(+), 87 deletions(-) diff --git a/cli/azd/internal/repository/infra_confirm.go b/cli/azd/internal/repository/infra_confirm.go index 5f446fd16b4..731ef7cd09d 100644 --- a/cli/azd/internal/repository/infra_confirm.go +++ b/cli/azd/internal/repository/infra_confirm.go @@ -85,10 +85,6 @@ func (i *Initializer) infraSpecFromDetect( return scaffold.InfraSpec{}, err } spec.DbCosmos = &scaffold.DatabaseCosmosAccount{ - // todo: - // Now all services (except aca) are named by '${abbrs.xxx}${resourceToken}' - // Consider to name it by AccountName defined here. - AccountName: "not used for now", DatabaseName: dbName, Containers: containers, } @@ -99,7 +95,7 @@ func (i *Initializer) infraSpecFromDetect( } for _, azureDep := range detect.AzureDeps { - err := i.promptForAzureResource(ctx, azureDep.first, &spec) + err := i.buildInfraSpecByAzureDep(ctx, azureDep.first, &spec) if err != nil { return scaffold.InfraSpec{}, err } @@ -349,95 +345,46 @@ func (i *Initializer) getAuthType(ctx context.Context) (scaffold.AuthType, error return authType, nil } -func (i *Initializer) promptForAzureResource( +func (i *Initializer) buildInfraSpecByAzureDep( ctx context.Context, azureDep appdetect.AzureDep, 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.ResourceDisplay()), - 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.", - }) + switch dependency := azureDep.(type) { + case appdetect.AzureDepServiceBus: + authType, err := i.chooseAuthTypeByPrompt(ctx, azureDep.ResourceDisplay()) if err != nil { return 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 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 err - } - - if !confirm { - continue azureDepPrompt - } + spec.AzureServiceBus = &scaffold.AzureDepServiceBus{ + Queues: dependency.Queues, + AuthUsingConnectionString: authType == scaffold.AuthType_PASSWORD, + AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, } - - switch dependency := azureDep.(type) { - case appdetect.AzureDepServiceBus: - authType, err := i.chooseAuthType(ctx, azureDepName) - if err != nil { - return err - } - spec.AzureServiceBus = &scaffold.AzureDepServiceBus{ - Name: azureDepName, - Queues: dependency.Queues, - AuthUsingConnectionString: authType == scaffold.AuthType_PASSWORD, - AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, - } - case appdetect.AzureDepEventHubs: - authType, err := i.chooseAuthType(ctx, azureDepName) - if err != nil { - return err - } - spec.AzureEventHubs = &scaffold.AzureDepEventHubs{ - Name: azureDepName, - EventHubNames: dependency.Names, - AuthUsingConnectionString: authType == scaffold.AuthType_PASSWORD, - AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, - } - case appdetect.AzureDepStorageAccount: - authType, err := i.chooseAuthType(ctx, azureDepName) - if err != nil { - return err - } - spec.AzureStorageAccount = &scaffold.AzureDepStorageAccount{ - Name: azureDepName, - ContainerNames: dependency.ContainerNames, - AuthUsingConnectionString: authType == scaffold.AuthType_PASSWORD, - AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, - } + case appdetect.AzureDepEventHubs: + authType, err := i.chooseAuthTypeByPrompt(ctx, azureDep.ResourceDisplay()) + if err != nil { + return err + } + spec.AzureEventHubs = &scaffold.AzureDepEventHubs{ + EventHubNames: dependency.Names, + AuthUsingConnectionString: authType == scaffold.AuthType_PASSWORD, + AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, + } + case appdetect.AzureDepStorageAccount: + authType, err := i.chooseAuthTypeByPrompt(ctx, azureDep.ResourceDisplay()) + if err != nil { + return err + } + spec.AzureStorageAccount = &scaffold.AzureDepStorageAccount{ + ContainerNames: dependency.ContainerNames, + AuthUsingConnectionString: authType == scaffold.AuthType_PASSWORD, + AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, } - break azureDepPrompt } return nil } -func (i *Initializer) chooseAuthType(ctx context.Context, serviceName string) (scaffold.AuthType, error) { +func (i *Initializer) chooseAuthTypeByPrompt(ctx context.Context, serviceName string) (scaffold.AuthType, error) { portOptions := []string{ "User assigned managed identity", "Connection string", diff --git a/cli/azd/internal/scaffold/spec.go b/cli/azd/internal/scaffold/spec.go index 043db0c10d9..9d5a94e57cc 100644 --- a/cli/azd/internal/scaffold/spec.go +++ b/cli/azd/internal/scaffold/spec.go @@ -22,7 +22,6 @@ type InfraSpec struct { AzureServiceBus *AzureDepServiceBus AzureEventHubs *AzureDepEventHubs AzureStorageAccount *AzureDepStorageAccount - } type Parameter struct { @@ -52,7 +51,6 @@ type CosmosSqlDatabaseContainer struct { } type DatabaseCosmosAccount struct { - AccountName string DatabaseName string Containers []CosmosSqlDatabaseContainer } @@ -79,7 +77,6 @@ type AIModelModel struct { } type AzureDepServiceBus struct { - Name string Queues []string TopicsAndSubscriptions map[string][]string AuthUsingConnectionString bool @@ -87,14 +84,12 @@ type AzureDepServiceBus struct { } type AzureDepEventHubs struct { - Name string EventHubNames []string AuthUsingConnectionString bool AuthUsingManagedIdentity bool } type AzureDepStorageAccount struct { - Name string ContainerNames []string AuthUsingConnectionString bool AuthUsingManagedIdentity bool From e161608493cf95226166a0455b8ff9267dc294ef Mon Sep 17 00:00:00 2001 From: Xiaolu Dai <31124698+saragluna@users.noreply.github.com> Date: Tue, 12 Nov 2024 10:49:25 +0800 Subject: [PATCH 59/92] Add support for Service Bus JMS (#19) * support svcbus jms --- cli/azd/internal/appdetect/appdetect.go | 1 + cli/azd/internal/appdetect/java.go | 8 ++++++++ cli/azd/internal/repository/infra_confirm.go | 1 + cli/azd/internal/scaffold/spec.go | 1 + .../resources/scaffold/templates/resources.bicept | 14 ++++++++++++-- 5 files changed, 23 insertions(+), 2 deletions(-) diff --git a/cli/azd/internal/appdetect/appdetect.go b/cli/azd/internal/appdetect/appdetect.go index ef1a9c708bb..3a0d9232b68 100644 --- a/cli/azd/internal/appdetect/appdetect.go +++ b/cli/azd/internal/appdetect/appdetect.go @@ -142,6 +142,7 @@ type AzureDep interface { type AzureDepServiceBus struct { Queues []string + IsJms bool } func (a AzureDepServiceBus) ResourceDisplay() string { diff --git a/cli/azd/internal/appdetect/java.go b/cli/azd/internal/appdetect/java.go index 2bbde68271f..4f754b9c389 100644 --- a/cli/azd/internal/appdetect/java.go +++ b/cli/azd/internal/appdetect/java.go @@ -177,6 +177,13 @@ func detectDependencies(mavenProject *mavenProject, project *Project) (*Project, databaseDepMap[DbMongo] = struct{}{} } + // we need to figure out multiple projects are using the same service bus + if dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-starter-servicebus-jms" { + project.AzureDeps = append(project.AzureDeps, AzureDepServiceBus{ + IsJms: true, + }) + } + if dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-stream-binder-servicebus" { bindingDestinations := findBindingDestinations(applicationProperties) destinations := make([]string, 0, len(bindingDestinations)) @@ -186,6 +193,7 @@ func detectDependencies(mavenProject *mavenProject, project *Project) (*Project, } project.AzureDeps = append(project.AzureDeps, AzureDepServiceBus{ Queues: destinations, + IsJms: false, }) } diff --git a/cli/azd/internal/repository/infra_confirm.go b/cli/azd/internal/repository/infra_confirm.go index 731ef7cd09d..aefea1cd69a 100644 --- a/cli/azd/internal/repository/infra_confirm.go +++ b/cli/azd/internal/repository/infra_confirm.go @@ -356,6 +356,7 @@ func (i *Initializer) buildInfraSpecByAzureDep( return err } spec.AzureServiceBus = &scaffold.AzureDepServiceBus{ + IsJms: dependency.IsJms, Queues: dependency.Queues, AuthUsingConnectionString: authType == scaffold.AuthType_PASSWORD, AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, diff --git a/cli/azd/internal/scaffold/spec.go b/cli/azd/internal/scaffold/spec.go index 9d5a94e57cc..8133ae5f474 100644 --- a/cli/azd/internal/scaffold/spec.go +++ b/cli/azd/internal/scaffold/spec.go @@ -81,6 +81,7 @@ type AzureDepServiceBus struct { TopicsAndSubscriptions map[string][]string AuthUsingConnectionString bool AuthUsingManagedIdentity bool + IsJms bool } type AzureDepEventHubs struct { diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index fc0c9dc842a..f90a437bd7a 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -846,7 +846,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { } {{- end}} - {{- if .AzureServiceBus }} + {{- if and .AzureServiceBus (not .AzureServiceBus.IsJms)}} { name: 'SPRING_CLOUD_AZURE_SERVICEBUS_NAMESPACE' value: serviceBusNamespace.outputs.name @@ -866,7 +866,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { value: {{bicepName .Name}}Identity.outputs.clientId } {{- end}} - {{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingConnectionString) }} + {{- if (and (and .AzureServiceBus .AzureServiceBus.AuthUsingConnectionString) (not .AzureServiceBus.IsJms)) }} { name: 'SPRING_CLOUD_AZURE_SERVICEBUS_CONNECTIONSTRING' secretRef: 'servicebus-connection-string' @@ -880,6 +880,16 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { value: '' } {{- end}} + {{- if (and (and .AzureServiceBus .AzureServiceBus.AuthUsingConnectionString) .AzureServiceBus.IsJms) }} + { + name: 'SPRING_JMS_SERVICEBUS_CONNECTIONSTRING' + secretRef: 'servicebus-connection-string' + } + { + name: 'SPRING_JMS_SERVICEBUS_PRICINGTIER' + value: 'premium' + } + {{- end}} {{- if .Frontend}} {{- range $i, $e := .Frontend.Backends}} From efd325d0daec8fde5798d4ffe6c5e33e980e8ecc Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Tue, 12 Nov 2024 15:30:58 +0800 Subject: [PATCH 60/92] Only provide "SPRING_DATASOURCE_URL" environment variable when using username & password as auth type. (#20) --- .../scaffold/templates/resources.bicept | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index f90a437bd7a..c65fc894e08 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -668,10 +668,6 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { name: 'POSTGRES_PORT' value: '5432' } - { - name: 'SPRING_DATASOURCE_URL' - value: 'jdbc:postgresql://${postgreServer.outputs.fqdn}:5432/${postgreSqlDatabaseName}' - } {{- end}} {{- if (and .DbPostgres .DbPostgres.AuthUsingUsernamePassword) }} { @@ -686,6 +682,10 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { name: 'POSTGRES_PASSWORD' secretRef: 'postgresql-password' } + { + name: 'SPRING_DATASOURCE_URL' + value: 'jdbc:postgresql://${postgreServer.outputs.fqdn}:5432/${postgreSqlDatabaseName}' + } { name: 'SPRING_DATASOURCE_USERNAME' value: postgreSqlDatabaseUser @@ -708,10 +708,6 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { name: 'MYSQL_PORT' value: '3306' } - { - name: 'SPRING_DATASOURCE_URL' - value: 'jdbc:mysql://${mysqlServer.outputs.fqdn}:3306/${mysqlDatabaseName}' - } {{- end}} {{- if (and .DbMySql .DbMySql.AuthUsingUsernamePassword) }} { @@ -726,6 +722,10 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { name: 'MYSQL_PASSWORD' secretRef: 'mysql-password' } + { + name: 'SPRING_DATASOURCE_URL' + value: 'jdbc:mysql://${mysqlServer.outputs.fqdn}:3306/${mysqlDatabaseName}' + } { name: 'SPRING_DATASOURCE_USERNAME' value: mysqlDatabaseUser From 5ee0aa0c13b7e8f434cc2b68f6f17c0aab0516ed Mon Sep 17 00:00:00 2001 From: HaoZhang Date: Tue, 12 Nov 2024 16:05:24 +0800 Subject: [PATCH 61/92] Support detecting Kafka by analyzing pom.xml and application.yml (#18) * support detecting kafka by analyzing pom.xml and application.yml * reuse EventHub, and lowercase Env Var * small fix * small fix --------- Co-authored-by: haozhang --- cli/azd/internal/appdetect/appdetect.go | 3 ++- cli/azd/internal/appdetect/java.go | 18 +++++++++++++- cli/azd/internal/repository/infra_confirm.go | 1 + cli/azd/internal/scaffold/spec.go | 1 + .../scaffold/templates/resources.bicept | 24 ++++++++++++------- 5 files changed, 36 insertions(+), 11 deletions(-) diff --git a/cli/azd/internal/appdetect/appdetect.go b/cli/azd/internal/appdetect/appdetect.go index 3a0d9232b68..480f9ad865f 100644 --- a/cli/azd/internal/appdetect/appdetect.go +++ b/cli/azd/internal/appdetect/appdetect.go @@ -150,7 +150,8 @@ func (a AzureDepServiceBus) ResourceDisplay() string { } type AzureDepEventHubs struct { - Names []string + Names []string + UseKafka bool } func (a AzureDepEventHubs) ResourceDisplay() string { diff --git a/cli/azd/internal/appdetect/java.go b/cli/azd/internal/appdetect/java.go index 4f754b9c389..f725af813a2 100644 --- a/cli/azd/internal/appdetect/java.go +++ b/cli/azd/internal/appdetect/java.go @@ -211,7 +211,8 @@ func detectDependencies(mavenProject *mavenProject, project *Project) (*Project, } } project.AzureDeps = append(project.AzureDeps, AzureDepEventHubs{ - Names: destinations, + Names: destinations, + UseKafka: false, }) if containsInBinding { project.AzureDeps = append(project.AzureDeps, AzureDepStorageAccount{ @@ -220,6 +221,21 @@ func detectDependencies(mavenProject *mavenProject, project *Project) (*Project, }) } } + + if dep.GroupId == "org.springframework.cloud" && dep.ArtifactId == "spring-cloud-starter-stream-kafka" { + bindingDestinations := findBindingDestinations(applicationProperties) + var destinations []string + for bindingName, destination := range bindingDestinations { + if !contains(destinations, destination) { + destinations = append(destinations, destination) + log.Printf("Kafka Topic [%s] found for binding [%s]", destination, bindingName) + } + } + project.AzureDeps = append(project.AzureDeps, AzureDepEventHubs{ + Names: destinations, + UseKafka: true, + }) + } } if len(databaseDepMap) > 0 { diff --git a/cli/azd/internal/repository/infra_confirm.go b/cli/azd/internal/repository/infra_confirm.go index aefea1cd69a..6cd183df2ed 100644 --- a/cli/azd/internal/repository/infra_confirm.go +++ b/cli/azd/internal/repository/infra_confirm.go @@ -370,6 +370,7 @@ func (i *Initializer) buildInfraSpecByAzureDep( EventHubNames: dependency.Names, AuthUsingConnectionString: authType == scaffold.AuthType_PASSWORD, AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, + UseKafka: dependency.UseKafka, } case appdetect.AzureDepStorageAccount: authType, err := i.chooseAuthTypeByPrompt(ctx, azureDep.ResourceDisplay()) diff --git a/cli/azd/internal/scaffold/spec.go b/cli/azd/internal/scaffold/spec.go index 8133ae5f474..11eaa273765 100644 --- a/cli/azd/internal/scaffold/spec.go +++ b/cli/azd/internal/scaffold/spec.go @@ -88,6 +88,7 @@ type AzureDepEventHubs struct { EventHubNames []string AuthUsingConnectionString bool AuthUsingManagedIdentity bool + UseKafka bool } type AzureDepStorageAccount struct { diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index c65fc894e08..d1fba000193 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -777,37 +777,43 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { value: account.outputs.endpoint } {{- end}} - {{- if .AzureEventHubs }} + {{- if (and .AzureEventHubs (not .AzureEventHubs.UseKafka)) }} { name: 'SPRING_CLOUD_AZURE_EVENTHUBS_NAMESPACE' value: eventHubNamespace.outputs.name } {{- end}} - {{- if (and .AzureEventHubs .AzureEventHubs.AuthUsingManagedIdentity) }} + {{- if (and .AzureEventHubs .AzureEventHubs.UseKafka) }} { - name: 'SPRING_CLOUD_AZURE_EVENTHUBS_CONNECTIONSTRING' - value: '' + name: 'spring.cloud.stream.kafka.binder.brokers' + value: '${eventHubNamespace.outputs.name}.servicebus.windows.net:9093' } + {{- end}} + {{- if (and .AzureEventHubs .AzureEventHubs.AuthUsingManagedIdentity) }} { - name: 'SPRING_CLOUD_AZURE_EVENTHUBS_CREDENTIAL_MANAGEDIDENTITYENABLED' + name: 'spring.cloud.azure.eventhubs.credential.managed-identity-enabled' value: 'true' } { - name: 'SPRING_CLOUD_AZURE_EVENTHUBS_CREDENTIAL_CLIENTID' + name: 'spring.cloud.azure.eventhubs.credential.client-id' value: {{bicepName .Name}}Identity.outputs.clientId } + { + name: 'spring.cloud.azure.eventhubs.connection-string' + value: '' + } {{- end}} {{- if (and .AzureEventHubs .AzureEventHubs.AuthUsingConnectionString) }} { - name: 'SPRING_CLOUD_AZURE_EVENTHUBS_CONNECTIONSTRING' + name: 'spring.cloud.azure.eventhubs.connection-string' secretRef: 'event-hubs-connection-string' } { - name: 'SPRING_CLOUD_AZURE_EVENTHUBS_CREDENTIAL_MANAGEDIDENTITYENABLED' + name: 'spring.cloud.azure.eventhubs.credential.managed-identity-enabled' value: 'false' } { - name: 'SPRING_CLOUD_AZURE_EVENTHUBS_CREDENTIAL_CLIENTID' + name: 'spring.cloud.azure.eventhubs.credential.client-id' value: '' } {{- end}} From 18a7cbca296c02ab1c262d10221fcc057e96efc5 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Tue, 12 Nov 2024 17:14:09 +0800 Subject: [PATCH 62/92] Change all spring related environment variable to lower-case-with-dash to avoid problems caused by ENVIRONMENT_VARIABLES_WITH_UNDERSCORE. (#21) --- .../scaffold/templates/resources.bicept | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index d1fba000193..a4aa3ae8b47 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -647,11 +647,11 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { secretRef: 'mongodb-url' } { - name: 'SPRING_DATA_MONGODB_URI' + name: 'spring.data.mongodb.uri' secretRef: 'mongodb-url' } { - name: 'SPRING_DATA_MONGODB_DATABASE' + name: 'spring.data.mongodb.database' value: '{{ .DbCosmosMongo.DatabaseName }}' } {{- end}} @@ -683,15 +683,15 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { secretRef: 'postgresql-password' } { - name: 'SPRING_DATASOURCE_URL' + name: 'spring.datasource.url' value: 'jdbc:postgresql://${postgreServer.outputs.fqdn}:5432/${postgreSqlDatabaseName}' } { - name: 'SPRING_DATASOURCE_USERNAME' + name: 'spring.datasource.username' value: postgreSqlDatabaseUser } { - name: 'SPRING_DATASOURCE_PASSWORD' + name: 'spring.datasource.password' secretRef: 'postgresql-password' } {{- end}} @@ -723,25 +723,25 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { secretRef: 'mysql-password' } { - name: 'SPRING_DATASOURCE_URL' + name: 'spring.datasource.url' value: 'jdbc:mysql://${mysqlServer.outputs.fqdn}:3306/${mysqlDatabaseName}' } { - name: 'SPRING_DATASOURCE_USERNAME' + name: 'spring.datasource.username' value: mysqlDatabaseUser } { - name: 'SPRING_DATASOURCE_PASSWORD' + name: 'spring.datasource.password' secretRef: 'mysql-password' } {{- end}} {{- if .DbCosmos }} { - name: 'SPRING_CLOUD_AZURE_COSMOS_ENDPOINT' + name: 'spring.cloud.azure.cosmos.endpoint' value: cosmos.outputs.endpoint } { - name: 'SPRING_CLOUD_AZURE_COSMOS_DATABASE' + name: 'spring.cloud.azure.cosmos.database' value: '{{ .DbCosmos.DatabaseName }}' } {{- end}} @@ -767,7 +767,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { secretRef: 'redis-pass' } { - name: 'SPRING_DATA_REDIS_URL' + name: 'spring.data.redis.url' secretRef: 'redis-url' } {{- end}} @@ -779,7 +779,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { {{- end}} {{- if (and .AzureEventHubs (not .AzureEventHubs.UseKafka)) }} { - name: 'SPRING_CLOUD_AZURE_EVENTHUBS_NAMESPACE' + name: 'spring.cloud.azure.eventhubs.namespace' value: eventHubNamespace.outputs.name } {{- end}} @@ -819,80 +819,80 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { {{- end}} {{- if .AzureStorageAccount }} { - name: 'SPRING_CLOUD_AZURE_EVENTHUBS_PROCESSOR_CHECKPOINTSTORE_ACCOUNTNAME' + name: 'spring.cloud.azure.eventhubs.processor.checkpoint-store.account-name' value: storageAccountName } {{- end}} {{- if (and .AzureStorageAccount .AzureStorageAccount.AuthUsingManagedIdentity) }} { - name: 'SPRING_CLOUD_AZURE_EVENTHUBS_PROCESSOR_CHECKPOINTSTORE_CONNECTIONSTRING' + name: 'spring.cloud.azure.eventhubs.processor.checkpoint-store.connection-string' value: '' } { - name: 'SPRING_CLOUD_AZURE_EVENTHUBS_PROCESSOR_CHECKPOINTSTORE_CREDENTIAL_MANAGEDIDENTITYENABLED' + name: 'spring.cloud.azure.eventhubs.processor.checkpoint-store.credential.managed-identity-enabled' value: 'true' } { - name: 'SPRING_CLOUD_AZURE_EVENTHUBS_PROCESSOR_CHECKPOINTSTORE_CREDENTIAL_CLIENTID' + name: 'spring.cloud.azure.eventhubs.processor.checkpoint-store.credential.client-id' value: {{bicepName .Name}}Identity.outputs.clientId } {{- end}} {{- if (and .AzureStorageAccount .AzureStorageAccount.AuthUsingConnectionString) }} { - name: 'SPRING_CLOUD_AZURE_EVENTHUBS_PROCESSOR_CHECKPOINTSTORE_CONNECTIONSTRING' + name: 'spring.cloud.azure.eventhubs.processor.checkpoint-store.connection-string' secretRef: 'storage-account-connection-string' } { - name: 'SPRING_CLOUD_AZURE_EVENTHUBS_PROCESSOR_CHECKPOINTSTORE_CREDENTIAL_MANAGEDIDENTITYENABLED' + name: 'spring.cloud.azure.eventhubs.processor.checkpoint-store.credential.managed-identity-enabled' value: 'false' } { - name: 'SPRING_CLOUD_AZURE_EVENTHUBS_PROCESSOR_CHECKPOINTSTORE_CREDENTIAL_CLIENTID' + name: 'spring.cloud.azure.eventhubs.processor.checkpoint-store.credential.client-id' value: '' } {{- end}} {{- if and .AzureServiceBus (not .AzureServiceBus.IsJms)}} { - name: 'SPRING_CLOUD_AZURE_SERVICEBUS_NAMESPACE' + name: 'spring.cloud.azure.servicebus.namespace' value: serviceBusNamespace.outputs.name } {{- end}} {{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingManagedIdentity) }} { - name: 'SPRING_CLOUD_AZURE_SERVICEBUS_CONNECTIONSTRING' + name: 'spring.cloud.azure.servicebus.connection-string' value: '' } { - name: 'SPRING_CLOUD_AZURE_SERVICEBUS_CREDENTIAL_MANAGEDIDENTITYENABLED' + name: 'spring.cloud.azure.servicebus.credential.managed-identity-enabled' value: 'true' } { - name: 'SPRING_CLOUD_AZURE_SERVICEBUS_CREDENTIAL_CLIENTID' + name: 'spring.cloud.azure.servicebus.credential.client-id' value: {{bicepName .Name}}Identity.outputs.clientId } {{- end}} {{- if (and (and .AzureServiceBus .AzureServiceBus.AuthUsingConnectionString) (not .AzureServiceBus.IsJms)) }} { - name: 'SPRING_CLOUD_AZURE_SERVICEBUS_CONNECTIONSTRING' + name: 'spring.cloud.azure.servicebus.connection-string' secretRef: 'servicebus-connection-string' } { - name: 'SPRING_CLOUD_AZURE_SERVICEBUS_CREDENTIAL_MANAGEDIDENTITYENABLED' + name: 'spring.cloud.azure.servicebus.credential.managed-identity-enabled' value: 'false' } { - name: 'SPRING_CLOUD_AZURE_EVENTHUBS_CREDENTIAL_CLIENTID' + name: 'spring.cloud.azure.eventhubs.credential.client-id' value: '' } {{- end}} {{- if (and (and .AzureServiceBus .AzureServiceBus.AuthUsingConnectionString) .AzureServiceBus.IsJms) }} { - name: 'SPRING_JMS_SERVICEBUS_CONNECTIONSTRING' + name: 'spring.jms.servicebus.connection-string' secretRef: 'servicebus-connection-string' } { - name: 'SPRING_JMS_SERVICEBUS_PRICINGTIER' + name: 'spring.jms.servicebus.pricing-tier' value: 'premium' } {{- end}} From f70b04638b3f1205e2075fd7fd9aa909723a53dd Mon Sep 17 00:00:00 2001 From: Xiaolu Dai <31124698+saragluna@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:45:29 +0800 Subject: [PATCH 63/92] Output the resources to azure.yaml when enable compose mode (#22) * generate resources in azure.yaml --- cli/azd/internal/auth_type.go | 14 +++ cli/azd/internal/repository/app_init.go | 96 ++++++++++++++++++- cli/azd/internal/repository/app_init_test.go | 2 +- cli/azd/internal/repository/infra_confirm.go | 94 +++++++++--------- .../internal/repository/infra_confirm_test.go | 8 +- cli/azd/internal/scaffold/spec.go | 52 ++++------ cli/azd/pkg/project/resources.go | 94 ++++++++++++++++-- .../scaffold/templates/resources.bicept | 66 ++++++------- 8 files changed, 292 insertions(+), 134 deletions(-) create mode 100644 cli/azd/internal/auth_type.go diff --git a/cli/azd/internal/auth_type.go b/cli/azd/internal/auth_type.go new file mode 100644 index 00000000000..ea8abcae097 --- /dev/null +++ b/cli/azd/internal/auth_type.go @@ -0,0 +1,14 @@ +package internal + +// AuthType defines different authentication types. +type AuthType string + +const ( + AuthTypeUnspecified AuthType = "UNSPECIFIED" + // Username and password, or key based authentication + AuthtypePassword AuthType = "PASSWORD" + // Connection string authentication + AuthtypeConnectionString AuthType = "CONNECTION_STRING" + // Microsoft EntraID token credential + AuthtypeManagedIdentity AuthType = "MANAGED_IDENTITY" +) diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index 388117bd7c5..e8959abe420 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -276,7 +276,7 @@ func (i *Initializer) InitFromApp( title = "Generating " + output.WithHighLightFormat("./"+azdcontext.ProjectFileName) i.console.ShowSpinner(ctx, title, input.Step) - err = i.genProjectFile(ctx, azdCtx, detect, *infraSpec, composeEnabled) + err = i.genProjectFile(ctx, azdCtx, detect, infraSpec, composeEnabled) if err != nil { i.console.StopSpinner(ctx, title, input.GetStepResultFormat(err)) return err @@ -370,7 +370,7 @@ func (i *Initializer) genProjectFile( ctx context.Context, azdCtx *azdcontext.AzdContext, detect detectConfirm, - spec scaffold.InfraSpec, + spec *scaffold.InfraSpec, addResources bool) error { config, err := i.prjConfigFromDetect(ctx, azdCtx.ProjectDirectory(), detect, spec, addResources) if err != nil { @@ -393,7 +393,7 @@ func (i *Initializer) prjConfigFromDetect( ctx context.Context, root string, detect detectConfirm, - spec scaffold.InfraSpec, + spec *scaffold.InfraSpec, addResources bool) (project.ProjectConfig, error) { config := project.ProjectConfig{ Name: azdcontext.ProjectName(root), @@ -472,23 +472,73 @@ func (i *Initializer) prjConfigFromDetect( config.Resources["postgres"] = &project.ResourceConfig{ Type: project.ResourceTypeDbPostgres, Name: spec.DbPostgres.DatabaseName, + Props: project.PostgresProps{ + DatabaseName: spec.DbPostgres.DatabaseName, + AuthType: spec.DbPostgres.AuthType, + }, } case appdetect.DbMySql: config.Resources["mysql"] = &project.ResourceConfig{ Type: project.ResourceTypeDbMySQL, Props: project.MySQLProps{ DatabaseName: spec.DbMySql.DatabaseName, - AuthType: "managedIdentity", + AuthType: spec.DbMySql.AuthType, }, } case appdetect.DbRedis: config.Resources["redis"] = &project.ResourceConfig{ Type: project.ResourceTypeDbRedis, } + case appdetect.DbCosmos: + cosmosDBProps := project.CosmosDBProps{ + DatabaseName: spec.DbCosmos.DatabaseName, + } + for _, container := range spec.DbCosmos.Containers { + cosmosDBProps.Containers = append(cosmosDBProps.Containers, project.CosmosDBContainerProps{ + ContainerName: container.ContainerName, + PartitionKeyPaths: container.PartitionKeyPaths, + }) + } + config.Resources["cosmos"] = &project.ResourceConfig{ + Type: project.ResourceTypeDbCosmos, + Props: cosmosDBProps, + } } + } - } + for _, azureDep := range prj.AzureDeps { + switch azureDep.(type) { + case appdetect.AzureDepServiceBus: + config.Resources["servicebus"] = &project.ResourceConfig{ + Type: project.ResourceTypeMessagingServiceBus, + Props: project.ServiceBusProps{ + Queues: spec.AzureServiceBus.Queues, + IsJms: spec.AzureServiceBus.IsJms, + AuthType: spec.AzureServiceBus.AuthType, + }, + } + case appdetect.AzureDepEventHubs: + if spec.AzureEventHubs.UseKafka { + config.Resources["kafka"] = &project.ResourceConfig{ + Type: project.ResourceTypeMessagingKafka, + Props: project.KafkaProps{ + Topics: spec.AzureEventHubs.EventHubNames, + AuthType: spec.AzureEventHubs.AuthType, + }, + } + } else { + config.Resources["eventhubs"] = &project.ResourceConfig{ + Type: project.ResourceTypeMessagingEventHubs, + Props: project.EventHubsProps{ + EventHubNames: spec.AzureEventHubs.EventHubNames, + AuthType: spec.AzureEventHubs.AuthType, + }, + } + } + } + } + } name := filepath.Base(rel) if name == "." { name = config.Name @@ -526,6 +576,10 @@ func (i *Initializer) prjConfigFromDetect( dbType = project.ResourceTypeDbMongo case appdetect.DbPostgres: dbType = project.ResourceTypeDbPostgres + case appdetect.DbMySql: + dbType = project.ResourceTypeDbMySQL + case appdetect.DbCosmos: + dbType = project.ResourceTypeDbCosmos } db := project.ResourceConfig{ @@ -551,6 +605,38 @@ func (i *Initializer) prjConfigFromDetect( dbNames[database] = db.Name } + for _, azureDepPair := range detect.AzureDeps { + azureDep := azureDepPair.first + authType, err := i.chooseAuthTypeByPrompt(ctx, azureDep.ResourceDisplay()) + if err != nil { + return config, err + } + switch azureDep.(type) { + case appdetect.AzureDepServiceBus: + azureDepServiceBus := azureDep.(appdetect.AzureDepServiceBus) + config.Resources["servicebus"] = &project.ResourceConfig{ + Type: project.ResourceTypeMessagingServiceBus, + Props: project.ServiceBusProps{ + Queues: azureDepServiceBus.Queues, + IsJms: azureDepServiceBus.IsJms, + AuthType: authType, + }, + } + case appdetect.AzureDepEventHubs: + config.Resources["eventhubs"] = &project.ResourceConfig{ + Type: project.ResourceTypeMessagingEventHubs, + Props: project.EventHubsProps{ + EventHubNames: spec.AzureEventHubs.EventHubNames, + AuthType: authType, + }, + } + case appdetect.AzureDepStorageAccount: + config.Resources["storage"] = &project.ResourceConfig{ + Type: project.ResourceTypeStorage, + } + } + } + backends := []*project.ResourceConfig{} frontends := []*project.ResourceConfig{} diff --git a/cli/azd/internal/repository/app_init_test.go b/cli/azd/internal/repository/app_init_test.go index 1721767a985..29be2930407 100644 --- a/cli/azd/internal/repository/app_init_test.go +++ b/cli/azd/internal/repository/app_init_test.go @@ -309,7 +309,7 @@ func TestInitializer_prjConfigFromDetect(t *testing.T) { context.Background(), dir, tt.detect, - scaffold.InfraSpec{}, + &scaffold.InfraSpec{}, true) // Print extra newline to avoid mangling `go test -v` final test result output while waiting for final stdin, diff --git a/cli/azd/internal/repository/infra_confirm.go b/cli/azd/internal/repository/infra_confirm.go index 6cd183df2ed..ffb560d2fd2 100644 --- a/cli/azd/internal/repository/infra_confirm.go +++ b/cli/azd/internal/repository/infra_confirm.go @@ -3,6 +3,7 @@ package repository import ( "context" "fmt" + "github.com/azure/azure-dev/cli/azd/internal" "os" "path/filepath" "regexp" @@ -55,9 +56,8 @@ func (i *Initializer) infraSpecFromDetect( return scaffold.InfraSpec{}, err } spec.DbPostgres = &scaffold.DatabasePostgres{ - DatabaseName: dbName, - AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, - AuthUsingUsernamePassword: authType == scaffold.AuthType_PASSWORD, + DatabaseName: dbName, + AuthType: authType, } break dbPrompt case appdetect.DbMySql: @@ -70,9 +70,8 @@ func (i *Initializer) infraSpecFromDetect( return scaffold.InfraSpec{}, err } spec.DbMySql = &scaffold.DatabaseMySql{ - DatabaseName: dbName, - AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, - AuthUsingUsernamePassword: authType == scaffold.AuthType_PASSWORD, + DatabaseName: dbName, + AuthType: authType, } break dbPrompt case appdetect.DbCosmos: @@ -133,15 +132,13 @@ func (i *Initializer) infraSpecFromDetect( } case appdetect.DbPostgres: serviceSpec.DbPostgres = &scaffold.DatabaseReference{ - DatabaseName: spec.DbPostgres.DatabaseName, - AuthUsingManagedIdentity: spec.DbPostgres.AuthUsingManagedIdentity, - AuthUsingUsernamePassword: spec.DbPostgres.AuthUsingUsernamePassword, + DatabaseName: spec.DbPostgres.DatabaseName, + AuthType: spec.DbPostgres.AuthType, } case appdetect.DbMySql: serviceSpec.DbMySql = &scaffold.DatabaseReference{ - DatabaseName: spec.DbMySql.DatabaseName, - AuthUsingManagedIdentity: spec.DbMySql.AuthUsingManagedIdentity, - AuthUsingUsernamePassword: spec.DbMySql.AuthUsingUsernamePassword, + DatabaseName: spec.DbMySql.DatabaseName, + AuthType: spec.DbMySql.AuthType, } case appdetect.DbCosmos: serviceSpec.DbCosmos = spec.DbCosmos @@ -322,29 +319,6 @@ func promptPort( return port, 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) buildInfraSpecByAzureDep( ctx context.Context, azureDep appdetect.AzureDep, @@ -356,10 +330,9 @@ func (i *Initializer) buildInfraSpecByAzureDep( return err } spec.AzureServiceBus = &scaffold.AzureDepServiceBus{ - IsJms: dependency.IsJms, - Queues: dependency.Queues, - AuthUsingConnectionString: authType == scaffold.AuthType_PASSWORD, - AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, + IsJms: dependency.IsJms, + Queues: dependency.Queues, + AuthType: authType, } case appdetect.AzureDepEventHubs: authType, err := i.chooseAuthTypeByPrompt(ctx, azureDep.ResourceDisplay()) @@ -367,10 +340,9 @@ func (i *Initializer) buildInfraSpecByAzureDep( return err } spec.AzureEventHubs = &scaffold.AzureDepEventHubs{ - EventHubNames: dependency.Names, - AuthUsingConnectionString: authType == scaffold.AuthType_PASSWORD, - AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, - UseKafka: dependency.UseKafka, + EventHubNames: dependency.Names, + AuthType: authType, + UseKafka: dependency.UseKafka, } case appdetect.AzureDepStorageAccount: authType, err := i.chooseAuthTypeByPrompt(ctx, azureDep.ResourceDisplay()) @@ -378,15 +350,14 @@ func (i *Initializer) buildInfraSpecByAzureDep( return err } spec.AzureStorageAccount = &scaffold.AzureDepStorageAccount{ - ContainerNames: dependency.ContainerNames, - AuthUsingConnectionString: authType == scaffold.AuthType_PASSWORD, - AuthUsingManagedIdentity: authType == scaffold.AuthType_TOKEN_CREDENTIAL, + ContainerNames: dependency.ContainerNames, + AuthType: authType, } } return nil } -func (i *Initializer) chooseAuthTypeByPrompt(ctx context.Context, serviceName string) (scaffold.AuthType, error) { +func (i *Initializer) chooseAuthTypeByPrompt(ctx context.Context, serviceName string) (internal.AuthType, error) { portOptions := []string{ "User assigned managed identity", "Connection string", @@ -396,13 +367,36 @@ func (i *Initializer) chooseAuthTypeByPrompt(ctx context.Context, serviceName st Options: portOptions, }) if err != nil { - return scaffold.AUTH_TYPE_UNSPECIFIED, err + return internal.AuthTypeUnspecified, err } if selection == 0 { - return scaffold.AuthType_TOKEN_CREDENTIAL, nil + return internal.AuthtypeManagedIdentity, nil } else { - return scaffold.AuthType_PASSWORD, nil + return internal.AuthtypeConnectionString, nil + } +} + +func (i *Initializer) getAuthType(ctx context.Context) (internal.AuthType, error) { + authType := internal.AuthTypeUnspecified + 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 = internal.AuthtypeManagedIdentity + case 1: + authType = internal.AuthtypePassword + default: + panic("unhandled selection") } + return authType, nil } func detectCosmosSqlDatabaseContainersInDirectory(root string) ([]scaffold.CosmosSqlDatabaseContainer, error) { diff --git a/cli/azd/internal/repository/infra_confirm_test.go b/cli/azd/internal/repository/infra_confirm_test.go index 0a2b8ebac3a..3589fcc6d16 100644 --- a/cli/azd/internal/repository/infra_confirm_test.go +++ b/cli/azd/internal/repository/infra_confirm_test.go @@ -174,8 +174,8 @@ func TestInitializer_infraSpecFromDetect(t *testing.T) { }, want: scaffold.InfraSpec{ DbPostgres: &scaffold.DatabasePostgres{ - DatabaseName: "myappdb", - AuthUsingManagedIdentity: true, + DatabaseName: "myappdb", + AuthType: "MANAGED_IDENTITY", }, Services: []scaffold.ServiceSpec{ { @@ -189,8 +189,8 @@ func TestInitializer_infraSpecFromDetect(t *testing.T) { }, }, DbPostgres: &scaffold.DatabaseReference{ - DatabaseName: "myappdb", - AuthUsingManagedIdentity: true, + DatabaseName: "myappdb", + AuthType: "MANAGED_IDENTITY", }, }, { diff --git a/cli/azd/internal/scaffold/spec.go b/cli/azd/internal/scaffold/spec.go index 11eaa273765..d9f6f3c5636 100644 --- a/cli/azd/internal/scaffold/spec.go +++ b/cli/azd/internal/scaffold/spec.go @@ -2,6 +2,7 @@ package scaffold import ( "fmt" + "github.com/azure/azure-dev/cli/azd/internal" "strings" ) @@ -32,17 +33,15 @@ type Parameter struct { } type DatabasePostgres struct { - DatabaseUser string - DatabaseName string - AuthUsingManagedIdentity bool - AuthUsingUsernamePassword bool + DatabaseUser string + DatabaseName string + AuthType internal.AuthType } type DatabaseMySql struct { - DatabaseUser string - DatabaseName string - AuthUsingManagedIdentity bool - AuthUsingUsernamePassword bool + DatabaseUser string + DatabaseName string + AuthType internal.AuthType } type CosmosSqlDatabaseContainer struct { @@ -77,37 +76,23 @@ type AIModelModel struct { } type AzureDepServiceBus struct { - Queues []string - TopicsAndSubscriptions map[string][]string - AuthUsingConnectionString bool - AuthUsingManagedIdentity bool - IsJms bool + Queues []string + TopicsAndSubscriptions map[string][]string + AuthType internal.AuthType + IsJms bool } type AzureDepEventHubs struct { - EventHubNames []string - AuthUsingConnectionString bool - AuthUsingManagedIdentity bool - UseKafka bool + EventHubNames []string + AuthType internal.AuthType + UseKafka bool } type AzureDepStorageAccount struct { - ContainerNames []string - AuthUsingConnectionString bool - AuthUsingManagedIdentity bool + ContainerNames []string + AuthType internal.AuthType } -// 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 @@ -148,9 +133,8 @@ type ServiceReference struct { } type DatabaseReference struct { - DatabaseName string - AuthUsingManagedIdentity bool - AuthUsingUsernamePassword bool + DatabaseName string + AuthType internal.AuthType } type AIModelReference struct { diff --git a/cli/azd/pkg/project/resources.go b/cli/azd/pkg/project/resources.go index 9f4bca49765..eaaa998fa95 100644 --- a/cli/azd/pkg/project/resources.go +++ b/cli/azd/pkg/project/resources.go @@ -5,6 +5,7 @@ package project import ( "fmt" + "github.com/azure/azure-dev/cli/azd/internal" "github.com/braydonk/yaml" ) @@ -12,12 +13,17 @@ import ( type ResourceType string const ( - ResourceTypeDbRedis ResourceType = "db.redis" - ResourceTypeDbPostgres ResourceType = "db.postgres" - ResourceTypeDbMySQL ResourceType = "db.mysql" - ResourceTypeDbMongo ResourceType = "db.mongo" - ResourceTypeHostContainerApp ResourceType = "host.containerapp" - ResourceTypeOpenAiModel ResourceType = "ai.openai.model" + ResourceTypeDbRedis ResourceType = "db.redis" + ResourceTypeDbPostgres ResourceType = "db.postgres" + ResourceTypeDbMySQL ResourceType = "db.mysql" + ResourceTypeDbMongo ResourceType = "db.mongo" + ResourceTypeDbCosmos ResourceType = "db.cosmos" + ResourceTypeHostContainerApp ResourceType = "host.containerapp" + ResourceTypeOpenAiModel ResourceType = "ai.openai.model" + ResourceTypeMessagingServiceBus ResourceType = "messaging.servicebus" + ResourceTypeMessagingEventHubs ResourceType = "messaging.eventhubs" + ResourceTypeMessagingKafka ResourceType = "messaging.kafka" + ResourceTypeStorage ResourceType = "storage" ) func (r ResourceType) String() string { @@ -30,10 +36,18 @@ func (r ResourceType) String() string { return "MySQL" case ResourceTypeDbMongo: return "MongoDB" + case ResourceTypeDbCosmos: + return "CosmosDB" case ResourceTypeHostContainerApp: return "Container App" case ResourceTypeOpenAiModel: return "Open AI Model" + case ResourceTypeMessagingServiceBus: + return "Service Bus" + case ResourceTypeMessagingEventHubs: + return "Event Hubs" + case ResourceTypeMessagingKafka: + return "Kafka" } return "" @@ -132,6 +146,36 @@ func (r *ResourceConfig) UnmarshalYAML(value *yaml.Node) error { return err } raw.Props = mp + case ResourceTypeDbPostgres: + pp := PostgresProps{} + if err := unmarshalProps(&pp); err != nil { + return err + } + raw.Props = pp + case ResourceTypeDbMongo: + mp := MongoDBProps{} + if err := unmarshalProps(&mp); err != nil { + return err + } + raw.Props = mp + case ResourceTypeDbCosmos: + cp := CosmosDBProps{} + if err := unmarshalProps(&cp); err != nil { + return err + } + raw.Props = cp + case ResourceTypeMessagingServiceBus: + sb := ServiceBusProps{} + if err := unmarshalProps(&sb); err != nil { + return err + } + raw.Props = sb + case ResourceTypeMessagingEventHubs: + eh := EventHubsProps{} + if err := unmarshalProps(&eh); err != nil { + return err + } + raw.Props = eh } *r = ResourceConfig(raw) @@ -161,6 +205,42 @@ type AIModelPropsModel struct { } type MySQLProps struct { + DatabaseName string `yaml:"databaseName,omitempty"` + AuthType internal.AuthType `yaml:"authType,omitempty"` +} + +type PostgresProps struct { + DatabaseName string `yaml:"databaseName,omitempty"` + AuthType internal.AuthType `yaml:"authType,omitempty"` +} + +type MongoDBProps struct { DatabaseName string `yaml:"databaseName,omitempty"` - AuthType string `yaml:"authType,omitempty"` +} + +type CosmosDBProps struct { + Containers []CosmosDBContainerProps `yaml:"containers,omitempty"` + DatabaseName string `yaml:"databaseName,omitempty"` + AuthType internal.AuthType `yaml:"authType,omitempty"` +} + +type CosmosDBContainerProps struct { + ContainerName string `yaml:"containerName,omitempty"` + PartitionKeyPaths []string `yaml:"partitionKeyPaths,omitempty"` +} + +type ServiceBusProps struct { + Queues []string `yaml:"queues,omitempty"` + IsJms bool `yaml:"isJms,omitempty"` + AuthType internal.AuthType `yaml:"authType,omitempty"` +} + +type EventHubsProps struct { + EventHubNames []string `yaml:"EventHubNames,omitempty"` + AuthType internal.AuthType `yaml:"authType,omitempty"` +} + +type KafkaProps struct { + Topics []string `yaml:"topics,omitempty"` + AuthType internal.AuthType `yaml:"authType,omitempty"` } diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index a4aa3ae8b47..9f9373c8a15 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -61,7 +61,7 @@ module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.4.5 name: '${abbrs.appManagedEnvironments}${resourceToken}' location: location zoneRedundant: false - {{- if (or (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity) (and .DbMySql .DbMySql.AuthUsingManagedIdentity))}} + {{- if (or (and .DbPostgres (eq .DbPostgres.AuthType "MANAGED_IDENTITY")) (and .DbMySql (eq .DbMySql.AuthType "MANAGED_IDENTITY")))}} roleAssignments: [ { principalId: connectionCreatorIdentity.outputs.principalId @@ -187,7 +187,7 @@ module postgreServer 'br/public:avm/res/db-for-postgre-sql/flexible-server:0.1.4 } ] location: location - {{- if (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity) }} + {{- if (and .DbPostgres (eq .DbPostgres.AuthType "MANAGED_IDENTITY")) }} roleAssignments: [ { principalId: connectionCreatorIdentity.outputs.principalId @@ -203,7 +203,7 @@ module postgreServer 'br/public:avm/res/db-for-postgre-sql/flexible-server:0.1.4 var mysqlDatabaseName = '{{ .DbMySql.DatabaseName }}' var mysqlDatabaseUser = 'mysqladmin' -{{- if (and .DbMySql .DbMySql.AuthUsingManagedIdentity) }} +{{- if (and .DbMySql (eq .DbMySql.AuthType "MANAGED_IDENTITY")) }} module mysqlIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { name: 'mysqlIdentity' params: { @@ -244,7 +244,7 @@ module mysqlServer 'br/public:avm/res/db-for-my-sql/flexible-server:0.4.1' = { ] location: location highAvailability: 'Disabled' - {{- if (and .DbMySql .DbMySql.AuthUsingManagedIdentity) }} + {{- if (and .DbMySql (eq .DbMySql.AuthType "MANAGED_IDENTITY")) }} managedIdentities: { userAssignedResourceIds: [ mysqlIdentity.outputs.resourceId @@ -261,7 +261,7 @@ module mysqlServer 'br/public:avm/res/db-for-my-sql/flexible-server:0.4.1' = { } } {{- end}} -{{- if (or (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity) (and .DbMySql .DbMySql.AuthUsingManagedIdentity))}} +{{- if (or (and .DbPostgres (eq .DbPostgres.AuthType "MANAGED_IDENTITY")) (and .DbMySql (eq .DbMySql.AuthType "MANAGED_IDENTITY")))}} module connectionCreatorIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { name: 'connectionCreatorIdentity' @@ -271,7 +271,7 @@ module connectionCreatorIdentity 'br/public:avm/res/managed-identity/user-assign } } {{- end}} -{{- if (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity) }} +{{- if (and .DbPostgres (eq .DbPostgres.AuthType "MANAGED_IDENTITY")) }} {{- range .Services}} module {{bicepName .Name}}CreateConnectionToPostgreSql 'br/public:avm/res/resources/deployment-script:0.4.0' = { name: '{{bicepName .Name}}CreateConnectionToPostgreSql' @@ -292,7 +292,7 @@ module {{bicepName .Name}}CreateConnectionToPostgreSql 'br/public:avm/res/resour } {{- end}} {{- end}} -{{- if (and .DbMySql .DbMySql.AuthUsingManagedIdentity) }} +{{- if (and .DbMySql (eq .DbMySql.AuthType "MANAGED_IDENTITY")) }} {{- range .Services}} module {{bicepName .Name}}CreateConnectionToMysql 'br/public:avm/res/resources/deployment-script:0.4.0' = { name: '{{bicepName .Name}}CreateConnectionToMysql' @@ -321,7 +321,7 @@ module eventHubNamespace 'br/public:avm/res/event-hub/namespace:0.7.1' = { name: '${abbrs.eventHubNamespaces}${resourceToken}' location: location roleAssignments: [ - {{- if (and .AzureEventHubs .AzureEventHubs.AuthUsingManagedIdentity) }} + {{- if (and .AzureEventHubs (eq .AzureEventHubs.AuthType "MANAGED_IDENTITY")) }} {{- range .Services}} { principalId: {{bicepName .Name}}Identity.outputs.principalId @@ -331,7 +331,7 @@ module eventHubNamespace 'br/public:avm/res/event-hub/namespace:0.7.1' = { {{- end}} {{- end}} ] - {{- if (and .AzureEventHubs .AzureEventHubs.AuthUsingConnectionString) }} + {{- if (and .AzureEventHubs (eq .AzureEventHubs.AuthType "CONNECTION_STRING")) }} disableLocalAuth: false {{- end}} eventhubs: [ @@ -343,7 +343,7 @@ module eventHubNamespace 'br/public:avm/res/event-hub/namespace:0.7.1' = { ] } } -{{- if (and .AzureEventHubs .AzureEventHubs.AuthUsingConnectionString) }} +{{- if (and .AzureEventHubs (eq .AzureEventHubs.AuthType "CONNECTION_STRING")) }} module eventHubsConnectionString './modules/set-event-hubs-namespace-connection-string.bicep' = { name: 'eventHubsConnectionString' params: { @@ -372,7 +372,7 @@ module storageAccount 'br/public:avm/res/storage/storage-account:0.14.3' = { } location: location roleAssignments: [ - {{- if (and .AzureStorageAccount .AzureStorageAccount.AuthUsingManagedIdentity) }} + {{- if (and .AzureStorageAccount (eq .AzureStorageAccount.AuthType "MANAGED_IDENTITY")) }} {{- range .Services}} { principalId: {{bicepName .Name}}Identity.outputs.principalId @@ -389,7 +389,7 @@ module storageAccount 'br/public:avm/res/storage/storage-account:0.14.3' = { } } -{{- if (and .AzureStorageAccount .AzureStorageAccount.AuthUsingConnectionString) }} +{{- if (and .AzureStorageAccount (eq .AzureStorageAccount.AuthType "CONNECTION_STRING")) }} module storageAccountConnectionString './modules/set-storage-account-connection-string.bicep' = { name: 'storageAccountConnectionString' params: { @@ -411,7 +411,7 @@ module serviceBusNamespace 'br/public:avm/res/service-bus/namespace:0.10.0' = { // Non-required parameters location: location roleAssignments: [ - {{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingManagedIdentity) }} + {{- if (and .AzureServiceBus (eq .AzureServiceBus.AuthType "MANAGED_IDENTITY")) }} {{- range .Services}} { principalId: {{bicepName .Name}}Identity.outputs.principalId @@ -421,7 +421,7 @@ module serviceBusNamespace 'br/public:avm/res/service-bus/namespace:0.10.0' = { {{- end}} {{- end}} ] - {{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingConnectionString) }} + {{- if (and .AzureServiceBus (eq .AzureServiceBus.AuthType "CONNECTION_STRING")) }} disableLocalAuth: false {{- end}} queues: [ @@ -434,7 +434,7 @@ module serviceBusNamespace 'br/public:avm/res/service-bus/namespace:0.10.0' = { } } -{{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingConnectionString) }} +{{- if (and .AzureServiceBus (eq .AzureServiceBus.AuthType "CONNECTION_STRING")) }} module serviceBusConnectionString './modules/set-servicebus-namespace-connection-string.bicep' = { name: 'serviceBusConnectionString' params: { @@ -493,7 +493,7 @@ module {{bicepName .Name}}Identity 'br/public:avm/res/managed-identity/user-assi params: { name: '${abbrs.managedIdentityUserAssignedIdentities}{{bicepName .Name}}-${resourceToken}' location: location - {{- if (or (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity) (and .DbMySql .DbMySql.AuthUsingManagedIdentity))}} + {{- if (or (and .DbPostgres (eq .DbPostgres.AuthType "MANAGED_IDENTITY")) (and .DbMySql (eq .DbMySql.AuthType "MANAGED_IDENTITY")))}} roleAssignments: [ { principalId: connectionCreatorIdentity.outputs.principalId @@ -565,7 +565,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { keyVaultUrl: cosmos.outputs.exportedSecrets['MONGODB-URL'].secretUri } {{- end}} - {{- if (and .DbPostgres .DbPostgres.AuthUsingUsernamePassword) }} + {{- if (and .DbPostgres (eq .DbPostgres.AuthType "PASSWORD")) }} { name: 'postgresql-password' value: postgreSqlDatabasePassword @@ -575,7 +575,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { value: 'postgresql://${postgreSqlDatabaseUser}:${postgreSqlDatabasePassword}@${postgreServer.outputs.fqdn}:5432/${postgreSqlDatabaseName}' } {{- end}} - {{- if (and .DbMySql .DbMySql.AuthUsingUsernamePassword) }} + {{- if (and .DbMySql (eq .DbMySql.AuthType "PASSWORD")) }} { name: 'mysql-password' value: mysqlDatabasePassword @@ -597,21 +597,21 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { keyVaultUrl: '${keyVault.outputs.uri}secrets/REDIS-URL' } {{- end}} - {{- if (and .AzureEventHubs .AzureEventHubs.AuthUsingConnectionString) }} + {{- if (and .AzureEventHubs (eq .AzureEventHubs.AuthType "CONNECTION_STRING")) }} { name: 'event-hubs-connection-string' identity:{{bicepName .Name}}Identity.outputs.resourceId keyVaultUrl: '${keyVault.outputs.uri}secrets/EVENT-HUBS-CONNECTION-STRING' } {{- end}} - {{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingConnectionString) }} + {{- if (and .AzureServiceBus (eq .AzureServiceBus.AuthType "CONNECTION_STRING")) }} { name: 'servicebus-connection-string' identity:{{bicepName .Name}}Identity.outputs.resourceId keyVaultUrl: '${keyVault.outputs.uri}secrets/SERVICEBUS-CONNECTION-STRING' } {{- end}} - {{- if (and .AzureStorageAccount .AzureStorageAccount.AuthUsingConnectionString) }} + {{- if (and .AzureStorageAccount (eq .AzureStorageAccount.AuthType "CONNECTION_STRING")) }} { name: 'storage-account-connection-string' identity:{{bicepName .Name}}Identity.outputs.resourceId @@ -669,7 +669,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { value: '5432' } {{- end}} - {{- if (and .DbPostgres .DbPostgres.AuthUsingUsernamePassword) }} + {{- if (and .DbPostgres (eq .DbPostgres.AuthType "PASSWORD")) }} { name: 'POSTGRES_URL' secretRef: 'postgresql-db-url' @@ -709,7 +709,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { value: '3306' } {{- end}} - {{- if (and .DbMySql .DbMySql.AuthUsingUsernamePassword) }} + {{- if (and .DbMySql (eq .DbMySql.AuthType "PASSWORD")) }} { name: 'MYSQL_URL' secretRef: 'mysql-db-url' @@ -789,7 +789,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { value: '${eventHubNamespace.outputs.name}.servicebus.windows.net:9093' } {{- end}} - {{- if (and .AzureEventHubs .AzureEventHubs.AuthUsingManagedIdentity) }} + {{- if (and .AzureEventHubs (eq .AzureEventHubs.AuthType "MANAGED_IDENTITY")) }} { name: 'spring.cloud.azure.eventhubs.credential.managed-identity-enabled' value: 'true' @@ -803,7 +803,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { value: '' } {{- end}} - {{- if (and .AzureEventHubs .AzureEventHubs.AuthUsingConnectionString) }} + {{- if (and .AzureEventHubs (eq .AzureEventHubs.AuthType "CONNECTION_STRING")) }} { name: 'spring.cloud.azure.eventhubs.connection-string' secretRef: 'event-hubs-connection-string' @@ -823,7 +823,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { value: storageAccountName } {{- end}} - {{- if (and .AzureStorageAccount .AzureStorageAccount.AuthUsingManagedIdentity) }} + {{- if (and .AzureStorageAccount (eq .AzureStorageAccount.AuthType "MANAGED_IDENTITY")) }} { name: 'spring.cloud.azure.eventhubs.processor.checkpoint-store.connection-string' value: '' @@ -837,7 +837,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { value: {{bicepName .Name}}Identity.outputs.clientId } {{- end}} - {{- if (and .AzureStorageAccount .AzureStorageAccount.AuthUsingConnectionString) }} + {{- if (and .AzureStorageAccount (eq .AzureStorageAccount.AuthType "CONNECTION_STRING")) }} { name: 'spring.cloud.azure.eventhubs.processor.checkpoint-store.connection-string' secretRef: 'storage-account-connection-string' @@ -858,7 +858,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { value: serviceBusNamespace.outputs.name } {{- end}} - {{- if (and .AzureServiceBus .AzureServiceBus.AuthUsingManagedIdentity) }} + {{- if (and .AzureServiceBus (eq .AzureServiceBus.AuthType "MANAGED_IDENTITY")) }} { name: 'spring.cloud.azure.servicebus.connection-string' value: '' @@ -872,7 +872,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { value: {{bicepName .Name}}Identity.outputs.clientId } {{- end}} - {{- if (and (and .AzureServiceBus .AzureServiceBus.AuthUsingConnectionString) (not .AzureServiceBus.IsJms)) }} + {{- if (and (and .AzureServiceBus (eq .AzureServiceBus.AuthType "CONNECTION_STRING")) (not .AzureServiceBus.IsJms)) }} { name: 'spring.cloud.azure.servicebus.connection-string' secretRef: 'servicebus-connection-string' @@ -886,7 +886,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { value: '' } {{- end}} - {{- if (and (and .AzureServiceBus .AzureServiceBus.AuthUsingConnectionString) .AzureServiceBus.IsJms) }} + {{- if (and (and .AzureServiceBus (eq .AzureServiceBus.AuthType "CONNECTION_STRING")) .AzureServiceBus.IsJms) }} { name: 'spring.jms.servicebus.connection-string' secretRef: 'servicebus-connection-string' @@ -932,7 +932,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { environmentResourceId: containerAppsEnvironment.outputs.resourceId location: location tags: union(tags, { 'azd-service-name': '{{.Name}}' }) - {{- if (or (and .DbPostgres .DbPostgres.AuthUsingManagedIdentity) (and .DbMySql .DbMySql.AuthUsingManagedIdentity))}} + {{- if (or (and .DbPostgres (eq .DbPostgres.AuthType "MANAGED_IDENTITY")) (and .DbMySql (eq .DbMySql.AuthType "MANAGED_IDENTITY")))}} roleAssignments: [ { principalId: connectionCreatorIdentity.outputs.principalId @@ -994,13 +994,13 @@ module keyVault 'br/public:avm/res/key-vault/vault:0.6.1' = { {{- end}} ] secrets: [ - {{- if (and .DbPostgres .DbPostgres.AuthUsingUsernamePassword) }} + {{- if (and .DbPostgres (eq .DbPostgres.AuthType "PASSWORD")) }} { name: 'postgresql-password' value: postgreSqlDatabasePassword } {{- end}} - {{- if (and .DbMySql .DbMySql.AuthUsingUsernamePassword) }} + {{- if (and .DbMySql (eq .DbMySql.AuthType "PASSWORD")) }} { name: 'mysql-password' value: mysqlDatabasePassword From d6b804047896f72b2312b3b48c8081f4b6689f57 Mon Sep 17 00:00:00 2001 From: Xiaolu Dai <31124698+saragluna@users.noreply.github.com> Date: Wed, 13 Nov 2024 16:23:14 +0800 Subject: [PATCH 64/92] convert resources to infraspec (#23) --- cli/azd/pkg/project/scaffold_gen.go | 39 +++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/cli/azd/pkg/project/scaffold_gen.go b/cli/azd/pkg/project/scaffold_gen.go index 595d5f14b8c..70a643612b0 100644 --- a/cli/azd/pkg/project/scaffold_gen.go +++ b/cli/azd/pkg/project/scaffold_gen.go @@ -147,6 +147,45 @@ func infraSpec(projectConfig *ProjectConfig) (*scaffold.InfraSpec, error) { infraSpec.DbPostgres = &scaffold.DatabasePostgres{ DatabaseName: res.Name, DatabaseUser: "pgadmin", + AuthType: res.Props.(PostgresProps).AuthType, + } + case ResourceTypeDbMySQL: + infraSpec.DbMySql = &scaffold.DatabaseMySql{ + DatabaseName: res.Name, + DatabaseUser: "mysqladmin", + AuthType: res.Props.(MySQLProps).AuthType, + } + case ResourceTypeDbCosmos: + infraSpec.DbCosmos = &scaffold.DatabaseCosmosAccount{ + DatabaseName: res.Name, + } + containers := res.Props.(CosmosDBProps).Containers + for _, container := range containers { + infraSpec.DbCosmos.Containers = append(infraSpec.DbCosmos.Containers, scaffold.CosmosSqlDatabaseContainer{ + ContainerName: container.ContainerName, + PartitionKeyPaths: container.PartitionKeyPaths, + }) + } + case ResourceTypeMessagingServiceBus: + props := res.Props.(ServiceBusProps) + infraSpec.AzureServiceBus = &scaffold.AzureDepServiceBus{ + Queues: props.Queues, + AuthType: props.AuthType, + IsJms: props.IsJms, + } + case ResourceTypeMessagingEventHubs: + props := res.Props.(EventHubsProps) + infraSpec.AzureEventHubs = &scaffold.AzureDepEventHubs{ + EventHubNames: props.EventHubNames, + AuthType: props.AuthType, + UseKafka: false, + } + case ResourceTypeMessagingKafka: + props := res.Props.(KafkaProps) + infraSpec.AzureEventHubs = &scaffold.AzureDepEventHubs{ + EventHubNames: props.Topics, + AuthType: props.AuthType, + UseKafka: true, } case ResourceTypeHostContainerApp: svcSpec := scaffold.ServiceSpec{ From 6ed094efb872acbf73e9636f22c346b48b4dfbce Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Thu, 14 Nov 2024 15:27:50 +0800 Subject: [PATCH 65/92] Fix error when run azd init (#24) * Fix error when run azd init * 1. Rename AuthTypeManagedIdentity to AuthTypeUserAssignedManagedIdentity, and related value. 2. Fix test failure by changing "Use user assigned managed identity" to "User assigned managed identity" * Change the "Name" into const string instead of using "databaseName". * 1. Fix error about database name. 2. Use DatabaseUser variable in go lang when generate resources.bicep from resources.bicept. --- cli/azd/internal/auth_type.go | 20 ++- cli/azd/internal/repository/app_init.go | 133 +++++++++++++----- cli/azd/internal/repository/infra_confirm.go | 74 +++------- .../internal/repository/infra_confirm_test.go | 8 +- cli/azd/pkg/project/resources.go | 1 - cli/azd/pkg/project/scaffold_gen.go | 8 +- .../scaffold/templates/resources.bicept | 34 ++--- 7 files changed, 158 insertions(+), 120 deletions(-) diff --git a/cli/azd/internal/auth_type.go b/cli/azd/internal/auth_type.go index ea8abcae097..4902d7bdf8f 100644 --- a/cli/azd/internal/auth_type.go +++ b/cli/azd/internal/auth_type.go @@ -6,9 +6,23 @@ type AuthType string const ( AuthTypeUnspecified AuthType = "UNSPECIFIED" // Username and password, or key based authentication - AuthtypePassword AuthType = "PASSWORD" + AuthTypePassword AuthType = "PASSWORD" // Connection string authentication - AuthtypeConnectionString AuthType = "CONNECTION_STRING" + AuthTypeConnectionString AuthType = "CONNECTION_STRING" // Microsoft EntraID token credential - AuthtypeManagedIdentity AuthType = "MANAGED_IDENTITY" + AuthTypeUserAssignedManagedIdentity AuthType = "USER_ASSIGNED_MANAGED_IDENTITY" ) + +func GetAuthTypeDescription(authType AuthType) string { + switch authType { + case AuthTypeUnspecified: + return "Unspecified" + case AuthTypePassword: + return "Username and password" + case AuthTypeConnectionString: + return "Connection string" + case AuthTypeUserAssignedManagedIdentity: + return "User assigned managed identity" + } + panic("unknown auth type") +} diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index e8959abe420..99d1563a3ae 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -560,54 +560,81 @@ func (i *Initializer) prjConfigFromDetect( }) for _, database := range databases { + var resourceConfig project.ResourceConfig + var databaseName string if database == appdetect.DbRedis { - redis := project.ResourceConfig{ - Type: project.ResourceTypeDbRedis, - Name: "redis", + databaseName = "redis" + } else { + var err error + databaseName, err = i.getDatabaseNameByPrompt(ctx, database) + if err != nil { + return config, err + } + } + var authType = internal.AuthTypeUnspecified + if database == appdetect.DbPostgres || database == appdetect.DbMySql { + var err error + authType, err = chooseAuthTypeByPrompt( + databaseName, + []internal.AuthType{internal.AuthTypeUserAssignedManagedIdentity, internal.AuthTypePassword}, + ctx, + i.console) + if err != nil { + return config, err } - config.Resources[redis.Name] = &redis - dbNames[database] = redis.Name - continue } - - var dbType project.ResourceType switch database { + case appdetect.DbRedis: + resourceConfig = project.ResourceConfig{ + Type: project.ResourceTypeDbRedis, + Name: "redis", + } case appdetect.DbMongo: - dbType = project.ResourceTypeDbMongo - case appdetect.DbPostgres: - dbType = project.ResourceTypeDbPostgres - case appdetect.DbMySql: - dbType = project.ResourceTypeDbMySQL + resourceConfig = project.ResourceConfig{ + Type: project.ResourceTypeDbMongo, + Name: "mongo", + Props: project.MongoDBProps{ + DatabaseName: databaseName, + }, + } case appdetect.DbCosmos: - dbType = project.ResourceTypeDbCosmos - } - - db := project.ResourceConfig{ - Type: dbType, - } - - for { - dbName, err := promptDbName(i.console, ctx, database) - if err != nil { - return config, err + resourceConfig = project.ResourceConfig{ + Type: project.ResourceTypeDbCosmos, + Name: "cosmos", + Props: project.CosmosDBProps{ + DatabaseName: databaseName, + }, } - - if dbName == "" { - i.console.Message(ctx, "Database name is required.") - continue + case appdetect.DbPostgres: + resourceConfig = project.ResourceConfig{ + Type: project.ResourceTypeDbPostgres, + Name: "postgresql", + Props: project.PostgresProps{ + DatabaseName: databaseName, + AuthType: authType, + }, + } + case appdetect.DbMySql: + resourceConfig = project.ResourceConfig{ + Type: project.ResourceTypeDbMySQL, + Name: "mysql", + Props: project.MySQLProps{ + DatabaseName: databaseName, + AuthType: authType, + }, } - - db.Name = dbName - break } - - config.Resources[db.Name] = &db - dbNames[database] = db.Name + config.Resources[resourceConfig.Name] = &resourceConfig + dbNames[database] = resourceConfig.Name } for _, azureDepPair := range detect.AzureDeps { azureDep := azureDepPair.first - authType, err := i.chooseAuthTypeByPrompt(ctx, azureDep.ResourceDisplay()) + authType, err := chooseAuthTypeByPrompt( + azureDep.ResourceDisplay(), + []internal.AuthType{internal.AuthTypeUserAssignedManagedIdentity, internal.AuthTypeConnectionString}, + ctx, + i.console) if err != nil { return config, err } @@ -686,3 +713,39 @@ func (i *Initializer) prjConfigFromDetect( return config, nil } + +func (i *Initializer) getDatabaseNameByPrompt(ctx context.Context, database appdetect.DatabaseDep) (string, error) { + var result string + for { + dbName, err := promptDbName(i.console, ctx, database) + if err != nil { + return dbName, err + } + if dbName == "" { + i.console.Message(ctx, "Database name is required.") + continue + } + result = dbName + break + } + return result, nil +} + +func chooseAuthTypeByPrompt( + name string, + authOptions []internal.AuthType, + ctx context.Context, + console input.Console) (internal.AuthType, error) { + var options []string + for _, option := range authOptions { + options = append(options, internal.GetAuthTypeDescription(option)) + } + selection, err := console.Select(ctx, input.ConsoleOptions{ + Message: "Choose auth type for '" + name + "'?", + Options: options, + }) + if err != nil { + return internal.AuthTypeUnspecified, err + } + return authOptions[selection], nil +} diff --git a/cli/azd/internal/repository/infra_confirm.go b/cli/azd/internal/repository/infra_confirm.go index ffb560d2fd2..c8aa3465704 100644 --- a/cli/azd/internal/repository/infra_confirm.go +++ b/cli/azd/internal/repository/infra_confirm.go @@ -51,7 +51,11 @@ func (i *Initializer) infraSpecFromDetect( i.console.Message(ctx, "Database name is required.") continue } - authType, err := i.getAuthType(ctx) + authType, err := chooseAuthTypeByPrompt( + dbName, + []internal.AuthType{internal.AuthTypeUserAssignedManagedIdentity, internal.AuthTypePassword}, + ctx, + i.console) if err != nil { return scaffold.InfraSpec{}, err } @@ -65,7 +69,11 @@ func (i *Initializer) infraSpecFromDetect( i.console.Message(ctx, "Database name is required.") continue } - authType, err := i.getAuthType(ctx) + authType, err := chooseAuthTypeByPrompt( + dbName, + []internal.AuthType{internal.AuthTypeUserAssignedManagedIdentity, internal.AuthTypePassword}, + ctx, + i.console) if err != nil { return scaffold.InfraSpec{}, err } @@ -323,32 +331,28 @@ func (i *Initializer) buildInfraSpecByAzureDep( ctx context.Context, azureDep appdetect.AzureDep, spec *scaffold.InfraSpec) error { + authType, err := chooseAuthTypeByPrompt( + azureDep.ResourceDisplay(), + []internal.AuthType{internal.AuthTypeUserAssignedManagedIdentity, internal.AuthTypeConnectionString}, + ctx, + i.console) + if err != nil { + return err + } switch dependency := azureDep.(type) { case appdetect.AzureDepServiceBus: - authType, err := i.chooseAuthTypeByPrompt(ctx, azureDep.ResourceDisplay()) - if err != nil { - return err - } spec.AzureServiceBus = &scaffold.AzureDepServiceBus{ IsJms: dependency.IsJms, Queues: dependency.Queues, AuthType: authType, } case appdetect.AzureDepEventHubs: - authType, err := i.chooseAuthTypeByPrompt(ctx, azureDep.ResourceDisplay()) - if err != nil { - return err - } spec.AzureEventHubs = &scaffold.AzureDepEventHubs{ EventHubNames: dependency.Names, AuthType: authType, UseKafka: dependency.UseKafka, } case appdetect.AzureDepStorageAccount: - authType, err := i.chooseAuthTypeByPrompt(ctx, azureDep.ResourceDisplay()) - if err != nil { - return err - } spec.AzureStorageAccount = &scaffold.AzureDepStorageAccount{ ContainerNames: dependency.ContainerNames, AuthType: authType, @@ -357,48 +361,6 @@ func (i *Initializer) buildInfraSpecByAzureDep( return nil } -func (i *Initializer) chooseAuthTypeByPrompt(ctx context.Context, serviceName string) (internal.AuthType, error) { - portOptions := []string{ - "User assigned managed identity", - "Connection string", - } - selection, err := i.console.Select(ctx, input.ConsoleOptions{ - Message: "Choose auth type for '" + serviceName + "'?", - Options: portOptions, - }) - if err != nil { - return internal.AuthTypeUnspecified, err - } - if selection == 0 { - return internal.AuthtypeManagedIdentity, nil - } else { - return internal.AuthtypeConnectionString, nil - } -} - -func (i *Initializer) getAuthType(ctx context.Context) (internal.AuthType, error) { - authType := internal.AuthTypeUnspecified - 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 = internal.AuthtypeManagedIdentity - case 1: - authType = internal.AuthtypePassword - default: - panic("unhandled selection") - } - return authType, nil -} - func detectCosmosSqlDatabaseContainersInDirectory(root string) ([]scaffold.CosmosSqlDatabaseContainer, error) { var result []scaffold.CosmosSqlDatabaseContainer err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { diff --git a/cli/azd/internal/repository/infra_confirm_test.go b/cli/azd/internal/repository/infra_confirm_test.go index 3589fcc6d16..85652331d6d 100644 --- a/cli/azd/internal/repository/infra_confirm_test.go +++ b/cli/azd/internal/repository/infra_confirm_test.go @@ -169,13 +169,13 @@ func TestInitializer_infraSpecFromDetect(t *testing.T) { "n", "my$special$db", "n", - "myappdb", // fill in db name - "Use user assigned managed identity", // confirm db authentication + "myappdb", // fill in db name + "User assigned managed identity", // confirm db authentication }, want: scaffold.InfraSpec{ DbPostgres: &scaffold.DatabasePostgres{ DatabaseName: "myappdb", - AuthType: "MANAGED_IDENTITY", + AuthType: "USER_ASSIGNED_MANAGED_IDENTITY", }, Services: []scaffold.ServiceSpec{ { @@ -190,7 +190,7 @@ func TestInitializer_infraSpecFromDetect(t *testing.T) { }, DbPostgres: &scaffold.DatabaseReference{ DatabaseName: "myappdb", - AuthType: "MANAGED_IDENTITY", + AuthType: "USER_ASSIGNED_MANAGED_IDENTITY", }, }, { diff --git a/cli/azd/pkg/project/resources.go b/cli/azd/pkg/project/resources.go index eaaa998fa95..3e8eb365cea 100644 --- a/cli/azd/pkg/project/resources.go +++ b/cli/azd/pkg/project/resources.go @@ -221,7 +221,6 @@ type MongoDBProps struct { type CosmosDBProps struct { Containers []CosmosDBContainerProps `yaml:"containers,omitempty"` DatabaseName string `yaml:"databaseName,omitempty"` - AuthType internal.AuthType `yaml:"authType,omitempty"` } type CosmosDBContainerProps struct { diff --git a/cli/azd/pkg/project/scaffold_gen.go b/cli/azd/pkg/project/scaffold_gen.go index 70a643612b0..d80a826d271 100644 --- a/cli/azd/pkg/project/scaffold_gen.go +++ b/cli/azd/pkg/project/scaffold_gen.go @@ -141,23 +141,23 @@ func infraSpec(projectConfig *ProjectConfig) (*scaffold.InfraSpec, error) { infraSpec.DbRedis = &scaffold.DatabaseRedis{} case ResourceTypeDbMongo: infraSpec.DbCosmosMongo = &scaffold.DatabaseCosmosMongo{ - DatabaseName: res.Name, + DatabaseName: res.Props.(CosmosDBProps).DatabaseName, } case ResourceTypeDbPostgres: infraSpec.DbPostgres = &scaffold.DatabasePostgres{ - DatabaseName: res.Name, + DatabaseName: res.Props.(PostgresProps).DatabaseName, DatabaseUser: "pgadmin", AuthType: res.Props.(PostgresProps).AuthType, } case ResourceTypeDbMySQL: infraSpec.DbMySql = &scaffold.DatabaseMySql{ - DatabaseName: res.Name, + DatabaseName: res.Props.(MySQLProps).DatabaseName, DatabaseUser: "mysqladmin", AuthType: res.Props.(MySQLProps).AuthType, } case ResourceTypeDbCosmos: infraSpec.DbCosmos = &scaffold.DatabaseCosmosAccount{ - DatabaseName: res.Name, + DatabaseName: res.Props.(CosmosDBProps).DatabaseName, } containers := res.Props.(CosmosDBProps).Containers for _, container := range containers { diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index 9f9373c8a15..740e62ca4ba 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -61,7 +61,7 @@ module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.4.5 name: '${abbrs.appManagedEnvironments}${resourceToken}' location: location zoneRedundant: false - {{- if (or (and .DbPostgres (eq .DbPostgres.AuthType "MANAGED_IDENTITY")) (and .DbMySql (eq .DbMySql.AuthType "MANAGED_IDENTITY")))}} + {{- if (or (and .DbPostgres (eq .DbPostgres.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) (and .DbMySql (eq .DbMySql.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")))}} roleAssignments: [ { principalId: connectionCreatorIdentity.outputs.principalId @@ -161,7 +161,7 @@ module cosmos 'br/public:avm/res/document-db/database-account:0.8.1' = { {{- if .DbPostgres}} var postgreSqlDatabaseName = '{{ .DbPostgres.DatabaseName }}' -var postgreSqlDatabaseUser = 'psqladmin' +var postgreSqlDatabaseUser = '{{ .DbPostgres.DatabaseUser }}' module postgreServer 'br/public:avm/res/db-for-postgre-sql/flexible-server:0.1.4' = { name: 'postgreServer' params: { @@ -187,7 +187,7 @@ module postgreServer 'br/public:avm/res/db-for-postgre-sql/flexible-server:0.1.4 } ] location: location - {{- if (and .DbPostgres (eq .DbPostgres.AuthType "MANAGED_IDENTITY")) }} + {{- if (and .DbPostgres (eq .DbPostgres.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) }} roleAssignments: [ { principalId: connectionCreatorIdentity.outputs.principalId @@ -202,8 +202,8 @@ module postgreServer 'br/public:avm/res/db-for-postgre-sql/flexible-server:0.1.4 {{- if .DbMySql}} var mysqlDatabaseName = '{{ .DbMySql.DatabaseName }}' -var mysqlDatabaseUser = 'mysqladmin' -{{- if (and .DbMySql (eq .DbMySql.AuthType "MANAGED_IDENTITY")) }} +var mysqlDatabaseUser = '{{ .DbMySql.DatabaseUser }}' +{{- if (and .DbMySql (eq .DbMySql.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) }} module mysqlIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { name: 'mysqlIdentity' params: { @@ -244,7 +244,7 @@ module mysqlServer 'br/public:avm/res/db-for-my-sql/flexible-server:0.4.1' = { ] location: location highAvailability: 'Disabled' - {{- if (and .DbMySql (eq .DbMySql.AuthType "MANAGED_IDENTITY")) }} + {{- if (and .DbMySql (eq .DbMySql.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) }} managedIdentities: { userAssignedResourceIds: [ mysqlIdentity.outputs.resourceId @@ -261,7 +261,7 @@ module mysqlServer 'br/public:avm/res/db-for-my-sql/flexible-server:0.4.1' = { } } {{- end}} -{{- if (or (and .DbPostgres (eq .DbPostgres.AuthType "MANAGED_IDENTITY")) (and .DbMySql (eq .DbMySql.AuthType "MANAGED_IDENTITY")))}} +{{- if (or (and .DbPostgres (eq .DbPostgres.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) (and .DbMySql (eq .DbMySql.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")))}} module connectionCreatorIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { name: 'connectionCreatorIdentity' @@ -271,7 +271,7 @@ module connectionCreatorIdentity 'br/public:avm/res/managed-identity/user-assign } } {{- end}} -{{- if (and .DbPostgres (eq .DbPostgres.AuthType "MANAGED_IDENTITY")) }} +{{- if (and .DbPostgres (eq .DbPostgres.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) }} {{- range .Services}} module {{bicepName .Name}}CreateConnectionToPostgreSql 'br/public:avm/res/resources/deployment-script:0.4.0' = { name: '{{bicepName .Name}}CreateConnectionToPostgreSql' @@ -292,7 +292,7 @@ module {{bicepName .Name}}CreateConnectionToPostgreSql 'br/public:avm/res/resour } {{- end}} {{- end}} -{{- if (and .DbMySql (eq .DbMySql.AuthType "MANAGED_IDENTITY")) }} +{{- if (and .DbMySql (eq .DbMySql.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) }} {{- range .Services}} module {{bicepName .Name}}CreateConnectionToMysql 'br/public:avm/res/resources/deployment-script:0.4.0' = { name: '{{bicepName .Name}}CreateConnectionToMysql' @@ -321,7 +321,7 @@ module eventHubNamespace 'br/public:avm/res/event-hub/namespace:0.7.1' = { name: '${abbrs.eventHubNamespaces}${resourceToken}' location: location roleAssignments: [ - {{- if (and .AzureEventHubs (eq .AzureEventHubs.AuthType "MANAGED_IDENTITY")) }} + {{- if (and .AzureEventHubs (eq .AzureEventHubs.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) }} {{- range .Services}} { principalId: {{bicepName .Name}}Identity.outputs.principalId @@ -372,7 +372,7 @@ module storageAccount 'br/public:avm/res/storage/storage-account:0.14.3' = { } location: location roleAssignments: [ - {{- if (and .AzureStorageAccount (eq .AzureStorageAccount.AuthType "MANAGED_IDENTITY")) }} + {{- if (and .AzureStorageAccount (eq .AzureStorageAccount.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) }} {{- range .Services}} { principalId: {{bicepName .Name}}Identity.outputs.principalId @@ -411,7 +411,7 @@ module serviceBusNamespace 'br/public:avm/res/service-bus/namespace:0.10.0' = { // Non-required parameters location: location roleAssignments: [ - {{- if (and .AzureServiceBus (eq .AzureServiceBus.AuthType "MANAGED_IDENTITY")) }} + {{- if (and .AzureServiceBus (eq .AzureServiceBus.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) }} {{- range .Services}} { principalId: {{bicepName .Name}}Identity.outputs.principalId @@ -493,7 +493,7 @@ module {{bicepName .Name}}Identity 'br/public:avm/res/managed-identity/user-assi params: { name: '${abbrs.managedIdentityUserAssignedIdentities}{{bicepName .Name}}-${resourceToken}' location: location - {{- if (or (and .DbPostgres (eq .DbPostgres.AuthType "MANAGED_IDENTITY")) (and .DbMySql (eq .DbMySql.AuthType "MANAGED_IDENTITY")))}} + {{- if (or (and .DbPostgres (eq .DbPostgres.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) (and .DbMySql (eq .DbMySql.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")))}} roleAssignments: [ { principalId: connectionCreatorIdentity.outputs.principalId @@ -789,7 +789,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { value: '${eventHubNamespace.outputs.name}.servicebus.windows.net:9093' } {{- end}} - {{- if (and .AzureEventHubs (eq .AzureEventHubs.AuthType "MANAGED_IDENTITY")) }} + {{- if (and .AzureEventHubs (eq .AzureEventHubs.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) }} { name: 'spring.cloud.azure.eventhubs.credential.managed-identity-enabled' value: 'true' @@ -823,7 +823,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { value: storageAccountName } {{- end}} - {{- if (and .AzureStorageAccount (eq .AzureStorageAccount.AuthType "MANAGED_IDENTITY")) }} + {{- if (and .AzureStorageAccount (eq .AzureStorageAccount.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) }} { name: 'spring.cloud.azure.eventhubs.processor.checkpoint-store.connection-string' value: '' @@ -858,7 +858,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { value: serviceBusNamespace.outputs.name } {{- end}} - {{- if (and .AzureServiceBus (eq .AzureServiceBus.AuthType "MANAGED_IDENTITY")) }} + {{- if (and .AzureServiceBus (eq .AzureServiceBus.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) }} { name: 'spring.cloud.azure.servicebus.connection-string' value: '' @@ -932,7 +932,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { environmentResourceId: containerAppsEnvironment.outputs.resourceId location: location tags: union(tags, { 'azd-service-name': '{{.Name}}' }) - {{- if (or (and .DbPostgres (eq .DbPostgres.AuthType "MANAGED_IDENTITY")) (and .DbMySql (eq .DbMySql.AuthType "MANAGED_IDENTITY")))}} + {{- if (or (and .DbPostgres (eq .DbPostgres.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) (and .DbMySql (eq .DbMySql.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")))}} roleAssignments: [ { principalId: connectionCreatorIdentity.outputs.principalId From 2e1483633485317f6e05a01473111917c45711aa Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Thu, 14 Nov 2024 17:26:12 +0800 Subject: [PATCH 66/92] Fix error that ".DbMySql" not take effect in resources.bicept. (#25) --- cli/azd/pkg/project/scaffold_gen.go | 32 +++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/cli/azd/pkg/project/scaffold_gen.go b/cli/azd/pkg/project/scaffold_gen.go index 96837704145..a2e1aa99853 100644 --- a/cli/azd/pkg/project/scaffold_gen.go +++ b/cli/azd/pkg/project/scaffold_gen.go @@ -225,14 +225,42 @@ func infraSpec(projectConfig *ProjectConfig) (*scaffold.InfraSpec, error) { } // create reverse frontends -> backends mapping - for _, svc := range infraSpec.Services { + for i := range infraSpec.Services { + svc := &infraSpec.Services[i] if front, ok := backendMapping[svc.Name]; ok { if svc.Backend == nil { svc.Backend = &scaffold.Backend{} } - svc.Backend.Frontends = append(svc.Backend.Frontends, scaffold.ServiceReference{Name: front}) } + if infraSpec.DbPostgres != nil { + svc.DbPostgres = &scaffold.DatabaseReference{ + DatabaseName: infraSpec.DbPostgres.DatabaseName, + AuthType: infraSpec.DbPostgres.AuthType, + } + } + if infraSpec.DbMySql != nil { + svc.DbMySql = &scaffold.DatabaseReference{ + DatabaseName: infraSpec.DbMySql.DatabaseName, + AuthType: infraSpec.DbMySql.AuthType, + } + } + if infraSpec.DbRedis != nil { + svc.DbRedis = &scaffold.DatabaseReference{ + DatabaseName: "redis", + } + } + if infraSpec.DbCosmosMongo != nil { + svc.DbCosmosMongo = &scaffold.DatabaseReference{ + DatabaseName: infraSpec.DbCosmosMongo.DatabaseName, + } + } + if infraSpec.DbCosmos != nil { + svc.DbCosmos = &scaffold.DatabaseCosmosAccount{ + DatabaseName: infraSpec.DbCosmos.DatabaseName, + Containers: infraSpec.DbCosmos.Containers, + } + } } slices.SortFunc(infraSpec.Services, func(a, b scaffold.ServiceSpec) int { From 3b33dc65e7aba31405d387c31ae175bc83a4ae47 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 15 Nov 2024 08:55:57 +0800 Subject: [PATCH 67/92] Fix error about marshal yaml (#26) --- cli/azd/pkg/project/resources.go | 30 +++++++++++++++++++++++++++++ cli/azd/pkg/project/scaffold_gen.go | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/cli/azd/pkg/project/resources.go b/cli/azd/pkg/project/resources.go index cd468eec9ce..2ef62e5875e 100644 --- a/cli/azd/pkg/project/resources.go +++ b/cli/azd/pkg/project/resources.go @@ -106,11 +106,41 @@ func (r *ResourceConfig) MarshalYAML() (interface{}, error) { if err != nil { return nil, err } + case ResourceTypeDbPostgres: + err := marshalRawProps(raw.Props.(PostgresProps)) + if err != nil { + return nil, err + } case ResourceTypeDbMySQL: err := marshalRawProps(raw.Props.(MySQLProps)) if err != nil { return nil, err } + case ResourceTypeDbMongo: + err := marshalRawProps(raw.Props.(MongoDBProps)) + if err != nil { + return nil, err + } + case ResourceTypeDbCosmos: + err := marshalRawProps(raw.Props.(CosmosDBProps)) + if err != nil { + return nil, err + } + case ResourceTypeMessagingServiceBus: + err := marshalRawProps(raw.Props.(ServiceBusProps)) + if err != nil { + return nil, err + } + case ResourceTypeMessagingEventHubs: + err := marshalRawProps(raw.Props.(EventHubsProps)) + if err != nil { + return nil, err + } + case ResourceTypeMessagingKafka: + err := marshalRawProps(raw.Props.(KafkaProps)) + if err != nil { + return nil, err + } } return raw, nil diff --git a/cli/azd/pkg/project/scaffold_gen.go b/cli/azd/pkg/project/scaffold_gen.go index a2e1aa99853..e1c5728b379 100644 --- a/cli/azd/pkg/project/scaffold_gen.go +++ b/cli/azd/pkg/project/scaffold_gen.go @@ -141,7 +141,7 @@ func infraSpec(projectConfig *ProjectConfig) (*scaffold.InfraSpec, error) { infraSpec.DbRedis = &scaffold.DatabaseRedis{} case ResourceTypeDbMongo: infraSpec.DbCosmosMongo = &scaffold.DatabaseCosmosMongo{ - DatabaseName: res.Props.(CosmosDBProps).DatabaseName, + DatabaseName: res.Props.(MongoDBProps).DatabaseName, } case ResourceTypeDbPostgres: infraSpec.DbPostgres = &scaffold.DatabasePostgres{ From a0b3d6baf545ee7086e64968290baa880c20fb9e Mon Sep 17 00:00:00 2001 From: HaoZhang Date: Fri, 15 Nov 2024 10:59:51 +0800 Subject: [PATCH 68/92] Prompt when Kafka detected but no spring-cloud-azure dependency found (#27) * prompt if detected kafka but no spring-cloud-azure found, can help to add this dep * remove auto-add dep code * add UT * resolve comments * remove unused ones --------- Co-authored-by: haozhang --- cli/azd/internal/appdetect/appdetect.go | 7 ++++ cli/azd/internal/appdetect/java.go | 4 ++ cli/azd/internal/repository/app_init.go | 55 ++++++++++++++++++++++++- 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/cli/azd/internal/appdetect/appdetect.go b/cli/azd/internal/appdetect/appdetect.go index 66e2d035984..26b76e96e73 100644 --- a/cli/azd/internal/appdetect/appdetect.go +++ b/cli/azd/internal/appdetect/appdetect.go @@ -166,6 +166,13 @@ func (a AzureDepStorageAccount) ResourceDisplay() string { return "Azure Storage Account" } +type SpringCloudAzureDep struct { +} + +func (a SpringCloudAzureDep) ResourceDisplay() string { + return "Spring Cloud Azure Starter" +} + type Project struct { // The language associated with the project. Language Language diff --git a/cli/azd/internal/appdetect/java.go b/cli/azd/internal/appdetect/java.go index f725af813a2..73b6b36bd9a 100644 --- a/cli/azd/internal/appdetect/java.go +++ b/cli/azd/internal/appdetect/java.go @@ -236,6 +236,10 @@ func detectDependencies(mavenProject *mavenProject, project *Project) (*Project, UseKafka: true, }) } + + if dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-starter" { + project.AzureDeps = append(project.AzureDeps, SpringCloudAzureDep{}) + } } if len(databaseDepMap) > 0 { diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index 0c056701814..93a5cff75f7 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -2,6 +2,7 @@ package repository import ( "context" + "errors" "fmt" "maps" "os" @@ -128,10 +129,30 @@ func (i *Initializer) InitFromApp( i.console.StopSpinner(ctx, title, input.StepDone) var prjAppHost []appdetect.Project - for _, prj := range projects { + for index, prj := range projects { if prj.Language == appdetect.DotNetAppHost { prjAppHost = append(prjAppHost, prj) } + + if prj.Language == appdetect.Java { + var hasKafkaDep bool + var hasSpringCloudAzureDep bool + for _, dep := range prj.AzureDeps { + if eventHubs, ok := dep.(appdetect.AzureDepEventHubs); ok && eventHubs.UseKafka { + hasKafkaDep = true + } + if _, ok := dep.(appdetect.SpringCloudAzureDep); ok { + hasSpringCloudAzureDep = true + } + } + + if hasKafkaDep && !hasSpringCloudAzureDep { + err := processSpringCloudAzureDepByPrompt(i.console, ctx, &projects[index]) + if err != nil { + return err + } + } + } } if len(prjAppHost) > 1 { @@ -768,3 +789,35 @@ func ServiceFromDetect( return svc, nil } + +func processSpringCloudAzureDepByPrompt(console input.Console, ctx context.Context, project *appdetect.Project) error { + continueOption, err := console.Select(ctx, input.ConsoleOptions{ + Message: "Detected Kafka dependency but no spring-cloud-azure-starter found. Select an option", + Options: []string{ + "Exit then I will manually add this dependency", + "Continue without this dependency, and provision Azure Event Hubs for Kafka", + "Continue without this dependency, and not provision Azure Event Hubs for Kafka", + }, + }) + if err != nil { + return err + } + + switch continueOption { + case 0: + return errors.New("you have to manually add dependency com.azure.spring:spring-cloud-azure-starter by following https://github.com/Azure/azure-sdk-for-java/wiki/Spring-Versions-Mapping") + case 1: + return nil + case 2: + // remove Kafka Azure Dep + var result []appdetect.AzureDep + for _, dep := range project.AzureDeps { + if eventHubs, ok := dep.(appdetect.AzureDepEventHubs); !(ok && eventHubs.UseKafka) { + result = append(result, dep) + } + } + project.AzureDeps = result + return nil + } + return nil +} From c04905cd16099f98cf3edc2fa64f78d763a34a32 Mon Sep 17 00:00:00 2001 From: Xiaolu Dai <31124698+saragluna@users.noreply.github.com> Date: Tue, 19 Nov 2024 08:20:34 +0800 Subject: [PATCH 69/92] fix code to support servicebus in azd composability (#29) --- cli/azd/internal/repository/app_init.go | 11 +++++++++++ cli/azd/internal/scaffold/scaffold.go | 13 +++++++++++++ cli/azd/pkg/project/scaffold_gen.go | 7 +++++++ 3 files changed, 31 insertions(+) diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index 93a5cff75f7..7ec4d2fa4e9 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -658,6 +658,17 @@ func (i *Initializer) prjConfigFromDetect( resSpec.Uses = append(resSpec.Uses, dbNames[db]) } + for _, azureDep := range svc.AzureDeps { + switch azureDep.(type) { + case appdetect.AzureDepServiceBus: + resSpec.Uses = append(resSpec.Uses, "servicebus") + case appdetect.AzureDepEventHubs: + resSpec.Uses = append(resSpec.Uses, "eventhubs") + case appdetect.AzureDepStorageAccount: + resSpec.Uses = append(resSpec.Uses, "storage") + } + } + resSpec.Name = name resSpec.Props = props config.Resources[name] = &resSpec diff --git a/cli/azd/internal/scaffold/scaffold.go b/cli/azd/internal/scaffold/scaffold.go index a13edc7f126..65cc9fc433e 100644 --- a/cli/azd/internal/scaffold/scaffold.go +++ b/cli/azd/internal/scaffold/scaffold.go @@ -3,6 +3,7 @@ package scaffold import ( "bytes" "fmt" + "github.com/azure/azure-dev/cli/azd/internal" "io/fs" "os" "path" @@ -76,6 +77,18 @@ func supportingFiles(spec InfraSpec) []string { files = append(files, "/modules/fetch-container-image.bicep") } + if spec.AzureServiceBus != nil && spec.AzureServiceBus.AuthType == internal.AuthTypeConnectionString { + files = append(files, "/modules/set-servicebus-namespace-connection-string.bicep") + } + + if spec.AzureEventHubs != nil && spec.AzureEventHubs.AuthType == internal.AuthTypeConnectionString { + files = append(files, "/modules/set-event-hubs-namespace-connection-string.bicep") + } + + if spec.AzureStorageAccount != nil && spec.AzureStorageAccount.AuthType == internal.AuthTypeConnectionString { + files = append(files, "/modules/set-storage-account-connection-string.bicep") + } + return files } diff --git a/cli/azd/pkg/project/scaffold_gen.go b/cli/azd/pkg/project/scaffold_gen.go index e1c5728b379..f7033c7ee9f 100644 --- a/cli/azd/pkg/project/scaffold_gen.go +++ b/cli/azd/pkg/project/scaffold_gen.go @@ -339,6 +339,13 @@ func mapHostUses( backendMapping[use] = res.Name // record the backend -> frontend mapping case ResourceTypeOpenAiModel: svcSpec.AIModels = append(svcSpec.AIModels, scaffold.AIModelReference{Name: use}) + case ResourceTypeMessagingServiceBus: + props := useRes.Props.(ServiceBusProps) + svcSpec.AzureServiceBus = &scaffold.AzureDepServiceBus{ + Queues: props.Queues, + AuthType: props.AuthType, + IsJms: props.IsJms, + } } } From 9f9c553880591af3c7d3e74963fc4f8032ffb10a Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Tue, 19 Nov 2024 14:37:39 +0800 Subject: [PATCH 70/92] Handle environment variable placeholder in property file. (#31) --- cli/azd/internal/appdetect/java.go | 13 +++++- cli/azd/internal/appdetect/java_test.go | 53 +++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 cli/azd/internal/appdetect/java_test.go diff --git a/cli/azd/internal/appdetect/java.go b/cli/azd/internal/appdetect/java.go index 73b6b36bd9a..a7ecc940e24 100644 --- a/cli/azd/internal/appdetect/java.go +++ b/cli/azd/internal/appdetect/java.go @@ -323,7 +323,7 @@ func parseYAML(prefix string, node *yaml.Node, result map[string]string) { } case yaml.ScalarNode: // If it's a scalar value, add it to the result map - result[prefix] = node.Value + result[prefix] = getEnvironmentVariablePlaceholderHandledValue(node.Value) default: // Handle other node types if necessary } @@ -349,12 +349,21 @@ func readPropertiesInPropertiesFile(propertiesFilePath string, result map[string parts := strings.SplitN(line, "=", 2) if len(parts) == 2 { key := strings.TrimSpace(parts[0]) - value := strings.TrimSpace(parts[1]) + value := getEnvironmentVariablePlaceholderHandledValue(parts[1]) result[key] = value } } } +func getEnvironmentVariablePlaceholderHandledValue(rawValue string) string { + trimmedRawValue := strings.TrimSpace(rawValue) + if strings.HasPrefix(trimmedRawValue, "${") && strings.HasSuffix(trimmedRawValue, "}") { + envVar := trimmedRawValue[2 : len(trimmedRawValue)-1] + return os.Getenv(envVar) + } + return trimmedRawValue +} + // Function to find all properties that match the pattern `spring.cloud.stream.bindings..destination` func findBindingDestinations(properties map[string]string) map[string]string { result := make(map[string]string) diff --git a/cli/azd/internal/appdetect/java_test.go b/cli/azd/internal/appdetect/java_test.go new file mode 100644 index 00000000000..5eac985f3f8 --- /dev/null +++ b/cli/azd/internal/appdetect/java_test.go @@ -0,0 +1,53 @@ +package appdetect + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetEnvironmentVariablePlaceholderHandledValue(t *testing.T) { + + tests := []struct { + name string + inputValue string + environmentVariables map[string]string + expectedValue string + }{ + { + "No environment variable placeholder", + "valueOne", + map[string]string{}, + "valueOne", + }, + { + "Has invalid environment variable placeholder", + "${VALUE_ONE", + map[string]string{}, + "${VALUE_ONE", + }, + { + "Has valid environment variable placeholder, but environment variable not set", + "${VALUE_TWO}", + map[string]string{}, + "", + }, + { + "Has valid environment variable placeholder, and environment variable set", + "${VALUE_THREE}", + map[string]string{"VALUE_THREE": "valueThree"}, + "valueThree", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for k, v := range tt.environmentVariables { + err := os.Setenv(k, v) + require.NoError(t, err) + } + handledValue := getEnvironmentVariablePlaceholderHandledValue(tt.inputValue) + require.Equal(t, tt.expectedValue, handledValue) + }) + } +} From 521d706b786a377192d0ea00542136bdac5564b3 Mon Sep 17 00:00:00 2001 From: HaoZhang Date: Tue, 19 Nov 2024 14:50:24 +0800 Subject: [PATCH 71/92] Inject AzureEventHubsKafkaAutoConfiguration package path based on spring boot version (#28) * for Kafka, inject AzureEventHubsKafkaAutoConfiguration package path based on spring boot version * parse properties to fill possible spring boot version * update UT * update detect spring boot version logic * small fix --------- Co-authored-by: haozhang --- cli/azd/internal/appdetect/appdetect.go | 7 +- cli/azd/internal/appdetect/java.go | 60 ++++- cli/azd/internal/appdetect/java_test.go | 226 +++++++++++++++++- cli/azd/internal/repository/app_init.go | 35 ++- cli/azd/internal/repository/infra_confirm.go | 7 +- cli/azd/internal/scaffold/scaffold.go | 1 + cli/azd/internal/scaffold/spec.go | 7 +- .../scaffold/templates/resources.bicept | 12 + 8 files changed, 339 insertions(+), 16 deletions(-) diff --git a/cli/azd/internal/appdetect/appdetect.go b/cli/azd/internal/appdetect/appdetect.go index 26b76e96e73..77a362f140b 100644 --- a/cli/azd/internal/appdetect/appdetect.go +++ b/cli/azd/internal/appdetect/appdetect.go @@ -150,8 +150,9 @@ func (a AzureDepServiceBus) ResourceDisplay() string { } type AzureDepEventHubs struct { - Names []string - UseKafka bool + Names []string + UseKafka bool + SpringBootVersion string } func (a AzureDepEventHubs) ResourceDisplay() string { @@ -173,6 +174,8 @@ func (a SpringCloudAzureDep) ResourceDisplay() string { return "Spring Cloud Azure Starter" } +const UnknownSpringBootVersion string = "unknownSpringBootVersion" + type Project struct { // The language associated with the project. Language Language diff --git a/cli/azd/internal/appdetect/java.go b/cli/azd/internal/appdetect/java.go index a7ecc940e24..24ff12b7810 100644 --- a/cli/azd/internal/appdetect/java.go +++ b/cli/azd/internal/appdetect/java.go @@ -52,7 +52,7 @@ func (jd *javaDetector) DetectProject(ctx context.Context, path string, entries } _ = currentRoot // use currentRoot here in the analysis - result, err := detectDependencies(project, &Project{ + result, err := detectDependencies(currentRoot, project, &Project{ Language: Java, Path: path, DetectionRule: "Inferred by presence of: pom.xml", @@ -74,6 +74,7 @@ type mavenProject struct { XmlName xml.Name `xml:"project"` Parent parent `xml:"parent"` Modules []string `xml:"modules>module"` // Capture the modules + Properties Properties `xml:"properties"` Dependencies []dependency `xml:"dependencies>dependency"` DependencyManagement dependencyManagement `xml:"dependencyManagement"` Build build `xml:"build"` @@ -87,6 +88,15 @@ type parent struct { Version string `xml:"version"` } +type Properties struct { + Entries []Property `xml:",any"` // Capture all elements inside +} + +type Property struct { + XMLName xml.Name + Value string `xml:",chardata"` +} + // Dependency represents a single Maven dependency. type dependency struct { GroupId string `xml:"groupId"` @@ -128,7 +138,7 @@ func readMavenProject(filePath string) (*mavenProject, error) { return &project, nil } -func detectDependencies(mavenProject *mavenProject, project *Project) (*Project, error) { +func detectDependencies(currentRoot *mavenProject, mavenProject *mavenProject, project *Project) (*Project, error) { // how can we tell it's a Spring Boot project? // 1. It has a parent with a groupId of org.springframework.boot and an artifactId of spring-boot-starter-parent // 2. It has a dependency with a groupId of org.springframework.boot and an artifactId that starts with @@ -145,8 +155,10 @@ func detectDependencies(mavenProject *mavenProject, project *Project) (*Project, } } applicationProperties := make(map[string]string) + var springBootVersion string if isSpringBoot { applicationProperties = readProperties(project.Path) + springBootVersion = detectSpringBootVersion(currentRoot, mavenProject) } databaseDepMap := map[DatabaseDep]struct{}{} @@ -232,8 +244,9 @@ func detectDependencies(mavenProject *mavenProject, project *Project) (*Project, } } project.AzureDeps = append(project.AzureDeps, AzureDepEventHubs{ - Names: destinations, - UseKafka: true, + Names: destinations, + UseKafka: true, + SpringBootVersion: springBootVersion, }) } @@ -390,3 +403,42 @@ func contains(array []string, str string) bool { } return false } + +func parseProperties(properties Properties) map[string]string { + result := make(map[string]string) + for _, entry := range properties.Entries { + result[entry.XMLName.Local] = entry.Value + } + return result +} + +func detectSpringBootVersion(currentRoot *mavenProject, mavenProject *mavenProject) string { + // mavenProject prioritize than rootProject + if mavenProject != nil { + return detectSpringBootVersionFromProject(mavenProject) + } else if currentRoot != nil { + return detectSpringBootVersionFromProject(currentRoot) + } + return UnknownSpringBootVersion +} + +func detectSpringBootVersionFromProject(project *mavenProject) string { + if project.Parent.ArtifactId == "spring-boot-starter-parent" { + return depVersion(project.Parent.Version, project.Properties) + } else { + for _, dep := range project.DependencyManagement.Dependencies { + if dep.ArtifactId == "spring-boot-dependencies" { + return depVersion(dep.Version, project.Properties) + } + } + } + return UnknownSpringBootVersion +} + +func depVersion(version string, properties Properties) string { + if strings.HasPrefix(version, "${") { + return parseProperties(properties)[version[2:len(version)-1]] + } else { + return version + } +} diff --git a/cli/azd/internal/appdetect/java_test.go b/cli/azd/internal/appdetect/java_test.go index 5eac985f3f8..f6d8e89a80d 100644 --- a/cli/azd/internal/appdetect/java_test.go +++ b/cli/azd/internal/appdetect/java_test.go @@ -1,14 +1,14 @@ package appdetect import ( + "encoding/xml" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "os" "testing" - - "github.com/stretchr/testify/require" ) func TestGetEnvironmentVariablePlaceholderHandledValue(t *testing.T) { - tests := []struct { name string inputValue string @@ -51,3 +51,223 @@ func TestGetEnvironmentVariablePlaceholderHandledValue(t *testing.T) { }) } } + +func TestDetectSpringBootVersion(t *testing.T) { + tests := []struct { + name string + currentRoot *mavenProject + project *mavenProject + expectedVersion string + }{ + { + "unknown", + nil, + nil, + UnknownSpringBootVersion, + }, + { + "project.parent", + nil, + &mavenProject{ + Parent: parent{ + GroupId: "org.springframework.boot", + ArtifactId: "spring-boot-starter-parent", + Version: "2.x", + }, + }, + "2.x", + }, + { + "project.dependencyManagement", + nil, + &mavenProject{ + DependencyManagement: dependencyManagement{ + Dependencies: []dependency{ + { + GroupId: "org.springframework.boot", + ArtifactId: "spring-boot-dependencies", + Version: "2.x", + }, + }, + }, + }, + "2.x", + }, + { + "project.dependencyManagement.property", + nil, + &mavenProject{ + Properties: Properties{ + Entries: []Property{ + { + XMLName: xml.Name{ + Local: "version.spring.boot", + }, + Value: "2.x", + }, + }, + }, + DependencyManagement: dependencyManagement{ + Dependencies: []dependency{ + { + GroupId: "org.springframework.boot", + ArtifactId: "spring-boot-dependencies", + Version: "${version.spring.boot}", + }, + }, + }, + }, + "2.x", + }, + { + "root.parent", + &mavenProject{ + Parent: parent{ + GroupId: "org.springframework.boot", + ArtifactId: "spring-boot-starter-parent", + Version: "3.x", + }, + }, + nil, + "3.x", + }, + { + "root.dependencyManagement", + &mavenProject{ + DependencyManagement: dependencyManagement{ + Dependencies: []dependency{ + { + GroupId: "org.springframework.boot", + ArtifactId: "spring-boot-dependencies", + Version: "3.x", + }, + }, + }, + }, + nil, + "3.x", + }, + { + "root.dependencyManagement.property", + nil, + &mavenProject{ + Properties: Properties{ + Entries: []Property{ + { + XMLName: xml.Name{ + Local: "version.spring.boot", + }, + Value: "3.x", + }, + }, + }, + DependencyManagement: dependencyManagement{ + Dependencies: []dependency{ + { + GroupId: "org.springframework.boot", + ArtifactId: "spring-boot-dependencies", + Version: "${version.spring.boot}", + }, + }, + }, + }, + "3.x", + }, + { + "both.root.and.project.parent", + &mavenProject{ + Parent: parent{ + GroupId: "org.springframework.boot", + ArtifactId: "spring-boot-starter-parent", + Version: "2.x", + }, + }, + &mavenProject{ + Parent: parent{ + GroupId: "org.springframework.boot", + ArtifactId: "spring-boot-starter-parent", + Version: "3.x", + }, + }, + "3.x", + }, + { + "both.root.and.project.dependencyManagement", + &mavenProject{ + DependencyManagement: dependencyManagement{ + Dependencies: []dependency{ + { + GroupId: "org.springframework.boot", + ArtifactId: "spring-boot-dependencies", + Version: "2.x", + }, + }, + }, + }, + &mavenProject{ + DependencyManagement: dependencyManagement{ + Dependencies: []dependency{ + { + GroupId: "org.springframework.boot", + ArtifactId: "spring-boot-dependencies", + Version: "3.x", + }, + }, + }, + }, + "3.x", + }, + { + "both.root.and.project.dependencyManagement.property", + &mavenProject{ + Properties: Properties{ + Entries: []Property{ + { + XMLName: xml.Name{ + Local: "version.spring.boot", + }, + Value: "2.x", + }, + }, + }, + DependencyManagement: dependencyManagement{ + Dependencies: []dependency{ + { + GroupId: "org.springframework.boot", + ArtifactId: "spring-boot-dependencies", + Version: "${version.spring.boot}", + }, + }, + }, + }, + &mavenProject{ + Properties: Properties{ + Entries: []Property{ + { + XMLName: xml.Name{ + Local: "version.spring.boot", + }, + Value: "3.x", + }, + }, + }, + DependencyManagement: dependencyManagement{ + Dependencies: []dependency{ + { + GroupId: "org.springframework.boot", + ArtifactId: "spring-boot-dependencies", + Version: "${version.spring.boot}", + }, + }, + }, + }, + "3.x", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + version := detectSpringBootVersion(tt.currentRoot, tt.project) + assert.Equal(t, tt.expectedVersion, version) + }) + } +} diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index 7ec4d2fa4e9..446fbc23d92 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -137,9 +137,20 @@ func (i *Initializer) InitFromApp( if prj.Language == appdetect.Java { var hasKafkaDep bool var hasSpringCloudAzureDep bool - for _, dep := range prj.AzureDeps { + for depIndex, dep := range prj.AzureDeps { if eventHubs, ok := dep.(appdetect.AzureDepEventHubs); ok && eventHubs.UseKafka { hasKafkaDep = true + springBootVersion := eventHubs.SpringBootVersion + + if springBootVersion == appdetect.UnknownSpringBootVersion { + var err error + springBootVersion, err = promptSpringBootVersion(i.console, ctx) + if err != nil { + return err + } + eventHubs.SpringBootVersion = springBootVersion + prj.AzureDeps[depIndex] = eventHubs + } } if _, ok := dep.(appdetect.SpringCloudAzureDep); ok { hasSpringCloudAzureDep = true @@ -832,3 +843,25 @@ func processSpringCloudAzureDepByPrompt(console input.Console, ctx context.Conte } return nil } + +func promptSpringBootVersion(console input.Console, ctx context.Context) (string, error) { + selection, err := console.Select(ctx, input.ConsoleOptions{ + Message: "No spring boot version detected, what is your spring boot version?", + Options: []string{ + "Spring Boot 2.x", + "Spring Boot 3.x", + }, + }) + if err != nil { + return "", err + } + + switch selection { + case 0: + return "2.x", nil + case 1: + return "3.x", nil + default: + panic("unhandled selection") + } +} diff --git a/cli/azd/internal/repository/infra_confirm.go b/cli/azd/internal/repository/infra_confirm.go index b8ed6d41dee..54c7eac0475 100644 --- a/cli/azd/internal/repository/infra_confirm.go +++ b/cli/azd/internal/repository/infra_confirm.go @@ -349,9 +349,10 @@ func (i *Initializer) buildInfraSpecByAzureDep( } case appdetect.AzureDepEventHubs: spec.AzureEventHubs = &scaffold.AzureDepEventHubs{ - EventHubNames: dependency.Names, - AuthType: authType, - UseKafka: dependency.UseKafka, + EventHubNames: dependency.Names, + AuthType: authType, + UseKafka: dependency.UseKafka, + SpringBootVersion: dependency.SpringBootVersion, } case appdetect.AzureDepStorageAccount: spec.AzureStorageAccount = &scaffold.AzureDepStorageAccount{ diff --git a/cli/azd/internal/scaffold/scaffold.go b/cli/azd/internal/scaffold/scaffold.go index 65cc9fc433e..b8042ffd88b 100644 --- a/cli/azd/internal/scaffold/scaffold.go +++ b/cli/azd/internal/scaffold/scaffold.go @@ -31,6 +31,7 @@ func Load() (*template.Template, error) { "lower": strings.ToLower, "alphaSnakeUpper": AlphaSnakeUpper, "formatParam": FormatParameter, + "hasPrefix": strings.HasPrefix, } t, err := template.New("templates"). diff --git a/cli/azd/internal/scaffold/spec.go b/cli/azd/internal/scaffold/spec.go index d9f6f3c5636..b803e488981 100644 --- a/cli/azd/internal/scaffold/spec.go +++ b/cli/azd/internal/scaffold/spec.go @@ -83,9 +83,10 @@ type AzureDepServiceBus struct { } type AzureDepEventHubs struct { - EventHubNames []string - AuthType internal.AuthType - UseKafka bool + EventHubNames []string + AuthType internal.AuthType + UseKafka bool + SpringBootVersion string } type AzureDepStorageAccount struct { diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index 01d2e0c7044..2516bf648c7 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -788,6 +788,18 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { name: 'spring.cloud.stream.kafka.binder.brokers' value: '${eventHubNamespace.outputs.name}.servicebus.windows.net:9093' } + {{- if (hasPrefix .AzureEventHubs.SpringBootVersion "2.") }} + { + name: 'spring.cloud.stream.binders.kafka.environment.spring.main.sources' + value: 'com.azure.spring.cloud.autoconfigure.eventhubs.kafka.AzureEventHubsKafkaAutoConfiguration' + } + {{- end}} + {{- if (hasPrefix .AzureEventHubs.SpringBootVersion "3.") }} + { + name: 'spring.cloud.stream.binders.kafka.environment.spring.main.sources' + value: 'com.azure.spring.cloud.autoconfigure.implementation.eventhubs.kafka.AzureEventHubsKafkaAutoConfiguration' + } + {{- end}} {{- end}} {{- if (and .AzureEventHubs (eq .AzureEventHubs.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) }} { From 3727ead214369444a711020a5e7f9cb7c581ea55 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Tue, 19 Nov 2024 17:50:33 +0800 Subject: [PATCH 72/92] Fix some bindings specified by uses keywork not work (#30) * 1. Fix error: Missing information in ServiceSpec. Like messaging service. 2. Use DatabaseMysql to replace DatabaseReference to make code simplifier. Same to all other db type. 3. Simplify the code of handleContainerAppProps. 4. Simplify the code of Frontend and Backend fulfill in ServiceSpec * Fix test error. * 1. Print hints about uses relationships. 2. Add frontend BASE_URL in backend service. * Delete "todo" because it's already done. * Use printf for each line of log to make it look better. * Use console.Message instead of log.Printf. Because log.Printf will print nothing when there is no "--debug" added when run azd. * Fix compile error in test files. * Update the description in hints. * 1. Split "mapUses" function into to functions: "mapUses" and "printHintsAboutUses". 2. Add missing hint for AI module. --- cli/azd/cmd/middleware/hooks_test.go | 3 +- cli/azd/internal/repository/infra_confirm.go | 18 +- .../internal/repository/infra_confirm_test.go | 2 +- cli/azd/internal/scaffold/scaffold_test.go | 20 +- cli/azd/internal/scaffold/spec.go | 17 +- cli/azd/pkg/pipeline/pipeline_manager_test.go | 5 +- cli/azd/pkg/project/importer.go | 9 +- cli/azd/pkg/project/importer_test.go | 13 +- cli/azd/pkg/project/scaffold_gen.go | 461 +++++++++++++----- .../scaffold/templates/resources.bicept | 9 +- 10 files changed, 389 insertions(+), 168 deletions(-) diff --git a/cli/azd/cmd/middleware/hooks_test.go b/cli/azd/cmd/middleware/hooks_test.go index ee4e42d4922..517bfefd7c8 100644 --- a/cli/azd/cmd/middleware/hooks_test.go +++ b/cli/azd/cmd/middleware/hooks_test.go @@ -3,6 +3,7 @@ package middleware import ( "context" "errors" + "github.com/azure/azure-dev/cli/azd/test/mocks/mockinput" "strings" "testing" @@ -355,7 +356,7 @@ func runMiddleware( lazyEnvManager, lazyEnv, lazyProjectConfig, - project.NewImportManager(nil), + project.NewImportManager(nil, mockinput.NewMockConsole()), mockContext.CommandRunner, mockContext.Console, runOptions, diff --git a/cli/azd/internal/repository/infra_confirm.go b/cli/azd/internal/repository/infra_confirm.go index 54c7eac0475..6874fd232c1 100644 --- a/cli/azd/internal/repository/infra_confirm.go +++ b/cli/azd/internal/repository/infra_confirm.go @@ -135,25 +135,15 @@ func (i *Initializer) infraSpecFromDetect( switch db { case appdetect.DbMongo: - serviceSpec.DbCosmosMongo = &scaffold.DatabaseReference{ - DatabaseName: spec.DbCosmosMongo.DatabaseName, - } + serviceSpec.DbCosmosMongo = spec.DbCosmosMongo case appdetect.DbPostgres: - serviceSpec.DbPostgres = &scaffold.DatabaseReference{ - DatabaseName: spec.DbPostgres.DatabaseName, - AuthType: spec.DbPostgres.AuthType, - } + serviceSpec.DbPostgres = spec.DbPostgres case appdetect.DbMySql: - serviceSpec.DbMySql = &scaffold.DatabaseReference{ - DatabaseName: spec.DbMySql.DatabaseName, - AuthType: spec.DbMySql.AuthType, - } + serviceSpec.DbMySql = spec.DbMySql case appdetect.DbCosmos: serviceSpec.DbCosmos = spec.DbCosmos case appdetect.DbRedis: - serviceSpec.DbRedis = &scaffold.DatabaseReference{ - DatabaseName: "redis", - } + serviceSpec.DbRedis = spec.DbRedis } } diff --git a/cli/azd/internal/repository/infra_confirm_test.go b/cli/azd/internal/repository/infra_confirm_test.go index 85652331d6d..98839ce520a 100644 --- a/cli/azd/internal/repository/infra_confirm_test.go +++ b/cli/azd/internal/repository/infra_confirm_test.go @@ -188,7 +188,7 @@ func TestInitializer_infraSpecFromDetect(t *testing.T) { }, }, }, - DbPostgres: &scaffold.DatabaseReference{ + DbPostgres: &scaffold.DatabasePostgres{ DatabaseName: "myappdb", AuthType: "USER_ASSIGNED_MANAGED_IDENTITY", }, diff --git a/cli/azd/internal/scaffold/scaffold_test.go b/cli/azd/internal/scaffold/scaffold_test.go index 238043c3673..d5a7dc212fb 100644 --- a/cli/azd/internal/scaffold/scaffold_test.go +++ b/cli/azd/internal/scaffold/scaffold_test.go @@ -98,13 +98,11 @@ func TestExecInfra(t *testing.T) { }, }, }, - DbCosmosMongo: &DatabaseReference{ + DbCosmosMongo: &DatabaseCosmosMongo{ DatabaseName: "appdb", }, - DbRedis: &DatabaseReference{ - DatabaseName: "redis", - }, - DbPostgres: &DatabaseReference{ + DbRedis: &DatabaseRedis{}, + DbPostgres: &DatabasePostgres{ DatabaseName: "appdb", }, }, @@ -133,7 +131,7 @@ func TestExecInfra(t *testing.T) { { Name: "api", Port: 3100, - DbPostgres: &DatabaseReference{ + DbPostgres: &DatabasePostgres{ DatabaseName: "appdb", }, }, @@ -150,7 +148,7 @@ func TestExecInfra(t *testing.T) { { Name: "api", Port: 3100, - DbCosmosMongo: &DatabaseReference{ + DbCosmosMongo: &DatabaseCosmosMongo{ DatabaseName: "appdb", }, }, @@ -163,11 +161,9 @@ func TestExecInfra(t *testing.T) { DbRedis: &DatabaseRedis{}, Services: []ServiceSpec{ { - Name: "api", - Port: 3100, - DbRedis: &DatabaseReference{ - DatabaseName: "redis", - }, + Name: "api", + Port: 3100, + DbRedis: &DatabaseRedis{}, }, }, }, diff --git a/cli/azd/internal/scaffold/spec.go b/cli/azd/internal/scaffold/spec.go index b803e488981..e1c5aa2c597 100644 --- a/cli/azd/internal/scaffold/spec.go +++ b/cli/azd/internal/scaffold/spec.go @@ -13,9 +13,9 @@ type InfraSpec struct { // Databases to create DbPostgres *DatabasePostgres DbMySql *DatabaseMySql - DbCosmos *DatabaseCosmosAccount - DbCosmosMongo *DatabaseCosmosMongo DbRedis *DatabaseRedis + DbCosmosMongo *DatabaseCosmosMongo + DbCosmos *DatabaseCosmosAccount // ai models AIModels []AIModel @@ -107,11 +107,11 @@ type ServiceSpec struct { Backend *Backend // Connection to a database - DbPostgres *DatabaseReference - DbMySql *DatabaseReference - DbCosmosMongo *DatabaseReference + DbPostgres *DatabasePostgres + DbMySql *DatabaseMySql + DbRedis *DatabaseRedis + DbCosmosMongo *DatabaseCosmosMongo DbCosmos *DatabaseCosmosAccount - DbRedis *DatabaseReference // AI model connections AIModels []AIModelReference @@ -133,11 +133,6 @@ type ServiceReference struct { Name string } -type DatabaseReference struct { - DatabaseName string - AuthType internal.AuthType -} - type AIModelReference struct { Name string } diff --git a/cli/azd/pkg/pipeline/pipeline_manager_test.go b/cli/azd/pkg/pipeline/pipeline_manager_test.go index 6396a37f925..4f96b6f685e 100644 --- a/cli/azd/pkg/pipeline/pipeline_manager_test.go +++ b/cli/azd/pkg/pipeline/pipeline_manager_test.go @@ -6,6 +6,7 @@ package pipeline import ( "context" "fmt" + "github.com/azure/azure-dev/cli/azd/test/mocks/mockinput" "os" "path/filepath" "strings" @@ -773,7 +774,9 @@ func createPipelineManager( mockContext.Console, args, mockContext.Container, - project.NewImportManager(project.NewDotNetImporter(nil, nil, nil, nil, mockContext.AlphaFeaturesManager)), + project.NewImportManager( + project.NewDotNetImporter(nil, nil, nil, nil, mockContext.AlphaFeaturesManager), + mockinput.NewMockConsole()), &mockUserConfigManager{}, ) } diff --git a/cli/azd/pkg/project/importer.go b/cli/azd/pkg/project/importer.go index 26fbde3a07e..7262e7f85d9 100644 --- a/cli/azd/pkg/project/importer.go +++ b/cli/azd/pkg/project/importer.go @@ -6,6 +6,7 @@ package project import ( "context" "fmt" + "github.com/azure/azure-dev/cli/azd/pkg/input" "io/fs" "log" "os" @@ -19,11 +20,13 @@ import ( type ImportManager struct { dotNetImporter *DotNetImporter + console input.Console } -func NewImportManager(dotNetImporter *DotNetImporter) *ImportManager { +func NewImportManager(dotNetImporter *DotNetImporter, console input.Console) *ImportManager { return &ImportManager{ dotNetImporter: dotNetImporter, + console: console, } } @@ -167,7 +170,7 @@ func (im *ImportManager) ProjectInfrastructure(ctx context.Context, projectConfi composeEnabled := im.dotNetImporter.alphaFeatureManager.IsEnabled(featureCompose) if composeEnabled && len(projectConfig.Resources) > 0 { - return tempInfra(ctx, projectConfig) + return tempInfra(ctx, projectConfig, &im.console, &ctx) } if !composeEnabled && len(projectConfig.Resources) > 0 { @@ -209,7 +212,7 @@ func (im *ImportManager) SynthAllInfrastructure(ctx context.Context, projectConf composeEnabled := im.dotNetImporter.alphaFeatureManager.IsEnabled(featureCompose) if composeEnabled && len(projectConfig.Resources) > 0 { - return infraFsForProject(ctx, projectConfig) + return infraFsForProject(ctx, projectConfig, &im.console, &ctx) } if !composeEnabled && len(projectConfig.Resources) > 0 { diff --git a/cli/azd/pkg/project/importer_test.go b/cli/azd/pkg/project/importer_test.go index 168e5c93261..cf9c671c4f4 100644 --- a/cli/azd/pkg/project/importer_test.go +++ b/cli/azd/pkg/project/importer_test.go @@ -6,6 +6,7 @@ package project import ( "context" _ "embed" + "github.com/azure/azure-dev/cli/azd/test/mocks/mockinput" "os" "path/filepath" "slices" @@ -43,7 +44,7 @@ func TestImportManagerHasService(t *testing.T) { lazyEnvManager: lazy.NewLazy(func() (environment.Manager, error) { return mockEnv, nil }), - }) + }, mockinput.NewMockConsole()) // has service r, e := manager.HasService(*mockContext.Context, &ProjectConfig{ @@ -85,7 +86,7 @@ func TestImportManagerHasServiceErrorNoMultipleServicesWithAppHost(t *testing.T) return mockEnv, nil }), hostCheck: make(map[string]hostCheckResult), - }) + }, mockinput.NewMockConsole()) mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { return strings.Contains(command, "dotnet") && @@ -138,7 +139,7 @@ func TestImportManagerHasServiceErrorAppHostMustTargetContainerApp(t *testing.T) return mockEnv, nil }), hostCheck: make(map[string]hostCheckResult), - }) + }, mockinput.NewMockConsole()) mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { return strings.Contains(command, "dotnet") && @@ -185,7 +186,7 @@ func TestImportManagerProjectInfrastructureDefaults(t *testing.T) { }), hostCheck: make(map[string]hostCheckResult), alphaFeatureManager: mockContext.AlphaFeaturesManager, - }) + }, mockinput.NewMockConsole()) // Get defaults and error b/c no infra found and no Aspire project r, e := manager.ProjectInfrastructure(*mockContext.Context, &ProjectConfig{}) @@ -234,7 +235,7 @@ func TestImportManagerProjectInfrastructure(t *testing.T) { return mockEnv, nil }), hostCheck: make(map[string]hostCheckResult), - }) + }, mockinput.NewMockConsole()) // Do not use defaults expectedDefaultFolder := "customFolder" @@ -316,7 +317,7 @@ func TestImportManagerProjectInfrastructureAspire(t *testing.T) { hostCheck: make(map[string]hostCheckResult), cache: make(map[manifestCacheKey]*apphost.Manifest), alphaFeatureManager: alpha.NewFeaturesManagerWithConfig(config.NewEmptyConfig()), - }) + }, mockinput.NewMockConsole()) // adding infra folder to test defaults err := os.Mkdir(DefaultPath, os.ModePerm) diff --git a/cli/azd/pkg/project/scaffold_gen.go b/cli/azd/pkg/project/scaffold_gen.go index f7033c7ee9f..efa2c6e8dbe 100644 --- a/cli/azd/pkg/project/scaffold_gen.go +++ b/cli/azd/pkg/project/scaffold_gen.go @@ -6,6 +6,8 @@ package project import ( "context" "fmt" + "github.com/azure/azure-dev/cli/azd/internal" + "github.com/azure/azure-dev/cli/azd/pkg/input" "io/fs" "os" "path/filepath" @@ -19,13 +21,14 @@ import ( ) // Generates the in-memory contents of an `infra` directory. -func infraFs(_ context.Context, prjConfig *ProjectConfig) (fs.FS, error) { +func infraFs(_ context.Context, prjConfig *ProjectConfig, + console *input.Console, context *context.Context) (fs.FS, error) { t, err := scaffold.Load() if err != nil { return nil, fmt.Errorf("loading scaffold templates: %w", err) } - infraSpec, err := infraSpec(prjConfig) + infraSpec, err := infraSpec(prjConfig, console, context) if err != nil { return nil, fmt.Errorf("generating infrastructure spec: %w", err) } @@ -41,13 +44,15 @@ func infraFs(_ context.Context, prjConfig *ProjectConfig) (fs.FS, error) { // Returns the infrastructure configuration that points to a temporary, generated `infra` directory on the filesystem. func tempInfra( ctx context.Context, - prjConfig *ProjectConfig) (*Infra, error) { + prjConfig *ProjectConfig, + console *input.Console, + context *context.Context) (*Infra, error) { tmpDir, err := os.MkdirTemp("", "azd-infra") if err != nil { return nil, fmt.Errorf("creating temporary directory: %w", err) } - files, err := infraFs(ctx, prjConfig) + files, err := infraFs(ctx, prjConfig, console, context) if err != nil { return nil, err } @@ -89,8 +94,9 @@ func tempInfra( // Generates the filesystem of all infrastructure files to be placed, rooted at the project directory. // The content only includes `./infra` currently. -func infraFsForProject(ctx context.Context, prjConfig *ProjectConfig) (fs.FS, error) { - infraFS, err := infraFs(ctx, prjConfig) +func infraFsForProject(ctx context.Context, prjConfig *ProjectConfig, + console *input.Console, context *context.Context) (fs.FS, error) { + infraFS, err := infraFs(ctx, prjConfig, console, context) if err != nil { return nil, err } @@ -130,36 +136,34 @@ func infraFsForProject(ctx context.Context, prjConfig *ProjectConfig) (fs.FS, er return generatedFS, nil } -func infraSpec(projectConfig *ProjectConfig) (*scaffold.InfraSpec, error) { +func infraSpec(projectConfig *ProjectConfig, + console *input.Console, context *context.Context) (*scaffold.InfraSpec, error) { infraSpec := scaffold.InfraSpec{} - // backends -> frontends - backendMapping := map[string]string{} - - for _, res := range projectConfig.Resources { - switch res.Type { + for _, resource := range projectConfig.Resources { + switch resource.Type { case ResourceTypeDbRedis: infraSpec.DbRedis = &scaffold.DatabaseRedis{} case ResourceTypeDbMongo: infraSpec.DbCosmosMongo = &scaffold.DatabaseCosmosMongo{ - DatabaseName: res.Props.(MongoDBProps).DatabaseName, + DatabaseName: resource.Props.(MongoDBProps).DatabaseName, } case ResourceTypeDbPostgres: infraSpec.DbPostgres = &scaffold.DatabasePostgres{ - DatabaseName: res.Props.(PostgresProps).DatabaseName, + DatabaseName: resource.Props.(PostgresProps).DatabaseName, DatabaseUser: "pgadmin", - AuthType: res.Props.(PostgresProps).AuthType, + AuthType: resource.Props.(PostgresProps).AuthType, } case ResourceTypeDbMySQL: infraSpec.DbMySql = &scaffold.DatabaseMySql{ - DatabaseName: res.Props.(MySQLProps).DatabaseName, + DatabaseName: resource.Props.(MySQLProps).DatabaseName, DatabaseUser: "mysqladmin", - AuthType: res.Props.(MySQLProps).AuthType, + AuthType: resource.Props.(MySQLProps).AuthType, } case ResourceTypeDbCosmos: infraSpec.DbCosmos = &scaffold.DatabaseCosmosAccount{ - DatabaseName: res.Props.(CosmosDBProps).DatabaseName, + DatabaseName: resource.Props.(CosmosDBProps).DatabaseName, } - containers := res.Props.(CosmosDBProps).Containers + containers := resource.Props.(CosmosDBProps).Containers for _, container := range containers { infraSpec.DbCosmos.Containers = append(infraSpec.DbCosmos.Containers, scaffold.CosmosSqlDatabaseContainer{ ContainerName: container.ContainerName, @@ -167,55 +171,48 @@ func infraSpec(projectConfig *ProjectConfig) (*scaffold.InfraSpec, error) { }) } case ResourceTypeMessagingServiceBus: - props := res.Props.(ServiceBusProps) + props := resource.Props.(ServiceBusProps) infraSpec.AzureServiceBus = &scaffold.AzureDepServiceBus{ Queues: props.Queues, AuthType: props.AuthType, IsJms: props.IsJms, } case ResourceTypeMessagingEventHubs: - props := res.Props.(EventHubsProps) + props := resource.Props.(EventHubsProps) infraSpec.AzureEventHubs = &scaffold.AzureDepEventHubs{ EventHubNames: props.EventHubNames, AuthType: props.AuthType, UseKafka: false, } case ResourceTypeMessagingKafka: - props := res.Props.(KafkaProps) + props := resource.Props.(KafkaProps) infraSpec.AzureEventHubs = &scaffold.AzureDepEventHubs{ EventHubNames: props.Topics, AuthType: props.AuthType, UseKafka: true, } case ResourceTypeHostContainerApp: - svcSpec := scaffold.ServiceSpec{ - Name: res.Name, + serviceSpec := scaffold.ServiceSpec{ + Name: resource.Name, Port: -1, } - - err := mapContainerApp(res, &svcSpec, &infraSpec) + err := handleContainerAppProps(resource, &serviceSpec, &infraSpec) if err != nil { return nil, err } - - err = mapHostUses(res, &svcSpec, backendMapping, projectConfig) - if err != nil { - return nil, err - } - - infraSpec.Services = append(infraSpec.Services, svcSpec) + infraSpec.Services = append(infraSpec.Services, serviceSpec) case ResourceTypeOpenAiModel: - props := res.Props.(AIModelProps) + props := resource.Props.(AIModelProps) if len(props.Model.Name) == 0 { - return nil, fmt.Errorf("resources.%s.model is required", res.Name) + return nil, fmt.Errorf("resources.%s.model is required", resource.Name) } if len(props.Model.Version) == 0 { - return nil, fmt.Errorf("resources.%s.version is required", res.Name) + return nil, fmt.Errorf("resources.%s.version is required", resource.Name) } infraSpec.AIModels = append(infraSpec.AIModels, scaffold.AIModel{ - Name: res.Name, + Name: resource.Name, Model: scaffold.AIModelModel{ Name: props.Model.Name, Version: props.Model.Version, @@ -224,67 +221,157 @@ func infraSpec(projectConfig *ProjectConfig) (*scaffold.InfraSpec, error) { } } - // create reverse frontends -> backends mapping + err := mapUses(&infraSpec, projectConfig) + if err != nil { + return nil, err + } + + err = printHintsAboutUses(&infraSpec, projectConfig, console, context) + if err != nil { + return nil, err + } + + slices.SortFunc(infraSpec.Services, func(a, b scaffold.ServiceSpec) int { + return strings.Compare(a.Name, b.Name) + }) + + return &infraSpec, nil +} + +func mapUses(infraSpec *scaffold.InfraSpec, projectConfig *ProjectConfig) error { for i := range infraSpec.Services { - svc := &infraSpec.Services[i] - if front, ok := backendMapping[svc.Name]; ok { - if svc.Backend == nil { - svc.Backend = &scaffold.Backend{} - } - svc.Backend.Frontends = append(svc.Backend.Frontends, scaffold.ServiceReference{Name: front}) + userSpec := &infraSpec.Services[i] + userResourceName := userSpec.Name + userResource, ok := projectConfig.Resources[userResourceName] + if !ok { + return fmt.Errorf("service (%s) exist, but there isn't a resource with that name", + userResourceName) } - if infraSpec.DbPostgres != nil { - svc.DbPostgres = &scaffold.DatabaseReference{ - DatabaseName: infraSpec.DbPostgres.DatabaseName, - AuthType: infraSpec.DbPostgres.AuthType, + for _, usedResourceName := range userResource.Uses { + usedResource, ok := projectConfig.Resources[usedResourceName] + if !ok { + return fmt.Errorf("in azure.yaml, (%s) uses (%s), but (%s) doesn't", + userResourceName, usedResourceName, usedResourceName) } - } - if infraSpec.DbMySql != nil { - svc.DbMySql = &scaffold.DatabaseReference{ - DatabaseName: infraSpec.DbMySql.DatabaseName, - AuthType: infraSpec.DbMySql.AuthType, + switch usedResource.Type { + case ResourceTypeDbPostgres: + userSpec.DbPostgres = infraSpec.DbPostgres + case ResourceTypeDbMySQL: + userSpec.DbMySql = infraSpec.DbMySql + case ResourceTypeDbRedis: + userSpec.DbRedis = infraSpec.DbRedis + case ResourceTypeDbMongo: + userSpec.DbCosmosMongo = infraSpec.DbCosmosMongo + case ResourceTypeDbCosmos: + userSpec.DbCosmos = infraSpec.DbCosmos + case ResourceTypeMessagingServiceBus: + userSpec.AzureServiceBus = infraSpec.AzureServiceBus + case ResourceTypeMessagingEventHubs, ResourceTypeMessagingKafka: + userSpec.AzureEventHubs = infraSpec.AzureEventHubs + case ResourceTypeStorage: + userSpec.AzureStorageAccount = infraSpec.AzureStorageAccount + case ResourceTypeHostContainerApp: + err := fulfillFrontendBackend(userSpec, usedResource, infraSpec) + if err != nil { + return err + } + case ResourceTypeOpenAiModel: + userSpec.AIModels = append(userSpec.AIModels, scaffold.AIModelReference{Name: usedResource.Name}) + default: + return fmt.Errorf("resource (%s) uses (%s), but the type of (%s) is (%s), which is unsupported", + userResource.Name, usedResource.Name, usedResource.Name, usedResource.Type) } } - if infraSpec.DbRedis != nil { - svc.DbRedis = &scaffold.DatabaseReference{ - DatabaseName: "redis", - } + } + return nil +} + +func printHintsAboutUses(infraSpec *scaffold.InfraSpec, projectConfig *ProjectConfig, + console *input.Console, + context *context.Context) error { + for i := range infraSpec.Services { + userSpec := &infraSpec.Services[i] + userResourceName := userSpec.Name + userResource, ok := projectConfig.Resources[userResourceName] + if !ok { + return fmt.Errorf("service (%s) exist, but there isn't a resource with that name", + userResourceName) } - if infraSpec.DbCosmosMongo != nil { - svc.DbCosmosMongo = &scaffold.DatabaseReference{ - DatabaseName: infraSpec.DbCosmosMongo.DatabaseName, + for _, usedResourceName := range userResource.Uses { + usedResource, ok := projectConfig.Resources[usedResourceName] + if !ok { + return fmt.Errorf("in azure.yaml, (%s) uses (%s), but (%s) doesn't", + userResourceName, usedResourceName, usedResourceName) } - } - if infraSpec.DbCosmos != nil { - svc.DbCosmos = &scaffold.DatabaseCosmosAccount{ - DatabaseName: infraSpec.DbCosmos.DatabaseName, - Containers: infraSpec.DbCosmos.Containers, + (*console).Message(*context, fmt.Sprintf("CAUTION: In azure.yaml, '%s' uses '%s'. "+ + "After deployed, the 'uses' is achieved by providing these environment variables: ", + userResourceName, usedResourceName)) + switch usedResource.Type { + case ResourceTypeDbPostgres: + err := printHintsAboutUsePostgres(userSpec.DbPostgres.AuthType, console, context) + if err != nil { + return err + } + case ResourceTypeDbMySQL: + err := printHintsAboutUseMySql(userSpec.DbPostgres.AuthType, console, context) + if err != nil { + return err + } + case ResourceTypeDbRedis: + printHintsAboutUseRedis(console, context) + case ResourceTypeDbMongo: + printHintsAboutUseMongo(console, context) + case ResourceTypeDbCosmos: + printHintsAboutUseCosmos(console, context) + case ResourceTypeMessagingServiceBus: + err := printHintsAboutUseServiceBus(userSpec.AzureServiceBus.IsJms, + userSpec.AzureServiceBus.AuthType, console, context) + if err != nil { + return err + } + case ResourceTypeMessagingEventHubs, ResourceTypeMessagingKafka: + err := printHintsAboutUseEventHubs(userSpec.AzureEventHubs.UseKafka, + userSpec.AzureEventHubs.AuthType, console, context) + if err != nil { + return err + } + case ResourceTypeStorage: + err := printHintsAboutUseStorageAccount(userSpec.AzureStorageAccount.AuthType, console, context) + if err != nil { + return err + } + case ResourceTypeHostContainerApp: + printHintsAboutUseHostContainerApp(userResourceName, usedResourceName, console, context) + case ResourceTypeOpenAiModel: + printHintsAboutUseOpenAiModel(console, context) + default: + return fmt.Errorf("resource (%s) uses (%s), but the type of (%s) is (%s), "+ + "which is doen't add necessary environment variable", + userResource.Name, usedResource.Name, usedResource.Name, usedResource.Type) } + (*console).Message(*context, "Please make sure your application used the right environment variable name.\n") } } + return nil - slices.SortFunc(infraSpec.Services, func(a, b scaffold.ServiceSpec) int { - return strings.Compare(a.Name, b.Name) - }) - - return &infraSpec, nil } -func mapContainerApp(res *ResourceConfig, svcSpec *scaffold.ServiceSpec, infraSpec *scaffold.InfraSpec) error { - props := res.Props.(ContainerAppProps) +func handleContainerAppProps( + resourceConfig *ResourceConfig, serviceSpec *scaffold.ServiceSpec, infraSpec *scaffold.InfraSpec) error { + props := resourceConfig.Props.(ContainerAppProps) for _, envVar := range props.Env { if len(envVar.Value) == 0 && len(envVar.Secret) == 0 { return fmt.Errorf( "environment variable %s for host %s is invalid: both value and secret are empty", envVar.Name, - res.Name) + resourceConfig.Name) } if len(envVar.Value) > 0 && len(envVar.Secret) > 0 { return fmt.Errorf( "environment variable %s for host %s is invalid: both value and secret are set", envVar.Name, - res.Name) + resourceConfig.Name) } isSecret := len(envVar.Secret) > 0 @@ -299,56 +386,15 @@ func mapContainerApp(res *ResourceConfig, svcSpec *scaffold.ServiceSpec, infraSp // Here, DB_HOST is not a secret, but DB_SECRET is. And yet, DB_HOST will be marked as a secret. // This is a limitation of the current implementation, but it's safer to mark both as secrets above. evaluatedValue := genBicepParamsFromEnvSubst(value, isSecret, infraSpec) - svcSpec.Env[envVar.Name] = evaluatedValue + serviceSpec.Env[envVar.Name] = evaluatedValue } port := props.Port if port < 1 || port > 65535 { - return fmt.Errorf("port value %d for host %s must be between 1 and 65535", port, res.Name) - } - - svcSpec.Port = port - return nil -} - -func mapHostUses( - res *ResourceConfig, - svcSpec *scaffold.ServiceSpec, - backendMapping map[string]string, - prj *ProjectConfig) error { - for _, use := range res.Uses { - useRes, ok := prj.Resources[use] - if !ok { - return fmt.Errorf("resource %s uses %s, which does not exist", res.Name, use) - } - - switch useRes.Type { - case ResourceTypeDbMongo: - svcSpec.DbCosmosMongo = &scaffold.DatabaseReference{DatabaseName: useRes.Name} - case ResourceTypeDbPostgres: - svcSpec.DbPostgres = &scaffold.DatabaseReference{DatabaseName: useRes.Name} - case ResourceTypeDbRedis: - svcSpec.DbRedis = &scaffold.DatabaseReference{DatabaseName: useRes.Name} - case ResourceTypeHostContainerApp: - if svcSpec.Frontend == nil { - svcSpec.Frontend = &scaffold.Frontend{} - } - - svcSpec.Frontend.Backends = append(svcSpec.Frontend.Backends, - scaffold.ServiceReference{Name: use}) - backendMapping[use] = res.Name // record the backend -> frontend mapping - case ResourceTypeOpenAiModel: - svcSpec.AIModels = append(svcSpec.AIModels, scaffold.AIModelReference{Name: use}) - case ResourceTypeMessagingServiceBus: - props := useRes.Props.(ServiceBusProps) - svcSpec.AzureServiceBus = &scaffold.AzureDepServiceBus{ - Queues: props.Queues, - AuthType: props.AuthType, - IsJms: props.IsJms, - } - } + return fmt.Errorf("port value %d for host %s must be between 1 and 65535", port, resourceConfig.Name) } + serviceSpec.Port = port return nil } @@ -421,3 +467,182 @@ func genBicepParamsFromEnvSubst( return result } + +func fulfillFrontendBackend( + userSpec *scaffold.ServiceSpec, usedResource *ResourceConfig, infraSpec *scaffold.InfraSpec) error { + if userSpec.Frontend == nil { + userSpec.Frontend = &scaffold.Frontend{} + } + userSpec.Frontend.Backends = + append(userSpec.Frontend.Backends, scaffold.ServiceReference{Name: usedResource.Name}) + + usedSpec := getServiceSpecByName(infraSpec, usedResource.Name) + if usedSpec == nil { + return fmt.Errorf("'%s' uses '%s', but %s doesn't", userSpec.Name, usedResource.Name, usedResource.Name) + } + if usedSpec.Backend == nil { + usedSpec.Backend = &scaffold.Backend{} + } + usedSpec.Backend.Frontends = + append(usedSpec.Backend.Frontends, scaffold.ServiceReference{Name: userSpec.Name}) + return nil +} + +func getServiceSpecByName(infraSpec *scaffold.InfraSpec, name string) *scaffold.ServiceSpec { + for i := range infraSpec.Services { + if infraSpec.Services[i].Name == name { + return &infraSpec.Services[i] + } + } + return nil +} + +func printHintsAboutUsePostgres(authType internal.AuthType, + console *input.Console, context *context.Context) error { + (*console).Message(*context, "POSTGRES_HOST") + (*console).Message(*context, "POSTGRES_DATABASE") + (*console).Message(*context, "POSTGRES_PORT") + (*console).Message(*context, "spring.datasource.url") + (*console).Message(*context, "spring.datasource.username") + if authType == internal.AuthTypePassword { + (*console).Message(*context, "POSTGRES_URL") + (*console).Message(*context, "POSTGRES_USERNAME") + (*console).Message(*context, "POSTGRES_PASSWORD") + (*console).Message(*context, "spring.datasource.password") + } else if authType == internal.AuthTypeUserAssignedManagedIdentity { + (*console).Message(*context, "spring.datasource.azure.passwordless-enabled") + (*console).Message(*context, "CAUTION: To make sure passwordless work well in your spring boot application, ") + (*console).Message(*context, "make sure the following 2 things:") + (*console).Message(*context, "1. Add required dependency: spring-cloud-azure-starter-jdbc-postgresql.") + (*console).Message(*context, "2. Delete property 'spring.datasource.password' in your property file.") + (*console).Message(*context, "Refs: https://learn.microsoft.com/en-us/azure/service-connector/") + (*console).Message(*context, "how-to-integrate-mysql?tabs=springBoot#sample-code-1") + } else { + return fmt.Errorf("unsupported auth type for PostgreSQL. Supported types: %s, %s", + internal.GetAuthTypeDescription(internal.AuthTypePassword), + internal.GetAuthTypeDescription(internal.AuthTypeUserAssignedManagedIdentity)) + } + return nil +} + +func printHintsAboutUseMySql(authType internal.AuthType, + console *input.Console, context *context.Context) error { + (*console).Message(*context, "MYSQL_HOST") + (*console).Message(*context, "MYSQL_DATABASE") + (*console).Message(*context, "MYSQL_PORT") + (*console).Message(*context, "spring.datasource.url") + (*console).Message(*context, "spring.datasource.username") + if authType == internal.AuthTypePassword { + (*console).Message(*context, "MYSQL_URL") + (*console).Message(*context, "MYSQL_USERNAME") + (*console).Message(*context, "MYSQL_PASSWORD") + (*console).Message(*context, "spring.datasource.password") + } else if authType == internal.AuthTypeUserAssignedManagedIdentity { + (*console).Message(*context, "spring.datasource.azure.passwordless-enabled") + (*console).Message(*context, "CAUTION: To make sure passwordless work well in your spring boot application, ") + (*console).Message(*context, "Make sure the following 2 things:") + (*console).Message(*context, "1. Add required dependency: spring-cloud-azure-starter-jdbc-postgresql.") + (*console).Message(*context, "2. Delete property 'spring.datasource.password' in your property file.") + (*console).Message(*context, "Refs: https://learn.microsoft.com/en-us/azure/service-connector/how-to-integrate-postgres?tabs=springBoot#sample-code-1") + } else { + return fmt.Errorf("unsupported auth type for MySql. Supported types are: %s, %s", + internal.GetAuthTypeDescription(internal.AuthTypePassword), + internal.GetAuthTypeDescription(internal.AuthTypeUserAssignedManagedIdentity)) + } + return nil +} + +func printHintsAboutUseRedis(console *input.Console, context *context.Context) { + (*console).Message(*context, "REDIS_HOST") + (*console).Message(*context, "REDIS_PORT") + (*console).Message(*context, "REDIS_URL") + (*console).Message(*context, "REDIS_ENDPOINT") + (*console).Message(*context, "REDIS_PASSWORD") + (*console).Message(*context, "spring.data.redis.url") +} + +func printHintsAboutUseMongo(console *input.Console, context *context.Context) { + (*console).Message(*context, "MONGODB_URL") + (*console).Message(*context, "spring.data.mongodb.uri") + (*console).Message(*context, "spring.data.mongodb.database") +} + +func printHintsAboutUseCosmos(console *input.Console, context *context.Context) { + (*console).Message(*context, "spring.cloud.azure.cosmos.endpoint") + (*console).Message(*context, "spring.cloud.azure.cosmos.database") +} + +func printHintsAboutUseServiceBus(isJms bool, authType internal.AuthType, + console *input.Console, context *context.Context) error { + if !isJms { + (*console).Message(*context, "spring.cloud.azure.servicebus.namespace") + } + if authType == internal.AuthTypeUserAssignedManagedIdentity { + (*console).Message(*context, "spring.cloud.azure.servicebus.connection-string=''") + (*console).Message(*context, "spring.cloud.azure.servicebus.credential.managed-identity-enabled=true") + (*console).Message(*context, "spring.cloud.azure.servicebus.credential.client-id") + } else if authType == internal.AuthTypeConnectionString { + (*console).Message(*context, "spring.cloud.azure.servicebus.connection-string") + (*console).Message(*context, "spring.cloud.azure.servicebus.credential.managed-identity-enabled=false") + (*console).Message(*context, "spring.cloud.azure.eventhubs.credential.client-id") + } else { + return fmt.Errorf("unsupported auth type for Service Bus. Supported types are: %s, %s", + internal.GetAuthTypeDescription(internal.AuthTypeUserAssignedManagedIdentity), + internal.GetAuthTypeDescription(internal.AuthTypeConnectionString)) + } + return nil +} + +func printHintsAboutUseEventHubs(UseKafka bool, authType internal.AuthType, + console *input.Console, context *context.Context) error { + if !UseKafka { + (*console).Message(*context, "spring.cloud.azure.eventhubs.namespace") + } else { + (*console).Message(*context, "spring.cloud.stream.kafka.binder.brokers") + } + if authType == internal.AuthTypeUserAssignedManagedIdentity { + (*console).Message(*context, "spring.cloud.azure.eventhubs.connection-string=''") + (*console).Message(*context, "spring.cloud.azure.eventhubs.credential.managed-identity-enabled=true") + (*console).Message(*context, "spring.cloud.azure.eventhubs.credential.client-id") + } else if authType == internal.AuthTypeConnectionString { + (*console).Message(*context, "spring.cloud.azure.eventhubs.connection-string") + (*console).Message(*context, "spring.cloud.azure.eventhubs.credential.managed-identity-enabled=false") + (*console).Message(*context, "spring.cloud.azure.eventhubs.credential.client-id") + } else { + return fmt.Errorf("unsupported auth type for Event Hubs. Supported types: %s, %s", + internal.GetAuthTypeDescription(internal.AuthTypeUserAssignedManagedIdentity), + internal.GetAuthTypeDescription(internal.AuthTypeConnectionString)) + } + return nil +} + +func printHintsAboutUseStorageAccount(authType internal.AuthType, + console *input.Console, context *context.Context) error { + (*console).Message(*context, "spring.cloud.azure.eventhubs.processor.checkpoint-store.account-name") + if authType == internal.AuthTypeUserAssignedManagedIdentity { + (*console).Message(*context, "spring.cloud.azure.eventhubs.processor.checkpoint-store.connection-string=''") + (*console).Message(*context, "spring.cloud.azure.eventhubs.processor.checkpoint-store.credential.managed-identity-enabled=true") + (*console).Message(*context, "spring.cloud.azure.eventhubs.processor.checkpoint-store.credential.client-id") + } else if authType == internal.AuthTypeConnectionString { + (*console).Message(*context, "spring.cloud.azure.eventhubs.processor.checkpoint-store.connection-string") + (*console).Message(*context, "spring.cloud.azure.eventhubs.processor.checkpoint-store.credential.managed-identity-enabled=false") + (*console).Message(*context, "spring.cloud.azure.eventhubs.processor.checkpoint-store.credential.client-id") + } else { + return fmt.Errorf("unsupported auth type for Storage Account. Supported types: %s, %s", + internal.GetAuthTypeDescription(internal.AuthTypeUserAssignedManagedIdentity), + internal.GetAuthTypeDescription(internal.AuthTypeConnectionString)) + } + return nil +} + +func printHintsAboutUseHostContainerApp(userResourceName string, usedResourceName string, + console *input.Console, context *context.Context) { + (*console).Message(*context, fmt.Sprintf("Environemnt variables in %s:", userResourceName)) + (*console).Message(*context, fmt.Sprintf("%s_BASE_URL", strings.ToUpper(usedResourceName))) + (*console).Message(*context, fmt.Sprintf("Environemnt variables in %s:", usedResourceName)) + (*console).Message(*context, fmt.Sprintf("%s_BASE_URL", strings.ToUpper(userResourceName))) +} + +func printHintsAboutUseOpenAiModel(console *input.Console, context *context.Context) { + (*console).Message(*context, "AZURE_OPENAI_ENDPOINT") +} diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index 2516bf648c7..452f9ebceb3 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -908,7 +908,6 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { value: 'premium' } {{- end}} - {{- if .Frontend}} {{- range $i, $e := .Frontend.Backends}} { @@ -917,6 +916,14 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { } {{- end}} {{- end}} + {{- if .Backend}} + {{- range $i, $e := .Backend.Frontends}} + { + name: '{{upper .Name}}_BASE_URL' + value: 'https://{{.Name}}.${containerAppsEnvironment.outputs.defaultDomain}' + } + {{- end}} + {{- end}} {{- if ne .Port 0}} { name: 'PORT' From feb1cf5aa22c7af3728b9d9fec38ee1c326dc12f Mon Sep 17 00:00:00 2001 From: Xiaolu Dai <31124698+saragluna@users.noreply.github.com> Date: Wed, 20 Nov 2024 14:37:57 +0800 Subject: [PATCH 73/92] support eventhubs when using azd composability (#33) --- cli/azd/internal/repository/app_init.go | 7 ++++++- cli/azd/pkg/project/resources.go | 22 ++++++++++++++++++++++ cli/azd/pkg/project/scaffold_gen.go | 6 ++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index 446fbc23d92..4231d42ccd8 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -627,16 +627,21 @@ func (i *Initializer) prjConfigFromDetect( }, } case appdetect.AzureDepEventHubs: + azureDepEventHubs := azureDep.(appdetect.AzureDepEventHubs) config.Resources["eventhubs"] = &project.ResourceConfig{ Type: project.ResourceTypeMessagingEventHubs, Props: project.EventHubsProps{ - EventHubNames: spec.AzureEventHubs.EventHubNames, + EventHubNames: azureDepEventHubs.Names, AuthType: authType, }, } case appdetect.AzureDepStorageAccount: config.Resources["storage"] = &project.ResourceConfig{ Type: project.ResourceTypeStorage, + Props: project.StorageProps{ + Containers: azureDep.(appdetect.AzureDepStorageAccount).ContainerNames, + AuthType: authType, + }, } } } diff --git a/cli/azd/pkg/project/resources.go b/cli/azd/pkg/project/resources.go index 2ef62e5875e..0dd2a50e45d 100644 --- a/cli/azd/pkg/project/resources.go +++ b/cli/azd/pkg/project/resources.go @@ -141,6 +141,11 @@ func (r *ResourceConfig) MarshalYAML() (interface{}, error) { if err != nil { return nil, err } + case ResourceTypeStorage: + err := marshalRawProps(raw.Props.(StorageProps)) + if err != nil { + return nil, err + } } return raw, nil @@ -216,6 +221,18 @@ func (r *ResourceConfig) UnmarshalYAML(value *yaml.Node) error { return err } raw.Props = eh + case ResourceTypeMessagingKafka: + kp := KafkaProps{} + if err := unmarshalProps(&kp); err != nil { + return err + } + raw.Props = kp + case ResourceTypeStorage: + sp := StorageProps{} + if err := unmarshalProps(&sp); err != nil { + return err + } + raw.Props = sp } *r = ResourceConfig(raw) @@ -283,3 +300,8 @@ type KafkaProps struct { Topics []string `yaml:"topics,omitempty"` AuthType internal.AuthType `yaml:"authType,omitempty"` } + +type StorageProps struct { + Containers []string `yaml:"containers,omitempty"` + AuthType internal.AuthType `yaml:"authType,omitempty"` +} diff --git a/cli/azd/pkg/project/scaffold_gen.go b/cli/azd/pkg/project/scaffold_gen.go index efa2c6e8dbe..627c39187c9 100644 --- a/cli/azd/pkg/project/scaffold_gen.go +++ b/cli/azd/pkg/project/scaffold_gen.go @@ -191,6 +191,12 @@ func infraSpec(projectConfig *ProjectConfig, AuthType: props.AuthType, UseKafka: true, } + case ResourceTypeStorage: + props := resource.Props.(StorageProps) + infraSpec.AzureStorageAccount = &scaffold.AzureDepStorageAccount{ + ContainerNames: props.Containers, + AuthType: props.AuthType, + } case ResourceTypeHostContainerApp: serviceSpec := scaffold.ServiceSpec{ Name: resource.Name, From c3e9cafd933fda7d1ef444f7a4bb548b2ec7b8df Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Wed, 20 Nov 2024 14:59:11 +0800 Subject: [PATCH 74/92] Add the missed codes for "storage" (#36) --- cli/azd/internal/repository/app_init.go | 8 ++++++++ cli/azd/pkg/project/resources.go | 2 ++ 2 files changed, 10 insertions(+) diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index 4231d42ccd8..d70e7d0829d 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -518,6 +518,14 @@ func (i *Initializer) prjConfigFromDetect( }, } } + case appdetect.AzureDepStorageAccount: + config.Resources["storage"] = &project.ResourceConfig{ + Type: project.ResourceTypeStorage, + Props: project.StorageProps{ + Containers: spec.AzureStorageAccount.ContainerNames, + AuthType: spec.AzureStorageAccount.AuthType, + }, + } } } diff --git a/cli/azd/pkg/project/resources.go b/cli/azd/pkg/project/resources.go index 0dd2a50e45d..31a2a79db8b 100644 --- a/cli/azd/pkg/project/resources.go +++ b/cli/azd/pkg/project/resources.go @@ -58,6 +58,8 @@ func (r ResourceType) String() string { return "Event Hubs" case ResourceTypeMessagingKafka: return "Kafka" + case ResourceTypeStorage: + return "Storage Account" } return "" From bf02b9381fb8f2cf83996ed3d63eb56da7d97b0a Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Wed, 20 Nov 2024 16:59:26 +0800 Subject: [PATCH 75/92] Add values for all environment variables (#35) --- cli/azd/pkg/project/scaffold_gen.go | 92 ++++++++++++++--------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/cli/azd/pkg/project/scaffold_gen.go b/cli/azd/pkg/project/scaffold_gen.go index 627c39187c9..160fcadafa6 100644 --- a/cli/azd/pkg/project/scaffold_gen.go +++ b/cli/azd/pkg/project/scaffold_gen.go @@ -505,18 +505,18 @@ func getServiceSpecByName(infraSpec *scaffold.InfraSpec, name string) *scaffold. func printHintsAboutUsePostgres(authType internal.AuthType, console *input.Console, context *context.Context) error { - (*console).Message(*context, "POSTGRES_HOST") - (*console).Message(*context, "POSTGRES_DATABASE") - (*console).Message(*context, "POSTGRES_PORT") - (*console).Message(*context, "spring.datasource.url") - (*console).Message(*context, "spring.datasource.username") + (*console).Message(*context, "POSTGRES_HOST=xxx") + (*console).Message(*context, "POSTGRES_DATABASE=xxx") + (*console).Message(*context, "POSTGRES_PORT=xxx") + (*console).Message(*context, "spring.datasource.url=xxx") + (*console).Message(*context, "spring.datasource.username=xxx") if authType == internal.AuthTypePassword { - (*console).Message(*context, "POSTGRES_URL") - (*console).Message(*context, "POSTGRES_USERNAME") - (*console).Message(*context, "POSTGRES_PASSWORD") - (*console).Message(*context, "spring.datasource.password") + (*console).Message(*context, "POSTGRES_URL=xxx") + (*console).Message(*context, "POSTGRES_USERNAME=xxx") + (*console).Message(*context, "POSTGRES_PASSWORD=xxx") + (*console).Message(*context, "spring.datasource.password=xxx") } else if authType == internal.AuthTypeUserAssignedManagedIdentity { - (*console).Message(*context, "spring.datasource.azure.passwordless-enabled") + (*console).Message(*context, "spring.datasource.azure.passwordless-enabled=true") (*console).Message(*context, "CAUTION: To make sure passwordless work well in your spring boot application, ") (*console).Message(*context, "make sure the following 2 things:") (*console).Message(*context, "1. Add required dependency: spring-cloud-azure-starter-jdbc-postgresql.") @@ -533,18 +533,18 @@ func printHintsAboutUsePostgres(authType internal.AuthType, func printHintsAboutUseMySql(authType internal.AuthType, console *input.Console, context *context.Context) error { - (*console).Message(*context, "MYSQL_HOST") - (*console).Message(*context, "MYSQL_DATABASE") - (*console).Message(*context, "MYSQL_PORT") - (*console).Message(*context, "spring.datasource.url") - (*console).Message(*context, "spring.datasource.username") + (*console).Message(*context, "MYSQL_HOST=xxx") + (*console).Message(*context, "MYSQL_DATABASE=xxx") + (*console).Message(*context, "MYSQL_PORT=xxx") + (*console).Message(*context, "spring.datasource.url=xxx") + (*console).Message(*context, "spring.datasource.username=xxx") if authType == internal.AuthTypePassword { - (*console).Message(*context, "MYSQL_URL") - (*console).Message(*context, "MYSQL_USERNAME") - (*console).Message(*context, "MYSQL_PASSWORD") - (*console).Message(*context, "spring.datasource.password") + (*console).Message(*context, "MYSQL_URL=xxx") + (*console).Message(*context, "MYSQL_USERNAME=xxx") + (*console).Message(*context, "MYSQL_PASSWORD=xxx") + (*console).Message(*context, "spring.datasource.password=xxx") } else if authType == internal.AuthTypeUserAssignedManagedIdentity { - (*console).Message(*context, "spring.datasource.azure.passwordless-enabled") + (*console).Message(*context, "spring.datasource.azure.passwordless-enabled=true") (*console).Message(*context, "CAUTION: To make sure passwordless work well in your spring boot application, ") (*console).Message(*context, "Make sure the following 2 things:") (*console).Message(*context, "1. Add required dependency: spring-cloud-azure-starter-jdbc-postgresql.") @@ -559,38 +559,38 @@ func printHintsAboutUseMySql(authType internal.AuthType, } func printHintsAboutUseRedis(console *input.Console, context *context.Context) { - (*console).Message(*context, "REDIS_HOST") - (*console).Message(*context, "REDIS_PORT") - (*console).Message(*context, "REDIS_URL") - (*console).Message(*context, "REDIS_ENDPOINT") - (*console).Message(*context, "REDIS_PASSWORD") - (*console).Message(*context, "spring.data.redis.url") + (*console).Message(*context, "REDIS_HOST=xxx") + (*console).Message(*context, "REDIS_PORT=xxx") + (*console).Message(*context, "REDIS_URL=xxx") + (*console).Message(*context, "REDIS_ENDPOINT=xxx") + (*console).Message(*context, "REDIS_PASSWORD=xxx") + (*console).Message(*context, "spring.data.redis.url=xxx") } func printHintsAboutUseMongo(console *input.Console, context *context.Context) { - (*console).Message(*context, "MONGODB_URL") - (*console).Message(*context, "spring.data.mongodb.uri") - (*console).Message(*context, "spring.data.mongodb.database") + (*console).Message(*context, "MONGODB_URL=xxx") + (*console).Message(*context, "spring.data.mongodb.uri=xxx") + (*console).Message(*context, "spring.data.mongodb.database=xxx") } func printHintsAboutUseCosmos(console *input.Console, context *context.Context) { - (*console).Message(*context, "spring.cloud.azure.cosmos.endpoint") - (*console).Message(*context, "spring.cloud.azure.cosmos.database") + (*console).Message(*context, "spring.cloud.azure.cosmos.endpoint=xxx") + (*console).Message(*context, "spring.cloud.azure.cosmos.database=xxx") } func printHintsAboutUseServiceBus(isJms bool, authType internal.AuthType, console *input.Console, context *context.Context) error { if !isJms { - (*console).Message(*context, "spring.cloud.azure.servicebus.namespace") + (*console).Message(*context, "spring.cloud.azure.servicebus.namespace=xxx") } if authType == internal.AuthTypeUserAssignedManagedIdentity { (*console).Message(*context, "spring.cloud.azure.servicebus.connection-string=''") (*console).Message(*context, "spring.cloud.azure.servicebus.credential.managed-identity-enabled=true") - (*console).Message(*context, "spring.cloud.azure.servicebus.credential.client-id") + (*console).Message(*context, "spring.cloud.azure.servicebus.credential.client-id=xxx") } else if authType == internal.AuthTypeConnectionString { - (*console).Message(*context, "spring.cloud.azure.servicebus.connection-string") + (*console).Message(*context, "spring.cloud.azure.servicebus.connection-string=xxx") (*console).Message(*context, "spring.cloud.azure.servicebus.credential.managed-identity-enabled=false") - (*console).Message(*context, "spring.cloud.azure.eventhubs.credential.client-id") + (*console).Message(*context, "spring.cloud.azure.eventhubs.credential.client-id=xxx") } else { return fmt.Errorf("unsupported auth type for Service Bus. Supported types are: %s, %s", internal.GetAuthTypeDescription(internal.AuthTypeUserAssignedManagedIdentity), @@ -602,18 +602,18 @@ func printHintsAboutUseServiceBus(isJms bool, authType internal.AuthType, func printHintsAboutUseEventHubs(UseKafka bool, authType internal.AuthType, console *input.Console, context *context.Context) error { if !UseKafka { - (*console).Message(*context, "spring.cloud.azure.eventhubs.namespace") + (*console).Message(*context, "spring.cloud.azure.eventhubs.namespace=xxx") } else { - (*console).Message(*context, "spring.cloud.stream.kafka.binder.brokers") + (*console).Message(*context, "spring.cloud.stream.kafka.binder.brokers=xxx") } if authType == internal.AuthTypeUserAssignedManagedIdentity { (*console).Message(*context, "spring.cloud.azure.eventhubs.connection-string=''") (*console).Message(*context, "spring.cloud.azure.eventhubs.credential.managed-identity-enabled=true") - (*console).Message(*context, "spring.cloud.azure.eventhubs.credential.client-id") + (*console).Message(*context, "spring.cloud.azure.eventhubs.credential.client-id=xxx") } else if authType == internal.AuthTypeConnectionString { - (*console).Message(*context, "spring.cloud.azure.eventhubs.connection-string") + (*console).Message(*context, "spring.cloud.azure.eventhubs.connection-string=xxx") (*console).Message(*context, "spring.cloud.azure.eventhubs.credential.managed-identity-enabled=false") - (*console).Message(*context, "spring.cloud.azure.eventhubs.credential.client-id") + (*console).Message(*context, "spring.cloud.azure.eventhubs.credential.client-id=xxx") } else { return fmt.Errorf("unsupported auth type for Event Hubs. Supported types: %s, %s", internal.GetAuthTypeDescription(internal.AuthTypeUserAssignedManagedIdentity), @@ -624,15 +624,15 @@ func printHintsAboutUseEventHubs(UseKafka bool, authType internal.AuthType, func printHintsAboutUseStorageAccount(authType internal.AuthType, console *input.Console, context *context.Context) error { - (*console).Message(*context, "spring.cloud.azure.eventhubs.processor.checkpoint-store.account-name") + (*console).Message(*context, "spring.cloud.azure.eventhubs.processor.checkpoint-store.account-name=xxx") if authType == internal.AuthTypeUserAssignedManagedIdentity { (*console).Message(*context, "spring.cloud.azure.eventhubs.processor.checkpoint-store.connection-string=''") (*console).Message(*context, "spring.cloud.azure.eventhubs.processor.checkpoint-store.credential.managed-identity-enabled=true") - (*console).Message(*context, "spring.cloud.azure.eventhubs.processor.checkpoint-store.credential.client-id") + (*console).Message(*context, "spring.cloud.azure.eventhubs.processor.checkpoint-store.credential.client-id=xxx") } else if authType == internal.AuthTypeConnectionString { - (*console).Message(*context, "spring.cloud.azure.eventhubs.processor.checkpoint-store.connection-string") + (*console).Message(*context, "spring.cloud.azure.eventhubs.processor.checkpoint-store.connection-string=xxx") (*console).Message(*context, "spring.cloud.azure.eventhubs.processor.checkpoint-store.credential.managed-identity-enabled=false") - (*console).Message(*context, "spring.cloud.azure.eventhubs.processor.checkpoint-store.credential.client-id") + (*console).Message(*context, "spring.cloud.azure.eventhubs.processor.checkpoint-store.credential.client-id=xxx") } else { return fmt.Errorf("unsupported auth type for Storage Account. Supported types: %s, %s", internal.GetAuthTypeDescription(internal.AuthTypeUserAssignedManagedIdentity), @@ -644,9 +644,9 @@ func printHintsAboutUseStorageAccount(authType internal.AuthType, func printHintsAboutUseHostContainerApp(userResourceName string, usedResourceName string, console *input.Console, context *context.Context) { (*console).Message(*context, fmt.Sprintf("Environemnt variables in %s:", userResourceName)) - (*console).Message(*context, fmt.Sprintf("%s_BASE_URL", strings.ToUpper(usedResourceName))) + (*console).Message(*context, fmt.Sprintf("%s_BASE_URL=xxx", strings.ToUpper(usedResourceName))) (*console).Message(*context, fmt.Sprintf("Environemnt variables in %s:", usedResourceName)) - (*console).Message(*context, fmt.Sprintf("%s_BASE_URL", strings.ToUpper(userResourceName))) + (*console).Message(*context, fmt.Sprintf("%s_BASE_URL=xxx", strings.ToUpper(userResourceName))) } func printHintsAboutUseOpenAiModel(console *input.Console, context *context.Context) { From 8ca2115115827fafa8eed9e722d1eddbcae531e1 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Wed, 20 Nov 2024 17:04:33 +0800 Subject: [PATCH 76/92] Improve the prompt message for database (#37) --- cli/azd/internal/cmd/add/add_configure.go | 4 +++- cli/azd/internal/repository/app_init.go | 4 ++-- cli/azd/internal/repository/infra_confirm.go | 8 +++++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/cli/azd/internal/cmd/add/add_configure.go b/cli/azd/internal/cmd/add/add_configure.go index fac15c5a0a8..f3e5ad18c0a 100644 --- a/cli/azd/internal/cmd/add/add_configure.go +++ b/cli/azd/internal/cmd/add/add_configure.go @@ -56,7 +56,9 @@ func fillDatabaseName( for { dbName, err := console.Prompt(ctx, input.ConsoleOptions{ - Message: fmt.Sprintf("Input the name of the app database (%s)", r.Type.String()), + Message: fmt.Sprintf("Input the databaseName for %s "+ + "(Not databaseServerName. This url can explain the difference: "+ + "'jdbc:mysql://databaseServerName:3306/databaseName'):", r.Type.String()), Help: "Hint: App database name\n\n" + "Name of the database that the app connects to. " + "This database will be created after running azd provision or azd up.", diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index d70e7d0829d..156ecee8fb0 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -560,7 +560,7 @@ func (i *Initializer) prjConfigFromDetect( if database == appdetect.DbPostgres || database == appdetect.DbMySql { var err error authType, err = chooseAuthTypeByPrompt( - databaseName, + database.Display(), []internal.AuthType{internal.AuthTypeUserAssignedManagedIdentity, internal.AuthTypePassword}, ctx, i.console) @@ -742,7 +742,7 @@ func chooseAuthTypeByPrompt( options = append(options, internal.GetAuthTypeDescription(option)) } selection, err := console.Select(ctx, input.ConsoleOptions{ - Message: "Choose auth type for '" + name + "'?", + Message: "Choose auth type for " + name + ":", Options: options, }) if err != nil { diff --git a/cli/azd/internal/repository/infra_confirm.go b/cli/azd/internal/repository/infra_confirm.go index 6874fd232c1..66ba9afdc79 100644 --- a/cli/azd/internal/repository/infra_confirm.go +++ b/cli/azd/internal/repository/infra_confirm.go @@ -52,7 +52,7 @@ func (i *Initializer) infraSpecFromDetect( continue } authType, err := chooseAuthTypeByPrompt( - dbName, + database.Display(), []internal.AuthType{internal.AuthTypeUserAssignedManagedIdentity, internal.AuthTypePassword}, ctx, i.console) @@ -70,7 +70,7 @@ func (i *Initializer) infraSpecFromDetect( continue } authType, err := chooseAuthTypeByPrompt( - dbName, + database.Display(), []internal.AuthType{internal.AuthTypeUserAssignedManagedIdentity, internal.AuthTypePassword}, ctx, i.console) @@ -219,7 +219,9 @@ func promptPortNumber(console input.Console, ctx context.Context, promptMessage func promptDbName(console input.Console, ctx context.Context, database appdetect.DatabaseDep) (string, error) { for { dbName, err := console.Prompt(ctx, input.ConsoleOptions{ - Message: fmt.Sprintf("Input the name of the app database (%s)", database.Display()), + Message: fmt.Sprintf("Input the databaseName for %s "+ + "(Not databaseServerName. This url can explain the difference: "+ + "'jdbc:mysql://databaseServerName:3306/databaseName'):", database.Display()), Help: "Hint: App database name\n\n" + "Name of the database that the app connects to. " + "This database will be created after running azd provision or azd up." + From 27f2cc5776a65ef4e50375f98faf6da23d21cda0 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Thu, 21 Nov 2024 16:50:18 +0800 Subject: [PATCH 77/92] 1. Add log about detecting rule. 2. Avoid duplicated queue name for service bus. (#38) --- cli/azd/internal/appdetect/java.go | 49 ++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/cli/azd/internal/appdetect/java.go b/cli/azd/internal/appdetect/java.go index 24ff12b7810..88d879c5e4d 100644 --- a/cli/azd/internal/appdetect/java.go +++ b/cli/azd/internal/appdetect/java.go @@ -165,28 +165,42 @@ func detectDependencies(currentRoot *mavenProject, mavenProject *mavenProject, p for _, dep := range mavenProject.Dependencies { if dep.GroupId == "com.mysql" && dep.ArtifactId == "mysql-connector-j" { databaseDepMap[DbMySql] = struct{}{} + log.Printf("Detected 'db.mysql' because found this dependency in project: " + + "com.mysql:mysql-connector-j.") } if dep.GroupId == "org.postgresql" && dep.ArtifactId == "postgresql" { databaseDepMap[DbPostgres] = struct{}{} + log.Printf("Detected 'db.postgres' because found this dependency in project: " + + "org.postgresql:postgresql") } if dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-starter-data-cosmos" { databaseDepMap[DbCosmos] = struct{}{} + log.Printf("Detected 'db.cosmos' because found this dependency in project: " + + "com.azure.spring:spring-cloud-azure-starter-data-cosmos.") } if dep.GroupId == "org.springframework.boot" && dep.ArtifactId == "spring-boot-starter-data-redis" { databaseDepMap[DbRedis] = struct{}{} + log.Printf("Detected 'db.redis' because found this dependency in project: " + + "org.springframework.boot:spring-boot-starter-data-redis.") } if dep.GroupId == "org.springframework.boot" && dep.ArtifactId == "spring-boot-starter-data-redis-reactive" { databaseDepMap[DbRedis] = struct{}{} + log.Printf("Detected 'db.redis' because found this dependency in project: " + + "org.springframework.boot:spring-boot-starter-data-redis-reactive.") } if dep.GroupId == "org.springframework.boot" && dep.ArtifactId == "spring-boot-starter-data-mongodb" { databaseDepMap[DbMongo] = struct{}{} + log.Printf("Detected 'db.mongo' because found this dependency in project: " + + "org.springframework.boot:spring-boot-starter-data-mongodb.") } if dep.GroupId == "org.springframework.boot" && dep.ArtifactId == "spring-boot-starter-data-mongodb-reactive" { databaseDepMap[DbMongo] = struct{}{} + log.Printf("Detected 'db.mongo' because found this dependency in project: " + + "org.springframework.boot:spring-boot-starter-data-mongodb-reactive.") } // we need to figure out multiple projects are using the same service bus @@ -194,19 +208,28 @@ func detectDependencies(currentRoot *mavenProject, mavenProject *mavenProject, p project.AzureDeps = append(project.AzureDeps, AzureDepServiceBus{ IsJms: true, }) + log.Printf("Detected 'messaging.servicebus' because found this dependency in project: " + + "com.azure.spring:spring-cloud-azure-starter-servicebus-jms.") } if dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-stream-binder-servicebus" { bindingDestinations := findBindingDestinations(applicationProperties) destinations := make([]string, 0, len(bindingDestinations)) - for bindingName, destination := range bindingDestinations { - destinations = append(destinations, destination) - log.Printf("Service Bus queue [%s] found for binding [%s]", destination, bindingName) + for _, destination := range bindingDestinations { + if !contains(destinations, destination) { + destinations = append(destinations, destination) + } } project.AzureDeps = append(project.AzureDeps, AzureDepServiceBus{ Queues: destinations, IsJms: false, }) + log.Printf("Detected 'messaging.servicebus' because found this dependency in project: " + + "com.azure.spring:spring-cloud-azure-stream-binder-servicebus") + for bindingName, destination := range bindingDestinations { + log.Printf(" Detected Service Bus queue [%s] for binding [%s] by analyzing property file.", + destination, bindingName) + } } if dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-stream-binder-eventhubs" { @@ -219,28 +242,36 @@ func detectDependencies(currentRoot *mavenProject, mavenProject *mavenProject, p } if !contains(destinations, destination) { destinations = append(destinations, destination) - log.Printf("Event Hubs [%s] found for binding [%s]", destination, bindingName) } } project.AzureDeps = append(project.AzureDeps, AzureDepEventHubs{ Names: destinations, UseKafka: false, }) + log.Printf("Detected 'messaging.eventhubs' because found this dependency in project: " + + "com.azure.spring:spring-cloud-azure-stream-binder-eventhubs.") + for bindingName, destination := range bindingDestinations { + log.Printf(" Detected Event Hub [%s] for binding [%s] by analyzing property file.", + destination, bindingName) + } if containsInBinding { project.AzureDeps = append(project.AzureDeps, AzureDepStorageAccount{ ContainerNames: []string{ applicationProperties["spring.cloud.azure.eventhubs.processor.checkpoint-store.container-name"]}, }) + log.Printf("Detected 'storage' because found this property in property file: " + + "spring.cloud.azure.eventhubs.processor.checkpoint-store.container-name.") + log.Printf(" Storage account container name: [%s].", + applicationProperties["spring.cloud.azure.eventhubs.processor.checkpoint-store.container-name"]) } } if dep.GroupId == "org.springframework.cloud" && dep.ArtifactId == "spring-cloud-starter-stream-kafka" { bindingDestinations := findBindingDestinations(applicationProperties) var destinations []string - for bindingName, destination := range bindingDestinations { + for _, destination := range bindingDestinations { if !contains(destinations, destination) { destinations = append(destinations, destination) - log.Printf("Kafka Topic [%s] found for binding [%s]", destination, bindingName) } } project.AzureDeps = append(project.AzureDeps, AzureDepEventHubs{ @@ -248,6 +279,12 @@ func detectDependencies(currentRoot *mavenProject, mavenProject *mavenProject, p UseKafka: true, SpringBootVersion: springBootVersion, }) + log.Printf("Detected 'messaging.eventhubs' because found this dependency in project: " + + "org.springframework.cloud:spring-cloud-starter-stream-kafka.") + for bindingName, destination := range bindingDestinations { + log.Printf(" Detected Kafka Topic [%s] for binding [%s] by analyzing property file.", + destination, bindingName) + } } if dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-starter" { From 1dc2e3eb09adb7c4fb1fe940313e42b31803474d Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Thu, 21 Nov 2024 21:34:27 +0800 Subject: [PATCH 78/92] Users can continue to use azd when java analyzer fails (#39) --- cli/azd/internal/appdetect/java.go | 8 ++++++-- cli/azd/internal/auth_type.go | 3 ++- cli/azd/internal/repository/app_init.go | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/cli/azd/internal/appdetect/java.go b/cli/azd/internal/appdetect/java.go index 88d879c5e4d..43891fc2fd9 100644 --- a/cli/azd/internal/appdetect/java.go +++ b/cli/azd/internal/appdetect/java.go @@ -33,7 +33,9 @@ func (jd *javaDetector) DetectProject(ctx context.Context, path string, entries pomFile := filepath.Join(path, entry.Name()) project, err := readMavenProject(pomFile) if err != nil { - return nil, fmt.Errorf("error reading pom.xml: %w", err) + log.Printf("Please edit azure.yaml manually to satisfy your requirement. azd can not help you "+ + "to that by detect your java project because error happened when reading pom.xml: %s. ", err) + return nil, nil } if len(project.Modules) > 0 { @@ -58,7 +60,9 @@ func (jd *javaDetector) DetectProject(ctx context.Context, path string, entries DetectionRule: "Inferred by presence of: pom.xml", }) if err != nil { - return nil, fmt.Errorf("detecting dependencies: %w", err) + log.Printf("Please edit azure.yaml manually to satisfy your requirement. azd can not help you "+ + "to that by detect your java project because error happened when detecting dependencies: %s", err) + return nil, nil } tracing.SetUsageAttributes(fields.AppInitJavaDetect.String("finish")) diff --git a/cli/azd/internal/auth_type.go b/cli/azd/internal/auth_type.go index 4902d7bdf8f..72fcc331580 100644 --- a/cli/azd/internal/auth_type.go +++ b/cli/azd/internal/auth_type.go @@ -23,6 +23,7 @@ func GetAuthTypeDescription(authType AuthType) string { return "Connection string" case AuthTypeUserAssignedManagedIdentity: return "User assigned managed identity" + default: + return "Unspecified" } - panic("unknown auth type") } diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index 156ecee8fb0..1b3d904abf9 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -875,6 +875,6 @@ func promptSpringBootVersion(console input.Console, ctx context.Context) (string case 1: return "3.x", nil default: - panic("unhandled selection") + return appdetect.UnknownSpringBootVersion, nil } } From 4c890419abd4ee06b187ab740a3985cbc3b1371d Mon Sep 17 00:00:00 2001 From: Xiaolu Dai <31124698+saragluna@users.noreply.github.com> Date: Thu, 21 Nov 2024 22:17:46 +0800 Subject: [PATCH 79/92] fix code to support servicebus jms with managed identity in azd composability (#40) --- .../scaffold/templates/resources.bicept | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index 452f9ebceb3..b99bc3c8c7c 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -870,7 +870,7 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { value: serviceBusNamespace.outputs.name } {{- end}} - {{- if (and .AzureServiceBus (eq .AzureServiceBus.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) }} + {{- if (and (and .AzureServiceBus (eq .AzureServiceBus.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) (not .AzureServiceBus.IsJms)) }} { name: 'spring.cloud.azure.servicebus.connection-string' value: '' @@ -908,6 +908,30 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { value: 'premium' } {{- end}} + + {{- if (and (and .AzureServiceBus (eq .AzureServiceBus.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) .AzureServiceBus.IsJms) }} + { + name: 'spring.jms.servicebus.passwordless-enabled' + value: 'true' + } + { + name: 'spring.jms.servicebus.namespace' + value: serviceBusNamespace.outputs.name + } + { + name: 'spring.jms.servicebus.credential.managed-identity-enabled' + value: 'true' + } + { + name: 'spring.jms.servicebus.credential.client-id' + value: {{bicepName .Name}}Identity.outputs.clientId + } + { + name: 'spring.jms.servicebus.pricing-tier' + value: 'premium' + } + {{- end}} + {{- if .Frontend}} {{- range $i, $e := .Frontend.Backends}} { From 06ca3c574b9b9b73265641a0c3f0ec2ac7d74791 Mon Sep 17 00:00:00 2001 From: HaoZhang Date: Fri, 22 Nov 2024 09:09:44 +0800 Subject: [PATCH 80/92] Update azure yaml schema (#32) --- cli/azd/internal/repository/app_init.go | 3 + cli/azd/pkg/project/resources.go | 2 +- schemas/alpha/azure.yaml.json | 232 ++++++++++++++++++++---- 3 files changed, 201 insertions(+), 36 deletions(-) diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index 1b3d904abf9..fd6cff13cea 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -450,6 +450,9 @@ func (i *Initializer) prjConfigFromDetect( config.Resources["mongo"] = &project.ResourceConfig{ Type: project.ResourceTypeDbMongo, Name: spec.DbCosmosMongo.DatabaseName, + Props: project.MongoDBProps{ + DatabaseName: spec.DbCosmosMongo.DatabaseName, + }, } case appdetect.DbPostgres: config.Resources["postgres"] = &project.ResourceConfig{ diff --git a/cli/azd/pkg/project/resources.go b/cli/azd/pkg/project/resources.go index 31a2a79db8b..87a140827fb 100644 --- a/cli/azd/pkg/project/resources.go +++ b/cli/azd/pkg/project/resources.go @@ -294,7 +294,7 @@ type ServiceBusProps struct { } type EventHubsProps struct { - EventHubNames []string `yaml:"EventHubNames,omitempty"` + EventHubNames []string `yaml:"eventHubNames,omitempty"` AuthType internal.AuthType `yaml:"authType,omitempty"` } diff --git a/schemas/alpha/azure.yaml.json b/schemas/alpha/azure.yaml.json index e375b3612e9..8c7667adfc1 100644 --- a/schemas/alpha/azure.yaml.json +++ b/schemas/alpha/azure.yaml.json @@ -368,10 +368,15 @@ "title": "Type of resource", "description": "The type of resource to be created. (Example: db.postgres)", "enum": [ - "db.postgres", "db.mysql", + "db.postgres", "db.redis", "db.mongo", + "db.cosmos", + "messaging.servicebus", + "messaging.eventhubs", + "messaging.kafka", + "storage", "ai.openai.model", "host.containerapp" ] @@ -383,43 +388,20 @@ "type": "string" }, "uniqueItems": true - }, - "authType": { - "type": "string", - "title": "The authentication type of Azure resource used for the application", - "description": "The application uses this kind of authentication to connect to the Azure resource.", - "enum": [ - "managedIdentity", - "usernamePassword" - ] - }, - "databaseName": { - "type": "string", - "title": "The name of Azure resource that the application depends on", - "description": "The Azure resource that will be accessed during application runtime." } }, "allOf": [ { "if": { "properties": { "type": { "const": "host.containerapp" }}}, "then": { "$ref": "#/definitions/containerAppResource" } }, { "if": { "properties": { "type": { "const": "ai.openai.model" }}}, "then": { "$ref": "#/definitions/aiModelResource" } }, - { "if": { "properties": { "type": { "const": "db.postgres" }}}, "then": { "$ref": "#/definitions/resource"} }, + { "if": { "properties": { "type": { "const": "db.mysql" }}}, "then": { "$ref": "#/definitions/mySqlDbResource"} }, + { "if": { "properties": { "type": { "const": "db.postgres" }}}, "then": { "$ref": "#/definitions/postgreSqlDbResource"} }, { "if": { "properties": { "type": { "const": "db.redis" }}}, "then": { "$ref": "#/definitions/resource"} }, - { "if": { "properties": { "type": { "const": "db.mongo" }}}, "then": { "$ref": "#/definitions/resource"} },, - { - "if": { - "properties": { - "type": { - "const": "db.mysql" - } - } - }, - "then": { - "required": [ - "authType", - "databaseName" - ] - } - } + { "if": { "properties": { "type": { "const": "db.mongo" }}}, "then": { "$ref": "#/definitions/mongoDbResource"} }, + { "if": { "properties": { "type": { "const": "db.cosmos" }}}, "then": { "$ref": "#/definitions/cosmosDbResource"} }, + { "if": { "properties": { "type": { "const": "messaging.servicebus" }}}, "then": { "$ref": "#/definitions/serviceBusResource"} }, + { "if": { "properties": { "type": { "const": "messaging.eventhubs" }}}, "then": { "$ref": "#/definitions/eventHubsResource"} }, + { "if": { "properties": { "type": { "const": "messaging.kafka" }}}, "then": { "$ref": "#/definitions/kafkaResource"} }, + { "if": { "properties": { "type": { "const": "storage" }}}, "then": { "$ref": "#/definitions/resource"} } ] } }, @@ -1238,11 +1220,10 @@ "type": { "type": "string", "title": "Type of resource", - "description": "The type of resource to be created. (Example: db.postgres)", + "description": "The type of resource to be created. (Example: db.redis)", "enum": [ - "db.postgres", "db.redis", - "db.mongo", + "storage", "host.containerapp", "ai.openai.model" ] @@ -1331,6 +1312,187 @@ } } } + }, + "mySqlDbResource": { + "type": "object", + "description": "A deployed, ready-to-use Azure Database for MySQL flexible server.", + "additionalProperties": false, + "properties": { + "type": true, + "uses": true, + "authType": { + "type": "string", + "title": "Authentication Type", + "description": "The type of authentication used for Azure MySQL database.", + "enum": [ + "USER_ASSIGNED_MANAGED_IDENTITY", + "PASSWORD" + ] + }, + "databaseName": { + "type": "string", + "title": "The Azure MySQL Database Name", + "description": "The name of Azure MySQL database." + } + } + }, + "postgreSqlDbResource": { + "type": "object", + "description": "A deployed, ready-to-use Azure Database for PostgreSQL flexible server.", + "additionalProperties": false, + "properties": { + "type": true, + "uses": true, + "authType": { + "type": "string", + "title": "Authentication Type", + "description": "The type of authentication used for Azure PostgreSQL database.", + "enum": [ + "USER_ASSIGNED_MANAGED_IDENTITY", + "PASSWORD" + ] + }, + "databaseName": { + "type": "string", + "title": "The Azure PostgreSQL Database Name", + "description": "The name of Azure PostgreSQL database." + } + } + }, + "mongoDbResource": { + "type": "object", + "description": "A deployed, ready-to-use Azure CosmosDB API for MongoDB.", + "additionalProperties": false, + "properties": { + "type": true, + "uses": true, + "databaseName": { + "type": "string", + "title": "The Azure MongoDB Name", + "description": "The name of Azure CosmosDB API for MongoDB." + } + } + }, + "cosmosDbResource": { + "type": "object", + "description": "A deployed, ready-to-use Azure Cosmos DB for NoSQL.", + "additionalProperties": false, + "properties": { + "type": true, + "uses": true, + "databaseName": { + "type": "string", + "title": "The Azure Cosmos DB Name", + "description": "The name of Azure Cosmos DB." + }, + "containers": { + "type": "array", + "title": "Azure Cosmos DB Containers", + "description": "A list of containers in the Azure CosmosDB.", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "containerName": { + "type": "string", + "title": "Container Name", + "description": "The name of the container." + }, + "partitionKeyPaths": { + "type": "array", + "title": "Partition Key Paths", + "description": "A list of partition key paths for the container.", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "serviceBusResource": { + "type": "object", + "description": "A deployed, ready-to-use Azure Service Bus.", + "additionalProperties": false, + "properties": { + "type": true, + "uses": true, + "queues": { + "type": "array", + "title": "Service Bus Queues", + "description": "A list of Service Bus queues.", + "items": { + "type": "string" + } + }, + "isJms": { + "type": "boolean", + "title": "Is JMS", + "description": "Indicates if JMS is enabled for the Service Bus." + }, + "authType": { + "type": "string", + "title": "Authentication Type", + "description": "The type of authentication used for the Service Bus.", + "enum": [ + "USER_ASSIGNED_MANAGED_IDENTITY", + "CONNECTION_STRING" + ] + } + } + }, + "eventHubsResource": { + "type": "object", + "description": "A deployed, ready-to-use Azure Event Hubs.", + "additionalProperties": false, + "properties": { + "type": true, + "uses": true, + "eventHubNames": { + "type": "array", + "title": "Event Hub Names", + "description": "A list of Event Hub names.", + "items": { + "type": "string" + } + }, + "authType": { + "type": "string", + "title": "Authentication Type", + "description": "The type of authentication used for Event Hubs.", + "enum": [ + "USER_ASSIGNED_MANAGED_IDENTITY", + "CONNECTION_STRING" + ] + } + } + }, + "kafkaResource": { + "type": "object", + "description": "A deployed, ready-to-use Azure Event Hubs for Apache Kafka.", + "additionalProperties": false, + "properties": { + "type": true, + "uses": true, + "topics": { + "type": "array", + "title": "Topics", + "description": "A list of Kafka topics.", + "items": { + "type": "string" + } + }, + "authType": { + "type": "string", + "title": "Authentication Type", + "description": "The type of authentication used for Kafka.", + "enum": [ + "USER_ASSIGNED_MANAGED_IDENTITY", + "CONNECTION_STRING" + ] + } + } } } } \ No newline at end of file From c8351bf83af12f6894d25801a9af97ba8a5aa07d Mon Sep 17 00:00:00 2001 From: HaoZhang Date: Fri, 22 Nov 2024 13:21:37 +0800 Subject: [PATCH 81/92] Fix code to support kafka when azd compose enabled (#42) --- cli/azd/internal/repository/app_init.go | 34 ++++++++++++++++++------- cli/azd/pkg/project/resources.go | 5 ++-- cli/azd/pkg/project/scaffold_gen.go | 16 ++++++++---- schemas/alpha/azure.yaml.json | 5 ++++ 4 files changed, 44 insertions(+), 16 deletions(-) diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index fd6cff13cea..d3c250cf546 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -508,8 +508,9 @@ func (i *Initializer) prjConfigFromDetect( config.Resources["kafka"] = &project.ResourceConfig{ Type: project.ResourceTypeMessagingKafka, Props: project.KafkaProps{ - Topics: spec.AzureEventHubs.EventHubNames, - AuthType: spec.AzureEventHubs.AuthType, + Topics: spec.AzureEventHubs.EventHubNames, + AuthType: spec.AzureEventHubs.AuthType, + SpringBootVersion: spec.AzureEventHubs.SpringBootVersion, }, } } else { @@ -639,12 +640,23 @@ func (i *Initializer) prjConfigFromDetect( } case appdetect.AzureDepEventHubs: azureDepEventHubs := azureDep.(appdetect.AzureDepEventHubs) - config.Resources["eventhubs"] = &project.ResourceConfig{ - Type: project.ResourceTypeMessagingEventHubs, - Props: project.EventHubsProps{ - EventHubNames: azureDepEventHubs.Names, - AuthType: authType, - }, + if azureDepEventHubs.UseKafka { + config.Resources["kafka"] = &project.ResourceConfig{ + Type: project.ResourceTypeMessagingKafka, + Props: project.KafkaProps{ + Topics: azureDepEventHubs.Names, + AuthType: authType, + SpringBootVersion: azureDepEventHubs.SpringBootVersion, + }, + } + } else { + config.Resources["eventhubs"] = &project.ResourceConfig{ + Type: project.ResourceTypeMessagingEventHubs, + Props: project.EventHubsProps{ + EventHubNames: azureDepEventHubs.Names, + AuthType: authType, + }, + } } case appdetect.AzureDepStorageAccount: config.Resources["storage"] = &project.ResourceConfig{ @@ -690,7 +702,11 @@ func (i *Initializer) prjConfigFromDetect( case appdetect.AzureDepServiceBus: resSpec.Uses = append(resSpec.Uses, "servicebus") case appdetect.AzureDepEventHubs: - resSpec.Uses = append(resSpec.Uses, "eventhubs") + if azureDep.(appdetect.AzureDepEventHubs).UseKafka { + resSpec.Uses = append(resSpec.Uses, "kafka") + } else { + resSpec.Uses = append(resSpec.Uses, "eventhubs") + } case appdetect.AzureDepStorageAccount: resSpec.Uses = append(resSpec.Uses, "storage") } diff --git a/cli/azd/pkg/project/resources.go b/cli/azd/pkg/project/resources.go index 87a140827fb..1eb336d6b1b 100644 --- a/cli/azd/pkg/project/resources.go +++ b/cli/azd/pkg/project/resources.go @@ -299,8 +299,9 @@ type EventHubsProps struct { } type KafkaProps struct { - Topics []string `yaml:"topics,omitempty"` - AuthType internal.AuthType `yaml:"authType,omitempty"` + Topics []string `yaml:"topics,omitempty"` + AuthType internal.AuthType `yaml:"authType,omitempty"` + SpringBootVersion string `yaml:"springBootVersion,omitempty"` } type StorageProps struct { diff --git a/cli/azd/pkg/project/scaffold_gen.go b/cli/azd/pkg/project/scaffold_gen.go index 160fcadafa6..53f24f14a58 100644 --- a/cli/azd/pkg/project/scaffold_gen.go +++ b/cli/azd/pkg/project/scaffold_gen.go @@ -187,9 +187,10 @@ func infraSpec(projectConfig *ProjectConfig, case ResourceTypeMessagingKafka: props := resource.Props.(KafkaProps) infraSpec.AzureEventHubs = &scaffold.AzureDepEventHubs{ - EventHubNames: props.Topics, - AuthType: props.AuthType, - UseKafka: true, + EventHubNames: props.Topics, + AuthType: props.AuthType, + UseKafka: true, + SpringBootVersion: props.SpringBootVersion, } case ResourceTypeStorage: props := resource.Props.(StorageProps) @@ -337,7 +338,7 @@ func printHintsAboutUses(infraSpec *scaffold.InfraSpec, projectConfig *ProjectCo } case ResourceTypeMessagingEventHubs, ResourceTypeMessagingKafka: err := printHintsAboutUseEventHubs(userSpec.AzureEventHubs.UseKafka, - userSpec.AzureEventHubs.AuthType, console, context) + userSpec.AzureEventHubs.AuthType, userSpec.AzureEventHubs.SpringBootVersion, console, context) if err != nil { return err } @@ -599,12 +600,17 @@ func printHintsAboutUseServiceBus(isJms bool, authType internal.AuthType, return nil } -func printHintsAboutUseEventHubs(UseKafka bool, authType internal.AuthType, +func printHintsAboutUseEventHubs(UseKafka bool, authType internal.AuthType, springBootVersion string, console *input.Console, context *context.Context) error { if !UseKafka { (*console).Message(*context, "spring.cloud.azure.eventhubs.namespace=xxx") } else { (*console).Message(*context, "spring.cloud.stream.kafka.binder.brokers=xxx") + if strings.HasPrefix(springBootVersion, "2.") { + (*console).Message(*context, "spring.cloud.stream.binders.kafka.environment.spring.main.sources=com.azure.spring.cloud.autoconfigure.eventhubs.kafka.AzureEventHubsKafkaAutoConfiguration") + } else if strings.HasPrefix(springBootVersion, "3.") { + (*console).Message(*context, "spring.cloud.stream.binders.kafka.environment.spring.main.sources=com.azure.spring.cloud.autoconfigure.implementation.eventhubs.kafka.AzureEventHubsKafkaAutoConfiguration") + } } if authType == internal.AuthTypeUserAssignedManagedIdentity { (*console).Message(*context, "spring.cloud.azure.eventhubs.connection-string=''") diff --git a/schemas/alpha/azure.yaml.json b/schemas/alpha/azure.yaml.json index 8c7667adfc1..5a30fb64c46 100644 --- a/schemas/alpha/azure.yaml.json +++ b/schemas/alpha/azure.yaml.json @@ -1491,6 +1491,11 @@ "USER_ASSIGNED_MANAGED_IDENTITY", "CONNECTION_STRING" ] + }, + "springBootVersion": { + "type": "string", + "title": "Spring Boot Version", + "description": "The Spring Boot version used in the project." } } } From 419b6992f871f5214fba6b17d154d0c76e33fc82 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 22 Nov 2024 13:38:11 +0800 Subject: [PATCH 82/92] Refactor code (#41) --- cli/azd/internal/appdetect/appdetect_test.go | 18 - cli/azd/internal/appdetect/java.go | 345 +---------------- cli/azd/internal/appdetect/spring_boot.go | 363 ++++++++++++++++++ .../appdetect/spring_boot_property.go | 124 ++++++ .../appdetect/spring_boot_property_test.go | 70 ++++ .../{java_test.go => spring_boot_test.go} | 46 --- 6 files changed, 558 insertions(+), 408 deletions(-) create mode 100644 cli/azd/internal/appdetect/spring_boot.go create mode 100644 cli/azd/internal/appdetect/spring_boot_property.go create mode 100644 cli/azd/internal/appdetect/spring_boot_property_test.go rename cli/azd/internal/appdetect/{java_test.go => spring_boot_test.go} (80%) diff --git a/cli/azd/internal/appdetect/appdetect_test.go b/cli/azd/internal/appdetect/appdetect_test.go index a51222cad2d..cba1dc94866 100644 --- a/cli/azd/internal/appdetect/appdetect_test.go +++ b/cli/azd/internal/appdetect/appdetect_test.go @@ -287,24 +287,6 @@ func TestDetectNested(t *testing.T) { }) } -func TestAnalyzeJavaSpringProject(t *testing.T) { - var properties = readProperties(filepath.Join("testdata", "java-spring", "project-one")) - require.Equal(t, "", properties["not.exist"]) - require.Equal(t, "jdbc:h2:mem:testdb", properties["spring.datasource.url"]) - - properties = readProperties(filepath.Join("testdata", "java-spring", "project-two")) - require.Equal(t, "", properties["not.exist"]) - require.Equal(t, "jdbc:h2:mem:testdb", properties["spring.datasource.url"]) - - properties = readProperties(filepath.Join("testdata", "java-spring", "project-three")) - require.Equal(t, "", properties["not.exist"]) - require.Equal(t, "HTML", properties["spring.thymeleaf.mode"]) - - properties = readProperties(filepath.Join("testdata", "java-spring", "project-four")) - require.Equal(t, "", properties["not.exist"]) - require.Equal(t, "mysql", properties["database"]) -} - func copyTestDataDir(glob string, dst string) error { root := "testdata" return fs.WalkDir(testDataFs, root, func(name string, d fs.DirEntry, err error) error { diff --git a/cli/azd/internal/appdetect/java.go b/cli/azd/internal/appdetect/java.go index 43891fc2fd9..26866f46241 100644 --- a/cli/azd/internal/appdetect/java.go +++ b/cli/azd/internal/appdetect/java.go @@ -1,20 +1,14 @@ package appdetect import ( - "bufio" "context" "encoding/xml" "fmt" "github.com/azure/azure-dev/cli/azd/internal/tracing" "github.com/azure/azure-dev/cli/azd/internal/tracing/fields" - "github.com/azure/azure-dev/cli/azd/pkg/osutil" - "github.com/braydonk/yaml" "io/fs" - "log" - "maps" "os" "path/filepath" - "slices" "strings" ) @@ -143,343 +137,6 @@ func readMavenProject(filePath string) (*mavenProject, error) { } func detectDependencies(currentRoot *mavenProject, mavenProject *mavenProject, project *Project) (*Project, error) { - // how can we tell it's a Spring Boot project? - // 1. It has a parent with a groupId of org.springframework.boot and an artifactId of spring-boot-starter-parent - // 2. It has a dependency with a groupId of org.springframework.boot and an artifactId that starts with - // spring-boot-starter - isSpringBoot := false - if mavenProject.Parent.GroupId == "org.springframework.boot" && - mavenProject.Parent.ArtifactId == "spring-boot-starter-parent" { - isSpringBoot = true - } - for _, dep := range mavenProject.Dependencies { - if dep.GroupId == "org.springframework.boot" && strings.HasPrefix(dep.ArtifactId, "spring-boot-starter") { - isSpringBoot = true - break - } - } - applicationProperties := make(map[string]string) - var springBootVersion string - if isSpringBoot { - applicationProperties = readProperties(project.Path) - springBootVersion = detectSpringBootVersion(currentRoot, mavenProject) - } - - databaseDepMap := map[DatabaseDep]struct{}{} - for _, dep := range mavenProject.Dependencies { - if dep.GroupId == "com.mysql" && dep.ArtifactId == "mysql-connector-j" { - databaseDepMap[DbMySql] = struct{}{} - log.Printf("Detected 'db.mysql' because found this dependency in project: " + - "com.mysql:mysql-connector-j.") - } - - if dep.GroupId == "org.postgresql" && dep.ArtifactId == "postgresql" { - databaseDepMap[DbPostgres] = struct{}{} - log.Printf("Detected 'db.postgres' because found this dependency in project: " + - "org.postgresql:postgresql") - } - - if dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-starter-data-cosmos" { - databaseDepMap[DbCosmos] = struct{}{} - log.Printf("Detected 'db.cosmos' because found this dependency in project: " + - "com.azure.spring:spring-cloud-azure-starter-data-cosmos.") - } - - if dep.GroupId == "org.springframework.boot" && dep.ArtifactId == "spring-boot-starter-data-redis" { - databaseDepMap[DbRedis] = struct{}{} - log.Printf("Detected 'db.redis' because found this dependency in project: " + - "org.springframework.boot:spring-boot-starter-data-redis.") - } - if dep.GroupId == "org.springframework.boot" && dep.ArtifactId == "spring-boot-starter-data-redis-reactive" { - databaseDepMap[DbRedis] = struct{}{} - log.Printf("Detected 'db.redis' because found this dependency in project: " + - "org.springframework.boot:spring-boot-starter-data-redis-reactive.") - } - - if dep.GroupId == "org.springframework.boot" && dep.ArtifactId == "spring-boot-starter-data-mongodb" { - databaseDepMap[DbMongo] = struct{}{} - log.Printf("Detected 'db.mongo' because found this dependency in project: " + - "org.springframework.boot:spring-boot-starter-data-mongodb.") - } - if dep.GroupId == "org.springframework.boot" && dep.ArtifactId == "spring-boot-starter-data-mongodb-reactive" { - databaseDepMap[DbMongo] = struct{}{} - log.Printf("Detected 'db.mongo' because found this dependency in project: " + - "org.springframework.boot:spring-boot-starter-data-mongodb-reactive.") - } - - // we need to figure out multiple projects are using the same service bus - if dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-starter-servicebus-jms" { - project.AzureDeps = append(project.AzureDeps, AzureDepServiceBus{ - IsJms: true, - }) - log.Printf("Detected 'messaging.servicebus' because found this dependency in project: " + - "com.azure.spring:spring-cloud-azure-starter-servicebus-jms.") - } - - if dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-stream-binder-servicebus" { - bindingDestinations := findBindingDestinations(applicationProperties) - destinations := make([]string, 0, len(bindingDestinations)) - for _, destination := range bindingDestinations { - if !contains(destinations, destination) { - destinations = append(destinations, destination) - } - } - project.AzureDeps = append(project.AzureDeps, AzureDepServiceBus{ - Queues: destinations, - IsJms: false, - }) - log.Printf("Detected 'messaging.servicebus' because found this dependency in project: " + - "com.azure.spring:spring-cloud-azure-stream-binder-servicebus") - for bindingName, destination := range bindingDestinations { - log.Printf(" Detected Service Bus queue [%s] for binding [%s] by analyzing property file.", - destination, bindingName) - } - } - - if dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-stream-binder-eventhubs" { - bindingDestinations := findBindingDestinations(applicationProperties) - var destinations []string - containsInBinding := false - for bindingName, destination := range bindingDestinations { - if strings.Contains(bindingName, "-in-") { // Example: consume-in-0 - containsInBinding = true - } - if !contains(destinations, destination) { - destinations = append(destinations, destination) - } - } - project.AzureDeps = append(project.AzureDeps, AzureDepEventHubs{ - Names: destinations, - UseKafka: false, - }) - log.Printf("Detected 'messaging.eventhubs' because found this dependency in project: " + - "com.azure.spring:spring-cloud-azure-stream-binder-eventhubs.") - for bindingName, destination := range bindingDestinations { - log.Printf(" Detected Event Hub [%s] for binding [%s] by analyzing property file.", - destination, bindingName) - } - if containsInBinding { - project.AzureDeps = append(project.AzureDeps, AzureDepStorageAccount{ - ContainerNames: []string{ - applicationProperties["spring.cloud.azure.eventhubs.processor.checkpoint-store.container-name"]}, - }) - log.Printf("Detected 'storage' because found this property in property file: " + - "spring.cloud.azure.eventhubs.processor.checkpoint-store.container-name.") - log.Printf(" Storage account container name: [%s].", - applicationProperties["spring.cloud.azure.eventhubs.processor.checkpoint-store.container-name"]) - } - } - - if dep.GroupId == "org.springframework.cloud" && dep.ArtifactId == "spring-cloud-starter-stream-kafka" { - bindingDestinations := findBindingDestinations(applicationProperties) - var destinations []string - for _, destination := range bindingDestinations { - if !contains(destinations, destination) { - destinations = append(destinations, destination) - } - } - project.AzureDeps = append(project.AzureDeps, AzureDepEventHubs{ - Names: destinations, - UseKafka: true, - SpringBootVersion: springBootVersion, - }) - log.Printf("Detected 'messaging.eventhubs' because found this dependency in project: " + - "org.springframework.cloud:spring-cloud-starter-stream-kafka.") - for bindingName, destination := range bindingDestinations { - log.Printf(" Detected Kafka Topic [%s] for binding [%s] by analyzing property file.", - destination, bindingName) - } - } - - if dep.GroupId == "com.azure.spring" && dep.ArtifactId == "spring-cloud-azure-starter" { - project.AzureDeps = append(project.AzureDeps, SpringCloudAzureDep{}) - } - } - - if len(databaseDepMap) > 0 { - project.DatabaseDeps = slices.SortedFunc(maps.Keys(databaseDepMap), - func(a, b DatabaseDep) int { - return strings.Compare(string(a), string(b)) - }) - } - + detectAzureDependenciesByAnalyzingSpringBootProject(currentRoot, mavenProject, project) return project, nil } - -func readProperties(projectPath string) map[string]string { - // todo: do we need to consider the bootstrap.properties - result := make(map[string]string) - readPropertiesInPropertiesFile(filepath.Join(projectPath, "/src/main/resources/application.properties"), result) - readPropertiesInYamlFile(filepath.Join(projectPath, "/src/main/resources/application.yml"), result) - readPropertiesInYamlFile(filepath.Join(projectPath, "/src/main/resources/application.yaml"), result) - profile, profileSet := result["spring.profiles.active"] - if profileSet { - readPropertiesInPropertiesFile( - filepath.Join(projectPath, "/src/main/resources/application-"+profile+".properties"), result) - readPropertiesInYamlFile(filepath.Join(projectPath, "/src/main/resources/application-"+profile+".yml"), result) - readPropertiesInYamlFile(filepath.Join(projectPath, "/src/main/resources/application-"+profile+".yaml"), result) - } - return result -} - -func readPropertiesInYamlFile(yamlFilePath string, result map[string]string) { - if !osutil.FileExists(yamlFilePath) { - return - } - data, err := os.ReadFile(yamlFilePath) - if err != nil { - log.Fatalf("error reading YAML file: %v", err) - return - } - - // Parse the YAML into a yaml.Node - var root yaml.Node - err = yaml.Unmarshal(data, &root) - if err != nil { - log.Fatalf("error unmarshalling YAML: %v", err) - return - } - - parseYAML("", &root, result) -} - -// Recursively parse the YAML and build dot-separated keys into a map -func parseYAML(prefix string, node *yaml.Node, result map[string]string) { - switch node.Kind { - case yaml.DocumentNode: - // Process each document's content - for _, contentNode := range node.Content { - parseYAML(prefix, contentNode, result) - } - case yaml.MappingNode: - // Process key-value pairs in a map - for i := 0; i < len(node.Content); i += 2 { - keyNode := node.Content[i] - valueNode := node.Content[i+1] - - // Ensure the key is a scalar - if keyNode.Kind != yaml.ScalarNode { - continue - } - - keyStr := keyNode.Value - newPrefix := keyStr - if prefix != "" { - newPrefix = prefix + "." + keyStr - } - parseYAML(newPrefix, valueNode, result) - } - case yaml.SequenceNode: - // Process items in a sequence (list) - for i, item := range node.Content { - newPrefix := fmt.Sprintf("%s[%d]", prefix, i) - parseYAML(newPrefix, item, result) - } - case yaml.ScalarNode: - // If it's a scalar value, add it to the result map - result[prefix] = getEnvironmentVariablePlaceholderHandledValue(node.Value) - default: - // Handle other node types if necessary - } -} - -func readPropertiesInPropertiesFile(propertiesFilePath string, result map[string]string) { - if !osutil.FileExists(propertiesFilePath) { - return - } - file, err := os.Open(propertiesFilePath) - if err != nil { - log.Fatalf("error opening properties file: %v", err) - return - } - defer file.Close() - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - if strings.TrimSpace(line) == "" || strings.HasPrefix(line, "#") { - continue - } - parts := strings.SplitN(line, "=", 2) - if len(parts) == 2 { - key := strings.TrimSpace(parts[0]) - value := getEnvironmentVariablePlaceholderHandledValue(parts[1]) - result[key] = value - } - } -} - -func getEnvironmentVariablePlaceholderHandledValue(rawValue string) string { - trimmedRawValue := strings.TrimSpace(rawValue) - if strings.HasPrefix(trimmedRawValue, "${") && strings.HasSuffix(trimmedRawValue, "}") { - envVar := trimmedRawValue[2 : len(trimmedRawValue)-1] - return os.Getenv(envVar) - } - return trimmedRawValue -} - -// Function to find all properties that match the pattern `spring.cloud.stream.bindings..destination` -func findBindingDestinations(properties map[string]string) map[string]string { - result := make(map[string]string) - - // Iterate through the properties map and look for matching keys - for key, value := range properties { - // Check if the key matches the pattern `spring.cloud.stream.bindings..destination` - if strings.HasPrefix(key, "spring.cloud.stream.bindings.") && strings.HasSuffix(key, ".destination") { - // Extract the binding name - bindingName := key[len("spring.cloud.stream.bindings.") : len(key)-len(".destination")] - // Store the binding name and destination value - result[bindingName] = fmt.Sprintf("%v", value) - } - } - - return result -} - -func contains(array []string, str string) bool { - for _, v := range array { - if v == str { - return true - } - } - return false -} - -func parseProperties(properties Properties) map[string]string { - result := make(map[string]string) - for _, entry := range properties.Entries { - result[entry.XMLName.Local] = entry.Value - } - return result -} - -func detectSpringBootVersion(currentRoot *mavenProject, mavenProject *mavenProject) string { - // mavenProject prioritize than rootProject - if mavenProject != nil { - return detectSpringBootVersionFromProject(mavenProject) - } else if currentRoot != nil { - return detectSpringBootVersionFromProject(currentRoot) - } - return UnknownSpringBootVersion -} - -func detectSpringBootVersionFromProject(project *mavenProject) string { - if project.Parent.ArtifactId == "spring-boot-starter-parent" { - return depVersion(project.Parent.Version, project.Properties) - } else { - for _, dep := range project.DependencyManagement.Dependencies { - if dep.ArtifactId == "spring-boot-dependencies" { - return depVersion(dep.Version, project.Properties) - } - } - } - return UnknownSpringBootVersion -} - -func depVersion(version string, properties Properties) string { - if strings.HasPrefix(version, "${") { - return parseProperties(properties)[version[2:len(version)-1]] - } else { - return version - } -} diff --git a/cli/azd/internal/appdetect/spring_boot.go b/cli/azd/internal/appdetect/spring_boot.go new file mode 100644 index 00000000000..b0d33a5164f --- /dev/null +++ b/cli/azd/internal/appdetect/spring_boot.go @@ -0,0 +1,363 @@ +package appdetect + +import ( + "fmt" + "log" + "maps" + "slices" + "strings" +) + +type SpringBootProject struct { + springBootVersion string + applicationProperties map[string]string + parentProject *mavenProject + mavenProject *mavenProject +} + +type DatabaseDependencyRule struct { + databaseDep DatabaseDep + mavenDependencies []MavenDependency +} + +type MavenDependency struct { + groupId string + artifactId string +} + +var databaseDependencyRules = []DatabaseDependencyRule{ + { + databaseDep: DbPostgres, + mavenDependencies: []MavenDependency{ + { + groupId: "org.postgresql", + artifactId: "postgresql", + }, + }, + }, + { + databaseDep: DbMySql, + mavenDependencies: []MavenDependency{ + { + groupId: "com.mysql", + artifactId: "mysql-connector-j", + }, + }, + }, + { + databaseDep: DbRedis, + mavenDependencies: []MavenDependency{ + { + groupId: "org.springframework.boot", + artifactId: "spring-boot-starter-data-redis", + }, + { + groupId: "org.springframework.boot", + artifactId: "spring-boot-starter-data-redis-reactive", + }, + }, + }, + { + databaseDep: DbMongo, + mavenDependencies: []MavenDependency{ + { + groupId: "org.springframework.boot", + artifactId: "spring-boot-starter-data-mongodb", + }, + { + groupId: "org.springframework.boot", + artifactId: "spring-boot-starter-data-mongodb-reactive", + }, + }, + }, + { + databaseDep: DbCosmos, + mavenDependencies: []MavenDependency{ + { + groupId: "com.azure.spring", + artifactId: "spring-cloud-azure-starter-data-cosmos", + }, + }, + }, +} + +func detectAzureDependenciesByAnalyzingSpringBootProject( + parentProject *mavenProject, mavenProject *mavenProject, azdProject *Project) { + if !isSpringBootApplication(mavenProject) { + log.Printf("Skip analyzing spring boot project. path = %s.", mavenProject.path) + return + } + var springBootProject = SpringBootProject{ + springBootVersion: detectSpringBootVersion(parentProject, mavenProject), + applicationProperties: readProperties(azdProject.Path), + parentProject: parentProject, + mavenProject: mavenProject, + } + detectDatabases(azdProject, &springBootProject) + detectServiceBus(azdProject, &springBootProject) + detectEventHubs(azdProject, &springBootProject) + detectStorageAccount(azdProject, &springBootProject) + detectSpringCloudAzure(azdProject, &springBootProject) +} + +func detectDatabases(azdProject *Project, springBootProject *SpringBootProject) { + databaseDepMap := map[DatabaseDep]struct{}{} + for _, rule := range databaseDependencyRules { + for _, targetDependency := range rule.mavenDependencies { + var targetGroupId = targetDependency.groupId + var targetArtifactId = targetDependency.artifactId + if hasDependency(springBootProject, targetGroupId, targetArtifactId) { + databaseDepMap[rule.databaseDep] = struct{}{} + logServiceAddedAccordingToMavenDependency(rule.databaseDep.Display(), + targetGroupId, targetArtifactId) + break + } + } + } + if len(databaseDepMap) > 0 { + azdProject.DatabaseDeps = slices.SortedFunc(maps.Keys(databaseDepMap), + func(a, b DatabaseDep) int { + return strings.Compare(string(a), string(b)) + }) + } +} + +func detectServiceBus(azdProject *Project, springBootProject *SpringBootProject) { + // we need to figure out multiple projects are using the same service bus + detectServiceBusAccordingToJMSMavenDependency(azdProject, springBootProject) + detectServiceBusAccordingToSpringCloudStreamBinderMavenDependency(azdProject, springBootProject) +} + +func detectServiceBusAccordingToJMSMavenDependency(azdProject *Project, springBootProject *SpringBootProject) { + var targetGroupId = "com.azure.spring" + var targetArtifactId = "spring-cloud-azure-starter-servicebus-jms" + if hasDependency(springBootProject, targetGroupId, targetArtifactId) { + newDependency := AzureDepServiceBus{ + IsJms: true, + } + azdProject.AzureDeps = append(azdProject.AzureDeps, newDependency) + logServiceAddedAccordingToMavenDependency(newDependency.ResourceDisplay(), targetGroupId, targetArtifactId) + } +} + +func detectServiceBusAccordingToSpringCloudStreamBinderMavenDependency( + azdProject *Project, springBootProject *SpringBootProject) { + var targetGroupId = "com.azure.spring" + var targetArtifactId = "spring-cloud-azure-stream-binder-servicebus" + if hasDependency(springBootProject, targetGroupId, targetArtifactId) { + bindingDestinations := getBindingDestinationMap(springBootProject.applicationProperties) + var destinations = distinctValues(bindingDestinations) + newDep := AzureDepServiceBus{ + Queues: destinations, + IsJms: false, + } + azdProject.AzureDeps = append(azdProject.AzureDeps, newDep) + logServiceAddedAccordingToMavenDependency(newDep.ResourceDisplay(), targetGroupId, targetArtifactId) + for bindingName, destination := range bindingDestinations { + log.Printf(" Detected Service Bus queue [%s] for binding [%s] by analyzing property file.", + destination, bindingName) + } + } +} + +func detectEventHubs(azdProject *Project, springBootProject *SpringBootProject) { + // we need to figure out multiple projects are using the same event hub + detectEventHubsAccordingToSpringCloudStreamBinderMavenDependency(azdProject, springBootProject) + detectEventHubsAccordingToSpringCloudStreamKafkaMavenDependency(azdProject, springBootProject) +} + +func detectEventHubsAccordingToSpringCloudStreamBinderMavenDependency( + azdProject *Project, springBootProject *SpringBootProject) { + var targetGroupId = "com.azure.spring" + var targetArtifactId = "spring-cloud-azure-stream-binder-eventhubs" + if hasDependency(springBootProject, targetGroupId, targetArtifactId) { + bindingDestinations := getBindingDestinationMap(springBootProject.applicationProperties) + var destinations = distinctValues(bindingDestinations) + newDep := AzureDepEventHubs{ + Names: destinations, + UseKafka: false, + } + azdProject.AzureDeps = append(azdProject.AzureDeps, newDep) + logServiceAddedAccordingToMavenDependency(newDep.ResourceDisplay(), targetGroupId, targetArtifactId) + for bindingName, destination := range bindingDestinations { + log.Printf(" Detected Event Hub [%s] for binding [%s] by analyzing property file.", + destination, bindingName) + } + } +} + +func detectEventHubsAccordingToSpringCloudStreamKafkaMavenDependency( + azdProject *Project, springBootProject *SpringBootProject) { + var targetGroupId = "com.azure.spring" + var targetArtifactId = "spring-cloud-starter-stream-kafka" + if hasDependency(springBootProject, targetGroupId, targetArtifactId) { + bindingDestinations := getBindingDestinationMap(springBootProject.applicationProperties) + var destinations = distinctValues(bindingDestinations) + newDep := AzureDepEventHubs{ + Names: destinations, + UseKafka: true, + SpringBootVersion: springBootProject.springBootVersion, + } + azdProject.AzureDeps = append(azdProject.AzureDeps, newDep) + logServiceAddedAccordingToMavenDependency(newDep.ResourceDisplay(), targetGroupId, targetArtifactId) + for bindingName, destination := range bindingDestinations { + log.Printf(" Detected Kafka Topic [%s] for binding [%s] by analyzing property file.", + destination, bindingName) + } + } +} + +func detectStorageAccount(azdProject *Project, springBootProject *SpringBootProject) { + detectStorageAccountAccordingToSpringCloudStreamBinderMavenDependencyAndProperty(azdProject, springBootProject) +} + +func detectStorageAccountAccordingToSpringCloudStreamBinderMavenDependencyAndProperty( + azdProject *Project, springBootProject *SpringBootProject) { + var targetGroupId = "com.azure.spring" + var targetArtifactId = "spring-cloud-azure-stream-binder-eventhubs" + var targetPropertyName = "spring.cloud.azure.eventhubs.processor.checkpoint-store.container-name" + if hasDependency(springBootProject, targetGroupId, targetArtifactId) { + bindingDestinations := getBindingDestinationMap(springBootProject.applicationProperties) + containsInBindingName := "" + for bindingName := range bindingDestinations { + if strings.Contains(bindingName, "-in-") { // Example: consume-in-0 + containsInBindingName = bindingName + break + } + } + if containsInBindingName != "" { + targetPropertyValue := springBootProject.applicationProperties[targetPropertyName] + newDep := AzureDepStorageAccount{ + ContainerNames: []string{targetPropertyValue}, + } + azdProject.AzureDeps = append(azdProject.AzureDeps, newDep) + logServiceAddedAccordingToMavenDependencyAndExtraCondition(newDep.ResourceDisplay(), targetGroupId, + targetArtifactId, "binding name ["+containsInBindingName+"] contains '-in-'") + log.Printf(" Detected Storage Account container name: [%s] by analyzing property file.", + targetPropertyValue) + } + } +} + +func detectSpringCloudAzure(azdProject *Project, springBootProject *SpringBootProject) { + var targetGroupId = "com.azure.spring" + var targetArtifactId = "spring-cloud-azure-starter" + if hasDependency(springBootProject, targetGroupId, targetArtifactId) { + newDep := SpringCloudAzureDep{} + azdProject.AzureDeps = append(azdProject.AzureDeps, newDep) + logServiceAddedAccordingToMavenDependency(newDep.ResourceDisplay(), targetGroupId, targetArtifactId) + } +} + +func logServiceAddedAccordingToMavenDependency(resourceName, groupId string, artifactId string) { + logServiceAddedAccordingToMavenDependencyAndExtraCondition(resourceName, groupId, artifactId, "") +} + +func logServiceAddedAccordingToMavenDependencyAndExtraCondition( + resourceName, groupId string, artifactId string, extraCondition string) { + insertedString := "" + extraCondition = strings.TrimSpace(extraCondition) + if extraCondition != "" { + insertedString = " and " + extraCondition + } + log.Printf("Detected '%s' because found dependency '%s:%s' in pom.xml file%s.", + resourceName, groupId, artifactId, insertedString) +} + +func detectSpringBootVersion(currentRoot *mavenProject, mavenProject *mavenProject) string { + // mavenProject prioritize than rootProject + if mavenProject != nil { + return detectSpringBootVersionFromProject(mavenProject) + } else if currentRoot != nil { + return detectSpringBootVersionFromProject(currentRoot) + } + return UnknownSpringBootVersion +} + +func detectSpringBootVersionFromProject(project *mavenProject) string { + if project.Parent.ArtifactId == "spring-boot-starter-parent" { + return depVersion(project.Parent.Version, project.Properties) + } else { + for _, dep := range project.DependencyManagement.Dependencies { + if dep.ArtifactId == "spring-boot-dependencies" { + return depVersion(dep.Version, project.Properties) + } + } + } + return UnknownSpringBootVersion +} + +func isSpringBootApplication(mavenProject *mavenProject) bool { + // how can we tell it's a Spring Boot project? + // 1. It has a parent with a groupId of org.springframework.boot and an artifactId of spring-boot-starter-parent + // 2. It has a dependency with a groupId of org.springframework.boot and an artifactId that starts with + // spring-boot-starter + if mavenProject.Parent.GroupId == "org.springframework.boot" && + mavenProject.Parent.ArtifactId == "spring-boot-starter-parent" { + return true + } + for _, dep := range mavenProject.Dependencies { + if dep.GroupId == "org.springframework.boot" && + strings.HasPrefix(dep.ArtifactId, "spring-boot-starter") { + return true + } + } + return false +} + +func depVersion(version string, properties Properties) string { + if strings.HasPrefix(version, "${") { + return parseProperties(properties)[version[2:len(version)-1]] + } else { + return version + } +} + +func parseProperties(properties Properties) map[string]string { + result := make(map[string]string) + for _, entry := range properties.Entries { + result[entry.XMLName.Local] = entry.Value + } + return result +} + +func distinctValues(input map[string]string) []string { + valueSet := make(map[string]struct{}) + for _, value := range input { + valueSet[value] = struct{}{} + } + + var result []string + for value := range valueSet { + result = append(result, value) + } + + return result +} + +// Function to find all properties that match the pattern `spring.cloud.stream.bindings..destination` +func getBindingDestinationMap(properties map[string]string) map[string]string { + result := make(map[string]string) + + // Iterate through the properties map and look for matching keys + for key, value := range properties { + // Check if the key matches the pattern `spring.cloud.stream.bindings..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 hasDependency(project *SpringBootProject, groupId string, artifactId string) bool { + for _, projectDependency := range project.mavenProject.Dependencies { + if projectDependency.GroupId == groupId && projectDependency.ArtifactId == artifactId { + return true + } + } + return false +} diff --git a/cli/azd/internal/appdetect/spring_boot_property.go b/cli/azd/internal/appdetect/spring_boot_property.go new file mode 100644 index 00000000000..95cbcde6248 --- /dev/null +++ b/cli/azd/internal/appdetect/spring_boot_property.go @@ -0,0 +1,124 @@ +package appdetect + +import ( + "bufio" + "fmt" + "github.com/azure/azure-dev/cli/azd/pkg/osutil" + "github.com/braydonk/yaml" + "log" + "os" + "path/filepath" + "strings" +) + +func readProperties(projectPath string) map[string]string { + // todo: do we need to consider the bootstrap.properties + result := make(map[string]string) + readPropertiesInPropertiesFile(filepath.Join(projectPath, "/src/main/resources/application.properties"), result) + readPropertiesInYamlFile(filepath.Join(projectPath, "/src/main/resources/application.yml"), result) + readPropertiesInYamlFile(filepath.Join(projectPath, "/src/main/resources/application.yaml"), result) + profile, profileSet := result["spring.profiles.active"] + if profileSet { + readPropertiesInPropertiesFile( + filepath.Join(projectPath, "/src/main/resources/application-"+profile+".properties"), result) + readPropertiesInYamlFile(filepath.Join(projectPath, "/src/main/resources/application-"+profile+".yml"), result) + readPropertiesInYamlFile(filepath.Join(projectPath, "/src/main/resources/application-"+profile+".yaml"), result) + } + return result +} + +func readPropertiesInYamlFile(yamlFilePath string, result map[string]string) { + if !osutil.FileExists(yamlFilePath) { + return + } + data, err := os.ReadFile(yamlFilePath) + if err != nil { + log.Fatalf("error reading YAML file: %v", err) + return + } + + // Parse the YAML into a yaml.Node + var root yaml.Node + err = yaml.Unmarshal(data, &root) + if err != nil { + log.Fatalf("error unmarshalling YAML: %v", err) + return + } + + parseYAML("", &root, result) +} + +// Recursively parse the YAML and build dot-separated keys into a map +func parseYAML(prefix string, node *yaml.Node, result map[string]string) { + switch node.Kind { + case yaml.DocumentNode: + // Process each document's content + for _, contentNode := range node.Content { + parseYAML(prefix, contentNode, result) + } + case yaml.MappingNode: + // Process key-value pairs in a map + for i := 0; i < len(node.Content); i += 2 { + keyNode := node.Content[i] + valueNode := node.Content[i+1] + + // Ensure the key is a scalar + if keyNode.Kind != yaml.ScalarNode { + continue + } + + keyStr := keyNode.Value + newPrefix := keyStr + if prefix != "" { + newPrefix = prefix + "." + keyStr + } + parseYAML(newPrefix, valueNode, result) + } + case yaml.SequenceNode: + // Process items in a sequence (list) + for i, item := range node.Content { + newPrefix := fmt.Sprintf("%s[%d]", prefix, i) + parseYAML(newPrefix, item, result) + } + case yaml.ScalarNode: + // If it's a scalar value, add it to the result map + result[prefix] = getEnvironmentVariablePlaceholderHandledValue(node.Value) + default: + // Handle other node types if necessary + } +} + +func readPropertiesInPropertiesFile(propertiesFilePath string, result map[string]string) { + if !osutil.FileExists(propertiesFilePath) { + return + } + file, err := os.Open(propertiesFilePath) + if err != nil { + log.Fatalf("error opening properties file: %v", err) + return + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if strings.TrimSpace(line) == "" || strings.HasPrefix(line, "#") { + continue + } + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 { + key := strings.TrimSpace(parts[0]) + value := getEnvironmentVariablePlaceholderHandledValue(parts[1]) + result[key] = value + } + } +} + +func getEnvironmentVariablePlaceholderHandledValue(rawValue string) string { + trimmedRawValue := strings.TrimSpace(rawValue) + if strings.HasPrefix(trimmedRawValue, "${") && strings.HasSuffix(trimmedRawValue, "}") { + envVar := trimmedRawValue[2 : len(trimmedRawValue)-1] + return os.Getenv(envVar) + } + return trimmedRawValue +} diff --git a/cli/azd/internal/appdetect/spring_boot_property_test.go b/cli/azd/internal/appdetect/spring_boot_property_test.go new file mode 100644 index 00000000000..922bd17503e --- /dev/null +++ b/cli/azd/internal/appdetect/spring_boot_property_test.go @@ -0,0 +1,70 @@ +package appdetect + +import ( + "github.com/stretchr/testify/require" + "os" + "path/filepath" + "testing" +) + +func TestReadProperties(t *testing.T) { + var properties = readProperties(filepath.Join("testdata", "java-spring", "project-one")) + require.Equal(t, "", properties["not.exist"]) + require.Equal(t, "jdbc:h2:mem:testdb", properties["spring.datasource.url"]) + + properties = readProperties(filepath.Join("testdata", "java-spring", "project-two")) + require.Equal(t, "", properties["not.exist"]) + require.Equal(t, "jdbc:h2:mem:testdb", properties["spring.datasource.url"]) + + properties = readProperties(filepath.Join("testdata", "java-spring", "project-three")) + require.Equal(t, "", properties["not.exist"]) + require.Equal(t, "HTML", properties["spring.thymeleaf.mode"]) + + properties = readProperties(filepath.Join("testdata", "java-spring", "project-four")) + require.Equal(t, "", properties["not.exist"]) + require.Equal(t, "mysql", properties["database"]) +} + +func TestGetEnvironmentVariablePlaceholderHandledValue(t *testing.T) { + tests := []struct { + name string + inputValue string + environmentVariables map[string]string + expectedValue string + }{ + { + "No environment variable placeholder", + "valueOne", + map[string]string{}, + "valueOne", + }, + { + "Has invalid environment variable placeholder", + "${VALUE_ONE", + map[string]string{}, + "${VALUE_ONE", + }, + { + "Has valid environment variable placeholder, but environment variable not set", + "${VALUE_TWO}", + map[string]string{}, + "", + }, + { + "Has valid environment variable placeholder, and environment variable set", + "${VALUE_THREE}", + map[string]string{"VALUE_THREE": "valueThree"}, + "valueThree", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for k, v := range tt.environmentVariables { + err := os.Setenv(k, v) + require.NoError(t, err) + } + handledValue := getEnvironmentVariablePlaceholderHandledValue(tt.inputValue) + require.Equal(t, tt.expectedValue, handledValue) + }) + } +} diff --git a/cli/azd/internal/appdetect/java_test.go b/cli/azd/internal/appdetect/spring_boot_test.go similarity index 80% rename from cli/azd/internal/appdetect/java_test.go rename to cli/azd/internal/appdetect/spring_boot_test.go index f6d8e89a80d..35a3e6be47f 100644 --- a/cli/azd/internal/appdetect/java_test.go +++ b/cli/azd/internal/appdetect/spring_boot_test.go @@ -3,55 +3,9 @@ package appdetect import ( "encoding/xml" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "os" "testing" ) -func TestGetEnvironmentVariablePlaceholderHandledValue(t *testing.T) { - tests := []struct { - name string - inputValue string - environmentVariables map[string]string - expectedValue string - }{ - { - "No environment variable placeholder", - "valueOne", - map[string]string{}, - "valueOne", - }, - { - "Has invalid environment variable placeholder", - "${VALUE_ONE", - map[string]string{}, - "${VALUE_ONE", - }, - { - "Has valid environment variable placeholder, but environment variable not set", - "${VALUE_TWO}", - map[string]string{}, - "", - }, - { - "Has valid environment variable placeholder, and environment variable set", - "${VALUE_THREE}", - map[string]string{"VALUE_THREE": "valueThree"}, - "valueThree", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - for k, v := range tt.environmentVariables { - err := os.Setenv(k, v) - require.NoError(t, err) - } - handledValue := getEnvironmentVariablePlaceholderHandledValue(tt.inputValue) - require.Equal(t, tt.expectedValue, handledValue) - }) - } -} - func TestDetectSpringBootVersion(t *testing.T) { tests := []struct { name string From 9b5d218419da1d2cc0bad1a4140abeaaba09602e Mon Sep 17 00:00:00 2001 From: HaoZhang Date: Fri, 22 Nov 2024 15:40:38 +0800 Subject: [PATCH 83/92] support cosmos when azd compose enabled (#43) --- cli/azd/internal/repository/app_init.go | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index d3c250cf546..b080c252e07 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -587,12 +587,23 @@ func (i *Initializer) prjConfigFromDetect( }, } case appdetect.DbCosmos: + cosmosDBProps := project.CosmosDBProps{ + DatabaseName: databaseName, + } + containers, err := detectCosmosSqlDatabaseContainersInDirectory(detect.root) + if err != nil { + return config, err + } + for _, container := range containers { + cosmosDBProps.Containers = append(cosmosDBProps.Containers, project.CosmosDBContainerProps{ + ContainerName: container.ContainerName, + PartitionKeyPaths: container.PartitionKeyPaths, + }) + } resourceConfig = project.ResourceConfig{ - Type: project.ResourceTypeDbCosmos, - Name: "cosmos", - Props: project.CosmosDBProps{ - DatabaseName: databaseName, - }, + Type: project.ResourceTypeDbCosmos, + Name: "cosmos", + Props: cosmosDBProps, } case appdetect.DbPostgres: resourceConfig = project.ResourceConfig{ From ace3a6032d10ae2bb03fff50127c7df79736c335 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 22 Nov 2024 16:13:51 +0800 Subject: [PATCH 84/92] Update yaml language server (#44) --- cli/azd/internal/appdetect/java.go | 1 + cli/azd/pkg/project/project.go | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cli/azd/internal/appdetect/java.go b/cli/azd/internal/appdetect/java.go index 26866f46241..c15a895dd13 100644 --- a/cli/azd/internal/appdetect/java.go +++ b/cli/azd/internal/appdetect/java.go @@ -7,6 +7,7 @@ import ( "github.com/azure/azure-dev/cli/azd/internal/tracing" "github.com/azure/azure-dev/cli/azd/internal/tracing/fields" "io/fs" + "log" "os" "path/filepath" "strings" diff --git a/cli/azd/pkg/project/project.go b/cli/azd/pkg/project/project.go index bdd902262a4..13f96c8766a 100644 --- a/cli/azd/pkg/project/project.go +++ b/cli/azd/pkg/project/project.go @@ -265,13 +265,13 @@ func Save(ctx context.Context, projectConfig *ProjectConfig, projectFilePath str return fmt.Errorf("marshalling project yaml: %w", err) } - version := "v1.0" + version := "alpha" if projectConfig.MetaSchemaVersion != "" { version = projectConfig.MetaSchemaVersion } annotation := fmt.Sprintf( - "# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/%s/azure.yaml.json", + "# yaml-language-server: $schema=https://raw.githubusercontent.com/azure-javaee/azure-dev/feature/sjad/schemas/%s/azure.yaml.json", version) projectFileContents := bytes.NewBufferString(annotation + "\n\n") _, err = projectFileContents.Write(projectBytes) From 7d5bdd69cb8f90d9a6cd148145b2bffe72b2ebdc Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 22 Nov 2024 16:43:44 +0800 Subject: [PATCH 85/92] Add "storageAccountResource" in azure.yaml.json (#45) --- schemas/alpha/azure.yaml.json | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/schemas/alpha/azure.yaml.json b/schemas/alpha/azure.yaml.json index 5a30fb64c46..2c5cd04d067 100644 --- a/schemas/alpha/azure.yaml.json +++ b/schemas/alpha/azure.yaml.json @@ -401,7 +401,7 @@ { "if": { "properties": { "type": { "const": "messaging.servicebus" }}}, "then": { "$ref": "#/definitions/serviceBusResource"} }, { "if": { "properties": { "type": { "const": "messaging.eventhubs" }}}, "then": { "$ref": "#/definitions/eventHubsResource"} }, { "if": { "properties": { "type": { "const": "messaging.kafka" }}}, "then": { "$ref": "#/definitions/kafkaResource"} }, - { "if": { "properties": { "type": { "const": "storage" }}}, "then": { "$ref": "#/definitions/resource"} } + { "if": { "properties": { "type": { "const": "storage" }}}, "then": { "$ref": "#/definitions/storageAccountResource"} } ] } }, @@ -1373,6 +1373,34 @@ } } }, + "storageAccountResource": { + "type": "object", + "description": "A deployed, ready-to-use Azure Storage Account.", + "additionalProperties": false, + "properties": { + "type": true, + "uses": true, + "authType": { + "type": "string", + "title": "Authentication Type", + "description": "The type of authentication used for Azure Storage Account.", + "enum": [ + "USER_ASSIGNED_MANAGED_IDENTITY", + "CONNECTION_STRING" + ] + }, + "containers": { + "type": "array", + "title": "Azure Storage Account container names.", + "description": "The container names of Azure Storage Account.", + "items": { + "type": "string", + "title": "Azure Storage Account container name", + "description": "The container name of Azure Storage Account." + } + } + } + }, "cosmosDbResource": { "type": "object", "description": "A deployed, ready-to-use Azure Cosmos DB for NoSQL.", From 8c72593407884c1e5d9f89607639fa5a4600fcfb Mon Sep 17 00:00:00 2001 From: Xiaolu Dai Date: Sat, 23 Nov 2024 00:25:41 +0800 Subject: [PATCH 86/92] fix test --- cli/azd/internal/repository/app_init_test.go | 18 +++++--- cli/azd/pkg/project/importer_test.go | 6 +++ cli/azd/pkg/project/scaffold_gen.go | 45 +++++++++++++++++--- cli/azd/test/functional/init_test.go | 2 +- 4 files changed, 60 insertions(+), 11 deletions(-) diff --git a/cli/azd/internal/repository/app_init_test.go b/cli/azd/internal/repository/app_init_test.go index 29be2930407..12b6fbabd40 100644 --- a/cli/azd/internal/repository/app_init_test.go +++ b/cli/azd/internal/repository/app_init_test.go @@ -217,6 +217,7 @@ func TestInitializer_prjConfigFromDetect(t *testing.T) { "my$special$db", "n", "postgres", // fill in db name + "Username and password", }, want: project.ProjectConfig{ Services: map[string]*project.ServiceConfig{ @@ -237,18 +238,25 @@ func TestInitializer_prjConfigFromDetect(t *testing.T) { Type: project.ResourceTypeDbRedis, Name: "redis", }, - "mongodb": { + "mongo": { Type: project.ResourceTypeDbMongo, - Name: "mongodb", + Name: "mongo", + Props: project.MongoDBProps{ + DatabaseName: "mongodb", + }, }, - "postgres": { + "postgresql": { Type: project.ResourceTypeDbPostgres, - Name: "postgres", + Name: "postgresql", + Props: project.PostgresProps{ + AuthType: internal.AuthTypePassword, + DatabaseName: "postgres", + }, }, "py": { Type: project.ResourceTypeHostContainerApp, Name: "py", - Uses: []string{"postgres", "mongodb", "redis"}, + Uses: []string{"postgresql", "mongo", "redis"}, Props: project.ContainerAppProps{ Port: 80, }, diff --git a/cli/azd/pkg/project/importer_test.go b/cli/azd/pkg/project/importer_test.go index cf9c671c4f4..03ab794d06f 100644 --- a/cli/azd/pkg/project/importer_test.go +++ b/cli/azd/pkg/project/importer_test.go @@ -411,6 +411,9 @@ func Test_ImportManager_ProjectInfrastructure_FromResources(t *testing.T) { prjConfig := &ProjectConfig{} err := yaml.Unmarshal([]byte(prjWithResources), prjConfig) + for key, res := range prjConfig.Resources { + res.Name = key + } require.NoError(t, err) infra, err := im.ProjectInfrastructure(context.Background(), prjConfig) @@ -443,6 +446,9 @@ func TestImportManager_SynthAllInfrastructure_FromResources(t *testing.T) { prjConfig := &ProjectConfig{} err := yaml.Unmarshal([]byte(prjWithResources), prjConfig) require.NoError(t, err) + for key, res := range prjConfig.Resources { + res.Name = key + } projectFs, err := im.SynthAllInfrastructure(context.Background(), prjConfig) require.NoError(t, err) diff --git a/cli/azd/pkg/project/scaffold_gen.go b/cli/azd/pkg/project/scaffold_gen.go index 53f24f14a58..479e9ab8c9c 100644 --- a/cli/azd/pkg/project/scaffold_gen.go +++ b/cli/azd/pkg/project/scaffold_gen.go @@ -310,9 +310,11 @@ func printHintsAboutUses(infraSpec *scaffold.InfraSpec, projectConfig *ProjectCo return fmt.Errorf("in azure.yaml, (%s) uses (%s), but (%s) doesn't", userResourceName, usedResourceName, usedResourceName) } - (*console).Message(*context, fmt.Sprintf("CAUTION: In azure.yaml, '%s' uses '%s'. "+ - "After deployed, the 'uses' is achieved by providing these environment variables: ", - userResourceName, usedResourceName)) + if *console != nil { + (*console).Message(*context, fmt.Sprintf("CAUTION: In azure.yaml, '%s' uses '%s'. "+ + "After deployed, the 'uses' is achieved by providing these environment variables: ", + userResourceName, usedResourceName)) + } switch usedResource.Type { case ResourceTypeDbPostgres: err := printHintsAboutUsePostgres(userSpec.DbPostgres.AuthType, console, context) @@ -356,7 +358,10 @@ func printHintsAboutUses(infraSpec *scaffold.InfraSpec, projectConfig *ProjectCo "which is doen't add necessary environment variable", userResource.Name, usedResource.Name, usedResource.Name, usedResource.Type) } - (*console).Message(*context, "Please make sure your application used the right environment variable name.\n") + if *console != nil { + (*console).Message(*context, "Please make sure your application used the right environment variable name.\n") + } + } } return nil @@ -485,7 +490,7 @@ func fulfillFrontendBackend( usedSpec := getServiceSpecByName(infraSpec, usedResource.Name) if usedSpec == nil { - return fmt.Errorf("'%s' uses '%s', but %s doesn't", userSpec.Name, usedResource.Name, usedResource.Name) + return fmt.Errorf("'%s' uses '%s', but %s doesn't exist", userSpec.Name, usedResource.Name, usedResource.Name) } if usedSpec.Backend == nil { usedSpec.Backend = &scaffold.Backend{} @@ -506,6 +511,9 @@ func getServiceSpecByName(infraSpec *scaffold.InfraSpec, name string) *scaffold. func printHintsAboutUsePostgres(authType internal.AuthType, console *input.Console, context *context.Context) error { + if *console == nil { + return nil + } (*console).Message(*context, "POSTGRES_HOST=xxx") (*console).Message(*context, "POSTGRES_DATABASE=xxx") (*console).Message(*context, "POSTGRES_PORT=xxx") @@ -534,6 +542,9 @@ func printHintsAboutUsePostgres(authType internal.AuthType, func printHintsAboutUseMySql(authType internal.AuthType, console *input.Console, context *context.Context) error { + if *console == nil { + return nil + } (*console).Message(*context, "MYSQL_HOST=xxx") (*console).Message(*context, "MYSQL_DATABASE=xxx") (*console).Message(*context, "MYSQL_PORT=xxx") @@ -560,6 +571,9 @@ func printHintsAboutUseMySql(authType internal.AuthType, } func printHintsAboutUseRedis(console *input.Console, context *context.Context) { + if *console == nil { + return + } (*console).Message(*context, "REDIS_HOST=xxx") (*console).Message(*context, "REDIS_PORT=xxx") (*console).Message(*context, "REDIS_URL=xxx") @@ -569,18 +583,27 @@ func printHintsAboutUseRedis(console *input.Console, context *context.Context) { } func printHintsAboutUseMongo(console *input.Console, context *context.Context) { + if *console == nil { + return + } (*console).Message(*context, "MONGODB_URL=xxx") (*console).Message(*context, "spring.data.mongodb.uri=xxx") (*console).Message(*context, "spring.data.mongodb.database=xxx") } func printHintsAboutUseCosmos(console *input.Console, context *context.Context) { + if *console == nil { + return + } (*console).Message(*context, "spring.cloud.azure.cosmos.endpoint=xxx") (*console).Message(*context, "spring.cloud.azure.cosmos.database=xxx") } func printHintsAboutUseServiceBus(isJms bool, authType internal.AuthType, console *input.Console, context *context.Context) error { + if *console == nil { + return nil + } if !isJms { (*console).Message(*context, "spring.cloud.azure.servicebus.namespace=xxx") } @@ -602,6 +625,9 @@ func printHintsAboutUseServiceBus(isJms bool, authType internal.AuthType, func printHintsAboutUseEventHubs(UseKafka bool, authType internal.AuthType, springBootVersion string, console *input.Console, context *context.Context) error { + if *console == nil { + return nil + } if !UseKafka { (*console).Message(*context, "spring.cloud.azure.eventhubs.namespace=xxx") } else { @@ -630,6 +656,9 @@ func printHintsAboutUseEventHubs(UseKafka bool, authType internal.AuthType, spri func printHintsAboutUseStorageAccount(authType internal.AuthType, console *input.Console, context *context.Context) error { + if *console == nil { + return nil + } (*console).Message(*context, "spring.cloud.azure.eventhubs.processor.checkpoint-store.account-name=xxx") if authType == internal.AuthTypeUserAssignedManagedIdentity { (*console).Message(*context, "spring.cloud.azure.eventhubs.processor.checkpoint-store.connection-string=''") @@ -649,6 +678,9 @@ func printHintsAboutUseStorageAccount(authType internal.AuthType, func printHintsAboutUseHostContainerApp(userResourceName string, usedResourceName string, console *input.Console, context *context.Context) { + if *console == nil { + return + } (*console).Message(*context, fmt.Sprintf("Environemnt variables in %s:", userResourceName)) (*console).Message(*context, fmt.Sprintf("%s_BASE_URL=xxx", strings.ToUpper(usedResourceName))) (*console).Message(*context, fmt.Sprintf("Environemnt variables in %s:", usedResourceName)) @@ -656,5 +688,8 @@ func printHintsAboutUseHostContainerApp(userResourceName string, usedResourceNam } func printHintsAboutUseOpenAiModel(console *input.Console, context *context.Context) { + if *console == nil { + return + } (*console).Message(*context, "AZURE_OPENAI_ENDPOINT") } diff --git a/cli/azd/test/functional/init_test.go b/cli/azd/test/functional/init_test.go index de961f0f1eb..3e4809947a5 100644 --- a/cli/azd/test/functional/init_test.go +++ b/cli/azd/test/functional/init_test.go @@ -203,7 +203,7 @@ func Test_CLI_Init_From_App_With_Infra(t *testing.T) { "Use code in the current directory\n"+ "Confirm and continue initializing my app\n"+ "appdb\n"+ - "Use user assigned managed identity\n"+ + "User assigned managed identity\n"+ "TESTENV\n", "init", ) From a7ef89243b263bcaa6895ec5dec8b3ec047abb07 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Mon, 25 Nov 2024 18:54:14 +0800 Subject: [PATCH 87/92] Fix bug: Deploy failed for the second time when resource name too long. (#47) --- cli/azd/resources/scaffold/templates/resources.bicept | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index b99bc3c8c7c..cf949011783 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -520,7 +520,7 @@ module {{bicepName .Name}}FetchLatestImage './modules/fetch-container-image.bice name: '{{bicepName .Name}}-fetch-image' params: { exists: {{bicepName .Name}}Exists - name: '{{.Name}}' + name: '{{containerAppName .Name}}' } } From 817ce354d9991751a90f05047f833a3b10b45a1f Mon Sep 17 00:00:00 2001 From: HaoZhang Date: Thu, 28 Nov 2024 09:54:11 +0800 Subject: [PATCH 88/92] replace placeholders when reading pom.xml (#50) Co-authored-by: haozhang --- cli/azd/internal/appdetect/java.go | 33 ++++++++++++++++++++++- cli/azd/internal/appdetect/spring_boot.go | 20 ++------------ 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/cli/azd/internal/appdetect/java.go b/cli/azd/internal/appdetect/java.go index c15a895dd13..1b2b66f624a 100644 --- a/cli/azd/internal/appdetect/java.go +++ b/cli/azd/internal/appdetect/java.go @@ -10,6 +10,7 @@ import ( "log" "os" "path/filepath" + "regexp" "strings" ) @@ -127,8 +128,16 @@ func readMavenProject(filePath string) (*mavenProject, error) { return nil, err } + var initialProject mavenProject + if err := xml.Unmarshal(bytes, &initialProject); err != nil { + return nil, fmt.Errorf("parsing xml: %w", err) + } + + // replace all placeholders with properties + str := replaceAllPlaceholders(initialProject, string(bytes)) + var project mavenProject - if err := xml.Unmarshal(bytes, &project); err != nil { + if err := xml.Unmarshal([]byte(str), &project); err != nil { return nil, fmt.Errorf("parsing xml: %w", err) } @@ -137,6 +146,28 @@ func readMavenProject(filePath string) (*mavenProject, error) { return &project, nil } +func replaceAllPlaceholders(project mavenProject, input string) string { + propsMap := parseProperties(project.Properties) + + re := regexp.MustCompile(`\$\{([A-Za-z0-9-_.]+)}`) + return re.ReplaceAllStringFunc(input, func(match string) string { + // Extract the key inside ${} + key := re.FindStringSubmatch(match)[1] + if value, exists := propsMap[key]; exists { + return value + } + return match + }) +} + +func parseProperties(properties Properties) map[string]string { + result := make(map[string]string) + for _, entry := range properties.Entries { + result[entry.XMLName.Local] = entry.Value + } + return result +} + func detectDependencies(currentRoot *mavenProject, mavenProject *mavenProject, project *Project) (*Project, error) { detectAzureDependenciesByAnalyzingSpringBootProject(currentRoot, mavenProject, project) return project, nil diff --git a/cli/azd/internal/appdetect/spring_boot.go b/cli/azd/internal/appdetect/spring_boot.go index b0d33a5164f..d2e4d03ba4e 100644 --- a/cli/azd/internal/appdetect/spring_boot.go +++ b/cli/azd/internal/appdetect/spring_boot.go @@ -276,11 +276,11 @@ func detectSpringBootVersion(currentRoot *mavenProject, mavenProject *mavenProje func detectSpringBootVersionFromProject(project *mavenProject) string { if project.Parent.ArtifactId == "spring-boot-starter-parent" { - return depVersion(project.Parent.Version, project.Properties) + return project.Parent.Version } else { for _, dep := range project.DependencyManagement.Dependencies { if dep.ArtifactId == "spring-boot-dependencies" { - return depVersion(dep.Version, project.Properties) + return dep.Version } } } @@ -305,22 +305,6 @@ func isSpringBootApplication(mavenProject *mavenProject) bool { return false } -func depVersion(version string, properties Properties) string { - if strings.HasPrefix(version, "${") { - return parseProperties(properties)[version[2:len(version)-1]] - } else { - return version - } -} - -func parseProperties(properties Properties) map[string]string { - result := make(map[string]string) - for _, entry := range properties.Entries { - result[entry.XMLName.Local] = entry.Value - } - return result -} - func distinctValues(input map[string]string) []string { valueSet := make(map[string]struct{}) for _, value := range input { From 40c305d4e97a67d621b3a3c62bd394a7b66e93ee Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Thu, 28 Nov 2024 12:26:31 +0800 Subject: [PATCH 89/92] Avoid maintain env list in multiple place (#46) --- cli/azd/internal/appdetect/spring_boot.go | 2 +- cli/azd/internal/scaffold/bicep_env.go | 214 +++++++ cli/azd/internal/scaffold/bicep_env_test.go | 172 ++++++ cli/azd/internal/scaffold/scaffold.go | 16 +- cli/azd/internal/scaffold/spec.go | 70 ++- cli/azd/internal/scaffold/spec_test.go | 94 +++ cli/azd/pkg/project/importer.go | 9 +- cli/azd/pkg/project/importer_test.go | 13 +- cli/azd/pkg/project/scaffold_gen.go | 363 ++++-------- .../scaffold_gen_environment_variables.go | 552 ++++++++++++++++++ ...scaffold_gen_environment_variables_test.go | 92 +++ cli/azd/pkg/project/scaffold_gen_test.go | 12 +- ...ent-hubs-namespace-connection-string.bicep | 2 + .../base/modules/set-redis-conn.bicep | 3 + ...rvicebus-namespace-connection-string.bicep | 2 + ...et-storage-account-connection-string.bicep | 2 + .../scaffold/templates/resources.bicept | 379 ++---------- 17 files changed, 1380 insertions(+), 617 deletions(-) create mode 100644 cli/azd/internal/scaffold/bicep_env.go create mode 100644 cli/azd/internal/scaffold/bicep_env_test.go create mode 100644 cli/azd/internal/scaffold/spec_test.go create mode 100644 cli/azd/pkg/project/scaffold_gen_environment_variables.go create mode 100644 cli/azd/pkg/project/scaffold_gen_environment_variables_test.go diff --git a/cli/azd/internal/appdetect/spring_boot.go b/cli/azd/internal/appdetect/spring_boot.go index d2e4d03ba4e..4f97c1ff23b 100644 --- a/cli/azd/internal/appdetect/spring_boot.go +++ b/cli/azd/internal/appdetect/spring_boot.go @@ -188,7 +188,7 @@ func detectEventHubsAccordingToSpringCloudStreamBinderMavenDependency( func detectEventHubsAccordingToSpringCloudStreamKafkaMavenDependency( azdProject *Project, springBootProject *SpringBootProject) { - var targetGroupId = "com.azure.spring" + var targetGroupId = "org.springframework.cloud" var targetArtifactId = "spring-cloud-starter-stream-kafka" if hasDependency(springBootProject, targetGroupId, targetArtifactId) { bindingDestinations := getBindingDestinationMap(springBootProject.applicationProperties) diff --git a/cli/azd/internal/scaffold/bicep_env.go b/cli/azd/internal/scaffold/bicep_env.go new file mode 100644 index 00000000000..330f83cbb60 --- /dev/null +++ b/cli/azd/internal/scaffold/bicep_env.go @@ -0,0 +1,214 @@ +package scaffold + +import ( + "fmt" + "github.com/azure/azure-dev/cli/azd/internal" + "strings" +) + +func ToBicepEnv(env Env) BicepEnv { + if isResourceConnectionEnv(env.Value) { + resourceType, resourceInfoType := toResourceConnectionInfo(env.Value) + value, ok := bicepEnv[resourceType][resourceInfoType] + if !ok { + panic(unsupportedType(env)) + } + if isSecret(resourceInfoType) { + if isKeyVaultSecret(value) { + return BicepEnv{ + BicepEnvType: BicepEnvTypeKeyVaultSecret, + Name: env.Name, + SecretName: secretName(env), + SecretValue: unwrapKeyVaultSecretValue(value), + } + } else { + return BicepEnv{ + BicepEnvType: BicepEnvTypeSecret, + Name: env.Name, + SecretName: secretName(env), + SecretValue: value, + } + } + } else { + return BicepEnv{ + BicepEnvType: BicepEnvTypePlainText, + Name: env.Name, + PlainTextValue: value, + } + } + } else { + return BicepEnv{ + BicepEnvType: BicepEnvTypePlainText, + Name: env.Name, + PlainTextValue: toBicepEnvPlainTextValue(env.Value), + } + } +} + +func ShouldAddToBicepFile(spec ServiceSpec, name string) bool { + return !willBeAddedByServiceConnector(spec, name) +} + +func willBeAddedByServiceConnector(spec ServiceSpec, name string) bool { + if (spec.DbPostgres != nil && spec.DbPostgres.AuthType == internal.AuthTypeUserAssignedManagedIdentity) || + (spec.DbMySql != nil && spec.DbMySql.AuthType == internal.AuthTypeUserAssignedManagedIdentity) { + return name == "spring.datasource.url" || + name == "spring.datasource.username" || + name == "spring.datasource.azure.passwordless-enabled" + } else { + return false + } +} + +// inputStringExample -> 'inputStringExample' +func addQuotation(input string) string { + return fmt.Sprintf("'%s'", input) +} + +// 'inputStringExample' -> 'inputStringExample' +// '${inputSingleVariableExample}' -> inputSingleVariableExample +// '${HOST}:${PORT}' -> '${HOST}:${PORT}' +func removeQuotationIfItIsASingleVariable(input string) string { + prefix := "'${" + suffix := "}'" + if strings.HasPrefix(input, prefix) && strings.HasSuffix(input, suffix) { + prefixTrimmed := strings.TrimPrefix(input, prefix) + trimmed := strings.TrimSuffix(prefixTrimmed, suffix) + if strings.IndexAny(trimmed, "}") == -1 { + return trimmed + } else { + return input + } + } else { + return input + } +} + +// The BicepEnv.PlainTextValue is handled as variable by default. +// If the value is string, it should contain ('). +// Here are some examples of input and output: +// inputStringExample -> 'inputStringExample' +// ${inputSingleVariableExample} -> inputSingleVariableExample +// ${HOST}:${PORT} -> '${HOST}:${PORT}' +func toBicepEnvPlainTextValue(input string) string { + return removeQuotationIfItIsASingleVariable(addQuotation(input)) +} + +// BicepEnv +// +// For Name and SecretName, they are handled as string by default. +// Which means quotation will be added before they are used in bicep file, because they are always string value. +// +// For PlainTextValue and SecretValue, they are handled as variable by default. +// When they are string value, quotation should be contained by themselves. +// Set variable as default is mainly to avoid this problem: +// https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/linter-rule-simplify-interpolation +type BicepEnv struct { + BicepEnvType BicepEnvType + Name string + PlainTextValue string + SecretName string + SecretValue string +} + +type BicepEnvType string + +const ( + BicepEnvTypePlainText BicepEnvType = "plainText" + BicepEnvTypeSecret BicepEnvType = "secret" + BicepEnvTypeKeyVaultSecret BicepEnvType = "keyVaultSecret" +) + +// Note: The value is handled as variable. +// If the value is string, it should contain quotation inside itself. +var bicepEnv = map[ResourceType]map[ResourceInfoType]string{ + ResourceTypeDbPostgres: { + ResourceInfoTypeHost: "postgreServer.outputs.fqdn", + ResourceInfoTypePort: "'5432'", + ResourceInfoTypeDatabaseName: "postgreSqlDatabaseName", + ResourceInfoTypeUsername: "postgreSqlDatabaseUser", + ResourceInfoTypePassword: "postgreSqlDatabasePassword", + ResourceInfoTypeUrl: "'postgresql://${postgreSqlDatabaseUser}:${postgreSqlDatabasePassword}@${postgreServer.outputs.fqdn}:5432/${postgreSqlDatabaseName}'", + ResourceInfoTypeJdbcUrl: "'jdbc:postgresql://${postgreServer.outputs.fqdn}:5432/${postgreSqlDatabaseName}'", + }, + ResourceTypeDbMySQL: { + ResourceInfoTypeHost: "mysqlServer.outputs.fqdn", + ResourceInfoTypePort: "'3306'", + ResourceInfoTypeDatabaseName: "mysqlDatabaseName", + ResourceInfoTypeUsername: "mysqlDatabaseUser", + ResourceInfoTypePassword: "mysqlDatabasePassword", + ResourceInfoTypeUrl: "'mysql://${mysqlDatabaseUser}:${mysqlDatabasePassword}@${mysqlServer.outputs.fqdn}:3306/${mysqlDatabaseName}'", + ResourceInfoTypeJdbcUrl: "'jdbc:mysql://${mysqlServer.outputs.fqdn}:3306/${mysqlDatabaseName}'", + }, + ResourceTypeDbRedis: { + ResourceInfoTypeHost: "redis.outputs.hostName", + ResourceInfoTypePort: "string(redis.outputs.sslPort)", + ResourceInfoTypeEndpoint: "'${redis.outputs.hostName}:${redis.outputs.sslPort}'", + ResourceInfoTypePassword: wrapToKeyVaultSecretValue("redisConn.outputs.keyVaultUrlForPass"), + ResourceInfoTypeUrl: wrapToKeyVaultSecretValue("redisConn.outputs.keyVaultUrlForUrl"), + }, + ResourceTypeDbMongo: { + ResourceInfoTypeDatabaseName: "mongoDatabaseName", + ResourceInfoTypeUrl: wrapToKeyVaultSecretValue("cosmos.outputs.exportedSecrets['MONGODB-URL'].secretUri"), + }, + ResourceTypeDbCosmos: { + ResourceInfoTypeEndpoint: "cosmos.outputs.endpoint", + ResourceInfoTypeDatabaseName: "cosmosDatabaseName", + }, + ResourceTypeMessagingServiceBus: { + ResourceInfoTypeNamespace: "serviceBusNamespace.outputs.name", + ResourceInfoTypeConnectionString: wrapToKeyVaultSecretValue("serviceBusConnectionString.outputs.keyVaultUrl"), + }, + ResourceTypeMessagingEventHubs: { + ResourceInfoTypeNamespace: "eventHubNamespace.outputs.name", + ResourceInfoTypeConnectionString: wrapToKeyVaultSecretValue("eventHubsConnectionString.outputs.keyVaultUrl"), + }, + ResourceTypeMessagingKafka: { + ResourceInfoTypeEndpoint: "'${eventHubNamespace.outputs.name}.servicebus.windows.net:9093'", + ResourceInfoTypeConnectionString: wrapToKeyVaultSecretValue("eventHubsConnectionString.outputs.keyVaultUrl"), + }, + ResourceTypeStorage: { + ResourceInfoTypeAccountName: "storageAccountName", + ResourceInfoTypeConnectionString: wrapToKeyVaultSecretValue("storageAccountConnectionString.outputs.keyVaultUrl"), + }, + ResourceTypeOpenAiModel: { + ResourceInfoTypeEndpoint: "account.outputs.endpoint", + }, + ResourceTypeHostContainerApp: {}, +} + +func unsupportedType(env Env) string { + return fmt.Sprintf("unsupported connection info type for resource type. "+ + "value = %s", env.Value) +} + +func PlaceHolderForServiceIdentityClientId() string { + return "__PlaceHolderForServiceIdentityClientId" +} + +func isSecret(info ResourceInfoType) bool { + return info == ResourceInfoTypePassword || info == ResourceInfoTypeUrl || info == ResourceInfoTypeConnectionString +} + +func secretName(env Env) string { + resourceType, resourceInfoType := toResourceConnectionInfo(env.Value) + name := fmt.Sprintf("%s-%s", resourceType, resourceInfoType) + lowerCaseName := strings.ToLower(name) + noDotName := strings.Replace(lowerCaseName, ".", "-", -1) + noUnderscoreName := strings.Replace(noDotName, "_", "-", -1) + return noUnderscoreName +} + +var keyVaultSecretPrefix = "keyvault:" + +func isKeyVaultSecret(value string) bool { + return strings.HasPrefix(value, keyVaultSecretPrefix) +} + +func wrapToKeyVaultSecretValue(value string) string { + return fmt.Sprintf("%s%s", keyVaultSecretPrefix, value) +} + +func unwrapKeyVaultSecretValue(value string) string { + return strings.TrimPrefix(value, keyVaultSecretPrefix) +} diff --git a/cli/azd/internal/scaffold/bicep_env_test.go b/cli/azd/internal/scaffold/bicep_env_test.go new file mode 100644 index 00000000000..d93efd57e98 --- /dev/null +++ b/cli/azd/internal/scaffold/bicep_env_test.go @@ -0,0 +1,172 @@ +package scaffold + +import ( + "github.com/azure/azure-dev/cli/azd/internal" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestToBicepEnv(t *testing.T) { + tests := []struct { + name string + in Env + want BicepEnv + }{ + { + name: "Plain text", + in: Env{ + Name: "enable-customer-related-feature", + Value: "true", + }, + want: BicepEnv{ + BicepEnvType: BicepEnvTypePlainText, + Name: "enable-customer-related-feature", + PlainTextValue: "'true'", // Note: Quotation add automatically + }, + }, + { + name: "Plain text from EnvTypeResourceConnectionPlainText", + in: Env{ + Name: "spring.jms.servicebus.pricing-tier", + Value: "premium", + }, + want: BicepEnv{ + BicepEnvType: BicepEnvTypePlainText, + Name: "spring.jms.servicebus.pricing-tier", + PlainTextValue: "'premium'", // Note: Quotation add automatically + }, + }, + { + name: "Plain text from EnvTypeResourceConnectionResourceInfo", + in: Env{ + Name: "POSTGRES_PORT", + Value: ToResourceConnectionEnv(ResourceTypeDbPostgres, ResourceInfoTypePort), + }, + want: BicepEnv{ + BicepEnvType: BicepEnvTypePlainText, + Name: "POSTGRES_PORT", + PlainTextValue: "'5432'", + }, + }, + { + name: "Secret", + in: Env{ + Name: "POSTGRES_PASSWORD", + Value: ToResourceConnectionEnv(ResourceTypeDbPostgres, ResourceInfoTypePassword), + }, + want: BicepEnv{ + BicepEnvType: BicepEnvTypeSecret, + Name: "POSTGRES_PASSWORD", + SecretName: "db-postgres-password", + SecretValue: "postgreSqlDatabasePassword", + }, + }, + { + name: "KeuVault Secret", + in: Env{ + Name: "REDIS_PASSWORD", + Value: ToResourceConnectionEnv(ResourceTypeDbRedis, ResourceInfoTypePassword), + }, + want: BicepEnv{ + BicepEnvType: BicepEnvTypeKeyVaultSecret, + Name: "REDIS_PASSWORD", + SecretName: "db-redis-password", + SecretValue: "redisConn.outputs.keyVaultUrlForPass", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := ToBicepEnv(tt.in) + assert.Equal(t, tt.want, actual) + }) + } +} + +func TestToBicepEnvPlainTextValue(t *testing.T) { + tests := []struct { + name string + in string + want string + }{ + { + name: "string", + in: "inputStringExample", + want: "'inputStringExample'", + }, + { + name: "single variable", + in: "${inputSingleVariableExample}", + want: "inputSingleVariableExample", + }, + { + name: "multiple variable", + in: "${HOST}:${PORT}", + want: "'${HOST}:${PORT}'", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := toBicepEnvPlainTextValue(tt.in) + assert.Equal(t, tt.want, actual) + }) + } +} + +func TestShouldAddToBicepFile(t *testing.T) { + tests := []struct { + name string + infraSpec ServiceSpec + propertyName string + want bool + }{ + { + name: "not related property and not using mysql and postgres", + infraSpec: ServiceSpec{}, + propertyName: "test", + want: true, + }, + { + name: "not using mysql and postgres", + infraSpec: ServiceSpec{}, + propertyName: "spring.datasource.url", + want: true, + }, + { + name: "not using user assigned managed identity", + infraSpec: ServiceSpec{ + DbMySql: &DatabaseMySql{ + AuthType: internal.AuthTypePassword, + }, + }, + propertyName: "spring.datasource.url", + want: true, + }, + { + name: "not service connector added property", + infraSpec: ServiceSpec{ + DbMySql: &DatabaseMySql{ + AuthType: internal.AuthTypePassword, + }, + }, + propertyName: "test", + want: true, + }, + { + name: "should not added", + infraSpec: ServiceSpec{ + DbMySql: &DatabaseMySql{ + AuthType: internal.AuthTypePassword, + }, + }, + propertyName: "spring.datasource.url", + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := ShouldAddToBicepFile(tt.infraSpec, tt.propertyName) + assert.Equal(t, tt.want, actual) + }) + } +} diff --git a/cli/azd/internal/scaffold/scaffold.go b/cli/azd/internal/scaffold/scaffold.go index b8042ffd88b..2b9d94a6abb 100644 --- a/cli/azd/internal/scaffold/scaffold.go +++ b/cli/azd/internal/scaffold/scaffold.go @@ -25,13 +25,15 @@ const templateRoot = "scaffold/templates" // To execute a named template, call Execute with the defined name. func Load() (*template.Template, error) { funcMap := template.FuncMap{ - "bicepName": BicepName, - "containerAppName": ContainerAppName, - "upper": strings.ToUpper, - "lower": strings.ToLower, - "alphaSnakeUpper": AlphaSnakeUpper, - "formatParam": FormatParameter, - "hasPrefix": strings.HasPrefix, + "bicepName": BicepName, + "containerAppName": ContainerAppName, + "upper": strings.ToUpper, + "lower": strings.ToLower, + "alphaSnakeUpper": AlphaSnakeUpper, + "formatParam": FormatParameter, + "hasPrefix": strings.HasPrefix, + "toBicepEnv": ToBicepEnv, + "shouldAddToBicepFile": ShouldAddToBicepFile, } t, err := template.New("templates"). diff --git a/cli/azd/internal/scaffold/spec.go b/cli/azd/internal/scaffold/spec.go index e1c5aa2c597..198556c5798 100644 --- a/cli/azd/internal/scaffold/spec.go +++ b/cli/azd/internal/scaffold/spec.go @@ -98,7 +98,7 @@ type ServiceSpec struct { Name string Port int - Env map[string]string + Envs []Env // Front-end properties. Frontend *Frontend @@ -121,6 +121,74 @@ type ServiceSpec struct { AzureStorageAccount *AzureDepStorageAccount } +type Env struct { + Name string + Value string +} + +var resourceConnectionEnvPrefix = "$resource.connection" + +func isResourceConnectionEnv(env string) bool { + if !strings.HasPrefix(env, resourceConnectionEnvPrefix) { + return false + } + a := strings.Split(env, ":") + if len(a) != 3 { + return false + } + return a[0] != "" && a[1] != "" && a[2] != "" +} + +func ToResourceConnectionEnv(resourceType ResourceType, resourceInfoType ResourceInfoType) string { + return fmt.Sprintf("%s:%s:%s", resourceConnectionEnvPrefix, resourceType, resourceInfoType) +} + +func toResourceConnectionInfo(resourceConnectionEnv string) (resourceType ResourceType, + resourceInfoType ResourceInfoType) { + if !isResourceConnectionEnv(resourceConnectionEnv) { + return "", "" + } + a := strings.Split(resourceConnectionEnv, ":") + return ResourceType(a[1]), ResourceInfoType(a[2]) +} + +// todo merge ResourceType and project.ResourceType +// Not use project.ResourceType because it will cause cycle import. +// Not merge it in current PR to avoid conflict with upstream main branch. +// Solution proposal: define a ResourceType in lower level that can be used both in scaffold and project package. + +type ResourceType string + +const ( + ResourceTypeDbRedis ResourceType = "db.redis" + ResourceTypeDbPostgres ResourceType = "db.postgres" + ResourceTypeDbMySQL ResourceType = "db.mysql" + ResourceTypeDbMongo ResourceType = "db.mongo" + ResourceTypeDbCosmos ResourceType = "db.cosmos" + ResourceTypeHostContainerApp ResourceType = "host.containerapp" + ResourceTypeOpenAiModel ResourceType = "ai.openai.model" + ResourceTypeMessagingServiceBus ResourceType = "messaging.servicebus" + ResourceTypeMessagingEventHubs ResourceType = "messaging.eventhubs" + ResourceTypeMessagingKafka ResourceType = "messaging.kafka" + ResourceTypeStorage ResourceType = "storage" +) + +type ResourceInfoType string + +const ( + ResourceInfoTypeHost ResourceInfoType = "host" + ResourceInfoTypePort ResourceInfoType = "port" + ResourceInfoTypeEndpoint ResourceInfoType = "endpoint" + ResourceInfoTypeDatabaseName ResourceInfoType = "databaseName" + ResourceInfoTypeNamespace ResourceInfoType = "namespace" + ResourceInfoTypeAccountName ResourceInfoType = "accountName" + ResourceInfoTypeUsername ResourceInfoType = "username" + ResourceInfoTypePassword ResourceInfoType = "password" + ResourceInfoTypeUrl ResourceInfoType = "url" + ResourceInfoTypeJdbcUrl ResourceInfoType = "jdbcUrl" + ResourceInfoTypeConnectionString ResourceInfoType = "connectionString" +) + type Frontend struct { Backends []ServiceReference } diff --git a/cli/azd/internal/scaffold/spec_test.go b/cli/azd/internal/scaffold/spec_test.go new file mode 100644 index 00000000000..34f69f07222 --- /dev/null +++ b/cli/azd/internal/scaffold/spec_test.go @@ -0,0 +1,94 @@ +package scaffold + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestToResourceConnectionEnv(t *testing.T) { + tests := []struct { + name string + inputResourceType ResourceType + inputResourceInfoType ResourceInfoType + want string + }{ + { + name: "mysql username", + inputResourceType: ResourceTypeDbMySQL, + inputResourceInfoType: ResourceInfoTypeUsername, + want: "$resource.connection:db.mysql:username", + }, + { + name: "postgres password", + inputResourceType: ResourceTypeDbPostgres, + inputResourceInfoType: ResourceInfoTypePassword, + want: "$resource.connection:db.postgres:password", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := ToResourceConnectionEnv(tt.inputResourceType, tt.inputResourceInfoType) + assert.Equal(t, tt.want, actual) + }) + } +} + +func TestIsResourceConnectionEnv(t *testing.T) { + tests := []struct { + name string + input string + want bool + }{ + { + name: "valid", + input: "$resource.connection:db.postgres:password", + want: true, + }, + { + name: "invalid", + input: "$resource.connection:db.postgres:", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isResourceConnectionEnv(tt.input) + assert.Equal(t, tt.want, result) + }) + } +} + +func TestToResourceConnectionInfo(t *testing.T) { + tests := []struct { + name string + input string + wantResourceType ResourceType + wantResourceInfoType ResourceInfoType + }{ + { + name: "invalid input", + input: "$resource.connection:db.mysql::username", + wantResourceType: "", + wantResourceInfoType: "", + }, + { + name: "mysql username", + input: "$resource.connection:db.mysql:username", + wantResourceType: ResourceTypeDbMySQL, + wantResourceInfoType: ResourceInfoTypeUsername, + }, + { + name: "postgres password", + input: "$resource.connection:db.postgres:password", + wantResourceType: ResourceTypeDbPostgres, + wantResourceInfoType: ResourceInfoTypePassword, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resourceType, resourceInfoType := toResourceConnectionInfo(tt.input) + assert.Equal(t, tt.wantResourceType, resourceType) + assert.Equal(t, tt.wantResourceInfoType, resourceInfoType) + }) + } +} diff --git a/cli/azd/pkg/project/importer.go b/cli/azd/pkg/project/importer.go index 7262e7f85d9..3494d76b81c 100644 --- a/cli/azd/pkg/project/importer.go +++ b/cli/azd/pkg/project/importer.go @@ -6,7 +6,6 @@ package project import ( "context" "fmt" - "github.com/azure/azure-dev/cli/azd/pkg/input" "io/fs" "log" "os" @@ -20,13 +19,11 @@ import ( type ImportManager struct { dotNetImporter *DotNetImporter - console input.Console } -func NewImportManager(dotNetImporter *DotNetImporter, console input.Console) *ImportManager { +func NewImportManager(dotNetImporter *DotNetImporter) *ImportManager { return &ImportManager{ dotNetImporter: dotNetImporter, - console: console, } } @@ -170,7 +167,7 @@ func (im *ImportManager) ProjectInfrastructure(ctx context.Context, projectConfi composeEnabled := im.dotNetImporter.alphaFeatureManager.IsEnabled(featureCompose) if composeEnabled && len(projectConfig.Resources) > 0 { - return tempInfra(ctx, projectConfig, &im.console, &ctx) + return tempInfra(ctx, projectConfig, im.dotNetImporter.console) } if !composeEnabled && len(projectConfig.Resources) > 0 { @@ -212,7 +209,7 @@ func (im *ImportManager) SynthAllInfrastructure(ctx context.Context, projectConf composeEnabled := im.dotNetImporter.alphaFeatureManager.IsEnabled(featureCompose) if composeEnabled && len(projectConfig.Resources) > 0 { - return infraFsForProject(ctx, projectConfig, &im.console, &ctx) + return infraFsForProject(ctx, projectConfig, im.dotNetImporter.console) } if !composeEnabled && len(projectConfig.Resources) > 0 { diff --git a/cli/azd/pkg/project/importer_test.go b/cli/azd/pkg/project/importer_test.go index 03ab794d06f..420ee2a564b 100644 --- a/cli/azd/pkg/project/importer_test.go +++ b/cli/azd/pkg/project/importer_test.go @@ -6,7 +6,6 @@ package project import ( "context" _ "embed" - "github.com/azure/azure-dev/cli/azd/test/mocks/mockinput" "os" "path/filepath" "slices" @@ -44,7 +43,7 @@ func TestImportManagerHasService(t *testing.T) { lazyEnvManager: lazy.NewLazy(func() (environment.Manager, error) { return mockEnv, nil }), - }, mockinput.NewMockConsole()) + }) // has service r, e := manager.HasService(*mockContext.Context, &ProjectConfig{ @@ -86,7 +85,7 @@ func TestImportManagerHasServiceErrorNoMultipleServicesWithAppHost(t *testing.T) return mockEnv, nil }), hostCheck: make(map[string]hostCheckResult), - }, mockinput.NewMockConsole()) + }) mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { return strings.Contains(command, "dotnet") && @@ -139,7 +138,7 @@ func TestImportManagerHasServiceErrorAppHostMustTargetContainerApp(t *testing.T) return mockEnv, nil }), hostCheck: make(map[string]hostCheckResult), - }, mockinput.NewMockConsole()) + }) mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { return strings.Contains(command, "dotnet") && @@ -186,7 +185,7 @@ func TestImportManagerProjectInfrastructureDefaults(t *testing.T) { }), hostCheck: make(map[string]hostCheckResult), alphaFeatureManager: mockContext.AlphaFeaturesManager, - }, mockinput.NewMockConsole()) + }) // Get defaults and error b/c no infra found and no Aspire project r, e := manager.ProjectInfrastructure(*mockContext.Context, &ProjectConfig{}) @@ -235,7 +234,7 @@ func TestImportManagerProjectInfrastructure(t *testing.T) { return mockEnv, nil }), hostCheck: make(map[string]hostCheckResult), - }, mockinput.NewMockConsole()) + }) // Do not use defaults expectedDefaultFolder := "customFolder" @@ -317,7 +316,7 @@ func TestImportManagerProjectInfrastructureAspire(t *testing.T) { hostCheck: make(map[string]hostCheckResult), cache: make(map[manifestCacheKey]*apphost.Manifest), alphaFeatureManager: alpha.NewFeaturesManagerWithConfig(config.NewEmptyConfig()), - }, mockinput.NewMockConsole()) + }) // adding infra folder to test defaults err := os.Mkdir(DefaultPath, os.ModePerm) diff --git a/cli/azd/pkg/project/scaffold_gen.go b/cli/azd/pkg/project/scaffold_gen.go index 479e9ab8c9c..b86b49e54e5 100644 --- a/cli/azd/pkg/project/scaffold_gen.go +++ b/cli/azd/pkg/project/scaffold_gen.go @@ -21,14 +21,13 @@ import ( ) // Generates the in-memory contents of an `infra` directory. -func infraFs(_ context.Context, prjConfig *ProjectConfig, - console *input.Console, context *context.Context) (fs.FS, error) { +func infraFs(cxt context.Context, prjConfig *ProjectConfig, console input.Console) (fs.FS, error) { t, err := scaffold.Load() if err != nil { return nil, fmt.Errorf("loading scaffold templates: %w", err) } - infraSpec, err := infraSpec(prjConfig, console, context) + infraSpec, err := infraSpec(prjConfig, console, cxt) if err != nil { return nil, fmt.Errorf("generating infrastructure spec: %w", err) } @@ -45,14 +44,13 @@ func infraFs(_ context.Context, prjConfig *ProjectConfig, func tempInfra( ctx context.Context, prjConfig *ProjectConfig, - console *input.Console, - context *context.Context) (*Infra, error) { + console input.Console) (*Infra, error) { tmpDir, err := os.MkdirTemp("", "azd-infra") if err != nil { return nil, fmt.Errorf("creating temporary directory: %w", err) } - files, err := infraFs(ctx, prjConfig, console, context) + files, err := infraFs(ctx, prjConfig, console) if err != nil { return nil, err } @@ -95,8 +93,8 @@ func tempInfra( // Generates the filesystem of all infrastructure files to be placed, rooted at the project directory. // The content only includes `./infra` currently. func infraFsForProject(ctx context.Context, prjConfig *ProjectConfig, - console *input.Console, context *context.Context) (fs.FS, error) { - infraFS, err := infraFs(ctx, prjConfig, console, context) + console input.Console) (fs.FS, error) { + infraFS, err := infraFs(ctx, prjConfig, console) if err != nil { return nil, err } @@ -137,7 +135,7 @@ func infraFsForProject(ctx context.Context, prjConfig *ProjectConfig, } func infraSpec(projectConfig *ProjectConfig, - console *input.Console, context *context.Context) (*scaffold.InfraSpec, error) { + console input.Console, ctx context.Context) (*scaffold.InfraSpec, error) { infraSpec := scaffold.InfraSpec{} for _, resource := range projectConfig.Resources { switch resource.Type { @@ -233,7 +231,7 @@ func infraSpec(projectConfig *ProjectConfig, return nil, err } - err = printHintsAboutUses(&infraSpec, projectConfig, console, context) + err = printEnvListAboutUses(&infraSpec, projectConfig, console, ctx) if err != nil { return nil, err } @@ -263,27 +261,63 @@ func mapUses(infraSpec *scaffold.InfraSpec, projectConfig *ProjectConfig) error switch usedResource.Type { case ResourceTypeDbPostgres: userSpec.DbPostgres = infraSpec.DbPostgres + err := addUsageByEnv(infraSpec, userSpec, usedResource) + if err != nil { + return err + } case ResourceTypeDbMySQL: userSpec.DbMySql = infraSpec.DbMySql + err := addUsageByEnv(infraSpec, userSpec, usedResource) + if err != nil { + return err + } case ResourceTypeDbRedis: userSpec.DbRedis = infraSpec.DbRedis + err := addUsageByEnv(infraSpec, userSpec, usedResource) + if err != nil { + return err + } case ResourceTypeDbMongo: userSpec.DbCosmosMongo = infraSpec.DbCosmosMongo + err := addUsageByEnv(infraSpec, userSpec, usedResource) + if err != nil { + return err + } case ResourceTypeDbCosmos: userSpec.DbCosmos = infraSpec.DbCosmos + err := addUsageByEnv(infraSpec, userSpec, usedResource) + if err != nil { + return err + } case ResourceTypeMessagingServiceBus: userSpec.AzureServiceBus = infraSpec.AzureServiceBus + err := addUsageByEnv(infraSpec, userSpec, usedResource) + if err != nil { + return err + } case ResourceTypeMessagingEventHubs, ResourceTypeMessagingKafka: userSpec.AzureEventHubs = infraSpec.AzureEventHubs + err := addUsageByEnv(infraSpec, userSpec, usedResource) + if err != nil { + return err + } case ResourceTypeStorage: userSpec.AzureStorageAccount = infraSpec.AzureStorageAccount - case ResourceTypeHostContainerApp: - err := fulfillFrontendBackend(userSpec, usedResource, infraSpec) + err := addUsageByEnv(infraSpec, userSpec, usedResource) if err != nil { return err } case ResourceTypeOpenAiModel: userSpec.AIModels = append(userSpec.AIModels, scaffold.AIModelReference{Name: usedResource.Name}) + err := addUsageByEnv(infraSpec, userSpec, usedResource) + if err != nil { + return err + } + case ResourceTypeHostContainerApp: + err := fulfillFrontendBackend(userSpec, usedResource, infraSpec) + if err != nil { + return err + } default: return fmt.Errorf("resource (%s) uses (%s), but the type of (%s) is (%s), which is unsupported", userResource.Name, usedResource.Name, usedResource.Name, usedResource.Type) @@ -293,9 +327,44 @@ func mapUses(infraSpec *scaffold.InfraSpec, projectConfig *ProjectConfig) error return nil } -func printHintsAboutUses(infraSpec *scaffold.InfraSpec, projectConfig *ProjectConfig, - console *input.Console, - context *context.Context) error { +func getAuthType(infraSpec *scaffold.InfraSpec, resourceType ResourceType) (internal.AuthType, error) { + switch resourceType { + case ResourceTypeDbPostgres: + return infraSpec.DbPostgres.AuthType, nil + case ResourceTypeDbMySQL: + return infraSpec.DbMySql.AuthType, nil + case ResourceTypeDbRedis: + return internal.AuthTypePassword, nil + case ResourceTypeDbMongo, + ResourceTypeDbCosmos, + ResourceTypeOpenAiModel, + ResourceTypeHostContainerApp: + return internal.AuthTypeUserAssignedManagedIdentity, nil + case ResourceTypeMessagingServiceBus: + return infraSpec.AzureServiceBus.AuthType, nil + case ResourceTypeMessagingEventHubs, ResourceTypeMessagingKafka: + return infraSpec.AzureEventHubs.AuthType, nil + case ResourceTypeStorage: + return infraSpec.AzureStorageAccount.AuthType, nil + default: + return internal.AuthTypeUnspecified, fmt.Errorf("can not get authType, resource type: %s", resourceType) + } +} + +func addUsageByEnv(infraSpec *scaffold.InfraSpec, userSpec *scaffold.ServiceSpec, usedResource *ResourceConfig) error { + envs, err := getResourceConnectionEnvs(usedResource, infraSpec) + if err != nil { + return err + } + userSpec.Envs, err = mergeEnvWithDuplicationCheck(userSpec.Envs, envs) + if err != nil { + return err + } + return nil +} + +func printEnvListAboutUses(infraSpec *scaffold.InfraSpec, projectConfig *ProjectConfig, + console input.Console, ctx context.Context) error { for i := range infraSpec.Services { userSpec := &infraSpec.Services[i] userResourceName := userSpec.Name @@ -310,62 +379,40 @@ func printHintsAboutUses(infraSpec *scaffold.InfraSpec, projectConfig *ProjectCo return fmt.Errorf("in azure.yaml, (%s) uses (%s), but (%s) doesn't", userResourceName, usedResourceName, usedResourceName) } - if *console != nil { - (*console).Message(*context, fmt.Sprintf("CAUTION: In azure.yaml, '%s' uses '%s'. "+ - "After deployed, the 'uses' is achieved by providing these environment variables: ", - userResourceName, usedResourceName)) - } + console.Message(ctx, fmt.Sprintf("\nInformation about environment variables:\n"+ + "In azure.yaml, '%s' uses '%s'. \n"+ + "The 'uses' relashipship is implemented by environment variables. \n"+ + "Please make sure your application used the right environment variable. \n"+ + "Here is the list of environment variables: ", + userResourceName, usedResourceName)) switch usedResource.Type { - case ResourceTypeDbPostgres: - err := printHintsAboutUsePostgres(userSpec.DbPostgres.AuthType, console, context) + case ResourceTypeDbPostgres, // do nothing. todo: add all other types + ResourceTypeDbMySQL, + ResourceTypeDbRedis, + ResourceTypeDbMongo, + ResourceTypeDbCosmos, + ResourceTypeMessagingServiceBus, + ResourceTypeMessagingEventHubs, + ResourceTypeMessagingKafka, + ResourceTypeStorage: + variables, err := getResourceConnectionEnvs(usedResource, infraSpec) if err != nil { return err } - case ResourceTypeDbMySQL: - err := printHintsAboutUseMySql(userSpec.DbPostgres.AuthType, console, context) - if err != nil { - return err - } - case ResourceTypeDbRedis: - printHintsAboutUseRedis(console, context) - case ResourceTypeDbMongo: - printHintsAboutUseMongo(console, context) - case ResourceTypeDbCosmos: - printHintsAboutUseCosmos(console, context) - case ResourceTypeMessagingServiceBus: - err := printHintsAboutUseServiceBus(userSpec.AzureServiceBus.IsJms, - userSpec.AzureServiceBus.AuthType, console, context) - if err != nil { - return err - } - case ResourceTypeMessagingEventHubs, ResourceTypeMessagingKafka: - err := printHintsAboutUseEventHubs(userSpec.AzureEventHubs.UseKafka, - userSpec.AzureEventHubs.AuthType, userSpec.AzureEventHubs.SpringBootVersion, console, context) - if err != nil { - return err - } - case ResourceTypeStorage: - err := printHintsAboutUseStorageAccount(userSpec.AzureStorageAccount.AuthType, console, context) - if err != nil { - return err + for _, variable := range variables { + console.Message(ctx, fmt.Sprintf(" %s=xxx", variable.Name)) } case ResourceTypeHostContainerApp: - printHintsAboutUseHostContainerApp(userResourceName, usedResourceName, console, context) - case ResourceTypeOpenAiModel: - printHintsAboutUseOpenAiModel(console, context) + printHintsAboutUseHostContainerApp(userResourceName, usedResourceName, console, ctx) default: return fmt.Errorf("resource (%s) uses (%s), but the type of (%s) is (%s), "+ "which is doen't add necessary environment variable", userResource.Name, usedResource.Name, usedResource.Name, usedResource.Type) } - if *console != nil { - (*console).Message(*context, "Please make sure your application used the right environment variable name.\n") - } - + console.Message(ctx, "\n") } } return nil - } func handleContainerAppProps( @@ -398,7 +445,11 @@ func handleContainerAppProps( // Here, DB_HOST is not a secret, but DB_SECRET is. And yet, DB_HOST will be marked as a secret. // This is a limitation of the current implementation, but it's safer to mark both as secrets above. evaluatedValue := genBicepParamsFromEnvSubst(value, isSecret, infraSpec) - serviceSpec.Env[envVar.Name] = evaluatedValue + err := addNewEnvironmentVariable(serviceSpec, envVar.Name, evaluatedValue) + if err != nil { + return err + } + return nil } port := props.Port @@ -442,6 +493,7 @@ func setParameter(spec *scaffold.InfraSpec, name string, value string, isSecret // // If the string is a literal, it is returned as is. // If isSecret is true, the parameter is marked as a secret. +// The returned value is string, all expression inside are wrapped by "${}". func genBicepParamsFromEnvSubst( s string, isSecret bool, @@ -456,16 +508,16 @@ func genBicepParamsFromEnvSubst( var result string if len(names) == 0 { - // literal string with no expressions, quote the value as a Bicep string - result = "'" + s + "'" + // literal string with no expressions + result = s } else if len(names) == 1 { // single expression, return the bicep parameter name to reference the expression - result = scaffold.BicepName(names[0]) + result = "${" + scaffold.BicepName(names[0]) + "}" } else { // multiple expressions // construct the string with all expressions replaced by parameter references as a Bicep interpolated string previous := 0 - result = "'" + result = "" for i, loc := range locations { // replace each expression with references by variable name result += s[previous:loc.start] @@ -474,7 +526,6 @@ func genBicepParamsFromEnvSubst( result += "}" previous = loc.stop + 1 } - result += "'" } return result @@ -509,187 +560,13 @@ func getServiceSpecByName(infraSpec *scaffold.InfraSpec, name string) *scaffold. return nil } -func printHintsAboutUsePostgres(authType internal.AuthType, - console *input.Console, context *context.Context) error { - if *console == nil { - return nil - } - (*console).Message(*context, "POSTGRES_HOST=xxx") - (*console).Message(*context, "POSTGRES_DATABASE=xxx") - (*console).Message(*context, "POSTGRES_PORT=xxx") - (*console).Message(*context, "spring.datasource.url=xxx") - (*console).Message(*context, "spring.datasource.username=xxx") - if authType == internal.AuthTypePassword { - (*console).Message(*context, "POSTGRES_URL=xxx") - (*console).Message(*context, "POSTGRES_USERNAME=xxx") - (*console).Message(*context, "POSTGRES_PASSWORD=xxx") - (*console).Message(*context, "spring.datasource.password=xxx") - } else if authType == internal.AuthTypeUserAssignedManagedIdentity { - (*console).Message(*context, "spring.datasource.azure.passwordless-enabled=true") - (*console).Message(*context, "CAUTION: To make sure passwordless work well in your spring boot application, ") - (*console).Message(*context, "make sure the following 2 things:") - (*console).Message(*context, "1. Add required dependency: spring-cloud-azure-starter-jdbc-postgresql.") - (*console).Message(*context, "2. Delete property 'spring.datasource.password' in your property file.") - (*console).Message(*context, "Refs: https://learn.microsoft.com/en-us/azure/service-connector/") - (*console).Message(*context, "how-to-integrate-mysql?tabs=springBoot#sample-code-1") - } else { - return fmt.Errorf("unsupported auth type for PostgreSQL. Supported types: %s, %s", - internal.GetAuthTypeDescription(internal.AuthTypePassword), - internal.GetAuthTypeDescription(internal.AuthTypeUserAssignedManagedIdentity)) - } - return nil -} - -func printHintsAboutUseMySql(authType internal.AuthType, - console *input.Console, context *context.Context) error { - if *console == nil { - return nil - } - (*console).Message(*context, "MYSQL_HOST=xxx") - (*console).Message(*context, "MYSQL_DATABASE=xxx") - (*console).Message(*context, "MYSQL_PORT=xxx") - (*console).Message(*context, "spring.datasource.url=xxx") - (*console).Message(*context, "spring.datasource.username=xxx") - if authType == internal.AuthTypePassword { - (*console).Message(*context, "MYSQL_URL=xxx") - (*console).Message(*context, "MYSQL_USERNAME=xxx") - (*console).Message(*context, "MYSQL_PASSWORD=xxx") - (*console).Message(*context, "spring.datasource.password=xxx") - } else if authType == internal.AuthTypeUserAssignedManagedIdentity { - (*console).Message(*context, "spring.datasource.azure.passwordless-enabled=true") - (*console).Message(*context, "CAUTION: To make sure passwordless work well in your spring boot application, ") - (*console).Message(*context, "Make sure the following 2 things:") - (*console).Message(*context, "1. Add required dependency: spring-cloud-azure-starter-jdbc-postgresql.") - (*console).Message(*context, "2. Delete property 'spring.datasource.password' in your property file.") - (*console).Message(*context, "Refs: https://learn.microsoft.com/en-us/azure/service-connector/how-to-integrate-postgres?tabs=springBoot#sample-code-1") - } else { - return fmt.Errorf("unsupported auth type for MySql. Supported types are: %s, %s", - internal.GetAuthTypeDescription(internal.AuthTypePassword), - internal.GetAuthTypeDescription(internal.AuthTypeUserAssignedManagedIdentity)) - } - return nil -} - -func printHintsAboutUseRedis(console *input.Console, context *context.Context) { - if *console == nil { - return - } - (*console).Message(*context, "REDIS_HOST=xxx") - (*console).Message(*context, "REDIS_PORT=xxx") - (*console).Message(*context, "REDIS_URL=xxx") - (*console).Message(*context, "REDIS_ENDPOINT=xxx") - (*console).Message(*context, "REDIS_PASSWORD=xxx") - (*console).Message(*context, "spring.data.redis.url=xxx") -} - -func printHintsAboutUseMongo(console *input.Console, context *context.Context) { - if *console == nil { - return - } - (*console).Message(*context, "MONGODB_URL=xxx") - (*console).Message(*context, "spring.data.mongodb.uri=xxx") - (*console).Message(*context, "spring.data.mongodb.database=xxx") -} - -func printHintsAboutUseCosmos(console *input.Console, context *context.Context) { - if *console == nil { - return - } - (*console).Message(*context, "spring.cloud.azure.cosmos.endpoint=xxx") - (*console).Message(*context, "spring.cloud.azure.cosmos.database=xxx") -} - -func printHintsAboutUseServiceBus(isJms bool, authType internal.AuthType, - console *input.Console, context *context.Context) error { - if *console == nil { - return nil - } - if !isJms { - (*console).Message(*context, "spring.cloud.azure.servicebus.namespace=xxx") - } - if authType == internal.AuthTypeUserAssignedManagedIdentity { - (*console).Message(*context, "spring.cloud.azure.servicebus.connection-string=''") - (*console).Message(*context, "spring.cloud.azure.servicebus.credential.managed-identity-enabled=true") - (*console).Message(*context, "spring.cloud.azure.servicebus.credential.client-id=xxx") - } else if authType == internal.AuthTypeConnectionString { - (*console).Message(*context, "spring.cloud.azure.servicebus.connection-string=xxx") - (*console).Message(*context, "spring.cloud.azure.servicebus.credential.managed-identity-enabled=false") - (*console).Message(*context, "spring.cloud.azure.eventhubs.credential.client-id=xxx") - } else { - return fmt.Errorf("unsupported auth type for Service Bus. Supported types are: %s, %s", - internal.GetAuthTypeDescription(internal.AuthTypeUserAssignedManagedIdentity), - internal.GetAuthTypeDescription(internal.AuthTypeConnectionString)) - } - return nil -} - -func printHintsAboutUseEventHubs(UseKafka bool, authType internal.AuthType, springBootVersion string, - console *input.Console, context *context.Context) error { - if *console == nil { - return nil - } - if !UseKafka { - (*console).Message(*context, "spring.cloud.azure.eventhubs.namespace=xxx") - } else { - (*console).Message(*context, "spring.cloud.stream.kafka.binder.brokers=xxx") - if strings.HasPrefix(springBootVersion, "2.") { - (*console).Message(*context, "spring.cloud.stream.binders.kafka.environment.spring.main.sources=com.azure.spring.cloud.autoconfigure.eventhubs.kafka.AzureEventHubsKafkaAutoConfiguration") - } else if strings.HasPrefix(springBootVersion, "3.") { - (*console).Message(*context, "spring.cloud.stream.binders.kafka.environment.spring.main.sources=com.azure.spring.cloud.autoconfigure.implementation.eventhubs.kafka.AzureEventHubsKafkaAutoConfiguration") - } - } - if authType == internal.AuthTypeUserAssignedManagedIdentity { - (*console).Message(*context, "spring.cloud.azure.eventhubs.connection-string=''") - (*console).Message(*context, "spring.cloud.azure.eventhubs.credential.managed-identity-enabled=true") - (*console).Message(*context, "spring.cloud.azure.eventhubs.credential.client-id=xxx") - } else if authType == internal.AuthTypeConnectionString { - (*console).Message(*context, "spring.cloud.azure.eventhubs.connection-string=xxx") - (*console).Message(*context, "spring.cloud.azure.eventhubs.credential.managed-identity-enabled=false") - (*console).Message(*context, "spring.cloud.azure.eventhubs.credential.client-id=xxx") - } else { - return fmt.Errorf("unsupported auth type for Event Hubs. Supported types: %s, %s", - internal.GetAuthTypeDescription(internal.AuthTypeUserAssignedManagedIdentity), - internal.GetAuthTypeDescription(internal.AuthTypeConnectionString)) - } - return nil -} - -func printHintsAboutUseStorageAccount(authType internal.AuthType, - console *input.Console, context *context.Context) error { - if *console == nil { - return nil - } - (*console).Message(*context, "spring.cloud.azure.eventhubs.processor.checkpoint-store.account-name=xxx") - if authType == internal.AuthTypeUserAssignedManagedIdentity { - (*console).Message(*context, "spring.cloud.azure.eventhubs.processor.checkpoint-store.connection-string=''") - (*console).Message(*context, "spring.cloud.azure.eventhubs.processor.checkpoint-store.credential.managed-identity-enabled=true") - (*console).Message(*context, "spring.cloud.azure.eventhubs.processor.checkpoint-store.credential.client-id=xxx") - } else if authType == internal.AuthTypeConnectionString { - (*console).Message(*context, "spring.cloud.azure.eventhubs.processor.checkpoint-store.connection-string=xxx") - (*console).Message(*context, "spring.cloud.azure.eventhubs.processor.checkpoint-store.credential.managed-identity-enabled=false") - (*console).Message(*context, "spring.cloud.azure.eventhubs.processor.checkpoint-store.credential.client-id=xxx") - } else { - return fmt.Errorf("unsupported auth type for Storage Account. Supported types: %s, %s", - internal.GetAuthTypeDescription(internal.AuthTypeUserAssignedManagedIdentity), - internal.GetAuthTypeDescription(internal.AuthTypeConnectionString)) - } - return nil -} - func printHintsAboutUseHostContainerApp(userResourceName string, usedResourceName string, - console *input.Console, context *context.Context) { - if *console == nil { - return - } - (*console).Message(*context, fmt.Sprintf("Environemnt variables in %s:", userResourceName)) - (*console).Message(*context, fmt.Sprintf("%s_BASE_URL=xxx", strings.ToUpper(usedResourceName))) - (*console).Message(*context, fmt.Sprintf("Environemnt variables in %s:", usedResourceName)) - (*console).Message(*context, fmt.Sprintf("%s_BASE_URL=xxx", strings.ToUpper(userResourceName))) -} - -func printHintsAboutUseOpenAiModel(console *input.Console, context *context.Context) { - if *console == nil { + console input.Console, ctx context.Context) { + if console == nil { return } - (*console).Message(*context, "AZURE_OPENAI_ENDPOINT") + console.Message(ctx, fmt.Sprintf("Environemnt variables in %s:", userResourceName)) + console.Message(ctx, fmt.Sprintf("%s_BASE_URL=xxx", strings.ToUpper(usedResourceName))) + console.Message(ctx, fmt.Sprintf("Environemnt variables in %s:", usedResourceName)) + console.Message(ctx, fmt.Sprintf("%s_BASE_URL=xxx", strings.ToUpper(userResourceName))) } diff --git a/cli/azd/pkg/project/scaffold_gen_environment_variables.go b/cli/azd/pkg/project/scaffold_gen_environment_variables.go new file mode 100644 index 00000000000..f02ea084f6c --- /dev/null +++ b/cli/azd/pkg/project/scaffold_gen_environment_variables.go @@ -0,0 +1,552 @@ +package project + +import ( + "fmt" + "github.com/azure/azure-dev/cli/azd/internal" + "github.com/azure/azure-dev/cli/azd/internal/scaffold" + "strings" +) + +func getResourceConnectionEnvs(usedResource *ResourceConfig, + infraSpec *scaffold.InfraSpec) ([]scaffold.Env, error) { + resourceType := usedResource.Type + authType, err := getAuthType(infraSpec, usedResource.Type) + if err != nil { + return []scaffold.Env{}, err + } + switch resourceType { + case ResourceTypeDbPostgres: + switch authType { + case internal.AuthTypePassword: + return []scaffold.Env{ + { + Name: "POSTGRES_USERNAME", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbPostgres, scaffold.ResourceInfoTypeUsername), + }, + { + Name: "POSTGRES_PASSWORD", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbPostgres, scaffold.ResourceInfoTypePassword), + }, + { + Name: "POSTGRES_HOST", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbPostgres, scaffold.ResourceInfoTypeHost), + }, + { + Name: "POSTGRES_DATABASE", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbPostgres, scaffold.ResourceInfoTypeDatabaseName), + }, + { + Name: "POSTGRES_PORT", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbPostgres, scaffold.ResourceInfoTypePort), + }, + { + Name: "POSTGRES_URL", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbPostgres, scaffold.ResourceInfoTypeUrl), + }, + { + Name: "spring.datasource.url", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbPostgres, scaffold.ResourceInfoTypeJdbcUrl), + }, + { + Name: "spring.datasource.username", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbPostgres, scaffold.ResourceInfoTypeUsername), + }, + { + Name: "spring.datasource.password", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbPostgres, scaffold.ResourceInfoTypePassword), + }, + }, nil + case internal.AuthTypeUserAssignedManagedIdentity: + return []scaffold.Env{ + { + Name: "POSTGRES_USERNAME", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbPostgres, scaffold.ResourceInfoTypeUsername), + }, + { + Name: "POSTGRES_HOST", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbPostgres, scaffold.ResourceInfoTypeHost), + }, + { + Name: "POSTGRES_DATABASE", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbPostgres, scaffold.ResourceInfoTypeDatabaseName), + }, + { + Name: "POSTGRES_PORT", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbPostgres, scaffold.ResourceInfoTypePort), + }, + { + Name: "spring.datasource.url", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbPostgres, scaffold.ResourceInfoTypeJdbcUrl), + }, + { + Name: "spring.datasource.username", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbPostgres, scaffold.ResourceInfoTypeUsername), + }, + { + Name: "spring.datasource.azure.passwordless-enabled", + Value: "true", + }, + }, nil + default: + return []scaffold.Env{}, unsupportedAuthTypeError(resourceType, authType) + } + case ResourceTypeDbMySQL: + switch authType { + case internal.AuthTypePassword: + return []scaffold.Env{ + { + Name: "MYSQL_USERNAME", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbMySQL, scaffold.ResourceInfoTypeUsername), + }, + { + Name: "MYSQL_PASSWORD", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbMySQL, scaffold.ResourceInfoTypePassword), + }, + { + Name: "MYSQL_HOST", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbMySQL, scaffold.ResourceInfoTypeHost), + }, + { + Name: "MYSQL_DATABASE", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbMySQL, scaffold.ResourceInfoTypeDatabaseName), + }, + { + Name: "MYSQL_PORT", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbMySQL, scaffold.ResourceInfoTypePort), + }, + { + Name: "MYSQL_URL", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbMySQL, scaffold.ResourceInfoTypeUrl), + }, + { + Name: "spring.datasource.url", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbMySQL, scaffold.ResourceInfoTypeJdbcUrl), + }, + { + Name: "spring.datasource.username", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbMySQL, scaffold.ResourceInfoTypeUsername), + }, + { + Name: "spring.datasource.password", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbMySQL, scaffold.ResourceInfoTypePassword), + }, + }, nil + case internal.AuthTypeUserAssignedManagedIdentity: + return []scaffold.Env{ + { + Name: "MYSQL_USERNAME", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbMySQL, scaffold.ResourceInfoTypeUsername), + }, + { + Name: "MYSQL_HOST", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbMySQL, scaffold.ResourceInfoTypeHost), + }, + { + Name: "MYSQL_PORT", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbMySQL, scaffold.ResourceInfoTypePort), + }, + { + Name: "MYSQL_DATABASE", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbMySQL, scaffold.ResourceInfoTypeDatabaseName), + }, + { + Name: "spring.datasource.url", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbMySQL, scaffold.ResourceInfoTypeJdbcUrl), + }, + { + Name: "spring.datasource.username", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbMySQL, scaffold.ResourceInfoTypeUsername), + }, + { + Name: "spring.datasource.azure.passwordless-enabled", + Value: "true", + }, + }, nil + default: + return []scaffold.Env{}, unsupportedAuthTypeError(resourceType, authType) + } + case ResourceTypeDbRedis: + switch authType { + case internal.AuthTypePassword: + return []scaffold.Env{ + { + Name: "REDIS_HOST", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbRedis, scaffold.ResourceInfoTypeHost), + }, + { + Name: "REDIS_PORT", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbRedis, scaffold.ResourceInfoTypePort), + }, + { + Name: "REDIS_ENDPOINT", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbRedis, scaffold.ResourceInfoTypeEndpoint), + }, + { + Name: "REDIS_URL", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbRedis, scaffold.ResourceInfoTypeUrl), + }, + { + Name: "REDIS_PASSWORD", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbRedis, scaffold.ResourceInfoTypePassword), + }, + { + Name: "spring.data.redis.url", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbRedis, scaffold.ResourceInfoTypeUrl), + }, + }, nil + default: + return []scaffold.Env{}, unsupportedAuthTypeError(resourceType, authType) + } + case ResourceTypeDbMongo: + switch authType { + case internal.AuthTypeUserAssignedManagedIdentity: + return []scaffold.Env{ + { + Name: "MONGODB_URL", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbMongo, scaffold.ResourceInfoTypeUrl), + }, + { + Name: "spring.data.mongodb.uri", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbMongo, scaffold.ResourceInfoTypeUrl), + }, + { + Name: "spring.data.mongodb.database", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbMongo, scaffold.ResourceInfoTypeDatabaseName), + }, + }, nil + default: + return []scaffold.Env{}, unsupportedAuthTypeError(resourceType, authType) + } + case ResourceTypeDbCosmos: + switch authType { + case internal.AuthTypeUserAssignedManagedIdentity: + return []scaffold.Env{ + { + Name: "spring.cloud.azure.cosmos.endpoint", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbCosmos, scaffold.ResourceInfoTypeEndpoint), + }, + { + Name: "spring.cloud.azure.cosmos.database", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeDbCosmos, scaffold.ResourceInfoTypeDatabaseName), + }, + }, nil + default: + return []scaffold.Env{}, unsupportedAuthTypeError(resourceType, authType) + } + case ResourceTypeMessagingServiceBus: + if infraSpec.AzureServiceBus.IsJms { + switch authType { + case internal.AuthTypeUserAssignedManagedIdentity: + return []scaffold.Env{ + { + Name: "spring.jms.servicebus.pricing-tier", + Value: "premium", + }, + { + Name: "spring.jms.servicebus.passwordless-enabled", + Value: "true", + }, + { + Name: "spring.jms.servicebus.credential.managed-identity-enabled", + Value: "true", + }, + { + Name: "spring.jms.servicebus.credential.client-id", + Value: scaffold.PlaceHolderForServiceIdentityClientId(), + }, + { + Name: "spring.jms.servicebus.namespace", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeMessagingServiceBus, scaffold.ResourceInfoTypeNamespace), + }, + { + Name: "spring.jms.servicebus.connection-string", + Value: "", + }, + }, nil + case internal.AuthTypeConnectionString: + return []scaffold.Env{ + { + Name: "spring.jms.servicebus.pricing-tier", + Value: "premium", + }, + { + Name: "spring.jms.servicebus.connection-string", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeMessagingServiceBus, scaffold.ResourceInfoTypeConnectionString), + }, + { + Name: "spring.jms.servicebus.passwordless-enabled", + Value: "false", + }, + { + Name: "spring.jms.servicebus.credential.managed-identity-enabled", + Value: "false", + }, + { + Name: "spring.jms.servicebus.credential.client-id", + Value: "", + }, + { + Name: "spring.jms.servicebus.namespace", + Value: "", + }, + }, nil + default: + return []scaffold.Env{}, unsupportedResourceTypeError(resourceType) + } + } else { + // service bus, not jms + switch authType { + case internal.AuthTypeUserAssignedManagedIdentity: + return []scaffold.Env{ + // Not add this: spring.cloud.azure.servicebus.connection-string = "" + // because of this: https://github.com/Azure/azure-sdk-for-java/issues/42880 + { + Name: "spring.cloud.azure.servicebus.credential.managed-identity-enabled", + Value: "true", + }, + { + Name: "spring.cloud.azure.servicebus.credential.client-id", + Value: scaffold.PlaceHolderForServiceIdentityClientId(), + }, + { + Name: "spring.cloud.azure.servicebus.namespace", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeMessagingServiceBus, scaffold.ResourceInfoTypeNamespace), + }, + }, nil + case internal.AuthTypeConnectionString: + return []scaffold.Env{ + { + Name: "spring.cloud.azure.servicebus.namespace", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeMessagingServiceBus, scaffold.ResourceInfoTypeNamespace), + }, + { + Name: "spring.cloud.azure.servicebus.connection-string", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeMessagingServiceBus, scaffold.ResourceInfoTypeConnectionString), + }, + { + Name: "spring.cloud.azure.servicebus.credential.managed-identity-enabled", + Value: "false", + }, + { + Name: "spring.cloud.azure.servicebus.credential.client-id", + Value: "", + }, + }, nil + default: + return []scaffold.Env{}, unsupportedResourceTypeError(resourceType) + } + } + case ResourceTypeMessagingKafka: + // event hubs for kafka + var springBootVersionDecidedInformation []scaffold.Env + if strings.HasPrefix(infraSpec.AzureEventHubs.SpringBootVersion, "2.") { + springBootVersionDecidedInformation = []scaffold.Env{ + { + Name: "spring.cloud.stream.binders.kafka.environment.spring.main.sources", + Value: "com.azure.spring.cloud.autoconfigure.eventhubs.kafka.AzureEventHubsKafkaAutoConfiguration", + }, + } + } else { + springBootVersionDecidedInformation = []scaffold.Env{ + { + Name: "spring.cloud.stream.binders.kafka.environment.spring.main.sources", + Value: "com.azure.spring.cloud.autoconfigure.implementation.eventhubs.kafka.AzureEventHubsKafkaAutoConfiguration", + }, + } + } + var commonInformation []scaffold.Env + switch authType { + case internal.AuthTypeUserAssignedManagedIdentity: + commonInformation = []scaffold.Env{ + // Not add this: spring.cloud.azure.eventhubs.connection-string = "" + // because of this: https://github.com/Azure/azure-sdk-for-java/issues/42880 + { + Name: "spring.cloud.stream.kafka.binder.brokers", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeMessagingKafka, scaffold.ResourceInfoTypeEndpoint), + }, + { + Name: "spring.cloud.azure.eventhubs.credential.managed-identity-enabled", + Value: "true", + }, + { + Name: "spring.cloud.azure.eventhubs.credential.client-id", + Value: scaffold.PlaceHolderForServiceIdentityClientId(), + }, + } + case internal.AuthTypeConnectionString: + commonInformation = []scaffold.Env{ + { + Name: "spring.cloud.stream.kafka.binder.brokers", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeMessagingKafka, scaffold.ResourceInfoTypeEndpoint), + }, + { + Name: "spring.cloud.azure.eventhubs.connection-string", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeMessagingKafka, scaffold.ResourceInfoTypeConnectionString), + }, + { + Name: "spring.cloud.azure.eventhubs.credential.managed-identity-enabled", + Value: "false", + }, + { + Name: "spring.cloud.azure.eventhubs.credential.client-id", + Value: "", + }, + } + default: + return []scaffold.Env{}, unsupportedAuthTypeError(resourceType, authType) + } + return mergeEnvWithDuplicationCheck(springBootVersionDecidedInformation, commonInformation) + case ResourceTypeMessagingEventHubs: + switch authType { + case internal.AuthTypeUserAssignedManagedIdentity: + return []scaffold.Env{ + // Not add this: spring.cloud.azure.eventhubs.connection-string = "" + // because of this: https://github.com/Azure/azure-sdk-for-java/issues/42880 + { + Name: "spring.cloud.azure.eventhubs.credential.managed-identity-enabled", + Value: "true", + }, + { + Name: "spring.cloud.azure.eventhubs.credential.client-id", + Value: scaffold.PlaceHolderForServiceIdentityClientId(), + }, + { + Name: "spring.cloud.azure.eventhubs.namespace", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeMessagingEventHubs, scaffold.ResourceInfoTypeNamespace), + }, + }, nil + case internal.AuthTypeConnectionString: + return []scaffold.Env{ + { + Name: "spring.cloud.azure.eventhubs.namespace", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeMessagingEventHubs, scaffold.ResourceInfoTypeNamespace), + }, + { + Name: "spring.cloud.azure.eventhubs.connection-string", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeMessagingEventHubs, scaffold.ResourceInfoTypeConnectionString), + }, + { + Name: "spring.cloud.azure.eventhubs.credential.managed-identity-enabled", + Value: "false", + }, + { + Name: "spring.cloud.azure.eventhubs.credential.client-id", + Value: "", + }, + }, nil + default: + return []scaffold.Env{}, unsupportedResourceTypeError(resourceType) + } + case ResourceTypeStorage: + switch authType { + case internal.AuthTypeUserAssignedManagedIdentity: + return []scaffold.Env{ + { + Name: "spring.cloud.azure.eventhubs.processor.checkpoint-store.account-name", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeStorage, scaffold.ResourceInfoTypeAccountName), + }, + { + Name: "spring.cloud.azure.eventhubs.processor.checkpoint-store.credential.managed-identity-enabled", + Value: "true", + }, + { + Name: "spring.cloud.azure.eventhubs.processor.checkpoint-store.credential.client-id", + Value: scaffold.PlaceHolderForServiceIdentityClientId(), + }, + { + Name: "spring.cloud.azure.eventhubs.processor.checkpoint-store.connection-string", + Value: "", + }, + }, nil + case internal.AuthTypeConnectionString: + return []scaffold.Env{ + { + Name: "spring.cloud.azure.eventhubs.processor.checkpoint-store.account-name", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeStorage, scaffold.ResourceInfoTypeAccountName), + }, + { + Name: "spring.cloud.azure.eventhubs.processor.checkpoint-store.connection-string", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeStorage, scaffold.ResourceInfoTypeConnectionString), + }, + { + Name: "spring.cloud.azure.eventhubs.processor.checkpoint-store.credential.managed-identity-enabled", + Value: "false", + }, + { + Name: "spring.cloud.azure.eventhubs.processor.checkpoint-store.credential.client-id", + Value: "", + }, + }, nil + default: + return []scaffold.Env{}, unsupportedResourceTypeError(resourceType) + } + case ResourceTypeOpenAiModel: + switch authType { + case internal.AuthTypeUserAssignedManagedIdentity: + return []scaffold.Env{ + { + Name: "AZURE_OPENAI_ENDPOINT", + Value: scaffold.ToResourceConnectionEnv(scaffold.ResourceTypeOpenAiModel, scaffold.ResourceInfoTypeEndpoint), + }, + }, nil + default: + return []scaffold.Env{}, unsupportedResourceTypeError(resourceType) + } + case ResourceTypeHostContainerApp: // todo improve this and delete Frontend and Backend in scaffold.ServiceSpec + switch authType { + case internal.AuthTypeUserAssignedManagedIdentity: + return []scaffold.Env{}, nil + default: + return []scaffold.Env{}, unsupportedAuthTypeError(resourceType, authType) + } + default: + return []scaffold.Env{}, unsupportedResourceTypeError(resourceType) + } +} + +func unsupportedResourceTypeError(resourceType ResourceType) error { + return fmt.Errorf("unsupported resource type, resourceType = %s", resourceType) +} + +func unsupportedAuthTypeError(resourceType ResourceType, authType internal.AuthType) error { + return fmt.Errorf("unsupported auth type, resourceType = %s, authType = %s", resourceType, authType) +} + +func mergeEnvWithDuplicationCheck(a []scaffold.Env, + b []scaffold.Env) ([]scaffold.Env, error) { + ab := append(a, b...) + var result []scaffold.Env + seenName := make(map[string]scaffold.Env) + for _, value := range ab { + if existingValue, exist := seenName[value.Name]; exist { + if value != existingValue { + return []scaffold.Env{}, duplicatedEnvError(existingValue, value) + } + } else { + seenName[value.Name] = value + result = append(result, value) + } + } + return result, nil +} + +func addNewEnvironmentVariable(serviceSpec *scaffold.ServiceSpec, name string, value string) error { + merged, err := mergeEnvWithDuplicationCheck(serviceSpec.Envs, + []scaffold.Env{ + { + Name: name, + Value: value, + }, + }, + ) + if err != nil { + return err + } + serviceSpec.Envs = merged + return nil +} + +func duplicatedEnvError(existingValue scaffold.Env, newValue scaffold.Env) error { + return fmt.Errorf("duplicated environment variable. existingValue = %s, newValue = %s", + existingValue, newValue) +} diff --git a/cli/azd/pkg/project/scaffold_gen_environment_variables_test.go b/cli/azd/pkg/project/scaffold_gen_environment_variables_test.go new file mode 100644 index 00000000000..6bd79a94c44 --- /dev/null +++ b/cli/azd/pkg/project/scaffold_gen_environment_variables_test.go @@ -0,0 +1,92 @@ +package project + +import ( + "fmt" + "github.com/azure/azure-dev/cli/azd/internal/scaffold" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestMergeEnvWithDuplicationCheck(t *testing.T) { + var empty []scaffold.Env + name1Value1 := []scaffold.Env{ + { + Name: "name1", + Value: "value1", + }, + } + name1Value2 := []scaffold.Env{ + { + Name: "name1", + Value: "value2", + }, + } + name2Value2 := []scaffold.Env{ + { + Name: "name2", + Value: "value2", + }, + } + name1Value1Name2Value2 := []scaffold.Env{ + { + Name: "name1", + Value: "value1", + }, + { + Name: "name2", + Value: "value2", + }, + } + + tests := []struct { + name string + a []scaffold.Env + b []scaffold.Env + wantEnv []scaffold.Env + wantError error + }{ + { + name: "2 empty array", + a: empty, + b: empty, + wantEnv: empty, + wantError: nil, + }, + { + name: "one is empty, another is not", + a: empty, + b: name1Value1, + wantEnv: name1Value1, + wantError: nil, + }, + { + name: "no duplication", + a: name1Value1, + b: name2Value2, + wantEnv: name1Value1Name2Value2, + wantError: nil, + }, + { + name: "duplicated name but same value", + a: name1Value1, + b: name1Value1, + wantEnv: name1Value1, + wantError: nil, + }, + { + name: "duplicated name, different value", + a: name1Value1, + b: name1Value2, + wantEnv: []scaffold.Env{}, + wantError: fmt.Errorf("duplicated environment variable. existingValue = %s, newValue = %s", + name1Value1[0], name1Value2[0]), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + env, err := mergeEnvWithDuplicationCheck(tt.a, tt.b) + assert.Equal(t, tt.wantEnv, env) + assert.Equal(t, tt.wantError, err) + }) + } +} diff --git a/cli/azd/pkg/project/scaffold_gen_test.go b/cli/azd/pkg/project/scaffold_gen_test.go index 85cf4125075..a3c11a38119 100644 --- a/cli/azd/pkg/project/scaffold_gen_test.go +++ b/cli/azd/pkg/project/scaffold_gen_test.go @@ -18,23 +18,23 @@ func Test_genBicepParamsFromEnvSubst(t *testing.T) { want string wantParams []scaffold.Parameter }{ - {"foo", false, "'foo'", nil}, - {"${MY_VAR}", false, "myVar", []scaffold.Parameter{{Name: "myVar", Value: "${MY_VAR}", Type: "string"}}}, + {"foo", false, "foo", nil}, + {"${MY_VAR}", false, "${myVar}", []scaffold.Parameter{{Name: "myVar", Value: "${MY_VAR}", Type: "string"}}}, - {"${MY_SECRET}", true, "mySecret", + {"${MY_SECRET}", true, "${mySecret}", []scaffold.Parameter{ {Name: "mySecret", Value: "${MY_SECRET}", Type: "string", Secret: true}}}, - {"Hello, ${world:=okay}!", false, "world", + {"Hello, ${world:=okay}!", false, "${world}", []scaffold.Parameter{ {Name: "world", Value: "${world:=okay}", Type: "string"}}}, - {"${CAT} and ${DOG}", false, "'${cat} and ${dog}'", + {"${CAT} and ${DOG}", false, "${cat} and ${dog}", []scaffold.Parameter{ {Name: "cat", Value: "${CAT}", Type: "string"}, {Name: "dog", Value: "${DOG}", Type: "string"}}}, - {"${DB_HOST:='local'}:${DB_USERNAME:='okay'}", true, "'${dbHost}:${dbUsername}'", + {"${DB_HOST:='local'}:${DB_USERNAME:='okay'}", true, "${dbHost}:${dbUsername}", []scaffold.Parameter{ {Name: "dbHost", Value: "${DB_HOST:='local'}", Type: "string", Secret: true}, {Name: "dbUsername", Value: "${DB_USERNAME:='okay'}", Type: "string", Secret: true}}}, diff --git a/cli/azd/resources/scaffold/base/modules/set-event-hubs-namespace-connection-string.bicep b/cli/azd/resources/scaffold/base/modules/set-event-hubs-namespace-connection-string.bicep index 7eee8d73cdc..64245640096 100644 --- a/cli/azd/resources/scaffold/base/modules/set-event-hubs-namespace-connection-string.bicep +++ b/cli/azd/resources/scaffold/base/modules/set-event-hubs-namespace-connection-string.bicep @@ -17,3 +17,5 @@ resource connectionStringSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = value: listKeys(concat(resourceId('Microsoft.EventHub/namespaces', eventHubsNamespaceName), '/AuthorizationRules/RootManageSharedAccessKey'), eventHubsNamespace.apiVersion).primaryConnectionString } } + +output keyVaultUrl string = 'https://${keyVaultName}${environment().suffixes.keyvaultDns}/secrets/${connectionStringSecretName}' diff --git a/cli/azd/resources/scaffold/base/modules/set-redis-conn.bicep b/cli/azd/resources/scaffold/base/modules/set-redis-conn.bicep index 813f96fbcbf..fbe41132a20 100644 --- a/cli/azd/resources/scaffold/base/modules/set-redis-conn.bicep +++ b/cli/azd/resources/scaffold/base/modules/set-redis-conn.bicep @@ -27,3 +27,6 @@ resource urlSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { } } +output keyVaultUrlForPass string = 'https://${keyVaultName}${environment().suffixes.keyvaultDns}/secrets/${passwordSecretName}' +output keyVaultUrlForUrl string = 'https://${keyVaultName}${environment().suffixes.keyvaultDns}/secrets/${urlSecretName}' + diff --git a/cli/azd/resources/scaffold/base/modules/set-servicebus-namespace-connection-string.bicep b/cli/azd/resources/scaffold/base/modules/set-servicebus-namespace-connection-string.bicep index 1152b5dcc12..b58a707370d 100644 --- a/cli/azd/resources/scaffold/base/modules/set-servicebus-namespace-connection-string.bicep +++ b/cli/azd/resources/scaffold/base/modules/set-servicebus-namespace-connection-string.bicep @@ -17,3 +17,5 @@ resource serviceBusConnectionStringSecret 'Microsoft.KeyVault/vaults/secrets@202 value: listKeys(concat(resourceId('Microsoft.ServiceBus/namespaces', serviceBusNamespaceName), '/AuthorizationRules/RootManageSharedAccessKey'), serviceBusNamespace.apiVersion).primaryConnectionString } } + +output keyVaultUrl string = 'https://${keyVaultName}${environment().suffixes.keyvaultDns}/secrets/${connectionStringSecretName}' diff --git a/cli/azd/resources/scaffold/base/modules/set-storage-account-connection-string.bicep b/cli/azd/resources/scaffold/base/modules/set-storage-account-connection-string.bicep index 2b04668f17b..6e0a7da7912 100644 --- a/cli/azd/resources/scaffold/base/modules/set-storage-account-connection-string.bicep +++ b/cli/azd/resources/scaffold/base/modules/set-storage-account-connection-string.bicep @@ -17,3 +17,5 @@ resource connectionStringSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccount.listKeys().keys[0].value};EndpointSuffix=${environment().suffixes.storage}' } } + +output keyVaultUrl string = 'https://${keyVaultName}${environment().suffixes.keyvaultDns}/secrets/${connectionStringSecretName}' diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index cf949011783..d011af5fa22 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -75,6 +75,7 @@ module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.4.5 {{- end}} {{- if .DbCosmosMongo}} +var mongoDatabaseName = '{{ .DbCosmosMongo.DatabaseName }}' module cosmos 'br/public:avm/res/document-db/database-account:0.8.1' = { name: 'cosmos' params: { @@ -93,13 +94,11 @@ module cosmos 'br/public:avm/res/document-db/database-account:0.8.1' = { virtualNetworkRules: [] publicNetworkAccess: 'Enabled' } - {{- if .DbCosmosMongo.DatabaseName}} mongodbDatabases: [ { - name: '{{ .DbCosmosMongo.DatabaseName }}' + name: mongoDatabaseName } ] - {{- end}} secretsExportConfiguration: { keyVaultResourceId: keyVault.outputs.resourceId primaryWriteConnectionStringSecretName: 'MONGODB-URL' @@ -109,7 +108,7 @@ module cosmos 'br/public:avm/res/document-db/database-account:0.8.1' = { } {{- end}} {{- if .DbCosmos }} - +var cosmosDatabaseName = '{{ .DbCosmos.DatabaseName }}' module cosmos 'br/public:avm/res/document-db/database-account:0.8.1' = { name: 'cosmos' params: { @@ -147,8 +146,10 @@ module cosmos 'br/public:avm/res/document-db/database-account:0.8.1' = { ] sqlRoleAssignmentsPrincipalIds: [ {{- range .Services}} + {{- if .DbCosmos }} {{bicepName .Name}}Identity.outputs.principalId {{- end}} + {{- end}} ] sqlRoleDefinitions: [ { @@ -321,8 +322,8 @@ module eventHubNamespace 'br/public:avm/res/event-hub/namespace:0.7.1' = { name: '${abbrs.eventHubNamespaces}${resourceToken}' location: location roleAssignments: [ - {{- if (and .AzureEventHubs (eq .AzureEventHubs.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) }} {{- range .Services}} + {{- if (and .AzureEventHubs (eq .AzureEventHubs.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) }} { principalId: {{bicepName .Name}}Identity.outputs.principalId principalType: 'ServicePrincipal' @@ -372,8 +373,8 @@ module storageAccount 'br/public:avm/res/storage/storage-account:0.14.3' = { } location: location roleAssignments: [ - {{- if (and .AzureStorageAccount (eq .AzureStorageAccount.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) }} {{- range .Services}} + {{- if (and .AzureStorageAccount (eq .AzureStorageAccount.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) }} { principalId: {{bicepName .Name}}Identity.outputs.principalId principalType: 'ServicePrincipal' @@ -411,8 +412,8 @@ module serviceBusNamespace 'br/public:avm/res/service-bus/namespace:0.10.0' = { // Non-required parameters location: location roleAssignments: [ - {{- if (and .AzureServiceBus (eq .AzureServiceBus.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) }} {{- range .Services}} + {{- if (and .AzureServiceBus (eq .AzureServiceBus.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) }} { principalId: {{bicepName .Name}}Identity.outputs.principalId principalType: 'ServicePrincipal' @@ -486,7 +487,7 @@ resource localUserOpenAIIdentity 'Microsoft.Authorization/roleAssignments@2022-0 } {{- end}} -{{- range .Services}} +{{- range $service := .Services}} module {{bicepName .Name}}Identity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { name: '{{bicepName .Name}}identity' @@ -558,65 +559,22 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { scaleMaxReplicas: 10 secrets: { secureList: union([ - {{- if .DbCosmosMongo}} + {{- range $env := .Envs}} + {{- if (shouldAddToBicepFile $service $env.Name) }} + {{- if (eq (toBicepEnv $env).BicepEnvType "keyVaultSecret") }} { - name: 'mongodb-url' - identity:{{bicepName .Name}}Identity.outputs.resourceId - keyVaultUrl: cosmos.outputs.exportedSecrets['MONGODB-URL'].secretUri + name: '{{ (toBicepEnv $env).SecretName }}' + identity:{{bicepName $service.Name}}Identity.outputs.resourceId + keyVaultUrl: {{ (toBicepEnv $env).SecretValue }} } {{- end}} - {{- if (and .DbPostgres (eq .DbPostgres.AuthType "PASSWORD")) }} - { - name: 'postgresql-password' - value: postgreSqlDatabasePassword - } + {{- if (eq (toBicepEnv $env).BicepEnvType "secret") }} { - name: 'postgresql-db-url' - value: 'postgresql://${postgreSqlDatabaseUser}:${postgreSqlDatabasePassword}@${postgreServer.outputs.fqdn}:5432/${postgreSqlDatabaseName}' + name: '{{ (toBicepEnv $env).SecretName }}' + value: {{ (toBicepEnv $env).SecretValue }} } {{- end}} - {{- if (and .DbMySql (eq .DbMySql.AuthType "PASSWORD")) }} - { - name: 'mysql-password' - value: mysqlDatabasePassword - } - { - name: 'mysql-db-url' - value: 'mysql://${mysqlDatabaseUser}:${mysqlDatabasePassword}@${mysqlServer.outputs.fqdn}:3306/${mysqlDatabaseName}' - } {{- end}} - {{- if .DbRedis}} - { - name: 'redis-pass' - identity:{{bicepName .Name}}Identity.outputs.resourceId - keyVaultUrl: '${keyVault.outputs.uri}secrets/REDIS-PASSWORD' - } - { - name: 'redis-url' - identity:{{bicepName .Name}}Identity.outputs.resourceId - keyVaultUrl: '${keyVault.outputs.uri}secrets/REDIS-URL' - } - {{- end}} - {{- if (and .AzureEventHubs (eq .AzureEventHubs.AuthType "CONNECTION_STRING")) }} - { - name: 'event-hubs-connection-string' - identity:{{bicepName .Name}}Identity.outputs.resourceId - keyVaultUrl: '${keyVault.outputs.uri}secrets/EVENT-HUBS-CONNECTION-STRING' - } - {{- end}} - {{- if (and .AzureServiceBus (eq .AzureServiceBus.AuthType "CONNECTION_STRING")) }} - { - name: 'servicebus-connection-string' - identity:{{bicepName .Name}}Identity.outputs.resourceId - keyVaultUrl: '${keyVault.outputs.uri}secrets/SERVICEBUS-CONNECTION-STRING' - } - {{- end}} - {{- if (and .AzureStorageAccount (eq .AzureStorageAccount.AuthType "CONNECTION_STRING")) }} - { - name: 'storage-account-connection-string' - identity:{{bicepName .Name}}Identity.outputs.resourceId - keyVaultUrl: '${keyVault.outputs.uri}secrets/STORAGE-ACCOUNT-CONNECTION-STRING' - } {{- end}} ], map({{bicepName .Name}}Secrets, secret => { @@ -633,305 +591,34 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { memory: '1.0Gi' } env: union([ + {{- range $env := .Envs }} + {{- if (shouldAddToBicepFile $service $env.Name) }} + {{- if (or (eq (toBicepEnv $env).BicepEnvType "keyVaultSecret") (eq (toBicepEnv $env).BicepEnvType "secret")) }} { - name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' - value: monitoring.outputs.applicationInsightsConnectionString - } - { - name: 'AZURE_CLIENT_ID' - value: {{bicepName .Name}}Identity.outputs.clientId - } - {{- if .DbCosmosMongo}} - { - name: 'MONGODB_URL' - secretRef: 'mongodb-url' - } - { - name: 'spring.data.mongodb.uri' - secretRef: 'mongodb-url' - } - { - name: 'spring.data.mongodb.database' - value: '{{ .DbCosmosMongo.DatabaseName }}' - } - {{- end}} - {{- if .DbPostgres}} - { - name: 'POSTGRES_HOST' - value: postgreServer.outputs.fqdn - } - { - name: 'POSTGRES_DATABASE' - value: postgreSqlDatabaseName - } - { - name: 'POSTGRES_PORT' - value: '5432' - } - {{- end}} - {{- if (and .DbPostgres (eq .DbPostgres.AuthType "PASSWORD")) }} - { - name: 'POSTGRES_URL' - secretRef: 'postgresql-db-url' - } - { - name: 'POSTGRES_USERNAME' - value: postgreSqlDatabaseUser - } - { - name: 'POSTGRES_PASSWORD' - secretRef: 'postgresql-password' - } - { - name: 'spring.datasource.url' - value: 'jdbc:postgresql://${postgreServer.outputs.fqdn}:5432/${postgreSqlDatabaseName}' - } - { - name: 'spring.datasource.username' - value: postgreSqlDatabaseUser - } - { - name: 'spring.datasource.password' - secretRef: 'postgresql-password' - } - {{- end}} - {{- if .DbMySql}} - { - name: 'MYSQL_HOST' - value: mysqlServer.outputs.fqdn - } - { - name: 'MYSQL_DATABASE' - value: mysqlDatabaseName - } - { - name: 'MYSQL_PORT' - value: '3306' - } - {{- end}} - {{- if (and .DbMySql (eq .DbMySql.AuthType "PASSWORD")) }} - { - name: 'MYSQL_URL' - secretRef: 'mysql-db-url' - } - { - name: 'MYSQL_USERNAME' - value: mysqlDatabaseUser - } - { - name: 'MYSQL_PASSWORD' - secretRef: 'mysql-password' - } - { - name: 'spring.datasource.url' - value: 'jdbc:mysql://${mysqlServer.outputs.fqdn}:3306/${mysqlDatabaseName}' - } - { - name: 'spring.datasource.username' - value: mysqlDatabaseUser - } - { - name: 'spring.datasource.password' - secretRef: 'mysql-password' - } - {{- end}} - {{- if .DbCosmos }} - { - name: 'spring.cloud.azure.cosmos.endpoint' - value: cosmos.outputs.endpoint - } - { - name: 'spring.cloud.azure.cosmos.database' - value: '{{ .DbCosmos.DatabaseName }}' - } - {{- end}} - {{- if .DbRedis}} - { - name: 'REDIS_HOST' - value: redis.outputs.hostName - } - { - name: 'REDIS_PORT' - value: string(redis.outputs.sslPort) - } - { - name: 'REDIS_URL' - secretRef: 'redis-url' - } - { - name: 'REDIS_ENDPOINT' - value: '${redis.outputs.hostName}:${string(redis.outputs.sslPort)}' - } - { - name: 'REDIS_PASSWORD' - secretRef: 'redis-pass' - } - { - name: 'spring.data.redis.url' - secretRef: 'redis-url' + name: '{{ (toBicepEnv $env).Name }}' + secretRef: '{{ (toBicepEnv $env).SecretName }}' } {{- end}} - {{- if .AIModels}} + {{- if (eq (toBicepEnv $env).BicepEnvType "plainText") }} { - name: 'AZURE_OPENAI_ENDPOINT' - value: account.outputs.endpoint + name: '{{ (toBicepEnv $env).Name }}' + {{- if (eq (toBicepEnv $env).PlainTextValue "'__PlaceHolderForServiceIdentityClientId'")}} + value: {{bicepName $service.Name}}Identity.outputs.clientId + {{- else}} + value: {{ (toBicepEnv $env).PlainTextValue }} + {{- end}} } {{- end}} - {{- if (and .AzureEventHubs (not .AzureEventHubs.UseKafka)) }} - { - name: 'spring.cloud.azure.eventhubs.namespace' - value: eventHubNamespace.outputs.name - } {{- end}} - {{- if (and .AzureEventHubs .AzureEventHubs.UseKafka) }} - { - name: 'spring.cloud.stream.kafka.binder.brokers' - value: '${eventHubNamespace.outputs.name}.servicebus.windows.net:9093' - } - {{- if (hasPrefix .AzureEventHubs.SpringBootVersion "2.") }} - { - name: 'spring.cloud.stream.binders.kafka.environment.spring.main.sources' - value: 'com.azure.spring.cloud.autoconfigure.eventhubs.kafka.AzureEventHubsKafkaAutoConfiguration' - } {{- end}} - {{- if (hasPrefix .AzureEventHubs.SpringBootVersion "3.") }} { - name: 'spring.cloud.stream.binders.kafka.environment.spring.main.sources' - value: 'com.azure.spring.cloud.autoconfigure.implementation.eventhubs.kafka.AzureEventHubsKafkaAutoConfiguration' - } - {{- end}} - {{- end}} - {{- if (and .AzureEventHubs (eq .AzureEventHubs.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) }} - { - name: 'spring.cloud.azure.eventhubs.credential.managed-identity-enabled' - value: 'true' - } - { - name: 'spring.cloud.azure.eventhubs.credential.client-id' - value: {{bicepName .Name}}Identity.outputs.clientId - } - { - name: 'spring.cloud.azure.eventhubs.connection-string' - value: '' - } - {{- end}} - {{- if (and .AzureEventHubs (eq .AzureEventHubs.AuthType "CONNECTION_STRING")) }} - { - name: 'spring.cloud.azure.eventhubs.connection-string' - secretRef: 'event-hubs-connection-string' - } - { - name: 'spring.cloud.azure.eventhubs.credential.managed-identity-enabled' - value: 'false' - } - { - name: 'spring.cloud.azure.eventhubs.credential.client-id' - value: '' - } - {{- end}} - {{- if .AzureStorageAccount }} - { - name: 'spring.cloud.azure.eventhubs.processor.checkpoint-store.account-name' - value: storageAccountName - } - {{- end}} - {{- if (and .AzureStorageAccount (eq .AzureStorageAccount.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) }} - { - name: 'spring.cloud.azure.eventhubs.processor.checkpoint-store.connection-string' - value: '' - } - { - name: 'spring.cloud.azure.eventhubs.processor.checkpoint-store.credential.managed-identity-enabled' - value: 'true' - } - { - name: 'spring.cloud.azure.eventhubs.processor.checkpoint-store.credential.client-id' - value: {{bicepName .Name}}Identity.outputs.clientId - } - {{- end}} - {{- if (and .AzureStorageAccount (eq .AzureStorageAccount.AuthType "CONNECTION_STRING")) }} - { - name: 'spring.cloud.azure.eventhubs.processor.checkpoint-store.connection-string' - secretRef: 'storage-account-connection-string' - } - { - name: 'spring.cloud.azure.eventhubs.processor.checkpoint-store.credential.managed-identity-enabled' - value: 'false' - } - { - name: 'spring.cloud.azure.eventhubs.processor.checkpoint-store.credential.client-id' - value: '' - } - {{- end}} - - {{- if and .AzureServiceBus (not .AzureServiceBus.IsJms)}} - { - name: 'spring.cloud.azure.servicebus.namespace' - value: serviceBusNamespace.outputs.name - } - {{- end}} - {{- if (and (and .AzureServiceBus (eq .AzureServiceBus.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) (not .AzureServiceBus.IsJms)) }} - { - name: 'spring.cloud.azure.servicebus.connection-string' - value: '' - } - { - name: 'spring.cloud.azure.servicebus.credential.managed-identity-enabled' - value: 'true' - } - { - name: 'spring.cloud.azure.servicebus.credential.client-id' - value: {{bicepName .Name}}Identity.outputs.clientId - } - {{- end}} - {{- if (and (and .AzureServiceBus (eq .AzureServiceBus.AuthType "CONNECTION_STRING")) (not .AzureServiceBus.IsJms)) }} - { - name: 'spring.cloud.azure.servicebus.connection-string' - secretRef: 'servicebus-connection-string' - } - { - name: 'spring.cloud.azure.servicebus.credential.managed-identity-enabled' - value: 'false' - } - { - name: 'spring.cloud.azure.eventhubs.credential.client-id' - value: '' - } - {{- end}} - {{- if (and (and .AzureServiceBus (eq .AzureServiceBus.AuthType "CONNECTION_STRING")) .AzureServiceBus.IsJms) }} - { - name: 'spring.jms.servicebus.connection-string' - secretRef: 'servicebus-connection-string' - } - { - name: 'spring.jms.servicebus.pricing-tier' - value: 'premium' - } - {{- end}} - - {{- if (and (and .AzureServiceBus (eq .AzureServiceBus.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) .AzureServiceBus.IsJms) }} - { - name: 'spring.jms.servicebus.passwordless-enabled' - value: 'true' - } - { - name: 'spring.jms.servicebus.namespace' - value: serviceBusNamespace.outputs.name - } - { - name: 'spring.jms.servicebus.credential.managed-identity-enabled' - value: 'true' + name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' + value: monitoring.outputs.applicationInsightsConnectionString } { - name: 'spring.jms.servicebus.credential.client-id' + name: 'AZURE_CLIENT_ID' value: {{bicepName .Name}}Identity.outputs.clientId } - { - name: 'spring.jms.servicebus.pricing-tier' - value: 'premium' - } - {{- end}} - {{- if .Frontend}} {{- range $i, $e := .Frontend.Backends}} { From f74aa50c5a2c29ecd6e7100a712d636e5ad41005 Mon Sep 17 00:00:00 2001 From: Xiaolu Dai <31124698+saragluna@users.noreply.github.com> Date: Thu, 28 Nov 2024 16:55:49 +0800 Subject: [PATCH 90/92] Update the wording or azd init in VS Code extension (#51) --- ext/vscode/package.nls.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/vscode/package.nls.json b/ext/vscode/package.nls.json index 3b633c1f39f..f0b358ad186 100644 --- a/ext/vscode/package.nls.json +++ b/ext/vscode/package.nls.json @@ -1,7 +1,7 @@ { "azure-dev.commands_category": "Azure Developer CLI (azd)", - "azure-dev.commands.cli.init.title": "Initialize App (init)", + "azure-dev.commands.cli.init.title": "Generate Azure Deployment Script (init)", "azure-dev.commands.cli.provision.title": "Provision Azure Resources (provision)", "azure-dev.commands.cli.deploy.title": "Deploy to Azure (deploy)", "azure-dev.commands.cli.restore.title": "Restore App Dependencies (restore)", From f4eddd03cd32c293ed223f37da943ca8bf95b9ca Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Thu, 28 Nov 2024 16:56:08 +0800 Subject: [PATCH 91/92] Fix pipeline failures. (#52) --- cli/azd/cmd/middleware/hooks_test.go | 3 +- .../internal/appdetect/spring_boot_test.go | 99 ------------------- cli/azd/pkg/pipeline/pipeline_manager_test.go | 4 +- cli/azd/pkg/project/importer_test.go | 4 + 4 files changed, 6 insertions(+), 104 deletions(-) diff --git a/cli/azd/cmd/middleware/hooks_test.go b/cli/azd/cmd/middleware/hooks_test.go index 517bfefd7c8..ee4e42d4922 100644 --- a/cli/azd/cmd/middleware/hooks_test.go +++ b/cli/azd/cmd/middleware/hooks_test.go @@ -3,7 +3,6 @@ package middleware import ( "context" "errors" - "github.com/azure/azure-dev/cli/azd/test/mocks/mockinput" "strings" "testing" @@ -356,7 +355,7 @@ func runMiddleware( lazyEnvManager, lazyEnv, lazyProjectConfig, - project.NewImportManager(nil, mockinput.NewMockConsole()), + project.NewImportManager(nil), mockContext.CommandRunner, mockContext.Console, runOptions, diff --git a/cli/azd/internal/appdetect/spring_boot_test.go b/cli/azd/internal/appdetect/spring_boot_test.go index 35a3e6be47f..23b6517d222 100644 --- a/cli/azd/internal/appdetect/spring_boot_test.go +++ b/cli/azd/internal/appdetect/spring_boot_test.go @@ -1,7 +1,6 @@ package appdetect import ( - "encoding/xml" "github.com/stretchr/testify/assert" "testing" ) @@ -47,32 +46,6 @@ func TestDetectSpringBootVersion(t *testing.T) { }, "2.x", }, - { - "project.dependencyManagement.property", - nil, - &mavenProject{ - Properties: Properties{ - Entries: []Property{ - { - XMLName: xml.Name{ - Local: "version.spring.boot", - }, - Value: "2.x", - }, - }, - }, - DependencyManagement: dependencyManagement{ - Dependencies: []dependency{ - { - GroupId: "org.springframework.boot", - ArtifactId: "spring-boot-dependencies", - Version: "${version.spring.boot}", - }, - }, - }, - }, - "2.x", - }, { "root.parent", &mavenProject{ @@ -101,32 +74,6 @@ func TestDetectSpringBootVersion(t *testing.T) { nil, "3.x", }, - { - "root.dependencyManagement.property", - nil, - &mavenProject{ - Properties: Properties{ - Entries: []Property{ - { - XMLName: xml.Name{ - Local: "version.spring.boot", - }, - Value: "3.x", - }, - }, - }, - DependencyManagement: dependencyManagement{ - Dependencies: []dependency{ - { - GroupId: "org.springframework.boot", - ArtifactId: "spring-boot-dependencies", - Version: "${version.spring.boot}", - }, - }, - }, - }, - "3.x", - }, { "both.root.and.project.parent", &mavenProject{ @@ -171,52 +118,6 @@ func TestDetectSpringBootVersion(t *testing.T) { }, "3.x", }, - { - "both.root.and.project.dependencyManagement.property", - &mavenProject{ - Properties: Properties{ - Entries: []Property{ - { - XMLName: xml.Name{ - Local: "version.spring.boot", - }, - Value: "2.x", - }, - }, - }, - DependencyManagement: dependencyManagement{ - Dependencies: []dependency{ - { - GroupId: "org.springframework.boot", - ArtifactId: "spring-boot-dependencies", - Version: "${version.spring.boot}", - }, - }, - }, - }, - &mavenProject{ - Properties: Properties{ - Entries: []Property{ - { - XMLName: xml.Name{ - Local: "version.spring.boot", - }, - Value: "3.x", - }, - }, - }, - DependencyManagement: dependencyManagement{ - Dependencies: []dependency{ - { - GroupId: "org.springframework.boot", - ArtifactId: "spring-boot-dependencies", - Version: "${version.spring.boot}", - }, - }, - }, - }, - "3.x", - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/cli/azd/pkg/pipeline/pipeline_manager_test.go b/cli/azd/pkg/pipeline/pipeline_manager_test.go index 4f96b6f685e..945a99726e7 100644 --- a/cli/azd/pkg/pipeline/pipeline_manager_test.go +++ b/cli/azd/pkg/pipeline/pipeline_manager_test.go @@ -6,7 +6,6 @@ package pipeline import ( "context" "fmt" - "github.com/azure/azure-dev/cli/azd/test/mocks/mockinput" "os" "path/filepath" "strings" @@ -775,8 +774,7 @@ func createPipelineManager( args, mockContext.Container, project.NewImportManager( - project.NewDotNetImporter(nil, nil, nil, nil, mockContext.AlphaFeaturesManager), - mockinput.NewMockConsole()), + project.NewDotNetImporter(nil, nil, nil, nil, mockContext.AlphaFeaturesManager)), &mockUserConfigManager{}, ) } diff --git a/cli/azd/pkg/project/importer_test.go b/cli/azd/pkg/project/importer_test.go index 420ee2a564b..4e4f9e6f0a5 100644 --- a/cli/azd/pkg/project/importer_test.go +++ b/cli/azd/pkg/project/importer_test.go @@ -392,10 +392,13 @@ resources: - api postgresdb: type: db.postgres + authType: PASSWORD mongodb: type: db.mongo + authType: USER_ASSIGNED_MANAGED_IDENTITY redis: type: db.redis + authType: PASSWORD ` func Test_ImportManager_ProjectInfrastructure_FromResources(t *testing.T) { @@ -405,6 +408,7 @@ func Test_ImportManager_ProjectInfrastructure_FromResources(t *testing.T) { im := &ImportManager{ dotNetImporter: &DotNetImporter{ alphaFeatureManager: alpha.NewFeaturesManagerWithConfig(config.NewEmptyConfig()), + console: mocks.NewMockContext(context.Background()).Console, }, } From dad722fd8f0dd03f946b54787824022e44efe01a Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Thu, 28 Nov 2024 17:53:01 +0800 Subject: [PATCH 92/92] Fix pipeline failure. (#53) --- cli/azd/pkg/project/importer_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/azd/pkg/project/importer_test.go b/cli/azd/pkg/project/importer_test.go index 4e4f9e6f0a5..35f76d9b7e3 100644 --- a/cli/azd/pkg/project/importer_test.go +++ b/cli/azd/pkg/project/importer_test.go @@ -443,6 +443,7 @@ func TestImportManager_SynthAllInfrastructure_FromResources(t *testing.T) { im := &ImportManager{ dotNetImporter: &DotNetImporter{ alphaFeatureManager: alpha.NewFeaturesManagerWithConfig(config.NewEmptyConfig()), + console: mocks.NewMockContext(context.Background()).Console, }, }