diff --git a/CHANGELOG.md b/CHANGELOG.md index 3833338..34808bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## Changelog +### UNRELEASED + +* Add an option to disable counting of tags if it is very slow: `-disable-count-tags` +* Add an option to specify a comma-separated list of repos to purge: `-purge-from-repos` + ### 0.9.7 (2024-02-21) * Fix timezone support: now when running a container with `TZ` env var, e.g. "-e TZ=America/Los_Angeles", it will be reflected everywhere on UI. diff --git a/main.go b/main.go index 53c692c..c3de8be 100644 --- a/main.go +++ b/main.go @@ -23,12 +23,14 @@ func main() { var ( a apiClient - configFile, loggingLevel string - purgeTags, purgeDryRun bool + configFile, loggingLevel, purgeFromRepos string + disableCountTags, purgeTags, purgeDryRun bool ) flag.StringVar(&configFile, "config-file", "config.yml", "path to the config file") flag.StringVar(&loggingLevel, "log-level", "info", "logging level") + flag.BoolVar(&disableCountTags, "disable-count-tags", false, "disable counting of tags if it is very slow") flag.BoolVar(&purgeTags, "purge-tags", false, "purge old tags instead of running a web server") + flag.StringVar(&purgeFromRepos, "purge-from-repos", "", "comma-separated list of repos to purge instead of all") flag.BoolVar(&purgeDryRun, "dry-run", false, "dry-run for purging task, does not delete anything") flag.Parse() @@ -50,7 +52,7 @@ func main() { } purgeFunc := func() { - registry.PurgeOldTags(a.client, a.config.PurgeConfig) + registry.PurgeOldTags(a.client, a.config.PurgeConfig, purgeFromRepos) } // Execute CLI task and exit. @@ -69,7 +71,9 @@ func main() { } // Count tags in background. - go a.client.CountTags(a.config.CacheRefreshInterval) + if !disableCountTags { + go a.client.CountTags(a.config.CacheRefreshInterval) + } if a.config.EventDatabaseDriver != "sqlite3" && a.config.EventDatabaseDriver != "mysql" { panic(fmt.Errorf("event_database_driver should be either sqlite3 or mysql")) diff --git a/registry/client.go b/registry/client.go index e9a50a7..d8b4b22 100644 --- a/registry/client.go +++ b/registry/client.go @@ -17,6 +17,8 @@ import ( const userAgent = "docker-registry-ui" +var paginationRegex = regexp.MustCompile("^<(.*?)>;.*$") + // Client main class. type Client struct { url string @@ -120,6 +122,8 @@ func (c *Client) getToken(scope string) string { // callRegistry make an HTTP request to retrieve data from Docker registry. func (c *Client) callRegistry(uri, scope, manifestFormat string) (string, gorequest.Response) { + // TODO Support OCI manifest https://github.com/opencontainers/image-spec/blob/main/manifest.md + // acceptHeader := "application/vnd.oci.image.manifest.v1+json" acceptHeader := fmt.Sprintf("application/vnd.docker.distribution.%s+json", manifestFormat) authHeader := "" if c.authURL != "" { @@ -176,31 +180,25 @@ func (c *Client) Repositories(useCache bool) map[string][]string { c.mux.Lock() defer c.mux.Unlock() - linkRegexp := regexp.MustCompile("^<(.*?)>;.*$") scope := "registry:catalog:*" uri := "/v2/_catalog" tmp := map[string][]string{} + count := 0 for { data, resp := c.callRegistry(uri, scope, "manifest.v2") if data == "" { - c.repos = tmp - return c.repos + break } for _, r := range gjson.Get(data, "repositories").Array() { - namespace := "library" - repo := r.String() - if strings.Contains(repo, "/") { - f := strings.SplitN(repo, "/", 2) - namespace = f[0] - repo = f[1] - } + namespace, repo := SplitRepoPath(r.String()) tmp[namespace] = append(tmp[namespace], repo) + count++ } // pagination linkHeader := resp.Header.Get("Link") - link := linkRegexp.FindStringSubmatch(linkHeader) + link := paginationRegex.FindStringSubmatch(linkHeader) if len(link) == 2 { // update uri and query next page uri = link[1] @@ -210,6 +208,7 @@ func (c *Client) Repositories(useCache bool) map[string][]string { } } c.repos = tmp + c.logger.Debugf("Refreshed the catalog of %d repositories.", count) return c.repos } @@ -286,7 +285,7 @@ func (c *Client) CountTags(interval uint8) { c.tagCounts[fmt.Sprintf("%s/%s", n, r)] = len(c.Tags(repoPath)) } } - c.logger.Infof("[CountTags] Job complete (%v).", time.Now().Sub(start)) + c.logger.Infof("[CountTags] Job complete (%v).", time.Since(start)) time.Sleep(time.Duration(interval) * time.Minute) } } diff --git a/registry/common.go b/registry/common.go index c60919e..94d1e43 100644 --- a/registry/common.go +++ b/registry/common.go @@ -5,6 +5,7 @@ import ( "os" "reflect" "sort" + "strings" "time" "github.com/sirupsen/logrus" @@ -58,3 +59,15 @@ func ItemInSlice(item string, slice []string) bool { } return false } + +// Sprit repo path by namespace and repo name +func SplitRepoPath(repoPath string) (string, string) { + namespace := "library" + repo := repoPath + if strings.Contains(repoPath, "/") { + f := strings.SplitN(repoPath, "/", 2) + namespace = f[0] + repo = f[1] + } + return namespace, repo +} diff --git a/registry/tasks.go b/registry/tasks.go index 6d81485..a2d2a5f 100644 --- a/registry/tasks.go +++ b/registry/tasks.go @@ -6,6 +6,7 @@ import ( "os" "regexp" "sort" + "strings" "time" "github.com/tidwall/gjson" @@ -48,7 +49,7 @@ func (p timeSlice) Swap(i, j int) { } // PurgeOldTags purge old tags. -func PurgeOldTags(client *Client, config *PurgeTagsConfig) { +func PurgeOldTags(client *Client, config *PurgeTagsConfig, purgeFromRepos string) { logger := SetupLogging("registry.tasks.PurgeOldTags") var keepTagsFromFile gjson.Result @@ -72,9 +73,19 @@ func PurgeOldTags(client *Client, config *PurgeTagsConfig) { logger.Warn("Dry-run mode enabled.") dryRunText = "skipped" } - logger.Info("Scanning registry for repositories, tags and their creation dates...") - catalog := client.Repositories(true) - // catalog := map[string][]string{"library": []string{""}} + + catalog := map[string][]string{} + if purgeFromRepos != "" { + logger.Infof("Working on repositories [%s] to scan their tags and creation dates...", purgeFromRepos) + for _, p := range strings.Split(purgeFromRepos, ",") { + namespace, repo := SplitRepoPath(p) + catalog[namespace] = append(catalog[namespace], repo) + } + } else { + logger.Info("Scanning registry for repositories, tags and their creation dates...") + catalog = client.Repositories(true) + } + now := time.Now().UTC() repos := map[string]timeSlice{} count := 0 @@ -97,6 +108,10 @@ func PurgeOldTags(client *Client, config *PurgeTagsConfig) { continue } created := gjson.Get(gjson.Get(infoV1, "history.0.v1Compatibility").String(), "created").Time() + if created.IsZero() { + // OCI manifest w/o creation time or any other case with zero time + continue + } repos[repo] = append(repos[repo], tagData{name: tag, created: created}) } } diff --git a/templates/tag_info.html b/templates/tag_info.html index 344f5d8..be4f6f3 100644 --- a/templates/tag_info.html +++ b/templates/tag_info.html @@ -107,7 +107,7 @@ {{end}} -{{if not isDigest}} +{{if not isDigest && layersV1}}