0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-16 21:56:37 -05:00

refactor(cli): added equivalent subcommands for each flag combination under every command (#1674)

- image command is now deprecated in favor of 'images'
- cve command is now deprecated in favor of 'cves'

Signed-off-by: Laurentiu Niculae <niculae.laurentiu1@gmail.com>
This commit is contained in:
LaurentiuNiculae 2023-08-30 20:12:24 +03:00 committed by GitHub
parent 2bd479edd7
commit 112fbec5b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 3857 additions and 315 deletions

View file

@ -3,7 +3,6 @@ package main
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"log" "log"
@ -19,6 +18,7 @@ import (
ispec "github.com/opencontainers/image-spec/specs-go/v1" ispec "github.com/opencontainers/image-spec/specs-go/v1"
"gopkg.in/resty.v1" "gopkg.in/resty.v1"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/test" "zotregistry.io/zot/pkg/test"
) )
@ -32,7 +32,8 @@ func makeHTTPGetRequest(url string, resultPtr interface{}, client *resty.Client)
if resp.StatusCode() != http.StatusOK { if resp.StatusCode() != http.StatusOK {
log.Printf("unable to make GET request on %s, response status code: %d", url, resp.StatusCode()) log.Printf("unable to make GET request on %s, response status code: %d", url, resp.StatusCode())
return errors.New(string(resp.Body())) //nolint: goerr113 return fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusOK,
resp.StatusCode(), string(resp.Body()))
} }
err = json.Unmarshal(resp.Body(), resultPtr) err = json.Unmarshal(resp.Body(), resultPtr)
@ -52,7 +53,8 @@ func makeHTTPDeleteRequest(url string, client *resty.Client) error {
if resp.StatusCode() != http.StatusAccepted { if resp.StatusCode() != http.StatusAccepted {
log.Printf("unable to make DELETE request on %s, response status code: %d", url, resp.StatusCode()) log.Printf("unable to make DELETE request on %s, response status code: %d", url, resp.StatusCode())
return errors.New(string(resp.Body())) //nolint: goerr113 return fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusAccepted,
resp.StatusCode(), string(resp.Body()))
} }
return nil return nil
@ -336,7 +338,8 @@ func pushMonolithImage(workdir, url, trepo string, repos []string, config testCo
// request specific check // request specific check
statusCode = resp.StatusCode() statusCode = resp.StatusCode()
if statusCode != http.StatusAccepted { if statusCode != http.StatusAccepted {
return nil, repos, errors.New(string(resp.Body())) //nolint: goerr113 return nil, repos, fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusAccepted,
resp.StatusCode(), string(resp.Body())) //nolint: goerr113
} }
loc := test.Location(url, resp) loc := test.Location(url, resp)
@ -374,7 +377,8 @@ func pushMonolithImage(workdir, url, trepo string, repos []string, config testCo
// request specific check // request specific check
statusCode = resp.StatusCode() statusCode = resp.StatusCode()
if statusCode != http.StatusCreated { if statusCode != http.StatusCreated {
return nil, repos, errors.New(string(resp.Body())) //nolint: goerr113 return nil, repos, fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusCreated,
resp.StatusCode(), string(resp.Body()))
} }
// upload image config blob // upload image config blob
@ -388,7 +392,8 @@ func pushMonolithImage(workdir, url, trepo string, repos []string, config testCo
// request specific check // request specific check
statusCode = resp.StatusCode() statusCode = resp.StatusCode()
if statusCode != http.StatusAccepted { if statusCode != http.StatusAccepted {
return nil, repos, errors.New(string(resp.Body())) //nolint: goerr113 return nil, repos, fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusAccepted,
resp.StatusCode(), string(resp.Body()))
} }
loc = test.Location(url, resp) loc = test.Location(url, resp)
@ -408,7 +413,8 @@ func pushMonolithImage(workdir, url, trepo string, repos []string, config testCo
// request specific check // request specific check
statusCode = resp.StatusCode() statusCode = resp.StatusCode()
if statusCode != http.StatusCreated { if statusCode != http.StatusCreated {
return nil, repos, errors.New(string(resp.Body())) //nolint: goerr113 return nil, repos, fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusCreated,
resp.StatusCode(), string(resp.Body()))
} }
// create a manifest // create a manifest
@ -451,7 +457,8 @@ func pushMonolithImage(workdir, url, trepo string, repos []string, config testCo
// request specific check // request specific check
statusCode = resp.StatusCode() statusCode = resp.StatusCode()
if statusCode != http.StatusCreated { if statusCode != http.StatusCreated {
return nil, repos, errors.New(string(resp.Body())) //nolint: goerr113 return nil, repos, fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusCreated,
resp.StatusCode(), string(resp.Body()))
} }
manifestHash[repo] = manifestTag manifestHash[repo] = manifestTag

View file

