diff --git a/main.go b/main.go index 9e6e865..ea5ab5a 100644 --- a/main.go +++ b/main.go @@ -210,9 +210,15 @@ func (a *apiClient) viewTagInfo(c echo.Context) error { } sha256, infoV1, infoV2 := a.client.TagInfo(repoPath, tag, false) - if infoV1 == "" || infoV2 == "" { + manifests := a.client.Manifests(repoPath, tag) + if (infoV1 == "" || infoV2 == "") && len(manifests) == 0 { return c.Redirect(http.StatusSeeOther, fmt.Sprintf("%s/%s/%s", a.config.BasePath, namespace, repo)) } + isListOnly := (infoV1 == "" && infoV2 == "") + newRepoPath := gjson.Get(infoV1, "name").String() + if newRepoPath != "" { + repoPath = newRepoPath + } var imageSize int64 if gjson.Get(infoV2, "layers").Exists() { @@ -243,17 +249,38 @@ func (a *apiClient) viewTagInfo(c echo.Context) error { layersCount = len(gjson.Get(infoV1, "fsLayers").Array()) } + isDigest := strings.HasPrefix(tag, "sha256:") + var digests []map[string]interface{} + for _, s := range manifests { + r, _ := gjson.Parse(s.String()).Value().(map[string]interface{}) + if s.Get("mediaType").String() == "application/vnd.docker.distribution.manifest.v2+json" { + _, _, dInfo := a.client.TagInfo(repoPath, s.Get("digest").String(), false) + var dSize int64 + for _, d := range gjson.Get(dInfo, "layers.#.size").Array() { + dSize = dSize + d.Int() + } + r["size"] = dSize + } else { + r["size"] = s.Get("size").Int() + } + r["ordered_keys"] = registry.SortedMapKeys(r) + digests = append(digests, r) + } + data := jet.VarMap{} data.Set("namespace", namespace) data.Set("repo", repo) data.Set("sha256", sha256) data.Set("imageSize", imageSize) - data.Set("tag", gjson.Get(infoV1, "tag").String()) - data.Set("repoPath", gjson.Get(infoV1, "name").String()) + data.Set("tag", tag) + data.Set("repoPath", repoPath) data.Set("created", gjson.Get(gjson.Get(infoV1, "history.0.v1Compatibility").String(), "created").String()) data.Set("layersCount", layersCount) data.Set("layersV2", layersV2) data.Set("layersV1", layersV1) + data.Set("isDigest", isDigest) + data.Set("isListOnly", isListOnly) + data.Set("digests", digests) return c.Render(http.StatusOK, "tag_info.html", data) } diff --git a/registry/client.go b/registry/client.go index ff53994..184c0ef 100644 --- a/registry/client.go +++ b/registry/client.go @@ -106,8 +106,12 @@ func (c *Client) getToken(scope string) string { } // callRegistry make an HTTP request to Docker registry. -func (c *Client) callRegistry(uri, scope string, manifest uint, delete bool) (string, gorequest.Response) { - acceptHeader := fmt.Sprintf("application/vnd.docker.distribution.manifest.v%d+json", manifest) +func (c *Client) callRegistry(uri, scope string, manifest uint, delete bool, list bool) (string, gorequest.Response) { + endpoint := "manifest" + if list { + endpoint = "manifest.list" + } + acceptHeader := fmt.Sprintf("application/vnd.docker.distribution.%s.v%d+json", endpoint, manifest) authHeader := "" if c.authURL != "" { authHeader = fmt.Sprintf("Bearer %s", c.getToken(scope)) @@ -179,7 +183,7 @@ func (c *Client) Repositories(useCache bool) map[string][]string { uri := "/v2/_catalog" c.repos = map[string][]string{} for { - data, resp := c.callRegistry(uri, scope, 2, false) + data, resp := c.callRegistry(uri, scope, 2, false, false) if data == "" { return c.repos } @@ -212,7 +216,7 @@ func (c *Client) Repositories(useCache bool) map[string][]string { // Tags get tags for the repo. func (c *Client) Tags(repo string) []string { scope := fmt.Sprintf("repository:%s:*", repo) - data, _ := c.callRegistry(fmt.Sprintf("/v2/%s/tags/list", repo), scope, 2, false) + data, _ := c.callRegistry(fmt.Sprintf("/v2/%s/tags/list", repo), scope, 2, false, false) var tags []string for _, t := range gjson.Get(data, "tags").Array() { tags = append(tags, t.String()) @@ -220,10 +224,17 @@ func (c *Client) Tags(repo string) []string { return tags } +// Manifests gets manifest list entries for a tag for the repo. +func (c *Client) Manifests(repo string, tag string) []gjson.Result { + scope := fmt.Sprintf("repository:%s:*", repo) + data, _ := c.callRegistry(fmt.Sprintf("/v2/%s/manifests/%s", repo, tag), scope, 2, false, true) + return gjson.Get(data, "manifests").Array() +} + // TagInfo get image info for the repo tag. func (c *Client) TagInfo(repo, tag string, v1only bool) (rsha256, rinfoV1, rinfoV2 string) { scope := fmt.Sprintf("repository:%s:*", repo) - infoV1, _ := c.callRegistry(fmt.Sprintf("/v2/%s/manifests/%s", repo, tag), scope, 1, false) + infoV1, _ := c.callRegistry(fmt.Sprintf("/v2/%s/manifests/%s", repo, tag), scope, 1, false, false) if infoV1 == "" { return "", "", "" } @@ -232,7 +243,7 @@ func (c *Client) TagInfo(repo, tag string, v1only bool) (rsha256, rinfoV1, rinfo return "", infoV1, "" } - infoV2, resp := c.callRegistry(fmt.Sprintf("/v2/%s/manifests/%s", repo, tag), scope, 2, false) + infoV2, resp := c.callRegistry(fmt.Sprintf("/v2/%s/manifests/%s", repo, tag), scope, 2, false, false) digest := resp.Header.Get("Docker-Content-Digest") if infoV2 == "" || digest == "" { return "", "", "" @@ -269,5 +280,5 @@ func (c *Client) CountTags(interval uint8) { // DeleteTag delete image tag. func (c *Client) DeleteTag(repo, tag string) { scope := fmt.Sprintf("repository:%s:*", repo) - c.callRegistry(fmt.Sprintf("/v2/%s/manifests/%s", repo, tag), scope, 2, true) + c.callRegistry(fmt.Sprintf("/v2/%s/manifests/%s", repo, tag), scope, 2, true, false) } diff --git a/templates/tag_info.html b/templates/tag_info.html index b5ad178..716f427 100644 --- a/templates/tag_info.html +++ b/templates/tag_info.html @@ -20,21 +20,59 @@ Image{{ registryHost }}/{{ repoPath }}:{{ tag }} + {{if not isListOnly}} sha256{{ sha256 }} + {{if not isDigest}} Created On{{ created|pretty_time }} + {{end}} Image Size{{ imageSize|pretty_size }} Layer Count{{ layersCount }} + {{end}} -{{if layersV2}} +{{if digests}} +

Manifest List v2

+{{range index, manifest := digests}} + + + + + + + {{range key := manifest["ordered_keys"]}} + + + {{if key == "platform" || key == "annotations"}} + + {{else if key == "size"}} + + {{else if key == "digest"}} + {{if not isListOnly}} + + {{else}} + + {{end}} + {{else}} + + {{end}} + + {{end}} +
Manifest #{{ index+1 }}
{{ key }} + + + {{ manifest[key]|parse_map|raw }} +
+
{{ manifest[key]|pretty_size }}{{ manifest["digest"] }}{{ manifest["digest"] }}{{ manifest[key] }}
+{{end}} +{{else if layersV2}}

Manifest v2

@@ -54,6 +92,7 @@
{{end}} +{{if not isListOnly && not isDigest}}

Manifest v1

{{range index, layer := layersV1}} @@ -81,5 +120,6 @@ {{end}}
{{end}} +{{end}} {{end}}