Skip to content

Commit 88f04bd

Browse files
authored
feat: introduce generic Resource[C] pattern for GCP (#983)
This PR introduces a generic Resource[C] pattern to reduce boilerplate when implementing cloud resource types. Starting with GCP as a proof of concept before migrating AWS resources. ## Changes ### New `resource/` package - `Resource[C]` - Generic struct for all nukeable resources - `Scope` - Cloud provider-specific scope (region/projectID) - `SimpleBatchDeleter` - Concurrent deletion with semaphore - `SequentialDeleter` - Sequential deletion for rate-limited APIs - `MultiStepDeleter` - Multi-step deletion (e.g., empty then delete) ### GCP changes - `GcpResourceAdapter[C]` - Minimal adapter for GcpResource interface - `GcpResource.Nuke(ctx, identifiers)` - Context passed directly - `NewGCSBuckets()` - Constructor using generic pattern - Extracted `nukeResource()` helper for proper defer scope - Added `IsNukable` filtering before nuking - Uses `util.Split` instead of duplicate `splitIntoBatches` ### Improvements - `report.Record` in batch deleters for consistent reporting - `errors.Is()` and `%w` for proper error handling - Type aliases in `gcp/types.go` to avoid import cycles - Comprehensive tests in `resource/resource_test.go`
1 parent b0d255a commit 88f04bd

File tree

13 files changed

+884
-439
lines changed

13 files changed

+884
-439
lines changed

commands/gcp_commands.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ func gcpNukeHelper(c *cli.Context, configObj config.Config, projectID string, ou
117117

118118
// Execute the nuke operation if confirmed
119119
if shouldProceed {
120-
gcp.NukeAllResources(account, nil)
120+
gcp.NukeAllResources(account, configObj, nil)
121121
ui.RenderRunReportWithFormat(outputFormat, outputFile)
122122
}
123123

gcp/gcp.go

Lines changed: 64 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ import (
1010
"github.com/gruntwork-io/cloud-nuke/gcp/resources"
1111
"github.com/gruntwork-io/cloud-nuke/logging"
1212
"github.com/gruntwork-io/cloud-nuke/telemetry"
13+
"github.com/gruntwork-io/cloud-nuke/util"
1314
commonTelemetry "github.com/gruntwork-io/go-commons/telemetry"
1415
"github.com/pterm/pterm"
1516
)
1617

17-
// GetAllResources - Lists all GCP resources that can be deleted.
18+
// GetAllResources lists all GCP resources that can be deleted.
1819
func GetAllResources(projectID string, configObj config.Config, excludeAfter time.Time, includeAfter time.Time) (*GcpProjectResources, error) {
1920
allResources := GcpProjectResources{
2021
Resources: map[string]GcpResources{},
@@ -34,15 +35,6 @@ func GetAllResources(projectID string, configObj config.Config, excludeAfter tim
3435
// Initialize the resource
3536
resourceType.Init(projectID)
3637

37-
// Get the resource config
38-
resourceConfig := resourceType.GetAndSetResourceConfig(configObj)
39-
40-
// Prepare context for the resource
41-
if err := resourceType.PrepareContext(context.Background(), resourceConfig); err != nil {
42-
logging.Debugf("Error preparing context for %s: %v", resourceType.ResourceName(), err)
43-
continue
44-
}
45-
4638
// Get all resource identifiers
4739
if _, err := resourceType.GetAndSetIdentifiers(context.Background(), configObj); err != nil {
4840
logging.Debugf("Error getting identifiers for %s: %v", resourceType.ResourceName(), err)
@@ -64,72 +56,87 @@ func GetAllResources(projectID string, configObj config.Config, excludeAfter tim
6456
return &allResources, nil
6557
}
6658

67-
// NukeAllResources - Nukes all GCP resources
68-
func NukeAllResources(account *GcpProjectResources, bar *pterm.ProgressbarPrinter) {
59+
// NukeAllResources nukes all GCP resources
60+
func NukeAllResources(account *GcpProjectResources, configObj config.Config, bar *pterm.ProgressbarPrinter) {
6961
resourcesInRegion := account.Resources["global"]
7062

7163
for _, gcpResource := range resourcesInRegion.Resources {
72-
length := len((*gcpResource).ResourceIdentifiers())
73-
74-
// Split API calls into batches
75-
logging.Debugf("Terminating %d gcpResource in batches", length)
76-
batches := splitIntoBatches((*gcpResource).ResourceIdentifiers(), (*gcpResource).MaxBatchSize())
77-
78-
for i := 0; i < len(batches); i++ {
79-
batch := batches[i]
80-
bar.UpdateTitle(fmt.Sprintf("Nuking batch of %d %s resource(s)",
81-
len(batch), (*gcpResource).ResourceName()))
82-
if err := (*gcpResource).Nuke(batch); err != nil {
83-
if strings.Contains(err.Error(), "QUOTA_EXCEEDED") {
84-
logging.Debug(
85-
"Quota exceeded. Waiting 1 minute before making new requests",
86-
)
87-
time.Sleep(1 * time.Minute)
88-
continue
89-
}
90-
91-
// Report to telemetry - aggregated metrics of failures per resources.
92-
telemetry.TrackEvent(commonTelemetry.EventContext{
93-
EventName: fmt.Sprintf("error:Nuke:%s", (*gcpResource).ResourceName()),
94-
}, map[string]interface{}{})
95-
}
64+
nukeResource(gcpResource, configObj, bar)
65+
}
66+
}
67+
68+
// nukeResource nukes a single GCP resource type
69+
func nukeResource(gcpResource *GcpResource, configObj config.Config, bar *pterm.ProgressbarPrinter) {
70+
// Filter to only nukable resources
71+
var nukableIdentifiers []string
72+
for _, id := range (*gcpResource).ResourceIdentifiers() {
73+
if nukable, reason := (*gcpResource).IsNukable(id); !nukable {
74+
logging.Debugf("[Skipping] %s %s because %v", (*gcpResource).ResourceName(), id, reason)
75+
continue
76+
}
77+
nukableIdentifiers = append(nukableIdentifiers, id)
78+
}
9679

97-
if i != len(batches)-1 {
98-
logging.Debug("Sleeping for 10 seconds before processing next batch...")
99-
time.Sleep(10 * time.Second)
80+
if len(nukableIdentifiers) == 0 {
81+
return
82+
}
83+
84+
// Create context with timeout from resource config
85+
ctx := context.Background()
86+
resourceConfig := (*gcpResource).GetAndSetResourceConfig(configObj)
87+
if resourceConfig.Timeout != "" {
88+
if duration, err := time.ParseDuration(resourceConfig.Timeout); err == nil {
89+
var cancel context.CancelFunc
90+
ctx, cancel = context.WithTimeout(ctx, duration)
91+
defer cancel()
92+
}
93+
}
94+
95+
// Split API calls into batches
96+
logging.Debugf("Terminating %d %s in batches", len(nukableIdentifiers), (*gcpResource).ResourceName())
97+
batches := util.Split(nukableIdentifiers, (*gcpResource).MaxBatchSize())
98+
99+
for i := 0; i < len(batches); i++ {
100+
batch := batches[i]
101+
bar.UpdateTitle(fmt.Sprintf("Nuking batch of %d %s resource(s)",
102+
len(batch), (*gcpResource).ResourceName()))
103+
if err := (*gcpResource).Nuke(ctx, batch); err != nil {
104+
if strings.Contains(err.Error(), "QUOTA_EXCEEDED") {
105+
logging.Debug(
106+
"Quota exceeded. Waiting 1 minute before making new requests",
107+
)
108+
time.Sleep(1 * time.Minute)
109+
continue
100110
}
101111

102-
// Update the spinner to show the current resource type being nuked
103-
bar.Add(len(batch))
112+
// Report to telemetry - aggregated metrics of failures per resources.
113+
telemetry.TrackEvent(commonTelemetry.EventContext{
114+
EventName: fmt.Sprintf("error:Nuke:%s", (*gcpResource).ResourceName()),
115+
}, map[string]interface{}{})
116+
}
117+
118+
if i != len(batches)-1 {
119+
logging.Debug("Sleeping for 10 seconds before processing next batch...")
120+
time.Sleep(10 * time.Second)
104121
}
122+
123+
// Update the spinner to show the current resource type being nuked
124+
bar.Add(len(batch))
105125
}
106126
}
107127

108128
// getAllResourceTypes - Returns all GCP resource types that can be deleted
109129
func getAllResourceTypes() []GcpResource {
110130
return []GcpResource{
111-
&resources.GCSBuckets{},
131+
resources.NewGCSBuckets(),
112132
}
113133
}
114134

115135
// ListResourceTypes - Returns list of resources which can be passed to --resource-type
116136
func ListResourceTypes() []string {
117137
resourceTypes := []string{}
118-
for _, resource := range getAllResourceTypes() {
119-
resourceTypes = append(resourceTypes, resource.ResourceName())
138+
for _, r := range getAllResourceTypes() {
139+
resourceTypes = append(resourceTypes, r.ResourceName())
120140
}
121141
return resourceTypes
122142
}
123-
124-
// splitIntoBatches - Splits a slice into batches
125-
func splitIntoBatches(slice []string, batchSize int) [][]string {
126-
var batches [][]string
127-
for i := 0; i < len(slice); i += batchSize {
128-
end := i + batchSize
129-
if end > len(slice) {
130-
end = len(slice)
131-
}
132-
batches = append(batches, slice[i:end])
133-
}
134-
return batches
135-
}

gcp/resources/adapter.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package resources
2+
3+
import (
4+
"context"
5+
6+
"github.com/gruntwork-io/cloud-nuke/resource"
7+
)
8+
9+
// GcpResourceAdapter wraps a generic Resource to satisfy the GcpResource interface.
10+
// It provides type-safe Init(string) for GCP's project ID initialization.
11+
type GcpResourceAdapter[C any] struct {
12+
*resource.Resource[C]
13+
}
14+
15+
// NewGcpResource creates a GcpResourceAdapter from a generic Resource.
16+
func NewGcpResource[C any](r *resource.Resource[C]) GcpResource {
17+
return &GcpResourceAdapter[C]{Resource: r}
18+
}
19+
20+
// Init initializes the resource with GCP project ID.
21+
func (g *GcpResourceAdapter[C]) Init(projectID string) {
22+
g.Resource.Init(projectID)
23+
}
24+
25+
// Nuke deletes the resources with the given identifiers.
26+
func (g *GcpResourceAdapter[C]) Nuke(ctx context.Context, identifiers []string) error {
27+
return g.Resource.Nuke(ctx, identifiers)
28+
}
29+
30+
// Compile-time interface satisfaction check
31+
var _ GcpResource = (*GcpResourceAdapter[any])(nil)

gcp/resources/base_resource.go

Lines changed: 0 additions & 97 deletions
This file was deleted.

0 commit comments

Comments
 (0)