Merge branch 'feature/tag_retention' into feature/tag_retention-performer

This commit is contained in:
Steven Zou 2019-07-19 16:00:13 +08:00
commit f0ea62caa9
15 changed files with 198 additions and 91 deletions

View File

@ -16,6 +16,9 @@ package models
import ( import (
"time" "time"
"github.com/goharbor/harbor/src/common/utils/notary/model"
"github.com/theupdateframework/notary/tuf/data"
) )
// RepoTable is the table name for repository // RepoTable is the table name for repository
@ -47,3 +50,36 @@ type RepositoryQuery struct {
Pagination Pagination
Sorting Sorting
} }
// TagResp holds the information of one image tag
type TagResp struct {
TagDetail
Signature *model.Target `json:"signature"`
ScanOverview *ImgScanOverview `json:"scan_overview,omitempty"`
Labels []*Label `json:"labels"`
}
// TagDetail ...
type TagDetail struct {
Digest string `json:"digest"`
Name string `json:"name"`
Size int64 `json:"size"`
Architecture string `json:"architecture"`
OS string `json:"os"`
OSVersion string `json:"os.version"`
DockerVersion string `json:"docker_version"`
Author string `json:"author"`
Created time.Time `json:"created"`
Config *TagCfg `json:"config"`
}
// TagCfg ...
type TagCfg struct {
Labels map[string]string `json:"labels"`
}
// Signature ...
type Signature struct {
Tag string `json:"tag"`
Hashes data.Hashes `json:"hashes"`
}

View File

