0
Fork 0
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:
Bogdan Bivolaru 2022-09-30 20:32:32 +03:00 committed by GitHub
parent 885f139e0e
commit 67294cc669
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 805 additions and 227 deletions

View file

@ -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")
)

View file

@ -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)

View file

@ -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()

View 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"`
}

View file

@ -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
}

View file

@ -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

View file

@ -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)
})

View file

@ -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 {
@ -602,7 +607,20 @@ func BuildImageInfo(repo string, tag string, manifestDigest godigest.Digest,
ConfigDigest: &manifest.Config.Digest.Hex,
Size: &formattedSize,
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
@ -629,7 +647,7 @@ 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,
@ -639,6 +657,19 @@ func BuildImageInfo(repo string, tag string, manifestDigest godigest.Digest,
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,
},
}
}
@ -671,6 +702,19 @@ func BuildImageInfo(repo string, tag string, manifestDigest godigest.Digest,
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
}

View file

@ -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")
})
})
}

View file

@ -125,4 +125,5 @@ type Query {
GlobalSearch(query: String!): GlobalSearchResult!
DerivedImageList(image: String!): [ImageSummary!]
BaseImageList(image: String!): [ImageSummary!]
Image(image: String!): ImageSummary
}

View file

@ -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} }

View file

@ -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()