Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add resource support for Azure PostgreSql managed identity #4650

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions cli/azd/internal/appdetect/appdetect.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@ func (db DatabaseDep) Display() string {
return ""
}

type Metadata struct {
ApplicationName string
DatabaseNameInPropertySpringDatasourceUrl map[DatabaseDep]string
ContainsDependencySpringCloudAzureStarterJdbcPostgresql bool
}

type Project struct {
// The language associated with the project.
Language Language
Expand All @@ -141,6 +147,9 @@ type Project struct {
// Experimental: Database dependencies inferred through heuristics while scanning dependencies in the project.
DatabaseDeps []DatabaseDep

// Experimental: Metadata inferred through heuristics while scanning the project.
Metadata Metadata

// The path to the project directory.
Path string

Expand Down
29 changes: 29 additions & 0 deletions cli/azd/internal/auth_type.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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 = "connectionString"
// Microsoft EntraID token credential
AuthTypeUserAssignedManagedIdentity AuthType = "userAssignedManagedIdentity"
)

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"
default:
return "Unspecified"
}
}
222 changes: 205 additions & 17 deletions cli/azd/internal/repository/app_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"maps"
"os"
"path/filepath"
"regexp"
"slices"
"strings"
"time"
Expand Down Expand Up @@ -424,31 +425,56 @@ func (i *Initializer) prjConfigFromDetect(
continue
}

var dbType project.ResourceType
switch database {
case appdetect.DbMongo:
dbType = project.ResourceTypeDbMongo
case appdetect.DbPostgres:
dbType = project.ResourceTypeDbPostgres
}

db := project.ResourceConfig{
Type: dbType,
var err error
databaseName, err := getDatabaseName(database, &detect, i.console, ctx)
if err != nil {
return config, err
}

for {
dbName, err := promptDbName(i.console, ctx, database)
var authType = internal.AuthTypeUnspecified
if database == appdetect.DbPostgres {
var err error
authType, err = chooseAuthTypeByPrompt(
database.Display(),
[]internal.AuthType{internal.AuthTypeUserAssignedManagedIdentity, internal.AuthTypePassword},
ctx,
i.console)
if err != nil {
return config, err
}

if dbName == "" {
i.console.Message(ctx, "Database name is required.")
continueProvision, err := checkPasswordlessConfigurationAndContinueProvision(database, authType, &detect,
i.console, ctx)
if err != nil {
return config, err
}
if !continueProvision {
continue
}
}

db.Name = dbName
break
if database == appdetect.DbPostgres {
postgres := project.ResourceConfig{
Type: project.ResourceTypeDbPostgres,
Name: "postgresql",
Props: project.PostgresProps{
DatabaseName: databaseName,
AuthType: authType,
},
}
config.Resources[postgres.Name] = &postgres
dbNames[database] = postgres.Name
continue
}

var dbType project.ResourceType
switch database {
case appdetect.DbMongo:
dbType = project.ResourceTypeDbMongo
}

db := project.ResourceConfig{
Type: dbType,
Name: databaseName,
}

config.Resources[db.Name] = &db
Expand Down Expand Up @@ -578,3 +604,165 @@ func ServiceFromDetect(

return svc, 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
}

func checkPasswordlessConfigurationAndContinueProvision(database appdetect.DatabaseDep, authType internal.AuthType,
detect *detectConfirm, console input.Console, ctx context.Context) (bool, error) {
if authType != internal.AuthTypeUserAssignedManagedIdentity {
return true, nil
}
for i, prj := range detect.Services {
if lackedDep := lackedAzureStarterJdbcDependency(prj, database); lackedDep != "" {
message := fmt.Sprintf("\nError!\n"+
"You selected '%s' as auth type for '%s'.\n"+
"For this auth type, this dependency is required:\n"+
"%s\n"+
"But this dependency is not found in your project:\n"+
"%s",
internal.AuthTypeUserAssignedManagedIdentity, database, lackedDep, prj.Path)
continueOption, err := console.Select(ctx, input.ConsoleOptions{
Message: fmt.Sprintf("%s\nSelect an option:", message),
Options: []string{
"Exit azd and fix problem manually",
fmt.Sprintf("Continue azd and use %s in this project: %s", database.Display(), prj.Path),
fmt.Sprintf("Continue azd and not use %s in this project: %s", database.Display(), prj.Path),
},
})
if err != nil {
return false, err
}

switch continueOption {
case 0:
os.Exit(0)
case 1:
continue
case 2:
// remove related database usage
var result []appdetect.DatabaseDep
for _, db := range prj.DatabaseDeps {
if db != database {
result = append(result, db)
}
}
prj.DatabaseDeps = result
detect.Services[i] = prj
// delete database if no other service used
dbUsed := false
for _, svc := range detect.Services {
for _, db := range svc.DatabaseDeps {
if db == database {
dbUsed = true
break
}
}
if dbUsed {
break
}
}
if !dbUsed {
console.Message(ctx, fmt.Sprintf(
"Deleting database %s due to no service used", database.Display()))
delete(detect.Databases, database)
return false, nil
}
}
}
}
return true, nil
}

func lackedAzureStarterJdbcDependency(project appdetect.Project, database appdetect.DatabaseDep) string {
if project.Language != appdetect.Java {
return ""
}

useDatabase := false
for _, db := range project.DatabaseDeps {
if db == database {
useDatabase = true
break
}
}
if !useDatabase {
return ""
}
if database == appdetect.DbPostgres && !project.Metadata.ContainsDependencySpringCloudAzureStarterJdbcPostgresql {
return "<dependency>\n" +
" <groupId>com.azure.spring</groupId>\n" +
" <artifactId>spring-cloud-azure-starter-jdbc-postgresql</artifactId>\n" +
" <version>xxx</version>\n" +
"</dependency>"
}
return ""
}

func getDatabaseName(database appdetect.DatabaseDep, detect *detectConfirm,
console input.Console, ctx context.Context) (string, error) {
dbName := getDatabaseNameFromProjectMetadata(detect, database)
if dbName != "" {
return dbName, nil
}
for {
dbName, err := console.Prompt(ctx, input.ConsoleOptions{
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.\n" +
"You may be able to skip this step by hitting enter, in which case the database will not be created.",
})
if err != nil {
return "", err
}
if isValidDatabaseName(dbName) {
return dbName, nil
} else {
console.Message(ctx, "Invalid database name. Please choose another name.")
}
}
}

func getDatabaseNameFromProjectMetadata(detect *detectConfirm, database appdetect.DatabaseDep) string {
result := ""
for _, service := range detect.Services {
// todo this should not be here, it should be part of the app detect
name := service.Metadata.DatabaseNameInPropertySpringDatasourceUrl[database]
if name != "" {
if result == "" {
result = name
} else {
// different project configured different db name, not use any of them.
return ""
}
}
}
return result
}

func isValidDatabaseName(name string) bool {
if len(name) < 3 || len(name) > 63 {
return false
}
re := regexp.MustCompile(`^[a-z0-9]+(-[a-z0-9]+)*$`)
return re.MatchString(name)
}
11 changes: 8 additions & 3 deletions cli/azd/internal/repository/app_init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,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{
Expand All @@ -240,14 +241,18 @@ func TestInitializer_prjConfigFromDetect(t *testing.T) {
Type: project.ResourceTypeDbMongo,
Name: "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", "mongodb", "redis"},
Props: project.ContainerAppProps{
Port: 80,
},
Expand Down
Loading
Loading