Skip to content

Commit 1f727ef

Browse files
Merge pull request #1555 from snyk/feat/state-discovery-hcl
Discover tfstate from hcl
2 parents 8dab805 + 8444e5a commit 1f727ef

File tree

13 files changed

+313
-1
lines changed

13 files changed

+313
-1
lines changed

pkg/cmd/scan.go

+48-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"fmt"
55
"os"
66
"os/signal"
7+
"path"
8+
"path/filepath"
79
"regexp"
810
"strings"
911
"syscall"
@@ -15,9 +17,12 @@ import (
1517
"github.com/sirupsen/logrus"
1618
"github.com/snyk/driftctl/build"
1719
"github.com/snyk/driftctl/pkg/analyser"
20+
"github.com/snyk/driftctl/pkg/iac/config"
21+
"github.com/snyk/driftctl/pkg/iac/terraform/state"
1822
"github.com/snyk/driftctl/pkg/memstore"
1923
"github.com/snyk/driftctl/pkg/remote/common"
2024
"github.com/snyk/driftctl/pkg/telemetry"
25+
"github.com/snyk/driftctl/pkg/terraform/hcl"
2126
"github.com/snyk/driftctl/pkg/terraform/lock"
2227
"github.com/spf13/cobra"
2328

@@ -146,7 +151,7 @@ func NewScanCmd(opts *pkg.ScanOptions) *cobra.Command {
146151
fl.StringSliceP(
147152
"from",
148153
"f",
149-
[]string{"tfstate://terraform.tfstate"},
154+
[]string{},
150155
"IaC sources, by default try to find local terraform.tfstate file\n"+
151156
"Accepted schemes are: "+strings.Join(supplier.GetSupportedSchemes(), ",")+"\n",
152157
)
@@ -259,6 +264,22 @@ func scanRun(opts *pkg.ScanOptions) error {
259264
globaloutput.ChangePrinter(globaloutput.NewConsolePrinter())
260265
}
261266

267+
if len(opts.From) == 0 {
268+
supplierConfigs, err := retrieveBackendsFromHCL("")
269+
if err != nil {
270+
return err
271+
}
272+
opts.From = append(opts.From, supplierConfigs...)
273+
}
274+
275+
if len(opts.From) == 0 {
276+
opts.From = append(opts.From, config.SupplierConfig{
277+
Key: state.TerraformStateReaderSupplier,
278+
Backend: backend.BackendKeyFile,
279+
Path: "terraform.tfstate",
280+
})
281+
}
282+
262283
providerLibrary := terraform.NewProviderLibrary()
263284
remoteLibrary := common.NewRemoteLibrary()
264285

@@ -360,3 +381,29 @@ func validateTfProviderVersionString(version string) error {
360381
}
361382
return nil
362383
}
384+
385+
func retrieveBackendsFromHCL(workdir string) ([]config.SupplierConfig, error) {
386+
matches, err := filepath.Glob(path.Join(workdir, "*.tf"))
387+
if err != nil {
388+
return nil, err
389+
}
390+
supplierConfigs := make([]config.SupplierConfig, 0)
391+
392+
for _, match := range matches {
393+
body, err := hcl.ParseTerraformFromHCL(match)
394+
if err != nil {
395+
logrus.
396+
WithField("file", match).
397+
WithField("error", err).
398+
Debug("Error parsing backend block in Terraform file")
399+
continue
400+
}
401+
402+
if supplierConfig := body.Backend.SupplierConfig(); supplierConfig != nil {
403+
globaloutput.Printf(color.WhiteString("Using Terraform state %s found in %s. Use the --from flag to specify another state file.\n"), supplierConfig, match)
404+
supplierConfigs = append(supplierConfigs, *supplierConfig)
405+
}
406+
}
407+
408+
return supplierConfigs, nil
409+
}

pkg/cmd/scan_test.go

+37
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import (
44
"testing"
55

66
"github.com/snyk/driftctl/pkg"
7+
"github.com/snyk/driftctl/pkg/iac/config"
8+
"github.com/snyk/driftctl/pkg/iac/terraform/state"
9+
"github.com/snyk/driftctl/pkg/iac/terraform/state/backend"
710
"github.com/snyk/driftctl/test"
811
"github.com/spf13/cobra"
912
"github.com/stretchr/testify/assert"
@@ -156,3 +159,37 @@ func Test_Options(t *testing.T) {
156159
})
157160
}
158161
}
162+
163+
func Test_RetrieveBackendsFromHCL(t *testing.T) {
164+
cases := []struct {
165+
name string
166+
dir string
167+
expected []config.SupplierConfig
168+
wantErr error
169+
}{
170+
{
171+
name: "should parse s3 backend and ignore invalid file",
172+
dir: "testdata/backend/s3",
173+
expected: []config.SupplierConfig{
174+
{
175+
Key: state.TerraformStateReaderSupplier,
176+
Backend: backend.BackendKeyS3,
177+
Path: "terraform-state-prod/network/terraform.tfstate",
178+
},
179+
},
180+
},
181+
{
182+
name: "should not find any match and return empty slice",
183+
dir: "testdata/backend",
184+
expected: []config.SupplierConfig{},
185+
},
186+
}
187+
188+
for _, tt := range cases {
189+
t.Run(tt.name, func(t *testing.T) {
190+
configs, err := retrieveBackendsFromHCL(tt.dir)
191+
assert.Equal(t, tt.wantErr, err)
192+
assert.Equal(t, tt.expected, configs)
193+
})
194+
}
195+
}
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
invalid {}

pkg/cmd/testdata/backend/s3/s3.tf

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
terraform {
2+
backend "s3" {
3+
bucket = "terraform-state-prod"
4+
key = "network/terraform.tfstate"
5+
region = "us-east-1"
6+
}
7+
}

pkg/iac/terraform/state/backend/gs_reader.go

+3
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ func (s *GSBackend) Read(p []byte) (int, error) {
5353
}
5454

5555
func (s *GSBackend) Close() error {
56+
if s.storageClient == nil {
57+
return nil
58+
}
5659
if err := s.storageClient.Close(); err != nil {
5760
return err
5861
}

pkg/terraform/hcl/backend.go

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package hcl
2+
3+
import (
4+
"fmt"
5+
"path"
6+
7+
"github.com/hashicorp/hcl/v2"
8+
"github.com/snyk/driftctl/pkg/iac/config"
9+
"github.com/snyk/driftctl/pkg/iac/terraform/state"
10+
"github.com/snyk/driftctl/pkg/iac/terraform/state/backend"
11+
)
12+
13+
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"`
23+
}
24+
25+
func (b BackendBlock) SupplierConfig() *config.SupplierConfig {
26+
switch b.Name {
27+
case "local":
28+
return b.parseLocalBackend()
29+
case "s3":
30+
return b.parseS3Backend()
31+
case "gcs":
32+
return b.parseGCSBackend()
33+
case "azurerm":
34+
return b.parseAzurermBackend()
35+
}
36+
return nil
37+
}
38+
39+
func (b BackendBlock) parseLocalBackend() *config.SupplierConfig {
40+
if b.Path == "" {
41+
return nil
42+
}
43+
return &config.SupplierConfig{
44+
Key: state.TerraformStateReaderSupplier,
45+
Backend: backend.BackendKeyFile,
46+
Path: path.Join(b.WorkspaceDir, b.Path),
47+
}
48+
}
49+
50+
func (b BackendBlock) parseS3Backend() *config.SupplierConfig {
51+
if b.Bucket == "" || b.Key == "" {
52+
return nil
53+
}
54+
return &config.SupplierConfig{
55+
Key: state.TerraformStateReaderSupplier,
56+
Backend: backend.BackendKeyS3,
57+
Path: path.Join(b.Bucket, b.Key),
58+
}
59+
}
60+
61+
func (b BackendBlock) parseGCSBackend() *config.SupplierConfig {
62+
if b.Bucket == "" || b.Prefix == "" {
63+
return nil
64+
}
65+
return &config.SupplierConfig{
66+
Key: state.TerraformStateReaderSupplier,
67+
Backend: backend.BackendKeyGS,
68+
Path: fmt.Sprintf("%s.tfstate", path.Join(b.Bucket, b.Prefix)),
69+
}
70+
}
71+
72+
func (b BackendBlock) parseAzurermBackend() *config.SupplierConfig {
73+
if b.ContainerName == "" || b.Key == "" {
74+
return nil
75+
}
76+
return &config.SupplierConfig{
77+
Key: state.TerraformStateReaderSupplier,
78+
Backend: backend.BackendKeyAzureRM,
79+
Path: path.Join(b.ContainerName, b.Key),
80+
}
81+
}

