Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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 docs/resources/fleet_agent_policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ resource "elasticstack_fleet_agent_policy" "test_policy" {
- `monitor_metrics` (Boolean) Enable collection of agent metrics.
- `monitoring_output_id` (String) The identifier for monitoring output.
- `policy_id` (String) Unique identifier of the agent policy.
- `required_versions` (Map of Number) Map of agent versions to target percentages for automatic upgrade. The key is the target version and the value is the percentage of agents to upgrade to that version.
- `skip_destroy` (Boolean) Set to true if you do not wish the agent policy to be deleted at destroy time, and instead just remove the agent policy from the Terraform state.
- `space_ids` (Set of String) The Kibana space IDs that this agent policy should be available in. When not specified, defaults to ["default"]. Note: The order of space IDs does not matter as this is a set.
- `supports_agentless` (Boolean) Set to true to enable agentless data collection.
Expand Down
83 changes: 83 additions & 0 deletions internal/fleet/agent_policy/acc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
)

var minVersionAgentPolicy = version.Must(version.NewVersion("8.6.0"))
var minVersionRequiredVersions = version.Must(version.NewVersion("9.1.0"))

//go:embed testdata/TestAccResourceAgentPolicyFromSDK/main.tf
var sdkCreateTestConfig string
Expand Down Expand Up @@ -524,3 +525,85 @@ func checkResourceAgentPolicySkipDestroy(s *terraform.State) error {
}
return nil
}

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

resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },
CheckDestroy: checkResourceAgentPolicyDestroy,
Steps: []resource.TestStep{
{
ProtoV6ProviderFactories: acctest.Providers,
SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionRequiredVersions),
ConfigDirectory: acctest.NamedTestCaseDirectory("create"),
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", "required_versions.%", "1"),
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "required_versions.8.15.0", "100"),
),
},
{
ProtoV6ProviderFactories: acctest.Providers,
SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionRequiredVersions),
ConfigDirectory: acctest.NamedTestCaseDirectory("update_percentage"),
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", "required_versions.%", "1"),
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "required_versions.8.15.0", "50"),
),
},
{
ProtoV6ProviderFactories: acctest.Providers,
SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionRequiredVersions),
ConfigDirectory: acctest.NamedTestCaseDirectory("add_version"),
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", "required_versions.%", "2"),
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "required_versions.8.15.0", "50"),
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "required_versions.8.16.0", "50"),
),
},
{
ProtoV6ProviderFactories: acctest.Providers,
SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionRequiredVersions),
ConfigDirectory: acctest.NamedTestCaseDirectory("unset_versions"),
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", "required_versions.%", "2"),
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "required_versions.8.15.0", "50"),
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "required_versions.8.16.0", "50"),
),
},
{
ProtoV6ProviderFactories: acctest.Providers,
SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionRequiredVersions),
ConfigDirectory: acctest.NamedTestCaseDirectory("remove_versions"),
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", "required_versions.%", "0"),
),
},
},
})
}
101 changes: 101 additions & 0 deletions internal/fleet/agent_policy/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type features struct {
SupportsInactivityTimeout bool
SupportsUnenrollmentTimeout bool
SupportsSpaceIds bool
SupportsRequiredVersions bool
}

