Skip to content
Open
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
38 changes: 36 additions & 2 deletions cli/azd/pkg/infra/provisioning/terraform/terraform_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,9 @@ func (t *TerraformProvider) dataDirPath() string {
}

// Check terraform file for remote backend provider
// Note: This uses simple string matching rather than full HCL parsing for performance and
// to avoid additional dependencies. While this may have edge cases (e.g., backends mentioned
// in comments), it's sufficient for the common case of detecting actual backend configurations.
func (t *TerraformProvider) isRemoteBackendConfig() (bool, error) {
modulePath := t.modulePath()
infraDir, _ := os.Open(modulePath)
Expand All @@ -655,6 +658,23 @@ func (t *TerraformProvider) isRemoteBackendConfig() (bool, error) {
return false, fmt.Errorf("reading .tf files contents: %w", err)
}

// List of currently supported remote backend types that should not use the -state flag.
// This list includes all standard Terraform backends as of Terraform 1.3+
// (deprecated backends like etcd, swift, artifactory, and manta were removed in 1.3)
// Reference: https://developer.hashicorp.com/terraform/language/backend
remoteBackends := []string{
`backend "azurerm"`, // Azure Resource Manager
`backend "remote"`, // Terraform Cloud (legacy)
`backend "s3"`, // AWS S3
`backend "gcs"`, // Google Cloud Storage
`backend "consul"`, // HashiCorp Consul
`backend "cos"`, // Tencent Cloud Object Storage
`backend "http"`, // HTTP/REST
`backend "kubernetes"`, // Kubernetes
`backend "oss"`, // Alibaba Cloud OSS
`backend "pg"`, // PostgreSQL
}

for index := range files {
if !files[index].IsDir() && filepath.Ext(files[index].Name()) == ".tf" {
fileContent, err := os.ReadFile(filepath.Join(modulePath, files[index].Name()))
Expand All @@ -663,8 +683,22 @@ func (t *TerraformProvider) isRemoteBackendConfig() (bool, error) {
return false, fmt.Errorf("error reading .tf files: %w", err)
}

if found := strings.Contains(string(fileContent), `backend "azurerm"`); found {
return true, nil
content := string(fileContent)

// Quick check: if the file doesn't contain "backend" keyword, skip detailed checks
if !strings.Contains(content, "backend") {
// Still need to check for Terraform Cloud "cloud {}" syntax
if strings.Contains(content, "terraform {") && strings.Contains(content, "cloud {") {
return true, nil
}
continue
}

// Check for standard backend blocks
for _, backend := range remoteBackends {
if strings.Contains(content, backend) {
return true, nil
}
}
}
}
Expand Down
141 changes: 141 additions & 0 deletions cli/azd/pkg/infra/provisioning/terraform/terraform_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"context"
_ "embed"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
Expand Down Expand Up @@ -222,3 +224,142 @@ func (m *mockCurrentPrincipal) CurrentPrincipalId(_ context.Context) (string, er
func (m *mockCurrentPrincipal) CurrentPrincipalType(_ context.Context) (provisioning.PrincipalType, error) {
return provisioning.UserType, nil
}

func TestIsRemoteBackendConfig(t *testing.T) {
tests := []struct {
name string
backendFile string
expectedRemote bool
}{
{
name: "azurerm backend",
backendFile: "azurerm.tf",
expectedRemote: true,
},
{
name: "remote backend (Terraform Cloud legacy)",
backendFile: "remote.tf",
expectedRemote: true,
},
{
name: "cloud block (Terraform Cloud new syntax)",
backendFile: "cloud.tf",
expectedRemote: true,
},
{
name: "s3 backend",
backendFile: "s3.tf",
expectedRemote: true,
},
{
name: "gcs backend",
backendFile: "gcs.tf",
expectedRemote: true,
},
{
name: "consul backend",
backendFile: "consul.tf",
expectedRemote: true,
},
{
name: "http backend",
backendFile: "http.tf",
expectedRemote: true,
},
{
name: "kubernetes backend",
backendFile: "kubernetes.tf",
expectedRemote: true,
},
{
name: "local backend",
backendFile: "local.tf",
expectedRemote: false,
},
{
name: "no backend specified",
backendFile: "no_backend.tf",
expectedRemote: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockContext := mocks.NewMockContext(context.Background())
prepareGenericMocks(mockContext.CommandRunner)

// Create a temporary directory for the test
tmpDir := t.TempDir()
infraDir := filepath.Join(tmpDir, "infra")
err := os.MkdirAll(infraDir, 0755)
require.NoError(t, err)

// Copy the test backend file to the temporary infra directory
testDataPath := filepath.Join("testdata", "backend_tests", tt.backendFile)
testContent, err := os.ReadFile(testDataPath)
require.NoError(t, err)

// #nosec G306 -- test file permissions are intentionally readable
err = os.WriteFile(filepath.Join(infraDir, "main.tf"), testContent, 0644)
require.NoError(t, err)

// Create a TerraformProvider instance
options := provisioning.Options{
Module: "main",
}

env := environment.NewWithValues("test-env", map[string]string{
"AZURE_LOCATION": "westus2",
"AZURE_SUBSCRIPTION_ID": "00000000-0000-0000-0000-000000000000",
})

resourceService := azapi.NewResourceService(
mockContext.SubscriptionCredentialProvider,
mockContext.ArmClientOptions,
)
accountManager := &mockaccount.MockAccountManager{
Subscriptions: []account.Subscription{
{
Id: "00000000-0000-0000-0000-000000000000",
Name: "test",
},
},
Locations: []account.Location{
{
Name: "location",
DisplayName: "Test Location",
RegionalDisplayName: "(US) Test Location",
},
},
}

envManager := &mockenv.MockEnvManager{}
envManager.On("Save", mock.Anything, mock.Anything).Return(nil)

provider := NewTerraformProvider(
terraformTools.NewCli(mockContext.CommandRunner),
envManager,
env,
mockContext.Console,
&mockCurrentPrincipal{},
prompt.NewDefaultPrompter(
env,
mockContext.Console,
accountManager,
resourceService,
cloud.AzurePublic(),
),
)

err = provider.Initialize(*mockContext.Context, tmpDir, options)
require.NoError(t, err)

tfProvider := provider.(*TerraformProvider)

// Test the isRemoteBackendConfig function
isRemote, err := tfProvider.isRemoteBackendConfig()
require.NoError(t, err)
require.Equal(t, tt.expectedRemote, isRemote, "Expected isRemote=%v for %s", tt.expectedRemote, tt.name)
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
terraform {
required_version = ">= 1.1.7"
backend "azurerm" {
resource_group_name = "rg-terraform-state"
storage_account_name = "tfstate"
container_name = "tfstate"
key = "terraform.tfstate"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
terraform {
required_version = ">= 1.1.7"
cloud {
hostname = "app.terraform.io"
organization = "my-org"
workspaces {
name = "my-workspace"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
terraform {
required_version = ">= 1.1.7"
backend "consul" {
address = "consul.example.com"
path = "terraform/state"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
terraform {
required_version = ">= 1.1.7"
backend "gcs" {
bucket = "my-tf-state-bucket"
prefix = "terraform/state"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
terraform {
required_version = ">= 1.1.7"
backend "http" {
address = "https://api.example.com/terraform/state"
lock_address = "https://api.example.com/terraform/lock"
unlock_address = "https://api.example.com/terraform/unlock"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
terraform {
required_version = ">= 1.1.7"
backend "kubernetes" {
secret_suffix = "state"
config_path = "~/.kube/config"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
terraform {
required_version = ">= 1.1.7"
backend "local" {
path = "terraform.tfstate"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
terraform {
required_version = ">= 1.1.7"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
terraform {
required_version = ">= 1.1.7"
backend "remote" {
hostname = "app.terraform.io"
organization = "my-org"
workspaces {
name = "my-workspace"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
terraform {
required_version = ">= 1.1.7"
backend "s3" {
bucket = "my-tf-state-bucket"
key = "terraform.tfstate"
region = "us-east-1"
}
}
Loading