refactor: remove code of admin job (#13819)

Remove code of admin job as it's not needed by scan all/gc now.

Signed-off-by: He Weiwei <hweiwei@vmware.com>
This commit is contained in:
He Weiwei 2020-12-22 11:48:16 +08:00 committed by GitHub
parent 8fa03e3739
commit 3831e82b20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 41 additions and 1381 deletions

View File

@ -1,5 +1,5 @@
/*
Fixes issue https://github.com/goharbor/harbor/issues/13317
Fixes issue https://github.com/goharbor/harbor/issues/13317
Ensure the role_id of maintainer is 4 and the role_id of limited guest is 5
*/
UPDATE role SET role_id=4 WHERE name='maintainer' AND role_id!=4;
@ -159,7 +159,7 @@ BEGIN
ELSIF rep_exec.status = 'Succeed' THEN
status = 'Success';
END IF;
INSERT INTO execution (vendor_type, vendor_id, status, status_message, revision, trigger, start_time, end_time)
VALUES ('REPLICATION', rep_exec.policy_id, status, rep_exec.status_text, 0, trigger, rep_exec.start_time, rep_exec.end_time) RETURNING id INTO new_exec_id;
UPDATE replication_execution SET new_execution_id=new_exec_id WHERE id=rep_exec.id;
@ -455,6 +455,9 @@ BEGIN
END LOOP;
END $$;
/* admin_job no more needed, drop it */
DROP TABLE IF EXISTS admin_job;
/*migrate robot_token_duration from minutes to days if exist*/
DO $$
DECLARE

View File

@ -1,160 +0,0 @@
// 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 (
"time"
"github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/lib/log"
)
// AddAdminJob ...
func AddAdminJob(job *models.AdminJob) (int64, error) {
o := GetOrmer()
if len(job.Status) == 0 {
job.Status = models.JobPending
}
sql := "insert into admin_job (job_name, job_parameters, job_kind, status, job_uuid, cron_str, creation_time, update_time) values (?, ?, ?, ?, ?, ?, ?, ?) RETURNING id"
var id int64
now := time.Now()
err := o.Raw(sql, job.Name, job.Parameters, job.Kind, job.Status, job.UUID, job.Cron, now, now).QueryRow(&id)
if err != nil {
return 0, err
}
return id, nil
}
// GetAdminJob ...
func GetAdminJob(id int64) (*models.AdminJob, error) {
o := GetOrmer()
aj := models.AdminJob{ID: id}
err := o.Read(&aj)
if err == orm.ErrNoRows {
return nil, err
}
return &aj, nil
}
// DeleteAdminJob ...
func DeleteAdminJob(id int64) error {
o := GetOrmer()
_, err := o.Raw(`update admin_job
set deleted = true where id = ?`, id).Exec()
return err
}
// UpdateAdminJobStatus ...
func UpdateAdminJobStatus(id int64, status string, statusCode uint16, revision int64) error {
o := GetOrmer()
qt := o.QueryTable(&models.AdminJob{})
// The generated sql statement example:{
//
// UPDATE "admin_job" SET "update_time" = $1, "status" = $2, "status_code" = $3, "revision" = $4
// WHERE "id" IN ( SELECT T0."id" FROM "admin_job" T0 WHERE
// ( T0."revision" = $5 AND T0."status_code" < $6 ) OR ( T0."revision" < $7 )
// AND T0."id" = $8 )
//
// }
cond := orm.NewCondition()
c1 := cond.And("revision", revision).And("status_code__lt", statusCode)
c2 := cond.And("revision__lt", revision)
c := cond.AndCond(c1).OrCond(c2)
data := make(orm.Params)
data["status"] = status
data["status_code"] = statusCode
data["revision"] = revision
data["update_time"] = time.Now()
n, err := qt.SetCond(c).Filter("id", id).Update(data)
if n == 0 {
log.Warningf("no records are updated when updating admin job %d", id)
}
return err
}
// SetAdminJobUUID ...
func SetAdminJobUUID(id int64, uuid string) error {
o := GetOrmer()
j := models.AdminJob{
ID: id,
UUID: uuid,
}
n, err := o.Update(&j, "UUID")
if n == 0 {
log.Warningf("no records are updated when updating admin job %d", id)
}
return err
}
// GetTop10AdminJobsOfName ...
func GetTop10AdminJobsOfName(name string) ([]*models.AdminJob, error) {
o := GetOrmer()
jobs := []*models.AdminJob{}
n, err := o.Raw(`select * from admin_job
where deleted = false and job_name = ? order by id desc limit 10`, name).QueryRows(&jobs)
if err != nil {
return nil, err
}
if n == 0 {
return nil, nil
}
return jobs, err
}
// GetAdminJobs get admin jobs bases on query conditions
func GetAdminJobs(query *models.AdminJobQuery) ([]*models.AdminJob, error) {
adjs := []*models.AdminJob{}
qs := adminQueryConditions(query)
if query.Size > 0 {
qs = qs.Limit(query.Size)
if query.Page > 0 {
qs = qs.Offset((query.Page - 1) * query.Size)
}
}
_, err := qs.All(&adjs)
return adjs, err
}
// adminQueryConditions
func adminQueryConditions(query *models.AdminJobQuery) orm.QuerySeter {
qs := GetOrmer().QueryTable(&models.AdminJob{})
if query.ID > 0 {
qs = qs.Filter("ID", query.ID)
}
if len(query.Kind) > 0 {
qs = qs.Filter("Kind", query.Kind)
}
if len(query.Name) > 0 {
qs = qs.Filter("Name", query.Name)
}
if len(query.Status) > 0 {
qs = qs.Filter("Status", query.Status)
}
if len(query.UUID) > 0 {
qs = qs.Filter("UUID", query.UUID)
}
qs = qs.Filter("Deleted", false)
return qs.OrderBy("-ID")
}

