Support V2 Manifest Lists (#45)

* Support V2 Manifest Lists

* Fix breadcrumb navigation for digest tags

* Support BuildX cache images & display extended attributes of manifests
This commit is contained in:
Christoph Honal 2020-02-17 11:11:37 +01:00 committed by GitHub
parent ee38e35ba6
commit 905e760956
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 89 additions and 11 deletions

33
main.go
View File

@ -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)
}

View File

@ -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)
}

View File

@ -20,21 +20,59 @@
<tr>
<td width="20%">Image</td><td>{{ registryHost }}/{{ repoPath }}:{{ tag }}</td>
</tr>
{{if not isListOnly}}
<tr>
<td>sha256</td><td>{{ sha256 }}</td>
</tr>
{{if not isDigest}}
<tr>
<td>Created On</td><td>{{ created|pretty_time }}</td>
</tr>
{{end}}
<tr>
<td>Image Size</td><td>{{ imageSize|pretty_size }}</td>
</tr>
<tr>
<td>Layer Count</td><td>{{ layersCount }}</td>
</tr>
{{end}}
</table>
{{if layersV2}}
{{if digests}}
<h4>Manifest List v2</h4>
{{range index, manifest := digests}}
<table class="table table-striped table-bordered">
<thead bgcolor="#ddd">
<tr>
<th colspan="2">Manifest #{{ index+1 }}</th>
</tr>
</thead>
{{range key := manifest["ordered_keys"]}}
<tr>
<td width="20%">{{ key }}</td>
{{if key == "platform" || key == "annotations"}}
<td style="padding: 0">
<table class="table table-bordered" style="padding: 0; width: 100%; margin-bottom: 0; min-height: 37px">
<!-- Nested range does not work. Iterating via filter over the map. -->
{{ manifest[key]|parse_map|raw }}
</table>
</td>
{{else if key == "size"}}
<td>{{ manifest[key]|pretty_size }}</td>
{{else if key == "digest"}}
{{if not isListOnly}}
<td><a href='{{ basePath }}/{{ namespace }}/{{ repo }}/{{ manifest["digest"] }}'>{{ manifest["digest"] }}</a></td>
{{else}}
<td>{{ manifest["digest"] }}</td>
{{end}}
{{else}}
<td>{{ manifest[key] }}</td>
{{end}}
</tr>
{{end}}
</table>
{{end}}
{{else if layersV2}}
<h4>Manifest v2</h4>
<table class="table table-striped table-bordered">
<thead bgcolor="#ddd">
@ -54,6 +92,7 @@
</table>
{{end}}
{{if not isListOnly && not isDigest}}
<h4>Manifest v1</h4>
{{range index, layer := layersV1}}
<table class="table table-striped table-bordered">
@ -81,5 +120,6 @@
{{end}}
</table>
{{end}}
{{end}}
{{end}}