0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-30 22:34:13 -05:00

Add GraphQL API for getting the information necessary to list images in the zot cli without download manifests.

If this GraphQL API is available, try that first, else fallback to the slowpath.

Signed-off-by: Roxana Nemulescu <roxana.nemulescu@gmail.com>
This commit is contained in:
Roxana Nemulescu 2022-01-19 17:57:10 +02:00 committed by Andrei Aaron
parent eb77307b63
commit ab9a20c1ae
23 changed files with 3080 additions and 2188 deletions

View file

@ -50,6 +50,7 @@ linters-settings:
- github.com/containers/image/v5
- github.com/opencontainers/image-spec
- github.com/open-policy-agent/opa
- github.com/vektah/gqlparser/v2
- go.opentelemetry.io/otel
- go.opentelemetry.io/otel/exporters/otlp
- go.opentelemetry.io/otel/metric

View file

@ -15,6 +15,7 @@ import (
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
@ -283,16 +284,12 @@ func (p *requestsPool) doJob(ctx context.Context, job *manifestJob) {
image := &imageStruct{}
image.verbose = *job.config.verbose
image.Name = job.imageName
image.Tags = []tags{
{
Name: job.tagName,
Digest: digest,
Size: size,
ConfigDigest: configDigest,
Layers: layers,
},
}
image.RepoName = job.imageName
image.Tag = job.tagName
image.Digest = digest
image.Size = strconv.Itoa(int(size))
image.ConfigDigest = configDigest
image.Layers = layers
str, err := image.string(*job.config.outputFormat)
if err != nil {

View file

@ -20,6 +20,7 @@ import (
"zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants"
extConf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/test"
)
@ -69,6 +70,11 @@ func TestTLSWithAuth(t *testing.T) {
CACert: CACert,
}
enable := true
conf.Extensions = &extConf.ExtensionConfig{
Search: &extConf.SearchConfig{Enable: &enable},
}
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()
go func() {
@ -161,6 +167,11 @@ func TestTLSWithoutAuth(t *testing.T) {
CACert: CACert,
}
enable := true
conf.Extensions = &extConf.ExtensionConfig{
Search: &extConf.SearchConfig{Enable: &enable},
}
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()
go func() {

View file

@ -4,13 +4,18 @@
package cli
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"path"
"github.com/briandowns/spinner"
"github.com/spf13/cobra"
"gopkg.in/resty.v1"
zotErrors "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api/constants"
)
func NewCveCommand(searchService SearchService) *cobra.Command {
@ -69,6 +74,7 @@ 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)
spin.Suffix = "\n\b"
verbose = false
@ -112,7 +118,7 @@ func NewCveCommand(searchService SearchService) *cobra.Command {
func setupCveFlags(cveCmd *cobra.Command, variables cveFlagVariables) {
variables.searchCveParams["imageName"] = cveCmd.Flags().StringP("image", "I", "", "List CVEs by IMAGENAME[:TAG]")
variables.searchCveParams["cvid"] = cveCmd.Flags().StringP("cve-id", "i", "", "List images affected by a CVE")
variables.searchCveParams["cveID"] = cveCmd.Flags().StringP("cve-id", "i", "", "List images affected by a CVE")
cveCmd.Flags().StringVar(variables.servURL, "url", "", "Specify zot server URL if config-name is not mentioned")
cveCmd.Flags().StringVarP(variables.user, "user", "u", "", `User Credentials of `+
@ -131,8 +137,80 @@ type cveFlagVariables struct {
fixedFlag *bool
}
type field struct {
Name string `json:"name"`
}
type schemaList struct {
Data struct {
Schema struct {
QueryType struct {
Fields []field `json:"fields"`
} `json:"queryType"` //nolint:tagliatelle // graphQL schema
} `json:"__schema"` //nolint:tagliatelle // graphQL schema
} `json:"data"`
}
func containsGQLQuery(queryList []field, query string) bool {
for _, q := range queryList {
if q.Name == query {
return true
}
}
return false
}
func checkExtEndPoint(serverURL string) bool {
client := resty.New()
extEndPoint, err := combineServerAndEndpointURL(serverURL, fmt.Sprintf("%s%s",
constants.RoutePrefix, constants.ExtOciDiscoverPrefix))
if err != nil {
return false
}
// nolint: gosec
resp, err := client.R().Get(extEndPoint)
if err != nil || resp.StatusCode() != http.StatusOK {
return false
}
searchEndPoint, _ := combineServerAndEndpointURL(serverURL, constants.ExtSearchPrefix)
query := `
{
__schema() {
queryType {
fields {
name
}
}
}
}`
resp, err = client.R().Get(searchEndPoint + "?query=" + url.QueryEscape(query))
if err != nil || resp.StatusCode() != http.StatusOK {
return false
}
queryList := &schemaList{}
_ = json.Unmarshal(resp.Body(), queryList)
return containsGQLQuery(queryList.Data.Schema.QueryType.Fields, "ImageList")
}
func searchCve(searchConfig searchConfig) error {
for _, searcher := range getCveSearchers() {
var searchers []searcher
if checkExtEndPoint(*searchConfig.servURL) {
searchers = getCveSearchersGQL()
} else {
searchers = getCveSearchers()
}
for _, searcher := range searchers {
found, err := searcher.search(searchConfig)
if found {
if err != nil {

View file

@ -15,6 +15,7 @@ import (
"time"
. "github.com/smartystreets/goconvey/convey"
"github.com/spf13/cobra"
"gopkg.in/resty.v1"
zotErrors "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api"
@ -51,6 +52,7 @@ func TestSearchCVECmd(t *testing.T) {
So(err, ShouldBeNil)
})
})
Convey("Test CVE no url", t, func() {
args := []string{"cvetest", "-i", "cveIdRandom"}
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`)
@ -136,10 +138,8 @@ func TestSearchCVECmd(t *testing.T) {
Convey("Test CVE url from config", t, func() {
args := []string{"cvetest", "--image", "dummyImageName:tag"}
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","url":"https://test-url.com","showspinner":false}]}`)
defer os.Remove(configPath)
cmd := NewCveCommand(new(mockService))
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
@ -162,10 +162,10 @@ func TestSearchCVECmd(t *testing.T) {
cveCmd.SetErr(buff)
cveCmd.SetArgs(args)
err := cveCmd.Execute()
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE dummyImageName tag DigestsA 123kB")
So(err, ShouldBeNil)
Convey("using shorthand", func() {
args := []string{"cvetest", "-I", "dummyImageName", "--cve-id", "aCVEID", "--url", "someURL"}
buff := bytes.NewBufferString("")
@ -176,11 +176,10 @@ func TestSearchCVECmd(t *testing.T) {
cveCmd.SetErr(buff)
cveCmd.SetArgs(args)
err := cveCmd.Execute()
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE dummyImageName tag DigestsA 123kB")
So(err, ShouldBeNil)
})
})
@ -266,6 +265,34 @@ func TestSearchCVECmd(t *testing.T) {
str := space.ReplaceAllString(buff.String(), " ")
So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE anImage tag DigestsA 123kB")
So(err, ShouldBeNil)
Convey("invalid CVE ID", func() {
args := []string{"cvetest", "--cve-id", "invalidCVEID"}
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`)
defer os.Remove(configPath)
cveCmd := NewCveCommand(new(mockService))
buff := bytes.NewBufferString("")
cveCmd.SetOut(buff)
cveCmd.SetErr(buff)
cveCmd.SetArgs(args)
err := cveCmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("invalid url", func() {
args := []string{"cvetest", "--cve-id", "aCVEID", "--url", "invalidURL"}
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`)
defer os.Remove(configPath)
cveCmd := NewCveCommand(NewSearchService())
buff := bytes.NewBufferString("")
cveCmd.SetOut(buff)
cveCmd.SetErr(buff)
cveCmd.SetArgs(args)
err := cveCmd.Execute()
So(err, ShouldNotBeNil)
So(err, ShouldEqual, zotErrors.ErrInvalidURL)
So(buff.String(), ShouldContainSubstring, "invalid URL format")
})
})
Convey("Test fixed tags by and image name CVE ID", t, func() {
@ -282,10 +309,24 @@ func TestSearchCVECmd(t *testing.T) {
str := space.ReplaceAllString(buff.String(), " ")
So(err, ShouldBeNil)
So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE fixedImage tag DigestsA 123kB")
Convey("invalid image name", func() {
args := []string{"cvetest", "--cve-id", "aCVEID", "--image", "invalidImageName"}
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`)
defer os.Remove(configPath)
cveCmd := NewCveCommand(NewSearchService())
buff := bytes.NewBufferString("")
cveCmd.SetOut(buff)
cveCmd.SetErr(buff)
cveCmd.SetArgs(args)
err := cveCmd.Execute()
So(err, ShouldNotBeNil)
})
})
}
func TestServerCVEResponse(t *testing.T) {
// nolint: dupl // GQL
func TestServerCVEResponseGQL(t *testing.T) {
port := test.GetFreePort()
url := test.GetBaseURL(port)
conf := config.New()
@ -351,6 +392,7 @@ func TestServerCVEResponse(t *testing.T) {
So(err, ShouldBeNil)
So(str, ShouldContainSubstring, "ID SEVERITY TITLE")
So(str, ShouldContainSubstring, "CVE")
Convey("invalid image", func() {
args := []string{"cvetest", "--image", "invalid:0.0.1"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
@ -363,6 +405,33 @@ func TestServerCVEResponse(t *testing.T) {
err = cveCmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("invalid image name and tag", func() {
args := []string{"cvetest", "--image", "invalid:"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cveCmd := NewCveCommand(new(searchService))
buff := bytes.NewBufferString("")
cveCmd.SetOut(buff)
cveCmd.SetErr(buff)
cveCmd.SetArgs(args)
err = cveCmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("invalid output format", func() {
args := []string{"cvetest", "--image", "zot-cve-test:0.0.1", "-o", "random"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cveCmd := NewCveCommand(new(searchService))
buff := bytes.NewBufferString("")
cveCmd.SetOut(buff)
cveCmd.SetErr(buff)
cveCmd.SetArgs(args)
err = cveCmd.Execute()
So(err, ShouldNotBeNil)
So(buff.String(), ShouldContainSubstring, "invalid output format")
})
})
Convey("Test images by CVE ID", t, func() {
@ -380,6 +449,7 @@ func TestServerCVEResponse(t *testing.T) {
str = strings.TrimSpace(str)
So(err, ShouldBeNil)
So(str, ShouldEqual, "IMAGE NAME TAG DIGEST SIZE zot-cve-test 0.0.1 63a795ca 75MB")
Convey("invalid CVE ID", func() {
args := []string{"cvetest", "--cve-id", "invalid"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
@ -396,6 +466,20 @@ func TestServerCVEResponse(t *testing.T) {
So(err, ShouldBeNil)
So(str, ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
})
Convey("invalid output format", func() {
args := []string{"cvetest", "--cve-id", "CVE-2019-9923", "-o", "random"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cveCmd := NewCveCommand(new(searchService))
buff := bytes.NewBufferString("")
cveCmd.SetOut(buff)
cveCmd.SetErr(buff)
cveCmd.SetArgs(args)
err = cveCmd.Execute()
So(err, ShouldNotBeNil)
So(buff.String(), ShouldContainSubstring, "invalid output format")
})
})
Convey("Test fixed tags by and image name CVE ID", t, func() {
@ -413,6 +497,7 @@ func TestServerCVEResponse(t *testing.T) {
str = strings.TrimSpace(str)
So(err, ShouldBeNil)
So(str, ShouldEqual, "")
Convey("random cve", func() {
args := []string{"cvetest", "--cve-id", "random", "--image", "zot-cve-test", "--fixed"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
@ -430,7 +515,7 @@ func TestServerCVEResponse(t *testing.T) {
So(strings.TrimSpace(str), ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
})
Convey("invalid image", func() {
Convey("random image", func() {
args := []string{"cvetest", "--cve-id", "CVE-2019-20807", "--image", "zot-cv-test", "--fixed"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
@ -446,6 +531,23 @@ func TestServerCVEResponse(t *testing.T) {
So(err, ShouldNotBeNil)
So(strings.TrimSpace(str), ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
})
Convey("invalid image", func() {
args := []string{"cvetest", "--cve-id", "CVE-2019-20807", "--image", "zot-cv-test:tag", "--fixed"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cveCmd := NewCveCommand(new(searchService))
buff := bytes.NewBufferString("")
cveCmd.SetOut(buff)
cveCmd.SetErr(buff)
cveCmd.SetArgs(args)
err := cveCmd.Execute()
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
str = strings.TrimSpace(str)
So(err, ShouldNotBeNil)
So(strings.TrimSpace(str), ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
})
})
Convey("Test CVE by name and CVE ID", t, func() {
@ -462,6 +564,7 @@ func TestServerCVEResponse(t *testing.T) {
str := space.ReplaceAllString(buff.String(), " ")
So(err, ShouldBeNil)
So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE zot-cve-test 0.0.1 63a795ca 75MB")
Convey("invalid name and CVE ID", func() {
args := []string{"cvetest", "--image", "test", "--cve-id", "CVE-20807"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
@ -477,5 +580,451 @@ func TestServerCVEResponse(t *testing.T) {
So(err, ShouldBeNil)
So(strings.TrimSpace(str), ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
})
Convey("invalid output format", func() {
args := []string{"cvetest", "--image", "zot-cve-test", "--cve-id", "CVE-2019-9923", "-o", "random"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cveCmd := NewCveCommand(new(searchService))
buff := bytes.NewBufferString("")
cveCmd.SetOut(buff)
cveCmd.SetErr(buff)
cveCmd.SetArgs(args)
err = cveCmd.Execute()
So(err, ShouldNotBeNil)
So(buff.String(), ShouldContainSubstring, "invalid output format")
})
})
}
func TestNegativeServerResponse(t *testing.T) {
Convey("Test from real server without search endpoint", t, func() {
port := test.GetFreePort()
url := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
dir := t.TempDir()
err := test.CopyFiles("../../test/data/zot-cve-test", path.Join(dir, "zot-cve-test"))
if err != nil {
panic(err)
}
conf.Storage.RootDirectory = dir
cveConfig := &extconf.CVEConfig{
UpdateInterval: 2,
}
defaultVal := false
searchConfig := &extconf.SearchConfig{
CVE: cveConfig,
Enable: &defaultVal,
}
conf.Extensions = &extconf.ExtensionConfig{
Search: searchConfig,
}
ctlr := api.NewController(conf)
go func(controller *api.Controller) {
// this blocks
if err := controller.Run(context.Background()); err != nil {
return
}
}(ctlr)
// wait till ready
for {
res, err := resty.R().Get(url)
if err == nil && res.StatusCode() == 404 {
break
}
time.Sleep(100 * time.Millisecond)
}
time.Sleep(90 * time.Second)
defer func(controller *api.Controller) {
ctx := context.Background()
_ = controller.Server.Shutdown(ctx)
}(ctlr)
Convey("Status Code Not Found", func() {
args := []string{"cvetest", "--image", "zot-cve-test:0.0.1"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cveCmd := NewCveCommand(new(searchService))
buff := bytes.NewBufferString("")
cveCmd.SetOut(buff)
cveCmd.SetErr(buff)
cveCmd.SetArgs(args)
err = cveCmd.Execute()
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
str = strings.TrimSpace(str)
So(err, ShouldNotBeNil)
So(str, ShouldContainSubstring, "404 page not found")
})
})
Convey("Test non-existing manifest blob", t, func() {
port := test.GetFreePort()
url := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
dir := t.TempDir()
err := test.CopyFiles("../../test/data/zot-cve-test", path.Join(dir, "zot-cve-test"))
if err != nil {
panic(err)
}
err = os.RemoveAll(path.Join(dir, "zot-cve-test/blobs"))
if err != nil {
panic(err)
}
conf.Storage.RootDirectory = dir
cveConfig := &extconf.CVEConfig{
UpdateInterval: 2,
}
defaultVal := true
searchConfig := &extconf.SearchConfig{
CVE: cveConfig,
Enable: &defaultVal,
}
conf.Extensions = &extconf.ExtensionConfig{
Search: searchConfig,
}
ctlr := api.NewController(conf)
go func(controller *api.Controller) {
// this blocks
if err := controller.Run(context.Background()); err != nil {
return
}
}(ctlr)
// wait till ready
for {
res, err := resty.R().Get(url)
if err == nil && res.StatusCode() == 404 {
break
}
time.Sleep(100 * time.Millisecond)
}
time.Sleep(90 * time.Second)
defer func(controller *api.Controller) {
ctx := context.Background()
_ = controller.Server.Shutdown(ctx)
}(ctlr)
args := []string{"cvetest", "--cve-id", "CVE-2019-9923", "--image", "zot-cve-test", "--fixed"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cveCmd := NewCveCommand(new(searchService))
buff := bytes.NewBufferString("")
cveCmd.SetOut(buff)
cveCmd.SetErr(buff)
cveCmd.SetArgs(args)
err = cveCmd.Execute()
So(err, ShouldNotBeNil)
})
}
// nolint: dupl
func TestServerCVEResponse(t *testing.T) {
port := test.GetFreePort()
url := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
dir := t.TempDir()
err := test.CopyFiles("../../test/data/zot-cve-test", path.Join(dir, "zot-cve-test"))
if err != nil {
panic(err)
}
conf.Storage.RootDirectory = dir
cveConfig := &extconf.CVEConfig{
UpdateInterval: 2,
}
defaultVal := true
searchConfig := &extconf.SearchConfig{
CVE: cveConfig,
Enable: &defaultVal,
}
conf.Extensions = &extconf.ExtensionConfig{
Search: searchConfig,
}
ctlr := api.NewController(conf)
go func(controller *api.Controller) {
// this blocks
if err := controller.Run(context.Background()); err != nil {
return
}
}(ctlr)
// wait till ready
for {
res, err := resty.R().Get(url + constants.ExtSearchPrefix)
if err == nil && res.StatusCode() == 422 {
break
}
time.Sleep(100 * time.Millisecond)
}
time.Sleep(90 * time.Second)
defer func(controller *api.Controller) {
ctx := context.Background()
_ = controller.Server.Shutdown(ctx)
}(ctlr)
Convey("Test CVE by image name", t, func() {
args := []string{"cvetest", "--image", "zot-cve-test:0.0.1"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cveCmd := MockNewCveCommand(new(searchService))
buff := bytes.NewBufferString("")
cveCmd.SetOut(buff)
cveCmd.SetErr(buff)
cveCmd.SetArgs(args)
err = cveCmd.Execute()
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
str = strings.TrimSpace(str)
So(err, ShouldBeNil)
So(str, ShouldContainSubstring, "ID SEVERITY TITLE")
So(str, ShouldContainSubstring, "CVE")
Convey("invalid image", func() {
args := []string{"cvetest", "--image", "invalid:0.0.1"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cveCmd := MockNewCveCommand(new(searchService))
buff := bytes.NewBufferString("")
cveCmd.SetOut(buff)
cveCmd.SetErr(buff)
cveCmd.SetArgs(args)
err = cveCmd.Execute()
So(err, ShouldNotBeNil)
})
})
Convey("Test images by CVE ID", t, func() {
args := []string{"cvetest", "--cve-id", "CVE-2019-9923"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cveCmd := MockNewCveCommand(new(searchService))
buff := bytes.NewBufferString("")
cveCmd.SetOut(buff)
cveCmd.SetErr(buff)
cveCmd.SetArgs(args)
err := cveCmd.Execute()
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
str = strings.TrimSpace(str)
So(err, ShouldBeNil)
So(str, ShouldEqual, "IMAGE NAME TAG DIGEST SIZE zot-cve-test 0.0.1 63a795ca 75MB")
Convey("invalid CVE ID", func() {
args := []string{"cvetest", "--cve-id", "invalid"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cveCmd := MockNewCveCommand(new(searchService))
buff := bytes.NewBufferString("")
cveCmd.SetOut(buff)
cveCmd.SetErr(buff)
cveCmd.SetArgs(args)
err := cveCmd.Execute()
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
str = strings.TrimSpace(str)
So(err, ShouldBeNil)
So(str, ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
})
})
Convey("Test fixed tags by and image name CVE ID", t, func() {
args := []string{"cvetest", "--cve-id", "CVE-2019-9923", "--image", "zot-cve-test", "--fixed"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cveCmd := MockNewCveCommand(new(searchService))
buff := bytes.NewBufferString("")
cveCmd.SetOut(buff)
cveCmd.SetErr(buff)
cveCmd.SetArgs(args)
err := cveCmd.Execute()
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
str = strings.TrimSpace(str)
So(err, ShouldBeNil)
So(str, ShouldEqual, "")
Convey("random cve", func() {
args := []string{"cvetest", "--cve-id", "random", "--image", "zot-cve-test", "--fixed"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cveCmd := MockNewCveCommand(new(searchService))
buff := bytes.NewBufferString("")
cveCmd.SetOut(buff)
cveCmd.SetErr(buff)
cveCmd.SetArgs(args)
err := cveCmd.Execute()
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
str = strings.TrimSpace(str)
So(err, ShouldBeNil)
So(strings.TrimSpace(str), ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
})
Convey("invalid image", func() {
args := []string{"cvetest", "--cve-id", "CVE-2019-20807", "--image", "zot-cv-test", "--fixed"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cveCmd := MockNewCveCommand(new(searchService))
buff := bytes.NewBufferString("")
cveCmd.SetOut(buff)
cveCmd.SetErr(buff)
cveCmd.SetArgs(args)
err := cveCmd.Execute()
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
str = strings.TrimSpace(str)
So(err, ShouldNotBeNil)
So(strings.TrimSpace(str), ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
})
})
Convey("Test CVE by name and CVE ID", t, func() {
args := []string{"cvetest", "--image", "zot-cve-test", "--cve-id", "CVE-2019-9923"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cveCmd := MockNewCveCommand(new(searchService))
buff := bytes.NewBufferString("")
cveCmd.SetOut(buff)
cveCmd.SetErr(buff)
cveCmd.SetArgs(args)
err := cveCmd.Execute()
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
So(err, ShouldBeNil)
So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE zot-cve-test 0.0.1 63a795ca 75MB")
Convey("invalid name and CVE ID", func() {
args := []string{"cvetest", "--image", "test", "--cve-id", "CVE-20807"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cveCmd := MockNewCveCommand(new(searchService))
buff := bytes.NewBufferString("")
cveCmd.SetOut(buff)
cveCmd.SetErr(buff)
cveCmd.SetArgs(args)
err := cveCmd.Execute()
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
So(err, ShouldBeNil)
So(strings.TrimSpace(str), ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
})
})
}
func MockNewCveCommand(searchService SearchService) *cobra.Command {
searchCveParams := make(map[string]*string)
var servURL, user, outputFormat string
var verifyTLS, fixedFlag, verbose bool
cveCmd := &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error {
home, err := os.UserHomeDir()
if err != nil {
panic(err)
}
configPath := path.Join(home + "/.zot")
if len(args) > 0 {
urlFromConfig, err := getConfigValue(configPath, args[0], "url")
if err != nil {
cmd.SilenceUsage = true
return err
}
if urlFromConfig == "" {
return zotErrors.ErrNoURLProvided
}
servURL = urlFromConfig
} else {
return zotErrors.ErrNoURLProvided
}
if len(args) > 0 {
var err error
verifyTLS, err = parseBooleanConfig(configPath, args[0], verifyTLSConfig)
if err != nil {
cmd.SilenceUsage = true
return err
}
}
verbose = false
searchConfig := searchConfig{
params: searchCveParams,
searchService: searchService,
servURL: &servURL,
user: &user,
outputFormat: &outputFormat,
fixedFlag: &fixedFlag,
verifyTLS: &verifyTLS,
verbose: &verbose,
resultWriter: cmd.OutOrStdout(),
}
err = MockSearchCve(searchConfig)
if err != nil {
cmd.SilenceUsage = true
return err
}
return nil
},
}
vars := cveFlagVariables{
searchCveParams: searchCveParams,
servURL: &servURL,
user: &user,
outputFormat: &outputFormat,
fixedFlag: &fixedFlag,
}
setupCveFlags(cveCmd, vars)
return cveCmd
}
func MockSearchCve(searchConfig searchConfig) error {
searchers := getCveSearchers()
for _, searcher := range searchers {
found, err := searcher.search(searchConfig)
if found {
if err != nil {
return err
}
return nil
}
}
return zotErrors.ErrInvalidFlagsCombination
}

View file

@ -129,7 +129,15 @@ func setupImageFlags(imageCmd *cobra.Command, searchImageParams map[string]*stri
}
func searchImage(searchConfig searchConfig) error {
for _, searcher := range getImageSearchers() {
var searchers []searcher
if checkExtEndPoint(*searchConfig.servURL) {
searchers = getImageSearchersGQL()
} else {
searchers = getImageSearchers()
}
for _, searcher := range searchers {
found, err := searcher.search(searchConfig)
if found {
if err != nil {

View file

@ -21,10 +21,12 @@ import (
godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
. "github.com/smartystreets/goconvey/convey"
"github.com/spf13/cobra"
"gopkg.in/resty.v1"
zotErrors "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants"
extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/test"
)
@ -56,6 +58,7 @@ func TestSearchImageCmd(t *testing.T) {
So(err, ShouldBeNil)
})
})
Convey("Test image no url", t, func() {
args := []string{"imagetest", "--name", "dummyIdRandom"}
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`)
@ -126,6 +129,7 @@ func TestSearchImageCmd(t *testing.T) {
So(err, ShouldEqual, zotErrors.ErrInvalidURL)
So(buff.String(), ShouldContainSubstring, "invalid URL format")
})
Convey("Test image invalid url port", t, func() {
args := []string{"imagetest", "--name", "dummyImageName", "--url", "http://localhost:99999"}
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`)
@ -153,6 +157,7 @@ func TestSearchImageCmd(t *testing.T) {
So(buff.String(), ShouldContainSubstring, "invalid port")
})
})
Convey("Test image unreachable", t, func() {
args := []string{"imagetest", "--name", "dummyImageName", "--url", "http://localhost:9999"}
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`)
@ -168,10 +173,8 @@ func TestSearchImageCmd(t *testing.T) {
Convey("Test image url from config", t, func() {
args := []string{"imagetest", "--name", "dummyImageName"}
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`)
defer os.Remove(configPath)
cmd := NewImageCommand(new(mockService))
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
@ -215,15 +218,44 @@ func TestSearchImageCmd(t *testing.T) {
So(err, ShouldBeNil)
})
})
Convey("Test image by digest", t, func() {
args := []string{"imagetest", "--digest", "DigestsA", "--url", "someUrlImage"}
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`)
defer os.Remove(configPath)
imageCmd := NewImageCommand(new(mockService))
buff := bytes.NewBufferString("")
imageCmd.SetOut(buff)
imageCmd.SetErr(buff)
imageCmd.SetArgs(args)
err := imageCmd.Execute()
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE anImage tag DigestsA 123kB")
So(err, ShouldBeNil)
Convey("invalid URL format", func() {
args := []string{"imagetest", "--digest", "digest", "--url", "invalidURL"}
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`)
defer os.Remove(configPath)
imageCmd := NewImageCommand(NewSearchService())
buff := bytes.NewBufferString("")
imageCmd.SetOut(buff)
imageCmd.SetErr(buff)
imageCmd.SetArgs(args)
err := imageCmd.Execute()
So(err, ShouldNotBeNil)
So(err, ShouldEqual, zotErrors.ErrInvalidURL)
So(buff.String(), ShouldContainSubstring, "invalid URL format")
})
})
}
func TestListRepos(t *testing.T) {
Convey("Test listing repositories", t, func() {
args := []string{"config-test"}
configPath := makeConfigFile(`{"configs":[{"_name":"config-test","url":"https://test-url.com","showspinner":false}]}`)
defer os.Remove(configPath)
cmd := NewRepoCommand(new(mockService))
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
@ -264,11 +296,9 @@ func TestListRepos(t *testing.T) {
Convey("Test listing repositories error", t, func() {
args := []string{"config-test"}
configPath := makeConfigFile(`{"configs":[{"_name":"config-test",
"url":"https://invalid.invalid","showspinner":false}]}`)
defer os.Remove(configPath)
cmd := NewRepoCommand(new(searchService))
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
@ -280,10 +310,8 @@ func TestListRepos(t *testing.T) {
Convey("Test unable to get config value", t, func() {
args := []string{"config-test-inexistent"}
configPath := makeConfigFile(`{"configs":[{"_name":"config-test","url":"https://test-url.com","showspinner":false}]}`)
defer os.Remove(configPath)
cmd := NewRepoCommand(new(mockService))
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
@ -295,10 +323,8 @@ func TestListRepos(t *testing.T) {
Convey("Test error - no url provided", t, func() {
args := []string{"config-test"}
configPath := makeConfigFile(`{"configs":[{"_name":"config-test","url":"","showspinner":false}]}`)
defer os.Remove(configPath)
cmd := NewRepoCommand(new(mockService))
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
@ -310,10 +336,8 @@ func TestListRepos(t *testing.T) {
Convey("Test error - no args provided", t, func() {
var args []string
configPath := makeConfigFile(`{"configs":[{"_name":"config-test","url":"","showspinner":false}]}`)
defer os.Remove(configPath)
cmd := NewRepoCommand(new(mockService))
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
@ -325,11 +349,9 @@ func TestListRepos(t *testing.T) {
Convey("Test error - spinner config invalid", t, func() {
args := []string{"config-test"}
configPath := makeConfigFile(`{"configs":[{"_name":"config-test",
"url":"https://test-url.com","showspinner":invalid}]}`)
defer os.Remove(configPath)
cmd := NewRepoCommand(new(mockService))
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
@ -341,11 +363,9 @@ func TestListRepos(t *testing.T) {
Convey("Test error - verifyTLSConfig fails", t, func() {
args := []string{"config-test"}
configPath := makeConfigFile(`{"configs":[{"_name":"config-test",
"verify-tls":"invalid", "url":"https://test-url.com","showspinner":false}]}`)
defer os.Remove(configPath)
cmd := NewRepoCommand(new(mockService))
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
@ -359,10 +379,8 @@ func TestListRepos(t *testing.T) {
func TestOutputFormat(t *testing.T) {
Convey("Test text", t, func() {
args := []string{"imagetest", "--name", "dummyImageName", "-o", "text"}
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`)
defer os.Remove(configPath)
cmd := NewImageCommand(new(mockService))
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
@ -375,12 +393,12 @@ func TestOutputFormat(t *testing.T) {
So(err, ShouldBeNil)
})
// get image config functia
Convey("Test json", t, func() {
args := []string{"imagetest", "--name", "dummyImageName", "-o", "json"}
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`)
defer os.Remove(configPath)
cmd := NewImageCommand(new(mockService))
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
@ -389,17 +407,15 @@ 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", "configDigest": "", "layerDigests": null } ] }`)
So(strings.TrimSpace(str), ShouldEqual, `{ "repoName": "dummyImageName", "tag": "tag", `+
`"configDigest": "", "digest": "DigestsAreReallyLong", "layers": null, "size": "123445" }`)
So(err, ShouldBeNil)
})
Convey("Test yaml", t, func() {
args := []string{"imagetest", "--name", "dummyImageName", "-o", "yaml"}
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`)
defer os.Remove(configPath)
cmd := NewImageCommand(new(mockService))
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
@ -408,16 +424,21 @@ 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 configdigest: "" layers: []`)
So(
strings.TrimSpace(str),
ShouldEqual,
`reponame: dummyImageName tag: tag configdigest: "" `+
`digest: DigestsAreReallyLong layers: [] size: "123445"`,
)
So(err, ShouldBeNil)
Convey("Test yml", func() {
args := []string{"imagetest", "--name", "dummyImageName", "-o", "yml"}
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`)
configPath := makeConfigFile(
`{"configs":[{"_name":"imagetest",` +
`"url":"https://test-url.com","showspinner":false}]}`,
)
defer os.Remove(configPath)
cmd := NewImageCommand(new(mockService))
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
@ -426,18 +447,20 @@ 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 configdigest: "" layers: []`)
So(
strings.TrimSpace(str),
ShouldEqual,
`reponame: dummyImageName tag: tag configdigest: "" `+
`digest: DigestsAreReallyLong layers: [] size: "123445"`,
)
So(err, ShouldBeNil)
})
})
Convey("Test invalid", t, func() {
args := []string{"imagetest", "--name", "dummyImageName", "-o", "random"}
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`)
defer os.Remove(configPath)
cmd := NewImageCommand(new(mockService))
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
@ -449,7 +472,7 @@ func TestOutputFormat(t *testing.T) {
})
}
func TestServerResponse(t *testing.T) {
func TestServerResponseGQL(t *testing.T) {
Convey("Test from real server", t, func() {
port := test.GetFreePort()
url := test.GetBaseURL(port)
@ -491,7 +514,6 @@ func TestServerResponse(t *testing.T) {
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cmd := NewImageCommand(new(searchService))
// buff := bytes.NewBufferString("")
buff := &bytes.Buffer{}
cmd.SetOut(buff)
cmd.SetErr(buff)
@ -504,6 +526,19 @@ func TestServerResponse(t *testing.T) {
So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 15B")
So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 15B")
Convey("Test all images invalid output format", func() {
args := []string{"imagetest", "-o", "random"}
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, ShouldNotBeNil)
So(buff.String(), ShouldContainSubstring, "invalid output format")
})
})
Convey("Test all images verbose", func() {
@ -567,6 +602,20 @@ func TestServerResponse(t *testing.T) {
So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 15B")
So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 15B")
})
Convey("invalid output format", func() {
args := []string{"imagetest", "--name", "repo7", "-o", "random"}
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, ShouldNotBeNil)
So(buff.String(), ShouldContainSubstring, "invalid output format")
})
})
Convey("Test image by digest", func() {
@ -590,6 +639,7 @@ func TestServerResponse(t *testing.T) {
So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 15B")
So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 15B")
Convey("with shorthand", func() {
args := []string{"imagetest", "-d", "883fc0c5"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
@ -608,10 +658,23 @@ func TestServerResponse(t *testing.T) {
So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 15B")
So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 15B")
})
Convey("nonexistent digest", func() {
args := []string{"imagetest", "--digest", "d1g35t"}
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)
So(len(buff.String()), ShouldEqual, 0)
})
Convey("Test image by name invalid name", func() {
args := []string{"imagetest", "--name", "repo777"}
Convey("invalid output format", func() {
args := []string{"imagetest", "--digest", "883fc0c5", "-o", "random"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cmd := NewImageCommand(new(searchService))
@ -621,8 +684,22 @@ func TestServerResponse(t *testing.T) {
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
actual := buff.String()
So(actual, ShouldContainSubstring, "unknown")
So(buff.String(), ShouldContainSubstring, "invalid output format")
})
})
Convey("Test image by name nonexistent name", func() {
args := []string{"imagetest", "--name", "repo777"}
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)
So(len(buff.String()), ShouldEqual, 0)
})
Convey("Test list repos error", func() {
@ -648,6 +725,361 @@ func TestServerResponse(t *testing.T) {
})
}
func TestServerResponse(t *testing.T) {
Convey("Test from real server", t, func() {
port := test.GetFreePort()
url := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
defaultVal := true
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{Enable: &defaultVal},
}
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()
go func(controller *api.Controller) {
// this blocks
if err := controller.Run(context.Background()); err != nil {
return
}
}(ctlr)
// wait till ready
for {
_, err := resty.R().Get(url)
if err == nil {
break
}
time.Sleep(100 * time.Millisecond)
}
defer func(controller *api.Controller) {
ctx := context.Background()
_ = controller.Server.Shutdown(ctx)
}(ctlr)
err := uploadManifest(url)
t.Logf("%s", ctlr.Config.Storage.RootDirectory)
So(err, ShouldBeNil)
Convey("Test all images", func() {
t.Logf("%s", ctlr.Config.Storage.RootDirectory)
args := []string{"imagetest"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cmd := MockNewImageCommand(new(searchService))
buff := &bytes.Buffer{}
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)
So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 15B")
So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 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 := MockNewImageCommand(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 883fc0c5 3a1d2d0c 15B b8781e88 15B")
So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 3a1d2d0c 15B b8781e88 15B")
})
Convey("Test image by name", func() {
args := []string{"imagetest", "--name", "repo7"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cmd := MockNewImageCommand(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)
So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 15B")
So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 15B")
})
Convey("Test image by digest", func() {
args := []string{"imagetest", "--digest", "883fc0c5"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cmd := MockNewImageCommand(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 SIZE
// repo7 test:2.0 a0ca253b 15B
// repo7 test:1.0 a0ca253b 15B
So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 15B")
So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 15B")
Convey("nonexistent digest", func() {
args := []string{"imagetest", "--digest", "d1g35t"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cmd := MockNewImageCommand(new(searchService))
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldBeNil)
So(len(buff.String()), ShouldEqual, 0)
})
})
Convey("Test image by name nonexistent name", func() {
args := []string{"imagetest", "--name", "repo777"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cmd := MockNewImageCommand(new(searchService))
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
actual := buff.String()
So(actual, ShouldContainSubstring, "unknown")
})
})
}
func TestServerResponseGQLWithoutPermissions(t *testing.T) {
port := test.GetFreePort()
url := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
dir := t.TempDir()
err := test.CopyFiles("../../test/data/zot-test", path.Join(dir, "zot-test"))
if err != nil {
panic(err)
}
err = os.Chmod(path.Join(dir, "zot-test", "blobs"), 0o000)
if err != nil {
panic(err)
}
conf.Storage.RootDirectory = dir
cveConfig := &extconf.CVEConfig{
UpdateInterval: 2,
}
defaultVal := true
searchConfig := &extconf.SearchConfig{
CVE: cveConfig,
Enable: &defaultVal,
}
conf.Extensions = &extconf.ExtensionConfig{
Search: searchConfig,
}
ctlr := api.NewController(conf)
go func(controller *api.Controller) {
// this blocks
if err := controller.Run(context.Background()); err != nil {
return
}
}(ctlr)
// wait till ready
for {
res, err := resty.R().Get(url + constants.ExtSearchPrefix)
if err == nil && res.StatusCode() == 422 {
break
}
time.Sleep(100 * time.Millisecond)
}
time.Sleep(90 * time.Second)
defer func(controller *api.Controller) {
err = os.Chmod(path.Join(dir, "zot-test", "blobs"), 0o777)
if err != nil {
panic(err)
}
ctx := context.Background()
_ = controller.Server.Shutdown(ctx)
}(ctlr)
Convey("Test all images", t, func() {
args := []string{"imagetest"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cveCmd := NewImageCommand(new(searchService))
buff := bytes.NewBufferString("")
cveCmd.SetOut(buff)
cveCmd.SetErr(buff)
cveCmd.SetArgs(args)
err = cveCmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("Test all images verbose", t, 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, ShouldNotBeNil)
})
Convey("Test image by name", t, func() {
args := []string{"imagetest", "--name", "zot-test"}
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, ShouldNotBeNil)
})
Convey("Test image by digest", t, func() {
args := []string{"imagetest", "--digest", "2bacca16"}
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, ShouldNotBeNil)
})
}
func MockNewImageCommand(searchService SearchService) *cobra.Command {
searchImageParams := make(map[string]*string)
var servURL, user, outputFormat string
var verifyTLS, verbose bool
imageCmd := &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error {
home, err := os.UserHomeDir()
if err != nil {
panic(err)
}
configPath := path.Join(home + "/.zot")
if len(args) > 0 {
urlFromConfig, err := getConfigValue(configPath, args[0], "url")
if err != nil {
cmd.SilenceUsage = true
return err
}
if urlFromConfig == "" {
return zotErrors.ErrNoURLProvided
}
servURL = urlFromConfig
} else {
return zotErrors.ErrNoURLProvided
}
if len(args) > 0 {
var err error
verifyTLS, err = parseBooleanConfig(configPath, args[0], verifyTLSConfig)
if err != nil {
cmd.SilenceUsage = true
return err
}
}
searchConfig := searchConfig{
params: searchImageParams,
searchService: searchService,
servURL: &servURL,
user: &user,
outputFormat: &outputFormat,
verbose: &verbose,
verifyTLS: &verifyTLS,
resultWriter: cmd.OutOrStdout(),
}
err = MockSearchImage(searchConfig)
if err != nil {
cmd.SilenceUsage = true
return err
}
return nil
},
}
setupImageFlags(imageCmd, searchImageParams, &servURL, &user, &outputFormat, &verbose)
imageCmd.SetUsageTemplate(imageCmd.UsageTemplate() + usageFooter)
return imageCmd
}
func MockSearchImage(searchConfig searchConfig) error {
searchers := getImageSearchers()
for _, searcher := range searchers {
found, err := searcher.search(searchConfig)
if found {
if err != nil {
return err
}
return nil
}
}
return zotErrors.ErrInvalidFlagsCombination
}
func uploadManifest(url string) error {
// create a blob/layer
resp, _ := resty.R().Post(url + "/v2/repo7/blobs/uploads/")
@ -741,6 +1173,131 @@ func (service mockService) getRepos(ctx context.Context, config searchConfig, us
channel <- stringResult{"", nil}
}
func (service mockService) getImagesGQL(ctx context.Context, config searchConfig, username, password string,
imageName string,
) (*imageListStructGQL, error) {
imageListGQLResponse := &imageListStructGQL{}
imageListGQLResponse.Data.ImageList = []imageStruct{
{
RepoName: "dummyImageName",
Tag: "tag",
Digest: "DigestsAreReallyLong",
Size: "123445",
},
}
return imageListGQLResponse, nil
}
func (service mockService) getImagesByDigestGQL(ctx context.Context, config searchConfig, username, password string,
digest string,
) (*imageListStructForDigestGQL, error) {
imageListGQLResponse := &imageListStructForDigestGQL{}
imageListGQLResponse.Data.ImageList = []imageStruct{
{
RepoName: "randomimageName",
Tag: "tag",
Digest: "DigestsAreReallyLong",
Size: "123445",
},
}
return imageListGQLResponse, nil
}
func (service mockService) getImagesByCveIDGQL(ctx context.Context, config searchConfig, username, password string,
digest string,
) (*imagesForCve, error) {
imagesForCve := &imagesForCve{
Errors: nil,
Data: struct {
ImageList []imageStruct `json:"ImageListForCVE"` // nolint:tagliatelle
}{},
}
imagesForCve.Errors = nil
mockedImage := service.getMockedImageByName("anImage")
imagesForCve.Data.ImageList = []imageStruct{mockedImage}
return imagesForCve, nil
}
func (service mockService) getTagsForCVEGQL(ctx context.Context, config searchConfig, username, password,
imageName, cveID string,
) (*imagesForCve, error) {
images := &imagesForCve{
Errors: nil,
Data: struct {
ImageList []imageStruct `json:"ImageListForCVE"` //nolint:tagliatelle // graphQL schema
}{},
}
images.Errors = nil
mockedImage := service.getMockedImageByName(imageName)
images.Data.ImageList = []imageStruct{mockedImage}
return images, nil
}
func (service mockService) getFixedTagsForCVEGQL(ctx context.Context, config searchConfig, username, password,
imageName, cveID string,
) (*fixedTags, error) {
fixedTags := &fixedTags{
Errors: nil,
Data: struct {
ImageList []imageStruct `json:"ImageListWithCVEFixed"` //nolint:tagliatelle // graphQL schema
}{},
}
fixedTags.Errors = nil
mockedImage := service.getMockedImageByName(imageName)
fixedTags.Data.ImageList = []imageStruct{mockedImage}
return fixedTags, nil
}
func (service mockService) getCveByImageGQL(ctx context.Context, config searchConfig, username, password,
imageName string,
) (*cveResult, error) {
cveRes := &cveResult{}
cveRes.Data = cveData{
CVEListForImage: cveListForImage{
Tag: imageName,
CVEList: []cve{
{
ID: "dummyCVEID",
Description: "Description of the CVE",
Title: "Title of that CVE",
Severity: "HIGH",
PackageList: []packageList{
{
Name: "packagename",
FixedVersion: "fixedver",
InstalledVersion: "installedver",
},
},
},
},
},
}
return cveRes, nil
}
// nolint: goconst
func (service mockService) getMockedImageByName(imageName string) imageStruct {
image := imageStruct{}
image.RepoName = imageName
image.Tag = "tag"
image.Digest = "DigestsAreReallyLong"
image.Size = "123445"
return image
}
func (service mockService) getAllImages(ctx context.Context, config searchConfig, username, password string,
channel chan stringResult, wtgrp *sync.WaitGroup,
) {
@ -748,14 +1305,10 @@ func (service mockService) getAllImages(ctx context.Context, config searchConfig
defer close(channel)
image := &imageStruct{}
image.Name = "randomimageName"
image.Tags = []tags{
{
Name: "tag",
Digest: "DigestsAreReallyLong",
Size: 123445,
},
}
image.RepoName = "randomimageName"
image.Tag = "tag"
image.Digest = "DigestsAreReallyLong"
image.Size = "123445"
str, err := image.string(*config.outputFormat)
if err != nil {
@ -774,14 +1327,10 @@ func (service mockService) getImageByName(ctx context.Context, config searchConf
defer close(channel)
image := &imageStruct{}
image.Name = imageName
image.Tags = []tags{
{
Name: "tag",
Digest: "DigestsAreReallyLong",
Size: 123445,
},
}
image.RepoName = imageName
image.Tag = "tag"
image.Digest = "DigestsAreReallyLong"
image.Size = "123445"
str, err := image.string(*config.outputFormat)
if err != nil {
@ -831,6 +1380,18 @@ func (service mockService) getCveByImage(ctx context.Context, config searchConfi
rch <- stringResult{str, nil}
}
func (service mockService) getFixedTagsForCVE(ctx context.Context, config searchConfig,
username, password, imageName, cvid string, rch chan stringResult, wtgrp *sync.WaitGroup,
) {
service.getImageByName(ctx, config, username, password, imageName, rch, wtgrp)
}
func (service mockService) getImageByNameAndCVEID(ctx context.Context, config searchConfig, username,
password, imageName, cvid string, rch chan stringResult, wtgrp *sync.WaitGroup,
) {
service.getImageByName(ctx, config, username, password, imageName, rch, wtgrp)
}
func (service mockService) getImagesByCveID(ctx context.Context, config searchConfig, username, password, cvid string,
rch chan stringResult, wtgrp *sync.WaitGroup,
) {
@ -843,18 +1404,6 @@ func (service mockService) getImagesByDigest(ctx context.Context, config searchC
service.getImageByName(ctx, config, username, password, "anImage", rch, wtgrp)
}
func (service mockService) getImageByNameAndCVEID(ctx context.Context, config searchConfig, username,
password, imageName, cvid string, rch chan stringResult, wtgrp *sync.WaitGroup,
) {
service.getImageByName(ctx, config, username, password, imageName, rch, wtgrp)
}
func (service mockService) getFixedTagsForCVE(ctx context.Context, config searchConfig,
username, password, imageName, cvid string, rch chan stringResult, wtgrp *sync.WaitGroup,
) {
service.getImageByName(ctx, config, username, password, imageName, rch, wtgrp)
}
func makeConfigFile(content string) string {
os.Setenv("HOME", os.TempDir())

View file

@ -37,6 +37,27 @@ func getCveSearchers() []searcher {
return searchers
}
func getImageSearchersGQL() []searcher {
searchers := []searcher{
new(allImagesSearcherGQL),
new(imageByNameSearcherGQL),
new(imagesByDigestSearcherGQL),
}
return searchers
}
func getCveSearchersGQL() []searcher {
searchers := []searcher{
new(cveByImageSearcherGQL),
new(imagesByCVEIDSearcherGQL),
new(tagsByImageNameAndCVEIDSearcherGQL),
new(fixedTagsSearcherGQL),
}
return searchers
}
type searcher interface {
search(searchConfig searchConfig) (bool, error)
}
@ -96,6 +117,18 @@ func (search allImagesSearcher) search(config searchConfig) (bool, error) {
}
}
type allImagesSearcherGQL struct{}
func (search allImagesSearcherGQL) search(config searchConfig) (bool, error) {
if !canSearch(config.params, newSet("")) {
return false, nil
}
err := getImages(config)
return true, err
}
type imageByNameSearcher struct{}
func (search imageByNameSearcher) search(config searchConfig) (bool, error) {
@ -128,6 +161,32 @@ func (search imageByNameSearcher) search(config searchConfig) (bool, error) {
}
}
type imageByNameSearcherGQL struct{}
func (search imageByNameSearcherGQL) search(config searchConfig) (bool, error) {
if !canSearch(config.params, newSet("imageName")) {
return false, nil
}
err := getImages(config)
return true, err
}
func getImages(config searchConfig) error {
username, password := getUsernameAndPassword(*config.user)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
imageList, err := config.searchService.getImagesGQL(ctx, config, username, password, *config.params["imageName"])
if err != nil {
return err
}
return printResult(config, imageList.Data.ImageList)
}
type imagesByDigestSearcher struct{}
func (search imagesByDigestSearcher) search(config searchConfig) (bool, error) {
@ -160,6 +219,32 @@ func (search imagesByDigestSearcher) search(config searchConfig) (bool, error) {
}
}
type imagesByDigestSearcherGQL struct{}
func (search imagesByDigestSearcherGQL) search(config searchConfig) (bool, error) {
if !canSearch(config.params, newSet("digest")) {
return false, nil
}
// var builder strings.Builder
username, password := getUsernameAndPassword(*config.user)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
imageList, err := config.searchService.getImagesByDigestGQL(ctx, config, username, password, *config.params["digest"])
if err != nil {
return true, err
}
if err := printResult(config, imageList.Data.ImageList); err != nil {
return true, err
}
return true, nil
}
type cveByImageSearcher struct{}
func (search cveByImageSearcher) search(config searchConfig) (bool, error) {
@ -195,10 +280,49 @@ func (search cveByImageSearcher) search(config searchConfig) (bool, error) {
}
}
type cveByImageSearcherGQL struct{}
func (search cveByImageSearcherGQL) search(config searchConfig) (bool, error) {
if !canSearch(config.params, newSet("imageName")) || *config.fixedFlag {
return false, nil
}
if !validateImageNameTag(*config.params["imageName"]) {
return true, errInvalidImageNameAndTag
}
var builder strings.Builder
username, password := getUsernameAndPassword(*config.user)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cveList, err := config.searchService.getCveByImageGQL(ctx, config, username, password, *config.params["imageName"])
if err != nil {
return true, err
}
if len(cveList.Data.CVEListForImage.CVEList) > 0 &&
(*config.outputFormat == defaultOutoutFormat || *config.outputFormat == "") {
printCVETableHeader(&builder, *config.verbose)
fmt.Fprint(config.resultWriter, builder.String())
}
out, err := cveList.string(*config.outputFormat)
if err != nil {
return true, err
}
fmt.Fprint(config.resultWriter, out)
return true, nil
}
type imagesByCVEIDSearcher struct{}
func (search imagesByCVEIDSearcher) search(config searchConfig) (bool, error) {
if !canSearch(config.params, newSet("cvid")) || *config.fixedFlag {
if !canSearch(config.params, newSet("cveID")) || *config.fixedFlag {
return false, nil
}
@ -210,7 +334,7 @@ func (search imagesByCVEIDSearcher) search(config searchConfig) (bool, error) {
wg.Add(1)
go config.searchService.getImagesByCveID(ctx, config, username, password, *config.params["cvid"], strErr, &wg)
go config.searchService.getImagesByCveID(ctx, config, username, password, *config.params["cveID"], strErr, &wg)
wg.Add(1)
errCh := make(chan error, 1)
@ -226,10 +350,34 @@ func (search imagesByCVEIDSearcher) search(config searchConfig) (bool, error) {
}
}
type imagesByCVEIDSearcherGQL struct{}
func (search imagesByCVEIDSearcherGQL) search(config searchConfig) (bool, error) {
if !canSearch(config.params, newSet("cveID")) || *config.fixedFlag {
return false, nil
}
username, password := getUsernameAndPassword(*config.user)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
imageList, err := config.searchService.getImagesByCveIDGQL(ctx, config, username, password, *config.params["cveID"])
if err != nil {
return true, err
}
if err := printResult(config, imageList.Data.ImageList); err != nil {
return true, err
}
return true, nil
}
type tagsByImageNameAndCVEIDSearcher struct{}
func (search tagsByImageNameAndCVEIDSearcher) search(config searchConfig) (bool, error) {
if !canSearch(config.params, newSet("cvid", "imageName")) || *config.fixedFlag {
if !canSearch(config.params, newSet("cveID", "imageName")) || *config.fixedFlag {
return false, nil
}
@ -246,7 +394,7 @@ func (search tagsByImageNameAndCVEIDSearcher) search(config searchConfig) (bool,
wg.Add(1)
go config.searchService.getImageByNameAndCVEID(ctx, config, username, password, *config.params["imageName"],
*config.params["cvid"], strErr, &wg)
*config.params["cveID"], strErr, &wg)
wg.Add(1)
errCh := make(chan error, 1)
@ -262,10 +410,38 @@ func (search tagsByImageNameAndCVEIDSearcher) search(config searchConfig) (bool,
}
}
type tagsByImageNameAndCVEIDSearcherGQL struct{}
func (search tagsByImageNameAndCVEIDSearcherGQL) search(config searchConfig) (bool, error) {
if !canSearch(config.params, newSet("cveID", "imageName")) || *config.fixedFlag {
return false, nil
}
if strings.Contains(*config.params["imageName"], ":") {
return true, errInvalidImageName
}
err := getTagsByCVE(config)
return true, err
}
type fixedTagsSearcherGQL struct{}
func (search fixedTagsSearcherGQL) search(config searchConfig) (bool, error) {
if !canSearch(config.params, newSet("cveID", "imageName")) || !*config.fixedFlag {
return false, nil
}
err := getTagsByCVE(config)
return true, err
}
type fixedTagsSearcher struct{}
func (search fixedTagsSearcher) search(config searchConfig) (bool, error) {
if !canSearch(config.params, newSet("cvid", "imageName")) || !*config.fixedFlag {
if !canSearch(config.params, newSet("cveID", "imageName")) || !*config.fixedFlag {
return false, nil
}
@ -282,7 +458,7 @@ func (search fixedTagsSearcher) search(config searchConfig) (bool, error) {
wg.Add(1)
go config.searchService.getFixedTagsForCVE(ctx, config, username, password, *config.params["imageName"],
*config.params["cvid"], strErr, &wg)
*config.params["cveID"], strErr, &wg)
wg.Add(1)
errCh := make(chan error, 1)
@ -298,6 +474,39 @@ func (search fixedTagsSearcher) search(config searchConfig) (bool, error) {
}
}
func getTagsByCVE(config searchConfig) error {
if strings.Contains(*config.params["imageName"], ":") {
return errInvalidImageName
}
username, password := getUsernameAndPassword(*config.user)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var imageList []imageStruct
if *config.fixedFlag {
fixedTags, err := config.searchService.getFixedTagsForCVEGQL(ctx, config, username, password,
*config.params["imageName"], *config.params["cveID"])
if err != nil {
return err
}
imageList = fixedTags.Data.ImageList
} else {
tags, err := config.searchService.getTagsForCVEGQL(ctx, config, username, password,
*config.params["imageName"], *config.params["cveID"])
if err != nil {
return err
}
imageList = tags.Data.ImageList
}
return printResult(config, imageList)
}
func collectResults(config searchConfig, wg *sync.WaitGroup, imageErr chan stringResult,
cancel context.CancelFunc, printHeader printHeader, errCh chan error,
) {
@ -376,12 +585,14 @@ type spinnerState struct {
enabled bool
}
// nolint
func (spinner *spinnerState) startSpinner() {
if spinner.enabled {
spinner.spinner.Start()
}
}
// nolint
func (spinner *spinnerState) stopSpinner() {
if spinner.enabled && spinner.spinner.Active() {
spinner.spinner.Stop()
@ -397,14 +608,14 @@ func getEmptyStruct() struct{} {
}
func newSet(initialValues ...string) *set {
ret := &set{}
ret.m = make(map[string]struct{})
setValues := &set{}
setValues.m = make(map[string]struct{})
for _, val := range initialValues {
ret.m[val] = getEmptyStruct()
setValues.m[val] = getEmptyStruct()
}
return ret
return setValues
}
func (s *set) contains(value string) bool {
@ -413,6 +624,10 @@ func (s *set) contains(value string) bool {
return c
}
const (
waitTimeout = httpTimeout + 5*time.Second
)
var (
ErrCannotSearch = errors.New("cannot search with these parameters")
ErrInvalidOutputFormat = errors.New("invalid output format")
@ -465,9 +680,28 @@ func printCVETableHeader(writer io.Writer, verbose bool) {
table.Render()
}
const (
waitTimeout = httpTimeout + 5*time.Second
)
func printResult(config searchConfig, imageList []imageStruct) error {
var builder strings.Builder
if len(imageList) > 0 {
printImageTableHeader(&builder, *config.verbose)
fmt.Fprint(config.resultWriter, builder.String())
}
for i := range imageList {
img := imageList[i]
img.verbose = *config.verbose
out, err := img.string(*config.outputFormat)
if err != nil {
return err
}
fmt.Fprint(config.resultWriter, out)
}
return nil
}
var (
errInvalidImageNameAndTag = errors.New("cli: Invalid input format. Expected IMAGENAME:TAG")

View file

@ -9,9 +9,9 @@ import (
"fmt"
"io"
"net/url"
"strconv"
"strings"
"sync"
"time"
"github.com/dustin/go-humanize"
jsoniter "github.com/json-iterator/go"
@ -22,22 +22,35 @@ import (
)
type SearchService interface {
getImagesGQL(ctx context.Context, config searchConfig, username, password string,
imageName string) (*imageListStructGQL, error)
getImagesByDigestGQL(ctx context.Context, config searchConfig, username, password string,
digest string) (*imageListStructForDigestGQL, error)
getCveByImageGQL(ctx context.Context, config searchConfig, username, password,
imageName string) (*cveResult, error)
getImagesByCveIDGQL(ctx context.Context, config searchConfig, username, password string,
digest string) (*imagesForCve, error)
getTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName,
cveID string) (*imagesForCve, error)
getFixedTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName,
cveID string) (*fixedTags, error)
getAllImages(ctx context.Context, config searchConfig, username, password string,
channel chan stringResult, wtgrp *sync.WaitGroup)
getImageByName(ctx context.Context, config searchConfig, username, password, imageName string,
channel chan stringResult, wtgrp *sync.WaitGroup)
getCveByImage(ctx context.Context, config searchConfig, username, password, imageName string,
channel chan stringResult, wtgrp *sync.WaitGroup)
getImagesByCveID(ctx context.Context, config searchConfig, username, password, cvid string,
channel chan stringResult, wtgrp *sync.WaitGroup)
getImagesByDigest(ctx context.Context, config searchConfig, username, password, digest string,
channel chan stringResult, wtgrp *sync.WaitGroup)
getImageByNameAndCVEID(ctx context.Context, config searchConfig, username, password, imageName, cvid string,
channel chan stringResult, wtgrp *sync.WaitGroup)
getFixedTagsForCVE(ctx context.Context, config searchConfig, username, password, imageName, cvid string,
channel chan stringResult, wtgrp *sync.WaitGroup)
getRepos(ctx context.Context, config searchConfig, username, password string,
channel chan stringResult, wtgrp *sync.WaitGroup)
getImageByName(ctx context.Context, config searchConfig, username, password, imageName string,
channel chan stringResult, wtgrp *sync.WaitGroup)
getImageByNameAndCVEID(ctx context.Context, config searchConfig, username, password, imageName, cvid string,
channel chan stringResult, wtgrp *sync.WaitGroup)
}
type searchService struct{}
@ -46,6 +59,116 @@ func NewSearchService() SearchService {
return searchService{}
}
func (service searchService) getImagesGQL(ctx context.Context, config searchConfig, username, password string,
imageName string,
) (*imageListStructGQL, error) {
query := fmt.Sprintf(`{ImageList(repo: "%s") {`+`
RepoName Tag Digest ConfigDigest Size Layers {Size Digest}}
}`,
imageName)
result := &imageListStructGQL{}
err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil {
return nil, errResult
}
return result, nil
}
func (service searchService) getImagesByDigestGQL(ctx context.Context, config searchConfig, username, password string,
digest string,
) (*imageListStructForDigestGQL, error) {
query := fmt.Sprintf(`{ImageListForDigest(id: "%s") {`+`
RepoName Tag Digest ConfigDigest Size Layers {Size Digest}}
}`,
digest)
result := &imageListStructForDigestGQL{}
err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil {
return nil, errResult
}
return result, nil
}
func (service searchService) getImagesByCveIDGQL(ctx context.Context, config searchConfig, username,
password, cveID string,
) (*imagesForCve, error) {
query := fmt.Sprintf(`{ImageListForCVE(id: "%s") {`+`
RepoName Tag Digest Size}
}`,
cveID)
result := &imagesForCve{}
err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil {
return nil, errResult
}
return result, nil
}
func (service searchService) getCveByImageGQL(ctx context.Context, config searchConfig, username, password,
imageName string,
) (*cveResult, error) {
query := fmt.Sprintf(`{ CVEListForImage (image:"%s")`+
` { Tag CVEList { Id Title Severity Description `+
`PackageList {Name InstalledVersion FixedVersion}} } }`, imageName)
result := &cveResult{}
err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil {
return nil, errResult
}
result.Data.CVEListForImage.CVEList = groupCVEsBySeverity(result.Data.CVEListForImage.CVEList)
return result, nil
}
func (service searchService) getTagsForCVEGQL(ctx context.Context, config searchConfig,
username, password, imageName, cveID string,
) (*imagesForCve, error) {
query := fmt.Sprintf(`{ImageListForCVE(id: "%s") {`+`
RepoName Tag Digest Size}
}`,
cveID)
result := &imagesForCve{}
err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil {
return nil, errResult
}
return result, nil
}
func (service searchService) getFixedTagsForCVEGQL(ctx context.Context, config searchConfig,
username, password, imageName, cveID string,
) (*fixedTags, error) {
query := fmt.Sprintf(`{ImageListWithCVEFixed(id: "%s", image: "%s") {`+`
RepoName Tag Digest Size}
}`,
cveID, imageName)
result := &fixedTags{}
err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil {
return nil, errResult
}
return result, nil
}
func (service searchService) getImageByName(ctx context.Context, config searchConfig,
username, password, imageName string, rch chan stringResult, wtgrp *sync.WaitGroup,
) {
@ -126,8 +249,8 @@ func getImage(ctx context.Context, config searchConfig, username, password, imag
return
}
tagsList := &tagListResp{}
_, err = makeGETRequest(ctx, tagListEndpoint, username, password, *config.verifyTLS, &tagsList)
tagList := &tagListResp{}
_, err = makeGETRequest(ctx, tagListEndpoint, username, password, *config.verifyTLS, &tagList)
if err != nil {
if isContextDone(ctx) {
@ -138,7 +261,7 @@ func getImage(ctx context.Context, config searchConfig, username, password, imag
return
}
for _, tag := range tagsList.Tags {
for _, tag := range tagList.Tags {
wtgrp.Add(1)
go addManifestCallToPool(ctx, config, pool, username, password, imageName, tag, rch, wtgrp)
@ -152,7 +275,7 @@ func (service searchService) getImagesByCveID(ctx context.Context, config search
defer close(rch)
query := fmt.Sprintf(`{ImageListForCVE(id: "%s") {`+`
Name Tags }
RepoName Tag Digest Size}
}`,
cvid)
result := &imagesForCve{}
@ -189,12 +312,10 @@ func (service searchService) getImagesByCveID(ctx context.Context, config search
go rlim.startRateLimiter(ctx)
for _, image := range result.Data.ImageListForCVE {
for _, tag := range image.Tags {
for _, image := range result.Data.ImageList {
localWg.Add(1)
go addManifestCallToPool(ctx, config, rlim, username, password, image.Name, tag, rch, &localWg)
}
go addManifestCallToPool(ctx, config, rlim, username, password, image.RepoName, image.Tag, rch, &localWg)
}
localWg.Wait()
@ -207,7 +328,7 @@ func (service searchService) getImagesByDigest(ctx context.Context, config searc
defer close(rch)
query := fmt.Sprintf(`{ImageListForDigest(id: "%s") {`+`
Name Tags }
RepoName Tag Digest ConfigDigest Size Layers {Size Digest}}
}`,
digest)
result := &imagesForDigest{}
@ -244,12 +365,10 @@ func (service searchService) getImagesByDigest(ctx context.Context, config searc
go rlim.startRateLimiter(ctx)
for _, image := range result.Data.ImageListForDigest {
for _, tag := range image.Tags {
for _, image := range result.Data.ImageList {
localWg.Add(1)
go addManifestCallToPool(ctx, config, rlim, username, password, image.Name, tag, rch, &localWg)
}
go addManifestCallToPool(ctx, config, rlim, username, password, image.RepoName, image.Tag, rch, &localWg)
}
localWg.Wait()
@ -262,7 +381,7 @@ func (service searchService) getImageByNameAndCVEID(ctx context.Context, config
defer close(rch)
query := fmt.Sprintf(`{ImageListForCVE(id: "%s") {`+`
Name Tags }
RepoName Tag Digest ConfigDigest Size Layers {Size Digest}}
}`,
cvid)
result := &imagesForCve{}
@ -299,16 +418,14 @@ func (service searchService) getImageByNameAndCVEID(ctx context.Context, config
go rlim.startRateLimiter(ctx)
for _, image := range result.Data.ImageListForCVE {
if !strings.EqualFold(imageName, image.Name) {
for _, image := range result.Data.ImageList {
if !strings.EqualFold(imageName, image.RepoName) {
continue
}
for _, tag := range image.Tags {
localWg.Add(1)
go addManifestCallToPool(ctx, config, rlim, username, password, image.Name, tag, rch, &localWg)
}
go addManifestCallToPool(ctx, config, rlim, username, password, image.RepoName, image.Tag, rch, &localWg)
}
localWg.Wait()
@ -368,6 +485,59 @@ func (service searchService) getCveByImage(ctx context.Context, config searchCon
rch <- stringResult{str, nil}
}
func (service searchService) getFixedTagsForCVE(ctx context.Context, config searchConfig,
username, password, imageName, cvid string, rch chan stringResult, wtgrp *sync.WaitGroup,
) {
defer wtgrp.Done()
defer close(rch)
query := fmt.Sprintf(`{ImageListWithCVEFixed (id: "%s", image: "%s") {`+`
RepoName Tag Digest Size}
}`,
cvid, imageName)
result := &fixedTags{}
err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
if err != nil {
if isContextDone(ctx) {
return
}
rch <- stringResult{"", err}
return
}
if result.Errors != nil {
var errBuilder strings.Builder
for _, err := range result.Errors {
fmt.Fprintln(&errBuilder, err.Message)
}
if isContextDone(ctx) {
return
}
rch <- stringResult{"", errors.New(errBuilder.String())} //nolint: goerr113
return
}
var localWg sync.WaitGroup
rlim := newSmoothRateLimiter(&localWg, rch)
localWg.Add(1)
go rlim.startRateLimiter(ctx)
for _, img := range result.Data.ImageList {
localWg.Add(1)
go addManifestCallToPool(ctx, config, rlim, username, password, imageName, img.Tag, rch, &localWg)
}
localWg.Wait()
}
func groupCVEsBySeverity(cveList []cve) []cve {
var (
unknown = make([]cve, 0)
@ -421,63 +591,10 @@ func isContextDone(ctx context.Context) bool {
}
}
func (service searchService) getFixedTagsForCVE(ctx context.Context, config searchConfig,
username, password, imageName, cvid string, rch chan stringResult, wtgrp *sync.WaitGroup,
) {
defer wtgrp.Done()
defer close(rch)
query := fmt.Sprintf(`{ImageListWithCVEFixed (id: "%s", image: "%s") {`+`
Tags {Name Timestamp} }
}`,
cvid, imageName)
result := &fixedTags{}
err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
if err != nil {
if isContextDone(ctx) {
return
}
rch <- stringResult{"", err}
return
}
if result.Errors != nil {
var errBuilder strings.Builder
for _, err := range result.Errors {
fmt.Fprintln(&errBuilder, err.Message)
}
if isContextDone(ctx) {
return
}
rch <- stringResult{"", errors.New(errBuilder.String())} //nolint: goerr113
return
}
var localWg sync.WaitGroup
rlim := newSmoothRateLimiter(&localWg, rch)
localWg.Add(1)
go rlim.startRateLimiter(ctx)
for _, imgTag := range result.Data.ImageListWithCVEFixed.Tags {
localWg.Add(1)
go addManifestCallToPool(ctx, config, rlim, username, password, imageName, imgTag.Name, rch, &localWg)
}
localWg.Wait()
}
// Query using JQL, the query string is passed as a parameter
// errors are returned in the stringResult channel, the unmarshalled payload is in resultPtr.
func (service searchService) makeGraphQLQuery(ctx context.Context, config searchConfig,
username, password, query string,
func (service searchService) makeGraphQLQuery(ctx context.Context,
config searchConfig, username, password, query string,
resultPtr interface{},
) error {
endPoint, err := combineServerAndEndpointURL(*config.servURL, constants.ExtSearchPrefix)
@ -493,6 +610,34 @@ func (service searchService) makeGraphQLQuery(ctx context.Context, config search
return nil
}
func checkResultGraphQLQuery(ctx context.Context, err error, resultErrors []errorGraphQL,
) error {
if err != nil {
if isContextDone(ctx) {
return nil // nolint:nilnil
}
return err
}
if resultErrors != nil {
var errBuilder strings.Builder
for _, error := range resultErrors {
fmt.Fprintln(&errBuilder, error.Message)
}
if isContextDone(ctx) {
return nil
}
// nolint: goerr113
return errors.New(errBuilder.String())
}
return nil
}
func addManifestCallToPool(ctx context.Context, config searchConfig, pool *requestsPool,
username, password, imageName, tagName string, rch chan stringResult, wtgrp *sync.WaitGroup,
) {
@ -533,6 +678,11 @@ type errorGraphQL struct {
Path []string `json:"path"`
}
type tagListResp struct {
Name string `json:"name"`
Tags []string `json:"tags"`
}
//nolint:tagliatelle // graphQL schema
type packageList struct {
Name string `json:"Name"`
@ -579,7 +729,7 @@ func (cve cveResult) stringPlainText() (string, error) {
table := getCVETableWriter(&builder)
for _, c := range cve.Data.CVEListForImage.CVEList {
id := ellipsize(c.ID, cvidWidth, ellipsis)
id := ellipsize(c.ID, cveIDWidth, ellipsis)
title := ellipsize(c.Title, cveTitleWidth, ellipsis)
severity := ellipsize(c.Severity, cveSeverityWidth, ellipsis)
row := make([]string, 3) //nolint:gomnd
@ -618,51 +768,50 @@ func (cve cveResult) stringYAML() (string, error) {
type fixedTags struct {
Errors []errorGraphQL `json:"errors"`
Data struct {
//nolint:tagliatelle // graphQL schema
ImageListWithCVEFixed struct {
Tags []struct {
Name string `json:"Name"`
Timestamp time.Time `json:"Timestamp"`
} `json:"Tags"`
} `json:"ImageListWithCVEFixed"`
ImageList []imageStruct `json:"ImageListWithCVEFixed"` //nolint:tagliatelle // graphQL schema
} `json:"data"`
}
type imagesForCve struct {
Errors []errorGraphQL `json:"errors"`
Data struct {
ImageListForCVE []tagListResp `json:"ImageListForCVE"` //nolint:tagliatelle // graphQL schema
ImageList []imageStruct `json:"ImageListForCVE"` //nolint:tagliatelle // graphQL schema
} `json:"data"`
}
type imageStruct struct {
RepoName string `json:"repoName"`
Tag string `json:"tag"`
ConfigDigest string `json:"configDigest"`
Digest string `json:"digest"`
Layers []layer `json:"layers"`
Size string `json:"size"`
verbose bool
}
type imageListStructGQL struct {
Errors []errorGraphQL `json:"errors"`
Data struct {
ImageList []imageStruct `json:"ImageList"` // nolint:tagliatelle
} `json:"data"`
}
type imageListStructForDigestGQL struct {
Errors []errorGraphQL `json:"errors"`
Data struct {
ImageList []imageStruct `json:"ImageListForDigest"` // nolint:tagliatelle
} `json:"data"`
}
type imagesForDigest struct {
Errors []errorGraphQL `json:"errors"`
Data struct {
ImageListForDigest []tagListResp `json:"ImageListForDigest"` //nolint:tagliatelle // graphQL schema
ImageList []imageStruct `json:"ImageListForDigest"` //nolint:tagliatelle // graphQL schema
} `json:"data"`
}
type tagListResp struct {
Name string `json:"name"`
Tags []string `json:"tags"`
}
type imageStruct struct {
Name string `json:"name"`
Tags []tags `json:"tags"`
verbose bool
}
type tags struct {
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"`
Size uint64 `json:"size,string"`
Digest string `json:"digest"`
}
@ -693,12 +842,12 @@ func (img imageStruct) stringPlainText() (string, error) {
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)
config := ellipsize(tag.ConfigDigest, configWidth, "")
imageName := ellipsize(img.RepoName, imageNameWidth, ellipsis)
tagName := ellipsize(img.Tag, tagWidth, ellipsis)
digest := ellipsize(img.Digest, digestWidth, "")
imgSize, _ := strconv.ParseUint(img.Size, 10, 64)
size := ellipsize(strings.ReplaceAll(humanize.Bytes(imgSize), " ", ""), sizeWidth, ellipsis)
config := ellipsize(img.ConfigDigest, configWidth, "")
row := make([]string, 6) // nolint:gomnd
row[colImageNameIndex] = imageName
@ -714,22 +863,22 @@ func (img imageStruct) stringPlainText() (string, error) {
table.Append(row)
if img.verbose {
for _, entry := range tag.Layers {
layerSize := ellipsize(strings.ReplaceAll(humanize.Bytes(entry.Size), " ", ""), sizeWidth, ellipsis)
for _, entry := range img.Layers {
layerSize := entry.Size
size := ellipsize(strings.ReplaceAll(humanize.Bytes(layerSize), " ", ""), sizeWidth, ellipsis)
layerDigest := ellipsize(entry.Digest, digestWidth, "")
layerRow := make([]string, 6) // nolint:gomnd
layerRow[colImageNameIndex] = ""
layerRow[colTagIndex] = ""
layerRow[colDigestIndex] = ""
layerRow[colSizeIndex] = layerSize
layerRow[colSizeIndex] = size
layerRow[colConfigIndex] = ""
layerRow[colLayersIndex] = layerDigest
table.Append(layerRow)
}
}
}
table.Render()
@ -760,6 +909,7 @@ type catalogResponse struct {
Repositories []string `json:"repositories"`
}
//nolint:tagliatelle
type manifestResponse struct {
Layers []struct {
MediaType string `json:"mediaType"`
@ -767,8 +917,8 @@ type manifestResponse struct {
Size uint64 `json:"size"`
} `json:"layers"`
Annotations struct {
WsTychoStackerStackerYaml string `json:"ws.tycho.stacker.stacker_yaml"` //nolint:tagliatelle // custom annotation
WsTychoStackerGitVersion string `json:"ws.tycho.stacker.git_version"` //nolint:tagliatelle // custom annotation
WsTychoStackerStackerYaml string `json:"ws.tycho.stacker.stacker_yaml"`
WsTychoStackerGitVersion string `json:"ws.tycho.stacker.git_version"`
} `json:"annotations"`
Config struct {
Size int `json:"size"`
@ -836,7 +986,7 @@ func getCVETableWriter(writer io.Writer) *tablewriter.Table {
table.SetBorder(false)
table.SetTablePadding(" ")
table.SetNoWhiteSpace(true)
table.SetColMinWidth(colCVEIDIndex, cvidWidth)
table.SetColMinWidth(colCVEIDIndex, cveIDWidth)
table.SetColMinWidth(colCVESeverityIndex, cveSeverityWidth)
table.SetColMinWidth(colCVETitleIndex, cveTitleWidth)
@ -895,7 +1045,7 @@ const (
colLayersIndex = 4
colSizeIndex = 5
cvidWidth = 16
cveIDWidth = 16
cveSeverityWidth = 8
cveTitleWidth = 48

View file

@ -2,7 +2,6 @@ package common
import (
"fmt"
"path"
"sort"
"strings"
"time"
@ -25,14 +24,6 @@ type TagInfo struct {
Timestamp time.Time
}
func GetImageRepoPath(image string, storeController storage.StoreController) string {
rootDir := GetRootDir(image, storeController)
repo := GetRepo(image)
return path.Join(rootDir, repo)
}
func GetRootDir(image string, storeController storage.StoreController) string {
var rootDir string

View file

@ -97,7 +97,7 @@ type RepoSummary struct {
Platforms []OsArch `json:"platforms"`
Vendors []string `json:"vendors"`
Score int `json:"score"`
NewestTag ImageSummary `json:"newestTag"`
NewestImage ImageSummary `json:"newestImage"`
}
type LayerSummary struct {
@ -126,8 +126,8 @@ type ErrorGQL struct {
}
type ImageInfo struct {
Name string
Latest string
RepoName string
Tag string
LastUpdated time.Time
Description string
Licenses string
@ -377,7 +377,7 @@ func TestLatestTagSearchHTTP(t *testing.T) {
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 422)
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){Name%20Latest}}")
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){RepoName%20Tag}}")
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
@ -388,9 +388,9 @@ func TestLatestTagSearchHTTP(t *testing.T) {
So(len(responseStruct.ImgListWithLatestTag.Images), ShouldEqual, 4)
images := responseStruct.ImgListWithLatestTag.Images
So(images[0].Latest, ShouldEqual, "0.0.1")
So(images[0].Tag, ShouldEqual, "0.0.1")
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){Name%20Latest}}")
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){RepoName%20Tag}}")
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
@ -399,7 +399,7 @@ func TestLatestTagSearchHTTP(t *testing.T) {
panic(err)
}
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){Name%20Latest}}")
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){RepoName%20Tag}}")
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
@ -423,7 +423,7 @@ func TestLatestTagSearchHTTP(t *testing.T) {
panic(err)
}
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){Name%20Latest}}")
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){RepoName%20Tag}}")
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
@ -434,7 +434,7 @@ func TestLatestTagSearchHTTP(t *testing.T) {
panic(err)
}
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){Name%20Latest}}")
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){RepoName%20Tag}}")
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
@ -444,7 +444,7 @@ func TestLatestTagSearchHTTP(t *testing.T) {
panic(err)
}
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){Name%20Latest}}")
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){RepoName%20Tag}}")
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
@ -455,7 +455,7 @@ func TestLatestTagSearchHTTP(t *testing.T) {
panic(err)
}
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){Name%20Latest}}")
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){RepoName%20Tag}}")
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
@ -532,7 +532,7 @@ func TestExpandedRepoInfo(t *testing.T) {
So(responseStruct.ExpandedRepoInfo.RepoInfo.Summary.Name, ShouldEqual, "zot-cve-test")
So(responseStruct.ExpandedRepoInfo.RepoInfo.Summary.Score, ShouldEqual, -1)
query = "{ExpandedRepoInfo(repo:\"zot-cve-test\"){Manifests%20{Digest%20IsSigned%20Tag%20Layers%20{Size%20Digest}}}}"
query = "{ExpandedRepoInfo(repo:\"zot-cve-test\"){Images%20{Digest%20IsSigned%20Tag%20Layers%20{Size%20Digest}}}}"
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + query)
So(resp, ShouldNotBeNil)
@ -543,10 +543,10 @@ func TestExpandedRepoInfo(t *testing.T) {
err = json.Unmarshal(resp.Body(), responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests), ShouldNotEqual, 0)
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests[0].Layers), ShouldNotEqual, 0)
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Images), ShouldNotEqual, 0)
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Images[0].Layers), ShouldNotEqual, 0)
found := false
for _, m := range responseStruct.ExpandedRepoInfo.RepoInfo.Manifests {
for _, m := range responseStruct.ExpandedRepoInfo.RepoInfo.Images {
if m.Digest == "63a795ca90aa6e7cca60941e826810a4cd0a2e73ea02bf458241df2a5c973e29" {
found = true
So(m.IsSigned, ShouldEqual, false)
@ -564,10 +564,10 @@ func TestExpandedRepoInfo(t *testing.T) {
err = json.Unmarshal(resp.Body(), responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests), ShouldNotEqual, 0)
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests[0].Layers), ShouldNotEqual, 0)
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Images), ShouldNotEqual, 0)
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Images[0].Layers), ShouldNotEqual, 0)
found = false
for _, m := range responseStruct.ExpandedRepoInfo.RepoInfo.Manifests {
for _, m := range responseStruct.ExpandedRepoInfo.RepoInfo.Images {
if m.Digest == "63a795ca90aa6e7cca60941e826810a4cd0a2e73ea02bf458241df2a5c973e29" {
found = true
So(m.IsSigned, ShouldEqual, true)
@ -575,14 +575,14 @@ func TestExpandedRepoInfo(t *testing.T) {
}
So(found, ShouldEqual, true)
query = "{ExpandedRepoInfo(repo:\"\"){Manifests%20{Digest%20Tag%20IsSigned%20Layers%20{Size%20Digest}}}}"
query = "{ExpandedRepoInfo(repo:\"\"){Images%20{Digest%20Tag%20IsSigned%20Layers%20{Size%20Digest}}}}"
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + query)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
query = "{ExpandedRepoInfo(repo:\"zot-test\"){Manifests%20{Digest%20Tag%20IsSigned%20Layers%20{Size%20Digest}}}}"
query = "{ExpandedRepoInfo(repo:\"zot-test\"){Images%20{Digest%20Tag%20IsSigned%20Layers%20{Size%20Digest}}}}"
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + query)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
@ -590,10 +590,10 @@ func TestExpandedRepoInfo(t *testing.T) {
err = json.Unmarshal(resp.Body(), responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests), ShouldNotEqual, 0)
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests[0].Layers), ShouldNotEqual, 0)
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Images), ShouldNotEqual, 0)
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Images[0].Layers), ShouldNotEqual, 0)
found = false
for _, m := range responseStruct.ExpandedRepoInfo.RepoInfo.Manifests {
for _, m := range responseStruct.ExpandedRepoInfo.RepoInfo.Images {
if m.Digest == "2bacca16b9df395fc855c14ccf50b12b58d35d468b8e7f25758aff90f89bf396" {
found = true
So(m.IsSigned, ShouldEqual, false)
@ -611,10 +611,10 @@ func TestExpandedRepoInfo(t *testing.T) {
err = json.Unmarshal(resp.Body(), responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests), ShouldNotEqual, 0)
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests[0].Layers), ShouldNotEqual, 0)
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Images), ShouldNotEqual, 0)
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Images[0].Layers), ShouldNotEqual, 0)
found = false
for _, m := range responseStruct.ExpandedRepoInfo.RepoInfo.Manifests {
for _, m := range responseStruct.ExpandedRepoInfo.RepoInfo.Images {
if m.Digest == "2bacca16b9df395fc855c14ccf50b12b58d35d468b8e7f25758aff90f89bf396" {
found = true
So(m.IsSigned, ShouldEqual, true)
@ -832,7 +832,7 @@ func TestGlobalSearch(t *testing.T) {
}
Vendors
Score
NewestTag {
NewestImage {
RepoName
Tag
LastUpdated
@ -911,14 +911,14 @@ func TestGlobalSearch(t *testing.T) {
So(repo.Vendors[0], ShouldEqual, image.Vendor)
So(repo.Platforms[0].Os, ShouldEqual, image.Platform.Os)
So(repo.Platforms[0].Arch, ShouldEqual, image.Platform.Arch)
So(repo.NewestTag.RepoName, ShouldEqual, image.RepoName)
So(repo.NewestTag.Tag, ShouldEqual, image.Tag)
So(repo.NewestTag.LastUpdated, ShouldEqual, image.LastUpdated)
So(repo.NewestTag.Size, ShouldEqual, image.Size)
So(repo.NewestTag.IsSigned, ShouldEqual, image.IsSigned)
So(repo.NewestTag.Vendor, ShouldEqual, image.Vendor)
So(repo.NewestTag.Platform.Os, ShouldEqual, image.Platform.Os)
So(repo.NewestTag.Platform.Arch, ShouldEqual, image.Platform.Arch)
So(repo.NewestImage.RepoName, ShouldEqual, image.RepoName)
So(repo.NewestImage.Tag, ShouldEqual, image.Tag)
So(repo.NewestImage.LastUpdated, ShouldEqual, image.LastUpdated)
So(repo.NewestImage.Size, ShouldEqual, image.Size)
So(repo.NewestImage.IsSigned, ShouldEqual, image.IsSigned)
So(repo.NewestImage.Vendor, ShouldEqual, image.Vendor)
So(repo.NewestImage.Platform.Os, ShouldEqual, image.Platform.Os)
So(repo.NewestImage.Platform.Arch, ShouldEqual, image.Platform.Arch)
}
// GetRepositories fail

View file

@ -43,11 +43,11 @@ type BaseOciLayoutUtils struct {
}
type RepoInfo struct {
Manifests []Manifest `json:"manifests"`
Summary RepoSummary
Images []Image `json:"images"`
}
type Manifest struct {
type Image struct {
Tag string `json:"tag"`
Digest string `json:"digest"`
IsSigned bool `json:"isSigned"`
@ -358,7 +358,7 @@ func (olu BaseOciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error)
// made up of all manifests, configs and image layers
repoSize := int64(0)
manifests := make([]Manifest, 0)
manifests := make([]Image, 0)
tagsInfo, err := olu.GetImageTagsWithTimestamp(name)
if err != nil {
@ -376,7 +376,7 @@ func (olu BaseOciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error)
repoVendors := make([]string, 0, len(manifestList))
for _, man := range manifestList {
manifestInfo := Manifest{}
manifestInfo := Image{}
manifestInfo.Digest = man.Digest.Encoded()
@ -441,7 +441,7 @@ func (olu BaseOciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error)
manifests = append(manifests, manifestInfo)
}
repo.Manifests = manifests
repo.Images = manifests
lastUpdate, err := olu.GetRepoLastUpdated(name)
if err != nil {

View file

@ -11,6 +11,7 @@ import (
"github.com/aquasecurity/trivy/pkg/commands/operation"
"github.com/aquasecurity/trivy/pkg/report"
"github.com/aquasecurity/trivy/pkg/types"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/urfave/cli/v2"
"zotregistry.io/zot/pkg/extensions/search/common"
"zotregistry.io/zot/pkg/log"
@ -141,19 +142,21 @@ func (cveinfo CveInfo) GetTrivyContext(image string) *TrivyCtx {
func (cveinfo CveInfo) GetImageListForCVE(repo, cvid string, imgStore storage.ImageStore,
trivyCtx *TrivyCtx,
) ([]*string, error) {
tags := make([]*string, 0)
tagList, err := imgStore.GetImageTags(repo)
if err != nil {
cveinfo.Log.Error().Err(err).Msg("unable to get list of image tag")
return tags, err
}
) ([]ImageInfoByCVE, error) {
imgList := make([]ImageInfoByCVE, 0)
rootDir := imgStore.RootDir()
for _, tag := range tagList {
manifests, err := cveinfo.LayoutUtils.GetImageManifests(repo)
if err != nil {
cveinfo.Log.Error().Err(err).Msg("unable to get list of image tag")
return imgList, err
}
for _, manifest := range manifests {
tag := manifest.Annotations[ispec.AnnotationRefName]
image := fmt.Sprintf("%s:%s", repo, tag)
trivyCtx.Input = path.Join(rootDir, image)
@ -177,8 +180,20 @@ func (cveinfo CveInfo) GetImageListForCVE(repo, cvid string, imgStore storage.Im
for _, result := range report.Results {
for _, vulnerability := range result.Vulnerabilities {
if vulnerability.VulnerabilityID == cvid {
copyImgTag := tag
tags = append(tags, &copyImgTag)
digest := manifest.Digest
imageBlobManifest, err := cveinfo.LayoutUtils.GetImageBlobManifest(repo, digest)
if err != nil {
cveinfo.Log.Error().Err(err).Msg("unable to read image blob manifest")
return []ImageInfoByCVE{}, err
}
imgList = append(imgList, ImageInfoByCVE{
Tag: tag,
Digest: digest,
Manifest: imageBlobManifest,
})
break
}
@ -186,5 +201,5 @@ func (cveinfo CveInfo) GetImageListForCVE(repo, cvid string, imgStore storage.Im
}
}
return tags, nil
return imgList, nil
}

View file

@ -46,23 +46,14 @@ type CveResult struct {
ImgList ImgList `json:"data"`
}
type ImgWithFixedCVE struct {
ImgResults ImgResults `json:"data"`
}
//nolint:tagliatelle // graphQL schema
type ImgResults struct {
ImgResultForFixedCVE ImgResultForFixedCVE `json:"ImgResultForFixedCVE"`
type ImgListWithCVEFixed struct {
Images []ImageInfo `json:"ImageListWithCVEFixed"`
}
//nolint:tagliatelle // graphQL schema
type ImgResultForFixedCVE struct {
Tags []TagInfo `json:"Tags"`
}
type TagInfo struct {
Name string
Timestamp time.Time
type ImageInfo struct {
RepoName string
LastUpdated time.Time
}
//nolint:tagliatelle // graphQL schema
@ -470,24 +461,24 @@ func TestCVESearch(t *testing.T) {
cvid := cveResult.ImgList.CVEResultForImage.CVEList[0].ID
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-test\"){Tags{Name%20Timestamp}}}")
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-test\"){RepoName%20LastUpdated}}")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
var imgFixedCVEResult ImgWithFixedCVE
err = json.Unmarshal(resp.Body(), &imgFixedCVEResult)
var imgListWithCVEFixed ImgListWithCVEFixed
err = json.Unmarshal(resp.Body(), &imgListWithCVEFixed)
So(err, ShouldBeNil)
So(len(imgFixedCVEResult.ImgResults.ImgResultForFixedCVE.Tags), ShouldEqual, 0)
So(len(imgListWithCVEFixed.Images), ShouldEqual, 0)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-cve-test\"){Tags{Name%20Timestamp}}}")
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-cve-test\"){RepoName%20LastUpdated}}")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
err = json.Unmarshal(resp.Body(), &imgFixedCVEResult)
err = json.Unmarshal(resp.Body(), &imgListWithCVEFixed)
So(err, ShouldBeNil)
So(len(imgFixedCVEResult.ImgResults.ImgResultForFixedCVE.Tags), ShouldEqual, 0)
So(len(imgListWithCVEFixed.Images), ShouldEqual, 0)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-test\"){Tags{Name%20Timestamp}}}")
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-test\"){RepoName%20LastUpdated}}")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
@ -504,7 +495,7 @@ func TestCVESearch(t *testing.T) {
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-noindex\"){Tags{Name%20Timestamp}}}")
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-noindex\"){RepoName%20LastUpdated}}")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
@ -512,7 +503,7 @@ func TestCVESearch(t *testing.T) {
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-invalid-index\"){Tags{Name%20Timestamp}}}")
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-invalid-index\"){RepoName%20LastUpdated}}")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
@ -520,11 +511,11 @@ func TestCVESearch(t *testing.T) {
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-noblob\"){Tags{Name%20Timestamp}}}")
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-noblob\"){RepoName%20LastUpdated}}")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-test\"){Tags{Name%20Timestamp}}}")
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-test\"){RepoName%20LastUpdated}}")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
@ -532,7 +523,7 @@ func TestCVESearch(t *testing.T) {
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-invalid-blob\"){Tags{Name%20Timestamp}}}")
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-invalid-blob\"){RepoName%20LastUpdated}}")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
@ -544,7 +535,7 @@ func TestCVESearch(t *testing.T) {
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListForCVE(id:\"CVE-201-20482\"){Name%20Tags}}")
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListForCVE(id:\"CVE-201-20482\"){RepoName%20Tag}}")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
@ -585,11 +576,11 @@ func TestCVESearch(t *testing.T) {
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 422)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListForCVE(tet:\"CVE-2018-20482\"){Name%20Tags}}")
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListForCVE(tet:\"CVE-2018-20482\"){RepoName%20Tag}}")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 422)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageistForCVE(id:\"CVE-2018-20482\"){Name%20Tags}}")
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageistForCVE(id:\"CVE-2018-20482\"){RepoName%20Tag}}")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 422)
@ -601,7 +592,7 @@ func TestCVESearch(t *testing.T) {
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 422)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListForCVE(id:\"" + cvid + "\"){Name%20Tags}}")
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListForCVE(id:\"" + cvid + "\"){RepoName%20Tag}}")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
})

View file

@ -2,6 +2,8 @@
package cveinfo
import (
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/opencontainers/go-digest"
"github.com/urfave/cli/v2"
"zotregistry.io/zot/pkg/extensions/search/common"
"zotregistry.io/zot/pkg/log"
@ -25,3 +27,9 @@ type TrivyCtx struct {
Input string
Ctx *cli.Context
}
type ImageInfoByCVE struct {
Tag string
Digest digest.Digest
Manifest v1.Manifest
}

View file

@ -3,6 +3,8 @@ package digestinfo
import (
"strings"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
"zotregistry.io/zot/pkg/extensions/search/common"
"zotregistry.io/zot/pkg/log"
@ -15,6 +17,12 @@ type DigestInfo struct {
LayoutUtils *common.BaseOciLayoutUtils
}
type ImageInfoByDigest struct {
Tag string
Digest digest.Digest
Manifest v1.Manifest
}
// NewDigestInfo initializes a new DigestInfo object.
func NewDigestInfo(storeController storage.StoreController, log log.Logger) *DigestInfo {
layoutUtils := common.NewBaseOciLayoutUtils(storeController, log)
@ -23,14 +31,14 @@ func NewDigestInfo(storeController storage.StoreController, log log.Logger) *Dig
}
// FilterImagesByDigest returns a list of image tags in a repository matching a specific divest.
func (digestinfo DigestInfo) GetImageTagsByDigest(repo, digest string) ([]*string, error) {
uniqueTags := []*string{}
func (digestinfo DigestInfo) GetImageTagsByDigest(repo, digest string) ([]ImageInfoByDigest, error) {
imageTags := []ImageInfoByDigest{}
manifests, err := digestinfo.LayoutUtils.GetImageManifests(repo)
if err != nil {
digestinfo.Log.Error().Err(err).Msg("unable to read image manifests")
return uniqueTags, err
return imageTags, err
}
for _, manifest := range manifests {
@ -42,7 +50,7 @@ func (digestinfo DigestInfo) GetImageTagsByDigest(repo, digest string) ([]*strin
if err != nil {
digestinfo.Log.Error().Err(err).Msg("unable to read image blob manifest")
return uniqueTags, err
return imageTags, err
}
tags := []*string{}
@ -71,12 +79,12 @@ func (digestinfo DigestInfo) GetImageTagsByDigest(repo, digest string) ([]*strin
for _, entry := range tags {
if _, value := keys[*entry]; !value {
uniqueTags = append(uniqueTags, entry)
imageTags = append(imageTags, ImageInfoByDigest{Tag: *entry, Digest: imageDigest, Manifest: imageBlobManifest})
keys[*entry] = true
}
}
}
}
return uniqueTags, nil
return imageTags, nil
}

View file

@ -12,7 +12,6 @@ import (
"testing"
"time"
"github.com/opencontainers/go-digest"
. "github.com/smartystreets/goconvey/convey"
"gopkg.in/resty.v1"
"zotregistry.io/zot/pkg/api"
@ -45,8 +44,11 @@ type ImgListForDigest struct {
//nolint:tagliatelle // graphQL schema
type ImgInfo struct {
Name string `json:"Name"`
Tags []string `json:"Tags"`
RepoName string `json:"RepoName"`
Tag string `json:"Tag"`
ConfigDigest string `json:"ConfigDigest"`
Digest string `json:"Digest"`
Size string `json:"Size"`
}
type ErrorGQL struct {
@ -97,15 +99,10 @@ func testSetup() error {
return err
}
conf := config.New()
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Lint = &extconf.LintConfig{}
log := log.NewLogger("debug", "")
metrics := monitoring.NewMetricsServer(false, log)
storeController := storage.StoreController{
DefaultStore: storage.NewImageStore(rootDir, false, storage.DefaultGCDelay,
false, false, log, metrics, nil),
DefaultStore: storage.NewImageStore(rootDir, false, storage.DefaultGCDelay, false, false, log, metrics, nil),
}
digestInfo = digestinfo.NewDigestInfo(storeController, log)
@ -115,33 +112,31 @@ func testSetup() error {
func TestDigestInfo(t *testing.T) {
Convey("Test image tag", t, func() {
log := log.NewLogger("debug", "")
metrics := monitoring.NewMetricsServer(false, log)
storeController := storage.StoreController{
DefaultStore: storage.NewImageStore(rootDir, false, storage.DefaultGCDelay, false, false, log, metrics, nil),
}
digestInfo = digestinfo.NewDigestInfo(storeController, log)
// Search by manifest digest
var (
manifestDigest digest.Digest
configDigest digest.Digest
layerDigest digest.Digest
)
manifestDigest, _, layerDigest = GetOciLayoutDigests("../../../../test/data/zot-cve-test")
imageTags, err := digestInfo.GetImageTagsByDigest("zot-cve-test", string(manifestDigest))
imageTags, err := digestInfo.GetImageTagsByDigest("zot-cve-test", "63a795ca")
So(err, ShouldBeNil)
So(len(imageTags), ShouldEqual, 1)
So(*imageTags[0], ShouldEqual, "0.0.1")
So(imageTags[0].Tag, ShouldEqual, "0.0.1")
// Search by config digest
_, configDigest, _ = GetOciLayoutDigests("../../../../test/data/zot-test")
imageTags, err = digestInfo.GetImageTagsByDigest("zot-test", string(configDigest))
imageTags, err = digestInfo.GetImageTagsByDigest("zot-test", "adf3bb6c")
So(err, ShouldBeNil)
So(len(imageTags), ShouldEqual, 1)
So(*imageTags[0], ShouldEqual, "0.0.1")
So(imageTags[0].Tag, ShouldEqual, "0.0.1")
// Search by layer digest
imageTags, err = digestInfo.GetImageTagsByDigest("zot-cve-test", string(layerDigest))
imageTags, err = digestInfo.GetImageTagsByDigest("zot-cve-test", "7a0437f0")
So(err, ShouldBeNil)
So(len(imageTags), ShouldEqual, 1)
So(*imageTags[0], ShouldEqual, "0.0.1")
So(imageTags[0].Tag, ShouldEqual, "0.0.1")
// Search by non-existent image
imageTags, err = digestInfo.GetImageTagsByDigest("zot-tes", "63a795ca")
@ -202,8 +197,10 @@ func TestDigestSearchHTTP(t *testing.T) {
So(resp.StatusCode(), ShouldEqual, 422)
// "sha" should match all digests in all images
resp, err = resty.R().Get(baseURL + constants.ExtSearchPrefix +
"?query={ImageListForDigest(id:\"sha\"){Name%20Tags}}")
resp, err = resty.R().Get(
baseURL + constants.ExtSearchPrefix + `?query={ImageListForDigest(id:"sha")` +
`{RepoName%20Tag%20Digest%20ConfigDigest%20Size%20Layers%20{%20Digest}}}`,
)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
@ -213,16 +210,14 @@ func TestDigestSearchHTTP(t *testing.T) {
So(err, ShouldBeNil)
So(len(responseStruct.Errors), ShouldEqual, 0)
So(len(responseStruct.ImgListForDigest.Images), ShouldEqual, 2)
So(len(responseStruct.ImgListForDigest.Images[0].Tags), ShouldEqual, 1)
So(len(responseStruct.ImgListForDigest.Images[0].Tags), ShouldEqual, 1)
So(responseStruct.ImgListForDigest.Images[0].Tag, ShouldEqual, "0.0.1")
// Call should return {"data":{"ImageListForDigest":[{"Name":"zot-test","Tags":["0.0.1"]}]}}
var layerDigest digest.Digest
var manifestDigest digest.Digest
manifestDigest, _, layerDigest = GetOciLayoutDigests("../../../../test/data/zot-test")
resp, err = resty.R().Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListForDigest(id:\"" +
string(layerDigest) + "\"){Name%20Tags}}")
// "2bacca16" should match the manifest of 1 image
resp, err = resty.R().Get(
baseURL + constants.ExtSearchPrefix + `?query={ImageListForDigest(id:"2bacca16")` +
`{RepoName%20Tag%20Digest%20ConfigDigest%20Size%20Layers%20{%20Digest}}}`,
)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
@ -231,15 +226,14 @@ func TestDigestSearchHTTP(t *testing.T) {
So(err, ShouldBeNil)
So(len(responseStruct.Errors), ShouldEqual, 0)
So(len(responseStruct.ImgListForDigest.Images), ShouldEqual, 1)
So(responseStruct.ImgListForDigest.Images[0].Name, ShouldEqual, "zot-test")
So(len(responseStruct.ImgListForDigest.Images[0].Tags), ShouldEqual, 1)
So(responseStruct.ImgListForDigest.Images[0].Tags[0], ShouldEqual, "0.0.1")
So(responseStruct.ImgListForDigest.Images[0].RepoName, ShouldEqual, "zot-test")
So(responseStruct.ImgListForDigest.Images[0].Tag, ShouldEqual, "0.0.1")
// Call should return {"data":{"ImageListForDigest":[{"Name":"zot-test","Tags":["0.0.1"]}]}}
resp, err = resty.R().Get(baseURL + constants.ExtSearchPrefix +
"?query={ImageListForDigest(id:\"" +
string(manifestDigest) + "\"){Name%20Tags}}")
// "adf3bb6c" should match the config of 1 image
resp, err = resty.R().Get(
baseURL + constants.ExtSearchPrefix + `?query={ImageListForDigest(id:"adf3bb6c")` +
`{RepoName%20Tag%20Digest%20ConfigDigest%20Size%20Layers%20{%20Digest}}}`,
)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
@ -248,15 +242,15 @@ func TestDigestSearchHTTP(t *testing.T) {
So(err, ShouldBeNil)
So(len(responseStruct.Errors), ShouldEqual, 0)
So(len(responseStruct.ImgListForDigest.Images), ShouldEqual, 1)
So(responseStruct.ImgListForDigest.Images[0].Name, ShouldEqual, "zot-test")
So(len(responseStruct.ImgListForDigest.Images[0].Tags), ShouldEqual, 1)
So(responseStruct.ImgListForDigest.Images[0].Tags[0], ShouldEqual, "0.0.1")
So(responseStruct.ImgListForDigest.Images[0].RepoName, ShouldEqual, "zot-test")
So(responseStruct.ImgListForDigest.Images[0].Tag, ShouldEqual, "0.0.1")
// Call should return {"data":{"ImageListForDigest":[{"Name":"zot-cve-test","Tags":["0.0.1"]}]}}
_, _, layerDigest = GetOciLayoutDigests("../../../../test/data/zot-cve-test")
resp, err = resty.R().Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListForDigest(id:\"" +
string(layerDigest) + "\"){Name%20Tags}}")
// "7a0437f0" should match the layer of 1 image
resp, err = resty.R().Get(
baseURL + constants.ExtSearchPrefix + `?query={ImageListForDigest(id:"7a0437f0")` +
`{RepoName%20Tag%20Digest%20ConfigDigest%20Size%20Layers%20{%20Digest}}}`,
)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
@ -265,14 +259,15 @@ func TestDigestSearchHTTP(t *testing.T) {
So(err, ShouldBeNil)
So(len(responseStruct.Errors), ShouldEqual, 0)
So(len(responseStruct.ImgListForDigest.Images), ShouldEqual, 1)
So(responseStruct.ImgListForDigest.Images[0].Name, ShouldEqual, "zot-cve-test")
So(len(responseStruct.ImgListForDigest.Images[0].Tags), ShouldEqual, 1)
So(responseStruct.ImgListForDigest.Images[0].Tags[0], ShouldEqual, "0.0.1")
So(responseStruct.ImgListForDigest.Images[0].RepoName, ShouldEqual, "zot-cve-test")
So(responseStruct.ImgListForDigest.Images[0].Tag, ShouldEqual, "0.0.1")
// Call should return {"data":{"ImageListForDigest":[]}}
// "1111111" should match 0 images
resp, err = resty.R().Get(baseURL + constants.ExtSearchPrefix +
"?query={ImageListForDigest(id:\"1111111\"){Name%20Tags}}")
resp, err = resty.R().Get(
baseURL + constants.ExtSearchPrefix + `?query={ImageListForDigest(id:"1111111")` +
`{RepoName%20Tag%20Digest%20ConfigDigest%20Size%20Layers%20{%20Digest}}}`,
)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
@ -283,8 +278,10 @@ func TestDigestSearchHTTP(t *testing.T) {
So(len(responseStruct.ImgListForDigest.Images), ShouldEqual, 0)
// Call should return {"errors": [{....}]", data":null}}
resp, err = resty.R().Get(baseURL + constants.ExtSearchPrefix +
"?query={ImageListForDigest(id:\"1111111\"){Name%20Tag343s}}")
resp, err = resty.R().Get(
baseURL + constants.ExtSearchPrefix + `?query={ImageListForDigest(id:"1111111")` +
`{RepoName%20Tag343s}}`,
)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 422)
@ -354,8 +351,10 @@ func TestDigestSearchHTTPSubPaths(t *testing.T) {
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 422)
resp, err = resty.R().Get(baseURL + constants.ExtSearchPrefix +
"?query={ImageListForDigest(id:\"sha\"){Name%20Tags}}")
resp, err = resty.R().Get(
baseURL + constants.ExtSearchPrefix + `?query={ImageListForDigest(id:"sha")` +
`{RepoName%20Tag%20Digest%20ConfigDigest%20Size%20Layers%20{%20Digest}}}`,
)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)

File diff suppressed because it is too large Load diff

View file

@ -25,45 +25,22 @@ type GlobalSearchResult struct {
Layers []*LayerSummary `json:"Layers"`
}
type ImageInfo struct {
Name *string `json:"Name"`
Latest *string `json:"Latest"`
LastUpdated *time.Time `json:"LastUpdated"`
Description *string `json:"Description"`
Licenses *string `json:"Licenses"`
Vendor *string `json:"Vendor"`
Size *string `json:"Size"`
Labels *string `json:"Labels"`
}
type ImageSummary struct {
RepoName *string `json:"RepoName"`
Tag *string `json:"Tag"`
Digest *string `json:"Digest"`
ConfigDigest *string `json:"ConfigDigest"`
LastUpdated *time.Time `json:"LastUpdated"`
IsSigned *bool `json:"IsSigned"`
Size *string `json:"Size"`
Platform *OsArch `json:"Platform"`
Vendor *string `json:"Vendor"`
Score *int `json:"Score"`
}
type ImgResultForCve struct {
Name *string `json:"Name"`
Tags []*string `json:"Tags"`
}
type ImgResultForDigest struct {
Name *string `json:"Name"`
Tags []*string `json:"Tags"`
}
type ImgResultForFixedCve struct {
Tags []*TagInfo `json:"Tags"`
}
type LayerInfo struct {
Size *string `json:"Size"`
Digest *string `json:"Digest"`
DownloadCount *int `json:"DownloadCount"`
Layers []*LayerSummary `json:"Layers"`
Description *string `json:"Description"`
Licenses *string `json:"Licenses"`
Labels *string `json:"Labels"`
}
type LayerSummary struct {
@ -72,13 +49,6 @@ type LayerSummary struct {
Score *int `json:"Score"`
}
type ManifestInfo struct {
Digest *string `json:"Digest"`
Tag *string `json:"Tag"`
IsSigned *bool `json:"IsSigned"`
Layers []*LayerInfo `json:"Layers"`
}
type OsArch struct {
Os *string `json:"Os"`
Arch *string `json:"Arch"`
@ -91,7 +61,7 @@ type PackageInfo struct {
}
type RepoInfo struct {
Manifests []*ManifestInfo `json:"Manifests"`
Images []*ImageSummary `json:"Images"`
Summary *RepoSummary `json:"Summary"`
}
@ -102,11 +72,8 @@ type RepoSummary struct {
Platforms []*OsArch `json:"Platforms"`
Vendors []*string `json:"Vendors"`
Score *int `json:"Score"`
NewestTag *ImageSummary `json:"NewestTag"`
}
type TagInfo struct {
Name *string `json:"Name"`
Digest *string `json:"Digest"`
Timestamp *time.Time `json:"Timestamp"`
NewestImage *ImageSummary `json:"NewestImage"`
DownloadCount *int `json:"DownloadCount"`
StarCount *int `json:"StarCount"`
IsBookmarked *bool `json:"IsBookmarked"`
}

View file

@ -9,6 +9,7 @@ import (
"strconv"
"strings"
v1 "github.com/google/go-containerregistry/pkg/v1"
godigest "github.com/opencontainers/go-digest"
"zotregistry.io/zot/pkg/log" // nolint: gci
@ -60,60 +61,56 @@ func GetResolverConfig(log log.Logger, storeController storage.StoreController,
func (r *queryResolver) getImageListForCVE(repoList []string, cvid string, imgStore storage.ImageStore,
trivyCtx *cveinfo.TrivyCtx,
) ([]*gql_generated.ImgResultForCve, error) {
cveResult := []*gql_generated.ImgResultForCve{}
) ([]*gql_generated.ImageSummary, error) {
cveResult := []*gql_generated.ImageSummary{}
for _, repo := range repoList {
r.log.Info().Str("repo", repo).Msg("extracting list of tags available in image repo")
name := repo
tags, err := r.cveInfo.GetImageListForCVE(repo, cvid, imgStore, trivyCtx)
imageListByCVE, err := r.cveInfo.GetImageListForCVE(repo, cvid, imgStore, trivyCtx)
if err != nil {
r.log.Error().Err(err).Msg("error getting tag")
return cveResult, err
}
if len(tags) != 0 {
cveResult = append(cveResult, &gql_generated.ImgResultForCve{Name: &name, Tags: tags})
for _, imageByCVE := range imageListByCVE {
cveResult = append(
cveResult,
buildImageInfo(repo, imageByCVE.Tag, imageByCVE.Digest, imageByCVE.Manifest),
)
}
}
return cveResult, nil
}
func (r *queryResolver) getImageListForDigest(repoList []string,
digest string,
) ([]*gql_generated.ImgResultForDigest, error) {
imgResultForDigest := []*gql_generated.ImgResultForDigest{}
func (r *queryResolver) getImageListForDigest(repoList []string, digest string) ([]*gql_generated.ImageSummary, error) {
imgResultForDigest := []*gql_generated.ImageSummary{}
var errResult error
for _, repo := range repoList {
r.log.Info().Str("repo", repo).Msg("filtering list of tags in image repo by digest")
tags, err := r.digestInfo.GetImageTagsByDigest(repo, digest)
imgTags, err := r.digestInfo.GetImageTagsByDigest(repo, digest)
if err != nil {
r.log.Error().Err(err).Msg("unable to get filtered list of image tags")
errResult = err
continue
return []*gql_generated.ImageSummary{}, err
}
if len(tags) != 0 {
name := repo
imgResultForDigest = append(imgResultForDigest, &gql_generated.ImgResultForDigest{Name: &name, Tags: tags})
for _, imageInfo := range imgTags {
imageInfo := buildImageInfo(repo, imageInfo.Tag, imageInfo.Digest, imageInfo.Manifest)
imgResultForDigest = append(imgResultForDigest, imageInfo)
}
}
return imgResultForDigest, errResult
}
func (r *queryResolver) getImageListWithLatestTag(store storage.ImageStore) ([]*gql_generated.ImageInfo, error) {
results := make([]*gql_generated.ImageInfo, 0)
func (r *queryResolver) getImageListWithLatestTag(store storage.ImageStore) ([]*gql_generated.ImageSummary, error) {
results := make([]*gql_generated.ImageSummary, 0)
repoList, err := store.GetRepositories()
if err != nil {
@ -167,7 +164,6 @@ func (r *queryResolver) getImageListWithLatestTag(store storage.ImageStore) ([]*
labels := imageConfig.Config.Labels
// Read Description
desc := common.GetDescription(labels)
// Read licenses
@ -179,8 +175,8 @@ func (r *queryResolver) getImageListWithLatestTag(store storage.ImageStore) ([]*
// Read categories
categories := common.GetCategories(labels)
results = append(results, &gql_generated.ImageInfo{
Name: &name, Latest: &latestTag.Name,
results = append(results, &gql_generated.ImageSummary{
RepoName: &name, Tag: &latestTag.Name,
Description: &desc, Licenses: &license, Vendor: &vendor,
Labels: &categories, Size: &size, LastUpdated: &latestTag.Timestamp,
})
@ -336,7 +332,7 @@ func globalSearch(repoList []string, name, tag string, olu common.OciLayoutUtils
Platforms: repoPlatforms,
Vendors: repoVendors,
Score: &index,
NewestTag: &lastUpdatedImageSummary,
NewestImage: &lastUpdatedImageSummary,
})
}
}
@ -382,15 +378,94 @@ func calculateImageMatchingScore(artefactName string, index int, matchesTag bool
return score
}
func getGraphqlCompatibleTags(fixedTags []common.TagInfo) []*gql_generated.TagInfo {
finalTagList := make([]*gql_generated.TagInfo, 0)
func (r *queryResolver) getImageList(store storage.ImageStore, imageName string) (
[]*gql_generated.ImageSummary, error,
) {
results := make([]*gql_generated.ImageSummary, 0)
for _, tag := range fixedTags {
fixTag := tag
repoList, err := store.GetRepositories()
if err != nil {
r.log.Error().Err(err).Msg("extension api: error extracting repositories list")
finalTagList = append(finalTagList,
&gql_generated.TagInfo{Name: &fixTag.Name, Digest: &fixTag.Digest, Timestamp: &fixTag.Timestamp})
return results, err
}
return finalTagList
layoutUtils := common.NewBaseOciLayoutUtils(r.storeController, r.log)
for _, repo := range repoList {
if (imageName != "" && repo == imageName) || imageName == "" {
tagsInfo, err := layoutUtils.GetImageTagsWithTimestamp(repo)
if err != nil {
r.log.Error().Err(err).Msg("extension api: error getting tag timestamp info")
return results, nil
}
if len(tagsInfo) == 0 {
r.log.Info().Str("no tagsinfo found for repo", repo).Msg(" continuing traversing")
continue
}
for i := range tagsInfo {
// using a loop variable called tag would be reassigned after each iteration, using the same memory address
// directly access the value at the current index in the slice as ImageInfo requires pointers to tag fields
tag := tagsInfo[i]
digest := godigest.Digest(tag.Digest)
manifest, err := layoutUtils.GetImageBlobManifest(repo, digest)
if err != nil {
r.log.Error().Err(err).Msg("extension api: error reading manifest")
return results, err
}
imageInfo := buildImageInfo(repo, tag.Name, digest, manifest)
results = append(results, imageInfo)
}
}
}
if len(results) == 0 {
r.log.Info().Msg("no repositories found")
}
return results, nil
}
func buildImageInfo(repo string, tag string, tagDigest godigest.Digest,
manifest v1.Manifest,
) *gql_generated.ImageSummary {
layers := []*gql_generated.LayerSummary{}
size := int64(0)
for _, entry := range manifest.Layers {
size += entry.Size
digest := entry.Digest.Hex
layerSize := strconv.FormatInt(entry.Size, 10)
layers = append(
layers,
&gql_generated.LayerSummary{
Size: &layerSize,
Digest: &digest,
},
)
}
formattedSize := strconv.FormatInt(size, 10)
formattedTagDigest := tagDigest.Hex()
imageInfo := &gql_generated.ImageSummary{
RepoName: &repo,
Tag: &tag,
Digest: &formattedTagDigest,
ConfigDigest: &manifest.Config.Digest.Hex,
Size: &formattedSize,
Layers: layers,
}
return imageInfo
}

View file

@ -139,7 +139,7 @@ func TestGlobalSearch(t *testing.T) {
mockOlum := mocks.OciLayoutUtilsMock{
GetExpandedRepoInfoFn: func(name string) (common.RepoInfo, error) {
return common.RepoInfo{
Manifests: []common.Manifest{
Images: []common.Image{
{
Tag: "latest",
Layers: []common.Layer{

View file

@ -19,54 +19,11 @@ type PackageInfo {
FixedVersion: String
}
type ImgResultForCVE {
Name: String
Tags: [String]
}
type ImgResultForFixedCVE {
Tags: [TagInfo]
}
type ImgResultForDigest {
Name: String
Tags: [String]
}
type TagInfo {
Name: String
Digest: String
Timestamp: Time
}
type ImageInfo {
Name: String
Latest: String
LastUpdated: Time
Description: String
Licenses: String
Vendor: String
Size: String
Labels: String
}
type RepoInfo {
Manifests: [ManifestInfo]
Images: [ImageSummary]
Summary: RepoSummary
}
type ManifestInfo {
Digest: String
Tag: String
IsSigned: Boolean
Layers: [LayerInfo]
}
type LayerInfo {
Size: String # Int64 is not supported.
Digest: String
}
# Search results in all repos/images/layers
# There will be other more structures for more detailed information
type GlobalSearchResult {
@ -80,12 +37,19 @@ type GlobalSearchResult {
type ImageSummary {
RepoName: String
Tag: String
Digest: String
ConfigDigest: String
LastUpdated: Time
IsSigned: Boolean
Size: String
Platform: OsArch
Vendor: String
Score: Int
DownloadCount: Int
Layers: [LayerSummary]
Description: String
Licenses: String
Labels: String
}
# Brief on a specific repo to be used in queries returning a list of repos
@ -96,7 +60,10 @@ type RepoSummary {
Platforms: [OsArch]
Vendors: [String]
Score: Int
NewestTag: ImageSummary
NewestImage: ImageSummary
DownloadCount: Int
StarCount: Int
IsBookmarked: Boolean
}
# Currently the same as LayerInfo, we can refactor later
@ -113,11 +80,12 @@ type OsArch {
}
type Query {
CVEListForImage(image: String!) :CVEResultForImage
ImageListForCVE(id: String!) :[ImgResultForCVE]
ImageListWithCVEFixed(id: String!, image: String!) :ImgResultForFixedCVE
ImageListForDigest(id: String!) :[ImgResultForDigest]
ImageListWithLatestTag:[ImageInfo]
ExpandedRepoInfo(repo: String!):RepoInfo
GlobalSearch(query: String!): GlobalSearchResult
CVEListForImage(image: String!): CVEResultForImage!
ImageListForCVE(id: String!): [ImageSummary!]
ImageListWithCVEFixed(id: String!, image: String!): [ImageSummary!]
ImageListForDigest(id: String!): [ImageSummary!]
ImageListWithLatestTag: [ImageSummary!]
ImageList(repo: String!): [ImageSummary!]
ExpandedRepoInfo(repo: String!): RepoInfo!
GlobalSearch(query: String!): GlobalSearchResult!
}

View file

@ -8,6 +8,7 @@ import (
"fmt"
"strings"
godigest "github.com/opencontainers/go-digest"
"zotregistry.io/zot/pkg/extensions/search/common"
cveinfo "zotregistry.io/zot/pkg/extensions/search/cve"
"zotregistry.io/zot/pkg/extensions/search/gql_generated"
@ -101,8 +102,8 @@ func (r *queryResolver) CVEListForImage(ctx context.Context, image string) (*gql
}
// ImageListForCve is the resolver for the ImageListForCVE field.
func (r *queryResolver) ImageListForCve(ctx context.Context, id string) ([]*gql_generated.ImgResultForCve, error) {
finalCveResult := []*gql_generated.ImgResultForCve{}
func (r *queryResolver) ImageListForCve(ctx context.Context, id string) ([]*gql_generated.ImageSummary, error) {
finalCveResult := []*gql_generated.ImageSummary{}
r.log.Info().Msg("extracting repositories")
@ -154,8 +155,8 @@ func (r *queryResolver) ImageListForCve(ctx context.Context, id string) ([]*gql_
}
// ImageListWithCVEFixed is the resolver for the ImageListWithCVEFixed field.
func (r *queryResolver) ImageListWithCVEFixed(ctx context.Context, id string, image string) (*gql_generated.ImgResultForFixedCve, error) {
imgResultForFixedCVE := &gql_generated.ImgResultForFixedCve{}
func (r *queryResolver) ImageListWithCVEFixed(ctx context.Context, id string, image string) ([]*gql_generated.ImageSummary, error) {
tagListForCVE := []*gql_generated.ImageSummary{}
r.log.Info().Str("image", image).Msg("extracting list of tags available in image")
@ -163,7 +164,7 @@ func (r *queryResolver) ImageListWithCVEFixed(ctx context.Context, id string, im
if err != nil {
r.log.Error().Err(err).Msg("unable to read image tags")
return imgResultForFixedCVE, err
return tagListForCVE, err
}
infectedTags := make([]common.TagInfo, 0)
@ -213,28 +214,34 @@ func (r *queryResolver) ImageListWithCVEFixed(ctx context.Context, id string, im
}
}
var finalTagList []*gql_generated.TagInfo
if len(infectedTags) != 0 {
r.log.Info().Msg("comparing fixed tags timestamp")
fixedTags := common.GetFixedTags(tagsInfo, infectedTags)
finalTagList = getGraphqlCompatibleTags(fixedTags)
tagsInfo = common.GetFixedTags(tagsInfo, infectedTags)
} else {
r.log.Info().Str("image", image).Str("cve-id", id).Msg("image does not contain any tag that have given cve")
finalTagList = getGraphqlCompatibleTags(tagsInfo)
}
imgResultForFixedCVE = &gql_generated.ImgResultForFixedCve{Tags: finalTagList}
for _, tag := range tagsInfo {
digest := godigest.Digest(tag.Digest)
return imgResultForFixedCVE, nil
manifest, err := r.cveInfo.LayoutUtils.GetImageBlobManifest(image, digest)
if err != nil {
r.log.Error().Err(err).Msg("extension api: error reading manifest")
return []*gql_generated.ImageSummary{}, err
}
imageInfo := buildImageInfo(image, tag.Name, digest, manifest)
tagListForCVE = append(tagListForCVE, imageInfo)
}
return tagListForCVE, nil
}
// ImageListForDigest is the resolver for the ImageListForDigest field.
func (r *queryResolver) ImageListForDigest(ctx context.Context, id string) ([]*gql_generated.ImgResultForDigest, error) {
imgResultForDigest := []*gql_generated.ImgResultForDigest{}
func (r *queryResolver) ImageListForDigest(ctx context.Context, id string) ([]*gql_generated.ImageSummary, error) {
imgResultForDigest := []*gql_generated.ImageSummary{}
r.log.Info().Msg("extracting repositories")
@ -281,10 +288,10 @@ func (r *queryResolver) ImageListForDigest(ctx context.Context, id string) ([]*g
}
// ImageListWithLatestTag is the resolver for the ImageListWithLatestTag field.
func (r *queryResolver) ImageListWithLatestTag(ctx context.Context) ([]*gql_generated.ImageInfo, error) {
func (r *queryResolver) ImageListWithLatestTag(ctx context.Context) ([]*gql_generated.ImageSummary, error) {
r.log.Info().Msg("extension api: finding image list")
imageList := make([]*gql_generated.ImageInfo, 0)
imageList := make([]*gql_generated.ImageSummary, 0)
defaultStore := r.storeController.DefaultStore
@ -317,6 +324,43 @@ func (r *queryResolver) ImageListWithLatestTag(ctx context.Context) ([]*gql_gene
return imageList, nil
}
// ImageList is the resolver for the ImageList field.
func (r *queryResolver) ImageList(ctx context.Context, repo string) ([]*gql_generated.ImageSummary, error) {
r.log.Info().Msg("extension api: getting a list of all images")
imageList := make([]*gql_generated.ImageSummary, 0)
defaultStore := r.storeController.DefaultStore
dsImageList, err := r.getImageList(defaultStore, repo)
if err != nil {
r.log.Error().Err(err).Msg("extension api: error extracting default store image list")
return imageList, err
}
if len(dsImageList) != 0 {
imageList = append(imageList, dsImageList...)
}
subStore := r.storeController.SubStore
for _, store := range subStore {
ssImageList, err := r.getImageList(store, repo)
if err != nil {
r.log.Error().Err(err).Msg("extension api: error extracting substore image list")
return imageList, err
}
if len(ssImageList) != 0 {
imageList = append(imageList, ssImageList...)
}
}
return imageList, nil
}
// ExpandedRepoInfo is the resolver for the ExpandedRepoInfo field.
func (r *queryResolver) ExpandedRepoInfo(ctx context.Context, repo string) (*gql_generated.RepoInfo, error) {
olu := common.NewBaseOciLayoutUtils(r.storeController, r.log)
@ -331,7 +375,7 @@ func (r *queryResolver) ExpandedRepoInfo(ctx context.Context, repo string) (*gql
// repos type is of common deep copy this to search
repoInfo := &gql_generated.RepoInfo{}
manifests := make([]*gql_generated.ManifestInfo, 0)
images := make([]*gql_generated.ImageSummary, 0)
summary := &gql_generated.RepoSummary{}
@ -358,34 +402,34 @@ func (r *queryResolver) ExpandedRepoInfo(ctx context.Context, repo string) (*gql
score := -1 // score not relevant for this query
summary.Score = &score
for _, manifest := range origRepoInfo.Manifests {
tag := manifest.Tag
for _, image := range origRepoInfo.Images {
tag := image.Tag
digest := manifest.Digest
digest := image.Digest
isSigned := manifest.IsSigned
isSigned := image.IsSigned
manifestInfo := &gql_generated.ManifestInfo{Tag: &tag, Digest: &digest, IsSigned: &isSigned}
imageSummary := &gql_generated.ImageSummary{Tag: &tag, Digest: &digest, IsSigned: &isSigned}
layers := make([]*gql_generated.LayerInfo, 0)
layers := make([]*gql_generated.LayerSummary, 0)
for _, l := range manifest.Layers {
for _, l := range image.Layers {
size := l.Size
digest := l.Digest
layerInfo := &gql_generated.LayerInfo{Digest: &digest, Size: &size}
layerInfo := &gql_generated.LayerSummary{Digest: &digest, Size: &size}
layers = append(layers, layerInfo)
}
manifestInfo.Layers = layers
imageSummary.Layers = layers
manifests = append(manifests, manifestInfo)
images = append(images, imageSummary)
}
repoInfo.Summary = summary
repoInfo.Manifests = manifests
repoInfo.Images = images
return repoInfo, nil
}