Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

terraform 0.15 support #860

Merged
merged 2 commits into from
Jul 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,21 @@ replace (

require (
github.com/VerbalExpressions/GoVerbalExpressions v0.0.0-20200410162751-4d76a1099a6e
github.com/apparentlymart/go-textseg/v12 v12.0.0 // indirect
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may need to run go mod tidy on this PR

github.com/awslabs/goformation/v4 v4.19.1
github.com/ghodss/yaml v1.0.0
github.com/go-errors/errors v1.0.1
github.com/google/uuid v1.2.0
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/hashicorp/go-cleanhttp v0.5.1
github.com/hashicorp/go-getter v1.5.1
github.com/hashicorp/go-multierror v1.1.0
github.com/hashicorp/go-getter v1.5.2
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-retryablehttp v0.6.6
github.com/hashicorp/go-version v1.2.1
github.com/hashicorp/hcl v1.0.0
github.com/hashicorp/hcl/v2 v2.8.2
github.com/hashicorp/terraform v0.14.4
github.com/hashicorp/hcl/v2 v2.10.0
github.com/hashicorp/terraform v0.15.3
github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734
github.com/iancoleman/strcase v0.1.3
github.com/itchyny/gojq v0.12.1
Expand All @@ -42,6 +43,8 @@ require (
go.uber.org/zap v1.16.0
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
golang.org/x/tools v0.1.4 // indirect
github.com/zclconf/go-cty v1.8.3
go.uber.org/zap v1.16.0
gopkg.in/src-d/go-git.v4 v4.13.1
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
helm.sh/helm/v3 v3.4.0
Expand Down
121 changes: 81 additions & 40 deletions go.sum

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion pkg/iac-providers/terraform.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package iacprovider

import (
tfv15 "github.com/accurics/terrascan/pkg/iac-providers/terraform/v15"
"reflect"

tfv12 "github.com/accurics/terrascan/pkg/iac-providers/terraform/v12"
Expand All @@ -13,7 +14,8 @@ const (
terraformV12 supportedIacVersion = "v12"
terraformV13 supportedIacVersion = "v13"
terraformV14 supportedIacVersion = "v14"
terraformDefaultVersion = terraformV14
terraformV15 supportedIacVersion = "v15"
terraformDefaultVersion = terraformV15
)

// register terraform as an IaC provider with terrascan
Expand All @@ -22,4 +24,5 @@ func init() {
RegisterIacProvider(terraform, terraformV12, terraformDefaultVersion, reflect.TypeOf(tfv12.TfV12{}))
RegisterIacProvider(terraform, terraformV13, terraformDefaultVersion, reflect.TypeOf(tfv14.TfV14{}))
RegisterIacProvider(terraform, terraformV14, terraformDefaultVersion, reflect.TypeOf(tfv14.TfV14{}))
RegisterIacProvider(terraform, terraformV15, terraformDefaultVersion, reflect.TypeOf(tfv15.TfV15{}))
}
30 changes: 30 additions & 0 deletions pkg/iac-providers/terraform/v15/load-dir.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
Copyright (C) 2020 Accurics, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package tfv15

import (
commons "github.com/accurics/terrascan/pkg/iac-providers/terraform/commons"

"github.com/accurics/terrascan/pkg/iac-providers/output"
)

// LoadIacDir starts traversing from the given rootDir and traverses through
// all the descendant modules present to create an output list of all the
// resources present in rootDir and descendant modules
func (*TfV15) LoadIacDir(absRootDir string, nonRecursive bool) (allResourcesConfig output.AllResourceConfigs, err error) {
return commons.NewTerraformDirectoryLoader(absRootDir, nonRecursive).LoadIacDir()
}
297 changes: 297 additions & 0 deletions pkg/iac-providers/terraform/v15/load-dir_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
/*
Copyright (C) 2020 Accurics, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package tfv15

import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"syscall"
"testing"

"github.com/accurics/terrascan/pkg/iac-providers/output"
commons_test "github.com/accurics/terrascan/pkg/iac-providers/terraform/commons/test"
"github.com/accurics/terrascan/pkg/utils"
"github.com/hashicorp/go-multierror"
)

func TestLoadIacDir(t *testing.T) {
var nilMultiErr *multierror.Error = nil

testErrorMessage := fmt.Sprintf(`failed to load terraform config dir '%s'. error from terraform:
%s:1,21-2,1: Invalid block definition; A block definition must have block content delimited by "{" and "}", starting on the same line as the block header.
%s:1,1-5: Unsupported block type; Blocks of type "some" are not expected here.
`, testDataDir, emptyTfFilePath, emptyTfFilePath)

errStringInvalidModuleConfigs := fmt.Sprintf(`failed to build unified config. errors:
<nil>: Failed to read module directory; Module directory %s does not exist or cannot be read.
`, filepath.Join(testDataDir, "invalid-moduleconfigs", "cloudfront", "sub-cloudfront"))

errStringDependsOnDir := fmt.Sprintf(`failed to build unified config. errors:
<nil>: Failed to read module directory; Module directory %s does not exist or cannot be read.
<nil>: Failed to read module directory; Module directory %s does not exist or cannot be read.
`, filepath.Join(testDataDir, "depends_on", "live", "log"), filepath.Join(testDataDir, "depends_on", "live", "security"))

errStringModuleSourceInvalid := fmt.Sprintf(`failed to build unified config. errors:
<nil>: Invalid module config directory; Module directory '%s' has no terraform config files for module cloudfront
<nil>: Invalid module config directory; Module directory '%s' has no terraform config files for module m1
`, filepath.Join(testDataDir, "invalid-module-source"), filepath.Join(testDataDir, "invalid-module-source"))

testDirPath1 := "not-there"
testDirPath2 := filepath.Join(testDataDir, "testfile")
invalidDirErrStringTemplate := "directory '%s' has no terraform config files"

pathErr := &os.PathError{Op: "lstat", Path: "not-there", Err: syscall.ENOENT}
if utils.IsWindowsPlatform() {
pathErr = &os.PathError{Op: "CreateFile", Path: "not-there", Err: syscall.ENOENT}
}

table := []struct {
name string
dirPath string
tfv15 TfV15
want output.AllResourceConfigs
nonRecursive bool
wantErr error
}{
{
name: "invalid dirPath",
dirPath: testDirPath1,
tfv15: TfV15{},
nonRecursive: true,
wantErr: multierror.Append(fmt.Errorf(invalidDirErrStringTemplate, testDirPath1)),
},
{
name: "invalid dirPath recursive",
dirPath: testDirPath1,
tfv15: TfV15{},
wantErr: multierror.Append(pathErr),
},
{
name: "empty config",
dirPath: testDirPath2,
tfv15: TfV15{},
nonRecursive: true,
wantErr: multierror.Append(fmt.Errorf(invalidDirErrStringTemplate, testDirPath2)),
},
{
name: "empty config recursive",
dirPath: testDirPath2,
tfv15: TfV15{},
wantErr: nilMultiErr,
},
{
name: "incorrect module structure",
dirPath: filepath.Join(testDataDir, "invalid-moduleconfigs"),
tfv15: TfV15{},
nonRecursive: true,
wantErr: multierror.Append(fmt.Errorf("failed to build terraform allResourcesConfig")),
},
{
name: "incorrect module structure recursive",
dirPath: filepath.Join(testDataDir, "invalid-moduleconfigs"),
tfv15: TfV15{},
// same error is loaded two times because, both root module and a child module will generated same error
wantErr: multierror.Append(fmt.Errorf(errStringInvalidModuleConfigs), fmt.Errorf(errStringInvalidModuleConfigs)),
},
{
name: "load invalid config dir",
dirPath: testDataDir,
tfv15: TfV15{},
nonRecursive: true,
wantErr: multierror.Append(fmt.Errorf(testErrorMessage)),
},
{
name: "load invalid config dir recursive",
dirPath: testDataDir,
tfv15: TfV15{},
wantErr: multierror.Append(fmt.Errorf(testErrorMessage),
fmt.Errorf(invalidDirErrStringTemplate, filepath.Join(testDataDir, "deep-modules", "modules")),
fmt.Errorf(invalidDirErrStringTemplate, filepath.Join(testDataDir, "deep-modules", "modules", "m4", "modules")),
fmt.Errorf(errStringDependsOnDir),
fmt.Errorf(invalidDirErrStringTemplate, filepath.Join(testDataDir, "invalid-module-source")),
fmt.Errorf(errStringModuleSourceInvalid),
fmt.Errorf(errStringInvalidModuleConfigs),
fmt.Errorf(errStringInvalidModuleConfigs),
fmt.Errorf(invalidDirErrStringTemplate, filepath.Join(testDataDir, "relative-moduleconfigs")),
fmt.Errorf(invalidDirErrStringTemplate, filepath.Join(testDataDir, "tfjson")),
),
},
{
name: "invalid module source directory",
dirPath: filepath.Join(testDataDir, "invalid-module-source", "invalid_source"),
tfv15: TfV15{},
wantErr: multierror.Append(fmt.Errorf(errStringModuleSourceInvalid)),
},
{
name: "provider block with only alias",
dirPath: filepath.Join(testDataDir, "provider-with-only-alias"),
tfv15: TfV15{},
wantErr: nilMultiErr,
},
{
name: "required provider with configuration aliases",
dirPath: filepath.Join(testDataDir, "required-providers", "configuration-alias"),
tfv15: TfV15{},
wantErr: nilMultiErr,
},
}

for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
_, gotErr := tt.tfv15.LoadIacDir(tt.dirPath, tt.nonRecursive)
me, ok := gotErr.(*multierror.Error)
if !ok {
t.Errorf("expected multierror.Error, got %T", gotErr)
}
if tt.wantErr == nilMultiErr {
if err := me.ErrorOrNil(); err != nil {
t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr)
}
} else if me.Error() != tt.wantErr.Error() {
t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr)
}
})
}

tfJSONDir := filepath.Join(testDataDir, "tfjson")
nestedModuleErr1 := fmt.Errorf(invalidDirErrStringTemplate, filepath.Join(testDataDir, "deep-modules", "modules"))
nestedModuleErr2 := fmt.Errorf(invalidDirErrStringTemplate, filepath.Join(testDataDir, "deep-modules", "modules", "m4", "modules"))

table2 := []struct {
name string
tfConfigDir string
tfJSONFile string
tfv15 TfV15
nonRecursive bool
wantErr error
}{
{
name: "config1",
tfConfigDir: filepath.Join(testDataDir, "tfconfigs"),
tfJSONFile: filepath.Join(tfJSONDir, "fullconfig.json"),
tfv15: TfV15{},
nonRecursive: true,
wantErr: nilMultiErr,
},
{
name: "config1 recursive",
tfConfigDir: filepath.Join(testDataDir, "tfconfigs"),
// no change in the output expected as the config dir doesn't contain subfolder
tfJSONFile: filepath.Join(tfJSONDir, "fullconfig.json"),
tfv15: TfV15{},
wantErr: nilMultiErr,
},
{
name: "module directory",
tfConfigDir: filepath.Join(testDataDir, "moduleconfigs"),
tfJSONFile: filepath.Join(tfJSONDir, "moduleconfigs.json"),
tfv15: TfV15{},
nonRecursive: true,
wantErr: nilMultiErr,
},
{
name: "module directory recursive",
tfConfigDir: filepath.Join(testDataDir, "moduleconfigs"),
// no change in the output expected as the config dir doesn't contain subfolder
tfJSONFile: filepath.Join(tfJSONDir, "moduleconfigs.json"),
tfv15: TfV15{},
wantErr: nilMultiErr,
},
{
name: "nested module directory",
tfConfigDir: filepath.Join(testDataDir, "deep-modules"),
tfJSONFile: filepath.Join(tfJSONDir, "deep-modules.json"),
tfv15: TfV15{},
nonRecursive: true,
wantErr: nilMultiErr,
},
{
name: "nested module directory recursive",
tfConfigDir: filepath.Join(testDataDir, "deep-modules"),
tfJSONFile: filepath.Join(tfJSONDir, "deep-modules-recursive.json"),
tfv15: TfV15{},
wantErr: multierror.Append(nestedModuleErr1, nestedModuleErr2),
},
{
name: "complex variables",
tfConfigDir: filepath.Join(testDataDir, "complex-variables"),
tfJSONFile: filepath.Join(tfJSONDir, "complex-variables.json"),
tfv15: TfV15{},
nonRecursive: true,
wantErr: nilMultiErr,
},
{
name: "recursive loop while resolving variables",
tfConfigDir: filepath.Join(testDataDir, "recursive-loop-variables"),
tfJSONFile: filepath.Join(tfJSONDir, "recursive-loop-variables.json"),
tfv15: TfV15{},
nonRecursive: true,
wantErr: nilMultiErr,
},
{
name: "recursive loop while resolving locals",
tfConfigDir: filepath.Join(testDataDir, "recursive-loop-locals"),
tfJSONFile: filepath.Join(tfJSONDir, "recursive-loop-locals.json"),
tfv15: TfV15{},
nonRecursive: true,
wantErr: nilMultiErr,
},
}

for _, tt := range table2 {
t.Run(tt.name, func(t *testing.T) {
got, gotErr := tt.tfv15.LoadIacDir(tt.tfConfigDir, tt.nonRecursive)
me, ok := gotErr.(*multierror.Error)
if !ok {
t.Errorf("expected multierror.Error, got %T", gotErr)
}
if tt.wantErr == nilMultiErr {
if err := me.ErrorOrNil(); err != nil {
t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr)
}
} else if me.Error() != tt.wantErr.Error() {
t.Errorf("unexpected error; gotErr: '%v', wantErr: '%v'", gotErr, tt.wantErr)
}

var want output.AllResourceConfigs

// Read the expected value and unmarshal into want
contents, _ := ioutil.ReadFile(tt.tfJSONFile)
if utils.IsWindowsPlatform() {
contents = utils.ReplaceWinNewLineBytes(contents)
}

err := json.Unmarshal(contents, &want)
if err != nil {
t.Errorf("unexpected error unmarshalling want: %v", err)
}

match, err := commons_test.IdenticalAllResourceConfigs(got, want)
if err != nil {
t.Errorf("unexpected error checking result: %v", err)
}
if !match {
g, _ := json.MarshalIndent(got, "", " ")
w, _ := json.MarshalIndent(want, "", " ")
t.Errorf("got '%v', want: '%v'", string(g), string(w))
}
})
}
}
Loading