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..064bff57 100644 --- a/internal/elasticsearch/security/api_key/acc_test.go +++ b/internal/elasticsearch/security/api_key/acc_test.go @@ -241,6 +241,82 @@ 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.TestCheckResourceAttrWith("elasticstack_elasticsearch_security_api_key.test", "role_descriptors", func(testValue string) error { + var testRoleDescriptor map[string]models.ApiKeyRoleDescriptor + if err := json.Unmarshal([]byte(testValue), &testRoleDescriptor); err != nil { + return err + } + + expectedRoleDescriptor := map[string]models.ApiKeyRoleDescriptor{ + "role-a": { + Cluster: []string{"all"}, + Indices: []models.IndexPerms{{ + Names: []string{"index-a*"}, + Privileges: []string{"read"}, + AllowRestrictedIndices: utils.Pointer(false), + }}, + }, + } + + if !reflect.DeepEqual(testRoleDescriptor, expectedRoleDescriptor) { + return fmt.Errorf("%v doesn't match %v", testRoleDescriptor, expectedRoleDescriptor) + } + + return nil + }), + 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 + }), + resource.TestCheckResourceAttrSet("elasticstack_elasticsearch_security_api_key.test", "encoded"), + resource.TestCheckResourceAttrSet("elasticstack_elasticsearch_security_api_key.test", "id"), + ), + }, + { + 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 +367,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..6929b753 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), @@ -67,6 +70,14 @@ func (r *Resource) getSchema() schema.Schema { Optional: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), + // stringplanmodifier.RequiresReplaceIf( + // func(ctx context.Context, req planmodifier.StringRequest, resp *stringplanmodifier.RequiresReplaceIfFuncResponse) { + // // Checking ValueString ensures that expiration does not trigger replacement for "" != null + // resp.RequiresReplace = req.ConfigValue.ValueString() != req.StateValue.ValueString() + // }, + // "Requires replace if the configured expiration value changes", + // "Requires replace if the configured expiration value changes", + // ), }, }, "expiration_timestamp": schema.Int64Attribute{ 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) + }, + }, + } +}