diff --git a/api/v2.0/swagger.yaml b/api/v2.0/swagger.yaml index 92fbd8d71..c688ecad7 100644 --- a/api/v2.0/swagger.yaml +++ b/api/v2.0/swagger.yaml @@ -4174,6 +4174,170 @@ paths: $ref: '#/responses/403' '500': $ref: '#/responses/500' + /system/purgeaudit: + get: + summary: Get purge job results. + description: get purge job execution history. + tags: + - purge + operationId: getPurgeHistory + parameters: + - $ref: '#/parameters/requestId' + - $ref: '#/parameters/query' + - $ref: '#/parameters/sort' + - $ref: '#/parameters/page' + - $ref: '#/parameters/pageSize' + responses: + '200': + description: Get purge job results successfully. + headers: + X-Total-Count: + description: The total count of history + type: integer + Link: + description: Link refers to the previous page and next page + type: string + schema: + type: array + items: + $ref: '#/definitions/ExecHistory' + '401': + $ref: '#/responses/401' + '403': + $ref: '#/responses/403' + '500': + $ref: '#/responses/500' + /system/purgeaudit/{purge_id}: + get: + summary: Get purge job status. + description: This endpoint let user get purge job status filtered by specific ID. + operationId: getPurgeJob + parameters: + - $ref: '#/parameters/requestId' + - $ref: '#/parameters/purgeId' + tags: + - purge + responses: + '200': + description: Get purge job results successfully. + schema: + $ref: '#/definitions/ExecHistory' + '401': + $ref: '#/responses/401' + '403': + $ref: '#/responses/403' + '404': + $ref: '#/responses/404' + '500': + $ref: '#/responses/500' + /system/purgeaudit/{purge_id}/log: + get: + summary: Get purge job log. + description: This endpoint let user get purge job logs filtered by specific ID. + operationId: getPurgeJobLog + parameters: + - $ref: '#/parameters/requestId' + - $ref: '#/parameters/purgeId' + tags: + - purge + produces: + - text/plain + responses: + '200': + description: Get successfully. + schema: + type: string + '400': + $ref: '#/responses/400' + '401': + $ref: '#/responses/401' + '403': + $ref: '#/responses/403' + '404': + $ref: '#/responses/404' + '500': + $ref: '#/responses/500' + /system/purgeaudit/schedule: + get: + summary: Get purge's schedule. + description: This endpoint is for get schedule of purge job. + operationId: getPurgeSchedule + tags: + - purge + parameters: + - $ref: '#/parameters/requestId' + responses: + '200': + description: Get purge job's schedule. + schema: + $ref: '#/definitions/ExecHistory' + '401': + $ref: '#/responses/401' + '403': + $ref: '#/responses/403' + '500': + $ref: '#/responses/500' + post: + summary: Create a purge job schedule. + description: | + This endpoint is for update purge job schedule. + operationId: createPurgeSchedule + parameters: + - $ref: '#/parameters/requestId' + - name: schedule + in: body + required: true + schema: + $ref: '#/definitions/Schedule' + description: | + The purge job's schedule, it is a json object. | + The sample format is | + {"parameters":{"audit_retention_hour":168,"dry_run":true, "include_operations":"create,delete,pull"},"schedule":{"type":"Hourly","cron":"0 0 * * * *"}} | + the include_operation should be a comma separated string, e.g. create,delete,pull, if it is empty, no operation will be purged. + tags: + - purge + responses: + '201': + $ref: '#/responses/201' + '400': + $ref: '#/responses/400' + '401': + $ref: '#/responses/401' + '403': + $ref: '#/responses/403' + '500': + $ref: '#/responses/500' + put: + summary: Update purge job's schedule. + description: | + This endpoint is for update purge job schedule. + operationId: updatePurgeSchedule + parameters: + - $ref: '#/parameters/requestId' + - name: schedule + in: body + required: true + schema: + $ref: '#/definitions/Schedule' + description: | + The purge job's schedule, it is a json object. | + The sample format is | + {"parameters":{"audit_retention_hour":168,"dry_run":true, "include_operations":"create,delete,pull"},"schedule":{"type":"Hourly","cron":"0 0 * * * *"}} | + the include_operation should be a comma separated string, e.g. create,delete,pull, if it is empty, no operation will be purged. + tags: + - purge + responses: + '200': + description: Updated purge's schedule successfully. + '400': + $ref: '#/responses/400' + '401': + $ref: '#/responses/401' + '403': + $ref: '#/responses/403' + '500': + $ref: '#/responses/500' + /system/CVEAllowlist: get: summary: Get the system level allowlist of CVE. @@ -5523,6 +5687,13 @@ parameters: required: true type: integer format: int64 + purgeId: + name: purge_id + in: path + description: The ID of the purge log + required: true + type: integer + format: int64 labelId: name: label_id in: path @@ -7224,6 +7395,37 @@ definitions: type: string format: date-time description: the update time of gc job. + ExecHistory: + type: object + properties: + id: + type: integer + description: the id of purge job. + job_name: + type: string + description: the job name of purge job. + job_kind: + type: string + description: the job kind of purge job. + job_parameters: + type: string + description: the job parameters of purge job. + schedule: + $ref: '#/definitions/ScheduleObj' + job_status: + type: string + description: the status of purge job. + deleted: + type: boolean + description: if purge job was deleted. + creation_time: + type: string + format: date-time + description: the creation time of purge job. + update_time: + type: string + format: date-time + description: the update time of purge job. Schedule: type: object properties: diff --git a/src/controller/jobservice/model.go b/src/controller/jobservice/model.go new file mode 100644 index 000000000..cc1e57ca0 --- /dev/null +++ b/src/controller/jobservice/model.go @@ -0,0 +1,28 @@ +// 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 jobservice + +import "time" + +// Execution model for replication +type Execution struct { + ID int64 + Status string + StatusMessage string + Trigger string + ExtraAttrs map[string]interface{} + StartTime time.Time + EndTime time.Time +} diff --git a/src/jobservice/runtime/bootstrap.go b/src/jobservice/runtime/bootstrap.go index b5d00267f..98042cd9e 100644 --- a/src/jobservice/runtime/bootstrap.go +++ b/src/jobservice/runtime/bootstrap.go @@ -308,8 +308,8 @@ func (bs *Bootstrap) loadAndRunRedisWorkerPool( job.SampleJob: (*sample.Job)(nil), // Functional jobs job.ImageScanJob: (*scan.Job)(nil), - job.GarbageCollection: (*gc.GarbageCollector)(nil), job.PurgeAudit: (*purge.Job)(nil), + job.GarbageCollection: (*gc.GarbageCollector)(nil), job.Replication: (*replication.Replication)(nil), job.Retention: (*retention.Job)(nil), scheduler.JobNameScheduler: (*scheduler.PeriodicJob)(nil), diff --git a/src/lib/strings.go b/src/lib/strings.go index 7d7f6cff6..adf1210ad 100644 --- a/src/lib/strings.go +++ b/src/lib/strings.go @@ -14,7 +14,11 @@ package lib -import "strings" +import ( + "golang.org/x/text/cases" + "golang.org/x/text/language" + "strings" +) // TrimsLineBreaks trims line breaks in string. func TrimLineBreaks(s string) string { @@ -22,3 +26,9 @@ func TrimLineBreaks(s string) string { escaped = strings.ReplaceAll(escaped, "\r", "") return escaped } + +// Title uppercase the first character, and lower case the rest, for example covert MANUAL to Manual +func Title(s string) string { + title := cases.Title(language.Und) + return title.String(strings.ToLower(s)) +} diff --git a/src/lib/strings_test.go b/src/lib/strings_test.go index 0b9416369..bcb4bfa48 100644 --- a/src/lib/strings_test.go +++ b/src/lib/strings_test.go @@ -32,3 +32,23 @@ def actual := TrimLineBreaks(s) assert.Equal(expect, actual, "should trim line breaks") } + +func TestTitle(t *testing.T) { + type args struct { + s string + } + tests := []struct { + name string + args args + want string + }{ + {"upper case", args{"MANUAL"}, "Manual"}, + {"lower case", args{"manual"}, "Manual"}, + {"empty", args{""}, ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, Title(tt.args.s), "Title(%v)", tt.args.s) + }) + } +} diff --git a/src/pkg/scheduler/scheduler.go b/src/pkg/scheduler/scheduler.go index f0b667c92..38ce54df8 100644 --- a/src/pkg/scheduler/scheduler.go +++ b/src/pkg/scheduler/scheduler.go @@ -114,7 +114,13 @@ func (s *scheduler) Schedule(ctx context.Context, vendorType string, vendorID in return 0, err } sched.CallbackFuncParam = string(paramsData) - + params := map[string]interface{}{} + if len(paramsData) > 0 { + err = json.Unmarshal(paramsData, ¶ms) + if err != nil { + log.Debugf("current paramsData is not a json string") + } + } extrasData, err := json.Marshal(extraAttrs) if err != nil { return 0, err @@ -129,7 +135,7 @@ func (s *scheduler) Schedule(ctx context.Context, vendorType string, vendorID in return 0, err } - execID, err := s.execMgr.Create(ctx, JobNameScheduler, id, task.ExecutionTriggerManual) + execID, err := s.execMgr.Create(ctx, JobNameScheduler, id, task.ExecutionTriggerManual, params) if err != nil { return 0, err } diff --git a/src/pkg/scheduler/scheduler_test.go b/src/pkg/scheduler/scheduler_test.go index a3782fad5..44e9f003c 100644 --- a/src/pkg/scheduler/scheduler_test.go +++ b/src/pkg/scheduler/scheduler_test.go @@ -66,7 +66,7 @@ func (s *schedulerTestSuite) TestSchedule() { // failed to submit to jobservice s.dao.On("Create", mock.Anything, mock.Anything).Return(int64(1), nil) - s.execMgr.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil) + s.execMgr.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil) s.taskMgr.On("Create", mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil) s.taskMgr.On("Get", mock.Anything, mock.Anything).Return(&task.Task{ ID: 1, @@ -84,7 +84,7 @@ func (s *schedulerTestSuite) TestSchedule() { s.SetupTest() // pass - s.execMgr.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil) + s.execMgr.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil) s.dao.On("Create", mock.Anything, mock.Anything).Return(int64(1), nil) s.taskMgr.On("Create", mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil) s.taskMgr.On("Get", mock.Anything, mock.Anything).Return(&task.Task{ diff --git a/src/server/v2.0/handler/handler.go b/src/server/v2.0/handler/handler.go index 6e347a071..e4a141741 100644 --- a/src/server/v2.0/handler/handler.go +++ b/src/server/v2.0/handler/handler.go @@ -64,6 +64,7 @@ func New() http.Handler { HealthAPI: newHealthAPI(), StatisticAPI: newStatisticAPI(), ProjectMetadataAPI: newProjectMetadaAPI(), + PurgeAPI: newPurgeAPI(), }) if err != nil { log.Fatal(err) diff --git a/src/server/v2.0/handler/model/jobservice.go b/src/server/v2.0/handler/model/jobservice.go new file mode 100644 index 000000000..5a9409eea --- /dev/null +++ b/src/server/v2.0/handler/model/jobservice.go @@ -0,0 +1,57 @@ +// 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/go-openapi/strfmt" + "github.com/goharbor/harbor/src/lib" + "github.com/goharbor/harbor/src/server/v2.0/models" + "strings" + "time" +) + +// ExecHistory execution history +type ExecHistory struct { + Schedule *ScheduleParam `json:"schedule"` + ID int64 `json:"id"` + Name string `json:"job_name"` + Kind string `json:"job_kind"` + Parameters string `json:"job_parameters"` + Status string `json:"job_status"` + UUID string `json:"-"` + Deleted bool `json:"deleted"` + CreationTime time.Time `json:"creation_time"` + UpdateTime time.Time `json:"update_time"` +} + +// ToSwagger converts the history to the swagger model +func (h *ExecHistory) ToSwagger() *models.ExecHistory { + return &models.ExecHistory{ + ID: h.ID, + JobName: h.Name, + JobKind: h.Kind, + JobParameters: h.Parameters, + Deleted: h.Deleted, + JobStatus: h.Status, + Schedule: &models.ScheduleObj{ + // covert MANUAL to Manual because the type of the ScheduleObj + // must be 'Hourly', 'Daily', 'Weekly', 'Custom', 'Manual' and 'None' + Type: lib.Title(strings.ToLower(h.Schedule.Type)), + Cron: h.Schedule.Cron, + }, + CreationTime: strfmt.DateTime(h.CreationTime), + UpdateTime: strfmt.DateTime(h.UpdateTime), + } +} diff --git a/src/server/v2.0/handler/purge.go b/src/server/v2.0/handler/purge.go new file mode 100644 index 000000000..ef8ed76c5 --- /dev/null +++ b/src/server/v2.0/handler/purge.go @@ -0,0 +1,301 @@ +// 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 handler + +import ( + "context" + "encoding/json" + "fmt" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/strfmt" + "github.com/goharbor/harbor/src/common" + "github.com/goharbor/harbor/src/common/rbac" + "github.com/goharbor/harbor/src/controller/jobservice" + pg "github.com/goharbor/harbor/src/controller/purge" + "github.com/goharbor/harbor/src/controller/task" + "github.com/goharbor/harbor/src/lib/errors" + "github.com/goharbor/harbor/src/lib/q" + taskPkg "github.com/goharbor/harbor/src/pkg/task" + "github.com/goharbor/harbor/src/server/v2.0/handler/model" + "github.com/goharbor/harbor/src/server/v2.0/models" + "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/purge" + operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/purge" + "path" +) + +type purgeAPI struct { + BaseAPI + purgeCtr pg.Controller + schedulerCtl jobservice.SchedulerController + taskCtl task.Controller + executionCtl task.ExecutionController +} + +func newPurgeAPI() *purgeAPI { + return &purgeAPI{ + purgeCtr: pg.Ctrl, + schedulerCtl: jobservice.SchedulerCtl, + taskCtl: task.Ctl, + executionCtl: task.ExecutionCtl, + } +} + +func (p *purgeAPI) CreatePurgeSchedule(ctx context.Context, params purge.CreatePurgeScheduleParams) middleware.Responder { + if err := p.RequireSystemAccess(ctx, rbac.ActionCreate, rbac.ResourcePurgeAuditLog); err != nil { + return p.SendError(ctx, err) + } + if err := verifyCreateRequest(params); err != nil { + return p.SendError(ctx, err) + } + id, err := p.kick(ctx, pg.VendorType, params.Schedule.Schedule.Type, params.Schedule.Schedule.Cron, params.Schedule.Parameters) + if err != nil { + return p.SendError(ctx, err) + } + location := path.Join(params.HTTPRequest.URL.Path, fmt.Sprintf("../%d", id)) + return purge.NewCreatePurgeScheduleCreated().WithLocation(location) +} + +func verifyCreateRequest(params purge.CreatePurgeScheduleParams) error { + if params.Schedule == nil || params.Schedule.Schedule == nil { + return errors.BadRequestError(fmt.Errorf("schedule cann't be empty")) + } + if len(params.Schedule.Parameters) == 0 { + return errors.BadRequestError(fmt.Errorf("schedule parameter cann't be empty")) + } + if _, exist := params.Schedule.Parameters[common.PurgeAuditRetentionHour]; !exist { + return errors.BadRequestError(fmt.Errorf("audit_retention_hour should provide")) + } + if _, exist := params.Schedule.Parameters[common.PurgeAuditIncludeOperations]; !exist { + return errors.BadRequestError(fmt.Errorf("include_operations should provide")) + } + return nil +} + +func (p *purgeAPI) kick(ctx context.Context, vendorType string, scheType string, cron string, parameters map[string]interface{}) (int64, error) { + if parameters == nil { + parameters = make(map[string]interface{}) + } + var err error + var id int64 + + policy := pg.JobPolicy{ + ExtraAttrs: parameters, + } + if dryRun, ok := parameters[common.PurgeAuditDryRun].(bool); ok { + policy.DryRun = dryRun + } + if includeOperations, ok := parameters[common.PurgeAuditIncludeOperations].(string); ok { + policy.IncludeOperations = includeOperations + } + if retentionHour, ok := parameters[common.PurgeAuditRetentionHour]; ok { + if rh, ok := retentionHour.(json.Number); ok { + ret, err := rh.Int64() + if err != nil { + return 0, errors.BadRequestError(fmt.Errorf("failed to convert audit_retention_hour, error: %v", err)) + } + policy.RetentionHour = int(ret) + } + } + + switch scheType { + case ScheduleManual: + id, err = p.purgeCtr.Start(ctx, policy, taskPkg.ExecutionTriggerManual) + case ScheduleNone: + // delete the schedule of purge + err = p.schedulerCtl.Delete(ctx, vendorType) + case ScheduleHourly, ScheduleDaily, ScheduleWeekly, ScheduleCustom: + err = p.updateSchedule(ctx, vendorType, scheType, cron, policy, parameters) + } + return id, err +} + +func (p *purgeAPI) updateSchedule(ctx context.Context, vendorType, cronType, cron string, policy pg.JobPolicy, extraParams map[string]interface{}) error { + if err := p.schedulerCtl.Delete(ctx, vendorType); err != nil { + return err + } + return p.createSchedule(ctx, vendorType, cronType, cron, policy, extraParams) +} + +func (p *purgeAPI) GetPurgeHistory(ctx context.Context, params purge.GetPurgeHistoryParams) middleware.Responder { + if err := p.RequireSystemAccess(ctx, rbac.ActionList, rbac.ResourcePurgeAuditLog); err != nil { + return p.SendError(ctx, err) + } + query, err := p.BuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize) + query.Keywords["VendorType"] = pg.VendorType + if err != nil { + return p.SendError(ctx, err) + } + total, err := p.executionCtl.Count(ctx, query) + if err != nil { + return p.SendError(ctx, err) + } + execs, err := p.executionCtl.List(ctx, query) + if err != nil { + p.SendError(ctx, err) + } + + var hs []*model.ExecHistory + for _, exec := range execs { + extraAttrsString, err := json.Marshal(exec.ExtraAttrs) + if err != nil { + return p.SendError(ctx, err) + } + hs = append(hs, &model.ExecHistory{ + ID: exec.ID, + Name: pg.VendorType, + Kind: exec.Trigger, + Parameters: string(extraAttrsString), + Schedule: &model.ScheduleParam{ + Type: exec.Trigger, + }, + Status: exec.Status, + CreationTime: exec.StartTime, + UpdateTime: exec.EndTime, + }) + } + var results []*models.ExecHistory + for _, h := range hs { + results = append(results, h.ToSwagger()) + } + + return operation.NewGetPurgeHistoryOK(). + WithXTotalCount(total). + WithLink(p.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String()). + WithPayload(results) +} + +func (p *purgeAPI) GetPurgeJob(ctx context.Context, params purge.GetPurgeJobParams) middleware.Responder { + if err := p.RequireSystemAccess(ctx, rbac.ActionRead, rbac.ResourcePurgeAuditLog); err != nil { + return p.SendError(ctx, err) + } + + exec, err := p.executionCtl.Get(ctx, params.PurgeID) + if err != nil { + return p.SendError(ctx, err) + } + + extraAttrsString, err := json.Marshal(exec.ExtraAttrs) + if err != nil { + return p.SendError(ctx, err) + } + + res := &model.ExecHistory{ + ID: exec.ID, + Name: pg.VendorType, + Kind: exec.Trigger, + Parameters: string(extraAttrsString), + Status: exec.Status, + Schedule: &model.ScheduleParam{ + Type: exec.Trigger, + }, + CreationTime: exec.StartTime, + UpdateTime: exec.EndTime, + } + + return operation.NewGetPurgeJobOK().WithPayload(res.ToSwagger()) +} + +func (p *purgeAPI) GetPurgeJobLog(ctx context.Context, params purge.GetPurgeJobLogParams) middleware.Responder { + if err := p.RequireSystemAccess(ctx, rbac.ActionRead, rbac.ResourcePurgeAuditLog); err != nil { + return p.SendError(ctx, err) + } + tasks, err := p.taskCtl.List(ctx, q.New(q.KeyWords{ + "ExecutionID": params.PurgeID, + "VendorType": pg.VendorType, + })) + if err != nil { + return p.SendError(ctx, err) + } + if len(tasks) == 0 { + return p.SendError(ctx, + errors.New(nil).WithCode(errors.NotFoundCode). + WithMessage("purge job with execution ID: %d taskLog is not found", params.PurgeID)) + } + taskLog, err := p.taskCtl.GetLog(ctx, tasks[0].ID) + if err != nil { + return p.SendError(ctx, err) + } + return operation.NewGetPurgeJobLogOK().WithPayload(string(taskLog)) +} + +func (p *purgeAPI) GetPurgeSchedule(ctx context.Context, params purge.GetPurgeScheduleParams) middleware.Responder { + if err := p.RequireSystemAccess(ctx, rbac.ActionRead, rbac.ResourcePurgeAuditLog); err != nil { + return p.SendError(ctx, err) + } + sch, err := p.schedulerCtl.Get(ctx, pg.VendorType) + if errors.IsNotFoundErr(err) { + return operation.NewGetPurgeScheduleOK() + } + if err != nil { + return p.SendError(ctx, err) + } + execHistory := &models.ExecHistory{ + ID: sch.ID, + JobName: "", + JobKind: sch.CRON, + JobParameters: pg.String(sch.ExtraAttrs), + Deleted: false, + JobStatus: sch.Status, + Schedule: &models.ScheduleObj{ + Cron: sch.CRON, + Type: sch.CRONType, + }, + CreationTime: strfmt.DateTime(sch.CreationTime), + UpdateTime: strfmt.DateTime(sch.UpdateTime), + } + return operation.NewGetPurgeScheduleOK().WithPayload(execHistory) +} + +func (p *purgeAPI) UpdatePurgeSchedule(ctx context.Context, params purge.UpdatePurgeScheduleParams) middleware.Responder { + if err := p.RequireSystemAccess(ctx, rbac.ActionUpdate, rbac.ResourcePurgeAuditLog); err != nil { + return p.SendError(ctx, err) + } + if err := verifyUpdateRequest(params); err != nil { + return p.SendError(ctx, err) + } + _, err := p.kick(ctx, pg.VendorType, params.Schedule.Schedule.Type, params.Schedule.Schedule.Cron, params.Schedule.Parameters) + if err != nil { + return p.SendError(ctx, err) + } + return operation.NewUpdatePurgeScheduleOK() +} + +func verifyUpdateRequest(params operation.UpdatePurgeScheduleParams) error { + if params.Schedule == nil || params.Schedule.Schedule == nil { + return errors.BadRequestError(fmt.Errorf("schedule cann't be empty")) + } + if len(params.Schedule.Parameters) == 0 { + return errors.BadRequestError(fmt.Errorf("schedule parameter cann't be empty")) + } + if _, exist := params.Schedule.Parameters[common.PurgeAuditRetentionHour]; !exist { + return errors.BadRequestError(fmt.Errorf("audit_retention_hour should provide")) + } + if _, exist := params.Schedule.Parameters[common.PurgeAuditIncludeOperations]; !exist { + return errors.BadRequestError(fmt.Errorf("include_operations should provide")) + } + return nil +} + +func (p *purgeAPI) createSchedule(ctx context.Context, vendorType string, cronType string, cron string, policy pg.JobPolicy, extraParam map[string]interface{}) error { + if cron == "" { + return errors.New(nil).WithCode(errors.BadRequestCode). + WithMessage("empty cron string for schedule") + } + _, err := p.schedulerCtl.Create(ctx, vendorType, cronType, cron, pg.SchedulerCallback, policy, extraParam) + if err != nil { + return err + } + return nil +} diff --git a/src/server/v2.0/handler/purge_test.go b/src/server/v2.0/handler/purge_test.go new file mode 100644 index 000000000..a56bab523 --- /dev/null +++ b/src/server/v2.0/handler/purge_test.go @@ -0,0 +1,69 @@ +// 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 handler + +import ( + "github.com/goharbor/harbor/src/common" + "github.com/goharbor/harbor/src/server/v2.0/models" + "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/purge" + "testing" +) + +func Test_verifyUpdateRequest(t *testing.T) { + type args struct { + params purge.UpdatePurgeScheduleParams + } + tests := []struct { + name string + args args + wantErr bool + }{ + {"normal", args{purge.UpdatePurgeScheduleParams{Schedule: &models.Schedule{Schedule: &models.ScheduleObj{}, Parameters: map[string]interface{}{common.PurgeAuditRetentionHour: "168", common.PurgeAuditIncludeOperations: "pull"}}}}, false}, + {"missing_schedule", args{purge.UpdatePurgeScheduleParams{Schedule: &models.Schedule{Parameters: map[string]interface{}{common.PurgeAuditRetentionHour: "168", common.PurgeAuditIncludeOperations: "pull"}}}}, true}, + {"missing_retention_hour", args{purge.UpdatePurgeScheduleParams{Schedule: &models.Schedule{Schedule: &models.ScheduleObj{}, Parameters: map[string]interface{}{common.PurgeAuditIncludeOperations: "pull"}}}}, true}, + {"missing_operations", args{purge.UpdatePurgeScheduleParams{Schedule: &models.Schedule{Schedule: &models.ScheduleObj{}, Parameters: map[string]interface{}{common.PurgeAuditRetentionHour: "168"}}}}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := verifyUpdateRequest(tt.args.params) + if tt.wantErr != (err != nil) { + t.Error("test failed") + } + }) + } +} +func Test_verifyCreateRequest(t *testing.T) { + type args struct { + params purge.CreatePurgeScheduleParams + } + tests := []struct { + name string + args args + wantErr bool + }{ + {"normal", args{purge.CreatePurgeScheduleParams{Schedule: &models.Schedule{Schedule: &models.ScheduleObj{}, Parameters: map[string]interface{}{common.PurgeAuditRetentionHour: "168", common.PurgeAuditIncludeOperations: "pull"}}}}, false}, + {"missing_schedule", args{purge.CreatePurgeScheduleParams{Schedule: &models.Schedule{Parameters: map[string]interface{}{common.PurgeAuditRetentionHour: "168", common.PurgeAuditIncludeOperations: "pull"}}}}, true}, + {"missing_retention_hour", args{purge.CreatePurgeScheduleParams{Schedule: &models.Schedule{Schedule: &models.ScheduleObj{}, Parameters: map[string]interface{}{common.PurgeAuditIncludeOperations: "pull"}}}}, true}, + {"missing_operations", args{purge.CreatePurgeScheduleParams{Schedule: &models.Schedule{Schedule: &models.ScheduleObj{}, Parameters: map[string]interface{}{common.PurgeAuditRetentionHour: "168"}}}}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := verifyCreateRequest(tt.args.params) + if tt.wantErr != (err != nil) { + t.Error("test failed") + } + }) + } +} diff --git a/src/testing/controller/controller.go b/src/testing/controller/controller.go index d847a857b..b74ca58d5 100644 --- a/src/testing/controller/controller.go +++ b/src/testing/controller/controller.go @@ -29,3 +29,4 @@ package controller //go:generate mockery --case snake --dir ../../controller/user --name Controller --output ./user --outpkg user //go:generate mockery --case snake --dir ../../controller/repository --name Controller --output ./repository --outpkg repository //go:generate mockery --case snake --dir ../../controller/purge --name Controller --output ./purge --outpkg purge +//go:generate mockery --case snake --dir ../../controller/jobservice --name SchedulerController --output ./jobservice --outpkg jobservice diff --git a/src/testing/controller/jobservice/execution_controller.go b/src/testing/controller/jobservice/execution_controller.go new file mode 100644 index 000000000..b8dcec468 --- /dev/null +++ b/src/testing/controller/jobservice/execution_controller.go @@ -0,0 +1,84 @@ +// Code generated by mockery v2.1.0. DO NOT EDIT. + +package jobservice + +import ( + context "context" + + jobservice "github.com/goharbor/harbor/src/controller/jobservice" + mock "github.com/stretchr/testify/mock" + + q "github.com/goharbor/harbor/src/lib/q" +) + +// ExecutionController is an autogenerated mock type for the ExecutionController type +type ExecutionController struct { + mock.Mock +} + +// Count provides a mock function with given fields: ctx, vendorType, query +func (_m *ExecutionController) Count(ctx context.Context, vendorType string, query *q.Query) (int64, error) { + ret := _m.Called(ctx, vendorType, query) + + var r0 int64 + if rf, ok := ret.Get(0).(func(context.Context, string, *q.Query) int64); ok { + r0 = rf(ctx, vendorType, query) + } else { + r0 = ret.Get(0).(int64) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, *q.Query) error); ok { + r1 = rf(ctx, vendorType, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Get provides a mock function with given fields: ctx, vendorType, executionID +func (_m *ExecutionController) Get(ctx context.Context, vendorType string, executionID int64) (*jobservice.Execution, error) { + ret := _m.Called(ctx, vendorType, executionID) + + var r0 *jobservice.Execution + if rf, ok := ret.Get(0).(func(context.Context, string, int64) *jobservice.Execution); ok { + r0 = rf(ctx, vendorType, executionID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*jobservice.Execution) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, int64) error); ok { + r1 = rf(ctx, vendorType, executionID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// List provides a mock function with given fields: ctx, vendorType, query +func (_m *ExecutionController) List(ctx context.Context, vendorType string, query *q.Query) ([]*jobservice.Execution, error) { + ret := _m.Called(ctx, vendorType, query) + + var r0 []*jobservice.Execution + if rf, ok := ret.Get(0).(func(context.Context, string, *q.Query) []*jobservice.Execution); ok { + r0 = rf(ctx, vendorType, query) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*jobservice.Execution) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, *q.Query) error); ok { + r1 = rf(ctx, vendorType, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/src/testing/controller/jobservice/scheduler_controller.go b/src/testing/controller/jobservice/scheduler_controller.go new file mode 100644 index 000000000..49351ab74 --- /dev/null +++ b/src/testing/controller/jobservice/scheduler_controller.go @@ -0,0 +1,74 @@ +// Code generated by mockery v2.1.0. DO NOT EDIT. + +package jobservice + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + scheduler "github.com/goharbor/harbor/src/pkg/scheduler" +) + +// SchedulerController is an autogenerated mock type for the SchedulerController type +type SchedulerController struct { + mock.Mock +} + +// Create provides a mock function with given fields: ctx, vendorType, cronType, cron, callbackFuncName, policy, extrasParam +func (_m *SchedulerController) Create(ctx context.Context, vendorType string, cronType string, cron string, callbackFuncName string, policy interface{}, extrasParam map[string]interface{}) (int64, error) { + ret := _m.Called(ctx, vendorType, cronType, cron, callbackFuncName, policy, extrasParam) + + var r0 int64 + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, interface{}, map[string]interface{}) int64); ok { + r0 = rf(ctx, vendorType, cronType, cron, callbackFuncName, policy, extrasParam) + } else { + r0 = ret.Get(0).(int64) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, string, string, string, interface{}, map[string]interface{}) error); ok { + r1 = rf(ctx, vendorType, cronType, cron, callbackFuncName, policy, extrasParam) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Delete provides a mock function with given fields: ctx, vendorType +func (_m *SchedulerController) Delete(ctx context.Context, vendorType string) error { + ret := _m.Called(ctx, vendorType) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, vendorType) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Get provides a mock function with given fields: ctx, vendorType +func (_m *SchedulerController) Get(ctx context.Context, vendorType string) (*scheduler.Schedule, error) { + ret := _m.Called(ctx, vendorType) + + var r0 *scheduler.Schedule + if rf, ok := ret.Get(0).(func(context.Context, string) *scheduler.Schedule); ok { + r0 = rf(ctx, vendorType) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*scheduler.Schedule) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, vendorType) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/src/testing/controller/jobservice/task_controller.go b/src/testing/controller/jobservice/task_controller.go new file mode 100644 index 000000000..49e3d51cf --- /dev/null +++ b/src/testing/controller/jobservice/task_controller.go @@ -0,0 +1,86 @@ +// Code generated by mockery v2.1.0. DO NOT EDIT. + +package jobservice + +import ( + context "context" + + jobservice "github.com/goharbor/harbor/src/controller/jobservice" + mock "github.com/stretchr/testify/mock" + + q "github.com/goharbor/harbor/src/lib/q" +) + +// TaskController is an autogenerated mock type for the TaskController type +type TaskController struct { + mock.Mock +} + +// Get provides a mock function with given fields: ctx, vendorType, id +func (_m *TaskController) Get(ctx context.Context, vendorType string, id int64) (*jobservice.Task, error) { + ret := _m.Called(ctx, vendorType, id) + + var r0 *jobservice.Task + if rf, ok := ret.Get(0).(func(context.Context, string, int64) *jobservice.Task); ok { + r0 = rf(ctx, vendorType, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*jobservice.Task) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, int64) error); ok { + r1 = rf(ctx, vendorType, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetLog provides a mock function with given fields: ctx, vendorType, id +func (_m *TaskController) GetLog(ctx context.Context, vendorType string, id int64) ([]byte, error) { + ret := _m.Called(ctx, vendorType, id) + + var r0 []byte + if rf, ok := ret.Get(0).(func(context.Context, string, int64) []byte); ok { + r0 = rf(ctx, vendorType, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, int64) error); ok { + r1 = rf(ctx, vendorType, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// List provides a mock function with given fields: ctx, vendorType, query +func (_m *TaskController) List(ctx context.Context, vendorType string, query *q.Query) ([]*jobservice.Task, error) { + ret := _m.Called(ctx, vendorType, query) + + var r0 []*jobservice.Task + if rf, ok := ret.Get(0).(func(context.Context, string, *q.Query) []*jobservice.Task); ok { + r0 = rf(ctx, vendorType, query) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*jobservice.Task) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, *q.Query) error); ok { + r1 = rf(ctx, vendorType, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +}