blob: d7f0382b6918d386313c166e191a554d07b7d802 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
"context"
"sort"
"strings"
"github.com/dustin/go-humanize"
"github.com/maruel/subcommands"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"infra/cmd/gaedeploy/gcloud"
)
var cmdCleanup = &subcommands.Command{
UsageLine: "cleanup [...]",
ShortDesc: "deletes oldest GAE versions with no traffic assignment",
LongDesc: `Deletes oldest GAE versions with no traffic assignment.
Lists all deployed versions of all modules and deletes inactive ones, i.e. ones
that are assigned no traffic in the traffic splitting configuration.
Keeps all versions that receive traffic (regardless of their age), and also up
to -keep of most recently deployed inactive versions.
`,
CommandRun: func() subcommands.CommandRun {
c := &cmdCleanupRun{}
c.init()
return c
},
}
type cmdCleanupRun struct {
commandBase
keep int // -keep flag
}
func (c *cmdCleanupRun) init() {
c.commandBase.init(c.exec, extraFlags{
appID: true,
dryRun: true,
})
c.Flags.IntVar(&c.keep, "keep", 5, "How many inactive versions to keep (default is 5).")
}
func (c *cmdCleanupRun) exec(ctx context.Context) error {
if c.keep < 0 {
return errBadFlag("-keep", "must be non-negative")
}
logging.Infof(ctx, "App ID: %s", c.appID)
logging.Infof(ctx, "Keep: %d", c.keep)
mods, err := gcloud.List(ctx, c.appID, "")
if err != nil {
return errors.Annotate(err, "failed to app versions").Err()
}
// Visit modules in deterministic order, cleanup each one separately.
names := make([]string, 0, len(mods))
for mod := range mods {
names = append(names, mod)
}
sort.Strings(names)
for _, mod := range names {
if err := cleanupVersions(ctx, c.appID, mod, mods[mod], c.keep, c.dryRun); err != nil {
return errors.Annotate(err, "failed to cleanup module %q", mod).Err()
}
}
return nil
}
// cleanupVersions cleans up old inactive version of some single module.
func cleanupVersions(ctx context.Context, appID, module string, vers gcloud.Versions, keepNum int, dryRun bool) error {
logging.Infof(ctx, "Module %q:", module)
list := make([]gcloud.Version, 0, len(vers))
for _, ver := range vers {
list = append(list, ver)
}
// Most recent first.
sort.Slice(list, func(i, j int) bool {
return list[i].LastDeployedTime.After(list[j].LastDeployedTime)
})
// Log all versions along with the verdict for them, to get the whole picture.
var versionsToDelete []string
for _, v := range list {
verdict := ""
if v.TrafficSplit == 0.0 {
if keepNum == 0 {
versionsToDelete = append(versionsToDelete, v.Name)
verdict = "deleting: too old"
} else {
keepNum--
verdict = "keeping: recent enough"
}
} else {
verdict = "keeping: receives traffic"
}
padding := 16 - len(v.Name)
if padding < 0 {
padding = 0
}
logging.Infof(ctx, " %s %s %.1f traffic, deployed %s, %s",
v.Name, strings.Repeat(" ", padding),
v.TrafficSplit, humanize.Time(v.LastDeployedTime), verdict)
}
if len(versionsToDelete) == 0 {
logging.Infof(ctx, "Nothing to cleanup in %s.", module)
return nil
}
logging.Infof(ctx, "Deleting %s...", strings.Join(versionsToDelete, ", "))
return gcloud.Run(ctx, append([]string{
"app", "versions", "delete",
"--quiet", // disable interactive prompts
"--project", appID,
"--service", module,
}, versionsToDelete...), "", nil, dryRun)
}