View File

@ -1,145 +0,0 @@
// 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 (
"fmt"
"testing"
"github.com/goharbor/harbor/src/common/models"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
// AdminJobSuite is a test suite for testing admin job
type AdminJobSuite struct {
suite.Suite
job0 *models.AdminJob
ids []int64
}
// TestAdminJob is the entry point of AdminJobSuite
func TestAdminJob(t *testing.T) {
suite.Run(t, &AdminJobSuite{})
}
// SetupSuite prepares testing env for the suite
func (suite *AdminJobSuite) SetupSuite() {
job := &models.AdminJob{
Name: "job",
Kind: "jobKind",
}
job0 := &models.AdminJob{
Name: "GC",
Kind: "testKind",
Parameters: "{test:test}",
}
suite.ids = make([]int64, 0)
// add
id, err := AddAdminJob(job0)
require.NoError(suite.T(), err)
job0.ID = id
suite.job0 = job0
suite.ids = append(suite.ids, id)
id1, err := AddAdminJob(job)
require.NoError(suite.T(), err)
suite.ids = append(suite.ids, id1)
}
// TearDownSuite cleans testing env
func (suite *AdminJobSuite) TearDownSuite() {
for _, id := range suite.ids {
err := DeleteAdminJob(id)
suite.NoError(err, fmt.Sprintf("clear admin job: %d", id))
}
}
// TestAdminJobBase ...
func (suite *AdminJobSuite) TestAdminJobBase() {
// get
job1, err := GetAdminJob(suite.job0.ID)
require.Nil(suite.T(), err)
suite.Equal(job1.ID, suite.job0.ID)
suite.Equal(job1.Name, suite.job0.Name)
suite.Equal(job1.Parameters, suite.job0.Parameters)
// set uuid
err = SetAdminJobUUID(suite.job0.ID, "f5ef34f4cb3588d663176132")
require.Nil(suite.T(), err)
job3, err := GetAdminJob(suite.job0.ID)
require.Nil(suite.T(), err)
suite.Equal(job3.UUID, "f5ef34f4cb3588d663176132")
// get admin jobs
query := &models.AdminJobQuery{
Name: "job",
}
jobs, err := GetAdminJobs(query)
suite.Equal(len(jobs), 1)
// get top 10
jobs, _ = GetTop10AdminJobsOfName("job")
suite.Equal(len(jobs), 1)
}
// TestAdminJobUpdateStatus ...
func (suite *AdminJobSuite) TestAdminJobUpdateStatus() {
// update status
err := UpdateAdminJobStatus(suite.job0.ID, "testStatus", 1, 10000)
require.Nil(suite.T(), err)
job2, err := GetAdminJob(suite.job0.ID)
require.Nil(suite.T(), err)
suite.Equal(job2.Status, "testStatus")
// Update status with same rev
err = UpdateAdminJobStatus(suite.job0.ID, "testStatus3", 3, 10000)
require.Nil(suite.T(), err)
job3, err := GetAdminJob(suite.job0.ID)
require.Nil(suite.T(), err)
suite.Equal(job3.Status, "testStatus3")
// Update status with same rev, previous status
err = UpdateAdminJobStatus(suite.job0.ID, "testStatus2", 2, 10000)
require.Nil(suite.T(), err)
job4, err := GetAdminJob(suite.job0.ID)
require.Nil(suite.T(), err)
// No status change
suite.Equal(job4.Status, "testStatus3")
// Update status with previous rev
err = UpdateAdminJobStatus(suite.job0.ID, "testStatus4", 4, 9999)
require.Nil(suite.T(), err)
job5, err := GetAdminJob(suite.job0.ID)
require.Nil(suite.T(), err)
// No status change
suite.Equal(job5.Status, "testStatus3")
// Update status with latest rev
err = UpdateAdminJobStatus(suite.job0.ID, "testStatus", 1, 10001)
require.Nil(suite.T(), err)
job6, err := GetAdminJob(suite.job0.ID)
require.Nil(suite.T(), err)
suite.Equal(job6.Status, "testStatus")
}

View File

@ -1,81 +0,0 @@
// 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 models
import (
"fmt"
"time"
)
const (
// AdminJobTable is table name for admin job
AdminJobTable = "admin_job"
)
// AdminJob ...
type AdminJob struct {
ID int64 `orm:"pk;auto;column(id)" json:"id"`
Name string `orm:"column(job_name)" json:"job_name"`
Kind string `orm:"column(job_kind)" json:"job_kind"`
Parameters string `orm:"column(job_parameters)" json:"job_parameters"`
Cron string `orm:"column(cron_str)" json:"cron_str"`
Status string `orm:"column(status)" json:"job_status"`
UUID string `orm:"column(job_uuid)" json:"-"`
Revision int64 `orm:"column(revision)" json:"-"`
StatusCode uint16 `orm:"column(status_code)" json:"-"`
Deleted bool `orm:"column(deleted)" json:"deleted"`
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"`
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
}
// TableName is required by by beego orm to map AdminJob to table AdminJob
func (a *AdminJob) TableName() string {
return AdminJobTable
}
// AdminJobQuery : query parameters for adminjob
type AdminJobQuery struct {
ID int64
Name string
Kind string
Status string
UUID string
Deleted bool
Pagination
}
// ScheduleParam ...
type ScheduleParam struct {
Type string `json:"type"`
Weekday int8 `json:"weekday"`
Offtime int64 `json:"offtime"`
}
// ParseScheduleParamToCron ...
func ParseScheduleParamToCron(param *ScheduleParam) string {
if param == nil {
return ""
}
offtime := param.Offtime
offtime = offtime % (3600 * 24)
hour := int(offtime / 3600)
offtime = offtime % 3600
minute := int(offtime / 60)
second := int(offtime % 60)
if param.Type == "Weekly" {
return fmt.Sprintf("%d %d %d * * %d", second, minute, hour, param.Weekday%7)
}
return fmt.Sprintf("%d %d %d * * *", second, minute, hour)
}

