mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-22 18:25:56 +01:00
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 <yinw@vmware.com>
This commit is contained in:
parent
8a74fcb074
commit
0f6057a22c
@ -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
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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{})
|
||||
}
|
||||
|
@ -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}
|
||||
}
|
||||
|
@ -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{})
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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{})
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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{})
|
||||
}
|
||||
|
@ -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}
|
||||
}
|
||||
|
@ -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{})
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
72
src/api/artifact/descriptor/descriptor.go
Normal file
72
src/api/artifact/descriptor/descriptor.go
Normal file
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -64,5 +64,6 @@ const (
|
||||
ResourceScanner = Resource("scanner")
|
||||
ResourceArtifact = Resource("artifact")
|
||||
ResourceTag = Resource("tag")
|
||||
ResourceArtifactAddition = Resource("artifact-addition")
|
||||
ResourceSelf = Resource("") // subresource for self
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
53
src/internal/context.go
Normal file
53
src/internal/context.go
Normal file
@ -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
|
||||
}
|
41
src/internal/context_test.go
Normal file
41
src/internal/context_test.go
Normal file
@ -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)
|
||||
}
|
31
src/server/middleware/apiversion/api_version.go
Normal file
31
src/server/middleware/apiversion/api_version.go
Normal file
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
34
src/server/middleware/apiversion/api_version_test.go
Normal file
34
src/server/middleware/apiversion/api_version_test.go
Normal file
@ -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)
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
43
src/testing/api/artifact/abstractor/resolver/resolver.go
Normal file
43
src/testing/api/artifact/abstractor/resolver/resolver.go
Normal file
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user