Skip to content

Commit

Permalink
Merge pull request #1555 from snyk/feat/state-discovery-hcl
Browse files Browse the repository at this point in the history
Discover tfstate from hcl
  • Loading branch information
sundowndev-snyk authored Jul 6, 2022
2 parents 8dab805 + 8444e5a commit 1f727ef
Show file tree
Hide file tree
Showing 13 changed files with 313 additions and 1 deletion.
49 changes: 48 additions & 1 deletion pkg/cmd/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"fmt"
"os"
"os/signal"
"path"
"path/filepath"
"regexp"
"strings"
"syscall"
Expand All @@ -15,9 +17,12 @@ import (
"github.com/sirupsen/logrus"
"github.com/snyk/driftctl/build"
"github.com/snyk/driftctl/pkg/analyser"
"github.com/snyk/driftctl/pkg/iac/config"
"github.com/snyk/driftctl/pkg/iac/terraform/state"
"github.com/snyk/driftctl/pkg/memstore"
"github.com/snyk/driftctl/pkg/remote/common"
"github.com/snyk/driftctl/pkg/telemetry"
"github.com/snyk/driftctl/pkg/terraform/hcl"
"github.com/snyk/driftctl/pkg/terraform/lock"
"github.com/spf13/cobra"

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

if len(opts.From) == 0 {
supplierConfigs, err := retrieveBackendsFromHCL("")
if err != nil {
return err
}
opts.From = append(opts.From, supplierConfigs...)
}

if len(opts.From) == 0 {
opts.From = append(opts.From, config.SupplierConfig{
Key: state.TerraformStateReaderSupplier,
Backend: backend.BackendKeyFile,
Path: "terraform.tfstate",
})
}

providerLibrary := terraform.NewProviderLibrary()
remoteLibrary := common.NewRemoteLibrary()

Expand Down Expand Up @@ -360,3 +381,29 @@ func validateTfProviderVersionString(version string) error {
}
return nil
}

func retrieveBackendsFromHCL(workdir string) ([]config.SupplierConfig, error) {
matches, err := filepath.Glob(path.Join(workdir, "*.tf"))
if err != nil {
return nil, err
}
supplierConfigs := make([]config.SupplierConfig, 0)

for _, match := range matches {
body, err := hcl.ParseTerraformFromHCL(match)
if err != nil {
logrus.
WithField("file", match).
WithField("error", err).
Debug("Error parsing backend block in Terraform file")
continue
}

if supplierConfig := body.Backend.SupplierConfig(); supplierConfig != nil {
globaloutput.Printf(color.WhiteString("Using Terraform state %s found in %s. Use the --from flag to specify another state file.\n"), supplierConfig, match)
supplierConfigs = append(supplierConfigs, *supplierConfig)
}
}

return supplierConfigs, nil
}
37 changes: 37 additions & 0 deletions pkg/cmd/scan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import (
"testing"

"github.com/snyk/driftctl/pkg"
"github.com/snyk/driftctl/pkg/iac/config"
"github.com/snyk/driftctl/pkg/iac/terraform/state"
"github.com/snyk/driftctl/pkg/iac/terraform/state/backend"
"github.com/snyk/driftctl/test"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -156,3 +159,37 @@ func Test_Options(t *testing.T) {
})
}
}

func Test_RetrieveBackendsFromHCL(t *testing.T) {
cases := []struct {
name string
dir string
expected []config.SupplierConfig
wantErr error
}{
{
name: "should parse s3 backend and ignore invalid file",
dir: "testdata/backend/s3",
expected: []config.SupplierConfig{
{
Key: state.TerraformStateReaderSupplier,
Backend: backend.BackendKeyS3,
Path: "terraform-state-prod/network/terraform.tfstate",
},
},
},
{
name: "should not find any match and return empty slice",
dir: "testdata/backend",
expected: []config.SupplierConfig{},
},
}

for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
configs, err := retrieveBackendsFromHCL(tt.dir)
assert.Equal(t, tt.wantErr, err)
assert.Equal(t, tt.expected, configs)
})
}
}
1 change: 1 addition & 0 deletions pkg/cmd/testdata/backend/s3/invalid.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
invalid {}
7 changes: 7 additions & 0 deletions pkg/cmd/testdata/backend/s3/s3.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
terraform {
backend "s3" {
bucket = "terraform-state-prod"
key = "network/terraform.tfstate"
region = "us-east-1"
}
}
3 changes: 3 additions & 0 deletions pkg/iac/terraform/state/backend/gs_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ func (s *GSBackend) Read(p []byte) (int, error) {
}

