merge with latest master code with quota feature branch

Signed-off-by: wang yan <wangyan@vmware.com>
This commit is contained in:
wang yan 2019-07-24 08:47:00 -07:00
commit 4763864dae
116 changed files with 10621 additions and 186 deletions

View File

@ -92,4 +92,52 @@ SELECT
creation_time,
update_time
FROM
quota;
quota;
create table retention_policy
(
id serial PRIMARY KEY NOT NULL,
scope_level varchar(20),
scope_reference integer,
trigger_kind varchar(20),
data text,
create_time time,
update_time time
);
create table retention_execution
(
id serial PRIMARY KEY NOT NULL,
policy_id integer,
status varchar(20),
dry_run boolean,
trigger varchar(20),
total integer,
succeed integer,
failed integer,
in_progress integer,
stopped integer,
start_time timestamp,
end_time timestamp
);
create table retention_task
(
id SERIAL NOT NULL,
execution_id integer,
status varchar(32),
start_time timestamp default CURRENT_TIMESTAMP,
end_time timestamp default CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
create table schedule
(
id SERIAL NOT NULL,
job_id varchar(64),
status varchar(64),
creation_time timestamp default CURRENT_TIMESTAMP,
update_time timestamp default CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);

View File

@ -11,9 +11,16 @@ import (
commonhttp "github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/common/http/modifier/auth"
"github.com/goharbor/harbor/src/common/job/models"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/jobservice/job"
)
var (
// GlobalClient is an instance of the default client that can be used globally
// Notes: the client needs to be initialized before can be used
GlobalClient Client
)
// Client wraps interface to access jobservice.
type Client interface {
SubmitJob(*models.JobData) (string, error)
@ -29,6 +36,11 @@ type DefaultClient struct {
client *commonhttp.Client
}
// Init the GlobalClient
func Init() {
GlobalClient = NewDefaultClient(config.InternalJobServiceURL(), config.CoreSecret())
}
// NewDefaultClient creates a default client based on endpoint and secret.
func NewDefaultClient(endpoint, secret string) *DefaultClient {
var c *commonhttp.Client

View File

@ -16,6 +16,9 @@ package models
import (
"time"
"github.com/goharbor/harbor/src/common/utils/notary/model"
"github.com/theupdateframework/notary/tuf/data"
)
// RepoTable is the table name for repository
@ -47,3 +50,36 @@ type RepositoryQuery struct {
Pagination
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

@ -27,6 +27,8 @@ const (
ActionUpdate = Action("update")
ActionDelete = Action("delete")
ActionList = Action("list")
ActionOperate = Action("operate")
)
// const resource variables
@ -46,6 +48,7 @@ const (
ResourceReplicationExecution = Resource("replication-execution")
ResourceReplicationTask = Resource("replication-task")
ResourceRepository = Resource("repository")
ResourceTagRetention = Resource("tag-retention")
ResourceRepositoryLabel = Resource("repository-label")
ResourceRepositoryTag = Resource("repository-tag")
ResourceRepositoryTagLabel = Resource("repository-tag-label")

View File

@ -88,6 +88,13 @@ var (
{Resource: rbac.ResourceReplicationTask, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceReplicationTask, Action: rbac.ActionDelete},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionCreate},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionRead},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionDelete},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionList},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionOperate},
{Resource: rbac.ResourceLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceLabel, Action: rbac.ActionRead},
{Resource: rbac.ResourceLabel, Action: rbac.ActionUpdate},

View File

@ -61,6 +61,13 @@ var (
{Resource: rbac.ResourceRepository, Action: rbac.ActionPull},
{Resource: rbac.ResourceRepository, Action: rbac.ActionPush},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionCreate},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionRead},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionDelete},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionList},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionOperate},
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionDelete},
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionList},
@ -133,6 +140,13 @@ var (
{Resource: rbac.ResourceRepository, Action: rbac.ActionPush},
{Resource: rbac.ResourceRepository, Action: rbac.ActionPull},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionCreate},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionRead},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionDelete},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionList},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionOperate},
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionDelete},
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionList},

View File

