mirror of
https://github.com/Quiq/docker-registry-ui.git
synced 2025-07-16 23:31:16 +00:00
Add -purge-from-repos="repo1,repo2,..." and -disable-count-tags options
This commit is contained in:
parent
8a48bd4e8b
commit
f91c3b9aca
@ -1,5 +1,10 @@
|
|||||||
## Changelog
|
## 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)
|
### 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.
|
* 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.
|
||||||
|
12
main.go
12
main.go
@ -23,12 +23,14 @@ func main() {
|
|||||||
var (
|
var (
|
||||||
a apiClient
|
a apiClient
|
||||||
|
|
||||||
configFile, loggingLevel string
|
configFile, loggingLevel, purgeFromRepos string
|
||||||
purgeTags, purgeDryRun bool
|
disableCountTags, purgeTags, purgeDryRun bool
|
||||||
)
|
)
|
||||||
flag.StringVar(&configFile, "config-file", "config.yml", "path to the config file")
|
flag.StringVar(&configFile, "config-file", "config.yml", "path to the config file")
|
||||||
flag.StringVar(&loggingLevel, "log-level", "info", "logging level")
|
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.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.BoolVar(&purgeDryRun, "dry-run", false, "dry-run for purging task, does not delete anything")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
@ -50,7 +52,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
purgeFunc := func() {
|
purgeFunc := func() {
|
||||||
registry.PurgeOldTags(a.client, a.config.PurgeConfig)
|
registry.PurgeOldTags(a.client, a.config.PurgeConfig, purgeFromRepos)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute CLI task and exit.
|
// Execute CLI task and exit.
|
||||||
@ -69,7 +71,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Count tags in background.
|
// 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" {
|
if a.config.EventDatabaseDriver != "sqlite3" && a.config.EventDatabaseDriver != "mysql" {
|
||||||
panic(fmt.Errorf("event_database_driver should be either sqlite3 or mysql"))
|
panic(fmt.Errorf("event_database_driver should be either sqlite3 or mysql"))
|
||||||
|
@ -17,6 +17,8 @@ import (
|
|||||||
|
|
||||||
const userAgent = "docker-registry-ui"
|
const userAgent = "docker-registry-ui"
|
||||||
|
|
||||||
|
var paginationRegex = regexp.MustCompile("^<(.*?)>;.*$")
|
||||||
|
|
||||||
// Client main class.
|
// Client main class.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
url string
|
url string
|
||||||
@ -120,6 +122,8 @@ func (c *Client) getToken(scope string) string {
|
|||||||
|
|
||||||
// callRegistry make an HTTP request to retrieve data from Docker registry.
|
// callRegistry make an HTTP request to retrieve data from Docker registry.
|
||||||
func (c *Client) callRegistry(uri, scope, manifestFormat string) (string, gorequest.Response) {
|
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)
|
acceptHeader := fmt.Sprintf("application/vnd.docker.distribution.%s+json", manifestFormat)
|
||||||
authHeader := ""
|
authHeader := ""
|
||||||
if c.authURL != "" {
|
if c.authURL != "" {
|
||||||
@ -176,31 +180,25 @@ func (c *Client) Repositories(useCache bool) map[string][]string {
|
|||||||
c.mux.Lock()
|
c.mux.Lock()
|
||||||
defer c.mux.Unlock()
|
defer c.mux.Unlock()
|
||||||
|
|
||||||
linkRegexp := regexp.MustCompile("^<(.*?)>;.*$")
|
|
||||||
scope := "registry:catalog:*"
|
scope := "registry:catalog:*"
|
||||||
uri := "/v2/_catalog"
|
uri := "/v2/_catalog"
|
||||||
tmp := map[string][]string{}
|
tmp := map[string][]string{}
|
||||||
|
count := 0
|
||||||
for {
|
for {
|
||||||
data, resp := c.callRegistry(uri, scope, "manifest.v2")
|
data, resp := c.callRegistry(uri, scope, "manifest.v2")
|
||||||
if data == "" {
|
if data == "" {
|
||||||
c.repos = tmp
|
break
|
||||||
return c.repos
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, r := range gjson.Get(data, "repositories").Array() {
|
for _, r := range gjson.Get(data, "repositories").Array() {
|
||||||
namespace := "library"
|
namespace, repo := SplitRepoPath(r.String())
|
||||||
repo := r.String()
|
|
||||||
if strings.Contains(repo, "/") {
|
|
||||||
f := strings.SplitN(repo, "/", 2)
|
|
||||||
namespace = f[0]
|
|
||||||
repo = f[1]
|
|
||||||
}
|
|
||||||
tmp[namespace] = append(tmp[namespace], repo)
|
tmp[namespace] = append(tmp[namespace], repo)
|
||||||
|
count++
|
||||||
}
|
}
|
||||||
|
|
||||||
// pagination
|
// pagination
|
||||||
linkHeader := resp.Header.Get("Link")
|
linkHeader := resp.Header.Get("Link")
|
||||||
link := linkRegexp.FindStringSubmatch(linkHeader)
|
link := paginationRegex.FindStringSubmatch(linkHeader)
|
||||||
if len(link) == 2 {
|
if len(link) == 2 {
|
||||||
// update uri and query next page
|
// update uri and query next page
|
||||||
uri = link[1]
|
uri = link[1]
|
||||||
@ -210,6 +208,7 @@ func (c *Client) Repositories(useCache bool) map[string][]string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.repos = tmp
|
c.repos = tmp
|
||||||
|
c.logger.Debugf("Refreshed the catalog of %d repositories.", count)
|
||||||
return c.repos
|
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.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)
|
time.Sleep(time.Duration(interval) * time.Minute)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -58,3 +59,15 @@ func ItemInSlice(item string, slice []string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
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
|
||||||
|
}
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
@ -48,7 +49,7 @@ func (p timeSlice) Swap(i, j int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PurgeOldTags purge old tags.
|
// PurgeOldTags purge old tags.
|
||||||
func PurgeOldTags(client *Client, config *PurgeTagsConfig) {
|
func PurgeOldTags(client *Client, config *PurgeTagsConfig, purgeFromRepos string) {
|
||||||
logger := SetupLogging("registry.tasks.PurgeOldTags")
|
logger := SetupLogging("registry.tasks.PurgeOldTags")
|
||||||
|
|
||||||
var keepTagsFromFile gjson.Result
|
var keepTagsFromFile gjson.Result
|
||||||
@ -72,9 +73,19 @@ func PurgeOldTags(client *Client, config *PurgeTagsConfig) {
|
|||||||
logger.Warn("Dry-run mode enabled.")
|
logger.Warn("Dry-run mode enabled.")
|
||||||
dryRunText = "skipped"
|
dryRunText = "skipped"
|
||||||
}
|
}
|
||||||
logger.Info("Scanning registry for repositories, tags and their creation dates...")
|
|
||||||
catalog := client.Repositories(true)
|
catalog := map[string][]string{}
|
||||||
// catalog := map[string][]string{"library": []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()
|
now := time.Now().UTC()
|
||||||
repos := map[string]timeSlice{}
|
repos := map[string]timeSlice{}
|
||||||
count := 0
|
count := 0
|
||||||
@ -97,6 +108,10 @@ func PurgeOldTags(client *Client, config *PurgeTagsConfig) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
created := gjson.Get(gjson.Get(infoV1, "history.0.v1Compatibility").String(), "created").Time()
|
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})
|
repos[repo] = append(repos[repo], tagData{name: tag, created: created})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,7 +107,7 @@
|
|||||||
</table>
|
</table>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if not isDigest}}
|
{{if not isDigest && layersV1}}
|
||||||
<h4>Image History <!-- Manifest v2 schema 1--></h4>
|
<h4>Image History <!-- Manifest v2 schema 1--></h4>
|
||||||
{{range index, layer := layersV1}}
|
{{range index, layer := layersV1}}
|
||||||
<table class="table table-striped table-bordered">
|
<table class="table table-striped table-bordered">
|
||||||
|
Loading…
Reference in New Issue
Block a user