mirror of
https://github.com/project-zot/zot.git
synced 2024-12-16 21:56:37 -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")
|
||||
ErrUnknownCode = errors.New("error: unknown error code")
|
||||
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")
|
||||
ErrLDAPEmptyPassphrase = errors.New("ldap: empty passphrase")
|
||||
ErrLDAPBadConn = errors.New("ldap: bad connection")
|
||||
|
@ -32,7 +32,7 @@ var (
|
|||
ErrInvalidArgs = errors.New("cli: Invalid Arguments")
|
||||
ErrInvalidFlagsCombination = errors.New("cli: Invalid combination of flags")
|
||||
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")
|
||||
ErrConfigNotFound = errors.New("cli: config with the given name does not exist")
|
||||
ErrNoURLProvided = errors.New("cli: no URL provided in argument or via config")
|
||||
|
@ -58,4 +58,5 @@ var (
|
|||
ErrBadType = errors.New("core: invalid type")
|
||||
ErrParsingHTTPHeader = errors.New("routes: invalid HTTP header")
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
sort.Slice(allTags, func(i, j int) bool {
|
||||
return allTags[i].Timestamp.Before(allTags[j].Timestamp)
|
||||
|
|
|
@ -66,7 +66,7 @@ type ImageListResponse struct {
|
|||
}
|
||||
|
||||
type ImageList struct {
|
||||
SummaryList []ImageSummary `json:"imageList"`
|
||||
SummaryList []common.ImageSummary `json:"imageList"`
|
||||
}
|
||||
|
||||
type ExpandedRepoInfoResp struct {
|
||||
|
@ -83,62 +83,9 @@ type GlobalSearchResult struct {
|
|||
GlobalSearch GlobalSearch `json:"globalSearch"`
|
||||
}
|
||||
type GlobalSearch struct {
|
||||
Images []ImageSummary `json:"images"`
|
||||
Repos []RepoSummary `json:"repos"`
|
||||
Layers []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"`
|
||||
Images []common.ImageSummary `json:"images"`
|
||||
Repos []common.RepoSummary `json:"repos"`
|
||||
Layers []common.LayerSummary `json:"layers"`
|
||||
}
|
||||
|
||||
type ExpandedRepoInfo struct {
|
||||
|
@ -147,7 +94,7 @@ type ExpandedRepoInfo struct {
|
|||
|
||||
//nolint:tagliatelle // graphQL schema
|
||||
type RepoListWithNewestImage struct {
|
||||
Repos []RepoSummary `json:"RepoListWithNewestImage"`
|
||||
Repos []common.RepoSummary `json:"RepoListWithNewestImage"`
|
||||
}
|
||||
|
||||
type ErrorGQL struct {
|
||||
|
@ -155,15 +102,12 @@ type ErrorGQL struct {
|
|||
Path []string `json:"path"`
|
||||
}
|
||||
|
||||
type ImageInfo struct {
|
||||
RepoName string
|
||||
Tag string
|
||||
LastUpdated time.Time
|
||||
Description string
|
||||
Licenses string
|
||||
Vendor string
|
||||
Size string
|
||||
Labels string
|
||||
type SingleImageSummary struct {
|
||||
ImageSummary common.ImageSummary `json:"Image"` //nolint:tagliatelle
|
||||
}
|
||||
type ImageSummaryResult struct {
|
||||
SingleImageSummary SingleImageSummary `json:"data"`
|
||||
Errors []ErrorGQL `json:"errors"`
|
||||
}
|
||||
|
||||
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(
|
||||
Image{
|
||||
|
@ -1245,7 +1189,7 @@ func TestDerivedImageList(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
repoName = "same-layers"
|
||||
repoName = "same-layers" //nolint:goconst
|
||||
|
||||
err = UploadImage(
|
||||
Image{
|
||||
|
@ -1378,7 +1322,7 @@ func TestDerivedImageList(t *testing.T) {
|
|||
|
||||
resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query))
|
||||
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()), "more-layers"), ShouldBeTrue)
|
||||
So(err, ShouldBeNil)
|
||||
|
@ -1497,7 +1441,7 @@ func TestGetImageManifest(t *testing.T) {
|
|||
}
|
||||
olu := common.NewBaseOciLayoutUtils(storeController, log.NewLogger("debug", ""))
|
||||
|
||||
_, err := olu.GetImageManifest("nonexistent-repo", "latest")
|
||||
_, _, err := olu.GetImageManifest("nonexistent-repo", "latest")
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
|
@ -1513,7 +1457,7 @@ func TestGetImageManifest(t *testing.T) {
|
|||
}
|
||||
olu := common.NewBaseOciLayoutUtils(storeController, log.NewLogger("debug", ""))
|
||||
|
||||
_, err := olu.GetImageManifest("test-repo", "latest")
|
||||
_, _, err := olu.GetImageManifest("test-repo", "latest") //nolint:goconst
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
}
|
||||
|
@ -1623,7 +1567,7 @@ func TestBaseImageList(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
repoName := "test-repo"
|
||||
repoName := "test-repo" //nolint:goconst
|
||||
|
||||
err = UploadImage(
|
||||
Image{
|
||||
|
@ -1671,7 +1615,7 @@ func TestBaseImageList(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
repoName = "same-layers"
|
||||
repoName = "same-layers" //nolint:goconst
|
||||
|
||||
err = UploadImage(
|
||||
Image{
|
||||
|
@ -1890,12 +1834,12 @@ func TestBaseImageList(t *testing.T) {
|
|||
|
||||
resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query))
|
||||
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-false"), 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()), "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(resp.StatusCode(), ShouldEqual, 200)
|
||||
})
|
||||
|
@ -2172,7 +2116,7 @@ func TestGlobalSearch(t *testing.T) {
|
|||
t.Logf("returned layers: %v", responseStruct.GlobalSearchResult.GlobalSearch.Layers)
|
||||
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 {
|
||||
// Make sure all returned results are supposed to be in the repo
|
||||
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)
|
||||
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 {
|
||||
// Make sure all returned results are supposed to be in the repo
|
||||
So(allExpectedTagMap[image.RepoName], ShouldContain, image.Tag)
|
||||
|
@ -2740,7 +2684,10 @@ func TestBuildImageInfo(t *testing.T) {
|
|||
imageConfig, err := olu.GetImageConfigInfo(invalid, manifestDigest)
|
||||
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))
|
||||
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) {
|
||||
// this blocks
|
||||
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"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
|
@ -20,6 +19,7 @@ import (
|
|||
)
|
||||
|
||||
type OciLayoutUtils interface {
|
||||
GetImageManifest(repo string, reference string) (ispec.Manifest, string, error)
|
||||
GetImageManifests(image string) ([]ispec.Descriptor, error)
|
||||
GetImageBlobManifest(imageDir string, digest godigest.Digest) (v1.Manifest, error)
|
||||
GetImageInfo(imageDir string, hash v1.Hash) (ispec.Image, error)
|
||||
|
@ -40,77 +40,31 @@ type BaseOciLayoutUtils struct {
|
|||
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.
|
||||
func NewBaseOciLayoutUtils(storeController storage.StoreController, log log.Logger) *BaseOciLayoutUtils {
|
||||
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)
|
||||
|
||||
if reference == "" {
|
||||
reference = "latest"
|
||||
}
|
||||
|
||||
buf, _, _, err := imageStore.GetImageManifest(repo, reference)
|
||||
buf, dig, _, err := imageStore.GetImageManifest(repo, reference)
|
||||
if err != nil {
|
||||
return ispec.Manifest{}, err
|
||||
return ispec.Manifest{}, "", err
|
||||
}
|
||||
|
||||
var manifest ispec.Manifest
|
||||
|
||||
err = json.Unmarshal(buf, &manifest)
|
||||
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.
|
||||
|
@ -435,10 +389,10 @@ func (olu BaseOciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error)
|
|||
|
||||
repoPlatforms = append(repoPlatforms, osArch)
|
||||
|
||||
layers := make([]Layer, 0)
|
||||
layers := make([]LayerSummary, 0)
|
||||
|
||||
for _, layer := range manifest.Layers {
|
||||
layerInfo := Layer{}
|
||||
layerInfo := LayerSummary{}
|
||||
|
||||
layerInfo.Digest = layer.Digest.Hex
|
||||
|
||||
|
@ -513,17 +467,3 @@ func (olu BaseOciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error)
|
|||
|
||||
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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -213,10 +214,13 @@ func TestDigestSearchHTTP(t *testing.T) {
|
|||
|
||||
// Call should return {"data":{"ImageListForDigest":[{"Name":"zot-test","Tags":["0.0.1"]}]}}
|
||||
// "2bacca16" should match the manifest of 1 image
|
||||
resp, err = resty.R().Get(
|
||||
baseURL + constants.ExtSearchPrefix + `?query={ImageListForDigest(id:"2bacca16")` +
|
||||
`{RepoName%20Tag%20Digest%20ConfigDigest%20Size%20Layers%20{%20Digest}}}`,
|
||||
)
|
||||
|
||||
gqlQuery := url.QueryEscape(`{ImageListForDigest(id:"2bacca16")
|
||||
{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(err, ShouldBeNil)
|
||||
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].Tag, ShouldEqual, "0.0.1")
|
||||
|
||||
// "adf3bb6c" should match the config of 1 image
|
||||
resp, err = resty.R().Get(
|
||||
baseURL + constants.ExtSearchPrefix + `?query={ImageListForDigest(id:"adf3bb6c")` +
|
||||
`{RepoName%20Tag%20Digest%20ConfigDigest%20Size%20Layers%20{%20Digest}}}`,
|
||||
)
|
||||
// "adf3bb6c" should match the config of 1 image.
|
||||
gqlQuery = url.QueryEscape(`{ImageListForDigest(id:"adf3bb6c")
|
||||
{RepoName Tag Digest ConfigDigest Size Layers { Digest }}}`)
|
||||
|
||||
targetURL = baseURL + constants.ExtSearchPrefix + `?query=` + gqlQuery
|
||||
resp, err = resty.R().Get(targetURL)
|
||||
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
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"]}]}}
|
||||
// "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(
|
||||
baseURL + constants.ExtSearchPrefix + `?query={ImageListForDigest(id:"7a0437f0")` +
|
||||
`{RepoName%20Tag%20Digest%20ConfigDigest%20Size%20Layers%20{%20Digest}}}`,
|
||||
targetURL,
|
||||
)
|
||||
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
var responseStruct2 ImgResponseForDigest
|
||||
|
||||
err = json.Unmarshal(resp.Body(), &responseStruct)
|
||||
err = json.Unmarshal(resp.Body(), &responseStruct2)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(responseStruct.Errors), ShouldEqual, 0)
|
||||
So(len(responseStruct.ImgListForDigest.Images), ShouldEqual, 1)
|
||||
So(responseStruct.ImgListForDigest.Images[0].RepoName, ShouldEqual, "zot-cve-test")
|
||||
So(responseStruct.ImgListForDigest.Images[0].Tag, ShouldEqual, "0.0.1")
|
||||
So(len(responseStruct2.Errors), ShouldEqual, 0)
|
||||
So(len(responseStruct2.ImgListForDigest.Images), ShouldEqual, 1)
|
||||
So(responseStruct2.ImgListForDigest.Images[0].RepoName, ShouldEqual, "zot-cve-test")
|
||||
So(responseStruct2.ImgListForDigest.Images[0].Tag, ShouldEqual, "0.0.1")
|
||||
|
||||
// Call should return {"data":{"ImageListForDigest":[]}}
|
||||
// "1111111" should match 0 images
|
||||
|
|
|
@ -126,6 +126,7 @@ type ComplexityRoot struct {
|
|||
DerivedImageList func(childComplexity int, image string) int
|
||||
ExpandedRepoInfo func(childComplexity int, repo string) int
|
||||
GlobalSearch func(childComplexity int, query string) int
|
||||
Image func(childComplexity int, image string) int
|
||||
ImageList func(childComplexity int, repo string) int
|
||||
ImageListForCve 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)
|
||||
DerivedImageList(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 {
|
||||
|
@ -569,6 +571,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
|||
|
||||
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":
|
||||
if e.complexity.Query.ImageList == nil {
|
||||
break
|
||||
|
@ -887,7 +901,9 @@ type Query {
|
|||
GlobalSearch(query: String!): GlobalSearchResult!
|
||||
DerivedImageList(image: String!): [ImageSummary!]
|
||||
BaseImageList(image: String!): [ImageSummary!]
|
||||
}`, BuiltIn: false},
|
||||
Image(image: String!): ImageSummary
|
||||
}
|
||||
`, BuiltIn: false},
|
||||
}
|
||||
var parsedSchema = gqlparser.MustLoadSchema(sources...)
|
||||
|
||||
|
@ -1039,6 +1055,21 @@ func (ec *executionContext) field_Query_ImageList_args(ctx context.Context, rawA
|
|||
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) {
|
||||
var err error
|
||||
args := map[string]interface{}{}
|
||||
|
@ -3972,6 +4003,100 @@ func (ec *executionContext) fieldContext_Query_BaseImageList(ctx context.Context
|
|||
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) {
|
||||
fc, err := ec.fieldContext_Query___type(ctx, field)
|
||||
if err != nil {
|
||||
|
@ -7112,6 +7237,26 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
|
|||
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 {
|
||||
return rrm(innerCtx)
|
||||
})
|
||||
|
|
|
@ -6,11 +6,11 @@ package search
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
glob "github.com/bmatcuk/doublestar/v4" // nolint:gci
|
||||
|
@ -18,6 +18,7 @@ import (
|
|||
godigest "github.com/opencontainers/go-digest"
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/vektah/gqlparser/v2/gqlerror"
|
||||
"zotregistry.io/zot/errors"
|
||||
"zotregistry.io/zot/pkg/extensions/search/common"
|
||||
cveinfo "zotregistry.io/zot/pkg/extensions/search/cve"
|
||||
digestinfo "zotregistry.io/zot/pkg/extensions/search/digest"
|
||||
|
@ -35,11 +36,6 @@ type Resolver struct {
|
|||
log log.Logger
|
||||
}
|
||||
|
||||
var (
|
||||
ErrBadCtxFormat = errors.New("type assertion failed")
|
||||
ErrBadLayerCount = errors.New("manifest: layers count doesn't correspond to config history")
|
||||
)
|
||||
|
||||
// GetResolverConfig ...
|
||||
func GetResolverConfig(log log.Logger, storeController storage.StoreController, cveInfo cveinfo.CveInfo,
|
||||
) gql_generated.Config {
|
||||
|
@ -75,7 +71,9 @@ func (r *queryResolver) getImageListForDigest(repoList []string, digest string)
|
|||
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)
|
||||
}
|
||||
|
@ -544,7 +542,9 @@ func (r *queryResolver) getImageList(store storage.ImageStore, imageName string)
|
|||
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)
|
||||
}
|
||||
|
@ -559,16 +559,21 @@ func (r *queryResolver) getImageList(store storage.ImageStore, imageName string)
|
|||
}
|
||||
|
||||
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 {
|
||||
layers := []*gql_generated.LayerSummary{}
|
||||
size := int64(0)
|
||||
|
||||
log := log.NewLogger("debug", "")
|
||||
|
||||
allHistory := []*gql_generated.LayerHistory{}
|
||||
|
||||
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
|
||||
if len(history) == 0 {
|
||||
|
@ -596,13 +601,26 @@ func BuildImageInfo(repo string, tag string, manifestDigest godigest.Digest,
|
|||
formattedSize := strconv.FormatInt(size, 10)
|
||||
|
||||
imageInfo := &gql_generated.ImageSummary{
|
||||
RepoName: &repo,
|
||||
Tag: &tag,
|
||||
Digest: &formattedManifestDigest,
|
||||
ConfigDigest: &manifest.Config.Digest.Hex,
|
||||
Size: &formattedSize,
|
||||
Layers: layers,
|
||||
History: []*gql_generated.LayerHistory{},
|
||||
RepoName: &repo,
|
||||
Tag: &tag,
|
||||
Digest: &formattedManifestDigest,
|
||||
ConfigDigest: &manifest.Config.Digest.Hex,
|
||||
Size: &formattedSize,
|
||||
Layers: layers,
|
||||
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
|
||||
|
@ -629,16 +647,29 @@ func BuildImageInfo(repo string, tag string, manifestDigest godigest.Digest,
|
|||
if layersIterator+1 > len(manifest.Layers) {
|
||||
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{
|
||||
RepoName: &repo,
|
||||
Tag: &tag,
|
||||
Digest: &formattedManifestDigest,
|
||||
ConfigDigest: &manifest.Config.Digest.Hex,
|
||||
Size: &formattedSize,
|
||||
Layers: layers,
|
||||
History: allHistory,
|
||||
RepoName: &repo,
|
||||
Tag: &tag,
|
||||
Digest: &formattedManifestDigest,
|
||||
ConfigDigest: &manifest.Config.Digest.Hex,
|
||||
Size: &formattedSize,
|
||||
Layers: layers,
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -664,13 +695,26 @@ func BuildImageInfo(repo string, tag string, manifestDigest godigest.Digest,
|
|||
formattedSize := strconv.FormatInt(size, 10)
|
||||
|
||||
imageInfo := &gql_generated.ImageSummary{
|
||||
RepoName: &repo,
|
||||
Tag: &tag,
|
||||
Digest: &formattedManifestDigest,
|
||||
ConfigDigest: &manifest.Config.Digest.Hex,
|
||||
Size: &formattedSize,
|
||||
Layers: layers,
|
||||
History: allHistory,
|
||||
RepoName: &repo,
|
||||
Tag: &tag,
|
||||
Digest: &formattedManifestDigest,
|
||||
ConfigDigest: &manifest.Config.Digest.Hex,
|
||||
Size: &formattedSize,
|
||||
Layers: layers,
|
||||
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
|
||||
|
@ -703,7 +747,7 @@ func userAvailableRepos(ctx context.Context, repoList []string) ([]string, error
|
|||
if authCtx := ctx.Value(authzCtxKey); authCtx != nil {
|
||||
acCtx, ok := authCtx.(localCtx.AccessControlContext)
|
||||
if !ok {
|
||||
err := ErrBadCtxFormat
|
||||
err := errors.ErrBadType
|
||||
|
||||
return []string{}, err
|
||||
}
|
||||
|
@ -719,3 +763,49 @@ func userAvailableRepos(ctx context.Context, repoList []string) ([]string, error
|
|||
|
||||
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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
|
@ -157,7 +158,7 @@ func TestGlobalSearch(t *testing.T) {
|
|||
ImageSummaries: []common.ImageSummary{
|
||||
{
|
||||
Tag: "latest",
|
||||
Layers: []common.Layer{
|
||||
Layers: []common.LayerSummary{
|
||||
{
|
||||
Size: "100",
|
||||
Digest: "sha256:855b1556a45637abf05c63407437f6f305b4627c4361fb965a78e5731999c0c7",
|
||||
|
@ -313,3 +314,197 @@ func TestMatching(t *testing.T) {
|
|||
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!
|
||||
DerivedImageList(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.
|
||||
func (r *queryResolver) ImageListForCve(ctx context.Context, id string) ([]*gql_generated.ImageSummary, error) {
|
||||
olu := common.NewBaseOciLayoutUtils(r.storeController, r.log)
|
||||
|
||||
affectedImages := []*gql_generated.ImageSummary{}
|
||||
|
||||
r.log.Info().Msg("extracting repositories")
|
||||
|
@ -90,7 +89,8 @@ func (r *queryResolver) ImageListForCve(ctx context.Context, id string) ([]*gql_
|
|||
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,
|
||||
|
@ -129,7 +129,8 @@ func (r *queryResolver) ImageListWithCVEFixed(ctx context.Context, id string, im
|
|||
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)
|
||||
}
|
||||
|
@ -413,7 +414,7 @@ func (r *queryResolver) DerivedImageList(ctx context.Context, image string) ([]*
|
|||
|
||||
imageDir, imageTag := common.GetImageDirAndTag(image)
|
||||
|
||||
imageManifest, err := layoutUtils.GetImageManifest(imageDir, imageTag)
|
||||
imageManifest, _, err := layoutUtils.GetImageManifest(imageDir, imageTag)
|
||||
if err != nil {
|
||||
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)
|
||||
|
||||
imageManifest, err := layoutUtils.GetImageManifest(imageDir, imageTag)
|
||||
imageManifest, _, err := layoutUtils.GetImageManifest(imageDir, imageTag)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (r *Resolver) Query() gql_generated.QueryResolver { return &queryResolver{r} }
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
)
|
||||
|
||||
type OciLayoutUtilsMock struct {
|
||||
GetImageManifestFn func(repo string, reference string) (ispec.Manifest, string, error)
|
||||
GetImageManifestsFn func(image string) ([]ispec.Descriptor, error)
|
||||
GetImageBlobManifestFn func(imageDir string, digest godigest.Digest) (v1.Manifest, error)
|
||||
GetImageInfoFn func(imageDir string, hash v1.Hash) (ispec.Image, error)
|
||||
|
@ -26,6 +27,14 @@ type OciLayoutUtilsMock struct {
|
|||
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) {
|
||||
if olum.GetRepositoriesFn != nil {
|
||||
return olum.GetRepositoriesFn()
|
||||
|
|
Loading…
Reference in a new issue