diff --git a/cli/azd/.vscode/cspell.yaml b/cli/azd/.vscode/cspell.yaml index 30e56b1fbf0..d22ab125382 100644 --- a/cli/azd/.vscode/cspell.yaml +++ b/cli/azd/.vscode/cspell.yaml @@ -21,6 +21,7 @@ words: - postgre - mysqladmin - sjad + - configserver languageSettings: - languageId: go ignoreRegExpList: diff --git a/cli/azd/internal/appdetect/appdetect.go b/cli/azd/internal/appdetect/appdetect.go index 77a362f140b..89b634647e9 100644 --- a/cli/azd/internal/appdetect/appdetect.go +++ b/cli/azd/internal/appdetect/appdetect.go @@ -59,6 +59,11 @@ const ( PyFlask Dependency = "flask" PyDjango Dependency = "django" PyFastApi Dependency = "fastapi" + + JavaEurekaServer Dependency = "eureka-server" + JavaEurekaClient Dependency = "eureka-client" + JavaConfigServer Dependency = "config-server" + JavaConfigClient Dependency = "config-client" ) var WebUIFrameworks = map[Dependency]struct{}{ @@ -89,6 +94,14 @@ func (f Dependency) Display() string { return "Vite" case JsNext: return "Next.js" + case JavaEurekaServer: + return "JavaEurekaServer" + case JavaEurekaClient: + return "JavaEurekaClient" + case JavaConfigServer: + return "JavaConfigServer" + case JavaConfigClient: + return "JavaConfigClient" } return "" @@ -312,7 +325,7 @@ func detectAny(ctx context.Context, detectors []projectDetector, path string, en if project != nil { log.Printf("Found project %s at %s", project.Language, path) - // docker is an optional property of a project, and thus is different than other detectors + // docker is an optional property of a project, and thus is different from other detectors docker, err := detectDockerInDirectory(path, entries) if err != nil { return nil, fmt.Errorf("detecting docker project: %w", err) diff --git a/cli/azd/internal/appdetect/java.go b/cli/azd/internal/appdetect/java.go index 1b2b66f624a..66cf6febb98 100644 --- a/cli/azd/internal/appdetect/java.go +++ b/cli/azd/internal/appdetect/java.go @@ -4,14 +4,15 @@ import ( "context" "encoding/xml" "fmt" - "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" "regexp" "strings" + + "github.com/azure/azure-dev/cli/azd/internal/tracing" + "github.com/azure/azure-dev/cli/azd/internal/tracing/fields" ) type javaDetector struct { @@ -49,7 +50,6 @@ func (jd *javaDetector) DetectProject(ctx context.Context, path string, entries } } - _ = currentRoot // use currentRoot here in the analysis result, err := detectDependencies(currentRoot, project, &Project{ Language: Java, Path: path, diff --git a/cli/azd/internal/appdetect/spring_boot.go b/cli/azd/internal/appdetect/spring_boot.go index 36abfa67d8f..23897bf749c 100644 --- a/cli/azd/internal/appdetect/spring_boot.go +++ b/cli/azd/internal/appdetect/spring_boot.go @@ -98,6 +98,8 @@ func detectAzureDependenciesByAnalyzingSpringBootProject( detectEventHubs(azdProject, &springBootProject) detectStorageAccount(azdProject, &springBootProject) detectSpringCloudAzure(azdProject, &springBootProject) + detectSpringCloudEureka(azdProject, &springBootProject) + detectSpringCloudConfig(azdProject, &springBootProject) } func detectDatabases(azdProject *Project, springBootProject *SpringBootProject) { @@ -249,6 +251,38 @@ func detectSpringCloudAzure(azdProject *Project, springBootProject *SpringBootPr } } +func detectSpringCloudEureka(azdProject *Project, springBootProject *SpringBootProject) { + var targetGroupId = "org.springframework.cloud" + var targetArtifactId = "spring-cloud-starter-netflix-eureka-server" + if hasDependency(springBootProject, targetGroupId, targetArtifactId) { + azdProject.Dependencies = append(azdProject.Dependencies, JavaEurekaServer) + logServiceAddedAccordingToMavenDependency(JavaEurekaServer.Display(), targetGroupId, targetArtifactId) + } + + targetGroupId = "org.springframework.cloud" + targetArtifactId = "spring-cloud-starter-netflix-eureka-client" + if hasDependency(springBootProject, targetGroupId, targetArtifactId) { + azdProject.Dependencies = append(azdProject.Dependencies, JavaEurekaClient) + logServiceAddedAccordingToMavenDependency(JavaEurekaClient.Display(), targetGroupId, targetArtifactId) + } +} + +func detectSpringCloudConfig(azdProject *Project, springBootProject *SpringBootProject) { + var targetGroupId = "org.springframework.cloud" + var targetArtifactId = "spring-cloud-config-server" + if hasDependency(springBootProject, targetGroupId, targetArtifactId) { + azdProject.Dependencies = append(azdProject.Dependencies, JavaConfigServer) + logServiceAddedAccordingToMavenDependency(JavaConfigServer.Display(), targetGroupId, targetArtifactId) + } + + targetGroupId = "org.springframework.cloud" + targetArtifactId = "spring-cloud-starter-config" + if hasDependency(springBootProject, targetGroupId, targetArtifactId) { + azdProject.Dependencies = append(azdProject.Dependencies, JavaConfigClient) + logServiceAddedAccordingToMavenDependency(JavaConfigClient.Display(), targetGroupId, targetArtifactId) + } +} + func logServiceAddedAccordingToMavenDependency(resourceName, groupId string, artifactId string) { logServiceAddedAccordingToMavenDependencyAndExtraCondition(resourceName, groupId, artifactId, "") } diff --git a/cli/azd/internal/appdetect/spring_boot_test.go b/cli/azd/internal/appdetect/spring_boot_test.go index eb58a5bdea8..09c5081521d 100644 --- a/cli/azd/internal/appdetect/spring_boot_test.go +++ b/cli/azd/internal/appdetect/spring_boot_test.go @@ -2,8 +2,9 @@ package appdetect import ( "encoding/xml" - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestDetectSpringBootVersion(t *testing.T) { diff --git a/cli/azd/internal/repository/app_init.go b/cli/azd/internal/repository/app_init.go index e5128f2323e..d4308aeed3c 100644 --- a/cli/azd/internal/repository/app_init.go +++ b/cli/azd/internal/repository/app_init.go @@ -436,6 +436,26 @@ func (i *Initializer) prjConfigFromDetect( Resources: map[string]*project.ResourceConfig{}, } + var javaEurekaServerService project.ServiceConfig + var javaConfigServerService project.ServiceConfig + var err error + for _, svc := range detect.Services { + for _, dep := range svc.Dependencies { + switch dep { + case appdetect.JavaEurekaServer: + javaEurekaServerService, err = ServiceFromDetect(root, "", svc) + if err != nil { + return config, err + } + case appdetect.JavaConfigServer: + javaConfigServerService, err = ServiceFromDetect(root, "", svc) + if err != nil { + return config, err + } + } + } + } + svcMapping := map[string]string{} for _, prj := range detect.Services { svc, err := ServiceFromDetect(root, "", prj) @@ -535,6 +555,29 @@ func (i *Initializer) prjConfigFromDetect( } } + for _, dep := range prj.Dependencies { + switch dep { + case appdetect.JavaEurekaClient: + err := appendJavaEurekaOrConfigClientEnv( + &svc, + javaEurekaServerService, + project.ResourceTypeJavaEurekaServer, + spec) + if err != nil { + return config, err + } + case appdetect.JavaConfigClient: + err := appendJavaEurekaOrConfigClientEnv( + &svc, + javaConfigServerService, + project.ResourceTypeJavaConfigServer, + spec) + if err != nil { + return config, err + } + } + } + config.Services[svc.Name] = &svc svcMapping[prj.Path] = svc.Name } @@ -697,6 +740,15 @@ func (i *Initializer) prjConfigFromDetect( } props.Port = port + for _, dep := range svc.Dependencies { + switch dep { + case appdetect.JavaEurekaClient: + resSpec.Uses = append(resSpec.Uses, javaEurekaServerService.Name) + case appdetect.JavaConfigClient: + resSpec.Uses = append(resSpec.Uses, javaConfigServerService.Name) + } + } + for _, db := range svc.DatabaseDeps { // filter out databases that were removed if _, ok := detect.Databases[db]; !ok { @@ -908,3 +960,25 @@ func promptSpringBootVersion(console input.Console, ctx context.Context) (string return appdetect.UnknownSpringBootVersion, nil } } + +func appendJavaEurekaOrConfigClientEnv(svc *project.ServiceConfig, + javaEurekaOrConfigServerService project.ServiceConfig, + resourceType project.ResourceType, + infraSpec *scaffold.InfraSpec) error { + if svc.Env == nil { + svc.Env = map[string]string{} + } + + clientEnvs, err := project.GetResourceConnectionEnvs(&project.ResourceConfig{ + Name: javaEurekaOrConfigServerService.Name, + Type: resourceType, + }, infraSpec) + if err != nil { + return err + } + + for _, env := range clientEnvs { + svc.Env[env.Name] = env.Value + } + return nil +} diff --git a/cli/azd/internal/repository/infra_confirm.go b/cli/azd/internal/repository/infra_confirm.go index 66ba9afdc79..a78cff2ab64 100644 --- a/cli/azd/internal/repository/infra_confirm.go +++ b/cli/azd/internal/repository/infra_confirm.go @@ -274,6 +274,14 @@ func PromptPort( svc appdetect.Project) (int, error) { if svc.Docker == nil || svc.Docker.Path == "" { // using default builder from azd if svc.Language == appdetect.Java { + for _, dep := range svc.Dependencies { + switch dep { + case appdetect.JavaEurekaServer: + return 8761, nil + case appdetect.JavaConfigServer: + return 8888, nil + } + } return 8080, nil } return 80, nil diff --git a/cli/azd/internal/scaffold/bicep_env.go b/cli/azd/internal/scaffold/bicep_env.go index fb410671969..f446a52d1b3 100644 --- a/cli/azd/internal/scaffold/bicep_env.go +++ b/cli/azd/internal/scaffold/bicep_env.go @@ -176,7 +176,17 @@ var bicepEnv = map[ResourceType]map[ResourceInfoType]string{ ResourceTypeOpenAiModel: { ResourceInfoTypeEndpoint: "account.outputs.endpoint", }, - ResourceTypeHostContainerApp: {}, + ResourceTypeHostContainerApp: { + ResourceInfoTypeHost: "https://{{BackendName}}.${containerAppsEnvironment.outputs.defaultDomain}", + }, +} + +func GetContainerAppHost(name string) string { + return strings.ReplaceAll( + bicepEnv[ResourceTypeHostContainerApp][ResourceInfoTypeHost], + "{{BackendName}}", + name, + ) } func unsupportedType(env Env) string { diff --git a/cli/azd/pkg/project/resources.go b/cli/azd/pkg/project/resources.go index 1eb336d6b1b..1d52e07b2c8 100644 --- a/cli/azd/pkg/project/resources.go +++ b/cli/azd/pkg/project/resources.go @@ -34,6 +34,9 @@ const ( ResourceTypeMessagingEventHubs ResourceType = "messaging.eventhubs" ResourceTypeMessagingKafka ResourceType = "messaging.kafka" ResourceTypeStorage ResourceType = "storage" + + ResourceTypeJavaEurekaServer ResourceType = "java.eureka.server" + ResourceTypeJavaConfigServer ResourceType = "java.config.server" ) func (r ResourceType) String() string { @@ -60,6 +63,10 @@ func (r ResourceType) String() string { return "Kafka" case ResourceTypeStorage: return "Storage Account" + case ResourceTypeJavaEurekaServer: + return "Java Eureka Server" + case ResourceTypeJavaConfigServer: + return "Java Config Server" } return "" diff --git a/cli/azd/pkg/project/scaffold_gen.go b/cli/azd/pkg/project/scaffold_gen.go index 9f8d04862a9..0b56f22c848 100644 --- a/cli/azd/pkg/project/scaffold_gen.go +++ b/cli/azd/pkg/project/scaffold_gen.go @@ -6,14 +6,15 @@ 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" "slices" "strings" + "github.com/azure/azure-dev/cli/azd/internal" + "github.com/azure/azure-dev/cli/azd/pkg/input" + "github.com/azure/azure-dev/cli/azd/internal/scaffold" "github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning" "github.com/azure/azure-dev/cli/azd/pkg/osutil" @@ -205,6 +206,7 @@ func infraSpec(projectConfig *ProjectConfig, if err != nil { return nil, err } + serviceSpec.Envs = append(serviceSpec.Envs, serviceConfigEnv(projectConfig.Services[resource.Name])...) infraSpec.Services = append(infraSpec.Services, serviceSpec) case ResourceTypeOpenAiModel: props := resource.Props.(AIModelProps) @@ -346,13 +348,16 @@ func getAuthType(infraSpec *scaffold.InfraSpec, resourceType ResourceType) (inte return infraSpec.AzureEventHubs.AuthType, nil case ResourceTypeStorage: return infraSpec.AzureStorageAccount.AuthType, nil + case ResourceTypeJavaEurekaServer, + ResourceTypeJavaConfigServer: + return internal.AuthTypeUnspecified, 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) + envs, err := GetResourceConnectionEnvs(usedResource, infraSpec) if err != nil { return err } @@ -395,7 +400,7 @@ func printEnvListAboutUses(infraSpec *scaffold.InfraSpec, projectConfig *Project ResourceTypeMessagingEventHubs, ResourceTypeMessagingKafka, ResourceTypeStorage: - variables, err := getResourceConnectionEnvs(usedResource, infraSpec) + variables, err := GetResourceConnectionEnvs(usedResource, infraSpec) if err != nil { return err } @@ -569,3 +574,16 @@ func printHintsAboutUseHostContainerApp(userResourceName string, usedResourceNam console.Message(ctx, fmt.Sprintf("Environment variables in %s:", usedResourceName)) console.Message(ctx, fmt.Sprintf("%s_BASE_URL=xxx", strings.ToUpper(userResourceName))) } + +func serviceConfigEnv(svcConfig *ServiceConfig) []scaffold.Env { + var envs []scaffold.Env + if svcConfig != nil { + for key, val := range svcConfig.Env { + envs = append(envs, scaffold.Env{ + Name: key, + Value: val, + }) + } + } + return envs +} diff --git a/cli/azd/pkg/project/scaffold_gen_environment_variables.go b/cli/azd/pkg/project/scaffold_gen_environment_variables.go index e1224f54fdd..6dd03e02a82 100644 --- a/cli/azd/pkg/project/scaffold_gen_environment_variables.go +++ b/cli/azd/pkg/project/scaffold_gen_environment_variables.go @@ -7,7 +7,7 @@ import ( "strings" ) -func getResourceConnectionEnvs(usedResource *ResourceConfig, +func GetResourceConnectionEnvs(usedResource *ResourceConfig, infraSpec *scaffold.InfraSpec) ([]scaffold.Env, error) { resourceType := usedResource.Type authType, err := getAuthType(infraSpec, usedResource.Type) @@ -556,6 +556,32 @@ func getResourceConnectionEnvs(usedResource *ResourceConfig, default: return []scaffold.Env{}, unsupportedAuthTypeError(resourceType, authType) } + case ResourceTypeJavaEurekaServer: + return []scaffold.Env{ + { + Name: "eureka.client.register-with-eureka", + Value: "true", + }, + { + Name: "eureka.client.fetch-registry", + Value: "true", + }, + { + Name: "eureka.instance.prefer-ip-address", + Value: "true", + }, + { + Name: "eureka.client.serviceUrl.defaultZone", + Value: fmt.Sprintf("%s/eureka", scaffold.GetContainerAppHost(usedResource.Name)), + }, + }, nil + case ResourceTypeJavaConfigServer: + return []scaffold.Env{ + { + Name: "spring.config.import", + Value: fmt.Sprintf("optional:configserver:%s", scaffold.GetContainerAppHost(usedResource.Name)), + }, + }, nil default: return []scaffold.Env{}, unsupportedResourceTypeError(resourceType) } diff --git a/cli/azd/pkg/project/service_config.go b/cli/azd/pkg/project/service_config.go index b1af653b911..7881fbc85df 100644 --- a/cli/azd/pkg/project/service_config.go +++ b/cli/azd/pkg/project/service_config.go @@ -45,6 +45,8 @@ type ServiceConfig struct { DotNetContainerApp *DotNetContainerAppOptions `yaml:"-,omitempty"` // Custom configuration for the service target Config map[string]any `yaml:"config,omitempty"` + // Environment variables for service + Env map[string]string `yaml:"env,omitempty"` *ext.EventDispatcher[ServiceLifecycleEventArgs] `yaml:"-"` } diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index d011af5fa22..f77382418ba 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -272,8 +272,8 @@ module connectionCreatorIdentity 'br/public:avm/res/managed-identity/user-assign } } {{- end}} -{{- if (and .DbPostgres (eq .DbPostgres.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) }} {{- range .Services}} +{{- if (and .DbPostgres (eq .DbPostgres.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) }} module {{bicepName .Name}}CreateConnectionToPostgreSql 'br/public:avm/res/resources/deployment-script:0.4.0' = { name: '{{bicepName .Name}}CreateConnectionToPostgreSql' params: { @@ -293,8 +293,8 @@ module {{bicepName .Name}}CreateConnectionToPostgreSql 'br/public:avm/res/resour } {{- end}} {{- end}} -{{- if (and .DbMySql (eq .DbMySql.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) }} {{- range .Services}} +{{- if (and .DbMySql (eq .DbMySql.AuthType "USER_ASSIGNED_MANAGED_IDENTITY")) }} module {{bicepName .Name}}CreateConnectionToMysql 'br/public:avm/res/resources/deployment-script:0.4.0' = { name: '{{bicepName .Name}}CreateConnectionToMysql' params: { diff --git a/schemas/alpha/azure.yaml.json b/schemas/alpha/azure.yaml.json index 2c5cd04d067..bb50a0cb0cc 100644 --- a/schemas/alpha/azure.yaml.json +++ b/schemas/alpha/azure.yaml.json @@ -159,6 +159,14 @@ "type": "object", "additionalProperties": true }, + "env": { + "type": "object", + "title": "A map of key value pairs used to set as environment variables for the service.", + "description": "Optional. Supports environment variable substitution.", + "additionalProperties": { + "type": "string" + } + }, "hooks": { "type": "object", "title": "Service level hooks",