|
| 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