mirror of https://github.com/goharbor/harbor.git
[cherry-pick] ] support accessory in either order (#19906)
* remove the log for ScannerSkipUpdatePullTime fixes #19795, remove the noise in the log Signed-off-by: wang yan <wangyan@vmware.com> * support accessor in either order In certain cases, the OCI client may push the subject artifact and accessory in either order. Therefore, it is necessary to handle situations where the client pushes the accessory ahead of the subject artifact. Signed-off-by: wang yan <wangyan@vmware.com> * fix issue 19392 Needs to set the repo when to handle the accessory before subject manifest. Signed-off-by: wang yan <wangyan@vmware.com> * fix the landing accessory data (#19661) Fix the keywords when to list accessories belong to the subject manifest. Signed-off-by: wang yan <wangyan@vmware.com> --------- Signed-off-by: wang yan <wangyan@vmware.com>
This commit is contained in:
parent
8bd8c6d10c
commit
5d27ccf7bb
|
@ -32,6 +32,8 @@ type DAO interface {
|
|||
Get(ctx context.Context, id int64) (accessory *Accessory, err error)
|
||||
// Create the accessory
|
||||
Create(ctx context.Context, accessory *Accessory) (id int64, err error)
|
||||
// Update the accessory
|
||||
Update(ctx context.Context, accessory *Accessory) error
|
||||
// Delete the accessory specified by ID
|
||||
Delete(ctx context.Context, id int64) (err error)
|
||||
// DeleteAccessories deletes accessories by query
|
||||
|
@ -103,6 +105,21 @@ func (d *dao) Create(ctx context.Context, acc *Accessory) (int64, error) {
|
|||
return id, err
|
||||
}
|
||||
|
||||
func (d *dao) Update(ctx context.Context, acc *Accessory) error {
|
||||
ormer, err := orm.FromContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := ormer.Update(acc, "SubjectArtifactID")
|
||||
if n == 0 {
|
||||
if e := orm.AsConflictError(err, "accessory %s already exists", acc.Digest); e != nil {
|
||||
err = e
|
||||
}
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *dao) Delete(ctx context.Context, id int64) error {
|
||||
ormer, err := orm.FromContext(ctx)
|
||||
if err != nil {
|
||||
|
|
|
@ -169,6 +169,19 @@ func (d *daoTestSuite) TestCreate() {
|
|||
d.True(errors.IsErr(err, errors.ConflictCode))
|
||||
}
|
||||
|
||||
func (d *daoTestSuite) TestUpdate() {
|
||||
acc := &Accessory{
|
||||
ID: d.accID,
|
||||
SubjectArtifactID: 333,
|
||||
}
|
||||
err := d.dao.Update(d.ctx, acc)
|
||||
d.Require().Nil(err)
|
||||
|
||||
accAfter, err := d.dao.Get(d.ctx, d.accID)
|
||||
d.Require().Nil(err)
|
||||
d.Require().Equal(int64(333), accAfter.SubjectArtifactID)
|
||||
}
|
||||
|
||||
func (d *daoTestSuite) TestDelete() {
|
||||
// happy pass is covered in TearDown
|
||||
|
||||
|
|
|
@ -48,6 +48,8 @@ type Manager interface {
|
|||
List(ctx context.Context, query *q.Query) (accs []model.Accessory, err error)
|
||||
// Create the accessory and returns the ID
|
||||
Create(ctx context.Context, accessory model.AccessoryData) (id int64, err error)
|
||||
// Update the accessory
|
||||
Update(ctx context.Context, accessory model.AccessoryData) error
|
||||
// Delete the accessory specified by ID
|
||||
Delete(ctx context.Context, id int64) (err error)
|
||||
// DeleteAccessories deletes accessories according to the query
|
||||
|
@ -150,6 +152,14 @@ func (m *manager) Create(ctx context.Context, accessory model.AccessoryData) (in
|
|||
return m.dao.Create(ctx, acc)
|
||||
}
|
||||
|
||||
func (m *manager) Update(ctx context.Context, accessory model.AccessoryData) error {
|
||||
acc := &dao.Accessory{
|
||||
ID: accessory.ID,
|
||||
SubjectArtifactID: accessory.SubArtifactID,
|
||||
}
|
||||
return m.dao.Update(ctx, acc)
|
||||
}
|
||||
|
||||
func (m *manager) Delete(ctx context.Context, id int64) error {
|
||||
return m.dao.Delete(ctx, id)
|
||||
}
|
||||
|
|
|
@ -87,6 +87,16 @@ func (m *managerTestSuite) TestCreate() {
|
|||
m.dao.AssertExpectations(m.T())
|
||||
}
|
||||
|
||||
func (m *managerTestSuite) TestUpdate() {
|
||||
mock.OnAnything(m.dao, "Update").Return(nil)
|
||||
err := m.mgr.Update(nil, model.AccessoryData{
|
||||
ID: 1,
|
||||
SubArtifactID: 2,
|
||||
})
|
||||
m.Require().Nil(err)
|
||||
m.dao.AssertExpectations(m.T())
|
||||
}
|
||||
|
||||
func (m *managerTestSuite) TestDelete() {
|
||||
mock.OnAnything(m.dao, "Delete").Return(nil)
|
||||
err := m.mgr.Delete(nil, 1)
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"net/http"
|
||||
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
|
||||
"github.com/goharbor/harbor/src/controller/artifact"
|
||||
|
@ -28,6 +29,7 @@ import (
|
|||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/pkg/accessory"
|
||||
"github.com/goharbor/harbor/src/pkg/accessory/model"
|
||||
"github.com/goharbor/harbor/src/server/middleware"
|
||||
|
@ -147,6 +149,34 @@ func Middleware() func(http.Handler) http.Handler {
|
|||
}
|
||||
}
|
||||
w.Header().Set("OCI-Subject", subjectArt.Digest)
|
||||
} else {
|
||||
// In certain cases, the OCI client may push the subject artifact and accessory in either order.
|
||||
// Therefore, it is necessary to handle situations where the client pushes the accessory ahead of the subject artifact.
|
||||
digest := digest.FromBytes(body)
|
||||
accs, err := accessory.Mgr.List(ctx, q.New(q.KeyWords{"SubjectArtifactDigest": digest, "SubjectArtifactRepo": info.Repository}))
|
||||
if err != nil {
|
||||
logger.Errorf("failed to list accessory artifact: %s, error: %v", digest, err)
|
||||
return err
|
||||
}
|
||||
if len(accs) <= 0 {
|
||||
return nil
|
||||
}
|
||||
art, err := artifact.Ctl.GetByReference(ctx, info.Repository, digest.String(), nil)
|
||||
if err != nil {
|
||||
logger.Errorf("failed to list artifact: %s, error: %v", digest, err)
|
||||
return err
|
||||
}
|
||||
if art != nil {
|
||||
for _, acc := range accs {
|
||||
accData := model.AccessoryData{
|
||||
ID: acc.GetData().ID,
|
||||
SubArtifactID: art.ID,
|
||||
}
|
||||
if err := accessory.Mgr.Update(ctx, accData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -37,7 +37,7 @@ func (suite *MiddlewareTestSuite) SetupTest() {
|
|||
func (suite *MiddlewareTestSuite) TearDownTest() {
|
||||
}
|
||||
|
||||
func (suite *MiddlewareTestSuite) prepare(name, subject string) (distribution.Manifest, distribution.Descriptor, *http.Request) {
|
||||
func (suite *MiddlewareTestSuite) prepare(name, digset string, withoutSub ...bool) (distribution.Manifest, distribution.Descriptor, *http.Request) {
|
||||
body := fmt.Sprintf(`
|
||||
{
|
||||
"schemaVersion":2,
|
||||
|
@ -61,7 +61,29 @@ func (suite *MiddlewareTestSuite) prepare(name, subject string) (distribution.Ma
|
|||
"mediaType":"application/vnd.oci.image.manifest.v1+json",
|
||||
"size":419,
|
||||
"digest":"%s"
|
||||
}}`, subject)
|
||||
}}`, digset)
|
||||
|
||||
if len(withoutSub) > 0 && withoutSub[0] {
|
||||
body = fmt.Sprintf(`
|
||||
{
|
||||
"schemaVersion":2,
|
||||
"mediaType":"application/vnd.oci.image.manifest.v1+json",
|
||||
"config":{
|
||||
"mediaType":"application/vnd.example.sbom",
|
||||
"size":2,
|
||||
"digest":"%s"
|
||||
},
|
||||
"layers":[
|
||||
{
|
||||
"mediaType":"application/vnd.example.sbom.text",
|
||||
"size":37,
|
||||
"digest":"sha256:45592a729ef6884ea3297e9510d79104f27aeef5f4919b3a921e3abb7f469709"
|
||||
}
|
||||
],
|
||||
"annotations":{
|
||||
"org.example.sbom.format":"text"
|
||||
}}`, digset)
|
||||
}
|
||||
|
||||
manifest, descriptor, err := distribution.UnmarshalManifest("application/vnd.oci.image.manifest.v1+json", []byte(body))
|
||||
suite.Nil(err)
|
||||
|
@ -94,19 +116,21 @@ func (suite *MiddlewareTestSuite) addArt(pid, repositoryID int64, repositoryName
|
|||
return afid
|
||||
}
|
||||
|
||||
func (suite *MiddlewareTestSuite) addArtAcc(pid, repositoryID int64, repositoryName, dgt, accdgt string) int64 {
|
||||
subaf := &artifact.Artifact{
|
||||
Type: "Docker-Image",
|
||||
ProjectID: pid,
|
||||
RepositoryID: repositoryID,
|
||||
RepositoryName: repositoryName,
|
||||
Digest: dgt,
|
||||
Size: 1024,
|
||||
PushTime: time.Now(),
|
||||
PullTime: time.Now(),
|
||||
func (suite *MiddlewareTestSuite) addArtAcc(pid, repositoryID int64, repositoryName, dgt, accdgt string, createSub ...bool) int64 {
|
||||
if len(createSub) > 0 && createSub[0] {
|
||||
subaf := &artifact.Artifact{
|
||||
Type: "Docker-Image",
|
||||
ProjectID: pid,
|
||||
RepositoryID: repositoryID,
|
||||
RepositoryName: repositoryName,
|
||||
Digest: dgt,
|
||||
Size: 1024,
|
||||
PushTime: time.Now(),
|
||||
PullTime: time.Now(),
|
||||
}
|
||||
_, err := pkg.ArtifactMgr.Create(suite.Context(), subaf)
|
||||
suite.Nil(err, fmt.Sprintf("Add artifact failed for %d", repositoryID))
|
||||
}
|
||||
_, err := pkg.ArtifactMgr.Create(suite.Context(), subaf)
|
||||
suite.Nil(err, fmt.Sprintf("Add artifact failed for %d", repositoryID))
|
||||
|
||||
af := &artifact.Artifact{
|
||||
Type: "Subject",
|
||||
|
@ -124,7 +148,8 @@ func (suite *MiddlewareTestSuite) addArtAcc(pid, repositoryID int64, repositoryN
|
|||
accid, err := accessory.Mgr.Create(suite.Context(), accessorymodel.AccessoryData{
|
||||
ID: 1,
|
||||
ArtifactID: afid,
|
||||
SubArtifactDigest: subaf.Digest,
|
||||
SubArtifactDigest: dgt,
|
||||
SubArtifactRepo: repositoryName,
|
||||
Digest: accdgt,
|
||||
Type: accessorymodel.TypeSubject,
|
||||
})
|
||||
|
@ -165,6 +190,40 @@ func (suite *MiddlewareTestSuite) TestSubject() {
|
|||
})
|
||||
}
|
||||
|
||||
func (suite *MiddlewareTestSuite) TestSubjectAfterAcc() {
|
||||
// add acc, with subject digest
|
||||
// add subject
|
||||
suite.WithProject(func(projectID int64, projectName string) {
|
||||
name := fmt.Sprintf("%s/hello-world", projectName)
|
||||
_, repoId, err := repository.Ctl.Ensure(suite.Context(), name)
|
||||
|
||||
_, descriptor, req := suite.prepare(name, suite.DigestString(), true)
|
||||
suite.Nil(err)
|
||||
|
||||
subArtDigest := descriptor.Digest.String()
|
||||
accArtDigest := suite.DigestString()
|
||||
|
||||
accID := suite.addArtAcc(projectID, repoId, name, subArtDigest, accArtDigest)
|
||||
subArtID := suite.addArt(projectID, repoId, name, subArtDigest)
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
next := suite.NextHandler(http.StatusCreated, map[string]string{"Docker-Content-Digest": subArtDigest})
|
||||
Middleware()(next).ServeHTTP(res, req)
|
||||
suite.Equal(http.StatusCreated, res.Code)
|
||||
|
||||
accs, err := accessory.Mgr.List(suite.Context(), &q.Query{
|
||||
Keywords: map[string]interface{}{
|
||||
"SubjectArtifactDigest": subArtDigest,
|
||||
"SubjectArtifactRepo": name,
|
||||
},
|
||||
})
|
||||
suite.Equal(1, len(accs))
|
||||
suite.Equal(subArtDigest, accs[0].GetData().SubArtifactDigest)
|
||||
suite.Equal(subArtID, accs[0].GetData().SubArtifactID)
|
||||
suite.Equal(accID, accs[0].GetData().ID)
|
||||
})
|
||||
}
|
||||
|
||||
func (suite *MiddlewareTestSuite) TestSubjectDup() {
|
||||
suite.WithProject(func(projectID int64, projectName string) {
|
||||
name := fmt.Sprintf("%s/hello-world", projectName)
|
||||
|
@ -174,7 +233,7 @@ func (suite *MiddlewareTestSuite) TestSubjectDup() {
|
|||
_, descriptor, req := suite.prepare(name, subArtDigest)
|
||||
suite.Nil(err)
|
||||
|
||||
accID := suite.addArtAcc(projectID, repoId, name, subArtDigest, descriptor.Digest.String())
|
||||
accID := suite.addArtAcc(projectID, repoId, name, subArtDigest, descriptor.Digest.String(), true)
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
next := suite.NextHandler(http.StatusCreated, map[string]string{"Docker-Content-Digest": descriptor.Digest.String()})
|
||||
|
|
|
@ -154,6 +154,20 @@ func (_m *DAO) List(ctx context.Context, query *q.Query) ([]*dao.Accessory, erro
|
|||
return r0, r1
|
||||
}
|
||||
|
||||
// Update provides a mock function with given fields: ctx, accessory
|
||||
func (_m *DAO) Update(ctx context.Context, accessory *dao.Accessory) error {
|
||||
ret := _m.Called(ctx, accessory)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *dao.Accessory) error); ok {
|
||||
r0 = rf(ctx, accessory)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
type mockConstructorTestingTNewDAO interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
|
|
|
@ -158,6 +158,20 @@ func (_m *Manager) List(ctx context.Context, query *q.Query) ([]model.Accessory,
|
|||
return r0, r1
|
||||
}
|
||||
|
||||
// Update provides a mock function with given fields: ctx, _a1
|
||||
func (_m *Manager) Update(ctx context.Context, _a1 model.AccessoryData) error {
|
||||
ret := _m.Called(ctx, _a1)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, model.AccessoryData) error); ok {
|
||||
r0 = rf(ctx, _a1)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
type mockConstructorTestingTNewManager interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
|
|
Loading…
Reference in New Issue