Skip to content

Commit 7e5c31f

Browse files
authoredJul 12, 2022
Merge pull request #1562 from snyk/fix/tf-hcl-schema
Support tfstate discovering within workspaces
2 parents 172e121 + 26ce6c8 commit 7e5c31f

File tree

16 files changed

+171
-42
lines changed

16 files changed

+171
-42
lines changed
 

‎pkg/cmd/scan.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,8 @@ func retrieveBackendsFromHCL(workdir string) ([]config.SupplierConfig, error) {
404404
continue
405405
}
406406

407-
if supplierConfig := body.Backend.SupplierConfig(); supplierConfig != nil {
407+
ws := hcl.GetCurrentWorkspaceName(path.Dir(match))
408+
if supplierConfig := body.Backend.SupplierConfig(ws); supplierConfig != nil {
408409
globaloutput.Printf(color.WhiteString("Using Terraform state %s found in %s. Use the --from flag to specify another state file.\n"), supplierConfig, match)
409410
supplierConfigs = append(supplierConfigs, *supplierConfig)
410411
}

‎pkg/terraform/hcl/backend.go

+31-18
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,28 @@ import (
1111
)
1212

1313
type BackendBlock struct {
14-
Name string `hcl:"name,label"`
15-
Path string `hcl:"path,optional"`
16-
WorkspaceDir string `hcl:"workspace_dir,optional"`
17-
Bucket string `hcl:"bucket,optional"`
18-
Key string `hcl:"key,optional"`
19-
Region string `hcl:"region,optional"`
20-
Prefix string `hcl:"prefix,optional"`
21-
ContainerName string `hcl:"container_name,optional"`
22-
Remain hcl.Body `hcl:",remain"`
14+
Name string `hcl:"name,label"`
15+
Path string `hcl:"path,optional"`
16+
WorkspaceDir string `hcl:"workspace_dir,optional"`
17+
Bucket string `hcl:"bucket,optional"`
18+
Key string `hcl:"key,optional"`
19+
Region string `hcl:"region,optional"`
20+
Prefix string `hcl:"prefix,optional"`
21+
ContainerName string `hcl:"container_name,optional"`
22+
WorkspaceKeyPrefix string `hcl:"workspace_key_prefix,optional"`
23+
Remain hcl.Body `hcl:",remain"`
2324
}
2425

25-
func (b BackendBlock) SupplierConfig() *config.SupplierConfig {
26+
func (b BackendBlock) SupplierConfig(workspace string) *config.SupplierConfig {
2627
switch b.Name {
2728
case "local":
2829
return b.parseLocalBackend()
2930
case "s3":
30-
return b.parseS3Backend()
31+
return b.parseS3Backend(workspace)
3132
case "gcs":
32-
return b.parseGCSBackend()
33+
return b.parseGCSBackend(workspace)
3334
case "azurerm":
34-
return b.parseAzurermBackend()
35+
return b.parseAzurermBackend(workspace)
3536
}
3637
return nil
3738
}
@@ -47,32 +48,44 @@ func (b BackendBlock) parseLocalBackend() *config.SupplierConfig {
4748
}
4849
}
4950

50-
func (b BackendBlock) parseS3Backend() *config.SupplierConfig {
51+
func (b BackendBlock) parseS3Backend(ws string) *config.SupplierConfig {
5152
if b.Bucket == "" || b.Key == "" {
5253
return nil
5354
}
55+
56+
keyPrefix := b.WorkspaceKeyPrefix
57+
if ws != DefaultStateName {
58+
if b.WorkspaceKeyPrefix == "" {
59+
b.WorkspaceKeyPrefix = "env:"
60+
}
61+
keyPrefix = path.Join(b.WorkspaceKeyPrefix, ws)
62+
}
63+
5464
return &config.SupplierConfig{
5565
Key: state.TerraformStateReaderSupplier,
5666
Backend: backend.BackendKeyS3,
57-
Path: path.Join(b.Bucket, b.Key),
67+
Path: path.Join(b.Bucket, keyPrefix, b.Key),
5868
}
5969
}
6070

61-
func (b BackendBlock) parseGCSBackend() *config.SupplierConfig {
71+
func (b BackendBlock) parseGCSBackend(ws string) *config.SupplierConfig {
6272
if b.Bucket == "" || b.Prefix == "" {
6373
return nil
6474
}
6575
return &config.SupplierConfig{
6676
Key: state.TerraformStateReaderSupplier,
6777
Backend: backend.BackendKeyGS,
68-
Path: fmt.Sprintf("%s.tfstate", path.Join(b.Bucket, b.Prefix)),
78+
Path: fmt.Sprintf("%s.tfstate", path.Join(b.Bucket, b.Prefix, ws)),
6979
}
7080
}
7181

72-
func (b BackendBlock) parseAzurermBackend() *config.SupplierConfig {
82+
func (b BackendBlock) parseAzurermBackend(ws string) *config.SupplierConfig {
7383
if b.ContainerName == "" || b.Key == "" {
7484
return nil
7585
}
86+
if ws != DefaultStateName {
87+
b.Key = fmt.Sprintf("%senv:%s", b.Key, ws)
88+
}
7689
return &config.SupplierConfig{
7790
Key: state.TerraformStateReaderSupplier,
7891
Backend: backend.BackendKeyAzureRM,

‎pkg/terraform/hcl/backend_test.go

+71-20
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,129 @@
11
package hcl
22

33
import (
4+
"path"
45
"testing"
56

67
"github.com/snyk/driftctl/pkg/iac/config"
78
"github.com/stretchr/testify/assert"
89
)
910

11+
func TestHCL_getCurrentWorkspaceName(t *testing.T) {
12+
cases := []struct {
13+
name string
14+
dir string
15+
want string
16+
}{
17+
{
18+
name: "test with non-default workspace",
19+
dir: "testdata/foo_workspace",
20+
want: "foo",
21+
},
22+
{
23+
name: "test with non-existing directory",
24+
dir: "testdata/noenvfile",
25+
want: "default",
26+
},
27+
}
28+
29+
for _, tt := range cases {
30+
t.Run(tt.name, func(t *testing.T) {
31+
workspace := GetCurrentWorkspaceName(tt.dir)
32+
assert.Equal(t, tt.want, workspace)
33+
})
34+
}
35+
}
36+
1037
func TestBackend_SupplierConfig(t *testing.T) {
1138
cases := []struct {
12-
name string
13-
dir string
14-
want *config.SupplierConfig
15-
wantErr string
39+
name string
40+
filename string
41+
want *config.SupplierConfig
42+
wantErr string
1643
}{
1744
{
18-
name: "test with no backend block",
19-
dir: "testdata/no_backend_block.tf",
20-
want: nil,
21-
wantErr: "testdata/no_backend_block.tf:1,11-11: Missing backend block; A backend block is required.",
45+
name: "test with no backend block",
46+
filename: "testdata/no_backend_block.tf",
47+
want: nil,
48+
wantErr: "testdata/no_backend_block.tf:1,11-11: Missing backend block; A backend block is required.",
2249
},
2350
{
24-
name: "test with local backend block",
25-
dir: "testdata/local_backend_block.tf",
51+
name: "test with local backend block",
52+
filename: "testdata/local_backend_block.tf",
2653
want: &config.SupplierConfig{
2754
Key: "tfstate",
2855
Path: "terraform-state-prod/network/terraform.tfstate",
2956
},
3057
},
3158
{
32-
name: "test with S3 backend block",
33-
dir: "testdata/s3_backend_block.tf",
59+
name: "test with S3 backend block",
60+
filename: "testdata/s3_backend_block.tf",
3461
want: &config.SupplierConfig{
3562
Key: "tfstate",
3663
Backend: "s3",
3764
Path: "terraform-state-prod/network/terraform.tfstate",
3865
},
3966
},
4067
{
41-
name: "test with GCS backend block",
42-
dir: "testdata/gcs_backend_block.tf",
68+
name: "test with S3 backend block with non-default workspace",
69+
filename: "testdata/s3_backend_workspace/s3_backend_block.tf",
70+
want: &config.SupplierConfig{
71+
Key: "tfstate",
72+
Backend: "s3",
73+
Path: "terraform-state-prod/env:/bar/network/terraform.tfstate",
74+
},
75+
},
76+
{
77+
name: "test with GCS backend block",
78+
filename: "testdata/gcs_backend_block.tf",
4379
want: &config.SupplierConfig{
4480
Key: "tfstate",
4581
Backend: "gs",
46-
Path: "tf-state-prod/terraform/state.tfstate",
82+
Path: "tf-state-prod/terraform/state/default.tfstate",
4783
},
4884
},
4985
{
50-
name: "test with Azure backend block",
51-
dir: "testdata/azurerm_backend_block.tf",
86+
name: "test with Azure backend block",
87+
filename: "testdata/azurerm_backend_block.tf",
5288
want: &config.SupplierConfig{
5389
Key: "tfstate",
5490
Backend: "azurerm",
5591
Path: "states/prod.terraform.tfstate",
5692
},
5793
},
94+
{
95+
name: "test with Azure backend block with non-default workspace",
96+
filename: "testdata/azurerm_backend_workspace/azurerm_backend_block.tf",
97+
want: &config.SupplierConfig{
98+
Key: "tfstate",
99+
Backend: "azurerm",
100+
Path: "states/prod.terraform.tfstateenv:bar",
101+
},
102+
},
103+
{
104+
name: "test with unknown backend",
105+
filename: "testdata/unknown_backend_block.tf",
106+
want: nil,
107+
},
58108
}
59109

60110
for _, tt := range cases {
61111
t.Run(tt.name, func(t *testing.T) {
62-
hcl, err := ParseTerraformFromHCL(tt.dir)
112+
hcl, err := ParseTerraformFromHCL(tt.filename)
63113
if tt.wantErr == "" {
64114
assert.NoError(t, err)
65115
} else {
66116
assert.EqualError(t, err, tt.wantErr)
67117
return
68118
}
69119

70-
if hcl.Backend.SupplierConfig() == nil {
120+
ws := GetCurrentWorkspaceName(path.Dir(tt.filename))
121+
if hcl.Backend.SupplierConfig(ws) == nil {
71122
assert.Nil(t, tt.want)
72123
return
73124
}
74125

75-
assert.Equal(t, *tt.want, *hcl.Backend.SupplierConfig())
126+
assert.Equal(t, *tt.want, *hcl.Backend.SupplierConfig(ws))
76127
})
77128
}
78129
}

‎pkg/terraform/hcl/hcl.go

+25-3
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,53 @@
11
package hcl
22

33
import (
4+
"io/ioutil"
5+
"path"
6+
"strings"
7+
8+
"github.com/hashicorp/hcl/v2"
49
"github.com/hashicorp/hcl/v2/gohcl"
510
"github.com/hashicorp/hcl/v2/hclparse"
611
)
712

13+
const DefaultStateName = "default"
14+
815
type MainBodyBlock struct {
916
Terraform TerraformBlock `hcl:"terraform,block"`
17+
Remain hcl.Body `hcl:",remain"`
1018
}
1119

1220
type TerraformBlock struct {
1321
Backend BackendBlock `hcl:"backend,block"`
22+
Remain hcl.Body `hcl:",remain"`
1423
}
1524

1625
func ParseTerraformFromHCL(filename string) (*TerraformBlock, error) {
17-
var v MainBodyBlock
26+
var body MainBodyBlock
1827

1928
parser := hclparse.NewParser()
2029
f, diags := parser.ParseHCLFile(filename)
2130
if diags.HasErrors() {
2231
return nil, diags
2332
}
2433

25-
diags = gohcl.DecodeBody(f.Body, nil, &v)
34+
diags = gohcl.DecodeBody(f.Body, nil, &body)
2635
if diags.HasErrors() {
2736
return nil, diags
2837
}
2938

30-
return &v.Terraform, nil
39+
return &body.Terraform, nil
40+
}
41+
42+
func GetCurrentWorkspaceName(cwd string) string {
43+
name := DefaultStateName // See https://github.com/hashicorp/terraform/blob/main/internal/backend/backend.go#L33
44+
45+
data, err := ioutil.ReadFile(path.Join(cwd, ".terraform/environment"))
46+
if err != nil {
47+
return name
48+
}
49+
if v := strings.Trim(string(data), "\n"); v != "" {
50+
name = v
51+
}
52+
return name
3153
}

‎pkg/terraform/hcl/testdata/azurerm_backend_block.tf

+4
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,7 @@ terraform {
66
key = "prod.terraform.tfstate"
77
}
88
}
9+
10+
provider "azurerm" {
11+
features {}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
!.terraform
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
bar
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
terraform {
2+
backend "azurerm" {
3+
resource_group_name = "StorageAccount-ResourceGroup"
4+
storage_account_name = "abcd1234"
5+
container_name = "states"
6+
key = "prod.terraform.tfstate"
7+
}
8+
}
9+
10+
provider "azurerm" {
11+
features {}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
!.terraform
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
foo

‎pkg/terraform/hcl/testdata/gcs_backend_block.tf

+6
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,9 @@ terraform {
44
prefix = "terraform/state"
55
}
66
}
7+
8+
provider "google" {
9+
project = "my-project"
10+
region = "us-central1"
11+
zone = "us-central1-c"
12+
}

‎pkg/terraform/hcl/testdata/s3_backend_block.tf

+2
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ terraform {
55
region = "us-east-1"
66
}
77
}
8+
9+
provider "aws" {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
!.terraform
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
bar
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
terraform {
2+
backend "s3" {
3+
bucket = "terraform-state-prod"
4+
key = "network/terraform.tfstate"
5+
region = "us-east-1"
6+
}
7+
}
8+
9+
provider "aws" {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
terraform {
2+
backend "oss" {}
3+
}

0 commit comments

Comments
 (0)
Failed to load comments.