diff --git a/.gitignore b/.gitignore
index 7d21d57..59cebc7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
config-dev.yml
data/registry_events.db
vendor/
+*.swp
diff --git a/Dockerfile b/Dockerfile
index 01841a9..da1026c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -6,8 +6,11 @@ RUN apk update && \
WORKDIR /opt/src
ADD events events
ADD registry registry
-ADD *.go go.mod go.sum ./
+ADD go.mod go.sum ./
+RUN go mod download
+
+ADD *.go ./
RUN go test -v ./registry && \
go build -o /opt/docker-registry-ui *.go
diff --git a/main.go b/main.go
index c76183b..c4f55bb 100644
--- a/main.go
+++ b/main.go
@@ -7,6 +7,7 @@ import (
"net/http"
"net/url"
"os"
+ "regexp"
"strings"
"github.com/CloudyKit/jet"
@@ -52,6 +53,21 @@ type apiClient struct {
config configData
}
+type breadCrumb struct {
+ segment string
+ path string
+}
+
+func getBreadCrumbs(path string) []breadCrumb {
+ ret := []breadCrumb{}
+ segments := strings.Split(path, "/")
+ for i := 0; i < len(segments); i++ {
+ e := breadCrumb{segment: segments[i], path: strings.Join(segments[0:i+1], "/")}
+ ret = append(ret, e)
+ }
+ return ret
+}
+
func main() {
var (
a apiClient
@@ -149,13 +165,10 @@ func main() {
e.File("/favicon.ico", "static/favicon.ico")
e.Static(a.config.BasePath+"/static", "static")
if a.config.BasePath != "" {
- e.GET(a.config.BasePath, a.viewRepositories)
+ e.GET(a.config.BasePath, a.dispatchRequest)
}
- e.GET(a.config.BasePath+"/", a.viewRepositories)
- e.GET(a.config.BasePath+"/:namespace", a.viewRepositories)
- e.GET(a.config.BasePath+"/:namespace/:repo", a.viewTags)
- e.GET(a.config.BasePath+"/:namespace/:repo/:tag", a.viewTagInfo)
- e.GET(a.config.BasePath+"/:namespace/:repo/:tag/delete", a.deleteTag)
+ e.GET(a.config.BasePath+"/", a.dispatchRequest)
+ e.GET(a.config.BasePath+"/*", a.dispatchRequest)
e.GET(a.config.BasePath+"/events", a.viewLog)
// Protected event listener.
@@ -170,58 +183,65 @@ func main() {
e.Logger.Fatal(e.Start(a.config.ListenAddr))
}
-func (a *apiClient) viewRepositories(c echo.Context) error {
- namespace := c.Param("namespace")
- if namespace == "" {
- namespace = "library"
+func (a *apiClient) dispatchRequest(c echo.Context) error {
+ path := c.Request().URL.Path
+ if strings.HasPrefix(path, a.config.BasePath) {
+ path = path[len(a.config.BasePath):]
+ }
+ if strings.HasPrefix(path, "/") {
+ path = path[1:]
}
- repos, _ := a.client.Repositories(true)[namespace]
- data := jet.VarMap{}
- data.Set("namespace", namespace)
- data.Set("namespaces", a.client.Namespaces())
- data.Set("repos", repos)
- data.Set("tagCounts", a.client.TagCounts())
-
- return c.Render(http.StatusOK, "repositories.html", data)
+ segments := strings.Split(path, "/")
+ manifestRequest, _ := regexp.MatchString(".*/manifests/[^/]*$", path)
+ manifestDeleteRequest, _ := regexp.MatchString(".*/manifests/[^/]*/delete$", path)
+ if manifestRequest {
+ repoPath := strings.Join(segments[0:len(segments)-2], "/")
+ tagName := segments[len(segments)-1]
+ return a.viewTagInfo(c, repoPath, tagName)
+ } else if manifestDeleteRequest {
+ repoPath := strings.Join(segments[0:len(segments)-3], "/")
+ tagName := segments[len(segments)-2]
+ return a.deleteTag(c, repoPath, tagName)
+ } else {
+ return a.listRepo(c, path)
+ }
}
-func (a *apiClient) viewTags(c echo.Context) error {
- namespace := c.Param("namespace")
- repo := c.Param("repo")
- repoPath := repo
- if namespace != "library" {
- repoPath = fmt.Sprintf("%s/%s", namespace, repo)
- }
-
+func (a *apiClient) listRepo(c echo.Context, repoPath string) error {
tags := a.client.Tags(repoPath)
+ repos := a.client.RepositoriesList(true)
+
+ filterExpression := repoPath
+ if !strings.HasSuffix(repoPath, "/") && repoPath != "" {
+ filterExpression += "/"
+ }
+ matching_repos := registry.FilterStringSlice(repos, func(s string) bool {
+ return strings.HasPrefix(s, filterExpression) && len(s) > len(repoPath)
+ })
deleteAllowed := a.checkDeletePermission(c.Request().Header.Get("X-WEBAUTH-USER"))
data := jet.VarMap{}
- data.Set("namespace", namespace)
- data.Set("repo", repo)
+ data.Set("repoPath", repoPath)
+ data.Set("breadCrumbs", getBreadCrumbs(repoPath))
data.Set("tags", tags)
+ data.Set("repos", matching_repos)
data.Set("deleteAllowed", deleteAllowed)
+ data.Set("tagCounts", a.client.TagCounts())
repoPath, _ = url.PathUnescape(repoPath)
data.Set("events", a.eventListener.GetEvents(repoPath))
- return c.Render(http.StatusOK, "tags.html", data)
+ return c.Render(http.StatusOK, "list.html", data)
}
-func (a *apiClient) viewTagInfo(c echo.Context) error {
- namespace := c.Param("namespace")
- repo := c.Param("repo")
- tag := c.Param("tag")
- repoPath := repo
- if namespace != "library" {
- repoPath = fmt.Sprintf("%s/%s", namespace, repo)
- }
+func (a *apiClient) viewTagInfo(c echo.Context, repoPath string, tag string) error {
+ repo := repoPath
// Retrieve full image info from various versions of manifests
sha256, infoV1, infoV2 := a.client.TagInfo(repoPath, tag, false)
sha256list, manifests := a.client.ManifestList(repoPath, tag)
if (infoV1 == "" || infoV2 == "") && len(manifests) == 0 {
- return c.Redirect(http.StatusSeeOther, fmt.Sprintf("%s/%s/%s", a.config.BasePath, namespace, repo))
+ return c.Redirect(http.StatusSeeOther, fmt.Sprintf("%s/%s", a.config.BasePath, repo))
}
created := gjson.Get(gjson.Get(infoV1, "history.0.v1Compatibility").String(), "created").String()
@@ -277,7 +297,7 @@ func (a *apiClient) viewTagInfo(c echo.Context) error {
r["size"] = dSize
// Create link here because there is a bug with jet template when referencing a value by map key in the "if" condition under "range".
if r["mediaType"] == "application/vnd.docker.distribution.manifest.v2+json" {
- r["digest"] = fmt.Sprintf(`%s`, a.config.BasePath, namespace, repo, r["digest"], r["digest"])
+ r["digest"] = fmt.Sprintf(`%s`, a.config.BasePath, repo, r["digest"], r["digest"])
}
} else {
// Sub-image of the cache type.
@@ -289,10 +309,10 @@ func (a *apiClient) viewTagInfo(c echo.Context) error {
// Populate template vars
data := jet.VarMap{}
- data.Set("namespace", namespace)
data.Set("repo", repo)
data.Set("tag", tag)
data.Set("repoPath", repoPath)
+ data.Set("breadCrumbs", getBreadCrumbs(repoPath))
data.Set("sha256", sha256)
data.Set("imageSize", imageSize)
data.Set("created", created)
@@ -305,20 +325,12 @@ func (a *apiClient) viewTagInfo(c echo.Context) error {
return c.Render(http.StatusOK, "tag_info.html", data)
}
-func (a *apiClient) deleteTag(c echo.Context) error {
- namespace := c.Param("namespace")
- repo := c.Param("repo")
- tag := c.Param("tag")
- repoPath := repo
- if namespace != "library" {
- repoPath = fmt.Sprintf("%s/%s", namespace, repo)
- }
-
+func (a *apiClient) deleteTag(c echo.Context, repoPath string, tag string) error {
if a.checkDeletePermission(c.Request().Header.Get("X-WEBAUTH-USER")) {
a.client.DeleteTag(repoPath, tag)
}
- return c.Redirect(http.StatusSeeOther, fmt.Sprintf("%s/%s/%s", a.config.BasePath, namespace, repo))
+ return c.Redirect(http.StatusSeeOther, fmt.Sprintf("%s/%s", a.config.BasePath, repoPath))
}
// checkDeletePermission check if tag deletion is allowed whether by anyone or permitted users.
diff --git a/registry/client.go b/registry/client.go
index eba7049..bee36f2 100644
--- a/registry/client.go
+++ b/registry/client.go
@@ -5,7 +5,6 @@ import (
"crypto/tls"
"fmt"
"regexp"
- "sort"
"strings"
"sync"
"time"
@@ -27,7 +26,7 @@ type Client struct {
logger *logrus.Entry
mux sync.Mutex
tokens map[string]string
- repos map[string][]string
+ repos []string
tagCounts map[string]int
authURL string
}
@@ -43,7 +42,7 @@ func NewClient(url string, verifyTLS bool, username, password string) *Client {
request: gorequest.New().TLSClientConfig(&tls.Config{InsecureSkipVerify: !verifyTLS}),
logger: SetupLogging("registry.client"),
tokens: map[string]string{},
- repos: map[string][]string{},
+ repos: []string{},
tagCounts: map[string]int{},
}
resp, _, errs := c.request.Get(c.url+"/v2/").
@@ -153,21 +152,7 @@ func (c *Client) callRegistry(uri, scope, manifestFormat string) (string, gorequ
return data, resp
}
-// Namespaces list repo namespaces.
-func (c *Client) Namespaces() []string {
- namespaces := make([]string, 0, len(c.repos))
- for k := range c.repos {
- namespaces = append(namespaces, k)
- }
- if !ItemInSlice("library", namespaces) {
- namespaces = append(namespaces, "library")
- }
- sort.Strings(namespaces)
- return namespaces
-}
-
-// Repositories list repos by namespaces where 'library' is the default one.
-func (c *Client) Repositories(useCache bool) map[string][]string {
+func (c *Client) RepositoriesList(useCache bool) []string {
// Return from cache if available.
if len(c.repos) > 0 && useCache {
return c.repos
@@ -179,7 +164,7 @@ func (c *Client) Repositories(useCache bool) map[string][]string {
linkRegexp := regexp.MustCompile("^<(.*?)>;.*$")
scope := "registry:catalog:*"
uri := "/v2/_catalog"
- c.repos = map[string][]string{}
+ c.repos = []string{}
for {
data, resp := c.callRegistry(uri, scope, "manifest.v2")
if data == "" {
@@ -187,14 +172,7 @@ func (c *Client) Repositories(useCache bool) map[string][]string {
}
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]
- }
- c.repos[namespace] = append(c.repos[namespace], repo)
+ c.repos = append(c.repos, r.String())
}
// pagination
@@ -274,15 +252,9 @@ func (c *Client) CountTags(interval uint8) {
for {
start := time.Now()
c.logger.Info("[CountTags] Calculating image tags...")
- catalog := c.Repositories(false)
- for n, repos := range catalog {
- for _, r := range repos {
- repoPath := r
- if n != "library" {
- repoPath = fmt.Sprintf("%s/%s", n, r)
- }
- c.tagCounts[fmt.Sprintf("%s/%s", n, r)] = len(c.Tags(repoPath))
- }
+ catalog := c.RepositoriesList(false)
+ for _, repoPath := range catalog {
+ c.tagCounts[repoPath] = len(c.Tags(repoPath))
}
c.logger.Infof("[CountTags] Job complete (%v).", time.Now().Sub(start))
time.Sleep(time.Duration(interval) * time.Minute)
@@ -307,10 +279,7 @@ func (c *Client) DeleteTag(repo, tag string) {
if len(errs) > 0 {
c.logger.Error(errs[0])
} else {
- // Returns 202 on success.
- if !strings.Contains(repo, "/") {
- c.tagCounts["library/"+repo]--
- } else {
+ if c.tagCounts[repo] > 0 {
c.tagCounts[repo]--
}
c.logger.Infof("DELETE %s (tag:%s) %s", uri, tag, resp.Status)
diff --git a/registry/common.go b/registry/common.go
index c60919e..87385bc 100644
--- a/registry/common.go
+++ b/registry/common.go
@@ -58,3 +58,12 @@ func ItemInSlice(item string, slice []string) bool {
}
return false
}
+
+func FilterStringSlice(ss []string, test func(string) bool) (ret []string) {
+ for _, s := range ss {
+ if test(s) {
+ ret = append(ret, s)
+ }
+ }
+ return
+}
diff --git a/registry/tasks.go b/registry/tasks.go
index c11cefe..6364410 100644
--- a/registry/tasks.go
+++ b/registry/tasks.go
@@ -44,40 +44,31 @@ func PurgeOldTags(client *Client, purgeDryRun bool, purgeTagsKeepDays, purgeTags
dryRunText = "skipped"
}
logger.Info("Scanning registry for repositories, tags and their creation dates...")
- catalog := client.Repositories(true)
- // catalog := map[string][]string{"library": []string{""}}
+ catalog := client.RepositoriesList(true)
now := time.Now().UTC()
repos := map[string]timeSlice{}
- count := 0
- for namespace := range catalog {
- count = count + len(catalog[namespace])
- for _, repo := range catalog[namespace] {
- if namespace != "library" {
- repo = fmt.Sprintf("%s/%s", namespace, repo)
- }
-
- tags := client.Tags(repo)
- logger.Infof("[%s] scanning %d tags...", repo, len(tags))
- if len(tags) == 0 {
+ for _, repo := range catalog {
+ tags := client.Tags(repo)
+ logger.Infof("[%s] scanning %d tags...", repo, len(tags))
+ if len(tags) == 0 {
+ continue
+ }
+ for _, tag := range tags {
+ _, infoV1, _ := client.TagInfo(repo, tag, true)
+ if infoV1 == "" {
+ logger.Errorf("[%s] missing manifest v1 for tag %s", repo, tag)
continue
}
- for _, tag := range tags {
- _, infoV1, _ := client.TagInfo(repo, tag, true)
- if infoV1 == "" {
- logger.Errorf("[%s] missing manifest v1 for tag %s", repo, tag)
- continue
- }
- created := gjson.Get(gjson.Get(infoV1, "history.0.v1Compatibility").String(), "created").Time()
- repos[repo] = append(repos[repo], tagData{name: tag, created: created})
- }
+ created := gjson.Get(gjson.Get(infoV1, "history.0.v1Compatibility").String(), "created").Time()
+ repos[repo] = append(repos[repo], tagData{name: tag, created: created})
}
}
- logger.Infof("Scanned %d repositories.", count)
+ logger.Infof("Scanned %d repositories.", len(catalog))
logger.Info("Filtering out tags for purging...")
purgeTags := map[string][]string{}
keepTags := map[string][]string{}
- count = 0
+ count := 0
for _, repo := range SortedMapKeys(repos) {
// Sort tags by "created" from newest to oldest.
sortedTags := make(timeSlice, 0, len(repos[repo]))
diff --git a/templates/tags.html b/templates/list.html
similarity index 63%
rename from templates/tags.html
rename to templates/list.html
index 8704a03..ae02397 100644
--- a/templates/tags.html
+++ b/templates/list.html
@@ -6,14 +6,18 @@
{{end}}
@@ -33,27 +36,43 @@
{{block body()}}
- {{ registryHost }}
- {{if namespace != "library"}}
- - {{ namespace }}
+ {{range crumb := breadCrumbs}}
+ - {{ crumb.segment }}
{{end}}
- - {{ repo|url_decode }}
- Tag Name |
+ Name |
+ Tag count |
+ Type |
{{range tag := tags}}
- {{ tag }}
+ {{ tag }}
{{if deleteAllowed}}
- Delete
+ Delete
{{end}}
|
+ n/a |
+ Tags |
+
+ {{end}}
+ {{range repo := repos}}
+
+
+ {{noprefix_repo := repo[len(repoPath):]}}
+ {{if noprefix_repo[0:1] == "/"}}
+ {{noprefix_repo = noprefix_repo[1:]}}
+ {{end}}
+ {{ noprefix_repo }}
+ |
+ {{ tagCounts[repo] }} |
+ Repositories |
{{end}}
diff --git a/templates/repositories.html b/templates/repositories.html
deleted file mode 100644
index fd7ac5d..0000000
--- a/templates/repositories.html
+++ /dev/null
@@ -1,68 +0,0 @@
-{{extends "base.html"}}
-
-{{block head()}}
-
-{{end}}
-
-{{block body()}}
-
-
-
-
-
-
- - {{ registryHost }}
- {{if namespace != "library"}}
- - {{ namespace }}
- {{end}}
-
-
-
-
-
- Repository |
- Tags |
-
-
-
- {{range repo := repos}}
- {{if !isset(tagCounts[namespace+"/"+repo]) || (isset(tagCounts[namespace+"/"+repo]) && tagCounts[namespace+"/"+repo] > 0)}}
-
- {{ repo }} |
- {{ tagCounts[namespace+"/"+repo] }} |
-
- {{end}}
- {{end}}
-
-
-{{end}}
diff --git a/templates/tag_info.html b/templates/tag_info.html
index 718f3d9..9a7f258 100644
--- a/templates/tag_info.html
+++ b/templates/tag_info.html
@@ -5,11 +5,10 @@
{{block body()}}
- {{ registryHost }}
- {{if namespace != "library"}}
- - {{ namespace }}
+ {{range crumb := breadCrumbs}}
+ - {{ crumb.segment }}
{{end}}
- - {{ repo|url_decode }}
- - {{ tag }}
+ - {{ tag }}
Image Details