type globalDataTagsItemModel struct {
Expand All @@ -48,6 +49,7 @@ type agentPolicyModel struct {
UnenrollmentTimeout customtypes.Duration `tfsdk:"unenrollment_timeout"`
GlobalDataTags types.Map `tfsdk:"global_data_tags"` //> globalDataTagsModel
SpaceIds types.Set `tfsdk:"space_ids"`
RequiredVersions types.Map `tfsdk:"required_versions"`
}

func (model *agentPolicyModel) populateFromAPI(ctx context.Context, data *kbapi.AgentPolicy) diag.Diagnostics {
Expand Down Expand Up @@ -134,6 +136,25 @@ func (model *agentPolicyModel) populateFromAPI(ctx context.Context, data *kbapi.
model.SpaceIds = types.SetNull(types.StringType)
}

// Handle required_versions
if data.RequiredVersions != nil {
versionMap := make(map[string]attr.Value)

for _, rv := range *data.RequiredVersions {
// Round the float32 percentage to nearest integer since we use Int32 in the schema
percentage := int32(rv.Percentage + 0.5)
versionMap[rv.Version] = types.Int32Value(percentage)
}

reqVersions, d := types.MapValue(types.Int32Type, versionMap)
if d.HasError() {
return d
}
model.RequiredVersions = reqVersions
} else {
model.RequiredVersions = types.MapNull(types.Int32Type)
}

return nil
}

Expand Down Expand Up @@ -186,6 +207,72 @@ func (model *agentPolicyModel) convertGlobalDataTags(ctx context.Context, feat f
return &itemsList, diags
}

// convertRequiredVersions converts the required versions from terraform model to API model
func (model *agentPolicyModel) convertRequiredVersions(ctx context.Context, feat features) (*[]struct {
Percentage float32 `json:"percentage"`
Version string `json:"version"`
}, diag.Diagnostics) {
var diags diag.Diagnostics

if model.RequiredVersions.IsNull() || model.RequiredVersions.IsUnknown() {
return nil, diags
}

// Check if required_versions is supported
if !feat.SupportsRequiredVersions {
return nil, diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(
path.Root("required_versions"),
"Unsupported Elasticsearch version",
fmt.Sprintf("Required versions (automatic agent upgrades) are only supported in Elastic Stack %s and above", MinVersionRequiredVersions),
),
}
}

elements := model.RequiredVersions.Elements()

// If the map is empty (required_versions = {}), return an empty array to clear upgrades
if len(elements) == 0 {
emptyArray := make([]struct {
Percentage float32 `json:"percentage"`
Version string `json:"version"`
}, 0)
return &emptyArray, diags
}

result := make([]struct {
Percentage float32 `json:"percentage"`
Version string `json:"version"`
}, 0, len(elements))

for version, percentageVal := range elements {
percentageInt32, ok := percentageVal.(types.Int32)
if !ok {
diags.AddError("required_versions conversion error", fmt.Sprintf("Expected Int32 value, got %T", percentageVal))
continue
}

if percentageInt32.IsNull() || percentageInt32.IsUnknown() {
diags.AddError("required_versions validation error", "percentage cannot be null or unknown")
continue
}

result = append(result, struct {
Percentage float32 `json:"percentage"`
Version string `json:"version"`
}{
Percentage: float32(percentageInt32.ValueInt32()),
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

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

The conversion from int32 to float32 involves an intermediate call to ValueInt32(). While not a critical issue, consider storing the value once to avoid repeated method calls if this code path is executed frequently.

Suggested change
Percentage: float32(percentageInt32.ValueInt32()),
Percentage: func() float32 {
val := percentageInt32.ValueInt32()
return float32(val)
}(),

Copilot uses AI. Check for mistakes.
Version: version,
})
}

if diags.HasError() {
return nil, diags
}

return &result, diags
}

func (model *agentPolicyModel) toAPICreateModel(ctx context.Context, feat features) (kbapi.PostFleetAgentPoliciesJSONRequestBody, diag.Diagnostics) {
monitoring := make([]kbapi.PostFleetAgentPoliciesJSONBodyMonitoringEnabled, 0, 2)

Expand Down Expand Up @@ -282,6 +369,13 @@ func (model *agentPolicyModel) toAPICreateModel(ctx context.Context, feat featur
body.SpaceIds = &spaceIds
}

// Handle required_versions
requiredVersions, d := model.convertRequiredVersions(ctx, feat)
if d.HasError() {
return kbapi.PostFleetAgentPoliciesJSONRequestBody{}, d
}
body.RequiredVersions = requiredVersions

return body, nil
}

Expand Down Expand Up @@ -379,5 +473,12 @@ func (model *agentPolicyModel) toAPIUpdateModel(ctx context.Context, feat featur
body.SpaceIds = &spaceIds
}

// Handle required_versions
requiredVersions, d := model.convertRequiredVersions(ctx, feat)
if d.HasError() {
return kbapi.PutFleetAgentPoliciesAgentpolicyidJSONRequestBody{}, d
}
body.RequiredVersions = requiredVersions

return body, nil
}
7 changes: 7 additions & 0 deletions internal/fleet/agent_policy/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ var (
MinVersionInactivityTimeout = version.Must(version.NewVersion("8.7.0"))
MinVersionUnenrollmentTimeout = version.Must(version.NewVersion("8.15.0"))
MinVersionSpaceIds = version.Must(version.NewVersion("9.1.0"))
MinVersionRequiredVersions = version.Must(version.NewVersion("9.1.0"))
)

