Skip to content

Commit 1a96a43

Browse files
RJirayatobio
andauthored
feat: add labels field to kibana synthetics monitor resource (#1360)
* feat: add labels field to kibana synthetics monitor resource - Add labels field to SyntheticsMonitorConfig and SyntheticsMonitor structs - Add labels field to terraform schema as MapAttribute - Add conversion functions for labels between API and terraform types - Update documentation with labels field description and example - Labels support key-value pairs for filtering and grouping monitors * Enforce a min server version for labels --------- Co-authored-by: Toby Brain <[email protected]>
1 parent df72da5 commit 1a96a43

File tree

9 files changed

+424
-5
lines changed

9 files changed

+424
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
## [Unreleased]
22

33
- Fix provider crash with `elasticstack_kibana_action_connector` when `config` or `secrets` was unset in 0.11.17 ([#1355](https://github.com/elastic/terraform-provider-elasticstack/pull/1355))
4+
- Added `labels` field to `elasticstack_kibana_synthetics_monitor` resource for associating key-value pairs with monitors ([#1360](https://github.com/elastic/terraform-provider-elasticstack/pull/1360))
45
- Fixes provider crash with `elasticstack_kibana_slo` when using `kql_custom_indicator` with no `filter` set. ([#1354](https://github.com/elastic/terraform-provider-elasticstack/pull/1354))
56
- Updates for Security Detection Rules ([#1361](https://github.com/elastic/terraform-provider-elasticstack/pull/1361)
67
- Add support for `threat` property

docs/resources/kibana_synthetics_monitor.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ resource "elasticstack_kibana_synthetics_monitor" "my_monitor" {
4040
locations = ["us_west"]
4141
enabled = false
4242
tags = ["tag"]
43+
labels = {
44+
environment = "production"
45+
team = "platform"
46+
service = "web-app"
47+
}
4348
alert = {
4449
status = {
4550
enabled = true
@@ -76,6 +81,7 @@ resource "elasticstack_kibana_synthetics_monitor" "my_monitor" {
7681
- `enabled` (Boolean) Whether the monitor is enabled. Default: `true`
7782
- `http` (Attributes) HTTP Monitor specific fields (see [below for nested schema](#nestedatt--http))
7883
- `icmp` (Attributes) ICMP Monitor specific fields (see [below for nested schema](#nestedatt--icmp))
84+
- `labels` (Map of String) Key-value pairs of labels to associate with the monitor. Labels can be used for filtering and grouping monitors.
7985
- `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.
8086
- `namespace` (String) The data stream namespace. Note: if you change its value, kibana creates new datastream. A user needs permissions for new/old datastream in update case to be able to see full monitor history. The `namespace` field should be lowercase and not contain spaces. The namespace must not include any of the following characters: *, \, /, ?, ", <, >, |, whitespace, ,, #, :, or -. Default: `default`
8187
- `params` (String) Monitor parameters. Raw JSON object, use `jsonencode` function to represent JSON

examples/resources/elasticstack_kibana_synthetics_monitor/resource.tf

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ resource "elasticstack_kibana_synthetics_monitor" "my_monitor" {
99
locations = ["us_west"]
1010
enabled = false
1111
tags = ["tag"]
12+
labels = {
13+
environment = "production"
14+
team = "platform"
15+
service = "web-app"
16+
}
1217
alert = {
1318
status = {
1419
enabled = true

internal/kibana/synthetics/acc_test.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"testing"
66

77
"github.com/elastic/terraform-provider-elasticstack/internal/acctest"
8+
"github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics"
89
"github.com/elastic/terraform-provider-elasticstack/internal/versionutils"
910
"github.com/hashicorp/go-version"
1011
sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest"
@@ -339,6 +340,46 @@ resource "elasticstack_kibana_synthetics_monitor" "%s" {
339340
playwright_options = jsonencode({"httpCredentials":{"password":"test","username":"test"},"ignoreHTTPSErrors":false})
340341
}
341342
}
343+
`
344+
345+
httpMonitorLabelsConfig = `
346+
resource "elasticstack_kibana_synthetics_monitor" "%s" {
347+
name = "TestHttpMonitorLabels - %s"
348+
private_locations = [elasticstack_kibana_synthetics_private_location.%s.label]
349+
labels = {
350+
environment = "production"
351+
team = "platform"
352+
service = "web-app"
353+
}
354+
http = {
355+
url = "http://localhost:5601"
356+
}
357+
}
358+
`
359+
360+
httpMonitorLabelsUpdated = `
361+
resource "elasticstack_kibana_synthetics_monitor" "%s" {
362+
name = "TestHttpMonitorLabels Updated - %s"
363+
private_locations = [elasticstack_kibana_synthetics_private_location.%s.label]
364+
labels = {
365+
environment = "staging"
366+
team = "platform-updated"
367+
service = "web-app-v2"
368+
}
369+
http = {
370+
url = "http://localhost:5601"
371+
}
372+
}
373+
`
374+
375+
httpMonitorLabelsRemoved = `
376+
resource "elasticstack_kibana_synthetics_monitor" "%s" {
377+
name = "TestHttpMonitorLabels Removed - %s"
378+
private_locations = [elasticstack_kibana_synthetics_private_location.%s.label]
379+
http = {
380+
url = "http://localhost:5601"
381+
}
382+
}
342383
`
343384
)
344385

@@ -828,6 +869,71 @@ func TestSyntheticMonitorBrowserResource(t *testing.T) {
828869
})
829870
}
830871

872+
func TestSyntheticMonitorLabelsResource(t *testing.T) {
873+
name := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlphaNum)
874+
id := "http-monitor-labels"
875+
labelsMonitorId, labelsConfig := testMonitorConfig(id, httpMonitorLabelsConfig, name)
876+
_, labelsConfigUpdated := testMonitorConfig(id, httpMonitorLabelsUpdated, name)
877+
_, labelsConfigRemoved := testMonitorConfig(id, httpMonitorLabelsRemoved, name)
878+
879+
resource.Test(t, resource.TestCase{
880+
PreCheck: func() { acctest.PreCheck(t) },
881+
ProtoV6ProviderFactories: acctest.Providers,
882+
Steps: []resource.TestStep{
883+
// Create and Read monitor with labels
884+
{
885+
SkipFunc: versionutils.CheckIfVersionIsUnsupported(synthetics.MinLabelsVersion),
886+
Config: labelsConfig,
887+
Check: resource.ComposeAggregateTestCheckFunc(
888+
resource.TestCheckResourceAttrSet(labelsMonitorId, "id"),
889+
resource.TestCheckResourceAttr(labelsMonitorId, "name", "TestHttpMonitorLabels - "+name),
890+
resource.TestCheckResourceAttr(labelsMonitorId, "labels.%", "3"),
891+
resource.TestCheckResourceAttr(labelsMonitorId, "labels.environment", "production"),
892+
resource.TestCheckResourceAttr(labelsMonitorId, "labels.team", "platform"),
893+
resource.TestCheckResourceAttr(labelsMonitorId, "labels.service", "web-app"),
894+
resource.TestCheckResourceAttr(labelsMonitorId, "http.url", "http://localhost:5601"),
895+
),
896+
},
897+
// ImportState testing
898+
{
899+
SkipFunc: versionutils.CheckIfVersionIsUnsupported(synthetics.MinLabelsVersion),
900+
ResourceName: labelsMonitorId,
901+
ImportState: true,
902+
ImportStateVerify: true,
903+
Config: labelsConfig,
904+
},
905+
// Update labels - change values but keep same keys
906+
{
907+
SkipFunc: versionutils.CheckIfVersionIsUnsupported(synthetics.MinLabelsVersion),
908+
Config: labelsConfigUpdated,
909+
Check: resource.ComposeAggregateTestCheckFunc(
910+
resource.TestCheckResourceAttrSet(labelsMonitorId, "id"),
911+
resource.TestCheckResourceAttr(labelsMonitorId, "name", "TestHttpMonitorLabels Updated - "+name),
912+
resource.TestCheckResourceAttr(labelsMonitorId, "labels.%", "3"),
913+
resource.TestCheckResourceAttr(labelsMonitorId, "labels.environment", "staging"),
914+
resource.TestCheckResourceAttr(labelsMonitorId, "labels.team", "platform-updated"),
915+
resource.TestCheckResourceAttr(labelsMonitorId, "labels.service", "web-app-v2"),
916+
),
917+
},
918+
// Remove all labels - this tests the round-trip consistency fix
919+
{
920+
SkipFunc: versionutils.CheckIfVersionIsUnsupported(synthetics.MinLabelsVersion),
921+
Config: labelsConfigRemoved,
922+
Check: resource.ComposeAggregateTestCheckFunc(
923+
resource.TestCheckResourceAttrSet(labelsMonitorId, "id"),
924+
resource.TestCheckResourceAttr(labelsMonitorId, "name", "TestHttpMonitorLabels Removed - "+name),
925+
resource.TestCheckNoResourceAttr(labelsMonitorId, "labels.%"),
926+
resource.TestCheckNoResourceAttr(labelsMonitorId, "labels.environment"),
927+
resource.TestCheckNoResourceAttr(labelsMonitorId, "labels.team"),
928+
resource.TestCheckNoResourceAttr(labelsMonitorId, "labels.service"),
929+
resource.TestCheckNoResourceAttr(labelsMonitorId, "labels.version"),
930+
),
931+
},
932+
// Delete testing automatically occurs in TestCase
933+
},
934+
})
935+
}
936+
831937
func testMonitorConfig(id, cfg, name string) (string, string) {
832938

833939
resourceId := "elasticstack_kibana_synthetics_monitor." + id

internal/kibana/synthetics/create.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ import (
44
"context"
55
"fmt"
66

7+
"github.com/hashicorp/go-version"
8+
79
"github.com/hashicorp/terraform-plugin-framework/resource"
810
)
911

10-
func (r *Resource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) {
12+
var MinLabelsVersion = version.Must(version.NewVersion("8.16.0"))
1113

14+
func (r *Resource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) {
1215
kibanaClient := GetKibanaClient(r, response.Diagnostics)
1316
if kibanaClient == nil {
1417
return
@@ -21,6 +24,11 @@ func (r *Resource) Create(ctx context.Context, request resource.CreateRequest, r
2124
return
2225
}
2326

27+
response.Diagnostics.Append(plan.enforceVersionConstraints(ctx, r.client)...)
28+
if response.Diagnostics.HasError() {
29+
return
30+
}
31+
2432
input, diags := plan.toKibanaAPIRequest(ctx)
2533
response.Diagnostics.Append(diags...)
2634
if response.Diagnostics.HasError() {

internal/kibana/synthetics/schema.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/disaster37/go-kibana-rest/v8/kbapi"
1212
"github.com/elastic/terraform-provider-elasticstack/internal/clients"
13+
"github.com/elastic/terraform-provider-elasticstack/internal/diagutil"
1314
"github.com/elastic/terraform-provider-elasticstack/internal/utils"
1415
"github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
1516
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
@@ -107,6 +108,7 @@ type tfModelV0 struct {
107108
PrivateLocations []types.String `tfsdk:"private_locations"`
108109
Enabled types.Bool `tfsdk:"enabled"`
109110
Tags []types.String `tfsdk:"tags"`
111+
Labels types.Map `tfsdk:"labels"`
110112
Alert types.Object `tfsdk:"alert"` //tfAlertConfigV0
111113
APMServiceName types.String `tfsdk:"service_name"`
112114
TimeoutSeconds types.Int64 `tfsdk:"timeout"`
@@ -217,6 +219,11 @@ func monitorConfigSchema() schema.Schema {
217219
Optional: true,
218220
MarkdownDescription: "An array of tags.",
219221
},
222+
"labels": schema.MapAttribute{
223+
ElementType: types.StringType,
224+
Optional: true,
225+
MarkdownDescription: "Key-value pairs of labels to associate with the monitor. Labels can be used for filtering and grouping monitors.",
226+
},
220227
"alert": monitorAlertConfigSchema(),
221228
"service_name": schema.StringAttribute{
222229
Optional: true,
@@ -557,6 +564,31 @@ func StringSliceValue(v []string) []types.String {
557564
return res
558565
}
559566

567+
func MapStringValue(v map[string]string) types.Map {
568+
if len(v) == 0 {
569+
return types.MapNull(types.StringType)
570+
}
571+
elements := make(map[string]attr.Value)
572+
for k, val := range v {
573+
elements[k] = types.StringValue(val)
574+
}
575+
mapValue, _ := types.MapValue(types.StringType, elements)
576+
return mapValue
577+
}
578+
579+
func ValueStringMap(v types.Map) map[string]string {
580+
if v.IsNull() || v.IsUnknown() {
581+
return make(map[string]string)
582+
}
583+
result := make(map[string]string)
584+
for k, val := range v.Elements() {
585+
if strVal, ok := val.(types.String); ok {
586+
result[k] = strVal.ValueString()
587+
}
588+
}
589+
return result
590+
}
591+
560592
func toNormalizedValue(jsObj kbapi.JsonObject) (jsontypes.Normalized, error) {
561593
res, err := json.Marshal(jsObj)
562594
if err != nil {
@@ -679,6 +711,7 @@ func (v *tfModelV0) toModelV0(ctx context.Context, api *kbapi.SyntheticsMonitor,
679711
PrivateLocations: StringSliceValue(privateLocLabels),
680712
Enabled: types.BoolPointerValue(api.Enabled),
681713
Tags: StringSliceValue(api.Tags),
714+
Labels: MapStringValue(api.Labels),
682715
Alert: alertV0,
683716
APMServiceName: types.StringValue(api.APMServiceName),
684717
TimeoutSeconds: types.Int64Value(timeout),
@@ -901,6 +934,7 @@ func (v *tfModelV0) toSyntheticsMonitorConfig(ctx context.Context) (*kbapi.Synth
901934
PrivateLocations: ValueStringSlice(v.PrivateLocations),
902935
Enabled: v.Enabled.ValueBoolPointer(),
903936
Tags: ValueStringSlice(v.Tags),
937+
Labels: ValueStringMap(v.Labels),
904938
Alert: toTFAlertConfig(ctx, v.Alert),
905939
APMServiceName: v.APMServiceName.ValueString(),
906940
TimeoutSeconds: int(v.TimeoutSeconds.ValueInt64()),
@@ -1068,3 +1102,24 @@ func (v tfStatusConfigV0) toTfStatusConfigV0() *kbapi.SyntheticsStatusConfig {
10681102
Enabled: v.Enabled.ValueBoolPointer(),
10691103
}
10701104
}
1105+
1106+
func (v tfModelV0) enforceVersionConstraints(ctx context.Context, client *clients.ApiClient) diag.Diagnostics {
1107+
if utils.IsKnown(v.Labels) {
1108+
isSupported, sdkDiags := client.EnforceMinVersion(ctx, MinLabelsVersion)
1109+
diags := diagutil.FrameworkDiagsFromSDK(sdkDiags)
1110+
if diags.HasError() {
1111+
return diags
1112+
}
1113+
1114+
if !isSupported {
1115+
diags.AddAttributeError(
1116+
path.Root("labels"),
1117+
"Unsupported version for `labels` attribute",
1118+
fmt.Sprintf("The `labels` attribute requires server version %s or higher. Either remove the `labels` attribute or upgrade your Elastic Stack installation.", MinLabelsVersion.String()),
1119+
)
1120+
return diags
1121+
}
1122+
}
1123+
1124+
return nil
1125+
}

0 commit comments

Comments
 (0)