Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions restapi/delta_checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import (
/*
* Performs a deep comparison of two maps - the resource as recorded in state, and the resource as returned by the API.
* Accepts a third argument that is a set of fields that are to be ignored when looking for differences.
* Accepts a fourth argument ignoreServerAdditions - when true, fields added by the server (but not in recordedResource) will be ignored.
* Returns 1. the recordedResource overlaid with fields that have been modified in actualResource but not ignored, and 2. a bool true if there were any changes.
*/
func getDelta(recordedResource map[string]interface{}, actualResource map[string]interface{}, ignoreList []string) (modifiedResource map[string]interface{}, hasChanges bool) {
func getDelta(recordedResource map[string]interface{}, actualResource map[string]interface{}, ignoreList []string, ignoreServerAdditions bool) (modifiedResource map[string]interface{}, hasChanges bool) {
modifiedResource = map[string]interface{}{}
hasChanges = false

Expand Down Expand Up @@ -46,7 +47,7 @@ func getDelta(recordedResource map[string]interface{}, actualResource map[string
}
// Recursively compare
deeperIgnoreList := _descendIgnoreList(key, ignoreList)
if modifiedSubResource, hasChange := getDelta(subMapA, subMapB, deeperIgnoreList); hasChange {
if modifiedSubResource, hasChange := getDelta(subMapA, subMapB, deeperIgnoreList, ignoreServerAdditions); hasChange {
modifiedResource[key] = modifiedSubResource
hasChanges = true
} else {
Expand Down Expand Up @@ -85,6 +86,10 @@ func getDelta(recordedResource map[string]interface{}, actualResource map[string
}

// If we've gotten here, that means actualResource has an additional key that wasn't in recordedResource
// When ignoreServerAdditions is true, we don't consider server-added fields as changes
if ignoreServerAdditions {
continue
}
modifiedResource[key] = valActual
hasChanges = true
}
Expand Down
78 changes: 75 additions & 3 deletions restapi/delta_checker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,15 +296,15 @@ func generateTypeConversionTests() []deltaTestCase {
func TestHasDelta(t *testing.T) {
// Run the main test cases
for _, testCase := range deltaTestCases {
_, result := getDelta(testCase.o1, testCase.o2, testCase.ignoreList)
_, result := getDelta(testCase.o1, testCase.o2, testCase.ignoreList, false)
if result != testCase.resultHasDelta {
t.Errorf("delta_checker_test.go: Test Case [%s] wanted [%v] got [%v]", testCase.testCase, testCase.resultHasDelta, result)
}
}

// Test type changes
for _, testCase := range generateTypeConversionTests() {
_, result := getDelta(testCase.o1, testCase.o2, testCase.ignoreList)
_, result := getDelta(testCase.o1, testCase.o2, testCase.ignoreList, false)
if result != testCase.resultHasDelta {
t.Errorf("delta_checker_test.go: TYPE CONVERSION Test Case [%d:%s] wanted [%v] got [%v]", testCase.testId, testCase.testCase, testCase.resultHasDelta, result)
}
Expand Down Expand Up @@ -345,8 +345,80 @@ func TestHasDeltaModifiedResource(t *testing.T) {

ignoreList := []string{"hairball", "hobbies.sleeping", "name"}

modified, _ := getDelta(recordedInput, actualInput, ignoreList)
modified, _ := getDelta(recordedInput, actualInput, ignoreList, false)
if !reflect.DeepEqual(expectedOutput, modified) {
t.Errorf("delta_checker_test.go: Unexpected delta: expected %v but got %v", expectedOutput, modified)
}
}

func TestIgnoreServerAdditions(t *testing.T) {
testCases := []struct {
name string
recorded map[string]interface{}
actual map[string]interface{}
ignoreServerAdditions bool
expectedHasChanges bool
expectedResult map[string]interface{}
}{
{
name: "Server adds field - not ignored",
recorded: MapAny{"foo": "bar"},
actual: MapAny{"foo": "bar", "status": "active"},
ignoreServerAdditions: false,
expectedHasChanges: true,
expectedResult: MapAny{"foo": "bar", "status": "active"},
},
{
name: "Server adds field - ignored",
recorded: MapAny{"foo": "bar"},
actual: MapAny{"foo": "bar", "status": "active"},
ignoreServerAdditions: true,
expectedHasChanges: false,
expectedResult: MapAny{"foo": "bar"},
},
{
name: "Server adds multiple fields - ignored",
recorded: MapAny{"name": "test"},
actual: MapAny{"name": "test", "created_at": "2025-01-01", "updated_at": "2025-01-02", "status": "active"},
ignoreServerAdditions: true,
expectedHasChanges: false,
expectedResult: MapAny{"name": "test"},
},
{
name: "Server changes configured field - not affected by ignoreServerAdditions",
recorded: MapAny{"name": "original"},
actual: MapAny{"name": "changed", "status": "active"},
ignoreServerAdditions: true,
expectedHasChanges: true,
expectedResult: MapAny{"name": "changed"},
},
{
name: "Server adds nested field - ignored",
recorded: MapAny{"config": MapAny{"enabled": true}},
actual: MapAny{"config": MapAny{"enabled": true, "timestamp": "2025-01-01"}},
ignoreServerAdditions: true,
expectedHasChanges: false,
expectedResult: MapAny{"config": MapAny{"enabled": true}},
},
{
name: "Server adds nested field - not ignored",
recorded: MapAny{"config": MapAny{"enabled": true}},
actual: MapAny{"config": MapAny{"enabled": true, "timestamp": "2025-01-01"}},
ignoreServerAdditions: false,
expectedHasChanges: true,
expectedResult: MapAny{"config": MapAny{"enabled": true, "timestamp": "2025-01-01"}},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, hasChanges := getDelta(tc.recorded, tc.actual, []string{}, tc.ignoreServerAdditions)
if hasChanges != tc.expectedHasChanges {
t.Errorf("Expected hasChanges=%v, got %v", tc.expectedHasChanges, hasChanges)
}
if !reflect.DeepEqual(result, tc.expectedResult) {
t.Errorf("Expected result=%v, got %v", tc.expectedResult, result)
}
})
}
}
2 changes: 1 addition & 1 deletion restapi/import_api_object_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func TestAccRestApiObject_importBasic(t *testing.T) {
ImportStateIdPrefix: "/api/objects/",
ImportStateVerify: true,
/* create_response isn't populated during import (we don't know the API response from creation) */
ImportStateVerifyIgnore: []string{"debug", "data", "create_response", "ignore_all_server_changes"},
ImportStateVerifyIgnore: []string{"debug", "data", "create_response", "ignore_all_server_changes", "ignore_server_additions"},
},
},
})
Expand Down
10 changes: 9 additions & 1 deletion restapi/resource_api_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,12 @@ func resourceRestAPI() *schema.Resource {
Optional: true,
Default: false,
},
"ignore_server_additions": {
Type: schema.TypeBool,
Description: "When set to 'true', fields added by the server (but not present in your configuration) will be ignored for drift detection. This prevents resource recreation when the API returns additional fields like defaults or metadata. Default: false",
Optional: true,
Default: false,
},
}, /* End schema */

}
Expand Down Expand Up @@ -325,9 +331,11 @@ func resourceRestAPIRead(d *schema.ResourceData, meta interface{}) error {
}
}

ignoreServerAdditions := d.Get("ignore_server_additions").(bool)

// This checks if there were any changes to the remote resource that will need to be corrected
// by comparing the current state with the response returned by the api.
modifiedResource, hasDifferences := getDelta(obj.data, obj.apiData, ignoreList)
modifiedResource, hasDifferences := getDelta(obj.data, obj.apiData, ignoreList, ignoreServerAdditions)

if hasDifferences {
log.Printf("resource_api_object.go: Found differences in remote resource\n")
Expand Down