Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ alias = [

### Changes

- Add `host_name_format` to `elasticstack_fleet_agent_policy` to configure host name format (hostname or FQDN) ([#1312](https://github.com/elastic/terraform-provider-elasticstack/pull/1312))
- Create `elasticstack_kibana_prebuilt_rule` resource ([#1296](https://github.com/elastic/terraform-provider-elasticstack/pull/1296))
- Add `required_versions` to `elasticstack_fleet_agent_policy` ([#1436](https://github.com/elastic/terraform-provider-elasticstack/pull/1436))
- Migrate `elasticstack_elasticsearch_security_role` resource to Terraform Plugin Framework ([#1330](https://github.com/elastic/terraform-provider-elasticstack/pull/1330))
Expand Down
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ KIBANA_API_KEY_NAME ?= kibana-api-key
FLEET_NAME ?= terraform-elasticstack-fleet
FLEET_ENDPOINT ?= https://$(FLEET_NAME):8220

RERUN_FAILS ?= 3

export GOBIN = $(shell pwd)/bin


Expand All @@ -53,7 +55,7 @@ testacc-vs-docker:

.PHONY: testacc
testacc: ## Run acceptance tests
TF_ACC=1 go tool gotestsum --format testname --rerun-fails=3 --packages="-v ./..." -- -count $(ACCTEST_COUNT) -parallel $(ACCTEST_PARALLELISM) $(TESTARGS) -timeout $(ACCTEST_TIMEOUT)
TF_ACC=1 go tool gotestsum --format testname --rerun-fails=$(RERUN_FAILS) --packages="-v ./..." -- -count $(ACCTEST_COUNT) -parallel $(ACCTEST_PARALLELISM) $(TESTARGS) -timeout $(ACCTEST_TIMEOUT)

.PHONY: test
test: ## Run unit tests
Expand Down
16 changes: 9 additions & 7 deletions docs/resources/fleet_agent_policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@ provider "elasticstack" {
}

resource "elasticstack_fleet_agent_policy" "test_policy" {
name = "Test Policy"
namespace = "default"
description = "Test Agent Policy"
sys_monitoring = true
monitor_logs = true
monitor_metrics = true
space_ids = ["default"]
name = "Test Policy"
namespace = "default"
description = "Test Agent Policy"
sys_monitoring = true
monitor_logs = true
monitor_metrics = true
space_ids = ["default"]
host_name_format = "hostname" # or "fqdn" for fully qualified domain names

global_data_tags = {
first_tag = {
Expand Down Expand Up @@ -52,6 +53,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))
- `host_name_format` (String) Determines the format of the host.name field in events. Can be 'hostname' (short hostname, e.g., 'myhost') or 'fqdn' (fully qualified domain name, e.g., 'myhost.example.com'). Defaults to 'hostname'.
- `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.
Expand Down
15 changes: 8 additions & 7 deletions examples/resources/elasticstack_fleet_agent_policy/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ provider "elasticstack" {
}

resource "elasticstack_fleet_agent_policy" "test_policy" {
name = "Test Policy"
namespace = "default"
description = "Test Agent Policy"
sys_monitoring = true
monitor_logs = true
monitor_metrics = true
space_ids = ["default"]
name = "Test Policy"
namespace = "default"
description = "Test Agent Policy"
sys_monitoring = true
monitor_logs = true
monitor_metrics = true
space_ids = ["default"]
host_name_format = "hostname" # or "fqdn" for fully qualified domain names

global_data_tags = {
first_tag = {
Expand Down
56 changes: 56 additions & 0 deletions internal/fleet/agent_policy/acc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,62 @@ func checkResourceAgentPolicySkipDestroy(s *terraform.State) error {
return nil
}

func TestAccResourceAgentPolicyWithHostNameFormat(t *testing.T) {
policyName := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlphaNum)

resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },
CheckDestroy: checkResourceAgentPolicyDestroy,
Steps: []resource.TestStep{
{
// Step 1: Create with host_name_format = "fqdn"
ProtoV6ProviderFactories: acctest.Providers,
SkipFunc: versionutils.CheckIfVersionIsUnsupported(agent_policy.MinVersionAgentFeatures),
ConfigDirectory: acctest.NamedTestCaseDirectory("create_with_fqdn"),
ConfigVariables: config.Variables{
"policy_name": config.StringVariable(fmt.Sprintf("Policy %s", 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"),
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "description", "Test Agent Policy with FQDN host name format"),
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "host_name_format", "fqdn"),
),
},
{
// Step 2: Remove host_name_format from config - should use default "hostname"
ProtoV6ProviderFactories: acctest.Providers,
SkipFunc: versionutils.CheckIfVersionIsUnsupported(agent_policy.MinVersionAgentFeatures),
ConfigDirectory: acctest.NamedTestCaseDirectory("remove_host_name_format"),
ConfigVariables: config.Variables{
"policy_name": config.StringVariable(fmt.Sprintf("Policy %s", 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"),
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "description", "Test Agent Policy without host_name_format"),
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "host_name_format", "hostname"),
),
},
{
// Step 3: Explicitly set host_name_format = "hostname"
ProtoV6ProviderFactories: acctest.Providers,
SkipFunc: versionutils.CheckIfVersionIsUnsupported(agent_policy.MinVersionAgentFeatures),
ConfigDirectory: acctest.NamedTestCaseDirectory("update_to_hostname"),
ConfigVariables: config.Variables{
"policy_name": config.StringVariable(fmt.Sprintf("Policy %s", 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"),
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "description", "Test Agent Policy with hostname format"),
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "host_name_format", "hostname"),
),
},
},
})
}

func TestAccResourceAgentPolicyWithRequiredVersions(t *testing.T) {
policyName := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlphaNum)

Expand Down
124 changes: 123 additions & 1 deletion internal/fleet/agent_policy/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,29 @@ import (
"github.com/hashicorp/terraform-plugin-framework/types"
)

const (
// HostNameFormatHostname represents the short hostname format (e.g., "myhost")
HostNameFormatHostname = "hostname"
// HostNameFormatFQDN represents the fully qualified domain name format (e.g., "myhost.example.com")
HostNameFormatFQDN = "fqdn"
// agentFeatureFQDN is the name of the agent feature that enables FQDN host name format
agentFeatureFQDN = "fqdn"
)

// apiAgentFeature is the type expected by the generated API for agent features
type apiAgentFeature = struct {
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type alias apiAgentFeature should be a named type rather than a type alias. Using type apiAgentFeature = struct creates an alias which may cause confusion when comparing types. Change to type apiAgentFeature struct to define a proper named type.

Suggested change
type apiAgentFeature = struct {
type apiAgentFeature struct {

Copilot uses AI. Check for mistakes.
Enabled bool `json:"enabled"`
Name string `json:"name"`
}

type features struct {
SupportsGlobalDataTags bool
SupportsSupportsAgentless bool
SupportsInactivityTimeout bool
SupportsUnenrollmentTimeout bool
SupportsSpaceIds bool
SupportsRequiredVersions bool
SupportsAgentFeatures bool
}

type globalDataTagsItemModel struct {
Expand All @@ -45,6 +61,7 @@ type agentPolicyModel struct {
MonitorMetrics types.Bool `tfsdk:"monitor_metrics"`
SysMonitoring types.Bool `tfsdk:"sys_monitoring"`
SkipDestroy types.Bool `tfsdk:"skip_destroy"`
HostNameFormat types.String `tfsdk:"host_name_format"`
SupportsAgentless types.Bool `tfsdk:"supports_agentless"`
InactivityTimeout customtypes.Duration `tfsdk:"inactivity_timeout"`
UnenrollmentTimeout customtypes.Duration `tfsdk:"unenrollment_timeout"`
Expand Down Expand Up @@ -84,6 +101,20 @@ 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)

// Determine host_name_format from AgentFeatures
// If AgentFeatures contains {"enabled": true, "name": "fqdn"}, then host_name_format is "fqdn"
// Otherwise, it defaults to "hostname"
model.HostNameFormat = types.StringValue(HostNameFormatHostname)
if data.AgentFeatures != nil {
for _, feature := range *data.AgentFeatures {
if feature.Name == agentFeatureFQDN && feature.Enabled {
model.HostNameFormat = types.StringValue(HostNameFormatFQDN)
break
}
}
}

if data.InactivityTimeout != nil {
// Convert seconds to duration string
seconds := int64(*data.InactivityTimeout)
Expand Down Expand Up @@ -377,10 +408,30 @@ func (model *agentPolicyModel) toAPICreateModel(ctx context.Context, feat featur
}
body.RequiredVersions = requiredVersions

// Handle host_name_format via AgentFeatures
if agentFeature := model.convertHostNameFormatToAgentFeature(); agentFeature != nil {
if !feat.SupportsAgentFeatures {
// Only error if user explicitly requests FQDN on unsupported version
// Default "hostname" is fine - just don't send agent_features
if agentFeature.Enabled {
return kbapi.PostFleetAgentPoliciesJSONRequestBody{}, diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(
path.Root("host_name_format"),
"Unsupported Elasticsearch version",
fmt.Sprintf("host_name_format (agent_features) is only supported in Elastic Stack %s and above", MinVersionAgentFeatures),
),
}
}
// On unsupported version with default "hostname", don't send agent_features
} else {
body.AgentFeatures = &[]apiAgentFeature{*agentFeature}
}
}

return body, nil
}

func (model *agentPolicyModel) toAPIUpdateModel(ctx context.Context, feat features) (kbapi.PutFleetAgentPoliciesAgentpolicyidJSONRequestBody, diag.Diagnostics) {
func (model *agentPolicyModel) toAPIUpdateModel(ctx context.Context, feat features, existingFeatures []apiAgentFeature) (kbapi.PutFleetAgentPoliciesAgentpolicyidJSONRequestBody, diag.Diagnostics) {
monitoring := make([]kbapi.PutFleetAgentPoliciesAgentpolicyidJSONBodyMonitoringEnabled, 0, 2)
if model.MonitorLogs.ValueBool() {
monitoring = append(monitoring, kbapi.PutFleetAgentPoliciesAgentpolicyidJSONBodyMonitoringEnabledLogs)
Expand Down Expand Up @@ -481,5 +532,76 @@ func (model *agentPolicyModel) toAPIUpdateModel(ctx context.Context, feat featur
}
body.RequiredVersions = requiredVersions

// Handle host_name_format via AgentFeatures, preserving other existing features
if agentFeature := model.convertHostNameFormatToAgentFeature(); agentFeature != nil {
if !feat.SupportsAgentFeatures {
// Only error if user explicitly requests FQDN on unsupported version
// Default "hostname" is fine - just don't send agent_features
if agentFeature.Enabled {
return kbapi.PutFleetAgentPoliciesAgentpolicyidJSONRequestBody{}, diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(
path.Root("host_name_format"),
"Unsupported Elasticsearch version",
fmt.Sprintf("host_name_format (agent_features) is only supported in Elastic Stack %s and above", MinVersionAgentFeatures),
),
}
}
// On unsupported version with default "hostname", don't send agent_features
} else {
body.AgentFeatures = mergeAgentFeature(existingFeatures, agentFeature)
}
} else if feat.SupportsAgentFeatures && len(existingFeatures) > 0 {
// Preserve existing features even when host_name_format is not set
body.AgentFeatures = &existingFeatures
}

return body, nil
}

// convertHostNameFormatToAgentFeature converts the host_name_format field to a single AgentFeature.
// - When host_name_format is "fqdn": returns {"name": "fqdn", "enabled": true}
// - When host_name_format is "hostname": returns {"name": "fqdn", "enabled": false} to explicitly disable
// - When not set: returns nil (no change to existing features)
func (model *agentPolicyModel) convertHostNameFormatToAgentFeature() *apiAgentFeature {
// If host_name_format is not set or unknown, don't modify AgentFeatures
if model.HostNameFormat.IsNull() || model.HostNameFormat.IsUnknown() {
return nil
}

// Explicitly set enabled based on the host_name_format value
// We need to send enabled: false when hostname is selected to override any existing fqdn setting
return &apiAgentFeature{
Enabled: model.HostNameFormat.ValueString() == HostNameFormatFQDN,
Name: agentFeatureFQDN,
}
}

// mergeAgentFeature merges a single feature into existing features, replacing any feature with the same name.
// If newFeature is nil, returns existing features unchanged (nil if existing is empty).
func mergeAgentFeature(existing []apiAgentFeature, newFeature *apiAgentFeature) *[]apiAgentFeature {
if newFeature == nil {
if len(existing) == 0 {
return nil
}
return &existing
}

// Check if the feature already exists and replace it, otherwise append
result := make([]apiAgentFeature, 0, len(existing)+1)
found := false

for _, f := range existing {
if f.Name == newFeature.Name {
result = append(result, *newFeature)
found = true
Comment on lines +590 to +596
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The capacity allocation len(existing)+1 will always over-allocate when replacing an existing feature. Consider using len(existing) as the capacity and only growing if appending a new feature.

Suggested change
result := make([]apiAgentFeature, 0, len(existing)+1)
found := false
for _, f := range existing {
if f.Name == newFeature.Name {
result = append(result, *newFeature)
found = true
// Pre-scan to determine if we are replacing or appending
found := false
for _, f := range existing {
if f.Name == newFeature.Name {
found = true
break
}
}
// Allocate with correct capacity: len(existing) if replacing, len(existing)+1 if appending
capacity := len(existing)
if !found {
capacity++
}
result := make([]apiAgentFeature, 0, capacity)
for _, f := range existing {
if f.Name == newFeature.Name {
result = append(result, *newFeature)

Copilot uses AI. Check for mistakes.
} else {
result = append(result, f)
}
}

if !found {
result = append(result, *newFeature)
}

return &result
}
Loading