diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 747672a3c..77bdff3aa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -128,12 +128,12 @@ jobs: - '8.13.4' - '8.14.3' - '8.15.5' - - '8.16.2' - - '8.17.10' - - '8.18.7' - - '8.19.3' - - '9.0.7' - - '9.1.3' + - '8.16.6' + - '8.17.1' + - '8.18.8' + - '8.19.8' + - '9.0.8' + - '9.1.8' # The default runner image can be overridden for specific versions if needed # include: # - version: '8.0.1' diff --git a/CHANGELOG.md b/CHANGELOG.md index 33e50001a..a30d35fa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## [Unreleased] +### Added + +- Add `advanced_settings` to `elasticstack_fleet_agent_policy` to configure agent logging, CPU limits, and download settings ([#1545](https://github.com/elastic/terraform-provider-elasticstack/pull/1545)) + ### Breaking changes #### `elasticstack_fleet_integration_policy` input block has changed to a map attribute. diff --git a/Makefile b/Makefile index c1bea9c8f..c455cdfc6 100644 --- a/Makefile +++ b/Makefile @@ -92,6 +92,10 @@ docker-fleet: ## Start Fleet node in docker container set-kibana-password: ## Sets the ES KIBANA_SYSTEM_USERNAME's password to KIBANA_SYSTEM_PASSWORD. This expects Elasticsearch to be available at localhost:9200 @ curl $(CURL_OPTS) http://localhost:9200/_security/user/$(KIBANA_SYSTEM_USERNAME)/_password -d '{"password":"$(KIBANA_SYSTEM_PASSWORD)"}' +.PHONY: setup-synthetics +setup-synthetics: ## Creates the synthetics policy required to run Synthetics. This expects Kibana to be available at localhost:5601 + @ curl $(CURL_OPTS) -H "kbn-xsrf: true" http://localhost:5601/api/fleet/epm/packages/synthetics/1.2.2 -d '{"force": true}' + .PHONY: create-es-api-key create-es-api-key: ## Creates and outputs a new API Key. This expects Elasticsearch to be available at localhost:9200 @ curl $(CURL_OPTS) http://localhost:9200/_security/api_key -d '{"name":"$(KIBANA_API_KEY_NAME)"}' diff --git a/docs/resources/fleet_agent_policy.md b/docs/resources/fleet_agent_policy.md index 3ba2483c2..ac255526b 100644 --- a/docs/resources/fleet_agent_policy.md +++ b/docs/resources/fleet_agent_policy.md @@ -74,6 +74,7 @@ resource "elasticstack_fleet_agent_policy" "test_policy" { ### Optional - `advanced_monitoring_options` (Attributes) Advanced monitoring options for the agent policy. Includes HTTP monitoring endpoint configuration and diagnostic settings. (see [below for nested schema](#nestedatt--advanced_monitoring_options)) +- `advanced_settings` (Attributes) Advanced agent settings for logging, resource limits, and downloads. These settings configure the behavior of Elastic Agents enrolled in this policy. (see [below for nested schema](#nestedatt--advanced_settings)) - `data_output_id` (String) The identifier for the data output. - `description` (String) The description of the agent policy. - `download_source_id` (String) The identifier for the Elastic Agent binary download server. @@ -145,6 +146,23 @@ Optional: + +### Nested Schema for `advanced_settings` + +Optional: + +- `download_target_directory` (String) Target directory for downloading agent updates. +- `download_timeout` (String) Timeout for downloading agent updates (e.g., '2h', '30m'). +- `go_max_procs` (Number) Maximum number of CPUs that the agent can use (GOMAXPROCS). Set to 0 to use all available CPUs. +- `logging_files_interval` (String) Interval for log file rotation (e.g., '30s', '1m', '1h'). +- `logging_files_keepfiles` (Number) Number of rotated log files to keep. +- `logging_files_rotateeverybytes` (Number) Rotate log files when they reach this size in bytes. +- `logging_level` (String) Logging level for the agent. Valid values: debug, info, warning, error. +- `logging_metrics_period` (String) Period for logging agent metrics (e.g., '30s', '1m'). +- `logging_to_files` (Boolean) Enable logging to files. +- `monitoring_runtime_experimental` (String) Experimental runtime monitoring mode. Valid values: '' (empty string to disable), 'process', 'otel'. + + ### Nested Schema for `global_data_tags` diff --git a/go.mod b/go.mod index f25861024..1826f2977 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/elastic/terraform-provider-elasticstack -go 1.25.1 +go 1.25.4 require ( github.com/disaster37/go-kibana-rest/v8 v8.5.0 diff --git a/internal/fleet/agent_policy/acc_test.go b/internal/fleet/agent_policy/acc_test.go index aab995593..2320e12fc 100644 --- a/internal/fleet/agent_policy/acc_test.go +++ b/internal/fleet/agent_policy/acc_test.go @@ -663,6 +663,102 @@ func TestAccResourceAgentPolicyWithRequiredVersions(t *testing.T) { }) } +func TestAccResourceAgentPolicyWithAdvancedSettings(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 logging settings + { + ProtoV6ProviderFactories: acctest.Providers, + SkipFunc: versionutils.CheckIfVersionIsUnsupported(agent_policy.MinVersionAdvancedSettings), + ConfigDirectory: acctest.NamedTestCaseDirectory("create_with_logging"), + 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", "advanced_settings.logging_level", "debug"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "advanced_settings.logging_to_files", "true"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "advanced_settings.go_max_procs", "2"), + ), + }, + // Step 2: Update settings + { + ProtoV6ProviderFactories: acctest.Providers, + SkipFunc: versionutils.CheckIfVersionIsUnsupported(agent_policy.MinVersionAdvancedSettings), + ConfigDirectory: acctest.NamedTestCaseDirectory("update_settings"), + 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", "advanced_settings.logging_level", "info"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "advanced_settings.logging_to_files", "true"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "advanced_settings.logging_files_keepfiles", "7"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "advanced_settings.logging_files_rotateeverybytes", "10485760"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "advanced_settings.go_max_procs", "4"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "advanced_settings.download_target_directory", "/tmp/elastic-agent"), + ), + }, + // Step 3: Import state verification + { + ProtoV6ProviderFactories: acctest.Providers, + SkipFunc: versionutils.CheckIfVersionIsUnsupported(agent_policy.MinVersionAdvancedSettings), + ConfigDirectory: acctest.NamedTestCaseDirectory("update_settings"), + ConfigVariables: config.Variables{ + "policy_name": config.StringVariable(fmt.Sprintf("Policy %s", policyName)), + }, + ResourceName: "elasticstack_fleet_agent_policy.test_policy", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"skip_destroy"}, + }, + // Step 4: Remove settings + { + ProtoV6ProviderFactories: acctest.Providers, + SkipFunc: versionutils.CheckIfVersionIsUnsupported(agent_policy.MinVersionAdvancedSettings), + ConfigDirectory: acctest.NamedTestCaseDirectory("remove_settings"), + 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"), + ), + }, + // Step 5: Set empty block - advanced_settings = {} applies schema defaults + { + ProtoV6ProviderFactories: acctest.Providers, + SkipFunc: versionutils.CheckIfVersionIsUnsupported(agent_policy.MinVersionAdvancedSettings), + ConfigDirectory: acctest.NamedTestCaseDirectory("set_to_defaults"), + 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 Default Advanced Settings"), + // Empty block applies schema defaults for flat attributes + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "advanced_settings.logging_level", "info"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "advanced_settings.logging_to_files", "true"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "advanced_settings.logging_files_interval", "30s"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "advanced_settings.logging_files_keepfiles", "7"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "advanced_settings.logging_files_rotateeverybytes", "10485760"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "advanced_settings.logging_metrics_period", "30s"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "advanced_settings.go_max_procs", "0"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "advanced_settings.download_timeout", "2h"), + // monitoring_runtime_experimental is not checked - it's null when not set (no default, UseStateForUnknown) + ), + }, + }, + }) +} + func TestAccResourceAgentPolicyWithAdvancedMonitoring(t *testing.T) { policyName := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlphaNum) @@ -731,6 +827,48 @@ func TestAccResourceAgentPolicyWithAdvancedMonitoring(t *testing.T) { ImportStateVerify: true, ImportStateVerifyIgnore: []string{"skip_destroy"}, }, + { + // Step 4: Remove advanced_monitoring_options from config + // UseStateForUnknown should preserve existing state values + ProtoV6ProviderFactories: acctest.Providers, + SkipFunc: versionutils.CheckIfVersionIsUnsupported(agent_policy.MinVersionAdvancedMonitoring), + ConfigDirectory: acctest.NamedTestCaseDirectory("remove_advanced_monitoring"), + ConfigVariables: config.Variables{ + "policy_name": config.StringVariable(fmt.Sprintf("Policy %s", policyName)), + "skip_destroy": config.BoolVariable(false), + }, + 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 - No Advanced Monitoring"), + ), + }, + { + // Step 5: Set empty nested blocks - schema defaults are applied + ProtoV6ProviderFactories: acctest.Providers, + SkipFunc: versionutils.CheckIfVersionIsUnsupported(agent_policy.MinVersionAdvancedMonitoring), + ConfigDirectory: acctest.NamedTestCaseDirectory("set_to_defaults"), + ConfigVariables: config.Variables{ + "policy_name": config.StringVariable(fmt.Sprintf("Policy %s", policyName)), + "skip_destroy": config.BoolVariable(false), + }, + 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 Default Advanced Monitoring"), + // Empty nested blocks apply schema defaults for leaf attributes + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "advanced_monitoring_options.http_monitoring_endpoint.enabled", "false"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "advanced_monitoring_options.http_monitoring_endpoint.host", "localhost"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "advanced_monitoring_options.http_monitoring_endpoint.port", "6791"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "advanced_monitoring_options.http_monitoring_endpoint.buffer_enabled", "false"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "advanced_monitoring_options.http_monitoring_endpoint.pprof_enabled", "false"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "advanced_monitoring_options.diagnostics.rate_limits.interval", "1m"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "advanced_monitoring_options.diagnostics.rate_limits.burst", "1"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "advanced_monitoring_options.diagnostics.file_uploader.init_duration", "1s"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "advanced_monitoring_options.diagnostics.file_uploader.backoff_duration", "1m"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "advanced_monitoring_options.diagnostics.file_uploader.max_retries", "10"), + ), + }, }, }) } diff --git a/internal/fleet/agent_policy/models.go b/internal/fleet/agent_policy/models.go index 3798804dd..4248369a8 100644 --- a/internal/fleet/agent_policy/models.go +++ b/internal/fleet/agent_policy/models.go @@ -14,9 +14,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) const ( @@ -43,6 +41,7 @@ type features struct { SupportsRequiredVersions bool SupportsAgentFeatures bool SupportsAdvancedMonitoring bool + SupportsAdvancedSettings bool } type globalDataTagsItemModel struct { @@ -50,50 +49,6 @@ type globalDataTagsItemModel struct { NumberValue types.Float32 `tfsdk:"number_value"` } -// Advanced Monitoring Options models -type advancedMonitoringOptionsModel struct { - HttpMonitoringEndpoint types.Object `tfsdk:"http_monitoring_endpoint"` - Diagnostics types.Object `tfsdk:"diagnostics"` -} - -type httpMonitoringEndpointModel struct { - Enabled types.Bool `tfsdk:"enabled"` - Host types.String `tfsdk:"host"` - Port types.Int32 `tfsdk:"port"` - BufferEnabled types.Bool `tfsdk:"buffer_enabled"` - PprofEnabled types.Bool `tfsdk:"pprof_enabled"` -} - -type diagnosticsModel struct { - RateLimits types.Object `tfsdk:"rate_limits"` - FileUploader types.Object `tfsdk:"file_uploader"` -} - -type rateLimitsModel struct { - Interval customtypes.Duration `tfsdk:"interval"` - Burst types.Int32 `tfsdk:"burst"` -} - -type fileUploaderModel struct { - InitDuration customtypes.Duration `tfsdk:"init_duration"` - BackoffDuration customtypes.Duration `tfsdk:"backoff_duration"` - MaxRetries types.Int32 `tfsdk:"max_retries"` -} - -// Default values for advanced monitoring options -const ( - defaultHttpMonitoringEnabled = false - defaultHttpMonitoringHost = "localhost" - defaultHttpMonitoringPort = 6791 - defaultHttpMonitoringBufferEnabled = false - defaultHttpMonitoringPprofEnabled = false - defaultDiagnosticsInterval = "1m" - defaultDiagnosticsBurst = 1 - defaultDiagnosticsInitDuration = "1s" - defaultDiagnosticsBackoffDuration = "1m" - defaultDiagnosticsMaxRetries = 10 -) - type agentPolicyModel struct { ID types.String `tfsdk:"id"` PolicyID types.String `tfsdk:"policy_id"` @@ -116,6 +71,7 @@ type agentPolicyModel struct { SpaceIds types.Set `tfsdk:"space_ids"` RequiredVersions types.Map `tfsdk:"required_versions"` AdvancedMonitoringOptions types.Object `tfsdk:"advanced_monitoring_options"` + AdvancedSettings types.Object `tfsdk:"advanced_settings"` } func (model *agentPolicyModel) populateFromAPI(ctx context.Context, data *kbapi.AgentPolicy) diag.Diagnostics { @@ -206,7 +162,7 @@ func (model *agentPolicyModel) populateFromAPI(ctx context.Context, data *kbapi. } - if data.SpaceIds != nil { + if data.SpaceIds != nil && len(*data.SpaceIds) > 0 { spaceIds, d := types.SetValueFrom(ctx, types.StringType, *data.SpaceIds) if d.HasError() { return d @@ -235,168 +191,17 @@ func (model *agentPolicyModel) populateFromAPI(ctx context.Context, data *kbapi. model.RequiredVersions = types.MapNull(types.Int32Type) } - // Handle advanced monitoring options - if diags := model.populateAdvancedMonitoringFromAPI(ctx, data); diags.HasError() { + // Handle advanced_settings + if diags := model.populateAdvancedSettingsFromAPI(ctx, data); diags.HasError() { return diags } - return nil -} - -// populateAdvancedMonitoringFromAPI populates the advanced monitoring options from API response -func (model *agentPolicyModel) populateAdvancedMonitoringFromAPI(ctx context.Context, data *kbapi.AgentPolicy) diag.Diagnostics { - // Check if any advanced monitoring data exists in the API response - hasHttpMonitoring := data.MonitoringHttp != nil - hasPprofEnabled := data.MonitoringPprofEnabled != nil - hasDiagnostics := data.MonitoringDiagnostics != nil - - if !hasHttpMonitoring && !hasPprofEnabled && !hasDiagnostics { - // No advanced monitoring options in API response - model.AdvancedMonitoringOptions = types.ObjectNull(advancedMonitoringOptionsAttrTypes()) - return nil - } - - var httpEndpointObj types.Object - var diagnosticsObj types.Object - - // Populate HTTP monitoring endpoint - if hasHttpMonitoring || hasPprofEnabled { - httpEndpoint := httpMonitoringEndpointModel{ - Enabled: types.BoolValue(defaultHttpMonitoringEnabled), - Host: types.StringValue(defaultHttpMonitoringHost), - Port: types.Int32Value(defaultHttpMonitoringPort), - BufferEnabled: types.BoolValue(defaultHttpMonitoringBufferEnabled), - PprofEnabled: types.BoolValue(defaultHttpMonitoringPprofEnabled), - } - - if data.MonitoringHttp != nil { - if data.MonitoringHttp.Enabled != nil { - httpEndpoint.Enabled = types.BoolValue(*data.MonitoringHttp.Enabled) - } - if data.MonitoringHttp.Host != nil { - httpEndpoint.Host = types.StringValue(*data.MonitoringHttp.Host) - } - if data.MonitoringHttp.Port != nil { - httpEndpoint.Port = types.Int32Value(int32(*data.MonitoringHttp.Port)) - } - if data.MonitoringHttp.Buffer != nil && data.MonitoringHttp.Buffer.Enabled != nil { - httpEndpoint.BufferEnabled = types.BoolValue(*data.MonitoringHttp.Buffer.Enabled) - } - } - - if data.MonitoringPprofEnabled != nil { - httpEndpoint.PprofEnabled = types.BoolValue(*data.MonitoringPprofEnabled) - } - - obj, diags := types.ObjectValueFrom(ctx, httpMonitoringEndpointAttrTypes(), httpEndpoint) - if diags.HasError() { - return diags - } - httpEndpointObj = obj - } else { - httpEndpointObj = types.ObjectNull(httpMonitoringEndpointAttrTypes()) - } - - // Populate diagnostics - if hasDiagnostics { - var rateLimitsObj types.Object - var fileUploaderObj types.Object - - if data.MonitoringDiagnostics.Limit != nil { - rateLimits := rateLimitsModel{ - Interval: customtypes.NewDurationValue(defaultDiagnosticsInterval), - Burst: types.Int32Value(defaultDiagnosticsBurst), - } - if data.MonitoringDiagnostics.Limit.Interval != nil { - rateLimits.Interval = customtypes.NewDurationValue(*data.MonitoringDiagnostics.Limit.Interval) - } - if data.MonitoringDiagnostics.Limit.Burst != nil { - rateLimits.Burst = types.Int32Value(int32(*data.MonitoringDiagnostics.Limit.Burst)) - } - obj, diags := types.ObjectValueFrom(ctx, rateLimitsAttrTypes(), rateLimits) - if diags.HasError() { - return diags - } - rateLimitsObj = obj - } else { - rateLimitsObj = types.ObjectNull(rateLimitsAttrTypes()) - } - - if data.MonitoringDiagnostics.Uploader != nil { - fileUploader := fileUploaderModel{ - InitDuration: customtypes.NewDurationValue(defaultDiagnosticsInitDuration), - BackoffDuration: customtypes.NewDurationValue(defaultDiagnosticsBackoffDuration), - MaxRetries: types.Int32Value(defaultDiagnosticsMaxRetries), - } - if data.MonitoringDiagnostics.Uploader.InitDur != nil { - fileUploader.InitDuration = customtypes.NewDurationValue(*data.MonitoringDiagnostics.Uploader.InitDur) - } - if data.MonitoringDiagnostics.Uploader.MaxDur != nil { - fileUploader.BackoffDuration = customtypes.NewDurationValue(*data.MonitoringDiagnostics.Uploader.MaxDur) - } - if data.MonitoringDiagnostics.Uploader.MaxRetries != nil { - fileUploader.MaxRetries = types.Int32Value(int32(*data.MonitoringDiagnostics.Uploader.MaxRetries)) - } - obj, diags := types.ObjectValueFrom(ctx, fileUploaderAttrTypes(), fileUploader) - if diags.HasError() { - return diags - } - fileUploaderObj = obj - } else { - fileUploaderObj = types.ObjectNull(fileUploaderAttrTypes()) - } - - diagModel := diagnosticsModel{ - RateLimits: rateLimitsObj, - FileUploader: fileUploaderObj, - } - obj, diags := types.ObjectValueFrom(ctx, diagnosticsAttrTypes(), diagModel) - if diags.HasError() { - return diags - } - diagnosticsObj = obj - } else { - diagnosticsObj = types.ObjectNull(diagnosticsAttrTypes()) - } - - amo := advancedMonitoringOptionsModel{ - HttpMonitoringEndpoint: httpEndpointObj, - Diagnostics: diagnosticsObj, - } - - obj, diags := types.ObjectValueFrom(ctx, advancedMonitoringOptionsAttrTypes(), amo) - if diags.HasError() { + // Handle advanced monitoring options + if diags := model.populateAdvancedMonitoringFromAPI(ctx, data); diags.HasError() { return diags } - model.AdvancedMonitoringOptions = obj - return nil -} - -// Attribute type helpers for advanced monitoring options - pulled from schema to avoid duplication -func advancedMonitoringOptionsAttrTypes() map[string]attr.Type { - return getSchema().Attributes["advanced_monitoring_options"].GetType().(attr.TypeWithAttributeTypes).AttributeTypes() -} -func httpMonitoringEndpointAttrTypes() map[string]attr.Type { - amoAttr := getSchema().Attributes["advanced_monitoring_options"].(schema.SingleNestedAttribute) - return amoAttr.Attributes["http_monitoring_endpoint"].GetType().(attr.TypeWithAttributeTypes).AttributeTypes() -} - -func diagnosticsAttrTypes() map[string]attr.Type { - amoAttr := getSchema().Attributes["advanced_monitoring_options"].(schema.SingleNestedAttribute) - return amoAttr.Attributes["diagnostics"].GetType().(attr.TypeWithAttributeTypes).AttributeTypes() -} - -func rateLimitsAttrTypes() map[string]attr.Type { - amoAttr := getSchema().Attributes["advanced_monitoring_options"].(schema.SingleNestedAttribute) - diagAttr := amoAttr.Attributes["diagnostics"].(schema.SingleNestedAttribute) - return diagAttr.Attributes["rate_limits"].GetType().(attr.TypeWithAttributeTypes).AttributeTypes() -} - -func fileUploaderAttrTypes() map[string]attr.Type { - amoAttr := getSchema().Attributes["advanced_monitoring_options"].(schema.SingleNestedAttribute) - diagAttr := amoAttr.Attributes["diagnostics"].(schema.SingleNestedAttribute) - return diagAttr.Attributes["file_uploader"].GetType().(attr.TypeWithAttributeTypes).AttributeTypes() + return nil } // convertGlobalDataTags converts the global data tags from terraform model to API model @@ -637,6 +442,20 @@ func (model *agentPolicyModel) toAPICreateModel(ctx context.Context, feat featur } } + // Handle advanced_settings + if utils.IsKnown(model.AdvancedSettings) { + if !feat.SupportsAdvancedSettings { + return kbapi.PostFleetAgentPoliciesJSONRequestBody{}, diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("advanced_settings"), + "Unsupported Elasticsearch version", + fmt.Sprintf("Advanced settings are only supported in Elastic Stack %s and above", MinVersionAdvancedSettings), + ), + } + } + body.AdvancedSettings = model.convertAdvancedSettingsToAPI(ctx) + } + // Handle advanced monitoring options if utils.IsKnown(model.AdvancedMonitoringOptions) { if !feat.SupportsAdvancedMonitoring { @@ -782,6 +601,20 @@ func (model *agentPolicyModel) toAPIUpdateModel(ctx context.Context, feat featur body.AgentFeatures = &existingFeatures } + // Handle advanced_settings + if utils.IsKnown(model.AdvancedSettings) { + if !feat.SupportsAdvancedSettings { + return kbapi.PutFleetAgentPoliciesAgentpolicyidJSONRequestBody{}, diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("advanced_settings"), + "Unsupported Elasticsearch version", + fmt.Sprintf("Advanced settings are only supported in Elastic Stack %s and above", MinVersionAdvancedSettings), + ), + } + } + body.AdvancedSettings = model.convertAdvancedSettingsToAPI(ctx) + } + // Handle advanced monitoring options if utils.IsKnown(model.AdvancedMonitoringOptions) { if !feat.SupportsAdvancedMonitoring { @@ -850,153 +683,3 @@ func mergeAgentFeature(existing []apiAgentFeature, newFeature *apiAgentFeature) return &result } - -// httpMonitoringEndpointAPIResult is the return type for convertHttpMonitoringEndpointToAPI -// This type alias matches the inline struct expected by kbapi.PostFleetAgentPoliciesJSONRequestBody.MonitoringHttp -type httpMonitoringEndpointAPIResult = struct { - Buffer *struct { - Enabled *bool `json:"enabled,omitempty"` - } `json:"buffer,omitempty"` - Enabled *bool `json:"enabled,omitempty"` - Host *string `json:"host,omitempty"` - Port *float32 `json:"port,omitempty"` -} - -// convertHttpMonitoringEndpointToAPI converts the HTTP monitoring endpoint config to API format -func (model *agentPolicyModel) convertHttpMonitoringEndpointToAPI(ctx context.Context) (*httpMonitoringEndpointAPIResult, *bool) { - if !utils.IsKnown(model.AdvancedMonitoringOptions) { - return nil, nil - } - - var amo advancedMonitoringOptionsModel - model.AdvancedMonitoringOptions.As(ctx, &amo, basetypes.ObjectAsOptions{}) - - if !utils.IsKnown(amo.HttpMonitoringEndpoint) { - return nil, nil - } - - var http httpMonitoringEndpointModel - amo.HttpMonitoringEndpoint.As(ctx, &http, basetypes.ObjectAsOptions{}) - - // Check if any values differ from defaults - hasNonDefaultValues := http.Enabled.ValueBool() || - http.Host.ValueString() != defaultHttpMonitoringHost || - http.Port.ValueInt32() != defaultHttpMonitoringPort || - http.BufferEnabled.ValueBool() || - http.PprofEnabled.ValueBool() - - if !hasNonDefaultValues { - return nil, nil - } - - enabled := http.Enabled.ValueBool() - host := http.Host.ValueString() - port := float32(http.Port.ValueInt32()) - bufferEnabled := http.BufferEnabled.ValueBool() - pprofEnabled := http.PprofEnabled.ValueBool() - - result := &httpMonitoringEndpointAPIResult{ - Enabled: &enabled, - Host: &host, - Port: &port, - Buffer: &struct { - Enabled *bool `json:"enabled,omitempty"` - }{ - Enabled: &bufferEnabled, - }, - } - - return result, &pprofEnabled -} - -// diagnosticsAPIResult is the return type for convertDiagnosticsToAPI -// This type alias matches the inline struct expected by kbapi.PostFleetAgentPoliciesJSONRequestBody.MonitoringDiagnostics -type diagnosticsAPIResult = struct { - Limit *struct { - Burst *float32 `json:"burst,omitempty"` - Interval *string `json:"interval,omitempty"` - } `json:"limit,omitempty"` - Uploader *struct { - InitDur *string `json:"init_dur,omitempty"` - MaxDur *string `json:"max_dur,omitempty"` - MaxRetries *float32 `json:"max_retries,omitempty"` - } `json:"uploader,omitempty"` -} - -// convertDiagnosticsToAPI converts the diagnostics config to API format -func (model *agentPolicyModel) convertDiagnosticsToAPI(ctx context.Context) *diagnosticsAPIResult { - if !utils.IsKnown(model.AdvancedMonitoringOptions) { - return nil - } - - var amo advancedMonitoringOptionsModel - model.AdvancedMonitoringOptions.As(ctx, &amo, basetypes.ObjectAsOptions{}) - - if !utils.IsKnown(amo.Diagnostics) { - return nil - } - - var diag diagnosticsModel - amo.Diagnostics.As(ctx, &diag, basetypes.ObjectAsOptions{}) - - // Check if any values differ from defaults - hasNonDefaultValues := false - - if utils.IsKnown(diag.RateLimits) { - var rateLimits rateLimitsModel - diag.RateLimits.As(ctx, &rateLimits, basetypes.ObjectAsOptions{}) - if rateLimits.Interval.ValueString() != defaultDiagnosticsInterval || - rateLimits.Burst.ValueInt32() != defaultDiagnosticsBurst { - hasNonDefaultValues = true - } - } - - if utils.IsKnown(diag.FileUploader) { - var fileUploader fileUploaderModel - diag.FileUploader.As(ctx, &fileUploader, basetypes.ObjectAsOptions{}) - if fileUploader.InitDuration.ValueString() != defaultDiagnosticsInitDuration || - fileUploader.BackoffDuration.ValueString() != defaultDiagnosticsBackoffDuration || - fileUploader.MaxRetries.ValueInt32() != defaultDiagnosticsMaxRetries { - hasNonDefaultValues = true - } - } - - if !hasNonDefaultValues { - return nil - } - - result := &diagnosticsAPIResult{} - - if utils.IsKnown(diag.RateLimits) { - var rateLimits rateLimitsModel - diag.RateLimits.As(ctx, &rateLimits, basetypes.ObjectAsOptions{}) - interval := rateLimits.Interval.ValueString() - burst := float32(rateLimits.Burst.ValueInt32()) - result.Limit = &struct { - Burst *float32 `json:"burst,omitempty"` - Interval *string `json:"interval,omitempty"` - }{ - Interval: &interval, - Burst: &burst, - } - } - - if utils.IsKnown(diag.FileUploader) { - var fileUploader fileUploaderModel - diag.FileUploader.As(ctx, &fileUploader, basetypes.ObjectAsOptions{}) - initDur := fileUploader.InitDuration.ValueString() - maxDur := fileUploader.BackoffDuration.ValueString() - maxRetries := float32(fileUploader.MaxRetries.ValueInt32()) - result.Uploader = &struct { - InitDur *string `json:"init_dur,omitempty"` - MaxDur *string `json:"max_dur,omitempty"` - MaxRetries *float32 `json:"max_retries,omitempty"` - }{ - InitDur: &initDur, - MaxDur: &maxDur, - MaxRetries: &maxRetries, - } - } - - return result -} diff --git a/internal/fleet/agent_policy/models_advanced_monitoring.go b/internal/fleet/agent_policy/models_advanced_monitoring.go new file mode 100644 index 000000000..6f41137e6 --- /dev/null +++ b/internal/fleet/agent_policy/models_advanced_monitoring.go @@ -0,0 +1,328 @@ +package agent_policy + +import ( + "context" + + "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" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Advanced Monitoring Options models +type advancedMonitoringOptionsModel struct { + HttpMonitoringEndpoint types.Object `tfsdk:"http_monitoring_endpoint"` + Diagnostics types.Object `tfsdk:"diagnostics"` +} + +type httpMonitoringEndpointModel struct { + Enabled types.Bool `tfsdk:"enabled"` + Host types.String `tfsdk:"host"` + Port types.Int32 `tfsdk:"port"` + BufferEnabled types.Bool `tfsdk:"buffer_enabled"` + PprofEnabled types.Bool `tfsdk:"pprof_enabled"` +} + +type diagnosticsModel struct { + RateLimits types.Object `tfsdk:"rate_limits"` + FileUploader types.Object `tfsdk:"file_uploader"` +} + +type rateLimitsModel struct { + Interval customtypes.Duration `tfsdk:"interval"` + Burst types.Int32 `tfsdk:"burst"` +} + +type fileUploaderModel struct { + InitDuration customtypes.Duration `tfsdk:"init_duration"` + BackoffDuration customtypes.Duration `tfsdk:"backoff_duration"` + MaxRetries types.Int32 `tfsdk:"max_retries"` +} + +// Default values for advanced monitoring options +const ( + defaultHttpMonitoringEnabled = false + defaultHttpMonitoringHost = "localhost" + defaultHttpMonitoringPort = 6791 + defaultHttpMonitoringBufferEnabled = false + defaultHttpMonitoringPprofEnabled = false + defaultDiagnosticsInterval = "1m" + defaultDiagnosticsBurst = 1 + defaultDiagnosticsInitDuration = "1s" + defaultDiagnosticsBackoffDuration = "1m" + defaultDiagnosticsMaxRetries = 10 +) + +// Attribute type helpers for advanced monitoring options - pulled from schema to avoid duplication +func advancedMonitoringOptionsAttrTypes() map[string]attr.Type { + return getSchema().Attributes["advanced_monitoring_options"].GetType().(attr.TypeWithAttributeTypes).AttributeTypes() +} + +func httpMonitoringEndpointAttrTypes() map[string]attr.Type { + amoAttr := getSchema().Attributes["advanced_monitoring_options"].(schema.SingleNestedAttribute) + return amoAttr.Attributes["http_monitoring_endpoint"].GetType().(attr.TypeWithAttributeTypes).AttributeTypes() +} + +func diagnosticsAttrTypes() map[string]attr.Type { + amoAttr := getSchema().Attributes["advanced_monitoring_options"].(schema.SingleNestedAttribute) + return amoAttr.Attributes["diagnostics"].GetType().(attr.TypeWithAttributeTypes).AttributeTypes() +} + +func rateLimitsAttrTypes() map[string]attr.Type { + amoAttr := getSchema().Attributes["advanced_monitoring_options"].(schema.SingleNestedAttribute) + diagAttr := amoAttr.Attributes["diagnostics"].(schema.SingleNestedAttribute) + return diagAttr.Attributes["rate_limits"].GetType().(attr.TypeWithAttributeTypes).AttributeTypes() +} + +func fileUploaderAttrTypes() map[string]attr.Type { + amoAttr := getSchema().Attributes["advanced_monitoring_options"].(schema.SingleNestedAttribute) + diagAttr := amoAttr.Attributes["diagnostics"].(schema.SingleNestedAttribute) + return diagAttr.Attributes["file_uploader"].GetType().(attr.TypeWithAttributeTypes).AttributeTypes() +} + +// populateAdvancedMonitoringFromAPI populates the advanced monitoring options from API response +func (model *agentPolicyModel) populateAdvancedMonitoringFromAPI(ctx context.Context, data *kbapi.AgentPolicy) diag.Diagnostics { + // Check if any advanced monitoring data exists in the API response + hasHttpMonitoring := data.MonitoringHttp != nil + hasPprofEnabled := data.MonitoringPprofEnabled != nil + hasDiagnostics := data.MonitoringDiagnostics != nil + + if !hasHttpMonitoring && !hasPprofEnabled && !hasDiagnostics { + // No advanced monitoring options in API response + model.AdvancedMonitoringOptions = types.ObjectNull(advancedMonitoringOptionsAttrTypes()) + return nil + } + + var httpEndpointObj types.Object + var diagnosticsObj types.Object + + // Populate HTTP monitoring endpoint + if hasHttpMonitoring || hasPprofEnabled { + httpEndpoint := httpMonitoringEndpointModel{ + Enabled: types.BoolValue(defaultHttpMonitoringEnabled), + Host: types.StringValue(defaultHttpMonitoringHost), + Port: types.Int32Value(defaultHttpMonitoringPort), + BufferEnabled: types.BoolValue(defaultHttpMonitoringBufferEnabled), + PprofEnabled: types.BoolValue(defaultHttpMonitoringPprofEnabled), + } + + if data.MonitoringHttp != nil { + if data.MonitoringHttp.Enabled != nil { + httpEndpoint.Enabled = types.BoolValue(*data.MonitoringHttp.Enabled) + } + if data.MonitoringHttp.Host != nil { + httpEndpoint.Host = types.StringValue(*data.MonitoringHttp.Host) + } + if data.MonitoringHttp.Port != nil { + httpEndpoint.Port = types.Int32Value(int32(*data.MonitoringHttp.Port)) + } + if data.MonitoringHttp.Buffer != nil && data.MonitoringHttp.Buffer.Enabled != nil { + httpEndpoint.BufferEnabled = types.BoolValue(*data.MonitoringHttp.Buffer.Enabled) + } + } + + if data.MonitoringPprofEnabled != nil { + httpEndpoint.PprofEnabled = types.BoolValue(*data.MonitoringPprofEnabled) + } + + obj, diags := types.ObjectValueFrom(ctx, httpMonitoringEndpointAttrTypes(), httpEndpoint) + if diags.HasError() { + return diags + } + httpEndpointObj = obj + } else { + httpEndpointObj = types.ObjectNull(httpMonitoringEndpointAttrTypes()) + } + + // Populate diagnostics + if hasDiagnostics { + var rateLimitsObj types.Object + var fileUploaderObj types.Object + + if data.MonitoringDiagnostics.Limit != nil { + rateLimits := rateLimitsModel{ + Interval: customtypes.NewDurationValue(defaultDiagnosticsInterval), + Burst: types.Int32Value(defaultDiagnosticsBurst), + } + if data.MonitoringDiagnostics.Limit.Interval != nil { + rateLimits.Interval = customtypes.NewDurationValue(*data.MonitoringDiagnostics.Limit.Interval) + } + if data.MonitoringDiagnostics.Limit.Burst != nil { + rateLimits.Burst = types.Int32Value(int32(*data.MonitoringDiagnostics.Limit.Burst)) + } + obj, diags := types.ObjectValueFrom(ctx, rateLimitsAttrTypes(), rateLimits) + if diags.HasError() { + return diags + } + rateLimitsObj = obj + } else { + rateLimitsObj = types.ObjectNull(rateLimitsAttrTypes()) + } + + if data.MonitoringDiagnostics.Uploader != nil { + fileUploader := fileUploaderModel{ + InitDuration: customtypes.NewDurationValue(defaultDiagnosticsInitDuration), + BackoffDuration: customtypes.NewDurationValue(defaultDiagnosticsBackoffDuration), + MaxRetries: types.Int32Value(defaultDiagnosticsMaxRetries), + } + if data.MonitoringDiagnostics.Uploader.InitDur != nil { + fileUploader.InitDuration = customtypes.NewDurationValue(*data.MonitoringDiagnostics.Uploader.InitDur) + } + if data.MonitoringDiagnostics.Uploader.MaxDur != nil { + fileUploader.BackoffDuration = customtypes.NewDurationValue(*data.MonitoringDiagnostics.Uploader.MaxDur) + } + if data.MonitoringDiagnostics.Uploader.MaxRetries != nil { + fileUploader.MaxRetries = types.Int32Value(int32(*data.MonitoringDiagnostics.Uploader.MaxRetries)) + } + obj, diags := types.ObjectValueFrom(ctx, fileUploaderAttrTypes(), fileUploader) + if diags.HasError() { + return diags + } + fileUploaderObj = obj + } else { + fileUploaderObj = types.ObjectNull(fileUploaderAttrTypes()) + } + + diagModel := diagnosticsModel{ + RateLimits: rateLimitsObj, + FileUploader: fileUploaderObj, + } + obj, diags := types.ObjectValueFrom(ctx, diagnosticsAttrTypes(), diagModel) + if diags.HasError() { + return diags + } + diagnosticsObj = obj + } else { + diagnosticsObj = types.ObjectNull(diagnosticsAttrTypes()) + } + + amo := advancedMonitoringOptionsModel{ + HttpMonitoringEndpoint: httpEndpointObj, + Diagnostics: diagnosticsObj, + } + + obj, diags := types.ObjectValueFrom(ctx, advancedMonitoringOptionsAttrTypes(), amo) + if diags.HasError() { + return diags + } + model.AdvancedMonitoringOptions = obj + return nil +} + +// httpMonitoringEndpointAPIResult is the return type for convertHttpMonitoringEndpointToAPI +// This type alias matches the inline struct expected by kbapi.PostFleetAgentPoliciesJSONRequestBody.MonitoringHttp +type httpMonitoringEndpointAPIResult = struct { + Buffer *struct { + Enabled *bool `json:"enabled,omitempty"` + } `json:"buffer,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + Host *string `json:"host,omitempty"` + Port *float32 `json:"port,omitempty"` +} + +// convertHttpMonitoringEndpointToAPI converts the HTTP monitoring endpoint config to API format +func (model *agentPolicyModel) convertHttpMonitoringEndpointToAPI(ctx context.Context) (*httpMonitoringEndpointAPIResult, *bool) { + if !utils.IsKnown(model.AdvancedMonitoringOptions) { + return nil, nil + } + + var amo advancedMonitoringOptionsModel + model.AdvancedMonitoringOptions.As(ctx, &amo, basetypes.ObjectAsOptions{}) + + if !utils.IsKnown(amo.HttpMonitoringEndpoint) { + return nil, nil + } + + var http httpMonitoringEndpointModel + amo.HttpMonitoringEndpoint.As(ctx, &http, basetypes.ObjectAsOptions{}) + + enabled := http.Enabled.ValueBool() + host := http.Host.ValueString() + port := float32(http.Port.ValueInt32()) + bufferEnabled := http.BufferEnabled.ValueBool() + pprofEnabled := http.PprofEnabled.ValueBool() + + result := &httpMonitoringEndpointAPIResult{ + Enabled: &enabled, + Host: &host, + Port: &port, + Buffer: &struct { + Enabled *bool `json:"enabled,omitempty"` + }{ + Enabled: &bufferEnabled, + }, + } + + return result, &pprofEnabled +} + +// diagnosticsAPIResult is the return type for convertDiagnosticsToAPI +// This type alias matches the inline struct expected by kbapi.PostFleetAgentPoliciesJSONRequestBody.MonitoringDiagnostics +type diagnosticsAPIResult = struct { + Limit *struct { + Burst *float32 `json:"burst,omitempty"` + Interval *string `json:"interval,omitempty"` + } `json:"limit,omitempty"` + Uploader *struct { + InitDur *string `json:"init_dur,omitempty"` + MaxDur *string `json:"max_dur,omitempty"` + MaxRetries *float32 `json:"max_retries,omitempty"` + } `json:"uploader,omitempty"` +} + +// convertDiagnosticsToAPI converts the diagnostics config to API format +func (model *agentPolicyModel) convertDiagnosticsToAPI(ctx context.Context) *diagnosticsAPIResult { + if !utils.IsKnown(model.AdvancedMonitoringOptions) { + return nil + } + + var amo advancedMonitoringOptionsModel + model.AdvancedMonitoringOptions.As(ctx, &amo, basetypes.ObjectAsOptions{}) + + if !utils.IsKnown(amo.Diagnostics) { + return nil + } + + var diag diagnosticsModel + amo.Diagnostics.As(ctx, &diag, basetypes.ObjectAsOptions{}) + + result := &diagnosticsAPIResult{} + + if utils.IsKnown(diag.RateLimits) { + var rateLimits rateLimitsModel + diag.RateLimits.As(ctx, &rateLimits, basetypes.ObjectAsOptions{}) + interval := rateLimits.Interval.ValueString() + burst := float32(rateLimits.Burst.ValueInt32()) + result.Limit = &struct { + Burst *float32 `json:"burst,omitempty"` + Interval *string `json:"interval,omitempty"` + }{ + Interval: &interval, + Burst: &burst, + } + } + + if utils.IsKnown(diag.FileUploader) { + var fileUploader fileUploaderModel + diag.FileUploader.As(ctx, &fileUploader, basetypes.ObjectAsOptions{}) + initDur := fileUploader.InitDuration.ValueString() + maxDur := fileUploader.BackoffDuration.ValueString() + maxRetries := float32(fileUploader.MaxRetries.ValueInt32()) + result.Uploader = &struct { + InitDur *string `json:"init_dur,omitempty"` + MaxDur *string `json:"max_dur,omitempty"` + MaxRetries *float32 `json:"max_retries,omitempty"` + }{ + InitDur: &initDur, + MaxDur: &maxDur, + MaxRetries: &maxRetries, + } + } + + return result +} diff --git a/internal/fleet/agent_policy/models_advanced_monitoring_test.go b/internal/fleet/agent_policy/models_advanced_monitoring_test.go new file mode 100644 index 000000000..ca6003321 --- /dev/null +++ b/internal/fleet/agent_policy/models_advanced_monitoring_test.go @@ -0,0 +1,263 @@ +package agent_policy + +import ( + "context" + "testing" + + "github.com/elastic/terraform-provider-elasticstack/internal/utils/customtypes" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stretchr/testify/assert" +) + +func TestConvertHttpMonitoringEndpointToAPI(t *testing.T) { + ctx := context.Background() + + // Helper to create types.Object from httpMonitoringEndpointModel + createHttpEndpointObject := func(m httpMonitoringEndpointModel) types.Object { + obj, _ := types.ObjectValueFrom(ctx, httpMonitoringEndpointAttrTypes(), m) + return obj + } + + // Helper to create types.Object from advancedMonitoringOptionsModel + createAmoObject := func(httpEndpoint types.Object) types.Object { + amo := advancedMonitoringOptionsModel{ + HttpMonitoringEndpoint: httpEndpoint, + Diagnostics: types.ObjectNull(diagnosticsAttrTypes()), + } + obj, _ := types.ObjectValueFrom(ctx, advancedMonitoringOptionsAttrTypes(), amo) + return obj + } + + tests := []struct { + name string + amo types.Object + wantHttp bool + wantPprof bool + wantPprofValue bool + }{ + { + name: "null advanced monitoring options returns nil", + amo: types.ObjectNull(advancedMonitoringOptionsAttrTypes()), + wantHttp: false, + }, + { + name: "null http monitoring endpoint returns nil", + amo: createAmoObject(types.ObjectNull(httpMonitoringEndpointAttrTypes())), + wantHttp: false, + }, + { + name: "default values are sent (allows reset to defaults)", + amo: createAmoObject(createHttpEndpointObject(httpMonitoringEndpointModel{ + Enabled: types.BoolValue(false), + Host: types.StringValue("localhost"), + Port: types.Int32Value(6791), + BufferEnabled: types.BoolValue(false), + PprofEnabled: types.BoolValue(false), + })), + wantHttp: true, + wantPprof: true, + wantPprofValue: false, + }, + { + name: "enabled http endpoint returns values", + amo: createAmoObject(createHttpEndpointObject(httpMonitoringEndpointModel{ + Enabled: types.BoolValue(true), + Host: types.StringValue("localhost"), + Port: types.Int32Value(6791), + BufferEnabled: types.BoolValue(false), + PprofEnabled: types.BoolValue(false), + })), + wantHttp: true, + wantPprof: true, + wantPprofValue: false, + }, + { + name: "custom port returns values", + amo: createAmoObject(createHttpEndpointObject(httpMonitoringEndpointModel{ + Enabled: types.BoolValue(false), + Host: types.StringValue("localhost"), + Port: types.Int32Value(8080), + BufferEnabled: types.BoolValue(false), + PprofEnabled: types.BoolValue(false), + })), + wantHttp: true, + wantPprof: true, + wantPprofValue: false, + }, + { + name: "pprof enabled returns values", + amo: createAmoObject(createHttpEndpointObject(httpMonitoringEndpointModel{ + Enabled: types.BoolValue(true), + Host: types.StringValue("localhost"), + Port: types.Int32Value(6791), + BufferEnabled: types.BoolValue(false), + PprofEnabled: types.BoolValue(true), + })), + wantHttp: true, + wantPprof: true, + wantPprofValue: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + model := &agentPolicyModel{ + AdvancedMonitoringOptions: tt.amo, + } + + gotHttp, gotPprof := model.convertHttpMonitoringEndpointToAPI(ctx) + + if !tt.wantHttp { + assert.Nil(t, gotHttp) + assert.Nil(t, gotPprof) + return + } + + assert.NotNil(t, gotHttp) + if tt.wantPprof { + assert.NotNil(t, gotPprof) + assert.Equal(t, tt.wantPprofValue, *gotPprof) + } + }) + } +} + +func TestConvertDiagnosticsToAPI(t *testing.T) { + ctx := context.Background() + + // Helper to create types.Object from rateLimitsModel + createRateLimitsObject := func(m rateLimitsModel) types.Object { + obj, _ := types.ObjectValueFrom(ctx, rateLimitsAttrTypes(), m) + return obj + } + + // Helper to create types.Object from fileUploaderModel + createFileUploaderObject := func(m fileUploaderModel) types.Object { + obj, _ := types.ObjectValueFrom(ctx, fileUploaderAttrTypes(), m) + return obj + } + + // Helper to create types.Object from diagnosticsModel + createDiagnosticsObject := func(rateLimits, fileUploader types.Object) types.Object { + diag := diagnosticsModel{ + RateLimits: rateLimits, + FileUploader: fileUploader, + } + obj, _ := types.ObjectValueFrom(ctx, diagnosticsAttrTypes(), diag) + return obj + } + + // Helper to create types.Object from advancedMonitoringOptionsModel + createAmoObject := func(diagnostics types.Object) types.Object { + amo := advancedMonitoringOptionsModel{ + HttpMonitoringEndpoint: types.ObjectNull(httpMonitoringEndpointAttrTypes()), + Diagnostics: diagnostics, + } + obj, _ := types.ObjectValueFrom(ctx, advancedMonitoringOptionsAttrTypes(), amo) + return obj + } + + tests := []struct { + name string + amo types.Object + wantDiag bool + wantRateLimits bool + wantUploader bool + }{ + { + name: "null advanced monitoring options returns nil", + amo: types.ObjectNull(advancedMonitoringOptionsAttrTypes()), + wantDiag: false, + }, + { + name: "null diagnostics returns nil", + amo: createAmoObject(types.ObjectNull(diagnosticsAttrTypes())), + wantDiag: false, + }, + { + name: "default rate limits values are sent (allows reset to defaults)", + amo: createAmoObject(createDiagnosticsObject( + createRateLimitsObject(rateLimitsModel{ + Interval: customtypes.NewDurationValue("1m"), + Burst: types.Int32Value(1), + }), + types.ObjectNull(fileUploaderAttrTypes()), + )), + wantDiag: true, + wantRateLimits: true, + }, + { + name: "default uploader values are sent (allows reset to defaults)", + amo: createAmoObject(createDiagnosticsObject( + types.ObjectNull(rateLimitsAttrTypes()), + createFileUploaderObject(fileUploaderModel{ + InitDuration: customtypes.NewDurationValue("1s"), + BackoffDuration: customtypes.NewDurationValue("1m"), + MaxRetries: types.Int32Value(10), + }), + )), + wantDiag: true, + wantUploader: true, + }, + { + name: "custom rate limits interval returns values", + amo: createAmoObject(createDiagnosticsObject( + createRateLimitsObject(rateLimitsModel{ + Interval: customtypes.NewDurationValue("2m"), + Burst: types.Int32Value(1), + }), + types.ObjectNull(fileUploaderAttrTypes()), + )), + wantDiag: true, + wantRateLimits: true, + }, + { + name: "custom rate limits burst returns values", + amo: createAmoObject(createDiagnosticsObject( + createRateLimitsObject(rateLimitsModel{ + Interval: customtypes.NewDurationValue("1m"), + Burst: types.Int32Value(5), + }), + types.ObjectNull(fileUploaderAttrTypes()), + )), + wantDiag: true, + wantRateLimits: true, + }, + { + name: "custom uploader max_retries returns values", + amo: createAmoObject(createDiagnosticsObject( + types.ObjectNull(rateLimitsAttrTypes()), + createFileUploaderObject(fileUploaderModel{ + InitDuration: customtypes.NewDurationValue("1s"), + BackoffDuration: customtypes.NewDurationValue("1m"), + MaxRetries: types.Int32Value(20), + }), + )), + wantDiag: true, + wantUploader: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + model := &agentPolicyModel{ + AdvancedMonitoringOptions: tt.amo, + } + + got := model.convertDiagnosticsToAPI(ctx) + + if !tt.wantDiag { + assert.Nil(t, got) + return + } + + assert.NotNil(t, got) + if tt.wantRateLimits { + assert.NotNil(t, got.Limit) + } + if tt.wantUploader { + assert.NotNil(t, got.Uploader) + } + }) + } +} diff --git a/internal/fleet/agent_policy/models_advanced_settings.go b/internal/fleet/agent_policy/models_advanced_settings.go new file mode 100644 index 000000000..8e15a4b08 --- /dev/null +++ b/internal/fleet/agent_policy/models_advanced_settings.go @@ -0,0 +1,235 @@ +package agent_policy + +import ( + "context" + + "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" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +type advancedSettingsModel struct { + LoggingLevel types.String `tfsdk:"logging_level"` + LoggingToFiles types.Bool `tfsdk:"logging_to_files"` + LoggingFilesInterval customtypes.Duration `tfsdk:"logging_files_interval"` + LoggingFilesKeepfiles types.Int32 `tfsdk:"logging_files_keepfiles"` + LoggingFilesRotateeverybytes types.Int64 `tfsdk:"logging_files_rotateeverybytes"` + LoggingMetricsPeriod customtypes.Duration `tfsdk:"logging_metrics_period"` + GoMaxProcs types.Int32 `tfsdk:"go_max_procs"` + DownloadTimeout customtypes.Duration `tfsdk:"download_timeout"` + DownloadTargetDirectory types.String `tfsdk:"download_target_directory"` + MonitoringRuntimeExperimental types.String `tfsdk:"monitoring_runtime_experimental"` +} + +// advancedSettingsAttrTypes returns attribute types for advanced_settings pulled from the schema +func advancedSettingsAttrTypes() map[string]attr.Type { + return getSchema().Attributes["advanced_settings"].GetType().(attr.TypeWithAttributeTypes).AttributeTypes() +} + +// advancedSettingsAPIResult is the return type for convertAdvancedSettingsToAPI +type advancedSettingsAPIResult = struct { + AgentDownloadTargetDirectory interface{} `json:"agent_download_target_directory,omitempty"` + AgentDownloadTimeout interface{} `json:"agent_download_timeout,omitempty"` + AgentInternal interface{} `json:"agent_internal,omitempty"` + AgentLimitsGoMaxProcs interface{} `json:"agent_limits_go_max_procs,omitempty"` + AgentLoggingFilesInterval interface{} `json:"agent_logging_files_interval,omitempty"` + AgentLoggingFilesKeepfiles interface{} `json:"agent_logging_files_keepfiles,omitempty"` + AgentLoggingFilesRotateeverybytes interface{} `json:"agent_logging_files_rotateeverybytes,omitempty"` + AgentLoggingLevel interface{} `json:"agent_logging_level,omitempty"` + AgentLoggingMetricsPeriod interface{} `json:"agent_logging_metrics_period,omitempty"` + AgentLoggingToFiles interface{} `json:"agent_logging_to_files,omitempty"` + AgentMonitoringRuntimeExperimental interface{} `json:"agent_monitoring_runtime_experimental,omitempty"` +} + +// populateAdvancedSettingsFromAPI populates the advanced settings from API response +func (model *agentPolicyModel) populateAdvancedSettingsFromAPI(ctx context.Context, data *kbapi.AgentPolicy) diag.Diagnostics { + if data.AdvancedSettings == nil { + model.AdvancedSettings = types.ObjectNull(advancedSettingsAttrTypes()) + return nil + } + + settings := advancedSettingsModel{} + + // Logging level + if data.AdvancedSettings.AgentLoggingLevel != nil { + if str, ok := data.AdvancedSettings.AgentLoggingLevel.(string); ok { + settings.LoggingLevel = types.StringValue(str) + } else { + settings.LoggingLevel = types.StringNull() + } + } else { + settings.LoggingLevel = types.StringNull() + } + + // Logging to files + if data.AdvancedSettings.AgentLoggingToFiles != nil { + if b, ok := data.AdvancedSettings.AgentLoggingToFiles.(bool); ok { + settings.LoggingToFiles = types.BoolValue(b) + } else { + settings.LoggingToFiles = types.BoolNull() + } + } else { + settings.LoggingToFiles = types.BoolNull() + } + + // Logging files interval + if data.AdvancedSettings.AgentLoggingFilesInterval != nil { + if str, ok := data.AdvancedSettings.AgentLoggingFilesInterval.(string); ok { + settings.LoggingFilesInterval = customtypes.NewDurationValue(str) + } else { + settings.LoggingFilesInterval = customtypes.NewDurationNull() + } + } else { + settings.LoggingFilesInterval = customtypes.NewDurationNull() + } + + // Logging files keepfiles + if data.AdvancedSettings.AgentLoggingFilesKeepfiles != nil { + if f, ok := data.AdvancedSettings.AgentLoggingFilesKeepfiles.(float64); ok { + settings.LoggingFilesKeepfiles = types.Int32Value(int32(f)) + } else { + settings.LoggingFilesKeepfiles = types.Int32Null() + } + } else { + settings.LoggingFilesKeepfiles = types.Int32Null() + } + + // Logging files rotateeverybytes + if data.AdvancedSettings.AgentLoggingFilesRotateeverybytes != nil { + if f, ok := data.AdvancedSettings.AgentLoggingFilesRotateeverybytes.(float64); ok { + settings.LoggingFilesRotateeverybytes = types.Int64Value(int64(f)) + } else { + settings.LoggingFilesRotateeverybytes = types.Int64Null() + } + } else { + settings.LoggingFilesRotateeverybytes = types.Int64Null() + } + + // Logging metrics period + if data.AdvancedSettings.AgentLoggingMetricsPeriod != nil { + if str, ok := data.AdvancedSettings.AgentLoggingMetricsPeriod.(string); ok { + settings.LoggingMetricsPeriod = customtypes.NewDurationValue(str) + } else { + settings.LoggingMetricsPeriod = customtypes.NewDurationNull() + } + } else { + settings.LoggingMetricsPeriod = customtypes.NewDurationNull() + } + + // Go max procs + if data.AdvancedSettings.AgentLimitsGoMaxProcs != nil { + if f, ok := data.AdvancedSettings.AgentLimitsGoMaxProcs.(float64); ok { + settings.GoMaxProcs = types.Int32Value(int32(f)) + } else { + settings.GoMaxProcs = types.Int32Null() + } + } else { + settings.GoMaxProcs = types.Int32Null() + } + + // Download timeout + if data.AdvancedSettings.AgentDownloadTimeout != nil { + if str, ok := data.AdvancedSettings.AgentDownloadTimeout.(string); ok { + settings.DownloadTimeout = customtypes.NewDurationValue(str) + } else { + settings.DownloadTimeout = customtypes.NewDurationNull() + } + } else { + settings.DownloadTimeout = customtypes.NewDurationNull() + } + + // Download target directory + if data.AdvancedSettings.AgentDownloadTargetDirectory != nil { + if str, ok := data.AdvancedSettings.AgentDownloadTargetDirectory.(string); ok { + settings.DownloadTargetDirectory = types.StringValue(str) + } else { + settings.DownloadTargetDirectory = types.StringNull() + } + } else { + settings.DownloadTargetDirectory = types.StringNull() + } + + // Monitoring runtime experimental + if data.AdvancedSettings.AgentMonitoringRuntimeExperimental != nil { + if str, ok := data.AdvancedSettings.AgentMonitoringRuntimeExperimental.(string); ok { + settings.MonitoringRuntimeExperimental = types.StringValue(str) + } else { + settings.MonitoringRuntimeExperimental = types.StringNull() + } + } else { + settings.MonitoringRuntimeExperimental = types.StringNull() + } + + obj, diags := types.ObjectValueFrom(ctx, advancedSettingsAttrTypes(), settings) + if diags.HasError() { + return diags + } + model.AdvancedSettings = obj + return nil +} + +// convertAdvancedSettingsToAPI converts the advanced settings config to API format +func (model *agentPolicyModel) convertAdvancedSettingsToAPI(ctx context.Context) *advancedSettingsAPIResult { + if !utils.IsKnown(model.AdvancedSettings) { + return nil + } + + var settings advancedSettingsModel + model.AdvancedSettings.As(ctx, &settings, basetypes.ObjectAsOptions{}) + + // Check if any values are set + hasValues := utils.IsKnown(settings.LoggingLevel) || + utils.IsKnown(settings.LoggingToFiles) || + utils.IsKnown(settings.LoggingFilesInterval) || + utils.IsKnown(settings.LoggingFilesKeepfiles) || + utils.IsKnown(settings.LoggingFilesRotateeverybytes) || + utils.IsKnown(settings.LoggingMetricsPeriod) || + utils.IsKnown(settings.GoMaxProcs) || + utils.IsKnown(settings.DownloadTimeout) || + utils.IsKnown(settings.DownloadTargetDirectory) || + utils.IsKnown(settings.MonitoringRuntimeExperimental) + + if !hasValues { + return nil + } + + result := &advancedSettingsAPIResult{} + + if utils.IsKnown(settings.LoggingLevel) { + result.AgentLoggingLevel = settings.LoggingLevel.ValueString() + } + if utils.IsKnown(settings.LoggingToFiles) { + result.AgentLoggingToFiles = settings.LoggingToFiles.ValueBool() + } + if utils.IsKnown(settings.LoggingFilesInterval) { + result.AgentLoggingFilesInterval = settings.LoggingFilesInterval.ValueString() + } + if utils.IsKnown(settings.LoggingFilesKeepfiles) { + result.AgentLoggingFilesKeepfiles = settings.LoggingFilesKeepfiles.ValueInt32() + } + if utils.IsKnown(settings.LoggingFilesRotateeverybytes) { + result.AgentLoggingFilesRotateeverybytes = settings.LoggingFilesRotateeverybytes.ValueInt64() + } + if utils.IsKnown(settings.LoggingMetricsPeriod) { + result.AgentLoggingMetricsPeriod = settings.LoggingMetricsPeriod.ValueString() + } + if utils.IsKnown(settings.GoMaxProcs) { + result.AgentLimitsGoMaxProcs = settings.GoMaxProcs.ValueInt32() + } + if utils.IsKnown(settings.DownloadTimeout) { + result.AgentDownloadTimeout = settings.DownloadTimeout.ValueString() + } + if utils.IsKnown(settings.DownloadTargetDirectory) { + result.AgentDownloadTargetDirectory = settings.DownloadTargetDirectory.ValueString() + } + if utils.IsKnown(settings.MonitoringRuntimeExperimental) { + result.AgentMonitoringRuntimeExperimental = settings.MonitoringRuntimeExperimental.ValueString() + } + + return result +} diff --git a/internal/fleet/agent_policy/models_advanced_settings_test.go b/internal/fleet/agent_policy/models_advanced_settings_test.go new file mode 100644 index 000000000..8c97e90da --- /dev/null +++ b/internal/fleet/agent_policy/models_advanced_settings_test.go @@ -0,0 +1,135 @@ +package agent_policy + +import ( + "context" + "testing" + + "github.com/elastic/terraform-provider-elasticstack/internal/utils/customtypes" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stretchr/testify/assert" +) + +func TestConvertAdvancedSettingsToAPI(t *testing.T) { + ctx := context.Background() + + createAdvancedSettingsObject := func(settings advancedSettingsModel) types.Object { + obj, _ := types.ObjectValueFrom(ctx, advancedSettingsAttrTypes(), settings) + return obj + } + + tests := []struct { + name string + advancedSettings types.Object + wantNil bool + checkResult func(t *testing.T, result *advancedSettingsAPIResult) + }{ + { + name: "null advanced_settings returns nil", + advancedSettings: types.ObjectNull(advancedSettingsAttrTypes()), + wantNil: true, + }, + { + name: "all null values returns nil", + advancedSettings: createAdvancedSettingsObject(advancedSettingsModel{ + LoggingLevel: types.StringNull(), + LoggingToFiles: types.BoolNull(), + LoggingFilesInterval: customtypes.NewDurationNull(), + LoggingFilesKeepfiles: types.Int32Null(), + LoggingFilesRotateeverybytes: types.Int64Null(), + LoggingMetricsPeriod: customtypes.NewDurationNull(), + GoMaxProcs: types.Int32Null(), + DownloadTimeout: customtypes.NewDurationNull(), + DownloadTargetDirectory: types.StringNull(), + MonitoringRuntimeExperimental: types.StringNull(), + }), + wantNil: true, + }, + { + name: "logging_level set returns value", + advancedSettings: createAdvancedSettingsObject(advancedSettingsModel{ + LoggingLevel: types.StringValue("debug"), + LoggingToFiles: types.BoolNull(), + LoggingFilesInterval: customtypes.NewDurationNull(), + LoggingFilesKeepfiles: types.Int32Null(), + LoggingFilesRotateeverybytes: types.Int64Null(), + LoggingMetricsPeriod: customtypes.NewDurationNull(), + GoMaxProcs: types.Int32Null(), + DownloadTimeout: customtypes.NewDurationNull(), + DownloadTargetDirectory: types.StringNull(), + MonitoringRuntimeExperimental: types.StringNull(), + }), + wantNil: false, + checkResult: func(t *testing.T, result *advancedSettingsAPIResult) { + assert.Equal(t, "debug", result.AgentLoggingLevel) + assert.Nil(t, result.AgentLoggingToFiles) + }, + }, + { + name: "go_max_procs set returns value", + advancedSettings: createAdvancedSettingsObject(advancedSettingsModel{ + LoggingLevel: types.StringNull(), + LoggingToFiles: types.BoolNull(), + LoggingFilesInterval: customtypes.NewDurationNull(), + LoggingFilesKeepfiles: types.Int32Null(), + LoggingFilesRotateeverybytes: types.Int64Null(), + LoggingMetricsPeriod: customtypes.NewDurationNull(), + GoMaxProcs: types.Int32Value(4), + DownloadTimeout: customtypes.NewDurationNull(), + DownloadTargetDirectory: types.StringNull(), + MonitoringRuntimeExperimental: types.StringNull(), + }), + wantNil: false, + checkResult: func(t *testing.T, result *advancedSettingsAPIResult) { + assert.Equal(t, int32(4), result.AgentLimitsGoMaxProcs) + }, + }, + { + name: "multiple values set returns all values", + advancedSettings: createAdvancedSettingsObject(advancedSettingsModel{ + LoggingLevel: types.StringValue("info"), + LoggingToFiles: types.BoolValue(true), + LoggingFilesInterval: customtypes.NewDurationValue("30s"), + LoggingFilesKeepfiles: types.Int32Value(7), + LoggingFilesRotateeverybytes: types.Int64Value(10485760), + LoggingMetricsPeriod: customtypes.NewDurationValue("1m"), + GoMaxProcs: types.Int32Value(2), + DownloadTimeout: customtypes.NewDurationValue("2h"), + DownloadTargetDirectory: types.StringValue("/tmp/elastic"), + MonitoringRuntimeExperimental: types.StringValue(""), + }), + wantNil: false, + checkResult: func(t *testing.T, result *advancedSettingsAPIResult) { + assert.Equal(t, "info", result.AgentLoggingLevel) + assert.Equal(t, true, result.AgentLoggingToFiles) + assert.Equal(t, "30s", result.AgentLoggingFilesInterval) + assert.Equal(t, int32(7), result.AgentLoggingFilesKeepfiles) + assert.Equal(t, int64(10485760), result.AgentLoggingFilesRotateeverybytes) + assert.Equal(t, "1m", result.AgentLoggingMetricsPeriod) + assert.Equal(t, int32(2), result.AgentLimitsGoMaxProcs) + assert.Equal(t, "2h", result.AgentDownloadTimeout) + assert.Equal(t, "/tmp/elastic", result.AgentDownloadTargetDirectory) + assert.Equal(t, "", result.AgentMonitoringRuntimeExperimental) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + model := &agentPolicyModel{ + AdvancedSettings: tt.advancedSettings, + } + + got := model.convertAdvancedSettingsToAPI(ctx) + + if tt.wantNil { + assert.Nil(t, got) + return + } + + assert.NotNil(t, got) + if tt.checkResult != nil { + tt.checkResult(t, got) + } + }) + } +} diff --git a/internal/fleet/agent_policy/models_test.go b/internal/fleet/agent_policy/models_test.go index bc88a7aa7..a49b30610 100644 --- a/internal/fleet/agent_policy/models_test.go +++ b/internal/fleet/agent_policy/models_test.go @@ -1,10 +1,8 @@ package agent_policy import ( - "context" "testing" - "github.com/elastic/terraform-provider-elasticstack/internal/utils/customtypes" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stretchr/testify/assert" ) @@ -145,252 +143,3 @@ func TestConvertHostNameFormatToAgentFeature(t *testing.T) { }) } } - -func TestConvertHttpMonitoringEndpointToAPI(t *testing.T) { - ctx := context.Background() - - // Helper to create types.Object from httpMonitoringEndpointModel - createHttpEndpointObject := func(m httpMonitoringEndpointModel) types.Object { - obj, _ := types.ObjectValueFrom(ctx, httpMonitoringEndpointAttrTypes(), m) - return obj - } - - // Helper to create types.Object from advancedMonitoringOptionsModel - createAmoObject := func(httpEndpoint types.Object) types.Object { - amo := advancedMonitoringOptionsModel{ - HttpMonitoringEndpoint: httpEndpoint, - Diagnostics: types.ObjectNull(diagnosticsAttrTypes()), - } - obj, _ := types.ObjectValueFrom(ctx, advancedMonitoringOptionsAttrTypes(), amo) - return obj - } - - tests := []struct { - name string - amo types.Object - wantHttp bool - wantPprof bool - wantPprofValue bool - }{ - { - name: "null advanced monitoring options returns nil", - amo: types.ObjectNull(advancedMonitoringOptionsAttrTypes()), - wantHttp: false, - }, - { - name: "null http monitoring endpoint returns nil", - amo: createAmoObject(types.ObjectNull(httpMonitoringEndpointAttrTypes())), - wantHttp: false, - }, - { - name: "default values returns nil (omit from payload)", - amo: createAmoObject(createHttpEndpointObject(httpMonitoringEndpointModel{ - Enabled: types.BoolValue(false), - Host: types.StringValue("localhost"), - Port: types.Int32Value(6791), - BufferEnabled: types.BoolValue(false), - PprofEnabled: types.BoolValue(false), - })), - wantHttp: false, - }, - { - name: "enabled http endpoint returns values", - amo: createAmoObject(createHttpEndpointObject(httpMonitoringEndpointModel{ - Enabled: types.BoolValue(true), - Host: types.StringValue("localhost"), - Port: types.Int32Value(6791), - BufferEnabled: types.BoolValue(false), - PprofEnabled: types.BoolValue(false), - })), - wantHttp: true, - wantPprof: true, - wantPprofValue: false, - }, - { - name: "custom port returns values", - amo: createAmoObject(createHttpEndpointObject(httpMonitoringEndpointModel{ - Enabled: types.BoolValue(false), - Host: types.StringValue("localhost"), - Port: types.Int32Value(8080), - BufferEnabled: types.BoolValue(false), - PprofEnabled: types.BoolValue(false), - })), - wantHttp: true, - wantPprof: true, - wantPprofValue: false, - }, - { - name: "pprof enabled returns values", - amo: createAmoObject(createHttpEndpointObject(httpMonitoringEndpointModel{ - Enabled: types.BoolValue(true), - Host: types.StringValue("localhost"), - Port: types.Int32Value(6791), - BufferEnabled: types.BoolValue(false), - PprofEnabled: types.BoolValue(true), - })), - wantHttp: true, - wantPprof: true, - wantPprofValue: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - model := &agentPolicyModel{ - AdvancedMonitoringOptions: tt.amo, - } - - gotHttp, gotPprof := model.convertHttpMonitoringEndpointToAPI(ctx) - - if !tt.wantHttp { - assert.Nil(t, gotHttp) - assert.Nil(t, gotPprof) - return - } - - assert.NotNil(t, gotHttp) - if tt.wantPprof { - assert.NotNil(t, gotPprof) - assert.Equal(t, tt.wantPprofValue, *gotPprof) - } - }) - } -} - -func TestConvertDiagnosticsToAPI(t *testing.T) { - ctx := context.Background() - - // Helper to create types.Object from rateLimitsModel - createRateLimitsObject := func(m rateLimitsModel) types.Object { - obj, _ := types.ObjectValueFrom(ctx, rateLimitsAttrTypes(), m) - return obj - } - - // Helper to create types.Object from fileUploaderModel - createFileUploaderObject := func(m fileUploaderModel) types.Object { - obj, _ := types.ObjectValueFrom(ctx, fileUploaderAttrTypes(), m) - return obj - } - - // Helper to create types.Object from diagnosticsModel - createDiagnosticsObject := func(rateLimits, fileUploader types.Object) types.Object { - diag := diagnosticsModel{ - RateLimits: rateLimits, - FileUploader: fileUploader, - } - obj, _ := types.ObjectValueFrom(ctx, diagnosticsAttrTypes(), diag) - return obj - } - - // Helper to create types.Object from advancedMonitoringOptionsModel - createAmoObject := func(diagnostics types.Object) types.Object { - amo := advancedMonitoringOptionsModel{ - HttpMonitoringEndpoint: types.ObjectNull(httpMonitoringEndpointAttrTypes()), - Diagnostics: diagnostics, - } - obj, _ := types.ObjectValueFrom(ctx, advancedMonitoringOptionsAttrTypes(), amo) - return obj - } - - tests := []struct { - name string - amo types.Object - wantDiag bool - wantRateLimits bool - wantUploader bool - }{ - { - name: "null advanced monitoring options returns nil", - amo: types.ObjectNull(advancedMonitoringOptionsAttrTypes()), - wantDiag: false, - }, - { - name: "null diagnostics returns nil", - amo: createAmoObject(types.ObjectNull(diagnosticsAttrTypes())), - wantDiag: false, - }, - { - name: "default rate limits values returns nil (omit from payload)", - amo: createAmoObject(createDiagnosticsObject( - createRateLimitsObject(rateLimitsModel{ - Interval: customtypes.NewDurationValue("1m"), - Burst: types.Int32Value(1), - }), - types.ObjectNull(fileUploaderAttrTypes()), - )), - wantDiag: false, - }, - { - name: "default uploader values returns nil (omit from payload)", - amo: createAmoObject(createDiagnosticsObject( - types.ObjectNull(rateLimitsAttrTypes()), - createFileUploaderObject(fileUploaderModel{ - InitDuration: customtypes.NewDurationValue("1s"), - BackoffDuration: customtypes.NewDurationValue("1m"), - MaxRetries: types.Int32Value(10), - }), - )), - wantDiag: false, - }, - { - name: "custom rate limits interval returns values", - amo: createAmoObject(createDiagnosticsObject( - createRateLimitsObject(rateLimitsModel{ - Interval: customtypes.NewDurationValue("2m"), - Burst: types.Int32Value(1), - }), - types.ObjectNull(fileUploaderAttrTypes()), - )), - wantDiag: true, - wantRateLimits: true, - }, - { - name: "custom rate limits burst returns values", - amo: createAmoObject(createDiagnosticsObject( - createRateLimitsObject(rateLimitsModel{ - Interval: customtypes.NewDurationValue("1m"), - Burst: types.Int32Value(5), - }), - types.ObjectNull(fileUploaderAttrTypes()), - )), - wantDiag: true, - wantRateLimits: true, - }, - { - name: "custom uploader max_retries returns values", - amo: createAmoObject(createDiagnosticsObject( - types.ObjectNull(rateLimitsAttrTypes()), - createFileUploaderObject(fileUploaderModel{ - InitDuration: customtypes.NewDurationValue("1s"), - BackoffDuration: customtypes.NewDurationValue("1m"), - MaxRetries: types.Int32Value(20), - }), - )), - wantDiag: true, - wantUploader: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - model := &agentPolicyModel{ - AdvancedMonitoringOptions: tt.amo, - } - - got := model.convertDiagnosticsToAPI(ctx) - - if !tt.wantDiag { - assert.Nil(t, got) - return - } - - assert.NotNil(t, got) - if tt.wantRateLimits { - assert.NotNil(t, got.Limit) - } - if tt.wantUploader { - assert.NotNil(t, got.Uploader) - } - }) - } -} diff --git a/internal/fleet/agent_policy/resource.go b/internal/fleet/agent_policy/resource.go index cd88d47fa..3cda6999e 100644 --- a/internal/fleet/agent_policy/resource.go +++ b/internal/fleet/agent_policy/resource.go @@ -27,6 +27,7 @@ var ( MinVersionRequiredVersions = version.Must(version.NewVersion("9.1.0")) MinVersionAgentFeatures = version.Must(version.NewVersion("8.7.0")) MinVersionAdvancedMonitoring = version.Must(version.NewVersion("8.16.0")) + MinVersionAdvancedSettings = version.Must(version.NewVersion("8.17.0")) ) // NewResource is a helper function to simplify the provider implementation. @@ -93,6 +94,11 @@ func (r *agentPolicyResource) buildFeatures(ctx context.Context) (features, diag return features{}, diagutil.FrameworkDiagsFromSDK(diags) } + supportsAdvancedSettings, diags := r.client.EnforceMinVersion(ctx, MinVersionAdvancedSettings) + if diags.HasError() { + return features{}, diagutil.FrameworkDiagsFromSDK(diags) + } + return features{ SupportsGlobalDataTags: supportsGDT, SupportsSupportsAgentless: supportsSupportsAgentless, @@ -102,5 +108,6 @@ func (r *agentPolicyResource) buildFeatures(ctx context.Context) (features, diag SupportsRequiredVersions: supportsRequiredVersions, SupportsAgentFeatures: supportsAgentFeatures, SupportsAdvancedMonitoring: supportsAdvancedMonitoring, + SupportsAdvancedSettings: supportsAdvancedSettings, }, nil } diff --git a/internal/fleet/agent_policy/schema.go b/internal/fleet/agent_policy/schema.go index 043c552ae..0194c32ad 100644 --- a/internal/fleet/agent_policy/schema.go +++ b/internal/fleet/agent_policy/schema.go @@ -15,9 +15,12 @@ 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/int32default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" "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/objectplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -117,12 +120,18 @@ func getSchema() schema.Schema { Computed: true, Optional: true, CustomType: customtypes.DurationType{}, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "unenrollment_timeout": schema.StringAttribute{ Description: "The unenrollment timeout for the agent policy. If an agent is inactive for this period, it will be automatically unenrolled. Supports duration strings (e.g., '30s', '2m', '1h').", Computed: true, Optional: true, CustomType: customtypes.DurationType{}, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "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}", @@ -158,6 +167,9 @@ func getSchema() schema.Schema { ElementType: types.StringType, Optional: true, Computed: true, + PlanModifiers: []planmodifier.Set{ + setplanmodifier.UseStateForUnknown(), + }, }, "required_versions": schema.MapAttribute{ 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.", @@ -173,15 +185,110 @@ func getSchema() schema.Schema { ), }, }, + "advanced_settings": schema.SingleNestedAttribute{ + Description: "Advanced agent settings for logging, resource limits, and downloads. These settings configure the behavior of Elastic Agents enrolled in this policy.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.UseStateForUnknown(), + }, + Attributes: map[string]schema.Attribute{ + "logging_level": schema.StringAttribute{ + Description: "Logging level for the agent. Valid values: debug, info, warning, error.", + Optional: true, + Computed: true, + Default: stringdefault.StaticString("info"), + Validators: []validator.String{ + stringvalidator.OneOf("debug", "info", "warning", "error"), + }, + }, + "logging_to_files": schema.BoolAttribute{ + Description: "Enable logging to files.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + }, + "logging_files_interval": schema.StringAttribute{ + Description: "Interval for log file rotation (e.g., '30s', '1m', '1h').", + Optional: true, + Computed: true, + CustomType: customtypes.DurationType{}, + Default: stringdefault.StaticString("30s"), + }, + "logging_files_keepfiles": schema.Int32Attribute{ + Description: "Number of rotated log files to keep.", + Optional: true, + Computed: true, + Default: int32default.StaticInt32(7), + Validators: []validator.Int32{ + int32validator.AtLeast(0), + }, + }, + "logging_files_rotateeverybytes": schema.Int64Attribute{ + Description: "Rotate log files when they reach this size in bytes.", + Optional: true, + Computed: true, + Default: int64default.StaticInt64(10485760), + }, + "logging_metrics_period": schema.StringAttribute{ + Description: "Period for logging agent metrics (e.g., '30s', '1m').", + Optional: true, + Computed: true, + CustomType: customtypes.DurationType{}, + Default: stringdefault.StaticString("30s"), + }, + "go_max_procs": schema.Int32Attribute{ + Description: "Maximum number of CPUs that the agent can use (GOMAXPROCS). Set to 0 to use all available CPUs.", + Optional: true, + Computed: true, + Default: int32default.StaticInt32(0), + Validators: []validator.Int32{ + int32validator.AtLeast(0), + }, + }, + "download_timeout": schema.StringAttribute{ + Description: "Timeout for downloading agent updates (e.g., '2h', '30m').", + Optional: true, + Computed: true, + CustomType: customtypes.DurationType{}, + Default: stringdefault.StaticString("2h"), + }, + "download_target_directory": schema.StringAttribute{ + Description: "Target directory for downloading agent updates.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "monitoring_runtime_experimental": schema.StringAttribute{ + Description: "Experimental runtime monitoring mode. Valid values: '' (empty string to disable), 'process', 'otel'.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + Validators: []validator.String{ + stringvalidator.OneOf("", "process", "otel"), + }, + }, + }, + }, "advanced_monitoring_options": schema.SingleNestedAttribute{ Description: "Advanced monitoring options for the agent policy. Includes HTTP monitoring endpoint configuration and diagnostic settings.", Optional: true, Computed: true, + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.UseStateForUnknown(), + }, Attributes: map[string]schema.Attribute{ "http_monitoring_endpoint": schema.SingleNestedAttribute{ Description: "HTTP monitoring endpoint configuration for agent health checks and liveness probes.", Optional: true, Computed: true, + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.UseStateForUnknown(), + }, Attributes: map[string]schema.Attribute{ "enabled": schema.BoolAttribute{ Description: "Enable the HTTP monitoring endpoint. When enabled, exposes a /liveness endpoint for health checks.", @@ -222,11 +329,17 @@ func getSchema() schema.Schema { Description: "Diagnostic settings for rate limiting and file upload behavior.", Optional: true, Computed: true, + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.UseStateForUnknown(), + }, Attributes: map[string]schema.Attribute{ "rate_limits": schema.SingleNestedAttribute{ Description: "Rate limiting configuration for diagnostics requests from Fleet.", Optional: true, Computed: true, + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.UseStateForUnknown(), + }, Attributes: map[string]schema.Attribute{ "interval": schema.StringAttribute{ Description: "Rate limiting interval for diagnostics requests (e.g., '1m', '30s').", @@ -247,6 +360,9 @@ func getSchema() schema.Schema { Description: "Diagnostic file upload retry configuration.", Optional: true, Computed: true, + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.UseStateForUnknown(), + }, Attributes: map[string]schema.Attribute{ "init_duration": schema.StringAttribute{ Description: "Initial duration before the first retry attempt (e.g., '1s', '500ms').", diff --git a/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedMonitoring/remove_advanced_monitoring/main.tf b/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedMonitoring/remove_advanced_monitoring/main.tf index f65310ff3..4fbbb521c 100644 --- a/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedMonitoring/remove_advanced_monitoring/main.tf +++ b/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedMonitoring/remove_advanced_monitoring/main.tf @@ -10,5 +10,7 @@ resource "elasticstack_fleet_agent_policy" "test_policy" { monitor_logs = true monitor_metrics = true skip_destroy = var.skip_destroy + + # advanced_monitoring_options removed entirely - UseStateForUnknown preserves state } diff --git a/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedMonitoring/remove_diagnostics/main.tf b/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedMonitoring/set_to_defaults/main.tf similarity index 56% rename from internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedMonitoring/remove_diagnostics/main.tf rename to internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedMonitoring/set_to_defaults/main.tf index f2b046008..43fd27bb4 100644 --- a/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedMonitoring/remove_diagnostics/main.tf +++ b/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedMonitoring/set_to_defaults/main.tf @@ -6,18 +6,17 @@ provider "elasticstack" { resource "elasticstack_fleet_agent_policy" "test_policy" { name = var.policy_name namespace = "default" - description = "Test Agent Policy - Diagnostics Removed" + description = "Test Agent Policy with Default Advanced Monitoring" monitor_logs = true monitor_metrics = true skip_destroy = var.skip_destroy + # Empty nested blocks - schema defaults are applied for leaf attributes advanced_monitoring_options = { - http_monitoring_endpoint = { - enabled = true - host = "0.0.0.0" - port = 8080 - buffer_enabled = true - pprof_enabled = true + http_monitoring_endpoint = {} + diagnostics = { + rate_limits = {} + file_uploader = {} } } } diff --git a/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedMonitoring/remove_diagnostics/variables.tf b/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedMonitoring/set_to_defaults/variables.tf similarity index 100% rename from internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedMonitoring/remove_diagnostics/variables.tf rename to internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedMonitoring/set_to_defaults/variables.tf diff --git a/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedSettings/create_with_logging/main.tf b/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedSettings/create_with_logging/main.tf new file mode 100644 index 000000000..745d3c28c --- /dev/null +++ b/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedSettings/create_with_logging/main.tf @@ -0,0 +1,19 @@ +provider "elasticstack" { + elasticsearch {} + kibana {} +} + +resource "elasticstack_fleet_agent_policy" "test_policy" { + name = var.policy_name + namespace = "default" + description = "Test Agent Policy with Advanced Settings" + monitor_logs = true + monitor_metrics = true + + advanced_settings = { + logging_level = "debug" + logging_to_files = true + go_max_procs = 2 + } +} + diff --git a/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedSettings/create_with_logging/variables.tf b/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedSettings/create_with_logging/variables.tf new file mode 100644 index 000000000..4f40a3ea5 --- /dev/null +++ b/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedSettings/create_with_logging/variables.tf @@ -0,0 +1,4 @@ +variable "policy_name" { + type = string +} + diff --git a/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedSettings/remove_settings/main.tf b/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedSettings/remove_settings/main.tf new file mode 100644 index 000000000..1e318d76f --- /dev/null +++ b/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedSettings/remove_settings/main.tf @@ -0,0 +1,15 @@ +provider "elasticstack" { + elasticsearch {} + kibana {} +} + +resource "elasticstack_fleet_agent_policy" "test_policy" { + name = var.policy_name + namespace = "default" + description = "Test Agent Policy without Advanced Settings" + monitor_logs = true + monitor_metrics = true + + # advanced_settings removed entirely - UseStateForUnknown preserves state +} + diff --git a/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedSettings/remove_settings/variables.tf b/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedSettings/remove_settings/variables.tf new file mode 100644 index 000000000..4f40a3ea5 --- /dev/null +++ b/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedSettings/remove_settings/variables.tf @@ -0,0 +1,4 @@ +variable "policy_name" { + type = string +} + diff --git a/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedSettings/set_to_defaults/main.tf b/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedSettings/set_to_defaults/main.tf new file mode 100644 index 000000000..4487bd99a --- /dev/null +++ b/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedSettings/set_to_defaults/main.tf @@ -0,0 +1,16 @@ +provider "elasticstack" { + elasticsearch {} + kibana {} +} + +resource "elasticstack_fleet_agent_policy" "test_policy" { + name = var.policy_name + namespace = "default" + description = "Test Agent Policy with Default Advanced Settings" + monitor_logs = true + monitor_metrics = true + + # Empty block - schema defaults are applied for flat attributes + advanced_settings = {} +} + diff --git a/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedSettings/set_to_defaults/variables.tf b/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedSettings/set_to_defaults/variables.tf new file mode 100644 index 000000000..4f40a3ea5 --- /dev/null +++ b/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedSettings/set_to_defaults/variables.tf @@ -0,0 +1,4 @@ +variable "policy_name" { + type = string +} + diff --git a/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedSettings/update_settings/main.tf b/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedSettings/update_settings/main.tf new file mode 100644 index 000000000..d2173b781 --- /dev/null +++ b/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedSettings/update_settings/main.tf @@ -0,0 +1,22 @@ +provider "elasticstack" { + elasticsearch {} + kibana {} +} + +resource "elasticstack_fleet_agent_policy" "test_policy" { + name = var.policy_name + namespace = "default" + description = "Test Agent Policy with Advanced Settings" + monitor_logs = true + monitor_metrics = true + + advanced_settings = { + logging_level = "info" + logging_to_files = true + logging_files_keepfiles = 7 + logging_files_rotateeverybytes = 10485760 + go_max_procs = 4 + download_target_directory = "/tmp/elastic-agent" + } +} + diff --git a/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedSettings/update_settings/variables.tf b/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedSettings/update_settings/variables.tf new file mode 100644 index 000000000..4f40a3ea5 --- /dev/null +++ b/internal/fleet/agent_policy/testdata/TestAccResourceAgentPolicyWithAdvancedSettings/update_settings/variables.tf @@ -0,0 +1,4 @@ +variable "policy_name" { + type = string +} + diff --git a/internal/fleet/agent_policy/version_test.go b/internal/fleet/agent_policy/version_test.go index 1f876d3b4..f4840bbaa 100644 --- a/internal/fleet/agent_policy/version_test.go +++ b/internal/fleet/agent_policy/version_test.go @@ -309,6 +309,96 @@ func TestAdvancedMonitoringVersionValidation(t *testing.T) { } } +func TestAdvancedSettingsVersionValidation(t *testing.T) { + ctx := context.Background() + + // Create advanced_settings object with some values set + advancedSettings, _ := types.ObjectValueFrom(ctx, advancedSettingsAttrTypes(), advancedSettingsModel{ + LoggingLevel: types.StringValue("debug"), + LoggingToFiles: types.BoolValue(true), + LoggingFilesInterval: customtypes.NewDurationNull(), + LoggingFilesKeepfiles: types.Int32Value(7), + LoggingFilesRotateeverybytes: types.Int64Null(), + LoggingMetricsPeriod: customtypes.NewDurationNull(), + GoMaxProcs: types.Int32Value(4), + DownloadTimeout: customtypes.NewDurationNull(), + DownloadTargetDirectory: types.StringNull(), + MonitoringRuntimeExperimental: types.StringNull(), + }) + + // Test case where advanced_settings is not supported (older version) + model := &agentPolicyModel{ + Name: types.StringValue("test"), + Namespace: types.StringValue("default"), + AdvancedSettings: advancedSettings, + } + + // Create features with advanced_settings NOT supported + feat := features{ + SupportsAdvancedSettings: false, + } + + // Test toAPICreateModel - should return error when advanced_settings is used but not supported + _, diags := model.toAPICreateModel(ctx, feat) + if !diags.HasError() { + t.Error("Expected error when using advanced_settings 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 advanced_settings is used but not supported + _, diags = model.toAPIUpdateModel(ctx, feat, nil) + if !diags.HasError() { + t.Error("Expected error when using advanced_settings on unsupported version in update, but got none") + } + + // Test case where advanced_settings IS supported (newer version) + featSupported := features{ + SupportsAdvancedSettings: true, + } + + // Test toAPICreateModel - should NOT return error when advanced_settings is supported + _, diags = model.toAPICreateModel(ctx, featSupported) + if diags.HasError() { + t.Errorf("Did not expect error when using advanced_settings on supported version: %v", diags) + } + + // Test toAPIUpdateModel - should NOT return error when advanced_settings is supported + _, diags = model.toAPIUpdateModel(ctx, featSupported, nil) + if diags.HasError() { + t.Errorf("Did not expect error when using advanced_settings on supported version in update: %v", diags) + } + + // Test case where advanced_settings is not set (should not cause validation errors) + modelWithoutAdvancedSettings := &agentPolicyModel{ + Name: types.StringValue("test"), + Namespace: types.StringValue("default"), + // AdvancedSettings is not set (null/unknown) + } + + // Test toAPICreateModel - should NOT return error when advanced_settings is not set, even on unsupported version + _, diags = modelWithoutAdvancedSettings.toAPICreateModel(ctx, feat) + if diags.HasError() { + t.Errorf("Did not expect error when advanced_settings is not set: %v", diags) + } + + // Test toAPIUpdateModel - should NOT return error when advanced_settings is not set, even on unsupported version + _, diags = modelWithoutAdvancedSettings.toAPIUpdateModel(ctx, feat, nil) + if diags.HasError() { + t.Errorf("Did not expect error when advanced_settings is not set in update: %v", diags) + } +} + func TestMinVersionSpaceIds(t *testing.T) { // Test that the MinVersionSpaceIds constant is set correctly expected := "9.1.0"