Skip to content

Commit c8c0ca8

Browse files
CopilottobioCopilot
authored
Add required_versions attribute to fleet_agent_policy for automatic agent upgrades (#1436)
* Initial plan * Add required_versions attribute to fleet_agent_policy resource Co-authored-by: tobio <444668+tobio@users.noreply.github.com> * Fix linting issues and regenerate documentation Co-authored-by: tobio <444668+tobio@users.noreply.github.com> * Add validator to prevent duplicate versions in required_versions Co-authored-by: tobio <444668+tobio@users.noreply.github.com> * Simplify required_versions to use MapAttribute instead of custom Set type Co-authored-by: tobio <444668+tobio@users.noreply.github.com> * Update internal/fleet/agent_policy/acc_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add version check for required_versions (9.1.0+) and fix empty map handling Co-authored-by: tobio <444668+tobio@users.noreply.github.com> * Restructure tests and fixup the null versions case * make lint * Use math.Round() for explicit rounding semantics Co-authored-by: tobio <444668+tobio@users.noreply.github.com> * Changelog * PR feedback * PR feedback --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: tobio <444668+tobio@users.noreply.github.com> Co-authored-by: Toby Brain <toby.brain@elastic.co> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Toby Brain <tobio85@gmail.com>
1 parent d437767 commit c8c0ca8

File tree

11 files changed

+311
-0
lines changed

11 files changed

+311
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ alias = [
4040

4141
### Changes
4242

43+
- Add `required_versions` to `elasticstack_fleet_agent_policy` ([#1436](https://github.com/elastic/terraform-provider-elasticstack/pull/1436))
4344
- Migrate `elasticstack_elasticsearch_security_role` resource to Terraform Plugin Framework ([#1330](https://github.com/elastic/terraform-provider-elasticstack/pull/1330))
4445
- Fix an issue where the `elasticstack_fleet_output` resource would error due to inconsistent state after an ouptut was edited in the Kibana UI ([#1506](https://github.com/elastic/terraform-provider-elasticstack/pull/1506))
4546
- Allow `index` and `data_view_id` values to both be unknown during planning in `elasticstack_kibana_security_detection_rule` ([#1499](https://github.com/elastic/terraform-provider-elasticstack/pull/1499))

docs/resources/fleet_agent_policy.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ resource "elasticstack_fleet_agent_policy" "test_policy" {
5757
- `monitor_metrics` (Boolean) Enable collection of agent metrics.
5858
- `monitoring_output_id` (String) The identifier for monitoring output.
5959
- `policy_id` (String) Unique identifier of the agent policy.
60+
- `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.
6061
- `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.
6162
- `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.
6263
- `supports_agentless` (Boolean) Set to true to enable agentless data collection.

internal/fleet/agent_policy/acc_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,3 +524,85 @@ func checkResourceAgentPolicySkipDestroy(s *terraform.State) error {
524524
}
525525
return nil
526526
}
527+
528+
func TestAccResourceAgentPolicyWithRequiredVersions(t *testing.T) {
529+
policyName := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlphaNum)
530+
531+
resource.Test(t, resource.TestCase{
532+
PreCheck: func() { acctest.PreCheck(t) },
533+
CheckDestroy: checkResourceAgentPolicyDestroy,
534+
Steps: []resource.TestStep{
535+
{
536+
ProtoV6ProviderFactories: acctest.Providers,
537+
SkipFunc: versionutils.CheckIfVersionIsUnsupported(agent_policy.MinVersionRequiredVersions),
538+
ConfigDirectory: acctest.NamedTestCaseDirectory("create"),
539+
ConfigVariables: config.Variables{
540+
"policy_name": config.StringVariable(fmt.Sprintf("Policy %s", policyName)),
541+
},
542+
Check: resource.ComposeTestCheckFunc(
543+
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "name", fmt.Sprintf("Policy %s", policyName)),
544+
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "namespace", "default"),
545+
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "required_versions.%", "1"),
546+
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "required_versions.8.15.0", "100"),
547+
),
548+
},
549+
{
550+
ProtoV6ProviderFactories: acctest.Providers,
551+
SkipFunc: versionutils.CheckIfVersionIsUnsupported(agent_policy.MinVersionRequiredVersions),
552+
ConfigDirectory: acctest.NamedTestCaseDirectory("update_percentage"),
553+
ConfigVariables: config.Variables{
554+
"policy_name": config.StringVariable(fmt.Sprintf("Policy %s", policyName)),
555+
},
556+
Check: resource.ComposeTestCheckFunc(
557+
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "name", fmt.Sprintf("Policy %s", policyName)),
558+
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "namespace", "default"),
559+
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "required_versions.%", "1"),
560+
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "required_versions.8.15.0", "50"),
561+
),
562+
},
563+
{
564+
ProtoV6ProviderFactories: acctest.Providers,
565+
SkipFunc: versionutils.CheckIfVersionIsUnsupported(agent_policy.MinVersionRequiredVersions),
566+
ConfigDirectory: acctest.NamedTestCaseDirectory("add_version"),
567+
ConfigVariables: config.Variables{
568+
"policy_name": config.StringVariable(fmt.Sprintf("Policy %s", policyName)),
569+
},
570+
Check: resource.ComposeTestCheckFunc(
571+
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "name", fmt.Sprintf("Policy %s", policyName)),
572+
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "namespace", "default"),
573+
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "required_versions.%", "2"),
574+
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "required_versions.8.15.0", "50"),
575+
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "required_versions.8.16.0", "50"),
576+
),
577+
},
578+
{
579+
ProtoV6ProviderFactories: acctest.Providers,
580+
SkipFunc: versionutils.CheckIfVersionIsUnsupported(agent_policy.MinVersionRequiredVersions),
581+
ConfigDirectory: acctest.NamedTestCaseDirectory("unset_versions"),
582+
ConfigVariables: config.Variables{
583+
"policy_name": config.StringVariable(fmt.Sprintf("Policy %s", policyName)),
584+
},
585+
Check: resource.ComposeTestCheckFunc(
586+
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "name", fmt.Sprintf("Policy %s", policyName)),
587+
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "namespace", "default"),
588+
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "required_versions.%", "2"),
589+
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "required_versions.8.15.0", "50"),
590+
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "required_versions.8.16.0", "50"),
591+
),
592+
},
593+
{
594+
ProtoV6ProviderFactories: acctest.Providers,
595+
SkipFunc: versionutils.CheckIfVersionIsUnsupported(agent_policy.MinVersionRequiredVersions),
596+
ConfigDirectory: acctest.NamedTestCaseDirectory("remove_versions"),
597+
ConfigVariables: config.Variables{
598+
"policy_name": config.StringVariable(fmt.Sprintf("Policy %s", policyName)),
599+
},
600+
Check: resource.ComposeTestCheckFunc(
601+
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "name", fmt.Sprintf("Policy %s", policyName)),
602+
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "namespace", "default"),
603+
resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "required_versions.%", "0"),
604+
),
605+
},
606+
},
607+
})
608+
}

internal/fleet/agent_policy/models.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package agent_policy
33
import (
44
"context"
55
"fmt"
6+
"math"
67
"slices"
78
"time"
89

@@ -22,6 +23,7 @@ type features struct {
2223
SupportsInactivityTimeout bool
2324
SupportsUnenrollmentTimeout bool
2425
SupportsSpaceIds bool
26+
SupportsRequiredVersions bool
2527
}
2628

2729
type globalDataTagsItemModel struct {
@@ -48,6 +50,7 @@ type agentPolicyModel struct {
4850
UnenrollmentTimeout customtypes.Duration `tfsdk:"unenrollment_timeout"`
4951
GlobalDataTags types.Map `tfsdk:"global_data_tags"` //> globalDataTagsModel
5052
SpaceIds types.Set `tfsdk:"space_ids"`
53+
RequiredVersions types.Map `tfsdk:"required_versions"`
5154
}
5255

5356
func (model *agentPolicyModel) populateFromAPI(ctx context.Context, data *kbapi.AgentPolicy) diag.Diagnostics {
@@ -134,6 +137,25 @@ func (model *agentPolicyModel) populateFromAPI(ctx context.Context, data *kbapi.
134137
model.SpaceIds = types.SetNull(types.StringType)
135138
}
136139

140+
// Handle required_versions
141+
if data.RequiredVersions != nil {
142+
versionMap := make(map[string]attr.Value)
143+
144+
for _, rv := range *data.RequiredVersions {
145+
// Round the float32 percentage to nearest integer since we use Int32 in the schema
146+
percentage := int32(math.Round(float64(rv.Percentage)))
147+
versionMap[rv.Version] = types.Int32Value(percentage)
148+
}
149+
150+
reqVersions, d := types.MapValue(types.Int32Type, versionMap)
151+
if d.HasError() {
152+
return d
153+
}
154+
model.RequiredVersions = reqVersions
155+
} else {
156+
model.RequiredVersions = types.MapNull(types.Int32Type)
157+
}
158+
137159
return nil
138160
}
139161

@@ -186,6 +208,72 @@ func (model *agentPolicyModel) convertGlobalDataTags(ctx context.Context, feat f
186208
return &itemsList, diags
187209
}
188210

211+
// convertRequiredVersions converts the required versions from terraform model to API model
212+
func (model *agentPolicyModel) convertRequiredVersions(feat features) (*[]struct {
213+
Percentage float32 `json:"percentage"`
214+
Version string `json:"version"`
215+
}, diag.Diagnostics) {
216+
var diags diag.Diagnostics
217+
218+
if model.RequiredVersions.IsNull() || model.RequiredVersions.IsUnknown() {
219+
return nil, diags
220+
}
221+
222+
// Check if required_versions is supported
223+
if !feat.SupportsRequiredVersions {
224+
return nil, diag.Diagnostics{
225+
diag.NewAttributeErrorDiagnostic(
226+
path.Root("required_versions"),
227+
"Unsupported Elasticsearch version",
228+
fmt.Sprintf("Required versions (automatic agent upgrades) are only supported in Elastic Stack %s and above", MinVersionRequiredVersions),
229+
),
230+
}
231+
}
232+
233+
elements := model.RequiredVersions.Elements()
234+
235+
// If the map is empty (required_versions = {}), return an empty array to clear upgrades
236+
if len(elements) == 0 {
237+
emptyArray := make([]struct {
238+
Percentage float32 `json:"percentage"`
239+
Version string `json:"version"`
240+
}, 0)
241+
return &emptyArray, diags
242+
}
243+
244+
result := make([]struct {
245+
Percentage float32 `json:"percentage"`
246+
Version string `json:"version"`
247+
}, 0, len(elements))
248+
249+
for version, percentageVal := range elements {
250+
percentageInt32, ok := percentageVal.(types.Int32)
251+
if !ok {
252+
diags.AddError("required_versions conversion error", fmt.Sprintf("Expected Int32 value, got %T", percentageVal))
253+
continue
254+
}
255+
256+
if percentageInt32.IsNull() || percentageInt32.IsUnknown() {
257+
diags.AddError("required_versions validation error", "percentage cannot be null or unknown")
258+
continue
259+
}
260+
261+
result = append(result, struct {
262+
Percentage float32 `json:"percentage"`
263+
Version string `json:"version"`
264+
}{
265+
Percentage: float32(percentageInt32.ValueInt32()),
266+
Version: version,
267+
})
268+
}
269+
270+
if diags.HasError() {
271+
return nil, diags
272+
}
273+
274+
return &result, diags
275+
}
276+
189277
func (model *agentPolicyModel) toAPICreateModel(ctx context.Context, feat features) (kbapi.PostFleetAgentPoliciesJSONRequestBody, diag.Diagnostics) {
190278
monitoring := make([]kbapi.PostFleetAgentPoliciesJSONBodyMonitoringEnabled, 0, 2)
191279

@@ -282,6 +370,13 @@ func (model *agentPolicyModel) toAPICreateModel(ctx context.Context, feat featur
282370
body.SpaceIds = &spaceIds
283371
}
284372

373+
// Handle required_versions
374+
requiredVersions, d := model.convertRequiredVersions(feat)
375+
if d.HasError() {
376+
return kbapi.PostFleetAgentPoliciesJSONRequestBody{}, d
377+
}
378+
body.RequiredVersions = requiredVersions
379+
285380
return body, nil
286381
}
287382

@@ -379,5 +474,12 @@ func (model *agentPolicyModel) toAPIUpdateModel(ctx context.Context, feat featur
379474
body.SpaceIds = &spaceIds
380475
}
381476

477+
// Handle required_versions
478+
requiredVersions, d := model.convertRequiredVersions(feat)
479+
if d.HasError() {
480+
return kbapi.PutFleetAgentPoliciesAgentpolicyidJSONRequestBody{}, d
481+
}
482+
body.RequiredVersions = requiredVersions
483+
382484
return body, nil
383485
}

internal/fleet/agent_policy/resource.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ var (
2424
MinVersionInactivityTimeout = version.Must(version.NewVersion("8.7.0"))
2525
MinVersionUnenrollmentTimeout = version.Must(version.NewVersion("8.15.0"))
2626
MinVersionSpaceIds = version.Must(version.NewVersion("9.1.0"))
27+
MinVersionRequiredVersions = version.Must(version.NewVersion("9.1.0"))
2728
)
2829

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

79+
supportsRequiredVersions, diags := r.client.EnforceMinVersion(ctx, MinVersionRequiredVersions)
80+
if diags.HasError() {
81+
return features{}, diagutil.FrameworkDiagsFromSDK(diags)
82+
}
83+
7884
return features{
7985
SupportsGlobalDataTags: supportsGDT,
8086
SupportsSupportsAgentless: supportsSupportsAgentless,
8187
SupportsInactivityTimeout: supportsInactivityTimeout,
8288
SupportsUnenrollmentTimeout: supportsUnenrollmentTimeout,
8389
SupportsSpaceIds: supportsSpaceIds,
90+
SupportsRequiredVersions: supportsRequiredVersions,
8491
}, nil
8592
}

internal/fleet/agent_policy/schema.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55

66
"github.com/elastic/terraform-provider-elasticstack/internal/utils/customtypes"
77
"github.com/hashicorp/terraform-plugin-framework-validators/float32validator"
8+
"github.com/hashicorp/terraform-plugin-framework-validators/int32validator"
9+
"github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator"
810
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
911
"github.com/hashicorp/terraform-plugin-framework/attr"
1012
"github.com/hashicorp/terraform-plugin-framework/path"
@@ -13,6 +15,7 @@ import (
1315
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
1416
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
1517
"github.com/hashicorp/terraform-plugin-framework/resource/schema/mapdefault"
18+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier"
1619
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
1720
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
1821
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
@@ -145,6 +148,20 @@ func getSchema() schema.Schema {
145148
Optional: true,
146149
Computed: true,
147150
},
151+
"required_versions": schema.MapAttribute{
152+
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.",
153+
ElementType: types.Int32Type,
154+
Optional: true,
155+
Computed: true,
156+
PlanModifiers: []planmodifier.Map{
157+
mapplanmodifier.UseStateForUnknown(),
158+
},
159+
Validators: []validator.Map{
160+
mapvalidator.ValueInt32sAre(
161+
int32validator.Between(0, 100),
162+
),
163+
},
164+
},
148165
}}
149166
}
150167
func getGlobalDataTagsAttrTypes() attr.Type {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
variable "policy_name" {
2+
type = string
3+
description = "Name for the agent policy"
4+
}
5+
6+
provider "elasticstack" {
7+
elasticsearch {}
8+
kibana {}
9+
}
10+
11+
resource "elasticstack_fleet_agent_policy" "test_policy" {
12+
name = var.policy_name
13+
namespace = "default"
14+
description = "Test Agent Policy with Multiple Required Versions"
15+
monitor_logs = true
16+
monitor_metrics = false
17+
skip_destroy = false
18+
required_versions = {
19+
"8.15.0" = 50
20+
"8.16.0" = 50
21+
}
22+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
variable "policy_name" {
2+
type = string
3+
description = "Name for the agent policy"
4+
}
5+
6+
provider "elasticstack" {
7+
elasticsearch {}
8+
kibana {}
9+
}
10+
11+
resource "elasticstack_fleet_agent_policy" "test_policy" {
12+
name = var.policy_name
13+
namespace = "default"
14+
description = "Test Agent Policy with Required Versions"
15+
monitor_logs = true
16+
monitor_metrics = false
17+
skip_destroy = false
18+
required_versions = {
19+
"8.15.0" = 100
20+
}
21+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
variable "policy_name" {
2+
type = string
3+
description = "Name for the agent policy"
4+
}
5+
6+
provider "elasticstack" {
7+
elasticsearch {}
8+
kibana {}
9+
}
10+
11+
resource "elasticstack_fleet_agent_policy" "test_policy" {
12+
name = var.policy_name
13+
namespace = "default"
14+
description = "Test Agent Policy without Required Versions"
15+
monitor_logs = true
16+
monitor_metrics = false
17+
skip_destroy = false
18+
required_versions = {}
19+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
variable "policy_name" {
2+
type = string
3+
description = "Name for the agent policy"
4+
}
5+
6+
provider "elasticstack" {
7+
elasticsearch {}
8+
kibana {}
9+
}
10+
11+
resource "elasticstack_fleet_agent_policy" "test_policy" {
12+
name = var.policy_name
13+
namespace = "default"
14+
description = "Test Agent Policy without Required Versions"
15+
monitor_logs = true
16+
monitor_metrics = false
17+
skip_destroy = false
18+
}

0 commit comments

Comments
 (0)