Merge branch 'main' into fix/progressBarResume

main
Raphaël 2021-04-26 17:20:02 +02:00 committed by GitHub
commit 30bc979e8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 175390 additions and 255 deletions

View File

@ -13,7 +13,8 @@ import (
type Change struct { type Change struct {
diff.Change diff.Change
Computed bool `json:"computed"` Computed bool `json:"computed"`
JsonString bool `json:"-"`
} }
type Changelog []Change type Changelog []Change

View File

@ -4,7 +4,6 @@ import (
"reflect" "reflect"
resourceaws "github.com/cloudskiff/driftctl/pkg/resource/aws" resourceaws "github.com/cloudskiff/driftctl/pkg/resource/aws"
"github.com/r3labs/diff/v2" "github.com/r3labs/diff/v2"
"github.com/cloudskiff/driftctl/pkg/alerter" "github.com/cloudskiff/driftctl/pkg/alerter"
@ -40,7 +39,8 @@ func (c *ComputedDiffAlert) ShouldIgnoreResource() bool {
} }
type Analyzer struct { type Analyzer struct {
alerter *alerter.Alerter alerter *alerter.Alerter
resourceSchemaRepository resource.SchemaRepositoryInterface
} }
type Filter interface { type Filter interface {
@ -48,8 +48,8 @@ type Filter interface {
IsFieldIgnored(res resource.Resource, path []string) bool IsFieldIgnored(res resource.Resource, path []string) bool
} }
func NewAnalyzer(alerter *alerter.Alerter) Analyzer { func NewAnalyzer(alerter *alerter.Alerter, resourceSchemaRepository resource.SchemaRepositoryInterface) Analyzer {
return Analyzer{alerter} return Analyzer{alerter, resourceSchemaRepository}
} }
func (a Analyzer) Analyze(remoteResources, resourcesFromState []resource.Resource, filter Filter) (Analysis, error) { func (a Analyzer) Analyze(remoteResources, resourcesFromState []resource.Resource, filter Filter) (Analysis, error) {
@ -81,7 +81,14 @@ func (a Analyzer) Analyze(remoteResources, resourcesFromState []resource.Resourc
filteredRemoteResource = removeResourceByIndex(i, filteredRemoteResource) filteredRemoteResource = removeResourceByIndex(i, filteredRemoteResource)
analysis.AddManaged(stateRes) analysis.AddManaged(stateRes)
delta, _ := diff.Diff(stateRes, remoteRes) var delta diff.Changelog
if resource.IsRefactoredResource(stateRes.TerraformType()) {
stateRes, _ := stateRes.(*resource.AbstractResource)
remoteRes, _ := remoteRes.(*resource.AbstractResource)
delta, _ = diff.Diff(stateRes.Attrs, remoteRes.Attrs)
} else {
delta, _ = diff.Diff(stateRes, remoteRes)
}
if len(delta) == 0 { if len(delta) == 0 {
continue continue
@ -93,7 +100,16 @@ func (a Analyzer) Analyze(remoteResources, resourcesFromState []resource.Resourc
continue continue
} }
c := Change{Change: change} c := Change{Change: change}
c.Computed = a.isComputedField(stateRes, c) if resource.IsRefactoredResource(stateRes.TerraformType()) {
resSchema, exist := a.resourceSchemaRepository.GetSchema(stateRes.TerraformType())
if exist {
c.Computed = resSchema.IsComputedField(c.Path)
c.JsonString = resSchema.IsJsonStringField(c.Path)
}
} else {
c.Computed = a.isComputedField(stateRes, c)
c.JsonString = a.isJsonStringField(stateRes, c)
}
if c.Computed { if c.Computed {
haveComputedDiff = true haveComputedDiff = true
} }
@ -152,6 +168,15 @@ func (a Analyzer) isComputedField(stateRes resource.Resource, change Change) boo
return false return false
} }
// isJsonStringField returns true if the field that generated the diff of a resource
// has a jsonfield tag
func (a Analyzer) isJsonStringField(stateRes resource.Resource, change Change) bool {
if field, ok := a.getField(reflect.TypeOf(stateRes), change.Path); ok {
return field.Tag.Get("jsonfield") == "true"
}
return false
}
// getField recursively finds the deepest field inside a resource depending on // getField recursively finds the deepest field inside a resource depending on
// its path and its type // its path and its type
func (a Analyzer) getField(t reflect.Type, path []string) (reflect.StructField, bool) { func (a Analyzer) getField(t reflect.Type, path []string) (reflect.StructField, bool) {

View File

@ -993,7 +993,10 @@ func TestAnalyze(t *testing.T) {
al.SetAlerts(c.alerts) al.SetAlerts(c.alerts)
} }
analyzer := NewAnalyzer(al) repo := testresource.InitFakeSchemaRepository("aws", "3.19.0")
aws.InitResourcesMetadata(repo)
analyzer := NewAnalyzer(al, repo)
result, err := analyzer.Analyze(c.cloud, c.iac, filter) result, err := analyzer.Analyze(c.cloud, c.iac, filter)
if err != nil { if err != nil {

View File

@ -146,7 +146,9 @@ func scanRun(opts *pkg.ScanOptions) error {
progress := globaloutput.NewProgress() progress := globaloutput.NewProgress()
err := remote.Activate(opts.To, alerter, providerLibrary, supplierLibrary, progress) resourceSchemaRepository := resource.NewSchemaRepository()
err := remote.Activate(opts.To, alerter, providerLibrary, supplierLibrary, progress, resourceSchemaRepository)
if err != nil { if err != nil {
return err return err
} }
@ -158,16 +160,16 @@ func scanRun(opts *pkg.ScanOptions) error {
logrus.Trace("Exited") logrus.Trace("Exited")
}() }()
scanner := pkg.NewScanner(supplierLibrary.Suppliers(), alerter) scanner := pkg.NewScanner(supplierLibrary.Suppliers(), alerter, resourceSchemaRepository)
iacSupplier, err := supplier.GetIACSupplier(opts.From, providerLibrary, opts.BackendOptions) iacSupplier, err := supplier.GetIACSupplier(opts.From, providerLibrary, opts.BackendOptions, resourceSchemaRepository)
if err != nil { if err != nil {
return err return err
} }
resFactory := terraform.NewTerraformResourceFactory(providerLibrary) resFactory := terraform.NewTerraformResourceFactory(providerLibrary)
ctl := pkg.NewDriftCTL(scanner, iacSupplier, alerter, resFactory, opts) ctl := pkg.NewDriftCTL(scanner, iacSupplier, alerter, resFactory, opts, resourceSchemaRepository)
go func() { go func() {
<-c <-c

View File

@ -77,8 +77,7 @@ func (c *Console) Write(analysis *analyser.Analysis) error {
pref = fmt.Sprintf("%s %s:", color.RedString("-"), path) pref = fmt.Sprintf("%s %s:", color.RedString("-"), path)
} }
if change.Type == diff.UPDATE { if change.Type == diff.UPDATE {
isJsonString := isFieldJsonString(difference.Res, path) if change.JsonString {
if isJsonString {
prefix := " " prefix := " "
fmt.Printf(" %s\n%s%s\n", pref, prefix, jsonDiff(change.From, change.To, prefix)) fmt.Printf(" %s\n%s%s\n", pref, prefix, jsonDiff(change.From, change.To, prefix))
continue continue
@ -181,23 +180,6 @@ func groupByType(resources []resource.Resource) map[string][]resource.Resource {
return result return result
} }
func isFieldJsonString(res resource.Resource, fieldName string) bool {
t := reflect.TypeOf(res)
var field reflect.StructField
var ok bool
if t.Kind() == reflect.Ptr {
field, ok = t.Elem().FieldByName(fieldName)
}
if t.Kind() != reflect.Ptr {
field, ok = t.FieldByName(fieldName)
}
if !ok {
return false
}
return field.Tag.Get("jsonstring") == "true"
}
func jsonDiff(a, b interface{}, prefix string) string { func jsonDiff(a, b interface{}, prefix string) string {
aStr := fmt.Sprintf("%s", a) aStr := fmt.Sprintf("%s", a)
bStr := fmt.Sprintf("%s", b) bStr := fmt.Sprintf("%s", b)

View File

@ -9,8 +9,11 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/zclconf/go-cty/cty/gocty"
"github.com/cloudskiff/driftctl/pkg/resource/aws"
"github.com/cloudskiff/driftctl/test/goldenfile" "github.com/cloudskiff/driftctl/test/goldenfile"
testresource "github.com/cloudskiff/driftctl/test/resource"
"github.com/cloudskiff/driftctl/pkg/analyser" "github.com/cloudskiff/driftctl/pkg/analyser"
) )
@ -70,6 +73,76 @@ func TestConsole_Write(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
repo := testresource.InitFakeSchemaRepository("aws", "3.19.0")
aws.InitResourcesMetadata(repo)
for _, res := range tt.args.analysis.Managed() {
fakeRes, ok := res.(*testresource.FakeResource)
if ok {
impliedType, _ := gocty.ImpliedType(fakeRes)
value, _ := gocty.ToCtyValue(fakeRes, impliedType)
fakeRes.CtyVal = &value
continue
}
fakeStringerRes, ok := res.(*testresource.FakeResourceStringer)
if ok {
impliedType, _ := gocty.ImpliedType(fakeStringerRes)
value, _ := gocty.ToCtyValue(fakeStringerRes, impliedType)
fakeStringerRes.CtyVal = &value
continue
}
}
for _, res := range tt.args.analysis.Unmanaged() {
fakeRes, ok := res.(*testresource.FakeResource)
if ok {
impliedType, _ := gocty.ImpliedType(fakeRes)
value, _ := gocty.ToCtyValue(fakeRes, impliedType)
fakeRes.CtyVal = &value
continue
}
fakeStringerRes, ok := res.(*testresource.FakeResourceStringer)
if ok {
impliedType, _ := gocty.ImpliedType(fakeStringerRes)
value, _ := gocty.ToCtyValue(fakeStringerRes, impliedType)
fakeStringerRes.CtyVal = &value
continue
}
}
for _, res := range tt.args.analysis.Deleted() {
fakeRes, ok := res.(*testresource.FakeResource)
if ok {
impliedType, _ := gocty.ImpliedType(fakeRes)
value, _ := gocty.ToCtyValue(fakeRes, impliedType)
fakeRes.CtyVal = &value
continue
}
fakeStringerRes, ok := res.(*testresource.FakeResourceStringer)
if ok {
impliedType, _ := gocty.ImpliedType(fakeStringerRes)
value, _ := gocty.ToCtyValue(fakeStringerRes, impliedType)
fakeStringerRes.CtyVal = &value
continue
}
}
for _, d := range tt.args.analysis.Differences() {
fakeRes, ok := d.Res.(*testresource.FakeResource)
if ok {
impliedType, _ := gocty.ImpliedType(fakeRes)
value, _ := gocty.ToCtyValue(fakeRes, impliedType)
fakeRes.CtyVal = &value
continue
}
fakeStringerRes, ok := d.Res.(*testresource.FakeResourceStringer)
if ok {
impliedType, _ := gocty.ImpliedType(fakeStringerRes)
value, _ := gocty.ToCtyValue(fakeStringerRes, impliedType)
fakeStringerRes.CtyVal = &value
continue
}
}
c := NewConsole() c := NewConsole()
stdout := os.Stdout // keep backup of the real stdout stdout := os.Stdout // keep backup of the real stdout

View File

@ -108,6 +108,7 @@ func fakeAnalysisWithJsonFields() *analyser.Analysis {
Type: "aws_diff_resource", Type: "aws_diff_resource",
}, Changelog: []analyser.Change{ }, Changelog: []analyser.Change{
{ {
JsonString: true,
Change: diff.Change{ Change: diff.Change{
Type: diff.UPDATE, Type: diff.UPDATE,
Path: []string{"Json"}, Path: []string{"Json"},
@ -121,6 +122,7 @@ func fakeAnalysisWithJsonFields() *analyser.Analysis {
Type: "aws_diff_resource", Type: "aws_diff_resource",
}, Changelog: []analyser.Change{ }, Changelog: []analyser.Change{
{ {
JsonString: true,
Change: diff.Change{ Change: diff.Change{
Type: diff.UPDATE, Type: diff.UPDATE,
Path: []string{"Json"}, Path: []string{"Json"},
@ -176,7 +178,7 @@ func fakeAnalysisWithComputedFields() *analyser.Analysis {
Type: "aws_diff_resource", Type: "aws_diff_resource",
}, },
) )
a.AddDifference(analyser.Difference{Res: testresource.FakeResource{ a.AddDifference(analyser.Difference{Res: &testresource.FakeResource{
Id: "diff-id-1", Id: "diff-id-1",
Type: "aws_diff_resource", Type: "aws_diff_resource",
}, Changelog: []analyser.Change{ }, Changelog: []analyser.Change{

View File

@ -38,12 +38,12 @@ type DriftCTL struct {
strictMode bool strictMode bool
} }
func NewDriftCTL(remoteSupplier resource.Supplier, iacSupplier resource.Supplier, alerter *alerter.Alerter, resFactory resource.ResourceFactory, opts *ScanOptions) *DriftCTL { func NewDriftCTL(remoteSupplier resource.Supplier, iacSupplier resource.Supplier, alerter *alerter.Alerter, resFactory resource.ResourceFactory, opts *ScanOptions, resourceSchemaRepository resource.SchemaRepositoryInterface) *DriftCTL {
return &DriftCTL{ return &DriftCTL{
remoteSupplier, remoteSupplier,
iacSupplier, iacSupplier,
alerter, alerter,
analyser.NewAnalyzer(alerter), analyser.NewAnalyzer(alerter, resourceSchemaRepository),
opts.Filter, opts.Filter,
resFactory, resFactory,
opts.StrictMode, opts.StrictMode,

View File

@ -16,13 +16,20 @@ import (
filter2 "github.com/cloudskiff/driftctl/pkg/filter" filter2 "github.com/cloudskiff/driftctl/pkg/filter"
"github.com/cloudskiff/driftctl/pkg/resource" "github.com/cloudskiff/driftctl/pkg/resource"
"github.com/cloudskiff/driftctl/pkg/resource/aws" "github.com/cloudskiff/driftctl/pkg/resource/aws"
"github.com/cloudskiff/driftctl/pkg/resource/github"
"github.com/cloudskiff/driftctl/pkg/terraform" "github.com/cloudskiff/driftctl/pkg/terraform"
"github.com/cloudskiff/driftctl/test" "github.com/cloudskiff/driftctl/test"
testresource "github.com/cloudskiff/driftctl/test/resource" testresource "github.com/cloudskiff/driftctl/test/resource"
) )
type TestProvider struct {
Name string
Version string
}
type TestCase struct { type TestCase struct {
name string name string
provider *TestProvider
stateResources []resource.Resource stateResources []resource.Resource
remoteResources []resource.Resource remoteResources []resource.Resource
filter string filter string
@ -34,18 +41,29 @@ type TestCases []TestCase
func runTest(t *testing.T, cases TestCases) { func runTest(t *testing.T, cases TestCases) {
for _, c := range cases { for _, c := range cases {
if c.provider == nil {
c.provider = &TestProvider{
Name: "aws",
Version: "3.19.0",
}
}
repo := testresource.InitFakeSchemaRepository(c.provider.Name, c.provider.Version)
aws.InitResourcesMetadata(repo)
github.InitMetadatas(repo)
t.Run(c.name, func(t *testing.T) { t.Run(c.name, func(t *testing.T) {
testAlerter := alerter.NewAlerter() testAlerter := alerter.NewAlerter()
if c.stateResources == nil { if c.stateResources == nil {
c.stateResources = []resource.Resource{} c.stateResources = []resource.Resource{}
} }
stateSupplier := &resource.MockSupplier{} stateSupplier := &resource.MockSupplier{}
stateSupplier.On("Resources").Return(c.stateResources, nil) stateSupplier.On("Resources").Return(c.stateResources, nil)
if c.remoteResources == nil { if c.remoteResources == nil {
c.remoteResources = []resource.Resource{} c.remoteResources = []resource.Resource{}
} }
remoteSupplier := &resource.MockSupplier{} remoteSupplier := &resource.MockSupplier{}
remoteSupplier.On("Resources").Return(c.remoteResources, nil) remoteSupplier.On("Resources").Return(c.remoteResources, nil)
@ -66,7 +84,7 @@ func runTest(t *testing.T, cases TestCases) {
driftctl := pkg.NewDriftCTL(remoteSupplier, stateSupplier, testAlerter, resourceFactory, &pkg.ScanOptions{ driftctl := pkg.NewDriftCTL(remoteSupplier, stateSupplier, testAlerter, resourceFactory, &pkg.ScanOptions{
Filter: filter, Filter: filter,
}) }, repo)
analysis, err := driftctl.Run() analysis, err := driftctl.Run()
@ -90,10 +108,10 @@ func TestDriftctlRun_BasicBehavior(t *testing.T) {
{ {
name: "infrastructure should be in sync", name: "infrastructure should be in sync",
stateResources: []resource.Resource{ stateResources: []resource.Resource{
testresource.FakeResource{}, &testresource.FakeResource{},
}, },
remoteResources: []resource.Resource{ remoteResources: []resource.Resource{
testresource.FakeResource{}, &testresource.FakeResource{},
}, },
assert: func(result *test.ScanResult, err error) { assert: func(result *test.ScanResult, err error) {
result.AssertInfrastructureIsInSync() result.AssertInfrastructureIsInSync()
@ -102,7 +120,7 @@ func TestDriftctlRun_BasicBehavior(t *testing.T) {
{ {
name: "we should have deleted resource", name: "we should have deleted resource",
stateResources: []resource.Resource{ stateResources: []resource.Resource{
testresource.FakeResource{}, &testresource.FakeResource{},
}, },
remoteResources: []resource.Resource{}, remoteResources: []resource.Resource{},
assert: func(result *test.ScanResult, err error) { assert: func(result *test.ScanResult, err error) {
@ -113,7 +131,7 @@ func TestDriftctlRun_BasicBehavior(t *testing.T) {
name: "we should have unmanaged resource", name: "we should have unmanaged resource",
stateResources: []resource.Resource{}, stateResources: []resource.Resource{},
remoteResources: []resource.Resource{ remoteResources: []resource.Resource{
testresource.FakeResource{}, &testresource.FakeResource{},
}, },
assert: func(result *test.ScanResult, err error) { assert: func(result *test.ScanResult, err error) {
result.AssertUnmanagedCount(1) result.AssertUnmanagedCount(1)
@ -122,13 +140,13 @@ func TestDriftctlRun_BasicBehavior(t *testing.T) {
{ {
name: "we should have changes of field update", name: "we should have changes of field update",
stateResources: []resource.Resource{ stateResources: []resource.Resource{
testresource.FakeResource{ &testresource.FakeResource{
Id: "fake", Id: "fake",
FooBar: "barfoo", FooBar: "barfoo",
}, },
}, },
remoteResources: []resource.Resource{ remoteResources: []resource.Resource{
testresource.FakeResource{ &testresource.FakeResource{
Id: "fake", Id: "fake",
FooBar: "foobar", FooBar: "foobar",
}, },
@ -149,13 +167,13 @@ func TestDriftctlRun_BasicBehavior(t *testing.T) {
{ {
name: "we should have changes on computed field", name: "we should have changes on computed field",
stateResources: []resource.Resource{ stateResources: []resource.Resource{
testresource.FakeResource{ &testresource.FakeResource{
Id: "fake", Id: "fake",
BarFoo: "barfoo", BarFoo: "barfoo",
}, },
}, },
remoteResources: []resource.Resource{ remoteResources: []resource.Resource{
testresource.FakeResource{ &testresource.FakeResource{
Id: "fake", Id: "fake",
BarFoo: "foobar", BarFoo: "foobar",
}, },
@ -176,7 +194,7 @@ func TestDriftctlRun_BasicBehavior(t *testing.T) {
{ {
name: "we should have changes of deleted field", name: "we should have changes of deleted field",
stateResources: []resource.Resource{ stateResources: []resource.Resource{
testresource.FakeResource{ &testresource.FakeResource{
Id: "fake", Id: "fake",
Tags: map[string]string{ Tags: map[string]string{
"tag1": "deleted", "tag1": "deleted",
@ -184,7 +202,7 @@ func TestDriftctlRun_BasicBehavior(t *testing.T) {
}, },
}, },
remoteResources: []resource.Resource{ remoteResources: []resource.Resource{
testresource.FakeResource{ &testresource.FakeResource{
Id: "fake", Id: "fake",
}, },
}, },
@ -204,12 +222,12 @@ func TestDriftctlRun_BasicBehavior(t *testing.T) {
{ {
name: "we should have changes of added field", name: "we should have changes of added field",
stateResources: []resource.Resource{ stateResources: []resource.Resource{
testresource.FakeResource{ &testresource.FakeResource{
Id: "fake", Id: "fake",
}, },
}, },
remoteResources: []resource.Resource{ remoteResources: []resource.Resource{
testresource.FakeResource{ &testresource.FakeResource{
Id: "fake", Id: "fake",
Tags: map[string]string{ Tags: map[string]string{
"tag1": "added", "tag1": "added",

View File

@ -4,11 +4,10 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"github.com/cloudskiff/driftctl/pkg/resource"
"github.com/jmespath/go-jmespath" "github.com/jmespath/go-jmespath"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json" ctyjson "github.com/zclconf/go-cty/cty/json"
"github.com/cloudskiff/driftctl/pkg/resource"
) )
type FilterEngine struct { type FilterEngine struct {
@ -37,13 +36,18 @@ func (e *FilterEngine) Run(resources []resource.Resource) ([]resource.Resource,
// We need to serialize all attributes to untyped interface from JMESPath to work // We need to serialize all attributes to untyped interface from JMESPath to work
// map[string]string and map[string]SomeThing will not work without it // map[string]string and map[string]SomeThing will not work without it
// https://github.com/jmespath/go-jmespath/issues/22 // https://github.com/jmespath/go-jmespath/issues/22
ctyVal := res.CtyValue()
if ctyVal == nil {
ctyVal = &cty.EmptyObjectVal
}
bytes, _ := ctyjson.Marshal(*ctyVal, ctyVal.Type())
var attrs interface{} var attrs interface{}
_ = json.Unmarshal(bytes, &attrs) if abstractRes, ok := res.(*resource.AbstractResource); ok {
attrs = abstractRes.Attrs
} else {
ctyVal := res.CtyValue()
if ctyVal == nil {
ctyVal = &cty.EmptyObjectVal
}
bytes, _ := ctyjson.Marshal(*ctyVal, ctyVal.Type())
_ = json.Unmarshal(bytes, &attrs)
}
f := filtrableResource{ f := filtrableResource{
Attr: attrs, Attr: attrs,
Res: res, Res: res,

View File

@ -28,7 +28,7 @@ func IsSupplierSupported(supplierKey string) bool {
return false return false
} }
func GetIACSupplier(configs []config.SupplierConfig, library *terraform.ProviderLibrary, backendOpts *backend.Options) (resource.Supplier, error) { func GetIACSupplier(configs []config.SupplierConfig, library *terraform.ProviderLibrary, backendOpts *backend.Options, resourceSchemaRepository resource.SchemaRepositoryInterface) (resource.Supplier, error) {
chainSupplier := resource.NewChainSupplier() chainSupplier := resource.NewChainSupplier()
for _, config := range configs { for _, config := range configs {
if !IsSupplierSupported(config.Key) { if !IsSupplierSupported(config.Key) {
@ -39,7 +39,7 @@ func GetIACSupplier(configs []config.SupplierConfig, library *terraform.Provider
var err error var err error
switch config.Key { switch config.Key {
case state.TerraformStateReaderSupplier: case state.TerraformStateReaderSupplier:
supplier, err = state.NewReader(config, library, backendOpts) supplier, err = state.NewReader(config, library, backendOpts, resourceSchemaRepository)
default: default:
return nil, errors.Errorf("Unsupported supplier '%s'", config.Key) return nil, errors.Errorf("Unsupported supplier '%s'", config.Key)
} }

View File

@ -8,6 +8,7 @@ import (
"github.com/cloudskiff/driftctl/pkg/iac/config" "github.com/cloudskiff/driftctl/pkg/iac/config"
"github.com/cloudskiff/driftctl/pkg/iac/terraform/state/backend" "github.com/cloudskiff/driftctl/pkg/iac/terraform/state/backend"
"github.com/cloudskiff/driftctl/pkg/terraform" "github.com/cloudskiff/driftctl/pkg/terraform"
"github.com/cloudskiff/driftctl/test/resource"
) )
func TestGetIACSupplier(t *testing.T) { func TestGetIACSupplier(t *testing.T) {
@ -82,7 +83,8 @@ func TestGetIACSupplier(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
_, err := GetIACSupplier(tt.args.config, terraform.NewProviderLibrary(), tt.args.options) repo := resource.InitFakeSchemaRepository("aws", "3.19.0")
_, err := GetIACSupplier(tt.args.config, terraform.NewProviderLibrary(), tt.args.options, repo)
if tt.wantErr != nil && err.Error() != tt.wantErr.Error() { if tt.wantErr != nil && err.Error() != tt.wantErr.Error() {
t.Errorf("GetIACSupplier() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("GetIACSupplier() error = %v, wantErr %v", err, tt.wantErr)
return return

View File

@ -9,6 +9,7 @@ import (
"github.com/cloudskiff/driftctl/pkg/iac/terraform/state/enumerator" "github.com/cloudskiff/driftctl/pkg/iac/terraform/state/enumerator"
"github.com/cloudskiff/driftctl/pkg/remote/deserializer" "github.com/cloudskiff/driftctl/pkg/remote/deserializer"
"github.com/cloudskiff/driftctl/pkg/resource" "github.com/cloudskiff/driftctl/pkg/resource"
"github.com/cloudskiff/driftctl/pkg/terraform" "github.com/cloudskiff/driftctl/pkg/terraform"
"github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/states"
@ -22,12 +23,13 @@ import (
const TerraformStateReaderSupplier = "tfstate" const TerraformStateReaderSupplier = "tfstate"
type TerraformStateReader struct { type TerraformStateReader struct {
library *terraform.ProviderLibrary library *terraform.ProviderLibrary
config config.SupplierConfig config config.SupplierConfig
backend backend.Backend backend backend.Backend
enumerator enumerator.StateEnumerator enumerator enumerator.StateEnumerator
deserializers []deserializer.CTYDeserializer deserializers []deserializer.CTYDeserializer
backendOptions *backend.Options backendOptions *backend.Options
resourceSchemaRepository resource.SchemaRepositoryInterface
} }
func (r *TerraformStateReader) initReader() error { func (r *TerraformStateReader) initReader() error {
@ -35,8 +37,8 @@ func (r *TerraformStateReader) initReader() error {
return nil return nil
} }
func NewReader(config config.SupplierConfig, library *terraform.ProviderLibrary, backendOpts *backend.Options) (*TerraformStateReader, error) { func NewReader(config config.SupplierConfig, library *terraform.ProviderLibrary, backendOpts *backend.Options, resourceSchemaRepository resource.SchemaRepositoryInterface) (*TerraformStateReader, error) {
reader := TerraformStateReader{library: library, config: config, deserializers: iac.Deserializers(), backendOptions: backendOpts} reader := TerraformStateReader{library: library, config: config, deserializers: iac.Deserializers(), backendOptions: backendOpts, resourceSchemaRepository: resourceSchemaRepository}
err := reader.initReader() err := reader.initReader()
if err != nil { if err != nil {
return nil, err return nil, err
@ -173,6 +175,22 @@ func (r *TerraformStateReader) decode(values map[string][]cty.Value) ([]resource
"id": res.TerraformId(), "id": res.TerraformId(),
"type": res.TerraformType(), "type": res.TerraformType(),
}).Debug("Found IAC resource") }).Debug("Found IAC resource")
if resource.IsRefactoredResource(res.TerraformType()) {
schema, exist := r.resourceSchemaRepository.GetSchema(res.TerraformType())
ctyAttr := resource.ToResourceAttributes(res.CtyValue())
ctyAttr.SanitizeDefaultsV3()
if exist && schema.NormalizeFunc != nil {
schema.NormalizeFunc(ctyAttr)
}
newRes := &resource.AbstractResource{
Id: res.TerraformId(),
Type: res.TerraformType(),
Attrs: ctyAttr,
}
results = append(results, newRes)
continue
}
normalisable, ok := res.(resource.NormalizedResource) normalisable, ok := res.(resource.NormalizedResource)
if ok { if ok {
normalizedRes, err := normalisable.NormalizeForState() normalizedRes, err := normalisable.NormalizeForState()

View File

@ -8,6 +8,7 @@ import (
"testing" "testing"
"github.com/cloudskiff/driftctl/pkg/output" "github.com/cloudskiff/driftctl/pkg/output"
testresource "github.com/cloudskiff/driftctl/test/resource"
"github.com/cloudskiff/driftctl/pkg/iac" "github.com/cloudskiff/driftctl/pkg/iac"
"github.com/cloudskiff/driftctl/pkg/iac/config" "github.com/cloudskiff/driftctl/pkg/iac/config"
@ -115,12 +116,15 @@ func TestTerraformStateReader_AWS_Resources(t *testing.T) {
library := terraform.NewProviderLibrary() library := terraform.NewProviderLibrary()
library.AddProvider(terraform.AWS, provider) library.AddProvider(terraform.AWS, provider)
repo := testresource.InitFakeSchemaRepository(terraform.AWS, "3.19.0")
r := &TerraformStateReader{ r := &TerraformStateReader{
config: config.SupplierConfig{ config: config.SupplierConfig{
Path: path.Join(goldenfile.GoldenFilePath, tt.dirName, "terraform.tfstate"), Path: path.Join(goldenfile.GoldenFilePath, tt.dirName, "terraform.tfstate"),
}, },
library: library, library: library,
deserializers: iac.Deserializers(), deserializers: iac.Deserializers(),
resourceSchemaRepository: repo,
} }
got, err := r.Resources() got, err := r.Resources()

View File

@ -1,173 +1,151 @@
[ [
{ {
"Aliases": null,
"Arn": "arn:aws:cloudfront::047081014315:distribution/E1M9CNS0XSHI19",
"CallerReference": "terraform-20210216101734792900000001",
"Comment": null,
"DefaultRootObject": "",
"DomainName": "d1g0dw0i1wvlgd.cloudfront.net",
"Enabled": false,
"Etag": "E2CKBANLXUPWGQ",
"HostedZoneId": "Z2FDTNDATAQYW2",
"HttpVersion": "http2",
"Id": "E1M9CNS0XSHI19", "Id": "E1M9CNS0XSHI19",
"InProgressValidationBatches": 0, "Type": "aws_cloudfront_distribution",
"IsIpv6Enabled": false, "Attrs": {
"LastModifiedTime": "2021-02-16 10:17:35.404 +0000 UTC", "arn": "arn:aws:cloudfront::047081014315:distribution/E1M9CNS0XSHI19",
"PriceClass": "PriceClass_All", "caller_reference": "terraform-20210216101734792900000001",
"RetainOnDelete": false, "default_cache_behavior": [
"Status": "Deployed", {
"Tags": {}, "allowed_methods": [
"TrustedSigners": [ "GET",
{ "HEAD"
"Enabled": false, ],
"Items": [] "cached_methods": [
} "GET",
], "HEAD"
"WaitForDeployment": true, ],
"WebAclId": "", "compress": false,
"CustomErrorResponse": [], "default_ttl": 86400,
"DefaultCacheBehavior": [ "field_level_encryption_id": "",
{ "forwarded_values": [
"AllowedMethods": [ {
"GET", "cookies": [
"HEAD" {
], "forward": "none"
"CachedMethods": [ }
"GET", ],
"HEAD" "query_string": false
], }
"Compress": false, ],
"DefaultTtl": 86400, "max_ttl": 31536000,
"FieldLevelEncryptionId": "", "min_ttl": 0,
"MaxTtl": 31536000, "smooth_streaming": false,
"MinTtl": 0, "target_origin_id": "S3-foo-cloudfront",
"SmoothStreaming": false, "viewer_protocol_policy": "allow-all"
"TargetOriginId": "S3-foo-cloudfront", }
"TrustedSigners": [], ],
"ViewerProtocolPolicy": "allow-all", "default_root_object": "",
"ForwardedValues": [ "domain_name": "d1g0dw0i1wvlgd.cloudfront.net",
{ "enabled": false,
"Headers": null, "etag": "E2CKBANLXUPWGQ",
"QueryString": false, "hosted_zone_id": "Z2FDTNDATAQYW2",
"QueryStringCacheKeys": [], "http_version": "http2",
"Cookies": [ "id": "E1M9CNS0XSHI19",
{ "in_progress_validation_batches": 0,
"Forward": "none", "is_ipv6_enabled": false,
"WhitelistedNames": null "last_modified_time": "2021-02-16 10:17:35.404 +0000 UTC",
} "ordered_cache_behavior": [
] {
} "allowed_methods": [
], "GET",
"LambdaFunctionAssociation": [] "HEAD",
} "OPTIONS"
], ],
"LoggingConfig": [], "cached_methods": [
"OrderedCacheBehavior": [ "GET",
{ "HEAD",
"AllowedMethods": [ "OPTIONS"
"GET", ],
"HEAD", "compress": true,
"OPTIONS" "default_ttl": 86400,
], "field_level_encryption_id": "",
"CachedMethods": [ "forwarded_values": [
"GET", {
"HEAD", "cookies": [
"OPTIONS" {
], "forward": "none"
"Compress": true, }
"DefaultTtl": 86400, ],
"FieldLevelEncryptionId": null, "headers": [
"MaxTtl": 31536000, "Origin"
"MinTtl": 0, ],
"PathPattern": "/content/immutable/*", "query_string": false
"SmoothStreaming": false, }
"TargetOriginId": "S3-foo-cloudfront", ],
"TrustedSigners": null, "max_ttl": 31536000,
"ViewerProtocolPolicy": "redirect-to-https", "min_ttl": 0,
"ForwardedValues": [ "path_pattern": "/content/immutable/*",
{ "smooth_streaming": false,
"Headers": [ "target_origin_id": "S3-foo-cloudfront",
"Origin" "viewer_protocol_policy": "redirect-to-https"
], },
"QueryString": false, {
"QueryStringCacheKeys": null, "allowed_methods": [
"Cookies": [ "GET",
{ "HEAD",
"Forward": "none", "OPTIONS"
"WhitelistedNames": null ],
} "cached_methods": [
] "GET",
} "HEAD"
], ],
"LambdaFunctionAssociation": null "compress": true,
}, "default_ttl": 3600,
{ "field_level_encryption_id": "",
"AllowedMethods": [ "forwarded_values": [
"GET", {
"HEAD", "cookies": [
"OPTIONS" {
], "forward": "none"
"CachedMethods": [ }
"GET", ],
"HEAD" "query_string": false
], }
"Compress": true, ],
"DefaultTtl": 3600, "max_ttl": 86400,
"FieldLevelEncryptionId": null, "min_ttl": 0,
"MaxTtl": 86400, "path_pattern": "/content/*",
"MinTtl": 0, "smooth_streaming": false,
"PathPattern": "/content/*", "target_origin_id": "S3-foo-cloudfront",
"SmoothStreaming": false, "viewer_protocol_policy": "redirect-to-https"
"TargetOriginId": "S3-foo-cloudfront", }
"TrustedSigners": null, ],
"ViewerProtocolPolicy": "redirect-to-https", "origin": [
"ForwardedValues": [ {
{ "domain_name": "foo-cloudfront.s3.eu-west-3.amazonaws.com",
"Headers": null, "origin_id": "S3-foo-cloudfront",
"QueryString": false, "origin_path": ""
"QueryStringCacheKeys": null, }
"Cookies": [ ],
{ "price_class": "PriceClass_All",
"Forward": "none", "restrictions": [
"WhitelistedNames": null {
} "geo_restriction": [
] {
} "restriction_type": "none"
], }
"LambdaFunctionAssociation": null ]
} }
], ],
"Origin": [ "retain_on_delete": false,
{ "status": "Deployed",
"DomainName": "foo-cloudfront.s3.eu-west-3.amazonaws.com", "trusted_signers": [
"OriginId": "S3-foo-cloudfront", {
"OriginPath": "", "enabled": false
"CustomHeader": [], }
"CustomOriginConfig": [], ],
"S3OriginConfig": [] "viewer_certificate": [
} {
], "acm_certificate_arn": "",
"OriginGroup": null, "cloudfront_default_certificate": true,
"Restrictions": [ "iam_certificate_id": "",
{ "minimum_protocol_version": "TLSv1",
"GeoRestriction": [ "ssl_support_method": ""
{ }
"Locations": null, ],
"RestrictionType": "none" "wait_for_deployment": true,
} "web_acl_id": ""
] }
}
],
"ViewerCertificate": [
{
"AcmCertificateArn": "",
"CloudfrontDefaultCertificate": true,
"IamCertificateId": "",
"MinimumProtocolVersion": "TLSv1",
"SslSupportMethod": ""
}
],
"CtyVal": {}
} }
] ]

View File

@ -6,6 +6,7 @@ import (
"github.com/cloudskiff/driftctl/pkg/remote/aws/client" "github.com/cloudskiff/driftctl/pkg/remote/aws/client"
"github.com/cloudskiff/driftctl/pkg/remote/aws/repository" "github.com/cloudskiff/driftctl/pkg/remote/aws/repository"
"github.com/cloudskiff/driftctl/pkg/resource" "github.com/cloudskiff/driftctl/pkg/resource"
"github.com/cloudskiff/driftctl/pkg/resource/aws"
"github.com/cloudskiff/driftctl/pkg/terraform" "github.com/cloudskiff/driftctl/pkg/terraform"
) )
@ -15,7 +16,7 @@ const RemoteAWSTerraform = "aws+tf"
* Initialize remote (configure credentials, launch tf providers and start gRPC clients) * Initialize remote (configure credentials, launch tf providers and start gRPC clients)
* Required to use Scanner * Required to use Scanner
*/ */
func Init(alerter *alerter.Alerter, providerLibrary *terraform.ProviderLibrary, supplierLibrary *resource.SupplierLibrary, progress output.Progress) error { func Init(alerter *alerter.Alerter, providerLibrary *terraform.ProviderLibrary, supplierLibrary *resource.SupplierLibrary, progress output.Progress, resourceSchemaRepository *resource.SchemaRepository) error {
provider, err := NewAWSTerraformProvider(progress) provider, err := NewAWSTerraformProvider(progress)
if err != nil { if err != nil {
return err return err
@ -77,5 +78,8 @@ func Init(alerter *alerter.Alerter, providerLibrary *terraform.ProviderLibrary,
supplierLibrary.AddSupplier(NewKMSAliasSupplier(provider)) supplierLibrary.AddSupplier(NewKMSAliasSupplier(provider))
supplierLibrary.AddSupplier(NewLambdaEventSourceMappingSupplier(provider)) supplierLibrary.AddSupplier(NewLambdaEventSourceMappingSupplier(provider))
resourceSchemaRepository.Init(provider.Schema())
aws.InitResourcesMetadata(resourceSchemaRepository)
return nil return nil
} }

View File

@ -4,6 +4,7 @@ import (
"github.com/cloudskiff/driftctl/pkg/alerter" "github.com/cloudskiff/driftctl/pkg/alerter"
"github.com/cloudskiff/driftctl/pkg/output" "github.com/cloudskiff/driftctl/pkg/output"
"github.com/cloudskiff/driftctl/pkg/resource" "github.com/cloudskiff/driftctl/pkg/resource"
"github.com/cloudskiff/driftctl/pkg/resource/github"
"github.com/cloudskiff/driftctl/pkg/terraform" "github.com/cloudskiff/driftctl/pkg/terraform"
) )
@ -13,7 +14,7 @@ const RemoteGithubTerraform = "github+tf"
* Initialize remote (configure credentials, launch tf providers and start gRPC clients) * Initialize remote (configure credentials, launch tf providers and start gRPC clients)
* Required to use Scanner * Required to use Scanner
*/ */
func Init(alerter *alerter.Alerter, providerLibrary *terraform.ProviderLibrary, supplierLibrary *resource.SupplierLibrary, progress output.Progress) error { func Init(alerter *alerter.Alerter, providerLibrary *terraform.ProviderLibrary, supplierLibrary *resource.SupplierLibrary, progress output.Progress, resourceSchemaRepository *resource.SchemaRepository) error {
provider, err := NewGithubTerraformProvider(progress) provider, err := NewGithubTerraformProvider(progress)
if err != nil { if err != nil {
return err return err
@ -33,5 +34,8 @@ func Init(alerter *alerter.Alerter, providerLibrary *terraform.ProviderLibrary,
supplierLibrary.AddSupplier(NewGithubTeamMembershipSupplier(provider, repository)) supplierLibrary.AddSupplier(NewGithubTeamMembershipSupplier(provider, repository))
supplierLibrary.AddSupplier(NewGithubBranchProtectionSupplier(provider, repository)) supplierLibrary.AddSupplier(NewGithubBranchProtectionSupplier(provider, repository))
resourceSchemaRepository.Init(provider.Schema())
github.InitMetadatas(resourceSchemaRepository)
return nil return nil
} }

View File

@ -24,12 +24,12 @@ func IsSupported(remote string) bool {
return false return false
} }
func Activate(remote string, alerter *alerter.Alerter, providerLibrary *terraform.ProviderLibrary, supplierLibrary *resource.SupplierLibrary, progress output.Progress) error { func Activate(remote string, alerter *alerter.Alerter, providerLibrary *terraform.ProviderLibrary, supplierLibrary *resource.SupplierLibrary, progress output.Progress, resourceSchemaRepository *resource.SchemaRepository) error {
switch remote { switch remote {
case aws.RemoteAWSTerraform: case aws.RemoteAWSTerraform:
return aws.Init(alerter, providerLibrary, supplierLibrary, progress) return aws.Init(alerter, providerLibrary, supplierLibrary, progress, resourceSchemaRepository)
case github.RemoteGithubTerraform: case github.RemoteGithubTerraform:
return github.Init(alerter, providerLibrary, supplierLibrary, progress) return github.Init(alerter, providerLibrary, supplierLibrary, progress, resourceSchemaRepository)
default: default:
return errors.Errorf("unsupported remote '%s'", remote) return errors.Errorf("unsupported remote '%s'", remote)
} }

View File

@ -1,7 +1,11 @@
// GENERATED, DO NOT EDIT THIS FILE // GENERATED, DO NOT EDIT THIS FILE
package aws package aws
import "github.com/zclconf/go-cty/cty" import (
"github.com/cloudskiff/driftctl/pkg/resource"
"github.com/zclconf/go-cty/cty"
)
const AwsCloudfrontDistributionResourceType = "aws_cloudfront_distribution" const AwsCloudfrontDistributionResourceType = "aws_cloudfront_distribution"
@ -155,3 +159,13 @@ func (r *AwsCloudfrontDistribution) TerraformType() string {
func (r *AwsCloudfrontDistribution) CtyValue() *cty.Value { func (r *AwsCloudfrontDistribution) CtyValue() *cty.Value {
return r.CtyVal return r.CtyVal
} }
func initAwsCloudfrontDistributionMetaData(resourceSchemaRepository resource.SchemaRepositoryInterface) {
resourceSchemaRepository.SetNormalizeFunc(AwsCloudfrontDistributionResourceType, func(val *resource.Attributes) {
val.SafeDelete([]string{"etag"})
val.SafeDelete([]string{"last_modified_time"})
val.SafeDelete([]string{"retain_on_delete"})
val.SafeDelete([]string{"status"})
val.SafeDelete([]string{"wait_for_deployment"})
})
}

View File

@ -0,0 +1,7 @@
package aws
import "github.com/cloudskiff/driftctl/pkg/resource"
func InitResourcesMetadata(resourceSchemaRepository resource.SchemaRepositoryInterface) {
initAwsCloudfrontDistributionMetaData(resourceSchemaRepository)
}

View File

@ -0,0 +1,7 @@
package github
import "github.com/cloudskiff/driftctl/pkg/resource"
func InitMetadatas(resourceSchemaRepository resource.SchemaRepositoryInterface) {
}

View File

@ -2,9 +2,14 @@ package resource
import ( import (
"encoding/json" "encoding/json"
"reflect"
"sort" "sort"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
) )
type Resource interface { type Resource interface {
@ -13,6 +18,37 @@ type Resource interface {
CtyValue() *cty.Value CtyValue() *cty.Value
} }
var refactoredResources = []string{
"aws_cloudfront_distribution",
}
func IsRefactoredResource(typ string) bool {
for _, refactoredResource := range refactoredResources {
if typ == refactoredResource {
return true
}
}
return false
}
type AbstractResource struct {
Id string
Type string
Attrs *Attributes
}
func (a *AbstractResource) TerraformId() string {
return a.Id
}
func (a *AbstractResource) TerraformType() string {
return a.Type
}
func (a *AbstractResource) CtyValue() *cty.Value {
return nil
}
type ResourceFactory interface { type ResourceFactory interface {
CreateResource(data interface{}, ty string) (*cty.Value, error) CreateResource(data interface{}, ty string) (*cty.Value, error)
} }
@ -70,3 +106,188 @@ func Sort(res []Resource) []Resource {
}) })
return res return res
} }
func ToResourceAttributes(val *cty.Value) *Attributes {
if val == nil {
return nil
}
bytes, _ := ctyjson.Marshal(*val, val.Type())
var attrs Attributes
err := json.Unmarshal(bytes, &attrs)
if err != nil {
panic(err)
}
return &attrs
}
type Attributes map[string]interface{}
func (a *Attributes) Get(path string) (interface{}, bool) {
val, exist := (*a)[path]
return val, exist
}
func (a *Attributes) SafeDelete(path []string) {
for i, key := range path {
if i == len(path)-1 {
delete(*a, key)
return
}
v, exists := (*a)[key]
if !exists {
return
}
m, ok := v.(Attributes)
if !ok {
return
}
*a = m
}
}
func (a *Attributes) SafeSet(path []string, value interface{}) error {
for i, key := range path {
if i == len(path)-1 {
(*a)[key] = value
return nil
}
v, exists := (*a)[key]
if !exists {
(*a)[key] = map[string]interface{}{}
v = (*a)[key]
}
m, ok := v.(Attributes)
if !ok {
return errors.Errorf("Path %s cannot be set: %s is not a nested struct", strings.Join(path, "."), key)
}
*a = m
}
return errors.New("Error setting value") // should not happen ?
}
func (a *Attributes) SanitizeDefaults() {
original := reflect.ValueOf(*a)
copy := reflect.New(original.Type()).Elem()
a.run("", original, copy)
*a = copy.Interface().(Attributes)
}
func (a *Attributes) run(path string, original, copy reflect.Value) {
switch original.Kind() {
case reflect.Ptr:
originalValue := original.Elem()
if !originalValue.IsValid() {
return
}
copy.Set(reflect.New(originalValue.Type()))
a.run(path, originalValue, copy.Elem())
case reflect.Interface:
// Get rid of the wrapping interface
originalValue := original.Elem()
if !originalValue.IsValid() {
return
}
if originalValue.Kind() == reflect.Slice || originalValue.Kind() == reflect.Map {
if originalValue.Len() == 0 {
return
}
}
// Create a new object. Now new gives us a pointer, but we want the value it
// points to, so we have to call Elem() to unwrap it
copyValue := reflect.New(originalValue.Type()).Elem()
a.run(path, originalValue, copyValue)
copy.Set(copyValue)
case reflect.Struct:
for i := 0; i < original.NumField(); i += 1 {
field := original.Field(i)
a.run(concatenatePath(path, field.String()), field, copy.Field(i))
}
case reflect.Slice:
copy.Set(reflect.MakeSlice(original.Type(), original.Len(), original.Cap()))
for i := 0; i < original.Len(); i += 1 {
a.run(concatenatePath(path, strconv.Itoa(i)), original.Index(i), copy.Index(i))
}
case reflect.Map:
copy.Set(reflect.MakeMap(original.Type()))
for _, key := range original.MapKeys() {
originalValue := original.MapIndex(key)
copyValue := reflect.New(originalValue.Type()).Elem()
a.run(concatenatePath(path, key.String()), originalValue, copyValue)
copy.SetMapIndex(key, copyValue)
}
default:
copy.Set(original)
}
}
func concatenatePath(path, next string) string {
if path == "" {
return next
}
return strings.Join([]string{path, next}, ".")
}
func (a *Attributes) SanitizeDefaultsV3() {
original := reflect.ValueOf(*a)
copy := reflect.New(original.Type()).Elem()
a.runV3("", original, copy)
*a = copy.Interface().(Attributes)
}
func (a *Attributes) runV3(path string, original, copy reflect.Value) bool {
switch original.Kind() {
case reflect.Ptr:
originalValue := original.Elem()
if !originalValue.IsValid() {
return false
}
copy.Set(reflect.New(originalValue.Type()))
a.runV3(path, originalValue, copy.Elem())
case reflect.Interface:
// Get rid of the wrapping interface
originalValue := original.Elem()
if !originalValue.IsValid() {
return false
}
if originalValue.Kind() == reflect.Slice || originalValue.Kind() == reflect.Map {
if originalValue.Len() == 0 {
return false
}
}
// Create a new object. Now new gives us a pointer, but we want the value it
// points to, so we have to call Elem() to unwrap it
copyValue := reflect.New(originalValue.Type()).Elem()
a.runV3(path, originalValue, copyValue)
copy.Set(copyValue)
case reflect.Struct:
for i := 0; i < original.NumField(); i += 1 {
field := original.Field(i)
a.runV3(concatenatePath(path, field.String()), field, copy.Field(i))
}
case reflect.Slice:
copy.Set(reflect.MakeSlice(original.Type(), original.Len(), original.Cap()))
for i := 0; i < original.Len(); i += 1 {
a.runV3(concatenatePath(path, strconv.Itoa(i)), original.Index(i), copy.Index(i))
}
case reflect.Map:
copy.Set(reflect.MakeMap(original.Type()))
for _, key := range original.MapKeys() {
originalValue := original.MapIndex(key)
copyValue := reflect.New(originalValue.Type()).Elem()
created := a.runV3(concatenatePath(path, key.String()), originalValue, copyValue)
if created {
copy.SetMapIndex(key, copyValue)
}
}
default:
copy.Set(original)
}
return true
}

View File

@ -0,0 +1,152 @@
package resource
import (
"testing"
"github.com/stretchr/testify/assert"
)
// Normalize empty slices and map to nil
func TestSanitizeDefaults(t *testing.T) {
cases := map[string]struct {
input Attributes
expected interface{}
}{
"simple": {
input: Attributes{
"emptyStringSlice": []string{},
"emptyIntSlice": []int{},
"emptyBoolSlice": []bool{},
"emptyMap": map[string]string{},
"nilInterface": interface{}(nil),
"not_deleted": "value",
},
expected: Attributes{
"emptyStringSlice": nil,
"emptyIntSlice": nil,
"emptyBoolSlice": nil,
"emptyMap": nil,
"nilInterface": nil,
"not_deleted": "value",
},
},
"nested": {
input: Attributes{
"should": map[string]interface{}{
"be_deleted": map[string]interface{}{},
"be_deleted_too": []string{},
"not_be_deleted": "no",
},
"not_deleted": "value",
},
expected: Attributes{
"should": map[string]interface{}{
"be_deleted": nil,
"be_deleted_too": nil,
"not_be_deleted": "no",
},
"not_deleted": "value",
},
},
"nested_slice": {
input: Attributes{
"should": []map[string][]interface{}{
{
"be": []interface{}{
map[string]interface{}{
"removed": []string{},
"removed_too": map[string]string{},
},
},
},
},
},
expected: Attributes{
"should": []map[string][]interface{}{
{
"be": []interface{}{
map[string]interface{}{
"removed": nil,
"removed_too": nil,
},
},
},
},
},
},
}
for k, c := range cases {
t.Run(k, func(t *testing.T) {
c.input.SanitizeDefaults()
assert.Equal(t, c.expected, c.input)
})
}
}
// Delete empty or nil slices and maps
func TestSanitizeDefaultsV3(t *testing.T) {
cases := map[string]struct {
input Attributes
expected interface{}
}{
"simple": {
input: Attributes{
"emptyStringSlice": []string{},
"emptyIntSlice": []int{},
"emptyBoolSlice": []bool{},
"emptyMap": map[string]string{},
"nilInterface": interface{}(nil),
"not_deleted": "value",
},
expected: Attributes{
"not_deleted": "value",
},
},
"nested": {
input: Attributes{
"should": map[string]interface{}{
"be_deleted": map[string]interface{}{},
"be_deleted_too": []string{},
"not_be_deleted": "no",
"not_be_deleted_too": []string(nil),
},
"not_deleted": "value",
},
expected: Attributes{
"should": map[string]interface{}{
"not_be_deleted": "no",
},
"not_deleted": "value",
},
},
"nested_slice": {
input: Attributes{
"should": []map[string][]interface{}{
{
"be": []interface{}{
map[string]interface{}{
"removed": []string{},
"removed_too": map[string]string{},
},
},
},
},
},
expected: Attributes{
"should": []map[string][]interface{}{
{
"be": []interface{}{
map[string]interface{}{},
},
},
},
},
},
}
for k, c := range cases {
t.Run(k, func(t *testing.T) {
c.input.SanitizeDefaultsV3()
assert.Equal(t, c.expected, c.input)
})
}
}

111
pkg/resource/schemas.go Normal file
View File

@ -0,0 +1,111 @@
package resource
import (
"strings"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/providers"
"github.com/sirupsen/logrus"
)
type AttributeSchema struct {
ConfigSchema configschema.Attribute
JsonString bool
}
type Schema struct {
Attributes map[string]AttributeSchema
NormalizeFunc func(val *Attributes)
}
func (s *Schema) IsComputedField(path []string) bool {
metadata, exist := s.Attributes[strings.Join(path, ".")]
if !exist {
return false
}
return metadata.ConfigSchema.Computed
}
func (s *Schema) IsJsonStringField(path []string) bool {
metadata, exist := s.Attributes[strings.Join(path, ".")]
if !exist {
return false
}
return metadata.JsonString
}
type SchemaRepositoryInterface interface {
GetSchema(resourceType string) (*Schema, bool)
UpdateSchema(typ string, schemasMutators map[string]func(attributeSchema *AttributeSchema))
SetNormalizeFunc(typ string, normalizeFunc func(val *Attributes))
}
type SchemaRepository struct {
schemas map[string]*Schema
}
func NewSchemaRepository() *SchemaRepository {
return &SchemaRepository{
schemas: make(map[string]*Schema),
}
}
func (r *SchemaRepository) GetSchema(resourceType string) (*Schema, bool) {
schema, exist := r.schemas[resourceType]
return schema, exist
}
func (r *SchemaRepository) fetchNestedBlocks(root string, metadata map[string]AttributeSchema, block map[string]*configschema.NestedBlock) {
for s, nestedBlock := range block {
path := s
if root != "" {
path = strings.Join([]string{root, s}, ".")
}
for s2, attr := range nestedBlock.Attributes {
nestedPath := strings.Join([]string{path, s2}, ".")
metadata[nestedPath] = AttributeSchema{
ConfigSchema: *attr,
}
}
r.fetchNestedBlocks(path, metadata, nestedBlock.BlockTypes)
}
}
func (r *SchemaRepository) Init(schema map[string]providers.Schema) {
for typ, sch := range schema {
attributeMetas := map[string]AttributeSchema{}
for s, attribute := range sch.Block.Attributes {
attributeMetas[s] = AttributeSchema{
ConfigSchema: *attribute,
}
}
r.fetchNestedBlocks("", attributeMetas, sch.Block.BlockTypes)
r.schemas[typ] = &Schema{
Attributes: attributeMetas,
}
}
}
func (r *SchemaRepository) UpdateSchema(typ string, schemasMutators map[string]func(attributeSchema *AttributeSchema)) {
for s, f := range schemasMutators {
metadata, exist := r.GetSchema(typ)
if !exist {
logrus.WithFields(logrus.Fields{"type": typ}).Warning("Unable to set metadata, no schema found")
return
}
m := (*metadata).Attributes[s]
f(&m)
(*metadata).Attributes[s] = m
}
}
func (r *SchemaRepository) SetNormalizeFunc(typ string, normalizeFunc func(val *Attributes)) {
metadata, exist := r.GetSchema(typ)
if !exist {
logrus.WithFields(logrus.Fields{"type": typ}).Warning("Unable to set normalize func, no schema found")
return
}
(*metadata).NormalizeFunc = normalizeFunc
}

View File

@ -3,27 +3,27 @@ package pkg
import ( import (
"context" "context"
"github.com/cloudskiff/driftctl/pkg/remote"
"github.com/pkg/errors"
"github.com/cloudskiff/driftctl/pkg/parallel"
"github.com/sirupsen/logrus"
"github.com/cloudskiff/driftctl/pkg/alerter" "github.com/cloudskiff/driftctl/pkg/alerter"
"github.com/cloudskiff/driftctl/pkg/parallel"
"github.com/cloudskiff/driftctl/pkg/remote"
"github.com/cloudskiff/driftctl/pkg/resource" "github.com/cloudskiff/driftctl/pkg/resource"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
) )
type Scanner struct { type Scanner struct {
resourceSuppliers []resource.Supplier resourceSuppliers []resource.Supplier
runner *parallel.ParallelRunner runner *parallel.ParallelRunner
alerter *alerter.Alerter alerter *alerter.Alerter
resourceSchemaRepository *resource.SchemaRepository
} }
func NewScanner(resourceSuppliers []resource.Supplier, alerter *alerter.Alerter) *Scanner { func NewScanner(resourceSuppliers []resource.Supplier, alerter *alerter.Alerter, resourceSchemaRepository *resource.SchemaRepository) *Scanner {
return &Scanner{ return &Scanner{
resourceSuppliers: resourceSuppliers, resourceSuppliers: resourceSuppliers,
runner: parallel.NewParallelRunner(context.TODO(), 10), runner: parallel.NewParallelRunner(context.TODO(), 10),
alerter: alerter, alerter: alerter,
resourceSchemaRepository: resourceSchemaRepository,
} }
} }
@ -58,6 +58,24 @@ loop:
break loop break loop
} }
for _, res := range resources.([]resource.Resource) { for _, res := range resources.([]resource.Resource) {
if resource.IsRefactoredResource(res.TerraformType()) {
schema, exist := s.resourceSchemaRepository.GetSchema(res.TerraformType())
ctyAttr := resource.ToResourceAttributes(res.CtyValue())
ctyAttr.SanitizeDefaultsV3()
if exist && schema.NormalizeFunc != nil {
schema.NormalizeFunc(ctyAttr)
}
newRes := &resource.AbstractResource{
Id: res.TerraformId(),
Type: res.TerraformType(),
Attrs: ctyAttr,
}
results = append(results, newRes)
continue
}
normalisable, ok := res.(resource.NormalizedResource) normalisable, ok := res.(resource.NormalizedResource)
if ok { if ok {
normalizedRes, err := normalisable.NormalizeForProvider() normalizedRes, err := normalisable.NormalizeForProvider()

View File

@ -3,6 +3,10 @@ package resource
import ( import (
"fmt" "fmt"
"github.com/cloudskiff/driftctl/pkg/resource"
"github.com/cloudskiff/driftctl/test/schemas"
"github.com/hashicorp/terraform/providers"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
) )
@ -64,3 +68,18 @@ func (r *FakeResourceStringer) CtyValue() *cty.Value {
func (d *FakeResourceStringer) String() string { func (d *FakeResourceStringer) String() string {
return fmt.Sprintf("Name: '%s'", d.Name) return fmt.Sprintf("Name: '%s'", d.Name)
} }
func InitFakeSchemaRepository(provider, version string) resource.SchemaRepositoryInterface {
repo := resource.NewSchemaRepository()
schema := make(map[string]providers.Schema)
if provider != "" {
s, err := schemas.ReadTestSchema(provider, version)
if err != nil {
// TODO HANDLER ERROR PROPERLY
panic(err)
}
schema = s
}
repo.Init(schema)
return repo
}

171757
test/schemas/aws/3.19.0/schema.json Executable file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

23
test/schemas/shemas.go Normal file
View File

@ -0,0 +1,23 @@
package schemas
import (
gojson "encoding/json"
"io/ioutil"
"path"
"runtime"
"github.com/hashicorp/terraform/providers"
)
func ReadTestSchema(provider, version string) (map[string]providers.Schema, error) {
_, filename, _, _ := runtime.Caller(0)
content, err := ioutil.ReadFile(path.Join(path.Dir(filename), provider, version, "schema.json"))
if err != nil {
return nil, err
}
var schema map[string]providers.Schema
if err := gojson.Unmarshal(content, &schema); err != nil {
return nil, err
}
return schema, nil
}