From bdb81b48e42f4e6407c759e78c46618c3afe9c04 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Sep 2025 00:15:47 +0000 Subject: [PATCH 1/9] Initial plan From f6f4a9bf39835f5ea8f6f6d5256ecf3c9e4ee49b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Sep 2025 00:28:39 +0000 Subject: [PATCH 2/9] Add inactivity_timeout support to Fleet agent policy resource Co-authored-by: tobio <444668+tobio@users.noreply.github.com> --- internal/fleet/agent_policy/models.go | 34 ++++++++++-------- internal/fleet/agent_policy/resource_test.go | 37 ++++++++++++++++++++ internal/fleet/agent_policy/schema.go | 5 +++ 3 files changed, 61 insertions(+), 15 deletions(-) diff --git a/internal/fleet/agent_policy/models.go b/internal/fleet/agent_policy/models.go index 1fddf95cd..69b4b0d8d 100644 --- a/internal/fleet/agent_policy/models.go +++ b/internal/fleet/agent_policy/models.go @@ -25,21 +25,22 @@ type globalDataTagsItemModel struct { } type agentPolicyModel struct { - ID types.String `tfsdk:"id"` - PolicyID types.String `tfsdk:"policy_id"` - Name types.String `tfsdk:"name"` - Namespace types.String `tfsdk:"namespace"` - Description types.String `tfsdk:"description"` - DataOutputId types.String `tfsdk:"data_output_id"` - MonitoringOutputId types.String `tfsdk:"monitoring_output_id"` - FleetServerHostId types.String `tfsdk:"fleet_server_host_id"` - DownloadSourceId types.String `tfsdk:"download_source_id"` - MonitorLogs types.Bool `tfsdk:"monitor_logs"` - MonitorMetrics types.Bool `tfsdk:"monitor_metrics"` - SysMonitoring types.Bool `tfsdk:"sys_monitoring"` - SkipDestroy types.Bool `tfsdk:"skip_destroy"` - SupportsAgentless types.Bool `tfsdk:"supports_agentless"` - GlobalDataTags types.Map `tfsdk:"global_data_tags"` //> globalDataTagsModel + ID types.String `tfsdk:"id"` + PolicyID types.String `tfsdk:"policy_id"` + Name types.String `tfsdk:"name"` + Namespace types.String `tfsdk:"namespace"` + Description types.String `tfsdk:"description"` + DataOutputId types.String `tfsdk:"data_output_id"` + MonitoringOutputId types.String `tfsdk:"monitoring_output_id"` + FleetServerHostId types.String `tfsdk:"fleet_server_host_id"` + DownloadSourceId types.String `tfsdk:"download_source_id"` + MonitorLogs types.Bool `tfsdk:"monitor_logs"` + MonitorMetrics types.Bool `tfsdk:"monitor_metrics"` + SysMonitoring types.Bool `tfsdk:"sys_monitoring"` + SkipDestroy types.Bool `tfsdk:"skip_destroy"` + SupportsAgentless types.Bool `tfsdk:"supports_agentless"` + InactivityTimeout types.Float32 `tfsdk:"inactivity_timeout"` + GlobalDataTags types.Map `tfsdk:"global_data_tags"` //> globalDataTagsModel } func (model *agentPolicyModel) populateFromAPI(ctx context.Context, data *kbapi.AgentPolicy) diag.Diagnostics { @@ -73,6 +74,7 @@ func (model *agentPolicyModel) populateFromAPI(ctx context.Context, data *kbapi. model.Name = types.StringValue(data.Name) model.Namespace = types.StringValue(data.Namespace) model.SupportsAgentless = types.BoolPointerValue(data.SupportsAgentless) + model.InactivityTimeout = types.Float32PointerValue(data.InactivityTimeout) if utils.Deref(data.GlobalDataTags) != nil { diags := diag.Diagnostics{} var map0 = make(map[string]globalDataTagsItemModel) @@ -183,6 +185,7 @@ func (model *agentPolicyModel) toAPICreateModel(ctx context.Context, feat featur Name: model.Name.ValueString(), Namespace: model.Namespace.ValueString(), SupportsAgentless: model.SupportsAgentless.ValueBoolPointer(), + InactivityTimeout: model.InactivityTimeout.ValueFloat32Pointer(), } tags, diags := model.convertGlobalDataTags(ctx, feat) @@ -223,6 +226,7 @@ func (model *agentPolicyModel) toAPIUpdateModel(ctx context.Context, feat featur Name: model.Name.ValueString(), Namespace: model.Namespace.ValueString(), SupportsAgentless: model.SupportsAgentless.ValueBoolPointer(), + InactivityTimeout: model.InactivityTimeout.ValueFloat32Pointer(), } tags, diags := model.convertGlobalDataTags(ctx, feat) diff --git a/internal/fleet/agent_policy/resource_test.go b/internal/fleet/agent_policy/resource_test.go index 8f90217f4..e7801bcf1 100644 --- a/internal/fleet/agent_policy/resource_test.go +++ b/internal/fleet/agent_policy/resource_test.go @@ -143,6 +143,19 @@ func TestAccResourceAgentPolicy(t *testing.T) { ImportStateVerify: true, ImportStateVerifyIgnore: []string{"skip_destroy"}, }, + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionAgentPolicy), + Config: testAccResourceAgentPolicyCreateWithInactivityTimeout(policyName, false, 120), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "name", fmt.Sprintf("Policy %s", policyName)), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "namespace", "default"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "description", "Test Agent Policy with Inactivity Timeout"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "monitor_logs", "true"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "monitor_metrics", "false"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "skip_destroy", "false"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "inactivity_timeout", "120"), + ), + }, { SkipFunc: versionutils.CheckIfVersionIsUnsupported(agent_policy.MinVersionGlobalDataTags), Config: testAccResourceAgentPolicyCreateWithGlobalDataTags(policyNameGlobalDataTags, false), @@ -295,6 +308,30 @@ data "elasticstack_fleet_enrollment_tokens" "test_policy" { `, fmt.Sprintf("Policy %s", id), skipDestroy) } +func testAccResourceAgentPolicyCreateWithInactivityTimeout(id string, skipDestroy bool, inactivityTimeout float32) string { + return fmt.Sprintf(` +provider "elasticstack" { + elasticsearch {} + kibana {} +} + +resource "elasticstack_fleet_agent_policy" "test_policy" { + name = "%s" + namespace = "default" + description = "Test Agent Policy with Inactivity Timeout" + monitor_logs = true + monitor_metrics = false + skip_destroy = %t + inactivity_timeout = %g +} + +data "elasticstack_fleet_enrollment_tokens" "test_policy" { + policy_id = elasticstack_fleet_agent_policy.test_policy.policy_id +} + +`, fmt.Sprintf("Policy %s", id), skipDestroy, inactivityTimeout) +} + func testAccResourceAgentPolicyCreateWithBadGlobalDataTags(id string, skipDestroy bool) string { return fmt.Sprintf(` provider "elasticstack" { diff --git a/internal/fleet/agent_policy/schema.go b/internal/fleet/agent_policy/schema.go index f1c531a29..0e8d311da 100644 --- a/internal/fleet/agent_policy/schema.go +++ b/internal/fleet/agent_policy/schema.go @@ -97,6 +97,11 @@ func getSchema() schema.Schema { boolplanmodifier.RequiresReplace(), }, }, + "inactivity_timeout": schema.Float32Attribute{ + Description: "The inactivity timeout (in seconds) for the agent policy. If an agent does not report within this time period, it will be considered inactive.", + Computed: true, + Optional: true, + }, "global_data_tags": schema.MapNestedAttribute{ Description: "User-defined data tags to apply to all inputs. Values can be strings (string_value) or numbers (number_value) but not both. Example -- key1 = {string_value = value1}, key2 = {number_value = 42}", NestedObject: schema.NestedAttributeObject{ From 6cf5c3c1a7ea4915ddde48141721af241dd21600 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Sep 2025 00:32:21 +0000 Subject: [PATCH 3/9] Update changelog and generate documentation for inactivity_timeout feature Co-authored-by: tobio <444668+tobio@users.noreply.github.com> --- CHANGELOG.md | 1 + docs/resources/fleet_agent_policy.md | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ffbadeef..06b65bd11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Allow version changes without a destroy/create cycle with `elasticstack_fleet_integration` ([#1255](https://github.com/elastic/terraform-provider-elasticstack/pull/1255)). This fixes an issue where it was impossible to upgrade integrations which are used by an integration policy. - Add `namespace` attribute to `elasticstack_kibana_synthetics_monitor` resource to support setting data stream namespace independently from `space_id` ([#1247](https://github.com/elastic/terraform-provider-elasticstack/pull/1247)) - Migrate `elasticstack_elasticsearch_security_role_mapping` resource and data source to Terraform Plugin Framework ([#1279](https://github.com/elastic/terraform-provider-elasticstack/pull/1279)) +- Add support for `inactivity_timeout` in `elasticstack_fleet_agent_policy` ([#641](https://github.com/elastic/terraform-provider-elasticstack/issues/641)) ## [0.11.17] - 2025-07-21 diff --git a/docs/resources/fleet_agent_policy.md b/docs/resources/fleet_agent_policy.md index 9eb4d4ea7..eefe3f336 100644 --- a/docs/resources/fleet_agent_policy.md +++ b/docs/resources/fleet_agent_policy.md @@ -51,6 +51,7 @@ resource "elasticstack_fleet_agent_policy" "test_policy" { - `download_source_id` (String) The identifier for the Elastic Agent binary download server. - `fleet_server_host_id` (String) The identifier for the Fleet server host. - `global_data_tags` (Attributes Map) User-defined data tags to apply to all inputs. Values can be strings (string_value) or numbers (number_value) but not both. Example -- key1 = {string_value = value1}, key2 = {number_value = 42} (see [below for nested schema](#nestedatt--global_data_tags)) +- `inactivity_timeout` (Number) The inactivity timeout (in seconds) for the agent policy. If an agent does not report within this time period, it will be considered inactive. - `monitor_logs` (Boolean) Enable collection of agent logs. - `monitor_metrics` (Boolean) Enable collection of agent metrics. - `monitoring_output_id` (String) The identifier for monitoring output. From 1be987cfd568e6268bc49978b889143b35f612ad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Sep 2025 02:20:47 +0000 Subject: [PATCH 4/9] Add version check for inactivity_timeout field in Fleet agent policy - Added MinVersionInactivityTimeout constant for 8.7.0 - Added SupportsInactivityTimeout field to features struct - Updated buildFeatures to check for inactivity timeout support - Added version validation in toAPICreateModel and toAPIUpdateModel - Updated test to use proper version check for inactivity_timeout Co-authored-by: tobio <444668+tobio@users.noreply.github.com> --- internal/fleet/agent_policy/models.go | 21 ++++++++++++++++++++ internal/fleet/agent_policy/resource.go | 7 +++++++ internal/fleet/agent_policy/resource_test.go | 2 +- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/internal/fleet/agent_policy/models.go b/internal/fleet/agent_policy/models.go index 69b4b0d8d..520e576c0 100644 --- a/internal/fleet/agent_policy/models.go +++ b/internal/fleet/agent_policy/models.go @@ -17,6 +17,7 @@ import ( type features struct { SupportsGlobalDataTags bool SupportsSupportsAgentless bool + SupportsInactivityTimeout bool } type globalDataTagsItemModel struct { @@ -174,6 +175,16 @@ func (model *agentPolicyModel) toAPICreateModel(ctx context.Context, feat featur } } + if utils.IsKnown(model.InactivityTimeout) && !feat.SupportsInactivityTimeout { + return kbapi.PostFleetAgentPoliciesJSONRequestBody{}, diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("inactivity_timeout"), + "Unsupported Elasticsearch version", + fmt.Sprintf("Inactivity timeout is only supported in Elastic Stack %s and above", MinVersionInactivityTimeout), + ), + } + } + body := kbapi.PostFleetAgentPoliciesJSONRequestBody{ DataOutputId: model.DataOutputId.ValueStringPointer(), Description: model.Description.ValueStringPointer(), @@ -216,6 +227,16 @@ func (model *agentPolicyModel) toAPIUpdateModel(ctx context.Context, feat featur } } + if utils.IsKnown(model.InactivityTimeout) && !feat.SupportsInactivityTimeout { + return kbapi.PutFleetAgentPoliciesAgentpolicyidJSONRequestBody{}, diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("inactivity_timeout"), + "Unsupported Elasticsearch version", + fmt.Sprintf("Inactivity timeout is only supported in Elastic Stack %s and above", MinVersionInactivityTimeout), + ), + } + } + body := kbapi.PutFleetAgentPoliciesAgentpolicyidJSONRequestBody{ DataOutputId: model.DataOutputId.ValueStringPointer(), Description: model.Description.ValueStringPointer(), diff --git a/internal/fleet/agent_policy/resource.go b/internal/fleet/agent_policy/resource.go index 34e4a3cf4..fbeda8d50 100644 --- a/internal/fleet/agent_policy/resource.go +++ b/internal/fleet/agent_policy/resource.go @@ -21,6 +21,7 @@ var ( var ( MinVersionGlobalDataTags = version.Must(version.NewVersion("8.15.0")) MinSupportsAgentlessVersion = version.Must(version.NewVersion("8.15.0")) + MinVersionInactivityTimeout = version.Must(version.NewVersion("8.7.0")) ) // NewResource is a helper function to simplify the provider implementation. @@ -57,8 +58,14 @@ func (r *agentPolicyResource) buildFeatures(ctx context.Context) (features, diag return features{}, utils.FrameworkDiagsFromSDK(diags) } + supportsInactivityTimeout, diags := r.client.EnforceMinVersion(ctx, MinVersionInactivityTimeout) + if diags.HasError() { + return features{}, utils.FrameworkDiagsFromSDK(diags) + } + return features{ SupportsGlobalDataTags: supportsGDT, SupportsSupportsAgentless: supportsSupportsAgentless, + SupportsInactivityTimeout: supportsInactivityTimeout, }, nil } diff --git a/internal/fleet/agent_policy/resource_test.go b/internal/fleet/agent_policy/resource_test.go index e7801bcf1..c1eb62a0f 100644 --- a/internal/fleet/agent_policy/resource_test.go +++ b/internal/fleet/agent_policy/resource_test.go @@ -144,7 +144,7 @@ func TestAccResourceAgentPolicy(t *testing.T) { ImportStateVerifyIgnore: []string{"skip_destroy"}, }, { - SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionAgentPolicy), + SkipFunc: versionutils.CheckIfVersionIsUnsupported(agent_policy.MinVersionInactivityTimeout), Config: testAccResourceAgentPolicyCreateWithInactivityTimeout(policyName, false, 120), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "name", fmt.Sprintf("Policy %s", policyName)), From a39c06247ed0805874ad2c2dac0ba19c6dbe712f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Sep 2025 02:24:42 +0000 Subject: [PATCH 5/9] Add comprehensive tests for inactivity_timeout version validation - Added unit tests to verify MinVersionInactivityTimeout constant - Added comprehensive validation tests for version checking logic - Verified version validation works for both create and update operations - Ensured null/unset inactivity_timeout values don't trigger validation errors Co-authored-by: tobio <444668+tobio@users.noreply.github.com> --- internal/fleet/agent_policy/version_test.go | 105 ++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 internal/fleet/agent_policy/version_test.go diff --git a/internal/fleet/agent_policy/version_test.go b/internal/fleet/agent_policy/version_test.go new file mode 100644 index 000000000..e6067df31 --- /dev/null +++ b/internal/fleet/agent_policy/version_test.go @@ -0,0 +1,105 @@ +package agent_policy + +import ( + "context" + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-framework/types" + "testing" +) + +func TestMinVersionInactivityTimeout(t *testing.T) { + // Test that the MinVersionInactivityTimeout constant is set correctly + expected := "8.7.0" + actual := MinVersionInactivityTimeout.String() + if actual != expected { + t.Errorf("Expected MinVersionInactivityTimeout to be '%s', got '%s'", expected, actual) + } + + // Test version comparison - should be greater than 8.6.0 + olderVersion := version.Must(version.NewVersion("8.6.0")) + if MinVersionInactivityTimeout.LessThan(olderVersion) { + t.Errorf("MinVersionInactivityTimeout (%s) should be greater than %s", MinVersionInactivityTimeout.String(), olderVersion.String()) + } + + // Test version comparison - should be less than 8.8.0 + newerVersion := version.Must(version.NewVersion("8.8.0")) + if MinVersionInactivityTimeout.GreaterThan(newerVersion) { + t.Errorf("MinVersionInactivityTimeout (%s) should be less than %s", MinVersionInactivityTimeout.String(), newerVersion.String()) + } +} + +func TestInactivityTimeoutVersionValidation(t *testing.T) { + ctx := context.Background() + + // Test case where inactivity_timeout is not supported (older version) + model := &agentPolicyModel{ + Name: types.StringValue("test"), + Namespace: types.StringValue("default"), + InactivityTimeout: types.Float32Value(120.0), + } + + // Create features with inactivity timeout NOT supported + feat := features{ + SupportsInactivityTimeout: false, + } + + // Test toAPICreateModel - should return error when inactivity_timeout is used but not supported + _, diags := model.toAPICreateModel(ctx, feat) + if !diags.HasError() { + t.Error("Expected error when using inactivity_timeout on unsupported version, but got none") + } + + // Check that the error message contains the expected text + found := false + for _, diag := range diags { + if diag.Summary() == "Unsupported Elasticsearch version" { + found = true + break + } + } + if !found { + t.Error("Expected 'Unsupported Elasticsearch version' error, but didn't find it") + } + + // Test toAPIUpdateModel - should return error when inactivity_timeout is used but not supported + _, diags = model.toAPIUpdateModel(ctx, feat) + if !diags.HasError() { + t.Error("Expected error when using inactivity_timeout on unsupported version in update, but got none") + } + + // Test case where inactivity_timeout IS supported (newer version) + featSupported := features{ + SupportsInactivityTimeout: true, + } + + // Test toAPICreateModel - should NOT return error when inactivity_timeout is supported + _, diags = model.toAPICreateModel(ctx, featSupported) + if diags.HasError() { + t.Errorf("Did not expect error when using inactivity_timeout on supported version: %v", diags) + } + + // Test toAPIUpdateModel - should NOT return error when inactivity_timeout is supported + _, diags = model.toAPIUpdateModel(ctx, featSupported) + if diags.HasError() { + t.Errorf("Did not expect error when using inactivity_timeout on supported version in update: %v", diags) + } + + // Test case where inactivity_timeout is not set (should not cause validation errors) + modelWithoutTimeout := &agentPolicyModel{ + Name: types.StringValue("test"), + Namespace: types.StringValue("default"), + // InactivityTimeout is not set (null/unknown) + } + + // Test toAPICreateModel - should NOT return error when inactivity_timeout is not set, even on unsupported version + _, diags = modelWithoutTimeout.toAPICreateModel(ctx, feat) + if diags.HasError() { + t.Errorf("Did not expect error when inactivity_timeout is not set: %v", diags) + } + + // Test toAPIUpdateModel - should NOT return error when inactivity_timeout is not set, even on unsupported version + _, diags = modelWithoutTimeout.toAPIUpdateModel(ctx, feat) + if diags.HasError() { + t.Errorf("Did not expect error when inactivity_timeout is not set in update: %v", diags) + } +} From 538551478ad1eb8ee1d598b321d91db2158a17da Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Sep 2025 07:41:23 +0000 Subject: [PATCH 6/9] Fix inactivity_timeout and supports_agentless field handling in Fleet agent policy Co-authored-by: tobio <444668+tobio@users.noreply.github.com> --- internal/fleet/agent_policy/models.go | 96 +++++++++++++++------------ 1 file changed, 52 insertions(+), 44 deletions(-) diff --git a/internal/fleet/agent_policy/models.go b/internal/fleet/agent_policy/models.go index 520e576c0..8fcd36c07 100644 --- a/internal/fleet/agent_policy/models.go +++ b/internal/fleet/agent_policy/models.go @@ -165,26 +165,6 @@ func (model *agentPolicyModel) toAPICreateModel(ctx context.Context, feat featur monitoring = append(monitoring, kbapi.PostFleetAgentPoliciesJSONBodyMonitoringEnabledMetrics) } - if utils.IsKnown(model.SupportsAgentless) && !feat.SupportsSupportsAgentless { - return kbapi.PostFleetAgentPoliciesJSONRequestBody{}, diag.Diagnostics{ - diag.NewAttributeErrorDiagnostic( - path.Root("supports_agentless"), - "Unsupported Elasticsearch version", - fmt.Sprintf("Supports agentless is only supported in Elastic Stack %s and above", MinSupportsAgentlessVersion), - ), - } - } - - if utils.IsKnown(model.InactivityTimeout) && !feat.SupportsInactivityTimeout { - return kbapi.PostFleetAgentPoliciesJSONRequestBody{}, diag.Diagnostics{ - diag.NewAttributeErrorDiagnostic( - path.Root("inactivity_timeout"), - "Unsupported Elasticsearch version", - fmt.Sprintf("Inactivity timeout is only supported in Elastic Stack %s and above", MinVersionInactivityTimeout), - ), - } - } - body := kbapi.PostFleetAgentPoliciesJSONRequestBody{ DataOutputId: model.DataOutputId.ValueStringPointer(), Description: model.Description.ValueStringPointer(), @@ -195,8 +175,32 @@ func (model *agentPolicyModel) toAPICreateModel(ctx context.Context, feat featur MonitoringOutputId: model.MonitoringOutputId.ValueStringPointer(), Name: model.Name.ValueString(), Namespace: model.Namespace.ValueString(), - SupportsAgentless: model.SupportsAgentless.ValueBoolPointer(), - InactivityTimeout: model.InactivityTimeout.ValueFloat32Pointer(), + } + + if utils.IsKnown(model.SupportsAgentless) { + if !feat.SupportsSupportsAgentless { + return kbapi.PostFleetAgentPoliciesJSONRequestBody{}, diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("supports_agentless"), + "Unsupported Elasticsearch version", + fmt.Sprintf("Supports agentless is only supported in Elastic Stack %s and above", MinSupportsAgentlessVersion), + ), + } + } + body.SupportsAgentless = model.SupportsAgentless.ValueBoolPointer() + } + + if utils.IsKnown(model.InactivityTimeout) { + if !feat.SupportsInactivityTimeout { + return kbapi.PostFleetAgentPoliciesJSONRequestBody{}, diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("inactivity_timeout"), + "Unsupported Elasticsearch version", + fmt.Sprintf("Inactivity timeout is only supported in Elastic Stack %s and above", MinVersionInactivityTimeout), + ), + } + } + body.InactivityTimeout = model.InactivityTimeout.ValueFloat32Pointer() } tags, diags := model.convertGlobalDataTags(ctx, feat) @@ -217,26 +221,6 @@ func (model *agentPolicyModel) toAPIUpdateModel(ctx context.Context, feat featur monitoring = append(monitoring, kbapi.Metrics) } - if utils.IsKnown(model.SupportsAgentless) && !feat.SupportsSupportsAgentless { - return kbapi.PutFleetAgentPoliciesAgentpolicyidJSONRequestBody{}, diag.Diagnostics{ - diag.NewAttributeErrorDiagnostic( - path.Root("supports_agentless"), - "Unsupported Elasticsearch version", - fmt.Sprintf("Supports agentless is only supported in Elastic Stack %s and above", MinSupportsAgentlessVersion), - ), - } - } - - if utils.IsKnown(model.InactivityTimeout) && !feat.SupportsInactivityTimeout { - return kbapi.PutFleetAgentPoliciesAgentpolicyidJSONRequestBody{}, diag.Diagnostics{ - diag.NewAttributeErrorDiagnostic( - path.Root("inactivity_timeout"), - "Unsupported Elasticsearch version", - fmt.Sprintf("Inactivity timeout is only supported in Elastic Stack %s and above", MinVersionInactivityTimeout), - ), - } - } - body := kbapi.PutFleetAgentPoliciesAgentpolicyidJSONRequestBody{ DataOutputId: model.DataOutputId.ValueStringPointer(), Description: model.Description.ValueStringPointer(), @@ -246,8 +230,32 @@ func (model *agentPolicyModel) toAPIUpdateModel(ctx context.Context, feat featur MonitoringOutputId: model.MonitoringOutputId.ValueStringPointer(), Name: model.Name.ValueString(), Namespace: model.Namespace.ValueString(), - SupportsAgentless: model.SupportsAgentless.ValueBoolPointer(), - InactivityTimeout: model.InactivityTimeout.ValueFloat32Pointer(), + } + + if utils.IsKnown(model.SupportsAgentless) { + if !feat.SupportsSupportsAgentless { + return kbapi.PutFleetAgentPoliciesAgentpolicyidJSONRequestBody{}, diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("supports_agentless"), + "Unsupported Elasticsearch version", + fmt.Sprintf("Supports agentless is only supported in Elastic Stack %s and above", MinSupportsAgentlessVersion), + ), + } + } + body.SupportsAgentless = model.SupportsAgentless.ValueBoolPointer() + } + + if utils.IsKnown(model.InactivityTimeout) { + if !feat.SupportsInactivityTimeout { + return kbapi.PutFleetAgentPoliciesAgentpolicyidJSONRequestBody{}, diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("inactivity_timeout"), + "Unsupported Elasticsearch version", + fmt.Sprintf("Inactivity timeout is only supported in Elastic Stack %s and above", MinVersionInactivityTimeout), + ), + } + } + body.InactivityTimeout = model.InactivityTimeout.ValueFloat32Pointer() } tags, diags := model.convertGlobalDataTags(ctx, feat) From 402095245b2c912ab1415ee1bdfef461fce7e4d0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Sep 2025 11:09:21 +0000 Subject: [PATCH 7/9] Convert inactivity_timeout to string duration type with seconds conversion Co-authored-by: tobio <444668+tobio@users.noreply.github.com> --- internal/fleet/agent_policy/models.go | 55 +++++++++++++------- internal/fleet/agent_policy/resource_test.go | 8 +-- internal/fleet/agent_policy/schema.go | 6 ++- internal/fleet/agent_policy/version_test.go | 3 +- 4 files changed, 46 insertions(+), 26 deletions(-) diff --git a/internal/fleet/agent_policy/models.go b/internal/fleet/agent_policy/models.go index 8fcd36c07..72d9e6d5d 100644 --- a/internal/fleet/agent_policy/models.go +++ b/internal/fleet/agent_policy/models.go @@ -7,6 +7,7 @@ import ( "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/elastic/terraform-provider-elasticstack/internal/utils/customtypes" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -26,22 +27,22 @@ type globalDataTagsItemModel struct { } type agentPolicyModel struct { - ID types.String `tfsdk:"id"` - PolicyID types.String `tfsdk:"policy_id"` - Name types.String `tfsdk:"name"` - Namespace types.String `tfsdk:"namespace"` - Description types.String `tfsdk:"description"` - DataOutputId types.String `tfsdk:"data_output_id"` - MonitoringOutputId types.String `tfsdk:"monitoring_output_id"` - FleetServerHostId types.String `tfsdk:"fleet_server_host_id"` - DownloadSourceId types.String `tfsdk:"download_source_id"` - MonitorLogs types.Bool `tfsdk:"monitor_logs"` - MonitorMetrics types.Bool `tfsdk:"monitor_metrics"` - SysMonitoring types.Bool `tfsdk:"sys_monitoring"` - SkipDestroy types.Bool `tfsdk:"skip_destroy"` - SupportsAgentless types.Bool `tfsdk:"supports_agentless"` - InactivityTimeout types.Float32 `tfsdk:"inactivity_timeout"` - GlobalDataTags types.Map `tfsdk:"global_data_tags"` //> globalDataTagsModel + ID types.String `tfsdk:"id"` + PolicyID types.String `tfsdk:"policy_id"` + Name types.String `tfsdk:"name"` + Namespace types.String `tfsdk:"namespace"` + Description types.String `tfsdk:"description"` + DataOutputId types.String `tfsdk:"data_output_id"` + MonitoringOutputId types.String `tfsdk:"monitoring_output_id"` + FleetServerHostId types.String `tfsdk:"fleet_server_host_id"` + DownloadSourceId types.String `tfsdk:"download_source_id"` + MonitorLogs types.Bool `tfsdk:"monitor_logs"` + MonitorMetrics types.Bool `tfsdk:"monitor_metrics"` + SysMonitoring types.Bool `tfsdk:"sys_monitoring"` + SkipDestroy types.Bool `tfsdk:"skip_destroy"` + SupportsAgentless types.Bool `tfsdk:"supports_agentless"` + InactivityTimeout customtypes.Duration `tfsdk:"inactivity_timeout"` + GlobalDataTags types.Map `tfsdk:"global_data_tags"` //> globalDataTagsModel } func (model *agentPolicyModel) populateFromAPI(ctx context.Context, data *kbapi.AgentPolicy) diag.Diagnostics { @@ -75,7 +76,13 @@ func (model *agentPolicyModel) populateFromAPI(ctx context.Context, data *kbapi. model.Name = types.StringValue(data.Name) model.Namespace = types.StringValue(data.Namespace) model.SupportsAgentless = types.BoolPointerValue(data.SupportsAgentless) - model.InactivityTimeout = types.Float32PointerValue(data.InactivityTimeout) + if data.InactivityTimeout != nil { + // Convert seconds to duration string + durationStr := fmt.Sprintf("%.0fs", *data.InactivityTimeout) + model.InactivityTimeout = customtypes.NewDurationValue(durationStr) + } else { + model.InactivityTimeout = customtypes.NewDurationNull() + } if utils.Deref(data.GlobalDataTags) != nil { diags := diag.Diagnostics{} var map0 = make(map[string]globalDataTagsItemModel) @@ -200,7 +207,12 @@ func (model *agentPolicyModel) toAPICreateModel(ctx context.Context, feat featur ), } } - body.InactivityTimeout = model.InactivityTimeout.ValueFloat32Pointer() + duration, diags := model.InactivityTimeout.Parse() + if diags.HasError() { + return kbapi.PostFleetAgentPoliciesJSONRequestBody{}, diags + } + seconds := float32(duration.Seconds()) + body.InactivityTimeout = &seconds } tags, diags := model.convertGlobalDataTags(ctx, feat) @@ -255,7 +267,12 @@ func (model *agentPolicyModel) toAPIUpdateModel(ctx context.Context, feat featur ), } } - body.InactivityTimeout = model.InactivityTimeout.ValueFloat32Pointer() + duration, diags := model.InactivityTimeout.Parse() + if diags.HasError() { + return kbapi.PutFleetAgentPoliciesAgentpolicyidJSONRequestBody{}, diags + } + seconds := float32(duration.Seconds()) + body.InactivityTimeout = &seconds } tags, diags := model.convertGlobalDataTags(ctx, feat) diff --git a/internal/fleet/agent_policy/resource_test.go b/internal/fleet/agent_policy/resource_test.go index c1eb62a0f..2f151c610 100644 --- a/internal/fleet/agent_policy/resource_test.go +++ b/internal/fleet/agent_policy/resource_test.go @@ -145,7 +145,7 @@ func TestAccResourceAgentPolicy(t *testing.T) { }, { SkipFunc: versionutils.CheckIfVersionIsUnsupported(agent_policy.MinVersionInactivityTimeout), - Config: testAccResourceAgentPolicyCreateWithInactivityTimeout(policyName, false, 120), + Config: testAccResourceAgentPolicyCreateWithInactivityTimeout(policyName, false, "2m"), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "name", fmt.Sprintf("Policy %s", policyName)), resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "namespace", "default"), @@ -153,7 +153,7 @@ func TestAccResourceAgentPolicy(t *testing.T) { resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "monitor_logs", "true"), resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "monitor_metrics", "false"), resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "skip_destroy", "false"), - resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "inactivity_timeout", "120"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "inactivity_timeout", "2m"), ), }, { @@ -308,7 +308,7 @@ data "elasticstack_fleet_enrollment_tokens" "test_policy" { `, fmt.Sprintf("Policy %s", id), skipDestroy) } -func testAccResourceAgentPolicyCreateWithInactivityTimeout(id string, skipDestroy bool, inactivityTimeout float32) string { +func testAccResourceAgentPolicyCreateWithInactivityTimeout(id string, skipDestroy bool, inactivityTimeout string) string { return fmt.Sprintf(` provider "elasticstack" { elasticsearch {} @@ -322,7 +322,7 @@ resource "elasticstack_fleet_agent_policy" "test_policy" { monitor_logs = true monitor_metrics = false skip_destroy = %t - inactivity_timeout = %g + inactivity_timeout = "%s" } data "elasticstack_fleet_enrollment_tokens" "test_policy" { diff --git a/internal/fleet/agent_policy/schema.go b/internal/fleet/agent_policy/schema.go index 0e8d311da..be49763ef 100644 --- a/internal/fleet/agent_policy/schema.go +++ b/internal/fleet/agent_policy/schema.go @@ -3,6 +3,7 @@ package agent_policy import ( "context" + "github.com/elastic/terraform-provider-elasticstack/internal/utils/customtypes" "github.com/hashicorp/terraform-plugin-framework-validators/float32validator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -97,10 +98,11 @@ func getSchema() schema.Schema { boolplanmodifier.RequiresReplace(), }, }, - "inactivity_timeout": schema.Float32Attribute{ - Description: "The inactivity timeout (in seconds) for the agent policy. If an agent does not report within this time period, it will be considered inactive.", + "inactivity_timeout": schema.StringAttribute{ + Description: "The inactivity timeout for the agent policy. If an agent does not report within this time period, it will be considered inactive. Supports duration strings (e.g., '30s', '2m', '1h').", Computed: true, Optional: true, + CustomType: customtypes.DurationType{}, }, "global_data_tags": schema.MapNestedAttribute{ Description: "User-defined data tags to apply to all inputs. Values can be strings (string_value) or numbers (number_value) but not both. Example -- key1 = {string_value = value1}, key2 = {number_value = 42}", diff --git a/internal/fleet/agent_policy/version_test.go b/internal/fleet/agent_policy/version_test.go index e6067df31..8cdc0d133 100644 --- a/internal/fleet/agent_policy/version_test.go +++ b/internal/fleet/agent_policy/version_test.go @@ -2,6 +2,7 @@ package agent_policy import ( "context" + "github.com/elastic/terraform-provider-elasticstack/internal/utils/customtypes" "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-framework/types" "testing" @@ -35,7 +36,7 @@ func TestInactivityTimeoutVersionValidation(t *testing.T) { model := &agentPolicyModel{ Name: types.StringValue("test"), Namespace: types.StringValue("default"), - InactivityTimeout: types.Float32Value(120.0), + InactivityTimeout: customtypes.NewDurationValue("2m"), } // Create features with inactivity timeout NOT supported From d448c546d79fad05150df507938017248d305233 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Sep 2025 11:10:49 +0000 Subject: [PATCH 8/9] Update documentation for inactivity_timeout duration string support Co-authored-by: tobio <444668+tobio@users.noreply.github.com> --- docs/resources/fleet_agent_policy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/resources/fleet_agent_policy.md b/docs/resources/fleet_agent_policy.md index eefe3f336..d8252b5b2 100644 --- a/docs/resources/fleet_agent_policy.md +++ b/docs/resources/fleet_agent_policy.md @@ -51,7 +51,7 @@ resource "elasticstack_fleet_agent_policy" "test_policy" { - `download_source_id` (String) The identifier for the Elastic Agent binary download server. - `fleet_server_host_id` (String) The identifier for the Fleet server host. - `global_data_tags` (Attributes Map) User-defined data tags to apply to all inputs. Values can be strings (string_value) or numbers (number_value) but not both. Example -- key1 = {string_value = value1}, key2 = {number_value = 42} (see [below for nested schema](#nestedatt--global_data_tags)) -- `inactivity_timeout` (Number) The inactivity timeout (in seconds) for the agent policy. If an agent does not report within this time period, it will be considered inactive. +- `inactivity_timeout` (String) The inactivity timeout for the agent policy. If an agent does not report within this time period, it will be considered inactive. Supports duration strings (e.g., '30s', '2m', '1h'). - `monitor_logs` (Boolean) Enable collection of agent logs. - `monitor_metrics` (Boolean) Enable collection of agent metrics. - `monitoring_output_id` (String) The identifier for monitoring output. From cd13249fc37824401c01a36f50d6b5a52fc6691a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Sep 2025 11:27:34 +0000 Subject: [PATCH 9/9] Apply code review suggestions: improve duration conversion and simplify test helpers Co-authored-by: tobio <444668+tobio@users.noreply.github.com> --- internal/fleet/agent_policy/models.go | 5 +++-- internal/fleet/agent_policy/resource_test.go | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/internal/fleet/agent_policy/models.go b/internal/fleet/agent_policy/models.go index 72d9e6d5d..a87c3d092 100644 --- a/internal/fleet/agent_policy/models.go +++ b/internal/fleet/agent_policy/models.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "slices" + "time" "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" "github.com/elastic/terraform-provider-elasticstack/internal/utils" @@ -78,8 +79,8 @@ func (model *agentPolicyModel) populateFromAPI(ctx context.Context, data *kbapi. model.SupportsAgentless = types.BoolPointerValue(data.SupportsAgentless) if data.InactivityTimeout != nil { // Convert seconds to duration string - durationStr := fmt.Sprintf("%.0fs", *data.InactivityTimeout) - model.InactivityTimeout = customtypes.NewDurationValue(durationStr) + d := time.Duration(*data.InactivityTimeout * float32(time.Second)).Truncate(time.Second) + model.InactivityTimeout = customtypes.NewDurationValue(d.String()) } else { model.InactivityTimeout = customtypes.NewDurationNull() } diff --git a/internal/fleet/agent_policy/resource_test.go b/internal/fleet/agent_policy/resource_test.go index 2f151c610..acd184210 100644 --- a/internal/fleet/agent_policy/resource_test.go +++ b/internal/fleet/agent_policy/resource_test.go @@ -145,7 +145,7 @@ func TestAccResourceAgentPolicy(t *testing.T) { }, { SkipFunc: versionutils.CheckIfVersionIsUnsupported(agent_policy.MinVersionInactivityTimeout), - Config: testAccResourceAgentPolicyCreateWithInactivityTimeout(policyName, false, "2m"), + Config: testAccResourceAgentPolicyCreateWithInactivityTimeout(policyName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "name", fmt.Sprintf("Policy %s", policyName)), resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "namespace", "default"), @@ -308,7 +308,7 @@ data "elasticstack_fleet_enrollment_tokens" "test_policy" { `, fmt.Sprintf("Policy %s", id), skipDestroy) } -func testAccResourceAgentPolicyCreateWithInactivityTimeout(id string, skipDestroy bool, inactivityTimeout string) string { +func testAccResourceAgentPolicyCreateWithInactivityTimeout(id string) string { return fmt.Sprintf(` provider "elasticstack" { elasticsearch {} @@ -321,15 +321,15 @@ resource "elasticstack_fleet_agent_policy" "test_policy" { description = "Test Agent Policy with Inactivity Timeout" monitor_logs = true monitor_metrics = false - skip_destroy = %t - inactivity_timeout = "%s" + skip_destroy = false + inactivity_timeout = "2m" } data "elasticstack_fleet_enrollment_tokens" "test_policy" { policy_id = elasticstack_fleet_agent_policy.test_policy.policy_id } -`, fmt.Sprintf("Policy %s", id), skipDestroy, inactivityTimeout) +`, fmt.Sprintf("Policy %s", id)) } func testAccResourceAgentPolicyCreateWithBadGlobalDataTags(id string, skipDestroy bool) string {