@ -22,6 +22,8 @@ import (
"path"
"strings"
"github.com/goharbor/harbor/src/common/utils/notary/model"
"github.com/docker/distribution/registry/auth/token"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/common/utils/registry"
@ -41,14 +43,6 @@ var (
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() {
mockRetriever = func(keyName, alias string, createNew bool, attempts int) (passphrase string, giveup bool, err error) {
passphrase = "hardcode"
@ -60,7 +54,7 @@ func init() {
}
// 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()
if err != nil {
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.
// 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)
func GetTargets(notaryEndpoint string, username string, fqRepo string) ([]Target, error) {
res := []Target{}
func GetTargets(notaryEndpoint string, username string, fqRepo string) ([]model.Target, error) {
res := []model.Target{}
t, err := tokenutil.MakeToken(username, tokenutil.Notary,
[]*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)
}
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
}
// 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"]
if !ok {
return "", fmt.Errorf("no valid hash, expecting sha256")

View File

@ -17,6 +17,8 @@ import (
"encoding/json"
"fmt"
"github.com/goharbor/harbor/src/common/utils/notary/model"
notarytest "github.com/goharbor/harbor/src/common/utils/notary/test"
"github.com/goharbor/harbor/src/common/utils/test"
"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)
if err != nil {
panic(err)
}
hash2 := make(map[string][]byte)
t2 := Target{"2.0", hash2}
t2 := model.Target{
Tag: "2.0",
Hashes: hash2,
}
d1, err1 := DigestFromTarget(t1)
assert.Nil(t, err1, "Unexpected error: %v", err1)
assert.Equal(t, "sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a7", d1, "digest mismatch")
_, err2 := DigestFromTarget(t2)
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

@ -16,6 +16,10 @@ package api
import (
"errors"
"github.com/goharbor/harbor/src/pkg/retention"
"github.com/goharbor/harbor/src/pkg/scheduler"
"net/http"
"github.com/ghodss/yaml"
@ -25,12 +29,24 @@ import (
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/core/filter"
"github.com/goharbor/harbor/src/core/promgr"
"github.com/goharbor/harbor/src/pkg/project"
"github.com/goharbor/harbor/src/pkg/repository"
)
const (
yamlFileContentType = "application/x-yaml"
)
// the managers/controllers used globally
var (
projectMgr project.Manager
repositoryMgr repository.Manager
retentionScheduler scheduler.Scheduler
retentionMgr retention.Manager
retentionLauncher retention.Launcher
retentionController retention.APIController
)
// BaseController ...
type BaseController struct {
api.BaseAPI
@ -41,13 +57,6 @@ type BaseController struct {
ProjectMgr promgr.ProjectManager
}
const (
// ReplicationJobType ...
ReplicationJobType = "replication"
// ScanJobType ...
ScanJobType = "scan"
)
// Prepare inits security context and project manager from request
// context
func (b *BaseController) Prepare() {
@ -85,12 +94,45 @@ func (b *BaseController) WriteYamlData(object interface{}) {
w := b.Ctx.ResponseWriter
w.Header().Set("Content-Type", yamlFileContentType)
w.WriteHeader(http.StatusOK)
w.Write(yData)
_, _ = w.Write(yData)
}
// Init related objects/configurations for the API controllers
func Init() error {
registerHealthCheckers()
// init chart controller
if err := initChartController(); err != nil {
return err
}
// init project manager
initProjectManager()
// init repository manager
initRepositoryManager()
initRetentionScheduler()
retentionMgr = retention.NewManager()
retentionLauncher = retention.NewLauncher(projectMgr, repositoryMgr, retentionMgr)
retentionController = retention.NewAPIController(projectMgr, repositoryMgr, retentionScheduler, retentionLauncher)
callbackFun := func(p interface{}) error {
r, ok := p.(retention.TriggerParam)
if ok {
return retentionController.TriggerRetentionExec(r.PolicyID, r.Trigger, false)
}
return errors.New("bad retention callback param")
}
err := scheduler.Register(retention.RetentionSchedulerCallback, callbackFun)
return err
}
func initChartController() error {
// If chart repository is not enabled then directly return
if !config.WithChartMuseum() {
return nil
@ -102,6 +144,17 @@ func Init() error {
}
chartController = chartCtl
return nil
}
func initProjectManager() {
projectMgr = project.New()
}
func initRepositoryManager() {
repositoryMgr = repository.New(projectMgr, chartController)
}
func initRetentionScheduler() {
retentionScheduler = scheduler.GlobalScheduler
}

View File

@ -46,6 +46,11 @@ const (
// chartController is a singleton instance
var chartController *chartserver.Controller
// GetChartController returns the chart controller
func GetChartController() *chartserver.Controller {
return chartController
}
// ChartRepositoryAPI provides related API handlers for the chart repository APIs
type ChartRepositoryAPI struct {
// The base controller to provide common utilities

View File

@ -556,7 +556,7 @@ func (a testapi) GetRepos(authInfo usrInfo, projectID, keyword string) (
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))
code, data, err := request(_sling, jsonAcceptHeader, authInfo)
if err != nil {
@ -568,7 +568,7 @@ func (a testapi) GetTag(authInfo usrInfo, repository string, tag string) (int, *
return code, nil, nil
}
result := tagResp{}
result := models.TagResp{}
if err := json.Unmarshal(data, &result); err != nil {
return 0, nil, err
}
@ -592,7 +592,7 @@ func (a testapi) GetReposTags(authInfo usrInfo, repoName string) (int, interface
return httpStatusCode, body, nil
}
result := []tagResp{}
result := []models.TagResp{}
if err := json.Unmarshal(body, &result); err != nil {
return 0, nil, err
}

View File

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

View File

@ -28,7 +28,7 @@ import (
var (
resourceLabelAPIBasePath = "/api/repositories"
repository = "library/hello-world"
repo = "library/hello-world"
tag = "latest"
proLibraryLabelID int64
)
@ -63,7 +63,7 @@ func TestAddToImage(t *testing.T) {
{
request: &testingRequest{
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath,
repository, tag),
repo, tag),
method: http.MethodPost,
},
code: http.StatusUnauthorized,
@ -72,13 +72,13 @@ func TestAddToImage(t *testing.T) {
{
request: &testingRequest{
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath,
repository, tag),
repo, tag),
method: http.MethodPost,
credential: projGuest,
},
code: http.StatusForbidden,
},
// 404 repository doesn't exist
// 404 repo doesn't exist
{
request: &testingRequest{
url: fmt.Sprintf("%s/library/non-exist-repo/tags/%s/labels", resourceLabelAPIBasePath, tag),
@ -90,7 +90,7 @@ func TestAddToImage(t *testing.T) {
// 404 image doesn't exist
{
request: &testingRequest{
url: fmt.Sprintf("%s/%s/tags/non-exist-tag/labels", resourceLabelAPIBasePath, repository),
url: fmt.Sprintf("%s/%s/tags/non-exist-tag/labels", resourceLabelAPIBasePath, repo),
method: http.MethodPost,
credential: projDeveloper,
},
@ -99,7 +99,7 @@ func TestAddToImage(t *testing.T) {
// 400
{
request: &testingRequest{
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath, repository, tag),
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath, repo, tag),
method: http.MethodPost,
credential: projDeveloper,
},
@ -109,7 +109,7 @@ func TestAddToImage(t *testing.T) {
{
request: &testingRequest{
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath,
repository, tag),
repo, tag),
method: http.MethodPost,
credential: projDeveloper,
bodyJSON: struct {
@ -124,7 +124,7 @@ func TestAddToImage(t *testing.T) {
{
request: &testingRequest{
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath,
repository, tag),
repo, tag),
method: http.MethodPost,
credential: projDeveloper,
bodyJSON: struct {
@ -139,7 +139,7 @@ func TestAddToImage(t *testing.T) {
{
request: &testingRequest{
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath,
repository, tag),
repo, tag),
method: http.MethodPost,
credential: projDeveloper,
bodyJSON: struct {
@ -154,7 +154,7 @@ func TestAddToImage(t *testing.T) {
{
request: &testingRequest{
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath,
repository, tag),
repo, tag),
method: http.MethodPost,
credential: projDeveloper,
bodyJSON: struct {
@ -172,7 +172,7 @@ func TestAddToImage(t *testing.T) {
func TestGetOfImage(t *testing.T) {
labels := []*models.Label{}
err := handleAndParse(&testingRequest{
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath, repository, tag),
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath, repo, tag),
method: http.MethodGet,
credential: projDeveloper,
}, &labels)
@ -185,7 +185,7 @@ func TestRemoveFromImage(t *testing.T) {
runCodeCheckingCases(t, &codeCheckingCase{
request: &testingRequest{
url: fmt.Sprintf("%s/%s/tags/%s/labels/%d", resourceLabelAPIBasePath,
repository, tag, proLibraryLabelID),
repo, tag, proLibraryLabelID),
method: http.MethodDelete,
credential: projDeveloper,
},
@ -195,7 +195,7 @@ func TestRemoveFromImage(t *testing.T) {
labels := []*models.Label{}
err := handleAndParse(&testingRequest{
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath,
repository, tag),
repo, tag),
method: http.MethodGet,
credential: projDeveloper,
}, &labels)
@ -206,7 +206,7 @@ func TestRemoveFromImage(t *testing.T) {
func TestAddToRepository(t *testing.T) {
runCodeCheckingCases(t, &codeCheckingCase{
request: &testingRequest{
url: fmt.Sprintf("%s/%s/labels", resourceLabelAPIBasePath, repository),
url: fmt.Sprintf("%s/%s/labels", resourceLabelAPIBasePath, repo),
method: http.MethodPost,
bodyJSON: struct {
ID int64
@ -222,7 +222,7 @@ func TestAddToRepository(t *testing.T) {
func TestGetOfRepository(t *testing.T) {
labels := []*models.Label{}
err := handleAndParse(&testingRequest{
url: fmt.Sprintf("%s/%s/labels", resourceLabelAPIBasePath, repository),
url: fmt.Sprintf("%s/%s/labels", resourceLabelAPIBasePath, repo),
method: http.MethodGet,
credential: projDeveloper,
}, &labels)
@ -235,7 +235,7 @@ func TestRemoveFromRepository(t *testing.T) {
runCodeCheckingCases(t, &codeCheckingCase{
request: &testingRequest{
url: fmt.Sprintf("%s/%s/labels/%d", resourceLabelAPIBasePath,
repository, proLibraryLabelID),
repo, proLibraryLabelID),
method: http.MethodDelete,
credential: projDeveloper,
},
@ -244,7 +244,7 @@ func TestRemoveFromRepository(t *testing.T) {
labels := []*models.Label{}
err := handleAndParse(&testingRequest{
url: fmt.Sprintf("%s/%s/labels", resourceLabelAPIBasePath, repository),
url: fmt.Sprintf("%s/%s/labels", resourceLabelAPIBasePath, repo),
method: http.MethodGet,
credential: projDeveloper,
}, &labels)

View File

@ -96,7 +96,7 @@ func TestGetReposTags(t *testing.T) {
t.Errorf("failed to get tags of repository %s: %v", repository, err)
} else {
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(tg[0].Name, "latest", "the tag should be latest")
} else {
@ -207,19 +207,19 @@ func TestGetReposTop(t *testing.T) {
func TestPopulateAuthor(t *testing.T) {
author := "author"
detail := &tagDetail{
detail := &models.TagDetail{
Author: author,
}
populateAuthor(detail)
assert.Equal(t, author, detail.Author)
detail = &tagDetail{}
detail = &models.TagDetail{}
populateAuthor(detail)
assert.Equal(t, "", detail.Author)
maintainer := "maintainer"
detail = &tagDetail{
Config: &cfg{
detail = &models.TagDetail{
Config: &models.TagCfg{
Labels: map[string]string{
"Maintainer": maintainer,
},

392
src/core/api/retention.go Normal file
View File

@ -0,0 +1,392 @@
package api
import (
"errors"
"fmt"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/core/filter"
"github.com/goharbor/harbor/src/core/promgr"
"github.com/goharbor/harbor/src/pkg/retention"
"github.com/goharbor/harbor/src/pkg/retention/policy"
"github.com/goharbor/harbor/src/pkg/retention/q"
"net/http"
"strconv"
)
// RetentionAPI ...
type RetentionAPI struct {
BaseController
pm promgr.ProjectManager
}
// Prepare validates the user
func (r *RetentionAPI) Prepare() {
r.BaseController.Prepare()
if !r.SecurityCtx.IsAuthenticated() {
r.SendUnAuthorizedError(errors.New("UnAuthorized"))
return
}
pm, e := filter.GetProjectManager(r.Ctx.Request)
if e != nil {
r.SendInternalServerError(e)
return
}
r.pm = pm
}
// GetMetadatas Get Metadatas
func (r *RetentionAPI) GetMetadatas() {
data := `
{
"templates": [
{
"rule_template": "lastXDays",
"display_text": "the images from the last # days",
"action": "retain",
"params": [
{
"type": "int",
"unit": "DAYS",
"required": true
}
]
},
{
"rule_template": "latestActiveK",
"display_text": "the most recent active # images",
"action": "retain",
"params": [
{
"type": "int",
"unit": "COUNT",
"required": true
}
]
},
{
"rule_template": "latestK",
"display_text": "the most recently pushed # images",
"action": "retain",
"params": [
{
"type": "int",
"unit": "COUNT",
"required": true
}
]
},
{
"rule_template": "latestPulledK",
"display_text": "the most recently pulled # images",
"action": "retain",
"params": [
{
"type": "int",
"unit": "COUNT",
"required": true
}
]
},
{
"rule_template": "always",
"display_text": "always",
"action": "retain",
"params": [
{
"type": "int",
"unit": "COUNT",
"required": true
}
]
}
],
"scope_selectors": [
{
"display_text": "Repositories",
"kind": "doublestar",
"decorations": [
"repoMatches",
"repoExcludes"
]
}
],
"tag_selectors": [
{
"display_text": "Labels",
"kind": "label",
"decorations": [
"withLabels",
"withoutLabels"
]
},
{
"display_text": "Tags",
"kind": "doublestar",
"decorations": [
"matches",
"excludes"
]
}
]
}
`
r.WriteJSONData(data)
}
// GetRetention Get Retention
func (r *RetentionAPI) GetRetention() {
id, err := r.GetIDFromURL()
if err != nil {
r.SendBadRequestError(err)
return
}
p, err := retentionController.GetRetention(id)
if err != nil {
r.SendBadRequestError(err)
return
}
if !r.requireAccess(p, rbac.ActionRead) {
return
}
r.WriteJSONData(p)
}
// CreateRetention Create Retention
func (r *RetentionAPI) CreateRetention() {
p := &policy.Metadata{}
isValid, err := r.DecodeJSONReqAndValidate(p)
if !isValid {
r.SendBadRequestError(err)
return
}
if !r.requireAccess(p, rbac.ActionCreate) {
return
}
switch p.Scope.Level {
case policy.ScopeLevelProject:
if p.Scope.Reference <= 0 {
r.SendBadRequestError(fmt.Errorf("Invalid Project id %d", p.Scope.Reference))
return
}
proj, err := r.pm.Get(p.Scope.Reference)
if err != nil {
r.SendBadRequestError(err)
}
if proj == nil {
r.SendBadRequestError(fmt.Errorf("Invalid Project id %d", p.Scope.Reference))
}
default:
r.SendBadRequestError(fmt.Errorf("scope %s is not support", p.Scope.Level))
return
}
if err = retentionController.CreateRetention(p); err != nil {
r.SendInternalServerError(err)
return
}
if err := r.pm.GetMetadataManager().Add(p.Scope.Reference,
map[string]string{"retention_id": strconv.FormatInt(p.Scope.Reference, 10)}); err != nil {
}
}
// UpdateRetention Update Retention
func (r *RetentionAPI) UpdateRetention() {
id, err := r.GetIDFromURL()
if err != nil {
r.SendBadRequestError(err)
return
}
p := &policy.Metadata{}
isValid, err := r.DecodeJSONReqAndValidate(p)
if !isValid {
r.SendBadRequestError(err)
return
}
p.ID = id
if !r.requireAccess(p, rbac.ActionUpdate) {
return
}
if err = retentionController.UpdateRetention(p); err != nil {
r.SendInternalServerError(err)
return
}
}
// TriggerRetentionExec Trigger Retention Execution
func (r *RetentionAPI) TriggerRetentionExec() {
id, err := r.GetIDFromURL()
if err != nil {
r.SendBadRequestError(err)
return
}
d := &struct {
DryRun bool `json:"dry_run"`
}{
DryRun: false,
}
isValid, err := r.DecodeJSONReqAndValidate(d)
if !isValid {
r.SendBadRequestError(err)
return
}
p, err := retentionController.GetRetention(id)
if err != nil {
r.SendBadRequestError(err)
return
}
if !r.requireAccess(p, rbac.ActionUpdate) {
return
}
if err = retentionController.TriggerRetentionExec(id, retention.ExecutionTriggerManual, d.DryRun); err != nil {
r.SendInternalServerError(err)
return
}
}
// OperateRetentionExec Operate Retention Execution
func (r *RetentionAPI) OperateRetentionExec() {
id, err := r.GetIDFromURL()
if err != nil {
r.SendBadRequestError(err)
return
}
eid, err := r.GetInt64FromPath(":eid")
if err != nil {
r.SendBadRequestError(err)
return
}
a := &struct {
Action string `json:"action" valid:"Required"`
}{}
isValid, err := r.DecodeJSONReqAndValidate(a)
if !isValid {
r.SendBadRequestError(err)
return
}
p, err := retentionController.GetRetention(id)
if err != nil {
r.SendBadRequestError(err)
return
}
if !r.requireAccess(p, rbac.ActionUpdate) {
return
}
if err = retentionController.OperateRetentionExec(eid, a.Action); err != nil {
r.SendInternalServerError(err)
return
}
}
// ListRetentionExecs List Retention Execution
func (r *RetentionAPI) ListRetentionExecs() {
id, err := r.GetIDFromURL()
if err != nil {
r.SendBadRequestError(err)
return
}
page, size, err := r.GetPaginationParams()
if err != nil {
r.SendInternalServerError(err)
return
}
query := &q.Query{
PageNumber: page,
PageSize: size,
}
p, err := retentionController.GetRetention(id)
if err != nil {
r.SendBadRequestError(err)
return
}
if !r.requireAccess(p, rbac.ActionList) {
return
}
execs, err := retentionController.ListRetentionExecs(id, query)
if err != nil {
r.SendInternalServerError(err)
return
}
r.WriteJSONData(execs)
}
// ListRetentionExecTasks List Retention Execution Tasks
func (r *RetentionAPI) ListRetentionExecTasks() {
id, err := r.GetIDFromURL()
if err != nil {
r.SendBadRequestError(err)
return
}
eid, err := r.GetInt64FromPath(":eid")
if err != nil {
r.SendBadRequestError(err)
return
}
page, size, err := r.GetPaginationParams()
if err != nil {
r.SendInternalServerError(err)
return
}
query := &q.Query{
PageNumber: page,
PageSize: size,
}
p, err := retentionController.GetRetention(id)
if err != nil {
r.SendBadRequestError(err)
return
}
if !r.requireAccess(p, rbac.ActionList) {
return
}
his, err := retentionController.ListRetentionExecTasks(eid, query)
if err != nil {
r.SendInternalServerError(err)
return
}
r.WriteJSONData(his)
}
// GetRetentionExecTaskLog Get Retention Execution Task log
func (r *RetentionAPI) GetRetentionExecTaskLog() {
tid, err := r.GetInt64FromPath(":tid")
if err != nil {
r.SendBadRequestError(err)
return
}
log, err := retentionController.GetRetentionExecTaskLog(tid)
if err != nil {
r.SendInternalServerError(err)
return
}
w := r.Ctx.ResponseWriter
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
w.Write(log)
}
func (r *RetentionAPI) requireAccess(p *policy.Metadata, action rbac.Action, subresources ...rbac.Resource) bool {
var hasPermission bool
switch p.Scope.Level {
case "project":
if len(subresources) == 0 {
subresources = append(subresources, rbac.ResourceTagRetention)
}
resource := rbac.NewProjectNamespace(p.Scope.Reference).Resource(subresources...)
hasPermission = r.SecurityCtx.Can(action, resource)
default:
hasPermission = r.SecurityCtx.IsSysAdmin()
}
if !hasPermission {
if !r.SecurityCtx.IsAuthenticated() {
r.SendUnAuthorizedError(errors.New("UnAuthorized"))
} else {
r.SendForbiddenError(errors.New(r.SecurityCtx.GetUsername()))
}
return false
}
return true
}

View File

@ -17,11 +17,14 @@ package main
import (
"encoding/gob"
"fmt"
"github.com/goharbor/harbor/src/pkg/retention"
"os"
"os/signal"
"strconv"
"syscall"
"github.com/goharbor/harbor/src/common/job"
"github.com/astaxie/beego"
_ "github.com/astaxie/beego/session/redis"
"github.com/goharbor/harbor/src/common/dao"
@ -37,6 +40,7 @@ import (
"github.com/goharbor/harbor/src/core/filter"
"github.com/goharbor/harbor/src/core/middlewares"
"github.com/goharbor/harbor/src/core/service/token"
"github.com/goharbor/harbor/src/pkg/scheduler"
"github.com/goharbor/harbor/src/replication"
)
@ -106,6 +110,11 @@ func main() {
log.Fatalf("failed to load config: %v", err)
}
// init the scheduler
scheduler.Init()
// init the jobservice client
job.Init()
password, err := config.InitialAdminPassword()
if err != nil {
log.Fatalf("failed to get admin's initia password: %v", err)
@ -157,6 +166,12 @@ func main() {
log.Infof("Because SYNC_REGISTRY set false , no need to sync registry \n")
}
// Initialize retention
log.Info("Initialize retention")
if err := retention.Init(); err != nil {
log.Fatalf("Failed to initialize retention with error: %s", err)
}
log.Info("Init proxy")
if err := middlewares.Init(); err != nil {
log.Errorf("init proxy error, %v", err)

View File

@ -23,6 +23,7 @@ import (
"github.com/goharbor/harbor/src/core/service/notifications/clair"
"github.com/goharbor/harbor/src/core/service/notifications/jobs"
"github.com/goharbor/harbor/src/core/service/notifications/registry"
"github.com/goharbor/harbor/src/core/service/notifications/scheduler"
"github.com/goharbor/harbor/src/core/service/token"
"github.com/astaxie/beego"
@ -131,6 +132,8 @@ func initRouters() {
beego.Router("/service/notifications/jobs/adminjob/:id([0-9]+)", &admin.Handler{}, "post:HandleAdminJob")
beego.Router("/service/notifications/jobs/replication/:id([0-9]+)", &jobs.Handler{}, "post:HandleReplicationScheduleJob")
beego.Router("/service/notifications/jobs/replication/task/:id([0-9]+)", &jobs.Handler{}, "post:HandleReplicationTask")
beego.Router("/service/notifications/jobs/retention/task/:id([0-9]+)", &jobs.Handler{}, "post:HandleRetentionTask")
beego.Router("/service/notifications/schedules/:id([0-9]+)", &scheduler.Handler{}, "post:Handle")
beego.Router("/service/token", &token.Handler{})
beego.Router("/api/registries", &api.RegistryAPI{}, "get:List;post:Post")
@ -140,6 +143,16 @@ func initRouters() {
beego.Router("/api/registries/:id/info", &api.RegistryAPI{}, "get:GetInfo")
beego.Router("/api/registries/:id/namespace", &api.RegistryAPI{}, "get:GetNamespace")
beego.Router("/api/retentions/metadatas", &api.RetentionAPI{}, "get:GetMetadatas")
beego.Router("/api/retentions/:id", &api.RetentionAPI{}, "get:GetRetention")
beego.Router("/api/retentions", &api.RetentionAPI{}, "post:CreateRetention")
beego.Router("/api/retentions/:id", &api.RetentionAPI{}, "put:UpdateRetention")
beego.Router("/api/retentions/:id/executions", &api.RetentionAPI{}, "post:TriggerRetentionExec")
beego.Router("/api/retentions/:id/executions/:eid", &api.RetentionAPI{}, "patch:OperateRetentionExec")
beego.Router("/api/retentions/:id/executions", &api.RetentionAPI{}, "get:ListRetentionExecs")
beego.Router("/api/retentions/:id/executions/:eid/tasks", &api.RetentionAPI{}, "get:ListRetentionExecTasks")
beego.Router("/api/retentions/:id/executions/:eid/tasks/:tid", &api.RetentionAPI{}, "get:GetRetentionExecTaskLog")
beego.Router("/v2/*", &controllers.RegistryProxy{}, "*:Handle")
// APIs for chart repository

View File

@ -16,6 +16,9 @@ package jobs
import (
"encoding/json"
"time"
"github.com/goharbor/harbor/src/pkg/retention"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/job"
@ -97,7 +100,27 @@ func (h *Handler) HandleReplicationScheduleJob() {
func (h *Handler) HandleReplicationTask() {
log.Debugf("received replication task status update event: task-%d, status-%s", h.id, h.status)
if err := hook.UpdateTask(replication.OperationCtl, h.id, h.rawStatus); err != nil {
log.Errorf("Failed to update replication task status, id: %d, status: %s", h.id, h.status)
log.Errorf("failed to update the status of the replication task %d: %v", h.id, err)
h.SendInternalServerError(err)
return
}
}
// HandleRetentionTask handles the webhook of retention task
func (h *Handler) HandleRetentionTask() {
log.Debugf("received retention task status update event: task-%d, status-%s", h.id, h.status)
mgr := &retention.DefaultManager{}
props := []string{"Status"}
task := &retention.Task{
ID: h.id,
Status: h.status,
}
if h.status == models.JobFinished {
task.EndTime = time.Now()
}
props = append(props, "EndTime")
if err := mgr.UpdateTask(task, props...); err != nil {
log.Errorf("failed to update the status of retention task %d: %v", h.id, err)
h.SendInternalServerError(err)
return
}

View File

@ -0,0 +1,70 @@
// 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 scheduler
import (
"encoding/json"
"fmt"
"github.com/goharbor/harbor/src/common/job/models"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/api"
"github.com/goharbor/harbor/src/pkg/scheduler/hook"
)
// Handler handles the scheduler requests
type Handler struct {
api.BaseController
}
// Handle ...
func (h *Handler) Handle() {
var data models.JobStatusChange
// status update
if len(data.CheckIn) == 0 {
schedulerID, err := h.GetInt64FromPath(":id")
if err != nil {
log.Errorf("failed to get the schedule ID: %v", err)
return
}
if err := hook.GlobalController.UpdateStatus(schedulerID, data.Status); err != nil {
h.SendInternalServerError(fmt.Errorf("failed to update status of job %s: %v", data.JobID, err))
return
}
return
}
// run callback function
// just log the error message when handling check in request if got any error
params := map[string]interface{}{}
if err := json.Unmarshal([]byte(data.CheckIn), &params); err != nil {
log.Errorf("failed to unmarshal parameters from check in message: %v", err)
return
}
callbackFuncNameParam, exist := params["callback_func_name"]
if !exist {
log.Error("cannot get the parameter \"callback_func_name\" from the check in message")
return
}
callbackFuncName, ok := callbackFuncNameParam.(string)
if !ok || len(callbackFuncName) == 0 {
log.Errorf("invalid \"callback_func_name\": %v", callbackFuncName)
return
}
if err := hook.GlobalController.Run(callbackFuncName, params["params"]); err != nil {
log.Errorf("failed to run the callback function %s: %v", callbackFuncName, err)
return
}
}

View File

@ -53,7 +53,9 @@ require (
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/lib/pq v1.1.0
github.com/mattn/go-runewidth v0.0.4 // indirect
github.com/miekg/pkcs11 v0.0.0-20170220202408-7283ca79f35e // indirect
github.com/olekukonko/tablewriter v0.0.1
github.com/opencontainers/go-digest v1.0.0-rc0
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/opentracing/opentracing-go v1.1.0 // indirect

View File

@ -190,6 +190,8 @@ github.com/lib/pq v1.1.0 h1:/5u4a+KGJptBRqGzPvYQL9p0d/tPR4S31+Tnzj9lEO4=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
@ -204,6 +206,8 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=

View File

@ -24,7 +24,7 @@ import (
"strings"
"github.com/goharbor/harbor/src/jobservice/common/utils"
"gopkg.in/yaml.v2"
yaml "gopkg.in/yaml.v2"
)
const (
@ -37,6 +37,7 @@ const (
jobServiceRedisURL = "JOB_SERVICE_POOL_REDIS_URL"
jobServiceRedisNamespace = "JOB_SERVICE_POOL_REDIS_NAMESPACE"
jobServiceAuthSecret = "JOBSERVICE_SECRET"
coreURL = "CORE_URL"
// JobServiceProtocolHTTPS points to the 'https' protocol
JobServiceProtocolHTTPS = "https"
@ -163,6 +164,11 @@ func GetAuthSecret() string {
return utils.ReadEnv(jobServiceAuthSecret)
}
// GetCoreURL get the core url from the env
func GetCoreURL() string {
return utils.ReadEnv(coreURL)
}
// GetUIAuthSecret get the auth secret of UI side
func GetUIAuthSecret() string {
return utils.ReadEnv(uiAuthSecret)

View File

@ -14,11 +14,12 @@
package config
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"os"
"testing"
)
// ConfigurationTestSuite tests the configuration loading
@ -84,6 +85,7 @@ func (suite *ConfigurationTestSuite) TestConfigLoadingWithEnv() {
)
assert.Equal(suite.T(), "js_secret", GetAuthSecret(), "expect auth secret 'js_secret' but got '%s'", GetAuthSecret())
assert.Equal(suite.T(), "core_secret", GetUIAuthSecret(), "expect auth secret 'core_secret' but got '%s'", GetUIAuthSecret())
assert.Equal(suite.T(), "core_url", GetCoreURL(), "expect core url 'core_url' but got '%s'", GetCoreURL())
}
// TestDefaultConfig ...
@ -134,6 +136,7 @@ func setENV() error {
err = os.Setenv("JOB_SERVICE_POOL_REDIS_NAMESPACE", "ut_namespace")
err = os.Setenv("JOBSERVICE_SECRET", "js_secret")
err = os.Setenv("CORE_SECRET", "core_secret")
err = os.Setenv("CORE_URL", "core_url")
return err
}

View File

@ -30,4 +30,6 @@ const (
Replication = "REPLICATION"
// ReplicationScheduler : the name of the replication scheduler job in job service
ReplicationScheduler = "IMAGE_REPLICATE"
// Retention : the name of the retention job
Retention = "RETENTION"
)

View File

@ -19,7 +19,6 @@ import (
"errors"
"flag"
"fmt"
"os"
"github.com/goharbor/harbor/src/common"
comcfg "github.com/goharbor/harbor/src/common/config"
@ -64,7 +63,7 @@ func main() {
if utils.IsEmptyStr(secret) {
return nil, errors.New("empty auth secret")
}
coreURL := os.Getenv("CORE_URL")
coreURL := config.GetCoreURL()
configURL := coreURL + common.CoreConfigPath
cfgMgr := comcfg.NewRESTCfgManager(configURL, secret)
jobCtx := impl.NewContext(ctx, cfgMgr)

View File

@ -17,8 +17,6 @@ package runtime
import (
"context"
"fmt"
"github.com/goharbor/harbor/src/jobservice/mgt"
"github.com/goharbor/harbor/src/jobservice/migration"
"os"
"os/signal"
"sync"
@ -38,8 +36,11 @@ import (
"github.com/goharbor/harbor/src/jobservice/job/impl/scan"
"github.com/goharbor/harbor/src/jobservice/lcm"
"github.com/goharbor/harbor/src/jobservice/logger"
"github.com/goharbor/harbor/src/jobservice/mgt"
"github.com/goharbor/harbor/src/jobservice/migration"
"github.com/goharbor/harbor/src/jobservice/worker"
"github.com/goharbor/harbor/src/jobservice/worker/cworker"
"github.com/goharbor/harbor/src/pkg/retention"
"github.com/gomodule/redigo/redis"
"github.com/pkg/errors"
)
@ -243,6 +244,7 @@ func (bs *Bootstrap) loadAndRunRedisWorkerPool(
job.ImageGC: (*gc.GarbageCollector)(nil),
job.Replication: (*replication.Replication)(nil),
job.ReplicationScheduler: (*replication.Scheduler)(nil),
job.Retention: (*retention.Job)(nil),
}); err != nil {
// exit
return nil, err

View File

@ -0,0 +1,35 @@
// 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 core
import (
"fmt"
"github.com/goharbor/harbor/src/chartserver"
)
func (c *client) ListAllCharts(project, repository string) ([]*chartserver.ChartVersion, error) {
url := c.buildURL(fmt.Sprintf("/api/chartrepo/%s/charts/%s", project, repository))
var charts []*chartserver.ChartVersion
if err := c.httpclient.Get(url, &charts); err != nil {
return nil, err
}
return charts, nil
}
func (c *client) DeleteChart(project, repository, version string) error {
url := c.buildURL(fmt.Sprintf("/api/chartrepo/%s/charts/%s/%s", project, repository, version))
return c.httpclient.Delete(url)
}

View File

@ -0,0 +1,63 @@
// 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 core
import (
"fmt"
"net/http"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/chartserver"
chttp "github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/common/http/modifier"
)
// Client defines the methods that a core client should implement
// Currently, it contains only part of the whole method collection
// and we should expand it when needed
type Client interface {
ImageClient
ChartClient
}
// ImageClient defines the methods that an image client should implement
type ImageClient interface {
ListAllImages(project, repository string) ([]*models.TagResp, error)
DeleteImage(project, repository, tag string) error
}
// ChartClient defines the methods that a chart client should implement
type ChartClient interface {
ListAllCharts(project, repository string) ([]*chartserver.ChartVersion, error)
DeleteChart(project, repository, version string) error
}
// New returns an instance of the client which is a default implement for Client
func New(url string, httpclient *http.Client, authorizer modifier.Modifier) Client {
return &client{
url: url,
httpclient: chttp.NewClient(httpclient, authorizer),
}
}
type client struct {
url string
httpclient *chttp.Client
}
func (c *client) buildURL(path string) string {
return fmt.Sprintf("%s/%s", c.url, path)
}

View File

@ -0,0 +1,35 @@
// 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 core
import (
"fmt"
"github.com/goharbor/harbor/src/common/models"
)
func (c *client) ListAllImages(project, repository string) ([]*models.TagResp, error) {
url := c.buildURL(fmt.Sprintf("/api/repositories/%s/%s/tags", project, repository))
var images []*models.TagResp
if err := c.httpclient.GetAndIteratePagination(url, &images); err != nil {
return nil, err
}
return images, nil
}
func (c *client) DeleteImage(project, repository, tag string) error {
url := c.buildURL(fmt.Sprintf("/api/repositories/%s/%s/tags/%s", project, repository, tag))
return c.httpclient.Delete(url)
}

View File

@ -0,0 +1,61 @@
// 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 project
import (
"fmt"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
)
// Manager is used for project management
// currently, the interface only defines the methods needed for tag retention
// will expand it when doing refactor
type Manager interface {
// List projects according to the query
List(...*models.ProjectQueryParam) ([]*models.Project, error)
// Get the project specified by the ID or name
Get(interface{}) (*models.Project, error)
}
// New returns a default implementation of Manager
func New() Manager {
return &manager{}
}
type manager struct{}
// List projects according to the query
func (m *manager) List(query ...*models.ProjectQueryParam) ([]*models.Project, error) {
var q *models.ProjectQueryParam
if len(query) > 0 {
q = query[0]
}
return dao.GetProjects(q)
}
// Get the project specified by the ID
func (m *manager) Get(idOrName interface{}) (*models.Project, error) {
id, ok := idOrName.(int64)
if ok {
return dao.GetProjectByID(id)
}
name, ok := idOrName.(string)
if ok {
return dao.GetProjectByName(name)
}
return nil, fmt.Errorf("invalid parameter: %v, should be ID(int64) or name(string)", idOrName)
}

View File

@ -0,0 +1,61 @@
// 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 repository
import (
"github.com/goharbor/harbor/src/chartserver"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/pkg/project"
)
// Manager is used for repository management
// currently, the interface only defines the methods needed for tag retention
// will expand it when doing refactor
type Manager interface {
// List image repositories under the project specified by the ID
ListImageRepositories(projectID int64) ([]*models.RepoRecord, error)
// List chart repositories under the project specified by the ID
ListChartRepositories(projectID int64) ([]*chartserver.ChartInfo, error)
}
// New returns a default implementation of Manager
func New(projectMgr project.Manager, chartCtl *chartserver.Controller) Manager {
return &manager{
projectMgr: projectMgr,
chartCtl: chartCtl,
}
}
type manager struct {
projectMgr project.Manager
chartCtl *chartserver.Controller
}
// List image repositories under the project specified by the ID
func (m *manager) ListImageRepositories(projectID int64) ([]*models.RepoRecord, error) {
return dao.GetRepositories(&models.RepositoryQuery{
ProjectIDs: []int64{projectID},
})
}
// List chart repositories under the project specified by the ID
func (m *manager) ListChartRepositories(projectID int64) ([]*chartserver.ChartInfo, error) {
project, err := m.projectMgr.Get(projectID)
if err != nil {
return nil, err
}
return m.chartCtl.ListCharts(project.Name)
}

23
src/pkg/retention/boot.go Normal file
View File

@ -0,0 +1,23 @@
// 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 retention
// TODO: Move to api.Init()
// Init the retention components
func Init() error {
return nil
}

View File

@ -0,0 +1,272 @@
// 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 retention
import (
"fmt"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/pkg/project"
"github.com/goharbor/harbor/src/pkg/repository"
"github.com/goharbor/harbor/src/pkg/retention/policy"
"github.com/goharbor/harbor/src/pkg/retention/q"
"github.com/goharbor/harbor/src/pkg/scheduler"
"time"
)
// APIController to handle the requests related with retention
type APIController interface {
// Handle the related hooks from the job service and launch the corresponding actions if needed
//
// Arguments:
// PolicyID string : uuid of the retention policy
// event *job.StatusChange : event object sent by job service
//
// Returns:
// common error object if any errors occurred
HandleHook(policyID string, event *job.StatusChange) error
GetRetention(id int64) (*policy.Metadata, error)
CreateRetention(p *policy.Metadata) error
UpdateRetention(p *policy.Metadata) error
DeleteRetention(id int64) error
TriggerRetentionExec(policyID int64, trigger string, dryRun bool) error
OperateRetentionExec(eid int64, action string) error
ListRetentionExecs(policyID int64, query *q.Query) ([]*Execution, error)
ListRetentionExecTasks(executionID int64, query *q.Query) ([]*Task, error)
GetRetentionExecTaskLog(taskID int64) ([]byte, error)
}
// DefaultAPIController ...
type DefaultAPIController struct {
manager Manager
launcher Launcher
projectManager project.Manager
repositoryMgr repository.Manager
scheduler scheduler.Scheduler
}
const (
// RetentionSchedulerCallback ...
RetentionSchedulerCallback = "RetentionSchedulerCallback"
)
// TriggerParam ...
type TriggerParam struct {
PolicyID int64
Trigger string
}
// GetRetention Get Retention
func (r *DefaultAPIController) GetRetention(id int64) (*policy.Metadata, error) {
return r.manager.GetPolicy(id)
}
// CreateRetention Create Retention
func (r *DefaultAPIController) CreateRetention(p *policy.Metadata) error {
if p.Trigger.Kind == policy.TriggerKindSchedule {
if p.Trigger.Settings != nil {
cron, ok := p.Trigger.Settings[policy.TriggerSettingsCron]
if ok {
jobid, err := r.scheduler.Schedule(cron.(string), RetentionSchedulerCallback, TriggerParam{
PolicyID: p.ID,
Trigger: ExecutionTriggerSchedule,
})
if err != nil {
return err
}
p.Trigger.References[policy.TriggerReferencesJobid] = jobid
}
}
}
if _, err := r.manager.CreatePolicy(p); err != nil {
return err
}
return nil
}
// UpdateRetention Update Retention
func (r *DefaultAPIController) UpdateRetention(p *policy.Metadata) error {
p0, err := r.manager.GetPolicy(p.ID)
if err != nil {
return err
}
needUn := false
needSch := false
if p0.Trigger.Kind != p.Trigger.Kind {
if p0.Trigger.Kind == policy.TriggerKindSchedule {
needUn = true
}
if p.Trigger.Kind == policy.TriggerKindSchedule {
needSch = true
}
} else {
switch p.Trigger.Kind {
case policy.TriggerKindSchedule:
if p0.Trigger.Settings["cron"] != p.Trigger.Settings["cron"] {
// unschedule old
if len(p0.Trigger.References[policy.TriggerReferencesJobid].(string)) > 0 {
needUn = true
}
// schedule new
if len(p.Trigger.Settings[policy.TriggerSettingsCron].(string)) > 0 {
// valid cron
needSch = true
}
}
case "":
default:
return fmt.Errorf("Not support Trigger %s", p.Trigger.Kind)
}
}
if needUn {
err = r.scheduler.UnSchedule(p0.Trigger.References[policy.TriggerReferencesJobid].(int64))
if err != nil {
return err
}
}
if needSch {
jobid, err := r.scheduler.Schedule(p.Trigger.Settings[policy.TriggerSettingsCron].(string), RetentionSchedulerCallback, TriggerParam{
PolicyID: p.ID,
Trigger: ExecutionTriggerSchedule,
})
if err != nil {
return err
}
p.Trigger.References[policy.TriggerReferencesJobid] = jobid
}
return r.manager.UpdatePolicy(p)
}
// DeleteRetention Delete Retention
func (r *DefaultAPIController) DeleteRetention(id int64) error {
p, err := r.manager.GetPolicy(id)
if err != nil {
return err
}
if p.Trigger.Kind == policy.TriggerKindSchedule && len(p.Trigger.Settings[policy.TriggerSettingsCron].(string)) > 0 {
err = r.scheduler.UnSchedule(p.Trigger.References[policy.TriggerReferencesJobid].(int64))
if err != nil {
return err
}
}
return r.manager.DeletePolicyAndExec(id)
}
// TriggerRetentionExec Trigger Retention Execution
func (r *DefaultAPIController) TriggerRetentionExec(policyID int64, trigger string, dryRun bool) error {
p, err := r.manager.GetPolicy(policyID)
if err != nil {
return err
}
exec := &Execution{
PolicyID: policyID,
StartTime: time.Now(),
Status: ExecutionStatusInProgress,
Trigger: trigger,
DryRun: dryRun,
}
id, err := r.manager.CreateExecution(exec)
// TODO launcher with DryRun param
num, err := r.launcher.Launch(p, id)
if err != nil {
return err
}
if num == 0 {
exec := &Execution{
ID: id,
EndTime: time.Now(),
Status: ExecutionStatusSucceed,
}
err = r.manager.UpdateExecution(exec)
if err != nil {
return err
}
}
return err
}
// OperateRetentionExec Operate Retention Execution
func (r *DefaultAPIController) OperateRetentionExec(eid int64, action string) error {
e, err := r.manager.GetExecution(eid)
if err != nil {
return err
}
exec := &Execution{}
switch action {
case "stop":
if e.Status != ExecutionStatusInProgress {
return fmt.Errorf("Can't abort, current status is %s", e.Status)
}
exec.ID = eid
exec.Status = ExecutionStatusStopped
exec.EndTime = time.Now()
// TODO stop the execution
default:
return fmt.Errorf("not support action %s", action)
}
return r.manager.UpdateExecution(exec)
}
// ListRetentionExecs List Retention Executions
func (r *DefaultAPIController) ListRetentionExecs(policyID int64, query *q.Query) ([]*Execution, error) {
return r.manager.ListExecutions(policyID, query)
}
// ListRetentionExecTasks List Retention Execution Histories
func (r *DefaultAPIController) ListRetentionExecTasks(executionID int64, query *q.Query) ([]*Task, error) {
q1 := &q.TaskQuery{
ExecutionID: executionID,
PageNumber: query.PageNumber,
PageSize: query.PageSize,
}
return r.manager.ListTasks(q1)
}
// GetRetentionExecTaskLog Get Retention Execution Task Log
func (r *DefaultAPIController) GetRetentionExecTaskLog(taskID int64) ([]byte, error) {
return r.manager.GetTaskLog(taskID)
}
// HandleHook HandleHook
func (r *DefaultAPIController) HandleHook(policyID string, event *job.StatusChange) error {
panic("implement me")
}
// NewAPIController ...
func NewAPIController(projectManager project.Manager, repositoryMgr repository.Manager, scheduler scheduler.Scheduler, retentionLauncher Launcher) APIController {
return &DefaultAPIController{
manager: NewManager(),
launcher: retentionLauncher,
projectManager: projectManager,
repositoryMgr: repositoryMgr,
scheduler: scheduler,
}
}

View File

@ -0,0 +1,65 @@
package models
import (
"time"
"github.com/astaxie/beego/orm"
)
func init() {
orm.RegisterModel(
new(RetentionPolicy),
new(RetentionExecution),
new(RetentionTask),
new(RetentionScheduleJob),
)
}
// RetentionPolicy Retention Policy
type RetentionPolicy struct {
ID int64 `orm:"pk;auto;column(id)" json:"id"`
// 'system', 'project' and 'repository'
ScopeLevel string
ScopeReference int64
TriggerKind string
// json format, include algorithm, rules, exclusions
Data string
CreateTime time.Time
UpdateTime time.Time
}
// RetentionExecution Retention Execution
type RetentionExecution struct {
ID int64 `orm:"pk;auto;column(id)" json:"id"`
PolicyID int64 `orm:"column(policy_id)"`
Status string
DryRun bool
// manual, scheduled
Trigger string
Total int
Succeed int
Failed int
InProgress int
Stopped int
StartTime time.Time
EndTime time.Time
}
// RetentionTask ...
type RetentionTask struct {
ID int64 `orm:"pk;auto;column(id)"`
ExecutionID int64 `orm:"column(execution_id)"`
Status string `orm:"column(status)"`
StartTime time.Time `orm:"column(start_time)"`
EndTime time.Time `orm:"column(end_time)"`
}
// RetentionScheduleJob Retention Schedule Job
type RetentionScheduleJob struct {
ID int64 `orm:"pk;auto;column(id)" json:"id"`
Status string
PolicyID int64 `orm:"column(policy_id)"`
JobID int64 `orm:"column(job_id)"`
CreateTime time.Time
UpdateTime time.Time
}

View File

@ -0,0 +1,181 @@
package dao
import (
"errors"
"fmt"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/pkg/retention/dao/models"
"github.com/goharbor/harbor/src/pkg/retention/q"
)
// CreatePolicy Create Policy
func CreatePolicy(p *models.RetentionPolicy) (int64, error) {
o := dao.GetOrmer()
return o.Insert(p)
}
// UpdatePolicy Update Policy
func UpdatePolicy(p *models.RetentionPolicy, cols ...string) error {
o := dao.GetOrmer()
_, err := o.Update(p, cols...)
return err
}
// DeletePolicyAndExec Delete Policy and Exec
func DeletePolicyAndExec(id int64) error {
o := dao.GetOrmer()
if _, err := o.Raw("delete from retention_task where execution_id in (select id from retention_execution where policy_id = ?) ", id).Exec(); err != nil {
return nil
}
if _, err := o.Raw("delete from retention_execution where policy_id = ?", id).Exec(); err != nil {
return err
}
if _, err := o.Delete(&models.RetentionExecution{
PolicyID: id,
}); err != nil {
return err
}
_, err := o.Delete(&models.RetentionPolicy{
ID: id,
})
return err
}
// GetPolicy Get Policy
func GetPolicy(id int64) (*models.RetentionPolicy, error) {
o := dao.GetOrmer()
p := &models.RetentionPolicy{
ID: id,
}
if err := o.Read(p); err != nil {
return nil, err
}
return p, nil
}
// CreateExecution Create Execution
func CreateExecution(e *models.RetentionExecution) (int64, error) {
o := dao.GetOrmer()
return o.Insert(e)
}
// UpdateExecution Update Execution
func UpdateExecution(e *models.RetentionExecution, cols ...string) error {
o := dao.GetOrmer()
_, err := o.Update(e, cols...)
return err
}
// DeleteExecution Delete Execution
func DeleteExecution(id int64) error {
o := dao.GetOrmer()
_, err := o.Delete(&models.RetentionExecution{
ID: id,
})
return err
}
// GetExecution Get Execution
func GetExecution(id int64) (*models.RetentionExecution, error) {
o := dao.GetOrmer()
e := &models.RetentionExecution{
ID: id,
}
if err := o.Read(e); err != nil {
return nil, err
}
return e, nil
}
// ListExecutions List Executions
func ListExecutions(policyID int64, query *q.Query) ([]*models.RetentionExecution, error) {
o := dao.GetOrmer()
qs := o.QueryTable(new(models.RetentionExecution))
qs = qs.Filter("policy_id", policyID)
if query != nil {
qs = qs.Limit(query.PageSize, (query.PageNumber-1)*query.PageSize)
}
var execs []*models.RetentionExecution
_, err := qs.All(&execs)
if err != nil {
return nil, err
}
return execs, nil
}
/*
// ListExecHistories List Execution Histories
func ListExecHistories(executionID int64, query *q.Query) ([]*models.RetentionTask, error) {
o := dao.GetOrmer()
qs := o.QueryTable(new(models.RetentionTask))
qs = qs.Filter("Execution_ID", executionID)
if query != nil {
qs = qs.Limit(query.PageSize, (query.PageNumber-1)*query.PageSize)
}
var tasks []*models.RetentionTask
_, err := qs.All(&tasks)
if err != nil {
return nil, err
}
return tasks, nil
}
// AppendExecHistory Append Execution History
func AppendExecHistory(t *models.RetentionTask) (int64, error) {
o := dao.GetOrmer()
return o.Insert(t)
}
*/
// CreateTask creates task record in database
func CreateTask(task *models.RetentionTask) (int64, error) {
if task == nil {
return 0, errors.New("nil task")
}
return dao.GetOrmer().Insert(task)
}
// UpdateTask updates the task record in database
func UpdateTask(task *models.RetentionTask, cols ...string) error {
if task == nil {
return errors.New("nil task")
}
if task.ID <= 0 {
return fmt.Errorf("invalid task ID: %d", task.ID)
}
_, err := dao.GetOrmer().Update(task, cols...)
return err
}
// DeleteTask deletes the task record specified by ID in database
func DeleteTask(id int64) error {
_, err := dao.GetOrmer().Delete(&models.RetentionTask{
ID: id,
})
return err
}
// ListTask lists the tasks according to the query
func ListTask(query ...*q.TaskQuery) ([]*models.RetentionTask, error) {
qs := dao.GetOrmer().QueryTable(&models.RetentionTask{})
if len(query) > 0 && query[0] != nil {
q := query[0]
if q.ExecutionID > 0 {
qs = qs.Filter("ExecutionID", q.ExecutionID)
}
if len(q.Status) > 0 {
qs = qs.Filter("Status", q.Status)
}
if q.PageSize > 0 {
qs = qs.Limit(q.PageSize)
if q.PageNumber > 0 {
qs = qs.Offset((q.PageNumber - 1) * q.PageSize)
}
}
}
tasks := []*models.RetentionTask{}
_, err := qs.All(&tasks)
return tasks, err
}

View File

@ -0,0 +1,210 @@
package dao
import (
"encoding/json"
"os"
"strings"
"testing"
"time"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/pkg/retention/dao/models"
"github.com/goharbor/harbor/src/pkg/retention/policy"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/q"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMain(m *testing.M) {
dao.PrepareTestForPostgresSQL()
os.Exit(m.Run())
}
func TestPolicy(t *testing.T) {
p := &policy.Metadata{
Algorithm: "OR",
Rules: []rule.Metadata{
{
ID: 1,
Priority: 1,
Template: "recentXdays",
Parameters: rule.Parameters{
"num": 10,
},
TagSelectors: []*rule.Selector{
{
Kind: "label",
Decoration: "with",
Pattern: "latest",
},
{
Kind: "regularExpression",
Decoration: "matches",
Pattern: "release-[\\d\\.]+",
},
},
ScopeSelectors: map[string][]*rule.Selector{
"repository": {
{
Kind: "regularExpression",
Decoration: "matches",
Pattern: ".+",
},
},
},
},
},
Trigger: &policy.Trigger{
Kind: "Schedule",
Settings: map[string]interface{}{
"cron": "* 22 11 * * *",
},
},
Scope: &policy.Scope{
Level: "project",
Reference: 1,
},
}
p1 := &models.RetentionPolicy{
ScopeLevel: p.Scope.Level,
TriggerKind: p.Trigger.Kind,
CreateTime: time.Now(),
UpdateTime: time.Now(),
}
data, _ := json.Marshal(p)
p1.Data = string(data)
id, err := CreatePolicy(p1)
assert.Nil(t, err)
assert.True(t, id > 0)
p1, err = GetPolicy(id)
assert.Nil(t, err)
assert.EqualValues(t, "project", p1.ScopeLevel)
assert.True(t, p1.ID > 0)
p1.ScopeLevel = "test"
err = UpdatePolicy(p1)
assert.Nil(t, err)
p1, err = GetPolicy(id)
assert.Nil(t, err)
assert.EqualValues(t, "test", p1.ScopeLevel)
err = DeletePolicyAndExec(id)
assert.Nil(t, err)
p1, err = GetPolicy(id)
assert.NotNil(t, err)
assert.True(t, strings.Contains(err.Error(), "no row found"))
}
func TestExecution(t *testing.T) {
p := &policy.Metadata{
Algorithm: "OR",
Rules: []rule.Metadata{
{
ID: 1,
Priority: 1,
Template: "recentXdays",
Parameters: rule.Parameters{
"num": 10,
},
TagSelectors: []*rule.Selector{
{
Kind: "label",
Decoration: "with",
Pattern: "latest",
},
{
Kind: "regularExpression",
Decoration: "matches",
Pattern: "release-[\\d\\.]+",
},
},
ScopeSelectors: map[string][]*rule.Selector{
"repository": {
{
Kind: "regularExpression",
Decoration: "matches",
Pattern: ".+",
},
},
},
},
},
Trigger: &policy.Trigger{
Kind: "Schedule",
Settings: map[string]interface{}{
"cron": "* 22 11 * * *",
},
},
Scope: &policy.Scope{
Level: "project",
Reference: 1,
},
}
p1 := &models.RetentionPolicy{
ScopeLevel: p.Scope.Level,
TriggerKind: p.Trigger.Kind,
CreateTime: time.Now(),
UpdateTime: time.Now(),
}
data, _ := json.Marshal(p)
p1.Data = string(data)
policyID, err := CreatePolicy(p1)
assert.Nil(t, err)
assert.True(t, policyID > 0)
e := &models.RetentionExecution{
PolicyID: policyID,
Status: "Running",
DryRun: false,
Trigger: "manual",
Total: 10,
StartTime: time.Now(),
}
id, err := CreateExecution(e)
assert.Nil(t, err)
assert.True(t, id > 0)
e1, err := GetExecution(id)
assert.Nil(t, err)
assert.NotNil(t, e1)
assert.EqualValues(t, id, e1.ID)
es, err := ListExecutions(policyID, nil)
assert.Nil(t, err)
assert.EqualValues(t, 1, len(es))
}
func TestTask(t *testing.T) {
task := &models.RetentionTask{
ExecutionID: 1,
Status: "pending",
}
// create
id, err := CreateTask(task)
require.Nil(t, err)
// update
task.ID = id
task.Status = "running"
err = UpdateTask(task, "Status")
require.Nil(t, err)
// list
tasks, err := ListTask(&q.TaskQuery{
ExecutionID: 1,
Status: "running",
})
require.Nil(t, err)
require.Equal(t, 1, len(tasks))
assert.Equal(t, int64(1), tasks[0].ExecutionID)
assert.Equal(t, "running", tasks[0].Status)
// delete
err = DeleteTask(id)
require.Nil(t, err)
}

View File

@ -0,0 +1,153 @@
// 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 dep
import (
"errors"
"fmt"
"net/http"
"github.com/goharbor/harbor/src/common/http/modifier/auth"
"github.com/goharbor/harbor/src/jobservice/config"
"github.com/goharbor/harbor/src/pkg/clients/core"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
// DefaultClient for the retention
var DefaultClient = NewClient()
// Client is designed to access core service to get required infos
type Client interface {
// Get the tag candidates under the repository
//
// Arguments:
// repo *res.Repository : repository info
//
// Returns:
// []*res.Candidate : candidates returned
// error : common error if any errors occurred
GetCandidates(repo *res.Repository) ([]*res.Candidate, error)
// Delete the specified candidate
//
// Arguments:
// candidate *res.Candidate : the deleting candidate
//
// Returns:
// error : common error if any errors occurred
Delete(candidate *res.Candidate) error
}
// NewClient new a basic client
func NewClient(client ...*http.Client) Client {
var c *http.Client
if len(client) > 0 {
c = client[0]
}
if c == nil {
c = http.DefaultClient
}
// init core client
internalCoreURL := config.GetCoreURL()
jobserviceSecret := config.GetAuthSecret()
authorizer := auth.NewSecretAuthorizer(jobserviceSecret)
coreClient := core.New(internalCoreURL, c, authorizer)
return &basicClient{
internalCoreURL: internalCoreURL,
coreClient: coreClient,
}
}
// basicClient is a default
type basicClient struct {
internalCoreURL string
coreClient core.Client
}
// GetCandidates gets the tag candidates under the repository
func (bc *basicClient) GetCandidates(repository *res.Repository) ([]*res.Candidate, error) {
if repository == nil {
return nil, errors.New("repository is nil")
}
candidates := make([]*res.Candidate, 0)
switch repository.Kind {
case res.Image:
images, err := bc.coreClient.ListAllImages(repository.Namespace, repository.Name)
if err != nil {
return nil, err
}
for _, image := range images {
labels := []string{}
for _, label := range image.Labels {
labels = append(labels, label.Name)
}
candidate := &res.Candidate{
Kind: res.Image,
Namespace: repository.Namespace,
Repository: repository.Name,
Tag: image.Name,
Labels: labels,
CreationTime: image.Created.Unix(),
// TODO: populate the pull/push time
// PulledTime: ,
// PushedTime:,
}
candidates = append(candidates, candidate)
}
case res.Chart:
charts, err := bc.coreClient.ListAllCharts(repository.Namespace, repository.Name)
if err != nil {
return nil, err
}
for _, chart := range charts {
labels := []string{}
for _, label := range chart.Labels {
labels = append(labels, label.Name)
}
candidate := &res.Candidate{
Kind: res.Chart,
Namespace: repository.Namespace,
Repository: repository.Name,
Tag: chart.Name,
Labels: labels,
CreationTime: chart.Created.Unix(),
// TODO: populate the pull/push time
// PulledTime: ,
// PushedTime:,
}
candidates = append(candidates, candidate)
}
default:
return nil, fmt.Errorf("unsupported repository kind: %s", repository.Kind)
}
return candidates, nil
}
// Deletes the specified candidate
func (bc *basicClient) Delete(candidate *res.Candidate) error {
if candidate == nil {
return errors.New("candidate is nil")
}
switch candidate.Kind {
case res.Image:
return bc.coreClient.DeleteImage(candidate.Namespace, candidate.Repository, candidate.Tag)
case res.Chart:
return bc.coreClient.DeleteChart(candidate.Namespace, candidate.Repository, candidate.Tag)
default:
return fmt.Errorf("unsupported candidate kind: %s", candidate.Kind)
}
}

View File

@ -0,0 +1,134 @@
// 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 dep
import (
"testing"
"github.com/goharbor/harbor/src/chartserver"
jmodels "github.com/goharbor/harbor/src/common/job/models"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/goharbor/harbor/src/testing/clients"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"k8s.io/helm/pkg/proto/hapi/chart"
"k8s.io/helm/pkg/repo"
)
type fakeCoreClient struct {
clients.DumbCoreClient
}
func (f *fakeCoreClient) ListAllImages(project, repository string) ([]*models.TagResp, error) {
image := &models.TagResp{}
image.Name = "latest"
return []*models.TagResp{image}, nil
}
func (f *fakeCoreClient) ListAllCharts(project, repository string) ([]*chartserver.ChartVersion, error) {
metadata := &chart.Metadata{
Name: "1.0",
}
chart := &chartserver.ChartVersion{}
chart.ChartVersion = repo.ChartVersion{
Metadata: metadata,
}
return []*chartserver.ChartVersion{chart}, nil
}
type fakeJobserviceClient struct{}
func (f *fakeJobserviceClient) SubmitJob(*jmodels.JobData) (string, error) {
return "1", nil
}
func (f *fakeJobserviceClient) GetJobLog(uuid string) ([]byte, error) {
return nil, nil
}
func (f *fakeJobserviceClient) PostAction(uuid, action string) error {
return nil
}
func (f *fakeJobserviceClient) GetExecutions(uuid string) ([]job.Stats, error) {
return nil, nil
}
type clientTestSuite struct {
suite.Suite
}
func (c *clientTestSuite) TestGetCandidates() {
client := &basicClient{}
client.coreClient = &fakeCoreClient{}
var repository *res.Repository
// nil repository
candidates, err := client.GetCandidates(repository)
require.NotNil(c.T(), err)
// image repository
repository = &res.Repository{}
repository.Kind = res.Image
repository.Namespace = "library"
repository.Name = "hello-world"
candidates, err = client.GetCandidates(repository)
require.Nil(c.T(), err)
assert.Equal(c.T(), 1, len(candidates))
assert.Equal(c.T(), res.Image, candidates[0].Kind)
assert.Equal(c.T(), "library", candidates[0].Namespace)
assert.Equal(c.T(), "hello-world", candidates[0].Repository)
assert.Equal(c.T(), "latest", candidates[0].Tag)
// chart repository
repository.Kind = res.Chart
repository.Namespace = "goharbor"
repository.Name = "harbor"
candidates, err = client.GetCandidates(repository)
require.Nil(c.T(), err)
assert.Equal(c.T(), 1, len(candidates))
assert.Equal(c.T(), res.Chart, candidates[0].Kind)
assert.Equal(c.T(), "goharbor", candidates[0].Namespace)
assert.Equal(c.T(), "1.0", candidates[0].Tag)
}
func (c *clientTestSuite) TestDelete() {
client := &basicClient{}
client.coreClient = &fakeCoreClient{}
var candidate *res.Candidate
// nil candidate
err := client.Delete(candidate)
require.NotNil(c.T(), err)
// image
candidate = &res.Candidate{}
candidate.Kind = res.Image
err = client.Delete(candidate)
require.Nil(c.T(), err)
// chart
candidate.Kind = res.Chart
err = client.Delete(candidate)
require.Nil(c.T(), err)
// unsupported type
candidate.Kind = "unsupported"
err = client.Delete(candidate)
require.NotNil(c.T(), err)
}
func TestClientTestSuite(t *testing.T) {
suite.Run(t, new(clientTestSuite))
}

233
src/pkg/retention/job.go Normal file
View File

@ -0,0 +1,233 @@
// 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 retention
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/jobservice/logger"
"github.com/goharbor/harbor/src/pkg/retention/dep"
"github.com/goharbor/harbor/src/pkg/retention/policy"
"github.com/goharbor/harbor/src/pkg/retention/policy/lwp"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/olekukonko/tablewriter"
"github.com/pkg/errors"
)
// Job of running retention process
type Job struct {
// client used to talk to core
// TODO: REFER THE GLOBAL CLIENT
client dep.Client
}
// MaxFails of the job
func (pj *Job) MaxFails() uint {
return 3
}
// ShouldRetry indicates job can be retried if failed
func (pj *Job) ShouldRetry() bool {
return true
}
// Validate the parameters
func (pj *Job) Validate(params job.Parameters) error {
if _, err := getParamRepo(params); err != nil {
return err
}
if _, err := getParamMeta(params); err != nil {
return err
}
return nil
}
// Run the job
func (pj *Job) Run(ctx job.Context, params job.Parameters) error {
// logger for logging
myLogger := ctx.GetLogger()
// Parameters have been validated, ignore error checking
repo, _ := getParamRepo(params)
liteMeta, _ := getParamMeta(params)
// Log stage: start
repoPath := fmt.Sprintf("%s/%s", repo.Namespace, repo.Name)
myLogger.Infof("Run retention process.\n Repository: %s \n Rule Algorithm: %s", repoPath, liteMeta.Algorithm)
// Stop check point 1:
if isStopped(ctx) {
logStop(myLogger)
return nil
}
// Retrieve all the candidates under the specified repository
allCandidates, err := pj.client.GetCandidates(repo)
if err != nil {
return logError(myLogger, err)
}
// Log stage: load candidates
myLogger.Infof("Load %d candidates from repository %s", len(allCandidates), repoPath)
// Build the processor
builder := policy.NewBuilder(allCandidates)
processor, err := builder.Build(liteMeta)
if err != nil {
return logError(myLogger, err)
}
// Stop check point 2:
if isStopped(ctx) {
logStop(myLogger)
return nil
}
// Run the flow
results, err := processor.Process(allCandidates)
if err != nil {
return logError(myLogger, err)
}
// Log stage: results with table view
logResults(myLogger, allCandidates, results)
// Check in the results
bytes, err := json.Marshal(results)
if err != nil {
return logError(myLogger, err)
}
if err := ctx.Checkin(string(bytes)); err != nil {
return logError(myLogger, err)
}
return nil
}
func logResults(logger logger.Interface, all []*res.Candidate, results []*res.Result) {
hash := make(map[string]error, len(results))
for _, r := range results {
if r.Target != nil {
hash[r.Target.Hash()] = r.Error
}
}
op := func(art *res.Candidate) string {
if e, exists := hash[art.Hash()]; exists {
if e != nil {
return "Err"
}
return "X"
}
return "√"
}
var buf bytes.Buffer
data := [][]string{}
for _, c := range all {
row := []string{
arn(c),
c.Kind,
strings.Join(c.Labels, ","),
t(c.PushedTime),
t(c.PulledTime),
t(c.CreationTime),
op(c),
}
data = append(data, row)
}
table := tablewriter.NewWriter(&buf)
table.SetHeader([]string{"Artifact", "Kind", "labels", "PushedTime", "PulledTime", "CreatedTime", "Retention"})
table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false})
table.SetCenterSeparator("|")
table.AppendBulk(data)
table.Render()
logger.Infof("%s", buf.String())
// log all the concrete errors if have
for _, r := range results {
if r.Error != nil {
logger.Infof("Retention error for artifact %s:%s : %s", r.Target.Kind, arn(r.Target), r.Error)
}
}
}
func arn(art *res.Candidate) string {
return fmt.Sprintf("%s/%s:%s", art.Namespace, art.Repository, art.Tag)
}
func t(tm int64) string {
return time.Unix(tm, 0).Format("2006/01/02 15:04:05")
}
func isStopped(ctx job.Context) (stopped bool) {
cmd, ok := ctx.OPCommand()
stopped = ok && cmd == job.StopCommand
return
}
func logStop(logger logger.Interface) {
logger.Info("Retention job is stopped")
}
func logError(logger logger.Interface, err error) error {
wrappedErr := errors.Wrap(err, "retention job")
logger.Error(wrappedErr)
return wrappedErr
}
func getParamRepo(params job.Parameters) (*res.Repository, error) {
v, ok := params[ParamRepo]
if !ok {
return nil, errors.Errorf("missing parameter: %s", ParamRepo)
}
repo, ok := v.(*res.Repository)
if !ok {
return nil, errors.Errorf("invalid parameter: %s", ParamRepo)
}
return repo, nil
}
func getParamMeta(params job.Parameters) (*lwp.Metadata, error) {
v, ok := params[ParamMeta]
if !ok {
return nil, errors.Errorf("missing parameter: %s", ParamMeta)
}
meta, ok := v.(*lwp.Metadata)
if !ok {
return nil, errors.Errorf("invalid parameter: %s", ParamMeta)
}
return meta, nil
}

View File

@ -0,0 +1,235 @@
// 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 retention
import (
"context"
"errors"
"fmt"
"testing"
"time"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/jobservice/logger"
"github.com/goharbor/harbor/src/pkg/retention/dep"
"github.com/goharbor/harbor/src/pkg/retention/policy"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/alg"
"github.com/goharbor/harbor/src/pkg/retention/policy/alg/or"
"github.com/goharbor/harbor/src/pkg/retention/policy/lwp"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule/latestk"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors/doublestar"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors/label"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
// JobTestSuite is test suite for testing job
type JobTestSuite struct {
suite.Suite
oldClient dep.Client
}
// TestJob is entry of running JobTestSuite
func TestJob(t *testing.T) {
suite.Run(t, new(JobTestSuite))
}
// SetupSuite ...
func (suite *JobTestSuite) SetupSuite() {
alg.Register(alg.AlgorithmOR, or.New)
selectors.Register(doublestar.Kind, []string{
doublestar.Matches,
doublestar.Excludes,
doublestar.RepoMatches,
doublestar.RepoExcludes,
doublestar.NSMatches,
doublestar.NSExcludes,
}, doublestar.New)
selectors.Register(label.Kind, []string{label.With, label.Without}, label.New)
action.Register(action.Retain, action.NewRetainAction)
suite.oldClient = dep.DefaultClient
dep.DefaultClient = &fakeRetentionClient{}
}
// TearDownSuite ...
func (suite *JobTestSuite) TearDownSuite() {
dep.DefaultClient = suite.oldClient
}
func (suite *JobTestSuite) TestRunSuccess() {
params := make(job.Parameters)
params[ParamRepo] = &res.Repository{
Namespace: "library",
Name: "harbor",
Kind: res.Image,
}
scopeSelectors := make(map[string][]*rule.Selector)
scopeSelectors["project"] = []*rule.Selector{{
Kind: doublestar.Kind,
Decoration: doublestar.RepoMatches,
Pattern: "{harbor}",
}}
ruleParams := make(rule.Parameters)
ruleParams[latestk.ParameterK] = 10
params[ParamMeta] = &lwp.Metadata{
Algorithm: policy.AlgorithmOR,
Rules: []*rule.Metadata{
{
ID: 1,
Priority: 999,
Action: action.Retain,
Template: latestk.TemplateID,
Parameters: ruleParams,
TagSelectors: []*rule.Selector{{
Kind: label.Kind,
Decoration: label.With,
Pattern: "L3",
}, {
Kind: doublestar.Kind,
Decoration: doublestar.Matches,
Pattern: "**",
}},
ScopeSelectors: scopeSelectors,
},
},
}
j := &Job{
client: &fakeRetentionClient{},
}
err := j.Validate(params)
require.NoError(suite.T(), err)
err = j.Run(&fakeJobContext{}, params)
require.NoError(suite.T(), err)
}
type fakeRetentionClient struct{}
// GetCandidates ...
func (frc *fakeRetentionClient) GetCandidates(repo *res.Repository) ([]*res.Candidate, error) {
return []*res.Candidate{
{
Namespace: "library",
Repository: "harbor",
Kind: "image",
Tag: "latest",
PushedTime: time.Now().Unix() - 11,
PulledTime: time.Now().Unix() - 2,
CreationTime: time.Now().Unix() - 10,
Labels: []string{"L1", "L2"},
},
{
Namespace: "library",
Repository: "harbor",
Kind: "image",
Tag: "dev",
PushedTime: time.Now().Unix() - 10,
PulledTime: time.Now().Unix() - 3,
CreationTime: time.Now().Unix() - 20,
Labels: []string{"L3"},
},
}, nil
}
// Delete ...
func (frc *fakeRetentionClient) Delete(candidate *res.Candidate) error {
return nil
}
// SubmitTask ...
func (frc *fakeRetentionClient) SubmitTask(taskID int64, repository *res.Repository, meta *lwp.Metadata) (string, error) {
return "", errors.New("not implemented")
}
type fakeLogger struct{}
// For debuging
func (l *fakeLogger) Debug(v ...interface{}) {}
// For debuging with format
func (l *fakeLogger) Debugf(format string, v ...interface{}) {}
// For logging info
func (l *fakeLogger) Info(v ...interface{}) {
fmt.Println(v...)
}
// For logging info with format
func (l *fakeLogger) Infof(format string, v ...interface{}) {
fmt.Printf(format+"\n", v...)
}
// For warning
func (l *fakeLogger) Warning(v ...interface{}) {}
// For warning with format
func (l *fakeLogger) Warningf(format string, v ...interface{}) {}
// For logging error
func (l *fakeLogger) Error(v ...interface{}) {
fmt.Println(v...)
}
// For logging error with format
func (l *fakeLogger) Errorf(format string, v ...interface{}) {
}
// For fatal error
func (l *fakeLogger) Fatal(v ...interface{}) {}
// For fatal error with error
func (l *fakeLogger) Fatalf(format string, v ...interface{}) {}
type fakeJobContext struct{}
func (c *fakeJobContext) Build(tracker job.Tracker) (job.Context, error) {
return nil, nil
}
func (c *fakeJobContext) Get(prop string) (interface{}, bool) {
return nil, false
}
func (c *fakeJobContext) SystemContext() context.Context {
return context.TODO()
}
func (c *fakeJobContext) Checkin(status string) error {
fmt.Printf("Check in: %s\n", status)
return nil
}
func (c *fakeJobContext) OPCommand() (job.OPCommand, bool) {
return "", false
}
func (c *fakeJobContext) GetLogger() logger.Interface {
return &fakeLogger{}
}
func (c *fakeJobContext) Tracker() job.Tracker {
return nil
}

View File

@ -0,0 +1,260 @@
// 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 retention
import (
"fmt"
cjob "github.com/goharbor/harbor/src/common/job"
"github.com/goharbor/harbor/src/common/job/models"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/pkg/project"
"github.com/goharbor/harbor/src/pkg/repository"
"github.com/goharbor/harbor/src/pkg/retention/policy"
"github.com/goharbor/harbor/src/pkg/retention/policy/lwp"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors"
"github.com/pkg/errors"
)
const (
// ParamRepo ...
ParamRepo = "repository"
// ParamMeta ...
ParamMeta = "liteMeta"
)
// Launcher provides function to launch the async jobs to run retentions based on the provided policy.
type Launcher interface {
// Launch async jobs for the retention policy
// A separate job will be launched for each repository
//
// Arguments:
// policy *policy.Metadata: the policy info
// executionID int64 : the execution ID
//
// Returns:
// int64 : the count of tasks
// error : common error if any errors occurred
Launch(policy *policy.Metadata, executionID int64) (int64, error)
}
// NewLauncher returns an instance of Launcher
func NewLauncher(projectMgr project.Manager, repositoryMgr repository.Manager,
retentionMgr Manager) Launcher {
return &launcher{
projectMgr: projectMgr,
repositoryMgr: repositoryMgr,
retentionMgr: retentionMgr,
jobserviceClient: cjob.GlobalClient,
internalCoreURL: config.InternalCoreURL(),
}
}
type launcher struct {
retentionMgr Manager
projectMgr project.Manager
repositoryMgr repository.Manager
jobserviceClient cjob.Client
internalCoreURL string
}
type jobData struct {
repository *res.Repository
policy *lwp.Metadata
taskID int64
}
func (l *launcher) Launch(ply *policy.Metadata, executionID int64) (int64, error) {
if ply == nil {
return 0, launcherError(fmt.Errorf("the policy is nil"))
}
// no rules, return directly
if len(ply.Rules) == 0 {
return 0, nil
}
scope := ply.Scope
if scope == nil {
return 0, launcherError(fmt.Errorf("the scope of policy is nil"))
}
repositoryRules := make(map[res.Repository]*lwp.Metadata, 0)
level := scope.Level
var projectCandidates []*res.Candidate
var err error
if level == "system" {
// get projects
projectCandidates, err = getProjects(l.projectMgr)
if err != nil {
return 0, launcherError(err)
}
}
for _, rule := range ply.Rules {
switch level {
case "system":
// filter projects according to the project selectors
for _, projectSelector := range rule.ScopeSelectors["project"] {
selector, err := selectors.Get(projectSelector.Kind, projectSelector.Decoration,
projectSelector.Pattern)
if err != nil {
return 0, launcherError(err)
}
projectCandidates, err = selector.Select(projectCandidates)
if err != nil {
return 0, launcherError(err)
}
}
case "project":
projectCandidates = append(projectCandidates, &res.Candidate{
NamespaceID: scope.Reference,
})
}
var repositoryCandidates []*res.Candidate
// get repositories of projects
for _, projectCandidate := range projectCandidates {
repositories, err := getRepositories(l.projectMgr, l.repositoryMgr, projectCandidate.NamespaceID)
if err != nil {
return 0, launcherError(err)
}
repositoryCandidates = append(repositoryCandidates, repositories...)
}
// filter repositories according to the repository selectors
for _, repositorySelector := range rule.ScopeSelectors["repository"] {
selector, err := selectors.Get(repositorySelector.Kind, repositorySelector.Decoration,
repositorySelector.Pattern)
if err != nil {
return 0, launcherError(err)
}
repositoryCandidates, err = selector.Select(repositoryCandidates)
if err != nil {
return 0, launcherError(err)
}
}
for _, repositoryCandidate := range repositoryCandidates {
reposit := res.Repository{
Namespace: repositoryCandidate.Namespace,
Name: repositoryCandidate.Repository,
Kind: repositoryCandidate.Kind,
}
if repositoryRules[reposit] == nil {
repositoryRules[reposit] = &lwp.Metadata{
Algorithm: ply.Algorithm,
}
}
repositoryRules[reposit].Rules = append(repositoryRules[reposit].Rules, &rule)
}
}
// no tasks need to be submitted
if len(repositoryRules) == 0 {
return 0, nil
}
// create task records
jobDatas := make([]*jobData, 0)
for repo, p := range repositoryRules {
taskID, err := l.retentionMgr.CreateTask(&Task{
ExecutionID: executionID,
})
if err != nil {
return 0, launcherError(err)
}
jobDatas = append(jobDatas, &jobData{
repository: &repo,
policy: p,
taskID: taskID,
})
}
allFailed := true
for _, jobData := range jobDatas {
j := &models.JobData{
Metadata: &models.JobMetadata{
JobKind: job.KindGeneric,
},
StatusHook: fmt.Sprintf("%s/service/notifications/jobs/retention/tasks/%d", l.internalCoreURL, jobData.taskID),
}
j.Name = job.Retention
j.Parameters = map[string]interface{}{
ParamRepo: jobData.repository,
ParamMeta: jobData.policy,
}
_, err := l.jobserviceClient.SubmitJob(j)
if err != nil {
log.Error(launcherError(fmt.Errorf("failed to submit task %d: %v", jobData.taskID, err)))
continue
}
allFailed = false
}
if allFailed {
return 0, launcherError(fmt.Errorf("all tasks failed"))
}
return int64(len(jobDatas)), nil
}
func launcherError(err error) error {
return errors.Wrap(err, "launcher")
}
func getProjects(projectMgr project.Manager) ([]*res.Candidate, error) {
projects, err := projectMgr.List()
if err != nil {
return nil, err
}
var candidates []*res.Candidate
for _, pro := range projects {
candidates = append(candidates, &res.Candidate{
NamespaceID: pro.ProjectID,
Namespace: pro.Name,
})
}
return candidates, nil
}
func getRepositories(projectMgr project.Manager, repositoryMgr repository.Manager, projectID int64) ([]*res.Candidate, error) {
var candidates []*res.Candidate
pro, err := projectMgr.Get(projectID)
if err != nil {
return nil, err
}
// get image repositories
imageRepositories, err := repositoryMgr.ListImageRepositories(projectID)
if err != nil {
return nil, err
}
for _, r := range imageRepositories {
namespace, repo := utils.ParseRepository(r.Name)
candidates = append(candidates, &res.Candidate{
Namespace: namespace,
Repository: repo,
Kind: "image",
})
}
// get chart repositories
chartRepositories, err := repositoryMgr.ListChartRepositories(projectID)
for _, r := range chartRepositories {
candidates = append(candidates, &res.Candidate{
Namespace: pro.Name,
Repository: r.Name,
Kind: "chart",
})
}
return candidates, nil
}

View File

@ -0,0 +1,236 @@
// 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 retention
import (
"fmt"
"testing"
"github.com/goharbor/harbor/src/chartserver"
"github.com/goharbor/harbor/src/common/job"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/pkg/project"
"github.com/goharbor/harbor/src/pkg/repository"
"github.com/goharbor/harbor/src/pkg/retention/policy"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/q"
_ "github.com/goharbor/harbor/src/pkg/retention/res/selectors/doublestar"
hjob "github.com/goharbor/harbor/src/testing/job"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type fakeProjectManager struct {
projects []*models.Project
}
func (f *fakeProjectManager) List(...*models.ProjectQueryParam) ([]*models.Project, error) {
return f.projects, nil
}
func (f *fakeProjectManager) Get(idOrName interface{}) (*models.Project, error) {
id, ok := idOrName.(int64)
if ok {
for _, pro := range f.projects {
if pro.ProjectID == id {
return pro, nil
}
}
return nil, nil
}
name, ok := idOrName.(string)
if ok {
for _, pro := range f.projects {
if pro.Name == name {
return pro, nil
}
}
return nil, nil
}
return nil, fmt.Errorf("invalid parameter: %v, should be ID(int64) or name(string)", idOrName)
}
type fakeRepositoryManager struct {
imageRepositories []*models.RepoRecord
chartRepositories []*chartserver.ChartInfo
}
func (f *fakeRepositoryManager) ListImageRepositories(projectID int64) ([]*models.RepoRecord, error) {
return f.imageRepositories, nil
}
func (f *fakeRepositoryManager) ListChartRepositories(projectID int64) ([]*chartserver.ChartInfo, error) {
return f.chartRepositories, nil
}
type fakeRetentionManager struct{}
func (f *fakeRetentionManager) CreatePolicy(p *policy.Metadata) (int64, error) {
return 0, nil
}
func (f *fakeRetentionManager) UpdatePolicy(p *policy.Metadata) error {
return nil
}
func (f *fakeRetentionManager) DeletePolicyAndExec(ID int64) error {
return nil
}
func (f *fakeRetentionManager) GetPolicy(ID int64) (*policy.Metadata, error) {
return nil, nil
}
func (f *fakeRetentionManager) CreateExecution(execution *Execution) (int64, error) {
return 0, nil
}
func (f *fakeRetentionManager) UpdateExecution(execution *Execution) error {
return nil
}
func (f *fakeRetentionManager) GetExecution(eid int64) (*Execution, error) {
return nil, nil
}
func (f *fakeRetentionManager) ListTasks(query ...*q.TaskQuery) ([]*Task, error) {
return nil, nil
}
func (f *fakeRetentionManager) CreateTask(task *Task) (int64, error) {
return 0, nil
}
func (f *fakeRetentionManager) UpdateTask(task *Task, cols ...string) error {
return nil
}
func (f *fakeRetentionManager) GetTaskLog(taskID int64) ([]byte, error) {
return nil, nil
}
func (f *fakeRetentionManager) ListExecutions(policyID int64, query *q.Query) ([]*Execution, error) {
return nil, nil
}
func (f *fakeRetentionManager) AppendHistory(history *History) (int64, error) {
return 0, nil
}
func (f *fakeRetentionManager) ListHistories(executionID int64, query *q.Query) ([]*History, error) {
return nil, nil
}
type launchTestSuite struct {
suite.Suite
projectMgr project.Manager
repositoryMgr repository.Manager
retentionMgr Manager
jobserviceClient job.Client
}
func (l *launchTestSuite) SetupTest() {
pro := &models.Project{
ProjectID: 1,
Name: "library",
}
l.projectMgr = &fakeProjectManager{
projects: []*models.Project{
pro,
}}
l.repositoryMgr = &fakeRepositoryManager{
imageRepositories: []*models.RepoRecord{
{
Name: "library/image",
},
},
chartRepositories: []*chartserver.ChartInfo{
{
Name: "chart",
},
},
}
l.retentionMgr = &fakeRetentionManager{}
l.jobserviceClient = &hjob.MockJobClient{}
}
func (l *launchTestSuite) TestGetProjects() {
projects, err := getProjects(l.projectMgr)
require.Nil(l.T(), err)
assert.Equal(l.T(), 1, len(projects))
assert.Equal(l.T(), int64(1), projects[0].NamespaceID)
assert.Equal(l.T(), "library", projects[0].Namespace)
}
func (l *launchTestSuite) TestGetRepositories() {
repositories, err := getRepositories(l.projectMgr, l.repositoryMgr, 1)
require.Nil(l.T(), err)
assert.Equal(l.T(), 2, len(repositories))
assert.Equal(l.T(), "library", repositories[0].Namespace)
assert.Equal(l.T(), "image", repositories[0].Repository)
assert.Equal(l.T(), "image", repositories[0].Kind)
assert.Equal(l.T(), "library", repositories[1].Namespace)
assert.Equal(l.T(), "chart", repositories[1].Repository)
assert.Equal(l.T(), "chart", repositories[1].Kind)
}
func (l *launchTestSuite) TestLaunch() {
launcher := &launcher{
projectMgr: l.projectMgr,
repositoryMgr: l.repositoryMgr,
retentionMgr: l.retentionMgr,
jobserviceClient: l.jobserviceClient,
}
var ply *policy.Metadata
// nil policy
n, err := launcher.Launch(ply, 1)
require.NotNil(l.T(), err)
// nil rules
ply = &policy.Metadata{}
n, err = launcher.Launch(ply, 1)
require.Nil(l.T(), err)
assert.Equal(l.T(), int64(0), n)
// nil scope
ply = &policy.Metadata{
Rules: []rule.Metadata{
{},
},
}
_, err = launcher.Launch(ply, 1)
require.NotNil(l.T(), err)
// system scope
ply = &policy.Metadata{
Scope: &policy.Scope{
Level: "system",
},
Rules: []rule.Metadata{
{
ScopeSelectors: map[string][]*rule.Selector{
"project": {
{
Kind: "doublestar",
Decoration: "nsMatches",
Pattern: "**",
},
},
"repository": {
{
Kind: "doublestar",
Decoration: "repoMatches",
Pattern: "**",
},
},
},
},
},
}
n, err = launcher.Launch(ply, 1)
require.Nil(l.T(), err)
assert.Equal(l.T(), int64(2), n)
}
func TestLaunchTestSuite(t *testing.T) {
suite.Run(t, new(launchTestSuite))
}

View File

@ -0,0 +1,230 @@
// 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 retention
import (
"encoding/json"
"errors"
"fmt"
"github.com/astaxie/beego/orm"
"time"
"github.com/goharbor/harbor/src/pkg/retention/dao"
"github.com/goharbor/harbor/src/pkg/retention/dao/models"
"github.com/goharbor/harbor/src/pkg/retention/policy"
"github.com/goharbor/harbor/src/pkg/retention/q"
)
// Manager defines operations of managing policy
type Manager interface {
// Create new policy and return ID
CreatePolicy(p *policy.Metadata) (int64, error)
// Update the existing policy
// Full update
UpdatePolicy(p *policy.Metadata) error
// Delete the specified policy
// No actual use so far
DeletePolicyAndExec(ID int64) error
// Get the specified policy
GetPolicy(ID int64) (*policy.Metadata, error)
// Create a new retention execution
CreateExecution(execution *Execution) (int64, error)
// Update the specified execution
UpdateExecution(execution *Execution) error
// Get the specified execution
GetExecution(eid int64) (*Execution, error)
// List execution histories
ListExecutions(policyID int64, query *q.Query) ([]*Execution, error)
// List tasks histories
ListTasks(query ...*q.TaskQuery) ([]*Task, error)
// Create a new retention task
CreateTask(task *Task) (int64, error)
// Update the specified task
UpdateTask(task *Task, cols ...string) error
// Get the log of the specified task
GetTaskLog(taskID int64) ([]byte, error)
}
// DefaultManager ...
type DefaultManager struct {
}
// CreatePolicy Create Policy
func (d *DefaultManager) CreatePolicy(p *policy.Metadata) (int64, error) {
p1 := &models.RetentionPolicy{}
p1.ScopeLevel = p.Scope.Level
p1.TriggerKind = p.Trigger.Kind
data, _ := json.Marshal(p)
p1.Data = string(data)
p1.CreateTime = time.Now()
p1.UpdateTime = p1.CreateTime
return dao.CreatePolicy(p1)
}
// UpdatePolicy Update Policy
func (d *DefaultManager) UpdatePolicy(p *policy.Metadata) error {
p1 := &models.RetentionPolicy{}
p1.ID = p.ID
p1.ScopeLevel = p.Scope.Level
p1.TriggerKind = p.Trigger.Kind
p.ID = 0
data, _ := json.Marshal(p)
p.ID = p1.ID
p1.Data = string(data)
p1.UpdateTime = time.Now()
return dao.UpdatePolicy(p1, "scope_level", "trigger_kind", "data", "update_time")
}
// DeletePolicyAndExec Delete Policy
func (d *DefaultManager) DeletePolicyAndExec(id int64) error {
return dao.DeletePolicyAndExec(id)
}
// GetPolicy Get Policy
func (d *DefaultManager) GetPolicy(id int64) (*policy.Metadata, error) {
p1, err := dao.GetPolicy(id)
if err != nil {
if err == orm.ErrNoRows {
return nil, nil
}
return nil, err
}
p := &policy.Metadata{}
if err = json.Unmarshal([]byte(p1.Data), p); err != nil {
return nil, err
}
p.ID = id
return p, nil
}
// CreateExecution Create Execution
func (d *DefaultManager) CreateExecution(execution *Execution) (int64, error) {
exec := &models.RetentionExecution{}
exec.PolicyID = execution.PolicyID
exec.StartTime = time.Now()
exec.DryRun = execution.DryRun
exec.Status = "Running"
exec.Trigger = "manual"
return dao.CreateExecution(exec)
}
// UpdateExecution Update Execution
func (d *DefaultManager) UpdateExecution(execution *Execution) error {
exec := &models.RetentionExecution{}
exec.ID = execution.ID
exec.EndTime = execution.EndTime
exec.Status = execution.Status
return dao.UpdateExecution(exec, "end_time", "status")
}
// ListExecutions List Executions
func (d *DefaultManager) ListExecutions(policyID int64, query *q.Query) ([]*Execution, error) {
execs, err := dao.ListExecutions(policyID, query)
if err != nil {
if err == orm.ErrNoRows {
return nil, nil
}
return nil, err
}
var execs1 []*Execution
for _, e := range execs {
e1 := &Execution{}
e1.ID = e.ID
e1.PolicyID = e.PolicyID
e1.Status = e.Status
e1.StartTime = e.StartTime
e1.EndTime = e.EndTime
execs1 = append(execs1, e1)
}
return execs1, nil
}
// GetExecution Get Execution
func (d *DefaultManager) GetExecution(eid int64) (*Execution, error) {
e, err := dao.GetExecution(eid)
if err != nil {
return nil, err
}
e1 := &Execution{}
e1.ID = e.ID
e1.PolicyID = e.PolicyID
e1.Status = e.Status
e1.StartTime = e.StartTime
e1.EndTime = e.EndTime
return e1, nil
}
// CreateTask creates task record
func (d *DefaultManager) CreateTask(task *Task) (int64, error) {
if task == nil {
return 0, errors.New("nil task")
}
t := &models.RetentionTask{
ExecutionID: task.ExecutionID,
Status: task.Status,
StartTime: task.StartTime,
EndTime: task.EndTime,
}
return dao.CreateTask(t)
}
// ListTasks lists tasks according to the query
func (d *DefaultManager) ListTasks(query ...*q.TaskQuery) ([]*Task, error) {
ts, err := dao.ListTask(query...)
if err != nil {
if err == orm.ErrNoRows {
return nil, nil
}
return nil, err
}
tasks := []*Task{}
for _, t := range ts {
tasks = append(tasks, &Task{
ID: t.ID,
ExecutionID: t.ExecutionID,
Status: t.Status,
StartTime: t.StartTime,
EndTime: t.EndTime,
})
}
return tasks, nil
}
// UpdateTask updates the task
func (d *DefaultManager) UpdateTask(task *Task, cols ...string) error {
if task == nil {
return errors.New("nil task")
}
if task.ID <= 0 {
return fmt.Errorf("invalid task ID: %d", task.ID)
}
return dao.UpdateTask(&models.RetentionTask{
ID: task.ID,
ExecutionID: task.ExecutionID,
Status: task.Status,
StartTime: task.StartTime,
EndTime: task.EndTime,
}, cols...)
}
// GetTaskLog gets the logs of task
func (d *DefaultManager) GetTaskLog(taskID int64) ([]byte, error) {
panic("implement me")
}
// NewManager ...
func NewManager() Manager {
return &DefaultManager{}
}

View File

@ -0,0 +1,203 @@
package retention
import (
"github.com/goharbor/harbor/src/pkg/retention/q"
"github.com/stretchr/testify/require"
"os"
"testing"
"time"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/pkg/retention/policy"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/stretchr/testify/assert"
)
func TestMain(m *testing.M) {
dao.PrepareTestForPostgresSQL()
os.Exit(m.Run())
}
func TestPolicy(t *testing.T) {
m := NewManager()
p1 := &policy.Metadata{
Algorithm: "OR",
Rules: []rule.Metadata{
{
ID: 1,
Priority: 1,
Template: "recentXdays",
Parameters: rule.Parameters{
"num": 10,
},
TagSelectors: []*rule.Selector{
{
Kind: "label",
Decoration: "with",
Pattern: "latest",
},
{
Kind: "regularExpression",
Decoration: "matches",
Pattern: "release-[\\d\\.]+",
},
},
ScopeSelectors: map[string][]*rule.Selector{
"repository": {
{
Kind: "regularExpression",
Decoration: "matches",
Pattern: ".+",
},
},
},
},
},
Trigger: &policy.Trigger{
Kind: "Schedule",
Settings: map[string]interface{}{
"cron": "* 22 11 * * *",
},
},
Scope: &policy.Scope{
Level: "project",
Reference: 1,
},
}
id, err := m.CreatePolicy(p1)
assert.Nil(t, err)
assert.True(t, id > 0)
p1, err = m.GetPolicy(id)
assert.Nil(t, err)
assert.EqualValues(t, "project", p1.Scope.Level)
assert.True(t, p1.ID > 0)
p1.Scope.Level = "test"
err = m.UpdatePolicy(p1)
assert.Nil(t, err)
p1, err = m.GetPolicy(id)
assert.Nil(t, err)
assert.EqualValues(t, "test", p1.Scope.Level)
err = m.DeletePolicyAndExec(id)
assert.Nil(t, err)
p1, err = m.GetPolicy(id)
assert.Nil(t, err)
assert.Nil(t, p1)
}
func TestExecution(t *testing.T) {
m := NewManager()
p1 := &policy.Metadata{
Algorithm: "OR",
Rules: []rule.Metadata{
{
ID: 1,
Priority: 1,
Template: "recentXdays",
Parameters: rule.Parameters{
"num": 10,
},
TagSelectors: []*rule.Selector{
{
Kind: "label",
Decoration: "with",
Pattern: "latest",
},
{
Kind: "regularExpression",
Decoration: "matches",
Pattern: "release-[\\d\\.]+",
},
},
ScopeSelectors: map[string][]*rule.Selector{
"repository": {
{
Kind: "regularExpression",
Decoration: "matches",
Pattern: ".+",
},
},
},
},
},
Trigger: &policy.Trigger{
Kind: "Schedule",
Settings: map[string]interface{}{
"cron": "* 22 11 * * *",
},
},
Scope: &policy.Scope{
Level: "project",
Reference: 1,
},
}
policyID, err := m.CreatePolicy(p1)
assert.Nil(t, err)
assert.True(t, policyID > 0)
e1 := &Execution{
PolicyID: policyID,
StartTime: time.Now(),
Status: ExecutionStatusInProgress,
Trigger: ExecutionTriggerManual,
DryRun: false,
}
id, err := m.CreateExecution(e1)
assert.Nil(t, err)
assert.True(t, id > 0)
e1, err = m.GetExecution(id)
assert.Nil(t, err)
assert.NotNil(t, e1)
assert.EqualValues(t, id, e1.ID)
e1.Status = ExecutionStatusFailed
err = m.UpdateExecution(e1)
assert.Nil(t, err)
e1, err = m.GetExecution(id)
assert.Nil(t, err)
assert.NotNil(t, e1)
assert.EqualValues(t, ExecutionStatusFailed, e1.Status)
es, err := m.ListExecutions(policyID, nil)
assert.Nil(t, err)
assert.EqualValues(t, 1, len(es))
}
func TestTask(t *testing.T) {
m := NewManager()
task := &Task{
ExecutionID: 1,
Status: TaskStatusPending,
StartTime: time.Now(),
}
// create
id, err := m.CreateTask(task)
require.Nil(t, err)
// update
task.ID = id
task.Status = TaskStatusInProgress
err = m.UpdateTask(task, "Status")
require.Nil(t, err)
// list
tasks, err := m.ListTasks(&q.TaskQuery{
ExecutionID: 1,
Status: TaskStatusInProgress,
})
require.Nil(t, err)
require.Equal(t, 1, len(tasks))
assert.Equal(t, int64(1), tasks[0].ExecutionID)
assert.Equal(t, TaskStatusInProgress, tasks[0].Status)
task.Status = TaskStatusFailed
err = m.UpdateTask(task, "Status")
require.Nil(t, err)
}

View File

@ -0,0 +1,70 @@
// 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 retention
import "time"
// const definitions
const (
ExecutionStatusInProgress string = "InProgress"
ExecutionStatusSucceed string = "Succeed"
ExecutionStatusFailed string = "Failed"
ExecutionStatusStopped string = "Stopped"
TaskStatusPending string = "Pending"
TaskStatusInProgress string = "InProgress"
TaskStatusSucceed string = "Succeed"
TaskStatusFailed string = "Failed"
TaskStatusStopped string = "Stopped"
CandidateKindImage string = "image"
CandidateKindChart string = "chart"
ExecutionTriggerManual string = "Manual"
ExecutionTriggerSchedule string = "Schedule"
)
// Execution of retention
type Execution struct {
ID int64 `json:"id"`
PolicyID int64 `json:"policy_id"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time,omitempty"`
Status string `json:"status"`
Trigger string `json:"Trigger"`
DryRun bool `json:"dry_run"`
}
// Task of retention
type Task struct {
ID int64 `json:"id"`
ExecutionID int64 `json:"execution_id"`
Status string `json:"status"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
}
// History of retention
type History struct {
ID int64 `json:"id,omitempty"`
ExecutionID int64 `json:"execution_id"`
Rule struct {
ID int `json:"id"`
DisplayText string `json:"display_text"`
} `json:"rule_id"`
// full path: :ns/:repo:tag
Artifact string `json:"tag"`
Timestamp time.Time `json:"timestamp"`
}

View File

@ -0,0 +1,52 @@
// 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 action
import (
"github.com/pkg/errors"
"sync"
)
// index for keeping the mapping action and its performer
var index sync.Map
// Register the performer with the corresponding action
func Register(action string, factory PerformerFactory) {
if len(action) == 0 || factory == nil {
// do nothing
return
}
index.Store(action, factory)
}
// Get performer with the provided action
func Get(action string, params interface{}) (Performer, error) {
if len(action) == 0 {
return nil, errors.New("empty action")
}
v, ok := index.Load(action)
if !ok {
return nil, errors.Errorf("action %s is not registered", action)
}
factory, ok := v.(PerformerFactory)
if !ok {
return nil, errors.Errorf("invalid action performer registered for action %s", action)
}
return factory(params), nil
}

View File

@ -0,0 +1,90 @@
// 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 action
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/stretchr/testify/suite"
)
// IndexTestSuite tests the rule index
type IndexTestSuite struct {
suite.Suite
candidates []*res.Candidate
}
// TestIndexEntry is entry of IndexTestSuite
func TestIndexEntry(t *testing.T) {
suite.Run(t, new(IndexTestSuite))
}
// SetupSuite ...
func (suite *IndexTestSuite) SetupSuite() {
Register("fakeAction", newFakePerformer)
suite.candidates = []*res.Candidate{{
Namespace: "library",
Repository: "harbor",
Kind: "image",
Tag: "latest",
PushedTime: time.Now().Unix(),
Labels: []string{"L1", "L2"},
}}
}
// TestRegister tests register
func (suite *IndexTestSuite) TestGet() {
p, err := Get("fakeAction", nil)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), p)
results, err := p.Perform(suite.candidates)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 1, len(results))
assert.Condition(suite.T(), func() (success bool) {
r := results[0]
success = r.Target != nil &&
r.Error == nil &&
r.Target.Repository == "harbor" &&
r.Target.Tag == "latest"
return
})
}
type fakePerformer struct{}
// Perform the artifacts
func (p *fakePerformer) Perform(candidates []*res.Candidate) (results []*res.Result, err error) {
for _, c := range candidates {
results = append(results, &res.Result{
Target: c,
})
}
return
}
func newFakePerformer(params interface{}) Performer {
return &fakePerformer{}
}

View 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 action
import (
"github.com/goharbor/harbor/src/pkg/retention/dep"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
const (
// Retain artifacts
Retain = "retain"
)
// Performer performs the related actions targeting the candidates
type Performer interface {
// Perform the action
//
// Arguments:
// candidates []*res.Candidate : the targets to perform
//
// Returns:
// []*res.Result : result infos
// error : common error if any errors occurred
Perform(candidates []*res.Candidate) ([]*res.Result, error)
}
// PerformerFactory is factory method for creating Performer
type PerformerFactory func(params interface{}) Performer
// retainAction make sure all the candidates will be retained and others will be cleared
type retainAction struct {
all []*res.Candidate
}
// Perform the action
func (ra *retainAction) Perform(candidates []*res.Candidate) (results []*res.Result, err error) {
retained := make(map[string]bool)
for _, c := range candidates {
retained[c.Hash()] = true
}
// start to delete
if len(ra.all) > 0 {
for _, art := range ra.all {
if _, ok := retained[art.Hash()]; !ok {
result := &res.Result{
Target: art,
}
if err := dep.DefaultClient.Delete(art); err != nil {
result.Error = err
}
results = append(results, result)
}
}
}
return
}
// NewRetainAction is factory method for RetainAction
func NewRetainAction(params interface{}) Performer {
if params != nil {
if all, ok := params.([]*res.Candidate); ok {
return &retainAction{
all: all,
}
}
}
return &retainAction{
all: make([]*res.Candidate, 0),
}
}
func init() {
// Register itself
Register(Retain, NewRetainAction)
}

View File

@ -0,0 +1,112 @@
// 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 action
import (
"github.com/goharbor/harbor/src/pkg/retention/dep"
"github.com/goharbor/harbor/src/pkg/retention/policy/lwp"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"testing"
"time"
)
// TestPerformerSuite tests the performer related function
type TestPerformerSuite struct {
suite.Suite
oldClient dep.Client
all []*res.Candidate
}
// TestPerformer is the entry of the TestPerformerSuite
func TestPerformer(t *testing.T) {
suite.Run(t, new(TestPerformerSuite))
}
// SetupSuite ...
func (suite *TestPerformerSuite) SetupSuite() {
suite.all = []*res.Candidate{
{
Namespace: "library",
Repository: "harbor",
Kind: "image",
Tag: "latest",
PushedTime: time.Now().Unix(),
Labels: []string{"L1", "L2"},
},
{
Namespace: "library",
Repository: "harbor",
Kind: "image",
Tag: "dev",
PushedTime: time.Now().Unix(),
Labels: []string{"L3"},
},
}
suite.oldClient = dep.DefaultClient
dep.DefaultClient = &fakeRetentionClient{}
}
// TearDownSuite ...
func (suite *TestPerformerSuite) TearDownSuite() {
dep.DefaultClient = suite.oldClient
}
// TestPerform tests Perform action
func (suite *TestPerformerSuite) TestPerform() {
p := &retainAction{
all: suite.all,
}
candidates := []*res.Candidate{
{
Namespace: "library",
Repository: "harbor",
Kind: "image",
Tag: "latest",
PushedTime: time.Now().Unix(),
Labels: []string{"L1", "L2"},
},
}
results, err := p.Perform(candidates)
require.NoError(suite.T(), err)
require.Equal(suite.T(), 1, len(results))
require.NotNil(suite.T(), results[0].Target)
assert.NoError(suite.T(), results[0].Error)
assert.Equal(suite.T(), "dev", results[0].Target.Tag)
}
type fakeRetentionClient struct{}
// GetCandidates ...
func (frc *fakeRetentionClient) GetCandidates(repo *res.Repository) ([]*res.Candidate, error) {
return nil, errors.New("not implemented")
}
// Delete ...
func (frc *fakeRetentionClient) Delete(candidate *res.Candidate) error {
return nil
}
// SubmitTask ...
func (frc *fakeRetentionClient) SubmitTask(taskID int64, repository *res.Repository, meta *lwp.Metadata) (string, error) {
return "", errors.New("not implemented")
}

View File

@ -0,0 +1,49 @@
// 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 alg
import (
"github.com/pkg/errors"
"sync"
)
const (
// AlgorithmOR for || algorithm
AlgorithmOR = "or"
)
// index for keeping the mapping between algorithm and its processor
var index sync.Map
// Register processor with the algorithm
func Register(algorithm string, processor Factory) {
if len(algorithm) > 0 && processor != nil {
index.Store(algorithm, processor)
}
}
// Get Processor
func Get(algorithm string, params []*Parameter) (Processor, error) {
v, ok := index.Load(algorithm)
if !ok {
return nil, errors.Errorf("no processor registered with algorithm: %s", algorithm)
}
if fac, ok := v.(Factory); ok {
return fac(params), nil
}
return nil, errors.Errorf("no valid processor registered for algorithm: %s", algorithm)
}

View File

@ -0,0 +1,221 @@
// 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 or
import (
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/alg"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/pkg/errors"
"sync"
)
// processor to handle the rules with OR mapping ways
type processor struct {
// keep evaluator and its related selector if existing
// attentions here, the selectors can be empty/nil, that means match all "**"
evaluators map[*rule.Evaluator][]res.Selector
// action performer
performers map[string]action.Performer
}
// New processor
func New(parameters []*alg.Parameter) alg.Processor {
p := &processor{
evaluators: make(map[*rule.Evaluator][]res.Selector),
performers: make(map[string]action.Performer),
}
if len(parameters) > 0 {
for _, param := range parameters {
if param.Evaluator != nil {
if len(param.Selectors) > 0 {
p.evaluators[&param.Evaluator] = param.Selectors
}
if param.Performer != nil {
p.performers[param.Evaluator.Action()] = param.Performer
}
}
}
}
return p
}
// Process the candidates with the rules
func (p *processor) Process(artifacts []*res.Candidate) ([]*res.Result, error) {
if len(artifacts) == 0 {
log.Debug("no artifacts to retention")
return make([]*res.Result, 0), nil
}
var (
// collect errors by wrapping
err error
// collect processed candidates
processedCandidates = make(map[string]cHash)
)
// for sync
type chanItem struct {
action string
processed []*res.Candidate
}
resChan := make(chan *chanItem, 1)
// handle error
errChan := make(chan error, 1)
// control chan
done := make(chan bool, 1)
// go routine for receiving results/error
go func() {
defer func() {
// done
done <- true
}()
for {
select {
case result := <-resChan:
if result == nil {
// chan is closed
return
}
if _, ok := processedCandidates[result.action]; !ok {
processedCandidates[result.action] = make(cHash)
}
listByAction := processedCandidates[result.action]
for _, rp := range result.processed {
// remove duplicated ones
listByAction[rp.Hash()] = rp
}
case e := <-errChan:
if err == nil {
err = errors.Wrap(e, "artifact processing error")
} else {
err = errors.Wrap(e, err.Error())
}
}
}
}()
wg := new(sync.WaitGroup)
wg.Add(len(p.evaluators))
for eva, selectors := range p.evaluators {
var evaluator = *eva
go func(evaluator rule.Evaluator, selectors []res.Selector) {
var (
processed []*res.Candidate
err error
)
defer func() {
wg.Done()
}()
// init
// pass array copy to the selector
processed = append(processed, artifacts...)
if len(selectors) > 0 {
// selecting artifacts one by one
// `&&` mappings
for _, s := range selectors {
if processed, err = s.Select(processed); err != nil {
errChan <- err
return
}
}
}
if processed, err = evaluator.Process(processed); err != nil {
errChan <- err
return
}
if len(processed) > 0 {
// Pass to the outside
resChan <- &chanItem{
action: evaluator.Action(),
processed: processed,
}
}
}(evaluator, selectors)
}
// waiting for all the rules are evaluated
wg.Wait()
// close result chan
close(resChan)
// check if the receiving loop exists
<-done
if err != nil {
return nil, err
}
results := make([]*res.Result, 0)
// Perform actions
for act, hash := range processedCandidates {
var attachedErr error
cl := hash.toList()
if pf, ok := p.performers[act]; ok {
if theRes, err := pf.Perform(cl); err != nil {
attachedErr = err
} else {
results = append(results, theRes...)
}
} else {
attachedErr = errors.Errorf("no performer added for action %s in OR processor", act)
}
if attachedErr != nil {
for _, c := range cl {
results = append(results, &res.Result{
Target: c,
Error: attachedErr,
})
}
}
}
return results, nil
}
func init() {
alg.Register(alg.AlgorithmOR, New)
}
type cHash map[string]*res.Candidate
func (ch cHash) toList() []*res.Candidate {
l := make([]*res.Candidate, 0)
for _, v := range ch {
l = append(l, v)
}
return l
}

View File

@ -0,0 +1,145 @@
// 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 or
import (
"errors"
"testing"
"time"
"github.com/goharbor/harbor/src/pkg/retention/dep"
"github.com/goharbor/harbor/src/pkg/retention/policy/lwp"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/alg"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule/lastx"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule/latestk"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors/doublestar"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors/label"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
// ProcessorTestSuite is suite for testing processor
type ProcessorTestSuite struct {
suite.Suite
p alg.Processor
all []*res.Candidate
oldClient dep.Client
}
// TestProcessor is entrance for ProcessorTestSuite
func TestProcessor(t *testing.T) {
suite.Run(t, new(ProcessorTestSuite))
}
// SetupSuite ...
func (suite *ProcessorTestSuite) SetupSuite() {
suite.all = []*res.Candidate{
{
Namespace: "library",
Repository: "harbor",
Kind: "image",
Tag: "latest",
PushedTime: time.Now().Unix(),
Labels: []string{"L1", "L2"},
},
{
Namespace: "library",
Repository: "harbor",
Kind: "image",
Tag: "dev",
PushedTime: time.Now().Unix(),
Labels: []string{"L3"},
},
}
params := make([]*alg.Parameter, 0)
perf := action.NewRetainAction(suite.all)
lastxParams := make(map[string]rule.Parameter)
lastxParams[lastx.ParameterX] = 10
params = append(params, &alg.Parameter{
Evaluator: lastx.New(lastxParams),
Selectors: []res.Selector{
doublestar.New(doublestar.Matches, "*dev*"),
label.New(label.With, "L1,L2"),
},
Performer: perf,
})
latestKParams := make(map[string]rule.Parameter)
latestKParams[latestk.ParameterK] = 10
params = append(params, &alg.Parameter{
Evaluator: latestk.New(latestKParams),
Selectors: []res.Selector{
label.New(label.With, "L3"),
},
Performer: perf,
})
p, err := alg.Get(alg.AlgorithmOR, params)
require.NoError(suite.T(), err)
suite.p = p
suite.oldClient = dep.DefaultClient
dep.DefaultClient = &fakeRetentionClient{}
}
// TearDownSuite ...
func (suite *ProcessorTestSuite) TearDownSuite() {
dep.DefaultClient = suite.oldClient
}
// TestProcess tests process method
func (suite *ProcessorTestSuite) TestProcess() {
results, err := suite.p.Process(suite.all)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 1, len(results))
assert.Condition(suite.T(), func() bool {
for _, r := range results {
if r.Error != nil {
return false
}
}
return true
}, "no errors in the returned result list")
}
type fakeRetentionClient struct{}
// GetCandidates ...
func (frc *fakeRetentionClient) GetCandidates(repo *res.Repository) ([]*res.Candidate, error) {
return nil, errors.New("not implemented")
}
// Delete ...
func (frc *fakeRetentionClient) Delete(candidate *res.Candidate) error {
return nil
}
// SubmitTask ...
func (frc *fakeRetentionClient) SubmitTask(taskID int64, repository *res.Repository, meta *lwp.Metadata) (string, error) {
return "", errors.New("not implemented")
}

View File

@ -0,0 +1,52 @@
// 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 alg
import (
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
// Processor processing the whole policy targeting a repository.
// Methods are defined to reflect the standard structure of the policy:
// list of rules with corresponding selectors plus an action performer.
type Processor interface {
// Process the artifact candidates
//
// Arguments:
// artifacts []*res.Candidate : process the retention candidates
//
// Returns:
// []*res.Result : the processed results
// error : common error object if any errors occurred
Process(artifacts []*res.Candidate) ([]*res.Result, error)
}
// Parameter for constructing a processor
// Represents one rule
type Parameter struct {
// Evaluator for the rule
Evaluator rule.Evaluator
// Selectors for the rule
Selectors []res.Selector
// Performer for the rule evaluator
Performer action.Performer
}
// Factory for creating processor
type Factory func([]*Parameter) Processor

View File

@ -0,0 +1,95 @@
// 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 policy
import (
"fmt"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/alg"
"github.com/goharbor/harbor/src/pkg/retention/policy/lwp"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors"
"github.com/pkg/errors"
)
// Builder builds the runnable processor from the raw policy
type Builder interface {
// Builds runnable processor
//
// Arguments:
// policy *Metadata : the simple metadata of retention policy
//
// Returns:
// Processor : a processor implementation to process the candidates
// error : common error object if any errors occurred
Build(policy *lwp.Metadata) (alg.Processor, error)
}
// NewBuilder news a basic builder
func NewBuilder(all []*res.Candidate) Builder {
return &basicBuilder{
allCandidates: all,
}
}
// basicBuilder is default implementation of Builder interface
type basicBuilder struct {
allCandidates []*res.Candidate
}
// Build policy processor from the raw policy
func (bb *basicBuilder) Build(policy *lwp.Metadata) (alg.Processor, error) {
if policy == nil {
return nil, errors.New("nil policy to build processor")
}
params := make([]*alg.Parameter, 0)
for _, r := range policy.Rules {
evaluator, err := rule.Get(r.Template, r.Parameters)
if err != nil {
return nil, err
}
perf, err := action.Get(r.Action, bb.allCandidates)
if err != nil {
return nil, errors.Wrap(err, "get action performer by metadata")
}
sl := make([]res.Selector, 0)
for _, s := range r.TagSelectors {
sel, err := selectors.Get(s.Kind, s.Decoration, s.Pattern)
if err != nil {
return nil, errors.Wrap(err, "get selector by metadata")
}
sl = append(sl, sel)
}
params = append(params, &alg.Parameter{
Evaluator: evaluator,
Selectors: sl,
Performer: perf,
})
}
p, err := alg.Get(policy.Algorithm, params)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("get processor for algorithm: %s", policy.Algorithm))
}
return p, nil
}

View File

@ -0,0 +1,185 @@
// 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 policy
import (
"testing"
"time"
"github.com/goharbor/harbor/src/pkg/retention/dep"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors"
"github.com/pkg/errors"
"github.com/goharbor/harbor/src/pkg/retention/policy/alg"
"github.com/goharbor/harbor/src/pkg/retention/policy/alg/or"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors/label"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors/doublestar"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule/latestk"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/policy/lwp"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/stretchr/testify/suite"
)
// TestBuilderSuite is the suite to test builder
type TestBuilderSuite struct {
suite.Suite
all []*res.Candidate
oldClient dep.Client
}
// TestBuilder is the entry of testing TestBuilderSuite
func TestBuilder(t *testing.T) {
suite.Run(t, new(TestBuilderSuite))
}
// SetupSuite prepares the testing content if needed
func (suite *TestBuilderSuite) SetupSuite() {
suite.all = []*res.Candidate{
{
NamespaceID: 1,
Namespace: "library",
Repository: "harbor",
Kind: "image",
Tag: "latest",
PushedTime: time.Now().Unix(),
Labels: []string{"L1", "L2"},
},
{
NamespaceID: 1,
Namespace: "library",
Repository: "harbor",
Kind: "image",
Tag: "dev",
PushedTime: time.Now().Unix(),
Labels: []string{"L3"},
},
}
alg.Register(alg.AlgorithmOR, or.New)
selectors.Register(doublestar.Kind, []string{
doublestar.Matches,
doublestar.Excludes,
doublestar.RepoMatches,
doublestar.RepoExcludes,
doublestar.NSMatches,
doublestar.NSExcludes,
}, doublestar.New)
selectors.Register(label.Kind, []string{label.With, label.Without}, label.New)
action.Register(action.Retain, action.NewRetainAction)
suite.oldClient = dep.DefaultClient
dep.DefaultClient = &fakeRetentionClient{}
}
// TearDownSuite ...
func (suite *TestBuilderSuite) TearDownSuite() {
dep.DefaultClient = suite.oldClient
}
// TestBuild tests the Build function
func (suite *TestBuilderSuite) TestBuild() {
b := &basicBuilder{suite.all}
params := make(rule.Parameters)
params[latestk.ParameterK] = 10
scopeSelectors := make(map[string][]*rule.Selector, 1)
scopeSelectors["repository"] = []*rule.Selector{{
Kind: doublestar.Kind,
Decoration: doublestar.RepoMatches,
Pattern: "**",
}}
lm := &lwp.Metadata{
Algorithm: AlgorithmOR,
Rules: []*rule.Metadata{{
ID: 1,
Priority: 999,
Action: action.Retain,
Template: latestk.TemplateID,
Parameters: params,
ScopeSelectors: scopeSelectors,
TagSelectors: []*rule.Selector{
{
Kind: label.Kind,
Decoration: label.With,
Pattern: "L3",
},
},
}},
}
p, err := b.Build(lm)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), p)
artifacts := []*res.Candidate{
{
NamespaceID: 1,
Namespace: "library",
Repository: "harbor",
Kind: "image",
Tag: "dev",
PushedTime: time.Now().Unix(),
Labels: []string{"L3"},
},
}
results, err := p.Process(artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 1, len(results))
assert.Condition(suite.T(), func() (success bool) {
art := results[0]
success = art.Error == nil &&
art.Target != nil &&
art.Target.Repository == "harbor" &&
art.Target.Tag == "latest"
return
})
}
type fakeRetentionClient struct{}
// GetCandidates ...
func (frc *fakeRetentionClient) GetCandidates(repo *res.Repository) ([]*res.Candidate, error) {
return nil, errors.New("not implemented")
}
// Delete ...
func (frc *fakeRetentionClient) Delete(candidate *res.Candidate) error {
return nil
}
// SubmitTask ...
func (frc *fakeRetentionClient) SubmitTask(taskID int64, repository *res.Repository, meta *lwp.Metadata) (string, error) {
return "", errors.New("not implemented")
}

View File

@ -0,0 +1,29 @@
// 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 lwp = lightweight policy
package lwp
import "github.com/goharbor/harbor/src/pkg/retention/policy/rule"
// Metadata contains partial metadata of policy
// It's a lightweight version of policy.Metadata
type Metadata struct {
// Algorithm applied to the rules
// "OR" / "AND"
Algorithm string `json:"algorithm"`
// Rule collection
Rules []*rule.Metadata `json:"rules"`
}

View File

@ -0,0 +1,83 @@
// 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 policy
import (
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
)
const (
// AlgorithmOR for OR algorithm
AlgorithmOR = "or"
// TriggerKindSchedule Schedule
TriggerKindSchedule = "Schedule"
// TriggerReferencesJobid job_id
TriggerReferencesJobid = "job_id"
// TriggerSettingsCron cron
TriggerSettingsCron = "cron"
// ScopeLevelProject project
ScopeLevelProject = "project"
)
// Metadata of policy
type Metadata struct {
// ID of the policy
ID int64 `json:"id"`
// Algorithm applied to the rules
// "OR" / "AND"
Algorithm string `json:"algorithm" valid:"Required;Match(/^(OR|AND)$/)"`
// Rule collection
Rules []rule.Metadata `json:"rules"`
// Trigger about how to launch the policy
Trigger *Trigger `json:"trigger" valid:"Required"`
// Which scope the policy will be applied to
Scope *Scope `json:"scope" valid:"Required"`
// The max number of rules in a policy
Capacity int `json:"cap"`
}
// Trigger of the policy
type Trigger struct {
// Const string to declare the trigger type
// 'Schedule'
Kind string `json:"kind"`
// Settings for the specified trigger
// '[cron]="* 22 11 * * *"' for the 'Schedule'
Settings map[string]interface{} `json:"settings"`
// References of the trigger
// e.g: schedule job ID
References map[string]interface{} `json:"references"`
}
// Scope definition
type Scope struct {
// Scope level declaration
// 'system', 'project' and 'repository'
Level string `json:"level" valid:"Required;Match(/^(project)$/)"`
// The reference identity for the specified level
// 0 for 'system', project ID for 'project' and repo ID for 'repository'
Reference int64 `json:"ref" valid:"Required"`
}

View File

@ -0,0 +1,50 @@
// 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 always
import (
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
const (
// TemplateID of the always retain rule
TemplateID = "always"
)
type evaluator struct{}
// Process for the "always" Evaluator simply returns the input with no error
func (e *evaluator) Process(artifacts []*res.Candidate) ([]*res.Candidate, error) {
return artifacts, nil
}
func (e *evaluator) Action() string {
return action.Retain
}
// New returns an "always" Evaluator. It requires no parameters.
func New(_ rule.Parameters) rule.Evaluator {
return &evaluator{}
}
func init() {
rule.Register(&rule.IndexMeta{
TemplateID: TemplateID,
Action: action.Retain,
Parameters: []*rule.IndexedParam{},
}, New)
}

View File

@ -0,0 +1,49 @@
// 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 always
import (
"testing"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type EvaluatorTestSuite struct {
suite.Suite
}
func (e *EvaluatorTestSuite) TestNew() {
sut := New(rule.Parameters{})
require.NotNil(e.T(), sut)
require.IsType(e.T(), &evaluator{}, sut)
}
func (e *EvaluatorTestSuite) TestProcess() {
sut := New(rule.Parameters{})
input := []*res.Candidate{{PushedTime: 0}, {PushedTime: 1}, {PushedTime: 2}, {PushedTime: 3}}
result, err := sut.Process(input)
require.NoError(e.T(), err)
require.Len(e.T(), result, len(input))
}
func TestEvaluatorSuite(t *testing.T) {
suite.Run(t, &EvaluatorTestSuite{})
}

View File

@ -0,0 +1,36 @@
// 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 rule
import "github.com/goharbor/harbor/src/pkg/retention/res"
// Evaluator defines method of executing rule
type Evaluator interface {
// Filter the inputs and return the filtered outputs
//
// Arguments:
// artifacts []*res.Candidate : candidates for processing
//
// Returns:
// []*res.Candidate : matched candidates for next stage
// error : common error object if any errors occurred
Process(artifacts []*res.Candidate) ([]*res.Candidate, error)
// Specify what action is performed to the candidates processed by this evaluator
Action() string
}
// Factory defines a factory method for creating rule evaluator
type Factory func(parameters Parameters) Evaluator

View File

@ -0,0 +1,116 @@
// 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 rule
import (
"github.com/pkg/errors"
"sync"
)
// index for keeping the mapping between template ID and evaluator
var index sync.Map
// IndexMeta defines metadata for rule registration
type IndexMeta struct {
TemplateID string `json:"rule_template"`
// Action of the rule performs
// "retain"
Action string `json:"action"`
Parameters []*IndexedParam `json:"params"`
}
// IndexedParam declares the param info
type IndexedParam struct {
Name string `json:"name"`
// Type of the param
// "int", "string" or "[]string"
Type string `json:"type"`
Unit string `json:"unit"`
Required bool `json:"required"`
}
// indexedItem is the item saved in the sync map
type indexedItem struct {
Meta *IndexMeta
Factory Factory
}
// Register the rule evaluator with the corresponding rule template
func Register(meta *IndexMeta, factory Factory) {
if meta == nil || factory == nil || len(meta.TemplateID) == 0 {
// do nothing
return
}
index.Store(meta.TemplateID, &indexedItem{
Meta: meta,
Factory: factory,
})
}
// Get rule evaluator with the provided template ID
func Get(templateID string, parameters Parameters) (Evaluator, error) {
if len(templateID) == 0 {
return nil, errors.New("empty rule template ID")
}
v, ok := index.Load(templateID)
if !ok {
return nil, errors.Errorf("rule evaluator %s is not registered", templateID)
}
item := v.(*indexedItem)
// We can check more things if we want to do in the future
if len(item.Meta.Parameters) > 0 {
for _, p := range item.Meta.Parameters {
if p.Required {
exists := parameters != nil
if exists {
_, exists = parameters[p.Name]
}
if !exists {
return nil, errors.Errorf("missing required parameter %s for rule %s", p.Name, templateID)
}
}
}
}
factory := item.Factory
return factory(parameters), nil
}
// Index returns all the metadata of the registered rules
func Index() []*IndexMeta {
res := make([]*IndexMeta, 0)
index.Range(func(k, v interface{}) bool {
if item, ok := v.(*indexedItem); ok {
res = append(res, item.Meta)
return true
}
return false
})
return res
}

View File

@ -0,0 +1,117 @@
// 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 rule
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/stretchr/testify/suite"
)
// IndexTestSuite tests the rule index
type IndexTestSuite struct {
suite.Suite
}
// TestIndexEntry is entry of IndexTestSuite
func TestIndexEntry(t *testing.T) {
suite.Run(t, new(IndexTestSuite))
}
// SetupSuite ...
func (suite *IndexTestSuite) SetupSuite() {
Register(&IndexMeta{
TemplateID: "fakeEvaluator",
Action: "retain",
Parameters: []*IndexedParam{
{
Name: "fakeParam",
Type: "int",
Unit: "count",
Required: true,
},
},
}, newFakeEvaluator)
}
// TestRegister tests register
func (suite *IndexTestSuite) TestGet() {
params := make(Parameters)
params["fakeParam"] = 99
evaluator, err := Get("fakeEvaluator", params)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), evaluator)
candidates := []*res.Candidate{{
Namespace: "library",
Repository: "harbor",
Kind: "image",
Tag: "latest",
PushedTime: time.Now().Unix(),
Labels: []string{"L1", "L2"},
}}
res, err := evaluator.Process(candidates)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 1, len(res))
assert.Condition(suite.T(), func() bool {
c := res[0]
return c.Repository == "harbor" && c.Tag == "latest"
})
}
// TestIndex tests Index
func (suite *IndexTestSuite) TestIndex() {
metas := Index()
require.Equal(suite.T(), 1, len(metas))
assert.Condition(suite.T(), func() bool {
m := metas[0]
return m.TemplateID == "fakeEvaluator" &&
m.Action == "retain" &&
len(m.Parameters) > 0
})
}
type fakeEvaluator struct {
i int
}
// Process rule
func (e *fakeEvaluator) Process(artifacts []*res.Candidate) ([]*res.Candidate, error) {
return artifacts, nil
}
// Action of the rule
func (e *fakeEvaluator) Action() string {
return "retain"
}
// newFakeEvaluator is the factory of fakeEvaluator
func newFakeEvaluator(parameters Parameters) Evaluator {
i := 10
if v, ok := parameters["fakeParam"]; ok {
i = v.(int)
}
return &fakeEvaluator{i}
}

View File

@ -0,0 +1,71 @@
// 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 lastpulled
import (
"sort"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
const (
// TemplateID of the rule
TemplateID = "lastpulled"
// ParameterN is the name of the metadata parameter for the N value
ParameterN = TemplateID
// DefaultN is the default number of tags to retain
DefaultN = 10
)
type evaluator struct {
n int
}
func (e *evaluator) Process(artifacts []*res.Candidate) ([]*res.Candidate, error) {
sort.Slice(artifacts, func(i, j int) bool {
return artifacts[i].PulledTime > artifacts[j].PulledTime
})
i := e.n
if i > len(artifacts) {
i = len(artifacts)
}
return artifacts[:i], nil
}
func (e *evaluator) Action() string {
return action.Retain
}
// New constructs an evaluator with the given parameters
func New(params rule.Parameters) rule.Evaluator {
if params != nil {
if p, ok := params[ParameterN]; ok {
if v, ok := p.(int); ok && v >= 0 {
return &evaluator{n: v}
}
}
}
log.Debugf("default parameter %d used for rule %s", DefaultN, TemplateID)
return &evaluator{n: DefaultN}
}

View File

@ -0,0 +1,89 @@
// 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 lastpulled
import (
"math/rand"
"strconv"
"testing"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type EvaluatorTestSuite struct {
suite.Suite
}
func (e *EvaluatorTestSuite) TestNew() {
tests := []struct {
Name string
args rule.Parameters
expectedK int
}{
{Name: "Valid", args: map[string]rule.Parameter{ParameterN: 5}, expectedK: 5},
{Name: "Default If Negative", args: map[string]rule.Parameter{ParameterN: -1}, expectedK: DefaultN},
{Name: "Default If Not Set", args: map[string]rule.Parameter{}, expectedK: DefaultN},
{Name: "Default If Wrong Type", args: map[string]rule.Parameter{ParameterN: "foo"}, expectedK: DefaultN},
}
for _, tt := range tests {
e.T().Run(tt.Name, func(t *testing.T) {
e := New(tt.args).(*evaluator)
require.Equal(t, tt.expectedK, e.n)
})
}
}
func (e *EvaluatorTestSuite) TestProcess() {
data := []*res.Candidate{{PulledTime: 0}, {PulledTime: 1}, {PulledTime: 2}, {PulledTime: 3}, {PulledTime: 4}}
rand.Shuffle(len(data), func(i, j int) {
data[i], data[j] = data[j], data[i]
})
tests := []struct {
n int
expected int
minPullTime int64
}{
{n: 0, expected: 0, minPullTime: 0},
{n: 1, expected: 1, minPullTime: 4},
{n: 3, expected: 3, minPullTime: 2},
{n: 5, expected: 5, minPullTime: 0},
{n: 6, expected: 5, minPullTime: 0},
}
for _, tt := range tests {
e.T().Run(strconv.Itoa(tt.n), func(t *testing.T) {
ev := New(map[string]rule.Parameter{ParameterN: tt.n})
result, err := ev.Process(data)
require.NoError(t, err)
require.Len(t, result, tt.expected)
for _, v := range result {
require.False(e.T(), v.PulledTime < tt.minPullTime)
}
})
}
}
func TestEvaluatorSuite(t *testing.T) {
suite.Run(t, &EvaluatorTestSuite{})
}

View File

@ -0,0 +1,91 @@
// 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 lastx
import (
"time"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
const (
// TemplateID of last x days rule
TemplateID = "lastXDays"
// ParameterX ...
ParameterX = TemplateID
// DefaultX defines the default X
DefaultX = 10
)
// evaluator for evaluating last x days
type evaluator struct {
// last x days
x int
}
// Process the candidates based on the rule definition
func (e *evaluator) Process(artifacts []*res.Candidate) (retain []*res.Candidate, err error) {
cutoff := time.Now().Add(time.Duration(e.x*-24) * time.Hour)
for _, a := range artifacts {
if time.Unix(a.PushedTime, 0).UTC().After(cutoff) {
retain = append(retain, a)
}
}
return
}
// Specify what action is performed to the candidates processed by this evaluator
func (e *evaluator) Action() string {
return action.Retain
}
// New a Evaluator
func New(params rule.Parameters) rule.Evaluator {
if params != nil {
if param, ok := params[ParameterX]; ok {
if v, ok := param.(int); ok && v >= 0 {
return &evaluator{
x: v,
}
}
}
}
log.Debugf("default parameter %d used for rule %s", DefaultX, TemplateID)
return &evaluator{
x: DefaultX,
}
}
func init() {
// Register itself
rule.Register(&rule.IndexMeta{
TemplateID: TemplateID,
Action: action.Retain,
Parameters: []*rule.IndexedParam{
{
Name: ParameterX,
Type: "int",
Unit: "days",
Required: true,
},
},
}, New)
}

View File

@ -0,0 +1,78 @@
package lastx
import (
"fmt"
"testing"
"time"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type EvaluatorTestSuite struct {
suite.Suite
}
func (e *EvaluatorTestSuite) TestNew() {
tests := []struct {
Name string
args rule.Parameters
expectedX int
}{
{Name: "Valid", args: map[string]rule.Parameter{ParameterX: 3}, expectedX: 3},
{Name: "Default If Negative", args: map[string]rule.Parameter{ParameterX: -3}, expectedX: DefaultX},
{Name: "Default If Not Set", args: map[string]rule.Parameter{}, expectedX: DefaultX},
{Name: "Default If Wrong Type", args: map[string]rule.Parameter{}, expectedX: DefaultX},
}
for _, tt := range tests {
e.T().Run(tt.Name, func(t *testing.T) {
e := New(tt.args).(*evaluator)
require.Equal(t, tt.expectedX, e.x)
})
}
}
func (e *EvaluatorTestSuite) TestProcess() {
now := time.Now().UTC()
data := []*res.Candidate{
{PushedTime: now.Add(time.Duration(1*-24) * time.Hour).Unix()},
{PushedTime: now.Add(time.Duration(2*-24) * time.Hour).Unix()},
{PushedTime: now.Add(time.Duration(3*-24) * time.Hour).Unix()},
{PushedTime: now.Add(time.Duration(4*-24) * time.Hour).Unix()},
{PushedTime: now.Add(time.Duration(5*-24) * time.Hour).Unix()},
{PushedTime: now.Add(time.Duration(99*-24) * time.Hour).Unix()},
}
tests := []struct {
days int
expected int
}{
{days: 0, expected: 0},
{days: 1, expected: 0},
{days: 2, expected: 1},
{days: 3, expected: 2},
{days: 4, expected: 3},
{days: 5, expected: 4},
{days: 6, expected: 5},
{days: 7, expected: 5},
}
for _, tt := range tests {
e.T().Run(fmt.Sprintf("%d days - should keep %d", tt.days, tt.expected), func(t *testing.T) {
e := New(rule.Parameters{ParameterX: tt.days})
result, err := e.Process(data)
require.NoError(t, err)
require.Len(t, result, tt.expected)
})
}
}
func TestEvaluatorSuite(t *testing.T) {
suite.Run(t, &EvaluatorTestSuite{})
}

View File

@ -0,0 +1,105 @@
// 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 latestk
import (
"sort"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
const (
// TemplateID of latest active k rule
TemplateID = "latestActiveK"
// ParameterK ...
ParameterK = TemplateID
// DefaultK defines the default K
DefaultK = 10
)
// evaluator for evaluating latest active k images
type evaluator struct {
// latest k
k int
}
// Process the candidates based on the rule definition
func (e *evaluator) Process(artifacts []*res.Candidate) ([]*res.Candidate, error) {
// Sort artifacts by their "active time"
//
// Active time is defined as the selection of c.PulledTime or c.PushedTime,
// whichever is bigger, aka more recent.
sort.Slice(artifacts, func(i, j int) bool {
return activeTime(artifacts[i]) > activeTime(artifacts[j])
})
i := e.k
if i > len(artifacts) {
i = len(artifacts)
}
return artifacts[:i], nil
}
// Specify what action is performed to the candidates processed by this evaluator
func (e *evaluator) Action() string {
return action.Retain
}
// New a Evaluator
func New(params rule.Parameters) rule.Evaluator {
if params != nil {
if param, ok := params[ParameterK]; ok {
if v, ok := param.(int); ok && v >= 0 {
return &evaluator{
k: v,
}
}
}
}
log.Debugf("default parameter %d used for rule %s", DefaultK, TemplateID)
return &evaluator{
k: DefaultK,
}
}
func activeTime(c *res.Candidate) int64 {
if c.PulledTime > c.PushedTime {
return c.PulledTime
}
return c.PushedTime
}
func init() {
// Register itself
rule.Register(&rule.IndexMeta{
TemplateID: TemplateID,
Action: action.Retain,
Parameters: []*rule.IndexedParam{
{
Name: ParameterK,
Type: "int",
Unit: "count",
Required: true,
},
},
}, New)
}

View File

@ -0,0 +1,99 @@
// 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 latestk
import (
"strconv"
"testing"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/stretchr/testify/suite"
)
type EvaluatorTestSuite struct {
suite.Suite
artifacts []*res.Candidate
}
func (e *EvaluatorTestSuite) SetupSuite() {
e.artifacts = []*res.Candidate{
{PulledTime: 1, PushedTime: 2},
{PulledTime: 3, PushedTime: 4},
{PulledTime: 6, PushedTime: 5},
{PulledTime: 8, PushedTime: 7},
{PulledTime: 9, PushedTime: 9},
{PulledTime: 10, PushedTime: 10},
{PulledTime: 0, PushedTime: 11},
}
}
func (e *EvaluatorTestSuite) TestProcess() {
tests := []struct {
k int
expected int
minActiveTime int64
}{
{k: 0, expected: 0},
{k: 1, expected: 1, minActiveTime: 11},
{k: 2, expected: 2, minActiveTime: 10},
{k: 5, expected: 5, minActiveTime: 6},
{k: 6, expected: 6, minActiveTime: 3},
{k: 99, expected: len(e.artifacts)},
}
for _, tt := range tests {
e.T().Run(strconv.Itoa(tt.k), func(t *testing.T) {
sut := &evaluator{k: tt.k}
result, err := sut.Process(e.artifacts)
require.NoError(t, err)
require.Len(t, result, tt.expected)
for _, v := range result {
assert.True(t, activeTime(v) >= tt.minActiveTime)
}
})
}
}
func (e *EvaluatorTestSuite) TestNew() {
tests := []struct {
name string
params rule.Parameters
expectedK int
}{
{name: "Valid", params: rule.Parameters{ParameterK: 5}, expectedK: 5},
{name: "Default If Negative", params: rule.Parameters{ParameterK: -5}, expectedK: DefaultK},
{name: "Default If Wrong Type", params: rule.Parameters{ParameterK: "5"}, expectedK: DefaultK},
{name: "Default If Wrong Key", params: rule.Parameters{"n": 5}, expectedK: DefaultK},
{name: "Default If Empty", params: rule.Parameters{}, expectedK: DefaultK},
}
for _, tt := range tests {
e.T().Run(tt.name, func(t *testing.T) {
sut := New(tt.params).(*evaluator)
require.Equal(t, tt.expectedK, sut.k)
})
}
}
func TestEvaluatorSuite(t *testing.T) {
suite.Run(t, &EvaluatorTestSuite{})
}

View File

@ -0,0 +1,94 @@
// 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 latestk
import (
"sort"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
const (
// TemplateID of latest k rule
TemplateID = "latestK"
// ParameterK ...
ParameterK = TemplateID
// DefaultK defines the default K
DefaultK = 10
)
// evaluator for evaluating latest k tags
type evaluator struct {
// latest k
k int
}
// Process the candidates based on the rule definition
func (e *evaluator) Process(artifacts []*res.Candidate) ([]*res.Candidate, error) {
// The updated proposal does not guarantee the order artifacts are provided, so we have to sort them first
sort.Slice(artifacts, func(i, j int) bool {
return artifacts[i].PushedTime < artifacts[j].PushedTime
})
i := e.k
if i > len(artifacts) {
i = len(artifacts)
}
return artifacts[:i], nil
}
// Specify what action is performed to the candidates processed by this evaluator
func (e *evaluator) Action() string {
return action.Retain
}
// New a Evaluator
func New(params rule.Parameters) rule.Evaluator {
if params != nil {
if param, ok := params[ParameterK]; ok {
if v, ok := param.(int); ok && v >= 0 {
return &evaluator{
k: v,
}
}
}
}
log.Debugf("default parameter %d used for rule %s", DefaultK, TemplateID)
return &evaluator{
k: DefaultK,
}
}
func init() {
// Register itself
rule.Register(&rule.IndexMeta{
TemplateID: TemplateID,
Action: action.Retain,
Parameters: []*rule.IndexedParam{
{
Name: ParameterK,
Type: "int",
Unit: "count",
Required: true,
},
},
}, New)
}

View File

@ -0,0 +1,71 @@
package latestk
import (
"math/rand"
"strconv"
"testing"
"github.com/stretchr/testify/suite"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/stretchr/testify/require"
)
type EvaluatorTestSuite struct {
suite.Suite
}
func (e *EvaluatorTestSuite) TestNew() {
tests := []struct {
Name string
args rule.Parameters
expectedK int
}{
{Name: "Valid", args: map[string]rule.Parameter{ParameterK: 5}, expectedK: 5},
{Name: "Default If Negative", args: map[string]rule.Parameter{ParameterK: -1}, expectedK: DefaultK},
{Name: "Default If Not Set", args: map[string]rule.Parameter{}, expectedK: DefaultK},
{Name: "Default If Wrong Type", args: map[string]rule.Parameter{ParameterK: "foo"}, expectedK: DefaultK},
}
for _, tt := range tests {
e.T().Run(tt.Name, func(t *testing.T) {
e := New(tt.args).(*evaluator)
require.Equal(t, tt.expectedK, e.k)
})
}
}
func (e *EvaluatorTestSuite) TestProcess() {
data := []*res.Candidate{{PushedTime: 0}, {PushedTime: 1}, {PushedTime: 2}, {PushedTime: 3}, {PushedTime: 4}}
rand.Shuffle(len(data), func(i, j int) {
data[i], data[j] = data[j], data[i]
})
tests := []struct {
k int
expected int
}{
{k: 0, expected: 0},
{k: 1, expected: 1},
{k: 3, expected: 3},
{k: 5, expected: 5},
{k: 6, expected: 5},
}
for _, tt := range tests {
e.T().Run(strconv.Itoa(tt.k), func(t *testing.T) {
e := New(map[string]rule.Parameter{ParameterK: tt.k})
result, err := e.Process(data)
require.NoError(t, err)
require.Len(t, result, tt.expected)
})
}
}
func TestEvaluator(t *testing.T) {
suite.Run(t, &EvaluatorTestSuite{})
}

View File

@ -0,0 +1,83 @@
// 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 latestk
import (
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
const (
// TemplateID of latest pulled k rule
TemplateID = "latestPulledK"
// ParameterK ...
ParameterK = TemplateID
// DefaultK defines the default K
DefaultK = 10
)
// evaluator for evaluating latest pulled k images
type evaluator struct {
// latest k
k int
}
// Process the candidates based on the rule definition
func (e *evaluator) Process(artifacts []*res.Candidate) ([]*res.Candidate, error) {
// TODO: REPLACE SAMPLE CODE WITH REAL IMPLEMENTATION
return artifacts, nil
}
// Specify what action is performed to the candidates processed by this evaluator
func (e *evaluator) Action() string {
return action.Retain
}
// New a Evaluator
func New(params rule.Parameters) rule.Evaluator {
if params != nil {
if param, ok := params[ParameterK]; ok {
if v, ok := param.(int); ok {
return &evaluator{
k: v,
}
}
}
}
log.Debugf("default parameter %d used for rule %s", DefaultK, TemplateID)
return &evaluator{
k: DefaultK,
}
}
func init() {
// Register itself
rule.Register(&rule.IndexMeta{
TemplateID: TemplateID,
Action: action.Retain,
Parameters: []*rule.IndexedParam{
{
Name: ParameterK,
Type: "int",
Unit: "count",
Required: true,
},
},
}, New)
}

View File

@ -0,0 +1,61 @@
// 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 rule
// Metadata of the retention rule
type Metadata struct {
// UUID of rule
ID int `json:"id"`
// Priority of rule when doing calculating
Priority int `json:"priority" valid:"Required"`
// Action of the rule performs
// "retain"
Action string `json:"action" valid:"Required"`
// Template ID
Template string `json:"template" valid:"Required"`
// The parameters of this rule
Parameters Parameters `json:"params"`
// Selector attached to the rule for filtering tags
TagSelectors []*Selector `json:"tag_selectors" valid:"Required"`
// Selector attached to the rule for filtering scope (e.g: repositories or namespaces)
ScopeSelectors map[string][]*Selector `json:"scope_selectors" valid:"Required"`
}
// Selector to narrow down the list
type Selector struct {
// Kind of the selector
// "regularExpression" or "label"
Kind string `json:"kind" valid:"Required"`
// Decorated the selector
// for "regularExpression" : "matches" and "excludes"
// for "label" : "with" and "without"
Decoration string `json:"decoration" valid:"Required"`
// Param for the selector
Pattern string `json:"pattern" valid:"Required"`
}
// Parameters of rule, indexed by the key
type Parameters map[string]Parameter
// Parameter of rule
type Parameter interface{}

View File

@ -0,0 +1,29 @@
// 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 q
// Query parameters
type Query struct {
PageNumber int64
PageSize int64
}
// TaskQuery parameters
type TaskQuery struct {
ExecutionID int64
Status string
PageNumber int64
PageSize int64
}

View File

@ -0,0 +1,68 @@
// 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 res
import (
"encoding/base64"
"fmt"
)
const (
// Image kind
Image = "image"
// Chart kind
Chart = "chart"
)
// Repository of candidate
type Repository struct {
// Namespace
Namespace string
// Repository name
Name string
// So far we need the kind of repository and retrieve candidates with different APIs
// TODO: REMOVE IT IN THE FUTURE IF WE SUPPORT UNIFIED ARTIFACT MODEL
Kind string
}
// Candidate for retention processor to match
type Candidate struct {
// Namespace(project) ID
NamespaceID int64
// Namespace
Namespace string
// Repository name
Repository string
// Kind of the candidate
// "image" or "chart"
Kind string
// Tag info
Tag string
// Pushed time in seconds
PushedTime int64
// Pulled time in seconds
PulledTime int64
// Created time in seconds
CreationTime int64
// Labels attached with the candidate
Labels []string
}
// Hash code based on the candidate info for differentiation
func (c *Candidate) Hash() string {
raw := fmt.Sprintf("%s:%s/%s:%s", c.Kind, c.Namespace, c.Repository, c.Tag)
return base64.StdEncoding.EncodeToString([]byte(raw))
}

View File

@ -0,0 +1,22 @@
// 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 res
// Result keeps the action result
type Result struct {
Target *Candidate `json:"target"`
// nil error means success
Error error `json:"error"`
}

View File

@ -0,0 +1,30 @@
// 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 res
// Selector is used to filter the inputting list
type Selector interface {
// Select the matched ones
//
// Arguments:
// artifacts []*Candidate : candidates for matching
//
// Returns:
// []*Candidate : matched candidates
Select(artifacts []*Candidate) ([]*Candidate, error)
}
// SelectorFactory is factory method to return a selector implementation
type SelectorFactory func(decoration string, pattern string) Selector

View File

@ -0,0 +1,115 @@
// 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 doublestar
import (
"github.com/bmatcuk/doublestar"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors"
)
const (
// Kind ...
Kind = "doublestar"
// Matches [pattern] for tag (default)
Matches = "matches"
// Excludes [pattern] for tag (default)
Excludes = "excludes"
// RepoMatches represents repository matches [pattern]
RepoMatches = "repoMatches"
// RepoExcludes represents repository excludes [pattern]
RepoExcludes = "repoExcludes"
// NSMatches represents namespace matches [pattern]
NSMatches = "nsMatches"
// NSExcludes represents namespace excludes [pattern]
NSExcludes = "nsExcludes"
)
// selector for regular expression
type selector struct {
// Pre defined pattern declarator
// "matches", "excludes", "repoMatches" or "repoExcludes"
decoration string
// The pattern expression
pattern string
}
// Select candidates by regular expressions
func (s *selector) Select(artifacts []*res.Candidate) (selected []*res.Candidate, err error) {
value := ""
excludes := false
for _, art := range artifacts {
switch s.decoration {
case Matches:
value = art.Tag
case Excludes:
value = art.Tag
excludes = true
case RepoMatches:
value = art.Repository
case RepoExcludes:
value = art.Repository
excludes = true
case NSMatches:
value = art.Namespace
case NSExcludes:
value = art.Namespace
excludes = true
}
if len(value) > 0 {
matched, err := match(s.pattern, value)
if err != nil {
// if error occurred, directly throw it out
return nil, err
}
if (matched && !excludes) || (!matched && excludes) {
selected = append(selected, art)
}
}
}
return selected, nil
}
// New is factory method for doublestar selector
func New(decoration string, pattern string) res.Selector {
return &selector{
decoration: decoration,
pattern: pattern,
}
}
// match returns whether the str matches the pattern
func match(pattern, str string) (bool, error) {
if len(pattern) == 0 {
return true, nil
}
return doublestar.Match(pattern, str)
}
func init() {
// Register doublestar selector
selectors.Register(Kind, []string{
Matches,
Excludes,
RepoMatches,
RepoExcludes,
NSMatches,
NSExcludes,
}, New)
}

View File

@ -0,0 +1,252 @@
// 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 doublestar
import (
"fmt"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"testing"
"time"
)
// RegExpSelectorTestSuite is a suite for testing the label selector
type RegExpSelectorTestSuite struct {
suite.Suite
artifacts []*res.Candidate
}
// TestRegExpSelector is entrance for RegExpSelectorTestSuite
func TestRegExpSelector(t *testing.T) {
suite.Run(t, new(RegExpSelectorTestSuite))
}
// SetupSuite to do preparation work
func (suite *RegExpSelectorTestSuite) SetupSuite() {
suite.artifacts = []*res.Candidate{
{
NamespaceID: 1,
Namespace: "library",
Repository: "harbor",
Tag: "latest",
Kind: res.Image,
PushedTime: time.Now().Unix() - 3600,
PulledTime: time.Now().Unix(),
CreationTime: time.Now().Unix() - 7200,
Labels: []string{"label1", "label2", "label3"},
},
{
NamespaceID: 2,
Namespace: "retention",
Repository: "redis",
Tag: "4.0",
Kind: res.Image,
PushedTime: time.Now().Unix() - 3600,
PulledTime: time.Now().Unix(),
CreationTime: time.Now().Unix() - 7200,
Labels: []string{"label1", "label4", "label5"},
},
{
NamespaceID: 2,
Namespace: "retention",
Repository: "redis",
Tag: "4.1",
Kind: res.Image,
PushedTime: time.Now().Unix() - 3600,
PulledTime: time.Now().Unix(),
CreationTime: time.Now().Unix() - 7200,
Labels: []string{"label1", "label4", "label5"},
},
}
}
// TestTagMatches tests the tag `matches` case
func (suite *RegExpSelectorTestSuite) TestTagMatches() {
tagMatches := &selector{
decoration: Matches,
pattern: "{latest,4.*}",
}
selected, err := tagMatches.Select(suite.artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 3, len(selected))
assert.Condition(suite.T(), func() bool {
return expect([]string{"harbor:latest", "redis:4.0", "redis:4.1"}, selected)
})
tagMatches2 := &selector{
decoration: Matches,
pattern: "4.*",
}
selected, err = tagMatches2.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)
})
}
// TestTagExcludes tests the tag `excludes` case
func (suite *RegExpSelectorTestSuite) TestTagExcludes() {
tagExcludes := &selector{
decoration: Excludes,
pattern: "{latest,4.*}",
}
selected, err := tagExcludes.Select(suite.artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 0, len(selected))
tagExcludes2 := &selector{
decoration: Excludes,
pattern: "4.*",
}
selected, err = tagExcludes2.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)
})
}
// TestRepoMatches tests the repository `matches` case
func (suite *RegExpSelectorTestSuite) TestRepoMatches() {
repoMatches := &selector{
decoration: RepoMatches,
pattern: "{redis}",
}
selected, err := repoMatches.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)
})
repoMatches2 := &selector{
decoration: RepoMatches,
pattern: "har*",
}
selected, err = repoMatches2.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)
})
}
// TestRepoExcludes tests the repository `excludes` case
func (suite *RegExpSelectorTestSuite) TestRepoExcludes() {
repoExcludes := &selector{
decoration: RepoExcludes,
pattern: "{redis}",
}
selected, err := repoExcludes.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)
})
repoExcludes2 := &selector{
decoration: RepoExcludes,
pattern: "har*",
}
selected, err = repoExcludes2.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)
})
}
// 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)
func expect(expected []string, candidates []*res.Candidate) bool {
hash := make(map[string]bool)
for _, art := range candidates {
hash[fmt.Sprintf("%s:%s", art.Repository, art.Tag)] = true
}
for _, exp := range expected {
if _, ok := hash[exp]; !ok {
return ok
}
}
return true
}

View File

@ -0,0 +1,90 @@
// 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 selectors
import (
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/pkg/errors"
"sync"
)
// index for keeping the mapping between selector meta and its implementation
var index sync.Map
// IndexedMeta describes the indexed selector
type IndexedMeta struct {
Kind string `json:"kind"`
Decorations []string `json:"decorations"`
}
// indexedItem defined item kept in the index
type indexedItem struct {
Meta *IndexedMeta
Factory res.SelectorFactory
}
// Register the selector with the corresponding selector kind and decoration
func Register(kind string, decorations []string, factory res.SelectorFactory) {
if len(kind) == 0 || factory == nil {
// do nothing
return
}
index.Store(kind, &indexedItem{
Meta: &IndexedMeta{
Kind: kind,
Decorations: decorations,
},
Factory: factory,
})
}
// Get selector with the provided kind and decoration
func Get(kind, decoration, pattern string) (res.Selector, error) {
if len(kind) == 0 || len(decoration) == 0 {
return nil, errors.New("empty selector kind or decoration")
}
v, ok := index.Load(kind)
if !ok {
return nil, errors.Errorf("selector %s is not registered", kind)
}
item := v.(*indexedItem)
for _, dec := range item.Meta.Decorations {
if dec == decoration {
factory := item.Factory
return factory(decoration, pattern), nil
}
}
return nil, errors.Errorf("decoration %s of selector %s is not supported", decoration, kind)
}
// Index returns all the declarative selectors
func Index() []*IndexedMeta {
all := make([]*IndexedMeta, 0)
index.Range(func(k, v interface{}) bool {
if item, ok := v.(*indexedItem); ok {
all = append(all, item.Meta)
return true
}
return false
})
return all
}

View File

@ -0,0 +1,88 @@
// 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 label
import (
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors"
"strings"
)
const (
// Kind ...
Kind = "label"
// With labels
With = "withLabels"
// Without labels
Without = "withoutLabels"
)
// selector is for label selector
type selector struct {
// Pre defined pattern decorations
// "with" or "without"
decoration string
// Label list
labels []string
}
// Select candidates by the labels
func (s *selector) Select(artifacts []*res.Candidate) (selected []*res.Candidate, err error) {
for _, art := range artifacts {
if isMatched(s.labels, art.Labels, s.decoration) {
selected = append(selected, art)
}
}
return selected, nil
}
// New is factory method for list selector
func New(decoration string, pattern string) res.Selector {
labels := strings.Split(pattern, ",")
return &selector{
decoration: decoration,
labels: labels,
}
}
// Check if the resource labels match the pattern labels
func isMatched(patternLbls []string, resLbls []string, decoration string) bool {
hash := make(map[string]bool)
for _, lbl := range resLbls {
hash[lbl] = true
}
for _, lbl := range patternLbls {
_, exists := hash[lbl]
if decoration == Without && exists {
return false
}
if decoration == With && !exists {
return false
}
}
return true
}
func init() {
// Register doublestar selector
selectors.Register(Kind, []string{With, Without}, New)
}

View File

@ -0,0 +1,148 @@
// 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 label
import (
"fmt"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"testing"
"time"
)
// LabelSelectorTestSuite is a suite for testing the label selector
type LabelSelectorTestSuite struct {
suite.Suite
artifacts []*res.Candidate
}
// TestLabelSelector is entrance for LabelSelectorTestSuite
func TestLabelSelector(t *testing.T) {
suite.Run(t, new(LabelSelectorTestSuite))
}
// SetupSuite to do preparation work
func (suite *LabelSelectorTestSuite) SetupSuite() {
suite.artifacts = []*res.Candidate{
{
NamespaceID: 1,
Namespace: "library",
Repository: "harbor",
Tag: "1.9",
Kind: res.Image,
PushedTime: time.Now().Unix() - 3600,
PulledTime: time.Now().Unix(),
CreationTime: time.Now().Unix() - 7200,
Labels: []string{"label1", "label2", "label3"},
},
{
NamespaceID: 1,
Namespace: "library",
Repository: "harbor",
Tag: "dev",
Kind: res.Image,
PushedTime: time.Now().Unix() - 3600,
PulledTime: time.Now().Unix(),
CreationTime: time.Now().Unix() - 7200,
Labels: []string{"label1", "label4", "label5"},
},
}
}
// TestWithLabelsUnMatched tests the selector of `with` labels but nothing matched
func (suite *LabelSelectorTestSuite) TestWithLabelsUnMatched() {
withNothing := &selector{
decoration: With,
labels: []string{"label6"},
}
selected, err := withNothing.Select(suite.artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 0, len(selected))
}
// TestWithLabelsMatched tests the selector of `with` labels and matched something
func (suite *LabelSelectorTestSuite) TestWithLabelsMatched() {
with1 := &selector{
decoration: With,
labels: []string{"label2"},
}
selected, err := with1.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:1.9"}, selected)
})
with2 := &selector{
decoration: With,
labels: []string{"label1"},
}
selected2, err := with2.Select(suite.artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 2, len(selected2))
assert.Condition(suite.T(), func() bool {
return expect([]string{"harbor:1.9", "harbor:dev"}, selected2)
})
}
// TestWithoutExistingLabels tests the selector of `without` existing labels
func (suite *LabelSelectorTestSuite) TestWithoutExistingLabels() {
withoutExisting := &selector{
decoration: Without,
labels: []string{"label1"},
}
selected, err := withoutExisting.Select(suite.artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 0, len(selected))
}
// TestWithoutNoneExistingLabels tests the selector of `without` non-existing labels
func (suite *LabelSelectorTestSuite) TestWithoutNoneExistingLabels() {
withoutNonExisting := &selector{
decoration: Without,
labels: []string{"label6"},
}
selected, err := withoutNonExisting.Select(suite.artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 2, len(selected))
assert.Condition(suite.T(), func() bool {
return expect([]string{"harbor:1.9", "harbor:dev"}, selected)
})
}
// Check whether the returned result matched the expected ones (only check repo:tag)
func expect(expected []string, candidates []*res.Candidate) bool {
hash := make(map[string]bool)
for _, art := range candidates {
hash[fmt.Sprintf("%s:%s", art.Repository, art.Tag)] = true
}
for _, exp := range expected {
if _, ok := hash[exp]; !ok {
return ok
}
}
return true
}

View File

@ -0,0 +1,99 @@
// 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 dao
import (
"errors"
"fmt"
"time"
"github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/pkg/scheduler/model"
)
// ScheduleDao defines the method that a schedule data access model should implement
type ScheduleDao interface {
Create(*model.Schedule) (int64, error)
Update(*model.Schedule, ...string) error
Delete(int64) error
Get(int64) (*model.Schedule, error)
List(...*model.ScheduleQuery) ([]*model.Schedule, error)
}
// New returns an instance of the default schedule data access model implementation
func New() ScheduleDao {
return &scheduleDao{}
}
type scheduleDao struct{}
func (s *scheduleDao) Create(schedule *model.Schedule) (int64, error) {
if schedule == nil {
return 0, errors.New("nil schedule")
}
now := time.Now()
schedule.CreationTime = &now
schedule.UpdateTime = &now
return dao.GetOrmer().Insert(schedule)
}
func (s *scheduleDao) Update(schedule *model.Schedule, cols ...string) error {
if schedule == nil {
return errors.New("nil schedule")
}
if schedule.ID <= 0 {
return fmt.Errorf("invalid ID: %d", schedule.ID)
}
now := time.Now()
schedule.UpdateTime = &now
_, err := dao.GetOrmer().Update(schedule, cols...)
return err
}
func (s *scheduleDao) Delete(id int64) error {
_, err := dao.GetOrmer().Delete(&model.Schedule{
ID: id,
})
return err
}
func (s *scheduleDao) Get(id int64) (*model.Schedule, error) {
schedule := &model.Schedule{
ID: id,
}
if err := dao.GetOrmer().Read(schedule); err != nil {
if err == orm.ErrNoRows {
return nil, nil
}
return nil, err
}
return schedule, nil
}
func (s *scheduleDao) List(query ...*model.ScheduleQuery) ([]*model.Schedule, error) {
qs := dao.GetOrmer().QueryTable(&model.Schedule{})
if len(query) > 0 && query[0] != nil {
if len(query[0].JobID) > 0 {
qs = qs.Filter("JobID", query[0].JobID)
}
}
schedules := []*model.Schedule{}
_, err := qs.All(&schedules)
if err != nil {
return nil, err
}
return schedules, nil
}

View File

@ -0,0 +1,122 @@
// 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 dao
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/pkg/scheduler/model"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
var schDao = &scheduleDao{}
type scheduleTestSuite struct {
suite.Suite
scheduleID int64
}
func (s *scheduleTestSuite) SetupSuite() {
dao.PrepareTestForPostgresSQL()
}
func (s *scheduleTestSuite) SetupTest() {
t := s.T()
id, err := schDao.Create(&model.Schedule{
JobID: "1",
Status: "pending",
})
require.Nil(t, err)
s.scheduleID = id
}
func (s *scheduleTestSuite) TearDownTest() {
// clear
dao.GetOrmer().Raw("delete from schedule").Exec()
}
func (s *scheduleTestSuite) TestCreate() {
t := s.T()
// nil schedule
_, err := schDao.Create(nil)
require.NotNil(t, err)
// pass
_, err = schDao.Create(&model.Schedule{
JobID: "1",
})
require.Nil(t, err)
}
func (s *scheduleTestSuite) TestUpdate() {
t := s.T()
// nil schedule
err := schDao.Update(nil)
require.NotNil(t, err)
// invalid ID
err = schDao.Update(&model.Schedule{})
require.NotNil(t, err)
// pass
err = schDao.Update(&model.Schedule{
ID: s.scheduleID,
Status: "running",
})
require.Nil(t, err)
schedule, err := schDao.Get(s.scheduleID)
require.Nil(t, err)
assert.Equal(t, "running", schedule.Status)
}
func (s *scheduleTestSuite) TestDelete() {
t := s.T()
err := schDao.Delete(s.scheduleID)
require.Nil(t, err)
schedule, err := schDao.Get(s.scheduleID)
require.Nil(t, err)
assert.Nil(t, schedule)
}
func (s *scheduleTestSuite) TestGet() {
t := s.T()
schedule, err := schDao.Get(s.scheduleID)
require.Nil(t, err)
assert.Equal(t, "pending", schedule.Status)
}
func (s *scheduleTestSuite) TestList() {
t := s.T()
// nil query
schedules, err := schDao.List()
require.Nil(t, err)
require.Equal(t, 1, len(schedules))
assert.Equal(t, s.scheduleID, schedules[0].ID)
// query by job ID
schedules, err = schDao.List(&model.ScheduleQuery{
JobID: "1",
})
require.Nil(t, err)
require.Equal(t, 1, len(schedules))
assert.Equal(t, s.scheduleID, schedules[0].ID)
}
func TestScheduleDao(t *testing.T) {
suite.Run(t, &scheduleTestSuite{})
}

View File

@ -0,0 +1,59 @@
// 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 hook
import (
"time"
"github.com/goharbor/harbor/src/pkg/scheduler"
"github.com/goharbor/harbor/src/pkg/scheduler/model"
)
// GlobalController is an instance of the default controller that can be used globally
var GlobalController = NewController()
// Controller updates the scheduler job status or runs the callback function
type Controller interface {
UpdateStatus(scheduleID int64, status string) error
Run(callbackFuncName string, params interface{}) error
}
// NewController returns an instance of the default controller
func NewController() Controller {
return &controller{
manager: scheduler.GlobalManager,
}
}
type controller struct {
manager scheduler.Manager
}
func (c *controller) UpdateStatus(scheduleID int64, status string) error {
now := time.Now()
return c.manager.Update(&model.Schedule{
ID: scheduleID,
Status: status,
UpdateTime: &now,
}, "Status", "UpdateTime")
}
func (c *controller) Run(callbackFuncName string, params interface{}) error {
f, err := scheduler.GetCallbackFunc(callbackFuncName)
if err != nil {
return err
}
return f(params)
}

View File

@ -0,0 +1,56 @@
// 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 hook
import (
"testing"
"github.com/goharbor/harbor/src/pkg/scheduler"
"github.com/goharbor/harbor/src/pkg/scheduler/model"
htesting "github.com/goharbor/harbor/src/testing"
"github.com/stretchr/testify/require"
)
var h = &controller{
manager: &htesting.FakeSchedulerManager{},
}
func TestUpdateStatus(t *testing.T) {
// task not exist
err := h.UpdateStatus(1, "running")
require.NotNil(t, err)
// pass
h.manager.(*htesting.FakeSchedulerManager).Schedules = []*model.Schedule{
{
ID: 1,
Status: "",
},
}
err = h.UpdateStatus(1, "running")
require.Nil(t, err)
}
func TestRun(t *testing.T) {
// callback function not exist
err := h.Run("not-exist", nil)
require.NotNil(t, err)
// pass
err = scheduler.Register("callback", func(interface{}) error { return nil })
require.Nil(t, err)
err = h.Run("callback", nil)
require.Nil(t, err)
}

View File

@ -0,0 +1,66 @@
// 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 scheduler
import (
"github.com/goharbor/harbor/src/pkg/scheduler/dao"
"github.com/goharbor/harbor/src/pkg/scheduler/model"
)
var (
// GlobalManager is an instance of the default manager that
// can be used globally
GlobalManager = NewManager()
)
// Manager manages the schedule of the scheduler
type Manager interface {
Create(*model.Schedule) (int64, error)
Update(*model.Schedule, ...string) error
Delete(int64) error
Get(int64) (*model.Schedule, error)
List(...*model.ScheduleQuery) ([]*model.Schedule, error)
}
// NewManager returns an instance of the default manager
func NewManager() Manager {
return &manager{
scheduleDao: dao.New(),
}
}
type manager struct {
scheduleDao dao.ScheduleDao
}
func (m *manager) Create(schedule *model.Schedule) (int64, error) {
return m.scheduleDao.Create(schedule)
}
func (m *manager) Update(schedule *model.Schedule, props ...string) error {
return m.scheduleDao.Update(schedule, props...)
}
func (m *manager) Delete(id int64) error {
return m.scheduleDao.Delete(id)
}
func (m *manager) List(query ...*model.ScheduleQuery) ([]*model.Schedule, error) {
return m.scheduleDao.List(query...)
}
func (m *manager) Get(id int64) (*model.Schedule, error) {
return m.scheduleDao.Get(id)
}

View File

@ -0,0 +1,110 @@
// 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 scheduler
import (
"testing"
"github.com/goharbor/harbor/src/pkg/scheduler/model"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)
var mgr *manager
type fakeScheduleDao struct {
schedules []*model.Schedule
mock.Mock
}
func (f *fakeScheduleDao) Create(*model.Schedule) (int64, error) {
f.Called()
return 1, nil
}
func (f *fakeScheduleDao) Update(*model.Schedule, ...string) error {
f.Called()
return nil
}
func (f *fakeScheduleDao) Delete(int64) error {
f.Called()
return nil
}
func (f *fakeScheduleDao) Get(int64) (*model.Schedule, error) {
f.Called()
return nil, nil
}
func (f *fakeScheduleDao) List(query ...*model.ScheduleQuery) ([]*model.Schedule, error) {
f.Called()
if len(query) == 0 || query[0] == nil {
return f.schedules, nil
}
result := []*model.Schedule{}
for _, sch := range f.schedules {
if sch.JobID == query[0].JobID {
result = append(result, sch)
}
}
return result, nil
}
type managerTestSuite struct {
suite.Suite
}
func (m *managerTestSuite) SetupTest() {
// recreate schedule manager
mgr = &manager{
scheduleDao: &fakeScheduleDao{},
}
}
func (m *managerTestSuite) TestCreate() {
t := m.T()
mgr.scheduleDao.(*fakeScheduleDao).On("Create", mock.Anything)
mgr.Create(nil)
mgr.scheduleDao.(*fakeScheduleDao).AssertCalled(t, "Create")
}
func (m *managerTestSuite) TestUpdate() {
t := m.T()
mgr.scheduleDao.(*fakeScheduleDao).On("Update", mock.Anything)
mgr.Update(nil)
mgr.scheduleDao.(*fakeScheduleDao).AssertCalled(t, "Update")
}
func (m *managerTestSuite) TestDelete() {
t := m.T()
mgr.scheduleDao.(*fakeScheduleDao).On("Delete", mock.Anything)
mgr.Delete(1)
mgr.scheduleDao.(*fakeScheduleDao).AssertCalled(t, "Delete")
}
func (m *managerTestSuite) TestGet() {
t := m.T()
mgr.scheduleDao.(*fakeScheduleDao).On("Get", mock.Anything)
mgr.Get(1)
mgr.scheduleDao.(*fakeScheduleDao).AssertCalled(t, "Get")
}
func (m *managerTestSuite) TestList() {
t := m.T()
mgr.scheduleDao.(*fakeScheduleDao).On("List", mock.Anything)
mgr.List(nil)
mgr.scheduleDao.(*fakeScheduleDao).AssertCalled(t, "List")
}
func TestManager(t *testing.T) {
suite.Run(t, &managerTestSuite{})
}

View File

@ -0,0 +1,40 @@
// 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 (
"time"
"github.com/astaxie/beego/orm"
)
func init() {
orm.RegisterModel(
new(Schedule))
}
// Schedule is a record for a scheduler job
type Schedule struct {
ID int64 `orm:"pk;auto;column(id)" json:"id"`
JobID string `orm:"column(job_id)" json:"job_id"`
Status string `orm:"column(status)" json:"status"`
CreationTime *time.Time `orm:"column(creation_time)" json:"creation_time"`
UpdateTime *time.Time `orm:"column(update_time)" json:"update_time"`
}
// ScheduleQuery is query for schedule
type ScheduleQuery struct {
JobID string
}

View File

@ -0,0 +1,54 @@
// 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 scheduler
import (
"encoding/json"
"github.com/goharbor/harbor/src/jobservice/job"
)
// const definitions
const (
// the job name that used to register to Jobservice
JobNameScheduler = "SCHEDULER"
)
// PeriodicJob is designed to generate hook event periodically
type PeriodicJob struct{}
// MaxFails of the job
func (pj *PeriodicJob) MaxFails() uint {
return 3
}
// ShouldRetry indicates job can be retried if failed
func (pj *PeriodicJob) ShouldRetry() bool {
return true
}
// Validate the parameters
func (pj *PeriodicJob) Validate(params job.Parameters) error {
return nil
}
// Run the job
func (pj *PeriodicJob) Run(ctx job.Context, params job.Parameters) error {
data, err := json.Marshal(params)
if err != nil {
return err
}
return ctx.Checkin(string(data))
}

View File

@ -0,0 +1,192 @@
// 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 scheduler
import (
"fmt"
"net/http"
"sync"
"time"
chttp "github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/common/job"
"github.com/goharbor/harbor/src/common/job/models"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/pkg/scheduler/model"
"github.com/pkg/errors"
)
const (
jobParamCallbackFunc = "callback_func"
jobParamCallbackFuncParams = "params"
)
var (
// GlobalScheduler is an instance of the default scheduler that
// can be used globally. Call Init() to initialize it first
GlobalScheduler Scheduler
registry = make(map[string]CallbackFunc)
)
// CallbackFunc defines the function that the scheduler calls when triggered
type CallbackFunc func(interface{}) error
// Scheduler provides the capability to run a periodic task, a callback function
// needs to be registered before using the scheduler
type Scheduler interface {
Schedule(cron string, callbackFuncName string, params interface{}) (int64, error)
UnSchedule(id int64) error
}
// Register the callback function with name, and the function will be called
// by the scheduler when the scheduler is triggered
func Register(name string, callbackFunc CallbackFunc) error {
if len(name) == 0 {
return errors.New("empty name")
}
if callbackFunc == nil {
return errors.New("callback function is nil")
}
_, exist := registry[name]
if exist {
return fmt.Errorf("callback function %s already exists", name)
}
registry[name] = callbackFunc
return nil
}
// GetCallbackFunc returns the registered callback function specified by the name
func GetCallbackFunc(name string) (CallbackFunc, error) {
f, exist := registry[name]
if !exist {
return nil, fmt.Errorf("callback function %s not found", name)
}
return f, nil
}
func callbackFuncExist(name string) bool {
_, exist := registry[name]
return exist
}
// Init the GlobalScheduler
func Init() {
GlobalScheduler = New(config.InternalCoreURL())
}
// New returns an instance of the default scheduler
func New(internalCoreURL string) Scheduler {
return &scheduler{
internalCoreURL: internalCoreURL,
jobserviceClient: job.GlobalClient,
manager: GlobalManager,
}
}
type scheduler struct {
sync.RWMutex
internalCoreURL string
manager Manager
jobserviceClient job.Client
}
func (s *scheduler) Schedule(cron string, callbackFuncName string, params interface{}) (int64, error) {
if !callbackFuncExist(callbackFuncName) {
return 0, fmt.Errorf("callback function %s not found", callbackFuncName)
}
// create schedule record
now := time.Now()
scheduleID, err := s.manager.Create(&model.Schedule{
CreationTime: &now,
UpdateTime: &now,
})
if err != nil {
return 0, err
}
log.Debugf("the schedule record %d created", scheduleID)
// submit scheduler job to Jobservice
statusHookURL := fmt.Sprintf("%s/service/notifications/schedules/%d", s.internalCoreURL, scheduleID)
jd := &models.JobData{
Name: JobNameScheduler,
Parameters: map[string]interface{}{
jobParamCallbackFunc: callbackFuncName,
jobParamCallbackFuncParams: params,
},
Metadata: &models.JobMetadata{
JobKind: job.JobKindPeriodic,
Cron: cron,
},
StatusHook: statusHookURL,
}
jobID, err := s.jobserviceClient.SubmitJob(jd)
if err != nil {
// if failed to submit to Jobservice, delete the schedule record in database
e := s.manager.Delete(scheduleID)
if e != nil {
log.Errorf("failed to delete the schedule %d: %v", scheduleID, e)
}
return 0, err
}
log.Debugf("the scheduler job submitted to Jobservice, job ID: %s", jobID)
// populate the job ID for the schedule
err = s.manager.Update(&model.Schedule{
ID: scheduleID,
JobID: jobID,
}, "JobID")
if err != nil {
// stop the scheduler job
if e := s.jobserviceClient.PostAction(jobID, job.JobActionStop); e != nil {
log.Errorf("failed to stop the scheduler job %s: %v", jobID, e)
}
// delete the schedule record
if e := s.manager.Delete(scheduleID); e != nil {
log.Errorf("failed to delete the schedule record %d: %v", scheduleID, e)
}
return 0, err
}
return scheduleID, nil
}
func (s *scheduler) UnSchedule(id int64) error {
schedule, err := s.manager.Get(id)
if err != nil {
return err
}
if schedule == nil {
return fmt.Errorf("the schedule record %d not found", id)
}
if err = s.jobserviceClient.PostAction(schedule.JobID, job.JobActionStop); err != nil {
herr, ok := err.(*chttp.Error)
// if the job specified by jobID is not found in Jobservice, just delete
// the schedule record
if !ok || herr.Code != http.StatusNotFound {
return err
}
}
log.Debugf("the stop action for job %s submitted to the Jobservice", schedule.JobID)
if err = s.manager.Delete(schedule.ID); err != nil {
return err
}
log.Debugf("the schedule record %d deleted", schedule.ID)
return nil
}

View File

@ -0,0 +1,115 @@
// 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 scheduler
import (
"testing"
htesting "github.com/goharbor/harbor/src/testing"
"github.com/goharbor/harbor/src/testing/job"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
var sch *scheduler
type schedulerTestSuite struct {
suite.Suite
}
func (s *schedulerTestSuite) SetupTest() {
t := s.T()
// empty callback function registry before running every test case
// and register a new callback function named "callback"
registry = make(map[string]CallbackFunc)
err := Register("callback", func(interface{}) error { return nil })
require.Nil(t, err)
// recreate the scheduler object
sch = &scheduler{
jobserviceClient: &job.MockJobClient{},
manager: &htesting.FakeSchedulerManager{},
}
}
func (s *schedulerTestSuite) TestRegister() {
t := s.T()
var name string
var callbackFun CallbackFunc
// empty name
err := Register(name, callbackFun)
require.NotNil(t, err)
// nil callback function
name = "test"
err = Register(name, callbackFun)
require.NotNil(t, err)
// pass
callbackFun = func(interface{}) error { return nil }
err = Register(name, callbackFun)
require.Nil(t, err)
// duplicate name
err = Register(name, callbackFun)
require.NotNil(t, err)
}
func (s *schedulerTestSuite) TestGetCallbackFunc() {
t := s.T()
// not exist
_, err := GetCallbackFunc("not-exist")
require.NotNil(t, err)
// pass
f, err := GetCallbackFunc("callback")
require.Nil(t, err)
assert.NotNil(t, f)
}
func (s *schedulerTestSuite) TestSchedule() {
t := s.T()
// callback function not exist
_, err := sch.Schedule("0 * * * * *", "not-exist", nil)
require.NotNil(t, err)
// pass
id, err := sch.Schedule("0 * * * * *", "callback", nil)
require.Nil(t, err)
assert.Equal(t, int64(1), id)
}
func (s *schedulerTestSuite) TestUnSchedule() {
t := s.T()
// schedule not exist
err := sch.UnSchedule(1)
require.NotNil(t, err)
// schedule exist
id, err := sch.Schedule("0 * * * * *", "callback", nil)
require.Nil(t, err)
assert.Equal(t, int64(1), id)
err = sch.UnSchedule(id)
require.Nil(t, err)
}
func TestScheduler(t *testing.T) {
s := &schedulerTestSuite{}
suite.Run(t, s)
}

View File

@ -1,8 +1,6 @@
package huawei
import (
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
@ -10,7 +8,10 @@ import (
"regexp"
"strings"
common_http "github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/common/http/modifier"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/common/utils/registry/auth"
adp "github.com/goharbor/harbor/src/replication/adapter"
"github.com/goharbor/harbor/src/replication/adapter/native"
"github.com/goharbor/harbor/src/replication/model"
@ -30,6 +31,7 @@ func init() {
type adapter struct {
*native.Adapter
registry *model.Registry
client *common_http.Client
}
// Info gets info about Huawei SWR
@ -56,18 +58,8 @@ func (a *adapter) ListNamespaces(query *model.NamespaceQuery) ([]*model.Namespac
}
r.Header.Add("content-type", "application/json; charset=utf-8")
encodeAuth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", a.registry.Credential.AccessKey, a.registry.Credential.AccessSecret)))
r.Header.Add("Authorization", "Basic "+encodeAuth)
client := &http.Client{}
if a.registry.Insecure == true {
client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
}
resp, err := client.Do(r)
resp, err := a.client.Do(r)
if err != nil {
return namespaces, err
}
@ -120,8 +112,11 @@ func (a *adapter) ConvertResourceMetadata(resourceMetadata *model.ResourceMetada
func (a *adapter) PrepareForPush(resources []*model.Resource) error {
namespaces := map[string]struct{}{}
for _, resource := range resources {
var namespace string
paths := strings.Split(resource.Metadata.Repository.Name, "/")
namespace := paths[0]
if len(paths) > 0 {
namespace = paths[0]
}
ns, err := a.GetNamespace(namespace)
if err != nil {
return err
@ -133,9 +128,7 @@ func (a *adapter) PrepareForPush(resources []*model.Resource) error {
}
url := fmt.Sprintf("%s/dockyard/v2/namespaces", a.registry.URL)
client := &http.Client{
Transport: util.GetHTTPTransport(a.registry.Insecure),
}
for namespace := range namespaces {
namespacebyte, err := json.Marshal(struct {
Namespace string `json:"namespace"`
@ -152,10 +145,8 @@ func (a *adapter) PrepareForPush(resources []*model.Resource) error {
}
r.Header.Add("content-type", "application/json; charset=utf-8")
encodeAuth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", a.registry.Credential.AccessKey, a.registry.Credential.AccessSecret)))
r.Header.Add("Authorization", "Basic "+encodeAuth)
resp, err := client.Do(r)
resp, err := a.client.Do(r)
if err != nil {
return err
}
@ -185,20 +176,8 @@ func (a *adapter) GetNamespace(namespaceStr string) (*model.Namespace, error) {
}
r.Header.Add("content-type", "application/json; charset=utf-8")
encodeAuth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", a.registry.Credential.AccessKey, a.registry.Credential.AccessSecret)))
r.Header.Add("Authorization", "Basic "+encodeAuth)
var client *http.Client
if a.registry.Insecure == true {
client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
} else {
client = &http.Client{}
}
resp, err := client.Do(r)
resp, err := a.client.Do(r)
if err != nil {
return namespace, err
}
@ -237,9 +216,30 @@ func AdapterFactory(registry *model.Registry) (adp.Adapter, error) {
if err != nil {
return nil, err
}
var (
modifiers = []modifier.Modifier{
&auth.UserAgentModifier{
UserAgent: adp.UserAgentReplication,
}}
authorizer modifier.Modifier
)
if registry.Credential != nil {
authorizer = auth.NewBasicAuthCredential(
registry.Credential.AccessKey,
registry.Credential.AccessSecret)
modifiers = append(modifiers, authorizer)
}
return &adapter{
registry: registry,
Adapter: dockerRegistryAdapter,
registry: registry,
client: common_http.NewClient(
&http.Client{
Transport: util.GetHTTPTransport(registry.Insecure),
},
modifiers...,
),
}, nil
}

View File

@ -1,8 +1,6 @@
package huawei
import (
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
@ -25,18 +23,8 @@ func (a *adapter) FetchImages(filters []*model.Filter) ([]*model.Resource, error
}
r.Header.Add("content-type", "application/json; charset=utf-8")
encodeAuth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", a.registry.Credential.AccessKey, a.registry.Credential.AccessSecret)))
r.Header.Add("Authorization", "Basic "+encodeAuth)
client := &http.Client{}
if a.registry.Insecure == true {
client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
}
resp, err := client.Do(r)
resp, err := a.client.Do(r)
if err != nil {
return resources, err
}
@ -82,15 +70,7 @@ func (a *adapter) ManifestExist(repository, reference string) (exist bool, diges
r.Header.Add("content-type", "application/json; charset=utf-8")
r.Header.Add("Authorization", "Bearer "+token.Token)
client := &http.Client{}
if a.registry.Insecure == true {
client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
}
resp, err := client.Do(r)
resp, err := a.client.Do(r)
if err != nil {
return exist, digest, err
}
@ -133,15 +113,7 @@ func (a *adapter) DeleteManifest(repository, reference string) error {
r.Header.Add("content-type", "application/json; charset=utf-8")
r.Header.Add("Authorization", "Bearer "+token.Token)
client := &http.Client{}
if a.registry.Insecure == true {
client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
}
resp, err := client.Do(r)
resp, err := a.client.Do(r)
if err != nil {
return err
}
@ -220,18 +192,8 @@ func getJwtToken(a *adapter, repository string) (token jwtToken, err error) {
}
r.Header.Add("content-type", "application/json; charset=utf-8")
encodeAuth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", a.registry.Credential.AccessKey, a.registry.Credential.AccessSecret)))
r.Header.Add("Authorization", "Basic "+encodeAuth)
client := &http.Client{}
if a.registry.Insecure == true {
client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
}
resp, err := client.Do(r)
resp, err := a.client.Do(r)
if err != nil {
return token, err
}

View File

@ -1,6 +1,7 @@
package huawei
import (
"os"
"strings"
"testing"
@ -20,7 +21,11 @@ func init() {
Insecure: false,
Status: "",
}
HWAdapter.registry = hwRegistry
adp, err := AdapterFactory(hwRegistry)
if err != nil {
os.Exit(1)
}
HWAdapter = *adp.(*adapter)
}
func TestAdapter_FetchImages(t *testing.T) {

View File

@ -0,0 +1,44 @@
// 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 clients
import (
"github.com/goharbor/harbor/src/chartserver"
"github.com/goharbor/harbor/src/common/models"
)
// DumbCoreClient provides an empty implement for pkg/clients/core.Client
// it is only used for testing
type DumbCoreClient struct{}
// ListAllImages ...
func (d *DumbCoreClient) ListAllImages(project, repository string) ([]*models.TagResp, error) {
return nil, nil
}
// DeleteImage ...
func (d *DumbCoreClient) DeleteImage(project, repository, tag string) error {
return nil
}
// ListAllCharts ...
func (d *DumbCoreClient) ListAllCharts(project, repository string) ([]*chartserver.ChartVersion, error) {
return nil, nil
}
// DeleteChart ...
func (d *DumbCoreClient) DeleteChart(project, repository, version string) error {
return nil
}

View File

@ -5,8 +5,8 @@ import (
"math/rand"
"github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/common/job"
"github.com/goharbor/harbor/src/common/job/models"
"github.com/goharbor/harbor/src/jobservice/job"
)
// MockJobClient ...
@ -27,12 +27,9 @@ func (mjc *MockJobClient) GetJobLog(uuid string) ([]byte, error) {
// SubmitJob ...
func (mjc *MockJobClient) SubmitJob(data *models.JobData) (string, error) {
if data.Name == job.ImageScanAllJob || data.Name == job.Replication || data.Name == job.ImageGC || data.Name == job.ImageScanJob {
uuid := fmt.Sprintf("u-%d", rand.Int())
mjc.JobUUID = append(mjc.JobUUID, uuid)
return uuid, nil
}
return "", fmt.Errorf("unsupported job %s", data.Name)
uuid := fmt.Sprintf("u-%d", rand.Int())
mjc.JobUUID = append(mjc.JobUUID, uuid)
return uuid, nil
}
// PostAction ...
@ -46,6 +43,11 @@ func (mjc *MockJobClient) PostAction(uuid, action string) error {
return nil
}
// GetExecutions ...
func (mjc *MockJobClient) GetExecutions(uuid string) ([]job.Stats, error) {
return nil, nil
}
func (mjc *MockJobClient) validUUID(uuid string) bool {
for _, u := range mjc.JobUUID {
if uuid == u {

77
src/testing/scheduler.go Normal file
View File

@ -0,0 +1,77 @@
// 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 testing
import (
"fmt"
"github.com/goharbor/harbor/src/pkg/scheduler/model"
)
// FakeSchedulerManager ...
type FakeSchedulerManager struct {
idCounter int64
Schedules []*model.Schedule
}
// Create ...
func (f *FakeSchedulerManager) Create(schedule *model.Schedule) (int64, error) {
f.idCounter++
id := f.idCounter
schedule.ID = id
f.Schedules = append(f.Schedules, schedule)
return id, nil
}
// Update ...
func (f *FakeSchedulerManager) Update(schedule *model.Schedule, props ...string) error {
for i, sch := range f.Schedules {
if sch.ID == schedule.ID {
f.Schedules[i] = schedule
return nil
}
}
return fmt.Errorf("the execution %d not found", schedule.ID)
}
// Delete ...
func (f *FakeSchedulerManager) Delete(id int64) error {
length := len(f.Schedules)
for i, sch := range f.Schedules {
if sch.ID == id {
f.Schedules = f.Schedules[:i]
if i != length-1 {
f.Schedules = append(f.Schedules, f.Schedules[i+1:]...)
}
return nil
}
}
return fmt.Errorf("the execution %d not found", id)
}
// Get ...
func (f *FakeSchedulerManager) Get(id int64) (*model.Schedule, error) {
for _, sch := range f.Schedules {
if sch.ID == id {
return sch, nil
}
}
return nil, fmt.Errorf("the execution %d not found", id)
}
// List ...
func (f *FakeSchedulerManager) List(...*model.ScheduleQuery) ([]*model.Schedule, error) {
return f.Schedules, nil
}

8
src/vendor/github.com/mattn/go-runewidth/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,8 @@
language: go
go:
- tip
before_install:
- go get github.com/mattn/goveralls
- go get golang.org/x/tools/cmd/cover
script:
- $HOME/gopath/bin/goveralls -repotoken lAKAWPzcGsD3A8yBX3BGGtRUdJ6CaGERL

21
src/vendor/github.com/mattn/go-runewidth/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Yasuhiro Matsumoto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Some files were not shown because too many files have changed in this diff Show More