modify artifact copy api to support cosign (#16194)

Signed-off-by: Wang Yan <wangyan@vmware.com>
This commit is contained in:
Wang Yan 2022-01-17 15:52:14 +08:00 committed by GitHub
parent 8f77567589
commit 01c6f6084b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 155 additions and 37 deletions

View File

@ -19,6 +19,7 @@ import (
"context"
stderrors "errors"
"fmt"
accessorymodel "github.com/goharbor/harbor/src/pkg/accessory/model"
"strings"
"time"
@ -78,7 +79,7 @@ type Controller interface {
// creates it if it doesn't exist. If tags are provided, ensure they exist
// and are attached to the artifact. If the tags don't exist, create them first.
// The "created" will be set as true when the artifact is created
Ensure(ctx context.Context, repository, digest string, tags ...string) (created bool, id int64, err error)
Ensure(ctx context.Context, repository, digest string, option *ArtOption) (created bool, id int64, err error)
// Count returns the total count of artifacts according to the query.
// The artifacts that referenced by others and without tags are not counted
Count(ctx context.Context, query *q.Query) (total int64, err error)
@ -140,14 +141,26 @@ type controller struct {
accessoryMgr accessory.Manager
}
func (c *controller) Ensure(ctx context.Context, repository, digest string, tags ...string) (bool, int64, error) {
type ArtOption struct {
Tags []string
Accs []accessorymodel.AccessoryData
}
func (c *controller) Ensure(ctx context.Context, repository, digest string, option *ArtOption) (bool, int64, error) {
created, artifact, err := c.ensureArtifact(ctx, repository, digest)
if err != nil {
return false, 0, err
}
for _, tag := range tags {
if err = c.tagCtl.Ensure(ctx, artifact.RepositoryID, artifact.ID, tag); err != nil {
return false, 0, err
if option != nil {
for _, tag := range option.Tags {
if err = c.tagCtl.Ensure(ctx, artifact.RepositoryID, artifact.ID, tag); err != nil {
return false, 0, err
}
}
for _, acc := range option.Accs {
if err = c.accessoryMgr.Ensure(ctx, artifact.ID, acc.ArtifactID, acc.Size, acc.Digest, acc.Type); err != nil {
return false, 0, err
}
}
}
// fire event
@ -155,8 +168,9 @@ func (c *controller) Ensure(ctx context.Context, repository, digest string, tags
Ctx: ctx,
Artifact: artifact,
}
if len(tags) > 0 {
e.Tag = tags[0]
if option != nil && len(option.Tags) > 0 {
e.Tag = option.Tags[0]
}
notification.AddEvent(ctx, e)
return created, artifact.ID, nil
@ -429,18 +443,19 @@ func (c *controller) deleteDeeply(ctx context.Context, id int64, isRoot, isAcces
}
func (c *controller) Copy(ctx context.Context, srcRepo, reference, dstRepo string) (int64, error) {
return c.copyDeeply(ctx, srcRepo, reference, dstRepo, true)
dstAccs := make([]accessorymodel.AccessoryData, 0)
return c.copyDeeply(ctx, srcRepo, reference, dstRepo, true, &dstAccs)
}
// as we call the docker registry APIs in the registry client directly,
// this bypass our own logic(ensure, fire event, etc.) inside the registry handlers,
// these logic must be covered explicitly here.
// "copyDeeply" iterates the child artifacts and copy them first
func (c *controller) copyDeeply(ctx context.Context, srcRepo, reference, dstRepo string, isRoot bool) (int64, error) {
func (c *controller) copyDeeply(ctx context.Context, srcRepo, reference, dstRepo string, isRoot bool, dstAccs *[]accessorymodel.AccessoryData) (int64, error) {
var option *Option
// only get the tags of the root parent
if isRoot {
option = &Option{WithTag: true}
option = &Option{WithTag: true, WithAccessory: true}
}
srcArt, err := c.GetByReference(ctx, srcRepo, reference, option)
@ -457,17 +472,32 @@ func (c *controller) copyDeeply(ctx context.Context, srcRepo, reference, dstRepo
if !isRoot {
return dstArt.ID, nil
}
// the root parent already exists, goto next step to copy tags
goto tags
// the root parent already exists, goto next step to ensure artifact: create artifact & references, copy tags & accessories.
goto ensureArt
}
if !errors.IsErr(err, errors.NotFoundCode) {
return 0, err
}
// copy accessory if contains any
for _, acc := range srcArt.Accessories {
id, err := c.copyDeeply(ctx, srcRepo, acc.GetData().Digest, dstRepo, false, dstAccs)
if err != nil {
return 0, err
}
dstAcc := accessorymodel.AccessoryData{
ArtifactID: id,
Digest: acc.GetData().Digest,
Type: acc.GetData().Type,
Size: acc.GetData().Size,
}
*dstAccs = append(*dstAccs, dstAcc)
}
// the artifact doesn't exist under the destination repository, continue to copy
// copy child artifacts if contains any
for _, reference := range srcArt.References {
if _, err = c.copyDeeply(ctx, srcRepo, reference.ChildDigest, dstRepo, false); err != nil {
if _, err = c.copyDeeply(ctx, srcRepo, reference.ChildDigest, dstRepo, false, dstAccs); err != nil {
return 0, err
}
}
@ -477,14 +507,18 @@ func (c *controller) copyDeeply(ctx context.Context, srcRepo, reference, dstRepo
return 0, err
}
tags:
ensureArt:
// only copy the tags of outermost artifact
var tags []string
for _, tag := range srcArt.Tags {
tags = append(tags, tag.Name)
}
// ensure the parent artifact exist in the database
_, id, err := c.Ensure(ctx, dstRepo, digest, tags...)
artopt := &ArtOption{
Tags: tags,
Accs: *dstAccs,
}
_, id, err := c.Ensure(ctx, dstRepo, digest, artopt)
if err != nil {
return 0, err
}
@ -586,6 +620,24 @@ func (c *controller) Walk(ctx context.Context, root *Artifact, walkFn func(*Arti
}
}
}
if len(artifact.Accessories) > 0 {
var ids []int64
for _, acc := range artifact.Accessories {
ids = append(ids, acc.GetData().ArtifactID)
}
children, err := c.List(ctx, q.New(q.KeyWords{"id__in": ids, "base": "*"}), option)
if err != nil {
return err
}
for _, child := range children {
if !walked[child.Digest] {
queue.PushBack(child)
}
}
}
}
return nil

View File

@ -250,7 +250,10 @@ func (c *controllerTestSuite) TestEnsure() {
c.artMgr.On("Create", mock.Anything, mock.Anything).Return(int64(1), nil)
c.abstractor.On("AbstractMetadata").Return(nil)
c.tagCtl.On("Ensure").Return(nil)
_, id, err := c.ctl.Ensure(orm.NewContext(nil, &ormtesting.FakeOrmer{}), "library/hello-world", digest, "latest")
c.accMgr.On("Ensure").Return(nil)
_, id, err := c.ctl.Ensure(orm.NewContext(nil, &ormtesting.FakeOrmer{}), "library/hello-world", digest, &ArtOption{
Tags: []string{"latest"},
})
c.Require().Nil(err)
c.repoMgr.AssertExpectations(c.T())
c.artMgr.AssertExpectations(c.T())
@ -540,6 +543,17 @@ func (c *controllerTestSuite) TestCopy() {
},
},
}, nil)
acc := &basemodel.Default{
Data: accessorymodel.AccessoryData{
ID: 1,
ArtifactID: 2,
SubArtifactID: 1,
Type: accessorymodel.TypeCosignSignature,
},
}
c.accMgr.On("List", mock.Anything, mock.Anything).Return([]accessorymodel.Accessory{
acc,
}, nil)
c.tagCtl.On("Update").Return(nil)
c.repoMgr.On("Get", mock.Anything, mock.Anything).Return(&repomodel.RepoRecord{
RepositoryID: 1,
@ -549,6 +563,7 @@ func (c *controllerTestSuite) TestCopy() {
c.artMgr.On("Create", mock.Anything, mock.Anything).Return(int64(1), nil)
c.regCli.On("Copy").Return(nil)
c.tagCtl.On("Ensure").Return(nil)
c.accMgr.On("Ensure", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
_, err := c.ctl.Copy(orm.NewContext(nil, &ormtesting.FakeOrmer{}), "library/hello-world", "latest", "library/hello-world2")
c.Require().Nil(err)
}

View File

@ -32,15 +32,17 @@ var (
// Manager is the only interface of artifact module to provide the management functions for artifacts
type Manager interface {
// Ensure ...
Ensure(ctx context.Context, subArtID, artifactID, size int64, digest, accType string) error
// Get the artifact specified by the ID
Get(ctx context.Context, id int64) (accessory model.Accessory, err error)
// Count returns the total count of tags according to the query.
// Count returns the total count of accessory according to the query.
Count(ctx context.Context, query *q.Query) (total int64, err error)
// List tags according to the query
// List accessory according to the query
List(ctx context.Context, query *q.Query) (accs []model.Accessory, err error)
// Create the tag and returns the ID
// Create the accessory and returns the ID
Create(ctx context.Context, accessory model.AccessoryData) (id int64, err error)
// Delete the tag specified by ID
// Delete the accessory specified by ID
Delete(ctx context.Context, id int64) (err error)
// DeleteAccessories deletes accessories according to the query
DeleteAccessories(ctx context.Context, q *q.Query) (err error)
@ -59,6 +61,26 @@ type manager struct {
dao dao.DAO
}
func (m *manager) Ensure(ctx context.Context, subArtID, artifactID, size int64, digest, accType string) error {
accs, err := m.dao.List(ctx, q.New(q.KeyWords{"ArtifactID": artifactID, "Digest": digest}))
if err != nil {
return err
}
if len(accs) > 0 {
return nil
}
acc := model.AccessoryData{
ArtifactID: artifactID,
SubArtifactID: subArtID,
Digest: digest,
Size: size,
Type: accType,
}
_, err = m.Create(ctx, acc)
return err
}
func (m *manager) Get(ctx context.Context, id int64) (model.Accessory, error) {
acc, err := m.dao.Get(ctx, id)
if err != nil {

View File

@ -37,6 +37,13 @@ func (m *managerTestSuite) SetupTest() {
}
}
func (m *managerTestSuite) TestEnsure() {
mock.OnAnything(m.dao, "List").Return([]*dao.Accessory{}, nil)
mock.OnAnything(m.dao, "Create").Return(int64(1), nil)
err := m.mgr.Ensure(nil, int64(1), int64(1), int64(1), "sha256:1234", model.TypeCosignSignature)
m.Require().Nil(err)
}
func (m *managerTestSuite) TestList() {
acc := &dao.Accessory{
ID: 1,

View File

@ -125,7 +125,9 @@ func putManifest(w http.ResponseWriter, req *http.Request) {
tags = append(tags, reference)
}
_, _, err = artifact.Ctl.Ensure(req.Context(), repo, dgt, tags...)
_, _, err = artifact.Ctl.Ensure(req.Context(), repo, dgt, &artifact.ArtOption{
Tags: tags,
})
if err != nil {
lib_http.SendError(w, err)
return

View File

@ -18,6 +18,7 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/goharbor/harbor/src/lib/q"
"net/http"
"strings"
"time"
@ -170,6 +171,18 @@ func (a *artifactAPI) CopyArtifact(ctx context.Context, params operation.CopyArt
return a.SendError(ctx, err)
}
srcArt, err := a.artCtl.GetByReference(ctx, srcRepo, ref, nil)
if err != nil {
return a.SendError(ctx, err)
}
accs, err := a.accMgr.List(ctx, q.New(q.KeyWords{"ArtifactID": srcArt.ID, "Digest": srcArt.Digest}))
if err != nil {
return a.SendError(ctx, err)
}
if len(accs) >= 1 && accs[0].IsHard() {
return a.SendError(ctx, errors.New(nil).WithCode(errors.DENIED).WithMessage("the operation isn't supported for an artifact accessory"))
}
dstRepo := fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName)
_, _, err = a.repoCtl.Ensure(ctx, dstRepo)
if err != nil {

View File

@ -91,34 +91,27 @@ func (_m *Controller) Delete(ctx context.Context, id int64) error {
return r0
}
// Ensure provides a mock function with given fields: ctx, repository, digest, tags
func (_m *Controller) Ensure(ctx context.Context, repository string, digest string, tags ...string) (bool, int64, error) {
_va := make([]interface{}, len(tags))
for _i := range tags {
_va[_i] = tags[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, repository, digest)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
// Ensure provides a mock function with given fields: ctx, repository, digest, option
func (_m *Controller) Ensure(ctx context.Context, repository string, digest string, option *artifact.ArtOption) (bool, int64, error) {
ret := _m.Called(ctx, repository, digest, option)
var r0 bool
if rf, ok := ret.Get(0).(func(context.Context, string, string, ...string) bool); ok {
r0 = rf(ctx, repository, digest, tags...)
if rf, ok := ret.Get(0).(func(context.Context, string, string, *artifact.ArtOption) bool); ok {
r0 = rf(ctx, repository, digest, option)
} else {
r0 = ret.Get(0).(bool)
}
var r1 int64
if rf, ok := ret.Get(1).(func(context.Context, string, string, ...string) int64); ok {
r1 = rf(ctx, repository, digest, tags...)
if rf, ok := ret.Get(1).(func(context.Context, string, string, *artifact.ArtOption) int64); ok {
r1 = rf(ctx, repository, digest, option)
} else {
r1 = ret.Get(1).(int64)
}
var r2 error
if rf, ok := ret.Get(2).(func(context.Context, string, string, ...string) error); ok {
r2 = rf(ctx, repository, digest, tags...)
if rf, ok := ret.Get(2).(func(context.Context, string, string, *artifact.ArtOption) error); ok {
r2 = rf(ctx, repository, digest, option)
} else {
r2 = ret.Error(2)
}

View File

@ -86,6 +86,20 @@ func (_m *Manager) DeleteAccessories(ctx context.Context, _a1 *q.Query) error {
return r0
}
// Ensure provides a mock function with given fields: ctx, subArtID, artifactID, size, digest, accType
func (_m *Manager) Ensure(ctx context.Context, subArtID int64, artifactID int64, size int64, digest string, accType string) error {
ret := _m.Called(ctx, subArtID, artifactID, size, digest, accType)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64, int64, int64, string, string) error); ok {
r0 = rf(ctx, subArtID, artifactID, size, digest, accType)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: ctx, id
func (_m *Manager) Get(ctx context.Context, id int64) (model.Accessory, error) {
ret := _m.Called(ctx, id)