mirror of
https://github.com/Quiq/docker-registry-ui.git
synced 2025-07-18 16:21:24 +00:00
Added support for nested namespaces
This commit is contained in:
parent
c424093470
commit
ae0e8f570d
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
config-dev.yml
|
config-dev.yml
|
||||||
data/registry_events.db
|
data/registry_events.db
|
||||||
vendor/
|
vendor/
|
||||||
|
*.swp
|
||||||
|
@ -6,8 +6,11 @@ RUN apk update && \
|
|||||||
WORKDIR /opt/src
|
WORKDIR /opt/src
|
||||||
ADD events events
|
ADD events events
|
||||||
ADD registry registry
|
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 && \
|
RUN go test -v ./registry && \
|
||||||
go build -o /opt/docker-registry-ui *.go
|
go build -o /opt/docker-registry-ui *.go
|
||||||
|
|
||||||
|
112
main.go
112
main.go
@ -7,6 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/CloudyKit/jet"
|
"github.com/CloudyKit/jet"
|
||||||
@ -52,6 +53,21 @@ type apiClient struct {
|
|||||||
config configData
|
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() {
|
func main() {
|
||||||
var (
|
var (
|
||||||
a apiClient
|
a apiClient
|
||||||
@ -149,13 +165,10 @@ func main() {
|
|||||||
e.File("/favicon.ico", "static/favicon.ico")
|
e.File("/favicon.ico", "static/favicon.ico")
|
||||||
e.Static(a.config.BasePath+"/static", "static")
|
e.Static(a.config.BasePath+"/static", "static")
|
||||||
if a.config.BasePath != "" {
|
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+"/", a.dispatchRequest)
|
||||||
e.GET(a.config.BasePath+"/:namespace", a.viewRepositories)
|
e.GET(a.config.BasePath+"/*", a.dispatchRequest)
|
||||||
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+"/events", a.viewLog)
|
e.GET(a.config.BasePath+"/events", a.viewLog)
|
||||||
|
|
||||||
// Protected event listener.
|
// Protected event listener.
|
||||||
@ -170,58 +183,65 @@ func main() {
|
|||||||
e.Logger.Fatal(e.Start(a.config.ListenAddr))
|
e.Logger.Fatal(e.Start(a.config.ListenAddr))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *apiClient) viewRepositories(c echo.Context) error {
|
func (a *apiClient) dispatchRequest(c echo.Context) error {
|
||||||
namespace := c.Param("namespace")
|
path := c.Request().URL.Path
|
||||||
if namespace == "" {
|
if strings.HasPrefix(path, a.config.BasePath) {
|
||||||
namespace = "library"
|
path = path[len(a.config.BasePath):]
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(path, "/") {
|
||||||
|
path = path[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
repos, _ := a.client.Repositories(true)[namespace]
|
segments := strings.Split(path, "/")
|
||||||
data := jet.VarMap{}
|
manifestRequest, _ := regexp.MatchString(".*/manifests/[^/]*$", path)
|
||||||
data.Set("namespace", namespace)
|
manifestDeleteRequest, _ := regexp.MatchString(".*/manifests/[^/]*/delete$", path)
|
||||||
data.Set("namespaces", a.client.Namespaces())
|
if manifestRequest {
|
||||||
data.Set("repos", repos)
|
repoPath := strings.Join(segments[0:len(segments)-2], "/")
|
||||||
data.Set("tagCounts", a.client.TagCounts())
|
tagName := segments[len(segments)-1]
|
||||||
|
return a.viewTagInfo(c, repoPath, tagName)
|
||||||
return c.Render(http.StatusOK, "repositories.html", data)
|
} 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 {
|
func (a *apiClient) listRepo(c echo.Context, repoPath string) error {
|
||||||
namespace := c.Param("namespace")
|
|
||||||
repo := c.Param("repo")
|
|
||||||
repoPath := repo
|
|
||||||
if namespace != "library" {
|
|
||||||
repoPath = fmt.Sprintf("%s/%s", namespace, repo)
|
|
||||||
}
|
|
||||||
|
|
||||||
tags := a.client.Tags(repoPath)
|
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"))
|
deleteAllowed := a.checkDeletePermission(c.Request().Header.Get("X-WEBAUTH-USER"))
|
||||||
|
|
||||||
data := jet.VarMap{}
|
data := jet.VarMap{}
|
||||||
data.Set("namespace", namespace)
|
data.Set("repoPath", repoPath)
|
||||||
data.Set("repo", repo)
|
data.Set("breadCrumbs", getBreadCrumbs(repoPath))
|
||||||
data.Set("tags", tags)
|
data.Set("tags", tags)
|
||||||
|
data.Set("repos", matching_repos)
|
||||||
data.Set("deleteAllowed", deleteAllowed)
|
data.Set("deleteAllowed", deleteAllowed)
|
||||||
|
data.Set("tagCounts", a.client.TagCounts())
|
||||||
repoPath, _ = url.PathUnescape(repoPath)
|
repoPath, _ = url.PathUnescape(repoPath)
|
||||||
data.Set("events", a.eventListener.GetEvents(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 {
|
func (a *apiClient) viewTagInfo(c echo.Context, repoPath string, tag string) error {
|
||||||
namespace := c.Param("namespace")
|
repo := repoPath
|
||||||
repo := c.Param("repo")
|
|
||||||
tag := c.Param("tag")
|
|
||||||
repoPath := repo
|
|
||||||
if namespace != "library" {
|
|
||||||
repoPath = fmt.Sprintf("%s/%s", namespace, repo)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve full image info from various versions of manifests
|
// Retrieve full image info from various versions of manifests
|
||||||
sha256, infoV1, infoV2 := a.client.TagInfo(repoPath, tag, false)
|
sha256, infoV1, infoV2 := a.client.TagInfo(repoPath, tag, false)
|
||||||
sha256list, manifests := a.client.ManifestList(repoPath, tag)
|
sha256list, manifests := a.client.ManifestList(repoPath, tag)
|
||||||
if (infoV1 == "" || infoV2 == "") && len(manifests) == 0 {
|
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()
|
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
|
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".
|
// 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" {
|
if r["mediaType"] == "application/vnd.docker.distribution.manifest.v2+json" {
|
||||||
r["digest"] = fmt.Sprintf(`<a href="%s/%s/%s/%s">%s</a>`, a.config.BasePath, namespace, repo, r["digest"], r["digest"])
|
r["digest"] = fmt.Sprintf(`<a href="%s/%s/%s">%s</a>`, a.config.BasePath, repo, r["digest"], r["digest"])
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Sub-image of the cache type.
|
// Sub-image of the cache type.
|
||||||
@ -289,10 +309,10 @@ func (a *apiClient) viewTagInfo(c echo.Context) error {
|
|||||||
|
|
||||||
// Populate template vars
|
// Populate template vars
|
||||||
data := jet.VarMap{}
|
data := jet.VarMap{}
|
||||||
data.Set("namespace", namespace)
|
|
||||||
data.Set("repo", repo)
|
data.Set("repo", repo)
|
||||||
data.Set("tag", tag)
|
data.Set("tag", tag)
|
||||||
data.Set("repoPath", repoPath)
|
data.Set("repoPath", repoPath)
|
||||||
|
data.Set("breadCrumbs", getBreadCrumbs(repoPath))
|
||||||
data.Set("sha256", sha256)
|
data.Set("sha256", sha256)
|
||||||
data.Set("imageSize", imageSize)
|
data.Set("imageSize", imageSize)
|
||||||
data.Set("created", created)
|
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)
|
return c.Render(http.StatusOK, "tag_info.html", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *apiClient) deleteTag(c echo.Context) error {
|
func (a *apiClient) deleteTag(c echo.Context, repoPath string, tag string) 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if a.checkDeletePermission(c.Request().Header.Get("X-WEBAUTH-USER")) {
|
if a.checkDeletePermission(c.Request().Header.Get("X-WEBAUTH-USER")) {
|
||||||
a.client.DeleteTag(repoPath, tag)
|
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.
|
// checkDeletePermission check if tag deletion is allowed whether by anyone or permitted users.
|
||||||
|
@ -5,7 +5,6 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@ -27,7 +26,7 @@ type Client struct {
|
|||||||
logger *logrus.Entry
|
logger *logrus.Entry
|
||||||
mux sync.Mutex
|
mux sync.Mutex
|
||||||
tokens map[string]string
|
tokens map[string]string
|
||||||
repos map[string][]string
|
repos []string
|
||||||
tagCounts map[string]int
|
tagCounts map[string]int
|
||||||
authURL string
|
authURL string
|
||||||
}
|
}
|
||||||
@ -43,7 +42,7 @@ func NewClient(url string, verifyTLS bool, username, password string) *Client {
|
|||||||
request: gorequest.New().TLSClientConfig(&tls.Config{InsecureSkipVerify: !verifyTLS}),
|
request: gorequest.New().TLSClientConfig(&tls.Config{InsecureSkipVerify: !verifyTLS}),
|
||||||
logger: SetupLogging("registry.client"),
|
logger: SetupLogging("registry.client"),
|
||||||
tokens: map[string]string{},
|
tokens: map[string]string{},
|
||||||
repos: map[string][]string{},
|
repos: []string{},
|
||||||
tagCounts: map[string]int{},
|
tagCounts: map[string]int{},
|
||||||
}
|
}
|
||||||
resp, _, errs := c.request.Get(c.url+"/v2/").
|
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
|
return data, resp
|
||||||
}
|
}
|
||||||
|
|
||||||
// Namespaces list repo namespaces.
|
func (c *Client) RepositoriesList(useCache bool) []string {
|
||||||
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 {
|
|
||||||
// Return from cache if available.
|
// Return from cache if available.
|
||||||
if len(c.repos) > 0 && useCache {
|
if len(c.repos) > 0 && useCache {
|
||||||
return c.repos
|
return c.repos
|
||||||
@ -179,7 +164,7 @@ func (c *Client) Repositories(useCache bool) map[string][]string {
|
|||||||
linkRegexp := regexp.MustCompile("^<(.*?)>;.*$")
|
linkRegexp := regexp.MustCompile("^<(.*?)>;.*$")
|
||||||
scope := "registry:catalog:*"
|
scope := "registry:catalog:*"
|
||||||
uri := "/v2/_catalog"
|
uri := "/v2/_catalog"
|
||||||
c.repos = map[string][]string{}
|
c.repos = []string{}
|
||||||
for {
|
for {
|
||||||
data, resp := c.callRegistry(uri, scope, "manifest.v2")
|
data, resp := c.callRegistry(uri, scope, "manifest.v2")
|
||||||
if data == "" {
|
if data == "" {
|
||||||
@ -187,14 +172,7 @@ func (c *Client) Repositories(useCache bool) map[string][]string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, r := range gjson.Get(data, "repositories").Array() {
|
for _, r := range gjson.Get(data, "repositories").Array() {
|
||||||
namespace := "library"
|
c.repos = append(c.repos, r.String())
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// pagination
|
// pagination
|
||||||
@ -274,15 +252,9 @@ func (c *Client) CountTags(interval uint8) {
|
|||||||
for {
|
for {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
c.logger.Info("[CountTags] Calculating image tags...")
|
c.logger.Info("[CountTags] Calculating image tags...")
|
||||||
catalog := c.Repositories(false)
|
catalog := c.RepositoriesList(false)
|
||||||
for n, repos := range catalog {
|
for _, repoPath := range catalog {
|
||||||
for _, r := range repos {
|
c.tagCounts[repoPath] = len(c.Tags(repoPath))
|
||||||
repoPath := r
|
|
||||||
if n != "library" {
|
|
||||||
repoPath = fmt.Sprintf("%s/%s", n, r)
|
|
||||||
}
|
|
||||||
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.Now().Sub(start))
|
||||||
time.Sleep(time.Duration(interval) * time.Minute)
|
time.Sleep(time.Duration(interval) * time.Minute)
|
||||||
@ -307,10 +279,7 @@ func (c *Client) DeleteTag(repo, tag string) {
|
|||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
c.logger.Error(errs[0])
|
c.logger.Error(errs[0])
|
||||||
} else {
|
} else {
|
||||||
// Returns 202 on success.
|
if c.tagCounts[repo] > 0 {
|
||||||
if !strings.Contains(repo, "/") {
|
|
||||||
c.tagCounts["library/"+repo]--
|
|
||||||
} else {
|
|
||||||
c.tagCounts[repo]--
|
c.tagCounts[repo]--
|
||||||
}
|
}
|
||||||
c.logger.Infof("DELETE %s (tag:%s) %s", uri, tag, resp.Status)
|
c.logger.Infof("DELETE %s (tag:%s) %s", uri, tag, resp.Status)
|
||||||
|
@ -58,3 +58,12 @@ func ItemInSlice(item string, slice []string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FilterStringSlice(ss []string, test func(string) bool) (ret []string) {
|
||||||
|
for _, s := range ss {
|
||||||
|
if test(s) {
|
||||||
|
ret = append(ret, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
@ -44,40 +44,31 @@ func PurgeOldTags(client *Client, purgeDryRun bool, purgeTagsKeepDays, purgeTags
|
|||||||
dryRunText = "skipped"
|
dryRunText = "skipped"
|
||||||
}
|
}
|
||||||
logger.Info("Scanning registry for repositories, tags and their creation dates...")
|
logger.Info("Scanning registry for repositories, tags and their creation dates...")
|
||||||
catalog := client.Repositories(true)
|
catalog := client.RepositoriesList(true)
|
||||||
// catalog := map[string][]string{"library": []string{""}}
|
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
repos := map[string]timeSlice{}
|
repos := map[string]timeSlice{}
|
||||||
count := 0
|
for _, repo := range catalog {
|
||||||
for namespace := range catalog {
|
tags := client.Tags(repo)
|
||||||
count = count + len(catalog[namespace])
|
logger.Infof("[%s] scanning %d tags...", repo, len(tags))
|
||||||
for _, repo := range catalog[namespace] {
|
if len(tags) == 0 {
|
||||||
if namespace != "library" {
|
continue
|
||||||
repo = fmt.Sprintf("%s/%s", namespace, repo)
|
}
|
||||||
}
|
for _, tag := range tags {
|
||||||
|
_, infoV1, _ := client.TagInfo(repo, tag, true)
|
||||||
tags := client.Tags(repo)
|
if infoV1 == "" {
|
||||||
logger.Infof("[%s] scanning %d tags...", repo, len(tags))
|
logger.Errorf("[%s] missing manifest v1 for tag %s", repo, tag)
|
||||||
if len(tags) == 0 {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, tag := range tags {
|
created := gjson.Get(gjson.Get(infoV1, "history.0.v1Compatibility").String(), "created").Time()
|
||||||
_, infoV1, _ := client.TagInfo(repo, tag, true)
|
repos[repo] = append(repos[repo], tagData{name: tag, created: created})
|
||||||
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})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Infof("Scanned %d repositories.", count)
|
logger.Infof("Scanned %d repositories.", len(catalog))
|
||||||
logger.Info("Filtering out tags for purging...")
|
logger.Info("Filtering out tags for purging...")
|
||||||
purgeTags := map[string][]string{}
|
purgeTags := map[string][]string{}
|
||||||
keepTags := map[string][]string{}
|
keepTags := map[string][]string{}
|
||||||
count = 0
|
count := 0
|
||||||
for _, repo := range SortedMapKeys(repos) {
|
for _, repo := range SortedMapKeys(repos) {
|
||||||
// Sort tags by "created" from newest to oldest.
|
// Sort tags by "created" from newest to oldest.
|
||||||
sortedTags := make(timeSlice, 0, len(repos[repo]))
|
sortedTags := make(timeSlice, 0, len(repos[repo]))
|
||||||
|
@ -6,14 +6,18 @@
|
|||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
$('#datatable').DataTable({
|
$('#datatable').DataTable({
|
||||||
"pageLength": 10,
|
"pageLength": 25,
|
||||||
"order": [[ 0, 'desc' ]],
|
"order": [[ 0, 'desc' ]],
|
||||||
"stateSave": true,
|
"stateSave": true,
|
||||||
columnDefs: [
|
columnDefs: [
|
||||||
{ type: 'natural', targets: 0 }
|
{ data: 'Type', visible: false, targets: 2 },
|
||||||
|
{ type: 'natural', targets: 0, width: "80%" }
|
||||||
],
|
],
|
||||||
"language": {
|
"language": {
|
||||||
"emptyTable": "No tags in this repository."
|
"emptyTable": "No entry."
|
||||||
|
},
|
||||||
|
rowGroup: {
|
||||||
|
dataSrc: 'Type'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
function populateConfirmation() {
|
function populateConfirmation() {
|
||||||
@ -25,7 +29,6 @@
|
|||||||
populateConfirmation()
|
populateConfirmation()
|
||||||
$('#datatable').on('draw.dt', populateConfirmation)
|
$('#datatable').on('draw.dt', populateConfirmation)
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{{end}}
|
{{end}}
|
||||||
@ -33,27 +36,43 @@
|
|||||||
{{block body()}}
|
{{block body()}}
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="{{ basePath }}/">{{ registryHost }}</a></li>
|
<li><a href="{{ basePath }}/">{{ registryHost }}</a></li>
|
||||||
{{if namespace != "library"}}
|
{{range crumb := breadCrumbs}}
|
||||||
<li><a href="{{ basePath }}/{{ namespace }}">{{ namespace }}</a></li>
|
<li><a href="{{ basePath }}/{{ crumb.path }}">{{ crumb.segment }}</a></li>
|
||||||
{{end}}
|
{{end}}
|
||||||
<li class="active">{{ repo|url_decode }}</li>
|
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<table id="datatable" class="table table-striped table-bordered">
|
<table id="datatable" class="table table-striped table-bordered">
|
||||||
<thead bgcolor="#ddd">
|
<thead bgcolor="#ddd">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Tag Name</th>
|
<th>Name</th>
|
||||||
|
<th>Tag count</th>
|
||||||
|
<th>Type</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{range tag := tags}}
|
{{range tag := tags}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ basePath }}/{{ namespace }}/{{ repo }}/{{ tag }}">{{ tag }}</a>
|
<a href="{{ basePath }}/{{ repoPath }}/manifests/{{ tag }}">{{ tag }}</a>
|
||||||
{{if deleteAllowed}}
|
{{if deleteAllowed}}
|
||||||
<a href="{{ basePath }}/{{ namespace }}/{{ repo }}/{{ tag }}/delete" data-toggle="confirmation" class="btn btn-danger btn-xs pull-right" role="button">Delete</a>
|
<a href="{{ basePath }}/{{ repoPath }}/manifests/{{ tag }}/delete" data-toggle="confirmation" class="btn btn-danger btn-xs pull-right" role="button">Delete</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</td>
|
</td>
|
||||||
|
<td>n/a</td>
|
||||||
|
<td>Tags</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
{{range repo := repos}}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{{noprefix_repo := repo[len(repoPath):]}}
|
||||||
|
{{if noprefix_repo[0:1] == "/"}}
|
||||||
|
{{noprefix_repo = noprefix_repo[1:]}}
|
||||||
|
{{end}}
|
||||||
|
<a href="{{ basePath }}/{{ repo }}">{{ noprefix_repo }}</a>
|
||||||
|
</td>
|
||||||
|
<td>{{ tagCounts[repo] }}</td>
|
||||||
|
<td>Repositories</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
@ -1,68 +0,0 @@
|
|||||||
{{extends "base.html"}}
|
|
||||||
|
|
||||||
{{block head()}}
|
|
||||||
<script type="text/javascript">
|
|
||||||
$(document).ready(function() {
|
|
||||||
$('#namespace').on('change', function (e) {
|
|
||||||
window.location = '{{ basePath }}/' + this.value;
|
|
||||||
});
|
|
||||||
namespace = window.location.pathname;
|
|
||||||
namespace = namespace.replace("{{ basePath }}", "");
|
|
||||||
if (namespace == '/') {
|
|
||||||
namespace = 'library';
|
|
||||||
} else {
|
|
||||||
namespace = namespace.split('/')[1]
|
|
||||||
}
|
|
||||||
$('#namespace').val(namespace);
|
|
||||||
|
|
||||||
$('#datatable').DataTable({
|
|
||||||
"pageLength": 25,
|
|
||||||
"stateSave": true,
|
|
||||||
"language": {
|
|
||||||
"emptyTable": "No repositories in \"" + namespace + "\" namespace."
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{block body()}}
|
|
||||||
<div style="float: right">
|
|
||||||
<select id="namespace" class="form-control input-sm" style="height: 36px">
|
|
||||||
{{range namespace := namespaces}}
|
|
||||||
<option value="{{ namespace }}">{{ namespace }}</option>
|
|
||||||
{{end}}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div style="float: right">
|
|
||||||
<ol class="breadcrumb">
|
|
||||||
<li class="active">Namespace</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ol class="breadcrumb">
|
|
||||||
<li><a href="{{ basePath }}/">{{ registryHost }}</a></li>
|
|
||||||
{{if namespace != "library"}}
|
|
||||||
<li><a href="{{ basePath }}/{{ namespace }}">{{ namespace }}</a></li>
|
|
||||||
{{end}}
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<table id="datatable" class="table table-striped table-bordered">
|
|
||||||
<thead bgcolor="#ddd">
|
|
||||||
<tr>
|
|
||||||
<th>Repository</th>
|
|
||||||
<th width="20%">Tags</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{range repo := repos}}
|
|
||||||
{{if !isset(tagCounts[namespace+"/"+repo]) || (isset(tagCounts[namespace+"/"+repo]) && tagCounts[namespace+"/"+repo] > 0)}}
|
|
||||||
<tr>
|
|
||||||
<td><a href="{{ basePath }}/{{ namespace }}/{{ repo|url }}">{{ repo }}</a></td>
|
|
||||||
<td>{{ tagCounts[namespace+"/"+repo] }}</td>
|
|
||||||
</tr>
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{{end}}
|
|
@ -5,11 +5,10 @@
|
|||||||
{{block body()}}
|
{{block body()}}
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="{{ basePath }}/">{{ registryHost }}</a></li>
|
<li><a href="{{ basePath }}/">{{ registryHost }}</a></li>
|
||||||
{{if namespace != "library"}}
|
{{range crumb := breadCrumbs}}
|
||||||
<li><a href="{{ basePath }}/{{ namespace }}">{{ namespace }}</a></li>
|
<li><a href="{{ basePath }}/{{ crumb.path }}">{{ crumb.segment }}</a></li>
|
||||||
{{end}}
|
{{end}}
|
||||||
<li><a href="{{ basePath }}/{{ namespace }}/{{ repo }}">{{ repo|url_decode }}</a></li>
|
<li><a href="{{ basePath }}/{{ repoPath }}/manifests/{{ tag }}">{{ tag }}</a></li>
|
||||||
<li class="active">{{ tag }}</li>
|
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<h4>Image Details</h4>
|
<h4>Image Details</h4>
|
||||||
|
Loading…
Reference in New Issue
Block a user