| 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 |
| } |