tools/diff-processor/diff/diff.go (265 lines of code) (raw):
package diff
import (
"reflect"
"strings"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
// SchemaDiff is a nested map with resource names as top-level keys.
type SchemaDiff map[string]ResourceDiff
type ResourceDiff struct {
ResourceConfig ResourceConfigDiff
Fields map[string]FieldDiff
FieldSets ResourceFieldSetsDiff
}
type ResourceFieldSetsDiff struct {
Old ResourceFieldSets
New ResourceFieldSets
}
type ResourceFieldSets struct {
ConflictsWith map[string]FieldSet
ExactlyOneOf map[string]FieldSet
AtLeastOneOf map[string]FieldSet
RequiredWith map[string]FieldSet
}
type FieldSet map[string]struct{}
type ResourceConfigDiff struct {
Old *schema.Resource
New *schema.Resource
}
type FieldDiff struct {
Old *schema.Schema
New *schema.Schema
}
func ComputeSchemaDiff(oldResourceMap, newResourceMap map[string]*schema.Resource) SchemaDiff {
schemaDiff := make(SchemaDiff)
for resource := range union(oldResourceMap, newResourceMap) {
// Compute diff between old and new resources and fields.
// TODO: add support for computing diff between resource configs, not just whether the
// resource was added/removed. b/300114839
resourceDiff := ResourceDiff{}
var flattenedOldSchema map[string]*schema.Schema
if oldResource, ok := oldResourceMap[resource]; ok {
flattenedOldSchema = flattenSchema("", oldResource.Schema)
resourceDiff.ResourceConfig.Old = &schema.Resource{}
}
var flattenedNewSchema map[string]*schema.Schema
if newResource, ok := newResourceMap[resource]; ok {
flattenedNewSchema = flattenSchema("", newResource.Schema)
resourceDiff.ResourceConfig.New = &schema.Resource{}
}
resourceDiff.Fields = make(map[string]FieldDiff)
for key := range union(flattenedOldSchema, flattenedNewSchema) {
oldField := flattenedOldSchema[key]
newField := flattenedNewSchema[key]
if fieldDiff, fieldSetsDiff, changed := diffFields(oldField, newField, key); changed {
resourceDiff.Fields[key] = fieldDiff
resourceDiff.FieldSets = mergeFieldSetsDiff(resourceDiff.FieldSets, fieldSetsDiff)
}
}
if len(resourceDiff.Fields) > 0 || !cmp.Equal(resourceDiff.ResourceConfig.Old, resourceDiff.ResourceConfig.New) {
schemaDiff[resource] = resourceDiff
}
}
return schemaDiff
}
func flattenSchema(parentKey string, schemaObj map[string]*schema.Schema) map[string]*schema.Schema {
flattened := make(map[string]*schema.Schema)
if parentKey != "" {
parentKey += "."
}
for fieldName, field := range schemaObj {
key := parentKey + fieldName
flattened[key] = field
childResource, hasNestedFields := field.Elem.(*schema.Resource)
if field.Elem != nil && hasNestedFields {
for childKey, childField := range flattenSchema(key, childResource.Schema) {
flattened[childKey] = childField
}
}
}
return flattened
}
func diffFields(oldField, newField *schema.Schema, fieldName string) (FieldDiff, ResourceFieldSetsDiff, bool) {
// If either field is nil, it is changed; if both are nil (which should never happen) it's not
if oldField == nil && newField == nil {
return FieldDiff{}, ResourceFieldSetsDiff{}, false
}
oldFieldSets := fieldSets(oldField, fieldName)
newFieldSets := fieldSets(newField, fieldName)
fieldDiff := FieldDiff{
Old: oldField,
New: newField,
}
fieldSetsDiff := ResourceFieldSetsDiff{
Old: oldFieldSets,
New: newFieldSets,
}
if oldField == nil || newField == nil {
return fieldDiff, fieldSetsDiff, true
}
// Check if any basic Schema struct fields have changed.
// https://github.com/hashicorp/terraform-plugin-sdk/blob/v2.24.0/helper/schema/schema.go#L44
if basicSchemaChanged(oldField, newField) {
return fieldDiff, fieldSetsDiff, true
}
if !cmp.Equal(oldFieldSets, newFieldSets) {
return fieldDiff, fieldSetsDiff, true
}
if elemChanged(oldField, newField) {
return fieldDiff, fieldSetsDiff, true
}
if funcsChanged(oldField, newField) {
return fieldDiff, fieldSetsDiff, true
}
return FieldDiff{}, ResourceFieldSetsDiff{}, false
}
func basicSchemaChanged(oldField, newField *schema.Schema) bool {
if oldField.Type != newField.Type {
return true
}
if oldField.ConfigMode != newField.ConfigMode {
return true
}
if oldField.Required != newField.Required {
return true
}
if oldField.Optional != newField.Optional {
return true
}
if oldField.Computed != newField.Computed {
return true
}
if oldField.ForceNew != newField.ForceNew {
return true
}
if oldField.DiffSuppressOnRefresh != newField.DiffSuppressOnRefresh {
return true
}
if oldField.Default != newField.Default {
return true
}
if oldField.Description != newField.Description {
return true
}
if oldField.InputDefault != newField.InputDefault {
return true
}
if oldField.MaxItems != newField.MaxItems {
return true
}
if oldField.MinItems != newField.MinItems {
return true
}
if oldField.Deprecated != newField.Deprecated {
return true
}
if oldField.Sensitive != newField.Sensitive {
return true
}
return false
}
func fieldSets(field *schema.Schema, fieldName string) ResourceFieldSets {
if field == nil {
return ResourceFieldSets{}
}
var conflictsWith, exactlyOneOf, atLeastOneOf, requiredWith map[string]FieldSet
if len(field.ConflictsWith) > 0 {
set := sliceToSetRemoveZeroPadding(append(field.ConflictsWith, fieldName))
conflictsWith = map[string]FieldSet{
setKey(set): set,
}
}
if len(field.ExactlyOneOf) > 0 {
set := sliceToSetRemoveZeroPadding(append(field.ExactlyOneOf, fieldName))
exactlyOneOf = map[string]FieldSet{
setKey(set): set,
}
}
if len(field.AtLeastOneOf) > 0 {
set := sliceToSetRemoveZeroPadding(append(field.AtLeastOneOf, fieldName))
atLeastOneOf = map[string]FieldSet{
setKey(set): set,
}
}
if len(field.RequiredWith) > 0 {
set := sliceToSetRemoveZeroPadding(append(field.RequiredWith, fieldName))
requiredWith = map[string]FieldSet{
setKey(set): set,
}
}
return ResourceFieldSets{
ConflictsWith: conflictsWith,
ExactlyOneOf: exactlyOneOf,
AtLeastOneOf: atLeastOneOf,
RequiredWith: requiredWith,
}
}
func elemChanged(oldField, newField *schema.Schema) bool {
// Check if Elem changed (unless old and new both represent nested fields)
if (oldField.Elem == nil && newField.Elem != nil) || (oldField.Elem != nil && newField.Elem == nil) {
return true
}
if oldField.Elem != nil && newField.Elem != nil {
// At this point new/old Elems are either schema.Schema or schema.Resource.
// If both are schema.Resource we don't need to do anything. Diffs on subfields
// are handled separately.
_, oldIsResource := oldField.Elem.(*schema.Resource)
_, newIsResource := newField.Elem.(*schema.Resource)
if (oldIsResource && !newIsResource) || (!oldIsResource && newIsResource) {
return true
}
if !oldIsResource && !newIsResource {
if _, _, changed := diffFields(oldField.Elem.(*schema.Schema), newField.Elem.(*schema.Schema), ""); changed {
return true
}
}
}
return false
}
func funcsChanged(oldField, newField *schema.Schema) bool {
// Check if any Schema struct fields that are functions have changed
if funcChanged(oldField.DiffSuppressFunc, newField.DiffSuppressFunc) {
return true
}
if funcChanged(oldField.DefaultFunc, newField.DefaultFunc) {
return true
}
if funcChanged(oldField.StateFunc, newField.StateFunc) {
return true
}
if funcChanged(oldField.Set, newField.Set) {
return true
}
if funcChanged(oldField.ValidateFunc, newField.ValidateFunc) {
return true
}
if funcChanged(oldField.ValidateDiagFunc, newField.ValidateDiagFunc) {
return true
}
return false
}
func funcChanged(oldFunc, newFunc interface{}) bool {
// If it changed to/from nil, it changed
oldFuncIsNil := reflect.ValueOf(oldFunc).IsNil()
newFuncIsNil := reflect.ValueOf(newFunc).IsNil()
if (oldFuncIsNil && !newFuncIsNil) || (!oldFuncIsNil && newFuncIsNil) {
return true
}
// If a func is set before and after we don't currently have a way to reliably
// determine whether the function changed, so we assume that it has not changed.
// b/300157205
return false
}
func mergeFieldSetsDiff(allFields ResourceFieldSetsDiff, currentField ResourceFieldSetsDiff) ResourceFieldSetsDiff {
allFields.Old = mergeResourceFieldSets(allFields.Old, currentField.Old)
allFields.New = mergeResourceFieldSets(allFields.New, currentField.New)
return allFields
}
func mergeResourceFieldSets(allFields ResourceFieldSets, currentField ResourceFieldSets) ResourceFieldSets {
allFields.ConflictsWith = mergeFieldSets(allFields.ConflictsWith, currentField.ConflictsWith)
allFields.ExactlyOneOf = mergeFieldSets(allFields.ExactlyOneOf, currentField.ExactlyOneOf)
allFields.AtLeastOneOf = mergeFieldSets(allFields.AtLeastOneOf, currentField.AtLeastOneOf)
allFields.RequiredWith = mergeFieldSets(allFields.RequiredWith, currentField.RequiredWith)
return allFields
}
func mergeFieldSets(allFields, currentField map[string]FieldSet) map[string]FieldSet {
if allFields == nil {
allFields = make(map[string]FieldSet)
}
for key, fieldSet := range currentField {
allFields[key] = fieldSet
}
if len(allFields) == 0 {
return nil
}
return allFields
}
func setKey(set FieldSet) string {
slice := setToSortedSlice(set)
return strings.Join(slice, ",")
}