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 Cosmos DB #4649

Draft
wants to merge 2 commits 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
3 changes: 3 additions & 0 deletions cli/azd/.vscode/cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ words:
- runcontext
- unmarshals
- usgovcloudapi
- datasource
- jdbc
- postgre
languageSettings:
- languageId: go
ignoreRegExpList:
Expand Down
11 changes: 11 additions & 0 deletions cli/azd/internal/appdetect/appdetect.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ const (
DbPostgres DatabaseDep = "postgres"
DbMongo DatabaseDep = "mongo"
DbMySql DatabaseDep = "mysql"
DbCosmos DatabaseDep = "cosmos"
DbSqlServer DatabaseDep = "sqlserver"
DbRedis DatabaseDep = "redis"
)
Expand All @@ -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:
Expand All @@ -131,6 +134,11 @@ func (db DatabaseDep) Display() string {
return ""
}

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

type Project struct {
// The language associated with the project.
Language Language
Expand All @@ -141,6 +149,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"
}
}
146 changes: 131 additions & 15 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 @@ -39,6 +40,7 @@ var LanguageMap = map[appdetect.Language]project.ServiceLanguageKind{
var dbMap = map[appdetect.DatabaseDep]struct{}{
appdetect.DbMongo: {},
appdetect.DbPostgres: {},
appdetect.DbCosmos: {},
appdetect.DbRedis: {},
}

Expand Down Expand Up @@ -424,6 +426,36 @@ func (i *Initializer) prjConfigFromDetect(
continue
}

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

if database == 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,
})
}
cosmos := project.ResourceConfig{
Type: project.ResourceTypeDbCosmos,
Name: "cosmos",
Props: cosmosDBProps,
}
config.Resources[cosmos.Name] = &cosmos
dbNames[database] = cosmos.Name
continue
}

var dbType project.ResourceType
switch database {
case appdetect.DbMongo:
Expand All @@ -434,21 +466,7 @@ func (i *Initializer) prjConfigFromDetect(

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

for {
dbName, err := promptDbName(i.console, ctx, database)
if err != nil {
return config, err
}

if dbName == "" {
i.console.Message(ctx, "Database name is required.")
continue
}

db.Name = dbName
break
Name: databaseName,
}

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

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

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)
}
48 changes: 48 additions & 0 deletions cli/azd/internal/repository/app_init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,52 @@ func TestInitializer_prjConfigFromDetect(t *testing.T) {
},
},
},
{
name: "api with cosmos db",
detect: detectConfirm{
Services: []appdetect.Project{
{
Language: appdetect.Java,
Path: "java",
DatabaseDeps: []appdetect.DatabaseDep{
appdetect.DbCosmos,
},
},
},
Databases: map[appdetect.DatabaseDep]EntryKind{
appdetect.DbCosmos: EntryKindDetected,
},
},
interactions: []string{
"cosmosdbname",
},
want: project.ProjectConfig{
Services: map[string]*project.ServiceConfig{
"java": {
Language: project.ServiceLanguageJava,
Host: project.ContainerAppTarget,
RelativePath: "java",
},
},
Resources: map[string]*project.ResourceConfig{
"java": {
Type: project.ResourceTypeHostContainerApp,
Name: "java",
Props: project.ContainerAppProps{
Port: 8080,
},
Uses: []string{"cosmos"},
},
"cosmos": {
Name: "cosmos",
Type: project.ResourceTypeDbCosmos,
Props: project.CosmosDBProps{
DatabaseName: "cosmosdbname",
},
},
},
},
},
{
name: "api and web",
detect: detectConfirm{
Expand Down Expand Up @@ -304,6 +350,8 @@ func TestInitializer_prjConfigFromDetect(t *testing.T) {
}
}

tt.detect.root = dir

spec, err := i.prjConfigFromDetect(
context.Background(),
dir,
Expand Down
24 changes: 24 additions & 0 deletions cli/azd/internal/repository/detect_confirm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,30 @@ func Test_detectConfirm_confirm(t *testing.T) {
},
},
},
{
name: "confirm single with cosmos db resource",
detection: []appdetect.Project{
{
Language: appdetect.Java,
Path: javaDir,
DatabaseDeps: []appdetect.DatabaseDep{
appdetect.DbCosmos,
},
},
},
interactions: []string{
"Confirm and continue initializing my app",
},
want: []appdetect.Project{
{
Language: appdetect.Java,
Path: javaDir,
DatabaseDeps: []appdetect.DatabaseDep{
appdetect.DbCosmos,
},
},
},
},
{
name: "add a language",
detection: []appdetect.Project{
Expand Down
Loading
Loading