/
reaper.go
275 lines (243 loc) · 9.01 KB
/
reaper.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package reaper
import (
"context"
"fmt"
"strings"
"time"
"github.com/googleinterns/cloudai-gcp-test-resource-reaper/pkg/clients"
"github.com/googleinterns/cloudai-gcp-test-resource-reaper/pkg/logger"
"github.com/googleinterns/cloudai-gcp-test-resource-reaper/pkg/resources"
"github.com/googleinterns/cloudai-gcp-test-resource-reaper/reaperconfig"
"github.com/robfig/cron/v3"
"google.golang.org/api/option"
)
// Reaper represents the resource reaper for a single GCP project. The reaper will
// run on a given schedule defined in cron time format.
type Reaper struct {
UUID string
ProjectID string
Watchlist []*resources.WatchedResource
Schedule cron.Schedule
config *reaperconfig.ReaperConfig
lastRun time.Time
*Clock
}
type Clock struct {
instant time.Time
}
func (c *Clock) Now() time.Time {
if c == nil {
return time.Now()
}
return c.instant
}
func (reaper *Reaper) FreezeClock(instant time.Time) {
if reaper.Clock == nil {
reaper.Clock = &Clock{}
}
reaper.Clock.instant = instant
}
// NewReaper constructs a new reaper.
func NewReaper() *Reaper {
return &Reaper{}
}
// RunOnSchedule updates the reaper's watchlist and runs a sweep if the current time is equal to or after
// the next schedule run time.
func (reaper *Reaper) RunOnSchedule(ctx context.Context, clientOptions ...option.ClientOption) bool {
nextRun := reaper.Schedule.Next(reaper.lastRun)
if reaper.lastRun.IsZero() || reaper.Clock.Now().After(nextRun) || reaper.Clock.Now().Equal(nextRun) {
logger.Logf("Running reaper with UUID: %s\n", reaper.UUID)
reaper.GetResources(ctx, clientOptions...)
logger.Logf("Reaper %s sweeping through the following resources: %s", reaper.UUID, reaper.WatchlistString())
reaper.SweepThroughResources(ctx, clientOptions...)
reaper.lastRun = reaper.Clock.Now()
return true
}
return false
}
// SweepThroughResources goes through all the resources in the reaper's Watchlist, and for each resource
// determines if it needs to be deleted. The necessary resources are deleted from GCP and the reaper's
// Watchlist is updated accordingly.
func (reaper *Reaper) SweepThroughResources(ctx context.Context, clientOptions ...option.ClientOption) {
var updatedWatchlist []*resources.WatchedResource
for _, watchedResource := range reaper.Watchlist {
if watchedResource.IsReadyForDeletion() {
resourceClient, err := getAuthedClient(ctx, reaper, watchedResource.Type, clientOptions...)
if err != nil {
logger.Error(err)
continue
}
if err := resourceClient.DeleteResource(reaper.ProjectID, watchedResource.Resource); err != nil {
deleteError := fmt.Errorf(
"%s client failed to delete resource %s with the following error: %s",
watchedResource.Type.String(), watchedResource.Name, err.Error(),
)
logger.Error(deleteError)
continue
}
logger.Logf(
"Deleted %s resource %s in zone %s\n",
watchedResource.Type.String(), watchedResource.Name, watchedResource.Zone,
)
} else {
updatedWatchlist = append(updatedWatchlist, watchedResource)
}
}
reaper.Watchlist = updatedWatchlist
}
// UpdateReaperConfig updates the reaper from a given ReaperConfig proto.
func (reaper *Reaper) UpdateReaperConfig(config *reaperconfig.ReaperConfig) error {
reaper.config = config
reaper.ProjectID = config.GetProjectId()
reaper.UUID = config.GetUuid()
parsedSchedule, err := parseSchedule(config.GetSchedule())
reaper.Schedule = parsedSchedule
return err
}
// GetResources gets all the GCP resources defined in the ReaperConfig, and adds them to the
// reaper's Watchlist. Note, if the same resource is referenced by multiple ResourceConfigs,
// then the TTL of that resource will be the one that deletes the resource the latest.
func (reaper *Reaper) GetResources(ctx context.Context, clientOptions ...option.ClientOption) {
var newWatchlist []*resources.WatchedResource
newWatchedResources := make(map[string]map[string]*resources.WatchedResource)
resourceConfigs := reaper.config.GetResources()
for _, resourceConfig := range resourceConfigs {
resourceType := resourceConfig.GetResourceType()
resourceClient, err := getAuthedClient(ctx, reaper, resourceType, clientOptions...)
if err != nil {
logger.Error(err)
continue
}
filteredResources, err := resourceClient.GetResources(reaper.ProjectID, resourceConfig)
if err != nil {
getResourcesError := fmt.Errorf(
"%s client failed to get resources with the following error: %s",
resourceType.String(), err.Error(),
)
logger.Error(getResourcesError)
continue
}
watchedResources := resources.CreateWatchlist(filteredResources, resourceConfig.GetTtl())
// Check for duplicates. If one exists, update the TTL by the max
for _, resource := range watchedResources {
if _, isZoneWatched := newWatchedResources[resource.Zone]; !isZoneWatched {
newWatchedResources[resource.Zone] = make(map[string]*resources.WatchedResource)
}
if _, alreadyWatched := newWatchedResources[resource.Zone][resource.Name]; alreadyWatched {
newTTL, err := maxTTL(resource, newWatchedResources[resource.Zone][resource.Name])
if err != nil {
logger.Error(err)
continue
}
newWatchedResources[resource.Zone][resource.Name].TTL = newTTL
} else {
newWatchedResources[resource.Zone][resource.Name] = resource
}
}
}
// Converting resources map into list
for zone := range newWatchedResources {
for _, resource := range newWatchedResources[zone] {
newWatchlist = append(newWatchlist, resource)
}
}
reaper.Watchlist = newWatchlist
}
// WatchlistString returns a near sting of the reaper's Watchlist.
func (reaper *Reaper) WatchlistString() string {
var watchlistBuidler strings.Builder
for _, resource := range reaper.Watchlist {
watchlistBuidler.WriteString(fmt.Sprintf("%s in %s, ", resource.Name, resource.Zone))
}
watchlist := watchlistBuidler.String()
if len(watchlist) > 0 {
watchlist = watchlist[:len(watchlist)-2]
}
return watchlist
}
// NewReaperConfig constructs a new ReaperConfig.
func NewReaperConfig(resources []*reaperconfig.ResourceConfig, schedule, projectID, uuid string) *reaperconfig.ReaperConfig {
return &reaperconfig.ReaperConfig{
Resources: resources,
Schedule: schedule,
ProjectId: projectID,
Uuid: uuid,
}
}
// NewResourceConfig constructs a new ResourceConfig.
func NewResourceConfig(resourceType reaperconfig.ResourceType, zones []string, nameFilter, skipFilter, ttl string) *reaperconfig.ResourceConfig {
return &reaperconfig.ResourceConfig{
ResourceType: resourceType,
NameFilter: nameFilter,
SkipFilter: skipFilter,
Zones: zones,
Ttl: ttl,
}
}
// getAuthedClient is a helper method for getting an authenticated GCP client for a given resource type.
func getAuthedClient(ctx context.Context, reaper *Reaper, resourceType reaperconfig.ResourceType, clientOptions ...option.ClientOption) (clients.Client, error) {
resourceClient, err := clients.NewClient(resourceType)
if err != nil {
clientError := fmt.Errorf(
"%s client failed with the following error: %s",
resourceType.String(), err.Error(),
)
return nil, clientError
}
err = resourceClient.Auth(ctx, clientOptions...)
if err != nil {
authError := fmt.Errorf(
"%s client failed authenticate with the following error: %s",
resourceType.String(), err.Error(),
)
return nil, authError
}
return resourceClient, nil
}
// FreezeTime is a helper method for freezing the clocks of all resources in a reaper's
// Watchlist to a given instant.
func (reaper *Reaper) FreezeTime(instant time.Time) {
for idx := range reaper.Watchlist {
reaper.Watchlist[idx].FreezeClock(instant)
}
}
// maxTTL is a helper function to determine which watched resource will be deleted later,
// and return its TTL.
func maxTTL(resourceA, resourceB *resources.WatchedResource) (string, error) {
timeA, err := resourceA.GetDeletionTime()
if err != nil {
return "", fmt.Errorf("Parsing TTL failed with following error: %s", err.Error())
}
timeB, err := resourceB.GetDeletionTime()
if err != nil {
return "", fmt.Errorf("Parsing TTL failed with following error: %s", err.Error())
}
if timeA.After(timeB) {
return resourceA.TTL, nil
} else {
return resourceB.TTL, nil
}
}
// parseSchedule parses the cron time string that defined the reaper's
// run schedule, and either returns a Schedule struct, or nil if the
// schedule string is malformed.
func parseSchedule(schedule string) (cron.Schedule, error) {
parsedSchedule, err := cron.ParseStandard(schedule)
if err != nil {
return nil, err
}
return parsedSchedule, nil
}