images: distinguish between tags and digests

Generate an image's RepoDigests list using all applicable digests, and
refrain from outputting a digest in the tag column of the "images"
output.

Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>
This commit is contained in:
Nalin Dahyabhai 2019-10-16 12:01:30 -04:00
parent 07195ff09f
commit 248bb61b14
4 changed files with 73 additions and 25 deletions

View File

@ -206,9 +206,9 @@ func (i imagesOptions) setOutputFormat() string {
if i.quiet { if i.quiet {
return formats.IDString return formats.IDString
} }
format := "table {{.Repository}}\t{{.Tag}}\t" format := "table {{.Repository}}\t{{if .Tag}}{{.Tag}}{{else}}<none>{{end}}\t"
if i.noHeading { if i.noHeading {
format = "{{.Repository}}\t{{.Tag}}\t" format = "{{.Repository}}\t{{if .Tag}}{{.Tag}}{{else}}<none>{{end}}\t"
} }
if i.digests { if i.digests {
format += "{{.Digest}}\t" format += "{{.Digest}}\t"
@ -270,7 +270,7 @@ func getImagesTemplateOutput(ctx context.Context, images []*adapter.ContainerIma
imageID = shortID(img.ID()) imageID = shortID(img.ID())
} }
// get all specified repo:tag pairs and print them separately // get all specified repo:tag and repo@digest pairs and print them separately
repopairs, err := image.ReposToMap(img.Names()) repopairs, err := image.ReposToMap(img.Names())
if err != nil { if err != nil {
logrus.Errorf("error finding tag/digest for %s", img.ID()) logrus.Errorf("error finding tag/digest for %s", img.ID())
@ -287,11 +287,16 @@ func getImagesTemplateOutput(ctx context.Context, images []*adapter.ContainerIma
lastNumIdx := strings.LastIndexFunc(sizeStr, unicode.IsNumber) lastNumIdx := strings.LastIndexFunc(sizeStr, unicode.IsNumber)
sizeStr = sizeStr[:lastNumIdx+1] + " " + sizeStr[lastNumIdx+1:] sizeStr = sizeStr[:lastNumIdx+1] + " " + sizeStr[lastNumIdx+1:]
} }
var imageDigest digest.Digest
if len(tag) == 71 && strings.HasPrefix(tag, "sha256:") {
imageDigest = digest.Digest(tag)
tag = ""
}
params := imagesTemplateParams{ params := imagesTemplateParams{
Repository: repo, Repository: repo,
Tag: tag, Tag: tag,
ID: imageID, ID: imageID,
Digest: img.Digest(), Digest: imageDigest,
Digests: img.Digests(), Digests: img.Digests(),
CreatedTime: createdTime, CreatedTime: createdTime,
Created: units.HumanDuration(time.Since(createdTime)) + " ago", Created: units.HumanDuration(time.Since(createdTime)) + " ago",
@ -302,7 +307,6 @@ func getImagesTemplateOutput(ctx context.Context, images []*adapter.ContainerIma
if opts.quiet { // Show only one image ID when quiet if opts.quiet { // Show only one image ID when quiet
break outer break outer
} }
} }
} }
} }

View File

