mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-24 09:38:09 +01:00
Merge branch 'master' into scanner-im-v2
This commit is contained in:
commit
2bee9d7047
@ -11,17 +11,16 @@
|
|||||||
|
|
||||||
|![notification](docs/img/bell-outline-badged.svg)Community Meeting|
|
|![notification](docs/img/bell-outline-badged.svg)Community Meeting|
|
||||||
|------------------|
|
|------------------|
|
||||||
|The Harbor Project holds bi-weekly community calls, to join them and watch previous meeting notes and recordings, please see [meeting schedule](https://github.com/goharbor/community/blob/master/MEETING_SCHEDULE.md).|
|
|The Harbor Project holds bi-weekly community calls in two different timezones. To join the community calls or to watch previous meeting notes and recordings, please visit the [meeting schedule](https://github.com/goharbor/community/blob/master/MEETING_SCHEDULE.md).|
|
||||||
|
|
||||||
Welcome to join below Harbor community events and meet with project maintainers and users:
|
We welcome you to join the below Harbor community events and meet with project maintainers and users:
|
||||||
|
|
||||||
**May 20-24, 2019**, [KubeCon EU, Barcelona](https://events.linuxfoundation.org/events/kubecon-cloudnativecon-europe-2019/): Harbor Community Reception, Intro and Deep-dive sessions.
|
**November 18-21, 2019**, [KubeCon US, San Diego](https://events19.linuxfoundation.org/events/kubecon-cloudnativecon-north-america-2019): Harbor Lunch & Learn led by Joe Beda, Intro and Deep-dive sessions.
|
||||||
|
|
||||||
**June 24-26, 2019**, [KubeCon Shanghai](https://www.lfasiallc.com/events/kubecon-cloudnativecon-china-2019/): Harbor community meetup, Harbor session.
|
|
||||||
</br> </br>
|
</br> </br>
|
||||||
|
|
||||||
**Note**: The `master` branch may be in an *unstable or even broken state* during development.
|
**Note**: The `master` branch may be in an *unstable or even broken state* during development.
|
||||||
Please use [releases](https://github.com/vmware/harbor/releases) instead of the `master` branch in order to get stable binaries.
|
Please use [releases](https://github.com/vmware/harbor/releases) instead of the `master` branch in order to get a stable set of binaries.
|
||||||
|
|
||||||
<img alt="Harbor" src="docs/img/harbor_logo.png">
|
<img alt="Harbor" src="docs/img/harbor_logo.png">
|
||||||
|
|
||||||
|
35
ROADMAP.md
35
ROADMAP.md
@ -2,40 +2,11 @@
|
|||||||
|
|
||||||
### About this document
|
### About this document
|
||||||
|
|
||||||
This document provides description of items that are gathered from the community and planned in Harbor's roadmap. This should serve as a reference point for Harbor users and contributors to understand where the project is heading, and help determine if a contribution could be conflicting with a longer term plan.
|
This document provides a link to the [Harbor Project board](https://github.com/orgs/goharbor/projects/1) that serves as the up to date description of items that are in the Harbor release pipeline. The board has separate swim lanes for each release. Most items are gathered from the community or include a feedback loop with the community. This should serve as a reference point for Harbor users and contributors to understand where the project is heading, and help determine if a contribution could be conflicting with a longer term plan.
|
||||||
|
|
||||||
### How to help?
|
### How to help?
|
||||||
|
|
||||||
Discussion on the roadmap can take place in threads under [Issues](https://github.com/vmware/harbor/issues). Please open and comment on an issue if you want to provide suggestions and feedback to an item in the roadmap. Please review the roadmap to avoid potential duplicated effort.
|
Discussion on the roadmap can take place in threads under [Issues](https://github.com/goharbor/harbor/issues) or in [community meetings](https://github.com/goharbor/community/blob/master/MEETING_SCHEDULE.md). Please open and comment on an issue if you want to provide suggestions and feedback to an item in the roadmap. Please review the roadmap to avoid potential duplicated effort.
|
||||||
|
|
||||||
### How to add an item to the roadmap?
|
### How to add an item to the roadmap?
|
||||||
Please open an issue to track any initiative on the roadmap of Harbor. We will work with and rely on our community to focus our efforts to improve Harbor.
|
Please open an issue to track any initiative on the roadmap of Harbor (Usually driven by new feature requests). We will work with and rely on our community to focus our efforts to improve Harbor.
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 1. Notary
|
|
||||||
The notary feature allows publishers to sign their images offline and to push the signed content to a notary server. This ensures the authenticity of images.
|
|
||||||
|
|
||||||
### 2. Vulnerability Scanning
|
|
||||||
The capability to scan images for vulnerability.
|
|
||||||
|
|
||||||
### 3. Image replication enhancement
|
|
||||||
To provide more sophisticated rule for image replication.
|
|
||||||
- Image filtering by tags
|
|
||||||
- Replication can be scheduled at a certain time using a rule like: one time only, daily, weekly, etc.
|
|
||||||
- Image deletion can have the option not to be replicated to a remote instance.
|
|
||||||
- Global replication rule: Instead of setting the rule of individual project, system admin can set a global rule for all projects.
|
|
||||||
- Project admin can set replication policy of the project.
|
|
||||||
|
|
||||||
### 4. Authentication (OAuth2)
|
|
||||||
In addition to LDAP/AD and local users, OAuth 2.0 can be used to authenticate a user.
|
|
||||||
|
|
||||||
### 5. High Availability
|
|
||||||
Support multi-node deployment of Harbor for high availability, scalability and load-balancing purposes.
|
|
||||||
|
|
||||||
### 6. Statistics and description for repositories
|
|
||||||
User can add a description to a repository. The access count of a repo can be aggregated and displayed.
|
|
||||||
|
|
||||||
### 7. Migration tool to move from an existing registry to Harbor
|
|
||||||
A tool to migrate images from a vanilla registry server to Harbor, without the need to export/import a large amount of data.
|
|
||||||
|
@ -12,7 +12,7 @@ This guide describes the steps to install and configure Harbor by using the onli
|
|||||||
|
|
||||||
If you run a previous version of Harbor, you may need to update ```harbor.yml``` and migrate the data to fit the new database schema. For more details, please refer to **[Harbor Migration Guide](migration_guide.md)**.
|
If you run a previous version of Harbor, you may need to update ```harbor.yml``` and migrate the data to fit the new database schema. For more details, please refer to **[Harbor Migration Guide](migration_guide.md)**.
|
||||||
|
|
||||||
In addition, the deployment instructions on Kubernetes has been created by the community. Refer to [Harbor on Kubernetes](kubernetes_deployment.md) for details.
|
In addition, the deployment instructions on Kubernetes has been created by the community. Refer to [Harbor on Kubernetes using Helm](https://github.com/goharbor/harbor-helm) for details.
|
||||||
|
|
||||||
## Harbor Components
|
## Harbor Components
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
67
src/core/middlewares/interceptor/immutable/deletemf.go
Normal file
67
src/core/middlewares/interceptor/immutable/deletemf.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
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,
|
||||||
|
Repo: dmf.mf.Repository,
|
||||||
|
PID: dmf.mf.ProjectID,
|
||||||
|
}
|
||||||
|
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}
|
||||||
|
}
|
@ -253,7 +253,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
|
|||||||
dest_registry: rule.dest_registry,
|
dest_registry: rule.dest_registry,
|
||||||
trigger: rule.trigger,
|
trigger: rule.trigger,
|
||||||
deletion: rule.deletion,
|
deletion: rule.deletion,
|
||||||
enabled: true,
|
enabled: rule.enabled,
|
||||||
override: rule.override
|
override: rule.override
|
||||||
});
|
});
|
||||||
let filtersArray = this.getFilterArray(rule);
|
let filtersArray = this.getFilterArray(rule);
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
<div *ngIf="withReplicationJob" class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
<div *ngIf="withReplicationJob" class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||||
<div class="row flex-items-xs-between jobsRow">
|
<div class="row flex-items-xs-between jobsRow">
|
||||||
<h5 class="flex-items-xs-bottom option-left-down">{{'REPLICATION.REPLICATION_EXECUTIONS' | translate}}</h5>
|
<h5 class="flex-items-xs-bottom option-left-down">{{'REPLICATION.REPLICATION_EXECUTIONS' | translate}}</h5>
|
||||||
<div class="row flex-items-xs-between flex-items-xs-bottom">
|
<div class="row flex-items-xs-between flex-items-xs-bottom fiter-task">
|
||||||
<div class="execution-select">
|
<div class="execution-select">
|
||||||
<div class="select filter-tag" [hidden]="!isOpenFilterTag">
|
<div class="select filter-tag" [hidden]="!isOpenFilterTag">
|
||||||
<select (change)="doFilterJob($event)">
|
<select (change)="doFilterJob($event)">
|
||||||
|
@ -49,7 +49,10 @@
|
|||||||
.row-right {
|
.row-right {
|
||||||
margin-left: 564px;
|
margin-left: 564px;
|
||||||
}
|
}
|
||||||
|
.fiter-task {
|
||||||
|
margin-left: .4rem;
|
||||||
|
margin-top: .05rem;
|
||||||
|
}
|
||||||
.replication-row {
|
.replication-row {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
@ -168,6 +168,12 @@ describe('RepositoryComponent (inline template)', () => {
|
|||||||
return of({});
|
return of({});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const permissions = [
|
||||||
|
{resource: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE},
|
||||||
|
{resource: USERSTATICPERMISSION.REPOSITORY.KEY, action: USERSTATICPERMISSION.REPOSITORY.VALUE.PULL},
|
||||||
|
{resource: USERSTATICPERMISSION.REPOSITORY_TAG.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE},
|
||||||
|
{resource: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE},
|
||||||
|
];
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
@ -224,16 +230,10 @@ describe('RepositoryComponent (inline template)', () => {
|
|||||||
|
|
||||||
spyLabels = spyOn(labelService, 'getGLabels').and.returnValues(of(mockLabels).pipe(delay(0)));
|
spyLabels = spyOn(labelService, 'getGLabels').and.returnValues(of(mockLabels).pipe(delay(0)));
|
||||||
spyLabels1 = spyOn(labelService, 'getPLabels').and.returnValues(of(mockLabels1).pipe(delay(0)));
|
spyLabels1 = spyOn(labelService, 'getPLabels').and.returnValues(of(mockLabels1).pipe(delay(0)));
|
||||||
spyOn(userPermissionService, "getPermission")
|
spyOn(userPermissionService, "hasProjectPermissions")
|
||||||
.withArgs(compRepo.projectId, USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY, USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE )
|
.withArgs(compRepo.projectId, permissions )
|
||||||
.and.returnValue(of(mockHasAddLabelImagePermission))
|
.and.returnValue(of([mockHasAddLabelImagePermission, mockHasRetagImagePermission,
|
||||||
.withArgs(compRepo.projectId, USERSTATICPERMISSION.REPOSITORY.KEY, USERSTATICPERMISSION.REPOSITORY.VALUE.PULL )
|
mockHasDeleteImagePermission, mockHasScanImagePermission]));
|
||||||
.and.returnValue(of(mockHasRetagImagePermission))
|
|
||||||
.withArgs(compRepo.projectId, USERSTATICPERMISSION.REPOSITORY_TAG.KEY, USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE )
|
|
||||||
.and.returnValue(of(mockHasDeleteImagePermission))
|
|
||||||
.withArgs(compRepo.projectId, USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY
|
|
||||||
, USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE)
|
|
||||||
.and.returnValue(of(mockHasScanImagePermission));
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
let originalTimeout;
|
let originalTimeout;
|
||||||
|
@ -112,6 +112,15 @@ describe("TagComponent (inline template)", () => {
|
|||||||
let mockHasRetagImagePermission: boolean = true;
|
let mockHasRetagImagePermission: boolean = true;
|
||||||
let mockHasDeleteImagePermission: boolean = true;
|
let mockHasDeleteImagePermission: boolean = true;
|
||||||
let mockHasScanImagePermission: boolean = true;
|
let mockHasScanImagePermission: boolean = true;
|
||||||
|
const mockErrorHandler = {
|
||||||
|
error: () => {}
|
||||||
|
};
|
||||||
|
const permissions = [
|
||||||
|
{resource: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE},
|
||||||
|
{resource: USERSTATICPERMISSION.REPOSITORY.KEY, action: USERSTATICPERMISSION.REPOSITORY.VALUE.PULL},
|
||||||
|
{resource: USERSTATICPERMISSION.REPOSITORY_TAG.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE},
|
||||||
|
{resource: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE},
|
||||||
|
];
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
@ -141,6 +150,7 @@ describe("TagComponent (inline template)", () => {
|
|||||||
{ provide: ScanningResultService, useClass: ScanningResultDefaultService },
|
{ provide: ScanningResultService, useClass: ScanningResultDefaultService },
|
||||||
{ provide: LabelService, useClass: LabelDefaultService },
|
{ provide: LabelService, useClass: LabelDefaultService },
|
||||||
{ provide: UserPermissionService, useClass: UserPermissionDefaultService },
|
{ provide: UserPermissionService, useClass: UserPermissionDefaultService },
|
||||||
|
{ provide: mockErrorHandler, useValue: ErrorHandler },
|
||||||
{ provide: OperationService },
|
{ provide: OperationService },
|
||||||
]
|
]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
@ -169,15 +179,10 @@ describe("TagComponent (inline template)", () => {
|
|||||||
let http: HttpClient;
|
let http: HttpClient;
|
||||||
http = fixture.debugElement.injector.get(HttpClient);
|
http = fixture.debugElement.injector.get(HttpClient);
|
||||||
spyScanner = spyOn(http, "get").and.returnValue(of(scannerMock));
|
spyScanner = spyOn(http, "get").and.returnValue(of(scannerMock));
|
||||||
spyOn(userPermissionService, "getPermission")
|
spyOn(userPermissionService, "hasProjectPermissions")
|
||||||
.withArgs(comp.projectId, USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY, USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE )
|
.withArgs(comp.projectId, permissions )
|
||||||
.and.returnValue(of(mockHasAddLabelImagePermission))
|
.and.returnValue(of([mockHasAddLabelImagePermission, mockHasRetagImagePermission,
|
||||||
.withArgs(comp.projectId, USERSTATICPERMISSION.REPOSITORY.KEY, USERSTATICPERMISSION.REPOSITORY.VALUE.PULL )
|
mockHasDeleteImagePermission, mockHasScanImagePermission]));
|
||||||
.and.returnValue(of(mockHasRetagImagePermission))
|
|
||||||
.withArgs(comp.projectId, USERSTATICPERMISSION.REPOSITORY_TAG.KEY, USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE )
|
|
||||||
.and.returnValue(of(mockHasDeleteImagePermission))
|
|
||||||
.withArgs(comp.projectId, USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE)
|
|
||||||
.and.returnValue(of(mockHasScanImagePermission));
|
|
||||||
|
|
||||||
labelService = fixture.debugElement.injector.get(LabelService);
|
labelService = fixture.debugElement.injector.get(LabelService);
|
||||||
|
|
||||||
|
@ -224,9 +224,6 @@ export class TagComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
if (!this.withAdmiral) {
|
|
||||||
this.getAllLabels();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get filterLabelPieceWidth() {
|
public get filterLabelPieceWidth() {
|
||||||
@ -730,21 +727,24 @@ export class TagComponent implements OnInit, AfterViewInit {
|
|||||||
return st !== VULNERABILITY_SCAN_STATUS.RUNNING;
|
return st !== VULNERABILITY_SCAN_STATUS.RUNNING;
|
||||||
}
|
}
|
||||||
getImagePermissionRule(projectId: number): void {
|
getImagePermissionRule(projectId: number): void {
|
||||||
let hasAddLabelImagePermission = this.userPermissionService.getPermission(projectId, USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY,
|
const permissions = [
|
||||||
USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE);
|
{resource: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE},
|
||||||
let hasRetagImagePermission = this.userPermissionService.getPermission(projectId,
|
{resource: USERSTATICPERMISSION.REPOSITORY.KEY, action: USERSTATICPERMISSION.REPOSITORY.VALUE.PULL},
|
||||||
USERSTATICPERMISSION.REPOSITORY.KEY, USERSTATICPERMISSION.REPOSITORY.VALUE.PULL);
|
{resource: USERSTATICPERMISSION.REPOSITORY_TAG.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE},
|
||||||
let hasDeleteImagePermission = this.userPermissionService.getPermission(projectId,
|
{resource: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE},
|
||||||
USERSTATICPERMISSION.REPOSITORY_TAG.KEY, USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE);
|
];
|
||||||
let hasScanImagePermission = this.userPermissionService.getPermission(projectId,
|
this.userPermissionService.hasProjectPermissions(this.projectId, permissions).subscribe((results: Array<boolean>) => {
|
||||||
USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE);
|
this.hasAddLabelImagePermission = results[0];
|
||||||
forkJoin(hasAddLabelImagePermission, hasRetagImagePermission, hasDeleteImagePermission, hasScanImagePermission)
|
this.hasRetagImagePermission = results[1];
|
||||||
.subscribe(permissions => {
|
this.hasDeleteImagePermission = results[2];
|
||||||
this.hasAddLabelImagePermission = permissions[0] as boolean;
|
this.hasScanImagePermission = results[3];
|
||||||
this.hasRetagImagePermission = permissions[1] as boolean;
|
// only has label permission
|
||||||
this.hasDeleteImagePermission = permissions[2] as boolean;
|
if (this.hasAddLabelImagePermission) {
|
||||||
this.hasScanImagePermission = permissions[3] as boolean;
|
if (!this.withAdmiral) {
|
||||||
}, error => this.errorHandler.error(error));
|
this.getAllLabels();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, error => this.errorHandler.error(error));
|
||||||
}
|
}
|
||||||
// Trigger scan
|
// Trigger scan
|
||||||
scanNow(t: Tag[]): void {
|
scanNow(t: Tag[]): void {
|
||||||
|
@ -73,7 +73,7 @@ class TestProjects(unittest.TestCase):
|
|||||||
#5. Get project quota
|
#5. Get project quota
|
||||||
quota = self.system.get_project_quota("project", TestProjects.project_test_quota_id, **ADMIN_CLIENT)
|
quota = self.system.get_project_quota("project", TestProjects.project_test_quota_id, **ADMIN_CLIENT)
|
||||||
self.assertEqual(quota[0].used["count"], 1)
|
self.assertEqual(quota[0].used["count"], 1)
|
||||||
self.assertEqual(quota[0].used["storage"], 2791709)
|
self.assertEqual(quota[0].used["storage"], 2789174)
|
||||||
|
|
||||||
#6. Delete repository(RA) by user(UA);
|
#6. Delete repository(RA) by user(UA);
|
||||||
self.repo.delete_repoitory(TestProjects.repo_name, **ADMIN_CLIENT)
|
self.repo.delete_repoitory(TestProjects.repo_name, **ADMIN_CLIENT)
|
||||||
|
@ -54,6 +54,6 @@ Generate And Return Secret
|
|||||||
Retry Element Click ${more_btn}
|
Retry Element Click ${more_btn}
|
||||||
Retry Element Click ${generate_secret_btn}
|
Retry Element Click ${generate_secret_btn}
|
||||||
Retry Double Keywords When Error Retry Element Click ${confirm_btn} Retry Wait Until Page Not Contains Element ${confirm_btn}
|
Retry Double Keywords When Error Retry Element Click ${confirm_btn} Retry Wait Until Page Not Contains Element ${confirm_btn}
|
||||||
Retry Wait Until Page Contains generate CLI secret success
|
Retry Wait Until Page Contains Cli secret setting is successful
|
||||||
${secret}= Get Secrete By API ${url}
|
${secret}= Get Secrete By API ${url}
|
||||||
[Return] ${secret}
|
[Return] ${secret}
|
Loading…
Reference in New Issue
Block a user