diff --git a/.github/workflows/terraform_provider_pr.yml b/.github/workflows/terraform_provider_pr.yml index 309d801e..cf80a88c 100644 --- a/.github/workflows/terraform_provider_pr.yml +++ b/.github/workflows/terraform_provider_pr.yml @@ -149,6 +149,19 @@ jobs: go-version-file: go.mod - run: EXECUTE_TESTS=true make testacc TESTARGS='-run="TestAccResourceRedisCloud(PrivateServiceConnect_CRUDI|AclRule_CRUDI)"' + # TODO: remove this after release + # qpf = query performance factor + go_test_smoke_qpf: + name: go test smoke qpf + needs: [ go_build ] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + with: + go-version-file: go.mod + - run: EXECUTE_TESTS=true make testacc TESTARGS='-run="TestAccResourceRedisCloudProDatabase_qpf.*"' + tfproviderlint: name: tfproviderlint needs: [go_build] diff --git a/provider/datasource_rediscloud_pro_database.go b/provider/datasource_rediscloud_pro_database.go index 01b20e1b..8b76ce26 100644 --- a/provider/datasource_rediscloud_pro_database.go +++ b/provider/datasource_rediscloud_pro_database.go @@ -59,6 +59,11 @@ func dataSourceRedisCloudProDatabase() *schema.Resource { Type: schema.TypeFloat, Computed: true, }, + "query_performance_factor": { + Description: "Query performance factor for this specific database", + Type: schema.TypeString, + Computed: true, + }, "support_oss_cluster_api": { Description: "Supports the Redis open-source (OSS) Cluster API", Type: schema.TypeBool, @@ -476,6 +481,10 @@ func dataSourceRedisCloudProDatabaseRead(ctx context.Context, d *schema.Resource return diag.FromErr(err) } + if err := d.Set("query_performance_factor", redis.String(*db.QueryPerformanceFactor)); err != nil { + return diag.FromErr(err) + } + return diags } diff --git a/provider/datasource_rediscloud_pro_database_test.go b/provider/datasource_rediscloud_pro_database_test.go index 1695cf1b..9cec9240 100644 --- a/provider/datasource_rediscloud_pro_database_test.go +++ b/provider/datasource_rediscloud_pro_database_test.go @@ -43,6 +43,7 @@ func TestAccDataSourceRedisCloudProDatabase_basic(t *testing.T) { resource.TestCheckResourceAttrSet(dataSourceById, "public_endpoint"), resource.TestCheckResourceAttrSet(dataSourceById, "private_endpoint"), resource.TestCheckResourceAttr(dataSourceById, "enable_default_user", "true"), + resource.TestCheckResourceAttr(dataSourceById, "query_performance_factor", "2x"), resource.TestCheckResourceAttr(dataSourceByName, "name", "tf-database"), resource.TestCheckResourceAttr(dataSourceByName, "protocol", "redis"), @@ -59,6 +60,7 @@ func TestAccDataSourceRedisCloudProDatabase_basic(t *testing.T) { resource.TestCheckResourceAttrSet(dataSourceByName, "public_endpoint"), resource.TestCheckResourceAttrSet(dataSourceByName, "private_endpoint"), resource.TestCheckResourceAttr(dataSourceByName, "enable_default_user", "true"), + resource.TestCheckResourceAttr(dataSourceByName, "query_performance_factor", "2x"), ), }, }, @@ -94,6 +96,8 @@ resource "rediscloud_subscription" "example" { support_oss_cluster_api=true throughput_measurement_by = "operations-per-second" throughput_measurement_value = 1000 + query_performance_factor = "2x" + modules = ["RediSearch"] } } resource "rediscloud_subscription_database" "example" { @@ -108,6 +112,12 @@ resource "rediscloud_subscription_database" "example" { support_oss_cluster_api = true replication = false enable_default_user = true + query_performance_factor = "2x" + modules = [ + { + name: "RediSearch" + } + ] } data "rediscloud_database" "example-by-id" { diff --git a/provider/resource_rediscloud_active_active_subscription_peering_test.go b/provider/resource_rediscloud_active_active_subscription_peering_test.go index 74b9c219..0928d75b 100644 --- a/provider/resource_rediscloud_active_active_subscription_peering_test.go +++ b/provider/resource_rediscloud_active_active_subscription_peering_test.go @@ -2,12 +2,11 @@ package provider import ( "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "os" "regexp" "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccResourceRedisCloudActiveActiveSubscriptionPeering_aws(t *testing.T) { diff --git a/provider/resource_rediscloud_pro_database.go b/provider/resource_rediscloud_pro_database.go index b6dfc7d5..a6d959ad 100644 --- a/provider/resource_rediscloud_pro_database.go +++ b/provider/resource_rediscloud_pro_database.go @@ -3,6 +3,7 @@ package provider import ( "context" "fmt" + "regexp" "strconv" "strings" "time" @@ -49,7 +50,7 @@ func resourceRedisCloudProDatabase() *schema.Resource { Delete: schema.DefaultTimeout(10 * time.Minute), }, - CustomizeDiff: remoteBackupIntervalSetCorrectly("remote_backup"), + CustomizeDiff: customizeDiff(), Schema: map[string]*schema.Schema{ "subscription_id": { @@ -215,6 +216,25 @@ func resourceRedisCloudProDatabase() *schema.Resource { }, }, }, + "query_performance_factor": { + Description: "Query performance factor for this specific database", + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { + v := val.(string) + matched, err := regexp.MatchString(`^([2468])x$`, v) + if err != nil { + errs = append(errs, fmt.Errorf("regex match failed: %s", err)) + return + } + if !matched { + errs = append(errs, fmt.Errorf("%q must be an even value between 2x and 8x (inclusive), got: %s", key, v)) + } + return + }, + }, "modules": { Description: "Modules to be provisioned in the database", Type: schema.TypeSet, @@ -341,6 +361,7 @@ func resourceRedisCloudProDatabaseCreate(ctx context.Context, d *schema.Resource throughputMeasurementBy := d.Get("throughput_measurement_by").(string) throughputMeasurementValue := d.Get("throughput_measurement_value").(int) averageItemSizeInBytes := d.Get("average_item_size_in_bytes").(int) + queryPerformanceFactor := d.Get("query_performance_factor").(string) createModules := make([]*databases.Module, 0) modules := d.Get("modules").(*schema.Set) @@ -388,6 +409,10 @@ func resourceRedisCloudProDatabaseCreate(ctx context.Context, d *schema.Resource RemoteBackup: buildBackupPlan(d.Get("remote_backup").([]interface{}), d.Get("periodic_backup_path")), } + if queryPerformanceFactor != "" { + createDatabase.QueryPerformanceFactor = redis.String(queryPerformanceFactor) + } + if password != "" { createDatabase.Password = redis.String(password) } @@ -513,6 +538,10 @@ func resourceRedisCloudProDatabaseRead(ctx context.Context, d *schema.ResourceDa return diag.FromErr(err) } + if err := d.Set("query_performance_factor", redis.StringValue(db.QueryPerformanceFactor)); err != nil { + return diag.FromErr(err) + } + if err := d.Set("modules", flattenModules(db.Modules)); err != nil { return diag.FromErr(err) } @@ -585,6 +614,12 @@ func resourceRedisCloudProDatabaseRead(ctx context.Context, d *schema.ResourceDa return diag.FromErr(err) } + if db.QueryPerformanceFactor != nil { + if err := d.Set("query_performance_factor", redis.String(*db.QueryPerformanceFactor)); err != nil { + return diag.FromErr(err) + } + } + if err := readTags(ctx, api, subId, dbId, d); err != nil { return diag.FromErr(err) } @@ -681,6 +716,11 @@ func resourceRedisCloudProDatabaseUpdate(ctx context.Context, d *schema.Resource update.SourceIP = []*string{redis.String("0.0.0.0/0")} } + queryPerformanceFactor := d.Get("query_performance_factor").(string) + if queryPerformanceFactor != "" { + update.QueryPerformanceFactor = redis.String(queryPerformanceFactor) + } + if d.Get("password").(string) != "" { update.Password = redis.String(d.Get("password").(string)) } @@ -873,6 +913,61 @@ func skipDiffIfIntervalIs12And12HourTimeDiff(k, oldValue, newValue string, d *sc return oldTime.Minute() == newTime.Minute() && oldTime.Add(12*time.Hour).Hour() == newTime.Hour() } +func customizeDiff() schema.CustomizeDiffFunc { + return func(ctx context.Context, diff *schema.ResourceDiff, meta interface{}) error { + if err := validateQueryPerformanceFactor()(ctx, diff, meta); err != nil { + return err + } + if err := remoteBackupIntervalSetCorrectly("remote_backup")(ctx, diff, meta); err != nil { + return err + } + return nil + } +} + +func validateQueryPerformanceFactor() schema.CustomizeDiffFunc { + return func(ctx context.Context, diff *schema.ResourceDiff, meta interface{}) error { + // Check if "query_performance_factor" is set + qpf, qpfExists := diff.GetOk("query_performance_factor") + + // Ensure "modules" is explicitly defined in the HCL + _, modulesExists := diff.GetOkExists("modules") + + if qpfExists && qpf.(string) != "" { + if !modulesExists { + return fmt.Errorf(`"query_performance_factor" requires the "modules" key to be explicitly defined in HCL`) + } + + // Retrieve modules as a slice of interfaces + rawModules := diff.Get("modules").(*schema.Set).List() + + // Convert modules to []map[string]interface{} + var modules []map[string]interface{} + for _, rawModule := range rawModules { + if moduleMap, ok := rawModule.(map[string]interface{}); ok { + modules = append(modules, moduleMap) + } + } + + // Check if "RediSearch" exists + if !containsDBModule(modules, "RediSearch") { + return fmt.Errorf(`"query_performance_factor" requires the "modules" list to contain "RediSearch"`) + } + } + return nil + } +} + +// Helper function to check if a module exists +func containsDBModule(modules []map[string]interface{}, moduleName string) bool { + for _, module := range modules { + if name, ok := module["name"].(string); ok && name == moduleName { + return true + } + } + return false +} + func remoteBackupIntervalSetCorrectly(key string) schema.CustomizeDiffFunc { // Validate multiple attributes - https://github.com/hashicorp/terraform-plugin-sdk/issues/233 diff --git a/provider/resource_rediscloud_pro_database_qpf_test.go b/provider/resource_rediscloud_pro_database_qpf_test.go new file mode 100644 index 00000000..24cb2496 --- /dev/null +++ b/provider/resource_rediscloud_pro_database_qpf_test.go @@ -0,0 +1,183 @@ +package provider + +import ( + "fmt" + "os" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +// Generates the base Terraform config for a Pro Subscription with QPF +func proSubscriptionQPFBoilerplate(name, cloudAccountName, qpf string) string { + return fmt.Sprintf(` +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" + + allowlist { + cidrs = ["192.168.0.0/16"] + security_group_ids = [] + } + + 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 { + dataset_size_in_gb = 1 + throughput_measurement_by = "operations-per-second" + throughput_measurement_value = 1000 + quantity = 1 + replication = false + support_oss_cluster_api = false + query_performance_factor = "%s" + modules = ["RediSearch"] + } +}`, cloudAccountName, name, qpf) +} + +// Generates Terraform configuration for the database +func formatDatabaseConfig(name, cloudAccountName, password, qpf, extraConfig string) string { + return proSubscriptionQPFBoilerplate(name, cloudAccountName, qpf) + fmt.Sprintf(` +resource "rediscloud_subscription_database" "example" { + subscription_id = rediscloud_subscription.example.id + name = "example" + protocol = "redis" + dataset_size_in_gb = 3 + data_persistence = "none" + data_eviction = "allkeys-random" + throughput_measurement_by = "operations-per-second" + throughput_measurement_value = 1000 + password = "%s" + support_oss_cluster_api = false + external_endpoint_for_oss_cluster_api = false + replication = false + average_item_size_in_bytes = 0 + client_ssl_certificate = "" + periodic_backup_path = "" + enable_default_user = true + query_performance_factor = "%s" + + alert { + name = "dataset-size" + value = 40 + } + + tags = { + "market" = "emea" + "material" = "cardboard" + } + + %s +}`, password, qpf, extraConfig) +} + +// Generic test helper for error cases +func testErrorCase(t *testing.T, config string, expectedError *regexp.Regexp) { + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccAwsPreExistingCloudAccountPreCheck(t) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckProSubscriptionDestroy, + Steps: []resource.TestStep{ + { + Config: config, + ExpectError: expectedError, + }, + }, + }) +} + +func TestAccResourceRedisCloudProDatabase_qpf(t *testing.T) { + name := acctest.RandomWithPrefix(testResourcePrefix) + password := acctest.RandString(20) + testCloudAccountName := os.Getenv("AWS_TEST_CLOUD_ACCOUNT_NAME") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccAwsPreExistingCloudAccountPreCheck(t) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckProSubscriptionDestroy, + Steps: []resource.TestStep{ + { + Config: formatDatabaseConfig(name, testCloudAccountName, password, "4x", `modules = [{ name = "RediSearch" }]`), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("rediscloud_subscription_database.example", "name", "example"), + resource.TestCheckResourceAttr("rediscloud_subscription_database.example", "protocol", "redis"), + resource.TestCheckResourceAttr("rediscloud_subscription_database.example", "dataset_size_in_gb", "3"), + resource.TestCheckResourceAttr("rediscloud_subscription_database.example", "query_performance_factor", "4x"), + resource.TestCheckResourceAttr("rediscloud_subscription_database.example", "tags.market", "emea"), + resource.TestCheckResourceAttr("rediscloud_subscription_database.example", "tags.material", "cardboard"), + ), + }, + + // Test plan to ensure query_performance_factor change forces a new resource + { + Config: formatDatabaseConfig(name, testCloudAccountName, password, "2x", `modules = [{ name = "RediSearch" }]`), + PlanOnly: true, // Runs terraform plan without applying + ExpectNonEmptyPlan: true, // Ensures that a change is detected + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("rediscloud_subscription_database.example", "query_performance_factor", "2x"), + ), + }, + }, + }) +} + +func TestAccResourceRedisCloudProDatabase_qpf_missingModule(t *testing.T) { + name := acctest.RandomWithPrefix(testResourcePrefix) + password := acctest.RandString(20) + testCloudAccountName := os.Getenv("AWS_TEST_CLOUD_ACCOUNT_NAME") + + config := formatDatabaseConfig(name, testCloudAccountName, password, "4x", "") + + testErrorCase(t, config, regexp.MustCompile("query_performance_factor\" requires the \"modules\" key to be explicitly defined in HCL")) +} + +func TestAccResourceRedisCloudProDatabase_qpf_missingRediSearchModule(t *testing.T) { + name := acctest.RandomWithPrefix(testResourcePrefix) + password := acctest.RandString(20) + testCloudAccountName := os.Getenv("AWS_TEST_CLOUD_ACCOUNT_NAME") + + config := formatDatabaseConfig(name, testCloudAccountName, password, "4x", `modules = [{ name = "RediBloom" }]`) + + testErrorCase(t, config, regexp.MustCompile("query_performance_factor\" requires the \"modules\" list to contain \"RediSearch")) +} + +func TestAccResourceRedisCloudProDatabase_qpf_invalidQueryPerformanceFactors(t *testing.T) { + name := acctest.RandomWithPrefix("tf-test") + password := acctest.RandString(20) + testCloudAccountName := os.Getenv("AWS_TEST_CLOUD_ACCOUNT_NAME") + + config := formatDatabaseConfig(name, testCloudAccountName, password, "5x", `modules = [{ name = "RediSearch" }]`) + + testSubErrorCase(t, config, regexp.MustCompile(`"creation_plan\.0\.query_performance_factor" must be an even value between 2x and 8x \(inclusive\), got: 5x`)) +} + +func TestAccResourceRedisCloudProDatabase_qpf_invalidQueryPerformanceFactors_outOfRange(t *testing.T) { + name := acctest.RandomWithPrefix("tf-test") + password := acctest.RandString(20) + testCloudAccountName := os.Getenv("AWS_TEST_CLOUD_ACCOUNT_NAME") + + config := formatDatabaseConfig(name, testCloudAccountName, password, "30x", `modules = [{ name = "RediSearch" }]`) + + testSubErrorCase(t, config, regexp.MustCompile(`"creation_plan\.0\.query_performance_factor" must be an even value between 2x and 8x \(inclusive\), got: 30x`)) +} diff --git a/provider/resource_rediscloud_pro_subscription.go b/provider/resource_rediscloud_pro_subscription.go index 922606c4..1d2e8e03 100644 --- a/provider/resource_rediscloud_pro_subscription.go +++ b/provider/resource_rediscloud_pro_subscription.go @@ -21,26 +21,60 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) +func containsModule(modules []interface{}, requiredModule string) bool { + for _, m := range modules { + if mod, ok := m.(string); ok && mod == requiredModule { + return true + } + } + return false +} + func resourceRedisCloudProSubscription() *schema.Resource { return &schema.Resource{ - Description: "Creates a Pro Subscription within your Redis Enterprise Cloud Account.", - CreateContext: resourceRedisCloudProSubscriptionCreate, - ReadContext: resourceRedisCloudProSubscriptionRead, - UpdateContext: resourceRedisCloudProSubscriptionUpdate, - DeleteContext: resourceRedisCloudProSubscriptionDelete, - CustomizeDiff: func(ctx context.Context, diff *schema.ResourceDiff, i interface{}) error { - _, cPlanExists := diff.GetOk("creation_plan") - if cPlanExists { + + CustomizeDiff: func(ctx context.Context, diff *schema.ResourceDiff, meta interface{}) error { + + // Ensure the "creation_plan" block exists + _, creationPlanExists := diff.GetOk("creation_plan") + if !creationPlanExists { + if diff.Id() == "" { + return fmt.Errorf(`the "creation_plan" block is required`) + } return nil } - // The resource hasn't been created yet, but the creation plan is missing. - if diff.Id() == "" { - return fmt.Errorf(`the "creation_plan" block is required`) + // Validate "query_performance_factor" dependency on "modules" + creationPlan := diff.Get("creation_plan").([]interface{}) + if len(creationPlan) > 0 { + plan := creationPlan[0].(map[string]interface{}) + + qpf, qpfExists := plan["query_performance_factor"].(string) + + // Ensure "modules" key is explicitly defined in HCL + _, modulesExists := diff.GetOkExists("creation_plan.0.modules") + + if qpfExists && qpf != "" { + if !modulesExists { + return fmt.Errorf(`"query_performance_factor" requires the "modules" key to be explicitly defined in HCL`) + } + + modules, _ := plan["modules"].([]interface{}) + if !containsModule(modules, "RediSearch") { + return fmt.Errorf(`"query_performance_factor" requires the "modules" list to contain "RediSearch"`) + } + } } + return nil }, + Description: "Creates a Pro Subscription within your Redis Enterprise Cloud Account.", + CreateContext: resourceRedisCloudProSubscriptionCreate, + ReadContext: resourceRedisCloudProSubscriptionRead, + UpdateContext: resourceRedisCloudProSubscriptionUpdate, + DeleteContext: resourceRedisCloudProSubscriptionDelete, + Importer: &schema.ResourceImporter{ // Let the READ operation do the heavy lifting for importing values from the API. StateContext: schema.ImportStatePassthroughContext, @@ -255,6 +289,24 @@ func resourceRedisCloudProSubscription() *schema.Resource { Optional: true, ConflictsWith: []string{"creation_plan.0.memory_limit_in_gb"}, }, + "query_performance_factor": { + Description: "Query performance factor for this specific database", + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { + v := val.(string) + matched, err := regexp.MatchString(`^([2468])x$`, v) + if err != nil { + errs = append(errs, fmt.Errorf("regex match failed: %s", err)) + return + } + if !matched { + errs = append(errs, fmt.Errorf("%q must be an even value between 2x and 8x (inclusive), got: %s", key, v)) + } + return + }, + }, "throughput_measurement_by": { Description: "Throughput measurement method, (either ‘number-of-shards’ or ‘operations-per-second’)", Type: schema.TypeString, @@ -798,6 +850,11 @@ func buildSubscriptionCreatePlanDatabases(memoryStorage string, planMap map[stri datasetSizeInGB = v.(float64) } + queryPerformanceFactor := "" + if v, ok := planMap["query_performance_factor"]; ok && v != nil { + queryPerformanceFactor = v.(string) + } + var diags diag.Diagnostics if memoryStorage == databases.MemoryStorageRam && averageItemSizeInBytes != 0 { // TODO This should be changed to an error when releasing 2.0 of the provider @@ -824,7 +881,7 @@ func buildSubscriptionCreatePlanDatabases(memoryStorage string, planMap map[stri for _, v := range planModules { modules = append(modules, &subscriptions.CreateModules{Name: v}) } - createDatabases = append(createDatabases, createDatabase(dbName, &idx, modules, throughputMeasurementBy, throughputMeasurementValue, memoryLimitInGB, datasetSizeInGB, averageItemSizeInBytes, supportOSSClusterAPI, replication, numDatabases)...) + createDatabases = append(createDatabases, createDatabase(dbName, &idx, modules, throughputMeasurementBy, throughputMeasurementValue, memoryLimitInGB, datasetSizeInGB, averageItemSizeInBytes, supportOSSClusterAPI, replication, numDatabases, queryPerformanceFactor)...) } else { // make RedisGraph module the first module, then append the rest of the modules var modules []*subscriptions.CreateModules @@ -835,20 +892,20 @@ func buildSubscriptionCreatePlanDatabases(memoryStorage string, planMap map[stri } } // create a DB with the RedisGraph module - createDatabases = append(createDatabases, createDatabase(dbName, &idx, modules[:1], throughputMeasurementBy, throughputMeasurementValue, memoryLimitInGB, datasetSizeInGB, averageItemSizeInBytes, supportOSSClusterAPI, replication, 1)...) + createDatabases = append(createDatabases, createDatabase(dbName, &idx, modules[:1], throughputMeasurementBy, throughputMeasurementValue, memoryLimitInGB, datasetSizeInGB, averageItemSizeInBytes, supportOSSClusterAPI, replication, 1, queryPerformanceFactor)...) if numDatabases == 1 { // create one extra DB with all other modules - createDatabases = append(createDatabases, createDatabase(dbName, &idx, modules[1:], throughputMeasurementBy, throughputMeasurementValue, memoryLimitInGB, datasetSizeInGB, averageItemSizeInBytes, supportOSSClusterAPI, replication, 1)...) + createDatabases = append(createDatabases, createDatabase(dbName, &idx, modules[1:], throughputMeasurementBy, throughputMeasurementValue, memoryLimitInGB, datasetSizeInGB, averageItemSizeInBytes, supportOSSClusterAPI, replication, 1, queryPerformanceFactor)...) } else if numDatabases > 1 { // create the remaining DBs with all other modules - createDatabases = append(createDatabases, createDatabase(dbName, &idx, modules[1:], throughputMeasurementBy, throughputMeasurementValue, memoryLimitInGB, datasetSizeInGB, averageItemSizeInBytes, supportOSSClusterAPI, replication, numDatabases-1)...) + createDatabases = append(createDatabases, createDatabase(dbName, &idx, modules[1:], throughputMeasurementBy, throughputMeasurementValue, memoryLimitInGB, datasetSizeInGB, averageItemSizeInBytes, supportOSSClusterAPI, replication, numDatabases-1, queryPerformanceFactor)...) } } return createDatabases, diags } // createDatabase returns a CreateDatabase struct with the given parameters -func createDatabase(dbName string, idx *int, modules []*subscriptions.CreateModules, throughputMeasurementBy string, throughputMeasurementValue int, memoryLimitInGB float64, datasetSizeInGB float64, averageItemSizeInBytes int, supportOSSClusterAPI bool, replication bool, numDatabases int) []*subscriptions.CreateDatabase { +func createDatabase(dbName string, idx *int, modules []*subscriptions.CreateModules, throughputMeasurementBy string, throughputMeasurementValue int, memoryLimitInGB float64, datasetSizeInGB float64, averageItemSizeInBytes int, supportOSSClusterAPI bool, replication bool, numDatabases int, queryPerformanceFactor string) []*subscriptions.CreateDatabase { createThroughput := &subscriptions.CreateThroughput{ By: redis.String(throughputMeasurementBy), Value: redis.Int(throughputMeasurementValue), @@ -887,6 +944,11 @@ func createDatabase(dbName string, idx *int, modules []*subscriptions.CreateModu if memoryLimitInGB > 0 { createDatabase.MemoryLimitInGB = redis.Float64(memoryLimitInGB) } + + if queryPerformanceFactor != "" { + createDatabase.QueryPerformanceFactor = redis.String(queryPerformanceFactor) + } + *idx++ dbs = append(dbs, &createDatabase) } diff --git a/provider/resource_rediscloud_pro_subscription_qpf_test.go b/provider/resource_rediscloud_pro_subscription_qpf_test.go new file mode 100644 index 00000000..9c97b8bb --- /dev/null +++ b/provider/resource_rediscloud_pro_subscription_qpf_test.go @@ -0,0 +1,149 @@ +package provider + +import ( + "fmt" + "os" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +// Generates the base Terraform config for a Pro Subscription with QPF +func formatSubscriptionConfig(name, cloudAccountName, qpf, extraConfig string) string { + return fmt.Sprintf(` +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" + + allowlist { + cidrs = ["192.168.0.0/16"] + security_group_ids = [] + } + + 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 { + dataset_size_in_gb = 1 + throughput_measurement_by = "operations-per-second" + throughput_measurement_value = 1000 + quantity = 1 + replication = false + support_oss_cluster_api = false + query_performance_factor = "%s" + + %s + } +}`, cloudAccountName, name, qpf, extraConfig) +} + +// Generic test helper for error cases +func testSubErrorCase(t *testing.T, config string, expectedError *regexp.Regexp) { + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccAwsPreExistingCloudAccountPreCheck(t) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckProSubscriptionDestroy, + Steps: []resource.TestStep{ + { + Config: config, + ExpectError: expectedError, + }, + }, + }) +} + +func TestAccResourceRedisCloudProSubscription_qpf(t *testing.T) { + name := acctest.RandomWithPrefix(testResourcePrefix) + testCloudAccountName := os.Getenv("AWS_TEST_CLOUD_ACCOUNT_NAME") + const resourceName = "rediscloud_subscription.example" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccAwsPreExistingCloudAccountPreCheck(t) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckProSubscriptionDestroy, + Steps: []resource.TestStep{ + { + Config: formatSubscriptionConfig(name, testCloudAccountName, "2x", `modules = ["RediSearch"]`), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttr(resourceName, "payment_method", "credit-card"), + resource.TestCheckResourceAttr(resourceName, "cloud_provider.0.provider", "AWS"), + resource.TestCheckResourceAttr(resourceName, "cloud_provider.0.region.0.preferred_availability_zones.#", "1"), + resource.TestCheckResourceAttrSet(resourceName, "cloud_provider.0.region.0.networks.0.networking_subnet_id"), + resource.TestCheckResourceAttr(resourceName, "creation_plan.#", "1"), + resource.TestCheckResourceAttr(resourceName, "creation_plan.0.average_item_size_in_bytes", "0"), + resource.TestCheckResourceAttr(resourceName, "creation_plan.0.dataset_size_in_gb", "1"), + resource.TestCheckResourceAttr(resourceName, "creation_plan.0.query_performance_factor", "2x"), + + resource.TestCheckResourceAttr(resourceName, "creation_plan.0.modules.#", "1"), + resource.TestCheckResourceAttr(resourceName, "creation_plan.0.modules.0", "RediSearch"), + resource.TestCheckResourceAttr(resourceName, "creation_plan.0.quantity", "1"), + resource.TestCheckResourceAttr(resourceName, "creation_plan.0.replication", "false"), + resource.TestCheckResourceAttr(resourceName, "creation_plan.0.support_oss_cluster_api", "false"), + resource.TestCheckResourceAttr(resourceName, "creation_plan.0.throughput_measurement_by", "operations-per-second"), + resource.TestCheckResourceAttr(resourceName, "creation_plan.0.throughput_measurement_value", "1000"), + ), + }, + }, + }) +} + +func TestAccResourceRedisCloudProSubscription_missingModule(t *testing.T) { + name := acctest.RandomWithPrefix(testResourcePrefix) + password := acctest.RandString(20) + testCloudAccountName := os.Getenv("AWS_TEST_CLOUD_ACCOUNT_NAME") + + config := formatDatabaseConfig(name, testCloudAccountName, password, "4x", "") + + testSubErrorCase(t, config, regexp.MustCompile("query_performance_factor\" requires the \"modules\" key to be explicitly defined in HCL")) +} + +func TestAccResourceRedisCloudProSubscription_missingRediSearchModule(t *testing.T) { + name := acctest.RandomWithPrefix(testResourcePrefix) + password := acctest.RandString(20) + testCloudAccountName := os.Getenv("AWS_TEST_CLOUD_ACCOUNT_NAME") + + config := formatDatabaseConfig(name, testCloudAccountName, password, "4x", `modules = [{ name = "RediBloom" }]`) + + testSubErrorCase(t, config, regexp.MustCompile("query_performance_factor\" requires the \"modules\" list to contain \"RediSearch")) +} + +func TestAccResourceRedisCloudProSubscription_invalidQueryPerformanceFactors(t *testing.T) { + name := acctest.RandomWithPrefix("tf-test") + password := acctest.RandString(20) + testCloudAccountName := os.Getenv("AWS_TEST_CLOUD_ACCOUNT_NAME") + + config := formatDatabaseConfig(name, testCloudAccountName, password, "5x", `modules = [{ name = "RediSearch" }]`) + + testSubErrorCase(t, config, regexp.MustCompile(`"creation_plan\.0\.query_performance_factor" must be an even value between 2x and 8x \(inclusive\), got: 5x`)) +} + +func TestAccResourceRedisCloudProSubscription_invalidQueryPerformanceFactors_outOfRange(t *testing.T) { + name := acctest.RandomWithPrefix("tf-test") + password := acctest.RandString(20) + testCloudAccountName := os.Getenv("AWS_TEST_CLOUD_ACCOUNT_NAME") + + config := formatDatabaseConfig(name, testCloudAccountName, password, "30x", `modules = [{ name = "RediSearch" }]`) + + testSubErrorCase(t, config, regexp.MustCompile(`"creation_plan\.0\.query_performance_factor" must be an even value between 2x and 8x \(inclusive\), got: 30x`)) +} diff --git a/provider/resource_rediscloud_pro_subscription_test.go b/provider/resource_rediscloud_pro_subscription_test.go index 6d839cc1..d16e0311 100644 --- a/provider/resource_rediscloud_pro_subscription_test.go +++ b/provider/resource_rediscloud_pro_subscription_test.go @@ -51,9 +51,12 @@ func TestAccResourceRedisCloudProSubscription_CRUDI(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "creation_plan.#", "1"), resource.TestCheckResourceAttr(resourceName, "creation_plan.0.average_item_size_in_bytes", "0"), resource.TestCheckResourceAttr(resourceName, "creation_plan.0.dataset_size_in_gb", "1"), + resource.TestCheckResourceAttr(resourceName, "creation_plan.0.query_performance_factor", "4x"), + resource.TestCheckResourceAttr(resourceName, "creation_plan.0.modules.#", "2"), resource.TestCheckResourceAttr(resourceName, "creation_plan.0.modules.0", "RedisJSON"), resource.TestCheckResourceAttr(resourceName, "creation_plan.0.modules.1", "RedisBloom"), + resource.TestCheckResourceAttr(resourceName, "creation_plan.0.modules.2", "RediSearch"), resource.TestCheckResourceAttr(resourceName, "creation_plan.0.quantity", "1"), resource.TestCheckResourceAttr(resourceName, "creation_plan.0.replication", "false"), resource.TestCheckResourceAttr(resourceName, "creation_plan.0.support_oss_cluster_api", "false"), @@ -756,11 +759,13 @@ resource "rediscloud_subscription" "example" { creation_plan { dataset_size_in_gb = 1 quantity = 1 - replication=false - support_oss_cluster_api=false + replication = false + support_oss_cluster_api = false + query_performance_factor = "4x" + throughput_measurement_by = "operations-per-second" throughput_measurement_value = 10000 - modules = ["RedisJSON", "RedisBloom"] + modules = ["RedisJSON", "RedisBloom", "RediSearch"] } } `