commit
1f727eff03
|
@ -4,6 +4,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
@ -15,9 +17,12 @@ import (
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/snyk/driftctl/build"
|
"github.com/snyk/driftctl/build"
|
||||||
"github.com/snyk/driftctl/pkg/analyser"
|
"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/memstore"
|
||||||
"github.com/snyk/driftctl/pkg/remote/common"
|
"github.com/snyk/driftctl/pkg/remote/common"
|
||||||
"github.com/snyk/driftctl/pkg/telemetry"
|
"github.com/snyk/driftctl/pkg/telemetry"
|
||||||
|
"github.com/snyk/driftctl/pkg/terraform/hcl"
|
||||||
"github.com/snyk/driftctl/pkg/terraform/lock"
|
"github.com/snyk/driftctl/pkg/terraform/lock"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
@ -146,7 +151,7 @@ func NewScanCmd(opts *pkg.ScanOptions) *cobra.Command {
|
||||||
fl.StringSliceP(
|
fl.StringSliceP(
|
||||||
"from",
|
"from",
|
||||||
"f",
|
"f",
|
||||||
[]string{"tfstate://terraform.tfstate"},
|
[]string{},
|
||||||
"IaC sources, by default try to find local terraform.tfstate file\n"+
|
"IaC sources, by default try to find local terraform.tfstate file\n"+
|
||||||
"Accepted schemes are: "+strings.Join(supplier.GetSupportedSchemes(), ",")+"\n",
|
"Accepted schemes are: "+strings.Join(supplier.GetSupportedSchemes(), ",")+"\n",
|
||||||
)
|
)
|
||||||
|
@ -259,6 +264,22 @@ func scanRun(opts *pkg.ScanOptions) error {
|
||||||
globaloutput.ChangePrinter(globaloutput.NewConsolePrinter())
|
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()
|
providerLibrary := terraform.NewProviderLibrary()
|
||||||
remoteLibrary := common.NewRemoteLibrary()
|
remoteLibrary := common.NewRemoteLibrary()
|
||||||
|
|
||||||
|
@ -360,3 +381,29 @@ func validateTfProviderVersionString(version string) error {
|
||||||
}
|
}
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,9 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/snyk/driftctl/pkg"
|
"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/snyk/driftctl/test"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/stretchr/testify/assert"
|
"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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
invalid {}
|
|
@ -0,0 +1,7 @@
|
||||||
|
terraform {
|
||||||
|
backend "s3" {
|
||||||
|
bucket = "terraform-state-prod"
|
||||||
|
key = "network/terraform.tfstate"
|
||||||
|
region = "us-east-1"
|
||||||
|
}
|
||||||
|
}
|
|
@ -53,6 +53,9 @@ func (s *GSBackend) Read(p []byte) (int, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *GSBackend) Close() error {
|
func (s *GSBackend) Close() error {
|
||||||
|
if s.storageClient == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if err := s.storageClient.Close(); err != nil {
|
if err := s.storageClient.Close(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
|
}
|
||||||
|
}
|
|
@ -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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
terraform {
|
||||||
|
backend "azurerm" {
|
||||||
|
resource_group_name = "StorageAccount-ResourceGroup"
|
||||||
|
storage_account_name = "abcd1234"
|
||||||
|
container_name = "states"
|
||||||
|
key = "prod.terraform.tfstate"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
terraform {
|
||||||
|
backend "gcs" {
|
||||||
|
bucket = "tf-state-prod"
|
||||||
|
prefix = "terraform/state"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
terraform {
|
||||||
|
backend "local" {
|
||||||
|
path = "terraform-state-prod/network/terraform.tfstate"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
terraform {}
|
|
@ -0,0 +1,7 @@
|
||||||
|
terraform {
|
||||||
|
backend "s3" {
|
||||||
|
bucket = "terraform-state-prod"
|
||||||
|
key = "network/terraform.tfstate"
|
||||||
|
region = "us-east-1"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue