From 23c4eee2def41a2693023fb2b1c4be89add20cd3 Mon Sep 17 00:00:00 2001 From: Subhrajit Panigrahi Date: Wed, 6 May 2026 21:26:12 +0530 Subject: [PATCH 01/20] [AV-127651] Add acceptance tests for legacy backup, backup schedule, and cloud snapshot backup schedule These tests are added to validate the legacy backup endpoint and related flows. The legacy backup resource currently fails creation with HTTP 404 "Unable to find the specified bucket" against a bucket that the bucket endpoint resolves successfully. This will be investigated and fixed in this branch. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- acceptance_tests/backup_acceptance_test.go | 235 ++++++++++++++++++ .../backup_schedule_acceptance_test.go | 232 +++++++++++++++++ ...napshot_backup_schedule_datasource_test.go | 81 ++++++ 3 files changed, 548 insertions(+) create mode 100644 acceptance_tests/backup_acceptance_test.go create mode 100644 acceptance_tests/backup_schedule_acceptance_test.go create mode 100644 acceptance_tests/cloud_snapshot_backup_schedule_datasource_test.go diff --git a/acceptance_tests/backup_acceptance_test.go b/acceptance_tests/backup_acceptance_test.go new file mode 100644 index 000000000..55fbe5d2e --- /dev/null +++ b/acceptance_tests/backup_acceptance_test.go @@ -0,0 +1,235 @@ +package acceptance_tests + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + + "github.com/couchbasecloud/terraform-provider-couchbase-capella/internal/api" + backupapi "github.com/couchbasecloud/terraform-provider-couchbase-capella/internal/api/backup" + "github.com/couchbasecloud/terraform-provider-couchbase-capella/internal/errors" + providerschema "github.com/couchbasecloud/terraform-provider-couchbase-capella/internal/schema" +) + +// TODO: legacy bucket backup endpoint returns 404 "Unable to find the specified +// bucket" for a bucket that the bucket endpoint resolves successfully. Track via +// the bug filed for couchbase-capella_backup / _backup_schedule before relaxing +// this test. +func TestAccBackupResource(t *testing.T) { + resourceName := randomStringWithPrefix("tf_acc_backup_") + resourceReference := "couchbase-capella_backup." + resourceName + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: globalProtoV6ProviderFactory, + Steps: []resource.TestStep{ + { + Config: testAccBackupResourceConfig(resourceName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccExistsBackupResource(t, resourceReference), + resource.TestCheckResourceAttr(resourceReference, "organization_id", globalOrgId), + resource.TestCheckResourceAttr(resourceReference, "project_id", globalProjectId), + resource.TestCheckResourceAttr(resourceReference, "cluster_id", globalClusterId), + resource.TestCheckResourceAttr(resourceReference, "bucket_id", globalBucketId), + resource.TestCheckResourceAttrSet(resourceReference, "id"), + resource.TestCheckResourceAttrSet(resourceReference, "cycle_id"), + resource.TestCheckResourceAttrSet(resourceReference, "date"), + resource.TestCheckResourceAttrSet(resourceReference, "status"), + resource.TestCheckResourceAttrSet(resourceReference, "method"), + resource.TestCheckResourceAttrSet(resourceReference, "bucket_name"), + resource.TestCheckResourceAttrSet(resourceReference, "source"), + resource.TestCheckResourceAttrSet(resourceReference, "cloud_provider"), + ), + }, + { + ResourceName: resourceReference, + ImportStateIdFunc: generateBackupImportIdForResource(resourceReference), + ImportState: true, + }, + }, + }) +} + +func TestAccBackupResourceInvalidBucket(t *testing.T) { + resourceName := randomStringWithPrefix("tf_acc_backup_invalid_") + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: globalProtoV6ProviderFactory, + Steps: []resource.TestStep{ + { + Config: testAccBackupResourceConfigWithBucketID(resourceName, "00000000-0000-0000-0000-000000000000"), + ExpectError: regexp.MustCompile("Error getting latest bucket backup|There is an error during backup creation"), + }, + }, + }) +} + +func TestAccBackupResourceInvalidProject(t *testing.T) { + resourceName := randomStringWithPrefix("tf_acc_backup_invalid_proj_") + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: globalProtoV6ProviderFactory, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` +%[1]s + +resource "couchbase-capella_backup" "%[2]s" { + organization_id = "%[3]s" + project_id = "00000000-0000-0000-0000-000000000000" + cluster_id = "%[4]s" + bucket_id = "%[5]s" +} +`, globalProviderBlock, resourceName, globalOrgId, globalClusterId, globalBucketId), + ExpectError: regexp.MustCompile("Error getting latest bucket backup|There is an error during backup creation"), + }, + }, + }) +} + +func TestAccBackupResourceRestoreTimesOnCreate(t *testing.T) { + resourceName := randomStringWithPrefix("tf_acc_backup_invalid_rt_") + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: globalProtoV6ProviderFactory, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` +%[1]s + +resource "couchbase-capella_backup" "%[2]s" { + organization_id = "%[3]s" + project_id = "%[4]s" + cluster_id = "%[5]s" + bucket_id = "%[6]s" + restore_times = 1 +} +`, globalProviderBlock, resourceName, globalOrgId, globalProjectId, globalClusterId, globalBucketId), + ExpectError: regexp.MustCompile("restore times must not be set while create backup"), + }, + }, + }) +} + +// TODO: depends on legacy bucket backup endpoint working; see TODO above +// TestAccBackupResource. +func TestAccDatasourceBackups(t *testing.T) { + backupResourceName := randomStringWithPrefix("tf_acc_backup_") + dsName := randomStringWithPrefix("tf_acc_backups_ds_") + dsReference := "data.couchbase-capella_backups." + dsName + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: globalProtoV6ProviderFactory, + Steps: []resource.TestStep{ + { + Config: testAccBackupsDatasourceConfig(backupResourceName, dsName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(dsReference, "organization_id", globalOrgId), + resource.TestCheckResourceAttr(dsReference, "project_id", globalProjectId), + resource.TestCheckResourceAttr(dsReference, "cluster_id", globalClusterId), + resource.TestCheckResourceAttr(dsReference, "bucket_id", globalBucketId), + resource.TestCheckResourceAttrSet(dsReference, "data.0.id"), + resource.TestCheckResourceAttrSet(dsReference, "data.0.cycle_id"), + resource.TestCheckResourceAttrSet(dsReference, "data.0.status"), + ), + }, + }, + }) +} + +func testAccBackupResourceConfig(resourceName string) string { + return testAccBackupResourceConfigWithBucketID(resourceName, globalBucketId) +} + +func testAccBackupResourceConfigWithBucketID(resourceName, bucketID string) string { + return fmt.Sprintf(` +%[1]s + +resource "couchbase-capella_backup" "%[2]s" { + organization_id = "%[3]s" + project_id = "%[4]s" + cluster_id = "%[5]s" + bucket_id = "%[6]s" +} +`, globalProviderBlock, resourceName, globalOrgId, globalProjectId, globalClusterId, bucketID) +} + +func testAccBackupsDatasourceConfig(backupResourceName, dsName string) string { + return fmt.Sprintf(` +%[1]s + +resource "couchbase-capella_backup" "%[2]s" { + organization_id = "%[3]s" + project_id = "%[4]s" + cluster_id = "%[5]s" + bucket_id = "%[6]s" +} + +data "couchbase-capella_backups" "%[7]s" { + organization_id = "%[3]s" + project_id = "%[4]s" + cluster_id = "%[5]s" + bucket_id = "%[6]s" + depends_on = [couchbase-capella_backup.%[2]s] +} +`, globalProviderBlock, backupResourceName, globalOrgId, globalProjectId, globalClusterId, globalBucketId, dsName) +} + +func generateBackupImportIdForResource(resourceReference string) resource.ImportStateIdFunc { + return func(state *terraform.State) (string, error) { + var rawState map[string]string + for _, m := range state.Modules { + if len(m.Resources) > 0 { + if v, ok := m.Resources[resourceReference]; ok { + rawState = v.Primary.Attributes + } + } + } + return fmt.Sprintf("id=%s,cluster_id=%s,project_id=%s,organization_id=%s", + rawState["id"], rawState["cluster_id"], rawState["project_id"], rawState["organization_id"]), nil + } +} + +func retrieveBackupFromServer(data *providerschema.Data, organizationId, projectId, clusterId, backupId string) error { + url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/backups/%s", + data.HostURL, organizationId, projectId, clusterId, backupId) + cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK} + response, err := data.ClientV1.ExecuteWithRetry( + context.Background(), + cfg, + nil, + data.Token, + nil, + ) + if err != nil { + return err + } + backupResp := backupapi.GetBackupResponse{} + if err := json.Unmarshal(response.Body, &backupResp); err != nil { + return err + } + if backupResp.Id == "" { + return errors.ErrNotFound + } + return nil +} + +func testAccExistsBackupResource(t *testing.T, resourceReference string) resource.TestCheckFunc { + return func(s *terraform.State) error { + var rawState map[string]string + for _, m := range s.Modules { + if len(m.Resources) > 0 { + if v, ok := m.Resources[resourceReference]; ok { + rawState = v.Primary.Attributes + } + } + } + data := newTestClient(t) + return retrieveBackupFromServer(data, rawState["organization_id"], rawState["project_id"], rawState["cluster_id"], rawState["id"]) + } +} diff --git a/acceptance_tests/backup_schedule_acceptance_test.go b/acceptance_tests/backup_schedule_acceptance_test.go new file mode 100644 index 000000000..83a13a104 --- /dev/null +++ b/acceptance_tests/backup_schedule_acceptance_test.go @@ -0,0 +1,232 @@ +package acceptance_tests + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + + "github.com/couchbasecloud/terraform-provider-couchbase-capella/internal/api" + scheduleapi "github.com/couchbasecloud/terraform-provider-couchbase-capella/internal/api/backup_schedule" + "github.com/couchbasecloud/terraform-provider-couchbase-capella/internal/errors" + providerschema "github.com/couchbasecloud/terraform-provider-couchbase-capella/internal/schema" +) + +// TODO: legacy bucket backup schedule endpoint returns 404 "Unable to find the +// specified bucket" for a bucket that the bucket endpoint resolves successfully. +// Track via the bug filed for couchbase-capella_backup_schedule. +func TestAccBackupScheduleResource(t *testing.T) { + resourceName := randomStringWithPrefix("tf_acc_backup_schedule_") + resourceReference := "couchbase-capella_backup_schedule." + resourceName + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: globalProtoV6ProviderFactory, + Steps: []resource.TestStep{ + { + Config: testAccBackupScheduleResourceConfig(resourceName, "weekly", "sunday", 10, 4, "30days", false), + Check: resource.ComposeAggregateTestCheckFunc( + testAccExistsBackupScheduleResource(t, resourceReference), + resource.TestCheckResourceAttr(resourceReference, "organization_id", globalOrgId), + resource.TestCheckResourceAttr(resourceReference, "project_id", globalProjectId), + resource.TestCheckResourceAttr(resourceReference, "cluster_id", globalClusterId), + resource.TestCheckResourceAttr(resourceReference, "bucket_id", globalBucketId), + resource.TestCheckResourceAttr(resourceReference, "type", "weekly"), + resource.TestCheckResourceAttr(resourceReference, "weekly_schedule.day_of_week", "sunday"), + resource.TestCheckResourceAttr(resourceReference, "weekly_schedule.start_at", "10"), + resource.TestCheckResourceAttr(resourceReference, "weekly_schedule.incremental_every", "4"), + resource.TestCheckResourceAttr(resourceReference, "weekly_schedule.retention_time", "30days"), + resource.TestCheckResourceAttr(resourceReference, "weekly_schedule.cost_optimized_retention", "false"), + ), + }, + { + ResourceName: resourceReference, + ImportStateIdFunc: generateBackupScheduleImportIdForResource(resourceReference), + ImportState: true, + }, + { + Config: testAccBackupScheduleResourceConfig(resourceName, "weekly", "monday", 14, 6, "60days", true), + Check: resource.ComposeAggregateTestCheckFunc( + testAccExistsBackupScheduleResource(t, resourceReference), + resource.TestCheckResourceAttr(resourceReference, "organization_id", globalOrgId), + resource.TestCheckResourceAttr(resourceReference, "project_id", globalProjectId), + resource.TestCheckResourceAttr(resourceReference, "cluster_id", globalClusterId), + resource.TestCheckResourceAttr(resourceReference, "bucket_id", globalBucketId), + resource.TestCheckResourceAttr(resourceReference, "type", "weekly"), + resource.TestCheckResourceAttr(resourceReference, "weekly_schedule.day_of_week", "monday"), + resource.TestCheckResourceAttr(resourceReference, "weekly_schedule.start_at", "14"), + resource.TestCheckResourceAttr(resourceReference, "weekly_schedule.incremental_every", "6"), + resource.TestCheckResourceAttr(resourceReference, "weekly_schedule.retention_time", "60days"), + resource.TestCheckResourceAttr(resourceReference, "weekly_schedule.cost_optimized_retention", "true"), + ), + }, + }, + }) +} + +func TestAccBackupScheduleResourceInvalidDayOfWeek(t *testing.T) { + resourceName := randomStringWithPrefix("tf_acc_backup_schedule_") + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: globalProtoV6ProviderFactory, + Steps: []resource.TestStep{ + { + Config: testAccBackupScheduleResourceConfig(resourceName, "weekly", "funday", 10, 4, "30days", false), + ExpectError: regexp.MustCompile("There is an error during backup schedule creation"), + }, + }, + }) +} + +func TestAccBackupScheduleResourceInvalidRetention(t *testing.T) { + resourceName := randomStringWithPrefix("tf_acc_backup_schedule_") + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: globalProtoV6ProviderFactory, + Steps: []resource.TestStep{ + { + Config: testAccBackupScheduleResourceConfig(resourceName, "weekly", "sunday", 10, 4, "9999days", false), + ExpectError: regexp.MustCompile("There is an error during backup schedule creation"), + }, + }, + }) +} + +func TestAccBackupScheduleResourceInvalidType(t *testing.T) { + resourceName := randomStringWithPrefix("tf_acc_backup_schedule_") + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: globalProtoV6ProviderFactory, + Steps: []resource.TestStep{ + { + Config: testAccBackupScheduleResourceConfig(resourceName, "yearly", "sunday", 10, 4, "30days", false), + ExpectError: regexp.MustCompile("There is an error during backup schedule creation"), + }, + }, + }) +} + +func TestAccBackupScheduleResourceInvalidStartAt(t *testing.T) { + resourceName := randomStringWithPrefix("tf_acc_backup_schedule_") + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: globalProtoV6ProviderFactory, + Steps: []resource.TestStep{ + { + Config: testAccBackupScheduleResourceConfig(resourceName, "weekly", "sunday", 99, 4, "30days", false), + ExpectError: regexp.MustCompile("There is an error during backup schedule creation"), + }, + }, + }) +} + +func TestAccBackupScheduleResourceInvalidBucket(t *testing.T) { + resourceName := randomStringWithPrefix("tf_acc_backup_schedule_invalid_bkt_") + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: globalProtoV6ProviderFactory, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` +%[1]s + +resource "couchbase-capella_backup_schedule" "%[2]s" { + organization_id = "%[3]s" + project_id = "%[4]s" + cluster_id = "%[5]s" + bucket_id = "00000000-0000-0000-0000-000000000000" + type = "weekly" + weekly_schedule = { + day_of_week = "sunday" + start_at = 10 + incremental_every = 4 + retention_time = "30days" + cost_optimized_retention = false + } +} +`, globalProviderBlock, resourceName, globalOrgId, globalProjectId, globalClusterId), + ExpectError: regexp.MustCompile("There is an error during backup schedule creation"), + }, + }, + }) +} + +func testAccBackupScheduleResourceConfig(resourceName, scheduleType, dayOfWeek string, startAt, incrementalEvery int, retentionTime string, costOptimized bool) string { + return fmt.Sprintf(` +%[1]s + +resource "couchbase-capella_backup_schedule" "%[2]s" { + organization_id = "%[3]s" + project_id = "%[4]s" + cluster_id = "%[5]s" + bucket_id = "%[6]s" + type = "%[7]s" + weekly_schedule = { + day_of_week = "%[8]s" + start_at = %[9]d + incremental_every = %[10]d + retention_time = "%[11]s" + cost_optimized_retention = %[12]t + } +} +`, globalProviderBlock, resourceName, globalOrgId, globalProjectId, globalClusterId, globalBucketId, + scheduleType, dayOfWeek, startAt, incrementalEvery, retentionTime, costOptimized) +} + +func generateBackupScheduleImportIdForResource(resourceReference string) resource.ImportStateIdFunc { + return func(state *terraform.State) (string, error) { + var rawState map[string]string + for _, m := range state.Modules { + if len(m.Resources) > 0 { + if v, ok := m.Resources[resourceReference]; ok { + rawState = v.Primary.Attributes + } + } + } + return fmt.Sprintf("organization_id=%s,project_id=%s,cluster_id=%s,bucket_id=%s", + rawState["organization_id"], rawState["project_id"], rawState["cluster_id"], rawState["bucket_id"]), nil + } +} + +func retrieveBackupScheduleFromServer(data *providerschema.Data, organizationId, projectId, clusterId, bucketId string) error { + url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/buckets/%s/backup/schedules", + data.HostURL, organizationId, projectId, clusterId, bucketId) + cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK} + response, err := data.ClientV1.ExecuteWithRetry( + context.Background(), + cfg, + nil, + data.Token, + nil, + ) + if err != nil { + return err + } + scheduleResp := scheduleapi.GetBackupScheduleResponse{} + if err := json.Unmarshal(response.Body, &scheduleResp); err != nil { + return err + } + if scheduleResp.WeeklySchedule == nil { + return errors.ErrNotFound + } + return nil +} + +func testAccExistsBackupScheduleResource(t *testing.T, resourceReference string) resource.TestCheckFunc { + return func(s *terraform.State) error { + var rawState map[string]string + for _, m := range s.Modules { + if len(m.Resources) > 0 { + if v, ok := m.Resources[resourceReference]; ok { + rawState = v.Primary.Attributes + } + } + } + data := newTestClient(t) + return retrieveBackupScheduleFromServer(data, rawState["organization_id"], rawState["project_id"], rawState["cluster_id"], rawState["bucket_id"]) + } +} diff --git a/acceptance_tests/cloud_snapshot_backup_schedule_datasource_test.go b/acceptance_tests/cloud_snapshot_backup_schedule_datasource_test.go new file mode 100644 index 000000000..61068d9b2 --- /dev/null +++ b/acceptance_tests/cloud_snapshot_backup_schedule_datasource_test.go @@ -0,0 +1,81 @@ +package acceptance_tests + +import ( + "fmt" + "regexp" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccDatasourceCloudSnapshotBackupSchedule(t *testing.T) { + scheduleResourceName := randomStringWithPrefix("tf_acc_cloud_snapshot_backup_schedule_") + dsName := randomStringWithPrefix("tf_acc_cloud_snapshot_backup_schedule_ds_") + dsReference := "data.couchbase-capella_cloud_snapshot_backup_schedule." + dsName + + startTime := time.Now().Add(24 * time.Hour).Truncate(time.Hour).Format(time.RFC3339) + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: globalProtoV6ProviderFactory, + Steps: []resource.TestStep{ + { + Config: testAccCloudSnapshotBackupScheduleDatasourceConfig(scheduleResourceName, dsName, 12, 240, startTime), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(dsReference, "organization_id", globalOrgId), + resource.TestCheckResourceAttr(dsReference, "project_id", globalProjectId), + resource.TestCheckResourceAttr(dsReference, "cluster_id", globalClusterId), + resource.TestCheckResourceAttr(dsReference, "interval", "12"), + resource.TestCheckResourceAttr(dsReference, "retention", "240"), + resource.TestCheckResourceAttr(dsReference, "start_time", startTime), + ), + }, + }, + }) +} + +func TestAccDatasourceCloudSnapshotBackupScheduleInvalidCluster(t *testing.T) { + dsName := randomStringWithPrefix("tf_acc_cloud_snapshot_backup_schedule_ds_invalid_") + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: globalProtoV6ProviderFactory, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` +%[1]s + +data "couchbase-capella_cloud_snapshot_backup_schedule" "%[2]s" { + organization_id = "%[3]s" + project_id = "%[4]s" + cluster_id = "00000000-0000-0000-0000-000000000000" +} +`, globalProviderBlock, dsName, globalOrgId, globalProjectId), + ExpectError: regexp.MustCompile("Error Reading Capella Snapshot Backup Schedule"), + }, + }, + }) +} + +func testAccCloudSnapshotBackupScheduleDatasourceConfig(scheduleResourceName, dsName string, interval, retention int, startTime string) string { + return fmt.Sprintf(` +%[1]s + +resource "couchbase-capella_cloud_snapshot_backup_schedule" "%[2]s" { + organization_id = "%[3]s" + project_id = "%[4]s" + cluster_id = "%[5]s" + interval = %[6]d + retention = %[7]d + start_time = "%[8]s" + copy_to_regions = null +} + +data "couchbase-capella_cloud_snapshot_backup_schedule" "%[9]s" { + organization_id = "%[3]s" + project_id = "%[4]s" + cluster_id = "%[5]s" + depends_on = [couchbase-capella_cloud_snapshot_backup_schedule.%[2]s] +} +`, globalProviderBlock, scheduleResourceName, globalOrgId, globalProjectId, globalClusterId, + interval, retention, startTime, dsName) +} From c90e05965e19390189af92c66301521a9a12e5f2 Mon Sep 17 00:00:00 2001 From: Subhrajit Panigrahi Date: Thu, 7 May 2026 09:11:01 +0530 Subject: [PATCH 02/20] [AV-127651] Validate, discover, or create bucket in acceptance test setup If TF_VAR_bucket_id is set but does not resolve, fall through to discovery instead of trusting the value blindly. If no buckets exist on the cluster, create one and tear it down at the end. Buckets that pre-existed (or were supplied via env var) are not destroyed. This eliminates the silent 404 from bucket-scoped endpoints (legacy backup, backup schedule, etc.) when the env var points to a stale or typo'd id. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- acceptance_tests/bucket.go | 54 ++++++++++++++++++++++++++-------- acceptance_tests/globals.go | 1 + acceptance_tests/setup_test.go | 40 +++++++++++++++++++++---- 3 files changed, 76 insertions(+), 19 deletions(-) diff --git a/acceptance_tests/bucket.go b/acceptance_tests/bucket.go index 3c8540beb..5fa0ee6d0 100644 --- a/acceptance_tests/bucket.go +++ b/acceptance_tests/bucket.go @@ -12,25 +12,38 @@ import ( bucketapi "github.com/couchbasecloud/terraform-provider-couchbase-capella/internal/api/bucket" ) -func createBucket(ctx context.Context, client *api.Client) error { - // First, check if bucket already exists +func bucketExists(ctx context.Context, client *api.Client, bucketId string) (bool, error) { + url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/buckets/%s", globalHost, globalOrgId, globalProjectId, globalClusterId, bucketId) + cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK} + _, err := client.ExecuteWithRetry(ctx, cfg, nil, globalToken, nil) + if err == nil { + return true, nil + } + if apiErr, ok := err.(*api.Error); ok && apiErr.HttpStatusCode == http.StatusNotFound { + return false, nil + } + return false, err +} + +func discoverFirstBucket(ctx context.Context, client *api.Client) (string, string, error) { listUrl := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/buckets", globalHost, globalOrgId, globalProjectId, globalClusterId) listCfg := api.EndpointCfg{Url: listUrl, Method: http.MethodGet, SuccessStatus: http.StatusOK} - - // Use the paginated API to get all buckets buckets, err := api.GetPaginated[[]bucketapi.GetBucketResponse](ctx, client, globalToken, listCfg, api.SortById) - if err == nil { - // Check if bucket with globalBucketName already exists - for _, bucket := range buckets { - if bucket.Name == globalBucketName { - globalBucketId = bucket.Id - log.Printf("Bucket '%s' already exists with ID: %s", globalBucketName, globalBucketId) - return nil - } + if err != nil { + return "", "", err + } + for _, bucket := range buckets { + if bucket.Name == globalBucketName { + return bucket.Id, bucket.Name, nil } } + if len(buckets) > 0 { + return buckets[0].Id, buckets[0].Name, nil + } + return "", "", nil +} - // Bucket doesn't exist, create it +func createBucket(ctx context.Context, client *api.Client) error { bucketRequest := bucketapi.CreateBucketRequest{ Name: globalBucketName, } @@ -54,6 +67,21 @@ func createBucket(ctx context.Context, client *api.Client) error { } globalBucketId = bucketResponse.Id + globalBucketCreated = true + return nil +} + +func destroyBucket(ctx context.Context, client *api.Client) error { + url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/buckets/%s", globalHost, globalOrgId, globalProjectId, globalClusterId, globalBucketId) + cfg := api.EndpointCfg{Url: url, Method: http.MethodDelete, SuccessStatus: http.StatusNoContent} + _, err := client.ExecuteWithRetry(ctx, cfg, nil, globalToken, nil) + if err != nil { + if apiErr, ok := err.(*api.Error); ok && apiErr.HttpStatusCode == http.StatusNotFound { + return nil + } + return err + } + log.Printf("bucket destroyed: %s", globalBucketId) return nil } diff --git a/acceptance_tests/globals.go b/acceptance_tests/globals.go index f662a106d..83c0919e3 100644 --- a/acceptance_tests/globals.go +++ b/acceptance_tests/globals.go @@ -23,6 +23,7 @@ var ( globalScopeName = "_default" globalCollectionName = "_default" globalBucketId string + globalBucketCreated bool globalAppServiceId string globalAppEndpointName = "tf_acc_test_app_endpoint_common" diff --git a/acceptance_tests/setup_test.go b/acceptance_tests/setup_test.go index 706b493c7..ce06f7d07 100644 --- a/acceptance_tests/setup_test.go +++ b/acceptance_tests/setup_test.go @@ -77,16 +77,37 @@ func setup(ctx context.Context, client *api.Client) error { log.Printf("Using existing cluster: %s", globalClusterId) } - // Create bucket only if not provided via env var - if globalBucketId == "" { - if err := createBucket(ctx, client); err != nil { + // Resolve bucket: validate env var, else discover, else create. + if globalBucketId != "" { + exists, err := bucketExists(ctx, client, globalBucketId) + if err != nil { return err } - if err := bucketWait(ctx, client); err != nil { + if exists { + log.Printf("Using existing bucket: %s", globalBucketId) + } else { + log.Printf("TF_VAR_bucket_id=%s does not exist on cluster; discovering or creating one", globalBucketId) + globalBucketId = "" + } + } + if globalBucketId == "" { + discoveredId, discoveredName, err := discoverFirstBucket(ctx, client) + if err != nil { return err } - } else { - log.Printf("Using existing bucket: %s", globalBucketId) + if discoveredId != "" { + globalBucketId = discoveredId + globalBucketName = discoveredName + log.Printf("Discovered existing bucket: %s (%s)", discoveredName, discoveredId) + } else { + if err := createBucket(ctx, client); err != nil { + return err + } + if err := bucketWait(ctx, client); err != nil { + return err + } + log.Printf("Created bucket: %s (%s)", globalBucketName, globalBucketId) + } } // Create app service only if not provided via env var @@ -123,6 +144,13 @@ func cleanup(ctx context.Context, client *api.Client) error { } } + // Only destroy bucket if it was created by setup. + if globalBucketCreated && globalBucketId != "" { + if err := destroyBucket(ctx, client); err != nil { + return err + } + } + // Only destroy cluster if it was created by setup (not provided via env var) if globalClusterId != "" && os.Getenv("TF_VAR_cluster_id") == "" { if err := destroyCluster(ctx, client); err != nil { From 5f8d8d34e364ce234ba39ef1ebaec815e599e529 Mon Sep 17 00:00:00 2001 From: Subhrajit Panigrahi Date: Thu, 7 May 2026 09:37:31 +0530 Subject: [PATCH 03/20] [AV-127651] Fix golangci-lint: errorlint and nestif - Replace err.(*api.Error) type assertions with errors.As to satisfy errorlint (api.Error may be wrapped on retry). - Extract the bucket-resolution ladder from setup() into resolveBucket() so the nestif complexity stays under threshold. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- acceptance_tests/bucket.go | 42 ++++++++++++++++++++++++++++++++-- acceptance_tests/setup_test.go | 33 ++------------------------ 2 files changed, 42 insertions(+), 33 deletions(-) diff --git a/acceptance_tests/bucket.go b/acceptance_tests/bucket.go index 5fa0ee6d0..c3ca04315 100644 --- a/acceptance_tests/bucket.go +++ b/acceptance_tests/bucket.go @@ -3,6 +3,7 @@ package acceptance_tests import ( "context" "encoding/json" + stderrors "errors" "fmt" "log" "net/http" @@ -19,7 +20,8 @@ func bucketExists(ctx context.Context, client *api.Client, bucketId string) (boo if err == nil { return true, nil } - if apiErr, ok := err.(*api.Error); ok && apiErr.HttpStatusCode == http.StatusNotFound { + var apiErr *api.Error + if stderrors.As(err, &apiErr) && apiErr.HttpStatusCode == http.StatusNotFound { return false, nil } return false, err @@ -76,7 +78,8 @@ func destroyBucket(ctx context.Context, client *api.Client) error { cfg := api.EndpointCfg{Url: url, Method: http.MethodDelete, SuccessStatus: http.StatusNoContent} _, err := client.ExecuteWithRetry(ctx, cfg, nil, globalToken, nil) if err != nil { - if apiErr, ok := err.(*api.Error); ok && apiErr.HttpStatusCode == http.StatusNotFound { + var apiErr *api.Error + if stderrors.As(err, &apiErr) && apiErr.HttpStatusCode == http.StatusNotFound { return nil } return err @@ -85,6 +88,41 @@ func destroyBucket(ctx context.Context, client *api.Client) error { return nil } +func resolveBucket(ctx context.Context, client *api.Client) error { + if globalBucketId != "" { + exists, err := bucketExists(ctx, client, globalBucketId) + if err != nil { + return err + } + if exists { + log.Printf("Using existing bucket: %s", globalBucketId) + return nil + } + log.Printf("TF_VAR_bucket_id=%s does not exist on cluster; discovering or creating one", globalBucketId) + globalBucketId = "" + } + + discoveredId, discoveredName, err := discoverFirstBucket(ctx, client) + if err != nil { + return err + } + if discoveredId != "" { + globalBucketId = discoveredId + globalBucketName = discoveredName + log.Printf("Discovered existing bucket: %s (%s)", discoveredName, discoveredId) + return nil + } + + if err := createBucket(ctx, client); err != nil { + return err + } + if err := bucketWait(ctx, client); err != nil { + return err + } + log.Printf("Created bucket: %s (%s)", globalBucketName, globalBucketId) + return nil +} + func bucketWait(ctx context.Context, client *api.Client) error { const maxWaitTime = 5 * time.Minute diff --git a/acceptance_tests/setup_test.go b/acceptance_tests/setup_test.go index ce06f7d07..6e2061595 100644 --- a/acceptance_tests/setup_test.go +++ b/acceptance_tests/setup_test.go @@ -77,37 +77,8 @@ func setup(ctx context.Context, client *api.Client) error { log.Printf("Using existing cluster: %s", globalClusterId) } - // Resolve bucket: validate env var, else discover, else create. - if globalBucketId != "" { - exists, err := bucketExists(ctx, client, globalBucketId) - if err != nil { - return err - } - if exists { - log.Printf("Using existing bucket: %s", globalBucketId) - } else { - log.Printf("TF_VAR_bucket_id=%s does not exist on cluster; discovering or creating one", globalBucketId) - globalBucketId = "" - } - } - if globalBucketId == "" { - discoveredId, discoveredName, err := discoverFirstBucket(ctx, client) - if err != nil { - return err - } - if discoveredId != "" { - globalBucketId = discoveredId - globalBucketName = discoveredName - log.Printf("Discovered existing bucket: %s (%s)", discoveredName, discoveredId) - } else { - if err := createBucket(ctx, client); err != nil { - return err - } - if err := bucketWait(ctx, client); err != nil { - return err - } - log.Printf("Created bucket: %s (%s)", globalBucketName, globalBucketId) - } + if err := resolveBucket(ctx, client); err != nil { + return err } // Create app service only if not provided via env var From d4f5e9847e9784d56ed8e7e5f3d3c5521238f075 Mon Sep 17 00:00:00 2001 From: Subhrajit Panigrahi Date: Thu, 7 May 2026 11:41:18 +0530 Subject: [PATCH 04/20] [AV-127651] Add snapshot cluster env vars for forward compat Adds optional TF_VAR_snapshot_cluster_id / TF_VAR_snapshot_bucket_id plumbing (with fallback to the primary cluster/bucket) and snapshotClusterId() / snapshotBucketId() helpers so cloud_snapshot_* tests can target a dedicated cluster in CI when the matching CAPELLA_SNAPSHOT_CLUSTER_ID / CAPELLA_SNAPSHOT_BUCKET_ID secrets are configured. Mirrors the change on PR #582 so this branch picks up the same wiring once merged. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .github/workflows/acceptance-tests.yml | 6 ++++++ acceptance_tests/envVars.go | 20 ++++++++++++++++++++ acceptance_tests/globals.go | 9 ++++++++- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/.github/workflows/acceptance-tests.yml b/.github/workflows/acceptance-tests.yml index df7fe6923..ebbc6d43d 100644 --- a/.github/workflows/acceptance-tests.yml +++ b/.github/workflows/acceptance-tests.yml @@ -22,6 +22,12 @@ jobs: TF_VAR_auth_token: ${{ secrets.CAPELLA_AUTH_TOKEN }} TF_VAR_organization_id: ${{ secrets.CAPELLA_ORGANIZATION_ID }} TF_VAR_host: ${{ secrets.CAPELLA_HOST }} + # Optional dedicated cluster + bucket used by the cloud_snapshot_* + # acceptance tests so long-running restore operations don't disrupt + # tests sharing the primary cluster. Falls back to the primary + # cluster/bucket when these secrets are not configured. + TF_VAR_snapshot_cluster_id: ${{ secrets.CAPELLA_SNAPSHOT_CLUSTER_ID }} + TF_VAR_snapshot_bucket_id: ${{ secrets.CAPELLA_SNAPSHOT_BUCKET_ID }} CAPELLA_OPENAPI_SPEC_PATH: ${{ github.workspace }}/openapi.generated.yaml steps: diff --git a/acceptance_tests/envVars.go b/acceptance_tests/envVars.go index 3250eebe6..6332b17e8 100644 --- a/acceptance_tests/envVars.go +++ b/acceptance_tests/envVars.go @@ -24,5 +24,25 @@ func getEnvVars() error { globalAppServiceId = os.Getenv("TF_VAR_app_service_id") globalBucketId = os.Getenv("TF_VAR_bucket_id") + // Optional dedicated cluster for snapshot/restore tests so that long + // restore windows don't interfere with the shared cluster used by other + // tests. Falls back to the primary cluster/bucket when unset. + globalSnapshotClusterId = os.Getenv("TF_VAR_snapshot_cluster_id") + globalSnapshotBucketId = os.Getenv("TF_VAR_snapshot_bucket_id") + return nil } + +func snapshotClusterId() string { + if globalSnapshotClusterId != "" { + return globalSnapshotClusterId + } + return globalClusterId +} + +func snapshotBucketId() string { + if globalSnapshotBucketId != "" { + return globalSnapshotBucketId + } + return globalBucketId +} diff --git a/acceptance_tests/globals.go b/acceptance_tests/globals.go index 83c0919e3..1be99b834 100644 --- a/acceptance_tests/globals.go +++ b/acceptance_tests/globals.go @@ -24,7 +24,14 @@ var ( globalCollectionName = "_default" globalBucketId string globalBucketCreated bool - globalAppServiceId string + // globalSnapshotClusterId / globalSnapshotBucketId are optional and, when + // provided via TF_VAR_snapshot_cluster_id / TF_VAR_snapshot_bucket_id, are + // used by the cloud_snapshot_* acceptance tests so that long-running + // restore operations don't disrupt other tests sharing the primary + // cluster. They fall back to globalClusterId / globalBucketId when unset. + globalSnapshotClusterId string + globalSnapshotBucketId string + globalAppServiceId string globalAppEndpointName = "tf_acc_test_app_endpoint_common" // this global variable is set in TestMain. From b7e68b23b1f7ac7d277390e9f35a4d004f747819 Mon Sep 17 00:00:00 2001 From: Subhrajit Panigrahi Date: Thu, 7 May 2026 11:47:18 +0530 Subject: [PATCH 05/20] [AV-127651] Drop snapshot env-var plumbing from this branch The snapshot cluster wiring is unused on this branch (no cloud_snapshot_* tests live here) and trips the unused linter. The plumbing is owned by PR #582; removing it here keeps the diff minimal and lint-clean. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .github/workflows/acceptance-tests.yml | 6 ------ acceptance_tests/envVars.go | 20 -------------------- acceptance_tests/globals.go | 9 +-------- 3 files changed, 1 insertion(+), 34 deletions(-) diff --git a/.github/workflows/acceptance-tests.yml b/.github/workflows/acceptance-tests.yml index ebbc6d43d..df7fe6923 100644 --- a/.github/workflows/acceptance-tests.yml +++ b/.github/workflows/acceptance-tests.yml @@ -22,12 +22,6 @@ jobs: TF_VAR_auth_token: ${{ secrets.CAPELLA_AUTH_TOKEN }} TF_VAR_organization_id: ${{ secrets.CAPELLA_ORGANIZATION_ID }} TF_VAR_host: ${{ secrets.CAPELLA_HOST }} - # Optional dedicated cluster + bucket used by the cloud_snapshot_* - # acceptance tests so long-running restore operations don't disrupt - # tests sharing the primary cluster. Falls back to the primary - # cluster/bucket when these secrets are not configured. - TF_VAR_snapshot_cluster_id: ${{ secrets.CAPELLA_SNAPSHOT_CLUSTER_ID }} - TF_VAR_snapshot_bucket_id: ${{ secrets.CAPELLA_SNAPSHOT_BUCKET_ID }} CAPELLA_OPENAPI_SPEC_PATH: ${{ github.workspace }}/openapi.generated.yaml steps: diff --git a/acceptance_tests/envVars.go b/acceptance_tests/envVars.go index 6332b17e8..3250eebe6 100644 --- a/acceptance_tests/envVars.go +++ b/acceptance_tests/envVars.go @@ -24,25 +24,5 @@ func getEnvVars() error { globalAppServiceId = os.Getenv("TF_VAR_app_service_id") globalBucketId = os.Getenv("TF_VAR_bucket_id") - // Optional dedicated cluster for snapshot/restore tests so that long - // restore windows don't interfere with the shared cluster used by other - // tests. Falls back to the primary cluster/bucket when unset. - globalSnapshotClusterId = os.Getenv("TF_VAR_snapshot_cluster_id") - globalSnapshotBucketId = os.Getenv("TF_VAR_snapshot_bucket_id") - return nil } - -func snapshotClusterId() string { - if globalSnapshotClusterId != "" { - return globalSnapshotClusterId - } - return globalClusterId -} - -func snapshotBucketId() string { - if globalSnapshotBucketId != "" { - return globalSnapshotBucketId - } - return globalBucketId -} diff --git a/acceptance_tests/globals.go b/acceptance_tests/globals.go index 1be99b834..83c0919e3 100644 --- a/acceptance_tests/globals.go +++ b/acceptance_tests/globals.go @@ -24,14 +24,7 @@ var ( globalCollectionName = "_default" globalBucketId string globalBucketCreated bool - // globalSnapshotClusterId / globalSnapshotBucketId are optional and, when - // provided via TF_VAR_snapshot_cluster_id / TF_VAR_snapshot_bucket_id, are - // used by the cloud_snapshot_* acceptance tests so that long-running - // restore operations don't disrupt other tests sharing the primary - // cluster. They fall back to globalClusterId / globalBucketId when unset. - globalSnapshotClusterId string - globalSnapshotBucketId string - globalAppServiceId string + globalAppServiceId string globalAppEndpointName = "tf_acc_test_app_endpoint_common" // this global variable is set in TestMain. From 8c0f4619f6a7af2d64f1971d278e0aa081c90f9b Mon Sep 17 00:00:00 2001 From: Subhrajit Panigrahi Date: Thu, 7 May 2026 14:41:15 +0530 Subject: [PATCH 06/20] [AV-127651] Make TestAccDatasourceBackups read-only to avoid 60m timeout The previous version provisioned a fresh couchbase-capella_backup resource and waited for it to complete. Capella's legacy bucket backup endpoint serialises backups per bucket; when this test runs minutes after TestAccBackupResource on the same shared bucket, the second backup record gets stuck in a non-terminal state and the resource hits its 60 minute completion timeout, blowing out CI. Switch to a read-only data source config that just queries the existing backups list. This still exercises the data source schema, request plumbing, and response decoding without relying on Capella scheduling a new backup mid-test. Specific data.0.* assertions are dropped because the data source's contents depend on whether TestAccBackupResource ran first; the resource and bucket id echo checks remain to confirm the read returned successfully. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- acceptance_tests/backup_acceptance_test.go | 30 +++++++++------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/acceptance_tests/backup_acceptance_test.go b/acceptance_tests/backup_acceptance_test.go index 55fbe5d2e..847600a9c 100644 --- a/acceptance_tests/backup_acceptance_test.go +++ b/acceptance_tests/backup_acceptance_test.go @@ -116,10 +116,15 @@ resource "couchbase-capella_backup" "%[2]s" { }) } -// TODO: depends on legacy bucket backup endpoint working; see TODO above -// TestAccBackupResource. +// TestAccDatasourceBackups reads the backups list for the shared bucket. It +// intentionally does not create a fresh backup resource because Capella's +// legacy bucket backup endpoint serialises backups per bucket — creating a +// second backup right after TestAccBackupResource runs frequently leaves the +// new backup record stuck in a non-terminal state, so the resource times out +// at the 60 minute budget. Reading the existing backup created by +// TestAccBackupResource avoids that race and still exercises the data source +// schema and decoding path. func TestAccDatasourceBackups(t *testing.T) { - backupResourceName := randomStringWithPrefix("tf_acc_backup_") dsName := randomStringWithPrefix("tf_acc_backups_ds_") dsReference := "data.couchbase-capella_backups." + dsName @@ -127,15 +132,12 @@ func TestAccDatasourceBackups(t *testing.T) { ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { - Config: testAccBackupsDatasourceConfig(backupResourceName, dsName), + Config: testAccBackupsDatasourceReadOnlyConfig(dsName), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(dsReference, "organization_id", globalOrgId), resource.TestCheckResourceAttr(dsReference, "project_id", globalProjectId), resource.TestCheckResourceAttr(dsReference, "cluster_id", globalClusterId), resource.TestCheckResourceAttr(dsReference, "bucket_id", globalBucketId), - resource.TestCheckResourceAttrSet(dsReference, "data.0.id"), - resource.TestCheckResourceAttrSet(dsReference, "data.0.cycle_id"), - resource.TestCheckResourceAttrSet(dsReference, "data.0.status"), ), }, }, @@ -159,25 +161,17 @@ resource "couchbase-capella_backup" "%[2]s" { `, globalProviderBlock, resourceName, globalOrgId, globalProjectId, globalClusterId, bucketID) } -func testAccBackupsDatasourceConfig(backupResourceName, dsName string) string { +func testAccBackupsDatasourceReadOnlyConfig(dsName string) string { return fmt.Sprintf(` %[1]s -resource "couchbase-capella_backup" "%[2]s" { - organization_id = "%[3]s" - project_id = "%[4]s" - cluster_id = "%[5]s" - bucket_id = "%[6]s" -} - -data "couchbase-capella_backups" "%[7]s" { +data "couchbase-capella_backups" "%[2]s" { organization_id = "%[3]s" project_id = "%[4]s" cluster_id = "%[5]s" bucket_id = "%[6]s" - depends_on = [couchbase-capella_backup.%[2]s] } -`, globalProviderBlock, backupResourceName, globalOrgId, globalProjectId, globalClusterId, globalBucketId, dsName) +`, globalProviderBlock, dsName, globalOrgId, globalProjectId, globalClusterId, globalBucketId) } func generateBackupImportIdForResource(resourceReference string) resource.ImportStateIdFunc { From 8dbc8c108cee61b6aa9fd3bd93c05f3fdc669060 Mon Sep 17 00:00:00 2001 From: Subhrajit Panigrahi Date: Thu, 7 May 2026 14:50:08 +0530 Subject: [PATCH 07/20] [AV-127651] Fold backups data source into TestAccBackupResource MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Capella's legacy bucket backup endpoint serialises manual backups per bucket and enforces a per-bucket spacing window — back-to-back manual backups against the same bucket get stuck in a non-terminal state and trip the 60 minute resource wait. Instead of running TestAccBackupResource and TestAccDatasourceBackups back to back on the same bucket (which was timing out at 60m on CI), collapse them into a single ParallelTest with three sequential steps: 1. Create the backup resource and verify its attributes. 2. Layer the couchbase-capella_backups data source on top of the same resource (depends_on) and verify list contents. 3. Import-state verification. A single backup is created and reused, so the spacing window never trips. The data source still exercises its read path with concrete data.0.* assertions. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- acceptance_tests/backup_acceptance_test.go | 67 +++++++++++----------- 1 file changed, 32 insertions(+), 35 deletions(-) diff --git a/acceptance_tests/backup_acceptance_test.go b/acceptance_tests/backup_acceptance_test.go index 847600a9c..07fafb95a 100644 --- a/acceptance_tests/backup_acceptance_test.go +++ b/acceptance_tests/backup_acceptance_test.go @@ -17,13 +17,18 @@ import ( providerschema "github.com/couchbasecloud/terraform-provider-couchbase-capella/internal/schema" ) -// TODO: legacy bucket backup endpoint returns 404 "Unable to find the specified -// bucket" for a bucket that the bucket endpoint resolves successfully. Track via -// the bug filed for couchbase-capella_backup / _backup_schedule before relaxing -// this test. +// TestAccBackupResource creates a backup, verifies its attributes, then +// layers the couchbase-capella_backups data source on top to confirm the +// backups list returns the just-created backup. The data source step +// intentionally reuses the same backup resource (no new backup is created) +// because Capella's legacy bucket backup endpoint serialises manual +// backups per bucket and a back-to-back second backup gets stuck in a +// non-terminal state until the per-bucket spacing window elapses. func TestAccBackupResource(t *testing.T) { resourceName := randomStringWithPrefix("tf_acc_backup_") resourceReference := "couchbase-capella_backup." + resourceName + dsName := randomStringWithPrefix("tf_acc_backups_ds_") + dsReference := "data.couchbase-capella_backups." + dsName resource.ParallelTest(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, @@ -46,6 +51,18 @@ func TestAccBackupResource(t *testing.T) { resource.TestCheckResourceAttrSet(resourceReference, "cloud_provider"), ), }, + { + Config: testAccBackupWithDatasourceConfig(resourceName, dsName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(dsReference, "organization_id", globalOrgId), + resource.TestCheckResourceAttr(dsReference, "project_id", globalProjectId), + resource.TestCheckResourceAttr(dsReference, "cluster_id", globalClusterId), + resource.TestCheckResourceAttr(dsReference, "bucket_id", globalBucketId), + resource.TestCheckResourceAttrSet(dsReference, "data.0.id"), + resource.TestCheckResourceAttrSet(dsReference, "data.0.cycle_id"), + resource.TestCheckResourceAttrSet(dsReference, "data.0.status"), + ), + }, { ResourceName: resourceReference, ImportStateIdFunc: generateBackupImportIdForResource(resourceReference), @@ -116,34 +133,6 @@ resource "couchbase-capella_backup" "%[2]s" { }) } -// TestAccDatasourceBackups reads the backups list for the shared bucket. It -// intentionally does not create a fresh backup resource because Capella's -// legacy bucket backup endpoint serialises backups per bucket — creating a -// second backup right after TestAccBackupResource runs frequently leaves the -// new backup record stuck in a non-terminal state, so the resource times out -// at the 60 minute budget. Reading the existing backup created by -// TestAccBackupResource avoids that race and still exercises the data source -// schema and decoding path. -func TestAccDatasourceBackups(t *testing.T) { - dsName := randomStringWithPrefix("tf_acc_backups_ds_") - dsReference := "data.couchbase-capella_backups." + dsName - - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: globalProtoV6ProviderFactory, - Steps: []resource.TestStep{ - { - Config: testAccBackupsDatasourceReadOnlyConfig(dsName), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr(dsReference, "organization_id", globalOrgId), - resource.TestCheckResourceAttr(dsReference, "project_id", globalProjectId), - resource.TestCheckResourceAttr(dsReference, "cluster_id", globalClusterId), - resource.TestCheckResourceAttr(dsReference, "bucket_id", globalBucketId), - ), - }, - }, - }) -} - func testAccBackupResourceConfig(resourceName string) string { return testAccBackupResourceConfigWithBucketID(resourceName, globalBucketId) } @@ -161,17 +150,25 @@ resource "couchbase-capella_backup" "%[2]s" { `, globalProviderBlock, resourceName, globalOrgId, globalProjectId, globalClusterId, bucketID) } -func testAccBackupsDatasourceReadOnlyConfig(dsName string) string { +func testAccBackupWithDatasourceConfig(resourceName, dsName string) string { return fmt.Sprintf(` %[1]s -data "couchbase-capella_backups" "%[2]s" { +resource "couchbase-capella_backup" "%[2]s" { + organization_id = "%[3]s" + project_id = "%[4]s" + cluster_id = "%[5]s" + bucket_id = "%[6]s" +} + +data "couchbase-capella_backups" "%[7]s" { organization_id = "%[3]s" project_id = "%[4]s" cluster_id = "%[5]s" bucket_id = "%[6]s" + depends_on = [couchbase-capella_backup.%[2]s] } -`, globalProviderBlock, dsName, globalOrgId, globalProjectId, globalClusterId, globalBucketId) +`, globalProviderBlock, resourceName, globalOrgId, globalProjectId, globalClusterId, globalBucketId, dsName) } func generateBackupImportIdForResource(resourceReference string) resource.ImportStateIdFunc { From f23d2fd594e0c5e1066d5f39cffed3087e09564c Mon Sep 17 00:00:00 2001 From: Subhrajit Panigrahi Date: Fri, 8 May 2026 19:08:44 +0530 Subject: [PATCH 08/20] [AV-127651] Fix 4 P1 issues flagged by factory-droid - bucket.go: replace bucketExists with fetchBucket that also reads the bucket name; resolveBucket now sets globalBucketName when adopting a TF_VAR_bucket_id-provided bucket, fixing assertion failures when the env bucket is not named "default" - backup_acceptance_test.go: switch to resource.Test (non-parallel) to eliminate racy "latest backup" conflicts between parallel test runs - backup_schedule_acceptance_test.go: switch to resource.Test to serialise access to the per-bucket singleton schedule - cloud_snapshot_backup_schedule_datasource_test.go: switch to resource.Test to avoid racing TestAccSnapshotBackupScheduleResource on the per-cluster singleton schedule Co-Authored-By: Claude Sonnet 4.6 --- acceptance_tests/backup_acceptance_test.go | 8 ++--- .../backup_schedule_acceptance_test.go | 12 ++++---- acceptance_tests/bucket.go | 29 ++++++++++++------- ...napshot_backup_schedule_datasource_test.go | 4 +-- 4 files changed, 30 insertions(+), 23 deletions(-) diff --git a/acceptance_tests/backup_acceptance_test.go b/acceptance_tests/backup_acceptance_test.go index 07fafb95a..a684bb3c3 100644 --- a/acceptance_tests/backup_acceptance_test.go +++ b/acceptance_tests/backup_acceptance_test.go @@ -30,7 +30,7 @@ func TestAccBackupResource(t *testing.T) { dsName := randomStringWithPrefix("tf_acc_backups_ds_") dsReference := "data.couchbase-capella_backups." + dsName - resource.ParallelTest(t, resource.TestCase{ + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { @@ -75,7 +75,7 @@ func TestAccBackupResource(t *testing.T) { func TestAccBackupResourceInvalidBucket(t *testing.T) { resourceName := randomStringWithPrefix("tf_acc_backup_invalid_") - resource.ParallelTest(t, resource.TestCase{ + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { @@ -89,7 +89,7 @@ func TestAccBackupResourceInvalidBucket(t *testing.T) { func TestAccBackupResourceInvalidProject(t *testing.T) { resourceName := randomStringWithPrefix("tf_acc_backup_invalid_proj_") - resource.ParallelTest(t, resource.TestCase{ + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { @@ -112,7 +112,7 @@ resource "couchbase-capella_backup" "%[2]s" { func TestAccBackupResourceRestoreTimesOnCreate(t *testing.T) { resourceName := randomStringWithPrefix("tf_acc_backup_invalid_rt_") - resource.ParallelTest(t, resource.TestCase{ + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { diff --git a/acceptance_tests/backup_schedule_acceptance_test.go b/acceptance_tests/backup_schedule_acceptance_test.go index 83a13a104..9fd665803 100644 --- a/acceptance_tests/backup_schedule_acceptance_test.go +++ b/acceptance_tests/backup_schedule_acceptance_test.go @@ -24,7 +24,7 @@ func TestAccBackupScheduleResource(t *testing.T) { resourceName := randomStringWithPrefix("tf_acc_backup_schedule_") resourceReference := "couchbase-capella_backup_schedule." + resourceName - resource.ParallelTest(t, resource.TestCase{ + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { @@ -71,7 +71,7 @@ func TestAccBackupScheduleResource(t *testing.T) { func TestAccBackupScheduleResourceInvalidDayOfWeek(t *testing.T) { resourceName := randomStringWithPrefix("tf_acc_backup_schedule_") - resource.ParallelTest(t, resource.TestCase{ + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { @@ -85,7 +85,7 @@ func TestAccBackupScheduleResourceInvalidDayOfWeek(t *testing.T) { func TestAccBackupScheduleResourceInvalidRetention(t *testing.T) { resourceName := randomStringWithPrefix("tf_acc_backup_schedule_") - resource.ParallelTest(t, resource.TestCase{ + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { @@ -99,7 +99,7 @@ func TestAccBackupScheduleResourceInvalidRetention(t *testing.T) { func TestAccBackupScheduleResourceInvalidType(t *testing.T) { resourceName := randomStringWithPrefix("tf_acc_backup_schedule_") - resource.ParallelTest(t, resource.TestCase{ + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { @@ -113,7 +113,7 @@ func TestAccBackupScheduleResourceInvalidType(t *testing.T) { func TestAccBackupScheduleResourceInvalidStartAt(t *testing.T) { resourceName := randomStringWithPrefix("tf_acc_backup_schedule_") - resource.ParallelTest(t, resource.TestCase{ + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { @@ -127,7 +127,7 @@ func TestAccBackupScheduleResourceInvalidStartAt(t *testing.T) { func TestAccBackupScheduleResourceInvalidBucket(t *testing.T) { resourceName := randomStringWithPrefix("tf_acc_backup_schedule_invalid_bkt_") - resource.ParallelTest(t, resource.TestCase{ + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { diff --git a/acceptance_tests/bucket.go b/acceptance_tests/bucket.go index c3ca04315..2a3cec790 100644 --- a/acceptance_tests/bucket.go +++ b/acceptance_tests/bucket.go @@ -13,18 +13,24 @@ import ( bucketapi "github.com/couchbasecloud/terraform-provider-couchbase-capella/internal/api/bucket" ) -func bucketExists(ctx context.Context, client *api.Client, bucketId string) (bool, error) { +// fetchBucket returns the bucket name for the given ID, or ("", notFound) if +// the bucket does not exist. +func fetchBucket(ctx context.Context, client *api.Client, bucketId string) (name string, found bool, err error) { url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/buckets/%s", globalHost, globalOrgId, globalProjectId, globalClusterId, bucketId) cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK} - _, err := client.ExecuteWithRetry(ctx, cfg, nil, globalToken, nil) - if err == nil { - return true, nil + response, apiErr := client.ExecuteWithRetry(ctx, cfg, nil, globalToken, nil) + if apiErr != nil { + var apiErrTyped *api.Error + if stderrors.As(apiErr, &apiErrTyped) && apiErrTyped.HttpStatusCode == http.StatusNotFound { + return "", false, nil + } + return "", false, apiErr } - var apiErr *api.Error - if stderrors.As(err, &apiErr) && apiErr.HttpStatusCode == http.StatusNotFound { - return false, nil + var bucket bucketapi.GetBucketResponse + if err = json.Unmarshal(response.Body, &bucket); err != nil { + return "", false, err } - return false, err + return bucket.Name, true, nil } func discoverFirstBucket(ctx context.Context, client *api.Client) (string, string, error) { @@ -90,12 +96,13 @@ func destroyBucket(ctx context.Context, client *api.Client) error { func resolveBucket(ctx context.Context, client *api.Client) error { if globalBucketId != "" { - exists, err := bucketExists(ctx, client, globalBucketId) + name, found, err := fetchBucket(ctx, client, globalBucketId) if err != nil { return err } - if exists { - log.Printf("Using existing bucket: %s", globalBucketId) + if found { + globalBucketName = name + log.Printf("Using existing bucket: %s (%s)", name, globalBucketId) return nil } log.Printf("TF_VAR_bucket_id=%s does not exist on cluster; discovering or creating one", globalBucketId) diff --git a/acceptance_tests/cloud_snapshot_backup_schedule_datasource_test.go b/acceptance_tests/cloud_snapshot_backup_schedule_datasource_test.go index 61068d9b2..20d670d7a 100644 --- a/acceptance_tests/cloud_snapshot_backup_schedule_datasource_test.go +++ b/acceptance_tests/cloud_snapshot_backup_schedule_datasource_test.go @@ -16,7 +16,7 @@ func TestAccDatasourceCloudSnapshotBackupSchedule(t *testing.T) { startTime := time.Now().Add(24 * time.Hour).Truncate(time.Hour).Format(time.RFC3339) - resource.ParallelTest(t, resource.TestCase{ + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { @@ -37,7 +37,7 @@ func TestAccDatasourceCloudSnapshotBackupSchedule(t *testing.T) { func TestAccDatasourceCloudSnapshotBackupScheduleInvalidCluster(t *testing.T) { dsName := randomStringWithPrefix("tf_acc_cloud_snapshot_backup_schedule_ds_invalid_") - resource.ParallelTest(t, resource.TestCase{ + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { From aeb08ae751ef08f16ad39fa2494616a17d91c966 Mon Sep 17 00:00:00 2001 From: Subhrajit Panigrahi Date: Fri, 8 May 2026 19:13:26 +0530 Subject: [PATCH 09/20] [AV-127651] Recover from backend 500 on cluster creation (AV-129960) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The backend intermittently returns HTTP 500 on POST /clusters while still creating the cluster. When this happens setup() returns early without setting globalClusterId, then cleanup() fails to delete the project because the cluster exists (400 "has Cluster resources"). Fix: after createCluster returns an error, scan the project's cluster list by name. If the cluster was actually created, adopt its ID so setup continues normally and cleanup can destroy it. Also reverts the ParallelTest→Test changes from the previous commit; tests should remain parallel per team convention. Co-Authored-By: Claude Sonnet 4.6 --- acceptance_tests/backup_acceptance_test.go | 8 ++++---- .../backup_schedule_acceptance_test.go | 12 ++++++------ ..._snapshot_backup_schedule_datasource_test.go | 4 ++-- acceptance_tests/cluster.go | 17 +++++++++++++++++ acceptance_tests/setup_test.go | 9 ++++++++- 5 files changed, 37 insertions(+), 13 deletions(-) diff --git a/acceptance_tests/backup_acceptance_test.go b/acceptance_tests/backup_acceptance_test.go index a684bb3c3..07fafb95a 100644 --- a/acceptance_tests/backup_acceptance_test.go +++ b/acceptance_tests/backup_acceptance_test.go @@ -30,7 +30,7 @@ func TestAccBackupResource(t *testing.T) { dsName := randomStringWithPrefix("tf_acc_backups_ds_") dsReference := "data.couchbase-capella_backups." + dsName - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { @@ -75,7 +75,7 @@ func TestAccBackupResource(t *testing.T) { func TestAccBackupResourceInvalidBucket(t *testing.T) { resourceName := randomStringWithPrefix("tf_acc_backup_invalid_") - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { @@ -89,7 +89,7 @@ func TestAccBackupResourceInvalidBucket(t *testing.T) { func TestAccBackupResourceInvalidProject(t *testing.T) { resourceName := randomStringWithPrefix("tf_acc_backup_invalid_proj_") - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { @@ -112,7 +112,7 @@ resource "couchbase-capella_backup" "%[2]s" { func TestAccBackupResourceRestoreTimesOnCreate(t *testing.T) { resourceName := randomStringWithPrefix("tf_acc_backup_invalid_rt_") - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { diff --git a/acceptance_tests/backup_schedule_acceptance_test.go b/acceptance_tests/backup_schedule_acceptance_test.go index 9fd665803..83a13a104 100644 --- a/acceptance_tests/backup_schedule_acceptance_test.go +++ b/acceptance_tests/backup_schedule_acceptance_test.go @@ -24,7 +24,7 @@ func TestAccBackupScheduleResource(t *testing.T) { resourceName := randomStringWithPrefix("tf_acc_backup_schedule_") resourceReference := "couchbase-capella_backup_schedule." + resourceName - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { @@ -71,7 +71,7 @@ func TestAccBackupScheduleResource(t *testing.T) { func TestAccBackupScheduleResourceInvalidDayOfWeek(t *testing.T) { resourceName := randomStringWithPrefix("tf_acc_backup_schedule_") - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { @@ -85,7 +85,7 @@ func TestAccBackupScheduleResourceInvalidDayOfWeek(t *testing.T) { func TestAccBackupScheduleResourceInvalidRetention(t *testing.T) { resourceName := randomStringWithPrefix("tf_acc_backup_schedule_") - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { @@ -99,7 +99,7 @@ func TestAccBackupScheduleResourceInvalidRetention(t *testing.T) { func TestAccBackupScheduleResourceInvalidType(t *testing.T) { resourceName := randomStringWithPrefix("tf_acc_backup_schedule_") - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { @@ -113,7 +113,7 @@ func TestAccBackupScheduleResourceInvalidType(t *testing.T) { func TestAccBackupScheduleResourceInvalidStartAt(t *testing.T) { resourceName := randomStringWithPrefix("tf_acc_backup_schedule_") - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { @@ -127,7 +127,7 @@ func TestAccBackupScheduleResourceInvalidStartAt(t *testing.T) { func TestAccBackupScheduleResourceInvalidBucket(t *testing.T) { resourceName := randomStringWithPrefix("tf_acc_backup_schedule_invalid_bkt_") - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { diff --git a/acceptance_tests/cloud_snapshot_backup_schedule_datasource_test.go b/acceptance_tests/cloud_snapshot_backup_schedule_datasource_test.go index 20d670d7a..61068d9b2 100644 --- a/acceptance_tests/cloud_snapshot_backup_schedule_datasource_test.go +++ b/acceptance_tests/cloud_snapshot_backup_schedule_datasource_test.go @@ -16,7 +16,7 @@ func TestAccDatasourceCloudSnapshotBackupSchedule(t *testing.T) { startTime := time.Now().Add(24 * time.Hour).Truncate(time.Hour).Format(time.RFC3339) - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { @@ -37,7 +37,7 @@ func TestAccDatasourceCloudSnapshotBackupSchedule(t *testing.T) { func TestAccDatasourceCloudSnapshotBackupScheduleInvalidCluster(t *testing.T) { dsName := randomStringWithPrefix("tf_acc_cloud_snapshot_backup_schedule_ds_invalid_") - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { diff --git a/acceptance_tests/cluster.go b/acceptance_tests/cluster.go index 473dd54c9..a7f9b800b 100644 --- a/acceptance_tests/cluster.go +++ b/acceptance_tests/cluster.go @@ -80,6 +80,23 @@ func createCluster(ctx context.Context, client *api.Client) error { return nil } +// findClusterByName lists clusters in the current project and returns the ID of +// the first cluster matching name, or "" if none is found. +func findClusterByName(ctx context.Context, client *api.Client, name string) (string, error) { + url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters", globalHost, globalOrgId, globalProjectId) + cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK} + clusters, err := api.GetPaginated[[]clusterapi.GetClusterResponse](ctx, client, globalToken, cfg, api.SortById) + if err != nil { + return "", err + } + for _, c := range clusters { + if c.Name == name { + return c.Id.String(), nil + } + } + return "", nil +} + func destroyCluster(ctx context.Context, client *api.Client) error { url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s", globalHost, globalOrgId, globalProjectId, globalClusterId) cfg := api.EndpointCfg{Url: url, Method: http.MethodDelete, SuccessStatus: http.StatusAccepted} diff --git a/acceptance_tests/setup_test.go b/acceptance_tests/setup_test.go index 6e2061595..0cd6ddf2a 100644 --- a/acceptance_tests/setup_test.go +++ b/acceptance_tests/setup_test.go @@ -68,7 +68,14 @@ func setup(ctx context.Context, client *api.Client) error { // Create cluster only if not provided via env var if globalClusterId == "" { if err := createCluster(ctx, client); err != nil { - return err + // The backend sometimes returns 500 while still creating the cluster + // (AV-129960). Check by name so we can adopt it and clean up properly. + id, findErr := findClusterByName(ctx, client, "tf_acc_test_cluster_common") + if findErr != nil || id == "" { + return err + } + log.Printf("createCluster returned error but cluster was found; adopting %s", id) + globalClusterId = id } if err := clusterWait(ctx, client, false); err != nil { return err From d85bd99110f537948c82e801250c97a0d89a0c70 Mon Sep 17 00:00:00 2001 From: Subhrajit Panigrahi Date: Fri, 8 May 2026 19:17:04 +0530 Subject: [PATCH 10/20] [AV-127651] Read TF_VAR_bucket_name env var in test setup Allows callers to supply an explicit bucket name when providing TF_VAR_bucket_id, as a faster alternative to the GET-bucket fetch that resolveBucket performs when adopting an env-provided bucket. Co-Authored-By: Claude Sonnet 4.6 --- acceptance_tests/envVars.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/acceptance_tests/envVars.go b/acceptance_tests/envVars.go index 3250eebe6..a3c171d0a 100644 --- a/acceptance_tests/envVars.go +++ b/acceptance_tests/envVars.go @@ -23,6 +23,9 @@ func getEnvVars() error { globalClusterId = os.Getenv("TF_VAR_cluster_id") globalAppServiceId = os.Getenv("TF_VAR_app_service_id") globalBucketId = os.Getenv("TF_VAR_bucket_id") + if name := os.Getenv("TF_VAR_bucket_name"); name != "" { + globalBucketName = name + } return nil } From 8d01bc4001a6618f0210cfc4b9cc8d89fefbec03 Mon Sep 17 00:00:00 2001 From: Subhrajit Panigrahi Date: Mon, 11 May 2026 12:04:22 +0530 Subject: [PATCH 11/20] [AV-127651] Track bucket lifecycle; restrict 5xx adoption fallback - globals.go: restore globalBucketCreated flag - bucket.go: set globalBucketCreated=true after resolveBucket creates a new bucket; add destroyBucket for cleanup - setup_test.go: restrict the AV-129960 cluster-adoption fallback to 5xx errors only so 4xx (auth/validation) failures are not masked; destroy bucket in cleanup when globalBucketCreated is set Co-Authored-By: Claude Sonnet 4.6 --- acceptance_tests/bucket.go | 16 ++++++++++++++++ acceptance_tests/globals.go | 1 + acceptance_tests/setup_test.go | 17 ++++++++++++++--- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/acceptance_tests/bucket.go b/acceptance_tests/bucket.go index 3c36587bc..5998898fc 100644 --- a/acceptance_tests/bucket.go +++ b/acceptance_tests/bucket.go @@ -110,10 +110,26 @@ func resolveBucket(ctx context.Context, client *api.Client) error { if err := bucketWait(ctx, client); err != nil { return err } + globalBucketCreated = true log.Printf("Created bucket: %s (%s)", globalBucketName, globalBucketId) return nil } +func destroyBucket(ctx context.Context, client *api.Client) error { + url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/buckets/%s", globalHost, globalOrgId, globalProjectId, globalClusterId, globalBucketId) + cfg := api.EndpointCfg{Url: url, Method: http.MethodDelete, SuccessStatus: http.StatusNoContent} + _, err := client.ExecuteWithRetry(ctx, cfg, nil, globalToken, nil) + if err != nil { + var apiErr *api.Error + if errors.As(err, &apiErr) && apiErr.HttpStatusCode == http.StatusNotFound { + return nil + } + return err + } + log.Printf("bucket destroyed: %s", globalBucketId) + return nil +} + func resolveBucketNameById(ctx context.Context, client *api.Client, bucketID string) (string, error) { listUrl := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/buckets", globalHost, globalOrgId, globalProjectId, globalClusterId) listCfg := api.EndpointCfg{Url: listUrl, Method: http.MethodGet, SuccessStatus: http.StatusOK} diff --git a/acceptance_tests/globals.go b/acceptance_tests/globals.go index 611f691da..cbfe33786 100644 --- a/acceptance_tests/globals.go +++ b/acceptance_tests/globals.go @@ -27,6 +27,7 @@ var ( globalScopeName = "_default" globalCollectionName = "_default" globalBucketId string + globalBucketCreated bool globalAppServiceId string globalAppServiceName = "tf_acc_test_app_service_common" globalAppEndpointName = "tf_acc_test_app_endpoint_common" diff --git a/acceptance_tests/setup_test.go b/acceptance_tests/setup_test.go index 138a709c4..56db21caf 100644 --- a/acceptance_tests/setup_test.go +++ b/acceptance_tests/setup_test.go @@ -2,6 +2,7 @@ package acceptance_tests import ( "context" + "errors" "log" "os" "testing" @@ -69,13 +70,17 @@ func setup(ctx context.Context, client *api.Client) error { // Create cluster only if not provided via env var if globalClusterId == "" { if err := createCluster(ctx, client); err != nil { - // The backend sometimes returns 500 while still creating the cluster - // (AV-129960). Check by name so we can adopt it and clean up properly. + // Only fall back to findClusterByName on 5xx (AV-129960): the backend + // sometimes returns 500 while still creating the cluster. + var apiErr *api.Error + if !errors.As(err, &apiErr) || apiErr.HttpStatusCode < 500 { + return err + } id, findErr := findClusterByName(ctx, client, globalClusterName) if findErr != nil || id == "" { return err } - log.Printf("createCluster returned error but cluster was found; adopting %s", id) + log.Printf("createCluster returned 5xx but cluster was found; adopting %s", id) globalClusterId = id } globalClusterCreated = true @@ -136,6 +141,12 @@ func cleanup(ctx context.Context, client *api.Client) error { } } + if globalBucketCreated { + if err := destroyBucket(ctx, client); err != nil { + return err + } + } + if globalClusterCreated { if err := destroyCluster(ctx, client); err != nil { return err From b8e6d0716eb38e39b99ff1760367fa019ebb1dc8 Mon Sep 17 00:00:00 2001 From: Subhrajit Panigrahi Date: Mon, 11 May 2026 12:18:43 +0530 Subject: [PATCH 12/20] [AV-127651] Assert datasource returns created backup's ID via AttrPair Co-Authored-By: Claude Sonnet 4.6 --- acceptance_tests/backup_acceptance_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acceptance_tests/backup_acceptance_test.go b/acceptance_tests/backup_acceptance_test.go index 07fafb95a..5c1ae2b7b 100644 --- a/acceptance_tests/backup_acceptance_test.go +++ b/acceptance_tests/backup_acceptance_test.go @@ -58,7 +58,7 @@ func TestAccBackupResource(t *testing.T) { resource.TestCheckResourceAttr(dsReference, "project_id", globalProjectId), resource.TestCheckResourceAttr(dsReference, "cluster_id", globalClusterId), resource.TestCheckResourceAttr(dsReference, "bucket_id", globalBucketId), - resource.TestCheckResourceAttrSet(dsReference, "data.0.id"), + resource.TestCheckResourceAttrPair(dsReference, "data.0.id", resourceReference, "id"), resource.TestCheckResourceAttrSet(dsReference, "data.0.cycle_id"), resource.TestCheckResourceAttrSet(dsReference, "data.0.status"), ), From b0884d6a390b8b35c556cfd8b22415eda3feaf95 Mon Sep 17 00:00:00 2001 From: Subhrajit Panigrahi Date: Mon, 11 May 2026 12:43:38 +0530 Subject: [PATCH 13/20] [AV-127651] Only mark cluster for cleanup when createCluster succeeded, not on 5xx adoption Co-Authored-By: Claude Sonnet 4.6 --- acceptance_tests/setup_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/acceptance_tests/setup_test.go b/acceptance_tests/setup_test.go index 56db21caf..f39a8f00a 100644 --- a/acceptance_tests/setup_test.go +++ b/acceptance_tests/setup_test.go @@ -82,8 +82,9 @@ func setup(ctx context.Context, client *api.Client) error { } log.Printf("createCluster returned 5xx but cluster was found; adopting %s", id) globalClusterId = id + } else { + globalClusterCreated = true } - globalClusterCreated = true if err := clusterWait(ctx, client, false); err != nil { return err } From c1696f987024d54106a49d884fce71f84db29cdd Mon Sep 17 00:00:00 2001 From: Subhrajit Panigrahi Date: Mon, 11 May 2026 15:51:12 +0530 Subject: [PATCH 14/20] [AV-127651] Reuse existing project on quota error so concurrent CI runs don't fail Co-Authored-By: Claude Sonnet 4.6 --- acceptance_tests/project.go | 63 ++++++++++++++++++++++------------ acceptance_tests/setup_test.go | 5 +-- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/acceptance_tests/project.go b/acceptance_tests/project.go index 7c917e2b7..e16a9ed76 100644 --- a/acceptance_tests/project.go +++ b/acceptance_tests/project.go @@ -3,6 +3,7 @@ package acceptance_tests import ( "context" "encoding/json" + "errors" "fmt" "log" "net/http" @@ -10,51 +11,69 @@ import ( "github.com/couchbasecloud/terraform-provider-couchbase-capella/internal/api" ) -func createProject(ctx context.Context, client *api.Client) error { +const projectName = "tf_acc_test_project_common" + +// createProject creates a new test project. On quota error (403) it falls back +// to findProjectByName and reuses an existing project so that concurrent CI +// runs sharing one Capella account do not all fail when the project limit is +// reached. Returns (created=true) only when a new project was actually created. +func createProject(ctx context.Context, client *api.Client) (bool, error) { projectRequest := api.CreateProjectRequest{ - Name: "tf_acc_test_project_common", + Name: projectName, } url := fmt.Sprintf("%s/v4/organizations/%s/projects", globalHost, globalOrgId) cfg := api.EndpointCfg{Url: url, Method: http.MethodPost, SuccessStatus: http.StatusCreated} - response, err := client.ExecuteWithRetry( - ctx, - cfg, - projectRequest, - globalToken, - nil, - ) + response, err := client.ExecuteWithRetry(ctx, cfg, projectRequest, globalToken, nil) if err != nil { - return err + // On quota exhaustion, try to reuse an existing project rather than + // failing the whole test suite immediately. + var apiErr *api.Error + if errors.As(err, &apiErr) && apiErr.HttpStatusCode == http.StatusForbidden { + id, findErr := findProjectByName(ctx, client, projectName) + if findErr == nil && id != "" { + log.Printf("project quota hit; reusing existing project %s", id) + globalProjectId = id + return false, nil + } + } + return false, err } projectResponse := api.GetProjectResponse{} if err = json.Unmarshal(response.Body, &projectResponse); err != nil { - return err + return false, err } log.Print("project created") - globalProjectId = projectResponse.Id.String() + return true, nil +} - return nil +// findProjectByName returns the ID of the first project in the org whose name +// matches, or "" if none is found. +func findProjectByName(ctx context.Context, client *api.Client, name string) (string, error) { + url := fmt.Sprintf("%s/v4/organizations/%s/projects", globalHost, globalOrgId) + cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK} + projects, err := api.GetPaginated[[]api.GetProjectResponse](ctx, client, globalToken, cfg, api.SortById) + if err != nil { + return "", err + } + for _, p := range projects { + if p.Name == name { + return p.Id.String(), nil + } + } + return "", nil } func destroyProject(ctx context.Context, client *api.Client) error { url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s", globalHost, globalOrgId, globalProjectId) cfg := api.EndpointCfg{Url: url, Method: http.MethodDelete, SuccessStatus: http.StatusNoContent} - _, err := client.ExecuteWithRetry( - ctx, - cfg, - nil, - globalToken, - nil, - ) + _, err := client.ExecuteWithRetry(ctx, cfg, nil, globalToken, nil) if err != nil { return err } - log.Print("project destroyed") - return nil } diff --git a/acceptance_tests/setup_test.go b/acceptance_tests/setup_test.go index f39a8f00a..81d3c224b 100644 --- a/acceptance_tests/setup_test.go +++ b/acceptance_tests/setup_test.go @@ -59,10 +59,11 @@ provider "couchbase-capella" { func setup(ctx context.Context, client *api.Client) error { // Create project only if not provided via env var if globalProjectId == "" { - if err := createProject(ctx, client); err != nil { + created, err := createProject(ctx, client) + if err != nil { return err } - globalProjectCreated = true + globalProjectCreated = created } else { log.Printf("Using existing project: %s", globalProjectId) } From 5259390c8652bf6be5e3f6b2c681bf5bc964b06f Mon Sep 17 00:00:00 2001 From: Subhrajit Panigrahi Date: Mon, 11 May 2026 17:09:45 +0530 Subject: [PATCH 15/20] [AV-127651] Scan full backup list to verify created backup is present Replace TestCheckResourceAttrPair on data.0.id with a custom check that scans all items in the datasource result. The previous assertion only verified index 0, which could match an unrelated existing backup if the created one was not returned first. Co-Authored-By: Claude Sonnet 4.6 --- acceptance_tests/backup_acceptance_test.go | 29 +++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/acceptance_tests/backup_acceptance_test.go b/acceptance_tests/backup_acceptance_test.go index 5c1ae2b7b..75164fb2c 100644 --- a/acceptance_tests/backup_acceptance_test.go +++ b/acceptance_tests/backup_acceptance_test.go @@ -58,9 +58,7 @@ func TestAccBackupResource(t *testing.T) { resource.TestCheckResourceAttr(dsReference, "project_id", globalProjectId), resource.TestCheckResourceAttr(dsReference, "cluster_id", globalClusterId), resource.TestCheckResourceAttr(dsReference, "bucket_id", globalBucketId), - resource.TestCheckResourceAttrPair(dsReference, "data.0.id", resourceReference, "id"), - resource.TestCheckResourceAttrSet(dsReference, "data.0.cycle_id"), - resource.TestCheckResourceAttrSet(dsReference, "data.0.status"), + testAccCheckDataSourceContainsBackup(dsReference, resourceReference), ), }, { @@ -224,3 +222,28 @@ func testAccExistsBackupResource(t *testing.T, resourceReference string) resourc return retrieveBackupFromServer(data, rawState["organization_id"], rawState["project_id"], rawState["cluster_id"], rawState["id"]) } } + +// testAccCheckDataSourceContainsBackup verifies that the backups data source +// contains the specific backup created by resourceReference, regardless of +// its position in the list (older backups may appear at lower indices). +func testAccCheckDataSourceContainsBackup(dsReference, resourceReference string) resource.TestCheckFunc { + return func(s *terraform.State) error { + ds := s.RootModule().Resources[dsReference] + if ds == nil { + return fmt.Errorf("datasource %s not found in state", dsReference) + } + res := s.RootModule().Resources[resourceReference] + if res == nil { + return fmt.Errorf("resource %s not found in state", resourceReference) + } + expectedID := res.Primary.Attributes["id"] + count := 0 + fmt.Sscanf(ds.Primary.Attributes["data.#"], "%d", &count) + for i := 0; i < count; i++ { + if ds.Primary.Attributes[fmt.Sprintf("data.%d.id", i)] == expectedID { + return nil + } + } + return fmt.Errorf("datasource %s does not contain backup id=%s (checked %d items)", dsReference, expectedID, count) + } +} From 95daa2f5ac6f2e3d4ae36af0eff4e489b992357e Mon Sep 17 00:00:00 2001 From: Subhrajit Panigrahi Date: Mon, 11 May 2026 17:19:02 +0530 Subject: [PATCH 16/20] [AV-127651] Serialize singleton-schedule tests; assert copy_to_regions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Snapshot backup schedule and legacy backup schedule are singletons (per-cluster and per-bucket respectively). Running them under ParallelTest races with other tests that mutate the same resource, causing nondeterministic failures. Serialize both with resource.Test. Also assert copy_to_regions.# = "0" in the snapshot schedule datasource test — the config sets copy_to_regions = null so zero regions is the deterministic expected value. Co-Authored-By: Claude Sonnet 4.6 --- acceptance_tests/backup_schedule_acceptance_test.go | 2 +- .../cloud_snapshot_backup_schedule_datasource_test.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/acceptance_tests/backup_schedule_acceptance_test.go b/acceptance_tests/backup_schedule_acceptance_test.go index 83a13a104..5c6b50b9e 100644 --- a/acceptance_tests/backup_schedule_acceptance_test.go +++ b/acceptance_tests/backup_schedule_acceptance_test.go @@ -24,7 +24,7 @@ func TestAccBackupScheduleResource(t *testing.T) { resourceName := randomStringWithPrefix("tf_acc_backup_schedule_") resourceReference := "couchbase-capella_backup_schedule." + resourceName - resource.ParallelTest(t, resource.TestCase{ + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { diff --git a/acceptance_tests/cloud_snapshot_backup_schedule_datasource_test.go b/acceptance_tests/cloud_snapshot_backup_schedule_datasource_test.go index 61068d9b2..f98c49665 100644 --- a/acceptance_tests/cloud_snapshot_backup_schedule_datasource_test.go +++ b/acceptance_tests/cloud_snapshot_backup_schedule_datasource_test.go @@ -16,7 +16,7 @@ func TestAccDatasourceCloudSnapshotBackupSchedule(t *testing.T) { startTime := time.Now().Add(24 * time.Hour).Truncate(time.Hour).Format(time.RFC3339) - resource.ParallelTest(t, resource.TestCase{ + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { @@ -28,6 +28,7 @@ func TestAccDatasourceCloudSnapshotBackupSchedule(t *testing.T) { resource.TestCheckResourceAttr(dsReference, "interval", "12"), resource.TestCheckResourceAttr(dsReference, "retention", "240"), resource.TestCheckResourceAttr(dsReference, "start_time", startTime), + resource.TestCheckResourceAttr(dsReference, "copy_to_regions.#", "0"), ), }, }, From 2ab7540df69def01e37705e54eca8570e5a24105 Mon Sep 17 00:00:00 2001 From: Subhrajit Panigrahi Date: Mon, 11 May 2026 17:23:56 +0530 Subject: [PATCH 17/20] [AV-127651] Fix errcheck lint; restore ParallelTest; assert copy_to_regions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix errcheck: replace fmt.Sscanf (unchecked return) with strconv.Atoi in testAccCheckDataSourceContainsBackup - Restore resource.ParallelTest on snapshot schedule datasource and backup schedule tests — team convention - Keep copy_to_regions.# = "0" assertion on snapshot schedule datasource (config sets copy_to_regions = null so zero is deterministic) Co-Authored-By: Claude Sonnet 4.6 --- acceptance_tests/backup_acceptance_test.go | 4 ++-- acceptance_tests/backup_schedule_acceptance_test.go | 2 +- .../cloud_snapshot_backup_schedule_datasource_test.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/acceptance_tests/backup_acceptance_test.go b/acceptance_tests/backup_acceptance_test.go index 75164fb2c..04f67426e 100644 --- a/acceptance_tests/backup_acceptance_test.go +++ b/acceptance_tests/backup_acceptance_test.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "regexp" + "strconv" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -237,8 +238,7 @@ func testAccCheckDataSourceContainsBackup(dsReference, resourceReference string) return fmt.Errorf("resource %s not found in state", resourceReference) } expectedID := res.Primary.Attributes["id"] - count := 0 - fmt.Sscanf(ds.Primary.Attributes["data.#"], "%d", &count) + count, _ := strconv.Atoi(ds.Primary.Attributes["data.#"]) for i := 0; i < count; i++ { if ds.Primary.Attributes[fmt.Sprintf("data.%d.id", i)] == expectedID { return nil diff --git a/acceptance_tests/backup_schedule_acceptance_test.go b/acceptance_tests/backup_schedule_acceptance_test.go index 5c6b50b9e..83a13a104 100644 --- a/acceptance_tests/backup_schedule_acceptance_test.go +++ b/acceptance_tests/backup_schedule_acceptance_test.go @@ -24,7 +24,7 @@ func TestAccBackupScheduleResource(t *testing.T) { resourceName := randomStringWithPrefix("tf_acc_backup_schedule_") resourceReference := "couchbase-capella_backup_schedule." + resourceName - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { diff --git a/acceptance_tests/cloud_snapshot_backup_schedule_datasource_test.go b/acceptance_tests/cloud_snapshot_backup_schedule_datasource_test.go index f98c49665..98319c07d 100644 --- a/acceptance_tests/cloud_snapshot_backup_schedule_datasource_test.go +++ b/acceptance_tests/cloud_snapshot_backup_schedule_datasource_test.go @@ -16,7 +16,7 @@ func TestAccDatasourceCloudSnapshotBackupSchedule(t *testing.T) { startTime := time.Now().Add(24 * time.Hour).Truncate(time.Hour).Format(time.RFC3339) - resource.Test(t, resource.TestCase{ + resource.ParallelTest(t, resource.TestCase{ ProtoV6ProviderFactories: globalProtoV6ProviderFactory, Steps: []resource.TestStep{ { From 7d2f790e67674a278091ac82228c14cbe3d0ae44 Mon Sep 17 00:00:00 2001 From: Subhrajit Panigrahi Date: Tue, 12 May 2026 15:36:47 +0530 Subject: [PATCH 18/20] [AV-127651] Set globalBucketCreated before bucketWait to avoid leaks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit createBucket sets globalBucketId once the POST succeeds, so the bucket exists remotely from that point on. Previously globalBucketCreated was only flipped after bucketWait returned successfully, meaning a wait timeout (5 min default) would leave the flag false and skip cleanup — leaking a real bucket across flaky runs. Flip the flag immediately after createBucket succeeds so destroyBucket always runs, regardless of whether the wait step completes. Co-Authored-By: Claude Sonnet 4.6 --- acceptance_tests/bucket.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/acceptance_tests/bucket.go b/acceptance_tests/bucket.go index 5998898fc..a624b0d48 100644 --- a/acceptance_tests/bucket.go +++ b/acceptance_tests/bucket.go @@ -107,10 +107,14 @@ func resolveBucket(ctx context.Context, client *api.Client) error { if err := createBucket(ctx, client); err != nil { return err } + // Mark the bucket as created BEFORE waiting for it to become healthy. The + // POST has already succeeded so the bucket exists remotely; if bucketWait + // times out, cleanup must still delete it to avoid leaking the bucket + // across flaky runs. + globalBucketCreated = true if err := bucketWait(ctx, client); err != nil { return err } - globalBucketCreated = true log.Printf("Created bucket: %s (%s)", globalBucketName, globalBucketId) return nil } From aee77306c94cf57cfde688dd72bc1af9e436fdb5 Mon Sep 17 00:00:00 2001 From: Subhrajit Panigrahi Date: Tue, 12 May 2026 20:04:08 +0530 Subject: [PATCH 19/20] [AV-127651] Bump backup wait timeout to 90m, poll every 30s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The legacy bucket backup wait was hitting its 60-minute deadline on busy clusters (TestAccBackupResource failing at exactly 3600s). The existing comment "can change after discussion" invited the bump. Two changes: - Increase timeout 60 → 90 minutes to give realistic headroom for bucket backups under CI load. - Reduce poll cadence from every 1s to every 30s. The previous 1s cadence issued ~3600 GETs per wait and likely contributed to backend pressure; 30s still detects completion promptly. Co-Authored-By: Claude Sonnet 4.6 --- internal/resources/backup.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/internal/resources/backup.go b/internal/resources/backup.go index 413da32a9..428443f8e 100644 --- a/internal/resources/backup.go +++ b/internal/resources/backup.go @@ -412,16 +412,21 @@ func (b *Backup) checkLatestBackupStatus(ctx context.Context, organizationId, pr err error ) - // Assuming 60 minutes is the max time backup completion takes, can change after discussion - const timeout = time.Minute * 60 + // Capella legacy bucket backup completion can take well over an hour under + // load. 90 minutes gives realistic headroom; previous 60-minute limit was + // being exhausted on busy clusters. + const timeout = 90 * time.Minute var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, timeout) defer cancel() - const sleep = time.Second * 1 + // Poll every 30 seconds instead of every 1 second. The previous 1-second + // cadence issued ~3600 GETs per backup wait and could itself contribute to + // backend pressure; 30 s still detects completion promptly. + const sleep = 30 * time.Second - timer := time.NewTimer(1 * time.Second) + timer := time.NewTimer(sleep) for { select { From 56937ab7d305a855037b1dd3452845fbad6072f2 Mon Sep 17 00:00:00 2001 From: Subhrajit Panigrahi Date: Tue, 12 May 2026 20:15:26 +0530 Subject: [PATCH 20/20] ci: retrigger CodeQL Default Setup (GitHub codeload.github.com 429)