diff --git a/docs/resources/elasticsearch_index.md b/docs/resources/elasticsearch_index.md
index 15e5f3377..16c79eff8 100644
--- a/docs/resources/elasticsearch_index.md
+++ b/docs/resources/elasticsearch_index.md
@@ -21,16 +21,14 @@ provider "elasticstack" {
resource "elasticstack_elasticsearch_index" "my_index" {
name = "my-index"
- alias {
+ alias = [{
name = "my_alias_1"
- }
-
- alias {
+ }, {
name = "my_alias_2"
filter = jsonencode({
term = { "user.id" = "developer" }
})
- }
+ }]
mappings = jsonencode({
properties = {
@@ -60,7 +58,7 @@ resource "elasticstack_elasticsearch_index" "my_index" {
### Optional
-- `alias` (Block Set) Aliases for the index. (see [below for nested schema](#nestedblock--alias))
+- `alias` (Attributes Set) Aliases for the index. (see [below for nested schema](#nestedatt--alias))
- `analysis_analyzer` (String) A JSON string describing the analyzers applied to the index.
- `analysis_char_filter` (String) A JSON string describing the char_filters applied to the index.
- `analysis_filter` (String) A JSON string describing the filters applied to the index.
@@ -136,7 +134,7 @@ resource "elasticstack_elasticsearch_index" "my_index" {
- `id` (String) Internal identifier of the resource
- `settings_raw` (String) All raw settings fetched from the cluster.
-
+
### Nested Schema for `alias`
Required:
diff --git a/docs/resources/elasticsearch_index_alias.md b/docs/resources/elasticsearch_index_alias.md
new file mode 100644
index 000000000..efb755569
--- /dev/null
+++ b/docs/resources/elasticsearch_index_alias.md
@@ -0,0 +1,61 @@
+
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "elasticstack_elasticsearch_index_alias Resource - terraform-provider-elasticstack"
+subcategory: "Index"
+description: |-
+ Manages an Elasticsearch alias. See the alias documentation https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-aliases.html for more details.
+---
+
+# elasticstack_elasticsearch_index_alias (Resource)
+
+Manages an Elasticsearch alias. See the [alias documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-aliases.html) for more details.
+
+
+
+
+## Schema
+
+### Required
+
+- `name` (String) The alias name.
+
+### Optional
+
+- `read_indices` (Attributes Set) Set of read indices for the alias. (see [below for nested schema](#nestedatt--read_indices))
+- `write_index` (Attributes) The write index for the alias. Only one write index is allowed per alias. (see [below for nested schema](#nestedatt--write_index))
+
+### Read-Only
+
+- `id` (String) Generated ID of the alias resource.
+
+
+### Nested Schema for `read_indices`
+
+Required:
+
+- `name` (String) Name of the read index.
+
+Optional:
+
+- `filter` (String) Query used to limit documents the alias can access.
+- `index_routing` (String) Value used to route indexing operations to a specific shard.
+- `is_hidden` (Boolean) If true, the alias is hidden.
+- `routing` (String) Value used to route indexing and search operations to a specific shard.
+- `search_routing` (String) Value used to route search operations to a specific shard.
+
+
+
+### Nested Schema for `write_index`
+
+Required:
+
+- `name` (String) Name of the write index.
+
+Optional:
+
+- `filter` (String) Query used to limit documents the alias can access.
+- `index_routing` (String) Value used to route indexing operations to a specific shard.
+- `is_hidden` (Boolean) If true, the alias is hidden.
+- `routing` (String) Value used to route indexing and search operations to a specific shard.
+- `search_routing` (String) Value used to route search operations to a specific shard.
diff --git a/examples/resources/elasticstack_elasticsearch_index/resource.tf b/examples/resources/elasticstack_elasticsearch_index/resource.tf
index 3b5801672..ce3d3bb25 100644
--- a/examples/resources/elasticstack_elasticsearch_index/resource.tf
+++ b/examples/resources/elasticstack_elasticsearch_index/resource.tf
@@ -5,16 +5,14 @@ provider "elasticstack" {
resource "elasticstack_elasticsearch_index" "my_index" {
name = "my-index"
- alias {
+ alias = [{
name = "my_alias_1"
- }
-
- alias {
+ }, {
name = "my_alias_2"
filter = jsonencode({
term = { "user.id" = "developer" }
})
- }
+ }]
mappings = jsonencode({
properties = {
diff --git a/internal/clients/elasticsearch/index.go b/internal/clients/elasticsearch/index.go
index 3ce52078c..32fdc5ac0 100644
--- a/internal/clients/elasticsearch/index.go
+++ b/internal/clients/elasticsearch/index.go
@@ -581,6 +581,133 @@ func DeleteDataStreamLifecycle(ctx context.Context, apiClient *clients.ApiClient
return nil
}
+func GetAlias(ctx context.Context, apiClient *clients.ApiClient, aliasName string) (map[string]models.Index, fwdiags.Diagnostics) {
+ esClient, err := apiClient.GetESClient()
+ if err != nil {
+ return nil, fwdiags.Diagnostics{
+ fwdiags.NewErrorDiagnostic(err.Error(), err.Error()),
+ }
+ }
+
+ res, err := esClient.Indices.GetAlias(
+ esClient.Indices.GetAlias.WithName(aliasName),
+ esClient.Indices.GetAlias.WithContext(ctx),
+ )
+ if err != nil {
+ return nil, fwdiags.Diagnostics{
+ fwdiags.NewErrorDiagnostic(err.Error(), err.Error()),
+ }
+ }
+ defer res.Body.Close()
+
+ if res.StatusCode == http.StatusNotFound {
+ return nil, nil
+ }
+
+ diags := diagutil.CheckErrorFromFW(res, fmt.Sprintf("Unable to get alias '%s'", aliasName))
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ indices := make(map[string]models.Index)
+ if err := json.NewDecoder(res.Body).Decode(&indices); err != nil {
+ return nil, fwdiags.Diagnostics{
+ fwdiags.NewErrorDiagnostic(err.Error(), err.Error()),
+ }
+ }
+
+ return indices, nil
+}
+
+// AliasAction represents a single action in an atomic alias update operation
+type AliasAction struct {
+ Type string // "add" or "remove"
+ Index string
+ Alias string
+ IsWriteIndex bool
+ Filter map[string]interface{}
+ IndexRouting string
+ IsHidden bool
+ Routing string
+ SearchRouting string
+}
+
+// UpdateAliasesAtomic performs atomic alias updates using multiple actions
+func UpdateAliasesAtomic(ctx context.Context, apiClient *clients.ApiClient, actions []AliasAction) fwdiags.Diagnostics {
+ esClient, err := apiClient.GetESClient()
+ if err != nil {
+ return fwdiags.Diagnostics{
+ fwdiags.NewErrorDiagnostic(err.Error(), err.Error()),
+ }
+ }
+
+ var aliasActions []map[string]interface{}
+
+ for _, action := range actions {
+ switch action.Type {
+ case "remove":
+ aliasActions = append(aliasActions, map[string]interface{}{
+ "remove": map[string]interface{}{
+ "index": action.Index,
+ "alias": action.Alias,
+ },
+ })
+ case "add":
+ addDetails := map[string]interface{}{
+ "index": action.Index,
+ "alias": action.Alias,
+ }
+
+ if action.IsWriteIndex {
+ addDetails["is_write_index"] = true
+ }
+ if action.Filter != nil {
+ addDetails["filter"] = action.Filter
+ }
+ if action.IndexRouting != "" {
+ addDetails["index_routing"] = action.IndexRouting
+ }
+ if action.SearchRouting != "" {
+ addDetails["search_routing"] = action.SearchRouting
+ }
+ if action.Routing != "" {
+ addDetails["routing"] = action.Routing
+ }
+ if action.IsHidden {
+ addDetails["is_hidden"] = action.IsHidden
+ }
+
+ aliasActions = append(aliasActions, map[string]interface{}{
+ "add": addDetails,
+ })
+ }
+ }
+
+ requestBody := map[string]interface{}{
+ "actions": aliasActions,
+ }
+
+ aliasBytes, err := json.Marshal(requestBody)
+ if err != nil {
+ return fwdiags.Diagnostics{
+ fwdiags.NewErrorDiagnostic(err.Error(), err.Error()),
+ }
+ }
+
+ res, err := esClient.Indices.UpdateAliases(
+ bytes.NewReader(aliasBytes),
+ esClient.Indices.UpdateAliases.WithContext(ctx),
+ )
+ if err != nil {
+ return fwdiags.Diagnostics{
+ fwdiags.NewErrorDiagnostic(err.Error(), err.Error()),
+ }
+ }
+ defer res.Body.Close()
+
+ return diagutil.CheckErrorFromFW(res, "Unable to update aliases atomically")
+}
+
func PutIngestPipeline(ctx context.Context, apiClient *clients.ApiClient, pipeline *models.IngestPipeline) diag.Diagnostics {
var diags diag.Diagnostics
pipelineBytes, err := json.Marshal(pipeline)
diff --git a/internal/elasticsearch/index/alias/acc_test.go b/internal/elasticsearch/index/alias/acc_test.go
new file mode 100644
index 000000000..a3e72bb48
--- /dev/null
+++ b/internal/elasticsearch/index/alias/acc_test.go
@@ -0,0 +1,621 @@
+package alias_test
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/elastic/terraform-provider-elasticstack/internal/acctest"
+ "github.com/elastic/terraform-provider-elasticstack/internal/clients"
+ "github.com/hashicorp/terraform-plugin-testing/config"
+ sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/hashicorp/terraform-plugin-testing/terraform"
+)
+
+func TestAccResourceAlias(t *testing.T) {
+ // generate random names
+ aliasName := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlpha)
+ indexName := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlpha)
+ indexName2 := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlpha)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() {
+ acctest.PreCheck(t)
+ },
+ CheckDestroy: checkResourceAliasDestroy,
+ ProtoV6ProviderFactories: acctest.Providers,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccResourceAliasCreateDirect,
+ ConfigVariables: map[string]config.Variable{
+ "alias_name": config.StringVariable(aliasName),
+ "index_name": config.StringVariable(indexName),
+ "index_name2": config.StringVariable(indexName2),
+ },
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_index_alias.test_alias", "name", aliasName),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_index_alias.test_alias", "write_index.name", indexName),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_index_alias.test_alias", "write_index.is_hidden", "false"),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_index_alias.test_alias", "read_indices.#", "0"),
+ ),
+ },
+ {
+ Config: testAccResourceAliasUpdateDirect,
+ ConfigVariables: map[string]config.Variable{
+ "alias_name": config.StringVariable(aliasName),
+ "index_name": config.StringVariable(indexName),
+ "index_name2": config.StringVariable(indexName2),
+ },
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_index_alias.test_alias", "name", aliasName),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_index_alias.test_alias", "write_index.name", indexName2),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_index_alias.test_alias", "read_indices.#", "1"),
+ ),
+ },
+ {
+ Config: testAccResourceAliasWithFilterDirect,
+ ConfigVariables: map[string]config.Variable{
+ "alias_name": config.StringVariable(aliasName),
+ "index_name": config.StringVariable(indexName),
+ "index_name2": config.StringVariable(indexName2),
+ },
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_index_alias.test_alias", "name", aliasName),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_index_alias.test_alias", "write_index.name", indexName),
+ resource.TestCheckResourceAttrSet("elasticstack_elasticsearch_index_alias.test_alias", "write_index.filter"),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_index_alias.test_alias", "write_index.index_routing", "write-routing"),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_index_alias.test_alias", "read_indices.#", "1"),
+ ),
+ },
+ },
+ })
+}
+
+func TestAccResourceAliasWriteIndex(t *testing.T) {
+ // generate random names
+ aliasName := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlpha)
+ indexName1 := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlpha)
+ indexName2 := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlpha)
+ indexName3 := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlpha)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() {
+ acctest.PreCheck(t)
+ },
+ CheckDestroy: checkResourceAliasDestroy,
+ ProtoV6ProviderFactories: acctest.Providers,
+ Steps: []resource.TestStep{
+ // Case 1: Single index with is_write_index=true
+ {
+ Config: testAccResourceAliasWriteIndexSingleDirect,
+ ConfigVariables: map[string]config.Variable{
+ "alias_name": config.StringVariable(aliasName),
+ "index_name1": config.StringVariable(indexName1),
+ "index_name2": config.StringVariable(indexName2),
+ "index_name3": config.StringVariable(indexName3),
+ },
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_index_alias.test_alias", "name", aliasName),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_index_alias.test_alias", "write_index.name", indexName1),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_index_alias.test_alias", "read_indices.#", "0"),
+ ),
+ },
+ // Case 2: Add new index with is_write_index=true, existing becomes read index
+ {
+ Config: testAccResourceAliasWriteIndexSwitchDirect,
+ ConfigVariables: map[string]config.Variable{
+ "alias_name": config.StringVariable(aliasName),
+ "index_name1": config.StringVariable(indexName1),
+ "index_name2": config.StringVariable(indexName2),
+ "index_name3": config.StringVariable(indexName3),
+ },
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_index_alias.test_alias", "name", aliasName),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_index_alias.test_alias", "write_index.name", indexName2),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_index_alias.test_alias", "read_indices.#", "1"),
+ ),
+ },
+ // Case 3: Add third index as write index
+ {
+ Config: testAccResourceAliasWriteIndexTripleDirect,
+ ConfigVariables: map[string]config.Variable{
+ "alias_name": config.StringVariable(aliasName),
+ "index_name1": config.StringVariable(indexName1),
+ "index_name2": config.StringVariable(indexName2),
+ "index_name3": config.StringVariable(indexName3),
+ },
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_index_alias.test_alias", "name", aliasName),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_index_alias.test_alias", "write_index.name", indexName3),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_index_alias.test_alias", "read_indices.#", "2"),
+ ),
+ },
+ // Case 4: Remove initial index, keep two indices with one as write index
+ {
+ Config: testAccResourceAliasWriteIndexRemoveFirstDirect,
+ ConfigVariables: map[string]config.Variable{
+ "alias_name": config.StringVariable(aliasName),
+ "index_name1": config.StringVariable(indexName1),
+ "index_name2": config.StringVariable(indexName2),
+ "index_name3": config.StringVariable(indexName3),
+ },
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_index_alias.test_alias", "name", aliasName),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_index_alias.test_alias", "write_index.name", indexName3),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_index_alias.test_alias", "read_indices.#", "1"),
+ ),
+ },
+ },
+ })
+}
+
+func TestAccResourceAliasDataStream(t *testing.T) {
+ // generate random names
+ aliasName := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlpha)
+ dsName := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlpha)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.PreCheck(t) },
+ CheckDestroy: checkResourceAliasDestroy,
+ ProtoV6ProviderFactories: acctest.Providers,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccResourceAliasDataStreamCreate,
+ ConfigVariables: map[string]config.Variable{
+ "alias_name": config.StringVariable(aliasName),
+ "ds_name": config.StringVariable(dsName),
+ },
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_index_alias.test_alias", "name", aliasName),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_index_alias.test_alias", "write_index.name", dsName),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_index_alias.test_alias", "read_indices.#", "0"),
+ ),
+ },
+ },
+ })
+}
+
+const testAccResourceAliasDataStreamCreate = `
+variable "alias_name" {
+ description = "The alias name"
+ type = string
+}
+
+variable "ds_name" {
+ description = "The data stream name"
+ type = string
+}
+
+provider "elasticstack" {
+ elasticsearch {}
+}
+
+resource "elasticstack_elasticsearch_index_template" "test_ds_template" {
+ name = var.ds_name
+ index_patterns = [var.ds_name]
+ data_stream {}
+}
+
+resource "elasticstack_elasticsearch_data_stream" "test_ds" {
+ name = var.ds_name
+ depends_on = [
+ elasticstack_elasticsearch_index_template.test_ds_template
+ ]
+}
+
+resource "elasticstack_elasticsearch_index_alias" "test_alias" {
+ name = var.alias_name
+
+ write_index = {
+ name = elasticstack_elasticsearch_data_stream.test_ds.name
+ }
+}
+`
+
+func checkResourceAliasDestroy(s *terraform.State) error {
+ client, err := clients.NewAcceptanceTestingClient()
+ if err != nil {
+ return err
+ }
+
+ for _, rs := range s.RootModule().Resources {
+ if rs.Type != "elasticstack_elasticsearch_index_alias" {
+ continue
+ }
+
+ // Handle the case where ID might not be in the expected format
+ aliasName := rs.Primary.ID
+ if compId, err := clients.CompositeIdFromStr(rs.Primary.ID); err == nil {
+ aliasName = compId.ResourceId
+ }
+
+ esClient, err := client.GetESClient()
+ if err != nil {
+ return err
+ }
+
+ res, err := esClient.Indices.GetAlias(
+ esClient.Indices.GetAlias.WithName(aliasName),
+ )
+ if err != nil {
+ return err
+ }
+ defer res.Body.Close()
+
+ if res.StatusCode != 404 {
+ return fmt.Errorf("Alias (%s) still exists", aliasName)
+ }
+ }
+ return nil
+}
+
+const testAccResourceAliasCreateDirect = `
+variable "alias_name" {
+ description = "The alias name"
+ type = string
+}
+
+variable "index_name" {
+ description = "The index name"
+ type = string
+}
+
+variable "index_name2" {
+ description = "The second index name"
+ type = string
+}
+
+provider "elasticstack" {
+ elasticsearch {}
+}
+
+resource "elasticstack_elasticsearch_index" "index1" {
+ name = var.index_name
+ deletion_protection = false
+ lifecycle {
+ ignore_changes = [settings_raw]
+ }
+}
+
+resource "elasticstack_elasticsearch_index_alias" "test_alias" {
+ name = var.alias_name
+
+ write_index = {
+ name = elasticstack_elasticsearch_index.index1.name
+ }
+}
+`
+
+const testAccResourceAliasUpdateDirect = `
+variable "alias_name" {
+ description = "The alias name"
+ type = string
+}
+
+variable "index_name" {
+ description = "The index name"
+ type = string
+}
+
+variable "index_name2" {
+ description = "The second index name"
+ type = string
+}
+
+provider "elasticstack" {
+ elasticsearch {}
+}
+
+resource "elasticstack_elasticsearch_index" "index1" {
+ name = var.index_name
+ deletion_protection = false
+ lifecycle {
+ ignore_changes = [settings_raw]
+ }
+}
+
+resource "elasticstack_elasticsearch_index" "index2" {
+ name = var.index_name2
+ deletion_protection = false
+ lifecycle {
+ ignore_changes = [settings_raw]
+ }
+}
+
+resource "elasticstack_elasticsearch_index_alias" "test_alias" {
+ name = var.alias_name
+
+ write_index = {
+ name = elasticstack_elasticsearch_index.index2.name
+ }
+
+ read_indices = [{
+ name = elasticstack_elasticsearch_index.index1.name
+ }]
+}
+`
+
+const testAccResourceAliasWithFilterDirect = `
+variable "alias_name" {
+ description = "The alias name"
+ type = string
+}
+
+variable "index_name" {
+ description = "The index name"
+ type = string
+}
+
+variable "index_name2" {
+ description = "The second index name"
+ type = string
+}
+
+provider "elasticstack" {
+ elasticsearch {}
+}
+
+resource "elasticstack_elasticsearch_index" "index1" {
+ name = var.index_name
+ deletion_protection = false
+ lifecycle {
+ ignore_changes = [settings_raw]
+ }
+}
+
+resource "elasticstack_elasticsearch_index" "index2" {
+ name = var.index_name2
+ deletion_protection = false
+ lifecycle {
+ ignore_changes = [settings_raw]
+ }
+}
+
+resource "elasticstack_elasticsearch_index_alias" "test_alias" {
+ name = var.alias_name
+
+ write_index = {
+ name = elasticstack_elasticsearch_index.index1.name
+ index_routing = "write-routing"
+ filter = jsonencode({
+ term = {
+ status = "published"
+ }
+ })
+ }
+
+ read_indices = [{
+ name = elasticstack_elasticsearch_index.index2.name
+ filter = jsonencode({
+ term = {
+ status = "draft"
+ }
+ })
+ }]
+}
+`
+
+const testAccResourceAliasWriteIndexSingleDirect = `
+variable "alias_name" {
+ description = "The alias name"
+ type = string
+}
+
+variable "index_name1" {
+ description = "The first index name"
+ type = string
+}
+
+variable "index_name2" {
+ description = "The second index name"
+ type = string
+}
+
+variable "index_name3" {
+ description = "The third index name"
+ type = string
+}
+
+provider "elasticstack" {
+ elasticsearch {}
+}
+
+resource "elasticstack_elasticsearch_index" "index1" {
+ name = var.index_name1
+ deletion_protection = false
+ lifecycle {
+ ignore_changes = [settings_raw]
+ }
+}
+
+resource "elasticstack_elasticsearch_index_alias" "test_alias" {
+ name = var.alias_name
+
+ write_index = {
+ name = elasticstack_elasticsearch_index.index1.name
+ }
+}
+`
+
+const testAccResourceAliasWriteIndexSwitchDirect = `
+variable "alias_name" {
+ description = "The alias name"
+ type = string
+}
+
+variable "index_name1" {
+ description = "The first index name"
+ type = string
+}
+
+variable "index_name2" {
+ description = "The second index name"
+ type = string
+}
+
+variable "index_name3" {
+ description = "The third index name"
+ type = string
+}
+
+provider "elasticstack" {
+ elasticsearch {}
+}
+
+resource "elasticstack_elasticsearch_index" "index1" {
+ name = var.index_name1
+ deletion_protection = false
+ lifecycle {
+ ignore_changes = [settings_raw]
+ }
+}
+
+resource "elasticstack_elasticsearch_index" "index2" {
+ name = var.index_name2
+ deletion_protection = false
+ lifecycle {
+ ignore_changes = [settings_raw]
+ }
+}
+
+resource "elasticstack_elasticsearch_index_alias" "test_alias" {
+ name = var.alias_name
+
+ write_index = {
+ name = elasticstack_elasticsearch_index.index2.name
+ }
+
+ read_indices = [{
+ name = elasticstack_elasticsearch_index.index1.name
+ }]
+}
+`
+
+const testAccResourceAliasWriteIndexTripleDirect = `
+variable "alias_name" {
+ description = "The alias name"
+ type = string
+}
+
+variable "index_name1" {
+ description = "The first index name"
+ type = string
+}
+
+variable "index_name2" {
+ description = "The second index name"
+ type = string
+}
+
+variable "index_name3" {
+ description = "The third index name"
+ type = string
+}
+
+provider "elasticstack" {
+ elasticsearch {}
+}
+
+resource "elasticstack_elasticsearch_index" "index1" {
+ name = var.index_name1
+ deletion_protection = false
+ lifecycle {
+ ignore_changes = [settings_raw]
+ }
+}
+
+resource "elasticstack_elasticsearch_index" "index2" {
+ name = var.index_name2
+ deletion_protection = false
+ lifecycle {
+ ignore_changes = [settings_raw]
+ }
+}
+
+resource "elasticstack_elasticsearch_index" "index3" {
+ name = var.index_name3
+ deletion_protection = false
+ lifecycle {
+ ignore_changes = [settings_raw]
+ }
+}
+
+resource "elasticstack_elasticsearch_index_alias" "test_alias" {
+ name = var.alias_name
+
+ write_index = {
+ name = elasticstack_elasticsearch_index.index3.name
+ }
+
+ read_indices = [
+ {
+ name = elasticstack_elasticsearch_index.index1.name
+ },
+ {
+ name = elasticstack_elasticsearch_index.index2.name
+ }
+ ]
+}
+`
+
+const testAccResourceAliasWriteIndexRemoveFirstDirect = `
+variable "alias_name" {
+ description = "The alias name"
+ type = string
+}
+
+variable "index_name1" {
+ description = "The first index name"
+ type = string
+}
+
+variable "index_name2" {
+ description = "The second index name"
+ type = string
+}
+
+variable "index_name3" {
+ description = "The third index name"
+ type = string
+}
+
+provider "elasticstack" {
+ elasticsearch {}
+}
+
+resource "elasticstack_elasticsearch_index" "index1" {
+ name = var.index_name1
+ deletion_protection = false
+ lifecycle {
+ ignore_changes = [settings_raw]
+ }
+}
+
+resource "elasticstack_elasticsearch_index" "index2" {
+ name = var.index_name2
+ deletion_protection = false
+ lifecycle {
+ ignore_changes = [settings_raw]
+ }
+}
+
+resource "elasticstack_elasticsearch_index" "index3" {
+ name = var.index_name3
+ deletion_protection = false
+ lifecycle {
+ ignore_changes = [settings_raw]
+ }
+}
+
+resource "elasticstack_elasticsearch_index_alias" "test_alias" {
+ name = var.alias_name
+
+ write_index = {
+ name = elasticstack_elasticsearch_index.index3.name
+ }
+
+ read_indices = [{
+ name = elasticstack_elasticsearch_index.index2.name
+ }]
+}
+`
diff --git a/internal/elasticsearch/index/alias/create.go b/internal/elasticsearch/index/alias/create.go
new file mode 100644
index 000000000..b9f79db6c
--- /dev/null
+++ b/internal/elasticsearch/index/alias/create.go
@@ -0,0 +1,68 @@
+package alias
+
+import (
+ "context"
+
+ "github.com/elastic/terraform-provider-elasticstack/internal/clients/elasticsearch"
+ "github.com/elastic/terraform-provider-elasticstack/internal/diagutil"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+)
+
+func (r *aliasResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var planModel tfModel
+
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ aliasName := planModel.Name.ValueString()
+
+ // Set the ID using client.ID
+ id, sdkDiags := r.client.ID(ctx, aliasName)
+ if sdkDiags.HasError() {
+ resp.Diagnostics.Append(diagutil.FrameworkDiagsFromSDK(sdkDiags)...)
+ return
+ }
+ planModel.ID = basetypes.NewStringValue(id.String())
+
+ // Get alias configurations from the plan
+ configs, diags := planModel.toAliasConfigs(ctx)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Convert to alias actions
+ var actions []elasticsearch.AliasAction
+ for _, config := range configs {
+ action := elasticsearch.AliasAction{
+ Type: "add",
+ Index: config.Name,
+ Alias: aliasName,
+ IsWriteIndex: config.IsWriteIndex,
+ Filter: config.Filter,
+ IndexRouting: config.IndexRouting,
+ IsHidden: config.IsHidden,
+ Routing: config.Routing,
+ SearchRouting: config.SearchRouting,
+ }
+ actions = append(actions, action)
+ }
+
+ // Create the alias atomically
+ resp.Diagnostics.Append(elasticsearch.UpdateAliasesAtomic(ctx, r.client, actions)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Read back the alias to ensure state consistency, updating the current model
+ diags = readAliasIntoModel(ctx, r.client, aliasName, &planModel)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, planModel)...)
+}
diff --git a/internal/elasticsearch/index/alias/delete.go b/internal/elasticsearch/index/alias/delete.go
new file mode 100644
index 000000000..645d815a6
--- /dev/null
+++ b/internal/elasticsearch/index/alias/delete.go
@@ -0,0 +1,41 @@
+package alias
+
+import (
+ "context"
+
+ "github.com/elastic/terraform-provider-elasticstack/internal/clients/elasticsearch"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+)
+
+func (r *aliasResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var stateModel tfModel
+
+ resp.Diagnostics.Append(req.State.Get(ctx, &stateModel)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ aliasName := stateModel.Name.ValueString()
+
+ // Get current configuration from state
+ currentConfigs, diags := stateModel.toAliasConfigs(ctx)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Build remove actions for all indices
+ var actions []elasticsearch.AliasAction
+ for _, config := range currentConfigs {
+ actions = append(actions, elasticsearch.AliasAction{
+ Type: "remove",
+ Index: config.Name,
+ Alias: aliasName,
+ })
+ }
+
+ // Remove the alias from all indices
+ if len(actions) > 0 {
+ resp.Diagnostics.Append(elasticsearch.UpdateAliasesAtomic(ctx, r.client, actions)...)
+ }
+}
diff --git a/internal/elasticsearch/index/alias/models.go b/internal/elasticsearch/index/alias/models.go
new file mode 100644
index 000000000..8e0750a9f
--- /dev/null
+++ b/internal/elasticsearch/index/alias/models.go
@@ -0,0 +1,175 @@
+package alias
+
+import (
+ "context"
+ "encoding/json"
+
+ "github.com/elastic/terraform-provider-elasticstack/internal/models"
+ "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+)
+
+type tfModel struct {
+ ID types.String `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+ WriteIndex types.Object `tfsdk:"write_index"`
+ ReadIndices types.Set `tfsdk:"read_indices"`
+}
+
+type indexModel struct {
+ Name types.String `tfsdk:"name"`
+ Filter jsontypes.Normalized `tfsdk:"filter"`
+ IndexRouting types.String `tfsdk:"index_routing"`
+ IsHidden types.Bool `tfsdk:"is_hidden"`
+ Routing types.String `tfsdk:"routing"`
+ SearchRouting types.String `tfsdk:"search_routing"`
+}
+
+// AliasIndexConfig represents a single index configuration within an alias
+type AliasIndexConfig struct {
+ Name string
+ IsWriteIndex bool
+ Filter map[string]interface{}
+ IndexRouting string
+ IsHidden bool
+ Routing string
+ SearchRouting string
+}
+
+func (model *tfModel) populateFromAPI(ctx context.Context, aliasName string, indices map[string]models.IndexAlias) diag.Diagnostics {
+ model.Name = types.StringValue(aliasName)
+
+ var writeIndex *indexModel
+ var readIndices []indexModel
+
+ for indexName, aliasData := range indices {
+ // Convert IndexAlias to indexModel
+ index, err := indexFromAlias(indexName, aliasData)
+ if err != nil {
+ return err
+ }
+
+ if aliasData.IsWriteIndex {
+ writeIndex = &index
+ } else {
+ readIndices = append(readIndices, index)
+ }
+ }
+
+ // Set write index
+ if writeIndex != nil {
+ writeIndexObj, diags := types.ObjectValueFrom(ctx, getIndexAttrTypes(), *writeIndex)
+ if diags.HasError() {
+ return diags
+ }
+ model.WriteIndex = writeIndexObj
+ } else {
+ model.WriteIndex = types.ObjectNull(getIndexAttrTypes())
+ }
+
+ // Set read indices
+ readIndicesSet, diags := types.SetValueFrom(ctx, types.ObjectType{
+ AttrTypes: getIndexAttrTypes(),
+ }, readIndices)
+ if diags.HasError() {
+ return diags
+ }
+ model.ReadIndices = readIndicesSet
+
+ return nil
+}
+
+// indexFromAlias converts a models.IndexAlias to an indexModel
+func indexFromAlias(indexName string, aliasData models.IndexAlias) (indexModel, diag.Diagnostics) {
+ index := indexModel{
+ Name: types.StringValue(indexName),
+ IsHidden: types.BoolValue(aliasData.IsHidden),
+ }
+
+ if aliasData.IndexRouting != "" {
+ index.IndexRouting = types.StringValue(aliasData.IndexRouting)
+ }
+ if aliasData.Routing != "" {
+ index.Routing = types.StringValue(aliasData.Routing)
+ }
+ if aliasData.SearchRouting != "" {
+ index.SearchRouting = types.StringValue(aliasData.SearchRouting)
+ }
+ if aliasData.Filter != nil {
+ filterBytes, err := json.Marshal(aliasData.Filter)
+ if err != nil {
+ return indexModel{}, diag.Diagnostics{
+ diag.NewErrorDiagnostic("failed to marshal alias filter", err.Error()),
+ }
+ }
+ index.Filter = jsontypes.NewNormalizedValue(string(filterBytes))
+ }
+
+ return index, nil
+}
+
+func (model *tfModel) toAliasConfigs(ctx context.Context) ([]AliasIndexConfig, diag.Diagnostics) {
+ var configs []AliasIndexConfig
+
+ // Handle write index
+ if !model.WriteIndex.IsNull() {
+ var writeIndex indexModel
+ diags := model.WriteIndex.As(ctx, &writeIndex, basetypes.ObjectAsOptions{})
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ config, configDiags := indexToConfig(writeIndex, true)
+ if configDiags.HasError() {
+ return nil, configDiags
+ }
+ configs = append(configs, config)
+ }
+
+ // Handle read indices
+ if !model.ReadIndices.IsNull() {
+ var readIndices []indexModel
+ diags := model.ReadIndices.ElementsAs(ctx, &readIndices, false)
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ for _, readIndex := range readIndices {
+ config, configDiags := indexToConfig(readIndex, false)
+ if configDiags.HasError() {
+ return nil, configDiags
+ }
+ configs = append(configs, config)
+ }
+ }
+
+ return configs, nil
+}
+
+// indexToConfig converts an indexModel to AliasIndexConfig
+func indexToConfig(index indexModel, isWriteIndex bool) (AliasIndexConfig, diag.Diagnostics) {
+ config := AliasIndexConfig{
+ Name: index.Name.ValueString(),
+ IsWriteIndex: isWriteIndex,
+ IsHidden: index.IsHidden.ValueBool(),
+ }
+
+ if !index.IndexRouting.IsNull() {
+ config.IndexRouting = index.IndexRouting.ValueString()
+ }
+ if !index.Routing.IsNull() {
+ config.Routing = index.Routing.ValueString()
+ }
+ if !index.SearchRouting.IsNull() {
+ config.SearchRouting = index.SearchRouting.ValueString()
+ }
+ if !index.Filter.IsNull() {
+ if diags := index.Filter.Unmarshal(&config.Filter); diags.HasError() {
+ return AliasIndexConfig{}, diags
+ }
+ }
+
+ return config, nil
+}
diff --git a/internal/elasticsearch/index/alias/read.go b/internal/elasticsearch/index/alias/read.go
new file mode 100644
index 000000000..fa48dbd2b
--- /dev/null
+++ b/internal/elasticsearch/index/alias/read.go
@@ -0,0 +1,73 @@
+package alias
+
+import (
+ "context"
+
+ "github.com/elastic/terraform-provider-elasticstack/internal/clients"
+ "github.com/elastic/terraform-provider-elasticstack/internal/clients/elasticsearch"
+ "github.com/elastic/terraform-provider-elasticstack/internal/models"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+func (r *aliasResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var stateModel tfModel
+
+ resp.Diagnostics.Append(req.State.Get(ctx, &stateModel)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ aliasName := stateModel.Name.ValueString()
+
+ // Read the alias and update the model
+ diags := readAliasIntoModel(ctx, r.client, aliasName, &stateModel)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Check if the alias was found
+ if stateModel.WriteIndex.IsNull() && stateModel.ReadIndices.IsNull() {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, stateModel)...)
+}
+
+// readAliasIntoModel reads an alias from Elasticsearch and populates the provided model
+func readAliasIntoModel(ctx context.Context, client *clients.ApiClient, aliasName string, model *tfModel) diag.Diagnostics {
+ // Get the alias
+ indices, diags := elasticsearch.GetAlias(ctx, client, aliasName)
+ if diags.HasError() {
+ return diags
+ }
+
+ // If no indices returned, the alias doesn't exist
+ if len(indices) == 0 {
+ // Set both to null to indicate the alias doesn't exist
+ model.WriteIndex = types.ObjectNull(getIndexAttrTypes())
+ model.ReadIndices = types.SetNull(types.ObjectType{AttrTypes: getIndexAttrTypes()})
+ return nil
+ }
+
+ // Extract alias data from the response
+ aliasData := make(map[string]models.IndexAlias)
+ for indexName, index := range indices {
+ if alias, exists := index.Aliases[aliasName]; exists {
+ aliasData[indexName] = alias
+ }
+ }
+
+ if len(aliasData) == 0 {
+ // Set both to null to indicate the alias doesn't exist
+ model.WriteIndex = types.ObjectNull(getIndexAttrTypes())
+ model.ReadIndices = types.SetNull(types.ObjectType{AttrTypes: getIndexAttrTypes()})
+ return nil
+ }
+
+ // Update the model with API data
+ return model.populateFromAPI(ctx, aliasName, aliasData)
+}
diff --git a/internal/elasticsearch/index/alias/resource.go b/internal/elasticsearch/index/alias/resource.go
new file mode 100644
index 000000000..28c7607d2
--- /dev/null
+++ b/internal/elasticsearch/index/alias/resource.go
@@ -0,0 +1,87 @@
+package alias
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/elastic/terraform-provider-elasticstack/internal/clients"
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+)
+
+// Ensure provider defined types fully satisfy framework interfaces
+var _ resource.Resource = &aliasResource{}
+var _ resource.ResourceWithConfigure = &aliasResource{}
+var _ resource.ResourceWithImportState = &aliasResource{}
+var _ resource.ResourceWithValidateConfig = &aliasResource{}
+
+func NewAliasResource() resource.Resource {
+ return &aliasResource{}
+}
+
+type aliasResource struct {
+ client *clients.ApiClient
+}
+
+func (r *aliasResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_elasticsearch_index_alias"
+}
+
+func (r *aliasResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ client, diags := clients.ConvertProviderData(req.ProviderData)
+ resp.Diagnostics.Append(diags...)
+ r.client = client
+}
+
+func (r *aliasResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ // Retrieve import ID and save to id attribute
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *aliasResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
+ var config tfModel
+
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Validate that write_index doesn't appear in read_indices
+ if config.WriteIndex.IsNull() {
+ return
+ }
+
+ if config.ReadIndices.IsNull() {
+ return
+ }
+
+ // Get the write index name
+ var writeIndex indexModel
+ diags := config.WriteIndex.As(ctx, &writeIndex, basetypes.ObjectAsOptions{})
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ writeIndexName := writeIndex.Name.ValueString()
+
+ // Only validate if write index name is not empty
+ if writeIndexName == "" {
+ return
+ }
+
+ // Get all read indices
+ var readIndices []indexModel
+ if diags := config.ReadIndices.ElementsAs(ctx, &readIndices, false); !diags.HasError() {
+ for _, readIndex := range readIndices {
+ readIndexName := readIndex.Name.ValueString()
+ if readIndexName != "" && readIndexName == writeIndexName {
+ resp.Diagnostics.AddError(
+ "Invalid Configuration",
+ fmt.Sprintf("Index '%s' cannot be both a write index and a read index", writeIndexName),
+ )
+ return
+ }
+ }
+ }
+}
diff --git a/internal/elasticsearch/index/alias/schema.go b/internal/elasticsearch/index/alias/schema.go
new file mode 100644
index 000000000..d170f2ee4
--- /dev/null
+++ b/internal/elasticsearch/index/alias/schema.go
@@ -0,0 +1,113 @@
+package alias
+
+import (
+ "context"
+
+ "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+)
+
+func (r *aliasResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = getSchema()
+}
+
+func getSchema() schema.Schema {
+ return schema.Schema{
+ Description: "Manages an Elasticsearch alias. " +
+ "See the [alias documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-aliases.html) for more details.",
+
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Description: "Generated ID of the alias resource.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "name": schema.StringAttribute{
+ Description: "The alias name.",
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ "write_index": schema.SingleNestedAttribute{
+ Description: "The write index for the alias. Only one write index is allowed per alias.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "name": schema.StringAttribute{
+ Description: "Name of the write index.",
+ Required: true,
+ },
+ "filter": schema.StringAttribute{
+ Description: "Query used to limit documents the alias can access.",
+ Optional: true,
+ CustomType: jsontypes.NormalizedType{},
+ },
+ "index_routing": schema.StringAttribute{
+ Description: "Value used to route indexing operations to a specific shard.",
+ Optional: true,
+ },
+ "is_hidden": schema.BoolAttribute{
+ Description: "If true, the alias is hidden.",
+ Optional: true,
+ Computed: true,
+ Default: booldefault.StaticBool(false),
+ },
+ "routing": schema.StringAttribute{
+ Description: "Value used to route indexing and search operations to a specific shard.",
+ Optional: true,
+ },
+ "search_routing": schema.StringAttribute{
+ Description: "Value used to route search operations to a specific shard.",
+ Optional: true,
+ },
+ },
+ },
+ "read_indices": schema.SetNestedAttribute{
+ Description: "Set of read indices for the alias.",
+ Optional: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "name": schema.StringAttribute{
+ Description: "Name of the read index.",
+ Required: true,
+ },
+ "filter": schema.StringAttribute{
+ Description: "Query used to limit documents the alias can access.",
+ Optional: true,
+ CustomType: jsontypes.NormalizedType{},
+ },
+ "index_routing": schema.StringAttribute{
+ Description: "Value used to route indexing operations to a specific shard.",
+ Optional: true,
+ },
+ "is_hidden": schema.BoolAttribute{
+ Description: "If true, the alias is hidden.",
+ Optional: true,
+ Computed: true,
+ Default: booldefault.StaticBool(false),
+ },
+ "routing": schema.StringAttribute{
+ Description: "Value used to route indexing and search operations to a specific shard.",
+ Optional: true,
+ },
+ "search_routing": schema.StringAttribute{
+ Description: "Value used to route search operations to a specific shard.",
+ Optional: true,
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func getIndexAttrTypes() map[string]attr.Type {
+ return getSchema().Attributes["write_index"].GetType().(attr.TypeWithAttributeTypes).AttributeTypes()
+}
diff --git a/internal/elasticsearch/index/alias/update.go b/internal/elasticsearch/index/alias/update.go
new file mode 100644
index 000000000..332d523dd
--- /dev/null
+++ b/internal/elasticsearch/index/alias/update.go
@@ -0,0 +1,97 @@
+package alias
+
+import (
+ "context"
+
+ "github.com/elastic/terraform-provider-elasticstack/internal/clients/elasticsearch"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+)
+
+func (r *aliasResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var planModel tfModel
+ var stateModel tfModel
+
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(req.State.Get(ctx, &stateModel)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ aliasName := planModel.Name.ValueString()
+
+ // Get current configuration from state
+ currentConfigs, diags := stateModel.toAliasConfigs(ctx)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Get planned configuration
+ plannedConfigs, diags := planModel.toAliasConfigs(ctx)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Build atomic actions
+ var actions []elasticsearch.AliasAction
+
+ // Create maps for easy lookup
+ currentIndexMap := make(map[string]AliasIndexConfig)
+ for _, config := range currentConfigs {
+ currentIndexMap[config.Name] = config
+ }
+
+ plannedIndexMap := make(map[string]AliasIndexConfig)
+ for _, config := range plannedConfigs {
+ plannedIndexMap[config.Name] = config
+ }
+
+ // Remove indices that are no longer in the plan
+ for indexName := range currentIndexMap {
+ if _, exists := plannedIndexMap[indexName]; !exists {
+ actions = append(actions, elasticsearch.AliasAction{
+ Type: "remove",
+ Index: indexName,
+ Alias: aliasName,
+ })
+ }
+ }
+
+ // Add or update indices in the plan
+ for _, config := range plannedConfigs {
+ action := elasticsearch.AliasAction{
+ Type: "add",
+ Index: config.Name,
+ Alias: aliasName,
+ IsWriteIndex: config.IsWriteIndex,
+ Filter: config.Filter,
+ IndexRouting: config.IndexRouting,
+ IsHidden: config.IsHidden,
+ Routing: config.Routing,
+ SearchRouting: config.SearchRouting,
+ }
+ actions = append(actions, action)
+ }
+
+ // Apply the atomic changes
+ if len(actions) > 0 {
+ resp.Diagnostics.Append(elasticsearch.UpdateAliasesAtomic(ctx, r.client, actions)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ }
+
+ // Read back the alias to ensure state consistency, updating the current model
+ diags = readAliasIntoModel(ctx, r.client, aliasName, &planModel)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, planModel)...)
+}
diff --git a/internal/elasticsearch/index/index/acc_test.go b/internal/elasticsearch/index/index/acc_test.go
index f0bfacff8..554c10782 100644
--- a/internal/elasticsearch/index/index/acc_test.go
+++ b/internal/elasticsearch/index/index/acc_test.go
@@ -270,16 +270,17 @@ provider "elasticstack" {
resource "elasticstack_elasticsearch_index" "test" {
name = "%s"
- alias {
- name = "test_alias_1"
- }
-
- alias {
- name = "test_alias_2"
- filter = jsonencode({
- term = { "user.id" = "developer" }
- })
- }
+ alias = [
+ {
+ name = "test_alias_1"
+ },
+ {
+ name = "test_alias_2"
+ filter = jsonencode({
+ term = { "user.id" = "developer" }
+ })
+ }
+ ]
mappings = jsonencode({
properties = {
@@ -303,9 +304,11 @@ provider "elasticstack" {
resource "elasticstack_elasticsearch_index" "test" {
name = "%s"
- alias {
- name = "test_alias_1"
- }
+ alias = [
+ {
+ name = "test_alias_1"
+ }
+ ]
mappings = jsonencode({
properties = {
@@ -328,9 +331,11 @@ resource "elasticstack_elasticsearch_index" "test" {
name = "%s"
number_of_replicas = 0
- alias {
- name = "test_alias_1"
- }
+ alias = [
+ {
+ name = "test_alias_1"
+ }
+ ]
mappings = jsonencode({
properties = {
@@ -516,10 +521,12 @@ resource "elasticstack_elasticsearch_index_template" "test" {
resource "elasticstack_elasticsearch_index" "test" {
name = "%s"
deletion_protection = false
- alias {
- name = "%s-alias"
- is_write_index = true
- }
+ alias = [
+ {
+ name = "%s-alias"
+ is_write_index = true
+ }
+ ]
lifecycle {
ignore_changes = [mappings]
}
diff --git a/internal/elasticsearch/index/index/schema.go b/internal/elasticsearch/index/index/schema.go
index 3c9264373..a0a8a4bb7 100644
--- a/internal/elasticsearch/index/index/schema.go
+++ b/internal/elasticsearch/index/index/schema.go
@@ -34,9 +34,66 @@ func getSchema() schema.Schema {
Description: "Creates Elasticsearch indices. See: https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html",
Blocks: map[string]schema.Block{
"elasticsearch_connection": providerschema.GetEsFWConnectionBlock("elasticsearch_connection", false),
- "alias": schema.SetNestedBlock{
- Description: "Aliases for the index.",
+ "settings": schema.ListNestedBlock{
+ Description: `DEPRECATED: Please use dedicated setting field. Configuration options for the index. See, https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html#index-modules-settings.
+**NOTE:** Static index settings (see: https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html#_static_index_settings) can be only set on the index creation and later cannot be removed or updated - _apply_ will return error`,
+ DeprecationMessage: "Using settings makes it easier to misconfigure. Use dedicated field for the each setting instead.",
+ Validators: []validator.List{
+ listvalidator.SizeBetween(1, 1),
+ },
NestedObject: schema.NestedBlockObject{
+ Blocks: map[string]schema.Block{
+ "setting": schema.SetNestedBlock{
+ Description: "Defines the setting for the index.",
+ Validators: []validator.Set{
+ setvalidator.SizeAtLeast(1),
+ },
+ NestedObject: schema.NestedBlockObject{
+ Attributes: map[string]schema.Attribute{
+ "name": schema.StringAttribute{
+ Description: "The name of the setting to set and track.",
+ Required: true,
+ },
+ "value": schema.StringAttribute{
+ Description: "The value of the setting to set and track.",
+ Required: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Description: "Internal identifier of the resource",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "name": schema.StringAttribute{
+ Description: "Name of the index you wish to create.",
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ Validators: []validator.String{
+ stringvalidator.LengthBetween(1, 255),
+ stringvalidator.NoneOf(".", ".."),
+ stringvalidator.RegexMatches(regexp.MustCompile(`^[^-_+]`), "cannot start with -, _, +"),
+ stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z0-9!$%&'()+.;=@[\]^{}~_-]+$`), "must contain lower case alphanumeric characters and selected punctuation, see: https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html#indices-create-api-path-params"),
+ },
+ },
+ "alias": schema.SetNestedAttribute{
+ Description: "Aliases for the index.",
+ Optional: true,
+ Computed: true,
+ PlanModifiers: []planmodifier.Set{
+ setplanmodifier.UseStateForUnknown(),
+ },
+ NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Description: "Index alias name.",
@@ -95,58 +152,6 @@ func getSchema() schema.Schema {
},
},
},
- "settings": schema.ListNestedBlock{
- Description: `DEPRECATED: Please use dedicated setting field. Configuration options for the index. See, https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html#index-modules-settings.
-**NOTE:** Static index settings (see: https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html#_static_index_settings) can be only set on the index creation and later cannot be removed or updated - _apply_ will return error`,
- DeprecationMessage: "Using settings makes it easier to misconfigure. Use dedicated field for the each setting instead.",
- Validators: []validator.List{
- listvalidator.SizeBetween(1, 1),
- },
- NestedObject: schema.NestedBlockObject{
- Blocks: map[string]schema.Block{
- "setting": schema.SetNestedBlock{
- Description: "Defines the setting for the index.",
- Validators: []validator.Set{
- setvalidator.SizeAtLeast(1),
- },
- NestedObject: schema.NestedBlockObject{
- Attributes: map[string]schema.Attribute{
- "name": schema.StringAttribute{
- Description: "The name of the setting to set and track.",
- Required: true,
- },
- "value": schema.StringAttribute{
- Description: "The value of the setting to set and track.",
- Required: true,
- },
- },
- },
- },
- },
- },
- },
- },
- Attributes: map[string]schema.Attribute{
- "id": schema.StringAttribute{
- Description: "Internal identifier of the resource",
- Computed: true,
- PlanModifiers: []planmodifier.String{
- stringplanmodifier.UseStateForUnknown(),
- },
- },
- "name": schema.StringAttribute{
- Description: "Name of the index you wish to create.",
- Required: true,
- PlanModifiers: []planmodifier.String{
- stringplanmodifier.RequiresReplace(),
- },
- Validators: []validator.String{
- stringvalidator.LengthBetween(1, 255),
- stringvalidator.NoneOf(".", ".."),
- stringvalidator.RegexMatches(regexp.MustCompile(`^[^-_+]`), "cannot start with -, _, +"),
- stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z0-9!$%&'()+.;=@[\]^{}~_-]+$`), "must contain lower case alphanumeric characters and selected punctuation, see: https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html#indices-create-api-path-params"),
- },
- },
// Static settings that can only be set on creation
"number_of_shards": schema.Int64Attribute{
Description: "Number of shards for the index. This can be set only on creation.",
@@ -511,7 +516,7 @@ func getSchema() schema.Schema {
}
func aliasElementType() attr.Type {
- return getSchema().Blocks["alias"].Type().(attr.TypeWithElementType).ElementType()
+ return getSchema().Attributes["alias"].GetType().(attr.TypeWithElementType).ElementType()
}
func settingsElementType() attr.Type {
diff --git a/internal/elasticsearch/index/index/update.go b/internal/elasticsearch/index/index/update.go
index 69e8e9347..067f149b7 100644
--- a/internal/elasticsearch/index/index/update.go
+++ b/internal/elasticsearch/index/index/update.go
@@ -2,7 +2,7 @@ package index
import (
"context"
- "maps"
+ "reflect"
"github.com/elastic/terraform-provider-elasticstack/internal/clients"
"github.com/elastic/terraform-provider-elasticstack/internal/clients/elasticsearch"
@@ -115,7 +115,7 @@ func (r *Resource) updateSettings(ctx context.Context, client *clients.ApiClient
}
}
- if !maps.Equal(planDynamicSettings, stateDynamicSettings) {
+ if !deepEqual(planDynamicSettings, stateDynamicSettings) {
// Settings which are being removed must be explicitly set to null in the new settings
for setting := range stateDynamicSettings {
if _, ok := planDynamicSettings[setting]; !ok {
@@ -149,3 +149,24 @@ func (r *Resource) updateMappings(ctx context.Context, client *clients.ApiClient
return nil
}
+
+// deepEqual compares two maps for deep equality, handling slices and other types
+// that are not comparable with maps.Equal
+func deepEqual(a, b map[string]interface{}) bool {
+ if len(a) != len(b) {
+ return false
+ }
+
+ for key, valueA := range a {
+ valueB, exists := b[key]
+ if !exists {
+ return false
+ }
+
+ if !reflect.DeepEqual(valueA, valueB) {
+ return false
+ }
+ }
+
+ return true
+}
diff --git a/internal/elasticsearch/transform/transform_test.go b/internal/elasticsearch/transform/transform_test.go
index 45d457205..c3e4c6e5d 100644
--- a/internal/elasticsearch/transform/transform_test.go
+++ b/internal/elasticsearch/transform/transform_test.go
@@ -210,9 +210,9 @@ provider "elasticstack" {
resource "elasticstack_elasticsearch_index" "test_source_index_1" {
name = "source_index_for_transform"
- alias {
+ alias = [{
name = "test_alias_1"
- }
+ }]
mappings = jsonencode({
properties = {
@@ -229,9 +229,9 @@ resource "elasticstack_elasticsearch_index" "test_source_index_1" {
resource "elasticstack_elasticsearch_index" "test_source_index_2" {
name = "additional_index"
- alias {
+ alias = [{
name = "test_alias_2"
- }
+ }]
mappings = jsonencode({
properties = {
@@ -342,9 +342,9 @@ provider "elasticstack" {
resource "elasticstack_elasticsearch_index" "test_index" {
name = "%s"
- alias {
+ alias = [{
name = "test_alias_1"
- }
+ }]
mappings = jsonencode({
properties = {
diff --git a/provider/plugin_framework.go b/provider/plugin_framework.go
index b5c681d33..e02d3e09d 100644
--- a/provider/plugin_framework.go
+++ b/provider/plugin_framework.go
@@ -8,6 +8,7 @@ import (
"github.com/elastic/terraform-provider-elasticstack/internal/clients/config"
"github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/cluster/script"
"github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/enrich"
+ "github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/index/alias"
"github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/index/data_stream_lifecycle"
"github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/index/index"
"github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/index/indices"
@@ -121,6 +122,7 @@ func (p *Provider) Resources(ctx context.Context) []func() resource.Resource {
maintenance_window.NewResource,
enrich.NewEnrichPolicyResource,
role_mapping.NewRoleMappingResource,
+ alias.NewAliasResource,
security_detection_rule.NewSecurityDetectionRuleResource,
}
}