diff --git a/GNUmakefile b/GNUmakefile index da566150..0fac5c9f 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 3 by default for tests, overriding whatever GOMAXPROCS is set to. -TEST_PARALLELISM?=3 +# Use a parallelism of 2 by default for tests, overriding whatever GOMAXPROCS is set to. +TEST_PARALLELISM?=2 TESTARGS?=-short BIN=$(CURDIR)/bin @@ -30,20 +30,21 @@ clean: @echo "Deleting local provider binary" rm -rf $(BIN) +# `-p=1` added to avoid testing packages in parallel which causes `go test` to not stream logs as they are written testacc: - TF_ACC=1 go test ./... -v $(TESTARGS) -timeout 360m -parallel=$(TEST_PARALLELISM) + TF_ACC=1 go test ./... -v $(TESTARGS) -timeout 360m -p=1 -parallel=$(TEST_PARALLELISM) install_local: build @echo "Installing local provider binary to plugins mirror path $(PLUGINS_PATH)/$(PLUGINS_PROVIDER_PATH)" @mkdir -p $(PLUGINS_PATH)/$(PLUGINS_PROVIDER_PATH) - @cp ./$(BIN)/terraform-provider-rediscloud_v$(PROVIDER_VERSION) $(PLUGINS_PATH)/$(PLUGINS_PROVIDER_PATH) + @cp $(BIN)/terraform-provider-rediscloud_v$(PROVIDER_VERSION) $(PLUGINS_PATH)/$(PLUGINS_PROVIDER_PATH) sweep: @echo "WARNING: This will destroy infrastructure. Use only in development accounts." go test ./provider -v -sweep=ALL $(SWEEPARGS) -timeout 30m -tfproviderlintx: $(BIN)/tfproviderlint +tfproviderlintx: $(BIN)/tfproviderlintx $(BIN)/tfproviderlintx $(TFPROVIDERLINT_ARGS) ./... -tfproviderlint: $(BIN)/tfproviderlintx +tfproviderlint: $(BIN)/tfproviderlint $(BIN)/tfproviderlint $(TFPROVIDERLINT_ARGS) ./... diff --git a/docs/resources/rediscloud_active_active_subscription_database.md b/docs/resources/rediscloud_active_active_subscription_database.md index 4b19daf0..4debac27 100644 --- a/docs/resources/rediscloud_active_active_subscription_database.md +++ b/docs/resources/rediscloud_active_active_subscription_database.md @@ -13,7 +13,7 @@ Creates a Database within a specified Active-Active Subscription in your Redis E ```hcl data "rediscloud_payment_method" "card" { - card_type = "Visa" + card_type = "Visa" } resource "rediscloud_active_active_subscription" "subscription-resource" { @@ -24,18 +24,18 @@ resource "rediscloud_active_active_subscription" "subscription-resource" { creation_plan { memory_limit_in_gb = 1 quantity = 1 - region { - region = "us-east-1" - networking_deployment_cidr = "192.168.0.0/24" - write_operations_per_second = 1000 - read_operations_per_second = 1000 - } - region { - region = "us-east-2" - networking_deployment_cidr = "10.0.1.0/24" - write_operations_per_second = 1000 - read_operations_per_second = 2000 - } + region { + region = "us-east-1" + networking_deployment_cidr = "192.168.0.0/24" + write_operations_per_second = 1000 + read_operations_per_second = 1000 + } + region { + region = "us-east-2" + networking_deployment_cidr = "10.0.1.0/24" + write_operations_per_second = 1000 + read_operations_per_second = 2000 + } } } @@ -47,23 +47,23 @@ resource "rediscloud_active_active_subscription_database" "database-resource" { global_password = "some-random-pass-2" global_source_ips = ["192.168.0.0/16"] global_alert { - name = "dataset-size" - value = 40 + name = "dataset-size" + value = 40 } override_region { - name = "us-east-2" - override_global_source_ips = ["192.10.0.0/16"] + name = "us-east-2" + override_global_source_ips = ["192.10.0.0/16"] } override_region { - name = "us-east-1" - override_global_data_persistence = "none" - override_global_password = "region-specific-password" - override_global_alert { - name = "dataset-size" - value = 60 - } + name = "us-east-1" + override_global_data_persistence = "none" + override_global_password = "region-specific-password" + override_global_alert { + name = "dataset-size" + value = 60 + } } } @@ -89,9 +89,10 @@ The following arguments are supported: * `client_ssl_certificate` - (Optional) SSL certificate to authenticate user connections. * `data_eviction` - (Optional) The data items eviction policy (either: 'allkeys-lru', 'allkeys-lfu', 'allkeys-random', 'volatile-lru', 'volatile-lfu', 'volatile-random', 'volatile-ttl' or 'noeviction'. Default: 'volatile-lru') * `global_data_persistence` - (Optional) Global rate of database data persistence (in persistent storage) of regions that dont override global settings. Default: 'none' -* `global_password` - (Optional) Password to access the database of regions that dont override global settings. If left empty, the password will be generated automatically -* `global_alert` - (Optional) A block defining Redis database alert of regions that dont override global settings, documented below, can be specified multiple times -* `global_source_ips` - (Optional) List of source IP addresses or subnet masks of regions that dont override global settings. If specified, Redis clients will be able to connect to this database only from within the specified source IP addresses ranges (example: ['192.168.10.0/32', '192.168.12.0/24']) +* `global_password` - (Optional) Password to access the database of regions that don't override global settings. If left empty, the password will be generated automatically +* `global_alert` - (Optional) A block defining Redis database alert of regions that don't override global settings, documented below, can be specified multiple times +* `global_source_ips` - (Optional) List of source IP addresses or subnet masks of regions that don't override global settings. If specified, Redis clients will be able to connect to this database only from within the specified source IP addresses ranges (example: ['192.168.10.0/32', '192.168.12.0/24']) +* `port` - (Optional) TCP port on which the database is available - must be between 10000 and 19999. * `override_region` - (Optional) Override region specific configuration, documented below @@ -102,12 +103,20 @@ The `override_region` block supports: * `override_global_password` - (Optional) If specified, this regional instance of an Active-Active database password will be used to access the database * `override_global_source_ips` - (Optional) List of regional instance of an Active-Active database source IP addresses or subnet masks. If specified, Redis clients will be able to connect to this database only from within the specified source IP addresses ranges (example: ['192.168.10.0/32', '192.168.12.0/24'] ) * `override_global_data_persistence` - (Optional) Regional instance of an Active-Active database data persistence rate (in persistent storage) +* `remote_backup` - (Optional) Specifies the backup options for the database in this region, documented below The `override_global_alert` block supports: * `name` - (Required) Alert name * `value` - (Required) Alert value +The `remote_backup` block supports: + +* `interval` (Required) - Defines the frequency of the automatic backup +* `time_utc` (Optional) - Defines the hour automatic backups are made - only applicable when interval is `every-12-hours` or `every-24-hours` +* `storage_type` (Required) - Defines the provider of the storage location +* `storage_path` (Required) - Defines a URI representing the backup storage location + ### Timeouts The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: @@ -129,5 +138,5 @@ The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/l $ terraform import rediscloud_active_active_subscription_database.database-resource 123456/12345678 ``` -Note: Due to constraints in the Redis Cloud API, the import process will not import global attributes or override region attributes. If you wish to use these attributes in your Terraform configuraton, you will need to manually add them to your Terraform configuration and run `terraform apply` to update the database. +Note: Due to constraints in the Redis Cloud API, the import process will not import global attributes or override region attributes. If you wish to use these attributes in your Terraform configuration, you will need to manually add them to your Terraform configuration and run `terraform apply` to update the database. diff --git a/docs/resources/rediscloud_active_active_subscription_peering.md b/docs/resources/rediscloud_active_active_subscription_peering.md index 430d93e0..afabc34b 100644 --- a/docs/resources/rediscloud_active_active_subscription_peering.md +++ b/docs/resources/rediscloud_active_active_subscription_peering.md @@ -77,7 +77,8 @@ The following arguments are supported: * `source_region` - (Required) Name of the region to create the VPC peering from * `destination_region` - (Required) Name of the region to create the VPC peering to * `vpc_id` - (Required) Identifier of the VPC to be peered -* `vpc_cidr` - (Required) CIDR range of the VPC to be peered +* `vpc_cidr` - (Optional) CIDR range of the VPC to be peered. Either this or `vpc_cidrs` must be specified +* `vpc_cidrs` - (Optional) CIDR ranges of the VPC to be peered. Either this or `vpc_cidr` must be specified **GCP ONLY:** * `gcp_project_id` - (Required) GCP project ID that the VPC to be peered lives in diff --git a/docs/resources/rediscloud_subscription_database.md b/docs/resources/rediscloud_subscription_database.md index e970ca07..bdc2e74a 100644 --- a/docs/resources/rediscloud_subscription_database.md +++ b/docs/resources/rediscloud_subscription_database.md @@ -100,7 +100,7 @@ The following arguments are supported: * `throughput_measurement_by` - (Required) Throughput measurement method, (either ‘number-of-shards’ or ‘operations-per-second’) * `throughput_measurement_value` - (Required) Throughput value (as applies to selected measurement method) * `memory_limit_in_gb` - (Required) Maximum memory usage for this specific database -* `protocol` - (Optional) The protocol that will be used to access the database, (either ‘redis’ or 'memcached’) Default: ‘redis’ +* `protocol` - (Optional) The protocol that will be used to access the database, (either ‘redis’ or ‘memcached’) Default: ‘redis’ * `support_oss_cluster_api` - (Optional) Support Redis open-source (OSS) Cluster API. Default: ‘false’ * `external_endpoint_for_oss_cluster_api` - (Optional) Should use the external endpoint for open-source (OSS) Cluster API. Can only be enabled if OSS Cluster API support is enabled. Default: 'false' @@ -122,6 +122,8 @@ The following arguments are supported: [the documentation on clustering](https://docs.redislabs.com/latest/rc/concepts/clustering/) for more information on the hashing policy. This cannot be set when `support_oss_cluster_api` is set to true. * `enable_tls` - (Optional) Use TLS for authentication. Default: ‘false’ +* `port` - (Optional) TCP port on which the database is available - must be between 10000 and 19999. +* `remote_backup` (Optional) Specifies the backup options for the database, documented below The `alert` block supports: @@ -145,6 +147,13 @@ The `modules` list supports: ] ``` +The `remote_backup` block supports: + +* `interval` (Required) - Defines the frequency of the automatic backup +* `time_utc` (Optional) - Defines the hour automatic backups are made - only applicable when interval is `every-12-hours` or `every-24-hours` +* `storage_type` (Required) - Defines the provider of the storage location +* `storage_path` (Required) - Defines a URI representing the backup storage location + ### Timeouts The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: diff --git a/docs/resources/rediscloud_subscription_peering.md b/docs/resources/rediscloud_subscription_peering.md index 59b675c3..9a0fe183 100644 --- a/docs/resources/rediscloud_subscription_peering.md +++ b/docs/resources/rediscloud_subscription_peering.md @@ -75,7 +75,8 @@ The following arguments are supported: * `aws_account_id` - (Required AWS) AWS account ID that the VPC to be peered lives in * `region` - (Required AWS) AWS Region that the VPC to be peered lives in * `vpc_id` - (Required AWS) Identifier of the VPC to be peered -* `vpc_cidr` - (Required AWS) CIDR range of the VPC to be peered +* `vpc_cidr` - (Optional) CIDR range of the VPC to be peered. Either this or `vpc_cidrs` must be specified +* `vpc_cidrs` - (Optional) CIDR ranges of the VPC to be peered. Either this or `vpc_cidr` must be specified **GCP ONLY:** * `gcp_project_id` - (Required GCP) GCP project ID that the VPC to be peered lives in diff --git a/go.mod b/go.mod index 3edfa79a..c6b6493d 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/RedisLabs/terraform-provider-rediscloud go 1.17 require ( - github.com/RedisLabs/rediscloud-go-api v0.2.0 + github.com/RedisLabs/rediscloud-go-api v0.3.0 github.com/bflad/tfproviderlint v0.28.1 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/terraform-plugin-sdk/v2 v2.24.1 diff --git a/go.sum b/go.sum index ee6f8f1a..212d66d3 100644 --- a/go.sum +++ b/go.sum @@ -386,8 +386,8 @@ github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugX github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 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.2.0 h1:tkS2AIMLLlI3yeW22dT5DP2BvaYO+K7xvytJvFo8z04= -github.com/RedisLabs/rediscloud-go-api v0.2.0/go.mod h1:EJXWMnC2ZSM5m7k2TCnmxenYv57o6yGlZXo0ZVnMgIs= +github.com/RedisLabs/rediscloud-go-api v0.3.0 h1:IiQZ8uFoyZVWIRytFMykiD0ohRijmQcAI3YeAT1kEgg= +github.com/RedisLabs/rediscloud-go-api v0.3.0/go.mod h1:EJXWMnC2ZSM5m7k2TCnmxenYv57o6yGlZXo0ZVnMgIs= 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.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= diff --git a/provider/datasource_rediscloud_subscription_peerings_test.go b/provider/datasource_rediscloud_subscription_peerings_test.go index caf37d4e..bdab5fd1 100644 --- a/provider/datasource_rediscloud_subscription_peerings_test.go +++ b/provider/datasource_rediscloud_subscription_peerings_test.go @@ -33,7 +33,7 @@ func TestAccDataSourceRedisCloudSubscriptionPeerings_basic(t *testing.T) { ) dataSourceName := "data.rediscloud_subscription_peerings.example" - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAwsPeeringPreCheck(t); testAccAwsPreExistingCloudAccountPreCheck(t) }, ProviderFactories: providerFactories, CheckDestroy: testAccCheckSubscriptionDestroy, diff --git a/provider/datasource_rediscloud_subscription_test.go b/provider/datasource_rediscloud_subscription_test.go index 4daa8cdf..df8afa25 100644 --- a/provider/datasource_rediscloud_subscription_test.go +++ b/provider/datasource_rediscloud_subscription_test.go @@ -17,7 +17,7 @@ func TestAccDataSourceRedisCloudSubscription_basic(t *testing.T) { dataSourceName := "data.rediscloud_subscription.example" testCloudAccountName := os.Getenv("AWS_TEST_CLOUD_ACCOUNT_NAME") - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAwsPreExistingCloudAccountPreCheck(t) }, ProviderFactories: providerFactories, CheckDestroy: testAccCheckSubscriptionDestroy, diff --git a/provider/provider.go b/provider/provider.go index 56edf475..ffbfcd30 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -69,7 +69,7 @@ func New(version string) func() *schema.Provider { } // Lock that must be acquired when modifying something related to a subscription as only one _thing_ can modify a subscription and all sub-resources at any time -var subscriptionMutex = NewPerIdLock() +var subscriptionMutex = newPerIdLock() type apiClient struct { client *rediscloud_api.Client diff --git a/provider/resource_rediscloud_active_active_subscription_database.go b/provider/resource_rediscloud_active_active_subscription_database.go index 08d311b6..00985d60 100644 --- a/provider/resource_rediscloud_active_active_subscription_database.go +++ b/provider/resource_rediscloud_active_active_subscription_database.go @@ -3,6 +3,7 @@ package provider import ( "context" "log" + "strings" "time" "github.com/RedisLabs/rediscloud-go-api/redis" @@ -45,6 +46,23 @@ func resourceRedisCloudActiveActiveSubscriptionDatabase() *schema.Resource { Delete: schema.DefaultTimeout(10 * time.Minute), }, + CustomizeDiff: func(ctx context.Context, diff *schema.ResourceDiff, i interface{}) error { + var keys []string + for _, key := range diff.GetChangedKeysPrefix("override_region") { + if strings.HasSuffix(key, "time_utc") { + keys = append(keys, strings.TrimSuffix(key, ".0.time_utc")) + } + } + + for _, key := range keys { + if err := remoteBackupIntervalSetCorrectly(key)(ctx, diff, i); err != nil { + return err + } + } + + return nil + }, + Schema: map[string]*schema.Schema{ "subscription_id": { Description: "Identifier of the subscription", @@ -191,6 +209,39 @@ func resourceRedisCloudActiveActiveSubscriptionDatabase() *schema.Resource { Type: schema.TypeString, Optional: true, }, + "remote_backup": { + Description: "An object that specifies the backup options for the database in this region", + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "interval": { + Description: "Defines the frequency of the automatic backup", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validateDiagFunc(validation.StringInSlice(databases.BackupIntervals(), false)), + }, + "time_utc": { + Description: "Defines the hour automatic backups are made - only applicable when interval is `every-12-hours` or `every-24-hours`", + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: isTime(), + }, + "storage_type": { + Description: "Defines the provider of the storage location", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validateDiagFunc(validation.StringInSlice(databases.BackupStorageTypes(), false)), + }, + "storage_path": { + Description: "Defines a URI representing the backup storage location", + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, }, }, }, @@ -210,6 +261,13 @@ func resourceRedisCloudActiveActiveSubscriptionDatabase() *schema.Resource { Type: schema.TypeString, }, }, + "port": { + Description: "TCP port on which the database is available", + Type: schema.TypeInt, + ValidateDiagFunc: validateDiagFunc(validation.IntBetween(10000, 19999)), + Optional: true, + ForceNew: true, + }, }, } } @@ -286,6 +344,10 @@ func resourceRedisCloudActiveActiveSubscriptionDatabaseCreate(ctx context.Contex createDatabase.GlobalPassword = redis.String(globalPassword) } + if v, ok := d.GetOk("port"); ok { + createDatabase.PortNumber = redis.Int(v.(int)) + } + // Confirm Subscription Active status before creating database err = waitForSubscriptionToBeActive(ctx, subId, api) if err != nil { @@ -416,6 +478,8 @@ func resourceRedisCloudActiveActiveSubscriptionDatabaseRead(ctx context.Context, regionDbConfig["override_global_alert"] = flattenAlerts(regionDb.Alerts) } + regionDbConfig["remote_backup"] = flattenBackupPlan(regionDb.Backup, getStateRemoteBackup(d, redis.StringValue(regionDb.Region)), "") + regionDbConfigs = append(regionDbConfigs, regionDbConfig) } @@ -500,8 +564,8 @@ func resourceRedisCloudActiveActiveSubscriptionDatabaseUpdate(ctx context.Contex // Make a list of region-specific source IPs for use in the regions list below var overrideSourceIps []*string - for _, source_ip := range dbRegion["override_global_source_ips"].(*schema.Set).List() { - overrideSourceIps = append(overrideSourceIps, redis.String(source_ip.(string))) + for _, sourceIp := range dbRegion["override_global_source_ips"].(*schema.Set).List() { + overrideSourceIps = append(overrideSourceIps, redis.String(sourceIp.(string))) } regionProps := &databases.LocalRegionProperties{ @@ -533,6 +597,9 @@ func resourceRedisCloudActiveActiveSubscriptionDatabaseUpdate(ctx context.Contex regionProps.Password = redis.String(d.Get("global_password").(string)) } } + + regionProps.RemoteBackup = buildBackupPlan(dbRegion["remote_backup"], nil) + regions = append(regions, regionProps) } @@ -608,6 +675,16 @@ func getStateOverrideRegion(d *schema.ResourceData, regionName string) map[strin return nil } +func getStateRemoteBackup(d *schema.ResourceData, regionName string) []interface{} { + for _, region := range d.Get("override_region").(*schema.Set).List() { + dbRegion := region.(map[string]interface{}) + if dbRegion["name"].(string) == regionName { + return dbRegion["remote_backup"].([]interface{}) + } + } + return nil +} + func getStateAlertsFromDbRegion(dbRegion map[string]interface{}) []*databases.UpdateAlert { // Make a list of region-specific alert configurations for use in the regions list below if dbRegion == nil { diff --git a/provider/resource_rediscloud_active_active_subscription_database_test.go b/provider/resource_rediscloud_active_active_subscription_database_test.go index ed796cbc..a0f9bd68 100644 --- a/provider/resource_rediscloud_active_active_subscription_database_test.go +++ b/provider/resource_rediscloud_active_active_subscription_database_test.go @@ -3,6 +3,8 @@ package provider import ( "context" "fmt" + "os" + "regexp" "strconv" "testing" @@ -23,7 +25,7 @@ func TestAccResourceRedisCloudActiveActiveSubscriptionDatabase_CRUDI(t *testing. var subId int - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAwsPreExistingCloudAccountPreCheck(t) }, ProviderFactories: providerFactories, CheckDestroy: testAccCheckActiveActiveSubscriptionDestroy, @@ -142,33 +144,74 @@ func TestAccResourceRedisCloudActiveActiveSubscriptionDatabase_CRUDI(t *testing. }) } +func TestAccResourceRedisCloudActiveActiveSubscriptionDatabase_optionalAttributes(t *testing.T) { + // Test that attributes can be optional, either by setting them or not having them set when compared to CRUDI test + subscriptionName := acctest.RandomWithPrefix(testResourcePrefix) + "-subscription" + name := acctest.RandomWithPrefix(testResourcePrefix) + "-database" + password := acctest.RandString(20) + resourceName := "rediscloud_active_active_subscription_database.example" + portNumber := 10101 + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccAwsPreExistingCloudAccountPreCheck(t) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckActiveActiveSubscriptionDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccResourceRedisCloudActiveActiveSubscriptionDatabaseOptionalAttributes, subscriptionName, name, password, portNumber), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "port", strconv.Itoa(portNumber)), + ), + }, + }, + }) +} + +func TestAccResourceRedisCloudActiveActiveSubscriptionDatabase_timeUtcRequiresValidInterval(t *testing.T) { + name := acctest.RandomWithPrefix(testResourcePrefix) + testCloudAccountName := os.Getenv("AWS_TEST_CLOUD_ACCOUNT_NAME") + password := acctest.RandString(20) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccAwsPreExistingCloudAccountPreCheck(t) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckSubscriptionDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccResourceRedisCloudActiveActiveSubscriptionDatabaseInvalidTimeUtc, testCloudAccountName, name, password), + ExpectError: regexp.MustCompile("unexpected value at override_region\\.\\d*\\.remote_backup\\.0\\.time_utc - time_utc can only be set when interval is either every-24-hours or every-12-hours"), + }, + }, + }) +} + const activeActiveSubscriptionBoilerplate = ` data "rediscloud_payment_method" "card" { card_type = "Visa" } - - resource "rediscloud_active_active_subscription" "example" { - name = "%s" - payment_method_id = data.rediscloud_payment_method.card.id - cloud_provider = "AWS" - - creation_plan { - memory_limit_in_gb = 1 - quantity = 1 - region { - region = "us-east-1" - networking_deployment_cidr = "192.168.0.0/24" - write_operations_per_second = 1000 - read_operations_per_second = 1000 - } - region { - region = "us-east-2" - networking_deployment_cidr = "10.0.1.0/24" - write_operations_per_second = 1000 - read_operations_per_second = 1000 - } + + resource "rediscloud_active_active_subscription" "example" { + name = "%s" + payment_method_id = data.rediscloud_payment_method.card.id + cloud_provider = "AWS" + + creation_plan { + memory_limit_in_gb = 1 + quantity = 1 + region { + region = "us-east-1" + networking_deployment_cidr = "192.168.0.0/24" + write_operations_per_second = 1000 + read_operations_per_second = 1000 + } + region { + region = "us-east-2" + networking_deployment_cidr = "10.0.1.0/24" + write_operations_per_second = 1000 + read_operations_per_second = 1000 + } + } } - } ` // Create and Read tests @@ -239,3 +282,74 @@ resource "rediscloud_active_active_subscription_database" "example" { memory_limit_in_gb = 1 } ` + +const testAccResourceRedisCloudActiveActiveSubscriptionDatabaseOptionalAttributes = activeActiveSubscriptionBoilerplate + ` +resource "rediscloud_active_active_subscription_database" "example" { + subscription_id = rediscloud_active_active_subscription.example.id + name = "%s" + memory_limit_in_gb = 3 + support_oss_cluster_api = false + external_endpoint_for_oss_cluster_api = false + enable_tls = false + + global_data_persistence = "none" + global_password = "%s" + global_source_ips = ["192.168.0.0/16", "192.170.0.0/16"] + global_alert { + name = "dataset-size" + value = 40 + } + override_region { + name = "us-east-1" + override_global_data_persistence = "aof-every-write" + override_global_source_ips = ["192.175.0.0/16"] + override_global_password = "region-specific-password" + override_global_alert { + name = "dataset-size" + value = 42 + } + } + override_region { + name = "us-east-2" + } + port = %d +} +` + +const testAccResourceRedisCloudActiveActiveSubscriptionDatabaseInvalidTimeUtc = activeActiveSubscriptionBoilerplate + ` +resource "rediscloud_active_active_subscription_database" "example" { + subscription_id = rediscloud_active_active_subscription.example.id + name = "%s" + memory_limit_in_gb = 3 + support_oss_cluster_api = false + external_endpoint_for_oss_cluster_api = false + enable_tls = false + + global_data_persistence = "none" + global_password = "%s" + global_source_ips = ["192.168.0.0/16", "192.170.0.0/16"] + global_alert { + name = "dataset-size" + value = 40 + } + override_region { + name = "us-east-1" + override_global_data_persistence = "aof-every-write" + override_global_source_ips = ["192.175.0.0/16"] + override_global_password = "region-specific-password" + override_global_alert { + name = "dataset-size" + value = 42 + } + remote_backup { + interval = "every-6-hours" + time_utc = "16:00" + storage_type = "aws-s3" + storage_path = "uri://interval.not.12.or.24.hours.test" + } + } + override_region { + name = "us-east-2" + } +} +` diff --git a/provider/resource_rediscloud_active_active_subscription_peering.go b/provider/resource_rediscloud_active_active_subscription_peering.go index 436b86f4..dd4fd91c 100644 --- a/provider/resource_rediscloud_active_active_subscription_peering.go +++ b/provider/resource_rediscloud_active_active_subscription_peering.go @@ -89,8 +89,24 @@ func resourceRedisCloudActiveActiveSubscriptionPeering() *schema.Resource { Description: "CIDR range of the VPC to be peered", Type: schema.TypeString, ForceNew: true, + Computed: true, Optional: true, ValidateDiagFunc: validateDiagFunc(validation.IsCIDR), + ConflictsWith: []string{"vpc_cidrs"}, + ExactlyOneOf: []string{"vpc_cidrs", "vpc_cidr"}, + }, + "vpc_cidrs": { + Description: "CIDR ranges of the VPC to be peered", + Type: schema.TypeSet, + Optional: true, + Computed: true, + ForceNew: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validateDiagFunc(validation.IsCIDR), + }, + ConflictsWith: []string{"vpc_cidr"}, + ExactlyOneOf: []string{"vpc_cidrs", "vpc_cidr"}, }, "gcp_project_id": { Description: "GCP project ID that the VPC to be peered lives in", @@ -170,16 +186,18 @@ func resourceRedisCloudSubscriptionActiveActivePeeringCreate(ctx context.Context return diag.Errorf("`vpc_id` must be set when `provider_name` is `AWS`") } - vpcCIDR, ok := d.GetOk("vpc_cidr") - if !ok { - return diag.Errorf("`vpc_cidrs` must be set when `provider_name` is `AWS`") + if vpcCIDR, ok := d.GetOk("vpc_cidr"); ok { + peeringRequest.VPCCidr = redis.String(vpcCIDR.(string)) + } else if vpcCIDRs, ok := d.GetOk("vpc_cidrs"); ok { + peeringRequest.VPCCidrs = setToStringSlice(vpcCIDRs.(*schema.Set)) + } else { + return diag.Errorf("`vpc_cidr` or `vpc_cidrs` must be set when `provider_name` is `AWS`") } peeringRequest.SourceRegion = redis.String(sourceRegion.(string)) peeringRequest.DestinationRegion = redis.String(destinationRegion.(string)) peeringRequest.AWSAccountID = redis.String(awsAccountID.(string)) peeringRequest.VPCId = redis.String(vpcID.(string)) - peeringRequest.VPCCidr = redis.String(vpcCIDR.(string)) } if providerName == "GCP" { @@ -272,15 +290,38 @@ func resourceRedisCloudSubscriptionActiveActivePeeringRead(ctx context.Context, if err := d.Set("vpc_id", redis.StringValue(peering.VPCId)); err != nil { return diag.FromErr(err) } - if err := d.Set("vpc_cidr", redis.StringValue(peering.VPCCidr)); err != nil { - return diag.FromErr(err) - } if err := d.Set("source_region", redis.StringValue(sourceRegion)); err != nil { return diag.FromErr(err) } if err := d.Set("destination_region", redis.StringValue(peering.RegionName)); err != nil { return diag.FromErr(err) } + + // A peering that was created with `VPCCidrs` containing a single item will be read back with the `VPCCidr` set + // and `VPCCidrs` unset. + var vpcCidr *string + if peering.VPCCidr != nil { + vpcCidr = peering.VPCCidr + } + + var cidrs []string + if len(peering.VPCCidrs) != 0 { + for _, cidr := range peering.VPCCidrs { + if vpcCidr == nil { + vpcCidr = cidr.VPCCidr + } + cidrs = append(cidrs, redis.StringValue(cidr.VPCCidr)) + } + } else { + cidrs = []string{redis.StringValue(vpcCidr)} + } + + if err := d.Set("vpc_cidr", redis.StringValue(vpcCidr)); err != nil { + return diag.FromErr(err) + } + if err := d.Set("vpc_cidrs", cidrs); err != nil { + return diag.FromErr(err) + } } if providerName == "GCP" { if err := d.Set("gcp_project_id", redis.StringValue(peering.GCPProjectUID)); err != nil { diff --git a/provider/resource_rediscloud_active_active_subscription_peering_test.go b/provider/resource_rediscloud_active_active_subscription_peering_test.go index 73b3cb43..8c490361 100644 --- a/provider/resource_rediscloud_active_active_subscription_peering_test.go +++ b/provider/resource_rediscloud_active_active_subscription_peering_test.go @@ -59,6 +59,8 @@ func TestAccResourceRedisCloudActiveActiveSubscriptionPeering_aws(t *testing.T) resource.TestCheckResourceAttrSet(resourceName, "aws_account_id"), resource.TestCheckResourceAttrSet(resourceName, "vpc_id"), resource.TestCheckResourceAttr(resourceName, "vpc_cidr", cidrRange), + resource.TestCheckResourceAttr(resourceName, "vpc_cidrs.#", "1"), + resource.TestCheckResourceAttr(resourceName, "vpc_cidrs.0", cidrRange), resource.TestCheckResourceAttrSet(resourceName, "source_region"), resource.TestCheckResourceAttrSet(resourceName, "destination_region"), resource.TestCheckResourceAttrSet(resourceName, "aws_peering_id"), diff --git a/provider/resource_rediscloud_active_active_subscription_regions_test.go b/provider/resource_rediscloud_active_active_subscription_regions_test.go index f4a67502..7789053d 100644 --- a/provider/resource_rediscloud_active_active_subscription_regions_test.go +++ b/provider/resource_rediscloud_active_active_subscription_regions_test.go @@ -20,7 +20,7 @@ func TestAccResourceRedisCloudActiveActiveSubscriptionRegions_CRUDI(t *testing.T var subId int - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProviderFactories: providerFactories, CheckDestroy: testAccCheckActiveActiveSubscriptionDestroy, diff --git a/provider/resource_rediscloud_active_active_subscription_test.go b/provider/resource_rediscloud_active_active_subscription_test.go index b318c8c7..bba2a5a0 100644 --- a/provider/resource_rediscloud_active_active_subscription_test.go +++ b/provider/resource_rediscloud_active_active_subscription_test.go @@ -28,7 +28,7 @@ func TestAccResourceRedisCloudActiveActiveSubscription_CRUDI(t *testing.T) { var subId int - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProviderFactories: providerFactories, CheckDestroy: testAccCheckActiveActiveSubscriptionDestroy, @@ -117,7 +117,7 @@ func TestAccResourceRedisCloudActiveActiveSubscription_createUpdateContractPayme updatedName := fmt.Sprintf("%v-updatedName", name) resourceName := "rediscloud_active_active_subscription.example" - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAwsPreExistingCloudAccountPreCheck(t) }, ProviderFactories: providerFactories, CheckDestroy: testAccCheckActiveActiveSubscriptionDestroy, @@ -155,7 +155,7 @@ func TestAccResourceRedisCloudActiveActiveSubscription_createUpdateMarketplacePa updatedName := fmt.Sprintf("%v-updatedName", name) resourceName := "rediscloud_active_active_subscription.example" - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAwsPreExistingCloudAccountPreCheck(t) }, ProviderFactories: providerFactories, CheckDestroy: testAccCheckActiveActiveSubscriptionDestroy, diff --git a/provider/resource_rediscloud_subscription_database.go b/provider/resource_rediscloud_subscription_database.go index f05fc97b..ab7005ce 100644 --- a/provider/resource_rediscloud_subscription_database.go +++ b/provider/resource_rediscloud_subscription_database.go @@ -46,6 +46,8 @@ func resourceRedisCloudSubscriptionDatabase() *schema.Resource { Delete: schema.DefaultTimeout(10 * time.Minute), }, + CustomizeDiff: remoteBackupIntervalSetCorrectly("remote_backup"), + Schema: map[string]*schema.Schema{ "subscription_id": { Description: "Identifier of the subscription", @@ -150,10 +152,11 @@ func resourceRedisCloudSubscriptionDatabase() *schema.Resource { Default: "", }, "periodic_backup_path": { - Description: "Path that will be used to store database backup files", - Type: schema.TypeString, - Optional: true, - Default: "", + Description: "Path that will be used to store database backup files", + Type: schema.TypeString, + Optional: true, + Default: "", + ConflictsWith: []string{"remote_backup"}, }, "replica_of": { Description: "Set of Redis database URIs, in the format `redis://user:password@host:port`, that this database will be a replica of. If the URI provided is Redis Labs Cloud instance, only host and port should be provided", @@ -231,6 +234,48 @@ func resourceRedisCloudSubscriptionDatabase() *schema.Resource { Optional: true, Default: false, }, + "port": { + Description: "TCP port on which the database is available", + Type: schema.TypeInt, + ValidateDiagFunc: validateDiagFunc(validation.IntBetween(10000, 19999)), + Optional: true, + ForceNew: true, + }, + "remote_backup": { + Description: "An object that specifies the backup options for the database", + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ConflictsWith: []string{"periodic_backup_path"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "interval": { + Description: "Defines the frequency of the automatic backup", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validateDiagFunc(validation.StringInSlice(databases.BackupIntervals(), false)), + }, + "time_utc": { + Description: "Defines the hour automatic backups are made - only applicable when interval is `every-12-hours` or `every-24-hours`", + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: isTime(), + DiffSuppressFunc: skipDiffIfIntervalIs12And12HourTimeDiff, + }, + "storage_type": { + Description: "Defines the provider of the storage location", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validateDiagFunc(validation.StringInSlice(databases.BackupStorageTypes(), false)), + }, + "storage_path": { + Description: "Defines a URI representing the backup storage location", + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, }, } } @@ -293,8 +338,9 @@ func resourceRedisCloudSubscriptionDatabaseCreate(ctx context.Context, d *schema By: redis.String(throughputMeasurementBy), Value: redis.Int(throughputMeasurementValue), }, - Modules: createModules, - Alerts: createAlerts, + Modules: createModules, + Alerts: createAlerts, + RemoteBackup: buildBackupPlan(d.Get("remote_backup").([]interface{}), d.Get("periodic_backup_path")), } if password != "" { createDatabase.Password = redis.String(password) @@ -308,6 +354,10 @@ func resourceRedisCloudSubscriptionDatabaseCreate(ctx context.Context, d *schema createDatabase.Protocol = redis.String(v.(string)) } + if v, ok := d.GetOk("port"); ok { + createDatabase.PortNumber = redis.Int(v.(int)) + } + dbId, err := api.client.Database.Create(ctx, subId, createDatabase) if err != nil { return diag.FromErr(err) @@ -445,6 +495,10 @@ func resourceRedisCloudSubscriptionDatabaseRead(ctx context.Context, d *schema.R return diag.FromErr(err) } + if err := d.Set("remote_backup", flattenBackupPlan(db.Backup, d.Get("remote_backup").([]interface{}), d.Get("periodic_backup_path").(string))); err != nil { + return diag.FromErr(err) + } + return diags } @@ -509,6 +563,7 @@ func resourceRedisCloudSubscriptionDatabaseUpdate(ctx context.Context, d *schema DataEvictionPolicy: redis.String(d.Get("data_eviction").(string)), SourceIP: setToStringSlice(d.Get("source_ips").(*schema.Set)), Alerts: alerts, + RemoteBackup: buildBackupPlan(d.Get("remote_backup").([]interface{}), d.Get("periodic_backup_path")), } if len(setToStringSlice(d.Get("source_ips").(*schema.Set))) == 0 { update.SourceIP = []*string{redis.String("0.0.0.0/0")} @@ -571,6 +626,58 @@ func resourceRedisCloudSubscriptionDatabaseUpdate(ctx context.Context, d *schema return resourceRedisCloudSubscriptionDatabaseRead(ctx, d, meta) } +func buildBackupPlan(data interface{}, periodicBackupPath interface{}) *databases.DatabaseBackupConfig { + var d map[string]interface{} + + switch v := data.(type) { + case []interface{}: + if len(v) != 1 { + if periodicBackupPath == nil { + return &databases.DatabaseBackupConfig{Active: redis.Bool(false)} + } else { + return nil + } + } + d = v[0].(map[string]interface{}) + default: + d = v.(map[string]interface{}) + } + + config := databases.DatabaseBackupConfig{ + Active: redis.Bool(true), + Interval: redis.String(d["interval"].(string)), + StorageType: redis.String(d["storage_type"].(string)), + StoragePath: redis.String(d["storage_path"].(string)), + } + + if v := d["time_utc"].(string); v != "" { + config.TimeUTC = redis.String(v) + } + + return &config +} + +func flattenBackupPlan(backup *databases.Backup, existing []interface{}, periodicBackupPath string) []map[string]interface{} { + if backup == nil || !redis.BoolValue(backup.Enabled) || periodicBackupPath != "" { + return nil + } + + storageType := "" + if len(existing) == 1 { + d := existing[0].(map[string]interface{}) + storageType = d["storage_type"].(string) + } + + return []map[string]interface{}{ + { + "interval": redis.StringValue(backup.Interval), + "time_utc": redis.StringValue(backup.TimeUTC), + "storage_type": storageType, + "storage_path": redis.StringValue(backup.Destination), + }, + } +} + func toDatabaseId(id string) (int, int, error) { parts := strings.Split(id, "/") @@ -598,3 +705,51 @@ func toDatabaseId(id string) (int, int, error) { return subId, dbId, nil } + +func skipDiffIfIntervalIs12And12HourTimeDiff(k, oldValue, newValue string, d *schema.ResourceData) bool { + // If interval is set to every 12 hours and the `time_utc` is in the afternoon, + // then the API will return the _morning_ time when queried. + // `interval` is assumed to be an attribute within the same block as the attribute being diffed. + + parts := strings.Split(k, ".") + parts[len(parts)-1] = "interval" + + var interval = d.Get(strings.Join(parts, ".")) + + if interval != databases.BackupIntervalEvery12Hours { + return false + } + + oldTime, err := time.Parse("15:04", oldValue) + if err != nil { + return false + } + newTime, err := time.Parse("15:04", newValue) + if err != nil { + return false + } + + return oldTime.Minute() == newTime.Minute() && oldTime.Add(12*time.Hour).Hour() == newTime.Hour() +} + +func remoteBackupIntervalSetCorrectly(key string) schema.CustomizeDiffFunc { + // Validate multiple attributes - https://github.com/hashicorp/terraform-plugin-sdk/issues/233 + + return func(ctx context.Context, diff *schema.ResourceDiff, i interface{}) error { + if v, ok := diff.GetOk(key); ok { + backups := v.([]interface{}) + if len(backups) == 1 { + v := backups[0].(map[string]interface{}) + + interval := v["interval"].(string) + timeUtc := v["time_utc"].(string) + + if interval != databases.BackupIntervalEvery12Hours && interval != databases.BackupIntervalEvery24Hours && timeUtc != "" { + return fmt.Errorf("unexpected value at %s.0.time_utc - time_utc can only be set when interval is either %s or %s", key, databases.BackupIntervalEvery24Hours, databases.BackupIntervalEvery12Hours) + } + } + } + return nil + } + +} diff --git a/provider/resource_rediscloud_subscription_database_test.go b/provider/resource_rediscloud_subscription_database_test.go index 9c907b5c..84875933 100644 --- a/provider/resource_rediscloud_subscription_database_test.go +++ b/provider/resource_rediscloud_subscription_database_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "regexp" "strconv" "testing" @@ -25,7 +26,7 @@ func TestAccResourceRedisCloudSubscriptionDatabase_CRUDI(t *testing.T) { var subId int - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAwsPreExistingCloudAccountPreCheck(t) }, ProviderFactories: providerFactories, CheckDestroy: testAccCheckSubscriptionDestroy, @@ -138,22 +139,41 @@ func TestAccResourceRedisCloudSubscriptionDatabase_optionalAttributes(t *testing name := acctest.RandomWithPrefix(testResourcePrefix) resourceName := "rediscloud_subscription_database.example" testCloudAccountName := os.Getenv("AWS_TEST_CLOUD_ACCOUNT_NAME") + portNumber := 10101 - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAwsPreExistingCloudAccountPreCheck(t) }, ProviderFactories: providerFactories, CheckDestroy: testAccCheckSubscriptionDestroy, Steps: []resource.TestStep{ { - Config: fmt.Sprintf(testAccResourceRedisCloudSubscriptionDatabaseOptionalAttributes, testCloudAccountName, name), + Config: fmt.Sprintf(testAccResourceRedisCloudSubscriptionDatabaseOptionalAttributes, testCloudAccountName, name, portNumber), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "protocol", "redis"), + resource.TestCheckResourceAttr(resourceName, "port", strconv.Itoa(portNumber)), ), }, }, }) } +func TestAccResourceRedisCloudSubscriptionDatabase_timeUtcRequiresValidInterval(t *testing.T) { + name := acctest.RandomWithPrefix(testResourcePrefix) + testCloudAccountName := os.Getenv("AWS_TEST_CLOUD_ACCOUNT_NAME") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccAwsPreExistingCloudAccountPreCheck(t) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckSubscriptionDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccResourceRedisCloudSubscriptionDatabaseInvalidTimeUtc, testCloudAccountName, name), + ExpectError: regexp.MustCompile("unexpected value at remote_backup\\.0\\.time_utc - time_utc can only be set when interval is either every-24-hours or every-12-hours"), + }, + }, + }) +} + // Tests the multi-modules feature in a database resource. func TestAccResourceRedisCloudSubscriptionDatabase_MultiModules(t *testing.T) { name := acctest.RandomWithPrefix(testResourcePrefix) @@ -269,7 +289,25 @@ resource "rediscloud_subscription_database" "example" { memory_limit_in_gb = 1 data_persistence = "none" throughput_measurement_by = "operations-per-second" - throughput_measurement_value = 1000 + throughput_measurement_value = 1000 + port = %d +} +` + +const testAccResourceRedisCloudSubscriptionDatabaseInvalidTimeUtc = subscriptionBoilerplate + ` +resource "rediscloud_subscription_database" "example" { + subscription_id = rediscloud_subscription.example.id + name = "example-no-protocol" + memory_limit_in_gb = 1 + data_persistence = "none" + throughput_measurement_by = "operations-per-second" + throughput_measurement_value = 1000 + remote_backup { + interval = "every-6-hours" + time_utc = "16:00" + storage_type = "aws-s3" + storage_path = "uri://interval.not.12.or.24.hours.test" + } } ` diff --git a/provider/resource_rediscloud_subscription_peering.go b/provider/resource_rediscloud_subscription_peering.go index d0c3a012..ecb20ff6 100644 --- a/provider/resource_rediscloud_subscription_peering.go +++ b/provider/resource_rediscloud_subscription_peering.go @@ -80,11 +80,26 @@ func resourceRedisCloudSubscriptionPeering() *schema.Resource { ForceNew: true, }, "vpc_cidr": { - Description: "CIDR range of the VPC to be peered", - Type: schema.TypeString, + Description: "CIDR range of the VPC to be peered", + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ConflictsWith: []string{"vpc_cidrs"}, + ExactlyOneOf: []string{"vpc_cidrs", "vpc_cidr"}, + }, + "vpc_cidrs": { + Description: "CIDR ranges of the VPC to be peered", + Type: schema.TypeSet, Optional: true, Computed: true, ForceNew: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validateDiagFunc(validation.IsCIDR), + }, + ConflictsWith: []string{"vpc_cidr"}, + ExactlyOneOf: []string{"vpc_cidrs", "vpc_cidr"}, }, "gcp_project_id": { Description: "GCP project ID that the VPC to be peered lives in", @@ -159,15 +174,17 @@ func resourceRedisCloudSubscriptionPeeringCreate(ctx context.Context, d *schema. return diag.Errorf("`vpc_id` must be set when `provider_name` is `AWS`") } - vpcCIDR, ok := d.GetOk("vpc_cidr") - if !ok { - return diag.Errorf("`vpc_cidr` must be set when `provider_name` is `AWS`") + if vpcCIDR, ok := d.GetOk("vpc_cidr"); ok { + peeringRequest.VPCCidr = redis.String(vpcCIDR.(string)) + } else if vpcCIDRs, ok := d.GetOk("vpc_cidrs"); ok { + peeringRequest.VPCCidrs = setToStringSlice(vpcCIDRs.(*schema.Set)) + } else { + return diag.Errorf("`vpc_cidr` or `vpc_cidrs` must be set when `provider_name` is `AWS`") } peeringRequest.Region = redis.String(region.(string)) peeringRequest.AWSAccountID = redis.String(awsAccountID.(string)) peeringRequest.VPCId = redis.String(vpcID.(string)) - peeringRequest.VPCCidr = redis.String(vpcCIDR.(string)) } if providerName == "GCP" { @@ -254,10 +271,33 @@ func resourceRedisCloudSubscriptionPeeringRead(ctx context.Context, d *schema.Re if err := d.Set("vpc_id", redis.StringValue(peering.VPCId)); err != nil { return diag.FromErr(err) } - if err := d.Set("vpc_cidr", redis.StringValue(peering.VPCCidr)); err != nil { + if err := d.Set("region", redis.StringValue(peering.Region)); err != nil { return diag.FromErr(err) } - if err := d.Set("region", redis.StringValue(peering.Region)); err != nil { + + // A peering that was created with `VPCCidrs` containing a single item will be read back with the `VPCCidr` set + // and `VPCCidrs` unset. + var vpcCidr *string + if peering.VPCCidr != nil { + vpcCidr = peering.VPCCidr + } + + var cidrs []string + if len(peering.VPCCidrs) != 0 { + for _, cidr := range peering.VPCCidrs { + if vpcCidr == nil { + vpcCidr = cidr.VPCCidr + } + cidrs = append(cidrs, redis.StringValue(cidr.VPCCidr)) + } + } else { + cidrs = []string{redis.StringValue(vpcCidr)} + } + + if err := d.Set("vpc_cidr", redis.StringValue(vpcCidr)); err != nil { + return diag.FromErr(err) + } + if err := d.Set("vpc_cidrs", cidrs); err != nil { return diag.FromErr(err) } } diff --git a/provider/resource_rediscloud_subscription_peering_test.go b/provider/resource_rediscloud_subscription_peering_test.go index bbc88b8d..65140922 100644 --- a/provider/resource_rediscloud_subscription_peering_test.go +++ b/provider/resource_rediscloud_subscription_peering_test.go @@ -62,7 +62,9 @@ func TestAccResourceRedisCloudSubscriptionPeering_aws(t *testing.T) { resource.TestCheckResourceAttrSet(resourceName, "provider_name"), resource.TestCheckResourceAttrSet(resourceName, "aws_account_id"), resource.TestCheckResourceAttrSet(resourceName, "vpc_id"), - resource.TestCheckResourceAttrSet(resourceName, "vpc_cidr"), + resource.TestCheckResourceAttr(resourceName, "vpc_cidr", cidrRange), + resource.TestCheckResourceAttr(resourceName, "vpc_cidrs.#", "1"), + resource.TestCheckResourceAttr(resourceName, "vpc_cidrs.0", cidrRange), resource.TestCheckResourceAttrSet(resourceName, "region"), resource.TestCheckResourceAttrSet(resourceName, "aws_peering_id"), ), @@ -168,7 +170,6 @@ resource "rediscloud_subscription" "example" { support_oss_cluster_api=false throughput_measurement_by = "operations-per-second" throughput_measurement_value = 10000 - modules = [] } } @@ -178,7 +179,7 @@ resource "rediscloud_subscription_peering" "test" { region = "%s" aws_account_id = "%s" vpc_id = "%s" - vpc_cidr = "%s" + vpc_cidrs = ["%s"] } ` diff --git a/provider/resource_rediscloud_subscription_test.go b/provider/resource_rediscloud_subscription_test.go index 630dc391..de7dbf80 100644 --- a/provider/resource_rediscloud_subscription_test.go +++ b/provider/resource_rediscloud_subscription_test.go @@ -31,7 +31,7 @@ func TestAccResourceRedisCloudSubscription_CRUDI(t *testing.T) { var subId int - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAwsPreExistingCloudAccountPreCheck(t) }, ProviderFactories: providerFactories, CheckDestroy: testAccCheckSubscriptionDestroy, @@ -117,7 +117,7 @@ func TestAccResourceRedisCloudSubscription_preferredAZsModulesOptional(t *testin resourceName := "rediscloud_subscription.example" testCloudAccountName := os.Getenv("AWS_TEST_CLOUD_ACCOUNT_NAME") - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAwsPreExistingCloudAccountPreCheck(t) }, ProviderFactories: providerFactories, CheckDestroy: testAccCheckSubscriptionDestroy, @@ -144,7 +144,7 @@ func TestAccResourceRedisCloudSubscription_createUpdateContractPayment(t *testin resourceName := "rediscloud_subscription.example" testCloudAccountName := os.Getenv("AWS_TEST_CLOUD_ACCOUNT_NAME") - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAwsPreExistingCloudAccountPreCheck(t) }, ProviderFactories: providerFactories, CheckDestroy: testAccCheckSubscriptionDestroy, @@ -181,7 +181,7 @@ func TestAccResourceRedisCloudSubscription_createUpdateMarketplacePayment(t *tes resourceName := "rediscloud_subscription.example" testCloudAccountName := os.Getenv("AWS_TEST_CLOUD_ACCOUNT_NAME") - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAwsPreExistingCloudAccountPreCheck(t) }, ProviderFactories: providerFactories, CheckDestroy: testAccCheckSubscriptionDestroy, diff --git a/provider/resource_rediscloud_subscription_tls_test.go b/provider/resource_rediscloud_subscription_tls_test.go index 70d9104d..6982cf5d 100644 --- a/provider/resource_rediscloud_subscription_tls_test.go +++ b/provider/resource_rediscloud_subscription_tls_test.go @@ -42,7 +42,7 @@ func TestAccResourceRedisCloudSubscription_createWithDatabaseWithEnabledTlsAndSs var subId int - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) testAccAwsPreExistingCloudAccountPreCheck(t) @@ -124,7 +124,7 @@ func TestAccResourceRedisCloudSubscription_createWithDatabaseWithEnabledTlsAndEm var subId int - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAwsPreExistingCloudAccountPreCheck(t) }, ProviderFactories: providerFactories, CheckDestroy: testAccCheckSubscriptionDestroy, @@ -203,7 +203,7 @@ func TestAccResourceRedisCloudSubscription_createWithDatabaseWithEnabledTlsAndIn invalidClientSslCertificate := os.Getenv("SSL_CERTIFICATE_INVALID") - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) testAccAwsPreExistingCloudAccountPreCheck(t) @@ -233,7 +233,7 @@ func TestAccResourceRedisCloudSubscription_createWithDatabaseAndDisabledTlsAndIn invalidClientSslCertificate := os.Getenv("SSL_CERTIFICATE_INVALID") - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) testAccAwsPreExistingCloudAccountPreCheck(t) diff --git a/provider/utils.go b/provider/utils.go index 8292be46..35cd3cd8 100644 --- a/provider/utils.go +++ b/provider/utils.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "sync" + "time" ) func validateDiagFunc(validateFunc func(interface{}, string) ([]string, []error)) schema.SchemaValidateDiagFunc { @@ -50,7 +51,7 @@ type perIdLock struct { store map[int]*sync.Mutex } -func NewPerIdLock() *perIdLock { +func newPerIdLock() *perIdLock { return &perIdLock{ store: map[int]*sync.Mutex{}, } @@ -81,3 +82,26 @@ func (m *perIdLock) get(id int) *sync.Mutex { func buildResourceId(subId int, id int) string { return fmt.Sprintf("%d/%d", subId, id) } + +func isTime() schema.SchemaValidateDiagFunc { + return func(i interface{}, path cty.Path) diag.Diagnostics { + var diags diag.Diagnostics + + v, ok := i.(string) + if !ok { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Value not a string", + Detail: fmt.Sprintf("Value should be a string rather than %T", i), + }) + } else if _, err := time.Parse("15:04", v); err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Value is not a time", + Detail: fmt.Sprintf("Value should be a valid time, got: %q: %s", i, err), + }) + } + + return diags + } +} diff --git a/provider/utils_test.go b/provider/utils_test.go new file mode 100644 index 00000000..be2b2257 --- /dev/null +++ b/provider/utils_test.go @@ -0,0 +1,28 @@ +package provider + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestIsTime(t *testing.T) { + tests := []struct { + input string + errors bool + }{ + {"0:00", false}, + {"09:00", false}, + {"12:00", false}, + {"24:00", true}, // '24' isn't a valid hour + {"12:00:00", true}, // seconds are invalid + {"blah", true}, // Not a valid time + {"", true}, // Nothing + } + + for _, test := range tests { + t.Run(test.input, func(t *testing.T) { + actual := isTime()(test.input, nil) + assert.Equal(t, test.errors, actual.HasError(), "%+v", actual) + }) + } +}