View File

@ -29,7 +29,6 @@ func init() {
new(Label),
new(ResourceLabel),
new(UserGroup),
new(AdminJob),
new(JobLog),
new(OIDCUser),
new(NotificationPolicy),

View File

@ -4,6 +4,10 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/goharbor/harbor/src/controller/quota"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/pkg/scheduler"
@ -15,6 +19,14 @@ func init() {
if err != nil {
log.Fatalf("failed to registry GC call back, %v", err)
}
if err := task.RegisterTaskStatusChangePostFunc(job.ImageGC, gcTaskStatusChange); err != nil {
log.Fatalf("failed to register the task status change post for the gc job, error %v", err)
}
if err := task.RegisterTaskStatusChangePostFunc(job.ImageGCReadOnly, gcTaskStatusChange); err != nil {
log.Fatalf("failed to register the task status change post for the gc readonly job, error %v", err)
}
}
func gcCallback(ctx context.Context, p string) error {
@ -25,3 +37,13 @@ func gcCallback(ctx context.Context, p string) error {
_, err := Ctl.Start(orm.Context(), *param, task.ExecutionTriggerSchedule)
return err
}
func gcTaskStatusChange(ctx context.Context, taskID int64, status string) error {
if status == job.SuccessStatus.String() && config.QuotaPerProjectEnable() {
go func() {
quota.RefreshForProjects(orm.Context())
}()
}
return nil
}

View File

@ -1,364 +0,0 @@
// Copyright 2018 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 api
import (
"fmt"
"math"
"net/http"
"strconv"
"time"
"github.com/goharbor/harbor/src/common/dao"
common_http "github.com/goharbor/harbor/src/common/http"
common_job "github.com/goharbor/harbor/src/common/job"
common_models "github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/core/api/models"
utils_core "github.com/goharbor/harbor/src/core/utils"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
)
// AJAPI manages the CRUD of admin job and its schedule, any API wants to handle manual and cron job like ScanAll and GC cloud reuse it.
type AJAPI struct {
BaseController
}
// Prepare validates the URL and parms, it needs the system admin permission.
func (aj *AJAPI) Prepare() {
aj.BaseController.Prepare()
}
// updateSchedule update a schedule of admin job.
func (aj *AJAPI) updateSchedule(ajr models.AdminJobReq) {
if ajr.Schedule.Type == models.ScheduleManual {
aj.SendInternalServerError((fmt.Errorf("fail to update admin job schedule as wrong schedule type: %s", ajr.Schedule.Type)))
return
}
query := &common_models.AdminJobQuery{
Name: ajr.Name,
Kind: common_job.JobKindPeriodic,
}
jobs, err := dao.GetAdminJobs(query)
if err != nil {
aj.SendInternalServerError(err)
return
}
if len(jobs) != 1 {
aj.SendInternalServerError(errors.New("fail to update admin job schedule as we found more than one schedule in system, please ensure that only one schedule left for your job"))
return
}
// stop the scheduled job and remove it.
if err = utils_core.GetJobServiceClient().PostAction(jobs[0].UUID, common_job.JobActionStop); err != nil {
_, ok := err.(*common_job.StatusBehindError)
if !ok {
if e, ok := err.(*common_http.Error); !ok || e.Code != http.StatusNotFound {
aj.SendInternalServerError(err)
return
}
}
}
if err = dao.DeleteAdminJob(jobs[0].ID); err != nil {
aj.SendInternalServerError(err)
return
}
// Set schedule to None means to cancel the schedule, won't add new job.
if ajr.Schedule.Type != models.ScheduleNone {
aj.submit(&ajr)
}
}
// get get a execution of admin job by ID
func (aj *AJAPI) get(id int64) {
jobs, err := dao.GetAdminJobs(&common_models.AdminJobQuery{
ID: id,
})
if err != nil {
aj.SendInternalServerError(fmt.Errorf("failed to get admin jobs: %v", err))
return
}
if len(jobs) == 0 {
aj.SendNotFoundError(errors.New("no admin job found"))
return
}
adminJobRep, err := convertToAdminJobRep(jobs[0])
if err != nil {
aj.SendInternalServerError(fmt.Errorf("failed to convert admin job response: %v", err))
return
}
aj.Data["json"] = adminJobRep
aj.ServeJSON()
}
// list list all executions of admin job by name
func (aj *AJAPI) list(name string) {
jobs, err := dao.GetTop10AdminJobsOfName(name)
if err != nil {
aj.SendInternalServerError(fmt.Errorf("failed to get admin jobs: %v", err))
return
}
AdminJobReps := []*models.AdminJobRep{}
for _, job := range jobs {
AdminJobRep, err := convertToAdminJobRep(job)
if err != nil {
aj.SendInternalServerError(fmt.Errorf("failed to convert admin job response: %v", err))
return
}
AdminJobReps = append(AdminJobReps, &AdminJobRep)
}
aj.Data["json"] = AdminJobReps
aj.ServeJSON()
}
// getSchedule gets admin job schedule ...
func (aj *AJAPI) getSchedule(name string) {
result := models.AdminJobRep{}
jobs, err := dao.GetAdminJobs(&common_models.AdminJobQuery{
Name: name,
Kind: common_job.JobKindPeriodic,
})
if err != nil {
aj.SendInternalServerError(fmt.Errorf("failed to get admin jobs: %v", err))
return
}
if len(jobs) > 1 {
aj.SendInternalServerError(errors.New("get more than one scheduled admin job, make sure there has only one"))
return
}
if len(jobs) != 0 {
adminJobRep, err := convertToAdminJobRep(jobs[0])
if err != nil {
aj.SendInternalServerError(fmt.Errorf("failed to convert admin job response: %v", err))
return
}
result.Schedule = adminJobRep.Schedule
result.Parameters = adminJobRep.Parameters
}
aj.Data["json"] = result
aj.ServeJSON()
}
// getLog ...
func (aj *AJAPI) getLog(id int64) {
job, err := dao.GetAdminJob(id)
if err != nil {
log.Errorf("Failed to load job data for job: %d, error: %v", id, err)
aj.SendInternalServerError(errors.New("Failed to get Job data"))
return
}
if job == nil {
log.Errorf("Failed to get admin job: %d", id)
aj.SendNotFoundError(errors.New("Failed to get Job"))
return
}
var jobID string
// to get the latest execution job id, then to query job log.
if job.Kind == common_job.JobKindPeriodic {
exes, err := utils_core.GetJobServiceClient().GetExecutions(job.UUID)
if err != nil {
aj.SendInternalServerError(err)
return
}
if len(exes) == 0 {
aj.SendNotFoundError(errors.New("no execution log found"))
return
}
// get the latest terminal status execution.
for _, exe := range exes {
if exe.Info.Status == "Error" || exe.Info.Status == "Success" {
jobID = exe.Info.JobID
break
}
}
// no execution found
if jobID == "" {
aj.SendNotFoundError(errors.New("no execution log found"))
return
}
} else {
jobID = job.UUID
}
logBytes, err := utils_core.GetJobServiceClient().GetJobLog(jobID)
if err != nil {
if httpErr, ok := err.(*common_http.Error); ok {
aj.RenderError(httpErr.Code, "")
log.Errorf(fmt.Sprintf("failed to get log of job %d: %d %s",
id, httpErr.Code, httpErr.Message))
return
}
aj.SendInternalServerError(fmt.Errorf("Failed to get job logs, uuid: %s, error: %v", job.UUID, err))
return
}
aj.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Length"), strconv.Itoa(len(logBytes)))
aj.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Type"), "text/plain")
_, err = aj.Ctx.ResponseWriter.Write(logBytes)
if err != nil {
aj.SendInternalServerError(fmt.Errorf("Failed to write job logs, uuid: %s, error: %v", job.UUID, err))
}
}
// submit submits a job to job service per request
func (aj *AJAPI) submit(ajr *models.AdminJobReq) {
// when the schedule is saved as None without any schedule, just return 200 and do nothing.
if ajr.Schedule == nil || ajr.Schedule.Type == models.ScheduleNone {
return
}
// cannot post multiple schedule for admin job.
if ajr.IsPeriodic() {
jobs, err := dao.GetAdminJobs(&common_models.AdminJobQuery{
Name: ajr.Name,
Kind: common_job.JobKindPeriodic,
})
if err != nil {
aj.SendInternalServerError(fmt.Errorf("failed to get admin jobs: %v", err))
return
}
if len(jobs) != 0 {
aj.SendPreconditionFailedError(errors.New("fail to set schedule for admin job as always had one, please delete it firstly then to re-schedule"))
return
}
} else {
// So far, it should be a generic job for the manually trigger case.
// Only needs to care the 1st generic job.
// Check if there are still ongoing scan jobs triggered by the previous admin job.
// TODO: REPLACE WITH TASK MANAGER METHODS IN FUTURE
jb, err := aj.getLatestAdminJob(ajr.Name, common_job.JobKindGeneric)
if err != nil {
aj.SendInternalServerError(errors.Wrap(err, "AJAPI"))
return
}
if jb != nil {
// With a reasonable timeout duration
if jb.UpdateTime.Add(2 * time.Hour).After(time.Now()) {
if isOnGoing(jb.Status) {
err := errors.Errorf("reject job submitting: job %s with ID %d is %s", jb.Name, jb.ID, jb.Status)
aj.SendConflictError(errors.Wrap(err, "submit : AJAPI"))
return
}
}
}
}
id, err := dao.AddAdminJob(&common_models.AdminJob{
Name: ajr.Name,
Kind: ajr.JobKind(),
Cron: ajr.CronString(),
Parameters: ajr.ParamString(),
})
if err != nil {
aj.SendInternalServerError(err)
return
}
ajr.ID = id
job := ajr.ToJob()
// submit job to job service
log.Debugf("submitting admin job to job service")
uuid, err := utils_core.GetJobServiceClient().SubmitJob(job)
if err != nil {
if err := dao.DeleteAdminJob(id); err != nil {
log.Debugf("Failed to delete admin job, err: %v", err)
}
aj.ParseAndHandleError("failed to submit admin job", err)
return
}
if err := dao.SetAdminJobUUID(id, uuid); err != nil {
aj.SendInternalServerError(err)
return
}
}
func (aj *AJAPI) getLatestAdminJob(name, kind string) (*common_models.AdminJob, error) {
query := &common_models.AdminJobQuery{
Name: name,
Kind: kind,
}
query.Size = 1
query.Page = 1
jbs, err := dao.GetAdminJobs(query)
if err != nil {
return nil, err
}
if len(jbs) == 0 {
// Not exist
return nil, nil
}
// Return the latest one (with biggest ID)
return jbs[0], nil
}
func convertToAdminJobRep(job *common_models.AdminJob) (models.AdminJobRep, error) {
if job == nil {
return models.AdminJobRep{}, nil
}
AdminJobRep := models.AdminJobRep{
ID: job.ID,
Name: job.Name,
Kind: job.Kind,
Status: job.Status,
Parameters: job.Parameters,
CreationTime: job.CreationTime,
UpdateTime: job.UpdateTime,
}
if len(job.Cron) > 0 {
schedule, err := models.ConvertSchedule(job.Cron)
if err != nil {
return models.AdminJobRep{}, err
}
AdminJobRep.Schedule = &schedule
}
return AdminJobRep, nil
}
func progress(completed, total uint) string {
if total == 0 {
return fmt.Sprintf("0%s", "%")
}
v := float64(completed)
vv := float64(total)
p := (int)(math.Round((v / vv) * 100))
return fmt.Sprintf("%d%s", p, "%")
}
func isOnGoing(status string) bool {
return status == common_models.JobRunning ||
status == common_models.JobScheduled ||
status == common_models.JobPending
}

View File

@ -24,7 +24,6 @@ import (
"github.com/goharbor/harbor/src/common/config/metadata"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/security"
"github.com/goharbor/harbor/src/core/api/models"
corecfg "github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/lib/log"
)
@ -148,6 +147,13 @@ func checkUnmodifiable(mgr *config.CfgManager, cfgs map[string]interface{}, keys
return
}
// ScanAllPolicy is represent the json request and object for scan all policy
// Only for migrating from the legacy schedule.
type ScanAllPolicy struct {
Type string `json:"type"`
Param map[string]interface{} `json:"parameter,omitempty"`
}
// delete sensitive attrs and add editable field to every attr
func convertForGet(cfg map[string]interface{}) (map[string]*value, error) {
result := map[string]*value{}
@ -161,7 +167,7 @@ func convertForGet(cfg map[string]interface{}) (map[string]*value, error) {
}
if _, ok := cfg[common.ScanAllPolicy]; !ok {
cfg[common.ScanAllPolicy] = models.ScanAllPolicy{
cfg[common.ScanAllPolicy] = ScanAllPolicy{
Type: "none", // For legacy compatible
}
}

View File

@ -35,7 +35,6 @@ import (
"github.com/goharbor/harbor/src/common/job/test"
"github.com/goharbor/harbor/src/common/models"
testutils "github.com/goharbor/harbor/src/common/utils/test"
api_models "github.com/goharbor/harbor/src/core/api/models"
apimodels "github.com/goharbor/harbor/src/core/api/models"
_ "github.com/goharbor/harbor/src/core/auth/db"
_ "github.com/goharbor/harbor/src/core/auth/ldap"
@ -860,36 +859,6 @@ func (a testapi) DeleteMeta(authInfor usrInfo, projectID int64, name string) (in
return code, string(body), err
}
func (a testapi) AddScanAll(authInfor usrInfo, adminReq apilib.AdminJobReq) (int, error) {
_sling := sling.New().Post(a.basePath)
path := "/api/system/scanAll/schedule"
_sling = _sling.Path(path)
// body params
_sling = _sling.BodyJSON(adminReq)
var httpStatusCode int
var err error
httpStatusCode, _, err = request(_sling, jsonAcceptHeader, authInfor)
return httpStatusCode, err
}
func (a testapi) ScanAllScheduleGet(authInfo usrInfo) (int, api_models.AdminJobSchedule, error) {
_sling := sling.New().Get(a.basePath)
path := "/api/system/scanAll/schedule"
_sling = _sling.Path(path)
httpStatusCode, body, err := request(_sling, jsonAcceptHeader, authInfo)
var successPayLoad api_models.AdminJobSchedule
if 200 == httpStatusCode && nil == err {
err = json.Unmarshal(body, &successPayLoad)
}
return httpStatusCode, successPayLoad, err
}
func (a testapi) RegistryGet(authInfo usrInfo, registryID int64) (*model.Registry, int, error) {
_sling := sling.New().Base(a.basePath).Get(fmt.Sprintf("/api/registries/%d", registryID))
code, body, err := request(_sling, jsonAcceptHeader, authInfo)

View File

@ -1,209 +0,0 @@
// Copyright 2018 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 models
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/astaxie/beego/validation"
"github.com/goharbor/harbor/src/common/job"
"github.com/goharbor/harbor/src/common/job/models"
common_models "github.com/goharbor/harbor/src/common/models"
common_utils "github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/lib/log"
"github.com/robfig/cron"
)
const (
// ScheduleHourly : 'Hourly'
ScheduleHourly = "Hourly"
// ScheduleDaily : 'Daily'
ScheduleDaily = "Daily"
// ScheduleWeekly : 'Weekly'
ScheduleWeekly = "Weekly"
// ScheduleCustom : 'Custom'
ScheduleCustom = "Custom"
// ScheduleManual : 'Manual'
ScheduleManual = "Manual"
// ScheduleNone : 'None'
ScheduleNone = "None"
)
// AdminJobReq holds request information for admin job
type AdminJobReq struct {
AdminJobSchedule
Name string `json:"name"`
Status string `json:"status"`
ID int64 `json:"id"`
Parameters map[string]interface{} `json:"parameters"`
}
// AdminJobSchedule ...
type AdminJobSchedule struct {
Schedule *ScheduleParam `json:"schedule"`
}
// ScheduleParam defines the parameter of schedule trigger
type ScheduleParam struct {
// Daily, Weekly, Custom, Manual, None
Type string `json:"type"`
// The cron string of scheduled job
Cron string `json:"cron"`
}
// AdminJobRep holds the response of query admin job
type AdminJobRep struct {
AdminJobSchedule
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"`
}
// Valid validates the schedule type of a admin job request.
// Only scheduleHourly, ScheduleDaily, ScheduleWeekly, ScheduleCustom, ScheduleManual, ScheduleNone are accepted.
func (ar *AdminJobReq) Valid(v *validation.Validation) {
if ar.Schedule == nil {
return
}
switch ar.Schedule.Type {
case ScheduleHourly, ScheduleDaily, ScheduleWeekly, ScheduleCustom:
if _, err := cron.Parse(ar.Schedule.Cron); err != nil {
v.SetError("cron", fmt.Sprintf("Invalid schedule trigger parameter cron: %s", ar.Schedule.Cron))
}
case ScheduleManual, ScheduleNone:
default:
v.SetError("kind", fmt.Sprintf("Invalid schedule kind: %s", ar.Schedule.Type))
}
}
// ToJob converts request to a job recognized by job service.
func (ar *AdminJobReq) ToJob() *models.JobData {
metadata := &models.JobMetadata{
JobKind: ar.JobKind(),
Cron: ar.Schedule.Cron,
// GC job must be unique ...
IsUnique: true,
}
jobData := &models.JobData{
Name: ar.Name,
Parameters: ar.Parameters,
Metadata: metadata,
StatusHook: fmt.Sprintf("%s/service/notifications/jobs/adminjob/%d",
config.InternalCoreURL(), ar.ID),
}
// Append admin job ID as job parameter
if jobData.Parameters == nil {
jobData.Parameters = make(models.Parameters)
}
// As string
jobData.Parameters["admin_job_id"] = fmt.Sprintf("%d", ar.ID)
return jobData
}
// IsPeriodic ...
func (ar *AdminJobReq) IsPeriodic() bool {
return ar.JobKind() == job.JobKindPeriodic
}
// JobKind ...
func (ar *AdminJobReq) JobKind() string {
switch ar.Schedule.Type {
case ScheduleHourly, ScheduleDaily, ScheduleWeekly, ScheduleCustom:
return job.JobKindPeriodic
case ScheduleManual:
return job.JobKindGeneric
default:
return ""
}
}
// CronString ...
func (ar *AdminJobReq) CronString() string {
str, err := json.Marshal(ar.Schedule)
if err != nil {
log.Debugf("failed to marshal json error, %v", err)
return ""
}
return string(str)
}
// ParamString ...
func (ar *AdminJobReq) ParamString() string {
str, err := json.Marshal(ar.Parameters)
if err != nil {
log.Debugf("failed to marshal json error, %v", err)
return ""
}
return string(str)
}
// ConvertSchedule converts different kinds of cron string into one standard for UI to show.
// in the latest design, it uses {"type":"Daily","cron":"0 0 0 * * *"} as the cron item.
// As for supporting migration from older version, it needs to convert {"parameter":{"daily_time":0},"type":"daily"}
// and {"type":"Daily","weekday":0,"offtime":57600} into one standard.
func ConvertSchedule(cronStr string) (ScheduleParam, error) {
if cronStr == "" {
return ScheduleParam{}, nil
}
convertedSchedule := ScheduleParam{}
convertedSchedule.Type = "custom"
if strings.Contains(cronStr, "parameter") {
scheduleModel := ScanAllPolicy{}
if err := json.Unmarshal([]byte(cronStr), &scheduleModel); err != nil {
return ScheduleParam{}, err
}
h, m, s := common_utils.ParseOfftime(int64(scheduleModel.Param["daily_time"].(float64)))
cron := fmt.Sprintf("%d %d %d * * *", s, m, h)
convertedSchedule.Cron = cron
return convertedSchedule, nil
} else if strings.Contains(cronStr, "offtime") {
scheduleModel := common_models.ScheduleParam{}
if err := json.Unmarshal([]byte(cronStr), &scheduleModel); err != nil {
return ScheduleParam{}, err
}
convertedSchedule.Cron = common_models.ParseScheduleParamToCron(&scheduleModel)
return convertedSchedule, nil
} else if strings.Contains(cronStr, "cron") {
scheduleModel := ScheduleParam{}
if err := json.Unmarshal([]byte(cronStr), &scheduleModel); err != nil {
return ScheduleParam{}, err
}
return scheduleModel, nil
}
return ScheduleParam{}, fmt.Errorf("unsupported cron format, %s", cronStr)
}
// ScanAllPolicy is represent the json request and object for scan all policy
// Only for migrating from the legacy schedule.
type ScanAllPolicy struct {
Type string `json:"type"`
Param map[string]interface{} `json:"parameter,omitempty"`
}

View File

@ -1,169 +0,0 @@
// Copyright 2018 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 models
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/goharbor/harbor/src/common"
common_job "github.com/goharbor/harbor/src/common/job"
"github.com/goharbor/harbor/src/common/utils/test"
"github.com/goharbor/harbor/src/core/config"
"os"
"strings"
)
var testConfig = map[string]interface{}{
common.DefaultCoreEndpoint: "test",
}
func TestMain(m *testing.M) {
test.InitDatabaseFromEnv()
config.Init()
config.Upload(testConfig)
os.Exit(m.Run())
}
func TestToJob(t *testing.T) {
adminJobSchedule := AdminJobSchedule{
Schedule: &ScheduleParam{
Type: "Daily",
Cron: "20 3 0 * * *",
},
}
adminjob := &AdminJobReq{
Name: common_job.ImageGC,
AdminJobSchedule: adminJobSchedule,
}
job := adminjob.ToJob()
assert.Equal(t, job.Name, "IMAGE_GC")
assert.Equal(t, job.Metadata.JobKind, common_job.JobKindPeriodic)
assert.Equal(t, job.Metadata.Cron, "20 3 0 * * *")
}
func TestToJobManual(t *testing.T) {
adminJobSchedule := AdminJobSchedule{
Schedule: &ScheduleParam{
Type: "Manual",
},
}
adminjob := &AdminJobReq{
AdminJobSchedule: adminJobSchedule,
Name: common_job.ImageGC,
}
job := adminjob.ToJob()
assert.Equal(t, job.Name, "IMAGE_GC")
assert.Equal(t, job.Metadata.JobKind, common_job.JobKindGeneric)
}
func TestIsPeriodic(t *testing.T) {
adminJobSchedule := AdminJobSchedule{
Schedule: &ScheduleParam{
Type: "Daily",
Cron: "20 3 0 * * *",
},
}
adminjob := &AdminJobReq{
AdminJobSchedule: adminJobSchedule,
}
isPeriodic := adminjob.IsPeriodic()
assert.Equal(t, isPeriodic, true)
}
func TestJobKind(t *testing.T) {
adminJobSchedule := AdminJobSchedule{
Schedule: &ScheduleParam{
Type: "Daily",
Cron: "20 3 0 * * *",
},
}
adminjob := &AdminJobReq{
AdminJobSchedule: adminJobSchedule,
}
kind := adminjob.JobKind()
assert.Equal(t, kind, "Periodic")
adminJobSchedule1 := AdminJobSchedule{
Schedule: &ScheduleParam{
Type: "Manual",
},
}
adminjob1 := &AdminJobReq{
AdminJobSchedule: adminJobSchedule1,
}
kind1 := adminjob1.JobKind()
assert.Equal(t, kind1, "Generic")
}
func TestCronString(t *testing.T) {
adminJobSchedule := AdminJobSchedule{
Schedule: &ScheduleParam{
Type: "Daily",
Cron: "20 3 0 * * *",
},
}
adminjob := &AdminJobReq{
AdminJobSchedule: adminJobSchedule,
}
cronStr := adminjob.CronString()
assert.True(t, strings.EqualFold(cronStr, "{\"type\":\"Daily\",\"Cron\":\"20 3 0 * * *\"}"))
}
func TestParamString(t *testing.T) {
adminJobPara := make(map[string]interface{})
adminJobPara["key1"] = "value1"
adminJobPara["key2"] = true
adminJobPara["key3"] = 88
adminjob := &AdminJobReq{
Parameters: adminJobPara,
}
paramStr := adminjob.ParamString()
assert.True(t, strings.EqualFold(paramStr, "{\"key1\":\"value1\",\"key2\":true,\"key3\":88}"))
}
func TestConvertSchedule(t *testing.T) {
schedule1 := "{\"type\":\"Daily\",\"cron\":\"20 3 0 * * *\"}"
converted1, err1 := ConvertSchedule(schedule1)
assert.Nil(t, err1)
assert.Equal(t, converted1.Cron, "20 3 0 * * *")
schedule2 := "{\"type\":\"Daily\",\"weekday\":0,\"offtime\":57720}"
converted2, err2 := ConvertSchedule(schedule2)
assert.Nil(t, err2)
assert.Equal(t, converted2.Cron, "0 2 16 * * *")
schedule3 := "{\"parameter\":{\"daily_time\":57720},\"type\":\"daily\"}"
converted3, err3 := ConvertSchedule(schedule3)
assert.Nil(t, err3)
assert.Equal(t, converted3.Cron, "0 2 16 * * *")
}

View File

@ -1,15 +0,0 @@
package models
import (
"time"
)
// Execution defines the data model used in API level
type Execution struct {
ID int64 `json:"id"`
Status string `json:"status"`
TriggerMode string `json:"trigger_mode"`
Duration int `json:"duration"`
SuccessRate string `json:"success_rate"`
StartTime time.Time `json:"start_time"`
}

View File

@ -33,8 +33,6 @@ func Test_readonlySkipper(t *testing.T) {
{"login get", args{httptest.NewRequest(http.MethodGet, "/c/login", nil)}, false},
{"onboard", args{httptest.NewRequest(http.MethodPost, "/c/oidc/onboard", nil)}, true},
{"user exist", args{httptest.NewRequest(http.MethodPost, "/c/userExists", nil)}, true},
{"user exist", args{httptest.NewRequest(http.MethodPost, "/service/notifications/jobs/adminjob/123456", nil)}, true},
{"user exist", args{httptest.NewRequest(http.MethodPost, "/service/notifications/jobs/adminjob/abcdefg", nil)}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@ -1,117 +0,0 @@
// Copyright 2018 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 admin
import (
"context"
"encoding/json"
o "github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/job"
job_model "github.com/goharbor/harbor/src/common/job/models"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/controller/quota"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/core/service/notifications"
j "github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/orm"
)
var statusMap = map[string]string{
job.JobServiceStatusPending: models.JobPending,
job.JobServiceStatusRunning: models.JobRunning,
job.JobServiceStatusStopped: models.JobStopped,
job.JobServiceStatusCancelled: models.JobCanceled,
job.JobServiceStatusError: models.JobError,
job.JobServiceStatusSuccess: models.JobFinished,
job.JobServiceStatusScheduled: models.JobScheduled,
}
// Handler handles request on /service/notifications/jobs/adminjob/*, which listens to the webhook of jobservice.
type Handler struct {
notifications.BaseHandler
id int64
UUID string
status string
UpstreamJobID string
revision int64
jobName string
checkIn string
statusCode uint16
}
// Prepare ...
func (h *Handler) Prepare() {
h.BaseHandler.Prepare()
var data job_model.JobStatusChange
err := json.Unmarshal(h.Ctx.Input.CopyBody(1<<32), &data)
if err != nil {
log.Errorf("Failed to decode job status change, error: %v", err)
h.Abort("200")
return
}
id, err := h.GetInt64FromPath(":id")
if err != nil {
log.Errorf("Failed to get job ID, error: %v", err)
// Avoid job service from resending...
h.Abort("200")
return
}
h.id = id
// UpstreamJobID is the periodic job id
if data.Metadata.UpstreamJobID != "" {
h.UUID = data.Metadata.UpstreamJobID
} else {
h.UUID = data.JobID
}
status, ok := statusMap[data.Status]
if !ok {
log.Infof("drop the job status update event: job id-%d, status-%s", h.id, status)
h.Abort("200")
return
}
h.statusCode = (uint16)(j.Status(data.Status).Code())
h.status = status
h.revision = data.Metadata.Revision
h.jobName = data.Metadata.JobName
h.checkIn = data.CheckIn
}
// HandleAdminJob handles the webhook of admin jobs
func (h *Handler) HandleAdminJob() {
log.Infof("received admin job status update event: job-%d, job_uuid-%s, status-%s, revision-%d", h.id, h.UUID, h.status, h.revision)
// create the mapping relationship between the jobs in database and jobservice
if err := dao.SetAdminJobUUID(h.id, h.UUID); err != nil {
h.SendInternalServerError(err)
return
}
if err := dao.UpdateAdminJobStatus(h.id, h.status, h.statusCode, h.revision); err != nil {
log.Errorf("Failed to update job status, id: %d, status: %s", h.id, h.status)
h.SendInternalServerError(err)
return
}
if h.jobName == job.ImageGC && h.status == models.JobFinished {
go func() {
if config.QuotaPerProjectEnable() {
quota.RefreshForProjects(orm.NewContext(context.TODO(), o.NewOrm()))
}
}()
}
}

View File

@ -89,12 +89,6 @@ func (h *Handler) Prepare() {
}
}
// HandleScan handles the webhook of scan job
func (h *Handler) HandleScan() {
// legacy handler for the scan job
return
}
// HandleRetentionTask handles the webhook of retention task
func (h *Handler) HandleRetentionTask() {
taskID := h.id

View File

@ -22,13 +22,16 @@ import (
"github.com/goharbor/harbor/src/core/api"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/core/controllers"
"github.com/goharbor/harbor/src/core/service/notifications/admin"
"github.com/goharbor/harbor/src/core/service/notifications/jobs"
"github.com/goharbor/harbor/src/core/service/token"
"github.com/goharbor/harbor/src/server/handler"
"github.com/goharbor/harbor/src/server/router"
)
func ignoreNotification(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
func registerRoutes() {
// API version
router.NewRoute().Method(http.MethodGet).Path("/api/version").HandlerFunc(GetAPIVersion)
@ -46,10 +49,10 @@ func registerRoutes() {
beego.Router("/api/internal/switchquota", &api.InternalAPI{}, "put:SwitchQuota")
beego.Router("/api/internal/syncquota", &api.InternalAPI{}, "post:SyncQuota")
beego.Router("/service/notifications/jobs/adminjob/:id([0-9]+)", &admin.Handler{}, "post:HandleAdminJob")
beego.Router("/service/notifications/jobs/webhook/:id([0-9]+)", &jobs.Handler{}, "post:HandleNotificationJob")
beego.Router("/service/notifications/jobs/retention/task/:id([0-9]+)", &jobs.Handler{}, "post:HandleRetentionTask")
beego.Router("/service/notifications/jobs/scan/:uuid", &jobs.Handler{}, "post:HandleScan")
router.NewRoute().Method(http.MethodPost).Path("/service/notifications/jobs/adminjob/:id([0-9]+)").Handler(handler.NewJobStatusHandler()) // legacy job status hook endpoint for adminjob
router.NewRoute().Method(http.MethodPost).Path("/service/notifications/jobs/scan/:uuid").HandlerFunc(ignoreNotification) // ignore legacy scan job notifaction
router.NewRoute().Method(http.MethodPost).Path("/service/notifications/schedules/:id([0-9]+)").Handler(handler.NewJobStatusHandler()) // legacy job status hook endpoint for scheduler
router.NewRoute().Method(http.MethodPost).Path("/service/notifications/jobs/replication/:id([0-9]+)").Handler(handler.NewJobStatusHandler()) // legacy job status hook endpoint for replication scheduler
router.NewRoute().Method(http.MethodPost).Path("/service/notifications/jobs/replication/task/:id([0-9]+)").Handler(handler.NewJobStatusHandler()) // legacy job status hook endpoint for replication task

View File

@ -1,35 +0,0 @@
/*
* Harbor API
*
* These APIs provide services for manipulating Harbor project.
*
* OpenAPI spec version: 0.3.0
*
* Generated by: https://github.com/swagger-api/swagger-codegen.git
*
* 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 apilib
// AdminJob ...
type AdminJob struct {
ID int64 `json:"id,omitempty"`
Name string `json:"job_name,omitempty"`
Kind string `json:"job_kind,omitempty"`
Status string `json:"job_status,omitempty"`
UUID string `json:"uuid,omitempty"`
Deleted bool `json:"deleted,omitempty"`
CreationTime string `json:"creation_time,omitempty"`
UpdateTime string `json:"update_time,omitempty"`
}

View File

@ -1,39 +0,0 @@
/*
* Harbor API
*
* These APIs provide services for manipulating Harbor project.
*
* OpenAPI spec version: 0.3.0
*
* Generated by: https://github.com/swagger-api/swagger-codegen.git
*
* 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 apilib
// AdminJobReq holds request information for admin job
type AdminJobReq struct {
Schedule *ScheduleParam `json:"schedule,omitempty"`
Name string `json:"name"`
Status string `json:"status,omitempty"`
ID int64 `json:"id,omitempty"`
Parameters map[string]interface{} `json:"parameters"`
}
// ScheduleParam ...
type ScheduleParam struct {
Type string `json:"type,omitempty"`
Weekday int8 `json:"Weekday,omitempty"`
Offtime int64 `json:"Offtime,omitempty"`
}