@ -9,6 +9,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"sort"
"strings" "strings"
"syscall" "syscall"
"time" "time"
@ -324,29 +325,54 @@ func (i *Image) Manifest(ctx context.Context) ([]byte, string, error) {
return imgRef.Manifest(ctx) return imgRef.Manifest(ctx)
} }
// Names returns a string array of names associated with the image // Names returns a string array of names associated with the image, which may be a mixture of tags and digests
func (i *Image) Names() []string { func (i *Image) Names() []string {
return i.image.Names return i.image.Names
} }
// RepoDigests returns a string array of repodigests associated with the image // RepoTags returns a string array of repotags associated with the image
func (i *Image) RepoDigests() ([]string, error) { func (i *Image) RepoTags() ([]string, error) {
var repoDigests []string var repoTags []string
imageDigest := i.Digest()
for _, name := range i.Names() { for _, name := range i.Names() {
named, err := reference.ParseNormalizedNamed(name) named, err := reference.ParseNormalizedNamed(name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if tagged, isTagged := named.(reference.NamedTagged); isTagged {
canonical, err := reference.WithDigest(reference.TrimNamed(named), imageDigest) repoTags = append(repoTags, tagged.String())
if err != nil {
return nil, err
} }
repoDigests = append(repoDigests, canonical.String())
} }
return repoTags, nil
}
// RepoDigests returns a string array of repodigests associated with the image
func (i *Image) RepoDigests() ([]string, error) {
var repoDigests []string
added := make(map[string]struct{})
for _, name := range i.Names() {
for _, imageDigest := range append(i.Digests(), i.Digest()) {
if imageDigest == "" {
continue
}
named, err := reference.ParseNormalizedNamed(name)
if err != nil {
return nil, err
}
canonical, err := reference.WithDigest(reference.TrimNamed(named), imageDigest)
if err != nil {
return nil, err
}
if _, alreadyInList := added[canonical.String()]; !alreadyInList {
repoDigests = append(repoDigests, canonical.String())
added[canonical.String()] = struct{}{}
}
}
}
sort.Strings(repoDigests)
return repoDigests, nil return repoDigests, nil
} }
@ -944,6 +970,11 @@ func (i *Image) Inspect(ctx context.Context) (*inspect.ImageData, error) {
size = int64(*usize) size = int64(*usize)
} }
repoTags, err := i.RepoTags()
if err != nil {
return nil, err
}
repoDigests, err := i.RepoDigests() repoDigests, err := i.RepoDigests()
if err != nil { if err != nil {
return nil, err return nil, err
@ -965,7 +996,7 @@ func (i *Image) Inspect(ctx context.Context) (*inspect.ImageData, error) {
data := &inspect.ImageData{ data := &inspect.ImageData{
ID: i.ID(), ID: i.ID(),
RepoTags: i.Names(), RepoTags: repoTags,
RepoDigests: repoDigests, RepoDigests: repoDigests,
Comment: comment, Comment: comment,
Created: ociv1Img.Created, Created: ociv1Img.Created,

View File

@ -247,6 +247,19 @@ func TestImage_RepoDigests(t *testing.T) {
} }
assert.Equal(t, test.expected, actual) assert.Equal(t, test.expected, actual)
image = &Image{
image: &storage.Image{
Names: test.names,
Digests: []digest.Digest{dgst},
},
}
actual, err = image.RepoDigests()
if err != nil {
t.Fatal(err)
}
assert.Equal(t, test.expected, actual)
}) })
} }
} }

View File

@ -87,18 +87,18 @@ func hasTransport(image string) bool {
} }
// ReposToMap parses the specified repotags and returns a map with repositories // ReposToMap parses the specified repotags and returns a map with repositories
// as keys and the corresponding arrays of tags as values. // as keys and the corresponding arrays of tags or digests-as-strings as values.
func ReposToMap(repotags []string) (map[string][]string, error) { func ReposToMap(names []string) (map[string][]string, error) {
// map format is repo -> tag // map format is repo -> []tag-or-digest
repos := make(map[string][]string) repos := make(map[string][]string)
for _, repo := range repotags { for _, name := range names {
var repository, tag string var repository, tag string
if len(repo) > 0 { if len(name) > 0 {
named, err := reference.ParseNormalizedNamed(repo) named, err := reference.ParseNormalizedNamed(name)
repository = named.Name()
if err != nil { if err != nil {
return nil, err return nil, err
} }
repository = named.Name()
if ref, ok := named.(reference.NamedTagged); ok { if ref, ok := named.(reference.NamedTagged); ok {
tag = ref.Tag() tag = ref.Tag()
} else if ref, ok := named.(reference.Canonical); ok { } else if ref, ok := named.(reference.Canonical); ok {