Skip to content

Commit e58dc20

Browse files
authored
feat: Support deletion of Billing Account Log Sinks (#227)
1 parent f962d8e commit e58dc20

File tree

5 files changed

+144
-94
lines changed

5 files changed

+144
-94
lines changed

modules/project_cleanup/README.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -12,34 +12,40 @@ Running this module requires an App Engine app in the specified project/region.
1212

1313
The following services must be enabled on the project housing the cleanup function prior to invoking this module:
1414

15+
- Artifact Registry API (`artifactregistry.googleapis.com`)
1516
- Cloud Functions (`cloudfunctions.googleapis.com`)
1617
- Cloud Scheduler (`cloudscheduler.googleapis.com`)
1718
- Cloud Resource Manager (`cloudresourcemanager.googleapis.com`)
1819
- Compute Engine API (`compute.googleapis.com`)
1920
- Cloud Asset API (`cloudasset.googleapis.com`)
2021
- Security Command Center API (`securitycenter.googleapis.com`)
22+
- Cloud Logging API (`logging.googleapis.com`)
2123

2224
<!-- BEGINNING OF PRE-COMMIT-TERRAFORM DOCS HOOK -->
2325
## Inputs
2426

2527
| Name | Description | Type | Default | Required |
2628
|------|-------------|------|---------|:--------:|
29+
| billing\_account | Billing Account used to provision resources. | `string` | `""` | no |
30+
| clean\_up\_billing\_sinks | Clean up Billing Account Sinks. | `bool` | `false` | no |
2731
| clean\_up\_org\_level\_cai\_feeds | Clean up organization level Cloud Asset Inventory Feeds. | `bool` | `false` | no |
2832
| clean\_up\_org\_level\_scc\_notifications | Clean up organization level Security Command Center notifications. | `bool` | `false` | no |
2933
| clean\_up\_org\_level\_tag\_keys | Clean up organization level Tag Keys. | `bool` | `false` | no |
3034
| function\_timeout\_s | The amount of time in seconds allotted for the execution of the function. | `number` | `500` | no |
3135
| job\_schedule | Cleaner function run frequency, in cron syntax | `string` | `"*/5 * * * *"` | no |
36+
| list\_billing\_sinks\_page\_size | The maximum number of Billing Account Log Sinks to return in the call to `BillingAccountsSinksService.List` service. | `number` | `200` | no |
3237
| list\_scc\_notifications\_page\_size | The maximum number of notification configs to return in the call to `ListNotificationConfigs` service. The minimun value is 1 and the maximum value is 1000. | `number` | `500` | no |
3338
| 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 |
3439
| organization\_id | The organization ID whose projects to clean up | `string` | n/a | yes |
3540
| project\_id | The project ID to host the scheduled function in | `string` | n/a | yes |
3641
| region | The region the project is in (App Engine specific) | `string` | n/a | yes |
42+
| target\_billing\_sinks | List of Billing Account Log Sinks names regex that will be deleted. Regex example: `.*/sinks/sk-c-logging-.*-billing-.*` | `list(string)` | `[]` | no |
3743
| target\_excluded\_labels | Map of project lablels that won't be deleted. | `map(string)` | `{}` | no |
3844
| target\_excluded\_tagkeys | List of organization Tag Key short names that won't be deleted. | `list(string)` | `[]` | no |
3945
| target\_folder\_id | Folder ID to delete all projects under. | `string` | `""` | no |
4046
| target\_included\_feeds | List of organization level Cloud Asset Inventory feeds that should be deleted. Regex example: `.*/feeds/fd-cai-monitoring-.*` | `list(string)` | `[]` | no |
4147
| target\_included\_labels | Map of project lablels that will be deleted. | `map(string)` | `{}` | no |
42-
| target\_included\_scc\_notifications | List of organization Security Command Center notifications names regex that will be deleted. Regex example: `.*/notificationConfigs/scc-notify-.*` | `list(string)` | `[]` | no |
48+
| target\_included\_scc\_notifications | List of organization Security Command Center notifications names regex that will be deleted. Regex example: `.*/notificationConfigs/scc-notify-.*` | `list(string)` | `[]` | no |
4349
| 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 |
4450
| target\_tag\_value | The value of a tag to filter GCP projects on for consideration by the cleanup utility (legacy, use `target_included_labels` map instead). | `string` | `""` | no |
4551
| topic\_name | Name of pubsub topic connecting the scheduled projects cleanup function | `string` | `"pubsub_scheduled_project_cleaner"` | no |

modules/project_cleanup/function_source/README.md

+14-1
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,24 @@ The following environment variables may be specified to configure the cleanup ut
1515

1616
| Name | Description | Type | Default | Required |
1717
|------|-------------|:----:|:-----:|:-----:|
18+
| `BILLING_ACCOUNT` | Billing Account used to provision resources. | `string` | n/a | no |
19+
| `BILLING_SINKS_PAGE_SIZE ` | The maximum number of Billing Account Log Sinks to return in the call to `BillingAccountsSinksService.List` service. | `number` | n/a | yes |
20+
| `CLEAN_UP_BILLING_SINKS` | Clean up Billing Account Sinks. | `bool` | n/a | yes |
21+
| `CLEAN_UP_CAI_FEEDS`| Clean up organization level Cloud Asset Inventory Feeds. | `bool` | n/a | yes |
22+
| `CLEAN_UP_SCC_NOTIFICATIONS` | Clean up organization level Security Command Center notifications. | `bool` | n/a | yes |
23+
| `CLEAN_UP_TAG_KEYS` | Clean up organization level Tag Keys. | `bool` | n/a | yes |
24+
| `MAX_PROJECT_AGE_HOURS` | The project age, in hours, at which point deletion should be considered | integer | n/a | yes |
25+
| `SCC_NOTIFICATIONS_PAGE_SIZE` | The maximum number of notification configs to return in the call to `ListNotificationConfigs` service. The minimun value is 1 and the maximum value is 1000. | `number` | n/a | yes |
26+
| `TARGET_BILLING_SINKS` | List of Billing Account Log Sinks names regex that will be deleted. Regex example: `.*/sinks/sk-c-logging-.*-billing-.*` | `list(string)` | n/a | no |
1827
| `TARGET_EXCLUDED_LABELS` | Labels to match on for identifying projects to avoid deletion | string | n/a | no |
28+
| `TARGET_EXCLUDED_TAGKEYS` | List of organization Tag Key short names that won't be deleted. | `list(string)` | n/a | no |
1929
| `TARGET_FOLDER_ID` | Folder ID to delete projects under | string | n/a | yes |
30+
| `TARGET_INCLUDED_FEEDS` | List of organization level Cloud Asset Inventory feeds that should be deleted. Regex example: `.*/feeds/fd-cai-monitoring-.*` | `list(string)` | n/a | no |
2031
| `TARGET_INCLUDED_LABELS` | Labels to match on for identifying projects to delete | string | n/a | no |
21-
| `MAX_PROJECT_AGE_HOURS` | The project age, in hours, at which point deletion should be considered | integer | n/a | yes |
32+
| `TARGET_INCLUDED_SCC_NOTIFICATIONS` | List of organization Security Command Center notifications names regex that will be deleted. Regex example: `.*/notificationConfigs/scc-notify-.*` | `list(string)` | n/a | no |
33+
| `TARGET_ORGANIZATION_ID` | The organization ID whose projects to clean up | `string` | n/a | yes |
2234

2335
## Required Permissions
2436

2537
This Cloud Function must be run as a Service Account with the `Organization Administrator` (`roles/resourcemanager.organizationAdmin`) role.
38+
If `CLEAN_UP_BILLING_SINKS` is enabled the Service Account running the Cloud Function needs role Logs Configuration Writer(`roles/logging.configWriter`) in the billing account `BILLING_ACCOUNT`.

modules/project_cleanup/function_source/main.go

+94-91
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import (
3939
"google.golang.org/api/compute/v1"
4040
"google.golang.org/api/googleapi"
4141
"google.golang.org/api/iterator"
42+
"google.golang.org/api/logging/v2"
4243
"google.golang.org/api/option"
4344
"google.golang.org/api/servicemanagement/v1"
4445
)
@@ -56,25 +57,34 @@ const (
5657
MaxProjectAgeHours = "MAX_PROJECT_AGE_HOURS"
5758
targetFolderRegexp = `^[0-9]+$`
5859
targetOrganizationRegexp = `^[0-9]+$`
60+
billingAccountRegex = `^[0-9A-Z][-0-9A-Z]{18}[0-9A-Z]$`
5961
SCCNotificationsPageSize = "SCC_NOTIFICATIONS_PAGE_SIZE"
6062
CleanUpCaiFeeds = "CLEAN_UP_CAI_FEEDS"
6163
TargetIncludedFeeds = "TARGET_INCLUDED_FEEDS"
64+
BillingAccount = "BILLING_ACCOUNT"
65+
CleanUpBillingSinks = "CLEAN_UP_BILLING_SINKS"
66+
TargetBillingSinks = "TARGET_BILLING_SINKS"
67+
BillingSinksPageSize = "BILLING_SINKS_PAGE_SIZE"
6268
)
6369

6470
var (
6571
logger = log.New(os.Stdout, "", 0)
6672
excludedLabelsMap = getLabelsMapFromEnv(TargetExcludedLabels)
6773
includedLabelsMap = getLabelsMapFromEnv(TargetIncludedLabels)
68-
cleanUpTagKeys = getCleanUpTagKeysOrTerminateExecution()
69-
cleanUpSCCNotfi = getCleanUpSCCNotfiOrTerminateExecution()
74+
cleanUpTagKeys = getBoolFromEnv(CleanUpTagKeys)
75+
cleanUpSCCNotfi = getBoolFromEnv(CleanUpSCCNotfi)
7076
excludedTagKeysList = getTagKeysListFromEnv(TargetExcludedTagKeys)
71-
includedSCCNotfisList = getSCCNotfiListFromEnv(TargetIncludedSCCNotfis)
72-
resourceCreationCutoff = getOldTime(int64(getCorrectMaxAgeInHoursOrTerminateExecution()) * 60 * 60)
77+
includedSCCNotfisList = getRegexListFromEnv(TargetIncludedSCCNotfis)
78+
resourceCreationCutoff = getOldTime(getIntFromEnv(MaxProjectAgeHours) * 60 * 60)
7379
rootFolderId = getCorrectFolderIdOrTerminateExecution()
7480
organizationId = getCorrectOrganizationIdOrTerminateExecution()
75-
sccPageSize = getSCCNotificationPageSizeOrTerminateExecution()
76-
cleanUpCaiFeeds = getCleanUpFeedsOrTerminateExecution()
77-
includedFeedsList = getFeedsListFromEnv(TargetIncludedFeeds)
81+
sccPageSize = int32(getIntFromEnv(SCCNotificationsPageSize))
82+
cleanUpCaiFeeds = getBoolFromEnv(CleanUpCaiFeeds)
83+
includedFeedsList = getRegexListFromEnv(TargetIncludedFeeds)
84+
billingAccount = getBillingAccountOrTerminateExecution()
85+
cleanUpBillingSinks = getBoolFromEnv(CleanUpBillingSinks)
86+
billingSinksPageSize = getIntFromEnv(BillingSinksPageSize)
87+
targetBillingSinks = getRegexListFromEnv(TargetBillingSinks)
7888
)
7989

8090
type PubSubMessage struct {
@@ -155,15 +165,6 @@ func processProjectsResponsePage(removeProjectById func(projectId string)) func(
155165
}
156166
}
157167

158-
func getCorrectMaxAgeInHoursOrTerminateExecution() int64 {
159-
maxAgeInHoursStr := os.Getenv(MaxProjectAgeHours)
160-
maxAgeInHours, err := strconv.ParseInt(os.Getenv(MaxProjectAgeHours), 10, 0)
161-
if err != nil {
162-
logger.Fatalf("Could not convert [%s] to integer. Specify correct value for environment variable [%s] and try again.", maxAgeInHoursStr, MaxProjectAgeHours)
163-
}
164-
return maxAgeInHours
165-
}
166-
167168
func checkIfAtLeastOneLabelPresentIfAny(project *cloudresourcemanager.Project, labels map[string]string, isExcludeCheck bool) bool {
168169
if len(labels) == 0 {
169170
return !isExcludeCheck
@@ -221,29 +222,29 @@ func getLabelsMapFromEnv(envVariableName string) map[string]string {
221222
return labels
222223
}
223224

224-
func getSCCNotfiListFromEnv(envVariableName string) []*regexp.Regexp {
225+
func getRegexListFromEnv(envVariableName string) []*regexp.Regexp {
225226
var compiledRegEx []*regexp.Regexp
226-
targetExcludedSCCNotfis := os.Getenv(envVariableName)
227-
logger.Println("Try to get SCC Notifications list")
228-
if targetExcludedSCCNotfis == "" {
229-
logger.Printf("No SCC Notifications provided.")
227+
envListVar := os.Getenv(envVariableName)
228+
logger.Printf("Try to get [%s] list", envVariableName)
229+
if envListVar == "" {
230+
logger.Printf("No value for [%s] list provided.", envVariableName)
230231
return compiledRegEx
231232
}
232233

233-
var sccNotfis []string
234-
err := json.Unmarshal([]byte(targetExcludedSCCNotfis), &sccNotfis)
234+
var regexList []string
235+
err := json.Unmarshal([]byte(envListVar), &regexList)
235236
if err != nil {
236-
logger.Printf("Failed to get SCC Notifications list from [%s] env variable, error [%s]", envVariableName, err.Error())
237+
logger.Printf("Failed to get Regex list from [%s] env variable, error [%s]", envVariableName, err.Error())
237238
return compiledRegEx
238239
} else {
239-
logger.Printf("Got SCC Notifications list [%s] from [%s] env variable", sccNotfis, envVariableName)
240+
logger.Printf("Got Regex list [%s] from [%s] env variable", regexList, envVariableName)
240241
}
241242

242243
//build Regexes
243-
for _, r := range sccNotfis {
244+
for _, r := range regexList {
244245
result, err := regexp.Compile(r)
245246
if err != nil {
246-
logger.Printf("Invalid regular expression [%s] for SCC Notification", r)
247+
logger.Printf("Invalid regular expression [%s] for [%s]", r, envVariableName)
247248
} else {
248249
compiledRegEx = append(compiledRegEx, result)
249250
}
@@ -269,79 +270,25 @@ func getTagKeysListFromEnv(envVariableName string) []string {
269270
return tagKeys
270271
}
271272

272-
func getCleanUpTagKeysOrTerminateExecution() bool {
273-
cleanUpTagKeys, exists := os.LookupEnv(CleanUpTagKeys)
274-
if !exists {
275-
logger.Fatalf("Clean up Tag Keys environment variable [%s] not set, set the environment variable and try again.", CleanUpTagKeys)
276-
}
277-
result, err := strconv.ParseBool(cleanUpTagKeys)
278-
if err != nil {
279-
logger.Fatalf("Invalid Clean up Tag Keys value [%s], specify correct value for environment variable [%s] and try again.", cleanUpTagKeys, CleanUpTagKeys)
280-
}
281-
return result
282-
}
283-
284-
func getCleanUpSCCNotfiOrTerminateExecution() bool {
285-
cleanUpSCCNotfiVal, exists := os.LookupEnv(CleanUpSCCNotfi)
273+
func getBoolFromEnv(envVariableName string) bool {
274+
envVariableNameVal, exists := os.LookupEnv(envVariableName)
286275
if !exists {
287-
logger.Fatalf("Clean up SCC notifications environment variable [%s] not set, set the environment variable and try again.", CleanUpSCCNotfi)
276+
logger.Fatalf("Environment variable [%s] not set, set the environment variable and try again.", envVariableName)
288277
}
289-
result, err := strconv.ParseBool(cleanUpSCCNotfiVal)
278+
result, err := strconv.ParseBool(envVariableNameVal)
290279
if err != nil {
291-
logger.Fatalf("Invalid Clean up SCC notifications value [%s], specify correct value for environment variable [%s] and try again.", cleanUpSCCNotfiVal, CleanUpSCCNotfi)
280+
logger.Fatalf("Invalid bool value [%s], specify correct value for environment variable [%s] and try again.", envVariableNameVal, envVariableName)
292281
}
293282
return result
294283
}
295284

296-
func getSCCNotificationPageSizeOrTerminateExecution() int32 {
297-
pageSize := os.Getenv(SCCNotificationsPageSize)
298-
size, err := strconv.ParseInt(pageSize, 10, 32)
299-
if err != nil {
300-
logger.Fatalf("Invalid page size [%s], specify correct value and try again.", pageSize)
301-
}
302-
return int32(size)
303-
}
304-
305-
func getFeedsListFromEnv(envVariableName string) []*regexp.Regexp {
306-
var compiledRegEx []*regexp.Regexp
307-
targetIncludedFeeds := os.Getenv(envVariableName)
308-
logger.Println("Try to get CAI Feeds list")
309-
if targetIncludedFeeds == "" {
310-
logger.Printf("No CAI Feeds provided.")
311-
return compiledRegEx
312-
}
313-
314-
var caiFeeds []string
315-
err := json.Unmarshal([]byte(targetIncludedFeeds), &caiFeeds)
285+
func getIntFromEnv(envVariableName string) int64 {
286+
envVariableStr := os.Getenv(envVariableName)
287+
intValue, err := strconv.ParseInt(envVariableStr, 10, 0)
316288
if err != nil {
317-
logger.Printf("Failed to get CAI Feeds list from [%s] env variable, error [%s]", envVariableName, err.Error())
318-
return nil
319-
} else {
320-
logger.Printf("Got CAI Feeds list [%s] from [%s] env variable", caiFeeds, envVariableName)
321-
}
322-
323-
//build Regexes
324-
for _, r := range caiFeeds {
325-
result, err := regexp.Compile(r)
326-
if err != nil {
327-
logger.Printf("Invalid regular expression [%s] for CAI Feed", r)
328-
} else {
329-
compiledRegEx = append(compiledRegEx, result)
330-
}
289+
logger.Fatalf("Could not convert [%s] to integer. Specify correct value for environment variable [%s] and try again.", envVariableStr, envVariableName)
331290
}
332-
return compiledRegEx
333-
}
334-
335-
func getCleanUpFeedsOrTerminateExecution() bool {
336-
cleanUpCaiFeeds, exists := os.LookupEnv(CleanUpCaiFeeds)
337-
if !exists {
338-
logger.Fatalf("Clean up CAI Feeds environment variable [%s] not set, set the environment variable and try again.", CleanUpCaiFeeds)
339-
}
340-
result, err := strconv.ParseBool(cleanUpCaiFeeds)
341-
if err != nil {
342-
logger.Fatalf("Invalid Clean up CAI Feeds value [%s], specify correct value for environment variable [%s] and try again.", cleanUpCaiFeeds, CleanUpCaiFeeds)
343-
}
344-
return result
291+
return intValue
345292
}
346293

347294
func getCorrectFolderIdOrTerminateExecution() string {
@@ -353,6 +300,21 @@ func getCorrectFolderIdOrTerminateExecution() string {
353300
return targetFolderIdString
354301
}
355302

303+
func getBillingAccountOrTerminateExecution() string {
304+
billingAccountVal := os.Getenv(BillingAccount)
305+
if billingAccountVal == "" {
306+
if cleanUpBillingSinks {
307+
logger.Fatal("If billing account sink clean up is enabled, billing account id should not be empty, specify correct value and try again.")
308+
}
309+
return billingAccountVal
310+
}
311+
matched, err := regexp.MatchString(billingAccountRegex, billingAccountVal)
312+
if err != nil || !matched {
313+
logger.Fatalf("Invalid billing account id [%s], specify correct value and try again.", billingAccountVal)
314+
}
315+
return billingAccountVal
316+
}
317+
356318
func getCorrectOrganizationIdOrTerminateExecution() string {
357319
targetOrganizationIdString := os.Getenv(TargetOrganizationId)
358320
matched, err := regexp.MatchString(targetOrganizationRegexp, targetOrganizationIdString)
@@ -410,6 +372,15 @@ func getTagValuesServiceOrTerminateExecution(ctx context.Context, client *http.C
410372
return cloudResourceManagerService.TagValues
411373
}
412374

375+
func getBillingAccountSinkServiceOrTerminateExecution(ctx context.Context, client *http.Client) *logging.BillingAccountsSinksService {
376+
loggingService, err := logging.NewService(ctx, option.WithHTTPClient(client))
377+
if err != nil {
378+
logger.Fatalf("Failed to get Logging Sink Service with error [%s], terminate execution", err.Error())
379+
}
380+
logger.Println("Got Logging Sink Service")
381+
return loggingService.BillingAccounts.Sinks
382+
}
383+
413384
func getSCCNotificationServiceOrTerminateExecution(ctx context.Context, client *http.Client) *securitycenter.Client {
414385
logger.Println("Try to get SCC Notification Service")
415386
securitycenterClient, err := securitycenter.NewClient(ctx)
@@ -458,6 +429,7 @@ func invoke(ctx context.Context) {
458429
sccService := getSCCNotificationServiceOrTerminateExecution(ctx, client)
459430
tagValuesService := getTagValuesServiceOrTerminateExecution(ctx, client)
460431
feedsService := getAssetServiceOrTerminateExecution(ctx, client)
432+
billingSinkService := getBillingAccountSinkServiceOrTerminateExecution(ctx, client)
461433
firewallPoliciesService := getFirewallPoliciesServiceOrTerminateExecution(ctx, client)
462434
endpointService := getServiceManagementServiceOrTerminateExecution(ctx, client)
463435

@@ -480,6 +452,15 @@ func invoke(ctx context.Context) {
480452
return tagKeyCreatedAt.Before(resourceCreationCutoff)
481453
}
482454

455+
billingSinkAgeFilter := func(logSink *logging.LogSink) bool {
456+
createdAt, err := time.Parse(time.RFC3339, logSink.CreateTime)
457+
if err != nil {
458+
logger.Printf("Failed to parse CreateTime for tagKey [%s], skipping it, error [%s]", logSink.ResourceName, err.Error())
459+
return false
460+
}
461+
return createdAt.Before(resourceCreationCutoff)
462+
}
463+
483464
projectDeleteRequestedFilter := func(projectID string) bool {
484465
p, err := cloudResourceManagerService.Projects.Get(projectID).Context(ctx).Do()
485466
if err != nil {
@@ -586,6 +567,24 @@ func invoke(ctx context.Context) {
586567
}
587568
}
588569

570+
removeBillingSinks := func(billing string) {
571+
logger.Printf("Try to remove billing account log sinks from billing account [%s]", billing)
572+
parent := fmt.Sprintf("billingAccounts/%s", billing)
573+
sinkList, err := billingSinkService.List(parent).PageSize(billingSinksPageSize).Context(ctx).Do()
574+
if err != nil {
575+
logger.Printf("Failed to list billing account log sinks from billing account [%s], error [%s]", billing, err.Error())
576+
return
577+
}
578+
for _, sink := range sinkList.Sinks {
579+
if sink.Name != "_Required" && sink.Name != "_Default" && billingSinkAgeFilter(sink) && checkIfNameIncluded(sink.ResourceName, targetBillingSinks) {
580+
_, err = billingSinkService.Delete(sink.ResourceName).Context(ctx).Do()
581+
if err != nil {
582+
logger.Printf("Failed to delete billing account log sink [%s] from billing account [%s], error [%s]", sink.ResourceName, billing, err.Error())
583+
}
584+
}
585+
}
586+
}
587+
589588
removeFirewallPolicies := func(folder string) {
590589
logger.Printf("Try to remove Firewall Policies from folder [%s]", folder)
591590
firewallPolicyList, err := firewallPoliciesService.List().ParentId(folder).Context(ctx).Do()
@@ -743,6 +742,10 @@ func invoke(ctx context.Context) {
743742
if cleanUpCaiFeeds {
744743
removeFeedsByName(organizationId)
745744
}
745+
746+
if cleanUpBillingSinks {
747+
removeBillingSinks(billingAccount)
748+
}
746749
}
747750

748751
func CleanUpProjects(ctx context.Context, m PubSubMessage) error {

0 commit comments

Comments
 (0)