mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-22 16:48:30 +01:00
add admin job api (#5344)
It supports Harbor admin to trigger job either manual or schedule. The job will be populated to job service to execute. The api includes: 1. POST /api/system/gc 2, GET /api/system/gc/:id 3, GET /api/system/gc/:id/log 4, PUT/GET/POST /api/system/gc/schedule
This commit is contained in:
parent
bbb190327d
commit
efdb57548f
@ -336,7 +336,25 @@ the resource_name is the name of image when the resource_type is i
|
||||
);
|
||||
|
||||
CREATE TRIGGER harbor_resource_label_update_time_at_modtime BEFORE UPDATE ON harbor_resource_label FOR EACH ROW EXECUTE PROCEDURE update_update_time_at_column();
|
||||
|
||||
|
||||
create table admin_job (
|
||||
id SERIAL NOT NULL,
|
||||
job_name varchar(64) NOT NULL,
|
||||
job_kind varchar(64) NOT NULL,
|
||||
cron_str varchar(256),
|
||||
status varchar(64) NOT NULL,
|
||||
job_uuid varchar(64),
|
||||
creation_time timestamp default 'now'::timestamp,
|
||||
update_time timestamp default 'now'::timestamp,
|
||||
deleted boolean DEFAULT false NOT NULL,
|
||||
PRIMARY KEY(id)
|
||||
);
|
||||
|
||||
CREATE TRIGGER admin_job_update_time_at_modtime BEFORE UPDATE ON admin_job FOR EACH ROW EXECUTE PROCEDURE update_update_time_at_column();
|
||||
|
||||
CREATE INDEX admin_job_status ON admin_job (status);
|
||||
CREATE INDEX admin_job_uuid ON admin_job (job_uuid);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS alembic_version (
|
||||
version_num varchar(32) NOT NULL
|
||||
);
|
||||
|
@ -72,6 +72,12 @@ func (b *BaseAPI) HandleBadRequest(text string) {
|
||||
b.RenderError(http.StatusBadRequest, text)
|
||||
}
|
||||
|
||||
// HandleStatusPreconditionFailed ...
|
||||
func (b *BaseAPI) HandleStatusPreconditionFailed(text string) {
|
||||
log.Info(text)
|
||||
b.RenderError(http.StatusPreconditionFailed, text)
|
||||
}
|
||||
|
||||
// HandleConflict ...
|
||||
func (b *BaseAPI) HandleConflict(text ...string) {
|
||||
msg := ""
|
||||
|
135
src/common/dao/admin_job.go
Normal file
135
src/common/dao/admin_job.go
Normal file
@ -0,0 +1,135 @@
|
||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// 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/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/utils/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_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.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) error {
|
||||
o := GetOrmer()
|
||||
j := models.AdminJob{
|
||||
ID: id,
|
||||
Status: status,
|
||||
UpdateTime: time.Now(),
|
||||
}
|
||||
n, err := o.Update(&j, "Status", "UpdateTime")
|
||||
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
|
||||
}
|
||||
|
||||
// GetTop10AdminJobs ...
|
||||
func GetTop10AdminJobs() ([]*models.AdminJob, error) {
|
||||
sql := `select * from admin_job
|
||||
where deleted = false order by update_time limit 10`
|
||||
jobs := []*models.AdminJob{}
|
||||
_, err := GetOrmer().Raw(sql).QueryRows(&jobs)
|
||||
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
|
||||
|
||||
}
|
75
src/common/dao/admin_job_test.go
Normal file
75
src/common/dao/admin_job_test.go
Normal file
@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dao
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
)
|
||||
|
||||
func TestAddAdminJob(t *testing.T) {
|
||||
job := &models.AdminJob{
|
||||
Name: "job",
|
||||
Kind: "jobKind",
|
||||
}
|
||||
|
||||
job0 := &models.AdminJob{
|
||||
Name: "GC",
|
||||
Kind: "testKind",
|
||||
}
|
||||
|
||||
// add
|
||||
id, err := AddAdminJob(job0)
|
||||
require.Nil(t, err)
|
||||
job0.ID = id
|
||||
|
||||
// get
|
||||
job1, err := GetAdminJob(id)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, job1.ID, job0.ID)
|
||||
assert.Equal(t, job1.Name, job0.Name)
|
||||
|
||||
// update status
|
||||
err = UpdateAdminJobStatus(id, "testStatus")
|
||||
require.Nil(t, err)
|
||||
job2, err := GetAdminJob(id)
|
||||
assert.Equal(t, job2.Status, "testStatus")
|
||||
|
||||
// set uuid
|
||||
err = SetAdminJobUUID(id, "f5ef34f4cb3588d663176132")
|
||||
require.Nil(t, err)
|
||||
job3, err := GetAdminJob(id)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, job3.UUID, "f5ef34f4cb3588d663176132")
|
||||
|
||||
// get admin jobs
|
||||
_, err = AddAdminJob(job)
|
||||
require.Nil(t, err)
|
||||
query := &models.AdminJobQuery{
|
||||
Name: "job",
|
||||
}
|
||||
jobs, err := GetAdminJobs(query)
|
||||
assert.Equal(t, len(jobs), 1)
|
||||
|
||||
// get top 10
|
||||
_, err = AddAdminJob(job)
|
||||
require.Nil(t, err)
|
||||
|
||||
jobs, _ = GetTop10AdminJobs()
|
||||
assert.Equal(t, len(jobs), 3)
|
||||
}
|
53
src/common/models/adminjob.go
Normal file
53
src/common/models/adminjob.go
Normal file
@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// 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 (
|
||||
"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"`
|
||||
Cron string `orm:"column(cron_str)" json:"cron_str"`
|
||||
Status string `orm:"column(status)" json:"job_status"`
|
||||
UUID string `orm:"column(job_uuid)" 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
|
||||
}
|
@ -35,5 +35,6 @@ func init() {
|
||||
new(ConfigEntry),
|
||||
new(Label),
|
||||
new(ResourceLabel),
|
||||
new(UserGroup))
|
||||
new(UserGroup),
|
||||
new(AdminJob))
|
||||
}
|
||||
|
@ -199,3 +199,13 @@ func SafeCastFloat64(value interface{}) float64 {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ParseOfftime ...
|
||||
func ParseOfftime(offtime int64) (hour, minite, second int) {
|
||||
offtime = offtime % (3600 * 24)
|
||||
hour = int(offtime / 3600)
|
||||
offtime = offtime % 3600
|
||||
minite = int(offtime / 60)
|
||||
second = int(offtime % 60)
|
||||
return
|
||||
}
|
||||
|
@ -336,3 +336,26 @@ func TestSafeCastFloat64(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseOfftime(t *testing.T) {
|
||||
cases := []struct {
|
||||
offtime int64
|
||||
hour int
|
||||
minite int
|
||||
second int
|
||||
}{
|
||||
{0, 0, 0, 0},
|
||||
{1, 0, 0, 1},
|
||||
{60, 0, 1, 0},
|
||||
{3600, 1, 0, 0},
|
||||
{3661, 1, 1, 1},
|
||||
{3600*24 + 60, 0, 1, 0},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
h, m, s := ParseOfftime(c.offtime)
|
||||
assert.Equal(t, c.hour, h)
|
||||
assert.Equal(t, c.minite, m)
|
||||
assert.Equal(t, c.second, s)
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,15 @@
|
||||
package gc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/vmware/harbor/src/common"
|
||||
common_http "github.com/vmware/harbor/src/common/http"
|
||||
"github.com/vmware/harbor/src/common/http/modifier/auth"
|
||||
"github.com/vmware/harbor/src/common/registryctl"
|
||||
reg "github.com/vmware/harbor/src/common/utils/registry"
|
||||
"github.com/vmware/harbor/src/jobservice/env"
|
||||
"github.com/vmware/harbor/src/jobservice/logger"
|
||||
"github.com/vmware/harbor/src/registryctl/client"
|
||||
@ -25,6 +33,9 @@ import (
|
||||
type GarbageCollector struct {
|
||||
registryCtlClient client.Client
|
||||
logger logger.Interface
|
||||
uiclient *common_http.Client
|
||||
UIURL string
|
||||
insecure bool
|
||||
}
|
||||
|
||||
// MaxFails implements the interface in job/Interface
|
||||
@ -47,6 +58,10 @@ func (gc *GarbageCollector) Run(ctx env.JobContext, params map[string]interface{
|
||||
if err := gc.init(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gc.readonly(true); err != nil {
|
||||
return err
|
||||
}
|
||||
defer gc.readonly(false)
|
||||
if err := gc.registryCtlClient.Health(); err != nil {
|
||||
gc.logger.Errorf("failed to start gc as regsitry controller is unreachable: %v", err)
|
||||
return err
|
||||
@ -66,5 +81,29 @@ func (gc *GarbageCollector) init(ctx env.JobContext) error {
|
||||
registryctl.Init()
|
||||
gc.registryCtlClient = registryctl.RegistryCtlClient
|
||||
gc.logger = ctx.GetLogger()
|
||||
cred := auth.NewSecretAuthorizer(os.Getenv("JOBSERVICE_SECRET"))
|
||||
gc.insecure = false
|
||||
gc.uiclient = common_http.NewClient(&http.Client{
|
||||
Transport: reg.GetHTTPTransport(gc.insecure),
|
||||
}, cred)
|
||||
errTpl := "Failed to get required property: %s"
|
||||
if v, ok := ctx.Get(common.UIURL); ok && len(v.(string)) > 0 {
|
||||
gc.UIURL = v.(string)
|
||||
} else {
|
||||
return fmt.Errorf(errTpl, common.UIURL)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gc *GarbageCollector) readonly(switcher bool) error {
|
||||
if err := gc.uiclient.Put(fmt.Sprintf("%s/api/configurations", gc.UIURL), struct {
|
||||
ReadOnly bool `json:"read_only"`
|
||||
}{
|
||||
ReadOnly: switcher,
|
||||
}); err != nil {
|
||||
gc.logger.Errorf("failed to send readonly request to %s: %v", gc.UIURL, err)
|
||||
return err
|
||||
}
|
||||
gc.logger.Info("the readonly request has been sent successfully")
|
||||
return nil
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"github.com/vmware/harbor/src/common/job"
|
||||
job_models "github.com/vmware/harbor/src/common/job/models"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
common_utils "github.com/vmware/harbor/src/common/utils"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/replication"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
@ -39,10 +40,10 @@ func (st *ScheduleTrigger) Setup() error {
|
||||
}
|
||||
switch st.params.Type {
|
||||
case replication.TriggerScheduleDaily:
|
||||
h, m, s := parseOfftime(st.params.Offtime)
|
||||
h, m, s := common_utils.ParseOfftime(st.params.Offtime)
|
||||
metadata.Cron = fmt.Sprintf("%d %d %d * * *", s, m, h)
|
||||
case replication.TriggerScheduleWeekly:
|
||||
h, m, s := parseOfftime(st.params.Offtime)
|
||||
h, m, s := common_utils.ParseOfftime(st.params.Offtime)
|
||||
metadata.Cron = fmt.Sprintf("%d %d %d * * %d", s, m, h, st.params.Weekday%7)
|
||||
default:
|
||||
return fmt.Errorf("unsupported schedual trigger type: %s", st.params.Type)
|
||||
@ -104,12 +105,3 @@ func (st *ScheduleTrigger) Unset() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseOfftime(offtime int64) (hour, minite, second int) {
|
||||
offtime = offtime % (3600 * 24)
|
||||
hour = int(offtime / 3600)
|
||||
offtime = offtime % 3600
|
||||
minite = int(offtime / 60)
|
||||
second = int(offtime % 60)
|
||||
return
|
||||
}
|
||||
|
@ -25,26 +25,3 @@ func TestKindOfScheduleTrigger(t *testing.T) {
|
||||
trigger := NewScheduleTrigger(ScheduleParam{})
|
||||
assert.Equal(t, replication.TriggerKindSchedule, trigger.Kind())
|
||||
}
|
||||
|
||||
func TestParseOfftime(t *testing.T) {
|
||||
cases := []struct {
|
||||
offtime int64
|
||||
hour int
|
||||
minite int
|
||||
second int
|
||||
}{
|
||||
{0, 0, 0, 0},
|
||||
{1, 0, 0, 1},
|
||||
{60, 0, 1, 0},
|
||||
{3600, 1, 0, 0},
|
||||
{3661, 1, 1, 1},
|
||||
{3600*24 + 60, 0, 1, 0},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
h, m, s := parseOfftime(c.offtime)
|
||||
assert.Equal(t, c.hour, h)
|
||||
assert.Equal(t, c.minite, m)
|
||||
assert.Equal(t, c.second, s)
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ func (c *ConfigAPI) Prepare() {
|
||||
c.HandleUnauthorized()
|
||||
return
|
||||
}
|
||||
if !c.SecurityCtx.IsSysAdmin() {
|
||||
if !c.SecurityCtx.IsSysAdmin() && !c.SecurityCtx.IsSolutionUser() {
|
||||
c.HandleForbidden(c.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/job/test"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/utils"
|
||||
ldapUtils "github.com/vmware/harbor/src/common/utils/ldap"
|
||||
@ -166,6 +167,9 @@ func init() {
|
||||
beego.Router("/api/labels/:id([0-9]+", &LabelAPI{}, "get:Get;put:Put;delete:Delete")
|
||||
beego.Router("/api/labels/:id([0-9]+)/resources", &LabelAPI{}, "get:ListResources")
|
||||
beego.Router("/api/ping", &SystemInfoAPI{}, "get:Ping")
|
||||
beego.Router("/api/system/gc/:id", &GCAPI{}, "get:GetGC")
|
||||
beego.Router("/api/system/gc/:id([0-9]+)/log", &GCAPI{}, "get:GetLog")
|
||||
beego.Router("/api/system/gc/schedule", &GCAPI{}, "get:Get;put:Put;post:Post")
|
||||
_ = updateInitPassword(1, "Harbor12345")
|
||||
|
||||
if err := core.Init(); err != nil {
|
||||
@ -182,6 +186,9 @@ func init() {
|
||||
unknownUsr = &usrInfo{"unknown", "unknown"}
|
||||
testUser = &usrInfo{TestUserName, TestUserPwd}
|
||||
|
||||
//Init mock jobservice
|
||||
mockServer := test.NewJobServiceServer()
|
||||
defer mockServer.Close()
|
||||
}
|
||||
|
||||
func request(_sling *sling.Sling, acceptHeader string, authInfo ...usrInfo) (int, []byte, error) {
|
||||
@ -1129,3 +1136,33 @@ func (a testapi) DeleteMeta(authInfor usrInfo, projectID int64, name string) (in
|
||||
code, body, err := request(_sling, jsonAcceptHeader, authInfor)
|
||||
return code, string(body), err
|
||||
}
|
||||
|
||||
func (a testapi) AddGC(authInfor usrInfo, adminReq apilib.GCReq) (int, error) {
|
||||
_sling := sling.New().Post(a.basePath)
|
||||
|
||||
path := "/api/system/gc/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) GCScheduleGet(authInfo usrInfo) (int, []apilib.AdminJob, error) {
|
||||
_sling := sling.New().Get(a.basePath)
|
||||
path := "/api/system/gc/schedule"
|
||||
_sling = _sling.Path(path)
|
||||
httpStatusCode, body, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||
var successPayLoad []apilib.AdminJob
|
||||
if 200 == httpStatusCode && nil == err {
|
||||
err = json.Unmarshal(body, &successPayLoad)
|
||||
}
|
||||
|
||||
return httpStatusCode, successPayLoad, err
|
||||
}
|
||||
|
124
src/ui/api/models/reg_gc.go
Normal file
124
src/ui/api/models/reg_gc.go
Normal file
@ -0,0 +1,124 @@
|
||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// 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"
|
||||
|
||||
"github.com/astaxie/beego/validation"
|
||||
"github.com/vmware/harbor/src/common/job"
|
||||
"github.com/vmware/harbor/src/common/job/models"
|
||||
"github.com/vmware/harbor/src/common/utils"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
)
|
||||
|
||||
const (
|
||||
//ScheduleDaily : 'Daily'
|
||||
ScheduleDaily = "Daily"
|
||||
//ScheduleWeekly : 'Weekly'
|
||||
ScheduleWeekly = "Weekly"
|
||||
//ScheduleManual : 'Manual'
|
||||
ScheduleManual = "Manual"
|
||||
//ScheduleNone : 'None'
|
||||
ScheduleNone = "None"
|
||||
)
|
||||
|
||||
// GCReq holds request information for admin job
|
||||
type GCReq struct {
|
||||
Schedule *ScheduleParam `json:"schedule"`
|
||||
Status string `json:"status"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
//ScheduleParam defines the parameter of schedule trigger
|
||||
type ScheduleParam struct {
|
||||
//Daily, Weekly, Manual, None
|
||||
Type string `json:"type"`
|
||||
//Optional, only used when type is 'weekly'
|
||||
Weekday int8 `json:"Weekday"`
|
||||
//The time offset with the UTC 00:00 in seconds
|
||||
Offtime int64 `json:"Offtime"`
|
||||
}
|
||||
|
||||
// Valid validates the gc request
|
||||
func (gr *GCReq) Valid(v *validation.Validation) {
|
||||
switch gr.Schedule.Type {
|
||||
case ScheduleDaily, ScheduleWeekly:
|
||||
if gr.Schedule.Offtime < 0 || gr.Schedule.Offtime > 3600*24 {
|
||||
v.SetError("offtime", fmt.Sprintf("Invalid schedule trigger parameter offtime: %d", gr.Schedule.Offtime))
|
||||
}
|
||||
case ScheduleManual, ScheduleNone:
|
||||
default:
|
||||
v.SetError("kind", fmt.Sprintf("Invalid schedule kind: %s", gr.Schedule.Type))
|
||||
}
|
||||
}
|
||||
|
||||
// ToJob converts request to a job reconiged by job service.
|
||||
func (gr *GCReq) ToJob() (*models.JobData, error) {
|
||||
metadata := &models.JobMetadata{
|
||||
JobKind: gr.JobKind(),
|
||||
// GC job must be unique ...
|
||||
IsUnique: true,
|
||||
}
|
||||
|
||||
switch gr.Schedule.Type {
|
||||
case ScheduleDaily:
|
||||
h, m, s := utils.ParseOfftime(gr.Schedule.Offtime)
|
||||
metadata.Cron = fmt.Sprintf("%d %d %d * * *", s, m, h)
|
||||
case ScheduleWeekly:
|
||||
h, m, s := utils.ParseOfftime(gr.Schedule.Offtime)
|
||||
metadata.Cron = fmt.Sprintf("%d %d %d * * %d", s, m, h, gr.Schedule.Weekday%7)
|
||||
case ScheduleManual, ScheduleNone:
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported schedual trigger type: %s", gr.Schedule.Type)
|
||||
}
|
||||
|
||||
jobData := &models.JobData{
|
||||
Name: job.ImageGC,
|
||||
Metadata: metadata,
|
||||
StatusHook: fmt.Sprintf("%s/service/notifications/jobs/adminjob/%d",
|
||||
config.InternalUIURL(), gr.ID),
|
||||
}
|
||||
return jobData, nil
|
||||
}
|
||||
|
||||
// IsPeriodic ...
|
||||
func (gr *GCReq) IsPeriodic() bool {
|
||||
return gr.JobKind() == job.JobKindPeriodic
|
||||
}
|
||||
|
||||
// JobKind ...
|
||||
func (gr *GCReq) JobKind() string {
|
||||
switch gr.Schedule.Type {
|
||||
case ScheduleDaily, ScheduleWeekly:
|
||||
return job.JobKindPeriodic
|
||||
case ScheduleManual:
|
||||
return job.JobKindGeneric
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// CronString ...
|
||||
func (gr *GCReq) CronString() string {
|
||||
str, err := json.Marshal(gr.Schedule)
|
||||
if err != nil {
|
||||
log.Debugf("failed to marshal json error, %v", err)
|
||||
return ""
|
||||
}
|
||||
return string(str)
|
||||
}
|
130
src/ui/api/models/reg_gc_test.go
Normal file
130
src/ui/api/models/reg_gc_test.go
Normal file
@ -0,0 +1,130 @@
|
||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// 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 (
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/vmware/harbor/src/common"
|
||||
common_job "github.com/vmware/harbor/src/common/job"
|
||||
"github.com/vmware/harbor/src/common/utils/test"
|
||||
)
|
||||
|
||||
var adminServerTestConfig = map[string]interface{}{
|
||||
common.DefaultUIEndpoint: "test",
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
server, err := test.NewAdminserver(adminServerTestConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create a mock admin server: %v", err)
|
||||
}
|
||||
defer server.Close()
|
||||
|
||||
}
|
||||
|
||||
func TestToJob(t *testing.T) {
|
||||
schedule := &ScheduleParam{
|
||||
Type: "Daily",
|
||||
Offtime: 200,
|
||||
}
|
||||
|
||||
adminjob := &GCReq{
|
||||
Schedule: schedule,
|
||||
}
|
||||
|
||||
job, err := adminjob.ToJob()
|
||||
assert.Nil(t, err)
|
||||
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) {
|
||||
schedule := &ScheduleParam{
|
||||
Type: "Manual",
|
||||
}
|
||||
|
||||
adminjob := &GCReq{
|
||||
Schedule: schedule,
|
||||
}
|
||||
|
||||
job, err := adminjob.ToJob()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, job.Name, "IMAGE_GC")
|
||||
assert.Equal(t, job.Metadata.JobKind, common_job.JobKindGeneric)
|
||||
}
|
||||
|
||||
func TestToJobErr(t *testing.T) {
|
||||
schedule := &ScheduleParam{
|
||||
Type: "test",
|
||||
}
|
||||
|
||||
adminjob := &GCReq{
|
||||
Schedule: schedule,
|
||||
}
|
||||
|
||||
_, err := adminjob.ToJob()
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestIsPeriodic(t *testing.T) {
|
||||
schedule := &ScheduleParam{
|
||||
Type: "Daily",
|
||||
Offtime: 200,
|
||||
}
|
||||
|
||||
adminjob := &GCReq{
|
||||
Schedule: schedule,
|
||||
}
|
||||
|
||||
isPeriodic := adminjob.IsPeriodic()
|
||||
assert.Equal(t, isPeriodic, true)
|
||||
}
|
||||
|
||||
func TestJobKind(t *testing.T) {
|
||||
schedule := &ScheduleParam{
|
||||
Type: "Daily",
|
||||
Offtime: 200,
|
||||
}
|
||||
adminjob := &GCReq{
|
||||
Schedule: schedule,
|
||||
}
|
||||
kind := adminjob.JobKind()
|
||||
assert.Equal(t, kind, "Periodic")
|
||||
|
||||
schedule1 := &ScheduleParam{
|
||||
Type: "Manual",
|
||||
}
|
||||
adminjob1 := &GCReq{
|
||||
Schedule: schedule1,
|
||||
}
|
||||
kind1 := adminjob1.JobKind()
|
||||
assert.Equal(t, kind1, "Generic")
|
||||
}
|
||||
|
||||
func TestCronString(t *testing.T) {
|
||||
schedule := &ScheduleParam{
|
||||
Type: "Daily",
|
||||
Offtime: 102,
|
||||
}
|
||||
adminjob := &GCReq{
|
||||
Schedule: schedule,
|
||||
}
|
||||
cronStr := adminjob.CronString()
|
||||
assert.Equal(t, cronStr, "{\"type\":\"Daily\",\"Weekday\":0,\"Offtime\":102}")
|
||||
}
|
228
src/ui/api/reg_gc.go
Normal file
228
src/ui/api/reg_gc.go
Normal file
@ -0,0 +1,228 @@
|
||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// 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"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
common_http "github.com/vmware/harbor/src/common/http"
|
||||
common_job "github.com/vmware/harbor/src/common/job"
|
||||
common_models "github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/ui/api/models"
|
||||
utils_ui "github.com/vmware/harbor/src/ui/utils"
|
||||
)
|
||||
|
||||
// GCAPI handles request of harbor admin...
|
||||
type GCAPI struct {
|
||||
BaseController
|
||||
}
|
||||
|
||||
// Prepare validates the URL and parms, it needs the system admin permission.
|
||||
func (gc *GCAPI) Prepare() {
|
||||
gc.BaseController.Prepare()
|
||||
if !gc.SecurityCtx.IsAuthenticated() {
|
||||
gc.HandleUnauthorized()
|
||||
return
|
||||
}
|
||||
if !gc.SecurityCtx.IsSysAdmin() {
|
||||
gc.HandleForbidden(gc.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Post ...
|
||||
func (gc *GCAPI) Post() {
|
||||
gr := models.GCReq{}
|
||||
gc.DecodeJSONReqAndValidate(&gr)
|
||||
gc.submitJob(&gr)
|
||||
gc.Redirect(http.StatusCreated, strconv.FormatInt(gr.ID, 10))
|
||||
}
|
||||
|
||||
//Put ...
|
||||
func (gc *GCAPI) Put() {
|
||||
gr := models.GCReq{}
|
||||
gc.DecodeJSONReqAndValidate(&gr)
|
||||
|
||||
if gr.Schedule.Type == models.ScheduleManual {
|
||||
gc.HandleInternalServerError(fmt.Sprintf("Fail to update GC schedule as wrong schedule type: %s.", gr.Schedule.Type))
|
||||
return
|
||||
}
|
||||
|
||||
query := &common_models.AdminJobQuery{
|
||||
Name: common_job.ImageGC,
|
||||
Kind: common_job.JobKindPeriodic,
|
||||
}
|
||||
jobs, err := dao.GetAdminJobs(query)
|
||||
if err != nil {
|
||||
gc.HandleInternalServerError(fmt.Sprintf("%v", err))
|
||||
return
|
||||
}
|
||||
if len(jobs) != 1 {
|
||||
gc.HandleInternalServerError("Fail to update GC schedule, only one schedule is accepted.")
|
||||
return
|
||||
}
|
||||
|
||||
// stop the scheduled job and remove it.
|
||||
if err = utils_ui.GetJobServiceClient().PostAction(jobs[0].UUID, common_job.JobActionStop); err != nil {
|
||||
if e, ok := err.(*common_http.Error); !ok || e.Code != http.StatusNotFound {
|
||||
gc.HandleInternalServerError(fmt.Sprintf("%v", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = dao.DeleteAdminJob(jobs[0].ID); err != nil {
|
||||
gc.HandleInternalServerError(fmt.Sprintf("%v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Set schedule to None means to cancel the schedule, won't add new job.
|
||||
if gr.Schedule.Type != models.ScheduleNone {
|
||||
gc.submitJob(&gr)
|
||||
}
|
||||
}
|
||||
|
||||
// GetGC ...
|
||||
func (gc *GCAPI) GetGC() {
|
||||
id, err := gc.GetInt64FromPath(":id")
|
||||
if err != nil {
|
||||
gc.HandleInternalServerError(fmt.Sprintf("need to specify gc id"))
|
||||
return
|
||||
}
|
||||
|
||||
jobs, err := dao.GetAdminJobs(&common_models.AdminJobQuery{
|
||||
ID: id,
|
||||
})
|
||||
if err != nil {
|
||||
gc.HandleInternalServerError(fmt.Sprintf("failed to get admin jobs: %v", err))
|
||||
return
|
||||
}
|
||||
gc.Data["json"] = jobs
|
||||
gc.ServeJSON()
|
||||
}
|
||||
|
||||
// List ...
|
||||
func (gc *GCAPI) List() {
|
||||
jobs, err := dao.GetTop10AdminJobs()
|
||||
if err != nil {
|
||||
gc.HandleInternalServerError(fmt.Sprintf("failed to get admin jobs: %v", err))
|
||||
return
|
||||
}
|
||||
gc.Data["json"] = jobs
|
||||
gc.ServeJSON()
|
||||
}
|
||||
|
||||
// Get gets GC schedule ...
|
||||
func (gc *GCAPI) Get() {
|
||||
jobs, err := dao.GetAdminJobs(&common_models.AdminJobQuery{
|
||||
Name: common_job.ImageGC,
|
||||
Kind: common_job.JobKindPeriodic,
|
||||
})
|
||||
if err != nil {
|
||||
gc.HandleNotFound(fmt.Sprintf("failed to get admin jobs: %v", err))
|
||||
return
|
||||
}
|
||||
if len(jobs) > 1 {
|
||||
gc.HandleInternalServerError("Get more than one GC scheduled job, make sure there has only one.")
|
||||
return
|
||||
}
|
||||
gc.Data["json"] = jobs
|
||||
gc.ServeJSON()
|
||||
}
|
||||
|
||||
//GetLog ...
|
||||
func (gc *GCAPI) GetLog() {
|
||||
id, err := gc.GetInt64FromPath(":id")
|
||||
if err != nil {
|
||||
gc.HandleBadRequest("invalid ID")
|
||||
return
|
||||
}
|
||||
job, err := dao.GetAdminJob(id)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to load job data for job: %d, error: %v", id, err)
|
||||
gc.CustomAbort(http.StatusInternalServerError, "Failed to get Job data")
|
||||
}
|
||||
if job == nil {
|
||||
log.Errorf("Failed to get admin job: %d", id)
|
||||
gc.CustomAbort(http.StatusNotFound, "Failed to get Job")
|
||||
}
|
||||
|
||||
logBytes, err := utils_ui.GetJobServiceClient().GetJobLog(job.UUID)
|
||||
if err != nil {
|
||||
if httpErr, ok := err.(*common_http.Error); ok {
|
||||
gc.RenderError(httpErr.Code, "")
|
||||
log.Errorf(fmt.Sprintf("failed to get log of job %d: %d %s",
|
||||
id, httpErr.Code, httpErr.Message))
|
||||
return
|
||||
}
|
||||
gc.HandleInternalServerError(fmt.Sprintf("Failed to get job logs, uuid: %s, error: %v", job.UUID, err))
|
||||
return
|
||||
}
|
||||
gc.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Length"), strconv.Itoa(len(logBytes)))
|
||||
gc.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Type"), "text/plain")
|
||||
_, err = gc.Ctx.ResponseWriter.Write(logBytes)
|
||||
if err != nil {
|
||||
gc.HandleInternalServerError(fmt.Sprintf("Failed to write job logs, uuid: %s, error: %v", job.UUID, err))
|
||||
}
|
||||
}
|
||||
|
||||
// submitJob submits a job to job service per request
|
||||
func (gc *GCAPI) submitJob(gr *models.GCReq) {
|
||||
// cannot post multiple schdule for GC job.
|
||||
if gr.IsPeriodic() {
|
||||
jobs, err := dao.GetAdminJobs(&common_models.AdminJobQuery{
|
||||
Name: common_job.ImageGC,
|
||||
Kind: common_job.JobKindPeriodic,
|
||||
})
|
||||
if err != nil {
|
||||
gc.HandleInternalServerError(fmt.Sprintf("failed to get admin jobs: %v", err))
|
||||
return
|
||||
}
|
||||
if len(jobs) != 0 {
|
||||
gc.HandleStatusPreconditionFailed("Fail to set schedule for GC as always had one, please delete it firstly then to re-schedule.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
id, err := dao.AddAdminJob(&common_models.AdminJob{
|
||||
Name: common_job.ImageGC,
|
||||
Kind: gr.JobKind(),
|
||||
Cron: gr.CronString(),
|
||||
})
|
||||
if err != nil {
|
||||
gc.HandleInternalServerError(fmt.Sprintf("%v", err))
|
||||
return
|
||||
}
|
||||
gr.ID = id
|
||||
job, err := gr.ToJob()
|
||||
if err != nil {
|
||||
gc.HandleInternalServerError(fmt.Sprintf("%v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// submit job to jobservice
|
||||
log.Debugf("submiting GC admin job to jobservice")
|
||||
_, err = utils_ui.GetJobServiceClient().SubmitJob(job)
|
||||
if err != nil {
|
||||
if err := dao.DeleteAdminJob(id); err != nil {
|
||||
log.Debugf("Failed to delete admin job, err: %v", err)
|
||||
}
|
||||
gc.HandleInternalServerError(fmt.Sprintf("%v", err))
|
||||
return
|
||||
}
|
||||
}
|
39
src/ui/api/reg_gc_test.go
Normal file
39
src/ui/api/reg_gc_test.go
Normal file
@ -0,0 +1,39 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/vmware/harbor/tests/apitests/apilib"
|
||||
)
|
||||
|
||||
var adminJob001 apilib.GCReq
|
||||
var adminJob001schdeule apilib.ScheduleParam
|
||||
|
||||
func TestAdminJobPost(t *testing.T) {
|
||||
|
||||
assert := assert.New(t)
|
||||
apiTest := newHarborAPI()
|
||||
|
||||
//case 1: add a new admin job
|
||||
code, err := apiTest.AddGC(*admin, adminJob001)
|
||||
if err != nil {
|
||||
t.Error("Error occured while add a admin job", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(200, code, "Add adminjob status should be 200")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminJobGet(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
apiTest := newHarborAPI()
|
||||
|
||||
code, _, err := apiTest.GCScheduleGet(*admin)
|
||||
if err != nil {
|
||||
t.Error("Error occured while get a admin job", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(200, code, "Get adminjob status should be 200")
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ import (
|
||||
"github.com/vmware/harbor/src/ui/api"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
"github.com/vmware/harbor/src/ui/controllers"
|
||||
"github.com/vmware/harbor/src/ui/service/notifications/admin"
|
||||
"github.com/vmware/harbor/src/ui/service/notifications/clair"
|
||||
"github.com/vmware/harbor/src/ui/service/notifications/jobs"
|
||||
"github.com/vmware/harbor/src/ui/service/notifications/registry"
|
||||
@ -89,6 +90,12 @@ func initRouters() {
|
||||
beego.Router("/api/jobs/replication/:id([0-9]+)", &api.RepJobAPI{})
|
||||
beego.Router("/api/jobs/replication/:id([0-9]+)/log", &api.RepJobAPI{}, "get:GetLog")
|
||||
beego.Router("/api/jobs/scan/:id([0-9]+)/log", &api.ScanJobAPI{}, "get:GetLog")
|
||||
|
||||
beego.Router("/api/system/gc", &api.GCAPI{}, "get:List")
|
||||
beego.Router("/api/system/gc/:id", &api.GCAPI{}, "get:GetGC")
|
||||
beego.Router("/api/system/gc/:id([0-9]+)/log", &api.GCAPI{}, "get:GetLog")
|
||||
beego.Router("/api/system/gc/schedule", &api.GCAPI{}, "get:Get;put:Put;post:Post")
|
||||
|
||||
beego.Router("/api/policies/replication/:id([0-9]+)", &api.RepPolicyAPI{})
|
||||
beego.Router("/api/policies/replication", &api.RepPolicyAPI{}, "get:List")
|
||||
beego.Router("/api/policies/replication", &api.RepPolicyAPI{}, "post:Post")
|
||||
@ -118,6 +125,7 @@ func initRouters() {
|
||||
beego.Router("/service/notifications/clair", &clair.Handler{}, "post:Handle")
|
||||
beego.Router("/service/notifications/jobs/scan/:id([0-9]+)", &jobs.Handler{}, "post:HandleScan")
|
||||
beego.Router("/service/notifications/jobs/replication/:id([0-9]+)", &jobs.Handler{}, "post:HandleReplication")
|
||||
beego.Router("/service/notifications/jobs/adminjob/:id([0-9]+)", &admin.Handler{}, "post:HandleAdminJob")
|
||||
beego.Router("/service/token", &token.Handler{})
|
||||
|
||||
beego.Router("/registryproxy/*", &controllers.RegistryProxy{}, "*:Handle")
|
||||
|
86
src/ui/service/notifications/admin/handler.go
Normal file
86
src/ui/service/notifications/admin/handler.go
Normal file
@ -0,0 +1,86 @@
|
||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// 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 (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/job"
|
||||
job_model "github.com/vmware/harbor/src/common/job/models"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/ui/api"
|
||||
)
|
||||
|
||||
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 reqeust on /service/notifications/jobs/adminjob/*, which listens to the webhook of jobservice.
|
||||
type Handler struct {
|
||||
api.BaseController
|
||||
id int64
|
||||
UUID string
|
||||
status string
|
||||
}
|
||||
|
||||
// Prepare ...
|
||||
func (h *Handler) 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
|
||||
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.status = status
|
||||
}
|
||||
|
||||
//HandleAdminJob handles the webhook of admin jobs
|
||||
func (h *Handler) HandleAdminJob() {
|
||||
log.Infof("received admin job status update event: job-%d, status-%s", h.id, h.status)
|
||||
// create the mapping relationship between the jobs in database and jobservice
|
||||
if err := dao.SetAdminJobUUID(h.id, h.UUID); err != nil {
|
||||
h.HandleInternalServerError(err.Error())
|
||||
return
|
||||
}
|
||||
if err := dao.UpdateAdminJobStatus(h.id, h.status); err != nil {
|
||||
log.Errorf("Failed to update job status, id: %d, status: %s", h.id, h.status)
|
||||
h.HandleInternalServerError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
35
tests/apitests/apilib/admin_job.go
Normal file
35
tests/apitests/apilib/admin_job.go
Normal file
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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"`
|
||||
}
|
37
tests/apitests/apilib/admin_job_req.go
Normal file
37
tests/apitests/apilib/admin_job_req.go
Normal file
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
// GCReq holds request information for admin job
|
||||
type GCReq struct {
|
||||
Schedule *ScheduleParam `json:"schedule,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
ID int64 `json:"id,omitempty"`
|
||||
}
|
||||
|
||||
// ScheduleParam ...
|
||||
type ScheduleParam struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Weekday int8 `json:"Weekday,omitempty"`
|
||||
Offtime int64 `json:"Offtime,omitempty"`
|
||||
}
|
Loading…
Reference in New Issue
Block a user