From d61925ca23ca72a15a09cb00ff03e42d060d331b Mon Sep 17 00:00:00 2001 From: Taylor Swanson Date: Tue, 28 Nov 2023 10:03:08 -0600 Subject: [PATCH 1/3] Sort Fleet integration policy inputs to ensure consistency - When new inputs are read from the Fleet API, the list is sorted according to the existing plan. Any new inputs added from the API will be moved to the end of the list. This ensures a consistency of inputs and prevents unnecssary changes from appearing. --- internal/fleet/integration_policy_resource.go | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/internal/fleet/integration_policy_resource.go b/internal/fleet/integration_policy_resource.go index a7a1528bd..5495665b1 100644 --- a/internal/fleet/integration_policy_resource.go +++ b/internal/fleet/integration_policy_resource.go @@ -3,6 +3,7 @@ package fleet import ( "context" "encoding/json" + "sort" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -340,7 +341,15 @@ func resourceIntegrationPolicyRead(ctx context.Context, d *schema.ResourceData, } } - var inputs []any + existingInputs, _ := d.Get("input").([]any) + inputIDToIndex := make(map[string]int, len(existingInputs)) + for i, v := range existingInputs { + inputData, _ := v.(map[string]any) + inputID, _ := inputData["input_id"].(string) + inputIDToIndex[inputID] = i + } + + newInputs := make([]any, 0, len(pkgPolicy.Inputs)) for inputID, input := range pkgPolicy.Inputs { inputMap := map[string]any{ "input_id": inputID, @@ -362,9 +371,28 @@ func resourceIntegrationPolicyRead(ctx context.Context, d *schema.ResourceData, inputMap["vars_json"] = string(data) } - inputs = append(inputs, inputMap) + newInputs = append(newInputs, inputMap) } - if err := d.Set("input", inputs); err != nil { + + sort.Slice(newInputs, func(i, j int) bool { + iInput, _ := newInputs[i].(map[string]any) + iID, _ := iInput["input_id"].(string) + iIdx, ok := inputIDToIndex[iID] + if !ok { + return false + } + + jInput, _ := newInputs[j].(map[string]any) + jID, _ := jInput["input_id"].(string) + jIdx, ok := inputIDToIndex[jID] + if !ok { + return true + } + + return iIdx < jIdx + }) + + if err := d.Set("input", newInputs); err != nil { return diag.FromErr(err) } From de88d3a2e34ea936f34ec327d64048b01da5f382 Mon Sep 17 00:00:00 2001 From: Taylor Swanson Date: Tue, 28 Nov 2023 10:07:13 -0600 Subject: [PATCH 2/3] Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ada879d9d..6aa148152 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Rename fleet package objects to `elasticstack_fleet_integration` and `elasticstack_fleet_integration_policy` ([#476](https://github.com/elastic/terraform-provider-elasticstack/pull/476)) - Fix a provider crash when managing SLOs outside of the default Kibana space. ([#485](https://github.com/elastic/terraform-provider-elasticstack/pull/485)) - Make input optional for `elasticstack_fleet_integration_policy` ([#493](https://github.com/elastic/terraform-provider-elasticstack/pull/493)) +- Sort Fleet integration policy inputs to ensure consistency ([#494](https://github.com/elastic/terraform-provider-elasticstack/pull/494)) ## [0.10.0] - 2023-11-02 From 6d0ce08ac4c4149856c7ff12f7e7d16e0f56f609 Mon Sep 17 00:00:00 2001 From: Taylor Swanson Date: Wed, 29 Nov 2023 08:13:58 -0600 Subject: [PATCH 3/3] Extract sort function and add unit test --- internal/fleet/integration_policy_resource.go | 66 +++++++++++-------- internal/fleet/shared_test.go | 63 ++++++++++++++++++ 2 files changed, 100 insertions(+), 29 deletions(-) create mode 100644 internal/fleet/shared_test.go diff --git a/internal/fleet/integration_policy_resource.go b/internal/fleet/integration_policy_resource.go index 5495665b1..e5a63422a 100644 --- a/internal/fleet/integration_policy_resource.go +++ b/internal/fleet/integration_policy_resource.go @@ -341,17 +341,9 @@ func resourceIntegrationPolicyRead(ctx context.Context, d *schema.ResourceData, } } - existingInputs, _ := d.Get("input").([]any) - inputIDToIndex := make(map[string]int, len(existingInputs)) - for i, v := range existingInputs { - inputData, _ := v.(map[string]any) - inputID, _ := inputData["input_id"].(string) - inputIDToIndex[inputID] = i - } - newInputs := make([]any, 0, len(pkgPolicy.Inputs)) for inputID, input := range pkgPolicy.Inputs { - inputMap := map[string]any{ + inputData := map[string]any{ "input_id": inputID, "enabled": input.Enabled, } @@ -361,36 +353,21 @@ func resourceIntegrationPolicyRead(ctx context.Context, d *schema.ResourceData, if err != nil { return diag.FromErr(err) } - inputMap["streams_json"] = string(data) + inputData["streams_json"] = string(data) } if input.Vars != nil { data, err := json.Marshal(*input.Vars) if err != nil { return diag.FromErr(err) } - inputMap["vars_json"] = string(data) + inputData["vars_json"] = string(data) } - newInputs = append(newInputs, inputMap) + newInputs = append(newInputs, inputData) } - sort.Slice(newInputs, func(i, j int) bool { - iInput, _ := newInputs[i].(map[string]any) - iID, _ := iInput["input_id"].(string) - iIdx, ok := inputIDToIndex[iID] - if !ok { - return false - } - - jInput, _ := newInputs[j].(map[string]any) - jID, _ := jInput["input_id"].(string) - jIdx, ok := inputIDToIndex[jID] - if !ok { - return true - } - - return iIdx < jIdx - }) + existingInputs, _ := d.Get("input").([]any) + sortInputs(newInputs, existingInputs) if err := d.Set("input", newInputs); err != nil { return diag.FromErr(err) @@ -414,3 +391,34 @@ func resourceIntegrationPolicyDelete(ctx context.Context, d *schema.ResourceData return diags } + +// sortInputs will sort the 'incoming' list of input definitions based on +// the order of inputs defined in the 'existing' list. Inputs not present in +// 'existing' will be placed at the end of the list. Inputs are identified by +// their ID ('input_id'). The 'incoming' slice will be sorted in-place. +func sortInputs(incoming []any, existing []any) { + idToIndex := make(map[string]int, len(existing)) + for i, v := range existing { + inputData, _ := v.(map[string]any) + inputID, _ := inputData["input_id"].(string) + idToIndex[inputID] = i + } + + sort.Slice(incoming, func(i, j int) bool { + iInput, _ := incoming[i].(map[string]any) + iID, _ := iInput["input_id"].(string) + iIdx, ok := idToIndex[iID] + if !ok { + return false + } + + jInput, _ := incoming[j].(map[string]any) + jID, _ := jInput["input_id"].(string) + jIdx, ok := idToIndex[jID] + if !ok { + return true + } + + return iIdx < jIdx + }) +} diff --git a/internal/fleet/shared_test.go b/internal/fleet/shared_test.go new file mode 100644 index 000000000..2b767880c --- /dev/null +++ b/internal/fleet/shared_test.go @@ -0,0 +1,63 @@ +package fleet + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_SortInputs(t *testing.T) { + t.Run("WithExisting", func(t *testing.T) { + existing := []any{ + map[string]any{"input_id": "A", "enabled": true}, + map[string]any{"input_id": "B", "enabled": true}, + map[string]any{"input_id": "C", "enabled": true}, + map[string]any{"input_id": "D", "enabled": true}, + map[string]any{"input_id": "E", "enabled": true}, + } + + incoming := []any{ + map[string]any{"input_id": "G", "enabled": true}, + map[string]any{"input_id": "F", "enabled": true}, + map[string]any{"input_id": "B", "enabled": true}, + map[string]any{"input_id": "E", "enabled": true}, + map[string]any{"input_id": "C", "enabled": true}, + } + + want := []any{ + map[string]any{"input_id": "B", "enabled": true}, + map[string]any{"input_id": "C", "enabled": true}, + map[string]any{"input_id": "E", "enabled": true}, + map[string]any{"input_id": "G", "enabled": true}, + map[string]any{"input_id": "F", "enabled": true}, + } + + sortInputs(incoming, existing) + + require.Equal(t, want, incoming) + }) + + t.Run("WithEmpty", func(t *testing.T) { + var existing []any + + incoming := []any{ + map[string]any{"input_id": "G", "enabled": true}, + map[string]any{"input_id": "F", "enabled": true}, + map[string]any{"input_id": "B", "enabled": true}, + map[string]any{"input_id": "E", "enabled": true}, + map[string]any{"input_id": "C", "enabled": true}, + } + + want := []any{ + map[string]any{"input_id": "G", "enabled": true}, + map[string]any{"input_id": "F", "enabled": true}, + map[string]any{"input_id": "B", "enabled": true}, + map[string]any{"input_id": "E", "enabled": true}, + map[string]any{"input_id": "C", "enabled": true}, + } + + sortInputs(incoming, existing) + + require.Equal(t, want, incoming) + }) +}