func (s *GSBackend) Close() error {
if s.storageClient == nil {
return nil
}
if err := s.storageClient.Close(); err != nil {
return err
}
Expand Down
81 changes: 81 additions & 0 deletions pkg/terraform/hcl/backend.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package hcl

import (
"fmt"
"path"

"github.com/hashicorp/hcl/v2"
"github.com/snyk/driftctl/pkg/iac/config"
"github.com/snyk/driftctl/pkg/iac/terraform/state"
"github.com/snyk/driftctl/pkg/iac/terraform/state/backend"
)

type BackendBlock struct {
Name string `hcl:"name,label"`
Path string `hcl:"path,optional"`
WorkspaceDir string `hcl:"workspace_dir,optional"`
Bucket string `hcl:"bucket,optional"`
Key string `hcl:"key,optional"`
Region string `hcl:"region,optional"`
Prefix string `hcl:"prefix,optional"`
ContainerName string `hcl:"container_name,optional"`
Remain hcl.Body `hcl:",remain"`
}

func (b BackendBlock) SupplierConfig() *config.SupplierConfig {
switch b.Name {
case "local":
return b.parseLocalBackend()
case "s3":
return b.parseS3Backend()
case "gcs":
return b.parseGCSBackend()
case "azurerm":
return b.parseAzurermBackend()
}
return nil
}

func (b BackendBlock) parseLocalBackend() *config.SupplierConfig {
if b.Path == "" {
return nil
}
return &config.SupplierConfig{
Key: state.TerraformStateReaderSupplier,
Backend: backend.BackendKeyFile,
Path: path.Join(b.WorkspaceDir, b.Path),
}
}

func (b BackendBlock) parseS3Backend() *config.SupplierConfig {
if b.Bucket == "" || b.Key == "" {
return nil
}
return &config.SupplierConfig{
Key: state.TerraformStateReaderSupplier,
Backend: backend.BackendKeyS3,
Path: path.Join(b.Bucket, b.Key),
}
}

func (b BackendBlock) parseGCSBackend() *config.SupplierConfig {
if b.Bucket == "" || b.Prefix == "" {
return nil
}
return &config.SupplierConfig{
Key: state.TerraformStateReaderSupplier,
Backend: backend.BackendKeyGS,
Path: fmt.Sprintf("%s.tfstate", path.Join(b.Bucket, b.Prefix)),
}
}

func (b BackendBlock) parseAzurermBackend() *config.SupplierConfig {
if b.ContainerName == "" || b.Key == "" {
return nil
}
return &config.SupplierConfig{
Key: state.TerraformStateReaderSupplier,
Backend: backend.BackendKeyAzureRM,
Path: path.Join(b.ContainerName, b.Key),
}
}
78 changes: 78 additions & 0 deletions pkg/terraform/hcl/backend_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package hcl

import (
"testing"

"github.com/snyk/driftctl/pkg/iac/config"
"github.com/stretchr/testify/assert"
)

func TestBackend_SupplierConfig(t *testing.T) {
cases := []struct {
name string
dir string
want *config.SupplierConfig
wantErr string
}{
{
name: "test with no backend block",
dir: "testdata/no_backend_block.tf",
want: nil,
wantErr: "testdata/no_backend_block.tf:1,11-11: Missing backend block; A backend block is required.",
},
{
name: "test with local backend block",
dir: "testdata/local_backend_block.tf",
want: &config.SupplierConfig{
Key: "tfstate",
Path: "terraform-state-prod/network/terraform.tfstate",
},
},
{
name: "test with S3 backend block",
dir: "testdata/s3_backend_block.tf",
want: &config.SupplierConfig{
Key: "tfstate",
Backend: "s3",
Path: "terraform-state-prod/network/terraform.tfstate",
},
},
{
name: "test with GCS backend block",
dir: "testdata/gcs_backend_block.tf",
want: &config.SupplierConfig{
Key: "tfstate",
Backend: "gs",
Path: "tf-state-prod/terraform/state.tfstate",
},
},
{
name: "test with Azure backend block",
dir: "testdata/azurerm_backend_block.tf",
want: &config.SupplierConfig{
Key: "tfstate",
Backend: "azurerm",
Path: "states/prod.terraform.tfstate",
},
},
}

for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
hcl, err := ParseTerraformFromHCL(tt.dir)
if tt.wantErr == "" {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, tt.wantErr)
return
}

if hcl.Backend.SupplierConfig() == nil {
assert.Nil(t, tt.want)
return
}

assert.Equal(t, *tt.want, *hcl.Backend.SupplierConfig())
})
}
}
31 changes: 31 additions & 0 deletions pkg/terraform/hcl/hcl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package hcl

import (
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclparse"
)

type MainBodyBlock struct {
Terraform TerraformBlock `hcl:"terraform,block"`
}

type TerraformBlock struct {
Backend BackendBlock `hcl:"backend,block"`
}

func ParseTerraformFromHCL(filename string) (*TerraformBlock, error) {
var v MainBodyBlock

parser := hclparse.NewParser()
f, diags := parser.ParseHCLFile(filename)
if diags.HasErrors() {
return nil, diags
}

diags = gohcl.DecodeBody(f.Body, nil, &v)
if diags.HasErrors() {
return nil, diags
}

return &v.Terraform, nil
}
8 changes: 8 additions & 0 deletions pkg/terraform/hcl/testdata/azurerm_backend_block.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
terraform {
backend "azurerm" {
resource_group_name = "StorageAccount-ResourceGroup"
storage_account_name = "abcd1234"
container_name = "states"
key = "prod.terraform.tfstate"
}
}
6 changes: 6 additions & 0 deletions pkg/terraform/hcl/testdata/gcs_backend_block.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
terraform {
backend "gcs" {
bucket = "tf-state-prod"
prefix = "terraform/state"
}
}
5 changes: 5 additions & 0 deletions pkg/terraform/hcl/testdata/local_backend_block.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
terraform {
backend "local" {
path = "terraform-state-prod/network/terraform.tfstate"
}
}
1 change: 1 addition & 0 deletions pkg/terraform/hcl/testdata/no_backend_block.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform {}
7 changes: 7 additions & 0 deletions pkg/terraform/hcl/testdata/s3_backend_block.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
terraform {
backend "s3" {
bucket = "terraform-state-prod"
key = "network/terraform.tfstate"
region = "us-east-1"
}
}

0 comments on commit 1f727ef

Please sign in to comment.