Skip to content

Commit

Permalink
Add script resource (#173)
Browse files Browse the repository at this point in the history
* Implement script resource

* Generate docs for script resource

* Update CHANGELOG

* Fixed reviewed ones

* Fix test

* Fix subcategory for elasticstack_elasticsearch_script

* Fix elasticstack_elasticsearch_script docs

* Format code

* Regenerate docs

* Remove new resource check
  • Loading branch information
k-yomo authored Nov 1, 2022
1 parent 99e7990 commit 1131f2a
Show file tree
Hide file tree
Showing 10 changed files with 490 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### Added
- New resource `elasticstack_elasticsearch_logstash_pipeline` to manage Logstash pipelines ([Centralized Pipeline Management](https://www.elastic.co/guide/en/logstash/current/logstash-centralized-pipeline-management.html)) ([#151](https://github.com/elastic/terraform-provider-elasticstack/pull/151))
- Add `elasticstack_elasticsearch_script` resource ([#173](https://github.com/elastic/terraform-provider-elasticstack/pull/173))
- Add `elasticstack_elasticsearch_security_role` data source ([#177](https://github.com/elastic/terraform-provider-elasticstack/pull/177))
- Add `elasticstack_elasticsearch_security_role_mapping` data source ([#178](https://github.com/elastic/terraform-provider-elasticstack/pull/178))

Expand Down
83 changes: 83 additions & 0 deletions docs/resources/elasticsearch_script.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
subcategory: "Cluster"
layout: ""
page_title: "Elasticstack: elasticstack_elasticsearch_script Resource"
description: |-
Creates or updates a stored script or search template.
---

# Resource: elasticstack_elasticsearch_script

Creates or updates a stored script or search template. See https://www.elastic.co/guide/en/elasticsearch/reference/current/create-stored-script-api.html

## Example Usage

```terraform
provider "elasticstack" {
elasticsearch {}
}
resource "elasticstack_elasticsearch_script" "my_script" {
script_id = "my_script"
lang = "painless"
source = "Math.log(_score * 2) + params['my_modifier']"
context = "score"
}
resource "elasticstack_elasticsearch_script" "my_search_template" {
script_id = "my_search_template"
lang = "mustache"
source = jsonencode({
query = {
match = {
message = "{{query_string}}"
}
}
from = "{{from}}"
size = "{{size}}"
})
params = jsonencode({
query_string = "My query string"
})
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `lang` (String) Script language. For search templates, use `mustache`.
- `script_id` (String) Identifier for the stored script. Must be unique within the cluster.
- `source` (String) For scripts, a string containing the script. For search templates, an object containing the search template.

### Optional

- `context` (String) Context in which the script or search template should run.
- `elasticsearch_connection` (Block List, Max: 1) Used to establish connection to Elasticsearch server. Overrides environment variables if present. (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.

<a id="nestedblock--elasticsearch_connection"></a>
### Nested Schema for `elasticsearch_connection`

Optional:

- `api_key` (String, Sensitive) API Key to use for authentication to Elasticsearch
- `ca_data` (String) PEM-encoded custom Certificate Authority certificate
- `ca_file` (String) Path to a custom Certificate Authority certificate
- `endpoints` (List of String, Sensitive) A list of endpoints the Terraform provider will point to. They must include the http(s) schema and port number.
- `insecure` (Boolean) Disable TLS certificate validation
- `password` (String, Sensitive) A password to use for API authentication to Elasticsearch.
- `username` (String) A username to use for API authentication to Elasticsearch.

## Import

Import is supported using the following syntax:

```shell
terraform import elasticstack_elasticsearch_script.my_script <cluster_uuid>/<script id>
```
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform import elasticstack_elasticsearch_script.my_script <cluster_uuid>/<script id>
27 changes: 27 additions & 0 deletions examples/resources/elasticstack_elasticsearch_script/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
provider "elasticstack" {
elasticsearch {}
}

resource "elasticstack_elasticsearch_script" "my_script" {
script_id = "my_script"
lang = "painless"
source = "Math.log(_score * 2) + params['my_modifier']"
context = "score"
}

resource "elasticstack_elasticsearch_script" "my_search_template" {
script_id = "my_search_template"
lang = "mustache"
source = jsonencode({
query = {
match = {
message = "{{query_string}}"
}
}
from = "{{from}}"
size = "{{size}}"
})
params = jsonencode({
query_string = "My query string"
})
}
55 changes: 55 additions & 0 deletions internal/clients/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,58 @@ func (a *ApiClient) GetElasticsearchSettings(ctx context.Context) (map[string]in
}
return clusterSettings, diags
}

func (a *ApiClient) GetElasticsearchScript(ctx context.Context, id string) (*models.Script, diag.Diagnostics) {
res, err := a.es.GetScript(id, a.es.GetScript.WithContext(ctx))
if err != nil {
return nil, diag.FromErr(err)
}
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() {
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 scriptResponse.Script, nil
}

func (a *ApiClient) PutElasticsearchScript(ctx context.Context, script *models.Script) diag.Diagnostics {
req := struct {
Script *models.Script `json:"script"`
}{
script,
}
scriptBytes, err := json.Marshal(req)
if err != nil {
return diag.FromErr(err)
}
res, err := a.es.PutScript(script.ID, bytes.NewReader(scriptBytes), a.es.PutScript.WithContext(ctx), a.es.PutScript.WithScriptContext(script.Context))
if err != nil {
return diag.FromErr(err)
}
defer res.Body.Close()
if diags := utils.CheckError(res, "Unable to put stored script"); diags.HasError() {
return diags
}
return nil
}

func (a *ApiClient) DeleteElasticsearchScript(ctx context.Context, id string) diag.Diagnostics {
res, err := a.es.DeleteScript(id, a.es.DeleteScript.WithContext(ctx))
if err != nil {
return diag.FromErr(err)
}
defer res.Body.Close()
if diags := utils.CheckError(res, fmt.Sprintf("Unable to delete script: %s", id)); diags.HasError() {
return diags
}
return nil
}
150 changes: 150 additions & 0 deletions internal/elasticsearch/cluster/script.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package cluster

import (
"context"
"encoding/json"
"fmt"

"github.com/elastic/terraform-provider-elasticstack/internal/clients"
"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 {
var diags diag.Diagnostics
client, err := clients.NewApiClient(d, meta)
if err != nil {
return diag.FromErr(err)
}

id := d.Id()
compId, diags := clients.CompositeIdFromStr(id)
if diags.HasError() {
return diags
}

script, diags := client.GetElasticsearchScript(ctx, compId.ResourceId)
if script == nil && diags == nil {
tflog.Warn(ctx, fmt.Sprintf(`Script "%s" not found, removing from state`, compId.ResourceId))
d.SetId("")
}
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, err := clients.NewApiClient(d, meta)
if err != nil {
return diag.FromErr(err)
}

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, &params)
if err != nil {
return diag.FromErr(err)
}
script.Params = params
}
if scriptContext, ok := d.GetOk("context"); ok {
script.Context = scriptContext.(string)
}
if diags := client.PutElasticsearchScript(ctx, &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, err := clients.NewApiClient(d, meta)
if err != nil {
return diag.FromErr(err)
}

compId, diags := clients.CompositeIdFromStr(d.Id())
if diags.HasError() {
return diags
}
return client.DeleteElasticsearchScript(ctx, compId.ResourceId)
}
Loading

0 comments on commit 1131f2a

Please sign in to comment.