Add script to delete old docker images
It takes a day as the "oldest image to keep around" and
supports a minimum number of images to keep around.
Bug: skia:
Change-Id: I2f236cb1e8e86fc5a8ea3c897a0ae3bd309699f0
Reviewed-on: https://skia-review.googlesource.com/c/180766
Commit-Queue: Kevin Lubick <kjlubick@google.com>
Reviewed-by: Kevin Lubick <kjlubick@google.com>
diff --git a/scripts/gcr_image_cleanup/gcr_image_cleanup.go b/scripts/gcr_image_cleanup/gcr_image_cleanup.go
new file mode 100644
index 0000000..8aa51b8
--- /dev/null
+++ b/scripts/gcr_image_cleanup/gcr_image_cleanup.go
@@ -0,0 +1,145 @@
+package main
+
+// This script will clear out Docker images in gcr.io that are older than the
+// specified date, leaving at least min_images remaining. The min_images
+// supersedes the age.
+
+import (
+ "bytes"
+ "context"
+ "flag"
+ "fmt"
+ "log"
+ "os"
+ "strconv"
+ "strings"
+ "time"
+
+ "go.skia.org/infra/go/exec"
+)
+
+var (
+ project = flag.String("project", "", "[REQUIRED] The GCP project to clean up images.")
+ olderThan = flag.String("older_than", "", "[REQUIRED] Date in YYYY-MM-DD of newest image to get rid of.")
+ minImages = flag.Int("min_images", 10, "Minimum number of images to keep around, ignoring age.")
+ dryRun = flag.Bool("dry_run", false, "Print out those images that would be deleted instead of actually deleting them.")
+)
+
+const YMD_FORMAT = "2006-01-02"
+
+func main() {
+ flag.Parse()
+ if *project == "" || *olderThan == "" {
+ fmt.Println("--project and --older_than are required")
+ flag.PrintDefaults()
+ os.Exit(1)
+ }
+
+ oldestDate, err := time.Parse(YMD_FORMAT, *olderThan)
+ if err != nil {
+ fmt.Println("Date must be in YYYY-MM-DD format")
+ os.Exit(1)
+ }
+
+ fmt.Println("Fetching images in project")
+ output := bytes.Buffer{}
+ err = exec.Run(context.Background(), &exec.Command{
+ Name: "gcloud",
+ Args: []string{"--project", *project, "container", "images", "list"},
+ Stdout: &output,
+ })
+ if err != nil {
+ fmt.Printf("Could not retrieve images: %s\n", err)
+ os.Exit(1)
+ }
+
+ images := strings.Split(strings.TrimSpace(output.String()), "\n")
+ fmt.Printf("Found %d images\n", len(images))
+ fmt.Println("Deleting old images")
+
+ for _, image := range images {
+ if !strings.HasPrefix(image, "gcr.io") {
+ // Skip the header and any footer
+ continue
+ }
+ olderThan := oldestDate
+
+ output := bytes.Buffer{}
+ err = exec.Run(context.Background(), &exec.Command{
+ Name: "gcloud",
+ Args: []string{"--project", *project, "container", "images", "list-tags",
+ image, "--sort-by=~TIMESTAMP", fmt.Sprintf("--limit=%d", *minImages),
+ "--format=csv(timestamp.year,timestamp.month,timestamp.day)"},
+ Stdout: &output,
+ })
+ if err != nil {
+ fmt.Printf("Could not retrieve image tags for %s: %s\n", image, err)
+ os.Exit(1)
+ }
+ // output now looks like
+ //year,month,day
+ // 2019,1,3
+ // 2019,1,2
+ // 2018,12,28
+
+ // trim off the header
+ newestDates := strings.Split(strings.TrimSpace(output.String()), "\n")[1:]
+ if len(newestDates) < *minImages {
+ fmt.Printf("%s has fewer than %d tags (%d), skipping\n", image, *minImages, len(newestDates))
+ continue
+ }
+ // Look at when nth newest image, if it is before the min timeline, we use
+ // 1 day before that nth newest image, just to be safe.
+ nthNewest := newestDates[len(newestDates)-1]
+ ymd := strings.Split(nthNewest, ",")
+ if altDate := time.Date(safeAtoI(ymd[0]), time.Month(safeAtoI(ymd[1])), safeAtoI(ymd[2]), 0, 0, 0, 0, time.UTC); altDate.Equal(olderThan) || altDate.Before(olderThan) {
+ olderThan = altDate.AddDate(0, 0, -1)
+ }
+
+ output = bytes.Buffer{}
+ err = exec.Run(context.Background(), &exec.Command{
+ Name: "gcloud",
+ Args: []string{"--project", *project, "container", "images", "list-tags",
+ image, "--sort-by=TIMESTAMP", "--limit=999999",
+ "--filter=timestamp.datetime < " + olderThan.Format(YMD_FORMAT),
+ "--format=get(digest)"},
+ Stdout: &output,
+ })
+ if err != nil {
+ fmt.Printf("Could not retrieve image tags for %s: %s\n", image, err)
+ os.Exit(1)
+ }
+ toDelete := strings.Split(strings.TrimSpace(output.String()), "\n")
+
+ fmt.Printf("=== Will delete %d containers from %s\n", len(toDelete), image)
+
+ for _, digest := range toDelete {
+ i := fmt.Sprintf("%s@%s", image, digest)
+ if *dryRun {
+ fmt.Println("dry run delete ", i)
+ } else {
+ err = exec.Run(context.Background(), &exec.Command{
+ Name: "gcloud",
+ Args: []string{"--project", *project, "container", "images", "delete",
+ "--quiet", "--force-delete-tags",
+ i},
+ LogStderr: true,
+ LogStdout: true,
+ })
+ if err != nil {
+ fmt.Printf("error while deleting %s, continuing anyway: %s\n", i, err)
+ } else {
+ fmt.Println("deleted ", i)
+ }
+ }
+ }
+ }
+}
+
+func safeAtoI(s string) int {
+ i, err := strconv.Atoi(s)
+ if err != nil {
+ log.Fatal(err)
+ }
+ return i
+}