diff --git a/internal/clients/elasticsearch/index.go b/internal/clients/elasticsearch/index.go index 024dd4121..9f0fc46ee 100644 --- a/internal/clients/elasticsearch/index.go +++ b/internal/clients/elasticsearch/index.go @@ -309,8 +309,11 @@ func GetIndex(ctx context.Context, apiClient *clients.ApiClient, name string) (* return nil, diags } - index := indices[name] - return &index, diags + if index, ok := indices[name]; ok { + return &index, nil + } + + return nil, nil } func GetIndices(ctx context.Context, apiClient *clients.ApiClient, name string) (map[string]models.Index, fwdiags.Diagnostics) { diff --git a/internal/elasticsearch/index/index/acc_test.go b/internal/elasticsearch/index/index/acc_test.go index e109802fb..4fd2c1311 100644 --- a/internal/elasticsearch/index/index/acc_test.go +++ b/internal/elasticsearch/index/index/acc_test.go @@ -212,6 +212,25 @@ func TestAccResourceIndexSettings(t *testing.T) { }) } +func TestAccResourceIndexWithTemplate(t *testing.T) { + indexName := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlphaNum) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: checkResourceIndexDestroy, + ProtoV6ProviderFactories: acctest.Providers, + Steps: []resource.TestStep{ + { + Config: testAccResourceIndexWithTemplate(indexName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_elasticsearch_index.test", "name", indexName), + resource.TestCheckNoResourceAttr("elasticstack_elasticsearch_index.test", "default_pipeline"), + ), + }, + }, + }) +} + func TestAccResourceIndexRemovingField(t *testing.T) { indexName := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlphaNum) @@ -450,6 +469,38 @@ resource "elasticstack_elasticsearch_index" "test_settings_removing_field" { `, name) } +func testAccResourceIndexWithTemplate(name string) string { + return fmt.Sprintf(` +provider "elasticstack" { + elasticsearch {} +} + +resource "elasticstack_elasticsearch_index_template" "test" { + name = "%s" + index_patterns = ["%s"] + template { + settings = jsonencode({ + default_pipeline = ".fleet_final_pipeline-1" + lifecycle = { name = ".monitoring-8-ilm-policy" } + }) + } +} + +resource "elasticstack_elasticsearch_index" "test" { + name = "%s" + deletion_protection = false + alias { + name = "%s-alias" + is_write_index = true + } + lifecycle { + ignore_changes = [mappings] + } + depends_on = [elasticstack_elasticsearch_index_template.test] +} +`, name, name, name, name) +} + func checkResourceIndexDestroy(s *terraform.State) error { client, err := clients.NewAcceptanceTestingClient() if err != nil { diff --git a/internal/elasticsearch/index/index/models.go b/internal/elasticsearch/index/index/models.go index 047b8f1d7..bc3c20573 100644 --- a/internal/elasticsearch/index/index/models.go +++ b/internal/elasticsearch/index/index/models.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "reflect" - "strconv" "strings" "github.com/elastic/terraform-provider-elasticstack/internal/clients" @@ -223,141 +222,6 @@ func aliasesFromAPI(ctx context.Context, apiModel models.Index) (basetypes.SetVa } func setSettingsFromAPI(ctx context.Context, model *tfModel, apiModel models.Index) diag.Diagnostics { - modelType := reflect.TypeOf(*model) - - for _, key := range dynamicSettingsKeys { - settingsValue, ok := apiModel.Settings["index."+key] - var tfValue attr.Value - if !ok { - continue - } - - tfFieldKey := convertSettingsKeyToTFFieldKey(key) - value, ok := model.getFieldValueByTagValue(tfFieldKey, modelType) - if !ok { - return diag.Diagnostics{ - diag.NewErrorDiagnostic( - "failed to find setting value", - fmt.Sprintf("expected setting with key %s", tfFieldKey), - ), - } - } - - switch a := value.(type) { - case types.String: - settingStr, ok := settingsValue.(string) - if !ok { - return diag.Diagnostics{ - diag.NewErrorDiagnostic( - "failed to convert setting to string", - fmt.Sprintf("expected setting to be a string but got %t", settingsValue), - )} - } - tfValue = basetypes.NewStringValue(settingStr) - case types.Bool: - settingBool, ok := settingsValue.(bool) - if !ok { - return diag.Diagnostics{ - diag.NewErrorDiagnostic( - "failed to convert setting to bool", - fmt.Sprintf("expected setting to be a bool but got %t", settingsValue), - )} - } - tfValue = basetypes.NewBoolValue(settingBool) - case types.Int64: - if settingStr, ok := settingsValue.(string); ok { - settingInt, err := strconv.Atoi(settingStr) - if err != nil { - return diag.Diagnostics{ - diag.NewErrorDiagnostic( - "failed to convert setting to int", - fmt.Sprintf("expected setting to be an int but it was a string. Attempted to parse it but got %s", err.Error()), - ), - } - } - - settingsValue = int64(settingInt) - } - - settingInt, ok := settingsValue.(int64) - if !ok { - return diag.Diagnostics{ - diag.NewErrorDiagnostic( - "failed to convert setting to int", - fmt.Sprintf("expected setting to be a int but got %t", settingsValue), - )} - } - tfValue = basetypes.NewInt64Value(settingInt) - case types.List: - elemType := a.ElementType(ctx) - if elemType != types.StringType { - return diag.Diagnostics{ - diag.NewErrorDiagnostic( - "expected list of string", - fmt.Sprintf("expected list element type to be string but got %s", elemType), - ), - } - } - - elems, ok := settingsValue.([]interface{}) - if !ok { - return diag.Diagnostics{ - diag.NewErrorDiagnostic( - "failed to convert setting to []string", - fmt.Sprintf("expected setting to be a []string but got %#v", settingsValue), - )} - } - - var diags diag.Diagnostics - tfValue, diags = basetypes.NewListValueFrom(ctx, basetypes.StringType{}, elems) - if diags.HasError() { - return diags - } - case types.Set: - elemType := a.ElementType(ctx) - if elemType != types.StringType { - return diag.Diagnostics{ - diag.NewErrorDiagnostic( - "expected set of string", - fmt.Sprintf("expected set element type to be string but got %s", elemType), - ), - } - } - - elems, ok := settingsValue.([]interface{}) - if !ok { - return diag.Diagnostics{ - diag.NewErrorDiagnostic( - "failed to convert setting to []string", - fmt.Sprintf("expected setting to be a thing []string but got %#v", settingsValue), - )} - } - - var diags diag.Diagnostics - tfValue, diags = basetypes.NewSetValueFrom(ctx, basetypes.StringType{}, elems) - if diags.HasError() { - return diags - } - default: - return diag.Diagnostics{ - diag.NewErrorDiagnostic( - "unknown value type", - fmt.Sprintf("unknown index setting value type %s", a.Type(ctx)), - ), - } - } - - ok = model.setFieldValueByTagValue(tfFieldKey, modelType, tfValue) - if !ok { - return diag.Diagnostics{ - diag.NewErrorDiagnostic( - "failed to find setting value", - fmt.Sprintf("expected setting with key %s", tfFieldKey), - ), - } - } - } - settingsBytes, err := json.Marshal(apiModel.Settings) if err != nil { return diag.Diagnostics{ @@ -569,19 +433,6 @@ func (model tfModel) toIndexSettings(ctx context.Context) (map[string]interface{ return settings, nil } -func (model *tfModel) setFieldValueByTagValue(tagName string, t reflect.Type, value attr.Value) bool { - numField := t.NumField() - for i := 0; i < numField; i++ { - field := t.Field(i) - if field.Tag.Get("tfsdk") == tagName { - reflect.ValueOf(model).Elem().Field(i).Set(reflect.ValueOf(value)) - return true - } - } - - return false -} - func (model tfModel) getFieldValueByTagValue(tagName string, t reflect.Type) (attr.Value, bool) { numField := t.NumField() for i := 0; i < numField; i++ { diff --git a/internal/elasticsearch/index/index/schema.go b/internal/elasticsearch/index/index/schema.go index 1b07689c7..4dc63bc4f 100644 --- a/internal/elasticsearch/index/index/schema.go +++ b/internal/elasticsearch/index/index/schema.go @@ -7,6 +7,7 @@ import ( "github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/index" providerschema "github.com/elastic/terraform-provider-elasticstack/internal/schema" "github.com/elastic/terraform-provider-elasticstack/internal/utils/customtypes" + "github.com/elastic/terraform-provider-elasticstack/internal/utils/planmodifiers" "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" @@ -14,13 +15,11 @@ import ( "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/boolplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -58,31 +57,46 @@ func getSchema() schema.Schema { Description: "Value used to route indexing operations to a specific shard. If specified, this overwrites the `routing` value for indexing operations.", Optional: true, Computed: true, - Default: stringdefault.StaticString(""), + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + planmodifiers.StringUseDefaultIfUnknown(""), + }, }, "is_hidden": schema.BoolAttribute{ Description: "If true, the alias is hidden.", Optional: true, Computed: true, - Default: booldefault.StaticBool(false), + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + planmodifiers.BoolUseDefaultIfUnknown(false), + }, }, "is_write_index": schema.BoolAttribute{ Description: "If true, the index is the write index for the alias.", Optional: true, Computed: true, - Default: booldefault.StaticBool(false), + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + planmodifiers.BoolUseDefaultIfUnknown(false), + }, }, "routing": schema.StringAttribute{ Description: "Value used to route indexing and search operations to a specific shard.", Optional: true, Computed: true, - Default: stringdefault.StaticString(""), + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + planmodifiers.StringUseDefaultIfUnknown(""), + }, }, "search_routing": schema.StringAttribute{ Description: "Value used to route search operations to a specific shard. If specified, this overwrites the routing value for search operations.", Optional: true, Computed: true, - Default: stringdefault.StaticString(""), + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + planmodifiers.StringUseDefaultIfUnknown(""), + }, }, }, }, @@ -216,10 +230,6 @@ func getSchema() schema.Schema { "number_of_replicas": schema.Int64Attribute{ Description: "Number of shard replicas.", Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.UseStateForUnknown(), - }, }, "auto_expand_replicas": schema.StringAttribute{ Description: "Set the number of replicas to the node count in the cluster. Set to a dash delimited lower and upper bound (e.g. 0-5) or use all for the upper bound (e.g. 0-all)", @@ -457,8 +467,8 @@ func getSchema() schema.Schema { Validators: []validator.String{ index.StringIsJSONObject{}, }, - Default: stringdefault.StaticString("{}"), PlanModifiers: []planmodifier.String{ + planmodifiers.StringUseDefaultIfUnknown("{}"), mappingsPlanModifier{}, }, }, @@ -471,34 +481,44 @@ func getSchema() schema.Schema { "deletion_protection": schema.BoolAttribute{ Optional: true, Computed: true, - Default: booldefault.StaticBool(true), Description: "Whether to allow Terraform to destroy the index. Unless this field is set to false in Terraform state, a terraform destroy or terraform apply command that deletes the instance will fail.", + PlanModifiers: []planmodifier.Bool{ + planmodifiers.BoolUseDefaultIfUnknown(true), + }, }, "include_type_name": schema.BoolAttribute{ Description: "If true, a mapping type is expected in the body of mappings. Defaults to false. Supported for Elasticsearch 7.x.", Optional: true, Computed: true, - Default: booldefault.StaticBool(false), + PlanModifiers: []planmodifier.Bool{ + planmodifiers.BoolUseDefaultIfUnknown(false), + }, }, "wait_for_active_shards": schema.StringAttribute{ Description: "The number of shard copies that must be active before proceeding with the operation. Set to `all` or any positive integer up to the total number of shards in the index (number_of_replicas+1). Default: `1`, the primary shard. This value is ignored when running against Serverless projects.", Optional: true, Computed: true, - Default: stringdefault.StaticString("1"), + PlanModifiers: []planmodifier.String{ + planmodifiers.StringUseDefaultIfUnknown("1"), + }, }, "master_timeout": schema.StringAttribute{ Description: "Period to wait for a connection to the master node. If no response is received before the timeout expires, the request fails and returns an error. Defaults to `30s`. This value is ignored when running against Serverless projects.", Optional: true, Computed: true, - Default: stringdefault.StaticString("30s"), - CustomType: customtypes.DurationType{}, + PlanModifiers: []planmodifier.String{ + planmodifiers.StringUseDefaultIfUnknown("30s"), + }, + CustomType: customtypes.DurationType{}, }, "timeout": schema.StringAttribute{ Description: "Period to wait for a response. If no response is received before the timeout expires, the request fails and returns an error. Defaults to `30s`.", Optional: true, Computed: true, - Default: stringdefault.StaticString("30s"), - CustomType: customtypes.DurationType{}, + PlanModifiers: []planmodifier.String{ + planmodifiers.StringUseDefaultIfUnknown("30s"), + }, + CustomType: customtypes.DurationType{}, }, }, } diff --git a/internal/utils/planmodifiers/bool_default.go b/internal/utils/planmodifiers/bool_default.go new file mode 100644 index 000000000..4c722969d --- /dev/null +++ b/internal/utils/planmodifiers/bool_default.go @@ -0,0 +1,39 @@ +package planmodifiers + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func BoolUseDefaultIfUnknown(defaultValue bool) boolDefault { + return boolDefault{defaultValue: defaultValue} +} + +type boolDefault struct { + defaultValue bool +} + +func (bd boolDefault) PlanModifyBool(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { + // Do nothing if there is a known planned value. + if !req.PlanValue.IsUnknown() { + return + } + + // Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up. + if req.ConfigValue.IsUnknown() { + return + } + + resp.PlanValue = basetypes.NewBoolValue(bd.defaultValue) +} + +func (bd boolDefault) Description(context.Context) string { + return fmt.Sprintf("Sets the value to [%t] if unknown", bd.defaultValue) +} + +func (bd boolDefault) MarkdownDescription(ctx context.Context) string { + return bd.Description(ctx) +} diff --git a/internal/utils/planmodifiers/string_default.go b/internal/utils/planmodifiers/string_default.go new file mode 100644 index 000000000..ea86bab21 --- /dev/null +++ b/internal/utils/planmodifiers/string_default.go @@ -0,0 +1,39 @@ +package planmodifiers + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func StringUseDefaultIfUnknown(defaultValue string) stringDefault { + return stringDefault{defaultValue: defaultValue} +} + +type stringDefault struct { + defaultValue string +} + +func (bd stringDefault) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + // Do nothing if there is a known planned value. + if !req.PlanValue.IsUnknown() { + return + } + + // Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up. + if req.ConfigValue.IsUnknown() { + return + } + + resp.PlanValue = basetypes.NewStringValue(bd.defaultValue) +} + +func (bd stringDefault) Description(context.Context) string { + return fmt.Sprintf("Sets the value to [%s] if unknown", bd.defaultValue) +} + +func (bd stringDefault) MarkdownDescription(ctx context.Context) string { + return bd.Description(ctx) +}