Merge pull request #8302 from ywk253100/190711_client

Implement the retention client
This commit is contained in:
Steven Zou 2019-07-18 09:52:57 +08:00 committed by GitHub
commit 5c840803bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 622 additions and 70 deletions

View File

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

View File

@ -17,7 +17,6 @@ package api
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/goharbor/harbor/src/pkg/scan"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"sort" "sort"
@ -25,6 +24,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/goharbor/harbor/src/pkg/scan"
"errors" "errors"
"github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema1"
@ -96,7 +97,8 @@ type cfg struct {
Labels map[string]string `json:"labels"` Labels map[string]string `json:"labels"`
} }
type tagResp struct { // TagResp holds the information of one image tag
type TagResp struct {
tagDetail tagDetail
Signature *notary.Target `json:"signature"` Signature *notary.Target `json:"signature"`
ScanOverview *models.ImgScanOverview `json:"scan_overview,omitempty"` ScanOverview *models.ImgScanOverview `json:"scan_overview,omitempty"`
@ -608,7 +610,7 @@ func (ra *RepositoryAPI) GetTags() {
// get config, signature and scan overview and assemble them into one // get config, signature and scan overview and assemble them into one
// struct for each tag in tags // struct for each tag in tags
func assembleTagsInParallel(client *registry.Repository, repository string, func assembleTagsInParallel(client *registry.Repository, repository string,
tags []string, username string) []*tagResp { tags []string, username string) []*TagResp {
var err error var err error
signatures := map[string][]notary.Target{} signatures := map[string][]notary.Target{}
if config.WithNotary() { if config.WithNotary() {
@ -619,13 +621,13 @@ func assembleTagsInParallel(client *registry.Repository, repository string,
} }
} }
c := make(chan *tagResp) c := make(chan *TagResp)
for _, tag := range tags { for _, tag := range tags {
go assembleTag(c, client, repository, tag, config.WithClair(), go assembleTag(c, client, repository, tag, config.WithClair(),
config.WithNotary(), signatures) config.WithNotary(), signatures)
} }
result := []*tagResp{} result := []*TagResp{}
var item *tagResp var item *TagResp
for i := 0; i < len(tags); i++ { for i := 0; i < len(tags); i++ {
item = <-c item = <-c
if item == nil { if item == nil {
@ -636,10 +638,10 @@ func assembleTagsInParallel(client *registry.Repository, repository string,
return result return result
} }
func assembleTag(c chan *tagResp, client *registry.Repository, func assembleTag(c chan *TagResp, client *registry.Repository,
repository, tag string, clairEnabled, notaryEnabled bool, repository, tag string, clairEnabled, notaryEnabled bool,
signatures map[string][]notary.Target) { signatures map[string][]notary.Target) {
item := &tagResp{} item := &TagResp{}
// labels // labels
image := fmt.Sprintf("%s:%s", repository, tag) image := fmt.Sprintf("%s:%s", repository, tag)
labels, err := dao.GetLabelsOfResource(common.ResourceTypeImage, image) labels, err := dao.GetLabelsOfResource(common.ResourceTypeImage, image)

View File

@ -96,7 +96,7 @@ func TestGetReposTags(t *testing.T) {
t.Errorf("failed to get tags of repository %s: %v", repository, err) t.Errorf("failed to get tags of repository %s: %v", repository, err)
} else { } else {
assert.Equal(int(200), code, "httpStatusCode should be 200") assert.Equal(int(200), code, "httpStatusCode should be 200")
if tg, ok := tags.([]tagResp); ok { if tg, ok := tags.([]TagResp); ok {
assert.Equal(1, len(tg), fmt.Sprintf("there should be only one tag, but now %v", tg)) assert.Equal(1, len(tg), fmt.Sprintf("there should be only one tag, but now %v", tg))
assert.Equal(tg[0].Name, "latest", "the tag should be latest") assert.Equal(tg[0].Name, "latest", "the tag should be latest")
} else { } else {

View File

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

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,62 @@
// 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/chartserver"
chttp "github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/common/http/modifier"
"github.com/goharbor/harbor/src/core/api"
)
// Client defines the methods that a core client should implement
// 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) ([]*api.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/core/api"
)
func (c *client) ListAllImages(project, repository string) ([]*api.TagResp, error) {
url := c.buildURL(fmt.Sprintf("/api/repositories/%s/%s/tags", project, repository))
var images []*api.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

@ -15,6 +15,16 @@
package retention package retention
import ( import (
"errors"
"fmt"
"net/http"
"github.com/goharbor/harbor/src/common/http/modifier/auth"
cjob "github.com/goharbor/harbor/src/common/job"
"github.com/goharbor/harbor/src/common/job/models"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/pkg/clients/core"
"github.com/goharbor/harbor/src/pkg/retention/policy" "github.com/goharbor/harbor/src/pkg/retention/policy"
"github.com/goharbor/harbor/src/pkg/retention/res" "github.com/goharbor/harbor/src/pkg/retention/res"
) )
@ -43,36 +53,137 @@ type Client interface {
// SubmitTask to jobservice // SubmitTask to jobservice
// //
// Arguments: // Arguments:
// repository: *res.Repository : repository info // taskID : the ID of task
// repository *res.Repository : repository info
// meta *policy.LiteMeta : policy lite metadata // meta *policy.LiteMeta : policy lite metadata
// //
// Returns: // Returns:
// string : the job ID // string : the job ID
// error : common error if any errors occurred // error : common error if any errors occurred
SubmitTask(repository *res.Repository, meta *policy.LiteMeta) (string, error) SubmitTask(taskID int64, repository *res.Repository, meta *policy.LiteMeta) (string, error)
} }
// New basic client // New basic client
func New() Client { func New(client ...*http.Client) Client {
return &basicClient{} var c *http.Client
if len(client) > 0 {
c = client[0]
}
if c == nil {
c = http.DefaultClient
}
// init core client
internalCoreURL := config.InternalCoreURL()
jobserviceSecret := config.JobserviceSecret()
authorizer := auth.NewSecretAuthorizer(jobserviceSecret)
coreClient := core.New(internalCoreURL, c, authorizer)
// init jobservice client
internalJobserviceURL := config.InternalJobServiceURL()
coreSecret := config.CoreSecret()
jobserviceClient := cjob.NewDefaultClient(internalJobserviceURL, coreSecret)
return &basicClient{
internalCoreURL: internalCoreURL,
coreClient: coreClient,
jobserviceClient: jobserviceClient,
}
} }
// basicClient is a default // basicClient is a default
type basicClient struct{} type basicClient struct {
internalCoreURL string
coreClient core.Client
jobserviceClient cjob.Client
}
// GetCandidates gets the tag candidates under the repository // GetCandidates gets the tag candidates under the repository
func (bc *basicClient) GetCandidates(repo *res.Repository) ([]*res.Candidate, error) { func (bc *basicClient) GetCandidates(repository *res.Repository) ([]*res.Candidate, error) {
results := make([]*res.Candidate, 0) if repository == nil {
return nil, errors.New("repository is nil")
return results, nil }
candidates := make([]*res.Candidate, 0)
switch repository.Kind {
case CandidateKindImage:
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: CandidateKindImage,
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 CandidateKindChart:
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: CandidateKindChart,
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 // Deletes the specified candidate
func (bc *basicClient) Delete(candidate *res.Candidate) error { func (bc *basicClient) Delete(candidate *res.Candidate) error {
return nil if candidate == nil {
return errors.New("candidate is nil")
}
switch candidate.Kind {
case CandidateKindImage:
return bc.coreClient.DeleteImage(candidate.Namespace, candidate.Repository, candidate.Tag)
case CandidateKindChart:
return bc.coreClient.DeleteChart(candidate.Namespace, candidate.Repository, candidate.Tag)
default:
return fmt.Errorf("unsupported candidate kind: %s", candidate.Kind)
}
} }
// SubmitTask to jobservice // SubmitTask to jobservice
func (bc *basicClient) SubmitTask(*res.Repository, *policy.LiteMeta) (string, error) { func (bc *basicClient) SubmitTask(taskID int64, repository *res.Repository, meta *policy.LiteMeta) (string, error) {
return "", nil j := &models.JobData{
Metadata: &models.JobMetadata{
JobKind: job.KindGeneric,
},
StatusHook: fmt.Sprintf("%s/service/notifications/jobs/retention/tasks/%d", bc.internalCoreURL, taskID),
}
j.Name = job.Retention
j.Parameters = map[string]interface{}{
ParamRepo: repository,
ParamMeta: meta,
}
return bc.jobserviceClient.SubmitJob(j)
} }

View File

@ -0,0 +1,142 @@
// 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 (
"testing"
"github.com/goharbor/harbor/src/chartserver"
"github.com/goharbor/harbor/src/common/job/models"
"github.com/goharbor/harbor/src/core/api"
"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) ([]*api.TagResp, error) {
image := &api.TagResp{}
image.Name = "latest"
return []*api.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(*models.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 = CandidateKindImage
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(), CandidateKindImage, 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 = CandidateKindChart
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(), CandidateKindChart, 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 = CandidateKindImage
err = client.Delete(candidate)
require.Nil(c.T(), err)
// chart
candidate.Kind = CandidateKindChart
err = client.Delete(candidate)
require.Nil(c.T(), err)
// unsupported type
candidate.Kind = "unsupported"
err = client.Delete(candidate)
require.NotNil(c.T(), err)
}
func (c *clientTestSuite) TestSubmitTask() {
client := &basicClient{}
client.jobserviceClient = &fakeJobserviceClient{}
jobID, err := client.SubmitTask(1, nil, nil)
require.Nil(c.T(), err)
assert.Equal(c.T(), "1", jobID)
}
func TestClientTestSuite(t *testing.T) {
suite.Run(t, new(clientTestSuite))
}

View File

@ -28,7 +28,10 @@ import (
) )
// TODO init the client // TODO init the client
var client Client var (
client Client
mgr Manager
)
// Launcher provides function to launch the async jobs to run retentions based on the provided policy. // Launcher provides function to launch the async jobs to run retentions based on the provided policy.
type Launcher interface { type Launcher interface {
@ -37,11 +40,12 @@ type Launcher interface {
// //
// Arguments: // Arguments:
// policy *policy.Metadata: the policy info // policy *policy.Metadata: the policy info
// executionID int64 : the execution ID
// //
// Returns: // Returns:
// []*TaskSubmitResult : the submit results of tasks // int64 : the count of tasks
// error : common error if any errors occurred // error : common error if any errors occurred
Launch(policy *policy.Metadata) ([]*TaskSubmitResult, error) Launch(policy *policy.Metadata, executionID int64) (int64, error)
} }
// NewLauncher returns an instance of Launcher // NewLauncher returns an instance of Launcher
@ -52,17 +56,23 @@ func NewLauncher() Launcher {
type launcher struct { type launcher struct {
} }
func (l *launcher) Launch(ply *policy.Metadata) ([]*TaskSubmitResult, error) { type jobData struct {
repository *res.Repository
policy *policy.LiteMeta
taskID int64
}
func (l *launcher) Launch(ply *policy.Metadata, executionID int64) (int64, error) {
if ply == nil { if ply == nil {
return nil, launcherError(fmt.Errorf("the policy is nil")) return 0, launcherError(fmt.Errorf("the policy is nil"))
} }
// no rules, return directly // no rules, return directly
if len(ply.Rules) == 0 { if len(ply.Rules) == 0 {
return nil, nil return 0, nil
} }
scope := ply.Scope scope := ply.Scope
if scope == nil { if scope == nil {
return nil, launcherError(fmt.Errorf("the scope of policy is nil")) return 0, launcherError(fmt.Errorf("the scope of policy is nil"))
} }
repositoryRules := make(map[res.Repository]*policy.LiteMeta, 0) repositoryRules := make(map[res.Repository]*policy.LiteMeta, 0)
@ -73,7 +83,7 @@ func (l *launcher) Launch(ply *policy.Metadata) ([]*TaskSubmitResult, error) {
// get projects // get projects
projectCandidates, err = getProjects() projectCandidates, err = getProjects()
if err != nil { if err != nil {
return nil, launcherError(err) return 0, launcherError(err)
} }
} }
@ -85,11 +95,11 @@ func (l *launcher) Launch(ply *policy.Metadata) ([]*TaskSubmitResult, error) {
selector, err := selectors.Get(projectSelector.Kind, projectSelector.Decoration, selector, err := selectors.Get(projectSelector.Kind, projectSelector.Decoration,
projectSelector.Pattern) projectSelector.Pattern)
if err != nil { if err != nil {
return nil, launcherError(err) return 0, launcherError(err)
} }
projectCandidates, err = selector.Select(projectCandidates) projectCandidates, err = selector.Select(projectCandidates)
if err != nil { if err != nil {
return nil, launcherError(err) return 0, launcherError(err)
} }
} }
case "project": case "project":
@ -103,7 +113,7 @@ func (l *launcher) Launch(ply *policy.Metadata) ([]*TaskSubmitResult, error) {
for _, projectCandidate := range projectCandidates { for _, projectCandidate := range projectCandidates {
repositories, err := getRepositories(projectCandidate.NamespaceID) repositories, err := getRepositories(projectCandidate.NamespaceID)
if err != nil { if err != nil {
return nil, launcherError(err) return 0, launcherError(err)
} }
repositoryCandidates = append(repositoryCandidates, repositories...) repositoryCandidates = append(repositoryCandidates, repositories...)
} }
@ -112,11 +122,11 @@ func (l *launcher) Launch(ply *policy.Metadata) ([]*TaskSubmitResult, error) {
selector, err := selectors.Get(repositorySelector.Kind, repositorySelector.Decoration, selector, err := selectors.Get(repositorySelector.Kind, repositorySelector.Decoration,
repositorySelector.Pattern) repositorySelector.Pattern)
if err != nil { if err != nil {
return nil, launcherError(err) return 0, launcherError(err)
} }
repositoryCandidates, err = selector.Select(repositoryCandidates) repositoryCandidates, err = selector.Select(repositoryCandidates)
if err != nil { if err != nil {
return nil, launcherError(err) return 0, launcherError(err)
} }
} }
@ -134,19 +144,40 @@ func (l *launcher) Launch(ply *policy.Metadata) ([]*TaskSubmitResult, error) {
repositoryRules[repository].Rules = append(repositoryRules[repository].Rules, &rule) repositoryRules[repository].Rules = append(repositoryRules[repository].Rules, &rule)
} }
} }
// no tasks need to be submitted
if len(repositoryRules) == 0 {
return 0, nil
}
var result []*TaskSubmitResult // create task records
for repository, rule := range repositoryRules { jobDatas := []*jobData{}
jobID, err := client.SubmitTask(&repository, rule) for repository, policy := range repositoryRules {
result = append(result, &TaskSubmitResult{ taskID, err := mgr.CreateTask(&Task{
JobID: jobID, ExecutionID: executionID,
Error: err,
}) })
if err != nil { if err != nil {
log.Error(launcherError(fmt.Errorf("failed to submit task: %v", err))) return 0, launcherError(err)
} }
jobDatas = append(jobDatas, &jobData{
repository: &repository,
policy: policy,
taskID: taskID,
})
} }
return result, nil
allFailed := true
for _, jobData := range jobDatas {
_, err := client.SubmitTask(jobData.taskID, jobData.repository, jobData.policy)
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 { func launcherError(err error) error {

View File

@ -19,18 +19,18 @@ import (
"strconv" "strconv"
"testing" "testing"
"github.com/stretchr/testify/suite"
"github.com/goharbor/harbor/src/chartserver" "github.com/goharbor/harbor/src/chartserver"
"github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/pkg/project" "github.com/goharbor/harbor/src/pkg/project"
"github.com/goharbor/harbor/src/pkg/repository" "github.com/goharbor/harbor/src/pkg/repository"
"github.com/goharbor/harbor/src/pkg/retention/policy" "github.com/goharbor/harbor/src/pkg/retention/policy"
"github.com/goharbor/harbor/src/pkg/retention/policy/rule" "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" "github.com/goharbor/harbor/src/pkg/retention/res"
_ "github.com/goharbor/harbor/src/pkg/retention/res/selectors/regexp" _ "github.com/goharbor/harbor/src/pkg/retention/res/selectors/regexp"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
) )
type fakeProjectManager struct { type fakeProjectManager struct {
@ -85,11 +85,56 @@ func (f *fakeClient) GetCandidates(repo *res.Repository) ([]*res.Candidate, erro
func (f *fakeClient) Delete(candidate *res.Candidate) error { func (f *fakeClient) Delete(candidate *res.Candidate) error {
return nil return nil
} }
func (f *fakeClient) SubmitTask(repository *res.Repository, meta *policy.LiteMeta) (string, error) { func (f *fakeClient) SubmitTask(taskID int64, repository *res.Repository, meta *policy.LiteMeta) (string, error) {
f.id++ f.id++
return strconv.Itoa(f.id), nil return strconv.Itoa(f.id), 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) DeletePolicy(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.Query) ([]*Task, error) {
return nil, nil
}
func (f *fakeRetentionManager) CreateTask(task *Task) (int64, error) {
return 0, nil
}
func (f *fakeRetentionManager) UpdateTask(task *Task) error {
return nil
}
func (f *fakeRetentionManager) GetTaskLog(taskID int64) ([]byte, error) {
return nil, nil
}
func (f *fakeRetentionManager) ListExecutions(query *q.Query) ([]*Execution, error) {
return nil, nil
}
func (f *fakeRetentionManager) AppendHistory(history *History) error {
return nil
}
func (f *fakeRetentionManager) ListHistories(executionID int64, query *q.Query) ([]*History, error) {
return nil, nil
}
type launchTestSuite struct { type launchTestSuite struct {
suite.Suite suite.Suite
} }
@ -116,6 +161,7 @@ func (l *launchTestSuite) SetupTest() {
}, },
} }
client = &fakeClient{} client = &fakeClient{}
mgr = &fakeRetentionManager{}
} }
func (l *launchTestSuite) TestGetProjects() { func (l *launchTestSuite) TestGetProjects() {
@ -142,14 +188,14 @@ func (l *launchTestSuite) TestLaunch() {
launcher := NewLauncher() launcher := NewLauncher()
var ply *policy.Metadata var ply *policy.Metadata
// nil policy // nil policy
result, err := launcher.Launch(ply) n, err := launcher.Launch(ply, 1)
require.NotNil(l.T(), err) require.NotNil(l.T(), err)
// nil rules // nil rules
ply = &policy.Metadata{} ply = &policy.Metadata{}
result, err = launcher.Launch(ply) n, err = launcher.Launch(ply, 1)
require.Nil(l.T(), err) require.Nil(l.T(), err)
assert.Equal(l.T(), 0, len(result)) assert.Equal(l.T(), int64(0), n)
// nil scope // nil scope
ply = &policy.Metadata{ ply = &policy.Metadata{
@ -157,7 +203,7 @@ func (l *launchTestSuite) TestLaunch() {
{}, {},
}, },
} }
_, err = launcher.Launch(ply) _, err = launcher.Launch(ply, 1)
require.NotNil(l.T(), err) require.NotNil(l.T(), err)
// system scope // system scope
@ -186,14 +232,9 @@ func (l *launchTestSuite) TestLaunch() {
}, },
}, },
} }
n, err = launcher.Launch(ply, 1)
result, err = launcher.Launch(ply)
require.Nil(l.T(), err) require.Nil(l.T(), err)
assert.Equal(l.T(), 2, len(result)) assert.Equal(l.T(), int64(2), n)
assert.Equal(l.T(), "1", result[0].JobID)
assert.Nil(l.T(), result[0].Error)
assert.Equal(l.T(), "2", result[1].JobID)
assert.Nil(l.T(), result[1].Error)
} }
func TestLaunchTestSuite(t *testing.T) { func TestLaunchTestSuite(t *testing.T) {

View File

@ -16,16 +16,17 @@ package retention
import ( import (
"encoding/json" "encoding/json"
"time"
"github.com/goharbor/harbor/src/pkg/retention/dao" "github.com/goharbor/harbor/src/pkg/retention/dao"
"github.com/goharbor/harbor/src/pkg/retention/dao/models" "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"
"github.com/goharbor/harbor/src/pkg/retention/q" "github.com/goharbor/harbor/src/pkg/retention/q"
"time"
) )
// Manager defines operations of managing policy // Manager defines operations of managing policy
type Manager interface { type Manager interface {
// Create new policy and return uuid // Create new policy and return ID
CreatePolicy(p *policy.Metadata) (int64, error) CreatePolicy(p *policy.Metadata) (int64, error)
// Update the existing policy // Update the existing policy
// Full update // Full update
@ -41,6 +42,14 @@ type Manager interface {
UpdateExecution(execution *Execution) error UpdateExecution(execution *Execution) error
// Get the specified execution // Get the specified execution
GetExecution(eid int64) (*Execution, error) GetExecution(eid int64) (*Execution, error)
// List tasks histories
ListTasks(query *q.Query) ([]*Task, error)
// Create a new retention task
CreateTask(task *Task) (int64, error)
// Update the specified task
UpdateTask(task *Task) error
// Get the log of the specified task
GetTaskLog(taskID int64) ([]byte, error)
// List execution histories // List execution histories
ListExecutions(query *q.Query) ([]*Execution, error) ListExecutions(query *q.Query) ([]*Execution, error)
// Add new history // Add new history
@ -150,6 +159,26 @@ func (d *DefaultManager) GetExecution(eid int64) (*Execution, error) {
return e1, nil return e1, nil
} }
// CreateTask creates task record
func (d *DefaultManager) CreateTask(task *Task) (int64, error) {
panic("implement me")
}
// ListTasks lists tasks according to the query
func (d *DefaultManager) ListTasks(query *q.Query) ([]*Task, error) {
panic("implement me")
}
// UpdateTask updates the task
func (d *DefaultManager) UpdateTask(task *Task) error {
panic("implement me")
}
// GetTaskLog gets the logs of task
func (d *DefaultManager) GetTaskLog(taskID int64) ([]byte, error) {
panic("implement me")
}
// ListHistories List Histories // ListHistories List Histories
func (d *DefaultManager) ListHistories(executionID int64, query *q.Query) ([]*History, error) { func (d *DefaultManager) ListHistories(executionID int64, query *q.Query) ([]*History, error) {
his, err := dao.ListExecHistories(executionID, query) his, err := dao.ListExecHistories(executionID, query)

View File

@ -16,21 +16,39 @@ package retention
import "time" 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"
)
// Execution of retention // Execution of retention
type Execution struct { type Execution struct {
ID int64 `json:"id,omitempty"` ID int64 `json:"id"`
PolicyID int64 `json:"policy_id"` PolicyID int64 `json:"policy_id"`
StartTime time.Time `json:"start_time"` StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time,omitempty"` EndTime time.Time `json:"end_time,omitempty"`
Status string `json:"status"` Status string `json:"status"`
} }
// TaskSubmitResult is the result of task submitting // Task of retention
// If the task is submitted successfully, JobID will be set type Task struct {
// and the Error is nil ID int64 `json:"id"`
type TaskSubmitResult struct { ExecutionID int64 `json:"execution_id"`
JobID string Status string `json:"status"`
Error error StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
} }
// History of retention // History of retention

View File

@ -25,8 +25,8 @@ const (
// Metadata of policy // Metadata of policy
type Metadata struct { type Metadata struct {
// UUID of the policy // ID of the policy
ID int64 `json:"id,omitempty"` ID int64 `json:"id"`
// Algorithm applied to the rules // Algorithm applied to the rules
// "OR" / "AND" // "OR" / "AND"

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/core/api"
)
// 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) ([]*api.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
}