driftctl/pkg/analyser/analyzer.go

222 lines
6.3 KiB
Go
Raw Normal View History

package analyser
import (
2020-12-16 12:02:02 +00:00
"reflect"
2021-02-15 14:49:20 +00:00
resourceaws "github.com/cloudskiff/driftctl/pkg/resource/aws"
"github.com/r3labs/diff/v2"
2020-12-16 12:02:02 +00:00
"github.com/cloudskiff/driftctl/pkg/alerter"
"github.com/cloudskiff/driftctl/pkg/resource"
)
type UnmanagedSecurityGroupRulesAlert struct{}
func newUnmanagedSecurityGroupRulesAlert() *UnmanagedSecurityGroupRulesAlert {
return &UnmanagedSecurityGroupRulesAlert{}
}
func (u *UnmanagedSecurityGroupRulesAlert) Message() string {
2021-04-13 08:25:18 +00:00
return "You have unmanaged security group rules that could be false positives, find out more at https://docs.driftctl.com/limitations"
}
func (u *UnmanagedSecurityGroupRulesAlert) ShouldIgnoreResource() bool {
return false
}
type ComputedDiffAlert struct{}
func NewComputedDiffAlert() *ComputedDiffAlert {
return &ComputedDiffAlert{}
}
func (c *ComputedDiffAlert) Message() string {
return "You have diffs on computed fields, check the documentation for potential false positive drifts"
}
func (c *ComputedDiffAlert) ShouldIgnoreResource() bool {
return false
}
2020-12-16 12:02:02 +00:00
type Analyzer struct {
2021-03-26 08:44:55 +00:00
alerter *alerter.Alerter
resourceSchemaRepository resource.SchemaRepositoryInterface
2020-12-16 12:02:02 +00:00
}
type Filter interface {
IsResourceIgnored(res resource.Resource) bool
IsFieldIgnored(res resource.Resource, path []string) bool
2020-12-18 14:28:46 +00:00
}
2021-03-26 08:44:55 +00:00
func NewAnalyzer(alerter *alerter.Alerter, resourceSchemaRepository resource.SchemaRepositoryInterface) Analyzer {
return Analyzer{alerter, resourceSchemaRepository}
}
2020-12-16 12:02:02 +00:00
func (a Analyzer) Analyze(remoteResources, resourcesFromState []resource.Resource, filter Filter) (Analysis, error) {
analysis := Analysis{}
2021-01-04 15:19:34 +00:00
// Iterate on remote resources and filter ignored resources
filteredRemoteResource := make([]resource.Resource, 0, len(remoteResources))
for _, remoteRes := range remoteResources {
2020-12-16 12:02:02 +00:00
if filter.IsResourceIgnored(remoteRes) || a.alerter.IsResourceIgnored(remoteRes) {
2021-01-04 15:19:34 +00:00
continue
}
filteredRemoteResource = append(filteredRemoteResource, remoteRes)
}
haveComputedDiff := false
for _, stateRes := range resourcesFromState {
2021-01-04 15:19:34 +00:00
i, remoteRes, found := findCorrespondingRes(filteredRemoteResource, stateRes)
2020-12-16 12:02:02 +00:00
if filter.IsResourceIgnored(stateRes) || a.alerter.IsResourceIgnored(stateRes) {
continue
}
if !found {
analysis.AddDeleted(stateRes)
continue
}
2021-01-04 15:19:34 +00:00
// Remove managed resources, so it will remain only unmanaged ones
filteredRemoteResource = removeResourceByIndex(i, filteredRemoteResource)
analysis.AddManaged(stateRes)
2021-03-26 08:44:55 +00:00
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)
}
2021-03-26 17:30:17 +00:00
if len(delta) == 0 {
continue
}
changelog := make([]Change, 0, len(delta))
for _, change := range delta {
if filter.IsFieldIgnored(stateRes, change.Path) {
continue
}
2021-03-26 17:30:17 +00:00
c := Change{Change: change}
2021-03-26 08:44:55 +00:00
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)
}
2021-03-26 17:30:17 +00:00
if c.Computed {
haveComputedDiff = true
}
2021-03-26 17:30:17 +00:00
changelog = append(changelog, c)
}
if len(changelog) > 0 {
analysis.AddDifference(Difference{
Res: stateRes,
Changelog: changelog,
})
}
}
2021-02-15 14:49:20 +00:00
if a.hasUnmanagedSecurityGroupRules(filteredRemoteResource) {
a.alerter.SendAlert("", newUnmanagedSecurityGroupRulesAlert())
2021-02-15 14:49:20 +00:00
}
if haveComputedDiff {
a.alerter.SendAlert("", NewComputedDiffAlert())
}
2021-01-04 15:19:34 +00:00
// Add remaining unmanaged resources
analysis.AddUnmanaged(filteredRemoteResource...)
// Sort resources by Terraform Id
// The purpose is to have a predictable output
analysis.SortResources()
2021-01-11 16:33:23 +00:00
analysis.SetAlerts(a.alerter.Retrieve())
2020-12-16 12:02:02 +00:00
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
}
2021-01-04 15:19:34 +00:00
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:]...)
}
2020-12-16 12:02:02 +00:00
2021-01-08 17:14:26 +00:00
// isComputedField returns true if the field that generated the diff of a resource
2020-12-16 12:02:02 +00:00
// has a computed tag
2021-01-08 17:14:26 +00:00
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
}
2021-03-26 08:44:55 +00:00
// 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
}
2020-12-16 12:02:02 +00:00
// 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:
{
2021-01-14 17:17:34 +00:00
if field, ok := t.FieldByName(path[0]); ok && a.hasNestedFields(field.Type) && len(path) > 1 {
2020-12-16 12:02:02 +00:00
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
}
}
2021-02-15 14:49:20 +00:00
// hasUnmanagedSecurityGroupRules returns true if we find at least one unmanaged
// security group rule
func (a Analyzer) hasUnmanagedSecurityGroupRules(unmanagedResources []resource.Resource) bool {
for _, res := range unmanagedResources {
if res.TerraformType() == resourceaws.AwsSecurityGroupRuleResourceType {
return true
}
}
return false
}