From 742e7ded00afc6f59d6906d7cf34d966dfd0dca4 Mon Sep 17 00:00:00 2001 From: Wang Yan Date: Fri, 3 Dec 2021 14:34:02 +0800 Subject: [PATCH] add accessory dao service (#16045) Signed-off-by: wang yan --- .../postgresql/0080_2.5.0_schema.up.sql | 22 ++ src/lib/icon/const.go | 4 + src/pkg/accessory/dao/dao.go | 130 +++++++++ src/pkg/accessory/dao/dao_test.go | 263 ++++++++++++++++++ src/pkg/accessory/dao/model.go | 40 +++ src/pkg/accessory/manager.go | 121 ++++++++ src/pkg/accessory/manager_test.go | 101 +++++++ src/pkg/accessory/model/accessory.go | 110 ++++++++ src/pkg/accessory/model/accessory_test.go | 55 ++++ src/pkg/accessory/model/base/base.go | 55 ++++ src/pkg/accessory/model/base/base_test.go | 65 +++++ src/pkg/accessory/model/cosign/cosign.go | 46 +++ src/pkg/accessory/model/cosign/cosign_test.go | 65 +++++ src/testing/pkg/accessory/dao/dao.go | 133 +++++++++ src/testing/pkg/accessory/model/accessory.go | 69 +++++ src/testing/pkg/pkg.go | 2 + 16 files changed, 1281 insertions(+) create mode 100644 make/migrations/postgresql/0080_2.5.0_schema.up.sql create mode 100644 src/pkg/accessory/dao/dao.go create mode 100644 src/pkg/accessory/dao/dao_test.go create mode 100644 src/pkg/accessory/dao/model.go create mode 100644 src/pkg/accessory/manager.go create mode 100644 src/pkg/accessory/manager_test.go create mode 100644 src/pkg/accessory/model/accessory.go create mode 100644 src/pkg/accessory/model/accessory_test.go create mode 100644 src/pkg/accessory/model/base/base.go create mode 100644 src/pkg/accessory/model/base/base_test.go create mode 100644 src/pkg/accessory/model/cosign/cosign.go create mode 100644 src/pkg/accessory/model/cosign/cosign_test.go create mode 100644 src/testing/pkg/accessory/dao/dao.go create mode 100644 src/testing/pkg/accessory/model/accessory.go diff --git a/make/migrations/postgresql/0080_2.5.0_schema.up.sql b/make/migrations/postgresql/0080_2.5.0_schema.up.sql new file mode 100644 index 000000000..5be423aa0 --- /dev/null +++ b/make/migrations/postgresql/0080_2.5.0_schema.up.sql @@ -0,0 +1,22 @@ +/* create table of accessory */ +CREATE TABLE IF NOT EXISTS artifact_accessory ( + id SERIAL PRIMARY KEY NOT NULL, + /* + the artifact id of the accessory itself. + */ + artifact_id bigint, + /* + the subject artifact id of the accessory. + */ + subject_artifact_id bigint, + /* + the type of the accessory, like signature.cosign. + */ + type varchar(256), + size bigint, + digest varchar(1024), + creation_time timestamp default CURRENT_TIMESTAMP, + FOREIGN KEY (artifact_id) REFERENCES artifact(id), + FOREIGN KEY (subject_artifact_id) REFERENCES artifact(id), + CONSTRAINT unique_artifact_accessory UNIQUE (artifact_id, subject_artifact_id) +); diff --git a/src/lib/icon/const.go b/src/lib/icon/const.go index e43a2e791..da7330628 100644 --- a/src/lib/icon/const.go +++ b/src/lib/icon/const.go @@ -6,4 +6,8 @@ const ( DigestOfIconChart = "sha256:61cf3a178ff0f75bf08a25d96b75cf7355dc197749a9f128ed3ef34b0df05518" DigestOfIconCNAB = "sha256:089bdda265c14d8686111402c8ad629e8177a1ceb7dcd0f7f39b6480f623b3bd" DigestOfIconDefault = "sha256:da834479c923584f4cbcdecc0dac61f32bef1d51e8aae598cf16bd154efab49f" + + // ToDo add the accessories images + DigestOfIconAccDefault = "" + DigestOfIconAccCosign = "" ) diff --git a/src/pkg/accessory/dao/dao.go b/src/pkg/accessory/dao/dao.go new file mode 100644 index 000000000..4d9a2e8fe --- /dev/null +++ b/src/pkg/accessory/dao/dao.go @@ -0,0 +1,130 @@ +// 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 dao + +import ( + "context" + "github.com/goharbor/harbor/src/lib/errors" + "github.com/goharbor/harbor/src/lib/orm" + "github.com/goharbor/harbor/src/lib/q" +) + +// DAO is the data access object for accessory +type DAO interface { + // Count returns the total count of accessory according to the query + Count(ctx context.Context, query *q.Query) (total int64, err error) + // List accessory according to the query + List(ctx context.Context, query *q.Query) (accs []*Accessory, err error) + // Get the accessory specified by ID + Get(ctx context.Context, id int64) (accessory *Accessory, err error) + // Create the accessory + Create(ctx context.Context, accessory *Accessory) (id int64, err error) + // Delete the accessory specified by ID + Delete(ctx context.Context, id int64) (err error) + // DeleteOfArtifact deletes all accessory attached to the artifact + DeleteOfArtifact(ctx context.Context, subArtifactID int64) (err error) +} + +// New returns an instance of the default DAO +func New() DAO { + return &dao{} +} + +type dao struct{} + +func (d *dao) Count(ctx context.Context, query *q.Query) (int64, error) { + qs, err := orm.QuerySetterForCount(ctx, &Accessory{}, query) + if err != nil { + return 0, err + } + return qs.Count() +} + +func (d *dao) List(ctx context.Context, query *q.Query) ([]*Accessory, error) { + accs := []*Accessory{} + qs, err := orm.QuerySetter(ctx, &Accessory{}, query) + if err != nil { + return nil, err + } + if _, err = qs.All(&accs); err != nil { + return nil, err + } + return accs, nil +} + +func (d *dao) Get(ctx context.Context, id int64) (*Accessory, error) { + acc := &Accessory{ + ID: id, + } + ormer, err := orm.FromContext(ctx) + if err != nil { + return nil, err + } + if err := ormer.Read(acc); err != nil { + if e := orm.AsNotFoundError(err, "accessory %d not found", id); e != nil { + err = e + } + return nil, err + } + return acc, nil +} + +func (d *dao) Create(ctx context.Context, acc *Accessory) (int64, error) { + ormer, err := orm.FromContext(ctx) + if err != nil { + return 0, err + } + id, err := ormer.Insert(acc) + if err != nil { + if e := orm.AsConflictError(err, "accessory %s already exists under the artifact %d", + acc.Digest, acc.SubjectArtifactID); e != nil { + err = e + } else if e := orm.AsForeignKeyError(err, "the accessory %s tries to attach to a non existing artifact %d", + acc.Digest, acc.SubjectArtifactID); e != nil { + err = e + } + } + return id, err +} + +func (d *dao) Delete(ctx context.Context, id int64) error { + ormer, err := orm.FromContext(ctx) + if err != nil { + return err + } + n, err := ormer.Delete(&Accessory{ + ID: id, + }) + if err != nil { + return err + } + if n == 0 { + return errors.NotFoundError(nil).WithMessage("accessory %d not found", id) + } + return nil +} + +func (d *dao) DeleteOfArtifact(ctx context.Context, subArtifactID int64) error { + qs, err := orm.QuerySetter(ctx, &Accessory{}, &q.Query{ + Keywords: map[string]interface{}{ + "SubjectArtifactID": subArtifactID, + }, + }) + if err != nil { + return err + } + _, err = qs.Delete() + return err +} diff --git a/src/pkg/accessory/dao/dao_test.go b/src/pkg/accessory/dao/dao_test.go new file mode 100644 index 000000000..1fb1b8f02 --- /dev/null +++ b/src/pkg/accessory/dao/dao_test.go @@ -0,0 +1,263 @@ +// 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 dao + +import ( + "context" + "fmt" + beegoorm "github.com/astaxie/beego/orm" + common_dao "github.com/goharbor/harbor/src/common/dao" + errors "github.com/goharbor/harbor/src/lib/errors" + "github.com/goharbor/harbor/src/lib/orm" + "github.com/goharbor/harbor/src/lib/q" + artdao "github.com/goharbor/harbor/src/pkg/artifact/dao" + htesting "github.com/goharbor/harbor/src/testing" + "github.com/stretchr/testify/suite" + "testing" +) + +type daoTestSuite struct { + htesting.Suite + dao DAO + artDAO artdao.DAO + artifactID int64 + subArtifactID int64 + accID int64 + ctx context.Context +} + +func (d *daoTestSuite) SetupSuite() { + d.dao = New() + common_dao.PrepareTestForPostgresSQL() + d.ctx = orm.NewContext(nil, beegoorm.NewOrm()) + d.ClearTables = []string{"artifact", "artifact_accessory"} + + d.artDAO = artdao.New() + artifactID, err := d.artDAO.Create(d.ctx, &artdao.Artifact{ + Type: "IMAGE", + MediaType: "application/vnd.oci.image.config.v1+json", + ManifestMediaType: "application/vnd.oci.image.manifest.v1+json", + ProjectID: 1, + RepositoryID: 1000, + Digest: d.DigestString(), + }) + d.Require().Nil(err) + d.subArtifactID = artifactID + + d.artDAO = artdao.New() + artifactID, err = d.artDAO.Create(d.ctx, &artdao.Artifact{ + Type: "Signature", + MediaType: "application/vnd.oci.image.config.v1+json", + ManifestMediaType: "application/vnd.oci.image.manifest.v1+json", + ProjectID: 1, + RepositoryID: 1000, + Digest: d.DigestString(), + }) + d.Require().Nil(err) + d.artifactID = artifactID + + accID, err := d.dao.Create(d.ctx, &Accessory{ + ArtifactID: d.artifactID, + SubjectArtifactID: d.subArtifactID, + Digest: d.DigestString(), + Size: 1234, + Type: "cosign.signature", + }) + d.Require().Nil(err) + d.accID = accID +} + +func (d *daoTestSuite) TearDownSuite() { + err := d.dao.Delete(d.ctx, d.accID) + d.Require().Nil(err) + err = d.artDAO.Delete(d.ctx, d.artifactID) + d.Require().Nil(err) + err = d.artDAO.Delete(d.ctx, d.subArtifactID) + d.Require().Nil(err) +} + +func (d *daoTestSuite) SetupTest() { +} + +func (d *daoTestSuite) TearDownTest() { +} + +func (d *daoTestSuite) TestCount() { + // nil query + total, err := d.dao.Count(d.ctx, nil) + d.Require().Nil(err) + d.True(total > 0) + total, err = d.dao.Count(d.ctx, &q.Query{ + Keywords: map[string]interface{}{ + "SubjectArtifactID": d.subArtifactID, + }, + }) + d.Require().Nil(err) + d.Equal(int64(1), total) +} + +func (d *daoTestSuite) TestList() { + // nil query + accs, err := d.dao.List(d.ctx, nil) + d.Require().Nil(err) + found := false + for _, acc := range accs { + if acc.Type == "cosign.signature" { + found = true + break + } + } + d.True(found) + + accs, err = d.dao.List(d.ctx, &q.Query{ + Keywords: map[string]interface{}{ + "SubjectArtifactID": d.subArtifactID, + }, + }) + d.Require().Nil(err) + d.Require().Equal(1, len(accs)) + d.Equal(d.accID, accs[0].ID) +} + +func (d *daoTestSuite) TestGet() { + _, err := d.dao.Get(d.ctx, 10000) + d.Require().NotNil(err) + d.True(errors.IsErr(err, errors.NotFoundCode)) + + acc, err := d.dao.Get(d.ctx, d.accID) + d.Require().Nil(err) + d.Require().NotNil(acc) + d.Equal(d.accID, acc.ID) +} + +func (d *daoTestSuite) TestCreate() { + // the happy pass case is covered in Setup + + // conflict + acc := &Accessory{ + ArtifactID: d.artifactID, + SubjectArtifactID: d.subArtifactID, + Digest: d.DigestString(), + Size: 1234, + Type: "cosign.signature", + } + _, err := d.dao.Create(d.ctx, acc) + d.Require().NotNil(err) + d.True(errors.IsErr(err, errors.ConflictCode)) + + // violating foreign key constraint: the artifact that the tag tries to attach doesn't exist + acc = &Accessory{ + ArtifactID: 999, + SubjectArtifactID: 998, + Digest: d.DigestString(), + Size: 1234, + Type: "cosign.signature", + } + _, err = d.dao.Create(d.ctx, acc) + d.Require().NotNil(err) + d.True(errors.IsErr(err, errors.ViolateForeignKeyConstraintCode)) +} + +func (d *daoTestSuite) TestDelete() { + // happy pass is covered in TearDown + + // not exist + err := d.dao.Delete(d.ctx, 10000) + d.Require().NotNil(err) + var e *errors.Error + d.Require().True(errors.As(err, &e)) + d.Equal(errors.NotFoundCode, e.Code) +} + +func (d *daoTestSuite) TestDeleteOfArtifact() { + subArtID, err := d.artDAO.Create(d.ctx, &artdao.Artifact{ + Type: "IMAGE", + MediaType: "application/vnd.oci.image.config.v1+json", + ManifestMediaType: "application/vnd.oci.image.manifest.v1+json", + ProjectID: 1, + RepositoryID: 1000, + Digest: d.DigestString(), + }) + d.Require().Nil(err) + defer d.artDAO.Delete(d.ctx, subArtID) + + artID1, err := d.artDAO.Create(d.ctx, &artdao.Artifact{ + Type: "Signature", + MediaType: "application/vnd.oci.image.config.v1+json", + ManifestMediaType: "application/vnd.oci.image.manifest.v1+json", + ProjectID: 1, + RepositoryID: 1000, + Digest: d.DigestString(), + }) + d.Require().Nil(err) + defer d.artDAO.Delete(d.ctx, artID1) + + artID2, err := d.artDAO.Create(d.ctx, &artdao.Artifact{ + Type: "Signature", + MediaType: "application/vnd.oci.image.config.v1+json", + ManifestMediaType: "application/vnd.oci.image.manifest.v1+json", + ProjectID: 1, + RepositoryID: 1000, + Digest: d.DigestString(), + }) + d.Require().Nil(err) + defer d.artDAO.Delete(d.ctx, artID2) + + acc1 := &Accessory{ + ArtifactID: artID1, + SubjectArtifactID: subArtID, + Digest: d.DigestString(), + Size: 1234, + Type: "cosign.signature", + } + _, err = d.dao.Create(d.ctx, acc1) + d.Require().Nil(err) + + acc2 := &Accessory{ + ArtifactID: artID2, + SubjectArtifactID: subArtID, + Digest: d.DigestString(), + Size: 1234, + Type: "cosign.signature", + } + _, err = d.dao.Create(d.ctx, acc2) + d.Require().Nil(err) + + accs, err := d.dao.List(d.ctx, &q.Query{ + Keywords: map[string]interface{}{ + "SubjectArtifactID": subArtID, + }, + }) + for _, acc := range accs { + fmt.Println(acc.ID) + } + d.Require().Nil(err) + d.Require().Len(accs, 2) + + err = d.dao.DeleteOfArtifact(d.ctx, subArtID) + d.Require().Nil(err) + + accs, err = d.dao.List(d.ctx, &q.Query{ + Keywords: map[string]interface{}{ + "SubjectArtifactID": subArtID, + }, + }) + d.Require().Nil(err) + d.Require().Len(accs, 0) +} + +func TestDaoTestSuite(t *testing.T) { + suite.Run(t, &daoTestSuite{}) +} diff --git a/src/pkg/accessory/dao/model.go b/src/pkg/accessory/dao/model.go new file mode 100644 index 000000000..fc3b52334 --- /dev/null +++ b/src/pkg/accessory/dao/model.go @@ -0,0 +1,40 @@ +// 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 dao + +import ( + "github.com/astaxie/beego/orm" + "time" +) + +func init() { + orm.RegisterModel(&Accessory{}) +} + +// Accessory model in database +type Accessory struct { + ID int64 `orm:"pk;auto;column(id)" json:"id"` + ArtifactID int64 `orm:"column(artifact_id)" json:"artifact_id"` + SubjectArtifactID int64 `orm:"column(subject_artifact_id)" json:"subject_artifact_id"` + Type string `orm:"column(type)" json:"type"` + Size int64 `orm:"column(size)" json:"size"` + Digest string `orm:"column(digest)" json:"digest"` + CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"` +} + +// TableName for artifact reference +func (a *Accessory) TableName() string { + return "artifact_accessory" +} diff --git a/src/pkg/accessory/manager.go b/src/pkg/accessory/manager.go new file mode 100644 index 000000000..d12c2ff87 --- /dev/null +++ b/src/pkg/accessory/manager.go @@ -0,0 +1,121 @@ +// 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 accessory + +import ( + "context" + "github.com/goharbor/harbor/src/lib/errors" + "github.com/goharbor/harbor/src/lib/q" + "github.com/goharbor/harbor/src/pkg/accessory/dao" + "github.com/goharbor/harbor/src/pkg/accessory/model" + + _ "github.com/goharbor/harbor/src/pkg/accessory/model/base" + _ "github.com/goharbor/harbor/src/pkg/accessory/model/cosign" +) + +var ( + // Mgr is a global artifact manager instance + Mgr = NewManager() +) + +// Manager is the only interface of artifact module to provide the management functions for artifacts +type Manager interface { + // 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(ctx context.Context, query *q.Query) (total int64, err error) + // List tags according to the query + List(ctx context.Context, query *q.Query) (accs []model.Accessory, err error) + // Create the tag and returns the ID + Create(ctx context.Context, accessory model.AccessoryData) (id int64, err error) + // Delete the tag specified by ID + Delete(ctx context.Context, id int64) (err error) + // DeleteOfArtifact deletes all tags attached to the artifact + DeleteOfArtifact(ctx context.Context, artifactID int64) (err error) +} + +// NewManager returns an instance of the default manager +func NewManager() Manager { + return &manager{ + dao.New(), + } +} + +var _ Manager = &manager{} + +type manager struct { + dao dao.DAO +} + +func (m *manager) Get(ctx context.Context, id int64) (model.Accessory, error) { + acc, err := m.dao.Get(ctx, id) + if err != nil { + return nil, err + } + return model.New(acc.Type, model.AccessoryData{ + ID: acc.ID, + ArtifactID: acc.ArtifactID, + SubArtifactID: acc.SubjectArtifactID, + Size: acc.Size, + Digest: acc.Digest, + CreatTime: acc.CreationTime, + }) +} + +func (m *manager) Count(ctx context.Context, query *q.Query) (int64, error) { + return m.dao.Count(ctx, query) +} + +func (m *manager) List(ctx context.Context, query *q.Query) ([]model.Accessory, error) { + accsDao, err := m.dao.List(ctx, query) + if err != nil { + return nil, err + } + var accs []model.Accessory + for _, accD := range accsDao { + acc, err := model.New(accD.Type, model.AccessoryData{ + ID: accD.ID, + ArtifactID: accD.ArtifactID, + SubArtifactID: accD.SubjectArtifactID, + Size: accD.Size, + Digest: accD.Digest, + CreatTime: accD.CreationTime, + }) + if err != nil { + return nil, errors.New(err).WithCode(errors.BadRequestCode) + } + accs = append(accs, acc) + } + return accs, nil +} + +func (m *manager) Create(ctx context.Context, accessory model.AccessoryData) (int64, error) { + acc := &dao.Accessory{ + ArtifactID: accessory.ArtifactID, + SubjectArtifactID: accessory.SubArtifactID, + Size: accessory.Size, + Digest: accessory.Digest, + Type: accessory.Type, + } + return m.dao.Create(ctx, acc) +} + +func (m *manager) Delete(ctx context.Context, id int64) error { + return m.dao.Delete(ctx, id) +} + +func (m *manager) DeleteOfArtifact(ctx context.Context, artifactID int64) error { + return m.dao.DeleteOfArtifact(ctx, artifactID) +} diff --git a/src/pkg/accessory/manager_test.go b/src/pkg/accessory/manager_test.go new file mode 100644 index 000000000..0ef1cf66f --- /dev/null +++ b/src/pkg/accessory/manager_test.go @@ -0,0 +1,101 @@ +// 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 accessory + +import ( + "github.com/goharbor/harbor/src/pkg/accessory/dao" + "github.com/goharbor/harbor/src/pkg/accessory/model" + "github.com/goharbor/harbor/src/testing/mock" + testingdao "github.com/goharbor/harbor/src/testing/pkg/accessory/dao" + "github.com/stretchr/testify/suite" + "testing" +) + +type managerTestSuite struct { + suite.Suite + mgr *manager + dao *testingdao.DAO +} + +func (m *managerTestSuite) SetupTest() { + m.dao = &testingdao.DAO{} + m.mgr = &manager{ + dao: m.dao, + } +} + +func (m *managerTestSuite) TestList() { + acc := &dao.Accessory{ + ID: 1, + Type: model.TypeCosignSignature, + } + mock.OnAnything(m.dao, "List").Return([]*dao.Accessory{ + acc, + }, nil) + accs, err := m.mgr.List(nil, nil) + m.Require().Nil(err) + m.Require().Equal(1, len(accs)) + m.Equal(int64(1), accs[0].GetData().ID) + m.dao.AssertExpectations(m.T()) +} + +func (m *managerTestSuite) TestGet() { + acc := &dao.Accessory{ + ID: 1, + Type: model.TypeCosignSignature, + } + mock.OnAnything(m.dao, "Get").Return(acc, nil) + accessory, err := m.mgr.Get(nil, 1) + m.Require().Nil(err) + m.Equal(int64(1), accessory.GetData().ID) + m.dao.AssertExpectations(m.T()) +} + +func (m *managerTestSuite) TestCreate() { + mock.OnAnything(m.dao, "Create").Return(int64(1), nil) + _, err := m.mgr.Create(nil, model.AccessoryData{ + ArtifactID: 1, + Size: 1, + Type: model.TypeCosignSignature, + }) + 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) + m.Require().Nil(err) + m.dao.AssertExpectations(m.T()) +} + +func (m *managerTestSuite) TestCount() { + mock.OnAnything(m.dao, "Count").Return(int64(1), nil) + n, err := m.mgr.Count(nil, nil) + m.Require().Nil(err) + m.Equal(int64(1), n) + m.dao.AssertExpectations(m.T()) +} + +func (m *managerTestSuite) TestDeleteOfArtifact() { + mock.OnAnything(m.dao, "DeleteOfArtifact").Return(nil) + err := m.mgr.DeleteOfArtifact(nil, 1) + m.Require().Nil(err) + m.dao.AssertExpectations(m.T()) +} + +func TestManager(t *testing.T) { + suite.Run(t, &managerTestSuite{}) +} diff --git a/src/pkg/accessory/model/accessory.go b/src/pkg/accessory/model/accessory.go new file mode 100644 index 000000000..9f36a0a85 --- /dev/null +++ b/src/pkg/accessory/model/accessory.go @@ -0,0 +1,110 @@ +// 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 model + +import ( + "fmt" + "sync" + "time" +) + +const ( + // RefNone identifies base reference + RefNone = "base" + // RefSoft identifies soft reference + RefSoft = "soft" + // RefHard identifies hard reference + RefHard = "hard" +) + +type RefProvider interface { + // Kind returns reference Kind. + Kind() string +} + +/* +Soft reference: The accessory is not tied to the subject manifest. +Hard reference: The accessory is tied to the subject manifest. + + Deletion +1. Soft Reference: If the linkage is Soft Reference, when the subject artifact is removed, the linkage will be removed as well, the accessory artifact becomes an individual artifact. +2. Hard Reference: If the linkage is Hard Reference, the accessory artifact will be removed together with the subject artifact. + + Garbage Collection +1. Soft Reference: If the linkage is Soft Reference, Harbor treats the accessory as normal artifact and will not set it as the GC candidate. +2. Hard Reference: If the linkage is Hard Reference, Harbor treats the accessory as an extra stuff of the subject artifact. It means, it being tied to the subject artifact and will be GCed whenever the subject artifact is marked and deleted. +*/ +type RefIdentifier interface { + // IsSoft indicates that the linkage of artifact and its accessory is soft reference. + IsSoft() bool + + // IsHard indicates that the linkage of artifact and its accessory is hard reference. + IsHard() bool +} + +const ( + // TypeNone + TypeNone = "base" + // TypeCosignSignature ... + TypeCosignSignature = "signature.cosign" +) + +// AccessoryData ... +type AccessoryData struct { + ID int64 + ArtifactID int64 + SubArtifactID int64 + Type string + Size int64 + Digest string + CreatTime time.Time +} + +// Accessory: Independent, but linked to an existing subject artifact, which enabling the extendibility of an OCI artifact. +type Accessory interface { + RefProvider + RefIdentifier + GetData() AccessoryData +} + +// NewAccessoryFunc takes data to return a Accessory. +type NewAccessoryFunc func(data AccessoryData) Accessory + +var ( + factories = map[string]NewAccessoryFunc{} + lock sync.RWMutex +) + +// Register register accessory factory for type +func Register(typ string, factory NewAccessoryFunc) { + lock.Lock() + defer lock.Unlock() + + factories[typ] = factory +} + +// New returns accessory instance +func New(typ string, data AccessoryData) (Accessory, error) { + lock.Lock() + defer lock.Unlock() + + factory, ok := factories[typ] + if !ok { + return nil, fmt.Errorf("accessory type %s not support", typ) + } + + data.Type = typ + return factory(data), nil +} diff --git a/src/pkg/accessory/model/accessory_test.go b/src/pkg/accessory/model/accessory_test.go new file mode 100644 index 000000000..65cadd20f --- /dev/null +++ b/src/pkg/accessory/model/accessory_test.go @@ -0,0 +1,55 @@ +// 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 model + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +type AccessoryTestSuite struct { + suite.Suite +} + +func (suite *AccessoryTestSuite) SetupSuite() { + Register("mock", func(data AccessoryData) Accessory { + return nil + }) +} + +func (suite *AccessoryTestSuite) TestNew() { + { + c, err := New("", AccessoryData{}) + suite.Nil(c) + suite.Error(err) + } + + { + c, err := New("mocks", AccessoryData{}) + suite.Nil(c) + suite.Error(err) + } + + { + c, err := New("mock", AccessoryData{}) + suite.Nil(c) + suite.Nil(err) + } +} + +func TestAccessoryTestSuite(t *testing.T) { + suite.Run(t, new(AccessoryTestSuite)) +} diff --git a/src/pkg/accessory/model/base/base.go b/src/pkg/accessory/model/base/base.go new file mode 100644 index 000000000..86c56e306 --- /dev/null +++ b/src/pkg/accessory/model/base/base.go @@ -0,0 +1,55 @@ +// 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 base + +import ( + "github.com/goharbor/harbor/src/pkg/accessory/model" +) + +var _ model.Accessory = (*Default)(nil) + +// Default default model with TypeNone and RefNone +type Default struct { + Data model.AccessoryData +} + +// Kind ... +func (a *Default) Kind() string { + return model.RefNone +} + +// IsSoft ... +func (a *Default) IsSoft() bool { + return false +} + +// IsHard ... +func (a *Default) IsHard() bool { + return false +} + +// GetData ... +func (a *Default) GetData() model.AccessoryData { + return a.Data +} + +// New returns base +func New(data model.AccessoryData) model.Accessory { + return &Default{Data: data} +} + +func init() { + model.Register(model.TypeNone, New) +} diff --git a/src/pkg/accessory/model/base/base_test.go b/src/pkg/accessory/model/base/base_test.go new file mode 100644 index 000000000..06d48e51d --- /dev/null +++ b/src/pkg/accessory/model/base/base_test.go @@ -0,0 +1,65 @@ +package base + +import ( + "github.com/goharbor/harbor/src/pkg/accessory/model" + htesting "github.com/goharbor/harbor/src/testing" + "github.com/stretchr/testify/suite" + "testing" +) + +type BaseTestSuite struct { + htesting.Suite + accessory model.Accessory + digest string +} + +func (suite *BaseTestSuite) SetupSuite() { + suite.digest = suite.DigestString() + suite.accessory, _ = model.New(model.TypeNone, + model.AccessoryData{ + ArtifactID: 1, + SubArtifactID: 2, + Size: 1234, + Digest: suite.digest, + }) +} + +func (suite *BaseTestSuite) TestGetID() { + suite.Equal(int64(0), suite.accessory.GetData().ID) +} + +func (suite *BaseTestSuite) TestGetArtID() { + suite.Equal(int64(1), suite.accessory.GetData().ArtifactID) +} + +func (suite *BaseTestSuite) TestSubGetArtID() { + suite.Equal(int64(2), suite.accessory.GetData().SubArtifactID) +} + +func (suite *BaseTestSuite) TestSubGetSize() { + suite.Equal(int64(1234), suite.accessory.GetData().Size) +} + +func (suite *BaseTestSuite) TestSubGetDigest() { + suite.Equal(suite.digest, suite.accessory.GetData().Digest) +} + +func (suite *BaseTestSuite) TestSubGetType() { + suite.Equal(model.TypeNone, suite.accessory.GetData().Type) +} + +func (suite *BaseTestSuite) TestSubGetRefType() { + suite.Equal(model.RefNone, suite.accessory.Kind()) +} + +func (suite *BaseTestSuite) TestIsSoft() { + suite.False(suite.accessory.IsSoft()) +} + +func (suite *BaseTestSuite) TestIsHard() { + suite.False(suite.accessory.IsHard()) +} + +func TestCacheTestSuite(t *testing.T) { + suite.Run(t, new(BaseTestSuite)) +} diff --git a/src/pkg/accessory/model/cosign/cosign.go b/src/pkg/accessory/model/cosign/cosign.go new file mode 100644 index 000000000..296c6da15 --- /dev/null +++ b/src/pkg/accessory/model/cosign/cosign.go @@ -0,0 +1,46 @@ +// 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 cosign + +import ( + "github.com/goharbor/harbor/src/pkg/accessory/model" + "github.com/goharbor/harbor/src/pkg/accessory/model/base" +) + +// Signature signature model +type Signature struct { + base.Default +} + +// Kind gives the reference type of cosign signature. +func (c *Signature) Kind() string { + return model.RefHard +} + +// IsHard ... +func (c *Signature) IsHard() bool { + return true +} + +// New returns cosign signature +func New(data model.AccessoryData) model.Accessory { + return &Signature{base.Default{ + Data: data, + }} +} + +func init() { + model.Register(model.TypeCosignSignature, New) +} diff --git a/src/pkg/accessory/model/cosign/cosign_test.go b/src/pkg/accessory/model/cosign/cosign_test.go new file mode 100644 index 000000000..4beff5a68 --- /dev/null +++ b/src/pkg/accessory/model/cosign/cosign_test.go @@ -0,0 +1,65 @@ +package cosign + +import ( + "github.com/goharbor/harbor/src/pkg/accessory/model" + htesting "github.com/goharbor/harbor/src/testing" + "github.com/stretchr/testify/suite" + "testing" +) + +type CosignTestSuite struct { + htesting.Suite + accessory model.Accessory + digest string +} + +func (suite *CosignTestSuite) SetupSuite() { + suite.digest = suite.DigestString() + suite.accessory, _ = model.New(model.TypeCosignSignature, + model.AccessoryData{ + ArtifactID: 1, + SubArtifactID: 2, + Size: 4321, + Digest: suite.digest, + }) +} + +func (suite *CosignTestSuite) TestGetID() { + suite.Equal(int64(0), suite.accessory.GetData().ID) +} + +func (suite *CosignTestSuite) TestGetArtID() { + suite.Equal(int64(1), suite.accessory.GetData().ArtifactID) +} + +func (suite *CosignTestSuite) TestSubGetArtID() { + suite.Equal(int64(2), suite.accessory.GetData().SubArtifactID) +} + +func (suite *CosignTestSuite) TestSubGetSize() { + suite.Equal(int64(4321), suite.accessory.GetData().Size) +} + +func (suite *CosignTestSuite) TestSubGetDigest() { + suite.Equal(suite.digest, suite.accessory.GetData().Digest) +} + +func (suite *CosignTestSuite) TestSubGetType() { + suite.Equal(model.TypeCosignSignature, suite.accessory.GetData().Type) +} + +func (suite *CosignTestSuite) TestSubGetRefType() { + suite.Equal(model.RefHard, suite.accessory.Kind()) +} + +func (suite *CosignTestSuite) TestIsSoft() { + suite.False(suite.accessory.IsSoft()) +} + +func (suite *CosignTestSuite) TestIsHard() { + suite.True(suite.accessory.IsHard()) +} + +func TestCacheTestSuite(t *testing.T) { + suite.Run(t, new(CosignTestSuite)) +} diff --git a/src/testing/pkg/accessory/dao/dao.go b/src/testing/pkg/accessory/dao/dao.go new file mode 100644 index 000000000..9fb3803c1 --- /dev/null +++ b/src/testing/pkg/accessory/dao/dao.go @@ -0,0 +1,133 @@ +// Code generated by mockery v2.1.0. DO NOT EDIT. + +package dao + +import ( + context "context" + + dao "github.com/goharbor/harbor/src/pkg/accessory/dao" + mock "github.com/stretchr/testify/mock" + + q "github.com/goharbor/harbor/src/lib/q" +) + +// DAO is an autogenerated mock type for the DAO type +type DAO struct { + mock.Mock +} + +// Count provides a mock function with given fields: ctx, query +func (_m *DAO) Count(ctx context.Context, query *q.Query) (int64, error) { + ret := _m.Called(ctx, query) + + var r0 int64 + if rf, ok := ret.Get(0).(func(context.Context, *q.Query) int64); ok { + r0 = rf(ctx, query) + } else { + r0 = ret.Get(0).(int64) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Create provides a mock function with given fields: ctx, accessory +func (_m *DAO) Create(ctx context.Context, accessory *dao.Accessory) (int64, error) { + ret := _m.Called(ctx, accessory) + + var r0 int64 + if rf, ok := ret.Get(0).(func(context.Context, *dao.Accessory) int64); ok { + r0 = rf(ctx, accessory) + } else { + r0 = ret.Get(0).(int64) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *dao.Accessory) error); ok { + r1 = rf(ctx, accessory) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Delete provides a mock function with given fields: ctx, id +func (_m *DAO) Delete(ctx context.Context, id int64) error { + ret := _m.Called(ctx, id) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteOfArtifact provides a mock function with given fields: ctx, subArtifactID +func (_m *DAO) DeleteOfArtifact(ctx context.Context, subArtifactID int64) error { + ret := _m.Called(ctx, subArtifactID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { + r0 = rf(ctx, subArtifactID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Get provides a mock function with given fields: ctx, id +func (_m *DAO) Get(ctx context.Context, id int64) (*dao.Accessory, error) { + ret := _m.Called(ctx, id) + + var r0 *dao.Accessory + if rf, ok := ret.Get(0).(func(context.Context, int64) *dao.Accessory); ok { + r0 = rf(ctx, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*dao.Accessory) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// List provides a mock function with given fields: ctx, query +func (_m *DAO) List(ctx context.Context, query *q.Query) ([]*dao.Accessory, error) { + ret := _m.Called(ctx, query) + + var r0 []*dao.Accessory + if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*dao.Accessory); ok { + r0 = rf(ctx, query) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*dao.Accessory) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/src/testing/pkg/accessory/model/accessory.go b/src/testing/pkg/accessory/model/accessory.go new file mode 100644 index 000000000..a35e1878a --- /dev/null +++ b/src/testing/pkg/accessory/model/accessory.go @@ -0,0 +1,69 @@ +// Code generated by mockery v2.1.0. DO NOT EDIT. + +package model + +import ( + model "github.com/goharbor/harbor/src/pkg/accessory/model" + mock "github.com/stretchr/testify/mock" +) + +// Accessory is an autogenerated mock type for the Accessory type +type Accessory struct { + mock.Mock +} + +// GetData provides a mock function with given fields: +func (_m *Accessory) GetData() model.AccessoryData { + ret := _m.Called() + + var r0 model.AccessoryData + if rf, ok := ret.Get(0).(func() model.AccessoryData); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(model.AccessoryData) + } + + return r0 +} + +// IsHard provides a mock function with given fields: +func (_m *Accessory) IsHard() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// IsSoft provides a mock function with given fields: +func (_m *Accessory) IsSoft() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// Kind provides a mock function with given fields: +func (_m *Accessory) Kind() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} diff --git a/src/testing/pkg/pkg.go b/src/testing/pkg/pkg.go index 0c0847913..82815f841 100644 --- a/src/testing/pkg/pkg.go +++ b/src/testing/pkg/pkg.go @@ -53,3 +53,5 @@ package pkg //go:generate mockery --case snake --dir ../../pkg/label/dao --name DAO --output ./label/dao --outpkg dao //go:generate mockery --case snake --dir ../../pkg/joblog --name Manager --output ./joblog --outpkg joblog //go:generate mockery --case snake --dir ../../pkg/joblog/dao --name DAO --output ./joblog/dao --outpkg dao +//go:generate mockery --case snake --dir ../../pkg/accessory/model --name Accessory --output ./accessory/model --outpkg model +//go:generate mockery --case snake --dir ../../pkg/accessory/dao --name DAO --output ./accessory/dao --outpkg dao