diff --git a/internal/kibana/data_view/acc_test.go b/internal/kibana/data_view/acc_test.go index 3d2c1fed2..658e51583 100644 --- a/internal/kibana/data_view/acc_test.go +++ b/internal/kibana/data_view/acc_test.go @@ -65,6 +65,39 @@ func TestAccResourceDataView(t *testing.T) { }) } +// TestAccResourceDataViewFieldAttrsReproduceIssue reproduces the issue where +// server-side generated field popularity counts cause forced replacement +func TestAccResourceDataViewFieldAttrsReproduceIssue(t *testing.T) { + indexName := "reproduce-issue-" + sdkacctest.RandStringFromCharSet(4, sdkacctest.CharSetAlphaNum) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.Providers, + Steps: []resource.TestStep{ + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minFullDataviewSupport), + Config: testAccResourceDataViewReproduceIssue(indexName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("elasticstack_kibana_data_view.reproduce_issue", "id"), + resource.TestCheckResourceAttr("elasticstack_kibana_data_view.reproduce_issue", "data_view.title", indexName+"*"), + resource.TestCheckResourceAttr("elasticstack_kibana_data_view.reproduce_issue", "data_view.name", "Reproduce Issue Data View"), + ), + }, + // This step verifies that server-side generated field_attrs don't force replacement + // In a real scenario, this would happen after Kibana UI interaction generates counts + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minFullDataviewSupport), + Config: testAccResourceDataViewReproduceIssue(indexName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("elasticstack_kibana_data_view.reproduce_issue", "id"), + resource.TestCheckResourceAttr("elasticstack_kibana_data_view.reproduce_issue", "data_view.title", indexName+"*"), + resource.TestCheckResourceAttr("elasticstack_kibana_data_view.reproduce_issue", "data_view.name", "Reproduce Issue Data View"), + ), + }, + }, + }) +} + func testAccResourceDataViewPre8_8DV(indexName string) string { return fmt.Sprintf(` provider "elasticstack" { @@ -151,3 +184,24 @@ resource "elasticstack_kibana_data_view" "dv" { } }`, indexName, indexName, indexName) } + +func testAccResourceDataViewReproduceIssue(indexName string) string { + return fmt.Sprintf(` +provider "elasticstack" { + elasticsearch {} + kibana {} +} + +resource "elasticstack_elasticsearch_index" "reproduce_issue_index" { + name = "%s" + deletion_protection = false +} + +resource "elasticstack_kibana_data_view" "reproduce_issue" { + data_view = { + title = "%s*" + name = "Reproduce Issue Data View" + id = "reproduce-issue-data-view-id" + } +}`, indexName, indexName) +} diff --git a/internal/kibana/data_view/models_test.go b/internal/kibana/data_view/models_test.go index 4cec22bd9..1314b5c9e 100644 --- a/internal/kibana/data_view/models_test.go +++ b/internal/kibana/data_view/models_test.go @@ -202,6 +202,111 @@ func TestPopulateFromAPI(t *testing.T) { }, getDataViewAttrTypes(), path.Root("data_view"), &diags), }, }, + { + // Test handling of server-side count updates without existing field_attrs + name: "server_side_count_updates_no_existing_attrs", + existingModel: dataViewModel{ + ID: types.StringValue("default/test-id"), + SpaceID: types.StringValue("default"), + DataView: utils.ObjectValueFrom(ctx, &innerModel{ + ID: types.StringValue("test-id"), + Title: types.StringValue("test-title"), + FieldAttributes: types.MapNull(getFieldAttrElemType()), + SourceFilters: types.ListNull(types.StringType), + RuntimeFieldMap: types.MapNull(getRuntimeFieldMapElemType()), + FieldFormats: types.MapNull(getFieldFormatElemType()), + Namespaces: utils.ListValueFrom[string](ctx, nil, types.StringType, path.Root("data_view").AtName("namespaces"), &diags), + }, getDataViewAttrTypes(), path.Root("data_view"), &diags), + }, + response: kbapi.DataViewsDataViewResponseObject{ + DataView: &kbapi.DataViewsDataViewResponseObjectInner{ + Id: utils.Pointer("test-id"), + Title: utils.Pointer("test-title"), + FieldAttrs: &map[string]kbapi.DataViewsFieldattrs{ + "host.hostname": { + Count: utils.Pointer(5), + }, + "event.action": { + Count: utils.Pointer(12), + }, + }, + }, + }, + expectedModel: dataViewModel{ + ID: types.StringValue("default/test-id"), + SpaceID: types.StringValue("default"), + DataView: utils.ObjectValueFrom(ctx, &innerModel{ + ID: types.StringValue("test-id"), + Title: types.StringValue("test-title"), + FieldAttributes: utils.MapValueFrom(ctx, map[string]fieldAttrModel{ + "host.hostname": { + CustomLabel: types.StringNull(), + Count: types.Int64Value(5), + }, + "event.action": { + CustomLabel: types.StringNull(), + Count: types.Int64Value(12), + }, + }, getFieldAttrElemType(), path.Root("data_view").AtName("field_attrs"), &diags), + SourceFilters: types.ListNull(types.StringType), + RuntimeFieldMap: types.MapNull(getRuntimeFieldMapElemType()), + FieldFormats: types.MapNull(getFieldFormatElemType()), + Namespaces: utils.ListValueFrom[string](ctx, nil, types.StringType, path.Root("data_view").AtName("namespaces"), &diags), + }, getDataViewAttrTypes(), path.Root("data_view"), &diags), + }, + }, + { + // Test handling of server-side count updates with existing field_attrs + name: "server_side_count_updates_with_existing_attrs", + existingModel: dataViewModel{ + ID: types.StringValue("default/test-id"), + SpaceID: types.StringValue("default"), + DataView: utils.ObjectValueFrom(ctx, &innerModel{ + ID: types.StringValue("test-id"), + Title: types.StringValue("test-title"), + FieldAttributes: utils.MapValueFrom(ctx, map[string]fieldAttrModel{ + "host.hostname": { + CustomLabel: types.StringValue("Host Name"), + Count: types.Int64Null(), // Null count in state + }, + }, getFieldAttrElemType(), path.Root("data_view").AtName("field_attrs"), &diags), + SourceFilters: types.ListNull(types.StringType), + RuntimeFieldMap: types.MapNull(getRuntimeFieldMapElemType()), + FieldFormats: types.MapNull(getFieldFormatElemType()), + Namespaces: utils.ListValueFrom[string](ctx, nil, types.StringType, path.Root("data_view").AtName("namespaces"), &diags), + }, getDataViewAttrTypes(), path.Root("data_view"), &diags), + }, + response: kbapi.DataViewsDataViewResponseObject{ + DataView: &kbapi.DataViewsDataViewResponseObjectInner{ + Id: utils.Pointer("test-id"), + Title: utils.Pointer("test-title"), + FieldAttrs: &map[string]kbapi.DataViewsFieldattrs{ + "host.hostname": { + CustomLabel: utils.Pointer("Host Name"), + Count: utils.Pointer(15), // Server-side count + }, + }, + }, + }, + expectedModel: dataViewModel{ + ID: types.StringValue("default/test-id"), + SpaceID: types.StringValue("default"), + DataView: utils.ObjectValueFrom(ctx, &innerModel{ + ID: types.StringValue("test-id"), + Title: types.StringValue("test-title"), + FieldAttributes: utils.MapValueFrom(ctx, map[string]fieldAttrModel{ + "host.hostname": { + CustomLabel: types.StringValue("Host Name"), + Count: types.Int64Value(15), // Should accept server-side value + }, + }, getFieldAttrElemType(), path.Root("data_view").AtName("field_attrs"), &diags), + SourceFilters: types.ListNull(types.StringType), + RuntimeFieldMap: types.MapNull(getRuntimeFieldMapElemType()), + FieldFormats: types.MapNull(getFieldFormatElemType()), + Namespaces: utils.ListValueFrom[string](ctx, nil, types.StringType, path.Root("data_view").AtName("namespaces"), &diags), + }, getDataViewAttrTypes(), path.Root("data_view"), &diags), + }, + }, } require.Empty(t, diags) diff --git a/internal/kibana/data_view/schema.go b/internal/kibana/data_view/schema.go index 32b100f92..d78211770 100644 --- a/internal/kibana/data_view/schema.go +++ b/internal/kibana/data_view/schema.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" @@ -99,6 +100,10 @@ func getSchema() schema.Schema { "count": schema.Int64Attribute{ Description: "Popularity count for the field.", Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, }, }, },