Merge pull request #8374 from goharbor/feature/tag_retention

[WIP]Feature/tag retention
This commit is contained in:
Steven Zou 2019-07-24 18:46:42 +08:00 committed by GitHub
commit 5bfa7a6515
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
123 changed files with 10727 additions and 194 deletions

View File

@ -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)
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,25 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package model
import "github.com/theupdateframework/notary/tuf/data"
// Target represents the json object of a target of a docker image in notary.
// The struct will be used when repository is know so it won'g contain the name of a repository.
type Target struct {
Tag string `json:"tag"`
Hashes data.Hashes `json:"hashes"`
// TODO: update fields as needed.
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@ -17,11 +17,14 @@ package main
import (
"encoding/gob"
"fmt"
"github.com/goharbor/harbor/src/pkg/retention"
"os"
"os/signal"
"strconv"
"syscall"
"github.com/goharbor/harbor/src/common/job"
"github.com/astaxie/beego"
_ "github.com/astaxie/beego/session/redis"
"github.com/goharbor/harbor/src/common/dao"
@ -37,6 +40,7 @@ import (
"github.com/goharbor/harbor/src/core/filter"
"github.com/goharbor/harbor/src/core/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()

View File

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

View File

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

View File

@ -0,0 +1,70 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scheduler
import (
"encoding/json"
"fmt"
"github.com/goharbor/harbor/src/common/job/models"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/api"
"github.com/goharbor/harbor/src/pkg/scheduler/hook"
)
// Handler handles the scheduler requests
type Handler struct {
api.BaseController
}
// Handle ...
func (h *Handler) Handle() {
var data models.JobStatusChange
// status update
if len(data.CheckIn) == 0 {
schedulerID, err := h.GetInt64FromPath(":id")
if err != nil {
log.Errorf("failed to get the schedule ID: %v", err)
return
}
if err := hook.GlobalController.UpdateStatus(schedulerID, data.Status); err != nil {
h.SendInternalServerError(fmt.Errorf("failed to update status of job %s: %v", data.JobID, err))
return
}
return
}
// run callback function
// just log the error message when handling check in request if got any error
params := map[string]interface{}{}
if err := json.Unmarshal([]byte(data.CheckIn), &params); err != nil {
log.Errorf("failed to unmarshal parameters from check in message: %v", err)
return
}
callbackFuncNameParam, exist := params["callback_func_name"]
if !exist {
log.Error("cannot get the parameter \"callback_func_name\" from the check in message")
return
}
callbackFuncName, ok := callbackFuncNameParam.(string)
if !ok || len(callbackFuncName) == 0 {
log.Errorf("invalid \"callback_func_name\": %v", callbackFuncName)
return
}
if err := hook.GlobalController.Run(callbackFuncName, params["params"]); err != nil {
log.Errorf("failed to run the callback function %s: %v", callbackFuncName, err)
return
}
}

View File

@ -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

View File

@ -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=

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,35 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package core
import (
"fmt"
"github.com/goharbor/harbor/src/chartserver"
)
func (c *client) ListAllCharts(project, repository string) ([]*chartserver.ChartVersion, error) {
url := c.buildURL(fmt.Sprintf("/api/chartrepo/%s/charts/%s", project, repository))
var charts []*chartserver.ChartVersion
if err := c.httpclient.Get(url, &charts); err != nil {
return nil, err
}
return charts, nil
}
func (c *client) DeleteChart(project, repository, version string) error {
url := c.buildURL(fmt.Sprintf("/api/chartrepo/%s/charts/%s/%s", project, repository, version))
return c.httpclient.Delete(url)
}

View File

@ -0,0 +1,63 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package core
import (
"fmt"
"net/http"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/chartserver"
chttp "github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/common/http/modifier"
)
// Client defines the methods that a core client should implement
// Currently, it contains only part of the whole method collection
// and we should expand it when needed
type Client interface {
ImageClient
ChartClient
}
// ImageClient defines the methods that an image client should implement
type ImageClient interface {
ListAllImages(project, repository string) ([]*models.TagResp, error)
DeleteImage(project, repository, tag string) error
}
// ChartClient defines the methods that a chart client should implement
type ChartClient interface {
ListAllCharts(project, repository string) ([]*chartserver.ChartVersion, error)
DeleteChart(project, repository, version string) error
}
// New returns an instance of the client which is a default implement for Client
func New(url string, httpclient *http.Client, authorizer modifier.Modifier) Client {
return &client{
url: url,
httpclient: chttp.NewClient(httpclient, authorizer),
}
}
type client struct {
url string
httpclient *chttp.Client
}
func (c *client) buildURL(path string) string {
return fmt.Sprintf("%s/%s", c.url, path)
}

View File

@ -0,0 +1,35 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package core
import (
"fmt"
"github.com/goharbor/harbor/src/common/models"
)
func (c *client) ListAllImages(project, repository string) ([]*models.TagResp, error) {
url := c.buildURL(fmt.Sprintf("/api/repositories/%s/%s/tags", project, repository))
var images []*models.TagResp
if err := c.httpclient.GetAndIteratePagination(url, &images); err != nil {
return nil, err
}
return images, nil
}
func (c *client) DeleteImage(project, repository, tag string) error {
url := c.buildURL(fmt.Sprintf("/api/repositories/%s/%s/tags/%s", project, repository, tag))
return c.httpclient.Delete(url)
}

View File

@ -0,0 +1,61 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package project
import (
"fmt"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
)
// Manager is used for project management
// currently, the interface only defines the methods needed for tag retention
// will expand it when doing refactor
type Manager interface {
// List projects according to the query
List(...*models.ProjectQueryParam) ([]*models.Project, error)
// Get the project specified by the ID or name
Get(interface{}) (*models.Project, error)
}
// New returns a default implementation of Manager
func New() Manager {
return &manager{}
}
type manager struct{}
// List projects according to the query
func (m *manager) List(query ...*models.ProjectQueryParam) ([]*models.Project, error) {
var q *models.ProjectQueryParam
if len(query) > 0 {
q = query[0]
}
return dao.GetProjects(q)
}
// Get the project specified by the ID
func (m *manager) Get(idOrName interface{}) (*models.Project, error) {
id, ok := idOrName.(int64)
if ok {
return dao.GetProjectByID(id)
}
name, ok := idOrName.(string)
if ok {
return dao.GetProjectByName(name)
}
return nil, fmt.Errorf("invalid parameter: %v, should be ID(int64) or name(string)", idOrName)
}

View File

@ -0,0 +1,61 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package repository
import (
"github.com/goharbor/harbor/src/chartserver"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/pkg/project"
)
// Manager is used for repository management
// currently, the interface only defines the methods needed for tag retention
// will expand it when doing refactor
type Manager interface {
// List image repositories under the project specified by the ID
ListImageRepositories(projectID int64) ([]*models.RepoRecord, error)
// List chart repositories under the project specified by the ID
ListChartRepositories(projectID int64) ([]*chartserver.ChartInfo, error)
}
// New returns a default implementation of Manager
func New(projectMgr project.Manager, chartCtl *chartserver.Controller) Manager {
return &manager{
projectMgr: projectMgr,
chartCtl: chartCtl,
}
}
type manager struct {
projectMgr project.Manager
chartCtl *chartserver.Controller
}
// List image repositories under the project specified by the ID
func (m *manager) ListImageRepositories(projectID int64) ([]*models.RepoRecord, error) {
return dao.GetRepositories(&models.RepositoryQuery{
ProjectIDs: []int64{projectID},
})
}
// List chart repositories under the project specified by the ID
func (m *manager) ListChartRepositories(projectID int64) ([]*chartserver.ChartInfo, error) {
project, err := m.projectMgr.Get(projectID)
if err != nil {
return nil, err
}
return m.chartCtl.ListCharts(project.Name)
}

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

@ -0,0 +1,23 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package retention
// TODO: Move to api.Init()
// Init the retention components
func Init() error {
return nil
}

View File

@ -0,0 +1,272 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package retention
import (
"fmt"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/pkg/project"
"github.com/goharbor/harbor/src/pkg/repository"
"github.com/goharbor/harbor/src/pkg/retention/policy"
"github.com/goharbor/harbor/src/pkg/retention/q"
"github.com/goharbor/harbor/src/pkg/scheduler"
"time"
)
// APIController to handle the requests related with retention
type APIController interface {
// Handle the related hooks from the job service and launch the corresponding actions if needed
//
// Arguments:
// PolicyID string : uuid of the retention policy
// event *job.StatusChange : event object sent by job service
//
// Returns:
// common error object if any errors occurred
HandleHook(policyID string, event *job.StatusChange) error
GetRetention(id int64) (*policy.Metadata, error)
CreateRetention(p *policy.Metadata) error
UpdateRetention(p *policy.Metadata) error
DeleteRetention(id int64) error
TriggerRetentionExec(policyID int64, trigger string, dryRun bool) error
OperateRetentionExec(eid int64, action string) error
ListRetentionExecs(policyID int64, query *q.Query) ([]*Execution, error)
ListRetentionExecTasks(executionID int64, query *q.Query) ([]*Task, error)
GetRetentionExecTaskLog(taskID int64) ([]byte, error)
}
// DefaultAPIController ...
type DefaultAPIController struct {
manager Manager
launcher Launcher
projectManager project.Manager
repositoryMgr repository.Manager
scheduler scheduler.Scheduler
}
const (
// RetentionSchedulerCallback ...
RetentionSchedulerCallback = "RetentionSchedulerCallback"
)
// TriggerParam ...
type TriggerParam struct {
PolicyID int64
Trigger string
}
// GetRetention Get Retention
func (r *DefaultAPIController) GetRetention(id int64) (*policy.Metadata, error) {
return r.manager.GetPolicy(id)
}
// CreateRetention Create Retention
func (r *DefaultAPIController) CreateRetention(p *policy.Metadata) error {
if p.Trigger.Kind == policy.TriggerKindSchedule {
if p.Trigger.Settings != nil {
cron, ok := p.Trigger.Settings[policy.TriggerSettingsCron]
if ok {
jobid, err := r.scheduler.Schedule(cron.(string), RetentionSchedulerCallback, TriggerParam{
PolicyID: p.ID,
Trigger: ExecutionTriggerSchedule,
})
if err != nil {
return err
}
p.Trigger.References[policy.TriggerReferencesJobid] = jobid
}
}
}
if _, err := r.manager.CreatePolicy(p); err != nil {
return err
}
return nil
}
// UpdateRetention Update Retention
func (r *DefaultAPIController) UpdateRetention(p *policy.Metadata) error {
p0, err := r.manager.GetPolicy(p.ID)
if err != nil {
return err
}
needUn := false
needSch := false
if p0.Trigger.Kind != p.Trigger.Kind {
if p0.Trigger.Kind == policy.TriggerKindSchedule {
needUn = true
}
if p.Trigger.Kind == policy.TriggerKindSchedule {
needSch = true
}
} else {
switch p.Trigger.Kind {
case policy.TriggerKindSchedule:
if p0.Trigger.Settings["cron"] != p.Trigger.Settings["cron"] {
// unschedule old
if len(p0.Trigger.References[policy.TriggerReferencesJobid].(string)) > 0 {
needUn = true
}
// schedule new
if len(p.Trigger.Settings[policy.TriggerSettingsCron].(string)) > 0 {
// valid cron
needSch = true
}
}
case "":
default:
return fmt.Errorf("Not support Trigger %s", p.Trigger.Kind)
}
}
if needUn {
err = r.scheduler.UnSchedule(p0.Trigger.References[policy.TriggerReferencesJobid].(int64))
if err != nil {
return err
}
}
if needSch {
jobid, err := r.scheduler.Schedule(p.Trigger.Settings[policy.TriggerSettingsCron].(string), RetentionSchedulerCallback, TriggerParam{
PolicyID: p.ID,
Trigger: ExecutionTriggerSchedule,
})
if err != nil {
return err
}
p.Trigger.References[policy.TriggerReferencesJobid] = jobid
}
return r.manager.UpdatePolicy(p)
}
// DeleteRetention Delete Retention
func (r *DefaultAPIController) DeleteRetention(id int64) error {
p, err := r.manager.GetPolicy(id)
if err != nil {
return err
}
if p.Trigger.Kind == policy.TriggerKindSchedule && len(p.Trigger.Settings[policy.TriggerSettingsCron].(string)) > 0 {
err = r.scheduler.UnSchedule(p.Trigger.References[policy.TriggerReferencesJobid].(int64))
if err != nil {
return err
}
}
return r.manager.DeletePolicyAndExec(id)
}
// TriggerRetentionExec Trigger Retention Execution
func (r *DefaultAPIController) TriggerRetentionExec(policyID int64, trigger string, dryRun bool) error {
p, err := r.manager.GetPolicy(policyID)
if err != nil {
return err
}
exec := &Execution{
PolicyID: policyID,
StartTime: time.Now(),
Status: ExecutionStatusInProgress,
Trigger: trigger,
DryRun: dryRun,
}
id, err := r.manager.CreateExecution(exec)
// TODO launcher with DryRun param
num, err := r.launcher.Launch(p, id)
if err != nil {
return err
}
if num == 0 {
exec := &Execution{
ID: id,
EndTime: time.Now(),
Status: ExecutionStatusSucceed,
}
err = r.manager.UpdateExecution(exec)
if err != nil {
return err
}
}
return err
}
// OperateRetentionExec Operate Retention Execution
func (r *DefaultAPIController) OperateRetentionExec(eid int64, action string) error {
e, err := r.manager.GetExecution(eid)
if err != nil {
return err
}
exec := &Execution{}
switch action {
case "stop":
if e.Status != ExecutionStatusInProgress {
return fmt.Errorf("Can't abort, current status is %s", e.Status)
}
exec.ID = eid
exec.Status = ExecutionStatusStopped
exec.EndTime = time.Now()
// TODO stop the execution
default:
return fmt.Errorf("not support action %s", action)
}
return r.manager.UpdateExecution(exec)
}
// ListRetentionExecs List Retention Executions
func (r *DefaultAPIController) ListRetentionExecs(policyID int64, query *q.Query) ([]*Execution, error) {
return r.manager.ListExecutions(policyID, query)
}
// ListRetentionExecTasks List Retention Execution Histories
func (r *DefaultAPIController) ListRetentionExecTasks(executionID int64, query *q.Query) ([]*Task, error) {
q1 := &q.TaskQuery{
ExecutionID: executionID,
PageNumber: query.PageNumber,
PageSize: query.PageSize,
}
return r.manager.ListTasks(q1)
}
// GetRetentionExecTaskLog Get Retention Execution Task Log
func (r *DefaultAPIController) GetRetentionExecTaskLog(taskID int64) ([]byte, error) {
return r.manager.GetTaskLog(taskID)
}
// HandleHook HandleHook
func (r *DefaultAPIController) HandleHook(policyID string, event *job.StatusChange) error {
panic("implement me")
}
// NewAPIController ...
func NewAPIController(projectManager project.Manager, repositoryMgr repository.Manager, scheduler scheduler.Scheduler, retentionLauncher Launcher) APIController {
return &DefaultAPIController{
manager: NewManager(),
launcher: retentionLauncher,
projectManager: projectManager,
repositoryMgr: repositoryMgr,
scheduler: scheduler,
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,153 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dep
import (
"errors"
"fmt"
"net/http"
"github.com/goharbor/harbor/src/common/http/modifier/auth"
"github.com/goharbor/harbor/src/jobservice/config"
"github.com/goharbor/harbor/src/pkg/clients/core"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
// DefaultClient for the retention
var DefaultClient = NewClient()
// Client is designed to access core service to get required infos
type Client interface {
// Get the tag candidates under the repository
//
// Arguments:
// repo *res.Repository : repository info
//
// Returns:
// []*res.Candidate : candidates returned
// error : common error if any errors occurred
GetCandidates(repo *res.Repository) ([]*res.Candidate, error)
// Delete the specified candidate
//
// Arguments:
// candidate *res.Candidate : the deleting candidate
//
// Returns:
// error : common error if any errors occurred
Delete(candidate *res.Candidate) error
}
// NewClient new a basic client
func NewClient(client ...*http.Client) Client {
var c *http.Client
if len(client) > 0 {
c = client[0]
}
if c == nil {
c = http.DefaultClient
}
// init core client
internalCoreURL := config.GetCoreURL()
jobserviceSecret := config.GetAuthSecret()
authorizer := auth.NewSecretAuthorizer(jobserviceSecret)
coreClient := core.New(internalCoreURL, c, authorizer)
return &basicClient{
internalCoreURL: internalCoreURL,
coreClient: coreClient,
}
}
// basicClient is a default
type basicClient struct {
internalCoreURL string
coreClient core.Client
}
// GetCandidates gets the tag candidates under the repository
func (bc *basicClient) GetCandidates(repository *res.Repository) ([]*res.Candidate, error) {
if repository == nil {
return nil, errors.New("repository is nil")
}
candidates := make([]*res.Candidate, 0)
switch repository.Kind {
case res.Image:
images, err := bc.coreClient.ListAllImages(repository.Namespace, repository.Name)
if err != nil {
return nil, err
}
for _, image := range images {
labels := []string{}
for _, label := range image.Labels {
labels = append(labels, label.Name)
}
candidate := &res.Candidate{
Kind: res.Image,
Namespace: repository.Namespace,
Repository: repository.Name,
Tag: image.Name,
Labels: labels,
CreationTime: image.Created.Unix(),
// TODO: populate the pull/push time
// PulledTime: ,
// PushedTime:,
}
candidates = append(candidates, candidate)
}
case res.Chart:
charts, err := bc.coreClient.ListAllCharts(repository.Namespace, repository.Name)
if err != nil {
return nil, err
}
for _, chart := range charts {
labels := []string{}
for _, label := range chart.Labels {
labels = append(labels, label.Name)
}
candidate := &res.Candidate{
Kind: res.Chart,
Namespace: repository.Namespace,
Repository: repository.Name,
Tag: chart.Name,
Labels: labels,
CreationTime: chart.Created.Unix(),
// TODO: populate the pull/push time
// PulledTime: ,
// PushedTime:,
}
candidates = append(candidates, candidate)
}
default:
return nil, fmt.Errorf("unsupported repository kind: %s", repository.Kind)
}
return candidates, nil
}
// Deletes the specified candidate
func (bc *basicClient) Delete(candidate *res.Candidate) error {
if candidate == nil {
return errors.New("candidate is nil")
}
switch candidate.Kind {
case res.Image:
return bc.coreClient.DeleteImage(candidate.Namespace, candidate.Repository, candidate.Tag)
case res.Chart:
return bc.coreClient.DeleteChart(candidate.Namespace, candidate.Repository, candidate.Tag)
default:
return fmt.Errorf("unsupported candidate kind: %s", candidate.Kind)
}
}

View File

@ -0,0 +1,134 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dep
import (
"testing"
"github.com/goharbor/harbor/src/chartserver"
jmodels "github.com/goharbor/harbor/src/common/job/models"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/goharbor/harbor/src/testing/clients"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"k8s.io/helm/pkg/proto/hapi/chart"
"k8s.io/helm/pkg/repo"
)
type fakeCoreClient struct {
clients.DumbCoreClient
}
func (f *fakeCoreClient) ListAllImages(project, repository string) ([]*models.TagResp, error) {
image := &models.TagResp{}
image.Name = "latest"
return []*models.TagResp{image}, nil
}
func (f *fakeCoreClient) ListAllCharts(project, repository string) ([]*chartserver.ChartVersion, error) {
metadata := &chart.Metadata{
Name: "1.0",
}
chart := &chartserver.ChartVersion{}
chart.ChartVersion = repo.ChartVersion{
Metadata: metadata,
}
return []*chartserver.ChartVersion{chart}, nil
}
type fakeJobserviceClient struct{}
func (f *fakeJobserviceClient) SubmitJob(*jmodels.JobData) (string, error) {
return "1", nil
}
func (f *fakeJobserviceClient) GetJobLog(uuid string) ([]byte, error) {
return nil, nil
}
func (f *fakeJobserviceClient) PostAction(uuid, action string) error {
return nil
}
func (f *fakeJobserviceClient) GetExecutions(uuid string) ([]job.Stats, error) {
return nil, nil
}
type clientTestSuite struct {
suite.Suite
}
func (c *clientTestSuite) TestGetCandidates() {
client := &basicClient{}
client.coreClient = &fakeCoreClient{}
var repository *res.Repository
// nil repository
candidates, err := client.GetCandidates(repository)
require.NotNil(c.T(), err)
// image repository
repository = &res.Repository{}
repository.Kind = res.Image
repository.Namespace = "library"
repository.Name = "hello-world"
candidates, err = client.GetCandidates(repository)
require.Nil(c.T(), err)
assert.Equal(c.T(), 1, len(candidates))
assert.Equal(c.T(), res.Image, candidates[0].Kind)
assert.Equal(c.T(), "library", candidates[0].Namespace)
assert.Equal(c.T(), "hello-world", candidates[0].Repository)
assert.Equal(c.T(), "latest", candidates[0].Tag)
// chart repository
repository.Kind = res.Chart
repository.Namespace = "goharbor"
repository.Name = "harbor"
candidates, err = client.GetCandidates(repository)
require.Nil(c.T(), err)
assert.Equal(c.T(), 1, len(candidates))
assert.Equal(c.T(), res.Chart, candidates[0].Kind)
assert.Equal(c.T(), "goharbor", candidates[0].Namespace)
assert.Equal(c.T(), "1.0", candidates[0].Tag)
}
func (c *clientTestSuite) TestDelete() {
client := &basicClient{}
client.coreClient = &fakeCoreClient{}
var candidate *res.Candidate
// nil candidate
err := client.Delete(candidate)
require.NotNil(c.T(), err)
// image
candidate = &res.Candidate{}
candidate.Kind = res.Image
err = client.Delete(candidate)
require.Nil(c.T(), err)
// chart
candidate.Kind = res.Chart
err = client.Delete(candidate)
require.Nil(c.T(), err)
// unsupported type
candidate.Kind = "unsupported"
err = client.Delete(candidate)
require.NotNil(c.T(), err)
}
func TestClientTestSuite(t *testing.T) {
suite.Run(t, new(clientTestSuite))
}

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

@ -0,0 +1,233 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package retention
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/jobservice/logger"
"github.com/goharbor/harbor/src/pkg/retention/dep"
"github.com/goharbor/harbor/src/pkg/retention/policy"
"github.com/goharbor/harbor/src/pkg/retention/policy/lwp"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/olekukonko/tablewriter"
"github.com/pkg/errors"
)
// Job of running retention process
type Job struct {
// client used to talk to core
// TODO: REFER THE GLOBAL CLIENT
client dep.Client
}
// MaxFails of the job
func (pj *Job) MaxFails() uint {
return 3
}
// ShouldRetry indicates job can be retried if failed
func (pj *Job) ShouldRetry() bool {
return true
}
// Validate the parameters
func (pj *Job) Validate(params job.Parameters) error {
if _, err := getParamRepo(params); err != nil {
return err
}
if _, err := getParamMeta(params); err != nil {
return err
}
return nil
}
// Run the job
func (pj *Job) Run(ctx job.Context, params job.Parameters) error {
// logger for logging
myLogger := ctx.GetLogger()
// Parameters have been validated, ignore error checking
repo, _ := getParamRepo(params)
liteMeta, _ := getParamMeta(params)
// Log stage: start
repoPath := fmt.Sprintf("%s/%s", repo.Namespace, repo.Name)
myLogger.Infof("Run retention process.\n Repository: %s \n Rule Algorithm: %s", repoPath, liteMeta.Algorithm)
// Stop check point 1:
if isStopped(ctx) {
logStop(myLogger)
return nil
}
// Retrieve all the candidates under the specified repository
allCandidates, err := pj.client.GetCandidates(repo)
if err != nil {
return logError(myLogger, err)
}
// Log stage: load candidates
myLogger.Infof("Load %d candidates from repository %s", len(allCandidates), repoPath)
// Build the processor
builder := policy.NewBuilder(allCandidates)
processor, err := builder.Build(liteMeta)
if err != nil {
return logError(myLogger, err)
}
// Stop check point 2:
if isStopped(ctx) {
logStop(myLogger)
return nil
}
// Run the flow
results, err := processor.Process(allCandidates)
if err != nil {
return logError(myLogger, err)
}
// Log stage: results with table view
logResults(myLogger, allCandidates, results)
// Check in the results
bytes, err := json.Marshal(results)
if err != nil {
return logError(myLogger, err)
}
if err := ctx.Checkin(string(bytes)); err != nil {
return logError(myLogger, err)
}
return nil
}
func logResults(logger logger.Interface, all []*res.Candidate, results []*res.Result) {
hash := make(map[string]error, len(results))
for _, r := range results {
if r.Target != nil {
hash[r.Target.Hash()] = r.Error
}
}
op := func(art *res.Candidate) string {
if e, exists := hash[art.Hash()]; exists {
if e != nil {
return "Err"
}
return "X"
}
return "√"
}
var buf bytes.Buffer
data := [][]string{}
for _, c := range all {
row := []string{
arn(c),
c.Kind,
strings.Join(c.Labels, ","),
t(c.PushedTime),
t(c.PulledTime),
t(c.CreationTime),
op(c),
}
data = append(data, row)
}
table := tablewriter.NewWriter(&buf)
table.SetHeader([]string{"Artifact", "Kind", "labels", "PushedTime", "PulledTime", "CreatedTime", "Retention"})
table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false})
table.SetCenterSeparator("|")
table.AppendBulk(data)
table.Render()
logger.Infof("%s", buf.String())
// log all the concrete errors if have
for _, r := range results {
if r.Error != nil {
logger.Infof("Retention error for artifact %s:%s : %s", r.Target.Kind, arn(r.Target), r.Error)
}
}
}
func arn(art *res.Candidate) string {
return fmt.Sprintf("%s/%s:%s", art.Namespace, art.Repository, art.Tag)
}
func t(tm int64) string {
return time.Unix(tm, 0).Format("2006/01/02 15:04:05")
}
func isStopped(ctx job.Context) (stopped bool) {
cmd, ok := ctx.OPCommand()
stopped = ok && cmd == job.StopCommand
return
}
func logStop(logger logger.Interface) {
logger.Info("Retention job is stopped")
}
func logError(logger logger.Interface, err error) error {
wrappedErr := errors.Wrap(err, "retention job")
logger.Error(wrappedErr)
return wrappedErr
}
func getParamRepo(params job.Parameters) (*res.Repository, error) {
v, ok := params[ParamRepo]
if !ok {
return nil, errors.Errorf("missing parameter: %s", ParamRepo)
}
repo, ok := v.(*res.Repository)
if !ok {
return nil, errors.Errorf("invalid parameter: %s", ParamRepo)
}
return repo, nil
}
func getParamMeta(params job.Parameters) (*lwp.Metadata, error) {
v, ok := params[ParamMeta]
if !ok {
return nil, errors.Errorf("missing parameter: %s", ParamMeta)
}
meta, ok := v.(*lwp.Metadata)
if !ok {
return nil, errors.Errorf("invalid parameter: %s", ParamMeta)
}
return meta, nil
}

View File

@ -0,0 +1,235 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package retention
import (
"context"
"errors"
"fmt"
"testing"
"time"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/jobservice/logger"
"github.com/goharbor/harbor/src/pkg/retention/dep"
"github.com/goharbor/harbor/src/pkg/retention/policy"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/alg"
"github.com/goharbor/harbor/src/pkg/retention/policy/alg/or"
"github.com/goharbor/harbor/src/pkg/retention/policy/lwp"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule/latestk"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors/doublestar"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors/label"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
// JobTestSuite is test suite for testing job
type JobTestSuite struct {
suite.Suite
oldClient dep.Client
}
// TestJob is entry of running JobTestSuite
func TestJob(t *testing.T) {
suite.Run(t, new(JobTestSuite))
}
// SetupSuite ...
func (suite *JobTestSuite) SetupSuite() {
alg.Register(alg.AlgorithmOR, or.New)
selectors.Register(doublestar.Kind, []string{
doublestar.Matches,
doublestar.Excludes,
doublestar.RepoMatches,
doublestar.RepoExcludes,
doublestar.NSMatches,
doublestar.NSExcludes,
}, doublestar.New)
selectors.Register(label.Kind, []string{label.With, label.Without}, label.New)
action.Register(action.Retain, action.NewRetainAction)
suite.oldClient = dep.DefaultClient
dep.DefaultClient = &fakeRetentionClient{}
}
// TearDownSuite ...
func (suite *JobTestSuite) TearDownSuite() {
dep.DefaultClient = suite.oldClient
}
func (suite *JobTestSuite) TestRunSuccess() {
params := make(job.Parameters)
params[ParamRepo] = &res.Repository{
Namespace: "library",
Name: "harbor",
Kind: res.Image,
}
scopeSelectors := make(map[string][]*rule.Selector)
scopeSelectors["project"] = []*rule.Selector{{
Kind: doublestar.Kind,
Decoration: doublestar.RepoMatches,
Pattern: "{harbor}",
}}
ruleParams := make(rule.Parameters)
ruleParams[latestk.ParameterK] = 10
params[ParamMeta] = &lwp.Metadata{
Algorithm: policy.AlgorithmOR,
Rules: []*rule.Metadata{
{
ID: 1,
Priority: 999,
Action: action.Retain,
Template: latestk.TemplateID,
Parameters: ruleParams,
TagSelectors: []*rule.Selector{{
Kind: label.Kind,
Decoration: label.With,
Pattern: "L3",
}, {
Kind: doublestar.Kind,
Decoration: doublestar.Matches,
Pattern: "**",
}},
ScopeSelectors: scopeSelectors,
},
},
}
j := &Job{
client: &fakeRetentionClient{},
}
err := j.Validate(params)
require.NoError(suite.T(), err)
err = j.Run(&fakeJobContext{}, params)
require.NoError(suite.T(), err)
}
type fakeRetentionClient struct{}
// GetCandidates ...
func (frc *fakeRetentionClient) GetCandidates(repo *res.Repository) ([]*res.Candidate, error) {
return []*res.Candidate{
{
Namespace: "library",
Repository: "harbor",
Kind: "image",
Tag: "latest",
PushedTime: time.Now().Unix() - 11,
PulledTime: time.Now().Unix() - 2,
CreationTime: time.Now().Unix() - 10,
Labels: []string{"L1", "L2"},
},
{
Namespace: "library",
Repository: "harbor",
Kind: "image",
Tag: "dev",
PushedTime: time.Now().Unix() - 10,
PulledTime: time.Now().Unix() - 3,
CreationTime: time.Now().Unix() - 20,
Labels: []string{"L3"},
},
}, nil
}
// Delete ...
func (frc *fakeRetentionClient) Delete(candidate *res.Candidate) error {
return nil
}
// SubmitTask ...
func (frc *fakeRetentionClient) SubmitTask(taskID int64, repository *res.Repository, meta *lwp.Metadata) (string, error) {
return "", errors.New("not implemented")
}
type fakeLogger struct{}
// For debuging
func (l *fakeLogger) Debug(v ...interface{}) {}
// For debuging with format
func (l *fakeLogger) Debugf(format string, v ...interface{}) {}
// For logging info
func (l *fakeLogger) Info(v ...interface{}) {
fmt.Println(v...)
}
// For logging info with format
func (l *fakeLogger) Infof(format string, v ...interface{}) {
fmt.Printf(format+"\n", v...)
}
// For warning
func (l *fakeLogger) Warning(v ...interface{}) {}
// For warning with format
func (l *fakeLogger) Warningf(format string, v ...interface{}) {}
// For logging error
func (l *fakeLogger) Error(v ...interface{}) {
fmt.Println(v...)
}
// For logging error with format
func (l *fakeLogger) Errorf(format string, v ...interface{}) {
}
// For fatal error
func (l *fakeLogger) Fatal(v ...interface{}) {}
// For fatal error with error
func (l *fakeLogger) Fatalf(format string, v ...interface{}) {}
type fakeJobContext struct{}
func (c *fakeJobContext) Build(tracker job.Tracker) (job.Context, error) {
return nil, nil
}
func (c *fakeJobContext) Get(prop string) (interface{}, bool) {
return nil, false
}
func (c *fakeJobContext) SystemContext() context.Context {
return context.TODO()
}
func (c *fakeJobContext) Checkin(status string) error {
fmt.Printf("Check in: %s\n", status)
return nil
}
func (c *fakeJobContext) OPCommand() (job.OPCommand, bool) {
return "", false
}
func (c *fakeJobContext) GetLogger() logger.Interface {
return &fakeLogger{}
}
func (c *fakeJobContext) Tracker() job.Tracker {
return nil
}

View File

@ -0,0 +1,260 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package retention
import (
"fmt"
cjob "github.com/goharbor/harbor/src/common/job"
"github.com/goharbor/harbor/src/common/job/models"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/pkg/project"
"github.com/goharbor/harbor/src/pkg/repository"
"github.com/goharbor/harbor/src/pkg/retention/policy"
"github.com/goharbor/harbor/src/pkg/retention/policy/lwp"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors"
"github.com/pkg/errors"
)
const (
// ParamRepo ...
ParamRepo = "repository"
// ParamMeta ...
ParamMeta = "liteMeta"
)
// Launcher provides function to launch the async jobs to run retentions based on the provided policy.
type Launcher interface {
// Launch async jobs for the retention policy
// A separate job will be launched for each repository
//
// Arguments:
// policy *policy.Metadata: the policy info
// executionID int64 : the execution ID
//
// Returns:
// int64 : the count of tasks
// error : common error if any errors occurred
Launch(policy *policy.Metadata, executionID int64) (int64, error)
}
// NewLauncher returns an instance of Launcher
func NewLauncher(projectMgr project.Manager, repositoryMgr repository.Manager,
retentionMgr Manager) Launcher {
return &launcher{
projectMgr: projectMgr,
repositoryMgr: repositoryMgr,
retentionMgr: retentionMgr,
jobserviceClient: cjob.GlobalClient,
internalCoreURL: config.InternalCoreURL(),
}
}
type launcher struct {
retentionMgr Manager
projectMgr project.Manager
repositoryMgr repository.Manager
jobserviceClient cjob.Client
internalCoreURL string
}
type jobData struct {
repository *res.Repository
policy *lwp.Metadata
taskID int64
}
func (l *launcher) Launch(ply *policy.Metadata, executionID int64) (int64, error) {
if ply == nil {
return 0, launcherError(fmt.Errorf("the policy is nil"))
}
// no rules, return directly
if len(ply.Rules) == 0 {
return 0, nil
}
scope := ply.Scope
if scope == nil {
return 0, launcherError(fmt.Errorf("the scope of policy is nil"))
}
repositoryRules := make(map[res.Repository]*lwp.Metadata, 0)
level := scope.Level
var projectCandidates []*res.Candidate
var err error
if level == "system" {
// get projects
projectCandidates, err = getProjects(l.projectMgr)
if err != nil {
return 0, launcherError(err)
}
}
for _, rule := range ply.Rules {
switch level {
case "system":
// filter projects according to the project selectors
for _, projectSelector := range rule.ScopeSelectors["project"] {
selector, err := selectors.Get(projectSelector.Kind, projectSelector.Decoration,
projectSelector.Pattern)
if err != nil {
return 0, launcherError(err)
}
projectCandidates, err = selector.Select(projectCandidates)
if err != nil {
return 0, launcherError(err)
}
}
case "project":
projectCandidates = append(projectCandidates, &res.Candidate{
NamespaceID: scope.Reference,
})
}
var repositoryCandidates []*res.Candidate
// get repositories of projects
for _, projectCandidate := range projectCandidates {
repositories, err := getRepositories(l.projectMgr, l.repositoryMgr, projectCandidate.NamespaceID)
if err != nil {
return 0, launcherError(err)
}
repositoryCandidates = append(repositoryCandidates, repositories...)
}
// filter repositories according to the repository selectors
for _, repositorySelector := range rule.ScopeSelectors["repository"] {
selector, err := selectors.Get(repositorySelector.Kind, repositorySelector.Decoration,
repositorySelector.Pattern)
if err != nil {
return 0, launcherError(err)
}
repositoryCandidates, err = selector.Select(repositoryCandidates)
if err != nil {
return 0, launcherError(err)
}
}
for _, repositoryCandidate := range repositoryCandidates {
reposit := res.Repository{
Namespace: repositoryCandidate.Namespace,
Name: repositoryCandidate.Repository,
Kind: repositoryCandidate.Kind,
}
if repositoryRules[reposit] == nil {
repositoryRules[reposit] = &lwp.Metadata{
Algorithm: ply.Algorithm,
}
}
repositoryRules[reposit].Rules = append(repositoryRules[reposit].Rules, &rule)
}
}
// no tasks need to be submitted
if len(repositoryRules) == 0 {
return 0, nil
}
// create task records
jobDatas := make([]*jobData, 0)
for repo, p := range repositoryRules {
taskID, err := l.retentionMgr.CreateTask(&Task{
ExecutionID: executionID,
})
if err != nil {
return 0, launcherError(err)
}
jobDatas = append(jobDatas, &jobData{
repository: &repo,
policy: p,
taskID: taskID,
})
}
allFailed := true
for _, jobData := range jobDatas {
j := &models.JobData{
Metadata: &models.JobMetadata{
JobKind: job.KindGeneric,
},
StatusHook: fmt.Sprintf("%s/service/notifications/jobs/retention/tasks/%d", l.internalCoreURL, jobData.taskID),
}
j.Name = job.Retention
j.Parameters = map[string]interface{}{
ParamRepo: jobData.repository,
ParamMeta: jobData.policy,
}
_, err := l.jobserviceClient.SubmitJob(j)
if err != nil {
log.Error(launcherError(fmt.Errorf("failed to submit task %d: %v", jobData.taskID, err)))
continue
}
allFailed = false
}
if allFailed {
return 0, launcherError(fmt.Errorf("all tasks failed"))
}
return int64(len(jobDatas)), nil
}
func launcherError(err error) error {
return errors.Wrap(err, "launcher")
}
func getProjects(projectMgr project.Manager) ([]*res.Candidate, error) {
projects, err := projectMgr.List()
if err != nil {
return nil, err
}
var candidates []*res.Candidate
for _, pro := range projects {
candidates = append(candidates, &res.Candidate{
NamespaceID: pro.ProjectID,
Namespace: pro.Name,
})
}
return candidates, nil
}
func getRepositories(projectMgr project.Manager, repositoryMgr repository.Manager, projectID int64) ([]*res.Candidate, error) {
var candidates []*res.Candidate
pro, err := projectMgr.Get(projectID)
if err != nil {
return nil, err
}
// get image repositories
imageRepositories, err := repositoryMgr.ListImageRepositories(projectID)
if err != nil {
return nil, err
}
for _, r := range imageRepositories {
namespace, repo := utils.ParseRepository(r.Name)
candidates = append(candidates, &res.Candidate{
Namespace: namespace,
Repository: repo,
Kind: "image",
})
}
// get chart repositories
chartRepositories, err := repositoryMgr.ListChartRepositories(projectID)
for _, r := range chartRepositories {
candidates = append(candidates, &res.Candidate{
Namespace: pro.Name,
Repository: r.Name,
Kind: "chart",
})
}
return candidates, nil
}

View File

@ -0,0 +1,236 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package retention
import (
"fmt"
"testing"
"github.com/goharbor/harbor/src/chartserver"
"github.com/goharbor/harbor/src/common/job"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/pkg/project"
"github.com/goharbor/harbor/src/pkg/repository"
"github.com/goharbor/harbor/src/pkg/retention/policy"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/q"
_ "github.com/goharbor/harbor/src/pkg/retention/res/selectors/doublestar"
hjob "github.com/goharbor/harbor/src/testing/job"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type fakeProjectManager struct {
projects []*models.Project
}
func (f *fakeProjectManager) List(...*models.ProjectQueryParam) ([]*models.Project, error) {
return f.projects, nil
}
func (f *fakeProjectManager) Get(idOrName interface{}) (*models.Project, error) {
id, ok := idOrName.(int64)
if ok {
for _, pro := range f.projects {
if pro.ProjectID == id {
return pro, nil
}
}
return nil, nil
}
name, ok := idOrName.(string)
if ok {
for _, pro := range f.projects {
if pro.Name == name {
return pro, nil
}
}
return nil, nil
}
return nil, fmt.Errorf("invalid parameter: %v, should be ID(int64) or name(string)", idOrName)
}
type fakeRepositoryManager struct {
imageRepositories []*models.RepoRecord
chartRepositories []*chartserver.ChartInfo
}
func (f *fakeRepositoryManager) ListImageRepositories(projectID int64) ([]*models.RepoRecord, error) {
return f.imageRepositories, nil
}
func (f *fakeRepositoryManager) ListChartRepositories(projectID int64) ([]*chartserver.ChartInfo, error) {
return f.chartRepositories, nil
}
type fakeRetentionManager struct{}
func (f *fakeRetentionManager) CreatePolicy(p *policy.Metadata) (int64, error) {
return 0, nil
}
func (f *fakeRetentionManager) UpdatePolicy(p *policy.Metadata) error {
return nil
}
func (f *fakeRetentionManager) DeletePolicyAndExec(ID int64) error {
return nil
}
func (f *fakeRetentionManager) GetPolicy(ID int64) (*policy.Metadata, error) {
return nil, nil
}
func (f *fakeRetentionManager) CreateExecution(execution *Execution) (int64, error) {
return 0, nil
}
func (f *fakeRetentionManager) UpdateExecution(execution *Execution) error {
return nil
}
func (f *fakeRetentionManager) GetExecution(eid int64) (*Execution, error) {
return nil, nil
}
func (f *fakeRetentionManager) ListTasks(query ...*q.TaskQuery) ([]*Task, error) {
return nil, nil
}
func (f *fakeRetentionManager) CreateTask(task *Task) (int64, error) {
return 0, nil
}
func (f *fakeRetentionManager) UpdateTask(task *Task, cols ...string) error {
return nil
}
func (f *fakeRetentionManager) GetTaskLog(taskID int64) ([]byte, error) {
return nil, nil
}
func (f *fakeRetentionManager) ListExecutions(policyID int64, query *q.Query) ([]*Execution, error) {
return nil, nil
}
func (f *fakeRetentionManager) AppendHistory(history *History) (int64, error) {
return 0, nil
}
func (f *fakeRetentionManager) ListHistories(executionID int64, query *q.Query) ([]*History, error) {
return nil, nil
}
type launchTestSuite struct {
suite.Suite
projectMgr project.Manager
repositoryMgr repository.Manager
retentionMgr Manager
jobserviceClient job.Client
}
func (l *launchTestSuite) SetupTest() {
pro := &models.Project{
ProjectID: 1,
Name: "library",
}
l.projectMgr = &fakeProjectManager{
projects: []*models.Project{
pro,
}}
l.repositoryMgr = &fakeRepositoryManager{
imageRepositories: []*models.RepoRecord{
{
Name: "library/image",
},
},
chartRepositories: []*chartserver.ChartInfo{
{
Name: "chart",
},
},
}
l.retentionMgr = &fakeRetentionManager{}
l.jobserviceClient = &hjob.MockJobClient{}
}
func (l *launchTestSuite) TestGetProjects() {
projects, err := getProjects(l.projectMgr)
require.Nil(l.T(), err)
assert.Equal(l.T(), 1, len(projects))
assert.Equal(l.T(), int64(1), projects[0].NamespaceID)
assert.Equal(l.T(), "library", projects[0].Namespace)
}
func (l *launchTestSuite) TestGetRepositories() {
repositories, err := getRepositories(l.projectMgr, l.repositoryMgr, 1)
require.Nil(l.T(), err)
assert.Equal(l.T(), 2, len(repositories))
assert.Equal(l.T(), "library", repositories[0].Namespace)
assert.Equal(l.T(), "image", repositories[0].Repository)
assert.Equal(l.T(), "image", repositories[0].Kind)
assert.Equal(l.T(), "library", repositories[1].Namespace)
assert.Equal(l.T(), "chart", repositories[1].Repository)
assert.Equal(l.T(), "chart", repositories[1].Kind)
}
func (l *launchTestSuite) TestLaunch() {
launcher := &launcher{
projectMgr: l.projectMgr,
repositoryMgr: l.repositoryMgr,
retentionMgr: l.retentionMgr,
jobserviceClient: l.jobserviceClient,
}
var ply *policy.Metadata
// nil policy
n, err := launcher.Launch(ply, 1)
require.NotNil(l.T(), err)
// nil rules
ply = &policy.Metadata{}
n, err = launcher.Launch(ply, 1)
require.Nil(l.T(), err)
assert.Equal(l.T(), int64(0), n)
// nil scope
ply = &policy.Metadata{
Rules: []rule.Metadata{
{},
},
}
_, err = launcher.Launch(ply, 1)
require.NotNil(l.T(), err)
// system scope
ply = &policy.Metadata{
Scope: &policy.Scope{
Level: "system",
},
Rules: []rule.Metadata{
{
ScopeSelectors: map[string][]*rule.Selector{
"project": {
{
Kind: "doublestar",
Decoration: "nsMatches",
Pattern: "**",
},
},
"repository": {
{
Kind: "doublestar",
Decoration: "repoMatches",
Pattern: "**",
},
},
},
},
},
}
n, err = launcher.Launch(ply, 1)
require.Nil(l.T(), err)
assert.Equal(l.T(), int64(2), n)
}
func TestLaunchTestSuite(t *testing.T) {
suite.Run(t, new(launchTestSuite))
}

View File

@ -0,0 +1,230 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package retention
import (
"encoding/json"
"errors"
"fmt"
"github.com/astaxie/beego/orm"
"time"
"github.com/goharbor/harbor/src/pkg/retention/dao"
"github.com/goharbor/harbor/src/pkg/retention/dao/models"
"github.com/goharbor/harbor/src/pkg/retention/policy"
"github.com/goharbor/harbor/src/pkg/retention/q"
)
// Manager defines operations of managing policy
type Manager interface {
// Create new policy and return ID
CreatePolicy(p *policy.Metadata) (int64, error)
// Update the existing policy
// Full update
UpdatePolicy(p *policy.Metadata) error
// Delete the specified policy
// No actual use so far
DeletePolicyAndExec(ID int64) error
// Get the specified policy
GetPolicy(ID int64) (*policy.Metadata, error)
// Create a new retention execution
CreateExecution(execution *Execution) (int64, error)
// Update the specified execution
UpdateExecution(execution *Execution) error
// Get the specified execution
GetExecution(eid int64) (*Execution, error)
// List execution histories
ListExecutions(policyID int64, query *q.Query) ([]*Execution, error)
// List tasks histories
ListTasks(query ...*q.TaskQuery) ([]*Task, error)
// Create a new retention task
CreateTask(task *Task) (int64, error)
// Update the specified task
UpdateTask(task *Task, cols ...string) error
// Get the log of the specified task
GetTaskLog(taskID int64) ([]byte, error)
}
// DefaultManager ...
type DefaultManager struct {
}
// CreatePolicy Create Policy
func (d *DefaultManager) CreatePolicy(p *policy.Metadata) (int64, error) {
p1 := &models.RetentionPolicy{}
p1.ScopeLevel = p.Scope.Level
p1.TriggerKind = p.Trigger.Kind
data, _ := json.Marshal(p)
p1.Data = string(data)
p1.CreateTime = time.Now()
p1.UpdateTime = p1.CreateTime
return dao.CreatePolicy(p1)
}
// UpdatePolicy Update Policy
func (d *DefaultManager) UpdatePolicy(p *policy.Metadata) error {
p1 := &models.RetentionPolicy{}
p1.ID = p.ID
p1.ScopeLevel = p.Scope.Level
p1.TriggerKind = p.Trigger.Kind
p.ID = 0
data, _ := json.Marshal(p)
p.ID = p1.ID
p1.Data = string(data)
p1.UpdateTime = time.Now()
return dao.UpdatePolicy(p1, "scope_level", "trigger_kind", "data", "update_time")
}
// DeletePolicyAndExec Delete Policy
func (d *DefaultManager) DeletePolicyAndExec(id int64) error {
return dao.DeletePolicyAndExec(id)
}
// GetPolicy Get Policy
func (d *DefaultManager) GetPolicy(id int64) (*policy.Metadata, error) {
p1, err := dao.GetPolicy(id)
if err != nil {
if err == orm.ErrNoRows {
return nil, nil
}
return nil, err
}
p := &policy.Metadata{}
if err = json.Unmarshal([]byte(p1.Data), p); err != nil {
return nil, err
}
p.ID = id
return p, nil
}
// CreateExecution Create Execution
func (d *DefaultManager) CreateExecution(execution *Execution) (int64, error) {
exec := &models.RetentionExecution{}
exec.PolicyID = execution.PolicyID
exec.StartTime = time.Now()
exec.DryRun = execution.DryRun
exec.Status = "Running"
exec.Trigger = "manual"
return dao.CreateExecution(exec)
}
// UpdateExecution Update Execution
func (d *DefaultManager) UpdateExecution(execution *Execution) error {
exec := &models.RetentionExecution{}
exec.ID = execution.ID
exec.EndTime = execution.EndTime
exec.Status = execution.Status
return dao.UpdateExecution(exec, "end_time", "status")
}
// ListExecutions List Executions
func (d *DefaultManager) ListExecutions(policyID int64, query *q.Query) ([]*Execution, error) {
execs, err := dao.ListExecutions(policyID, query)
if err != nil {
if err == orm.ErrNoRows {
return nil, nil
}
return nil, err
}
var execs1 []*Execution
for _, e := range execs {
e1 := &Execution{}
e1.ID = e.ID
e1.PolicyID = e.PolicyID
e1.Status = e.Status
e1.StartTime = e.StartTime
e1.EndTime = e.EndTime
execs1 = append(execs1, e1)
}
return execs1, nil
}
// GetExecution Get Execution
func (d *DefaultManager) GetExecution(eid int64) (*Execution, error) {
e, err := dao.GetExecution(eid)
if err != nil {
return nil, err
}
e1 := &Execution{}
e1.ID = e.ID
e1.PolicyID = e.PolicyID
e1.Status = e.Status
e1.StartTime = e.StartTime
e1.EndTime = e.EndTime
return e1, nil
}
// CreateTask creates task record
func (d *DefaultManager) CreateTask(task *Task) (int64, error) {
if task == nil {
return 0, errors.New("nil task")
}
t := &models.RetentionTask{
ExecutionID: task.ExecutionID,
Status: task.Status,
StartTime: task.StartTime,
EndTime: task.EndTime,
}
return dao.CreateTask(t)
}
// ListTasks lists tasks according to the query
func (d *DefaultManager) ListTasks(query ...*q.TaskQuery) ([]*Task, error) {
ts, err := dao.ListTask(query...)
if err != nil {
if err == orm.ErrNoRows {
return nil, nil
}
return nil, err
}
tasks := []*Task{}
for _, t := range ts {
tasks = append(tasks, &Task{
ID: t.ID,
ExecutionID: t.ExecutionID,
Status: t.Status,
StartTime: t.StartTime,
EndTime: t.EndTime,
})
}
return tasks, nil
}
// UpdateTask updates the task
func (d *DefaultManager) UpdateTask(task *Task, cols ...string) error {
if task == nil {
return errors.New("nil task")
}
if task.ID <= 0 {
return fmt.Errorf("invalid task ID: %d", task.ID)
}
return dao.UpdateTask(&models.RetentionTask{
ID: task.ID,
ExecutionID: task.ExecutionID,
Status: task.Status,
StartTime: task.StartTime,
EndTime: task.EndTime,
}, cols...)
}
// GetTaskLog gets the logs of task
func (d *DefaultManager) GetTaskLog(taskID int64) ([]byte, error) {
panic("implement me")
}
// NewManager ...
func NewManager() Manager {
return &DefaultManager{}
}

View File

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

View File

@ -0,0 +1,70 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package retention
import "time"
// const definitions
const (
ExecutionStatusInProgress string = "InProgress"
ExecutionStatusSucceed string = "Succeed"
ExecutionStatusFailed string = "Failed"
ExecutionStatusStopped string = "Stopped"
TaskStatusPending string = "Pending"
TaskStatusInProgress string = "InProgress"
TaskStatusSucceed string = "Succeed"
TaskStatusFailed string = "Failed"
TaskStatusStopped string = "Stopped"
CandidateKindImage string = "image"
CandidateKindChart string = "chart"
ExecutionTriggerManual string = "Manual"
ExecutionTriggerSchedule string = "Schedule"
)
// Execution of retention
type Execution struct {
ID int64 `json:"id"`
PolicyID int64 `json:"policy_id"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time,omitempty"`
Status string `json:"status"`
Trigger string `json:"Trigger"`
DryRun bool `json:"dry_run"`
}
// Task of retention
type Task struct {
ID int64 `json:"id"`
ExecutionID int64 `json:"execution_id"`
Status string `json:"status"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
}
// History of retention
type History struct {
ID int64 `json:"id,omitempty"`
ExecutionID int64 `json:"execution_id"`
Rule struct {
ID int `json:"id"`
DisplayText string `json:"display_text"`
} `json:"rule_id"`
// full path: :ns/:repo:tag
Artifact string `json:"tag"`
Timestamp time.Time `json:"timestamp"`
}

View File

@ -0,0 +1,52 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package action
import (
"github.com/pkg/errors"
"sync"
)
// index for keeping the mapping action and its performer
var index sync.Map
// Register the performer with the corresponding action
func Register(action string, factory PerformerFactory) {
if len(action) == 0 || factory == nil {
// do nothing
return
}
index.Store(action, factory)
}
// Get performer with the provided action
func Get(action string, params interface{}) (Performer, error) {
if len(action) == 0 {
return nil, errors.New("empty action")
}
v, ok := index.Load(action)
if !ok {
return nil, errors.Errorf("action %s is not registered", action)
}
factory, ok := v.(PerformerFactory)
if !ok {
return nil, errors.Errorf("invalid action performer registered for action %s", action)
}
return factory(params), nil
}

View File

@ -0,0 +1,90 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package action
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/stretchr/testify/suite"
)
// IndexTestSuite tests the rule index
type IndexTestSuite struct {
suite.Suite
candidates []*res.Candidate
}
// TestIndexEntry is entry of IndexTestSuite
func TestIndexEntry(t *testing.T) {
suite.Run(t, new(IndexTestSuite))
}
// SetupSuite ...
func (suite *IndexTestSuite) SetupSuite() {
Register("fakeAction", newFakePerformer)
suite.candidates = []*res.Candidate{{
Namespace: "library",
Repository: "harbor",
Kind: "image",
Tag: "latest",
PushedTime: time.Now().Unix(),
Labels: []string{"L1", "L2"},
}}
}
// TestRegister tests register
func (suite *IndexTestSuite) TestGet() {
p, err := Get("fakeAction", nil)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), p)
results, err := p.Perform(suite.candidates)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 1, len(results))
assert.Condition(suite.T(), func() (success bool) {
r := results[0]
success = r.Target != nil &&
r.Error == nil &&
r.Target.Repository == "harbor" &&
r.Target.Tag == "latest"
return
})
}
type fakePerformer struct{}
// Perform the artifacts
func (p *fakePerformer) Perform(candidates []*res.Candidate) (results []*res.Result, err error) {
for _, c := range candidates {
results = append(results, &res.Result{
Target: c,
})
}
return
}
func newFakePerformer(params interface{}) Performer {
return &fakePerformer{}
}

View File

@ -0,0 +1,93 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package action
import (
"github.com/goharbor/harbor/src/pkg/retention/dep"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
const (
// Retain artifacts
Retain = "retain"
)
// Performer performs the related actions targeting the candidates
type Performer interface {
// Perform the action
//
// Arguments:
// candidates []*res.Candidate : the targets to perform
//
// Returns:
// []*res.Result : result infos
// error : common error if any errors occurred
Perform(candidates []*res.Candidate) ([]*res.Result, error)
}
// PerformerFactory is factory method for creating Performer
type PerformerFactory func(params interface{}) Performer
// retainAction make sure all the candidates will be retained and others will be cleared
type retainAction struct {
all []*res.Candidate
}
// Perform the action
func (ra *retainAction) Perform(candidates []*res.Candidate) (results []*res.Result, err error) {
retained := make(map[string]bool)
for _, c := range candidates {
retained[c.Hash()] = true
}
// start to delete
if len(ra.all) > 0 {
for _, art := range ra.all {
if _, ok := retained[art.Hash()]; !ok {
result := &res.Result{
Target: art,
}
if err := dep.DefaultClient.Delete(art); err != nil {
result.Error = err
}
results = append(results, result)
}
}
}
return
}
// NewRetainAction is factory method for RetainAction
func NewRetainAction(params interface{}) Performer {
if params != nil {
if all, ok := params.([]*res.Candidate); ok {
return &retainAction{
all: all,
}
}
}
return &retainAction{
all: make([]*res.Candidate, 0),
}
}
func init() {
// Register itself
Register(Retain, NewRetainAction)
}

View File

@ -0,0 +1,112 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package action
import (
"github.com/goharbor/harbor/src/pkg/retention/dep"
"github.com/goharbor/harbor/src/pkg/retention/policy/lwp"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"testing"
"time"
)
// TestPerformerSuite tests the performer related function
type TestPerformerSuite struct {
suite.Suite
oldClient dep.Client
all []*res.Candidate
}
// TestPerformer is the entry of the TestPerformerSuite
func TestPerformer(t *testing.T) {
suite.Run(t, new(TestPerformerSuite))
}
// SetupSuite ...
func (suite *TestPerformerSuite) SetupSuite() {
suite.all = []*res.Candidate{
{
Namespace: "library",
Repository: "harbor",
Kind: "image",
Tag: "latest",
PushedTime: time.Now().Unix(),
Labels: []string{"L1", "L2"},
},
{
Namespace: "library",
Repository: "harbor",
Kind: "image",
Tag: "dev",
PushedTime: time.Now().Unix(),
Labels: []string{"L3"},
},
}
suite.oldClient = dep.DefaultClient
dep.DefaultClient = &fakeRetentionClient{}
}
// TearDownSuite ...
func (suite *TestPerformerSuite) TearDownSuite() {
dep.DefaultClient = suite.oldClient
}
// TestPerform tests Perform action
func (suite *TestPerformerSuite) TestPerform() {
p := &retainAction{
all: suite.all,
}
candidates := []*res.Candidate{
{
Namespace: "library",
Repository: "harbor",
Kind: "image",
Tag: "latest",
PushedTime: time.Now().Unix(),
Labels: []string{"L1", "L2"},
},
}
results, err := p.Perform(candidates)
require.NoError(suite.T(), err)
require.Equal(suite.T(), 1, len(results))
require.NotNil(suite.T(), results[0].Target)
assert.NoError(suite.T(), results[0].Error)
assert.Equal(suite.T(), "dev", results[0].Target.Tag)
}
type fakeRetentionClient struct{}
// GetCandidates ...
func (frc *fakeRetentionClient) GetCandidates(repo *res.Repository) ([]*res.Candidate, error) {
return nil, errors.New("not implemented")
}
// Delete ...
func (frc *fakeRetentionClient) Delete(candidate *res.Candidate) error {
return nil
}
// SubmitTask ...
func (frc *fakeRetentionClient) SubmitTask(taskID int64, repository *res.Repository, meta *lwp.Metadata) (string, error) {
return "", errors.New("not implemented")
}

View File

@ -0,0 +1,49 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package alg
import (
"github.com/pkg/errors"
"sync"
)
const (
// AlgorithmOR for || algorithm
AlgorithmOR = "or"
)
// index for keeping the mapping between algorithm and its processor
var index sync.Map
// Register processor with the algorithm
func Register(algorithm string, processor Factory) {
if len(algorithm) > 0 && processor != nil {
index.Store(algorithm, processor)
}
}
// Get Processor
func Get(algorithm string, params []*Parameter) (Processor, error) {
v, ok := index.Load(algorithm)
if !ok {
return nil, errors.Errorf("no processor registered with algorithm: %s", algorithm)
}
if fac, ok := v.(Factory); ok {
return fac(params), nil
}
return nil, errors.Errorf("no valid processor registered for algorithm: %s", algorithm)
}

View File

@ -0,0 +1,221 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package or
import (
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/alg"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/pkg/errors"
"sync"
)
// processor to handle the rules with OR mapping ways
type processor struct {
// keep evaluator and its related selector if existing
// attentions here, the selectors can be empty/nil, that means match all "**"
evaluators map[*rule.Evaluator][]res.Selector
// action performer
performers map[string]action.Performer
}
// New processor
func New(parameters []*alg.Parameter) alg.Processor {
p := &processor{
evaluators: make(map[*rule.Evaluator][]res.Selector),
performers: make(map[string]action.Performer),
}
if len(parameters) > 0 {
for _, param := range parameters {
if param.Evaluator != nil {
if len(param.Selectors) > 0 {
p.evaluators[&param.Evaluator] = param.Selectors
}
if param.Performer != nil {
p.performers[param.Evaluator.Action()] = param.Performer
}
}
}
}
return p
}
// Process the candidates with the rules
func (p *processor) Process(artifacts []*res.Candidate) ([]*res.Result, error) {
if len(artifacts) == 0 {
log.Debug("no artifacts to retention")
return make([]*res.Result, 0), nil
}
var (
// collect errors by wrapping
err error
// collect processed candidates
processedCandidates = make(map[string]cHash)
)
// for sync
type chanItem struct {
action string
processed []*res.Candidate
}
resChan := make(chan *chanItem, 1)
// handle error
errChan := make(chan error, 1)
// control chan
done := make(chan bool, 1)
// go routine for receiving results/error
go func() {
defer func() {
// done
done <- true
}()
for {
select {
case result := <-resChan:
if result == nil {
// chan is closed
return
}
if _, ok := processedCandidates[result.action]; !ok {
processedCandidates[result.action] = make(cHash)
}
listByAction := processedCandidates[result.action]
for _, rp := range result.processed {
// remove duplicated ones
listByAction[rp.Hash()] = rp
}
case e := <-errChan:
if err == nil {
err = errors.Wrap(e, "artifact processing error")
} else {
err = errors.Wrap(e, err.Error())
}
}
}
}()
wg := new(sync.WaitGroup)
wg.Add(len(p.evaluators))
for eva, selectors := range p.evaluators {
var evaluator = *eva
go func(evaluator rule.Evaluator, selectors []res.Selector) {
var (
processed []*res.Candidate
err error
)
defer func() {
wg.Done()
}()
// init
// pass array copy to the selector
processed = append(processed, artifacts...)
if len(selectors) > 0 {
// selecting artifacts one by one
// `&&` mappings
for _, s := range selectors {
if processed, err = s.Select(processed); err != nil {
errChan <- err
return
}
}
}
if processed, err = evaluator.Process(processed); err != nil {
errChan <- err
return
}
if len(processed) > 0 {
// Pass to the outside
resChan <- &chanItem{
action: evaluator.Action(),
processed: processed,
}
}
}(evaluator, selectors)
}
// waiting for all the rules are evaluated
wg.Wait()
// close result chan
close(resChan)
// check if the receiving loop exists
<-done
if err != nil {
return nil, err
}
results := make([]*res.Result, 0)
// Perform actions
for act, hash := range processedCandidates {
var attachedErr error
cl := hash.toList()
if pf, ok := p.performers[act]; ok {
if theRes, err := pf.Perform(cl); err != nil {
attachedErr = err
} else {
results = append(results, theRes...)
}
} else {
attachedErr = errors.Errorf("no performer added for action %s in OR processor", act)
}
if attachedErr != nil {
for _, c := range cl {
results = append(results, &res.Result{
Target: c,
Error: attachedErr,
})
}
}
}
return results, nil
}
func init() {
alg.Register(alg.AlgorithmOR, New)
}
type cHash map[string]*res.Candidate
func (ch cHash) toList() []*res.Candidate {
l := make([]*res.Candidate, 0)
for _, v := range ch {
l = append(l, v)
}
return l
}

View File

@ -0,0 +1,145 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package or
import (
"errors"
"testing"
"time"
"github.com/goharbor/harbor/src/pkg/retention/dep"
"github.com/goharbor/harbor/src/pkg/retention/policy/lwp"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/alg"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule/lastx"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule/latestk"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors/doublestar"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors/label"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
// ProcessorTestSuite is suite for testing processor
type ProcessorTestSuite struct {
suite.Suite
p alg.Processor
all []*res.Candidate
oldClient dep.Client
}
// TestProcessor is entrance for ProcessorTestSuite
func TestProcessor(t *testing.T) {
suite.Run(t, new(ProcessorTestSuite))
}
// SetupSuite ...
func (suite *ProcessorTestSuite) SetupSuite() {
suite.all = []*res.Candidate{
{
Namespace: "library",
Repository: "harbor",
Kind: "image",
Tag: "latest",
PushedTime: time.Now().Unix(),
Labels: []string{"L1", "L2"},
},
{
Namespace: "library",
Repository: "harbor",
Kind: "image",
Tag: "dev",
PushedTime: time.Now().Unix(),
Labels: []string{"L3"},
},
}
params := make([]*alg.Parameter, 0)
perf := action.NewRetainAction(suite.all)
lastxParams := make(map[string]rule.Parameter)
lastxParams[lastx.ParameterX] = 10
params = append(params, &alg.Parameter{
Evaluator: lastx.New(lastxParams),
Selectors: []res.Selector{
doublestar.New(doublestar.Matches, "*dev*"),
label.New(label.With, "L1,L2"),
},
Performer: perf,
})
latestKParams := make(map[string]rule.Parameter)
latestKParams[latestk.ParameterK] = 10
params = append(params, &alg.Parameter{
Evaluator: latestk.New(latestKParams),
Selectors: []res.Selector{
label.New(label.With, "L3"),
},
Performer: perf,
})
p, err := alg.Get(alg.AlgorithmOR, params)
require.NoError(suite.T(), err)
suite.p = p
suite.oldClient = dep.DefaultClient
dep.DefaultClient = &fakeRetentionClient{}
}
// TearDownSuite ...
func (suite *ProcessorTestSuite) TearDownSuite() {
dep.DefaultClient = suite.oldClient
}
// TestProcess tests process method
func (suite *ProcessorTestSuite) TestProcess() {
results, err := suite.p.Process(suite.all)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 1, len(results))
assert.Condition(suite.T(), func() bool {
for _, r := range results {
if r.Error != nil {
return false
}
}
return true
}, "no errors in the returned result list")
}
type fakeRetentionClient struct{}
// GetCandidates ...
func (frc *fakeRetentionClient) GetCandidates(repo *res.Repository) ([]*res.Candidate, error) {
return nil, errors.New("not implemented")
}
// Delete ...
func (frc *fakeRetentionClient) Delete(candidate *res.Candidate) error {
return nil
}
// SubmitTask ...
func (frc *fakeRetentionClient) SubmitTask(taskID int64, repository *res.Repository, meta *lwp.Metadata) (string, error) {
return "", errors.New("not implemented")
}

View File

@ -0,0 +1,52 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package alg
import (
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
// Processor processing the whole policy targeting a repository.
// Methods are defined to reflect the standard structure of the policy:
// list of rules with corresponding selectors plus an action performer.
type Processor interface {
// Process the artifact candidates
//
// Arguments:
// artifacts []*res.Candidate : process the retention candidates
//
// Returns:
// []*res.Result : the processed results
// error : common error object if any errors occurred
Process(artifacts []*res.Candidate) ([]*res.Result, error)
}
// Parameter for constructing a processor
// Represents one rule
type Parameter struct {
// Evaluator for the rule
Evaluator rule.Evaluator
// Selectors for the rule
Selectors []res.Selector
// Performer for the rule evaluator
Performer action.Performer
}
// Factory for creating processor
type Factory func([]*Parameter) Processor

View File

@ -0,0 +1,95 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package policy
import (
"fmt"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/alg"
"github.com/goharbor/harbor/src/pkg/retention/policy/lwp"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors"
"github.com/pkg/errors"
)
// Builder builds the runnable processor from the raw policy
type Builder interface {
// Builds runnable processor
//
// Arguments:
// policy *Metadata : the simple metadata of retention policy
//
// Returns:
// Processor : a processor implementation to process the candidates
// error : common error object if any errors occurred
Build(policy *lwp.Metadata) (alg.Processor, error)
}
// NewBuilder news a basic builder
func NewBuilder(all []*res.Candidate) Builder {
return &basicBuilder{
allCandidates: all,
}
}
// basicBuilder is default implementation of Builder interface
type basicBuilder struct {
allCandidates []*res.Candidate
}
// Build policy processor from the raw policy
func (bb *basicBuilder) Build(policy *lwp.Metadata) (alg.Processor, error) {
if policy == nil {
return nil, errors.New("nil policy to build processor")
}
params := make([]*alg.Parameter, 0)
for _, r := range policy.Rules {
evaluator, err := rule.Get(r.Template, r.Parameters)
if err != nil {
return nil, err
}
perf, err := action.Get(r.Action, bb.allCandidates)
if err != nil {
return nil, errors.Wrap(err, "get action performer by metadata")
}
sl := make([]res.Selector, 0)
for _, s := range r.TagSelectors {
sel, err := selectors.Get(s.Kind, s.Decoration, s.Pattern)
if err != nil {
return nil, errors.Wrap(err, "get selector by metadata")
}
sl = append(sl, sel)
}
params = append(params, &alg.Parameter{
Evaluator: evaluator,
Selectors: sl,
Performer: perf,
})
}
p, err := alg.Get(policy.Algorithm, params)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("get processor for algorithm: %s", policy.Algorithm))
}
return p, nil
}

View File

@ -0,0 +1,185 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package policy
import (
"testing"
"time"
"github.com/goharbor/harbor/src/pkg/retention/dep"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors"
"github.com/pkg/errors"
"github.com/goharbor/harbor/src/pkg/retention/policy/alg"
"github.com/goharbor/harbor/src/pkg/retention/policy/alg/or"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors/label"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors/doublestar"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule/latestk"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/policy/lwp"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/stretchr/testify/suite"
)
// TestBuilderSuite is the suite to test builder
type TestBuilderSuite struct {
suite.Suite
all []*res.Candidate
oldClient dep.Client
}
// TestBuilder is the entry of testing TestBuilderSuite
func TestBuilder(t *testing.T) {
suite.Run(t, new(TestBuilderSuite))
}
// SetupSuite prepares the testing content if needed
func (suite *TestBuilderSuite) SetupSuite() {
suite.all = []*res.Candidate{
{
NamespaceID: 1,
Namespace: "library",
Repository: "harbor",
Kind: "image",
Tag: "latest",
PushedTime: time.Now().Unix(),
Labels: []string{"L1", "L2"},
},
{
NamespaceID: 1,
Namespace: "library",
Repository: "harbor",
Kind: "image",
Tag: "dev",
PushedTime: time.Now().Unix(),
Labels: []string{"L3"},
},
}
alg.Register(alg.AlgorithmOR, or.New)
selectors.Register(doublestar.Kind, []string{
doublestar.Matches,
doublestar.Excludes,
doublestar.RepoMatches,
doublestar.RepoExcludes,
doublestar.NSMatches,
doublestar.NSExcludes,
}, doublestar.New)
selectors.Register(label.Kind, []string{label.With, label.Without}, label.New)
action.Register(action.Retain, action.NewRetainAction)
suite.oldClient = dep.DefaultClient
dep.DefaultClient = &fakeRetentionClient{}
}
// TearDownSuite ...
func (suite *TestBuilderSuite) TearDownSuite() {
dep.DefaultClient = suite.oldClient
}
// TestBuild tests the Build function
func (suite *TestBuilderSuite) TestBuild() {
b := &basicBuilder{suite.all}
params := make(rule.Parameters)
params[latestk.ParameterK] = 10
scopeSelectors := make(map[string][]*rule.Selector, 1)
scopeSelectors["repository"] = []*rule.Selector{{
Kind: doublestar.Kind,
Decoration: doublestar.RepoMatches,
Pattern: "**",
}}
lm := &lwp.Metadata{
Algorithm: AlgorithmOR,
Rules: []*rule.Metadata{{
ID: 1,
Priority: 999,
Action: action.Retain,
Template: latestk.TemplateID,
Parameters: params,
ScopeSelectors: scopeSelectors,
TagSelectors: []*rule.Selector{
{
Kind: label.Kind,
Decoration: label.With,
Pattern: "L3",
},
},
}},
}
p, err := b.Build(lm)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), p)
artifacts := []*res.Candidate{
{
NamespaceID: 1,
Namespace: "library",
Repository: "harbor",
Kind: "image",
Tag: "dev",
PushedTime: time.Now().Unix(),
Labels: []string{"L3"},
},
}
results, err := p.Process(artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 1, len(results))
assert.Condition(suite.T(), func() (success bool) {
art := results[0]
success = art.Error == nil &&
art.Target != nil &&
art.Target.Repository == "harbor" &&
art.Target.Tag == "latest"
return
})
}
type fakeRetentionClient struct{}
// GetCandidates ...
func (frc *fakeRetentionClient) GetCandidates(repo *res.Repository) ([]*res.Candidate, error) {
return nil, errors.New("not implemented")
}
// Delete ...
func (frc *fakeRetentionClient) Delete(candidate *res.Candidate) error {
return nil
}
// SubmitTask ...
func (frc *fakeRetentionClient) SubmitTask(taskID int64, repository *res.Repository, meta *lwp.Metadata) (string, error) {
return "", errors.New("not implemented")
}

View File

@ -0,0 +1,29 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package lwp = lightweight policy
package lwp
import "github.com/goharbor/harbor/src/pkg/retention/policy/rule"
// Metadata contains partial metadata of policy
// It's a lightweight version of policy.Metadata
type Metadata struct {
// Algorithm applied to the rules
// "OR" / "AND"
Algorithm string `json:"algorithm"`
// Rule collection
Rules []*rule.Metadata `json:"rules"`
}

View File

@ -0,0 +1,83 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package policy
import (
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
)
const (
// AlgorithmOR for OR algorithm
AlgorithmOR = "or"
// TriggerKindSchedule Schedule
TriggerKindSchedule = "Schedule"
// TriggerReferencesJobid job_id
TriggerReferencesJobid = "job_id"
// TriggerSettingsCron cron
TriggerSettingsCron = "cron"
// ScopeLevelProject project
ScopeLevelProject = "project"
)
// Metadata of policy
type Metadata struct {
// ID of the policy
ID int64 `json:"id"`
// Algorithm applied to the rules
// "OR" / "AND"
Algorithm string `json:"algorithm" valid:"Required;Match(/^(OR|AND)$/)"`
// Rule collection
Rules []rule.Metadata `json:"rules"`
// Trigger about how to launch the policy
Trigger *Trigger `json:"trigger" valid:"Required"`
// Which scope the policy will be applied to
Scope *Scope `json:"scope" valid:"Required"`
// The max number of rules in a policy
Capacity int `json:"cap"`
}
// Trigger of the policy
type Trigger struct {
// Const string to declare the trigger type
// 'Schedule'
Kind string `json:"kind"`
// Settings for the specified trigger
// '[cron]="* 22 11 * * *"' for the 'Schedule'
Settings map[string]interface{} `json:"settings"`
// References of the trigger
// e.g: schedule job ID
References map[string]interface{} `json:"references"`
}
// Scope definition
type Scope struct {
// Scope level declaration
// 'system', 'project' and 'repository'
Level string `json:"level" valid:"Required;Match(/^(project)$/)"`
// The reference identity for the specified level
// 0 for 'system', project ID for 'project' and repo ID for 'repository'
Reference int64 `json:"ref" valid:"Required"`
}

View File

@ -0,0 +1,50 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package always
import (
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
const (
// TemplateID of the always retain rule
TemplateID = "always"
)
type evaluator struct{}
// Process for the "always" Evaluator simply returns the input with no error
func (e *evaluator) Process(artifacts []*res.Candidate) ([]*res.Candidate, error) {
return artifacts, nil
}
func (e *evaluator) Action() string {
return action.Retain
}
// New returns an "always" Evaluator. It requires no parameters.
func New(_ rule.Parameters) rule.Evaluator {
return &evaluator{}
}
func init() {
rule.Register(&rule.IndexMeta{
TemplateID: TemplateID,
Action: action.Retain,
Parameters: []*rule.IndexedParam{},
}, New)
}

View File

@ -0,0 +1,49 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package always
import (
"testing"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type EvaluatorTestSuite struct {
suite.Suite
}
func (e *EvaluatorTestSuite) TestNew() {
sut := New(rule.Parameters{})
require.NotNil(e.T(), sut)
require.IsType(e.T(), &evaluator{}, sut)
}
func (e *EvaluatorTestSuite) TestProcess() {
sut := New(rule.Parameters{})
input := []*res.Candidate{{PushedTime: 0}, {PushedTime: 1}, {PushedTime: 2}, {PushedTime: 3}}
result, err := sut.Process(input)
require.NoError(e.T(), err)
require.Len(e.T(), result, len(input))
}
func TestEvaluatorSuite(t *testing.T) {
suite.Run(t, &EvaluatorTestSuite{})
}

View File

@ -0,0 +1,36 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rule
import "github.com/goharbor/harbor/src/pkg/retention/res"
// Evaluator defines method of executing rule
type Evaluator interface {
// Filter the inputs and return the filtered outputs
//
// Arguments:
// artifacts []*res.Candidate : candidates for processing
//
// Returns:
// []*res.Candidate : matched candidates for next stage
// error : common error object if any errors occurred
Process(artifacts []*res.Candidate) ([]*res.Candidate, error)
// Specify what action is performed to the candidates processed by this evaluator
Action() string
}
// Factory defines a factory method for creating rule evaluator
type Factory func(parameters Parameters) Evaluator

View File

@ -0,0 +1,116 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rule
import (
"github.com/pkg/errors"
"sync"
)
// index for keeping the mapping between template ID and evaluator
var index sync.Map
// IndexMeta defines metadata for rule registration
type IndexMeta struct {
TemplateID string `json:"rule_template"`
// Action of the rule performs
// "retain"
Action string `json:"action"`
Parameters []*IndexedParam `json:"params"`
}
// IndexedParam declares the param info
type IndexedParam struct {
Name string `json:"name"`
// Type of the param
// "int", "string" or "[]string"
Type string `json:"type"`
Unit string `json:"unit"`
Required bool `json:"required"`
}
// indexedItem is the item saved in the sync map
type indexedItem struct {
Meta *IndexMeta
Factory Factory
}
// Register the rule evaluator with the corresponding rule template
func Register(meta *IndexMeta, factory Factory) {
if meta == nil || factory == nil || len(meta.TemplateID) == 0 {
// do nothing
return
}
index.Store(meta.TemplateID, &indexedItem{
Meta: meta,
Factory: factory,
})
}
// Get rule evaluator with the provided template ID
func Get(templateID string, parameters Parameters) (Evaluator, error) {
if len(templateID) == 0 {
return nil, errors.New("empty rule template ID")
}
v, ok := index.Load(templateID)
if !ok {
return nil, errors.Errorf("rule evaluator %s is not registered", templateID)
}
item := v.(*indexedItem)
// We can check more things if we want to do in the future
if len(item.Meta.Parameters) > 0 {
for _, p := range item.Meta.Parameters {
if p.Required {
exists := parameters != nil
if exists {
_, exists = parameters[p.Name]
}
if !exists {
return nil, errors.Errorf("missing required parameter %s for rule %s", p.Name, templateID)
}
}
}
}
factory := item.Factory
return factory(parameters), nil
}
// Index returns all the metadata of the registered rules
func Index() []*IndexMeta {
res := make([]*IndexMeta, 0)
index.Range(func(k, v interface{}) bool {
if item, ok := v.(*indexedItem); ok {
res = append(res, item.Meta)
return true
}
return false
})
return res
}

View File

@ -0,0 +1,117 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rule
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/stretchr/testify/suite"
)
// IndexTestSuite tests the rule index
type IndexTestSuite struct {
suite.Suite
}
// TestIndexEntry is entry of IndexTestSuite
func TestIndexEntry(t *testing.T) {
suite.Run(t, new(IndexTestSuite))
}
// SetupSuite ...
func (suite *IndexTestSuite) SetupSuite() {
Register(&IndexMeta{
TemplateID: "fakeEvaluator",
Action: "retain",
Parameters: []*IndexedParam{
{
Name: "fakeParam",
Type: "int",
Unit: "count",
Required: true,
},
},
}, newFakeEvaluator)
}
// TestRegister tests register
func (suite *IndexTestSuite) TestGet() {
params := make(Parameters)
params["fakeParam"] = 99
evaluator, err := Get("fakeEvaluator", params)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), evaluator)
candidates := []*res.Candidate{{
Namespace: "library",
Repository: "harbor",
Kind: "image",
Tag: "latest",
PushedTime: time.Now().Unix(),
Labels: []string{"L1", "L2"},
}}
res, err := evaluator.Process(candidates)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 1, len(res))
assert.Condition(suite.T(), func() bool {
c := res[0]
return c.Repository == "harbor" && c.Tag == "latest"
})
}
// TestIndex tests Index
func (suite *IndexTestSuite) TestIndex() {
metas := Index()
require.Equal(suite.T(), 1, len(metas))
assert.Condition(suite.T(), func() bool {
m := metas[0]
return m.TemplateID == "fakeEvaluator" &&
m.Action == "retain" &&
len(m.Parameters) > 0
})
}
type fakeEvaluator struct {
i int
}
// Process rule
func (e *fakeEvaluator) Process(artifacts []*res.Candidate) ([]*res.Candidate, error) {
return artifacts, nil
}
// Action of the rule
func (e *fakeEvaluator) Action() string {
return "retain"
}
// newFakeEvaluator is the factory of fakeEvaluator
func newFakeEvaluator(parameters Parameters) Evaluator {
i := 10
if v, ok := parameters["fakeParam"]; ok {
i = v.(int)
}
return &fakeEvaluator{i}
}

View File

@ -0,0 +1,71 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package lastpulled
import (
"sort"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
const (
// TemplateID of the rule
TemplateID = "lastpulled"
// ParameterN is the name of the metadata parameter for the N value
ParameterN = TemplateID
// DefaultN is the default number of tags to retain
DefaultN = 10
)
type evaluator struct {
n int
}
func (e *evaluator) Process(artifacts []*res.Candidate) ([]*res.Candidate, error) {
sort.Slice(artifacts, func(i, j int) bool {
return artifacts[i].PulledTime > artifacts[j].PulledTime
})
i := e.n
if i > len(artifacts) {
i = len(artifacts)
}
return artifacts[:i], nil
}
func (e *evaluator) Action() string {
return action.Retain
}
// New constructs an evaluator with the given parameters
func New(params rule.Parameters) rule.Evaluator {
if params != nil {
if p, ok := params[ParameterN]; ok {
if v, ok := p.(int); ok && v >= 0 {
return &evaluator{n: v}
}
}
}
log.Debugf("default parameter %d used for rule %s", DefaultN, TemplateID)
return &evaluator{n: DefaultN}
}

View File

@ -0,0 +1,89 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package lastpulled
import (
"math/rand"
"strconv"
"testing"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type EvaluatorTestSuite struct {
suite.Suite
}
func (e *EvaluatorTestSuite) TestNew() {
tests := []struct {
Name string
args rule.Parameters
expectedK int
}{
{Name: "Valid", args: map[string]rule.Parameter{ParameterN: 5}, expectedK: 5},
{Name: "Default If Negative", args: map[string]rule.Parameter{ParameterN: -1}, expectedK: DefaultN},
{Name: "Default If Not Set", args: map[string]rule.Parameter{}, expectedK: DefaultN},
{Name: "Default If Wrong Type", args: map[string]rule.Parameter{ParameterN: "foo"}, expectedK: DefaultN},
}
for _, tt := range tests {
e.T().Run(tt.Name, func(t *testing.T) {
e := New(tt.args).(*evaluator)
require.Equal(t, tt.expectedK, e.n)
})
}
}
func (e *EvaluatorTestSuite) TestProcess() {
data := []*res.Candidate{{PulledTime: 0}, {PulledTime: 1}, {PulledTime: 2}, {PulledTime: 3}, {PulledTime: 4}}
rand.Shuffle(len(data), func(i, j int) {
data[i], data[j] = data[j], data[i]
})
tests := []struct {
n int
expected int
minPullTime int64
}{
{n: 0, expected: 0, minPullTime: 0},
{n: 1, expected: 1, minPullTime: 4},
{n: 3, expected: 3, minPullTime: 2},
{n: 5, expected: 5, minPullTime: 0},
{n: 6, expected: 5, minPullTime: 0},
}
for _, tt := range tests {
e.T().Run(strconv.Itoa(tt.n), func(t *testing.T) {
ev := New(map[string]rule.Parameter{ParameterN: tt.n})
result, err := ev.Process(data)
require.NoError(t, err)
require.Len(t, result, tt.expected)
for _, v := range result {
require.False(e.T(), v.PulledTime < tt.minPullTime)
}
})
}
}
func TestEvaluatorSuite(t *testing.T) {
suite.Run(t, &EvaluatorTestSuite{})
}

View File

@ -0,0 +1,91 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package lastx
import (
"time"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
const (
// TemplateID of last x days rule
TemplateID = "lastXDays"
// ParameterX ...
ParameterX = TemplateID
// DefaultX defines the default X
DefaultX = 10
)
// evaluator for evaluating last x days
type evaluator struct {
// last x days
x int
}
// Process the candidates based on the rule definition
func (e *evaluator) Process(artifacts []*res.Candidate) (retain []*res.Candidate, err error) {
cutoff := time.Now().Add(time.Duration(e.x*-24) * time.Hour)
for _, a := range artifacts {
if time.Unix(a.PushedTime, 0).UTC().After(cutoff) {
retain = append(retain, a)
}
}
return
}
// Specify what action is performed to the candidates processed by this evaluator
func (e *evaluator) Action() string {
return action.Retain
}
// New a Evaluator
func New(params rule.Parameters) rule.Evaluator {
if params != nil {
if param, ok := params[ParameterX]; ok {
if v, ok := param.(int); ok && v >= 0 {
return &evaluator{
x: v,
}
}
}
}
log.Debugf("default parameter %d used for rule %s", DefaultX, TemplateID)
return &evaluator{
x: DefaultX,
}
}
func init() {
// Register itself
rule.Register(&rule.IndexMeta{
TemplateID: TemplateID,
Action: action.Retain,
Parameters: []*rule.IndexedParam{
{
Name: ParameterX,
Type: "int",
Unit: "days",
Required: true,
},
},
}, New)
}

View File

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

View File

@ -0,0 +1,105 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package latestk
import (
"sort"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
const (
// TemplateID of latest active k rule
TemplateID = "latestActiveK"
// ParameterK ...
ParameterK = TemplateID
// DefaultK defines the default K
DefaultK = 10
)
// evaluator for evaluating latest active k images
type evaluator struct {
// latest k
k int
}
// Process the candidates based on the rule definition
func (e *evaluator) Process(artifacts []*res.Candidate) ([]*res.Candidate, error) {
// Sort artifacts by their "active time"
//
// Active time is defined as the selection of c.PulledTime or c.PushedTime,
// whichever is bigger, aka more recent.
sort.Slice(artifacts, func(i, j int) bool {
return activeTime(artifacts[i]) > activeTime(artifacts[j])
})
i := e.k
if i > len(artifacts) {
i = len(artifacts)
}
return artifacts[:i], nil
}
// Specify what action is performed to the candidates processed by this evaluator
func (e *evaluator) Action() string {
return action.Retain
}
// New a Evaluator
func New(params rule.Parameters) rule.Evaluator {
if params != nil {
if param, ok := params[ParameterK]; ok {
if v, ok := param.(int); ok && v >= 0 {
return &evaluator{
k: v,
}
}
}
}
log.Debugf("default parameter %d used for rule %s", DefaultK, TemplateID)
return &evaluator{
k: DefaultK,
}
}
func activeTime(c *res.Candidate) int64 {
if c.PulledTime > c.PushedTime {
return c.PulledTime
}
return c.PushedTime
}
func init() {
// Register itself
rule.Register(&rule.IndexMeta{
TemplateID: TemplateID,
Action: action.Retain,
Parameters: []*rule.IndexedParam{
{
Name: ParameterK,
Type: "int",
Unit: "count",
Required: true,
},
},
}, New)
}

View File

@ -0,0 +1,99 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package latestk
import (
"strconv"
"testing"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/stretchr/testify/suite"
)
type EvaluatorTestSuite struct {
suite.Suite
artifacts []*res.Candidate
}
func (e *EvaluatorTestSuite) SetupSuite() {
e.artifacts = []*res.Candidate{
{PulledTime: 1, PushedTime: 2},
{PulledTime: 3, PushedTime: 4},
{PulledTime: 6, PushedTime: 5},
{PulledTime: 8, PushedTime: 7},
{PulledTime: 9, PushedTime: 9},
{PulledTime: 10, PushedTime: 10},
{PulledTime: 0, PushedTime: 11},
}
}
func (e *EvaluatorTestSuite) TestProcess() {
tests := []struct {
k int
expected int
minActiveTime int64
}{
{k: 0, expected: 0},
{k: 1, expected: 1, minActiveTime: 11},
{k: 2, expected: 2, minActiveTime: 10},
{k: 5, expected: 5, minActiveTime: 6},
{k: 6, expected: 6, minActiveTime: 3},
{k: 99, expected: len(e.artifacts)},
}
for _, tt := range tests {
e.T().Run(strconv.Itoa(tt.k), func(t *testing.T) {
sut := &evaluator{k: tt.k}
result, err := sut.Process(e.artifacts)
require.NoError(t, err)
require.Len(t, result, tt.expected)
for _, v := range result {
assert.True(t, activeTime(v) >= tt.minActiveTime)
}
})
}
}
func (e *EvaluatorTestSuite) TestNew() {
tests := []struct {
name string
params rule.Parameters
expectedK int
}{
{name: "Valid", params: rule.Parameters{ParameterK: 5}, expectedK: 5},
{name: "Default If Negative", params: rule.Parameters{ParameterK: -5}, expectedK: DefaultK},
{name: "Default If Wrong Type", params: rule.Parameters{ParameterK: "5"}, expectedK: DefaultK},
{name: "Default If Wrong Key", params: rule.Parameters{"n": 5}, expectedK: DefaultK},
{name: "Default If Empty", params: rule.Parameters{}, expectedK: DefaultK},
}
for _, tt := range tests {
e.T().Run(tt.name, func(t *testing.T) {
sut := New(tt.params).(*evaluator)
require.Equal(t, tt.expectedK, sut.k)
})
}
}
func TestEvaluatorSuite(t *testing.T) {
suite.Run(t, &EvaluatorTestSuite{})
}

View File

@ -0,0 +1,94 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package latestk
import (
"sort"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
const (
// TemplateID of latest k rule
TemplateID = "latestK"
// ParameterK ...
ParameterK = TemplateID
// DefaultK defines the default K
DefaultK = 10
)
// evaluator for evaluating latest k tags
type evaluator struct {
// latest k
k int
}
// Process the candidates based on the rule definition
func (e *evaluator) Process(artifacts []*res.Candidate) ([]*res.Candidate, error) {
// The updated proposal does not guarantee the order artifacts are provided, so we have to sort them first
sort.Slice(artifacts, func(i, j int) bool {
return artifacts[i].PushedTime < artifacts[j].PushedTime
})
i := e.k
if i > len(artifacts) {
i = len(artifacts)
}
return artifacts[:i], nil
}
// Specify what action is performed to the candidates processed by this evaluator
func (e *evaluator) Action() string {
return action.Retain
}
// New a Evaluator
func New(params rule.Parameters) rule.Evaluator {
if params != nil {
if param, ok := params[ParameterK]; ok {
if v, ok := param.(int); ok && v >= 0 {
return &evaluator{
k: v,
}
}
}
}
log.Debugf("default parameter %d used for rule %s", DefaultK, TemplateID)
return &evaluator{
k: DefaultK,
}
}
func init() {
// Register itself
rule.Register(&rule.IndexMeta{
TemplateID: TemplateID,
Action: action.Retain,
Parameters: []*rule.IndexedParam{
{
Name: ParameterK,
Type: "int",
Unit: "count",
Required: true,
},
},
}, New)
}

View File

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

View File

@ -0,0 +1,83 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package latestk
import (
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/pkg/retention/policy/action"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
"github.com/goharbor/harbor/src/pkg/retention/res"
)
const (
// TemplateID of latest pulled k rule
TemplateID = "latestPulledK"
// ParameterK ...
ParameterK = TemplateID
// DefaultK defines the default K
DefaultK = 10
)
// evaluator for evaluating latest pulled k images
type evaluator struct {
// latest k
k int
}
// Process the candidates based on the rule definition
func (e *evaluator) Process(artifacts []*res.Candidate) ([]*res.Candidate, error) {
// TODO: REPLACE SAMPLE CODE WITH REAL IMPLEMENTATION
return artifacts, nil
}
// Specify what action is performed to the candidates processed by this evaluator
func (e *evaluator) Action() string {
return action.Retain
}
// New a Evaluator
func New(params rule.Parameters) rule.Evaluator {
if params != nil {
if param, ok := params[ParameterK]; ok {
if v, ok := param.(int); ok {
return &evaluator{
k: v,
}
}
}
}
log.Debugf("default parameter %d used for rule %s", DefaultK, TemplateID)
return &evaluator{
k: DefaultK,
}
}
func init() {
// Register itself
rule.Register(&rule.IndexMeta{
TemplateID: TemplateID,
Action: action.Retain,
Parameters: []*rule.IndexedParam{
{
Name: ParameterK,
Type: "int",
Unit: "count",
Required: true,
},
},
}, New)
}

View File

@ -0,0 +1,61 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rule
// Metadata of the retention rule
type Metadata struct {
// UUID of rule
ID int `json:"id"`
// Priority of rule when doing calculating
Priority int `json:"priority" valid:"Required"`
// Action of the rule performs
// "retain"
Action string `json:"action" valid:"Required"`
// Template ID
Template string `json:"template" valid:"Required"`
// The parameters of this rule
Parameters Parameters `json:"params"`
// Selector attached to the rule for filtering tags
TagSelectors []*Selector `json:"tag_selectors" valid:"Required"`
// Selector attached to the rule for filtering scope (e.g: repositories or namespaces)
ScopeSelectors map[string][]*Selector `json:"scope_selectors" valid:"Required"`
}
// Selector to narrow down the list
type Selector struct {
// Kind of the selector
// "regularExpression" or "label"
Kind string `json:"kind" valid:"Required"`
// Decorated the selector
// for "regularExpression" : "matches" and "excludes"
// for "label" : "with" and "without"
Decoration string `json:"decoration" valid:"Required"`
// Param for the selector
Pattern string `json:"pattern" valid:"Required"`
}
// Parameters of rule, indexed by the key
type Parameters map[string]Parameter
// Parameter of rule
type Parameter interface{}

View File

@ -0,0 +1,29 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package q
// Query parameters
type Query struct {
PageNumber int64
PageSize int64
}
// TaskQuery parameters
type TaskQuery struct {
ExecutionID int64
Status string
PageNumber int64
PageSize int64
}

View File

@ -0,0 +1,68 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package res
import (
"encoding/base64"
"fmt"
)
const (
// Image kind
Image = "image"
// Chart kind
Chart = "chart"
)
// Repository of candidate
type Repository struct {
// Namespace
Namespace string
// Repository name
Name string
// So far we need the kind of repository and retrieve candidates with different APIs
// TODO: REMOVE IT IN THE FUTURE IF WE SUPPORT UNIFIED ARTIFACT MODEL
Kind string
}
// Candidate for retention processor to match
type Candidate struct {
// Namespace(project) ID
NamespaceID int64
// Namespace
Namespace string
// Repository name
Repository string
// Kind of the candidate
// "image" or "chart"
Kind string
// Tag info
Tag string
// Pushed time in seconds
PushedTime int64
// Pulled time in seconds
PulledTime int64
// Created time in seconds
CreationTime int64
// Labels attached with the candidate
Labels []string
}
// Hash code based on the candidate info for differentiation
func (c *Candidate) Hash() string {
raw := fmt.Sprintf("%s:%s/%s:%s", c.Kind, c.Namespace, c.Repository, c.Tag)
return base64.StdEncoding.EncodeToString([]byte(raw))
}

View File

@ -0,0 +1,22 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package res
// Result keeps the action result
type Result struct {
Target *Candidate `json:"target"`
// nil error means success
Error error `json:"error"`
}

View File

@ -0,0 +1,30 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package res
// Selector is used to filter the inputting list
type Selector interface {
// Select the matched ones
//
// Arguments:
// artifacts []*Candidate : candidates for matching
//
// Returns:
// []*Candidate : matched candidates
Select(artifacts []*Candidate) ([]*Candidate, error)
}
// SelectorFactory is factory method to return a selector implementation
type SelectorFactory func(decoration string, pattern string) Selector

View File

@ -0,0 +1,115 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package doublestar
import (
"github.com/bmatcuk/doublestar"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors"
)
const (
// Kind ...
Kind = "doublestar"
// Matches [pattern] for tag (default)
Matches = "matches"
// Excludes [pattern] for tag (default)
Excludes = "excludes"
// RepoMatches represents repository matches [pattern]
RepoMatches = "repoMatches"
// RepoExcludes represents repository excludes [pattern]
RepoExcludes = "repoExcludes"
// NSMatches represents namespace matches [pattern]
NSMatches = "nsMatches"
// NSExcludes represents namespace excludes [pattern]
NSExcludes = "nsExcludes"
)
// selector for regular expression
type selector struct {
// Pre defined pattern declarator
// "matches", "excludes", "repoMatches" or "repoExcludes"
decoration string
// The pattern expression
pattern string
}
// Select candidates by regular expressions
func (s *selector) Select(artifacts []*res.Candidate) (selected []*res.Candidate, err error) {
value := ""
excludes := false
for _, art := range artifacts {
switch s.decoration {
case Matches:
value = art.Tag
case Excludes:
value = art.Tag
excludes = true
case RepoMatches:
value = art.Repository
case RepoExcludes:
value = art.Repository
excludes = true
case NSMatches:
value = art.Namespace
case NSExcludes:
value = art.Namespace
excludes = true
}
if len(value) > 0 {
matched, err := match(s.pattern, value)
if err != nil {
// if error occurred, directly throw it out
return nil, err
}
if (matched && !excludes) || (!matched && excludes) {
selected = append(selected, art)
}
}
}
return selected, nil
}
// New is factory method for doublestar selector
func New(decoration string, pattern string) res.Selector {
return &selector{
decoration: decoration,
pattern: pattern,
}
}
// match returns whether the str matches the pattern
func match(pattern, str string) (bool, error) {
if len(pattern) == 0 {
return true, nil
}
return doublestar.Match(pattern, str)
}
func init() {
// Register doublestar selector
selectors.Register(Kind, []string{
Matches,
Excludes,
RepoMatches,
RepoExcludes,
NSMatches,
NSExcludes,
}, New)
}

View File

@ -0,0 +1,252 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package doublestar
import (
"fmt"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"testing"
"time"
)
// RegExpSelectorTestSuite is a suite for testing the label selector
type RegExpSelectorTestSuite struct {
suite.Suite
artifacts []*res.Candidate
}
// TestRegExpSelector is entrance for RegExpSelectorTestSuite
func TestRegExpSelector(t *testing.T) {
suite.Run(t, new(RegExpSelectorTestSuite))
}
// SetupSuite to do preparation work
func (suite *RegExpSelectorTestSuite) SetupSuite() {
suite.artifacts = []*res.Candidate{
{
NamespaceID: 1,
Namespace: "library",
Repository: "harbor",
Tag: "latest",
Kind: res.Image,
PushedTime: time.Now().Unix() - 3600,
PulledTime: time.Now().Unix(),
CreationTime: time.Now().Unix() - 7200,
Labels: []string{"label1", "label2", "label3"},
},
{
NamespaceID: 2,
Namespace: "retention",
Repository: "redis",
Tag: "4.0",
Kind: res.Image,
PushedTime: time.Now().Unix() - 3600,
PulledTime: time.Now().Unix(),
CreationTime: time.Now().Unix() - 7200,
Labels: []string{"label1", "label4", "label5"},
},
{
NamespaceID: 2,
Namespace: "retention",
Repository: "redis",
Tag: "4.1",
Kind: res.Image,
PushedTime: time.Now().Unix() - 3600,
PulledTime: time.Now().Unix(),
CreationTime: time.Now().Unix() - 7200,
Labels: []string{"label1", "label4", "label5"},
},
}
}
// TestTagMatches tests the tag `matches` case
func (suite *RegExpSelectorTestSuite) TestTagMatches() {
tagMatches := &selector{
decoration: Matches,
pattern: "{latest,4.*}",
}
selected, err := tagMatches.Select(suite.artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 3, len(selected))
assert.Condition(suite.T(), func() bool {
return expect([]string{"harbor:latest", "redis:4.0", "redis:4.1"}, selected)
})
tagMatches2 := &selector{
decoration: Matches,
pattern: "4.*",
}
selected, err = tagMatches2.Select(suite.artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 2, len(selected))
assert.Condition(suite.T(), func() bool {
return expect([]string{"redis:4.0", "redis:4.1"}, selected)
})
}
// TestTagExcludes tests the tag `excludes` case
func (suite *RegExpSelectorTestSuite) TestTagExcludes() {
tagExcludes := &selector{
decoration: Excludes,
pattern: "{latest,4.*}",
}
selected, err := tagExcludes.Select(suite.artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 0, len(selected))
tagExcludes2 := &selector{
decoration: Excludes,
pattern: "4.*",
}
selected, err = tagExcludes2.Select(suite.artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 1, len(selected))
assert.Condition(suite.T(), func() bool {
return expect([]string{"harbor:latest"}, selected)
})
}
// TestRepoMatches tests the repository `matches` case
func (suite *RegExpSelectorTestSuite) TestRepoMatches() {
repoMatches := &selector{
decoration: RepoMatches,
pattern: "{redis}",
}
selected, err := repoMatches.Select(suite.artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 2, len(selected))
assert.Condition(suite.T(), func() bool {
return expect([]string{"redis:4.0", "redis:4.1"}, selected)
})
repoMatches2 := &selector{
decoration: RepoMatches,
pattern: "har*",
}
selected, err = repoMatches2.Select(suite.artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 1, len(selected))
assert.Condition(suite.T(), func() bool {
return expect([]string{"harbor:latest"}, selected)
})
}
// TestRepoExcludes tests the repository `excludes` case
func (suite *RegExpSelectorTestSuite) TestRepoExcludes() {
repoExcludes := &selector{
decoration: RepoExcludes,
pattern: "{redis}",
}
selected, err := repoExcludes.Select(suite.artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 1, len(selected))
assert.Condition(suite.T(), func() bool {
return expect([]string{"harbor:latest"}, selected)
})
repoExcludes2 := &selector{
decoration: RepoExcludes,
pattern: "har*",
}
selected, err = repoExcludes2.Select(suite.artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 2, len(selected))
assert.Condition(suite.T(), func() bool {
return expect([]string{"redis:4.0", "redis:4.1"}, selected)
})
}
// TestNSMatches tests the namespace `matches` case
func (suite *RegExpSelectorTestSuite) TestNSMatches() {
repoMatches := &selector{
decoration: NSMatches,
pattern: "{library}",
}
selected, err := repoMatches.Select(suite.artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 1, len(selected))
assert.Condition(suite.T(), func() bool {
return expect([]string{"harbor:latest"}, selected)
})
repoMatches2 := &selector{
decoration: RepoMatches,
pattern: "re*",
}
selected, err = repoMatches2.Select(suite.artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 2, len(selected))
assert.Condition(suite.T(), func() bool {
return expect([]string{"redis:4.0", "redis:4.1"}, selected)
})
}
// TestNSExcludes tests the namespace `excludes` case
func (suite *RegExpSelectorTestSuite) TestNSExcludes() {
repoExcludes := &selector{
decoration: NSExcludes,
pattern: "{library}",
}
selected, err := repoExcludes.Select(suite.artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 2, len(selected))
assert.Condition(suite.T(), func() bool {
return expect([]string{"redis:4.0", "redis:4.1"}, selected)
})
repoExcludes2 := &selector{
decoration: NSExcludes,
pattern: "re*",
}
selected, err = repoExcludes2.Select(suite.artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 1, len(selected))
assert.Condition(suite.T(), func() bool {
return expect([]string{"harbor:latest"}, selected)
})
}
// Check whether the returned result matched the expected ones (only check repo:tag)
func expect(expected []string, candidates []*res.Candidate) bool {
hash := make(map[string]bool)
for _, art := range candidates {
hash[fmt.Sprintf("%s:%s", art.Repository, art.Tag)] = true
}
for _, exp := range expected {
if _, ok := hash[exp]; !ok {
return ok
}
}
return true
}

View File

@ -0,0 +1,90 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package selectors
import (
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/pkg/errors"
"sync"
)
// index for keeping the mapping between selector meta and its implementation
var index sync.Map
// IndexedMeta describes the indexed selector
type IndexedMeta struct {
Kind string `json:"kind"`
Decorations []string `json:"decorations"`
}
// indexedItem defined item kept in the index
type indexedItem struct {
Meta *IndexedMeta
Factory res.SelectorFactory
}
// Register the selector with the corresponding selector kind and decoration
func Register(kind string, decorations []string, factory res.SelectorFactory) {
if len(kind) == 0 || factory == nil {
// do nothing
return
}
index.Store(kind, &indexedItem{
Meta: &IndexedMeta{
Kind: kind,
Decorations: decorations,
},
Factory: factory,
})
}
// Get selector with the provided kind and decoration
func Get(kind, decoration, pattern string) (res.Selector, error) {
if len(kind) == 0 || len(decoration) == 0 {
return nil, errors.New("empty selector kind or decoration")
}
v, ok := index.Load(kind)
if !ok {
return nil, errors.Errorf("selector %s is not registered", kind)
}
item := v.(*indexedItem)
for _, dec := range item.Meta.Decorations {
if dec == decoration {
factory := item.Factory
return factory(decoration, pattern), nil
}
}
return nil, errors.Errorf("decoration %s of selector %s is not supported", decoration, kind)
}
// Index returns all the declarative selectors
func Index() []*IndexedMeta {
all := make([]*IndexedMeta, 0)
index.Range(func(k, v interface{}) bool {
if item, ok := v.(*indexedItem); ok {
all = append(all, item.Meta)
return true
}
return false
})
return all
}

View File

@ -0,0 +1,88 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package label
import (
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/goharbor/harbor/src/pkg/retention/res/selectors"
"strings"
)
const (
// Kind ...
Kind = "label"
// With labels
With = "withLabels"
// Without labels
Without = "withoutLabels"
)
// selector is for label selector
type selector struct {
// Pre defined pattern decorations
// "with" or "without"
decoration string
// Label list
labels []string
}
// Select candidates by the labels
func (s *selector) Select(artifacts []*res.Candidate) (selected []*res.Candidate, err error) {
for _, art := range artifacts {
if isMatched(s.labels, art.Labels, s.decoration) {
selected = append(selected, art)
}
}
return selected, nil
}
// New is factory method for list selector
func New(decoration string, pattern string) res.Selector {
labels := strings.Split(pattern, ",")
return &selector{
decoration: decoration,
labels: labels,
}
}
// Check if the resource labels match the pattern labels
func isMatched(patternLbls []string, resLbls []string, decoration string) bool {
hash := make(map[string]bool)
for _, lbl := range resLbls {
hash[lbl] = true
}
for _, lbl := range patternLbls {
_, exists := hash[lbl]
if decoration == Without && exists {
return false
}
if decoration == With && !exists {
return false
}
}
return true
}
func init() {
// Register doublestar selector
selectors.Register(Kind, []string{With, Without}, New)
}

View File

@ -0,0 +1,148 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package label
import (
"fmt"
"github.com/goharbor/harbor/src/pkg/retention/res"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"testing"
"time"
)
// LabelSelectorTestSuite is a suite for testing the label selector
type LabelSelectorTestSuite struct {
suite.Suite
artifacts []*res.Candidate
}
// TestLabelSelector is entrance for LabelSelectorTestSuite
func TestLabelSelector(t *testing.T) {
suite.Run(t, new(LabelSelectorTestSuite))
}
// SetupSuite to do preparation work
func (suite *LabelSelectorTestSuite) SetupSuite() {
suite.artifacts = []*res.Candidate{
{
NamespaceID: 1,
Namespace: "library",
Repository: "harbor",
Tag: "1.9",
Kind: res.Image,
PushedTime: time.Now().Unix() - 3600,
PulledTime: time.Now().Unix(),
CreationTime: time.Now().Unix() - 7200,
Labels: []string{"label1", "label2", "label3"},
},
{
NamespaceID: 1,
Namespace: "library",
Repository: "harbor",
Tag: "dev",
Kind: res.Image,
PushedTime: time.Now().Unix() - 3600,
PulledTime: time.Now().Unix(),
CreationTime: time.Now().Unix() - 7200,
Labels: []string{"label1", "label4", "label5"},
},
}
}
// TestWithLabelsUnMatched tests the selector of `with` labels but nothing matched
func (suite *LabelSelectorTestSuite) TestWithLabelsUnMatched() {
withNothing := &selector{
decoration: With,
labels: []string{"label6"},
}
selected, err := withNothing.Select(suite.artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 0, len(selected))
}
// TestWithLabelsMatched tests the selector of `with` labels and matched something
func (suite *LabelSelectorTestSuite) TestWithLabelsMatched() {
with1 := &selector{
decoration: With,
labels: []string{"label2"},
}
selected, err := with1.Select(suite.artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 1, len(selected))
assert.Condition(suite.T(), func() bool {
return expect([]string{"harbor:1.9"}, selected)
})
with2 := &selector{
decoration: With,
labels: []string{"label1"},
}
selected2, err := with2.Select(suite.artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 2, len(selected2))
assert.Condition(suite.T(), func() bool {
return expect([]string{"harbor:1.9", "harbor:dev"}, selected2)
})
}
// TestWithoutExistingLabels tests the selector of `without` existing labels
func (suite *LabelSelectorTestSuite) TestWithoutExistingLabels() {
withoutExisting := &selector{
decoration: Without,
labels: []string{"label1"},
}
selected, err := withoutExisting.Select(suite.artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 0, len(selected))
}
// TestWithoutNoneExistingLabels tests the selector of `without` non-existing labels
func (suite *LabelSelectorTestSuite) TestWithoutNoneExistingLabels() {
withoutNonExisting := &selector{
decoration: Without,
labels: []string{"label6"},
}
selected, err := withoutNonExisting.Select(suite.artifacts)
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 2, len(selected))
assert.Condition(suite.T(), func() bool {
return expect([]string{"harbor:1.9", "harbor:dev"}, selected)
})
}
// Check whether the returned result matched the expected ones (only check repo:tag)
func expect(expected []string, candidates []*res.Candidate) bool {
hash := make(map[string]bool)
for _, art := range candidates {
hash[fmt.Sprintf("%s:%s", art.Repository, art.Tag)] = true
}
for _, exp := range expected {
if _, ok := hash[exp]; !ok {
return ok
}
}
return true
}

View File

@ -0,0 +1,99 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dao
import (
"errors"
"fmt"
"time"
"github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/pkg/scheduler/model"
)
// ScheduleDao defines the method that a schedule data access model should implement
type ScheduleDao interface {
Create(*model.Schedule) (int64, error)
Update(*model.Schedule, ...string) error
Delete(int64) error
Get(int64) (*model.Schedule, error)
List(...*model.ScheduleQuery) ([]*model.Schedule, error)
}
// New returns an instance of the default schedule data access model implementation
func New() ScheduleDao {
return &scheduleDao{}
}
type scheduleDao struct{}
func (s *scheduleDao) Create(schedule *model.Schedule) (int64, error) {
if schedule == nil {
return 0, errors.New("nil schedule")
}
now := time.Now()
schedule.CreationTime = &now
schedule.UpdateTime = &now
return dao.GetOrmer().Insert(schedule)
}
func (s *scheduleDao) Update(schedule *model.Schedule, cols ...string) error {
if schedule == nil {
return errors.New("nil schedule")
}
if schedule.ID <= 0 {
return fmt.Errorf("invalid ID: %d", schedule.ID)
}
now := time.Now()
schedule.UpdateTime = &now
_, err := dao.GetOrmer().Update(schedule, cols...)
return err
}
func (s *scheduleDao) Delete(id int64) error {
_, err := dao.GetOrmer().Delete(&model.Schedule{
ID: id,
})
return err
}
func (s *scheduleDao) Get(id int64) (*model.Schedule, error) {
schedule := &model.Schedule{
ID: id,
}
if err := dao.GetOrmer().Read(schedule); err != nil {
if err == orm.ErrNoRows {
return nil, nil
}
return nil, err
}
return schedule, nil
}
func (s *scheduleDao) List(query ...*model.ScheduleQuery) ([]*model.Schedule, error) {
qs := dao.GetOrmer().QueryTable(&model.Schedule{})
if len(query) > 0 && query[0] != nil {
if len(query[0].JobID) > 0 {
qs = qs.Filter("JobID", query[0].JobID)
}
}
schedules := []*model.Schedule{}
_, err := qs.All(&schedules)
if err != nil {
return nil, err
}
return schedules, nil
}

View File

@ -0,0 +1,122 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dao
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/pkg/scheduler/model"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
var schDao = &scheduleDao{}
type scheduleTestSuite struct {
suite.Suite
scheduleID int64
}
func (s *scheduleTestSuite) SetupSuite() {
dao.PrepareTestForPostgresSQL()
}
func (s *scheduleTestSuite) SetupTest() {
t := s.T()
id, err := schDao.Create(&model.Schedule{
JobID: "1",
Status: "pending",
})
require.Nil(t, err)
s.scheduleID = id
}
func (s *scheduleTestSuite) TearDownTest() {
// clear
dao.GetOrmer().Raw("delete from schedule").Exec()
}
func (s *scheduleTestSuite) TestCreate() {
t := s.T()
// nil schedule
_, err := schDao.Create(nil)
require.NotNil(t, err)
// pass
_, err = schDao.Create(&model.Schedule{
JobID: "1",
})
require.Nil(t, err)
}
func (s *scheduleTestSuite) TestUpdate() {
t := s.T()
// nil schedule
err := schDao.Update(nil)
require.NotNil(t, err)
// invalid ID
err = schDao.Update(&model.Schedule{})
require.NotNil(t, err)
// pass
err = schDao.Update(&model.Schedule{
ID: s.scheduleID,
Status: "running",
})
require.Nil(t, err)
schedule, err := schDao.Get(s.scheduleID)
require.Nil(t, err)
assert.Equal(t, "running", schedule.Status)
}
func (s *scheduleTestSuite) TestDelete() {
t := s.T()
err := schDao.Delete(s.scheduleID)
require.Nil(t, err)
schedule, err := schDao.Get(s.scheduleID)
require.Nil(t, err)
assert.Nil(t, schedule)
}
func (s *scheduleTestSuite) TestGet() {
t := s.T()
schedule, err := schDao.Get(s.scheduleID)
require.Nil(t, err)
assert.Equal(t, "pending", schedule.Status)
}
func (s *scheduleTestSuite) TestList() {
t := s.T()
// nil query
schedules, err := schDao.List()
require.Nil(t, err)
require.Equal(t, 1, len(schedules))
assert.Equal(t, s.scheduleID, schedules[0].ID)
// query by job ID
schedules, err = schDao.List(&model.ScheduleQuery{
JobID: "1",
})
require.Nil(t, err)
require.Equal(t, 1, len(schedules))
assert.Equal(t, s.scheduleID, schedules[0].ID)
}
func TestScheduleDao(t *testing.T) {
suite.Run(t, &scheduleTestSuite{})
}

View File

@ -0,0 +1,59 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hook
import (
"time"
"github.com/goharbor/harbor/src/pkg/scheduler"
"github.com/goharbor/harbor/src/pkg/scheduler/model"
)
// GlobalController is an instance of the default controller that can be used globally
var GlobalController = NewController()
// Controller updates the scheduler job status or runs the callback function
type Controller interface {
UpdateStatus(scheduleID int64, status string) error
Run(callbackFuncName string, params interface{}) error
}
// NewController returns an instance of the default controller
func NewController() Controller {
return &controller{
manager: scheduler.GlobalManager,
}
}
type controller struct {
manager scheduler.Manager
}
func (c *controller) UpdateStatus(scheduleID int64, status string) error {
now := time.Now()
return c.manager.Update(&model.Schedule{
ID: scheduleID,
Status: status,
UpdateTime: &now,
}, "Status", "UpdateTime")
}
func (c *controller) Run(callbackFuncName string, params interface{}) error {
f, err := scheduler.GetCallbackFunc(callbackFuncName)
if err != nil {
return err
}
return f(params)
}

View File

@ -0,0 +1,56 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hook
import (
"testing"
"github.com/goharbor/harbor/src/pkg/scheduler"
"github.com/goharbor/harbor/src/pkg/scheduler/model"
htesting "github.com/goharbor/harbor/src/testing"
"github.com/stretchr/testify/require"
)
var h = &controller{
manager: &htesting.FakeSchedulerManager{},
}
func TestUpdateStatus(t *testing.T) {
// task not exist
err := h.UpdateStatus(1, "running")
require.NotNil(t, err)
// pass
h.manager.(*htesting.FakeSchedulerManager).Schedules = []*model.Schedule{
{
ID: 1,
Status: "",
},
}
err = h.UpdateStatus(1, "running")
require.Nil(t, err)
}
func TestRun(t *testing.T) {
// callback function not exist
err := h.Run("not-exist", nil)
require.NotNil(t, err)
// pass
err = scheduler.Register("callback", func(interface{}) error { return nil })
require.Nil(t, err)
err = h.Run("callback", nil)
require.Nil(t, err)
}

View File

@ -0,0 +1,66 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scheduler
import (
"github.com/goharbor/harbor/src/pkg/scheduler/dao"
"github.com/goharbor/harbor/src/pkg/scheduler/model"
)
var (
// GlobalManager is an instance of the default manager that
// can be used globally
GlobalManager = NewManager()
)
// Manager manages the schedule of the scheduler
type Manager interface {
Create(*model.Schedule) (int64, error)
Update(*model.Schedule, ...string) error
Delete(int64) error
Get(int64) (*model.Schedule, error)
List(...*model.ScheduleQuery) ([]*model.Schedule, error)
}
// NewManager returns an instance of the default manager
func NewManager() Manager {
return &manager{
scheduleDao: dao.New(),
}
}
type manager struct {
scheduleDao dao.ScheduleDao
}
func (m *manager) Create(schedule *model.Schedule) (int64, error) {
return m.scheduleDao.Create(schedule)
}
func (m *manager) Update(schedule *model.Schedule, props ...string) error {
return m.scheduleDao.Update(schedule, props...)
}
func (m *manager) Delete(id int64) error {
return m.scheduleDao.Delete(id)
}
func (m *manager) List(query ...*model.ScheduleQuery) ([]*model.Schedule, error) {
return m.scheduleDao.List(query...)
}
func (m *manager) Get(id int64) (*model.Schedule, error) {
return m.scheduleDao.Get(id)
}

View File

@ -0,0 +1,110 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scheduler
import (
"testing"
"github.com/goharbor/harbor/src/pkg/scheduler/model"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)
var mgr *manager
type fakeScheduleDao struct {
schedules []*model.Schedule
mock.Mock
}
func (f *fakeScheduleDao) Create(*model.Schedule) (int64, error) {
f.Called()
return 1, nil
}
func (f *fakeScheduleDao) Update(*model.Schedule, ...string) error {
f.Called()
return nil
}
func (f *fakeScheduleDao) Delete(int64) error {
f.Called()
return nil
}
func (f *fakeScheduleDao) Get(int64) (*model.Schedule, error) {
f.Called()
return nil, nil
}
func (f *fakeScheduleDao) List(query ...*model.ScheduleQuery) ([]*model.Schedule, error) {
f.Called()
if len(query) == 0 || query[0] == nil {
return f.schedules, nil
}
result := []*model.Schedule{}
for _, sch := range f.schedules {
if sch.JobID == query[0].JobID {
result = append(result, sch)
}
}
return result, nil
}
type managerTestSuite struct {
suite.Suite
}
func (m *managerTestSuite) SetupTest() {
// recreate schedule manager
mgr = &manager{
scheduleDao: &fakeScheduleDao{},
}
}
func (m *managerTestSuite) TestCreate() {
t := m.T()
mgr.scheduleDao.(*fakeScheduleDao).On("Create", mock.Anything)
mgr.Create(nil)
mgr.scheduleDao.(*fakeScheduleDao).AssertCalled(t, "Create")
}
func (m *managerTestSuite) TestUpdate() {
t := m.T()
mgr.scheduleDao.(*fakeScheduleDao).On("Update", mock.Anything)
mgr.Update(nil)
mgr.scheduleDao.(*fakeScheduleDao).AssertCalled(t, "Update")
}
func (m *managerTestSuite) TestDelete() {
t := m.T()
mgr.scheduleDao.(*fakeScheduleDao).On("Delete", mock.Anything)
mgr.Delete(1)
mgr.scheduleDao.(*fakeScheduleDao).AssertCalled(t, "Delete")
}
func (m *managerTestSuite) TestGet() {
t := m.T()
mgr.scheduleDao.(*fakeScheduleDao).On("Get", mock.Anything)
mgr.Get(1)
mgr.scheduleDao.(*fakeScheduleDao).AssertCalled(t, "Get")
}
func (m *managerTestSuite) TestList() {
t := m.T()
mgr.scheduleDao.(*fakeScheduleDao).On("List", mock.Anything)
mgr.List(nil)
mgr.scheduleDao.(*fakeScheduleDao).AssertCalled(t, "List")
}
func TestManager(t *testing.T) {
suite.Run(t, &managerTestSuite{})
}

View File

@ -0,0 +1,40 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package model
import (
"time"
"github.com/astaxie/beego/orm"
)
func init() {
orm.RegisterModel(
new(Schedule))
}
// Schedule is a record for a scheduler job
type Schedule struct {
ID int64 `orm:"pk;auto;column(id)" json:"id"`
JobID string `orm:"column(job_id)" json:"job_id"`
Status string `orm:"column(status)" json:"status"`
CreationTime *time.Time `orm:"column(creation_time)" json:"creation_time"`
UpdateTime *time.Time `orm:"column(update_time)" json:"update_time"`
}
// ScheduleQuery is query for schedule
type ScheduleQuery struct {
JobID string
}

View File

@ -0,0 +1,54 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scheduler
import (
"encoding/json"
"github.com/goharbor/harbor/src/jobservice/job"
)
// const definitions
const (
// the job name that used to register to Jobservice
JobNameScheduler = "SCHEDULER"
)
// PeriodicJob is designed to generate hook event periodically
type PeriodicJob struct{}
// MaxFails of the job
func (pj *PeriodicJob) MaxFails() uint {
return 3
}
// ShouldRetry indicates job can be retried if failed
func (pj *PeriodicJob) ShouldRetry() bool {
return true
}
// Validate the parameters
func (pj *PeriodicJob) Validate(params job.Parameters) error {
return nil
}
// Run the job
func (pj *PeriodicJob) Run(ctx job.Context, params job.Parameters) error {
data, err := json.Marshal(params)
if err != nil {
return err
}
return ctx.Checkin(string(data))
}

View File

@ -0,0 +1,192 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scheduler
import (
"fmt"
"net/http"
"sync"
"time"
chttp "github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/common/job"
"github.com/goharbor/harbor/src/common/job/models"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/pkg/scheduler/model"
"github.com/pkg/errors"
)
const (
jobParamCallbackFunc = "callback_func"
jobParamCallbackFuncParams = "params"
)
var (
// GlobalScheduler is an instance of the default scheduler that
// can be used globally. Call Init() to initialize it first
GlobalScheduler Scheduler
registry = make(map[string]CallbackFunc)
)
// CallbackFunc defines the function that the scheduler calls when triggered
type CallbackFunc func(interface{}) error
// Scheduler provides the capability to run a periodic task, a callback function
// needs to be registered before using the scheduler
type Scheduler interface {
Schedule(cron string, callbackFuncName string, params interface{}) (int64, error)
UnSchedule(id int64) error
}
// Register the callback function with name, and the function will be called
// by the scheduler when the scheduler is triggered
func Register(name string, callbackFunc CallbackFunc) error {
if len(name) == 0 {
return errors.New("empty name")
}
if callbackFunc == nil {
return errors.New("callback function is nil")
}
_, exist := registry[name]
if exist {
return fmt.Errorf("callback function %s already exists", name)
}
registry[name] = callbackFunc
return nil
}
// GetCallbackFunc returns the registered callback function specified by the name
func GetCallbackFunc(name string) (CallbackFunc, error) {
f, exist := registry[name]
if !exist {
return nil, fmt.Errorf("callback function %s not found", name)
}
return f, nil
}
func callbackFuncExist(name string) bool {
_, exist := registry[name]
return exist
}
// Init the GlobalScheduler
func Init() {
GlobalScheduler = New(config.InternalCoreURL())
}
// New returns an instance of the default scheduler
func New(internalCoreURL string) Scheduler {
return &scheduler{
internalCoreURL: internalCoreURL,
jobserviceClient: job.GlobalClient,
manager: GlobalManager,
}
}
type scheduler struct {
sync.RWMutex
internalCoreURL string
manager Manager
jobserviceClient job.Client
}
func (s *scheduler) Schedule(cron string, callbackFuncName string, params interface{}) (int64, error) {
if !callbackFuncExist(callbackFuncName) {
return 0, fmt.Errorf("callback function %s not found", callbackFuncName)
}
// create schedule record
now := time.Now()
scheduleID, err := s.manager.Create(&model.Schedule{
CreationTime: &now,
UpdateTime: &now,
})
if err != nil {
return 0, err
}
log.Debugf("the schedule record %d created", scheduleID)
// submit scheduler job to Jobservice
statusHookURL := fmt.Sprintf("%s/service/notifications/schedules/%d", s.internalCoreURL, scheduleID)
jd := &models.JobData{
Name: JobNameScheduler,
Parameters: map[string]interface{}{
jobParamCallbackFunc: callbackFuncName,
jobParamCallbackFuncParams: params,
},
Metadata: &models.JobMetadata{
JobKind: job.JobKindPeriodic,
Cron: cron,
},
StatusHook: statusHookURL,
}
jobID, err := s.jobserviceClient.SubmitJob(jd)
if err != nil {
// if failed to submit to Jobservice, delete the schedule record in database
e := s.manager.Delete(scheduleID)
if e != nil {
log.Errorf("failed to delete the schedule %d: %v", scheduleID, e)
}
return 0, err
}
log.Debugf("the scheduler job submitted to Jobservice, job ID: %s", jobID)
// populate the job ID for the schedule
err = s.manager.Update(&model.Schedule{
ID: scheduleID,
JobID: jobID,
}, "JobID")
if err != nil {
// stop the scheduler job
if e := s.jobserviceClient.PostAction(jobID, job.JobActionStop); e != nil {
log.Errorf("failed to stop the scheduler job %s: %v", jobID, e)
}
// delete the schedule record
if e := s.manager.Delete(scheduleID); e != nil {
log.Errorf("failed to delete the schedule record %d: %v", scheduleID, e)
}
return 0, err
}
return scheduleID, nil
}
func (s *scheduler) UnSchedule(id int64) error {
schedule, err := s.manager.Get(id)
if err != nil {
return err
}
if schedule == nil {
return fmt.Errorf("the schedule record %d not found", id)
}
if err = s.jobserviceClient.PostAction(schedule.JobID, job.JobActionStop); err != nil {
herr, ok := err.(*chttp.Error)
// if the job specified by jobID is not found in Jobservice, just delete
// the schedule record
if !ok || herr.Code != http.StatusNotFound {
return err
}
}
log.Debugf("the stop action for job %s submitted to the Jobservice", schedule.JobID)
if err = s.manager.Delete(schedule.ID); err != nil {
return err
}
log.Debugf("the schedule record %d deleted", schedule.ID)
return nil
}

View File

@ -0,0 +1,115 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scheduler
import (
"testing"
htesting "github.com/goharbor/harbor/src/testing"
"github.com/goharbor/harbor/src/testing/job"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
var sch *scheduler
type schedulerTestSuite struct {
suite.Suite
}
func (s *schedulerTestSuite) SetupTest() {
t := s.T()
// empty callback function registry before running every test case
// and register a new callback function named "callback"
registry = make(map[string]CallbackFunc)
err := Register("callback", func(interface{}) error { return nil })
require.Nil(t, err)
// recreate the scheduler object
sch = &scheduler{
jobserviceClient: &job.MockJobClient{},
manager: &htesting.FakeSchedulerManager{},
}
}
func (s *schedulerTestSuite) TestRegister() {
t := s.T()
var name string
var callbackFun CallbackFunc
// empty name
err := Register(name, callbackFun)
require.NotNil(t, err)
// nil callback function
name = "test"
err = Register(name, callbackFun)
require.NotNil(t, err)
// pass
callbackFun = func(interface{}) error { return nil }
err = Register(name, callbackFun)
require.Nil(t, err)
// duplicate name
err = Register(name, callbackFun)
require.NotNil(t, err)
}
func (s *schedulerTestSuite) TestGetCallbackFunc() {
t := s.T()
// not exist
_, err := GetCallbackFunc("not-exist")
require.NotNil(t, err)
// pass
f, err := GetCallbackFunc("callback")
require.Nil(t, err)
assert.NotNil(t, f)
}
func (s *schedulerTestSuite) TestSchedule() {
t := s.T()
// callback function not exist
_, err := sch.Schedule("0 * * * * *", "not-exist", nil)
require.NotNil(t, err)
// pass
id, err := sch.Schedule("0 * * * * *", "callback", nil)
require.Nil(t, err)
assert.Equal(t, int64(1), id)
}
func (s *schedulerTestSuite) TestUnSchedule() {
t := s.T()
// schedule not exist
err := sch.UnSchedule(1)
require.NotNil(t, err)
// schedule exist
id, err := sch.Schedule("0 * * * * *", "callback", nil)
require.Nil(t, err)
assert.Equal(t, int64(1), id)
err = sch.UnSchedule(id)
require.Nil(t, err)
}
func TestScheduler(t *testing.T) {
s := &schedulerTestSuite{}
suite.Run(t, s)
}

View File

@ -0,0 +1,44 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package clients
import (
"github.com/goharbor/harbor/src/chartserver"
"github.com/goharbor/harbor/src/common/models"
)
// DumbCoreClient provides an empty implement for pkg/clients/core.Client
// it is only used for testing
type DumbCoreClient struct{}
// ListAllImages ...
func (d *DumbCoreClient) ListAllImages(project, repository string) ([]*models.TagResp, error) {
return nil, nil
}
// DeleteImage ...
func (d *DumbCoreClient) DeleteImage(project, repository, tag string) error {
return nil
}
// ListAllCharts ...
func (d *DumbCoreClient) ListAllCharts(project, repository string) ([]*chartserver.ChartVersion, error) {
return nil, nil
}
// DeleteChart ...
func (d *DumbCoreClient) DeleteChart(project, repository, version string) error {
return nil
}

View File

@ -5,8 +5,8 @@ import (
"math/rand"
"github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/common/job"
"github.com/goharbor/harbor/src/common/job/models"
"github.com/goharbor/harbor/src/jobservice/job"
)
// MockJobClient ...
@ -27,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
View File

@ -0,0 +1,77 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package testing
import (
"fmt"
"github.com/goharbor/harbor/src/pkg/scheduler/model"
)
// FakeSchedulerManager ...
type FakeSchedulerManager struct {
idCounter int64
Schedules []*model.Schedule
}
// Create ...
func (f *FakeSchedulerManager) Create(schedule *model.Schedule) (int64, error) {
f.idCounter++
id := f.idCounter
schedule.ID = id
f.Schedules = append(f.Schedules, schedule)
return id, nil
}
// Update ...
func (f *FakeSchedulerManager) Update(schedule *model.Schedule, props ...string) error {
for i, sch := range f.Schedules {
if sch.ID == schedule.ID {
f.Schedules[i] = schedule
return nil
}
}
return fmt.Errorf("the execution %d not found", schedule.ID)
}
// Delete ...
func (f *FakeSchedulerManager) Delete(id int64) error {
length := len(f.Schedules)
for i, sch := range f.Schedules {
if sch.ID == id {
f.Schedules = f.Schedules[:i]
if i != length-1 {
f.Schedules = append(f.Schedules, f.Schedules[i+1:]...)
}
return nil
}
}
return fmt.Errorf("the execution %d not found", id)
}
// Get ...
func (f *FakeSchedulerManager) Get(id int64) (*model.Schedule, error) {
for _, sch := range f.Schedules {
if sch.ID == id {
return sch, nil
}
}
return nil, fmt.Errorf("the execution %d not found", id)
}
// List ...
func (f *FakeSchedulerManager) List(...*model.ScheduleQuery) ([]*model.Schedule, error) {
return f.Schedules, nil
}

View File

@ -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{},
},
},

View File

@ -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")
}

View File

@ -1,11 +0,0 @@
// +build appengine plan9
package request
import (
"strings"
)
func isErrConnectionReset(err error) bool {
return strings.Contains(err.Error(), "connection reset")
}

View File

@ -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:

View File

@ -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