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

feat(graphql): add an api to return referrers (#1009)

UI can now make use of OCI artifacts and references using `Referrers` gQL query.
It returns a list of descriptors that refer on their `subject` field to another
digest.

Signed-off-by: Alex Stan <alexandrustan96@yahoo.ro>
This commit is contained in:
alexstan12 2022-11-23 20:53:28 +02:00 committed by GitHub
parent 8746a49268
commit f75bce3085
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 1148 additions and 0 deletions

View file

@ -76,6 +76,14 @@ type ExpandedRepoInfoResp struct {
Errors []ErrorGQL `json:"errors"` 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 { type GlobalSearchResultResp struct {
GlobalSearchResult GlobalSearchResult `json:"data"` GlobalSearchResult GlobalSearchResult `json:"data"`
Errors []ErrorGQL `json:"errors"` 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) { func TestExpandedRepoInfo(t *testing.T) {
Convey("Filter out manifests with no tag", t, func() { Convey("Filter out manifests with no tag", t, func() {
tagToBeRemoved := "3.0" tagToBeRemoved := "3.0"

View file

@ -71,3 +71,16 @@ type HistoryDescription struct {
Comment string `json:"comment"` Comment string `json:"comment"`
EmptyLayer bool `json:"emptyLayer"` 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"`
}

View file

@ -43,6 +43,11 @@ type DirectiveRoot struct {
} }
type ComplexityRoot struct { type ComplexityRoot struct {
Annotation struct {
Key func(childComplexity int) int
Value func(childComplexity int) int
}
CVE struct { CVE struct {
Description func(childComplexity int) int Description func(childComplexity int) int
ID func(childComplexity int) int ID func(childComplexity int) int
@ -132,9 +137,18 @@ type ComplexityRoot struct {
ImageListForCve func(childComplexity int, id string) int ImageListForCve func(childComplexity int, id string) int
ImageListForDigest func(childComplexity int, id string) int ImageListForDigest func(childComplexity int, id string) int
ImageListWithCVEFixed func(childComplexity int, id string, image 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 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 { RepoInfo struct {
Images func(childComplexity int) int Images func(childComplexity int) int
Summary func(childComplexity int) int Summary func(childComplexity int) int
@ -166,6 +180,7 @@ type QueryResolver interface {
DerivedImageList(ctx context.Context, image string) ([]*ImageSummary, error) DerivedImageList(ctx context.Context, image string) ([]*ImageSummary, error)
BaseImageList(ctx context.Context, image string) ([]*ImageSummary, error) BaseImageList(ctx context.Context, image string) ([]*ImageSummary, error)
Image(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 { type executableSchema struct {
@ -183,6 +198,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
_ = ec _ = ec
switch typeName + "." + field { 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": case "CVE.Description":
if e.complexity.CVE.Description == nil { if e.complexity.CVE.Description == nil {
break 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 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": case "Query.RepoListWithNewestImage":
if e.complexity.Query.RepoListWithNewestImage == nil { if e.complexity.Query.RepoListWithNewestImage == nil {
break break
@ -646,6 +687,41 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Query.RepoListWithNewestImage(childComplexity), true 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": case "RepoInfo.Images":
if e.complexity.RepoInfo.Images == nil { if e.complexity.RepoInfo.Images == nil {
break break
@ -918,6 +994,19 @@ type LayerHistory {
HistoryDescription: HistoryDescription 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 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 Search for a specific image using its name
""" """
Image(image: String!): ImageSummary Image(image: String!): ImageSummary
"""
Returns a list of descriptors of an image or artifact manifest that are found in a <repo> and have a subject field of <digest>
Can be filtered based on a specific artifact type <type>
"""
Referrers(repo: String!, digest: String!, type: String!): [Referrer]!
} }
`, BuiltIn: false}, `, BuiltIn: false},
} }
@ -1149,6 +1244,39 @@ func (ec *executionContext) field_Query_Image_args(ctx context.Context, rawArgs
return args, nil 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) { func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error var err error
args := map[string]interface{}{} args := map[string]interface{}{}
@ -1202,6 +1330,88 @@ func (ec *executionContext) field___Type_fields_args(ctx context.Context, rawArg
// region **************************** field.gotpl ***************************** // 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) { func (ec *executionContext) _CVE_Id(ctx context.Context, field graphql.CollectedField, obj *Cve) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_CVE_Id(ctx, field) fc, err := ec.fieldContext_CVE_Id(ctx, field)
if err != nil { if err != nil {
@ -4233,6 +4443,73 @@ func (ec *executionContext) fieldContext_Query_Image(ctx context.Context, field
return fc, nil 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) { func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Query___type(ctx, field) fc, err := ec.fieldContext_Query___type(ctx, field)
if err != nil { if err != nil {
@ -4362,6 +4639,220 @@ func (ec *executionContext) fieldContext_Query___schema(ctx context.Context, fie
return fc, nil 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) { func (ec *executionContext) _RepoInfo_Images(ctx context.Context, field graphql.CollectedField, obj *RepoInfo) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_RepoInfo_Images(ctx, field) fc, err := ec.fieldContext_RepoInfo_Images(ctx, field)
if err != nil { if err != nil {
@ -6751,6 +7242,35 @@ func (ec *executionContext) fieldContext___Type_specifiedByURL(ctx context.Conte
// region **************************** object.gotpl **************************** // 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"} var cVEImplementors = []string{"CVE"}
func (ec *executionContext) _CVE(ctx context.Context, sel ast.SelectionSet, obj *Cve) graphql.Marshaler { 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) 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 { out.Concurrently(i, func() graphql.Marshaler {
return rrm(innerCtx) return rrm(innerCtx)
}) })
@ -7427,6 +7970,50 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
return out 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"} var repoInfoImplementors = []string{"RepoInfo"}
func (ec *executionContext) _RepoInfo(ctx context.Context, sel ast.SelectionSet, obj *RepoInfo) graphql.Marshaler { 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 ***************************** // 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) { func (ec *executionContext) unmarshalNBoolean2bool(ctx context.Context, v interface{}) (bool, error) {
res, err := graphql.UnmarshalBoolean(v) res, err := graphql.UnmarshalBoolean(v)
return res, graphql.ErrorOnPath(ctx, err) return res, graphql.ErrorOnPath(ctx, err)
@ -7888,6 +8513,44 @@ func (ec *executionContext) marshalNImageSummary2ᚖzotregistryᚗioᚋzotᚋpkg
return ec._ImageSummary(ctx, sel, v) 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 { 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) return ec._RepoInfo(ctx, sel, &v)
} }
@ -8224,6 +8887,13 @@ func (ec *executionContext) marshalN__TypeKind2string(ctx context.Context, sel a
return res 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) { func (ec *executionContext) unmarshalOBoolean2bool(ctx context.Context, v interface{}) (bool, error) {
res, err := graphql.UnmarshalBoolean(v) res, err := graphql.UnmarshalBoolean(v)
return res, graphql.ErrorOnPath(ctx, err) return res, graphql.ErrorOnPath(ctx, err)
@ -8615,6 +9285,13 @@ func (ec *executionContext) marshalOPackageInfo2ᚖzotregistryᚗioᚋzotᚋpkg
return ec._PackageInfo(ctx, sel, v) 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 { 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 { if v == nil {
return graphql.Null return graphql.Null

View file

@ -6,6 +6,11 @@ import (
"time" "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 // Contains various details about the CVE and a list of PackageInfo about the affected packages
type Cve struct { type Cve struct {
ID *string `json:"Id"` ID *string `json:"Id"`
@ -95,6 +100,14 @@ type PackageInfo struct {
FixedVersion *string `json:"FixedVersion"` 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 // Contains details about the repo: a list of image summaries and a summary of the repo
type RepoInfo struct { type RepoInfo struct {
Images []*ImageSummary `json:"Images"` Images []*ImageSummary `json:"Images"`

View file

@ -576,6 +576,47 @@ func (r *queryResolver) getImageList(store storage.ImageStore, imageName string)
return results, nil 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, func BuildImageInfo(repo string, tag string, manifestDigest godigest.Digest,
manifest ispec.Manifest, imageConfig ispec.Image, isSigned bool, manifest ispec.Manifest, imageConfig ispec.Image, isSigned bool,
) *gql_generated.ImageSummary { ) *gql_generated.ImageSummary {

View file

@ -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) { func TestExtractImageDetails(t *testing.T) {
Convey("repoListWithNewestImage", t, func() { Convey("repoListWithNewestImage", t, func() {
// log := log.Logger{Logger: zerolog.New(os.Stdout)} // log := log.Logger{Logger: zerolog.New(os.Stdout)}

View file

@ -134,6 +134,19 @@ type LayerHistory {
HistoryDescription: HistoryDescription 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 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 Search for a specific image using its name
""" """
Image(image: String!): ImageSummary Image(image: String!): ImageSummary
"""
Returns a list of descriptors of an image or artifact manifest that are found in a <repo> and have a subject field of <digest>
Can be filtered based on a specific artifact type <type>
"""
Referrers(repo: String!, digest: String!, type: String!): [Referrer]!
} }

View file

@ -564,6 +564,20 @@ func (r *queryResolver) Image(ctx context.Context, image string) (*gql_generated
return result, nil 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. // Query returns gql_generated.QueryResolver implementation.
func (r *Resolver) Query() gql_generated.QueryResolver { return &queryResolver{r} } func (r *Resolver) Query() gql_generated.QueryResolver { return &queryResolver{r} }

View file

@ -446,6 +446,55 @@ func UploadImage(img Image, baseURL, repo string) error {
return err 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) { func ReadLogFileAndSearchString(logPath string, stringToMatch string, timeout time.Duration) (bool, error) {
ctx, cancelFunc := context.WithTimeout(context.Background(), timeout) ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
defer cancelFunc() defer cancelFunc()

View file

@ -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) { func TestUploadImage(t *testing.T) {
Convey("Post request results in an error", t, func() { Convey("Post request results in an error", t, func() {
port := test.GetFreePort() port := test.GetFreePort()