Merge branch 'main' into feat/htmlOutput

main
Raphaël 2021-06-22 17:17:15 +02:00 committed by GitHub
commit 9ead8d1523
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 343 additions and 26 deletions

View File

@ -29,7 +29,7 @@ func NewComputedDiffAlert() *ComputedDiffAlert {
}
func (c *ComputedDiffAlert) Message() string {
return "You have diffs on computed fields, check the documentation for potential false positive drifts"
return "You have diffs on computed fields, check the documentation for potential false positive drifts: https://docs.driftctl.com/limitations"
}
func (c *ComputedDiffAlert) ShouldIgnoreResource() bool {

View File

@ -157,6 +157,11 @@ func NewScanCmd() *cobra.Command {
false,
"Includes cloud provider service-linked roles (disabled by default)",
)
fl.StringVar(&opts.DriftignorePath,
"driftignore",
".driftignore",
"Path to the driftignore file",
)
configDir, err := homedir.Dir()
if err != nil {

View File

@ -80,7 +80,7 @@
"alerts": {
"": [
{
"message": "You have diffs on computed fields, check the documentation for potential false positive drifts"
"message": "You have diffs on computed fields, check the documentation for potential false positive drifts: https://docs.driftctl.com/limitations"
}
]
}

View File

@ -11,4 +11,4 @@ Found 1 resource(s)
- 0 not covered by IaC
- 0 missing on cloud provider
- 1/1 changed outside of IaC
You have diffs on computed fields, check the documentation for potential false positive drifts
You have diffs on computed fields, check the documentation for potential false positive drifts: https://docs.driftctl.com/limitations

View File

@ -45,6 +45,8 @@ func TestScanCmd_Valid(t *testing.T) {
{args: []string{"scan", "--strict"}},
{args: []string{"scan", "--tf-provider-version", "1.2.3"}},
{args: []string{"scan", "--tf-provider-version", "3.30.2"}},
{args: []string{"scan", "--driftignore", "./path/to/driftignore.s3"}},
{args: []string{"scan", "--driftignore", ".driftignore"}},
}
for _, tt := range cases {
@ -85,6 +87,7 @@ func TestScanCmd_Invalid(t *testing.T) {
{args: []string{"scan", "--filter", "Type='test'", "--filter", "Type='test2'"}, expected: "Filter flag should be specified only once"},
{args: []string{"scan", "--tf-provider-version", ".30.2"}, expected: "Invalid version argument .30.2, expected a valid semver string (e.g. 2.13.4)"},
{args: []string{"scan", "--tf-provider-version", "foo"}, expected: "Invalid version argument foo, expected a valid semver string (e.g. 2.13.4)"},
{args: []string{"scan", "--driftignore"}, expected: "flag needs an argument: --driftignore"},
}
for _, tt := range cases {

View File

@ -31,6 +31,7 @@ type ScanOptions struct {
DisableTelemetry bool
ProviderVersion string
ConfigDir string
DriftignorePath string
}
type DriftCTL struct {
@ -38,12 +39,11 @@ type DriftCTL struct {
iacSupplier resource.Supplier
alerter alerter.AlerterInterface
analyzer analyser.Analyzer
filter *jmespath.JMESPath
resourceFactory resource.ResourceFactory
strictMode bool
scanProgress globaloutput.Progress
iacProgress globaloutput.Progress
resourceSchemaRepository resource.SchemaRepositoryInterface
opts *ScanOptions
}
func NewDriftCTL(remoteSupplier resource.Supplier,
@ -59,12 +59,11 @@ func NewDriftCTL(remoteSupplier resource.Supplier,
iacSupplier,
alerter,
analyser.NewAnalyzer(alerter),
opts.Filter,
resFactory,
opts.StrictMode,
scanProgress,
iacProgress,
resourceSchemaRepository,
opts,
}
}
@ -98,9 +97,10 @@ func (d DriftCTL) Run() (*analyser.Analysis, error) {
middlewares.NewAwsDefaultSqsQueuePolicy(),
middlewares.NewAwsSNSTopicPolicyExpander(d.resourceFactory, d.resourceSchemaRepository),
middlewares.NewAwsRoleManagedPolicyExpander(d.resourceFactory),
middlewares.NewTagsAllManager(),
)
if !d.strictMode {
if !d.opts.StrictMode {
middleware = append(middleware,
middlewares.NewAwsDefaults(),
)
@ -112,8 +112,8 @@ func (d DriftCTL) Run() (*analyser.Analysis, error) {
return nil, err
}
if d.filter != nil {
engine := filter.NewFilterEngine(d.filter)
if d.opts.Filter != nil {
engine := filter.NewFilterEngine(d.opts.Filter)
remoteResources, err = engine.Run(remoteResources)
if err != nil {
return nil, err
@ -125,7 +125,7 @@ func (d DriftCTL) Run() (*analyser.Analysis, error) {
}
logrus.Debug("Checking for driftignore")
driftIgnore := filter.NewDriftIgnore()
driftIgnore := filter.NewDriftIgnore(d.opts.DriftignorePath)
analysis, err := d.analyzer.Analyze(remoteResources, resourcesFromState, driftIgnore)
if err != nil {

View File

@ -1559,6 +1559,14 @@ func TestDriftctlRun_Middlewares(t *testing.T) {
},
},
},
&resource.AbstractResource{
Id: "role_with_managed_policy_attr-arn2",
Type: aws.AwsIamPolicyAttachmentResourceType,
Attrs: &resource.Attributes{
"policy_arn": "arn2",
"roles": []interface{}{"role_with_managed_policy_attr"},
},
},
&resource.AbstractResource{
Id: "role_with_empty_managed_policy_attribute",
Type: aws.AwsIamRoleResourceType,

View File

@ -15,13 +15,15 @@ type DriftIgnore struct {
resExclusionList map[string]struct{} // map[type.id] exists to ignore
resExclusionWildcardList map[string]struct{} // map[type.id] exists with wildcard to ignore
driftExclusionList map[string][]string // map[type.id] contains path for drift to ignore
driftignorePath string
}
func NewDriftIgnore() *DriftIgnore {
func NewDriftIgnore(path string) *DriftIgnore {
d := DriftIgnore{
resExclusionList: map[string]struct{}{},
resExclusionWildcardList: map[string]struct{}{},
driftExclusionList: map[string][]string{},
driftignorePath: path,
}
err := d.readIgnoreFile()
if err != nil {
@ -31,7 +33,7 @@ func NewDriftIgnore() *DriftIgnore {
}
func (r *DriftIgnore) readIgnoreFile() error {
file, err := os.Open(".driftignore")
file, err := os.Open(r.driftignorePath)
if err != nil {
return err
}

View File

@ -2,7 +2,6 @@ package filter
import (
"os"
"path"
"reflect"
"strings"
"testing"
@ -19,6 +18,7 @@ func TestDriftIgnore_IsResourceIgnored(t *testing.T) {
name string
resources []resource.Resource
want []bool
path string
}{
{
name: "drift_ignore_no_file",
@ -28,10 +28,10 @@ func TestDriftIgnore_IsResourceIgnored(t *testing.T) {
Id: "id1",
},
},
want: []bool{
false,
},
path: "testdata/drift_ignore_no_file/.driftignore",
},
{
name: "drift_ignore_empty",
@ -44,6 +44,7 @@ func TestDriftIgnore_IsResourceIgnored(t *testing.T) {
want: []bool{
false,
},
path: "testdata/drift_ignore_empty/.driftignore",
},
{
name: "drift_ignore_invalid_lines",
@ -61,6 +62,7 @@ func TestDriftIgnore_IsResourceIgnored(t *testing.T) {
false,
true,
},
path: "testdata/drift_ignore_invalid_lines/.driftignore",
},
{
name: "drift_ignore_valid",
@ -108,6 +110,7 @@ func TestDriftIgnore_IsResourceIgnored(t *testing.T) {
true,
true,
},
path: "testdata/drift_ignore_valid/.driftignore",
},
{
name: "drift_ignore_wildcard",
@ -150,16 +153,15 @@ func TestDriftIgnore_IsResourceIgnored(t *testing.T) {
false,
true,
},
path: "testdata/drift_ignore_wildcard/.driftignore",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cwd, _ := os.Getwd()
defer func() { _ = os.Chdir(cwd) }()
if err := os.Chdir(path.Join("testdata", tt.name)); err != nil {
t.Fatal(err)
}
r := NewDriftIgnore()
r := NewDriftIgnore(tt.path)
got := make([]bool, 0, len(tt.want))
for _, res := range tt.resources {
got = append(got, r.IsResourceIgnored(res))
@ -180,6 +182,7 @@ func TestDriftIgnore_IsFieldIgnored(t *testing.T) {
tests := []struct {
name string
args []Args
path string
}{
{
name: "drift_ignore_no_file",
@ -196,6 +199,7 @@ func TestDriftIgnore_IsFieldIgnored(t *testing.T) {
Want: false,
},
},
path: "testdata/drift_ignore_no_file/.driftignore",
},
{
name: "drift_ignore_empty",
@ -211,6 +215,7 @@ func TestDriftIgnore_IsFieldIgnored(t *testing.T) {
Want: false,
},
},
path: "testdata/drift_ignore_empty/.driftignore",
},
{
name: "drift_ignore_fields",
@ -281,16 +286,15 @@ func TestDriftIgnore_IsFieldIgnored(t *testing.T) {
Want: true,
},
},
path: "testdata/drift_ignore_fields/.driftignore",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cwd, _ := os.Getwd()
defer func() { _ = os.Chdir(cwd) }()
if err := os.Chdir(path.Join("testdata", tt.name)); err != nil {
t.Fatal(err)
}
r := NewDriftIgnore()
r := NewDriftIgnore(tt.path)
for _, arg := range tt.args {
got := r.IsFieldIgnored(arg.Res, arg.Path)
if arg.Want != got {

View File

@ -47,7 +47,10 @@ func (a AwsInstanceBlockDeviceResourceMapper) Execute(remoteResources, resources
"size": rootBlock["volume_size"],
"type": rootBlock["volume_type"],
"multi_attach_enabled": false,
"tags": (*instance.Attrs)["volume_tags"],
"tags": a.volumeTags(instance, rootBlock),
}
if throughput, exist := rootBlock["throughput"]; exist {
data["throughput"] = throughput
}
newRes := a.resourceFactory.CreateAbstractResource("aws_ebs_volume", rootBlock["volume_id"].(string), data)
newStateResources = append(newStateResources, newRes)
@ -72,7 +75,10 @@ func (a AwsInstanceBlockDeviceResourceMapper) Execute(remoteResources, resources
"size": blockDevice["volume_size"],
"type": blockDevice["volume_type"],
"multi_attach_enabled": false,
"tags": (*instance.Attrs)["volume_tags"],
"tags": a.volumeTags(instance, blockDevice),
}
if throughput, exist := blockDevice["throughput"]; exist {
data["throughput"] = throughput
}
newRes := a.resourceFactory.CreateAbstractResource("aws_ebs_volume", blockDevice["volume_id"].(string), data)
newStateResources = append(newStateResources, newRes)
@ -101,3 +107,10 @@ func (a AwsInstanceBlockDeviceResourceMapper) Execute(remoteResources, resources
return nil
}
func (a AwsInstanceBlockDeviceResourceMapper) volumeTags(instance *resource.AbstractResource, blockDevice map[string]interface{}) interface{} {
if tags, exist := instance.Attrs.Get("volume_tags"); exist {
return tags
}
return blockDevice["tags"]
}

View File

@ -48,6 +48,7 @@ func TestAwsInstanceBlockDeviceResourceMapper_Execute(t *testing.T) {
"kms_key_id": "kms",
"size": 8,
"type": "gp2",
"throughput": 125,
"tags": map[string]interface{}{
"Name": "rootVol",
},
@ -63,6 +64,7 @@ func TestAwsInstanceBlockDeviceResourceMapper_Execute(t *testing.T) {
"availability_zone": "eu-west-3",
"size": 23,
"type": "gp2",
"throughput": 125,
"tags": map[string]interface{}{
"Name": "rootVol",
},
@ -96,6 +98,7 @@ func TestAwsInstanceBlockDeviceResourceMapper_Execute(t *testing.T) {
"encrypted": true,
"kms_key_id": "kms",
"volume_size": 8,
"throughput": 125,
"iops": 1234,
},
},
@ -107,6 +110,7 @@ func TestAwsInstanceBlockDeviceResourceMapper_Execute(t *testing.T) {
"encrypted": true,
"delete_on_termination": true,
"volume_size": 23,
"throughput": 125,
},
},
},
@ -126,6 +130,7 @@ func TestAwsInstanceBlockDeviceResourceMapper_Execute(t *testing.T) {
"kms_key_id": "kms",
"size": 8,
"type": "gp2",
"throughput": 125,
"tags": map[string]interface{}{
"Name": "rootVol",
},
@ -145,6 +150,7 @@ func TestAwsInstanceBlockDeviceResourceMapper_Execute(t *testing.T) {
"availability_zone": "eu-west-3",
"size": 23,
"type": "gp2",
"throughput": 125,
"tags": map[string]interface{}{
"Name": "rootVol",
},
@ -156,6 +162,139 @@ func TestAwsInstanceBlockDeviceResourceMapper_Execute(t *testing.T) {
},
false,
},
{
"Test with tags inside root/ebs block device",
struct {
expectedResource *[]resource.Resource
resourcesFromState *[]resource.Resource
}{
expectedResource: &[]resource.Resource{
&resource.AbstractResource{
Id: "dummy-instance",
Type: "aws_instance",
Attrs: &resource.Attributes{
"availability_zone": "eu-west-3",
},
},
&resource.AbstractResource{
Id: "vol-02862d9b39045a3a4",
Type: "aws_ebs_volume",
Attrs: &resource.Attributes{
"id": "vol-02862d9b39045a3a4",
"encrypted": true,
"multi_attach_enabled": false,
"availability_zone": "eu-west-3",
"iops": 1234,
"kms_key_id": "kms",
"size": 8,
"type": "gp2",
"throughput": 125,
"tags": map[string]interface{}{
"Name": "rootVol",
},
},
},
&resource.AbstractResource{
Id: "vol-018c5ae89895aca4c",
Type: "aws_ebs_volume",
Attrs: &resource.Attributes{
"id": "vol-018c5ae89895aca4c",
"encrypted": true,
"multi_attach_enabled": false,
"availability_zone": "eu-west-3",
"size": 23,
"type": "gp2",
"throughput": 125,
"tags": map[string]interface{}{
"Name": "ebsVol",
},
},
},
},
resourcesFromState: &[]resource.Resource{
&resource.AbstractResource{
Id: "dummy-instance",
Type: "aws_instance",
Attrs: &resource.Attributes{
"availability_zone": "eu-west-3",
"root_block_device": []interface{}{
map[string]interface{}{
"volume_id": "vol-02862d9b39045a3a4",
"volume_type": "gp2",
"device_name": "/dev/sda1",
"encrypted": true,
"kms_key_id": "kms",
"volume_size": 8,
"throughput": 125,
"iops": 1234,
"tags": map[string]interface{}{
"Name": "rootVol",
},
},
},
"ebs_block_device": []interface{}{
map[string]interface{}{
"volume_id": "vol-018c5ae89895aca4c",
"volume_type": "gp2",
"device_name": "/dev/sdb",
"encrypted": true,
"delete_on_termination": true,
"volume_size": 23,
"throughput": 125,
"tags": map[string]interface{}{
"Name": "ebsVol",
},
},
},
},
},
},
},
func(factory *terraform.MockResourceFactory) {
foo := resource.AbstractResource{
Id: "vol-02862d9b39045a3a4",
Type: "aws_ebs_volume",
Attrs: &resource.Attributes{
"id": "vol-02862d9b39045a3a4",
"encrypted": true,
"multi_attach_enabled": false,
"availability_zone": "eu-west-3",
"iops": 1234,
"kms_key_id": "kms",
"size": 8,
"type": "gp2",
"throughput": 125,
"tags": map[string]interface{}{
"Name": "rootVol",
},
},
}
factory.On("CreateAbstractResource", "aws_ebs_volume", mock.Anything, mock.MatchedBy(func(input map[string]interface{}) bool {
return input["id"] == "vol-02862d9b39045a3a4" && len(input["tags"].(map[string]interface{})) == 1
})).Times(1).Return(&foo, nil)
bar := resource.AbstractResource{
Id: "vol-018c5ae89895aca4c",
Type: "aws_ebs_volume",
Attrs: &resource.Attributes{
"id": "vol-018c5ae89895aca4c",
"encrypted": true,
"multi_attach_enabled": false,
"availability_zone": "eu-west-3",
"size": 23,
"type": "gp2",
"throughput": 125,
"tags": map[string]interface{}{
"Name": "ebsVol",
},
},
}
factory.On("CreateAbstractResource", "aws_ebs_volume", mock.Anything, mock.MatchedBy(func(input map[string]interface{}) bool {
return input["id"] == "vol-018c5ae89895aca4c" && len(input["tags"].(map[string]interface{})) == 1
})).Times(1).Return(&bar, nil)
},
false,
},
}
for _, c := range tests {
t.Run(c.name, func(tt *testing.T) {

View File

@ -61,6 +61,8 @@ func (a AwsRoleManagedPolicyExpander) Execute(remoteResources, resourcesFromStat
for _, arn := range managedPolicyArns {
arn := arn.(string)
id := fmt.Sprintf("%s-%s", *roleName, arn)
policyAttachmentData := resource.Attributes{
"policy_arn": arn,
"users": []interface{}{},
@ -73,7 +75,19 @@ func (a AwsRoleManagedPolicyExpander) Execute(remoteResources, resourcesFromStat
"policy_arn": arn,
}).Debug("Expanded managed_policy_arns from role")
newList = append(newList, a.resourceFactory.CreateAbstractResource(aws.AwsIamPolicyAttachmentResourceType, fmt.Sprintf("%s-%s", *roleName, arn), policyAttachmentData))
newRes := a.resourceFactory.CreateAbstractResource(aws.AwsIamPolicyAttachmentResourceType, id, policyAttachmentData)
alreadyExist := false
for _, resInState := range *resourcesFromState {
if resource.IsSameResource(resInState, newRes) {
alreadyExist = true
break
}
}
if !alreadyExist {
newList = append(newList, newRes)
}
}
res.Attributes().SafeDelete([]string{"managed_policy_arns"})

View File

@ -0,0 +1,31 @@
package middlewares
import (
"github.com/cloudskiff/driftctl/pkg/resource"
)
// Manage tags_all attribute on each compatible resources
type TagsAllManager struct{}
func NewTagsAllManager() TagsAllManager {
return TagsAllManager{}
}
func (a TagsAllManager) Execute(remoteResources, resourcesFromState *[]resource.Resource) error {
for _, remoteRes := range *remoteResources {
if res, ok := remoteRes.(*resource.AbstractResource); ok {
if _, exist := res.Attrs.Get("tags_all"); exist {
res.Attrs.SafeDelete([]string{"tags_all"})
}
}
}
for _, stateRes := range *resourcesFromState {
if res, ok := stateRes.(*resource.AbstractResource); ok {
if allTags, exist := res.Attrs.Get("tags_all"); exist {
_ = res.Attrs.SafeSet([]string{"tags"}, allTags)
res.Attrs.SafeDelete([]string{"tags_all"})
}
}
}
return nil
}

View File

@ -0,0 +1,98 @@
package middlewares
import (
"strings"
"testing"
"github.com/aws/aws-sdk-go/aws/awsutil"
"github.com/cloudskiff/driftctl/pkg/resource"
"github.com/r3labs/diff/v2"
)
func TestTagsAllManager_Execute(t *testing.T) {
tests := []struct {
name string
remoteResources *[]resource.Resource
resourcesFromState *[]resource.Resource
wantErr bool
}{
{
name: "With multiple resources that are tags_all compatible",
remoteResources: &[]resource.Resource{
&resource.AbstractResource{
Id: "dummy-instance",
Type: "aws_instance",
Attrs: &resource.Attributes{
"tags": map[string]interface{}{
"Name": "toto",
"Terraform": "true",
},
"tags_all": map[string]interface{}{
"Name": "toto",
"Terraform": "true",
},
},
},
&resource.AbstractResource{
Id: "dummy-ebs-volume",
Type: "aws_ebs_volume",
Attrs: &resource.Attributes{
"tags": map[string]interface{}{
"Name": "tata",
"Terraform": "true",
},
"tags_all": map[string]interface{}{
"Name": "tata",
"Terraform": "true",
},
},
},
},
resourcesFromState: &[]resource.Resource{
&resource.AbstractResource{
Id: "dummy-instance",
Type: "aws_instance",
Attrs: &resource.Attributes{
"tags": map[string]interface{}{
"Name": "toto",
},
"tags_all": map[string]interface{}{
"Name": "toto",
"Terraform": "true",
},
},
},
&resource.AbstractResource{
Id: "dummy-ebs-volume",
Type: "aws_ebs_volume",
Attrs: &resource.Attributes{
"tags": map[string]interface{}{
"Name": "tata",
},
"tags_all": map[string]interface{}{
"Name": "tata",
"Terraform": "true",
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := NewTagsAllManager()
if err := a.Execute(tt.remoteResources, tt.resourcesFromState); (err != nil) != tt.wantErr {
t.Errorf("Execute() error = %v, wantErr %v", err, tt.wantErr)
}
changelog, err := diff.Diff(tt.resourcesFromState, tt.remoteResources)
if err != nil {
t.Error(err)
}
if len(changelog) > 0 {
for _, change := range changelog {
t.Errorf("%s got = %v, want %v", strings.Join(change.Path, "."), awsutil.Prettify(change.From), awsutil.Prettify(change.To))
}
}
})
}
}