mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-27 04:35:16 +01:00
Merge pull request #8374 from goharbor/feature/tag_retention
[WIP]Feature/tag retention
This commit is contained in:
commit
5bfa7a6515
@ -8,3 +8,50 @@ CREATE TABLE cve_whitelist (
|
||||
items text NOT NULL,
|
||||
UNIQUE (project_id)
|
||||
);
|
||||
|
||||
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)
|
||||
);
|
||||
|
@ -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
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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},
|
||||
|
@ -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},
|
||||
|
@ -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")
|
||||
|
@ -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, "")
|
||||
|
||||
}
|
||||
|
25
src/common/utils/notary/model/model.go
Normal file
25
src/common/utils/notary/model/model.go
Normal 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.
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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
392
src/core/api/retention.go
Normal 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
|
||||
}
|
@ -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/proxy"
|
||||
"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")
|
||||
proxy.Init()
|
||||
// go proxy.StartProxy()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
70
src/core/service/notifications/scheduler/handler.go
Normal file
70
src/core/service/notifications/scheduler/handler.go
Normal 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), ¶ms); 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
|
||||
}
|
||||
}
|
@ -49,7 +49,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/pkg/errors v0.8.1
|
||||
|
@ -186,6 +186,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=
|
||||
@ -200,6 +202,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=
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
35
src/pkg/clients/core/chart.go
Normal file
35
src/pkg/clients/core/chart.go
Normal 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)
|
||||
}
|
63
src/pkg/clients/core/client.go
Normal file
63
src/pkg/clients/core/client.go
Normal 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)
|
||||
}
|
35
src/pkg/clients/core/image.go
Normal file
35
src/pkg/clients/core/image.go
Normal 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)
|
||||
}
|
61
src/pkg/project/manager.go
Normal file
61
src/pkg/project/manager.go
Normal 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)
|
||||
}
|
61
src/pkg/repository/manager.go
Normal file
61
src/pkg/repository/manager.go
Normal 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
23
src/pkg/retention/boot.go
Normal 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
|
||||
}
|
272
src/pkg/retention/controller.go
Normal file
272
src/pkg/retention/controller.go
Normal 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,
|
||||
}
|
||||
}
|
65
src/pkg/retention/dao/models/retention.go
Normal file
65
src/pkg/retention/dao/models/retention.go
Normal 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
|
||||
}
|
181
src/pkg/retention/dao/retention.go
Normal file
181
src/pkg/retention/dao/retention.go
Normal 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
|
||||
}
|
210
src/pkg/retention/dao/retention_test.go
Normal file
210
src/pkg/retention/dao/retention_test.go
Normal 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)
|
||||
}
|
153
src/pkg/retention/dep/client.go
Normal file
153
src/pkg/retention/dep/client.go
Normal 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)
|
||||
}
|
||||
}
|
134
src/pkg/retention/dep/client_test.go
Normal file
134
src/pkg/retention/dep/client_test.go
Normal 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
233
src/pkg/retention/job.go
Normal 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
|
||||
}
|
235
src/pkg/retention/job_test.go
Normal file
235
src/pkg/retention/job_test.go
Normal 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
|
||||
}
|
260
src/pkg/retention/launcher.go
Normal file
260
src/pkg/retention/launcher.go
Normal 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
|
||||
}
|
236
src/pkg/retention/launcher_test.go
Normal file
236
src/pkg/retention/launcher_test.go
Normal 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))
|
||||
}
|
230
src/pkg/retention/manager.go
Normal file
230
src/pkg/retention/manager.go
Normal 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{}
|
||||
}
|
203
src/pkg/retention/manager_test.go
Normal file
203
src/pkg/retention/manager_test.go
Normal 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)
|
||||
}
|
70
src/pkg/retention/models.go
Normal file
70
src/pkg/retention/models.go
Normal 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"`
|
||||
}
|
52
src/pkg/retention/policy/action/index.go
Normal file
52
src/pkg/retention/policy/action/index.go
Normal 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
|
||||
}
|
90
src/pkg/retention/policy/action/index_test.go
Normal file
90
src/pkg/retention/policy/action/index_test.go
Normal 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{}
|
||||
}
|
93
src/pkg/retention/policy/action/performer.go
Normal file
93
src/pkg/retention/policy/action/performer.go
Normal file
@ -0,0 +1,93 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package 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)
|
||||
}
|
112
src/pkg/retention/policy/action/performer_test.go
Normal file
112
src/pkg/retention/policy/action/performer_test.go
Normal 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")
|
||||
}
|
49
src/pkg/retention/policy/alg/index.go
Normal file
49
src/pkg/retention/policy/alg/index.go
Normal 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)
|
||||
}
|
221
src/pkg/retention/policy/alg/or/processor.go
Normal file
221
src/pkg/retention/policy/alg/or/processor.go
Normal 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[¶m.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
|
||||
}
|
145
src/pkg/retention/policy/alg/or/processor_test.go
Normal file
145
src/pkg/retention/policy/alg/or/processor_test.go
Normal 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")
|
||||
}
|
52
src/pkg/retention/policy/alg/processor.go
Normal file
52
src/pkg/retention/policy/alg/processor.go
Normal 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
|
95
src/pkg/retention/policy/builder.go
Normal file
95
src/pkg/retention/policy/builder.go
Normal 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
|
||||
}
|
185
src/pkg/retention/policy/builder_test.go
Normal file
185
src/pkg/retention/policy/builder_test.go
Normal 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")
|
||||
}
|
29
src/pkg/retention/policy/lwp/models.go
Normal file
29
src/pkg/retention/policy/lwp/models.go
Normal 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"`
|
||||
}
|
83
src/pkg/retention/policy/models.go
Normal file
83
src/pkg/retention/policy/models.go
Normal 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"`
|
||||
}
|
50
src/pkg/retention/policy/rule/always/evaluator.go
Normal file
50
src/pkg/retention/policy/rule/always/evaluator.go
Normal 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)
|
||||
}
|
49
src/pkg/retention/policy/rule/always/evaluator_test.go
Normal file
49
src/pkg/retention/policy/rule/always/evaluator_test.go
Normal 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{})
|
||||
}
|
36
src/pkg/retention/policy/rule/evaluator.go
Normal file
36
src/pkg/retention/policy/rule/evaluator.go
Normal 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
|
116
src/pkg/retention/policy/rule/index.go
Normal file
116
src/pkg/retention/policy/rule/index.go
Normal 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
|
||||
}
|
117
src/pkg/retention/policy/rule/index_test.go
Normal file
117
src/pkg/retention/policy/rule/index_test.go
Normal 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}
|
||||
}
|
71
src/pkg/retention/policy/rule/lastpulled/evaluator.go
Normal file
71
src/pkg/retention/policy/rule/lastpulled/evaluator.go
Normal 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}
|
||||
}
|
89
src/pkg/retention/policy/rule/lastpulled/evaluator_test.go
Normal file
89
src/pkg/retention/policy/rule/lastpulled/evaluator_test.go
Normal 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{})
|
||||
}
|
91
src/pkg/retention/policy/rule/lastx/evaluator.go
Normal file
91
src/pkg/retention/policy/rule/lastx/evaluator.go
Normal 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)
|
||||
}
|
78
src/pkg/retention/policy/rule/lastx/evaluator_test.go
Normal file
78
src/pkg/retention/policy/rule/lastx/evaluator_test.go
Normal 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{})
|
||||
}
|
105
src/pkg/retention/policy/rule/latest/evaluator.go
Normal file
105
src/pkg/retention/policy/rule/latest/evaluator.go
Normal 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)
|
||||
}
|
99
src/pkg/retention/policy/rule/latest/evaluator_test.go
Normal file
99
src/pkg/retention/policy/rule/latest/evaluator_test.go
Normal 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{})
|
||||
}
|
94
src/pkg/retention/policy/rule/latestk/evaluator.go
Normal file
94
src/pkg/retention/policy/rule/latestk/evaluator.go
Normal 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)
|
||||
}
|
71
src/pkg/retention/policy/rule/latestk/evaluator_test.go
Normal file
71
src/pkg/retention/policy/rule/latestk/evaluator_test.go
Normal 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{})
|
||||
}
|
83
src/pkg/retention/policy/rule/latestpull/evaluator.go
Normal file
83
src/pkg/retention/policy/rule/latestpull/evaluator.go
Normal 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)
|
||||
}
|
61
src/pkg/retention/policy/rule/models.go
Normal file
61
src/pkg/retention/policy/rule/models.go
Normal 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{}
|
29
src/pkg/retention/q/query.go
Normal file
29
src/pkg/retention/q/query.go
Normal 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
|
||||
}
|
68
src/pkg/retention/res/candidate.go
Normal file
68
src/pkg/retention/res/candidate.go
Normal 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))
|
||||
}
|
22
src/pkg/retention/res/result.go
Normal file
22
src/pkg/retention/res/result.go
Normal 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"`
|
||||
}
|
30
src/pkg/retention/res/selector.go
Normal file
30
src/pkg/retention/res/selector.go
Normal 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
|
115
src/pkg/retention/res/selectors/doublestar/selector.go
Normal file
115
src/pkg/retention/res/selectors/doublestar/selector.go
Normal 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)
|
||||
}
|
252
src/pkg/retention/res/selectors/doublestar/selector_test.go
Normal file
252
src/pkg/retention/res/selectors/doublestar/selector_test.go
Normal 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
|
||||
}
|
90
src/pkg/retention/res/selectors/index.go
Normal file
90
src/pkg/retention/res/selectors/index.go
Normal 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
|
||||
}
|
88
src/pkg/retention/res/selectors/label/selector.go
Normal file
88
src/pkg/retention/res/selectors/label/selector.go
Normal 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)
|
||||
}
|
148
src/pkg/retention/res/selectors/label/selector_test.go
Normal file
148
src/pkg/retention/res/selectors/label/selector_test.go
Normal 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
|
||||
}
|
99
src/pkg/scheduler/dao/schedule.go
Normal file
99
src/pkg/scheduler/dao/schedule.go
Normal 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
|
||||
}
|
122
src/pkg/scheduler/dao/schedule_test.go
Normal file
122
src/pkg/scheduler/dao/schedule_test.go
Normal 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{})
|
||||
}
|
59
src/pkg/scheduler/hook/handler.go
Normal file
59
src/pkg/scheduler/hook/handler.go
Normal 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)
|
||||
}
|
56
src/pkg/scheduler/hook/handler_test.go
Normal file
56
src/pkg/scheduler/hook/handler_test.go
Normal 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)
|
||||
}
|
66
src/pkg/scheduler/manager.go
Normal file
66
src/pkg/scheduler/manager.go
Normal 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)
|
||||
}
|
110
src/pkg/scheduler/manager_test.go
Normal file
110
src/pkg/scheduler/manager_test.go
Normal 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{})
|
||||
}
|
40
src/pkg/scheduler/model/schedule.go
Normal file
40
src/pkg/scheduler/model/schedule.go
Normal 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
|
||||
}
|
54
src/pkg/scheduler/periodic_job.go
Normal file
54
src/pkg/scheduler/periodic_job.go
Normal 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))
|
||||
}
|
192
src/pkg/scheduler/scheduler.go
Normal file
192
src/pkg/scheduler/scheduler.go
Normal 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
|
||||
}
|
115
src/pkg/scheduler/scheduler_test.go
Normal file
115
src/pkg/scheduler/scheduler_test.go
Normal 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)
|
||||
}
|
44
src/testing/clients/dumb_core_client.go
Normal file
44
src/testing/clients/dumb_core_client.go
Normal 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
|
||||
}
|
@ -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,13 +27,10 @@ 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)
|
||||
}
|
||||
|
||||
// PostAction ...
|
||||
func (mjc *MockJobClient) PostAction(uuid, action string) error {
|
||||
@ -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
77
src/testing/scheduler.go
Normal 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
|
||||
}
|
56
src/vendor/github.com/aws/aws-sdk-go/aws/endpoints/defaults.go
generated
vendored
56
src/vendor/github.com/aws/aws-sdk-go/aws/endpoints/defaults.go
generated
vendored
@ -533,6 +533,17 @@ var awsPartition = partition{
|
||||
"us-west-2": endpoint{},
|
||||
},
|
||||
},
|
||||
"backup": service{
|
||||
|
||||
Endpoints: endpoints{
|
||||
"ap-southeast-2": endpoint{},
|
||||
"eu-central-1": endpoint{},
|
||||
"eu-west-1": endpoint{},
|
||||
"us-east-1": endpoint{},
|
||||
"us-east-2": endpoint{},
|
||||
"us-west-2": endpoint{},
|
||||
},
|
||||
},
|
||||
"batch": service{
|
||||
|
||||
Endpoints: endpoints{
|
||||
@ -959,7 +970,10 @@ var awsPartition = partition{
|
||||
"comprehendmedical": service{
|
||||
|
||||
Endpoints: endpoints{
|
||||
"ap-southeast-2": endpoint{},
|
||||
"ca-central-1": endpoint{},
|
||||
"eu-west-1": endpoint{},
|
||||
"eu-west-2": endpoint{},
|
||||
"us-east-1": endpoint{},
|
||||
"us-east-2": endpoint{},
|
||||
"us-west-2": endpoint{},
|
||||
@ -1287,6 +1301,7 @@ var awsPartition = partition{
|
||||
"elasticbeanstalk": service{
|
||||
|
||||
Endpoints: endpoints{
|
||||
"ap-east-1": endpoint{},
|
||||
"ap-northeast-1": endpoint{},
|
||||
"ap-northeast-2": endpoint{},
|
||||
"ap-south-1": endpoint{},
|
||||
@ -1585,6 +1600,13 @@ var awsPartition = partition{
|
||||
"us-west-2": endpoint{},
|
||||
},
|
||||
},
|
||||
"groundstation": service{
|
||||
|
||||
Endpoints: endpoints{
|
||||
"us-east-2": endpoint{},
|
||||
"us-west-2": endpoint{},
|
||||
},
|
||||
},
|
||||
"guardduty": service{
|
||||
IsRegionalized: boxedTrue,
|
||||
Defaults: endpoint{
|
||||
@ -1652,6 +1674,7 @@ var awsPartition = partition{
|
||||
"ap-southeast-2": endpoint{},
|
||||
"eu-central-1": endpoint{},
|
||||
"eu-west-1": endpoint{},
|
||||
"eu-west-2": endpoint{},
|
||||
"us-east-1": endpoint{},
|
||||
"us-east-2": endpoint{},
|
||||
"us-west-1": endpoint{},
|
||||
@ -1689,13 +1712,30 @@ var awsPartition = partition{
|
||||
"us-west-2": endpoint{},
|
||||
},
|
||||
},
|
||||
"iotthingsgraph": service{
|
||||
Defaults: endpoint{
|
||||
CredentialScope: credentialScope{
|
||||
Service: "iotthingsgraph",
|
||||
},
|
||||
},
|
||||
Endpoints: endpoints{
|
||||
"ap-northeast-1": endpoint{},
|
||||
"ap-southeast-2": endpoint{},
|
||||
"eu-west-1": endpoint{},
|
||||
"us-east-1": endpoint{},
|
||||
"us-west-2": endpoint{},
|
||||
},
|
||||
},
|
||||
"kafka": service{
|
||||
|
||||
Endpoints: endpoints{
|
||||
"ap-northeast-1": endpoint{},
|
||||
"ap-southeast-1": endpoint{},
|
||||
"ap-southeast-2": endpoint{},
|
||||
"eu-central-1": endpoint{},
|
||||
"eu-west-1": endpoint{},
|
||||
"eu-west-2": endpoint{},
|
||||
"eu-west-3": endpoint{},
|
||||
"us-east-1": endpoint{},
|
||||
"us-east-2": endpoint{},
|
||||
"us-west-2": endpoint{},
|
||||
@ -3106,7 +3146,7 @@ var awsPartition = partition{
|
||||
"support": service{
|
||||
|
||||
Endpoints: endpoints{
|
||||
"us-east-1": endpoint{},
|
||||
"aws-global": endpoint{},
|
||||
},
|
||||
},
|
||||
"swf": service{
|
||||
@ -3583,6 +3623,19 @@ var awscnPartition = partition{
|
||||
"cn-northwest-1": endpoint{},
|
||||
},
|
||||
},
|
||||
"kms": service{
|
||||
|
||||
Endpoints: endpoints{
|
||||
"ProdFips": endpoint{
|
||||
Hostname: "kms-fips.cn-northwest-1.amazonaws.com.cn",
|
||||
CredentialScope: credentialScope{
|
||||
Region: "cn-northwest-1",
|
||||
},
|
||||
},
|
||||
"cn-north-1": endpoint{},
|
||||
"cn-northwest-1": endpoint{},
|
||||
},
|
||||
},
|
||||
"lambda": service{
|
||||
|
||||
Endpoints: endpoints{
|
||||
@ -3847,6 +3900,7 @@ var awsusgovPartition = partition{
|
||||
"athena": service{
|
||||
|
||||
Endpoints: endpoints{
|
||||
"us-gov-east-1": endpoint{},
|
||||
"us-gov-west-1": endpoint{},
|
||||
},
|
||||
},
|
||||
|
14
src/vendor/github.com/aws/aws-sdk-go/aws/request/connection_reset_error.go
generated
vendored
14
src/vendor/github.com/aws/aws-sdk-go/aws/request/connection_reset_error.go
generated
vendored
@ -1,19 +1,9 @@
|
||||
// +build !appengine,!plan9
|
||||
|
||||
package request
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"syscall"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func isErrConnectionReset(err error) bool {
|
||||
if opErr, ok := err.(*net.OpError); ok {
|
||||
if sysErr, ok := opErr.Err.(*os.SyscallError); ok {
|
||||
return sysErr.Err == syscall.ECONNRESET
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return strings.Contains(err.Error(), "connection reset")
|
||||
}
|
||||
|
11
src/vendor/github.com/aws/aws-sdk-go/aws/request/connection_reset_error_other.go
generated
vendored
11
src/vendor/github.com/aws/aws-sdk-go/aws/request/connection_reset_error_other.go
generated
vendored
@ -1,11 +0,0 @@
|
||||
// +build appengine plan9
|
||||
|
||||
package request
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func isErrConnectionReset(err error) bool {
|
||||
return strings.Contains(err.Error(), "connection reset")
|
||||
}
|
2
src/vendor/github.com/aws/aws-sdk-go/aws/request/request.go
generated
vendored
2
src/vendor/github.com/aws/aws-sdk-go/aws/request/request.go
generated
vendored
@ -588,7 +588,7 @@ func shouldRetryCancel(err error) bool {
|
||||
return err.Temporary()
|
||||
case nil:
|
||||
// `awserr.Error.OrigErr()` can be nil, meaning there was an error but
|
||||
// because we don't know the cause, it is marked as retriable. See
|
||||
// because we don't know the cause, it is marked as retryable. See
|
||||
// TestRequest4xxUnretryable for an example.
|
||||
return true
|
||||
default:
|
||||
|
20
src/vendor/github.com/aws/aws-sdk-go/aws/types.go
generated
vendored
20
src/vendor/github.com/aws/aws-sdk-go/aws/types.go
generated
vendored
@ -7,13 +7,18 @@ import (
|
||||
"github.com/aws/aws-sdk-go/internal/sdkio"
|
||||
)
|
||||
|
||||
// ReadSeekCloser wraps a io.Reader returning a ReaderSeekerCloser. Should
|
||||
// only be used with an io.Reader that is also an io.Seeker. Doing so may
|
||||
// cause request signature errors, or request body's not sent for GET, HEAD
|
||||
// and DELETE HTTP methods.
|
||||
// ReadSeekCloser wraps a io.Reader returning a ReaderSeekerCloser. Allows the
|
||||
// SDK to accept an io.Reader that is not also an io.Seeker for unsigned
|
||||
// streaming payload API operations.
|
||||
//
|
||||
// Deprecated: Should only be used with io.ReadSeeker. If using for
|
||||
// S3 PutObject to stream content use s3manager.Uploader instead.
|
||||
// A ReadSeekCloser wrapping an nonseekable io.Reader used in an API
|
||||
// operation's input will prevent that operation being retried in the case of
|
||||
// network errors, and cause operation requests to fail if the operation
|
||||
// requires payload signing.
|
||||
//
|
||||
// Note: If using With S3 PutObject to stream an object upload The SDK's S3
|
||||
// Upload manager (s3manager.Uploader) provides support for streaming with the
|
||||
// ability to retry network errors.
|
||||
func ReadSeekCloser(r io.Reader) ReaderSeekerCloser {
|
||||
return ReaderSeekerCloser{r}
|
||||
}
|
||||
@ -43,7 +48,8 @@ func IsReaderSeekable(r io.Reader) bool {
|
||||
// Read reads from the reader up to size of p. The number of bytes read, and
|
||||
// error if it occurred will be returned.
|
||||
//
|
||||
// If the reader is not an io.Reader zero bytes read, and nil error will be returned.
|
||||
// If the reader is not an io.Reader zero bytes read, and nil error will be
|
||||
// returned.
|
||||
//
|
||||
// Performs the same functionality as io.Reader Read
|
||||
func (r ReaderSeekerCloser) Read(p []byte) (int, error) {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user