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/containers/image/v5
- github.com/opencontainers/image-spec - github.com/opencontainers/image-spec
- github.com/open-policy-agent/opa - github.com/open-policy-agent/opa
- github.com/vektah/gqlparser/v2
- go.opentelemetry.io/otel - go.opentelemetry.io/otel
- go.opentelemetry.io/otel/exporters/otlp - go.opentelemetry.io/otel/exporters/otlp
- go.opentelemetry.io/otel/metric - go.opentelemetry.io/otel/metric

View file

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

View file

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

View file

@ -4,13 +4,18 @@
package cli package cli
import ( import (
"encoding/json"
"fmt" "fmt"
"net/http"
"net/url"
"os" "os"
"path" "path"
"github.com/briandowns/spinner" "github.com/briandowns/spinner"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gopkg.in/resty.v1"
zotErrors "zotregistry.io/zot/errors" zotErrors "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api/constants"
) )
func NewCveCommand(searchService SearchService) *cobra.Command { func NewCveCommand(searchService SearchService) *cobra.Command {
@ -68,7 +73,8 @@ func NewCveCommand(searchService SearchService) *cobra.Command {
} }
spin := spinner.New(spinner.CharSets[39], spinnerDuration, spinner.WithWriter(cmd.ErrOrStderr())) spin := spinner.New(spinner.CharSets[39], spinnerDuration, spinner.WithWriter(cmd.ErrOrStderr()))
spin.Prefix = fmt.Sprintf("Fetching from %s.. ", servURL) spin.Prefix = fmt.Sprintf("Fetching from %s..", servURL)
spin.Suffix = "\n\b"
verbose = false verbose = false
@ -112,7 +118,7 @@ func NewCveCommand(searchService SearchService) *cobra.Command {
func setupCveFlags(cveCmd *cobra.Command, variables cveFlagVariables) { func setupCveFlags(cveCmd *cobra.Command, variables cveFlagVariables) {
variables.searchCveParams["imageName"] = cveCmd.Flags().StringP("image", "I", "", "List CVEs by IMAGENAME[:TAG]") 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().StringVar(variables.servURL, "url", "", "Specify zot server URL if config-name is not mentioned")
cveCmd.Flags().StringVarP(variables.user, "user", "u", "", `User Credentials of `+ cveCmd.Flags().StringVarP(variables.user, "user", "u", "", `User Credentials of `+
@ -131,8 +137,80 @@ type cveFlagVariables struct {
fixedFlag *bool 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 { 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) found, err := searcher.search(searchConfig)
if found { if found {
if err != nil { if err != nil {

View file

@ -15,6 +15,7 @@ import (
"time" "time"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
"github.com/spf13/cobra"
"gopkg.in/resty.v1" "gopkg.in/resty.v1"
zotErrors "zotregistry.io/zot/errors" zotErrors "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/api"
@ -51,6 +52,7 @@ func TestSearchCVECmd(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
}) })
}) })
Convey("Test CVE no url", t, func() { Convey("Test CVE no url", t, func() {
args := []string{"cvetest", "-i", "cveIdRandom"} args := []string{"cvetest", "-i", "cveIdRandom"}
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`) configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`)
@ -136,10 +138,8 @@ func TestSearchCVECmd(t *testing.T) {
Convey("Test CVE url from config", t, func() { Convey("Test CVE url from config", t, func() {
args := []string{"cvetest", "--image", "dummyImageName:tag"} args := []string{"cvetest", "--image", "dummyImageName:tag"}
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","url":"https://test-url.com","showspinner":false}]}`) configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","url":"https://test-url.com","showspinner":false}]}`)
defer os.Remove(configPath) defer os.Remove(configPath)
cmd := NewCveCommand(new(mockService)) cmd := NewCveCommand(new(mockService))
buff := bytes.NewBufferString("") buff := bytes.NewBufferString("")
cmd.SetOut(buff) cmd.SetOut(buff)
@ -162,10 +162,10 @@ func TestSearchCVECmd(t *testing.T) {
cveCmd.SetErr(buff) cveCmd.SetErr(buff)
cveCmd.SetArgs(args) cveCmd.SetArgs(args)
err := cveCmd.Execute() err := cveCmd.Execute()
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`) space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ") str := space.ReplaceAllString(buff.String(), " ")
So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE dummyImageName tag DigestsA 123kB") So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE dummyImageName tag DigestsA 123kB")
So(err, ShouldBeNil)
Convey("using shorthand", func() { Convey("using shorthand", func() {
args := []string{"cvetest", "-I", "dummyImageName", "--cve-id", "aCVEID", "--url", "someURL"} args := []string{"cvetest", "-I", "dummyImageName", "--cve-id", "aCVEID", "--url", "someURL"}
buff := bytes.NewBufferString("") buff := bytes.NewBufferString("")
@ -176,11 +176,10 @@ func TestSearchCVECmd(t *testing.T) {
cveCmd.SetErr(buff) cveCmd.SetErr(buff)
cveCmd.SetArgs(args) cveCmd.SetArgs(args)
err := cveCmd.Execute() err := cveCmd.Execute()
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`) space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ") str := space.ReplaceAllString(buff.String(), " ")
So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE dummyImageName tag DigestsA 123kB") 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(), " ") str := space.ReplaceAllString(buff.String(), " ")
So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE anImage tag DigestsA 123kB") So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE anImage tag DigestsA 123kB")
So(err, ShouldBeNil) 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() { 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(), " ") str := space.ReplaceAllString(buff.String(), " ")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE fixedImage tag DigestsA 123kB") 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() port := test.GetFreePort()
url := test.GetBaseURL(port) url := test.GetBaseURL(port)
conf := config.New() conf := config.New()
@ -351,6 +392,7 @@ func TestServerCVEResponse(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(str, ShouldContainSubstring, "ID SEVERITY TITLE") So(str, ShouldContainSubstring, "ID SEVERITY TITLE")
So(str, ShouldContainSubstring, "CVE") So(str, ShouldContainSubstring, "CVE")
Convey("invalid image", func() { Convey("invalid image", func() {
args := []string{"cvetest", "--image", "invalid:0.0.1"} args := []string{"cvetest", "--image", "invalid:0.0.1"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
@ -363,6 +405,33 @@ func TestServerCVEResponse(t *testing.T) {
err = cveCmd.Execute() err = cveCmd.Execute()
So(err, ShouldNotBeNil) 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() { Convey("Test images by CVE ID", t, func() {
@ -380,6 +449,7 @@ func TestServerCVEResponse(t *testing.T) {
str = strings.TrimSpace(str) str = strings.TrimSpace(str)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(str, ShouldEqual, "IMAGE NAME TAG DIGEST SIZE zot-cve-test 0.0.1 63a795ca 75MB") So(str, ShouldEqual, "IMAGE NAME TAG DIGEST SIZE zot-cve-test 0.0.1 63a795ca 75MB")
Convey("invalid CVE ID", func() { Convey("invalid CVE ID", func() {
args := []string{"cvetest", "--cve-id", "invalid"} args := []string{"cvetest", "--cve-id", "invalid"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) 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(err, ShouldBeNil)
So(str, ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST SIZE") 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() { 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) str = strings.TrimSpace(str)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(str, ShouldEqual, "") So(str, ShouldEqual, "")
Convey("random cve", func() { Convey("random cve", func() {
args := []string{"cvetest", "--cve-id", "random", "--image", "zot-cve-test", "--fixed"} args := []string{"cvetest", "--cve-id", "random", "--image", "zot-cve-test", "--fixed"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) 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") 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"} 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)) configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath) defer os.Remove(configPath)
@ -446,6 +531,23 @@ func TestServerCVEResponse(t *testing.T) {
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
So(strings.TrimSpace(str), ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST SIZE") 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() { Convey("Test CVE by name and CVE ID", t, func() {
@ -462,7 +564,8 @@ func TestServerCVEResponse(t *testing.T) {
str := space.ReplaceAllString(buff.String(), " ") str := space.ReplaceAllString(buff.String(), " ")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE zot-cve-test 0.0.1 63a795ca 75MB") So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE zot-cve-test 0.0.1 63a795ca 75MB")
Convey("invalidname and CVE ID", func() {
Convey("invalid name and CVE ID", func() {
args := []string{"cvetest", "--image", "test", "--cve-id", "CVE-20807"} args := []string{"cvetest", "--image", "test", "--cve-id", "CVE-20807"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath) defer os.Remove(configPath)
@ -477,5 +580,451 @@ func TestServerCVEResponse(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(strings.TrimSpace(str), ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST SIZE") 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 { 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) found, err := searcher.search(searchConfig)
if found { if found {
if err != nil { if err != nil {

View file

@ -21,10 +21,12 @@ import (
godigest "github.com/opencontainers/go-digest" godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1" ispec "github.com/opencontainers/image-spec/specs-go/v1"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
"github.com/spf13/cobra"
"gopkg.in/resty.v1" "gopkg.in/resty.v1"
zotErrors "zotregistry.io/zot/errors" zotErrors "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants"
extconf "zotregistry.io/zot/pkg/extensions/config" extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/test" "zotregistry.io/zot/pkg/test"
) )
@ -56,6 +58,7 @@ func TestSearchImageCmd(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
}) })
}) })
Convey("Test image no url", t, func() { Convey("Test image no url", t, func() {
args := []string{"imagetest", "--name", "dummyIdRandom"} args := []string{"imagetest", "--name", "dummyIdRandom"}
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`) configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`)
@ -126,6 +129,7 @@ func TestSearchImageCmd(t *testing.T) {
So(err, ShouldEqual, zotErrors.ErrInvalidURL) So(err, ShouldEqual, zotErrors.ErrInvalidURL)
So(buff.String(), ShouldContainSubstring, "invalid URL format") So(buff.String(), ShouldContainSubstring, "invalid URL format")
}) })
Convey("Test image invalid url port", t, func() { Convey("Test image invalid url port", t, func() {
args := []string{"imagetest", "--name", "dummyImageName", "--url", "http://localhost:99999"} args := []string{"imagetest", "--name", "dummyImageName", "--url", "http://localhost:99999"}
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`) configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`)
@ -153,6 +157,7 @@ func TestSearchImageCmd(t *testing.T) {
So(buff.String(), ShouldContainSubstring, "invalid port") So(buff.String(), ShouldContainSubstring, "invalid port")
}) })
}) })
Convey("Test image unreachable", t, func() { Convey("Test image unreachable", t, func() {
args := []string{"imagetest", "--name", "dummyImageName", "--url", "http://localhost:9999"} args := []string{"imagetest", "--name", "dummyImageName", "--url", "http://localhost:9999"}
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`) configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`)
@ -168,10 +173,8 @@ func TestSearchImageCmd(t *testing.T) {
Convey("Test image url from config", t, func() { Convey("Test image url from config", t, func() {
args := []string{"imagetest", "--name", "dummyImageName"} args := []string{"imagetest", "--name", "dummyImageName"}
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) defer os.Remove(configPath)
cmd := NewImageCommand(new(mockService)) cmd := NewImageCommand(new(mockService))
buff := bytes.NewBufferString("") buff := bytes.NewBufferString("")
cmd.SetOut(buff) cmd.SetOut(buff)
@ -215,15 +218,44 @@ func TestSearchImageCmd(t *testing.T) {
So(err, ShouldBeNil) 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) { func TestListRepos(t *testing.T) {
Convey("Test listing repositories", t, func() { Convey("Test listing repositories", t, func() {
args := []string{"config-test"} args := []string{"config-test"}
configPath := makeConfigFile(`{"configs":[{"_name":"config-test","url":"https://test-url.com","showspinner":false}]}`) configPath := makeConfigFile(`{"configs":[{"_name":"config-test","url":"https://test-url.com","showspinner":false}]}`)
defer os.Remove(configPath) defer os.Remove(configPath)
cmd := NewRepoCommand(new(mockService)) cmd := NewRepoCommand(new(mockService))
buff := bytes.NewBufferString("") buff := bytes.NewBufferString("")
cmd.SetOut(buff) cmd.SetOut(buff)
@ -264,11 +296,9 @@ func TestListRepos(t *testing.T) {
Convey("Test listing repositories error", t, func() { Convey("Test listing repositories error", t, func() {
args := []string{"config-test"} args := []string{"config-test"}
configPath := makeConfigFile(`{"configs":[{"_name":"config-test", configPath := makeConfigFile(`{"configs":[{"_name":"config-test",
"url":"https://invalid.invalid","showspinner":false}]}`) "url":"https://invalid.invalid","showspinner":false}]}`)
defer os.Remove(configPath) defer os.Remove(configPath)
cmd := NewRepoCommand(new(searchService)) cmd := NewRepoCommand(new(searchService))
buff := bytes.NewBufferString("") buff := bytes.NewBufferString("")
cmd.SetOut(buff) cmd.SetOut(buff)
@ -280,10 +310,8 @@ func TestListRepos(t *testing.T) {
Convey("Test unable to get config value", t, func() { Convey("Test unable to get config value", t, func() {
args := []string{"config-test-inexistent"} args := []string{"config-test-inexistent"}
configPath := makeConfigFile(`{"configs":[{"_name":"config-test","url":"https://test-url.com","showspinner":false}]}`) configPath := makeConfigFile(`{"configs":[{"_name":"config-test","url":"https://test-url.com","showspinner":false}]}`)
defer os.Remove(configPath) defer os.Remove(configPath)
cmd := NewRepoCommand(new(mockService)) cmd := NewRepoCommand(new(mockService))
buff := bytes.NewBufferString("") buff := bytes.NewBufferString("")
cmd.SetOut(buff) cmd.SetOut(buff)
@ -295,10 +323,8 @@ func TestListRepos(t *testing.T) {
Convey("Test error - no url provided", t, func() { Convey("Test error - no url provided", t, func() {
args := []string{"config-test"} args := []string{"config-test"}
configPath := makeConfigFile(`{"configs":[{"_name":"config-test","url":"","showspinner":false}]}`) configPath := makeConfigFile(`{"configs":[{"_name":"config-test","url":"","showspinner":false}]}`)
defer os.Remove(configPath) defer os.Remove(configPath)
cmd := NewRepoCommand(new(mockService)) cmd := NewRepoCommand(new(mockService))
buff := bytes.NewBufferString("") buff := bytes.NewBufferString("")
cmd.SetOut(buff) cmd.SetOut(buff)
@ -310,10 +336,8 @@ func TestListRepos(t *testing.T) {
Convey("Test error - no args provided", t, func() { Convey("Test error - no args provided", t, func() {
var args []string var args []string
configPath := makeConfigFile(`{"configs":[{"_name":"config-test","url":"","showspinner":false}]}`) configPath := makeConfigFile(`{"configs":[{"_name":"config-test","url":"","showspinner":false}]}`)
defer os.Remove(configPath) defer os.Remove(configPath)
cmd := NewRepoCommand(new(mockService)) cmd := NewRepoCommand(new(mockService))
buff := bytes.NewBufferString("") buff := bytes.NewBufferString("")
cmd.SetOut(buff) cmd.SetOut(buff)
@ -325,11 +349,9 @@ func TestListRepos(t *testing.T) {
Convey("Test error - spinner config invalid", t, func() { Convey("Test error - spinner config invalid", t, func() {
args := []string{"config-test"} args := []string{"config-test"}
configPath := makeConfigFile(`{"configs":[{"_name":"config-test", configPath := makeConfigFile(`{"configs":[{"_name":"config-test",
"url":"https://test-url.com","showspinner":invalid}]}`) "url":"https://test-url.com","showspinner":invalid}]}`)
defer os.Remove(configPath) defer os.Remove(configPath)
cmd := NewRepoCommand(new(mockService)) cmd := NewRepoCommand(new(mockService))
buff := bytes.NewBufferString("") buff := bytes.NewBufferString("")
cmd.SetOut(buff) cmd.SetOut(buff)
@ -341,11 +363,9 @@ func TestListRepos(t *testing.T) {
Convey("Test error - verifyTLSConfig fails", t, func() { Convey("Test error - verifyTLSConfig fails", t, func() {
args := []string{"config-test"} args := []string{"config-test"}
configPath := makeConfigFile(`{"configs":[{"_name":"config-test",
configPath := makeConfigFile(`{"configs":[{"_name":"config-test", "verify-tls":"invalid", "url":"https://test-url.com","showspinner":false}]}`)
"verify-tls":"invalid", "url":"https://test-url.com","showspinner":false}]}`)
defer os.Remove(configPath) defer os.Remove(configPath)
cmd := NewRepoCommand(new(mockService)) cmd := NewRepoCommand(new(mockService))
buff := bytes.NewBufferString("") buff := bytes.NewBufferString("")
cmd.SetOut(buff) cmd.SetOut(buff)
@ -359,10 +379,8 @@ func TestListRepos(t *testing.T) {
func TestOutputFormat(t *testing.T) { func TestOutputFormat(t *testing.T) {
Convey("Test text", t, func() { Convey("Test text", t, func() {
args := []string{"imagetest", "--name", "dummyImageName", "-o", "text"} args := []string{"imagetest", "--name", "dummyImageName", "-o", "text"}
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) defer os.Remove(configPath)
cmd := NewImageCommand(new(mockService)) cmd := NewImageCommand(new(mockService))
buff := bytes.NewBufferString("") buff := bytes.NewBufferString("")
cmd.SetOut(buff) cmd.SetOut(buff)
@ -375,12 +393,12 @@ func TestOutputFormat(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
}) })
// get image config functia
Convey("Test json", t, func() { Convey("Test json", t, func() {
args := []string{"imagetest", "--name", "dummyImageName", "-o", "json"} args := []string{"imagetest", "--name", "dummyImageName", "-o", "json"}
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) defer os.Remove(configPath)
cmd := NewImageCommand(new(mockService)) cmd := NewImageCommand(new(mockService))
buff := bytes.NewBufferString("") buff := bytes.NewBufferString("")
cmd.SetOut(buff) cmd.SetOut(buff)
@ -389,17 +407,15 @@ func TestOutputFormat(t *testing.T) {
err := cmd.Execute() err := cmd.Execute()
space := regexp.MustCompile(`\s+`) space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ") str := space.ReplaceAllString(buff.String(), " ")
So(strings.TrimSpace(str), ShouldEqual, `{ "name": "dummyImageName", "tags": [ { "name":`+ So(strings.TrimSpace(str), ShouldEqual, `{ "repoName": "dummyImageName", "tag": "tag", `+
` "tag", "size": 123445, "digest": "DigestsAreReallyLong", "configDigest": "", "layerDigests": null } ] }`) `"configDigest": "", "digest": "DigestsAreReallyLong", "layers": null, "size": "123445" }`)
So(err, ShouldBeNil) So(err, ShouldBeNil)
}) })
Convey("Test yaml", t, func() { Convey("Test yaml", t, func() {
args := []string{"imagetest", "--name", "dummyImageName", "-o", "yaml"} args := []string{"imagetest", "--name", "dummyImageName", "-o", "yaml"}
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) defer os.Remove(configPath)
cmd := NewImageCommand(new(mockService)) cmd := NewImageCommand(new(mockService))
buff := bytes.NewBufferString("") buff := bytes.NewBufferString("")
cmd.SetOut(buff) cmd.SetOut(buff)
@ -408,16 +424,21 @@ func TestOutputFormat(t *testing.T) {
err := cmd.Execute() err := cmd.Execute()
space := regexp.MustCompile(`\s+`) space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ") str := space.ReplaceAllString(buff.String(), " ")
So(strings.TrimSpace(str), ShouldEqual, `name: dummyImageName tags: -`+ So(
` name: tag size: 123445 digest: DigestsAreReallyLong configdigest: "" layers: []`) strings.TrimSpace(str),
ShouldEqual,
`reponame: dummyImageName tag: tag configdigest: "" `+
`digest: DigestsAreReallyLong layers: [] size: "123445"`,
)
So(err, ShouldBeNil) So(err, ShouldBeNil)
Convey("Test yml", func() { Convey("Test yml", func() {
args := []string{"imagetest", "--name", "dummyImageName", "-o", "yml"} args := []string{"imagetest", "--name", "dummyImageName", "-o", "yml"}
configPath := makeConfigFile(
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`) `{"configs":[{"_name":"imagetest",` +
`"url":"https://test-url.com","showspinner":false}]}`,
)
defer os.Remove(configPath) defer os.Remove(configPath)
cmd := NewImageCommand(new(mockService)) cmd := NewImageCommand(new(mockService))
buff := bytes.NewBufferString("") buff := bytes.NewBufferString("")
cmd.SetOut(buff) cmd.SetOut(buff)
@ -426,18 +447,20 @@ func TestOutputFormat(t *testing.T) {
err := cmd.Execute() err := cmd.Execute()
space := regexp.MustCompile(`\s+`) space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ") str := space.ReplaceAllString(buff.String(), " ")
So(strings.TrimSpace(str), ShouldEqual, `name: dummyImageName tags: -`+ So(
` name: tag size: 123445 digest: DigestsAreReallyLong configdigest: "" layers: []`) strings.TrimSpace(str),
ShouldEqual,
`reponame: dummyImageName tag: tag configdigest: "" `+
`digest: DigestsAreReallyLong layers: [] size: "123445"`,
)
So(err, ShouldBeNil) So(err, ShouldBeNil)
}) })
}) })
Convey("Test invalid", t, func() { Convey("Test invalid", t, func() {
args := []string{"imagetest", "--name", "dummyImageName", "-o", "random"} args := []string{"imagetest", "--name", "dummyImageName", "-o", "random"}
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) defer os.Remove(configPath)
cmd := NewImageCommand(new(mockService)) cmd := NewImageCommand(new(mockService))
buff := bytes.NewBufferString("") buff := bytes.NewBufferString("")
cmd.SetOut(buff) 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() { Convey("Test from real server", t, func() {
port := test.GetFreePort() port := test.GetFreePort()
url := test.GetBaseURL(port) 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)) configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath) defer os.Remove(configPath)
cmd := NewImageCommand(new(searchService)) cmd := NewImageCommand(new(searchService))
// buff := bytes.NewBufferString("")
buff := &bytes.Buffer{} buff := &bytes.Buffer{}
cmd.SetOut(buff) cmd.SetOut(buff)
cmd.SetErr(buff) cmd.SetErr(buff)
@ -504,6 +526,19 @@ func TestServerResponse(t *testing.T) {
So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIZE") So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 15B") So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 15B")
So(actual, ShouldContainSubstring, "repo7 test:1.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() { 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:2.0 883fc0c5 15B")
So(actual, ShouldContainSubstring, "repo7 test:1.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() { 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, "IMAGE NAME TAG DIGEST SIZE")
So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 15B") So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 15B")
So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 15B") So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 15B")
Convey("with shorthand", func() { Convey("with shorthand", func() {
args := []string{"imagetest", "-d", "883fc0c5"} args := []string{"imagetest", "-d", "883fc0c5"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
@ -608,9 +658,37 @@ func TestServerResponse(t *testing.T) {
So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 15B") So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 15B")
So(actual, ShouldContainSubstring, "repo7 test:1.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("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))
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 name invalid name", func() { Convey("Test image by name nonexistent name", func() {
args := []string{"imagetest", "--name", "repo777"} args := []string{"imagetest", "--name", "repo777"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath) defer os.Remove(configPath)
@ -620,16 +698,15 @@ func TestServerResponse(t *testing.T) {
cmd.SetErr(buff) cmd.SetErr(buff)
cmd.SetArgs(args) cmd.SetArgs(args)
err := cmd.Execute() err := cmd.Execute()
So(err, ShouldNotBeNil) So(err, ShouldBeNil)
actual := buff.String() So(len(buff.String()), ShouldEqual, 0)
So(actual, ShouldContainSubstring, "unknown")
}) })
Convey("Test list repos error", func() { Convey("Test list repos error", func() {
args := []string{"config-test"} args := []string{"config-test"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"config-test", configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"config-test",
"url":"%s","showspinner":false}]}`, url)) "url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath) defer os.Remove(configPath)
cmd := NewRepoCommand(new(searchService)) cmd := NewRepoCommand(new(searchService))
@ -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 { func uploadManifest(url string) error {
// create a blob/layer // create a blob/layer
resp, _ := resty.R().Post(url + "/v2/repo7/blobs/uploads/") 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} 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, func (service mockService) getAllImages(ctx context.Context, config searchConfig, username, password string,
channel chan stringResult, wtgrp *sync.WaitGroup, channel chan stringResult, wtgrp *sync.WaitGroup,
) { ) {
@ -748,14 +1305,10 @@ func (service mockService) getAllImages(ctx context.Context, config searchConfig
defer close(channel) defer close(channel)
image := &imageStruct{} image := &imageStruct{}
image.Name = "randomimageName" image.RepoName = "randomimageName"
image.Tags = []tags{ image.Tag = "tag"
{ image.Digest = "DigestsAreReallyLong"
Name: "tag", image.Size = "123445"
Digest: "DigestsAreReallyLong",
Size: 123445,
},
}
str, err := image.string(*config.outputFormat) str, err := image.string(*config.outputFormat)
if err != nil { if err != nil {
@ -774,14 +1327,10 @@ func (service mockService) getImageByName(ctx context.Context, config searchConf
defer close(channel) defer close(channel)
image := &imageStruct{} image := &imageStruct{}
image.Name = imageName image.RepoName = imageName
image.Tags = []tags{ image.Tag = "tag"
{ image.Digest = "DigestsAreReallyLong"
Name: "tag", image.Size = "123445"
Digest: "DigestsAreReallyLong",
Size: 123445,
},
}
str, err := image.string(*config.outputFormat) str, err := image.string(*config.outputFormat)
if err != nil { if err != nil {
@ -831,6 +1380,18 @@ func (service mockService) getCveByImage(ctx context.Context, config searchConfi
rch <- stringResult{str, nil} 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, func (service mockService) getImagesByCveID(ctx context.Context, config searchConfig, username, password, cvid string,
rch chan stringResult, wtgrp *sync.WaitGroup, 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) 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 { func makeConfigFile(content string) string {
os.Setenv("HOME", os.TempDir()) os.Setenv("HOME", os.TempDir())

View file

@ -37,6 +37,27 @@ func getCveSearchers() []searcher {
return searchers 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 { type searcher interface {
search(searchConfig searchConfig) (bool, error) 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{} type imageByNameSearcher struct{}
func (search imageByNameSearcher) search(config searchConfig) (bool, error) { 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{} type imagesByDigestSearcher struct{}
func (search imagesByDigestSearcher) search(config searchConfig) (bool, error) { 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{} type cveByImageSearcher struct{}
func (search cveByImageSearcher) search(config searchConfig) (bool, error) { 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{} type imagesByCVEIDSearcher struct{}
func (search imagesByCVEIDSearcher) search(config searchConfig) (bool, error) { 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 return false, nil
} }
@ -210,7 +334,7 @@ func (search imagesByCVEIDSearcher) search(config searchConfig) (bool, error) {
wg.Add(1) 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) wg.Add(1)
errCh := make(chan error, 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{} type tagsByImageNameAndCVEIDSearcher struct{}
func (search tagsByImageNameAndCVEIDSearcher) search(config searchConfig) (bool, error) { 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 return false, nil
} }
@ -246,7 +394,7 @@ func (search tagsByImageNameAndCVEIDSearcher) search(config searchConfig) (bool,
wg.Add(1) wg.Add(1)
go config.searchService.getImageByNameAndCVEID(ctx, config, username, password, *config.params["imageName"], go config.searchService.getImageByNameAndCVEID(ctx, config, username, password, *config.params["imageName"],
*config.params["cvid"], strErr, &wg) *config.params["cveID"], strErr, &wg)
wg.Add(1) wg.Add(1)
errCh := make(chan error, 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{} type fixedTagsSearcher struct{}
func (search fixedTagsSearcher) search(config searchConfig) (bool, error) { 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 return false, nil
} }
@ -282,7 +458,7 @@ func (search fixedTagsSearcher) search(config searchConfig) (bool, error) {
wg.Add(1) wg.Add(1)
go config.searchService.getFixedTagsForCVE(ctx, config, username, password, *config.params["imageName"], go config.searchService.getFixedTagsForCVE(ctx, config, username, password, *config.params["imageName"],
*config.params["cvid"], strErr, &wg) *config.params["cveID"], strErr, &wg)
wg.Add(1) wg.Add(1)
errCh := make(chan error, 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, func collectResults(config searchConfig, wg *sync.WaitGroup, imageErr chan stringResult,
cancel context.CancelFunc, printHeader printHeader, errCh chan error, cancel context.CancelFunc, printHeader printHeader, errCh chan error,
) { ) {
@ -376,12 +585,14 @@ type spinnerState struct {
enabled bool enabled bool
} }
// nolint
func (spinner *spinnerState) startSpinner() { func (spinner *spinnerState) startSpinner() {
if spinner.enabled { if spinner.enabled {
spinner.spinner.Start() spinner.spinner.Start()
} }
} }
// nolint
func (spinner *spinnerState) stopSpinner() { func (spinner *spinnerState) stopSpinner() {
if spinner.enabled && spinner.spinner.Active() { if spinner.enabled && spinner.spinner.Active() {
spinner.spinner.Stop() spinner.spinner.Stop()
@ -397,14 +608,14 @@ func getEmptyStruct() struct{} {
} }
func newSet(initialValues ...string) *set { func newSet(initialValues ...string) *set {
ret := &set{} setValues := &set{}
ret.m = make(map[string]struct{}) setValues.m = make(map[string]struct{})
for _, val := range initialValues { for _, val := range initialValues {
ret.m[val] = getEmptyStruct() setValues.m[val] = getEmptyStruct()
} }
return ret return setValues
} }
func (s *set) contains(value string) bool { func (s *set) contains(value string) bool {
@ -413,6 +624,10 @@ func (s *set) contains(value string) bool {
return c return c
} }
const (
waitTimeout = httpTimeout + 5*time.Second
)
var ( var (
ErrCannotSearch = errors.New("cannot search with these parameters") ErrCannotSearch = errors.New("cannot search with these parameters")
ErrInvalidOutputFormat = errors.New("invalid output format") ErrInvalidOutputFormat = errors.New("invalid output format")
@ -438,7 +653,7 @@ func printImageTableHeader(writer io.Writer, verbose bool) {
table.SetColMinWidth(colLayersIndex, layersWidth) table.SetColMinWidth(colLayersIndex, layersWidth)
} }
row := make([]string, 6) //nolint:gomnd row := make([]string, 6) // nolint:gomnd
row[colImageNameIndex] = "IMAGE NAME" row[colImageNameIndex] = "IMAGE NAME"
row[colTagIndex] = "TAG" row[colTagIndex] = "TAG"
@ -465,9 +680,28 @@ func printCVETableHeader(writer io.Writer, verbose bool) {
table.Render() table.Render()
} }
const ( func printResult(config searchConfig, imageList []imageStruct) error {
waitTimeout = httpTimeout + 5*time.Second 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 ( var (
errInvalidImageNameAndTag = errors.New("cli: Invalid input format. Expected IMAGENAME:TAG") errInvalidImageNameAndTag = errors.New("cli: Invalid input format. Expected IMAGENAME:TAG")

View file

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

View file

@ -2,7 +2,6 @@ package common
import ( import (
"fmt" "fmt"
"path"
"sort" "sort"
"strings" "strings"
"time" "time"
@ -25,14 +24,6 @@ type TagInfo struct {
Timestamp time.Time 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 { func GetRootDir(image string, storeController storage.StoreController) string {
var rootDir string var rootDir string

View file

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

View file

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

View file

@ -11,6 +11,7 @@ import (
"github.com/aquasecurity/trivy/pkg/commands/operation" "github.com/aquasecurity/trivy/pkg/commands/operation"
"github.com/aquasecurity/trivy/pkg/report" "github.com/aquasecurity/trivy/pkg/report"
"github.com/aquasecurity/trivy/pkg/types" "github.com/aquasecurity/trivy/pkg/types"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"zotregistry.io/zot/pkg/extensions/search/common" "zotregistry.io/zot/pkg/extensions/search/common"
"zotregistry.io/zot/pkg/log" "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, func (cveinfo CveInfo) GetImageListForCVE(repo, cvid string, imgStore storage.ImageStore,
trivyCtx *TrivyCtx, trivyCtx *TrivyCtx,
) ([]*string, error) { ) ([]ImageInfoByCVE, error) {
tags := make([]*string, 0) imgList := make([]ImageInfoByCVE, 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
}
rootDir := imgStore.RootDir() 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) image := fmt.Sprintf("%s:%s", repo, tag)
trivyCtx.Input = path.Join(rootDir, image) 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 _, result := range report.Results {
for _, vulnerability := range result.Vulnerabilities { for _, vulnerability := range result.Vulnerabilities {
if vulnerability.VulnerabilityID == cvid { if vulnerability.VulnerabilityID == cvid {
copyImgTag := tag digest := manifest.Digest
tags = append(tags, &copyImgTag)
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 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"` ImgList ImgList `json:"data"`
} }
type ImgWithFixedCVE struct {
ImgResults ImgResults `json:"data"`
}
//nolint:tagliatelle // graphQL schema //nolint:tagliatelle // graphQL schema
type ImgResults struct { type ImgListWithCVEFixed struct {
ImgResultForFixedCVE ImgResultForFixedCVE `json:"ImgResultForFixedCVE"` Images []ImageInfo `json:"ImageListWithCVEFixed"`
} }
//nolint:tagliatelle // graphQL schema type ImageInfo struct {
type ImgResultForFixedCVE struct { RepoName string
Tags []TagInfo `json:"Tags"` LastUpdated time.Time
}
type TagInfo struct {
Name string
Timestamp time.Time
} }
//nolint:tagliatelle // graphQL schema //nolint:tagliatelle // graphQL schema
@ -470,24 +461,24 @@ func TestCVESearch(t *testing.T) {
cvid := cveResult.ImgList.CVEResultForImage.CVEList[0].ID 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, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200) So(resp.StatusCode(), ShouldEqual, 200)
var imgFixedCVEResult ImgWithFixedCVE var imgListWithCVEFixed ImgListWithCVEFixed
err = json.Unmarshal(resp.Body(), &imgFixedCVEResult) err = json.Unmarshal(resp.Body(), &imgListWithCVEFixed)
So(err, ShouldBeNil) 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, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200) So(resp.StatusCode(), ShouldEqual, 200)
err = json.Unmarshal(resp.Body(), &imgFixedCVEResult) err = json.Unmarshal(resp.Body(), &imgListWithCVEFixed)
So(err, ShouldBeNil) 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, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200) So(resp.StatusCode(), ShouldEqual, 200)
@ -504,7 +495,7 @@ func TestCVESearch(t *testing.T) {
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200) 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, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200) So(resp.StatusCode(), ShouldEqual, 200)
@ -512,7 +503,7 @@ func TestCVESearch(t *testing.T) {
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200) 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, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200) So(resp.StatusCode(), ShouldEqual, 200)
@ -520,11 +511,11 @@ func TestCVESearch(t *testing.T) {
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200) 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, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200) 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, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200) So(resp.StatusCode(), ShouldEqual, 200)
@ -532,7 +523,7 @@ func TestCVESearch(t *testing.T) {
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200) 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, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200) So(resp.StatusCode(), ShouldEqual, 200)
@ -544,7 +535,7 @@ func TestCVESearch(t *testing.T) {
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200) 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, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200) So(resp.StatusCode(), ShouldEqual, 200)
@ -585,11 +576,11 @@ func TestCVESearch(t *testing.T) {
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 422) 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, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 422) 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, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 422) So(resp.StatusCode(), ShouldEqual, 422)
@ -601,7 +592,7 @@ func TestCVESearch(t *testing.T) {
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 422) 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, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200) So(resp.StatusCode(), ShouldEqual, 200)
}) })

View file

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

View file

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

View file

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

View file

@ -9,6 +9,7 @@ import (
"strconv" "strconv"
"strings" "strings"
v1 "github.com/google/go-containerregistry/pkg/v1"
godigest "github.com/opencontainers/go-digest" godigest "github.com/opencontainers/go-digest"
"zotregistry.io/zot/pkg/log" // nolint: gci "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, func (r *queryResolver) getImageListForCVE(repoList []string, cvid string, imgStore storage.ImageStore,
trivyCtx *cveinfo.TrivyCtx, trivyCtx *cveinfo.TrivyCtx,
) ([]*gql_generated.ImgResultForCve, error) { ) ([]*gql_generated.ImageSummary, error) {
cveResult := []*gql_generated.ImgResultForCve{} cveResult := []*gql_generated.ImageSummary{}
for _, repo := range repoList { for _, repo := range repoList {
r.log.Info().Str("repo", repo).Msg("extracting list of tags available in image repo") r.log.Info().Str("repo", repo).Msg("extracting list of tags available in image repo")
name := repo imageListByCVE, err := r.cveInfo.GetImageListForCVE(repo, cvid, imgStore, trivyCtx)
tags, err := r.cveInfo.GetImageListForCVE(repo, cvid, imgStore, trivyCtx)
if err != nil { if err != nil {
r.log.Error().Err(err).Msg("error getting tag") r.log.Error().Err(err).Msg("error getting tag")
return cveResult, err return cveResult, err
} }
if len(tags) != 0 { for _, imageByCVE := range imageListByCVE {
cveResult = append(cveResult, &gql_generated.ImgResultForCve{Name: &name, Tags: tags}) cveResult = append(
cveResult,
buildImageInfo(repo, imageByCVE.Tag, imageByCVE.Digest, imageByCVE.Manifest),
)
} }
} }
return cveResult, nil return cveResult, nil
} }
func (r *queryResolver) getImageListForDigest(repoList []string, func (r *queryResolver) getImageListForDigest(repoList []string, digest string) ([]*gql_generated.ImageSummary, error) {
digest string, imgResultForDigest := []*gql_generated.ImageSummary{}
) ([]*gql_generated.ImgResultForDigest, error) {
imgResultForDigest := []*gql_generated.ImgResultForDigest{}
var errResult error var errResult error
for _, repo := range repoList { for _, repo := range repoList {
r.log.Info().Str("repo", repo).Msg("filtering list of tags in image repo by digest") 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 { if err != nil {
r.log.Error().Err(err).Msg("unable to get filtered list of image tags") r.log.Error().Err(err).Msg("unable to get filtered list of image tags")
errResult = err return []*gql_generated.ImageSummary{}, err
continue
} }
if len(tags) != 0 { for _, imageInfo := range imgTags {
name := repo imageInfo := buildImageInfo(repo, imageInfo.Tag, imageInfo.Digest, imageInfo.Manifest)
imgResultForDigest = append(imgResultForDigest, imageInfo)
imgResultForDigest = append(imgResultForDigest, &gql_generated.ImgResultForDigest{Name: &name, Tags: tags})
} }
} }
return imgResultForDigest, errResult return imgResultForDigest, errResult
} }
func (r *queryResolver) getImageListWithLatestTag(store storage.ImageStore) ([]*gql_generated.ImageInfo, error) { func (r *queryResolver) getImageListWithLatestTag(store storage.ImageStore) ([]*gql_generated.ImageSummary, error) {
results := make([]*gql_generated.ImageInfo, 0) results := make([]*gql_generated.ImageSummary, 0)
repoList, err := store.GetRepositories() repoList, err := store.GetRepositories()
if err != nil { if err != nil {
@ -167,7 +164,6 @@ func (r *queryResolver) getImageListWithLatestTag(store storage.ImageStore) ([]*
labels := imageConfig.Config.Labels labels := imageConfig.Config.Labels
// Read Description // Read Description
desc := common.GetDescription(labels) desc := common.GetDescription(labels)
// Read licenses // Read licenses
@ -179,8 +175,8 @@ func (r *queryResolver) getImageListWithLatestTag(store storage.ImageStore) ([]*
// Read categories // Read categories
categories := common.GetCategories(labels) categories := common.GetCategories(labels)
results = append(results, &gql_generated.ImageInfo{ results = append(results, &gql_generated.ImageSummary{
Name: &name, Latest: &latestTag.Name, RepoName: &name, Tag: &latestTag.Name,
Description: &desc, Licenses: &license, Vendor: &vendor, Description: &desc, Licenses: &license, Vendor: &vendor,
Labels: &categories, Size: &size, LastUpdated: &latestTag.Timestamp, Labels: &categories, Size: &size, LastUpdated: &latestTag.Timestamp,
}) })
@ -336,7 +332,7 @@ func globalSearch(repoList []string, name, tag string, olu common.OciLayoutUtils
Platforms: repoPlatforms, Platforms: repoPlatforms,
Vendors: repoVendors, Vendors: repoVendors,
Score: &index, Score: &index,
NewestTag: &lastUpdatedImageSummary, NewestImage: &lastUpdatedImageSummary,
}) })
} }
} }
@ -382,15 +378,94 @@ func calculateImageMatchingScore(artefactName string, index int, matchesTag bool
return score return score
} }
func getGraphqlCompatibleTags(fixedTags []common.TagInfo) []*gql_generated.TagInfo { func (r *queryResolver) getImageList(store storage.ImageStore, imageName string) (
finalTagList := make([]*gql_generated.TagInfo, 0) []*gql_generated.ImageSummary, error,
) {
results := make([]*gql_generated.ImageSummary, 0)
for _, tag := range fixedTags { repoList, err := store.GetRepositories()
fixTag := tag if err != nil {
r.log.Error().Err(err).Msg("extension api: error extracting repositories list")
finalTagList = append(finalTagList, return results, err
&gql_generated.TagInfo{Name: &fixTag.Name, Digest: &fixTag.Digest, Timestamp: &fixTag.Timestamp})
} }
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{ mockOlum := mocks.OciLayoutUtilsMock{
GetExpandedRepoInfoFn: func(name string) (common.RepoInfo, error) { GetExpandedRepoInfoFn: func(name string) (common.RepoInfo, error) {
return common.RepoInfo{ return common.RepoInfo{
Manifests: []common.Manifest{ Images: []common.Image{
{ {
Tag: "latest", Tag: "latest",
Layers: []common.Layer{ Layers: []common.Layer{

View file

@ -1,123 +1,91 @@
scalar Time scalar Time
type CVEResultForImage { type CVEResultForImage {
Tag: String Tag: String
CVEList: [CVE] CVEList: [CVE]
} }
type CVE { type CVE {
Id: String Id: String
Title: String Title: String
Description: String Description: String
Severity: String Severity: String
PackageList: [PackageInfo] PackageList: [PackageInfo]
} }
type PackageInfo { type PackageInfo {
Name: String Name: String
InstalledVersion: String InstalledVersion: String
FixedVersion: String 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 { type RepoInfo {
Manifests: [ManifestInfo] Images: [ImageSummary]
Summary: RepoSummary 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 # Search results in all repos/images/layers
# There will be other more structures for more detailed information # There will be other more structures for more detailed information
type GlobalSearchResult { type GlobalSearchResult {
Images: [ImageSummary] Images: [ImageSummary]
Repos: [RepoSummary] Repos: [RepoSummary]
Layers: [LayerSummary] Layers: [LayerSummary]
} }
# Brief on a specific image to be used in queries returning a list of images # Brief on a specific image to be used in queries returning a list of images
# We define an image as a pairing or a repo and a tag belonging to that repo # We define an image as a pairing or a repo and a tag belonging to that repo
type ImageSummary { type ImageSummary {
RepoName: String RepoName: String
Tag: String Tag: String
LastUpdated: Time Digest: String
IsSigned: Boolean ConfigDigest: String
Size: String LastUpdated: Time
Platform: OsArch IsSigned: Boolean
Vendor: String Size: String
Score: Int 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 # Brief on a specific repo to be used in queries returning a list of repos
type RepoSummary { type RepoSummary {
Name: String Name: String
LastUpdated: Time LastUpdated: Time
Size: String Size: String
Platforms: [OsArch] Platforms: [OsArch]
Vendors: [String] Vendors: [String]
Score: Int Score: Int
NewestTag: ImageSummary NewestImage: ImageSummary
DownloadCount: Int
StarCount: Int
IsBookmarked: Boolean
} }
# Currently the same as LayerInfo, we can refactor later # Currently the same as LayerInfo, we can refactor later
# For detailed information on the layer a ImageListForDigest call can be made # For detailed information on the layer a ImageListForDigest call can be made
type LayerSummary { type LayerSummary {
Size: String # Int64 is not supported. Size: String # Int64 is not supported.
Digest: String Digest: String
Score: Int Score: Int
} }
type OsArch { type OsArch {
Os: String Os: String
Arch: String Arch: String
} }
type Query { type Query {
CVEListForImage(image: String!) :CVEResultForImage CVEListForImage(image: String!): CVEResultForImage!
ImageListForCVE(id: String!) :[ImgResultForCVE] ImageListForCVE(id: String!): [ImageSummary!]
ImageListWithCVEFixed(id: String!, image: String!) :ImgResultForFixedCVE ImageListWithCVEFixed(id: String!, image: String!): [ImageSummary!]
ImageListForDigest(id: String!) :[ImgResultForDigest] ImageListForDigest(id: String!): [ImageSummary!]
ImageListWithLatestTag:[ImageInfo] ImageListWithLatestTag: [ImageSummary!]
ExpandedRepoInfo(repo: String!):RepoInfo ImageList(repo: String!): [ImageSummary!]
GlobalSearch(query: String!): GlobalSearchResult ExpandedRepoInfo(repo: String!): RepoInfo!
GlobalSearch(query: String!): GlobalSearchResult!
} }

View file

@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"strings" "strings"
godigest "github.com/opencontainers/go-digest"
"zotregistry.io/zot/pkg/extensions/search/common" "zotregistry.io/zot/pkg/extensions/search/common"
cveinfo "zotregistry.io/zot/pkg/extensions/search/cve" cveinfo "zotregistry.io/zot/pkg/extensions/search/cve"
"zotregistry.io/zot/pkg/extensions/search/gql_generated" "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. // ImageListForCve is the resolver for the ImageListForCVE field.
func (r *queryResolver) ImageListForCve(ctx context.Context, id string) ([]*gql_generated.ImgResultForCve, error) { func (r *queryResolver) ImageListForCve(ctx context.Context, id string) ([]*gql_generated.ImageSummary, error) {
finalCveResult := []*gql_generated.ImgResultForCve{} finalCveResult := []*gql_generated.ImageSummary{}
r.log.Info().Msg("extracting repositories") 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. // ImageListWithCVEFixed is the resolver for the ImageListWithCVEFixed field.
func (r *queryResolver) ImageListWithCVEFixed(ctx context.Context, id string, image string) (*gql_generated.ImgResultForFixedCve, error) { func (r *queryResolver) ImageListWithCVEFixed(ctx context.Context, id string, image string) ([]*gql_generated.ImageSummary, error) {
imgResultForFixedCVE := &gql_generated.ImgResultForFixedCve{} tagListForCVE := []*gql_generated.ImageSummary{}
r.log.Info().Str("image", image).Msg("extracting list of tags available in image") 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 { if err != nil {
r.log.Error().Err(err).Msg("unable to read image tags") r.log.Error().Err(err).Msg("unable to read image tags")
return imgResultForFixedCVE, err return tagListForCVE, err
} }
infectedTags := make([]common.TagInfo, 0) 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 { if len(infectedTags) != 0 {
r.log.Info().Msg("comparing fixed tags timestamp") r.log.Info().Msg("comparing fixed tags timestamp")
fixedTags := common.GetFixedTags(tagsInfo, infectedTags) tagsInfo = common.GetFixedTags(tagsInfo, infectedTags)
finalTagList = getGraphqlCompatibleTags(fixedTags)
} else { } else {
r.log.Info().Str("image", image).Str("cve-id", id).Msg("image does not contain any tag that have given cve") 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. // ImageListForDigest is the resolver for the ImageListForDigest field.
func (r *queryResolver) ImageListForDigest(ctx context.Context, id string) ([]*gql_generated.ImgResultForDigest, error) { func (r *queryResolver) ImageListForDigest(ctx context.Context, id string) ([]*gql_generated.ImageSummary, error) {
imgResultForDigest := []*gql_generated.ImgResultForDigest{} imgResultForDigest := []*gql_generated.ImageSummary{}
r.log.Info().Msg("extracting repositories") 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. // 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") 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 defaultStore := r.storeController.DefaultStore
@ -317,6 +324,43 @@ func (r *queryResolver) ImageListWithLatestTag(ctx context.Context) ([]*gql_gene
return imageList, nil 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. // ExpandedRepoInfo is the resolver for the ExpandedRepoInfo field.
func (r *queryResolver) ExpandedRepoInfo(ctx context.Context, repo string) (*gql_generated.RepoInfo, error) { func (r *queryResolver) ExpandedRepoInfo(ctx context.Context, repo string) (*gql_generated.RepoInfo, error) {
olu := common.NewBaseOciLayoutUtils(r.storeController, r.log) 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 // repos type is of common deep copy this to search
repoInfo := &gql_generated.RepoInfo{} repoInfo := &gql_generated.RepoInfo{}
manifests := make([]*gql_generated.ManifestInfo, 0) images := make([]*gql_generated.ImageSummary, 0)
summary := &gql_generated.RepoSummary{} 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 score := -1 // score not relevant for this query
summary.Score = &score summary.Score = &score
for _, manifest := range origRepoInfo.Manifests { for _, image := range origRepoInfo.Images {
tag := manifest.Tag 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 size := l.Size
digest := l.Digest digest := l.Digest
layerInfo := &gql_generated.LayerInfo{Digest: &digest, Size: &size} layerInfo := &gql_generated.LayerSummary{Digest: &digest, Size: &size}
layers = append(layers, layerInfo) layers = append(layers, layerInfo)
} }
manifestInfo.Layers = layers imageSummary.Layers = layers
manifests = append(manifests, manifestInfo) images = append(images, imageSummary)
} }
repoInfo.Summary = summary repoInfo.Summary = summary
repoInfo.Manifests = manifests repoInfo.Images = images
return repoInfo, nil return repoInfo, nil
} }