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:
parent
eb77307b63
commit
ab9a20c1ae
23 changed files with 3080 additions and 2188 deletions
|
@ -50,6 +50,7 @@ linters-settings:
|
|||
- github.com/containers/image/v5
|
||||
- github.com/opencontainers/image-spec
|
||||
- github.com/open-policy-agent/opa
|
||||
- github.com/vektah/gqlparser/v2
|
||||
- go.opentelemetry.io/otel
|
||||
- go.opentelemetry.io/otel/exporters/otlp
|
||||
- go.opentelemetry.io/otel/metric
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
@ -283,16 +284,12 @@ func (p *requestsPool) doJob(ctx context.Context, job *manifestJob) {
|
|||
|
||||
image := &imageStruct{}
|
||||
image.verbose = *job.config.verbose
|
||||
image.Name = job.imageName
|
||||
image.Tags = []tags{
|
||||
{
|
||||
Name: job.tagName,
|
||||
Digest: digest,
|
||||
Size: size,
|
||||
ConfigDigest: configDigest,
|
||||
Layers: layers,
|
||||
},
|
||||
}
|
||||
image.RepoName = job.imageName
|
||||
image.Tag = job.tagName
|
||||
image.Digest = digest
|
||||
image.Size = strconv.Itoa(int(size))
|
||||
image.ConfigDigest = configDigest
|
||||
image.Layers = layers
|
||||
|
||||
str, err := image.string(*job.config.outputFormat)
|
||||
if err != nil {
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
"zotregistry.io/zot/pkg/api"
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/api/constants"
|
||||
extConf "zotregistry.io/zot/pkg/extensions/config"
|
||||
"zotregistry.io/zot/pkg/test"
|
||||
)
|
||||
|
||||
|
@ -69,6 +70,11 @@ func TestTLSWithAuth(t *testing.T) {
|
|||
CACert: CACert,
|
||||
}
|
||||
|
||||
enable := true
|
||||
conf.Extensions = &extConf.ExtensionConfig{
|
||||
Search: &extConf.SearchConfig{Enable: &enable},
|
||||
}
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
ctlr.Config.Storage.RootDirectory = t.TempDir()
|
||||
go func() {
|
||||
|
@ -161,6 +167,11 @@ func TestTLSWithoutAuth(t *testing.T) {
|
|||
CACert: CACert,
|
||||
}
|
||||
|
||||
enable := true
|
||||
conf.Extensions = &extConf.ExtensionConfig{
|
||||
Search: &extConf.SearchConfig{Enable: &enable},
|
||||
}
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
ctlr.Config.Storage.RootDirectory = t.TempDir()
|
||||
go func() {
|
||||
|
|
|
@ -4,13 +4,18 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/briandowns/spinner"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/resty.v1"
|
||||
zotErrors "zotregistry.io/zot/errors"
|
||||
"zotregistry.io/zot/pkg/api/constants"
|
||||
)
|
||||
|
||||
func NewCveCommand(searchService SearchService) *cobra.Command {
|
||||
|
@ -69,6 +74,7 @@ func NewCveCommand(searchService SearchService) *cobra.Command {
|
|||
|
||||
spin := spinner.New(spinner.CharSets[39], spinnerDuration, spinner.WithWriter(cmd.ErrOrStderr()))
|
||||
spin.Prefix = fmt.Sprintf("Fetching from %s..", servURL)
|
||||
spin.Suffix = "\n\b"
|
||||
|
||||
verbose = false
|
||||
|
||||
|
@ -112,7 +118,7 @@ func NewCveCommand(searchService SearchService) *cobra.Command {
|
|||
|
||||
func setupCveFlags(cveCmd *cobra.Command, variables cveFlagVariables) {
|
||||
variables.searchCveParams["imageName"] = cveCmd.Flags().StringP("image", "I", "", "List CVEs by IMAGENAME[:TAG]")
|
||||
variables.searchCveParams["cvid"] = cveCmd.Flags().StringP("cve-id", "i", "", "List images affected by a CVE")
|
||||
variables.searchCveParams["cveID"] = cveCmd.Flags().StringP("cve-id", "i", "", "List images affected by a CVE")
|
||||
|
||||
cveCmd.Flags().StringVar(variables.servURL, "url", "", "Specify zot server URL if config-name is not mentioned")
|
||||
cveCmd.Flags().StringVarP(variables.user, "user", "u", "", `User Credentials of `+
|
||||
|
@ -131,8 +137,80 @@ type cveFlagVariables struct {
|
|||
fixedFlag *bool
|
||||
}
|
||||
|
||||
type field struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type schemaList struct {
|
||||
Data struct {
|
||||
Schema struct {
|
||||
QueryType struct {
|
||||
Fields []field `json:"fields"`
|
||||
} `json:"queryType"` //nolint:tagliatelle // graphQL schema
|
||||
} `json:"__schema"` //nolint:tagliatelle // graphQL schema
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
func containsGQLQuery(queryList []field, query string) bool {
|
||||
for _, q := range queryList {
|
||||
if q.Name == query {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func checkExtEndPoint(serverURL string) bool {
|
||||
client := resty.New()
|
||||
|
||||
extEndPoint, err := combineServerAndEndpointURL(serverURL, fmt.Sprintf("%s%s",
|
||||
constants.RoutePrefix, constants.ExtOciDiscoverPrefix))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// nolint: gosec
|
||||
resp, err := client.R().Get(extEndPoint)
|
||||
if err != nil || resp.StatusCode() != http.StatusOK {
|
||||
return false
|
||||
}
|
||||
|
||||
searchEndPoint, _ := combineServerAndEndpointURL(serverURL, constants.ExtSearchPrefix)
|
||||
|
||||
query := `
|
||||
{
|
||||
__schema() {
|
||||
queryType {
|
||||
fields {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
resp, err = client.R().Get(searchEndPoint + "?query=" + url.QueryEscape(query))
|
||||
if err != nil || resp.StatusCode() != http.StatusOK {
|
||||
return false
|
||||
}
|
||||
|
||||
queryList := &schemaList{}
|
||||
|
||||
_ = json.Unmarshal(resp.Body(), queryList)
|
||||
|
||||
return containsGQLQuery(queryList.Data.Schema.QueryType.Fields, "ImageList")
|
||||
}
|
||||
|
||||
func searchCve(searchConfig searchConfig) error {
|
||||
for _, searcher := range getCveSearchers() {
|
||||
var searchers []searcher
|
||||
|
||||
if checkExtEndPoint(*searchConfig.servURL) {
|
||||
searchers = getCveSearchersGQL()
|
||||
} else {
|
||||
searchers = getCveSearchers()
|
||||
}
|
||||
|
||||
for _, searcher := range searchers {
|
||||
found, err := searcher.search(searchConfig)
|
||||
if found {
|
||||
if err != nil {
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"time"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/resty.v1"
|
||||
zotErrors "zotregistry.io/zot/errors"
|
||||
"zotregistry.io/zot/pkg/api"
|
||||
|
@ -51,6 +52,7 @@ func TestSearchCVECmd(t *testing.T) {
|
|||
So(err, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test CVE no url", t, func() {
|
||||
args := []string{"cvetest", "-i", "cveIdRandom"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`)
|
||||
|
@ -136,10 +138,8 @@ func TestSearchCVECmd(t *testing.T) {
|
|||
|
||||
Convey("Test CVE url from config", t, func() {
|
||||
args := []string{"cvetest", "--image", "dummyImageName:tag"}
|
||||
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","url":"https://test-url.com","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
|
||||
cmd := NewCveCommand(new(mockService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
|
@ -162,10 +162,10 @@ func TestSearchCVECmd(t *testing.T) {
|
|||
cveCmd.SetErr(buff)
|
||||
cveCmd.SetArgs(args)
|
||||
err := cveCmd.Execute()
|
||||
So(err, ShouldBeNil)
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE dummyImageName tag DigestsA 123kB")
|
||||
So(err, ShouldBeNil)
|
||||
Convey("using shorthand", func() {
|
||||
args := []string{"cvetest", "-I", "dummyImageName", "--cve-id", "aCVEID", "--url", "someURL"}
|
||||
buff := bytes.NewBufferString("")
|
||||
|
@ -176,11 +176,10 @@ func TestSearchCVECmd(t *testing.T) {
|
|||
cveCmd.SetErr(buff)
|
||||
cveCmd.SetArgs(args)
|
||||
err := cveCmd.Execute()
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE dummyImageName tag DigestsA 123kB")
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -266,6 +265,34 @@ func TestSearchCVECmd(t *testing.T) {
|
|||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE anImage tag DigestsA 123kB")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("invalid CVE ID", func() {
|
||||
args := []string{"cvetest", "--cve-id", "invalidCVEID"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := NewCveCommand(new(mockService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(buff)
|
||||
cveCmd.SetArgs(args)
|
||||
err := cveCmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("invalid url", func() {
|
||||
args := []string{"cvetest", "--cve-id", "aCVEID", "--url", "invalidURL"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := NewCveCommand(NewSearchService())
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(buff)
|
||||
cveCmd.SetArgs(args)
|
||||
err := cveCmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldEqual, zotErrors.ErrInvalidURL)
|
||||
So(buff.String(), ShouldContainSubstring, "invalid URL format")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test fixed tags by and image name CVE ID", t, func() {
|
||||
|
@ -282,10 +309,24 @@ func TestSearchCVECmd(t *testing.T) {
|
|||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(err, ShouldBeNil)
|
||||
So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE fixedImage tag DigestsA 123kB")
|
||||
|
||||
Convey("invalid image name", func() {
|
||||
args := []string{"cvetest", "--cve-id", "aCVEID", "--image", "invalidImageName"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := NewCveCommand(NewSearchService())
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(buff)
|
||||
cveCmd.SetArgs(args)
|
||||
err := cveCmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestServerCVEResponse(t *testing.T) {
|
||||
// nolint: dupl // GQL
|
||||
func TestServerCVEResponseGQL(t *testing.T) {
|
||||
port := test.GetFreePort()
|
||||
url := test.GetBaseURL(port)
|
||||
conf := config.New()
|
||||
|
@ -351,6 +392,7 @@ func TestServerCVEResponse(t *testing.T) {
|
|||
So(err, ShouldBeNil)
|
||||
So(str, ShouldContainSubstring, "ID SEVERITY TITLE")
|
||||
So(str, ShouldContainSubstring, "CVE")
|
||||
|
||||
Convey("invalid image", func() {
|
||||
args := []string{"cvetest", "--image", "invalid:0.0.1"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
|
@ -363,6 +405,33 @@ func TestServerCVEResponse(t *testing.T) {
|
|||
err = cveCmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("invalid image name and tag", func() {
|
||||
args := []string{"cvetest", "--image", "invalid:"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := NewCveCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(buff)
|
||||
cveCmd.SetArgs(args)
|
||||
err = cveCmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("invalid output format", func() {
|
||||
args := []string{"cvetest", "--image", "zot-cve-test:0.0.1", "-o", "random"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := NewCveCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(buff)
|
||||
cveCmd.SetArgs(args)
|
||||
err = cveCmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
So(buff.String(), ShouldContainSubstring, "invalid output format")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test images by CVE ID", t, func() {
|
||||
|
@ -380,6 +449,7 @@ func TestServerCVEResponse(t *testing.T) {
|
|||
str = strings.TrimSpace(str)
|
||||
So(err, ShouldBeNil)
|
||||
So(str, ShouldEqual, "IMAGE NAME TAG DIGEST SIZE zot-cve-test 0.0.1 63a795ca 75MB")
|
||||
|
||||
Convey("invalid CVE ID", func() {
|
||||
args := []string{"cvetest", "--cve-id", "invalid"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
|
@ -396,6 +466,20 @@ func TestServerCVEResponse(t *testing.T) {
|
|||
So(err, ShouldBeNil)
|
||||
So(str, ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
|
||||
})
|
||||
|
||||
Convey("invalid output format", func() {
|
||||
args := []string{"cvetest", "--cve-id", "CVE-2019-9923", "-o", "random"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := NewCveCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(buff)
|
||||
cveCmd.SetArgs(args)
|
||||
err = cveCmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
So(buff.String(), ShouldContainSubstring, "invalid output format")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test fixed tags by and image name CVE ID", t, func() {
|
||||
|
@ -413,6 +497,7 @@ func TestServerCVEResponse(t *testing.T) {
|
|||
str = strings.TrimSpace(str)
|
||||
So(err, ShouldBeNil)
|
||||
So(str, ShouldEqual, "")
|
||||
|
||||
Convey("random cve", func() {
|
||||
args := []string{"cvetest", "--cve-id", "random", "--image", "zot-cve-test", "--fixed"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
|
@ -430,7 +515,7 @@ func TestServerCVEResponse(t *testing.T) {
|
|||
So(strings.TrimSpace(str), ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
|
||||
})
|
||||
|
||||
Convey("invalid image", func() {
|
||||
Convey("random image", func() {
|
||||
args := []string{"cvetest", "--cve-id", "CVE-2019-20807", "--image", "zot-cv-test", "--fixed"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
|
@ -446,6 +531,23 @@ func TestServerCVEResponse(t *testing.T) {
|
|||
So(err, ShouldNotBeNil)
|
||||
So(strings.TrimSpace(str), ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
|
||||
})
|
||||
|
||||
Convey("invalid image", func() {
|
||||
args := []string{"cvetest", "--cve-id", "CVE-2019-20807", "--image", "zot-cv-test:tag", "--fixed"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := NewCveCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(buff)
|
||||
cveCmd.SetArgs(args)
|
||||
err := cveCmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
str = strings.TrimSpace(str)
|
||||
So(err, ShouldNotBeNil)
|
||||
So(strings.TrimSpace(str), ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test CVE by name and CVE ID", t, func() {
|
||||
|
@ -462,6 +564,7 @@ func TestServerCVEResponse(t *testing.T) {
|
|||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(err, ShouldBeNil)
|
||||
So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE zot-cve-test 0.0.1 63a795ca 75MB")
|
||||
|
||||
Convey("invalid name and CVE ID", func() {
|
||||
args := []string{"cvetest", "--image", "test", "--cve-id", "CVE-20807"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
|
@ -477,5 +580,451 @@ func TestServerCVEResponse(t *testing.T) {
|
|||
So(err, ShouldBeNil)
|
||||
So(strings.TrimSpace(str), ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
|
||||
})
|
||||
|
||||
Convey("invalid output format", func() {
|
||||
args := []string{"cvetest", "--image", "zot-cve-test", "--cve-id", "CVE-2019-9923", "-o", "random"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := NewCveCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(buff)
|
||||
cveCmd.SetArgs(args)
|
||||
err = cveCmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
So(buff.String(), ShouldContainSubstring, "invalid output format")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestNegativeServerResponse(t *testing.T) {
|
||||
Convey("Test from real server without search endpoint", t, func() {
|
||||
port := test.GetFreePort()
|
||||
url := test.GetBaseURL(port)
|
||||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
|
||||
dir := t.TempDir()
|
||||
|
||||
err := test.CopyFiles("../../test/data/zot-cve-test", path.Join(dir, "zot-cve-test"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
conf.Storage.RootDirectory = dir
|
||||
cveConfig := &extconf.CVEConfig{
|
||||
UpdateInterval: 2,
|
||||
}
|
||||
defaultVal := false
|
||||
searchConfig := &extconf.SearchConfig{
|
||||
CVE: cveConfig,
|
||||
Enable: &defaultVal,
|
||||
}
|
||||
conf.Extensions = &extconf.ExtensionConfig{
|
||||
Search: searchConfig,
|
||||
}
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
|
||||
go func(controller *api.Controller) {
|
||||
// this blocks
|
||||
if err := controller.Run(context.Background()); err != nil {
|
||||
return
|
||||
}
|
||||
}(ctlr)
|
||||
// wait till ready
|
||||
for {
|
||||
res, err := resty.R().Get(url)
|
||||
if err == nil && res.StatusCode() == 404 {
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
time.Sleep(90 * time.Second)
|
||||
|
||||
defer func(controller *api.Controller) {
|
||||
ctx := context.Background()
|
||||
_ = controller.Server.Shutdown(ctx)
|
||||
}(ctlr)
|
||||
|
||||
Convey("Status Code Not Found", func() {
|
||||
args := []string{"cvetest", "--image", "zot-cve-test:0.0.1"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := NewCveCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(buff)
|
||||
cveCmd.SetArgs(args)
|
||||
err = cveCmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
str = strings.TrimSpace(str)
|
||||
So(err, ShouldNotBeNil)
|
||||
So(str, ShouldContainSubstring, "404 page not found")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test non-existing manifest blob", t, func() {
|
||||
port := test.GetFreePort()
|
||||
url := test.GetBaseURL(port)
|
||||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
|
||||
dir := t.TempDir()
|
||||
|
||||
err := test.CopyFiles("../../test/data/zot-cve-test", path.Join(dir, "zot-cve-test"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = os.RemoveAll(path.Join(dir, "zot-cve-test/blobs"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
conf.Storage.RootDirectory = dir
|
||||
cveConfig := &extconf.CVEConfig{
|
||||
UpdateInterval: 2,
|
||||
}
|
||||
defaultVal := true
|
||||
searchConfig := &extconf.SearchConfig{
|
||||
CVE: cveConfig,
|
||||
Enable: &defaultVal,
|
||||
}
|
||||
conf.Extensions = &extconf.ExtensionConfig{
|
||||
Search: searchConfig,
|
||||
}
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
|
||||
go func(controller *api.Controller) {
|
||||
// this blocks
|
||||
if err := controller.Run(context.Background()); err != nil {
|
||||
return
|
||||
}
|
||||
}(ctlr)
|
||||
// wait till ready
|
||||
for {
|
||||
res, err := resty.R().Get(url)
|
||||
if err == nil && res.StatusCode() == 404 {
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
time.Sleep(90 * time.Second)
|
||||
|
||||
defer func(controller *api.Controller) {
|
||||
ctx := context.Background()
|
||||
_ = controller.Server.Shutdown(ctx)
|
||||
}(ctlr)
|
||||
|
||||
args := []string{"cvetest", "--cve-id", "CVE-2019-9923", "--image", "zot-cve-test", "--fixed"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := NewCveCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(buff)
|
||||
cveCmd.SetArgs(args)
|
||||
err = cveCmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
}
|
||||
|
||||
// nolint: dupl
|
||||
func TestServerCVEResponse(t *testing.T) {
|
||||
port := test.GetFreePort()
|
||||
url := test.GetBaseURL(port)
|
||||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
|
||||
dir := t.TempDir()
|
||||
|
||||
err := test.CopyFiles("../../test/data/zot-cve-test", path.Join(dir, "zot-cve-test"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
conf.Storage.RootDirectory = dir
|
||||
cveConfig := &extconf.CVEConfig{
|
||||
UpdateInterval: 2,
|
||||
}
|
||||
defaultVal := true
|
||||
searchConfig := &extconf.SearchConfig{
|
||||
CVE: cveConfig,
|
||||
Enable: &defaultVal,
|
||||
}
|
||||
conf.Extensions = &extconf.ExtensionConfig{
|
||||
Search: searchConfig,
|
||||
}
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
|
||||
go func(controller *api.Controller) {
|
||||
// this blocks
|
||||
if err := controller.Run(context.Background()); err != nil {
|
||||
return
|
||||
}
|
||||
}(ctlr)
|
||||
// wait till ready
|
||||
for {
|
||||
res, err := resty.R().Get(url + constants.ExtSearchPrefix)
|
||||
if err == nil && res.StatusCode() == 422 {
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
time.Sleep(90 * time.Second)
|
||||
|
||||
defer func(controller *api.Controller) {
|
||||
ctx := context.Background()
|
||||
_ = controller.Server.Shutdown(ctx)
|
||||
}(ctlr)
|
||||
|
||||
Convey("Test CVE by image name", t, func() {
|
||||
args := []string{"cvetest", "--image", "zot-cve-test:0.0.1"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := MockNewCveCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(buff)
|
||||
cveCmd.SetArgs(args)
|
||||
err = cveCmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
str = strings.TrimSpace(str)
|
||||
So(err, ShouldBeNil)
|
||||
So(str, ShouldContainSubstring, "ID SEVERITY TITLE")
|
||||
So(str, ShouldContainSubstring, "CVE")
|
||||
Convey("invalid image", func() {
|
||||
args := []string{"cvetest", "--image", "invalid:0.0.1"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := MockNewCveCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(buff)
|
||||
cveCmd.SetArgs(args)
|
||||
err = cveCmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test images by CVE ID", t, func() {
|
||||
args := []string{"cvetest", "--cve-id", "CVE-2019-9923"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := MockNewCveCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(buff)
|
||||
cveCmd.SetArgs(args)
|
||||
err := cveCmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
str = strings.TrimSpace(str)
|
||||
So(err, ShouldBeNil)
|
||||
So(str, ShouldEqual, "IMAGE NAME TAG DIGEST SIZE zot-cve-test 0.0.1 63a795ca 75MB")
|
||||
Convey("invalid CVE ID", func() {
|
||||
args := []string{"cvetest", "--cve-id", "invalid"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := MockNewCveCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(buff)
|
||||
cveCmd.SetArgs(args)
|
||||
err := cveCmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
str = strings.TrimSpace(str)
|
||||
So(err, ShouldBeNil)
|
||||
So(str, ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test fixed tags by and image name CVE ID", t, func() {
|
||||
args := []string{"cvetest", "--cve-id", "CVE-2019-9923", "--image", "zot-cve-test", "--fixed"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := MockNewCveCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(buff)
|
||||
cveCmd.SetArgs(args)
|
||||
err := cveCmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
str = strings.TrimSpace(str)
|
||||
So(err, ShouldBeNil)
|
||||
So(str, ShouldEqual, "")
|
||||
Convey("random cve", func() {
|
||||
args := []string{"cvetest", "--cve-id", "random", "--image", "zot-cve-test", "--fixed"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := MockNewCveCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(buff)
|
||||
cveCmd.SetArgs(args)
|
||||
err := cveCmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
str = strings.TrimSpace(str)
|
||||
So(err, ShouldBeNil)
|
||||
So(strings.TrimSpace(str), ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
|
||||
})
|
||||
|
||||
Convey("invalid image", func() {
|
||||
args := []string{"cvetest", "--cve-id", "CVE-2019-20807", "--image", "zot-cv-test", "--fixed"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := MockNewCveCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(buff)
|
||||
cveCmd.SetArgs(args)
|
||||
err := cveCmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
str = strings.TrimSpace(str)
|
||||
So(err, ShouldNotBeNil)
|
||||
So(strings.TrimSpace(str), ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test CVE by name and CVE ID", t, func() {
|
||||
args := []string{"cvetest", "--image", "zot-cve-test", "--cve-id", "CVE-2019-9923"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := MockNewCveCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(buff)
|
||||
cveCmd.SetArgs(args)
|
||||
err := cveCmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(err, ShouldBeNil)
|
||||
So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE zot-cve-test 0.0.1 63a795ca 75MB")
|
||||
Convey("invalid name and CVE ID", func() {
|
||||
args := []string{"cvetest", "--image", "test", "--cve-id", "CVE-20807"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := MockNewCveCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(buff)
|
||||
cveCmd.SetArgs(args)
|
||||
err := cveCmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(err, ShouldBeNil)
|
||||
So(strings.TrimSpace(str), ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func MockNewCveCommand(searchService SearchService) *cobra.Command {
|
||||
searchCveParams := make(map[string]*string)
|
||||
|
||||
var servURL, user, outputFormat string
|
||||
|
||||
var verifyTLS, fixedFlag, verbose bool
|
||||
|
||||
cveCmd := &cobra.Command{
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
configPath := path.Join(home + "/.zot")
|
||||
if len(args) > 0 {
|
||||
urlFromConfig, err := getConfigValue(configPath, args[0], "url")
|
||||
if err != nil {
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if urlFromConfig == "" {
|
||||
return zotErrors.ErrNoURLProvided
|
||||
}
|
||||
|
||||
servURL = urlFromConfig
|
||||
} else {
|
||||
return zotErrors.ErrNoURLProvided
|
||||
}
|
||||
|
||||
if len(args) > 0 {
|
||||
var err error
|
||||
|
||||
verifyTLS, err = parseBooleanConfig(configPath, args[0], verifyTLSConfig)
|
||||
if err != nil {
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
verbose = false
|
||||
|
||||
searchConfig := searchConfig{
|
||||
params: searchCveParams,
|
||||
searchService: searchService,
|
||||
servURL: &servURL,
|
||||
user: &user,
|
||||
outputFormat: &outputFormat,
|
||||
fixedFlag: &fixedFlag,
|
||||
verifyTLS: &verifyTLS,
|
||||
verbose: &verbose,
|
||||
resultWriter: cmd.OutOrStdout(),
|
||||
}
|
||||
|
||||
err = MockSearchCve(searchConfig)
|
||||
|
||||
if err != nil {
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
vars := cveFlagVariables{
|
||||
searchCveParams: searchCveParams,
|
||||
servURL: &servURL,
|
||||
user: &user,
|
||||
outputFormat: &outputFormat,
|
||||
fixedFlag: &fixedFlag,
|
||||
}
|
||||
|
||||
setupCveFlags(cveCmd, vars)
|
||||
|
||||
return cveCmd
|
||||
}
|
||||
|
||||
func MockSearchCve(searchConfig searchConfig) error {
|
||||
searchers := getCveSearchers()
|
||||
|
||||
for _, searcher := range searchers {
|
||||
found, err := searcher.search(searchConfig)
|
||||
if found {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return zotErrors.ErrInvalidFlagsCombination
|
||||
}
|
||||
|
|
|
@ -129,7 +129,15 @@ func setupImageFlags(imageCmd *cobra.Command, searchImageParams map[string]*stri
|
|||
}
|
||||
|
||||
func searchImage(searchConfig searchConfig) error {
|
||||
for _, searcher := range getImageSearchers() {
|
||||
var searchers []searcher
|
||||
|
||||
if checkExtEndPoint(*searchConfig.servURL) {
|
||||
searchers = getImageSearchersGQL()
|
||||
} else {
|
||||
searchers = getImageSearchers()
|
||||
}
|
||||
|
||||
for _, searcher := range searchers {
|
||||
found, err := searcher.search(searchConfig)
|
||||
if found {
|
||||
if err != nil {
|
||||
|
|
|
@ -21,10 +21,12 @@ import (
|
|||
godigest "github.com/opencontainers/go-digest"
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/resty.v1"
|
||||
zotErrors "zotregistry.io/zot/errors"
|
||||
"zotregistry.io/zot/pkg/api"
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/api/constants"
|
||||
extconf "zotregistry.io/zot/pkg/extensions/config"
|
||||
"zotregistry.io/zot/pkg/test"
|
||||
)
|
||||
|
@ -56,6 +58,7 @@ func TestSearchImageCmd(t *testing.T) {
|
|||
So(err, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test image no url", t, func() {
|
||||
args := []string{"imagetest", "--name", "dummyIdRandom"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`)
|
||||
|
@ -126,6 +129,7 @@ func TestSearchImageCmd(t *testing.T) {
|
|||
So(err, ShouldEqual, zotErrors.ErrInvalidURL)
|
||||
So(buff.String(), ShouldContainSubstring, "invalid URL format")
|
||||
})
|
||||
|
||||
Convey("Test image invalid url port", t, func() {
|
||||
args := []string{"imagetest", "--name", "dummyImageName", "--url", "http://localhost:99999"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`)
|
||||
|
@ -153,6 +157,7 @@ func TestSearchImageCmd(t *testing.T) {
|
|||
So(buff.String(), ShouldContainSubstring, "invalid port")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test image unreachable", t, func() {
|
||||
args := []string{"imagetest", "--name", "dummyImageName", "--url", "http://localhost:9999"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`)
|
||||
|
@ -168,10 +173,8 @@ func TestSearchImageCmd(t *testing.T) {
|
|||
|
||||
Convey("Test image url from config", t, func() {
|
||||
args := []string{"imagetest", "--name", "dummyImageName"}
|
||||
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
|
||||
cmd := NewImageCommand(new(mockService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
|
@ -215,15 +218,44 @@ func TestSearchImageCmd(t *testing.T) {
|
|||
So(err, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test image by digest", t, func() {
|
||||
args := []string{"imagetest", "--digest", "DigestsA", "--url", "someUrlImage"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
imageCmd := NewImageCommand(new(mockService))
|
||||
buff := bytes.NewBufferString("")
|
||||
imageCmd.SetOut(buff)
|
||||
imageCmd.SetErr(buff)
|
||||
imageCmd.SetArgs(args)
|
||||
err := imageCmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE anImage tag DigestsA 123kB")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("invalid URL format", func() {
|
||||
args := []string{"imagetest", "--digest", "digest", "--url", "invalidURL"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
imageCmd := NewImageCommand(NewSearchService())
|
||||
buff := bytes.NewBufferString("")
|
||||
imageCmd.SetOut(buff)
|
||||
imageCmd.SetErr(buff)
|
||||
imageCmd.SetArgs(args)
|
||||
err := imageCmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldEqual, zotErrors.ErrInvalidURL)
|
||||
So(buff.String(), ShouldContainSubstring, "invalid URL format")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestListRepos(t *testing.T) {
|
||||
Convey("Test listing repositories", t, func() {
|
||||
args := []string{"config-test"}
|
||||
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"config-test","url":"https://test-url.com","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
|
||||
cmd := NewRepoCommand(new(mockService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
|
@ -264,11 +296,9 @@ func TestListRepos(t *testing.T) {
|
|||
|
||||
Convey("Test listing repositories error", t, func() {
|
||||
args := []string{"config-test"}
|
||||
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"config-test",
|
||||
"url":"https://invalid.invalid","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
|
||||
cmd := NewRepoCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
|
@ -280,10 +310,8 @@ func TestListRepos(t *testing.T) {
|
|||
|
||||
Convey("Test unable to get config value", t, func() {
|
||||
args := []string{"config-test-inexistent"}
|
||||
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"config-test","url":"https://test-url.com","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
|
||||
cmd := NewRepoCommand(new(mockService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
|
@ -295,10 +323,8 @@ func TestListRepos(t *testing.T) {
|
|||
|
||||
Convey("Test error - no url provided", t, func() {
|
||||
args := []string{"config-test"}
|
||||
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"config-test","url":"","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
|
||||
cmd := NewRepoCommand(new(mockService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
|
@ -310,10 +336,8 @@ func TestListRepos(t *testing.T) {
|
|||
|
||||
Convey("Test error - no args provided", t, func() {
|
||||
var args []string
|
||||
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"config-test","url":"","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
|
||||
cmd := NewRepoCommand(new(mockService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
|
@ -325,11 +349,9 @@ func TestListRepos(t *testing.T) {
|
|||
|
||||
Convey("Test error - spinner config invalid", t, func() {
|
||||
args := []string{"config-test"}
|
||||
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"config-test",
|
||||
"url":"https://test-url.com","showspinner":invalid}]}`)
|
||||
defer os.Remove(configPath)
|
||||
|
||||
cmd := NewRepoCommand(new(mockService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
|
@ -341,11 +363,9 @@ func TestListRepos(t *testing.T) {
|
|||
|
||||
Convey("Test error - verifyTLSConfig fails", t, func() {
|
||||
args := []string{"config-test"}
|
||||
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"config-test",
|
||||
"verify-tls":"invalid", "url":"https://test-url.com","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
|
||||
cmd := NewRepoCommand(new(mockService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
|
@ -359,10 +379,8 @@ func TestListRepos(t *testing.T) {
|
|||
func TestOutputFormat(t *testing.T) {
|
||||
Convey("Test text", t, func() {
|
||||
args := []string{"imagetest", "--name", "dummyImageName", "-o", "text"}
|
||||
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
|
||||
cmd := NewImageCommand(new(mockService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
|
@ -375,12 +393,12 @@ func TestOutputFormat(t *testing.T) {
|
|||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
// get image config functia
|
||||
|
||||
Convey("Test json", t, func() {
|
||||
args := []string{"imagetest", "--name", "dummyImageName", "-o", "json"}
|
||||
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
|
||||
cmd := NewImageCommand(new(mockService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
|
@ -389,17 +407,15 @@ func TestOutputFormat(t *testing.T) {
|
|||
err := cmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(strings.TrimSpace(str), ShouldEqual, `{ "name": "dummyImageName", "tags": [ { "name":`+
|
||||
` "tag", "size": 123445, "digest": "DigestsAreReallyLong", "configDigest": "", "layerDigests": null } ] }`)
|
||||
So(strings.TrimSpace(str), ShouldEqual, `{ "repoName": "dummyImageName", "tag": "tag", `+
|
||||
`"configDigest": "", "digest": "DigestsAreReallyLong", "layers": null, "size": "123445" }`)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Test yaml", t, func() {
|
||||
args := []string{"imagetest", "--name", "dummyImageName", "-o", "yaml"}
|
||||
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
|
||||
cmd := NewImageCommand(new(mockService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
|
@ -408,16 +424,21 @@ func TestOutputFormat(t *testing.T) {
|
|||
err := cmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(strings.TrimSpace(str), ShouldEqual, `name: dummyImageName tags: -`+
|
||||
` name: tag size: 123445 digest: DigestsAreReallyLong configdigest: "" layers: []`)
|
||||
So(
|
||||
strings.TrimSpace(str),
|
||||
ShouldEqual,
|
||||
`reponame: dummyImageName tag: tag configdigest: "" `+
|
||||
`digest: DigestsAreReallyLong layers: [] size: "123445"`,
|
||||
)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Test yml", func() {
|
||||
args := []string{"imagetest", "--name", "dummyImageName", "-o", "yml"}
|
||||
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`)
|
||||
configPath := makeConfigFile(
|
||||
`{"configs":[{"_name":"imagetest",` +
|
||||
`"url":"https://test-url.com","showspinner":false}]}`,
|
||||
)
|
||||
defer os.Remove(configPath)
|
||||
|
||||
cmd := NewImageCommand(new(mockService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
|
@ -426,18 +447,20 @@ func TestOutputFormat(t *testing.T) {
|
|||
err := cmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(strings.TrimSpace(str), ShouldEqual, `name: dummyImageName tags: -`+
|
||||
` name: tag size: 123445 digest: DigestsAreReallyLong configdigest: "" layers: []`)
|
||||
So(
|
||||
strings.TrimSpace(str),
|
||||
ShouldEqual,
|
||||
`reponame: dummyImageName tag: tag configdigest: "" `+
|
||||
`digest: DigestsAreReallyLong layers: [] size: "123445"`,
|
||||
)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test invalid", t, func() {
|
||||
args := []string{"imagetest", "--name", "dummyImageName", "-o", "random"}
|
||||
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
|
||||
cmd := NewImageCommand(new(mockService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
|
@ -449,7 +472,7 @@ func TestOutputFormat(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestServerResponse(t *testing.T) {
|
||||
func TestServerResponseGQL(t *testing.T) {
|
||||
Convey("Test from real server", t, func() {
|
||||
port := test.GetFreePort()
|
||||
url := test.GetBaseURL(port)
|
||||
|
@ -491,7 +514,6 @@ func TestServerResponse(t *testing.T) {
|
|||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewImageCommand(new(searchService))
|
||||
// buff := bytes.NewBufferString("")
|
||||
buff := &bytes.Buffer{}
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(buff)
|
||||
|
@ -504,6 +526,19 @@ func TestServerResponse(t *testing.T) {
|
|||
So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
|
||||
So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 15B")
|
||||
So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 15B")
|
||||
Convey("Test all images invalid output format", func() {
|
||||
args := []string{"imagetest", "-o", "random"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewImageCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(buff)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
So(buff.String(), ShouldContainSubstring, "invalid output format")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test all images verbose", func() {
|
||||
|
@ -567,6 +602,20 @@ func TestServerResponse(t *testing.T) {
|
|||
So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 15B")
|
||||
So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 15B")
|
||||
})
|
||||
|
||||
Convey("invalid output format", func() {
|
||||
args := []string{"imagetest", "--name", "repo7", "-o", "random"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewImageCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(buff)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
So(buff.String(), ShouldContainSubstring, "invalid output format")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test image by digest", func() {
|
||||
|
@ -590,6 +639,7 @@ func TestServerResponse(t *testing.T) {
|
|||
So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
|
||||
So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 15B")
|
||||
So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 15B")
|
||||
|
||||
Convey("with shorthand", func() {
|
||||
args := []string{"imagetest", "-d", "883fc0c5"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
|
||||
|
@ -608,10 +658,23 @@ func TestServerResponse(t *testing.T) {
|
|||
So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 15B")
|
||||
So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 15B")
|
||||
})
|
||||
|
||||
Convey("nonexistent digest", func() {
|
||||
args := []string{"imagetest", "--digest", "d1g35t"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewImageCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(buff)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldBeNil)
|
||||
So(len(buff.String()), ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("Test image by name invalid name", func() {
|
||||
args := []string{"imagetest", "--name", "repo777"}
|
||||
Convey("invalid output format", func() {
|
||||
args := []string{"imagetest", "--digest", "883fc0c5", "-o", "random"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewImageCommand(new(searchService))
|
||||
|
@ -621,8 +684,22 @@ func TestServerResponse(t *testing.T) {
|
|||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
actual := buff.String()
|
||||
So(actual, ShouldContainSubstring, "unknown")
|
||||
So(buff.String(), ShouldContainSubstring, "invalid output format")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test image by name nonexistent name", func() {
|
||||
args := []string{"imagetest", "--name", "repo777"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewImageCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(buff)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldBeNil)
|
||||
So(len(buff.String()), ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("Test list repos error", func() {
|
||||
|
@ -648,6 +725,361 @@ func TestServerResponse(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestServerResponse(t *testing.T) {
|
||||
Convey("Test from real server", t, func() {
|
||||
port := test.GetFreePort()
|
||||
url := test.GetBaseURL(port)
|
||||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
defaultVal := true
|
||||
conf.Extensions = &extconf.ExtensionConfig{
|
||||
Search: &extconf.SearchConfig{Enable: &defaultVal},
|
||||
}
|
||||
ctlr := api.NewController(conf)
|
||||
ctlr.Config.Storage.RootDirectory = t.TempDir()
|
||||
go func(controller *api.Controller) {
|
||||
// this blocks
|
||||
if err := controller.Run(context.Background()); err != nil {
|
||||
return
|
||||
}
|
||||
}(ctlr)
|
||||
// wait till ready
|
||||
for {
|
||||
_, err := resty.R().Get(url)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
defer func(controller *api.Controller) {
|
||||
ctx := context.Background()
|
||||
_ = controller.Server.Shutdown(ctx)
|
||||
}(ctlr)
|
||||
|
||||
err := uploadManifest(url)
|
||||
t.Logf("%s", ctlr.Config.Storage.RootDirectory)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Test all images", func() {
|
||||
t.Logf("%s", ctlr.Config.Storage.RootDirectory)
|
||||
args := []string{"imagetest"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cmd := MockNewImageCommand(new(searchService))
|
||||
buff := &bytes.Buffer{}
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(buff)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldBeNil)
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
actual := strings.TrimSpace(str)
|
||||
So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
|
||||
So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 15B")
|
||||
So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 15B")
|
||||
})
|
||||
|
||||
Convey("Test all images verbose", func() {
|
||||
args := []string{"imagetest", "--verbose"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cmd := MockNewImageCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(buff)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldBeNil)
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
actual := strings.TrimSpace(str)
|
||||
// Actual cli output should be something similar to (order of images may differ):
|
||||
// IMAGE NAME TAG DIGEST CONFIG LAYERS SIZE
|
||||
// repo7 test:2.0 a0ca253b b8781e88 15B
|
||||
// b8781e88 15B
|
||||
// repo7 test:1.0 a0ca253b b8781e88 15B
|
||||
// b8781e88 15B
|
||||
So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST CONFIG LAYERS SIZE")
|
||||
So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 3a1d2d0c 15B b8781e88 15B")
|
||||
So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 3a1d2d0c 15B b8781e88 15B")
|
||||
})
|
||||
|
||||
Convey("Test image by name", func() {
|
||||
args := []string{"imagetest", "--name", "repo7"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cmd := MockNewImageCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(buff)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldBeNil)
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
actual := strings.TrimSpace(str)
|
||||
So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
|
||||
So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 15B")
|
||||
So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 15B")
|
||||
})
|
||||
|
||||
Convey("Test image by digest", func() {
|
||||
args := []string{"imagetest", "--digest", "883fc0c5"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cmd := MockNewImageCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(buff)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldBeNil)
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
actual := strings.TrimSpace(str)
|
||||
// Actual cli output should be something similar to (order of images may differ):
|
||||
// IMAGE NAME TAG DIGEST SIZE
|
||||
// repo7 test:2.0 a0ca253b 15B
|
||||
// repo7 test:1.0 a0ca253b 15B
|
||||
So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
|
||||
So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 15B")
|
||||
So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 15B")
|
||||
|
||||
Convey("nonexistent digest", func() {
|
||||
args := []string{"imagetest", "--digest", "d1g35t"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cmd := MockNewImageCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(buff)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldBeNil)
|
||||
So(len(buff.String()), ShouldEqual, 0)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test image by name nonexistent name", func() {
|
||||
args := []string{"imagetest", "--name", "repo777"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cmd := MockNewImageCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(buff)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
actual := buff.String()
|
||||
So(actual, ShouldContainSubstring, "unknown")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestServerResponseGQLWithoutPermissions(t *testing.T) {
|
||||
port := test.GetFreePort()
|
||||
url := test.GetBaseURL(port)
|
||||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
|
||||
dir := t.TempDir()
|
||||
|
||||
err := test.CopyFiles("../../test/data/zot-test", path.Join(dir, "zot-test"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = os.Chmod(path.Join(dir, "zot-test", "blobs"), 0o000)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
conf.Storage.RootDirectory = dir
|
||||
cveConfig := &extconf.CVEConfig{
|
||||
UpdateInterval: 2,
|
||||
}
|
||||
defaultVal := true
|
||||
searchConfig := &extconf.SearchConfig{
|
||||
CVE: cveConfig,
|
||||
Enable: &defaultVal,
|
||||
}
|
||||
conf.Extensions = &extconf.ExtensionConfig{
|
||||
Search: searchConfig,
|
||||
}
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
|
||||
go func(controller *api.Controller) {
|
||||
// this blocks
|
||||
if err := controller.Run(context.Background()); err != nil {
|
||||
return
|
||||
}
|
||||
}(ctlr)
|
||||
// wait till ready
|
||||
for {
|
||||
res, err := resty.R().Get(url + constants.ExtSearchPrefix)
|
||||
if err == nil && res.StatusCode() == 422 {
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
time.Sleep(90 * time.Second)
|
||||
|
||||
defer func(controller *api.Controller) {
|
||||
err = os.Chmod(path.Join(dir, "zot-test", "blobs"), 0o777)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
_ = controller.Server.Shutdown(ctx)
|
||||
}(ctlr)
|
||||
|
||||
Convey("Test all images", t, func() {
|
||||
args := []string{"imagetest"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cveCmd := NewImageCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cveCmd.SetOut(buff)
|
||||
cveCmd.SetErr(buff)
|
||||
cveCmd.SetArgs(args)
|
||||
err = cveCmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("Test all images verbose", t, func() {
|
||||
args := []string{"imagetest", "--verbose"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewImageCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(buff)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("Test image by name", t, func() {
|
||||
args := []string{"imagetest", "--name", "zot-test"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewImageCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(buff)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("Test image by digest", t, func() {
|
||||
args := []string{"imagetest", "--digest", "2bacca16"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewImageCommand(new(searchService))
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(buff)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
}
|
||||
|
||||
func MockNewImageCommand(searchService SearchService) *cobra.Command {
|
||||
searchImageParams := make(map[string]*string)
|
||||
|
||||
var servURL, user, outputFormat string
|
||||
|
||||
var verifyTLS, verbose bool
|
||||
|
||||
imageCmd := &cobra.Command{
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
configPath := path.Join(home + "/.zot")
|
||||
if len(args) > 0 {
|
||||
urlFromConfig, err := getConfigValue(configPath, args[0], "url")
|
||||
if err != nil {
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if urlFromConfig == "" {
|
||||
return zotErrors.ErrNoURLProvided
|
||||
}
|
||||
|
||||
servURL = urlFromConfig
|
||||
} else {
|
||||
return zotErrors.ErrNoURLProvided
|
||||
}
|
||||
|
||||
if len(args) > 0 {
|
||||
var err error
|
||||
|
||||
verifyTLS, err = parseBooleanConfig(configPath, args[0], verifyTLSConfig)
|
||||
if err != nil {
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
searchConfig := searchConfig{
|
||||
params: searchImageParams,
|
||||
searchService: searchService,
|
||||
servURL: &servURL,
|
||||
user: &user,
|
||||
outputFormat: &outputFormat,
|
||||
verbose: &verbose,
|
||||
verifyTLS: &verifyTLS,
|
||||
resultWriter: cmd.OutOrStdout(),
|
||||
}
|
||||
|
||||
err = MockSearchImage(searchConfig)
|
||||
|
||||
if err != nil {
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
setupImageFlags(imageCmd, searchImageParams, &servURL, &user, &outputFormat, &verbose)
|
||||
imageCmd.SetUsageTemplate(imageCmd.UsageTemplate() + usageFooter)
|
||||
|
||||
return imageCmd
|
||||
}
|
||||
|
||||
func MockSearchImage(searchConfig searchConfig) error {
|
||||
searchers := getImageSearchers()
|
||||
|
||||
for _, searcher := range searchers {
|
||||
found, err := searcher.search(searchConfig)
|
||||
if found {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return zotErrors.ErrInvalidFlagsCombination
|
||||
}
|
||||
|
||||
func uploadManifest(url string) error {
|
||||
// create a blob/layer
|
||||
resp, _ := resty.R().Post(url + "/v2/repo7/blobs/uploads/")
|
||||
|
@ -741,6 +1173,131 @@ func (service mockService) getRepos(ctx context.Context, config searchConfig, us
|
|||
channel <- stringResult{"", nil}
|
||||
}
|
||||
|
||||
func (service mockService) getImagesGQL(ctx context.Context, config searchConfig, username, password string,
|
||||
imageName string,
|
||||
) (*imageListStructGQL, error) {
|
||||
imageListGQLResponse := &imageListStructGQL{}
|
||||
imageListGQLResponse.Data.ImageList = []imageStruct{
|
||||
{
|
||||
RepoName: "dummyImageName",
|
||||
Tag: "tag",
|
||||
Digest: "DigestsAreReallyLong",
|
||||
Size: "123445",
|
||||
},
|
||||
}
|
||||
|
||||
return imageListGQLResponse, nil
|
||||
}
|
||||
|
||||
func (service mockService) getImagesByDigestGQL(ctx context.Context, config searchConfig, username, password string,
|
||||
digest string,
|
||||
) (*imageListStructForDigestGQL, error) {
|
||||
imageListGQLResponse := &imageListStructForDigestGQL{}
|
||||
imageListGQLResponse.Data.ImageList = []imageStruct{
|
||||
{
|
||||
RepoName: "randomimageName",
|
||||
Tag: "tag",
|
||||
Digest: "DigestsAreReallyLong",
|
||||
Size: "123445",
|
||||
},
|
||||
}
|
||||
|
||||
return imageListGQLResponse, nil
|
||||
}
|
||||
|
||||
func (service mockService) getImagesByCveIDGQL(ctx context.Context, config searchConfig, username, password string,
|
||||
digest string,
|
||||
) (*imagesForCve, error) {
|
||||
imagesForCve := &imagesForCve{
|
||||
Errors: nil,
|
||||
Data: struct {
|
||||
ImageList []imageStruct `json:"ImageListForCVE"` // nolint:tagliatelle
|
||||
}{},
|
||||
}
|
||||
|
||||
imagesForCve.Errors = nil
|
||||
|
||||
mockedImage := service.getMockedImageByName("anImage")
|
||||
imagesForCve.Data.ImageList = []imageStruct{mockedImage}
|
||||
|
||||
return imagesForCve, nil
|
||||
}
|
||||
|
||||
func (service mockService) getTagsForCVEGQL(ctx context.Context, config searchConfig, username, password,
|
||||
imageName, cveID string,
|
||||
) (*imagesForCve, error) {
|
||||
images := &imagesForCve{
|
||||
Errors: nil,
|
||||
Data: struct {
|
||||
ImageList []imageStruct `json:"ImageListForCVE"` //nolint:tagliatelle // graphQL schema
|
||||
}{},
|
||||
}
|
||||
|
||||
images.Errors = nil
|
||||
|
||||
mockedImage := service.getMockedImageByName(imageName)
|
||||
images.Data.ImageList = []imageStruct{mockedImage}
|
||||
|
||||
return images, nil
|
||||
}
|
||||
|
||||
func (service mockService) getFixedTagsForCVEGQL(ctx context.Context, config searchConfig, username, password,
|
||||
imageName, cveID string,
|
||||
) (*fixedTags, error) {
|
||||
fixedTags := &fixedTags{
|
||||
Errors: nil,
|
||||
Data: struct {
|
||||
ImageList []imageStruct `json:"ImageListWithCVEFixed"` //nolint:tagliatelle // graphQL schema
|
||||
}{},
|
||||
}
|
||||
|
||||
fixedTags.Errors = nil
|
||||
|
||||
mockedImage := service.getMockedImageByName(imageName)
|
||||
fixedTags.Data.ImageList = []imageStruct{mockedImage}
|
||||
|
||||
return fixedTags, nil
|
||||
}
|
||||
|
||||
func (service mockService) getCveByImageGQL(ctx context.Context, config searchConfig, username, password,
|
||||
imageName string,
|
||||
) (*cveResult, error) {
|
||||
cveRes := &cveResult{}
|
||||
cveRes.Data = cveData{
|
||||
CVEListForImage: cveListForImage{
|
||||
Tag: imageName,
|
||||
CVEList: []cve{
|
||||
{
|
||||
ID: "dummyCVEID",
|
||||
Description: "Description of the CVE",
|
||||
Title: "Title of that CVE",
|
||||
Severity: "HIGH",
|
||||
PackageList: []packageList{
|
||||
{
|
||||
Name: "packagename",
|
||||
FixedVersion: "fixedver",
|
||||
InstalledVersion: "installedver",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return cveRes, nil
|
||||
}
|
||||
|
||||
// nolint: goconst
|
||||
func (service mockService) getMockedImageByName(imageName string) imageStruct {
|
||||
image := imageStruct{}
|
||||
image.RepoName = imageName
|
||||
image.Tag = "tag"
|
||||
image.Digest = "DigestsAreReallyLong"
|
||||
image.Size = "123445"
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
func (service mockService) getAllImages(ctx context.Context, config searchConfig, username, password string,
|
||||
channel chan stringResult, wtgrp *sync.WaitGroup,
|
||||
) {
|
||||
|
@ -748,14 +1305,10 @@ func (service mockService) getAllImages(ctx context.Context, config searchConfig
|
|||
defer close(channel)
|
||||
|
||||
image := &imageStruct{}
|
||||
image.Name = "randomimageName"
|
||||
image.Tags = []tags{
|
||||
{
|
||||
Name: "tag",
|
||||
Digest: "DigestsAreReallyLong",
|
||||
Size: 123445,
|
||||
},
|
||||
}
|
||||
image.RepoName = "randomimageName"
|
||||
image.Tag = "tag"
|
||||
image.Digest = "DigestsAreReallyLong"
|
||||
image.Size = "123445"
|
||||
|
||||
str, err := image.string(*config.outputFormat)
|
||||
if err != nil {
|
||||
|
@ -774,14 +1327,10 @@ func (service mockService) getImageByName(ctx context.Context, config searchConf
|
|||
defer close(channel)
|
||||
|
||||
image := &imageStruct{}
|
||||
image.Name = imageName
|
||||
image.Tags = []tags{
|
||||
{
|
||||
Name: "tag",
|
||||
Digest: "DigestsAreReallyLong",
|
||||
Size: 123445,
|
||||
},
|
||||
}
|
||||
image.RepoName = imageName
|
||||
image.Tag = "tag"
|
||||
image.Digest = "DigestsAreReallyLong"
|
||||
image.Size = "123445"
|
||||
|
||||
str, err := image.string(*config.outputFormat)
|
||||
if err != nil {
|
||||
|
@ -831,6 +1380,18 @@ func (service mockService) getCveByImage(ctx context.Context, config searchConfi
|
|||
rch <- stringResult{str, nil}
|
||||
}
|
||||
|
||||
func (service mockService) getFixedTagsForCVE(ctx context.Context, config searchConfig,
|
||||
username, password, imageName, cvid string, rch chan stringResult, wtgrp *sync.WaitGroup,
|
||||
) {
|
||||
service.getImageByName(ctx, config, username, password, imageName, rch, wtgrp)
|
||||
}
|
||||
|
||||
func (service mockService) getImageByNameAndCVEID(ctx context.Context, config searchConfig, username,
|
||||
password, imageName, cvid string, rch chan stringResult, wtgrp *sync.WaitGroup,
|
||||
) {
|
||||
service.getImageByName(ctx, config, username, password, imageName, rch, wtgrp)
|
||||
}
|
||||
|
||||
func (service mockService) getImagesByCveID(ctx context.Context, config searchConfig, username, password, cvid string,
|
||||
rch chan stringResult, wtgrp *sync.WaitGroup,
|
||||
) {
|
||||
|
@ -843,18 +1404,6 @@ func (service mockService) getImagesByDigest(ctx context.Context, config searchC
|
|||
service.getImageByName(ctx, config, username, password, "anImage", rch, wtgrp)
|
||||
}
|
||||
|
||||
func (service mockService) getImageByNameAndCVEID(ctx context.Context, config searchConfig, username,
|
||||
password, imageName, cvid string, rch chan stringResult, wtgrp *sync.WaitGroup,
|
||||
) {
|
||||
service.getImageByName(ctx, config, username, password, imageName, rch, wtgrp)
|
||||
}
|
||||
|
||||
func (service mockService) getFixedTagsForCVE(ctx context.Context, config searchConfig,
|
||||
username, password, imageName, cvid string, rch chan stringResult, wtgrp *sync.WaitGroup,
|
||||
) {
|
||||
service.getImageByName(ctx, config, username, password, imageName, rch, wtgrp)
|
||||
}
|
||||
|
||||
func makeConfigFile(content string) string {
|
||||
os.Setenv("HOME", os.TempDir())
|
||||
|
||||
|
|
|
@ -37,6 +37,27 @@ func getCveSearchers() []searcher {
|
|||
return searchers
|
||||
}
|
||||
|
||||
func getImageSearchersGQL() []searcher {
|
||||
searchers := []searcher{
|
||||
new(allImagesSearcherGQL),
|
||||
new(imageByNameSearcherGQL),
|
||||
new(imagesByDigestSearcherGQL),
|
||||
}
|
||||
|
||||
return searchers
|
||||
}
|
||||
|
||||
func getCveSearchersGQL() []searcher {
|
||||
searchers := []searcher{
|
||||
new(cveByImageSearcherGQL),
|
||||
new(imagesByCVEIDSearcherGQL),
|
||||
new(tagsByImageNameAndCVEIDSearcherGQL),
|
||||
new(fixedTagsSearcherGQL),
|
||||
}
|
||||
|
||||
return searchers
|
||||
}
|
||||
|
||||
type searcher interface {
|
||||
search(searchConfig searchConfig) (bool, error)
|
||||
}
|
||||
|
@ -96,6 +117,18 @@ func (search allImagesSearcher) search(config searchConfig) (bool, error) {
|
|||
}
|
||||
}
|
||||
|
||||
type allImagesSearcherGQL struct{}
|
||||
|
||||
func (search allImagesSearcherGQL) search(config searchConfig) (bool, error) {
|
||||
if !canSearch(config.params, newSet("")) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
err := getImages(config)
|
||||
|
||||
return true, err
|
||||
}
|
||||
|
||||
type imageByNameSearcher struct{}
|
||||
|
||||
func (search imageByNameSearcher) search(config searchConfig) (bool, error) {
|
||||
|
@ -128,6 +161,32 @@ func (search imageByNameSearcher) search(config searchConfig) (bool, error) {
|
|||
}
|
||||
}
|
||||
|
||||
type imageByNameSearcherGQL struct{}
|
||||
|
||||
func (search imageByNameSearcherGQL) search(config searchConfig) (bool, error) {
|
||||
if !canSearch(config.params, newSet("imageName")) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
err := getImages(config)
|
||||
|
||||
return true, err
|
||||
}
|
||||
|
||||
func getImages(config searchConfig) error {
|
||||
username, password := getUsernameAndPassword(*config.user)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
defer cancel()
|
||||
|
||||
imageList, err := config.searchService.getImagesGQL(ctx, config, username, password, *config.params["imageName"])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return printResult(config, imageList.Data.ImageList)
|
||||
}
|
||||
|
||||
type imagesByDigestSearcher struct{}
|
||||
|
||||
func (search imagesByDigestSearcher) search(config searchConfig) (bool, error) {
|
||||
|
@ -160,6 +219,32 @@ func (search imagesByDigestSearcher) search(config searchConfig) (bool, error) {
|
|||
}
|
||||
}
|
||||
|
||||
type imagesByDigestSearcherGQL struct{}
|
||||
|
||||
func (search imagesByDigestSearcherGQL) search(config searchConfig) (bool, error) {
|
||||
if !canSearch(config.params, newSet("digest")) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// var builder strings.Builder
|
||||
|
||||
username, password := getUsernameAndPassword(*config.user)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
defer cancel()
|
||||
|
||||
imageList, err := config.searchService.getImagesByDigestGQL(ctx, config, username, password, *config.params["digest"])
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
if err := printResult(config, imageList.Data.ImageList); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
type cveByImageSearcher struct{}
|
||||
|
||||
func (search cveByImageSearcher) search(config searchConfig) (bool, error) {
|
||||
|
@ -195,10 +280,49 @@ func (search cveByImageSearcher) search(config searchConfig) (bool, error) {
|
|||
}
|
||||
}
|
||||
|
||||
type cveByImageSearcherGQL struct{}
|
||||
|
||||
func (search cveByImageSearcherGQL) search(config searchConfig) (bool, error) {
|
||||
if !canSearch(config.params, newSet("imageName")) || *config.fixedFlag {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if !validateImageNameTag(*config.params["imageName"]) {
|
||||
return true, errInvalidImageNameAndTag
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
|
||||
username, password := getUsernameAndPassword(*config.user)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
defer cancel()
|
||||
|
||||
cveList, err := config.searchService.getCveByImageGQL(ctx, config, username, password, *config.params["imageName"])
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
if len(cveList.Data.CVEListForImage.CVEList) > 0 &&
|
||||
(*config.outputFormat == defaultOutoutFormat || *config.outputFormat == "") {
|
||||
printCVETableHeader(&builder, *config.verbose)
|
||||
fmt.Fprint(config.resultWriter, builder.String())
|
||||
}
|
||||
|
||||
out, err := cveList.string(*config.outputFormat)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
fmt.Fprint(config.resultWriter, out)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
type imagesByCVEIDSearcher struct{}
|
||||
|
||||
func (search imagesByCVEIDSearcher) search(config searchConfig) (bool, error) {
|
||||
if !canSearch(config.params, newSet("cvid")) || *config.fixedFlag {
|
||||
if !canSearch(config.params, newSet("cveID")) || *config.fixedFlag {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
@ -210,7 +334,7 @@ func (search imagesByCVEIDSearcher) search(config searchConfig) (bool, error) {
|
|||
|
||||
wg.Add(1)
|
||||
|
||||
go config.searchService.getImagesByCveID(ctx, config, username, password, *config.params["cvid"], strErr, &wg)
|
||||
go config.searchService.getImagesByCveID(ctx, config, username, password, *config.params["cveID"], strErr, &wg)
|
||||
wg.Add(1)
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
|
@ -226,10 +350,34 @@ func (search imagesByCVEIDSearcher) search(config searchConfig) (bool, error) {
|
|||
}
|
||||
}
|
||||
|
||||
type imagesByCVEIDSearcherGQL struct{}
|
||||
|
||||
func (search imagesByCVEIDSearcherGQL) search(config searchConfig) (bool, error) {
|
||||
if !canSearch(config.params, newSet("cveID")) || *config.fixedFlag {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
username, password := getUsernameAndPassword(*config.user)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
defer cancel()
|
||||
|
||||
imageList, err := config.searchService.getImagesByCveIDGQL(ctx, config, username, password, *config.params["cveID"])
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
if err := printResult(config, imageList.Data.ImageList); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
type tagsByImageNameAndCVEIDSearcher struct{}
|
||||
|
||||
func (search tagsByImageNameAndCVEIDSearcher) search(config searchConfig) (bool, error) {
|
||||
if !canSearch(config.params, newSet("cvid", "imageName")) || *config.fixedFlag {
|
||||
if !canSearch(config.params, newSet("cveID", "imageName")) || *config.fixedFlag {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
@ -246,7 +394,7 @@ func (search tagsByImageNameAndCVEIDSearcher) search(config searchConfig) (bool,
|
|||
wg.Add(1)
|
||||
|
||||
go config.searchService.getImageByNameAndCVEID(ctx, config, username, password, *config.params["imageName"],
|
||||
*config.params["cvid"], strErr, &wg)
|
||||
*config.params["cveID"], strErr, &wg)
|
||||
wg.Add(1)
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
|
@ -262,10 +410,38 @@ func (search tagsByImageNameAndCVEIDSearcher) search(config searchConfig) (bool,
|
|||
}
|
||||
}
|
||||
|
||||
type tagsByImageNameAndCVEIDSearcherGQL struct{}
|
||||
|
||||
func (search tagsByImageNameAndCVEIDSearcherGQL) search(config searchConfig) (bool, error) {
|
||||
if !canSearch(config.params, newSet("cveID", "imageName")) || *config.fixedFlag {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if strings.Contains(*config.params["imageName"], ":") {
|
||||
return true, errInvalidImageName
|
||||
}
|
||||
|
||||
err := getTagsByCVE(config)
|
||||
|
||||
return true, err
|
||||
}
|
||||
|
||||
type fixedTagsSearcherGQL struct{}
|
||||
|
||||
func (search fixedTagsSearcherGQL) search(config searchConfig) (bool, error) {
|
||||
if !canSearch(config.params, newSet("cveID", "imageName")) || !*config.fixedFlag {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
err := getTagsByCVE(config)
|
||||
|
||||
return true, err
|
||||
}
|
||||
|
||||
type fixedTagsSearcher struct{}
|
||||
|
||||
func (search fixedTagsSearcher) search(config searchConfig) (bool, error) {
|
||||
if !canSearch(config.params, newSet("cvid", "imageName")) || !*config.fixedFlag {
|
||||
if !canSearch(config.params, newSet("cveID", "imageName")) || !*config.fixedFlag {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
@ -282,7 +458,7 @@ func (search fixedTagsSearcher) search(config searchConfig) (bool, error) {
|
|||
wg.Add(1)
|
||||
|
||||
go config.searchService.getFixedTagsForCVE(ctx, config, username, password, *config.params["imageName"],
|
||||
*config.params["cvid"], strErr, &wg)
|
||||
*config.params["cveID"], strErr, &wg)
|
||||
wg.Add(1)
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
|
@ -298,6 +474,39 @@ func (search fixedTagsSearcher) search(config searchConfig) (bool, error) {
|
|||
}
|
||||
}
|
||||
|
||||
func getTagsByCVE(config searchConfig) error {
|
||||
if strings.Contains(*config.params["imageName"], ":") {
|
||||
return errInvalidImageName
|
||||
}
|
||||
|
||||
username, password := getUsernameAndPassword(*config.user)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
defer cancel()
|
||||
|
||||
var imageList []imageStruct
|
||||
|
||||
if *config.fixedFlag {
|
||||
fixedTags, err := config.searchService.getFixedTagsForCVEGQL(ctx, config, username, password,
|
||||
*config.params["imageName"], *config.params["cveID"])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imageList = fixedTags.Data.ImageList
|
||||
} else {
|
||||
tags, err := config.searchService.getTagsForCVEGQL(ctx, config, username, password,
|
||||
*config.params["imageName"], *config.params["cveID"])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imageList = tags.Data.ImageList
|
||||
}
|
||||
|
||||
return printResult(config, imageList)
|
||||
}
|
||||
|
||||
func collectResults(config searchConfig, wg *sync.WaitGroup, imageErr chan stringResult,
|
||||
cancel context.CancelFunc, printHeader printHeader, errCh chan error,
|
||||
) {
|
||||
|
@ -376,12 +585,14 @@ type spinnerState struct {
|
|||
enabled bool
|
||||
}
|
||||
|
||||
// nolint
|
||||
func (spinner *spinnerState) startSpinner() {
|
||||
if spinner.enabled {
|
||||
spinner.spinner.Start()
|
||||
}
|
||||
}
|
||||
|
||||
// nolint
|
||||
func (spinner *spinnerState) stopSpinner() {
|
||||
if spinner.enabled && spinner.spinner.Active() {
|
||||
spinner.spinner.Stop()
|
||||
|
@ -397,14 +608,14 @@ func getEmptyStruct() struct{} {
|
|||
}
|
||||
|
||||
func newSet(initialValues ...string) *set {
|
||||
ret := &set{}
|
||||
ret.m = make(map[string]struct{})
|
||||
setValues := &set{}
|
||||
setValues.m = make(map[string]struct{})
|
||||
|
||||
for _, val := range initialValues {
|
||||
ret.m[val] = getEmptyStruct()
|
||||
setValues.m[val] = getEmptyStruct()
|
||||
}
|
||||
|
||||
return ret
|
||||
return setValues
|
||||
}
|
||||
|
||||
func (s *set) contains(value string) bool {
|
||||
|
@ -413,6 +624,10 @@ func (s *set) contains(value string) bool {
|
|||
return c
|
||||
}
|
||||
|
||||
const (
|
||||
waitTimeout = httpTimeout + 5*time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCannotSearch = errors.New("cannot search with these parameters")
|
||||
ErrInvalidOutputFormat = errors.New("invalid output format")
|
||||
|
@ -465,9 +680,28 @@ func printCVETableHeader(writer io.Writer, verbose bool) {
|
|||
table.Render()
|
||||
}
|
||||
|
||||
const (
|
||||
waitTimeout = httpTimeout + 5*time.Second
|
||||
)
|
||||
func printResult(config searchConfig, imageList []imageStruct) error {
|
||||
var builder strings.Builder
|
||||
|
||||
if len(imageList) > 0 {
|
||||
printImageTableHeader(&builder, *config.verbose)
|
||||
fmt.Fprint(config.resultWriter, builder.String())
|
||||
}
|
||||
|
||||
for i := range imageList {
|
||||
img := imageList[i]
|
||||
img.verbose = *config.verbose
|
||||
|
||||
out, err := img.string(*config.outputFormat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprint(config.resultWriter, out)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
errInvalidImageNameAndTag = errors.New("cli: Invalid input format. Expected IMAGENAME:TAG")
|
||||
|
|
|
@ -9,9 +9,9 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
|
@ -22,22 +22,35 @@ import (
|
|||
)
|
||||
|
||||
type SearchService interface {
|
||||
getImagesGQL(ctx context.Context, config searchConfig, username, password string,
|
||||
imageName string) (*imageListStructGQL, error)
|
||||
getImagesByDigestGQL(ctx context.Context, config searchConfig, username, password string,
|
||||
digest string) (*imageListStructForDigestGQL, error)
|
||||
getCveByImageGQL(ctx context.Context, config searchConfig, username, password,
|
||||
imageName string) (*cveResult, error)
|
||||
getImagesByCveIDGQL(ctx context.Context, config searchConfig, username, password string,
|
||||
digest string) (*imagesForCve, error)
|
||||
getTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName,
|
||||
cveID string) (*imagesForCve, error)
|
||||
getFixedTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName,
|
||||
cveID string) (*fixedTags, error)
|
||||
|
||||
getAllImages(ctx context.Context, config searchConfig, username, password string,
|
||||
channel chan stringResult, wtgrp *sync.WaitGroup)
|
||||
getImageByName(ctx context.Context, config searchConfig, username, password, imageName string,
|
||||
channel chan stringResult, wtgrp *sync.WaitGroup)
|
||||
getCveByImage(ctx context.Context, config searchConfig, username, password, imageName string,
|
||||
channel chan stringResult, wtgrp *sync.WaitGroup)
|
||||
getImagesByCveID(ctx context.Context, config searchConfig, username, password, cvid string,
|
||||
channel chan stringResult, wtgrp *sync.WaitGroup)
|
||||
getImagesByDigest(ctx context.Context, config searchConfig, username, password, digest string,
|
||||
channel chan stringResult, wtgrp *sync.WaitGroup)
|
||||
getImageByNameAndCVEID(ctx context.Context, config searchConfig, username, password, imageName, cvid string,
|
||||
channel chan stringResult, wtgrp *sync.WaitGroup)
|
||||
getFixedTagsForCVE(ctx context.Context, config searchConfig, username, password, imageName, cvid string,
|
||||
channel chan stringResult, wtgrp *sync.WaitGroup)
|
||||
getRepos(ctx context.Context, config searchConfig, username, password string,
|
||||
channel chan stringResult, wtgrp *sync.WaitGroup)
|
||||
getImageByName(ctx context.Context, config searchConfig, username, password, imageName string,
|
||||
channel chan stringResult, wtgrp *sync.WaitGroup)
|
||||
getImageByNameAndCVEID(ctx context.Context, config searchConfig, username, password, imageName, cvid string,
|
||||
channel chan stringResult, wtgrp *sync.WaitGroup)
|
||||
}
|
||||
|
||||
type searchService struct{}
|
||||
|
@ -46,6 +59,116 @@ func NewSearchService() SearchService {
|
|||
return searchService{}
|
||||
}
|
||||
|
||||
func (service searchService) getImagesGQL(ctx context.Context, config searchConfig, username, password string,
|
||||
imageName string,
|
||||
) (*imageListStructGQL, error) {
|
||||
query := fmt.Sprintf(`{ImageList(repo: "%s") {`+`
|
||||
RepoName Tag Digest ConfigDigest Size Layers {Size Digest}}
|
||||
}`,
|
||||
imageName)
|
||||
result := &imageListStructGQL{}
|
||||
|
||||
err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
|
||||
|
||||
if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil {
|
||||
return nil, errResult
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (service searchService) getImagesByDigestGQL(ctx context.Context, config searchConfig, username, password string,
|
||||
digest string,
|
||||
) (*imageListStructForDigestGQL, error) {
|
||||
query := fmt.Sprintf(`{ImageListForDigest(id: "%s") {`+`
|
||||
RepoName Tag Digest ConfigDigest Size Layers {Size Digest}}
|
||||
}`,
|
||||
digest)
|
||||
result := &imageListStructForDigestGQL{}
|
||||
|
||||
err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
|
||||
|
||||
if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil {
|
||||
return nil, errResult
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (service searchService) getImagesByCveIDGQL(ctx context.Context, config searchConfig, username,
|
||||
password, cveID string,
|
||||
) (*imagesForCve, error) {
|
||||
query := fmt.Sprintf(`{ImageListForCVE(id: "%s") {`+`
|
||||
RepoName Tag Digest Size}
|
||||
}`,
|
||||
cveID)
|
||||
result := &imagesForCve{}
|
||||
|
||||
err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
|
||||
|
||||
if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil {
|
||||
return nil, errResult
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (service searchService) getCveByImageGQL(ctx context.Context, config searchConfig, username, password,
|
||||
imageName string,
|
||||
) (*cveResult, error) {
|
||||
query := fmt.Sprintf(`{ CVEListForImage (image:"%s")`+
|
||||
` { Tag CVEList { Id Title Severity Description `+
|
||||
`PackageList {Name InstalledVersion FixedVersion}} } }`, imageName)
|
||||
result := &cveResult{}
|
||||
|
||||
err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
|
||||
|
||||
if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil {
|
||||
return nil, errResult
|
||||
}
|
||||
|
||||
result.Data.CVEListForImage.CVEList = groupCVEsBySeverity(result.Data.CVEListForImage.CVEList)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (service searchService) getTagsForCVEGQL(ctx context.Context, config searchConfig,
|
||||
username, password, imageName, cveID string,
|
||||
) (*imagesForCve, error) {
|
||||
query := fmt.Sprintf(`{ImageListForCVE(id: "%s") {`+`
|
||||
RepoName Tag Digest Size}
|
||||
}`,
|
||||
cveID)
|
||||
result := &imagesForCve{}
|
||||
|
||||
err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
|
||||
|
||||
if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil {
|
||||
return nil, errResult
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (service searchService) getFixedTagsForCVEGQL(ctx context.Context, config searchConfig,
|
||||
username, password, imageName, cveID string,
|
||||
) (*fixedTags, error) {
|
||||
query := fmt.Sprintf(`{ImageListWithCVEFixed(id: "%s", image: "%s") {`+`
|
||||
RepoName Tag Digest Size}
|
||||
}`,
|
||||
cveID, imageName)
|
||||
|
||||
result := &fixedTags{}
|
||||
|
||||
err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
|
||||
|
||||
if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil {
|
||||
return nil, errResult
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (service searchService) getImageByName(ctx context.Context, config searchConfig,
|
||||
username, password, imageName string, rch chan stringResult, wtgrp *sync.WaitGroup,
|
||||
) {
|
||||
|
@ -126,8 +249,8 @@ func getImage(ctx context.Context, config searchConfig, username, password, imag
|
|||
return
|
||||
}
|
||||
|
||||
tagsList := &tagListResp{}
|
||||
_, err = makeGETRequest(ctx, tagListEndpoint, username, password, *config.verifyTLS, &tagsList)
|
||||
tagList := &tagListResp{}
|
||||
_, err = makeGETRequest(ctx, tagListEndpoint, username, password, *config.verifyTLS, &tagList)
|
||||
|
||||
if err != nil {
|
||||
if isContextDone(ctx) {
|
||||
|
@ -138,7 +261,7 @@ func getImage(ctx context.Context, config searchConfig, username, password, imag
|
|||
return
|
||||
}
|
||||
|
||||
for _, tag := range tagsList.Tags {
|
||||
for _, tag := range tagList.Tags {
|
||||
wtgrp.Add(1)
|
||||
|
||||
go addManifestCallToPool(ctx, config, pool, username, password, imageName, tag, rch, wtgrp)
|
||||
|
@ -152,7 +275,7 @@ func (service searchService) getImagesByCveID(ctx context.Context, config search
|
|||
defer close(rch)
|
||||
|
||||
query := fmt.Sprintf(`{ImageListForCVE(id: "%s") {`+`
|
||||
Name Tags }
|
||||
RepoName Tag Digest Size}
|
||||
}`,
|
||||
cvid)
|
||||
result := &imagesForCve{}
|
||||
|
@ -189,12 +312,10 @@ func (service searchService) getImagesByCveID(ctx context.Context, config search
|
|||
|
||||
go rlim.startRateLimiter(ctx)
|
||||
|
||||
for _, image := range result.Data.ImageListForCVE {
|
||||
for _, tag := range image.Tags {
|
||||
for _, image := range result.Data.ImageList {
|
||||
localWg.Add(1)
|
||||
|
||||
go addManifestCallToPool(ctx, config, rlim, username, password, image.Name, tag, rch, &localWg)
|
||||
}
|
||||
go addManifestCallToPool(ctx, config, rlim, username, password, image.RepoName, image.Tag, rch, &localWg)
|
||||
}
|
||||
|
||||
localWg.Wait()
|
||||
|
@ -207,7 +328,7 @@ func (service searchService) getImagesByDigest(ctx context.Context, config searc
|
|||
defer close(rch)
|
||||
|
||||
query := fmt.Sprintf(`{ImageListForDigest(id: "%s") {`+`
|
||||
Name Tags }
|
||||
RepoName Tag Digest ConfigDigest Size Layers {Size Digest}}
|
||||
}`,
|
||||
digest)
|
||||
result := &imagesForDigest{}
|
||||
|
@ -244,12 +365,10 @@ func (service searchService) getImagesByDigest(ctx context.Context, config searc
|
|||
|
||||
go rlim.startRateLimiter(ctx)
|
||||
|
||||
for _, image := range result.Data.ImageListForDigest {
|
||||
for _, tag := range image.Tags {
|
||||
for _, image := range result.Data.ImageList {
|
||||
localWg.Add(1)
|
||||
|
||||
go addManifestCallToPool(ctx, config, rlim, username, password, image.Name, tag, rch, &localWg)
|
||||
}
|
||||
go addManifestCallToPool(ctx, config, rlim, username, password, image.RepoName, image.Tag, rch, &localWg)
|
||||
}
|
||||
|
||||
localWg.Wait()
|
||||
|
@ -262,7 +381,7 @@ func (service searchService) getImageByNameAndCVEID(ctx context.Context, config
|
|||
defer close(rch)
|
||||
|
||||
query := fmt.Sprintf(`{ImageListForCVE(id: "%s") {`+`
|
||||
Name Tags }
|
||||
RepoName Tag Digest ConfigDigest Size Layers {Size Digest}}
|
||||
}`,
|
||||
cvid)
|
||||
result := &imagesForCve{}
|
||||
|
@ -299,16 +418,14 @@ func (service searchService) getImageByNameAndCVEID(ctx context.Context, config
|
|||
|
||||
go rlim.startRateLimiter(ctx)
|
||||
|
||||
for _, image := range result.Data.ImageListForCVE {
|
||||
if !strings.EqualFold(imageName, image.Name) {
|
||||
for _, image := range result.Data.ImageList {
|
||||
if !strings.EqualFold(imageName, image.RepoName) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, tag := range image.Tags {
|
||||
localWg.Add(1)
|
||||
|
||||
go addManifestCallToPool(ctx, config, rlim, username, password, image.Name, tag, rch, &localWg)
|
||||
}
|
||||
go addManifestCallToPool(ctx, config, rlim, username, password, image.RepoName, image.Tag, rch, &localWg)
|
||||
}
|
||||
|
||||
localWg.Wait()
|
||||
|
@ -368,6 +485,59 @@ func (service searchService) getCveByImage(ctx context.Context, config searchCon
|
|||
rch <- stringResult{str, nil}
|
||||
}
|
||||
|
||||
func (service searchService) getFixedTagsForCVE(ctx context.Context, config searchConfig,
|
||||
username, password, imageName, cvid string, rch chan stringResult, wtgrp *sync.WaitGroup,
|
||||
) {
|
||||
defer wtgrp.Done()
|
||||
defer close(rch)
|
||||
|
||||
query := fmt.Sprintf(`{ImageListWithCVEFixed (id: "%s", image: "%s") {`+`
|
||||
RepoName Tag Digest Size}
|
||||
}`,
|
||||
cvid, imageName)
|
||||
result := &fixedTags{}
|
||||
|
||||
err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
|
||||
if err != nil {
|
||||
if isContextDone(ctx) {
|
||||
return
|
||||
}
|
||||
rch <- stringResult{"", err}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if result.Errors != nil {
|
||||
var errBuilder strings.Builder
|
||||
|
||||
for _, err := range result.Errors {
|
||||
fmt.Fprintln(&errBuilder, err.Message)
|
||||
}
|
||||
|
||||
if isContextDone(ctx) {
|
||||
return
|
||||
}
|
||||
rch <- stringResult{"", errors.New(errBuilder.String())} //nolint: goerr113
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var localWg sync.WaitGroup
|
||||
|
||||
rlim := newSmoothRateLimiter(&localWg, rch)
|
||||
localWg.Add(1)
|
||||
|
||||
go rlim.startRateLimiter(ctx)
|
||||
|
||||
for _, img := range result.Data.ImageList {
|
||||
localWg.Add(1)
|
||||
|
||||
go addManifestCallToPool(ctx, config, rlim, username, password, imageName, img.Tag, rch, &localWg)
|
||||
}
|
||||
|
||||
localWg.Wait()
|
||||
}
|
||||
|
||||
func groupCVEsBySeverity(cveList []cve) []cve {
|
||||
var (
|
||||
unknown = make([]cve, 0)
|
||||
|
@ -421,63 +591,10 @@ func isContextDone(ctx context.Context) bool {
|
|||
}
|
||||
}
|
||||
|
||||
func (service searchService) getFixedTagsForCVE(ctx context.Context, config searchConfig,
|
||||
username, password, imageName, cvid string, rch chan stringResult, wtgrp *sync.WaitGroup,
|
||||
) {
|
||||
defer wtgrp.Done()
|
||||
defer close(rch)
|
||||
|
||||
query := fmt.Sprintf(`{ImageListWithCVEFixed (id: "%s", image: "%s") {`+`
|
||||
Tags {Name Timestamp} }
|
||||
}`,
|
||||
cvid, imageName)
|
||||
result := &fixedTags{}
|
||||
|
||||
err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
|
||||
if err != nil {
|
||||
if isContextDone(ctx) {
|
||||
return
|
||||
}
|
||||
rch <- stringResult{"", err}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if result.Errors != nil {
|
||||
var errBuilder strings.Builder
|
||||
|
||||
for _, err := range result.Errors {
|
||||
fmt.Fprintln(&errBuilder, err.Message)
|
||||
}
|
||||
|
||||
if isContextDone(ctx) {
|
||||
return
|
||||
}
|
||||
rch <- stringResult{"", errors.New(errBuilder.String())} //nolint: goerr113
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var localWg sync.WaitGroup
|
||||
|
||||
rlim := newSmoothRateLimiter(&localWg, rch)
|
||||
localWg.Add(1)
|
||||
|
||||
go rlim.startRateLimiter(ctx)
|
||||
|
||||
for _, imgTag := range result.Data.ImageListWithCVEFixed.Tags {
|
||||
localWg.Add(1)
|
||||
|
||||
go addManifestCallToPool(ctx, config, rlim, username, password, imageName, imgTag.Name, rch, &localWg)
|
||||
}
|
||||
|
||||
localWg.Wait()
|
||||
}
|
||||
|
||||
// Query using JQL, the query string is passed as a parameter
|
||||
// errors are returned in the stringResult channel, the unmarshalled payload is in resultPtr.
|
||||
func (service searchService) makeGraphQLQuery(ctx context.Context, config searchConfig,
|
||||
username, password, query string,
|
||||
func (service searchService) makeGraphQLQuery(ctx context.Context,
|
||||
config searchConfig, username, password, query string,
|
||||
resultPtr interface{},
|
||||
) error {
|
||||
endPoint, err := combineServerAndEndpointURL(*config.servURL, constants.ExtSearchPrefix)
|
||||
|
@ -493,6 +610,34 @@ func (service searchService) makeGraphQLQuery(ctx context.Context, config search
|
|||
return nil
|
||||
}
|
||||
|
||||
func checkResultGraphQLQuery(ctx context.Context, err error, resultErrors []errorGraphQL,
|
||||
) error {
|
||||
if err != nil {
|
||||
if isContextDone(ctx) {
|
||||
return nil // nolint:nilnil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if resultErrors != nil {
|
||||
var errBuilder strings.Builder
|
||||
|
||||
for _, error := range resultErrors {
|
||||
fmt.Fprintln(&errBuilder, error.Message)
|
||||
}
|
||||
|
||||
if isContextDone(ctx) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// nolint: goerr113
|
||||
return errors.New(errBuilder.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addManifestCallToPool(ctx context.Context, config searchConfig, pool *requestsPool,
|
||||
username, password, imageName, tagName string, rch chan stringResult, wtgrp *sync.WaitGroup,
|
||||
) {
|
||||
|
@ -533,6 +678,11 @@ type errorGraphQL struct {
|
|||
Path []string `json:"path"`
|
||||
}
|
||||
|
||||
type tagListResp struct {
|
||||
Name string `json:"name"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
//nolint:tagliatelle // graphQL schema
|
||||
type packageList struct {
|
||||
Name string `json:"Name"`
|
||||
|
@ -579,7 +729,7 @@ func (cve cveResult) stringPlainText() (string, error) {
|
|||
table := getCVETableWriter(&builder)
|
||||
|
||||
for _, c := range cve.Data.CVEListForImage.CVEList {
|
||||
id := ellipsize(c.ID, cvidWidth, ellipsis)
|
||||
id := ellipsize(c.ID, cveIDWidth, ellipsis)
|
||||
title := ellipsize(c.Title, cveTitleWidth, ellipsis)
|
||||
severity := ellipsize(c.Severity, cveSeverityWidth, ellipsis)
|
||||
row := make([]string, 3) //nolint:gomnd
|
||||
|
@ -618,51 +768,50 @@ func (cve cveResult) stringYAML() (string, error) {
|
|||
type fixedTags struct {
|
||||
Errors []errorGraphQL `json:"errors"`
|
||||
Data struct {
|
||||
//nolint:tagliatelle // graphQL schema
|
||||
ImageListWithCVEFixed struct {
|
||||
Tags []struct {
|
||||
Name string `json:"Name"`
|
||||
Timestamp time.Time `json:"Timestamp"`
|
||||
} `json:"Tags"`
|
||||
} `json:"ImageListWithCVEFixed"`
|
||||
ImageList []imageStruct `json:"ImageListWithCVEFixed"` //nolint:tagliatelle // graphQL schema
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type imagesForCve struct {
|
||||
Errors []errorGraphQL `json:"errors"`
|
||||
Data struct {
|
||||
ImageListForCVE []tagListResp `json:"ImageListForCVE"` //nolint:tagliatelle // graphQL schema
|
||||
ImageList []imageStruct `json:"ImageListForCVE"` //nolint:tagliatelle // graphQL schema
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type imageStruct struct {
|
||||
RepoName string `json:"repoName"`
|
||||
Tag string `json:"tag"`
|
||||
ConfigDigest string `json:"configDigest"`
|
||||
Digest string `json:"digest"`
|
||||
Layers []layer `json:"layers"`
|
||||
Size string `json:"size"`
|
||||
verbose bool
|
||||
}
|
||||
|
||||
type imageListStructGQL struct {
|
||||
Errors []errorGraphQL `json:"errors"`
|
||||
Data struct {
|
||||
ImageList []imageStruct `json:"ImageList"` // nolint:tagliatelle
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type imageListStructForDigestGQL struct {
|
||||
Errors []errorGraphQL `json:"errors"`
|
||||
Data struct {
|
||||
ImageList []imageStruct `json:"ImageListForDigest"` // nolint:tagliatelle
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type imagesForDigest struct {
|
||||
Errors []errorGraphQL `json:"errors"`
|
||||
Data struct {
|
||||
ImageListForDigest []tagListResp `json:"ImageListForDigest"` //nolint:tagliatelle // graphQL schema
|
||||
ImageList []imageStruct `json:"ImageListForDigest"` //nolint:tagliatelle // graphQL schema
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type tagListResp struct {
|
||||
Name string `json:"name"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
type imageStruct struct {
|
||||
Name string `json:"name"`
|
||||
Tags []tags `json:"tags"`
|
||||
verbose bool
|
||||
}
|
||||
|
||||
type tags struct {
|
||||
Name string `json:"name"`
|
||||
Size uint64 `json:"size"`
|
||||
Digest string `json:"digest"`
|
||||
ConfigDigest string `json:"configDigest"`
|
||||
Layers []layer `json:"layerDigests"`
|
||||
}
|
||||
|
||||
type layer struct {
|
||||
Size uint64 `json:"size"`
|
||||
Size uint64 `json:"size,string"`
|
||||
Digest string `json:"digest"`
|
||||
}
|
||||
|
||||
|
@ -693,12 +842,12 @@ func (img imageStruct) stringPlainText() (string, error) {
|
|||
table.SetColMinWidth(colLayersIndex, layersWidth)
|
||||
}
|
||||
|
||||
for _, tag := range img.Tags {
|
||||
imageName := ellipsize(img.Name, imageNameWidth, ellipsis)
|
||||
tagName := ellipsize(tag.Name, tagWidth, ellipsis)
|
||||
digest := ellipsize(tag.Digest, digestWidth, "")
|
||||
size := ellipsize(strings.ReplaceAll(humanize.Bytes(tag.Size), " ", ""), sizeWidth, ellipsis)
|
||||
config := ellipsize(tag.ConfigDigest, configWidth, "")
|
||||
imageName := ellipsize(img.RepoName, imageNameWidth, ellipsis)
|
||||
tagName := ellipsize(img.Tag, tagWidth, ellipsis)
|
||||
digest := ellipsize(img.Digest, digestWidth, "")
|
||||
imgSize, _ := strconv.ParseUint(img.Size, 10, 64)
|
||||
size := ellipsize(strings.ReplaceAll(humanize.Bytes(imgSize), " ", ""), sizeWidth, ellipsis)
|
||||
config := ellipsize(img.ConfigDigest, configWidth, "")
|
||||
row := make([]string, 6) // nolint:gomnd
|
||||
|
||||
row[colImageNameIndex] = imageName
|
||||
|
@ -714,22 +863,22 @@ func (img imageStruct) stringPlainText() (string, error) {
|
|||
table.Append(row)
|
||||
|
||||
if img.verbose {
|
||||
for _, entry := range tag.Layers {
|
||||
layerSize := ellipsize(strings.ReplaceAll(humanize.Bytes(entry.Size), " ", ""), sizeWidth, ellipsis)
|
||||
for _, entry := range img.Layers {
|
||||
layerSize := entry.Size
|
||||
size := ellipsize(strings.ReplaceAll(humanize.Bytes(layerSize), " ", ""), sizeWidth, ellipsis)
|
||||
layerDigest := ellipsize(entry.Digest, digestWidth, "")
|
||||
|
||||
layerRow := make([]string, 6) // nolint:gomnd
|
||||
layerRow[colImageNameIndex] = ""
|
||||
layerRow[colTagIndex] = ""
|
||||
layerRow[colDigestIndex] = ""
|
||||
layerRow[colSizeIndex] = layerSize
|
||||
layerRow[colSizeIndex] = size
|
||||
layerRow[colConfigIndex] = ""
|
||||
layerRow[colLayersIndex] = layerDigest
|
||||
|
||||
table.Append(layerRow)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table.Render()
|
||||
|
||||
|
@ -760,6 +909,7 @@ type catalogResponse struct {
|
|||
Repositories []string `json:"repositories"`
|
||||
}
|
||||
|
||||
//nolint:tagliatelle
|
||||
type manifestResponse struct {
|
||||
Layers []struct {
|
||||
MediaType string `json:"mediaType"`
|
||||
|
@ -767,8 +917,8 @@ type manifestResponse struct {
|
|||
Size uint64 `json:"size"`
|
||||
} `json:"layers"`
|
||||
Annotations struct {
|
||||
WsTychoStackerStackerYaml string `json:"ws.tycho.stacker.stacker_yaml"` //nolint:tagliatelle // custom annotation
|
||||
WsTychoStackerGitVersion string `json:"ws.tycho.stacker.git_version"` //nolint:tagliatelle // custom annotation
|
||||
WsTychoStackerStackerYaml string `json:"ws.tycho.stacker.stacker_yaml"`
|
||||
WsTychoStackerGitVersion string `json:"ws.tycho.stacker.git_version"`
|
||||
} `json:"annotations"`
|
||||
Config struct {
|
||||
Size int `json:"size"`
|
||||
|
@ -836,7 +986,7 @@ func getCVETableWriter(writer io.Writer) *tablewriter.Table {
|
|||
table.SetBorder(false)
|
||||
table.SetTablePadding(" ")
|
||||
table.SetNoWhiteSpace(true)
|
||||
table.SetColMinWidth(colCVEIDIndex, cvidWidth)
|
||||
table.SetColMinWidth(colCVEIDIndex, cveIDWidth)
|
||||
table.SetColMinWidth(colCVESeverityIndex, cveSeverityWidth)
|
||||
table.SetColMinWidth(colCVETitleIndex, cveTitleWidth)
|
||||
|
||||
|
@ -895,7 +1045,7 @@ const (
|
|||
colLayersIndex = 4
|
||||
colSizeIndex = 5
|
||||
|
||||
cvidWidth = 16
|
||||
cveIDWidth = 16
|
||||
cveSeverityWidth = 8
|
||||
cveTitleWidth = 48
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ package common
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -25,14 +24,6 @@ type TagInfo struct {
|
|||
Timestamp time.Time
|
||||
}
|
||||
|
||||
func GetImageRepoPath(image string, storeController storage.StoreController) string {
|
||||
rootDir := GetRootDir(image, storeController)
|
||||
|
||||
repo := GetRepo(image)
|
||||
|
||||
return path.Join(rootDir, repo)
|
||||
}
|
||||
|
||||
func GetRootDir(image string, storeController storage.StoreController) string {
|
||||
var rootDir string
|
||||
|
||||
|
|
|
@ -97,7 +97,7 @@ type RepoSummary struct {
|
|||
Platforms []OsArch `json:"platforms"`
|
||||
Vendors []string `json:"vendors"`
|
||||
Score int `json:"score"`
|
||||
NewestTag ImageSummary `json:"newestTag"`
|
||||
NewestImage ImageSummary `json:"newestImage"`
|
||||
}
|
||||
|
||||
type LayerSummary struct {
|
||||
|
@ -126,8 +126,8 @@ type ErrorGQL struct {
|
|||
}
|
||||
|
||||
type ImageInfo struct {
|
||||
Name string
|
||||
Latest string
|
||||
RepoName string
|
||||
Tag string
|
||||
LastUpdated time.Time
|
||||
Description string
|
||||
Licenses string
|
||||
|
@ -377,7 +377,7 @@ func TestLatestTagSearchHTTP(t *testing.T) {
|
|||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 422)
|
||||
|
||||
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){Name%20Latest}}")
|
||||
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){RepoName%20Tag}}")
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
@ -388,9 +388,9 @@ func TestLatestTagSearchHTTP(t *testing.T) {
|
|||
So(len(responseStruct.ImgListWithLatestTag.Images), ShouldEqual, 4)
|
||||
|
||||
images := responseStruct.ImgListWithLatestTag.Images
|
||||
So(images[0].Latest, ShouldEqual, "0.0.1")
|
||||
So(images[0].Tag, ShouldEqual, "0.0.1")
|
||||
|
||||
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){Name%20Latest}}")
|
||||
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){RepoName%20Tag}}")
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
|
@ -399,7 +399,7 @@ func TestLatestTagSearchHTTP(t *testing.T) {
|
|||
panic(err)
|
||||
}
|
||||
|
||||
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){Name%20Latest}}")
|
||||
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){RepoName%20Tag}}")
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
@ -423,7 +423,7 @@ func TestLatestTagSearchHTTP(t *testing.T) {
|
|||
panic(err)
|
||||
}
|
||||
|
||||
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){Name%20Latest}}")
|
||||
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){RepoName%20Tag}}")
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
@ -434,7 +434,7 @@ func TestLatestTagSearchHTTP(t *testing.T) {
|
|||
panic(err)
|
||||
}
|
||||
|
||||
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){Name%20Latest}}")
|
||||
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){RepoName%20Tag}}")
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
@ -444,7 +444,7 @@ func TestLatestTagSearchHTTP(t *testing.T) {
|
|||
panic(err)
|
||||
}
|
||||
|
||||
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){Name%20Latest}}")
|
||||
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){RepoName%20Tag}}")
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
@ -455,7 +455,7 @@ func TestLatestTagSearchHTTP(t *testing.T) {
|
|||
panic(err)
|
||||
}
|
||||
|
||||
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){Name%20Latest}}")
|
||||
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){RepoName%20Tag}}")
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
@ -532,7 +532,7 @@ func TestExpandedRepoInfo(t *testing.T) {
|
|||
So(responseStruct.ExpandedRepoInfo.RepoInfo.Summary.Name, ShouldEqual, "zot-cve-test")
|
||||
So(responseStruct.ExpandedRepoInfo.RepoInfo.Summary.Score, ShouldEqual, -1)
|
||||
|
||||
query = "{ExpandedRepoInfo(repo:\"zot-cve-test\"){Manifests%20{Digest%20IsSigned%20Tag%20Layers%20{Size%20Digest}}}}"
|
||||
query = "{ExpandedRepoInfo(repo:\"zot-cve-test\"){Images%20{Digest%20IsSigned%20Tag%20Layers%20{Size%20Digest}}}}"
|
||||
|
||||
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + query)
|
||||
So(resp, ShouldNotBeNil)
|
||||
|
@ -543,10 +543,10 @@ func TestExpandedRepoInfo(t *testing.T) {
|
|||
|
||||
err = json.Unmarshal(resp.Body(), responseStruct)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests), ShouldNotEqual, 0)
|
||||
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests[0].Layers), ShouldNotEqual, 0)
|
||||
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Images), ShouldNotEqual, 0)
|
||||
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Images[0].Layers), ShouldNotEqual, 0)
|
||||
found := false
|
||||
for _, m := range responseStruct.ExpandedRepoInfo.RepoInfo.Manifests {
|
||||
for _, m := range responseStruct.ExpandedRepoInfo.RepoInfo.Images {
|
||||
if m.Digest == "63a795ca90aa6e7cca60941e826810a4cd0a2e73ea02bf458241df2a5c973e29" {
|
||||
found = true
|
||||
So(m.IsSigned, ShouldEqual, false)
|
||||
|
@ -564,10 +564,10 @@ func TestExpandedRepoInfo(t *testing.T) {
|
|||
|
||||
err = json.Unmarshal(resp.Body(), responseStruct)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests), ShouldNotEqual, 0)
|
||||
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests[0].Layers), ShouldNotEqual, 0)
|
||||
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Images), ShouldNotEqual, 0)
|
||||
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Images[0].Layers), ShouldNotEqual, 0)
|
||||
found = false
|
||||
for _, m := range responseStruct.ExpandedRepoInfo.RepoInfo.Manifests {
|
||||
for _, m := range responseStruct.ExpandedRepoInfo.RepoInfo.Images {
|
||||
if m.Digest == "63a795ca90aa6e7cca60941e826810a4cd0a2e73ea02bf458241df2a5c973e29" {
|
||||
found = true
|
||||
So(m.IsSigned, ShouldEqual, true)
|
||||
|
@ -575,14 +575,14 @@ func TestExpandedRepoInfo(t *testing.T) {
|
|||
}
|
||||
So(found, ShouldEqual, true)
|
||||
|
||||
query = "{ExpandedRepoInfo(repo:\"\"){Manifests%20{Digest%20Tag%20IsSigned%20Layers%20{Size%20Digest}}}}"
|
||||
query = "{ExpandedRepoInfo(repo:\"\"){Images%20{Digest%20Tag%20IsSigned%20Layers%20{Size%20Digest}}}}"
|
||||
|
||||
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + query)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
||||
query = "{ExpandedRepoInfo(repo:\"zot-test\"){Manifests%20{Digest%20Tag%20IsSigned%20Layers%20{Size%20Digest}}}}"
|
||||
query = "{ExpandedRepoInfo(repo:\"zot-test\"){Images%20{Digest%20Tag%20IsSigned%20Layers%20{Size%20Digest}}}}"
|
||||
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + query)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
|
@ -590,10 +590,10 @@ func TestExpandedRepoInfo(t *testing.T) {
|
|||
|
||||
err = json.Unmarshal(resp.Body(), responseStruct)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests), ShouldNotEqual, 0)
|
||||
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests[0].Layers), ShouldNotEqual, 0)
|
||||
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Images), ShouldNotEqual, 0)
|
||||
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Images[0].Layers), ShouldNotEqual, 0)
|
||||
found = false
|
||||
for _, m := range responseStruct.ExpandedRepoInfo.RepoInfo.Manifests {
|
||||
for _, m := range responseStruct.ExpandedRepoInfo.RepoInfo.Images {
|
||||
if m.Digest == "2bacca16b9df395fc855c14ccf50b12b58d35d468b8e7f25758aff90f89bf396" {
|
||||
found = true
|
||||
So(m.IsSigned, ShouldEqual, false)
|
||||
|
@ -611,10 +611,10 @@ func TestExpandedRepoInfo(t *testing.T) {
|
|||
|
||||
err = json.Unmarshal(resp.Body(), responseStruct)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests), ShouldNotEqual, 0)
|
||||
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests[0].Layers), ShouldNotEqual, 0)
|
||||
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Images), ShouldNotEqual, 0)
|
||||
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Images[0].Layers), ShouldNotEqual, 0)
|
||||
found = false
|
||||
for _, m := range responseStruct.ExpandedRepoInfo.RepoInfo.Manifests {
|
||||
for _, m := range responseStruct.ExpandedRepoInfo.RepoInfo.Images {
|
||||
if m.Digest == "2bacca16b9df395fc855c14ccf50b12b58d35d468b8e7f25758aff90f89bf396" {
|
||||
found = true
|
||||
So(m.IsSigned, ShouldEqual, true)
|
||||
|
@ -832,7 +832,7 @@ func TestGlobalSearch(t *testing.T) {
|
|||
}
|
||||
Vendors
|
||||
Score
|
||||
NewestTag {
|
||||
NewestImage {
|
||||
RepoName
|
||||
Tag
|
||||
LastUpdated
|
||||
|
@ -911,14 +911,14 @@ func TestGlobalSearch(t *testing.T) {
|
|||
So(repo.Vendors[0], ShouldEqual, image.Vendor)
|
||||
So(repo.Platforms[0].Os, ShouldEqual, image.Platform.Os)
|
||||
So(repo.Platforms[0].Arch, ShouldEqual, image.Platform.Arch)
|
||||
So(repo.NewestTag.RepoName, ShouldEqual, image.RepoName)
|
||||
So(repo.NewestTag.Tag, ShouldEqual, image.Tag)
|
||||
So(repo.NewestTag.LastUpdated, ShouldEqual, image.LastUpdated)
|
||||
So(repo.NewestTag.Size, ShouldEqual, image.Size)
|
||||
So(repo.NewestTag.IsSigned, ShouldEqual, image.IsSigned)
|
||||
So(repo.NewestTag.Vendor, ShouldEqual, image.Vendor)
|
||||
So(repo.NewestTag.Platform.Os, ShouldEqual, image.Platform.Os)
|
||||
So(repo.NewestTag.Platform.Arch, ShouldEqual, image.Platform.Arch)
|
||||
So(repo.NewestImage.RepoName, ShouldEqual, image.RepoName)
|
||||
So(repo.NewestImage.Tag, ShouldEqual, image.Tag)
|
||||
So(repo.NewestImage.LastUpdated, ShouldEqual, image.LastUpdated)
|
||||
So(repo.NewestImage.Size, ShouldEqual, image.Size)
|
||||
So(repo.NewestImage.IsSigned, ShouldEqual, image.IsSigned)
|
||||
So(repo.NewestImage.Vendor, ShouldEqual, image.Vendor)
|
||||
So(repo.NewestImage.Platform.Os, ShouldEqual, image.Platform.Os)
|
||||
So(repo.NewestImage.Platform.Arch, ShouldEqual, image.Platform.Arch)
|
||||
}
|
||||
|
||||
// GetRepositories fail
|
||||
|
|
|
@ -43,11 +43,11 @@ type BaseOciLayoutUtils struct {
|
|||
}
|
||||
|
||||
type RepoInfo struct {
|
||||
Manifests []Manifest `json:"manifests"`
|
||||
Summary RepoSummary
|
||||
Images []Image `json:"images"`
|
||||
}
|
||||
|
||||
type Manifest struct {
|
||||
type Image struct {
|
||||
Tag string `json:"tag"`
|
||||
Digest string `json:"digest"`
|
||||
IsSigned bool `json:"isSigned"`
|
||||
|
@ -358,7 +358,7 @@ func (olu BaseOciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error)
|
|||
// made up of all manifests, configs and image layers
|
||||
repoSize := int64(0)
|
||||
|
||||
manifests := make([]Manifest, 0)
|
||||
manifests := make([]Image, 0)
|
||||
|
||||
tagsInfo, err := olu.GetImageTagsWithTimestamp(name)
|
||||
if err != nil {
|
||||
|
@ -376,7 +376,7 @@ func (olu BaseOciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error)
|
|||
repoVendors := make([]string, 0, len(manifestList))
|
||||
|
||||
for _, man := range manifestList {
|
||||
manifestInfo := Manifest{}
|
||||
manifestInfo := Image{}
|
||||
|
||||
manifestInfo.Digest = man.Digest.Encoded()
|
||||
|
||||
|
@ -441,7 +441,7 @@ func (olu BaseOciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error)
|
|||
manifests = append(manifests, manifestInfo)
|
||||
}
|
||||
|
||||
repo.Manifests = manifests
|
||||
repo.Images = manifests
|
||||
|
||||
lastUpdate, err := olu.GetRepoLastUpdated(name)
|
||||
if err != nil {
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/aquasecurity/trivy/pkg/commands/operation"
|
||||
"github.com/aquasecurity/trivy/pkg/report"
|
||||
"github.com/aquasecurity/trivy/pkg/types"
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/urfave/cli/v2"
|
||||
"zotregistry.io/zot/pkg/extensions/search/common"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
|
@ -141,19 +142,21 @@ func (cveinfo CveInfo) GetTrivyContext(image string) *TrivyCtx {
|
|||
|
||||
func (cveinfo CveInfo) GetImageListForCVE(repo, cvid string, imgStore storage.ImageStore,
|
||||
trivyCtx *TrivyCtx,
|
||||
) ([]*string, error) {
|
||||
tags := make([]*string, 0)
|
||||
|
||||
tagList, err := imgStore.GetImageTags(repo)
|
||||
if err != nil {
|
||||
cveinfo.Log.Error().Err(err).Msg("unable to get list of image tag")
|
||||
|
||||
return tags, err
|
||||
}
|
||||
) ([]ImageInfoByCVE, error) {
|
||||
imgList := make([]ImageInfoByCVE, 0)
|
||||
|
||||
rootDir := imgStore.RootDir()
|
||||
|
||||
for _, tag := range tagList {
|
||||
manifests, err := cveinfo.LayoutUtils.GetImageManifests(repo)
|
||||
if err != nil {
|
||||
cveinfo.Log.Error().Err(err).Msg("unable to get list of image tag")
|
||||
|
||||
return imgList, err
|
||||
}
|
||||
|
||||
for _, manifest := range manifests {
|
||||
tag := manifest.Annotations[ispec.AnnotationRefName]
|
||||
|
||||
image := fmt.Sprintf("%s:%s", repo, tag)
|
||||
|
||||
trivyCtx.Input = path.Join(rootDir, image)
|
||||
|
@ -177,8 +180,20 @@ func (cveinfo CveInfo) GetImageListForCVE(repo, cvid string, imgStore storage.Im
|
|||
for _, result := range report.Results {
|
||||
for _, vulnerability := range result.Vulnerabilities {
|
||||
if vulnerability.VulnerabilityID == cvid {
|
||||
copyImgTag := tag
|
||||
tags = append(tags, ©ImgTag)
|
||||
digest := manifest.Digest
|
||||
|
||||
imageBlobManifest, err := cveinfo.LayoutUtils.GetImageBlobManifest(repo, digest)
|
||||
if err != nil {
|
||||
cveinfo.Log.Error().Err(err).Msg("unable to read image blob manifest")
|
||||
|
||||
return []ImageInfoByCVE{}, err
|
||||
}
|
||||
|
||||
imgList = append(imgList, ImageInfoByCVE{
|
||||
Tag: tag,
|
||||
Digest: digest,
|
||||
Manifest: imageBlobManifest,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
@ -186,5 +201,5 @@ func (cveinfo CveInfo) GetImageListForCVE(repo, cvid string, imgStore storage.Im
|
|||
}
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
return imgList, nil
|
||||
}
|
||||
|
|
|
@ -46,23 +46,14 @@ type CveResult struct {
|
|||
ImgList ImgList `json:"data"`
|
||||
}
|
||||
|
||||
type ImgWithFixedCVE struct {
|
||||
ImgResults ImgResults `json:"data"`
|
||||
}
|
||||
|
||||
//nolint:tagliatelle // graphQL schema
|
||||
type ImgResults struct {
|
||||
ImgResultForFixedCVE ImgResultForFixedCVE `json:"ImgResultForFixedCVE"`
|
||||
type ImgListWithCVEFixed struct {
|
||||
Images []ImageInfo `json:"ImageListWithCVEFixed"`
|
||||
}
|
||||
|
||||
//nolint:tagliatelle // graphQL schema
|
||||
type ImgResultForFixedCVE struct {
|
||||
Tags []TagInfo `json:"Tags"`
|
||||
}
|
||||
|
||||
type TagInfo struct {
|
||||
Name string
|
||||
Timestamp time.Time
|
||||
type ImageInfo struct {
|
||||
RepoName string
|
||||
LastUpdated time.Time
|
||||
}
|
||||
|
||||
//nolint:tagliatelle // graphQL schema
|
||||
|
@ -470,24 +461,24 @@ func TestCVESearch(t *testing.T) {
|
|||
|
||||
cvid := cveResult.ImgList.CVEResultForImage.CVEList[0].ID
|
||||
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-test\"){Tags{Name%20Timestamp}}}")
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-test\"){RepoName%20LastUpdated}}")
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
||||
var imgFixedCVEResult ImgWithFixedCVE
|
||||
err = json.Unmarshal(resp.Body(), &imgFixedCVEResult)
|
||||
var imgListWithCVEFixed ImgListWithCVEFixed
|
||||
err = json.Unmarshal(resp.Body(), &imgListWithCVEFixed)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(imgFixedCVEResult.ImgResults.ImgResultForFixedCVE.Tags), ShouldEqual, 0)
|
||||
So(len(imgListWithCVEFixed.Images), ShouldEqual, 0)
|
||||
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-cve-test\"){Tags{Name%20Timestamp}}}")
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-cve-test\"){RepoName%20LastUpdated}}")
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
||||
err = json.Unmarshal(resp.Body(), &imgFixedCVEResult)
|
||||
err = json.Unmarshal(resp.Body(), &imgListWithCVEFixed)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(imgFixedCVEResult.ImgResults.ImgResultForFixedCVE.Tags), ShouldEqual, 0)
|
||||
So(len(imgListWithCVEFixed.Images), ShouldEqual, 0)
|
||||
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-test\"){Tags{Name%20Timestamp}}}")
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-test\"){RepoName%20LastUpdated}}")
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
||||
|
@ -504,7 +495,7 @@ func TestCVESearch(t *testing.T) {
|
|||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-noindex\"){Tags{Name%20Timestamp}}}")
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-noindex\"){RepoName%20LastUpdated}}")
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
||||
|
@ -512,7 +503,7 @@ func TestCVESearch(t *testing.T) {
|
|||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-invalid-index\"){Tags{Name%20Timestamp}}}")
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-invalid-index\"){RepoName%20LastUpdated}}")
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
||||
|
@ -520,11 +511,11 @@ func TestCVESearch(t *testing.T) {
|
|||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-noblob\"){Tags{Name%20Timestamp}}}")
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-noblob\"){RepoName%20LastUpdated}}")
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-test\"){Tags{Name%20Timestamp}}}")
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-test\"){RepoName%20LastUpdated}}")
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
||||
|
@ -532,7 +523,7 @@ func TestCVESearch(t *testing.T) {
|
|||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-invalid-blob\"){Tags{Name%20Timestamp}}}")
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-invalid-blob\"){RepoName%20LastUpdated}}")
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
||||
|
@ -544,7 +535,7 @@ func TestCVESearch(t *testing.T) {
|
|||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListForCVE(id:\"CVE-201-20482\"){Name%20Tags}}")
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListForCVE(id:\"CVE-201-20482\"){RepoName%20Tag}}")
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
||||
|
@ -585,11 +576,11 @@ func TestCVESearch(t *testing.T) {
|
|||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 422)
|
||||
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListForCVE(tet:\"CVE-2018-20482\"){Name%20Tags}}")
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListForCVE(tet:\"CVE-2018-20482\"){RepoName%20Tag}}")
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 422)
|
||||
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageistForCVE(id:\"CVE-2018-20482\"){Name%20Tags}}")
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageistForCVE(id:\"CVE-2018-20482\"){RepoName%20Tag}}")
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 422)
|
||||
|
||||
|
@ -601,7 +592,7 @@ func TestCVESearch(t *testing.T) {
|
|||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 422)
|
||||
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListForCVE(id:\"" + cvid + "\"){Name%20Tags}}")
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListForCVE(id:\"" + cvid + "\"){RepoName%20Tag}}")
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
})
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
package cveinfo
|
||||
|
||||
import (
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/urfave/cli/v2"
|
||||
"zotregistry.io/zot/pkg/extensions/search/common"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
|
@ -25,3 +27,9 @@ type TrivyCtx struct {
|
|||
Input string
|
||||
Ctx *cli.Context
|
||||
}
|
||||
|
||||
type ImageInfoByCVE struct {
|
||||
Tag string
|
||||
Digest digest.Digest
|
||||
Manifest v1.Manifest
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ package digestinfo
|
|||
import (
|
||||
"strings"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"zotregistry.io/zot/pkg/extensions/search/common"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
|
@ -15,6 +17,12 @@ type DigestInfo struct {
|
|||
LayoutUtils *common.BaseOciLayoutUtils
|
||||
}
|
||||
|
||||
type ImageInfoByDigest struct {
|
||||
Tag string
|
||||
Digest digest.Digest
|
||||
Manifest v1.Manifest
|
||||
}
|
||||
|
||||
// NewDigestInfo initializes a new DigestInfo object.
|
||||
func NewDigestInfo(storeController storage.StoreController, log log.Logger) *DigestInfo {
|
||||
layoutUtils := common.NewBaseOciLayoutUtils(storeController, log)
|
||||
|
@ -23,14 +31,14 @@ func NewDigestInfo(storeController storage.StoreController, log log.Logger) *Dig
|
|||
}
|
||||
|
||||
// FilterImagesByDigest returns a list of image tags in a repository matching a specific divest.
|
||||
func (digestinfo DigestInfo) GetImageTagsByDigest(repo, digest string) ([]*string, error) {
|
||||
uniqueTags := []*string{}
|
||||
func (digestinfo DigestInfo) GetImageTagsByDigest(repo, digest string) ([]ImageInfoByDigest, error) {
|
||||
imageTags := []ImageInfoByDigest{}
|
||||
|
||||
manifests, err := digestinfo.LayoutUtils.GetImageManifests(repo)
|
||||
if err != nil {
|
||||
digestinfo.Log.Error().Err(err).Msg("unable to read image manifests")
|
||||
|
||||
return uniqueTags, err
|
||||
return imageTags, err
|
||||
}
|
||||
|
||||
for _, manifest := range manifests {
|
||||
|
@ -42,7 +50,7 @@ func (digestinfo DigestInfo) GetImageTagsByDigest(repo, digest string) ([]*strin
|
|||
if err != nil {
|
||||
digestinfo.Log.Error().Err(err).Msg("unable to read image blob manifest")
|
||||
|
||||
return uniqueTags, err
|
||||
return imageTags, err
|
||||
}
|
||||
|
||||
tags := []*string{}
|
||||
|
@ -71,12 +79,12 @@ func (digestinfo DigestInfo) GetImageTagsByDigest(repo, digest string) ([]*strin
|
|||
|
||||
for _, entry := range tags {
|
||||
if _, value := keys[*entry]; !value {
|
||||
uniqueTags = append(uniqueTags, entry)
|
||||
imageTags = append(imageTags, ImageInfoByDigest{Tag: *entry, Digest: imageDigest, Manifest: imageBlobManifest})
|
||||
keys[*entry] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueTags, nil
|
||||
return imageTags, nil
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/opencontainers/go-digest"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"gopkg.in/resty.v1"
|
||||
"zotregistry.io/zot/pkg/api"
|
||||
|
@ -45,8 +44,11 @@ type ImgListForDigest struct {
|
|||
|
||||
//nolint:tagliatelle // graphQL schema
|
||||
type ImgInfo struct {
|
||||
Name string `json:"Name"`
|
||||
Tags []string `json:"Tags"`
|
||||
RepoName string `json:"RepoName"`
|
||||
Tag string `json:"Tag"`
|
||||
ConfigDigest string `json:"ConfigDigest"`
|
||||
Digest string `json:"Digest"`
|
||||
Size string `json:"Size"`
|
||||
}
|
||||
|
||||
type ErrorGQL struct {
|
||||
|
@ -97,15 +99,10 @@ func testSetup() error {
|
|||
return err
|
||||
}
|
||||
|
||||
conf := config.New()
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Lint = &extconf.LintConfig{}
|
||||
|
||||
log := log.NewLogger("debug", "")
|
||||
metrics := monitoring.NewMetricsServer(false, log)
|
||||
storeController := storage.StoreController{
|
||||
DefaultStore: storage.NewImageStore(rootDir, false, storage.DefaultGCDelay,
|
||||
false, false, log, metrics, nil),
|
||||
DefaultStore: storage.NewImageStore(rootDir, false, storage.DefaultGCDelay, false, false, log, metrics, nil),
|
||||
}
|
||||
|
||||
digestInfo = digestinfo.NewDigestInfo(storeController, log)
|
||||
|
@ -115,33 +112,31 @@ func testSetup() error {
|
|||
|
||||
func TestDigestInfo(t *testing.T) {
|
||||
Convey("Test image tag", t, func() {
|
||||
log := log.NewLogger("debug", "")
|
||||
metrics := monitoring.NewMetricsServer(false, log)
|
||||
storeController := storage.StoreController{
|
||||
DefaultStore: storage.NewImageStore(rootDir, false, storage.DefaultGCDelay, false, false, log, metrics, nil),
|
||||
}
|
||||
|
||||
digestInfo = digestinfo.NewDigestInfo(storeController, log)
|
||||
|
||||
// Search by manifest digest
|
||||
var (
|
||||
manifestDigest digest.Digest
|
||||
configDigest digest.Digest
|
||||
layerDigest digest.Digest
|
||||
)
|
||||
|
||||
manifestDigest, _, layerDigest = GetOciLayoutDigests("../../../../test/data/zot-cve-test")
|
||||
|
||||
imageTags, err := digestInfo.GetImageTagsByDigest("zot-cve-test", string(manifestDigest))
|
||||
imageTags, err := digestInfo.GetImageTagsByDigest("zot-cve-test", "63a795ca")
|
||||
So(err, ShouldBeNil)
|
||||
So(len(imageTags), ShouldEqual, 1)
|
||||
So(*imageTags[0], ShouldEqual, "0.0.1")
|
||||
So(imageTags[0].Tag, ShouldEqual, "0.0.1")
|
||||
|
||||
// Search by config digest
|
||||
_, configDigest, _ = GetOciLayoutDigests("../../../../test/data/zot-test")
|
||||
|
||||
imageTags, err = digestInfo.GetImageTagsByDigest("zot-test", string(configDigest))
|
||||
imageTags, err = digestInfo.GetImageTagsByDigest("zot-test", "adf3bb6c")
|
||||
So(err, ShouldBeNil)
|
||||
So(len(imageTags), ShouldEqual, 1)
|
||||
So(*imageTags[0], ShouldEqual, "0.0.1")
|
||||
So(imageTags[0].Tag, ShouldEqual, "0.0.1")
|
||||
|
||||
// Search by layer digest
|
||||
imageTags, err = digestInfo.GetImageTagsByDigest("zot-cve-test", string(layerDigest))
|
||||
imageTags, err = digestInfo.GetImageTagsByDigest("zot-cve-test", "7a0437f0")
|
||||
So(err, ShouldBeNil)
|
||||
So(len(imageTags), ShouldEqual, 1)
|
||||
So(*imageTags[0], ShouldEqual, "0.0.1")
|
||||
So(imageTags[0].Tag, ShouldEqual, "0.0.1")
|
||||
|
||||
// Search by non-existent image
|
||||
imageTags, err = digestInfo.GetImageTagsByDigest("zot-tes", "63a795ca")
|
||||
|
@ -202,8 +197,10 @@ func TestDigestSearchHTTP(t *testing.T) {
|
|||
So(resp.StatusCode(), ShouldEqual, 422)
|
||||
|
||||
// "sha" should match all digests in all images
|
||||
resp, err = resty.R().Get(baseURL + constants.ExtSearchPrefix +
|
||||
"?query={ImageListForDigest(id:\"sha\"){Name%20Tags}}")
|
||||
resp, err = resty.R().Get(
|
||||
baseURL + constants.ExtSearchPrefix + `?query={ImageListForDigest(id:"sha")` +
|
||||
`{RepoName%20Tag%20Digest%20ConfigDigest%20Size%20Layers%20{%20Digest}}}`,
|
||||
)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
@ -213,16 +210,14 @@ func TestDigestSearchHTTP(t *testing.T) {
|
|||
So(err, ShouldBeNil)
|
||||
So(len(responseStruct.Errors), ShouldEqual, 0)
|
||||
So(len(responseStruct.ImgListForDigest.Images), ShouldEqual, 2)
|
||||
So(len(responseStruct.ImgListForDigest.Images[0].Tags), ShouldEqual, 1)
|
||||
So(len(responseStruct.ImgListForDigest.Images[0].Tags), ShouldEqual, 1)
|
||||
So(responseStruct.ImgListForDigest.Images[0].Tag, ShouldEqual, "0.0.1")
|
||||
|
||||
// Call should return {"data":{"ImageListForDigest":[{"Name":"zot-test","Tags":["0.0.1"]}]}}
|
||||
var layerDigest digest.Digest
|
||||
var manifestDigest digest.Digest
|
||||
manifestDigest, _, layerDigest = GetOciLayoutDigests("../../../../test/data/zot-test")
|
||||
|
||||
resp, err = resty.R().Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListForDigest(id:\"" +
|
||||
string(layerDigest) + "\"){Name%20Tags}}")
|
||||
// "2bacca16" should match the manifest of 1 image
|
||||
resp, err = resty.R().Get(
|
||||
baseURL + constants.ExtSearchPrefix + `?query={ImageListForDigest(id:"2bacca16")` +
|
||||
`{RepoName%20Tag%20Digest%20ConfigDigest%20Size%20Layers%20{%20Digest}}}`,
|
||||
)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
@ -231,15 +226,14 @@ func TestDigestSearchHTTP(t *testing.T) {
|
|||
So(err, ShouldBeNil)
|
||||
So(len(responseStruct.Errors), ShouldEqual, 0)
|
||||
So(len(responseStruct.ImgListForDigest.Images), ShouldEqual, 1)
|
||||
So(responseStruct.ImgListForDigest.Images[0].Name, ShouldEqual, "zot-test")
|
||||
So(len(responseStruct.ImgListForDigest.Images[0].Tags), ShouldEqual, 1)
|
||||
So(responseStruct.ImgListForDigest.Images[0].Tags[0], ShouldEqual, "0.0.1")
|
||||
So(responseStruct.ImgListForDigest.Images[0].RepoName, ShouldEqual, "zot-test")
|
||||
So(responseStruct.ImgListForDigest.Images[0].Tag, ShouldEqual, "0.0.1")
|
||||
|
||||
// Call should return {"data":{"ImageListForDigest":[{"Name":"zot-test","Tags":["0.0.1"]}]}}
|
||||
|
||||
resp, err = resty.R().Get(baseURL + constants.ExtSearchPrefix +
|
||||
"?query={ImageListForDigest(id:\"" +
|
||||
string(manifestDigest) + "\"){Name%20Tags}}")
|
||||
// "adf3bb6c" should match the config of 1 image
|
||||
resp, err = resty.R().Get(
|
||||
baseURL + constants.ExtSearchPrefix + `?query={ImageListForDigest(id:"adf3bb6c")` +
|
||||
`{RepoName%20Tag%20Digest%20ConfigDigest%20Size%20Layers%20{%20Digest}}}`,
|
||||
)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
@ -248,15 +242,15 @@ func TestDigestSearchHTTP(t *testing.T) {
|
|||
So(err, ShouldBeNil)
|
||||
So(len(responseStruct.Errors), ShouldEqual, 0)
|
||||
So(len(responseStruct.ImgListForDigest.Images), ShouldEqual, 1)
|
||||
So(responseStruct.ImgListForDigest.Images[0].Name, ShouldEqual, "zot-test")
|
||||
So(len(responseStruct.ImgListForDigest.Images[0].Tags), ShouldEqual, 1)
|
||||
So(responseStruct.ImgListForDigest.Images[0].Tags[0], ShouldEqual, "0.0.1")
|
||||
So(responseStruct.ImgListForDigest.Images[0].RepoName, ShouldEqual, "zot-test")
|
||||
So(responseStruct.ImgListForDigest.Images[0].Tag, ShouldEqual, "0.0.1")
|
||||
|
||||
// Call should return {"data":{"ImageListForDigest":[{"Name":"zot-cve-test","Tags":["0.0.1"]}]}}
|
||||
|
||||
_, _, layerDigest = GetOciLayoutDigests("../../../../test/data/zot-cve-test")
|
||||
resp, err = resty.R().Get(baseURL + constants.ExtSearchPrefix + "?query={ImageListForDigest(id:\"" +
|
||||
string(layerDigest) + "\"){Name%20Tags}}")
|
||||
// "7a0437f0" should match the layer of 1 image
|
||||
resp, err = resty.R().Get(
|
||||
baseURL + constants.ExtSearchPrefix + `?query={ImageListForDigest(id:"7a0437f0")` +
|
||||
`{RepoName%20Tag%20Digest%20ConfigDigest%20Size%20Layers%20{%20Digest}}}`,
|
||||
)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
@ -265,14 +259,15 @@ func TestDigestSearchHTTP(t *testing.T) {
|
|||
So(err, ShouldBeNil)
|
||||
So(len(responseStruct.Errors), ShouldEqual, 0)
|
||||
So(len(responseStruct.ImgListForDigest.Images), ShouldEqual, 1)
|
||||
So(responseStruct.ImgListForDigest.Images[0].Name, ShouldEqual, "zot-cve-test")
|
||||
So(len(responseStruct.ImgListForDigest.Images[0].Tags), ShouldEqual, 1)
|
||||
So(responseStruct.ImgListForDigest.Images[0].Tags[0], ShouldEqual, "0.0.1")
|
||||
So(responseStruct.ImgListForDigest.Images[0].RepoName, ShouldEqual, "zot-cve-test")
|
||||
So(responseStruct.ImgListForDigest.Images[0].Tag, ShouldEqual, "0.0.1")
|
||||
|
||||
// Call should return {"data":{"ImageListForDigest":[]}}
|
||||
// "1111111" should match 0 images
|
||||
resp, err = resty.R().Get(baseURL + constants.ExtSearchPrefix +
|
||||
"?query={ImageListForDigest(id:\"1111111\"){Name%20Tags}}")
|
||||
resp, err = resty.R().Get(
|
||||
baseURL + constants.ExtSearchPrefix + `?query={ImageListForDigest(id:"1111111")` +
|
||||
`{RepoName%20Tag%20Digest%20ConfigDigest%20Size%20Layers%20{%20Digest}}}`,
|
||||
)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
@ -283,8 +278,10 @@ func TestDigestSearchHTTP(t *testing.T) {
|
|||
So(len(responseStruct.ImgListForDigest.Images), ShouldEqual, 0)
|
||||
|
||||
// Call should return {"errors": [{....}]", data":null}}
|
||||
resp, err = resty.R().Get(baseURL + constants.ExtSearchPrefix +
|
||||
"?query={ImageListForDigest(id:\"1111111\"){Name%20Tag343s}}")
|
||||
resp, err = resty.R().Get(
|
||||
baseURL + constants.ExtSearchPrefix + `?query={ImageListForDigest(id:"1111111")` +
|
||||
`{RepoName%20Tag343s}}`,
|
||||
)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 422)
|
||||
|
@ -354,8 +351,10 @@ func TestDigestSearchHTTPSubPaths(t *testing.T) {
|
|||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 422)
|
||||
|
||||
resp, err = resty.R().Get(baseURL + constants.ExtSearchPrefix +
|
||||
"?query={ImageListForDigest(id:\"sha\"){Name%20Tags}}")
|
||||
resp, err = resty.R().Get(
|
||||
baseURL + constants.ExtSearchPrefix + `?query={ImageListForDigest(id:"sha")` +
|
||||
`{RepoName%20Tag%20Digest%20ConfigDigest%20Size%20Layers%20{%20Digest}}}`,
|
||||
)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -25,45 +25,22 @@ type GlobalSearchResult struct {
|
|||
Layers []*LayerSummary `json:"Layers"`
|
||||
}
|
||||
|
||||
type ImageInfo struct {
|
||||
Name *string `json:"Name"`
|
||||
Latest *string `json:"Latest"`
|
||||
LastUpdated *time.Time `json:"LastUpdated"`
|
||||
Description *string `json:"Description"`
|
||||
Licenses *string `json:"Licenses"`
|
||||
Vendor *string `json:"Vendor"`
|
||||
Size *string `json:"Size"`
|
||||
Labels *string `json:"Labels"`
|
||||
}
|
||||
|
||||
type ImageSummary struct {
|
||||
RepoName *string `json:"RepoName"`
|
||||
Tag *string `json:"Tag"`
|
||||
Digest *string `json:"Digest"`
|
||||
ConfigDigest *string `json:"ConfigDigest"`
|
||||
LastUpdated *time.Time `json:"LastUpdated"`
|
||||
IsSigned *bool `json:"IsSigned"`
|
||||
Size *string `json:"Size"`
|
||||
Platform *OsArch `json:"Platform"`
|
||||
Vendor *string `json:"Vendor"`
|
||||
Score *int `json:"Score"`
|
||||
}
|
||||
|
||||
type ImgResultForCve struct {
|
||||
Name *string `json:"Name"`
|
||||
Tags []*string `json:"Tags"`
|
||||
}
|
||||
|
||||
type ImgResultForDigest struct {
|
||||
Name *string `json:"Name"`
|
||||
Tags []*string `json:"Tags"`
|
||||
}
|
||||
|
||||
type ImgResultForFixedCve struct {
|
||||
Tags []*TagInfo `json:"Tags"`
|
||||
}
|
||||
|
||||
type LayerInfo struct {
|
||||
Size *string `json:"Size"`
|
||||
Digest *string `json:"Digest"`
|
||||
DownloadCount *int `json:"DownloadCount"`
|
||||
Layers []*LayerSummary `json:"Layers"`
|
||||
Description *string `json:"Description"`
|
||||
Licenses *string `json:"Licenses"`
|
||||
Labels *string `json:"Labels"`
|
||||
}
|
||||
|
||||
type LayerSummary struct {
|
||||
|
@ -72,13 +49,6 @@ type LayerSummary struct {
|
|||
Score *int `json:"Score"`
|
||||
}
|
||||
|
||||
type ManifestInfo struct {
|
||||
Digest *string `json:"Digest"`
|
||||
Tag *string `json:"Tag"`
|
||||
IsSigned *bool `json:"IsSigned"`
|
||||
Layers []*LayerInfo `json:"Layers"`
|
||||
}
|
||||
|
||||
type OsArch struct {
|
||||
Os *string `json:"Os"`
|
||||
Arch *string `json:"Arch"`
|
||||
|
@ -91,7 +61,7 @@ type PackageInfo struct {
|
|||
}
|
||||
|
||||
type RepoInfo struct {
|
||||
Manifests []*ManifestInfo `json:"Manifests"`
|
||||
Images []*ImageSummary `json:"Images"`
|
||||
Summary *RepoSummary `json:"Summary"`
|
||||
}
|
||||
|
||||
|
@ -102,11 +72,8 @@ type RepoSummary struct {
|
|||
Platforms []*OsArch `json:"Platforms"`
|
||||
Vendors []*string `json:"Vendors"`
|
||||
Score *int `json:"Score"`
|
||||
NewestTag *ImageSummary `json:"NewestTag"`
|
||||
}
|
||||
|
||||
type TagInfo struct {
|
||||
Name *string `json:"Name"`
|
||||
Digest *string `json:"Digest"`
|
||||
Timestamp *time.Time `json:"Timestamp"`
|
||||
NewestImage *ImageSummary `json:"NewestImage"`
|
||||
DownloadCount *int `json:"DownloadCount"`
|
||||
StarCount *int `json:"StarCount"`
|
||||
IsBookmarked *bool `json:"IsBookmarked"`
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
godigest "github.com/opencontainers/go-digest"
|
||||
"zotregistry.io/zot/pkg/log" // nolint: gci
|
||||
|
||||
|
@ -60,60 +61,56 @@ func GetResolverConfig(log log.Logger, storeController storage.StoreController,
|
|||
|
||||
func (r *queryResolver) getImageListForCVE(repoList []string, cvid string, imgStore storage.ImageStore,
|
||||
trivyCtx *cveinfo.TrivyCtx,
|
||||
) ([]*gql_generated.ImgResultForCve, error) {
|
||||
cveResult := []*gql_generated.ImgResultForCve{}
|
||||
) ([]*gql_generated.ImageSummary, error) {
|
||||
cveResult := []*gql_generated.ImageSummary{}
|
||||
|
||||
for _, repo := range repoList {
|
||||
r.log.Info().Str("repo", repo).Msg("extracting list of tags available in image repo")
|
||||
|
||||
name := repo
|
||||
|
||||
tags, err := r.cveInfo.GetImageListForCVE(repo, cvid, imgStore, trivyCtx)
|
||||
imageListByCVE, err := r.cveInfo.GetImageListForCVE(repo, cvid, imgStore, trivyCtx)
|
||||
if err != nil {
|
||||
r.log.Error().Err(err).Msg("error getting tag")
|
||||
|
||||
return cveResult, err
|
||||
}
|
||||
|
||||
if len(tags) != 0 {
|
||||
cveResult = append(cveResult, &gql_generated.ImgResultForCve{Name: &name, Tags: tags})
|
||||
for _, imageByCVE := range imageListByCVE {
|
||||
cveResult = append(
|
||||
cveResult,
|
||||
buildImageInfo(repo, imageByCVE.Tag, imageByCVE.Digest, imageByCVE.Manifest),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return cveResult, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) getImageListForDigest(repoList []string,
|
||||
digest string,
|
||||
) ([]*gql_generated.ImgResultForDigest, error) {
|
||||
imgResultForDigest := []*gql_generated.ImgResultForDigest{}
|
||||
func (r *queryResolver) getImageListForDigest(repoList []string, digest string) ([]*gql_generated.ImageSummary, error) {
|
||||
imgResultForDigest := []*gql_generated.ImageSummary{}
|
||||
|
||||
var errResult error
|
||||
|
||||
for _, repo := range repoList {
|
||||
r.log.Info().Str("repo", repo).Msg("filtering list of tags in image repo by digest")
|
||||
|
||||
tags, err := r.digestInfo.GetImageTagsByDigest(repo, digest)
|
||||
imgTags, err := r.digestInfo.GetImageTagsByDigest(repo, digest)
|
||||
if err != nil {
|
||||
r.log.Error().Err(err).Msg("unable to get filtered list of image tags")
|
||||
|
||||
errResult = err
|
||||
|
||||
continue
|
||||
return []*gql_generated.ImageSummary{}, err
|
||||
}
|
||||
|
||||
if len(tags) != 0 {
|
||||
name := repo
|
||||
|
||||
imgResultForDigest = append(imgResultForDigest, &gql_generated.ImgResultForDigest{Name: &name, Tags: tags})
|
||||
for _, imageInfo := range imgTags {
|
||||
imageInfo := buildImageInfo(repo, imageInfo.Tag, imageInfo.Digest, imageInfo.Manifest)
|
||||
imgResultForDigest = append(imgResultForDigest, imageInfo)
|
||||
}
|
||||
}
|
||||
|
||||
return imgResultForDigest, errResult
|
||||
}
|
||||
|
||||
func (r *queryResolver) getImageListWithLatestTag(store storage.ImageStore) ([]*gql_generated.ImageInfo, error) {
|
||||
results := make([]*gql_generated.ImageInfo, 0)
|
||||
func (r *queryResolver) getImageListWithLatestTag(store storage.ImageStore) ([]*gql_generated.ImageSummary, error) {
|
||||
results := make([]*gql_generated.ImageSummary, 0)
|
||||
|
||||
repoList, err := store.GetRepositories()
|
||||
if err != nil {
|
||||
|
@ -167,7 +164,6 @@ func (r *queryResolver) getImageListWithLatestTag(store storage.ImageStore) ([]*
|
|||
labels := imageConfig.Config.Labels
|
||||
|
||||
// Read Description
|
||||
|
||||
desc := common.GetDescription(labels)
|
||||
|
||||
// Read licenses
|
||||
|
@ -179,8 +175,8 @@ func (r *queryResolver) getImageListWithLatestTag(store storage.ImageStore) ([]*
|
|||
// Read categories
|
||||
categories := common.GetCategories(labels)
|
||||
|
||||
results = append(results, &gql_generated.ImageInfo{
|
||||
Name: &name, Latest: &latestTag.Name,
|
||||
results = append(results, &gql_generated.ImageSummary{
|
||||
RepoName: &name, Tag: &latestTag.Name,
|
||||
Description: &desc, Licenses: &license, Vendor: &vendor,
|
||||
Labels: &categories, Size: &size, LastUpdated: &latestTag.Timestamp,
|
||||
})
|
||||
|
@ -336,7 +332,7 @@ func globalSearch(repoList []string, name, tag string, olu common.OciLayoutUtils
|
|||
Platforms: repoPlatforms,
|
||||
Vendors: repoVendors,
|
||||
Score: &index,
|
||||
NewestTag: &lastUpdatedImageSummary,
|
||||
NewestImage: &lastUpdatedImageSummary,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -382,15 +378,94 @@ func calculateImageMatchingScore(artefactName string, index int, matchesTag bool
|
|||
return score
|
||||
}
|
||||
|
||||
func getGraphqlCompatibleTags(fixedTags []common.TagInfo) []*gql_generated.TagInfo {
|
||||
finalTagList := make([]*gql_generated.TagInfo, 0)
|
||||
func (r *queryResolver) getImageList(store storage.ImageStore, imageName string) (
|
||||
[]*gql_generated.ImageSummary, error,
|
||||
) {
|
||||
results := make([]*gql_generated.ImageSummary, 0)
|
||||
|
||||
for _, tag := range fixedTags {
|
||||
fixTag := tag
|
||||
repoList, err := store.GetRepositories()
|
||||
if err != nil {
|
||||
r.log.Error().Err(err).Msg("extension api: error extracting repositories list")
|
||||
|
||||
finalTagList = append(finalTagList,
|
||||
&gql_generated.TagInfo{Name: &fixTag.Name, Digest: &fixTag.Digest, Timestamp: &fixTag.Timestamp})
|
||||
return results, err
|
||||
}
|
||||
|
||||
return finalTagList
|
||||
layoutUtils := common.NewBaseOciLayoutUtils(r.storeController, r.log)
|
||||
|
||||
for _, repo := range repoList {
|
||||
if (imageName != "" && repo == imageName) || imageName == "" {
|
||||
tagsInfo, err := layoutUtils.GetImageTagsWithTimestamp(repo)
|
||||
if err != nil {
|
||||
r.log.Error().Err(err).Msg("extension api: error getting tag timestamp info")
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
if len(tagsInfo) == 0 {
|
||||
r.log.Info().Str("no tagsinfo found for repo", repo).Msg(" continuing traversing")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
for i := range tagsInfo {
|
||||
// using a loop variable called tag would be reassigned after each iteration, using the same memory address
|
||||
// directly access the value at the current index in the slice as ImageInfo requires pointers to tag fields
|
||||
tag := tagsInfo[i]
|
||||
|
||||
digest := godigest.Digest(tag.Digest)
|
||||
|
||||
manifest, err := layoutUtils.GetImageBlobManifest(repo, digest)
|
||||
if err != nil {
|
||||
r.log.Error().Err(err).Msg("extension api: error reading manifest")
|
||||
|
||||
return results, err
|
||||
}
|
||||
|
||||
imageInfo := buildImageInfo(repo, tag.Name, digest, manifest)
|
||||
|
||||
results = append(results, imageInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
r.log.Info().Msg("no repositories found")
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func buildImageInfo(repo string, tag string, tagDigest godigest.Digest,
|
||||
manifest v1.Manifest,
|
||||
) *gql_generated.ImageSummary {
|
||||
layers := []*gql_generated.LayerSummary{}
|
||||
size := int64(0)
|
||||
|
||||
for _, entry := range manifest.Layers {
|
||||
size += entry.Size
|
||||
digest := entry.Digest.Hex
|
||||
layerSize := strconv.FormatInt(entry.Size, 10)
|
||||
|
||||
layers = append(
|
||||
layers,
|
||||
&gql_generated.LayerSummary{
|
||||
Size: &layerSize,
|
||||
Digest: &digest,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
formattedSize := strconv.FormatInt(size, 10)
|
||||
formattedTagDigest := tagDigest.Hex()
|
||||
|
||||
imageInfo := &gql_generated.ImageSummary{
|
||||
RepoName: &repo,
|
||||
Tag: &tag,
|
||||
Digest: &formattedTagDigest,
|
||||
ConfigDigest: &manifest.Config.Digest.Hex,
|
||||
Size: &formattedSize,
|
||||
Layers: layers,
|
||||
}
|
||||
|
||||
return imageInfo
|
||||
}
|
||||
|
|
|
@ -139,7 +139,7 @@ func TestGlobalSearch(t *testing.T) {
|
|||
mockOlum := mocks.OciLayoutUtilsMock{
|
||||
GetExpandedRepoInfoFn: func(name string) (common.RepoInfo, error) {
|
||||
return common.RepoInfo{
|
||||
Manifests: []common.Manifest{
|
||||
Images: []common.Image{
|
||||
{
|
||||
Tag: "latest",
|
||||
Layers: []common.Layer{
|
||||
|
|
|
@ -19,54 +19,11 @@ type PackageInfo {
|
|||
FixedVersion: String
|
||||
}
|
||||
|
||||
type ImgResultForCVE {
|
||||
Name: String
|
||||
Tags: [String]
|
||||
}
|
||||
|
||||
type ImgResultForFixedCVE {
|
||||
Tags: [TagInfo]
|
||||
}
|
||||
|
||||
type ImgResultForDigest {
|
||||
Name: String
|
||||
Tags: [String]
|
||||
}
|
||||
|
||||
type TagInfo {
|
||||
Name: String
|
||||
Digest: String
|
||||
Timestamp: Time
|
||||
}
|
||||
|
||||
type ImageInfo {
|
||||
Name: String
|
||||
Latest: String
|
||||
LastUpdated: Time
|
||||
Description: String
|
||||
Licenses: String
|
||||
Vendor: String
|
||||
Size: String
|
||||
Labels: String
|
||||
}
|
||||
|
||||
type RepoInfo {
|
||||
Manifests: [ManifestInfo]
|
||||
Images: [ImageSummary]
|
||||
Summary: RepoSummary
|
||||
}
|
||||
|
||||
type ManifestInfo {
|
||||
Digest: String
|
||||
Tag: String
|
||||
IsSigned: Boolean
|
||||
Layers: [LayerInfo]
|
||||
}
|
||||
|
||||
type LayerInfo {
|
||||
Size: String # Int64 is not supported.
|
||||
Digest: String
|
||||
}
|
||||
|
||||
# Search results in all repos/images/layers
|
||||
# There will be other more structures for more detailed information
|
||||
type GlobalSearchResult {
|
||||
|
@ -80,12 +37,19 @@ type GlobalSearchResult {
|
|||
type ImageSummary {
|
||||
RepoName: String
|
||||
Tag: String
|
||||
Digest: String
|
||||
ConfigDigest: String
|
||||
LastUpdated: Time
|
||||
IsSigned: Boolean
|
||||
Size: String
|
||||
Platform: OsArch
|
||||
Vendor: String
|
||||
Score: Int
|
||||
DownloadCount: Int
|
||||
Layers: [LayerSummary]
|
||||
Description: String
|
||||
Licenses: String
|
||||
Labels: String
|
||||
}
|
||||
|
||||
# Brief on a specific repo to be used in queries returning a list of repos
|
||||
|
@ -96,7 +60,10 @@ type RepoSummary {
|
|||
Platforms: [OsArch]
|
||||
Vendors: [String]
|
||||
Score: Int
|
||||
NewestTag: ImageSummary
|
||||
NewestImage: ImageSummary
|
||||
DownloadCount: Int
|
||||
StarCount: Int
|
||||
IsBookmarked: Boolean
|
||||
}
|
||||
|
||||
# Currently the same as LayerInfo, we can refactor later
|
||||
|
@ -113,11 +80,12 @@ type OsArch {
|
|||
}
|
||||
|
||||
type Query {
|
||||
CVEListForImage(image: String!) :CVEResultForImage
|
||||
ImageListForCVE(id: String!) :[ImgResultForCVE]
|
||||
ImageListWithCVEFixed(id: String!, image: String!) :ImgResultForFixedCVE
|
||||
ImageListForDigest(id: String!) :[ImgResultForDigest]
|
||||
ImageListWithLatestTag:[ImageInfo]
|
||||
ExpandedRepoInfo(repo: String!):RepoInfo
|
||||
GlobalSearch(query: String!): GlobalSearchResult
|
||||
CVEListForImage(image: String!): CVEResultForImage!
|
||||
ImageListForCVE(id: String!): [ImageSummary!]
|
||||
ImageListWithCVEFixed(id: String!, image: String!): [ImageSummary!]
|
||||
ImageListForDigest(id: String!): [ImageSummary!]
|
||||
ImageListWithLatestTag: [ImageSummary!]
|
||||
ImageList(repo: String!): [ImageSummary!]
|
||||
ExpandedRepoInfo(repo: String!): RepoInfo!
|
||||
GlobalSearch(query: String!): GlobalSearchResult!
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
godigest "github.com/opencontainers/go-digest"
|
||||
"zotregistry.io/zot/pkg/extensions/search/common"
|
||||
cveinfo "zotregistry.io/zot/pkg/extensions/search/cve"
|
||||
"zotregistry.io/zot/pkg/extensions/search/gql_generated"
|
||||
|
@ -101,8 +102,8 @@ func (r *queryResolver) CVEListForImage(ctx context.Context, image string) (*gql
|
|||
}
|
||||
|
||||
// ImageListForCve is the resolver for the ImageListForCVE field.
|
||||
func (r *queryResolver) ImageListForCve(ctx context.Context, id string) ([]*gql_generated.ImgResultForCve, error) {
|
||||
finalCveResult := []*gql_generated.ImgResultForCve{}
|
||||
func (r *queryResolver) ImageListForCve(ctx context.Context, id string) ([]*gql_generated.ImageSummary, error) {
|
||||
finalCveResult := []*gql_generated.ImageSummary{}
|
||||
|
||||
r.log.Info().Msg("extracting repositories")
|
||||
|
||||
|
@ -154,8 +155,8 @@ func (r *queryResolver) ImageListForCve(ctx context.Context, id string) ([]*gql_
|
|||
}
|
||||
|
||||
// ImageListWithCVEFixed is the resolver for the ImageListWithCVEFixed field.
|
||||
func (r *queryResolver) ImageListWithCVEFixed(ctx context.Context, id string, image string) (*gql_generated.ImgResultForFixedCve, error) {
|
||||
imgResultForFixedCVE := &gql_generated.ImgResultForFixedCve{}
|
||||
func (r *queryResolver) ImageListWithCVEFixed(ctx context.Context, id string, image string) ([]*gql_generated.ImageSummary, error) {
|
||||
tagListForCVE := []*gql_generated.ImageSummary{}
|
||||
|
||||
r.log.Info().Str("image", image).Msg("extracting list of tags available in image")
|
||||
|
||||
|
@ -163,7 +164,7 @@ func (r *queryResolver) ImageListWithCVEFixed(ctx context.Context, id string, im
|
|||
if err != nil {
|
||||
r.log.Error().Err(err).Msg("unable to read image tags")
|
||||
|
||||
return imgResultForFixedCVE, err
|
||||
return tagListForCVE, err
|
||||
}
|
||||
|
||||
infectedTags := make([]common.TagInfo, 0)
|
||||
|
@ -213,28 +214,34 @@ func (r *queryResolver) ImageListWithCVEFixed(ctx context.Context, id string, im
|
|||
}
|
||||
}
|
||||
|
||||
var finalTagList []*gql_generated.TagInfo
|
||||
|
||||
if len(infectedTags) != 0 {
|
||||
r.log.Info().Msg("comparing fixed tags timestamp")
|
||||
|
||||
fixedTags := common.GetFixedTags(tagsInfo, infectedTags)
|
||||
|
||||
finalTagList = getGraphqlCompatibleTags(fixedTags)
|
||||
tagsInfo = common.GetFixedTags(tagsInfo, infectedTags)
|
||||
} else {
|
||||
r.log.Info().Str("image", image).Str("cve-id", id).Msg("image does not contain any tag that have given cve")
|
||||
|
||||
finalTagList = getGraphqlCompatibleTags(tagsInfo)
|
||||
}
|
||||
|
||||
imgResultForFixedCVE = &gql_generated.ImgResultForFixedCve{Tags: finalTagList}
|
||||
for _, tag := range tagsInfo {
|
||||
digest := godigest.Digest(tag.Digest)
|
||||
|
||||
return imgResultForFixedCVE, nil
|
||||
manifest, err := r.cveInfo.LayoutUtils.GetImageBlobManifest(image, digest)
|
||||
if err != nil {
|
||||
r.log.Error().Err(err).Msg("extension api: error reading manifest")
|
||||
|
||||
return []*gql_generated.ImageSummary{}, err
|
||||
}
|
||||
|
||||
imageInfo := buildImageInfo(image, tag.Name, digest, manifest)
|
||||
tagListForCVE = append(tagListForCVE, imageInfo)
|
||||
}
|
||||
|
||||
return tagListForCVE, nil
|
||||
}
|
||||
|
||||
// ImageListForDigest is the resolver for the ImageListForDigest field.
|
||||
func (r *queryResolver) ImageListForDigest(ctx context.Context, id string) ([]*gql_generated.ImgResultForDigest, error) {
|
||||
imgResultForDigest := []*gql_generated.ImgResultForDigest{}
|
||||
func (r *queryResolver) ImageListForDigest(ctx context.Context, id string) ([]*gql_generated.ImageSummary, error) {
|
||||
imgResultForDigest := []*gql_generated.ImageSummary{}
|
||||
|
||||
r.log.Info().Msg("extracting repositories")
|
||||
|
||||
|
@ -281,10 +288,10 @@ func (r *queryResolver) ImageListForDigest(ctx context.Context, id string) ([]*g
|
|||
}
|
||||
|
||||
// ImageListWithLatestTag is the resolver for the ImageListWithLatestTag field.
|
||||
func (r *queryResolver) ImageListWithLatestTag(ctx context.Context) ([]*gql_generated.ImageInfo, error) {
|
||||
func (r *queryResolver) ImageListWithLatestTag(ctx context.Context) ([]*gql_generated.ImageSummary, error) {
|
||||
r.log.Info().Msg("extension api: finding image list")
|
||||
|
||||
imageList := make([]*gql_generated.ImageInfo, 0)
|
||||
imageList := make([]*gql_generated.ImageSummary, 0)
|
||||
|
||||
defaultStore := r.storeController.DefaultStore
|
||||
|
||||
|
@ -317,6 +324,43 @@ func (r *queryResolver) ImageListWithLatestTag(ctx context.Context) ([]*gql_gene
|
|||
return imageList, nil
|
||||
}
|
||||
|
||||
// ImageList is the resolver for the ImageList field.
|
||||
func (r *queryResolver) ImageList(ctx context.Context, repo string) ([]*gql_generated.ImageSummary, error) {
|
||||
r.log.Info().Msg("extension api: getting a list of all images")
|
||||
|
||||
imageList := make([]*gql_generated.ImageSummary, 0)
|
||||
|
||||
defaultStore := r.storeController.DefaultStore
|
||||
|
||||
dsImageList, err := r.getImageList(defaultStore, repo)
|
||||
if err != nil {
|
||||
r.log.Error().Err(err).Msg("extension api: error extracting default store image list")
|
||||
|
||||
return imageList, err
|
||||
}
|
||||
|
||||
if len(dsImageList) != 0 {
|
||||
imageList = append(imageList, dsImageList...)
|
||||
}
|
||||
|
||||
subStore := r.storeController.SubStore
|
||||
|
||||
for _, store := range subStore {
|
||||
ssImageList, err := r.getImageList(store, repo)
|
||||
if err != nil {
|
||||
r.log.Error().Err(err).Msg("extension api: error extracting substore image list")
|
||||
|
||||
return imageList, err
|
||||
}
|
||||
|
||||
if len(ssImageList) != 0 {
|
||||
imageList = append(imageList, ssImageList...)
|
||||
}
|
||||
}
|
||||
|
||||
return imageList, nil
|
||||
}
|
||||
|
||||
// ExpandedRepoInfo is the resolver for the ExpandedRepoInfo field.
|
||||
func (r *queryResolver) ExpandedRepoInfo(ctx context.Context, repo string) (*gql_generated.RepoInfo, error) {
|
||||
olu := common.NewBaseOciLayoutUtils(r.storeController, r.log)
|
||||
|
@ -331,7 +375,7 @@ func (r *queryResolver) ExpandedRepoInfo(ctx context.Context, repo string) (*gql
|
|||
// repos type is of common deep copy this to search
|
||||
repoInfo := &gql_generated.RepoInfo{}
|
||||
|
||||
manifests := make([]*gql_generated.ManifestInfo, 0)
|
||||
images := make([]*gql_generated.ImageSummary, 0)
|
||||
|
||||
summary := &gql_generated.RepoSummary{}
|
||||
|
||||
|
@ -358,34 +402,34 @@ func (r *queryResolver) ExpandedRepoInfo(ctx context.Context, repo string) (*gql
|
|||
score := -1 // score not relevant for this query
|
||||
summary.Score = &score
|
||||
|
||||
for _, manifest := range origRepoInfo.Manifests {
|
||||
tag := manifest.Tag
|
||||
for _, image := range origRepoInfo.Images {
|
||||
tag := image.Tag
|
||||
|
||||
digest := manifest.Digest
|
||||
digest := image.Digest
|
||||
|
||||
isSigned := manifest.IsSigned
|
||||
isSigned := image.IsSigned
|
||||
|
||||
manifestInfo := &gql_generated.ManifestInfo{Tag: &tag, Digest: &digest, IsSigned: &isSigned}
|
||||
imageSummary := &gql_generated.ImageSummary{Tag: &tag, Digest: &digest, IsSigned: &isSigned}
|
||||
|
||||
layers := make([]*gql_generated.LayerInfo, 0)
|
||||
layers := make([]*gql_generated.LayerSummary, 0)
|
||||
|
||||
for _, l := range manifest.Layers {
|
||||
for _, l := range image.Layers {
|
||||
size := l.Size
|
||||
|
||||
digest := l.Digest
|
||||
|
||||
layerInfo := &gql_generated.LayerInfo{Digest: &digest, Size: &size}
|
||||
layerInfo := &gql_generated.LayerSummary{Digest: &digest, Size: &size}
|
||||
|
||||
layers = append(layers, layerInfo)
|
||||
}
|
||||
|
||||
manifestInfo.Layers = layers
|
||||
imageSummary.Layers = layers
|
||||
|
||||
manifests = append(manifests, manifestInfo)
|
||||
images = append(images, imageSummary)
|
||||
}
|
||||
|
||||
repoInfo.Summary = summary
|
||||
repoInfo.Manifests = manifests
|
||||
repoInfo.Images = images
|
||||
|
||||
return repoInfo, nil
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue