From 77b2046876cb557a91dc7d83c620e9936242b37b Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Wed, 26 Nov 2025 13:30:45 -0700 Subject: [PATCH 01/15] Add security list item resource --- .../kibana/security_list_item/acc_test.go | 147 ++++++++++++++++++ internal/kibana/security_list_item/create.go | 78 ++++++++++ internal/kibana/security_list_item/delete.go | 33 ++++ internal/kibana/security_list_item/models.go | 115 ++++++++++++++ internal/kibana/security_list_item/read.go | 59 +++++++ .../resource-description.md | 15 ++ .../kibana/security_list_item/resource.go | 38 +++++ internal/kibana/security_list_item/schema.go | 77 +++++++++ .../create/main.tf | 16 ++ .../update/main.tf | 16 ++ .../space_create/main.tf | 26 ++++ .../space_update/main.tf | 26 ++++ internal/kibana/security_list_item/update.go | 78 ++++++++++ 13 files changed, 724 insertions(+) create mode 100644 internal/kibana/security_list_item/acc_test.go create mode 100644 internal/kibana/security_list_item/create.go create mode 100644 internal/kibana/security_list_item/delete.go create mode 100644 internal/kibana/security_list_item/models.go create mode 100644 internal/kibana/security_list_item/read.go create mode 100644 internal/kibana/security_list_item/resource-description.md create mode 100644 internal/kibana/security_list_item/resource.go create mode 100644 internal/kibana/security_list_item/schema.go create mode 100644 internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem/create/main.tf create mode 100644 internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem/update/main.tf create mode 100644 internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem_Space/space_create/main.tf create mode 100644 internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem_Space/space_update/main.tf create mode 100644 internal/kibana/security_list_item/update.go 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..796af073e --- /dev/null +++ b/internal/kibana/security_list_item/acc_test.go @@ -0,0 +1,147 @@ +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 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 TestAccResourceSecurityListItem(t *testing.T) { + listID := "test-list-items-" + 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), + "value": config.StringVariable("test-value-1"), + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list_item.test", "id"), + resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "value", "test-value-1"), + 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("test-value-updated"), + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "value", "test-value-updated"), + ), + }, + }, + }) +} + +func TestAccResourceSecurityListItem_Space(t *testing.T) { + spaceID := "test-space-" + uuid.New().String() + listID := "test-list-" + uuid.New().String() + + 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("192.168.1.1"), + }, + Check: resource.ComposeTestCheckFunc( + // Check space + resource.TestCheckResourceAttr("elasticstack_kibana_space.test", "space_id", spaceID), + resource.TestCheckResourceAttr("elasticstack_kibana_space.test", "name", "Test Security Lists Space"), + // 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", "IP Blocklist"), + resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "type", "ip"), + // 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", "192.168.1.1"), + 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("10.0.0.1"), + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "value", "10.0.0.1"), + ), + }, + }, + }) +} + +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..4fcd587da --- /dev/null +++ b/internal/kibana/security_list_item/create.go @@ -0,0 +1,78 @@ +package security_list_item + +import ( + "context" + "encoding/json" + + "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 + createResp, diags := kibana_oapi.CreateListItem(ctx, client, plan.SpaceID.ValueString(), *createReq) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if createResp == nil || createResp.JSON200 == 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(createResp.JSON200.Id) + readParams := &kbapi.ReadListItemParams{ + Id: &id, + } + + readResp, diags := kibana_oapi.GetListItem(ctx, client, plan.SpaceID.ValueString(), readParams) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if readResp == nil || readResp.JSON200 == nil { + resp.State.RemoveResource(ctx) + resp.Diagnostics.AddError("Failed to fetch security list item", "API returned empty response") + return + } + + // Unmarshal the response body to get the list item + var listItem kbapi.SecurityListsAPIListItem + if err := json.Unmarshal(readResp.Body, &listItem); err != nil { + resp.Diagnostics.AddError("Failed to parse list item response", err.Error()) + 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..a28a499eb --- /dev/null +++ b/internal/kibana/security_list_item/delete.go @@ -0,0 +1,33 @@ +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) 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 + } + + // Delete by ID + id := kbapi.SecurityListsAPIListItemId(state.ID.ValueString()) + params := &kbapi.DeleteListItemParams{ + Id: &id, + } + + diags := kibana_oapi.DeleteListItem(ctx, client, state.SpaceID.ValueString(), 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..01d730517 --- /dev/null +++ b/internal/kibana/security_list_item/models.go @@ -0,0 +1,115 @@ +package security_list_item + +import ( + "context" + "encoding/json" + + "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" + "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "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"` + 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"` + Version types.String `tfsdk:"version"` +} + +// 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.ID) { + id := kbapi.SecurityListsAPIListItemId(m.ID.ValueString()) + body.Id = &id + } + + // Set optional meta if specified + if utils.IsKnown(m.Meta) { + var meta kbapi.SecurityListsAPIListItemMetadata + if err := json.Unmarshal([]byte(m.Meta.ValueString()), &meta); err != nil { + diags.AddError("Failed to parse meta JSON", err.Error()) + 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 + + body := &kbapi.UpdateListItemJSONRequestBody{ + Id: kbapi.SecurityListsAPIListItemId(m.ID.ValueString()), + Value: kbapi.SecurityListsAPIListItemValue(m.Value.ValueString()), + } + + // Set optional version if available + if utils.IsKnown(m.Version) { + version := kbapi.SecurityListsAPIListVersionId(m.Version.ValueString()) + body.UnderscoreVersion = &version + } + + // Set optional meta if specified + if utils.IsKnown(m.Meta) { + var meta kbapi.SecurityListsAPIListItemMetadata + if err := json.Unmarshal([]byte(m.Meta.ValueString()), &meta); err != nil { + diags.AddError("Failed to parse meta JSON", err.Error()) + 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 + + m.ID = types.StringValue(string(apiItem.Id)) + m.ListID = types.StringValue(string(apiItem.ListId)) + m.Value = types.StringValue(string(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 + if apiItem.UnderscoreVersion != nil { + m.Version = types.StringValue(string(*apiItem.UnderscoreVersion)) + } else { + m.Version = types.StringNull() + } + + // Set meta if available + if apiItem.Meta != nil { + metaJSON, err := json.Marshal(apiItem.Meta) + if err != nil { + diags.AddError("Failed to serialize meta", 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..c995d9ccd --- /dev/null +++ b/internal/kibana/security_list_item/read.go @@ -0,0 +1,59 @@ +package security_list_item + +import ( + "context" + "encoding/json" + + "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) 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 + } + + // Read by ID + id := kbapi.SecurityListsAPIListId(state.ID.ValueString()) + params := &kbapi.ReadListItemParams{ + Id: &id, + } + + readResp, diags := kibana_oapi.GetListItem(ctx, client, state.SpaceID.ValueString(), params) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if readResp == nil || readResp.JSON200 == nil { + resp.State.RemoveResource(ctx) + return + } + + // The response can be a single item or an array, so we need to unmarshal from the body + // When querying by ID, we expect a single item + var listItem kbapi.SecurityListsAPIListItem + if err := json.Unmarshal(readResp.Body, &listItem); err != nil { + resp.Diagnostics.AddError("Failed to parse list item response", err.Error()) + 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..bb4fd0acb --- /dev/null +++ b/internal/kibana/security_list_item/resource-description.md @@ -0,0 +1,15 @@ +--- +subcategory: "Kibana" +layout: "" +page_title: "Elasticstack: elasticstack_kibana_security_list_item Resource" +description: |- + Manages items within Kibana security value lists. +--- + +# Resource: elasticstack_kibana_security_list_item + +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..f2f082c6e --- /dev/null +++ b/internal/kibana/security_list_item/schema.go @@ -0,0 +1,77 @@ +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: "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": 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/update.go b/internal/kibana/security_list_item/update.go new file mode 100644 index 000000000..2538429d8 --- /dev/null +++ b/internal/kibana/security_list_item/update.go @@ -0,0 +1,78 @@ +package security_list_item + +import ( + "context" + "encoding/json" + + "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) 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 + } + + // Convert plan to API request + updateReq, diags := plan.toAPIUpdateModel(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Update the list item + updateResp, diags := kibana_oapi.UpdateListItem(ctx, client, plan.SpaceID.ValueString(), *updateReq) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if updateResp == nil || updateResp.JSON200 == 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(updateResp.JSON200.Id) + readParams := &kbapi.ReadListItemParams{ + Id: &id, + } + + readResp, diags := kibana_oapi.GetListItem(ctx, client, plan.SpaceID.ValueString(), readParams) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if readResp == nil || readResp.JSON200 == nil { + resp.State.RemoveResource(ctx) + resp.Diagnostics.AddError("Failed to fetch security list item", "API returned empty response") + return + } + + // Unmarshal the response body to get the list item + var listItem kbapi.SecurityListsAPIListItem + if err := json.Unmarshal(readResp.Body, &listItem); err != nil { + resp.Diagnostics.AddError("Failed to parse list item response", err.Error()) + 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)...) +} From 3e36f1bf72f8ecb2ca57762fb653ce36a29662d6 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Wed, 26 Nov 2025 13:31:10 -0700 Subject: [PATCH 02/15] Add security list item resources --- .../resource.tf | 13 +++++++++++++ .../resource_ip.tf | 13 +++++++++++++ .../resource_with_meta.tf | 18 ++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 examples/resources/elasticstack_kibana_security_list_item/resource.tf create mode 100644 examples/resources/elasticstack_kibana_security_list_item/resource_ip.tf create mode 100644 examples/resources/elasticstack_kibana_security_list_item/resource_with_meta.tf 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..705311869 --- /dev/null +++ b/examples/resources/elasticstack_kibana_security_list_item/resource.tf @@ -0,0 +1,13 @@ +# 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" +} diff --git a/examples/resources/elasticstack_kibana_security_list_item/resource_ip.tf b/examples/resources/elasticstack_kibana_security_list_item/resource_ip.tf new file mode 100644 index 000000000..d306ef013 --- /dev/null +++ b/examples/resources/elasticstack_kibana_security_list_item/resource_ip.tf @@ -0,0 +1,13 @@ +# First create an IP address list +resource "elasticstack_kibana_security_list" "ip_list" { + list_id = "allowed_ips" + name = "Allowed IP Addresses" + description = "List of allowed IP addresses" + type = "ip" +} + +# Add an IP address to the list +resource "elasticstack_kibana_security_list_item" "ip_example" { + list_id = elasticstack_kibana_security_list.ip_list.list_id + value = "192.168.1.1" +} diff --git a/examples/resources/elasticstack_kibana_security_list_item/resource_with_meta.tf b/examples/resources/elasticstack_kibana_security_list_item/resource_with_meta.tf new file mode 100644 index 000000000..03131f882 --- /dev/null +++ b/examples/resources/elasticstack_kibana_security_list_item/resource_with_meta.tf @@ -0,0 +1,18 @@ +# First create a security list +resource "elasticstack_kibana_security_list" "tagged_domains" { + list_id = "tagged_domains" + name = "Tagged Domains" + description = "Domains with associated metadata" + type = "keyword" +} + +# Add an item with metadata +resource "elasticstack_kibana_security_list_item" "domain_with_meta" { + list_id = elasticstack_kibana_security_list.tagged_domains.list_id + value = "internal.example.com" + meta = jsonencode({ + category = "internal" + owner = "infrastructure-team" + note = "Primary internal domain" + }) +} From 1847c2a6346f56e120443fb0ee2ac0fd06011e93 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Wed, 26 Nov 2025 13:31:53 -0700 Subject: [PATCH 03/15] Add resource to experimental resources --- provider/plugin_framework.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/provider/plugin_framework.go b/provider/plugin_framework.go index 7d237654b..111c128f0 100644 --- a/provider/plugin_framework.go +++ b/provider/plugin_framework.go @@ -32,6 +32,7 @@ import ( "github.com/elastic/terraform-provider-elasticstack/internal/kibana/import_saved_objects" "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_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" @@ -145,7 +146,9 @@ func (p *Provider) resources(ctx context.Context) []func() resource.Resource { } func (p *Provider) experimentalResources(ctx context.Context) []func() resource.Resource { - return []func() resource.Resource{} + return []func() resource.Resource{ + security_list_item.NewResource, + } } func (p *Provider) dataSources(ctx context.Context) []func() datasource.DataSource { From 823380c3a81e30bd8a9b0b35b902ec9ecf638075 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Wed, 26 Nov 2025 18:54:10 -0700 Subject: [PATCH 04/15] Improve meta property handling / include test --- .../kibana/security_list_item/acc_test.go | 42 +++++++++++++++++++ internal/kibana/security_list_item/models.go | 10 ++--- .../create/main.tf | 18 ++++++++ .../update/main.tf | 18 ++++++++ 4 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem_WithMeta/create/main.tf create mode 100644 internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem_WithMeta/update/main.tf diff --git a/internal/kibana/security_list_item/acc_test.go b/internal/kibana/security_list_item/acc_test.go index 796af073e..2cac39142 100644 --- a/internal/kibana/security_list_item/acc_test.go +++ b/internal/kibana/security_list_item/acc_test.go @@ -72,6 +72,48 @@ func TestAccResourceSecurityListItem(t *testing.T) { }) } +func TestAccResourceSecurityListItem_WithMeta(t *testing.T) { + listID := "test-list-items-meta-" + uuid.New().String() + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + ensureListIndexExists(t) + }, + ProtoV6ProviderFactories: acctest.Providers, + Steps: []resource.TestStep{ + { // Create with meta + ConfigDirectory: acctest.NamedTestCaseDirectory("create"), + ConfigVariables: config.Variables{ + "list_id": config.StringVariable(listID), + "value": config.StringVariable("test-value-with-meta"), + "meta": config.StringVariable(`{"category":"suspicious","severity":"high"}`), + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("elasticstack_kibana_security_list_item.test", "id"), + resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "value", "test-value-with-meta"), + resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "meta", `{"category":"suspicious","severity":"high"}`), + 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("test-value-with-meta"), + "meta": config.StringVariable(`{"category":"malicious","notes":"Updated metadata","severity":"critical"}`), + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "value", "test-value-with-meta"), + resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "meta", `{"category":"malicious","notes":"Updated metadata","severity":"critical"}`), + ), + }, + }, + }) +} + func TestAccResourceSecurityListItem_Space(t *testing.T) { spaceID := "test-space-" + uuid.New().String() listID := "test-list-" + uuid.New().String() diff --git a/internal/kibana/security_list_item/models.go b/internal/kibana/security_list_item/models.go index 01d730517..3d05ff360 100644 --- a/internal/kibana/security_list_item/models.go +++ b/internal/kibana/security_list_item/models.go @@ -42,8 +42,8 @@ func (m *SecurityListItemModel) toAPICreateModel(ctx context.Context) (*kbapi.Cr // Set optional meta if specified if utils.IsKnown(m.Meta) { var meta kbapi.SecurityListsAPIListItemMetadata - if err := json.Unmarshal([]byte(m.Meta.ValueString()), &meta); err != nil { - diags.AddError("Failed to parse meta JSON", err.Error()) + diags.Append(m.Meta.Unmarshal(&meta)...) + if diags.HasError() { return nil, diags } body.Meta = &meta @@ -70,8 +70,8 @@ func (m *SecurityListItemModel) toAPIUpdateModel(ctx context.Context) (*kbapi.Up // Set optional meta if specified if utils.IsKnown(m.Meta) { var meta kbapi.SecurityListsAPIListItemMetadata - if err := json.Unmarshal([]byte(m.Meta.ValueString()), &meta); err != nil { - diags.AddError("Failed to parse meta JSON", err.Error()) + diags.Append(m.Meta.Unmarshal(&meta)...) + if diags.HasError() { return nil, diags } body.Meta = &meta @@ -103,7 +103,7 @@ func (m *SecurityListItemModel) fromAPIModel(ctx context.Context, apiItem *kbapi if apiItem.Meta != nil { metaJSON, err := json.Marshal(apiItem.Meta) if err != nil { - diags.AddError("Failed to serialize meta", err.Error()) + diags.AddError("Failed to serialize meta field", err.Error()) return diags } m.Meta = jsontypes.NewNormalizedValue(string(metaJSON)) 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 +} From 86907ccddddaa5ab37911e9498c5832edfa2b563 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Thu, 27 Nov 2025 13:51:19 -0700 Subject: [PATCH 05/15] Use composite id --- internal/kibana/security_list_item/delete.go | 14 +++++++++--- internal/kibana/security_list_item/models.go | 24 +++++++++++++++++--- internal/kibana/security_list_item/read.go | 14 +++++++++--- internal/kibana/security_list_item/update.go | 12 ++++++++-- 4 files changed, 53 insertions(+), 11 deletions(-) diff --git a/internal/kibana/security_list_item/delete.go b/internal/kibana/security_list_item/delete.go index a28a499eb..d2afe068c 100644 --- a/internal/kibana/security_list_item/delete.go +++ b/internal/kibana/security_list_item/delete.go @@ -4,6 +4,7 @@ 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" ) @@ -22,12 +23,19 @@ func (r *securityListItemResource) Delete(ctx context.Context, req resource.Dele return } - // Delete by ID - id := kbapi.SecurityListsAPIListItemId(state.ID.ValueString()) + // 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, state.SpaceID.ValueString(), params) + 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 index 3d05ff360..74d83726e 100644 --- a/internal/kibana/security_list_item/models.go +++ b/internal/kibana/security_list_item/models.go @@ -5,6 +5,7 @@ import ( "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/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -35,7 +36,13 @@ func (m *SecurityListItemModel) toAPICreateModel(ctx context.Context) (*kbapi.Cr // Set optional ID if specified if utils.IsKnown(m.ID) { - id := kbapi.SecurityListsAPIListItemId(m.ID.ValueString()) + // Parse composite ID to get resource_id + compId, compIdDiags := clients.CompositeIdFromStrFw(m.ID.ValueString()) + diags.Append(compIdDiags...) + if diags.HasError() { + return nil, diags + } + id := kbapi.SecurityListsAPIListItemId(compId.ResourceId) body.Id = &id } @@ -56,8 +63,15 @@ func (m *SecurityListItemModel) toAPICreateModel(ctx context.Context) (*kbapi.Cr 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(m.ID.ValueString()), + Id: kbapi.SecurityListsAPIListItemId(compId.ResourceId), Value: kbapi.SecurityListsAPIListItemValue(m.Value.ValueString()), } @@ -84,7 +98,11 @@ func (m *SecurityListItemModel) toAPIUpdateModel(ctx context.Context) (*kbapi.Up func (m *SecurityListItemModel) fromAPIModel(ctx context.Context, apiItem *kbapi.SecurityListsAPIListItem) diag.Diagnostics { var diags diag.Diagnostics - m.ID = types.StringValue(string(apiItem.Id)) + compId := clients.CompositeId{ + ClusterId: m.SpaceID.ValueString(), + ResourceId: string(apiItem.Id), + } + m.ID = types.StringValue(compId.String()) m.ListID = types.StringValue(string(apiItem.ListId)) m.Value = types.StringValue(string(apiItem.Value)) m.CreatedAt = types.StringValue(apiItem.CreatedAt.Format("2006-01-02T15:04:05.000Z")) diff --git a/internal/kibana/security_list_item/read.go b/internal/kibana/security_list_item/read.go index c995d9ccd..ccb612de1 100644 --- a/internal/kibana/security_list_item/read.go +++ b/internal/kibana/security_list_item/read.go @@ -5,6 +5,7 @@ import ( "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/clients/kibana_oapi" "github.com/hashicorp/terraform-plugin-framework/resource" ) @@ -23,13 +24,20 @@ func (r *securityListItemResource) Read(ctx context.Context, req resource.ReadRe return } - // Read by ID - id := kbapi.SecurityListsAPIListId(state.ID.ValueString()) + // 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 + } + + // Read by resource ID from composite ID + id := kbapi.SecurityListsAPIListId(compId.ResourceId) params := &kbapi.ReadListItemParams{ Id: &id, } - readResp, diags := kibana_oapi.GetListItem(ctx, client, state.SpaceID.ValueString(), params) + readResp, diags := kibana_oapi.GetListItem(ctx, client, compId.ClusterId, params) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return diff --git a/internal/kibana/security_list_item/update.go b/internal/kibana/security_list_item/update.go index 2538429d8..ee15f2b07 100644 --- a/internal/kibana/security_list_item/update.go +++ b/internal/kibana/security_list_item/update.go @@ -5,6 +5,7 @@ import ( "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/clients/kibana_oapi" "github.com/hashicorp/terraform-plugin-framework/resource" ) @@ -23,6 +24,13 @@ func (r *securityListItemResource) Update(ctx context.Context, req resource.Upda 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...) @@ -31,7 +39,7 @@ func (r *securityListItemResource) Update(ctx context.Context, req resource.Upda } // Update the list item - updateResp, diags := kibana_oapi.UpdateListItem(ctx, client, plan.SpaceID.ValueString(), *updateReq) + updateResp, diags := kibana_oapi.UpdateListItem(ctx, client, compId.ClusterId, *updateReq) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return @@ -48,7 +56,7 @@ func (r *securityListItemResource) Update(ctx context.Context, req resource.Upda Id: &id, } - readResp, diags := kibana_oapi.GetListItem(ctx, client, plan.SpaceID.ValueString(), readParams) + readResp, diags := kibana_oapi.GetListItem(ctx, client, compId.ClusterId, readParams) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return From c50ab9a218cc26673fbed7b22a66caaec31a1427 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Thu, 27 Nov 2025 13:59:43 -0700 Subject: [PATCH 06/15] Fix docs typo --- internal/kibana/security_list/schema.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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{ From 9e273061e79c67224e5df61054996526872bba95 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Thu, 27 Nov 2025 14:13:32 -0700 Subject: [PATCH 07/15] Add import test cases --- .../kibana/security_list_item/acc_test.go | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/internal/kibana/security_list_item/acc_test.go b/internal/kibana/security_list_item/acc_test.go index 2cac39142..83986fc17 100644 --- a/internal/kibana/security_list_item/acc_test.go +++ b/internal/kibana/security_list_item/acc_test.go @@ -68,6 +68,16 @@ func TestAccResourceSecurityListItem(t *testing.T) { resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "value", "test-value-updated"), ), }, + { // Import + ConfigDirectory: acctest.NamedTestCaseDirectory("update"), + ConfigVariables: config.Variables{ + "list_id": config.StringVariable(listID), + "value": config.StringVariable("test-value-updated"), + }, + ResourceName: "elasticstack_kibana_security_list_item.test", + ImportState: true, + ImportStateVerify: true, + }, }, }) } @@ -110,6 +120,17 @@ func TestAccResourceSecurityListItem_WithMeta(t *testing.T) { resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "meta", `{"category":"malicious","notes":"Updated metadata","severity":"critical"}`), ), }, + { // Import + ConfigDirectory: acctest.NamedTestCaseDirectory("update"), + ConfigVariables: config.Variables{ + "list_id": config.StringVariable(listID), + "value": config.StringVariable("test-value-with-meta"), + "meta": config.StringVariable(`{"category":"malicious","notes":"Updated metadata","severity":"critical"}`), + }, + ResourceName: "elasticstack_kibana_security_list_item.test", + ImportState: true, + ImportStateVerify: true, + }, }, }) } @@ -162,6 +183,17 @@ func TestAccResourceSecurityListItem_Space(t *testing.T) { resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "value", "10.0.0.1"), ), }, + { // Import + ConfigDirectory: acctest.NamedTestCaseDirectory("space_update"), + ConfigVariables: config.Variables{ + "space_id": config.StringVariable(spaceID), + "list_id": config.StringVariable(listID), + "value": config.StringVariable("10.0.0.1"), + }, + ResourceName: "elasticstack_kibana_security_list_item.test", + ImportState: true, + ImportStateVerify: true, + }, }, }) } From df5b37a6f4d1e5f15b9160d25c93e6ac0f984e67 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Thu, 27 Nov 2025 14:30:25 -0700 Subject: [PATCH 08/15] Add list_item_id to allow manually set human readable id --- .../kibana/security_list_item/acc_test.go | 56 +++++++++++++++++++ internal/kibana/security_list_item/models.go | 32 +++++------ internal/kibana/security_list_item/read.go | 8 ++- internal/kibana/security_list_item/schema.go | 4 ++ .../with_list_item_id_create/main.tf | 18 ++++++ .../with_list_item_id_update/main.tf | 18 ++++++ 6 files changed, 117 insertions(+), 19 deletions(-) create mode 100644 internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem_WithListItemID/with_list_item_id_create/main.tf create mode 100644 internal/kibana/security_list_item/testdata/TestAccResourceSecurityListItem_WithListItemID/with_list_item_id_update/main.tf diff --git a/internal/kibana/security_list_item/acc_test.go b/internal/kibana/security_list_item/acc_test.go index 83986fc17..51ff47218 100644 --- a/internal/kibana/security_list_item/acc_test.go +++ b/internal/kibana/security_list_item/acc_test.go @@ -198,6 +198,62 @@ func TestAccResourceSecurityListItem_Space(t *testing.T) { }) } +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" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + ensureListIndexExists(t) + }, + 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("test-value-1"), + }, + 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", "test-value-1"), + 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("test-value-2"), + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "list_item_id", listItemID2), + resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "value", "test-value-2"), + ), + }, + { // 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("test-value-2"), + }, + 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 { diff --git a/internal/kibana/security_list_item/models.go b/internal/kibana/security_list_item/models.go index 74d83726e..3ea2a717b 100644 --- a/internal/kibana/security_list_item/models.go +++ b/internal/kibana/security_list_item/models.go @@ -13,16 +13,17 @@ import ( ) type SecurityListItemModel struct { - ID types.String `tfsdk:"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"` - Version types.String `tfsdk:"version"` + 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"` + Version types.String `tfsdk:"version"` } // toAPICreateModel converts the Terraform model to the API create request body @@ -35,14 +36,8 @@ func (m *SecurityListItemModel) toAPICreateModel(ctx context.Context) (*kbapi.Cr } // Set optional ID if specified - if utils.IsKnown(m.ID) { - // Parse composite ID to get resource_id - compId, compIdDiags := clients.CompositeIdFromStrFw(m.ID.ValueString()) - diags.Append(compIdDiags...) - if diags.HasError() { - return nil, diags - } - id := kbapi.SecurityListsAPIListItemId(compId.ResourceId) + if utils.IsKnown(m.ListItemID) { + id := kbapi.SecurityListsAPIListItemId(m.ListItemID.ValueString()) body.Id = &id } @@ -103,6 +98,7 @@ func (m *SecurityListItemModel) fromAPIModel(ctx context.Context, apiItem *kbapi ResourceId: string(apiItem.Id), } m.ID = types.StringValue(compId.String()) + m.ListItemID = types.StringValue(string(apiItem.Id)) m.ListID = types.StringValue(string(apiItem.ListId)) m.Value = types.StringValue(string(apiItem.Value)) m.CreatedAt = types.StringValue(apiItem.CreatedAt.Format("2006-01-02T15:04:05.000Z")) diff --git a/internal/kibana/security_list_item/read.go b/internal/kibana/security_list_item/read.go index ccb612de1..7146020f2 100644 --- a/internal/kibana/security_list_item/read.go +++ b/internal/kibana/security_list_item/read.go @@ -8,6 +8,7 @@ import ( "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) { @@ -24,8 +25,13 @@ func (r *securityListItemResource) Read(ctx context.Context, req resource.ReadRe return } - // Parse composite ID to get space_id and resource_id + // 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 diff --git a/internal/kibana/security_list_item/schema.go b/internal/kibana/security_list_item/schema.go index f2f082c6e..abc558e80 100644 --- a/internal/kibana/security_list_item/schema.go +++ b/internal/kibana/security_list_item/schema.go @@ -20,6 +20,10 @@ func (r *securityListItemResource) Schema(_ context.Context, _ resource.SchemaRe 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, 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 +} From e7d0cebb2b7a85d66813faf42752d0d3aa6dbad9 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Thu, 27 Nov 2025 14:33:48 -0700 Subject: [PATCH 09/15] Swap version -> version_id --- internal/kibana/security_list_item/models.go | 10 +++++----- internal/kibana/security_list_item/schema.go | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/kibana/security_list_item/models.go b/internal/kibana/security_list_item/models.go index 3ea2a717b..de60fd576 100644 --- a/internal/kibana/security_list_item/models.go +++ b/internal/kibana/security_list_item/models.go @@ -23,7 +23,7 @@ type SecurityListItemModel struct { CreatedBy types.String `tfsdk:"created_by"` UpdatedAt types.String `tfsdk:"updated_at"` UpdatedBy types.String `tfsdk:"updated_by"` - Version types.String `tfsdk:"version"` + VersionID types.String `tfsdk:"version_id"` } // toAPICreateModel converts the Terraform model to the API create request body @@ -71,8 +71,8 @@ func (m *SecurityListItemModel) toAPIUpdateModel(ctx context.Context) (*kbapi.Up } // Set optional version if available - if utils.IsKnown(m.Version) { - version := kbapi.SecurityListsAPIListVersionId(m.Version.ValueString()) + if utils.IsKnown(m.VersionID) { + version := kbapi.SecurityListsAPIListVersionId(m.VersionID.ValueString()) body.UnderscoreVersion = &version } @@ -108,9 +108,9 @@ func (m *SecurityListItemModel) fromAPIModel(ctx context.Context, apiItem *kbapi // Set version if available if apiItem.UnderscoreVersion != nil { - m.Version = types.StringValue(string(*apiItem.UnderscoreVersion)) + m.VersionID = types.StringValue(string(*apiItem.UnderscoreVersion)) } else { - m.Version = types.StringNull() + m.VersionID = types.StringNull() } // Set meta if available diff --git a/internal/kibana/security_list_item/schema.go b/internal/kibana/security_list_item/schema.go index abc558e80..0ec55e196 100644 --- a/internal/kibana/security_list_item/schema.go +++ b/internal/kibana/security_list_item/schema.go @@ -56,7 +56,7 @@ func (r *securityListItemResource) Schema(_ context.Context, _ resource.SchemaRe Optional: true, CustomType: jsontypes.NormalizedType{}, }, - "version": schema.StringAttribute{ + "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, }, From 26ee41c729b9027e753a2cd5612ffdc68f502bde Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Thu, 27 Nov 2025 14:42:10 -0700 Subject: [PATCH 10/15] Add typeutils package --- internal/kibana/security_list/models.go | 15 ++++++++------- internal/kibana/security_list_item/models.go | 13 +++++-------- internal/utils/typeutils/typeutils.go | 16 ++++++++++++++++ internal/utils/utils.go | 13 ------------- 4 files changed, 29 insertions(+), 28 deletions(-) create mode 100644 internal/utils/typeutils/typeutils.go 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_item/models.go b/internal/kibana/security_list_item/models.go index de60fd576..d00202c75 100644 --- a/internal/kibana/security_list_item/models.go +++ b/internal/kibana/security_list_item/models.go @@ -7,6 +7,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" @@ -98,20 +99,16 @@ func (m *SecurityListItemModel) fromAPIModel(ctx context.Context, apiItem *kbapi ResourceId: string(apiItem.Id), } m.ID = types.StringValue(compId.String()) - m.ListItemID = types.StringValue(string(apiItem.Id)) - m.ListID = types.StringValue(string(apiItem.ListId)) - m.Value = types.StringValue(string(apiItem.Value)) + 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 - if apiItem.UnderscoreVersion != nil { - m.VersionID = types.StringValue(string(*apiItem.UnderscoreVersion)) - } else { - m.VersionID = types.StringNull() - } + m.VersionID = typeutils.StringishPointerValue(apiItem.UnderscoreVersion) // Set meta if available if apiItem.Meta != nil { 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)) -} From 4b69c4233312f1c0cb9a3094e389d0fd70d58949 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Thu, 27 Nov 2025 14:56:39 -0700 Subject: [PATCH 11/15] Return parsed responses from security_lists client helper --- .../clients/kibana_oapi/security_lists.go | 59 +++++++++++++++---- internal/kibana/security_list/create.go | 12 ++-- internal/kibana/security_list/read.go | 6 +- internal/kibana/security_list/update.go | 12 ++-- internal/kibana/security_list_item/create.go | 20 ++----- internal/kibana/security_list_item/read.go | 15 +---- internal/kibana/security_list_item/update.go | 20 ++----- 7 files changed, 77 insertions(+), 67 deletions(-) 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/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/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/create.go b/internal/kibana/security_list_item/create.go index 4fcd587da..665d01ebe 100644 --- a/internal/kibana/security_list_item/create.go +++ b/internal/kibana/security_list_item/create.go @@ -2,7 +2,6 @@ package security_list_item import ( "context" - "encoding/json" "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi" @@ -31,44 +30,37 @@ func (r *securityListItemResource) Create(ctx context.Context, req resource.Crea } // Create the list item - createResp, diags := kibana_oapi.CreateListItem(ctx, client, plan.SpaceID.ValueString(), *createReq) + createdListItem, diags := kibana_oapi.CreateListItem(ctx, client, plan.SpaceID.ValueString(), *createReq) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - if createResp == nil || createResp.JSON200 == nil { + 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(createResp.JSON200.Id) + id := kbapi.SecurityListsAPIListId(createdListItem.Id) readParams := &kbapi.ReadListItemParams{ Id: &id, } - readResp, diags := kibana_oapi.GetListItem(ctx, client, plan.SpaceID.ValueString(), readParams) + listItem, diags := kibana_oapi.GetListItem(ctx, client, plan.SpaceID.ValueString(), readParams) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - if readResp == nil || readResp.JSON200 == nil { + if listItem == nil { resp.State.RemoveResource(ctx) resp.Diagnostics.AddError("Failed to fetch security list item", "API returned empty response") return } - // Unmarshal the response body to get the list item - var listItem kbapi.SecurityListsAPIListItem - if err := json.Unmarshal(readResp.Body, &listItem); err != nil { - resp.Diagnostics.AddError("Failed to parse list item response", err.Error()) - return - } - // Update state with read response - diags = plan.fromAPIModel(ctx, &listItem) + diags = plan.fromAPIModel(ctx, listItem) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return diff --git a/internal/kibana/security_list_item/read.go b/internal/kibana/security_list_item/read.go index 7146020f2..15dcd4a4b 100644 --- a/internal/kibana/security_list_item/read.go +++ b/internal/kibana/security_list_item/read.go @@ -2,7 +2,6 @@ package security_list_item import ( "context" - "encoding/json" "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" "github.com/elastic/terraform-provider-elasticstack/internal/clients" @@ -43,27 +42,19 @@ func (r *securityListItemResource) Read(ctx context.Context, req resource.ReadRe Id: &id, } - readResp, diags := kibana_oapi.GetListItem(ctx, client, compId.ClusterId, params) + listItem, diags := kibana_oapi.GetListItem(ctx, client, compId.ClusterId, params) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - if readResp == nil || readResp.JSON200 == nil { + if listItem == nil { resp.State.RemoveResource(ctx) return } - // The response can be a single item or an array, so we need to unmarshal from the body - // When querying by ID, we expect a single item - var listItem kbapi.SecurityListsAPIListItem - if err := json.Unmarshal(readResp.Body, &listItem); err != nil { - resp.Diagnostics.AddError("Failed to parse list item response", err.Error()) - return - } - // Update state with response - diags = state.fromAPIModel(ctx, &listItem) + diags = state.fromAPIModel(ctx, listItem) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return diff --git a/internal/kibana/security_list_item/update.go b/internal/kibana/security_list_item/update.go index ee15f2b07..aba41e879 100644 --- a/internal/kibana/security_list_item/update.go +++ b/internal/kibana/security_list_item/update.go @@ -2,7 +2,6 @@ package security_list_item import ( "context" - "encoding/json" "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" "github.com/elastic/terraform-provider-elasticstack/internal/clients" @@ -39,44 +38,37 @@ func (r *securityListItemResource) Update(ctx context.Context, req resource.Upda } // Update the list item - updateResp, diags := kibana_oapi.UpdateListItem(ctx, client, compId.ClusterId, *updateReq) + updatedListItem, diags := kibana_oapi.UpdateListItem(ctx, client, compId.ClusterId, *updateReq) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - if updateResp == nil || updateResp.JSON200 == nil { + 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(updateResp.JSON200.Id) + id := kbapi.SecurityListsAPIListId(updatedListItem.Id) readParams := &kbapi.ReadListItemParams{ Id: &id, } - readResp, diags := kibana_oapi.GetListItem(ctx, client, compId.ClusterId, readParams) + listItem, diags := kibana_oapi.GetListItem(ctx, client, compId.ClusterId, readParams) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - if readResp == nil || readResp.JSON200 == nil { + if listItem == nil { resp.State.RemoveResource(ctx) resp.Diagnostics.AddError("Failed to fetch security list item", "API returned empty response") return } - // Unmarshal the response body to get the list item - var listItem kbapi.SecurityListsAPIListItem - if err := json.Unmarshal(readResp.Body, &listItem); err != nil { - resp.Diagnostics.AddError("Failed to parse list item response", err.Error()) - return - } - // Update state with read response - diags = plan.fromAPIModel(ctx, &listItem) + diags = plan.fromAPIModel(ctx, listItem) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return From 9c1b4e4231a3d0e248a8b3d310495074e8545962 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Thu, 27 Nov 2025 15:01:30 -0700 Subject: [PATCH 12/15] Remove duplicate ensureListIndexExistsInSpace --- .../kibana/security_list_item/acc_test.go | 28 ++----------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/internal/kibana/security_list_item/acc_test.go b/internal/kibana/security_list_item/acc_test.go index 51ff47218..59fa19027 100644 --- a/internal/kibana/security_list_item/acc_test.go +++ b/internal/kibana/security_list_item/acc_test.go @@ -12,34 +12,12 @@ import ( "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 TestAccResourceSecurityListItem(t *testing.T) { listID := "test-list-items-" + uuid.New().String() resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) - ensureListIndexExists(t) + ensureListIndexExistsInSpace(t, "default") }, ProtoV6ProviderFactories: acctest.Providers, Steps: []resource.TestStep{ @@ -87,7 +65,7 @@ func TestAccResourceSecurityListItem_WithMeta(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) - ensureListIndexExists(t) + ensureListIndexExistsInSpace(t, "default") }, ProtoV6ProviderFactories: acctest.Providers, Steps: []resource.TestStep{ @@ -206,7 +184,7 @@ func TestAccResourceSecurityListItem_WithListItemID(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) - ensureListIndexExists(t) + ensureListIndexExistsInSpace(t, "default") }, ProtoV6ProviderFactories: acctest.Providers, Steps: []resource.TestStep{ From f6e76bee7754c5d4151fe6a4797b0878145ffbdd Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Thu, 27 Nov 2025 19:57:49 -0700 Subject: [PATCH 13/15] Update internal/kibana/security_list_item/resource-description.md Co-authored-by: Toby Brain --- .../kibana/security_list_item/resource-description.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/internal/kibana/security_list_item/resource-description.md b/internal/kibana/security_list_item/resource-description.md index bb4fd0acb..5b68d8e7d 100644 --- a/internal/kibana/security_list_item/resource-description.md +++ b/internal/kibana/security_list_item/resource-description.md @@ -1,13 +1,3 @@ ---- -subcategory: "Kibana" -layout: "" -page_title: "Elasticstack: elasticstack_kibana_security_list_item Resource" -description: |- - Manages items within Kibana security value lists. ---- - -# Resource: elasticstack_kibana_security_list_item - 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. From 6a2e28e2308cbc1b1a0521a6079b7d2fbd2b0a45 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Thu, 27 Nov 2025 20:01:54 -0700 Subject: [PATCH 14/15] Add meta to resource.tf example --- .../resource.tf | 5 +++++ .../resource_ip.tf | 13 ------------- .../resource_with_meta.tf | 18 ------------------ 3 files changed, 5 insertions(+), 31 deletions(-) delete mode 100644 examples/resources/elasticstack_kibana_security_list_item/resource_ip.tf delete mode 100644 examples/resources/elasticstack_kibana_security_list_item/resource_with_meta.tf diff --git a/examples/resources/elasticstack_kibana_security_list_item/resource.tf b/examples/resources/elasticstack_kibana_security_list_item/resource.tf index 705311869..e74bca6a6 100644 --- a/examples/resources/elasticstack_kibana_security_list_item/resource.tf +++ b/examples/resources/elasticstack_kibana_security_list_item/resource.tf @@ -10,4 +10,9 @@ resource "elasticstack_kibana_security_list" "my_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/examples/resources/elasticstack_kibana_security_list_item/resource_ip.tf b/examples/resources/elasticstack_kibana_security_list_item/resource_ip.tf deleted file mode 100644 index d306ef013..000000000 --- a/examples/resources/elasticstack_kibana_security_list_item/resource_ip.tf +++ /dev/null @@ -1,13 +0,0 @@ -# First create an IP address list -resource "elasticstack_kibana_security_list" "ip_list" { - list_id = "allowed_ips" - name = "Allowed IP Addresses" - description = "List of allowed IP addresses" - type = "ip" -} - -# Add an IP address to the list -resource "elasticstack_kibana_security_list_item" "ip_example" { - list_id = elasticstack_kibana_security_list.ip_list.list_id - value = "192.168.1.1" -} diff --git a/examples/resources/elasticstack_kibana_security_list_item/resource_with_meta.tf b/examples/resources/elasticstack_kibana_security_list_item/resource_with_meta.tf deleted file mode 100644 index 03131f882..000000000 --- a/examples/resources/elasticstack_kibana_security_list_item/resource_with_meta.tf +++ /dev/null @@ -1,18 +0,0 @@ -# First create a security list -resource "elasticstack_kibana_security_list" "tagged_domains" { - list_id = "tagged_domains" - name = "Tagged Domains" - description = "Domains with associated metadata" - type = "keyword" -} - -# Add an item with metadata -resource "elasticstack_kibana_security_list_item" "domain_with_meta" { - list_id = elasticstack_kibana_security_list.tagged_domains.list_id - value = "internal.example.com" - meta = jsonencode({ - category = "internal" - owner = "infrastructure-team" - note = "Primary internal domain" - }) -} From 2bb8966bd98766731ee23036b8d8c677ce8b2655 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Thu, 27 Nov 2025 20:07:40 -0700 Subject: [PATCH 15/15] Extract test values into reusable variables --- .../kibana/security_list_item/acc_test.go | 70 +++++++++++-------- 1 file changed, 42 insertions(+), 28 deletions(-) diff --git a/internal/kibana/security_list_item/acc_test.go b/internal/kibana/security_list_item/acc_test.go index 59fa19027..4e2dc84d1 100644 --- a/internal/kibana/security_list_item/acc_test.go +++ b/internal/kibana/security_list_item/acc_test.go @@ -14,6 +14,9 @@ import ( 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) @@ -25,11 +28,11 @@ func TestAccResourceSecurityListItem(t *testing.T) { ConfigDirectory: acctest.NamedTestCaseDirectory("create"), ConfigVariables: config.Variables{ "list_id": config.StringVariable(listID), - "value": config.StringVariable("test-value-1"), + "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", "test-value-1"), + 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"), @@ -40,17 +43,17 @@ func TestAccResourceSecurityListItem(t *testing.T) { ConfigDirectory: acctest.NamedTestCaseDirectory("update"), ConfigVariables: config.Variables{ "list_id": config.StringVariable(listID), - "value": config.StringVariable("test-value-updated"), + "value": config.StringVariable(valueUpdated), }, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "value", "test-value-updated"), + 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("test-value-updated"), + "value": config.StringVariable(valueUpdated), }, ResourceName: "elasticstack_kibana_security_list_item.test", ImportState: true, @@ -62,6 +65,10 @@ func TestAccResourceSecurityListItem(t *testing.T) { 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) @@ -73,13 +80,13 @@ func TestAccResourceSecurityListItem_WithMeta(t *testing.T) { ConfigDirectory: acctest.NamedTestCaseDirectory("create"), ConfigVariables: config.Variables{ "list_id": config.StringVariable(listID), - "value": config.StringVariable("test-value-with-meta"), - "meta": config.StringVariable(`{"category":"suspicious","severity":"high"}`), + "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", "test-value-with-meta"), - resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "meta", `{"category":"suspicious","severity":"high"}`), + 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"), @@ -90,20 +97,20 @@ func TestAccResourceSecurityListItem_WithMeta(t *testing.T) { ConfigDirectory: acctest.NamedTestCaseDirectory("update"), ConfigVariables: config.Variables{ "list_id": config.StringVariable(listID), - "value": config.StringVariable("test-value-with-meta"), - "meta": config.StringVariable(`{"category":"malicious","notes":"Updated metadata","severity":"critical"}`), + "value": config.StringVariable(value), + "meta": config.StringVariable(meta2), }, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "value", "test-value-with-meta"), - resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "meta", `{"category":"malicious","notes":"Updated metadata","severity":"critical"}`), + 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("test-value-with-meta"), - "meta": config.StringVariable(`{"category":"malicious","notes":"Updated metadata","severity":"critical"}`), + "value": config.StringVariable(value), + "meta": config.StringVariable(meta2), }, ResourceName: "elasticstack_kibana_security_list_item.test", ImportState: true, @@ -116,6 +123,11 @@ func TestAccResourceSecurityListItem_WithMeta(t *testing.T) { 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() { @@ -129,23 +141,23 @@ func TestAccResourceSecurityListItem_Space(t *testing.T) { ConfigVariables: config.Variables{ "space_id": config.StringVariable(spaceID), "list_id": config.StringVariable(listID), - "value": config.StringVariable("192.168.1.1"), + "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", "Test Security Lists Space"), + 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", "IP Blocklist"), - resource.TestCheckResourceAttr("elasticstack_kibana_security_list.test", "type", "ip"), + 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", "192.168.1.1"), + 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"), ), @@ -155,10 +167,10 @@ func TestAccResourceSecurityListItem_Space(t *testing.T) { ConfigVariables: config.Variables{ "space_id": config.StringVariable(spaceID), "list_id": config.StringVariable(listID), - "value": config.StringVariable("10.0.0.1"), + "value": config.StringVariable(value2), }, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "value", "10.0.0.1"), + resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "value", value2), ), }, { // Import @@ -166,7 +178,7 @@ func TestAccResourceSecurityListItem_Space(t *testing.T) { ConfigVariables: config.Variables{ "space_id": config.StringVariable(spaceID), "list_id": config.StringVariable(listID), - "value": config.StringVariable("10.0.0.1"), + "value": config.StringVariable(value2), }, ResourceName: "elasticstack_kibana_security_list_item.test", ImportState: true, @@ -180,6 +192,8 @@ 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() { @@ -193,12 +207,12 @@ func TestAccResourceSecurityListItem_WithListItemID(t *testing.T) { ConfigVariables: config.Variables{ "list_id": config.StringVariable(listID), "list_item_id": config.StringVariable(listItemID1), - "value": config.StringVariable("test-value-1"), + "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", "test-value-1"), + 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"), @@ -210,11 +224,11 @@ func TestAccResourceSecurityListItem_WithListItemID(t *testing.T) { ConfigVariables: config.Variables{ "list_id": config.StringVariable(listID), "list_item_id": config.StringVariable(listItemID2), - "value": config.StringVariable("test-value-2"), + "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", "test-value-2"), + resource.TestCheckResourceAttr("elasticstack_kibana_security_list_item.test", "value", value2), ), }, { // Import @@ -222,7 +236,7 @@ func TestAccResourceSecurityListItem_WithListItemID(t *testing.T) { ConfigVariables: config.Variables{ "list_id": config.StringVariable(listID), "list_item_id": config.StringVariable(listItemID2), - "value": config.StringVariable("test-value-2"), + "value": config.StringVariable(value2), }, ResourceName: "elasticstack_kibana_security_list_item.test", ImportState: true,