From fcd90c49555e82821da45aa67cb37cdb9f4651e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Jul 2025 02:07:36 +0000 Subject: [PATCH 01/10] Initial plan From 430e4d3731d9a952aa1dc68bd922d08eb9985cad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Jul 2025 02:26:02 +0000 Subject: [PATCH 02/10] Add namespace support to synthetics monitor resource Co-authored-by: tobio <444668+tobio@users.noreply.github.com> --- CHANGELOG.md | 1 + internal/kibana/synthetics/acc_test.go | 67 +++++++++++++++++++++++ internal/kibana/synthetics/create.go | 7 ++- internal/kibana/synthetics/schema.go | 19 ++++++- internal/kibana/synthetics/schema_test.go | 12 ++++ internal/kibana/synthetics/update.go | 7 ++- 6 files changed, 110 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77f88e222..ca178293e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - Add support for `timeslice_metric_indicator` in `elasticstack_kibana_slo` ([#1195](https://github.com/elastic/terraform-provider-elasticstack/pull/1195)) - Add `elasticstack_elasticsearch_ingest_processor_reroute` data source ([#678](https://github.com/elastic/terraform-provider-elasticstack/issues/678)) +- Add `namespace` attribute to `elasticstack_kibana_synthetics_monitor` resource to support setting data stream namespace independently from `space_id` ([#1164](https://github.com/elastic/terraform-provider-elasticstack/issues/1164)) ## [0.11.16] - 2025-07-09 diff --git a/internal/kibana/synthetics/acc_test.go b/internal/kibana/synthetics/acc_test.go index dca2ed096..d3c79b6d9 100644 --- a/internal/kibana/synthetics/acc_test.go +++ b/internal/kibana/synthetics/acc_test.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/go-version" sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) var ( @@ -53,6 +54,34 @@ resource "elasticstack_kibana_synthetics_monitor" "%s" { ipv6 = false } } +` + httpMonitorConfigWithNamespace = ` + +resource "elasticstack_kibana_synthetics_monitor" "%s" { + name = "TestHttpMonitorResource - %s" + space_id = "testacc" + namespace = "test-namespace" + schedule = 5 + private_locations = [elasticstack_kibana_synthetics_private_location.%s.label] + enabled = true + tags = ["a", "b"] + alert = { + status = { + enabled = true + } + tls = { + enabled = true + } + } + service_name = "test apm service" + timeout = 30 + http = { + url = "http://localhost:5601" + mode = "any" + ipv4 = true + ipv6 = false + } +} ` httpMonitorSslConfig = ` @@ -842,3 +871,41 @@ resource "elasticstack_kibana_synthetics_private_location" "%s" { return resourceId, provider + config } + +func TestSyntheticMonitorHTTPResourceWithNamespace(t *testing.T) { + + name := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlphaNum) + id := "http-monitor-namespace" + httpMonitorId, config := testMonitorConfig(id, httpMonitorConfigWithNamespace, name) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.Providers, + Steps: []resource.TestStep{ + // Create and Read http monitor with explicit namespace + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minKibanaVersion), + Config: config, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(httpMonitorId, "id"), + resource.TestCheckResourceAttr(httpMonitorId, "name", "TestHttpMonitorResource - "+name), + resource.TestCheckResourceAttr(httpMonitorId, "space_id", "testacc"), + resource.TestCheckResourceAttr(httpMonitorId, "namespace", "test-namespace"), + resource.TestCheckResourceAttr(httpMonitorId, "schedule", "5"), + resource.TestCheckResourceAttr(httpMonitorId, "enabled", "true"), + resource.TestCheckResourceAttr(httpMonitorId, "http.url", "http://localhost:5601"), + ), + }, + // Import + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minKibanaVersion), + ResourceName: httpMonitorId, + ImportState: true, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + return fmt.Sprintf("%s/%s", "testacc", s.RootModule().Resources[httpMonitorId].Primary.Attributes["id"]), nil + }, + ImportStateVerify: true, + }, + }, + }) +} diff --git a/internal/kibana/synthetics/create.go b/internal/kibana/synthetics/create.go index 830b9aa73..125d06bef 100644 --- a/internal/kibana/synthetics/create.go +++ b/internal/kibana/synthetics/create.go @@ -26,7 +26,12 @@ func (r *Resource) Create(ctx context.Context, request resource.CreateRequest, r return } - namespace := plan.SpaceID.ValueString() + // Use namespace if explicitly set, otherwise fall back to space_id + namespace := plan.Namespace.ValueString() + if namespace == "" || plan.Namespace.IsNull() || plan.Namespace.IsUnknown() { + namespace = plan.SpaceID.ValueString() + } + result, err := kibanaClient.KibanaSynthetics.Monitor.Add(ctx, input.config, input.fields, namespace) if err != nil { response.Diagnostics.AddError(fmt.Sprintf("Failed to create Kibana monitor `%s`, namespace %s", input.config.Name, namespace), err.Error()) diff --git a/internal/kibana/synthetics/schema.go b/internal/kibana/synthetics/schema.go index e77fce16e..dd9e5fdf3 100644 --- a/internal/kibana/synthetics/schema.go +++ b/internal/kibana/synthetics/schema.go @@ -99,6 +99,7 @@ type tfModelV0 struct { ID types.String `tfsdk:"id"` Name types.String `tfsdk:"name"` SpaceID types.String `tfsdk:"space_id"` + Namespace types.String `tfsdk:"namespace"` Schedule types.Int64 `tfsdk:"schedule"` Locations []types.String `tfsdk:"locations"` PrivateLocations []types.String `tfsdk:"private_locations"` @@ -151,6 +152,15 @@ func monitorConfigSchema() schema.Schema { }, Computed: true, }, + "namespace": schema.StringAttribute{ + MarkdownDescription: "The data stream namespace. If not specified, defaults to the value of space_id. The namespace must be lowercase and not contain spaces. The namespace must not include any of the following characters: *, \\, /, ?, \", <, >, |, whitespace, ,, #, :, or -.", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + stringplanmodifier.RequiresReplace(), + }, + Computed: true, + }, "schedule": schema.Int64Attribute{ Optional: true, MarkdownDescription: "The monitor’s schedule in minutes. Supported values are 1, 3, 5, 10, 15, 30, 60, 120 and 240.", @@ -653,6 +663,7 @@ func (v *tfModelV0) toModelV0(ctx context.Context, api *kbapi.SyntheticsMonitor) ID: types.StringValue(resourceID.String()), Name: types.StringValue(api.Name), SpaceID: types.StringValue(api.Namespace), + Namespace: types.StringValue(api.Namespace), Schedule: types.Int64Value(schedule), Locations: v.Locations, PrivateLocations: StringSliceValue(privateLocLabels), @@ -873,6 +884,12 @@ func (v *tfModelV0) toSyntheticsMonitorConfig(ctx context.Context) (*kbapi.Synth return nil, dg } + // Use namespace if explicitly set, otherwise fall back to space_id + namespace := v.Namespace.ValueString() + if namespace == "" || v.Namespace.IsNull() || v.Namespace.IsUnknown() { + namespace = v.SpaceID.ValueString() + } + return &kbapi.SyntheticsMonitorConfig{ Name: v.Name.ValueString(), Schedule: kbapi.MonitorSchedule(v.Schedule.ValueInt64()), @@ -883,7 +900,7 @@ func (v *tfModelV0) toSyntheticsMonitorConfig(ctx context.Context) (*kbapi.Synth Alert: toTFAlertConfig(ctx, v.Alert), APMServiceName: v.APMServiceName.ValueString(), TimeoutSeconds: int(v.TimeoutSeconds.ValueInt64()), - Namespace: v.SpaceID.ValueString(), + Namespace: namespace, Params: params, RetestOnFailure: v.RetestOnFailure.ValueBoolPointer(), }, diag.Diagnostics{} //dg diff --git a/internal/kibana/synthetics/schema_test.go b/internal/kibana/synthetics/schema_test.go index 1711530bf..ef909e35e 100644 --- a/internal/kibana/synthetics/schema_test.go +++ b/internal/kibana/synthetics/schema_test.go @@ -49,6 +49,7 @@ func TestToModelV0(t *testing.T) { ID: types.StringValue("/"), Name: types.StringValue(""), SpaceID: types.StringValue(""), + Namespace: types.StringValue(""), Schedule: types.Int64Value(0), APMServiceName: types.StringValue(""), TimeoutSeconds: types.Int64Value(0), @@ -83,6 +84,7 @@ func TestToModelV0(t *testing.T) { ID: types.StringValue("/"), Name: types.StringValue(""), SpaceID: types.StringValue(""), + Namespace: types.StringValue(""), Schedule: types.Int64Value(0), APMServiceName: types.StringValue(""), TimeoutSeconds: types.Int64Value(0), @@ -111,6 +113,7 @@ func TestToModelV0(t *testing.T) { ID: types.StringValue("/"), Name: types.StringValue(""), SpaceID: types.StringValue(""), + Namespace: types.StringValue(""), Schedule: types.Int64Value(0), APMServiceName: types.StringValue(""), TimeoutSeconds: types.Int64Value(0), @@ -130,6 +133,7 @@ func TestToModelV0(t *testing.T) { ID: types.StringValue("/"), Name: types.StringValue(""), SpaceID: types.StringValue(""), + Namespace: types.StringValue(""), Schedule: types.Int64Value(0), APMServiceName: types.StringValue(""), TimeoutSeconds: types.Int64Value(0), @@ -320,6 +324,7 @@ func TestToModelV0(t *testing.T) { ID: types.StringValue("default/test-id-icmp"), Name: types.StringValue("test-name-icmp"), SpaceID: types.StringValue("default"), + Namespace: types.StringValue("default"), Schedule: types.Int64Value(5), Locations: nil, PrivateLocations: []types.String{types.StringValue("test private location")}, @@ -375,6 +380,7 @@ func TestToModelV0(t *testing.T) { ID: types.StringValue("default/test-id-browser"), Name: types.StringValue("test-name-browser"), SpaceID: types.StringValue("default"), + Namespace: types.StringValue("default"), Schedule: types.Int64Value(5), Locations: nil, PrivateLocations: []types.String{types.StringValue("test private location")}, @@ -457,6 +463,7 @@ func TestToKibanaAPIRequest(t *testing.T) { ID: types.StringValue("test-id-http"), Name: types.StringValue("test-name-http"), SpaceID: types.StringValue("default"), + Namespace: types.StringValue("default"), Schedule: types.Int64Value(5), Locations: []types.String{types.StringValue("us_east")}, PrivateLocations: []types.String{types.StringValue("test private location")}, @@ -533,6 +540,7 @@ func TestToKibanaAPIRequest(t *testing.T) { ID: types.StringValue("test-id-tcp"), Name: types.StringValue("test-name-tcp"), SpaceID: types.StringValue("default"), + Namespace: types.StringValue("default"), Schedule: types.Int64Value(5), Locations: []types.String{types.StringValue("us_east")}, PrivateLocations: nil, @@ -597,6 +605,7 @@ func TestToKibanaAPIRequest(t *testing.T) { ID: types.StringValue("test-id-icmp"), Name: types.StringValue("test-name-icmp"), SpaceID: types.StringValue("default"), + Namespace: types.StringValue("default"), Schedule: types.Int64Value(5), Locations: []types.String{types.StringValue("us_east")}, PrivateLocations: nil, @@ -637,6 +646,7 @@ func TestToKibanaAPIRequest(t *testing.T) { ID: types.StringValue("test-id-browser"), Name: types.StringValue("test-name-browser"), SpaceID: types.StringValue("default"), + Namespace: types.StringValue("default"), Schedule: types.Int64Value(5), Locations: []types.String{types.StringValue("us_east")}, PrivateLocations: nil, @@ -767,6 +777,7 @@ func TestToModelV0MergeAttributes(t *testing.T) { ID: types.StringValue("/"), Name: types.StringValue(""), SpaceID: types.StringValue(""), + Namespace: types.StringValue(""), Schedule: types.Int64Value(0), APMServiceName: types.StringValue(""), TimeoutSeconds: types.Int64Value(0), @@ -801,6 +812,7 @@ func TestToModelV0MergeAttributes(t *testing.T) { ID: types.StringValue("/"), Name: types.StringValue(""), SpaceID: types.StringValue(""), + Namespace: types.StringValue(""), Schedule: types.Int64Value(0), APMServiceName: types.StringValue(""), TimeoutSeconds: types.Int64Value(0), diff --git a/internal/kibana/synthetics/update.go b/internal/kibana/synthetics/update.go index c7544622b..0b774e548 100644 --- a/internal/kibana/synthetics/update.go +++ b/internal/kibana/synthetics/update.go @@ -33,7 +33,12 @@ func (r *Resource) Update(ctx context.Context, request resource.UpdateRequest, r return } - namespace := plan.SpaceID.ValueString() + // Use namespace if explicitly set, otherwise fall back to space_id + namespace := plan.Namespace.ValueString() + if namespace == "" || plan.Namespace.IsNull() || plan.Namespace.IsUnknown() { + namespace = plan.SpaceID.ValueString() + } + result, err := kibanaClient.KibanaSynthetics.Monitor.Update(ctx, kbapi.MonitorID(monitorId.ResourceId), input.config, input.fields, namespace) if err != nil { response.Diagnostics.AddError(fmt.Sprintf("Failed to update Kibana monitor `%s`, namespace %s", input.config.Name, namespace), err.Error()) From 0b861138d8cd9b6a5122a94c21d12f0579083828 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Jul 2025 02:34:09 +0000 Subject: [PATCH 03/10] Fix unit tests for namespace support in synthetics monitor Co-authored-by: tobio <444668+tobio@users.noreply.github.com> --- internal/kibana/synthetics/schema_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/kibana/synthetics/schema_test.go b/internal/kibana/synthetics/schema_test.go index ef909e35e..ebfdc9587 100644 --- a/internal/kibana/synthetics/schema_test.go +++ b/internal/kibana/synthetics/schema_test.go @@ -195,6 +195,7 @@ func TestToModelV0(t *testing.T) { ID: types.StringValue("default/test-id-http"), Name: types.StringValue("test-name-http"), SpaceID: types.StringValue("default"), + Namespace: types.StringValue("default"), Schedule: types.Int64Value(5), Locations: []types.String{types.StringValue("us_east")}, PrivateLocations: []types.String{types.StringValue("test private location")}, @@ -265,6 +266,7 @@ func TestToModelV0(t *testing.T) { ID: types.StringValue("default/test-id-tcp"), Name: types.StringValue("test-name-tcp"), SpaceID: types.StringValue("default"), + Namespace: types.StringValue("default"), Schedule: types.Int64Value(5), Locations: nil, PrivateLocations: []types.String{types.StringValue("test private location")}, @@ -732,6 +734,7 @@ func TestToModelV0MergeAttributes(t *testing.T) { ID: types.StringValue("/"), Name: types.StringValue(""), SpaceID: types.StringValue(""), + Namespace: types.StringValue(""), Schedule: types.Int64Value(0), APMServiceName: types.StringValue(""), TimeoutSeconds: types.Int64Value(0), From e5820b3eeeb508f14de8d9c672dffc4dab960fb6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Jul 2025 04:15:32 +0000 Subject: [PATCH 04/10] Add namespace validator and fix invalid test namespaces Co-authored-by: tobio <444668+tobio@users.noreply.github.com> --- internal/kibana/synthetics/acc_test.go | 4 ++-- internal/kibana/synthetics/parameter/schema_test.go | 8 ++++---- internal/kibana/synthetics/schema.go | 7 +++++++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/internal/kibana/synthetics/acc_test.go b/internal/kibana/synthetics/acc_test.go index d3c79b6d9..94ea88876 100644 --- a/internal/kibana/synthetics/acc_test.go +++ b/internal/kibana/synthetics/acc_test.go @@ -60,7 +60,7 @@ resource "elasticstack_kibana_synthetics_monitor" "%s" { resource "elasticstack_kibana_synthetics_monitor" "%s" { name = "TestHttpMonitorResource - %s" space_id = "testacc" - namespace = "test-namespace" + namespace = "testnamespace" schedule = 5 private_locations = [elasticstack_kibana_synthetics_private_location.%s.label] enabled = true @@ -890,7 +890,7 @@ func TestSyntheticMonitorHTTPResourceWithNamespace(t *testing.T) { resource.TestCheckResourceAttrSet(httpMonitorId, "id"), resource.TestCheckResourceAttr(httpMonitorId, "name", "TestHttpMonitorResource - "+name), resource.TestCheckResourceAttr(httpMonitorId, "space_id", "testacc"), - resource.TestCheckResourceAttr(httpMonitorId, "namespace", "test-namespace"), + resource.TestCheckResourceAttr(httpMonitorId, "namespace", "testnamespace"), resource.TestCheckResourceAttr(httpMonitorId, "schedule", "5"), resource.TestCheckResourceAttr(httpMonitorId, "enabled", "true"), resource.TestCheckResourceAttr(httpMonitorId, "http.url", "http://localhost:5601"), diff --git a/internal/kibana/synthetics/parameter/schema_test.go b/internal/kibana/synthetics/parameter/schema_test.go index 1eae2009e..a01b795d2 100644 --- a/internal/kibana/synthetics/parameter/schema_test.go +++ b/internal/kibana/synthetics/parameter/schema_test.go @@ -18,7 +18,7 @@ func Test_roundtrip(t *testing.T) { { name: "only required fields", id: "id-1", - namespaces: []string{"ns-1"}, + namespaces: []string{"ns1"}, request: kboapi.SyntheticsParameterRequest{ Key: "key-1", Value: "value-1", @@ -39,7 +39,7 @@ func Test_roundtrip(t *testing.T) { { name: "only description", id: "id-3", - namespaces: []string{"ns-3"}, + namespaces: []string{"ns3"}, request: kboapi.SyntheticsParameterRequest{ Key: "key-3", Value: "value-3", @@ -49,7 +49,7 @@ func Test_roundtrip(t *testing.T) { { name: "only tags", id: "id-4", - namespaces: []string{"ns-4"}, + namespaces: []string{"ns4"}, request: kboapi.SyntheticsParameterRequest{ Key: "key-4", Value: "value-4", @@ -59,7 +59,7 @@ func Test_roundtrip(t *testing.T) { { name: "all namespaces", id: "id-5", - namespaces: []string{"ns-5"}, + namespaces: []string{"ns5"}, request: kboapi.SyntheticsParameterRequest{ Key: "key-5", Value: "value-5", diff --git a/internal/kibana/synthetics/schema.go b/internal/kibana/synthetics/schema.go index dd9e5fdf3..3d811a204 100644 --- a/internal/kibana/synthetics/schema.go +++ b/internal/kibana/synthetics/schema.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "regexp" "strconv" "github.com/disaster37/go-kibana-rest/v8/kbapi" @@ -159,6 +160,12 @@ func monitorConfigSchema() schema.Schema { stringplanmodifier.UseStateForUnknown(), stringplanmodifier.RequiresReplace(), }, + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile(`^[^*\\/?\"<>|\s,#:-]*$`), + "namespace must not contain any of the following characters: *, \\, /, ?, \", <, >, |, whitespace, ,, #, :, or -", + ), + }, Computed: true, }, "schedule": schema.Int64Attribute{ From 11d2ab5c93fb9c3d93fe9139565403bac18dafca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Jul 2025 06:15:27 +0000 Subject: [PATCH 05/10] Revert namespace usage in create/update operations to use space_id Co-authored-by: tobio <444668+tobio@users.noreply.github.com> --- internal/kibana/synthetics/create.go | 6 +----- internal/kibana/synthetics/update.go | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/internal/kibana/synthetics/create.go b/internal/kibana/synthetics/create.go index 125d06bef..57f099f29 100644 --- a/internal/kibana/synthetics/create.go +++ b/internal/kibana/synthetics/create.go @@ -26,11 +26,7 @@ func (r *Resource) Create(ctx context.Context, request resource.CreateRequest, r return } - // Use namespace if explicitly set, otherwise fall back to space_id - namespace := plan.Namespace.ValueString() - if namespace == "" || plan.Namespace.IsNull() || plan.Namespace.IsUnknown() { - namespace = plan.SpaceID.ValueString() - } + namespace := plan.SpaceID.ValueString() result, err := kibanaClient.KibanaSynthetics.Monitor.Add(ctx, input.config, input.fields, namespace) if err != nil { diff --git a/internal/kibana/synthetics/update.go b/internal/kibana/synthetics/update.go index 0b774e548..3a5552741 100644 --- a/internal/kibana/synthetics/update.go +++ b/internal/kibana/synthetics/update.go @@ -33,11 +33,7 @@ func (r *Resource) Update(ctx context.Context, request resource.UpdateRequest, r return } - // Use namespace if explicitly set, otherwise fall back to space_id - namespace := plan.Namespace.ValueString() - if namespace == "" || plan.Namespace.IsNull() || plan.Namespace.IsUnknown() { - namespace = plan.SpaceID.ValueString() - } + namespace := plan.SpaceID.ValueString() result, err := kibanaClient.KibanaSynthetics.Monitor.Update(ctx, kbapi.MonitorID(monitorId.ResourceId), input.config, input.fields, namespace) if err != nil { From ab53e8f42c5f5925574c62c19587daf70452059c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Jul 2025 07:01:00 +0000 Subject: [PATCH 06/10] Fix space ID handling in toModelV0 function Co-authored-by: tobio <444668+tobio@users.noreply.github.com> --- internal/kibana/synthetics/create.go | 2 +- internal/kibana/synthetics/read.go | 2 +- internal/kibana/synthetics/schema.go | 6 +++--- internal/kibana/synthetics/schema_test.go | 6 ++++-- internal/kibana/synthetics/update.go | 2 +- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/internal/kibana/synthetics/create.go b/internal/kibana/synthetics/create.go index 57f099f29..00a9e083f 100644 --- a/internal/kibana/synthetics/create.go +++ b/internal/kibana/synthetics/create.go @@ -34,7 +34,7 @@ func (r *Resource) Create(ctx context.Context, request resource.CreateRequest, r return } - plan, diags = plan.toModelV0(ctx, result) + plan, diags = plan.toModelV0(ctx, result, namespace) response.Diagnostics.Append(diags...) if response.Diagnostics.HasError() { return diff --git a/internal/kibana/synthetics/read.go b/internal/kibana/synthetics/read.go index e7a13db7f..a07fc9e4b 100644 --- a/internal/kibana/synthetics/read.go +++ b/internal/kibana/synthetics/read.go @@ -42,7 +42,7 @@ func (r *Resource) Read(ctx context.Context, request resource.ReadRequest, respo return } - state, diags = state.toModelV0(ctx, result) + state, diags = state.toModelV0(ctx, result, namespace) response.Diagnostics.Append(diags...) if response.Diagnostics.HasError() { return diff --git a/internal/kibana/synthetics/schema.go b/internal/kibana/synthetics/schema.go index 3d811a204..c29987767 100644 --- a/internal/kibana/synthetics/schema.go +++ b/internal/kibana/synthetics/schema.go @@ -583,7 +583,7 @@ func stringToInt64(v string) (int64, error) { return res, err } -func (v *tfModelV0) toModelV0(ctx context.Context, api *kbapi.SyntheticsMonitor) (*tfModelV0, diag.Diagnostics) { +func (v *tfModelV0) toModelV0(ctx context.Context, api *kbapi.SyntheticsMonitor, spaceID string) (*tfModelV0, diag.Diagnostics) { var schedule int64 var err error dg := diag.Diagnostics{} @@ -657,7 +657,7 @@ func (v *tfModelV0) toModelV0(ctx context.Context, api *kbapi.SyntheticsMonitor) } resourceID := clients.CompositeId{ - ClusterId: api.Namespace, + ClusterId: spaceID, ResourceId: string(api.Id), } @@ -669,7 +669,7 @@ func (v *tfModelV0) toModelV0(ctx context.Context, api *kbapi.SyntheticsMonitor) return &tfModelV0{ ID: types.StringValue(resourceID.String()), Name: types.StringValue(api.Name), - SpaceID: types.StringValue(api.Namespace), + SpaceID: types.StringValue(spaceID), Namespace: types.StringValue(api.Namespace), Schedule: types.Int64Value(schedule), Locations: v.Locations, diff --git a/internal/kibana/synthetics/schema_test.go b/internal/kibana/synthetics/schema_test.go index ebfdc9587..59bdb0261 100644 --- a/internal/kibana/synthetics/schema_test.go +++ b/internal/kibana/synthetics/schema_test.go @@ -406,7 +406,8 @@ func TestToModelV0(t *testing.T) { for _, tt := range testcases { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() - model, diag := tt.expected.toModelV0(ctx, &tt.input) + expectedSpaceID := tt.expected.SpaceID.ValueString() + model, diag := tt.expected.toModelV0(ctx, &tt.input, expectedSpaceID) assert.False(t, diag.HasError()) assert.Equal(t, &tt.expected, model) }) @@ -831,7 +832,8 @@ func TestToModelV0MergeAttributes(t *testing.T) { for _, tt := range testcases { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() - actual, diag := tt.state.toModelV0(ctx, &tt.input) + expectedSpaceID := tt.expected.SpaceID.ValueString() + actual, diag := tt.state.toModelV0(ctx, &tt.input, expectedSpaceID) assert.False(t, diag.HasError()) assert.NotNil(t, actual) assert.Equal(t, &tt.expected, actual) diff --git a/internal/kibana/synthetics/update.go b/internal/kibana/synthetics/update.go index 3a5552741..bc936566c 100644 --- a/internal/kibana/synthetics/update.go +++ b/internal/kibana/synthetics/update.go @@ -41,7 +41,7 @@ func (r *Resource) Update(ctx context.Context, request resource.UpdateRequest, r return } - plan, diags = plan.toModelV0(ctx, result) + plan, diags = plan.toModelV0(ctx, result, namespace) response.Diagnostics.Append(diags...) if response.Diagnostics.HasError() { return From f9589ad5fe59bd93bbe9b2283cdf80b28252d57c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Jul 2025 10:08:48 +0000 Subject: [PATCH 07/10] Add default plan modifier and RequiresReplaceIf for space_id, fix test import ID Co-authored-by: tobio <444668+tobio@users.noreply.github.com> --- internal/kibana/synthetics/acc_test.go | 2 +- internal/kibana/synthetics/schema.go | 59 +++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/internal/kibana/synthetics/acc_test.go b/internal/kibana/synthetics/acc_test.go index 94ea88876..4daa38015 100644 --- a/internal/kibana/synthetics/acc_test.go +++ b/internal/kibana/synthetics/acc_test.go @@ -902,7 +902,7 @@ func TestSyntheticMonitorHTTPResourceWithNamespace(t *testing.T) { ResourceName: httpMonitorId, ImportState: true, ImportStateIdFunc: func(s *terraform.State) (string, error) { - return fmt.Sprintf("%s/%s", "testacc", s.RootModule().Resources[httpMonitorId].Primary.Attributes["id"]), nil + return s.RootModule().Resources[httpMonitorId].Primary.Attributes["id"], nil }, ImportStateVerify: true, }, diff --git a/internal/kibana/synthetics/schema.go b/internal/kibana/synthetics/schema.go index c29987767..f5045ce02 100644 --- a/internal/kibana/synthetics/schema.go +++ b/internal/kibana/synthetics/schema.go @@ -10,6 +10,7 @@ import ( "github.com/disaster37/go-kibana-rest/v8/kbapi" "github.com/elastic/terraform-provider-elasticstack/internal/clients" "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/elastic/terraform-provider-elasticstack/internal/utils/planmodifiers" "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" @@ -149,7 +150,8 @@ func monitorConfigSchema() schema.Schema { Optional: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), - stringplanmodifier.RequiresReplace(), + planmodifiers.StringUseDefaultIfUnknown("default"), + requiresReplaceIfSpaceIdChanged(), }, Computed: true, }, @@ -1071,3 +1073,58 @@ func (v tfStatusConfigV0) toTfStatusConfigV0() *kbapi.SyntheticsStatusConfig { Enabled: v.Enabled.ValueBoolPointer(), } } + +func requiresReplaceIfSpaceIdChanged() planmodifier.String { + return stringplanmodifier.RequiresReplaceIf( + func(ctx context.Context, req planmodifier.StringRequest, resp *stringplanmodifier.RequiresReplaceIfFuncResponse) { + // Don't require replace if plan value is unknown + if req.PlanValue.IsUnknown() { + resp.RequiresReplace = false + return + } + + // Don't require replace if state value is null (creating) + if req.StateValue.IsNull() { + resp.RequiresReplace = false + return + } + + // Don't require replace if config value is null (not configured by user) + if req.ConfigValue.IsNull() { + resp.RequiresReplace = false + return + } + + stateValue := req.StateValue.ValueString() + planValue := req.PlanValue.ValueString() + + // Don't require replace if values are the same + if stateValue == planValue { + resp.RequiresReplace = false + return + } + + // Normalize empty and "default" values for comparison + normalizeValue := func(v string) string { + if v == "" || v == "default" { + return "default" + } + return v + } + + normalizedState := normalizeValue(stateValue) + normalizedPlan := normalizeValue(planValue) + + // Don't require replace if the change is between empty/"" and "default" + if normalizedState == normalizedPlan { + resp.RequiresReplace = false + return + } + + // Otherwise, require replace + resp.RequiresReplace = true + }, + "Requires replace if the space_id changes, except when changing between empty and 'default'", + "Requires replace if the space_id changes, except when changing between empty and 'default'", + ) +} From 4e631199f24696691b57175018b20070e3bad1a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Jul 2025 10:40:11 +0000 Subject: [PATCH 08/10] Generate updated documentation for namespace attribute Co-authored-by: tobio <444668+tobio@users.noreply.github.com> --- docs/resources/kibana_synthetics_monitor.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/resources/kibana_synthetics_monitor.md b/docs/resources/kibana_synthetics_monitor.md index fabdec368..af27544bc 100644 --- a/docs/resources/kibana_synthetics_monitor.md +++ b/docs/resources/kibana_synthetics_monitor.md @@ -72,6 +72,7 @@ resource "elasticstack_kibana_synthetics_monitor" "my_monitor" { - `http` (Attributes) HTTP Monitor specific fields (see [below for nested schema](#nestedatt--http)) - `icmp` (Attributes) ICMP Monitor specific fields (see [below for nested schema](#nestedatt--icmp)) - `locations` (List of String) Where to deploy the monitor. Monitors can be deployed in multiple locations so that you can detect differences in availability and response times across those locations. +- `namespace` (String) The data stream namespace. If not specified, defaults to the value of space_id. The namespace must be lowercase and not contain spaces. The namespace must not include any of the following characters: *, \, /, ?, ", <, >, |, whitespace, ,, #, :, or -. - `params` (String) Monitor parameters. Raw JSON object, use `jsonencode` function to represent JSON - `private_locations` (List of String) These Private Locations refer to locations hosted and managed by you, whereas locations are hosted by Elastic. You can specify a Private Location using the location’s name. - `retest_on_failure` (Boolean) Enable or disable retesting when a monitor fails. By default, monitors are automatically retested if the monitor goes from "up" to "down". If the result of the retest is also "down", an error will be created, and if configured, an alert sent. Then the monitor will resume running according to the defined schedule. Using retest_on_failure can reduce noise related to transient problems. Default: `true`. From d75c5fb3a2b9de22151121bc0ab9a360611d5101 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Jul 2025 11:46:01 +0000 Subject: [PATCH 09/10] Fix space_id MarkdownDescription to correctly describe its purpose Co-authored-by: tobio <444668+tobio@users.noreply.github.com> --- internal/kibana/synthetics/schema.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/kibana/synthetics/schema.go b/internal/kibana/synthetics/schema.go index f5045ce02..a8216a236 100644 --- a/internal/kibana/synthetics/schema.go +++ b/internal/kibana/synthetics/schema.go @@ -146,7 +146,7 @@ func monitorConfigSchema() schema.Schema { MarkdownDescription: "The monitor’s name.", }, "space_id": schema.StringAttribute{ - MarkdownDescription: "The namespace field should be lowercase and not contain spaces. The namespace must not include any of the following characters: *, \\, /, ?, \", <, >, |, whitespace, ,, #, :, or -. Default: `default`", + MarkdownDescription: "An identifier for the space. If space_id is not provided, the default space is used. This value is used for the default for `namespace` when that attribute is not provided.", Optional: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), From 81e9976d8bf5f6c0a89be6d75fc58030b6eb737e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Jul 2025 12:12:49 +0000 Subject: [PATCH 10/10] Update generated documentation for namespace support Co-authored-by: tobio <444668+tobio@users.noreply.github.com> --- docs/resources/kibana_synthetics_monitor.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/resources/kibana_synthetics_monitor.md b/docs/resources/kibana_synthetics_monitor.md index af27544bc..1ee3fdb25 100644 --- a/docs/resources/kibana_synthetics_monitor.md +++ b/docs/resources/kibana_synthetics_monitor.md @@ -78,7 +78,7 @@ resource "elasticstack_kibana_synthetics_monitor" "my_monitor" { - `retest_on_failure` (Boolean) Enable or disable retesting when a monitor fails. By default, monitors are automatically retested if the monitor goes from "up" to "down". If the result of the retest is also "down", an error will be created, and if configured, an alert sent. Then the monitor will resume running according to the defined schedule. Using retest_on_failure can reduce noise related to transient problems. Default: `true`. - `schedule` (Number) The monitor’s schedule in minutes. Supported values are 1, 3, 5, 10, 15, 30, 60, 120 and 240. - `service_name` (String) The APM service name. -- `space_id` (String) The namespace field should be lowercase and not contain spaces. The namespace must not include any of the following characters: *, \, /, ?, ", <, >, |, whitespace, ,, #, :, or -. Default: `default` +- `space_id` (String) An identifier for the space. If space_id is not provided, the default space is used. This value is used for the default for `namespace` when that attribute is not provided. - `tags` (List of String) An array of tags. - `tcp` (Attributes) TCP Monitor specific fields (see [below for nested schema](#nestedatt--tcp)) - `timeout` (Number) The monitor timeout in seconds, monitor will fail if it doesn’t complete within this time. Default: `16`