Skip to content
Closed
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 .env
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ FLEET_CONTAINER_NAME=terraform-elasticstack-fleet
ACCEPTANCE_TESTS_CONTAINER_NAME=terraform-elasticstack-acceptance-tests
TOKEN_ACCEPTANCE_TESTS_CONTAINER_NAME=terraform-elasticstack-token-acceptance-tests
GOVERSION=1.25.1
TF_ELASTICSTACK_INCLUDE_EXPERIMENTAL=true
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ jobs:
ELASTIC_PASSWORD: password
KIBANA_SYSTEM_USERNAME: kibana_system
KIBANA_SYSTEM_PASSWORD: password
TF_ELASTICSTACK_INCLUDE_EXPERIMENTAL: true
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:${{ matrix.version }}
Expand Down
1 change: 1 addition & 0 deletions CODING_STANDARDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ This document outlines the coding standards and conventions used in the terrafor
- Prefer using existing util functions over longer form, duplicated code:
- `utils.IsKnown(val)` instead of `!val.IsNull() && !val.IsUnknown()`
- `utils.ListTypeAs` instead of `val.ElementsAs` or similar for other collection types
- The final state for a resource should be derived from a read request following a mutative request (eg create or update). We should not use the response from a mutative request to build the final resource state.

## Schema Definitions

Expand Down
3 changes: 2 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,8 @@ services:
ELASTICSEARCH_USERNAME: elastic
ELASTICSEARCH_PASSWORD: ${ELASTICSEARCH_PASSWORD}
TF_LOG: ${TF_LOG:-info}
command: make testacc TESTARGS=${TESTARGS:-}
TF_ELASTICSTACK_INCLUDE_EXPERIMENTAL: "true"
command: make testacc TESTARGS='${TESTARGS:-}'

token-acceptance-tests:
profiles: ["token-acceptance-tests"]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
resource "elasticstack_kibana_security_list" "ip_list" {
space_id = "default"
name = "Trusted IP Addresses"
description = "List of trusted IP addresses for security rules"
type = "ip"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
resource "elasticstack_kibana_security_list" "keyword_list" {
space_id = "security"
list_id = "custom-keywords"
name = "Custom Keywords"
description = "Custom keyword list for detection rules"
type = "keyword"
}
9,211 changes: 4,679 additions & 4,532 deletions generated/kbapi/kibana.gen.go

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions generated/kbapi/transform_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,11 @@ func transformKibanaPaths(schema *Schema) {
"/api/actions/connector/{id}",
"/api/actions/connectors",
"/api/detection_engine/rules",
"/api/exception_lists",
"/api/exception_lists/items",
"/api/lists",
"/api/lists/index",
"/api/lists/items",
}

// Add a spaceId parameter if not already present
Expand Down
154 changes: 154 additions & 0 deletions internal/clients/kibana_oapi/security_lists.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package kibana_oapi

import (
"context"
"net/http"

"github.com/elastic/terraform-provider-elasticstack/generated/kbapi"
"github.com/elastic/terraform-provider-elasticstack/internal/diagutil"
"github.com/hashicorp/terraform-plugin-framework/diag"
)

// CreateListIndex creates the .lists and .items data streams for a space if they don't exist.
// This is required before any list operations can be performed.
func CreateListIndex(ctx context.Context, client *Client, spaceId string) diag.Diagnostics {
resp, err := client.API.CreateListIndexWithResponse(ctx, kbapi.SpaceId(spaceId))
if err != nil {
return diagutil.FrameworkDiagFromError(err)
}

switch resp.StatusCode() {
case http.StatusOK:
return nil
default:
return reportUnknownError(resp.StatusCode(), resp.Body)
}
}

// GetList reads a security list from the API by ID
func GetList(ctx context.Context, client *Client, spaceId string, params *kbapi.ReadListParams) (*kbapi.ReadListResponse, diag.Diagnostics) {
resp, err := client.API.ReadListWithResponse(ctx, kbapi.SpaceId(spaceId), params)
if err != nil {
return nil, diagutil.FrameworkDiagFromError(err)
}

switch resp.StatusCode() {
case http.StatusOK:
return resp, nil
case http.StatusNotFound:
return nil, nil
default:
return nil, reportUnknownError(resp.StatusCode(), resp.Body)
}
}

// CreateList creates a new security list.
func CreateList(ctx context.Context, client *Client, spaceId string, body kbapi.CreateListJSONRequestBody) (*kbapi.CreateListResponse, diag.Diagnostics) {
resp, err := client.API.CreateListWithResponse(ctx, kbapi.SpaceId(spaceId), body)
if err != nil {
return nil, diagutil.FrameworkDiagFromError(err)
}

switch resp.StatusCode() {
case http.StatusOK:
return resp, nil
default:
return nil, reportUnknownError(resp.StatusCode(), resp.Body)
}
}

// UpdateList updates an existing security list.
func UpdateList(ctx context.Context, client *Client, spaceId string, body kbapi.UpdateListJSONRequestBody) (*kbapi.UpdateListResponse, diag.Diagnostics) {
resp, err := client.API.UpdateListWithResponse(ctx, kbapi.SpaceId(spaceId), body)
if err != nil {
return nil, diagutil.FrameworkDiagFromError(err)
}

switch resp.StatusCode() {
case http.StatusOK:
return resp, nil
default:
return nil, reportUnknownError(resp.StatusCode(), resp.Body)
}
}

// DeleteList deletes an existing security list.
func DeleteList(ctx context.Context, client *Client, spaceId string, params *kbapi.DeleteListParams) diag.Diagnostics {
resp, err := client.API.DeleteListWithResponse(ctx, kbapi.SpaceId(spaceId), params)
if err != nil {
return diagutil.FrameworkDiagFromError(err)
}

switch resp.StatusCode() {
case http.StatusOK:
return nil
case http.StatusNotFound:
return nil
default:
return reportUnknownError(resp.StatusCode(), resp.Body)
}
}

// GetListItem reads a security list item from the API by ID or list_id and value
func GetListItem(ctx context.Context, client *Client, spaceId string, params *kbapi.ReadListItemParams) (*kbapi.ReadListItemResponse, diag.Diagnostics) {
resp, err := client.API.ReadListItemWithResponse(ctx, kbapi.SpaceId(spaceId), params)
if err != nil {
return nil, diagutil.FrameworkDiagFromError(err)
}

switch resp.StatusCode() {
case http.StatusOK:
return resp, nil
case http.StatusNotFound:
return nil, nil
default:
return nil, reportUnknownError(resp.StatusCode(), resp.Body)
}
}

// CreateListItem creates a new security list item.
func CreateListItem(ctx context.Context, client *Client, spaceId string, body kbapi.CreateListItemJSONRequestBody) (*kbapi.CreateListItemResponse, diag.Diagnostics) {
resp, err := client.API.CreateListItemWithResponse(ctx, kbapi.SpaceId(spaceId), body)
if err != nil {
return nil, diagutil.FrameworkDiagFromError(err)
}

switch resp.StatusCode() {
case http.StatusOK:
return resp, nil
default:
return nil, reportUnknownError(resp.StatusCode(), resp.Body)
}
}

// UpdateListItem updates an existing security list item.
func UpdateListItem(ctx context.Context, client *Client, spaceId string, body kbapi.UpdateListItemJSONRequestBody) (*kbapi.UpdateListItemResponse, diag.Diagnostics) {
resp, err := client.API.UpdateListItemWithResponse(ctx, kbapi.SpaceId(spaceId), body)
if err != nil {
return nil, diagutil.FrameworkDiagFromError(err)
}

switch resp.StatusCode() {
case http.StatusOK:
return resp, nil
default:
return nil, reportUnknownError(resp.StatusCode(), resp.Body)
}
}

// DeleteListItem deletes an existing security list item.
func DeleteListItem(ctx context.Context, client *Client, spaceId string, params *kbapi.DeleteListItemParams) diag.Diagnostics {
resp, err := client.API.DeleteListItemWithResponse(ctx, kbapi.SpaceId(spaceId), params)
if err != nil {
return diagutil.FrameworkDiagFromError(err)
}

switch resp.StatusCode() {
case http.StatusOK:
return nil
case http.StatusNotFound:
return nil
default:
return reportUnknownError(resp.StatusCode(), resp.Body)
}
}
153 changes: 153 additions & 0 deletions internal/kibana/security_list/acc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package security_list_test

import (
"context"
"testing"

"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/google/uuid"
"github.com/hashicorp/terraform-plugin-testing/config"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)

func ensureListIndexExists(t *testing.T) {
client, err := clients.NewAcceptanceTestingClient()
if err != nil {
t.Fatalf("Failed to create client: %v", err)
}

kibanaClient, err := client.GetKibanaOapiClient()
if err != nil {
t.Fatalf("Failed to get Kibana client: %v", err)
}

diags := kibana_oapi.CreateListIndex(context.Background(), kibanaClient, "default")
if diags.HasError() {
// It's OK if it already exists, we'll only fail on other errors
for _, d := range diags {
if d.Summary() != "Unexpected status code from server: got HTTP 409" {
t.Fatalf("Failed to create list index: %v", d.Detail())
}
}
}
}

func TestAccResourceSecurityList(t *testing.T) {
listID := "test-list-" + uuid.New().String()
resource.Test(t, resource.TestCase{
PreCheck: func() {
acctest.PreCheck(t)
ensureListIndexExists(t)
},
ProtoV6ProviderFactories: acctest.Providers,
Steps: []resource.TestStep{
{ // Create
ConfigDirectory: acctest.NamedTestCaseDirectory("create"),
ConfigVariables: config.Variables{
"list_id": config.StringVariable(listID),
"name": config.StringVariable("Test Security List"),
"description": config.StringVariable("A test security list for IP addresses"),
"type": config.StringVariable("ip"),
},
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list.test", "id"),
resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "name", "Test Security List"),
resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "description", "A test security list for IP addresses"),
resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "type", "ip"),
resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list.test", "created_at"),
resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list.test", "created_by"),
resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list.test", "updated_at"),
resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list.test", "updated_by"),
),
},
{ // Update
ConfigDirectory: acctest.NamedTestCaseDirectory("update"),
ConfigVariables: config.Variables{
"list_id": config.StringVariable(listID),
"name": config.StringVariable("Updated Security List"),
"description": config.StringVariable("An updated test security list"),
"type": config.StringVariable("ip"),
},
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "name", "Updated Security List"),
resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "description", "An updated test security list"),
),
},
},
})
}

func TestAccResourceSecurityList_KeywordType(t *testing.T) {
listID := "keyword-list-" + uuid.New().String()
resource.Test(t, resource.TestCase{
PreCheck: func() {
acctest.PreCheck(t)
ensureListIndexExists(t)
},
ProtoV6ProviderFactories: acctest.Providers,
Steps: []resource.TestStep{
{
ConfigDirectory: acctest.NamedTestCaseDirectory("keyword_type"),
ConfigVariables: config.Variables{
"list_id": config.StringVariable(listID),
"name": config.StringVariable("Keyword Security List"),
"description": config.StringVariable("A test security list for keywords"),
"type": config.StringVariable("keyword"),
},
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "type", "keyword"),
),
},
},
})
}

func TestAccResourceSecurityList_SerializerDeserializer(t *testing.T) {
listID := "serializer-list-" + uuid.New().String()
resource.Test(t, resource.TestCase{
PreCheck: func() {
acctest.PreCheck(t)
ensureListIndexExists(t)
},
ProtoV6ProviderFactories: acctest.Providers,
Steps: []resource.TestStep{
{ // Create with serializer and deserializer
ConfigDirectory: acctest.NamedTestCaseDirectory("create"),
ConfigVariables: config.Variables{
"list_id": config.StringVariable(listID),
"name": config.StringVariable("Custom Serializer List"),
"description": config.StringVariable("A test list with custom serializer and deserializer"),
"type": config.StringVariable("ip"),
"serializer": config.StringVariable("{{ip}}"),
"deserializer": config.StringVariable("{{ip}}"),
},
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list.test", "id"),
resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "name", "Custom Serializer List"),
resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "type", "ip"),
resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "serializer", "{{ip}}"),
resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "deserializer", "{{ip}}"),
),
},
{ // Update name and description (serializer/deserializer are immutable)
ConfigDirectory: acctest.NamedTestCaseDirectory("update"),
ConfigVariables: config.Variables{
"list_id": config.StringVariable(listID),
"name": config.StringVariable("Updated Serializer List"),
"description": config.StringVariable("Updated test list description"),
"type": config.StringVariable("ip"),
"serializer": config.StringVariable("{{ip}}"),
"deserializer": config.StringVariable("{{ip}}"),
},
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "name", "Updated Serializer List"),
resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "description", "Updated test list description"),
resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "serializer", "{{ip}}"),
resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "deserializer", "{{ip}}"),
),
},
},
})
}
Loading