Skip to content

Commit 1fd08da

Browse files
ScottSuarezanoopkverma-google
authored andcommitted
refactor sweepers to run as tests (GoogleCloudPlatform#12778)
1 parent 9192723 commit 1fd08da

File tree

4 files changed

+285
-8
lines changed

4 files changed

+285
-8
lines changed

mmv1/third_party/terraform/.teamcity/components/builds/build_steps.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ fun BuildSteps.downloadTerraformBinary() {
7979
fun BuildSteps.runSweepers(sweeperStepName: String) {
8080
step(ScriptBuildStep{
8181
name = sweeperStepName
82-
scriptContent = "go test -v \"%PACKAGE_PATH%\" -sweep=\"%SWEEPER_REGIONS%\" -sweep-allow-failures -sweep-run=\"%SWEEP_RUN%\" -timeout 30m"
82+
scriptContent = "go test -v \"%PACKAGE_PATH%\" -sweep=\"%SWEEPER_REGIONS%\" -sweep-allow-failures -sweep-run=\"%SWEEP_RUN%\" -timeout 30m -json"
8383
})
8484
}
8585

mmv1/third_party/terraform/sweeper/gcp_sweeper.go

+1-3
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import (
88
"runtime"
99
"strings"
1010

11-
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
12-
1311
"github.com/hashicorp/terraform-provider-google/google/envvar"
1412
"github.com/hashicorp/terraform-provider-google/google/tpgresource"
1513
transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport"
@@ -110,7 +108,7 @@ func AddTestSweepers(name string, sweeper func(region string) error) {
110108
hashedFilename := hex.EncodeToString(hash.Sum(nil))
111109
uniqueName := name + "_" + hashedFilename
112110

113-
resource.AddTestSweepers(uniqueName, &resource.Sweeper{
111+
addTestSweepers(uniqueName, &Sweeper{
114112
Name: name,
115113
F: sweeper,
116114
})

mmv1/third_party/terraform/sweeper/gcp_sweeper_test.go.tmpl

+9-4
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ package sweeper_test
44
import (
55
"testing"
66

7-
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
8-
97
{{- range $product := $.Products }}
108
_ "github.com/hashicorp/terraform-provider-google/google/services/{{ lower $product.Name }}"
119
{{- end }}
@@ -23,8 +21,15 @@ import (
2321
_ "github.com/hashicorp/terraform-provider-google/google/services/firebaserules"
2422
_ "github.com/hashicorp/terraform-provider-google/google/services/networkconnectivity"
2523
_ "github.com/hashicorp/terraform-provider-google/google/services/recaptchaenterprise"
24+
25+
// TODO: remove dependency on hashicorp flags
26+
// need to blank import hashicorp sweeper code to maintain the flags declared in their package
27+
_ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
28+
29+
"github.com/hashicorp/terraform-provider-google/google/sweeper"
2630
)
2731

28-
func TestMain(m *testing.M) {
29-
resource.TestMain(m)
32+
func TestSweepers(t *testing.T) {
33+
sweeper.ExecuteSweepers(t)
3034
}
35+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
package sweeper
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"log"
7+
"os"
8+
"strings"
9+
"testing"
10+
"time"
11+
)
12+
13+
// flagSweep is a flag available when running tests on the command line. It
14+
// contains a comma separated list of regions to for the sweeper functions to
15+
// run in. This flag bypasses the normal Test path and instead runs functions designed to
16+
// clean up any leaked resources a testing environment could have created. It is
17+
// a best effort attempt, and relies on Provider authors to implement "Sweeper"
18+
// methods for resources.
19+
20+
// Adding Sweeper methods with AddTestSweepers will
21+
// construct a list of sweeper funcs to be called here. We iterate through
22+
// regions provided by the sweep flag, and for each region we iterate through the
23+
// tests, and exit on any errors. At time of writing, sweepers are ran
24+
// sequentially, however they can list dependencies to be ran first. We track
25+
// the sweepers that have been ran, so as to not run a sweeper twice for a given
26+
// region.
27+
//
28+
// WARNING:
29+
// Sweepers are designed to be destructive. You should not use the -sweep flag
30+
// in any environment that is not strictly a test environment. Resources will be
31+
// destroyed.
32+
33+
var (
34+
flagSweep *string
35+
flagSweepAllowFailures *bool
36+
flagSweepRun *string
37+
sweeperFuncs map[string]*Sweeper
38+
)
39+
40+
// SweeperFunc is a signature for a function that acts as a sweeper. It
41+
// accepts a string for the region that the sweeper is to be ran in. This
42+
// function must be able to construct a valid client for that region.
43+
type SweeperFunc func(r string) error
44+
45+
type Sweeper struct {
46+
// Name for sweeper. Must be unique to be ran by the Sweeper Runner
47+
Name string
48+
49+
// Dependencies list the const names of other Sweeper functions that must be ran
50+
// prior to running this Sweeper. This is an ordered list that will be invoked
51+
// recursively at the helper/resource level
52+
Dependencies []string
53+
54+
// Sweeper function that when invoked sweeps the Provider of specific
55+
// resources
56+
F SweeperFunc
57+
}
58+
59+
func init() {
60+
sweeperFuncs = make(map[string]*Sweeper)
61+
}
62+
63+
// registerFlags checks for and gets existing flag definitions before trying to redefine them.
64+
// This is needed because this package and terraform-plugin-testing both define the same sweep flags.
65+
// By checking first, we ensure we reuse any existing flags rather than causing a panic from flag redefinition.
66+
// This allows this module to be used alongside terraform-plugin-testing without conflicts.
67+
func registerFlags() {
68+
// Check for existing flags in global CommandLine
69+
if f := flag.Lookup("sweep"); f != nil {
70+
// Use the Value.Get() interface to get the values
71+
if getter, ok := f.Value.(flag.Getter); ok {
72+
vs := getter.Get().(string)
73+
flagSweep = &vs
74+
}
75+
if f := flag.Lookup("sweep-allow-failures"); f != nil {
76+
if getter, ok := f.Value.(flag.Getter); ok {
77+
vb := getter.Get().(bool)
78+
flagSweepAllowFailures = &vb
79+
}
80+
}
81+
if f := flag.Lookup("sweep-run"); f != nil {
82+
if getter, ok := f.Value.(flag.Getter); ok {
83+
vs := getter.Get().(string)
84+
flagSweepRun = &vs
85+
}
86+
}
87+
} else {
88+
// Define our flags if they don't exist
89+
fsDefault := ""
90+
fsafDefault := true
91+
fsrDefault := ""
92+
flagSweep = &fsDefault
93+
flagSweepAllowFailures = &fsafDefault
94+
flagSweepRun = &fsrDefault
95+
}
96+
}
97+
98+
// AddTestSweepers function adds a given name and Sweeper configuration
99+
// pair to the internal sweeperFuncs map. Invoke this function to register a
100+
// resource sweeper to be available for running when the -sweep flag is used
101+
// with `go test`. Sweeper names must be unique to help ensure a given sweeper
102+
// is only ran once per run.
103+
func addTestSweepers(name string, s *Sweeper) {
104+
if _, ok := sweeperFuncs[name]; ok {
105+
log.Fatalf("[ERR] Error adding (%s) to sweeperFuncs: function already exists in map", name)
106+
}
107+
108+
sweeperFuncs[name] = s
109+
}
110+
111+
// ExecuteSweepers
112+
//
113+
// Sweepers enable infrastructure cleanup functions to be included with
114+
// resource definitions, typically so developers can remove all resources of
115+
// that resource type from testing infrastructure in case of failures that
116+
// prevented the normal resource destruction behavior of acceptance tests.
117+
// Use the AddTestSweepers() function to configure available sweepers.
118+
//
119+
// Sweeper flags added to the "go test" command:
120+
//
121+
// -sweep: Comma-separated list of locations/regions to run available sweepers.
122+
// -sweep-allow-failues: Enable to allow other sweepers to run after failures.
123+
// -sweep-run: Comma-separated list of resource type sweepers to run. Defaults
124+
// to all sweepers.
125+
//
126+
// Refer to the Env prefixed constants for environment variables that further
127+
// control testing functionality.
128+
func ExecuteSweepers(t *testing.T) {
129+
registerFlags()
130+
flag.Parse()
131+
if *flagSweep != "" {
132+
// parse flagSweep contents for regions to run
133+
regions := strings.Split(*flagSweep, ",")
134+
135+
// get filtered list of sweepers to run based on sweep-run flag
136+
sweepers := filterSweepers(*flagSweepRun, sweeperFuncs)
137+
138+
if err := runSweepers(t, regions, sweepers, *flagSweepAllowFailures); err != nil {
139+
os.Exit(1)
140+
}
141+
} else {
142+
t.Skip("skipping sweeper run. No region supplied")
143+
}
144+
}
145+
146+
func runSweepers(t *testing.T, regions []string, sweepers map[string]*Sweeper, allowFailures bool) error {
147+
// Sort sweepers by dependency order
148+
sorted, err := validateAndOrderSweepers(sweepers)
149+
if err != nil {
150+
return fmt.Errorf("failed to sort sweepers: %v", err)
151+
}
152+
153+
// Run each sweeper in dependency order
154+
for _, sweeper := range sorted {
155+
sweeper := sweeper // capture for closure
156+
t.Run(sweeper.Name, func(t *testing.T) {
157+
for _, region := range regions {
158+
region := strings.TrimSpace(region)
159+
log.Printf("[DEBUG] Running Sweeper (%s) in region (%s)", sweeper.Name, region)
160+
161+
start := time.Now()
162+
err := sweeper.F(region)
163+
elapsed := time.Since(start)
164+
165+
log.Printf("[DEBUG] Completed Sweeper (%s) in region (%s) in %s", sweeper.Name, region, elapsed)
166+
167+
if err != nil {
168+
log.Printf("[ERROR] Error running Sweeper (%s) in region (%s): %s", sweeper.Name, region, err)
169+
if allowFailures {
170+
t.Errorf("failed in region %s: %s", region, err)
171+
} else {
172+
t.Fatalf("failed in region %s: %s", region, err)
173+
}
174+
}
175+
}
176+
})
177+
}
178+
179+
return nil
180+
}
181+
182+
// filterSweepers takes a comma separated string listing the names of sweepers
183+
// to be ran, and returns a filtered set from the list of all of sweepers to
184+
// run based on the names given.
185+
func filterSweepers(f string, source map[string]*Sweeper) map[string]*Sweeper {
186+
filterSlice := strings.Split(strings.ToLower(f), ",")
187+
if len(filterSlice) == 1 && filterSlice[0] == "" {
188+
// if the filter slice is a single element of "" then no sweeper list was
189+
// given, so just return the full list
190+
return source
191+
}
192+
193+
sweepers := make(map[string]*Sweeper)
194+
for name := range source {
195+
for _, s := range filterSlice {
196+
if strings.Contains(strings.ToLower(name), s) {
197+
for foundName, foundSweeper := range filterSweeperWithDependencies(name, source) {
198+
sweepers[foundName] = foundSweeper
199+
}
200+
}
201+
}
202+
}
203+
return sweepers
204+
}
205+
206+
// filterSweeperWithDependencies recursively returns sweeper and all dependencies.
207+
// Since filterSweepers performs fuzzy matching, this function is used
208+
// to perform exact sweeper and dependency lookup.
209+
func filterSweeperWithDependencies(name string, source map[string]*Sweeper) map[string]*Sweeper {
210+
result := make(map[string]*Sweeper)
211+
212+
currentSweeper, ok := source[name]
213+
if !ok {
214+
log.Printf("[WARN] Sweeper has dependency (%s), but that sweeper was not found", name)
215+
return result
216+
}
217+
218+
result[name] = currentSweeper
219+
220+
for _, dependency := range currentSweeper.Dependencies {
221+
for foundName, foundSweeper := range filterSweeperWithDependencies(dependency, source) {
222+
result[foundName] = foundSweeper
223+
}
224+
}
225+
226+
return result
227+
}
228+
229+
// validateAndOrderSweepers performs topological sort on sweepers based on their dependencies.
230+
// It ensures there are no cycles in the dependency graph and all referenced dependencies exist.
231+
// Returns an ordered list of sweepers where each sweeper appears after its dependencies.
232+
// Returns error if there are any cycles or missing dependencies.
233+
func validateAndOrderSweepers(sweepers map[string]*Sweeper) ([]*Sweeper, error) {
234+
// Detect cycles and get sorted list
235+
visited := make(map[string]bool)
236+
inPath := make(map[string]bool)
237+
sorted := make([]*Sweeper, 0, len(sweepers))
238+
239+
var visit func(name string) error
240+
visit = func(name string) error {
241+
if inPath[name] {
242+
return fmt.Errorf("dependency cycle detected: %s", name)
243+
}
244+
if visited[name] {
245+
return nil
246+
}
247+
248+
inPath[name] = true
249+
sweeper := sweepers[name]
250+
for _, dep := range sweeper.Dependencies {
251+
if _, exists := sweepers[dep]; !exists {
252+
return fmt.Errorf("sweeper %s depends on %s, but %s not found", name, dep, dep)
253+
}
254+
if err := visit(dep); err != nil {
255+
return err
256+
}
257+
}
258+
inPath[name] = false
259+
visited[name] = true
260+
sorted = append(sorted, sweeper)
261+
return nil
262+
}
263+
264+
// Visit all sweepers
265+
for name := range sweepers {
266+
if !visited[name] {
267+
if err := visit(name); err != nil {
268+
return nil, err
269+
}
270+
}
271+
}
272+
273+
return sorted, nil
274+
}

0 commit comments

Comments
 (0)