Skip to content

Commit

Permalink
add middleware for artifact with subject (#18369)
Browse files Browse the repository at this point in the history
As for the distribution spec 1.1, it supports client to push an manifest with subject field. By leverging this fidle, harbor could build up the linkage between the subject artifact and it's accessories.

Signed-off-by: wang yan <[email protected]>
  • Loading branch information
wy65701436 authored Mar 19, 2023
1 parent ff01efc commit bb291aa
Show file tree
Hide file tree
Showing 9 changed files with 440 additions and 3 deletions.
1 change: 1 addition & 0 deletions src/core/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import (
"github.com/goharbor/harbor/src/migration"
_ "github.com/goharbor/harbor/src/pkg/accessory/model/base"
_ "github.com/goharbor/harbor/src/pkg/accessory/model/cosign"
_ "github.com/goharbor/harbor/src/pkg/accessory/model/subject"
"github.com/goharbor/harbor/src/pkg/audit"
dbCfg "github.com/goharbor/harbor/src/pkg/config/db"
_ "github.com/goharbor/harbor/src/pkg/config/inmemory"
Expand Down
4 changes: 4 additions & 0 deletions src/pkg/accessory/model/accessory.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,15 @@ type RefIdentifier interface {
const (
// TypeNone ...
TypeNone = "base"

// TypeCosignSignature ...
TypeCosignSignature = "signature.cosign"

// TypeNydusAccelerator ...
TypeNydusAccelerator = "accelerator.nydus"

// TypeSubject ...
TypeSubject = "subject.accessory"
)

// AccessoryData ...
Expand Down
46 changes: 46 additions & 0 deletions src/pkg/accessory/model/subject/subject.go
Original file line number Diff line number Diff line change
@@ -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 subject

import (
"github.com/goharbor/harbor/src/pkg/accessory/model"
"github.com/goharbor/harbor/src/pkg/accessory/model/base"
)

// Subject model
type Subject struct {
base.Default
}

// Kind gives the reference type of subject.
func (s *Subject) Kind() string {
return model.RefHard
}

// IsHard ...
func (s *Subject) IsHard() bool {
return true
}

// New returns subject
func New(data model.AccessoryData) model.Accessory {
return &Subject{base.Default{
Data: data,
}}
}

func init() {
model.Register(model.TypeSubject, New)
}
73 changes: 73 additions & 0 deletions src/pkg/accessory/model/subject/subject_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package subject

import (
"testing"

"github.com/stretchr/testify/suite"

"github.com/goharbor/harbor/src/pkg/accessory/model"
htesting "github.com/goharbor/harbor/src/testing"
)

type SubjectTestSuite struct {
htesting.Suite
accessory model.Accessory
digest string
subDigest string
}

func (suite *SubjectTestSuite) SetupSuite() {
suite.digest = suite.DigestString()
suite.subDigest = suite.DigestString()
suite.accessory, _ = model.New(model.TypeSubject,
model.AccessoryData{
ArtifactID: 1,
SubArtifactDigest: suite.subDigest,
Size: 4321,
Digest: suite.digest,
})
}

func (suite *SubjectTestSuite) TestGetID() {
suite.Equal(int64(0), suite.accessory.GetData().ID)
}

func (suite *SubjectTestSuite) TestGetArtID() {
suite.Equal(int64(1), suite.accessory.GetData().ArtifactID)
}

func (suite *SubjectTestSuite) TestSubGetArtID() {
suite.Equal(suite.subDigest, suite.accessory.GetData().SubArtifactDigest)
}

func (suite *SubjectTestSuite) TestSubGetSize() {
suite.Equal(int64(4321), suite.accessory.GetData().Size)
}

func (suite *SubjectTestSuite) TestSubGetDigest() {
suite.Equal(suite.digest, suite.accessory.GetData().Digest)
}

func (suite *SubjectTestSuite) TestSubGetType() {
suite.Equal(model.TypeSubject, suite.accessory.GetData().Type)
}

func (suite *SubjectTestSuite) TestSubGetRefType() {
suite.Equal(model.RefHard, suite.accessory.Kind())
}

func (suite *SubjectTestSuite) TestIsSoft() {
suite.False(suite.accessory.IsSoft())
}

func (suite *SubjectTestSuite) TestIsHard() {
suite.True(suite.accessory.IsHard())
}

func (suite *SubjectTestSuite) TestDisplay() {
suite.False(suite.accessory.Display())
}

func TestCacheTestSuite(t *testing.T) {
suite.Run(t, new(SubjectTestSuite))
}
115 changes: 115 additions & 0 deletions src/server/middleware/subject/subject.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package subject

import (
"context"
"encoding/json"
"io"
"net/http"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"

"github.com/goharbor/harbor/src/controller/artifact"
"github.com/goharbor/harbor/src/lib"
"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/pkg/accessory"
"github.com/goharbor/harbor/src/pkg/accessory/model"
"github.com/goharbor/harbor/src/server/middleware"
)

/*
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"size": 7023,
"digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7"
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"size": 32654,
"digest": "sha256:9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0"
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"size": 16724,
"digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b"
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"size": 73109,
"digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736"
}
],
"subject": {
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 7682,
"digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270"
},
"annotations": {
"com.example.key1": "value1",
"com.example.key2": "value2"
}
}
*/
func Middleware() func(http.Handler) http.Handler {
return middleware.AfterResponse(func(w http.ResponseWriter, r *http.Request, statusCode int) error {
if statusCode != http.StatusCreated {
return nil
}

ctx := r.Context()
logger := log.G(ctx).WithFields(log.Fields{"middleware": "subject"})

none := lib.ArtifactInfo{}
info := lib.GetArtifactInfo(ctx)
if info == none {
return errors.New("artifactinfo middleware required before this middleware").WithCode(errors.NotFoundCode)
}

body, err := io.ReadAll(r.Body)
if err != nil {
return err
}

mf := &ocispec.Manifest{}
if err := json.Unmarshal(body, mf); err != nil {
logger.Errorf("unmarshal manifest failed, error: %v", err)
return err
}

if mf.Subject != nil {
subjectArt, err := artifact.Ctl.GetByReference(ctx, info.Repository, mf.Subject.Digest.String(), nil)
if err != nil {
logger.Errorf("failed to get subject artifact: %s, error: %v", mf.Subject.Digest, err)
return err
}
art, err := artifact.Ctl.GetByReference(ctx, info.Repository, info.Reference, nil)
if err != nil {
logger.Errorf("failed to get artifact with subject field: %s, error: %v", info.Reference, err)
return err
}

if err := orm.WithTransaction(func(ctx context.Context) error {
_, err := accessory.Mgr.Create(ctx, model.AccessoryData{
ArtifactID: art.ID,
SubArtifactDigest: subjectArt.Digest,
Size: art.Size,
Digest: art.Digest,
Type: model.TypeSubject,
})
return err
})(orm.SetTransactionOpNameToContext(ctx, "tx-create-subject-accessory")); err != nil {
if !errors.IsConflictErr(err) {
logger.Errorf("failed to create subject accessory artifact: %s, error: %v", art.Digest, err)
return err
}
}
}

return nil
})
}
Loading

0 comments on commit bb291aa

Please sign in to comment.