mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-23 10:45:45 +01:00
Merge pull request #2830 from reasonerjt/swagger-update-1.2.0
provide api to show log of scan job
This commit is contained in:
commit
c68514bfb7
@ -16,6 +16,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/vmware/harbor/src/common/dao"
|
"github.com/vmware/harbor/src/common/dao"
|
||||||
"github.com/vmware/harbor/src/common/models"
|
"github.com/vmware/harbor/src/common/models"
|
||||||
@ -83,3 +84,17 @@ func (isj *ImageScanJob) Post() {
|
|||||||
log.Debugf("Sent job to scheduler, job: %v", sj)
|
log.Debugf("Sent job to scheduler, job: %v", sj)
|
||||||
job.Schedule(sj)
|
job.Schedule(sj)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetLog gets logs of the job
|
||||||
|
func (isj *ImageScanJob) GetLog() {
|
||||||
|
idStr := isj.Ctx.Input.Param(":id")
|
||||||
|
jid, err := strconv.ParseInt(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error parsing job id: %s, error: %v", idStr, err)
|
||||||
|
isj.RenderError(http.StatusBadRequest, "Invalid job id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
scanJob := job.NewScanJob(jid)
|
||||||
|
logFile := scanJob.LogPath()
|
||||||
|
isj.Ctx.Output.Download(logFile)
|
||||||
|
}
|
||||||
|
@ -25,4 +25,5 @@ func initRouters() {
|
|||||||
beego.Router("/api/jobs/replication/:id/log", &api.ReplicationJob{}, "get:GetLog")
|
beego.Router("/api/jobs/replication/:id/log", &api.ReplicationJob{}, "get:GetLog")
|
||||||
beego.Router("/api/jobs/replication/actions", &api.ReplicationJob{}, "post:HandleAction")
|
beego.Router("/api/jobs/replication/actions", &api.ReplicationJob{}, "post:HandleAction")
|
||||||
beego.Router("/api/jobs/scan", &api.ImageScanJob{})
|
beego.Router("/api/jobs/scan", &api.ImageScanJob{})
|
||||||
|
beego.Router("/api/jobs/scan/:id/log", &api.ImageScanJob{}, "get:GetLog")
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,13 @@ type BaseController struct {
|
|||||||
ProjectMgr projectmanager.ProjectManager
|
ProjectMgr projectmanager.ProjectManager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
//ReplicationJobType ...
|
||||||
|
ReplicationJobType = "replication"
|
||||||
|
//ScanJobType ...
|
||||||
|
ScanJobType = "scan"
|
||||||
|
)
|
||||||
|
|
||||||
// Prepare inits security context and project manager from request
|
// Prepare inits security context and project manager from request
|
||||||
// context
|
// context
|
||||||
func (b *BaseController) Prepare() {
|
func (b *BaseController) Prepare() {
|
||||||
|
@ -16,8 +16,6 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
@ -147,38 +145,12 @@ func (ra *RepJobAPI) GetLog() {
|
|||||||
if ra.jobID == 0 {
|
if ra.jobID == 0 {
|
||||||
ra.CustomAbort(http.StatusBadRequest, "id is nil")
|
ra.CustomAbort(http.StatusBadRequest, "id is nil")
|
||||||
}
|
}
|
||||||
|
url := buildJobLogURL(strconv.FormatInt(ra.jobID, 10), ReplicationJobType)
|
||||||
req, err := http.NewRequest("GET", buildJobLogURL(strconv.FormatInt(ra.jobID, 10)), nil)
|
err := utils.RequestAsUI(http.MethodGet, url, nil, utils.NewJobLogRespHandler(&ra.BaseAPI))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to create a request: %v", err)
|
ra.RenderError(http.StatusInternalServerError, err.Error())
|
||||||
ra.CustomAbort(http.StatusInternalServerError, "")
|
|
||||||
}
|
|
||||||
utils.AddUISecret(req)
|
|
||||||
client := &http.Client{}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to get log for job %d: %v", ra.jobID, err)
|
|
||||||
ra.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
if resp.StatusCode == http.StatusOK {
|
|
||||||
ra.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Length"), resp.Header.Get(http.CanonicalHeaderKey("Content-Length")))
|
|
||||||
ra.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Type"), "text/plain")
|
|
||||||
|
|
||||||
if _, err = io.Copy(ra.Ctx.ResponseWriter, resp.Body); err != nil {
|
|
||||||
log.Errorf("failed to write log to response; %v", err)
|
|
||||||
ra.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to read reponse body: %v", err)
|
|
||||||
ra.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
|
||||||
}
|
|
||||||
|
|
||||||
ra.CustomAbort(resp.StatusCode, string(b))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO:add Post handler to call job service API to submit jobs by policy
|
//TODO:add Post handler to call job service API to submit jobs by policy
|
||||||
|
68
src/ui/api/scan_job.go
Normal file
68
src/ui/api/scan_job.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
// 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 (
|
||||||
|
"github.com/vmware/harbor/src/common/dao"
|
||||||
|
"github.com/vmware/harbor/src/common/utils/log"
|
||||||
|
"github.com/vmware/harbor/src/ui/utils"
|
||||||
|
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ScanJobAPI handles request to /api/scanJobs/:id/log
|
||||||
|
type ScanJobAPI struct {
|
||||||
|
BaseController
|
||||||
|
jobID int64
|
||||||
|
projectName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare validates that whether user has read permission to the project of the repo the scan job scanned.
|
||||||
|
func (sj *ScanJobAPI) Prepare() {
|
||||||
|
sj.BaseController.Prepare()
|
||||||
|
if !sj.SecurityCtx.IsAuthenticated() {
|
||||||
|
sj.HandleUnauthorized()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := sj.GetInt64FromPath(":id")
|
||||||
|
if err != nil {
|
||||||
|
sj.CustomAbort(http.StatusBadRequest, "ID is invalid")
|
||||||
|
}
|
||||||
|
sj.jobID = id
|
||||||
|
|
||||||
|
data, err := dao.GetScanJob(id)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Failed to load job data for job: %d, error: %v", id, err)
|
||||||
|
sj.CustomAbort(http.StatusInternalServerError, "Failed to get Job data")
|
||||||
|
}
|
||||||
|
projectName := strings.SplitN(data.Repository, "/", 2)[0]
|
||||||
|
if !sj.SecurityCtx.HasReadPerm(projectName) {
|
||||||
|
log.Errorf("User does not have read permission for project: %s", projectName)
|
||||||
|
sj.HandleForbidden(sj.SecurityCtx.GetUsername())
|
||||||
|
}
|
||||||
|
sj.projectName = projectName
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetLog ...
|
||||||
|
func (sj *ScanJobAPI) GetLog() {
|
||||||
|
url := buildJobLogURL(strconv.FormatInt(sj.jobID, 10), ScanJobType)
|
||||||
|
err := utils.RequestAsUI(http.MethodGet, url, nil, utils.NewJobLogRespHandler(&sj.BaseAPI))
|
||||||
|
if err != nil {
|
||||||
|
sj.RenderError(http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
@ -97,7 +97,7 @@ func TriggerReplication(policyID int64, repository string,
|
|||||||
}
|
}
|
||||||
url := buildReplicationURL()
|
url := buildReplicationURL()
|
||||||
|
|
||||||
return uiutils.RequestAsUI("POST", url, bytes.NewBuffer(b), http.StatusOK)
|
return uiutils.RequestAsUI("POST", url, bytes.NewBuffer(b), uiutils.NewStatusRespHandler(http.StatusOK))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TriggerReplicationByRepository triggers the replication according to the repository
|
// TriggerReplicationByRepository triggers the replication according to the repository
|
||||||
@ -406,9 +406,9 @@ func buildReplicationURL() string {
|
|||||||
return fmt.Sprintf("%s/api/jobs/replication", url)
|
return fmt.Sprintf("%s/api/jobs/replication", url)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildJobLogURL(jobID string) string {
|
func buildJobLogURL(jobID string, jobType string) string {
|
||||||
url := config.InternalJobServiceURL()
|
url := config.InternalJobServiceURL()
|
||||||
return fmt.Sprintf("%s/api/jobs/replication/%s/log", url, jobID)
|
return fmt.Sprintf("%s/api/jobs/%s/%s/log", url, jobType, jobID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildReplicationActionURL() string {
|
func buildReplicationActionURL() string {
|
||||||
|
@ -99,6 +99,7 @@ func initRouters() {
|
|||||||
beego.Router("/api/jobs/replication/", &api.RepJobAPI{}, "get:List")
|
beego.Router("/api/jobs/replication/", &api.RepJobAPI{}, "get:List")
|
||||||
beego.Router("/api/jobs/replication/:id([0-9]+)", &api.RepJobAPI{})
|
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/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/policies/replication/:id([0-9]+)", &api.RepPolicyAPI{})
|
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{}, "get:List")
|
||||||
beego.Router("/api/policies/replication", &api.RepPolicyAPI{}, "post:Post")
|
beego.Router("/api/policies/replication", &api.RepPolicyAPI{}, "post:Post")
|
||||||
|
90
src/ui/utils/response_handlers.go
Normal file
90
src/ui/utils/response_handlers.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
// 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 utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/vmware/harbor/src/common/api"
|
||||||
|
"github.com/vmware/harbor/src/common/utils/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
//ResponseHandler provides utility to handle http response.
|
||||||
|
type ResponseHandler interface {
|
||||||
|
Handle(*http.Response) error
|
||||||
|
}
|
||||||
|
|
||||||
|
//StatusRespHandler handles the response to check if the status is expected, if not returns an error.
|
||||||
|
type StatusRespHandler struct {
|
||||||
|
status int
|
||||||
|
}
|
||||||
|
|
||||||
|
//Handle ...
|
||||||
|
func (s StatusRespHandler) Handle(resp *http.Response) error {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != s.status {
|
||||||
|
b, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fmt.Errorf("Unexpected status code: %d, text: %s", resp.StatusCode, string(b))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStatusRespHandler ...
|
||||||
|
func NewStatusRespHandler(sc int) ResponseHandler {
|
||||||
|
return StatusRespHandler{
|
||||||
|
status: sc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//JobLogRespHandler handles the response from jobservice to show the log of a job
|
||||||
|
type JobLogRespHandler struct {
|
||||||
|
theAPI *api.BaseAPI
|
||||||
|
}
|
||||||
|
|
||||||
|
//Handle will consume the response of job service and put the content of the job log in the reponse of the API.
|
||||||
|
func (h JobLogRespHandler) Handle(resp *http.Response) error {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
h.theAPI.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Length"), resp.Header.Get(http.CanonicalHeaderKey("Content-Length")))
|
||||||
|
h.theAPI.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Type"), "text/plain")
|
||||||
|
|
||||||
|
if _, err := io.Copy(h.theAPI.Ctx.ResponseWriter, resp.Body); err != nil {
|
||||||
|
log.Errorf("failed to write log to response; %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to read reponse body: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.theAPI.RenderError(resp.StatusCode, fmt.Sprintf("message from jobservice: %s", string(b)))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewJobLogRespHandler ...
|
||||||
|
func NewJobLogRespHandler(apiHandler *api.BaseAPI) ResponseHandler {
|
||||||
|
return &JobLogRespHandler{
|
||||||
|
theAPI: apiHandler,
|
||||||
|
}
|
||||||
|
}
|
40
src/ui/utils/response_handlers_test.go
Normal file
40
src/ui/utils/response_handlers_test.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// 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 utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStatusRespHandler(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
h := NewStatusRespHandler(http.StatusCreated)
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
recorder.WriteHeader(http.StatusCreated)
|
||||||
|
recorder.WriteString("test passed")
|
||||||
|
resp1 := recorder.Result()
|
||||||
|
err := h.Handle(resp1)
|
||||||
|
assert.Nil(err)
|
||||||
|
recorder2 := httptest.NewRecorder()
|
||||||
|
recorder2.WriteHeader(http.StatusForbidden)
|
||||||
|
recorder2.WriteString("test forbidden")
|
||||||
|
resp2 := recorder2.Result()
|
||||||
|
err = h.Handle(resp2)
|
||||||
|
assert.NotNil(err)
|
||||||
|
assert.Contains(err.Error(), "forbidden")
|
||||||
|
}
|
@ -27,7 +27,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -74,8 +73,7 @@ func ScanAllImages() error {
|
|||||||
|
|
||||||
// RequestAsUI is a shortcut to make a request attach UI secret and send the request.
|
// RequestAsUI is a shortcut to make a request attach UI secret and send the request.
|
||||||
// Do not use this when you want to handle the response
|
// Do not use this when you want to handle the response
|
||||||
// TODO: add a response handler to replace expectSC *when needed*
|
func RequestAsUI(method, url string, body io.Reader, h ResponseHandler) error {
|
||||||
func RequestAsUI(method, url string, body io.Reader, expectSC int) error {
|
|
||||||
req, err := http.NewRequest(method, url, body)
|
req, err := http.NewRequest(method, url, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -87,16 +85,7 @@ func RequestAsUI(method, url string, body io.Reader, expectSC int) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
return h.Handle(resp)
|
||||||
|
|
||||||
if resp.StatusCode != expectSC {
|
|
||||||
b, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return fmt.Errorf("Unexpected status code: %d, text: %s", resp.StatusCode, string(b))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//AddUISecret add secret cookie to a request
|
//AddUISecret add secret cookie to a request
|
||||||
@ -120,7 +109,7 @@ func TriggerImageScan(repository string, tag string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
url := fmt.Sprintf("%s/api/jobs/scan", config.InternalJobServiceURL())
|
url := fmt.Sprintf("%s/api/jobs/scan", config.InternalJobServiceURL())
|
||||||
return RequestAsUI("POST", url, bytes.NewBuffer(b), http.StatusOK)
|
return RequestAsUI("POST", url, bytes.NewBuffer(b), NewStatusRespHandler(http.StatusOK))
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRepositoryClientForUI ...
|
// NewRepositoryClientForUI ...
|
||||||
|
Loading…
Reference in New Issue
Block a user