pkg/terraform/hcl/backend_test.go

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package hcl
2+
3+
import (
4+
"testing"
5+
6+
"github.com/snyk/driftctl/pkg/iac/config"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestBackend_SupplierConfig(t *testing.T) {
11+
cases := []struct {
12+
name string
13+
dir string
14+
want *config.SupplierConfig
15+
wantErr string
16+
}{
17+
{
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.",
22+
},
23+
{
24+
name: "test with local backend block",
25+
dir: "testdata/local_backend_block.tf",
26+
want: &config.SupplierConfig{
27+
Key: "tfstate",
28+
Path: "terraform-state-prod/network/terraform.tfstate",
29+
},
30+
},
31+
{
32+
name: "test with S3 backend block",
33+
dir: "testdata/s3_backend_block.tf",
34+
want: &config.SupplierConfig{
35+
Key: "tfstate",
36+
Backend: "s3",
37+
Path: "terraform-state-prod/network/terraform.tfstate",
38+
},
39+
},
40+
{
41+
name: "test with GCS backend block",
42+
dir: "testdata/gcs_backend_block.tf",
43+
want: &config.SupplierConfig{
44+
Key: "tfstate",
45+
Backend: "gs",
46+
Path: "tf-state-prod/terraform/state.tfstate",
47+
},
48+
},
49+
{
50+
name: "test with Azure backend block",
51+
dir: "testdata/azurerm_backend_block.tf",
52+
want: &config.SupplierConfig{
53+
Key: "tfstate",
54+
Backend: "azurerm",
55+
Path: "states/prod.terraform.tfstate",
56+
},
57+
},
58+
}
59+
60+
for _, tt := range cases {
61+
t.Run(tt.name, func(t *testing.T) {
62+
hcl, err := ParseTerraformFromHCL(tt.dir)
63+
if tt.wantErr == "" {
64+
assert.NoError(t, err)
65+
} else {
66+
assert.EqualError(t, err, tt.wantErr)
67+
return
68+
}
69+
70+
if hcl.Backend.SupplierConfig() == nil {
71+
assert.Nil(t, tt.want)
72+
return
73+
}
74+
75+
assert.Equal(t, *tt.want, *hcl.Backend.SupplierConfig())
76+
})
77+
}
78+
}

pkg/terraform/hcl/hcl.go

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package hcl
2+
3+
import (
4+
"github.com/hashicorp/hcl/v2/gohcl"
5+
"github.com/hashicorp/hcl/v2/hclparse"
6+
)
7+
8+
type MainBodyBlock struct {
9+
Terraform TerraformBlock `hcl:"terraform,block"`
10+
}
11+
12+
type TerraformBlock struct {
13+
Backend BackendBlock `hcl:"backend,block"`
14+
}
15+
16+
func ParseTerraformFromHCL(filename string) (*TerraformBlock, error) {
17+
var v MainBodyBlock
18+
19+
parser := hclparse.NewParser()
20+
f, diags := parser.ParseHCLFile(filename)
21+
if diags.HasErrors() {
22+
return nil, diags
23+
}
24+
25+
diags = gohcl.DecodeBody(f.Body, nil, &v)
26+
if diags.HasErrors() {
27+
return nil, diags
28+
}
29+
30+
return &v.Terraform, nil
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
terraform {
2+
backend "gcs" {
3+
bucket = "tf-state-prod"
4+
prefix = "terraform/state"
5+
}
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
terraform {
2+
backend "local" {
3+
path = "terraform-state-prod/network/terraform.tfstate"
4+
}
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
terraform {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
terraform {
2+
backend "s3" {
3+
bucket = "terraform-state-prod"
4+
key = "network/terraform.tfstate"
5+
region = "us-east-1"
6+
}
7+
}

0 commit comments

Comments
 (0)