Merge pull request #1555 from snyk/feat/state-discovery-hcl

Discover tfstate from hcl
main
Raphaël 2022-07-06 16:42:02 +04:00 committed by GitHub
commit 1f727eff03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 313 additions and 1 deletions

View File

@ -4,6 +4,8 @@ import (
"fmt"
"os"
"os/signal"
"path"
"path/filepath"
"regexp"
"strings"
"syscall"
@ -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"
@ -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",
)
@ -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()
@ -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
}

View File

@ -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"
@ -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)
})
}
}

View File

@ -0,0 +1 @@
invalid {}

7
pkg/cmd/testdata/backend/s3/s3.tf vendored Normal file
View File

@ -0,0 +1,7 @@
terraform {
backend "s3" {
bucket = "terraform-state-prod"
key = "network/terraform.tfstate"
region = "us-east-1"
}
}

View File

@ -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
}

View File

@ -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),
}
}

View File

@ -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
pkg/terraform/hcl/hcl.go Normal file
View File

@ -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
}

View File

@ -0,0 +1,8 @@
terraform {
backend "azurerm" {
resource_group_name = "StorageAccount-ResourceGroup"
storage_account_name = "abcd1234"
container_name = "states"
key = "prod.terraform.tfstate"
}
}

View File

@ -0,0 +1,6 @@
terraform {
backend "gcs" {
bucket = "tf-state-prod"
prefix = "terraform/state"
}
}

View File

@ -0,0 +1,5 @@
terraform {
backend "local" {
path = "terraform-state-prod/network/terraform.tfstate"
}
}

View File

@ -0,0 +1 @@
terraform {}

View File

@ -0,0 +1,7 @@
terraform {
backend "s3" {
bucket = "terraform-state-prod"
key = "network/terraform.tfstate"
region = "us-east-1"
}
}