mirror of
https://github.com/Quiq/docker-registry-ui.git
synced 2025-09-30 14:57:26 +00:00
Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
bbefd03dbd | ||
|
f7e40bece8 | ||
|
b49076db7c | ||
|
c7c3a815fb | ||
|
929daf733f | ||
|
86ee1d56bd | ||
|
d29c24a78f |
16
CHANGELOG.md
16
CHANGELOG.md
@@ -1,12 +1,24 @@
|
||||
## Changelog
|
||||
|
||||
## 0.10.2 (2024-05-31)
|
||||
|
||||
* Fix repo tag count when a repo name is a prefix for another repo name(s)
|
||||
* Allow to override any config option via environment variables using SECTION_KEY_NAME syntax, e.g.
|
||||
LISTEN_ADDR, PERFORMANCE_TAGS_COUNT_REFRESH_INTERVAL, REGISTRY_HOSTNAME etc.
|
||||
|
||||
## 0.10.1 (2024-04-19)
|
||||
|
||||
* Rename cmd flag `-purge-from-repos` to `-purge-include-repos` to purge tags only for the specified repositories.
|
||||
* Add a new cmd flag `-purge-exclude-repos` to skip the specified repositories from the tag purging.
|
||||
* Make image column clickable in Event Log.
|
||||
|
||||
### 0.10.0 (2024-04-16)
|
||||
|
||||
**JUST BREAKING CHANGES**
|
||||
|
||||
* We have made a full rewrite. Over 6 years many things have been changed.
|
||||
* Renamed github/dockerhub repo from docker-registry-ui -> registry-ui
|
||||
* Switched from doing raw http calls to github.com/google/go-containerregistry
|
||||
* Switched from doing raw http calls to `github.com/google/go-containerregistry`
|
||||
* URLs and links are now matching the image references, no more "library" or other weird URL parts.
|
||||
* No namespace or only 2-level deep concept
|
||||
* An arbitrary repository levels are supported
|
||||
@@ -18,7 +30,7 @@
|
||||
* Changed format of config.yml but the same concept is preserved
|
||||
* Event listener path has been changed from /api/events to /event-receiver and you may need to update your registry config
|
||||
* Removed built-in cron scheduler for purging tags, please use the normal cron :)
|
||||
* Now you can now tune the refresh of catalog and separately refresh of tag counting, disable them etc.
|
||||
* Now you can tune the refresh of catalog and separately refresh of tag counting, disable them etc.
|
||||
* Everything has been made better! :)
|
||||
|
||||
### 0.9.7 (2024-02-21)
|
||||
|
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.22.1-alpine3.19 as builder
|
||||
FROM golang:1.22.3-alpine3.19 as builder
|
||||
|
||||
RUN apk update && \
|
||||
apk add ca-certificates git bash gcc musl-dev
|
||||
|
@@ -27,6 +27,9 @@ Docker images [quiq/registry-ui](https://hub.docker.com/r/quiq/registry-ui/tags/
|
||||
|
||||
The configuration is stored in `config.yml` and the options are self-descriptive.
|
||||
|
||||
You can override any config option via environment variables using SECTION_KEY_NAME syntax,
|
||||
e.g. `LISTEN_ADDR`, `PERFORMANCE_TAGS_COUNT_REFRESH_INTERVAL`, `REGISTRY_HOSTNAME` etc.
|
||||
|
||||
### Run UI
|
||||
|
||||
docker run -d -p 8000:8000 -v /local/config.yml:/opt/config.yml:ro quiq/registry-ui
|
||||
|
2
go.mod
2
go.mod
@@ -21,7 +21,7 @@ require (
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect
|
||||
github.com/docker/cli v26.0.0+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker v26.0.0+incompatible // indirect
|
||||
github.com/docker/docker v26.0.2+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.8.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
|
4
go.sum
4
go.sum
@@ -14,8 +14,8 @@ github.com/docker/cli v26.0.0+incompatible h1:90BKrx1a1HKYpSnnBFR6AgDq/FqkHxwlUy
|
||||
github.com/docker/cli v26.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v26.0.0+incompatible h1:Ng2qi+gdKADUa/VM+6b6YaY2nlZhk/lVJiKR/2bMudU=
|
||||
github.com/docker/docker v26.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v26.0.2+incompatible h1:yGVmKUFGgcxA6PXWAokO0sQL22BrQ67cgVjko8tGdXE=
|
||||
github.com/docker/docker v26.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo=
|
||||
github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
|
||||
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
|
||||
|
9
main.go
9
main.go
@@ -24,15 +24,16 @@ func main() {
|
||||
a apiClient
|
||||
|
||||
configFile, loggingLevel string
|
||||
purgeFromRepos string
|
||||
purgeTags, purgeDryRun bool
|
||||
purgeIncludeRepos, purgeExcludeRepos string
|
||||
)
|
||||
flag.StringVar(&configFile, "config-file", "config.yml", "path to the config file")
|
||||
flag.StringVar(&loggingLevel, "log-level", "info", "logging level")
|
||||
|
||||
flag.BoolVar(&purgeTags, "purge-tags", false, "purge old tags instead of running a web server")
|
||||
flag.BoolVar(&purgeDryRun, "dry-run", false, "dry-run for purging task, does not delete anything")
|
||||
flag.StringVar(&purgeFromRepos, "purge-from-repos", "", "comma-separated list of repos to purge instead of all")
|
||||
flag.StringVar(&purgeIncludeRepos, "purge-include-repos", "", "comma-separated list of repos to purge tags from, otherwise all")
|
||||
flag.StringVar(&purgeExcludeRepos, "purge-exclude-repos", "", "comma-separated list of repos to skip from purging tags, otherwise none")
|
||||
flag.Parse()
|
||||
|
||||
// Setup logging
|
||||
@@ -50,13 +51,15 @@ func main() {
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("fatal error reading config file: %w", err))
|
||||
}
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
viper.AutomaticEnv()
|
||||
|
||||
// Init registry API client.
|
||||
a.client = registry.NewClient()
|
||||
|
||||
// Execute CLI task and exit.
|
||||
if purgeTags {
|
||||
registry.PurgeOldTags(a.client, purgeDryRun, purgeFromRepos)
|
||||
registry.PurgeOldTags(a.client, purgeDryRun, purgeIncludeRepos, purgeExcludeRepos)
|
||||
return
|
||||
}
|
||||
|
||||
|
@@ -272,8 +272,8 @@ func (c *Client) GetImageCreated(imageRef string) time.Time {
|
||||
return cfg.Created.Time
|
||||
}
|
||||
|
||||
// TagCounts return map with tag counts according to the provided list of repos/sub-repos etc.
|
||||
func (c *Client) TagCounts(repoPath string, repos []string) map[string]int {
|
||||
// SubRepoTagCounts return map with tag counts according to the provided list of repos/sub-repos etc.
|
||||
func (c *Client) SubRepoTagCounts(repoPath string, repos []string) map[string]int {
|
||||
counts := map[string]int{}
|
||||
for _, r := range repos {
|
||||
subRepo := r
|
||||
@@ -281,7 +281,7 @@ func (c *Client) TagCounts(repoPath string, repos []string) map[string]int {
|
||||
subRepo = repoPath + "/" + r
|
||||
}
|
||||
for k, v := range c.tagCounts {
|
||||
if strings.HasPrefix(k, subRepo) {
|
||||
if k == subRepo || strings.HasPrefix(k, subRepo+"/") {
|
||||
counts[subRepo] = counts[subRepo] + v
|
||||
}
|
||||
}
|
||||
|
@@ -42,11 +42,20 @@ func (p timeSlice) Swap(i, j int) {
|
||||
}
|
||||
|
||||
// PurgeOldTags purge old tags.
|
||||
func PurgeOldTags(client *Client, purgeDryRun bool, purgeFromRepos string) {
|
||||
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
|
||||
keepFromFile := viper.GetString("purge_tags.keep_from_file")
|
||||
if keepFromFile != "" {
|
||||
if _, err := os.Stat(keepFromFile); os.IsNotExist(err) {
|
||||
logger.Warnf("Cannot open %s: %s", keepFromFile, err)
|
||||
@@ -62,21 +71,25 @@ func PurgeOldTags(client *Client, purgeDryRun bool, purgeFromRepos string) {
|
||||
dataFromFile = gjson.ParseBytes(data)
|
||||
}
|
||||
|
||||
dryRunText := ""
|
||||
if purgeDryRun {
|
||||
logger.Warn("Dry-run mode enabled.")
|
||||
dryRunText = "skipped"
|
||||
}
|
||||
|
||||
catalog := []string{}
|
||||
if purgeFromRepos != "" {
|
||||
logger.Infof("Working on repositories [%s] to scan their tags and creation dates...", purgeFromRepos)
|
||||
catalog = append(catalog, strings.Split(purgeFromRepos, ",")...)
|
||||
if purgeIncludeRepos != "" {
|
||||
logger.Infof("Including repositories: %s", purgeIncludeRepos)
|
||||
catalog = append(catalog, strings.Split(purgeIncludeRepos, ",")...)
|
||||
} else {
|
||||
logger.Info("Scanning registry for repositories, tags and their creation dates...")
|
||||
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{}
|
||||
@@ -91,7 +104,7 @@ func PurgeOldTags(client *Client, purgeDryRun bool, purgeFromRepos string) {
|
||||
imageRef := repo + ":" + tag
|
||||
created := client.GetImageCreated(imageRef)
|
||||
if created.IsZero() {
|
||||
// Image manifest with zero creation time, e.g. cosign one
|
||||
// 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
|
||||
}
|
||||
@@ -100,16 +113,12 @@ func PurgeOldTags(client *Client, purgeDryRun bool, purgeFromRepos string) {
|
||||
}
|
||||
|
||||
logger.Infof("Scanned %d repositories.", len(catalog))
|
||||
|
||||
keepDays := viper.GetInt("purge_tags.keep_days")
|
||||
keepCount := viper.GetInt("purge_tags.keep_count")
|
||||
logger.Infof("Filtering out tags for purging: keep %d days, keep count %d", keepDays, keepCount)
|
||||
keepRegexp := viper.GetString("purge_tags.keep_regexp")
|
||||
if keepRegexp != "" {
|
||||
logger.Infof("Keeping tags matching regexp: %s", keepRegexp)
|
||||
}
|
||||
if keepFromFile != "" {
|
||||
logger.Infof("Keeping tags for repos from the file: %+v", dataFromFile)
|
||||
logger.Infof("Keeping tags from file: %+v", dataFromFile)
|
||||
}
|
||||
purgeTags := map[string][]string{}
|
||||
keepTags := map[string][]string{}
|
||||
|
@@ -26,7 +26,7 @@
|
||||
|
||||
<div style="padding: 10px 0; margin-bottom: 20px">
|
||||
<div style="text-align: center; color:darkgrey">
|
||||
Registry UI v{{version}} | <a href="https://quiq.com" target="_blank">Quiq Inc.</a>
|
||||
Registry UI v{{version}} | <a href="https://quiq.com" target="_blank">Quiq Inc.</a> | <a href="https://github.com/Quiq/registry-ui" target="_blank">Github</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -89,7 +89,7 @@
|
||||
{{end}} {* end tags *}
|
||||
|
||||
{{if eventsAllowed and isset(events) }}
|
||||
<h4>Latest activity</h4>
|
||||
<h4>Recent Activity</h4>
|
||||
<table id="datatable_events" class="table table-striped table-bordered">
|
||||
<thead bgcolor="#ddd">
|
||||
<tr>
|
||||
@@ -104,7 +104,7 @@
|
||||
{{range _, e := events}}
|
||||
<tr>
|
||||
<td>{{ e.Action }}</td>
|
||||
{{if hasPrefix(e.Tag,"sha256") }}
|
||||
{{if hasPrefix(e.Tag,"sha256:") }}
|
||||
<td title="{{ e.Tag }}">{{ e.Repository }}@{{ e.Tag[:19] }}...</td>
|
||||
{{else}}
|
||||
<td>{{ e.Repository }}:{{ e.Tag }}</td>
|
||||
|
@@ -72,10 +72,10 @@
|
||||
{{range _, e := events}}
|
||||
<tr>
|
||||
<td>{{ e.Action }}</td>
|
||||
{{if hasPrefix(e.Tag,"sha256") }}
|
||||
<td title="{{ e.Tag }}">{{ e.Repository }}@{{ e.Tag[:19] }}...</td>
|
||||
{{if hasPrefix(e.Tag,"sha256:") }}
|
||||
<td title="{{ e.Tag }}"><a href="{{ basePath }}/{{ e.Repository }}@{{ e.Tag }}">{{ e.Repository }}@{{ e.Tag[:19] }}...</a></td>
|
||||
{{else}}
|
||||
<td>{{ e.Repository }}:{{ e.Tag }}</td>
|
||||
<td><a href="{{ basePath }}/{{ e.Repository }}:{{ e.Tag }}">{{ e.Repository }}:{{ e.Tag }}</a></td>
|
||||
{{end}}
|
||||
<td>{{ e.IP }}</td>
|
||||
<td>{{ e.User }}</td>
|
||||
|
@@ -1,3 +1,3 @@
|
||||
package main
|
||||
|
||||
const version = "0.10.0"
|
||||
const version = "0.10.2"
|
||||
|
2
web.go
2
web.go
@@ -76,7 +76,7 @@ func (a *apiClient) viewCatalog(c echo.Context) error {
|
||||
}
|
||||
data.Set("repos", repos)
|
||||
data.Set("isCatalogReady", a.client.IsCatalogReady())
|
||||
data.Set("tagCounts", a.client.TagCounts(repoPath, repos))
|
||||
data.Set("tagCounts", a.client.SubRepoTagCounts(repoPath, repos))
|
||||
data.Set("tags", tags)
|
||||
if repoPath != "" && (len(repos) > 0 || len(tags) > 0) {
|
||||
// Do not show events in the root of catalog.
|
||||
|
Reference in New Issue
Block a user