From 42c947fa03c9f34cf79426456dc13acc80097c31 Mon Sep 17 00:00:00 2001 From: Alex Stan Date: Wed, 21 Sep 2022 20:53:56 +0300 Subject: [PATCH] Enrich ImageSummary with a new field representing image History Closing #748 Signed-off-by: Alex Stan --- pkg/extensions/search/common/common_test.go | 337 +++++++++- .../search/gql_generated/generated.go | 593 ++++++++++++++++++ .../search/gql_generated/models_gen.go | 18 + pkg/extensions/search/resolver.go | 139 +++- pkg/extensions/search/schema.graphql | 26 + pkg/extensions/search/schema.resolvers.go | 10 +- 6 files changed, 1096 insertions(+), 27 deletions(-) diff --git a/pkg/extensions/search/common/common_test.go b/pkg/extensions/search/common/common_test.go index 4611d19d..a59c7eba 100644 --- a/pkg/extensions/search/common/common_test.go +++ b/pkg/extensions/search/common/common_test.go @@ -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" diff --git a/pkg/extensions/search/gql_generated/generated.go b/pkg/extensions/search/gql_generated/generated.go index 98711f76..cd63fa9e 100644 --- a/pkg/extensions/search/gql_generated/generated.go +++ b/pkg/extensions/search/gql_generated/generated.go @@ -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 diff --git a/pkg/extensions/search/gql_generated/models_gen.go b/pkg/extensions/search/gql_generated/models_gen.go index a17e79bb..7b017133 100644 --- a/pkg/extensions/search/gql_generated/models_gen.go +++ b/pkg/extensions/search/gql_generated/models_gen.go @@ -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 { diff --git a/pkg/extensions/search/resolver.go b/pkg/extensions/search/resolver.go index 8aee953d..850ae978 100644 --- a/pkg/extensions/search/resolver.go +++ b/pkg/extensions/search/resolver.go @@ -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 diff --git a/pkg/extensions/search/schema.graphql b/pkg/extensions/search/schema.graphql index 4330527f..91dac0c1 100644 --- a/pkg/extensions/search/schema.graphql +++ b/pkg/extensions/search/schema.graphql @@ -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 diff --git a/pkg/extensions/search/schema.resolvers.go b/pkg/extensions/search/schema.resolvers.go index 2db8086e..2d7be997 100644 --- a/pkg/extensions/search/schema.resolvers.go +++ b/pkg/extensions/search/schema.resolvers.go @@ -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) }