diff --git a/docs/resources/deployment.md b/docs/resources/deployment.md index 2d32b1d24..8f6b80b6a 100644 --- a/docs/resources/deployment.md +++ b/docs/resources/deployment.md @@ -266,6 +266,7 @@ resource "ec_deployment" "ccs" { - `name` (String) Name for the deployment - `observability` (Attributes) Observability settings that you can set to ship logs and metrics to a deployment. The target deployment can also be the current deployment itself by setting observability.deployment_id to `self`. (see [below for nested schema](#nestedatt--observability)) - `request_id` (String) Request ID to set when you create the deployment. Use it only when previous attempts return an error and `request_id` is returned as part of the error. +- `reset_elasticsearch_password` (Boolean) Explicitly resets the elasticsearch_password when true - `tags` (Map of String) Optional map of deployment tags - `traffic_filter` (Set of String) List of traffic filters rule identifiers that will be applied to the deployment. diff --git a/ec/acc/deployment_basic_test.go b/ec/acc/deployment_basic_test.go index d39415191..fd69fc82d 100644 --- a/ec/acc/deployment_basic_test.go +++ b/ec/acc/deployment_basic_test.go @@ -18,12 +18,14 @@ package acc import ( + "errors" "fmt" "os" "testing" "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 TestAccDeployment_basic_tf(t *testing.T) { @@ -33,14 +35,17 @@ func TestAccDeployment_basic_tf(t *testing.T) { randomAlias := "alias" + acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) trafficFilterCfg := "testdata/deployment_basic_with_traffic_filter_2.tf" trafficFilterUpdateCfg := "testdata/deployment_basic_with_traffic_filter_3.tf" + resetPasswordCfg := "testdata/deployment_basic_reset_password.tf" cfg := fixtureAccDeploymentResourceBasicWithAppsAlias(t, startCfg, randomAlias, randomName, getRegion(), defaultTemplate) cfgWithTrafficFilter := fixtureAccDeploymentResourceBasicWithTF(t, trafficFilterCfg, randomName, getRegion(), defaultTemplate) cfgWithTrafficFilterUpdate := fixtureAccDeploymentResourceBasicWithTF(t, trafficFilterUpdateCfg, randomName, getRegion(), defaultTemplate) + cfgResetPassword := fixtureAccDeploymentResourceBasicWithAppsAlias(t, resetPasswordCfg, randomAlias, randomName, getRegion(), defaultTemplate) deploymentVersion, err := latestStackVersion() if err != nil { t.Fatal(err) } + elasticsearchPassword := "" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProviderFactory, @@ -76,12 +81,47 @@ func TestAccDeployment_basic_tf(t *testing.T) { Config: cfg, Check: checkBasicDeploymentResource(resName, randomName, deploymentVersion, resource.TestCheckResourceAttr(resName, "traffic_filter.#", "0"), + func(s *terraform.State) error { + pw, ok := captureElasticsearchPassword(s, resName) + if !ok { + return errors.New("unable to capture current elasticsearch_password") + } + + elasticsearchPassword = pw + return nil + }, + ), + }, + // Reset the elasticsearch_password + { + Config: cfgResetPassword, + ExpectNonEmptyPlan: true, // reset_elasticsearch_password will always result in a non-empty plan + Check: checkBasicDeploymentResource(resName, randomName, deploymentVersion, + resource.TestCheckResourceAttr(resName, "traffic_filter.#", "0"), + func(s *terraform.State) error { + currentPw, ok := captureElasticsearchPassword(s, resName) + if !ok { + return errors.New("unable to capture current elasticsearch_password") + } + + if currentPw == elasticsearchPassword { + return fmt.Errorf("expected elasticsearch_password to be reset: %s == %s", elasticsearchPassword, currentPw) + } + + return nil + }, ), }, }, }) } +func captureElasticsearchPassword(s *terraform.State, resName string) (string, bool) { + res := s.RootModule().Resources[resName] + pw, ok := res.Primary.Attributes["elasticsearch_password"] + return pw, ok +} + func TestAccDeployment_basic_config(t *testing.T) { resName := "ec_deployment.basic" randomName := prefix + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) diff --git a/ec/acc/testdata/deployment_basic_reset_password.tf b/ec/acc/testdata/deployment_basic_reset_password.tf new file mode 100644 index 000000000..7d29fa80c --- /dev/null +++ b/ec/acc/testdata/deployment_basic_reset_password.tf @@ -0,0 +1,33 @@ +data "ec_stack" "latest" { + version_regex = "latest" + region = "%s" +} + +resource "ec_deployment" "basic" { + alias = "%s" + name = "%s" + region = "%s" + version = data.ec_stack.latest.version + deployment_template_id = "%s" + + elasticsearch = { + hot = { + size = "1g" + autoscaling = {} + } + } + + kibana = { + instance_configuration_id = "%s" + } + + apm = { + instance_configuration_id = "%s" + } + + enterprise_search = { + instance_configuration_id = "%s" + } + + reset_elasticsearch_password = true +} diff --git a/ec/ecresource/deploymentresource/deployment/v2/deployment_create_payload.go b/ec/ecresource/deploymentresource/deployment/v2/deployment_create_payload.go index 8bc050244..5403f40f4 100644 --- a/ec/ecresource/deploymentresource/deployment/v2/deployment_create_payload.go +++ b/ec/ecresource/deploymentresource/deployment/v2/deployment_create_payload.go @@ -38,24 +38,25 @@ import ( ) type DeploymentTF struct { - Id types.String `tfsdk:"id"` - Alias types.String `tfsdk:"alias"` - Version types.String `tfsdk:"version"` - Region types.String `tfsdk:"region"` - DeploymentTemplateId types.String `tfsdk:"deployment_template_id"` - Name types.String `tfsdk:"name"` - RequestId types.String `tfsdk:"request_id"` - ElasticsearchUsername types.String `tfsdk:"elasticsearch_username"` - ElasticsearchPassword types.String `tfsdk:"elasticsearch_password"` - ApmSecretToken types.String `tfsdk:"apm_secret_token"` - TrafficFilter types.Set `tfsdk:"traffic_filter"` - Tags types.Map `tfsdk:"tags"` - Elasticsearch types.Object `tfsdk:"elasticsearch"` - Kibana types.Object `tfsdk:"kibana"` - Apm types.Object `tfsdk:"apm"` - IntegrationsServer types.Object `tfsdk:"integrations_server"` - EnterpriseSearch types.Object `tfsdk:"enterprise_search"` - Observability types.Object `tfsdk:"observability"` + Id types.String `tfsdk:"id"` + Alias types.String `tfsdk:"alias"` + Version types.String `tfsdk:"version"` + Region types.String `tfsdk:"region"` + DeploymentTemplateId types.String `tfsdk:"deployment_template_id"` + Name types.String `tfsdk:"name"` + RequestId types.String `tfsdk:"request_id"` + ElasticsearchUsername types.String `tfsdk:"elasticsearch_username"` + ElasticsearchPassword types.String `tfsdk:"elasticsearch_password"` + ApmSecretToken types.String `tfsdk:"apm_secret_token"` + TrafficFilter types.Set `tfsdk:"traffic_filter"` + Tags types.Map `tfsdk:"tags"` + Elasticsearch types.Object `tfsdk:"elasticsearch"` + Kibana types.Object `tfsdk:"kibana"` + Apm types.Object `tfsdk:"apm"` + IntegrationsServer types.Object `tfsdk:"integrations_server"` + EnterpriseSearch types.Object `tfsdk:"enterprise_search"` + Observability types.Object `tfsdk:"observability"` + ResetElasticsearchPassword types.Bool `tfsdk:"reset_elasticsearch_password"` } func (dep DeploymentTF) CreateRequest(ctx context.Context, client *api.API) (*models.DeploymentCreateRequest, diag.Diagnostics) { diff --git a/ec/ecresource/deploymentresource/deployment/v2/deployment_read.go b/ec/ecresource/deploymentresource/deployment/v2/deployment_read.go index b49fcd4ae..4d6e2cd53 100644 --- a/ec/ecresource/deploymentresource/deployment/v2/deployment_read.go +++ b/ec/ecresource/deploymentresource/deployment/v2/deployment_read.go @@ -39,24 +39,25 @@ import ( ) type Deployment struct { - Id string `tfsdk:"id"` - Alias string `tfsdk:"alias"` - Version string `tfsdk:"version"` - Region string `tfsdk:"region"` - DeploymentTemplateId string `tfsdk:"deployment_template_id"` - Name string `tfsdk:"name"` - RequestId string `tfsdk:"request_id"` - ElasticsearchUsername string `tfsdk:"elasticsearch_username"` - ElasticsearchPassword string `tfsdk:"elasticsearch_password"` - ApmSecretToken *string `tfsdk:"apm_secret_token"` - TrafficFilter []string `tfsdk:"traffic_filter"` - Tags map[string]string `tfsdk:"tags"` - Elasticsearch *elasticsearchv2.Elasticsearch `tfsdk:"elasticsearch"` - Kibana *kibanav2.Kibana `tfsdk:"kibana"` - Apm *apmv2.Apm `tfsdk:"apm"` - IntegrationsServer *integrationsserverv2.IntegrationsServer `tfsdk:"integrations_server"` - EnterpriseSearch *enterprisesearchv2.EnterpriseSearch `tfsdk:"enterprise_search"` - Observability *observabilityv2.Observability `tfsdk:"observability"` + Id string `tfsdk:"id"` + Alias string `tfsdk:"alias"` + Version string `tfsdk:"version"` + Region string `tfsdk:"region"` + DeploymentTemplateId string `tfsdk:"deployment_template_id"` + Name string `tfsdk:"name"` + RequestId string `tfsdk:"request_id"` + ElasticsearchUsername string `tfsdk:"elasticsearch_username"` + ElasticsearchPassword string `tfsdk:"elasticsearch_password"` + ApmSecretToken *string `tfsdk:"apm_secret_token"` + TrafficFilter []string `tfsdk:"traffic_filter"` + Tags map[string]string `tfsdk:"tags"` + Elasticsearch *elasticsearchv2.Elasticsearch `tfsdk:"elasticsearch"` + Kibana *kibanav2.Kibana `tfsdk:"kibana"` + Apm *apmv2.Apm `tfsdk:"apm"` + IntegrationsServer *integrationsserverv2.IntegrationsServer `tfsdk:"integrations_server"` + EnterpriseSearch *enterprisesearchv2.EnterpriseSearch `tfsdk:"enterprise_search"` + Observability *observabilityv2.Observability `tfsdk:"observability"` + ResetElasticsearchPassword *bool `tfsdk:"reset_elasticsearch_password"` } // Nullify Elasticsearch topologies that have zero size and are not specified in plan diff --git a/ec/ecresource/deploymentresource/deployment/v2/schema.go b/ec/ecresource/deploymentresource/deployment/v2/schema.go index da8a05bc0..b76e8eefa 100644 --- a/ec/ecresource/deploymentresource/deployment/v2/schema.go +++ b/ec/ecresource/deploymentresource/deployment/v2/schema.go @@ -18,6 +18,9 @@ package v2 import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -102,6 +105,7 @@ func DeploymentSchema() tfsdk.Schema { Sensitive: true, PlanModifiers: tfsdk.AttributePlanModifiers{ resource.UseStateForUnknown(), + setUnknownIfResetPasswordIsTrue{}, }, }, "apm_secret_token": { @@ -123,6 +127,11 @@ func DeploymentSchema() tfsdk.Schema { }, Optional: true, }, + "reset_elasticsearch_password": { + Description: "Explicitly resets the elasticsearch_password when true", + Type: types.BoolType, + Optional: true, + }, "elasticsearch": elasticsearchv2.ElasticsearchSchema(), "kibana": kibanav2.KibanaSchema(), "apm": apmv2.ApmSchema(), @@ -132,3 +141,36 @@ func DeploymentSchema() tfsdk.Schema { }, } } + +type setUnknownIfResetPasswordIsTrue struct{} + +var _ tfsdk.AttributePlanModifier = setUnknownIfResetPasswordIsTrue{} + +func (m setUnknownIfResetPasswordIsTrue) Description(ctx context.Context) string { + return m.MarkdownDescription(ctx) +} + +func (m setUnknownIfResetPasswordIsTrue) MarkdownDescription(ctx context.Context) string { + return "Sets the planned value to unknown if the reset_elasticsearch_password config value is true" +} + +func (m setUnknownIfResetPasswordIsTrue) Modify(ctx context.Context, req tfsdk.ModifyAttributePlanRequest, resp *tfsdk.ModifyAttributePlanResponse) { + if resp.AttributePlan == nil || req.AttributeConfig == nil { + return + } + + // if the config is the unknown value, use the unknown value otherwise, interpolation gets messed up + if req.AttributeConfig.IsUnknown() { + return + } + + var isResetting *bool + resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("reset_elasticsearch_password"), &isResetting)...) + if resp.Diagnostics.HasError() { + return + } + + if isResetting != nil && *isResetting { + resp.AttributePlan = types.String{Unknown: true} + } +} diff --git a/ec/ecresource/deploymentresource/read.go b/ec/ecresource/deploymentresource/read.go index 97ed59b4f..df5832b9f 100644 --- a/ec/ecresource/deploymentresource/read.go +++ b/ec/ecresource/deploymentresource/read.go @@ -154,6 +154,9 @@ func (r *Resource) read(ctx context.Context, id string, state *deploymentv2.Depl } deployment.RequestId = base.RequestId.Value + if !base.ResetElasticsearchPassword.IsNull() && !base.ResetElasticsearchPassword.IsUnknown() { + deployment.ResetElasticsearchPassword = &base.ResetElasticsearchPassword.Value + } deployment.SetCredentialsIfEmpty(state) diff --git a/ec/ecresource/deploymentresource/update.go b/ec/ecresource/deploymentresource/update.go index ffc66d04b..597ea968a 100644 --- a/ec/ecresource/deploymentresource/update.go +++ b/ec/ecresource/deploymentresource/update.go @@ -22,6 +22,7 @@ import ( "github.com/elastic/cloud-sdk-go/pkg/api" "github.com/elastic/cloud-sdk-go/pkg/api/deploymentapi" + "github.com/elastic/cloud-sdk-go/pkg/api/deploymentapi/depresourceapi" "github.com/elastic/cloud-sdk-go/pkg/api/deploymentapi/trafficfilterapi" v2 "github.com/elastic/terraform-provider-ec/ec/ecresource/deploymentresource/deployment/v2" "github.com/elastic/terraform-provider-ec/ec/internal/util" @@ -77,7 +78,6 @@ func (r *Resource) Update(ctx context.Context, req resource.UpdateRequest, resp resp.Diagnostics.Append(v2.HandleRemoteClusters(ctx, r.client, plan.Id.Value, plan.Elasticsearch)...) deployment, diags := r.read(ctx, plan.Id.Value, &state, &plan, res.Resources) - resp.Diagnostics.Append(diags...) if deployment == nil { @@ -86,9 +86,35 @@ func (r *Resource) Update(ctx context.Context, req resource.UpdateRequest, resp return } + if plan.ResetElasticsearchPassword.Value { + newPassword, diags := r.ResetElasticsearchPassword(plan.Id.Value, *deployment.Elasticsearch.RefId) + if resp.Diagnostics.Append(diags...); resp.Diagnostics.HasError() { + return + } + + deployment.ElasticsearchPassword = newPassword + } + resp.Diagnostics.Append(resp.State.Set(ctx, deployment)...) } +func (r *Resource) ResetElasticsearchPassword(deploymentID string, refID string) (string, diag.Diagnostics) { + var diags diag.Diagnostics + + resetResp, err := depresourceapi.ResetElasticsearchPassword(depresourceapi.ResetElasticsearchPasswordParams{ + API: r.client, + ID: deploymentID, + RefID: refID, + }) + + if err != nil { + diags.AddError("failed to reset elasticsearch password", err.Error()) + return "", diags + } + + return *resetResp.Password, diags +} + func HandleTrafficFilterChange(ctx context.Context, client *api.API, plan, state v2.DeploymentTF) diag.Diagnostics { if plan.TrafficFilter.IsNull() || plan.TrafficFilter.Equal(state.TrafficFilter) { return nil diff --git a/ec/requests.log b/ec/requests.log new file mode 100644 index 000000000..e69de29bb