Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
61 changes: 61 additions & 0 deletions cli/azd/pkg/project/importer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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 "<module>" or "<module.bicep>" in path.
func pathHasModule(path, module string) (bool, error) {
files, err := os.ReadDir(path)
Expand Down
123 changes: 123 additions & 0 deletions cli/azd/pkg/project/importer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Loading