diff --git a/mmv1/third_party/terraform/.teamcity/components/builds/build_parameters.kt b/mmv1/third_party/terraform/.teamcity/components/builds/build_parameters.kt index 0e100814fe8f..ab111ca66d03 100644 --- a/mmv1/third_party/terraform/.teamcity/components/builds/build_parameters.kt +++ b/mmv1/third_party/terraform/.teamcity/components/builds/build_parameters.kt @@ -225,27 +225,33 @@ fun ParametrizedWithType.sweeperParameters(sweeperRegions: String, sweepRun: Str text("SWEEP_RUN", sweepRun) } -// ParametrizedWithType.terraformSkipProjectSweeper sets an environment variable to skip the sweeper for project resources -fun ParametrizedWithType.terraformSkipProjectSweeper() { - text("env.SKIP_PROJECT_SWEEPER", "1") +// ParametrizedWithType.terraformSkipSweeper sets an environment variable used to skip the sweeper for resources +fun ParametrizedWithType.terraformSkipSweeper(resourceType: String) { + // Converts "PROJECT" into "env.SKIP_PROJECT_SWEEPER" + // Converts "FOLDER" into "env.SKIP_FOLDER_SWEEPER" + text("env.SKIP_${resourceType.uppercase()}_SWEEPER", "1") } // BuildType.disableProjectSweep disabled sweeping project resources after a build configuration has been initialised fun BuildType.disableProjectSweep(){ params { - terraformSkipProjectSweeper() + terraformSkipSweeper("PROJECT") + terraformSkipSweeper("FOLDER") } } -// ParametrizedWithType.terraformEnableProjectSweeper unsets an environment variable used to skip the sweeper for project resources -fun ParametrizedWithType.terraformEnableProjectSweeper() { - text("env.SKIP_PROJECT_SWEEPER", "") +// ParametrizedWithType.terraformEnableSweeper unsets an environment variable used to skip the sweeper for resources +fun ParametrizedWithType.terraformEnableSweeper(resourceType: String) { + // Converts "PROJECT" into "env.SKIP_PROJECT_SWEEPER" + // Converts "FOLDER" into "env.SKIP_FOLDER_SWEEPER" + text("env.SKIP_${resourceType.uppercase()}_SWEEPER", "") } // BuildType.enableProjectSweep enables sweeping project resources after a build configuration has been initialised fun BuildType.enableProjectSweep(){ params { - terraformEnableProjectSweeper() + terraformEnableSweeper("PROJECT") + terraformEnableSweeper("FOLDER") } } diff --git a/mmv1/third_party/terraform/services/resourcemanager/resource_google_folder_sweeper.go b/mmv1/third_party/terraform/services/resourcemanager/resource_google_folder_sweeper.go new file mode 100644 index 000000000000..c79befe27f4a --- /dev/null +++ b/mmv1/third_party/terraform/services/resourcemanager/resource_google_folder_sweeper.go @@ -0,0 +1,193 @@ +package resourcemanager + +import ( + "context" + "fmt" + "log" + "os" + "strings" + "time" + + "github.com/hashicorp/terraform-provider-google/google/envvar" + "github.com/hashicorp/terraform-provider-google/google/services/resourcemanager3" + "github.com/hashicorp/terraform-provider-google/google/sweeper" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" + resourceManagerV3 "google.golang.org/api/cloudresourcemanager/v3" + "google.golang.org/api/googleapi" +) + +func init() { + // SKIP_FOLDER_SWEEPER can be set for a sweeper run to prevent it from + // sweeping folders. This can be useful when running sweepers in + // organizations where acceptance tests intiated by another folder may + // already be in-progress. + // Example: SKIP_FOLDER_SWEEPER=1 go test ./google -v -sweep=us-central1 -sweep-run= + if os.Getenv("SKIP_FOLDER_SWEEPER") != "" { + return + } + + sweeper.AddTestSweepersLegacy("GoogleFolder", testSweepFolder) +} + +func testSweepFolder(region string) error { + config, err := sweeper.SharedConfigForRegion(region) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error getting shared config for region: %s", err) + return err + } + + err = config.LoadAndValidate(context.Background()) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error loading: %s", err) + return err + } + + org := envvar.UnsafeGetTestOrgFromEnv() + + if org == "" { + log.Printf("[INFO][SWEEPER_LOG] no organization set, failing folder sweeper") + return fmt.Errorf("no organization set") + } + + parent := "organizations/" + org + + token := "" + svc := config.NewResourceManagerV3Client(config.UserAgent) + for paginate := true; paginate; { + found, err := config.NewResourceManagerV3Client(config.UserAgent).Folders.List().Parent(parent).PageToken(token).Do() + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error listing folders: %s", err) + return nil + } + + for _, folder := range found.Folders { + if !strings.HasPrefix(folder.DisplayName, TestPrefix) { + continue + } + log.Printf("[INFO][SWEEPER_LOG] Sweeping Folder id: %s, name: %s", folder.Name, folder.DisplayName) + _, err := config.NewResourceManagerV3Client(config.UserAgent).Folders.Delete(folder.Name).Do() + if err != nil { + if isCapabilityError(err) { + log.Println("[INFO][SWEEPER_LOG]Detected 'configured capability' violation. Starting cleanup...") + + // 2. Get Folder to find ManagementProject + folder, err := config.NewResourceManagerV3Client(config.UserAgent).Folders.Get(folder.Name).Do() + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] Error, failed to delete folder %s: %s", folder.Name, err) + continue + } + + // 3. Handle Liens on Management Project + if folder.ManagementProject != "" { + cleanupLiens(svc, folder.ManagementProject) + } + + // 4. Disable configured capability + disableCapability(folder.Name) + + // 5. Retry Delete + log.Println("[INFO][SWEEPER_LOG]Retrying folder deletion...") + _, err = config.NewResourceManagerV3Client(config.UserAgent).Folders.Delete(folder.Name).Do() + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] Error, failed to delete folder %s: %s", folder.Name, err) + continue + } + } else { + log.Printf("[INFO][SWEEPER_LOG] Error, failed to delete folder %s: %s", folder.Name, err) + continue + } + } + } + token = found.NextPageToken + paginate = token != "" + } + + return nil +} + +// isCapabilityError parses the GCP error for the specific PreconditionFailure +func isCapabilityError(err error) bool { + if gErr, ok := err.(*googleapi.Error); ok { + if gErr.Code == 400 && strings.Contains(gErr.Message, "Precondition check failed") { + // Deep check for the description in the error details + for _, detail := range gErr.Details { + if dMap, ok := detail.(map[string]interface{}); ok { + if violations, ok := dMap["violations"].([]interface{}); ok { + for _, v := range violations { + if vMap, ok := v.(map[string]interface{}); ok { + if strings.Contains(fmt.Sprint(vMap["description"]), "configured capability") { + return true + } + } + } + } + } + } + } + } + return false +} + +func cleanupLiens(svc *resourceManagerV3.Service, project string) { + log.Printf("[INFO][SWEEPER_LOG]Checking liens on %s...\n", project) + resp, err := svc.Liens.List().Parent(project).Do() + if err != nil { + log.Printf("[INFO][SWEEPER_LOG]Failed to list liens: %v", err) + return + } + for _, l := range resp.Liens { + log.Printf("[INFO][SWEEPER_LOG]Deleting lien: %s\n", l.Name) + _, err = svc.Liens.Delete(l.Name).Do() + + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] Error deleting lien: %s, err: %s", l.Name, err) + } + } +} + +func disableCapability(folderName string) { + // Format is folders/{id}/capabilities/app-management + capName := fmt.Sprintf("%s/capabilities/app-management", folderName) + log.Printf("[INFO][SWEEPER_LOG]Disabling capability: %s\n", capName) + + config, err := sweeper.SharedConfigForRegion("global") + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error getting shared config for region: %s", err) + return + } + + err = config.LoadAndValidate(context.Background()) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error loading: %s", err) + return + } + + obj := map[string]interface{}{ + "value": false, + } + + url := "https://cloudresourcemanager.googleapis.com/v3/" + capName + + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "PATCH", + Project: config.Project, + RawURL: url, + UserAgent: config.UserAgent, + Body: obj, + }) + + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] Error disabling Capability: %s, err: %s", capName, err) + } else { + log.Printf("[INFO][SWEEPER_LOG] Finished disabled Capability: %s", capName) + } + + err = resourcemanager3.ResourceManager3OperationWaitTime( + config, res, "Updating Capability", config.UserAgent, + 20*time.Minute) + + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] Error for disabling Capability operation: %s, err: %s", capName, err) + } +}