// NewResource is a helper function to simplify the provider implementation.
Expand Down Expand Up @@ -75,11 +76,17 @@ func (r *agentPolicyResource) buildFeatures(ctx context.Context) (features, diag
return features{}, diagutil.FrameworkDiagsFromSDK(diags)
}

supportsRequiredVersions, diags := r.client.EnforceMinVersion(ctx, MinVersionRequiredVersions)
if diags.HasError() {
return features{}, diagutil.FrameworkDiagsFromSDK(diags)
}

return features{
SupportsGlobalDataTags: supportsGDT,
SupportsSupportsAgentless: supportsSupportsAgentless,
SupportsInactivityTimeout: supportsInactivityTimeout,
SupportsUnenrollmentTimeout: supportsUnenrollmentTimeout,
SupportsSpaceIds: supportsSpaceIds,
SupportsRequiredVersions: supportsRequiredVersions,
}, nil
}
10 changes: 10 additions & 0 deletions internal/fleet/agent_policy/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"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/mapdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
Expand Down Expand Up @@ -145,6 +146,15 @@ func getSchema() schema.Schema {
Optional: true,
Computed: true,
},
"required_versions": schema.MapAttribute{
Copy link
Member

Choose a reason for hiding this comment

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

Support for automatic upgrades was added in 9.1.0. Add a version check to the model code validating that this attribute is only set when the backing Stack supports automatic upgrades (i.e is 9.1.0 or higher).

Similarly skip the acceptance tests when this feature is unsupported.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added version check for 9.1.0+. Created MinVersionRequiredVersions constant, added SupportsRequiredVersions feature flag, and updated convertRequiredVersions() to validate support. Acceptance tests now skip on versions < 9.1.0. Commit: e858db4

Description: "Map of agent versions to target percentages for automatic upgrade. The key is the target version and the value is the percentage of agents to upgrade to that version.",
ElementType: types.Int32Type,
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.Map{
mapplanmodifier.UseStateForUnknown(),
},
},
}}
}
func getGlobalDataTagsAttrTypes() attr.Type {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
variable "policy_name" {
type = string
description = "Name for the agent policy"
}

provider "elasticstack" {
elasticsearch {}
kibana {}
}

resource "elasticstack_fleet_agent_policy" "test_policy" {
name = var.policy_name
namespace = "default"
description = "Test Agent Policy with Multiple Required Versions"
monitor_logs = true
monitor_metrics = false
skip_destroy = false
required_versions = {
"8.15.0" = 50
"8.16.0" = 50
}
}

data "elasticstack_fleet_enrollment_tokens" "test_policy" {
policy_id = elasticstack_fleet_agent_policy.test_policy.policy_id
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
variable "policy_name" {
type = string
description = "Name for the agent policy"
}

provider "elasticstack" {
elasticsearch {}
kibana {}
}

resource "elasticstack_fleet_agent_policy" "test_policy" {
name = var.policy_name
namespace = "default"
description = "Test Agent Policy with Required Versions"
monitor_logs = true
monitor_metrics = false
skip_destroy = false
required_versions = {
"8.15.0" = 100
}
}

data "elasticstack_fleet_enrollment_tokens" "test_policy" {
policy_id = elasticstack_fleet_agent_policy.test_policy.policy_id
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
variable "policy_name" {
type = string
description = "Name for the agent policy"
}

provider "elasticstack" {
elasticsearch {}
kibana {}
}

resource "elasticstack_fleet_agent_policy" "test_policy" {
name = var.policy_name
namespace = "default"
description = "Test Agent Policy without Required Versions"
monitor_logs = true
monitor_metrics = false
skip_destroy = false
required_versions = {}
}

data "elasticstack_fleet_enrollment_tokens" "test_policy" {
policy_id = elasticstack_fleet_agent_policy.test_policy.policy_id
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
variable "policy_name" {
type = string
description = "Name for the agent policy"
}

provider "elasticstack" {
elasticsearch {}
kibana {}
}

resource "elasticstack_fleet_agent_policy" "test_policy" {
name = var.policy_name
namespace = "default"
description = "Test Agent Policy without Required Versions"
monitor_logs = true
monitor_metrics = false
skip_destroy = false
}

data "elasticstack_fleet_enrollment_tokens" "test_policy" {
policy_id = elasticstack_fleet_agent_policy.test_policy.policy_id
}
Loading
Loading