Skip to content

Commit de55c0f

Browse files
refactor sweepers to run as tests (#12778) (#20945)
[upstream:0196090f4322a1daa1a2bff283e2dfb702f3e801] Signed-off-by: Modular Magician <[email protected]>
1 parent 49ba38d commit de55c0f

File tree

5 files changed

+290
-9
lines changed

5 files changed

+290
-9
lines changed

.changelog/12778.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:none
2+
3+
```

.teamcity/components/builds/build_steps.kt

Lines changed: 1 addition & 1 deletion
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

google/sweeper/gcp_sweeper.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import (
1010
"runtime"
1111
"strings"
1212

13-
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
14-
1513
"github.com/hashicorp/terraform-provider-google/google/envvar"
1614
"github.com/hashicorp/terraform-provider-google/google/tpgresource"
1715
transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport"
@@ -112,7 +110,7 @@ func AddTestSweepers(name string, sweeper func(region string) error) {
112110
hashedFilename := hex.EncodeToString(hash.Sum(nil))
113111
uniqueName := name + "_" + hashedFilename
114112

115-
resource.AddTestSweepers(uniqueName, &resource.Sweeper{
113+
addTestSweepers(uniqueName, &Sweeper{
116114
Name: name,
117115
F: sweeper,
118116
})

google/sweeper/gcp_sweeper_test.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@
33
package sweeper_test
44

55
import (
6-
"testing"
7-
8-
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
96
_ "github.com/hashicorp/terraform-provider-google/google/services/accessapproval"
107
_ "github.com/hashicorp/terraform-provider-google/google/services/accesscontextmanager"
118
_ "github.com/hashicorp/terraform-provider-google/google/services/activedirectory"
@@ -138,6 +135,7 @@ import (
138135
_ "github.com/hashicorp/terraform-provider-google/google/services/vpcaccess"
139136
_ "github.com/hashicorp/terraform-provider-google/google/services/workbench"
140137
_ "github.com/hashicorp/terraform-provider-google/google/services/workflows"
138+
"testing"
141139

142140
// Manually add the services for DCL resource and handwritten resource sweepers if they are not in the above list
143141
_ "github.com/hashicorp/terraform-provider-google/google/services/apikeys"
@@ -152,8 +150,14 @@ import (
152150
_ "github.com/hashicorp/terraform-provider-google/google/services/firebaserules"
153151
_ "github.com/hashicorp/terraform-provider-google/google/services/networkconnectivity"
154152
_ "github.com/hashicorp/terraform-provider-google/google/services/recaptchaenterprise"
153+
154+
// TODO: remove dependency on hashicorp flags
155+
// need to blank import hashicorp sweeper code to maintain the flags declared in their package
156+
_ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
157+
158+
"github.com/hashicorp/terraform-provider-google/google/sweeper"
155159
)
156160

157-
func TestMain(m *testing.M) {
158-
resource.TestMain(m)
161+
func TestSweepers(t *testing.T) {
162+
sweeper.ExecuteSweepers(t)
159163
}

google/sweeper/hashi_sweeper_fork.go

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

0 commit comments

Comments
 (0)