diff --git a/.github/workflows/terraform_provider.yml b/.github/workflows/terraform_provider.yml index 5c1acfc3..6f3d5d3d 100644 --- a/.github/workflows/terraform_provider.yml +++ b/.github/workflows/terraform_provider.yml @@ -110,7 +110,7 @@ jobs: env: REDISCLOUD_ACCESS_KEY: ${{ secrets.REDISCLOUD_ACCESS_KEY_QA }} REDISCLOUD_SECRET_KEY: ${{ secrets.REDISCLOUD_SECRET_KEY_QA }} - REDISCLOUD_URL: https://api-cloudapi.qa.redislabs.com/v1 + REDISCLOUD_URL: https://api-k8s-cloudapi.qa.redislabs.com/v1 AWS_TEST_CLOUD_ACCOUNT_NAME: "${{ secrets.AWS_TEST_CLOUD_ACCOUNT_NAME }}" AWS_PEERING_REGION: ${{ secrets.AWS_PEERING_REGION }} AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }} diff --git a/GNUmakefile b/GNUmakefile index 05994c6f..ae5b171b 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -9,8 +9,8 @@ PROVIDER_VERSION = 99.99.99 PLUGINS_PATH = ~/.terraform.d/plugins PLUGINS_PROVIDER_PATH=$(PROVIDER_HOSTNAME)/$(PROVIDER_NAMESPACE)/$(PROVIDER_TYPE)/$(PROVIDER_VERSION)/$(PROVIDER_TARGET) -# Use a parallelism of 2 by default for tests, overriding whatever GOMAXPROCS is set to. -TEST_PARALLELISM?=2 +# Use a parallelism of 4 by default for tests, overriding whatever GOMAXPROCS is set to. +TEST_PARALLELISM?=4 TESTARGS?=-short bin: diff --git a/docs/data-sources/rediscloud_acl_rule.md b/docs/data-sources/rediscloud_acl_rule.md index e4783bbc..0865ea0c 100644 --- a/docs/data-sources/rediscloud_acl_rule.md +++ b/docs/data-sources/rediscloud_acl_rule.md @@ -7,7 +7,8 @@ description: |- # Data Source: rediscloud_acl_rule -The Rule (a.k.a Redis Rule, Redis ACL) data source allows access to an existing Rule within your Redis Enterprise Cloud Account. +The Rule (a.k.a Redis Rule, Redis ACL) data source allows access to an existing Rule within your Redis Enterprise Cloud +Account. ## Example Usage diff --git a/docs/data-sources/rediscloud_acl_user.md b/docs/data-sources/rediscloud_acl_user.md new file mode 100644 index 00000000..357d63ab --- /dev/null +++ b/docs/data-sources/rediscloud_acl_user.md @@ -0,0 +1,32 @@ +--- +layout: "rediscloud" +page_title: "Redis Cloud: rediscloud_acl_user" +description: |- + ACL User data source in the Terraform provider Redis Cloud. +--- + +# Data Source: rediscloud_acl_user + +The User data source allows access to an existing Rule within your Redis Enterprise Cloud Account. + +## Example Usage + +```hcl +data "rediscloud_acl_user" "example" { + name = "fast-admin-john" +} + +output "rediscloud_acl_user" { + value = data.rediscloud_acl_user.example.id +} +``` + +## Argument Reference + +* `name` - (Required) The name of the User to filter returned subscriptions + +## Attribute reference + +* `id` - Identifier of the found User. +* `name` - The User's name. +* `role` - The name of the User's Role. diff --git a/docs/resources/rediscloud_acl_role.md b/docs/resources/rediscloud_acl_role.md index 1a05c414..54932959 100644 --- a/docs/resources/rediscloud_acl_role.md +++ b/docs/resources/rediscloud_acl_role.md @@ -12,20 +12,42 @@ Creates a Role in your Redis Enterprise Cloud Account. ## Example Usage ```hcl -resource "rediscloud_acl_role" "role-resource" { +resource "rediscloud_acl_role" "role-resource-implicit" { name = "fast-admin" rules { - name = "cache-reader-rule" + # An implicit dependency is recommended + name = rediscloud_acl_role.cache_reader.name + # Implicit dependencies used throughout databases { - subscription = 123456 - database = 9829 - regions = ["us-east-1", "us-east-2"] + subscription = rediscloud_active_active_subscription_database.subscription-resource-1.id + database = rediscloud_active_active_subscription_database.database-resource-1.db_id + regions = [ + for r in rediscloud_active_active_subscription_database.database-resource-1.override_region : r.name + ] + } + databases { + subscription = rediscloud_subscription.subscription-resource-2.id + database = rediscloud_subscription_database.database-resource-2.db_id } + } +} + +resource "rediscloud_acl_role" "role-resource-explicit" { + name = "fast-admin" + rules { + name = "cache-reader" + # Active-Active database omitted for brevity databases { subscription = 123456 - database = 9830 + database = 9830 } } + # An explicit resource dependency can be used if preferred + depends_on = [ + rediscloud_acl_rule.cache_reader, + rediscloud_subscription.subscription-resource-2, + rediscloud_subscription_database.database-resource-2 + ] } ``` @@ -33,13 +55,16 @@ resource "rediscloud_acl_role" "role-resource" { The following arguments are supported: -* `name` - (Required) A meaningful name for the role. Must be unique. **This can be modified, but since the Role is referred to - by name (and not ID), this could break existing references. See the [User](rediscloud_acl_user.md) resource documentation.** +* `name` - (Required) A meaningful name for the role. Must be unique. **This can be modified, but since the Role is + referred to + by name (and not ID), this could break existing references. See the [User](rediscloud_acl_user.md) resource + documentation.** * `rules` - (Required, minimum 1) A list of rule association objects, documented below. The `rules` list supports: -* `name` (Required) - Name of the Rule. +* `name` (Required) - Name of the Rule. It is recommended an implicit dependency is used here. `depends_on` could be + used instead by waiting for a Rule resource with a matching `name`. * `databases` - (Required, minimum 1) a list of database association objects, documented below. The `databases` list supports: @@ -48,14 +73,14 @@ The `databases` list supports: * `database` (Required) - ID of the database to which the Rule should apply. * `regions` (Optional) - For databases in Active/Active subscriptions only, the regions to which the Rule should apply. - ### Timeouts -The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: +The `timeouts` block allows you to +specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: -* `create` - (Defaults to 3 mins) Used when creating the Role. -* `update` - (Defaults to 3 mins) Used when updating the Role. -* `delete` - (Defaults to 1 mins) Used when destroying the Role. +* `create` - (Defaults to 5 mins) Used when creating the Role. +* `update` - (Defaults to 5 mins) Used when updating the Role. +* `delete` - (Defaults to 5 mins) Used when destroying the Role. ## Attribute reference @@ -75,6 +100,7 @@ The `databases` list is made of objects with: * `regions` The regions to which the Rule should apply, if appropriate to the database. ## Import + `rediscloud_acl_role` can be imported using the Identifier of the Role, e.g. ``` diff --git a/docs/resources/rediscloud_acl_rule.md b/docs/resources/rediscloud_acl_rule.md index 0138f5c1..a3e7b262 100644 --- a/docs/resources/rediscloud_acl_rule.md +++ b/docs/resources/rediscloud_acl_rule.md @@ -22,17 +22,21 @@ resource "rediscloud_acl_rule" "rule-resource" { The following arguments are supported: -* `name` - (Required) A meaningful name for the rule. Must be unique. **This can be modified, but since the Rule is referred to - by name (and not ID), this could break existing references. See the [Role](rediscloud_acl_role.md) resource documentation.** -* `rule` - (Required) The ACL rule itself, build up as permissions/restrictions written in the [ACL Syntax](https://docs.redis.com/latest/rc/security/access-control/data-access-control/configure-acls/#define-permissions-with-acl-syntax). +* `name` - (Required) A meaningful name for the rule. Must be unique. **This can be modified, but since the Rule is + referred to + by name (and not ID), this could break existing references. See the [Role](rediscloud_acl_role.md) resource + documentation.** +* `rule` - (Required) The ACL rule itself, build up as permissions/restrictions written in + the [ACL Syntax](https://docs.redis.com/latest/rc/security/access-control/data-access-control/configure-acls/#define-permissions-with-acl-syntax). ### Timeouts -The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: +The `timeouts` block allows you to +specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: -* `create` - (Defaults to 3 mins) Used when creating the Rule. -* `update` - (Defaults to 3 mins) Used when updating the Rule. -* `delete` - (Defaults to 1 mins) Used when destroying the Rule. +* `create` - (Defaults to 5 mins) Used when creating the Rule. +* `update` - (Defaults to 5 mins) Used when updating the Rule. +* `delete` - (Defaults to 5 mins) Used when destroying the Rule. ## Attribute reference @@ -41,6 +45,7 @@ The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/l * `rule` - The ACL Rule itself. ## Import + `rediscloud_acl_rule` can be imported using the Identifier of the Rule, e.g. ``` diff --git a/docs/resources/rediscloud_acl_user.md b/docs/resources/rediscloud_acl_user.md new file mode 100644 index 00000000..9b50e4ca --- /dev/null +++ b/docs/resources/rediscloud_acl_user.md @@ -0,0 +1,69 @@ +--- +layout: "rediscloud" +page_title: "Redis Cloud: rediscloud_acl_user" +description: |- + ACL User resource in the Terraform provider Redis Cloud. +--- + +# Resource: rediscloud_acl_user + +Creates a User in your Redis Enterprise Cloud Account. + +## Example Usage + +```hcl +resource "rediscloud_acl_user" "user-resource-implicit" { + name = "fast-admin-john" + # An implicit dependency is recommended + role = rediscloud_acl_role.fast_admin.name + password = "mY.passw0rd" +} + +resource "rediscloud_acl_user" "user-resource-explicit" { + name = "fast-admin-john" + role = "fast-admin" + password = "mY.passw0rd" + + # An explicit resource dependency can be used if preferred + depends_on = [ + rediscloud_acl_role.fast_admin + ] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required, change forces recreation) A meaningful name for the User. Must be unique. An error occurs if a + user tries to connect to + a `memcached` database with the username `admin`. +* `role` - (Required) The name of the Role held by the User. It is recommended an implicit dependency is used + here. `depends_on` could be used instead by waiting for a Role resource with a matching `name`. +* `password` - (Required, change forces recreation) The password for this ACL User. Must contain a lower-case letter, a + upper-case letter, a + number and a special character. Can be updated but since it is not returned by the API, we have no way of detecting + drift, so the entity would be entirely replaced. Take special care with multiple versions of Terraform State. + +### Timeouts + +The `timeouts` block allows you to +specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: + +* `create` - (Defaults to 5 mins) Used when creating the User. +* `update` - (Defaults to 5 mins) Used when updating the User. +* `delete` - (Defaults to 5 mins) Used when destroying the User. + +## Attribute reference + +* `id` - Identifier of the User created. +* `name` - The User's name. +* `role` - The User's role name. + +## Import + +`rediscloud_acl_user` can be imported using the Identifier of the User, e.g. + +``` +$ terraform import rediscloud_acl_user.user-resource 123456 +``` diff --git a/go.mod b/go.mod index ae25b9f4..4314a1b5 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/RedisLabs/terraform-provider-rediscloud go 1.19 require ( - github.com/RedisLabs/rediscloud-go-api v0.5.1 + github.com/RedisLabs/rediscloud-go-api v0.5.2 github.com/bflad/tfproviderlint v0.29.0 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/terraform-plugin-sdk/v2 v2.26.1 diff --git a/go.sum b/go.sum index 003bd156..d4c5ed16 100644 --- a/go.sum +++ b/go.sum @@ -7,8 +7,8 @@ github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/ github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ= github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= -github.com/RedisLabs/rediscloud-go-api v0.5.1 h1:3g+qhS5U3arNO890go17DyAbDseZuxTLEewASRMofu8= -github.com/RedisLabs/rediscloud-go-api v0.5.1/go.mod h1:cfuU+p/rgB+TObm0cq+AkyxwXWra8JOrPLKKj+nv7lM= +github.com/RedisLabs/rediscloud-go-api v0.5.2 h1:wwfUEbrH2oMOwk32ZLQpu/cVYpAgHsp1oqX40+ro/ns= +github.com/RedisLabs/rediscloud-go-api v0.5.2/go.mod h1:cfuU+p/rgB+TObm0cq+AkyxwXWra8JOrPLKKj+nv7lM= github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= diff --git a/provider/datasource_rediscloud_acl_role.go b/provider/datasource_rediscloud_acl_role.go index 37129fa9..e10f14ec 100644 --- a/provider/datasource_rediscloud_acl_role.go +++ b/provider/datasource_rediscloud_acl_role.go @@ -11,7 +11,7 @@ import ( func dataSourceRedisCloudAclRole() *schema.Resource { return &schema.Resource{ - Description: "The ACL Role grants a number of permissions to databases.", + Description: "The ACL Role grants a number of permissions to databases", ReadContext: dataSourceRedisCloudAclRoleRead, Schema: map[string]*schema.Schema{ @@ -21,34 +21,34 @@ func dataSourceRedisCloudAclRole() *schema.Resource { Required: true, }, "rules": { - Description: "This Role's permissions and the databases to which they apply.", + Description: "This Role's permissions and the databases to which they apply", Type: schema.TypeSet, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { - Description: "The name of the Rule.", + Description: "The name of the Rule", Type: schema.TypeString, Computed: true, }, "databases": { - Description: "The databases to which this Rule applies.", + Description: "The databases to which this Rule applies", Type: schema.TypeSet, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "subscription": { - Description: "The name of the Rule.", + Description: "The name of the Rule", Type: schema.TypeInt, Computed: true, }, "database": { - Description: "The databases to which this Rule applies.", + Description: "The databases to which this Rule applies", Type: schema.TypeInt, Computed: true, }, "regions": { - Description: "The regional deployments of this database to which the Rule applies. Only relevant to Active/Active databases, otherwise omit.", + Description: "The regional deployments of this database to which the Rule applies. Only relevant to Active/Active databases, otherwise omit", Type: schema.TypeSet, Computed: true, Elem: &schema.Schema{ diff --git a/provider/datasource_rediscloud_acl_rule.go b/provider/datasource_rediscloud_acl_rule.go index e8e4e04d..2334ffea 100644 --- a/provider/datasource_rediscloud_acl_rule.go +++ b/provider/datasource_rediscloud_acl_rule.go @@ -11,7 +11,7 @@ import ( func dataSourceRedisCloudAclRule() *schema.Resource { return &schema.Resource{ - Description: "The ACL Rule (known also as RedisRule) allows fine-grained permissions to be assigned to a subset of ACL Users.", + Description: "The ACL Rule (known also as RedisRule) allows fine-grained permissions to be assigned to a subset of ACL Users", ReadContext: dataSourceRedisCloudAclRuleRead, Schema: map[string]*schema.Schema{ diff --git a/provider/datasource_rediscloud_acl_user.go b/provider/datasource_rediscloud_acl_user.go new file mode 100644 index 00000000..df70ba8b --- /dev/null +++ b/provider/datasource_rediscloud_acl_user.go @@ -0,0 +1,86 @@ +package provider + +import ( + "context" + "github.com/RedisLabs/rediscloud-go-api/redis" + "github.com/RedisLabs/rediscloud-go-api/service/access_control_lists/users" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "strconv" +) + +func dataSourceRedisCloudAclUser() *schema.Resource { + return &schema.Resource{ + Description: "The ACL User is an authenticated entity whose permissions are described by an associated Role", + ReadContext: dataSourceRedisCloudAclUserRead, + + Schema: map[string]*schema.Schema{ + "name": { + Description: "A meaningful name to identify the user", + Type: schema.TypeString, + Required: true, + }, + "role": { + Description: "The Role which this User has", + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceRedisCloudAclUserRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + api := meta.(*apiClient) + + var filters []func(user *users.GetUserResponse) bool + if v, ok := d.GetOk("name"); ok { + filters = append(filters, func(user *users.GetUserResponse) bool { + return redis.StringValue(user.Name) == v.(string) + }) + } + + list, err := api.client.Users.List(ctx) + if err != nil { + return diag.FromErr(err) + } + list = filterUsers(list, filters) + + if len(list) == 0 { + return diag.Errorf("Your query returned no results. Please change your search criteria and try again.") + } + + if len(list) > 1 { + return diag.Errorf("Your query returned more than one result. Please change try a more specific search criteria and try again.") + } + + user := list[0] + d.SetId(strconv.Itoa(redis.IntValue(user.ID))) + if err := d.Set("name", redis.StringValue(user.Name)); err != nil { + return diag.FromErr(err) + } + if err := d.Set("role", redis.StringValue(user.Role)); err != nil { + return diag.FromErr(err) + } + + return diags +} + +func filterUsers(list []*users.GetUserResponse, filters []func(*users.GetUserResponse) bool) []*users.GetUserResponse { + var filtered []*users.GetUserResponse + for _, user := range list { + if filterUser(user, filters) { + filtered = append(filtered, user) + } + } + return filtered +} + +func filterUser(rule *users.GetUserResponse, filters []func(*users.GetUserResponse) bool) bool { + for _, filter := range filters { + if !filter(rule) { + return false + } + } + return true +} diff --git a/provider/datasource_rediscloud_acl_user_test.go b/provider/datasource_rediscloud_acl_user_test.go new file mode 100644 index 00000000..c6a1d697 --- /dev/null +++ b/provider/datasource_rediscloud_acl_user_test.go @@ -0,0 +1,126 @@ +package provider + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "os" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDataSourceRedisCloudAclUser_Default(t *testing.T) { + + prefix := acctest.RandomWithPrefix(testResourcePrefix) + exampleCloudAccountName := os.Getenv("AWS_TEST_CLOUD_ACCOUNT_NAME") + exampleSubscriptionName := prefix + "-subscription" + exampleDatabaseName := prefix + "-database" + exampleDatabasePassword := prefix + "aA.1" + exampleRoleName := prefix + "-role" + + testName := prefix + "-test-user" + testRoleName := exampleRoleName + testPassword := prefix + "aA.1" + + createAndGetUserTerraform := fmt.Sprintf( + testAccDatasourceRedisCloudUserDataSource, + exampleCloudAccountName, + exampleSubscriptionName, + exampleDatabaseName, + exampleDatabasePassword, + exampleRoleName, + testName, + testPassword, + ) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + CheckDestroy: nil, // test doesn't create a resource at the moment, so don't need to check anything + Steps: []resource.TestStep{ + { + Config: createAndGetUserTerraform, + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr( + "data.rediscloud_acl_user.test", "id", regexp.MustCompile("^\\d*$")), + resource.TestCheckResourceAttr("data.rediscloud_acl_user.test", "name", testName), + resource.TestCheckResourceAttr("data.rediscloud_acl_user.test", "role", testRoleName), + ), + }, + }, + }) +} + +const testAccDatasourceRedisCloudUserDataSource = ` +data "rediscloud_payment_method" "card" { + card_type = "Visa" +} + +data "rediscloud_cloud_account" "account" { + exclude_internal_account = true + provider_type = "AWS" + name = "%s" +} + +resource "rediscloud_subscription" "example" { + + name = "%s" + payment_method_id = data.rediscloud_payment_method.card.id + memory_storage = "ram" + + cloud_provider { + provider = data.rediscloud_cloud_account.account.provider_type + cloud_account_id = data.rediscloud_cloud_account.account.id + region { + region = "eu-west-1" + networking_deployment_cidr = "10.0.0.0/24" + preferred_availability_zones = ["eu-west-1a"] + } + } + + creation_plan { + memory_limit_in_gb = 1 + quantity = 1 + replication=false + support_oss_cluster_api=true + throughput_measurement_by = "operations-per-second" + throughput_measurement_value = 1000 + modules = ["RedisJSON", "RedisBloom"] + } +} + +resource "rediscloud_subscription_database" "example" { + subscription_id = rediscloud_subscription.example.id + name = "%s" + protocol = "redis" + memory_limit_in_gb = 1 + data_persistence = "none" + throughput_measurement_by = "operations-per-second" + throughput_measurement_value = 1000 + password = "%s" + support_oss_cluster_api = true + replication = false +} + +resource "rediscloud_acl_role" "example" { + name = "%s" + rules { + name = "Read-Only" + databases { + subscription = rediscloud_subscription.example.id + database = rediscloud_subscription_database.example.db_id + } + } +} + +resource "rediscloud_acl_user" "test" { + name = "%s" + role = rediscloud_acl_role.example.name + password = "%s" +} + +data "rediscloud_acl_user" "test" { + name = rediscloud_acl_user.test.name +} +` diff --git a/provider/provider.go b/provider/provider.go index f6ea2669..630bafdb 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -51,6 +51,7 @@ func New(version string) func() *schema.Provider { "rediscloud_subscription_peerings": dataSourceRedisCloudSubscriptionPeerings(), "rediscloud_acl_rule": dataSourceRedisCloudAclRule(), "rediscloud_acl_role": dataSourceRedisCloudAclRole(), + "rediscloud_acl_user": dataSourceRedisCloudAclUser(), }, ResourcesMap: map[string]*schema.Resource{ "rediscloud_cloud_account": resourceRedisCloudCloudAccount(), @@ -63,6 +64,7 @@ func New(version string) func() *schema.Provider { "rediscloud_active_active_subscription_peering": resourceRedisCloudActiveActiveSubscriptionPeering(), "rediscloud_acl_rule": resourceRedisCloudAclRule(), "rediscloud_acl_role": resourceRedisCloudAclRole(), + "rediscloud_acl_user": resourceRedisCloudAclUser(), }, } diff --git a/provider/resource_rediscloud_acl_role.go b/provider/resource_rediscloud_acl_role.go index 39064577..b8ee47c8 100644 --- a/provider/resource_rediscloud_acl_role.go +++ b/provider/resource_rediscloud_acl_role.go @@ -2,10 +2,13 @@ package provider import ( "context" + "fmt" "github.com/RedisLabs/rediscloud-go-api/redis" "github.com/RedisLabs/rediscloud-go-api/service/access_control_lists/roles" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "log" "strconv" "time" ) @@ -19,20 +22,19 @@ func resourceRedisCloudAclRole() *schema.Resource { DeleteContext: resourceRedisCloudAclRoleDelete, Importer: &schema.ResourceImporter{ - // Let the READ operation do the heavy lifting for importing values from the API. StateContext: schema.ImportStatePassthroughContext, }, Timeouts: &schema.ResourceTimeout{ - Create: schema.DefaultTimeout(3 * time.Minute), - Read: schema.DefaultTimeout(1 * time.Minute), - Update: schema.DefaultTimeout(3 * time.Minute), - Delete: schema.DefaultTimeout(1 * time.Minute), + Create: schema.DefaultTimeout(5 * time.Minute), + Read: schema.DefaultTimeout(3 * time.Minute), + Update: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), }, Schema: map[string]*schema.Schema{ "name": { - Description: "A meaningful name to identify the role", + Description: "A meaningful name to identify the role, must be unique", Type: schema.TypeString, Required: true, }, @@ -85,7 +87,6 @@ func resourceRedisCloudAclRole() *schema.Resource { func resourceRedisCloudAclRoleCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { api := meta.(*apiClient) - var diags diag.Diagnostics name := d.Get("name").(string) associateWithRules := extractRules(d) @@ -103,7 +104,12 @@ func resourceRedisCloudAclRoleCreate(ctx context.Context, d *schema.ResourceData d.SetId(strconv.Itoa(id)) - return diags + err = waitForAclRoleToBeActive(ctx, id, api) + if err != nil { + return diag.FromErr(err) + } + + return resourceRedisCloudAclRoleRead(ctx, d, meta) } func resourceRedisCloudAclRoleRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { @@ -117,6 +123,10 @@ func resourceRedisCloudAclRoleRead(ctx context.Context, d *schema.ResourceData, role, err := api.client.Roles.Get(ctx, id) if err != nil { + if _, ok := err.(*roles.NotFound); ok { + d.SetId("") + return diags + } return diag.FromErr(err) } @@ -131,7 +141,6 @@ func resourceRedisCloudAclRoleRead(ctx context.Context, d *schema.ResourceData, func resourceRedisCloudAclRoleUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { api := meta.(*apiClient) - var diags diag.Diagnostics id, err := strconv.Atoi(d.Id()) if err != nil { @@ -150,9 +159,14 @@ func resourceRedisCloudAclRoleUpdate(ctx context.Context, d *schema.ResourceData if err != nil { return diag.FromErr(err) } + + err = waitForAclRoleToBeActive(ctx, id, api) + if err != nil { + return diag.FromErr(err) + } } - return diags + return resourceRedisCloudAclRoleRead(ctx, d, meta) } func resourceRedisCloudAclRoleDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { @@ -172,6 +186,29 @@ func resourceRedisCloudAclRoleDelete(ctx context.Context, d *schema.ResourceData d.SetId("") + // Wait until it's really disappeared + err = retry.RetryContext(ctx, 5*time.Minute, func() *retry.RetryError { + role, err := api.client.Roles.Get(ctx, id) + + if err != nil { + if _, ok := err.(*roles.NotFound); ok { + // All good, the resource is gone + return nil + } + // This was an unexpected error + return retry.NonRetryableError(fmt.Errorf("error getting role: %s", err)) + } + + if role != nil { + return retry.RetryableError(fmt.Errorf("expected role %d to be deleted but was in state %s", id, redis.StringValue(role.Status))) + } + // Unclear at this point what's going on! + return retry.NonRetryableError(fmt.Errorf("unexpected error getting role")) + }) + if err != nil { + return diag.FromErr(err) + } + return diags } @@ -244,3 +281,28 @@ func flattenDatabases(databases []*roles.GetDatabaseInRuleInRoleResponse) []map[ return tfs } + +func waitForAclRoleToBeActive(ctx context.Context, id int, api *apiClient) error { + wait := &retry.StateChangeConf{ + Delay: 5 * time.Second, + Pending: []string{roles.StatusPending}, + Target: []string{roles.StatusActive}, + Timeout: 5 * time.Minute, + + Refresh: func() (result interface{}, state string, err error) { + log.Printf("[DEBUG] Waiting for role %d to be active", id) + + role, err := api.client.Roles.Get(ctx, id) + if err != nil { + return nil, "", err + } + + return redis.StringValue(role.Status), redis.StringValue(role.Status), nil + }, + } + if _, err := wait.WaitForStateContext(ctx); err != nil { + return err + } + + return nil +} diff --git a/provider/resource_rediscloud_acl_role_test.go b/provider/resource_rediscloud_acl_role_test.go index 88685342..d93af16c 100644 --- a/provider/resource_rediscloud_acl_role_test.go +++ b/provider/resource_rediscloud_acl_role_test.go @@ -206,7 +206,9 @@ resource "rediscloud_acl_role" "test" { databases { subscription = rediscloud_active_active_subscription.example.id database = rediscloud_active_active_subscription_database.example.db_id - regions = ["us-east-1", "us-east-2"] + regions = [ + for r in rediscloud_active_active_subscription_database.example.override_region : r.name + ] } } } diff --git a/provider/resource_rediscloud_acl_rule.go b/provider/resource_rediscloud_acl_rule.go index 7a12e349..e0b60308 100644 --- a/provider/resource_rediscloud_acl_rule.go +++ b/provider/resource_rediscloud_acl_rule.go @@ -2,10 +2,13 @@ package provider import ( "context" + "fmt" "github.com/RedisLabs/rediscloud-go-api/redis" "github.com/RedisLabs/rediscloud-go-api/service/access_control_lists/redis_rules" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "log" "strconv" "time" ) @@ -19,20 +22,19 @@ func resourceRedisCloudAclRule() *schema.Resource { DeleteContext: resourceRedisCloudAclRuleDelete, Importer: &schema.ResourceImporter{ - // Let the READ operation do the heavy lifting for importing values from the API. StateContext: schema.ImportStatePassthroughContext, }, Timeouts: &schema.ResourceTimeout{ - Create: schema.DefaultTimeout(3 * time.Minute), - Read: schema.DefaultTimeout(1 * time.Minute), - Update: schema.DefaultTimeout(3 * time.Minute), - Delete: schema.DefaultTimeout(1 * time.Minute), + Create: schema.DefaultTimeout(5 * time.Minute), + Read: schema.DefaultTimeout(3 * time.Minute), + Update: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), }, Schema: map[string]*schema.Schema{ "name": { - Description: "A meaningful name to identify the rule. Must be unique.", + Description: "A meaningful name to identify the rule, must be unique", Type: schema.TypeString, Required: true, }, @@ -47,7 +49,6 @@ func resourceRedisCloudAclRule() *schema.Resource { func resourceRedisCloudAclRuleCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { api := meta.(*apiClient) - var diags diag.Diagnostics name := d.Get("name").(string) rule := d.Get("rule").(string) @@ -64,7 +65,12 @@ func resourceRedisCloudAclRuleCreate(ctx context.Context, d *schema.ResourceData d.SetId(strconv.Itoa(id)) - return diags + err = waitForAclRuleToBeActive(ctx, id, api) + if err != nil { + return diag.FromErr(err) + } + + return resourceRedisCloudAclRuleRead(ctx, d, meta) } func resourceRedisCloudAclRuleRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { @@ -78,6 +84,10 @@ func resourceRedisCloudAclRuleRead(ctx context.Context, d *schema.ResourceData, rule, err := api.client.RedisRules.Get(ctx, id) if err != nil { + if _, ok := err.(*redis_rules.NotFound); ok { + d.SetId("") + return diags + } return diag.FromErr(err) } @@ -93,7 +103,6 @@ func resourceRedisCloudAclRuleRead(ctx context.Context, d *schema.ResourceData, func resourceRedisCloudAclRuleUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { api := meta.(*apiClient) - var diags diag.Diagnostics id, err := strconv.Atoi(d.Id()) if err != nil { @@ -112,9 +121,14 @@ func resourceRedisCloudAclRuleUpdate(ctx context.Context, d *schema.ResourceData if err != nil { return diag.FromErr(err) } + + err = waitForAclRuleToBeActive(ctx, id, api) + if err != nil { + return diag.FromErr(err) + } } - return diags + return resourceRedisCloudAclRuleRead(ctx, d, meta) } func resourceRedisCloudAclRuleDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { @@ -134,5 +148,53 @@ func resourceRedisCloudAclRuleDelete(ctx context.Context, d *schema.ResourceData d.SetId("") + // Wait until it's really disappeared + err = retry.RetryContext(ctx, 5*time.Minute, func() *retry.RetryError { + rule, err := api.client.RedisRules.Get(ctx, id) + + if err != nil { + if _, ok := err.(*redis_rules.NotFound); ok { + // All good, the resource is gone + return nil + } + // This was an unexpected error + return retry.NonRetryableError(fmt.Errorf("error getting rule: %s", err)) + } + + if rule != nil { + return retry.RetryableError(fmt.Errorf("expected rule %d to be deleted but was in state %s", id, redis.StringValue(rule.Status))) + } + // Unclear at this point what's going on! + return retry.NonRetryableError(fmt.Errorf("unexpected error getting rule")) + }) + if err != nil { + return diag.FromErr(err) + } + return diags } + +func waitForAclRuleToBeActive(ctx context.Context, id int, api *apiClient) error { + wait := &retry.StateChangeConf{ + Delay: 5 * time.Second, + Pending: []string{redis_rules.StatusPending}, + Target: []string{redis_rules.StatusActive}, + Timeout: 5 * time.Minute, + + Refresh: func() (result interface{}, state string, err error) { + log.Printf("[DEBUG] Waiting for rule %d to be active", id) + + rule, err := api.client.RedisRules.Get(ctx, id) + if err != nil { + return nil, "", err + } + + return redis.StringValue(rule.Status), redis.StringValue(rule.Status), nil + }, + } + if _, err := wait.WaitForStateContext(ctx); err != nil { + return err + } + + return nil +} diff --git a/provider/resource_rediscloud_acl_user.go b/provider/resource_rediscloud_acl_user.go new file mode 100644 index 00000000..b7cde1d4 --- /dev/null +++ b/provider/resource_rediscloud_acl_user.go @@ -0,0 +1,174 @@ +package provider + +import ( + "context" + "fmt" + "github.com/RedisLabs/rediscloud-go-api/redis" + "github.com/RedisLabs/rediscloud-go-api/service/access_control_lists/users" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "strconv" + "time" +) + +func resourceRedisCloudAclUser() *schema.Resource { + return &schema.Resource{ + Description: "Create an ACL User within your Redis Enterprise Cloud Account", + CreateContext: resourceRedisCloudAclUserCreate, + ReadContext: resourceRedisCloudAclUserRead, + UpdateContext: resourceRedisCloudAclUserUpdate, + DeleteContext: resourceRedisCloudAclUserDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(5 * time.Minute), + Read: schema.DefaultTimeout(3 * time.Minute), + Update: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "name": { + Description: "A meaningful name to identify the user", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "role": { + Description: "The role which the user has", + Type: schema.TypeString, + Required: true, + }, + "password": { + Description: "The user's password", + Type: schema.TypeString, + ForceNew: true, + Required: true, + Sensitive: true, + }, + }, + } +} + +func resourceRedisCloudAclUserCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + api := meta.(*apiClient) + + name := d.Get("name").(string) + role := d.Get("role").(string) + password := d.Get("password").(string) + + createUser := users.CreateUserRequest{ + Name: redis.String(name), + Role: redis.String(role), + Password: redis.String(password), + } + + id, err := api.client.Users.Create(ctx, createUser) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(strconv.Itoa(id)) + + return resourceRedisCloudAclUserRead(ctx, d, meta) +} + +func resourceRedisCloudAclUserRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + api := meta.(*apiClient) + var diags diag.Diagnostics + + id, err := strconv.Atoi(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + user, err := api.client.Users.Get(ctx, id) + if err != nil { + if _, ok := err.(*users.NotFound); ok { + d.SetId("") + return diags + } + return diag.FromErr(err) + } + + if err := d.Set("name", redis.StringValue(user.Name)); err != nil { + return diag.FromErr(err) + } + if err := d.Set("role", redis.StringValue(user.Role)); err != nil { + return diag.FromErr(err) + } + + return diags +} + +func resourceRedisCloudAclUserUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + api := meta.(*apiClient) + + id, err := strconv.Atoi(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + if d.HasChanges("role", "password") { + updateUserRequest := users.UpdateUserRequest{} + + role := d.Get("role").(string) + updateUserRequest.Role = &role + password := d.Get("password").(string) + updateUserRequest.Password = &password + + err = api.client.Users.Update(ctx, id, updateUserRequest) + if err != nil { + return diag.FromErr(err) + } + } + + return resourceRedisCloudAclUserRead(ctx, d, meta) +} + +func resourceRedisCloudAclUserDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + api := meta.(*apiClient) + var diags diag.Diagnostics + + id, err := strconv.Atoi(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + err = api.client.Users.Delete(ctx, id) + + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + + // Wait until it's really disappeared + err = retry.RetryContext(ctx, 5*time.Minute, func() *retry.RetryError { + user, err := api.client.Users.Get(ctx, id) + + if err != nil { + if _, ok := err.(*users.NotFound); ok { + // All good, the resource is gone + return nil + } + // This was an unexpected error + return retry.NonRetryableError(fmt.Errorf("error getting user: %s", err)) + } + + if user != nil { + return retry.RetryableError(fmt.Errorf("expected user %d to be deleted but was not", id)) + } + // Unclear at this point what's going on! + return retry.NonRetryableError(fmt.Errorf("unexpected error getting user")) + }) + if err != nil { + return diag.FromErr(err) + } + + return diags +} diff --git a/provider/resource_rediscloud_acl_user_test.go b/provider/resource_rediscloud_acl_user_test.go new file mode 100644 index 00000000..4d16f88d --- /dev/null +++ b/provider/resource_rediscloud_acl_user_test.go @@ -0,0 +1,248 @@ +package provider + +import ( + "context" + "fmt" + "github.com/RedisLabs/rediscloud-go-api/redis" + "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" + "os" + "strconv" + "testing" +) + +func TestAccResourceRedisCloudAclUser_CRUDI(t *testing.T) { + + prefix := acctest.RandomWithPrefix(testResourcePrefix) + exampleCloudAccountName := os.Getenv("AWS_TEST_CLOUD_ACCOUNT_NAME") + exampleSubscriptionName := prefix + "-subscription" + exampleDatabasePassword := prefix + "aA.1" + exampleRoleName := prefix + "-role" + + testName := prefix + "-test-user" + testPassword := prefix + "aA.1" + + testCreateTerraform := fmt.Sprintf(testAccResourceRedisCloudSubscriptionDatabase, exampleCloudAccountName, exampleSubscriptionName, exampleDatabasePassword) + + fmt.Sprintf(referencableRole, exampleRoleName) + + fmt.Sprintf(testUser, testName, testPassword) + + // The User will be updated because the Role's name will have changed + testUpdateTerraform := fmt.Sprintf(testAccResourceRedisCloudSubscriptionDatabase, exampleCloudAccountName, exampleSubscriptionName, exampleDatabasePassword) + + fmt.Sprintf(referencableRole, exampleRoleName+"-updated") + + fmt.Sprintf(testUser, testName, testPassword) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccAwsPreExistingCloudAccountPreCheck(t) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckAclUserDestroy, + Steps: []resource.TestStep{ + // Test user creation + { + Config: testCreateTerraform, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("rediscloud_acl_user.test", "name", testName), + resource.TestCheckResourceAttr("rediscloud_acl_user.test", "role", exampleRoleName), + + // Test user exists + func(s *terraform.State) error { + r := s.RootModule().Resources["rediscloud_acl_user.test"] + + id, err := strconv.Atoi(r.Primary.ID) + if err != nil { + return fmt.Errorf("couldn't parse the role ID: %s", redis.StringValue(&r.Primary.ID)) + } + + client := testProvider.Meta().(*apiClient) + user, err := client.client.Users.Get(context.TODO(), id) + if err != nil { + return err + } + + if redis.StringValue(user.Name) != testName { + return fmt.Errorf("unexpected name value: %s", redis.StringValue(user.Name)) + } + if redis.StringValue(user.Role) != exampleRoleName { + return fmt.Errorf("unexpected role value: %s", redis.StringValue(user.Role)) + } + + return nil + }, + ), + }, + // Test user is updated successfully, id should not have changed + { + Config: testUpdateTerraform, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("rediscloud_acl_user.test", "name", testName), + resource.TestCheckResourceAttr("rediscloud_acl_user.test", "role", exampleRoleName+"-updated"), + ), + }, + // Test that the user is imported successfully + { + Config: fmt.Sprintf(testUser, testName+"_updated", testPassword+"-updated"), + ResourceName: "rediscloud_acl_user.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"password"}, + }, + }, + }) +} + +func TestAccResourceRedisCloudAclUser_NewName(t *testing.T) { + + prefix := acctest.RandomWithPrefix(testResourcePrefix) + exampleCloudAccountName := os.Getenv("AWS_TEST_CLOUD_ACCOUNT_NAME") + exampleSubscriptionName := prefix + "-subscription" + exampleDatabasePassword := prefix + "aA.1" + exampleRoleName := prefix + "-role" + + testName := prefix + "-test-user" + testPassword := prefix + "aA.1" + + testCreateTerraform := fmt.Sprintf(testAccResourceRedisCloudSubscriptionDatabase, exampleCloudAccountName, exampleSubscriptionName, exampleDatabasePassword) + + fmt.Sprintf(referencableRole, exampleRoleName) + + fmt.Sprintf(testUser, testName, testPassword) + + testUpdateTerraform := fmt.Sprintf(testAccResourceRedisCloudSubscriptionDatabase, exampleCloudAccountName, exampleSubscriptionName, exampleDatabasePassword) + + fmt.Sprintf(referencableRole, exampleRoleName) + + fmt.Sprintf(testUser, testName+"-updated", testPassword) + + identifier := "" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccAwsPreExistingCloudAccountPreCheck(t) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckAclUserDestroy, + Steps: []resource.TestStep{ + // Test user creation + { + Config: testCreateTerraform, + Check: resource.ComposeTestCheckFunc( + func(s *terraform.State) error { + r := s.RootModule().Resources["rediscloud_acl_user.test"] + identifier = r.Primary.ID + return nil + }, + ), + }, + // Test user is updated successfully. A name change should forcibly generate a new entity with a new id + { + Config: testUpdateTerraform, + Check: resource.ComposeTestCheckFunc( + func(s *terraform.State) error { + r := s.RootModule().Resources["rediscloud_acl_user.test"] + if r.Primary.ID == identifier { + return fmt.Errorf("entity should have a new identifier, but is still: %s", identifier) + } + return nil + }, + ), + }, + }, + }) +} + +func TestAccResourceRedisCloudAclUser_NewPassword(t *testing.T) { + + prefix := acctest.RandomWithPrefix(testResourcePrefix) + exampleCloudAccountName := os.Getenv("AWS_TEST_CLOUD_ACCOUNT_NAME") + exampleSubscriptionName := prefix + "-subscription" + exampleDatabasePassword := prefix + "aA.1" + exampleRoleName := prefix + "-role" + + testName := prefix + "-test-user" + testPassword := prefix + "aA.1" + + testCreateTerraform := fmt.Sprintf(testAccResourceRedisCloudSubscriptionDatabase, exampleCloudAccountName, exampleSubscriptionName, exampleDatabasePassword) + + fmt.Sprintf(referencableRole, exampleRoleName) + + fmt.Sprintf(testUser, testName, testPassword) + + testUpdateTerraform := fmt.Sprintf(testAccResourceRedisCloudSubscriptionDatabase, exampleCloudAccountName, exampleSubscriptionName, exampleDatabasePassword) + + fmt.Sprintf(referencableRole, exampleRoleName) + + fmt.Sprintf(testUser, testName, testPassword+"-updated") + + identifier := "" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccAwsPreExistingCloudAccountPreCheck(t) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckAclUserDestroy, + Steps: []resource.TestStep{ + // Test user creation + { + Config: testCreateTerraform, + Check: resource.ComposeTestCheckFunc( + func(s *terraform.State) error { + r := s.RootModule().Resources["rediscloud_acl_user.test"] + identifier = r.Primary.ID + return nil + }, + ), + }, + // Test user is updated successfully. A password change should forcibly generate a new entity with a new id + { + Config: testUpdateTerraform, + Check: resource.ComposeTestCheckFunc( + func(s *terraform.State) error { + r := s.RootModule().Resources["rediscloud_acl_user.test"] + if r.Primary.ID == identifier { + return fmt.Errorf("entity should have a new identifier, but is still: %s", identifier) + } + return nil + }, + ), + }, + }, + }) +} + +const referencableRole = ` +resource "rediscloud_acl_role" "example" { + name = "%s" + rules { + name = "Read-Only" + databases { + subscription = rediscloud_subscription.example.id + database = rediscloud_subscription_database.example.db_id + } + } +} +` + +const testUser = ` +resource "rediscloud_acl_user" "test" { + name = "%s" + role = rediscloud_acl_role.example.name + password = "%s" +} +` + +func testAccCheckAclUserDestroy(s *terraform.State) error { + client := testProvider.Meta().(*apiClient) + + for _, r := range s.RootModule().Resources { + if r.Type != "rediscloud_acl_user" { + continue + } + + id, err := strconv.Atoi(r.Primary.ID) + if err != nil { + return err + } + + roles, err := client.client.Users.List(context.TODO()) + if err != nil { + return err + } + + for _, role := range roles { + if redis.IntValue(role.ID) == id { + return fmt.Errorf("user %d still exists", id) + } + } + } + + return nil +} diff --git a/provider/sweeper_test.go b/provider/sweeper_test.go index 4b27073d..424df963 100644 --- a/provider/sweeper_test.go +++ b/provider/sweeper_test.go @@ -235,22 +235,17 @@ func testSweepAcl(region string) error { ctx := context.TODO() - rules, err := client.RedisRules.List(ctx) + users, err := client.Users.List(ctx) if err != nil { return err } - for _, rule := range rules { - // There are 3 'default' rules which can't be deleted (Read-Only, Read-Write, Full-Access) - if redis.BoolValue(rule.IsDefault) { - continue - } - - if !strings.HasPrefix(redis.StringValue(rule.Name), testResourcePrefix) { + for _, user := range users { + if !strings.HasPrefix(redis.StringValue(user.Name), testResourcePrefix) { continue } - if client.RedisRules.Delete(ctx, redis.IntValue(rule.ID)) != nil { + if client.Users.Delete(ctx, redis.IntValue(user.ID)) != nil { return err } } @@ -270,5 +265,25 @@ func testSweepAcl(region string) error { } } + rules, err := client.RedisRules.List(ctx) + if err != nil { + return err + } + + for _, rule := range rules { + // There are 3 'default' rules which can't be deleted (Read-Only, Read-Write, Full-Access) + if redis.BoolValue(rule.IsDefault) { + continue + } + + if !strings.HasPrefix(redis.StringValue(rule.Name), testResourcePrefix) { + continue + } + + if client.RedisRules.Delete(ctx, redis.IntValue(rule.ID)) != nil { + return err + } + } + return nil }