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

Enrich ImageSummary with a new field representing image History

Closing #748

Signed-off-by: Alex Stan <alexandrustan96@yahoo.ro>
This commit is contained in:
Alex Stan 2022-09-21 20:53:56 +03:00 committed by Ramkumar Chinchani
parent 7804ba7ce0
commit 42c947fa03
6 changed files with 1096 additions and 27 deletions

View file

@ -18,6 +18,7 @@ import (
"time"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sigstore/cosign/cmd/cosign/cli/generate"
"github.com/sigstore/cosign/cmd/cosign/cli/options"
@ -29,6 +30,7 @@ import (
"zotregistry.io/zot/pkg/api/constants"
extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/extensions/monitoring"
"zotregistry.io/zot/pkg/extensions/search"
"zotregistry.io/zot/pkg/extensions/search/common"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage"
@ -56,6 +58,15 @@ type RepoWithNewestImageResponse struct {
Errors []ErrorGQL `json:"errors"`
}
type ImageListResponse struct {
ImageList ImageList `json:"data"`
Errors []ErrorGQL `json:"errors"`
}
type ImageList struct {
SummaryList []ImageSummary `json:"imageList"`
}
type ExpandedRepoInfoResp struct {
ExpandedRepoInfo ExpandedRepoInfo `json:"data"`
Errors []ErrorGQL `json:"errors"`
@ -76,14 +87,29 @@ type GlobalSearch struct {
}
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"`
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"`
}
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 RepoSummary struct {
@ -1163,6 +1189,301 @@ func TestGlobalSearch(t *testing.T) {
})
}
func TestImageList(t *testing.T) {
Convey("Test ImageList", t, func() {
subpath := "/a"
err := testSetup(t, subpath)
if err != nil {
panic(err)
}
port := GetFreePort()
baseURL := GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
conf.Storage.RootDirectory = rootDir
defaultVal := true
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{Enable: &defaultVal},
}
conf.Extensions.Search.CVE = nil
ctlr := api.NewController(conf)
go startServer(ctlr)
defer stopServer(ctlr)
WaitTillServerReady(baseURL)
imageStore := ctlr.StoreController.DefaultStore
repos, err := imageStore.GetRepositories()
So(err, ShouldBeNil)
tags, err := imageStore.GetImageTags(repos[0])
So(err, ShouldBeNil)
buf, _, _, err := imageStore.GetImageManifest(repos[0], tags[0])
So(err, ShouldBeNil)
var imageManifest ispec.Manifest
err = json.Unmarshal(buf, &imageManifest)
So(err, ShouldBeNil)
var imageConfigInfo ispec.Image
imageConfigBuf, err := imageStore.GetBlobContent(repos[0], imageManifest.Config.Digest.String())
So(err, ShouldBeNil)
err = json.Unmarshal(imageConfigBuf, &imageConfigInfo)
So(err, ShouldBeNil)
query := fmt.Sprintf(`{
ImageList(repo:"%s"){
History{
HistoryDescription{
Author
Comment
Created
CreatedBy
EmptyLayer
},
Layer{
Digest
Size
}
}
}
}`, repos[0])
resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query))
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
So(resp, ShouldNotBeNil)
var responseStruct ImageListResponse
err = json.Unmarshal(resp.Body(), &responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.ImageList.SummaryList[0].History), ShouldEqual, len(imageConfigInfo.History))
})
Convey("Test ImageSummary retuned by ImageList when getting tags timestamp info fails", t, func() {
invalid := "test"
tempDir := t.TempDir()
port := GetFreePort()
baseURL := GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
conf.Storage.RootDirectory = tempDir
defaultVal := true
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{Enable: &defaultVal},
}
conf.Extensions.Search.CVE = nil
ctlr := api.NewController(conf)
go startServer(ctlr)
defer stopServer(ctlr)
WaitTillServerReady(baseURL)
config := ispec.Image{
Architecture: "amd64",
OS: "linux",
RootFS: ispec.RootFS{
Type: "layers",
DiffIDs: []digest.Digest{},
},
Author: "ZotUser",
History: []ispec.History{},
}
configBlob, err := json.Marshal(config)
So(err, ShouldBeNil)
configDigest := digest.FromBytes(configBlob)
layerDigest := digest.FromString(invalid)
layerblob := []byte(invalid)
schemaVersion := 2
ispecManifest := ispec.Manifest{
Versioned: specs.Versioned{
SchemaVersion: schemaVersion,
},
Config: ispec.Descriptor{
MediaType: "application/vnd.oci.image.config.v1+json",
Digest: configDigest,
Size: int64(len(configBlob)),
},
Layers: []ispec.Descriptor{ // just 1 layer in manifest
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: layerDigest,
Size: int64(len(layerblob)),
},
},
Annotations: map[string]string{
ispec.AnnotationRefName: "1.0",
},
}
err = UploadImage(
Image{
Manifest: ispecManifest,
Config: config,
Layers: [][]byte{
layerblob,
},
Tag: "0.0.1",
},
baseURL,
invalid,
)
So(err, ShouldBeNil)
configPath := path.Join(conf.Storage.RootDirectory, invalid, "blobs",
configDigest.Algorithm().String(), configDigest.Encoded())
err = os.Remove(configPath)
So(err, ShouldBeNil)
query := fmt.Sprintf(`{
ImageList(repo:"%s"){
History{
HistoryDescription{
Author
Comment
Created
CreatedBy
EmptyLayer
},
Layer{
Digest
Size
}
}
}
}`, invalid)
resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query))
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
So(resp, ShouldNotBeNil)
var responseStruct ImageListResponse
err = json.Unmarshal(resp.Body(), &responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.ImageList.SummaryList), ShouldBeZeroValue)
})
}
func TestBuildImageInfo(t *testing.T) {
Convey("Check image summary when layer count does not match history", t, func() {
invalid := "invalid"
port := GetFreePort()
baseURL := GetBaseURL(port)
rootDir = t.TempDir()
conf := config.New()
conf.HTTP.Port = port
conf.Storage.RootDirectory = rootDir
defaultVal := true
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{Enable: &defaultVal},
}
conf.Extensions.Search.CVE = nil
ctlr := api.NewController(conf)
go startServer(ctlr)
defer stopServer(ctlr)
WaitTillServerReady(baseURL)
olu := &common.BaseOciLayoutUtils{
StoreController: ctlr.StoreController,
Log: ctlr.Log,
}
config := ispec.Image{
Architecture: "amd64",
OS: "linux",
RootFS: ispec.RootFS{
Type: "layers",
DiffIDs: []digest.Digest{},
},
Author: "ZotUser",
History: []ispec.History{ // should contain 3 elements, 2 of which corresponding to layers
{
EmptyLayer: false,
},
{
EmptyLayer: false,
},
{
EmptyLayer: true,
},
},
}
configBlob, err := json.Marshal(config)
So(err, ShouldBeNil)
configDigest := digest.FromBytes(configBlob)
layerDigest := digest.FromString(invalid)
layerblob := []byte(invalid)
schemaVersion := 2
ispecManifest := ispec.Manifest{
Versioned: specs.Versioned{
SchemaVersion: schemaVersion,
},
Config: ispec.Descriptor{
MediaType: "application/vnd.oci.image.config.v1+json",
Digest: configDigest,
Size: int64(len(configBlob)),
},
Layers: []ispec.Descriptor{ // just 1 layer in manifest
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: layerDigest,
Size: int64(len(layerblob)),
},
},
}
manifestLayersSize := ispecManifest.Layers[0].Size
manifestBlob, err := json.Marshal(ispecManifest)
So(err, ShouldBeNil)
manifestDigest := digest.FromBytes(manifestBlob)
err = UploadImage(
Image{
Manifest: ispecManifest,
Config: config,
Layers: [][]byte{
layerblob,
},
Tag: "0.0.1",
},
baseURL,
invalid,
)
So(err, ShouldBeNil)
manifest, err := olu.GetImageBlobManifest(invalid, manifestDigest)
So(err, ShouldBeNil)
imageConfig, err := olu.GetImageConfigInfo(invalid, manifestDigest)
So(err, ShouldBeNil)
imageSummary := search.BuildImageInfo(invalid, invalid, manifestDigest, manifest, imageConfig)
So(len(imageSummary.Layers), ShouldEqual, len(manifest.Layers))
imageSummaryLayerSize, err := strconv.Atoi(*imageSummary.Size)
So(err, ShouldBeNil)
So(imageSummaryLayerSize, ShouldEqual, manifestLayersSize)
})
}
func TestBaseOciLayoutUtils(t *testing.T) {
manifestDigest := "sha256:adf3bb6cc81f8bd6a9d5233be5f0c1a4f1e3ed1cf5bbdfad7708cc8d4099b741"

View file

@ -62,12 +62,21 @@ type ComplexityRoot struct {
Repos func(childComplexity int) int
}
HistoryDescription struct {
Author func(childComplexity int) int
Comment func(childComplexity int) int
Created func(childComplexity int) int
CreatedBy func(childComplexity int) int
EmptyLayer func(childComplexity int) int
}
ImageSummary struct {
ConfigDigest func(childComplexity int) int
Description func(childComplexity int) int
Digest func(childComplexity int) int
Documentation func(childComplexity int) int
DownloadCount func(childComplexity int) int
History func(childComplexity int) int
IsSigned func(childComplexity int) int
Labels func(childComplexity int) int
LastUpdated func(childComplexity int) int
@ -83,6 +92,11 @@ type ComplexityRoot struct {
Vendor func(childComplexity int) int
}
LayerHistory struct {
HistoryDescription func(childComplexity int) int
Layer func(childComplexity int) int
}
LayerSummary struct {
Digest func(childComplexity int) int
Score func(childComplexity int) int
@ -226,6 +240,41 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.GlobalSearchResult.Repos(childComplexity), true
case "HistoryDescription.Author":
if e.complexity.HistoryDescription.Author == nil {
break
}
return e.complexity.HistoryDescription.Author(childComplexity), true
case "HistoryDescription.Comment":
if e.complexity.HistoryDescription.Comment == nil {
break
}
return e.complexity.HistoryDescription.Comment(childComplexity), true
case "HistoryDescription.Created":
if e.complexity.HistoryDescription.Created == nil {
break
}
return e.complexity.HistoryDescription.Created(childComplexity), true
case "HistoryDescription.CreatedBy":
if e.complexity.HistoryDescription.CreatedBy == nil {
break
}
return e.complexity.HistoryDescription.CreatedBy(childComplexity), true
case "HistoryDescription.EmptyLayer":
if e.complexity.HistoryDescription.EmptyLayer == nil {
break
}
return e.complexity.HistoryDescription.EmptyLayer(childComplexity), true
case "ImageSummary.ConfigDigest":
if e.complexity.ImageSummary.ConfigDigest == nil {
break
@ -261,6 +310,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.ImageSummary.DownloadCount(childComplexity), true
case "ImageSummary.History":
if e.complexity.ImageSummary.History == nil {
break
}
return e.complexity.ImageSummary.History(childComplexity), true
case "ImageSummary.IsSigned":
if e.complexity.ImageSummary.IsSigned == nil {
break
@ -352,6 +408,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.ImageSummary.Vendor(childComplexity), true
case "LayerHistory.HistoryDescription":
if e.complexity.LayerHistory.HistoryDescription == nil {
break
}
return e.complexity.LayerHistory.HistoryDescription(childComplexity), true
case "LayerHistory.Layer":
if e.complexity.LayerHistory.Layer == nil {
break
}
return e.complexity.LayerHistory.Layer(childComplexity), true
case "LayerSummary.Digest":
if e.complexity.LayerSummary.Digest == nil {
break
@ -690,6 +760,7 @@ type ImageSummary {
Title: String
Source: String
Documentation: String
History: [LayerHistory]
}
# Brief on a specific repo to be used in queries returning a list of repos
@ -714,6 +785,31 @@ type LayerSummary {
Score: Int
}
type HistoryDescription {
Created: Time
"""
CreatedBy is the command which created the layer.
"""
CreatedBy: String
"""
Author is the author of the build point.
"""
Author: String
"""
Comment is a custom message set when creating the layer.
"""
Comment: String
"""
EmptyLayer is used to mark if the history item created a filesystem diff.
"""
EmptyLayer: Boolean
}
type LayerHistory {
Layer: LayerSummary
HistoryDescription: HistoryDescription
}
type OsArch {
Os: String
Arch: String
@ -1283,6 +1379,8 @@ func (ec *executionContext) fieldContext_GlobalSearchResult_Images(ctx context.C
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)
}
return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name)
},
@ -1402,6 +1500,211 @@ func (ec *executionContext) fieldContext_GlobalSearchResult_Layers(ctx context.C
return fc, nil
}
func (ec *executionContext) _HistoryDescription_Created(ctx context.Context, field graphql.CollectedField, obj *HistoryDescription) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_HistoryDescription_Created(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 obj.Created, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*time.Time)
fc.Result = res
return ec.marshalOTime2ᚖtimeᚐTime(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_HistoryDescription_Created(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "HistoryDescription",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Time does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _HistoryDescription_CreatedBy(ctx context.Context, field graphql.CollectedField, obj *HistoryDescription) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_HistoryDescription_CreatedBy(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 obj.CreatedBy, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*string)
fc.Result = res
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_HistoryDescription_CreatedBy(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "HistoryDescription",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _HistoryDescription_Author(ctx context.Context, field graphql.CollectedField, obj *HistoryDescription) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_HistoryDescription_Author(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 obj.Author, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*string)
fc.Result = res
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_HistoryDescription_Author(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "HistoryDescription",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _HistoryDescription_Comment(ctx context.Context, field graphql.CollectedField, obj *HistoryDescription) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_HistoryDescription_Comment(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 obj.Comment, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*string)
fc.Result = res
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_HistoryDescription_Comment(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "HistoryDescription",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _HistoryDescription_EmptyLayer(ctx context.Context, field graphql.CollectedField, obj *HistoryDescription) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_HistoryDescription_EmptyLayer(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 obj.EmptyLayer, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*bool)
fc.Result = res
return ec.marshalOBoolean2ᚖbool(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_HistoryDescription_EmptyLayer(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "HistoryDescription",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Boolean does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _ImageSummary_RepoName(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_ImageSummary_RepoName(ctx, field)
if err != nil {
@ -2154,6 +2457,155 @@ func (ec *executionContext) fieldContext_ImageSummary_Documentation(ctx context.
return fc, nil
}
func (ec *executionContext) _ImageSummary_History(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_ImageSummary_History(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 obj.History, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.([]*LayerHistory)
fc.Result = res
return ec.marshalOLayerHistory2ᚕᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐLayerHistory(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_ImageSummary_History(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "ImageSummary",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "Layer":
return ec.fieldContext_LayerHistory_Layer(ctx, field)
case "HistoryDescription":
return ec.fieldContext_LayerHistory_HistoryDescription(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type LayerHistory", field.Name)
},
}
return fc, nil
}
func (ec *executionContext) _LayerHistory_Layer(ctx context.Context, field graphql.CollectedField, obj *LayerHistory) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_LayerHistory_Layer(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 obj.Layer, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*LayerSummary)
fc.Result = res
return ec.marshalOLayerSummary2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐLayerSummary(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_LayerHistory_Layer(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "LayerHistory",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "Size":
return ec.fieldContext_LayerSummary_Size(ctx, field)
case "Digest":
return ec.fieldContext_LayerSummary_Digest(ctx, field)
case "Score":
return ec.fieldContext_LayerSummary_Score(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type LayerSummary", field.Name)
},
}
return fc, nil
}
func (ec *executionContext) _LayerHistory_HistoryDescription(ctx context.Context, field graphql.CollectedField, obj *LayerHistory) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_LayerHistory_HistoryDescription(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 obj.HistoryDescription, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*HistoryDescription)
fc.Result = res
return ec.marshalOHistoryDescription2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐHistoryDescription(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_LayerHistory_HistoryDescription(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "LayerHistory",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "Created":
return ec.fieldContext_HistoryDescription_Created(ctx, field)
case "CreatedBy":
return ec.fieldContext_HistoryDescription_CreatedBy(ctx, field)
case "Author":
return ec.fieldContext_HistoryDescription_Author(ctx, field)
case "Comment":
return ec.fieldContext_HistoryDescription_Comment(ctx, field)
case "EmptyLayer":
return ec.fieldContext_HistoryDescription_EmptyLayer(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type HistoryDescription", field.Name)
},
}
return fc, nil
}
func (ec *executionContext) _LayerSummary_Size(ctx context.Context, field graphql.CollectedField, obj *LayerSummary) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_LayerSummary_Size(ctx, field)
if err != nil {
@ -2615,6 +3067,8 @@ func (ec *executionContext) fieldContext_Query_ImageListForCVE(ctx context.Conte
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)
}
return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name)
},
@ -2705,6 +3159,8 @@ func (ec *executionContext) fieldContext_Query_ImageListWithCVEFixed(ctx context
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)
}
return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name)
},
@ -2795,6 +3251,8 @@ func (ec *executionContext) fieldContext_Query_ImageListForDigest(ctx context.Co
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)
}
return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name)
},
@ -2951,6 +3409,8 @@ func (ec *executionContext) fieldContext_Query_ImageList(ctx context.Context, fi
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)
}
return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name)
},
@ -3294,6 +3754,8 @@ func (ec *executionContext) fieldContext_RepoInfo_Images(ctx context.Context, fi
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)
}
return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name)
},
@ -3688,6 +4150,8 @@ func (ec *executionContext) fieldContext_RepoSummary_NewestImage(ctx context.Con
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)
}
return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name)
},
@ -5702,6 +6166,47 @@ func (ec *executionContext) _GlobalSearchResult(ctx context.Context, sel ast.Sel
return out
}
var historyDescriptionImplementors = []string{"HistoryDescription"}
func (ec *executionContext) _HistoryDescription(ctx context.Context, sel ast.SelectionSet, obj *HistoryDescription) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, historyDescriptionImplementors)
out := graphql.NewFieldSet(fields)
var invalids uint32
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("HistoryDescription")
case "Created":
out.Values[i] = ec._HistoryDescription_Created(ctx, field, obj)
case "CreatedBy":
out.Values[i] = ec._HistoryDescription_CreatedBy(ctx, field, obj)
case "Author":
out.Values[i] = ec._HistoryDescription_Author(ctx, field, obj)
case "Comment":
out.Values[i] = ec._HistoryDescription_Comment(ctx, field, obj)
case "EmptyLayer":
out.Values[i] = ec._HistoryDescription_EmptyLayer(ctx, field, obj)
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch()
if invalids > 0 {
return graphql.Null
}
return out
}
var imageSummaryImplementors = []string{"ImageSummary"}
func (ec *executionContext) _ImageSummary(ctx context.Context, sel ast.SelectionSet, obj *ImageSummary) graphql.Marshaler {
@ -5784,6 +6289,39 @@ func (ec *executionContext) _ImageSummary(ctx context.Context, sel ast.Selection
out.Values[i] = ec._ImageSummary_Documentation(ctx, field, obj)
case "History":
out.Values[i] = ec._ImageSummary_History(ctx, field, obj)
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch()
if invalids > 0 {
return graphql.Null
}
return out
}
var layerHistoryImplementors = []string{"LayerHistory"}
func (ec *executionContext) _LayerHistory(ctx context.Context, sel ast.SelectionSet, obj *LayerHistory) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, layerHistoryImplementors)
out := graphql.NewFieldSet(fields)
var invalids uint32
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("LayerHistory")
case "Layer":
out.Values[i] = ec._LayerHistory_Layer(ctx, field, obj)
case "HistoryDescription":
out.Values[i] = ec._LayerHistory_HistoryDescription(ctx, field, obj)
default:
panic("unknown field " + strconv.Quote(field.Name))
}
@ -6975,6 +7513,13 @@ func (ec *executionContext) marshalOCVE2ᚖzotregistryᚗioᚋzotᚋpkgᚋextens
return ec._CVE(ctx, sel, v)
}
func (ec *executionContext) marshalOHistoryDescription2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐHistoryDescription(ctx context.Context, sel ast.SelectionSet, v *HistoryDescription) graphql.Marshaler {
if v == nil {
return graphql.Null
}
return ec._HistoryDescription(ctx, sel, v)
}
func (ec *executionContext) marshalOImageSummary2ᚕᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐImageSummary(ctx context.Context, sel ast.SelectionSet, v []*ImageSummary) graphql.Marshaler {
if v == nil {
return graphql.Null
@ -7086,6 +7631,54 @@ func (ec *executionContext) marshalOInt2ᚖint(ctx context.Context, sel ast.Sele
return res
}
func (ec *executionContext) marshalOLayerHistory2ᚕᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐLayerHistory(ctx context.Context, sel ast.SelectionSet, v []*LayerHistory) graphql.Marshaler {
if v == nil {
return graphql.Null
}
ret := make(graphql.Array, len(v))
var wg sync.WaitGroup
isLen1 := len(v) == 1
if !isLen1 {
wg.Add(len(v))
}
for i := range v {
i := i
fc := &graphql.FieldContext{
Index: &i,
Result: &v[i],
}
ctx := graphql.WithFieldContext(ctx, fc)
f := func(i int) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = nil
}
}()
if !isLen1 {
defer wg.Done()
}
ret[i] = ec.marshalOLayerHistory2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐLayerHistory(ctx, sel, v[i])
}
if isLen1 {
f(i)
} else {
go f(i)
}
}
wg.Wait()
return ret
}
func (ec *executionContext) marshalOLayerHistory2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐLayerHistory(ctx context.Context, sel ast.SelectionSet, v *LayerHistory) graphql.Marshaler {
if v == nil {
return graphql.Null
}
return ec._LayerHistory(ctx, sel, v)
}
func (ec *executionContext) marshalOLayerSummary2ᚕᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐLayerSummary(ctx context.Context, sel ast.SelectionSet, v []*LayerSummary) graphql.Marshaler {
if v == nil {
return graphql.Null

View file

@ -25,6 +25,18 @@ type GlobalSearchResult struct {
Layers []*LayerSummary `json:"Layers"`
}
type HistoryDescription struct {
Created *time.Time `json:"Created"`
// CreatedBy is the command which created the layer.
CreatedBy *string `json:"CreatedBy"`
// Author is the author of the build point.
Author *string `json:"Author"`
// Comment is a custom message set when creating the layer.
Comment *string `json:"Comment"`
// EmptyLayer is used to mark if the history item created a filesystem diff.
EmptyLayer *bool `json:"EmptyLayer"`
}
type ImageSummary struct {
RepoName *string `json:"RepoName"`
Tag *string `json:"Tag"`
@ -44,6 +56,12 @@ type ImageSummary struct {
Title *string `json:"Title"`
Source *string `json:"Source"`
Documentation *string `json:"Documentation"`
History []*LayerHistory `json:"History"`
}
type LayerHistory struct {
Layer *LayerSummary `json:"Layer"`
HistoryDescription *HistoryDescription `json:"HistoryDescription"`
}
type LayerSummary struct {

View file

@ -42,7 +42,10 @@ type cveDetail struct {
PackageList []*gql_generated.PackageInfo
}
var ErrBadCtxFormat = errors.New("type assertion failed")
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, enableCVE bool) gql_generated.Config {
@ -71,6 +74,7 @@ func (r *queryResolver) getImageListForCVE(repoList []string, cvid string, imgSt
trivyCtx *cveinfo.TrivyCtx,
) ([]*gql_generated.ImageSummary, error) {
cveResult := []*gql_generated.ImageSummary{}
olu := common.NewBaseOciLayoutUtils(r.storeController, r.log)
for _, repo := range repoList {
r.log.Info().Str("repo", repo).Msg("extracting list of tags available in image repo")
@ -83,9 +87,15 @@ func (r *queryResolver) getImageListForCVE(repoList []string, cvid string, imgSt
}
for _, imageByCVE := range imageListByCVE {
imageConfig, err := olu.GetImageConfigInfo(repo, imageByCVE.Digest)
if err != nil {
return []*gql_generated.ImageSummary{}, err
}
imageInfo := BuildImageInfo(repo, imageByCVE.Tag, imageByCVE.Digest, imageByCVE.Manifest, imageConfig)
cveResult = append(
cveResult,
buildImageInfo(repo, imageByCVE.Tag, imageByCVE.Digest, imageByCVE.Manifest),
cveResult, imageInfo,
)
}
}
@ -95,6 +105,7 @@ func (r *queryResolver) getImageListForCVE(repoList []string, cvid string, imgSt
func (r *queryResolver) getImageListForDigest(repoList []string, digest string) ([]*gql_generated.ImageSummary, error) {
imgResultForDigest := []*gql_generated.ImageSummary{}
olu := common.NewBaseOciLayoutUtils(r.storeController, r.log)
var errResult error
@ -109,7 +120,13 @@ func (r *queryResolver) getImageListForDigest(repoList []string, digest string)
}
for _, imageInfo := range imgTags {
imageInfo := buildImageInfo(repo, imageInfo.Tag, imageInfo.Digest, imageInfo.Manifest)
imageConfig, err := olu.GetImageConfigInfo(repo, imageInfo.Digest)
if err != nil {
return []*gql_generated.ImageSummary{}, err
}
imageInfo := BuildImageInfo(repo, imageInfo.Tag, imageInfo.Digest, imageInfo.Manifest, imageConfig)
imgResultForDigest = append(imgResultForDigest, imageInfo)
}
}
@ -526,7 +543,12 @@ func (r *queryResolver) getImageList(store storage.ImageStore, imageName string)
return results, err
}
imageInfo := buildImageInfo(repo, tag.Name, digest, manifest)
imageConfig, err := layoutUtils.GetImageConfigInfo(repo, digest)
if err != nil {
return results, err
}
imageInfo := BuildImageInfo(repo, tag.Name, digest, manifest, imageConfig)
results = append(results, imageInfo)
}
@ -540,36 +562,119 @@ func (r *queryResolver) getImageList(store storage.ImageStore, imageName string)
return results, nil
}
func buildImageInfo(repo string, tag string, tagDigest godigest.Digest,
manifest v1.Manifest,
func BuildImageInfo(repo string, tag string, manifestDigest godigest.Digest,
manifest v1.Manifest, imageConfig ispec.Image,
) *gql_generated.ImageSummary {
layers := []*gql_generated.LayerSummary{}
size := int64(0)
for _, entry := range manifest.Layers {
size += entry.Size
digest := entry.Digest.Hex
layerSize := strconv.FormatInt(entry.Size, 10)
log := log.NewLogger("debug", "")
allHistory := []*gql_generated.LayerHistory{}
formattedManifestDigest := manifestDigest.Hex()
history := imageConfig.History
if len(history) == 0 {
for _, layer := range manifest.Layers {
size += layer.Size
digest := layer.Digest.Hex
layerSize := strconv.FormatInt(layer.Size, 10)
layer := &gql_generated.LayerSummary{
Size: &layerSize,
Digest: &digest,
}
layers = append(
layers,
layer,
)
allHistory = append(allHistory, &gql_generated.LayerHistory{
Layer: layer,
HistoryDescription: &gql_generated.HistoryDescription{},
})
}
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{},
}
return imageInfo
}
// iterator over manifest layers
var layersIterator int
// since we are appending pointers, it is important to iterate with an index over slice
for i := range history {
allHistory = append(allHistory, &gql_generated.LayerHistory{
HistoryDescription: &gql_generated.HistoryDescription{
Created: history[i].Created,
CreatedBy: &history[i].CreatedBy,
Author: &history[i].Author,
Comment: &history[i].Comment,
EmptyLayer: &history[i].EmptyLayer,
},
})
if history[i].EmptyLayer {
continue
}
if layersIterator+1 > len(manifest.Layers) {
formattedSize := strconv.FormatInt(size, 10)
log.Error().Err(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,
}
}
size += manifest.Layers[layersIterator].Size
digest := manifest.Layers[layersIterator].Digest.Hex
layerSize := strconv.FormatInt(manifest.Layers[layersIterator].Size, 10)
layer := &gql_generated.LayerSummary{
Size: &layerSize,
Digest: &digest,
}
layers = append(
layers,
&gql_generated.LayerSummary{
Size: &layerSize,
Digest: &digest,
},
layer,
)
allHistory[i].Layer = layer
layersIterator++
}
formattedSize := strconv.FormatInt(size, 10)
formattedTagDigest := tagDigest.Hex()
imageInfo := &gql_generated.ImageSummary{
RepoName: &repo,
Tag: &tag,
Digest: &formattedTagDigest,
Digest: &formattedManifestDigest,
ConfigDigest: &manifest.Config.Digest.Hex,
Size: &formattedSize,
Layers: layers,
History: allHistory,
}
return imageInfo

View file

@ -53,6 +53,7 @@ type ImageSummary {
Title: String
Source: String
Documentation: String
History: [LayerHistory]
}
# Brief on a specific repo to be used in queries returning a list of repos
@ -77,6 +78,31 @@ type LayerSummary {
Score: Int
}
type HistoryDescription {
Created: Time
"""
CreatedBy is the command which created the layer.
"""
CreatedBy: String
"""
Author is the author of the build point.
"""
Author: String
"""
Comment is a custom message set when creating the layer.
"""
Comment: String
"""
EmptyLayer is used to mark if the history item created a filesystem diff.
"""
EmptyLayer: Boolean
}
type LayerHistory {
Layer: LayerSummary
HistoryDescription: HistoryDescription
}
type OsArch {
Os: String
Arch: String

View file

@ -158,7 +158,7 @@ func (r *queryResolver) ImageListForCve(ctx context.Context, id string) ([]*gql_
func (r *queryResolver) ImageListWithCVEFixed(ctx context.Context, id string, image string) ([]*gql_generated.ImageSummary, error) {
tagListForCVE := []*gql_generated.ImageSummary{}
r.log.Info().Str("image", image).Msg("extracting list of tags available in image")
r.log.Info().Str("image", image).Msg("extracting list of tags available in repo")
tagsInfo, err := r.cveInfo.LayoutUtils.GetImageTagsWithTimestamp(image)
if err != nil {
@ -232,7 +232,13 @@ func (r *queryResolver) ImageListWithCVEFixed(ctx context.Context, id string, im
return []*gql_generated.ImageSummary{}, err
}
imageInfo := buildImageInfo(image, tag.Name, digest, manifest)
imageConfig, err := r.cveInfo.LayoutUtils.GetImageConfigInfo(image, digest)
if err != nil {
return []*gql_generated.ImageSummary{}, err
}
imageInfo := BuildImageInfo(image, tag.Name, digest, manifest, imageConfig)
tagListForCVE = append(tagListForCVE, imageInfo)
}