commit
44d1a8ec64
|
@ -13,7 +13,8 @@ import (
|
|||
|
||||
type Change struct {
|
||||
diff.Change
|
||||
Computed bool `json:"computed"`
|
||||
Computed bool `json:"computed"`
|
||||
JsonString bool `json:"-"`
|
||||
}
|
||||
|
||||
type Changelog []Change
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"reflect"
|
||||
|
||||
resourceaws "github.com/cloudskiff/driftctl/pkg/resource/aws"
|
||||
|
||||
"github.com/r3labs/diff/v2"
|
||||
|
||||
"github.com/cloudskiff/driftctl/pkg/alerter"
|
||||
|
@ -40,7 +39,8 @@ func (c *ComputedDiffAlert) ShouldIgnoreResource() bool {
|
|||
}
|
||||
|
||||
type Analyzer struct {
|
||||
alerter *alerter.Alerter
|
||||
alerter *alerter.Alerter
|
||||
resourceSchemaRepository resource.SchemaRepositoryInterface
|
||||
}
|
||||
|
||||
type Filter interface {
|
||||
|
@ -48,8 +48,8 @@ type Filter interface {
|
|||
IsFieldIgnored(res resource.Resource, path []string) bool
|
||||
}
|
||||
|
||||
func NewAnalyzer(alerter *alerter.Alerter) Analyzer {
|
||||
return Analyzer{alerter}
|
||||
func NewAnalyzer(alerter *alerter.Alerter, resourceSchemaRepository resource.SchemaRepositoryInterface) Analyzer {
|
||||
return Analyzer{alerter, resourceSchemaRepository}
|
||||
}
|
||||
|
||||
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)
|
||||
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 {
|
||||
continue
|
||||
|
@ -93,7 +100,16 @@ func (a Analyzer) Analyze(remoteResources, resourcesFromState []resource.Resourc
|
|||
continue
|
||||
}
|
||||
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 {
|
||||
haveComputedDiff = true
|
||||
}
|
||||
|
@ -152,6 +168,15 @@ func (a Analyzer) isComputedField(stateRes resource.Resource, change Change) boo
|
|||
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
|
||||
// its path and its type
|
||||
func (a Analyzer) getField(t reflect.Type, path []string) (reflect.StructField, bool) {
|
||||
|
|
|
@ -993,7 +993,10 @@ func TestAnalyze(t *testing.T) {
|
|||
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)
|
||||
|
||||
if err != nil {
|
||||
|
|
|
@ -146,7 +146,9 @@ func scanRun(opts *pkg.ScanOptions) error {
|
|||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
@ -158,16 +160,16 @@ func scanRun(opts *pkg.ScanOptions) error {
|
|||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
resFactory := terraform.NewTerraformResourceFactory(providerLibrary)
|
||||
|
||||
ctl := pkg.NewDriftCTL(scanner, iacSupplier, alerter, resFactory, opts)
|
||||
ctl := pkg.NewDriftCTL(scanner, iacSupplier, alerter, resFactory, opts, resourceSchemaRepository)
|
||||
|
||||
go func() {
|
||||
<-c
|
||||
|
|
|
@ -77,8 +77,7 @@ func (c *Console) Write(analysis *analyser.Analysis) error {
|
|||
pref = fmt.Sprintf("%s %s:", color.RedString("-"), path)
|
||||
}
|
||||
if change.Type == diff.UPDATE {
|
||||
isJsonString := isFieldJsonString(difference.Res, path)
|
||||
if isJsonString {
|
||||
if change.JsonString {
|
||||
prefix := " "
|
||||
fmt.Printf(" %s\n%s%s\n", pref, prefix, jsonDiff(change.From, change.To, prefix))
|
||||
continue
|
||||
|
@ -181,23 +180,6 @@ func groupByType(resources []resource.Resource) map[string][]resource.Resource {
|
|||
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 {
|
||||
aStr := fmt.Sprintf("%s", a)
|
||||
bStr := fmt.Sprintf("%s", b)
|
||||
|
|
|
@ -9,8 +9,11 @@ import (
|
|||
"testing"
|
||||
|
||||
"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"
|
||||
testresource "github.com/cloudskiff/driftctl/test/resource"
|
||||
|
||||
"github.com/cloudskiff/driftctl/pkg/analyser"
|
||||
)
|
||||
|
@ -70,6 +73,76 @@ func TestConsole_Write(t *testing.T) {
|
|||
}
|
||||
for _, tt := range tests {
|
||||
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()
|
||||
|
||||
stdout := os.Stdout // keep backup of the real stdout
|
||||
|
|
|
@ -108,6 +108,7 @@ func fakeAnalysisWithJsonFields() *analyser.Analysis {
|
|||
Type: "aws_diff_resource",
|
||||
}, Changelog: []analyser.Change{
|
||||
{
|
||||
JsonString: true,
|
||||
Change: diff.Change{
|
||||
Type: diff.UPDATE,
|
||||
Path: []string{"Json"},
|
||||
|
@ -121,6 +122,7 @@ func fakeAnalysisWithJsonFields() *analyser.Analysis {
|
|||
Type: "aws_diff_resource",
|
||||
}, Changelog: []analyser.Change{
|
||||
{
|
||||
JsonString: true,
|
||||
Change: diff.Change{
|
||||
Type: diff.UPDATE,
|
||||
Path: []string{"Json"},
|
||||
|
@ -176,7 +178,7 @@ func fakeAnalysisWithComputedFields() *analyser.Analysis {
|
|||
Type: "aws_diff_resource",
|
||||
},
|
||||
)
|
||||
a.AddDifference(analyser.Difference{Res: testresource.FakeResource{
|
||||
a.AddDifference(analyser.Difference{Res: &testresource.FakeResource{
|
||||
Id: "diff-id-1",
|
||||
Type: "aws_diff_resource",
|
||||
}, Changelog: []analyser.Change{
|
||||
|
|
|
@ -38,12 +38,12 @@ type DriftCTL struct {
|
|||
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{
|
||||
remoteSupplier,
|
||||
iacSupplier,
|
||||
alerter,
|
||||
analyser.NewAnalyzer(alerter),
|
||||
analyser.NewAnalyzer(alerter, resourceSchemaRepository),
|
||||
opts.Filter,
|
||||
resFactory,
|
||||
opts.StrictMode,
|
||||
|
|
|
@ -16,13 +16,20 @@ import (
|
|||
filter2 "github.com/cloudskiff/driftctl/pkg/filter"
|
||||
"github.com/cloudskiff/driftctl/pkg/resource"
|
||||
"github.com/cloudskiff/driftctl/pkg/resource/aws"
|
||||
"github.com/cloudskiff/driftctl/pkg/resource/github"
|
||||
"github.com/cloudskiff/driftctl/pkg/terraform"
|
||||
"github.com/cloudskiff/driftctl/test"
|
||||
testresource "github.com/cloudskiff/driftctl/test/resource"
|
||||
)
|
||||
|
||||
type TestProvider struct {
|
||||
Name string
|
||||
Version string
|
||||
}
|
||||
|
||||
type TestCase struct {
|
||||
name string
|
||||
provider *TestProvider
|
||||
stateResources []resource.Resource
|
||||
remoteResources []resource.Resource
|
||||
filter string
|
||||
|
@ -34,18 +41,29 @@ type TestCases []TestCase
|
|||
|
||||
func runTest(t *testing.T, cases TestCases) {
|
||||
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) {
|
||||
testAlerter := alerter.NewAlerter()
|
||||
|
||||
if c.stateResources == nil {
|
||||
c.stateResources = []resource.Resource{}
|
||||
}
|
||||
|
||||
stateSupplier := &resource.MockSupplier{}
|
||||
stateSupplier.On("Resources").Return(c.stateResources, nil)
|
||||
|
||||
if c.remoteResources == nil {
|
||||
c.remoteResources = []resource.Resource{}
|
||||
}
|
||||
|
||||
remoteSupplier := &resource.MockSupplier{}
|
||||
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{
|
||||
Filter: filter,
|
||||
})
|
||||
}, repo)
|
||||
|
||||
analysis, err := driftctl.Run()
|
||||
|
||||
|
@ -90,10 +108,10 @@ func TestDriftctlRun_BasicBehavior(t *testing.T) {
|
|||
{
|
||||
name: "infrastructure should be in sync",
|
||||
stateResources: []resource.Resource{
|
||||
testresource.FakeResource{},
|
||||
&testresource.FakeResource{},
|
||||
},
|
||||
remoteResources: []resource.Resource{
|
||||
testresource.FakeResource{},
|
||||
&testresource.FakeResource{},
|
||||
},
|
||||
assert: func(result *test.ScanResult, err error) {
|
||||
result.AssertInfrastructureIsInSync()
|
||||
|
@ -102,7 +120,7 @@ func TestDriftctlRun_BasicBehavior(t *testing.T) {
|
|||
{
|
||||
name: "we should have deleted resource",
|
||||
stateResources: []resource.Resource{
|
||||
testresource.FakeResource{},
|
||||
&testresource.FakeResource{},
|
||||
},
|
||||
remoteResources: []resource.Resource{},
|
||||
assert: func(result *test.ScanResult, err error) {
|
||||
|
@ -113,7 +131,7 @@ func TestDriftctlRun_BasicBehavior(t *testing.T) {
|
|||
name: "we should have unmanaged resource",
|
||||
stateResources: []resource.Resource{},
|
||||
remoteResources: []resource.Resource{
|
||||
testresource.FakeResource{},
|
||||
&testresource.FakeResource{},
|
||||
},
|
||||
assert: func(result *test.ScanResult, err error) {
|
||||
result.AssertUnmanagedCount(1)
|
||||
|
@ -122,13 +140,13 @@ func TestDriftctlRun_BasicBehavior(t *testing.T) {
|
|||
{
|
||||
name: "we should have changes of field update",
|
||||
stateResources: []resource.Resource{
|
||||
testresource.FakeResource{
|
||||
&testresource.FakeResource{
|
||||
Id: "fake",
|
||||
FooBar: "barfoo",
|
||||
},
|
||||
},
|
||||
remoteResources: []resource.Resource{
|
||||
testresource.FakeResource{
|
||||
&testresource.FakeResource{
|
||||
Id: "fake",
|
||||
FooBar: "foobar",
|
||||
},
|
||||
|
@ -149,13 +167,13 @@ func TestDriftctlRun_BasicBehavior(t *testing.T) {
|
|||
{
|
||||
name: "we should have changes on computed field",
|
||||
stateResources: []resource.Resource{
|
||||
testresource.FakeResource{
|
||||
&testresource.FakeResource{
|
||||
Id: "fake",
|
||||
BarFoo: "barfoo",
|
||||
},
|
||||
},
|
||||
remoteResources: []resource.Resource{
|
||||
testresource.FakeResource{
|
||||
&testresource.FakeResource{
|
||||
Id: "fake",
|
||||
BarFoo: "foobar",
|
||||
},
|
||||
|
@ -176,7 +194,7 @@ func TestDriftctlRun_BasicBehavior(t *testing.T) {
|
|||
{
|
||||
name: "we should have changes of deleted field",
|
||||
stateResources: []resource.Resource{
|
||||
testresource.FakeResource{
|
||||
&testresource.FakeResource{
|
||||
Id: "fake",
|
||||
Tags: map[string]string{
|
||||
"tag1": "deleted",
|
||||
|
@ -184,7 +202,7 @@ func TestDriftctlRun_BasicBehavior(t *testing.T) {
|
|||
},
|
||||
},
|
||||
remoteResources: []resource.Resource{
|
||||
testresource.FakeResource{
|
||||
&testresource.FakeResource{
|
||||
Id: "fake",
|
||||
},
|
||||
},
|
||||
|
@ -204,12 +222,12 @@ func TestDriftctlRun_BasicBehavior(t *testing.T) {
|
|||
{
|
||||
name: "we should have changes of added field",
|
||||
stateResources: []resource.Resource{
|
||||
testresource.FakeResource{
|
||||
&testresource.FakeResource{
|
||||
Id: "fake",
|
||||
},
|
||||
},
|
||||
remoteResources: []resource.Resource{
|
||||
testresource.FakeResource{
|
||||
&testresource.FakeResource{
|
||||
Id: "fake",
|
||||
Tags: map[string]string{
|
||||
"tag1": "added",
|
||||
|
|
|
@ -4,11 +4,10 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/cloudskiff/driftctl/pkg/resource"
|
||||
"github.com/jmespath/go-jmespath"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
ctyjson "github.com/zclconf/go-cty/cty/json"
|
||||
|
||||
"github.com/cloudskiff/driftctl/pkg/resource"
|
||||
)
|
||||
|
||||
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
|
||||
// map[string]string and map[string]SomeThing will not work without it
|
||||
// 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{}
|
||||
_ = 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{
|
||||
Attr: attrs,
|
||||
Res: res,
|
||||
|
|
|
@ -28,7 +28,7 @@ func IsSupplierSupported(supplierKey string) bool {
|
|||
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()
|
||||
for _, config := range configs {
|
||||
if !IsSupplierSupported(config.Key) {
|
||||
|
@ -39,7 +39,7 @@ func GetIACSupplier(configs []config.SupplierConfig, library *terraform.Provider
|
|||
var err error
|
||||
switch config.Key {
|
||||
case state.TerraformStateReaderSupplier:
|
||||
supplier, err = state.NewReader(config, library, backendOpts)
|
||||
supplier, err = state.NewReader(config, library, backendOpts, resourceSchemaRepository)
|
||||
default:
|
||||
return nil, errors.Errorf("Unsupported supplier '%s'", config.Key)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/cloudskiff/driftctl/pkg/iac/config"
|
||||
"github.com/cloudskiff/driftctl/pkg/iac/terraform/state/backend"
|
||||
"github.com/cloudskiff/driftctl/pkg/terraform"
|
||||
"github.com/cloudskiff/driftctl/test/resource"
|
||||
)
|
||||
|
||||
func TestGetIACSupplier(t *testing.T) {
|
||||
|
@ -82,7 +83,8 @@ func TestGetIACSupplier(t *testing.T) {
|
|||
}
|
||||
for _, tt := range tests {
|
||||
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() {
|
||||
t.Errorf("GetIACSupplier() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/cloudskiff/driftctl/pkg/iac/terraform/state/enumerator"
|
||||
"github.com/cloudskiff/driftctl/pkg/remote/deserializer"
|
||||
"github.com/cloudskiff/driftctl/pkg/resource"
|
||||
|
||||
"github.com/cloudskiff/driftctl/pkg/terraform"
|
||||
"github.com/hashicorp/terraform/addrs"
|
||||
"github.com/hashicorp/terraform/states"
|
||||
|
@ -22,12 +23,13 @@ import (
|
|||
const TerraformStateReaderSupplier = "tfstate"
|
||||
|
||||
type TerraformStateReader struct {
|
||||
library *terraform.ProviderLibrary
|
||||
config config.SupplierConfig
|
||||
backend backend.Backend
|
||||
enumerator enumerator.StateEnumerator
|
||||
deserializers []deserializer.CTYDeserializer
|
||||
backendOptions *backend.Options
|
||||
library *terraform.ProviderLibrary
|
||||
config config.SupplierConfig
|
||||
backend backend.Backend
|
||||
enumerator enumerator.StateEnumerator
|
||||
deserializers []deserializer.CTYDeserializer
|
||||
backendOptions *backend.Options
|
||||
resourceSchemaRepository resource.SchemaRepositoryInterface
|
||||
}
|
||||
|
||||
func (r *TerraformStateReader) initReader() error {
|
||||
|
@ -35,8 +37,8 @@ func (r *TerraformStateReader) initReader() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func NewReader(config config.SupplierConfig, library *terraform.ProviderLibrary, backendOpts *backend.Options) (*TerraformStateReader, error) {
|
||||
reader := TerraformStateReader{library: library, config: config, deserializers: iac.Deserializers(), backendOptions: backendOpts}
|
||||
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, resourceSchemaRepository: resourceSchemaRepository}
|
||||
err := reader.initReader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -173,6 +175,22 @@ func (r *TerraformStateReader) decode(values map[string][]cty.Value) ([]resource
|
|||
"id": res.TerraformId(),
|
||||
"type": res.TerraformType(),
|
||||
}).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)
|
||||
if ok {
|
||||
normalizedRes, err := normalisable.NormalizeForState()
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"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/config"
|
||||
|
@ -115,12 +116,15 @@ func TestTerraformStateReader_AWS_Resources(t *testing.T) {
|
|||
library := terraform.NewProviderLibrary()
|
||||
library.AddProvider(terraform.AWS, provider)
|
||||
|
||||
repo := testresource.InitFakeSchemaRepository(terraform.AWS, "3.19.0")
|
||||
|
||||
r := &TerraformStateReader{
|
||||
config: config.SupplierConfig{
|
||||
Path: path.Join(goldenfile.GoldenFilePath, tt.dirName, "terraform.tfstate"),
|
||||
},
|
||||
library: library,
|
||||
deserializers: iac.Deserializers(),
|
||||
library: library,
|
||||
deserializers: iac.Deserializers(),
|
||||
resourceSchemaRepository: repo,
|
||||
}
|
||||
|
||||
got, err := r.Resources()
|
||||
|
|
|
@ -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",
|
||||
"InProgressValidationBatches": 0,
|
||||
"IsIpv6Enabled": false,
|
||||
"LastModifiedTime": "2021-02-16 10:17:35.404 +0000 UTC",
|
||||
"PriceClass": "PriceClass_All",
|
||||
"RetainOnDelete": false,
|
||||
"Status": "Deployed",
|
||||
"Tags": {},
|
||||
"TrustedSigners": [
|
||||
{
|
||||
"Enabled": false,
|
||||
"Items": []
|
||||
}
|
||||
],
|
||||
"WaitForDeployment": true,
|
||||
"WebAclId": "",
|
||||
"CustomErrorResponse": [],
|
||||
"DefaultCacheBehavior": [
|
||||
{
|
||||
"AllowedMethods": [
|
||||
"GET",
|
||||
"HEAD"
|
||||
],
|
||||
"CachedMethods": [
|
||||
"GET",
|
||||
"HEAD"
|
||||
],
|
||||
"Compress": false,
|
||||
"DefaultTtl": 86400,
|
||||
"FieldLevelEncryptionId": "",
|
||||
"MaxTtl": 31536000,
|
||||
"MinTtl": 0,
|
||||
"SmoothStreaming": false,
|
||||
"TargetOriginId": "S3-foo-cloudfront",
|
||||
"TrustedSigners": [],
|
||||
"ViewerProtocolPolicy": "allow-all",
|
||||
"ForwardedValues": [
|
||||
{
|
||||
"Headers": null,
|
||||
"QueryString": false,
|
||||
"QueryStringCacheKeys": [],
|
||||
"Cookies": [
|
||||
{
|
||||
"Forward": "none",
|
||||
"WhitelistedNames": null
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"LambdaFunctionAssociation": []
|
||||
}
|
||||
],
|
||||
"LoggingConfig": [],
|
||||
"OrderedCacheBehavior": [
|
||||
{
|
||||
"AllowedMethods": [
|
||||
"GET",
|
||||
"HEAD",
|
||||
"OPTIONS"
|
||||
],
|
||||
"CachedMethods": [
|
||||
"GET",
|
||||
"HEAD",
|
||||
"OPTIONS"
|
||||
],
|
||||
"Compress": true,
|
||||
"DefaultTtl": 86400,
|
||||
"FieldLevelEncryptionId": null,
|
||||
"MaxTtl": 31536000,
|
||||
"MinTtl": 0,
|
||||
"PathPattern": "/content/immutable/*",
|
||||
"SmoothStreaming": false,
|
||||
"TargetOriginId": "S3-foo-cloudfront",
|
||||
"TrustedSigners": null,
|
||||
"ViewerProtocolPolicy": "redirect-to-https",
|
||||
"ForwardedValues": [
|
||||
{
|
||||
"Headers": [
|
||||
"Origin"
|
||||
],
|
||||
"QueryString": false,
|
||||
"QueryStringCacheKeys": null,
|
||||
"Cookies": [
|
||||
{
|
||||
"Forward": "none",
|
||||
"WhitelistedNames": null
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"LambdaFunctionAssociation": null
|
||||
},
|
||||
{
|
||||
"AllowedMethods": [
|
||||
"GET",
|
||||
"HEAD",
|
||||
"OPTIONS"
|
||||
],
|
||||
"CachedMethods": [
|
||||
"GET",
|
||||
"HEAD"
|
||||
],
|
||||
"Compress": true,
|
||||
"DefaultTtl": 3600,
|
||||
"FieldLevelEncryptionId": null,
|
||||
"MaxTtl": 86400,
|
||||
"MinTtl": 0,
|
||||
"PathPattern": "/content/*",
|
||||
"SmoothStreaming": false,
|
||||
"TargetOriginId": "S3-foo-cloudfront",
|
||||
"TrustedSigners": null,
|
||||
"ViewerProtocolPolicy": "redirect-to-https",
|
||||
"ForwardedValues": [
|
||||
{
|
||||
"Headers": null,
|
||||
"QueryString": false,
|
||||
"QueryStringCacheKeys": null,
|
||||
"Cookies": [
|
||||
{
|
||||
"Forward": "none",
|
||||
"WhitelistedNames": null
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"LambdaFunctionAssociation": null
|
||||
}
|
||||
],
|
||||
"Origin": [
|
||||
{
|
||||
"DomainName": "foo-cloudfront.s3.eu-west-3.amazonaws.com",
|
||||
"OriginId": "S3-foo-cloudfront",
|
||||
"OriginPath": "",
|
||||
"CustomHeader": [],
|
||||
"CustomOriginConfig": [],
|
||||
"S3OriginConfig": []
|
||||
}
|
||||
],
|
||||
"OriginGroup": null,
|
||||
"Restrictions": [
|
||||
{
|
||||
"GeoRestriction": [
|
||||
{
|
||||
"Locations": null,
|
||||
"RestrictionType": "none"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ViewerCertificate": [
|
||||
{
|
||||
"AcmCertificateArn": "",
|
||||
"CloudfrontDefaultCertificate": true,
|
||||
"IamCertificateId": "",
|
||||
"MinimumProtocolVersion": "TLSv1",
|
||||
"SslSupportMethod": ""
|
||||
}
|
||||
],
|
||||
"CtyVal": {}
|
||||
"Type": "aws_cloudfront_distribution",
|
||||
"Attrs": {
|
||||
"arn": "arn:aws:cloudfront::047081014315:distribution/E1M9CNS0XSHI19",
|
||||
"caller_reference": "terraform-20210216101734792900000001",
|
||||
"default_cache_behavior": [
|
||||
{
|
||||
"allowed_methods": [
|
||||
"GET",
|
||||
"HEAD"
|
||||
],
|
||||
"cached_methods": [
|
||||
"GET",
|
||||
"HEAD"
|
||||
],
|
||||
"compress": false,
|
||||
"default_ttl": 86400,
|
||||
"field_level_encryption_id": "",
|
||||
"forwarded_values": [
|
||||
{
|
||||
"cookies": [
|
||||
{
|
||||
"forward": "none"
|
||||
}
|
||||
],
|
||||
"query_string": false
|
||||
}
|
||||
],
|
||||
"max_ttl": 31536000,
|
||||
"min_ttl": 0,
|
||||
"smooth_streaming": false,
|
||||
"target_origin_id": "S3-foo-cloudfront",
|
||||
"viewer_protocol_policy": "allow-all"
|
||||
}
|
||||
],
|
||||
"default_root_object": "",
|
||||
"domain_name": "d1g0dw0i1wvlgd.cloudfront.net",
|
||||
"enabled": false,
|
||||
"etag": "E2CKBANLXUPWGQ",
|
||||
"hosted_zone_id": "Z2FDTNDATAQYW2",
|
||||
"http_version": "http2",
|
||||
"id": "E1M9CNS0XSHI19",
|
||||
"in_progress_validation_batches": 0,
|
||||
"is_ipv6_enabled": false,
|
||||
"last_modified_time": "2021-02-16 10:17:35.404 +0000 UTC",
|
||||
"ordered_cache_behavior": [
|
||||
{
|
||||
"allowed_methods": [
|
||||
"GET",
|
||||
"HEAD",
|
||||
"OPTIONS"
|
||||
],
|
||||
"cached_methods": [
|
||||
"GET",
|
||||
"HEAD",
|
||||
"OPTIONS"
|
||||
],
|
||||
"compress": true,
|
||||
"default_ttl": 86400,
|
||||
"field_level_encryption_id": "",
|
||||
"forwarded_values": [
|
||||
{
|
||||
"cookies": [
|
||||
{
|
||||
"forward": "none"
|
||||
}
|
||||
],
|
||||
"headers": [
|
||||
"Origin"
|
||||
],
|
||||
"query_string": false
|
||||
}
|
||||
],
|
||||
"max_ttl": 31536000,
|
||||
"min_ttl": 0,
|
||||
"path_pattern": "/content/immutable/*",
|
||||
"smooth_streaming": false,
|
||||
"target_origin_id": "S3-foo-cloudfront",
|
||||
"viewer_protocol_policy": "redirect-to-https"
|
||||
},
|
||||
{
|
||||
"allowed_methods": [
|
||||
"GET",
|
||||
"HEAD",
|
||||
"OPTIONS"
|
||||
],
|
||||
"cached_methods": [
|
||||
"GET",
|
||||
"HEAD"
|
||||
],
|
||||
"compress": true,
|
||||
"default_ttl": 3600,
|
||||
"field_level_encryption_id": "",
|
||||
"forwarded_values": [
|
||||
{
|
||||
"cookies": [
|
||||
{
|
||||
"forward": "none"
|
||||
}
|
||||
],
|
||||
"query_string": false
|
||||
}
|
||||
],
|
||||
"max_ttl": 86400,
|
||||
"min_ttl": 0,
|
||||
"path_pattern": "/content/*",
|
||||
"smooth_streaming": false,
|
||||
"target_origin_id": "S3-foo-cloudfront",
|
||||
"viewer_protocol_policy": "redirect-to-https"
|
||||
}
|
||||
],
|
||||
"origin": [
|
||||
{
|
||||
"domain_name": "foo-cloudfront.s3.eu-west-3.amazonaws.com",
|
||||
"origin_id": "S3-foo-cloudfront",
|
||||
"origin_path": ""
|
||||
}
|
||||
],
|
||||
"price_class": "PriceClass_All",
|
||||
"restrictions": [
|
||||
{
|
||||
"geo_restriction": [
|
||||
{
|
||||
"restriction_type": "none"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"retain_on_delete": false,
|
||||
"status": "Deployed",
|
||||
"trusted_signers": [
|
||||
{
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"viewer_certificate": [
|
||||
{
|
||||
"acm_certificate_arn": "",
|
||||
"cloudfront_default_certificate": true,
|
||||
"iam_certificate_id": "",
|
||||
"minimum_protocol_version": "TLSv1",
|
||||
"ssl_support_method": ""
|
||||
}
|
||||
],
|
||||
"wait_for_deployment": true,
|
||||
"web_acl_id": ""
|
||||
}
|
||||
}
|
||||
]
|
|
@ -6,6 +6,7 @@ import (
|
|||
"github.com/cloudskiff/driftctl/pkg/remote/aws/client"
|
||||
"github.com/cloudskiff/driftctl/pkg/remote/aws/repository"
|
||||
"github.com/cloudskiff/driftctl/pkg/resource"
|
||||
"github.com/cloudskiff/driftctl/pkg/resource/aws"
|
||||
"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)
|
||||
* 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)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -77,5 +78,8 @@ func Init(alerter *alerter.Alerter, providerLibrary *terraform.ProviderLibrary,
|
|||
supplierLibrary.AddSupplier(NewKMSAliasSupplier(provider))
|
||||
supplierLibrary.AddSupplier(NewLambdaEventSourceMappingSupplier(provider))
|
||||
|
||||
resourceSchemaRepository.Init(provider.Schema())
|
||||
aws.InitResourcesMetadata(resourceSchemaRepository)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"github.com/cloudskiff/driftctl/pkg/alerter"
|
||||
"github.com/cloudskiff/driftctl/pkg/output"
|
||||
"github.com/cloudskiff/driftctl/pkg/resource"
|
||||
"github.com/cloudskiff/driftctl/pkg/resource/github"
|
||||
"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)
|
||||
* 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)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -33,5 +34,8 @@ func Init(alerter *alerter.Alerter, providerLibrary *terraform.ProviderLibrary,
|
|||
supplierLibrary.AddSupplier(NewGithubTeamMembershipSupplier(provider, repository))
|
||||
supplierLibrary.AddSupplier(NewGithubBranchProtectionSupplier(provider, repository))
|
||||
|
||||
resourceSchemaRepository.Init(provider.Schema())
|
||||
github.InitMetadatas(resourceSchemaRepository)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -24,12 +24,12 @@ func IsSupported(remote string) bool {
|
|||
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 {
|
||||
case aws.RemoteAWSTerraform:
|
||||
return aws.Init(alerter, providerLibrary, supplierLibrary, progress)
|
||||
return aws.Init(alerter, providerLibrary, supplierLibrary, progress, resourceSchemaRepository)
|
||||
case github.RemoteGithubTerraform:
|
||||
return github.Init(alerter, providerLibrary, supplierLibrary, progress)
|
||||
return github.Init(alerter, providerLibrary, supplierLibrary, progress, resourceSchemaRepository)
|
||||
default:
|
||||
return errors.Errorf("unsupported remote '%s'", remote)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
// GENERATED, DO NOT EDIT THIS FILE
|
||||
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"
|
||||
|
||||
|
@ -155,3 +159,13 @@ func (r *AwsCloudfrontDistribution) TerraformType() string {
|
|||
func (r *AwsCloudfrontDistribution) CtyValue() *cty.Value {
|
||||
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"})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package aws
|
||||
|
||||
import "github.com/cloudskiff/driftctl/pkg/resource"
|
||||
|
||||
func InitResourcesMetadata(resourceSchemaRepository resource.SchemaRepositoryInterface) {
|
||||
initAwsCloudfrontDistributionMetaData(resourceSchemaRepository)
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package github
|
||||
|
||||
import "github.com/cloudskiff/driftctl/pkg/resource"
|
||||
|
||||
func InitMetadatas(resourceSchemaRepository resource.SchemaRepositoryInterface) {
|
||||
|
||||
}
|
|
@ -2,9 +2,14 @@ package resource
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
ctyjson "github.com/zclconf/go-cty/cty/json"
|
||||
)
|
||||
|
||||
type Resource interface {
|
||||
|
@ -13,6 +18,37 @@ type Resource interface {
|
|||
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 {
|
||||
CreateResource(data interface{}, ty string) (*cty.Value, error)
|
||||
}
|
||||
|
@ -70,3 +106,188 @@ func Sort(res []Resource) []Resource {
|
|||
})
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -3,27 +3,27 @@ package pkg
|
|||
import (
|
||||
"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/parallel"
|
||||
"github.com/cloudskiff/driftctl/pkg/remote"
|
||||
"github.com/cloudskiff/driftctl/pkg/resource"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Scanner struct {
|
||||
resourceSuppliers []resource.Supplier
|
||||
runner *parallel.ParallelRunner
|
||||
alerter *alerter.Alerter
|
||||
resourceSuppliers []resource.Supplier
|
||||
runner *parallel.ParallelRunner
|
||||
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{
|
||||
resourceSuppliers: resourceSuppliers,
|
||||
runner: parallel.NewParallelRunner(context.TODO(), 10),
|
||||
alerter: alerter,
|
||||
resourceSuppliers: resourceSuppliers,
|
||||
runner: parallel.NewParallelRunner(context.TODO(), 10),
|
||||
alerter: alerter,
|
||||
resourceSchemaRepository: resourceSchemaRepository,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -58,6 +58,24 @@ loop:
|
|||
break loop
|
||||
}
|
||||
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)
|
||||
if ok {
|
||||
normalizedRes, err := normalisable.NormalizeForProvider()
|
||||
|
|
|
@ -3,6 +3,10 @@ package resource
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cloudskiff/driftctl/pkg/resource"
|
||||
"github.com/cloudskiff/driftctl/test/schemas"
|
||||
|
||||
"github.com/hashicorp/terraform/providers"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
|
@ -64,3 +68,18 @@ func (r *FakeResourceStringer) CtyValue() *cty.Value {
|
|||
func (d *FakeResourceStringer) String() string {
|
||||
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
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue