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:
parent
519ea75d9a
commit
c1dd7878e4
6 changed files with 131 additions and 27 deletions
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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},
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue