Skip to content

Commit

Permalink
add azure cosmos db
Browse files Browse the repository at this point in the history
  • Loading branch information
saragluna committed Dec 24, 2024
1 parent ef05268 commit a0f57d3
Show file tree
Hide file tree
Showing 17 changed files with 1,238 additions and 137 deletions.
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)
}
18 changes: 9 additions & 9 deletions cli/azd/internal/repository/infra_confirm.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,19 +85,19 @@ func (i *Initializer) infraSpecFromDetect(

switch db {
case appdetect.DbMongo:
serviceSpec.DbCosmosMongo = &scaffold.DatabaseReference{
DatabaseName: spec.DbCosmosMongo.DatabaseName,
}
err = scaffold.BindToMongoDb(&serviceSpec, spec.DbCosmosMongo)
case appdetect.DbPostgres:
serviceSpec.DbPostgres = &scaffold.DatabaseReference{
DatabaseName: spec.DbPostgres.DatabaseName,
}
err = scaffold.BindToPostgres(&serviceSpec, spec.DbPostgres)
case appdetect.DbRedis:
serviceSpec.DbRedis = &scaffold.DatabaseReference{
DatabaseName: "redis",
}
err = scaffold.BindToRedis(&serviceSpec, spec.DbRedis)
case appdetect.DbCosmos:
err = scaffold.BindToCosmosDb(&serviceSpec, spec.DbCosmos)
}
}

if err != nil {
return scaffold.InfraSpec{}, err
}
spec.Services = append(spec.Services, serviceSpec)
}

Expand Down
4 changes: 3 additions & 1 deletion cli/azd/internal/repository/infra_confirm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
)

func TestInitializer_infraSpecFromDetect(t *testing.T) {
envs, _ := scaffold.GetServiceBindingEnvsForPostgres()
tests := []struct {
name string
detect detectConfirm
Expand Down Expand Up @@ -183,9 +184,10 @@ func TestInitializer_infraSpecFromDetect(t *testing.T) {
},
},
},
DbPostgres: &scaffold.DatabaseReference{
DbPostgres: &scaffold.DatabasePostgres{
DatabaseName: "myappdb",
},
Envs: envs,
},
{
Name: "js",
Expand Down
Loading

0 comments on commit a0f57d3

Please sign in to comment.