diff --git a/docs/resources/mongodbflex_instance.md b/docs/resources/mongodbflex_instance.md index 75b45eb9..75aa2950 100644 --- a/docs/resources/mongodbflex_instance.md +++ b/docs/resources/mongodbflex_instance.md @@ -30,6 +30,7 @@ resource "stackit_mongodbflex_instance" "example" { options = { type = "Single" } + backup_schedule = "0 0 * * *" } ``` @@ -39,6 +40,7 @@ resource "stackit_mongodbflex_instance" "example" { ### Required - `acl` (List of String) The Access Control List (ACL) for the MongoDB Flex instance. +- `backup_schedule` (String) - `flavor` (Attributes) (see [below for nested schema](#nestedatt--flavor)) - `name` (String) Instance name. - `options` (Attributes) (see [below for nested schema](#nestedatt--options)) @@ -49,7 +51,6 @@ resource "stackit_mongodbflex_instance" "example" { ### Read-Only -- `backup_schedule` (String) - `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`instance_id`". - `instance_id` (String) ID of the MongoDB Flex instance. diff --git a/examples/resources/stackit_mongodbflex_instance/resource.tf b/examples/resources/stackit_mongodbflex_instance/resource.tf index 537b5f95..ef39003e 100644 --- a/examples/resources/stackit_mongodbflex_instance/resource.tf +++ b/examples/resources/stackit_mongodbflex_instance/resource.tf @@ -15,4 +15,5 @@ resource "stackit_mongodbflex_instance" "example" { options = { type = "Single" } + backup_schedule = "0 0 * * *" } diff --git a/stackit/internal/services/mongodbflex/instance/resource.go b/stackit/internal/services/mongodbflex/instance/resource.go index 2a61c8db..5dc954fd 100644 --- a/stackit/internal/services/mongodbflex/instance/resource.go +++ b/stackit/internal/services/mongodbflex/instance/resource.go @@ -21,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stackitcloud/stackit-sdk-go/core/config" @@ -206,11 +205,7 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r Required: true, }, "backup_schedule": schema.StringAttribute{ - Computed: true, // Update functionality for this field is currently not working properly on the API side - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Default: stringdefault.StaticString(DefaultBackupSchedule), // Using the same default value as the Portal, as the field is required + Required: true, }, "flavor": schema.SingleNestedAttribute{ Required: true, @@ -637,6 +632,13 @@ func mapFields(resp *mongodbflex.GetInstanceResponse, model *Model, flavor *flav return fmt.Errorf("creating options: %w", core.DiagsToError(diags)) } + simplifiedModelBackupSchedule := simplifyBackupSchedule(model.BackupSchedule.ValueString()) + // If the value returned by the API is different from the one in the model after simplification, + // we update the model so that it causes an error in Terraform + if simplifiedModelBackupSchedule != types.StringPointerValue(instance.BackupSchedule).ValueString() { + model.BackupSchedule = types.StringPointerValue(instance.BackupSchedule) + } + idParts := []string{ model.ProjectId.ValueString(), instanceId, @@ -647,7 +649,6 @@ func mapFields(resp *mongodbflex.GetInstanceResponse, model *Model, flavor *flav model.InstanceId = types.StringValue(instanceId) model.Name = types.StringPointerValue(instance.Name) model.ACL = aclList - model.BackupSchedule = types.StringPointerValue(instance.BackupSchedule) model.Flavor = flavorObject model.Replicas = conversion.ToTypeInt64(instance.Replicas) model.Storage = storageObject @@ -781,3 +782,17 @@ func loadFlavorId(ctx context.Context, client mongoDBFlexClient, model *Model, f return nil } + +// Remove leading 0s from backup schedule numbers (e.g. "00 00 * * *" becomes "0 0 * * *") +// Needed as the API does it internally and would otherwise cause inconsistent result in Terraform +func simplifyBackupSchedule(schedule string) string { + regex := regexp.MustCompile(`0+\d+`) // Matches series of one or more zeros followed by a series of one or more digits + simplifiedSchedule := regex.ReplaceAllStringFunc(schedule, func(match string) string { + simplified := strings.TrimLeft(match, "0") + if simplified == "" { + simplified = "0" + } + return simplified + }) + return simplifiedSchedule +} diff --git a/stackit/internal/services/mongodbflex/instance/resource_test.go b/stackit/internal/services/mongodbflex/instance/resource_test.go index 6008d222..42a88e94 100644 --- a/stackit/internal/services/mongodbflex/instance/resource_test.go +++ b/stackit/internal/services/mongodbflex/instance/resource_test.go @@ -748,3 +748,75 @@ func TestLoadFlavorId(t *testing.T) { }) } } + +func TestSimplifyBackupSchedule(t *testing.T) { + tests := []struct { + description string + input string + expected string + }{ + { + "simple schedule", + "0 0 * * *", + "0 0 * * *", + }, + { + "schedule with leading zeros", + "00 00 * * *", + "0 0 * * *", + }, + { + "schedule with leading zeros 2", + "00 001 * * *", + "0 1 * * *", + }, + { + "schedule with leading zeros 3", + "00 0010 * * *", + "0 10 * * *", + }, + { + "simple schedule with slash", + "0 0/6 * * *", + "0 0/6 * * *", + }, + { + "schedule with leading zeros and slash", + "00 00/6 * * *", + "0 0/6 * * *", + }, + { + "schedule with leading zeros and slash 2", + "00 001/06 * * *", + "0 1/6 * * *", + }, + { + "simple schedule with comma", + "0 10,15 * * *", + "0 10,15 * * *", + }, + { + "schedule with leading zeros and comma", + "0 010,0015 * * *", + "0 10,15 * * *", + }, + { + "simple schedule with comma and slash", + "0 0-11/10 * * *", + "0 0-11/10 * * *", + }, + { + "schedule with leading zeros, comma, and slash", + "00 000-011/010 * * *", + "0 0-11/10 * * *", + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + output := simplifyBackupSchedule(tt.input) + if output != tt.expected { + t.Fatalf("Data does not match: %s", output) + } + }) + } +} diff --git a/stackit/internal/services/mongodbflex/mongodbflex_acc_test.go b/stackit/internal/services/mongodbflex/mongodbflex_acc_test.go index 7b5958cc..f820ccf2 100644 --- a/stackit/internal/services/mongodbflex/mongodbflex_acc_test.go +++ b/stackit/internal/services/mongodbflex/mongodbflex_acc_test.go @@ -20,19 +20,22 @@ import ( // Instance resource data var instanceResource = map[string]string{ - "project_id": testutil.ProjectId, - "name": fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum)), - "acl": "192.168.0.0/16", - "flavor_cpu": "2", - "flavor_ram": "4", - "flavor_description": "Small, Compute optimized", - "replicas": "1", - "storage_class": "premium-perf2-mongodb", - "storage_size": "10", - "version": "5.0", - "version_updated": "6.0", - "options_type": "Single", - "flavor_id": "2.4", + "project_id": testutil.ProjectId, + "name": fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum)), + "acl": "192.168.0.0/16", + "flavor_cpu": "2", + "flavor_ram": "4", + "flavor_description": "Small, Compute optimized", + "replicas": "1", + "storage_class": "premium-perf2-mongodb", + "storage_size": "10", + "version": "5.0", + "version_updated": "6.0", + "options_type": "Single", + "flavor_id": "2.4", + "backup_schedule": "00 6 * * *", + "backup_schedule_updated": "00 12 * * *", + "backup_schedule_read": "0 6 * * *", } // User resource data @@ -43,7 +46,7 @@ var userResource = map[string]string{ "project_id": instanceResource["project_id"], } -func configResources(version string) string { +func configResources(version, backupSchedule string) string { return fmt.Sprintf(` %s @@ -64,6 +67,7 @@ func configResources(version string) string { options = { type = "%s" } + backup_schedule = "%s" } resource "stackit_mongodbflex_user" "user" { @@ -85,6 +89,7 @@ func configResources(version string) string { instanceResource["storage_size"], version, instanceResource["options_type"], + backupSchedule, userResource["username"], userResource["role"], userResource["database"], @@ -98,7 +103,7 @@ func TestAccMongoDBFlexFlexResource(t *testing.T) { Steps: []resource.TestStep{ // Creation { - Config: configResources(instanceResource["version"]), + Config: configResources(instanceResource["version"], instanceResource["backup_schedule"]), Check: resource.ComposeAggregateTestCheckFunc( // Instance resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "project_id", instanceResource["project_id"]), @@ -115,6 +120,7 @@ func TestAccMongoDBFlexFlexResource(t *testing.T) { resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "storage.size", instanceResource["storage_size"]), resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "version", instanceResource["version"]), resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "options.type", instanceResource["options_type"]), + resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "backup_schedule", instanceResource["backup_schedule"]), // User resource.TestCheckResourceAttrPair( @@ -147,7 +153,7 @@ func TestAccMongoDBFlexFlexResource(t *testing.T) { user_id = stackit_mongodbflex_user.user.user_id } `, - configResources(instanceResource["version"]), + configResources(instanceResource["version"], instanceResource["backup_schedule"]), ), Check: resource.ComposeAggregateTestCheckFunc( // Instance data @@ -174,6 +180,7 @@ func TestAccMongoDBFlexFlexResource(t *testing.T) { resource.TestCheckResourceAttr("data.stackit_mongodbflex_instance.instance", "flavor.ram", instanceResource["flavor_ram"]), resource.TestCheckResourceAttr("data.stackit_mongodbflex_instance.instance", "replicas", instanceResource["replicas"]), resource.TestCheckResourceAttr("data.stackit_mongodbflex_instance.instance", "options.type", instanceResource["options_type"]), + resource.TestCheckResourceAttr("data.stackit_mongodbflex_instance.instance", "backup_schedule", instanceResource["backup_schedule_read"]), // User data resource.TestCheckResourceAttr("data.stackit_mongodbflex_user.user", "project_id", userResource["project_id"]), @@ -201,8 +208,18 @@ func TestAccMongoDBFlexFlexResource(t *testing.T) { return fmt.Sprintf("%s,%s", testutil.ProjectId, instanceId), nil }, - ImportState: true, - ImportStateVerify: true, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"backup_schedule"}, + ImportStateCheck: func(s []*terraform.InstanceState) error { + if len(s) != 1 { + return fmt.Errorf("expected 1 state, got %d", len(s)) + } + if s[0].Attributes["backup_schedule"] != instanceResource["backup_schedule_read"] { + return fmt.Errorf("expected backup_schedule %s, got %s", instanceResource["backup_schedule_read"], s[0].Attributes["backup_schedule"]) + } + return nil + }, }, { ResourceName: "stackit_mongodbflex_user.user", @@ -228,7 +245,7 @@ func TestAccMongoDBFlexFlexResource(t *testing.T) { }, // Update { - Config: configResources(instanceResource["version_updated"]), + Config: configResources(instanceResource["version_updated"], instanceResource["backup_schedule_updated"]), Check: resource.ComposeAggregateTestCheckFunc( // Instance data resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "project_id", instanceResource["project_id"]), @@ -245,6 +262,7 @@ func TestAccMongoDBFlexFlexResource(t *testing.T) { resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "storage.size", instanceResource["storage_size"]), resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "version", instanceResource["version_updated"]), resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "options.type", instanceResource["options_type"]), + resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "backup_schedule", instanceResource["backup_schedule_updated"]), ), }, // Deletion is done by the framework implicitly