diff --git a/CHANGELOG.md b/CHANGELOG.md index 19c388454..37c580172 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## [Unreleased] - 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)) +- 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)) - 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)) - Updates for Security Detection Rules ([#1361](https://github.com/elastic/terraform-provider-elasticstack/pull/1361) - Add support for `threat` property diff --git a/docs/resources/kibana_synthetics_monitor.md b/docs/resources/kibana_synthetics_monitor.md index 75dbd6c72..e19aaca92 100644 --- a/docs/resources/kibana_synthetics_monitor.md +++ b/docs/resources/kibana_synthetics_monitor.md @@ -40,6 +40,11 @@ resource "elasticstack_kibana_synthetics_monitor" "my_monitor" { locations = ["us_west"] enabled = false tags = ["tag"] + labels = { + environment = "production" + team = "platform" + service = "web-app" + } alert = { status = { enabled = true @@ -76,6 +81,7 @@ resource "elasticstack_kibana_synthetics_monitor" "my_monitor" { - `enabled` (Boolean) Whether the monitor is enabled. Default: `true` - `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)) +- `labels` (Map of String) Key-value pairs of labels to associate with the monitor. Labels can be used for filtering and grouping monitors. - `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. 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` - `params` (String) Monitor parameters. Raw JSON object, use `jsonencode` function to represent JSON diff --git a/examples/resources/elasticstack_kibana_synthetics_monitor/resource.tf b/examples/resources/elasticstack_kibana_synthetics_monitor/resource.tf index b38e70609..67ec58080 100644 --- a/examples/resources/elasticstack_kibana_synthetics_monitor/resource.tf +++ b/examples/resources/elasticstack_kibana_synthetics_monitor/resource.tf @@ -9,6 +9,11 @@ resource "elasticstack_kibana_synthetics_monitor" "my_monitor" { locations = ["us_west"] enabled = false tags = ["tag"] + labels = { + environment = "production" + team = "platform" + service = "web-app" + } alert = { status = { enabled = true diff --git a/internal/kibana/synthetics/acc_test.go b/internal/kibana/synthetics/acc_test.go index ddde5a150..bb6da968c 100644 --- a/internal/kibana/synthetics/acc_test.go +++ b/internal/kibana/synthetics/acc_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/elastic/terraform-provider-elasticstack/internal/acctest" + "github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics" "github.com/elastic/terraform-provider-elasticstack/internal/versionutils" "github.com/hashicorp/go-version" sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" @@ -339,6 +340,46 @@ resource "elasticstack_kibana_synthetics_monitor" "%s" { playwright_options = jsonencode({"httpCredentials":{"password":"test","username":"test"},"ignoreHTTPSErrors":false}) } } +` + + httpMonitorLabelsConfig = ` +resource "elasticstack_kibana_synthetics_monitor" "%s" { + name = "TestHttpMonitorLabels - %s" + private_locations = [elasticstack_kibana_synthetics_private_location.%s.label] + labels = { + environment = "production" + team = "platform" + service = "web-app" + } + http = { + url = "http://localhost:5601" + } +} +` + + httpMonitorLabelsUpdated = ` +resource "elasticstack_kibana_synthetics_monitor" "%s" { + name = "TestHttpMonitorLabels Updated - %s" + private_locations = [elasticstack_kibana_synthetics_private_location.%s.label] + labels = { + environment = "staging" + team = "platform-updated" + service = "web-app-v2" + } + http = { + url = "http://localhost:5601" + } +} +` + + httpMonitorLabelsRemoved = ` +resource "elasticstack_kibana_synthetics_monitor" "%s" { + name = "TestHttpMonitorLabels Removed - %s" + private_locations = [elasticstack_kibana_synthetics_private_location.%s.label] + http = { + url = "http://localhost:5601" + } +} ` ) @@ -828,6 +869,71 @@ func TestSyntheticMonitorBrowserResource(t *testing.T) { }) } +func TestSyntheticMonitorLabelsResource(t *testing.T) { + name := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlphaNum) + id := "http-monitor-labels" + labelsMonitorId, labelsConfig := testMonitorConfig(id, httpMonitorLabelsConfig, name) + _, labelsConfigUpdated := testMonitorConfig(id, httpMonitorLabelsUpdated, name) + _, labelsConfigRemoved := testMonitorConfig(id, httpMonitorLabelsRemoved, name) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.Providers, + Steps: []resource.TestStep{ + // Create and Read monitor with labels + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(synthetics.MinLabelsVersion), + Config: labelsConfig, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(labelsMonitorId, "id"), + resource.TestCheckResourceAttr(labelsMonitorId, "name", "TestHttpMonitorLabels - "+name), + resource.TestCheckResourceAttr(labelsMonitorId, "labels.%", "3"), + resource.TestCheckResourceAttr(labelsMonitorId, "labels.environment", "production"), + resource.TestCheckResourceAttr(labelsMonitorId, "labels.team", "platform"), + resource.TestCheckResourceAttr(labelsMonitorId, "labels.service", "web-app"), + resource.TestCheckResourceAttr(labelsMonitorId, "http.url", "http://localhost:5601"), + ), + }, + // ImportState testing + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(synthetics.MinLabelsVersion), + ResourceName: labelsMonitorId, + ImportState: true, + ImportStateVerify: true, + Config: labelsConfig, + }, + // Update labels - change values but keep same keys + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(synthetics.MinLabelsVersion), + Config: labelsConfigUpdated, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(labelsMonitorId, "id"), + resource.TestCheckResourceAttr(labelsMonitorId, "name", "TestHttpMonitorLabels Updated - "+name), + resource.TestCheckResourceAttr(labelsMonitorId, "labels.%", "3"), + resource.TestCheckResourceAttr(labelsMonitorId, "labels.environment", "staging"), + resource.TestCheckResourceAttr(labelsMonitorId, "labels.team", "platform-updated"), + resource.TestCheckResourceAttr(labelsMonitorId, "labels.service", "web-app-v2"), + ), + }, + // Remove all labels - this tests the round-trip consistency fix + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(synthetics.MinLabelsVersion), + Config: labelsConfigRemoved, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(labelsMonitorId, "id"), + resource.TestCheckResourceAttr(labelsMonitorId, "name", "TestHttpMonitorLabels Removed - "+name), + resource.TestCheckNoResourceAttr(labelsMonitorId, "labels.%"), + resource.TestCheckNoResourceAttr(labelsMonitorId, "labels.environment"), + resource.TestCheckNoResourceAttr(labelsMonitorId, "labels.team"), + resource.TestCheckNoResourceAttr(labelsMonitorId, "labels.service"), + resource.TestCheckNoResourceAttr(labelsMonitorId, "labels.version"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + func testMonitorConfig(id, cfg, name string) (string, string) { resourceId := "elasticstack_kibana_synthetics_monitor." + id diff --git a/internal/kibana/synthetics/create.go b/internal/kibana/synthetics/create.go index 2aacea31a..5fbc80aa0 100644 --- a/internal/kibana/synthetics/create.go +++ b/internal/kibana/synthetics/create.go @@ -4,11 +4,14 @@ import ( "context" "fmt" + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-framework/resource" ) -func (r *Resource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { +var MinLabelsVersion = version.Must(version.NewVersion("8.16.0")) +func (r *Resource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { kibanaClient := GetKibanaClient(r, response.Diagnostics) if kibanaClient == nil { return @@ -21,6 +24,11 @@ func (r *Resource) Create(ctx context.Context, request resource.CreateRequest, r return } + response.Diagnostics.Append(plan.enforceVersionConstraints(ctx, r.client)...) + if response.Diagnostics.HasError() { + return + } + input, diags := plan.toKibanaAPIRequest(ctx) response.Diagnostics.Append(diags...) if response.Diagnostics.HasError() { diff --git a/internal/kibana/synthetics/schema.go b/internal/kibana/synthetics/schema.go index b7024270b..942e7b2b2 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/diagutil" "github.com/elastic/terraform-provider-elasticstack/internal/utils" "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" @@ -107,6 +108,7 @@ type tfModelV0 struct { PrivateLocations []types.String `tfsdk:"private_locations"` Enabled types.Bool `tfsdk:"enabled"` Tags []types.String `tfsdk:"tags"` + Labels types.Map `tfsdk:"labels"` Alert types.Object `tfsdk:"alert"` //tfAlertConfigV0 APMServiceName types.String `tfsdk:"service_name"` TimeoutSeconds types.Int64 `tfsdk:"timeout"` @@ -217,6 +219,11 @@ func monitorConfigSchema() schema.Schema { Optional: true, MarkdownDescription: "An array of tags.", }, + "labels": schema.MapAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: "Key-value pairs of labels to associate with the monitor. Labels can be used for filtering and grouping monitors.", + }, "alert": monitorAlertConfigSchema(), "service_name": schema.StringAttribute{ Optional: true, @@ -557,6 +564,31 @@ func StringSliceValue(v []string) []types.String { return res } +func MapStringValue(v map[string]string) types.Map { + if len(v) == 0 { + return types.MapNull(types.StringType) + } + elements := make(map[string]attr.Value) + for k, val := range v { + elements[k] = types.StringValue(val) + } + mapValue, _ := types.MapValue(types.StringType, elements) + return mapValue +} + +func ValueStringMap(v types.Map) map[string]string { + if v.IsNull() || v.IsUnknown() { + return make(map[string]string) + } + result := make(map[string]string) + for k, val := range v.Elements() { + if strVal, ok := val.(types.String); ok { + result[k] = strVal.ValueString() + } + } + return result +} + func toNormalizedValue(jsObj kbapi.JsonObject) (jsontypes.Normalized, error) { res, err := json.Marshal(jsObj) if err != nil { @@ -679,6 +711,7 @@ func (v *tfModelV0) toModelV0(ctx context.Context, api *kbapi.SyntheticsMonitor, PrivateLocations: StringSliceValue(privateLocLabels), Enabled: types.BoolPointerValue(api.Enabled), Tags: StringSliceValue(api.Tags), + Labels: MapStringValue(api.Labels), Alert: alertV0, APMServiceName: types.StringValue(api.APMServiceName), TimeoutSeconds: types.Int64Value(timeout), @@ -901,6 +934,7 @@ func (v *tfModelV0) toSyntheticsMonitorConfig(ctx context.Context) (*kbapi.Synth PrivateLocations: ValueStringSlice(v.PrivateLocations), Enabled: v.Enabled.ValueBoolPointer(), Tags: ValueStringSlice(v.Tags), + Labels: ValueStringMap(v.Labels), Alert: toTFAlertConfig(ctx, v.Alert), APMServiceName: v.APMServiceName.ValueString(), TimeoutSeconds: int(v.TimeoutSeconds.ValueInt64()), @@ -1068,3 +1102,24 @@ func (v tfStatusConfigV0) toTfStatusConfigV0() *kbapi.SyntheticsStatusConfig { Enabled: v.Enabled.ValueBoolPointer(), } } + +func (v tfModelV0) enforceVersionConstraints(ctx context.Context, client *clients.ApiClient) diag.Diagnostics { + if utils.IsKnown(v.Labels) { + isSupported, sdkDiags := client.EnforceMinVersion(ctx, MinLabelsVersion) + diags := diagutil.FrameworkDiagsFromSDK(sdkDiags) + if diags.HasError() { + return diags + } + + if !isSupported { + diags.AddAttributeError( + path.Root("labels"), + "Unsupported version for `labels` attribute", + fmt.Sprintf("The `labels` attribute requires server version %s or higher. Either remove the `labels` attribute or upgrade your Elastic Stack installation.", MinLabelsVersion.String()), + ) + return diags + } + } + + return nil +} diff --git a/internal/kibana/synthetics/schema_test.go b/internal/kibana/synthetics/schema_test.go index e5c5bb60a..2b0ce8d69 100644 --- a/internal/kibana/synthetics/schema_test.go +++ b/internal/kibana/synthetics/schema_test.go @@ -25,6 +25,150 @@ func boolPointer(v bool) *bool { return res } +func TestMapStringValue(t *testing.T) { + testcases := []struct { + name string + input map[string]string + expected types.Map + }{ + { + name: "nil map", + input: nil, + expected: types.MapNull(types.StringType), + }, + { + name: "empty map", + input: map[string]string{}, + expected: types.MapNull(types.StringType), + }, + { + name: "map with values", + input: map[string]string{ + "environment": "production", + "team": "platform", + }, + expected: func() types.Map { + elements := map[string]attr.Value{ + "environment": types.StringValue("production"), + "team": types.StringValue("platform"), + } + mapValue, _ := types.MapValue(types.StringType, elements) + return mapValue + }(), + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + result := MapStringValue(tc.input) + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestValueStringMap(t *testing.T) { + testcases := []struct { + name string + input types.Map + expected map[string]string + }{ + { + name: "null map", + input: types.MapNull(types.StringType), + expected: map[string]string{}, + }, + { + name: "unknown map", + input: types.MapUnknown(types.StringType), + expected: map[string]string{}, + }, + { + name: "empty map", + input: func() types.Map { + mapValue, _ := types.MapValue(types.StringType, map[string]attr.Value{}) + return mapValue + }(), + expected: map[string]string{}, + }, + { + name: "map with values", + input: func() types.Map { + elements := map[string]attr.Value{ + "environment": types.StringValue("production"), + "team": types.StringValue("platform"), + } + mapValue, _ := types.MapValue(types.StringType, elements) + return mapValue + }(), + expected: map[string]string{ + "environment": "production", + "team": "platform", + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + result := ValueStringMap(tc.input) + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestLabelsFieldConversion(t *testing.T) { + testcases := []struct { + name string + input kbapi.SyntheticsMonitor + expected types.Map + }{ + { + name: "monitor with nil labels", + input: kbapi.SyntheticsMonitor{ + Type: kbapi.Http, + Labels: nil, + }, + expected: types.MapNull(types.StringType), + }, + { + name: "monitor with empty labels", + input: kbapi.SyntheticsMonitor{ + Type: kbapi.Http, + Labels: map[string]string{}, + }, + expected: types.MapNull(types.StringType), + }, + { + name: "monitor with labels", + input: kbapi.SyntheticsMonitor{ + Type: kbapi.Http, + Labels: map[string]string{ + "environment": "production", + "team": "platform", + "service": "web-app", + }, + }, + expected: func() types.Map { + elements := map[string]attr.Value{ + "environment": types.StringValue("production"), + "team": types.StringValue("platform"), + "service": types.StringValue("web-app"), + } + mapValue, _ := types.MapValue(types.StringType, elements) + return mapValue + }(), + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + model := &tfModelV0{} + result, diags := model.toModelV0(context.Background(), &tc.input, "default") + assert.False(t, diags.HasError()) + assert.Equal(t, tc.expected, result.Labels) + }) + } +} + func toAlertObject(t *testing.T, v tfAlertConfigV0) basetypes.ObjectValue { alertAttributes := monitorAlertConfigSchema().GetType().(attr.TypeWithAttributeTypes).AttributeTypes() @@ -52,6 +196,7 @@ func TestToModelV0(t *testing.T) { SpaceID: types.StringValue(""), Namespace: types.StringValue(""), Schedule: types.Int64Value(0), + Labels: types.MapNull(types.StringType), APMServiceName: types.StringValue(""), TimeoutSeconds: types.Int64Value(0), Params: jsontypes.NewNormalizedValue("null"), @@ -87,6 +232,7 @@ func TestToModelV0(t *testing.T) { SpaceID: types.StringValue(""), Namespace: types.StringValue(""), Schedule: types.Int64Value(0), + Labels: types.MapNull(types.StringType), APMServiceName: types.StringValue(""), TimeoutSeconds: types.Int64Value(0), Params: jsontypes.NewNormalizedValue("null"), @@ -116,6 +262,7 @@ func TestToModelV0(t *testing.T) { SpaceID: types.StringValue(""), Namespace: types.StringValue(""), Schedule: types.Int64Value(0), + Labels: types.MapNull(types.StringType), APMServiceName: types.StringValue(""), TimeoutSeconds: types.Int64Value(0), Params: jsontypes.NewNormalizedValue("null"), @@ -136,6 +283,7 @@ func TestToModelV0(t *testing.T) { SpaceID: types.StringValue(""), Namespace: types.StringValue(""), Schedule: types.Int64Value(0), + Labels: types.MapNull(types.StringType), APMServiceName: types.StringValue(""), TimeoutSeconds: types.Int64Value(0), Params: jsontypes.NewNormalizedValue("null"), @@ -202,6 +350,7 @@ func TestToModelV0(t *testing.T) { PrivateLocations: []types.String{types.StringValue("test private location")}, Enabled: types.BoolPointerValue(tBool), Tags: []types.String{types.StringValue("tag1"), types.StringValue("tag2")}, + Labels: types.MapNull(types.StringType), Alert: toAlertObject(t, tfAlertConfigV0{Status: &tfStatusConfigV0{Enabled: types.BoolPointerValue(tBool)}, TLS: &tfStatusConfigV0{Enabled: types.BoolPointerValue(fBool)}}), APMServiceName: types.StringValue("test-service-http"), TimeoutSeconds: types.Int64Value(30), @@ -273,6 +422,7 @@ func TestToModelV0(t *testing.T) { PrivateLocations: []types.String{types.StringValue("test private location")}, Enabled: types.BoolPointerValue(tBool), Tags: nil, + Labels: types.MapNull(types.StringType), Alert: toAlertObject(t, tfAlertConfigV0{Status: &tfStatusConfigV0{Enabled: types.BoolPointerValue(tBool)}}), APMServiceName: types.StringValue("test-service-tcp"), TimeoutSeconds: types.Int64Value(30), @@ -333,6 +483,7 @@ func TestToModelV0(t *testing.T) { PrivateLocations: []types.String{types.StringValue("test private location")}, Enabled: types.BoolPointerValue(tBool), Tags: nil, + Labels: types.MapNull(types.StringType), Alert: toAlertObject(t, tfAlertConfigV0{Status: &tfStatusConfigV0{Enabled: types.BoolPointerValue(tBool)}}), APMServiceName: types.StringValue("test-service-tcp"), TimeoutSeconds: types.Int64Value(30), @@ -389,6 +540,7 @@ func TestToModelV0(t *testing.T) { PrivateLocations: []types.String{types.StringValue("test private location")}, Enabled: types.BoolPointerValue(tBool), Tags: nil, + Labels: types.MapNull(types.StringType), Alert: toAlertObject(t, tfAlertConfigV0{Status: &tfStatusConfigV0{Enabled: types.BoolPointerValue(tBool)}}), APMServiceName: types.StringValue("test-service-tcp"), TimeoutSeconds: types.Int64Value(30), @@ -427,7 +579,9 @@ func TestToKibanaAPIRequest(t *testing.T) { }, expected: kibanaAPIRequest{ fields: kbapi.HTTPMonitorFields{}, - config: kbapi.SyntheticsMonitorConfig{}, + config: kbapi.SyntheticsMonitorConfig{ + Labels: map[string]string{}, + }, }, }, { @@ -437,7 +591,9 @@ func TestToKibanaAPIRequest(t *testing.T) { }, expected: kibanaAPIRequest{ fields: kbapi.TCPMonitorFields{}, - config: kbapi.SyntheticsMonitorConfig{}, + config: kbapi.SyntheticsMonitorConfig{ + Labels: map[string]string{}, + }, }, }, { @@ -447,7 +603,9 @@ func TestToKibanaAPIRequest(t *testing.T) { }, expected: kibanaAPIRequest{ fields: kbapi.ICMPMonitorFields{}, - config: kbapi.SyntheticsMonitorConfig{}, + config: kbapi.SyntheticsMonitorConfig{ + Labels: map[string]string{}, + }, }, }, { @@ -457,7 +615,9 @@ func TestToKibanaAPIRequest(t *testing.T) { }, expected: kibanaAPIRequest{ fields: kbapi.BrowserMonitorFields{}, - config: kbapi.SyntheticsMonitorConfig{}, + config: kbapi.SyntheticsMonitorConfig{ + Labels: map[string]string{}, + }, }, }, { @@ -508,6 +668,7 @@ func TestToKibanaAPIRequest(t *testing.T) { PrivateLocations: []string{"test private location"}, Enabled: tBool, Tags: []string{"tag1", "tag2"}, + Labels: map[string]string{}, Alert: &kbapi.MonitorAlertConfig{Status: &kbapi.SyntheticsStatusConfig{Enabled: tBool}, Tls: &kbapi.SyntheticsStatusConfig{Enabled: fBool}}, APMServiceName: "test-service-http", Namespace: "default-3", @@ -579,6 +740,7 @@ func TestToKibanaAPIRequest(t *testing.T) { PrivateLocations: nil, Enabled: tBool, Tags: []string{"tag1", "tag2"}, + Labels: map[string]string{}, Alert: &kbapi.MonitorAlertConfig{Status: &kbapi.SyntheticsStatusConfig{Enabled: tBool}}, APMServiceName: "test-service-tcp", Namespace: "default", @@ -631,6 +793,7 @@ func TestToKibanaAPIRequest(t *testing.T) { PrivateLocations: nil, Enabled: tBool, Tags: []string{"tag1", "tag2"}, + Labels: map[string]string{}, Alert: &kbapi.MonitorAlertConfig{Status: &kbapi.SyntheticsStatusConfig{Enabled: tBool}}, APMServiceName: "test-service-tcp", Namespace: "default", @@ -675,6 +838,7 @@ func TestToKibanaAPIRequest(t *testing.T) { PrivateLocations: nil, Enabled: tBool, Tags: []string{"tag1", "tag2"}, + Labels: map[string]string{}, Alert: &kbapi.MonitorAlertConfig{Status: &kbapi.SyntheticsStatusConfig{Enabled: tBool}}, APMServiceName: "test-service-tcp", Namespace: "default", @@ -737,6 +901,7 @@ func TestToModelV0MergeAttributes(t *testing.T) { SpaceID: types.StringValue(""), Namespace: types.StringValue(""), Schedule: types.Int64Value(0), + Labels: types.MapNull(types.StringType), APMServiceName: types.StringValue(""), TimeoutSeconds: types.Int64Value(0), Params: jsontypes.NewNormalizedValue(`{"param1":"value1"}`), @@ -783,6 +948,7 @@ func TestToModelV0MergeAttributes(t *testing.T) { SpaceID: types.StringValue(""), Namespace: types.StringValue(""), Schedule: types.Int64Value(0), + Labels: types.MapNull(types.StringType), APMServiceName: types.StringValue(""), TimeoutSeconds: types.Int64Value(0), Locations: []types.String{types.StringValue("us_east")}, @@ -818,6 +984,7 @@ func TestToModelV0MergeAttributes(t *testing.T) { SpaceID: types.StringValue(""), Namespace: types.StringValue(""), Schedule: types.Int64Value(0), + Labels: types.MapNull(types.StringType), APMServiceName: types.StringValue(""), TimeoutSeconds: types.Int64Value(0), Browser: &tfBrowserMonitorFieldsV0{ @@ -839,3 +1006,67 @@ func TestToModelV0MergeAttributes(t *testing.T) { }) } } + +func TestToSyntheticsMonitorConfig(t *testing.T) { + testcases := []struct { + name string + input tfModelV0 + expected kbapi.SyntheticsMonitorConfig + }{ + { + name: "monitor config with nil labels", + input: tfModelV0{ + Name: types.StringValue("test-monitor"), + Labels: types.MapNull(types.StringType), + }, + expected: kbapi.SyntheticsMonitorConfig{ + Name: "test-monitor", + Labels: map[string]string{}, + }, + }, + { + name: "monitor config with empty labels", + input: tfModelV0{ + Name: types.StringValue("test-monitor"), + Labels: func() types.Map { + mapValue, _ := types.MapValue(types.StringType, map[string]attr.Value{}) + return mapValue + }(), + }, + expected: kbapi.SyntheticsMonitorConfig{ + Name: "test-monitor", + Labels: map[string]string{}, + }, + }, + { + name: "monitor config with labels", + input: tfModelV0{ + Name: types.StringValue("test-monitor"), + Labels: func() types.Map { + elements := map[string]attr.Value{ + "environment": types.StringValue("production"), + "team": types.StringValue("platform"), + } + mapValue, _ := types.MapValue(types.StringType, elements) + return mapValue + }(), + }, + expected: kbapi.SyntheticsMonitorConfig{ + Name: "test-monitor", + Labels: map[string]string{ + "environment": "production", + "team": "platform", + }, + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + result, diags := tc.input.toSyntheticsMonitorConfig(context.Background()) + assert.False(t, diags.HasError()) + assert.Equal(t, tc.expected.Name, result.Name) + assert.Equal(t, tc.expected.Labels, result.Labels) + }) + } +} diff --git a/internal/kibana/synthetics/update.go b/internal/kibana/synthetics/update.go index 3902615b3..02063bb4b 100644 --- a/internal/kibana/synthetics/update.go +++ b/internal/kibana/synthetics/update.go @@ -22,6 +22,11 @@ func (r *Resource) Update(ctx context.Context, request resource.UpdateRequest, r return } + response.Diagnostics.Append(plan.enforceVersionConstraints(ctx, r.client)...) + if response.Diagnostics.HasError() { + return + } + input, diags := plan.toKibanaAPIRequest(ctx) response.Diagnostics.Append(diags...) if response.Diagnostics.HasError() { diff --git a/libs/go-kibana-rest/kbapi/api.kibana_synthetics.go b/libs/go-kibana-rest/kbapi/api.kibana_synthetics.go index 283c27613..78bc1b953 100644 --- a/libs/go-kibana-rest/kbapi/api.kibana_synthetics.go +++ b/libs/go-kibana-rest/kbapi/api.kibana_synthetics.go @@ -153,6 +153,7 @@ type SyntheticsMonitorConfig struct { PrivateLocations []string `json:"private_locations,omitempty"` Enabled *bool `json:"enabled,omitempty"` Tags []string `json:"tags,omitempty"` + Labels map[string]string `json:"labels"` Alert *MonitorAlertConfig `json:"alert,omitempty"` APMServiceName string `json:"service.name,omitempty"` TimeoutSeconds int `json:"timeout,omitempty"` @@ -208,6 +209,7 @@ type SyntheticsMonitor struct { Alert *MonitorAlertConfig `json:"alert,omitempty"` Schedule *MonitorScheduleConfig `json:"schedule,omitempty"` Tags []string `json:"tags,omitempty"` + Labels map[string]string `json:"labels,omitempty"` APMServiceName string `json:"service.name,omitempty"` Timeout json.Number `json:"timeout,omitempty"` Locations []MonitorLocationConfig `json:"locations,omitempty"`