mirror of
https://github.com/Quiq/docker-registry-ui.git
synced 2025-07-19 00:27:07 +00:00
184 lines
5.1 KiB
Go
184 lines
5.1 KiB
Go
package registry
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/spf13/viper"
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
type TagData struct {
|
|
name string
|
|
created time.Time
|
|
}
|
|
|
|
func (t TagData) String() string {
|
|
return fmt.Sprintf(`"%s <%s>"`, t.name, t.created.Format("2006-01-02 15:04:05"))
|
|
}
|
|
|
|
type timeSlice []TagData
|
|
|
|
func (p timeSlice) Len() int {
|
|
return len(p)
|
|
}
|
|
|
|
func (p timeSlice) Less(i, j int) bool {
|
|
// reverse sort tags on name if equal dates (OCI image case)
|
|
// see https://github.com/Quiq/registry-ui/pull/62
|
|
if p[i].created.Equal(p[j].created) {
|
|
return p[i].name > p[j].name
|
|
}
|
|
return p[i].created.After(p[j].created)
|
|
}
|
|
|
|
func (p timeSlice) Swap(i, j int) {
|
|
p[i], p[j] = p[j], p[i]
|
|
}
|
|
|
|
// PurgeOldTags purge old tags.
|
|
func PurgeOldTags(client *Client, purgeDryRun bool, purgeIncludeRepos, purgeExcludeRepos string) {
|
|
logger := SetupLogging("registry.tasks.PurgeOldTags")
|
|
keepDays := viper.GetInt("purge_tags.keep_days")
|
|
keepCount := viper.GetInt("purge_tags.keep_count")
|
|
keepRegexp := viper.GetString("purge_tags.keep_regexp")
|
|
keepFromFile := viper.GetString("purge_tags.keep_from_file")
|
|
|
|
dryRunText := ""
|
|
if purgeDryRun {
|
|
logger.Warn("Dry-run mode enabled.")
|
|
dryRunText = "skipped"
|
|
}
|
|
|
|
var dataFromFile gjson.Result
|
|
if keepFromFile != "" {
|
|
if _, err := os.Stat(keepFromFile); os.IsNotExist(err) {
|
|
logger.Warnf("Cannot open %s: %s", keepFromFile, err)
|
|
logger.Error("Not purging anything!")
|
|
return
|
|
}
|
|
data, err := os.ReadFile(keepFromFile)
|
|
if err != nil {
|
|
logger.Warnf("Cannot read %s: %s", keepFromFile, err)
|
|
logger.Error("Not purging anything!")
|
|
return
|
|
}
|
|
dataFromFile = gjson.ParseBytes(data)
|
|
}
|
|
|
|
catalog := []string{}
|
|
if purgeIncludeRepos != "" {
|
|
logger.Infof("Including repositories: %s", purgeIncludeRepos)
|
|
catalog = append(catalog, strings.Split(purgeIncludeRepos, ",")...)
|
|
} else {
|
|
client.RefreshCatalog()
|
|
catalog = client.GetRepos()
|
|
}
|
|
if purgeExcludeRepos != "" {
|
|
logger.Infof("Excluding repositories: %s", purgeExcludeRepos)
|
|
tmpCatalog := []string{}
|
|
for _, repo := range catalog {
|
|
if !ItemInSlice(repo, strings.Split(purgeExcludeRepos, ",")) {
|
|
tmpCatalog = append(tmpCatalog, repo)
|
|
}
|
|
}
|
|
catalog = tmpCatalog
|
|
}
|
|
logger.Infof("Working on repositories: %s", catalog)
|
|
|
|
now := time.Now().UTC()
|
|
repos := map[string]timeSlice{}
|
|
count := 0
|
|
for _, repo := range catalog {
|
|
tags := client.ListTags(repo)
|
|
if len(tags) == 0 {
|
|
continue
|
|
}
|
|
logger.Infof("[%s] scanning %d tags...", repo, len(tags))
|
|
for _, tag := range tags {
|
|
imageRef := repo + ":" + tag
|
|
created := client.GetImageCreated(imageRef)
|
|
if created.IsZero() {
|
|
// Image manifest with zero creation time, e.g. cosign w/o --record-creation-timestamp
|
|
logger.Debugf("[%s] tag with zero creation time: %s", repo, tag)
|
|
continue
|
|
}
|
|
repos[repo] = append(repos[repo], TagData{name: tag, created: created})
|
|
}
|
|
}
|
|
|
|
logger.Infof("Scanned %d repositories.", len(catalog))
|
|
logger.Infof("Filtering out tags for purging: keep %d days, keep count %d", keepDays, keepCount)
|
|
if keepRegexp != "" {
|
|
logger.Infof("Keeping tags matching regexp: %s", keepRegexp)
|
|
}
|
|
if keepFromFile != "" {
|
|
logger.Infof("Keeping tags from file: %+v", dataFromFile)
|
|
}
|
|
purgeTags := map[string][]string{}
|
|
keepTags := map[string][]string{}
|
|
count = 0
|
|
for _, repo := range SortedMapKeys(repos) {
|
|
// Sort tags by "created" from newest to oldest.
|
|
sort.Sort(repos[repo])
|
|
|
|
// Prep the list of tags to preserve if defined in the file
|
|
tagsFromFile := []string{}
|
|
for _, i := range dataFromFile.Get(repo).Array() {
|
|
tagsFromFile = append(tagsFromFile, i.String())
|
|
}
|
|
|
|
// Filter out tags
|
|
for _, tag := range repos[repo] {
|
|
daysOld := int(now.Sub(tag.created).Hours() / 24)
|
|
matchByRegexp := false
|
|
if keepRegexp != "" {
|
|
matchByRegexp, _ = regexp.MatchString(keepRegexp, tag.name)
|
|
}
|
|
|
|
if daysOld > keepDays && !matchByRegexp && !ItemInSlice(tag.name, tagsFromFile) {
|
|
purgeTags[repo] = append(purgeTags[repo], tag.name)
|
|
} else {
|
|
keepTags[repo] = append(keepTags[repo], tag.name)
|
|
}
|
|
}
|
|
|
|
// Keep minimal count of tags no matter how old they are.
|
|
if len(keepTags[repo]) < keepCount {
|
|
// At least "threshold"-"keep" but not more than available for "purge".
|
|
takeFromPurge := int(math.Min(float64(keepCount-len(keepTags[repo])), float64(len(purgeTags[repo]))))
|
|
keepTags[repo] = append(keepTags[repo], purgeTags[repo][:takeFromPurge]...)
|
|
purgeTags[repo] = purgeTags[repo][takeFromPurge:]
|
|
}
|
|
|
|
count = count + len(purgeTags[repo])
|
|
logger.Infof("[%s] All %d: %v", repo, len(repos[repo]), repos[repo])
|
|
logger.Infof("[%s] Keep %d: %v", repo, len(keepTags[repo]), keepTags[repo])
|
|
logger.Infof("[%s] Purge %d: %v", repo, len(purgeTags[repo]), purgeTags[repo])
|
|
}
|
|
|
|
logger.Infof("There are %d tags to purge.", count)
|
|
if count > 0 {
|
|
logger.Info("Purging old tags...")
|
|
}
|
|
|
|
for _, repo := range SortedMapKeys(purgeTags) {
|
|
if len(purgeTags[repo]) == 0 {
|
|
continue
|
|
}
|
|
logger.Infof("[%s] Purging %d tags... %s", repo, len(purgeTags[repo]), dryRunText)
|
|
if purgeDryRun {
|
|
continue
|
|
}
|
|
for _, tag := range purgeTags[repo] {
|
|
client.DeleteTag(repo, tag)
|
|
}
|
|
}
|
|
logger.Info("Done.")
|
|
}
|