Skip to content

Commit 3942049

Browse files
ScottSuarezDawid212
authored andcommitted
implement sweeper dependencies and parent relationships (GoogleCloudPlatform#13396)
1 parent 7cc10a6 commit 3942049

File tree

72 files changed

+1489
-601
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+1489
-601
lines changed

mmv1/api/resource.go

+5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222

2323
"github.com/GoogleCloudPlatform/magic-modules/mmv1/api/product"
2424
"github.com/GoogleCloudPlatform/magic-modules/mmv1/api/resource"
25+
"github.com/GoogleCloudPlatform/magic-modules/mmv1/api/utils"
2526
"github.com/GoogleCloudPlatform/magic-modules/mmv1/google"
2627
"golang.org/x/exp/slices"
2728
)
@@ -1841,6 +1842,10 @@ func urlContainsOnlyAllowedKeys(templateURL string, allowedKeys []string) bool {
18411842
}
18421843

18431844
func (r Resource) ShouldGenerateSweepers() bool {
1845+
if !r.ExcludeSweeper && !utils.IsEmpty(r.Sweeper) {
1846+
return true
1847+
}
1848+
18441849
allowedKeys := []string{"project", "region", "location", "zone", "billing_account"}
18451850
if !urlContainsOnlyAllowedKeys(r.ListUrlTemplate(), allowedKeys) {
18461851
return false

mmv1/api/resource/sweeper.go

+52-11
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,59 @@
1313

1414
package resource
1515

16-
// Sweeper provides configuration for the test sweeper
16+
// Sweeper provides configuration for the test sweeper to clean up test resources
1717
type Sweeper struct {
18-
// The field checked by sweeper to determine
19-
// eligibility for deletion for generated resources
20-
IdentifierField string `yaml:"identifier_field"`
21-
Regions []string `yaml:"regions,omitempty"`
22-
Prefixes []string `yaml:"prefixes,omitempty"`
23-
URLSubstitutions []URLSubstitution `yaml:"url_substitutions,omitempty"`
18+
// IdentifierField specifies which field in the resource object should be used
19+
// to identify resources for deletion (typically "name" or "id")
20+
IdentifierField string `yaml:"identifier_field"`
21+
22+
// Regions defines which regions to run the sweeper in
23+
// If empty, defaults to just us-central1
24+
Regions []string `yaml:"regions,omitempty"`
25+
26+
// Prefixes specifies name prefixes that identify resources eligible for sweeping
27+
// Resources whose names start with any of these prefixes will be deleted
28+
Prefixes []string `yaml:"prefixes,omitempty"`
29+
30+
// URLSubstitutions allows customizing URL parameters when listing resources
31+
// Each map entry represents a set of key-value pairs to substitute in the URL template
32+
URLSubstitutions []map[string]interface{} `yaml:"url_substitutions,omitempty"`
33+
34+
// Dependencies lists other resource types that must be swept before this one
35+
Dependencies []string `yaml:"dependencies,omitempty"`
36+
37+
// Parent defines the parent-child relationship for hierarchical resources
38+
// When specified, the sweeper will first collect parent resources before listing child resources
39+
Parent *ParentResource `yaml:"parent,omitempty"`
2440
}
2541

26-
// URLSubstitution represents a region-zone pair for URL substitution
27-
type URLSubstitution struct {
28-
Region string `yaml:"region,omitempty"`
29-
Zone string `yaml:"zone,omitempty"`
42+
// ParentResource specifies how to handle parent-child resource dependencies
43+
type ParentResource struct {
44+
// ResourceType is the parent resource type that will be used to find the parent sweeper
45+
// Example: "GoogleContainerCluster"
46+
ResourceType string `yaml:"resource_type"`
47+
48+
// ParentField specifies which field to extract from the parent resource
49+
// Example: "name" or "id"
50+
// Required unless Template is provided
51+
ParentField string `yaml:"parent_field"`
52+
53+
// ParentFieldRegex is a regex pattern to apply to the parent field value
54+
// The first capture group will be used as the final value
55+
ParentFieldRegex string `yaml:"parent_field_regex"`
56+
57+
// ParentFieldExtractName when true indicates the parent field contains a self-link
58+
// and only the resource name (portion after the last slash) should be used
59+
ParentFieldExtractName bool `yaml:"parent_field_extract_name"`
60+
61+
// ChildField is the field in the child resource that needs to reference the parent
62+
// Example: "cluster", "instance", etc.
63+
ChildField string `yaml:"child_field"`
64+
65+
// Template provides a format string to construct the parent reference
66+
// Variables in {{brackets}} will be replaced with values from the parent resource
67+
// The special placeholder {{value}} is populated with the processed parent field value
68+
// Example: "projects/{{project}}/locations/{{location}}/clusters/{{value}}"
69+
// If specified, takes precedence over direct field mapping
70+
Template string `yaml:"template"`
3071
}

mmv1/api/utils/utils.go

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package utils
2+
3+
import "reflect"
4+
5+
// IsEmpty checks if a value is meaningfully empty in a recursive way
6+
func IsEmpty(v interface{}) bool {
7+
if v == nil {
8+
return true
9+
}
10+
11+
val := reflect.ValueOf(v)
12+
13+
// Handle pointers
14+
if val.Kind() == reflect.Ptr {
15+
if val.IsNil() {
16+
return true
17+
}
18+
return IsEmpty(val.Elem().Interface())
19+
}
20+
21+
// Handle different types
22+
switch val.Kind() {
23+
case reflect.Struct:
24+
// Check if all fields are empty
25+
allEmpty := true
26+
for i := 0; i < val.NumField(); i++ {
27+
field := val.Field(i)
28+
if field.CanInterface() && !IsEmpty(field.Interface()) {
29+
allEmpty = false
30+
break
31+
}
32+
}
33+
return allEmpty
34+
35+
case reflect.Map:
36+
if val.Len() == 0 {
37+
return true
38+
}
39+
// Check if all map values are empty
40+
allEmpty := true
41+
iter := val.MapRange()
42+
for iter.Next() {
43+
if !IsEmpty(iter.Value().Interface()) {
44+
allEmpty = false
45+
break
46+
}
47+
}
48+
return allEmpty
49+
50+
case reflect.Slice, reflect.Array:
51+
if val.Len() == 0 {
52+
return true
53+
}
54+
// Check if all elements are empty
55+
allEmpty := true
56+
for i := 0; i < val.Len(); i++ {
57+
if !IsEmpty(val.Index(i).Interface()) {
58+
allEmpty = false
59+
break
60+
}
61+
}
62+
return allEmpty
63+
64+
case reflect.Chan, reflect.Func, reflect.UnsafePointer:
65+
return val.IsNil()
66+
67+
default:
68+
// For simple types (int, string, etc.), check if it's a zero value
69+
return val.IsZero()
70+
}
71+
}

mmv1/api/utils/utils_test.go

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package utils
2+
3+
import (
4+
"testing"
5+
)
6+
7+
type SimpleStruct struct {
8+
Name string
9+
Value int
10+
}
11+
12+
type NestedStruct struct {
13+
Simple *SimpleStruct
14+
Values []int
15+
Map map[string]interface{}
16+
}
17+
18+
type ComplexStruct struct {
19+
Nested *NestedStruct
20+
StructMap map[string]*SimpleStruct
21+
StructArr []*SimpleStruct
22+
}
23+
24+
func TestIsEmpty(t *testing.T) {
25+
tests := []struct {
26+
name string
27+
value interface{}
28+
expected bool
29+
}{
30+
// Nil values
31+
{"nil", nil, true},
32+
{"nil pointer", (*SimpleStruct)(nil), true},
33+
34+
// Basic types
35+
{"empty string", "", true},
36+
{"non-empty string", "test", false},
37+
{"zero int", 0, true},
38+
{"non-zero int", 42, false},
39+
{"false bool", false, true},
40+
{"true bool", true, false},
41+
42+
// Slices and arrays
43+
{"empty slice", []int{}, true},
44+
{"nil slice", []int(nil), true},
45+
{"non-empty slice", []int{1, 2, 3}, false},
46+
{"slice with zero values", []int{0, 0, 0}, true},
47+
{"slice with mixed values", []int{0, 1, 0}, false},
48+
49+
// Maps
50+
{"empty map", map[string]int{}, true},
51+
{"nil map", map[string]int(nil), true},
52+
{"map with values", map[string]int{"one": 1, "two": 2}, false},
53+
{"map with zero values", map[string]int{"one": 0, "two": 0}, true},
54+
{"map with mixed values", map[string]int{"one": 0, "two": 2}, false},
55+
56+
// Simple struct
57+
{"empty struct", SimpleStruct{}, true},
58+
{"partially filled struct", SimpleStruct{Name: "test"}, false},
59+
{"fully filled struct", SimpleStruct{Name: "test", Value: 42}, false},
60+
61+
// Pointers to simple structs
62+
{"pointer to empty struct", &SimpleStruct{}, true},
63+
{"pointer to partially filled struct", &SimpleStruct{Name: "test"}, false},
64+
65+
// Nested structs with nil fields
66+
{"nested struct with all nil", NestedStruct{}, true},
67+
{"nested struct with simple", NestedStruct{Simple: &SimpleStruct{Name: "test"}}, false},
68+
{"nested struct with empty array", NestedStruct{Values: []int{}}, true},
69+
{"nested struct with non-empty array", NestedStruct{Values: []int{1, 2}}, false},
70+
71+
// Complex nested scenarios
72+
{
73+
"complex all empty",
74+
&ComplexStruct{
75+
Nested: &NestedStruct{
76+
Simple: &SimpleStruct{},
77+
Values: []int{},
78+
Map: map[string]interface{}{},
79+
},
80+
StructMap: map[string]*SimpleStruct{},
81+
StructArr: []*SimpleStruct{},
82+
},
83+
true,
84+
},
85+
{
86+
"complex with one non-empty value",
87+
&ComplexStruct{
88+
Nested: &NestedStruct{
89+
Simple: &SimpleStruct{Name: "test"},
90+
Values: []int{},
91+
Map: map[string]interface{}{},
92+
},
93+
StructMap: map[string]*SimpleStruct{},
94+
StructArr: []*SimpleStruct{},
95+
},
96+
false,
97+
},
98+
{
99+
"complex with non-empty map",
100+
&ComplexStruct{
101+
Nested: &NestedStruct{
102+
Simple: &SimpleStruct{},
103+
Values: []int{},
104+
Map: map[string]interface{}{"key": "value"},
105+
},
106+
StructMap: map[string]*SimpleStruct{},
107+
StructArr: []*SimpleStruct{},
108+
},
109+
false,
110+
},
111+
}
112+
113+
for _, test := range tests {
114+
t.Run(test.name, func(t *testing.T) {
115+
result := IsEmpty(test.value)
116+
if result != test.expected {
117+
t.Errorf("IsEmpty(%v) = %v, expected %v", test.value, result, test.expected)
118+
}
119+
})
120+
}
121+
}

mmv1/products/apigateway/ApiConfig.yaml

+6-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ self_link: 'projects/{{project}}/locations/global/apis/{{api}}/configs/{{api_con
2929
create_url: 'projects/{{project}}/locations/global/apis/{{api}}/configs?apiConfigId={{api_config_id}}'
3030
update_verb: 'PATCH'
3131
update_mask: true
32-
3332
read_query_params: '?view=FULL'
3433
timeouts:
3534
insert_minutes: 20
@@ -60,6 +59,12 @@ iam_policy:
6059
custom_code:
6160
extra_schema_entry: 'templates/terraform/extra_schema_entry/api_config.tmpl'
6261
encoder: 'templates/terraform/encoders/api_config.go.tmpl'
62+
sweeper:
63+
parent:
64+
resource_type: "google_api_gateway_api"
65+
parent_field: "name"
66+
parent_field_extract_name: true
67+
child_field: "api"
6368
examples:
6469
- name: 'apigateway_api_config_basic'
6570
primary_resource_id: 'api_cfg'

mmv1/products/compute/ForwardingRule.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ custom_diff:
4848
- 'forwardingRuleCustomizeDiff'
4949
legacy_long_form_project: true
5050
sweeper:
51+
dependencies:
52+
- "google_network_connectivity_service_connection_policy"
5153
url_substitutions:
5254
- region: "us-west2"
5355
- region: "us-central1"

mmv1/products/compute/HealthCheck.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ custom_code:
5454
encoder: 'templates/terraform/encoders/health_check_type.tmpl'
5555
custom_diff:
5656
- 'healthCheckCustomizeDiff'
57+
sweeper:
58+
dependencies:
59+
- "google_compute_subnetwork"
5760
examples:
5861
- name: 'health_check_tcp'
5962
primary_resource_id: 'tcp-health-check'

mmv1/products/compute/InstantSnapshot.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ iam_policy:
4949
allowed_iam_role: 'roles/compute.storageAdmin'
5050
import_format:
5151
- 'projects/{{project}}/zones/{{zone}}/instantSnapshots/{{name}}'
52+
sweeper:
53+
url_substitutions:
54+
- zone: "us-central1-a"
5255
examples:
5356
- name: 'instant_snapshot_basic'
5457
primary_resource_id: 'default'

mmv1/products/compute/Subnetwork.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ custom_diff:
7171
- 'customdiff.ForceNewIfChange("ip_cidr_range", IsShrinkageIpCidr)'
7272
- 'sendSecondaryIpRangeIfEmptyDiff'
7373
sweeper:
74+
dependencies:
75+
- "google_compute_forwarding_rule"
7476
url_substitutions:
7577
- region: "us-west2"
7678
- region: "us-central1"

mmv1/products/discoveryengine/ChatEngine.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ async:
4444
resource_inside_response: true
4545
custom_code:
4646
encoder: 'templates/terraform/encoders/discovery_engine_chat_engine_hardcode_solution_type.go.tmpl'
47+
sweeper:
48+
url_substitutions:
49+
- collection_id: default_collection
50+
region: global
51+
- collection_id: default_collection
52+
region: eu
4753
examples:
4854
- name: 'discoveryengine_chat_engine_basic'
4955
primary_resource_id: 'primary'

mmv1/products/discoveryengine/DataStore.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ async:
4444
resource_inside_response: false
4545
custom_code:
4646
sweeper:
47+
dependencies:
48+
- 'google_discovery_engine_chat_engine'
4749
url_substitutions:
4850
- region: "eu"
4951
- region: "global"

mmv1/products/networkconnectivity/ServiceConnectionPolicy.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ async:
4646
custom_code:
4747
update_encoder: 'templates/terraform/encoders/service_connection_policy.go.tmpl'
4848
sweeper:
49+
prefixes:
50+
- "gcp-memorystore"
4951
url_substitutions:
5052
- region: "us-central1"
5153
- region: "us-east1"

mmv1/products/networkservices/GrpcRoute.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ async:
4545
custom_code:
4646
schema_version: 1
4747
state_upgraders: true
48+
sweeper:
49+
url_substitutions:
50+
- region: "global"
4851
examples:
4952
- name: 'network_services_grpc_route_basic'
5053
primary_resource_id: 'default'

mmv1/products/networkservices/Mesh.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ async:
4747
custom_code:
4848
schema_version: 1
4949
state_upgraders: true
50+
sweeper:
51+
url_substitutions:
52+
- region: "global"
5053
examples:
5154
- name: 'network_services_mesh_basic'
5255
primary_resource_id: 'default'

0 commit comments

Comments
 (0)