mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-26 04:05:40 +01:00
add immutable match in the repository/tag delete api
Signed-off-by: wang yan <wangyan@vmware.com>
This commit is contained in:
parent
71bb8815bf
commit
424f11e697
@ -1089,6 +1089,8 @@ paths:
|
|||||||
description: Forbidden.
|
description: Forbidden.
|
||||||
'404':
|
'404':
|
||||||
description: Repository not found.
|
description: Repository not found.
|
||||||
|
'412':
|
||||||
|
description: Precondition Failed.
|
||||||
put:
|
put:
|
||||||
summary: Update description of the repository.
|
summary: Update description of the repository.
|
||||||
description: |
|
description: |
|
||||||
|
@ -73,6 +73,7 @@ type TagDetail struct {
|
|||||||
Author string `json:"author"`
|
Author string `json:"author"`
|
||||||
Created time.Time `json:"created"`
|
Created time.Time `json:"created"`
|
||||||
Config *TagCfg `json:"config"`
|
Config *TagCfg `json:"config"`
|
||||||
|
Immutable bool `json:"immutable"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TagCfg ...
|
// TagCfg ...
|
||||||
|
@ -28,7 +28,7 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/jobservice/logger"
|
"github.com/goharbor/harbor/src/jobservice/logger"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/api/scan"
|
"github.com/goharbor/harbor/src/pkg/scan/api/scan"
|
||||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
"github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||||
|
|
||||||
"github.com/docker/distribution/manifest/schema1"
|
"github.com/docker/distribution/manifest/schema1"
|
||||||
"github.com/docker/distribution/manifest/schema2"
|
"github.com/docker/distribution/manifest/schema2"
|
||||||
@ -45,6 +45,8 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/core/config"
|
"github.com/goharbor/harbor/src/core/config"
|
||||||
notifierEvt "github.com/goharbor/harbor/src/core/notifier/event"
|
notifierEvt "github.com/goharbor/harbor/src/core/notifier/event"
|
||||||
coreutils "github.com/goharbor/harbor/src/core/utils"
|
coreutils "github.com/goharbor/harbor/src/core/utils"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/art"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/immutabletag/match/rule"
|
||||||
"github.com/goharbor/harbor/src/replication"
|
"github.com/goharbor/harbor/src/replication"
|
||||||
"github.com/goharbor/harbor/src/replication/event"
|
"github.com/goharbor/harbor/src/replication/event"
|
||||||
"github.com/goharbor/harbor/src/replication/model"
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
@ -283,11 +285,6 @@ func (ra *RepositoryAPI) Delete() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, t := range tags {
|
for _, t := range tags {
|
||||||
image := fmt.Sprintf("%s:%s", repoName, t)
|
|
||||||
if err = dao.DeleteLabelsOfResource(common.ResourceTypeImage, image); err != nil {
|
|
||||||
ra.SendInternalServerError(fmt.Errorf("failed to delete labels of image %s: %v", image, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = rc.DeleteTag(t); err != nil {
|
if err = rc.DeleteTag(t); err != nil {
|
||||||
if regErr, ok := err.(*commonhttp.Error); ok {
|
if regErr, ok := err.(*commonhttp.Error); ok {
|
||||||
if regErr.Code == http.StatusNotFound {
|
if regErr.Code == http.StatusNotFound {
|
||||||
@ -298,6 +295,11 @@ func (ra *RepositoryAPI) Delete() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Infof("delete tag: %s:%s", repoName, t)
|
log.Infof("delete tag: %s:%s", repoName, t)
|
||||||
|
image := fmt.Sprintf("%s:%s", repoName, t)
|
||||||
|
if err = dao.DeleteLabelsOfResource(common.ResourceTypeImage, image); err != nil {
|
||||||
|
ra.SendInternalServerError(fmt.Errorf("failed to delete labels of image %s: %v", image, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
go func(tag string) {
|
go func(tag string) {
|
||||||
e := &event.Event{
|
e := &event.Event{
|
||||||
@ -711,6 +713,9 @@ func assembleTag(c chan *models.TagResp, client *registry.Repository, projectID
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get immutable status
|
||||||
|
item.Immutable = isImmutable(projectID, repository, tag)
|
||||||
|
|
||||||
c <- item
|
c <- item
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -791,6 +796,21 @@ func populateAuthor(detail *models.TagDetail) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check whether the tag is immutable
|
||||||
|
func isImmutable(projectID int64, repo string, tag string) bool {
|
||||||
|
_, repoName := utils.ParseRepository(repo)
|
||||||
|
matched, err := rule.NewRuleMatcher(projectID).Match(art.Candidate{
|
||||||
|
Repository: repoName,
|
||||||
|
Tag: tag,
|
||||||
|
NamespaceID: projectID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return matched
|
||||||
|
}
|
||||||
|
|
||||||
// GetManifests returns the manifest of a tag
|
// GetManifests returns the manifest of a tag
|
||||||
func (ra *RepositoryAPI) GetManifests() {
|
func (ra *RepositoryAPI) GetManifests() {
|
||||||
repoName := ra.GetString(":splat")
|
repoName := ra.GetString(":splat")
|
||||||
|
@ -35,4 +35,4 @@ var ChartMiddlewares = []string{CHART}
|
|||||||
var Middlewares = []string{READONLY, URL, MUITIPLEMANIFEST, LISTREPO, CONTENTTRUST, VULNERABLE, SIZEQUOTA, IMMUTABLE, COUNTQUOTA}
|
var Middlewares = []string{READONLY, URL, MUITIPLEMANIFEST, LISTREPO, CONTENTTRUST, VULNERABLE, SIZEQUOTA, IMMUTABLE, COUNTQUOTA}
|
||||||
|
|
||||||
// MiddlewaresLocal ...
|
// MiddlewaresLocal ...
|
||||||
var MiddlewaresLocal = []string{SIZEQUOTA, COUNTQUOTA}
|
var MiddlewaresLocal = []string{SIZEQUOTA, IMMUTABLE, COUNTQUOTA}
|
||||||
|
54
src/core/middlewares/immutable/builder.go
Normal file
54
src/core/middlewares/immutable/builder.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package immutable
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/goharbor/harbor/src/core/middlewares/interceptor"
|
||||||
|
"github.com/goharbor/harbor/src/core/middlewares/interceptor/immutable"
|
||||||
|
"github.com/goharbor/harbor/src/core/middlewares/util"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultBuilders = []interceptor.Builder{
|
||||||
|
&manifestDeletionBuilder{},
|
||||||
|
&manifestCreationBuilder{},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type manifestDeletionBuilder struct{}
|
||||||
|
|
||||||
|
func (*manifestDeletionBuilder) Build(req *http.Request) (interceptor.Interceptor, error) {
|
||||||
|
if match, _, _ := util.MatchDeleteManifest(req); !match {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
info, ok := util.ManifestInfoFromContext(req.Context())
|
||||||
|
if !ok {
|
||||||
|
var err error
|
||||||
|
info, err = util.ParseManifestInfoFromPath(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse manifest, error %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return immutable.NewDeleteMFInteceptor(info), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type manifestCreationBuilder struct{}
|
||||||
|
|
||||||
|
func (*manifestCreationBuilder) Build(req *http.Request) (interceptor.Interceptor, error) {
|
||||||
|
if match, _, _ := util.MatchPushManifest(req); !match {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
info, ok := util.ManifestInfoFromContext(req.Context())
|
||||||
|
if !ok {
|
||||||
|
var err error
|
||||||
|
info, err = util.ParseManifestInfoFromReq(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse manifest, error %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return immutable.NewPushMFInteceptor(info), nil
|
||||||
|
}
|
@ -16,78 +16,74 @@ package immutable
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"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/common/utils/log"
|
||||||
|
"github.com/goharbor/harbor/src/core/middlewares/interceptor"
|
||||||
"github.com/goharbor/harbor/src/core/middlewares/util"
|
"github.com/goharbor/harbor/src/core/middlewares/util"
|
||||||
"github.com/goharbor/harbor/src/pkg/art"
|
middlerware_err "github.com/goharbor/harbor/src/core/middlewares/util/error"
|
||||||
"github.com/goharbor/harbor/src/pkg/immutabletag/match/rule"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
type immutableHandler struct {
|
type immutableHandler struct {
|
||||||
next http.Handler
|
builders []interceptor.Builder
|
||||||
|
next http.Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
// New ...
|
// New ...
|
||||||
func New(next http.Handler) http.Handler {
|
func New(next http.Handler, builders ...interceptor.Builder) http.Handler {
|
||||||
|
if len(builders) == 0 {
|
||||||
|
builders = defaultBuilders
|
||||||
|
}
|
||||||
|
|
||||||
return &immutableHandler{
|
return &immutableHandler{
|
||||||
next: next,
|
builders: builders,
|
||||||
|
next: next,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP ...
|
// ServeHTTP ...
|
||||||
func (rh immutableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
func (rh *immutableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
if match, _, _ := util.MatchPushManifest(req); !match {
|
|
||||||
|
interceptor, err := rh.getInterceptor(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("Error occurred when to handle request in immutable handler: %v", err)
|
||||||
|
http.Error(rw, util.MarshalError("InternalError", fmt.Sprintf("Error occurred when to handle request in immutable handler: %v", err)),
|
||||||
|
http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if interceptor == nil {
|
||||||
rh.next.ServeHTTP(rw, req)
|
rh.next.ServeHTTP(rw, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
info, ok := util.ManifestInfoFromContext(req.Context())
|
|
||||||
if !ok {
|
if err := interceptor.HandleRequest(req); err != nil {
|
||||||
var err error
|
log.Warningf("Error occurred when to handle request in immutable handler: %v", err)
|
||||||
info, err = util.ParseManifestInfoFromPath(req)
|
if _, ok := err.(middlerware_err.ErrImmutable); ok {
|
||||||
if err != nil {
|
http.Error(rw, util.MarshalError("DENIED",
|
||||||
log.Error(err)
|
fmt.Sprintf("The tag is immutable, cannot be overwrite: %v", err)), http.StatusPreconditionFailed)
|
||||||
rh.next.ServeHTTP(rw, req)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
http.Error(rw, util.MarshalError("InternalError", fmt.Sprintf("Error occurred when to handle request in immutable handler: %v", err)),
|
||||||
|
http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rh.next.ServeHTTP(rw, req)
|
||||||
|
|
||||||
|
interceptor.HandleResponse(rw, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rh *immutableHandler) getInterceptor(req *http.Request) (interceptor.Interceptor, error) {
|
||||||
|
for _, builder := range rh.builders {
|
||||||
|
interceptor, err := builder.Build(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if interceptor != nil {
|
||||||
|
return interceptor, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, repoName := common_util.ParseRepository(info.Repository)
|
return nil, nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
65
src/core/middlewares/interceptor/immutable/deletemf.go
Normal file
65
src/core/middlewares/interceptor/immutable/deletemf.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package immutable
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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/interceptor"
|
||||||
|
"github.com/goharbor/harbor/src/core/middlewares/util"
|
||||||
|
middlerware_err "github.com/goharbor/harbor/src/core/middlewares/util/error"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/art"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/immutabletag/match/rule"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewDeleteMFInteceptor ....
|
||||||
|
func NewDeleteMFInteceptor(mf *util.ManifestInfo) interceptor.Interceptor {
|
||||||
|
return &delmfInterceptor{
|
||||||
|
mf: mf,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type delmfInterceptor struct {
|
||||||
|
mf *util.ManifestInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleRequest ...
|
||||||
|
func (dmf *delmfInterceptor) HandleRequest(req *http.Request) (err error) {
|
||||||
|
|
||||||
|
artifactQuery := &models.ArtifactQuery{
|
||||||
|
Digest: dmf.mf.Digest,
|
||||||
|
}
|
||||||
|
var afs []*models.Artifact
|
||||||
|
afs, err = dao.ListArtifacts(artifactQuery)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(afs) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, af := range afs {
|
||||||
|
_, repoName := common_util.ParseRepository(dmf.mf.Repository)
|
||||||
|
var matched bool
|
||||||
|
matched, err = rule.NewRuleMatcher(dmf.mf.ProjectID).Match(art.Candidate{
|
||||||
|
Repository: repoName,
|
||||||
|
Tag: af.Tag,
|
||||||
|
NamespaceID: dmf.mf.ProjectID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if matched {
|
||||||
|
return middlerware_err.NewErrImmutable(repoName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleRequest ...
|
||||||
|
func (dmf *delmfInterceptor) HandleResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
}
|
65
src/core/middlewares/interceptor/immutable/pushmf.go
Normal file
65
src/core/middlewares/interceptor/immutable/pushmf.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package immutable
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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/interceptor"
|
||||||
|
"github.com/goharbor/harbor/src/core/middlewares/util"
|
||||||
|
middlerware_err "github.com/goharbor/harbor/src/core/middlewares/util/error"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/art"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/immutabletag/match/rule"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewPushMFInteceptor ....
|
||||||
|
func NewPushMFInteceptor(mf *util.ManifestInfo) interceptor.Interceptor {
|
||||||
|
return &pushmfInterceptor{
|
||||||
|
mf: mf,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type pushmfInterceptor struct {
|
||||||
|
mf *util.ManifestInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleRequest ...
|
||||||
|
func (pmf *pushmfInterceptor) HandleRequest(req *http.Request) (err error) {
|
||||||
|
|
||||||
|
_, repoName := common_util.ParseRepository(pmf.mf.Repository)
|
||||||
|
var matched bool
|
||||||
|
matched, err = rule.NewRuleMatcher(pmf.mf.ProjectID).Match(art.Candidate{
|
||||||
|
Repository: repoName,
|
||||||
|
Tag: pmf.mf.Tag,
|
||||||
|
NamespaceID: pmf.mf.ProjectID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !matched {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
artifactQuery := &models.ArtifactQuery{
|
||||||
|
PID: pmf.mf.ProjectID,
|
||||||
|
Repo: pmf.mf.Repository,
|
||||||
|
Tag: pmf.mf.Tag,
|
||||||
|
}
|
||||||
|
var afs []*models.Artifact
|
||||||
|
afs, err = dao.ListArtifacts(artifactQuery)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(afs) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return middlerware_err.NewErrImmutable(repoName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleRequest ...
|
||||||
|
func (pmf *pushmfInterceptor) HandleResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
}
|
20
src/core/middlewares/util/error/immutable.go
Normal file
20
src/core/middlewares/util/error/immutable.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package error
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrImmutable ...
|
||||||
|
type ErrImmutable struct {
|
||||||
|
repo string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error ...
|
||||||
|
func (ei ErrImmutable) Error() string {
|
||||||
|
return fmt.Sprintf("Failed to process request, due to immutable. '%s'", ei.repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewErrImmutable ...
|
||||||
|
func NewErrImmutable(msg string) ErrImmutable {
|
||||||
|
return ErrImmutable{repo: msg}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user