Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions docs/resources/fleet_agent_policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
55 changes: 40 additions & 15 deletions internal/fleet/agent_policy/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
type features struct {
SupportsGlobalDataTags bool
SupportsSupportsAgentless bool
SupportsInactivityTimeout bool
}

type globalDataTagsItemModel struct {
Expand All @@ -25,21 +26,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 {
Expand Down Expand Up @@ -73,6 +75,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)
Expand Down Expand Up @@ -172,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(),
Expand All @@ -183,6 +196,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)
Expand Down Expand Up @@ -213,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(),
Expand All @@ -223,6 +247,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)
Expand Down
7 changes: 7 additions & 0 deletions internal/fleet/agent_policy/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
37 changes: 37 additions & 0 deletions internal/fleet/agent_policy/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,19 @@ func TestAccResourceAgentPolicy(t *testing.T) {
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"skip_destroy"},
},
{
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)),
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),
Expand Down Expand Up @@ -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" {
Expand Down
5 changes: 5 additions & 0 deletions internal/fleet/agent_policy/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
105 changes: 105 additions & 0 deletions internal/fleet/agent_policy/version_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading