diff --git a/examples/resources/elasticstack_kibana_security_list_item/resource.tf b/examples/resources/elasticstack_kibana_security_list_item/resource.tf new file mode 100644 index 000000000..e74bca6a6 --- /dev/null +++ b/examples/resources/elasticstack_kibana_security_list_item/resource.tf @@ -0,0 +1,18 @@ +# First create a security list +resource "elasticstack_kibana_security_list" "my_list" { + list_id = "allowed_domains" + name = "Allowed Domains" + description = "List of allowed domains" + type = "keyword" +} + +# Add an item to the list +resource "elasticstack_kibana_security_list_item" "domain_example" { + list_id = elasticstack_kibana_security_list.my_list.list_id + value = "example.com" + meta = jsonencode({ + category = "internal" + owner = "infrastructure-team" + note = "Primary internal domain" + }) +} diff --git a/internal/clients/kibana_oapi/security_lists.go b/internal/clients/kibana_oapi/security_lists.go index 454e44d89..e1856e731 100644 --- a/internal/clients/kibana_oapi/security_lists.go +++ b/internal/clients/kibana_oapi/security_lists.go @@ -2,6 +2,7 @@ package kibana_oapi import ( "context" + "encoding/json" "net/http" "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" @@ -26,7 +27,7 @@ func CreateListIndex(ctx context.Context, client *Client, spaceId string) diag.D } // 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) { +func GetList(ctx context.Context, client *Client, spaceId string, params *kbapi.ReadListParams) (*kbapi.SecurityListsAPIList, diag.Diagnostics) { resp, err := client.API.ReadListWithResponse(ctx, kbapi.SpaceId(spaceId), params) if err != nil { return nil, diagutil.FrameworkDiagFromError(err) @@ -34,7 +35,12 @@ func GetList(ctx context.Context, client *Client, spaceId string, params *kbapi. switch resp.StatusCode() { case http.StatusOK: - return resp, nil + if resp.JSON200 == nil { + return nil, diag.Diagnostics{ + diag.NewErrorDiagnostic("Failed to parse list response", "API returned 200 but JSON200 is nil"), + } + } + return resp.JSON200, nil case http.StatusNotFound: return nil, nil default: @@ -43,7 +49,7 @@ func GetList(ctx context.Context, client *Client, spaceId string, params *kbapi. } // CreateList creates a new security list. -func CreateList(ctx context.Context, client *Client, spaceId string, body kbapi.CreateListJSONRequestBody) (*kbapi.CreateListResponse, diag.Diagnostics) { +func CreateList(ctx context.Context, client *Client, spaceId string, body kbapi.CreateListJSONRequestBody) (*kbapi.SecurityListsAPIList, diag.Diagnostics) { resp, err := client.API.CreateListWithResponse(ctx, kbapi.SpaceId(spaceId), body) if err != nil { return nil, diagutil.FrameworkDiagFromError(err) @@ -51,14 +57,19 @@ func CreateList(ctx context.Context, client *Client, spaceId string, body kbapi. switch resp.StatusCode() { case http.StatusOK: - return resp, nil + if resp.JSON200 == nil { + return nil, diag.Diagnostics{ + diag.NewErrorDiagnostic("Failed to parse list response", "API returned 200 but JSON200 is nil"), + } + } + return resp.JSON200, 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) { +func UpdateList(ctx context.Context, client *Client, spaceId string, body kbapi.UpdateListJSONRequestBody) (*kbapi.SecurityListsAPIList, diag.Diagnostics) { resp, err := client.API.UpdateListWithResponse(ctx, kbapi.SpaceId(spaceId), body) if err != nil { return nil, diagutil.FrameworkDiagFromError(err) @@ -66,7 +77,12 @@ func UpdateList(ctx context.Context, client *Client, spaceId string, body kbapi. switch resp.StatusCode() { case http.StatusOK: - return resp, nil + if resp.JSON200 == nil { + return nil, diag.Diagnostics{ + diag.NewErrorDiagnostic("Failed to parse list response", "API returned 200 but JSON200 is nil"), + } + } + return resp.JSON200, nil default: return nil, reportUnknownError(resp.StatusCode(), resp.Body) } @@ -90,7 +106,9 @@ func DeleteList(ctx context.Context, client *Client, spaceId string, params *kba } // 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) { +// The response can be a single item or an array, so we unmarshal from the body. +// When querying by ID, we expect a single item. +func GetListItem(ctx context.Context, client *Client, spaceId string, params *kbapi.ReadListItemParams) (*kbapi.SecurityListsAPIListItem, diag.Diagnostics) { resp, err := client.API.ReadListItemWithResponse(ctx, kbapi.SpaceId(spaceId), params) if err != nil { return nil, diagutil.FrameworkDiagFromError(err) @@ -98,7 +116,14 @@ func GetListItem(ctx context.Context, client *Client, spaceId string, params *kb switch resp.StatusCode() { case http.StatusOK: - return resp, nil + var listItem kbapi.SecurityListsAPIListItem + if err := json.Unmarshal(resp.Body, &listItem); err != nil { + return nil, diag.Diagnostics{ + diag.NewErrorDiagnostic("Failed to parse list item response", err.Error()), + } + } + + return &listItem, nil case http.StatusNotFound: return nil, nil default: @@ -107,7 +132,7 @@ func GetListItem(ctx context.Context, client *Client, spaceId string, params *kb } // CreateListItem creates a new security list item. -func CreateListItem(ctx context.Context, client *Client, spaceId string, body kbapi.CreateListItemJSONRequestBody) (*kbapi.CreateListItemResponse, diag.Diagnostics) { +func CreateListItem(ctx context.Context, client *Client, spaceId string, body kbapi.CreateListItemJSONRequestBody) (*kbapi.SecurityListsAPIListItem, diag.Diagnostics) { resp, err := client.API.CreateListItemWithResponse(ctx, kbapi.SpaceId(spaceId), body) if err != nil { return nil, diagutil.FrameworkDiagFromError(err) @@ -115,14 +140,19 @@ func CreateListItem(ctx context.Context, client *Client, spaceId string, body kb switch resp.StatusCode() { case http.StatusOK: - return resp, nil + if resp.JSON200 == nil { + return nil, diag.Diagnostics{ + diag.NewErrorDiagnostic("Failed to parse list item response", "API returned 200 but JSON200 is nil"), + } + } + return resp.JSON200, 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) { +func UpdateListItem(ctx context.Context, client *Client, spaceId string, body kbapi.UpdateListItemJSONRequestBody) (*kbapi.SecurityListsAPIListItem, diag.Diagnostics) { resp, err := client.API.UpdateListItemWithResponse(ctx, kbapi.SpaceId(spaceId), body) if err != nil { return nil, diagutil.FrameworkDiagFromError(err) @@ -130,7 +160,12 @@ func UpdateListItem(ctx context.Context, client *Client, spaceId string, body kb switch resp.StatusCode() { case http.StatusOK: - return resp, nil + if resp.JSON200 == nil { + return nil, diag.Diagnostics{ + diag.NewErrorDiagnostic("Failed to parse list item response", "API returned 200 but JSON200 is nil"), + } + } + return resp.JSON200, nil default: return nil, reportUnknownError(resp.StatusCode(), resp.Body) } diff --git a/internal/kibana/security_list/create.go b/internal/kibana/security_list/create.go index 81e9b4109..63c0bd202 100644 --- a/internal/kibana/security_list/create.go +++ b/internal/kibana/security_list/create.go @@ -31,36 +31,36 @@ func (r *securityListResource) Create(ctx context.Context, req resource.CreateRe // Create the list spaceID := plan.SpaceID.ValueString() - createResp, diags := kibana_oapi.CreateList(ctx, client, spaceID, *createReq) + createdList, diags := kibana_oapi.CreateList(ctx, client, spaceID, *createReq) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - if createResp == nil || createResp.JSON200 == nil { + if createdList == nil { resp.Diagnostics.AddError("Failed to create security list", "API returned empty response") return } // Read the created list to populate state readParams := &kbapi.ReadListParams{ - Id: createResp.JSON200.Id, + Id: createdList.Id, } - readResp, diags := kibana_oapi.GetList(ctx, client, spaceID, readParams) + list, diags := kibana_oapi.GetList(ctx, client, spaceID, readParams) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - if readResp == nil || readResp.JSON200 == nil { + if list == nil { resp.State.RemoveResource(ctx) resp.Diagnostics.AddError("Failed to fetch security list", "API returned empty response") return } // Update state with read response - diags = plan.fromAPI(ctx, readResp.JSON200) + diags = plan.fromAPI(ctx, list) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return diff --git a/internal/kibana/security_list/models.go b/internal/kibana/security_list/models.go index 5b97f2bc5..40ccc8e57 100644 --- a/internal/kibana/security_list/models.go +++ b/internal/kibana/security_list/models.go @@ -8,6 +8,7 @@ import ( "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" "github.com/elastic/terraform-provider-elasticstack/internal/clients" "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/elastic/terraform-provider-elasticstack/internal/utils/typeutils" "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" @@ -120,10 +121,10 @@ func (m *SecurityListModel) fromAPI(ctx context.Context, apiList *kbapi.Security } m.ID = types.StringValue(compId.String()) - m.ListID = utils.StringishValue(apiList.Id) - m.Name = utils.StringishValue(apiList.Name) - m.Description = utils.StringishValue(apiList.Description) - m.Type = utils.StringishValue(apiList.Type) + m.ListID = typeutils.StringishValue(apiList.Id) + m.Name = typeutils.StringishValue(apiList.Name) + m.Description = typeutils.StringishValue(apiList.Description) + m.Type = typeutils.StringishValue(apiList.Type) m.Immutable = types.BoolValue(apiList.Immutable) m.Version = types.Int64Value(int64(apiList.Version)) m.TieBreakerID = types.StringValue(apiList.TieBreakerId) @@ -133,11 +134,11 @@ func (m *SecurityListModel) fromAPI(ctx context.Context, apiList *kbapi.Security m.UpdatedBy = types.StringValue(apiList.UpdatedBy) // Set optional _version field - m.VersionID = utils.StringishPointerValue(apiList.UnderscoreVersion) + m.VersionID = typeutils.StringishPointerValue(apiList.UnderscoreVersion) - m.Deserializer = utils.StringishPointerValue(apiList.Deserializer) + m.Deserializer = typeutils.StringishPointerValue(apiList.Deserializer) - m.Serializer = utils.StringishPointerValue(apiList.Serializer) + m.Serializer = typeutils.StringishPointerValue(apiList.Serializer) if apiList.Meta != nil { metaBytes, err := json.Marshal(apiList.Meta) diff --git a/internal/kibana/security_list/read.go b/internal/kibana/security_list/read.go index 045d63df4..0564ed4e0 100644 --- a/internal/kibana/security_list/read.go +++ b/internal/kibana/security_list/read.go @@ -39,19 +39,19 @@ func (r *securityListResource) Read(ctx context.Context, req resource.ReadReques Id: kbapi.SecurityListsAPIListId(listID), } - readResp, diags := kibana_oapi.GetList(ctx, client, spaceID, params) + list, diags := kibana_oapi.GetList(ctx, client, spaceID, params) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - if readResp == nil || readResp.JSON200 == nil { + if list == nil { resp.State.RemoveResource(ctx) return } // Convert API response to model - diags = state.fromAPI(ctx, readResp.JSON200) + diags = state.fromAPI(ctx, list) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return diff --git a/internal/kibana/security_list/schema.go b/internal/kibana/security_list/schema.go index d8ce9e0da..0eedc98f7 100644 --- a/internal/kibana/security_list/schema.go +++ b/internal/kibana/security_list/schema.go @@ -91,7 +91,7 @@ func (r *securityListResource) Schema(_ context.Context, _ resource.SchemaReques Computed: true, }, "version_id": schema.StringAttribute{ - MarkdownDescription: "The version id, normally returned by the API when the document is retrieved. Use it ensure updates are done against the latest version.", + MarkdownDescription: "The version id, normally returned by the API when the document is retrieved. Use it to ensure updates are done against the latest version.", Computed: true, }, "immutable": schema.BoolAttribute{ diff --git a/internal/kibana/security_list/update.go b/internal/kibana/security_list/update.go index 475f805cd..1022a4a53 100644 --- a/internal/kibana/security_list/update.go +++ b/internal/kibana/security_list/update.go @@ -37,36 +37,36 @@ func (r *securityListResource) Update(ctx context.Context, req resource.UpdateRe // Update the list spaceID := plan.SpaceID.ValueString() - updateResp, diags := kibana_oapi.UpdateList(ctx, client, spaceID, *updateReq) + updatedList, diags := kibana_oapi.UpdateList(ctx, client, spaceID, *updateReq) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - if updateResp == nil || updateResp.JSON200 == nil { + if updatedList == nil { resp.Diagnostics.AddError("Failed to update security list", "API returned empty response") return } // Read the updated list to populate state readParams := &kbapi.ReadListParams{ - Id: updateResp.JSON200.Id, + Id: updatedList.Id, } - readResp, diags := kibana_oapi.GetList(ctx, client, spaceID, readParams) + list, diags := kibana_oapi.GetList(ctx, client, spaceID, readParams) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - if readResp == nil || readResp.JSON200 == nil { + if list == nil { resp.State.RemoveResource(ctx) resp.Diagnostics.AddError("Failed to fetch security list", "API returned empty response") return } // Update state with read response - diags = plan.fromAPI(ctx, readResp.JSON200) + diags = plan.fromAPI(ctx, list) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return diff --git a/internal/kibana/security_list_item/acc_test.go b/internal/kibana/security_list_item/acc_test.go new file mode 100644 index 000000000..4e2dc84d1 --- /dev/null +++ b/internal/kibana/security_list_item/acc_test.go @@ -0,0 +1,269 @@ +package security_list_item_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 TestAccResourceSecurityListItem(t *testing.T) { + listID := "test-list-items-" + uuid.New().String() + value1 := "test-value-1" + valueUpdated := "test-value-updated" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + ensureListIndexExistsInSpace(t, "default") + }, + ProtoV6ProviderFactories: acctest.Providers, + Steps: []resource.TestStep{ + { // Create + ConfigDirectory: acctest.NamedTestCaseDirectory("create"), + ConfigVariables: config.Variables{ + "list_id": config.StringVariable(listID), + "value": config.StringVariable(value1), + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list_item.test", "id"), + resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "value", value1), + resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list_item.test", "created_at"), + resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list_item.test", "created_by"), + resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list_item.test", "updated_at"), + resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list_item.test", "updated_by"), + ), + }, + { // Update + ConfigDirectory: acctest.NamedTestCaseDirectory("update"), + ConfigVariables: config.Variables{ + "list_id": config.StringVariable(listID), + "value": config.StringVariable(valueUpdated), + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "value", valueUpdated), + ), + }, + { // Import + ConfigDirectory: acctest.NamedTestCaseDirectory("update"), + ConfigVariables: config.Variables{ + "list_id": config.StringVariable(listID), + "value": config.StringVariable(valueUpdated), + }, + ResourceName: "elasticstack_kibana_security_list_item.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccResourceSecurityListItem_WithMeta(t *testing.T) { + listID := "test-list-items-meta-" + uuid.New().String() + value := "test-value-with-meta" + meta1 := `{"category":"suspicious","severity":"high"}` + meta2 := `{"category":"malicious","notes":"Updated metadata","severity":"critical"}` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + ensureListIndexExistsInSpace(t, "default") + }, + ProtoV6ProviderFactories: acctest.Providers, + Steps: []resource.TestStep{ + { // Create with meta + ConfigDirectory: acctest.NamedTestCaseDirectory("create"), + ConfigVariables: config.Variables{ + "list_id": config.StringVariable(listID), + "value": config.StringVariable(value), + "meta": config.StringVariable(meta1), + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list_item.test", "id"), + resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "value", value), + resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "meta", meta1), + resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list_item.test", "created_at"), + resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list_item.test", "created_by"), + resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list_item.test", "updated_at"), + resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list_item.test", "updated_by"), + ), + }, + { // Update meta + ConfigDirectory: acctest.NamedTestCaseDirectory("update"), + ConfigVariables: config.Variables{ + "list_id": config.StringVariable(listID), + "value": config.StringVariable(value), + "meta": config.StringVariable(meta2), + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "value", value), + resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "meta", meta2), + ), + }, + { // Import + ConfigDirectory: acctest.NamedTestCaseDirectory("update"), + ConfigVariables: config.Variables{ + "list_id": config.StringVariable(listID), + "value": config.StringVariable(value), + "meta": config.StringVariable(meta2), + }, + ResourceName: "elasticstack_kibana_security_list_item.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccResourceSecurityListItem_Space(t *testing.T) { + spaceID := "test-space-" + uuid.New().String() + listID := "test-list-" + uuid.New().String() + spaceName := "Test Security Lists Space" + listName := "IP Blocklist" + listType := "ip" + value1 := "192.168.1.1" + value2 := "10.0.0.1" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + ensureListIndexExistsInSpace(t, spaceID) + }, + ProtoV6ProviderFactories: acctest.Providers, + Steps: []resource.TestStep{ + { // Create space, list, and list item + ConfigDirectory: acctest.NamedTestCaseDirectory("space_create"), + ConfigVariables: config.Variables{ + "space_id": config.StringVariable(spaceID), + "list_id": config.StringVariable(listID), + "value": config.StringVariable(value1), + }, + Check: resource.ComposeTestCheckFunc( + // Check space + resource.TestCheckResourceAttr("elasticstack_kibana_space.test", "space_id", spaceID), + resource.TestCheckResourceAttr("elasticstack_kibana_space.test", "name", spaceName), + // Check list + resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list.test", "id"), + resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "space_id", spaceID), + resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "list_id", listID), + resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "name", listName), + resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "type", listType), + // Check list item + resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list_item.test", "id"), + resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "space_id", spaceID), + resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "list_id", listID), + resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "value", value1), + resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list_item.test", "created_at"), + resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list_item.test", "created_by"), + ), + }, + { // Update list item + ConfigDirectory: acctest.NamedTestCaseDirectory("space_update"), + ConfigVariables: config.Variables{ + "space_id": config.StringVariable(spaceID), + "list_id": config.StringVariable(listID), + "value": config.StringVariable(value2), + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "value", value2), + ), + }, + { // Import + ConfigDirectory: acctest.NamedTestCaseDirectory("space_update"), + ConfigVariables: config.Variables{ + "space_id": config.StringVariable(spaceID), + "list_id": config.StringVariable(listID), + "value": config.StringVariable(value2), + }, + ResourceName: "elasticstack_kibana_security_list_item.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccResourceSecurityListItem_WithListItemID(t *testing.T) { + listID := "test-list-items-with-id-" + uuid.New().String() + listItemID1 := "custom-item-id-1" + listItemID2 := "custom-item-id-2" + value1 := "test-value-1" + value2 := "test-value-2" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + ensureListIndexExistsInSpace(t, "default") + }, + ProtoV6ProviderFactories: acctest.Providers, + Steps: []resource.TestStep{ + { // Create with custom list_item_id + ConfigDirectory: acctest.NamedTestCaseDirectory("with_list_item_id_create"), + ConfigVariables: config.Variables{ + "list_id": config.StringVariable(listID), + "list_item_id": config.StringVariable(listItemID1), + "value": config.StringVariable(value1), + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list_item.test", "id"), + resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "list_item_id", listItemID1), + resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "value", value1), + resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list_item.test", "created_at"), + resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list_item.test", "created_by"), + resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list_item.test", "updated_at"), + resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list_item.test", "updated_by"), + ), + }, + { // Update list_item_id (should force replacement) + ConfigDirectory: acctest.NamedTestCaseDirectory("with_list_item_id_update"), + ConfigVariables: config.Variables{ + "list_id": config.StringVariable(listID), + "list_item_id": config.StringVariable(listItemID2), + "value": config.StringVariable(value2), + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "list_item_id", listItemID2), + resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "value", value2), + ), + }, + { // Import + ConfigDirectory: acctest.NamedTestCaseDirectory("with_list_item_id_update"), + ConfigVariables: config.Variables{ + "list_id": config.StringVariable(listID), + "list_item_id": config.StringVariable(listItemID2), + "value": config.StringVariable(value2), + }, + ResourceName: "elasticstack_kibana_security_list_item.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func ensureListIndexExistsInSpace(t *testing.T, spaceID string) { + 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, spaceID) + 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 in space %s: %v", spaceID, d.Detail()) + } + } + } +} diff --git a/internal/kibana/security_list_item/create.go b/internal/kibana/security_list_item/create.go new file mode 100644 index 000000000..665d01ebe --- /dev/null +++ b/internal/kibana/security_list_item/create.go @@ -0,0 +1,70 @@ +package security_list_item + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" + "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *securityListItemResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan SecurityListItemModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + // Get Kibana client + client, err := r.client.GetKibanaOapiClient() + if err != nil { + resp.Diagnostics.AddError("Failed to get Kibana client", err.Error()) + return + } + + // Convert plan to API request + createReq, diags := plan.toAPICreateModel(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Create the list item + createdListItem, diags := kibana_oapi.CreateListItem(ctx, client, plan.SpaceID.ValueString(), *createReq) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if createdListItem == nil { + resp.Diagnostics.AddError("Failed to create security list item", "API returned empty response") + return + } + + // Read the created list item to populate state + id := kbapi.SecurityListsAPIListId(createdListItem.Id) + readParams := &kbapi.ReadListItemParams{ + Id: &id, + } + + listItem, diags := kibana_oapi.GetListItem(ctx, client, plan.SpaceID.ValueString(), readParams) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if listItem == nil { + resp.State.RemoveResource(ctx) + resp.Diagnostics.AddError("Failed to fetch security list item", "API returned empty response") + return + } + + // Update state with read response + diags = plan.fromAPIModel(ctx, listItem) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} diff --git a/internal/kibana/security_list_item/delete.go b/internal/kibana/security_list_item/delete.go new file mode 100644 index 000000000..d2afe068c --- /dev/null +++ b/internal/kibana/security_list_item/delete.go @@ -0,0 +1,41 @@ +package security_list_item + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *securityListItemResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state SecurityListItemModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + // Get Kibana client + client, err := r.client.GetKibanaOapiClient() + if err != nil { + resp.Diagnostics.AddError("Failed to get Kibana client", err.Error()) + return + } + + // Parse composite ID to get space_id and resource_id + compId, compIdDiags := clients.CompositeIdFromStrFw(state.ID.ValueString()) + resp.Diagnostics.Append(compIdDiags...) + if resp.Diagnostics.HasError() { + return + } + + // Delete by resource ID from composite ID + id := kbapi.SecurityListsAPIListItemId(compId.ResourceId) + params := &kbapi.DeleteListItemParams{ + Id: &id, + } + + diags := kibana_oapi.DeleteListItem(ctx, client, compId.ClusterId, params) + resp.Diagnostics.Append(diags...) +} diff --git a/internal/kibana/security_list_item/models.go b/internal/kibana/security_list_item/models.go new file mode 100644 index 000000000..d00202c75 --- /dev/null +++ b/internal/kibana/security_list_item/models.go @@ -0,0 +1,126 @@ +package security_list_item + +import ( + "context" + "encoding/json" + + "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/elastic/terraform-provider-elasticstack/internal/utils/typeutils" + "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type SecurityListItemModel struct { + ID types.String `tfsdk:"id"` + ListItemID types.String `tfsdk:"list_item_id"` + SpaceID types.String `tfsdk:"space_id"` + ListID types.String `tfsdk:"list_id"` + Value types.String `tfsdk:"value"` + Meta jsontypes.Normalized `tfsdk:"meta"` + CreatedAt types.String `tfsdk:"created_at"` + CreatedBy types.String `tfsdk:"created_by"` + UpdatedAt types.String `tfsdk:"updated_at"` + UpdatedBy types.String `tfsdk:"updated_by"` + VersionID types.String `tfsdk:"version_id"` +} + +// toAPICreateModel converts the Terraform model to the API create request body +func (m *SecurityListItemModel) toAPICreateModel(ctx context.Context) (*kbapi.CreateListItemJSONRequestBody, diag.Diagnostics) { + var diags diag.Diagnostics + + body := &kbapi.CreateListItemJSONRequestBody{ + ListId: kbapi.SecurityListsAPIListId(m.ListID.ValueString()), + Value: kbapi.SecurityListsAPIListItemValue(m.Value.ValueString()), + } + + // Set optional ID if specified + if utils.IsKnown(m.ListItemID) { + id := kbapi.SecurityListsAPIListItemId(m.ListItemID.ValueString()) + body.Id = &id + } + + // Set optional meta if specified + if utils.IsKnown(m.Meta) { + var meta kbapi.SecurityListsAPIListItemMetadata + diags.Append(m.Meta.Unmarshal(&meta)...) + if diags.HasError() { + return nil, diags + } + body.Meta = &meta + } + + return body, diags +} + +// toAPIUpdateModel converts the Terraform model to the API update request body +func (m *SecurityListItemModel) toAPIUpdateModel(ctx context.Context) (*kbapi.UpdateListItemJSONRequestBody, diag.Diagnostics) { + var diags diag.Diagnostics + + // Parse composite ID to get resource_id + compId, compIdDiags := clients.CompositeIdFromStrFw(m.ID.ValueString()) + diags.Append(compIdDiags...) + if diags.HasError() { + return nil, diags + } + + body := &kbapi.UpdateListItemJSONRequestBody{ + Id: kbapi.SecurityListsAPIListItemId(compId.ResourceId), + Value: kbapi.SecurityListsAPIListItemValue(m.Value.ValueString()), + } + + // Set optional version if available + if utils.IsKnown(m.VersionID) { + version := kbapi.SecurityListsAPIListVersionId(m.VersionID.ValueString()) + body.UnderscoreVersion = &version + } + + // Set optional meta if specified + if utils.IsKnown(m.Meta) { + var meta kbapi.SecurityListsAPIListItemMetadata + diags.Append(m.Meta.Unmarshal(&meta)...) + if diags.HasError() { + return nil, diags + } + body.Meta = &meta + } + + return body, diags +} + +// fromAPIModel populates the Terraform model from an API response +func (m *SecurityListItemModel) fromAPIModel(ctx context.Context, apiItem *kbapi.SecurityListsAPIListItem) diag.Diagnostics { + var diags diag.Diagnostics + + compId := clients.CompositeId{ + ClusterId: m.SpaceID.ValueString(), + ResourceId: string(apiItem.Id), + } + m.ID = types.StringValue(compId.String()) + m.ListItemID = typeutils.StringishValue(apiItem.Id) + m.ListID = typeutils.StringishValue(apiItem.ListId) + m.Value = typeutils.StringishValue(apiItem.Value) + m.CreatedAt = types.StringValue(apiItem.CreatedAt.Format("2006-01-02T15:04:05.000Z")) + m.CreatedBy = types.StringValue(apiItem.CreatedBy) + m.UpdatedAt = types.StringValue(apiItem.UpdatedAt.Format("2006-01-02T15:04:05.000Z")) + m.UpdatedBy = types.StringValue(apiItem.UpdatedBy) + + // Set version if available + m.VersionID = typeutils.StringishPointerValue(apiItem.UnderscoreVersion) + + // Set meta if available + if apiItem.Meta != nil { + metaJSON, err := json.Marshal(apiItem.Meta) + if err != nil { + diags.AddError("Failed to serialize meta field", err.Error()) + return diags + } + m.Meta = jsontypes.NewNormalizedValue(string(metaJSON)) + } else { + m.Meta = jsontypes.NewNormalizedNull() + } + + return diags +} diff --git a/internal/kibana/security_list_item/read.go b/internal/kibana/security_list_item/read.go new file mode 100644 index 000000000..15dcd4a4b --- /dev/null +++ b/internal/kibana/security_list_item/read.go @@ -0,0 +1,64 @@ +package security_list_item + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func (r *securityListItemResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state SecurityListItemModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + // Get Kibana client + client, err := r.client.GetKibanaOapiClient() + if err != nil { + resp.Diagnostics.AddError("Failed to get Kibana client", err.Error()) + return + } + + // Parse composite ID to get space_id and resource id + compId, compIdDiags := clients.CompositeIdFromStrFw(state.ID.ValueString()) + + if !compIdDiags.HasError() { + state.SpaceID = types.StringValue(compId.ClusterId) + } + + resp.Diagnostics.Append(compIdDiags...) + if resp.Diagnostics.HasError() { + return + } + + // Read by resource ID from composite ID + id := kbapi.SecurityListsAPIListId(compId.ResourceId) + params := &kbapi.ReadListItemParams{ + Id: &id, + } + + listItem, diags := kibana_oapi.GetListItem(ctx, client, compId.ClusterId, params) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if listItem == nil { + resp.State.RemoveResource(ctx) + return + } + + // Update state with response + diags = state.fromAPIModel(ctx, listItem) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) +} diff --git a/internal/kibana/security_list_item/resource-description.md b/internal/kibana/security_list_item/resource-description.md new file mode 100644 index 000000000..5b68d8e7d --- /dev/null +++ b/internal/kibana/security_list_item/resource-description.md @@ -0,0 +1,5 @@ +Manages items within Kibana security value lists. Value lists are containers for values that can be used within exception lists to define conditions. This resource allows you to add, update, and remove individual values (items) in those lists. + +Value list items are used to store data values that match the type of their parent security list (e.g., IP addresses, keywords, etc.). These items can then be referenced in exception list entries to define exception conditions. + +Kibana docs can be found [here](https://www.elastic.co/docs/api/doc/kibana/group/endpoint-security-lists-api) \ No newline at end of file diff --git a/internal/kibana/security_list_item/resource.go b/internal/kibana/security_list_item/resource.go new file mode 100644 index 000000000..0f9402be3 --- /dev/null +++ b/internal/kibana/security_list_item/resource.go @@ -0,0 +1,38 @@ +package security_list_item + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +// Ensure provider defined types fully satisfy framework interfaces +var ( + _ resource.Resource = &securityListItemResource{} + _ resource.ResourceWithConfigure = &securityListItemResource{} + _ resource.ResourceWithImportState = &securityListItemResource{} +) + +func NewResource() resource.Resource { + return &securityListItemResource{} +} + +type securityListItemResource struct { + client *clients.ApiClient +} + +func (r *securityListItemResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_kibana_security_list_item" +} + +func (r *securityListItemResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + client, diags := clients.ConvertProviderData(req.ProviderData) + resp.Diagnostics.Append(diags...) + r.client = client +} + +func (r *securityListItemResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), request, response) +} diff --git a/internal/kibana/security_list_item/schema.go b/internal/kibana/security_list_item/schema.go new file mode 100644 index 000000000..0ec55e196 --- /dev/null +++ b/internal/kibana/security_list_item/schema.go @@ -0,0 +1,81 @@ +package security_list_item + +import ( + "context" + _ "embed" + + "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" +) + +//go:embed resource-description.md +var securityListItemResourceDescription string + +func (r *securityListItemResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: securityListItemResourceDescription, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "Internal identifier for the resource (format: `/`).", + Computed: true, + }, + "list_item_id": schema.StringAttribute{ + MarkdownDescription: "The value list item's identifier (auto-generated by Kibana if not specified).", + Computed: true, + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "space_id": schema.StringAttribute{ + MarkdownDescription: "An identifier for the space. If space_id is not provided, the default space is used.", + Optional: true, + Computed: true, + Default: stringdefault.StaticString("default"), + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "list_id": schema.StringAttribute{ + MarkdownDescription: "The value list's identifier that this item belongs to.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "value": schema.StringAttribute{ + MarkdownDescription: "The value used to evaluate exceptions. The value's data type must match the list's type.", + Required: true, + }, + "meta": schema.StringAttribute{ + MarkdownDescription: "Placeholder for metadata about the value list item as JSON string.", + Optional: true, + CustomType: jsontypes.NormalizedType{}, + }, + "version_id": schema.StringAttribute{ + MarkdownDescription: "The version id, normally returned by the API when the document is retrieved. Used to ensure updates are done against the latest version.", + Computed: true, + }, + "created_at": schema.StringAttribute{ + MarkdownDescription: "The timestamp of when the list item was created.", + Computed: true, + }, + "created_by": schema.StringAttribute{ + MarkdownDescription: "The user who created the list item.", + Computed: true, + }, + "updated_at": schema.StringAttribute{ + MarkdownDescription: "The timestamp of when the list item was last updated.", + Computed: true, + }, + "updated_by": schema.StringAttribute{ + MarkdownDescription: "The user who last updated the list item.", + Computed: true, + }, + }, + } +} diff --git a/internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem/create/main.tf b/internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem/create/main.tf new file mode 100644 index 000000000..246c973a6 --- /dev/null +++ b/internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem/create/main.tf @@ -0,0 +1,16 @@ +variable "list_id" {} +variable "value" {} + +# First create a security list to put items in +resource "elasticstack_kibana_security_list" "test" { + list_id = var.list_id + name = "Test List for Items" + description = "A test security list for IP addresses" + type = "keyword" +} + +# Create a list item +resource "elasticstack_kibana_security_list_item" "test" { + list_id = elasticstack_kibana_security_list.test.list_id + value = var.value +} diff --git a/internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem/update/main.tf b/internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem/update/main.tf new file mode 100644 index 000000000..df1d6c668 --- /dev/null +++ b/internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem/update/main.tf @@ -0,0 +1,16 @@ +variable "list_id" {} +variable "value" {} + +# First create a security list to put items in +resource "elasticstack_kibana_security_list" "test" { + list_id = var.list_id + name = "Test List for Items" + description = "A test security list for IP addresses" + type = "keyword" +} + +# Create a list item with updated value +resource "elasticstack_kibana_security_list_item" "test" { + list_id = elasticstack_kibana_security_list.test.list_id + value = var.value +} diff --git a/internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem_Space/space_create/main.tf b/internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem_Space/space_create/main.tf new file mode 100644 index 000000000..be1e2ee3f --- /dev/null +++ b/internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem_Space/space_create/main.tf @@ -0,0 +1,26 @@ +variable "space_id" {} +variable "list_id" {} +variable "value" {} + +# Create a dedicated space for security lists +resource "elasticstack_kibana_space" "test" { + space_id = var.space_id + name = "Test Security Lists Space" + description = "A test space for security lists and list items" +} + +# Create a security list in the space +resource "elasticstack_kibana_security_list" "test" { + space_id = elasticstack_kibana_space.test.space_id + list_id = var.list_id + name = "IP Blocklist" + description = "A test security list for blocking IP addresses" + type = "ip" +} + +# Create a list item in the space +resource "elasticstack_kibana_security_list_item" "test" { + space_id = elasticstack_kibana_space.test.space_id + list_id = elasticstack_kibana_security_list.test.list_id + value = var.value +} diff --git a/internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem_Space/space_update/main.tf b/internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem_Space/space_update/main.tf new file mode 100644 index 000000000..ae8358922 --- /dev/null +++ b/internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem_Space/space_update/main.tf @@ -0,0 +1,26 @@ +variable "space_id" {} +variable "list_id" {} +variable "value" {} + +# Create a dedicated space for security lists +resource "elasticstack_kibana_space" "test" { + space_id = var.space_id + name = "Test Security Lists Space" + description = "A test space for security lists and list items" +} + +# Create a security list in the space +resource "elasticstack_kibana_security_list" "test" { + space_id = elasticstack_kibana_space.test.space_id + list_id = var.list_id + name = "IP Blocklist" + description = "A test security list for blocking IP addresses" + type = "ip" +} + +# Update the list item value +resource "elasticstack_kibana_security_list_item" "test" { + space_id = elasticstack_kibana_space.test.space_id + list_id = elasticstack_kibana_security_list.test.list_id + value = var.value +} diff --git a/internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem_WithListItemID/with_list_item_id_create/main.tf b/internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem_WithListItemID/with_list_item_id_create/main.tf new file mode 100644 index 000000000..f3c5d47b6 --- /dev/null +++ b/internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem_WithListItemID/with_list_item_id_create/main.tf @@ -0,0 +1,18 @@ +variable "list_id" {} +variable "list_item_id" {} +variable "value" {} + +# First create a security list to put items in +resource "elasticstack_kibana_security_list" "test" { + list_id = var.list_id + name = "Test List for Items with Custom ID" + description = "A test security list for items with custom list_item_id" + type = "keyword" +} + +# Create a list item with custom list_item_id +resource "elasticstack_kibana_security_list_item" "test" { + list_id = elasticstack_kibana_security_list.test.list_id + list_item_id = var.list_item_id + value = var.value +} diff --git a/internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem_WithListItemID/with_list_item_id_update/main.tf b/internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem_WithListItemID/with_list_item_id_update/main.tf new file mode 100644 index 000000000..11e294774 --- /dev/null +++ b/internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem_WithListItemID/with_list_item_id_update/main.tf @@ -0,0 +1,18 @@ +variable "list_id" {} +variable "list_item_id" {} +variable "value" {} + +# First create a security list to put items in +resource "elasticstack_kibana_security_list" "test" { + list_id = var.list_id + name = "Test List for Items with Custom ID" + description = "A test security list for items with custom list_item_id" + type = "keyword" +} + +# Update list_item_id (will force replacement) +resource "elasticstack_kibana_security_list_item" "test" { + list_id = elasticstack_kibana_security_list.test.list_id + list_item_id = var.list_item_id + value = var.value +} diff --git a/internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem_WithMeta/create/main.tf b/internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem_WithMeta/create/main.tf new file mode 100644 index 000000000..b681cc5a8 --- /dev/null +++ b/internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem_WithMeta/create/main.tf @@ -0,0 +1,18 @@ +variable "list_id" {} +variable "value" {} +variable "meta" {} + +# First create a security list to put items in +resource "elasticstack_kibana_security_list" "test" { + list_id = var.list_id + name = "Test List for Items with Meta" + description = "A test security list for items with metadata" + type = "keyword" +} + +# Create a list item with meta +resource "elasticstack_kibana_security_list_item" "test" { + list_id = elasticstack_kibana_security_list.test.list_id + value = var.value + meta = var.meta +} diff --git a/internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem_WithMeta/update/main.tf b/internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem_WithMeta/update/main.tf new file mode 100644 index 000000000..2dee99106 --- /dev/null +++ b/internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem_WithMeta/update/main.tf @@ -0,0 +1,18 @@ +variable "list_id" {} +variable "value" {} +variable "meta" {} + +# First create a security list to put items in +resource "elasticstack_kibana_security_list" "test" { + list_id = var.list_id + name = "Test List for Items with Meta" + description = "A test security list for items with metadata" + type = "keyword" +} + +# Update list item with different meta +resource "elasticstack_kibana_security_list_item" "test" { + list_id = elasticstack_kibana_security_list.test.list_id + value = var.value + meta = var.meta +} diff --git a/internal/kibana/security_list_item/update.go b/internal/kibana/security_list_item/update.go new file mode 100644 index 000000000..aba41e879 --- /dev/null +++ b/internal/kibana/security_list_item/update.go @@ -0,0 +1,78 @@ +package security_list_item + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *securityListItemResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan SecurityListItemModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + // Get Kibana client + client, err := r.client.GetKibanaOapiClient() + if err != nil { + resp.Diagnostics.AddError("Failed to get Kibana client", err.Error()) + return + } + + // Parse composite ID to get space_id + compId, compIdDiags := clients.CompositeIdFromStrFw(plan.ID.ValueString()) + resp.Diagnostics.Append(compIdDiags...) + if resp.Diagnostics.HasError() { + return + } + + // Convert plan to API request + updateReq, diags := plan.toAPIUpdateModel(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Update the list item + updatedListItem, diags := kibana_oapi.UpdateListItem(ctx, client, compId.ClusterId, *updateReq) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if updatedListItem == nil { + resp.Diagnostics.AddError("Failed to update security list item", "API returned empty response") + return + } + + // Read the updated list item to populate state + id := kbapi.SecurityListsAPIListId(updatedListItem.Id) + readParams := &kbapi.ReadListItemParams{ + Id: &id, + } + + listItem, diags := kibana_oapi.GetListItem(ctx, client, compId.ClusterId, readParams) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if listItem == nil { + resp.State.RemoveResource(ctx) + resp.Diagnostics.AddError("Failed to fetch security list item", "API returned empty response") + return + } + + // Update state with read response + diags = plan.fromAPIModel(ctx, listItem) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} diff --git a/internal/utils/typeutils/typeutils.go b/internal/utils/typeutils/typeutils.go new file mode 100644 index 000000000..bcc9f626a --- /dev/null +++ b/internal/utils/typeutils/typeutils.go @@ -0,0 +1,16 @@ +package typeutils + +import "github.com/hashicorp/terraform-plugin-framework/types" + +// StringishPointerValue converts a pointer to a string-like type to a Terraform types.String value. +func StringishPointerValue[T ~string](ptr *T) types.String { + if ptr == nil { + return types.StringNull() + } + return types.StringValue(string(*ptr)) +} + +// StringishValue converts a value of any string-like type T to a Terraform types.String. +func StringishValue[T ~string](value T) types.String { + return types.StringValue(string(value)) +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go index b16f25116..15b6d423c 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -231,16 +231,3 @@ func NonNilSlice[T any](s []T) []T { func TimeToStringValue(t time.Time) types.String { return types.StringValue(FormatStrictDateTime(t)) } - -// StringishPointerValue converts a pointer to a string-like type to a Terraform types.String value. -func StringishPointerValue[T ~string](ptr *T) types.String { - if ptr == nil { - return types.StringNull() - } - return types.StringValue(string(*ptr)) -} - -// StringishValue converts a value of any string-like type T to a Terraform types.String. -func StringishValue[T ~string](value T) types.String { - return types.StringValue(string(value)) -} diff --git a/provider/plugin_framework.go b/provider/plugin_framework.go index e6aa43a00..0d8abec60 100644 --- a/provider/plugin_framework.go +++ b/provider/plugin_framework.go @@ -34,6 +34,7 @@ import ( "github.com/elastic/terraform-provider-elasticstack/internal/kibana/maintenance_window" "github.com/elastic/terraform-provider-elasticstack/internal/kibana/security_detection_rule" "github.com/elastic/terraform-provider-elasticstack/internal/kibana/security_list" + "github.com/elastic/terraform-provider-elasticstack/internal/kibana/security_list_item" "github.com/elastic/terraform-provider-elasticstack/internal/kibana/spaces" "github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics/monitor" "github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics/parameter" @@ -149,6 +150,7 @@ func (p *Provider) resources(ctx context.Context) []func() resource.Resource { func (p *Provider) experimentalResources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ + security_list_item.NewResource, security_list.NewResource, } }