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 diff --git a/internal/fleet/integration_policy_resource.go b/internal/fleet/integration_policy_resource.go index a7a1528bd..e5a63422a 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,9 +341,9 @@ func resourceIntegrationPolicyRead(ctx context.Context, d *schema.ResourceData, } } - var inputs []any + 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, } @@ -352,19 +353,23 @@ 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) } - inputs = append(inputs, inputMap) + newInputs = append(newInputs, inputData) } - if err := d.Set("input", inputs); err != nil { + + existingInputs, _ := d.Get("input").([]any) + sortInputs(newInputs, existingInputs) + + if err := d.Set("input", newInputs); err != nil { return diag.FromErr(err) } @@ -386,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) + }) +}