Skip to content

Commit bb0cae6

Browse files
committed
Move the input blocks to an inputs map
By itself this is just a bit of a painful breaking change, however it allows us to provide much nicer handling for default value handling, allowing practitioners to only supply non-default input/stream variables
1 parent 863d7cf commit bb0cae6

File tree

26 files changed

+1514
-455
lines changed

26 files changed

+1514
-455
lines changed

docs/resources/fleet_integration_policy.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ resource "elasticstack_fleet_integration_policy" "sample" {
103103
- `description` (String) The description of the integration policy.
104104
- `enabled` (Boolean) Enable the integration policy.
105105
- `force` (Boolean) Force operations, such as creation and deletion, to occur.
106-
- `input` (Block List) Integration inputs. (see [below for nested schema](#nestedblock--input))
106+
- `inputs` (Attributes Map) Integration inputs mapped by input ID. (see [below for nested schema](#nestedatt--inputs))
107107
- `output_id` (String) The ID of the output to send data to. When not specified, the default output of the agent policy will be used.
108108
- `policy_id` (String) Unique identifier of the integration policy.
109109
- `space_ids` (Set of String) The Kibana space IDs where this integration policy is available. When set, must match the space_ids of the referenced agent policy. If not set, will be inherited from the agent policy. Note: The order of space IDs does not matter as this is a set.
@@ -113,18 +113,22 @@ resource "elasticstack_fleet_integration_policy" "sample" {
113113

114114
- `id` (String) The ID of this resource.
115115

116-
<a id="nestedblock--input"></a>
117-
### Nested Schema for `input`
116+
<a id="nestedatt--inputs"></a>
117+
### Nested Schema for `inputs`
118118

119-
Required:
119+
Optional:
120+
121+
- `enabled` (Boolean) Enable the input.
122+
- `streams` (Attributes Map) Input streams mapped by stream ID. (see [below for nested schema](#nestedatt--inputs--streams))
123+
- `vars` (String, Sensitive) Input-level variables as JSON.
120124

121-
- `input_id` (String) The identifier of the input.
125+
<a id="nestedatt--inputs--streams"></a>
126+
### Nested Schema for `inputs.streams`
122127

123128
Optional:
124129

125-
- `enabled` (Boolean) Enable the input.
126-
- `streams_json` (String, Sensitive) Input streams as JSON.
127-
- `vars_json` (String, Sensitive) Input variables as JSON.
130+
- `enabled` (Boolean) Enable the stream.
131+
- `vars` (String, Sensitive) Stream-level variables as JSON.
128132

129133
## Import
130134

internal/fleet/integration/acc_test.go

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -199,22 +199,23 @@ resource "elasticstack_fleet_integration_policy" "sample" {
199199
integration_name = elasticstack_fleet_integration.test_integration.name
200200
integration_version = elasticstack_fleet_integration.test_integration.version
201201
202-
input {
203-
input_id = "tcp-tcp"
204-
streams_json = jsonencode({
205-
"tcp.generic" : {
206-
"enabled" : true,
207-
"vars" : {
208-
"listen_address" : "localhost",
209-
"listen_port" : 8080,
210-
"data_stream.dataset" : "tcp.generic",
211-
"tags" : [],
212-
"syslog_options" : "field: message\n#format: auto\n#timezone: Local\n",
213-
"ssl" : "#certificate: |\n# -----BEGIN CERTIFICATE-----\n# ...\n# -----END CERTIFICATE-----\n#key: |\n# -----BEGIN PRIVATE KEY-----\n# ...\n# -----END PRIVATE KEY-----\n",
214-
"custom" : ""
215-
}
202+
inputs = {
203+
"tcp-tcp" = {
204+
streams = {
205+
"tcp.generic" = {
206+
enabled = true,
207+
vars = jsonencode({
208+
"listen_address" : "localhost",
209+
"listen_port" : 8080,
210+
"data_stream.dataset" : "tcp.generic",
211+
"tags" : [],
212+
"syslog_options" : "field: message\n#format: auto\n#timezone: Local\n",
213+
"ssl" : "#certificate: |\n# -----BEGIN CERTIFICATE-----\n# ...\n# -----END CERTIFICATE-----\n#key: |\n# -----BEGIN PRIVATE KEY-----\n# ...\n# -----END PRIVATE KEY-----\n",
214+
"custom" : ""
215+
})
216+
}
216217
}
217-
})
218+
}
218219
}
219220
}
220221
`, version, policyName, policyName)

internal/fleet/integration_policy/acc_test.go

Lines changed: 28 additions & 37 deletions
Large diffs are not rendered by default.

internal/fleet/integration_policy/create.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func (r *integrationPolicyResource) Create(ctx context.Context, req resource.Cre
6565
}
6666

6767
// Remember if the user configured input in the plan
68-
planHadInput := utils.IsKnown(planModel.Input) && !planModel.Input.IsNull() && len(planModel.Input.Elements()) > 0
68+
planHadInput := utils.IsKnown(planModel.Inputs) && !planModel.Inputs.IsNull() && len(planModel.Inputs.Elements()) > 0
6969

7070
diags = planModel.populateFromAPI(ctx, policy)
7171
resp.Diagnostics.Append(diags...)
@@ -76,7 +76,7 @@ func (r *integrationPolicyResource) Create(ctx context.Context, req resource.Cre
7676
// If plan didn't have input configured, ensure we don't add it now
7777
// This prevents "Provider produced inconsistent result" errors
7878
if !planHadInput {
79-
planModel.Input = types.ListNull(getInputTypeV1())
79+
planModel.Inputs = types.MapNull(getInputsTypes())
8080
}
8181

8282
diags = resp.State.Set(ctx, planModel)

internal/fleet/integration_policy/models.go

Lines changed: 78 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package integration_policy
33
import (
44
"context"
55
"fmt"
6-
"sort"
76

87
"github.com/elastic/terraform-provider-elasticstack/generated/kbapi"
98
"github.com/elastic/terraform-provider-elasticstack/internal/utils"
@@ -31,16 +30,20 @@ type integrationPolicyModel struct {
3130
IntegrationName types.String `tfsdk:"integration_name"`
3231
IntegrationVersion types.String `tfsdk:"integration_version"`
3332
OutputID types.String `tfsdk:"output_id"`
34-
Input types.List `tfsdk:"input"` //> integrationPolicyInputModel
33+
Inputs types.Map `tfsdk:"inputs"` //> integrationPolicyInputsModel
3534
VarsJson jsontypes.Normalized `tfsdk:"vars_json"`
3635
SpaceIds types.Set `tfsdk:"space_ids"`
3736
}
3837

39-
type integrationPolicyInputModel struct {
40-
InputID types.String `tfsdk:"input_id"`
41-
Enabled types.Bool `tfsdk:"enabled"`
42-
StreamsJson jsontypes.Normalized `tfsdk:"streams_json"`
43-
VarsJson jsontypes.Normalized `tfsdk:"vars_json"`
38+
type integrationPolicyInputsModel struct {
39+
Enabled types.Bool `tfsdk:"enabled"`
40+
Vars jsontypes.Normalized `tfsdk:"vars"`
41+
Streams types.Map `tfsdk:"streams"` //> integrationPolicyInputStreamModel
42+
}
43+
44+
type integrationPolicyInputStreamModel struct {
45+
Enabled types.Bool `tfsdk:"enabled"`
46+
Vars jsontypes.Normalized `tfsdk:"vars"`
4447
}
4548

4649
func (model *integrationPolicyModel) populateFromAPI(ctx context.Context, data *kbapi.PackagePolicy) diag.Diagnostics {
@@ -107,20 +110,19 @@ func (model *integrationPolicyModel) populateFromAPI(ctx context.Context, data *
107110
model.SpaceIds = types.SetNull(types.StringType)
108111
}
109112
// If originally set but API didn't return it, keep the original value
110-
111-
model.populateInputFromAPI(ctx, data.Inputs, &diags)
113+
model.populateInputsFromAPI(ctx, data.Inputs, &diags)
112114

113115
return diags
114116
}
115117

116-
func (model *integrationPolicyModel) populateInputFromAPI(ctx context.Context, inputs map[string]kbapi.PackagePolicyInput, diags *diag.Diagnostics) {
118+
func (model *integrationPolicyModel) populateInputsFromAPI(ctx context.Context, inputs map[string]kbapi.PackagePolicyInput, diags *diag.Diagnostics) {
117119
// Handle input population based on context:
118120
// 1. If model.Input is unknown: we're importing or reading fresh state → populate from API
119121
// 2. If model.Input is known and null/empty: user explicitly didn't configure inputs → don't populate (avoid inconsistent state)
120122
// 3. If model.Input is known and has values: user configured inputs → populate from API
121123

122-
isInputKnown := utils.IsKnown(model.Input)
123-
isInputNullOrEmpty := model.Input.IsNull() || (isInputKnown && len(model.Input.Elements()) == 0)
124+
isInputKnown := utils.IsKnown(model.Inputs)
125+
isInputNullOrEmpty := model.Inputs.IsNull() || (isInputKnown && len(model.Inputs.Elements()) == 0)
124126

125127
// Case 1: Unknown (import/fresh read) - always populate
126128
if !isInputKnown {
@@ -129,32 +131,43 @@ func (model *integrationPolicyModel) populateInputFromAPI(ctx context.Context, i
129131
} else if isInputNullOrEmpty {
130132
// Case 2: Known and null/empty - user explicitly didn't configure inputs
131133
// Don't populate to avoid "Provider produced inconsistent result" error
132-
model.Input = types.ListNull(getInputTypeV1())
134+
model.Inputs = types.MapNull(getInputsTypes())
133135
return
134136
}
135137
// Case 3: Known and not null/empty - user configured inputs, populate from API (continue below)
136138

137-
newInputs := utils.TransformMapToSlice(ctx, inputs, path.Root("input"), diags,
138-
func(inputData kbapi.PackagePolicyInput, meta utils.MapMeta) integrationPolicyInputModel {
139-
return integrationPolicyInputModel{
140-
InputID: types.StringValue(meta.Key),
141-
Enabled: types.BoolPointerValue(inputData.Enabled),
142-
StreamsJson: utils.MapToNormalizedType(utils.Deref(inputData.Streams), meta.Path.AtName("streams_json"), diags),
143-
VarsJson: utils.MapToNormalizedType(utils.Deref(inputData.Vars), meta.Path.AtName("vars_json"), diags),
144-
}
145-
})
146-
if newInputs == nil {
147-
model.Input = types.ListNull(getInputTypeV1())
148-
} else {
149-
oldInputs := utils.ListTypeAs[integrationPolicyInputModel](ctx, model.Input, path.Root("input"), diags)
139+
newInputs := make(map[string]integrationPolicyInputsModel)
140+
for inputID, inputData := range inputs {
141+
inputModel := integrationPolicyInputsModel{
142+
Enabled: types.BoolPointerValue(inputData.Enabled),
143+
Vars: utils.MapToNormalizedType(utils.Deref(inputData.Vars), path.Root("inputs").AtMapKey(inputID).AtName("vars"), diags),
144+
}
150145

151-
sortInputs(newInputs, oldInputs)
146+
// Populate streams
147+
if inputData.Streams != nil && len(*inputData.Streams) > 0 {
148+
streams := make(map[string]integrationPolicyInputStreamModel)
149+
for streamID, streamData := range *inputData.Streams {
150+
streamModel := integrationPolicyInputStreamModel{
151+
Enabled: types.BoolPointerValue(streamData.Enabled),
152+
Vars: utils.MapToNormalizedType(utils.Deref(streamData.Vars), path.Root("inputs").AtMapKey(inputID).AtName("streams").AtMapKey(streamID).AtName("vars"), diags),
153+
}
152154

153-
inputList, d := types.ListValueFrom(ctx, getInputTypeV1(), newInputs)
154-
diags.Append(d...)
155+
streams[streamID] = streamModel
156+
}
157+
158+
streamsMap, d := types.MapValueFrom(ctx, getInputStreamType(), streams)
159+
diags.Append(d...)
160+
inputModel.Streams = streamsMap
161+
} else {
162+
inputModel.Streams = types.MapNull(getInputStreamType())
163+
}
155164

156-
model.Input = inputList
165+
newInputs[inputID] = inputModel
157166
}
167+
168+
inputsMap, d := types.MapValueFrom(ctx, getInputsTypes(), newInputs)
169+
diags.Append(d...)
170+
model.Inputs = inputsMap
158171
}
159172

160173
func (model integrationPolicyModel) toAPIModel(ctx context.Context, isUpdate bool, feat features) (kbapi.PackagePolicyRequest, diag.Diagnostics) {
@@ -218,51 +231,53 @@ func (model integrationPolicyModel) toAPIModel(ctx context.Context, isUpdate boo
218231
body.Id = model.ID.ValueStringPointer()
219232
}
220233

221-
body.Inputs = utils.MapRef(utils.ListTypeToMap(ctx, model.Input, path.Root("input"), &diags,
222-
func(inputModel integrationPolicyInputModel, meta utils.ListMeta) (string, kbapi.PackagePolicyRequestInput) {
223-
return inputModel.InputID.ValueString(), kbapi.PackagePolicyRequestInput{
224-
Enabled: inputModel.Enabled.ValueBoolPointer(),
225-
Streams: utils.MapRef(utils.NormalizedTypeToMap[kbapi.PackagePolicyRequestInputStream](inputModel.StreamsJson, meta.Path.AtName("streams_json"), &diags)),
226-
Vars: utils.MapRef(utils.NormalizedTypeToMap[any](inputModel.VarsJson, meta.Path.AtName("vars_json"), &diags)),
227-
}
228-
}))
234+
if utils.IsKnown(model.Inputs) && len(model.Inputs.Elements()) > 0 {
235+
// Use the 'inputs' attribute (v2 format)
236+
body.Inputs = utils.MapRef(model.toAPIInputsFromInputsAttribute(ctx, &diags))
237+
}
229238

230239
// Note: space_ids is read-only for integration policies and inherited from the agent policy
231240

232241
return body, diags
233242
}
234243

235-
// sortInputs will sort the 'incoming' list of input definitions based on
236-
// the order of inputs defined in the 'existing' list. Inputs not present in
237-
// 'existing' will be placed at the end of the list. Inputs are identified by
238-
// their ID ('input_id'). The 'incoming' slice will be sorted in-place.
239-
func sortInputs(incoming []integrationPolicyInputModel, existing []integrationPolicyInputModel) {
240-
if len(existing) == 0 {
241-
sort.Slice(incoming, func(i, j int) bool {
242-
return incoming[i].InputID.ValueString() < incoming[j].InputID.ValueString()
243-
})
244-
return
244+
// toAPIInputsFromInputsAttribute converts the 'inputs' attribute to the API model format
245+
func (model integrationPolicyModel) toAPIInputsFromInputsAttribute(ctx context.Context, diags *diag.Diagnostics) map[string]kbapi.PackagePolicyRequestInput {
246+
if !utils.IsKnown(model.Inputs) {
247+
return nil
245248
}
246249

247-
idToIndex := make(map[string]int, len(existing))
248-
for index, inputData := range existing {
249-
inputID := inputData.InputID.ValueString()
250-
idToIndex[inputID] = index
250+
inputsMap := utils.MapTypeAs[integrationPolicyInputsModel](ctx, model.Inputs, path.Root("inputs"), diags)
251+
if inputsMap == nil {
252+
return nil
251253
}
252254

253-
sort.Slice(incoming, func(i, j int) bool {
254-
iID := incoming[i].InputID.ValueString()
255-
iIdx, ok := idToIndex[iID]
256-
if !ok {
257-
return false
255+
result := make(map[string]kbapi.PackagePolicyRequestInput, len(inputsMap))
256+
for inputID, inputModel := range inputsMap {
257+
inputPath := path.Root("inputs").AtMapKey(inputID)
258+
259+
apiInput := kbapi.PackagePolicyRequestInput{
260+
Enabled: inputModel.Enabled.ValueBoolPointer(),
261+
Vars: utils.MapRef(utils.NormalizedTypeToMap[any](inputModel.Vars, inputPath.AtName("vars"), diags)),
258262
}
259263

260-
jID := incoming[j].InputID.ValueString()
261-
jIdx, ok := idToIndex[jID]
262-
if !ok {
263-
return true
264+
// Convert streams if present
265+
if utils.IsKnown(inputModel.Streams) && len(inputModel.Streams.Elements()) > 0 {
266+
streamsMap := utils.MapTypeAs[integrationPolicyInputStreamModel](ctx, inputModel.Streams, inputPath.AtName("streams"), diags)
267+
if streamsMap != nil {
268+
streams := make(map[string]kbapi.PackagePolicyRequestInputStream, len(streamsMap))
269+
for streamID, streamModel := range streamsMap {
270+
streams[streamID] = kbapi.PackagePolicyRequestInputStream{
271+
Enabled: streamModel.Enabled.ValueBoolPointer(),
272+
Vars: utils.MapRef(utils.NormalizedTypeToMap[any](streamModel.Vars, inputPath.AtName("streams").AtMapKey(streamID).AtName("vars"), diags)),
273+
}
274+
}
275+
apiInput.Streams = &streams
276+
}
264277
}
265278

266-
return iIdx < jIdx
267-
})
279+
result[inputID] = apiInput
280+
}
281+
282+
return result
268283
}

internal/fleet/integration_policy/models_test.go

Lines changed: 0 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -9,62 +9,6 @@ import (
99
"github.com/stretchr/testify/require"
1010
)
1111

12-
func Test_SortInputs(t *testing.T) {
13-
t.Run("WithExisting", func(t *testing.T) {
14-
existing := []integrationPolicyInputModel{
15-
{InputID: types.StringValue("A"), Enabled: types.BoolValue(true)},
16-
{InputID: types.StringValue("B"), Enabled: types.BoolValue(true)},
17-
{InputID: types.StringValue("C"), Enabled: types.BoolValue(true)},
18-
{InputID: types.StringValue("D"), Enabled: types.BoolValue(true)},
19-
{InputID: types.StringValue("E"), Enabled: types.BoolValue(true)},
20-
}
21-
22-
incoming := []integrationPolicyInputModel{
23-
{InputID: types.StringValue("G"), Enabled: types.BoolValue(true)},
24-
{InputID: types.StringValue("F"), Enabled: types.BoolValue(true)},
25-
{InputID: types.StringValue("B"), Enabled: types.BoolValue(true)},
26-
{InputID: types.StringValue("E"), Enabled: types.BoolValue(true)},
27-
{InputID: types.StringValue("C"), Enabled: types.BoolValue(true)},
28-
}
29-
30-
want := []integrationPolicyInputModel{
31-
{InputID: types.StringValue("B"), Enabled: types.BoolValue(true)},
32-
{InputID: types.StringValue("C"), Enabled: types.BoolValue(true)},
33-
{InputID: types.StringValue("E"), Enabled: types.BoolValue(true)},
34-
{InputID: types.StringValue("G"), Enabled: types.BoolValue(true)},
35-
{InputID: types.StringValue("F"), Enabled: types.BoolValue(true)},
36-
}
37-
38-
sortInputs(incoming, existing)
39-
40-
require.Equal(t, want, incoming)
41-
})
42-
43-
t.Run("WithEmpty", func(t *testing.T) {
44-
var existing []integrationPolicyInputModel
45-
46-
incoming := []integrationPolicyInputModel{
47-
{InputID: types.StringValue("G"), Enabled: types.BoolValue(true)},
48-
{InputID: types.StringValue("F"), Enabled: types.BoolValue(true)},
49-
{InputID: types.StringValue("B"), Enabled: types.BoolValue(true)},
50-
{InputID: types.StringValue("E"), Enabled: types.BoolValue(true)},
51-
{InputID: types.StringValue("C"), Enabled: types.BoolValue(true)},
52-
}
53-
54-
want := []integrationPolicyInputModel{
55-
{InputID: types.StringValue("B"), Enabled: types.BoolValue(true)},
56-
{InputID: types.StringValue("C"), Enabled: types.BoolValue(true)},
57-
{InputID: types.StringValue("E"), Enabled: types.BoolValue(true)},
58-
{InputID: types.StringValue("F"), Enabled: types.BoolValue(true)},
59-
{InputID: types.StringValue("G"), Enabled: types.BoolValue(true)},
60-
}
61-
62-
sortInputs(incoming, existing)
63-
64-
require.Equal(t, want, incoming)
65-
})
66-
}
67-
6812
func TestOutputIdHandling(t *testing.T) {
6913
t.Run("populateFromAPI", func(t *testing.T) {
7014
model := &integrationPolicyModel{}

internal/fleet/integration_policy/read.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func (r *integrationPolicyResource) Read(ctx context.Context, req resource.ReadR
5454
}
5555

5656
// Remember if the state had input configured
57-
stateHadInput := utils.IsKnown(stateModel.Input) && !stateModel.Input.IsNull() && len(stateModel.Input.Elements()) > 0
57+
stateHadInput := utils.IsKnown(stateModel.Inputs) && !stateModel.Inputs.IsNull() && len(stateModel.Inputs.Elements()) > 0
5858

5959
// Check if this is an import operation (PolicyID is the only field set)
6060
isImport := stateModel.PolicyID.ValueString() != "" &&
@@ -70,7 +70,7 @@ func (r *integrationPolicyResource) Read(ctx context.Context, req resource.ReadR
7070
// This prevents "Provider produced inconsistent result" errors during refresh
7171
// However, during import we should always populate inputs from the API
7272
if !stateHadInput && !isImport {
73-
stateModel.Input = types.ListNull(getInputTypeV1())
73+
stateModel.Inputs = types.MapNull(getInputsTypes())
7474
}
7575

7676
diags = resp.State.Set(ctx, stateModel)

internal/fleet/integration_policy/resource.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ func (r *integrationPolicyResource) ImportState(ctx context.Context, req resourc
4949

5050
func (r *integrationPolicyResource) UpgradeState(context.Context) map[int64]resource.StateUpgrader {
5151
return map[int64]resource.StateUpgrader{
52-
0: {PriorSchema: getSchemaV0(), StateUpgrader: upgradeV0},
52+
0: {PriorSchema: getSchemaV0(), StateUpgrader: upgradeV0ToV2},
53+
1: {PriorSchema: getSchemaV1(), StateUpgrader: upgradeV1ToV2},
5354
}
5455
}
5556

0 commit comments

Comments
 (0)