mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-30 06:03:45 +01:00
add immutable tag middleware
Signed-off-by: wang yan <wangyan@vmware.com>
This commit is contained in:
parent
b231814533
commit
da02b820ad
@ -21,6 +21,7 @@ import (
|
||||
"github.com/goharbor/harbor/src/core/middlewares/chart"
|
||||
"github.com/goharbor/harbor/src/core/middlewares/contenttrust"
|
||||
"github.com/goharbor/harbor/src/core/middlewares/countquota"
|
||||
"github.com/goharbor/harbor/src/core/middlewares/immutable"
|
||||
"github.com/goharbor/harbor/src/core/middlewares/listrepo"
|
||||
"github.com/goharbor/harbor/src/core/middlewares/multiplmanifest"
|
||||
"github.com/goharbor/harbor/src/core/middlewares/readonly"
|
||||
@ -70,6 +71,7 @@ func (b *DefaultCreator) geMiddleware(mName string) alice.Constructor {
|
||||
VULNERABLE: func(next http.Handler) http.Handler { return vulnerable.New(next) },
|
||||
SIZEQUOTA: func(next http.Handler) http.Handler { return sizequota.New(next) },
|
||||
COUNTQUOTA: func(next http.Handler) http.Handler { return countquota.New(next) },
|
||||
IMMUTABLE: func(next http.Handler) http.Handler { return immutable.New(next) },
|
||||
}
|
||||
return middlewares[mName]
|
||||
}
|
||||
|
@ -25,13 +25,14 @@ const (
|
||||
VULNERABLE = "vulnerable"
|
||||
SIZEQUOTA = "sizequota"
|
||||
COUNTQUOTA = "countquota"
|
||||
IMMUTABLE = "immutable"
|
||||
)
|
||||
|
||||
// ChartMiddlewares middlewares for chart server
|
||||
var ChartMiddlewares = []string{CHART}
|
||||
|
||||
// Middlewares with sequential organization
|
||||
var Middlewares = []string{READONLY, URL, MUITIPLEMANIFEST, LISTREPO, CONTENTTRUST, VULNERABLE, SIZEQUOTA, COUNTQUOTA}
|
||||
var Middlewares = []string{READONLY, URL, MUITIPLEMANIFEST, LISTREPO, CONTENTTRUST, VULNERABLE, SIZEQUOTA, IMMUTABLE, COUNTQUOTA}
|
||||
|
||||
// MiddlewaresLocal ...
|
||||
var MiddlewaresLocal = []string{SIZEQUOTA, COUNTQUOTA}
|
||||
|
93
src/core/middlewares/immutable/handler.go
Normal file
93
src/core/middlewares/immutable/handler.go
Normal file
@ -0,0 +1,93 @@
|
||||
// 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 immutable
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
common_util "github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/middlewares/util"
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/immutabletag/match/rule"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type immutableHandler struct {
|
||||
next http.Handler
|
||||
}
|
||||
|
||||
// New ...
|
||||
func New(next http.Handler) http.Handler {
|
||||
return &immutableHandler{
|
||||
next: next,
|
||||
}
|
||||
}
|
||||
|
||||
// ServeHTTP ...
|
||||
func (rh immutableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
if match, _, _ := util.MatchPushManifest(req); !match {
|
||||
rh.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
info, ok := util.ManifestInfoFromContext(req.Context())
|
||||
if !ok {
|
||||
var err error
|
||||
info, err = util.ParseManifestInfoFromPath(req)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
rh.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_, repoName := common_util.ParseRepository(info.Repository)
|
||||
matched, err := rule.NewRuleMatcher(info.ProjectID).Match(art.Candidate{
|
||||
Repository: repoName,
|
||||
Tag: info.Tag,
|
||||
NamespaceID: info.ProjectID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
rh.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
if !matched {
|
||||
rh.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
|
||||
artifactQuery := &models.ArtifactQuery{
|
||||
PID: info.ProjectID,
|
||||
Repo: info.Repository,
|
||||
Tag: info.Tag,
|
||||
}
|
||||
afs, err := dao.ListArtifacts(artifactQuery)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
rh.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
if len(afs) == 0 {
|
||||
rh.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
|
||||
// rule matched and non-existent is a immutable tag
|
||||
http.Error(rw, util.MarshalError("DENIED",
|
||||
fmt.Sprintf("The tag:%s:%s is immutable, cannot be overwrite.", info.Repository, info.Tag)), http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
151
src/core/middlewares/immutable/handler_test.go
Normal file
151
src/core/middlewares/immutable/handler_test.go
Normal file
@ -0,0 +1,151 @@
|
||||
package immutable
|
||||
|
||||
import (
|
||||
"github.com/docker/distribution"
|
||||
"github.com/goharbor/harbor/src/core/middlewares/util"
|
||||
"github.com/opencontainers/go-digest"
|
||||
|
||||
"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/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 := &util.ManifestInfo{
|
||||
ProjectID: projectID,
|
||||
Repository: repository,
|
||||
Tag: tag,
|
||||
Digest: dgt,
|
||||
References: []distribution.Descriptor{
|
||||
{Digest: digest.FromString(randomString(15))},
|
||||
{Digest: digest.FromString(randomString(15))},
|
||||
},
|
||||
}
|
||||
ctx := util.NewManifestInfoContext(req.Context(), mfInfo)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
h := New(http.HandlerFunc(n))
|
||||
h.ServeHTTP(util.NewCustomResponseWriter(rr), req.WithContext(ctx))
|
||||
|
||||
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(pid int64, repo string, tag string) int64 {
|
||||
afid, err := dao.AddArtifact(&models.Artifact{
|
||||
PID: pid,
|
||||
Repo: repo,
|
||||
Tag: tag,
|
||||
Digest: digest.FromString(randomString(15)).String(),
|
||||
Kind: "Docker-Image",
|
||||
})
|
||||
suite.Nil(err, fmt.Sprintf("Add artifact failed for %s", repo))
|
||||
return afid
|
||||
}
|
||||
|
||||
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) TestPutManifestCreated() {
|
||||
projectName := randomString(5)
|
||||
|
||||
projectID := suite.addProject(projectName)
|
||||
immuRuleID := suite.addImmutableRule(projectID)
|
||||
afID := suite.addArt(projectID, projectName+"/photon", "release-1.10")
|
||||
defer func() {
|
||||
dao.DeleteProject(projectID)
|
||||
dao.DeleteArtifact(afID)
|
||||
immutabletag.ImmuCtr.DeleteImmutableRule(immuRuleID)
|
||||
}()
|
||||
|
||||
dgt := digest.FromString(randomString(15)).String()
|
||||
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)
|
||||
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
@ -1,15 +1,15 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common/utils/test"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/immutabletag"
|
||||
"github.com/goharbor/harbor/src/pkg/immutabletag/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MatchTestSuite ...
|
||||
@ -25,7 +25,6 @@ type MatchTestSuite struct {
|
||||
|
||||
// SetupSuite ...
|
||||
func (s *MatchTestSuite) SetupSuite() {
|
||||
test.InitDatabaseFromEnv()
|
||||
s.t = s.T()
|
||||
s.assert = assert.New(s.t)
|
||||
s.require = require.New(s.t)
|
||||
@ -34,34 +33,31 @@ func (s *MatchTestSuite) SetupSuite() {
|
||||
|
||||
func (s *MatchTestSuite) TestImmuMatch() {
|
||||
rule := &model.Metadata{
|
||||
ID: 1,
|
||||
ProjectID: 2,
|
||||
ProjectID: 1,
|
||||
Priority: 1,
|
||||
Template: "latestPushedK",
|
||||
Action: "immuablity",
|
||||
Action: "immutable",
|
||||
Template: "immutable_template",
|
||||
TagSelectors: []*model.Selector{
|
||||
{
|
||||
Kind: "doublestar",
|
||||
Decoration: "matches",
|
||||
Pattern: "release-[\\d\\.]+",
|
||||
Pattern: "release-**",
|
||||
},
|
||||
},
|
||||
ScopeSelectors: map[string][]*model.Selector{
|
||||
"repository": {
|
||||
{
|
||||
Kind: "doublestar",
|
||||
Decoration: "matches",
|
||||
Decoration: "repoMatches",
|
||||
Pattern: "redis",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rule2 := &model.Metadata{
|
||||
ID: 1,
|
||||
ProjectID: 2,
|
||||
ProjectID: 1,
|
||||
Priority: 1,
|
||||
Template: "latestPushedK",
|
||||
Template: "immutable_template",
|
||||
Action: "immuablity",
|
||||
TagSelectors: []*model.Selector{
|
||||
{
|
||||
@ -74,7 +70,7 @@ func (s *MatchTestSuite) TestImmuMatch() {
|
||||
"repository": {
|
||||
{
|
||||
Kind: "doublestar",
|
||||
Decoration: "matches",
|
||||
Decoration: "repoMatches",
|
||||
Pattern: "mysql",
|
||||
},
|
||||
},
|
||||
@ -83,69 +79,52 @@ func (s *MatchTestSuite) TestImmuMatch() {
|
||||
|
||||
id, err := s.ctr.CreateImmutableRule(rule)
|
||||
s.ruleID = id
|
||||
s.require.NotNil(err)
|
||||
s.require.Nil(err)
|
||||
|
||||
id, err = s.ctr.CreateImmutableRule(rule2)
|
||||
s.ruleID2 = id
|
||||
s.require.NotNil(err)
|
||||
s.require.Nil(err)
|
||||
|
||||
match := NewRuleMatcher(2)
|
||||
match := NewRuleMatcher(1)
|
||||
|
||||
c1 := art.Candidate{
|
||||
NamespaceID: 2,
|
||||
Namespace: "immutable",
|
||||
NamespaceID: 1,
|
||||
Namespace: "library",
|
||||
Repository: "redis",
|
||||
Tag: "release-1.10",
|
||||
Kind: art.Image,
|
||||
PushedTime: time.Now().Unix() - 3600,
|
||||
PulledTime: time.Now().Unix(),
|
||||
CreationTime: time.Now().Unix() - 7200,
|
||||
Labels: []string{"label1", "label4", "label5"},
|
||||
}
|
||||
isMatch, err := match.Match(c1)
|
||||
s.require.Equal(isMatch, true)
|
||||
s.require.Nil(err)
|
||||
|
||||
c2 := art.Candidate{
|
||||
NamespaceID: 2,
|
||||
Namespace: "immutable",
|
||||
NamespaceID: 1,
|
||||
Namespace: "library",
|
||||
Repository: "redis",
|
||||
Tag: "1.10",
|
||||
Kind: art.Image,
|
||||
PushedTime: time.Now().Unix() - 3600,
|
||||
PulledTime: time.Now().Unix(),
|
||||
CreationTime: time.Now().Unix() - 7200,
|
||||
Labels: []string{"label1", "label4", "label5"},
|
||||
}
|
||||
isMatch, err = match.Match(c2)
|
||||
s.require.Equal(isMatch, false)
|
||||
s.require.Nil(err)
|
||||
|
||||
c3 := art.Candidate{
|
||||
NamespaceID: 2,
|
||||
NamespaceID: 1,
|
||||
Namespace: "immutable",
|
||||
Repository: "mysql",
|
||||
Tag: "9.4.8",
|
||||
Kind: art.Image,
|
||||
PushedTime: time.Now().Unix() - 3600,
|
||||
PulledTime: time.Now().Unix(),
|
||||
CreationTime: time.Now().Unix() - 7200,
|
||||
Labels: []string{"label1"},
|
||||
}
|
||||
isMatch, err = match.Match(c3)
|
||||
s.require.Equal(isMatch, true)
|
||||
s.require.Nil(err)
|
||||
|
||||
c4 := art.Candidate{
|
||||
NamespaceID: 2,
|
||||
NamespaceID: 1,
|
||||
Namespace: "immutable",
|
||||
Repository: "hello",
|
||||
Tag: "world",
|
||||
Kind: art.Image,
|
||||
PushedTime: time.Now().Unix() - 3600,
|
||||
PulledTime: time.Now().Unix(),
|
||||
CreationTime: time.Now().Unix() - 7200,
|
||||
Labels: []string{"label1"},
|
||||
}
|
||||
isMatch, err = match.Match(c4)
|
||||
s.require.Equal(isMatch, false)
|
||||
@ -160,3 +139,15 @@ func (s *MatchTestSuite) TearDownSuite() {
|
||||
err = s.ctr.DeleteImmutableRule(s.ruleID2)
|
||||
require.NoError(s.T(), err, "delete immutable")
|
||||
}
|
||||
|
||||
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(MatchTestSuite))
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user