0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2025-01-27 23:01:43 -05:00

Add a '--verbose' flag to the 'zot images' output

- Show individual layers with size and digest under each image
- Include config digest for each image

See example below
```
IMAGE NAME                        TAG                       DIGEST    CONFIG    LAYERS    SIZE
test/godev                        0.4.7                     7d38d8ca  05b9f86e            519MB
                                                                                f824a027  65MB
                                                                                a98af0f5  52MB
                                                                                ba5b2bc4  163MB
                                                                                58b1ca8d  228MB
                                                                                67d798ee  12MB
test/cdev                         test                      2292b4ae  cf6f6c77            280MB
                                                                                f824a027  65MB
                                                                                a98af0f5  52MB
                                                                                ba5b2bc4  163MB
test/cdev                         0.4.7                     2292b4ae  cf6f6c77            280MB
                                                                                f824a027  65MB
                                                                                a98af0f5  52MB
                                                                                ba5b2bc4  163MB

Note the new layers and config fields will be visible in the json/yaml format regardless of the value of the verbose flag
```
This commit is contained in:
Andrei Aaron 2021-05-28 19:27:17 +03:00 committed by Ramkumar Chinchani
parent 519ea75d9a
commit c1dd7878e4
6 changed files with 131 additions and 27 deletions

View file

@ -168,19 +168,35 @@ func (p *requestsPool) doJob(job *manifestJob) {
digest := header.Get("docker-content-digest")
digest = strings.TrimPrefix(digest, "sha256:")
configDigest := job.manifestResp.Config.Digest
configDigest = strings.TrimPrefix(configDigest, "sha256:")
var size uint64
for _, layer := range job.manifestResp.Layers {
size += layer.Size
layers := []layer{}
for _, entry := range job.manifestResp.Layers {
size += entry.Size
layers = append(
layers,
layer{
Size: entry.Size,
Digest: strings.TrimPrefix(entry.Digest, "sha256:"),
},
)
}
image := &imageStruct{}
image.verbose = *job.config.verbose
image.Name = job.imageName
image.Tags = []tags{
{
Name: job.tagName,
Digest: digest,
Size: size,
Name: job.tagName,
Digest: digest,
Size: size,
ConfigDigest: configDigest,
Layers: layers,
},
}

View file

@ -18,7 +18,7 @@ func NewCveCommand(searchService SearchService) *cobra.Command {
var servURL, user, outputFormat string
var isSpinner, verifyTLS, fixedFlag bool
var isSpinner, verifyTLS, fixedFlag, verbose bool
var cveCmd = &cobra.Command{
Use: "cve [config-name]",
@ -64,6 +64,8 @@ func NewCveCommand(searchService SearchService) *cobra.Command {
spin := spinner.New(spinner.CharSets[39], spinnerDuration, spinner.WithWriter(cmd.ErrOrStderr()))
spin.Prefix = fmt.Sprintf("Fetching from %s.. ", servURL)
verbose = false
searchConfig := searchConfig{
params: searchCveParams,
searchService: searchService,
@ -72,6 +74,7 @@ func NewCveCommand(searchService SearchService) *cobra.Command {
outputFormat: &outputFormat,
fixedFlag: &fixedFlag,
verifyTLS: &verifyTLS,
verbose: &verbose,
resultWriter: cmd.OutOrStdout(),
spinner: spinnerState{spin, isSpinner},
}

View file

@ -18,7 +18,7 @@ func NewImageCommand(searchService SearchService) *cobra.Command {
var servURL, user, outputFormat string
var isSpinner, verifyTLS bool
var isSpinner, verifyTLS, verbose bool
var imageCmd = &cobra.Command{
Use: "images [config-name]",
@ -70,6 +70,7 @@ func NewImageCommand(searchService SearchService) *cobra.Command {
servURL: &servURL,
user: &user,
outputFormat: &outputFormat,
verbose: &verbose,
spinner: spinnerState{spin, isSpinner},
verifyTLS: &verifyTLS,
resultWriter: cmd.OutOrStdout(),
@ -86,7 +87,7 @@ func NewImageCommand(searchService SearchService) *cobra.Command {
},
}
setupImageFlags(imageCmd, searchImageParams, &servURL, &user, &outputFormat)
setupImageFlags(imageCmd, searchImageParams, &servURL, &user, &outputFormat, &verbose)
imageCmd.SetUsageTemplate(imageCmd.UsageTemplate() + usageFooter)
return imageCmd
@ -107,7 +108,7 @@ func parseBooleanConfig(configPath, configName, configParam string) (bool, error
}
func setupImageFlags(imageCmd *cobra.Command, searchImageParams map[string]*string,
servURL, user, outputFormat *string) {
servURL, user, outputFormat *string, verbose *bool) {
searchImageParams["imageName"] = imageCmd.Flags().StringP("name", "n", "", "List image details by name")
searchImageParams["digest"] = imageCmd.Flags().StringP("digest", "d", "",
"List images containing a specific manifest, config, or layer digest")
@ -115,6 +116,7 @@ func setupImageFlags(imageCmd *cobra.Command, searchImageParams map[string]*stri
imageCmd.Flags().StringVar(servURL, "url", "", "Specify zot server URL if config-name is not mentioned")
imageCmd.Flags().StringVarP(user, "user", "u", "", `User Credentials of zot server in "username:password" format`)
imageCmd.Flags().StringVarP(outputFormat, "output", "o", "", "Specify output format [text/json/yaml]")
imageCmd.Flags().BoolVar(verbose, "verbose", false, "Show verbose output")
}
func searchImage(searchConfig searchConfig) error {

View file

@ -221,7 +221,7 @@ func TestOutputFormat(t *testing.T) {
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
So(strings.TrimSpace(str), ShouldEqual, `{ "name": "dummyImageName", "tags": [ { "name":`+
` "tag", "size": 123445, "digest": "DigestsAreReallyLong" } ] }`)
` "tag", "size": 123445, "digest": "DigestsAreReallyLong", "configDigest": "", "layerDigests": null } ] }`)
So(err, ShouldBeNil)
})
@ -240,7 +240,7 @@ func TestOutputFormat(t *testing.T) {
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
So(strings.TrimSpace(str), ShouldEqual, `name: dummyImageName tags: -`+
` name: tag size: 123445 digest: DigestsAreReallyLong`)
` name: tag size: 123445 digest: DigestsAreReallyLong configdigest: "" layers: []`)
So(err, ShouldBeNil)
Convey("Test yml", func() {
@ -257,8 +257,8 @@ func TestOutputFormat(t *testing.T) {
err := cmd.Execute()
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
So(strings.TrimSpace(str), ShouldEqual, "name: dummyImageName tags: - name: "+
"tag size: 123445 digest: DigestsAreReallyLong")
So(strings.TrimSpace(str), ShouldEqual, `name: dummyImageName tags: -`+
` name: tag size: 123445 digest: DigestsAreReallyLong configdigest: "" layers: []`)
So(err, ShouldBeNil)
})
})
@ -338,6 +338,31 @@ func TestServerResponse(t *testing.T) {
So(actual, ShouldContainSubstring, "repo7 test:1.0 a0ca253b 15B")
})
Convey("Test all images verbose", func() {
args := []string{"imagetest", "--verbose"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cmd := NewImageCommand(new(searchService))
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
// Actual cli output should be something similar to (order of images may differ):
// IMAGE NAME TAG DIGEST CONFIG LAYERS SIZE
// repo7 test:2.0 a0ca253b b8781e88 15B
// b8781e88 15B
// repo7 test:1.0 a0ca253b b8781e88 15B
// b8781e88 15B
So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST CONFIG LAYERS SIZE")
So(actual, ShouldContainSubstring, "repo7 test:2.0 a0ca253b b8781e88 15B b8781e88 15B")
So(actual, ShouldContainSubstring, "repo7 test:1.0 a0ca253b b8781e88 15B b8781e88 15B")
})
Convey("Test image by name config url", func() {
args := []string{"imagetest", "--name", "repo7"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))

View file

@ -60,6 +60,7 @@ type searchConfig struct {
outputFormat *string
verifyTLS *bool
fixedFlag *bool
verbose *bool
resultWriter io.Writer
spinner spinnerState
}
@ -323,7 +324,7 @@ func collectResults(config searchConfig, wg *sync.WaitGroup, imageErr chan strin
if !foundResult && (*config.outputFormat == defaultOutoutFormat || *config.outputFormat == "") {
var builder strings.Builder
printHeader(&builder)
printHeader(&builder, *config.verbose)
fmt.Fprint(config.resultWriter, builder.String())
}
@ -417,22 +418,38 @@ type stringResult struct {
Err error
}
type printHeader func(writer io.Writer)
type printHeader func(writer io.Writer, verbose bool)
func printImageTableHeader(writer io.Writer) {
func printImageTableHeader(writer io.Writer, verbose bool) {
table := getImageTableWriter(writer)
row := make([]string, 4)
table.SetColMinWidth(colImageNameIndex, imageNameWidth)
table.SetColMinWidth(colTagIndex, tagWidth)
table.SetColMinWidth(colDigestIndex, digestWidth)
table.SetColMinWidth(colSizeIndex, sizeWidth)
if verbose {
table.SetColMinWidth(colConfigIndex, configWidth)
table.SetColMinWidth(colLayersIndex, layersWidth)
}
row := make([]string, 6)
row[colImageNameIndex] = "IMAGE NAME"
row[colTagIndex] = "TAG"
row[colDigestIndex] = "DIGEST"
row[colSizeIndex] = "SIZE"
if verbose {
row[colConfigIndex] = "CONFIG"
row[colLayersIndex] = "LAYERS"
}
table.Append(row)
table.Render()
}
func printCVETableHeader(writer io.Writer) {
func printCVETableHeader(writer io.Writer, verbose bool) {
table := getCVETableWriter(writer)
row := make([]string, 3)
row[colCVEIDIndex] = "ID"

View file

@ -606,11 +606,20 @@ type tagListResp struct {
}
type imageStruct struct {
Name string `json:"name"`
Tags []tags `json:"tags"`
Name string `json:"name"`
Tags []tags `json:"tags"`
verbose bool
}
type tags struct {
Name string `json:"name"`
Name string `json:"name"`
Size uint64 `json:"size"`
Digest string `json:"digest"`
ConfigDigest string `json:"configDigest"`
Layers []layer `json:"layerDigests"`
}
type layer struct {
Size uint64 `json:"size"`
Digest string `json:"digest"`
}
@ -632,20 +641,52 @@ func (img imageStruct) stringPlainText() (string, error) {
var builder strings.Builder
table := getImageTableWriter(&builder)
table.SetColMinWidth(colImageNameIndex, imageNameWidth)
table.SetColMinWidth(colTagIndex, tagWidth)
table.SetColMinWidth(colDigestIndex, digestWidth)
table.SetColMinWidth(colSizeIndex, sizeWidth)
if img.verbose {
table.SetColMinWidth(colConfigIndex, configWidth)
table.SetColMinWidth(colLayersIndex, layersWidth)
}
for _, tag := range img.Tags {
imageName := ellipsize(img.Name, imageNameWidth, ellipsis)
tagName := ellipsize(tag.Name, tagWidth, ellipsis)
digest := ellipsize(tag.Digest, digestWidth, "")
size := ellipsize(strings.ReplaceAll(humanize.Bytes(tag.Size), " ", ""), sizeWidth, ellipsis)
row := make([]string, 4)
config := ellipsize(tag.ConfigDigest, configWidth, "")
row := make([]string, 6)
row[colImageNameIndex] = imageName
row[colTagIndex] = tagName
row[colDigestIndex] = digest
row[colSizeIndex] = size
if img.verbose {
row[colConfigIndex] = config
row[colLayersIndex] = ""
}
table.Append(row)
if img.verbose {
for _, entry := range tag.Layers {
layerSize := ellipsize(strings.ReplaceAll(humanize.Bytes(entry.Size), " ", ""), sizeWidth, ellipsis)
layerDigest := ellipsize(entry.Digest, digestWidth, "")
layerRow := make([]string, 6)
layerRow[colImageNameIndex] = ""
layerRow[colTagIndex] = ""
layerRow[colDigestIndex] = ""
layerRow[colSizeIndex] = layerSize
layerRow[colConfigIndex] = ""
layerRow[colLayersIndex] = layerDigest
table.Append(layerRow)
}
}
}
table.Render()
@ -737,10 +778,6 @@ func getImageTableWriter(writer io.Writer) *tablewriter.Table {
table.SetBorder(false)
table.SetTablePadding(" ")
table.SetNoWhiteSpace(true)
table.SetColMinWidth(colImageNameIndex, imageNameWidth)
table.SetColMinWidth(colTagIndex, tagWidth)
table.SetColMinWidth(colDigestIndex, digestWidth)
table.SetColMinWidth(colSizeIndex, sizeWidth)
return table
}
@ -771,12 +808,16 @@ const (
tagWidth = 24
digestWidth = 8
sizeWidth = 8
configWidth = 8
layersWidth = 8
ellipsis = "..."
colImageNameIndex = 0
colTagIndex = 1
colDigestIndex = 2
colSizeIndex = 3
colConfigIndex = 3
colLayersIndex = 4
colSizeIndex = 5
cveIDWidth = 16
cveSeverityWidth = 8