driftctl/pkg/analyser/analyzer.go

168 lines
4.7 KiB
Go

package analyser
import (
"fmt"
"reflect"
"sort"
"strings"
"github.com/cloudskiff/driftctl/pkg/alerter"
"github.com/cloudskiff/driftctl/pkg/resource"
"github.com/r3labs/diff/v2"
)
type Analyzer struct {
alerter *alerter.Alerter
}
type Filter interface {
IsResourceIgnored(res resource.Resource) bool
IsFieldIgnored(res resource.Resource, path []string) bool
}
func NewAnalyzer(alerter *alerter.Alerter) Analyzer {
return Analyzer{alerter}
}
func (a Analyzer) Analyze(remoteResources, resourcesFromState []resource.Resource, filter Filter) (Analysis, error) {
analysis := Analysis{}
// Iterate on remote resources and filter ignored resources
filteredRemoteResource := make([]resource.Resource, 0, len(remoteResources))
for _, remoteRes := range remoteResources {
if filter.IsResourceIgnored(remoteRes) || a.alerter.IsResourceIgnored(remoteRes) {
continue
}
filteredRemoteResource = append(filteredRemoteResource, remoteRes)
}
for _, stateRes := range resourcesFromState {
i, remoteRes, found := findCorrespondingRes(filteredRemoteResource, stateRes)
if filter.IsResourceIgnored(stateRes) || a.alerter.IsResourceIgnored(stateRes) {
continue
}
if !found {
analysis.AddDeleted(stateRes)
continue
}
// Remove managed resources, so it will remain only unmanaged ones
filteredRemoteResource = removeResourceByIndex(i, filteredRemoteResource)
analysis.AddManaged(stateRes)
delta, _ := diff.Diff(stateRes, remoteRes)
if len(delta) > 0 {
sort.Slice(delta, func(i, j int) bool {
return delta[i].Type < delta[j].Type
})
changelog := make([]Change, 0, len(delta))
for _, change := range delta {
if filter.IsFieldIgnored(stateRes, change.Path) {
continue
}
c := Change{Change: change}
if a.isComputedField(stateRes, c) {
c.Computed = true
}
changelog = append(changelog, c)
}
if len(changelog) > 0 {
analysis.AddDifference(Difference{
Res: stateRes,
Changelog: changelog,
})
a.sendAlertOnComputedField(stateRes, changelog)
}
}
}
// Add remaining unmanaged resources
analysis.AddUnmanaged(filteredRemoteResource...)
analysis.AddAlerts(a.alerter.GetAlerts())
return analysis, nil
}
func findCorrespondingRes(resources []resource.Resource, res resource.Resource) (int, resource.Resource, bool) {
for i, r := range resources {
if resource.IsSameResource(res, r) {
return i, r, true
}
}
return -1, nil, false
}
func removeResourceByIndex(i int, resources []resource.Resource) []resource.Resource {
if i == len(resources)-1 {
return resources[:len(resources)-1]
}
return append(resources[:i], resources[i+1:]...)
}
// isComputedField returns true if the field that generated the diff of a resource
// has a computed tag
func (a Analyzer) isComputedField(stateRes resource.Resource, change Change) bool {
if field, ok := a.getField(reflect.TypeOf(stateRes), change.Path); ok {
return field.Tag.Get("computed") == "true"
}
return false
}
// sendAlertOnComputedField will send an alert to a channel for diffs on computed field
func (a Analyzer) sendAlertOnComputedField(stateRes resource.Resource, delta Changelog) {
for _, d := range delta {
if d.Computed {
// We need to copy the path (for console output compatibility) and remove the
// last index if it's a slice.
// We want a console output of format: struct.0.array.0: "foo" => "bar" (computed)
// We want a json output of format: "message": "struct.0.array is a computed field"
tmp := make([]string, len(d.Path))
copy(tmp, d.Path)
field, _ := a.getField(reflect.TypeOf(stateRes), tmp)
if field.Type.Kind() == reflect.Slice {
tmp = tmp[:len(tmp)-1]
}
path := strings.Join(tmp, ".")
a.alerter.SendAlert(fmt.Sprintf("%s.%s", stateRes.TerraformType(), stateRes.TerraformId()),
alerter.Alert{
Message: fmt.Sprintf("%s is a computed field", path),
})
}
}
}
// 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) {
switch t.Kind() {
case reflect.Ptr:
return a.getField(t.Elem(), path)
case reflect.Slice:
return a.getField(t.Elem(), path[1:])
default:
{
if field, ok := t.FieldByName(path[0]); ok && a.hasNestedFields(field.Type) {
return a.getField(field.Type, path[1:])
} else {
return field, ok
}
}
}
}
// hasNestedFields will return true if the current field is either a struct
// or a slice of struct
func (a Analyzer) hasNestedFields(t reflect.Type) bool {
switch t.Kind() {
case reflect.Ptr:
return a.hasNestedFields(t.Elem())
case reflect.Slice:
return t.Elem().Kind() == reflect.Struct
default:
return t.Kind() == reflect.Struct
}
}