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,