From be657dfcaf1724d181c530a67485cdbf69d27d94 Mon Sep 17 00:00:00 2001 From: Yann Soubeyrand Date: Thu, 4 Feb 2021 18:04:32 +0100 Subject: [PATCH] Add support for ElasticSearch 7.8+ component templates Signed-off-by: Yann Soubeyrand --- es/diff_suppress_funcs.go | 20 ++ es/provider.go | 1 + ...source_elasticsearch_component_template.go | 178 +++++++++++++++++ ...e_elasticsearch_component_template_test.go | 189 ++++++++++++++++++ es/util.go | 13 ++ go.mod | 2 + go.sum | 4 + 7 files changed, 407 insertions(+) create mode 100644 es/resource_elasticsearch_component_template.go create mode 100644 es/resource_elasticsearch_component_template_test.go diff --git a/es/diff_suppress_funcs.go b/es/diff_suppress_funcs.go index b29c8b5f..e23cbbe6 100644 --- a/es/diff_suppress_funcs.go +++ b/es/diff_suppress_funcs.go @@ -51,6 +51,26 @@ func diffSuppressComposableIndexTemplate(k, old, new string, d *schema.ResourceD return reflect.DeepEqual(oo, no) } +func diffSuppressComponentTemplate(k, old, new string, d *schema.ResourceData) bool { + var oo, no interface{} + if err := json.Unmarshal([]byte(old), &oo); err != nil { + return false + } + if err := json.Unmarshal([]byte(new), &no); err != nil { + return false + } + + if om, ok := oo.(map[string]interface{}); ok { + normalizeComponentTemplate(om) + } + + if nm, ok := no.(map[string]interface{}); ok { + normalizeComponentTemplate(nm) + } + + return reflect.DeepEqual(oo, no) +} + func diffSuppressDestination(k, old, new string, d *schema.ResourceData) bool { var oo, no interface{} if err := json.Unmarshal([]byte(old), &oo); err != nil { diff --git a/es/provider.go b/es/provider.go index 21d652f7..4ac4e52e 100644 --- a/es/provider.go +++ b/es/provider.go @@ -169,6 +169,7 @@ func Provider() terraform.ResourceProvider { "elasticsearch_index_lifecycle_policy": resourceElasticsearchDeprecatedIndexLifecyclePolicy(), "elasticsearch_index_template": resourceElasticsearchIndexTemplate(), "elasticsearch_composable_index_template": resourceElasticsearchComposableIndexTemplate(), + "elasticsearch_component_template": resourceElasticsearchComponentTemplate(), "elasticsearch_ingest_pipeline": resourceElasticsearchIngestPipeline(), "elasticsearch_kibana_object": resourceElasticsearchKibanaObject(), "elasticsearch_monitor": resourceElasticsearchDeprecatedMonitor(), diff --git a/es/resource_elasticsearch_component_template.go b/es/resource_elasticsearch_component_template.go new file mode 100644 index 00000000..16b616dc --- /dev/null +++ b/es/resource_elasticsearch_component_template.go @@ -0,0 +1,178 @@ +package es + +import ( + "context" + "encoding/json" + "fmt" + "log" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + elastic7 "github.com/olivere/elastic/v7" +) + +var componentTemplateMinimalVersion, _ = version.NewVersion("7.8.0") + +func resourceElasticsearchComponentTemplate() *schema.Resource { + return &schema.Resource{ + Create: resourceElasticsearchComponentTemplateCreate, + Read: resourceElasticsearchComponentTemplateRead, + Update: resourceElasticsearchComponentTemplateUpdate, + Delete: resourceElasticsearchComponentTemplateDelete, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + ForceNew: true, + Required: true, + }, + "body": { + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: diffSuppressComponentTemplate, + ValidateFunc: validation.StringIsJSON, + }, + }, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + } +} + +func resourceElasticsearchComponentTemplateCreate(d *schema.ResourceData, meta interface{}) error { + err := resourceElasticsearchPutComponentTemplate(d, meta, true) + if err != nil { + return err + } + d.SetId(d.Get("name").(string)) + return nil +} + +func resourceElasticsearchComponentTemplateRead(d *schema.ResourceData, meta interface{}) error { + id := d.Id() + + var result string + var elasticVersion *version.Version + + esClient, err := getClient(meta.(*ProviderConf)) + if err != nil { + return err + } + + switch client := esClient.(type) { + case *elastic7.Client: + elasticVersion, err = elastic7GetVersion(client) + if err == nil { + if elasticVersion.LessThan(componentTemplateMinimalVersion) { + err = fmt.Errorf("component_template endpoint only available from ElasticSearch >= 7.8, got version %s", elasticVersion.String()) + } else { + result, err = elastic7GetComponentTemplate(client, id) + } + } + default: + err = fmt.Errorf("component_template endpoint only available from ElasticSearch >= 7.8, got version < 7.0.0") + } + if err != nil { + if elastic7.IsNotFound(err) { + log.Printf("[WARN] Index template (%s) not found, removing from state", id) + d.SetId("") + return nil + } + + return err + } + + ds := &resourceDataSetter{d: d} + ds.set("name", d.Id()) + ds.set("body", result) + return ds.err +} + +func elastic7GetComponentTemplate(client *elastic7.Client, id string) (string, error) { + res, err := client.IndexGetComponentTemplate(id).Do(context.TODO()) + if err != nil { + return "", err + } + + // No more than 1 element is expected, if the index template is not found, previous call should + // return a 404 error + t := res.ComponentTemplates[0].ComponentTemplate + tj, err := json.Marshal(t) + if err != nil { + return "", err + } + return string(tj), nil +} + +func resourceElasticsearchComponentTemplateUpdate(d *schema.ResourceData, meta interface{}) error { + return resourceElasticsearchPutComponentTemplate(d, meta, false) +} + +func resourceElasticsearchComponentTemplateDelete(d *schema.ResourceData, meta interface{}) error { + id := d.Id() + + var elasticVersion *version.Version + + esClient, err := getClient(meta.(*ProviderConf)) + if err != nil { + return err + } + + switch client := esClient.(type) { + case *elastic7.Client: + elasticVersion, err = elastic7GetVersion(client) + if err == nil { + if elasticVersion.LessThan(componentTemplateMinimalVersion) { + err = fmt.Errorf("component_template endpoint only available from ElasticSearch >= 7.8, got version %s", elasticVersion.String()) + } else { + err = elastic7DeleteComponentTemplate(client, id) + } + } + default: + err = fmt.Errorf("component_template endpoint only available from ElasticSearch >= 7.8, got version < 7.0.0") + } + + if err != nil { + return err + } + d.SetId("") + return nil +} + +func elastic7DeleteComponentTemplate(client *elastic7.Client, id string) error { + _, err := client.IndexDeleteComponentTemplate(id).Do(context.TODO()) + return err +} + +func resourceElasticsearchPutComponentTemplate(d *schema.ResourceData, meta interface{}, create bool) error { + name := d.Get("name").(string) + body := d.Get("body").(string) + + var elasticVersion *version.Version + + esClient, err := getClient(meta.(*ProviderConf)) + if err != nil { + return err + } + + switch client := esClient.(type) { + case *elastic7.Client: + elasticVersion, err = elastic7GetVersion(client) + if err == nil { + if elasticVersion.LessThan(componentTemplateMinimalVersion) { + err = fmt.Errorf("component_template endpoint only available from ElasticSearch >= 7.8, got version %s", elasticVersion.String()) + } else { + err = elastic7PutComponentTemplate(client, name, body, create) + } + } + default: + err = fmt.Errorf("component_template endpoint only available from ElasticSearch >= 7.8, got version < 7.0.0") + } + + return err +} + +func elastic7PutComponentTemplate(client *elastic7.Client, name string, body string, create bool) error { + _, err := client.IndexPutComponentTemplate(name).BodyString(body).Create(create).Do(context.TODO()) + return err +} diff --git a/es/resource_elasticsearch_component_template_test.go b/es/resource_elasticsearch_component_template_test.go new file mode 100644 index 00000000..257fb470 --- /dev/null +++ b/es/resource_elasticsearch_component_template_test.go @@ -0,0 +1,189 @@ +package es + +import ( + "context" + "errors" + "fmt" + "testing" + + elastic7 "github.com/olivere/elastic/v7" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/terraform" +) + +func TestAccElasticsearchComponentTemplate(t *testing.T) { + provider := Provider().(*schema.Provider) + err := provider.Configure(&terraform.ResourceConfig{}) + if err != nil { + t.Skipf("err: %s", err) + } + meta := provider.Meta() + + esClient, err := getClient(meta.(*ProviderConf)) + if err != nil { + t.Skipf("err: %s", err) + } + + var allowed bool + switch esClient.(type) { + case *elastic7.Client: + allowed = true + default: + allowed = false + } + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + if !allowed { + t.Skip("/_component_template endpoint only supported on ES >= 7.8") + } + }, + Providers: testAccProviders, + CheckDestroy: testCheckElasticsearchComponentTemplateDestroy, + Steps: []resource.TestStep{ + { + Config: testAccElasticsearchComponentTemplate, + Check: resource.ComposeTestCheckFunc( + testCheckElasticsearchComponentTemplateExists("elasticsearch_component_template.test"), + ), + }, + }, + }) +} + +func TestAccElasticsearchComponentTemplate_importBasic(t *testing.T) { + provider := Provider().(*schema.Provider) + err := provider.Configure(&terraform.ResourceConfig{}) + if err != nil { + t.Skipf("err: %s", err) + } + meta := provider.Meta() + + esClient, err := getClient(meta.(*ProviderConf)) + if err != nil { + t.Skipf("err: %s", err) + } + + var allowed bool + switch esClient.(type) { + case *elastic7.Client: + allowed = true + default: + allowed = false + } + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + if !allowed { + t.Skip("/_component_template endpoint only supported on ES >= 7.8") + } + }, + Providers: testAccProviders, + CheckDestroy: testCheckElasticsearchComponentTemplateDestroy, + Steps: []resource.TestStep{ + { + Config: testAccElasticsearchComponentTemplate, + }, + { + ResourceName: "elasticsearch_component_template.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testCheckElasticsearchComponentTemplateExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + if rs.Primary.ID == "" { + return fmt.Errorf("No component template ID is set") + } + + meta := testAccProvider.Meta() + + esClient, err := getClient(meta.(*ProviderConf)) + if err != nil { + return err + } + + switch client := esClient.(type) { + case *elastic7.Client: + _, err = client.IndexGetComponentTemplate(rs.Primary.ID).Do(context.TODO()) + default: + err = errors.New("/_component_template endpoint only supported on ES >= 7.8") + } + + if err != nil { + return err + } + + return nil + } +} + +func testCheckElasticsearchComponentTemplateDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "elasticsearch_component_template" { + continue + } + + meta := testAccProvider.Meta() + + esClient, err := getClient(meta.(*ProviderConf)) + if err != nil { + return err + } + + switch client := esClient.(type) { + case *elastic7.Client: + _, err = client.IndexGetComponentTemplate(rs.Primary.ID).Do(context.TODO()) + default: + err = errors.New("/_component_template endpoint only supported on ES >= 7.8") + } + + if err != nil { + return nil // should be not found error + } + + return fmt.Errorf("Component template %q still exists", rs.Primary.ID) + } + + return nil +} + +var testAccElasticsearchComponentTemplate = ` +resource "elasticsearch_component_template" "test" { + name = "terraform-test" + body = < github.com/camptocamp/elastic/v7 v7.0.23-0.20210204165908-e3d81ffeb2ca diff --git a/go.sum b/go.sum index 10971b6c..801597f8 100644 --- a/go.sum +++ b/go.sum @@ -34,6 +34,10 @@ github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1U github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/camptocamp/elastic/v7 v7.0.23-0.20210203185451-ae41d53f9aa9 h1:tSI/TIbioutH2yVRTu0rxvDqUGHmwRRbH310T7SmAFg= +github.com/camptocamp/elastic/v7 v7.0.23-0.20210203185451-ae41d53f9aa9/go.mod h1:VDexNy9NjmtAkrjNoI7tImv7FR4tf5zUA3ickqu5Pc8= +github.com/camptocamp/elastic/v7 v7.0.23-0.20210204165908-e3d81ffeb2ca h1:CyuZfYRC+MeTJCbVGzAuEPJvtD0ZkUP6zGBRiyGJC34= +github.com/camptocamp/elastic/v7 v7.0.23-0.20210204165908-e3d81ffeb2ca/go.mod h1:VDexNy9NjmtAkrjNoI7tImv7FR4tf5zUA3ickqu5Pc8= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=