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:
Wenkai Yin 2020-01-29 11:34:55 +08:00
parent 8a74fcb074
commit 0f6057a22c
30 changed files with 806 additions and 198 deletions

View File

@ -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

View File

@ -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 {

View File

@ -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{})
}

View File

@ -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}
}

View File

@ -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{})
}

View File

@ -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
}

View File

@ -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{})
}

View File

@ -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
}

View File

@ -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{})
}

View File

@ -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}
}

View File

@ -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{})
}

View File

@ -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
}

View File

@ -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

View File

@ -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,
}
}
}

View File

@ -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) {

View 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
}

View File

@ -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
}

View File

@ -64,5 +64,6 @@ const (
ResourceScanner = Resource("scanner")
ResourceArtifact = Resource("artifact")
ResourceTag = Resource("tag")
ResourceArtifactAddition = Resource("artifact-addition")
ResourceSelf = Resource("") // subresource for self
)

View File

@ -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

View File

@ -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
View 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
}

View 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)
}

View 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))
})
}
}

View 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)
}

View File

@ -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

View File

@ -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)

View File

@ -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())
}

View File

@ -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
}

View 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)
}

View File

@ -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)
}