mirror of
https://github.com/project-zot/zot.git
synced 2024-12-30 22:34:13 -05:00
feat(cli): add referrers and search commands to cli (#1497)
* feat(cli): add referrers command to cli Signed-off-by: Laurentiu Niculae <niculae.laurentiu1@gmail.com> * feat(cli): add global search command Signed-off-by: Laurentiu Niculae <niculae.laurentiu1@gmail.com> * feat(cli): fix comments Signed-off-by: Laurentiu Niculae <niculae.laurentiu1@gmail.com> --------- Signed-off-by: Laurentiu Niculae <niculae.laurentiu1@gmail.com>
This commit is contained in:
parent
ea7dbf9e5c
commit
620287c7a4
16 changed files with 1993 additions and 81 deletions
|
@ -32,6 +32,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")
|
||||||
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")
|
||||||
|
|
|
@ -5226,7 +5226,7 @@ func TestManifestImageIndex(t *testing.T) {
|
||||||
Layers: layers,
|
Layers: layers,
|
||||||
Manifest: manifest,
|
Manifest: manifest,
|
||||||
}, baseURL, repoName)
|
}, baseURL, repoName)
|
||||||
So(err, ShouldNotBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
content, err = json.Marshal(manifest)
|
content, err = json.Marshal(manifest)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
@ -5251,7 +5251,7 @@ func TestManifestImageIndex(t *testing.T) {
|
||||||
Layers: layers,
|
Layers: layers,
|
||||||
Manifest: manifest,
|
Manifest: manifest,
|
||||||
}, baseURL, repoName)
|
}, baseURL, repoName)
|
||||||
So(err, ShouldNotBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
content, err = json.Marshal(manifest)
|
content, err = json.Marshal(manifest)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
@ -5307,7 +5307,7 @@ func TestManifestImageIndex(t *testing.T) {
|
||||||
Layers: layers,
|
Layers: layers,
|
||||||
Manifest: manifest,
|
Manifest: manifest,
|
||||||
}, baseURL, repoName)
|
}, baseURL, repoName)
|
||||||
So(err, ShouldNotBeNil)
|
So(err, ShouldBeNil)
|
||||||
content, err = json.Marshal(manifest)
|
content, err = json.Marshal(manifest)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
digest = godigest.FromBytes(content)
|
digest = godigest.FromBytes(content)
|
||||||
|
@ -5480,7 +5480,7 @@ func TestManifestImageIndex(t *testing.T) {
|
||||||
Layers: layers,
|
Layers: layers,
|
||||||
Manifest: manifest,
|
Manifest: manifest,
|
||||||
}, baseURL, repoName)
|
}, baseURL, repoName)
|
||||||
So(err, ShouldNotBeNil)
|
So(err, ShouldBeNil)
|
||||||
content, err = json.Marshal(manifest)
|
content, err = json.Marshal(manifest)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
digest = godigest.FromBytes(content)
|
digest = godigest.FromBytes(content)
|
||||||
|
|
|
@ -10,4 +10,5 @@ func enableCli(rootCmd *cobra.Command) {
|
||||||
rootCmd.AddCommand(NewImageCommand(NewSearchService()))
|
rootCmd.AddCommand(NewImageCommand(NewSearchService()))
|
||||||
rootCmd.AddCommand(NewCveCommand(NewSearchService()))
|
rootCmd.AddCommand(NewCveCommand(NewSearchService()))
|
||||||
rootCmd.AddCommand(NewRepoCommand(NewSearchService()))
|
rootCmd.AddCommand(NewRepoCommand(NewSearchService()))
|
||||||
|
rootCmd.AddCommand(NewSearchCommand(NewSearchService()))
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
zotErrors "zotregistry.io/zot/errors"
|
zotErrors "zotregistry.io/zot/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//nolint:dupl
|
||||||
func NewImageCommand(searchService SearchService) *cobra.Command {
|
func NewImageCommand(searchService SearchService) *cobra.Command {
|
||||||
searchImageParams := make(map[string]*string)
|
searchImageParams := make(map[string]*string)
|
||||||
|
|
||||||
|
@ -70,7 +71,7 @@ func NewImageCommand(searchService SearchService) *cobra.Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
spin := spinner.New(spinner.CharSets[39], spinnerDuration, spinner.WithWriter(cmd.ErrOrStderr()))
|
spin := spinner.New(spinner.CharSets[39], spinnerDuration, spinner.WithWriter(cmd.ErrOrStderr()))
|
||||||
spin.Prefix = "Searching... "
|
spin.Prefix = prefix
|
||||||
|
|
||||||
searchConfig := searchConfig{
|
searchConfig := searchConfig{
|
||||||
params: searchImageParams,
|
params: searchImageParams,
|
||||||
|
|
|
@ -1710,6 +1710,53 @@ func (service mockService) getRepos(ctx context.Context, config searchConfig, us
|
||||||
channel <- stringResult{"", nil}
|
channel <- stringResult{"", nil}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (service mockService) getReferrers(ctx context.Context, config searchConfig, username, password string,
|
||||||
|
repo, digest string,
|
||||||
|
) (referrersResult, error) {
|
||||||
|
return referrersResult{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service mockService) globalSearchGQL(ctx context.Context, config searchConfig, username, password string,
|
||||||
|
query string,
|
||||||
|
) (*common.GlobalSearch, error) {
|
||||||
|
return &common.GlobalSearch{
|
||||||
|
Images: []common.ImageSummary{
|
||||||
|
{
|
||||||
|
RepoName: "repo",
|
||||||
|
MediaType: ispec.MediaTypeImageManifest,
|
||||||
|
Manifests: []common.ManifestSummary{
|
||||||
|
{
|
||||||
|
Digest: godigest.FromString("str").String(),
|
||||||
|
Size: "100",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Repos: []common.RepoSummary{
|
||||||
|
{
|
||||||
|
Name: "repo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service mockService) getReferrersGQL(ctx context.Context, config searchConfig, username, password string,
|
||||||
|
repo, digest string,
|
||||||
|
) (*common.ReferrersResp, error) {
|
||||||
|
return &common.ReferrersResp{
|
||||||
|
ReferrersResult: common.ReferrersResult{
|
||||||
|
Referrers: []common.Referrer{
|
||||||
|
{
|
||||||
|
MediaType: "MediaType",
|
||||||
|
ArtifactType: "ArtifactType",
|
||||||
|
Size: 100,
|
||||||
|
Digest: "Digest",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
|
|
@ -13,6 +13,8 @@ import (
|
||||||
zotErrors "zotregistry.io/zot/errors"
|
zotErrors "zotregistry.io/zot/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const prefix = "Searching... "
|
||||||
|
|
||||||
func NewRepoCommand(searchService SearchService) *cobra.Command {
|
func NewRepoCommand(searchService SearchService) *cobra.Command {
|
||||||
var servURL, user, outputFormat string
|
var servURL, user, outputFormat string
|
||||||
|
|
||||||
|
@ -66,7 +68,7 @@ func NewRepoCommand(searchService SearchService) *cobra.Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
spin := spinner.New(spinner.CharSets[39], spinnerDuration, spinner.WithWriter(cmd.ErrOrStderr()))
|
spin := spinner.New(spinner.CharSets[39], spinnerDuration, spinner.WithWriter(cmd.ErrOrStderr()))
|
||||||
spin.Prefix = "Searching... "
|
spin.Prefix = prefix
|
||||||
|
|
||||||
searchConfig := searchConfig{
|
searchConfig := searchConfig{
|
||||||
searchService: searchService,
|
searchService: searchService,
|
||||||
|
|
148
pkg/cli/search_cmd.go
Normal file
148
pkg/cli/search_cmd.go
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
//go:build search
|
||||||
|
// +build search
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/briandowns/spinner"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
zotErrors "zotregistry.io/zot/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
//nolint:dupl
|
||||||
|
func NewSearchCommand(searchService SearchService) *cobra.Command {
|
||||||
|
searchImageParams := make(map[string]*string)
|
||||||
|
|
||||||
|
var servURL, user, outputFormat string
|
||||||
|
|
||||||
|
var isSpinner, verifyTLS, verbose, debug bool
|
||||||
|
|
||||||
|
imageCmd := &cobra.Command{
|
||||||
|
Use: "search [config-name]",
|
||||||
|
Short: "Search images and their tags",
|
||||||
|
Long: `Search repos or images
|
||||||
|
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.
|
||||||
|
`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := path.Join(home + "/.zot")
|
||||||
|
if servURL == "" {
|
||||||
|
if len(args) > 0 {
|
||||||
|
urlFromConfig, err := getConfigValue(configPath, args[0], "url")
|
||||||
|
if err != nil {
|
||||||
|
cmd.SilenceUsage = true
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if urlFromConfig == "" {
|
||||||
|
return zotErrors.ErrNoURLProvided
|
||||||
|
}
|
||||||
|
|
||||||
|
servURL = urlFromConfig
|
||||||
|
} else {
|
||||||
|
return zotErrors.ErrNoURLProvided
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) > 0 {
|
||||||
|
var err error
|
||||||
|
isSpinner, err = parseBooleanConfig(configPath, args[0], showspinnerConfig)
|
||||||
|
if err != nil {
|
||||||
|
cmd.SilenceUsage = true
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyTLS, err = parseBooleanConfig(configPath, args[0], verifyTLSConfig)
|
||||||
|
if err != nil {
|
||||||
|
cmd.SilenceUsage = true
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spin := spinner.New(spinner.CharSets[39], spinnerDuration, spinner.WithWriter(cmd.ErrOrStderr()))
|
||||||
|
spin.Prefix = prefix
|
||||||
|
|
||||||
|
searchConfig := searchConfig{
|
||||||
|
params: searchImageParams,
|
||||||
|
searchService: searchService,
|
||||||
|
servURL: &servURL,
|
||||||
|
user: &user,
|
||||||
|
outputFormat: &outputFormat,
|
||||||
|
verbose: &verbose,
|
||||||
|
debug: &debug,
|
||||||
|
spinner: spinnerState{spin, isSpinner},
|
||||||
|
verifyTLS: &verifyTLS,
|
||||||
|
resultWriter: cmd.OutOrStdout(),
|
||||||
|
}
|
||||||
|
|
||||||
|
err = globalSearch(searchConfig)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
cmd.SilenceUsage = true
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
setupSearchFlags(imageCmd, searchImageParams, &servURL, &user, &outputFormat, &verbose, &debug)
|
||||||
|
imageCmd.SetUsageTemplate(imageCmd.UsageTemplate() + usageFooter)
|
||||||
|
|
||||||
|
return imageCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupSearchFlags(imageCmd *cobra.Command, searchImageParams map[string]*string,
|
||||||
|
servURL, user, outputFormat *string, verbose *bool, debug *bool,
|
||||||
|
) {
|
||||||
|
searchImageParams["query"] = imageCmd.Flags().StringP("query", "q", "",
|
||||||
|
"Specify what repo or image(repo:tag) to be searched")
|
||||||
|
|
||||||
|
searchImageParams["subject"] = imageCmd.Flags().StringP("subject", "s", "", "List all referrers for this subject")
|
||||||
|
|
||||||
|
imageCmd.Flags().StringVar(servURL, "url", "", "Specify zot server URL if config-name is not mentioned")
|
||||||
|
imageCmd.Flags().StringVarP(user, "user", "u", "", `User Credentials of zot server in "username:password" format`)
|
||||||
|
imageCmd.Flags().StringVarP(outputFormat, "output", "o", "", "Specify output format [text/json/yaml]")
|
||||||
|
imageCmd.Flags().BoolVar(verbose, "verbose", false, "Show verbose output")
|
||||||
|
imageCmd.Flags().BoolVar(debug, "debug", false, "Show debug output")
|
||||||
|
}
|
||||||
|
|
||||||
|
func globalSearch(searchConfig searchConfig) error {
|
||||||
|
var searchers []searcher
|
||||||
|
|
||||||
|
if checkExtEndPoint(searchConfig) {
|
||||||
|
searchers = getGlobalSearchersGQL()
|
||||||
|
} else {
|
||||||
|
searchers = getGlobalSearchersREST()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, searcher := range searchers {
|
||||||
|
found, err := searcher.search(searchConfig)
|
||||||
|
if found {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return zotErrors.ErrInvalidFlagsCombination
|
||||||
|
}
|
619
pkg/cli/search_cmd_referrers_test.go
Normal file
619
pkg/cli/search_cmd_referrers_test.go
Normal file
|
@ -0,0 +1,619 @@
|
||||||
|
//go:build search
|
||||||
|
// +build search
|
||||||
|
|
||||||
|
package cli //nolint:testpackage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
. "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 ref[T any](input T) *T {
|
||||||
|
obj := input
|
||||||
|
|
||||||
|
return &obj
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
customArtTypeV1 = "custom.art.type.v1"
|
||||||
|
customArtTypeV2 = "custom.art.type.v2"
|
||||||
|
repoName = "repo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReferrersSearchers(t *testing.T) {
|
||||||
|
refSearcherGQL := referrerSearcherGQL{}
|
||||||
|
refSearcher := referrerSearcher{}
|
||||||
|
|
||||||
|
Convey("GQL Searcher", t, func() {
|
||||||
|
Convey("Bad parameters", func() {
|
||||||
|
ok, err := refSearcherGQL.search(searchConfig{params: map[string]*string{
|
||||||
|
"badParam": ref("badParam"),
|
||||||
|
}})
|
||||||
|
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(ok, ShouldBeFalse)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("GetRepoRefference fails", func() {
|
||||||
|
conf := searchConfig{
|
||||||
|
params: map[string]*string{
|
||||||
|
"subject": ref("bad-subject"),
|
||||||
|
},
|
||||||
|
user: ref("test:pass"),
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err := refSearcherGQL.search(conf)
|
||||||
|
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
So(ok, ShouldBeTrue)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("fetchImageDigest for tags fails", func() {
|
||||||
|
conf := searchConfig{
|
||||||
|
params: map[string]*string{
|
||||||
|
"subject": ref("repo:tag"),
|
||||||
|
},
|
||||||
|
user: ref("test:pass"),
|
||||||
|
servURL: ref("127.0.0.1:8080"),
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err := refSearcherGQL.search(conf)
|
||||||
|
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
So(ok, ShouldBeTrue)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("search service fails", func() {
|
||||||
|
port := test.GetFreePort()
|
||||||
|
|
||||||
|
conf := searchConfig{
|
||||||
|
params: map[string]*string{
|
||||||
|
"subject": ref("repo:tag"),
|
||||||
|
},
|
||||||
|
searchService: NewSearchService(),
|
||||||
|
user: ref("test:pass"),
|
||||||
|
servURL: ref("http://127.0.0.1:" + port),
|
||||||
|
verifyTLS: ref(false),
|
||||||
|
debug: ref(false),
|
||||||
|
verbose: ref(false),
|
||||||
|
}
|
||||||
|
|
||||||
|
server := test.StartTestHTTPServer(test.HTTPRoutes{
|
||||||
|
test.RouteHandler{
|
||||||
|
Route: "/v2/{repo}/manifests/{ref}",
|
||||||
|
HandlerFunc: func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
},
|
||||||
|
AllowedMethods: []string{"HEAD"},
|
||||||
|
},
|
||||||
|
}, port)
|
||||||
|
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
ok, err := refSearcherGQL.search(conf)
|
||||||
|
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
So(ok, ShouldBeTrue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("REST searcher", t, func() {
|
||||||
|
Convey("Bad parameters", func() {
|
||||||
|
ok, err := refSearcher.search(searchConfig{params: map[string]*string{
|
||||||
|
"badParam": ref("badParam"),
|
||||||
|
}})
|
||||||
|
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(ok, ShouldBeFalse)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("GetRepoRefference fails", func() {
|
||||||
|
conf := searchConfig{
|
||||||
|
params: map[string]*string{
|
||||||
|
"subject": ref("bad-subject"),
|
||||||
|
},
|
||||||
|
user: ref("test:pass"),
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err := refSearcher.search(conf)
|
||||||
|
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
So(ok, ShouldBeTrue)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("fetchImageDigest for tags fails", func() {
|
||||||
|
conf := searchConfig{
|
||||||
|
params: map[string]*string{
|
||||||
|
"subject": ref("repo:tag"),
|
||||||
|
},
|
||||||
|
user: ref("test:pass"),
|
||||||
|
servURL: ref("127.0.0.1:1000"),
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err := refSearcher.search(conf)
|
||||||
|
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
So(ok, ShouldBeTrue)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("search service fails", func() {
|
||||||
|
port := test.GetFreePort()
|
||||||
|
|
||||||
|
conf := searchConfig{
|
||||||
|
params: map[string]*string{
|
||||||
|
"subject": ref("repo:tag"),
|
||||||
|
},
|
||||||
|
searchService: NewSearchService(),
|
||||||
|
user: ref("test:pass"),
|
||||||
|
servURL: ref("http://127.0.0.1:" + port),
|
||||||
|
verifyTLS: ref(false),
|
||||||
|
debug: ref(false),
|
||||||
|
verbose: ref(false),
|
||||||
|
fixedFlag: ref(false),
|
||||||
|
}
|
||||||
|
|
||||||
|
server := test.StartTestHTTPServer(test.HTTPRoutes{
|
||||||
|
test.RouteHandler{
|
||||||
|
Route: "/v2/{repo}/manifests/{ref}",
|
||||||
|
HandlerFunc: func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
},
|
||||||
|
AllowedMethods: []string{"HEAD"},
|
||||||
|
},
|
||||||
|
}, port)
|
||||||
|
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
ok, err := refSearcher.search(conf)
|
||||||
|
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
So(ok, ShouldBeTrue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReferrerCLI(t *testing.T) {
|
||||||
|
Convey("Test GQL", t, func() {
|
||||||
|
rootDir := t.TempDir()
|
||||||
|
|
||||||
|
port := test.GetFreePort()
|
||||||
|
baseURL := test.GetBaseURL(port)
|
||||||
|
conf := config.New()
|
||||||
|
conf.HTTP.Port = port
|
||||||
|
conf.Storage.GC = false
|
||||||
|
defaultVal := true
|
||||||
|
conf.Extensions = &extconf.ExtensionConfig{
|
||||||
|
Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}},
|
||||||
|
}
|
||||||
|
ctlr := api.NewController(conf)
|
||||||
|
ctlr.Config.Storage.RootDirectory = rootDir
|
||||||
|
cm := test.NewControllerManager(ctlr)
|
||||||
|
cm.StartAndWait(conf.HTTP.Port)
|
||||||
|
defer cm.StopServer()
|
||||||
|
|
||||||
|
repo := repoName
|
||||||
|
image, err := test.GetRandomImage("tag")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
imgDigest, err := image.Digest()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
err = test.UploadImage(image, baseURL, repo)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
// add referrers
|
||||||
|
ref1, err := test.GetImageWithSubject(imgDigest, ispec.MediaTypeImageManifest)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
ref1.Reference = ""
|
||||||
|
|
||||||
|
ref1Digest, err := ref1.Digest()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
ref2, err := test.GetImageWithSubject(imgDigest, ispec.MediaTypeImageManifest)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
ref2.Reference = ""
|
||||||
|
ref2.Manifest.Config.MediaType = customArtTypeV1
|
||||||
|
ref2Digest, err := ref2.Digest()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
ref3, err := test.GetImageWithSubject(imgDigest, ispec.MediaTypeImageManifest)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
ref3.Manifest.ArtifactType = customArtTypeV2
|
||||||
|
ref3.Manifest.Config = ispec.DescriptorEmptyJSON
|
||||||
|
ref3.Reference = ""
|
||||||
|
ref3Digest, err := ref3.Digest()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
err = test.UploadImage(ref1, baseURL, repo)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
err = test.UploadImage(ref2, baseURL, repo)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
err = test.UploadImage(ref3, baseURL, repo)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
args := []string{"reftest", "--subject", repo + "@" + imgDigest.String()}
|
||||||
|
|
||||||
|
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`,
|
||||||
|
baseURL))
|
||||||
|
defer os.Remove(configPath)
|
||||||
|
|
||||||
|
cmd := NewSearchCommand(new(searchService))
|
||||||
|
|
||||||
|
buff := &bytes.Buffer{}
|
||||||
|
cmd.SetOut(buff)
|
||||||
|
cmd.SetErr(buff)
|
||||||
|
cmd.SetArgs(args)
|
||||||
|
err = cmd.Execute()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
space := regexp.MustCompile(`\s+`)
|
||||||
|
str := strings.TrimSpace(space.ReplaceAllString(buff.String(), " "))
|
||||||
|
So(str, ShouldContainSubstring, "ARTIFACT TYPE SIZE DIGEST")
|
||||||
|
So(str, ShouldContainSubstring, "application/vnd.oci.image.config.v1+json 557 B "+ref1Digest.String())
|
||||||
|
So(str, ShouldContainSubstring, "custom.art.type.v1 535 B "+ref2Digest.String())
|
||||||
|
So(str, ShouldContainSubstring, "custom.art.type.v2 598 B "+ref3Digest.String())
|
||||||
|
|
||||||
|
fmt.Println(buff.String())
|
||||||
|
|
||||||
|
os.Remove(configPath)
|
||||||
|
|
||||||
|
args = []string{"reftest", "--subject", repo + ":" + "tag"}
|
||||||
|
|
||||||
|
configPath = makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`,
|
||||||
|
baseURL))
|
||||||
|
defer os.Remove(configPath)
|
||||||
|
|
||||||
|
cmd = NewSearchCommand(new(searchService))
|
||||||
|
|
||||||
|
buff = &bytes.Buffer{}
|
||||||
|
cmd.SetOut(buff)
|
||||||
|
cmd.SetErr(buff)
|
||||||
|
cmd.SetArgs(args)
|
||||||
|
err = cmd.Execute()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
str = strings.TrimSpace(space.ReplaceAllString(buff.String(), " "))
|
||||||
|
So(str, ShouldContainSubstring, "ARTIFACT TYPE SIZE DIGEST")
|
||||||
|
So(str, ShouldContainSubstring, "application/vnd.oci.image.config.v1+json 557 B "+ref1Digest.String())
|
||||||
|
So(str, ShouldContainSubstring, "custom.art.type.v1 535 B "+ref2Digest.String())
|
||||||
|
So(str, ShouldContainSubstring, "custom.art.type.v2 598 B "+ref3Digest.String())
|
||||||
|
|
||||||
|
fmt.Println(buff.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Test REST", t, func() {
|
||||||
|
rootDir := t.TempDir()
|
||||||
|
|
||||||
|
port := test.GetFreePort()
|
||||||
|
baseURL := test.GetBaseURL(port)
|
||||||
|
conf := config.New()
|
||||||
|
conf.HTTP.Port = port
|
||||||
|
conf.Storage.GC = false
|
||||||
|
defaultVal := false
|
||||||
|
conf.Extensions = &extconf.ExtensionConfig{
|
||||||
|
Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}},
|
||||||
|
}
|
||||||
|
ctlr := api.NewController(conf)
|
||||||
|
ctlr.Config.Storage.RootDirectory = rootDir
|
||||||
|
cm := test.NewControllerManager(ctlr)
|
||||||
|
cm.StartAndWait(conf.HTTP.Port)
|
||||||
|
defer cm.StopServer()
|
||||||
|
|
||||||
|
repo := repoName
|
||||||
|
image, err := test.GetRandomImage("tag")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
imgDigest, err := image.Digest()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
err = test.UploadImage(image, baseURL, repo)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
// add referrers
|
||||||
|
ref1, err := test.GetImageWithSubject(imgDigest, ispec.MediaTypeImageManifest)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
ref1Digest, err := ref1.Digest()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
ref2, err := test.GetImageWithSubject(imgDigest, ispec.MediaTypeImageManifest)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
ref2.Manifest.Config.MediaType = customArtTypeV1
|
||||||
|
ref2Digest, err := ref2.Digest()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
ref3, err := test.GetImageWithSubject(imgDigest, ispec.MediaTypeImageManifest)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
ref3.Manifest.ArtifactType = customArtTypeV2
|
||||||
|
ref3.Manifest.Config = ispec.DescriptorEmptyJSON
|
||||||
|
|
||||||
|
ref3Digest, err := ref3.Digest()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
ref1.Reference = ""
|
||||||
|
err = test.UploadImage(ref1, baseURL, repo)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
ref2.Reference = ""
|
||||||
|
err = test.UploadImage(ref2, baseURL, repo)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
ref3.Reference = ""
|
||||||
|
err = test.UploadImage(ref3, baseURL, repo)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
// get referrers by digest
|
||||||
|
args := []string{"reftest", "--subject", repo + "@" + imgDigest.String()}
|
||||||
|
|
||||||
|
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`,
|
||||||
|
baseURL))
|
||||||
|
|
||||||
|
cmd := NewSearchCommand(new(searchService))
|
||||||
|
|
||||||
|
buff := &bytes.Buffer{}
|
||||||
|
cmd.SetOut(buff)
|
||||||
|
cmd.SetErr(buff)
|
||||||
|
cmd.SetArgs(args)
|
||||||
|
err = cmd.Execute()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
space := regexp.MustCompile(`\s+`)
|
||||||
|
str := strings.TrimSpace(space.ReplaceAllString(buff.String(), " "))
|
||||||
|
So(str, ShouldContainSubstring, "ARTIFACT TYPE SIZE DIGEST")
|
||||||
|
So(str, ShouldContainSubstring, "application/vnd.oci.image.config.v1+json 557 B "+ref1Digest.String())
|
||||||
|
So(str, ShouldContainSubstring, "custom.art.type.v1 535 B "+ref2Digest.String())
|
||||||
|
So(str, ShouldContainSubstring, "custom.art.type.v2 598 B "+ref3Digest.String())
|
||||||
|
fmt.Println(buff.String())
|
||||||
|
|
||||||
|
os.Remove(configPath)
|
||||||
|
|
||||||
|
args = []string{"reftest", "--subject", repo + ":" + "tag"}
|
||||||
|
|
||||||
|
configPath = makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`,
|
||||||
|
baseURL))
|
||||||
|
defer os.Remove(configPath)
|
||||||
|
|
||||||
|
buff = &bytes.Buffer{}
|
||||||
|
cmd.SetOut(buff)
|
||||||
|
cmd.SetErr(buff)
|
||||||
|
cmd.SetArgs(args)
|
||||||
|
err = cmd.Execute()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
str = strings.TrimSpace(space.ReplaceAllString(buff.String(), " "))
|
||||||
|
So(str, ShouldContainSubstring, "ARTIFACT TYPE SIZE DIGEST")
|
||||||
|
So(str, ShouldContainSubstring, "application/vnd.oci.image.config.v1+json 557 B "+ref1Digest.String())
|
||||||
|
So(str, ShouldContainSubstring, "custom.art.type.v1 535 B "+ref2Digest.String())
|
||||||
|
So(str, ShouldContainSubstring, "custom.art.type.v2 598 B "+ref3Digest.String())
|
||||||
|
fmt.Println(buff.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatsReferrersCLI(t *testing.T) {
|
||||||
|
Convey("Create server", t, func() {
|
||||||
|
rootDir := t.TempDir()
|
||||||
|
|
||||||
|
port := test.GetFreePort()
|
||||||
|
baseURL := test.GetBaseURL(port)
|
||||||
|
conf := config.New()
|
||||||
|
conf.HTTP.Port = port
|
||||||
|
conf.Storage.GC = false
|
||||||
|
defaultVal := false
|
||||||
|
conf.Extensions = &extconf.ExtensionConfig{
|
||||||
|
Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}},
|
||||||
|
}
|
||||||
|
ctlr := api.NewController(conf)
|
||||||
|
ctlr.Config.Storage.RootDirectory = rootDir
|
||||||
|
cm := test.NewControllerManager(ctlr)
|
||||||
|
cm.StartAndWait(conf.HTTP.Port)
|
||||||
|
defer cm.StopServer()
|
||||||
|
|
||||||
|
repo := repoName
|
||||||
|
image, err := test.GetRandomImage("tag")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
imgDigest, err := image.Digest()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
err = test.UploadImage(image, baseURL, repo)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
// add referrers
|
||||||
|
ref1, err := test.GetImageWithSubject(imgDigest, ispec.MediaTypeImageManifest)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
ref2, err := test.GetImageWithSubject(imgDigest, ispec.MediaTypeImageManifest)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
ref2.Manifest.Config.MediaType = customArtTypeV1
|
||||||
|
|
||||||
|
ref3, err := test.GetImageWithSubject(imgDigest, ispec.MediaTypeImageManifest)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
ref3.Manifest.ArtifactType = customArtTypeV2
|
||||||
|
ref3.Manifest.Config = ispec.DescriptorEmptyJSON
|
||||||
|
|
||||||
|
ref1.Reference = ""
|
||||||
|
err = test.UploadImage(ref1, baseURL, repo)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
ref2.Reference = ""
|
||||||
|
err = test.UploadImage(ref2, baseURL, repo)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
ref3.Reference = ""
|
||||||
|
err = test.UploadImage(ref3, baseURL, repo)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
Convey("JSON format", func() {
|
||||||
|
args := []string{"reftest", "--output", "json", "--subject", repo + "@" + imgDigest.String()}
|
||||||
|
|
||||||
|
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`,
|
||||||
|
baseURL))
|
||||||
|
|
||||||
|
defer os.Remove(configPath)
|
||||||
|
|
||||||
|
cmd := NewSearchCommand(new(searchService))
|
||||||
|
|
||||||
|
buff := &bytes.Buffer{}
|
||||||
|
cmd.SetOut(buff)
|
||||||
|
cmd.SetErr(buff)
|
||||||
|
cmd.SetArgs(args)
|
||||||
|
err = cmd.Execute()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
fmt.Println(buff.String())
|
||||||
|
})
|
||||||
|
Convey("YAML format", func() {
|
||||||
|
args := []string{"reftest", "--output", "yaml", "--subject", repo + "@" + imgDigest.String()}
|
||||||
|
|
||||||
|
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`,
|
||||||
|
baseURL))
|
||||||
|
|
||||||
|
defer os.Remove(configPath)
|
||||||
|
|
||||||
|
cmd := NewSearchCommand(new(searchService))
|
||||||
|
|
||||||
|
buff := &bytes.Buffer{}
|
||||||
|
cmd.SetOut(buff)
|
||||||
|
cmd.SetErr(buff)
|
||||||
|
cmd.SetArgs(args)
|
||||||
|
err = cmd.Execute()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
fmt.Println(buff.String())
|
||||||
|
})
|
||||||
|
Convey("Invalid format", func() {
|
||||||
|
args := []string{"reftest", "--output", "invalid_format", "--subject", repo + "@" + imgDigest.String()}
|
||||||
|
|
||||||
|
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`,
|
||||||
|
baseURL))
|
||||||
|
|
||||||
|
defer os.Remove(configPath)
|
||||||
|
|
||||||
|
cmd := NewSearchCommand(new(searchService))
|
||||||
|
|
||||||
|
buff := &bytes.Buffer{}
|
||||||
|
cmd.SetOut(buff)
|
||||||
|
cmd.SetErr(buff)
|
||||||
|
cmd.SetArgs(args)
|
||||||
|
err = cmd.Execute()
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReferrersCLIErrors(t *testing.T) {
|
||||||
|
Convey("Errors", t, func() {
|
||||||
|
cmd := NewSearchCommand(new(searchService))
|
||||||
|
|
||||||
|
Convey("no url provided", func() {
|
||||||
|
args := []string{"reftest", "--output", "invalid", "--query", "repo/alpine"}
|
||||||
|
|
||||||
|
configPath := makeConfigFile(`{"configs":[{"_name":"reftest","showspinner":false}]}`)
|
||||||
|
|
||||||
|
defer os.Remove(configPath)
|
||||||
|
|
||||||
|
buff := &bytes.Buffer{}
|
||||||
|
cmd.SetOut(buff)
|
||||||
|
cmd.SetErr(buff)
|
||||||
|
cmd.SetArgs(args)
|
||||||
|
err := cmd.Execute()
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("getConfigValue", func() {
|
||||||
|
args := []string{"reftest", "--subject", "repo/alpine"}
|
||||||
|
|
||||||
|
configPath := makeConfigFile(`bad-json`)
|
||||||
|
|
||||||
|
defer os.Remove(configPath)
|
||||||
|
|
||||||
|
buff := &bytes.Buffer{}
|
||||||
|
cmd.SetOut(buff)
|
||||||
|
cmd.SetErr(buff)
|
||||||
|
cmd.SetArgs(args)
|
||||||
|
err := cmd.Execute()
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("bad showspinnerConfig ", func() {
|
||||||
|
args := []string{"reftest"}
|
||||||
|
|
||||||
|
configPath := makeConfigFile(`{"configs":[{"_name":"reftest", "url":"http://127.0.0.1:8080", "showspinner":"bad"}]}`)
|
||||||
|
|
||||||
|
defer os.Remove(configPath)
|
||||||
|
|
||||||
|
buff := &bytes.Buffer{}
|
||||||
|
cmd.SetOut(buff)
|
||||||
|
cmd.SetErr(buff)
|
||||||
|
cmd.SetArgs(args)
|
||||||
|
err := cmd.Execute()
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("bad verifyTLSConfig ", func() {
|
||||||
|
args := []string{"reftest"}
|
||||||
|
|
||||||
|
configPath := makeConfigFile(
|
||||||
|
`{"configs":[{"_name":"reftest", "url":"http://127.0.0.1:8080", "showspinner":false, "verify-tls": "bad"}]}`)
|
||||||
|
|
||||||
|
defer os.Remove(configPath)
|
||||||
|
|
||||||
|
buff := &bytes.Buffer{}
|
||||||
|
cmd.SetOut(buff)
|
||||||
|
cmd.SetErr(buff)
|
||||||
|
cmd.SetArgs(args)
|
||||||
|
err := cmd.Execute()
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("url from config is empty", func() {
|
||||||
|
args := []string{"reftest", "--subject", "repo/alpine"}
|
||||||
|
|
||||||
|
configPath := makeConfigFile(`{"configs":[{"_name":"reftest", "url":"", "showspinner":false}]}`)
|
||||||
|
|
||||||
|
defer os.Remove(configPath)
|
||||||
|
|
||||||
|
buff := &bytes.Buffer{}
|
||||||
|
cmd.SetOut(buff)
|
||||||
|
cmd.SetErr(buff)
|
||||||
|
cmd.SetArgs(args)
|
||||||
|
err := cmd.Execute()
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("bad params combination", func() {
|
||||||
|
args := []string{"reftest"}
|
||||||
|
|
||||||
|
configPath := makeConfigFile(`{"configs":[{"_name":"reftest", "url":"http://127.0.0.1:8080", "showspinner":false}]}`)
|
||||||
|
|
||||||
|
defer os.Remove(configPath)
|
||||||
|
|
||||||
|
buff := &bytes.Buffer{}
|
||||||
|
cmd.SetOut(buff)
|
||||||
|
cmd.SetErr(buff)
|
||||||
|
cmd.SetArgs(args)
|
||||||
|
err := cmd.Execute()
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("no url provided error", func() {
|
||||||
|
args := []string{}
|
||||||
|
|
||||||
|
configPath := makeConfigFile(`bad-json`)
|
||||||
|
|
||||||
|
defer os.Remove(configPath)
|
||||||
|
|
||||||
|
buff := &bytes.Buffer{}
|
||||||
|
cmd.SetOut(buff)
|
||||||
|
cmd.SetErr(buff)
|
||||||
|
cmd.SetArgs(args)
|
||||||
|
err := cmd.Execute()
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
433
pkg/cli/search_cmd_test.go
Normal file
433
pkg/cli/search_cmd_test.go
Normal file
|
@ -0,0 +1,433 @@
|
||||||
|
//go:build search
|
||||||
|
// +build search
|
||||||
|
|
||||||
|
package cli //nolint:testpackage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
. "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 TestGlobalSearchers(t *testing.T) {
|
||||||
|
globalSearcher := globalSearcherGQL{}
|
||||||
|
|
||||||
|
Convey("GQL Searcher", t, func() {
|
||||||
|
Convey("Bad parameters", func() {
|
||||||
|
ok, err := globalSearcher.search(searchConfig{params: map[string]*string{
|
||||||
|
"badParam": ref("badParam"),
|
||||||
|
}})
|
||||||
|
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(ok, ShouldBeFalse)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("global searcher service fail", func() {
|
||||||
|
conf := searchConfig{
|
||||||
|
params: map[string]*string{
|
||||||
|
"query": ref("repo"),
|
||||||
|
},
|
||||||
|
searchService: NewSearchService(),
|
||||||
|
user: ref("test:pass"),
|
||||||
|
servURL: ref("127.0.0.1:8080"),
|
||||||
|
verifyTLS: ref(false),
|
||||||
|
debug: ref(false),
|
||||||
|
verbose: ref(false),
|
||||||
|
fixedFlag: ref(false),
|
||||||
|
}
|
||||||
|
ok, err := globalSearcher.search(conf)
|
||||||
|
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
So(ok, ShouldBeTrue)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("print images fail", func() {
|
||||||
|
conf := searchConfig{
|
||||||
|
params: map[string]*string{
|
||||||
|
"query": ref("repo"),
|
||||||
|
},
|
||||||
|
user: ref("user:pass"),
|
||||||
|
outputFormat: ref("bad-format"),
|
||||||
|
searchService: mockService{},
|
||||||
|
resultWriter: io.Discard,
|
||||||
|
verbose: ref(false),
|
||||||
|
}
|
||||||
|
ok, err := globalSearcher.search(conf)
|
||||||
|
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
So(ok, ShouldBeTrue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSearchCLI(t *testing.T) {
|
||||||
|
Convey("Test GQL", t, func() {
|
||||||
|
rootDir := t.TempDir()
|
||||||
|
|
||||||
|
port := test.GetFreePort()
|
||||||
|
baseURL := test.GetBaseURL(port)
|
||||||
|
conf := config.New()
|
||||||
|
conf.HTTP.Port = port
|
||||||
|
conf.Storage.GC = false
|
||||||
|
defaultVal := true
|
||||||
|
conf.Extensions = &extconf.ExtensionConfig{
|
||||||
|
Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}},
|
||||||
|
}
|
||||||
|
ctlr := api.NewController(conf)
|
||||||
|
ctlr.Config.Storage.RootDirectory = rootDir
|
||||||
|
cm := test.NewControllerManager(ctlr)
|
||||||
|
cm.StartAndWait(conf.HTTP.Port)
|
||||||
|
defer cm.StopServer()
|
||||||
|
|
||||||
|
const (
|
||||||
|
repo1 = "repo"
|
||||||
|
r1tag1 = "repo1tag1"
|
||||||
|
r1tag2 = "repo1tag2"
|
||||||
|
|
||||||
|
repo2 = "repo/alpine"
|
||||||
|
r2tag1 = "repo2tag1"
|
||||||
|
r2tag2 = "repo2tag2"
|
||||||
|
|
||||||
|
repo3 = "repo/test/alpine"
|
||||||
|
r3tag1 = "repo3tag1"
|
||||||
|
r3tag2 = "repo3tag2"
|
||||||
|
)
|
||||||
|
|
||||||
|
image1, err := test.GetImageWithConfig(ispec.Image{
|
||||||
|
Platform: ispec.Platform{
|
||||||
|
OS: "Os",
|
||||||
|
Architecture: "Arch",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
img1Digest, err := image1.Digest()
|
||||||
|
formatterDigest1 := img1Digest.Encoded()[:8]
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
image2, err := test.GetRandomImage("")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
img2Digest, err := image2.Digest()
|
||||||
|
formatterDigest2 := img2Digest.Encoded()[:8]
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
// repo1
|
||||||
|
image1.Reference = r1tag1
|
||||||
|
err = test.UploadImage(image1, baseURL, repo1)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
image2.Reference = r1tag2
|
||||||
|
err = test.UploadImage(image2, baseURL, repo1)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
// repo2
|
||||||
|
image1.Reference = r2tag1
|
||||||
|
err = test.UploadImage(image1, baseURL, repo2)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
image2.Reference = r2tag2
|
||||||
|
err = test.UploadImage(image2, baseURL, repo2)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
// repo3
|
||||||
|
image1.Reference = r3tag1
|
||||||
|
err = test.UploadImage(image1, baseURL, repo3)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
image2.Reference = r3tag2
|
||||||
|
err = test.UploadImage(image2, baseURL, repo3)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
// search by repos
|
||||||
|
|
||||||
|
args := []string{"searchtest", "--query", "test/alpin", "--verbose"}
|
||||||
|
|
||||||
|
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"searchtest","url":"%s","showspinner":false}]}`,
|
||||||
|
baseURL))
|
||||||
|
defer os.Remove(configPath)
|
||||||
|
|
||||||
|
cmd := NewSearchCommand(new(searchService))
|
||||||
|
|
||||||
|
buff := &bytes.Buffer{}
|
||||||
|
cmd.SetOut(buff)
|
||||||
|
cmd.SetErr(buff)
|
||||||
|
cmd.SetArgs(args)
|
||||||
|
err = cmd.Execute()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
space := regexp.MustCompile(`\s+`)
|
||||||
|
str := strings.TrimSpace(space.ReplaceAllString(buff.String(), " "))
|
||||||
|
So(str, ShouldContainSubstring, "NAME SIZE LAST UPDATED DOWNLOADS STARS PLATFORMS")
|
||||||
|
So(str, ShouldContainSubstring, "repo/test/alpine 1.1kB 0001-01-01 00:00:00 +0000 UTC 0 0")
|
||||||
|
So(str, ShouldContainSubstring, "Os/Arch")
|
||||||
|
So(str, ShouldContainSubstring, "linux/amd64")
|
||||||
|
|
||||||
|
fmt.Println("\n", buff.String())
|
||||||
|
|
||||||
|
os.Remove(configPath)
|
||||||
|
|
||||||
|
cmd = NewSearchCommand(new(searchService))
|
||||||
|
|
||||||
|
args = []string{"searchtest", "--query", "repo/alpine:"}
|
||||||
|
|
||||||
|
configPath = makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"searchtest","url":"%s","showspinner":false}]}`,
|
||||||
|
baseURL))
|
||||||
|
|
||||||
|
defer os.Remove(configPath)
|
||||||
|
|
||||||
|
buff = &bytes.Buffer{}
|
||||||
|
cmd.SetOut(buff)
|
||||||
|
cmd.SetErr(buff)
|
||||||
|
cmd.SetArgs(args)
|
||||||
|
err = cmd.Execute()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
str = strings.TrimSpace(space.ReplaceAllString(buff.String(), " "))
|
||||||
|
So(str, ShouldContainSubstring, "IMAGE NAME TAG OS/ARCH DIGEST SIGNED SIZE")
|
||||||
|
So(str, ShouldContainSubstring, "repo/alpine repo2tag1 Os/Arch "+formatterDigest1+" false 577B")
|
||||||
|
So(str, ShouldContainSubstring, "repo/alpine repo2tag2 linux/amd64 "+formatterDigest2+" false 524B")
|
||||||
|
|
||||||
|
fmt.Println("\n", buff.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatsSearchCLI(t *testing.T) {
|
||||||
|
Convey("", t, func() {
|
||||||
|
rootDir := t.TempDir()
|
||||||
|
|
||||||
|
port := test.GetFreePort()
|
||||||
|
baseURL := test.GetBaseURL(port)
|
||||||
|
conf := config.New()
|
||||||
|
conf.HTTP.Port = port
|
||||||
|
conf.Storage.GC = false
|
||||||
|
defaultVal := true
|
||||||
|
conf.Extensions = &extconf.ExtensionConfig{
|
||||||
|
Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}},
|
||||||
|
}
|
||||||
|
ctlr := api.NewController(conf)
|
||||||
|
ctlr.Config.Storage.RootDirectory = rootDir
|
||||||
|
cm := test.NewControllerManager(ctlr)
|
||||||
|
cm.StartAndWait(conf.HTTP.Port)
|
||||||
|
defer cm.StopServer()
|
||||||
|
|
||||||
|
const (
|
||||||
|
repo1 = "repo"
|
||||||
|
r1tag1 = "repo1tag1"
|
||||||
|
r1tag2 = "repo1tag2"
|
||||||
|
|
||||||
|
repo2 = "repo/alpine"
|
||||||
|
r2tag1 = "repo2tag1"
|
||||||
|
r2tag2 = "repo2tag2"
|
||||||
|
|
||||||
|
repo3 = "repo/test/alpine"
|
||||||
|
r3tag1 = "repo3tag1"
|
||||||
|
r3tag2 = "repo3tag2"
|
||||||
|
)
|
||||||
|
|
||||||
|
image1, err := test.GetRandomImage("")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
image2, err := test.GetRandomImage("")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
// repo1
|
||||||
|
image1.Reference = r1tag1
|
||||||
|
err = test.UploadImage(image1, baseURL, repo1)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
image2.Reference = r1tag2
|
||||||
|
err = test.UploadImage(image2, baseURL, repo1)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
// repo2
|
||||||
|
image1.Reference = r2tag1
|
||||||
|
err = test.UploadImage(image1, baseURL, repo2)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
image2.Reference = r2tag2
|
||||||
|
err = test.UploadImage(image2, baseURL, repo2)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
// repo3
|
||||||
|
image1.Reference = r3tag1
|
||||||
|
err = test.UploadImage(image1, baseURL, repo3)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
image2.Reference = r3tag2
|
||||||
|
err = test.UploadImage(image2, baseURL, repo3)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
cmd := NewSearchCommand(new(searchService))
|
||||||
|
|
||||||
|
Convey("JSON format", func() {
|
||||||
|
args := []string{"searchtest", "--output", "json", "--query", "repo/alpine"}
|
||||||
|
|
||||||
|
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"searchtest","url":"%s","showspinner":false}]}`,
|
||||||
|
baseURL))
|
||||||
|
|
||||||
|
defer os.Remove(configPath)
|
||||||
|
|
||||||
|
buff := &bytes.Buffer{}
|
||||||
|
cmd.SetOut(buff)
|
||||||
|
cmd.SetErr(buff)
|
||||||
|
cmd.SetArgs(args)
|
||||||
|
err = cmd.Execute()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
fmt.Println(buff.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("YAML format", func() {
|
||||||
|
args := []string{"searchtest", "--output", "yaml", "--query", "repo/alpine"}
|
||||||
|
|
||||||
|
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"searchtest","url":"%s","showspinner":false}]}`,
|
||||||
|
baseURL))
|
||||||
|
|
||||||
|
defer os.Remove(configPath)
|
||||||
|
|
||||||
|
buff := &bytes.Buffer{}
|
||||||
|
cmd.SetOut(buff)
|
||||||
|
cmd.SetErr(buff)
|
||||||
|
cmd.SetArgs(args)
|
||||||
|
err = cmd.Execute()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
fmt.Println(buff.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Invalid format", func() {
|
||||||
|
args := []string{"searchtest", "--output", "invalid", "--query", "repo/alpine"}
|
||||||
|
|
||||||
|
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"searchtest","url":"%s","showspinner":false}]}`,
|
||||||
|
baseURL))
|
||||||
|
|
||||||
|
defer os.Remove(configPath)
|
||||||
|
|
||||||
|
buff := &bytes.Buffer{}
|
||||||
|
cmd.SetOut(buff)
|
||||||
|
cmd.SetErr(buff)
|
||||||
|
cmd.SetArgs(args)
|
||||||
|
err = cmd.Execute()
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSearchCLIErrors(t *testing.T) {
|
||||||
|
Convey("Errors", t, func() {
|
||||||
|
cmd := NewSearchCommand(new(searchService))
|
||||||
|
|
||||||
|
Convey("no url provided", func() {
|
||||||
|
args := []string{"searchtest", "--output", "invalid", "--query", "repo/alpine"}
|
||||||
|
|
||||||
|
configPath := makeConfigFile(`{"configs":[{"_name":"searchtest","showspinner":false}]}`)
|
||||||
|
|
||||||
|
defer os.Remove(configPath)
|
||||||
|
|
||||||
|
buff := &bytes.Buffer{}
|
||||||
|
cmd.SetOut(buff)
|
||||||
|
cmd.SetErr(buff)
|
||||||
|
cmd.SetArgs(args)
|
||||||
|
err := cmd.Execute()
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("getConfigValue", func() {
|
||||||
|
args := []string{"searchtest", "--output", "invalid", "--query", "repo/alpine"}
|
||||||
|
|
||||||
|
configPath := makeConfigFile(`bad-json`)
|
||||||
|
|
||||||
|
defer os.Remove(configPath)
|
||||||
|
|
||||||
|
buff := &bytes.Buffer{}
|
||||||
|
cmd.SetOut(buff)
|
||||||
|
cmd.SetErr(buff)
|
||||||
|
cmd.SetArgs(args)
|
||||||
|
err := cmd.Execute()
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("bad showspinnerConfig ", func() {
|
||||||
|
args := []string{"searchtest"}
|
||||||
|
|
||||||
|
configPath := makeConfigFile(
|
||||||
|
`{"configs":[{"_name":"searchtest", "url":"http://127.0.0.1:8080", "showspinner":"bad"}]}`)
|
||||||
|
|
||||||
|
defer os.Remove(configPath)
|
||||||
|
|
||||||
|
buff := &bytes.Buffer{}
|
||||||
|
cmd.SetOut(buff)
|
||||||
|
cmd.SetErr(buff)
|
||||||
|
cmd.SetArgs(args)
|
||||||
|
err := cmd.Execute()
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("bad verifyTLSConfig ", func() {
|
||||||
|
args := []string{"searchtest"}
|
||||||
|
|
||||||
|
configPath := makeConfigFile(
|
||||||
|
`{"configs":[{"_name":"searchtest", "url":"http://127.0.0.1:8080", "showspinner":false, "verify-tls": "bad"}]}`)
|
||||||
|
|
||||||
|
defer os.Remove(configPath)
|
||||||
|
|
||||||
|
buff := &bytes.Buffer{}
|
||||||
|
cmd.SetOut(buff)
|
||||||
|
cmd.SetErr(buff)
|
||||||
|
cmd.SetArgs(args)
|
||||||
|
err := cmd.Execute()
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("url from config is empty", func() {
|
||||||
|
args := []string{"searchtest", "--output", "invalid", "--query", "repo/alpine"}
|
||||||
|
|
||||||
|
configPath := makeConfigFile(`{"configs":[{"_name":"searchtest", "url":"", "showspinner":false}]}`)
|
||||||
|
|
||||||
|
defer os.Remove(configPath)
|
||||||
|
|
||||||
|
buff := &bytes.Buffer{}
|
||||||
|
cmd.SetOut(buff)
|
||||||
|
cmd.SetErr(buff)
|
||||||
|
cmd.SetArgs(args)
|
||||||
|
err := cmd.Execute()
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("no url provided error", func() {
|
||||||
|
args := []string{}
|
||||||
|
|
||||||
|
configPath := makeConfigFile(`bad-json`)
|
||||||
|
|
||||||
|
defer os.Remove(configPath)
|
||||||
|
|
||||||
|
buff := &bytes.Buffer{}
|
||||||
|
cmd.SetOut(buff)
|
||||||
|
cmd.SetErr(buff)
|
||||||
|
cmd.SetArgs(args)
|
||||||
|
err := cmd.Execute()
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("globalSearch without gql active", func() {
|
||||||
|
err := globalSearch(searchConfig{
|
||||||
|
user: ref("t"),
|
||||||
|
servURL: ref("t"),
|
||||||
|
verifyTLS: ref(false),
|
||||||
|
debug: ref(false),
|
||||||
|
params: map[string]*string{
|
||||||
|
"query": ref("t"),
|
||||||
|
},
|
||||||
|
resultWriter: io.Discard,
|
||||||
|
})
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
@ -15,6 +16,8 @@ import (
|
||||||
"github.com/briandowns/spinner"
|
"github.com/briandowns/spinner"
|
||||||
|
|
||||||
zotErrors "zotregistry.io/zot/errors"
|
zotErrors "zotregistry.io/zot/errors"
|
||||||
|
"zotregistry.io/zot/pkg/api/constants"
|
||||||
|
zcommon "zotregistry.io/zot/pkg/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getImageSearchers() []searcher {
|
func getImageSearchers() []searcher {
|
||||||
|
@ -61,6 +64,24 @@ func getCveSearchersGQL() []searcher {
|
||||||
return searchers
|
return searchers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getGlobalSearchersGQL() []searcher {
|
||||||
|
searchers := []searcher{
|
||||||
|
new(globalSearcherGQL),
|
||||||
|
new(referrerSearcherGQL),
|
||||||
|
}
|
||||||
|
|
||||||
|
return searchers
|
||||||
|
}
|
||||||
|
|
||||||
|
func getGlobalSearchersREST() []searcher {
|
||||||
|
searchers := []searcher{
|
||||||
|
new(referrerSearcher),
|
||||||
|
new(globalSearcherREST),
|
||||||
|
}
|
||||||
|
|
||||||
|
return searchers
|
||||||
|
}
|
||||||
|
|
||||||
type searcher interface {
|
type searcher interface {
|
||||||
search(searchConfig searchConfig) (bool, error)
|
search(searchConfig searchConfig) (bool, error)
|
||||||
}
|
}
|
||||||
|
@ -194,7 +215,7 @@ func getImages(config searchConfig) error {
|
||||||
imageListData = append(imageListData, imageStruct(image))
|
imageListData = append(imageListData, imageStruct(image))
|
||||||
}
|
}
|
||||||
|
|
||||||
return printResult(config, imageListData)
|
return printImageResult(config, imageListData)
|
||||||
}
|
}
|
||||||
|
|
||||||
type imagesByDigestSearcher struct{}
|
type imagesByDigestSearcher struct{}
|
||||||
|
@ -253,7 +274,7 @@ func (search derivedImageListSearcherGQL) search(config searchConfig) (bool, err
|
||||||
imageListData = append(imageListData, imageStruct(image))
|
imageListData = append(imageListData, imageStruct(image))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := printResult(config, imageListData); err != nil {
|
if err := printImageResult(config, imageListData); err != nil {
|
||||||
return true, err
|
return true, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -284,7 +305,7 @@ func (search baseImageListSearcherGQL) search(config searchConfig) (bool, error)
|
||||||
imageListData = append(imageListData, imageStruct(image))
|
imageListData = append(imageListData, imageStruct(image))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := printResult(config, imageListData); err != nil {
|
if err := printImageResult(config, imageListData); err != nil {
|
||||||
return true, err
|
return true, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -316,7 +337,7 @@ func (search imagesByDigestSearcherGQL) search(config searchConfig) (bool, error
|
||||||
imageListData = append(imageListData, imageStruct(image))
|
imageListData = append(imageListData, imageStruct(image))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := printResult(config, imageListData); err != nil {
|
if err := printImageResult(config, imageListData); err != nil {
|
||||||
return true, err
|
return true, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -461,7 +482,7 @@ func (search imagesByCVEIDSearcherGQL) search(config searchConfig) (bool, error)
|
||||||
imageListData = append(imageListData, imageStruct(image))
|
imageListData = append(imageListData, imageStruct(image))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := printResult(config, imageListData); err != nil {
|
if err := printImageResult(config, imageListData); err != nil {
|
||||||
return true, err
|
return true, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -603,7 +624,153 @@ func getTagsByCVE(config searchConfig) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return printResult(config, imageList)
|
return printImageResult(config, imageList)
|
||||||
|
}
|
||||||
|
|
||||||
|
type referrerSearcherGQL struct{}
|
||||||
|
|
||||||
|
func (search referrerSearcherGQL) search(config searchConfig) (bool, error) {
|
||||||
|
if !canSearch(config.params, newSet("subject")) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
username, password := getUsernameAndPassword(*config.user)
|
||||||
|
|
||||||
|
repo, ref, refIsTag, err := zcommon.GetRepoRefference(*config.params["subject"])
|
||||||
|
if err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
digest := ref
|
||||||
|
|
||||||
|
if refIsTag {
|
||||||
|
digest, err = fetchImageDigest(repo, ref, username, password, config)
|
||||||
|
if err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := config.searchService.getReferrersGQL(context.Background(), config, username, password, repo, digest)
|
||||||
|
if err != nil {
|
||||||
|
return true, 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 true, printReferrersResult(config, referrersList, maxArtifactTypeLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchImageDigest(repo, ref, username, password string, config searchConfig) (string, error) {
|
||||||
|
url, err := combineServerAndEndpointURL(*config.servURL, fmt.Sprintf("/v2/%s/manifests/%s", repo, ref))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := makeHEADRequest(context.Background(), url, username, password, *config.verifyTLS, false)
|
||||||
|
|
||||||
|
digestStr := res.Get(constants.DistContentDigestKey)
|
||||||
|
|
||||||
|
return digestStr, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type referrerSearcher struct{}
|
||||||
|
|
||||||
|
func (search referrerSearcher) search(config searchConfig) (bool, error) {
|
||||||
|
if !canSearch(config.params, newSet("subject")) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
username, password := getUsernameAndPassword(*config.user)
|
||||||
|
|
||||||
|
repo, ref, refIsTag, err := zcommon.GetRepoRefference(*config.params["subject"])
|
||||||
|
if err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
digest := ref
|
||||||
|
|
||||||
|
if refIsTag {
|
||||||
|
digest, err = fetchImageDigest(repo, ref, username, password, config)
|
||||||
|
if err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
referrersList, err := config.searchService.getReferrers(context.Background(), config, username, password,
|
||||||
|
repo, digest)
|
||||||
|
if err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
maxArtifactTypeLen := math.MinInt
|
||||||
|
|
||||||
|
for _, referrer := range referrersList {
|
||||||
|
if maxArtifactTypeLen < len(referrer.ArtifactType) {
|
||||||
|
maxArtifactTypeLen = len(referrer.ArtifactType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
printReferrersTableHeader(config, config.resultWriter, maxArtifactTypeLen)
|
||||||
|
|
||||||
|
return true, printReferrersResult(config, referrersList, maxArtifactTypeLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
type globalSearcherGQL struct{}
|
||||||
|
|
||||||
|
func (search globalSearcherGQL) search(config searchConfig) (bool, error) {
|
||||||
|
if !canSearch(config.params, newSet("query")) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
username, password := getUsernameAndPassword(*config.user)
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
query := *config.params["query"]
|
||||||
|
|
||||||
|
globalSearchResult, err := config.searchService.globalSearchGQL(ctx, config, username, password, query)
|
||||||
|
if err != nil {
|
||||||
|
return true, 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 true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, printRepoResults(config, reposList)
|
||||||
|
}
|
||||||
|
|
||||||
|
type globalSearcherREST struct{}
|
||||||
|
|
||||||
|
func (search globalSearcherREST) search(config searchConfig) (bool, error) {
|
||||||
|
if !canSearch(config.params, newSet("query")) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, fmt.Errorf("search extension is not enabled: %w", zotErrors.ErrExtensionNotEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectResults(config searchConfig, wg *sync.WaitGroup, imageErr chan stringResult,
|
func collectResults(config searchConfig, wg *sync.WaitGroup, imageErr chan stringResult,
|
||||||
|
@ -779,7 +946,7 @@ func printImageTableHeader(writer io.Writer, verbose bool, maxImageNameLen, maxT
|
||||||
}
|
}
|
||||||
|
|
||||||
row[colDigestIndex] = "DIGEST"
|
row[colDigestIndex] = "DIGEST"
|
||||||
row[colSizeIndex] = "SIZE"
|
row[colSizeIndex] = sizeColumn
|
||||||
row[colIsSignedIndex] = "SIGNED"
|
row[colIsSignedIndex] = "SIGNED"
|
||||||
|
|
||||||
if verbose {
|
if verbose {
|
||||||
|
@ -802,7 +969,94 @@ func printCVETableHeader(writer io.Writer, verbose bool, maxImgLen, maxTagLen, m
|
||||||
table.Render()
|
table.Render()
|
||||||
}
|
}
|
||||||
|
|
||||||
func printResult(config searchConfig, imageList []imageStruct) error {
|
func printReferrersTableHeader(config searchConfig, writer io.Writer, maxArtifactTypeLen int) {
|
||||||
|
if *config.outputFormat != "" && *config.outputFormat != defaultOutoutFormat {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
table := getReferrersTableWriter(writer)
|
||||||
|
|
||||||
|
table.SetColMinWidth(refArtifactTypeIndex, maxArtifactTypeLen)
|
||||||
|
table.SetColMinWidth(refDigestIndex, digestWidth)
|
||||||
|
table.SetColMinWidth(refSizeIndex, sizeWidth)
|
||||||
|
|
||||||
|
row := make([]string, refRowWidth)
|
||||||
|
|
||||||
|
// adding spaces so that image name and tag columns are aligned
|
||||||
|
// in case the name/tag are fully shown and too long
|
||||||
|
var offset string
|
||||||
|
|
||||||
|
if maxArtifactTypeLen > len("ARTIFACT TYPE") {
|
||||||
|
offset = strings.Repeat(" ", maxArtifactTypeLen-len("ARTIFACT TYPE"))
|
||||||
|
row[refArtifactTypeIndex] = "ARTIFACT TYPE" + offset
|
||||||
|
} else {
|
||||||
|
row[refArtifactTypeIndex] = "ARTIFACT TYPE"
|
||||||
|
}
|
||||||
|
|
||||||
|
row[refDigestIndex] = "DIGEST"
|
||||||
|
row[refSizeIndex] = sizeColumn
|
||||||
|
|
||||||
|
table.Append(row)
|
||||||
|
table.Render()
|
||||||
|
}
|
||||||
|
|
||||||
|
func printRepoTableHeader(writer io.Writer, repoMaxLen, maxTimeLen int, verbose bool) {
|
||||||
|
table := getRepoTableWriter(writer)
|
||||||
|
|
||||||
|
table.SetColMinWidth(repoNameIndex, repoMaxLen)
|
||||||
|
table.SetColMinWidth(repoSizeIndex, sizeWidth)
|
||||||
|
table.SetColMinWidth(repoLastUpdatedIndex, maxTimeLen)
|
||||||
|
table.SetColMinWidth(repoDownloadsIndex, sizeWidth)
|
||||||
|
table.SetColMinWidth(repoStarsIndex, sizeWidth)
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
table.SetColMinWidth(repoPlatformsIndex, platformWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
row := make([]string, repoRowWidth)
|
||||||
|
|
||||||
|
// adding spaces so that image name and tag columns are aligned
|
||||||
|
// in case the name/tag are fully shown and too long
|
||||||
|
var offset string
|
||||||
|
|
||||||
|
if repoMaxLen > len("NAME") {
|
||||||
|
offset = strings.Repeat(" ", repoMaxLen-len("NAME"))
|
||||||
|
row[repoNameIndex] = "NAME" + offset
|
||||||
|
} else {
|
||||||
|
row[repoNameIndex] = "NAME"
|
||||||
|
}
|
||||||
|
|
||||||
|
if repoMaxLen > len("LAST UPDATED") {
|
||||||
|
offset = strings.Repeat(" ", repoMaxLen-len("LAST UPDATED"))
|
||||||
|
row[repoLastUpdatedIndex] = "LAST UPDATED" + offset
|
||||||
|
} else {
|
||||||
|
row[repoLastUpdatedIndex] = "LAST UPDATED"
|
||||||
|
}
|
||||||
|
|
||||||
|
row[repoSizeIndex] = sizeColumn
|
||||||
|
row[repoDownloadsIndex] = "DOWNLOADS"
|
||||||
|
row[repoStarsIndex] = "STARS"
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
row[repoPlatformsIndex] = "PLATFORMS"
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Append(row)
|
||||||
|
table.Render()
|
||||||
|
}
|
||||||
|
|
||||||
|
func printReferrersResult(config searchConfig, referrersList referrersResult, maxArtifactTypeLen int) error {
|
||||||
|
out, err := referrersList.string(*config.outputFormat, maxArtifactTypeLen)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(config.resultWriter, out)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printImageResult(config searchConfig, imageList []imageStruct) error {
|
||||||
var builder strings.Builder
|
var builder strings.Builder
|
||||||
maxImgNameLen := 0
|
maxImgNameLen := 0
|
||||||
maxTagLen := 0
|
maxTagLen := 0
|
||||||
|
@ -846,6 +1100,36 @@ func printResult(config searchConfig, imageList []imageStruct) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func printRepoResults(config searchConfig, repoList []repoStruct) error {
|
||||||
|
maxRepoNameLen := 0
|
||||||
|
maxTimeLen := 0
|
||||||
|
|
||||||
|
for _, repo := range repoList {
|
||||||
|
if maxRepoNameLen < len(repo.Name) {
|
||||||
|
maxRepoNameLen = len(repo.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxTimeLen < len(repo.LastUpdated.String()) {
|
||||||
|
maxTimeLen = len(repo.LastUpdated.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(repoList) > 0 {
|
||||||
|
printRepoTableHeader(config.resultWriter, maxRepoNameLen, maxTimeLen, *config.verbose)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, repo := range repoList {
|
||||||
|
out, err := repo.string(*config.outputFormat, maxRepoNameLen, maxTimeLen, *config.verbose)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(config.resultWriter, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errInvalidImageNameAndTag = errors.New("cli: Invalid input format. Expected IMAGENAME:TAG")
|
errInvalidImageNameAndTag = errors.New("cli: Invalid input format. Expected IMAGENAME:TAG")
|
||||||
errInvalidImageName = errors.New("cli: Invalid input format. Expected IMAGENAME without :TAG")
|
errInvalidImageName = errors.New("cli: Invalid input format. Expected IMAGENAME without :TAG")
|
||||||
|
@ -876,3 +1160,7 @@ func (search repoSearcher) searchRepos(config searchConfig) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
sizeColumn = "SIZE"
|
||||||
|
)
|
||||||
|
|
|
@ -25,6 +25,12 @@ import (
|
||||||
"zotregistry.io/zot/pkg/common"
|
"zotregistry.io/zot/pkg/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
jsonFormat = "json"
|
||||||
|
yamlFormat = "yaml"
|
||||||
|
ymlFormat = "yml"
|
||||||
|
)
|
||||||
|
|
||||||
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)
|
||||||
|
@ -42,6 +48,10 @@ type SearchService interface { //nolint:interfacebloat
|
||||||
derivedImage string) (*common.DerivedImageListResponse, error)
|
derivedImage string) (*common.DerivedImageListResponse, error)
|
||||||
getBaseImageListGQL(ctx context.Context, config searchConfig, username, password string,
|
getBaseImageListGQL(ctx context.Context, config searchConfig, username, password string,
|
||||||
baseImage string) (*common.BaseImageListResponse, error)
|
baseImage string) (*common.BaseImageListResponse, error)
|
||||||
|
getReferrersGQL(ctx context.Context, config searchConfig, username, password string,
|
||||||
|
repo, digest string) (*common.ReferrersResp, error)
|
||||||
|
globalSearchGQL(ctx context.Context, config searchConfig, username, password string,
|
||||||
|
query string) (*common.GlobalSearch, error)
|
||||||
|
|
||||||
getAllImages(ctx context.Context, config searchConfig, username, password string,
|
getAllImages(ctx context.Context, config searchConfig, username, password string,
|
||||||
channel chan stringResult, wtgrp *sync.WaitGroup)
|
channel chan stringResult, wtgrp *sync.WaitGroup)
|
||||||
|
@ -59,6 +69,8 @@ type SearchService interface { //nolint:interfacebloat
|
||||||
channel chan stringResult, wtgrp *sync.WaitGroup)
|
channel chan stringResult, wtgrp *sync.WaitGroup)
|
||||||
getImageByNameAndCVEID(ctx context.Context, config searchConfig, username, password, imageName, cvid string,
|
getImageByNameAndCVEID(ctx context.Context, config searchConfig, username, password, imageName, cvid string,
|
||||||
channel chan stringResult, wtgrp *sync.WaitGroup)
|
channel chan stringResult, wtgrp *sync.WaitGroup)
|
||||||
|
getReferrers(ctx context.Context, config searchConfig, username, password string, repo, digest string,
|
||||||
|
) (referrersResult, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type searchService struct{}
|
type searchService struct{}
|
||||||
|
@ -103,6 +115,75 @@ func (service searchService) getDerivedImageListGQL(ctx context.Context, config
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (service searchService) getReferrersGQL(ctx context.Context, config searchConfig, username, password string,
|
||||||
|
repo, digest string,
|
||||||
|
) (*common.ReferrersResp, error) {
|
||||||
|
query := fmt.Sprintf(`
|
||||||
|
{
|
||||||
|
Referrers( repo: "%s", digest: "%s" ){
|
||||||
|
ArtifactType,
|
||||||
|
Digest,
|
||||||
|
MediaType,
|
||||||
|
Size,
|
||||||
|
Annotations{
|
||||||
|
Key
|
||||||
|
Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`, repo, digest)
|
||||||
|
|
||||||
|
result := &common.ReferrersResp{}
|
||||||
|
|
||||||
|
err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
|
||||||
|
if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil {
|
||||||
|
return nil, errResult
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service searchService) globalSearchGQL(ctx context.Context, config searchConfig, username, password string,
|
||||||
|
query string,
|
||||||
|
) (*common.GlobalSearch, error) {
|
||||||
|
GQLQuery := fmt.Sprintf(`
|
||||||
|
{
|
||||||
|
GlobalSearch(query:"%s"){
|
||||||
|
Images {
|
||||||
|
RepoName
|
||||||
|
Tag
|
||||||
|
MediaType
|
||||||
|
Digest
|
||||||
|
Size
|
||||||
|
Manifests {
|
||||||
|
Digest
|
||||||
|
ConfigDigest
|
||||||
|
Platform {Os Arch}
|
||||||
|
Size
|
||||||
|
IsSigned
|
||||||
|
Layers {Digest Size}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Repos {
|
||||||
|
Name
|
||||||
|
Platforms { Os Arch }
|
||||||
|
LastUpdated
|
||||||
|
Size
|
||||||
|
DownloadCount
|
||||||
|
StarCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`, query)
|
||||||
|
|
||||||
|
result := &common.GlobalSearchResultResp{}
|
||||||
|
|
||||||
|
err := service.makeGraphQLQuery(ctx, config, username, password, GQLQuery, result)
|
||||||
|
if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil {
|
||||||
|
return nil, errResult
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result.GlobalSearch, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (service searchService) getBaseImageListGQL(ctx context.Context, config searchConfig, username, password string,
|
func (service searchService) getBaseImageListGQL(ctx context.Context, config searchConfig, username, password string,
|
||||||
baseImage string,
|
baseImage string,
|
||||||
) (*common.BaseImageListResponse, error) {
|
) (*common.BaseImageListResponse, error) {
|
||||||
|
@ -328,6 +409,44 @@ func (service searchService) getFixedTagsForCVEGQL(ctx context.Context, config s
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (service searchService) getReferrers(ctx context.Context, config searchConfig, username, password string,
|
||||||
|
repo, digest string,
|
||||||
|
) (referrersResult, error) {
|
||||||
|
referrersEndpoint, err := combineServerAndEndpointURL(*config.servURL,
|
||||||
|
fmt.Sprintf("/v2/%s/referrers/%s", repo, digest))
|
||||||
|
if err != nil {
|
||||||
|
if isContextDone(ctx) {
|
||||||
|
return referrersResult{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return referrersResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
referrerResp := &ispec.Index{}
|
||||||
|
_, err = makeGETRequest(ctx, referrersEndpoint, username, password, *config.verifyTLS,
|
||||||
|
*config.debug, &referrerResp, config.resultWriter)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if isContextDone(ctx) {
|
||||||
|
return referrersResult{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return referrersResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
referrersList := referrersResult{}
|
||||||
|
|
||||||
|
for _, referrer := range referrerResp.Manifests {
|
||||||
|
referrersList = append(referrersList, common.Referrer{
|
||||||
|
ArtifactType: referrer.ArtifactType,
|
||||||
|
Digest: referrer.Digest.String(),
|
||||||
|
Size: int(referrer.Size),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return referrersList, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (service searchService) getImageByName(ctx context.Context, config searchConfig,
|
func (service searchService) getImageByName(ctx context.Context, config searchConfig,
|
||||||
username, password, imageName string, rch chan stringResult, wtgrp *sync.WaitGroup,
|
username, password, imageName string, rch chan stringResult, wtgrp *sync.WaitGroup,
|
||||||
) {
|
) {
|
||||||
|
@ -940,9 +1059,9 @@ func (cve cveResult) string(format string) (string, error) {
|
||||||
switch strings.ToLower(format) {
|
switch strings.ToLower(format) {
|
||||||
case "", defaultOutoutFormat:
|
case "", defaultOutoutFormat:
|
||||||
return cve.stringPlainText()
|
return cve.stringPlainText()
|
||||||
case "json":
|
case jsonFormat:
|
||||||
return cve.stringJSON()
|
return cve.stringJSON()
|
||||||
case "yml", "yaml":
|
case ymlFormat, yamlFormat:
|
||||||
return cve.stringYAML()
|
return cve.stringYAML()
|
||||||
default:
|
default:
|
||||||
return "", ErrInvalidOutputFormat
|
return "", ErrInvalidOutputFormat
|
||||||
|
@ -991,15 +1110,167 @@ func (cve cveResult) stringYAML() (string, error) {
|
||||||
return string(body), nil
|
return string(body), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type referrersResult []common.Referrer
|
||||||
|
|
||||||
|
func (ref referrersResult) string(format string, maxArtifactTypeLen int) (string, error) {
|
||||||
|
switch strings.ToLower(format) {
|
||||||
|
case "", defaultOutoutFormat:
|
||||||
|
return ref.stringPlainText(maxArtifactTypeLen)
|
||||||
|
case jsonFormat:
|
||||||
|
return ref.stringJSON()
|
||||||
|
case ymlFormat, yamlFormat:
|
||||||
|
return ref.stringYAML()
|
||||||
|
default:
|
||||||
|
return "", ErrInvalidOutputFormat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ref referrersResult) stringPlainText(maxArtifactTypeLen int) (string, error) {
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
table := getImageTableWriter(&builder)
|
||||||
|
|
||||||
|
table.SetColMinWidth(refArtifactTypeIndex, maxArtifactTypeLen)
|
||||||
|
table.SetColMinWidth(refDigestIndex, digestWidth)
|
||||||
|
table.SetColMinWidth(refSizeIndex, sizeWidth)
|
||||||
|
|
||||||
|
for _, referrer := range ref {
|
||||||
|
artifactType := ellipsize(referrer.ArtifactType, maxArtifactTypeLen, ellipsis)
|
||||||
|
// digest := ellipsize(godigest.Digest(referrer.Digest).Encoded(), digestWidth, "")
|
||||||
|
size := ellipsize(humanize.Bytes(uint64(referrer.Size)), sizeWidth, ellipsis)
|
||||||
|
|
||||||
|
row := make([]string, refRowWidth)
|
||||||
|
row[refArtifactTypeIndex] = artifactType
|
||||||
|
row[refDigestIndex] = referrer.Digest
|
||||||
|
row[refSizeIndex] = size
|
||||||
|
|
||||||
|
table.Append(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Render()
|
||||||
|
|
||||||
|
return builder.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ref referrersResult) stringJSON() (string, error) {
|
||||||
|
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
||||||
|
|
||||||
|
body, err := json.MarshalIndent(ref, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(body), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ref referrersResult) stringYAML() (string, error) {
|
||||||
|
body, err := yaml.Marshal(ref)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(body), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type repoStruct common.RepoSummary
|
||||||
|
|
||||||
|
func (repo repoStruct) string(format string, maxImgNameLen, maxTimeLen int, verbose bool) (string, error) { //nolint: lll
|
||||||
|
switch strings.ToLower(format) {
|
||||||
|
case "", defaultOutoutFormat:
|
||||||
|
return repo.stringPlainText(maxImgNameLen, maxTimeLen, verbose)
|
||||||
|
case jsonFormat:
|
||||||
|
return repo.stringJSON()
|
||||||
|
case ymlFormat, yamlFormat:
|
||||||
|
return repo.stringYAML()
|
||||||
|
default:
|
||||||
|
return "", ErrInvalidOutputFormat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo repoStruct) stringPlainText(repoMaxLen, maxTimeLen int, verbose bool) (string, error) {
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
table := getImageTableWriter(&builder)
|
||||||
|
|
||||||
|
table.SetColMinWidth(repoNameIndex, repoMaxLen)
|
||||||
|
table.SetColMinWidth(repoSizeIndex, sizeWidth)
|
||||||
|
table.SetColMinWidth(repoLastUpdatedIndex, maxTimeLen)
|
||||||
|
table.SetColMinWidth(repoDownloadsIndex, dounloadsWidth)
|
||||||
|
table.SetColMinWidth(repoStarsIndex, signedWidth)
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
table.SetColMinWidth(repoPlatformsIndex, platformWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
repoSize, err := strconv.Atoi(repo.Size)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
repoName := repo.Name
|
||||||
|
repoLastUpdated := repo.LastUpdated
|
||||||
|
repoDownloads := repo.DownloadCount
|
||||||
|
repoStars := repo.StarCount
|
||||||
|
repoPlatforms := repo.Platforms
|
||||||
|
|
||||||
|
row := make([]string, repoRowWidth)
|
||||||
|
row[repoNameIndex] = repoName
|
||||||
|
row[repoSizeIndex] = ellipsize(strings.ReplaceAll(humanize.Bytes(uint64(repoSize)), " ", ""), sizeWidth, ellipsis)
|
||||||
|
row[repoLastUpdatedIndex] = repoLastUpdated.String()
|
||||||
|
row[repoDownloadsIndex] = strconv.Itoa(repoDownloads)
|
||||||
|
row[repoStarsIndex] = strconv.Itoa(repoStars)
|
||||||
|
|
||||||
|
if verbose && len(repoPlatforms) > 0 {
|
||||||
|
row[repoPlatformsIndex] = getPlatformStr(repoPlatforms[0])
|
||||||
|
repoPlatforms = repoPlatforms[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Append(row)
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
for _, platform := range repoPlatforms {
|
||||||
|
row := make([]string, repoRowWidth)
|
||||||
|
|
||||||
|
row[repoPlatformsIndex] = getPlatformStr(platform)
|
||||||
|
|
||||||
|
table.Append(row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Render()
|
||||||
|
|
||||||
|
return builder.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo repoStruct) stringJSON() (string, error) {
|
||||||
|
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
||||||
|
|
||||||
|
body, err := json.MarshalIndent(repo, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(body), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo repoStruct) stringYAML() (string, error) {
|
||||||
|
body, err := yaml.Marshal(&repo)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(body), nil
|
||||||
|
}
|
||||||
|
|
||||||
type imageStruct common.ImageSummary
|
type imageStruct common.ImageSummary
|
||||||
|
|
||||||
func (img imageStruct) string(format string, maxImgNameLen, maxTagLen, maxPlatformLen int, verbose bool) (string, error) { //nolint: lll
|
func (img imageStruct) string(format string, maxImgNameLen, maxTagLen, maxPlatformLen int, verbose bool) (string, error) { //nolint: lll
|
||||||
switch strings.ToLower(format) {
|
switch strings.ToLower(format) {
|
||||||
case "", defaultOutoutFormat:
|
case "", defaultOutoutFormat:
|
||||||
return img.stringPlainText(maxImgNameLen, maxTagLen, maxPlatformLen, verbose)
|
return img.stringPlainText(maxImgNameLen, maxTagLen, maxPlatformLen, verbose)
|
||||||
case "json":
|
case jsonFormat:
|
||||||
return img.stringJSON()
|
return img.stringJSON()
|
||||||
case "yml", "yaml":
|
case ymlFormat, yamlFormat:
|
||||||
return img.stringYAML()
|
return img.stringYAML()
|
||||||
default:
|
default:
|
||||||
return "", ErrInvalidOutputFormat
|
return "", ErrInvalidOutputFormat
|
||||||
|
@ -1283,6 +1554,42 @@ func getCVETableWriter(writer io.Writer) *tablewriter.Table {
|
||||||
return table
|
return table
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getReferrersTableWriter(writer io.Writer) *tablewriter.Table {
|
||||||
|
table := tablewriter.NewWriter(writer)
|
||||||
|
|
||||||
|
table.SetAutoWrapText(false)
|
||||||
|
table.SetAutoFormatHeaders(true)
|
||||||
|
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
|
||||||
|
table.SetAlignment(tablewriter.ALIGN_LEFT)
|
||||||
|
table.SetCenterSeparator("")
|
||||||
|
table.SetColumnSeparator("")
|
||||||
|
table.SetRowSeparator("")
|
||||||
|
table.SetHeaderLine(false)
|
||||||
|
table.SetBorder(false)
|
||||||
|
table.SetTablePadding(" ")
|
||||||
|
table.SetNoWhiteSpace(true)
|
||||||
|
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRepoTableWriter(writer io.Writer) *tablewriter.Table {
|
||||||
|
table := tablewriter.NewWriter(writer)
|
||||||
|
|
||||||
|
table.SetAutoWrapText(false)
|
||||||
|
table.SetAutoFormatHeaders(true)
|
||||||
|
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
|
||||||
|
table.SetAlignment(tablewriter.ALIGN_LEFT)
|
||||||
|
table.SetCenterSeparator("")
|
||||||
|
table.SetColumnSeparator("")
|
||||||
|
table.SetRowSeparator("")
|
||||||
|
table.SetHeaderLine(false)
|
||||||
|
table.SetBorder(false)
|
||||||
|
table.SetTablePadding(" ")
|
||||||
|
table.SetNoWhiteSpace(true)
|
||||||
|
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
|
||||||
func (service searchService) getRepos(ctx context.Context, config searchConfig, username, password string,
|
func (service searchService) getRepos(ctx context.Context, config searchConfig, username, password string,
|
||||||
rch chan stringResult, wtgrp *sync.WaitGroup,
|
rch chan stringResult, wtgrp *sync.WaitGroup,
|
||||||
) {
|
) {
|
||||||
|
@ -1321,15 +1628,18 @@ func (service searchService) getRepos(ctx context.Context, config searchConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
imageNameWidth = 10
|
imageNameWidth = 10
|
||||||
tagWidth = 8
|
tagWidth = 8
|
||||||
digestWidth = 8
|
digestWidth = 8
|
||||||
platformWidth = 14
|
platformWidth = 14
|
||||||
sizeWidth = 8
|
sizeWidth = 10
|
||||||
isSignedWidth = 8
|
isSignedWidth = 8
|
||||||
configWidth = 8
|
dounloadsWidth = 10
|
||||||
layersWidth = 8
|
signedWidth = 10
|
||||||
ellipsis = "..."
|
lastUpdatedWidth = 14
|
||||||
|
configWidth = 8
|
||||||
|
layersWidth = 8
|
||||||
|
ellipsis = "..."
|
||||||
|
|
||||||
cveIDWidth = 16
|
cveIDWidth = 16
|
||||||
cveSeverityWidth = 8
|
cveSeverityWidth = 8
|
||||||
|
@ -1354,3 +1664,22 @@ const (
|
||||||
|
|
||||||
rowWidth
|
rowWidth
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
repoNameIndex = iota
|
||||||
|
repoSizeIndex
|
||||||
|
repoLastUpdatedIndex
|
||||||
|
repoDownloadsIndex
|
||||||
|
repoStarsIndex
|
||||||
|
repoPlatformsIndex
|
||||||
|
|
||||||
|
repoRowWidth
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
refArtifactTypeIndex = iota
|
||||||
|
refSizeIndex
|
||||||
|
refDigestIndex
|
||||||
|
|
||||||
|
refRowWidth
|
||||||
|
)
|
||||||
|
|
|
@ -15,15 +15,16 @@ type RepoInfo struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type RepoSummary struct {
|
type RepoSummary struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
LastUpdated time.Time `json:"lastUpdated"`
|
LastUpdated time.Time `json:"lastUpdated"`
|
||||||
Size string `json:"size"`
|
Size string `json:"size"`
|
||||||
Platforms []Platform `json:"platforms"`
|
Platforms []Platform `json:"platforms"`
|
||||||
Vendors []string `json:"vendors"`
|
Vendors []string `json:"vendors"`
|
||||||
IsStarred bool `json:"isStarred"`
|
IsStarred bool `json:"isStarred"`
|
||||||
IsBookmarked bool `json:"isBookmarked"`
|
IsBookmarked bool `json:"isBookmarked"`
|
||||||
StarCount int `json:"starCount"`
|
StarCount int `json:"starCount"`
|
||||||
NewestImage ImageSummary `json:"newestImage"`
|
DownloadCount int `json:"downloadCount"`
|
||||||
|
NewestImage ImageSummary `json:"newestImage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PaginatedImagesResult struct {
|
type PaginatedImagesResult struct {
|
||||||
|
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
|
||||||
|
zerr "zotregistry.io/zot/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetImageDirAndTag(imageName string) (string, string) {
|
func GetImageDirAndTag(imageName string) (string, string) {
|
||||||
|
@ -76,3 +78,30 @@ func GetImageLastUpdated(imageInfo ispec.Image) time.Time {
|
||||||
|
|
||||||
return *timeStamp
|
return *timeStamp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetRepoRefference returns the components of a repoName:tag or repoName@digest string. If the format is wrong
|
||||||
|
// an error is returned.
|
||||||
|
// The returned values have the following meaning:
|
||||||
|
//
|
||||||
|
// - string: repo name
|
||||||
|
//
|
||||||
|
// - string: reference (tag or digest)
|
||||||
|
//
|
||||||
|
// - bool: value for the statement: "the reference is a tag"
|
||||||
|
//
|
||||||
|
// - error: error value.
|
||||||
|
func GetRepoRefference(repo string) (string, string, bool, error) {
|
||||||
|
repoName, digest, found := strings.Cut(repo, "@")
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
repoName, tag, found := strings.Cut(repo, ":")
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return "", "", false, zerr.ErrInvalidRepoTagFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
return repoName, tag, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return repoName, digest, false, nil
|
||||||
|
}
|
||||||
|
|
|
@ -60,45 +60,25 @@ func ParseRepo(repo string, repoDB RepoDB, storeController storage.StoreControll
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = resetRepoMetaTags(repo, repoDB, log)
|
err = resetRepoMeta(repo, repoDB, log)
|
||||||
if err != nil && !errors.Is(err, zerr.ErrRepoMetaNotFound) {
|
if err != nil && !errors.Is(err, zerr.ErrRepoMetaNotFound) {
|
||||||
log.Error().Err(err).Str("repository", repo).Msg("load-repo: failed to reset tag field in RepoMetadata for repo")
|
log.Error().Err(err).Str("repository", repo).Msg("load-repo: failed to reset tag field in RepoMetadata for repo")
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, manifest := range indexContent.Manifests {
|
for _, descriptor := range indexContent.Manifests {
|
||||||
tag, hasTag := manifest.Annotations[ispec.AnnotationRefName]
|
tag := descriptor.Annotations[ispec.AnnotationRefName]
|
||||||
|
|
||||||
manifestMetaIsPresent, err := isManifestMetaPresent(repo, manifest, repoDB)
|
descriptorBlob, err := getCachedBlob(repo, descriptor, repoDB, imageStore, log)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("load-repo: error checking manifestMeta in RepoDB")
|
log.Error().Err(err).Msg("load-repo: error checking manifestMeta in RepoDB")
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// this check helps reduce unecesary reads from storage
|
|
||||||
if manifestMetaIsPresent && hasTag {
|
|
||||||
err = repoDB.SetRepoReference(repo, tag, manifest.Digest, manifest.MediaType)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Str("repository", repo).Str("tag", tag).Msg("load-repo: failed to set repo tag")
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
manifestBlob, digest, _, err := imageStore.GetImageManifest(repo, manifest.Digest.String())
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Str("repository", repo).Str("tag", tag).
|
|
||||||
Msg("load-repo: failed to set repo tag for image")
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
isSignature, signatureType, signedManifestDigest, err := storage.CheckIsImageSignature(repo,
|
isSignature, signatureType, signedManifestDigest, err := storage.CheckIsImageSignature(repo,
|
||||||
manifestBlob, tag)
|
descriptorBlob, tag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Str("repository", repo).Str("tag", tag).
|
log.Error().Err(err).Str("repository", repo).Str("tag", tag).
|
||||||
Msg("load-repo: failed checking if image is signature for specified image")
|
Msg("load-repo: failed checking if image is signature for specified image")
|
||||||
|
@ -107,8 +87,8 @@ func ParseRepo(repo string, repoDB RepoDB, storeController storage.StoreControll
|
||||||
}
|
}
|
||||||
|
|
||||||
if isSignature {
|
if isSignature {
|
||||||
layers, err := GetSignatureLayersInfo(repo, tag, manifest.Digest.String(), signatureType, manifestBlob,
|
layers, err := GetSignatureLayersInfo(repo, tag, descriptor.Digest.String(), signatureType,
|
||||||
imageStore, log)
|
descriptorBlob, imageStore, log)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -116,7 +96,7 @@ func ParseRepo(repo string, repoDB RepoDB, storeController storage.StoreControll
|
||||||
err = repoDB.AddManifestSignature(repo, signedManifestDigest,
|
err = repoDB.AddManifestSignature(repo, signedManifestDigest,
|
||||||
SignatureMetadata{
|
SignatureMetadata{
|
||||||
SignatureType: signatureType,
|
SignatureType: signatureType,
|
||||||
SignatureDigest: digest.String(),
|
SignatureDigest: descriptor.Digest.String(),
|
||||||
LayersInfo: layers,
|
LayersInfo: layers,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -141,10 +121,10 @@ func ParseRepo(repo string, repoDB RepoDB, storeController storage.StoreControll
|
||||||
reference := tag
|
reference := tag
|
||||||
|
|
||||||
if tag == "" {
|
if tag == "" {
|
||||||
reference = manifest.Digest.String()
|
reference = descriptor.Digest.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
err = SetImageMetaFromInput(repo, reference, manifest.MediaType, manifest.Digest, manifestBlob,
|
err = SetImageMetaFromInput(repo, reference, descriptor.MediaType, descriptor.Digest, descriptorBlob,
|
||||||
imageStore, repoDB, log)
|
imageStore, repoDB, log)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Str("repository", repo).Str("tag", tag).
|
log.Error().Err(err).Str("repository", repo).Str("tag", tag).
|
||||||
|
@ -157,8 +137,9 @@ func ParseRepo(repo string, repoDB RepoDB, storeController storage.StoreControll
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// resetRepoMetaTags will delete all tags from a repometadata.
|
// resetRepoMeta will delete all tags and non-user related information from a RepoMetadata.
|
||||||
func resetRepoMetaTags(repo string, repoDB RepoDB, log log.Logger) error {
|
// It is used to recalculate and keep RepoDB consistent with the layout in case of unexpected changes.
|
||||||
|
func resetRepoMeta(repo string, repoDB RepoDB, log log.Logger) error {
|
||||||
repoMeta, err := repoDB.GetRepoMeta(repo)
|
repoMeta, err := repoDB.GetRepoMeta(repo)
|
||||||
if err != nil && !errors.Is(err, zerr.ErrRepoMetaNotFound) {
|
if err != nil && !errors.Is(err, zerr.ErrRepoMetaNotFound) {
|
||||||
log.Error().Err(err).Str("repository", repo).Msg("load-repo: failed to get RepoMeta for repo")
|
log.Error().Err(err).Str("repository", repo).Msg("load-repo: failed to get RepoMeta for repo")
|
||||||
|
@ -202,18 +183,41 @@ func getAllRepos(storeController storage.StoreController) ([]string, error) {
|
||||||
return allRepos, nil
|
return allRepos, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// isManifestMetaPresent checks if the manifest with a certain digest is present in a certain repo.
|
func getCachedBlob(repo string, descriptor ispec.Descriptor, repoDB RepoDB,
|
||||||
func isManifestMetaPresent(repo string, manifest ispec.Descriptor, repoDB RepoDB) (bool, error) {
|
imageStore storageTypes.ImageStore, log log.Logger,
|
||||||
_, err := repoDB.GetManifestMeta(repo, manifest.Digest)
|
) ([]byte, error) {
|
||||||
if err != nil && !errors.Is(err, zerr.ErrManifestMetaNotFound) {
|
digest := descriptor.Digest
|
||||||
return false, err
|
|
||||||
|
descriptorBlob, err := getCachedBlobFromRepoDB(descriptor, repoDB)
|
||||||
|
|
||||||
|
if err != nil || len(descriptorBlob) == 0 {
|
||||||
|
descriptorBlob, _, _, err = imageStore.GetImageManifest(repo, digest.String())
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("repository", repo).Str("digest", digest.String()).
|
||||||
|
Msg("load-repo: failed to get blob for image")
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return descriptorBlob, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if errors.Is(err, zerr.ErrManifestMetaNotFound) {
|
return descriptorBlob, nil
|
||||||
return false, nil
|
}
|
||||||
|
|
||||||
|
func getCachedBlobFromRepoDB(descriptor ispec.Descriptor, repoDB RepoDB) ([]byte, error) {
|
||||||
|
switch descriptor.MediaType {
|
||||||
|
case ispec.MediaTypeImageManifest:
|
||||||
|
manifestData, err := repoDB.GetManifestData(descriptor.Digest)
|
||||||
|
|
||||||
|
return manifestData.ManifestBlob, err
|
||||||
|
case ispec.MediaTypeImageIndex:
|
||||||
|
indexData, err := repoDB.GetIndexData(descriptor.Digest)
|
||||||
|
|
||||||
|
return indexData.IndexBlob, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetSignatureLayersInfo(repo, tag, manifestDigest, signatureType string, manifestBlob []byte,
|
func GetSignatureLayersInfo(repo, tag, manifestDigest, signatureType string, manifestBlob []byte,
|
||||||
|
|
|
@ -799,7 +799,7 @@ func GetImageWithSubject(subjectDigest godigest.Digest, mediaType string) (Image
|
||||||
MediaType: mediaType,
|
MediaType: mediaType,
|
||||||
}
|
}
|
||||||
|
|
||||||
manifestBlob, err := json.Marshal(manifest)
|
blob, err := json.Marshal(manifest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Image{}, err
|
return Image{}, err
|
||||||
}
|
}
|
||||||
|
@ -808,7 +808,7 @@ func GetImageWithSubject(subjectDigest godigest.Digest, mediaType string) (Image
|
||||||
Manifest: manifest,
|
Manifest: manifest,
|
||||||
Config: conf,
|
Config: conf,
|
||||||
Layers: layers,
|
Layers: layers,
|
||||||
Reference: godigest.FromBytes(manifestBlob).String(),
|
Reference: godigest.FromBytes(blob).String(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -850,7 +850,8 @@ func UploadImage(img Image, baseURL, repo string) error {
|
||||||
|
|
||||||
cdigest := godigest.FromBytes(cblob)
|
cdigest := godigest.FromBytes(cblob)
|
||||||
|
|
||||||
if img.Manifest.Config.MediaType == ispec.MediaTypeEmptyJSON {
|
if img.Manifest.Config.MediaType == ispec.MediaTypeEmptyJSON ||
|
||||||
|
img.Manifest.Config.Digest == ispec.DescriptorEmptyJSON.Digest {
|
||||||
cblob = ispec.DescriptorEmptyJSON.Data
|
cblob = ispec.DescriptorEmptyJSON.Data
|
||||||
cdigest = ispec.DescriptorEmptyJSON.Digest
|
cdigest = ispec.DescriptorEmptyJSON.Digest
|
||||||
}
|
}
|
||||||
|
@ -888,6 +889,10 @@ func UploadImage(img Image, baseURL, repo string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if img.Reference == "" {
|
||||||
|
img.Reference = godigest.FromBytes(manifestBlob).String()
|
||||||
|
}
|
||||||
|
|
||||||
resp, err = resty.R().
|
resp, err = resty.R().
|
||||||
SetHeader("Content-type", ispec.MediaTypeImageManifest).
|
SetHeader("Content-type", ispec.MediaTypeImageManifest).
|
||||||
SetBody(manifestBlob).
|
SetBody(manifestBlob).
|
||||||
|
@ -1559,6 +1564,10 @@ func UploadImageWithBasicAuth(img Image, baseURL, repo, user, password string) e
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if img.Reference == "" {
|
||||||
|
img.Reference = godigest.FromBytes(manifestBlob).String()
|
||||||
|
}
|
||||||
|
|
||||||
_, err = resty.R().
|
_, err = resty.R().
|
||||||
SetBasicAuth(user, password).
|
SetBasicAuth(user, password).
|
||||||
SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json").
|
SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json").
|
||||||
|
|
|
@ -464,12 +464,12 @@ func TestUploadImage(t *testing.T) {
|
||||||
img := test.Image{
|
img := test.Image{
|
||||||
Layers: [][]byte{
|
Layers: [][]byte{
|
||||||
layerBlob,
|
layerBlob,
|
||||||
}, // invalid format that will result in an error
|
},
|
||||||
Config: ispec.Image{},
|
Config: ispec.Image{},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := test.UploadImage(img, baseURL, "test")
|
err := test.UploadImage(img, baseURL, "test")
|
||||||
So(err, ShouldNotBeNil)
|
So(err, ShouldBeNil)
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("Upload image with authentification", t, func() {
|
Convey("Upload image with authentification", t, func() {
|
||||||
|
|
Loading…
Reference in a new issue