Skip to content
Merged
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 @@ -40,6 +40,7 @@ alias = [

### Changes

- 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))
- Add `host_name_format` to `elasticstack_fleet_agent_policy` to configure host name format (hostname or FQDN) ([#1312](https://github.com/elastic/terraform-provider-elasticstack/pull/1312))
- 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))
Expand Down
22 changes: 21 additions & 1 deletion internal/kibana/connectors/acc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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\"`)),
Expand Down Expand Up @@ -119,6 +119,26 @@ 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/\"`)),
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\"`)),
),
},
},
})
})
Expand Down
16 changes: 16 additions & 0 deletions internal/kibana/connectors/config_value.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -76,6 +77,21 @@ 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)
continue
}

if nestedMap, ok := value.(map[string]interface{}); ok {
removeNulls(nestedMap)
continue
}
}
}

// 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) {
Expand Down
41 changes: 41 additions & 0 deletions internal/kibana/connectors/config_value_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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"
}