Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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))
- Add support for `supports_agentless` to `elasticstack_fleet_agent_policy` ([#1197](https://github.com/elastic/terraform-provider-elasticstack/pull/1197))
- Ignore `master_timeout` when targeting Serverless projects ([#1207](https://github.com/elastic/terraform-provider-elasticstack/pull/1207))

Expand Down
3 changes: 2 additions & 1 deletion docs/resources/kibana_synthetics_monitor.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,13 @@ 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`.
- `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`
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
91 changes: 86 additions & 5 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 @@ -143,12 +146,28 @@ 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(),
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