diff --git a/.github/workflows/go-test-for-sjad-branch.yml b/.github/workflows/go-test-for-sjad-branch.yml index c44aefe5b36..31b7c897341 100644 --- a/.github/workflows/go-test-for-sjad-branch.yml +++ b/.github/workflows/go-test-for-sjad-branch.yml @@ -39,4 +39,4 @@ jobs: - name: Run tests run: | cd ./cli/azd - go test $(go list ./... | grep -v github.com/azure/azure-dev/cli/azd/test/functional) -cover -v + go test $(go list ./... | grep -v github.com/azure/azure-dev/cli/azd/test/functional | grep -v github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning/terraform) -cover -v diff --git a/cli/azd/.vscode/cspell.yaml b/cli/azd/.vscode/cspell.yaml index 3c9b1f42d26..889925332db 100644 --- a/cli/azd/.vscode/cspell.yaml +++ b/cli/azd/.vscode/cspell.yaml @@ -26,6 +26,7 @@ words: - sjad - configserver - chardata + - webflux languageSettings: - languageId: go ignoreRegExpList: diff --git a/cli/azd/internal/appdetect/appdetect.go b/cli/azd/internal/appdetect/appdetect.go index 0fca45ca557..38bbc546824 100644 --- a/cli/azd/internal/appdetect/appdetect.go +++ b/cli/azd/internal/appdetect/appdetect.go @@ -195,7 +195,7 @@ func (a AzureDepStorageAccount) ResourceDisplay() string { type Metadata struct { ApplicationName string - ServerPort string + ServerPort int DatabaseNameInPropertySpringDatasourceUrl map[DatabaseDep]string ContainsDependencySpringCloudAzureStarter bool ContainsDependencySpringCloudAzureStarterJdbcPostgresql bool @@ -204,6 +204,7 @@ type Metadata struct { ContainsDependencySpringCloudEurekaClient bool ContainsDependencySpringCloudConfigServer bool ContainsDependencySpringCloudConfigClient bool + ContainsDependencyAboutEmbeddedWebServer bool } type Project struct { diff --git a/cli/azd/internal/appdetect/appdetect_test.go b/cli/azd/internal/appdetect/appdetect_test.go index 5dbd4be2b0e..9a0d14cfa4c 100644 --- a/cli/azd/internal/appdetect/appdetect_test.go +++ b/cli/azd/internal/appdetect/appdetect_test.go @@ -75,6 +75,9 @@ func TestDetect(t *testing.T) { Options: map[string]interface{}{ JavaProjectOptionParentPomDir: filepath.Join(dir, "java-multimodules"), }, + Metadata: Metadata{ + ContainsDependencyAboutEmbeddedWebServer: true, + }, }, { Language: JavaScript, @@ -180,6 +183,9 @@ func TestDetect(t *testing.T) { Options: map[string]interface{}{ JavaProjectOptionParentPomDir: filepath.Join(dir, "java-multimodules"), }, + Metadata: Metadata{ + ContainsDependencyAboutEmbeddedWebServer: true, + }, }, }, }, @@ -234,6 +240,9 @@ func TestDetect(t *testing.T) { Options: map[string]interface{}{ JavaProjectOptionParentPomDir: filepath.Join(dir, "java-multimodules"), }, + Metadata: Metadata{ + ContainsDependencyAboutEmbeddedWebServer: true, + }, }, }, }, @@ -291,6 +300,9 @@ func TestDetect(t *testing.T) { Options: map[string]interface{}{ JavaProjectOptionParentPomDir: filepath.Join(dir, "java-multimodules"), }, + Metadata: Metadata{ + ContainsDependencyAboutEmbeddedWebServer: true, + }, }, { Language: Python, diff --git a/cli/azd/internal/appdetect/spring_boot.go b/cli/azd/internal/appdetect/spring_boot.go index a3b5e7d5fad..58250650881 100644 --- a/cli/azd/internal/appdetect/spring_boot.go +++ b/cli/azd/internal/appdetect/spring_boot.go @@ -6,6 +6,7 @@ import ( "maps" "regexp" "slices" + "strconv" "strings" ) @@ -407,6 +408,7 @@ func detectMetadata(azdProject *Project, springBootProject *SpringBootProject) { detectDependencySpringCloudAzureStarterJdbcPostgresql(azdProject, springBootProject) detectDependencySpringCloudConfig(azdProject, springBootProject) detectDependencySpringCloudEureka(azdProject, springBootProject) + detectDependencyAboutEmbeddedWebServer(azdProject, springBootProject) } func detectPropertySpringCloudAzureCosmosDatabase(azdProject *Project, springBootProject *SpringBootProject) { @@ -553,7 +555,11 @@ func detectPropertySpringApplicationName(azdProject *Project, springBootProject func detectPropertyServerPort(azdProject *Project, springBootProject *SpringBootProject) { var targetPropertyName = "server.port" if serverPort, ok := springBootProject.applicationProperties[targetPropertyName]; ok { - azdProject.Metadata.ServerPort = serverPort + if port, err := strconv.Atoi(serverPort); err == nil { + azdProject.Metadata.ServerPort = port + } else { + log.Printf("Failed to convert the value of server.port to int. %v.", err) + } } } @@ -589,6 +595,25 @@ func detectDependencySpringCloudConfig(azdProject *Project, springBootProject *S } } +func detectDependencyAboutEmbeddedWebServer(azdProject *Project, springBootProject *SpringBootProject) { + var targetGroupId = "org.springframework.boot" + var targetArtifactIds = []string{ + "spring-boot-starter-web", + "spring-boot-starter-webflux", + "spring-boot-starter-tomcat", + "spring-boot-starter-jetty", + "spring-boot-starter-undertow", + "spring-boot-starter-reactor-netty", + } + for _, targetArtifactId := range targetArtifactIds { + if hasDependency(springBootProject, targetGroupId, targetArtifactId) { + azdProject.Metadata.ContainsDependencyAboutEmbeddedWebServer = true + logMetadataUpdated("ContainsDependencyAboutEmbeddedWebServer = true") + return + } + } +} + 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 ab049ffbe9e..d3120daf12b 100644 --- a/cli/azd/internal/appdetect/spring_boot_test.go +++ b/cli/azd/internal/appdetect/spring_boot_test.go @@ -1,7 +1,12 @@ package appdetect import ( + "context" + "path/filepath" "testing" + + "github.com/azure/azure-dev/cli/azd/pkg/exec" + "github.com/azure/azure-dev/cli/azd/pkg/tools/maven" ) func TestGetDatabaseName(t *testing.T) { @@ -62,3 +67,104 @@ func TestIsValidDatabaseName(t *testing.T) { }) } } + +func TestDetectDependencyAboutEmbeddedWebServer(t *testing.T) { + tests := []struct { + name string + testPoms []testPom + expected bool + }{ + { + name: "no web dependency", + testPoms: []testPom{ + { + pomFilePath: "pom.xml", + pomContentString: ` + + 4.0.0 + com.example + example-project + 1.0.0 + + `, + }, + }, + expected: false, + }, + { + name: "has dependency: spring-boot-starter-web", + testPoms: []testPom{ + { + pomFilePath: "pom.xml", + pomContentString: ` + + 4.0.0 + com.example + example-project + 1.0.0 + + + org.springframework.boot + spring-boot-starter-web + 3.0.0 + + + + `, + }, + }, + expected: true, + }, + { + name: "has dependency: spring-boot-starter-webflux", + testPoms: []testPom{ + { + pomFilePath: "pom.xml", + pomContentString: ` + + 4.0.0 + com.example + example-project + 1.0.0 + + + org.springframework.boot + spring-boot-starter-webflux + 3.0.0 + + + + `, + }, + }, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + workingDir, err := prepareTestPomFiles(tt.testPoms) + if err != nil { + t.Fatalf("%v", err) + } + for _, testPom := range tt.testPoms { + pomFilePath := filepath.Join(workingDir, testPom.pomFilePath) + mavenProject, err := createMavenProject(context.TODO(), maven.NewCli(exec.NewCommandRunner(nil)), + pomFilePath) + if err != nil { + t.Fatalf("%v", err) + } + project := Project{ + Language: Java, + Path: pomFilePath, + DetectionRule: "Inferred by presence of: pom.xml", + } + detectAzureDependenciesByAnalyzingSpringBootProject(mavenProject, &project) + if project.Metadata.ContainsDependencyAboutEmbeddedWebServer != tt.expected { + t.Errorf("\nExpected: %v\nActual: %v", tt.expected, + project.Metadata.ContainsDependencyAboutEmbeddedWebServer) + } + } + }) + } +} diff --git a/cli/azd/internal/repository/app_init_test.go b/cli/azd/internal/repository/app_init_test.go index 8669c6a09cd..b75795c3146 100644 --- a/cli/azd/internal/repository/app_init_test.go +++ b/cli/azd/internal/repository/app_init_test.go @@ -3,7 +3,6 @@ package repository import ( "context" "fmt" - "github.com/azure/azure-dev/cli/azd/internal/scaffold" "os" "path/filepath" "strings" @@ -11,6 +10,7 @@ import ( "github.com/azure/azure-dev/cli/azd/internal" "github.com/azure/azure-dev/cli/azd/internal/appdetect" + "github.com/azure/azure-dev/cli/azd/internal/scaffold" "github.com/azure/azure-dev/cli/azd/pkg/input" "github.com/azure/azure-dev/cli/azd/pkg/project" "github.com/stretchr/testify/require" @@ -98,13 +98,7 @@ func TestInitializer_prjConfigFromDetect(t *testing.T) { }, }, }, - interactions: []string{ - // prompt for port -- hit multiple validation cases - "notAnInteger", - "-2", - "65536", - "1234", - }, + interactions: []string{}, want: project.ProjectConfig{ Services: map[string]*project.ServiceConfig{ "dotnet": { @@ -118,11 +112,9 @@ func TestInitializer_prjConfigFromDetect(t *testing.T) { }, Resources: map[string]*project.ResourceConfig{ "dotnet": { - Type: project.ResourceTypeHostContainerApp, - Name: "dotnet", - Props: project.ContainerAppProps{ - Port: 1234, - }, + Type: project.ResourceTypeHostContainerApp, + Name: "dotnet", + Props: project.ContainerAppProps{}, }, }, }, diff --git a/cli/azd/internal/repository/infra_confirm.go b/cli/azd/internal/repository/infra_confirm.go index e76244ec8f9..98b9ee580d2 100644 --- a/cli/azd/internal/repository/infra_confirm.go +++ b/cli/azd/internal/repository/infra_confirm.go @@ -275,17 +275,10 @@ func GetOrPromptPort( ctx context.Context, name string, svc appdetect.Project) (int, error) { - if svc.Metadata.ServerPort != "" { - return strconv.Atoi(svc.Metadata.ServerPort) - } if svc.Docker == nil || svc.Docker.Path == "" { // using default builder from azd - if svc.Language == appdetect.Java || svc.Language == appdetect.DotNet { - if svc.Metadata.ContainsDependencySpringCloudEurekaServer { - return 8761, nil - } - if svc.Metadata.ContainsDependencySpringCloudConfigServer { - return 8888, nil - } + if svc.Language == appdetect.Java { + return getJavaApplicationPort(svc), nil + } else if svc.Language == appdetect.DotNet { return 8080, nil } return 80, nil @@ -296,12 +289,8 @@ func GetOrPromptPort( switch len(ports) { case 1: // only one port was exposed, that's the one return ports[0].Number, nil - case 0: // no ports exposed, prompt for port - port, err := promptPortNumber(console, ctx, "What port does '"+name+"' listen on?") - if err != nil { - return -1, err - } - return port, nil + case 0: // no ports exposed, not expose port + return 0, nil } // multiple ports exposed, prompt for selection @@ -332,6 +321,23 @@ func GetOrPromptPort( return port, nil } +func getJavaApplicationPort(svc appdetect.Project) int { + if !shouldExposePort(svc) { + return 0 + } + if svc.Metadata.ServerPort != 0 { + return svc.Metadata.ServerPort + } else { + return 8080 + } +} + +func shouldExposePort(svc appdetect.Project) bool { + return svc.Metadata.ContainsDependencySpringCloudEurekaServer || + svc.Metadata.ContainsDependencySpringCloudConfigServer || + svc.Metadata.ContainsDependencyAboutEmbeddedWebServer +} + func (i *Initializer) buildInfraSpecByAzureDep( 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 aeb7f2d89a5..ed6432c44cb 100644 --- a/cli/azd/internal/repository/infra_confirm_test.go +++ b/cli/azd/internal/repository/infra_confirm_test.go @@ -85,19 +85,11 @@ func TestInitializer_infraSpecFromDetect(t *testing.T) { }, }, }, - interactions: []string{ - // prompt for port -- hit multiple validation cases - "notAnInteger", - "-2", - "65536", - "1234", - }, + interactions: []string{}, want: scaffold.InfraSpec{ Services: []scaffold.ServiceSpec{ { - Name: "dotnet", - Port: 1234, - Backend: &scaffold.Backend{}, + Name: "dotnet", }, }, }, @@ -308,3 +300,83 @@ func TestDetectCosmosSqlDatabaseContainerInFile(t *testing.T) { }) } } + +func Test_getJavaApplicationPort(t *testing.T) { + tests := []struct { + name string + svc appdetect.Project + expected int + }{ + { + name: "not configure anything", + svc: appdetect.Project{ + Metadata: appdetect.Metadata{}, + }, + expected: 0, + }, + { + name: "only configure ServerPort", + svc: appdetect.Project{ + Metadata: appdetect.Metadata{ + ServerPort: 8888, + }, + }, + expected: 0, + }, + { + name: "only configure ContainsDependencySpringCloudEurekaServer", + svc: appdetect.Project{ + Metadata: appdetect.Metadata{ + ContainsDependencySpringCloudEurekaServer: true, + }, + }, + expected: 8080, + }, + { + name: "only configure ContainsDependencySpringCloudConfigServer", + svc: appdetect.Project{ + Metadata: appdetect.Metadata{ + ContainsDependencySpringCloudConfigServer: true, + }, + }, + expected: 8080, + }, + { + name: "only configure ContainsDependencyAboutEmbeddedWebServer", + svc: appdetect.Project{ + Metadata: appdetect.Metadata{ + ContainsDependencyAboutEmbeddedWebServer: true, + }, + }, + expected: 8080, + }, + { + name: "configure multiple dependencies", + svc: appdetect.Project{ + Metadata: appdetect.Metadata{ + ContainsDependencySpringCloudEurekaServer: true, + ContainsDependencySpringCloudConfigServer: true, + ContainsDependencyAboutEmbeddedWebServer: true, + }, + }, + expected: 8080, + }, + { + name: "configure ServerPort and multiple dependencies", + svc: appdetect.Project{ + Metadata: appdetect.Metadata{ + ServerPort: 8888, + ContainsDependencySpringCloudEurekaServer: true, + ContainsDependencySpringCloudConfigServer: true, + ContainsDependencyAboutEmbeddedWebServer: true, + }, + }, + expected: 8888, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, getJavaApplicationPort(tt.svc)) + }) + } +} diff --git a/cli/azd/pkg/project/scaffold_gen.go b/cli/azd/pkg/project/scaffold_gen.go index c6cda5c4b09..cc20456064d 100644 --- a/cli/azd/pkg/project/scaffold_gen.go +++ b/cli/azd/pkg/project/scaffold_gen.go @@ -397,8 +397,9 @@ func handleContainerAppProps( } port := props.Port - if port < 1 || port > 65535 { - return fmt.Errorf("port value %d for host %s must be between 1 and 65535", port, resourceConfig.Name) + if port < 0 || port > 65535 { + return fmt.Errorf("port value for '%s' must be between 0 and 65535 (port = 0 means ingress disabled), "+ + "but it's %d ", resourceConfig.Name, port) } serviceSpec.Port = port diff --git a/cli/azd/resources/scaffold/templates/resources.bicept b/cli/azd/resources/scaffold/templates/resources.bicept index 8997db4c67e..5fd37dcd83e 100644 --- a/cli/azd/resources/scaffold/templates/resources.bicept +++ b/cli/azd/resources/scaffold/templates/resources.bicept @@ -550,7 +550,6 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { name: '{{containerAppName .Name}}' {{- if ne .Port 0}} ingressTargetPort: {{.Port}} - {{- end}} {{- if (and .Backend .Backend.Frontends)}} corsPolicy: { allowedOrigins: [ @@ -563,6 +562,9 @@ module {{bicepName .Name}} 'br/public:avm/res/app/container-app:0.8.0' = { ] } {{- end}} + {{- else}} + disableIngress: true + {{- end}} scaleMinReplicas: 1 scaleMaxReplicas: 10 secrets: { diff --git a/schemas/alpha/azure.yaml.json b/schemas/alpha/azure.yaml.json index 7611a0d23f7..02e9fa72880 100644 --- a/schemas/alpha/azure.yaml.json +++ b/schemas/alpha/azure.yaml.json @@ -1254,9 +1254,6 @@ "type": "object", "description": "A Docker-based container app.", "additionalProperties": false, - "required": [ - "port" - ], "properties": { "type": true, "uses": true,