mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-18 08:15:16 +01:00
Merge pull request #10535 from wy65701436/middleware-immutabletag
add immutable tag middleware into new v2 handler
This commit is contained in:
commit
9ac5fa0c83
101
src/server/middleware/immutable/deletemf.go
Normal file
101
src/server/middleware/immutable/deletemf.go
Normal file
@ -0,0 +1,101 @@
|
||||
package immutable
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
common_util "github.com/goharbor/harbor/src/common/utils"
|
||||
internal_errors "github.com/goharbor/harbor/src/internal/error"
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
"github.com/goharbor/harbor/src/pkg/immutabletag/match/rule"
|
||||
"github.com/goharbor/harbor/src/pkg/q"
|
||||
"github.com/goharbor/harbor/src/pkg/repository"
|
||||
"github.com/goharbor/harbor/src/pkg/tag"
|
||||
"github.com/goharbor/harbor/src/server/middleware"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// MiddlewareDelete ...
|
||||
func MiddlewareDelete() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
if err := handleDelete(req); err != nil {
|
||||
var e *ErrImmutable
|
||||
if errors.As(err, &e) {
|
||||
pkgE := internal_errors.New(e).WithCode(internal_errors.PreconditionCode)
|
||||
msg := internal_errors.NewErrs(pkgE).Error()
|
||||
http.Error(rw, msg, http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
pkgE := internal_errors.New(fmt.Errorf("error occurred when to handle request in immutable handler: %v", err)).WithCode(internal_errors.GeneralCode)
|
||||
msg := internal_errors.NewErrs(pkgE).Error()
|
||||
http.Error(rw, msg, http.StatusInternalServerError)
|
||||
}
|
||||
next.ServeHTTP(rw, req)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// handleDelete ...
|
||||
func handleDelete(req *http.Request) error {
|
||||
mf, ok := middleware.ManifestInfoFromContext(req.Context())
|
||||
if !ok {
|
||||
return errors.New("cannot get the manifest information from request context")
|
||||
}
|
||||
|
||||
_, repoName := common_util.ParseRepository(mf.Repository)
|
||||
total, repos, err := repository.Mgr.List(req.Context(), &q.Query{
|
||||
Keywords: map[string]interface{}{
|
||||
"Name": mf.Repository,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if total == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
total, afs, err := artifact.Mgr.List(req.Context(), &q.Query{
|
||||
Keywords: map[string]interface{}{
|
||||
"ProjectID": mf.ProjectID,
|
||||
"RepositoryID": repos[0].RepositoryID,
|
||||
"Digest": mf.Digest,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if total == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
total, tags, err := tag.Mgr.List(req.Context(), &q.Query{
|
||||
Keywords: map[string]interface{}{
|
||||
"ArtifactID": afs[0].ID,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if total == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, tag := range tags {
|
||||
var matched bool
|
||||
matched, err = rule.NewRuleMatcher(mf.ProjectID).Match(art.Candidate{
|
||||
Repository: repoName,
|
||||
Tag: tag.Name,
|
||||
NamespaceID: mf.ProjectID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if matched {
|
||||
return NewErrImmutable(repoName, tag.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
3
src/server/middleware/immutable/deletemf_test.go
Normal file
3
src/server/middleware/immutable/deletemf_test.go
Normal file
@ -0,0 +1,3 @@
|
||||
package immutable
|
||||
|
||||
// Tests are in the push_mf_test
|
27
src/server/middleware/immutable/error.go
Normal file
27
src/server/middleware/immutable/error.go
Normal file
@ -0,0 +1,27 @@
|
||||
package immutable
|
||||
|
||||
import "fmt"
|
||||
|
||||
// ErrImmutable ...
|
||||
type ErrImmutable struct {
|
||||
repo string
|
||||
tag string
|
||||
}
|
||||
|
||||
// Error ...
|
||||
func (ei *ErrImmutable) Error() string {
|
||||
return fmt.Sprintf("Failed to process request due to '%s:%s' configured as immutable.", ei.repo, ei.tag)
|
||||
}
|
||||
|
||||
// Unwrap ...
|
||||
func (ei *ErrImmutable) Unwrap() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewErrImmutable ...
|
||||
func NewErrImmutable(msg, tag string) error {
|
||||
return &ErrImmutable{
|
||||
repo: msg,
|
||||
tag: tag,
|
||||
}
|
||||
}
|
111
src/server/middleware/immutable/pushmf.go
Normal file
111
src/server/middleware/immutable/pushmf.go
Normal file
@ -0,0 +1,111 @@
|
||||
package immutable
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
common_util "github.com/goharbor/harbor/src/common/utils"
|
||||
internal_errors "github.com/goharbor/harbor/src/internal/error"
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
"github.com/goharbor/harbor/src/pkg/immutabletag/match/rule"
|
||||
"github.com/goharbor/harbor/src/pkg/q"
|
||||
"github.com/goharbor/harbor/src/pkg/repository"
|
||||
"github.com/goharbor/harbor/src/pkg/tag"
|
||||
"github.com/goharbor/harbor/src/server/middleware"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// MiddlewarePush ...
|
||||
func MiddlewarePush() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
if err := handlePush(req); err != nil {
|
||||
var e *ErrImmutable
|
||||
if errors.As(err, &e) {
|
||||
pkgE := internal_errors.New(e).WithCode(internal_errors.PreconditionCode)
|
||||
msg := internal_errors.NewErrs(pkgE).Error()
|
||||
http.Error(rw, msg, http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
pkgE := internal_errors.New(fmt.Errorf("error occurred when to handle request in immutable handler: %v", err)).WithCode(internal_errors.GeneralCode)
|
||||
msg := internal_errors.NewErrs(pkgE).Error()
|
||||
http.Error(rw, msg, http.StatusInternalServerError)
|
||||
}
|
||||
next.ServeHTTP(rw, req)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// handlePush ...
|
||||
// If the pushing image matched by any of immutable rule, will have to whether it is the first time to push it,
|
||||
// as the immutable rule only impacts the existing tag.
|
||||
func handlePush(req *http.Request) error {
|
||||
mf, ok := middleware.ManifestInfoFromContext(req.Context())
|
||||
if !ok {
|
||||
return errors.New("cannot get the manifest information from request context")
|
||||
}
|
||||
|
||||
_, repoName := common_util.ParseRepository(mf.Repository)
|
||||
var matched bool
|
||||
matched, err := rule.NewRuleMatcher(mf.ProjectID).Match(art.Candidate{
|
||||
Repository: repoName,
|
||||
Tag: mf.Tag,
|
||||
NamespaceID: mf.ProjectID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !matched {
|
||||
return nil
|
||||
}
|
||||
|
||||
// match repository ...
|
||||
total, repos, err := repository.Mgr.List(req.Context(), &q.Query{
|
||||
Keywords: map[string]interface{}{
|
||||
"Name": mf.Repository,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if total == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// match artifacts ...
|
||||
total, afs, err := artifact.Mgr.List(req.Context(), &q.Query{
|
||||
Keywords: map[string]interface{}{
|
||||
"ProjectID": mf.ProjectID,
|
||||
"RepositoryID": repos[0].RepositoryID,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if total == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// match tags ...
|
||||
for _, af := range afs {
|
||||
total, tags, err := tag.Mgr.List(req.Context(), &q.Query{
|
||||
Keywords: map[string]interface{}{
|
||||
"ArtifactID": af.ID,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if total == 0 {
|
||||
continue
|
||||
}
|
||||
for _, tag := range tags {
|
||||
// push a existing immutable tag, reject the request
|
||||
if tag.Name == mf.Tag {
|
||||
return NewErrImmutable(repoName, mf.Tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
223
src/server/middleware/immutable/pushmf_test.go
Normal file
223
src/server/middleware/immutable/pushmf_test.go
Normal file
@ -0,0 +1,223 @@
|
||||
package immutable
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/goharbor/harbor/src/core/middlewares/util"
|
||||
internal_orm "github.com/goharbor/harbor/src/internal/orm"
|
||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||
"github.com/goharbor/harbor/src/pkg/repository"
|
||||
"github.com/goharbor/harbor/src/server/middleware"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"time"
|
||||
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/pkg/immutabletag"
|
||||
immu_model "github.com/goharbor/harbor/src/pkg/immutabletag/model"
|
||||
"github.com/goharbor/harbor/src/pkg/tag"
|
||||
tag_model "github.com/goharbor/harbor/src/pkg/tag/model/tag"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type HandlerSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func doPutManifestRequest(projectID int64, projectName, name, tag, dgt string, next ...http.HandlerFunc) int {
|
||||
repository := fmt.Sprintf("%s/%s", projectName, name)
|
||||
|
||||
url := fmt.Sprintf("/v2/%s/manifests/%s", repository, tag)
|
||||
req, _ := http.NewRequest("PUT", url, nil)
|
||||
|
||||
mfInfo := &middleware.ManifestInfo{
|
||||
ProjectID: projectID,
|
||||
Repository: repository,
|
||||
Tag: tag,
|
||||
Digest: dgt,
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
var n http.HandlerFunc
|
||||
if len(next) > 0 {
|
||||
n = next[0]
|
||||
} else {
|
||||
n = func(w http.ResponseWriter, req *http.Request) {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
}
|
||||
*req = *(req.WithContext(internal_orm.NewContext(context.TODO(), dao.GetOrmer())))
|
||||
*req = *(req.WithContext(middleware.NewManifestInfoContext(req.Context(), mfInfo)))
|
||||
h := MiddlewarePush()(n)
|
||||
h.ServeHTTP(util.NewCustomResponseWriter(rr), req)
|
||||
|
||||
return rr.Code
|
||||
}
|
||||
|
||||
func doDeleteManifestRequest(projectID int64, projectName, name, tag, dgt string, next ...http.HandlerFunc) int {
|
||||
repository := fmt.Sprintf("%s/%s", projectName, name)
|
||||
|
||||
url := fmt.Sprintf("/v2/%s/manifests/%s", repository, tag)
|
||||
req, _ := http.NewRequest("DELETE", url, nil)
|
||||
|
||||
mfInfo := &middleware.ManifestInfo{
|
||||
ProjectID: projectID,
|
||||
Repository: repository,
|
||||
Tag: tag,
|
||||
Digest: dgt,
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
var n http.HandlerFunc
|
||||
if len(next) > 0 {
|
||||
n = next[0]
|
||||
} else {
|
||||
n = func(w http.ResponseWriter, req *http.Request) {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
}
|
||||
*req = *(req.WithContext(internal_orm.NewContext(context.TODO(), dao.GetOrmer())))
|
||||
*req = *(req.WithContext(middleware.NewManifestInfoContext(req.Context(), mfInfo)))
|
||||
h := MiddlewareDelete()(n)
|
||||
h.ServeHTTP(util.NewCustomResponseWriter(rr), req)
|
||||
|
||||
return rr.Code
|
||||
}
|
||||
|
||||
func randomString(n int) string {
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = letterBytes[rand.Intn(len(letterBytes))]
|
||||
}
|
||||
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func (suite *HandlerSuite) addProject(projectName string) int64 {
|
||||
projectID, err := dao.AddProject(models.Project{
|
||||
Name: projectName,
|
||||
OwnerID: 1,
|
||||
})
|
||||
suite.Nil(err, fmt.Sprintf("Add project failed for %s", projectName))
|
||||
return projectID
|
||||
}
|
||||
|
||||
func (suite *HandlerSuite) addArt(ctx context.Context, pid, repositoryID int64, dgt string) int64 {
|
||||
af := &artifact.Artifact{
|
||||
Type: "Docker-Image",
|
||||
ProjectID: pid,
|
||||
RepositoryID: repositoryID,
|
||||
Digest: dgt,
|
||||
Size: 1024,
|
||||
PushTime: time.Now(),
|
||||
PullTime: time.Now(),
|
||||
}
|
||||
afid, err := artifact.Mgr.Create(ctx, af)
|
||||
suite.Nil(err, fmt.Sprintf("Add artifact failed for %d", repositoryID))
|
||||
return afid
|
||||
}
|
||||
|
||||
func (suite *HandlerSuite) addRepo(ctx context.Context, pid int64, repo string) int64 {
|
||||
repoRec := &models.RepoRecord{
|
||||
Name: repo,
|
||||
ProjectID: pid,
|
||||
}
|
||||
repoid, err := repository.Mgr.Create(ctx, repoRec)
|
||||
suite.Nil(err, fmt.Sprintf("Add repository failed for %s", repo))
|
||||
return repoid
|
||||
}
|
||||
|
||||
func (suite *HandlerSuite) addTags(ctx context.Context, repoid int64, afid int64, name string) int64 {
|
||||
t := &tag_model.Tag{
|
||||
RepositoryID: repoid,
|
||||
ArtifactID: afid,
|
||||
Name: name,
|
||||
PushTime: time.Time{},
|
||||
PullTime: time.Time{},
|
||||
}
|
||||
tid, err := tag.Mgr.Create(ctx, t)
|
||||
suite.Nil(err, fmt.Sprintf("Add artifact failed for %s", name))
|
||||
return tid
|
||||
}
|
||||
|
||||
func (suite *HandlerSuite) addImmutableRule(pid int64) int64 {
|
||||
metadata := &immu_model.Metadata{
|
||||
ProjectID: pid,
|
||||
Priority: 1,
|
||||
Action: "immutable",
|
||||
Template: "immutable_template",
|
||||
TagSelectors: []*immu_model.Selector{
|
||||
{
|
||||
Kind: "doublestar",
|
||||
Decoration: "matches",
|
||||
Pattern: "release-**",
|
||||
},
|
||||
},
|
||||
ScopeSelectors: map[string][]*immu_model.Selector{
|
||||
"repository": {
|
||||
{
|
||||
Kind: "doublestar",
|
||||
Decoration: "repoMatches",
|
||||
Pattern: "**",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
id, err := immutabletag.ImmuCtr.CreateImmutableRule(metadata)
|
||||
require.NoError(suite.T(), err, "nil error expected but got %s", err)
|
||||
return id
|
||||
}
|
||||
|
||||
func (suite *HandlerSuite) TestPutDeleteManifestCreated() {
|
||||
projectName := randomString(5)
|
||||
repoName := projectName + "/photon"
|
||||
dgt := digest.FromString(randomString(15)).String()
|
||||
ctx := internal_orm.NewContext(context.TODO(), dao.GetOrmer())
|
||||
|
||||
projectID := suite.addProject(projectName)
|
||||
immuRuleID := suite.addImmutableRule(projectID)
|
||||
repoID := suite.addRepo(ctx, projectID, repoName)
|
||||
afID := suite.addArt(ctx, projectID, repoID, dgt)
|
||||
tagID := suite.addTags(ctx, repoID, afID, "release-1.10")
|
||||
|
||||
defer func() {
|
||||
dao.DeleteProject(projectID)
|
||||
artifact.Mgr.Delete(ctx, afID)
|
||||
repository.Mgr.Delete(ctx, repoID)
|
||||
tag.Mgr.Delete(ctx, tagID)
|
||||
immutabletag.ImmuCtr.DeleteImmutableRule(immuRuleID)
|
||||
}()
|
||||
|
||||
code1 := doPutManifestRequest(projectID, projectName, "photon", "release-1.10", dgt)
|
||||
suite.Equal(http.StatusPreconditionFailed, code1)
|
||||
|
||||
code2 := doPutManifestRequest(projectID, projectName, "photon", "latest", dgt)
|
||||
suite.Equal(http.StatusCreated, code2)
|
||||
|
||||
code3 := doDeleteManifestRequest(projectID, projectName, "photon", "release-1.10", dgt)
|
||||
suite.Equal(http.StatusPreconditionFailed, code3)
|
||||
|
||||
code4 := doDeleteManifestRequest(projectID, projectName, "photon", "latest", dgt)
|
||||
suite.Equal(http.StatusPreconditionFailed, code4)
|
||||
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
dao.PrepareTestForPostgresSQL()
|
||||
|
||||
if result := m.Run(); result != 0 {
|
||||
os.Exit(result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunHandlerSuite(t *testing.T) {
|
||||
suite.Run(t, new(HandlerSuite))
|
||||
}
|
@ -23,6 +23,7 @@ import (
|
||||
pkg_repo "github.com/goharbor/harbor/src/pkg/repository"
|
||||
pkg_tag "github.com/goharbor/harbor/src/pkg/tag"
|
||||
"github.com/goharbor/harbor/src/server/middleware"
|
||||
"github.com/goharbor/harbor/src/server/middleware/immutable"
|
||||
"github.com/goharbor/harbor/src/server/middleware/manifestinfo"
|
||||
"github.com/goharbor/harbor/src/server/middleware/readonly"
|
||||
"github.com/goharbor/harbor/src/server/middleware/regtoken"
|
||||
@ -54,8 +55,8 @@ func New(url *url.URL) http.Handler {
|
||||
manifestRouter := rootRouter.Path("/v2/{name:.*}/manifests/{reference}").Subrouter()
|
||||
manifestRouter.NewRoute().Methods(http.MethodGet).Handler(middleware.WithMiddlewares(manifest.NewHandler(project.Mgr, proxy), manifestinfo.Middleware(), regtoken.Middleware()))
|
||||
manifestRouter.NewRoute().Methods(http.MethodHead).Handler(manifest.NewHandler(project.Mgr, proxy))
|
||||
manifestRouter.NewRoute().Methods(http.MethodPut).Handler(middleware.WithMiddlewares(manifest.NewHandler(project.Mgr, proxy), readonly.Middleware()))
|
||||
manifestRouter.NewRoute().Methods(http.MethodDelete).Handler(middleware.WithMiddlewares(manifest.NewHandler(project.Mgr, proxy), readonly.Middleware()))
|
||||
manifestRouter.NewRoute().Methods(http.MethodPut).Handler(middleware.WithMiddlewares(manifest.NewHandler(project.Mgr, proxy), readonly.Middleware(), manifestinfo.Middleware(), immutable.MiddlewarePush()))
|
||||
manifestRouter.NewRoute().Methods(http.MethodDelete).Handler(middleware.WithMiddlewares(manifest.NewHandler(project.Mgr, proxy), readonly.Middleware(), manifestinfo.Middleware(), immutable.MiddlewareDelete()))
|
||||
|
||||
// handle blob
|
||||
// as we need to apply middleware to the blob requests, so create a sub router to handle the blob APIs
|
||||
|
Loading…
Reference in New Issue
Block a user