diff --git a/.gitignore b/.gitignore index a7905e501a2..7ff4c1c3197 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,4 @@ cli/azd/extensions/microsoft.azd.demo/microsoft.azd.demo.exe cli/azd/extensions/microsoft.azd.concurx/concurx cli/azd/extensions/microsoft.azd.concurx/concurx.exe cli/azd/azd-test +cli/azd/azd diff --git a/cli/azd/pkg/project/importer.go b/cli/azd/pkg/project/importer.go index ae290c79de6..84d9afa1658 100644 --- a/cli/azd/pkg/project/importer.go +++ b/cli/azd/pkg/project/importer.go @@ -246,6 +246,18 @@ func (im *ImportManager) ProjectInfrastructure(ctx context.Context, projectConfi infraRoot = filepath.Join(projectConfig.Path, infraRoot) } + // Auto-detect provider if not explicitly set + if infraOptions.Provider == provisioning.NotSpecified { + detectedProvider, err := detectProviderFromFiles(infraRoot) + if err != nil { + return nil, err + } + if detectedProvider != provisioning.NotSpecified { + log.Printf("auto-detected infrastructure provider: %s", detectedProvider) + infraOptions.Provider = detectedProvider + } + } + // short-circuit: If layers are defined, we know it's an explicit infrastructure if len(infraOptions.Layers) > 0 { return &Infra{ @@ -291,6 +303,55 @@ func (im *ImportManager) ProjectInfrastructure(ctx context.Context, projectConfi }, nil } +// detectProviderFromFiles scans the infra directory and detects the IaC provider +// based on file extensions present. Returns an error if both bicep and terraform files exist. +func detectProviderFromFiles(infraPath string) (provisioning.ProviderKind, error) { + files, err := os.ReadDir(infraPath) + if err != nil { + if os.IsNotExist(err) { + return provisioning.NotSpecified, nil + } + return provisioning.NotSpecified, fmt.Errorf("reading infra directory: %w", err) + } + + hasBicep := false + hasTerraform := false + + for _, file := range files { + if file.IsDir() { + continue + } + + ext := filepath.Ext(file.Name()) + switch ext { + case ".bicep", ".bicepparam": + hasBicep = true + case ".tf", ".tfvars": + hasTerraform = true + } + + // Early exit if both found + if hasBicep && hasTerraform { + break + } + } + + // Decision logic + switch { + case hasBicep && hasTerraform: + return provisioning.NotSpecified, fmt.Errorf( + "both Bicep and Terraform files detected in %s. "+ + "Please specify 'infra.provider' in azure.yaml as either 'bicep' or 'terraform'", + infraPath) + case hasBicep: + return provisioning.Bicep, nil + case hasTerraform: + return provisioning.Terraform, nil + default: + return provisioning.NotSpecified, nil + } +} + // pathHasModule returns true if there is a file named "" or "" in path. func pathHasModule(path, module string) (bool, error) { files, err := os.ReadDir(path) diff --git a/cli/azd/pkg/project/importer_test.go b/cli/azd/pkg/project/importer_test.go index dc2af3497c7..f5121ad0b83 100644 --- a/cli/azd/pkg/project/importer_test.go +++ b/cli/azd/pkg/project/importer_test.go @@ -771,3 +771,126 @@ func TestImportManagerServiceStableWithDependencies(t *testing.T) { assert.True(t, backendIdx < frontendIdx, "backend should come before frontend") assert.True(t, authIdx < frontendIdx, "auth should come before frontend") } + +func TestDetectProviderFromFiles(t *testing.T) { + tests := []struct { + name string + files []string + expectedResult provisioning.ProviderKind + expectError bool + errorContains string + }{ + { + name: "only bicep files", + files: []string{"main.bicep", "modules.bicep"}, + expectedResult: provisioning.Bicep, + expectError: false, + }, + { + name: "only bicepparam files", + files: []string{"main.bicepparam"}, + expectedResult: provisioning.Bicep, + expectError: false, + }, + { + name: "only terraform files", + files: []string{"main.tf", "variables.tf"}, + expectedResult: provisioning.Terraform, + expectError: false, + }, + { + name: "only tfvars files", + files: []string{"terraform.tfvars"}, + expectedResult: provisioning.Terraform, + expectError: false, + }, + { + name: "both bicep and terraform files", + files: []string{"main.bicep", "main.tf"}, + expectedResult: provisioning.NotSpecified, + expectError: true, + errorContains: "both Bicep and Terraform files detected", + }, + { + name: "no IaC files", + files: []string{"readme.md", "config.json"}, + expectedResult: provisioning.NotSpecified, + expectError: false, + }, + { + name: "empty directory", + files: []string{}, + expectedResult: provisioning.NotSpecified, + expectError: false, + }, + { + name: "mixed with bicep and non-IaC files", + files: []string{"main.bicep", "readme.md", "config.json"}, + expectedResult: provisioning.Bicep, + expectError: false, + }, + { + name: "mixed with terraform and non-IaC files", + files: []string{"main.tf", "readme.md", "LICENSE"}, + expectedResult: provisioning.Terraform, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create temporary directory + tmpDir, err := os.MkdirTemp("", "test-detect-provider-*") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + // Create test files + for _, fileName := range tt.files { + filePath := filepath.Join(tmpDir, fileName) + err := os.WriteFile(filePath, []byte("test content"), 0600) + require.NoError(t, err) + } + + // Test detectProviderFromFiles + result, err := detectProviderFromFiles(tmpDir) + + if tt.expectError { + require.Error(t, err) + require.Contains(t, err.Error(), tt.errorContains) + } else { + require.NoError(t, err) + } + + require.Equal(t, tt.expectedResult, result) + }) + } +} + +func TestDetectProviderFromFilesNonExistentDirectory(t *testing.T) { + // Test with non-existent directory + result, err := detectProviderFromFiles("/nonexistent/path/that/does/not/exist") + require.NoError(t, err, "should not error when directory doesn't exist") + require.Equal(t, provisioning.NotSpecified, result) +} + +func TestDetectProviderFromFilesIgnoresDirectories(t *testing.T) { + // Create temporary directory structure + tmpDir, err := os.MkdirTemp("", "test-detect-provider-dirs-*") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + // Create subdirectories with IaC-like names + err = os.Mkdir(filepath.Join(tmpDir, "main.bicep"), 0755) + require.NoError(t, err) + err = os.Mkdir(filepath.Join(tmpDir, "main.tf"), 0755) + require.NoError(t, err) + + // Create a real Bicep file + err = os.WriteFile(filepath.Join(tmpDir, "resources.bicep"), []byte("test"), 0600) + require.NoError(t, err) + + // Should detect Bicep and ignore directories + result, err := detectProviderFromFiles(tmpDir) + require.NoError(t, err) + require.Equal(t, provisioning.Bicep, result) +}