Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
12 changes: 7 additions & 5 deletions internal/kibana/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ var MinVersionSupportingPreconfiguredIDs = version.Must(version.NewVersion("8.8.
func ResourceActionConnector() *schema.Resource {
var connectorSchema = map[string]*schema.Schema{
"connector_id": {
Description: "A UUID v1 or v4 to use instead of a randomly generated ID.",
Type: schema.TypeString,
Computed: true,
Optional: true,
ForceNew: true,
Description: "A UUID v1 or v4 to use instead of a randomly generated ID.",
Type: schema.TypeString,
Computed: true,
Optional: true,
ForceNew: true,
ValidateFunc: validation.IsUUID,
Copy link
Contributor

@nick-benoit nick-benoit Aug 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure IsUUID is the right validation here? This seems to have a very particular view of what counts as a UUID.

func ParseUUID(uuid string) ([]byte, error) {
	if len(uuid) != 2 * uuidLen + 4 {
		return nil, fmt.Errorf("uuid string is wrong length")
	}

	if uuid[8] != '-' ||
		uuid[13] != '-' ||
		uuid[18] != '-' ||
		uuid[23] != '-' {
		return nil, fmt.Errorf("uuid is improperly formatted")
	}

	hexStr := uuid[0:8] + uuid[9:13] + uuid[14:18] + uuid[19:23] + uuid[24:36]

	ret, err := hex.DecodeString(hexStr)
	if err != nil {
		return nil, err
	}
	if len(ret) != uuidLen {
		return nil, fmt.Errorf("decoded hex is the wrong length")
	}

	return ret, nil
}

Notably it doesn't seem to allow the example given in the issue that inspired this change "lugoz-safes-rusin-bubov-fytex-cydeb".

I can't seem to find any details in the connector api docs about requirements (but maybe i'm just not looking in the right place).

This open api doc I found in the Kibana repo seems to have a less opinionated view though

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the description of this field it mentions that the connector_id must be either UUIDv1 or UUIDv4, both of which are expected to match this validation. Manually testing with Kibana seems to back that assertion up, but I can't find anything in the Kibana docs to link to here.

Notably it doesn't seem to allow the example given in the issue that inspired this change "lugoz-safes-rusin-bubov-fytex-cydeb".

FWIW this value is rejected by Kibana (at least v9), I expect that ID has been made up for the issue rather than something that's expected to work.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the description of this field it mentions that the connector_id must be either UUIDv1 or UUIDv4

Yeah this is a good point. Presumably this comes from the Kibana open api spec somewhere?

This sounds good 👍 I just wanted to make sure we weren't adding a more strict validation than we intended.

Copy link
Member

@gigerdo gigerdo Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would also be totally fine to just rely on the validation of the API. So it would construct a request with a wrong UUID, and then Kibana would respond with a validation error that we can present to the user.
(I thas the advantage that we avoid the risk of having a more strict validation than the underlying API and preventing something that should actually work)

},
"space_id": {
Description: "An identifier for the space. If space_id is not provided, the default space is used.",
Expand Down Expand Up @@ -255,6 +256,7 @@ func expandActionConnector(d *schema.ResourceData) (models.KibanaActionConnector
var diags diag.Diagnostics

connector := models.KibanaActionConnector{
ConnectorID: d.Get("connector_id").(string),
SpaceID: d.Get("space_id").(string),
Name: d.Get("name").(string),
ConnectorTypeID: d.Get("connector_type_id").(string),
Expand Down
142 changes: 89 additions & 53 deletions internal/kibana/connector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import (
"github.com/elastic/terraform-provider-elasticstack/internal/acctest"
"github.com/elastic/terraform-provider-elasticstack/internal/clients"
"github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi"
"github.com/elastic/terraform-provider-elasticstack/internal/kibana"
"github.com/elastic/terraform-provider-elasticstack/internal/versionutils"
"github.com/google/uuid"
"github.com/hashicorp/go-version"
sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
Expand All @@ -21,7 +23,11 @@ func TestAccResourceKibanaConnectorCasesWebhook(t *testing.T) {

connectorName := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlphaNum)

create := func(name string) string {
create := func(name, id string) string {
idAttribute := ""
if id != "" {
idAttribute = fmt.Sprintf(`connector_id = "%s"`, id)
}
return fmt.Sprintf(`
provider "elasticstack" {
elasticsearch {}
Expand All @@ -30,6 +36,7 @@ func TestAccResourceKibanaConnectorCasesWebhook(t *testing.T) {

resource "elasticstack_kibana_action_connector" "test" {
name = "%s"
%s
config = jsonencode({
createIncidentJson = "{}"
createIncidentResponseKey = "key"
Expand All @@ -46,10 +53,14 @@ func TestAccResourceKibanaConnectorCasesWebhook(t *testing.T) {
})
connector_type_id = ".cases-webhook"
}`,
name)
name, idAttribute)
}

update := func(name string) string {
update := func(name, id string) string {
idAttribute := ""
if id != "" {
idAttribute = fmt.Sprintf(`connector_id = "%s"`, id)
}
return fmt.Sprintf(`
provider "elasticstack" {
elasticsearch {}
Expand All @@ -58,6 +69,7 @@ func TestAccResourceKibanaConnectorCasesWebhook(t *testing.T) {

resource "elasticstack_kibana_action_connector" "test" {
name = "Updated %s"
%s
config = jsonencode({
createIncidentJson = "{}"
createIncidentResponseKey = "key"
Expand All @@ -75,57 +87,81 @@ func TestAccResourceKibanaConnectorCasesWebhook(t *testing.T) {
})
connector_type_id = ".cases-webhook"
}`,
name)
name, idAttribute)
}

for _, connectorID := range []string{"", uuid.NewString()} {
t.Run(fmt.Sprintf("with connector ID '%s'", connectorID), func(t *testing.T) {
minVersion := minSupportedVersion
if connectorID != "" {
minVersion = kibana.MinVersionSupportingPreconfiguredIDs
}

resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },
CheckDestroy: checkResourceKibanaConnectorDestroy,
ProtoV6ProviderFactories: acctest.Providers,
Steps: []resource.TestStep{
{
SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersion),
Config: create(connectorName, connectorID),
Check: resource.ComposeTestCheckFunc(
testCommonAttributes(connectorName, ".cases-webhook"),

resource.TestCheckResourceAttrWith("elasticstack_kibana_action_connector.test", "connector_id", func(value string) error {
if connectorID == "" {
if _, err := uuid.Parse(value); err != nil {
return fmt.Errorf("expected connector_id to be a uuid: %w", err)
}

return nil
}

if connectorID != value {
return fmt.Errorf("expected connector_id to match pre-defined id. '%s' != %s", connectorID, value)
}

return nil
}),
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/\"`)),
// `post` is the default value that is returned by backend
resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "config", regexp.MustCompile(`"createIncidentMethod\":\"post\"`)),

resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "secrets", regexp.MustCompile(`\"user\":\"user1\"`)),
resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "secrets", regexp.MustCompile(`\"password\":\"password1\"`)),
),
},
{
SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersion),
Config: update(connectorName, connectorID),
Check: resource.ComposeTestCheckFunc(
testCommonAttributes(fmt.Sprintf("Updated %s", 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://elasticsearch\.com/\"`)),
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(`createIncidentMethod\":\"put\"`)),

resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "secrets", regexp.MustCompile(`\"user\":\"user2\"`)),
resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "secrets", regexp.MustCompile(`\"password\":\"password2\"`)),
),
},
},
})
})
}

resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },
CheckDestroy: checkResourceKibanaConnectorDestroy,
ProtoV6ProviderFactories: acctest.Providers,
Steps: []resource.TestStep{
{
SkipFunc: versionutils.CheckIfVersionIsUnsupported(minSupportedVersion),
Config: create(connectorName),
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/\"`)),
// `post` is the default value that is returned by backend
resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "config", regexp.MustCompile(`"createIncidentMethod\":\"post\"`)),

resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "secrets", regexp.MustCompile(`\"user\":\"user1\"`)),
resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "secrets", regexp.MustCompile(`\"password\":\"password1\"`)),
),
},
{
SkipFunc: versionutils.CheckIfVersionIsUnsupported(minSupportedVersion),
Config: update(connectorName),
Check: resource.ComposeTestCheckFunc(
testCommonAttributes(fmt.Sprintf("Updated %s", 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://elasticsearch\.com/\"`)),
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(`createIncidentMethod\":\"put\"`)),

resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "secrets", regexp.MustCompile(`\"user\":\"user2\"`)),
resource.TestMatchResourceAttr("elasticstack_kibana_action_connector.test", "secrets", regexp.MustCompile(`\"password\":\"password2\"`)),
),
},
},
})
}

func TestAccResourceKibanaConnectorEmail(t *testing.T) {
Expand Down
Loading