diff --git a/CHANGELOG.md b/CHANGELOG.md index ea4cdf1f..4d6d28a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ ## [Unreleased] - Allow `elasticstack_kibana_alerting_rule` to be used without Elasticsearch being configured. ([#869](https://github.com/elastic/terraform-provider-elasticstack/pull/869)) -- Add resource `elasticstack_elasticsearch_data_stream_lifecycle` ([838](https://github.com/elastic/terraform-provider-elasticstack/issues/838)) +- Add resource `elasticstack_elasticsearch_data_stream_lifecycle` ([#838](https://github.com/elastic/terraform-provider-elasticstack/issues/838)) +- Ensure API keys are not replaced when upgrading from 0.11.9 or earlier. ([#875](https://github.com/elastic/terraform-provider-elasticstack/pull/875)) ## [0.11.10] - 2024-10-23 diff --git a/internal/clients/elasticsearch/security.go b/internal/clients/elasticsearch/security.go index d0ece033..11800549 100644 --- a/internal/clients/elasticsearch/security.go +++ b/internal/clients/elasticsearch/security.go @@ -341,7 +341,7 @@ func UpdateApiKey(apiClient *clients.ApiClient, apikey models.ApiKey) fwdiag.Dia return utils.FrameworkDiagFromError(err) } defer res.Body.Close() - if diags := utils.CheckError(res, "Unable to create apikey"); diags.HasError() { + if diags := utils.CheckError(res, "Unable to update apikey"); diags.HasError() { return utils.FrameworkDiagsFromSDK(diags) } diff --git a/internal/elasticsearch/security/api_key/acc_test.go b/internal/elasticsearch/security/api_key/acc_test.go index dc6cfc12..7c17b70c 100644 --- a/internal/elasticsearch/security/api_key/acc_test.go +++ b/internal/elasticsearch/security/api_key/acc_test.go @@ -241,6 +241,60 @@ func SkipWhenApiKeysAreNotSupportedOrRestrictionsAreSupported(minApiKeySupported } } +func TestAccResourceSecurityApiKeyFromSDK(t *testing.T) { + // generate a random name + apiKeyName := sdkacctest.RandStringFromCharSet(10, sdkacctest.CharSetAlphaNum) + var initialApiKey string + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: checkResourceSecurityApiKeyDestroy, + Steps: []resource.TestStep{ + { + // Create the api_key with the last provider version where the api_key resource was built on the SDK + ExternalProviders: map[string]resource.ExternalProvider{ + "elasticstack": { + Source: "elastic/elasticstack", + VersionConstraint: "0.11.9", + }, + }, + ProtoV6ProviderFactories: acctest.Providers, + SkipFunc: versionutils.CheckIfVersionIsUnsupported(api_key.MinVersion), + Config: testAccResourceSecurityApiKeyWithoutExpiration(apiKeyName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_elasticsearch_security_api_key.test", "name", apiKeyName), + resource.TestCheckResourceAttrSet("elasticstack_elasticsearch_security_api_key.test", "role_descriptors"), + resource.TestCheckResourceAttrSet("elasticstack_elasticsearch_security_api_key.test", "encoded"), + resource.TestCheckResourceAttrSet("elasticstack_elasticsearch_security_api_key.test", "id"), + resource.TestCheckResourceAttrWith("elasticstack_elasticsearch_security_api_key.test", "api_key", func(value string) error { + initialApiKey = value + + if value == "" { + return fmt.Errorf("expected api_key to be non-empty") + } + + return nil + }), + ), + }, + { + ProtoV6ProviderFactories: acctest.Providers, + SkipFunc: versionutils.CheckIfVersionIsUnsupported(api_key.MinVersion), + Config: testAccResourceSecurityApiKeyWithoutExpiration(apiKeyName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrWith("elasticstack_elasticsearch_security_api_key.test", "api_key", func(value string) error { + if value != initialApiKey { + return fmt.Errorf("expected api_key to be unchanged") + } + + return nil + }), + ), + }, + }, + }) +} + func testAccResourceSecurityApiKeyCreate(apiKeyName string) string { return fmt.Sprintf(` provider "elasticstack" { @@ -291,6 +345,29 @@ resource "elasticstack_elasticsearch_security_api_key" "test" { `, apiKeyName) } +func testAccResourceSecurityApiKeyWithoutExpiration(apiKeyName string) string { + return fmt.Sprintf(` +provider "elasticstack" { + elasticsearch {} +} + +resource "elasticstack_elasticsearch_security_api_key" "test" { + name = "%s" + + role_descriptors = jsonencode({ + role-a = { + cluster = ["all"] + indices = [{ + names = ["index-a*"] + privileges = ["read"] + allow_restricted_indices = false + }] + } + }) +} + `, apiKeyName) +} + func testAccResourceSecurityApiKeyRemoteIndices(apiKeyName string) string { return fmt.Sprintf(` provider "elasticstack" { diff --git a/internal/elasticsearch/security/api_key/resource.go b/internal/elasticsearch/security/api_key/resource.go index 3405fd30..5cc9027e 100644 --- a/internal/elasticsearch/security/api_key/resource.go +++ b/internal/elasticsearch/security/api_key/resource.go @@ -14,6 +14,7 @@ import ( // Ensure provider defined types fully satisfy framework interfaces var _ resource.Resource = &Resource{} var _ resource.ResourceWithConfigure = &Resource{} +var _ resource.ResourceWithUpgradeState = &Resource{} var ( MinVersion = version.Must(version.NewVersion("8.0.0")) // Enabled in 8.0 MinVersionWithUpdate = version.Must(version.NewVersion("8.4.0")) diff --git a/internal/elasticsearch/security/api_key/schema.go b/internal/elasticsearch/security/api_key/schema.go index 5a5895fe..f2d9923b 100644 --- a/internal/elasticsearch/security/api_key/schema.go +++ b/internal/elasticsearch/security/api_key/schema.go @@ -16,12 +16,15 @@ import ( providerschema "github.com/elastic/terraform-provider-elasticstack/internal/schema" ) +const currentSchemaVersion int64 = 1 + func (r *Resource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = r.getSchema() + resp.Schema = r.getSchema(currentSchemaVersion) } -func (r *Resource) getSchema() schema.Schema { +func (r *Resource) getSchema(version int64) schema.Schema { return schema.Schema{ + Version: version, Description: "Creates an API key for access without requiring basic authentication. See, https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html", Blocks: map[string]schema.Block{ "elasticsearch_connection": providerschema.GetEsFWConnectionBlock("elasticsearch_connection", false), diff --git a/internal/elasticsearch/security/api_key/state_upgrade.go b/internal/elasticsearch/security/api_key/state_upgrade.go new file mode 100644 index 00000000..b9072fdd --- /dev/null +++ b/internal/elasticsearch/security/api_key/state_upgrade.go @@ -0,0 +1,30 @@ +package api_key + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func (r *Resource) UpgradeState(context.Context) map[int64]resource.StateUpgrader { + return map[int64]resource.StateUpgrader{ + 0: { + PriorSchema: utils.Pointer(r.getSchema(0)), + StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) { + var model tfModel + resp.Diagnostics.Append(req.State.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + if utils.IsKnown(model.Expiration) && model.Expiration.ValueString() == "" { + model.Expiration = basetypes.NewStringNull() + } + + resp.State.Set(ctx, model) + }, + }, + } +}