Skip to content

Commit 0ac1ade

Browse files
committed
fix(resources): purge PostgresFlex instances in one nuke run
DeleteInstance is a soft delete; the instance stays listed as Deleted, so libnuke waited on it forever. Remove now soft-deletes, polls until the instance reaches Deleted, then force-deletes to fully purge.
1 parent 609321b commit 0ac1ade

1 file changed

Lines changed: 43 additions & 6 deletions

File tree

resources/postgresflex-instance.go

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package resources
33
import (
44
"context"
55
"fmt"
6+
"time"
67

78
"github.com/sirupsen/logrus"
89

@@ -32,13 +33,15 @@ func init() {
3233
//
3334
// Endpoints used:
3435
// - GET /v2/projects/{projectId}/regions/{region}/instances
36+
// - GET /v2/projects/{projectId}/regions/{region}/instances/{instanceId}
3537
// - DELETE /v2/projects/{projectId}/regions/{region}/instances/{instanceId}
3638
// - DELETE /v2/projects/{projectId}/regions/{region}/instances/{instanceId}/force
3739
//
3840
// Deleting an instance is a soft delete: it stays listed with status
39-
// "Deleted" during the retention window. Remove therefore force-deletes
40-
// instances already in that state so a nuke run converges instead of
41-
// re-deleting the same instance forever.
41+
// "Deleted" during the retention window, so libnuke would wait on it
42+
// forever (Remove is only invoked once per item per run). Remove therefore
43+
// soft-deletes, polls until the instance reaches "Deleted", then
44+
// force-deletes to purge it within the same run.
4245
type PostgresFlexInstance struct {
4346
*BaseResource `property:",inline"`
4447

@@ -49,14 +52,48 @@ type PostgresFlexInstance struct {
4952
Status string
5053
}
5154

55+
const postgresFlexStatusDeleted = "Deleted"
56+
5257
func (r *PostgresFlexInstance) Remove(ctx context.Context) error {
5358
if r.api == nil {
5459
return fmt.Errorf("PostgresFlexInstance.Remove: api client not set")
5560
}
56-
if r.Status == "Deleted" {
57-
return r.api.ForceDeleteInstance(ctx, r.ProjectID, r.Region, r.ID).Execute()
61+
if r.Status != postgresFlexStatusDeleted {
62+
if err := r.api.DeleteInstance(ctx, r.ProjectID, r.Region, r.ID).Execute(); err != nil {
63+
return fmt.Errorf("delete instance: %w", err)
64+
}
65+
if err := r.waitUntilDeleted(ctx); err != nil {
66+
return err
67+
}
68+
}
69+
if err := r.api.ForceDeleteInstance(ctx, r.ProjectID, r.Region, r.ID).Execute(); err != nil {
70+
return fmt.Errorf("force delete instance: %w", err)
71+
}
72+
return nil
73+
}
74+
75+
// waitUntilDeleted polls the instance until the soft delete has landed
76+
// (status "Deleted"); force delete is rejected before that.
77+
func (r *PostgresFlexInstance) waitUntilDeleted(ctx context.Context) error {
78+
ctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
79+
defer cancel()
80+
ticker := time.NewTicker(5 * time.Second)
81+
defer ticker.Stop()
82+
83+
for {
84+
resp, err := r.api.GetInstance(ctx, r.ProjectID, r.Region, r.ID).Execute()
85+
if err != nil {
86+
return fmt.Errorf("get instance while waiting for soft delete: %w", err)
87+
}
88+
if item, ok := resp.GetItemOk(); ok && item.GetStatus() == postgresFlexStatusDeleted {
89+
return nil
90+
}
91+
select {
92+
case <-ctx.Done():
93+
return fmt.Errorf("timed out waiting for instance %s to reach %s state: %w", r.ID, postgresFlexStatusDeleted, ctx.Err())
94+
case <-ticker.C:
95+
}
5896
}
59-
return r.api.DeleteInstance(ctx, r.ProjectID, r.Region, r.ID).Execute()
6097
}
6198

6299
func (r *PostgresFlexInstance) Properties() types.Properties { return PropsFromStruct(r) }

0 commit comments

Comments
 (0)