Merge pull request #10535 from wy65701436/middleware-immutabletag

add immutable tag middleware into new v2 handler
This commit is contained in:
Wang Yan 2020-01-20 23:28:35 +08:00 committed by GitHub
commit 9ac5fa0c83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 468 additions and 2 deletions

View 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
}

View File

@ -0,0 +1,3 @@
package immutable
// Tests are in the push_mf_test

View 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,
}
}

View 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
}

View 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))
}

View File

@ -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