diff --git a/docs/resources/ec_deployment_elasticsearch_keystore.md b/docs/resources/ec_deployment_elasticsearch_keystore.md new file mode 100644 index 000000000..0091f8ae8 --- /dev/null +++ b/docs/resources/ec_deployment_elasticsearch_keystore.md @@ -0,0 +1,93 @@ +--- +page_title: "Elastic Cloud: ec_deployment_elasticsearch_keystore" +description: |- + Provides an Elastic Cloud Deployment Elasticsearch keystore resource, which allows creating and updating Elasticsearch Keystore settings. +--- + +# Resource: ec_deployment_elasticsearch_keystore +Provides an Elastic Cloud Deployment Elasticsearch keystore resource, which allows you to create and update Elasticsearch keystore settings. + +Elasticsearch keystore settings can be created and updated through this resource, **each resource represents a single Elasticsearch Keystore setting**. After adding a key and its secret value to the keystore, you can use the key in place of the secret value when you configure sensitive settings. + +~> **Note on Elastic keystore settings** This resource offers weaker consistency guarantees and will not detect and update keystore setting values that have been modified outside of the scope of Terraform, usually referred to as _drift_. For example, consider the following scenario: + 1. A keystore setting is created using this resource. + 2. The keystore setting's value is modified to a different value using the Elasticsearch Service API. + 3. Running `terraform apply` fails to detect the changes and does not update the keystore setting to the value defined in the terraform configuration. + To force the keystore setting to the value it is configured to hold, you may want to taint the resource and force its recreation. + +Before you create Elasticsearch keystore settings, check the [official Elasticsearch keystore documentation](https://www.elastic.co/guide/en/elasticsearch/reference/master/elasticsearch-keystore.html) and the [Elastic Cloud specific documentation](https://www.elastic.co/guide/en/cloud/current/ec-configuring-keystore.html). + +## Example Usage + +These examples show how to use the resource at a basic level, and can be copied. This resource becomes really useful when combined with other data providers, like vault or similar. + +### Adding a new keystore setting to your deployment + +```hcl +data "ec_stack" "latest" { + version_regex = "latest" + region = "us-east-1" +} + +# Create an Elastic Cloud deployment +resource "ec_deployment" "example_keystore" { + region = "us-east-1" + version = data.ec_stack.latest.version + deployment_template_id = "aws-io-optimized-v2" + + elasticsearch {} +} + +# Create the keystore secret entry +resource "ec_deployment_elasticsearch_keystore" "secure_url" { + deployment_id = ec_deployment.example_keystore.id + setting_name = "xpack.notification.slack.account.hello.secure_url" + value = "http://my-secure-url.com" +} + +``` + +### Adding credentials to use GCS as a snapshot repository + +For up-to-date documentation on the `setting_name`, refer to the [ESS documentation](https://www.elastic.co/guide/en/cloud/current/ec-gcs-snapshotting.html#ec-gcs-service-account-key). + +```hcl +data "ec_stack" "latest" { + version_regex = "latest" + region = "us-east-1" +} + +# Create an Elastic Cloud deployment +resource "ec_deployment" "example_keystore" { + region = "us-east-1" + version = data.ec_stack.latest.version + deployment_template_id = "aws-io-optimized-v2" + + elasticsearch {} +} + +# Create the keystore secret entry +resource "ec_deployment_elasticsearch_keystore" "gcs_credential" { + deployment_id = ec_deployment.example_keystore.id + setting_name = "gcs.client.default.credentials_file" + value = file("service-account-key.json") + as_file = true +} +``` + +## Argument reference +The following arguments are supported: + +* `deployment_id` - (Required) Deployment ID of the deployment that holds the Elasticsearch cluster where the keystore setting is written to. +* `setting_name` - (Required) Required name for the keystore setting, if the setting already exists in the Elasticsearch cluster, it will be overridden. +* `value` - (Required) Value of this setting. This can either be a string or a JSON object that is stored as a JSON string in the keystore. +* `as_file` - (Optional) if set to `true`, it stores the remote keystore setting as a file. The default value is `false`, which stores the keystore setting as string when value is a plain string. + + +## Attributes reference + +There are no additional attributes exported by this resource other than the referenced arguments. + +## Import + +This resource cannot be imported. diff --git a/ec/acc/deployment_elasticsearch_kesytore_destroy_test.go b/ec/acc/deployment_elasticsearch_kesytore_destroy_test.go new file mode 100644 index 000000000..6a4332355 --- /dev/null +++ b/ec/acc/deployment_elasticsearch_kesytore_destroy_test.go @@ -0,0 +1,53 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package acc + +import ( + "fmt" + + "github.com/elastic/cloud-sdk-go/pkg/api/deploymentapi/eskeystoreapi" + "github.com/elastic/cloud-sdk-go/pkg/multierror" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func testAccDeploymentElasticsearchKeystoreDestroy(s *terraform.State) error { + // retrieve the connection established in Provider configuration + client, err := newAPI() + if err != nil { + return err + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "ec_deployment_elasticsearch_keystore" { + continue + } + + res, err := eskeystoreapi.Get(eskeystoreapi.GetParams{ + API: client, + DeploymentID: rs.Primary.Attributes["deployment_id"], + }) + + if err == nil || res != nil { + return multierror.NewPrefixed("ec_deployment_elasticsearch_keystore found", + fmt.Errorf("deployment (%s) still exists", rs.Primary.Attributes["deployment_id"]), + ) + } + } + + return nil +} diff --git a/ec/acc/deployment_elasticsearch_kesytore_test.go b/ec/acc/deployment_elasticsearch_kesytore_test.go new file mode 100644 index 000000000..5870bc2a6 --- /dev/null +++ b/ec/acc/deployment_elasticsearch_kesytore_test.go @@ -0,0 +1,143 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package acc + +import ( + "fmt" + "testing" + + "github.com/elastic/cloud-sdk-go/pkg/multierror" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccDeploymentElasticsearchKeystore_full(t *testing.T) { + var previousID, currentID string + + resType := "ec_deployment_elasticsearch_keystore" + firstResName := resType + ".test" + secondResName := resType + ".gcs_creds" + randomName := prefix + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + startCfg := "testdata/deployment_elasticsearch_keystore_1.tf" + updateKeystoreSetting := "testdata/deployment_elasticsearch_keystore_2.tf" + changeKeystoreSettingName := "testdata/deployment_elasticsearch_keystore_3.tf" + deleteAllKeystoreSettings := "testdata/deployment_elasticsearch_keystore_4.tf" + + cfgF := func(cfg string) string { + return fixtureAccDeploymentResourceBasic( + t, cfg, randomName, getRegion(), defaultTemplate, + ) + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactory, + CheckDestroy: resource.ComposeAggregateTestCheckFunc( + testAccDeploymentDestroy, + testAccDeploymentElasticsearchKeystoreDestroy, + ), + Steps: []resource.TestStep{ + { + Config: cfgF(startCfg), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(firstResName, "setting_name", "xpack.notification.slack.account.hello.secure_url"), + resource.TestCheckResourceAttr(firstResName, "value", "hella"), + resource.TestCheckResourceAttr(firstResName, "as_file", "false"), + resource.TestCheckResourceAttrSet(firstResName, "deployment_id"), + + resource.TestCheckResourceAttr(secondResName, "setting_name", "gcs.client.secondary.credentials_file"), + resource.TestCheckResourceAttr(secondResName, "value", "{\n \"type\": \"service_account\",\n \"project_id\": \"project-id\",\n \"private_key_id\": \"key-id\",\n \"private_key\": \"-----BEGIN PRIVATE KEY-----\\nprivate-key\\n-----END PRIVATE KEY-----\\n\",\n \"client_email\": \"service-account-email\",\n \"client_id\": \"client-id\",\n \"auth_uri\": \"https://accounts.google.com/o/oauth2/auth\",\n \"token_uri\": \"https://accounts.google.com/o/oauth2/token\",\n \"auth_provider_x509_cert_url\": \"https://www.googleapis.com/oauth2/v1/certs\",\n \"client_x509_cert_url\": \"https://www.googleapis.com/robot/v1/metadata/x509/service-account-email\"\n}"), + resource.TestCheckResourceAttr(secondResName, "as_file", "false"), + resource.TestCheckResourceAttrSet(secondResName, "deployment_id"), + ), + }, + { + Config: cfgF(updateKeystoreSetting), + Check: resource.ComposeAggregateTestCheckFunc( + checkESKeystoreResourceID(firstResName, &previousID), + + resource.TestCheckResourceAttr(firstResName, "setting_name", "xpack.notification.slack.account.hello.secure_url"), + resource.TestCheckResourceAttr(firstResName, "value", "hello2u"), + resource.TestCheckResourceAttr(firstResName, "as_file", "false"), + resource.TestCheckResourceAttrSet(firstResName, "deployment_id"), + + resource.TestCheckResourceAttr(secondResName, "setting_name", "gcs.client.secondary.credentials_file"), + resource.TestCheckResourceAttr(secondResName, "value", "{\n \"type\": \"service_account\",\n \"project_id\": \"project-id\",\n \"private_key_id\": \"key-id\",\n \"private_key\": \"-----BEGIN PRIVATE KEY-----\\nprivate-key\\n-----END PRIVATE KEY-----\\n\",\n \"client_email\": \"service-account-email\",\n \"client_id\": \"client-id\",\n \"auth_uri\": \"https://accounts.google.com/o/oauth2/auth\",\n \"token_uri\": \"https://accounts.google.com/o/oauth2/token\",\n \"auth_provider_x509_cert_url\": \"https://www.googleapis.com/oauth2/v1/certs\",\n \"client_x509_cert_url\": \"https://www.googleapis.com/robot/v1/metadata/x509/service-account-email\"\n}"), + resource.TestCheckResourceAttr(secondResName, "as_file", "false"), + resource.TestCheckResourceAttrSet(secondResName, "deployment_id"), + ), + }, + { + Config: cfgF(changeKeystoreSettingName), + Check: resource.ComposeAggregateTestCheckFunc( + checkESKeystoreResourceID(firstResName, ¤tID), + + resource.TestCheckResourceAttr(firstResName, "setting_name", "xpack.notification.slack.account.hello.secure_urla"), + resource.TestCheckResourceAttr(firstResName, "value", "hello2u"), + resource.TestCheckResourceAttr(firstResName, "as_file", "false"), + resource.TestCheckResourceAttrSet(firstResName, "deployment_id"), + + resource.TestCheckResourceAttr(secondResName, "setting_name", "gcs.client.secondary.credentials_file"), + resource.TestCheckResourceAttr(secondResName, "value", "{\n \"type\": \"service_account\",\n \"project_id\": \"project-id\",\n \"private_key_id\": \"key-id\",\n \"private_key\": \"-----BEGIN PRIVATE KEY-----\\nprivate-key\\n-----END PRIVATE KEY-----\\n\",\n \"client_email\": \"service-account-email\",\n \"client_id\": \"client-id\",\n \"auth_uri\": \"https://accounts.google.com/o/oauth2/auth\",\n \"token_uri\": \"https://accounts.google.com/o/oauth2/token\",\n \"auth_provider_x509_cert_url\": \"https://www.googleapis.com/oauth2/v1/certs\",\n \"client_x509_cert_url\": \"https://www.googleapis.com/robot/v1/metadata/x509/service-account-email\"\n}"), + resource.TestCheckResourceAttr(secondResName, "as_file", "false"), + resource.TestCheckResourceAttrSet(secondResName, "deployment_id"), + ), + }, + { + Config: cfgF(deleteAllKeystoreSettings), + Check: resource.ComposeAggregateTestCheckFunc( + checkNoKeystoreResourcesLeft(firstResName, secondResName), + func(current, previous *string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if *current == *previous { + return fmt.Errorf("%s id (%s) should not equal %s", firstResName, *current, *previous) + } + return nil + } + }(¤tID, &previousID), + ), + }, + }, + }) +} + +func checkESKeystoreResourceID(resourceName string, id *string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + *id = rs.Primary.ID + return nil + } +} + +func checkNoKeystoreResourcesLeft(resourceName ...string) resource.TestCheckFunc { + return func(s *terraform.State) error { + merr := multierror.NewPrefixed("found 'ec_deployment_elasticsearch_keystore' resources") + for _, resName := range resourceName { + if rs, ok := s.RootModule().Resources[resName]; ok { + merr = merr.Append(fmt.Errorf("found: %s with ID: %s", resName, rs.Primary.ID)) + } + } + + return merr.ErrorOrNil() + } +} diff --git a/ec/acc/testdata/deployment_elasticsearch_keystore_1.tf b/ec/acc/testdata/deployment_elasticsearch_keystore_1.tf new file mode 100644 index 000000000..094e23a3b --- /dev/null +++ b/ec/acc/testdata/deployment_elasticsearch_keystore_1.tf @@ -0,0 +1,32 @@ +data "ec_stack" "keystore" { + version_regex = "latest" + region = "%s" +} + +resource "ec_deployment" "keystore" { + name = "%s" + region = "%s" + version = data.ec_stack.keystore.version + deployment_template_id = "%s" + + elasticsearch { + topology { + id = "hot_content" + size = "1g" + zone_count = 1 + } + } +} + +resource "ec_deployment_elasticsearch_keystore" "test" { + deployment_id = ec_deployment.keystore.id + setting_name = "xpack.notification.slack.account.hello.secure_url" + value = "hella" +} + +resource "ec_deployment_elasticsearch_keystore" "gcs_creds" { + deployment_id = ec_deployment.keystore.id + setting_name = "gcs.client.secondary.credentials_file" + value = file("testdata/deployment_elasticsearch_keystore_creds.json") +} + diff --git a/ec/acc/testdata/deployment_elasticsearch_keystore_2.tf b/ec/acc/testdata/deployment_elasticsearch_keystore_2.tf new file mode 100644 index 000000000..4a04a7c2f --- /dev/null +++ b/ec/acc/testdata/deployment_elasticsearch_keystore_2.tf @@ -0,0 +1,31 @@ +data "ec_stack" "keystore" { + version_regex = "latest" + region = "%s" +} + +resource "ec_deployment" "keystore" { + name = "%s" + region = "%s" + version = data.ec_stack.keystore.version + deployment_template_id = "%s" + + elasticsearch { + topology { + id = "hot_content" + size = "1g" + zone_count = 1 + } + } +} + +resource "ec_deployment_elasticsearch_keystore" "test" { + deployment_id = ec_deployment.keystore.id + setting_name = "xpack.notification.slack.account.hello.secure_url" + value = "hello2u" +} + +resource "ec_deployment_elasticsearch_keystore" "gcs_creds" { + deployment_id = ec_deployment.keystore.id + setting_name = "gcs.client.secondary.credentials_file" + value = file("testdata/deployment_elasticsearch_keystore_creds.json") +} diff --git a/ec/acc/testdata/deployment_elasticsearch_keystore_3.tf b/ec/acc/testdata/deployment_elasticsearch_keystore_3.tf new file mode 100644 index 000000000..572e49b21 --- /dev/null +++ b/ec/acc/testdata/deployment_elasticsearch_keystore_3.tf @@ -0,0 +1,31 @@ +data "ec_stack" "keystore" { + version_regex = "latest" + region = "%s" +} + +resource "ec_deployment" "keystore" { + name = "%s" + region = "%s" + version = data.ec_stack.keystore.version + deployment_template_id = "%s" + + elasticsearch { + topology { + id = "hot_content" + size = "1g" + zone_count = 1 + } + } +} + +resource "ec_deployment_elasticsearch_keystore" "test" { + deployment_id = ec_deployment.keystore.id + setting_name = "xpack.notification.slack.account.hello.secure_urla" + value = "hello2u" +} + +resource "ec_deployment_elasticsearch_keystore" "gcs_creds" { + deployment_id = ec_deployment.keystore.id + setting_name = "gcs.client.secondary.credentials_file" + value = file("testdata/deployment_elasticsearch_keystore_creds.json") +} diff --git a/ec/acc/testdata/deployment_elasticsearch_keystore_4.tf b/ec/acc/testdata/deployment_elasticsearch_keystore_4.tf new file mode 100644 index 000000000..85c3f2fb4 --- /dev/null +++ b/ec/acc/testdata/deployment_elasticsearch_keystore_4.tf @@ -0,0 +1,19 @@ +data "ec_stack" "keystore" { + version_regex = "latest" + region = "%s" +} + +resource "ec_deployment" "keystore" { + name = "%s" + region = "%s" + version = data.ec_stack.keystore.version + deployment_template_id = "%s" + + elasticsearch { + topology { + id = "hot_content" + size = "1g" + zone_count = 1 + } + } +} diff --git a/ec/acc/testdata/deployment_elasticsearch_keystore_creds.json b/ec/acc/testdata/deployment_elasticsearch_keystore_creds.json new file mode 100644 index 000000000..337663782 --- /dev/null +++ b/ec/acc/testdata/deployment_elasticsearch_keystore_creds.json @@ -0,0 +1,12 @@ +{ + "type": "service_account", + "project_id": "project-id", + "private_key_id": "key-id", + "private_key": "-----BEGIN PRIVATE KEY-----\nprivate-key\n-----END PRIVATE KEY-----\n", + "client_email": "service-account-email", + "client_id": "client-id", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://accounts.google.com/o/oauth2/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/service-account-email" +} \ No newline at end of file diff --git a/ec/ecresource/elasticsearchkeystoreresource/create.go b/ec/ecresource/elasticsearchkeystoreresource/create.go new file mode 100644 index 000000000..bade94c14 --- /dev/null +++ b/ec/ecresource/elasticsearchkeystoreresource/create.go @@ -0,0 +1,52 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package elasticsearchkeystoreresource + +import ( + "context" + "strconv" + "strings" + + "github.com/elastic/cloud-sdk-go/pkg/api" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/elastic/cloud-sdk-go/pkg/api/deploymentapi/eskeystoreapi" +) + +// create will create an item in the Elasticsearch keystore +func create(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*api.API) + deploymentID := d.Get("deployment_id").(string) + settingName := d.Get("setting_name").(string) + + if _, err := eskeystoreapi.Update(eskeystoreapi.UpdateParams{ + API: client, + DeploymentID: deploymentID, + Contents: expandModel(d), + }); err != nil { + return diag.FromErr(err) + } + + d.SetId(hashID(deploymentID, settingName)) + return read(ctx, d, meta) +} + +func hashID(elem ...string) string { + return strconv.Itoa(schema.HashString(strings.Join(elem, "-"))) +} diff --git a/ec/ecresource/elasticsearchkeystoreresource/delete.go b/ec/ecresource/elasticsearchkeystoreresource/delete.go new file mode 100644 index 000000000..b6c6d7e63 --- /dev/null +++ b/ec/ecresource/elasticsearchkeystoreresource/delete.go @@ -0,0 +1,51 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package elasticsearchkeystoreresource + +import ( + "context" + + "github.com/elastic/cloud-sdk-go/pkg/api" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/elastic/cloud-sdk-go/pkg/api/deploymentapi/eskeystoreapi" +) + +// delete will delete an existing element in the Elasticsearch keystore +func delete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*api.API) + contents := expandModel(d) + + // Since we're using the Update API (PATCH method), we need to se the Value + // field to nil for the keystore setting to be unset. + if secret, ok := contents.Secrets[d.Get("setting_name").(string)]; ok { + secret.Value = nil + } + + if _, err := eskeystoreapi.Update(eskeystoreapi.UpdateParams{ + API: client, + DeploymentID: d.Get("deployment_id").(string), + Contents: contents, + }); err != nil { + return diag.FromErr(err) + } + + d.SetId("") + return read(ctx, d, meta) +} diff --git a/ec/ecresource/elasticsearchkeystoreresource/expanders.go b/ec/ecresource/elasticsearchkeystoreresource/expanders.go new file mode 100644 index 000000000..9ed695f8b --- /dev/null +++ b/ec/ecresource/elasticsearchkeystoreresource/expanders.go @@ -0,0 +1,47 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package elasticsearchkeystoreresource + +import ( + "encoding/json" + + "github.com/elastic/cloud-sdk-go/pkg/models" + "github.com/elastic/cloud-sdk-go/pkg/util/ec" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func expandModel(d *schema.ResourceData) *models.KeystoreContents { + var value interface{} + secretName := d.Get("setting_name").(string) + strVal := d.Get("value").(string) + + // Tries to unmarshal the contents of the value into an `interface{}`, + // if it fails, then the contents aren't a JSON object. + if err := json.Unmarshal([]byte(strVal), &value); err != nil { + value = strVal + } + + return &models.KeystoreContents{ + Secrets: map[string]models.KeystoreSecret{ + secretName: { + AsFile: ec.Bool(d.Get("as_file").(bool)), + Value: value, + }, + }, + } +} diff --git a/ec/ecresource/elasticsearchkeystoreresource/expanders_test.go b/ec/ecresource/elasticsearchkeystoreresource/expanders_test.go new file mode 100644 index 000000000..a1ebe37f8 --- /dev/null +++ b/ec/ecresource/elasticsearchkeystoreresource/expanders_test.go @@ -0,0 +1,107 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package elasticsearchkeystoreresource + +import ( + "testing" + + "github.com/elastic/cloud-sdk-go/pkg/api/mock" + "github.com/elastic/cloud-sdk-go/pkg/models" + "github.com/elastic/cloud-sdk-go/pkg/util/ec" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/stretchr/testify/assert" +) + +func Test_expandModel(t *testing.T) { + type args struct { + d *schema.ResourceData + } + tests := []struct { + name string + args args + want *models.KeystoreContents + }{ + { + name: "parses the resource with a string value", + args: args{d: newResourceData(t, resDataParams{ + ID: "some-random-id", + Resources: map[string]interface{}{ + "deployment_id": mock.ValidClusterID, + "setting_name": "my_secret", + "value": "supersecret", + }, + })}, + want: &models.KeystoreContents{ + Secrets: map[string]models.KeystoreSecret{ + "my_secret": { + AsFile: ec.Bool(false), + Value: "supersecret", + }, + }, + }, + }, + { + name: "parses the resource with a json formatted value", + args: args{d: newResourceData(t, resDataParams{ + ID: "some-random-id", + Resources: map[string]interface{}{ + "deployment_id": mock.ValidClusterID, + "setting_name": "my_secret", + "value": `{ + "type": "service_account", + "project_id": "project-id", + "private_key_id": "key-id", + "private_key": "-----BEGIN PRIVATE KEY-----\nprivate-key\n-----END PRIVATE KEY-----\n", + "client_email": "service-account-email", + "client_id": "client-id", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://accounts.google.com/o/oauth2/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/service-account-email" +}`, + "as_file": true, + }, + })}, + want: &models.KeystoreContents{ + Secrets: map[string]models.KeystoreSecret{ + "my_secret": { + AsFile: ec.Bool(true), + Value: map[string]interface{}{ + "type": "service_account", + "project_id": "project-id", + "private_key_id": "key-id", + "private_key": "-----BEGIN PRIVATE KEY-----\nprivate-key\n-----END PRIVATE KEY-----\n", + "client_email": "service-account-email", + "client_id": "client-id", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://accounts.google.com/o/oauth2/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/service-account-email", + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := expandModel(tt.args.d) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/ec/ecresource/elasticsearchkeystoreresource/read.go b/ec/ecresource/elasticsearchkeystoreresource/read.go new file mode 100644 index 000000000..beec83b44 --- /dev/null +++ b/ec/ecresource/elasticsearchkeystoreresource/read.go @@ -0,0 +1,70 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package elasticsearchkeystoreresource + +import ( + "context" + + "github.com/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/cloud-sdk-go/pkg/models" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/elastic/cloud-sdk-go/pkg/api/deploymentapi/eskeystoreapi" +) + +// read queries the remote Elasticsearch keystore state and updates the local state. +func read(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var client = meta.(*api.API) + deploymentID := d.Get("deployment_id").(string) + + res, err := eskeystoreapi.Get(eskeystoreapi.GetParams{ + API: client, + DeploymentID: deploymentID, + }) + if err != nil { + return diag.FromErr(err) + } + + if err := modelToState(d, res); err != nil { + return diag.FromErr(err) + } + + return nil +} + +// This modelToState function is a little different than others in that it does +// not set any other fields than "as_file". This is because the "value" is not +// returned by the API for obvious reasons and thus we cannot reconcile that the +// value of the secret is the same in the remote as it is in the configuration. +func modelToState(d *schema.ResourceData, res *models.KeystoreContents) error { + if secret, ok := res.Secrets[d.Get("setting_name").(string)]; ok { + if secret.AsFile != nil { + if err := d.Set("as_file", *secret.AsFile); err != nil { + return err + } + } + return nil + } + + // When the secret is not found in the returned map of secrets, set the id + // to an empty string so that the resource is marked as destroyed. Would + // only happen if secrets are removed from the underlying Deployment. + d.SetId("") + return nil +} diff --git a/ec/ecresource/elasticsearchkeystoreresource/read_test.go b/ec/ecresource/elasticsearchkeystoreresource/read_test.go new file mode 100644 index 000000000..e8b568d9e --- /dev/null +++ b/ec/ecresource/elasticsearchkeystoreresource/read_test.go @@ -0,0 +1,120 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package elasticsearchkeystoreresource + +import ( + "testing" + + "github.com/elastic/cloud-sdk-go/pkg/api/mock" + "github.com/elastic/cloud-sdk-go/pkg/models" + "github.com/elastic/cloud-sdk-go/pkg/util/ec" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/stretchr/testify/assert" +) + +func Test_modelToState(t *testing.T) { + esKeystoreSchemaArg := schema.TestResourceDataRaw(t, newSchema(), map[string]interface{}{ + "deployment_id": mock.ValidClusterID, + "setting_name": "my_secret", + "value": "supersecret", + "as_file": false, // This field is overridden. + }) + esKeystoreSchemaArg.SetId(mock.ValidClusterID) + + esKeystoreSchemaArgMissing := schema.TestResourceDataRaw(t, newSchema(), map[string]interface{}{ + "deployment_id": mock.ValidClusterID, + "setting_name": "my_secret", + "value": "supersecret", + "as_file": false, + }) + esKeystoreSchemaArgMissing.SetId(mock.ValidClusterID) + + type args struct { + d *schema.ResourceData + res *models.KeystoreContents + } + tests := []struct { + name string + args args + want *schema.ResourceData + err error + }{ + { + name: "flattens the keystore secret (not really since the value is not returned)", + args: args{ + d: esKeystoreSchemaArg, + res: &models.KeystoreContents{ + Secrets: map[string]models.KeystoreSecret{ + "my_secret": { + AsFile: ec.Bool(true), + }, + "some_other_secret": { + AsFile: ec.Bool(false), + }, + }, + }, + }, + want: newResourceData(t, resDataParams{ + ID: mock.ValidClusterID, + Resources: map[string]interface{}{ + "deployment_id": mock.ValidClusterID, + "setting_name": "my_secret", + "value": "supersecret", + "as_file": true, + }, + }), + }, + { + name: "unsets the ID when our secret is not in the returned list of secrets", + args: args{ + d: esKeystoreSchemaArgMissing, + res: &models.KeystoreContents{ + Secrets: map[string]models.KeystoreSecret{ + "my_other_secret": { + AsFile: ec.Bool(true), + }, + "some_other_secret": { + AsFile: ec.Bool(false), + }, + }, + }, + }, + want: newResourceData(t, resDataParams{ + Resources: map[string]interface{}{}, + }), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := modelToState(tt.args.d, tt.args.res) + if tt.err != nil || err != nil { + assert.EqualError(t, err, tt.err.Error()) + } else { + assert.NoError(t, err) + } + + wantState := tt.want.State() + gotState := tt.args.d.State() + + if wantState != nil && gotState != nil { + assert.Equal(t, wantState.Attributes, gotState.Attributes) + return + } + }) + } +} diff --git a/ec/ecresource/elasticsearchkeystoreresource/resource.go b/ec/ecresource/elasticsearchkeystoreresource/resource.go new file mode 100644 index 000000000..70fefacb0 --- /dev/null +++ b/ec/ecresource/elasticsearchkeystoreresource/resource.go @@ -0,0 +1,41 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package elasticsearchkeystoreresource + +import ( + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// Resource returns the ec_deployment_elasticsearch_keystore resource schema. +func Resource() *schema.Resource { + return &schema.Resource{ + Description: "Elastic Cloud deployment Elasticsearch keystore", + Schema: newSchema(), + + CreateContext: create, + ReadContext: read, + UpdateContext: update, + DeleteContext: delete, + + Timeouts: &schema.ResourceTimeout{ + Default: schema.DefaultTimeout(5 * time.Minute), + }, + } +} diff --git a/ec/ecresource/elasticsearchkeystoreresource/schema.go b/ec/ecresource/elasticsearchkeystoreresource/schema.go new file mode 100644 index 000000000..6a4e4af97 --- /dev/null +++ b/ec/ecresource/elasticsearchkeystoreresource/schema.go @@ -0,0 +1,50 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package elasticsearchkeystoreresource + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func newSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "deployment_id": { + Type: schema.TypeString, + Description: `Required deployment ID of the Deployment that holds the Elasticsearch cluster where the keystore setting will be written to`, + Required: true, + ForceNew: true, + }, + "setting_name": { + Type: schema.TypeString, + Description: "Required name for the keystore setting, if the setting already exists in the Elasticsearch cluster, it will be overridden", + ForceNew: true, + Required: true, + }, + "value": { + Type: schema.TypeString, + Description: "Required value of this setting. This can either be a string or a JSON object that is stored as a JSON string in the keystore.", + Sensitive: true, + Required: true, + }, + "as_file": { + Type: schema.TypeBool, + Description: "Optionally stores the remote keystore setting as a file. The default is false, which stores the keystore setting as string when value is a plain string", + Optional: true, + }, + } +} diff --git a/ec/ecresource/elasticsearchkeystoreresource/testutils.go b/ec/ecresource/elasticsearchkeystoreresource/testutils.go new file mode 100644 index 000000000..6fc80835b --- /dev/null +++ b/ec/ecresource/elasticsearchkeystoreresource/testutils.go @@ -0,0 +1,36 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package elasticsearchkeystoreresource + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +type resDataParams struct { + Resources map[string]interface{} + ID string +} + +func newResourceData(t *testing.T, params resDataParams) *schema.ResourceData { + raw := schema.TestResourceDataRaw(t, newSchema(), params.Resources) + raw.SetId(params.ID) + + return raw +} diff --git a/ec/ecresource/elasticsearchkeystoreresource/update.go b/ec/ecresource/elasticsearchkeystoreresource/update.go new file mode 100644 index 000000000..2cc190a5e --- /dev/null +++ b/ec/ecresource/elasticsearchkeystoreresource/update.go @@ -0,0 +1,45 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package elasticsearchkeystoreresource + +import ( + "context" + + "github.com/elastic/cloud-sdk-go/pkg/api" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/elastic/cloud-sdk-go/pkg/api/deploymentapi/eskeystoreapi" +) + +// update will update an existing element in the Elasticsearch keystore +func update(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var client = meta.(*api.API) + deploymentID := d.Get("deployment_id").(string) + + _, err := eskeystoreapi.Update(eskeystoreapi.UpdateParams{ + API: client, + DeploymentID: deploymentID, + Contents: expandModel(d), + }) + if err != nil { + return diag.FromErr(err) + } + + return read(ctx, d, meta) +} diff --git a/ec/provider.go b/ec/provider.go index 43f5562c1..b88f654bf 100644 --- a/ec/provider.go +++ b/ec/provider.go @@ -29,6 +29,7 @@ import ( "github.com/elastic/terraform-provider-ec/ec/ecdatasource/deploymentsdatasource" "github.com/elastic/terraform-provider-ec/ec/ecdatasource/stackdatasource" "github.com/elastic/terraform-provider-ec/ec/ecresource/deploymentresource" + "github.com/elastic/terraform-provider-ec/ec/ecresource/elasticsearchkeystoreresource" "github.com/elastic/terraform-provider-ec/ec/ecresource/extensionresource" "github.com/elastic/terraform-provider-ec/ec/ecresource/trafficfilterassocresource" "github.com/elastic/terraform-provider-ec/ec/ecresource/trafficfilterresource" @@ -70,6 +71,7 @@ func Provider() *schema.Provider { }, ResourcesMap: map[string]*schema.Resource{ "ec_deployment": deploymentresource.Resource(), + "ec_deployment_elasticsearch_keystore": elasticsearchkeystoreresource.Resource(), "ec_deployment_traffic_filter": trafficfilterresource.Resource(), "ec_deployment_traffic_filter_association": trafficfilterassocresource.Resource(), "ec_deployment_extension": extensionresource.Resource(), diff --git a/examples/deployment/deployment.tf b/examples/deployment/deployment.tf index 7c6d74301..f722b453f 100644 --- a/examples/deployment/deployment.tf +++ b/examples/deployment/deployment.tf @@ -24,7 +24,6 @@ resource "ec_deployment" "example_minimal" { # Optional name. name = "my_example_deployment" - # Mandatory fields region = "us-east-1" version = data.ec_stack.latest.version deployment_template_id = "aws-io-optimized-v2" @@ -48,4 +47,4 @@ resource "ec_deployment" "example_minimal" { size = "0.5g" } } -} +} \ No newline at end of file diff --git a/examples/deployment/outputs.tf b/examples/deployment/outputs.tf index 97d36ff8f..0d63eb92d 100644 --- a/examples/deployment/outputs.tf +++ b/examples/deployment/outputs.tf @@ -1,7 +1,3 @@ -output "deployment_id" { - value = ec_deployment.example_minimal.id -} - output "elasticsearch_version" { value = ec_deployment.example_minimal.version }