blob: cedbd22f7164fd6dad2c20fda506b88d4c164583 [file] [log] [blame]
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.ParseInLocation(YMD_FORMAT, *olderThan, time.UTC)
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
}