diff --git a/pkg/extensions/search/common/common_test.go b/pkg/extensions/search/common/common_test.go index 3ce6f983..f35a0e18 100644 --- a/pkg/extensions/search/common/common_test.go +++ b/pkg/extensions/search/common/common_test.go @@ -76,6 +76,14 @@ type ExpandedRepoInfoResp struct { Errors []ErrorGQL `json:"errors"` } +type ReferrersResp struct { + ReferrersResult ReferrersResult `json:"data"` + Errors []ErrorGQL `json:"errors"` +} + +type ReferrersResult struct { + Referrers []common.Referrer `json:"referrers"` +} type GlobalSearchResultResp struct { GlobalSearchResult GlobalSearchResult `json:"data"` Errors []ErrorGQL `json:"errors"` @@ -658,6 +666,138 @@ func TestRepoListWithNewestImage(t *testing.T) { }) } +func TestGetReferrersGQL(t *testing.T) { + Convey("get referrers", 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{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, + Lint: &extconf.LintConfig{ + BaseConfig: extconf.BaseConfig{ + Enable: &defaultVal, + }, + }, + } + + gqlEndpoint := fmt.Sprintf("%s%s?query=", baseURL, graphqlQueryPrefix) + + conf.Extensions.Search.CVE = nil + + ctlr := api.NewController(conf) + + go startServer(ctlr) + defer stopServer(ctlr) + + WaitTillServerReady(baseURL) + + // ======================= + + config, layers, manifest, err := GetImageComponents(1000) + So(err, ShouldBeNil) + + repo := "artifact-ref" + + err = UploadImage( + Image{ + Manifest: manifest, + Config: config, + Layers: layers, + Tag: "1.0", + }, + baseURL, + repo) + + So(err, ShouldBeNil) + + manifestBlob, err := json.Marshal(manifest) + So(err, ShouldBeNil) + manifestDigest := godigest.FromBytes(manifestBlob) + manifestSize := int64(len(manifestBlob)) + + subjectDescriptor := &ispec.Descriptor{ + MediaType: "application/vnd.oci.image.manifest.v1+json", + Size: manifestSize, + Digest: manifestDigest, + } + + artifactContentBlob := []byte("test artifact") + artifactContentBlobSize := int64(len(artifactContentBlob)) + artifactContentType := "application/octet-stream" + artifactContentBlobDigest := godigest.FromBytes(artifactContentBlob) + artifactType := "com.artifact.test" + + err = UploadBlob(baseURL, repo, artifactContentBlob, artifactContentType) + So(err, ShouldBeNil) + + artifact := &ispec.Artifact{ + Blobs: []ispec.Descriptor{ + { + MediaType: artifactContentType, + Digest: artifactContentBlobDigest, + Size: artifactContentBlobSize, + }, + }, + Subject: subjectDescriptor, + ArtifactType: artifactType, + Annotations: map[string]string{ + "com.artifact.format": "test", + }, + } + + artifactManifestBlob, err := json.Marshal(artifact) + So(err, ShouldBeNil) + artifactManifestDigest := godigest.FromBytes(artifactManifestBlob) + + err = UploadArtifact(baseURL, repo, artifact) + So(err, ShouldBeNil) + + gqlQuery := ` + {Referrers( + repo: "%s", + digest: "%s", + type: "" + ){ + ArtifactType, + Digest, + MediaType, + Size, + Annotations{ + Key + Value + } + } + }` + + strQuery := fmt.Sprintf(gqlQuery, repo, manifestDigest.String()) + + 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) + + refferrsResp := &ReferrersResp{} + + err = json.Unmarshal(resp.Body(), refferrsResp) + So(err, ShouldBeNil) + So(refferrsResp.Errors, ShouldBeNil) + So(refferrsResp.ReferrersResult.Referrers[0].ArtifactType, ShouldEqual, artifactType) + So(refferrsResp.ReferrersResult.Referrers[0].MediaType, ShouldEqual, ispec.MediaTypeArtifactManifest) + + So(refferrsResp.ReferrersResult.Referrers[0].Annotations[0].Key, ShouldEqual, "com.artifact.format") + So(refferrsResp.ReferrersResult.Referrers[0].Annotations[0].Value, ShouldEqual, "test") + + So(refferrsResp.ReferrersResult.Referrers[0].Digest, ShouldEqual, artifactManifestDigest) + }) +} + func TestExpandedRepoInfo(t *testing.T) { Convey("Filter out manifests with no tag", t, func() { tagToBeRemoved := "3.0" diff --git a/pkg/extensions/search/common/model.go b/pkg/extensions/search/common/model.go index 88a7933e..a522513e 100644 --- a/pkg/extensions/search/common/model.go +++ b/pkg/extensions/search/common/model.go @@ -71,3 +71,16 @@ type HistoryDescription struct { Comment string `json:"comment"` EmptyLayer bool `json:"emptyLayer"` } + +type Referrer struct { + MediaType string `json:"mediatype"` + ArtifactType string `json:"artifacttype"` + Size int `json:"size"` + Digest string `json:"digest"` + Annotations []Annotation `json:"annotations"` +} + +type Annotation struct { + Key string `json:"key"` + Value string `json:"value"` +} diff --git a/pkg/extensions/search/gql_generated/generated.go b/pkg/extensions/search/gql_generated/generated.go index 649fb9ce..58504ab9 100644 --- a/pkg/extensions/search/gql_generated/generated.go +++ b/pkg/extensions/search/gql_generated/generated.go @@ -43,6 +43,11 @@ type DirectiveRoot struct { } type ComplexityRoot struct { + Annotation struct { + Key func(childComplexity int) int + Value func(childComplexity int) int + } + CVE struct { Description func(childComplexity int) int ID func(childComplexity int) int @@ -132,9 +137,18 @@ type ComplexityRoot struct { ImageListForCve func(childComplexity int, id string) int ImageListForDigest func(childComplexity int, id string) int ImageListWithCVEFixed func(childComplexity int, id string, image string) int + Referrers func(childComplexity int, repo string, digest string, typeArg string) int RepoListWithNewestImage func(childComplexity int) int } + Referrer struct { + Annotations func(childComplexity int) int + ArtifactType func(childComplexity int) int + Digest func(childComplexity int) int + MediaType func(childComplexity int) int + Size func(childComplexity int) int + } + RepoInfo struct { Images func(childComplexity int) int Summary func(childComplexity int) int @@ -166,6 +180,7 @@ type QueryResolver interface { DerivedImageList(ctx context.Context, image string) ([]*ImageSummary, error) BaseImageList(ctx context.Context, image string) ([]*ImageSummary, error) Image(ctx context.Context, image string) (*ImageSummary, error) + Referrers(ctx context.Context, repo string, digest string, typeArg string) ([]*Referrer, error) } type executableSchema struct { @@ -183,6 +198,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in _ = ec switch typeName + "." + field { + case "Annotation.Key": + if e.complexity.Annotation.Key == nil { + break + } + + return e.complexity.Annotation.Key(childComplexity), true + + case "Annotation.Value": + if e.complexity.Annotation.Value == nil { + break + } + + return e.complexity.Annotation.Value(childComplexity), true + case "CVE.Description": if e.complexity.CVE.Description == nil { break @@ -639,6 +668,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.ImageListWithCVEFixed(childComplexity, args["id"].(string), args["image"].(string)), true + case "Query.Referrers": + if e.complexity.Query.Referrers == nil { + break + } + + args, err := ec.field_Query_Referrers_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.Referrers(childComplexity, args["repo"].(string), args["digest"].(string), args["type"].(string)), true + case "Query.RepoListWithNewestImage": if e.complexity.Query.RepoListWithNewestImage == nil { break @@ -646,6 +687,41 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.RepoListWithNewestImage(childComplexity), true + case "Referrer.Annotations": + if e.complexity.Referrer.Annotations == nil { + break + } + + return e.complexity.Referrer.Annotations(childComplexity), true + + case "Referrer.ArtifactType": + if e.complexity.Referrer.ArtifactType == nil { + break + } + + return e.complexity.Referrer.ArtifactType(childComplexity), true + + case "Referrer.Digest": + if e.complexity.Referrer.Digest == nil { + break + } + + return e.complexity.Referrer.Digest(childComplexity), true + + case "Referrer.MediaType": + if e.complexity.Referrer.MediaType == nil { + break + } + + return e.complexity.Referrer.MediaType(childComplexity), true + + case "Referrer.Size": + if e.complexity.Referrer.Size == nil { + break + } + + return e.complexity.Referrer.Size(childComplexity), true + case "RepoInfo.Images": if e.complexity.RepoInfo.Images == nil { break @@ -918,6 +994,19 @@ type LayerHistory { HistoryDescription: HistoryDescription } +type Annotation { + Key: String + Value: String +} + +type Referrer { + MediaType: String + ArtifactType: String + Size: Int + Digest: String + Annotations: [Annotation]! +} + """ Contains details about the supported OS and architecture of the image """ @@ -981,6 +1070,12 @@ type Query { Search for a specific image using its name """ Image(image: String!): ImageSummary + + """ + Returns a list of descriptors of an image or artifact manifest that are found in a and have a subject field of + Can be filtered based on a specific artifact type + """ + Referrers(repo: String!, digest: String!, type: String!): [Referrer]! } `, BuiltIn: false}, } @@ -1149,6 +1244,39 @@ func (ec *executionContext) field_Query_Image_args(ctx context.Context, rawArgs return args, nil } +func (ec *executionContext) field_Query_Referrers_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["repo"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("repo")) + arg0, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["repo"] = arg0 + var arg1 string + if tmp, ok := rawArgs["digest"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("digest")) + arg1, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["digest"] = arg1 + var arg2 string + if tmp, ok := rawArgs["type"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("type")) + arg2, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["type"] = arg2 + 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{}{} @@ -1202,6 +1330,88 @@ func (ec *executionContext) field___Type_fields_args(ctx context.Context, rawArg // region **************************** field.gotpl ***************************** +func (ec *executionContext) _Annotation_Key(ctx context.Context, field graphql.CollectedField, obj *Annotation) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Annotation_Key(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.Key, 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_Annotation_Key(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Annotation", + 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) _Annotation_Value(ctx context.Context, field graphql.CollectedField, obj *Annotation) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Annotation_Value(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.Value, 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_Annotation_Value(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Annotation", + 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) _CVE_Id(ctx context.Context, field graphql.CollectedField, obj *Cve) (ret graphql.Marshaler) { fc, err := ec.fieldContext_CVE_Id(ctx, field) if err != nil { @@ -4233,6 +4443,73 @@ func (ec *executionContext) fieldContext_Query_Image(ctx context.Context, field return fc, nil } +func (ec *executionContext) _Query_Referrers(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_Referrers(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().Referrers(rctx, fc.Args["repo"].(string), fc.Args["digest"].(string), fc.Args["type"].(string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]*Referrer) + fc.Result = res + return ec.marshalNReferrer2ᚕᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐReferrer(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_Referrers(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 "MediaType": + return ec.fieldContext_Referrer_MediaType(ctx, field) + case "ArtifactType": + return ec.fieldContext_Referrer_ArtifactType(ctx, field) + case "Size": + return ec.fieldContext_Referrer_Size(ctx, field) + case "Digest": + return ec.fieldContext_Referrer_Digest(ctx, field) + case "Annotations": + return ec.fieldContext_Referrer_Annotations(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Referrer", 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_Referrers_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 { @@ -4362,6 +4639,220 @@ func (ec *executionContext) fieldContext_Query___schema(ctx context.Context, fie return fc, nil } +func (ec *executionContext) _Referrer_MediaType(ctx context.Context, field graphql.CollectedField, obj *Referrer) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Referrer_MediaType(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.MediaType, 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_Referrer_MediaType(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Referrer", + 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) _Referrer_ArtifactType(ctx context.Context, field graphql.CollectedField, obj *Referrer) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Referrer_ArtifactType(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.ArtifactType, 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_Referrer_ArtifactType(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Referrer", + 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) _Referrer_Size(ctx context.Context, field graphql.CollectedField, obj *Referrer) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Referrer_Size(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.Size, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*int) + fc.Result = res + return ec.marshalOInt2ᚖint(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Referrer_Size(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Referrer", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Referrer_Digest(ctx context.Context, field graphql.CollectedField, obj *Referrer) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Referrer_Digest(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.Digest, 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_Referrer_Digest(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Referrer", + 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) _Referrer_Annotations(ctx context.Context, field graphql.CollectedField, obj *Referrer) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Referrer_Annotations(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.Annotations, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]*Annotation) + fc.Result = res + return ec.marshalNAnnotation2ᚕᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐAnnotation(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Referrer_Annotations(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Referrer", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "Key": + return ec.fieldContext_Annotation_Key(ctx, field) + case "Value": + return ec.fieldContext_Annotation_Value(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Annotation", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _RepoInfo_Images(ctx context.Context, field graphql.CollectedField, obj *RepoInfo) (ret graphql.Marshaler) { fc, err := ec.fieldContext_RepoInfo_Images(ctx, field) if err != nil { @@ -6751,6 +7242,35 @@ func (ec *executionContext) fieldContext___Type_specifiedByURL(ctx context.Conte // region **************************** object.gotpl **************************** +var annotationImplementors = []string{"Annotation"} + +func (ec *executionContext) _Annotation(ctx context.Context, sel ast.SelectionSet, obj *Annotation) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, annotationImplementors) + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Annotation") + case "Key": + + out.Values[i] = ec._Annotation_Key(ctx, field, obj) + + case "Value": + + out.Values[i] = ec._Annotation_Value(ctx, field, obj) + + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var cVEImplementors = []string{"CVE"} func (ec *executionContext) _CVE(ctx context.Context, sel ast.SelectionSet, obj *Cve) graphql.Marshaler { @@ -7401,6 +7921,29 @@ 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 "Referrers": + 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_Referrers(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, innerFunc) + } + out.Concurrently(i, func() graphql.Marshaler { return rrm(innerCtx) }) @@ -7427,6 +7970,50 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr return out } +var referrerImplementors = []string{"Referrer"} + +func (ec *executionContext) _Referrer(ctx context.Context, sel ast.SelectionSet, obj *Referrer) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, referrerImplementors) + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Referrer") + case "MediaType": + + out.Values[i] = ec._Referrer_MediaType(ctx, field, obj) + + case "ArtifactType": + + out.Values[i] = ec._Referrer_ArtifactType(ctx, field, obj) + + case "Size": + + out.Values[i] = ec._Referrer_Size(ctx, field, obj) + + case "Digest": + + out.Values[i] = ec._Referrer_Digest(ctx, field, obj) + + case "Annotations": + + out.Values[i] = ec._Referrer_Annotations(ctx, field, obj) + + if out.Values[i] == graphql.Null { + invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var repoInfoImplementors = []string{"RepoInfo"} func (ec *executionContext) _RepoInfo(ctx context.Context, sel ast.SelectionSet, obj *RepoInfo) graphql.Marshaler { @@ -7835,6 +8422,44 @@ func (ec *executionContext) ___Type(ctx context.Context, sel ast.SelectionSet, o // region ***************************** type.gotpl ***************************** +func (ec *executionContext) marshalNAnnotation2ᚕᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐAnnotation(ctx context.Context, sel ast.SelectionSet, v []*Annotation) graphql.Marshaler { + 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.marshalOAnnotation2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐAnnotation(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + return ret +} + func (ec *executionContext) unmarshalNBoolean2bool(ctx context.Context, v interface{}) (bool, error) { res, err := graphql.UnmarshalBoolean(v) return res, graphql.ErrorOnPath(ctx, err) @@ -7888,6 +8513,44 @@ func (ec *executionContext) marshalNImageSummary2ᚖzotregistryᚗioᚋzotᚋpkg return ec._ImageSummary(ctx, sel, v) } +func (ec *executionContext) marshalNReferrer2ᚕᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐReferrer(ctx context.Context, sel ast.SelectionSet, v []*Referrer) graphql.Marshaler { + 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.marshalOReferrer2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐReferrer(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + return ret +} + func (ec *executionContext) marshalNRepoInfo2zotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐRepoInfo(ctx context.Context, sel ast.SelectionSet, v RepoInfo) graphql.Marshaler { return ec._RepoInfo(ctx, sel, &v) } @@ -8224,6 +8887,13 @@ func (ec *executionContext) marshalN__TypeKind2string(ctx context.Context, sel a return res } +func (ec *executionContext) marshalOAnnotation2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐAnnotation(ctx context.Context, sel ast.SelectionSet, v *Annotation) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._Annotation(ctx, sel, v) +} + func (ec *executionContext) unmarshalOBoolean2bool(ctx context.Context, v interface{}) (bool, error) { res, err := graphql.UnmarshalBoolean(v) return res, graphql.ErrorOnPath(ctx, err) @@ -8615,6 +9285,13 @@ func (ec *executionContext) marshalOPackageInfo2ᚖzotregistryᚗioᚋzotᚋpkg return ec._PackageInfo(ctx, sel, v) } +func (ec *executionContext) marshalOReferrer2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐReferrer(ctx context.Context, sel ast.SelectionSet, v *Referrer) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._Referrer(ctx, sel, v) +} + func (ec *executionContext) marshalORepoSummary2ᚕᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐRepoSummary(ctx context.Context, sel ast.SelectionSet, v []*RepoSummary) 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 e7cfff45..388ea9e1 100644 --- a/pkg/extensions/search/gql_generated/models_gen.go +++ b/pkg/extensions/search/gql_generated/models_gen.go @@ -6,6 +6,11 @@ import ( "time" ) +type Annotation struct { + Key *string `json:"Key"` + Value *string `json:"Value"` +} + // Contains various details about the CVE and a list of PackageInfo about the affected packages type Cve struct { ID *string `json:"Id"` @@ -95,6 +100,14 @@ type PackageInfo struct { FixedVersion *string `json:"FixedVersion"` } +type Referrer struct { + MediaType *string `json:"MediaType"` + ArtifactType *string `json:"ArtifactType"` + Size *int `json:"Size"` + Digest *string `json:"Digest"` + Annotations []*Annotation `json:"Annotations"` +} + // Contains details about the repo: a list of image summaries and a summary of the repo type RepoInfo struct { Images []*ImageSummary `json:"Images"` diff --git a/pkg/extensions/search/resolver.go b/pkg/extensions/search/resolver.go index 18f28ec1..f9c525fc 100644 --- a/pkg/extensions/search/resolver.go +++ b/pkg/extensions/search/resolver.go @@ -576,6 +576,47 @@ func (r *queryResolver) getImageList(store storage.ImageStore, imageName string) return results, nil } +func getReferrers(store storage.ImageStore, repoName string, digest string, artifactType string, log log.Logger) ( + []*gql_generated.Referrer, error, +) { + results := make([]*gql_generated.Referrer, 0) + + index, err := store.GetReferrers(repoName, godigest.Digest(digest), artifactType) + if err != nil { + log.Error().Err(err).Msg("error extracting referrers list") + + return results, err + } + + for _, manifest := range index.Manifests { + size := int(manifest.Size) + digest := manifest.Digest.String() + annotations := make([]*gql_generated.Annotation, 0) + artifactType := manifest.ArtifactType + mediaType := manifest.MediaType + + for k, v := range manifest.Annotations { + key := k + value := v + + annotations = append(annotations, &gql_generated.Annotation{ + Key: &key, + Value: &value, + }) + } + + results = append(results, &gql_generated.Referrer{ + MediaType: &mediaType, + ArtifactType: &artifactType, + Digest: &digest, + Size: &size, + Annotations: annotations, + }) + } + + return results, nil +} + func BuildImageInfo(repo string, tag string, manifestDigest godigest.Digest, manifest ispec.Manifest, imageConfig ispec.Image, isSigned bool, ) *gql_generated.ImageSummary { diff --git a/pkg/extensions/search/resolver_test.go b/pkg/extensions/search/resolver_test.go index 09698ade..2e8a0e12 100644 --- a/pkg/extensions/search/resolver_test.go +++ b/pkg/extensions/search/resolver_test.go @@ -315,6 +315,52 @@ func TestMatching(t *testing.T) { }) } +func TestGetReferrers(t *testing.T) { + Convey("getReferrers", t, func() { + Convey("GetReferrers returns error", func() { + testLogger := log.NewLogger("debug", "") + mockedStore := mocks.MockedImageStore{ + GetReferrersFn: func(repo string, digest godigest.Digest, artifactType string) (ispec.Index, error) { + return ispec.Index{}, ErrTestError + }, + } + + _, err := getReferrers(mockedStore, "test", "", "", testLogger) + So(err, ShouldNotBeNil) + }) + + Convey("GetReferrers return index of descriptors", func() { + testLogger := log.NewLogger("debug", "") + referrerDescriptor := ispec.Descriptor{ + MediaType: ispec.MediaTypeArtifactManifest, + ArtifactType: "com.artifact.test", + Size: 403, + Digest: godigest.FromString("test"), + Annotations: map[string]string{ + "key": "value", + }, + } + mockedStore := mocks.MockedImageStore{ + GetReferrersFn: func(repo string, digest godigest.Digest, artifactType string) (ispec.Index, error) { + return ispec.Index{ + Manifests: []ispec.Descriptor{ + referrerDescriptor, + }, + }, nil + }, + } + + referrers, err := getReferrers(mockedStore, "test", "", "", testLogger) + So(err, ShouldBeNil) + So(*referrers[0].ArtifactType, ShouldEqual, referrerDescriptor.ArtifactType) + So(*referrers[0].MediaType, ShouldEqual, referrerDescriptor.MediaType) + So(*referrers[0].Size, ShouldEqual, referrerDescriptor.Size) + So(*referrers[0].Digest, ShouldEqual, referrerDescriptor.Digest) + So(*referrers[0].Annotations[0].Value, ShouldEqual, referrerDescriptor.Annotations["key"]) + }) + }) +} + func TestExtractImageDetails(t *testing.T) { Convey("repoListWithNewestImage", t, func() { // log := log.Logger{Logger: zerolog.New(os.Stdout)} diff --git a/pkg/extensions/search/schema.graphql b/pkg/extensions/search/schema.graphql index 50987ba3..ca78847e 100644 --- a/pkg/extensions/search/schema.graphql +++ b/pkg/extensions/search/schema.graphql @@ -134,6 +134,19 @@ type LayerHistory { HistoryDescription: HistoryDescription } +type Annotation { + Key: String + Value: String +} + +type Referrer { + MediaType: String + ArtifactType: String + Size: Int + Digest: String + Annotations: [Annotation]! +} + """ Contains details about the supported OS and architecture of the image """ @@ -197,4 +210,10 @@ type Query { Search for a specific image using its name """ Image(image: String!): ImageSummary + + """ + Returns a list of descriptors of an image or artifact manifest that are found in a and have a subject field of + Can be filtered based on a specific artifact type + """ + Referrers(repo: String!, digest: String!, type: String!): [Referrer]! } diff --git a/pkg/extensions/search/schema.resolvers.go b/pkg/extensions/search/schema.resolvers.go index 4d92d729..2f3e6a10 100644 --- a/pkg/extensions/search/schema.resolvers.go +++ b/pkg/extensions/search/schema.resolvers.go @@ -564,6 +564,20 @@ func (r *queryResolver) Image(ctx context.Context, image string) (*gql_generated return result, nil } +// Referrers is the resolver for the Referrers field. +func (r *queryResolver) Referrers(ctx context.Context, repo string, digest string, typeArg string) ([]*gql_generated.Referrer, error) { + store := r.storeController.GetImageStore(repo) + + referrers, err := getReferrers(store, repo, digest, typeArg, r.log) + if err != nil { + r.log.Error().Err(err).Msg("unable to get referrers from default store") + + return []*gql_generated.Referrer{}, err + } + + return referrers, nil +} + // Query returns gql_generated.QueryResolver implementation. func (r *Resolver) Query() gql_generated.QueryResolver { return &queryResolver{r} } diff --git a/pkg/test/common.go b/pkg/test/common.go index 510cb62b..b5b0e9ec 100644 --- a/pkg/test/common.go +++ b/pkg/test/common.go @@ -446,6 +446,55 @@ func UploadImage(img Image, baseURL, repo string) error { return err } +func UploadArtifact(baseURL, repo string, artifactManifest *imagespec.Artifact) error { + // put manifest + artifactManifestBlob, err := json.Marshal(artifactManifest) + if err != nil { + return err + } + + artifactManifestDigest := godigest.FromBytes(artifactManifestBlob) + + _, err = resty.R(). + SetHeader("Content-type", imagespec.MediaTypeArtifactManifest). + SetBody(artifactManifestBlob). + Put(baseURL + "/v2/" + repo + "/manifests/" + artifactManifestDigest.String()) + + return err +} + +func UploadBlob(baseURL, repo string, blob []byte, artifactBlobMediaType string) error { + resp, err := resty.R().Post(baseURL + "/v2/" + repo + "/blobs/uploads/") + if err != nil { + return err + } + + if resp.StatusCode() != http.StatusAccepted { + return ErrPostBlob + } + + loc := resp.Header().Get("Location") + + blobDigest := godigest.FromBytes(blob).String() + + resp, err = resty.R(). + SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))). + SetHeader("Content-Type", artifactBlobMediaType). + SetQueryParam("digest", blobDigest). + SetBody(blob). + Put(baseURL + loc) + + if err != nil { + return err + } + + if resp.StatusCode() != http.StatusCreated { + return ErrPutBlob + } + + return nil +} + func ReadLogFileAndSearchString(logPath string, stringToMatch string, timeout time.Duration) (bool, error) { ctx, cancelFunc := context.WithTimeout(context.Background(), timeout) defer cancelFunc() diff --git a/pkg/test/common_test.go b/pkg/test/common_test.go index 84dd2617..e1a05055 100644 --- a/pkg/test/common_test.go +++ b/pkg/test/common_test.go @@ -156,6 +156,142 @@ func TestWaitTillTrivyDBDownloadStarted(t *testing.T) { }) } +func TestUploadArtifact(t *testing.T) { + Convey("Put request results in an error", t, func() { + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + + artifact := ispec.Artifact{} + + err := test.UploadArtifact(baseURL, "test", &artifact) + So(err, ShouldNotBeNil) + }) +} + +func TestUploadBlob(t *testing.T) { + Convey("Post request results in an error", t, func() { + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + + err := test.UploadBlob(baseURL, "test", []byte("test"), "zot.com.test") + So(err, ShouldNotBeNil) + }) + + Convey("Post request status differs from accepted", t, func() { + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + + tempDir := t.TempDir() + conf := config.New() + conf.HTTP.Port = port + conf.Storage.RootDirectory = tempDir + + err := os.Chmod(tempDir, 0o400) + if err != nil { + t.Fatal(err) + } + + ctlr := api.NewController(conf) + go startServer(ctlr) + defer stopServer(ctlr) + + test.WaitTillServerReady(baseURL) + + err = test.UploadBlob(baseURL, "test", []byte("test"), "zot.com.test") + So(err, ShouldEqual, test.ErrPostBlob) + }) + + Convey("Put request results in an error", t, func() { + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + + tempDir := t.TempDir() + conf := config.New() + conf.HTTP.Port = port + conf.Storage.RootDirectory = tempDir + + ctlr := api.NewController(conf) + go startServer(ctlr) + defer stopServer(ctlr) + + test.WaitTillServerReady(baseURL) + + blob := new([]byte) + + err := test.UploadBlob(baseURL, "test", *blob, "zot.com.test") + So(err, ShouldNotBeNil) + }) + + Convey("Put request status differs from accepted", t, func() { + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + + tempDir := t.TempDir() + conf := config.New() + conf.HTTP.Port = port + conf.Storage.RootDirectory = tempDir + + ctlr := api.NewController(conf) + go startServer(ctlr) + defer stopServer(ctlr) + + test.WaitTillServerReady(baseURL) + + blob := []byte("test") + blobDigest := godigest.FromBytes(blob) + layerPath := path.Join(tempDir, "test", "blobs", "sha256") + blobPath := path.Join(layerPath, blobDigest.String()) + if _, err := os.Stat(layerPath); os.IsNotExist(err) { + err = os.MkdirAll(layerPath, 0o700) + if err != nil { + t.Fatal(err) + } + + file, err := os.Create(blobPath) + if err != nil { + t.Fatal(err) + } + + err = os.Chmod(layerPath, 0o000) + if err != nil { + t.Fatal(err) + } + defer func() { + err = os.Chmod(layerPath, 0o700) + if err != nil { + t.Fatal(err) + } + + os.RemoveAll(file.Name()) + }() + } + + err := test.UploadBlob(baseURL, "test", blob, "zot.com.test") + So(err, ShouldEqual, test.ErrPutBlob) + }) + + Convey("Put request successful", t, func() { + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + + tempDir := t.TempDir() + conf := config.New() + conf.HTTP.Port = port + conf.Storage.RootDirectory = tempDir + + ctlr := api.NewController(conf) + go startServer(ctlr) + defer stopServer(ctlr) + + test.WaitTillServerReady(baseURL) + + blob := []byte("test") + + err := test.UploadBlob(baseURL, "test", blob, "zot.com.test") + So(err, ShouldEqual, nil) + }) +} + func TestUploadImage(t *testing.T) { Convey("Post request results in an error", t, func() { port := test.GetFreePort()