@ -22,6 +22,8 @@ import (
"path" "path"
"strings" "strings"
"github.com/goharbor/harbor/src/common/utils/notary/model"
"github.com/docker/distribution/registry/auth/token" "github.com/docker/distribution/registry/auth/token"
"github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/common/utils/registry" "github.com/goharbor/harbor/src/common/utils/registry"
@ -41,14 +43,6 @@ var (
mockRetriever notary.PassRetriever mockRetriever notary.PassRetriever
) )
// Target represents the json object of a target of a docker image in notary.
// The struct will be used when repository is know so it won'g contain the name of a repository.
type Target struct {
Tag string `json:"tag"`
Hashes data.Hashes `json:"hashes"`
// TODO: update fields as needed.
}
func init() { func init() {
mockRetriever = func(keyName, alias string, createNew bool, attempts int) (passphrase string, giveup bool, err error) { mockRetriever = func(keyName, alias string, createNew bool, attempts int) (passphrase string, giveup bool, err error) {
passphrase = "hardcode" passphrase = "hardcode"
@ -60,7 +54,7 @@ func init() {
} }
// GetInternalTargets wraps GetTargets to read config values for getting full-qualified repo from internal notary instance. // GetInternalTargets wraps GetTargets to read config values for getting full-qualified repo from internal notary instance.
func GetInternalTargets(notaryEndpoint string, username string, repo string) ([]Target, error) { func GetInternalTargets(notaryEndpoint string, username string, repo string) ([]model.Target, error) {
ext, err := config.ExtEndpoint() ext, err := config.ExtEndpoint()
if err != nil { if err != nil {
log.Errorf("Error while reading external endpoint: %v", err) log.Errorf("Error while reading external endpoint: %v", err)
@ -74,8 +68,8 @@ func GetInternalTargets(notaryEndpoint string, username string, repo string) ([]
// GetTargets is a help function called by API to fetch signature information of a given repository. // GetTargets is a help function called by API to fetch signature information of a given repository.
// Per docker's convention the repository should contain the information of endpoint, i.e. it should look // Per docker's convention the repository should contain the information of endpoint, i.e. it should look
// like "192.168.0.1/library/ubuntu", instead of "library/ubuntu" (fqRepo for fully-qualified repo) // like "192.168.0.1/library/ubuntu", instead of "library/ubuntu" (fqRepo for fully-qualified repo)
func GetTargets(notaryEndpoint string, username string, fqRepo string) ([]Target, error) { func GetTargets(notaryEndpoint string, username string, fqRepo string) ([]model.Target, error) {
res := []Target{} res := []model.Target{}
t, err := tokenutil.MakeToken(username, tokenutil.Notary, t, err := tokenutil.MakeToken(username, tokenutil.Notary,
[]*token.ResourceActions{ []*token.ResourceActions{
{ {
@ -109,13 +103,16 @@ func GetTargets(notaryEndpoint string, username string, fqRepo string) ([]Target
log.Warningf("Failed to clear cached root.json: %s, error: %v, when repo is removed from notary the signature status maybe incorrect", rootJSON, rmErr) log.Warningf("Failed to clear cached root.json: %s, error: %v, when repo is removed from notary the signature status maybe incorrect", rootJSON, rmErr)
} }
for _, t := range targets { for _, t := range targets {
res = append(res, Target{t.Name, t.Hashes}) res = append(res, model.Target{
Tag: t.Name,
Hashes: t.Hashes,
})
} }
return res, nil return res, nil
} }
// DigestFromTarget get a target and return the value of digest, in accordance to Docker-Content-Digest // DigestFromTarget get a target and return the value of digest, in accordance to Docker-Content-Digest
func DigestFromTarget(t Target) (string, error) { func DigestFromTarget(t model.Target) (string, error) {
sha, ok := t.Hashes["sha256"] sha, ok := t.Hashes["sha256"]
if !ok { if !ok {
return "", fmt.Errorf("no valid hash, expecting sha256") return "", fmt.Errorf("no valid hash, expecting sha256")

View File

@ -17,6 +17,8 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/goharbor/harbor/src/common/utils/notary/model"
notarytest "github.com/goharbor/harbor/src/common/utils/notary/test" notarytest "github.com/goharbor/harbor/src/common/utils/notary/test"
"github.com/goharbor/harbor/src/common/utils/test" "github.com/goharbor/harbor/src/common/utils/test"
"github.com/goharbor/harbor/src/core/config" "github.com/goharbor/harbor/src/core/config"
@ -81,17 +83,19 @@ func TestGetDigestFromTarget(t *testing.T) {
} }
}` }`
var t1 Target var t1 model.Target
err := json.Unmarshal([]byte(str), &t1) err := json.Unmarshal([]byte(str), &t1)
if err != nil { if err != nil {
panic(err) panic(err)
} }
hash2 := make(map[string][]byte) hash2 := make(map[string][]byte)
t2 := Target{"2.0", hash2} t2 := model.Target{
Tag: "2.0",
Hashes: hash2,
}
d1, err1 := DigestFromTarget(t1) d1, err1 := DigestFromTarget(t1)
assert.Nil(t, err1, "Unexpected error: %v", err1) assert.Nil(t, err1, "Unexpected error: %v", err1)
assert.Equal(t, "sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a7", d1, "digest mismatch") assert.Equal(t, "sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a7", d1, "digest mismatch")
_, err2 := DigestFromTarget(t2) _, err2 := DigestFromTarget(t2)
assert.NotNil(t, err2, "") assert.NotNil(t, err2, "")
} }

View File

@ -0,0 +1,25 @@
// 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 model
import "github.com/theupdateframework/notary/tuf/data"
// Target represents the json object of a target of a docker image in notary.
// The struct will be used when repository is know so it won'g contain the name of a repository.
type Target struct {
Tag string `json:"tag"`
Hashes data.Hashes `json:"hashes"`
// TODO: update fields as needed.
}

View File

@ -555,7 +555,7 @@ func (a testapi) GetRepos(authInfo usrInfo, projectID, keyword string) (
return code, nil, nil return code, nil, nil
} }
func (a testapi) GetTag(authInfo usrInfo, repository string, tag string) (int, *TagResp, error) { func (a testapi) GetTag(authInfo usrInfo, repository string, tag string) (int, *models.TagResp, error) {
_sling := sling.New().Get(a.basePath).Path(fmt.Sprintf("/api/repositories/%s/tags/%s", repository, tag)) _sling := sling.New().Get(a.basePath).Path(fmt.Sprintf("/api/repositories/%s/tags/%s", repository, tag))
code, data, err := request(_sling, jsonAcceptHeader, authInfo) code, data, err := request(_sling, jsonAcceptHeader, authInfo)
if err != nil { if err != nil {
@ -567,7 +567,7 @@ func (a testapi) GetTag(authInfo usrInfo, repository string, tag string) (int, *
return code, nil, nil return code, nil, nil
} }
result := TagResp{} result := models.TagResp{}
if err := json.Unmarshal(data, &result); err != nil { if err := json.Unmarshal(data, &result); err != nil {
return 0, nil, err return 0, nil, err
} }
@ -591,7 +591,7 @@ func (a testapi) GetReposTags(authInfo usrInfo, repoName string) (int, interface
return httpStatusCode, body, nil return httpStatusCode, body, nil
} }
result := []TagResp{} result := []models.TagResp{}
if err := json.Unmarshal(body, &result); err != nil { if err := json.Unmarshal(body, &result); err != nil {
return 0, nil, err return 0, nil, err
} }

View File

@ -16,6 +16,7 @@ package api
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
@ -24,10 +25,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/goharbor/harbor/src/pkg/scan"
"errors"
"github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/manifest/schema2" "github.com/docker/distribution/manifest/schema2"
"github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common"
@ -38,9 +35,11 @@ import (
"github.com/goharbor/harbor/src/common/utils" "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/common/utils/notary" "github.com/goharbor/harbor/src/common/utils/notary"
notarymodel "github.com/goharbor/harbor/src/common/utils/notary/model"
"github.com/goharbor/harbor/src/common/utils/registry" "github.com/goharbor/harbor/src/common/utils/registry"
"github.com/goharbor/harbor/src/core/config" "github.com/goharbor/harbor/src/core/config"
coreutils "github.com/goharbor/harbor/src/core/utils" coreutils "github.com/goharbor/harbor/src/core/utils"
"github.com/goharbor/harbor/src/pkg/scan"
"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"
@ -80,31 +79,6 @@ func (r reposSorter) Less(i, j int) bool {
return r[i].Index < r[j].Index return r[i].Index < r[j].Index
} }
type tagDetail struct {
Digest string `json:"digest"`
Name string `json:"name"`
Size int64 `json:"size"`
Architecture string `json:"architecture"`
OS string `json:"os"`
OSVersion string `json:"os.version"`
DockerVersion string `json:"docker_version"`
Author string `json:"author"`
Created time.Time `json:"created"`
Config *cfg `json:"config"`
}
type cfg struct {
Labels map[string]string `json:"labels"`
}
// TagResp holds the information of one image tag
type TagResp struct {
tagDetail
Signature *notary.Target `json:"signature"`
ScanOverview *models.ImgScanOverview `json:"scan_overview,omitempty"`
Labels []*models.Label `json:"labels"`
}
type manifestResp struct { type manifestResp struct {
Manifest interface{} `json:"manifest"` Manifest interface{} `json:"manifest"`
Config interface{} `json:"config,omitempty" ` Config interface{} `json:"config,omitempty" `
@ -610,24 +584,24 @@ func (ra *RepositoryAPI) GetTags() {
// get config, signature and scan overview and assemble them into one // get config, signature and scan overview and assemble them into one
// struct for each tag in tags // struct for each tag in tags
func assembleTagsInParallel(client *registry.Repository, repository string, func assembleTagsInParallel(client *registry.Repository, repository string,
tags []string, username string) []*TagResp { tags []string, username string) []*models.TagResp {
var err error var err error
signatures := map[string][]notary.Target{} signatures := map[string][]notarymodel.Target{}
if config.WithNotary() { if config.WithNotary() {
signatures, err = getSignatures(username, repository) signatures, err = getSignatures(username, repository)
if err != nil { if err != nil {
signatures = map[string][]notary.Target{} signatures = map[string][]notarymodel.Target{}
log.Errorf("failed to get signatures of %s: %v", repository, err) log.Errorf("failed to get signatures of %s: %v", repository, err)
} }
} }
c := make(chan *TagResp) c := make(chan *models.TagResp)
for _, tag := range tags { for _, tag := range tags {
go assembleTag(c, client, repository, tag, config.WithClair(), go assembleTag(c, client, repository, tag, config.WithClair(),
config.WithNotary(), signatures) config.WithNotary(), signatures)
} }
result := []*TagResp{} result := []*models.TagResp{}
var item *TagResp var item *models.TagResp
for i := 0; i < len(tags); i++ { for i := 0; i < len(tags); i++ {
item = <-c item = <-c
if item == nil { if item == nil {
@ -638,10 +612,10 @@ func assembleTagsInParallel(client *registry.Repository, repository string,
return result return result
} }
func assembleTag(c chan *TagResp, client *registry.Repository, func assembleTag(c chan *models.TagResp, client *registry.Repository,
repository, tag string, clairEnabled, notaryEnabled bool, repository, tag string, clairEnabled, notaryEnabled bool,
signatures map[string][]notary.Target) { signatures map[string][]notarymodel.Target) {
item := &TagResp{} item := &models.TagResp{}
// labels // labels
image := fmt.Sprintf("%s:%s", repository, tag) image := fmt.Sprintf("%s:%s", repository, tag)
labels, err := dao.GetLabelsOfResource(common.ResourceTypeImage, image) labels, err := dao.GetLabelsOfResource(common.ResourceTypeImage, image)
@ -657,7 +631,7 @@ func assembleTag(c chan *TagResp, client *registry.Repository,
log.Errorf("failed to get v2 manifest of %s:%s: %v", repository, tag, err) log.Errorf("failed to get v2 manifest of %s:%s: %v", repository, tag, err)
} }
if tagDetail != nil { if tagDetail != nil {
item.tagDetail = *tagDetail item.TagDetail = *tagDetail
} }
// scan overview // scan overview
@ -680,8 +654,8 @@ func assembleTag(c chan *TagResp, client *registry.Repository,
// getTagDetail returns the detail information for v2 manifest image // getTagDetail returns the detail information for v2 manifest image
// The information contains architecture, os, author, size, etc. // The information contains architecture, os, author, size, etc.
func getTagDetail(client *registry.Repository, tag string) (*tagDetail, error) { func getTagDetail(client *registry.Repository, tag string) (*models.TagDetail, error) {
detail := &tagDetail{ detail := &models.TagDetail{
Name: tag, Name: tag,
} }
@ -738,7 +712,7 @@ func getTagDetail(client *registry.Repository, tag string) (*tagDetail, error) {
return detail, nil return detail, nil
} }
func populateAuthor(detail *tagDetail) { func populateAuthor(detail *models.TagDetail) {
// has author info already // has author info already
if len(detail.Author) > 0 { if len(detail.Author) > 0 {
return return
@ -1046,14 +1020,14 @@ func (ra *RepositoryAPI) VulnerabilityDetails() {
ra.ServeJSON() ra.ServeJSON()
} }
func getSignatures(username, repository string) (map[string][]notary.Target, error) { func getSignatures(username, repository string) (map[string][]notarymodel.Target, error) {
targets, err := notary.GetInternalTargets(config.InternalNotaryEndpoint(), targets, err := notary.GetInternalTargets(config.InternalNotaryEndpoint(),
username, repository) username, repository)
if err != nil { if err != nil {
return nil, err return nil, err
} }
signatures := map[string][]notary.Target{} signatures := map[string][]notarymodel.Target{}
for _, tgt := range targets { for _, tgt := range targets {
digest, err := notary.DigestFromTarget(tgt) digest, err := notary.DigestFromTarget(tgt)
if err != nil { if err != nil {

View File

@ -96,7 +96,7 @@ func TestGetReposTags(t *testing.T) {
t.Errorf("failed to get tags of repository %s: %v", repository, err) t.Errorf("failed to get tags of repository %s: %v", repository, err)
} else { } else {
assert.Equal(int(200), code, "httpStatusCode should be 200") assert.Equal(int(200), code, "httpStatusCode should be 200")
if tg, ok := tags.([]TagResp); ok { if tg, ok := tags.([]models.TagResp); ok {
assert.Equal(1, len(tg), fmt.Sprintf("there should be only one tag, but now %v", tg)) assert.Equal(1, len(tg), fmt.Sprintf("there should be only one tag, but now %v", tg))
assert.Equal(tg[0].Name, "latest", "the tag should be latest") assert.Equal(tg[0].Name, "latest", "the tag should be latest")
} else { } else {
@ -207,19 +207,19 @@ func TestGetReposTop(t *testing.T) {
func TestPopulateAuthor(t *testing.T) { func TestPopulateAuthor(t *testing.T) {
author := "author" author := "author"
detail := &tagDetail{ detail := &models.TagDetail{
Author: author, Author: author,
} }
populateAuthor(detail) populateAuthor(detail)
assert.Equal(t, author, detail.Author) assert.Equal(t, author, detail.Author)
detail = &tagDetail{} detail = &models.TagDetail{}
populateAuthor(detail) populateAuthor(detail)
assert.Equal(t, "", detail.Author) assert.Equal(t, "", detail.Author)
maintainer := "maintainer" maintainer := "maintainer"
detail = &tagDetail{ detail = &models.TagDetail{
Config: &cfg{ Config: &models.TagCfg{
Labels: map[string]string{ Labels: map[string]string{
"Maintainer": maintainer, "Maintainer": maintainer,
}, },

View File

@ -18,10 +18,11 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/chartserver" "github.com/goharbor/harbor/src/chartserver"
chttp "github.com/goharbor/harbor/src/common/http" chttp "github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/common/http/modifier" "github.com/goharbor/harbor/src/common/http/modifier"
"github.com/goharbor/harbor/src/core/api"
) )
// Client defines the methods that a core client should implement // Client defines the methods that a core client should implement
@ -34,7 +35,7 @@ type Client interface {
// ImageClient defines the methods that an image client should implement // ImageClient defines the methods that an image client should implement
type ImageClient interface { type ImageClient interface {
ListAllImages(project, repository string) ([]*api.TagResp, error) ListAllImages(project, repository string) ([]*models.TagResp, error)
DeleteImage(project, repository, tag string) error DeleteImage(project, repository, tag string) error
} }

View File

@ -17,12 +17,12 @@ package core
import ( import (
"fmt" "fmt"
"github.com/goharbor/harbor/src/core/api" "github.com/goharbor/harbor/src/common/models"
) )
func (c *client) ListAllImages(project, repository string) ([]*api.TagResp, error) { func (c *client) ListAllImages(project, repository string) ([]*models.TagResp, error) {
url := c.buildURL(fmt.Sprintf("/api/repositories/%s/%s/tags", project, repository)) url := c.buildURL(fmt.Sprintf("/api/repositories/%s/%s/tags", project, repository))
var images []*api.TagResp var images []*models.TagResp
if err := c.httpclient.GetAndIteratePagination(url, &images); err != nil { if err := c.httpclient.GetAndIteratePagination(url, &images); err != nil {
return nil, err return nil, err
} }

View File

@ -18,8 +18,8 @@ import (
"testing" "testing"
"github.com/goharbor/harbor/src/chartserver" "github.com/goharbor/harbor/src/chartserver"
"github.com/goharbor/harbor/src/common/job/models" jmodels "github.com/goharbor/harbor/src/common/job/models"
"github.com/goharbor/harbor/src/core/api" "github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/jobservice/job" "github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/pkg/retention/res" "github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/goharbor/harbor/src/testing/clients" "github.com/goharbor/harbor/src/testing/clients"
@ -34,10 +34,10 @@ type fakeCoreClient struct {
clients.DumbCoreClient clients.DumbCoreClient
} }
func (f *fakeCoreClient) ListAllImages(project, repository string) ([]*api.TagResp, error) { func (f *fakeCoreClient) ListAllImages(project, repository string) ([]*models.TagResp, error) {
image := &api.TagResp{} image := &models.TagResp{}
image.Name = "latest" image.Name = "latest"
return []*api.TagResp{image}, nil return []*models.TagResp{image}, nil
} }
func (f *fakeCoreClient) ListAllCharts(project, repository string) ([]*chartserver.ChartVersion, error) { func (f *fakeCoreClient) ListAllCharts(project, repository string) ([]*chartserver.ChartVersion, error) {
@ -53,7 +53,7 @@ func (f *fakeCoreClient) ListAllCharts(project, repository string) ([]*chartserv
type fakeJobserviceClient struct{} type fakeJobserviceClient struct{}
func (f *fakeJobserviceClient) SubmitJob(*models.JobData) (string, error) { func (f *fakeJobserviceClient) SubmitJob(*jmodels.JobData) (string, error) {
return "1", nil return "1", nil
} }
func (f *fakeJobserviceClient) GetJobLog(uuid string) ([]byte, error) { func (f *fakeJobserviceClient) GetJobLog(uuid string) ([]byte, error) {

View File

@ -216,15 +216,15 @@ func (l *launchTestSuite) TestLaunch() {
ScopeSelectors: map[string][]*rule.Selector{ ScopeSelectors: map[string][]*rule.Selector{
"project": { "project": {
{ {
Kind: "regularExpression", Kind: "doublestar",
Decoration: "matches", Decoration: "nsMatches",
Pattern: "**", Pattern: "**",
}, },
}, },
"repository": { "repository": {
{ {
Kind: "regularExpression", Kind: "doublestar",
Decoration: "matches", Decoration: "repoMatches",
Pattern: "**", Pattern: "**",
}, },
}, },

View File

@ -102,7 +102,7 @@ func (suite *ProcessorTestSuite) TearDownSuite() {}
func (suite *ProcessorTestSuite) TestProcess() { func (suite *ProcessorTestSuite) TestProcess() {
results, err := suite.p.Process(suite.all) results, err := suite.p.Process(suite.all)
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
assert.Equal(suite.T(), 2, len(results)) assert.Equal(suite.T(), 1, len(results))
assert.Condition(suite.T(), func() bool { assert.Condition(suite.T(), func() bool {
for _, r := range results { for _, r := range results {
if r.Error != nil { if r.Error != nil {

View File

@ -31,6 +31,10 @@ const (
RepoMatches = "repoMatches" RepoMatches = "repoMatches"
// RepoExcludes represents repository excludes [pattern] // RepoExcludes represents repository excludes [pattern]
RepoExcludes = "repoExcludes" RepoExcludes = "repoExcludes"
// NSMatches represents namespace matches [pattern]
NSMatches = "nsMatches"
// NSExcludes represents namespace excludes [pattern]
NSExcludes = "nsExcludes"
) )
// selector for regular expression // selector for regular expression
@ -59,6 +63,11 @@ func (s *selector) Select(artifacts []*res.Candidate) (selected []*res.Candidate
case RepoExcludes: case RepoExcludes:
value = art.Repository value = art.Repository
excludes = true excludes = true
case NSMatches:
value = art.Namespace
case NSExcludes:
value = art.Namespace
excludes = true
} }
if len(value) > 0 { if len(value) > 0 {
@ -95,5 +104,12 @@ func match(pattern, str string) (bool, error) {
func init() { func init() {
// Register doublestar selector // Register doublestar selector
selectors.Register(Kind, []string{Matches, Excludes}, New) selectors.Register(Kind, []string{
Matches,
Excludes,
RepoMatches,
RepoExcludes,
NSMatches,
NSExcludes,
}, New)
} }

View File

@ -51,8 +51,8 @@ func (suite *RegExpSelectorTestSuite) SetupSuite() {
Labels: []string{"label1", "label2", "label3"}, Labels: []string{"label1", "label2", "label3"},
}, },
{ {
NamespaceID: 1, NamespaceID: 2,
Namespace: "library", Namespace: "retention",
Repository: "redis", Repository: "redis",
Tag: "4.0", Tag: "4.0",
Kind: res.Image, Kind: res.Image,
@ -62,8 +62,8 @@ func (suite *RegExpSelectorTestSuite) SetupSuite() {
Labels: []string{"label1", "label4", "label5"}, Labels: []string{"label1", "label4", "label5"},
}, },
{ {
NamespaceID: 1, NamespaceID: 2,
Namespace: "library", Namespace: "retention",
Repository: "redis", Repository: "redis",
Tag: "4.1", Tag: "4.1",
Kind: res.Image, Kind: res.Image,
@ -180,6 +180,60 @@ func (suite *RegExpSelectorTestSuite) TestRepoExcludes() {
}) })
} }
// TestNSMatches tests the namespace `matches` case
func (suite *RegExpSelectorTestSuite) TestNSMatches() {
repoMatches := &selector{
decoration: NSMatches,
pattern: "{library}",
}
selected, err := repoMatches.Select(suite.artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 1, len(selected))
assert.Condition(suite.T(), func() bool {
return expect([]string{"harbor:latest"}, selected)
})
repoMatches2 := &selector{
decoration: RepoMatches,
pattern: "re*",
}
selected, err = repoMatches2.Select(suite.artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 2, len(selected))
assert.Condition(suite.T(), func() bool {
return expect([]string{"redis:4.0", "redis:4.1"}, selected)
})
}
// TestNSExcludes tests the namespace `excludes` case
func (suite *RegExpSelectorTestSuite) TestNSExcludes() {
repoExcludes := &selector{
decoration: NSExcludes,
pattern: "{library}",
}
selected, err := repoExcludes.Select(suite.artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 2, len(selected))
assert.Condition(suite.T(), func() bool {
return expect([]string{"redis:4.0", "redis:4.1"}, selected)
})
repoExcludes2 := &selector{
decoration: NSExcludes,
pattern: "re*",
}
selected, err = repoExcludes2.Select(suite.artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 1, len(selected))
assert.Condition(suite.T(), func() bool {
return expect([]string{"harbor:latest"}, selected)
})
}
// Check whether the returned result matched the expected ones (only check repo:tag) // Check whether the returned result matched the expected ones (only check repo:tag)
func expect(expected []string, candidates []*res.Candidate) bool { func expect(expected []string, candidates []*res.Candidate) bool {
hash := make(map[string]bool) hash := make(map[string]bool)

View File

@ -16,7 +16,7 @@ package clients
import ( import (
"github.com/goharbor/harbor/src/chartserver" "github.com/goharbor/harbor/src/chartserver"
"github.com/goharbor/harbor/src/core/api" "github.com/goharbor/harbor/src/common/models"
) )
// DumbCoreClient provides an empty implement for pkg/clients/core.Client // DumbCoreClient provides an empty implement for pkg/clients/core.Client
@ -24,7 +24,7 @@ import (
type DumbCoreClient struct{} type DumbCoreClient struct{}
// ListAllImages ... // ListAllImages ...
func (d *DumbCoreClient) ListAllImages(project, repository string) ([]*api.TagResp, error) { func (d *DumbCoreClient) ListAllImages(project, repository string) ([]*models.TagResp, error) {
return nil, nil return nil, nil
} }