internal/elasticsearch/index/template.go (427 lines of code) (raw):
package index
import (
"context"
"encoding/json"
"fmt"
"strings"
"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/elastic/terraform-provider-elasticstack/internal/utils"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)
func ResourceTemplate() *schema.Resource {
templateSchema := map[string]*schema.Schema{
"id": {
Description: "Internal identifier of the resource",
Type: schema.TypeString,
Computed: true,
},
"name": {
Description: "Name of the index template to create.",
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"composed_of": {
Description: "An ordered list of component template names.",
Type: schema.TypeList,
Optional: true,
Computed: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"data_stream": {
Description: "If this object is included, the template is used to create data streams and their backing indices. Supports an empty object.",
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"hidden": {
Description: "If true, the data stream is hidden.",
Type: schema.TypeBool,
Default: false,
Optional: true,
},
"allow_custom_routing": {
Description: "If `true`, the data stream supports custom routing. Defaults to `false`. Available only in **8.x**",
Type: schema.TypeBool,
Optional: true,
},
},
},
},
"index_patterns": {
Description: "Array of wildcard (*) expressions used to match the names of data streams and indices during creation.",
Type: schema.TypeSet,
Required: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"metadata": {
Description: "Optional user metadata about the index template.",
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.StringIsJSON,
DiffSuppressFunc: utils.DiffJsonSuppress,
},
"priority": {
Description: "Priority to determine index template precedence when a new data stream or index is created.",
Type: schema.TypeInt,
ValidateFunc: validation.IntAtLeast(0),
Optional: true,
},
"template": {
Description: "Template to be applied. It may optionally include an aliases, mappings, lifecycle, or settings configuration.",
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"alias": {
Description: "Alias to add.",
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": {
Description: "The alias name.",
Type: schema.TypeString,
Required: true,
},
"filter": {
Description: "Query used to limit documents the alias can access.",
Type: schema.TypeString,
Optional: true,
Default: "",
DiffSuppressFunc: utils.DiffJsonSuppress,
ValidateFunc: validation.StringIsJSON,
},
"index_routing": {
Description: "Value used to route indexing operations to a specific shard. If specified, this overwrites the `routing` value for indexing operations.",
Type: schema.TypeString,
Optional: true,
Default: "",
},
"is_hidden": {
Description: "If true, the alias is hidden.",
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"is_write_index": {
Description: "If true, the index is the write index for the alias.",
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"routing": {
Description: "Value used to route indexing and search operations to a specific shard.",
Type: schema.TypeString,
Optional: true,
Default: "",
},
"search_routing": {
Description: "Value used to route search operations to a specific shard. If specified, this overwrites the routing value for search operations.",
Type: schema.TypeString,
Optional: true,
Default: "",
},
},
},
},
"mappings": {
Description: "Mapping for fields in the index. Should be specified as a JSON object of field mappings. See the documentation (https://www.elastic.co/guide/en/elasticsearch/reference/current/explicit-mapping.html) for more details",
Type: schema.TypeString,
Optional: true,
DiffSuppressFunc: utils.DiffJsonSuppress,
ValidateFunc: validation.All(
validation.StringIsJSON, stringIsJSONObject,
),
},
"settings": {
Description: "Configuration options for the index. See, https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html#index-modules-settings",
Type: schema.TypeString,
Optional: true,
DiffSuppressFunc: utils.DiffIndexSettingSuppress,
ValidateFunc: validation.All(
validation.StringIsJSON, stringIsJSONObject,
),
},
"lifecycle": {
Description: "Lifecycle of data stream. See, https://www.elastic.co/guide/en/elasticsearch/reference/current/data-stream-lifecycle.html",
Type: schema.TypeSet,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"data_retention": {
Description: "The retention period of the data indexed in this data stream.",
Type: schema.TypeString,
Required: true,
},
},
},
},
},
},
},
"version": {
Description: "Version number used to manage index templates externally.",
Type: schema.TypeInt,
Optional: true,
},
}
utils.AddConnectionSchema(templateSchema)
return &schema.Resource{
Description: "Creates or updates an index template. Index templates define settings, mappings, and aliases that can be applied automatically to new indices. See, https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-put-template.html",
CreateContext: resourceIndexTemplatePut,
UpdateContext: resourceIndexTemplatePut,
ReadContext: resourceIndexTemplateRead,
DeleteContext: resourceIndexTemplateDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
Schema: templateSchema,
}
}
func resourceIndexTemplatePut(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client, diags := clients.NewApiClientFromSDKResource(d, meta)
if diags.HasError() {
return diags
}
templateId := d.Get("name").(string)
id, diags := client.ID(ctx, templateId)
if diags.HasError() {
return diags
}
var indexTemplate models.IndexTemplate
indexTemplate.Name = templateId
compsOf := make([]string, 0)
if v, ok := d.GetOk("composed_of"); ok {
for _, c := range v.([]interface{}) {
compsOf = append(compsOf, c.(string))
}
}
indexTemplate.ComposedOf = compsOf
if v, ok := d.GetOk("data_stream"); ok {
// 8.x workaround
hasAllowCustomRouting := false
if d.HasChange("data_stream") {
old, _ := d.GetChange("data_stream")
if old != nil && len(old.([]interface{})) == 1 {
if old.([]interface{})[0] != nil {
setting := old.([]interface{})[0].(map[string]interface{})
if acr, ok := setting["allow_custom_routing"]; ok && acr.(bool) {
hasAllowCustomRouting = true
}
}
}
}
// only one definition of stream allowed
if v.([]interface{})[0] != nil {
stream := v.([]interface{})[0].(map[string]interface{})
dSettings := &models.DataStreamSettings{}
if s, ok := stream["hidden"]; ok {
hidden := s.(bool)
dSettings.Hidden = &hidden
}
if s, ok := stream["allow_custom_routing"]; ok && (hasAllowCustomRouting || s.(bool)) {
allow := s.(bool)
dSettings.AllowCustomRouting = &allow
}
indexTemplate.DataStream = dSettings
}
}
if v, ok := d.GetOk("index_patterns"); ok {
definedIndPats := v.(*schema.Set)
indPats := make([]string, definedIndPats.Len())
for i, p := range definedIndPats.List() {
indPats[i] = p.(string)
}
indexTemplate.IndexPatterns = indPats
}
if v, ok := d.GetOk("metadata"); ok {
metadata := make(map[string]interface{})
if err := json.NewDecoder(strings.NewReader(v.(string))).Decode(&metadata); err != nil {
return diag.FromErr(err)
}
indexTemplate.Meta = metadata
}
if v, ok := d.GetOk("priority"); ok {
definedPr := v.(int)
indexTemplate.Priority = &definedPr
}
if v, ok := d.GetOk("template"); ok {
templ, ok, diags := expandTemplate(v)
if diags != nil {
return diags
}
if ok {
indexTemplate.Template = &templ
}
}
if v, ok := d.GetOk("version"); ok {
definedVer := v.(int)
indexTemplate.Version = &definedVer
}
if diags := elasticsearch.PutIndexTemplate(ctx, client, &indexTemplate); diags.HasError() {
return diags
}
d.SetId(id.String())
return resourceIndexTemplateRead(ctx, d, meta)
}
func expandTemplate(config interface{}) (models.Template, bool, diag.Diagnostics) {
templ := models.Template{}
// only one template block allowed to be declared
definedTempl, ok := config.([]interface{})[0].(map[string]interface{})
if !ok {
return templ, false, nil
}
aliases, diags := ExpandIndexAliases(definedTempl["alias"].(*schema.Set))
if diags.HasError() {
return templ, false, diags
}
templ.Aliases = aliases
if lc, ok := definedTempl["lifecycle"]; ok {
lifecycle := ExpandLifecycle(lc.(*schema.Set))
if lifecycle != nil {
templ.Lifecycle = lifecycle
}
}
if mappings, ok := definedTempl["mappings"]; ok {
if mappings.(string) != "" {
maps := make(map[string]interface{})
if err := json.Unmarshal([]byte(mappings.(string)), &maps); err != nil {
return templ, false, diag.FromErr(err)
}
templ.Mappings = maps
}
}
if settings, ok := definedTempl["settings"]; ok {
if settings.(string) != "" {
sets := make(map[string]interface{})
if err := json.Unmarshal([]byte(settings.(string)), &sets); err != nil {
return templ, false, diag.FromErr(err)
}
templ.Settings = sets
}
}
return templ, true, nil
}
func resourceIndexTemplateRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client, diags := clients.NewApiClientFromSDKResource(d, meta)
if diags.HasError() {
return diags
}
compId, diags := clients.CompositeIdFromStr(d.Id())
if diags.HasError() {
return diags
}
templateId := compId.ResourceId
tpl, diags := elasticsearch.GetIndexTemplate(ctx, client, templateId)
if tpl == nil && diags == nil {
tflog.Warn(ctx, fmt.Sprintf(`Index template "%s" not found, removing from state`, compId.ResourceId))
d.SetId("")
return diags
}
if diags.HasError() {
return diags
}
// set the fields
if err := d.Set("name", tpl.Name); err != nil {
return diag.FromErr(err)
}
if err := d.Set("composed_of", tpl.IndexTemplate.ComposedOf); err != nil {
return diag.FromErr(err)
}
if stream := tpl.IndexTemplate.DataStream; stream != nil {
ds := make([]interface{}, 1)
dSettings := make(map[string]interface{})
if v := stream.Hidden; v != nil {
dSettings["hidden"] = *v
}
if v := stream.AllowCustomRouting; v != nil {
dSettings["allow_custom_routing"] = *v
}
ds[0] = dSettings
if err := d.Set("data_stream", ds); err != nil {
return diag.FromErr(err)
}
}
if err := d.Set("index_patterns", tpl.IndexTemplate.IndexPatterns); err != nil {
return diag.FromErr(err)
}
if tpl.IndexTemplate.Meta != nil {
metadata, err := json.Marshal(tpl.IndexTemplate.Meta)
if err != nil {
return diag.FromErr(err)
}
if err := d.Set("metadata", string(metadata)); err != nil {
return diag.FromErr(err)
}
}
if err := d.Set("priority", tpl.IndexTemplate.Priority); err != nil {
return diag.FromErr(err)
}
if tpl.IndexTemplate.Template != nil {
template, diags := flattenTemplateData(tpl.IndexTemplate.Template)
if diags.HasError() {
return diags
}
if err := d.Set("template", template); err != nil {
return diag.FromErr(err)
}
}
if err := d.Set("version", tpl.IndexTemplate.Version); err != nil {
return diag.FromErr(err)
}
return diags
}
func flattenTemplateData(template *models.Template) ([]interface{}, diag.Diagnostics) {
var diags diag.Diagnostics
tmpl := make(map[string]interface{})
if template.Mappings != nil {
m, err := json.Marshal(template.Mappings)
if err != nil {
return nil, diag.FromErr(err)
}
tmpl["mappings"] = string(m)
}
if template.Settings != nil {
s, err := json.Marshal(template.Settings)
if err != nil {
return nil, diag.FromErr(err)
}
tmpl["settings"] = string(s)
}
if template.Aliases != nil {
aliases, diags := FlattenIndexAliases(template.Aliases)
if diags.HasError() {
return nil, diags
}
tmpl["alias"] = aliases
}
if template.Lifecycle != nil {
lifecycle := FlattenLifecycle(template.Lifecycle)
tmpl["lifecycle"] = lifecycle
}
return []interface{}{tmpl}, diags
}
func resourceIndexTemplateDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client, diags := clients.NewApiClientFromSDKResource(d, meta)
if diags.HasError() {
return diags
}
id := d.Id()
compId, diags := clients.CompositeIdFromStr(id)
if diags.HasError() {
return diags
}
if diags := elasticsearch.DeleteIndexTemplate(ctx, client, compId.ResourceId); diags.HasError() {
return diags
}
return diags
}