From 0f6057a22ca029577cb0d1236d265e4127678b01 Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Wed, 29 Jan 2020 11:34:55 +0800 Subject: [PATCH] Implement get addition API for image This commit implements the API to get build history of image with manifest version 2 and populates the addition links when listing/getting the artifact Signed-off-by: Wenkai Yin --- api/v2.0/swagger.yaml | 50 +++++++++-- src/api/artifact/abstractor/abstractor.go | 25 ++++-- .../artifact/abstractor/abstractor_test.go | 80 +++++++++-------- .../abstractor/resolver/chart/chart.go | 33 +++++-- .../abstractor/resolver/chart/chart_test.go | 17 ++-- .../abstractor/resolver/image/index.go | 33 +++++-- .../abstractor/resolver/image/index_test.go | 23 +++-- .../abstractor/resolver/image/manifest_v1.go | 27 ++++-- .../resolver/image/manifest_v1_test.go | 23 +++-- .../abstractor/resolver/image/manifest_v2.go | 68 +++++++++++++-- .../resolver/image/manifest_v2_test.go | 75 ++++++++++------ .../artifact/abstractor/resolver/resolver.go | 18 ++-- .../abstractor/resolver/resolver_test.go | 9 +- src/api/artifact/controller.go | 81 +++++++++++++++-- src/api/artifact/controller_test.go | 86 ++++++++++++------- src/api/artifact/descriptor/descriptor.go | 72 ++++++++++++++++ src/api/artifact/model.go | 30 +++---- src/common/rbac/const.go | 1 + src/common/rbac/project/util.go | 1 + src/common/rbac/project/visitor_role.go | 5 ++ src/internal/context.go | 53 ++++++++++++ src/internal/context_test.go | 41 +++++++++ .../middleware/apiversion/api_version.go | 31 +++++++ .../middleware/apiversion/api_version_test.go | 34 ++++++++ src/server/v2.0/handler/artifact.go | 21 +++++ src/server/v2.0/handler/repository.go | 4 + src/server/v2.0/route/route.go | 9 +- .../api/artifact/abstractor/blob/fetcher.go | 2 +- .../artifact/abstractor/resolver/resolver.go | 43 ++++++++++ src/testing/api/artifact/controller.go | 9 +- 30 files changed, 806 insertions(+), 198 deletions(-) create mode 100644 src/api/artifact/descriptor/descriptor.go create mode 100644 src/internal/context.go create mode 100644 src/internal/context_test.go create mode 100644 src/server/middleware/apiversion/api_version.go create mode 100644 src/server/middleware/apiversion/api_version_test.go create mode 100644 src/testing/api/artifact/abstractor/resolver/resolver.go diff --git a/api/v2.0/swagger.yaml b/api/v2.0/swagger.yaml index adfe53a65..49fda123d 100644 --- a/api/v2.0/swagger.yaml +++ b/api/v2.0/swagger.yaml @@ -249,6 +249,39 @@ paths: $ref: '#/responses/404' '500': $ref: '#/responses/500' + /projects/{project_name}/repositories/{repository_name}/artifacts/{reference}/additions/{addition}: + get: + summary: Get the addition of the specific artifact + description: Get the addition of the artifact specified by the reference under the project and repository. + tags: + - artifact + operationId: getAddition + parameters: + - $ref: '#/parameters/requestId' + - $ref: '#/parameters/projectName' + - $ref: '#/parameters/repositoryName' + - $ref: '#/parameters/reference' + - name: addition + in: path + description: The addition name, "build_history" for images; "values.yaml", "readme", "dependencies" for charts + type: string + enum: [build_history, values.yaml, readme, dependencies] + required: true + responses: + '200': + description: Success + schema: + type: string + '400': + $ref: '#/responses/400' + '401': + $ref: '#/responses/401' + '403': + $ref: '#/responses/403' + '404': + $ref: '#/responses/404' + '500': + $ref: '#/responses/500' parameters: requestId: name: X-Request-Id @@ -425,8 +458,8 @@ definitions: type: array items: $ref: '#/definitions/Tag' - sub_resource_links: - $ref: '#/definitions/SubResourceLinks' + addition_links: + $ref: '#/definitions/AdditionLinks' Tag: type: object properties: @@ -455,6 +488,7 @@ definitions: description: The latest pull time of the tag immutable: type: boolean + x-omitempty: false description: The immutable status of the tag ExtraAttrs: type: object @@ -464,20 +498,19 @@ definitions: type: object additionalProperties: type: string - SubResourceLinks: + AdditionLinks: type: object additionalProperties: - type: array - items: - $ref: '#/definitions/ResourceLink' - ResourceLink: + $ref: '#/definitions/AdditionLink' + AdditionLink: type: object properties: href: type: string - description: The link of the resource + description: The link of the addition absolute: type: boolean + x-omitempty: false description: Determine whether the link is an absolute URL or not Reference: type: object @@ -515,3 +548,4 @@ definitions: variant: type: string description: The variant of the CPU + diff --git a/src/api/artifact/abstractor/abstractor.go b/src/api/artifact/abstractor/abstractor.go index 76fdaacd3..7ee3e55a9 100644 --- a/src/api/artifact/abstractor/abstractor.go +++ b/src/api/artifact/abstractor/abstractor.go @@ -23,6 +23,7 @@ import ( "github.com/docker/distribution/manifest/schema2" "github.com/goharbor/harbor/src/api/artifact/abstractor/blob" "github.com/goharbor/harbor/src/api/artifact/abstractor/resolver" + ierror "github.com/goharbor/harbor/src/internal/error" "github.com/goharbor/harbor/src/pkg/artifact" "github.com/goharbor/harbor/src/pkg/repository" "github.com/opencontainers/image-spec/specs-go/v1" @@ -37,9 +38,13 @@ var artifactTypeRegExp = regexp.MustCompile(`^application/vnd\.[^.]*\.(.*)\.conf // Abstractor abstracts the specific information for different types of artifacts type Abstractor interface { - // Abstract the specific information for the specific artifact type into the artifact model, - // the information can be got from the manifest or other layers referenced by the manifest. - Abstract(ctx context.Context, artifact *artifact.Artifact) error + // AbstractMetadata abstracts the metadata for the specific artifact type into the artifact model, + // the metadata can be got from the manifest or other layers referenced by the manifest. + AbstractMetadata(ctx context.Context, artifact *artifact.Artifact) error + // AbstractAddition abstracts the addition of the artifact. + // The additions are different for different artifacts: + // build history for image; values.yaml, readme and dependencies for chart, etc + AbstractAddition(ctx context.Context, artifact *artifact.Artifact, additionType string) (addition *resolver.Addition, err error) } // NewAbstractor returns an instance of the default abstractor @@ -58,7 +63,7 @@ type abstractor struct { // TODO try CNAB, how to forbid CNAB // TODO add white list for supported artifact type -func (a *abstractor) Abstract(ctx context.Context, artifact *artifact.Artifact) error { +func (a *abstractor) AbstractMetadata(ctx context.Context, artifact *artifact.Artifact) error { repository, err := a.repoMgr.Get(ctx, artifact.RepositoryID) if err != nil { return err @@ -117,8 +122,7 @@ func (a *abstractor) Abstract(ctx context.Context, artifact *artifact.Artifact) resolver := resolver.Get(artifact.MediaType) if resolver != nil { - artifact.Type = resolver.ArtifactType() - return resolver.Resolve(ctx, content, artifact) + return resolver.ResolveMetadata(ctx, content, artifact) } // if got no resolver, try to parse the artifact type based on the media type @@ -126,6 +130,15 @@ func (a *abstractor) Abstract(ctx context.Context, artifact *artifact.Artifact) return nil } +func (a *abstractor) AbstractAddition(ctx context.Context, artifact *artifact.Artifact, addition string) (*resolver.Addition, error) { + resolver := resolver.Get(artifact.MediaType) + if resolver == nil { + return nil, ierror.New(nil).WithCode(ierror.BadRequestCode). + WithMessage("the resolver for artifact %s not found, cannot get the addition", artifact.Type) + } + return resolver.ResolveAddition(ctx, artifact, addition) +} + func parseArtifactType(mediaType string) string { strs := artifactTypeRegExp.FindStringSubmatch(mediaType) if len(strs) == 2 { diff --git a/src/api/artifact/abstractor/abstractor_test.go b/src/api/artifact/abstractor/abstractor_test.go index 972a41fb2..de11b2fc0 100644 --- a/src/api/artifact/abstractor/abstractor_test.go +++ b/src/api/artifact/abstractor/abstractor_test.go @@ -15,14 +15,15 @@ package abstractor import ( - "context" "github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema2" "github.com/goharbor/harbor/src/api/artifact/abstractor/resolver" "github.com/goharbor/harbor/src/common/models" + ierror "github.com/goharbor/harbor/src/internal/error" "github.com/goharbor/harbor/src/pkg/artifact" "github.com/goharbor/harbor/src/testing/api/artifact/abstractor/blob" - repotesting "github.com/goharbor/harbor/src/testing/pkg/repository" + tresolver "github.com/goharbor/harbor/src/testing/api/artifact/abstractor/resolver" + "github.com/goharbor/harbor/src/testing/pkg/repository" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/suite" "testing" @@ -198,31 +199,18 @@ var ( }` ) -type fakeResolver struct{} - -func (f *fakeResolver) ArtifactType() string { - return fakeArtifactType - -} -func (f *fakeResolver) Resolve(ctx context.Context, manifest []byte, artifact *artifact.Artifact) error { - return nil -} - type abstractorTestSuite struct { suite.Suite abstractor Abstractor fetcher *blob.FakeFetcher - repoMgr *repotesting.FakeManager -} - -func (a *abstractorTestSuite) SetupSuite() { - resolver.Register(&fakeResolver{}, schema1.MediaTypeSignedManifest, - schema2.MediaTypeImageConfig, v1.MediaTypeImageIndex) + repoMgr *repository.FakeManager + resolver *tresolver.FakeResolver } func (a *abstractorTestSuite) SetupTest() { a.fetcher = &blob.FakeFetcher{} - a.repoMgr = &repotesting.FakeManager{} + a.repoMgr = &repository.FakeManager{} + a.resolver = &tresolver.FakeResolver{} a.abstractor = &abstractor{ repoMgr: a.repoMgr, blobFetcher: a.fetcher, @@ -230,55 +218,55 @@ func (a *abstractorTestSuite) SetupTest() { } // docker manifest v1 -func (a *abstractorTestSuite) TestAbstractV1Manifest() { +func (a *abstractorTestSuite) TestAbstractMetadataOfV1Manifest() { + resolver.Register(a.resolver, schema1.MediaTypeSignedManifest) a.repoMgr.On("Get").Return(&models.RepoRecord{}, nil) a.fetcher.On("FetchManifest").Return(schema1.MediaTypeSignedManifest, []byte(v1Manifest), nil) + a.resolver.On("ArtifactType").Return(fakeArtifactType) + a.resolver.On("ResolveMetadata").Return(nil) artifact := &artifact.Artifact{ ID: 1, } - err := a.abstractor.Abstract(nil, artifact) + err := a.abstractor.AbstractMetadata(nil, artifact) a.Require().Nil(err) - a.repoMgr.AssertExpectations(a.T()) - a.fetcher.AssertExpectations(a.T()) a.Assert().Equal(int64(1), artifact.ID) - a.Assert().Equal(fakeArtifactType, artifact.Type) a.Assert().Equal(schema1.MediaTypeSignedManifest, artifact.ManifestMediaType) a.Assert().Equal(schema1.MediaTypeSignedManifest, artifact.MediaType) a.Assert().Equal(int64(0), artifact.Size) } // docker manifest v2 -func (a *abstractorTestSuite) TestAbstractV2Manifest() { +func (a *abstractorTestSuite) TestAbstractMetadataOfV2Manifest() { + resolver.Register(a.resolver, schema2.MediaTypeImageConfig) a.repoMgr.On("Get").Return(&models.RepoRecord{}, nil) a.fetcher.On("FetchManifest").Return(schema2.MediaTypeManifest, []byte(v2Manifest), nil) + a.resolver.On("ArtifactType").Return(fakeArtifactType) + a.resolver.On("ResolveMetadata").Return(nil) artifact := &artifact.Artifact{ ID: 1, } - err := a.abstractor.Abstract(nil, artifact) + err := a.abstractor.AbstractMetadata(nil, artifact) a.Require().Nil(err) - a.repoMgr.AssertExpectations(a.T()) - a.fetcher.AssertExpectations(a.T()) a.Assert().Equal(int64(1), artifact.ID) - a.Assert().Equal(fakeArtifactType, artifact.Type) a.Assert().Equal(schema2.MediaTypeManifest, artifact.ManifestMediaType) a.Assert().Equal(schema2.MediaTypeImageConfig, artifact.MediaType) a.Assert().Equal(int64(3043), artifact.Size) } // OCI index -func (a *abstractorTestSuite) TestAbstractIndex() { +func (a *abstractorTestSuite) TestAbstractMetadataOfIndex() { + resolver.Register(a.resolver, v1.MediaTypeImageIndex) a.repoMgr.On("Get").Return(&models.RepoRecord{}, nil) a.fetcher.On("FetchManifest").Return(v1.MediaTypeImageIndex, []byte(index), nil) + a.resolver.On("ArtifactType").Return(fakeArtifactType) + a.resolver.On("ResolveMetadata").Return(nil) artifact := &artifact.Artifact{ ID: 1, } - err := a.abstractor.Abstract(nil, artifact) + err := a.abstractor.AbstractMetadata(nil, artifact) a.Require().Nil(err) - a.repoMgr.AssertExpectations(a.T()) - a.fetcher.AssertExpectations(a.T()) a.Assert().Equal(int64(1), artifact.ID) - a.Assert().Equal(fakeArtifactType, artifact.Type) a.Assert().Equal(v1.MediaTypeImageIndex, artifact.ManifestMediaType) a.Assert().Equal(v1.MediaTypeImageIndex, artifact.MediaType) a.Assert().Equal(int64(0), artifact.Size) @@ -286,16 +274,14 @@ func (a *abstractorTestSuite) TestAbstractIndex() { } // OCI index -func (a *abstractorTestSuite) TestAbstractUnsupported() { +func (a *abstractorTestSuite) TestAbstractMetadataOfUnsupported() { a.repoMgr.On("Get").Return(&models.RepoRecord{}, nil) a.fetcher.On("FetchManifest").Return("unsupported-manifest", []byte{}, nil) artifact := &artifact.Artifact{ ID: 1, } - err := a.abstractor.Abstract(nil, artifact) + err := a.abstractor.AbstractMetadata(nil, artifact) a.Require().NotNil(err) - a.repoMgr.AssertExpectations(a.T()) - a.fetcher.AssertExpectations(a.T()) } func (a *abstractorTestSuite) TestParseArtifactType() { @@ -320,6 +306,24 @@ func (a *abstractorTestSuite) TestParseArtifactType() { a.Equal("SIF", typee) } +func (a *abstractorTestSuite) TestAbstractAddition() { + resolver.Register(a.resolver, v1.MediaTypeImageConfig) + // cannot get the resolver + art := &artifact.Artifact{ + MediaType: "unknown", + } + _, err := a.abstractor.AbstractAddition(nil, art, "addition") + a.True(ierror.IsErr(err, ierror.BadRequestCode)) + + // get the resolver + art = &artifact.Artifact{ + MediaType: v1.MediaTypeImageConfig, + } + a.resolver.On("ResolveAddition").Return(nil, nil) + _, err = a.abstractor.AbstractAddition(nil, art, "addition") + a.Require().Nil(err) +} + func TestAbstractorTestSuite(t *testing.T) { suite.Run(t, &abstractorTestSuite{}) } diff --git a/src/api/artifact/abstractor/resolver/chart/chart.go b/src/api/artifact/abstractor/resolver/chart/chart.go index 266fcf3ba..8c557acad 100644 --- a/src/api/artifact/abstractor/resolver/chart/chart.go +++ b/src/api/artifact/abstractor/resolver/chart/chart.go @@ -19,15 +19,20 @@ import ( "encoding/json" "github.com/goharbor/harbor/src/api/artifact/abstractor/blob" resolv "github.com/goharbor/harbor/src/api/artifact/abstractor/resolver" + "github.com/goharbor/harbor/src/api/artifact/descriptor" "github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/pkg/artifact" "github.com/goharbor/harbor/src/pkg/repository" v1 "github.com/opencontainers/image-spec/specs-go/v1" ) +// const definitions const ( // ArtifactTypeChart defines the artifact type for helm chart - ArtifactTypeChart = "CHART" + ArtifactTypeChart = "CHART" + AdditionTypeValues = "VALUES.YAML" + AdditionTypeReadme = "README" + AdditionTypeDependencies = "DEPENDENCIES" // TODO import it from helm chart repository mediaType = "application/vnd.cncf.helm.config.v1+json" ) @@ -38,7 +43,11 @@ func init() { blobFetcher: blob.Fcher, } if err := resolv.Register(resolver, mediaType); err != nil { - log.Errorf("failed to register resolver for artifact %s: %v", resolver.ArtifactType(), err) + log.Errorf("failed to register resolver for media type %s: %v", mediaType, err) + return + } + if err := descriptor.Register(resolver, mediaType); err != nil { + log.Errorf("failed to register descriptor for media type %s: %v", mediaType, err) return } } @@ -48,11 +57,7 @@ type resolver struct { blobFetcher blob.Fetcher } -func (r *resolver) ArtifactType() string { - return ArtifactTypeChart -} - -func (r *resolver) Resolve(ctx context.Context, manifest []byte, artifact *artifact.Artifact) error { +func (r *resolver) ResolveMetadata(ctx context.Context, manifest []byte, artifact *artifact.Artifact) error { repository, err := r.repoMgr.Get(ctx, artifact.RepositoryID) if err != nil { return err @@ -66,7 +71,6 @@ func (r *resolver) Resolve(ctx context.Context, manifest []byte, artifact *artif if err != nil { return err } - // TODO should we abstract all values? metadata := map[string]interface{}{} if err := json.Unmarshal(layer, &metadata); err != nil { return err @@ -80,3 +84,16 @@ func (r *resolver) Resolve(ctx context.Context, manifest []byte, artifact *artif return nil } + +func (r *resolver) ResolveAddition(ctx context.Context, artifact *artifact.Artifact, addition string) (*resolv.Addition, error) { + // TODO implement + return nil, nil +} + +func (r *resolver) GetArtifactType() string { + return ArtifactTypeChart +} + +func (r *resolver) ListAdditionTypes() []string { + return []string{AdditionTypeValues, AdditionTypeReadme, AdditionTypeDependencies} +} diff --git a/src/api/artifact/abstractor/resolver/chart/chart_test.go b/src/api/artifact/abstractor/resolver/chart/chart_test.go index 9cf1f91a9..2bdc8b7c7 100644 --- a/src/api/artifact/abstractor/resolver/chart/chart_test.go +++ b/src/api/artifact/abstractor/resolver/chart/chart_test.go @@ -40,11 +40,7 @@ func (r *resolverTestSuite) SetupTest() { } -func (r *resolverTestSuite) TestArtifactType() { - r.Assert().Equal(ArtifactTypeChart, r.resolver.ArtifactType()) -} - -func (r *resolverTestSuite) TestResolve() { +func (r *resolverTestSuite) TestResolveMetadata() { content := `{ "schemaVersion": 2, "config": { @@ -91,7 +87,7 @@ func (r *resolverTestSuite) TestResolve() { artifact := &artifact.Artifact{} r.repoMgr.On("Get").Return(&models.RepoRecord{}, nil) r.blobFetcher.On("FetchLayer").Return([]byte(config), nil) - err := r.resolver.Resolve(nil, []byte(content), artifact) + err := r.resolver.ResolveMetadata(nil, []byte(content), artifact) r.Require().Nil(err) r.repoMgr.AssertExpectations(r.T()) r.blobFetcher.AssertExpectations(r.T()) @@ -99,6 +95,15 @@ func (r *resolverTestSuite) TestResolve() { r.Assert().Equal("1.8.2", artifact.ExtraAttrs["appVersion"].(string)) } +func (r *resolverTestSuite) TestGetArtifactType() { + r.Assert().Equal(ArtifactTypeChart, r.resolver.GetArtifactType()) +} + +func (r *resolverTestSuite) TestListAdditionTypes() { + additions := r.resolver.ListAdditionTypes() + r.EqualValues([]string{AdditionTypeValues, AdditionTypeReadme, AdditionTypeDependencies}, additions) +} + func TestResolverTestSuite(t *testing.T) { suite.Run(t, &resolverTestSuite{}) } diff --git a/src/api/artifact/abstractor/resolver/image/index.go b/src/api/artifact/abstractor/resolver/image/index.go index 54b2a6288..de10d2293 100644 --- a/src/api/artifact/abstractor/resolver/image/index.go +++ b/src/api/artifact/abstractor/resolver/image/index.go @@ -20,7 +20,9 @@ import ( "fmt" "github.com/docker/distribution/manifest/manifestlist" "github.com/goharbor/harbor/src/api/artifact/abstractor/resolver" + "github.com/goharbor/harbor/src/api/artifact/descriptor" "github.com/goharbor/harbor/src/common/utils/log" + ierror "github.com/goharbor/harbor/src/internal/error" "github.com/goharbor/harbor/src/pkg/artifact" "github.com/goharbor/harbor/src/pkg/q" v1 "github.com/opencontainers/image-spec/specs-go/v1" @@ -30,8 +32,16 @@ func init() { rslver := &indexResolver{ artMgr: artifact.Mgr, } - if err := resolver.Register(rslver, v1.MediaTypeImageIndex, manifestlist.MediaTypeManifestList); err != nil { - log.Errorf("failed to register resolver for artifact %s: %v", rslver.ArtifactType(), err) + mediaTypes := []string{ + v1.MediaTypeImageIndex, + manifestlist.MediaTypeManifestList, + } + if err := resolver.Register(rslver, mediaTypes...); err != nil { + log.Errorf("failed to register resolver for media type %v: %v", mediaTypes, err) + return + } + if err := descriptor.Register(rslver, mediaTypes...); err != nil { + log.Errorf("failed to register descriptor for media type %v: %v", mediaTypes, err) return } } @@ -41,11 +51,7 @@ type indexResolver struct { artMgr artifact.Manager } -func (i *indexResolver) ArtifactType() string { - return ArtifactTypeImage -} - -func (i *indexResolver) Resolve(ctx context.Context, manifest []byte, art *artifact.Artifact) error { +func (i *indexResolver) ResolveMetadata(ctx context.Context, manifest []byte, art *artifact.Artifact) error { index := &v1.Index{} if err := json.Unmarshal(manifest, index); err != nil { return err @@ -74,3 +80,16 @@ func (i *indexResolver) Resolve(ctx context.Context, manifest []byte, art *artif } return nil } + +func (i *indexResolver) ResolveAddition(ctx context.Context, artifact *artifact.Artifact, addition string) (*resolver.Addition, error) { + return nil, ierror.New(nil).WithCode(ierror.BadRequestCode). + WithMessage("addition %s isn't supported for %s(index)", addition, ArtifactTypeImage) +} + +func (i *indexResolver) GetArtifactType() string { + return ArtifactTypeImage +} + +func (i *indexResolver) ListAdditionTypes() []string { + return nil +} diff --git a/src/api/artifact/abstractor/resolver/image/index_test.go b/src/api/artifact/abstractor/resolver/image/index_test.go index 292ff76d9..54e88c00c 100644 --- a/src/api/artifact/abstractor/resolver/image/index_test.go +++ b/src/api/artifact/abstractor/resolver/image/index_test.go @@ -15,6 +15,7 @@ package image import ( + ierror "github.com/goharbor/harbor/src/internal/error" "github.com/goharbor/harbor/src/pkg/artifact" arttesting "github.com/goharbor/harbor/src/testing/pkg/artifact" "github.com/stretchr/testify/suite" @@ -35,11 +36,7 @@ func (i *indexResolverTestSuite) SetupTest() { } -func (i *indexResolverTestSuite) TestArtifactType() { - i.Assert().Equal(ArtifactTypeImage, i.resolver.ArtifactType()) -} - -func (i *indexResolverTestSuite) TestResolve() { +func (i *indexResolverTestSuite) TestResolveMetadata() { manifest := `{ "manifests": [ { @@ -128,7 +125,7 @@ func (i *indexResolverTestSuite) TestResolve() { ID: 1, }, }, nil) - err := i.resolver.Resolve(nil, []byte(manifest), art) + err := i.resolver.ResolveMetadata(nil, []byte(manifest), art) i.Require().Nil(err) i.artMgr.AssertExpectations(i.T()) i.Assert().Len(art.References, 8) @@ -136,6 +133,20 @@ func (i *indexResolverTestSuite) TestResolve() { i.Assert().Equal("amd64", art.References[0].Platform.Architecture) } +func (i *indexResolverTestSuite) TestResolveAddition() { + _, err := i.resolver.ResolveAddition(nil, nil, AdditionTypeBuildHistory) + i.True(ierror.IsErr(err, ierror.BadRequestCode)) +} + +func (i *indexResolverTestSuite) TestGetArtifactType() { + i.Assert().Equal(ArtifactTypeImage, i.resolver.GetArtifactType()) +} + +func (i *indexResolverTestSuite) TestListAdditionTypes() { + additions := i.resolver.ListAdditionTypes() + i.Len(additions, 0) +} + func TestIndexResolverTestSuite(t *testing.T) { suite.Run(t, &indexResolverTestSuite{}) } diff --git a/src/api/artifact/abstractor/resolver/image/manifest_v1.go b/src/api/artifact/abstractor/resolver/image/manifest_v1.go index f7fa9527c..d47aca703 100644 --- a/src/api/artifact/abstractor/resolver/image/manifest_v1.go +++ b/src/api/artifact/abstractor/resolver/image/manifest_v1.go @@ -19,14 +19,20 @@ import ( "encoding/json" "github.com/docker/distribution/manifest/schema1" "github.com/goharbor/harbor/src/api/artifact/abstractor/resolver" + "github.com/goharbor/harbor/src/api/artifact/descriptor" "github.com/goharbor/harbor/src/common/utils/log" + ierror "github.com/goharbor/harbor/src/internal/error" "github.com/goharbor/harbor/src/pkg/artifact" ) func init() { rslver := &manifestV1Resolver{} if err := resolver.Register(rslver, schema1.MediaTypeSignedManifest); err != nil { - log.Errorf("failed to register resolver for artifact %s: %v", rslver.ArtifactType(), err) + log.Errorf("failed to register resolver for media type %s: %v", schema1.MediaTypeSignedManifest, err) + return + } + if err := descriptor.Register(rslver, schema1.MediaTypeSignedManifest); err != nil { + log.Errorf("failed to register descriptor for media type %s: %v", schema1.MediaTypeSignedManifest, err) return } } @@ -35,11 +41,7 @@ func init() { type manifestV1Resolver struct { } -func (m *manifestV1Resolver) ArtifactType() string { - return ArtifactTypeImage -} - -func (m *manifestV1Resolver) Resolve(ctx context.Context, manifest []byte, artifact *artifact.Artifact) error { +func (m *manifestV1Resolver) ResolveMetadata(ctx context.Context, manifest []byte, artifact *artifact.Artifact) error { mani := &schema1.Manifest{} if err := json.Unmarshal([]byte(manifest), mani); err != nil { return err @@ -50,3 +52,16 @@ func (m *manifestV1Resolver) Resolve(ctx context.Context, manifest []byte, artif artifact.ExtraAttrs["architecture"] = mani.Architecture return nil } + +func (m *manifestV1Resolver) ResolveAddition(ctx context.Context, artifact *artifact.Artifact, addition string) (*resolver.Addition, error) { + return nil, ierror.New(nil).WithCode(ierror.BadRequestCode). + WithMessage("addition %s isn't supported for %s(manifest version 1)", addition, ArtifactTypeImage) +} + +func (m *manifestV1Resolver) GetArtifactType() string { + return ArtifactTypeImage +} + +func (m *manifestV1Resolver) ListAdditionTypes() []string { + return nil +} diff --git a/src/api/artifact/abstractor/resolver/image/manifest_v1_test.go b/src/api/artifact/abstractor/resolver/image/manifest_v1_test.go index eb4aec8ac..598dfc834 100644 --- a/src/api/artifact/abstractor/resolver/image/manifest_v1_test.go +++ b/src/api/artifact/abstractor/resolver/image/manifest_v1_test.go @@ -15,6 +15,7 @@ package image import ( + ierror "github.com/goharbor/harbor/src/internal/error" "github.com/goharbor/harbor/src/pkg/artifact" "github.com/stretchr/testify/suite" "testing" @@ -30,11 +31,7 @@ func (m *manifestV1ResolverTestSuite) SetupSuite() { } -func (m *manifestV1ResolverTestSuite) TestArtifactType() { - m.Assert().Equal(ArtifactTypeImage, m.resolver.ArtifactType()) -} - -func (m *manifestV1ResolverTestSuite) TestResolve() { +func (m *manifestV1ResolverTestSuite) TestResolveMetadata() { manifest := `{ "name": "hello-world", "tag": "latest", @@ -81,11 +78,25 @@ func (m *manifestV1ResolverTestSuite) TestResolve() { } ` artifact := &artifact.Artifact{} - err := m.resolver.Resolve(nil, []byte(manifest), artifact) + err := m.resolver.ResolveMetadata(nil, []byte(manifest), artifact) m.Require().Nil(err) m.Assert().Equal("amd64", artifact.ExtraAttrs["architecture"].(string)) } +func (m *manifestV1ResolverTestSuite) TestResolveAddition() { + _, err := m.resolver.ResolveAddition(nil, nil, AdditionTypeBuildHistory) + m.True(ierror.IsErr(err, ierror.BadRequestCode)) +} + +func (m *manifestV1ResolverTestSuite) TestGetArtifactType() { + m.Assert().Equal(ArtifactTypeImage, m.resolver.GetArtifactType()) +} + +func (m *manifestV1ResolverTestSuite) TestListAdditionTypes() { + additions := m.resolver.ListAdditionTypes() + m.Len(additions, 0) +} + func TestManifestV1ResolverTestSuite(t *testing.T) { suite.Run(t, &manifestV1ResolverTestSuite{}) } diff --git a/src/api/artifact/abstractor/resolver/image/manifest_v2.go b/src/api/artifact/abstractor/resolver/image/manifest_v2.go index 8964d9ae6..a89aacdd7 100644 --- a/src/api/artifact/abstractor/resolver/image/manifest_v2.go +++ b/src/api/artifact/abstractor/resolver/image/manifest_v2.go @@ -20,15 +20,20 @@ import ( "github.com/docker/distribution/manifest/schema2" "github.com/goharbor/harbor/src/api/artifact/abstractor/blob" "github.com/goharbor/harbor/src/api/artifact/abstractor/resolver" + "github.com/goharbor/harbor/src/api/artifact/descriptor" "github.com/goharbor/harbor/src/common/utils/log" + ierror "github.com/goharbor/harbor/src/internal/error" "github.com/goharbor/harbor/src/pkg/artifact" "github.com/goharbor/harbor/src/pkg/repository" "github.com/opencontainers/image-spec/specs-go/v1" ) +// const definitions const ( // ArtifactTypeImage is the artifact type for image - ArtifactTypeImage = "IMAGE" + ArtifactTypeImage = "IMAGE" + AdditionTypeBuildHistory = "BUILD_HISTORY" + AdditionTypeVulnerabilities = "VULNERABILITIES" ) func init() { @@ -36,8 +41,16 @@ func init() { repoMgr: repository.Mgr, blobFetcher: blob.Fcher, } - if err := resolver.Register(rslver, v1.MediaTypeImageConfig, schema2.MediaTypeImageConfig); err != nil { - log.Errorf("failed to register resolver for artifact %s: %v", rslver.ArtifactType(), err) + mediaTypes := []string{ + v1.MediaTypeImageConfig, + schema2.MediaTypeImageConfig, + } + if err := resolver.Register(rslver, mediaTypes...); err != nil { + log.Errorf("failed to register resolver for media type %v: %v", mediaTypes, err) + return + } + if err := descriptor.Register(rslver, mediaTypes...); err != nil { + log.Errorf("failed to register descriptor for media type %v: %v", mediaTypes, err) return } } @@ -48,11 +61,7 @@ type manifestV2Resolver struct { blobFetcher blob.Fetcher } -func (m *manifestV2Resolver) ArtifactType() string { - return ArtifactTypeImage -} - -func (m *manifestV2Resolver) Resolve(ctx context.Context, content []byte, artifact *artifact.Artifact) error { +func (m *manifestV2Resolver) ResolveMetadata(ctx context.Context, content []byte, artifact *artifact.Artifact) error { repository, err := m.repoMgr.Get(ctx, artifact.RepositoryID) if err != nil { return err @@ -79,3 +88,46 @@ func (m *manifestV2Resolver) Resolve(ctx context.Context, content []byte, artifa artifact.ExtraAttrs["os"] = image.OS return nil } + +func (m *manifestV2Resolver) ResolveAddition(ctx context.Context, artifact *artifact.Artifact, addition string) (*resolver.Addition, error) { + if addition != AdditionTypeBuildHistory { + return nil, ierror.New(nil).WithCode(ierror.BadRequestCode). + WithMessage("addition %s isn't supported for %s(manifest version 2)", addition, ArtifactTypeImage) + } + repository, err := m.repoMgr.Get(ctx, artifact.RepositoryID) + if err != nil { + return nil, err + } + _, content, err := m.blobFetcher.FetchManifest(repository.Name, artifact.Digest) + if err != nil { + return nil, err + } + manifest := &v1.Manifest{} + if err := json.Unmarshal(content, manifest); err != nil { + return nil, err + } + content, err = m.blobFetcher.FetchLayer(repository.Name, manifest.Config.Digest.String()) + if err != nil { + return nil, err + } + image := &v1.Image{} + if err := json.Unmarshal(content, image); err != nil { + return nil, err + } + content, err = json.Marshal(image.History) + if err != nil { + return nil, err + } + return &resolver.Addition{ + Content: content, + ContentType: "application/json; charset=utf-8", + }, nil +} + +func (m *manifestV2Resolver) GetArtifactType() string { + return ArtifactTypeImage +} + +func (m *manifestV2Resolver) ListAdditionTypes() []string { + return []string{AdditionTypeBuildHistory, AdditionTypeVulnerabilities} +} diff --git a/src/api/artifact/abstractor/resolver/image/manifest_v2_test.go b/src/api/artifact/abstractor/resolver/image/manifest_v2_test.go index f90348392..451a42033 100644 --- a/src/api/artifact/abstractor/resolver/image/manifest_v2_test.go +++ b/src/api/artifact/abstractor/resolver/image/manifest_v2_test.go @@ -16,6 +16,7 @@ package image import ( "github.com/goharbor/harbor/src/common/models" + ierror "github.com/goharbor/harbor/src/internal/error" "github.com/goharbor/harbor/src/pkg/artifact" "github.com/goharbor/harbor/src/testing/api/artifact/abstractor/blob" "github.com/goharbor/harbor/src/testing/pkg/repository" @@ -23,29 +24,8 @@ import ( "testing" ) -type manifestV2ResolverTestSuite struct { - suite.Suite - resolver *manifestV2Resolver - repoMgr *repository.FakeManager - blobFetcher *blob.FakeFetcher -} - -func (m *manifestV2ResolverTestSuite) SetupTest() { - m.repoMgr = &repository.FakeManager{} - m.blobFetcher = &blob.FakeFetcher{} - m.resolver = &manifestV2Resolver{ - repoMgr: m.repoMgr, - blobFetcher: m.blobFetcher, - } - -} - -func (m *manifestV2ResolverTestSuite) TestArtifactType() { - m.Assert().Equal(ArtifactTypeImage, m.resolver.ArtifactType()) -} - -func (m *manifestV2ResolverTestSuite) TestResolve() { - content := `{ +var ( + manifest = `{ "schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "config": { @@ -61,7 +41,7 @@ func (m *manifestV2ResolverTestSuite) TestResolve() { } ] }` - config := `{ + config = `{ "architecture": "amd64", "config": { "Hostname": "", @@ -138,10 +118,30 @@ func (m *manifestV2ResolverTestSuite) TestResolve() { ] } }` +) + +type manifestV2ResolverTestSuite struct { + suite.Suite + resolver *manifestV2Resolver + repoMgr *repository.FakeManager + blobFetcher *blob.FakeFetcher +} + +func (m *manifestV2ResolverTestSuite) SetupTest() { + m.repoMgr = &repository.FakeManager{} + m.blobFetcher = &blob.FakeFetcher{} + m.resolver = &manifestV2Resolver{ + repoMgr: m.repoMgr, + blobFetcher: m.blobFetcher, + } + +} + +func (m *manifestV2ResolverTestSuite) TestResolveMetadata() { artifact := &artifact.Artifact{} m.repoMgr.On("Get").Return(&models.RepoRecord{}, nil) m.blobFetcher.On("FetchLayer").Return([]byte(config), nil) - err := m.resolver.Resolve(nil, []byte(content), artifact) + err := m.resolver.ResolveMetadata(nil, []byte(manifest), artifact) m.Require().Nil(err) m.repoMgr.AssertExpectations(m.T()) m.blobFetcher.AssertExpectations(m.T()) @@ -149,6 +149,31 @@ func (m *manifestV2ResolverTestSuite) TestResolve() { m.Assert().Equal("linux", artifact.ExtraAttrs["os"].(string)) } +func (m *manifestV2ResolverTestSuite) TestResolveAddition() { + // unknown addition + _, err := m.resolver.ResolveAddition(nil, nil, "unknown_addition") + m.True(ierror.IsErr(err, ierror.BadRequestCode)) + + // build history + artifact := &artifact.Artifact{} + m.repoMgr.On("Get").Return(&models.RepoRecord{}, nil) + m.blobFetcher.On("FetchManifest").Return("", []byte(manifest), nil) + m.blobFetcher.On("FetchLayer").Return([]byte(config), nil) + addition, err := m.resolver.ResolveAddition(nil, artifact, AdditionTypeBuildHistory) + m.Require().Nil(err) + m.Equal("application/json; charset=utf-8", addition.ContentType) + m.Equal(`[{"created":"2019-01-01T01:29:27.416803627Z","created_by":"/bin/sh -c #(nop) COPY file:f77490f70ce51da25bd21bfc30cb5e1a24b2b65eb37d4af0c327ddc24f0986a6 in / "},{"created":"2019-01-01T01:29:27.650294696Z","created_by":"/bin/sh -c #(nop) CMD [\"/hello\"]","empty_layer":true}]`, string(addition.Content)) +} + +func (m *manifestV2ResolverTestSuite) TestGetArtifactType() { + m.Assert().Equal(ArtifactTypeImage, m.resolver.GetArtifactType()) +} + +func (m *manifestV2ResolverTestSuite) TestListAdditionTypes() { + additions := m.resolver.ListAdditionTypes() + m.EqualValues([]string{AdditionTypeBuildHistory, AdditionTypeVulnerabilities}, additions) +} + func TestManifestV2ResolverTestSuite(t *testing.T) { suite.Run(t, &manifestV2ResolverTestSuite{}) } diff --git a/src/api/artifact/abstractor/resolver/resolver.go b/src/api/artifact/abstractor/resolver/resolver.go index f8043b3c1..4b032e9cf 100644 --- a/src/api/artifact/abstractor/resolver/resolver.go +++ b/src/api/artifact/abstractor/resolver/resolver.go @@ -27,12 +27,14 @@ var ( // Resolver resolves the detail information for a specific kind of artifact type Resolver interface { - // ArtifactType returns the type of artifact that the resolver handles - ArtifactType() string - // Resolve receives the manifest content, resolves the detail information + // ResolveMetadata receives the manifest content, resolves the metadata // from the manifest or the layers referenced by the manifest, and populates - // the detail information into the artifact - Resolve(ctx context.Context, manifest []byte, artifact *artifact.Artifact) error + // the metadata into the artifact + ResolveMetadata(ctx context.Context, manifest []byte, artifact *artifact.Artifact) error + // ResolveAddition returns the addition of the artifact. + // The additions are different for different artifacts: + // build history for image; values.yaml, readme and dependencies for chart, etc + ResolveAddition(ctx context.Context, artifact *artifact.Artifact, additionType string) (addition *Addition, err error) } // Register resolver, one resolver can handle multiple media types for one kind of artifact @@ -52,3 +54,9 @@ func Register(resolver Resolver, mediaTypes ...string) error { func Get(mediaType string) Resolver { return registry[mediaType] } + +// Addition defines the specific addition of different artifacts: build history for image, values.yaml for chart, etc +type Addition struct { + Content []byte // the content of the addition + ContentType string // the content type of the addition, returned as "Content-Type" header in API +} diff --git a/src/api/artifact/abstractor/resolver/resolver_test.go b/src/api/artifact/abstractor/resolver/resolver_test.go index adff05475..e77e4ac90 100644 --- a/src/api/artifact/abstractor/resolver/resolver_test.go +++ b/src/api/artifact/abstractor/resolver/resolver_test.go @@ -23,13 +23,12 @@ import ( type fakeResolver struct{} -func (f *fakeResolver) ArtifactType() string { - return "" - -} -func (f *fakeResolver) Resolve(ctx context.Context, manifest []byte, artifact *artifact.Artifact) error { +func (f *fakeResolver) ResolveMetadata(ctx context.Context, manifest []byte, artifact *artifact.Artifact) error { return nil } +func (f *fakeResolver) ResolveAddition(ctx context.Context, artifact *artifact.Artifact, additionType string) (*Addition, error) { + return nil, nil +} type resolverTestSuite struct { suite.Suite diff --git a/src/api/artifact/controller.go b/src/api/artifact/controller.go index 7c4ad8e79..ad9621df0 100644 --- a/src/api/artifact/controller.go +++ b/src/api/artifact/controller.go @@ -18,11 +18,17 @@ import ( "context" "fmt" "github.com/goharbor/harbor/src/api/artifact/abstractor" + "github.com/goharbor/harbor/src/api/artifact/abstractor/resolver" + "github.com/goharbor/harbor/src/api/artifact/abstractor/resolver/image" + "github.com/goharbor/harbor/src/api/artifact/descriptor" "github.com/goharbor/harbor/src/common/utils" + "github.com/goharbor/harbor/src/internal" "github.com/goharbor/harbor/src/pkg/art" "github.com/goharbor/harbor/src/pkg/immutabletag/match" "github.com/goharbor/harbor/src/pkg/immutabletag/match/rule" "github.com/opencontainers/go-digest" + "strings" + // registry image resolvers _ "github.com/goharbor/harbor/src/api/artifact/abstractor/resolver/image" // register chart resolver @@ -67,10 +73,10 @@ type Controller interface { // UpdatePullTime updates the pull time for the artifact. If the tagID is provides, update the pull // time of the tag as well UpdatePullTime(ctx context.Context, artifactID int64, tagID int64, time time.Time) (err error) - // GetSubResource returns the sub resource of the artifact - // The sub resource is different according to the artifact type: + // GetAddition returns the addition of the artifact. + // The addition is different according to the artifact type: // build history for image; values.yaml, readme and dependencies for chart, etc - GetSubResource(ctx context.Context, artifactID int64, resource string) (*Resource, error) + GetAddition(ctx context.Context, artifactID int64, additionType string) (addition *resolver.Addition, err error) // TODO move this to GC controller? // Prune removes the useless artifact records. The underlying registry data will // be removed during garbage collection @@ -139,11 +145,18 @@ func (c *controller) ensureArtifact(ctx context.Context, repositoryID int64, dig Digest: digest, PushTime: time.Now(), } - // abstract the specific information for the artifact - if err = c.abstractor.Abstract(ctx, artifact); err != nil { + // abstract the metadata for the artifact + if err = c.abstractor.AbstractMetadata(ctx, artifact); err != nil { return false, 0, err } + // populate the artifact type + typee, err := descriptor.GetArtifactType(artifact.MediaType) + if err != nil { + return false, 0, err + } + artifact.Type = typee + // create it id, err := c.artMgr.Create(ctx, artifact) if err != nil { @@ -333,9 +346,20 @@ func (c *controller) UpdatePullTime(ctx context.Context, artifactID int64, tagID ID: tagID, }, "PullTime") } -func (c *controller) GetSubResource(ctx context.Context, artifactID int64, resource string) (*Resource, error) { - // TODO implement - return nil, nil + +func (c *controller) GetAddition(ctx context.Context, artifactID int64, addition string) (*resolver.Addition, error) { + artifact, err := c.artMgr.Get(ctx, artifactID) + if err != nil { + return nil, err + } + switch addition { + case image.AdditionTypeVulnerabilities: + // get the vulnerabilities from scan service + // TODO implement + return &resolver.Addition{}, nil + default: + return c.abstractor.AbstractAddition(ctx, artifact, addition) + } } // assemble several part into a single artifact @@ -368,6 +392,8 @@ func (c *controller) assembleArtifact(ctx context.Context, art *artifact.Artifac if option.WithScanOverview { // TODO populate scan overview } + // populate addition links + c.populateAdditionLinks(ctx, artifact) // TODO populate signature on artifact or label level? return artifact } @@ -406,3 +432,42 @@ func (c *controller) isImmutable(projectID int64, repo string, tag string) bool } return matched } + +func (c *controller) populateAdditionLinks(ctx context.Context, artifact *Artifact) { + types, err := descriptor.ListAdditionTypes(artifact.MediaType) + if err != nil { + log.Error(err.Error()) + return + } + if len(types) == 0 { + return + } + repository, err := c.repoMgr.Get(ctx, artifact.RepositoryID) + if err != nil { + log.Error(err.Error()) + return + } + pro, repo := utils.ParseRepository(repository.Name) + version := internal.GetAPIVersion(ctx) + if artifact.AdditionLinks == nil { + artifact.AdditionLinks = make(map[string]*AdditionLink) + } + href := "" + for _, t := range types { + t = strings.ToLower(t) + switch t { + case image.AdditionTypeVulnerabilities: + // check whether the scan service is enabled and set the addition link + // TODO implement + href = fmt.Sprintf("/api/%s/projects/%s/repositories/%s/artifacts/%s/vulnerabilities", + version, pro, repo, artifact.Digest) + default: + href = fmt.Sprintf("/api/%s/projects/%s/repositories/%s/artifacts/%s/additions/%s", + version, pro, repo, artifact.Digest, t) + } + artifact.AdditionLinks[t] = &AdditionLink{ + HREF: href, + Absolute: false, + } + } +} diff --git a/src/api/artifact/controller_test.go b/src/api/artifact/controller_test.go index f0cf06c8a..1d88fc2cd 100644 --- a/src/api/artifact/controller_test.go +++ b/src/api/artifact/controller_test.go @@ -16,7 +16,10 @@ package artifact import ( "context" + "github.com/goharbor/harbor/src/api/artifact/abstractor/resolver" + "github.com/goharbor/harbor/src/api/artifact/descriptor" "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/internal" ierror "github.com/goharbor/harbor/src/internal/error" "github.com/goharbor/harbor/src/pkg/artifact" "github.com/goharbor/harbor/src/pkg/q" @@ -35,10 +38,30 @@ type fakeAbstractor struct { mock.Mock } -func (f *fakeAbstractor) Abstract(ctx context.Context, artifact *artifact.Artifact) error { +func (f *fakeAbstractor) AbstractMetadata(ctx context.Context, artifact *artifact.Artifact) error { args := f.Called() return args.Error(0) } +func (f *fakeAbstractor) AbstractAddition(ctx context.Context, artifact *artifact.Artifact, additionType string) (*resolver.Addition, error) { + args := f.Called() + var addition *resolver.Addition + if args.Get(0) != nil { + addition = args.Get(0).(*resolver.Addition) + } + return addition, args.Error(1) +} + +type fakeDescriptor struct { + mock.Mock +} + +func (f *fakeDescriptor) GetArtifactType() string { + return "IMAGE" +} + +func (f *fakeDescriptor) ListAdditionTypes() []string { + return []string{"BUILD_HISTORY"} +} type controllerTestSuite struct { suite.Suite @@ -63,6 +86,7 @@ func (c *controllerTestSuite) SetupTest() { abstractor: c.abstractor, immutableMtr: c.immutableMtr, } + descriptor.Register(&fakeDescriptor{}, "") } func (c *controllerTestSuite) TestAssembleTag() { @@ -93,12 +117,12 @@ func (c *controllerTestSuite) TestAssembleTag() { func (c *controllerTestSuite) TestAssembleArtifact() { art := &artifact.Artifact{ - ID: 1, + ID: 1, + Digest: "sha256:123", } option := &Option{ WithTag: true, TagOption: &TagOption{ - WithImmutableStatus: false, }, WithLabel: false, @@ -114,11 +138,19 @@ func (c *controllerTestSuite) TestAssembleArtifact() { PullTime: time.Now(), } c.tagMgr.On("List").Return(1, []*tag.Tag{tg}, nil) - artifact := c.ctl.assembleArtifact(nil, art, option) + c.repoMgr.On("Get").Return(&models.RepoRecord{ + Name: "library/hello-world", + }, nil) + ctx := internal.SetAPIVersion(nil, "2.0") + artifact := c.ctl.assembleArtifact(ctx, art, option) c.Require().NotNil(artifact) - c.tagMgr.AssertExpectations(c.T()) c.Equal(art.ID, artifact.ID) c.Contains(artifact.Tags, &Tag{Tag: *tg}) + c.Require().NotNil(artifact.AdditionLinks) + c.Require().NotNil(artifact.AdditionLinks["build_history"]) + c.False(artifact.AdditionLinks["build_history"].Absolute) + c.Equal("/api/2.0/projects/library/repositories/hello-world/artifacts/sha256:123/additions/build_history", + artifact.AdditionLinks["build_history"].HREF) // TODO check other fields of option } @@ -133,8 +165,6 @@ func (c *controllerTestSuite) TestEnsureArtifact() { }, nil) created, id, err := c.ctl.ensureArtifact(nil, 1, digest) c.Require().Nil(err) - c.repoMgr.AssertExpectations(c.T()) - c.artMgr.AssertExpectations(c.T()) c.False(created) c.Equal(int64(1), id) @@ -147,12 +177,9 @@ func (c *controllerTestSuite) TestEnsureArtifact() { }, nil) c.artMgr.On("List").Return(1, []*artifact.Artifact{}, nil) c.artMgr.On("Create").Return(1, nil) - c.abstractor.On("Abstract").Return(nil) + c.abstractor.On("AbstractMetadata").Return(nil) created, id, err = c.ctl.ensureArtifact(nil, 1, digest) c.Require().Nil(err) - c.repoMgr.AssertExpectations(c.T()) - c.artMgr.AssertExpectations(c.T()) - c.abstractor.AssertExpectations(c.T()) c.True(created) c.Equal(int64(1), id) } @@ -210,7 +237,7 @@ func (c *controllerTestSuite) TestEnsure() { c.artMgr.On("Create").Return(1, nil) c.tagMgr.On("List").Return(1, []*tag.Tag{}, nil) c.tagMgr.On("Create").Return(1, nil) - c.abstractor.On("Abstract").Return(nil) + c.abstractor.On("AbstractMetadata").Return(nil) _, id, err := c.ctl.Ensure(nil, 1, digest, "latest") c.Require().Nil(err) c.repoMgr.AssertExpectations(c.T()) @@ -241,10 +268,12 @@ func (c *controllerTestSuite) TestList() { Name: "latest", }, }, nil) + c.repoMgr.On("Get").Return(&models.RepoRecord{ + Name: "library/hello-world", + }, nil) + c.abstractor.On("ListSupportedAdditions").Return([]string{"BUILD_HISTORY"}) total, artifacts, err := c.ctl.List(nil, query, option) c.Require().Nil(err) - c.artMgr.AssertExpectations(c.T()) - c.tagMgr.AssertExpectations(c.T()) c.Equal(int64(1), total) c.Require().Len(artifacts, 1) c.Equal(int64(1), artifacts[0].ID) @@ -257,9 +286,9 @@ func (c *controllerTestSuite) TestGet() { ID: 1, RepositoryID: 1, }, nil) + c.abstractor.On("ListSupportedAdditions").Return([]string{"BUILD_HISTORY"}) art, err := c.ctl.Get(nil, 1, nil) c.Require().Nil(err) - c.artMgr.AssertExpectations(c.T()) c.Require().NotNil(art) c.Equal(int64(1), art.ID) } @@ -270,11 +299,10 @@ func (c *controllerTestSuite) TestGetByDigest() { RepositoryID: 1, }, nil) c.artMgr.On("List").Return(0, nil, nil) + c.abstractor.On("ListSupportedAdditions").Return([]string{"BUILD_HISTORY"}) art, err := c.ctl.getByDigest(nil, "library/hello-world", "sha256:418fb88ec412e340cdbef913b8ca1bbe8f9e8dc705f9617414c1f2c8db980180", nil) c.Require().NotNil(err) - c.repoMgr.AssertExpectations(c.T()) - c.artMgr.AssertExpectations(c.T()) c.True(ierror.IsErr(err, ierror.NotFoundCode)) // reset the mock @@ -290,11 +318,10 @@ func (c *controllerTestSuite) TestGetByDigest() { RepositoryID: 1, }, }, nil) + c.abstractor.On("ListSupportedAdditions").Return([]string{"BUILD_HISTORY"}) art, err = c.ctl.getByDigest(nil, "library/hello-world", "sha256:418fb88ec412e340cdbef913b8ca1bbe8f9e8dc705f9617414c1f2c8db980180", nil) c.Require().Nil(err) - c.repoMgr.AssertExpectations(c.T()) - c.artMgr.AssertExpectations(c.T()) c.Require().NotNil(art) c.Equal(int64(1), art.ID) } @@ -305,10 +332,9 @@ func (c *controllerTestSuite) TestGetByTag() { RepositoryID: 1, }, nil) c.tagMgr.On("List").Return(0, nil, nil) + c.abstractor.On("ListSupportedAdditions").Return([]string{"BUILD_HISTORY"}) art, err := c.ctl.getByTag(nil, "library/hello-world", "latest", nil) c.Require().NotNil(err) - c.repoMgr.AssertExpectations(c.T()) - c.tagMgr.AssertExpectations(c.T()) c.True(ierror.IsErr(err, ierror.NotFoundCode)) // reset the mock @@ -329,11 +355,9 @@ func (c *controllerTestSuite) TestGetByTag() { c.artMgr.On("Get").Return(&artifact.Artifact{ ID: 1, }, nil) + c.abstractor.On("ListSupportedAdditions").Return([]string{"BUILD_HISTORY"}) art, err = c.ctl.getByTag(nil, "library/hello-world", "latest", nil) c.Require().Nil(err) - c.repoMgr.AssertExpectations(c.T()) - c.tagMgr.AssertExpectations(c.T()) - c.artMgr.AssertExpectations(c.T()) c.Require().NotNil(art) c.Equal(int64(1), art.ID) } @@ -349,11 +373,10 @@ func (c *controllerTestSuite) TestGetByReference() { RepositoryID: 1, }, }, nil) + c.abstractor.On("ListSupportedAdditions").Return([]string{"BUILD_HISTORY"}) art, err := c.ctl.GetByReference(nil, "library/hello-world", "sha256:418fb88ec412e340cdbef913b8ca1bbe8f9e8dc705f9617414c1f2c8db980180", nil) c.Require().Nil(err) - c.repoMgr.AssertExpectations(c.T()) - c.artMgr.AssertExpectations(c.T()) c.Require().NotNil(art) c.Equal(int64(1), art.ID) @@ -375,11 +398,9 @@ func (c *controllerTestSuite) TestGetByReference() { c.artMgr.On("Get").Return(&artifact.Artifact{ ID: 1, }, nil) + c.abstractor.On("ListSupportedAdditions").Return([]string{"BUILD_HISTORY"}) art, err = c.ctl.GetByReference(nil, "library/hello-world", "latest", nil) c.Require().Nil(err) - c.repoMgr.AssertExpectations(c.T()) - c.tagMgr.AssertExpectations(c.T()) - c.artMgr.AssertExpectations(c.T()) c.Require().NotNil(art) c.Equal(int64(1), art.ID) } @@ -457,8 +478,11 @@ func (c *controllerTestSuite) TestUpdatePullTime() { } -func (c *controllerTestSuite) TestGetSubResource() { - // TODO +func (c *controllerTestSuite) TestGetAddition() { + c.artMgr.On("Get").Return(nil, nil) + c.abstractor.On("AbstractAddition").Return(nil, nil) + _, err := c.ctl.GetAddition(nil, 1, "addition") + c.Require().Nil(err) } func TestControllerTestSuite(t *testing.T) { diff --git a/src/api/artifact/descriptor/descriptor.go b/src/api/artifact/descriptor/descriptor.go new file mode 100644 index 000000000..0254e31df --- /dev/null +++ b/src/api/artifact/descriptor/descriptor.go @@ -0,0 +1,72 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package descriptor + +import ( + "fmt" + "github.com/goharbor/harbor/src/common/utils/log" +) + +var ( + registry = map[string]Descriptor{} +) + +// Descriptor describes the static information for one kind of media type +type Descriptor interface { + // GetArtifactType returns the type of one kind of artifact specified by media type + GetArtifactType() string + // ListAdditionTypes returns the supported addition types of one kind of artifact specified by media type + ListAdditionTypes() []string +} + +// Register descriptor, one descriptor can handle multiple media types for one kind of artifact +func Register(descriptor Descriptor, mediaTypes ...string) error { + for _, mediaType := range mediaTypes { + _, exist := registry[mediaType] + if exist { + return fmt.Errorf("descriptor to handle media type %s already exists", mediaType) + } + registry[mediaType] = descriptor + log.Infof("descriptor to handle media type %s registered", mediaType) + } + return nil +} + +// Get the descriptor according to the media type +func Get(mediaType string) (Descriptor, error) { + descriptor := registry[mediaType] + if descriptor == nil { + return nil, fmt.Errorf("descriptor for media type %s not found", mediaType) + } + return descriptor, nil +} + +// GetArtifactType gets the artifact type according to the media type +func GetArtifactType(mediaType string) (string, error) { + descriptor, err := Get(mediaType) + if err != nil { + return "", err + } + return descriptor.GetArtifactType(), nil +} + +// ListAdditionTypes lists the supported addition types according to the media type +func ListAdditionTypes(mediaType string) ([]string, error) { + descriptor, err := Get(mediaType) + if err != nil { + return nil, err + } + return descriptor.ListAdditionTypes(), nil +} diff --git a/src/api/artifact/model.go b/src/api/artifact/model.go index 817aaafb1..9b37cdbfb 100644 --- a/src/api/artifact/model.go +++ b/src/api/artifact/model.go @@ -24,8 +24,8 @@ import ( // Artifact is the overall view of artifact type Artifact struct { artifact.Artifact - Tags []*Tag // the list of tags that attached to the artifact - SubResourceLinks map[string][]*ResourceLink // the resource link for build history(image), values.yaml(chart), dependency(chart), etc + Tags []*Tag // the list of tags that attached to the artifact + AdditionLinks map[string]*AdditionLink // the link for build history(image), values.yaml(chart), dependency(chart), etc // TODO add other attrs: signature, scan result, etc } @@ -73,15 +73,13 @@ func (a *Artifact) ToSwagger() *models.Artifact { Immutable: tag.Immutable, }) } - for resource, links := range a.SubResourceLinks { - for _, link := range links { - art.SubResourceLinks[resource] = []models.ResourceLink{} - if link != nil { - art.SubResourceLinks[resource] = append(art.SubResourceLinks[resource], models.ResourceLink{ - Absolute: link.Absolute, - Href: link.HREF, - }) - } + for addition, link := range a.AdditionLinks { + if art.AdditionLinks == nil { + art.AdditionLinks = make(map[string]models.AdditionLink) + } + art.AdditionLinks[addition] = models.AdditionLink{ + Absolute: link.Absolute, + Href: link.HREF, } } return art @@ -94,14 +92,8 @@ type Tag struct { // TODO add other attrs: signature, label, etc } -// Resource defines the specific resource of different artifacts: build history for image, values.yaml for chart, etc -type Resource struct { - Content []byte // the content of the resource - ContentType string // the content type of the resource, returned as "Content-Type" header in API -} - -// ResourceLink is a link via that a resource can be fetched -type ResourceLink struct { +// AdditionLink is a link via that the addition can be fetched +type AdditionLink struct { HREF string Absolute bool // specify the href is an absolute URL or not } diff --git a/src/common/rbac/const.go b/src/common/rbac/const.go index f1947ffad..e7fb65711 100755 --- a/src/common/rbac/const.go +++ b/src/common/rbac/const.go @@ -64,5 +64,6 @@ const ( ResourceScanner = Resource("scanner") ResourceArtifact = Resource("artifact") ResourceTag = Resource("tag") + ResourceArtifactAddition = Resource("artifact-addition") ResourceSelf = Resource("") // subresource for self ) diff --git a/src/common/rbac/project/util.go b/src/common/rbac/project/util.go index cdfc4756f..495e273e2 100644 --- a/src/common/rbac/project/util.go +++ b/src/common/rbac/project/util.go @@ -51,6 +51,7 @@ var ( {Resource: rbac.ResourceArtifact, Action: rbac.ActionRead}, {Resource: rbac.ResourceArtifact, Action: rbac.ActionList}, + {Resource: rbac.ResourceArtifactAddition, Action: rbac.ActionRead}, } // all policies for the projects diff --git a/src/common/rbac/project/visitor_role.go b/src/common/rbac/project/visitor_role.go index ab100bdfb..de58ab546 100755 --- a/src/common/rbac/project/visitor_role.go +++ b/src/common/rbac/project/visitor_role.go @@ -131,6 +131,7 @@ var ( {Resource: rbac.ResourceArtifact, Action: rbac.ActionRead}, {Resource: rbac.ResourceArtifact, Action: rbac.ActionDelete}, {Resource: rbac.ResourceArtifact, Action: rbac.ActionList}, + {Resource: rbac.ResourceArtifactAddition, Action: rbac.ActionRead}, {Resource: rbac.ResourceTag, Action: rbac.ActionCreate}, {Resource: rbac.ResourceTag, Action: rbac.ActionDelete}, @@ -227,6 +228,7 @@ var ( {Resource: rbac.ResourceArtifact, Action: rbac.ActionRead}, {Resource: rbac.ResourceArtifact, Action: rbac.ActionDelete}, {Resource: rbac.ResourceArtifact, Action: rbac.ActionList}, + {Resource: rbac.ResourceArtifactAddition, Action: rbac.ActionRead}, {Resource: rbac.ResourceTag, Action: rbac.ActionCreate}, {Resource: rbac.ResourceTag, Action: rbac.ActionDelete}, @@ -289,6 +291,7 @@ var ( {Resource: rbac.ResourceArtifact, Action: rbac.ActionRead}, {Resource: rbac.ResourceArtifact, Action: rbac.ActionList}, + {Resource: rbac.ResourceArtifactAddition, Action: rbac.ActionRead}, {Resource: rbac.ResourceTag, Action: rbac.ActionCreate}, }, @@ -338,6 +341,7 @@ var ( {Resource: rbac.ResourceArtifact, Action: rbac.ActionRead}, {Resource: rbac.ResourceArtifact, Action: rbac.ActionList}, + {Resource: rbac.ResourceArtifactAddition, Action: rbac.ActionRead}, }, "limitedGuest": { @@ -369,6 +373,7 @@ var ( {Resource: rbac.ResourceArtifact, Action: rbac.ActionRead}, {Resource: rbac.ResourceArtifact, Action: rbac.ActionList}, + {Resource: rbac.ResourceArtifactAddition, Action: rbac.ActionRead}, }, } ) diff --git a/src/internal/context.go b/src/internal/context.go new file mode 100644 index 000000000..74f6542f1 --- /dev/null +++ b/src/internal/context.go @@ -0,0 +1,53 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import "context" + +type contextKey string + +// define all context key here to avoid conflict +const ( + contextKeyAPIVersion contextKey = "apiVersion" +) + +func setToContext(ctx context.Context, key contextKey, value interface{}) context.Context { + if ctx == nil { + ctx = context.Background() + } + return context.WithValue(ctx, key, value) +} + +func getFromContext(ctx context.Context, key contextKey) interface{} { + if ctx == nil { + return nil + } + return ctx.Value(key) +} + +// SetAPIVersion sets the API version into the context +func SetAPIVersion(ctx context.Context, version string) context.Context { + return setToContext(ctx, contextKeyAPIVersion, version) +} + +// GetAPIVersion gets the API version from the context +func GetAPIVersion(ctx context.Context) string { + version := "" + value := getFromContext(ctx, contextKeyAPIVersion) + if value != nil { + version = value.(string) + } + return version +} diff --git a/src/internal/context_test.go b/src/internal/context_test.go new file mode 100644 index 000000000..c3cdfd670 --- /dev/null +++ b/src/internal/context_test.go @@ -0,0 +1,41 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "context" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestSetAPIVersion(t *testing.T) { + ctx := SetAPIVersion(context.Background(), "1.0") + assert.NotNil(t, ctx) +} + +func TestGetAPIVersion(t *testing.T) { + // nil context + version := GetAPIVersion(nil) + assert.Empty(t, version) + + // no version set in context + version = GetAPIVersion(context.Background()) + assert.Empty(t, version) + + // version set in context + ctx := SetAPIVersion(context.Background(), "1.0") + version = GetAPIVersion(ctx) + assert.Equal(t, "1.0", version) +} diff --git a/src/server/middleware/apiversion/api_version.go b/src/server/middleware/apiversion/api_version.go new file mode 100644 index 000000000..cdd59cf4b --- /dev/null +++ b/src/server/middleware/apiversion/api_version.go @@ -0,0 +1,31 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apiversion + +import ( + "github.com/goharbor/harbor/src/internal" + "github.com/goharbor/harbor/src/server/middleware" + "net/http" +) + +// Middleware returns a middleware that set the API version into the context +func Middleware(version string) middleware.Middleware { + return func(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + ctx := internal.SetAPIVersion(req.Context(), version) + handler.ServeHTTP(w, req.WithContext(ctx)) + }) + } +} diff --git a/src/server/middleware/apiversion/api_version_test.go b/src/server/middleware/apiversion/api_version_test.go new file mode 100644 index 000000000..e372bbc8e --- /dev/null +++ b/src/server/middleware/apiversion/api_version_test.go @@ -0,0 +1,34 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apiversion + +import ( + "github.com/goharbor/harbor/src/internal" + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "testing" +) + +func TestMiddleware(t *testing.T) { + version := "" + middleware := Middleware("1.0") + req := httptest.NewRequest("GET", "http://localhost", nil) + handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + version = internal.GetAPIVersion(req.Context()) + }) + middleware(handler).ServeHTTP(nil, req) + assert.Equal(t, "1.0", version) +} diff --git a/src/server/v2.0/handler/artifact.go b/src/server/v2.0/handler/artifact.go index 451724a88..2f2f9158b 100644 --- a/src/server/v2.0/handler/artifact.go +++ b/src/server/v2.0/handler/artifact.go @@ -17,6 +17,7 @@ package handler import ( "context" "fmt" + "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/middleware" "github.com/goharbor/harbor/src/api/artifact" "github.com/goharbor/harbor/src/common/rbac" @@ -26,6 +27,8 @@ import ( "github.com/goharbor/harbor/src/pkg/repository" "github.com/goharbor/harbor/src/server/v2.0/models" operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/artifact" + "net/http" + "strings" "time" ) @@ -169,6 +172,24 @@ func (a *artifactAPI) DeleteTag(ctx context.Context, params operation.DeleteTagP return operation.NewDeleteTagOK() } +func (a *artifactAPI) GetAddition(ctx context.Context, params operation.GetAdditionParams) middleware.Responder { + if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionRead, rbac.ResourceArtifactAddition); err != nil { + return a.SendError(ctx, err) + } + artifact, err := a.artCtl.GetByReference(ctx, fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName), params.Reference, nil) + if err != nil { + return a.SendError(ctx, err) + } + addition, err := a.artCtl.GetAddition(ctx, artifact.ID, strings.ToUpper(params.Addition)) + if err != nil { + return a.SendError(ctx, err) + } + return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) { + w.Header().Set("Content-Type", addition.ContentType) + w.Write(addition.Content) + }) +} + func option(withTag, withImmutableStatus, withLabel, withScanOverview, withSignature *bool) *artifact.Option { option := &artifact.Option{ WithTag: true, // return the tag by default diff --git a/src/server/v2.0/handler/repository.go b/src/server/v2.0/handler/repository.go index 78229ebbe..bc7a96554 100644 --- a/src/server/v2.0/handler/repository.go +++ b/src/server/v2.0/handler/repository.go @@ -19,6 +19,7 @@ import ( "fmt" "github.com/go-openapi/runtime/middleware" "github.com/goharbor/harbor/src/api/repository" + "github.com/goharbor/harbor/src/common/rbac" operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/repository" ) @@ -34,6 +35,9 @@ type repositoryAPI struct { } func (r *repositoryAPI) DeleteRepository(ctx context.Context, params operation.DeleteRepositoryParams) middleware.Responder { + if err := r.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionDelete, rbac.ResourceRepository); err != nil { + return r.SendError(ctx, err) + } repository, err := r.repoCtl.GetByName(ctx, fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName)) if err != nil { return r.SendError(ctx, err) diff --git a/src/server/v2.0/route/route.go b/src/server/v2.0/route/route.go index 9d962c1c2..15c3acb65 100644 --- a/src/server/v2.0/route/route.go +++ b/src/server/v2.0/route/route.go @@ -15,11 +15,18 @@ package route import ( + "github.com/goharbor/harbor/src/server/middleware/apiversion" "github.com/goharbor/harbor/src/server/router" "github.com/goharbor/harbor/src/server/v2.0/handler" ) +const ( + version = "v2.0" +) + // RegisterRoutes for Harbor v2.0 APIs func RegisterRoutes() { - router.NewRoute().Path("/api/v2.0/*").Handler(handler.New()) + router.NewRoute().Path("/api/" + version + "/*"). + Middleware(apiversion.Middleware(version)). + Handler(handler.New()) } diff --git a/src/testing/api/artifact/abstractor/blob/fetcher.go b/src/testing/api/artifact/abstractor/blob/fetcher.go index 0d533eef1..0230161f9 100644 --- a/src/testing/api/artifact/abstractor/blob/fetcher.go +++ b/src/testing/api/artifact/abstractor/blob/fetcher.go @@ -18,7 +18,7 @@ import ( "github.com/stretchr/testify/mock" ) -// FakeFetcher is a fake blob fetcher that implement the src/api/artifact/blob.Fetcher interface +// FakeFetcher is a fake blob fetcher that implement the src/api/artifact/abstractor/blob.Fetcher interface type FakeFetcher struct { mock.Mock } diff --git a/src/testing/api/artifact/abstractor/resolver/resolver.go b/src/testing/api/artifact/abstractor/resolver/resolver.go new file mode 100644 index 000000000..f9a57f1ae --- /dev/null +++ b/src/testing/api/artifact/abstractor/resolver/resolver.go @@ -0,0 +1,43 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package resolver + +import ( + "context" + "github.com/goharbor/harbor/src/api/artifact/abstractor/resolver" + "github.com/goharbor/harbor/src/pkg/artifact" + "github.com/stretchr/testify/mock" +) + +// FakeResolver is a fake resolver that implement the src/api/artifact/abstractor/resolver.Resolver interface +type FakeResolver struct { + mock.Mock +} + +// ResolveMetadata ... +func (f *FakeResolver) ResolveMetadata(ctx context.Context, manifest []byte, artifact *artifact.Artifact) error { + args := f.Called() + return args.Error(0) +} + +// ResolveAddition ... +func (f *FakeResolver) ResolveAddition(ctx context.Context, artifact *artifact.Artifact, additionType string) (*resolver.Addition, error) { + args := f.Called() + var addition *resolver.Addition + if args.Get(0) != nil { + addition = args.Get(0).(*resolver.Addition) + } + return addition, args.Error(1) +} diff --git a/src/testing/api/artifact/controller.go b/src/testing/api/artifact/controller.go index 384ef4ad5..ef3afe95c 100644 --- a/src/testing/api/artifact/controller.go +++ b/src/testing/api/artifact/controller.go @@ -17,6 +17,7 @@ package artifact import ( "context" "github.com/goharbor/harbor/src/api/artifact" + "github.com/goharbor/harbor/src/api/artifact/abstractor/resolver" "github.com/goharbor/harbor/src/pkg/q" "github.com/stretchr/testify/mock" "time" @@ -97,12 +98,12 @@ func (f *FakeController) UpdatePullTime(ctx context.Context, artifactID int64, t return args.Error(0) } -// GetSubResource ... -func (f *FakeController) GetSubResource(ctx context.Context, artifactID int64, resource string) (*artifact.Resource, error) { +// GetAddition ... +func (f *FakeController) GetAddition(ctx context.Context, artifactID int64, addition string) (*resolver.Addition, error) { args := f.Called() - var res *artifact.Resource + var res *resolver.Addition if args.Get(0) != nil { - res = args.Get(0).(*artifact.Resource) + res = args.Get(0).(*resolver.Addition) } return res, args.Error(1) }