Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
67 changes: 67 additions & 0 deletions internal/kibana/synthetics/acc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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 = "testnamespace"
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 = `

Expand Down Expand Up @@ -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", "testnamespace"),
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 s.RootModule().Resources[httpMonitorId].Primary.Attributes["id"], nil
},
ImportStateVerify: true,
},
},
})
}
3 changes: 2 additions & 1 deletion internal/kibana/synthetics/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@ func (r *Resource) Create(ctx context.Context, request resource.CreateRequest, r
}

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())
return
}

plan, diags = plan.toModelV0(ctx, result)
plan, diags = plan.toModelV0(ctx, result, namespace)
response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
Expand Down
8 changes: 4 additions & 4 deletions internal/kibana/synthetics/parameter/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion internal/kibana/synthetics/read.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
89 changes: 85 additions & 4 deletions internal/kibana/synthetics/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import (
"context"
"encoding/json"
"fmt"
"regexp"
"strconv"

"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"
Expand Down Expand Up @@ -99,6 +101,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"`
Expand Down Expand Up @@ -145,10 +148,26 @@ func monitorConfigSchema() schema.Schema {
"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`",
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
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.",

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated the space_id MarkdownDescription to correctly describe its purpose and relationship with the namespace attribute in commit d75c5fb.

Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
planmodifiers.StringUseDefaultIfUnknown("default"),
requiresReplaceIfSpaceIdChanged(),
},
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(),
},
Validators: []validator.String{
stringvalidator.RegexMatches(
regexp.MustCompile(`^[^*\\/?\"<>|\s,#:-]*$`),
"namespace must not contain any of the following characters: *, \\, /, ?, \", <, >, |, whitespace, ,, #, :, or -",
),
},
Computed: true,
},
"schedule": schema.Int64Attribute{
Expand Down Expand Up @@ -566,7 +585,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{}
Expand Down Expand Up @@ -640,7 +659,7 @@ func (v *tfModelV0) toModelV0(ctx context.Context, api *kbapi.SyntheticsMonitor)
}

resourceID := clients.CompositeId{
ClusterId: api.Namespace,
ClusterId: spaceID,
ResourceId: string(api.Id),
}

Expand All @@ -652,7 +671,8 @@ 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,
PrivateLocations: StringSliceValue(privateLocLabels),
Expand Down Expand Up @@ -873,6 +893,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()),
Expand All @@ -883,7 +909,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
Expand Down Expand Up @@ -1047,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'",
)
}
Loading
Loading