From d75d300b50fcf1a1a4198fc8a3a1969ca739b12f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Dec 2025 20:50:39 +0000 Subject: [PATCH 1/7] Initial plan From b6fe4e6210f0c164d36660dc88fc9c3b4d80dc8a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Dec 2025 21:03:18 +0000 Subject: [PATCH 2/7] Add null value removal to SanitizedValue function and unit tests Co-authored-by: tobio <444668+tobio@users.noreply.github.com> --- internal/kibana/connectors/acc_test.go | 32 +++++++++++++++ internal/kibana/connectors/config_value.go | 12 ++++++ .../kibana/connectors/config_value_test.go | 41 +++++++++++++++++++ .../null_headers/connector.tf | 24 +++++++++++ .../null_headers/connector.tf | 30 ++++++++++++++ 5 files changed, 139 insertions(+) create mode 100644 internal/kibana/connectors/testdata/TestAccResourceKibanaConnectorCasesWebhook/with_empty_connector_id/null_headers/connector.tf create mode 100644 internal/kibana/connectors/testdata/TestAccResourceKibanaConnectorCasesWebhook/with_predefined_connector_id/null_headers/connector.tf diff --git a/internal/kibana/connectors/acc_test.go b/internal/kibana/connectors/acc_test.go index 367fe6597..39a73868e 100644 --- a/internal/kibana/connectors/acc_test.go +++ b/internal/kibana/connectors/acc_test.go @@ -119,6 +119,38 @@ func TestAccResourceKibanaConnectorCasesWebhook(t *testing.T) { resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "secrets", regexp.MustCompile(`\"password\":\"password2\"`)), ), }, + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(tc.minVersion), + ConfigDirectory: acctest.NamedTestCaseDirectory("null_headers"), + ConfigVariables: vars, + Check: resource.ComposeTestCheckFunc( + testCommonAttributes(connectorName, ".cases-webhook"), + + resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "config", regexp.MustCompile(`\"createIncidentJson\":\"{}\"`)), + resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "config", regexp.MustCompile(`\"createIncidentResponseKey\":\"key\"`)), + resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "config", regexp.MustCompile(`\"createIncidentUrl\":\"https://www\.elastic\.co/\"`)), + resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "config", regexp.MustCompile(`\"getIncidentResponseExternalTitleKey\":\"title\"`)), + resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "config", regexp.MustCompile(`\"getIncidentUrl\":\"https://www\.elastic\.co/\"`)), + resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "config", regexp.MustCompile(`\"updateIncidentJson\":\"{}\"`)), + resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "config", regexp.MustCompile(`\"updateIncidentUrl\":\"https://www.elastic\.co/\"`)), + resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "config", regexp.MustCompile(`\"viewIncidentUrl\":\"https://www\.elastic\.co/\"`)), + // Verify that null headers field is removed from the config + func(s *terraform.State) error { + rs, ok := s.RootModule().Resources["elasticstack_kibana_action_connector.test"] + if !ok { + return fmt.Errorf("resource not found") + } + configStr := rs.Primary.Attributes["config"] + if regexp.MustCompile(`\"headers\"`).MatchString(configStr) { + return fmt.Errorf("headers field should not be present in config when null, got: %s", configStr) + } + return nil + }, + + resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "secrets", regexp.MustCompile(`\"user\":\"user1\"`)), + resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "secrets", regexp.MustCompile(`\"password\":\"password1\"`)), + ), + }, }, }) }) diff --git a/internal/kibana/connectors/config_value.go b/internal/kibana/connectors/config_value.go index baafc3fa1..dc29959a7 100644 --- a/internal/kibana/connectors/config_value.go +++ b/internal/kibana/connectors/config_value.go @@ -67,6 +67,7 @@ func (v ConfigValue) SanitizedValue() (string, diag.Diagnostics) { } delete(unsanitizedMap, connectorTypeIDKey) + removeNulls(unsanitizedMap) sanitizedValue, err := json.Marshal(unsanitizedMap) if err != nil { diags.AddError("Failed to marshal sanitized config value", err.Error()) @@ -76,6 +77,17 @@ func (v ConfigValue) SanitizedValue() (string, diag.Diagnostics) { return string(sanitizedValue), diags } +// removeNulls recursively removes all null values from the map +func removeNulls(m map[string]interface{}) { + for key, value := range m { + if value == nil { + delete(m, key) + } else if nestedMap, ok := value.(map[string]interface{}); ok { + removeNulls(nestedMap) + } + } +} + // StringSemanticEquals returns true if the given config object value is semantically equal to the current config object value. // The comparison will ignore any default values present in one value, but unset in the other. func (v ConfigValue) StringSemanticEquals(ctx context.Context, newValuable basetypes.StringValuable) (bool, diag.Diagnostics) { diff --git a/internal/kibana/connectors/config_value_test.go b/internal/kibana/connectors/config_value_test.go index b651ad2bf..84ee3ca25 100644 --- a/internal/kibana/connectors/config_value_test.go +++ b/internal/kibana/connectors/config_value_test.go @@ -125,6 +125,47 @@ func TestConfigValue_SanitizedValue(t *testing.T) { expectError: true, errorContains: "Failed to unmarshal config value", }, + { + name: "JSON with null values gets sanitized - top level", + configValue: ConfigValue{ + Normalized: jsontypes.NewNormalizedValue(`{"key": "value", "nullField": null, "another": "field"}`), + }, + expectedResult: `{"another":"field","key":"value"}`, + expectError: false, + }, + { + name: "JSON with null values gets sanitized - nested", + configValue: ConfigValue{ + Normalized: jsontypes.NewNormalizedValue(`{"key": "value", "nested": {"field": "value", "nullField": null}}`), + }, + expectedResult: `{"key":"value","nested":{"field":"value"}}`, + expectError: false, + }, + { + name: "JSON with null values gets sanitized - mixed", + configValue: ConfigValue{ + Normalized: jsontypes.NewNormalizedValue(`{"key": "value", "nullTop": null, "nested": {"field": "value", "nullNested": null}, "another": null}`), + }, + expectedResult: `{"key":"value","nested":{"field":"value"}}`, + expectError: false, + }, + { + name: "JSON with only null values results in empty object", + configValue: ConfigValue{ + Normalized: jsontypes.NewNormalizedValue(`{"nullField1": null, "nullField2": null}`), + }, + expectedResult: `{}`, + expectError: false, + }, + { + name: "JSON with null and connector type ID gets both sanitized", + configValue: ConfigValue{ + Normalized: jsontypes.NewNormalizedValue(`{"key": "value", "nullField": null, "__tf_provider_connector_type_id": "test-connector"}`), + connectorTypeID: "test-connector", + }, + expectedResult: `{"key":"value"}`, + expectError: false, + }, } for _, tt := range tests { diff --git a/internal/kibana/connectors/testdata/TestAccResourceKibanaConnectorCasesWebhook/with_empty_connector_id/null_headers/connector.tf b/internal/kibana/connectors/testdata/TestAccResourceKibanaConnectorCasesWebhook/with_empty_connector_id/null_headers/connector.tf new file mode 100644 index 000000000..ba9320cd5 --- /dev/null +++ b/internal/kibana/connectors/testdata/TestAccResourceKibanaConnectorCasesWebhook/with_empty_connector_id/null_headers/connector.tf @@ -0,0 +1,24 @@ +variable "connector_name" { + description = "The connector name" + type = string +} + +resource "elasticstack_kibana_action_connector" "test" { + name = var.connector_name + config = jsonencode({ + createIncidentJson = "{}" + createIncidentResponseKey = "key" + createIncidentUrl = "https://www.elastic.co/" + getIncidentResponseExternalTitleKey = "title" + getIncidentUrl = "https://www.elastic.co/" + headers = null + updateIncidentJson = "{}" + updateIncidentUrl = "https://www.elastic.co/" + viewIncidentUrl = "https://www.elastic.co/" + }) + secrets = jsonencode({ + user = "user1" + password = "password1" + }) + connector_type_id = ".cases-webhook" +} diff --git a/internal/kibana/connectors/testdata/TestAccResourceKibanaConnectorCasesWebhook/with_predefined_connector_id/null_headers/connector.tf b/internal/kibana/connectors/testdata/TestAccResourceKibanaConnectorCasesWebhook/with_predefined_connector_id/null_headers/connector.tf new file mode 100644 index 000000000..17b13f9cf --- /dev/null +++ b/internal/kibana/connectors/testdata/TestAccResourceKibanaConnectorCasesWebhook/with_predefined_connector_id/null_headers/connector.tf @@ -0,0 +1,30 @@ +variable "connector_name" { + description = "The connector name" + type = string +} + +variable "connector_id" { + description = "Connector ID" + type = string +} + +resource "elasticstack_kibana_action_connector" "test" { + name = var.connector_name + connector_id = var.connector_id + config = jsonencode({ + createIncidentJson = "{}" + createIncidentResponseKey = "key" + createIncidentUrl = "https://www.elastic.co/" + getIncidentResponseExternalTitleKey = "title" + getIncidentUrl = "https://www.elastic.co/" + headers = null + updateIncidentJson = "{}" + updateIncidentUrl = "https://www.elastic.co/" + viewIncidentUrl = "https://www.elastic.co/" + }) + secrets = jsonencode({ + user = "user1" + password = "password1" + }) + connector_type_id = ".cases-webhook" +} From bb44732aca15f34f71efd7d782f1f1e8d079c391 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Dec 2025 21:08:15 +0000 Subject: [PATCH 3/7] Fix regex escaping consistency in acceptance test Co-authored-by: tobio <444668+tobio@users.noreply.github.com> --- internal/kibana/connectors/acc_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/kibana/connectors/acc_test.go b/internal/kibana/connectors/acc_test.go index 39a73868e..dcdda9ca2 100644 --- a/internal/kibana/connectors/acc_test.go +++ b/internal/kibana/connectors/acc_test.go @@ -91,7 +91,7 @@ func TestAccResourceKibanaConnectorCasesWebhook(t *testing.T) { resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "config", regexp.MustCompile(`\"getIncidentResponseExternalTitleKey\":\"title\"`)), resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "config", regexp.MustCompile(`\"getIncidentUrl\":\"https://www\.elastic\.co/\"`)), resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "config", regexp.MustCompile(`\"updateIncidentJson\":\"{}\"`)), - resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "config", regexp.MustCompile(`\"updateIncidentUrl\":\"https://www.elastic\.co/\"`)), + resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "config", regexp.MustCompile(`\"updateIncidentUrl\":\"https://www\.elastic\.co/\"`)), resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "config", regexp.MustCompile(`\"viewIncidentUrl\":\"https://www\.elastic\.co/\"`)), resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "secrets", regexp.MustCompile(`\"user\":\"user1\"`)), @@ -132,7 +132,7 @@ func TestAccResourceKibanaConnectorCasesWebhook(t *testing.T) { resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "config", regexp.MustCompile(`\"getIncidentResponseExternalTitleKey\":\"title\"`)), resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "config", regexp.MustCompile(`\"getIncidentUrl\":\"https://www\.elastic\.co/\"`)), resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "config", regexp.MustCompile(`\"updateIncidentJson\":\"{}\"`)), - resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "config", regexp.MustCompile(`\"updateIncidentUrl\":\"https://www.elastic\.co/\"`)), + resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "config", regexp.MustCompile(`\"updateIncidentUrl\":\"https://www\.elastic\.co/\"`)), resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "config", regexp.MustCompile(`\"viewIncidentUrl\":\"https://www\.elastic\.co/\"`)), // Verify that null headers field is removed from the config func(s *terraform.State) error { From b2f795e2e8127b32eba75de465983e6a2b69d8b6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Dec 2025 22:09:31 +0000 Subject: [PATCH 4/7] Refactor removeNulls to use explicit continue statements Co-authored-by: tobio <444668+tobio@users.noreply.github.com> --- internal/kibana/connectors/config_value.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/kibana/connectors/config_value.go b/internal/kibana/connectors/config_value.go index dc29959a7..14a684712 100644 --- a/internal/kibana/connectors/config_value.go +++ b/internal/kibana/connectors/config_value.go @@ -82,8 +82,12 @@ func removeNulls(m map[string]interface{}) { for key, value := range m { if value == nil { delete(m, key) - } else if nestedMap, ok := value.(map[string]interface{}); ok { + continue + } + + if nestedMap, ok := value.(map[string]interface{}); ok { removeNulls(nestedMap) + continue } } } From a689ed4f94415584d366e42f537c007f77496324 Mon Sep 17 00:00:00 2001 From: Toby Brain Date: Tue, 9 Dec 2025 09:36:42 +1100 Subject: [PATCH 5/7] Fix tests --- internal/kibana/connectors/acc_test.go | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/internal/kibana/connectors/acc_test.go b/internal/kibana/connectors/acc_test.go index dcdda9ca2..5817cb4b5 100644 --- a/internal/kibana/connectors/acc_test.go +++ b/internal/kibana/connectors/acc_test.go @@ -134,19 +134,7 @@ func TestAccResourceKibanaConnectorCasesWebhook(t *testing.T) { resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "config", regexp.MustCompile(`\"updateIncidentJson\":\"{}\"`)), resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "config", regexp.MustCompile(`\"updateIncidentUrl\":\"https://www\.elastic\.co/\"`)), resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "config", regexp.MustCompile(`\"viewIncidentUrl\":\"https://www\.elastic\.co/\"`)), - // Verify that null headers field is removed from the config - func(s *terraform.State) error { - rs, ok := s.RootModule().Resources["elasticstack_kibana_action_connector.test"] - if !ok { - return fmt.Errorf("resource not found") - } - configStr := rs.Primary.Attributes["config"] - if regexp.MustCompile(`\"headers\"`).MatchString(configStr) { - return fmt.Errorf("headers field should not be present in config when null, got: %s", configStr) - } - return nil - }, - + resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "config", regexp.MustCompile(`\"headers\":null`)), resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "secrets", regexp.MustCompile(`\"user\":\"user1\"`)), resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "secrets", regexp.MustCompile(`\"password\":\"password1\"`)), ), From 69ff68bf09286a0baf66a31039c0e4ad3dd65e42 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 07:50:56 +0000 Subject: [PATCH 6/7] Add CHANGELOG entry for null value handling fix Co-authored-by: tobio <444668+tobio@users.noreply.github.com> --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07d68cb2f..98b0c79c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ alias = [ ### Changes +- Fix `elasticstack_kibana_action_connector` failing with "inconsistent result after apply" when config contains null values - Create `elasticstack_kibana_prebuilt_rule` resource ([#1296](https://github.com/elastic/terraform-provider-elasticstack/pull/1296)) - Add `required_versions` to `elasticstack_fleet_agent_policy` ([#1436](https://github.com/elastic/terraform-provider-elasticstack/pull/1436)) - Migrate `elasticstack_elasticsearch_security_role` resource to Terraform Plugin Framework ([#1330](https://github.com/elastic/terraform-provider-elasticstack/pull/1330)) From 36df21d5896213cc8b5660b0dfaaf726666d3e17 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 09:47:54 +0000 Subject: [PATCH 7/7] Update CHANGELOG entry to include PR number for consistency Co-authored-by: tobio <444668+tobio@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98b0c79c5..235680108 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,7 +40,7 @@ alias = [ ### Changes -- Fix `elasticstack_kibana_action_connector` failing with "inconsistent result after apply" when config contains null values +- Fix `elasticstack_kibana_action_connector` failing with "inconsistent result after apply" when config contains null values ([#1524](https://github.com/elastic/terraform-provider-elasticstack/pull/1524)) - Create `elasticstack_kibana_prebuilt_rule` resource ([#1296](https://github.com/elastic/terraform-provider-elasticstack/pull/1296)) - Add `required_versions` to `elasticstack_fleet_agent_policy` ([#1436](https://github.com/elastic/terraform-provider-elasticstack/pull/1436)) - Migrate `elasticstack_elasticsearch_security_role` resource to Terraform Plugin Framework ([#1330](https://github.com/elastic/terraform-provider-elasticstack/pull/1330))