Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor sweepers to run as tests #20945

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/12778.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:none

```
2 changes: 1 addition & 1 deletion .teamcity/components/builds/build_steps.kt
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ fun BuildSteps.downloadTerraformBinary() {
fun BuildSteps.runSweepers(sweeperStepName: String) {
step(ScriptBuildStep{
name = sweeperStepName
scriptContent = "go test -v \"%PACKAGE_PATH%\" -sweep=\"%SWEEPER_REGIONS%\" -sweep-allow-failures -sweep-run=\"%SWEEP_RUN%\" -timeout 30m"
scriptContent = "go test -v \"%PACKAGE_PATH%\" -sweep=\"%SWEEPER_REGIONS%\" -sweep-allow-failures -sweep-run=\"%SWEEP_RUN%\" -timeout 30m -json"
})
}

Expand Down
4 changes: 1 addition & 3 deletions google/sweeper/gcp_sweeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import (
"runtime"
"strings"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"

"github.com/hashicorp/terraform-provider-google/google/envvar"
"github.com/hashicorp/terraform-provider-google/google/tpgresource"
transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport"
Expand Down Expand Up @@ -112,7 +110,7 @@ func AddTestSweepers(name string, sweeper func(region string) error) {
hashedFilename := hex.EncodeToString(hash.Sum(nil))
uniqueName := name + "_" + hashedFilename

resource.AddTestSweepers(uniqueName, &resource.Sweeper{
addTestSweepers(uniqueName, &Sweeper{
Name: name,
F: sweeper,
})
Expand Down
14 changes: 9 additions & 5 deletions google/sweeper/gcp_sweeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
package sweeper_test

import (
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
_ "github.com/hashicorp/terraform-provider-google/google/services/accessapproval"
_ "github.com/hashicorp/terraform-provider-google/google/services/accesscontextmanager"
_ "github.com/hashicorp/terraform-provider-google/google/services/activedirectory"
Expand Down Expand Up @@ -138,6 +135,7 @@ import (
_ "github.com/hashicorp/terraform-provider-google/google/services/vpcaccess"
_ "github.com/hashicorp/terraform-provider-google/google/services/workbench"
_ "github.com/hashicorp/terraform-provider-google/google/services/workflows"
"testing"

// Manually add the services for DCL resource and handwritten resource sweepers if they are not in the above list
_ "github.com/hashicorp/terraform-provider-google/google/services/apikeys"
Expand All @@ -152,8 +150,14 @@ import (
_ "github.com/hashicorp/terraform-provider-google/google/services/firebaserules"
_ "github.com/hashicorp/terraform-provider-google/google/services/networkconnectivity"
_ "github.com/hashicorp/terraform-provider-google/google/services/recaptchaenterprise"

// TODO: remove dependency on hashicorp flags
// need to blank import hashicorp sweeper code to maintain the flags declared in their package
_ "github.com/hashicorp/terraform-plugin-testing/helper/resource"

"github.com/hashicorp/terraform-provider-google/google/sweeper"
)

func TestMain(m *testing.M) {
resource.TestMain(m)
func TestSweepers(t *testing.T) {
sweeper.ExecuteSweepers(t)
}
276 changes: 276 additions & 0 deletions google/sweeper/hashi_sweeper_fork.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package sweeper

import (
"flag"
"fmt"
"log"
"os"
"strings"
"testing"
"time"
)

// flagSweep is a flag available when running tests on the command line. It
// contains a comma separated list of regions to for the sweeper functions to
// run in. This flag bypasses the normal Test path and instead runs functions designed to
// clean up any leaked resources a testing environment could have created. It is
// a best effort attempt, and relies on Provider authors to implement "Sweeper"
// methods for resources.

// Adding Sweeper methods with AddTestSweepers will
// construct a list of sweeper funcs to be called here. We iterate through
// regions provided by the sweep flag, and for each region we iterate through the
// tests, and exit on any errors. At time of writing, sweepers are ran
// sequentially, however they can list dependencies to be ran first. We track
// the sweepers that have been ran, so as to not run a sweeper twice for a given
// region.
//
// WARNING:
// Sweepers are designed to be destructive. You should not use the -sweep flag
// in any environment that is not strictly a test environment. Resources will be
// destroyed.

var (
flagSweep *string
flagSweepAllowFailures *bool
flagSweepRun *string
sweeperFuncs map[string]*Sweeper
)

// SweeperFunc is a signature for a function that acts as a sweeper. It
// accepts a string for the region that the sweeper is to be ran in. This
// function must be able to construct a valid client for that region.
type SweeperFunc func(r string) error

type Sweeper struct {
// Name for sweeper. Must be unique to be ran by the Sweeper Runner
Name string

// Dependencies list the const names of other Sweeper functions that must be ran
// prior to running this Sweeper. This is an ordered list that will be invoked
// recursively at the helper/resource level
Dependencies []string

// Sweeper function that when invoked sweeps the Provider of specific
// resources
F SweeperFunc
}

func init() {
sweeperFuncs = make(map[string]*Sweeper)
}

// registerFlags checks for and gets existing flag definitions before trying to redefine them.
// This is needed because this package and terraform-plugin-testing both define the same sweep flags.
// By checking first, we ensure we reuse any existing flags rather than causing a panic from flag redefinition.
// This allows this module to be used alongside terraform-plugin-testing without conflicts.
func registerFlags() {
// Check for existing flags in global CommandLine
if f := flag.Lookup("sweep"); f != nil {
// Use the Value.Get() interface to get the values
if getter, ok := f.Value.(flag.Getter); ok {
vs := getter.Get().(string)
flagSweep = &vs
}
if f := flag.Lookup("sweep-allow-failures"); f != nil {
if getter, ok := f.Value.(flag.Getter); ok {
vb := getter.Get().(bool)
flagSweepAllowFailures = &vb
}
}
if f := flag.Lookup("sweep-run"); f != nil {
if getter, ok := f.Value.(flag.Getter); ok {
vs := getter.Get().(string)
flagSweepRun = &vs
}
}
} else {
// Define our flags if they don't exist
fsDefault := ""
fsafDefault := true
fsrDefault := ""
flagSweep = &fsDefault
flagSweepAllowFailures = &fsafDefault
flagSweepRun = &fsrDefault
}
}

// AddTestSweepers function adds a given name and Sweeper configuration
// pair to the internal sweeperFuncs map. Invoke this function to register a
// resource sweeper to be available for running when the -sweep flag is used
// with `go test`. Sweeper names must be unique to help ensure a given sweeper
// is only ran once per run.
func addTestSweepers(name string, s *Sweeper) {
if _, ok := sweeperFuncs[name]; ok {
log.Fatalf("[ERR] Error adding (%s) to sweeperFuncs: function already exists in map", name)
}

sweeperFuncs[name] = s
}

// ExecuteSweepers
//
// Sweepers enable infrastructure cleanup functions to be included with
// resource definitions, typically so developers can remove all resources of
// that resource type from testing infrastructure in case of failures that
// prevented the normal resource destruction behavior of acceptance tests.
// Use the AddTestSweepers() function to configure available sweepers.
//
// Sweeper flags added to the "go test" command:
//
// -sweep: Comma-separated list of locations/regions to run available sweepers.
// -sweep-allow-failues: Enable to allow other sweepers to run after failures.
// -sweep-run: Comma-separated list of resource type sweepers to run. Defaults
// to all sweepers.
//
// Refer to the Env prefixed constants for environment variables that further
// control testing functionality.
func ExecuteSweepers(t *testing.T) {
registerFlags()
flag.Parse()
if *flagSweep != "" {
// parse flagSweep contents for regions to run
regions := strings.Split(*flagSweep, ",")

// get filtered list of sweepers to run based on sweep-run flag
sweepers := filterSweepers(*flagSweepRun, sweeperFuncs)

if err := runSweepers(t, regions, sweepers, *flagSweepAllowFailures); err != nil {
os.Exit(1)
}
} else {
t.Skip("skipping sweeper run. No region supplied")
}
}

func runSweepers(t *testing.T, regions []string, sweepers map[string]*Sweeper, allowFailures bool) error {
// Sort sweepers by dependency order
sorted, err := validateAndOrderSweepers(sweepers)
if err != nil {
return fmt.Errorf("failed to sort sweepers: %v", err)
}

// Run each sweeper in dependency order
for _, sweeper := range sorted {
sweeper := sweeper // capture for closure
t.Run(sweeper.Name, func(t *testing.T) {
for _, region := range regions {
region := strings.TrimSpace(region)
log.Printf("[DEBUG] Running Sweeper (%s) in region (%s)", sweeper.Name, region)

start := time.Now()
err := sweeper.F(region)
elapsed := time.Since(start)

log.Printf("[DEBUG] Completed Sweeper (%s) in region (%s) in %s", sweeper.Name, region, elapsed)

if err != nil {
log.Printf("[ERROR] Error running Sweeper (%s) in region (%s): %s", sweeper.Name, region, err)
if allowFailures {
t.Errorf("failed in region %s: %s", region, err)
} else {
t.Fatalf("failed in region %s: %s", region, err)
}
}
}
})
}

return nil
}

// filterSweepers takes a comma separated string listing the names of sweepers
// to be ran, and returns a filtered set from the list of all of sweepers to
// run based on the names given.
func filterSweepers(f string, source map[string]*Sweeper) map[string]*Sweeper {
filterSlice := strings.Split(strings.ToLower(f), ",")
if len(filterSlice) == 1 && filterSlice[0] == "" {
// if the filter slice is a single element of "" then no sweeper list was
// given, so just return the full list
return source
}

sweepers := make(map[string]*Sweeper)
for name := range source {
for _, s := range filterSlice {
if strings.Contains(strings.ToLower(name), s) {
for foundName, foundSweeper := range filterSweeperWithDependencies(name, source) {
sweepers[foundName] = foundSweeper
}
}
}
}
return sweepers
}

// filterSweeperWithDependencies recursively returns sweeper and all dependencies.
// Since filterSweepers performs fuzzy matching, this function is used
// to perform exact sweeper and dependency lookup.
func filterSweeperWithDependencies(name string, source map[string]*Sweeper) map[string]*Sweeper {
result := make(map[string]*Sweeper)

currentSweeper, ok := source[name]
if !ok {
log.Printf("[WARN] Sweeper has dependency (%s), but that sweeper was not found", name)
return result
}

result[name] = currentSweeper

for _, dependency := range currentSweeper.Dependencies {
for foundName, foundSweeper := range filterSweeperWithDependencies(dependency, source) {
result[foundName] = foundSweeper
}
}

return result
}

// validateAndOrderSweepers performs topological sort on sweepers based on their dependencies.
// It ensures there are no cycles in the dependency graph and all referenced dependencies exist.
// Returns an ordered list of sweepers where each sweeper appears after its dependencies.
// Returns error if there are any cycles or missing dependencies.
func validateAndOrderSweepers(sweepers map[string]*Sweeper) ([]*Sweeper, error) {
// Detect cycles and get sorted list
visited := make(map[string]bool)
inPath := make(map[string]bool)
sorted := make([]*Sweeper, 0, len(sweepers))

var visit func(name string) error
visit = func(name string) error {
if inPath[name] {
return fmt.Errorf("dependency cycle detected: %s", name)
}
if visited[name] {
return nil
}

inPath[name] = true
sweeper := sweepers[name]
for _, dep := range sweeper.Dependencies {
if _, exists := sweepers[dep]; !exists {
return fmt.Errorf("sweeper %s depends on %s, but %s not found", name, dep, dep)
}
if err := visit(dep); err != nil {
return err
}
}
inPath[name] = false
visited[name] = true
sorted = append(sorted, sweeper)
return nil
}

// Visit all sweepers
for name := range sweepers {
if !visited[name] {
if err := visit(name); err != nil {
return nil, err
}
}
}

return sorted, nil
}
Loading