diff --git a/.gitignore b/.gitignore index d1d9ee79..c8c40d9d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ terraform.tfstate credentials.json *.iml .idea +.vscode/ *.pyc .kitchen diff --git a/modules/project_cleanup/README.md b/modules/project_cleanup/README.md index 8a48e23a..a557c034 100644 --- a/modules/project_cleanup/README.md +++ b/modules/project_cleanup/README.md @@ -22,6 +22,7 @@ The following services must be enabled on the project housing the cleanup functi | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| +| clean\_up\_org\_level\_tag\_keys | Clean up organization level Tag Keys. | `bool` | `false` | no | | function\_timeout\_s | The amount of time in seconds allotted for the execution of the function. | `number` | `500` | no | | job\_schedule | Cleaner function run frequency, in cron syntax | `string` | `"*/5 * * * *"` | no | | max\_project\_age\_in\_hours | The maximum number of hours that a GCP project, selected by `target_tag_name` and `target_tag_value`, can exist | `number` | `6` | no | @@ -29,6 +30,7 @@ The following services must be enabled on the project housing the cleanup functi | project\_id | The project ID to host the scheduled function in | `string` | n/a | yes | | region | The region the project is in (App Engine specific) | `string` | n/a | yes | | target\_excluded\_labels | Map of project lablels that won't be deleted. | `map(string)` | `{}` | no | +| target\_excluded\_tagkeys | List of organization Tag Key short names that won't be deleted. | `list(string)` | `[]` | no | | target\_folder\_id | Folder ID to delete all projects under. | `string` | `""` | no | | target\_included\_labels | Map of project lablels that will be deleted. | `map(string)` | `{}` | no | | target\_tag\_name | The name of a tag to filter GCP projects on for consideration by the cleanup utility (legacy, use `target_included_labels` map instead). | `string` | `""` | no | diff --git a/modules/project_cleanup/function_source/go.mod b/modules/project_cleanup/function_source/go.mod index 1aea6053..7f4a4bd7 100644 --- a/modules/project_cleanup/function_source/go.mod +++ b/modules/project_cleanup/function_source/go.mod @@ -1,6 +1,6 @@ module github.com/terraform-google-modules/terraform-google-scheduled-function/modules/project_cleanup -go 1.20 +go 1.21 require ( golang.org/x/net v0.19.0 diff --git a/modules/project_cleanup/function_source/go.sum b/modules/project_cleanup/function_source/go.sum index 68df124a..a118d5cc 100644 --- a/modules/project_cleanup/function_source/go.sum +++ b/modules/project_cleanup/function_source/go.sum @@ -47,6 +47,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -66,6 +67,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= @@ -100,6 +102,7 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -127,7 +130,9 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f h1:Vn+VyHU5guc9KjB5KrjI2q0wCOWEOIh0OEsleqakHJg= +google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f/go.mod h1:nWSwAFPb+qfNJXsoeO3Io7zf4tMSfN8EA8RlDA04GhY= google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f h1:2yNACc1O40tTnrsbk9Cv6oxiW8pxI/pXj0wRtdlYmgY= +google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f/go.mod h1:Uy9bTZJqmfrw2rIBxgGLnamc78euZULUBrLZ9XTITKI= google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 h1:DC7wcm+i+P1rN3Ff07vL+OndGg5OhNddHyTA+ocPqYE= google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4/go.mod h1:eJVxU6o+4G1PSczBr85xmyvSNYAKvAYgkub40YGomFM= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= diff --git a/modules/project_cleanup/function_source/main.go b/modules/project_cleanup/function_source/main.go index b408068a..85b85ee2 100644 --- a/modules/project_cleanup/function_source/main.go +++ b/modules/project_cleanup/function_source/main.go @@ -31,6 +31,7 @@ import ( "golang.org/x/oauth2/google" "google.golang.org/api/cloudresourcemanager/v1" cloudresourcemanager2 "google.golang.org/api/cloudresourcemanager/v2" + cloudresourcemanager3 "google.golang.org/api/cloudresourcemanager/v3" "google.golang.org/api/compute/v1" "google.golang.org/api/googleapi" "google.golang.org/api/servicemanagement/v1" @@ -40,17 +41,24 @@ const ( LifecycleStateActiveRequested = "ACTIVE" TargetExcludedLabels = "TARGET_EXCLUDED_LABELS" TargetIncludedLabels = "TARGET_INCLUDED_LABELS" + CleanUpTagKeys = "CLEAN_UP_TAG_KEYS" + TargetExcludedTagKeys = "TARGET_EXCLUDED_TAGKEYS" TargetFolderId = "TARGET_FOLDER_ID" + TargetOrganizationId = "TARGET_ORGANIZATION_ID" MaxProjectAgeHours = "MAX_PROJECT_AGE_HOURS" targetFolderRegexp = `^[0-9]+$` + targetOrganizationRegexp = `^[0-9]+$` ) var ( logger = log.New(os.Stdout, "", 0) excludedLabelsMap = getLabelsMapFromEnv(TargetExcludedLabels) includedLabelsMap = getLabelsMapFromEnv(TargetIncludedLabels) + cleanUpTagKeys = getCleanUpTagKeysOrTerminateExecution() + excludedTagKeysList = getTagKeysListFromEnv(TargetExcludedTagKeys) resourceCreationCutoff = getOldTime(int64(getCorrectMaxAgeInHoursOrTerminateExecution()) * 60 * 60) rootFolderId = getCorrectFolderIdOrTerminateExecution() + organizationId = getCorrectOrganizationIdOrTerminateExecution() ) type PubSubMessage struct { @@ -110,7 +118,7 @@ func processProjectsResponsePage(removeProjectById func(projectId string)) func( ageFilter := func(project *cloudresourcemanager.Project) bool { projectCreatedAt, err := time.Parse(time.RFC3339, project.CreateTime) if err != nil { - logger.Printf("Fail to parse CreateTime for [%s], skip it. Error [%s]", project.Name, err.Error()) + logger.Printf("Failed to parse CreateTime for [%s], skipping it, error [%s]", project.Name, err.Error()) return false } return projectCreatedAt.Before(resourceCreationCutoff) @@ -135,7 +143,7 @@ func getCorrectMaxAgeInHoursOrTerminateExecution() int64 { maxAgeInHoursStr := os.Getenv(MaxProjectAgeHours) maxAgeInHours, err := strconv.ParseInt(os.Getenv(MaxProjectAgeHours), 10, 0) if err != nil { - logger.Fatalf("Could not convert [%s] to integer. Specify correct value, Please.", maxAgeInHoursStr) + logger.Fatalf("Could not convert [%s] to integer. Specify correct value for environment variable [%s] and try again.", maxAgeInHoursStr, MaxProjectAgeHours) } return maxAgeInHours } @@ -154,6 +162,18 @@ func checkIfAtLeastOneLabelPresentIfAny(project *cloudresourcemanager.Project, l return result } +func checkIfTagKeyShortNameExcluded(shortName string, excludedTagKeys []string) bool { + if len(excludedTagKeys) == 0 { + return false + } + for _, name := range excludedTagKeys { + if shortName == name { + return true + } + } + return false +} + func getLabelsMapFromEnv(envVariableName string) map[string]string { targetExcludedLabels := os.Getenv(envVariableName) logger.Println("Try to get labels map") @@ -166,22 +186,61 @@ func getLabelsMapFromEnv(envVariableName string) map[string]string { err := json.Unmarshal([]byte(targetExcludedLabels), &labels) if err != nil { - logger.Printf("Fail to get labels map from [%s] env variable, error [%s]", envVariableName, err.Error()) + logger.Printf("Failed to get labels map from [%s] env variable, error [%s]", envVariableName, err.Error()) } else { logger.Printf("Got labels map [%s] from [%s] env variable", labels, envVariableName) } return labels } +func getTagKeysListFromEnv(envVariableName string) []string { + targetExcludedTagKeys := os.Getenv(envVariableName) + logger.Println("Try to get Tag Keys list") + if targetExcludedTagKeys == "" { + logger.Printf("No Tag Keys provided.") + return nil + } + + var tagKeys []string + err := json.Unmarshal([]byte(targetExcludedTagKeys), &tagKeys) + if err != nil { + logger.Printf("Failed to get Tag Keys list from [%s] env variable, error [%s]", envVariableName, err.Error()) + } else { + logger.Printf("Got Tag Keys list [%s] from [%s] env variable", tagKeys, envVariableName) + } + return tagKeys +} + +func getCleanUpTagKeysOrTerminateExecution() bool { + cleanUpTagKeys, exists := os.LookupEnv(CleanUpTagKeys) + if !exists { + logger.Fatalf("Clean up Tag Keys environment variable [%s] not set, set the environment variable and try again.", CleanUpTagKeys) + } + result, err := strconv.ParseBool(cleanUpTagKeys) + if err != nil { + logger.Fatalf("Invalid Clean up Tag Keys value [%s], specify correct value for environment variable [%s] and try again.", cleanUpTagKeys, CleanUpTagKeys) + } + return result +} + func getCorrectFolderIdOrTerminateExecution() string { targetFolderIdString := os.Getenv(TargetFolderId) matched, err := regexp.MatchString(targetFolderRegexp, targetFolderIdString) if err != nil || !matched { - logger.Fatalf("Invalid folder id [%s]. Specify correct value, Please.", targetFolderIdString) + logger.Fatalf("Invalid folder id [%s], specify correct value and try again.", targetFolderIdString) } return targetFolderIdString } +func getCorrectOrganizationIdOrTerminateExecution() string { + targetOrganizationIdString := os.Getenv(TargetOrganizationId) + matched, err := regexp.MatchString(targetOrganizationRegexp, targetOrganizationIdString) + if err != nil || !matched { + logger.Fatalf("Invalid organization id [%s], specify correct value and try again.", targetOrganizationIdString) + } + return targetOrganizationIdString +} + func getServiceManagementServiceOrTerminateExecution(client *http.Client) *servicemanagement.APIService { service, err := servicemanagement.New(client) if err != nil { @@ -194,7 +253,7 @@ func getResourceManagerServiceOrTerminateExecution(client *http.Client) *cloudre logger.Println("Try to get Cloud Resource Manager") cloudResourceManagerService, err := cloudresourcemanager.New(client) if err != nil { - logger.Fatalf("Fail to get Cloud Resource Manager with error [%s], terminate execution", err.Error()) + logger.Fatalf("Failed to get Cloud Resource Manager with error [%s], terminate execution", err.Error()) } logger.Println("Got Cloud Resource Manager") return cloudResourceManagerService @@ -204,17 +263,37 @@ func getFolderServiceOrTerminateExecution(client *http.Client) *cloudresourceman logger.Println("Try to get Folders Service") cloudResourceManagerService, err := cloudresourcemanager2.New(client) if err != nil { - logger.Fatalf("Fail to get Folders Service with error [%s], terminate execution", err.Error()) + logger.Fatalf("Failed to get Folders Service with error [%s], terminate execution", err.Error()) } logger.Println("Got Folders Service") return cloudResourceManagerService.Folders } +func getTagKeysServiceOrTerminateExecution(client *http.Client) *cloudresourcemanager3.TagKeysService { + logger.Println("Try to get TagKeys Service") + cloudResourceManagerService, err := cloudresourcemanager3.New(client) + if err != nil { + logger.Fatalf("Failed to get TagKeys Service with error [%s], terminate execution", err.Error()) + } + logger.Println("Got TagKeys Service") + return cloudResourceManagerService.TagKeys +} + +func getTagValuesServiceOrTerminateExecution(client *http.Client) *cloudresourcemanager3.TagValuesService { + logger.Println("Try to get TagValues Service") + cloudResourceManagerService, err := cloudresourcemanager3.New(client) + if err != nil { + logger.Fatalf("Failed to get TagValues Service with error [%s], terminate execution", err.Error()) + } + logger.Println("Got TagValues Service") + return cloudResourceManagerService.TagValues +} + func getFirewallPoliciesServiceOrTerminateExecution(client *http.Client) *compute.FirewallPoliciesService { logger.Println("Try to get Firewall Policies Service") computeService, err := compute.New(client) if err != nil { - logger.Fatalf("Fail to get Firewall Policies Service with error [%s], terminate execution", err.Error()) + logger.Fatalf("Failed to get Firewall Policies Service with error [%s], terminate execution", err.Error()) } logger.Println("Got Firewall Policies Service") return computeService.FirewallPolicies @@ -224,7 +303,7 @@ func initializeGoogleClient(ctx context.Context) *http.Client { logger.Println("Try to initialize Google client") client, err := google.DefaultClient(ctx, cloudresourcemanager.CloudPlatformScope) if err != nil { - logger.Fatalf("Fail to initialize Google client with error [%s], terminate execution", err.Error()) + logger.Fatalf("Failed to initialize Google client with error [%s], terminate execution", err.Error()) } logger.Println("Initialized Google client") return client @@ -234,6 +313,8 @@ func invoke(ctx context.Context) { client := initializeGoogleClient(ctx) cloudResourceManagerService := getResourceManagerServiceOrTerminateExecution(client) folderService := getFolderServiceOrTerminateExecution(client) + tagKeyService := getTagKeysServiceOrTerminateExecution(client) + tagValuesService := getTagValuesServiceOrTerminateExecution(client) firewallPoliciesService := getFirewallPoliciesServiceOrTerminateExecution(client) endpointService := getServiceManagementServiceOrTerminateExecution(client) @@ -241,29 +322,72 @@ func invoke(ctx context.Context) { logger.Printf("Try to remove lien [%s]", name) _, err := cloudResourceManagerService.Liens.Delete(name).Context(ctx).Do() if err != nil { - logger.Printf("Fail to remove lien [%s], error [%s]", name, err.Error()) + logger.Printf("Failed to remove lien [%s], error [%s]", name, err.Error()) } else { logger.Printf("Removed lien [%s]", name) } } + tagKeyAgeFilter := func(tagKey *cloudresourcemanager3.TagKey) bool { + tagKeyCreatedAt, err := time.Parse(time.RFC3339, tagKey.CreateTime) + if err != nil { + logger.Printf("Failed to parse CreateTime for tagKey [%s], skipping it, error [%s]", tagKey.Name, err.Error()) + return false + } + return tagKeyCreatedAt.Before(resourceCreationCutoff) + } + + removeTagValues := func(tagKey string) { + logger.Printf("Try to remove Tag Values from TagKey [%s]", tagKey) + tagValuesList, err := tagValuesService.List().Parent(tagKey).Context(ctx).Do() + if err != nil { + logger.Printf("Failed to list Tag values from TagKey [%s], error [%s]", tagKey, err.Error()) + return + } + for _, tagValue := range tagValuesList.TagValues { + _, err := tagValuesService.Delete(tagValue.Name).Context(ctx).Do() + if err != nil { + logger.Printf("Failed to delete tagValue from TagKey [%s], error [%s]", tagKey, err.Error()) + } + } + } + + removeTagKeys := func(organization string) { + logger.Printf("Try to remove Tag Keys from organization [%s]", organization) + parent := fmt.Sprintf("organizations/%s", organization) + tagKeysList, err := tagKeyService.List().Parent(parent).Context(ctx).Do() + if err != nil { + logger.Printf("Failed to list Tag Keys from organization [%s], error [%s]", organization, err.Error()) + return + } + for _, tagKey := range tagKeysList.TagKeys { + if !checkIfTagKeyShortNameExcluded(tagKey.ShortName, excludedTagKeysList) && tagKeyAgeFilter(tagKey) { + removeTagValues(tagKey.Name) + _, err := tagKeyService.Delete(tagKey.Name).Context(ctx).Do() + if err != nil { + logger.Printf("Failed to delete tagKey from organization [%s], error [%s]", organization, err.Error()) + } + } + } + } + removeFirewallPolicies := func(folder string) { logger.Printf("Try to remove Firewall Policies from folder [%s]", folder) firewallPolicyList, err := firewallPoliciesService.List().ParentId(folder).Context(ctx).Do() if err != nil { - logger.Printf("Fail to list Firewall Policies from folder [%s], error [%s]", folder, err.Error()) + logger.Printf("Failed to list Firewall Policies from folder [%s], error [%s]", folder, err.Error()) return } for _, policy := range firewallPolicyList.Items { for _, association := range policy.Associations { _, err := firewallPoliciesService.RemoveAssociation(policy.Name).Name(association.Name).Context(ctx).Do() if err != nil { - logger.Printf("Fail to Remove Association for Firewall Policies from folder [%s], error [%s]", folder, err.Error()) + logger.Printf("Failed to Remove Association for Firewall Policies from folder [%s], error [%s]", folder, err.Error()) } } _, err := firewallPoliciesService.Delete(policy.Name).Context(ctx).Do() if err != nil { - logger.Printf("Fail to delete Firewall Policy [%s] from folder [%s], error [%s]", policy.Name, folder, err.Error()) + logger.Printf("Failed to delete Firewall Policy [%s] from folder [%s], error [%s]", policy.Name, folder, err.Error()) } } } @@ -277,7 +401,7 @@ func invoke(ctx context.Context) { logger.Printf("Try to remove endpoints for [%s]", projectId) listResponse, err := endpointService.Services.List().ProducerProjectId(projectId).Do() if err != nil { - logger.Printf("Fail to list services for [%s], error [%s]", projectId, err.Error()) + logger.Printf("Failed to list services for [%s], error [%s]", projectId, err.Error()) return } @@ -289,7 +413,7 @@ func invoke(ctx context.Context) { logger.Printf("Try to remove service: %s", service.ServiceName) _, err = endpointService.Services.Delete(service.ServiceName).Do() if err != nil { - logger.Printf("Fail to delete service [%s] for [%s], error [%s]", service.ServiceName, projectId, err.Error()) + logger.Printf("Failed to delete service [%s] for [%s], error [%s]", service.ServiceName, projectId, err.Error()) } } @@ -305,7 +429,7 @@ func invoke(ctx context.Context) { err = removeProjectById(projectId) } if err != nil { - logger.Printf("Fail to remove project [%s], error [%s]", projectId, err.Error()) + logger.Printf("Failed to remove project [%s], error [%s]", projectId, err.Error()) } else { logger.Printf("Removed project [%s]", projectId) } @@ -323,7 +447,7 @@ func invoke(ctx context.Context) { cleanupProjectById(projectId) return nil }); err != nil { - logger.Printf("Fail to get all liens for the project [%s], error [%s]", projectId, err.Error()) + logger.Printf("Failed to get all liens for the project [%s], error [%s]", projectId, err.Error()) } } @@ -337,7 +461,7 @@ func invoke(ctx context.Context) { return }, 5, time.Minute) if err != nil { - logger.Printf("Fail to get projects for the folder with id [%s], error [%s]", localFolderId, err.Error()) + logger.Printf("Failed to get projects for the folder with id [%s], error [%s]", localFolderId, err.Error()) } else { logger.Printf("Got and processed all projects for the folder with id [%s]", localFolderId) } @@ -346,7 +470,7 @@ func invoke(ctx context.Context) { folderAgeFilter := func(folder *cloudresourcemanager2.Folder) bool { folderCreatedAt, err := time.Parse(time.RFC3339, folder.CreateTime) if err != nil { - logger.Printf("Fail to parse CreateTime for folder [%s], skip it. Error [%s]", folder.Name, err.Error()) + logger.Printf("Failed to parse CreateTime for folder [%s], skipping it, error [%s]", folder.Name, err.Error()) return false } return folderCreatedAt.Before(resourceCreationCutoff) @@ -377,17 +501,21 @@ func invoke(ctx context.Context) { } return nil }); err != nil { - logger.Fatalf("Fail to get subfolders for the folder with id [%s], error [%s]", folderId, err.Error()) + logger.Fatalf("Failed to get subfolders for the folder with id [%s], error [%s]", folderId, err.Error()) } } rootFolderId := fmt.Sprintf("folders/%s", rootFolderId) rootFolder, err := folderService.Get(rootFolderId).Do() if err != nil { - logger.Printf("Fail to get parent folder [%s], error [%s]", rootFolderId, err.Error()) + logger.Printf("Failed to get parent folder [%s], error [%s]", rootFolderId, err.Error()) } else { getSubFoldersAndRemoveProjectsFoldersRecursively(rootFolder, getSubFoldersAndRemoveProjectsFoldersRecursively) } + // Only Tag Keys whose values are not in use can be deleted. + if cleanUpTagKeys { + removeTagKeys(organizationId) + } } func CleanUpProjects(ctx context.Context, m PubSubMessage) error { diff --git a/modules/project_cleanup/main.tf b/modules/project_cleanup/main.tf index 9f1f5609..fb188f34 100644 --- a/modules/project_cleanup/main.tf +++ b/modules/project_cleanup/main.tf @@ -32,6 +32,7 @@ resource "google_organization_iam_member" "main" { "roles/serviceusage.serviceUsageAdmin", "roles/compute.orgSecurityResourceAdmin", "roles/compute.orgSecurityPolicyAdmin", + "roles/resourcemanager.tagAdmin", "roles/viewer" ]) @@ -52,14 +53,17 @@ module "scheduled_project_cleaner" { topic_name = var.topic_name function_available_memory_mb = 128 function_description = "Clean up GCP projects older than ${var.max_project_age_in_hours} hours matching particular tags" - function_runtime = "go113" + function_runtime = "go121" function_service_account_email = google_service_account.project_cleaner_function.email function_timeout_s = var.function_timeout_s function_environment_variables = { - TARGET_EXCLUDED_LABELS = jsonencode(var.target_excluded_labels) - TARGET_FOLDER_ID = var.target_folder_id - TARGET_INCLUDED_LABELS = jsonencode(local.target_included_labels) - MAX_PROJECT_AGE_HOURS = var.max_project_age_in_hours + TARGET_ORGANIZATION_ID = var.organization_id + TARGET_FOLDER_ID = var.target_folder_id + TARGET_EXCLUDED_LABELS = jsonencode(var.target_excluded_labels) + TARGET_INCLUDED_LABELS = jsonencode(local.target_included_labels) + MAX_PROJECT_AGE_HOURS = var.max_project_age_in_hours + CLEAN_UP_TAG_KEYS = var.clean_up_org_level_tag_keys + TARGET_EXCLUDED_TAGKEYS = jsonencode(var.target_excluded_tagkeys) } } diff --git a/modules/project_cleanup/variables.tf b/modules/project_cleanup/variables.tf index 13f97894..4b8efdd6 100644 --- a/modules/project_cleanup/variables.tf +++ b/modules/project_cleanup/variables.tf @@ -77,6 +77,18 @@ variable "target_included_labels" { default = {} } +variable "clean_up_org_level_tag_keys" { + type = bool + description = "Clean up organization level Tag Keys." + default = false +} + +variable "target_excluded_tagkeys" { + type = list(string) + description = "List of organization Tag Key short names that won't be deleted." + default = [] +} + variable "target_folder_id" { type = string description = "Folder ID to delete all projects under."