From a768116210a7e450031273258a733d077bff2aaa Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 11 Sep 2025 09:39:32 +0000
Subject: [PATCH 01/11] Initial plan
From 190e627686a0feab370be6a788c1c8fbf674236f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 11 Sep 2025 09:55:56 +0000
Subject: [PATCH 02/11] Implement Plugin Framework script resource with full
functionality
Co-authored-by: tobio <444668+tobio@users.noreply.github.com>
---
.../elasticsearch/cluster/script/acc_test.go | 220 ++++++++++++++++++
.../elasticsearch/cluster/script/create.go | 15 ++
.../elasticsearch/cluster/script/delete.go | 33 +++
.../elasticsearch/cluster/script/models.go | 15 ++
internal/elasticsearch/cluster/script/read.go | 70 ++++++
.../elasticsearch/cluster/script/resource.go | 26 +++
.../elasticsearch/cluster/script/schema.go | 63 +++++
.../elasticsearch/cluster/script/update.go | 75 ++++++
internal/utils/validators/string_is_json.go | 35 +++
provider/plugin_framework.go | 2 +
provider/provider.go | 2 +-
11 files changed, 555 insertions(+), 1 deletion(-)
create mode 100644 internal/elasticsearch/cluster/script/acc_test.go
create mode 100644 internal/elasticsearch/cluster/script/create.go
create mode 100644 internal/elasticsearch/cluster/script/delete.go
create mode 100644 internal/elasticsearch/cluster/script/models.go
create mode 100644 internal/elasticsearch/cluster/script/read.go
create mode 100644 internal/elasticsearch/cluster/script/resource.go
create mode 100644 internal/elasticsearch/cluster/script/schema.go
create mode 100644 internal/elasticsearch/cluster/script/update.go
create mode 100644 internal/utils/validators/string_is_json.go
diff --git a/internal/elasticsearch/cluster/script/acc_test.go b/internal/elasticsearch/cluster/script/acc_test.go
new file mode 100644
index 000000000..a41990867
--- /dev/null
+++ b/internal/elasticsearch/cluster/script/acc_test.go
@@ -0,0 +1,220 @@
+package script_test
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/elastic/terraform-provider-elasticstack/internal/acctest"
+ "github.com/elastic/terraform-provider-elasticstack/internal/clients"
+ sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/hashicorp/terraform-plugin-testing/terraform"
+ "github.com/stretchr/testify/require"
+)
+
+func TestAccResourceScript(t *testing.T) {
+ scriptID := sdkacctest.RandStringFromCharSet(10, sdkacctest.CharSetAlphaNum)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.PreCheck(t) },
+ CheckDestroy: checkScriptDestroy,
+ ProtoV6ProviderFactories: acctest.Providers,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccScriptCreate(scriptID),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_script.test", "script_id", scriptID),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_script.test", "lang", "painless"),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_script.test", "source", "Math.log(_score * 2) + params['my_modifier']"),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_script.test", "context", "score"),
+ ),
+ },
+ {
+ Config: testAccScriptUpdate(scriptID),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_script.test", "script_id", scriptID),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_script.test", "lang", "painless"),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_script.test", "source", "Math.log(_score * 4) + params['changed_modifier']"),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_script.test", "params", `{"changed_modifier":2}`),
+ ),
+ },
+ {
+ // Ensure the provider doesn't panic if the script has been deleted outside of the Terraform flow
+ PreConfig: func() {
+ client, err := clients.NewAcceptanceTestingClient()
+ require.NoError(t, err)
+
+ esClient, err := client.GetESClient()
+ require.NoError(t, err)
+
+ _, err = esClient.DeleteScript(scriptID)
+ require.NoError(t, err)
+ },
+ Config: testAccScriptUpdate(scriptID),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_script.test", "script_id", scriptID),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_script.test", "lang", "painless"),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_script.test", "source", "Math.log(_score * 4) + params['changed_modifier']"),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_script.test", "params", `{"changed_modifier":2}`),
+ ),
+ },
+ },
+ })
+}
+
+func TestAccResourceScriptSearchTemplate(t *testing.T) {
+ scriptID := sdkacctest.RandStringFromCharSet(10, sdkacctest.CharSetAlphaNum)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.PreCheck(t) },
+ CheckDestroy: checkScriptDestroy,
+ ProtoV6ProviderFactories: acctest.Providers,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccSearchTemplateCreate(scriptID),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_script.search_template_test", "script_id", scriptID),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_script.search_template_test", "lang", "mustache"),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_script.search_template_test", "source", `{"from":"{{from}}","query":{"match":{"message":"{{query_string}}"}},"size":"{{size}}"}`),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_script.search_template_test", "params", `{"query_string":"My query string"}`),
+ ),
+ },
+ },
+ })
+}
+
+func TestAccResourceScriptFromSDK(t *testing.T) {
+ scriptID := sdkacctest.RandStringFromCharSet(10, sdkacctest.CharSetAlphaNum)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.PreCheck(t) },
+ Steps: []resource.TestStep{
+ {
+ // Create the script with the last provider version where the script resource was built on the SDK
+ ExternalProviders: map[string]resource.ExternalProvider{
+ "elasticstack": {
+ Source: "elastic/elasticstack",
+ VersionConstraint: "0.11.17",
+ },
+ },
+ Config: testAccScriptCreateFromSDK(scriptID),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_script.test", "script_id", scriptID),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_script.test", "lang", "painless"),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_script.test", "source", "Math.log(_score * 2) + params['my_modifier']"),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_script.test", "context", "score"),
+ ),
+ },
+ {
+ ProtoV6ProviderFactories: acctest.Providers,
+ Config: testAccScriptCreateFromSDK(scriptID),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_script.test", "script_id", scriptID),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_script.test", "lang", "painless"),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_script.test", "source", "Math.log(_score * 2) + params['my_modifier']"),
+ resource.TestCheckResourceAttr("elasticstack_elasticsearch_script.test", "context", "score"),
+ ),
+ },
+ },
+ })
+}
+
+func testAccScriptCreate(id string) string {
+ return fmt.Sprintf(`
+provider "elasticstack" {
+ elasticsearch {}
+}
+
+resource "elasticstack_elasticsearch_script" "test" {
+ script_id = "%s"
+ lang = "painless"
+ source = "Math.log(_score * 2) + params['my_modifier']"
+ context = "score"
+}
+ `, id)
+}
+
+func testAccScriptUpdate(id string) string {
+ return fmt.Sprintf(`
+provider "elasticstack" {
+ elasticsearch {}
+}
+
+resource "elasticstack_elasticsearch_script" "test" {
+ script_id = "%s"
+ lang = "painless"
+ source = "Math.log(_score * 4) + params['changed_modifier']"
+ params = jsonencode({
+ changed_modifier = 2
+ })
+}
+ `, id)
+}
+
+func testAccSearchTemplateCreate(id string) string {
+ return fmt.Sprintf(`
+provider "elasticstack" {
+ elasticsearch {}
+}
+
+resource "elasticstack_elasticsearch_script" "search_template_test" {
+ script_id = "%s"
+ lang = "mustache"
+ source = jsonencode({
+ query = {
+ match = {
+ message = "{{query_string}}"
+ }
+ }
+ from = "{{from}}"
+ size = "{{size}}"
+ })
+ params = jsonencode({
+ query_string = "My query string"
+ })
+}
+ `, id)
+}
+
+func testAccScriptCreateFromSDK(id string) string {
+ return fmt.Sprintf(`
+provider "elasticstack" {
+ elasticsearch {}
+}
+
+resource "elasticstack_elasticsearch_script" "test" {
+ script_id = "%s"
+ lang = "painless"
+ source = "Math.log(_score * 2) + params['my_modifier']"
+ context = "score"
+}
+ `, id)
+}
+
+func checkScriptDestroy(s *terraform.State) error {
+ client, err := clients.NewAcceptanceTestingClient()
+ if err != nil {
+ return err
+ }
+
+ for _, rs := range s.RootModule().Resources {
+ if rs.Type != "elasticstack_elasticsearch_script" {
+ continue
+ }
+
+ compId, _ := clients.CompositeIdFromStr(rs.Primary.ID)
+ esClient, err := client.GetESClient()
+ if err != nil {
+ return err
+ }
+ res, err := esClient.GetScript(compId.ResourceId)
+ if err != nil {
+ return err
+ }
+
+ if res.StatusCode != 404 {
+ return fmt.Errorf("script (%s) still exists", compId.ResourceId)
+ }
+ }
+ return nil
+}
\ No newline at end of file
diff --git a/internal/elasticsearch/cluster/script/create.go b/internal/elasticsearch/cluster/script/create.go
new file mode 100644
index 000000000..25f9df1ea
--- /dev/null
+++ b/internal/elasticsearch/cluster/script/create.go
@@ -0,0 +1,15 @@
+package script
+
+import (
+ "context"
+
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+)
+
+func (r *scriptResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ diags := r.update(ctx, req.Plan, &resp.State)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+}
\ No newline at end of file
diff --git a/internal/elasticsearch/cluster/script/delete.go b/internal/elasticsearch/cluster/script/delete.go
new file mode 100644
index 000000000..1f86a3555
--- /dev/null
+++ b/internal/elasticsearch/cluster/script/delete.go
@@ -0,0 +1,33 @@
+package script
+
+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/utils"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+)
+
+func (r *scriptResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var data ScriptData
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ compId, diags := clients.CompositeIdFromStrFw(data.Id.ValueString())
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ client, diags := clients.MaybeNewApiClientFromFrameworkResource(ctx, data.ElasticsearchConnection, r.client)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ sdkDiags := elasticsearch.DeleteScript(ctx, client, compId.ResourceId)
+ resp.Diagnostics.Append(utils.FrameworkDiagsFromSDK(sdkDiags)...)
+}
\ No newline at end of file
diff --git a/internal/elasticsearch/cluster/script/models.go b/internal/elasticsearch/cluster/script/models.go
new file mode 100644
index 000000000..d9f5fa79c
--- /dev/null
+++ b/internal/elasticsearch/cluster/script/models.go
@@ -0,0 +1,15 @@
+package script
+
+import (
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+type ScriptData struct {
+ Id types.String `tfsdk:"id"`
+ ElasticsearchConnection types.List `tfsdk:"elasticsearch_connection"`
+ ScriptId types.String `tfsdk:"script_id"`
+ Lang types.String `tfsdk:"lang"`
+ Source types.String `tfsdk:"source"`
+ Params types.String `tfsdk:"params"`
+ Context types.String `tfsdk:"context"`
+}
\ No newline at end of file
diff --git a/internal/elasticsearch/cluster/script/read.go b/internal/elasticsearch/cluster/script/read.go
new file mode 100644
index 000000000..3865d4c2c
--- /dev/null
+++ b/internal/elasticsearch/cluster/script/read.go
@@ -0,0 +1,70 @@
+package script
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "github.com/elastic/terraform-provider-elasticstack/internal/clients"
+ "github.com/elastic/terraform-provider-elasticstack/internal/clients/elasticsearch"
+ "github.com/elastic/terraform-provider-elasticstack/internal/utils"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+)
+
+func (r *scriptResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data ScriptData
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ compId, diags := clients.CompositeIdFromStrFw(data.Id.ValueString())
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ scriptId := compId.ResourceId
+
+ client, diags := clients.MaybeNewApiClientFromFrameworkResource(ctx, data.ElasticsearchConnection, r.client)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ script, sdkDiags := elasticsearch.GetScript(ctx, client, scriptId)
+ resp.Diagnostics.Append(utils.FrameworkDiagsFromSDK(sdkDiags)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if script == nil {
+ tflog.Warn(ctx, fmt.Sprintf(`Script "%s" not found, removing from state`, compId.ResourceId))
+ resp.State.RemoveResource(ctx)
+ return
+ }
+
+ data.ScriptId = types.StringValue(scriptId)
+ data.Lang = types.StringValue(script.Language)
+ data.Source = types.StringValue(script.Source)
+
+ // Handle params if returned by the API
+ if script.Params != nil && len(script.Params) > 0 {
+ paramsBytes, err := json.Marshal(script.Params)
+ if err != nil {
+ resp.Diagnostics.AddError("Error marshaling script params", err.Error())
+ return
+ }
+ data.Params = types.StringValue(string(paramsBytes))
+ } else if !data.Params.IsNull() {
+ // If params were set but API doesn't return them, preserve from state
+ // This maintains backwards compatibility
+ }
+
+ // Note: context is not returned by the Elasticsearch API (json:"-" in model)
+ // It's only used during script creation, so we preserve it from state
+ // This is consistent with the SDKv2 implementation
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
\ No newline at end of file
diff --git a/internal/elasticsearch/cluster/script/resource.go b/internal/elasticsearch/cluster/script/resource.go
new file mode 100644
index 000000000..bf4e10afb
--- /dev/null
+++ b/internal/elasticsearch/cluster/script/resource.go
@@ -0,0 +1,26 @@
+package script
+
+import (
+ "context"
+
+ "github.com/elastic/terraform-provider-elasticstack/internal/clients"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+)
+
+func NewScriptResource() resource.Resource {
+ return &scriptResource{}
+}
+
+type scriptResource struct {
+ client *clients.ApiClient
+}
+
+func (r *scriptResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_elasticsearch_script"
+}
+
+func (r *scriptResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ client, diags := clients.ConvertProviderData(req.ProviderData)
+ resp.Diagnostics.Append(diags...)
+ r.client = client
+}
\ No newline at end of file
diff --git a/internal/elasticsearch/cluster/script/schema.go b/internal/elasticsearch/cluster/script/schema.go
new file mode 100644
index 000000000..24c6349ee
--- /dev/null
+++ b/internal/elasticsearch/cluster/script/schema.go
@@ -0,0 +1,63 @@
+package script
+
+import (
+ "context"
+
+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+
+ providerschema "github.com/elastic/terraform-provider-elasticstack/internal/schema"
+ "github.com/elastic/terraform-provider-elasticstack/internal/utils/validators"
+)
+
+func (r *scriptResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = GetSchema()
+}
+
+func GetSchema() schema.Schema {
+ return schema.Schema{
+ MarkdownDescription: "Creates or updates a stored script or search template. See, https://www.elastic.co/guide/en/elasticsearch/reference/current/create-stored-script-api.html",
+ Blocks: map[string]schema.Block{
+ "elasticsearch_connection": providerschema.GetEsFWConnectionBlock("elasticsearch_connection", false),
+ },
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ MarkdownDescription: "Internal identifier of the resource",
+ Computed: true,
+ },
+ "script_id": schema.StringAttribute{
+ MarkdownDescription: "Identifier for the stored script. Must be unique within the cluster.",
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ "lang": schema.StringAttribute{
+ MarkdownDescription: "Script language. For search templates, use `mustache`.",
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf("painless", "expression", "mustache", "java"),
+ },
+ },
+ "source": schema.StringAttribute{
+ MarkdownDescription: "For scripts, a string containing the script. For search templates, an object containing the search template.",
+ Required: true,
+ },
+ "params": schema.StringAttribute{
+ MarkdownDescription: "Parameters for the script or search template.",
+ Optional: true,
+ Validators: []validator.String{
+ validators.StringIsJSON{},
+ },
+ },
+ "context": schema.StringAttribute{
+ MarkdownDescription: "Context in which the script or search template should run.",
+ Optional: true,
+ },
+ },
+ }
+}
\ No newline at end of file
diff --git a/internal/elasticsearch/cluster/script/update.go b/internal/elasticsearch/cluster/script/update.go
new file mode 100644
index 000000000..037175a26
--- /dev/null
+++ b/internal/elasticsearch/cluster/script/update.go
@@ -0,0 +1,75 @@
+package script
+
+import (
+ "context"
+ "encoding/json"
+
+ "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-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/tfsdk"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+func (r *scriptResource) update(ctx context.Context, plan tfsdk.Plan, state *tfsdk.State) diag.Diagnostics {
+ var data ScriptData
+ var diags diag.Diagnostics
+ diags.Append(plan.Get(ctx, &data)...)
+ if diags.HasError() {
+ return diags
+ }
+
+ scriptId := data.ScriptId.ValueString()
+ id, sdkDiags := r.client.ID(ctx, scriptId)
+ diags.Append(utils.FrameworkDiagsFromSDK(sdkDiags)...)
+ if diags.HasError() {
+ return diags
+ }
+
+ client, diags := clients.MaybeNewApiClientFromFrameworkResource(ctx, data.ElasticsearchConnection, r.client)
+ diags.Append(diags...)
+ if diags.HasError() {
+ return diags
+ }
+
+ script := models.Script{
+ ID: scriptId,
+ Language: data.Lang.ValueString(),
+ Source: data.Source.ValueString(),
+ }
+
+ if utils.IsKnown(data.Params) && !data.Params.IsNull() {
+ var params map[string]interface{}
+ err := json.Unmarshal([]byte(data.Params.ValueString()), ¶ms)
+ if err != nil {
+ diags.AddError("Error unmarshaling script params", err.Error())
+ return diags
+ }
+ script.Params = params
+ }
+
+ if utils.IsKnown(data.Context) && !data.Context.IsNull() {
+ script.Context = data.Context.ValueString()
+ }
+
+ sdkDiags = elasticsearch.PutScript(ctx, client, &script)
+ diags.Append(utils.FrameworkDiagsFromSDK(sdkDiags)...)
+ if diags.HasError() {
+ return diags
+ }
+
+ data.Id = types.StringValue(id.String())
+ diags.Append(state.Set(ctx, &data)...)
+ return diags
+}
+
+func (r *scriptResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ diags := r.update(ctx, req.Plan, &resp.State)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+}
\ No newline at end of file
diff --git a/internal/utils/validators/string_is_json.go b/internal/utils/validators/string_is_json.go
new file mode 100644
index 000000000..67dc925e4
--- /dev/null
+++ b/internal/utils/validators/string_is_json.go
@@ -0,0 +1,35 @@
+package validators
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+)
+
+type StringIsJSON struct{}
+
+func (s StringIsJSON) Description(_ context.Context) string {
+ return "Ensure that the attribute contains valid JSON"
+}
+
+func (s StringIsJSON) MarkdownDescription(ctx context.Context) string {
+ return s.Description(ctx)
+}
+
+func (s StringIsJSON) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) {
+ if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
+ return
+ }
+
+ var jsonData interface{}
+ if err := json.Unmarshal([]byte(req.ConfigValue.ValueString()), &jsonData); err != nil {
+ resp.Diagnostics.AddAttributeError(
+ req.Path,
+ "expected value to be valid JSON",
+ fmt.Sprintf("The provided value is not valid JSON: %s", err),
+ )
+ return
+ }
+}
\ No newline at end of file
diff --git a/provider/plugin_framework.go b/provider/plugin_framework.go
index bba736241..d2e4cbd8d 100644
--- a/provider/plugin_framework.go
+++ b/provider/plugin_framework.go
@@ -10,6 +10,7 @@ import (
"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"
+ "github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/cluster/script"
"github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/security/api_key"
"github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/security/role_mapping"
"github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/security/system_user"
@@ -113,6 +114,7 @@ func (p *Provider) Resources(ctx context.Context) []func() resource.Resource {
output.NewResource,
server_host.NewResource,
system_user.NewSystemUserResource,
+ script.NewScriptResource,
maintenance_window.NewResource,
enrich.NewEnrichPolicyResource,
role_mapping.NewRoleMappingResource,
diff --git a/provider/provider.go b/provider/provider.go
index 3e78eee50..ef92197f6 100644
--- a/provider/provider.go
+++ b/provider/provider.go
@@ -92,7 +92,7 @@ func New(version string) *schema.Provider {
"elasticstack_elasticsearch_security_user": security.ResourceUser(),
"elasticstack_elasticsearch_snapshot_lifecycle": cluster.ResourceSlm(),
"elasticstack_elasticsearch_snapshot_repository": cluster.ResourceSnapshotRepository(),
- "elasticstack_elasticsearch_script": cluster.ResourceScript(),
+ // "elasticstack_elasticsearch_script": cluster.ResourceScript(), // Migrated to Plugin Framework
"elasticstack_elasticsearch_transform": transform.ResourceTransform(),
"elasticstack_elasticsearch_watch": watcher.ResourceWatch(),
From 6a362c73e8f51407ce321eb8e23cc9dd1daa780c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 11 Sep 2025 10:01:42 +0000
Subject: [PATCH 03/11] Complete script resource migration with lint fixes and
changelog update
Co-authored-by: tobio <444668+tobio@users.noreply.github.com>
---
CHANGELOG.md | 1 +
internal/elasticsearch/cluster/script/acc_test.go | 2 +-
internal/elasticsearch/cluster/script/create.go | 2 +-
internal/elasticsearch/cluster/script/delete.go | 2 +-
internal/elasticsearch/cluster/script/models.go | 2 +-
internal/elasticsearch/cluster/script/read.go | 9 ++++-----
internal/elasticsearch/cluster/script/resource.go | 2 +-
internal/elasticsearch/cluster/script/schema.go | 2 +-
internal/elasticsearch/cluster/script/update.go | 2 +-
internal/utils/validators/string_is_json.go | 2 +-
provider/plugin_framework.go | 2 +-
provider/provider.go | 4 ++--
12 files changed, 16 insertions(+), 16 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9bfc460cd..12d87e92e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@
- Migrate `elasticstack_kibana_action_connector` to the Terraform plugin framework ([#1269](https://github.com/elastic/terraform-provider-elasticstack/pull/1269))
- Migrate `elasticstack_elasticsearch_security_role_mapping` resource and data source to Terraform Plugin Framework ([#1279](https://github.com/elastic/terraform-provider-elasticstack/pull/1279))
- Add support for `inactivity_timeout` in `elasticstack_fleet_agent_policy` ([#641](https://github.com/elastic/terraform-provider-elasticstack/issues/641))
+- Migrate `elasticstack_elasticsearch_script` resource to Terraform Plugin Framework ([#1297](https://github.com/elastic/terraform-provider-elasticstack/pull/1297))
## [0.11.17] - 2025-07-21
diff --git a/internal/elasticsearch/cluster/script/acc_test.go b/internal/elasticsearch/cluster/script/acc_test.go
index a41990867..ecd8abdb2 100644
--- a/internal/elasticsearch/cluster/script/acc_test.go
+++ b/internal/elasticsearch/cluster/script/acc_test.go
@@ -217,4 +217,4 @@ func checkScriptDestroy(s *terraform.State) error {
}
}
return nil
-}
\ No newline at end of file
+}
diff --git a/internal/elasticsearch/cluster/script/create.go b/internal/elasticsearch/cluster/script/create.go
index 25f9df1ea..5ab88f551 100644
--- a/internal/elasticsearch/cluster/script/create.go
+++ b/internal/elasticsearch/cluster/script/create.go
@@ -12,4 +12,4 @@ func (r *scriptResource) Create(ctx context.Context, req resource.CreateRequest,
if resp.Diagnostics.HasError() {
return
}
-}
\ No newline at end of file
+}
diff --git a/internal/elasticsearch/cluster/script/delete.go b/internal/elasticsearch/cluster/script/delete.go
index 1f86a3555..43c40788d 100644
--- a/internal/elasticsearch/cluster/script/delete.go
+++ b/internal/elasticsearch/cluster/script/delete.go
@@ -30,4 +30,4 @@ func (r *scriptResource) Delete(ctx context.Context, req resource.DeleteRequest,
sdkDiags := elasticsearch.DeleteScript(ctx, client, compId.ResourceId)
resp.Diagnostics.Append(utils.FrameworkDiagsFromSDK(sdkDiags)...)
-}
\ No newline at end of file
+}
diff --git a/internal/elasticsearch/cluster/script/models.go b/internal/elasticsearch/cluster/script/models.go
index d9f5fa79c..ad30c879b 100644
--- a/internal/elasticsearch/cluster/script/models.go
+++ b/internal/elasticsearch/cluster/script/models.go
@@ -12,4 +12,4 @@ type ScriptData struct {
Source types.String `tfsdk:"source"`
Params types.String `tfsdk:"params"`
Context types.String `tfsdk:"context"`
-}
\ No newline at end of file
+}
diff --git a/internal/elasticsearch/cluster/script/read.go b/internal/elasticsearch/cluster/script/read.go
index 3865d4c2c..95782f26a 100644
--- a/internal/elasticsearch/cluster/script/read.go
+++ b/internal/elasticsearch/cluster/script/read.go
@@ -50,21 +50,20 @@ func (r *scriptResource) Read(ctx context.Context, req resource.ReadRequest, res
data.Source = types.StringValue(script.Source)
// Handle params if returned by the API
- if script.Params != nil && len(script.Params) > 0 {
+ if len(script.Params) > 0 {
paramsBytes, err := json.Marshal(script.Params)
if err != nil {
resp.Diagnostics.AddError("Error marshaling script params", err.Error())
return
}
data.Params = types.StringValue(string(paramsBytes))
- } else if !data.Params.IsNull() {
- // If params were set but API doesn't return them, preserve from state
- // This maintains backwards compatibility
}
+ // Note: If params were set but API doesn't return them, they are preserved from state
+ // This maintains backwards compatibility
// Note: context is not returned by the Elasticsearch API (json:"-" in model)
// It's only used during script creation, so we preserve it from state
// This is consistent with the SDKv2 implementation
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
-}
\ No newline at end of file
+}
diff --git a/internal/elasticsearch/cluster/script/resource.go b/internal/elasticsearch/cluster/script/resource.go
index bf4e10afb..34130fe98 100644
--- a/internal/elasticsearch/cluster/script/resource.go
+++ b/internal/elasticsearch/cluster/script/resource.go
@@ -23,4 +23,4 @@ func (r *scriptResource) Configure(_ context.Context, req resource.ConfigureRequ
client, diags := clients.ConvertProviderData(req.ProviderData)
resp.Diagnostics.Append(diags...)
r.client = client
-}
\ No newline at end of file
+}
diff --git a/internal/elasticsearch/cluster/script/schema.go b/internal/elasticsearch/cluster/script/schema.go
index 24c6349ee..9f9791a30 100644
--- a/internal/elasticsearch/cluster/script/schema.go
+++ b/internal/elasticsearch/cluster/script/schema.go
@@ -60,4 +60,4 @@ func GetSchema() schema.Schema {
},
},
}
-}
\ No newline at end of file
+}
diff --git a/internal/elasticsearch/cluster/script/update.go b/internal/elasticsearch/cluster/script/update.go
index 037175a26..505646bf7 100644
--- a/internal/elasticsearch/cluster/script/update.go
+++ b/internal/elasticsearch/cluster/script/update.go
@@ -72,4 +72,4 @@ func (r *scriptResource) Update(ctx context.Context, req resource.UpdateRequest,
if resp.Diagnostics.HasError() {
return
}
-}
\ No newline at end of file
+}
diff --git a/internal/utils/validators/string_is_json.go b/internal/utils/validators/string_is_json.go
index 67dc925e4..952bd95ec 100644
--- a/internal/utils/validators/string_is_json.go
+++ b/internal/utils/validators/string_is_json.go
@@ -32,4 +32,4 @@ func (s StringIsJSON) ValidateString(_ context.Context, req validator.StringRequ
)
return
}
-}
\ No newline at end of file
+}
diff --git a/provider/plugin_framework.go b/provider/plugin_framework.go
index d2e4cbd8d..8487b6bd8 100644
--- a/provider/plugin_framework.go
+++ b/provider/plugin_framework.go
@@ -6,11 +6,11 @@ import (
"github.com/elastic/terraform-provider-elasticstack/internal/apm/agent_configuration"
"github.com/elastic/terraform-provider-elasticstack/internal/clients"
"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/data_stream_lifecycle"
"github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/index/index"
"github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/index/indices"
- "github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/cluster/script"
"github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/security/api_key"
"github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/security/role_mapping"
"github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/security/system_user"
diff --git a/provider/provider.go b/provider/provider.go
index ef92197f6..ce13ffae4 100644
--- a/provider/provider.go
+++ b/provider/provider.go
@@ -93,8 +93,8 @@ func New(version string) *schema.Provider {
"elasticstack_elasticsearch_snapshot_lifecycle": cluster.ResourceSlm(),
"elasticstack_elasticsearch_snapshot_repository": cluster.ResourceSnapshotRepository(),
// "elasticstack_elasticsearch_script": cluster.ResourceScript(), // Migrated to Plugin Framework
- "elasticstack_elasticsearch_transform": transform.ResourceTransform(),
- "elasticstack_elasticsearch_watch": watcher.ResourceWatch(),
+ "elasticstack_elasticsearch_transform": transform.ResourceTransform(),
+ "elasticstack_elasticsearch_watch": watcher.ResourceWatch(),
"elasticstack_kibana_alerting_rule": kibana.ResourceAlertingRule(),
"elasticstack_kibana_space": kibana.ResourceSpace(),
From 555dccbcbba9e9c2c04bec8a7d9e7b5967b77ccd Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 11 Sep 2025 10:03:13 +0000
Subject: [PATCH 04/11] Add comprehensive test for StringIsJSON validator
Co-authored-by: tobio <444668+tobio@users.noreply.github.com>
---
.../utils/validators/string_is_json_test.go | 77 +++++++++++++++++++
1 file changed, 77 insertions(+)
create mode 100644 internal/utils/validators/string_is_json_test.go
diff --git a/internal/utils/validators/string_is_json_test.go b/internal/utils/validators/string_is_json_test.go
new file mode 100644
index 000000000..271423563
--- /dev/null
+++ b/internal/utils/validators/string_is_json_test.go
@@ -0,0 +1,77 @@
+package validators
+
+import (
+ "context"
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+func TestStringIsJSON_ValidateString(t *testing.T) {
+ t.Parallel()
+
+ testCases := map[string]struct {
+ input types.String
+ expectError bool
+ }{
+ "valid json object": {
+ input: types.StringValue(`{"key": "value"}`),
+ expectError: false,
+ },
+ "valid json array": {
+ input: types.StringValue(`[1, 2, 3]`),
+ expectError: false,
+ },
+ "valid json string": {
+ input: types.StringValue(`"string"`),
+ expectError: false,
+ },
+ "valid json number": {
+ input: types.StringValue(`42`),
+ expectError: false,
+ },
+ "invalid json": {
+ input: types.StringValue(`{invalid json}`),
+ expectError: true,
+ },
+ "empty string": {
+ input: types.StringValue(""),
+ expectError: true,
+ },
+ "null value": {
+ input: types.StringNull(),
+ expectError: false,
+ },
+ "unknown value": {
+ input: types.StringUnknown(),
+ expectError: false,
+ },
+ }
+
+ for name, testCase := range testCases {
+ name, testCase := name, testCase
+
+ t.Run(name, func(t *testing.T) {
+ t.Parallel()
+
+ request := validator.StringRequest{
+ Path: path.Root("test"),
+ PathExpression: path.MatchRoot("test"),
+ ConfigValue: testCase.input,
+ }
+ response := &validator.StringResponse{}
+
+ StringIsJSON{}.ValidateString(context.Background(), request, response)
+
+ if !response.Diagnostics.HasError() && testCase.expectError {
+ t.Fatal("expected error, got no error")
+ }
+
+ if response.Diagnostics.HasError() && !testCase.expectError {
+ t.Fatalf("expected no error, got: %s", response.Diagnostics)
+ }
+ })
+ }
+}
\ No newline at end of file
From ae668024875c9622d9c321c017cddd2c3ce01f1e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 11 Sep 2025 10:39:17 +0000
Subject: [PATCH 05/11] Address review feedback: migrate to
jsontypes.NormalizedType and framework diagnostics
Co-authored-by: tobio <444668+tobio@users.noreply.github.com>
---
internal/clients/elasticsearch/cluster.go | 18 ++++
.../elasticsearch/cluster/script/delete.go | 4 +-
.../elasticsearch/cluster/script/models.go | 15 ++--
internal/elasticsearch/cluster/script/read.go | 40 ++++-----
.../elasticsearch/cluster/script/schema.go | 6 +-
.../elasticsearch/cluster/script/update.go | 86 ++++++++++++++++---
internal/utils/validators/string_is_json.go | 35 --------
.../utils/validators/string_is_json_test.go | 77 -----------------
provider/provider.go | 5 +-
9 files changed, 120 insertions(+), 166 deletions(-)
delete mode 100644 internal/utils/validators/string_is_json.go
delete mode 100644 internal/utils/validators/string_is_json_test.go
diff --git a/internal/clients/elasticsearch/cluster.go b/internal/clients/elasticsearch/cluster.go
index fb075bed0..7ed715fe7 100644
--- a/internal/clients/elasticsearch/cluster.go
+++ b/internal/clients/elasticsearch/cluster.go
@@ -10,6 +10,7 @@ import (
"github.com/elastic/terraform-provider-elasticstack/internal/clients"
"github.com/elastic/terraform-provider-elasticstack/internal/models"
"github.com/elastic/terraform-provider-elasticstack/internal/utils"
+ fwdiag "github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
)
@@ -300,3 +301,20 @@ func DeleteScript(ctx context.Context, apiClient *clients.ApiClient, id string)
}
return nil
}
+
+// Framework versions that return framework diagnostics
+
+func PutScriptFw(ctx context.Context, apiClient *clients.ApiClient, script *models.Script) fwdiag.Diagnostics {
+ sdkDiags := PutScript(ctx, apiClient, script)
+ return utils.FrameworkDiagsFromSDK(sdkDiags)
+}
+
+func GetScriptFw(ctx context.Context, apiClient *clients.ApiClient, id string) (*models.Script, fwdiag.Diagnostics) {
+ script, sdkDiags := GetScript(ctx, apiClient, id)
+ return script, utils.FrameworkDiagsFromSDK(sdkDiags)
+}
+
+func DeleteScriptFw(ctx context.Context, apiClient *clients.ApiClient, id string) fwdiag.Diagnostics {
+ sdkDiags := DeleteScript(ctx, apiClient, id)
+ return utils.FrameworkDiagsFromSDK(sdkDiags)
+}
diff --git a/internal/elasticsearch/cluster/script/delete.go b/internal/elasticsearch/cluster/script/delete.go
index 43c40788d..be4dd3d6d 100644
--- a/internal/elasticsearch/cluster/script/delete.go
+++ b/internal/elasticsearch/cluster/script/delete.go
@@ -5,7 +5,6 @@ import (
"github.com/elastic/terraform-provider-elasticstack/internal/clients"
"github.com/elastic/terraform-provider-elasticstack/internal/clients/elasticsearch"
- "github.com/elastic/terraform-provider-elasticstack/internal/utils"
"github.com/hashicorp/terraform-plugin-framework/resource"
)
@@ -28,6 +27,5 @@ func (r *scriptResource) Delete(ctx context.Context, req resource.DeleteRequest,
return
}
- sdkDiags := elasticsearch.DeleteScript(ctx, client, compId.ResourceId)
- resp.Diagnostics.Append(utils.FrameworkDiagsFromSDK(sdkDiags)...)
+ resp.Diagnostics.Append(elasticsearch.DeleteScriptFw(ctx, client, compId.ResourceId)...)
}
diff --git a/internal/elasticsearch/cluster/script/models.go b/internal/elasticsearch/cluster/script/models.go
index ad30c879b..c5e0a26d4 100644
--- a/internal/elasticsearch/cluster/script/models.go
+++ b/internal/elasticsearch/cluster/script/models.go
@@ -1,15 +1,16 @@
package script
import (
+ "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
"github.com/hashicorp/terraform-plugin-framework/types"
)
type ScriptData struct {
- Id types.String `tfsdk:"id"`
- ElasticsearchConnection types.List `tfsdk:"elasticsearch_connection"`
- ScriptId types.String `tfsdk:"script_id"`
- Lang types.String `tfsdk:"lang"`
- Source types.String `tfsdk:"source"`
- Params types.String `tfsdk:"params"`
- Context types.String `tfsdk:"context"`
+ Id types.String `tfsdk:"id"`
+ ElasticsearchConnection types.List `tfsdk:"elasticsearch_connection"`
+ ScriptId types.String `tfsdk:"script_id"`
+ Lang types.String `tfsdk:"lang"`
+ Source types.String `tfsdk:"source"`
+ Params jsontypes.Normalized `tfsdk:"params"`
+ Context types.String `tfsdk:"context"`
}
diff --git a/internal/elasticsearch/cluster/script/read.go b/internal/elasticsearch/cluster/script/read.go
index 95782f26a..cc0a89deb 100644
--- a/internal/elasticsearch/cluster/script/read.go
+++ b/internal/elasticsearch/cluster/script/read.go
@@ -2,14 +2,10 @@ package script
import (
"context"
- "encoding/json"
"fmt"
"github.com/elastic/terraform-provider-elasticstack/internal/clients"
- "github.com/elastic/terraform-provider-elasticstack/internal/clients/elasticsearch"
- "github.com/elastic/terraform-provider-elasticstack/internal/utils"
"github.com/hashicorp/terraform-plugin-framework/resource"
- "github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
@@ -33,37 +29,31 @@ func (r *scriptResource) Read(ctx context.Context, req resource.ReadRequest, res
return
}
- script, sdkDiags := elasticsearch.GetScript(ctx, client, scriptId)
- resp.Diagnostics.Append(utils.FrameworkDiagsFromSDK(sdkDiags)...)
+ // Use the helper read function
+ readData, readDiags := r.read(ctx, scriptId, client)
+ resp.Diagnostics.Append(readDiags...)
if resp.Diagnostics.HasError() {
return
}
- if script == nil {
+ // Check if script was found
+ if readData.ScriptId.IsNull() {
tflog.Warn(ctx, fmt.Sprintf(`Script "%s" not found, removing from state`, compId.ResourceId))
resp.State.RemoveResource(ctx)
return
}
- data.ScriptId = types.StringValue(scriptId)
- data.Lang = types.StringValue(script.Language)
- data.Source = types.StringValue(script.Source)
+ // Preserve connection and ID from original state
+ readData.ElasticsearchConnection = data.ElasticsearchConnection
+ readData.Id = data.Id
- // Handle params if returned by the API
- if len(script.Params) > 0 {
- paramsBytes, err := json.Marshal(script.Params)
- if err != nil {
- resp.Diagnostics.AddError("Error marshaling script params", err.Error())
- return
- }
- data.Params = types.StringValue(string(paramsBytes))
- }
- // Note: If params were set but API doesn't return them, they are preserved from state
- // This maintains backwards compatibility
+ // Preserve context from state as it's not returned by the API
+ readData.Context = data.Context
- // Note: context is not returned by the Elasticsearch API (json:"-" in model)
- // It's only used during script creation, so we preserve it from state
- // This is consistent with the SDKv2 implementation
+ // Preserve params from state if API didn't return them
+ if readData.Params.IsNull() && !data.Params.IsNull() {
+ readData.Params = data.Params
+ }
- resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+ resp.Diagnostics.Append(resp.State.Set(ctx, &readData)...)
}
diff --git a/internal/elasticsearch/cluster/script/schema.go b/internal/elasticsearch/cluster/script/schema.go
index 9f9791a30..9b57a3e17 100644
--- a/internal/elasticsearch/cluster/script/schema.go
+++ b/internal/elasticsearch/cluster/script/schema.go
@@ -3,6 +3,7 @@ package script
import (
"context"
+ "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
@@ -11,7 +12,6 @@ import (
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
providerschema "github.com/elastic/terraform-provider-elasticstack/internal/schema"
- "github.com/elastic/terraform-provider-elasticstack/internal/utils/validators"
)
func (r *scriptResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
@@ -50,9 +50,7 @@ func GetSchema() schema.Schema {
"params": schema.StringAttribute{
MarkdownDescription: "Parameters for the script or search template.",
Optional: true,
- Validators: []validator.String{
- validators.StringIsJSON{},
- },
+ CustomType: jsontypes.NormalizedType{},
},
"context": schema.StringAttribute{
MarkdownDescription: "Context in which the script or search template should run.",
diff --git a/internal/elasticsearch/cluster/script/update.go b/internal/elasticsearch/cluster/script/update.go
index 505646bf7..26f92c81c 100644
--- a/internal/elasticsearch/cluster/script/update.go
+++ b/internal/elasticsearch/cluster/script/update.go
@@ -8,6 +8,7 @@ import (
"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-framework-jsontypes/jsontypes"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
@@ -41,31 +42,92 @@ func (r *scriptResource) update(ctx context.Context, plan tfsdk.Plan, state *tfs
Source: data.Source.ValueString(),
}
- if utils.IsKnown(data.Params) && !data.Params.IsNull() {
- var params map[string]interface{}
- err := json.Unmarshal([]byte(data.Params.ValueString()), ¶ms)
- if err != nil {
- diags.AddError("Error unmarshaling script params", err.Error())
- return diags
+ if utils.IsKnown(data.Params) {
+ paramsStr := data.Params.ValueString()
+ if paramsStr != "" {
+ var params map[string]interface{}
+ err := json.Unmarshal([]byte(paramsStr), ¶ms)
+ if err != nil {
+ diags.AddError("Error unmarshaling script params", err.Error())
+ return diags
+ }
+ script.Params = params
}
- script.Params = params
}
- if utils.IsKnown(data.Context) && !data.Context.IsNull() {
+ if utils.IsKnown(data.Context) {
script.Context = data.Context.ValueString()
}
- sdkDiags = elasticsearch.PutScript(ctx, client, &script)
- diags.Append(utils.FrameworkDiagsFromSDK(sdkDiags)...)
+ diags.Append(elasticsearch.PutScriptFw(ctx, client, &script)...)
if diags.HasError() {
return diags
}
- data.Id = types.StringValue(id.String())
- diags.Append(state.Set(ctx, &data)...)
+ // Read the script back from Elasticsearch to populate state
+ readData, readDiags := r.read(ctx, scriptId, client)
+ diags.Append(readDiags...)
+ if diags.HasError() {
+ return diags
+ }
+
+ // Preserve connection and ID from the original data
+ readData.ElasticsearchConnection = data.ElasticsearchConnection
+ readData.Id = types.StringValue(id.String())
+
+ // Preserve context from the original data as it's not returned by the API
+ readData.Context = data.Context
+
+ // Preserve params from original data if API didn't return them
+ if readData.Params.IsNull() && !data.Params.IsNull() {
+ readData.Params = data.Params
+ }
+
+ diags.Append(state.Set(ctx, &readData)...)
return diags
}
+func (r *scriptResource) read(ctx context.Context, scriptID string, client *clients.ApiClient) (ScriptData, diag.Diagnostics) {
+ var data ScriptData
+ var diags diag.Diagnostics
+
+ script, frameworkDiags := elasticsearch.GetScriptFw(ctx, client, scriptID)
+ diags.Append(frameworkDiags...)
+ if diags.HasError() {
+ return data, diags
+ }
+
+ if script == nil {
+ // Script not found - return empty data with null ScriptId to signal not found
+ data.ScriptId = types.StringNull()
+ return data, diags
+ }
+
+ data.ScriptId = types.StringValue(scriptID)
+ data.Lang = types.StringValue(script.Language)
+ data.Source = types.StringValue(script.Source)
+
+ // Handle params if returned by the API
+ if len(script.Params) > 0 {
+ paramsBytes, err := json.Marshal(script.Params)
+ if err != nil {
+ diags.AddError("Error marshaling script params", err.Error())
+ return data, diags
+ }
+ data.Params = jsontypes.NewNormalizedValue(string(paramsBytes))
+ } else {
+ data.Params = jsontypes.NewNormalizedNull()
+ }
+ // Note: If params were set but API doesn't return them, they are preserved from state
+ // This maintains backwards compatibility
+
+ // Note: context is not returned by the Elasticsearch API (json:"-" in model)
+ // It's only used during script creation, so we preserve it from state
+ // This is consistent with the SDKv2 implementation
+
+ return data, diags
+}
+
func (r *scriptResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
diags := r.update(ctx, req.Plan, &resp.State)
resp.Diagnostics.Append(diags...)
diff --git a/internal/utils/validators/string_is_json.go b/internal/utils/validators/string_is_json.go
deleted file mode 100644
index 952bd95ec..000000000
--- a/internal/utils/validators/string_is_json.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package validators
-
-import (
- "context"
- "encoding/json"
- "fmt"
-
- "github.com/hashicorp/terraform-plugin-framework/schema/validator"
-)
-
-type StringIsJSON struct{}
-
-func (s StringIsJSON) Description(_ context.Context) string {
- return "Ensure that the attribute contains valid JSON"
-}
-
-func (s StringIsJSON) MarkdownDescription(ctx context.Context) string {
- return s.Description(ctx)
-}
-
-func (s StringIsJSON) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) {
- if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
- return
- }
-
- var jsonData interface{}
- if err := json.Unmarshal([]byte(req.ConfigValue.ValueString()), &jsonData); err != nil {
- resp.Diagnostics.AddAttributeError(
- req.Path,
- "expected value to be valid JSON",
- fmt.Sprintf("The provided value is not valid JSON: %s", err),
- )
- return
- }
-}
diff --git a/internal/utils/validators/string_is_json_test.go b/internal/utils/validators/string_is_json_test.go
deleted file mode 100644
index 271423563..000000000
--- a/internal/utils/validators/string_is_json_test.go
+++ /dev/null
@@ -1,77 +0,0 @@
-package validators
-
-import (
- "context"
- "testing"
-
- "github.com/hashicorp/terraform-plugin-framework/path"
- "github.com/hashicorp/terraform-plugin-framework/schema/validator"
- "github.com/hashicorp/terraform-plugin-framework/types"
-)
-
-func TestStringIsJSON_ValidateString(t *testing.T) {
- t.Parallel()
-
- testCases := map[string]struct {
- input types.String
- expectError bool
- }{
- "valid json object": {
- input: types.StringValue(`{"key": "value"}`),
- expectError: false,
- },
- "valid json array": {
- input: types.StringValue(`[1, 2, 3]`),
- expectError: false,
- },
- "valid json string": {
- input: types.StringValue(`"string"`),
- expectError: false,
- },
- "valid json number": {
- input: types.StringValue(`42`),
- expectError: false,
- },
- "invalid json": {
- input: types.StringValue(`{invalid json}`),
- expectError: true,
- },
- "empty string": {
- input: types.StringValue(""),
- expectError: true,
- },
- "null value": {
- input: types.StringNull(),
- expectError: false,
- },
- "unknown value": {
- input: types.StringUnknown(),
- expectError: false,
- },
- }
-
- for name, testCase := range testCases {
- name, testCase := name, testCase
-
- t.Run(name, func(t *testing.T) {
- t.Parallel()
-
- request := validator.StringRequest{
- Path: path.Root("test"),
- PathExpression: path.MatchRoot("test"),
- ConfigValue: testCase.input,
- }
- response := &validator.StringResponse{}
-
- StringIsJSON{}.ValidateString(context.Background(), request, response)
-
- if !response.Diagnostics.HasError() && testCase.expectError {
- t.Fatal("expected error, got no error")
- }
-
- if response.Diagnostics.HasError() && !testCase.expectError {
- t.Fatalf("expected no error, got: %s", response.Diagnostics)
- }
- })
- }
-}
\ No newline at end of file
diff --git a/provider/provider.go b/provider/provider.go
index ce13ffae4..d0a177b56 100644
--- a/provider/provider.go
+++ b/provider/provider.go
@@ -92,9 +92,8 @@ func New(version string) *schema.Provider {
"elasticstack_elasticsearch_security_user": security.ResourceUser(),
"elasticstack_elasticsearch_snapshot_lifecycle": cluster.ResourceSlm(),
"elasticstack_elasticsearch_snapshot_repository": cluster.ResourceSnapshotRepository(),
- // "elasticstack_elasticsearch_script": cluster.ResourceScript(), // Migrated to Plugin Framework
- "elasticstack_elasticsearch_transform": transform.ResourceTransform(),
- "elasticstack_elasticsearch_watch": watcher.ResourceWatch(),
+ "elasticstack_elasticsearch_transform": transform.ResourceTransform(),
+ "elasticstack_elasticsearch_watch": watcher.ResourceWatch(),
"elasticstack_kibana_alerting_rule": kibana.ResourceAlertingRule(),
"elasticstack_kibana_space": kibana.ResourceSpace(),
From 3445f1402236afe2aa5bb8e9f20c39dd34a9575f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 11 Sep 2025 10:40:47 +0000
Subject: [PATCH 06/11] Update documentation for script resource schema changes
Co-authored-by: tobio <444668+tobio@users.noreply.github.com>
---
docs/resources/elasticsearch_script.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/resources/elasticsearch_script.md b/docs/resources/elasticsearch_script.md
index f551360af..144c7703b 100644
--- a/docs/resources/elasticsearch_script.md
+++ b/docs/resources/elasticsearch_script.md
@@ -54,12 +54,12 @@ resource "elasticstack_elasticsearch_script" "my_search_template" {
### Optional
- `context` (String) Context in which the script or search template should run.
-- `elasticsearch_connection` (Block List, Max: 1, Deprecated) Elasticsearch connection configuration block. This property will be removed in a future provider version. Configure the Elasticsearch connection via the provider configuration instead. (see [below for nested schema](#nestedblock--elasticsearch_connection))
+- `elasticsearch_connection` (Block List, Deprecated) Elasticsearch connection configuration block. (see [below for nested schema](#nestedblock--elasticsearch_connection))
- `params` (String) Parameters for the script or search template.
### Read-Only
-- `id` (String) The ID of this resource.
+- `id` (String) Internal identifier of the resource
### Nested Schema for `elasticsearch_connection`
From 446852fe3e59c0deced0d24e48e91b03fd895ccd Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 11 Sep 2025 11:13:02 +0000
Subject: [PATCH 07/11] Address review feedback: update original script
functions to return framework diagnostics and move read function to read.go
Co-authored-by: tobio <444668+tobio@users.noreply.github.com>
---
internal/clients/elasticsearch/cluster.go | 49 +++++++++----------
internal/elasticsearch/cluster/script.go | 6 +--
.../elasticsearch/cluster/script/delete.go | 2 +-
internal/elasticsearch/cluster/script/read.go | 46 +++++++++++++++++
.../elasticsearch/cluster/script/update.go | 44 +----------------
5 files changed, 75 insertions(+), 72 deletions(-)
diff --git a/internal/clients/elasticsearch/cluster.go b/internal/clients/elasticsearch/cluster.go
index 7ed715fe7..234cc644b 100644
--- a/internal/clients/elasticsearch/cluster.go
+++ b/internal/clients/elasticsearch/cluster.go
@@ -235,33 +235,33 @@ func GetSettings(ctx context.Context, apiClient *clients.ApiClient) (map[string]
return clusterSettings, diags
}
-func GetScript(ctx context.Context, apiClient *clients.ApiClient, id string) (*models.Script, diag.Diagnostics) {
+func GetScript(ctx context.Context, apiClient *clients.ApiClient, id string) (*models.Script, fwdiag.Diagnostics) {
esClient, err := apiClient.GetESClient()
if err != nil {
- return nil, diag.FromErr(err)
+ return nil, fwdiag.Diagnostics{fwdiag.NewErrorDiagnostic("Failed to get ES client", err.Error())}
}
res, err := esClient.GetScript(id, esClient.GetScript.WithContext(ctx))
if err != nil {
- return nil, diag.FromErr(err)
+ return nil, fwdiag.Diagnostics{fwdiag.NewErrorDiagnostic("Failed to get script", err.Error())}
}
defer res.Body.Close()
if res.StatusCode == http.StatusNotFound {
return nil, nil
}
- if diags := utils.CheckError(res, fmt.Sprintf("Unable to get stored script: %s", id)); diags.HasError() {
+ if diags := utils.CheckErrorFromFW(res, fmt.Sprintf("Unable to get stored script: %s", id)); diags.HasError() {
return nil, diags
}
var scriptResponse struct {
Script *models.Script `json:"script"`
}
if err := json.NewDecoder(res.Body).Decode(&scriptResponse); err != nil {
- return nil, diag.FromErr(err)
+ return nil, fwdiag.Diagnostics{fwdiag.NewErrorDiagnostic("Failed to decode script response", err.Error())}
}
return scriptResponse.Script, nil
}
-func PutScript(ctx context.Context, apiClient *clients.ApiClient, script *models.Script) diag.Diagnostics {
+func PutScript(ctx context.Context, apiClient *clients.ApiClient, script *models.Script) fwdiag.Diagnostics {
req := struct {
Script *models.Script `json:"script"`
}{
@@ -269,52 +269,51 @@ func PutScript(ctx context.Context, apiClient *clients.ApiClient, script *models
}
scriptBytes, err := json.Marshal(req)
if err != nil {
- return diag.FromErr(err)
+ return fwdiag.Diagnostics{fwdiag.NewErrorDiagnostic("Failed to marshal script", err.Error())}
}
esClient, err := apiClient.GetESClient()
if err != nil {
- return diag.FromErr(err)
+ return fwdiag.Diagnostics{fwdiag.NewErrorDiagnostic("Failed to get ES client", err.Error())}
}
res, err := esClient.PutScript(script.ID, bytes.NewReader(scriptBytes), esClient.PutScript.WithContext(ctx), esClient.PutScript.WithScriptContext(script.Context))
if err != nil {
- return diag.FromErr(err)
+ return fwdiag.Diagnostics{fwdiag.NewErrorDiagnostic("Failed to put script", err.Error())}
}
defer res.Body.Close()
- if diags := utils.CheckError(res, "Unable to put stored script"); diags.HasError() {
+ if diags := utils.CheckErrorFromFW(res, "Unable to put stored script"); diags.HasError() {
return diags
}
return nil
}
-func DeleteScript(ctx context.Context, apiClient *clients.ApiClient, id string) diag.Diagnostics {
+func DeleteScript(ctx context.Context, apiClient *clients.ApiClient, id string) fwdiag.Diagnostics {
esClient, err := apiClient.GetESClient()
if err != nil {
- return diag.FromErr(err)
+ return fwdiag.Diagnostics{fwdiag.NewErrorDiagnostic("Failed to get ES client", err.Error())}
}
res, err := esClient.DeleteScript(id, esClient.DeleteScript.WithContext(ctx))
if err != nil {
- return diag.FromErr(err)
+ return fwdiag.Diagnostics{fwdiag.NewErrorDiagnostic("Failed to delete script", err.Error())}
}
defer res.Body.Close()
- if diags := utils.CheckError(res, fmt.Sprintf("Unable to delete script: %s", id)); diags.HasError() {
+ if diags := utils.CheckErrorFromFW(res, fmt.Sprintf("Unable to delete script: %s", id)); diags.HasError() {
return diags
}
return nil
}
-// Framework versions that return framework diagnostics
-
-func PutScriptFw(ctx context.Context, apiClient *clients.ApiClient, script *models.Script) fwdiag.Diagnostics {
- sdkDiags := PutScript(ctx, apiClient, script)
- return utils.FrameworkDiagsFromSDK(sdkDiags)
+// SDK-compatible wrapper functions for legacy SDKv2 script resource
+func GetScriptSDK(ctx context.Context, apiClient *clients.ApiClient, id string) (*models.Script, diag.Diagnostics) {
+ script, fwDiags := GetScript(ctx, apiClient, id)
+ return script, utils.SDKDiagsFromFramework(fwDiags)
}
-func GetScriptFw(ctx context.Context, apiClient *clients.ApiClient, id string) (*models.Script, fwdiag.Diagnostics) {
- script, sdkDiags := GetScript(ctx, apiClient, id)
- return script, utils.FrameworkDiagsFromSDK(sdkDiags)
+func PutScriptSDK(ctx context.Context, apiClient *clients.ApiClient, script *models.Script) diag.Diagnostics {
+ fwDiags := PutScript(ctx, apiClient, script)
+ return utils.SDKDiagsFromFramework(fwDiags)
}
-func DeleteScriptFw(ctx context.Context, apiClient *clients.ApiClient, id string) fwdiag.Diagnostics {
- sdkDiags := DeleteScript(ctx, apiClient, id)
- return utils.FrameworkDiagsFromSDK(sdkDiags)
+func DeleteScriptSDK(ctx context.Context, apiClient *clients.ApiClient, id string) diag.Diagnostics {
+ fwDiags := DeleteScript(ctx, apiClient, id)
+ return utils.SDKDiagsFromFramework(fwDiags)
}
diff --git a/internal/elasticsearch/cluster/script.go b/internal/elasticsearch/cluster/script.go
index 1a96f7c27..492743e5a 100644
--- a/internal/elasticsearch/cluster/script.go
+++ b/internal/elasticsearch/cluster/script.go
@@ -77,7 +77,7 @@ func resourceScriptRead(ctx context.Context, d *schema.ResourceData, meta interf
return diags
}
- script, diags := elasticsearch.GetScript(ctx, client, compId.ResourceId)
+ script, diags := elasticsearch.GetScriptSDK(ctx, client, compId.ResourceId)
if script == nil && diags == nil {
tflog.Warn(ctx, fmt.Sprintf(`Script "%s" not found, removing from state`, compId.ResourceId))
d.SetId("")
@@ -129,7 +129,7 @@ func resourceScriptPut(ctx context.Context, d *schema.ResourceData, meta interfa
if scriptContext, ok := d.GetOk("context"); ok {
script.Context = scriptContext.(string)
}
- if diags := elasticsearch.PutScript(ctx, client, &script); diags.HasError() {
+ if diags := elasticsearch.PutScriptSDK(ctx, client, &script); diags.HasError() {
return diags
}
@@ -147,5 +147,5 @@ func resourceScriptDelete(ctx context.Context, d *schema.ResourceData, meta inte
if diags.HasError() {
return diags
}
- return elasticsearch.DeleteScript(ctx, client, compId.ResourceId)
+ return elasticsearch.DeleteScriptSDK(ctx, client, compId.ResourceId)
}
diff --git a/internal/elasticsearch/cluster/script/delete.go b/internal/elasticsearch/cluster/script/delete.go
index be4dd3d6d..5af8c4078 100644
--- a/internal/elasticsearch/cluster/script/delete.go
+++ b/internal/elasticsearch/cluster/script/delete.go
@@ -27,5 +27,5 @@ func (r *scriptResource) Delete(ctx context.Context, req resource.DeleteRequest,
return
}
- resp.Diagnostics.Append(elasticsearch.DeleteScriptFw(ctx, client, compId.ResourceId)...)
+ resp.Diagnostics.Append(elasticsearch.DeleteScript(ctx, client, compId.ResourceId)...)
}
diff --git a/internal/elasticsearch/cluster/script/read.go b/internal/elasticsearch/cluster/script/read.go
index cc0a89deb..90b56a1b9 100644
--- a/internal/elasticsearch/cluster/script/read.go
+++ b/internal/elasticsearch/cluster/script/read.go
@@ -2,10 +2,15 @@ package script
import (
"context"
+ "encoding/json"
"fmt"
"github.com/elastic/terraform-provider-elasticstack/internal/clients"
+ "github.com/elastic/terraform-provider-elasticstack/internal/clients/elasticsearch"
+ "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
@@ -57,3 +62,44 @@ func (r *scriptResource) Read(ctx context.Context, req resource.ReadRequest, res
resp.Diagnostics.Append(resp.State.Set(ctx, &readData)...)
}
+
+func (r *scriptResource) read(ctx context.Context, scriptID string, client *clients.ApiClient) (ScriptData, diag.Diagnostics) {
+ var data ScriptData
+ var diags diag.Diagnostics
+
+ script, frameworkDiags := elasticsearch.GetScript(ctx, client, scriptID)
+ diags.Append(frameworkDiags...)
+ if diags.HasError() {
+ return data, diags
+ }
+
+ if script == nil {
+ // Script not found - return empty data with null ScriptId to signal not found
+ data.ScriptId = types.StringNull()
+ return data, diags
+ }
+
+ data.ScriptId = types.StringValue(scriptID)
+ data.Lang = types.StringValue(script.Language)
+ data.Source = types.StringValue(script.Source)
+
+ // Handle params if returned by the API
+ if len(script.Params) > 0 {
+ paramsBytes, err := json.Marshal(script.Params)
+ if err != nil {
+ diags.AddError("Error marshaling script params", err.Error())
+ return data, diags
+ }
+ data.Params = jsontypes.NewNormalizedValue(string(paramsBytes))
+ } else {
+ data.Params = jsontypes.NewNormalizedNull()
+ }
+ // Note: If params were set but API doesn't return them, they are preserved from state
+ // This maintains backwards compatibility
+
+ // Note: context is not returned by the Elasticsearch API (json:"-" in model)
+ // It's only used during script creation, so we preserve it from state
+ // This is consistent with the SDKv2 implementation
+
+ return data, diags
+}
diff --git a/internal/elasticsearch/cluster/script/update.go b/internal/elasticsearch/cluster/script/update.go
index 26f92c81c..c7896062c 100644
--- a/internal/elasticsearch/cluster/script/update.go
+++ b/internal/elasticsearch/cluster/script/update.go
@@ -8,7 +8,6 @@ import (
"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-framework-jsontypes/jsontypes"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
@@ -59,7 +58,7 @@ func (r *scriptResource) update(ctx context.Context, plan tfsdk.Plan, state *tfs
script.Context = data.Context.ValueString()
}
- diags.Append(elasticsearch.PutScriptFw(ctx, client, &script)...)
+ diags.Append(elasticsearch.PutScript(ctx, client, &script)...)
if diags.HasError() {
return diags
}
@@ -87,47 +86,6 @@ func (r *scriptResource) update(ctx context.Context, plan tfsdk.Plan, state *tfs
return diags
}
-func (r *scriptResource) read(ctx context.Context, scriptID string, client *clients.ApiClient) (ScriptData, diag.Diagnostics) {
- var data ScriptData
- var diags diag.Diagnostics
-
- script, frameworkDiags := elasticsearch.GetScriptFw(ctx, client, scriptID)
- diags.Append(frameworkDiags...)
- if diags.HasError() {
- return data, diags
- }
-
- if script == nil {
- // Script not found - return empty data with null ScriptId to signal not found
- data.ScriptId = types.StringNull()
- return data, diags
- }
-
- data.ScriptId = types.StringValue(scriptID)
- data.Lang = types.StringValue(script.Language)
- data.Source = types.StringValue(script.Source)
-
- // Handle params if returned by the API
- if len(script.Params) > 0 {
- paramsBytes, err := json.Marshal(script.Params)
- if err != nil {
- diags.AddError("Error marshaling script params", err.Error())
- return data, diags
- }
- data.Params = jsontypes.NewNormalizedValue(string(paramsBytes))
- } else {
- data.Params = jsontypes.NewNormalizedNull()
- }
- // Note: If params were set but API doesn't return them, they are preserved from state
- // This maintains backwards compatibility
-
- // Note: context is not returned by the Elasticsearch API (json:"-" in model)
- // It's only used during script creation, so we preserve it from state
- // This is consistent with the SDKv2 implementation
-
- return data, diags
-}
-
func (r *scriptResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
diags := r.update(ctx, req.Plan, &resp.State)
resp.Diagnostics.Append(diags...)
From 49ed8b158e0fc7123efe37be43db6046dfca197d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 14 Sep 2025 09:50:39 +0000
Subject: [PATCH 08/11] Remove SDK version of script resource and SDK wrapper
functions as requested in review
Co-authored-by: tobio <444668+tobio@users.noreply.github.com>
---
internal/clients/elasticsearch/cluster.go | 16 ---
internal/elasticsearch/cluster/script.go | 151 ----------------------
2 files changed, 167 deletions(-)
delete mode 100644 internal/elasticsearch/cluster/script.go
diff --git a/internal/clients/elasticsearch/cluster.go b/internal/clients/elasticsearch/cluster.go
index 234cc644b..f01ed48db 100644
--- a/internal/clients/elasticsearch/cluster.go
+++ b/internal/clients/elasticsearch/cluster.go
@@ -301,19 +301,3 @@ func DeleteScript(ctx context.Context, apiClient *clients.ApiClient, id string)
}
return nil
}
-
-// SDK-compatible wrapper functions for legacy SDKv2 script resource
-func GetScriptSDK(ctx context.Context, apiClient *clients.ApiClient, id string) (*models.Script, diag.Diagnostics) {
- script, fwDiags := GetScript(ctx, apiClient, id)
- return script, utils.SDKDiagsFromFramework(fwDiags)
-}
-
-func PutScriptSDK(ctx context.Context, apiClient *clients.ApiClient, script *models.Script) diag.Diagnostics {
- fwDiags := PutScript(ctx, apiClient, script)
- return utils.SDKDiagsFromFramework(fwDiags)
-}
-
-func DeleteScriptSDK(ctx context.Context, apiClient *clients.ApiClient, id string) diag.Diagnostics {
- fwDiags := DeleteScript(ctx, apiClient, id)
- return utils.SDKDiagsFromFramework(fwDiags)
-}
diff --git a/internal/elasticsearch/cluster/script.go b/internal/elasticsearch/cluster/script.go
deleted file mode 100644
index 492743e5a..000000000
--- a/internal/elasticsearch/cluster/script.go
+++ /dev/null
@@ -1,151 +0,0 @@
-package cluster
-
-import (
- "context"
- "encoding/json"
- "fmt"
-
- "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 ResourceScript() *schema.Resource {
- scriptSchema := map[string]*schema.Schema{
- "script_id": {
- Description: "Identifier for the stored script. Must be unique within the cluster.",
- Type: schema.TypeString,
- Required: true,
- ForceNew: true,
- },
- "lang": {
- Description: "Script language. For search templates, use `mustache`.",
- Type: schema.TypeString,
- Required: true,
- ValidateFunc: validation.StringInSlice([]string{"painless", "expression", "mustache", "java"}, false),
- },
- "source": {
- Description: "For scripts, a string containing the script. For search templates, an object containing the search template.",
- Type: schema.TypeString,
- Required: true,
- },
- "params": {
- Description: "Parameters for the script or search template.",
- Type: schema.TypeString,
- Optional: true,
- DiffSuppressFunc: utils.DiffJsonSuppress,
- ValidateFunc: validation.StringIsJSON,
- },
- "context": {
- Description: "Context in which the script or search template should run.",
- Type: schema.TypeString,
- Optional: true,
- },
- }
- utils.AddConnectionSchema(scriptSchema)
-
- return &schema.Resource{
- Description: "Creates or updates a stored script or search template. See https://www.elastic.co/guide/en/elasticsearch/reference/current/create-stored-script-api.html",
-
- CreateContext: resourceScriptPut,
- UpdateContext: resourceScriptPut,
- ReadContext: resourceScriptRead,
- DeleteContext: resourceScriptDelete,
-
- Importer: &schema.ResourceImporter{
- StateContext: schema.ImportStatePassthroughContext,
- },
-
- Schema: scriptSchema,
- }
-}
-
-func resourceScriptRead(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
- }
-
- script, diags := elasticsearch.GetScriptSDK(ctx, client, compId.ResourceId)
- if script == nil && diags == nil {
- tflog.Warn(ctx, fmt.Sprintf(`Script "%s" not found, removing from state`, compId.ResourceId))
- d.SetId("")
- return nil
- }
- if diags.HasError() {
- return diags
- }
-
- if err := d.Set("script_id", compId.ResourceId); err != nil {
- return diag.FromErr(err)
- }
- if err := d.Set("lang", script.Language); err != nil {
- return diag.FromErr(err)
- }
- if err := d.Set("source", script.Source); err != nil {
- return diag.FromErr(err)
- }
-
- return diags
-}
-
-func resourceScriptPut(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
- client, diags := clients.NewApiClientFromSDKResource(d, meta)
- if diags.HasError() {
- return diags
- }
-
- scriptID := d.Get("script_id").(string)
- id, diags := client.ID(ctx, scriptID)
- if diags.HasError() {
- return diags
- }
-
- script := models.Script{
- ID: scriptID,
- Language: d.Get("lang").(string),
- Source: d.Get("source").(string),
- }
- if paramsJSON, ok := d.GetOk("params"); ok {
- var params map[string]interface{}
- bytes := []byte(paramsJSON.(string))
- err := json.Unmarshal(bytes, ¶ms)
- if err != nil {
- return diag.FromErr(err)
- }
- script.Params = params
- }
- if scriptContext, ok := d.GetOk("context"); ok {
- script.Context = scriptContext.(string)
- }
- if diags := elasticsearch.PutScriptSDK(ctx, client, &script); diags.HasError() {
- return diags
- }
-
- d.SetId(id.String())
- return resourceScriptRead(ctx, d, meta)
-}
-
-func resourceScriptDelete(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
- }
- return elasticsearch.DeleteScriptSDK(ctx, client, compId.ResourceId)
-}
From 536cec67b9c20cfb94c34e3866966e866eb180cd Mon Sep 17 00:00:00 2001
From: Toby Brain
Date: Mon, 15 Sep 2025 09:22:34 +1000
Subject: [PATCH 09/11] Apply suggestions from code review
---
internal/elasticsearch/cluster/script/create.go | 3 ---
internal/elasticsearch/cluster/script/update.go | 3 ---
2 files changed, 6 deletions(-)
diff --git a/internal/elasticsearch/cluster/script/create.go b/internal/elasticsearch/cluster/script/create.go
index 5ab88f551..f71a42a67 100644
--- a/internal/elasticsearch/cluster/script/create.go
+++ b/internal/elasticsearch/cluster/script/create.go
@@ -9,7 +9,4 @@ import (
func (r *scriptResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
diags := r.update(ctx, req.Plan, &resp.State)
resp.Diagnostics.Append(diags...)
- if resp.Diagnostics.HasError() {
- return
- }
}
diff --git a/internal/elasticsearch/cluster/script/update.go b/internal/elasticsearch/cluster/script/update.go
index c7896062c..f38079344 100644
--- a/internal/elasticsearch/cluster/script/update.go
+++ b/internal/elasticsearch/cluster/script/update.go
@@ -89,7 +89,4 @@ func (r *scriptResource) update(ctx context.Context, plan tfsdk.Plan, state *tfs
func (r *scriptResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
diags := r.update(ctx, req.Plan, &resp.State)
resp.Diagnostics.Append(diags...)
- if resp.Diagnostics.HasError() {
- return
- }
}
From d900145b830ee114a4726d8afe778d7a9d371081 Mon Sep 17 00:00:00 2001
From: Toby Brain
Date: Tue, 23 Sep 2025 19:56:44 +1000
Subject: [PATCH 10/11] Remove duplicate acceptance test
---
.../security/role_mapping_data_source_test.go | 54 -------------------
1 file changed, 54 deletions(-)
delete mode 100644 internal/elasticsearch/security/role_mapping_data_source_test.go
diff --git a/internal/elasticsearch/security/role_mapping_data_source_test.go b/internal/elasticsearch/security/role_mapping_data_source_test.go
deleted file mode 100644
index 026257a15..000000000
--- a/internal/elasticsearch/security/role_mapping_data_source_test.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package security_test
-
-import (
- "testing"
-
- "github.com/elastic/terraform-provider-elasticstack/internal/acctest"
- "github.com/elastic/terraform-provider-elasticstack/internal/acctest/checks"
- "github.com/hashicorp/terraform-plugin-testing/helper/resource"
-)
-
-func TestAccDataSourceSecurityRoleMapping(t *testing.T) {
- resource.Test(t, resource.TestCase{
- PreCheck: func() { acctest.PreCheck(t) },
- ProtoV6ProviderFactories: acctest.Providers,
- Steps: []resource.TestStep{
- {
- Config: testAccDataSourceSecurityRoleMapping,
- Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttr("data.elasticstack_elasticsearch_security_role_mapping.test", "name", "data_source_test"),
- resource.TestCheckResourceAttr("data.elasticstack_elasticsearch_security_role_mapping.test", "enabled", "true"),
- checks.TestCheckResourceListAttr("data.elasticstack_elasticsearch_security_role_mapping.test", "roles", []string{"admin"}),
- resource.TestCheckResourceAttr("data.elasticstack_elasticsearch_security_role_mapping.test", "rules", `{"any":[{"field":{"username":"esadmin"}},{"field":{"groups":"cn=admins,dc=example,dc=com"}}]}`),
- resource.TestCheckResourceAttr("data.elasticstack_elasticsearch_security_role_mapping.test", "metadata", `{"version":1}`),
- ),
- },
- },
- })
-}
-
-const testAccDataSourceSecurityRoleMapping = `
-provider "elasticstack" {
- elasticsearch {}
-}
-
-resource "elasticstack_elasticsearch_security_role_mapping" "test" {
- name = "data_source_test"
- enabled = true
- roles = [
- "admin"
- ]
- rules = jsonencode({
- any = [
- { field = { username = "esadmin" } },
- { field = { groups = "cn=admins,dc=example,dc=com" } },
- ]
- })
-
- metadata = jsonencode({ version = 1 })
-}
-
-data "elasticstack_elasticsearch_security_role_mapping" "test" {
- name = elasticstack_elasticsearch_security_role_mapping.test.name
-}
-`
From e4b66d40745d09632afca7be3491c42ec4d21475 Mon Sep 17 00:00:00 2001
From: Toby Brain
Date: Tue, 23 Sep 2025 19:58:18 +1000
Subject: [PATCH 11/11] Tidy up watcher docs link
---
docs/resources/elasticsearch_watch.md | 4 ++--
internal/elasticsearch/watcher/watch.go | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/docs/resources/elasticsearch_watch.md b/docs/resources/elasticsearch_watch.md
index 3625f84d3..4464ea28d 100644
--- a/docs/resources/elasticsearch_watch.md
+++ b/docs/resources/elasticsearch_watch.md
@@ -4,12 +4,12 @@
page_title: "elasticstack_elasticsearch_watch Resource - terraform-provider-elasticstack"
subcategory: "Elasticsearch"
description: |-
- Manage Watches. See, https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api.html
+ Manage Watches. See the Watcher API documentation https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api.html for more details.
---
# elasticstack_elasticsearch_watch (Resource)
-Manage Watches. See, https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api.html
+Manage Watches. See the [Watcher API documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api.html) for more details.
## Example Usage
diff --git a/internal/elasticsearch/watcher/watch.go b/internal/elasticsearch/watcher/watch.go
index 7190e4db8..2f1629689 100644
--- a/internal/elasticsearch/watcher/watch.go
+++ b/internal/elasticsearch/watcher/watch.go
@@ -89,7 +89,7 @@ func ResourceWatch() *schema.Resource {
}
return &schema.Resource{
- Description: "Manage Watches. See, https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api.html",
+ Description: "Manage Watches. See the [Watcher API documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api.html) for more details.",
CreateContext: resourceWatchPut,
UpdateContext: resourceWatchPut,