@ -74,7 +74,7 @@ var (
ErrInvalidArgs = errors.New("cli: Invalid Arguments") ErrInvalidArgs = errors.New("cli: Invalid Arguments")
ErrInvalidFlagsCombination = errors.New("cli: Invalid combination of flags") ErrInvalidFlagsCombination = errors.New("cli: Invalid combination of flags")
ErrInvalidURL = errors.New("cli: invalid URL format") ErrInvalidURL = errors.New("cli: invalid URL format")
ErrExtensionNotEnabled = errors.New("cli: functionality is not built in current version") ErrExtensionNotEnabled = errors.New("cli: functionality is not built/configured in the current server")
ErrUnauthorizedAccess = errors.New("auth: unauthorized access. check credentials") ErrUnauthorizedAccess = errors.New("auth: unauthorized access. check credentials")
ErrCannotResetConfigKey = errors.New("cli: cannot reset given config key") ErrCannotResetConfigKey = errors.New("cli: cannot reset given config key")
ErrConfigNotFound = errors.New("cli: config with the given name does not exist") ErrConfigNotFound = errors.New("cli: config with the given name does not exist")
@ -115,7 +115,7 @@ var (
ErrEmptyRepoName = errors.New("metadb: repo name can't be empty string") ErrEmptyRepoName = errors.New("metadb: repo name can't be empty string")
ErrEmptyTag = errors.New("metadb: tag can't be empty string") ErrEmptyTag = errors.New("metadb: tag can't be empty string")
ErrEmptyDigest = errors.New("metadb: digest can't be empty string") ErrEmptyDigest = errors.New("metadb: digest can't be empty string")
ErrInvalidRepoRefFormat = errors.New("invalid image reference format") ErrInvalidRepoRefFormat = errors.New("invalid image reference format [repo:tag] or [repo@digest]")
ErrLimitIsNegative = errors.New("pageturner: limit has negative value") ErrLimitIsNegative = errors.New("pageturner: limit has negative value")
ErrOffsetIsNegative = errors.New("pageturner: offset has negative value") ErrOffsetIsNegative = errors.New("pageturner: offset has negative value")
ErrSortCriteriaNotSupported = errors.New("pageturner: the sort criteria is not supported") ErrSortCriteriaNotSupported = errors.New("pageturner: the sort criteria is not supported")
@ -151,4 +151,8 @@ var (
ErrInvalidPublicKeyContent = errors.New("signatures: invalid public key content") ErrInvalidPublicKeyContent = errors.New("signatures: invalid public key content")
ErrInvalidStateCookie = errors.New("auth: state cookie not present or differs from original state") ErrInvalidStateCookie = errors.New("auth: state cookie not present or differs from original state")
ErrSyncNoURLsLeft = errors.New("sync: no valid registry urls left after filtering local ones") ErrSyncNoURLsLeft = errors.New("sync: no valid registry urls left after filtering local ones")
ErrInvalidCLIParameter = errors.New("cli: invalid parameter")
ErrGQLEndpointNotFound = errors.New("cli: the server doesn't have a gql endpoint")
ErrGQLQueryNotSupported = errors.New("cli: query is not supported or has different arguments")
ErrBadHTTPStatusCode = errors.New("cli: the response doesn't contain the expected status code")
) )

View file

@ -5,6 +5,8 @@ const (
ExtCatalogPrefix = "/_catalog" ExtCatalogPrefix = "/_catalog"
ExtOciDiscoverPrefix = "/_oci/ext/discover" ExtOciDiscoverPrefix = "/_oci/ext/discover"
BaseExtension = "_zot"
// zot specific extensions. // zot specific extensions.
BasePrefix = "/_zot" BasePrefix = "/_zot"
ExtPrefix = BasePrefix + "/ext" ExtPrefix = BasePrefix + "/ext"

View file

@ -8491,7 +8491,7 @@ func TestDistSpecExtensions(t *testing.T) {
t.Log(extensionList.Extensions) t.Log(extensionList.Extensions)
So(len(extensionList.Extensions), ShouldEqual, 1) So(len(extensionList.Extensions), ShouldEqual, 1)
So(len(extensionList.Extensions[0].Endpoints), ShouldEqual, 5) So(len(extensionList.Extensions[0].Endpoints), ShouldEqual, 5)
So(extensionList.Extensions[0].Name, ShouldEqual, "_zot") So(extensionList.Extensions[0].Name, ShouldEqual, constants.BaseExtension)
So(extensionList.Extensions[0].URL, ShouldContainSubstring, "_zot.md") So(extensionList.Extensions[0].URL, ShouldContainSubstring, "_zot.md")
So(extensionList.Extensions[0].Description, ShouldNotBeEmpty) So(extensionList.Extensions[0].Description, ShouldNotBeEmpty)
// Verify the endpoints below are enabled by search // Verify the endpoints below are enabled by search
@ -8539,7 +8539,7 @@ func TestDistSpecExtensions(t *testing.T) {
t.Log(extensionList.Extensions) t.Log(extensionList.Extensions)
So(len(extensionList.Extensions), ShouldEqual, 1) So(len(extensionList.Extensions), ShouldEqual, 1)
So(len(extensionList.Extensions[0].Endpoints), ShouldEqual, 2) So(len(extensionList.Extensions[0].Endpoints), ShouldEqual, 2)
So(extensionList.Extensions[0].Name, ShouldEqual, "_zot") So(extensionList.Extensions[0].Name, ShouldEqual, constants.BaseExtension)
So(extensionList.Extensions[0].URL, ShouldContainSubstring, "_zot.md") So(extensionList.Extensions[0].URL, ShouldContainSubstring, "_zot.md")
So(extensionList.Extensions[0].Description, ShouldNotBeEmpty) So(extensionList.Extensions[0].Description, ShouldNotBeEmpty)
// Verify the endpoints below are enabled by search // Verify the endpoints below are enabled by search

View file

@ -717,7 +717,7 @@ func (rh *RouteHandler) UpdateManifest(response http.ResponseWriter, request *ht
rh.c.Log.Error().Err(err).Msg("unexpected error: performing cleanup") rh.c.Log.Error().Err(err).Msg("unexpected error: performing cleanup")
if err = imgStore.DeleteImageManifest(name, reference, false); err != nil { if err = imgStore.DeleteImageManifest(name, reference, false); err != nil {
// deletion of image manifest is important, but not critical for image repo consistancy // deletion of image manifest is important, but not critical for image repo consistency
// in the worst scenario a partial manifest file written to disk will not affect the repo because // in the worst scenario a partial manifest file written to disk will not affect the repo because
// the new manifest was not added to "index.json" file (it is possible that GC will take care of it) // the new manifest was not added to "index.json" file (it is possible that GC will take care of it)
rh.c.Log.Error().Err(err).Str("repository", name).Str("reference", reference). rh.c.Log.Error().Err(err).Str("repository", name).Str("reference", reference).

View file

@ -8,7 +8,9 @@ import "github.com/spf13/cobra"
func enableCli(rootCmd *cobra.Command) { func enableCli(rootCmd *cobra.Command) {
rootCmd.AddCommand(NewConfigCommand()) rootCmd.AddCommand(NewConfigCommand())
rootCmd.AddCommand(NewImageCommand(NewSearchService())) rootCmd.AddCommand(NewImageCommand(NewSearchService()))
rootCmd.AddCommand(NewImagesCommand(NewSearchService()))
rootCmd.AddCommand(NewCveCommand(NewSearchService())) rootCmd.AddCommand(NewCveCommand(NewSearchService()))
rootCmd.AddCommand(NewCVESCommand(NewSearchService()))
rootCmd.AddCommand(NewRepoCommand(NewSearchService())) rootCmd.AddCommand(NewRepoCommand(NewSearchService()))
rootCmd.AddCommand(NewSearchCommand(NewSearchService())) rootCmd.AddCommand(NewSearchCommand(NewSearchService()))
} }

View file

@ -7,7 +7,6 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@ -21,7 +20,7 @@ import (
ispec "github.com/opencontainers/image-spec/specs-go/v1" ispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sigstore/cosign/v2/pkg/oci/remote" "github.com/sigstore/cosign/v2/pkg/oci/remote"
zotErrors "zotregistry.io/zot/errors" zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/common" "zotregistry.io/zot/pkg/common"
) )
@ -124,19 +123,20 @@ func doHTTPRequest(req *http.Request, verifyTLS bool, debug bool,
if debug { if debug {
fmt.Fprintln(configWriter, "[debug] ", req.Method, req.URL, "[status] ", fmt.Fprintln(configWriter, "[debug] ", req.Method, req.URL, "[status] ",
resp.StatusCode, " ", "[respoonse header] ", resp.Header) resp.StatusCode, " ", "[response header] ", resp.Header)
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
if resp.StatusCode == http.StatusUnauthorized { if resp.StatusCode == http.StatusUnauthorized {
return nil, zotErrors.ErrUnauthorizedAccess return nil, zerr.ErrUnauthorizedAccess
} }
bodyBytes, _ := io.ReadAll(resp.Body) bodyBytes, _ := io.ReadAll(resp.Body)
return nil, errors.New(string(bodyBytes)) //nolint: goerr113 return nil, fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusOK,
resp.StatusCode, string(bodyBytes))
} }
if resultsPtr == nil { if resultsPtr == nil {

13
pkg/cli/cmdflags/flags.go Normal file
View file

@ -0,0 +1,13 @@
package cmdflags
const (
URLFlag = "url"
ConfigFlag = "config"
UserFlag = "user"
OutputFormatFlag = "format"
FixedFlag = "fixed"
VerboseFlag = "verbose"
VersionFlag = "version"
DebugFlag = "debug"
SearchedCVEID = "cve-id"
)

View file

@ -40,7 +40,7 @@ func NewConfigCommand() *cobra.Command {
panic(err) panic(err)
} }
configPath := path.Join(home + "/.zot") configPath := path.Join(home, "/.zot")
switch len(args) { switch len(args) {
case noArgs: case noArgs:
if isListing { // zot config -l if isListing { // zot config -l
@ -115,7 +115,7 @@ func NewConfigAddCommand() *cobra.Command {
panic(err) panic(err)
} }
configPath := path.Join(home + "/.zot") configPath := path.Join(home, "/.zot")
// zot config add <config-name> <url> // zot config add <config-name> <url>
err = addConfig(configPath, args[0], args[1]) err = addConfig(configPath, args[0], args[1])
if err != nil { if err != nil {
@ -145,7 +145,7 @@ func NewConfigRemoveCommand() *cobra.Command {
panic(err) panic(err)
} }
configPath := path.Join(home + "/.zot") configPath := path.Join(home, "/.zot")
// zot config add <config-name> <url> // zot config add <config-name> <url>
err = removeConfig(configPath, args[0]) err = removeConfig(configPath, args[0])
if err != nil { if err != nil {

View file

@ -13,7 +13,7 @@ import (
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
zotErrors "zotregistry.io/zot/errors" zerr "zotregistry.io/zot/errors"
) )
func TestConfigCmdBasics(t *testing.T) { func TestConfigCmdBasics(t *testing.T) {
@ -146,7 +146,7 @@ func TestConfigCmdMain(t *testing.T) {
cmd.SetErr(buff) cmd.SetErr(buff)
cmd.SetArgs(args) cmd.SetArgs(args)
err := cmd.Execute() err := cmd.Execute()
So(err, ShouldEqual, zotErrors.ErrCliBadConfig) So(err, ShouldEqual, zerr.ErrCliBadConfig)
}) })
Convey("Test add config with invalid URL", t, func() { Convey("Test add config with invalid URL", t, func() {
@ -160,7 +160,7 @@ func TestConfigCmdMain(t *testing.T) {
cmd.SetArgs(args) cmd.SetArgs(args)
err := cmd.Execute() err := cmd.Execute()
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
So(err, ShouldEqual, zotErrors.ErrInvalidURL) So(err, ShouldEqual, zerr.ErrInvalidURL)
}) })
Convey("Test remove config entry successfully", t, func() { Convey("Test remove config entry successfully", t, func() {

View file

@ -13,7 +13,7 @@ import (
"github.com/briandowns/spinner" "github.com/briandowns/spinner"
"github.com/spf13/cobra" "github.com/spf13/cobra"
zotErrors "zotregistry.io/zot/errors" zerr "zotregistry.io/zot/errors"
) )
const ( const (
@ -29,15 +29,15 @@ func NewCveCommand(searchService SearchService) *cobra.Command {
cveCmd := &cobra.Command{ cveCmd := &cobra.Command{
Use: "cve [config-name]", Use: "cve [config-name]",
Short: "Lookup CVEs in images hosted on the zot registry", Short: "DEPRECATED (see cves)",
Long: `List CVEs (Common Vulnerabilities and Exposures) of images hosted on the zot registry`, Long: `DEPRECATED (see cves)! List CVEs (Common Vulnerabilities and Exposures) of images hosted on the zot registry`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
home, err := os.UserHomeDir() home, err := os.UserHomeDir()
if err != nil { if err != nil {
panic(err) panic(err)
} }
configPath := path.Join(home + "/.zot") configPath := path.Join(home, "/.zot")
if servURL == "" { if servURL == "" {
if len(args) > 0 { if len(args) > 0 {
urlFromConfig, err := getConfigValue(configPath, args[0], "url") urlFromConfig, err := getConfigValue(configPath, args[0], "url")
@ -48,12 +48,12 @@ func NewCveCommand(searchService SearchService) *cobra.Command {
} }
if urlFromConfig == "" { if urlFromConfig == "" {
return zotErrors.ErrNoURLProvided return zerr.ErrNoURLProvided
} }
servURL = urlFromConfig servURL = urlFromConfig
} else { } else {
return zotErrors.ErrNoURLProvided return zerr.ErrNoURLProvided
} }
} }
@ -177,7 +177,7 @@ func searchCve(searchConfig searchConfig) error {
return err return err
} }
if strings.Contains(err.Error(), zotErrors.ErrCVEDBNotFound.Error()) { if strings.Contains(err.Error(), zerr.ErrCVEDBNotFound.Error()) {
// searches matches search config but CVE DB is not ready server side // searches matches search config but CVE DB is not ready server side
// wait and retry a few more times // wait and retry a few more times
fmt.Fprintln(searchConfig.resultWriter, fmt.Fprintln(searchConfig.resultWriter,
@ -192,5 +192,5 @@ func searchCve(searchConfig searchConfig) error {
} }
} }
return zotErrors.ErrInvalidFlagsCombination return zerr.ErrInvalidFlagsCombination
} }

View file

@ -26,9 +26,10 @@ import (
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
"github.com/spf13/cobra" "github.com/spf13/cobra"
zotErrors "zotregistry.io/zot/errors" zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/cli/cmdflags"
zcommon "zotregistry.io/zot/pkg/common" zcommon "zotregistry.io/zot/pkg/common"
extconf "zotregistry.io/zot/pkg/extensions/config" extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/extensions/monitoring" "zotregistry.io/zot/pkg/extensions/monitoring"
@ -82,7 +83,7 @@ func TestSearchCVECmd(t *testing.T) {
cmd.SetArgs(args) cmd.SetArgs(args)
err := cmd.Execute() err := cmd.Execute()
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
So(err, ShouldEqual, zotErrors.ErrNoURLProvided) So(err, ShouldEqual, zerr.ErrNoURLProvided)
}) })
Convey("Test CVE no params", t, func() { Convey("Test CVE no params", t, func() {
@ -95,7 +96,7 @@ func TestSearchCVECmd(t *testing.T) {
cmd.SetErr(buff) cmd.SetErr(buff)
cmd.SetArgs(args) cmd.SetArgs(args)
err := cmd.Execute() err := cmd.Execute()
So(err, ShouldEqual, zotErrors.ErrInvalidFlagsCombination) So(err, ShouldEqual, zerr.ErrInvalidFlagsCombination)
}) })
Convey("Test CVE invalid url", t, func() { Convey("Test CVE invalid url", t, func() {
@ -109,7 +110,7 @@ func TestSearchCVECmd(t *testing.T) {
cmd.SetArgs(args) cmd.SetArgs(args)
err := cmd.Execute() err := cmd.Execute()
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
So(err, ShouldEqual, zotErrors.ErrInvalidURL) So(err, ShouldEqual, zerr.ErrInvalidURL)
So(buff.String(), ShouldContainSubstring, "invalid URL format") So(buff.String(), ShouldContainSubstring, "invalid URL format")
}) })
@ -285,7 +286,7 @@ func TestSearchCVECmd(t *testing.T) {
err := cveCmd.Execute() err := cveCmd.Execute()
space := regexp.MustCompile(`\s+`) space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ") str := space.ReplaceAllString(buff.String(), " ")
So(strings.TrimSpace(str), ShouldEqual, "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE anImage tag os/arch 6e2f80bf false 123kB") //nolint:lll So(strings.TrimSpace(str), ShouldContainSubstring, "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE anImage tag os/arch 6e2f80bf false 123kB") //nolint:lll
So(err, ShouldBeNil) So(err, ShouldBeNil)
}) })
@ -355,7 +356,7 @@ func TestSearchCVECmd(t *testing.T) {
cveCmd.SetArgs(args) cveCmd.SetArgs(args)
err := cveCmd.Execute() err := cveCmd.Execute()
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
So(err, ShouldEqual, zotErrors.ErrInvalidURL) So(err, ShouldEqual, zerr.ErrInvalidURL)
So(buff.String(), ShouldContainSubstring, "invalid URL format") So(buff.String(), ShouldContainSubstring, "invalid URL format")
}) })
@ -1058,6 +1059,330 @@ func TestServerCVEResponse(t *testing.T) {
}) })
} }
func TestCVECommandGQL(t *testing.T) {
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
defaultVal := true
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
},
}
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(conf.HTTP.Port)
defer cm.StopServer()
Convey("commands without gql", t, func() {
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, baseURL))
defer os.Remove(configPath)
Convey("cveid", func() {
args := []string{"cveid", "CVE-1942"}
cmd := NewCVESCommand(mockService{})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "")
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 6e2f80bf false 123kB")
})
Convey("cveid db download wait", func() {
count := 0
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`,
baseURL))
args := []string{"cveid", "CVE-12345"}
defer os.Remove(configPath)
cmd := NewCVESCommand(mockService{
getTagsForCVEGQLFn: func(ctx context.Context, config searchConfig, username, password,
imageName, cveID string) (*zcommon.ImagesForCve, error,
) {
if count == 0 {
count++
fmt.Println("Count:", count)
return &zcommon.ImagesForCve{}, zerr.ErrCVEDBNotFound
}
return &zcommon.ImagesForCve{}, zerr.ErrInjected
},
})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "")
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "[warning] CVE DB is not ready")
})
Convey("fixed", func() {
args := []string{"fixed", "image-name", "CVE-123"}
cmd := NewCVESCommand(mockService{})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "")
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 6e2f80bf false 123kB")
})
Convey("fixed db download wait", func() {
count := 0
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`,
baseURL))
args := []string{"fixed", "repo", "CVE-2222"}
defer os.Remove(configPath)
cmd := NewCVESCommand(mockService{
getFixedTagsForCVEGQLFn: func(ctx context.Context, config searchConfig, username, password,
imageName, cveID string) (*zcommon.ImageListWithCVEFixedResponse, error,
) {
if count == 0 {
count++
fmt.Println("Count:", count)
return &zcommon.ImageListWithCVEFixedResponse{}, zerr.ErrCVEDBNotFound
}
return &zcommon.ImageListWithCVEFixedResponse{}, zerr.ErrInjected
},
})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "")
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "[warning] CVE DB is not ready")
})
Convey("image", func() {
args := []string{"image", "repo:tag"}
cmd := NewCVESCommand(mockService{})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "")
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, "dummyCVEID HIGH Title of that CVE")
})
Convey("image db download wait", func() {
count := 0
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`,
baseURL))
args := []string{"image", "repo:vuln"}
defer os.Remove(configPath)
cmd := NewCVESCommand(mockService{
getCveByImageGQLFn: func(ctx context.Context, config searchConfig, username, password,
imageName, searchedCVE string) (*cveResult, error,
) {
if count == 0 {
count++
fmt.Println("Count:", count)
return &cveResult{}, zerr.ErrCVEDBNotFound
}
return &cveResult{}, zerr.ErrInjected
},
})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "")
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "[warning] CVE DB is not ready")
})
})
}
func TestCVECommandREST(t *testing.T) {
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(conf.HTTP.Port)
defer cm.StopServer()
Convey("commands without gql", t, func() {
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, baseURL))
defer os.Remove(configPath)
Convey("cveid", func() {
args := []string{"cveid", "CVE-1942"}
cmd := NewCVESCommand(mockService{})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "")
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("cveid error", func() {
// too many args
args := []string{"too", "many", "args"}
cmd := NewImagesByCVEIDCommand(mockService{})
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
// bad args
args = []string{"not-a-cve-id"}
cmd = NewImagesByCVEIDCommand(mockService{})
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
// no URL
args = []string{"CVE-1942"}
cmd = NewImagesByCVEIDCommand(mockService{})
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("fixed command", func() {
args := []string{"fixed", "image-name", "CVE-123"}
cmd := NewCVESCommand(mockService{})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "")
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("fixed command error", func() {
// too many args
args := []string{"too", "many", "args", "args"}
cmd := NewFixedTagsCommand(mockService{})
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
// bad args
args = []string{"repo-tag-instead-of-just-repo:fail-here", "CVE-123"}
cmd = NewFixedTagsCommand(mockService{})
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
// no URL
args = []string{"CVE-1942"}
cmd = NewFixedTagsCommand(mockService{})
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("image", func() {
args := []string{"image", "repo:tag"}
cmd := NewCVESCommand(mockService{})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "")
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("image command error", func() {
// too many args
args := []string{"too", "many", "args", "args"}
cmd := NewCveForImageCommand(mockService{})
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
// bad args
args = []string{"repo-tag-instead-of-just-repo:fail-here", "CVE-123"}
cmd = NewCveForImageCommand(mockService{})
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
// no URL
args = []string{"CVE-1942"}
cmd = NewCveForImageCommand(mockService{})
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
})
})
}
func MockNewCveCommand(searchService SearchService) *cobra.Command { func MockNewCveCommand(searchService SearchService) *cobra.Command {
searchCveParams := make(map[string]*string) searchCveParams := make(map[string]*string)
@ -1072,7 +1397,7 @@ func MockNewCveCommand(searchService SearchService) *cobra.Command {
panic(err) panic(err)
} }
configPath := path.Join(home + "/.zot") configPath := path.Join(home, "/.zot")
if len(args) > 0 { if len(args) > 0 {
urlFromConfig, err := getConfigValue(configPath, args[0], "url") urlFromConfig, err := getConfigValue(configPath, args[0], "url")
if err != nil { if err != nil {
@ -1082,12 +1407,12 @@ func MockNewCveCommand(searchService SearchService) *cobra.Command {
} }
if urlFromConfig == "" { if urlFromConfig == "" {
return zotErrors.ErrNoURLProvided return zerr.ErrNoURLProvided
} }
servURL = urlFromConfig servURL = urlFromConfig
} else { } else {
return zotErrors.ErrNoURLProvided return zerr.ErrNoURLProvided
} }
if len(args) > 0 { if len(args) > 0 {
@ -1157,7 +1482,7 @@ func MockSearchCve(searchConfig searchConfig) error {
} }
} }
return zotErrors.ErrInvalidFlagsCombination return zerr.ErrInvalidFlagsCombination
} }
func getMockCveInfo(metaDB mTypes.MetaDB, log log.Logger) cveinfo.CveInfo { func getMockCveInfo(metaDB mTypes.MetaDB, log log.Logger) cveinfo.CveInfo {
@ -1232,7 +1557,7 @@ func getMockCveInfo(metaDB mTypes.MetaDB, log log.Logger) cveinfo.CveInfo {
descriptor, ok := repoMeta.Tags[inputTag] descriptor, ok := repoMeta.Tags[inputTag]
if !ok { if !ok {
return false, zotErrors.ErrTagMetaNotFound return false, zerr.ErrTagMetaNotFound
} }
manifestDigestStr = descriptor.Digest manifestDigestStr = descriptor.Digest
@ -1252,7 +1577,7 @@ func getMockCveInfo(metaDB mTypes.MetaDB, log log.Logger) cveinfo.CveInfo {
err = json.Unmarshal(manifestData.ManifestBlob, &manifestContent) err = json.Unmarshal(manifestData.ManifestBlob, &manifestContent)
if err != nil { if err != nil {
return false, zotErrors.ErrScanNotSupported return false, zerr.ErrScanNotSupported
} }
for _, imageLayer := range manifestContent.Layers { for _, imageLayer := range manifestContent.Layers {
@ -1262,7 +1587,7 @@ func getMockCveInfo(metaDB mTypes.MetaDB, log log.Logger) cveinfo.CveInfo {
return true, nil return true, nil
default: default:
return false, zotErrors.ErrScanNotSupported return false, zerr.ErrScanNotSupported
} }
} }
@ -1284,12 +1609,12 @@ type mockServiceForRetry struct {
} }
func (service *mockServiceForRetry) getImagesByCveID(ctx context.Context, config searchConfig, func (service *mockServiceForRetry) getImagesByCveID(ctx context.Context, config searchConfig,
username, password, cvid string, rch chan stringResult, wtgrp *sync.WaitGroup, username, password, cveid string, rch chan stringResult, wtgrp *sync.WaitGroup,
) { ) {
service.retryCounter += 1 service.retryCounter += 1
if service.retryCounter < service.succeedOn || service.succeedOn < 0 { if service.retryCounter < service.succeedOn || service.succeedOn < 0 {
rch <- stringResult{"", zotErrors.ErrCVEDBNotFound} rch <- stringResult{"", zerr.ErrCVEDBNotFound}
close(rch) close(rch)
wtgrp.Done() wtgrp.Done()

30
pkg/cli/cves_cmd.go Normal file
View file

@ -0,0 +1,30 @@
//go:build search
// +build search
package cli
import (
"github.com/spf13/cobra"
"zotregistry.io/zot/pkg/cli/cmdflags"
)
func NewCVESCommand(searchService SearchService) *cobra.Command {
cvesCmd := &cobra.Command{
Use: "cves [command]",
Short: "Lookup CVEs in images hosted on the zot registry",
Long: `List CVEs (Common Vulnerabilities and Exposures) of images hosted on the zot registry`,
}
cvesCmd.SetUsageTemplate(cvesCmd.UsageTemplate() + usageFooter)
cvesCmd.PersistentFlags().StringP(cmdflags.OutputFormatFlag, "f", "", "Specify output format [text/json/yaml]")
cvesCmd.PersistentFlags().Bool(cmdflags.VerboseFlag, false, "Show verbose output")
cvesCmd.PersistentFlags().Bool(cmdflags.DebugFlag, false, "Show debug output")
cvesCmd.AddCommand(NewCveForImageCommand(searchService))
cvesCmd.AddCommand(NewImagesByCVEIDCommand(searchService))
cvesCmd.AddCommand(NewFixedTagsCommand(searchService))
return cvesCmd
}

128
pkg/cli/cves_sub_cmd.go Normal file
View file

@ -0,0 +1,128 @@
//go:build search
// +build search
package cli
import (
"fmt"
"strings"
"github.com/spf13/cobra"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/cli/cmdflags"
zcommon "zotregistry.io/zot/pkg/common"
)
const (
maxRetries = 20
)
func NewCveForImageCommand(searchService SearchService) *cobra.Command {
var searchedCVEID string
cveForImageCmd := &cobra.Command{
Use: "image [repo:tag]|[repo@digest]",
Short: "List CVEs by REPO:TAG or REPO@DIGEST",
Long: `List CVEs by REPO:TAG or REPO@DIGEST`,
Args: OneImageWithRefArg,
RunE: func(cmd *cobra.Command, args []string) error {
searchConfig, err := GetSearchConfigFromFlags(cmd, searchService)
if err != nil {
return err
}
err = CheckExtEndPointQuery(searchConfig, CVEListForImageQuery())
if err != nil {
return fmt.Errorf("%w: '%s'", err, CVEListForImageQuery().Name)
}
image := args[0]
return SearchCVEForImageGQL(searchConfig, image, searchedCVEID)
},
}
cveForImageCmd.Flags().StringVar(&searchedCVEID, cmdflags.SearchedCVEID, "", "Search for a specific CVE by name/id")
return cveForImageCmd
}
func NewImagesByCVEIDCommand(searchService SearchService) *cobra.Command {
var repo string
imagesByCVEIDCmd := &cobra.Command{
Use: "cveid [cveId]",
Short: "List images affected by a CVE",
Long: `List images affected by a CVE`,
Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.ExactArgs(1)(cmd, args); err != nil {
return err
}
if !strings.HasPrefix(args[0], "CVE") {
return fmt.Errorf("%w: expected a cveid 'CVE-...' got '%s'", zerr.ErrInvalidCLIParameter, args[0])
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
searchConfig, err := GetSearchConfigFromFlags(cmd, searchService)
if err != nil {
return err
}
err = CheckExtEndPointQuery(searchConfig, ImageListForCVEQuery())
if err != nil {
return fmt.Errorf("%w: '%s'", err, ImageListForCVEQuery().Name)
}
searchedCVEID := args[0]
return SearchImagesByCVEIDGQL(searchConfig, repo, searchedCVEID)
},
}
imagesByCVEIDCmd.Flags().StringVar(&repo, "repo", "", "Search for a specific CVE by name/id")
return imagesByCVEIDCmd
}
func NewFixedTagsCommand(searchService SearchService) *cobra.Command {
fixedTagsCmd := &cobra.Command{
Use: "fixed [repo] [cveId]",
Short: "List tags where a CVE is fixedRetryWithContext",
Long: `List tags where a CVE is fixedRetryWithContext`,
Args: func(cmd *cobra.Command, args []string) error {
const argCount = 2
if err := cobra.ExactArgs(argCount)(cmd, args); err != nil {
return err
}
if !zcommon.CheckIsCorrectRepoNameFormat(args[0]) {
return fmt.Errorf("%w: expected a valid repo name for first argument '%s'", zerr.ErrInvalidCLIParameter, args[0])
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
searchConfig, err := GetSearchConfigFromFlags(cmd, searchService)
if err != nil {
return err
}
err = CheckExtEndPointQuery(searchConfig, ImageListWithCVEFixedQuery())
if err != nil {
return fmt.Errorf("%w: '%s'", err, ImageListWithCVEFixedQuery().Name)
}
repo := args[0]
searchedCVEID := args[1]
return SearchFixedTagsGQL(searchConfig, repo, searchedCVEID)
},
}
return fixedTagsCmd
}

View file

@ -9,12 +9,16 @@ import (
distext "github.com/opencontainers/distribution-spec/specs-go/v1/extensions" distext "github.com/opencontainers/distribution-spec/specs-go/v1/extensions"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api/constants" "zotregistry.io/zot/pkg/api/constants"
"zotregistry.io/zot/pkg/common" zcommon "zotregistry.io/zot/pkg/common"
) )
type field struct { type field struct {
Name string `json:"name"` Name string `json:"name"`
Args []struct {
Name string `json:"name"`
} `json:"args"`
} }
type schemaList struct { type schemaList struct {
@ -23,9 +27,19 @@ type schemaList struct {
QueryType struct { QueryType struct {
Fields []field `json:"fields"` Fields []field `json:"fields"`
} `json:"queryType"` //nolint:tagliatelle // graphQL schema } `json:"queryType"` //nolint:tagliatelle // graphQL schema
Types []typeInfo `json:"types"`
} `json:"__schema"` //nolint:tagliatelle // graphQL schema } `json:"__schema"` //nolint:tagliatelle // graphQL schema
} `json:"data"` } `json:"data"`
Errors []common.ErrorGQL `json:"errors"` Errors []zcommon.ErrorGQL `json:"errors"`
}
type typeInfo struct {
Name string `json:"name"`
Fields []typeField `json:"fields"`
}
type typeField struct {
Name string `json:"name"`
} }
func containsGQLQuery(queryList []field, query string) bool { func containsGQLQuery(queryList []field, query string) bool {
@ -38,6 +52,54 @@ func containsGQLQuery(queryList []field, query string) bool {
return false return false
} }
func containsGQLQueryWithParams(queryList []field, serverGQLTypesList []typeInfo, requiredQueries ...GQLQuery) error {
serverGQLTypes := map[string][]typeField{}
for _, typeInfo := range serverGQLTypesList {
serverGQLTypes[typeInfo.Name] = typeInfo.Fields
}
for _, reqQuery := range requiredQueries {
foundQuery := false
for _, query := range queryList {
if query.Name == reqQuery.Name && haveSameArgs(query, reqQuery) {
foundQuery = true
}
}
if !foundQuery {
return fmt.Errorf("%w: %s", zerr.ErrGQLQueryNotSupported, reqQuery.Name)
}
// let's check just the name of the returned type
returnType := reqQuery.ReturnType.Name
// we can next define fields of the returned types and check them recursively
// for now we will just check the name of the returned type to be known by the server
_, ok := serverGQLTypes[returnType]
if !ok {
return fmt.Errorf("%w: server doesn't support needed type '%s'", zerr.ErrGQLQueryNotSupported, returnType)
}
}
return nil
}
func haveSameArgs(query field, reqQuery GQLQuery) bool {
if len(query.Args) != len(reqQuery.Args) {
return false
}
for i := range query.Args {
if query.Args[i].Name != reqQuery.Args[i] {
return false
}
}
return true
}
func checkExtEndPoint(config searchConfig) bool { func checkExtEndPoint(config searchConfig) bool {
username, password := getUsernameAndPassword(*config.user) username, password := getUsernameAndPassword(*config.user)
ctx := context.Background() ctx := context.Background()
@ -59,7 +121,7 @@ func checkExtEndPoint(config searchConfig) bool {
searchEnabled := false searchEnabled := false
for _, extension := range discoverResponse.Extensions { for _, extension := range discoverResponse.Extensions {
if extension.Name == "_zot" { if extension.Name == constants.BaseExtension {
for _, endpoint := range extension.Endpoints { for _, endpoint := range extension.Endpoints {
if endpoint == constants.FullSearchPrefix { if endpoint == constants.FullSearchPrefix {
searchEnabled = true searchEnabled = true
@ -99,3 +161,80 @@ func checkExtEndPoint(config searchConfig) bool {
return containsGQLQuery(queryResponse.Data.Schema.QueryType.Fields, "ImageList") return containsGQLQuery(queryResponse.Data.Schema.QueryType.Fields, "ImageList")
} }
func CheckExtEndPointQuery(config searchConfig, requiredQueries ...GQLQuery) error {
username, password := getUsernameAndPassword(*config.user)
ctx := context.Background()
discoverEndPoint, err := combineServerAndEndpointURL(*config.servURL, fmt.Sprintf("%s%s",
constants.RoutePrefix, constants.ExtOciDiscoverPrefix))
if err != nil {
return err
}
discoverResponse := &distext.ExtensionList{}
_, err = makeGETRequest(ctx, discoverEndPoint, username, password, *config.verifyTLS,
*config.debug, &discoverResponse, config.resultWriter)
if err != nil {
return err
}
searchEnabled := false
for _, extension := range discoverResponse.Extensions {
if extension.Name == constants.BaseExtension {
for _, endpoint := range extension.Endpoints {
if endpoint == constants.FullSearchPrefix {
searchEnabled = true
}
}
}
}
if !searchEnabled {
return fmt.Errorf("%w: search extension gql endpoints not found", zerr.ErrExtensionNotEnabled)
}
searchEndPoint, _ := combineServerAndEndpointURL(*config.servURL, constants.FullSearchPrefix)
schemaQuery := `
{
__schema() {
queryType {
fields {
name
args {
name
}
type {
name
kind
}
}
__typename
}
types {
name
fields {
name
}
}
}
}`
queryResponse := &schemaList{}
err = makeGraphQLRequest(ctx, searchEndPoint, schemaQuery, username, password, *config.verifyTLS,
*config.debug, queryResponse, config.resultWriter)
if err != nil {
return fmt.Errorf("gql query failed: %w", err)
}
if err = checkResultGraphQLQuery(ctx, err, queryResponse.Errors); err != nil {
return fmt.Errorf("gql query failed: %w", err)
}
return containsGQLQueryWithParams(queryResponse.Data.Schema.QueryType.Fields,
queryResponse.Data.Schema.Types, requiredQueries...)
}

116
pkg/cli/gql_queries.go Normal file
View file

@ -0,0 +1,116 @@
//go:build search
// +build search
package cli
type GQLField struct {
Name string
Type GQLType
}
type GQLType struct {
Name string
Fields []GQLField
}
type GQLQuery struct {
Name string
Args []string
ReturnType GQLType
}
func CVEResultForImage() GQLType {
return GQLType{
Name: "CVEResultForImage",
}
}
func PaginatedImagesResult() GQLType {
return GQLType{
Name: "PaginatedImagesResult",
}
}
func Referrer() GQLType {
return GQLType{
Name: "Referrer",
}
}
func GlobalSearchResult() GQLType {
return GQLType{
Name: "GlobalSearchResult",
}
}
func ImageListQuery() GQLQuery {
return GQLQuery{
Name: "ImageList",
Args: []string{"repo", "requestedPage"},
ReturnType: PaginatedImagesResult(),
}
}
func ImageListForDigestQuery() GQLQuery {
return GQLQuery{
Name: "ImageListForDigest",
Args: []string{"id", "requestedPage"},
ReturnType: PaginatedImagesResult(),
}
}
func BaseImageListQuery() GQLQuery {
return GQLQuery{
Name: "BaseImageList",
Args: []string{"image", "digest", "requestedPage"},
ReturnType: PaginatedImagesResult(),
}
}
func DerivedImageListQuery() GQLQuery {
return GQLQuery{
Name: "DerivedImageList",
Args: []string{"image", "digest", "requestedPage"},
ReturnType: PaginatedImagesResult(),
}
}
func CVEListForImageQuery() GQLQuery {
return GQLQuery{
Name: "CVEListForImage",
Args: []string{"image", "requestedPage", "searchedCVE"},
ReturnType: CVEResultForImage(),
}
}
func ImageListForCVEQuery() GQLQuery {
return GQLQuery{
Name: "ImageListForCVE",
Args: []string{"id", "filter", "requestedPage"},
ReturnType: PaginatedImagesResult(),
}
}
func ImageListWithCVEFixedQuery() GQLQuery {
return GQLQuery{
Name: "ImageListWithCVEFixed",
Args: []string{"id", "image", "filter", "requestedPage"},
ReturnType: PaginatedImagesResult(),
}
}
func ReferrersQuery() GQLQuery {
return GQLQuery{
Name: "Referrers",
Args: []string{"repo", "digest", "type"},
ReturnType: Referrer(),
}
}
func GlobalSearchQuery() GQLQuery {
return GQLQuery{
Name: "GlobalSearch",
Args: []string{"query", "filter", "requestedPage"},
ReturnType: GlobalSearchResult(),
}
}

View file

@ -0,0 +1,93 @@
//go:build search
// +build search
package cli //nolint:testpackage
import (
"io"
"testing"
. "github.com/smartystreets/goconvey/convey"
"zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config"
extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/test"
)
func TestGQLQueries(t *testing.T) {
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
dir := t.TempDir()
conf.Storage.RootDirectory = dir
defaultVal := true
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
},
}
ctlr := api.NewController(conf)
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(conf.HTTP.Port)
defer cm.StopServer()
searchConfig := searchConfig{
servURL: &baseURL,
user: ref(""),
verifyTLS: ref(false),
debug: ref(false),
resultWriter: io.Discard,
}
Convey("Make sure the current CLI used the right queries in case they change", t, func() {
Convey("ImageList", func() {
err := CheckExtEndPointQuery(searchConfig, ImageListQuery())
So(err, ShouldBeNil)
})
Convey("ImageListForDigest", func() {
err := CheckExtEndPointQuery(searchConfig, ImageListForDigestQuery())
So(err, ShouldBeNil)
})
Convey("BaseImageList", func() {
err := CheckExtEndPointQuery(searchConfig, BaseImageListQuery())
So(err, ShouldBeNil)
})
Convey("DerivedImageList", func() {
err := CheckExtEndPointQuery(searchConfig, DerivedImageListQuery())
So(err, ShouldBeNil)
})
Convey("CVEListForImage", func() {
err := CheckExtEndPointQuery(searchConfig, CVEListForImageQuery())
So(err, ShouldBeNil)
})
Convey("ImageListForCVE", func() {
err := CheckExtEndPointQuery(searchConfig, ImageListForCVEQuery())
So(err, ShouldBeNil)
})
Convey("ImageListWithCVEFixed", func() {
err := CheckExtEndPointQuery(searchConfig, ImageListWithCVEFixedQuery())
So(err, ShouldBeNil)
})
Convey("Referrers", func() {
err := CheckExtEndPointQuery(searchConfig, ReferrersQuery())
So(err, ShouldBeNil)
})
Convey("GlobalSearch", func() {
err := CheckExtEndPointQuery(searchConfig, GlobalSearchQuery())
So(err, ShouldBeNil)
})
})
}

View file

@ -12,7 +12,7 @@ import (
"github.com/briandowns/spinner" "github.com/briandowns/spinner"
"github.com/spf13/cobra" "github.com/spf13/cobra"
zotErrors "zotregistry.io/zot/errors" zerr "zotregistry.io/zot/errors"
) )
//nolint:dupl //nolint:dupl
@ -24,16 +24,16 @@ func NewImageCommand(searchService SearchService) *cobra.Command {
var isSpinner, verifyTLS, verbose, debug bool var isSpinner, verifyTLS, verbose, debug bool
imageCmd := &cobra.Command{ imageCmd := &cobra.Command{
Use: "images [config-name]", Use: "image [config-name]",
Short: "List images hosted on the zot registry", Short: "DEPRECATED (see images)",
Long: `List images hosted on the zot registry`, Long: `DEPRECATED (see images)! List images hosted on the zot registry`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
home, err := os.UserHomeDir() home, err := os.UserHomeDir()
if err != nil { if err != nil {
panic(err) panic(err)
} }
configPath := path.Join(home + "/.zot") configPath := path.Join(home, "/.zot")
if servURL == "" { if servURL == "" {
if len(args) > 0 { if len(args) > 0 {
urlFromConfig, err := getConfigValue(configPath, args[0], "url") urlFromConfig, err := getConfigValue(configPath, args[0], "url")
@ -44,12 +44,12 @@ func NewImageCommand(searchService SearchService) *cobra.Command {
} }
if urlFromConfig == "" { if urlFromConfig == "" {
return zotErrors.ErrNoURLProvided return zerr.ErrNoURLProvided
} }
servURL = urlFromConfig servURL = urlFromConfig
} else { } else {
return zotErrors.ErrNoURLProvided return zerr.ErrNoURLProvided
} }
} }
@ -129,11 +129,12 @@ func setupImageFlags(imageCmd *cobra.Command, searchImageParams map[string]*stri
searchImageParams["baseImage"] = imageCmd.Flags().StringP("base-images", "b", "", searchImageParams["baseImage"] = imageCmd.Flags().StringP("base-images", "b", "",
"List images that are base for the given image") "List images that are base for the given image")
imageCmd.Flags().StringVar(servURL, "url", "", "Specify zot server URL if config-name is not mentioned") imageCmd.PersistentFlags().StringVar(servURL, "url", "", "Specify zot server URL if config-name is not mentioned")
imageCmd.Flags().StringVarP(user, "user", "u", "", `User Credentials of zot server in "username:password" format`) imageCmd.PersistentFlags().StringVarP(user, "user", "u", "",
imageCmd.Flags().StringVarP(outputFormat, "output", "o", "", "Specify output format [text/json/yaml]") `User Credentials of zot server in "username:password" format`)
imageCmd.Flags().BoolVar(verbose, "verbose", false, "Show verbose output") imageCmd.PersistentFlags().StringVarP(outputFormat, "output", "o", "", "Specify output format [text/json/yaml]")
imageCmd.Flags().BoolVar(debug, "debug", false, "Show debug output") imageCmd.PersistentFlags().BoolVar(verbose, "verbose", false, "Show verbose output")
imageCmd.PersistentFlags().BoolVar(debug, "debug", false, "Show debug output")
} }
func searchImage(searchConfig searchConfig) error { func searchImage(searchConfig searchConfig) error {
@ -156,7 +157,7 @@ func searchImage(searchConfig searchConfig) error {
} }
} }
return zotErrors.ErrInvalidFlagsCombination return zerr.ErrInvalidFlagsCombination
} }
const ( const (

View file

@ -26,9 +26,10 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gopkg.in/resty.v1" "gopkg.in/resty.v1"
zotErrors "zotregistry.io/zot/errors" zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/cli/cmdflags"
"zotregistry.io/zot/pkg/common" "zotregistry.io/zot/pkg/common"
extconf "zotregistry.io/zot/pkg/extensions/config" extconf "zotregistry.io/zot/pkg/extensions/config"
zlog "zotregistry.io/zot/pkg/log" zlog "zotregistry.io/zot/pkg/log"
@ -74,7 +75,7 @@ func TestSearchImageCmd(t *testing.T) {
cmd.SetArgs(args) cmd.SetArgs(args)
err := cmd.Execute() err := cmd.Execute()
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
So(err, ShouldEqual, zotErrors.ErrNoURLProvided) So(err, ShouldEqual, zerr.ErrNoURLProvided)
}) })
Convey("Test image invalid home directory", t, func() { Convey("Test image invalid home directory", t, func() {
@ -130,7 +131,7 @@ func TestSearchImageCmd(t *testing.T) {
cmd.SetArgs(args) cmd.SetArgs(args)
err := cmd.Execute() err := cmd.Execute()
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
So(err, ShouldEqual, zotErrors.ErrInvalidURL) So(err, ShouldEqual, zerr.ErrInvalidURL)
So(buff.String(), ShouldContainSubstring, "invalid URL format") So(buff.String(), ShouldContainSubstring, "invalid URL format")
}) })
@ -253,7 +254,7 @@ func TestSearchImageCmd(t *testing.T) {
imageCmd.SetArgs(args) imageCmd.SetArgs(args)
err := imageCmd.Execute() err := imageCmd.Execute()
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
So(err, ShouldEqual, zotErrors.ErrInvalidURL) So(err, ShouldEqual, zerr.ErrInvalidURL)
So(buff.String(), ShouldContainSubstring, "invalid URL format") So(buff.String(), ShouldContainSubstring, "invalid URL format")
}) })
}) })
@ -1555,6 +1556,564 @@ func runDisplayIndexTests(baseURL string) {
}) })
} }
func TestImagesCommandGQL(t *testing.T) {
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
defaultVal := true
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
},
}
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(conf.HTTP.Port)
defer cm.StopServer()
Convey("commands with gql", t, func() {
err := test.RemoveLocalStorageContents(ctlr.StoreController.DefaultStore)
So(err, ShouldBeNil)
Convey("base and derived command", func() {
baseImage := test.CreateImageWith().LayerBlobs(
[][]byte{{1, 2, 3}, {11, 22, 33}},
).DefaultConfig().Build()
derivedImage := test.CreateImageWith().LayerBlobs(
[][]byte{{1, 2, 3}, {11, 22, 33}, {44, 55, 66}},
).DefaultConfig().Build()
err := test.UploadImage(baseImage, baseURL, "repo", "base")
So(err, ShouldBeNil)
err = test.UploadImage(derivedImage, baseURL, "repo", "derived")
So(err, ShouldBeNil)
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`,
baseURL))
defer os.Remove(configPath)
args := []string{"base", "repo:derived"}
cmd := NewImagesCommand(NewSearchService())
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "")
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, "repo base linux/amd64 df554ddd false 699B")
args = []string{"derived", "repo:base"}
cmd = NewImagesCommand(NewSearchService())
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "")
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldBeNil)
str = space.ReplaceAllString(buff.String(), " ")
actual = strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "repo derived linux/amd64 79f4b82e false 854B")
})
Convey("base and derived command errors", func() {
// too many parameters
buff := bytes.NewBufferString("")
args := []string{"too", "many", "args"}
cmd := NewImageBaseCommand(NewSearchService())
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
cmd = NewImageDerivedCommand(NewSearchService())
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
// bad input
buff = bytes.NewBufferString("")
args = []string{"only-repo"}
cmd = NewImageBaseCommand(NewSearchService())
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
cmd = NewImageDerivedCommand(NewSearchService())
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
// no url
buff = bytes.NewBufferString("")
args = []string{"repo:tag"}
cmd = NewImageBaseCommand(NewSearchService())
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
cmd = NewImageDerivedCommand(NewSearchService())
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("digest command", func() {
image := test.CreateImageWith().RandomLayers(1, 10).DefaultConfig().Build()
err := test.UploadImage(image, baseURL, "repo", "img")
So(err, ShouldBeNil)
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`,
baseURL))
defer os.Remove(configPath)
args := []string{"digest", image.DigestStr()}
cmd := NewImagesCommand(NewSearchService())
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "")
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, fmt.Sprintf("repo img linux/amd64 %s false 552B",
image.DigestStr()[7:7+8]))
})
Convey("digest command errors", func() {
// too many parameters
buff := bytes.NewBufferString("")
args := []string{"too", "many", "args"}
cmd := NewImageDigestCommand(NewSearchService())
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
// bad input
buff = bytes.NewBufferString("")
args = []string{"bad-digest"}
cmd = NewImageDigestCommand(NewSearchService())
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
// no url
buff = bytes.NewBufferString("")
args = []string{godigest.FromString("str").String()}
cmd = NewImageDigestCommand(NewSearchService())
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("list command", func() {
image := test.CreateImageWith().RandomLayers(1, 10).DefaultConfig().Build()
err := test.UploadImage(image, baseURL, "repo", "img")
So(err, ShouldBeNil)
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`,
baseURL))
defer os.Remove(configPath)
args := []string{"list"}
cmd := NewImagesCommand(NewSearchService())
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "")
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)
fmt.Println(actual)
So(actual, ShouldContainSubstring, fmt.Sprintf("repo img linux/amd64 %s false 552B",
image.DigestStr()[7:7+8]))
fmt.Println(actual)
})
Convey("list command errors", func() {
// too many parameters
buff := bytes.NewBufferString("")
args := []string{"repo:img", "arg"}
cmd := NewImageListCommand(NewSearchService())
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
// no url
buff = bytes.NewBufferString("")
args = []string{}
cmd = NewImageListCommand(NewSearchService())
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("name command", func() {
image := test.CreateImageWith().RandomLayers(1, 10).DefaultConfig().Build()
err := test.UploadImage(image, baseURL, "repo", "img")
So(err, ShouldBeNil)
err = test.UploadImage(test.CreateRandomImage(), baseURL, "repo", "img2")
So(err, ShouldBeNil)
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`,
baseURL))
defer os.Remove(configPath)
args := []string{"name", "repo:img"}
cmd := NewImagesCommand(NewSearchService())
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "")
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)
fmt.Println(actual)
So(actual, ShouldContainSubstring, fmt.Sprintf("repo img linux/amd64 %s false 552B",
image.DigestStr()[7:7+8]))
fmt.Println(actual)
})
Convey("name command errors", func() {
// too many parameters
buff := bytes.NewBufferString("")
args := []string{"repo:img", "arg"}
cmd := NewImageNameCommand(NewSearchService())
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
// bad input
buff = bytes.NewBufferString("")
args = []string{":tag"}
cmd = NewImageNameCommand(NewSearchService())
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
// no url
buff = bytes.NewBufferString("")
args = []string{"repo:tag"}
cmd = NewImageNameCommand(NewSearchService())
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("CVE", func() {
vulnImage := test.CreateDefaultVulnerableImage()
err := test.UploadImage(vulnImage, baseURL, "repo", "vuln")
So(err, ShouldBeNil)
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`,
baseURL))
args := []string{"cve", "repo:vuln"}
defer os.Remove(configPath)
cmd := NewImagesCommand(mockService{})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "")
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, "dummyCVEID HIGH Title of that CVE")
})
Convey("CVE errors", func() {
count := 0
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`,
baseURL))
args := []string{"cve", "repo:vuln"}
defer os.Remove(configPath)
cmd := NewImagesCommand(mockService{
getCveByImageGQLFn: func(ctx context.Context, config searchConfig, username, password,
imageName, searchedCVE string) (*cveResult, error,
) {
if count == 0 {
count++
fmt.Println("Count:", count)
return &cveResult{}, zerr.ErrCVEDBNotFound
}
return &cveResult{}, zerr.ErrInjected
},
})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "")
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "[warning] CVE DB is not ready")
})
})
Convey("Config error", t, func() {
args := []string{"base", "repo:derived"}
cmd := NewImagesCommand(NewSearchService())
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "")
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
So(err, ShouldNotBeNil)
args = []string{"derived", "repo:base"}
cmd = NewImagesCommand(NewSearchService())
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
args = []string{"digest", ispec.DescriptorEmptyJSON.Digest.String()}
cmd = NewImagesCommand(NewSearchService())
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
args = []string{"list"}
cmd = NewImagesCommand(NewSearchService())
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
args = []string{"name", "repo:img"}
cmd = NewImagesCommand(NewSearchService())
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
args = []string{"cve", "repo:vuln"}
cmd = NewImagesCommand(mockService{})
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
})
}
func TestImageCommandREST(t *testing.T) {
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(conf.HTTP.Port)
defer cm.StopServer()
Convey("commands without gql", t, func() {
err := test.RemoveLocalStorageContents(ctlr.StoreController.DefaultStore)
So(err, ShouldBeNil)
Convey("base and derived command", func() {
baseImage := test.CreateImageWith().LayerBlobs(
[][]byte{{1, 2, 3}, {11, 22, 33}},
).DefaultConfig().Build()
derivedImage := test.CreateImageWith().LayerBlobs(
[][]byte{{1, 2, 3}, {11, 22, 33}, {44, 55, 66}},
).DefaultConfig().Build()
err := test.UploadImage(baseImage, baseURL, "repo", "base")
So(err, ShouldBeNil)
err = test.UploadImage(derivedImage, baseURL, "repo", "derived")
So(err, ShouldBeNil)
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`,
baseURL))
defer os.Remove(configPath)
args := []string{"base", "repo:derived"}
cmd := NewImagesCommand(NewSearchService())
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "")
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
args = []string{"derived", "repo:base"}
cmd = NewImagesCommand(NewSearchService())
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "")
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("digest command", func() {
image := test.CreateRandomImage()
err := test.UploadImage(image, baseURL, "repo", "img")
So(err, ShouldBeNil)
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`,
baseURL))
defer os.Remove(configPath)
args := []string{"digest", image.DigestStr()}
cmd := NewImagesCommand(NewSearchService())
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "")
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("list command", func() {
image := test.CreateRandomImage()
err := test.UploadImage(image, baseURL, "repo", "img")
So(err, ShouldBeNil)
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`,
baseURL))
defer os.Remove(configPath)
args := []string{"list"}
cmd := NewImagesCommand(NewSearchService())
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "")
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldBeNil)
fmt.Println(buff.String())
fmt.Println()
})
Convey("name command", func() {
image := test.CreateRandomImage()
err := test.UploadImage(image, baseURL, "repo", "img")
So(err, ShouldBeNil)
err = test.UploadImage(test.CreateRandomImage(), baseURL, "repo", "img2")
So(err, ShouldBeNil)
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`,
baseURL))
defer os.Remove(configPath)
args := []string{"name", "repo:img"}
cmd := NewImagesCommand(NewSearchService())
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "")
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldBeNil)
fmt.Println(buff.String())
fmt.Println()
})
Convey("CVE", func() {
vulnImage := test.CreateDefaultVulnerableImage()
err := test.UploadImage(vulnImage, baseURL, "repo", "vuln")
So(err, ShouldBeNil)
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`,
baseURL))
args := []string{"cve", "repo:vuln"}
defer os.Remove(configPath)
cmd := NewImagesCommand(mockService{})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "")
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
})
})
}
func uploadTestMultiarch(baseURL string) { func uploadTestMultiarch(baseURL string) {
// ------- Define Image1 // ------- Define Image1
layer11 := []byte{11, 12, 13, 14} layer11 := []byte{11, 12, 13, 14}
@ -1606,7 +2165,7 @@ func MockNewImageCommand(searchService SearchService) *cobra.Command {
panic(err) panic(err)
} }
configPath := path.Join(home + "/.zot") configPath := path.Join(home, "/.zot")
if len(args) > 0 { if len(args) > 0 {
urlFromConfig, err := getConfigValue(configPath, args[0], "url") urlFromConfig, err := getConfigValue(configPath, args[0], "url")
if err != nil { if err != nil {
@ -1616,12 +2175,12 @@ func MockNewImageCommand(searchService SearchService) *cobra.Command {
} }
if urlFromConfig == "" { if urlFromConfig == "" {
return zotErrors.ErrNoURLProvided return zerr.ErrNoURLProvided
} }
servURL = urlFromConfig servURL = urlFromConfig
} else { } else {
return zotErrors.ErrNoURLProvided return zerr.ErrNoURLProvided
} }
if len(args) > 0 { if len(args) > 0 {
@ -1679,7 +2238,7 @@ func MockSearchImage(searchConfig searchConfig) error {
} }
} }
return zotErrors.ErrInvalidFlagsCombination return zerr.ErrInvalidFlagsCombination
} }
func uploadManifest(url string) error { func uploadManifest(url string) error {
@ -1895,7 +2454,73 @@ func uploadManifestDerivedBase(url string) error {
return nil return nil
} }
type mockService struct{} type mockService struct {
getAllImagesFn func(ctx context.Context, config searchConfig, username, password string,
channel chan stringResult, wtgrp *sync.WaitGroup)
getImagesGQLFn func(ctx context.Context, config searchConfig, username, password string,
imageName string) (*common.ImageListResponse, error)
getImageByNameFn func(ctx context.Context, config searchConfig,
username, password, imageName string, channel chan stringResult, wtgrp *sync.WaitGroup,
)
getFixedTagsForCVEFn func(ctx context.Context, config searchConfig,
username, password, imageName, cveid string, rch chan stringResult, wtgrp *sync.WaitGroup,
)
getImageByNameAndCVEIDFn func(ctx context.Context, config searchConfig, username,
password, imageName, cveid string, rch chan stringResult, wtgrp *sync.WaitGroup,
)
getImagesByCveIDFn func(ctx context.Context, config searchConfig, username, password, cveid string,
rch chan stringResult, wtgrp *sync.WaitGroup,
)
getImagesByDigestFn func(ctx context.Context, config searchConfig, username,
password, digest string, rch chan stringResult, wtgrp *sync.WaitGroup,
)
getReferrersFn func(ctx context.Context, config searchConfig, username, password string,
repo, digest string,
) (referrersResult, error)
globalSearchGQLFn func(ctx context.Context, config searchConfig, username, password string,
query string,
) (*common.GlobalSearch, error)
getReferrersGQLFn func(ctx context.Context, config searchConfig, username, password string,
repo, digest string,
) (*common.ReferrersResp, error)
getDerivedImageListGQLFn func(ctx context.Context, config searchConfig, username, password string,
derivedImage string,
) (*common.DerivedImageListResponse, error)
getBaseImageListGQLFn func(ctx context.Context, config searchConfig, username, password string,
derivedImage string,
) (*common.BaseImageListResponse, error)
getImagesForDigestGQLFn func(ctx context.Context, config searchConfig, username, password string,
digest string,
) (*common.ImagesForDigest, error)
getCveByImageGQLFn func(ctx context.Context, config searchConfig, username, password,
imageName, searchedCVE string,
) (*cveResult, error)
getImagesByCveIDGQLFn func(ctx context.Context, config searchConfig, username, password string,
digest string,
) (*common.ImagesForCve, error)
getTagsForCVEGQLFn func(ctx context.Context, config searchConfig, username, password,
imageName, cveID string,
) (*common.ImagesForCve, error)
getFixedTagsForCVEGQLFn func(ctx context.Context, config searchConfig, username, password,
imageName, cveID string,
) (*common.ImageListWithCVEFixedResponse, error)
}
func (service mockService) getRepos(ctx context.Context, config searchConfig, username, func (service mockService) getRepos(ctx context.Context, config searchConfig, username,
password string, channel chan stringResult, wtgrp *sync.WaitGroup, password string, channel chan stringResult, wtgrp *sync.WaitGroup,
@ -1903,39 +2528,56 @@ func (service mockService) getRepos(ctx context.Context, config searchConfig, us
defer wtgrp.Done() defer wtgrp.Done()
defer close(channel) defer close(channel)
var catalog [3]string fmt.Fprintln(config.resultWriter, "\n\nREPOSITORY NAME")
catalog[0] = "python"
catalog[1] = "busybox"
catalog[2] = "hello-world"
channel <- stringResult{"", nil} fmt.Fprintln(config.resultWriter, "repo1")
fmt.Fprintln(config.resultWriter, "repo2")
} }
func (service mockService) getReferrers(ctx context.Context, config searchConfig, username, password string, func (service mockService) getReferrers(ctx context.Context, config searchConfig, username, password string,
repo, digest string, repo, digest string,
) (referrersResult, error) { ) (referrersResult, error) {
return referrersResult{}, nil if service.getReferrersFn != nil {
return service.getReferrersFn(ctx, config, username, password, repo, digest)
}
return referrersResult{
common.Referrer{
ArtifactType: "art.type",
Digest: ispec.DescriptorEmptyJSON.Digest.String(),
MediaType: ispec.MediaTypeImageManifest,
Size: 100,
},
}, nil
} }
func (service mockService) globalSearchGQL(ctx context.Context, config searchConfig, username, password string, func (service mockService) globalSearchGQL(ctx context.Context, config searchConfig, username, password string,
query string, query string,
) (*common.GlobalSearch, error) { ) (*common.GlobalSearch, error) {
if service.globalSearchGQLFn != nil {
return service.globalSearchGQLFn(ctx, config, username, password, query)
}
return &common.GlobalSearch{ return &common.GlobalSearch{
Images: []common.ImageSummary{ Images: []common.ImageSummary{
{ {
RepoName: "repo", RepoName: "repo",
MediaType: ispec.MediaTypeImageManifest, MediaType: ispec.MediaTypeImageManifest,
Size: "100",
Manifests: []common.ManifestSummary{ Manifests: []common.ManifestSummary{
{ {
Digest: godigest.FromString("str").String(), Digest: godigest.FromString("str").String(),
Size: "100", Size: "100",
ConfigDigest: ispec.DescriptorEmptyJSON.Digest.String(),
}, },
}, },
}, },
}, },
Repos: []common.RepoSummary{ Repos: []common.RepoSummary{
{ {
Name: "repo", Name: "repo",
Size: "100",
LastUpdated: time.Date(2010, 1, 1, 1, 1, 1, 0, time.UTC),
}, },
}, },
}, nil }, nil
@ -1944,6 +2586,10 @@ func (service mockService) globalSearchGQL(ctx context.Context, config searchCon
func (service mockService) getReferrersGQL(ctx context.Context, config searchConfig, username, password string, func (service mockService) getReferrersGQL(ctx context.Context, config searchConfig, username, password string,
repo, digest string, repo, digest string,
) (*common.ReferrersResp, error) { ) (*common.ReferrersResp, error) {
if service.getReferrersGQLFn != nil {
return service.getReferrersGQLFn(ctx, config, username, password, repo, digest)
}
return &common.ReferrersResp{ return &common.ReferrersResp{
ReferrersResult: common.ReferrersResult{ ReferrersResult: common.ReferrersResult{
Referrers: []common.Referrer{ Referrers: []common.Referrer{
@ -1961,6 +2607,10 @@ func (service mockService) getReferrersGQL(ctx context.Context, config searchCon
func (service mockService) getDerivedImageListGQL(ctx context.Context, config searchConfig, username, password string, func (service mockService) getDerivedImageListGQL(ctx context.Context, config searchConfig, username, password string,
derivedImage string, derivedImage string,
) (*common.DerivedImageListResponse, error) { ) (*common.DerivedImageListResponse, error) {
if service.getDerivedImageListGQLFn != nil {
return service.getDerivedImageListGQLFn(ctx, config, username, password, derivedImage)
}
imageListGQLResponse := &common.DerivedImageListResponse{} imageListGQLResponse := &common.DerivedImageListResponse{}
imageListGQLResponse.DerivedImageList.Results = []common.ImageSummary{ imageListGQLResponse.DerivedImageList.Results = []common.ImageSummary{
{ {
@ -1982,8 +2632,12 @@ func (service mockService) getDerivedImageListGQL(ctx context.Context, config se
} }
func (service mockService) getBaseImageListGQL(ctx context.Context, config searchConfig, username, password string, func (service mockService) getBaseImageListGQL(ctx context.Context, config searchConfig, username, password string,
derivedImage string, baseImage string,
) (*common.BaseImageListResponse, error) { ) (*common.BaseImageListResponse, error) {
if service.getBaseImageListGQLFn != nil {
return service.getBaseImageListGQLFn(ctx, config, username, password, baseImage)
}
imageListGQLResponse := &common.BaseImageListResponse{} imageListGQLResponse := &common.BaseImageListResponse{}
imageListGQLResponse.BaseImageList.Results = []common.ImageSummary{ imageListGQLResponse.BaseImageList.Results = []common.ImageSummary{
{ {
@ -2007,6 +2661,10 @@ func (service mockService) getBaseImageListGQL(ctx context.Context, config searc
func (service mockService) getImagesGQL(ctx context.Context, config searchConfig, username, password string, func (service mockService) getImagesGQL(ctx context.Context, config searchConfig, username, password string,
imageName string, imageName string,
) (*common.ImageListResponse, error) { ) (*common.ImageListResponse, error) {
if service.getImagesGQLFn != nil {
return service.getImagesGQLFn(ctx, config, username, password, imageName)
}
imageListGQLResponse := &common.ImageListResponse{} imageListGQLResponse := &common.ImageListResponse{}
imageListGQLResponse.PaginatedImagesResult.Results = []common.ImageSummary{ imageListGQLResponse.PaginatedImagesResult.Results = []common.ImageSummary{
{ {
@ -2029,9 +2687,13 @@ func (service mockService) getImagesGQL(ctx context.Context, config searchConfig
return imageListGQLResponse, nil return imageListGQLResponse, nil
} }
func (service mockService) getImagesByDigestGQL(ctx context.Context, config searchConfig, username, password string, func (service mockService) getImagesForDigestGQL(ctx context.Context, config searchConfig, username, password string,
digest string, digest string,
) (*common.ImagesForDigest, error) { ) (*common.ImagesForDigest, error) {
if service.getImagesForDigestGQLFn != nil {
return service.getImagesForDigestGQLFn(ctx, config, username, password, digest)
}
imageListGQLResponse := &common.ImagesForDigest{} imageListGQLResponse := &common.ImagesForDigest{}
imageListGQLResponse.Results = []common.ImageSummary{ imageListGQLResponse.Results = []common.ImageSummary{
{ {
@ -2057,6 +2719,10 @@ func (service mockService) getImagesByDigestGQL(ctx context.Context, config sear
func (service mockService) getImagesByCveIDGQL(ctx context.Context, config searchConfig, username, password string, func (service mockService) getImagesByCveIDGQL(ctx context.Context, config searchConfig, username, password string,
digest string, digest string,
) (*common.ImagesForCve, error) { ) (*common.ImagesForCve, error) {
if service.getImagesByCveIDGQLFn != nil {
return service.getImagesByCveIDGQLFn(ctx, config, username, password, digest)
}
imagesForCve := &common.ImagesForCve{ imagesForCve := &common.ImagesForCve{
Errors: nil, Errors: nil,
ImagesForCVEList: struct { ImagesForCVEList: struct {
@ -2075,6 +2741,10 @@ func (service mockService) getImagesByCveIDGQL(ctx context.Context, config searc
func (service mockService) getTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, func (service mockService) getTagsForCVEGQL(ctx context.Context, config searchConfig, username, password,
imageName, cveID string, imageName, cveID string,
) (*common.ImagesForCve, error) { ) (*common.ImagesForCve, error) {
if service.getTagsForCVEGQLFn != nil {
return service.getTagsForCVEGQLFn(ctx, config, username, password, imageName, cveID)
}
images := &common.ImagesForCve{ images := &common.ImagesForCve{
Errors: nil, Errors: nil,
ImagesForCVEList: struct { ImagesForCVEList: struct {
@ -2082,6 +2752,10 @@ func (service mockService) getTagsForCVEGQL(ctx context.Context, config searchCo
}{}, }{},
} }
if imageName == "" {
imageName = "image-name"
}
images.Errors = nil images.Errors = nil
mockedImage := service.getMockedImageByName(imageName) mockedImage := service.getMockedImageByName(imageName)
@ -2093,6 +2767,10 @@ func (service mockService) getTagsForCVEGQL(ctx context.Context, config searchCo
func (service mockService) getFixedTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, func (service mockService) getFixedTagsForCVEGQL(ctx context.Context, config searchConfig, username, password,
imageName, cveID string, imageName, cveID string,
) (*common.ImageListWithCVEFixedResponse, error) { ) (*common.ImageListWithCVEFixedResponse, error) {
if service.getFixedTagsForCVEGQLFn != nil {
return service.getFixedTagsForCVEGQLFn(ctx, config, username, password, imageName, cveID)
}
fixedTags := &common.ImageListWithCVEFixedResponse{ fixedTags := &common.ImageListWithCVEFixedResponse{
Errors: nil, Errors: nil,
ImageListWithCVEFixed: struct { ImageListWithCVEFixed: struct {
@ -2111,6 +2789,9 @@ func (service mockService) getFixedTagsForCVEGQL(ctx context.Context, config sea
func (service mockService) getCveByImageGQL(ctx context.Context, config searchConfig, username, password, func (service mockService) getCveByImageGQL(ctx context.Context, config searchConfig, username, password,
imageName, searchedCVE string, imageName, searchedCVE string,
) (*cveResult, error) { ) (*cveResult, error) {
if service.getCveByImageGQLFn != nil {
return service.getCveByImageGQLFn(ctx, config, username, password, imageName, searchedCVE)
}
cveRes := &cveResult{} cveRes := &cveResult{}
cveRes.Data = cveData{ cveRes.Data = cveData{
CVEListForImage: cveListForImage{ CVEListForImage: cveListForImage{
@ -2141,6 +2822,7 @@ func (service mockService) getMockedImageByName(imageName string) imageStruct {
image := imageStruct{} image := imageStruct{}
image.RepoName = imageName image.RepoName = imageName
image.Tag = "tag" image.Tag = "tag"
image.MediaType = ispec.MediaTypeImageManifest
image.Manifests = []common.ManifestSummary{ image.Manifests = []common.ManifestSummary{
{ {
Digest: godigest.FromString("Digest").String(), Digest: godigest.FromString("Digest").String(),
@ -2160,6 +2842,12 @@ func (service mockService) getAllImages(ctx context.Context, config searchConfig
defer wtgrp.Done() defer wtgrp.Done()
defer close(channel) defer close(channel)
if service.getAllImagesFn != nil {
service.getAllImagesFn(ctx, config, username, password, channel, wtgrp)
return
}
image := &imageStruct{} image := &imageStruct{}
image.RepoName = "randomimageName" image.RepoName = "randomimageName"
image.Tag = "tag" image.Tag = "tag"
@ -2192,6 +2880,12 @@ func (service mockService) getImageByName(ctx context.Context, config searchConf
defer wtgrp.Done() defer wtgrp.Done()
defer close(channel) defer close(channel)
if service.getImageByNameFn != nil {
service.getImageByNameFn(ctx, config, username, password, imageName, channel, wtgrp)
return
}
image := &imageStruct{} image := &imageStruct{}
image.RepoName = imageName image.RepoName = imageName
image.Tag = "tag" image.Tag = "tag"
@ -2257,26 +2951,62 @@ func (service mockService) getCveByImage(ctx context.Context, config searchConfi
} }
func (service mockService) getFixedTagsForCVE(ctx context.Context, config searchConfig, func (service mockService) getFixedTagsForCVE(ctx context.Context, config searchConfig,
username, password, imageName, cvid string, rch chan stringResult, wtgrp *sync.WaitGroup, username, password, imageName, cveid string, rch chan stringResult, wtgrp *sync.WaitGroup,
) { ) {
if service.getFixedTagsForCVEFn != nil {
defer wtgrp.Done()
defer close(rch)
service.getFixedTagsForCVEFn(ctx, config, username, password, imageName, cveid, rch, wtgrp)
return
}
service.getImageByName(ctx, config, username, password, imageName, rch, wtgrp) service.getImageByName(ctx, config, username, password, imageName, rch, wtgrp)
} }
func (service mockService) getImageByNameAndCVEID(ctx context.Context, config searchConfig, username, func (service mockService) getImageByNameAndCVEID(ctx context.Context, config searchConfig, username,
password, imageName, cvid string, rch chan stringResult, wtgrp *sync.WaitGroup, password, imageName, cveid string, rch chan stringResult, wtgrp *sync.WaitGroup,
) { ) {
if service.getImageByNameAndCVEIDFn != nil {
defer wtgrp.Done()
defer close(rch)
service.getImageByNameAndCVEIDFn(ctx, config, username, password, imageName, cveid, rch, wtgrp)
return
}
service.getImageByName(ctx, config, username, password, imageName, rch, wtgrp) service.getImageByName(ctx, config, username, password, imageName, rch, wtgrp)
} }
func (service mockService) getImagesByCveID(ctx context.Context, config searchConfig, username, password, cvid string, func (service mockService) getImagesByCveID(ctx context.Context, config searchConfig, username, password, cveid string,
rch chan stringResult, wtgrp *sync.WaitGroup, rch chan stringResult, wtgrp *sync.WaitGroup,
) { ) {
if service.getImagesByCveIDFn != nil {
defer wtgrp.Done()
defer close(rch)
service.getImagesByCveIDFn(ctx, config, username, password, cveid, rch, wtgrp)
return
}
service.getImageByName(ctx, config, username, password, "anImage", rch, wtgrp) service.getImageByName(ctx, config, username, password, "anImage", rch, wtgrp)
} }
func (service mockService) getImagesByDigest(ctx context.Context, config searchConfig, username, func (service mockService) getImagesByDigest(ctx context.Context, config searchConfig, username,
password, digest string, rch chan stringResult, wtgrp *sync.WaitGroup, password, digest string, rch chan stringResult, wtgrp *sync.WaitGroup,
) { ) {
if service.getImagesByDigestFn != nil {
defer wtgrp.Done()
defer close(rch)
service.getImagesByDigestFn(ctx, config, username, password, digest, rch, wtgrp)
return
}
service.getImageByName(ctx, config, username, password, "anImage", rch, wtgrp) service.getImageByName(ctx, config, username, password, "anImage", rch, wtgrp)
} }
@ -2288,7 +3018,7 @@ func makeConfigFile(content string) string {
panic(err) panic(err)
} }
configPath := path.Join(home + "/.zot") configPath := path.Join(home, "/.zot")
if err := os.WriteFile(configPath, []byte(content), 0o600); err != nil { if err := os.WriteFile(configPath, []byte(content), 0o600); err != nil {
panic(err) panic(err)

33
pkg/cli/images_cmd.go Normal file
View file

@ -0,0 +1,33 @@
//go:build search
// +build search
package cli
import (
"github.com/spf13/cobra"
"zotregistry.io/zot/pkg/cli/cmdflags"
)
func NewImagesCommand(searchService SearchService) *cobra.Command {
imageCmd := &cobra.Command{
Use: "images [command]",
Short: "List images hosted on the zot registry",
Long: `List images hosted on the zot registry`,
}
imageCmd.SetUsageTemplate(imageCmd.UsageTemplate() + usageFooter)
imageCmd.PersistentFlags().StringP(cmdflags.OutputFormatFlag, "f", "", "Specify output format [text/json/yaml]")
imageCmd.PersistentFlags().Bool(cmdflags.VerboseFlag, false, "Show verbose output")
imageCmd.PersistentFlags().Bool(cmdflags.DebugFlag, false, "Show debug output")
imageCmd.AddCommand(NewImageListCommand(searchService))
imageCmd.AddCommand(NewImageCVEListCommand(searchService))
imageCmd.AddCommand(NewImageBaseCommand(searchService))
imageCmd.AddCommand(NewImageDerivedCommand(searchService))
imageCmd.AddCommand(NewImageDigestCommand(searchService))
imageCmd.AddCommand(NewImageNameCommand(searchService))
return imageCmd
}

284
pkg/cli/images_sub_cmd.go Normal file
View file

@ -0,0 +1,284 @@
//go:build search
// +build search
package cli
import (
"fmt"
"os"
"path"
"github.com/briandowns/spinner"
"github.com/spf13/cobra"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/cli/cmdflags"
zcommon "zotregistry.io/zot/pkg/common"
)
func NewImageListCommand(searchService SearchService) *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List all images",
Long: "List all images",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
searchConfig, err := GetSearchConfigFromFlags(cmd, searchService)
if err != nil {
return err
}
if err := CheckExtEndPointQuery(searchConfig, ImageListQuery()); err == nil {
return SearchAllImagesGQL(searchConfig)
}
return SearchAllImages(searchConfig)
},
}
}
func NewImageCVEListCommand(searchService SearchService) *cobra.Command {
var searchedCVEID string
cmd := &cobra.Command{
Use: "cve [repo-name:tag][repo-name@digest]",
Short: "List all CVE's of the image",
Long: "List all CVE's of the image",
Args: OneImageWithRefArg,
RunE: func(cmd *cobra.Command, args []string) error {
searchConfig, err := GetSearchConfigFromFlags(cmd, searchService)
if err != nil {
return err
}
if err := CheckExtEndPointQuery(searchConfig, CVEListForImageQuery()); err == nil {
image := args[0]
return SearchCVEForImageGQL(searchConfig, image, searchedCVEID)
} else {
return err
}
},
}
cmd.Flags().StringVar(&searchedCVEID, cmdflags.SearchedCVEID, "", "Search for a specific CVE by name/id")
return cmd
}
func NewImageDerivedCommand(searchService SearchService) *cobra.Command {
cmd := &cobra.Command{
Use: "derived [repo-name:tag][repo-name@digest]",
Short: "List images that are derived from given image",
Long: "List images that are derived from given image",
Args: OneImageWithRefArg,
RunE: func(cmd *cobra.Command, args []string) error {
searchConfig, err := GetSearchConfigFromFlags(cmd, searchService)
if err != nil {
return err
}
if err := CheckExtEndPointQuery(searchConfig, DerivedImageListQuery()); err == nil {
return SearchDerivedImageListGQL(searchConfig, args[0])
} else {
return err
}
},
}
return cmd
}
func NewImageBaseCommand(searchService SearchService) *cobra.Command {
cmd := &cobra.Command{
Use: "base [repo-name:tag][repo-name@digest]",
Short: "List images that are base for the given image",
Long: "List images that are base for the given image",
Args: OneImageWithRefArg,
RunE: func(cmd *cobra.Command, args []string) error {
searchConfig, err := GetSearchConfigFromFlags(cmd, searchService)
if err != nil {
return err
}
if err := CheckExtEndPointQuery(searchConfig, BaseImageListQuery()); err == nil {
return SearchBaseImageListGQL(searchConfig, args[0])
} else {
return err
}
},
}
return cmd
}
func NewImageDigestCommand(searchService SearchService) *cobra.Command {
cmd := &cobra.Command{
Use: "digest [digest]",
Short: "List images that contain a blob(manifest, config or layer) with the given digest",
Long: "List images that contain a blob(manifest, config or layer) with the given digest",
Args: OneDigestArg,
RunE: func(cmd *cobra.Command, args []string) error {
searchConfig, err := GetSearchConfigFromFlags(cmd, searchService)
if err != nil {
return err
}
if err := CheckExtEndPointQuery(searchConfig, ImageListForDigestQuery()); err == nil {
return SearchImagesForDigestGQL(searchConfig, args[0])
} else {
return err
}
},
}
return cmd
}
func NewImageNameCommand(searchService SearchService) *cobra.Command {
cmd := &cobra.Command{
Use: "name [repo:tag]",
Short: "List image details by name",
Long: "List image details by name",
Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.ExactArgs(1)(cmd, args); err != nil {
return err
}
image := args[0]
if dir, _ := zcommon.GetImageDirAndTag(image); dir == "" {
return zerr.ErrInvalidRepoRefFormat
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
searchConfig, err := GetSearchConfigFromFlags(cmd, searchService)
if err != nil {
return err
}
if err := CheckExtEndPointQuery(searchConfig, ImageListQuery()); err == nil {
return SearchImageByNameGQL(searchConfig, args[0])
}
return SearchImageByName(searchConfig, args[0])
},
}
return cmd
}
func GetSearchConfigFromFlags(cmd *cobra.Command, searchService SearchService) (searchConfig, error) {
serverURL, err := GetServerURLFromFlags(cmd)
if err != nil {
return searchConfig{}, err
}
isSpinner, verifyTLS := GetCliConfigOptions(cmd)
flags := cmd.Flags()
user := defaultIfError(flags.GetString(cmdflags.UserFlag))
fixed := defaultIfError(flags.GetBool(cmdflags.FixedFlag))
debug := defaultIfError(flags.GetBool(cmdflags.DebugFlag))
verbose := defaultIfError(flags.GetBool(cmdflags.VerboseFlag))
outputFormat := defaultIfError(flags.GetString(cmdflags.OutputFormatFlag))
spin := spinner.New(spinner.CharSets[39], spinnerDuration, spinner.WithWriter(cmd.ErrOrStderr()))
spin.Prefix = prefix
return searchConfig{
params: map[string]*string{},
searchService: searchService,
servURL: &serverURL,
user: &user,
outputFormat: &outputFormat,
verifyTLS: &verifyTLS,
fixedFlag: &fixed,
verbose: &verbose,
debug: &debug,
spinner: spinnerState{spin, isSpinner},
resultWriter: cmd.OutOrStdout(),
}, nil
}
func defaultIfError[T any](out T, err error) T {
var defaultVal T
if err != nil {
return defaultVal
}
return out
}
func GetCliConfigOptions(cmd *cobra.Command) (bool, bool) {
configName, err := cmd.Flags().GetString(cmdflags.ConfigFlag)
if err != nil {
return false, false
}
home, err := os.UserHomeDir()
if err != nil {
return false, false
}
configDir := path.Join(home, "/.zot")
isSpinner, err := parseBooleanConfig(configDir, configName, showspinnerConfig)
if err != nil {
return false, false
}
verifyTLS, err := parseBooleanConfig(configDir, configName, verifyTLSConfig)
if err != nil {
return false, false
}
return isSpinner, verifyTLS
}
func GetServerURLFromFlags(cmd *cobra.Command) (string, error) {
serverURL, err := cmd.Flags().GetString(cmdflags.URLFlag)
if err == nil && serverURL != "" {
return serverURL, nil
}
configName, err := cmd.Flags().GetString(cmdflags.ConfigFlag)
if err != nil {
return "", err
}
if configName == "" {
return "", fmt.Errorf("%w: specify either '--%s' or '--%s' flags", zerr.ErrNoURLProvided, cmdflags.URLFlag,
cmdflags.ConfigFlag)
}
serverURL, err = ReadServerURLFromConfig(configName)
if err != nil {
return serverURL, fmt.Errorf("reading url from config failed: %w", err)
}
if serverURL == "" {
return "", fmt.Errorf("%w: url field from config is empty", zerr.ErrNoURLProvided)
}
return serverURL, nil
}
func ReadServerURLFromConfig(configName string) (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
configDir := path.Join(home, "/.zot")
urlFromConfig, err := getConfigValue(configDir, configName, "url")
if err != nil {
return "", err
}
return urlFromConfig, nil
}

View file

@ -10,7 +10,8 @@ import (
"github.com/briandowns/spinner" "github.com/briandowns/spinner"
"github.com/spf13/cobra" "github.com/spf13/cobra"
zotErrors "zotregistry.io/zot/errors" zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/cli/cmdflags"
) )
const prefix = "Searching... " const prefix = "Searching... "
@ -24,13 +25,14 @@ func NewRepoCommand(searchService SearchService) *cobra.Command {
Use: "repos [config-name]", Use: "repos [config-name]",
Short: "List all repositories", Short: "List all repositories",
Long: `List all repositories`, Long: `List all repositories`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
home, err := os.UserHomeDir() home, err := os.UserHomeDir()
if err != nil { if err != nil {
panic(err) panic(err)
} }
configPath := path.Join(home + "/.zot") configPath := path.Join(home, "/.zot")
if servURL == "" { if servURL == "" {
if len(args) > 0 { if len(args) > 0 {
urlFromConfig, err := getConfigValue(configPath, args[0], "url") urlFromConfig, err := getConfigValue(configPath, args[0], "url")
@ -41,12 +43,12 @@ func NewRepoCommand(searchService SearchService) *cobra.Command {
} }
if urlFromConfig == "" { if urlFromConfig == "" {
return zotErrors.ErrNoURLProvided return zerr.ErrNoURLProvided
} }
servURL = urlFromConfig servURL = urlFromConfig
} else { } else {
return zotErrors.ErrNoURLProvided return zerr.ErrNoURLProvided
} }
} }
@ -96,9 +98,12 @@ func NewRepoCommand(searchService SearchService) *cobra.Command {
repoCmd.SetUsageTemplate(repoCmd.UsageTemplate() + usageFooter) repoCmd.SetUsageTemplate(repoCmd.UsageTemplate() + usageFooter)
repoCmd.Flags().StringVar(&servURL, "url", "", "Specify zot server URL if config-name is not mentioned") repoCmd.AddCommand(NewListReposCommand(searchService))
repoCmd.Flags().StringVarP(&user, "user", "u", "", `User Credentials of zot server in "username:password" format`)
repoCmd.Flags().BoolVar(&debug, "debug", false, "Show debug output") repoCmd.Flags().StringVar(&servURL, cmdflags.URLFlag, "", "Specify zot server URL if config-name is not mentioned")
repoCmd.Flags().StringVarP(&user, cmdflags.UserFlag, "u", "",
`User Credentials of zot server in "username:password" format`)
repoCmd.Flags().BoolVar(&debug, cmdflags.DebugFlag, false, "Show debug output")
return repoCmd return repoCmd
} }

25
pkg/cli/repos_sub_cmd.go Normal file
View file

@ -0,0 +1,25 @@
//go:build search
// +build search
package cli
import "github.com/spf13/cobra"
func NewListReposCommand(searchService SearchService) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List all repositories",
Long: "List all repositories",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
searchConfig, err := GetSearchConfigFromFlags(cmd, searchService)
if err != nil {
return err
}
return SearchRepos(searchConfig)
},
}
return cmd
}

55
pkg/cli/repos_test.go Normal file
View file

@ -0,0 +1,55 @@
//go:build search
// +build search
package cli //nolint:testpackage
import (
"bytes"
"fmt"
"os"
"regexp"
"strings"
"testing"
. "github.com/smartystreets/goconvey/convey"
"zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/cli/cmdflags"
"zotregistry.io/zot/pkg/test"
)
func TestReposCommand(t *testing.T) {
Convey("repos", t, func() {
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(conf.HTTP.Port)
defer cm.StopServer()
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"repostest","url":"%s","showspinner":false}]}`,
baseURL))
defer os.Remove(configPath)
args := []string{"list"}
cmd := NewRepoCommand(mockService{})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "repostest", "")
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, "repo1")
So(actual, ShouldContainSubstring, "repo2")
})
}

View file

@ -18,10 +18,11 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
"zotregistry.io/zot/errors" zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants" "zotregistry.io/zot/pkg/api/constants"
"zotregistry.io/zot/pkg/cli/cmdflags"
extconf "zotregistry.io/zot/pkg/extensions/config" extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/extensions/monitoring" "zotregistry.io/zot/pkg/extensions/monitoring"
zlog "zotregistry.io/zot/pkg/log" zlog "zotregistry.io/zot/pkg/log"
@ -213,7 +214,13 @@ func NewCliRootCmd() *cobra.Command {
// additional cmds // additional cmds
enableCli(rootCmd) enableCli(rootCmd)
// "version" // "version"
rootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "show the version and exit") rootCmd.Flags().BoolVarP(&showVersion, cmdflags.VersionFlag, "v", false, "show the version and exit")
rootCmd.PersistentFlags().String(cmdflags.URLFlag, "",
"Specify zot server URL if config-name is not mentioned")
rootCmd.PersistentFlags().String(cmdflags.ConfigFlag, "",
"Specify the repository where to connect")
rootCmd.PersistentFlags().StringP(cmdflags.UserFlag, "u", "",
`User Credentials of zot server in "username:password" format`)
return rootCmd return rootCmd
} }
@ -225,18 +232,18 @@ func validateStorageConfig(cfg *config.Config, log zlog.Logger) error {
for _, storageConfig := range cfg.Storage.SubPaths { for _, storageConfig := range cfg.Storage.SubPaths {
if strings.EqualFold(defaultRootDir, storageConfig.RootDirectory) { if strings.EqualFold(defaultRootDir, storageConfig.RootDirectory) {
log.Error().Err(errors.ErrBadConfig).Msg("storage subpaths cannot use default storage root directory") log.Error().Err(zerr.ErrBadConfig).Msg("storage subpaths cannot use default storage root directory")
return errors.ErrBadConfig return zerr.ErrBadConfig
} }
expConfig, ok := expConfigMap[storageConfig.RootDirectory] expConfig, ok := expConfigMap[storageConfig.RootDirectory]
if ok { if ok {
equal := expConfig.ParamsEqual(storageConfig) equal := expConfig.ParamsEqual(storageConfig)
if !equal { if !equal {
log.Error().Err(errors.ErrBadConfig).Msg("storage config with same root directory should have same parameters") log.Error().Err(zerr.ErrBadConfig).Msg("storage config with same root directory should have same parameters")
return errors.ErrBadConfig return zerr.ErrBadConfig
} }
} else { } else {
expConfigMap[storageConfig.RootDirectory] = storageConfig expConfigMap[storageConfig.RootDirectory] = storageConfig
@ -251,31 +258,31 @@ func validateCacheConfig(cfg *config.Config, log zlog.Logger) error {
// dedupe true, remote storage, remoteCache true, but no cacheDriver (remote) // dedupe true, remote storage, remoteCache true, but no cacheDriver (remote)
//nolint: lll //nolint: lll
if cfg.Storage.Dedupe && cfg.Storage.StorageDriver != nil && cfg.Storage.RemoteCache && cfg.Storage.CacheDriver == nil { if cfg.Storage.Dedupe && cfg.Storage.StorageDriver != nil && cfg.Storage.RemoteCache && cfg.Storage.CacheDriver == nil {
log.Error().Err(errors.ErrBadConfig).Msg( log.Error().Err(zerr.ErrBadConfig).Msg(
"dedupe set to true with remote storage and caching, but no remote cache configured!") "dedupe set to true with remote storage and caching, but no remote cache configured!")
return errors.ErrBadConfig return zerr.ErrBadConfig
} }
if cfg.Storage.CacheDriver != nil && cfg.Storage.RemoteCache { if cfg.Storage.CacheDriver != nil && cfg.Storage.RemoteCache {
// local storage with remote caching // local storage with remote caching
if cfg.Storage.StorageDriver == nil { if cfg.Storage.StorageDriver == nil {
log.Error().Err(errors.ErrBadConfig).Msg("cannot have local storage driver with remote caching!") log.Error().Err(zerr.ErrBadConfig).Msg("cannot have local storage driver with remote caching!")
return errors.ErrBadConfig return zerr.ErrBadConfig
} }
// unsupported cache driver // unsupported cache driver
if cfg.Storage.CacheDriver["name"] != storageConstants.DynamoDBDriverName { if cfg.Storage.CacheDriver["name"] != storageConstants.DynamoDBDriverName {
log.Error().Err(errors.ErrBadConfig). log.Error().Err(zerr.ErrBadConfig).
Interface("cacheDriver", cfg.Storage.CacheDriver["name"]).Msg("unsupported cache driver") Interface("cacheDriver", cfg.Storage.CacheDriver["name"]).Msg("unsupported cache driver")
return errors.ErrBadConfig return zerr.ErrBadConfig
} }
} }
if !cfg.Storage.RemoteCache && cfg.Storage.CacheDriver != nil { if !cfg.Storage.RemoteCache && cfg.Storage.CacheDriver != nil {
log.Warn().Err(errors.ErrBadConfig).Str("directory", cfg.Storage.RootDirectory). log.Warn().Err(zerr.ErrBadConfig).Str("directory", cfg.Storage.RootDirectory).
Msg("remoteCache set to false but cacheDriver config (remote caching) provided for directory" + Msg("remoteCache set to false but cacheDriver config (remote caching) provided for directory" +
"will ignore and use local caching") "will ignore and use local caching")
} }
@ -285,30 +292,30 @@ func validateCacheConfig(cfg *config.Config, log zlog.Logger) error {
// dedupe true, remote storage, remoteCache true, but no cacheDriver (remote) // dedupe true, remote storage, remoteCache true, but no cacheDriver (remote)
//nolint: lll //nolint: lll
if subPath.Dedupe && subPath.StorageDriver != nil && subPath.RemoteCache && subPath.CacheDriver == nil { if subPath.Dedupe && subPath.StorageDriver != nil && subPath.RemoteCache && subPath.CacheDriver == nil {
log.Error().Err(errors.ErrBadConfig).Msg("dedupe set to true with remote storage and caching, but no remote cache configured!") log.Error().Err(zerr.ErrBadConfig).Msg("dedupe set to true with remote storage and caching, but no remote cache configured!")
return errors.ErrBadConfig return zerr.ErrBadConfig
} }
if subPath.CacheDriver != nil && subPath.RemoteCache { if subPath.CacheDriver != nil && subPath.RemoteCache {
// local storage with remote caching // local storage with remote caching
if subPath.StorageDriver == nil { if subPath.StorageDriver == nil {
log.Error().Err(errors.ErrBadConfig).Msg("cannot have local storage driver with remote caching!") log.Error().Err(zerr.ErrBadConfig).Msg("cannot have local storage driver with remote caching!")
return errors.ErrBadConfig return zerr.ErrBadConfig
} }
// unsupported cache driver // unsupported cache driver
if subPath.CacheDriver["name"] != storageConstants.DynamoDBDriverName { if subPath.CacheDriver["name"] != storageConstants.DynamoDBDriverName {
log.Error().Err(errors.ErrBadConfig).Interface("cacheDriver", cfg.Storage.CacheDriver["name"]). log.Error().Err(zerr.ErrBadConfig).Interface("cacheDriver", cfg.Storage.CacheDriver["name"]).
Msg("unsupported cache driver") Msg("unsupported cache driver")
return errors.ErrBadConfig return zerr.ErrBadConfig
} }
} }
if !subPath.RemoteCache && subPath.CacheDriver != nil { if !subPath.RemoteCache && subPath.CacheDriver != nil {
log.Warn().Err(errors.ErrBadConfig).Str("directory", cfg.Storage.RootDirectory). log.Warn().Err(zerr.ErrBadConfig).Str("directory", cfg.Storage.RootDirectory).
Msg("remoteCache set to false but cacheDriver config (remote caching) provided for directory," + Msg("remoteCache set to false but cacheDriver config (remote caching) provided for directory," +
"will ignore and use local caching") "will ignore and use local caching")
} }
@ -331,27 +338,27 @@ func validateExtensionsConfig(cfg *config.Config, log zlog.Logger) error {
// it would make sense to also check for mgmt and user prefs to be enabled, // it would make sense to also check for mgmt and user prefs to be enabled,
// but those are both enabled by having the search and ui extensions enabled // but those are both enabled by having the search and ui extensions enabled
if cfg.Extensions.Search == nil || !*cfg.Extensions.Search.Enable { if cfg.Extensions.Search == nil || !*cfg.Extensions.Search.Enable {
log.Warn().Err(errors.ErrBadConfig).Msg("UI functionality can't be used without search extension.") log.Warn().Err(zerr.ErrBadConfig).Msg("UI functionality can't be used without search extension.")
return errors.ErrBadConfig return zerr.ErrBadConfig
} }
} }
//nolint:lll //nolint:lll
if cfg.Storage.StorageDriver != nil && cfg.Extensions != nil && cfg.Extensions.Search != nil && if cfg.Storage.StorageDriver != nil && cfg.Extensions != nil && cfg.Extensions.Search != nil &&
cfg.Extensions.Search.Enable != nil && *cfg.Extensions.Search.Enable && cfg.Extensions.Search.CVE != nil { cfg.Extensions.Search.Enable != nil && *cfg.Extensions.Search.Enable && cfg.Extensions.Search.CVE != nil {
log.Warn().Err(errors.ErrBadConfig).Msg("CVE functionality can't be used with remote storage. Please disable CVE") log.Warn().Err(zerr.ErrBadConfig).Msg("CVE functionality can't be used with remote storage. Please disable CVE")
return errors.ErrBadConfig return zerr.ErrBadConfig
} }
for _, subPath := range cfg.Storage.SubPaths { for _, subPath := range cfg.Storage.SubPaths {
//nolint:lll //nolint:lll
if subPath.StorageDriver != nil && cfg.Extensions != nil && cfg.Extensions.Search != nil && if subPath.StorageDriver != nil && cfg.Extensions != nil && cfg.Extensions.Search != nil &&
cfg.Extensions.Search.Enable != nil && *cfg.Extensions.Search.Enable && cfg.Extensions.Search.CVE != nil { cfg.Extensions.Search.Enable != nil && *cfg.Extensions.Search.Enable && cfg.Extensions.Search.CVE != nil {
log.Warn().Err(errors.ErrBadConfig).Msg("CVE functionality can't be used with remote storage. Please disable CVE") log.Warn().Err(zerr.ErrBadConfig).Msg("CVE functionality can't be used with remote storage. Please disable CVE")
return errors.ErrBadConfig return zerr.ErrBadConfig
} }
} }
@ -402,17 +409,17 @@ func validateConfiguration(config *config.Config, log zlog.Logger) error {
if len(config.Storage.StorageDriver) != 0 { if len(config.Storage.StorageDriver) != 0 {
// enforce s3 driver in case of using storage driver // enforce s3 driver in case of using storage driver
if config.Storage.StorageDriver["name"] != storageConstants.S3StorageDriverName { if config.Storage.StorageDriver["name"] != storageConstants.S3StorageDriverName {
log.Error().Err(errors.ErrBadConfig).Interface("cacheDriver", config.Storage.StorageDriver["name"]). log.Error().Err(zerr.ErrBadConfig).Interface("cacheDriver", config.Storage.StorageDriver["name"]).
Msg("unsupported storage driver") Msg("unsupported storage driver")
return errors.ErrBadConfig return zerr.ErrBadConfig
} }
// enforce filesystem storage in case sync feature is enabled // enforce filesystem storage in case sync feature is enabled
if config.Extensions != nil && config.Extensions.Sync != nil { if config.Extensions != nil && config.Extensions.Sync != nil {
log.Error().Err(errors.ErrBadConfig).Msg("sync supports only filesystem storage") log.Error().Err(zerr.ErrBadConfig).Msg("sync supports only filesystem storage")
return errors.ErrBadConfig return zerr.ErrBadConfig
} }
} }
@ -424,10 +431,10 @@ func validateConfiguration(config *config.Config, log zlog.Logger) error {
for route, storageConfig := range subPaths { for route, storageConfig := range subPaths {
if len(storageConfig.StorageDriver) != 0 { if len(storageConfig.StorageDriver) != 0 {
if storageConfig.StorageDriver["name"] != storageConstants.S3StorageDriverName { if storageConfig.StorageDriver["name"] != storageConstants.S3StorageDriverName {
log.Error().Err(errors.ErrBadConfig).Str("subpath", route).Interface("storageDriver", log.Error().Err(zerr.ErrBadConfig).Str("subpath", route).Interface("storageDriver",
storageConfig.StorageDriver["name"]).Msg("unsupported storage driver") storageConfig.StorageDriver["name"]).Msg("unsupported storage driver")
return errors.ErrBadConfig return zerr.ErrBadConfig
} }
} }
} }
@ -456,23 +463,23 @@ func validateOpenIDConfig(cfg *config.Config, log zlog.Logger) error {
if config.IsOpenIDSupported(provider) { if config.IsOpenIDSupported(provider) {
if providerConfig.ClientID == "" || providerConfig.Issuer == "" || if providerConfig.ClientID == "" || providerConfig.Issuer == "" ||
len(providerConfig.Scopes) == 0 { len(providerConfig.Scopes) == 0 {
log.Error().Err(errors.ErrBadConfig). log.Error().Err(zerr.ErrBadConfig).
Msg("OpenID provider config requires clientid, issuer and scopes parameters") Msg("OpenID provider config requires clientid, issuer and scopes parameters")
return errors.ErrBadConfig return zerr.ErrBadConfig
} }
} else if config.IsOauth2Supported(provider) { } else if config.IsOauth2Supported(provider) {
if providerConfig.ClientID == "" || len(providerConfig.Scopes) == 0 { if providerConfig.ClientID == "" || len(providerConfig.Scopes) == 0 {
log.Error().Err(errors.ErrBadConfig). log.Error().Err(zerr.ErrBadConfig).
Msg("OAuth2 provider config requires clientid and scopes parameters") Msg("OAuth2 provider config requires clientid and scopes parameters")
return errors.ErrBadConfig return zerr.ErrBadConfig
} }
} else { } else {
log.Error().Err(errors.ErrBadConfig). log.Error().Err(zerr.ErrBadConfig).
Msg("unsupported openid/oauth2 provider") Msg("unsupported openid/oauth2 provider")
return errors.ErrBadConfig return zerr.ErrBadConfig
} }
} }
} }
@ -483,11 +490,11 @@ func validateOpenIDConfig(cfg *config.Config, log zlog.Logger) error {
func validateAuthzPolicies(config *config.Config, log zlog.Logger) error { func validateAuthzPolicies(config *config.Config, log zlog.Logger) error {
if (config.HTTP.Auth == nil || (config.HTTP.Auth.HTPasswd.Path == "" && config.HTTP.Auth.LDAP == nil && if (config.HTTP.Auth == nil || (config.HTTP.Auth.HTPasswd.Path == "" && config.HTTP.Auth.LDAP == nil &&
config.HTTP.Auth.OpenID == nil)) && !authzContainsOnlyAnonymousPolicy(config) { config.HTTP.Auth.OpenID == nil)) && !authzContainsOnlyAnonymousPolicy(config) {
log.Error().Err(errors.ErrBadConfig). log.Error().Err(zerr.ErrBadConfig).
Msg("access control config requires one of httpasswd, ldap or openid authentication " + Msg("access control config requires one of httpasswd, ldap or openid authentication " +
"or using only 'anonymousPolicy' policies") "or using only 'anonymousPolicy' policies")
return errors.ErrBadConfig return zerr.ErrBadConfig
} }
return nil return nil
@ -730,15 +737,15 @@ func LoadConfiguration(config *config.Config, configPath string) error {
log := zlog.NewLogger(config.Log.Level, config.Log.Output) log := zlog.NewLogger(config.Log.Level, config.Log.Output)
if len(metaData.Keys) == 0 { if len(metaData.Keys) == 0 {
log.Error().Err(errors.ErrBadConfig).Msg("config doesn't contain any key:value pair") log.Error().Err(zerr.ErrBadConfig).Msg("config doesn't contain any key:value pair")
return errors.ErrBadConfig return zerr.ErrBadConfig
} }
if len(metaData.Unused) > 0 { if len(metaData.Unused) > 0 {
log.Error().Err(errors.ErrBadConfig).Strs("keys", metaData.Unused).Msg("unknown keys") log.Error().Err(zerr.ErrBadConfig).Strs("keys", metaData.Unused).Msg("unknown keys")
return errors.ErrBadConfig return zerr.ErrBadConfig
} }
// defaults // defaults
@ -803,21 +810,21 @@ func validateLDAP(config *config.Config, log zlog.Logger) error {
log.Error().Str("userAttribute", ldap.UserAttribute). log.Error().Str("userAttribute", ldap.UserAttribute).
Msg("invalid LDAP configuration, missing mandatory key: userAttribute") Msg("invalid LDAP configuration, missing mandatory key: userAttribute")
return errors.ErrLDAPConfig return zerr.ErrLDAPConfig
} }
if ldap.Address == "" { if ldap.Address == "" {
log.Error().Str("address", ldap.Address). log.Error().Str("address", ldap.Address).
Msg("invalid LDAP configuration, missing mandatory key: address") Msg("invalid LDAP configuration, missing mandatory key: address")
return errors.ErrLDAPConfig return zerr.ErrLDAPConfig
} }
if ldap.BaseDN == "" { if ldap.BaseDN == "" {
log.Error().Str("basedn", ldap.BaseDN). log.Error().Str("basedn", ldap.BaseDN).
Msg("invalid LDAP configuration, missing mandatory key: basedn") Msg("invalid LDAP configuration, missing mandatory key: basedn")
return errors.ErrLDAPConfig return zerr.ErrLDAPConfig
} }
} }
@ -830,7 +837,7 @@ func validateHTTP(config *config.Config, log zlog.Logger) error {
if err != nil || (port < 0 || port > 65535) { if err != nil || (port < 0 || port > 65535) {
log.Error().Str("port", config.HTTP.Port).Msg("invalid port") log.Error().Str("port", config.HTTP.Port).Msg("invalid port")
return errors.ErrBadConfig return zerr.ErrBadConfig
} }
} }
@ -840,27 +847,27 @@ func validateHTTP(config *config.Config, log zlog.Logger) error {
func validateGC(config *config.Config, log zlog.Logger) error { func validateGC(config *config.Config, log zlog.Logger) error {
// enforce GC params // enforce GC params
if config.Storage.GCDelay < 0 { if config.Storage.GCDelay < 0 {
log.Error().Err(errors.ErrBadConfig).Dur("delay", config.Storage.GCDelay). log.Error().Err(zerr.ErrBadConfig).Dur("delay", config.Storage.GCDelay).
Msg("invalid garbage-collect delay specified") Msg("invalid garbage-collect delay specified")
return errors.ErrBadConfig return zerr.ErrBadConfig
} }
if config.Storage.GCInterval < 0 { if config.Storage.GCInterval < 0 {
log.Error().Err(errors.ErrBadConfig).Dur("interval", config.Storage.GCInterval). log.Error().Err(zerr.ErrBadConfig).Dur("interval", config.Storage.GCInterval).
Msg("invalid garbage-collect interval specified") Msg("invalid garbage-collect interval specified")
return errors.ErrBadConfig return zerr.ErrBadConfig
} }
if !config.Storage.GC { if !config.Storage.GC {
if config.Storage.GCDelay != 0 { if config.Storage.GCDelay != 0 {
log.Warn().Err(errors.ErrBadConfig). log.Warn().Err(zerr.ErrBadConfig).
Msg("garbage-collect delay specified without enabling garbage-collect, will be ignored") Msg("garbage-collect delay specified without enabling garbage-collect, will be ignored")
} }
if config.Storage.GCInterval != 0 { if config.Storage.GCInterval != 0 {
log.Warn().Err(errors.ErrBadConfig). log.Warn().Err(zerr.ErrBadConfig).
Msg("periodic garbage-collect interval specified without enabling garbage-collect, will be ignored") Msg("periodic garbage-collect interval specified without enabling garbage-collect, will be ignored")
} }
} }
@ -868,12 +875,12 @@ func validateGC(config *config.Config, log zlog.Logger) error {
// subpaths // subpaths
for name, subPath := range config.Storage.SubPaths { for name, subPath := range config.Storage.SubPaths {
if subPath.GC && subPath.GCDelay <= 0 { if subPath.GC && subPath.GCDelay <= 0 {
log.Error().Err(errors.ErrBadConfig). log.Error().Err(zerr.ErrBadConfig).
Str("subPath", name). Str("subPath", name).
Interface("gcDelay", subPath.GCDelay). Interface("gcDelay", subPath.GCDelay).
Msg("invalid GC delay configuration - cannot be negative or zero") Msg("invalid GC delay configuration - cannot be negative or zero")
return errors.ErrBadConfig return zerr.ErrBadConfig
} }
} }
@ -886,10 +893,10 @@ func validateSync(config *config.Config, log zlog.Logger) error {
for id, regCfg := range config.Extensions.Sync.Registries { for id, regCfg := range config.Extensions.Sync.Registries {
// check retry options are configured for sync // check retry options are configured for sync
if regCfg.MaxRetries != nil && regCfg.RetryDelay == nil { if regCfg.MaxRetries != nil && regCfg.RetryDelay == nil {
log.Error().Err(errors.ErrBadConfig).Int("id", id).Interface("extensions.sync.registries[id]", log.Error().Err(zerr.ErrBadConfig).Int("id", id).Interface("extensions.sync.registries[id]",
config.Extensions.Sync.Registries[id]).Msg("retryDelay is required when using maxRetries") config.Extensions.Sync.Registries[id]).Msg("retryDelay is required when using maxRetries")
return errors.ErrBadConfig return zerr.ErrBadConfig
} }
if regCfg.Content != nil { if regCfg.Content != nil {
@ -902,11 +909,11 @@ func validateSync(config *config.Config, log zlog.Logger) error {
} }
if content.StripPrefix && !strings.Contains(content.Prefix, "/*") && content.Destination == "/" { if content.StripPrefix && !strings.Contains(content.Prefix, "/*") && content.Destination == "/" {
log.Error().Err(errors.ErrBadConfig). log.Error().Err(zerr.ErrBadConfig).
Interface("sync content", content). Interface("sync content", content).
Msg("sync config: can not use stripPrefix true and destination '/' without using glob patterns in prefix") Msg("sync config: can not use stripPrefix true and destination '/' without using glob patterns in prefix")
return errors.ErrBadConfig return zerr.ErrBadConfig
} }
} }
} }

View file

@ -91,7 +91,7 @@ func TestServe(t *testing.T) {
Convey("config with missing rootDir", func(c C) { Convey("config with missing rootDir", func(c C) {
rootDir := t.TempDir() rootDir := t.TempDir()
// missing storag config should result in an error in Controller.Init() // missing storage config should result in an error in Controller.Init()
content := []byte(`{ content := []byte(`{
"distSpecVersion": "1.1.0-dev", "distSpecVersion": "1.1.0-dev",
"http": { "http": {

View file

@ -10,7 +10,8 @@ import (
"github.com/briandowns/spinner" "github.com/briandowns/spinner"
"github.com/spf13/cobra" "github.com/spf13/cobra"
zotErrors "zotregistry.io/zot/errors" zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/cli/cmdflags"
) )
//nolint:dupl //nolint:dupl
@ -21,7 +22,7 @@ func NewSearchCommand(searchService SearchService) *cobra.Command {
var isSpinner, verifyTLS, verbose, debug bool var isSpinner, verifyTLS, verbose, debug bool
imageCmd := &cobra.Command{ searchCmd := &cobra.Command{
Use: "search [config-name]", Use: "search [config-name]",
Short: "Search images and their tags", Short: "Search images and their tags",
Long: `Search repos or images Long: `Search repos or images
@ -36,13 +37,14 @@ Example:
zli search --subject repo@sha256:f9a0981... zli search --subject repo@sha256:f9a0981...
zli search --subject repo:tag zli search --subject repo:tag
`, `,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
home, err := os.UserHomeDir() home, err := os.UserHomeDir()
if err != nil { if err != nil {
panic(err) panic(err)
} }
configPath := path.Join(home + "/.zot") configPath := path.Join(home, "/.zot")
if servURL == "" { if servURL == "" {
if len(args) > 0 { if len(args) > 0 {
urlFromConfig, err := getConfigValue(configPath, args[0], "url") urlFromConfig, err := getConfigValue(configPath, args[0], "url")
@ -53,12 +55,12 @@ Example:
} }
if urlFromConfig == "" { if urlFromConfig == "" {
return zotErrors.ErrNoURLProvided return zerr.ErrNoURLProvided
} }
servURL = urlFromConfig servURL = urlFromConfig
} else { } else {
return zotErrors.ErrNoURLProvided return zerr.ErrNoURLProvided
} }
} }
@ -107,27 +109,32 @@ Example:
}, },
} }
setupSearchFlags(imageCmd, searchImageParams, &servURL, &user, &outputFormat, &verbose, &debug) setupSearchFlags(searchCmd, searchImageParams, &servURL, &user, &outputFormat, &verbose, &debug)
imageCmd.SetUsageTemplate(imageCmd.UsageTemplate() + usageFooter) searchCmd.SetUsageTemplate(searchCmd.UsageTemplate() + usageFooter)
return imageCmd searchCmd.AddCommand(NewSearchQueryCommand(searchService))
searchCmd.AddCommand(NewSearchSubjectCommand(searchService))
return searchCmd
} }
func setupSearchFlags(imageCmd *cobra.Command, searchImageParams map[string]*string, func setupSearchFlags(searchCmd *cobra.Command, searchImageParams map[string]*string,
servURL, user, outputFormat *string, verbose *bool, debug *bool, servURL, user, outputFormat *string, verbose *bool, debug *bool,
) { ) {
searchImageParams["query"] = imageCmd.Flags().StringP("query", "q", "", searchImageParams["query"] = searchCmd.Flags().StringP("query", "q", "",
"Specify what repo or image(repo:tag) to be searched") "Specify what repo or image(repo:tag) to be searched")
searchImageParams["subject"] = imageCmd.Flags().StringP("subject", "s", "", searchImageParams["subject"] = searchCmd.Flags().StringP("subject", "s", "",
"List all referrers for this subject. The subject can be specified by tag(repo:tag) or by digest"+ "List all referrers for this subject. The subject can be specified by tag(repo:tag) or by digest"+
"(repo@digest)") "(repo@digest)")
imageCmd.Flags().StringVar(servURL, "url", "", "Specify zot server URL if config-name is not mentioned") searchCmd.Flags().StringVar(servURL, cmdflags.URLFlag, "", "Specify zot server URL if config-name is not mentioned")
imageCmd.Flags().StringVarP(user, "user", "u", "", `User Credentials of zot server in "username:password" format`) searchCmd.Flags().StringVarP(user, cmdflags.UserFlag, "u", "",
imageCmd.Flags().StringVarP(outputFormat, "output", "o", "", "Specify output format [text/json/yaml]") `User Credentials of zot server in "username:password" format`)
imageCmd.Flags().BoolVar(verbose, "verbose", false, "Show verbose output") searchCmd.PersistentFlags().StringVarP(outputFormat, cmdflags.OutputFormatFlag, "f", "",
imageCmd.Flags().BoolVar(debug, "debug", false, "Show debug output") "Specify output format [text/json/yaml]")
searchCmd.PersistentFlags().BoolVar(verbose, cmdflags.VerboseFlag, false, "Show verbose output")
searchCmd.PersistentFlags().BoolVar(debug, cmdflags.DebugFlag, false, "Show debug output")
} }
func globalSearch(searchConfig searchConfig) error { func globalSearch(searchConfig searchConfig) error {
@ -150,5 +157,5 @@ func globalSearch(searchConfig searchConfig) error {
} }
} }
return zotErrors.ErrInvalidFlagsCombination return zerr.ErrInvalidFlagsCombination
} }

View file

@ -20,12 +20,6 @@ import (
"zotregistry.io/zot/pkg/test" "zotregistry.io/zot/pkg/test"
) )
func ref[T any](input T) *T {
ref := input
return &ref
}
const ( const (
customArtTypeV1 = "application/custom.art.type.v1" customArtTypeV1 = "application/custom.art.type.v1"
customArtTypeV2 = "application/custom.art.type.v2" customArtTypeV2 = "application/custom.art.type.v2"
@ -428,7 +422,7 @@ func TestFormatsReferrersCLI(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
Convey("JSON format", func() { Convey("JSON format", func() {
args := []string{"reftest", "--output", "json", "--subject", repo + "@" + image.DigestStr()} args := []string{"reftest", "--format", "json", "--subject", repo + "@" + image.DigestStr()}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`, configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`,
baseURL)) baseURL))
@ -446,7 +440,7 @@ func TestFormatsReferrersCLI(t *testing.T) {
fmt.Println(buff.String()) fmt.Println(buff.String())
}) })
Convey("YAML format", func() { Convey("YAML format", func() {
args := []string{"reftest", "--output", "yaml", "--subject", repo + "@" + image.DigestStr()} args := []string{"reftest", "--format", "yaml", "--subject", repo + "@" + image.DigestStr()}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`, configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`,
baseURL)) baseURL))
@ -464,7 +458,7 @@ func TestFormatsReferrersCLI(t *testing.T) {
fmt.Println(buff.String()) fmt.Println(buff.String())
}) })
Convey("Invalid format", func() { Convey("Invalid format", func() {
args := []string{"reftest", "--output", "invalid_format", "--subject", repo + "@" + image.DigestStr()} args := []string{"reftest", "--format", "invalid_format", "--subject", repo + "@" + image.DigestStr()}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`, configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`,
baseURL)) baseURL))
@ -488,7 +482,7 @@ func TestReferrersCLIErrors(t *testing.T) {
cmd := NewSearchCommand(new(searchService)) cmd := NewSearchCommand(new(searchService))
Convey("no url provided", func() { Convey("no url provided", func() {
args := []string{"reftest", "--output", "invalid", "--query", "repo/alpine"} args := []string{"reftest", "--format", "invalid", "--query", "repo/alpine"}
configPath := makeConfigFile(`{"configs":[{"_name":"reftest","showspinner":false}]}`) configPath := makeConfigFile(`{"configs":[{"_name":"reftest","showspinner":false}]}`)
@ -594,3 +588,9 @@ func TestReferrersCLIErrors(t *testing.T) {
}) })
}) })
} }
func ref[T any](input T) *T {
ref := input
return &ref
}

View file

@ -17,6 +17,7 @@ import (
"zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/cli/cmdflags"
extconf "zotregistry.io/zot/pkg/extensions/config" extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/test" "zotregistry.io/zot/pkg/test"
) )
@ -240,7 +241,7 @@ func TestFormatsSearchCLI(t *testing.T) {
cmd := NewSearchCommand(new(searchService)) cmd := NewSearchCommand(new(searchService))
Convey("JSON format", func() { Convey("JSON format", func() {
args := []string{"searchtest", "--output", "json", "--query", "repo/alpine"} args := []string{"searchtest", "--format", "json", "--query", "repo/alpine"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"searchtest","url":"%s","showspinner":false}]}`, configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"searchtest","url":"%s","showspinner":false}]}`,
baseURL)) baseURL))
@ -257,7 +258,7 @@ func TestFormatsSearchCLI(t *testing.T) {
}) })
Convey("YAML format", func() { Convey("YAML format", func() {
args := []string{"searchtest", "--output", "yaml", "--query", "repo/alpine"} args := []string{"searchtest", "--format", "yaml", "--query", "repo/alpine"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"searchtest","url":"%s","showspinner":false}]}`, configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"searchtest","url":"%s","showspinner":false}]}`,
baseURL)) baseURL))
@ -274,7 +275,7 @@ func TestFormatsSearchCLI(t *testing.T) {
}) })
Convey("Invalid format", func() { Convey("Invalid format", func() {
args := []string{"searchtest", "--output", "invalid", "--query", "repo/alpine"} args := []string{"searchtest", "--format", "invalid", "--query", "repo/alpine"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"searchtest","url":"%s","showspinner":false}]}`, configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"searchtest","url":"%s","showspinner":false}]}`,
baseURL)) baseURL))
@ -296,7 +297,7 @@ func TestSearchCLIErrors(t *testing.T) {
cmd := NewSearchCommand(new(searchService)) cmd := NewSearchCommand(new(searchService))
Convey("no url provided", func() { Convey("no url provided", func() {
args := []string{"searchtest", "--output", "invalid", "--query", "repo/alpine"} args := []string{"searchtest", "--format", "invalid", "--query", "repo/alpine"}
configPath := makeConfigFile(`{"configs":[{"_name":"searchtest","showspinner":false}]}`) configPath := makeConfigFile(`{"configs":[{"_name":"searchtest","showspinner":false}]}`)
@ -311,7 +312,7 @@ func TestSearchCLIErrors(t *testing.T) {
}) })
Convey("getConfigValue", func() { Convey("getConfigValue", func() {
args := []string{"searchtest", "--output", "invalid", "--query", "repo/alpine"} args := []string{"searchtest", "--format", "invalid", "--query", "repo/alpine"}
configPath := makeConfigFile(`bad-json`) configPath := makeConfigFile(`bad-json`)
@ -358,7 +359,7 @@ func TestSearchCLIErrors(t *testing.T) {
}) })
Convey("url from config is empty", func() { Convey("url from config is empty", func() {
args := []string{"searchtest", "--output", "invalid", "--query", "repo/alpine"} args := []string{"searchtest", "--format", "invalid", "--query", "repo/alpine"}
configPath := makeConfigFile(`{"configs":[{"_name":"searchtest", "url":"", "showspinner":false}]}`) configPath := makeConfigFile(`{"configs":[{"_name":"searchtest", "url":"", "showspinner":false}]}`)
@ -402,3 +403,142 @@ func TestSearchCLIErrors(t *testing.T) {
}) })
}) })
} }
func TestSearchCommandGQL(t *testing.T) {
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
defaultVal := true
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
},
}
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(conf.HTTP.Port)
defer cm.StopServer()
Convey("commands without gql", t, func() {
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"searchtest","url":"%s","showspinner":false}]}`,
baseURL))
defer os.Remove(configPath)
Convey("query", func() {
args := []string{"query", "repo/al"}
cmd := NewSearchCommand(mockService{})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "searchtest", "")
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, "repo 8c25cb36 false 100B")
So(actual, ShouldContainSubstring, "repo 100B 2010-01-01 01:01:01 +0000 UTC 0 0")
})
Convey("query command errors", func() {
// no url
args := []string{"repo/al"}
cmd := NewSearchQueryCommand(mockService{})
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("subject", func() {
err := test.UploadImage(test.CreateRandomImage(), baseURL, "repo", "tag")
So(err, ShouldBeNil)
args := []string{"subject", "repo:tag"}
cmd := NewSearchCommand(mockService{})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "searchtest", "")
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, "ArtifactType 100 B Digest")
})
Convey("subject command errors", func() {
// no url
args := []string{"repo:tag"}
cmd := NewSearchSubjectCommand(mockService{})
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
})
})
}
func TestSearchCommandREST(t *testing.T) {
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(conf.HTTP.Port)
defer cm.StopServer()
Convey("commands without gql", t, func() {
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"searchtest","url":"%s","showspinner":false}]}`,
baseURL))
defer os.Remove(configPath)
Convey("query", func() {
args := []string{"query", "repo/al"}
cmd := NewSearchCommand(mockService{})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "searchtest", "")
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("subject", func() {
err := test.UploadImage(test.CreateRandomImage(), baseURL, "repo", "tag")
So(err, ShouldBeNil)
args := []string{"subject", "repo:tag"}
cmd := NewSearchCommand(mockService{})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "searchtest", "")
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,
"art.type 100 B sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a")
})
})
}

459
pkg/cli/search_functions.go Normal file
View file

@ -0,0 +1,459 @@
//go:build search
// +build search
package cli
import (
"context"
"fmt"
"math"
"strings"
"sync"
"time"
zerr "zotregistry.io/zot/errors"
zcommon "zotregistry.io/zot/pkg/common"
)
func SearchAllImages(config searchConfig) error {
username, password := getUsernameAndPassword(*config.user)
imageErr := make(chan stringResult)
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
wg.Add(1)
go config.searchService.getAllImages(ctx, config, username, password, imageErr, &wg)
wg.Add(1)
errCh := make(chan error, 1)
go collectResults(config, &wg, imageErr, cancel, printImageTableHeader, errCh)
wg.Wait()
select {
case err := <-errCh:
return err
default:
return nil
}
}
func SearchAllImagesGQL(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, "")
if err != nil {
return err
}
imageListData := []imageStruct{}
for _, image := range imageList.Results {
imageListData = append(imageListData, imageStruct(image))
}
return printImageResult(config, imageListData)
}
func SearchImageByName(config searchConfig, image string) error {
username, password := getUsernameAndPassword(*config.user)
imageErr := make(chan stringResult)
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
wg.Add(1)
go config.searchService.getImageByName(ctx, config, username, password,
image, imageErr, &wg)
wg.Add(1)
errCh := make(chan error, 1)
go collectResults(config, &wg, imageErr, cancel, printImageTableHeader, errCh)
wg.Wait()
select {
case err := <-errCh:
return err
default:
return nil
}
}
func SearchImageByNameGQL(config searchConfig, imageName string) error {
username, password := getUsernameAndPassword(*config.user)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
repo, tag := zcommon.GetImageDirAndTag(imageName)
imageList, err := config.searchService.getImagesGQL(ctx, config, username, password, repo)
if err != nil {
return err
}
imageListData := []imageStruct{}
for _, image := range imageList.Results {
if tag == "" || image.Tag == tag {
imageListData = append(imageListData, imageStruct(image))
}
}
return printImageResult(config, imageListData)
}
func SearchImagesByDigest(config searchConfig, digest string) error {
username, password := getUsernameAndPassword(*config.user)
imageErr := make(chan stringResult)
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
wg.Add(1)
go config.searchService.getImagesByDigest(ctx, config, username, password,
digest, imageErr, &wg)
wg.Add(1)
errCh := make(chan error, 1)
go collectResults(config, &wg, imageErr, cancel, printImageTableHeader, errCh)
wg.Wait()
select {
case err := <-errCh:
return err
default:
return nil
}
}
func SearchDerivedImageListGQL(config searchConfig, derivedImage string) error {
username, password := getUsernameAndPassword(*config.user)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
imageList, err := config.searchService.getDerivedImageListGQL(ctx, config, username,
password, derivedImage)
if err != nil {
return err
}
imageListData := []imageStruct{}
for _, image := range imageList.DerivedImageList.Results {
imageListData = append(imageListData, imageStruct(image))
}
return printImageResult(config, imageListData)
}
func SearchBaseImageListGQL(config searchConfig, baseImage string) error {
username, password := getUsernameAndPassword(*config.user)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
imageList, err := config.searchService.getBaseImageListGQL(ctx, config, username,
password, baseImage)
if err != nil {
return err
}
imageListData := []imageStruct{}
for _, image := range imageList.BaseImageList.Results {
imageListData = append(imageListData, imageStruct(image))
}
return printImageResult(config, imageListData)
}
func SearchImagesForDigestGQL(config searchConfig, digest string) error {
username, password := getUsernameAndPassword(*config.user)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
imageList, err := config.searchService.getImagesForDigestGQL(ctx, config, username, password, digest)
if err != nil {
return err
}
imageListData := []imageStruct{}
for _, image := range imageList.Results {
imageListData = append(imageListData, imageStruct(image))
}
if err := printImageResult(config, imageListData); err != nil {
return err
}
return nil
}
func SearchCVEForImageGQL(config searchConfig, image, searchedCveID string) error {
username, password := getUsernameAndPassword(*config.user)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var cveList *cveResult
err := zcommon.RetryWithContext(ctx, func(attempt int, retryIn time.Duration) error {
var err error
cveList, err = config.searchService.getCveByImageGQL(ctx, config, username, password, image, searchedCveID)
if err != nil {
if !strings.Contains(err.Error(), zerr.ErrCVEDBNotFound.Error()) {
cancel()
return err
}
fmt.Fprintf(config.resultWriter,
"[warning] CVE DB is not ready [%d] - retry in %d seconds\n", attempt, int(retryIn.Seconds()))
}
return err
}, maxRetries, cveDBRetryInterval*time.Second)
if err != nil {
return err
}
if len(cveList.Data.CVEListForImage.CVEList) == 0 {
fmt.Fprint(config.resultWriter, "No CVEs found for image\n")
return nil
}
var builder strings.Builder
if *config.outputFormat == defaultOutputFormat || *config.outputFormat == "" {
printCVETableHeader(&builder, *config.verbose, 0, 0, 0)
fmt.Fprint(config.resultWriter, builder.String())
}
out, err := cveList.string(*config.outputFormat)
if err != nil {
return err
}
fmt.Fprint(config.resultWriter, out)
return nil
}
func SearchImagesByCVEIDGQL(config searchConfig, repo, cveid string) error {
username, password := getUsernameAndPassword(*config.user)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var imageList *zcommon.ImagesForCve
err := zcommon.RetryWithContext(ctx, func(attempt int, retryIn time.Duration) error {
var err error
imageList, err = config.searchService.getTagsForCVEGQL(ctx, config, username, password,
repo, cveid)
if err != nil {
if !strings.Contains(err.Error(), zerr.ErrCVEDBNotFound.Error()) {
cancel()
return err
}
fmt.Fprintf(config.resultWriter,
"[warning] CVE DB is not ready [%d] - retry in %d seconds\n", attempt, int(retryIn.Seconds()))
}
return err
}, maxRetries, cveDBRetryInterval*time.Second)
if err != nil {
return err
}
imageListData := []imageStruct{}
for _, image := range imageList.Results {
imageListData = append(imageListData, imageStruct(image))
}
return printImageResult(config, imageListData)
}
func SearchFixedTagsGQL(config searchConfig, repo, cveid string) error {
username, password := getUsernameAndPassword(*config.user)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var fixedTags *zcommon.ImageListWithCVEFixedResponse
err := zcommon.RetryWithContext(ctx, func(attempt int, retryIn time.Duration) error {
var err error
fixedTags, err = config.searchService.getFixedTagsForCVEGQL(ctx, config, username, password,
repo, cveid)
if err != nil {
if !strings.Contains(err.Error(), zerr.ErrCVEDBNotFound.Error()) {
cancel()
return err
}
fmt.Fprintf(config.resultWriter,
"[warning] CVE DB is not ready [%d] - retry in %d seconds\n", attempt, int(retryIn.Seconds()))
}
return err
}, maxRetries, cveDBRetryInterval*time.Second)
if err != nil {
return err
}
imageList := make([]imageStruct, 0, len(fixedTags.Results))
for _, image := range fixedTags.Results {
imageList = append(imageList, imageStruct(image))
}
return printImageResult(config, imageList)
}
func GlobalSearchGQL(config searchConfig, query string) error {
username, password := getUsernameAndPassword(*config.user)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
globalSearchResult, err := config.searchService.globalSearchGQL(ctx, config, username, password, query)
if err != nil {
return err
}
imagesList := []imageStruct{}
for _, image := range globalSearchResult.Images {
imagesList = append(imagesList, imageStruct(image))
}
reposList := []repoStruct{}
for _, repo := range globalSearchResult.Repos {
reposList = append(reposList, repoStruct(repo))
}
if err := printImageResult(config, imagesList); err != nil {
return err
}
return printRepoResults(config, reposList)
}
func SearchReferrersGQL(config searchConfig, subject string) error {
username, password := getUsernameAndPassword(*config.user)
repo, ref, refIsTag, err := zcommon.GetRepoReference(subject)
if err != nil {
return err
}
digest := ref
if refIsTag {
digest, err = fetchImageDigest(repo, ref, username, password, config)
if err != nil {
return err
}
}
response, err := config.searchService.getReferrersGQL(context.Background(), config, username, password, repo, digest)
if err != nil {
return err
}
referrersList := referrersResult(response.Referrers)
maxArtifactTypeLen := math.MinInt
for _, referrer := range referrersList {
if maxArtifactTypeLen < len(referrer.ArtifactType) {
maxArtifactTypeLen = len(referrer.ArtifactType)
}
}
printReferrersTableHeader(config, config.resultWriter, maxArtifactTypeLen)
return printReferrersResult(config, referrersList, maxArtifactTypeLen)
}
func SearchReferrers(config searchConfig, subject string) error {
username, password := getUsernameAndPassword(*config.user)
repo, ref, refIsTag, err := zcommon.GetRepoReference(subject)
if err != nil {
return err
}
digest := ref
if refIsTag {
digest, err = fetchImageDigest(repo, ref, username, password, config)
if err != nil {
return err
}
}
referrersList, err := config.searchService.getReferrers(context.Background(), config, username, password,
repo, digest)
if err != nil {
return err
}
maxArtifactTypeLen := math.MinInt
for _, referrer := range referrersList {
if maxArtifactTypeLen < len(referrer.ArtifactType) {
maxArtifactTypeLen = len(referrer.ArtifactType)
}
}
printReferrersTableHeader(config, config.resultWriter, maxArtifactTypeLen)
return printReferrersResult(config, referrersList, maxArtifactTypeLen)
}
func SearchRepos(config searchConfig) error {
username, password := getUsernameAndPassword(*config.user)
repoErr := make(chan stringResult)
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
wg.Add(1)
go config.searchService.getRepos(ctx, config, username, password, repoErr, &wg)
wg.Add(1)
errCh := make(chan error, 1)
go collectResults(config, &wg, repoErr, cancel, printImageTableHeader, errCh)
wg.Wait()
select {
case err := <-errCh:
return err
default:
return nil
}
}

View file

@ -0,0 +1,761 @@
//go:build search
// +build search
//
//nolint:dupl
package cli //nolint:testpackage
import (
"bytes"
"context"
"io"
"os"
"regexp"
"strings"
"sync"
"testing"
"time"
godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
. "github.com/smartystreets/goconvey/convey"
"github.com/spf13/cobra"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/cli/cmdflags"
"zotregistry.io/zot/pkg/common"
)
func TestSearchAllImages(t *testing.T) {
Convey("SearchAllImages", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getAllImagesFn: func(ctx context.Context, config searchConfig, username, password string,
channel chan stringResult, wtgrp *sync.WaitGroup,
) {
str, err := getMockImageStruct().stringPlainText(10, 10, 10, false)
channel <- stringResult{StrValue: str, Err: err}
},
})
err := SearchAllImages(searchConfig)
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B")
})
}
func TestSearchAllImagesGQL(t *testing.T) {
Convey("SearchAllImagesGQL", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getImagesGQLFn: func(ctx context.Context, config searchConfig, username, password, imageName string,
) (*common.ImageListResponse, error) {
return &common.ImageListResponse{ImageList: common.ImageList{
PaginatedImagesResult: common.PaginatedImagesResult{
Results: []common.ImageSummary{getMockImageSummary()},
},
}}, nil
},
})
err := SearchAllImagesGQL(searchConfig)
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B")
})
Convey("SearchAllImagesGQL error", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getImagesGQLFn: func(ctx context.Context, config searchConfig, username, password, imageName string,
) (*common.ImageListResponse, error) {
return &common.ImageListResponse{ImageList: common.ImageList{
PaginatedImagesResult: common.PaginatedImagesResult{
Results: []common.ImageSummary{getMockImageSummary()},
},
}}, zerr.ErrInjected
},
})
err := SearchAllImagesGQL(searchConfig)
So(err, ShouldNotBeNil)
})
}
func TestSearchImageByName(t *testing.T) {
Convey("SearchImageByName", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getImageByNameFn: func(ctx context.Context, config searchConfig, username string, password string, imageName string,
channel chan stringResult, wtgrp *sync.WaitGroup,
) {
str, err := getMockImageStruct().stringPlainText(10, 10, 10, false)
channel <- stringResult{StrValue: str, Err: err}
},
})
err := SearchImageByName(searchConfig, "repo")
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B")
})
Convey("SearchImageByName error", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getImageByNameFn: func(ctx context.Context, config searchConfig, username string, password string, imageName string,
channel chan stringResult, wtgrp *sync.WaitGroup,
) {
channel <- stringResult{StrValue: "", Err: zerr.ErrInjected}
},
})
err := SearchImageByName(searchConfig, "repo")
So(err, ShouldNotBeNil)
})
}
func TestSearchImageByNameGQL(t *testing.T) {
Convey("SearchImageByNameGQL", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getImagesGQLFn: func(ctx context.Context, config searchConfig, username, password, imageName string,
) (*common.ImageListResponse, error) {
return &common.ImageListResponse{ImageList: common.ImageList{
PaginatedImagesResult: common.PaginatedImagesResult{
Results: []common.ImageSummary{getMockImageSummary()},
},
}}, nil
},
})
err := SearchImageByNameGQL(searchConfig, "repo")
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B")
})
Convey("SearchImageByNameGQL error", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getImagesGQLFn: func(ctx context.Context, config searchConfig, username, password, imageName string,
) (*common.ImageListResponse, error) {
return &common.ImageListResponse{ImageList: common.ImageList{
PaginatedImagesResult: common.PaginatedImagesResult{
Results: []common.ImageSummary{getMockImageSummary()},
},
}}, zerr.ErrInjected
},
})
err := SearchImageByNameGQL(searchConfig, "repo")
So(err, ShouldNotBeNil)
})
}
func TestSearchImagesByDigest(t *testing.T) {
Convey("SearchImagesByDigest", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getImagesByDigestFn: func(ctx context.Context, config searchConfig, username string, password string, digest string,
rch chan stringResult, wtgrp *sync.WaitGroup,
) {
str, err := getMockImageStruct().stringPlainText(10, 10, 10, false)
rch <- stringResult{StrValue: str, Err: err}
},
})
err := SearchImagesByDigest(searchConfig, godigest.FromString("str").String())
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B")
})
Convey("SearchImagesByDigest error", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getImagesByDigestFn: func(ctx context.Context, config searchConfig, username string, password string, digest string,
rch chan stringResult, wtgrp *sync.WaitGroup,
) {
rch <- stringResult{StrValue: "", Err: zerr.ErrInjected}
},
})
err := SearchImagesByDigest(searchConfig, godigest.FromString("str").String())
So(err, ShouldNotBeNil)
})
}
func TestSearchDerivedImageListGQL(t *testing.T) {
Convey("SearchDerivedImageListGQL", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getDerivedImageListGQLFn: func(ctx context.Context, config searchConfig, username string, password string,
derivedImage string) (*common.DerivedImageListResponse, error,
) {
return &common.DerivedImageListResponse{DerivedImageList: common.DerivedImageList{
PaginatedImagesResult: common.PaginatedImagesResult{
Results: []common.ImageSummary{
getMockImageSummary(),
},
},
}}, nil
},
})
err := SearchDerivedImageListGQL(searchConfig, "repo:tag")
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B")
})
Convey("SearchDerivedImageListGQL error", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getDerivedImageListGQLFn: func(ctx context.Context, config searchConfig, username string, password string,
derivedImage string) (*common.DerivedImageListResponse, error,
) {
return &common.DerivedImageListResponse{DerivedImageList: common.DerivedImageList{
PaginatedImagesResult: common.PaginatedImagesResult{Results: []common.ImageSummary{}},
}}, zerr.ErrInjected
},
})
err := SearchDerivedImageListGQL(searchConfig, "repo:tag")
So(err, ShouldNotBeNil)
})
}
func TestSearchBaseImageListGQL(t *testing.T) {
Convey("SearchBaseImageListGQL", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getBaseImageListGQLFn: func(ctx context.Context, config searchConfig, username string, password string,
derivedImage string) (*common.BaseImageListResponse, error,
) {
return &common.BaseImageListResponse{BaseImageList: common.BaseImageList{
PaginatedImagesResult: common.PaginatedImagesResult{Results: []common.ImageSummary{
getMockImageSummary(),
}},
}}, nil
},
})
err := SearchBaseImageListGQL(searchConfig, "repo:tag")
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B")
})
Convey("SearchBaseImageListGQL error", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getBaseImageListGQLFn: func(ctx context.Context, config searchConfig, username string, password string,
derivedImage string) (*common.BaseImageListResponse, error,
) {
return &common.BaseImageListResponse{BaseImageList: common.BaseImageList{
PaginatedImagesResult: common.PaginatedImagesResult{Results: []common.ImageSummary{}},
}}, zerr.ErrInjected
},
})
err := SearchBaseImageListGQL(searchConfig, "repo:tag")
So(err, ShouldNotBeNil)
})
}
func TestSearchImagesForDigestGQL(t *testing.T) {
Convey("SearchImagesForDigestGQL", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getImagesForDigestGQLFn: func(ctx context.Context, config searchConfig, username string,
password string, digest string) (*common.ImagesForDigest, error,
) {
return &common.ImagesForDigest{ImagesForDigestList: common.ImagesForDigestList{
PaginatedImagesResult: common.PaginatedImagesResult{
Results: []common.ImageSummary{getMockImageSummary()},
},
}}, nil
},
})
err := SearchImagesForDigestGQL(searchConfig, "digest")
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B")
})
Convey("SearchImagesForDigestGQL error", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getImagesForDigestGQLFn: func(ctx context.Context, config searchConfig, username string,
password string, digest string) (*common.ImagesForDigest, error,
) {
return &common.ImagesForDigest{ImagesForDigestList: common.ImagesForDigestList{
PaginatedImagesResult: common.PaginatedImagesResult{},
}}, zerr.ErrInjected
},
})
err := SearchImagesForDigestGQL(searchConfig, "digest")
So(err, ShouldNotBeNil)
})
}
func TestSearchCVEForImageGQL(t *testing.T) {
Convey("SearchCVEForImageGQL", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getCveByImageGQLFn: func(ctx context.Context, config searchConfig, username string, password string,
imageName string, searchedCVE string) (*cveResult, error,
) {
return &cveResult{
Data: cveData{
CVEListForImage: cveListForImage{
CVEList: []cve{
{
ID: "dummyCVEID",
Description: "Description of the CVE",
Title: "Title of that CVE",
Severity: "HIGH",
PackageList: []packageList{
{
Name: "packagename",
FixedVersion: "fixedver",
InstalledVersion: "installedver",
},
},
},
},
},
},
}, nil
},
})
err := SearchCVEForImageGQL(searchConfig, "repo-test", "dummyCVEID")
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "dummyCVEID HIGH Title of that CVE")
})
Convey("SearchCVEForImageGQL", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getCveByImageGQLFn: func(ctx context.Context, config searchConfig, username string, password string,
imageName string, searchedCVE string) (*cveResult, error,
) {
return &cveResult{}, zerr.ErrInjected
},
})
err := SearchCVEForImageGQL(searchConfig, "repo-test", "dummyCVEID")
So(err, ShouldNotBeNil)
})
}
func TestSearchImagesByCVEIDGQL(t *testing.T) {
Convey("SearchImagesByCVEIDGQL", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getTagsForCVEGQLFn: func(ctx context.Context, config searchConfig, username, password,
imageName, cveID string) (*common.ImagesForCve, error,
) {
return &common.ImagesForCve{
ImagesForCVEList: common.ImagesForCVEList{
PaginatedImagesResult: common.PaginatedImagesResult{
Results: []common.ImageSummary{
getMockImageSummary(),
},
},
},
}, nil
},
})
err := SearchImagesByCVEIDGQL(searchConfig, "repo", "CVE-12345")
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B")
})
Convey("SearchImagesByCVEIDGQL error", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getTagsForCVEGQLFn: func(ctx context.Context, config searchConfig, username, password,
imageName, cveID string) (*common.ImagesForCve, error,
) {
return &common.ImagesForCve{
ImagesForCVEList: common.ImagesForCVEList{
PaginatedImagesResult: common.PaginatedImagesResult{},
},
}, zerr.ErrInjected
},
})
err := SearchImagesByCVEIDGQL(searchConfig, "repo", "CVE-12345")
So(err, ShouldNotBeNil)
})
}
func TestSearchFixedTagsGQL(t *testing.T) {
Convey("SearchFixedTagsGQL", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getFixedTagsForCVEGQLFn: func(ctx context.Context, config searchConfig, username, password,
imageName, cveID string) (*common.ImageListWithCVEFixedResponse, error,
) {
return &common.ImageListWithCVEFixedResponse{
ImageListWithCVEFixed: common.ImageListWithCVEFixed{
PaginatedImagesResult: common.PaginatedImagesResult{
Results: []common.ImageSummary{getMockImageSummary()},
},
},
}, nil
},
})
err := SearchFixedTagsGQL(searchConfig, "repo", "CVE-12345")
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B")
})
Convey("SearchFixedTagsGQL error", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getFixedTagsForCVEGQLFn: func(ctx context.Context, config searchConfig, username, password,
imageName, cveID string) (*common.ImageListWithCVEFixedResponse, error,
) {
return &common.ImageListWithCVEFixedResponse{
ImageListWithCVEFixed: common.ImageListWithCVEFixed{
PaginatedImagesResult: common.PaginatedImagesResult{},
},
}, zerr.ErrInjected
},
})
err := SearchFixedTagsGQL(searchConfig, "repo", "CVE-12345")
So(err, ShouldNotBeNil)
})
}
func TestSearchReferrersGQL(t *testing.T) {
Convey("SearchReferrersGQL", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getReferrersGQLFn: func(ctx context.Context, config searchConfig, username, password,
repo, digest string) (*common.ReferrersResp, error,
) {
return &common.ReferrersResp{
ReferrersResult: common.ReferrersResult{
Referrers: []common.Referrer{{
MediaType: ispec.MediaTypeImageManifest,
Size: 100,
ArtifactType: "art.type",
Digest: godigest.FromString("123").String(),
}},
},
}, nil
},
})
err := SearchReferrersGQL(searchConfig, "repo@"+godigest.FromString("str").String())
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring,
"art.type 100 B sha256:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3")
})
Convey("SearchReferrersGQL error", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getReferrersGQLFn: func(ctx context.Context, config searchConfig, username, password,
repo, digest string) (*common.ReferrersResp, error,
) {
return &common.ReferrersResp{}, zerr.ErrInjected
},
})
err := SearchReferrersGQL(searchConfig, "repo@"+godigest.FromString("str").String())
So(err, ShouldNotBeNil)
})
}
func TestGlobalSearchGQL(t *testing.T) {
Convey("GlobalSearchGQL", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
globalSearchGQLFn: func(ctx context.Context, config searchConfig, username, password,
query string) (*common.GlobalSearch, error,
) {
return &common.GlobalSearch{
Repos: []common.RepoSummary{{
Name: "repo",
Size: "100",
LastUpdated: time.Date(2010, 1, 1, 1, 1, 1, 0, time.UTC),
}},
}, nil
},
})
err := GlobalSearchGQL(searchConfig, "repo")
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring,
"repo ")
})
Convey("GlobalSearchGQL error", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
globalSearchGQLFn: func(ctx context.Context, config searchConfig, username, password,
query string) (*common.GlobalSearch, error,
) {
return &common.GlobalSearch{}, zerr.ErrInjected
},
})
err := GlobalSearchGQL(searchConfig, "repo")
So(err, ShouldNotBeNil)
})
}
func TestSearchReferrers(t *testing.T) {
Convey("SearchReferrers", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getReferrersFn: func(ctx context.Context, config searchConfig, username string, password string,
repo string, digest string) (referrersResult, error,
) {
return referrersResult([]common.Referrer{
{
MediaType: ispec.MediaTypeImageManifest,
Size: 100,
ArtifactType: "art.type",
Digest: godigest.FromString("123").String(),
},
}), nil
},
})
err := SearchReferrers(searchConfig, "repo@"+godigest.FromString("str").String())
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring,
"art.type 100 B sha256:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3")
})
Convey("SearchReferrers error", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getReferrersFn: func(ctx context.Context, config searchConfig, username string, password string,
repo string, digest string) (referrersResult, error,
) {
return referrersResult{}, zerr.ErrInjected
},
})
err := SearchReferrers(searchConfig, "repo@"+godigest.FromString("str").String())
So(err, ShouldNotBeNil)
})
}
func TestSearchRepos(t *testing.T) {
Convey("SearchRepos", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{})
err := SearchRepos(searchConfig)
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "repo1")
So(actual, ShouldContainSubstring, "repo2")
})
}
func getMockSearchConfig(buff *bytes.Buffer, mockService mockService) searchConfig {
return searchConfig{
resultWriter: buff,
user: ref(""),
searchService: mockService,
servURL: ref("http://127.0.0.1:8000"),
outputFormat: ref(""),
verifyTLS: ref(false),
fixedFlag: ref(false),
verbose: ref(false),
debug: ref(false),
}
}
func getMockImageStruct() imageStruct {
return imageStruct(common.ImageSummary{
RepoName: "repo", Tag: "tag",
MediaType: ispec.MediaTypeImageManifest,
Digest: godigest.FromString("str").String(),
Size: "100",
Manifests: []common.ManifestSummary{{
Size: "100",
Platform: common.Platform{Os: "os", Arch: "arch"},
Digest: godigest.FromString("str").String(),
ConfigDigest: godigest.FromString("str").String(),
}},
})
}
func getMockImageSummary() common.ImageSummary {
return common.ImageSummary{
RepoName: "repo", Tag: "tag",
MediaType: ispec.MediaTypeImageManifest,
Digest: godigest.FromString("str").String(),
Size: "100",
Manifests: []common.ManifestSummary{{
Size: "100",
Platform: common.Platform{Os: "os", Arch: "arch"},
Digest: godigest.FromString("str").String(),
ConfigDigest: godigest.FromString("str").String(),
}},
}
}
func TestUtils(t *testing.T) {
Convey("Utils", t, func() {
ok := haveSameArgs(field{"query", []struct {
Name string "json:\"name\""
}{
{Name: "arg1"}, {Name: "arg2"},
}}, GQLQuery{
Name: "query", Args: []string{"arg1"},
})
So(ok, ShouldBeFalse)
ok = haveSameArgs(field{"query", []struct {
Name string "json:\"name\""
}{
{Name: "arg1"}, {Name: "arg2"},
}}, GQLQuery{
Name: "query", Args: []string{"arg1", "arg3"},
})
So(ok, ShouldBeFalse)
err := containsGQLQueryWithParams(
[]field{
{Name: "query"},
},
[]typeInfo{},
GQLQuery{Name: "other-name"},
)
So(err, ShouldNotBeNil)
})
Convey("GetConfigOptions", t, func() {
// no flags
cmd := &cobra.Command{}
isSpinner, verifyTLS := GetCliConfigOptions(cmd)
So(isSpinner, ShouldBeFalse)
So(verifyTLS, ShouldBeFalse)
// bad showspinner
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":"bad", "verify-tls": false}]}`)
cmd = &cobra.Command{}
cmd.Flags().String(cmdflags.ConfigFlag, "imagetest", "")
isSpinner, verifyTLS = GetCliConfigOptions(cmd)
So(isSpinner, ShouldBeFalse)
So(verifyTLS, ShouldBeFalse)
os.Remove(configPath)
// bad verify-tls
configPath = makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false, "verify-tls": "bad"}]}`)
cmd = &cobra.Command{}
cmd.Flags().String(cmdflags.ConfigFlag, "imagetest", "")
isSpinner, verifyTLS = GetCliConfigOptions(cmd)
So(isSpinner, ShouldBeFalse)
So(verifyTLS, ShouldBeFalse)
os.Remove(configPath)
})
Convey("GetServerURLFromFlags", t, func() {
cmd := &cobra.Command{}
cmd.Flags().String(cmdflags.URLFlag, "url", "")
url, err := GetServerURLFromFlags(cmd)
So(url, ShouldResemble, "url")
So(err, ShouldBeNil)
// err no config or url
cmd = &cobra.Command{}
url, err = GetServerURLFromFlags(cmd)
So(url, ShouldResemble, "")
So(err, ShouldNotBeNil)
// err ulr from config is empty
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest"}]}`)
cmd = &cobra.Command{}
cmd.Flags().String(cmdflags.ConfigFlag, "imagetest", "")
url, err = GetServerURLFromFlags(cmd)
So(url, ShouldResemble, "")
So(err, ShouldNotBeNil)
os.Remove(configPath)
// err reading the server url from config
configPath = makeConfigFile("{}")
cmd = &cobra.Command{}
cmd.Flags().String(cmdflags.ConfigFlag, "imagetest", "")
url, err = GetServerURLFromFlags(cmd)
So(url, ShouldResemble, "")
So(err, ShouldNotBeNil)
os.Remove(configPath)
})
Convey("CheckExtEndPointQuery", t, func() {
// invalid url
err := CheckExtEndPointQuery(searchConfig{
user: ref(""),
servURL: ref("bad-url"),
})
So(err, ShouldNotBeNil)
// good url but no connection
err = CheckExtEndPointQuery(searchConfig{
user: ref(""),
servURL: ref("http://127.0.0.1:5000"),
verifyTLS: ref(false),
debug: ref(false),
resultWriter: io.Discard,
})
So(err, ShouldNotBeNil)
})
}

109
pkg/cli/search_sub_cmd.go Normal file
View file

@ -0,0 +1,109 @@
//go:build search
// +build search
package cli
import (
"fmt"
godigest "github.com/opencontainers/go-digest"
"github.com/spf13/cobra"
zerr "zotregistry.io/zot/errors"
zcommon "zotregistry.io/zot/pkg/common"
)
func NewSearchSubjectCommand(searchService SearchService) *cobra.Command {
imageCmd := &cobra.Command{
Use: "subject [repo:tag]|[repo@digest]",
Short: "List all referrers for this subject.",
Long: `List all referrers for this subject. The subject can be specified by tag(repo:tag) or by digest" +
"(repo@digest)`,
Example: `# For referrers search specify the referred subject using it's full digest or tag:
zli search subject "repo@sha256:f9a0981..."
zli search subject "repo:tag"`,
Args: OneImageWithRefArg,
RunE: func(cmd *cobra.Command, args []string) error {
searchConfig, err := GetSearchConfigFromFlags(cmd, searchService)
if err != nil {
return err
}
if err := CheckExtEndPointQuery(searchConfig, ReferrersQuery()); err == nil {
return SearchReferrersGQL(searchConfig, args[0])
} else {
return SearchReferrers(searchConfig, args[0])
}
},
}
return imageCmd
}
func NewSearchQueryCommand(searchService SearchService) *cobra.Command {
imageCmd := &cobra.Command{
Use: "query",
Short: "Fuzzy search for repos and their tags.",
Long: "Fuzzy search for repos and their tags.",
Example: `# For repo search specify a substring of the repo name without the tag
zli search query "test/repo"
# For image search specify the full repo name followed by the tag or a prefix of the tag.
zli search query "test/repo:2.1."
# To search all tags in all repos.
zli search query ":"`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
searchConfig, err := GetSearchConfigFromFlags(cmd, searchService)
if err != nil {
return err
}
// special format for searching all images and tags
if args[0] == ":" {
err := CheckExtEndPointQuery(searchConfig, GlobalSearchQuery())
if err != nil {
return fmt.Errorf("%w: '%s'", err, ImageListQuery().Name)
}
return SearchAllImagesGQL(searchConfig)
}
if err := CheckExtEndPointQuery(searchConfig, GlobalSearchQuery()); err != nil {
return fmt.Errorf("%w: '%s'", err, CVEListForImageQuery().Name)
}
return GlobalSearchGQL(searchConfig, args[0])
},
}
return imageCmd
}
func OneImageWithRefArg(cmd *cobra.Command, args []string) error {
if err := cobra.ExactArgs(1)(cmd, args); err != nil {
return err
}
image := args[0]
if dir, ref, _ := zcommon.GetImageDirAndReference(image); dir == "" || ref == "" {
return zerr.ErrInvalidRepoRefFormat
}
return nil
}
func OneDigestArg(cmd *cobra.Command, args []string) error {
if err := cobra.ExactArgs(1)(cmd, args); err != nil {
return err
}
digest := args[0]
if _, err := godigest.Parse(digest); err != nil {
return err
}
return nil
}

View file

@ -15,7 +15,7 @@ import (
"github.com/briandowns/spinner" "github.com/briandowns/spinner"
zotErrors "zotregistry.io/zot/errors" zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api/constants" "zotregistry.io/zot/pkg/api/constants"
zcommon "zotregistry.io/zot/pkg/common" zcommon "zotregistry.io/zot/pkg/common"
) )
@ -326,7 +326,7 @@ func (search imagesByDigestSearcherGQL) search(config searchConfig) (bool, error
defer cancel() defer cancel()
imageList, err := config.searchService.getImagesByDigestGQL(ctx, config, username, password, *config.params["digest"]) imageList, err := config.searchService.getImagesForDigestGQL(ctx, config, username, password, *config.params["digest"])
if err != nil { if err != nil {
return true, err return true, err
} }
@ -770,7 +770,7 @@ func (search globalSearcherREST) search(config searchConfig) (bool, error) {
return false, nil return false, nil
} }
return true, fmt.Errorf("search extension is not enabled: %w", zotErrors.ErrExtensionNotEnabled) return true, fmt.Errorf("search extension is not enabled: %w", zerr.ErrExtensionNotEnabled)
} }
func collectResults(config searchConfig, wg *sync.WaitGroup, imageErr chan stringResult, func collectResults(config searchConfig, wg *sync.WaitGroup, imageErr chan stringResult,
@ -813,7 +813,7 @@ func collectResults(config searchConfig, wg *sync.WaitGroup, imageErr chan strin
config.spinner.stopSpinner() config.spinner.stopSpinner()
cancel() cancel()
errCh <- zotErrors.ErrCLITimeout errCh <- zerr.ErrCLITimeout
return return
} }

View file

@ -20,7 +20,7 @@ import (
ispec "github.com/opencontainers/image-spec/specs-go/v1" ispec "github.com/opencontainers/image-spec/specs-go/v1"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
zotErrors "zotregistry.io/zot/errors" zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api/constants" "zotregistry.io/zot/pkg/api/constants"
"zotregistry.io/zot/pkg/common" "zotregistry.io/zot/pkg/common"
) )
@ -34,13 +34,13 @@ const (
type SearchService interface { //nolint:interfacebloat type SearchService interface { //nolint:interfacebloat
getImagesGQL(ctx context.Context, config searchConfig, username, password string, getImagesGQL(ctx context.Context, config searchConfig, username, password string,
imageName string) (*common.ImageListResponse, error) imageName string) (*common.ImageListResponse, error)
getImagesByDigestGQL(ctx context.Context, config searchConfig, username, password string, getImagesForDigestGQL(ctx context.Context, config searchConfig, username, password string,
digest string) (*common.ImagesForDigest, error) digest string) (*common.ImagesForDigest, error)
getCveByImageGQL(ctx context.Context, config searchConfig, username, password, getCveByImageGQL(ctx context.Context, config searchConfig, username, password,
imageName string, searchedCVE string) (*cveResult, error) imageName string, searchedCVE string) (*cveResult, error)
getImagesByCveIDGQL(ctx context.Context, config searchConfig, username, password string, getImagesByCveIDGQL(ctx context.Context, config searchConfig, username, password string,
digest string) (*common.ImagesForCve, error) digest string) (*common.ImagesForCve, error)
getTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName, getTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, repo,
cveID string) (*common.ImagesForCve, error) cveID string) (*common.ImagesForCve, error)
getFixedTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName, getFixedTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName,
cveID string) (*common.ImageListWithCVEFixedResponse, error) cveID string) (*common.ImageListWithCVEFixedResponse, error)
@ -260,7 +260,7 @@ func (service searchService) getImagesGQL(ctx context.Context, config searchConf
return result, nil return result, nil
} }
func (service searchService) getImagesByDigestGQL(ctx context.Context, config searchConfig, username, password string, func (service searchService) getImagesForDigestGQL(ctx context.Context, config searchConfig, username, password string,
digest string, digest string,
) (*common.ImagesForDigest, error) { ) (*common.ImagesForDigest, error) {
query := fmt.Sprintf(` query := fmt.Sprintf(`
@ -354,7 +354,7 @@ func (service searchService) getCveByImageGQL(ctx context.Context, config search
} }
func (service searchService) getTagsForCVEGQL(ctx context.Context, config searchConfig, func (service searchService) getTagsForCVEGQL(ctx context.Context, config searchConfig,
username, password, imageName, cveID string, username, password, repo, cveID string,
) (*common.ImagesForCve, error) { ) (*common.ImagesForCve, error) {
query := fmt.Sprintf(` query := fmt.Sprintf(`
{ {
@ -387,7 +387,19 @@ func (service searchService) getTagsForCVEGQL(ctx context.Context, config search
return nil, errResult return nil, errResult
} }
return result, nil if repo == "" {
return result, nil
}
filteredResults := &common.ImagesForCve{}
for _, image := range result.Results {
if image.RepoName == repo {
filteredResults.Results = append(filteredResults.Results, image)
}
}
return filteredResults, nil
} }
func (service searchService) getFixedTagsForCVEGQL(ctx context.Context, config searchConfig, func (service searchService) getFixedTagsForCVEGQL(ctx context.Context, config searchConfig,
@ -537,7 +549,9 @@ func getImage(ctx context.Context, config searchConfig, username, password, imag
) { ) {
defer wtgrp.Done() defer wtgrp.Done()
tagListEndpoint, err := combineServerAndEndpointURL(*config.servURL, fmt.Sprintf("/v2/%s/tags/list", imageName)) repo, imageTag := common.GetImageDirAndTag(imageName)
tagListEndpoint, err := combineServerAndEndpointURL(*config.servURL, fmt.Sprintf("/v2/%s/tags/list", repo))
if err != nil { if err != nil {
if isContextDone(ctx) { if isContextDone(ctx) {
return return
@ -570,9 +584,17 @@ func getImage(ctx context.Context, config searchConfig, username, password, imag
continue continue
} }
shouldMatchTag := imageTag != ""
matchesTag := tag == imageTag
// when the tag is empty we match everything
if shouldMatchTag && !matchesTag {
continue
}
wtgrp.Add(1) wtgrp.Add(1)
go addManifestCallToPool(ctx, config, pool, username, password, imageName, tag, rch, wtgrp) go addManifestCallToPool(ctx, config, pool, username, password, repo, tag, rch, wtgrp)
} }
} }
@ -787,7 +809,7 @@ func (service searchService) getImageByNameAndCVEID(ctx context.Context, config
go rlim.startRateLimiter(ctx) go rlim.startRateLimiter(ctx)
for _, image := range result.Results { for _, image := range result.Results {
if !strings.EqualFold(imageName, image.RepoName) { if imageName != "" && !strings.EqualFold(imageName, image.RepoName) {
continue continue
} }
@ -1438,7 +1460,7 @@ func addManifestToTable(table *tablewriter.Table, imageName, tagName string, man
platform += offset platform += offset
} }
minifestDigestStr := ellipsize(manifestDigest.Encoded(), digestWidth, "") manifestDigestStr := ellipsize(manifestDigest.Encoded(), digestWidth, "")
configDigestStr := ellipsize(configDigest.Encoded(), configWidth, "") configDigestStr := ellipsize(configDigest.Encoded(), configWidth, "")
imgSize, _ := strconv.ParseUint(manifest.Size, 10, 64) imgSize, _ := strconv.ParseUint(manifest.Size, 10, 64)
size := ellipsize(strings.ReplaceAll(humanize.Bytes(imgSize), " ", ""), sizeWidth, ellipsis) size := ellipsize(strings.ReplaceAll(humanize.Bytes(imgSize), " ", ""), sizeWidth, ellipsis)
@ -1447,7 +1469,7 @@ func addManifestToTable(table *tablewriter.Table, imageName, tagName string, man
row[colImageNameIndex] = imageName row[colImageNameIndex] = imageName
row[colTagIndex] = tagName row[colTagIndex] = tagName
row[colDigestIndex] = minifestDigestStr row[colDigestIndex] = manifestDigestStr
row[colPlatformIndex] = platform row[colPlatformIndex] = platform
row[colSizeIndex] = size row[colSizeIndex] = size
row[colIsSignedIndex] = strconv.FormatBool(isSigned) row[colIsSignedIndex] = strconv.FormatBool(isSigned)
@ -1487,23 +1509,23 @@ func addManifestToTable(table *tablewriter.Table, imageName, tagName string, man
return nil return nil
} }
func getPlatformStr(platf common.Platform) string { func getPlatformStr(platform common.Platform) string {
if platf.Arch == "" && platf.Os == "" { if platform.Arch == "" && platform.Os == "" {
return "" return ""
} }
platform := platf.Os fullPlatform := platform.Os
if platf.Arch != "" { if platform.Arch != "" {
platform = platform + "/" + platf.Arch fullPlatform = fullPlatform + "/" + platform.Arch
platform = strings.Trim(platform, "/") fullPlatform = strings.Trim(fullPlatform, "/")
if platf.Variant != "" { if platform.Variant != "" {
platform = platform + "/" + platf.Variant fullPlatform = fullPlatform + "/" + platform.Variant
} }
} }
return platform return fullPlatform
} }
func (img imageStruct) stringJSON() (string, error) { func (img imageStruct) stringJSON() (string, error) {
@ -1534,12 +1556,12 @@ type catalogResponse struct {
func combineServerAndEndpointURL(serverURL, endPoint string) (string, error) { func combineServerAndEndpointURL(serverURL, endPoint string) (string, error) {
if !isURL(serverURL) { if !isURL(serverURL) {
return "", zotErrors.ErrInvalidURL return "", zerr.ErrInvalidURL
} }
newURL, err := url.Parse(serverURL) newURL, err := url.Parse(serverURL)
if err != nil { if err != nil {
return "", zotErrors.ErrInvalidURL return "", zerr.ErrInvalidURL
} }
newURL, _ = newURL.Parse(endPoint) newURL, _ = newURL.Parse(endPoint)

View file

@ -111,7 +111,7 @@ func GetRepoReference(repo string) (string, string, bool, error) {
return repoName, digest, false, nil return repoName, digest, false, nil
} }
// GetFullImageName returns the formated string for the given repo/tag or repo/digest. // GetFullImageName returns the formatted string for the given repo/tag or repo/digest.
func GetFullImageName(repo, ref string) string { func GetFullImageName(repo, ref string) string {
if IsTag(ref) { if IsTag(ref) {
return repo + ":" + ref return repo + ":" + ref
@ -129,3 +129,7 @@ func IsDigest(ref string) bool {
func IsTag(ref string) bool { func IsTag(ref string) bool {
return !IsDigest(ref) return !IsDigest(ref)
} }
func CheckIsCorrectRepoNameFormat(repo string) bool {
return !strings.ContainsAny(repo, ":@")
}

24
pkg/common/retry.go Normal file
View file

@ -0,0 +1,24 @@
package common
import (
"context"
"time"
)
func RetryWithContext(ctx context.Context, operation func(attempt int, retryIn time.Duration) error, maxRetries int,
delay time.Duration,
) error {
err := operation(1, delay)
for attempt := 1; err != nil && attempt < maxRetries; attempt++ {
select {
case <-time.After(delay):
case <-ctx.Done():
return err
}
err = operation(attempt+1, delay)
}
return err
}

View file

@ -9,7 +9,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
"zotregistry.io/zot/errors" zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/exporter/api" "zotregistry.io/zot/pkg/exporter/api"
) )
@ -66,12 +66,12 @@ func loadConfiguration(config *api.Config, configPath string) {
metaData := &mapstructure.Metadata{} metaData := &mapstructure.Metadata{}
if err := viper.Unmarshal(&config, metadataConfig(metaData)); err != nil { if err := viper.Unmarshal(&config, metadataConfig(metaData)); err != nil {
log.Error().Err(err).Msg("Error while unmarshalling new config") log.Error().Err(err).Msg("Error while unmarshaling new config")
panic(err) panic(err)
} }
if len(metaData.Keys) == 0 || len(metaData.Unused) > 0 { if len(metaData.Keys) == 0 || len(metaData.Unused) > 0 {
log.Error().Err(errors.ErrBadConfig).Msg("Bad configuration, retry writing it") log.Error().Err(zerr.ErrBadConfig).Msg("Bad configuration, retry writing it")
panic(errors.ErrBadConfig) panic(zerr.ErrBadConfig)
} }
} }

View file

@ -38,7 +38,7 @@ func GetExtensions(config *config.Config) distext.ExtensionList {
if len(endpoints) > 0 { if len(endpoints) > 0 {
extensions = append(extensions, distext.Extension{ extensions = append(extensions, distext.Extension{
Name: "_zot", Name: constants.BaseExtension,
URL: "https://github.com/project-zot/zot/blob/" + config.ReleaseTag + "/pkg/extensions/_zot.md", URL: "https://github.com/project-zot/zot/blob/" + config.ReleaseTag + "/pkg/extensions/_zot.md",
Description: "zot registry extensions", Description: "zot registry extensions",
Endpoints: endpoints, Endpoints: endpoints,

View file

@ -30,7 +30,7 @@ type ImageAnnotations struct {
/* /*
OCI annotation/label with backwards compatibility OCI annotation/label with backwards compatibility
arg can be either lables or annotations arg can be either labels or annotations
https://github.com/opencontainers/image-spec/blob/main/annotations.md. https://github.com/opencontainers/image-spec/blob/main/annotations.md.
*/ */
func GetAnnotationValue(annotations map[string]string, annotationKey, labelKey string) string { func GetAnnotationValue(annotations map[string]string, annotationKey, labelKey string) string {

View file

@ -61,7 +61,7 @@ func TestConvertErrors(t *testing.T) {
err = metaDB.SetRepoReference("repo1", "0.1.0", digest11, ispec.MediaTypeImageManifest) err = metaDB.SetRepoReference("repo1", "0.1.0", digest11, ispec.MediaTypeImageManifest)
So(err, ShouldBeNil) So(err, ShouldBeNil)
repoMetas, manifestMetaMap, _, err := metaDB.SearchRepos(context.Background(), "") reposMeta, manifestMetaMap, _, err := metaDB.SearchRepos(context.Background(), "")
So(err, ShouldBeNil) So(err, ShouldBeNil)
ctx := graphql.WithResponseContext(context.Background(), ctx := graphql.WithResponseContext(context.Background(),
@ -69,7 +69,7 @@ func TestConvertErrors(t *testing.T) {
_ = convert.RepoMeta2RepoSummary( _ = convert.RepoMeta2RepoSummary(
ctx, ctx,
repoMetas[0], reposMeta[0],
manifestMetaMap, manifestMetaMap,
map[string]mTypes.IndexData{}, map[string]mTypes.IndexData{},
convert.SkipQGLField{}, convert.SkipQGLField{},
@ -286,7 +286,7 @@ func TestConvertErrors(t *testing.T) {
}) })
} }
func TestUpdateLastUpdatedTimestam(t *testing.T) { func TestUpdateLastUpdatedTimestamp(t *testing.T) {
Convey("Image summary is the first image checked for the repo", t, func() { Convey("Image summary is the first image checked for the repo", t, func() {
before := time.Time{} before := time.Time{}
after := time.Date(2023, time.April, 1, 11, 0, 0, 0, time.UTC) after := time.Date(2023, time.April, 1, 11, 0, 0, 0, time.UTC)

View file

@ -44,7 +44,7 @@ func RepoMeta2RepoSummary(ctx context.Context, repoMeta mTypes.RepoMetadata,
repoIsUserStarred = repoMeta.IsStarred // value specific to the current user repoIsUserStarred = repoMeta.IsStarred // value specific to the current user
repoIsUserBookMarked = repoMeta.IsBookmarked // value specific to the current user repoIsUserBookMarked = repoMeta.IsBookmarked // value specific to the current user
// map used to keep track of all blobs of a repo without dublicates as // map used to keep track of all blobs of a repo without duplicates as
// some images may have the same layers // some images may have the same layers
repoBlob2Size = make(map[string]int64, 10) repoBlob2Size = make(map[string]int64, 10)
@ -140,7 +140,7 @@ func RepoMeta2RepoSummary(ctx context.Context, repoMeta mTypes.RepoMetadata,
} }
} }
func PaginatedRepoMeta2RepoSummaries(ctx context.Context, repoMetas []mTypes.RepoMetadata, func PaginatedRepoMeta2RepoSummaries(ctx context.Context, reposMeta []mTypes.RepoMetadata,
manifestMetaMap map[string]mTypes.ManifestMetadata, indexDataMap map[string]mTypes.IndexData, manifestMetaMap map[string]mTypes.ManifestMetadata, indexDataMap map[string]mTypes.IndexData,
skip SkipQGLField, cveInfo cveinfo.CveInfo, filter mTypes.Filter, pageInput pagination.PageInput, skip SkipQGLField, cveInfo cveinfo.CveInfo, filter mTypes.Filter, pageInput pagination.PageInput,
) ([]*gql_generated.RepoSummary, zcommon.PageInfo, error) { ) ([]*gql_generated.RepoSummary, zcommon.PageInfo, error) {
@ -149,7 +149,7 @@ func PaginatedRepoMeta2RepoSummaries(ctx context.Context, repoMetas []mTypes.Rep
return []*gql_generated.RepoSummary{}, zcommon.PageInfo{}, err return []*gql_generated.RepoSummary{}, zcommon.PageInfo{}, err
} }
for _, repoMeta := range repoMetas { for _, repoMeta := range reposMeta {
repoSummary := RepoMeta2RepoSummary(ctx, repoMeta, manifestMetaMap, indexDataMap, skip, cveInfo) repoSummary := RepoMeta2RepoSummary(ctx, repoMeta, manifestMetaMap, indexDataMap, skip, cveInfo)
if RepoSumAcceptedByFilter(repoSummary, filter) { if RepoSumAcceptedByFilter(repoSummary, filter) {
@ -679,7 +679,7 @@ func RepoMeta2ExpandedRepoInfo(ctx context.Context, repoMeta mTypes.RepoMetadata
isStarred = repoMeta.IsStarred // value specific to the current user isStarred = repoMeta.IsStarred // value specific to the current user
isBookmarked = repoMeta.IsBookmarked // value specific to the current user isBookmarked = repoMeta.IsBookmarked // value specific to the current user
// map used to keep track of all blobs of a repo without dublicates as // map used to keep track of all blobs of a repo without duplicates as
// some images may have the same layers // some images may have the same layers
repoBlob2Size = make(map[string]int64, 10) repoBlob2Size = make(map[string]int64, 10)
@ -694,7 +694,7 @@ func RepoMeta2ExpandedRepoInfo(ctx context.Context, repoMeta mTypes.RepoMetadata
skip.Vulnerabilities, repoMeta, manifestMetaMap, indexDataMap, cveInfo) skip.Vulnerabilities, repoMeta, manifestMetaMap, indexDataMap, cveInfo)
if err != nil { if err != nil {
log.Error().Str("repository", repoName).Str("reference", tag). log.Error().Str("repository", repoName).Str("reference", tag).
Msg("metadb: erorr while converting descriptor for image") Msg("metadb: error while converting descriptor for image")
continue continue
} }

View file

@ -599,9 +599,9 @@ func TestCVESearch(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(len(cveResult.ImgList.CVEResultForImage.CVEList), ShouldNotBeZeroValue) So(len(cveResult.ImgList.CVEResultForImage.CVEList), ShouldNotBeZeroValue)
cvid := cveResult.ImgList.CVEResultForImage.CVEList[0].ID cveid := cveResult.ImgList.CVEResultForImage.CVEList[0].ID
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-test\"){Results{RepoName%20LastUpdated}}}") resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cveid + "\",image:\"zot-test\"){Results{RepoName%20LastUpdated}}}")
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200) So(resp.StatusCode(), ShouldEqual, 200)
@ -610,7 +610,7 @@ func TestCVESearch(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(len(imgListWithCVEFixed.Images), ShouldEqual, 0) So(len(imgListWithCVEFixed.Images), ShouldEqual, 0)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-cve-test\"){Results{RepoName%20LastUpdated}}}") resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cveid + "\",image:\"zot-cve-test\"){Results{RepoName%20LastUpdated}}}")
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200) So(resp.StatusCode(), ShouldEqual, 200)
@ -618,7 +618,7 @@ func TestCVESearch(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(len(imgListWithCVEFixed.Images), ShouldEqual, 0) So(len(imgListWithCVEFixed.Images), ShouldEqual, 0)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-test\"){Results{RepoName%20LastUpdated}}}") resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cveid + "\",image:\"zot-test\"){Results{RepoName%20LastUpdated}}}")
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200) So(resp.StatusCode(), ShouldEqual, 200)
@ -635,7 +635,7 @@ func TestCVESearch(t *testing.T) {
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200) So(resp.StatusCode(), ShouldEqual, 200)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-noindex\"){Results{RepoName%20LastUpdated}}}") resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cveid + "\",image:\"zot-squashfs-noindex\"){Results{RepoName%20LastUpdated}}}")
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200) So(resp.StatusCode(), ShouldEqual, 200)
@ -643,7 +643,7 @@ func TestCVESearch(t *testing.T) {
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200) So(resp.StatusCode(), ShouldEqual, 200)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-invalid-index\"){Results{RepoName%20LastUpdated}}}") resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cveid + "\",image:\"zot-squashfs-invalid-index\"){Results{RepoName%20LastUpdated}}}")
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200) So(resp.StatusCode(), ShouldEqual, 200)
@ -651,11 +651,11 @@ func TestCVESearch(t *testing.T) {
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200) So(resp.StatusCode(), ShouldEqual, 200)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-noblob\"){Results{RepoName%20LastUpdated}}}") resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cveid + "\",image:\"zot-squashfs-noblob\"){Results{RepoName%20LastUpdated}}}")
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200) So(resp.StatusCode(), ShouldEqual, 200)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-test\"){Results{RepoName%20LastUpdated}}}") resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cveid + "\",image:\"zot-squashfs-test\"){Results{RepoName%20LastUpdated}}}")
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200) So(resp.StatusCode(), ShouldEqual, 200)
@ -663,7 +663,7 @@ func TestCVESearch(t *testing.T) {
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200) So(resp.StatusCode(), ShouldEqual, 200)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-invalid-blob\"){Results{RepoName%20LastUpdated}}}") resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cveid + "\",image:\"zot-squashfs-invalid-blob\"){Results{RepoName%20LastUpdated}}}")
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200) So(resp.StatusCode(), ShouldEqual, 200)
@ -732,7 +732,7 @@ func TestCVESearch(t *testing.T) {
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 422) So(resp.StatusCode(), ShouldEqual, 422)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListForCVE(id:\"" + cvid + "\"){Results{RepoName%20Tag}}}") resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListForCVE(id:\"" + cveid + "\"){Results{RepoName%20Tag}}}")
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200) So(resp.StatusCode(), ShouldEqual, 200)
}) })
@ -1412,7 +1412,7 @@ func TestCVEStruct(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(len(tagList), ShouldEqual, 0) So(len(tagList), ShouldEqual, 0)
// Repo is not found, assume it is affetected by the CVE // Repo is not found, assume it is affected by the CVE
// But we don't have enough of it's data to actually return it // But we don't have enough of it's data to actually return it
tagList, err = cveInfo.GetImageListForCVE("repo100", "CVE100") tagList, err = cveInfo.GetImageListForCVE("repo100", "CVE100")
So(err, ShouldEqual, zerr.ErrRepoMetaNotFound) So(err, ShouldEqual, zerr.ErrRepoMetaNotFound)

View file

@ -123,7 +123,7 @@ func TestGlobalSearch(t *testing.T) {
manifestBlob, err := json.Marshal(ispec.Manifest{}) manifestBlob, err := json.Marshal(ispec.Manifest{})
So(err, ShouldBeNil) So(err, ShouldBeNil)
manifestMetas := map[string]mTypes.ManifestMetadata{ manifestsMeta := map[string]mTypes.ManifestMetadata{
"digestTag1.0.1": { "digestTag1.0.1": {
ManifestBlob: manifestBlob, ManifestBlob: manifestBlob,
ConfigBlob: configBlob1, ConfigBlob: configBlob1,
@ -134,7 +134,7 @@ func TestGlobalSearch(t *testing.T) {
}, },
} }
return repos, manifestMetas, map[string]mTypes.IndexData{}, nil return repos, manifestsMeta, map[string]mTypes.IndexData{}, nil
}, },
} }
@ -189,14 +189,14 @@ func TestGlobalSearch(t *testing.T) {
configBlob, err := json.Marshal(ispec.Image{}) configBlob, err := json.Marshal(ispec.Image{})
So(err, ShouldBeNil) So(err, ShouldBeNil)
manifestMetas := map[string]mTypes.ManifestMetadata{ manifestsMeta := map[string]mTypes.ManifestMetadata{
"digestTag1.0.1": { "digestTag1.0.1": {
ManifestBlob: []byte("bad manifest blob"), ManifestBlob: []byte("bad manifest blob"),
ConfigBlob: configBlob, ConfigBlob: configBlob,
}, },
} }
return repos, manifestMetas, map[string]mTypes.IndexData{}, nil return repos, manifestsMeta, map[string]mTypes.IndexData{}, nil
}, },
} }
@ -262,14 +262,14 @@ func TestGlobalSearch(t *testing.T) {
manifestBlob, err := json.Marshal(ispec.Manifest{}) manifestBlob, err := json.Marshal(ispec.Manifest{})
So(err, ShouldBeNil) So(err, ShouldBeNil)
manifestMetas := map[string]mTypes.ManifestMetadata{ manifestsMeta := map[string]mTypes.ManifestMetadata{
"digestTag1.0.1": { "digestTag1.0.1": {
ManifestBlob: manifestBlob, ManifestBlob: manifestBlob,
ConfigBlob: []byte("bad config blob"), ConfigBlob: []byte("bad config blob"),
}, },
} }
return repos, manifestMetas, map[string]mTypes.IndexData{}, nil return repos, manifestsMeta, map[string]mTypes.IndexData{}, nil
}, },
} }
@ -375,7 +375,7 @@ func TestGlobalSearch(t *testing.T) {
manifestBlob, err := json.Marshal(ispec.Manifest{}) manifestBlob, err := json.Marshal(ispec.Manifest{})
So(err, ShouldBeNil) So(err, ShouldBeNil)
manifestMetas := map[string]mTypes.ManifestMetadata{ manifestsMeta := map[string]mTypes.ManifestMetadata{
"digestTag1.0.1": { "digestTag1.0.1": {
ManifestBlob: manifestBlob, ManifestBlob: manifestBlob,
ConfigBlob: configBlob1, ConfigBlob: configBlob1,
@ -386,7 +386,7 @@ func TestGlobalSearch(t *testing.T) {
}, },
} }
return repos, manifestMetas, map[string]mTypes.IndexData{}, nil return repos, manifestsMeta, map[string]mTypes.IndexData{}, nil
}, },
} }
@ -505,7 +505,7 @@ func TestRepoListWithNewestImage(t *testing.T) {
}) })
So(err, ShouldBeNil) So(err, ShouldBeNil)
manifestMetas := map[string]mTypes.ManifestMetadata{ manifestsMeta := map[string]mTypes.ManifestMetadata{
"digestTag1.0.1": { "digestTag1.0.1": {
ManifestBlob: []byte("bad manifest blob"), ManifestBlob: []byte("bad manifest blob"),
ConfigBlob: configBlob1, ConfigBlob: configBlob1,
@ -516,7 +516,7 @@ func TestRepoListWithNewestImage(t *testing.T) {
}, },
} }
return repos, manifestMetas, map[string]mTypes.IndexData{}, nil return repos, manifestsMeta, map[string]mTypes.IndexData{}, nil
}, },
} }
@ -601,7 +601,7 @@ func TestRepoListWithNewestImage(t *testing.T) {
manifestBlob, err := json.Marshal(ispec.Manifest{}) manifestBlob, err := json.Marshal(ispec.Manifest{})
So(err, ShouldBeNil) So(err, ShouldBeNil)
manifestMetas := map[string]mTypes.ManifestMetadata{ manifestsMeta := map[string]mTypes.ManifestMetadata{
"digestTag1.0.1": { "digestTag1.0.1": {
ManifestBlob: manifestBlob, ManifestBlob: manifestBlob,
ConfigBlob: configBlob1, ConfigBlob: configBlob1,
@ -612,7 +612,7 @@ func TestRepoListWithNewestImage(t *testing.T) {
}, },
} }
return repos, manifestMetas, map[string]mTypes.IndexData{}, nil return repos, manifestsMeta, map[string]mTypes.IndexData{}, nil
}, },
} }
Convey("MetaDB missing requestedPage", func() { Convey("MetaDB missing requestedPage", func() {
@ -776,7 +776,7 @@ func TestImageListForDigest(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
manifestBlob := []byte("invalid") manifestBlob := []byte("invalid")
manifestMetaDatas := map[string]mTypes.ManifestMetadata{ manifestsMetaData := map[string]mTypes.ManifestMetadata{
"digestTag1.0.1": { "digestTag1.0.1": {
ManifestBlob: manifestBlob, ManifestBlob: manifestBlob,
ConfigBlob: configBlob, ConfigBlob: configBlob,
@ -784,7 +784,7 @@ func TestImageListForDigest(t *testing.T) {
}, },
} }
return repos, manifestMetaDatas, map[string]mTypes.IndexData{}, nil return repos, manifestsMetaData, map[string]mTypes.IndexData{}, nil
}, },
} }
@ -820,7 +820,7 @@ func TestImageListForDigest(t *testing.T) {
configBlob, err := json.Marshal(ispec.ImageConfig{}) configBlob, err := json.Marshal(ispec.ImageConfig{})
So(err, ShouldBeNil) So(err, ShouldBeNil)
manifestMetaDatas := map[string]mTypes.ManifestMetadata{ manifestsMetaData := map[string]mTypes.ManifestMetadata{
manifestDigest: { manifestDigest: {
ManifestBlob: manifestBlob, ManifestBlob: manifestBlob,
ConfigBlob: configBlob, ConfigBlob: configBlob,
@ -829,9 +829,9 @@ func TestImageListForDigest(t *testing.T) {
} }
matchedTags := repos[0].Tags matchedTags := repos[0].Tags
for tag, manifestDescriptor := range repos[0].Tags { for tag, manifestDescriptor := range repos[0].Tags {
if !filterFunc(repos[0], manifestMetaDatas[manifestDescriptor.Digest]) { if !filterFunc(repos[0], manifestsMetaData[manifestDescriptor.Digest]) {
delete(matchedTags, tag) delete(matchedTags, tag)
delete(manifestMetaDatas, manifestDescriptor.Digest) delete(manifestsMetaData, manifestDescriptor.Digest)
continue continue
} }
@ -839,7 +839,7 @@ func TestImageListForDigest(t *testing.T) {
repos[0].Tags = matchedTags repos[0].Tags = matchedTags
return repos, manifestMetaDatas, map[string]mTypes.IndexData{}, nil return repos, manifestsMetaData, map[string]mTypes.IndexData{}, nil
}, },
} }
@ -898,7 +898,7 @@ func TestImageListForDigest(t *testing.T) {
}) })
So(err, ShouldBeNil) So(err, ShouldBeNil)
manifestMetaDatas := map[string]mTypes.ManifestMetadata{ manifestsMetaData := map[string]mTypes.ManifestMetadata{
manifestDigest: { manifestDigest: {
ManifestBlob: manifestBlob, ManifestBlob: manifestBlob,
ConfigBlob: configBlob, ConfigBlob: configBlob,
@ -908,9 +908,9 @@ func TestImageListForDigest(t *testing.T) {
matchedTags := repos[0].Tags matchedTags := repos[0].Tags
for tag, manifestDescriptor := range repos[0].Tags { for tag, manifestDescriptor := range repos[0].Tags {
if !filterFunc(repos[0], manifestMetaDatas[manifestDescriptor.Digest]) { if !filterFunc(repos[0], manifestsMetaData[manifestDescriptor.Digest]) {
delete(matchedTags, tag) delete(matchedTags, tag)
delete(manifestMetaDatas, manifestDescriptor.Digest) delete(manifestsMetaData, manifestDescriptor.Digest)
continue continue
} }
@ -918,7 +918,7 @@ func TestImageListForDigest(t *testing.T) {
repos[0].Tags = matchedTags repos[0].Tags = matchedTags
return repos, manifestMetaDatas, map[string]mTypes.IndexData{}, nil return repos, manifestsMetaData, map[string]mTypes.IndexData{}, nil
}, },
} }
@ -975,7 +975,7 @@ func TestImageListForDigest(t *testing.T) {
}) })
So(err, ShouldBeNil) So(err, ShouldBeNil)
manifestMetaDatas := map[string]mTypes.ManifestMetadata{ manifestsMetaData := map[string]mTypes.ManifestMetadata{
manifestDigest: { manifestDigest: {
ManifestBlob: manifestBlob, ManifestBlob: manifestBlob,
ConfigBlob: configBlob, ConfigBlob: configBlob,
@ -985,9 +985,9 @@ func TestImageListForDigest(t *testing.T) {
matchedTags := repos[0].Tags matchedTags := repos[0].Tags
for tag, manifestDescriptor := range repos[0].Tags { for tag, manifestDescriptor := range repos[0].Tags {
if !filterFunc(repos[0], manifestMetaDatas[manifestDescriptor.Digest]) { if !filterFunc(repos[0], manifestsMetaData[manifestDescriptor.Digest]) {
delete(matchedTags, tag) delete(matchedTags, tag)
delete(manifestMetaDatas, manifestDescriptor.Digest) delete(manifestsMetaData, manifestDescriptor.Digest)
continue continue
} }
@ -995,7 +995,7 @@ func TestImageListForDigest(t *testing.T) {
repos[0].Tags = matchedTags repos[0].Tags = matchedTags
return repos, manifestMetaDatas, map[string]mTypes.IndexData{}, nil return repos, manifestsMetaData, map[string]mTypes.IndexData{}, nil
}, },
} }
@ -1042,7 +1042,7 @@ func TestImageListForDigest(t *testing.T) {
}, },
} }
manifestMetaDatas := map[string]mTypes.ManifestMetadata{ manifestsMetaData := map[string]mTypes.ManifestMetadata{
manifestDigest: { manifestDigest: {
ManifestBlob: manifestBlob, ManifestBlob: manifestBlob,
ConfigBlob: configBlob, ConfigBlob: configBlob,
@ -1054,9 +1054,9 @@ func TestImageListForDigest(t *testing.T) {
matchedTags := repo.Tags matchedTags := repo.Tags
for tag, manifestDescriptor := range repo.Tags { for tag, manifestDescriptor := range repo.Tags {
if !filterFunc(repo, manifestMetaDatas[manifestDescriptor.Digest]) { if !filterFunc(repo, manifestsMetaData[manifestDescriptor.Digest]) {
delete(matchedTags, tag) delete(matchedTags, tag)
delete(manifestMetaDatas, manifestDescriptor.Digest) delete(manifestsMetaData, manifestDescriptor.Digest)
continue continue
} }
@ -1065,7 +1065,7 @@ func TestImageListForDigest(t *testing.T) {
repos[i].Tags = matchedTags repos[i].Tags = matchedTags
} }
return repos, manifestMetaDatas, map[string]mTypes.IndexData{}, nil return repos, manifestsMetaData, map[string]mTypes.IndexData{}, nil
}, },
} }
@ -1111,7 +1111,7 @@ func TestImageListForDigest(t *testing.T) {
}, },
} }
manifestMetaDatas := map[string]mTypes.ManifestMetadata{ manifestsMetaData := map[string]mTypes.ManifestMetadata{
manifestDigest: { manifestDigest: {
ManifestBlob: manifestBlob, ManifestBlob: manifestBlob,
ConfigBlob: configBlob, ConfigBlob: configBlob,
@ -1123,9 +1123,9 @@ func TestImageListForDigest(t *testing.T) {
matchedTags := repo.Tags matchedTags := repo.Tags
for tag, manifestDescriptor := range repo.Tags { for tag, manifestDescriptor := range repo.Tags {
if !filterFunc(repo, manifestMetaDatas[manifestDescriptor.Digest]) { if !filterFunc(repo, manifestsMetaData[manifestDescriptor.Digest]) {
delete(matchedTags, tag) delete(matchedTags, tag)
delete(manifestMetaDatas, manifestDescriptor.Digest) delete(manifestsMetaData, manifestDescriptor.Digest)
continue continue
} }
@ -1136,7 +1136,7 @@ func TestImageListForDigest(t *testing.T) {
repos = append(repos, repo) repos = append(repos, repo)
} }
return repos, manifestMetaDatas, map[string]mTypes.IndexData{}, nil return repos, manifestsMetaData, map[string]mTypes.IndexData{}, nil
}, },
} }
@ -1429,7 +1429,7 @@ func TestImageList(t *testing.T) {
manifestBlob, err := json.Marshal(ispec.Manifest{}) manifestBlob, err := json.Marshal(ispec.Manifest{})
So(err, ShouldBeNil) So(err, ShouldBeNil)
manifestMetaDatas := map[string]mTypes.ManifestMetadata{ manifestsMetaData := map[string]mTypes.ManifestMetadata{
"digestTag1.0.1": { "digestTag1.0.1": {
ManifestBlob: manifestBlob, ManifestBlob: manifestBlob,
ConfigBlob: configBlob, ConfigBlob: configBlob,
@ -1442,21 +1442,21 @@ func TestImageList(t *testing.T) {
}, },
} }
if !filterFunc(repos[0], manifestMetaDatas["digestTag1.0.1"]) { if !filterFunc(repos[0], manifestsMetaData["digestTag1.0.1"]) {
return []mTypes.RepoMetadata{}, map[string]mTypes.ManifestMetadata{}, return []mTypes.RepoMetadata{}, map[string]mTypes.ManifestMetadata{},
map[string]mTypes.IndexData{}, nil map[string]mTypes.IndexData{}, nil
} }
return repos, manifestMetaDatas, map[string]mTypes.IndexData{}, nil return repos, manifestsMetaData, map[string]mTypes.IndexData{}, nil
}, },
} }
limit := 1 limit := 1
ofset := 0 offset := 0
sortCriteria := gql_generated.SortCriteriaAlphabeticAsc sortCriteria := gql_generated.SortCriteriaAlphabeticAsc
pageInput := gql_generated.PageInput{ pageInput := gql_generated.PageInput{
Limit: &limit, Limit: &limit,
Offset: &ofset, Offset: &offset,
SortBy: &sortCriteria, SortBy: &sortCriteria,
} }
@ -1889,7 +1889,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo
} }
// Create metadb data for scannable image with vulnerabilities // Create metadb data for scannable image with vulnerabilities
// Create manifets metadata first // Create manifest metadata first
timeStamp1 := time.Date(2008, 1, 1, 12, 0, 0, 0, time.UTC) timeStamp1 := time.Date(2008, 1, 1, 12, 0, 0, 0, time.UTC)
configBlob1, err := json.Marshal(ispec.Image{ configBlob1, err := json.Marshal(ispec.Image{
@ -2076,7 +2076,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo
}, },
"CVE2": { "CVE2": {
ID: "CVE2", ID: "CVE2",
Severity: "MEDIM", Severity: "MEDIUM",
Title: "Title CVE2", Title: "Title CVE2",
Description: "Description CVE2", Description: "Description CVE2",
}, },
@ -2961,7 +2961,7 @@ func TestDerivedImageList(t *testing.T) {
}) })
So(err, ShouldBeNil) So(err, ShouldBeNil)
manifestMetas := map[string]mTypes.ManifestMetadata{ manifestsMeta := map[string]mTypes.ManifestMetadata{
"digestTag1.0.1": { "digestTag1.0.1": {
ManifestBlob: manifestBlob, ManifestBlob: manifestBlob,
ConfigBlob: configBlob, ConfigBlob: configBlob,
@ -3019,9 +3019,9 @@ func TestDerivedImageList(t *testing.T) {
matchedTags := repo.Tags matchedTags := repo.Tags
for tag, descriptor := range repo.Tags { for tag, descriptor := range repo.Tags {
if !filterFunc(repo, manifestMetas[descriptor.Digest]) { if !filterFunc(repo, manifestsMeta[descriptor.Digest]) {
delete(matchedTags, tag) delete(matchedTags, tag)
delete(manifestMetas, descriptor.Digest) delete(manifestsMeta, descriptor.Digest)
continue continue
} }
@ -3030,7 +3030,7 @@ func TestDerivedImageList(t *testing.T) {
repos[i].Tags = matchedTags repos[i].Tags = matchedTags
} }
return repos, manifestMetas, map[string]mTypes.IndexData{}, nil return repos, manifestsMeta, map[string]mTypes.IndexData{}, nil
}, },
} }
@ -3249,7 +3249,7 @@ func TestBaseImageList(t *testing.T) {
}) })
So(err, ShouldBeNil) So(err, ShouldBeNil)
manifestMetas := map[string]mTypes.ManifestMetadata{ manifestsMeta := map[string]mTypes.ManifestMetadata{
"digestTag1.0.1": { "digestTag1.0.1": {
ManifestBlob: manifestBlob, ManifestBlob: manifestBlob,
ConfigBlob: configBlob, ConfigBlob: configBlob,
@ -3301,9 +3301,9 @@ func TestBaseImageList(t *testing.T) {
matchedTags := repo.Tags matchedTags := repo.Tags
for tag, descriptor := range repo.Tags { for tag, descriptor := range repo.Tags {
if !filterFunc(repo, manifestMetas[descriptor.Digest]) { if !filterFunc(repo, manifestsMeta[descriptor.Digest]) {
delete(matchedTags, tag) delete(matchedTags, tag)
delete(manifestMetas, descriptor.Digest) delete(manifestsMeta, descriptor.Digest)
continue continue
} }
@ -3312,7 +3312,7 @@ func TestBaseImageList(t *testing.T) {
repos[i].Tags = matchedTags repos[i].Tags = matchedTags
} }
return repos, manifestMetas, map[string]mTypes.IndexData{}, nil return repos, manifestsMeta, map[string]mTypes.IndexData{}, nil
}, },
} }
responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter,
@ -3416,7 +3416,7 @@ func TestBaseImageList(t *testing.T) {
}) })
So(err, ShouldBeNil) So(err, ShouldBeNil)
manifestMetas := map[string]mTypes.ManifestMetadata{ manifestsMeta := map[string]mTypes.ManifestMetadata{
"digestTag1.0.1": { "digestTag1.0.1": {
ManifestBlob: manifestBlob, ManifestBlob: manifestBlob,
ConfigBlob: configBlob, ConfigBlob: configBlob,
@ -3467,9 +3467,9 @@ func TestBaseImageList(t *testing.T) {
matchedTags := repo.Tags matchedTags := repo.Tags
for tag, descriptor := range repo.Tags { for tag, descriptor := range repo.Tags {
if !filterFunc(repo, manifestMetas[descriptor.Digest]) { if !filterFunc(repo, manifestsMeta[descriptor.Digest]) {
delete(matchedTags, tag) delete(matchedTags, tag)
delete(manifestMetas, descriptor.Digest) delete(manifestsMeta, descriptor.Digest)
continue continue
} }
@ -3478,7 +3478,7 @@ func TestBaseImageList(t *testing.T) {
repos[i].Tags = matchedTags repos[i].Tags = matchedTags
} }
return repos, manifestMetas, map[string]mTypes.IndexData{}, nil return repos, manifestsMeta, map[string]mTypes.IndexData{}, nil
}, },
} }
responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter,
@ -3519,12 +3519,12 @@ func TestExpandedRepoInfo(t *testing.T) {
Digest: "goodIndexBadManifests", Digest: "goodIndexBadManifests",
MediaType: ispec.MediaTypeImageIndex, MediaType: ispec.MediaTypeImageIndex,
}, },
"tagGoodIndex1GoodManfest": { "tagGoodIndex1GoodManifest": {
Digest: "goodIndexGoodManfest", Digest: "goodIndexGoodManifest",
MediaType: ispec.MediaTypeImageIndex, MediaType: ispec.MediaTypeImageIndex,
}, },
"tagGoodIndex2GoodManfest": { "tagGoodIndex2GoodManifest": {
Digest: "goodIndexGoodManfest", Digest: "goodIndexGoodManifest",
MediaType: ispec.MediaTypeImageIndex, MediaType: ispec.MediaTypeImageIndex,
}, },
}, },
@ -3569,7 +3569,7 @@ func TestExpandedRepoInfo(t *testing.T) {
return mTypes.IndexData{ return mTypes.IndexData{
IndexBlob: goodIndexBadManifestsBlob, IndexBlob: goodIndexBadManifestsBlob,
}, nil }, nil
case "goodIndexGoodManfest": case "goodIndexGoodManifest":
return mTypes.IndexData{ return mTypes.IndexData{
IndexBlob: goodIndexGoodManifestBlob, IndexBlob: goodIndexGoodManifestBlob,
}, nil }, nil

View file

@ -6242,8 +6242,8 @@ func TestImageSummary(t *testing.T) {
WaitTillServerReady(baseURL) WaitTillServerReady(baseURL)
manifestBlob, errMarsal := json.Marshal(manifest) manifestBlob, errMarshal := json.Marshal(manifest)
So(errMarsal, ShouldBeNil) So(errMarshal, ShouldBeNil)
So(manifestBlob, ShouldNotBeNil) So(manifestBlob, ShouldNotBeNil)
manifestDigest := godigest.FromBytes(manifestBlob) manifestDigest := godigest.FromBytes(manifestBlob)
repoName := "test-repo" //nolint:goconst repoName := "test-repo" //nolint:goconst
@ -6427,7 +6427,7 @@ func TestImageSummary(t *testing.T) {
}) })
} }
func TestUplodingArtifactsWithDifferentMediaType(t *testing.T) { func TestUploadingArtifactsWithDifferentMediaType(t *testing.T) {
Convey("", t, func() { Convey("", t, func() {
port := GetFreePort() port := GetFreePort()
baseURL := GetBaseURL(port) baseURL := GetBaseURL(port)

View file

@ -20,7 +20,7 @@ import (
"github.com/rs/zerolog" "github.com/rs/zerolog"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
"zotregistry.io/zot/errors" zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/extensions/config" "zotregistry.io/zot/pkg/extensions/config"
syncconf "zotregistry.io/zot/pkg/extensions/config/sync" syncconf "zotregistry.io/zot/pkg/extensions/config/sync"
"zotregistry.io/zot/pkg/extensions/lint" "zotregistry.io/zot/pkg/extensions/lint"
@ -92,7 +92,7 @@ func TestSyncInternal(t *testing.T) {
repositoryReference = fmt.Sprintf("%s/%s:tagged", host, testImage) repositoryReference = fmt.Sprintf("%s/%s:tagged", host, testImage)
_, err = parseRepositoryReference(repositoryReference) _, err = parseRepositoryReference(repositoryReference)
So(err, ShouldEqual, errors.ErrInvalidRepositoryName) So(err, ShouldEqual, zerr.ErrInvalidRepositoryName)
repositoryReference = fmt.Sprintf("http://%s/%s", host, testImage) repositoryReference = fmt.Sprintf("http://%s/%s", host, testImage)
_, err = parseRepositoryReference(repositoryReference) _, err = parseRepositoryReference(repositoryReference)
@ -341,7 +341,7 @@ func TestLocalRegistry(t *testing.T) {
registry := NewLocalRegistry(storage.StoreController{DefaultStore: syncImgStore}, mocks.MetaDBMock{ registry := NewLocalRegistry(storage.StoreController{DefaultStore: syncImgStore}, mocks.MetaDBMock{
SetRepoReferenceFn: func(repo, Reference string, manifestDigest godigest.Digest, mediaType string) error { SetRepoReferenceFn: func(repo, Reference string, manifestDigest godigest.Digest, mediaType string) error {
if Reference == "1.0" { if Reference == "1.0" {
return errors.ErrRepoMetaNotFound return zerr.ErrRepoMetaNotFound
} }
return nil return nil
@ -355,7 +355,7 @@ func TestLocalRegistry(t *testing.T) {
Convey("trigger metaDB error on image manifest in CommitImage()", func() { Convey("trigger metaDB error on image manifest in CommitImage()", func() {
registry := NewLocalRegistry(storage.StoreController{DefaultStore: syncImgStore}, mocks.MetaDBMock{ registry := NewLocalRegistry(storage.StoreController{DefaultStore: syncImgStore}, mocks.MetaDBMock{
SetRepoReferenceFn: func(repo, Reference string, manifestDigest godigest.Digest, mediaType string) error { SetRepoReferenceFn: func(repo, Reference string, manifestDigest godigest.Digest, mediaType string) error {
return errors.ErrRepoMetaNotFound return zerr.ErrRepoMetaNotFound
}, },
}, log) }, log)

View file

@ -1156,7 +1156,7 @@ func (bdw *BoltDB) FilterTags(ctx context.Context, filterFunc mTypes.FilterFunc,
repoMeta.IsStarred = zcommon.Contains(userStars, repoMeta.Name) repoMeta.IsStarred = zcommon.Contains(userStars, repoMeta.Name)
matchedTags := make(map[string]mTypes.Descriptor) matchedTags := make(map[string]mTypes.Descriptor)
// take all manifestMetas // take all manifestsMeta
for tag, descriptor := range repoMeta.Tags { for tag, descriptor := range repoMeta.Tags {
switch descriptor.MediaType { switch descriptor.MediaType {
case ispec.MediaTypeImageManifest: case ispec.MediaTypeImageManifest:

View file

@ -5,7 +5,7 @@ import (
glob "github.com/bmatcuk/doublestar/v4" //nolint:gci glob "github.com/bmatcuk/doublestar/v4" //nolint:gci
"zotregistry.io/zot/errors" zerr "zotregistry.io/zot/errors"
) )
type Key int type Key int
@ -41,7 +41,7 @@ func GetAccessControlContext(ctx context.Context) (*AccessControlContext, error)
if authCtx := ctx.Value(authzCtxKey); authCtx != nil { if authCtx := ctx.Value(authzCtxKey); authCtx != nil {
acCtx, ok := authCtx.(AccessControlContext) acCtx, ok := authCtx.(AccessControlContext)
if !ok { if !ok {
return nil, errors.ErrBadType return nil, zerr.ErrBadType
} }
return &acCtx, nil return &acCtx, nil
@ -110,7 +110,7 @@ func GetAuthnMiddlewareContext(ctx context.Context) (*AuthnMiddlewareContext, er
if authnMiddlewareCtx := ctx.Value(authnMiddlewareCtxKey); authnMiddlewareCtx != nil { if authnMiddlewareCtx := ctx.Value(authnMiddlewareCtxKey); authnMiddlewareCtx != nil {
amCtx, ok := authnMiddlewareCtx.(AuthnMiddlewareContext) amCtx, ok := authnMiddlewareCtx.(AuthnMiddlewareContext)
if !ok { if !ok {
return nil, errors.ErrBadType return nil, zerr.ErrBadType
} }
return &amCtx, nil return &amCtx, nil

View file

@ -54,7 +54,7 @@ import (
"zotregistry.io/zot/pkg/storage" "zotregistry.io/zot/pkg/storage"
storageCommon "zotregistry.io/zot/pkg/storage/common" storageCommon "zotregistry.io/zot/pkg/storage/common"
"zotregistry.io/zot/pkg/storage/local" "zotregistry.io/zot/pkg/storage/local"
"zotregistry.io/zot/pkg/storage/types" stypes "zotregistry.io/zot/pkg/storage/types"
"zotregistry.io/zot/pkg/test/inject" "zotregistry.io/zot/pkg/test/inject"
"zotregistry.io/zot/pkg/test/mocks" "zotregistry.io/zot/pkg/test/mocks"
) )
@ -1437,12 +1437,12 @@ func ListNotarySignatures(reference string, tdir string) ([]godigest.Digest, err
sigRepo := notreg.NewRepository(remoteRepo) sigRepo := notreg.NewRepository(remoteRepo)
artifectDesc, err := sigRepo.Resolve(ctx, reference) artifactDesc, err := sigRepo.Resolve(ctx, reference)
if err != nil { if err != nil {
return signatures, err return signatures, err
} }
err = sigRepo.ListSignatures(ctx, artifectDesc, func(signatureManifests []ispec.Descriptor) error { err = sigRepo.ListSignatures(ctx, artifactDesc, func(signatureManifests []ispec.Descriptor) error {
for _, sigManifestDesc := range signatureManifests { for _, sigManifestDesc := range signatureManifests {
signatures = append(signatures, sigManifestDesc.Digest) signatures = append(signatures, sigManifestDesc.Digest)
} }
@ -2067,11 +2067,11 @@ func GetDefaultLayersBlobs() [][]byte {
} }
} }
func GetDefaultImageStore(rootDir string, log zLog.Logger) types.ImageStore { func GetDefaultImageStore(rootDir string, log zLog.Logger) stypes.ImageStore {
return local.NewImageStore(rootDir, false, time.Hour, false, false, log, return local.NewImageStore(rootDir, false, time.Hour, false, false, log,
monitoring.NewMetricsServer(false, log), monitoring.NewMetricsServer(false, log),
mocks.MockedLint{ mocks.MockedLint{
LintFn: func(repo string, manifestDigest godigest.Digest, imageStore types.ImageStore) (bool, error) { LintFn: func(repo string, manifestDigest godigest.Digest, imageStore stypes.ImageStore) (bool, error) {
return true, nil return true, nil
}, },
}, },
@ -2084,3 +2084,20 @@ func GetDefaultStoreController(rootDir string, log zLog.Logger) storage.StoreCon
DefaultStore: GetDefaultImageStore(rootDir, log), DefaultStore: GetDefaultImageStore(rootDir, log),
} }
} }
func RemoveLocalStorageContents(imageStore stypes.ImageStore) error {
repos, err := imageStore.GetRepositories()
if err != nil {
return err
}
for _, repo := range repos {
// take just the first path
err = os.RemoveAll(filepath.Join(imageStore.RootDir(), filepath.SplitList(repo)[0]))
if err != nil {
return err
}
}
return nil
}

View file

@ -56,7 +56,7 @@ type ConfigBuilder interface {
RandomConfig() ManifestBuilder RandomConfig() ManifestBuilder
} }
// VulnerableConfigBuilder abstracts specifying the config of an vulnerage OCI image. // VulnerableConfigBuilder abstracts specifying the config of an vulnerable OCI image.
// Keeping the RootFS field consistent with the vulnerable layers. // Keeping the RootFS field consistent with the vulnerable layers.
type VulnerableConfigBuilder interface { type VulnerableConfigBuilder interface {
// VulnerableConfig sets the given config while keeping the correct RootFS values for the // VulnerableConfig sets the given config while keeping the correct RootFS values for the