mirror of
https://github.com/project-zot/zot.git
synced 2024-12-30 22:34:13 -05:00
Add graphql query for retrieving imgSummary based on repo:tag image id. (#814)
Refactor Image GqlResolver to better suit GetManifest. Changed GetManifest to also return digest. Signed-off-by: Bogdan BIVOLARU <104334+bogdanbiv@users.noreply.github.com>
This commit is contained in:
parent
885f139e0e
commit
67294cc669
12 changed files with 805 additions and 227 deletions
|
@ -18,7 +18,7 @@ var (
|
||||||
ErrBadBlobDigest = errors.New("blob: bad blob digest")
|
ErrBadBlobDigest = errors.New("blob: bad blob digest")
|
||||||
ErrUnknownCode = errors.New("error: unknown error code")
|
ErrUnknownCode = errors.New("error: unknown error code")
|
||||||
ErrBadCACert = errors.New("tls: invalid ca cert")
|
ErrBadCACert = errors.New("tls: invalid ca cert")
|
||||||
ErrBadUser = errors.New("ldap: non-existent user")
|
ErrBadUser = errors.New("auth: non-existent user")
|
||||||
ErrEntriesExceeded = errors.New("ldap: too many entries returned")
|
ErrEntriesExceeded = errors.New("ldap: too many entries returned")
|
||||||
ErrLDAPEmptyPassphrase = errors.New("ldap: empty passphrase")
|
ErrLDAPEmptyPassphrase = errors.New("ldap: empty passphrase")
|
||||||
ErrLDAPBadConn = errors.New("ldap: bad connection")
|
ErrLDAPBadConn = errors.New("ldap: bad connection")
|
||||||
|
@ -32,7 +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")
|
||||||
ErrUnauthorizedAccess = errors.New("cli: 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")
|
||||||
ErrNoURLProvided = errors.New("cli: no URL provided in argument or via config")
|
ErrNoURLProvided = errors.New("cli: no URL provided in argument or via config")
|
||||||
|
@ -58,4 +58,5 @@ var (
|
||||||
ErrBadType = errors.New("core: invalid type")
|
ErrBadType = errors.New("core: invalid type")
|
||||||
ErrParsingHTTPHeader = errors.New("routes: invalid HTTP header")
|
ErrParsingHTTPHeader = errors.New("routes: invalid HTTP header")
|
||||||
ErrBadRange = errors.New("storage: bad range")
|
ErrBadRange = errors.New("storage: bad range")
|
||||||
|
ErrBadLayerCount = errors.New("manifest: layers count doesn't correspond to config history")
|
||||||
)
|
)
|
||||||
|
|
|
@ -61,6 +61,20 @@ func GetRepo(image string) string {
|
||||||
return image
|
return image
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetImageDirAndTag(imageName string) (string, string) {
|
||||||
|
var imageDir string
|
||||||
|
|
||||||
|
var imageTag string
|
||||||
|
|
||||||
|
if strings.Contains(imageName, ":") {
|
||||||
|
imageDir, imageTag, _ = strings.Cut(imageName, ":")
|
||||||
|
} else {
|
||||||
|
imageDir = imageName
|
||||||
|
}
|
||||||
|
|
||||||
|
return imageDir, imageTag
|
||||||
|
}
|
||||||
|
|
||||||
func GetFixedTags(allTags, vulnerableTags []TagInfo) []TagInfo {
|
func GetFixedTags(allTags, vulnerableTags []TagInfo) []TagInfo {
|
||||||
sort.Slice(allTags, func(i, j int) bool {
|
sort.Slice(allTags, func(i, j int) bool {
|
||||||
return allTags[i].Timestamp.Before(allTags[j].Timestamp)
|
return allTags[i].Timestamp.Before(allTags[j].Timestamp)
|
||||||
|
|
|
@ -66,7 +66,7 @@ type ImageListResponse struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageList struct {
|
type ImageList struct {
|
||||||
SummaryList []ImageSummary `json:"imageList"`
|
SummaryList []common.ImageSummary `json:"imageList"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpandedRepoInfoResp struct {
|
type ExpandedRepoInfoResp struct {
|
||||||
|
@ -83,62 +83,9 @@ type GlobalSearchResult struct {
|
||||||
GlobalSearch GlobalSearch `json:"globalSearch"`
|
GlobalSearch GlobalSearch `json:"globalSearch"`
|
||||||
}
|
}
|
||||||
type GlobalSearch struct {
|
type GlobalSearch struct {
|
||||||
Images []ImageSummary `json:"images"`
|
Images []common.ImageSummary `json:"images"`
|
||||||
Repos []RepoSummary `json:"repos"`
|
Repos []common.RepoSummary `json:"repos"`
|
||||||
Layers []LayerSummary `json:"layers"`
|
Layers []common.LayerSummary `json:"layers"`
|
||||||
}
|
|
||||||
|
|
||||||
type ImageSummary struct {
|
|
||||||
RepoName string `json:"repoName"`
|
|
||||||
Tag string `json:"tag"`
|
|
||||||
LastUpdated time.Time `json:"lastUpdated"`
|
|
||||||
Size string `json:"size"`
|
|
||||||
Platform OsArch `json:"platform"`
|
|
||||||
Vendor string `json:"vendor"`
|
|
||||||
Score int `json:"score"`
|
|
||||||
IsSigned bool `json:"isSigned"`
|
|
||||||
History []LayerHistory `json:"history"`
|
|
||||||
Layers []LayerSummary `json:"layers"`
|
|
||||||
Vulnerabilities ImageVulnerabilitySummary `json:"vulnerabilities"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LayerHistory struct {
|
|
||||||
Layer LayerSummary `json:"layer"`
|
|
||||||
HistoryDescription HistoryDescription `json:"historyDescription"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HistoryDescription struct {
|
|
||||||
Created time.Time `json:"created"`
|
|
||||||
CreatedBy string `json:"createdBy"`
|
|
||||||
Author string `json:"author"`
|
|
||||||
Comment string `json:"comment"`
|
|
||||||
EmptyLayer bool `json:"emptyLayer"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ImageVulnerabilitySummary struct {
|
|
||||||
MaxSeverity string `json:"maxSeverity"`
|
|
||||||
Count int `json:"count"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RepoSummary struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
LastUpdated time.Time `json:"lastUpdated"`
|
|
||||||
Size string `json:"size"`
|
|
||||||
Platforms []OsArch `json:"platforms"`
|
|
||||||
Vendors []string `json:"vendors"`
|
|
||||||
Score int `json:"score"`
|
|
||||||
NewestImage ImageSummary `json:"newestImage"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LayerSummary struct {
|
|
||||||
Size string `json:"size"`
|
|
||||||
Digest string `json:"digest"`
|
|
||||||
Score int `json:"score"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OsArch struct {
|
|
||||||
Os string `json:"os"`
|
|
||||||
Arch string `json:"arch"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpandedRepoInfo struct {
|
type ExpandedRepoInfo struct {
|
||||||
|
@ -147,7 +94,7 @@ type ExpandedRepoInfo struct {
|
||||||
|
|
||||||
//nolint:tagliatelle // graphQL schema
|
//nolint:tagliatelle // graphQL schema
|
||||||
type RepoListWithNewestImage struct {
|
type RepoListWithNewestImage struct {
|
||||||
Repos []RepoSummary `json:"RepoListWithNewestImage"`
|
Repos []common.RepoSummary `json:"RepoListWithNewestImage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrorGQL struct {
|
type ErrorGQL struct {
|
||||||
|
@ -155,15 +102,12 @@ type ErrorGQL struct {
|
||||||
Path []string `json:"path"`
|
Path []string `json:"path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageInfo struct {
|
type SingleImageSummary struct {
|
||||||
RepoName string
|
ImageSummary common.ImageSummary `json:"Image"` //nolint:tagliatelle
|
||||||
Tag string
|
}
|
||||||
LastUpdated time.Time
|
type ImageSummaryResult struct {
|
||||||
Description string
|
SingleImageSummary SingleImageSummary `json:"data"`
|
||||||
Licenses string
|
Errors []ErrorGQL `json:"errors"`
|
||||||
Vendor string
|
|
||||||
Size string
|
|
||||||
Labels string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSetup(t *testing.T, subpath string) error {
|
func testSetup(t *testing.T, subpath string) error {
|
||||||
|
@ -1202,7 +1146,7 @@ func TestDerivedImageList(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
repoName := "test-repo"
|
repoName := "test-repo" //nolint:goconst
|
||||||
|
|
||||||
err = UploadImage(
|
err = UploadImage(
|
||||||
Image{
|
Image{
|
||||||
|
@ -1245,7 +1189,7 @@ func TestDerivedImageList(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
repoName = "same-layers"
|
repoName = "same-layers" //nolint:goconst
|
||||||
|
|
||||||
err = UploadImage(
|
err = UploadImage(
|
||||||
Image{
|
Image{
|
||||||
|
@ -1378,7 +1322,7 @@ func TestDerivedImageList(t *testing.T) {
|
||||||
|
|
||||||
resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query))
|
resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query))
|
||||||
So(resp, ShouldNotBeNil)
|
So(resp, ShouldNotBeNil)
|
||||||
So(strings.Contains(string(resp.Body()), "same-layers"), ShouldBeTrue)
|
So(strings.Contains(string(resp.Body()), "same-layers"), ShouldBeTrue) //nolint:goconst
|
||||||
So(strings.Contains(string(resp.Body()), "missing-layers"), ShouldBeFalse)
|
So(strings.Contains(string(resp.Body()), "missing-layers"), ShouldBeFalse)
|
||||||
So(strings.Contains(string(resp.Body()), "more-layers"), ShouldBeTrue)
|
So(strings.Contains(string(resp.Body()), "more-layers"), ShouldBeTrue)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
@ -1497,7 +1441,7 @@ func TestGetImageManifest(t *testing.T) {
|
||||||
}
|
}
|
||||||
olu := common.NewBaseOciLayoutUtils(storeController, log.NewLogger("debug", ""))
|
olu := common.NewBaseOciLayoutUtils(storeController, log.NewLogger("debug", ""))
|
||||||
|
|
||||||
_, err := olu.GetImageManifest("nonexistent-repo", "latest")
|
_, _, err := olu.GetImageManifest("nonexistent-repo", "latest")
|
||||||
So(err, ShouldNotBeNil)
|
So(err, ShouldNotBeNil)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1513,7 +1457,7 @@ func TestGetImageManifest(t *testing.T) {
|
||||||
}
|
}
|
||||||
olu := common.NewBaseOciLayoutUtils(storeController, log.NewLogger("debug", ""))
|
olu := common.NewBaseOciLayoutUtils(storeController, log.NewLogger("debug", ""))
|
||||||
|
|
||||||
_, err := olu.GetImageManifest("test-repo", "latest")
|
_, _, err := olu.GetImageManifest("test-repo", "latest") //nolint:goconst
|
||||||
So(err, ShouldNotBeNil)
|
So(err, ShouldNotBeNil)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1623,7 +1567,7 @@ func TestBaseImageList(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
repoName := "test-repo"
|
repoName := "test-repo" //nolint:goconst
|
||||||
|
|
||||||
err = UploadImage(
|
err = UploadImage(
|
||||||
Image{
|
Image{
|
||||||
|
@ -1671,7 +1615,7 @@ func TestBaseImageList(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
repoName = "same-layers"
|
repoName = "same-layers" //nolint:goconst
|
||||||
|
|
||||||
err = UploadImage(
|
err = UploadImage(
|
||||||
Image{
|
Image{
|
||||||
|
@ -1890,12 +1834,12 @@ func TestBaseImageList(t *testing.T) {
|
||||||
|
|
||||||
resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query))
|
resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query))
|
||||||
So(resp, ShouldNotBeNil)
|
So(resp, ShouldNotBeNil)
|
||||||
So(strings.Contains(string(resp.Body()), "same-layers"), ShouldBeTrue)
|
So(strings.Contains(string(resp.Body()), "same-layers"), ShouldBeTrue) //nolint:goconst
|
||||||
So(strings.Contains(string(resp.Body()), "less-layers"), ShouldBeTrue)
|
So(strings.Contains(string(resp.Body()), "less-layers"), ShouldBeTrue)
|
||||||
So(strings.Contains(string(resp.Body()), "less-layers-false"), ShouldBeFalse)
|
So(strings.Contains(string(resp.Body()), "less-layers-false"), ShouldBeFalse)
|
||||||
So(strings.Contains(string(resp.Body()), "more-layers"), ShouldBeFalse)
|
So(strings.Contains(string(resp.Body()), "more-layers"), ShouldBeFalse)
|
||||||
So(strings.Contains(string(resp.Body()), "diff-layers"), ShouldBeFalse)
|
So(strings.Contains(string(resp.Body()), "diff-layers"), ShouldBeFalse)
|
||||||
So(strings.Contains(string(resp.Body()), "test-repo"), ShouldBeTrue) // should not list given image
|
So(strings.Contains(string(resp.Body()), "test-repo"), ShouldBeTrue) //nolint:goconst // should not list given image
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(resp.StatusCode(), ShouldEqual, 200)
|
So(resp.StatusCode(), ShouldEqual, 200)
|
||||||
})
|
})
|
||||||
|
@ -2172,7 +2116,7 @@ func TestGlobalSearch(t *testing.T) {
|
||||||
t.Logf("returned layers: %v", responseStruct.GlobalSearchResult.GlobalSearch.Layers)
|
t.Logf("returned layers: %v", responseStruct.GlobalSearchResult.GlobalSearch.Layers)
|
||||||
So(len(responseStruct.GlobalSearchResult.GlobalSearch.Layers), ShouldNotBeEmpty)
|
So(len(responseStruct.GlobalSearchResult.GlobalSearch.Layers), ShouldNotBeEmpty)
|
||||||
|
|
||||||
newestImageMap := make(map[string]ImageSummary)
|
newestImageMap := make(map[string]common.ImageSummary)
|
||||||
for _, image := range responseStruct.GlobalSearchResult.GlobalSearch.Images {
|
for _, image := range responseStruct.GlobalSearchResult.GlobalSearch.Images {
|
||||||
// Make sure all returned results are supposed to be in the repo
|
// Make sure all returned results are supposed to be in the repo
|
||||||
So(allExpectedTagMap[image.RepoName], ShouldContain, image.Tag)
|
So(allExpectedTagMap[image.RepoName], ShouldContain, image.Tag)
|
||||||
|
@ -2395,7 +2339,7 @@ func TestGlobalSearch(t *testing.T) {
|
||||||
t.Logf("returned layers: %v", responseStruct.GlobalSearchResult.GlobalSearch.Layers)
|
t.Logf("returned layers: %v", responseStruct.GlobalSearchResult.GlobalSearch.Layers)
|
||||||
So(len(responseStruct.GlobalSearchResult.GlobalSearch.Layers), ShouldNotBeEmpty)
|
So(len(responseStruct.GlobalSearchResult.GlobalSearch.Layers), ShouldNotBeEmpty)
|
||||||
|
|
||||||
newestImageMap := make(map[string]ImageSummary)
|
newestImageMap := make(map[string]common.ImageSummary)
|
||||||
for _, image := range responseStruct.GlobalSearchResult.GlobalSearch.Images {
|
for _, image := range responseStruct.GlobalSearchResult.GlobalSearch.Images {
|
||||||
// Make sure all returned results are supposed to be in the repo
|
// Make sure all returned results are supposed to be in the repo
|
||||||
So(allExpectedTagMap[image.RepoName], ShouldContain, image.Tag)
|
So(allExpectedTagMap[image.RepoName], ShouldContain, image.Tag)
|
||||||
|
@ -2740,7 +2684,10 @@ func TestBuildImageInfo(t *testing.T) {
|
||||||
imageConfig, err := olu.GetImageConfigInfo(invalid, manifestDigest)
|
imageConfig, err := olu.GetImageConfigInfo(invalid, manifestDigest)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
imageSummary := search.BuildImageInfo(invalid, invalid, manifestDigest, manifest, imageConfig)
|
isSigned := false
|
||||||
|
|
||||||
|
imageSummary := search.BuildImageInfo(invalid, invalid, manifestDigest, manifest,
|
||||||
|
imageConfig, isSigned)
|
||||||
|
|
||||||
So(len(imageSummary.Layers), ShouldEqual, len(manifest.Layers))
|
So(len(imageSummary.Layers), ShouldEqual, len(manifest.Layers))
|
||||||
imageSummaryLayerSize, err := strconv.Atoi(*imageSummary.Size)
|
imageSummaryLayerSize, err := strconv.Atoi(*imageSummary.Size)
|
||||||
|
@ -2943,6 +2890,140 @@ func TestSearchSize(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestImageSummary(t *testing.T) {
|
||||||
|
Convey("GraphQL query ImageSummary", t, func() {
|
||||||
|
port := GetFreePort()
|
||||||
|
baseURL := GetBaseURL(port)
|
||||||
|
conf := config.New()
|
||||||
|
conf.HTTP.Port = port
|
||||||
|
conf.Storage.RootDirectory = t.TempDir()
|
||||||
|
|
||||||
|
defaultVal := true
|
||||||
|
conf.Extensions = &extconf.ExtensionConfig{
|
||||||
|
Search: &extconf.SearchConfig{Enable: &defaultVal},
|
||||||
|
}
|
||||||
|
|
||||||
|
conf.Extensions.Search.CVE = nil
|
||||||
|
|
||||||
|
ctlr := api.NewController(conf)
|
||||||
|
|
||||||
|
gqlQuery := `
|
||||||
|
{
|
||||||
|
Image(image:"%s:%s"){
|
||||||
|
RepoName,
|
||||||
|
Tag,
|
||||||
|
Digest,
|
||||||
|
ConfigDigest,
|
||||||
|
LastUpdated,
|
||||||
|
IsSigned,
|
||||||
|
Size
|
||||||
|
Layers { Digest Size }
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
gqlEndpoint := fmt.Sprintf("%s%s?query=", baseURL, graphqlQueryPrefix)
|
||||||
|
config, layers, manifest, err := GetImageComponents(100)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
configBlob, errConfig := json.Marshal(config)
|
||||||
|
configDigest := digest.FromBytes(configBlob)
|
||||||
|
So(errConfig, ShouldBeNil) // marshall success, config is valid JSON
|
||||||
|
go startServer(ctlr)
|
||||||
|
defer stopServer(ctlr)
|
||||||
|
WaitTillServerReady(baseURL)
|
||||||
|
|
||||||
|
manifestBlob, errMarsal := json.Marshal(manifest)
|
||||||
|
So(errMarsal, ShouldBeNil)
|
||||||
|
So(manifestBlob, ShouldNotBeNil)
|
||||||
|
manifestDigest := digest.FromBytes(manifestBlob)
|
||||||
|
repoName := "test-repo" //nolint:goconst
|
||||||
|
|
||||||
|
tagTarget := "latest"
|
||||||
|
err = UploadImage(
|
||||||
|
Image{
|
||||||
|
Manifest: manifest,
|
||||||
|
Config: config,
|
||||||
|
Layers: layers,
|
||||||
|
Tag: tagTarget,
|
||||||
|
},
|
||||||
|
baseURL,
|
||||||
|
repoName,
|
||||||
|
)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
var (
|
||||||
|
imgSummaryResponse ImageSummaryResult
|
||||||
|
strQuery string
|
||||||
|
targetURL string
|
||||||
|
resp *resty.Response
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Log("starting Test retrieve image based on image identifier")
|
||||||
|
// gql is parametrized with the repo.
|
||||||
|
strQuery = fmt.Sprintf(gqlQuery, repoName, tagTarget)
|
||||||
|
targetURL = fmt.Sprintf("%s%s", gqlEndpoint, url.QueryEscape(strQuery))
|
||||||
|
|
||||||
|
resp, err = resty.R().Get(targetURL)
|
||||||
|
So(resp, ShouldNotBeNil)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(resp.StatusCode(), ShouldEqual, 200)
|
||||||
|
So(resp.Body(), ShouldNotBeNil)
|
||||||
|
|
||||||
|
err = json.Unmarshal(resp.Body(), &imgSummaryResponse)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(imgSummaryResponse, ShouldNotBeNil)
|
||||||
|
So(imgSummaryResponse.SingleImageSummary, ShouldNotBeNil)
|
||||||
|
So(imgSummaryResponse.SingleImageSummary.ImageSummary, ShouldNotBeNil)
|
||||||
|
|
||||||
|
imgSummary := imgSummaryResponse.SingleImageSummary.ImageSummary
|
||||||
|
So(imgSummary.RepoName, ShouldContainSubstring, repoName)
|
||||||
|
So(imgSummary.ConfigDigest, ShouldContainSubstring, configDigest.Hex())
|
||||||
|
So(imgSummary.Digest, ShouldContainSubstring, manifestDigest.Hex())
|
||||||
|
So(len(imgSummary.Layers), ShouldEqual, 1)
|
||||||
|
So(imgSummary.Layers[0].Digest, ShouldContainSubstring,
|
||||||
|
digest.FromBytes(layers[0]).Hex())
|
||||||
|
|
||||||
|
t.Log("starting Test retrieve duplicated image same layers based on image identifier")
|
||||||
|
// gqlEndpoint
|
||||||
|
strQuery = fmt.Sprintf(gqlQuery, "wrong-repo-does-not-exist", "latest")
|
||||||
|
targetURL = fmt.Sprintf("%s%s", gqlEndpoint, url.QueryEscape(strQuery))
|
||||||
|
|
||||||
|
resp, err = resty.R().Get(targetURL)
|
||||||
|
So(resp, ShouldNotBeNil)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(resp.StatusCode(), ShouldEqual, 200)
|
||||||
|
So(resp.Body(), ShouldNotBeNil)
|
||||||
|
err = json.Unmarshal(resp.Body(), &imgSummaryResponse)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(imgSummaryResponse, ShouldNotBeNil)
|
||||||
|
So(imgSummaryResponse.SingleImageSummary, ShouldNotBeNil)
|
||||||
|
So(imgSummaryResponse.SingleImageSummary.ImageSummary, ShouldNotBeNil)
|
||||||
|
|
||||||
|
So(len(imgSummaryResponse.Errors), ShouldEqual, 1)
|
||||||
|
So(imgSummaryResponse.Errors[0].Message,
|
||||||
|
ShouldContainSubstring, "repository: not found")
|
||||||
|
|
||||||
|
t.Log("starting Test retrieve image with bad tag")
|
||||||
|
// gql is parametrized with the repo.
|
||||||
|
strQuery = fmt.Sprintf(gqlQuery, repoName, "nonexisttag")
|
||||||
|
targetURL = fmt.Sprintf("%s%s", gqlEndpoint, url.QueryEscape(strQuery))
|
||||||
|
|
||||||
|
resp, err = resty.R().Get(targetURL)
|
||||||
|
So(resp, ShouldNotBeNil)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(resp.StatusCode(), ShouldEqual, 200)
|
||||||
|
So(resp.Body(), ShouldNotBeNil)
|
||||||
|
err = json.Unmarshal(resp.Body(), &imgSummaryResponse)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(imgSummaryResponse, ShouldNotBeNil)
|
||||||
|
So(imgSummaryResponse.SingleImageSummary, ShouldNotBeNil)
|
||||||
|
So(imgSummaryResponse.SingleImageSummary.ImageSummary, ShouldNotBeNil)
|
||||||
|
|
||||||
|
So(len(imgSummaryResponse.Errors), ShouldEqual, 1)
|
||||||
|
So(imgSummaryResponse.Errors[0].Message,
|
||||||
|
ShouldContainSubstring, "manifest: not found")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func startServer(c *api.Controller) {
|
func startServer(c *api.Controller) {
|
||||||
// this blocks
|
// this blocks
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
72
pkg/extensions/search/common/model.go
Normal file
72
pkg/extensions/search/common/model.go
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RepoInfo struct {
|
||||||
|
Summary RepoSummary
|
||||||
|
ImageSummaries []ImageSummary `json:"images"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RepoSummary struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
LastUpdated time.Time `json:"lastUpdated"`
|
||||||
|
Size string `json:"size"`
|
||||||
|
Platforms []OsArch `json:"platforms"`
|
||||||
|
Vendors []string `json:"vendors"`
|
||||||
|
Score int `json:"score"`
|
||||||
|
NewestImage ImageSummary `json:"newestImage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageSummary struct {
|
||||||
|
RepoName string `json:"repoName"`
|
||||||
|
Tag string `json:"tag"`
|
||||||
|
Digest string `json:"digest"`
|
||||||
|
ConfigDigest string `json:"configDigest"`
|
||||||
|
LastUpdated time.Time `json:"lastUpdated"`
|
||||||
|
IsSigned bool `json:"isSigned"`
|
||||||
|
Size string `json:"size"`
|
||||||
|
Platform OsArch `json:"platform"`
|
||||||
|
Vendor string `json:"vendor"`
|
||||||
|
Score int `json:"score"`
|
||||||
|
DownloadCount int `json:"downloadCount"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Licenses string `json:"licenses"`
|
||||||
|
Labels string `json:"labels"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
Documentation string `json:"documentation"`
|
||||||
|
History []LayerHistory `json:"history"`
|
||||||
|
Layers []LayerSummary `json:"layers"`
|
||||||
|
Vulnerabilities ImageVulnerabilitySummary `json:"vulnerabilities"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OsArch struct {
|
||||||
|
Os string `json:"os"`
|
||||||
|
Arch string `json:"arch"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageVulnerabilitySummary struct {
|
||||||
|
MaxSeverity string `json:"maxSeverity"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LayerSummary struct {
|
||||||
|
Size string `json:"size"`
|
||||||
|
Digest string `json:"digest"`
|
||||||
|
Score int `json:"score"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LayerHistory struct {
|
||||||
|
Layer LayerSummary `json:"layer"`
|
||||||
|
HistoryDescription HistoryDescription `json:"historyDescription"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HistoryDescription struct {
|
||||||
|
Created time.Time `json:"created"`
|
||||||
|
CreatedBy string `json:"createdBy"`
|
||||||
|
Author string `json:"author"`
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
EmptyLayer bool `json:"emptyLayer"`
|
||||||
|
}
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
|
@ -20,6 +19,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type OciLayoutUtils interface {
|
type OciLayoutUtils interface {
|
||||||
|
GetImageManifest(repo string, reference string) (ispec.Manifest, string, error)
|
||||||
GetImageManifests(image string) ([]ispec.Descriptor, error)
|
GetImageManifests(image string) ([]ispec.Descriptor, error)
|
||||||
GetImageBlobManifest(imageDir string, digest godigest.Digest) (v1.Manifest, error)
|
GetImageBlobManifest(imageDir string, digest godigest.Digest) (v1.Manifest, error)
|
||||||
GetImageInfo(imageDir string, hash v1.Hash) (ispec.Image, error)
|
GetImageInfo(imageDir string, hash v1.Hash) (ispec.Image, error)
|
||||||
|
@ -40,77 +40,31 @@ type BaseOciLayoutUtils struct {
|
||||||
StoreController storage.StoreController
|
StoreController storage.StoreController
|
||||||
}
|
}
|
||||||
|
|
||||||
type RepoInfo struct {
|
|
||||||
Summary RepoSummary
|
|
||||||
ImageSummaries []ImageSummary `json:"images"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RepoSummary struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
LastUpdated time.Time `json:"lastUpdated"`
|
|
||||||
Size string `json:"size"`
|
|
||||||
Platforms []OsArch `json:"platforms"`
|
|
||||||
Vendors []string `json:"vendors"`
|
|
||||||
Score int `json:"score"`
|
|
||||||
NewestImage ImageSummary `json:"newestImage"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ImageSummary struct {
|
|
||||||
RepoName string `json:"repoName"`
|
|
||||||
Tag string `json:"tag"`
|
|
||||||
Digest string `json:"digest"`
|
|
||||||
ConfigDigest string `json:"configDigest"`
|
|
||||||
LastUpdated time.Time `json:"lastUpdated"`
|
|
||||||
IsSigned bool `json:"isSigned"`
|
|
||||||
Size string `json:"size"`
|
|
||||||
Platform OsArch `json:"platform"`
|
|
||||||
Vendor string `json:"vendor"`
|
|
||||||
Score int `json:"score"`
|
|
||||||
DownloadCount int `json:"downloadCount"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Licenses string `json:"licenses"`
|
|
||||||
Labels string `json:"labels"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Source string `json:"source"`
|
|
||||||
Documentation string `json:"documentation"`
|
|
||||||
Layers []Layer `json:"layers"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OsArch struct {
|
|
||||||
Os string `json:"os"`
|
|
||||||
Arch string `json:"arch"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Layer struct {
|
|
||||||
Size string `json:"size"`
|
|
||||||
Digest string `json:"digest"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBaseOciLayoutUtils initializes a new OciLayoutUtils object.
|
// NewBaseOciLayoutUtils initializes a new OciLayoutUtils object.
|
||||||
func NewBaseOciLayoutUtils(storeController storage.StoreController, log log.Logger) *BaseOciLayoutUtils {
|
func NewBaseOciLayoutUtils(storeController storage.StoreController, log log.Logger) *BaseOciLayoutUtils {
|
||||||
return &BaseOciLayoutUtils{Log: log, StoreController: storeController}
|
return &BaseOciLayoutUtils{Log: log, StoreController: storeController}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (olu BaseOciLayoutUtils) GetImageManifest(repo string, reference string) (ispec.Manifest, error) {
|
func (olu BaseOciLayoutUtils) GetImageManifest(repo string, reference string) (ispec.Manifest, string, error) {
|
||||||
imageStore := olu.StoreController.GetImageStore(repo)
|
imageStore := olu.StoreController.GetImageStore(repo)
|
||||||
|
|
||||||
if reference == "" {
|
if reference == "" {
|
||||||
reference = "latest"
|
reference = "latest"
|
||||||
}
|
}
|
||||||
|
|
||||||
buf, _, _, err := imageStore.GetImageManifest(repo, reference)
|
buf, dig, _, err := imageStore.GetImageManifest(repo, reference)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ispec.Manifest{}, err
|
return ispec.Manifest{}, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var manifest ispec.Manifest
|
var manifest ispec.Manifest
|
||||||
|
|
||||||
err = json.Unmarshal(buf, &manifest)
|
err = json.Unmarshal(buf, &manifest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ispec.Manifest{}, err
|
return ispec.Manifest{}, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return manifest, nil
|
return manifest, dig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provide a list of repositories from all the available image stores.
|
// Provide a list of repositories from all the available image stores.
|
||||||
|
@ -435,10 +389,10 @@ func (olu BaseOciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error)
|
||||||
|
|
||||||
repoPlatforms = append(repoPlatforms, osArch)
|
repoPlatforms = append(repoPlatforms, osArch)
|
||||||
|
|
||||||
layers := make([]Layer, 0)
|
layers := make([]LayerSummary, 0)
|
||||||
|
|
||||||
for _, layer := range manifest.Layers {
|
for _, layer := range manifest.Layers {
|
||||||
layerInfo := Layer{}
|
layerInfo := LayerSummary{}
|
||||||
|
|
||||||
layerInfo.Digest = layer.Digest.Hex
|
layerInfo.Digest = layer.Digest.Hex
|
||||||
|
|
||||||
|
@ -513,17 +467,3 @@ func (olu BaseOciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error)
|
||||||
|
|
||||||
return repo, nil
|
return repo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetImageDirAndTag(imageName string) (string, string) {
|
|
||||||
var imageDir string
|
|
||||||
|
|
||||||
var imageTag string
|
|
||||||
|
|
||||||
if strings.Contains(imageName, ":") {
|
|
||||||
imageDir, imageTag, _ = strings.Cut(imageName, ":")
|
|
||||||
} else {
|
|
||||||
imageDir = imageName
|
|
||||||
}
|
|
||||||
|
|
||||||
return imageDir, imageTag
|
|
||||||
}
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ package digestinfo_test
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -213,10 +214,13 @@ func TestDigestSearchHTTP(t *testing.T) {
|
||||||
|
|
||||||
// Call should return {"data":{"ImageListForDigest":[{"Name":"zot-test","Tags":["0.0.1"]}]}}
|
// Call should return {"data":{"ImageListForDigest":[{"Name":"zot-test","Tags":["0.0.1"]}]}}
|
||||||
// "2bacca16" should match the manifest of 1 image
|
// "2bacca16" should match the manifest of 1 image
|
||||||
resp, err = resty.R().Get(
|
|
||||||
baseURL + constants.ExtSearchPrefix + `?query={ImageListForDigest(id:"2bacca16")` +
|
gqlQuery := url.QueryEscape(`{ImageListForDigest(id:"2bacca16")
|
||||||
`{RepoName%20Tag%20Digest%20ConfigDigest%20Size%20Layers%20{%20Digest}}}`,
|
{RepoName Tag Digest ConfigDigest Size Layers { Digest }}}`)
|
||||||
)
|
targetURL := baseURL + constants.ExtSearchPrefix + `?query=` + gqlQuery
|
||||||
|
|
||||||
|
resp, err = resty.R().Get(targetURL)
|
||||||
|
So(string(resp.Body()), ShouldNotBeNil)
|
||||||
So(resp, ShouldNotBeNil)
|
So(resp, ShouldNotBeNil)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(resp.StatusCode(), ShouldEqual, 200)
|
So(resp.StatusCode(), ShouldEqual, 200)
|
||||||
|
@ -228,11 +232,13 @@ func TestDigestSearchHTTP(t *testing.T) {
|
||||||
So(responseStruct.ImgListForDigest.Images[0].RepoName, ShouldEqual, "zot-test")
|
So(responseStruct.ImgListForDigest.Images[0].RepoName, ShouldEqual, "zot-test")
|
||||||
So(responseStruct.ImgListForDigest.Images[0].Tag, ShouldEqual, "0.0.1")
|
So(responseStruct.ImgListForDigest.Images[0].Tag, ShouldEqual, "0.0.1")
|
||||||
|
|
||||||
// "adf3bb6c" should match the config of 1 image
|
// "adf3bb6c" should match the config of 1 image.
|
||||||
resp, err = resty.R().Get(
|
gqlQuery = url.QueryEscape(`{ImageListForDigest(id:"adf3bb6c")
|
||||||
baseURL + constants.ExtSearchPrefix + `?query={ImageListForDigest(id:"adf3bb6c")` +
|
{RepoName Tag Digest ConfigDigest Size Layers { Digest }}}`)
|
||||||
`{RepoName%20Tag%20Digest%20ConfigDigest%20Size%20Layers%20{%20Digest}}}`,
|
|
||||||
)
|
targetURL = baseURL + constants.ExtSearchPrefix + `?query=` + gqlQuery
|
||||||
|
resp, err = resty.R().Get(targetURL)
|
||||||
|
|
||||||
So(resp, ShouldNotBeNil)
|
So(resp, ShouldNotBeNil)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(resp.StatusCode(), ShouldEqual, 200)
|
So(resp.StatusCode(), ShouldEqual, 200)
|
||||||
|
@ -246,20 +252,25 @@ func TestDigestSearchHTTP(t *testing.T) {
|
||||||
|
|
||||||
// Call should return {"data":{"ImageListForDigest":[{"Name":"zot-cve-test","Tags":["0.0.1"]}]}}
|
// Call should return {"data":{"ImageListForDigest":[{"Name":"zot-cve-test","Tags":["0.0.1"]}]}}
|
||||||
// "7a0437f0" should match the layer of 1 image
|
// "7a0437f0" should match the layer of 1 image
|
||||||
|
gqlQuery = url.QueryEscape(`{ImageListForDigest(id:"7a0437f0")
|
||||||
|
{RepoName Tag Digest ConfigDigest Size Layers { Digest }}}`)
|
||||||
|
targetURL = baseURL + constants.ExtSearchPrefix + `?query=` + gqlQuery
|
||||||
|
|
||||||
resp, err = resty.R().Get(
|
resp, err = resty.R().Get(
|
||||||
baseURL + constants.ExtSearchPrefix + `?query={ImageListForDigest(id:"7a0437f0")` +
|
targetURL,
|
||||||
`{RepoName%20Tag%20Digest%20ConfigDigest%20Size%20Layers%20{%20Digest}}}`,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
So(resp, ShouldNotBeNil)
|
So(resp, ShouldNotBeNil)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(resp.StatusCode(), ShouldEqual, 200)
|
So(resp.StatusCode(), ShouldEqual, 200)
|
||||||
|
var responseStruct2 ImgResponseForDigest
|
||||||
|
|
||||||
err = json.Unmarshal(resp.Body(), &responseStruct)
|
err = json.Unmarshal(resp.Body(), &responseStruct2)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(len(responseStruct.Errors), ShouldEqual, 0)
|
So(len(responseStruct2.Errors), ShouldEqual, 0)
|
||||||
So(len(responseStruct.ImgListForDigest.Images), ShouldEqual, 1)
|
So(len(responseStruct2.ImgListForDigest.Images), ShouldEqual, 1)
|
||||||
So(responseStruct.ImgListForDigest.Images[0].RepoName, ShouldEqual, "zot-cve-test")
|
So(responseStruct2.ImgListForDigest.Images[0].RepoName, ShouldEqual, "zot-cve-test")
|
||||||
So(responseStruct.ImgListForDigest.Images[0].Tag, ShouldEqual, "0.0.1")
|
So(responseStruct2.ImgListForDigest.Images[0].Tag, ShouldEqual, "0.0.1")
|
||||||
|
|
||||||
// Call should return {"data":{"ImageListForDigest":[]}}
|
// Call should return {"data":{"ImageListForDigest":[]}}
|
||||||
// "1111111" should match 0 images
|
// "1111111" should match 0 images
|
||||||
|
|
|
@ -126,6 +126,7 @@ type ComplexityRoot struct {
|
||||||
DerivedImageList func(childComplexity int, image string) int
|
DerivedImageList func(childComplexity int, image string) int
|
||||||
ExpandedRepoInfo func(childComplexity int, repo string) int
|
ExpandedRepoInfo func(childComplexity int, repo string) int
|
||||||
GlobalSearch func(childComplexity int, query string) int
|
GlobalSearch func(childComplexity int, query string) int
|
||||||
|
Image func(childComplexity int, image string) int
|
||||||
ImageList func(childComplexity int, repo string) int
|
ImageList func(childComplexity int, repo string) int
|
||||||
ImageListForCve func(childComplexity int, id string) int
|
ImageListForCve func(childComplexity int, id string) int
|
||||||
ImageListForDigest func(childComplexity int, id string) int
|
ImageListForDigest func(childComplexity int, id string) int
|
||||||
|
@ -163,6 +164,7 @@ type QueryResolver interface {
|
||||||
GlobalSearch(ctx context.Context, query string) (*GlobalSearchResult, error)
|
GlobalSearch(ctx context.Context, query string) (*GlobalSearchResult, error)
|
||||||
DerivedImageList(ctx context.Context, image string) ([]*ImageSummary, error)
|
DerivedImageList(ctx context.Context, image string) ([]*ImageSummary, error)
|
||||||
BaseImageList(ctx context.Context, image string) ([]*ImageSummary, error)
|
BaseImageList(ctx context.Context, image string) ([]*ImageSummary, error)
|
||||||
|
Image(ctx context.Context, image string) (*ImageSummary, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type executableSchema struct {
|
type executableSchema struct {
|
||||||
|
@ -569,6 +571,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||||
|
|
||||||
return e.complexity.Query.GlobalSearch(childComplexity, args["query"].(string)), true
|
return e.complexity.Query.GlobalSearch(childComplexity, args["query"].(string)), true
|
||||||
|
|
||||||
|
case "Query.Image":
|
||||||
|
if e.complexity.Query.Image == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
args, err := ec.field_Query_Image_args(context.TODO(), rawArgs)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.Query.Image(childComplexity, args["image"].(string)), true
|
||||||
|
|
||||||
case "Query.ImageList":
|
case "Query.ImageList":
|
||||||
if e.complexity.Query.ImageList == nil {
|
if e.complexity.Query.ImageList == nil {
|
||||||
break
|
break
|
||||||
|
@ -887,7 +901,9 @@ type Query {
|
||||||
GlobalSearch(query: String!): GlobalSearchResult!
|
GlobalSearch(query: String!): GlobalSearchResult!
|
||||||
DerivedImageList(image: String!): [ImageSummary!]
|
DerivedImageList(image: String!): [ImageSummary!]
|
||||||
BaseImageList(image: String!): [ImageSummary!]
|
BaseImageList(image: String!): [ImageSummary!]
|
||||||
}`, BuiltIn: false},
|
Image(image: String!): ImageSummary
|
||||||
|
}
|
||||||
|
`, BuiltIn: false},
|
||||||
}
|
}
|
||||||
var parsedSchema = gqlparser.MustLoadSchema(sources...)
|
var parsedSchema = gqlparser.MustLoadSchema(sources...)
|
||||||
|
|
||||||
|
@ -1039,6 +1055,21 @@ func (ec *executionContext) field_Query_ImageList_args(ctx context.Context, rawA
|
||||||
return args, nil
|
return args, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) field_Query_Image_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
|
||||||
|
var err error
|
||||||
|
args := map[string]interface{}{}
|
||||||
|
var arg0 string
|
||||||
|
if tmp, ok := rawArgs["image"]; ok {
|
||||||
|
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("image"))
|
||||||
|
arg0, err = ec.unmarshalNString2string(ctx, tmp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
args["image"] = arg0
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
|
func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
|
||||||
var err error
|
var err error
|
||||||
args := map[string]interface{}{}
|
args := map[string]interface{}{}
|
||||||
|
@ -3972,6 +4003,100 @@ func (ec *executionContext) fieldContext_Query_BaseImageList(ctx context.Context
|
||||||
return fc, nil
|
return fc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _Query_Image(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_Query_Image(ctx, field)
|
||||||
|
if err != nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
|
ret = graphql.Null
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return ec.resolvers.Query().Image(rctx, fc.Args["image"].(string))
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(*ImageSummary)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalOImageSummary2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐImageSummary(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_Query_Image(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "Query",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: true,
|
||||||
|
IsResolver: true,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
switch field.Name {
|
||||||
|
case "RepoName":
|
||||||
|
return ec.fieldContext_ImageSummary_RepoName(ctx, field)
|
||||||
|
case "Tag":
|
||||||
|
return ec.fieldContext_ImageSummary_Tag(ctx, field)
|
||||||
|
case "Digest":
|
||||||
|
return ec.fieldContext_ImageSummary_Digest(ctx, field)
|
||||||
|
case "ConfigDigest":
|
||||||
|
return ec.fieldContext_ImageSummary_ConfigDigest(ctx, field)
|
||||||
|
case "LastUpdated":
|
||||||
|
return ec.fieldContext_ImageSummary_LastUpdated(ctx, field)
|
||||||
|
case "IsSigned":
|
||||||
|
return ec.fieldContext_ImageSummary_IsSigned(ctx, field)
|
||||||
|
case "Size":
|
||||||
|
return ec.fieldContext_ImageSummary_Size(ctx, field)
|
||||||
|
case "Platform":
|
||||||
|
return ec.fieldContext_ImageSummary_Platform(ctx, field)
|
||||||
|
case "Vendor":
|
||||||
|
return ec.fieldContext_ImageSummary_Vendor(ctx, field)
|
||||||
|
case "Score":
|
||||||
|
return ec.fieldContext_ImageSummary_Score(ctx, field)
|
||||||
|
case "DownloadCount":
|
||||||
|
return ec.fieldContext_ImageSummary_DownloadCount(ctx, field)
|
||||||
|
case "Layers":
|
||||||
|
return ec.fieldContext_ImageSummary_Layers(ctx, field)
|
||||||
|
case "Description":
|
||||||
|
return ec.fieldContext_ImageSummary_Description(ctx, field)
|
||||||
|
case "Licenses":
|
||||||
|
return ec.fieldContext_ImageSummary_Licenses(ctx, field)
|
||||||
|
case "Labels":
|
||||||
|
return ec.fieldContext_ImageSummary_Labels(ctx, field)
|
||||||
|
case "Title":
|
||||||
|
return ec.fieldContext_ImageSummary_Title(ctx, field)
|
||||||
|
case "Source":
|
||||||
|
return ec.fieldContext_ImageSummary_Source(ctx, field)
|
||||||
|
case "Documentation":
|
||||||
|
return ec.fieldContext_ImageSummary_Documentation(ctx, field)
|
||||||
|
case "History":
|
||||||
|
return ec.fieldContext_ImageSummary_History(ctx, field)
|
||||||
|
case "Vulnerabilities":
|
||||||
|
return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
err = ec.Recover(ctx, r)
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
if fc.Args, err = ec.field_Query_Image_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
fc, err := ec.fieldContext_Query___type(ctx, field)
|
fc, err := ec.fieldContext_Query___type(ctx, field)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -7112,6 +7237,26 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
|
||||||
return ec.OperationContext.RootResolverMiddleware(ctx, innerFunc)
|
return ec.OperationContext.RootResolverMiddleware(ctx, innerFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
out.Concurrently(i, func() graphql.Marshaler {
|
||||||
|
return rrm(innerCtx)
|
||||||
|
})
|
||||||
|
case "Image":
|
||||||
|
field := field
|
||||||
|
|
||||||
|
innerFunc := func(ctx context.Context) (res graphql.Marshaler) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
res = ec._Query_Image(ctx, field)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
rrm := func(ctx context.Context) graphql.Marshaler {
|
||||||
|
return ec.OperationContext.RootResolverMiddleware(ctx, innerFunc)
|
||||||
|
}
|
||||||
|
|
||||||
out.Concurrently(i, func() graphql.Marshaler {
|
out.Concurrently(i, func() graphql.Marshaler {
|
||||||
return rrm(innerCtx)
|
return rrm(innerCtx)
|
||||||
})
|
})
|
||||||
|
|
|
@ -6,11 +6,11 @@ package search
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/99designs/gqlgen/graphql"
|
"github.com/99designs/gqlgen/graphql"
|
||||||
glob "github.com/bmatcuk/doublestar/v4" // nolint:gci
|
glob "github.com/bmatcuk/doublestar/v4" // nolint:gci
|
||||||
|
@ -18,6 +18,7 @@ import (
|
||||||
godigest "github.com/opencontainers/go-digest"
|
godigest "github.com/opencontainers/go-digest"
|
||||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/vektah/gqlparser/v2/gqlerror"
|
"github.com/vektah/gqlparser/v2/gqlerror"
|
||||||
|
"zotregistry.io/zot/errors"
|
||||||
"zotregistry.io/zot/pkg/extensions/search/common"
|
"zotregistry.io/zot/pkg/extensions/search/common"
|
||||||
cveinfo "zotregistry.io/zot/pkg/extensions/search/cve"
|
cveinfo "zotregistry.io/zot/pkg/extensions/search/cve"
|
||||||
digestinfo "zotregistry.io/zot/pkg/extensions/search/digest"
|
digestinfo "zotregistry.io/zot/pkg/extensions/search/digest"
|
||||||
|
@ -35,11 +36,6 @@ type Resolver struct {
|
||||||
log log.Logger
|
log log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
ErrBadCtxFormat = errors.New("type assertion failed")
|
|
||||||
ErrBadLayerCount = errors.New("manifest: layers count doesn't correspond to config history")
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetResolverConfig ...
|
// GetResolverConfig ...
|
||||||
func GetResolverConfig(log log.Logger, storeController storage.StoreController, cveInfo cveinfo.CveInfo,
|
func GetResolverConfig(log log.Logger, storeController storage.StoreController, cveInfo cveinfo.CveInfo,
|
||||||
) gql_generated.Config {
|
) gql_generated.Config {
|
||||||
|
@ -75,7 +71,9 @@ func (r *queryResolver) getImageListForDigest(repoList []string, digest string)
|
||||||
return []*gql_generated.ImageSummary{}, err
|
return []*gql_generated.ImageSummary{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
imageInfo := BuildImageInfo(repo, imageInfo.Tag, imageInfo.Digest, imageInfo.Manifest, imageConfig)
|
isSigned := olu.CheckManifestSignature(repo, imageInfo.Digest)
|
||||||
|
imageInfo := BuildImageInfo(repo, imageInfo.Tag, imageInfo.Digest,
|
||||||
|
imageInfo.Manifest, imageConfig, isSigned)
|
||||||
|
|
||||||
imgResultForDigest = append(imgResultForDigest, imageInfo)
|
imgResultForDigest = append(imgResultForDigest, imageInfo)
|
||||||
}
|
}
|
||||||
|
@ -544,7 +542,9 @@ func (r *queryResolver) getImageList(store storage.ImageStore, imageName string)
|
||||||
return results, err
|
return results, err
|
||||||
}
|
}
|
||||||
|
|
||||||
imageInfo := BuildImageInfo(repo, tag.Name, digest, manifest, imageConfig)
|
isSigned := layoutUtils.CheckManifestSignature(repo, digest)
|
||||||
|
imageInfo := BuildImageInfo(repo, tag.Name, digest, manifest,
|
||||||
|
imageConfig, isSigned)
|
||||||
|
|
||||||
results = append(results, imageInfo)
|
results = append(results, imageInfo)
|
||||||
}
|
}
|
||||||
|
@ -559,16 +559,21 @@ func (r *queryResolver) getImageList(store storage.ImageStore, imageName string)
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildImageInfo(repo string, tag string, manifestDigest godigest.Digest,
|
func BuildImageInfo(repo string, tag string, manifestDigest godigest.Digest,
|
||||||
manifest v1.Manifest, imageConfig ispec.Image,
|
manifest v1.Manifest, imageConfig ispec.Image, isSigned bool,
|
||||||
) *gql_generated.ImageSummary {
|
) *gql_generated.ImageSummary {
|
||||||
layers := []*gql_generated.LayerSummary{}
|
layers := []*gql_generated.LayerSummary{}
|
||||||
size := int64(0)
|
size := int64(0)
|
||||||
|
|
||||||
log := log.NewLogger("debug", "")
|
log := log.NewLogger("debug", "")
|
||||||
|
|
||||||
allHistory := []*gql_generated.LayerHistory{}
|
allHistory := []*gql_generated.LayerHistory{}
|
||||||
|
|
||||||
formattedManifestDigest := manifestDigest.Hex()
|
formattedManifestDigest := manifestDigest.Hex()
|
||||||
|
annotations := common.GetAnnotations(manifest.Annotations, imageConfig.Config.Labels)
|
||||||
|
|
||||||
|
lastUpdated := imageConfig.Created
|
||||||
|
|
||||||
|
if (lastUpdated == nil || *lastUpdated == (time.Time{})) &&
|
||||||
|
len(imageConfig.History) > 0 {
|
||||||
|
lastUpdated = imageConfig.History[len(imageConfig.History)-1].Created
|
||||||
|
}
|
||||||
|
|
||||||
history := imageConfig.History
|
history := imageConfig.History
|
||||||
if len(history) == 0 {
|
if len(history) == 0 {
|
||||||
|
@ -602,7 +607,20 @@ func BuildImageInfo(repo string, tag string, manifestDigest godigest.Digest,
|
||||||
ConfigDigest: &manifest.Config.Digest.Hex,
|
ConfigDigest: &manifest.Config.Digest.Hex,
|
||||||
Size: &formattedSize,
|
Size: &formattedSize,
|
||||||
Layers: layers,
|
Layers: layers,
|
||||||
History: []*gql_generated.LayerHistory{},
|
History: allHistory,
|
||||||
|
Vendor: &annotations.Vendor,
|
||||||
|
Description: &annotations.Description,
|
||||||
|
Title: &annotations.Title,
|
||||||
|
Documentation: &annotations.Documentation,
|
||||||
|
Licenses: &annotations.Licenses,
|
||||||
|
Labels: &annotations.Labels,
|
||||||
|
Source: &annotations.Source,
|
||||||
|
LastUpdated: lastUpdated,
|
||||||
|
IsSigned: &isSigned,
|
||||||
|
Platform: &gql_generated.OsArch{
|
||||||
|
Os: &imageConfig.OS,
|
||||||
|
Arch: &imageConfig.Architecture,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return imageInfo
|
return imageInfo
|
||||||
|
@ -629,7 +647,7 @@ func BuildImageInfo(repo string, tag string, manifestDigest godigest.Digest,
|
||||||
if layersIterator+1 > len(manifest.Layers) {
|
if layersIterator+1 > len(manifest.Layers) {
|
||||||
formattedSize := strconv.FormatInt(size, 10)
|
formattedSize := strconv.FormatInt(size, 10)
|
||||||
|
|
||||||
log.Error().Err(ErrBadLayerCount).Msg("error on creating layer history for ImageSummary")
|
log.Error().Err(errors.ErrBadLayerCount).Msg("error on creating layer history for ImageSummary")
|
||||||
|
|
||||||
return &gql_generated.ImageSummary{
|
return &gql_generated.ImageSummary{
|
||||||
RepoName: &repo,
|
RepoName: &repo,
|
||||||
|
@ -639,6 +657,19 @@ func BuildImageInfo(repo string, tag string, manifestDigest godigest.Digest,
|
||||||
Size: &formattedSize,
|
Size: &formattedSize,
|
||||||
Layers: layers,
|
Layers: layers,
|
||||||
History: allHistory,
|
History: allHistory,
|
||||||
|
Vendor: &annotations.Vendor,
|
||||||
|
Description: &annotations.Description,
|
||||||
|
Title: &annotations.Title,
|
||||||
|
Documentation: &annotations.Documentation,
|
||||||
|
Licenses: &annotations.Licenses,
|
||||||
|
Labels: &annotations.Labels,
|
||||||
|
Source: &annotations.Source,
|
||||||
|
LastUpdated: lastUpdated,
|
||||||
|
IsSigned: &isSigned,
|
||||||
|
Platform: &gql_generated.OsArch{
|
||||||
|
Os: &imageConfig.OS,
|
||||||
|
Arch: &imageConfig.Architecture,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -671,6 +702,19 @@ func BuildImageInfo(repo string, tag string, manifestDigest godigest.Digest,
|
||||||
Size: &formattedSize,
|
Size: &formattedSize,
|
||||||
Layers: layers,
|
Layers: layers,
|
||||||
History: allHistory,
|
History: allHistory,
|
||||||
|
Vendor: &annotations.Vendor,
|
||||||
|
Description: &annotations.Description,
|
||||||
|
Title: &annotations.Title,
|
||||||
|
Documentation: &annotations.Documentation,
|
||||||
|
Licenses: &annotations.Licenses,
|
||||||
|
Labels: &annotations.Labels,
|
||||||
|
Source: &annotations.Source,
|
||||||
|
LastUpdated: lastUpdated,
|
||||||
|
IsSigned: &isSigned,
|
||||||
|
Platform: &gql_generated.OsArch{
|
||||||
|
Os: &imageConfig.OS,
|
||||||
|
Arch: &imageConfig.Architecture,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return imageInfo
|
return imageInfo
|
||||||
|
@ -703,7 +747,7 @@ func userAvailableRepos(ctx context.Context, repoList []string) ([]string, error
|
||||||
if authCtx := ctx.Value(authzCtxKey); authCtx != nil {
|
if authCtx := ctx.Value(authzCtxKey); authCtx != nil {
|
||||||
acCtx, ok := authCtx.(localCtx.AccessControlContext)
|
acCtx, ok := authCtx.(localCtx.AccessControlContext)
|
||||||
if !ok {
|
if !ok {
|
||||||
err := ErrBadCtxFormat
|
err := errors.ErrBadType
|
||||||
|
|
||||||
return []string{}, err
|
return []string{}, err
|
||||||
}
|
}
|
||||||
|
@ -719,3 +763,49 @@ func userAvailableRepos(ctx context.Context, repoList []string) ([]string, error
|
||||||
|
|
||||||
return availableRepos, nil
|
return availableRepos, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func extractImageDetails(
|
||||||
|
ctx context.Context,
|
||||||
|
layoutUtils common.OciLayoutUtils,
|
||||||
|
repo, tag string,
|
||||||
|
log log.Logger) (
|
||||||
|
godigest.Digest, *v1.Manifest, *ispec.Image, error,
|
||||||
|
) {
|
||||||
|
validRepoList, err := userAvailableRepos(ctx, []string{repo})
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("unable to retrieve access token")
|
||||||
|
|
||||||
|
return "", nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(validRepoList) == 0 {
|
||||||
|
log.Error().Err(err).Msg("user is not authorized")
|
||||||
|
|
||||||
|
return "", nil, nil, errors.ErrUnauthorizedAccess
|
||||||
|
}
|
||||||
|
|
||||||
|
_, dig, err := layoutUtils.GetImageManifest(repo, tag)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Could not retrieve image ispec manifest")
|
||||||
|
|
||||||
|
return "", nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
digest := godigest.Digest(dig)
|
||||||
|
|
||||||
|
manifest, err := layoutUtils.GetImageBlobManifest(repo, digest)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Could not retrieve image godigest manifest")
|
||||||
|
|
||||||
|
return "", nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
imageConfig, err := layoutUtils.GetImageConfigInfo(repo, digest)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Could not retrieve image config")
|
||||||
|
|
||||||
|
return "", nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return digest, &manifest, &imageConfig, nil
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package search //nolint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -157,7 +158,7 @@ func TestGlobalSearch(t *testing.T) {
|
||||||
ImageSummaries: []common.ImageSummary{
|
ImageSummaries: []common.ImageSummary{
|
||||||
{
|
{
|
||||||
Tag: "latest",
|
Tag: "latest",
|
||||||
Layers: []common.Layer{
|
Layers: []common.LayerSummary{
|
||||||
{
|
{
|
||||||
Size: "100",
|
Size: "100",
|
||||||
Digest: "sha256:855b1556a45637abf05c63407437f6f305b4627c4361fb965a78e5731999c0c7",
|
Digest: "sha256:855b1556a45637abf05c63407437f6f305b4627c4361fb965a78e5731999c0c7",
|
||||||
|
@ -313,3 +314,197 @@ func TestMatching(t *testing.T) {
|
||||||
So(score, ShouldEqual, 12)
|
So(score, ShouldEqual, 12)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExtractImageDetails(t *testing.T) {
|
||||||
|
Convey("repoListWithNewestImage", t, func() {
|
||||||
|
// log := log.Logger{Logger: zerolog.New(os.Stdout)}
|
||||||
|
content := []byte("this is a blob5")
|
||||||
|
testLogger := log.NewLogger("debug", "")
|
||||||
|
layerDigest := godigest.FromBytes(content)
|
||||||
|
config := ispec.Image{
|
||||||
|
Architecture: "amd64",
|
||||||
|
OS: "linux",
|
||||||
|
RootFS: ispec.RootFS{
|
||||||
|
Type: "layers",
|
||||||
|
DiffIDs: []godigest.Digest{},
|
||||||
|
},
|
||||||
|
Author: "some author",
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.TODO()
|
||||||
|
authzCtxKey := localCtx.GetContextKey()
|
||||||
|
ctx = context.WithValue(ctx, authzCtxKey,
|
||||||
|
localCtx.AccessControlContext{
|
||||||
|
GlobPatterns: map[string]bool{"*": true, "**": true},
|
||||||
|
Username: "jane_doe",
|
||||||
|
})
|
||||||
|
configBlobContent, _ := json.MarshalIndent(&config, "", "\t")
|
||||||
|
configDigest := godigest.FromBytes(configBlobContent)
|
||||||
|
|
||||||
|
localTestManifest := ispec.Manifest{
|
||||||
|
Config: ispec.Descriptor{
|
||||||
|
MediaType: "application/vnd.oci.image.config.v1+json",
|
||||||
|
Digest: configDigest,
|
||||||
|
Size: int64(len(configBlobContent)),
|
||||||
|
},
|
||||||
|
Layers: []ispec.Descriptor{
|
||||||
|
{
|
||||||
|
MediaType: "application/vnd.oci.image.layer.v1.tar",
|
||||||
|
Digest: layerDigest,
|
||||||
|
Size: int64(len(content)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
localTestDigestTry, _ := json.Marshal(localTestManifest)
|
||||||
|
localTestDigest := godigest.FromBytes(localTestDigestTry)
|
||||||
|
localTestManifestV1 := v1.Manifest{
|
||||||
|
Config: v1.Descriptor{
|
||||||
|
Digest: v1.Hash{
|
||||||
|
Algorithm: "sha256",
|
||||||
|
Hex: configDigest.Encoded(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Layers: []v1.Descriptor{
|
||||||
|
{
|
||||||
|
Size: 4,
|
||||||
|
Digest: v1.Hash{
|
||||||
|
Algorithm: "sha256",
|
||||||
|
Hex: layerDigest.Encoded(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Convey("extractImageDetails good workflow", func() {
|
||||||
|
mockOlum := mocks.OciLayoutUtilsMock{
|
||||||
|
GetImageBlobManifestFn: func(imageDir string, digest godigest.Digest) (v1.Manifest, error) {
|
||||||
|
return localTestManifestV1, nil
|
||||||
|
},
|
||||||
|
GetImageConfigInfoFn: func(repo string, digest godigest.Digest) (
|
||||||
|
ispec.Image, error,
|
||||||
|
) {
|
||||||
|
return config, nil
|
||||||
|
},
|
||||||
|
GetImageManifestFn: func(repo string, tag string) (
|
||||||
|
ispec.Manifest, string, error,
|
||||||
|
) {
|
||||||
|
return localTestManifest, localTestDigest.String(), nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resDigest, resManifest, resIspecImage, resErr := extractImageDetails(ctx,
|
||||||
|
mockOlum, "zot-test", "latest", testLogger)
|
||||||
|
So(string(resDigest), ShouldContainSubstring, "sha256:d004018b9f")
|
||||||
|
So(resManifest.Config.Digest.String(), ShouldContainSubstring, configDigest.Encoded())
|
||||||
|
|
||||||
|
So(resIspecImage.Architecture, ShouldContainSubstring, "amd64")
|
||||||
|
So(resErr, ShouldBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("extractImageDetails bad ispec.ImageManifest", func() {
|
||||||
|
mockOlum := mocks.OciLayoutUtilsMock{
|
||||||
|
GetImageBlobManifestFn: func(imageDir string, digest godigest.Digest) (v1.Manifest, error) {
|
||||||
|
return localTestManifestV1, nil
|
||||||
|
},
|
||||||
|
GetImageConfigInfoFn: func(repo string, digest godigest.Digest) (
|
||||||
|
ispec.Image, error,
|
||||||
|
) {
|
||||||
|
return config, nil
|
||||||
|
},
|
||||||
|
GetImageManifestFn: func(repo string, tag string) (
|
||||||
|
ispec.Manifest, string, error,
|
||||||
|
) {
|
||||||
|
// localTestManifest = nil
|
||||||
|
return ispec.Manifest{}, localTestDigest.String() + "aaa", ErrTestError
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resDigest, resManifest, resIspecImage, resErr := extractImageDetails(ctx,
|
||||||
|
mockOlum, "zot-test", "latest", testLogger)
|
||||||
|
So(resErr, ShouldEqual, ErrTestError)
|
||||||
|
So(string(resDigest), ShouldEqual, "")
|
||||||
|
So(resManifest, ShouldBeNil)
|
||||||
|
|
||||||
|
So(resIspecImage, ShouldBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("extractImageDetails bad ImageBlobManifest", func() {
|
||||||
|
mockOlum := mocks.OciLayoutUtilsMock{
|
||||||
|
GetImageBlobManifestFn: func(imageDir string, digest godigest.Digest) (v1.Manifest, error) {
|
||||||
|
return localTestManifestV1, ErrTestError
|
||||||
|
},
|
||||||
|
GetImageConfigInfoFn: func(repo string, digest godigest.Digest) (
|
||||||
|
ispec.Image, error,
|
||||||
|
) {
|
||||||
|
return config, nil
|
||||||
|
},
|
||||||
|
GetImageManifestFn: func(repo string, tag string) (
|
||||||
|
ispec.Manifest, string, error,
|
||||||
|
) {
|
||||||
|
return localTestManifest, localTestDigest.String(), nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resDigest, resManifest, resIspecImage, resErr := extractImageDetails(ctx,
|
||||||
|
mockOlum, "zot-test", "latest", testLogger)
|
||||||
|
So(string(resDigest), ShouldEqual, "")
|
||||||
|
So(resManifest, ShouldBeNil)
|
||||||
|
|
||||||
|
So(resIspecImage, ShouldBeNil)
|
||||||
|
So(resErr, ShouldEqual, ErrTestError)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("extractImageDetails bad imageConfig", func() {
|
||||||
|
mockOlum := mocks.OciLayoutUtilsMock{
|
||||||
|
GetImageBlobManifestFn: func(imageDir string, digest godigest.Digest) (v1.Manifest, error) {
|
||||||
|
return localTestManifestV1, nil
|
||||||
|
},
|
||||||
|
GetImageConfigInfoFn: func(repo string, digest godigest.Digest) (
|
||||||
|
ispec.Image, error,
|
||||||
|
) {
|
||||||
|
return config, nil
|
||||||
|
},
|
||||||
|
GetImageManifestFn: func(repo string, tag string) (
|
||||||
|
ispec.Manifest, string, error,
|
||||||
|
) {
|
||||||
|
return localTestManifest, localTestDigest.String(), ErrTestError
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resDigest, resManifest, resIspecImage, resErr := extractImageDetails(ctx,
|
||||||
|
mockOlum, "zot-test", "latest", testLogger)
|
||||||
|
So(string(resDigest), ShouldEqual, "")
|
||||||
|
So(resManifest, ShouldBeNil)
|
||||||
|
|
||||||
|
So(resIspecImage, ShouldBeNil)
|
||||||
|
So(resErr, ShouldEqual, ErrTestError)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("extractImageDetails without proper authz", func() {
|
||||||
|
ctx = context.WithValue(ctx, authzCtxKey,
|
||||||
|
localCtx.AccessControlContext{
|
||||||
|
GlobPatterns: map[string]bool{},
|
||||||
|
Username: "jane_doe",
|
||||||
|
})
|
||||||
|
mockOlum := mocks.OciLayoutUtilsMock{
|
||||||
|
GetImageBlobManifestFn: func(imageDir string, digest godigest.Digest) (v1.Manifest, error) {
|
||||||
|
return localTestManifestV1, nil
|
||||||
|
},
|
||||||
|
GetImageConfigInfoFn: func(repo string, digest godigest.Digest) (
|
||||||
|
ispec.Image, error,
|
||||||
|
) {
|
||||||
|
return config, nil
|
||||||
|
},
|
||||||
|
GetImageManifestFn: func(repo string, tag string) (
|
||||||
|
ispec.Manifest, string, error,
|
||||||
|
) {
|
||||||
|
return localTestManifest, localTestDigest.String(), ErrTestError
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resDigest, resManifest, resIspecImage, resErr := extractImageDetails(ctx,
|
||||||
|
mockOlum, "zot-test", "latest", testLogger)
|
||||||
|
So(string(resDigest), ShouldEqual, "")
|
||||||
|
So(resManifest, ShouldBeNil)
|
||||||
|
|
||||||
|
So(resIspecImage, ShouldBeNil)
|
||||||
|
So(resErr, ShouldNotBeNil)
|
||||||
|
So(strings.ToLower(resErr.Error()), ShouldContainSubstring, "unauthorized access")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -125,4 +125,5 @@ type Query {
|
||||||
GlobalSearch(query: String!): GlobalSearchResult!
|
GlobalSearch(query: String!): GlobalSearchResult!
|
||||||
DerivedImageList(image: String!): [ImageSummary!]
|
DerivedImageList(image: String!): [ImageSummary!]
|
||||||
BaseImageList(image: String!): [ImageSummary!]
|
BaseImageList(image: String!): [ImageSummary!]
|
||||||
|
Image(image: String!): ImageSummary
|
||||||
}
|
}
|
|
@ -60,7 +60,6 @@ func (r *queryResolver) CVEListForImage(ctx context.Context, image string) (*gql
|
||||||
// ImageListForCve is the resolver for the ImageListForCVE field.
|
// ImageListForCve is the resolver for the ImageListForCVE field.
|
||||||
func (r *queryResolver) ImageListForCve(ctx context.Context, id string) ([]*gql_generated.ImageSummary, error) {
|
func (r *queryResolver) ImageListForCve(ctx context.Context, id string) ([]*gql_generated.ImageSummary, error) {
|
||||||
olu := common.NewBaseOciLayoutUtils(r.storeController, r.log)
|
olu := common.NewBaseOciLayoutUtils(r.storeController, r.log)
|
||||||
|
|
||||||
affectedImages := []*gql_generated.ImageSummary{}
|
affectedImages := []*gql_generated.ImageSummary{}
|
||||||
|
|
||||||
r.log.Info().Msg("extracting repositories")
|
r.log.Info().Msg("extracting repositories")
|
||||||
|
@ -90,7 +89,8 @@ func (r *queryResolver) ImageListForCve(ctx context.Context, id string) ([]*gql_
|
||||||
return affectedImages, err
|
return affectedImages, err
|
||||||
}
|
}
|
||||||
|
|
||||||
imageInfo := BuildImageInfo(repo, imageByCVE.Tag, imageByCVE.Digest, imageByCVE.Manifest, imageConfig)
|
isSigned := olu.CheckManifestSignature(repo, imageByCVE.Digest)
|
||||||
|
imageInfo := BuildImageInfo(repo, imageByCVE.Tag, imageByCVE.Digest, imageByCVE.Manifest, imageConfig, isSigned)
|
||||||
|
|
||||||
affectedImages = append(
|
affectedImages = append(
|
||||||
affectedImages,
|
affectedImages,
|
||||||
|
@ -129,7 +129,8 @@ func (r *queryResolver) ImageListWithCVEFixed(ctx context.Context, id string, im
|
||||||
return []*gql_generated.ImageSummary{}, err
|
return []*gql_generated.ImageSummary{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
imageInfo := BuildImageInfo(image, tag.Name, digest, manifest, imageConfig)
|
isSigned := olu.CheckManifestSignature(image, digest)
|
||||||
|
imageInfo := BuildImageInfo(image, tag.Name, digest, manifest, imageConfig, isSigned)
|
||||||
|
|
||||||
unaffectedImages = append(unaffectedImages, imageInfo)
|
unaffectedImages = append(unaffectedImages, imageInfo)
|
||||||
}
|
}
|
||||||
|
@ -413,7 +414,7 @@ func (r *queryResolver) DerivedImageList(ctx context.Context, image string) ([]*
|
||||||
|
|
||||||
imageDir, imageTag := common.GetImageDirAndTag(image)
|
imageDir, imageTag := common.GetImageDirAndTag(image)
|
||||||
|
|
||||||
imageManifest, err := layoutUtils.GetImageManifest(imageDir, imageTag)
|
imageManifest, _, err := layoutUtils.GetImageManifest(imageDir, imageTag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.log.Info().Str("image", image).Msg("image not found")
|
r.log.Info().Str("image", image).Msg("image not found")
|
||||||
|
|
||||||
|
@ -481,7 +482,7 @@ func (r *queryResolver) BaseImageList(ctx context.Context, image string) ([]*gql
|
||||||
|
|
||||||
imageDir, imageTag := common.GetImageDirAndTag(image)
|
imageDir, imageTag := common.GetImageDirAndTag(image)
|
||||||
|
|
||||||
imageManifest, err := layoutUtils.GetImageManifest(imageDir, imageTag)
|
imageManifest, _, err := layoutUtils.GetImageManifest(imageDir, imageTag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.log.Info().Str("image", image).Msg("image not found")
|
r.log.Info().Str("image", image).Msg("image not found")
|
||||||
|
|
||||||
|
@ -539,6 +540,24 @@ func (r *queryResolver) BaseImageList(ctx context.Context, image string) ([]*gql
|
||||||
return imageList, nil
|
return imageList, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Image is the resolver for the Image field.
|
||||||
|
func (r *queryResolver) Image(ctx context.Context, image string) (*gql_generated.ImageSummary, error) {
|
||||||
|
repo, tag := common.GetImageDirAndTag(image)
|
||||||
|
layoutUtils := common.NewBaseOciLayoutUtils(r.storeController, r.log)
|
||||||
|
|
||||||
|
digest, manifest, imageConfig, err := extractImageDetails(ctx, layoutUtils, repo, tag, r.log)
|
||||||
|
if err != nil {
|
||||||
|
r.log.Error().Err(err).Msg("unable to get image details")
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
isSigned := layoutUtils.CheckManifestSignature(repo, digest)
|
||||||
|
result := BuildImageInfo(repo, tag, digest, *manifest, *imageConfig, isSigned)
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Query returns gql_generated.QueryResolver implementation.
|
// Query returns gql_generated.QueryResolver implementation.
|
||||||
func (r *Resolver) Query() gql_generated.QueryResolver { return &queryResolver{r} }
|
func (r *Resolver) Query() gql_generated.QueryResolver { return &queryResolver{r} }
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type OciLayoutUtilsMock struct {
|
type OciLayoutUtilsMock struct {
|
||||||
|
GetImageManifestFn func(repo string, reference string) (ispec.Manifest, string, error)
|
||||||
GetImageManifestsFn func(image string) ([]ispec.Descriptor, error)
|
GetImageManifestsFn func(image string) ([]ispec.Descriptor, error)
|
||||||
GetImageBlobManifestFn func(imageDir string, digest godigest.Digest) (v1.Manifest, error)
|
GetImageBlobManifestFn func(imageDir string, digest godigest.Digest) (v1.Manifest, error)
|
||||||
GetImageInfoFn func(imageDir string, hash v1.Hash) (ispec.Image, error)
|
GetImageInfoFn func(imageDir string, hash v1.Hash) (ispec.Image, error)
|
||||||
|
@ -26,6 +27,14 @@ type OciLayoutUtilsMock struct {
|
||||||
GetRepositoriesFn func() ([]string, error)
|
GetRepositoriesFn func() ([]string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (olum OciLayoutUtilsMock) GetImageManifest(repo string, reference string) (ispec.Manifest, string, error) {
|
||||||
|
if olum.GetImageManifestFn != nil {
|
||||||
|
return olum.GetImageManifestFn(repo, reference)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ispec.Manifest{}, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
func (olum OciLayoutUtilsMock) GetRepositories() ([]string, error) {
|
func (olum OciLayoutUtilsMock) GetRepositories() ([]string, error) {
|
||||||
if olum.GetRepositoriesFn != nil {
|
if olum.GetRepositoriesFn != nil {
|
||||||
return olum.GetRepositoriesFn()
|
return olum.GetRepositoriesFn()
|
||||||
|
|
Loading…
Reference in a new issue