diff --git a/docs/resources/sm_policy.md b/docs/resources/sm_policy.md new file mode 100644 index 0000000..2a5e9e9 --- /dev/null +++ b/docs/resources/sm_policy.md @@ -0,0 +1,46 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "opensearch_sm_policy Resource - terraform-provider-opensearch" +subcategory: "" +description: |- + Provides an OpenSearch Snapshot Management (SM) policy. Please refer to the OpenSearch SM documentation for details. +--- + +# opensearch_sm_policy (Resource) + +Provides an OpenSearch Snapshot Management (SM) policy. Please refer to the OpenSearch SM documentation for details. + +## Example Usage + +```terraform +# Create an SM policy +resource "opensearch_sm_policy" "snapshot_to_s3" { + policy_id = "snapshot_to_s3" + body = file("${path.module}/policies/snapshot_to_s3.json") +} +``` + + +## Schema + +### Required + +- `body` (String) The policy document. +- `policy_id` (String) The id of the SM policy. + +### Optional + +- `primary_term` (Number) The primary term of the SM policy version. +- `seq_no` (Number) The sequence number of the SM policy version. + +### Read-Only + +- `id` (String) The ID of this resource. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import opensearch_sm_policy.cleanup snapshot_to_s3 +``` diff --git a/examples/resources/opensearch_sm_policy/import.sh b/examples/resources/opensearch_sm_policy/import.sh new file mode 100644 index 0000000..e3c8568 --- /dev/null +++ b/examples/resources/opensearch_sm_policy/import.sh @@ -0,0 +1 @@ +terraform import opensearch_sm_policy.cleanup snapshot_to_s3 diff --git a/examples/resources/opensearch_sm_policy/resource.tf b/examples/resources/opensearch_sm_policy/resource.tf new file mode 100644 index 0000000..2c6d479 --- /dev/null +++ b/examples/resources/opensearch_sm_policy/resource.tf @@ -0,0 +1,5 @@ +# Create an SM policy +resource "opensearch_sm_policy" "snapshot_to_s3" { + policy_id = "snapshot_to_s3" + body = file("${path.module}/policies/snapshot_to_s3.json") +} diff --git a/provider/provider.go b/provider/provider.go index 975a87f..d7cf2ad 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -245,6 +245,7 @@ func Provider() *schema.Provider { "opensearch_snapshot_repository": resourceOpensearchSnapshotRepository(), "opensearch_channel_configuration": resourceOpenSearchChannelConfiguration(), "opensearch_anomaly_detection": resourceOpenSearchAnomalyDetection(), + "opensearch_sm_policy": resourceOpenSearchSMPolicy(), }, DataSourcesMap: map[string]*schema.Resource{ diff --git a/provider/resource_opensearch_sm_policy.go b/provider/resource_opensearch_sm_policy.go new file mode 100644 index 0000000..c53b757 --- /dev/null +++ b/provider/resource_opensearch_sm_policy.go @@ -0,0 +1,244 @@ +package provider + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/structure" + "github.com/olivere/elastic/uritemplates" + + elastic7 "github.com/olivere/elastic/v7" + elastic6 "gopkg.in/olivere/elastic.v6" +) + +var openSearchSMPolicySchema = map[string]*schema.Schema{ + "policy_id": { + Description: "The id of the SM policy.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "body": { + Description: "The policy document.", + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: diffSuppressPolicy, + StateFunc: func(v interface{}) string { + json, _ := structure.NormalizeJsonString(v) + return json + }, + }, + "primary_term": { + Description: "The primary term of the SM policy version.", + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + "seq_no": { + Description: "The sequence number of the SM policy version.", + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, +} + +func resourceOpenSearchSMPolicy() *schema.Resource { + return &schema.Resource{ + Description: "Provides an OpenSearch Snapshot Management (SM) policy. Please refer to the OpenSearch SM documentation for details.", + Create: resourceOpensearchSMPolicyCreate, + Read: resourceOpensearchSMPolicyRead, + Update: resourceOpensearchSMPolicyUpdate, + Delete: resourceOpensearchSMPolicyDelete, + Schema: openSearchSMPolicySchema, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + } +} + +func resourceOpensearchSMPolicyCreate(d *schema.ResourceData, m interface{}) error { + if _, err := resourceOpensearchPostPutSMPolicy(d, m, "POST"); err != nil { + log.Printf("[INFO] Failed to create OpensearchPolicy: %+v", err) + return err + } + + policyID := d.Get("policy_id").(string) + d.SetId(policyID) + return resourceOpensearchSMPolicyRead(d, m) +} + +func resourceOpensearchSMPolicyRead(d *schema.ResourceData, m interface{}) error { + policyResponse, err := resourceOpensearchGetSMPolicy(d.Id(), m) + + if err != nil { + if elastic6.IsNotFound(err) || elastic7.IsNotFound(err) { + log.Printf("[WARN] Opensearch Policy (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + return err + } + + bodyString, err := json.Marshal(policyResponse.Policy) + if err != nil { + return err + } + + if err := d.Set("policy_id", policyResponse.PolicyID); err != nil { + return fmt.Errorf("error setting policy_id: %s", err) + } + if err := d.Set("body", bodyString); err != nil { + return fmt.Errorf("error setting body: %s", err) + } + if err := d.Set("primary_term", policyResponse.PrimaryTerm); err != nil { + return fmt.Errorf("error setting primary_term: %s", err) + } + if err := d.Set("seq_no", policyResponse.SeqNo); err != nil { + return fmt.Errorf("error setting seq_no: %s", err) + } + + return nil +} + +func resourceOpensearchSMPolicyUpdate(d *schema.ResourceData, m interface{}) error { + if _, err := resourceOpensearchPostPutSMPolicy(d, m, "PUT"); err != nil { + return err + } + + return resourceOpensearchSMPolicyRead(d, m) +} + +func resourceOpensearchSMPolicyDelete(d *schema.ResourceData, m interface{}) error { + path, err := uritemplates.Expand("/_plugins/_sm/policies/{policy_id}", map[string]string{ + "policy_id": d.Id(), + }) + if err != nil { + return fmt.Errorf("error building URL path for policy: %+v", err) + } + + osclient, err := getClient(m.(*ProviderConf)) + if err != nil { + return err + } + _, err = osclient.PerformRequest(context.TODO(), elastic7.PerformRequestOptions{ + Method: "DELETE", + Path: path, + RetryStatusCodes: []int{http.StatusConflict}, + Retrier: elastic7.NewBackoffRetrier( + elastic7.NewExponentialBackoff(100*time.Millisecond, 30*time.Second), + ), + }) + + if err != nil { + return fmt.Errorf("error deleting policy: %+v : %+v", path, err) + } + + return err +} + +func resourceOpensearchGetSMPolicy(policyID string, m interface{}) (SMPolicyResponse, error) { + var err error + response := new(SMPolicyResponse) + + path, err := uritemplates.Expand("/_plugins/_sm/policies/{policy_id}", map[string]string{ + "policy_id": policyID, + }) + + if err != nil { + return *response, fmt.Errorf("error building URL path for policy: %+v", err) + } + + var body *json.RawMessage + osclient, err := getClient(m.(*ProviderConf)) + if err != nil { + return *response, err + } + var res *elastic7.Response + res, err = osclient.PerformRequest(context.TODO(), elastic7.PerformRequestOptions{ + Method: "GET", + Path: path, + }) + + if err != nil { + return *response, fmt.Errorf("error getting policy: %+v : %+v", path, err) + } + body = &res.Body + + if err != nil { + return *response, err + } + + if err := json.Unmarshal(*body, &response); err != nil { + return *response, fmt.Errorf("error unmarshalling policy body: %+v: %+v", err, body) + } + + normalizePolicy(response.Policy) + + return *response, err +} + +func resourceOpensearchPostPutSMPolicy(d *schema.ResourceData, m interface{}, method string) (*SMPolicyResponse, error) { + response := new(SMPolicyResponse) + policyJSON := d.Get("body").(string) + seq := d.Get("seq_no").(int) + primTerm := d.Get("primary_term").(int) + params := url.Values{} + + if seq >= 0 && primTerm > 0 { + params.Set("if_seq_no", strconv.Itoa(seq)) + params.Set("if_primary_term", strconv.Itoa(primTerm)) + } + + path, err := uritemplates.Expand("/_plugins/_sm/policies/{policy_id}", map[string]string{ + "policy_id": d.Get("policy_id").(string), + }) + if err != nil { + return response, fmt.Errorf("error building URL path for policy: %+v", err) + } + + var body *json.RawMessage + osclient, err := getClient(m.(*ProviderConf)) + if err != nil { + return nil, err + } + var res *elastic7.Response + res, err = osclient.PerformRequest(context.TODO(), elastic7.PerformRequestOptions{ + Method: method, + Path: path, + Params: params, + Body: string(policyJSON), + RetryStatusCodes: []int{http.StatusConflict}, + Retrier: elastic7.NewBackoffRetrier( + elastic7.NewExponentialBackoff(100*time.Millisecond, 30*time.Second), + ), + }) + if err != nil { + return response, fmt.Errorf("error posting policy: %+v : %+v : %+v", path, policyJSON, err) + } + body = &res.Body + + if err != nil { + return response, fmt.Errorf("error creating policy mapping: %+v", err) + } + + if err := json.Unmarshal(*body, response); err != nil { + return response, fmt.Errorf("error unmarshalling policy body: %+v: %+v", err, body) + } + + return response, nil +} + +type SMPolicyResponse struct { + PolicyID string `json:"_id"` + Version int `json:"_version"` + PrimaryTerm int `json:"_primary_term"` + SeqNo int `json:"_seq_no"` + Policy map[string]interface{} `json:"sm_policy"` +} diff --git a/provider/resource_opensearch_sm_policy_test.go b/provider/resource_opensearch_sm_policy_test.go new file mode 100644 index 0000000..ac649c4 --- /dev/null +++ b/provider/resource_opensearch_sm_policy_test.go @@ -0,0 +1,153 @@ +// TODO! + +package provider + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccOpensearchSMPolicy(t *testing.T) { + provider := Provider() + diags := provider.Configure(context.Background(), &terraform.ResourceConfig{}) + if diags.HasError() { + t.Skipf("err: %#v", diags) + } + var allowed bool + + config := testAccOpensearchSMPolicyV7 + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + if !allowed { + t.Skip("OpenSearch SMPolicies only supported on ES 6.") + } + }, + Providers: testAccOpendistroProviders, + CheckDestroy: testCheckOpensearchSMPolicyDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckOpensearchSMPolicyExists("opensearch_sm_policy.test_policy"), + resource.TestCheckResourceAttr( + "opensearch_sm_policy.test_policy", + "policy_id", + "test_policy", + ), + ), + }, + }, + }) +} + +func testCheckOpensearchSMPolicyExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + if rs.Primary.ID == "" { + return fmt.Errorf("No policy ID is set") + } + + meta := testAccOpendistroProvider.Meta() + + var err error + _, err = resourceOpensearchGetSMPolicy(rs.Primary.ID, meta.(*ProviderConf)) + + if err != nil { + return err + } + + return nil + } +} + +func testCheckOpensearchSMPolicyDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "opensearch_sm_policy" { + continue + } + + meta := testAccOpendistroProvider.Meta() + + var err error + if err != nil { + return err + } + _, err = resourceOpensearchGetSMPolicy(rs.Primary.ID, meta.(*ProviderConf)) + + if err != nil { + return nil // should be not found error + } + + return fmt.Errorf("OpenDistroSMPolicy %q still exists", rs.Primary.ID) + } + + return nil +} + +var testAccOpensearchSMPolicyV7 = ` +resource "opensearch_sm_policy" "test_policy" { + policy_id = "test_policy" + body = <